diff --git a/.config/cypress-devcontainer.yml b/.config/cypress-devcontainer.yml
new file mode 100644
index 0000000000..3907615f73
--- /dev/null
+++ b/.config/cypress-devcontainer.yml
@@ -0,0 +1,224 @@
+#━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+# Misskey configuration
+#━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+#   ┌────────────────────────┐
+#───┘ Initial Setup Password └─────────────────────────────────────────────────────
+
+# Password to initiate setting up admin account.
+# It will not be used after the initial setup is complete.
+#
+# Be sure to change this when you set up Misskey via the Internet.
+#
+# The provider of the service who sets up Misskey on behalf of the customer should
+# set this value to something unique when generating the Misskey config file,
+# and provide it to the customer.
+setupPassword: example_password_please_change_this_or_you_will_get_hacked
+
+#   ┌─────┐
+#───┘ URL └─────────────────────────────────────────────────────
+
+# Final accessible URL seen by a user.
+url: 'http://misskey.local'
+
+# ONCE YOU HAVE STARTED THE INSTANCE, DO NOT CHANGE THE
+# URL SETTINGS AFTER THAT!
+
+#   ┌───────────────────────┐
+#───┘ Port and TLS settings └───────────────────────────────────
+
+#
+# Misskey requires a reverse proxy to support HTTPS connections.
+#
+#                 +----- https://example.tld/ ------------+
+#   +------+      |+-------------+      +----------------+|
+#   | User | ---> || Proxy (443) | ---> | Misskey (3000) ||
+#   +------+      |+-------------+      +----------------+|
+#                 +---------------------------------------+
+#
+#   You need to set up a reverse proxy. (e.g. nginx)
+#   An encrypted connection with HTTPS is highly recommended
+#   because tokens may be transferred in GET requests.
+
+# The port that your Misskey server should listen on.
+port: 61812
+
+#   ┌──────────────────────────┐
+#───┘ PostgreSQL configuration └────────────────────────────────
+
+db:
+  host: db
+  port: 5432
+
+  # Database name
+  db: misskey
+
+  # Auth
+  user: postgres
+  pass: postgres
+
+  # Whether disable Caching queries
+  #disableCache: true
+
+  # Extra Connection options
+  #extra:
+  #  ssl: true
+
+dbReplications: false
+
+# You can configure any number of replicas here
+#dbSlaves:
+#  -
+#    host:
+#    port:
+#    db:
+#    user:
+#    pass:
+#  -
+#    host:
+#    port:
+#    db:
+#    user:
+#    pass:
+
+#   ┌─────────────────────┐
+#───┘ Redis configuration └─────────────────────────────────────
+
+redis:
+  host: redis
+  port: 6379
+  #family: 0  # 0=Both, 4=IPv4, 6=IPv6
+  #pass: example-pass
+  #prefix: example-prefix
+  #db: 1
+
+#redisForPubsub:
+#  host: redis
+#  port: 6379
+#  #family: 0  # 0=Both, 4=IPv4, 6=IPv6
+#  #pass: example-pass
+#  #prefix: example-prefix
+#  #db: 1
+
+#redisForJobQueue:
+#  host: redis
+#  port: 6379
+#  #family: 0  # 0=Both, 4=IPv4, 6=IPv6
+#  #pass: example-pass
+#  #prefix: example-prefix
+#  #db: 1
+
+#redisForTimelines:
+#  host: redis
+#  port: 6379
+#  #family: 0  # 0=Both, 4=IPv4, 6=IPv6
+#  #pass: example-pass
+#  #prefix: example-prefix
+#  #db: 1
+
+#redisForReactions:
+#  host: redis
+#  port: 6379
+#  #family: 0  # 0=Both, 4=IPv4, 6=IPv6
+#  #pass: example-pass
+#  #prefix: example-prefix
+#  #db: 1
+
+#   ┌───────────────────────────┐
+#───┘ MeiliSearch configuration └─────────────────────────────
+
+#meilisearch:
+#  host: meilisearch
+#  port: 7700
+#  apiKey: ''
+#  ssl: true
+#  index: ''
+
+#   ┌───────────────┐
+#───┘ ID generation └───────────────────────────────────────────
+
+# You can select the ID generation method.
+# You don't usually need to change this setting, but you can
+# change it according to your preferences.
+
+# Available methods:
+# aid ... Short, Millisecond accuracy
+# aidx ... Millisecond accuracy
+# meid ... Similar to ObjectID, Millisecond accuracy
+# ulid ... Millisecond accuracy
+# objectid ... This is left for backward compatibility
+
+# ONCE YOU HAVE STARTED THE INSTANCE, DO NOT CHANGE THE
+# ID SETTINGS AFTER THAT!
+
+id: 'aidx'
+
+#   ┌────────────────┐
+#───┘ Error tracking └──────────────────────────────────────────
+
+# Sentry is available for error tracking.
+# See the Sentry documentation for more details on options.
+
+#sentryForBackend:
+#  enableNodeProfiling: true
+#  options:
+#    dsn: 'https://examplePublicKey@o0.ingest.sentry.io/0'
+
+#sentryForFrontend:
+#  options:
+#    dsn: 'https://examplePublicKey@o0.ingest.sentry.io/0'
+
+#   ┌─────────────────────┐
+#───┘ Other configuration └─────────────────────────────────────
+
+# Whether disable HSTS
+#disableHsts: true
+
+# Number of worker processes
+#clusterLimit: 1
+
+# Job concurrency per worker
+# deliverJobConcurrency: 128
+# inboxJobConcurrency: 16
+
+# Job rate limiter
+# deliverJobPerSec: 128
+# inboxJobPerSec: 32
+
+# Job attempts
+# deliverJobMaxAttempts: 12
+# inboxJobMaxAttempts: 8
+
+# IP address family used for outgoing request (ipv4, ipv6 or dual)
+#outgoingAddressFamily: ipv4
+
+# Proxy for HTTP/HTTPS
+#proxy: http://127.0.0.1:3128
+
+proxyBypassHosts:
+  - api.deepl.com
+  - api-free.deepl.com
+  - www.recaptcha.net
+  - hcaptcha.com
+  - challenges.cloudflare.com
+
+# Proxy for SMTP/SMTPS
+#proxySmtp: http://127.0.0.1:3128   # use HTTP/1.1 CONNECT
+#proxySmtp: socks4://127.0.0.1:1080 # use SOCKS4
+#proxySmtp: socks5://127.0.0.1:1080 # use SOCKS5
+
+# Media Proxy
+#mediaProxy: https://example.com/proxy
+
+# Proxy remote files (default: true)
+proxyRemoteFiles: true
+
+# Sign to ActivityPub GET request (default: true)
+signToActivityPubGet: true
+
+allowedPrivateNetworks: [
+  '127.0.0.1/32'
+]
+
+# Upload or download file size limits (bytes)
+#maxFileSize: 262144000
diff --git a/.config/docker_example.env b/.config/docker_example.env
index 4fe8e76b78..c61248da2e 100644
--- a/.config/docker_example.env
+++ b/.config/docker_example.env
@@ -1,5 +1,11 @@
+# misskey settings
+# MISSKEY_URL=https://example.tld/
+
 # db settings
 POSTGRES_PASSWORD=example-misskey-pass
+# DATABASE_PASSWORD=${POSTGRES_PASSWORD}
 POSTGRES_USER=example-misskey-user
+# DATABASE_USER=${POSTGRES_USER}
 POSTGRES_DB=misskey
+# DATABASE_DB=${POSTGRES_DB}
 DATABASE_URL="postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}"
diff --git a/.config/docker_example.yml b/.config/docker_example.yml
index 42ac18de1b..3f8e5734ce 100644
--- a/.config/docker_example.yml
+++ b/.config/docker_example.yml
@@ -6,6 +6,7 @@
 #───┘ URL └─────────────────────────────────────────────────────
 
 # Final accessible URL seen by a user.
+# You can set url from an environment variable instead.
 url: https://example.tld/
 
 # ONCE YOU HAVE STARTED THE INSTANCE, DO NOT CHANGE THE
@@ -38,9 +39,11 @@ db:
   port: 5432
 
   # Database name
+  # You can set db from an environment variable instead.
   db: misskey
 
   # Auth
+  # You can set user and pass from environment variables instead.
   user: example-misskey-user
   pass: example-misskey-pass
 
@@ -103,6 +106,14 @@ redis:
 #  #prefix: example-prefix
 #  #db: 1
 
+#redisForReactions:
+#  host: redis
+#  port: 6379
+#  #family: 0  # 0=Both, 4=IPv4, 6=IPv6
+#  #pass: example-pass
+#  #prefix: example-prefix
+#  #db: 1
+
 #   ┌───────────────────────────┐
 #───┘ MeiliSearch configuration └─────────────────────────────
 
diff --git a/.config/example.yml b/.config/example.yml
index b11cbd1373..60a6a0aa71 100644
--- a/.config/example.yml
+++ b/.config/example.yml
@@ -59,6 +59,20 @@
 #
 # publishTarballInsteadOfProvideRepositoryUrl: true
 
+#   ┌────────────────────────┐
+#───┘ Initial Setup Password └─────────────────────────────────────────────────────
+
+# Password to initiate setting up admin account.
+# It will not be used after the initial setup is complete.
+#
+# Be sure to change this when you set up Misskey via the Internet.
+#
+# The provider of the service who sets up Misskey on behalf of the customer should
+# set this value to something unique when generating the Misskey config file,
+# and provide it to the customer.
+#
+# setupPassword: example_password_please_change_this_or_you_will_get_hacked
+
 #   ┌─────┐
 #───┘ URL └─────────────────────────────────────────────────────
 
@@ -172,6 +186,16 @@ redis:
 #  # You can specify more ioredis options...
 #  #username: example-username
 
+#redisForReactions:
+#  host: localhost
+#  port: 6379
+#  #family: 0  # 0=Both, 4=IPv4, 6=IPv6
+#  #pass: example-pass
+#  #prefix: example-prefix
+#  #db: 1
+#  # You can specify more ioredis options...
+#  #username: example-username
+
 #   ┌───────────────────────────┐
 #───┘ MeiliSearch configuration └─────────────────────────────
 
diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/compose.yml
similarity index 93%
rename from .devcontainer/docker-compose.yml
rename to .devcontainer/compose.yml
index 2809cd2ca4..d02d2a8f4a 100644
--- a/.devcontainer/docker-compose.yml
+++ b/.devcontainer/compose.yml
@@ -1,5 +1,3 @@
-version: '3.8'
-
 services:
   app:
     build:
@@ -8,6 +6,7 @@ services:
 
     volumes:
       - ../:/workspace:cached
+      - node_modules:/workspace/node_modules
 
     command: sleep infinity
 
@@ -46,6 +45,7 @@ services:
 volumes:
   postgres-data:
   redis-data:
+  node_modules:
 
 networks:
   internal_network:
diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
index 31b6212cb5..fbf959d449 100644
--- a/.devcontainer/devcontainer.json
+++ b/.devcontainer/devcontainer.json
@@ -1,16 +1,16 @@
 {
 	"name": "Misskey",
-	"dockerComposeFile": "docker-compose.yml",
+	"dockerComposeFile": "compose.yml",
 	"service": "app",
 	"workspaceFolder": "/workspace",
 	"features": {
 		"ghcr.io/devcontainers/features/node:1": {
-			"version": "20.12.2"
+			"version": "20.16.0"
 		},
 		"ghcr.io/devcontainers-contrib/features/corepack:1": {}
 	},
 	"forwardPorts": [3000],
-	"postCreateCommand": "sudo chmod 755 .devcontainer/init.sh && .devcontainer/init.sh",
+	"postCreateCommand": "/bin/bash .devcontainer/init.sh",
 	"customizations": {
 		"vscode": {
 			"extensions": [
diff --git a/.devcontainer/devcontainer.yml b/.devcontainer/devcontainer.yml
index beefcfd0a2..3eb4fc2879 100644
--- a/.devcontainer/devcontainer.yml
+++ b/.devcontainer/devcontainer.yml
@@ -103,6 +103,14 @@ redis:
 #  #prefix: example-prefix
 #  #db: 1
 
+#redisForReactions:
+#  host: redis
+#  port: 6379
+#  #family: 0  # 0=Both, 4=IPv4, 6=IPv6
+#  #pass: example-pass
+#  #prefix: example-prefix
+#  #db: 1
+
 #   ┌───────────────────────────┐
 #───┘ MeiliSearch configuration └─────────────────────────────
 
diff --git a/.devcontainer/init.sh b/.devcontainer/init.sh
index 729e1a9d2d..e02a533c15 100755
--- a/.devcontainer/init.sh
+++ b/.devcontainer/init.sh
@@ -2,7 +2,10 @@
 
 set -xe
 
-sudo chown -R node /workspace
+sudo chown node node_modules
+sudo apt-get update
+sudo apt-get -y install libgtk2.0-0 libgtk-3-0 libgbm-dev libnotify-dev libnss3 libxss1 libasound2 libxtst6 xauth xvfb
+git config --global --add safe.directory /workspace
 git submodule update --init
 corepack install
 corepack enable
@@ -11,3 +14,4 @@ pnpm install --frozen-lockfile
 cp .devcontainer/devcontainer.yml .config/default.yml
 pnpm build
 pnpm migrate
+pnpm exec cypress install
diff --git a/.dockerignore b/.dockerignore
index 1de0c7982b..f204349160 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -7,12 +7,11 @@ Dockerfile
 build/
 built/
 db/
-docker-compose.yml
+.devcontainer/compose.yml
 node_modules/
 packages/*/node_modules
 redis/
 files/
-misskey-assets/
 fluent-emojis/
 .pnp.*
 
@@ -28,4 +27,4 @@ fluent-emojis/
 
 .idea/
 packages/*/.vscode/
-packages/backend/test/docker-compose.yml
+packages/backend/test/compose.yml
diff --git a/.github/ISSUE_TEMPLATE/01_bug-report.yml b/.github/ISSUE_TEMPLATE/01_bug-report.yml
index ac2b39cc12..315e712c30 100644
--- a/.github/ISSUE_TEMPLATE/01_bug-report.yml
+++ b/.github/ISSUE_TEMPLATE/01_bug-report.yml
@@ -53,8 +53,8 @@ body:
         Examples:
           * Model and OS of the device(s): MacBook Pro (14inch, 2021), macOS Ventura 13.4
           * Browser: Chrome 113.0.5672.126
-          * Server URL: misskey.io
-          * Misskey: 13.x.x
+          * Server URL: misskey.example.com
+          * Misskey: 2024.x.x
       value: |
         * Model and OS of the device(s):
         * Browser:
@@ -74,11 +74,11 @@ body:
 
         Examples:
           * Installation Method or Hosting Service: docker compose, k8s/docker, systemd, "Misskey install shell script", development environment
-          * Misskey: 13.x.x
+          * Misskey: 2024.x.x
           * Node: 20.x.x
           * PostgreSQL: 15.x.x
           * Redis: 7.x.x
-          * OS and Architecture: Ubuntu 22.04.2 LTS aarch64
+          * OS and Architecture: Ubuntu 24.04.2 LTS aarch64
       value: |
         * Installation Method or Hosting Service:
         * Misskey:
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
index e8b65dc3b9..5acad83336 100644
--- a/.github/ISSUE_TEMPLATE/config.yml
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -2,3 +2,7 @@ contact_links:
   - name: 💬 Misskey official Discord
     url: https://discord.gg/Wp8gVStHW3
     about: Chat freely about Misskey
+  # 仮
+  - name: 💬 Start discussion
+    url: https://github.com/misskey-dev/misskey/discussions
+    about: The official forum to join conversation and ask question
diff --git a/.github/labeler.yml b/.github/labeler.yml
index a77f73706b..b64d726d65 100644
--- a/.github/labeler.yml
+++ b/.github/labeler.yml
@@ -6,7 +6,7 @@
 'packages/backend:test':
 - any:
   - changed-files:
-    - any-glob-to-any-file: ['packages/backend/test/**/*']
+    - any-glob-to-any-file: ['packages/backend/test/**/*', 'packages/backend/test-federation/**/*']
 
 'packages/frontend':
 - any:
diff --git a/.github/misskey/test.yml b/.github/misskey/test.yml
index 7a4aa4ae6c..3c807e8b9e 100644
--- a/.github/misskey/test.yml
+++ b/.github/misskey/test.yml
@@ -1,5 +1,7 @@
 url: 'http://misskey.local'
 
+setupPassword: example_password_please_change_this_or_you_will_get_hacked
+
 # ローカルでテストするときにポートを被らないようにするためデフォルトのものとは変える(以下同じ)
 port: 61812
 
diff --git a/.github/workflows/api-misskey-js.yml b/.github/workflows/api-misskey-js.yml
index 1b7b68b14f..8380a3bb23 100644
--- a/.github/workflows/api-misskey-js.yml
+++ b/.github/workflows/api-misskey-js.yml
@@ -4,10 +4,11 @@ on:
   push:
     paths:
       - packages/misskey-js/**
+      - .github/workflows/api-misskey-js.yml
   pull_request:
     paths:
       - packages/misskey-js/**
-
+      - .github/workflows/api-misskey-js.yml
 jobs:
   report:
 
@@ -20,7 +21,7 @@ jobs:
       - run: corepack enable
 
       - name: Setup Node.js
-        uses: actions/setup-node@v4.0.2
+        uses: actions/setup-node@v4.0.4
         with:
           node-version-file: '.node-version'
           cache: 'pnpm'
diff --git a/.github/workflows/changelog-check.yml b/.github/workflows/changelog-check.yml
index f254af0d1f..44cc1a04f2 100644
--- a/.github/workflows/changelog-check.yml
+++ b/.github/workflows/changelog-check.yml
@@ -14,7 +14,7 @@ jobs:
       - name: Checkout head
         uses: actions/checkout@v4.1.1
       - name: Setup Node.js
-        uses: actions/setup-node@v4.0.2
+        uses: actions/setup-node@v4.0.4
         with:
           node-version-file: '.node-version'
 
diff --git a/.github/workflows/check-misskey-js-autogen.yml b/.github/workflows/check-misskey-js-autogen.yml
index 39acad8bc3..f26c9a4d45 100644
--- a/.github/workflows/check-misskey-js-autogen.yml
+++ b/.github/workflows/check-misskey-js-autogen.yml
@@ -21,6 +21,7 @@ jobs:
         uses: actions/checkout@v4.1.1
         with:
           submodules: true
+          persist-credentials: false
           ref: refs/pull/${{ github.event.pull_request.number }}/merge
 
       - name: setup pnpm
@@ -28,7 +29,7 @@ jobs:
 
       - name: setup node
         id: setup-node
-        uses: actions/setup-node@v4.0.2
+        uses: actions/setup-node@v4.0.4
         with:
           node-version-file: '.node-version'
           cache: pnpm
@@ -57,7 +58,7 @@ jobs:
           name: generated-misskey-js
           path: packages/misskey-js/generator/built/autogen
 
-  # pull_request_target safety: permissions: read-all, and there are no secrets used in this job
+  # pull_request_target safety: permissions: read-all, and no user codes are executed
   get-actual-misskey-js:
     runs-on: ubuntu-latest
     permissions:
@@ -68,6 +69,7 @@ jobs:
         uses: actions/checkout@v4.1.1
         with:
           submodules: true
+          persist-credentials: false
           ref: refs/pull/${{ github.event.pull_request.number }}/merge
 
       - name: Upload From Merged
@@ -131,3 +133,7 @@ jobs:
           mode: delete
           message: "Thank you!"
           create_if_not_exists: false
+
+      - name: Make failure if changes are detected
+        if: steps.check-changes.outputs.changes == 'true'
+        run: exit 1
diff --git a/.github/workflows/check-misskey-js-version.yml b/.github/workflows/check-misskey-js-version.yml
index 325a893605..99c29ac974 100644
--- a/.github/workflows/check-misskey-js-version.yml
+++ b/.github/workflows/check-misskey-js-version.yml
@@ -6,12 +6,13 @@ on:
     paths:
       - packages/misskey-js/package.json
       - package.json
+      - .github/workflows/check-misskey-js-version.yml
   pull_request:
     branches: [ develop ]
     paths:
       - packages/misskey-js/package.json
       - package.json
-
+      - .github/workflows/check-misskey-js-version.yml
 jobs:
   check-version:
     # ルートの package.json と packages/misskey-js/package.json のバージョンが一致しているかを確認する
diff --git a/.github/workflows/check-spdx-license-id.yml b/.github/workflows/check-spdx-license-id.yml
index 6cd8bf60d5..05582008b5 100644
--- a/.github/workflows/check-spdx-license-id.yml
+++ b/.github/workflows/check-spdx-license-id.yml
@@ -48,12 +48,16 @@ jobs:
             "packages/backend/migration"
             "packages/backend/src"
             "packages/backend/test"
+            "packages/frontend-shared/@types"
+            "packages/frontend-shared/js"
             "packages/frontend/.storybook"
             "packages/frontend/@types"
             "packages/frontend/lib"
             "packages/frontend/public"
             "packages/frontend/src"
             "packages/frontend/test"
+            "packages/frontend-embed/@types"
+            "packages/frontend-embed/src"
             "packages/misskey-bubble-game/src"
             "packages/misskey-reversi/src"
             "packages/sw/src"
diff --git a/.github/workflows/docker-develop.yml b/.github/workflows/docker-develop.yml
index cb84849580..ac2b1b4d35 100644
--- a/.github/workflows/docker-develop.yml
+++ b/.github/workflows/docker-develop.yml
@@ -37,7 +37,7 @@ jobs:
           password: ${{ secrets.DOCKER_PASSWORD }}
       - name: Build and push by digest
         id: build
-        uses: docker/build-push-action@v5
+        uses: docker/build-push-action@v6
         with:
           context: .
           push: true
diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml
index 23c1bdbc16..db899ba386 100644
--- a/.github/workflows/docker.yml
+++ b/.github/workflows/docker.yml
@@ -48,7 +48,7 @@ jobs:
           password: ${{ secrets.DOCKER_PASSWORD }}
       - name: Build and Push to Docker Hub
         id: build
-        uses: docker/build-push-action@v5
+        uses: docker/build-push-action@v6
         with:
           context: .
           push: true
diff --git a/.github/workflows/dockle.yml b/.github/workflows/dockle.yml
index eee7a78fed..c3dba4213d 100644
--- a/.github/workflows/dockle.yml
+++ b/.github/workflows/dockle.yml
@@ -13,14 +13,16 @@ jobs:
     runs-on: ubuntu-latest
     env:
       DOCKER_CONTENT_TRUST: 1
+      DOCKLE_VERSION: 0.4.14
     steps:
       - uses: actions/checkout@v4.1.1
-      - run: |
-          curl -L -o dockle.deb "https://github.com/goodwithtech/dockle/releases/download/v0.4.10/dockle_0.4.10_Linux-64bit.deb"
+      - name: Download and install dockle v${{ env.DOCKLE_VERSION }}
+        run: |
+          curl -L -o dockle.deb "https://github.com/goodwithtech/dockle/releases/download/v${DOCKLE_VERSION}/dockle_${DOCKLE_VERSION}_Linux-64bit.deb"
           sudo dpkg -i dockle.deb
       - run: |
           cp .config/docker_example.env .config/docker.env
-          cp ./docker-compose_example.yml ./docker-compose.yml
+          cp ./compose_example.yml ./compose.yml
       - run: |
           docker compose up -d web
           docker tag "$(docker compose images web | awk 'OFS=":" {print $4}' | tail -n +2)" misskey-web:latest
diff --git a/.github/workflows/get-api-diff.yml b/.github/workflows/get-api-diff.yml
index 9b9c8f11c4..1bcaa0d9c4 100644
--- a/.github/workflows/get-api-diff.yml
+++ b/.github/workflows/get-api-diff.yml
@@ -9,7 +9,6 @@ on:
     paths:
       - packages/backend/**
       - .github/workflows/get-api-diff.yml
-
 jobs:
   get-from-misskey:
     runs-on: ubuntu-latest
@@ -18,7 +17,7 @@ jobs:
 
     strategy:
       matrix:
-        node-version: [20.12.2]
+        node-version: [20.16.0]
         api-json-name: [api-base.json, api-head.json]
         include:
           - api-json-name: api-base.json
@@ -34,7 +33,7 @@ jobs:
     - name: Install pnpm
       uses: pnpm/action-setup@v4
     - name: Use Node.js ${{ matrix.node-version }}
-      uses: actions/setup-node@v4.0.2
+      uses: actions/setup-node@v4.0.4
       with:
         node-version: ${{ matrix.node-version }}
         cache: 'pnpm'
diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
index 76616ec5a7..90eb268dda 100644
--- a/.github/workflows/lint.yml
+++ b/.github/workflows/lint.yml
@@ -8,17 +8,26 @@ on:
     paths:
       - packages/backend/**
       - packages/frontend/**
+      - packages/frontend-shared/**
+      - packages/frontend-embed/**
       - packages/sw/**
       - packages/misskey-js/**
-      - packages/shared/.eslintrc.js
+      - packages/misskey-bubble-game/**
+      - packages/misskey-reversi/**
+      - packages/shared/eslint.config.js
+      - .github/workflows/lint.yml
   pull_request:
     paths:
       - packages/backend/**
       - packages/frontend/**
+      - packages/frontend-shared/**
+      - packages/frontend-embed/**
       - packages/sw/**
       - packages/misskey-js/**
-      - packages/shared/.eslintrc.js
-
+      - packages/misskey-bubble-game/**
+      - packages/misskey-reversi/**
+      - packages/shared/eslint.config.js
+      - .github/workflows/lint.yml
 jobs:
   pnpm_install:
     runs-on: ubuntu-latest
@@ -28,7 +37,7 @@ jobs:
         fetch-depth: 0
         submodules: true
     - uses: pnpm/action-setup@v4
-    - uses: actions/setup-node@v4.0.2
+    - uses: actions/setup-node@v4.0.4
       with:
         node-version-file: '.node-version'
         cache: 'pnpm'
@@ -44,21 +53,34 @@ jobs:
         workspace:
         - backend
         - frontend
+        - frontend-shared
+        - frontend-embed
         - sw
         - misskey-js
+        - misskey-bubble-game
+        - misskey-reversi
+    env:
+      eslint-cache-version: v1
+      eslint-cache-path: ${{ github.workspace }}/node_modules/.cache/eslint-${{ matrix.workspace }}
     steps:
     - uses: actions/checkout@v4.1.1
       with:
         fetch-depth: 0
         submodules: true
     - uses: pnpm/action-setup@v4
-    - uses: actions/setup-node@v4.0.2
+    - uses: actions/setup-node@v4.0.4
       with:
         node-version-file: '.node-version'
         cache: 'pnpm'
     - run: corepack enable
     - run: pnpm i --frozen-lockfile
-    - run: pnpm --filter ${{ matrix.workspace }} run eslint
+    - name: Restore eslint cache
+      uses: actions/cache@v4.1.0
+      with:
+        path: ${{ env.eslint-cache-path }}
+        key: eslint-${{ env.eslint-cache-version }}-${{ matrix.workspace }}-${{ hashFiles('**/pnpm-lock.yaml') }}-${{ github.ref_name }}-${{ github.sha }}
+        restore-keys: eslint-${{ env.eslint-cache-version }}-${{ matrix.workspace }}-${{ hashFiles('**/pnpm-lock.yaml') }}-
+    - run: pnpm --filter ${{ matrix.workspace }} run eslint --cache --cache-location ${{ env.eslint-cache-path }} --cache-strategy content
 
   typecheck:
     needs: [pnpm_install]
@@ -68,6 +90,7 @@ jobs:
       matrix:
         workspace:
         - backend
+        - sw
         - misskey-js
     steps:
     - uses: actions/checkout@v4.1.1
@@ -75,14 +98,14 @@ jobs:
         fetch-depth: 0
         submodules: true
     - uses: pnpm/action-setup@v4
-    - uses: actions/setup-node@v4.0.2
+    - uses: actions/setup-node@v4.0.4
       with:
         node-version-file: '.node-version'
         cache: 'pnpm'
     - run: corepack enable
     - run: pnpm i --frozen-lockfile
     - run: pnpm --filter misskey-js run build
-      if: ${{ matrix.workspace == 'backend' }}
+      if: ${{ matrix.workspace == 'backend' || matrix.workspace == 'sw' }}
     - run: pnpm --filter misskey-reversi run build
       if: ${{ matrix.workspace == 'backend' }}
     - run: pnpm --filter ${{ matrix.workspace }} run typecheck
diff --git a/.github/workflows/locale.yml b/.github/workflows/locale.yml
index de2247e772..6bc8860a11 100644
--- a/.github/workflows/locale.yml
+++ b/.github/workflows/locale.yml
@@ -4,10 +4,11 @@ on:
   push:
     paths:
       - locales/**
+      - .github/workflows/locale.yml
   pull_request:
     paths:
       - locales/**
-
+      - .github/workflows/locale.yml
 jobs:
   locale_verify:
     runs-on: ubuntu-latest
@@ -18,7 +19,7 @@ jobs:
         fetch-depth: 0
         submodules: true
     - uses: pnpm/action-setup@v4
-    - uses: actions/setup-node@v4.0.2
+    - uses: actions/setup-node@v4.0.4
       with:
         node-version-file: '.node-version'
         cache: 'pnpm'
diff --git a/.github/workflows/on-release-created.yml b/.github/workflows/on-release-created.yml
index edfdab99e9..ffaf7bc038 100644
--- a/.github/workflows/on-release-created.yml
+++ b/.github/workflows/on-release-created.yml
@@ -17,7 +17,7 @@ jobs:
 
     strategy:
       matrix:
-        node-version: [20.12.2]
+        node-version: [20.16.0]
 
     steps:
       - uses: actions/checkout@v4.1.1
@@ -26,7 +26,7 @@ jobs:
       - name: Install pnpm
         uses: pnpm/action-setup@v4
       - name: Use Node.js ${{ matrix.node-version }}
-        uses: actions/setup-node@v4.0.2
+        uses: actions/setup-node@v4.0.4
         with:
           node-version: ${{ matrix.node-version }}
           cache: 'pnpm'
diff --git a/.github/workflows/release-edit-with-push.yml b/.github/workflows/release-edit-with-push.yml
index 86ee0b3fb5..57657a4ba7 100644
--- a/.github/workflows/release-edit-with-push.yml
+++ b/.github/workflows/release-edit-with-push.yml
@@ -3,7 +3,7 @@ name: "Release Manager: sync changelog with PR"
 on:
   push:
     branches:
-      - release/**
+      - develop
     paths:
       - 'CHANGELOG.md'
 
@@ -20,24 +20,29 @@ jobs:
     runs-on: ubuntu-latest
     steps:
       - uses: actions/checkout@v4
-      # headがrelease/かつopenのPRを1つ取得
+      # headが$GITHUB_REF_NAME, baseが$STABLE_BRANCHかつopenのPRを1つ取得
       - name: Get PR
         run: |
-          echo "pr_number=$(gh pr list --limit 1 --head "$GITHUB_REF_NAME" --json number --jq '.[] | .number')" >> $GITHUB_OUTPUT
+          echo "pr_number=$(gh pr list --limit 1 --search "head:$GITHUB_REF_NAME base:$STABLE_BRANCH is:open" --json number  --jq '.[] | .number')" >> $GITHUB_OUTPUT
         id: get_pr
+        env:
+          STABLE_BRANCH: ${{ vars.STABLE_BRANCH }}
       - name: Get target version
-        uses: misskey-dev/release-manager-actions/.github/actions/get-target-version@v1
+        if: steps.get_pr.outputs.pr_number != ''
+        uses: misskey-dev/release-manager-actions/.github/actions/get-target-version@v2
         id: v
       # CHANGELOG.mdの内容を取得
       - name: Get changelog
-        uses: misskey-dev/release-manager-actions/.github/actions/get-changelog@v1
+        if: steps.get_pr.outputs.pr_number != ''
+        uses: misskey-dev/release-manager-actions/.github/actions/get-changelog@v2
         with:
           version: ${{ steps.v.outputs.target_version }}
         id: changelog
       # PRのnotesを更新
       - name: Update PR
+        if: steps.get_pr.outputs.pr_number != ''
         run: |
           gh pr edit "$PR_NUMBER" --body "$CHANGELOG"
         env:
-          CHANGELOG: ${{ steps.changelog.outputs.changelog }}
           PR_NUMBER: ${{ steps.get_pr.outputs.pr_number }}
+          CHANGELOG: ${{ steps.changelog.outputs.changelog }}
diff --git a/.github/workflows/release-with-dispatch.yml b/.github/workflows/release-with-dispatch.yml
index bc6448cb37..ed2f822269 100644
--- a/.github/workflows/release-with-dispatch.yml
+++ b/.github/workflows/release-with-dispatch.yml
@@ -17,6 +17,10 @@ on:
         type: boolean
         description: 'MERGE RELEASE BRANCH TO MAIN'
         default: false
+      start-rc:
+        type: boolean
+        description: 'Start Release Candidate'
+        default: false
 
 env:
   GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -33,18 +37,21 @@ jobs:
       pr_number: ${{ steps.get_pr.outputs.pr_number }}
     steps:
       - uses: actions/checkout@v4
-      # headがrelease/かつopenのPRを1つ取得
+      # headが$GITHUB_REF_NAME, baseが$STABLE_BRANCHかつopenのPRを1つ取得
       - name: Get PRs
         run: |
-          echo "pr_number=$(gh pr list --limit 1 --search "head:release/ is:open" --json number  --jq '.[] | .number')" >> $GITHUB_OUTPUT
+          echo "pr_number=$(gh pr list --limit 1 --search "head:$GITHUB_REF_NAME base:$STABLE_BRANCH is:open" --json number  --jq '.[] | .number')" >> $GITHUB_OUTPUT
         id: get_pr
+        env:
+          STABLE_BRANCH: ${{ vars.STABLE_BRANCH }}
 
   merge:
-    uses: misskey-dev/release-manager-actions/.github/workflows/merge.yml@v1
+    uses: misskey-dev/release-manager-actions/.github/workflows/merge.yml@v2
     needs: get-pr
     if: ${{ needs.get-pr.outputs.pr_number != '' && inputs.merge == true }}
     with:
       pr_number: ${{ needs.get-pr.outputs.pr_number }}
+      user: 'github-actions[bot]'
       package_jsons_to_rewrite: ${{ vars.PACKAGE_JSONS_TO_REWRITE }}
       # Text to prepend to the changelog
       # The first line must be `## Unreleased`
@@ -53,39 +60,42 @@ jobs:
 
         ### General
         -
-
+        
         ### Client
         -
-
+        
         ### Server
         -
-
+        
       use_external_app_to_release: ${{ vars.USE_RELEASE_APP == 'true' }}
       indent: ${{ vars.INDENT }}
     secrets:
       RELEASE_APP_ID: ${{ secrets.RELEASE_APP_ID }}
       RELEASE_APP_PRIVATE_KEY: ${{ secrets.RELEASE_APP_PRIVATE_KEY }}
-      RULESET_EDIT_APP_ID: ${{ secrets.RULESET_EDIT_APP_ID }}
-      RULESET_EDIT_APP_PRIVATE_KEY: ${{ secrets.RULESET_EDIT_APP_PRIVATE_KEY }}
 
   create-prerelease:
-    uses: misskey-dev/release-manager-actions/.github/workflows/create-prerelease.yml@v1
+    uses: misskey-dev/release-manager-actions/.github/workflows/create-prerelease.yml@v2
     needs: get-pr
     if: ${{ needs.get-pr.outputs.pr_number != '' && inputs.merge != true  }}
     with:
       pr_number: ${{ needs.get-pr.outputs.pr_number }}
+      user: 'github-actions[bot]'
       package_jsons_to_rewrite: ${{ vars.PACKAGE_JSONS_TO_REWRITE }}
       use_external_app_to_release: ${{ vars.USE_RELEASE_APP == 'true' }}
       indent: ${{ vars.INDENT }}
+      draft_prerelease_channel: alpha
+      ready_start_prerelease_channel: beta
+      prerelease_channel: ${{ inputs.start-rc && 'rc' || '' }}
     secrets:
       RELEASE_APP_ID: ${{ secrets.RELEASE_APP_ID }}
       RELEASE_APP_PRIVATE_KEY: ${{ secrets.RELEASE_APP_PRIVATE_KEY }}
 
   create-target:
-    uses: misskey-dev/release-manager-actions/.github/workflows/create-target.yml@v1
+    uses: misskey-dev/release-manager-actions/.github/workflows/create-target.yml@v2
     needs: get-pr
     if: ${{ needs.get-pr.outputs.pr_number == '' }}
     with:
+      user: 'github-actions[bot]'
       # The script for version increment.
       # process.env.CURRENT_VERSION: The current version.
       #
@@ -118,8 +128,8 @@ jobs:
       package_jsons_to_rewrite: ${{ vars.PACKAGE_JSONS_TO_REWRITE }}
       use_external_app_to_release: ${{ vars.USE_RELEASE_APP == 'true' }}
       indent: ${{ vars.INDENT }}
+      stable_branch: ${{ vars.STABLE_BRANCH }}
+      draft_prerelease_channel: alpha
     secrets:
       RELEASE_APP_ID: ${{ secrets.RELEASE_APP_ID }}
       RELEASE_APP_PRIVATE_KEY: ${{ secrets.RELEASE_APP_PRIVATE_KEY }}
-      RULESET_EDIT_APP_ID: ${{ secrets.RULESET_EDIT_APP_ID }}
-      RULESET_EDIT_APP_PRIVATE_KEY: ${{ secrets.RULESET_EDIT_APP_PRIVATE_KEY }}
diff --git a/.github/workflows/release-with-ready.yml b/.github/workflows/release-with-ready.yml
index a0fad0e336..e863b5e2e8 100644
--- a/.github/workflows/release-with-ready.yml
+++ b/.github/workflows/release-with-ready.yml
@@ -16,26 +16,31 @@ jobs:
   check:
     runs-on: ubuntu-latest
     outputs:
-      ref: ${{ steps.get_pr.outputs.ref }}
+      head: ${{ steps.get_pr.outputs.head }}
+      base: ${{ steps.get_pr.outputs.base }}
     steps:
       - uses: actions/checkout@v4
       # PR情報を取得
       - name: Get PR
         run: |
-          pr_json=$(gh pr view "$PR_NUMBER" --json isDraft,headRefName)
-          echo "ref=$(echo $pr_json | jq -r '.headRefName')" >> $GITHUB_OUTPUT
+          pr_json=$(gh pr view "$PR_NUMBER" --json isDraft,headRefName,baseRefName)
+          echo "head=$(echo $pr_json | jq -r '.headRefName')" >> $GITHUB_OUTPUT
+          echo "base=$(echo $pr_json | jq -r '.baseRefName')" >> $GITHUB_OUTPUT
         id: get_pr
         env:
           PR_NUMBER: ${{ github.event.pull_request.number }}
   release:
-    uses: misskey-dev/release-manager-actions/.github/workflows/create-prerelease.yml@v1
+    uses: misskey-dev/release-manager-actions/.github/workflows/create-prerelease.yml@v2
     needs: check
-    if: startsWith(needs.check.outputs.ref, 'release/')
+    if: needs.check.outputs.head == github.event.repository.default_branch && needs.check.outputs.base == vars.STABLE_BRANCH
     with:
       pr_number: ${{ github.event.pull_request.number }}
+      user: 'github-actions[bot]'
       package_jsons_to_rewrite: ${{ vars.PACKAGE_JSONS_TO_REWRITE }}
       use_external_app_to_release: ${{ vars.USE_RELEASE_APP == 'true' }}
       indent: ${{ vars.INDENT }}
+      draft_prerelease_channel: alpha
+      ready_start_prerelease_channel: beta
     secrets:
       RELEASE_APP_ID: ${{ secrets.RELEASE_APP_ID }}
       RELEASE_APP_PRIVATE_KEY: ${{ secrets.RELEASE_APP_PRIVATE_KEY }}
diff --git a/.github/workflows/report-api-diff.yml b/.github/workflows/report-api-diff.yml
index df9cc279e8..1170f898ce 100644
--- a/.github/workflows/report-api-diff.yml
+++ b/.github/workflows/report-api-diff.yml
@@ -70,18 +70,25 @@ jobs:
       - id: out-diff
         name: Build diff Comment
         run: |
-          cat <<- EOF > ./output.md
-          このPRによるapi.jsonの差分
-          <details>
-          <summary>差分はこちら</summary>
-
-          \`\`\`diff
-          $(cat ./api.json.diff)
-          \`\`\`
-          </details>
-
-          [Get diff files from Workflow Page](https://github.com/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID})
-          EOF
+          HEADER="このPRによるapi.jsonの差分"
+          FOOTER="[Get diff files from Workflow Page](https://github.com/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID})"
+          DIFF_BYTES="$(stat ./api.json.diff -c '%s' | tr -d '\n')"
+          
+          echo "$HEADER" > ./output.md
+          
+          if (( "$DIFF_BYTES" <= 1 )); then
+            echo '差分はありません。' >> ./output.md
+          else
+            echo '<details>' >> ./output.md
+            echo '<summary>差分はこちら</summary>' >> ./output.md
+            echo >> ./output.md
+            echo '```diff' >> ./output.md
+            cat ./api.json.diff >> ./output.md
+            echo '```' >> ./output.md
+            echo '</details>' >> .output.md
+          fi
+          
+          echo "$FOOTER" >> ./output.md
       - uses: thollander/actions-comment-pull-request@v2
         with:
           pr_number: ${{ steps.load-pr-num.outputs.pr-number }}
diff --git a/.github/workflows/storybook.yml b/.github/workflows/storybook.yml
index c52883ffdd..c02f38ee0b 100644
--- a/.github/workflows/storybook.yml
+++ b/.github/workflows/storybook.yml
@@ -7,6 +7,11 @@ on:
       - develop
       - dev/storybook8 # for testing
   pull_request_target:
+    branches-ignore:
+      # Since pull requests targets master mostly is the "develop" branch.
+      # Storybook CI is checked on the "push" event of "develop" branch so it would cause a duplicate build.
+      # This is a waste of chromatic build quota, so we don't run storybook CI on pull requests targets master.
+      - master
 
 jobs:
   build:
@@ -36,7 +41,7 @@ jobs:
     - name: Install pnpm
       uses: pnpm/action-setup@v4
     - name: Use Node.js 20.x
-      uses: actions/setup-node@v4.0.2
+      uses: actions/setup-node@v4.0.4
       with:
         node-version-file: '.node-version'
         cache: 'pnpm'
@@ -88,7 +93,7 @@ jobs:
         if [ "$BRANCH" = "misskey-dev:$HEAD_REF" ]; then
           BRANCH="$HEAD_REF"
         fi
-        pnpm --filter frontend chromatic --exit-once-uploaded -d storybook-static --branch-name $BRANCH $(echo "$CHROMATIC_PARAMETER")
+        pnpm --filter frontend chromatic --exit-once-uploaded -d storybook-static --branch-name "$BRANCH" $(echo "$CHROMATIC_PARAMETER")
       env:
         HEAD_REF: ${{ github.event.pull_request.head.ref }}
         CHROMATIC_PROJECT_TOKEN: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
diff --git a/.github/workflows/test-backend.yml b/.github/workflows/test-backend.yml
index b1c54bb3e7..d95d6676f9 100644
--- a/.github/workflows/test-backend.yml
+++ b/.github/workflows/test-backend.yml
@@ -9,19 +9,20 @@ on:
       - packages/backend/**
       # for permissions
       - packages/misskey-js/**
+      - .github/workflows/test-backend.yml
   pull_request:
     paths:
       - packages/backend/**
       # for permissions
       - packages/misskey-js/**
-
+      - .github/workflows/test-backend.yml
 jobs:
   unit:
     runs-on: ubuntu-latest
 
     strategy:
       matrix:
-        node-version: [20.12.2]
+        node-version: [20.16.0]
 
     services:
       postgres:
@@ -45,7 +46,7 @@ jobs:
     - name: Install FFmpeg
       uses: FedericoCarboni/setup-ffmpeg@v3
     - name: Use Node.js ${{ matrix.node-version }}
-      uses: actions/setup-node@v4.0.2
+      uses: actions/setup-node@v4.0.4
       with:
         node-version: ${{ matrix.node-version }}
         cache: 'pnpm'
@@ -70,7 +71,7 @@ jobs:
 
     strategy:
       matrix:
-        node-version: [20.12.2]
+        node-version: [20.16.0]
 
     services:
       postgres:
@@ -92,7 +93,7 @@ jobs:
       - name: Install pnpm
         uses: pnpm/action-setup@v4
       - name: Use Node.js ${{ matrix.node-version }}
-        uses: actions/setup-node@v4.0.2
+        uses: actions/setup-node@v4.0.4
         with:
           node-version: ${{ matrix.node-version }}
           cache: 'pnpm'
diff --git a/.github/workflows/test-federation.yml b/.github/workflows/test-federation.yml
new file mode 100644
index 0000000000..183ddb6f34
--- /dev/null
+++ b/.github/workflows/test-federation.yml
@@ -0,0 +1,59 @@
+name: Test (federation)
+
+on:
+  push:
+    branches:
+      - master
+      - develop
+    paths:
+      - packages/backend/**
+      - packages/misskey-js/**
+      - .github/workflows/test-federation.yml
+  pull_request:
+    paths:
+      - packages/backend/**
+      - packages/misskey-js/**
+      - .github/workflows/test-federation.yml
+
+jobs:
+  test:
+    runs-on: ubuntu-latest
+    strategy:
+      matrix:
+        node-version: [20.16.0]
+    steps:
+      - uses: actions/checkout@v4
+        with:
+          submodules: true
+      - name: Install pnpm
+        uses: pnpm/action-setup@v4
+      - name: Install FFmpeg
+        uses: FedericoCarboni/setup-ffmpeg@v3
+      - name: Use Node.js ${{ matrix.node-version }}
+        uses: actions/setup-node@v4.0.3
+        with:
+          node-version: ${{ matrix.node-version }}
+          cache: 'pnpm'
+      - name: Build Misskey
+        run: |
+          corepack enable && corepack prepare
+          pnpm i --frozen-lockfile
+          pnpm build
+      - name: Setup
+        run: |
+          cd packages/backend/test-federation
+          bash ./setup.sh
+          sudo chmod 644 ./certificates/*.test.key
+      - name: Start servers
+        # https://github.com/docker/compose/issues/1294#issuecomment-374847206
+        run: |
+          cd packages/backend/test-federation
+          docker compose up -d --scale tester=0
+      - name: Test
+        run: |
+          cd packages/backend/test-federation
+          docker compose run --no-deps tester
+      - name: Stop servers
+        run: |
+          cd packages/backend/test-federation
+          docker compose down
diff --git a/.github/workflows/test-frontend.yml b/.github/workflows/test-frontend.yml
index 9d5053b82a..c68e1a8ef1 100644
--- a/.github/workflows/test-frontend.yml
+++ b/.github/workflows/test-frontend.yml
@@ -11,7 +11,7 @@ on:
       - packages/misskey-js/**
       # for e2e
       - packages/backend/**
-
+      - .github/workflows/test-frontend.yml
   pull_request:
     paths:
       - packages/frontend/**
@@ -19,14 +19,14 @@ on:
       - packages/misskey-js/**
       # for e2e
       - packages/backend/**
-
+      - .github/workflows/test-frontend.yml
 jobs:
   vitest:
     runs-on: ubuntu-latest
 
     strategy:
       matrix:
-        node-version: [20.12.2]
+        node-version: [20.16.0]
 
     steps:
     - uses: actions/checkout@v4.1.1
@@ -35,7 +35,7 @@ jobs:
     - name: Install pnpm
       uses: pnpm/action-setup@v4
     - name: Use Node.js ${{ matrix.node-version }}
-      uses: actions/setup-node@v4.0.2
+      uses: actions/setup-node@v4.0.4
       with:
         node-version: ${{ matrix.node-version }}
         cache: 'pnpm'
@@ -61,7 +61,7 @@ jobs:
     strategy:
       fail-fast: false
       matrix:
-        node-version: [20.12.2]
+        node-version: [20.16.0]
         browser: [chrome]
 
     services:
@@ -90,7 +90,7 @@ jobs:
     - name: Install pnpm
       uses: pnpm/action-setup@v4
     - name: Use Node.js ${{ matrix.node-version }}
-      uses: actions/setup-node@v4.0.2
+      uses: actions/setup-node@v4.0.4
       with:
         node-version: ${{ matrix.node-version }}
         cache: 'pnpm'
diff --git a/.github/workflows/test-misskey-js.yml b/.github/workflows/test-misskey-js.yml
index 2589d908b8..63e81f8c92 100644
--- a/.github/workflows/test-misskey-js.yml
+++ b/.github/workflows/test-misskey-js.yml
@@ -8,11 +8,12 @@ on:
     branches: [ develop ]
     paths:
       - packages/misskey-js/**
+      - .github/workflows/test-misskey-js.yml
   pull_request:
     branches: [ develop ]
     paths:
       - packages/misskey-js/**
-
+      - .github/workflows/test-misskey-js.yml
 jobs:
   test:
 
@@ -20,7 +21,7 @@ jobs:
 
     strategy:
       matrix:
-        node-version: [20.12.2]
+        node-version: [20.16.0]
         # See supported Node.js release schedule at https://nodejs.org/en/about/releases/
 
     steps:
@@ -30,7 +31,7 @@ jobs:
       - run: corepack enable
 
       - name: Setup Node.js ${{ matrix.node-version }}
-        uses: actions/setup-node@v4.0.2
+        uses: actions/setup-node@v4.0.4
         with:
           node-version: ${{ matrix.node-version }}
           cache: 'pnpm'
diff --git a/.github/workflows/test-production.yml b/.github/workflows/test-production.yml
index 7f8db65293..0abc09c5a6 100644
--- a/.github/workflows/test-production.yml
+++ b/.github/workflows/test-production.yml
@@ -16,7 +16,7 @@ jobs:
 
     strategy:
       matrix:
-        node-version: [20.12.2]
+        node-version: [20.16.0]
 
     steps:
     - uses: actions/checkout@v4.1.1
@@ -25,7 +25,7 @@ jobs:
     - name: Install pnpm
       uses: pnpm/action-setup@v4
     - name: Use Node.js ${{ matrix.node-version }}
-      uses: actions/setup-node@v4.0.2
+      uses: actions/setup-node@v4.0.4
       with:
         node-version: ${{ matrix.node-version }}
         cache: 'pnpm'
diff --git a/.github/workflows/validate-api-json.yml b/.github/workflows/validate-api-json.yml
index 24340e7d81..f809af1063 100644
--- a/.github/workflows/validate-api-json.yml
+++ b/.github/workflows/validate-api-json.yml
@@ -7,17 +7,18 @@ on:
       - develop
     paths:
       - packages/backend/**
+      - .github/workflows/validate-api-json.yml
   pull_request:
     paths:
       - packages/backend/**
-
+      - .github/workflows/validate-api-json.yml
 jobs:
   validate-api-json:
     runs-on: ubuntu-latest
 
     strategy:
       matrix:
-        node-version: [20.12.2]
+        node-version: [20.16.0]
 
     steps:
     - uses: actions/checkout@v4.1.1
@@ -26,7 +27,7 @@ jobs:
     - name: Install pnpm
       uses: pnpm/action-setup@v4
     - name: Use Node.js ${{ matrix.node-version }}
-      uses: actions/setup-node@v4.0.2
+      uses: actions/setup-node@v4.0.4
       with:
         node-version: ${{ matrix.node-version }}
         cache: 'pnpm'
diff --git a/.gitignore b/.gitignore
index bdc14fea0a..5b8a798ba6 100644
--- a/.gitignore
+++ b/.gitignore
@@ -35,13 +35,17 @@ coverage
 !/.config/example.yml
 !/.config/docker_example.yml
 !/.config/docker_example.env
+!/.config/cypress-devcontainer.yml
 docker-compose.yml
-!/.devcontainer/docker-compose.yml
+./compose.yml
+.devcontainer/compose.yml
+!/.devcontainer/compose.yml
 
 # misskey
 /build
 built
 built-test
+js-built
 /data
 /.cache-loader
 /db
@@ -59,6 +63,11 @@ ormconfig.json
 temp
 /packages/frontend/src/**/*.stories.ts
 tsdoc-metadata.json
+misskey-assets
+
+# Vite temporary files
+vite.config.js.timestamp-*
+vite.config.ts.timestamp-*
 
 # blender backups
 *.blend1
diff --git a/.gitmodules b/.gitmodules
index 225a69a652..3218575273 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -1,6 +1,3 @@
-[submodule "misskey-assets"]
-	path = misskey-assets
-	url = https://github.com/misskey-dev/assets.git
 [submodule "fluent-emojis"]
 	path = fluent-emojis
 	url = https://github.com/misskey-dev/emojis.git
diff --git a/.node-version b/.node-version
index 87834047a6..8ce7030825 100644
--- a/.node-version
+++ b/.node-version
@@ -1 +1 @@
-20.12.2
+20.16.0
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f68336698d..d34fcfdd15 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,14 +1,313 @@
-## Unreleased
+## 2024.10.2
 
 ### General
--
+- Feat: コンテンツの表示にログインを必須にできるように
+- Feat: 過去のノートを非公開化/フォロワーのみ表示可能にできるように
 
 ### Client
+- Enhance: Bull DashboardでRelationship Queueの状態も確認できるように  
+  (Cherry-picked from https://github.com/MisskeyIO/misskey/pull/751)
+- Enhance: ドライブでソートができるように
+- Enhance: アイコンデコレーション管理画面の改善
+- Enhance: 「単なるラッキー」の取得条件を変更
+- Enhance: 投稿フォームでEscキーを押したときIME入力中ならフォームを閉じないように( #10866 )  
+- Enhance: MiAuth, OAuthの認可画面の改善
+  - どのアカウントで認証しようとしているのかがわかるように
+  - 認証するアカウントを切り替えられるように
+- Enhance: Self-XSS防止用の警告を追加
+- Enhance: カタルーニャ語 (ca-ES) に対応  
+- Fix: 通知の範囲指定の設定項目が必要ない通知設定でも範囲指定の設定がでている問題を修正
+- Fix: Turnstileが失敗・期限切れした際にも成功扱いとなってしまう問題を修正  
+  (Cherry-picked from https://github.com/MisskeyIO/misskey/pull/768)
+- Fix: デッキのタイムラインカラムで「センシティブなファイルを含むノートを表示」設定が使用できなかった問題を修正
+- Fix: Encode RSS urls with escape sequences before fetching allowing query parameters to be used
+- Fix: リンク切れを修正
 - Fix: 別アカウントを指定してノートを投稿した場合でも自分のアカウントの実績が解除されてしまう問題を修正
 
 ### Server
--
+- Enhance: 起動前の疎通チェックで、DBとメイン以外のRedisの疎通確認も行うように  
+  (Based on https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/588)  
+  (Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/715)
+- Fix: Nested proxy requestsを検出した際にブロックするように
+  [ghsa-gq5q-c77c-v236](https://github.com/misskey-dev/misskey/security/advisories/ghsa-gq5q-c77c-v236)
+- Fix: 招待コードの発行可能な残り数算出に使用すべきロールポリシーの値が違う問題を修正  
+  (Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/706)
+- Fix: 連合への配信時に、acctの大小文字が区別されてしまい正しくメンションが処理されないことがある問題を修正  
+  (Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/711)
 
+### Misskey.js
+- Fix: Stream初期化時、別途WebSocketを指定する場合の型定義を修正
+
+## 2024.10.1
+
+### Note
+- スパム対策として、モデレータ権限を持つユーザのアクティビティが7日以上確認できない場合は自動的に招待制へと切り替え(コントロールパネル -> モデレーション -> "誰でも新規登録できるようにする"をオフに変更)るようになりました。 ( #13437 )
+	- 切り替わった際はモデレーターへお知らせとして通知されます。登録をオープンな状態で継続したい場合は、コントロールパネルから再度設定を行ってください。
+
+### General
+- Feat: ユーザーの名前に禁止ワードを設定できるように
+
+### Client
+- Enhance: タイムライン表示時のパフォーマンスを向上
+- Enhance: アーカイブした個人宛のお知らせを表示・編集できるように
+- Enhance: l10nの更新
+- Fix: メールアドレス不要でCaptchaが有効な場合にアカウント登録完了後自動でのログインに失敗する問題を修正
+
+### Server
+- Feat: モデレータ権限を持つユーザが全員7日間活動しなかった場合は自動的に招待制へと切り替えるように ( #13437 )
+- Enhance: 個人宛のお知らせは「わかった」を押すと自動的にアーカイブされるように
+- Fix: `admin/emoji/update`エンドポイントのidのみ指定した時不正なエラーが発生するバグを修正
+- Fix: RBT有効時、リノートのリアクションが反映されない問題を修正
+- Fix: キューのエラーログを簡略化するように  
+  (Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/649)
+
+## 2024.10.0
+
+### Note
+- セキュリティ向上のため、サーバー初期設定時に使用する初期パスワードを設定できるようになりました。今後Misskeyサーバーを新たに設置する際には、初回の起動前にコンフィグファイルの`setupPassword`をコメントアウトし、初期パスワードを設定することをおすすめします。(すでに初期設定を完了しているサーバーについては、この変更に伴い対応する必要はありません)  
+  - ホスティングサービスを運営している場合は、コンフィグファイルを構築する際に`setupPassword`をランダムな値に設定し、ユーザーに通知するようにシステムを更新することをおすすめします。
+  - なお、初期パスワードが設定されていない場合でも初期設定を行うことが可能です(UI上で初期パスワードの入力欄を空欄にすると続行できます)。
+- ユーザーデータを読み込む際の型が一部変更されました。
+	- `twoFactorEnabled`, `usePasswordLessLogin`, `securityKeys`: 自分とモデレーター以外のユーザーからは取得できなくなりました
+
+### General
+- Feat: サーバー初期設定時に初期パスワードを設定できるように
+- Feat: 通報にモデレーションノートを残せるように
+- Feat: 通報の解決種別を設定できるように
+- Enhance: 通報の解決と転送を個別に行えるように
+- Enhance: セキュリティ向上のため、サインイン時もCAPTCHAを求めるようになりました
+- Enhance: 依存関係の更新
+- Enhance: l10nの更新
+- Enhance: Playの「人気」タブで10件以上表示可能に #14399
+- Fix: 連合のホワイトリストが正常に登録されない問題を修正
+
+### Client
+- Enhance: デザインの調整
+- Enhance: ログイン画面の認証フローを改善
+- Fix: クライアント上での時間ベースの実績獲得動作が実績獲得後も発動していた問題を修正  
+  (Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/657)
+
+### Server
+- Enhance: セキュリティ向上のため、ログイン時にメール通知を行うように
+- Enhance: 自分とモデレーター以外のユーザーから二要素認証関連のデータが取得できないように
+- Enhance: 通報および通報解決時に送出されるSystemWebhookにユーザ情報を含めるように ( #14697 )
+- Fix: `admin/abuse-user-reports`エンドポイントのスキーマが間違っていた問題を修正
+
+## 2024.9.0
+
+### General
+- Feat: ノート単体・ユーザーのノート・クリップのノートの埋め込み機能
+  - 埋め込みコードやウェブサイトへの実装方法の詳細は https://misskey-hub.net/docs/for-users/features/embed/ をご覧ください
+- Feat: パスキーでログインボタンを実装 (#14574)
+- Feat: フォローされた際のメッセージを設定できるように
+- Feat: 連合をホワイトリスト制にできるように
+- Feat: UserWebhookとSystemWebhookのテスト送信機能を追加 (#14445)
+- Feat: モデレーターはユーザーにかかわらずファイルが添付されているノートを検索できるように  
+  (Cherry-picked from https://github.com/MisskeyIO/misskey/pull/680)
+- Feat: データエクスポートが完了した際に通知を発行するように
+- Enhance: ユーザーによるコンテンツインポートの可否をロールポリシーで制御できるように
+- Enhance: 依存関係の更新
+- Enhance: l10nの更新
+
+### Client
+- Enhance: サイズ制限を超過するファイルをアップロードしようとした際にエラーを出すように
+- Enhance: アイコンデコレーション管理画面にプレビューを追加
+- Enhance: コントロールパネル内のファイル一覧でセンシティブなファイルを区別しやすく
+- Enhance: ScratchpadにUIインスペクターを追加
+- Enhance: Play編集画面の項目の並びを少しリデザイン
+- Enhance: 各種メニューをドロワー表示するかどうか設定可能に
+- Enhance: AiScriptのMk:C:containerのオプションに`borderStyle`と`borderRadius`を追加
+- Enhance: CWでも絵文字をクリックしてメニューを表示できるように
+- Fix: サーバーメトリクスが2つ以上あるとリロード直後の表示がおかしくなる問題を修正
+- Fix: コントロールパネル内のAp requests内のチャートの表示がおかしかった問題を修正
+- Fix: 月の違う同じ日はセパレータが表示されないのを修正
+- Fix: タッチ画面でレンジスライダーを操作するとツールチップが複数表示される問題を修正  
+  (Cherry-picked from https://github.com/taiyme/misskey/pull/265)
+- Fix: 縦横比が極端なカスタム絵文字を表示する際にレイアウトが崩れる箇所があるのを修正  
+  (Cherry-picked from https://github.com/MisskeyIO/misskey/pull/725)
+- Fix: 設定変更時のリロード確認ダイアログが複数個表示されることがある問題を修正
+- Fix: ファイルの詳細ページのファイルの説明で改行が正しく表示されない問題を修正  
+  (Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/commit/bde6bb0bd2e8b0d027e724d2acdb8ae0585a8110)
+- Fix: 一部画面のページネーションが動作しにくくなっていたのを修正 ( #12766 , #11449 )
+
+### Server
+- Feat: Misskey® Reactions Boost Technology™ (RBT)により、リアクションの作成負荷を低減することが可能に
+- Fix: アンテナの書き込み時にキーワードが与えられなかった場合のエラーをApiErrorとして投げるように
+  - この変更により、公式フロントエンドでは入力の不備が内部エラーとして報告される代わりに一般的なエラーダイアログで報告されます
+- Fix: ファイルがサイズの制限を超えてアップロードされた際にエラーを返さなかった問題を修正
+- Fix: 外部ページを解析する際に、ページに紐づけられた関連リソースも読み込まれてしまう問題を修正  
+  (Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/commit/26e0412fbb91447c37e8fb06ffb0487346063bb8)
+- Fix: Continue importing from file if single emoji import fails
+- Fix: `Retry-After`ヘッダーが送信されなかった問題を修正  
+  (Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/commit/8a982c61c01909e7540ff1be9f019df07c3f0624)
+- Fix: サーバーサイドのDOM解析完了時にリソースを開放するように  
+  (Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/634)
+- Fix: `<link rel="alternate">`を追って照会するのはOKレスポンスが返却された場合のみに  
+  (Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/633)
+- Fix: メールにスタイルが適用されていなかった問題を修正
+
+## 2024.8.0
+
+### General
+- Enhance: モデレーターはすべてのユーザーのフォロー・フォロワーの一覧を見られるように
+- Enhance: アカウントの削除のモデレーションログを残すように
+- Enhance: 不適切なページ、ギャラリー、Playを管理者権限で削除できるように
+- Fix: リモートユーザのフォロー・フォロワーの一覧が非公開設定の場合も表示できてしまう問題を修正
+
+### Client
+- Enhance: 「自分のPlay」ページにおいてPlayが非公開かどうかが一目でわかるように
+- Enhance: 不適切なページ、ギャラリー、Playを通報できるように
+- Fix: Play編集時に公開範囲が「パブリック」にリセットされる問題を修正
+- Fix: ページ遷移に失敗することがある問題を修正
+- Fix: iOSでユーザー名などがリンクとして誤検知される現象を抑制
+- Fix: mCaptchaを使用していてもbotプロテクションに関する警告が消えないのを修正
+- Fix: ユーザーのモデレーションページにおいてユーザー名にドットが入っているとシステムアカウントとして表示されてしまう問題を修正
+- Fix: 特定の条件下でノートの削除ボタンが出ないのを修正
+
+### Server
+- Enhance: 照会時にURLがhtmlかつheadタグ内に`rel="alternate"`, `type="application/activity+json"`の`link`タグがある場合に追ってリンク先を照会できるように
+- Enhance: 凍結されたアカウントのフォローリクエストを表示しないように
+- Fix: WSの`readAllNotifications` メッセージが `body` を持たない場合に動作しない問題 #14374
+  - 通知ページや通知カラム(デッキ)を開いている状態において、新たに発生した通知が既読されない問題が修正されます。
+  - これにより、プッシュ通知が有効な同条件下の環境において、プッシュ通知が常に発生してしまう問題も修正されます。
+- Fix: Play各種エンドポイントの返り値に`visibility`が含まれていない問題を修正
+- Fix: サーバー情報取得の際にモデレーター限定の情報が取得できないことがあるのを修正  
+  (Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/582)
+- Fix: 公開範囲がダイレクトのノートをユーザーアクティビティのチャート生成に使用しないように  
+  (Cherry-picked from https://github.com/MisskeyIO/misskey/pull/679)
+- Fix: ActivityPubのエンティティタイプ判定で不明なタイプを受け取った場合でも処理を継続するように
+  - キュー処理のつまりが改善される可能性があります
+- Fix: リバーシの対局設定の変更が反映されないのを修正
+- Fix: 無制限にストリーミングのチャンネルに接続できる問題を修正
+- Fix: ベースロールのポリシーを変更した際にモデログに記録されないのを修正  
+  (Cherry-picked from https://github.com/MisskeyIO/misskey/pull/700)
+- Fix: Prevent memory leak from memory caches (#14310)
+- Fix: More reliable memory cache eviction (#14311)
+
+## 2024.7.0
+
+### Note
+- デッキUIの新着ノートをサウンドで通知する機能の追加(v2024.5.0)に伴い、以前から動作しなくなっていたクライアント設定内の「アンテナ受信」「チャンネル通知」サウンドを削除しました。
+- Streaming APIにて入力が不正な場合にはそのメッセージを無視するようになりました。 #14251
+
+### General
+- Feat: 通報を受けた際、または解決した際に、予め登録した宛先に通知を飛ばせるように(mail or webhook) #13705
+- Feat: ユーザーのアイコン/バナーの変更可否をロールで設定可能に
+  - 変更不可となっていても、設定済みのものを解除してデフォルト画像に戻すことは出来ます
+- Feat: ユーザ作成時にSystemWebhookを送信可能に #14281
+- Feat: メディアサイレンスを実装 #13842
+  - メディアサイレンスされたサーバーに所属するアカウントによるファイルはすべてセンシティブとして扱われ、カスタム絵文字が使用できないようになります。
+- Enhance: 管理画面でアーカイブにしたお知らせを表示・編集できるように
+- Fix: 配信停止したインスタンス一覧が見れなくなる問題を修正
+- Fix: Dockerコンテナの立ち上げ時に`pnpm`のインストールで固まることがある問題
+- Fix: デフォルトテーマに無効なテーマコードを入力するとUIが使用できなくなる問題を修正
+- 翻訳の更新
+- 依存関係の更新
+
+### Client
+- Feat: ユーザーページから「このユーザーのノートを検索」できるように (#14128)
+- Feat: 検索ページはクエリを受け付けるようになりました (#14128)
+- Enhance: 検索ページのUI改善 (#14128)
+- Enhance: 内蔵APIドキュメントのデザイン・パフォーマンスを改善
+- Enhance: 非ログイン時に他サーバーに遷移するアクションを追加
+- Enhance: 非ログイン時のハイライトTLのデザインを改善
+- Enhance: フロントエンドのアクセシビリティ改善  
+  (Based on https://github.com/taiyme/misskey/pull/226)
+- Enhance: サーバー情報ページ・お問い合わせページを改善  
+  (Cherry-picked from https://github.com/taiyme/misskey/pull/238)
+- Enhance: AiScriptを0.19.0にアップデート
+- Enhance: Allow negative delay for MFM animation elements (`tada`, `jelly`, `twitch`, `shake`, `spin`, `jump`, `bounce`, `rainbow`)
+- Enhance: センシティブなメディアを開く際に確認ダイアログを出せるように
+- Enhance: 検索(ノート/ユーザー)で `#` から始まる文字列を入力すると、そのハッシュタグのノート/ユーザー一覧ページが表示できるように
+- Enhance: 検索(ノート/ユーザー)において、入力に空白が含まれている場合は照会を行わないように
+- Enhance: 検索(ノート/ユーザー)において、照会を行うかどうか、ハッシュタグのノート/ユーザー一覧ページを表示するかどうかの確認ダイアログを出すように
+- Enhance: 検索(ノート/ユーザー)で `@` から始まる文字列(`@user@host`など)を入力すると、そのユーザーを照会できるように
+- Enhance: ドライブのファイル・フォルダをドラッグしなくても移動できるように  
+  (Cherry-picked from https://github.com/nafu-at/misskey/commit/b89c2af6945c6a9f9f10e83f54d2bcf0f240b0b4, https://github.com/nafu-at/misskey/commit/8a7d710c6acb83f50c83f050bd1423c764d60a99)
+- Enhance: デッキのアンテナ・リスト選択画面からそれぞれを新規作成できるように
+- Enhance: ブラウザのコンテキストメニューを使用できるように
+- Enhance: 連合の「連合中」,「購読中」,「配信中」に対してブロックしているサーバー、配信停止しているサーバーを含めないように
+- Fix: `/about#federation` ページなどで各インスタンスのチャートが表示されなくなっていた問題を修正
+- Fix: ユーザーページの追加情報のラベルを投稿者のサーバーの絵文字で表示する (#13968)
+- Fix: リバーシの対局を正しく共有できないことがある問題を修正
+- Fix: コントロールパネルでベースロールのポリシーを編集してもUI上では変更が反映されない問題を修正 
+- Fix: アンテナの編集画面のボタンに隙間を追加
+- Fix: テーマプレビューが見れない問題を修正
+- Fix: ショートカットキーが連打できる問題を修正  
+  (Cherry-picked from https://github.com/taiyme/misskey/pull/234)
+- Fix: MkSignin.vueのcredentialRequestからReactivityを削除(ProxyがPasskey認証処理に渡ることを避けるため)
+- Fix: 「アニメーション画像を再生しない」がオンのときでもサーバーのバナー画像・背景画像がアニメーションしてしまう問題を修正  
+  (Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/574)
+- Fix: Twitchの埋め込みが開けない問題を修正
+- Fix: 子メニューの高さがウィンドウからはみ出ることがある問題を修正
+- Fix: 個人宛てのダイアログ形式のお知らせが即時表示されない問題を修正
+- Fix: 一部の画像がセンシティブ指定されているときに画面に何も表示されないことがあるのを修正
+- Fix: リアクションしたユーザー一覧のユーザー名がはみ出る問題を修正  
+  (Cherry-picked from https://github.com/MisskeyIO/misskey/pull/672)
+- Fix: `/share`ページにおいて絵文字ピッカーを開くことができない問題を修正
+- Fix: deck uiの通知音が重なる問題 (#14029)
+- Fix: ダイレクト投稿の"削除して編集"において、宛先が保持されていなかった問題を修正
+- Fix: 投稿フォームへのURL貼り付けによる引用が下書きに保存されていなかった問題を修正
+- Fix: "削除して編集"や下書きにおいて、リアクションの受け入れ設定が保持/保存されていなかった問題を修正
+- Fix: 照会に `#` から始まる文字列を入力してそのハッシュタグのページを表示する際、入力が `#` のみの場合に「指定されたURLに該当するページはありませんでした。」が表示されてしまう問題を修正
+- Fix: 照会に `@` から始まる文字列を入力してユーザーを照会する際、入力が `@` のみの場合に「問題が発生しました」が表示されてしまう問題を修正
+- Fix: 投稿フォームにノートのURLを貼り付けて"引用として添付"した場合、投稿文を空にすることによるRenote化が出来なかった問題を修正
+- Fix: フォロー中のユーザーに関する"TLに他の人への返信を含める"の設定が分かりづらい問題を修正
+- Fix: タイムラインページを開いた時、`TLに他の人への返信を含める`がオフのときに`ファイル付きのみ`をオンにできない問題を修正
+- Fix: deck uiでタイムラインを切り替えた際にTLの設定項目が更新されず、`TLに他の人への返信を含める`のトグルが表示されない問題を修正
+- Fix: ウィジェットのタイムライン選択欄に無効化されたタイムラインが表示される問題を修正
+- Fix: サウンドにドライブの音声を使用している際にドライブの音声が再生できなくなると設定が変更できなくなる問題を修正
+
+### Server
+- Feat: レートリミット制限に引っかかったときに`Retry-After`ヘッダーを返すように (#13949)
+- Enhance: エンドポイント`clips/update`の必須項目を`clipId`のみに
+- Enhance: エンドポイント`admin/roles/update`の必須項目を`roleId`のみに
+- Enhance: エンドポイント`pages/update`の必須項目を`pageId`のみに
+- Enhance: エンドポイント`gallery/posts/update`の必須項目を`postId`のみに
+- Enhance: エンドポイント`i/webhook/update`の必須項目を`webhookId`のみに
+- Enhance: エンドポイント`admin/ad/update`の必須項目を`id`のみに
+- Enhance: `default.yml`内の`url`, `db.db`, `db.user`, `db.pass`を環境変数から読み込めるように
+- Enhance: エンドポイント`api/meta`にプロパティ`noteSearchableScope`が増え、`string`値`local`または`global`を返却します
+- Fix: チャート生成時にinstance.suspensionStateに置き換えられたinstance.isSuspendedが参照されてしまう問題を修正
+- Fix: ユーザーのフィードページのMFMをHTMLに展開するように (#14006)
+- Fix: アンテナ・クリップ・リスト・ウェブフックがロールポリシーの上限より一つ多く作れてしまうのを修正 (#14036)
+- Fix: notRespondingSinceが実装される前に不通になったインスタンスが自動的に配信停止にならない (#14059)
+- Fix: FTT有効時、タイムライン用エンドポイントで`sinceId`にキャッシュ内最古のものより古いものを指定した場合に正しく結果が返ってこない問題を修正
+- Fix: 自分以外のクリップ内のノート個数が見えることがあるのを修正
+- Fix: 空文字列のリアクションはフォールバックされるように
+- Fix: リノートにリアクションできないように
+- Fix: ユーザー名の前後に空白文字列がある場合は省略するように
+- Fix: プロフィール編集時に名前を空白文字列のみにできる問題を修正
+- Fix: ユーザ名のサジェスト時に表示される内容と順番を調整(以下の順番になります) #14149
+  1. フォロー中かつアクティブなユーザ
+  2. フォロー中かつ非アクティブなユーザ
+  3. フォローしていないアクティブなユーザ
+  4. フォローしていない非アクティブなユーザ
+
+  また、自分自身のアカウントもサジェストされるようになりました。
+- Fix: 一般ユーザーから見たユーザーのバッジの一覧に公開されていないものが含まれることがある問題を修正  
+  (Cherry-picked from https://github.com/MisskeyIO/misskey/pull/652)
+- Fix: ユーザーのリアクション一覧でミュート/ブロックが機能していなかった問題を修正
+- Fix: FTT有効時にリモートユーザーのノートがHTLにキャッシュされる問題を修正
+- Fix: 一部の通知がローカル上のリモートユーザーに対して行われていた問題を修正
+- Fix: エラーメッセージの誤字を修正 (#14213)
+- Fix: ソーシャルタイムラインにローカルタイムラインに表示される自分へのリプライが表示されない問題を修正
+- Fix: リノートのミュートが適用されるまでに時間がかかることがある問題を修正  
+  (Cherry-picked from https://github.com/Type4ny-Project/Type4ny/commit/e9601029b52e0ad43d9131b555b614e56c84ebc1)
+- Fix: Steaming APIが不正なデータを受けた場合の動作が不安定である問題 #14251
+- Fix: `users/search`において `@` から始まる文字列が与えられた際の処理が正しくなかった問題を修正
+  - 名前や自己紹介に `@` から始まる文言が含まれるユーザーも検索できるようになります
+- Fix: 一部のMisskey以外のソフトウェアからファイルを受け取れない問題
+  (Cherry-picked from https://github.com/Secineralyr/misskey.dream/pull/73/commits/652eaff1e8aa00b890d71d2e1e52c263c1e67c76)
+  - NOTE: `drive_file`の`url`, `uri`, `src`の上限が512から1024に変更されます
+	  Migrationではカラム定義の変更のみが行われます。
+		サーバー管理者は各サーバーの必要に応じ`drive_file` `("uri")`に対するインデックスを張りなおすことでより安定しDBの探索が行われる可能性があります。詳細 は [GitHub](https://github.com/misskey-dev/misskey/pull/14323#issuecomment-2257562228)で確認可能です
+- Fix: 自分のフォロワー限定投稿に対するリプライがホームタイムラインで見えないことが有る問題を修正
+- Fix: フォローしていないユーザによるフォロワー限定投稿に対するリプライがソーシャルタイムラインで表示されることがある問題を修正
+
+### Misskey.js
+- Feat: `/drive/files/create` のリクエストに対応(`multipart/form-data`に対応)
+- Feat: `/admin/role/create` のロールポリシーの型を修正
 
 ## 2024.5.0
 
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index dcb625626d..4bcf7e1642 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -1,7 +1,7 @@
 # Contribution guide
 We're glad you're interested in contributing Misskey! In this document you will find the information you need to contribute to the project.
 
-> **Note**
+> [!NOTE]
 > This project uses Japanese as its major language, **but you do not need to translate and write the Issues/PRs in Japanese.**
 > Also, you might receive comments on your Issue/PR in Japanese, but you do not need to reply to them in Japanese as well.\
 > The accuracy of machine translation into Japanese is not high, so it will be easier for us to understand if you write it in the original language.
@@ -17,16 +17,31 @@ Before creating an issue, please check the following:
 	- Issues should only be used to feature requests, suggestions, and bug tracking.
 	- Please ask questions or troubleshooting in [GitHub Discussions](https://github.com/misskey-dev/misskey/discussions) or [Discord](https://discord.gg/Wp8gVStHW3).
 
-> **Warning**
+> [!WARNING]
 > Do not close issues that are about to be resolved. It should remain open until a commit that actually resolves it is merged.
 
-## Before implementation
+### Recommended discussing before implementation
+We welcome your proposal.
+
 When you want to add a feature or fix a bug, **first have the design and policy reviewed in an Issue** (if it is not there, please make one). Without this step, there is a high possibility that the PR will not be merged even if it is implemented.
 
 At this point, you also need to clarify the goals of the PR you will create, and make sure that the other members of the team are aware of them.
 PRs that do not have a clear set of do's and don'ts tend to be bloated and difficult to review.
 
-Also, when you start implementation, assign yourself to the Issue (if you cannot do it yourself, ask another member to assign you). By expressing your intention to work the Issue, you can prevent conflicts in the work.
+Also, when you start implementation, assign yourself to the Issue (if you cannot do it yourself, ask Committer to assign you).
+By expressing your intention to work on the Issue, you can prevent conflicts in the work.
+
+To the Committers: you should not assign someone on it before the Final Decision.
+
+### How issues are triaged
+
+The Committers may:
+* close an issue that is not reproducible on latest stable release,
+* merge an issue into another issue,
+* split an issue into multiple issues,
+* or re-open that has been closed for some reason which is not applicable anymore.
+
+@syuilo reserves the Final Decision rights including whether the project will implement feature and how to implement, these rights are not always exercised.
 
 ## Well-known branches
 - **`master`** branch is tracking the latest release and used for production purposes.
@@ -37,25 +52,41 @@ Also, when you start implementation, assign yourself to the Issue (if you cannot
 ## Creating a PR
 Thank you for your PR! Before creating a PR, please check the following:
 - If possible, prefix the title with a keyword that identifies the type of this PR, as shown below.
-  - `fix` / `refactor` / `feat` / `enhance` / `perf` / `chore` etc
-  - Also, make sure that the granularity of this PR is appropriate. Please do not include more than one type of change or interest in a single PR.
+	- `fix` / `refactor` / `feat` / `enhance` / `perf` / `chore` etc
+	- Also, make sure that the granularity of this PR is appropriate. Please do not include more than one type of change or interest in a single PR.
 - If there is an Issue which will be resolved by this PR, please include a reference to the Issue in the text.
 - Please add the summary of the changes to [`CHANGELOG.md`](/CHANGELOG.md). However, this is not necessary for changes that do not affect the users, such as refactoring.
 - Check if there are any documents that need to be created or updated due to this change.
 - If you have added a feature or fixed a bug, please add a test case if possible.
 - Please make sure that tests and Lint are passed in advance.
-  - You can run it with `pnpm test` and `pnpm lint`. [See more info](#testing)
+	- You can run it with `pnpm test` and `pnpm lint`. [See more info](#testing)
 - If this PR includes UI changes, please attach a screenshot in the text.
 
 Thanks for your cooperation 🤗
 
+### Additional things for ActivityPub payload changes
+*This section is specific to misskey-dev implementation. Other fork or implementation may take different way. A significant difference is that non-"misskey-dev" extension is not described in the misskey-hub's document.*
+
+If PR includes changes to ActivityPub payload, please reflect it in [misskey-hub's document](https://github.com/misskey-dev/misskey-hub-next/blob/master/content/ns.md) by sending PR.
+
+The name of purporsed extension property (referred as "extended property" in later) to ActivityPub shall be prefixed by `_misskey_`. (i.e. `_misskey_quote`)
+
+The extended property in `packages/backend/src/core/activitypub/type.ts` **must** be declared as optional because ActivityPub payloads that comes from older Misskey or other implementation may not contain it.
+
+The extended property must be included in the context definition. Context is defined in `packages/backend/src/core/activitypub/misc/contexts.ts`.
+The key shall be same as the name of extended property, and the value shall be same as "short IRI".
+
+"Short IRI" is defined in misskey-hub's document, but usually takes form of `misskey:<name of extended property>`. (i.e. `misskey:_misskey_quote`)
+
+One should not add property that has defined before by other implementation, or add custom variant value to "well-known" property.
+
 ## Reviewers guide
 Be willing to comment on the good points and not just the things you want fixed 💯
 
 ### Review perspective
 - Scope
-  - Are the goals of the PR clear?
-  - Is the granularity of the PR appropriate?
+	- Are the goals of the PR clear?
+	- Is the granularity of the PR appropriate?
 - Security
 	- Does merging this PR create a vulnerability?
 - Performance
@@ -77,7 +108,7 @@ An actual domain will be assigned so you can test the federation.
 
 ## Release
 ### Release Instructions
-1. Commit version changes in the `develop` branch ([package.json](https://github.com/misskey-dev/misskey/blob/develop/package.json))
+1. Commit version changes in the `develop` branch ([package.json](package.json))
 2. Create a release PR.
 	- Into `master` from `develop` branch.
 	- The title must be in the format `Release: x.y.z`.
@@ -88,7 +119,7 @@ An actual domain will be assigned so you can test the federation.
 	- The target branch must be `master`
 	- The tag name must be the version
 
-> **Note**
+> [!NOTE]
 > Why this instruction is necessary:
 > - To perform final QA checks
 > - To distribute responsibility
@@ -101,17 +132,48 @@ You can improve our translations with your Crowdin account.
 Your changes in Crowdin are automatically submitted as a PR (with the title "New Crowdin translations") to the repository.
 The owner [@syuilo](https://github.com/syuilo) merges the PR into the develop branch before the next release.
 
-If your language is not listed in Crowdin, please open an issue.
+If your language is not listed in Crowdin, please open an issue. We will add it to Crowdin.
+For newly added languages, once the translation progress per language exceeds 70%, it will be officially introduced into Misskey and made available to users.
 
 ![Crowdin](https://d322cqt584bo4o.cloudfront.net/misskey/localized.svg)
 
 ## Development
-During development, it is useful to use the
+### Setup
+Before developing, you have to set up environment. Misskey requires Redis, PostgreSQL, and FFmpeg.
 
+You would want to install Meilisearch to experiment related features. Technically, meilisearch is not strict requirement, but some features and tests require it.
+
+There are a few ways to proceed.
+
+#### Use system-wide software
+You could install them in system-wide (such as from package manager).
+
+#### Use `docker compose`
+You could obtain middleware container by typing `docker compose -f $PROJECT_ROOT/compose.local-db.yml up -d`.
+
+#### Use Devcontainer
+Devcontainer also has necessary setting. This method can be done by connecting from VSCode.
+
+Instead of running `pnpm` locally, you can use Dev Container to set up your development environment.
+To use Dev Container, open the project directory on VSCode with Dev Containers installed.
+**Note:** If you are using Windows, please clone the repository with WSL. Using Git for Windows will result in broken files due to the difference in how newlines are handled.
+
+It will run the following command automatically inside the container.
+``` bash
+git submodule update --init
+pnpm install --frozen-lockfile
+cp .devcontainer/devcontainer.yml .config/default.yml
+pnpm build
+pnpm migrate
+```
+
+After finishing the migration, you can proceed.
+
+### Start developing
+During development, it is useful to use the
 ```
 pnpm dev
 ```
-
 command.
 
 - Server-side source files and automatically builds them if they are modified. Automatically start the server process(es).
@@ -135,52 +197,46 @@ MK_DEV_PREFER=backend pnpm dev
 - To change the port of Vite, specify with `VITE_PORT` environment variable.
 - HMR may not work in some environments such as Windows.
 
-### Dev Container
-Instead of running `pnpm` locally, you can use Dev Container to set up your development environment.
-To use Dev Container, open the project directory on VSCode with Dev Containers installed.  
-**Note:** If you are using Windows, please clone the repository with WSL. Using Git for Windows will result in broken files due to the difference in how newlines are handled.
-
-It will run the following command automatically inside the container.
-``` bash
-git submodule update --init
-pnpm install --frozen-lockfile
-cp .devcontainer/devcontainer.yml .config/default.yml
-pnpm build
-pnpm migrate
-```
-
-After finishing the migration, run the `pnpm dev` command to start the development server.
-
-``` bash
-pnpm dev
-```
-
 ## Testing
-- Test codes are located in [`/packages/backend/test`](/packages/backend/test).
-
-### Run test
-Create a config file.
+You can run non-backend tests by executing following commands:
+```sh
+pnpm --filter frontend test
+pnpm --filter misskey-js test
 ```
+
+Backend tests require manual preparation of servers. See the next section for more on this.
+
+### Backend
+There are three types of test codes for the backend:
+- Unit tests: [`/packages/backend/test/unit`](/packages/backend/test/unit)
+- Single-server E2E tests: [`/packages/backend/test/e2e`](/packages/backend/test/e2e)
+- Multiple-server E2E tests: [`/packages/backend/test-federation`](/packages/backend/test-federation)
+
+#### Running Unit Tests or Single-server E2E Tests
+1. Create a config file:
+```sh
 cp .github/misskey/test.yml .config/
 ```
-Prepare DB/Redis for testing.
-```
-docker compose -f packages/backend/test/docker-compose.yml up
-```
-Alternatively, prepare an empty (data can be erased) DB and edit `.config/test.yml`.
 
-Run all test.
+2. Start DB and Redis servers for testing:
+```sh
+docker compose -f packages/backend/test/compose.yml up
 ```
-pnpm test
+Instead, you can prepare an empty (data can be erased) DB and edit `.config/test.yml` appropriately.
+
+3. Run all tests:
+```sh
+pnpm --filter backend test     # unit tests
+pnpm --filter backend test:e2e # single-server E2E tests
+```
+If you want to run a specific test, run as a following command:
+```sh
+pnpm --filter backend test -- packages/backend/test/unit/activitypub.ts
+pnpm --filter backend test:e2e -- packages/backend/test/e2e/nodeinfo.ts
 ```
 
-#### Run specify test
-```
-pnpm jest -- foo.ts
-```
-
-### e2e tests
-TODO
+#### Running Multiple-server E2E Tests
+See [`/packages/backend/test-federation/README.md`](/packages/backend/test-federation/README.md).
 
 ## Environment Variable
 
@@ -204,7 +260,7 @@ niraxは、Misskeyで使用しているオリジナルのフロントエンド
 ### ルート定義
 ルート定義は、以下の形式のオブジェクトの配列です。
 
-``` ts
+```ts
 {
 	name?: string;
 	path: string;
@@ -217,7 +273,7 @@ niraxは、Misskeyで使用しているオリジナルのフロントエンド
 }
 ```
 
-> **Warning**
+> [!WARNING]
 > 現状、ルートは定義された順に評価されます。
 > たとえば、`/foo/:id`ルート定義の次に`/foo/bar`ルート定義がされていた場合、後者がマッチすることはありません。
 
@@ -279,7 +335,7 @@ export const Default = {
 	parameters: {
 		layout: 'centered',
 	},
-} satisfies StoryObj<typeof MkAvatar>;
+} satisfies StoryObj<typeof MyComponent>;
 ```
 
 If you want to opt-out from the automatic generation, create a `MyComponent.stories.impl.ts` file and add the following line to the file.
@@ -390,7 +446,7 @@ describe('test', () => {
 		})
 			.useMocker(...
 			.compile();
-	
+
 		fooService = app.get<FooService>(FooService);
 		barService = app.get<BarService>(BarService) as jest.Mocked<BarService>;
 
@@ -511,13 +567,13 @@ pnpm dlx typeorm migration:generate -d ormconfig.js -o <migration name>
 - 作成されたスクリプトは不必要な変更を含むため除去してください
 
 ### JSON SchemaのobjectでanyOfを使うとき
-JSON Schemaで、objectに対してanyOfを使う場合、anyOfの中でpropertiesを定義しないこと。  
-バリデーションが効かないため。(SchemaTypeもそのように作られており、objectのanyOf内のpropertiesは捨てられます)  
+JSON Schemaで、objectに対してanyOfを使う場合、anyOfの中でpropertiesを定義しないこと。
+バリデーションが効かないため。(SchemaTypeもそのように作られており、objectのanyOf内のpropertiesは捨てられます)
 https://github.com/misskey-dev/misskey/pull/10082
 
 テキストhogeおよびfugaについて、片方を必須としつつ両方の指定もありうる場合:
 
-```
+```ts
 export const paramDef = {
 	type: 'object',
 	properties: {
@@ -547,3 +603,24 @@ marginはそのコンポーネントを使う側が設定する
 
 ### indexというファイル名を使うな
 ESMではディレクトリインポートは廃止されているのと、ディレクトリインポートせずともファイル名が index だと何故か一部のライブラリ?でディレクトリインポートだと見做されてエラーになる
+
+## CSS Recipe
+
+### Lighten CSS vars
+
+``` css
+color: hsl(from var(--MI_THEME-accent) h s calc(l + 10));
+```
+
+### Darken CSS vars
+
+``` css
+color: hsl(from var(--MI_THEME-accent) h s calc(l - 10));
+```
+
+### Add alpha to CSS vars
+
+``` css
+color: color(from var(--MI_THEME-accent) srgb r g b / 0.5);
+```
+
diff --git a/Dockerfile b/Dockerfile
index 9fc2d611cd..e21b2a31fc 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,6 +1,6 @@
 # syntax = docker/dockerfile:1.4
 
-ARG NODE_VERSION=20.12.2-bullseye
+ARG NODE_VERSION=20.16.0-bullseye
 
 # build assets & compile TypeScript
 
@@ -21,7 +21,9 @@ WORKDIR /misskey
 COPY --link ["pnpm-lock.yaml", "pnpm-workspace.yaml", "package.json", "./"]
 COPY --link ["scripts", "./scripts"]
 COPY --link ["packages/backend/package.json", "./packages/backend/"]
+COPY --link ["packages/frontend-shared/package.json", "./packages/frontend-shared/"]
 COPY --link ["packages/frontend/package.json", "./packages/frontend/"]
+COPY --link ["packages/frontend-embed/package.json", "./packages/frontend-embed/"]
 COPY --link ["packages/sw/package.json", "./packages/sw/"]
 COPY --link ["packages/misskey-js/package.json", "./packages/misskey-js/"]
 COPY --link ["packages/misskey-reversi/package.json", "./packages/misskey-reversi/"]
@@ -82,6 +84,10 @@ RUN apt-get update \
 USER misskey
 WORKDIR /misskey
 
+# add package.json to add pnpm
+COPY --chown=misskey:misskey ./package.json ./package.json
+RUN corepack install
+
 COPY --chown=misskey:misskey --from=target-builder /misskey/node_modules ./node_modules
 COPY --chown=misskey:misskey --from=target-builder /misskey/packages/backend/node_modules ./packages/backend/node_modules
 COPY --chown=misskey:misskey --from=target-builder /misskey/packages/misskey-js/node_modules ./packages/misskey-js/node_modules
diff --git a/chart/files/default.yml b/chart/files/default.yml
index f98b8ebfee..4d17131c25 100644
--- a/chart/files/default.yml
+++ b/chart/files/default.yml
@@ -124,6 +124,14 @@ redis:
 #  #prefix: example-prefix
 #  #db: 1
 
+#redisForReactions:
+#  host: redis
+#  port: 6379
+#  #family: 0  # 0=Both, 4=IPv4, 6=IPv6
+#  #pass: example-pass
+#  #prefix: example-prefix
+#  #db: 1
+
 #   ┌───────────────────────────┐
 #───┘ MeiliSearch configuration └─────────────────────────────
 
diff --git a/docker-compose.local-db.yml b/compose.local-db.yml
similarity index 98%
rename from docker-compose.local-db.yml
rename to compose.local-db.yml
index 16ba4b49e1..3835cb23db 100644
--- a/docker-compose.local-db.yml
+++ b/compose.local-db.yml
@@ -1,5 +1,3 @@
-version: "3"
-
 # このconfigは、 dockerでMisskey本体を起動せず、 redisとpostgresql などだけを起動します
 
 services:
diff --git a/docker-compose_example.yml b/compose_example.yml
similarity index 97%
rename from docker-compose_example.yml
rename to compose_example.yml
index 5cebbe4164..336bd814a7 100644
--- a/docker-compose_example.yml
+++ b/compose_example.yml
@@ -1,5 +1,3 @@
-version: "3"
-
 services:
   web:
     build: .
@@ -19,6 +17,8 @@ services:
     networks:
       - internal_network
       - external_network
+    # env_file:
+    #   - .config/docker.env
     volumes:
       - ./files:/misskey/files
       - ./.config:/misskey/.config:ro
diff --git a/cypress/e2e/basic.cy.ts b/cypress/e2e/basic.cy.ts
index d2525e0a7d..d2efbf709c 100644
--- a/cypress/e2e/basic.cy.ts
+++ b/cypress/e2e/basic.cy.ts
@@ -23,6 +23,7 @@ describe('Before setup instance', () => {
 
 		cy.intercept('POST', '/api/admin/accounts/create').as('signup');
 
+		cy.get('[data-cy-admin-initial-password] input').type('example_password_please_change_this_or_you_will_get_hacked');
 		cy.get('[data-cy-admin-username] input').type('admin');
 		cy.get('[data-cy-admin-password] input').type('admin1234');
 		cy.get('[data-cy-admin-ok]').click();
@@ -119,11 +120,16 @@ describe('After user signup', () => {
 	it('signin', () => {
 		cy.visitHome();
 
-		cy.intercept('POST', '/api/signin').as('signin');
+		cy.intercept('POST', '/api/signin-flow').as('signin');
 
 		cy.get('[data-cy-signin]').click();
-		cy.get('[data-cy-signin-username] input').type('alice');
-		// Enterキーでサインインできるかの確認も兼ねる
+
+		cy.get('[data-cy-signin-page-input]').should('be.visible', { timeout: 1000 });
+		// Enterキーで続行できるかの確認も兼ねる
+		cy.get('[data-cy-signin-username] input').type('alice{enter}');
+
+		cy.get('[data-cy-signin-page-password]').should('be.visible', { timeout: 10000 });
+		// Enterキーで続行できるかの確認も兼ねる
 		cy.get('[data-cy-signin-password] input').type('alice1234{enter}');
 
 		cy.wait('@signin');
@@ -138,8 +144,9 @@ describe('After user signup', () => {
 		cy.visitHome();
 
 		cy.get('[data-cy-signin]').click();
-		cy.get('[data-cy-signin-username] input').type('alice');
-		cy.get('[data-cy-signin-password] input').type('alice1234{enter}');
+
+		cy.get('[data-cy-signin-page-input]').should('be.visible', { timeout: 1000 });
+		cy.get('[data-cy-signin-username] input').type('alice{enter}');
 
 		// TODO: cypressにブラウザの言語指定できる機能が実装され次第英語のみテストするようにする
 		cy.contains(/アカウントが凍結されています|This account has been suspended due to/gi);
diff --git a/cypress/support/commands.ts b/cypress/support/commands.ts
index 281f2e6ccd..197ff963ac 100644
--- a/cypress/support/commands.ts
+++ b/cypress/support/commands.ts
@@ -48,16 +48,19 @@ Cypress.Commands.add('registerUser', (username, password, isAdmin = false) => {
 	cy.request('POST', route, {
 		username: username,
 		password: password,
+		...(isAdmin ? { setupPassword: 'example_password_please_change_this_or_you_will_get_hacked' } : {}),
 	}).its('body').as(username);
 });
 
 Cypress.Commands.add('login', (username, password) => {
 	cy.visitHome();
 
-	cy.intercept('POST', '/api/signin').as('signin');
+	cy.intercept('POST', '/api/signin-flow').as('signin');
 
 	cy.get('[data-cy-signin]').click();
-	cy.get('[data-cy-signin-username] input').type(username);
+	cy.get('[data-cy-signin-page-input]').should('be.visible', { timeout: 1000 });
+	cy.get('[data-cy-signin-username] input').type(`${username}{enter}`);
+	cy.get('[data-cy-signin-page-password]').should('be.visible', { timeout: 10000 });
 	cy.get('[data-cy-signin-password] input').type(`${password}{enter}`);
 
 	cy.wait('@signin').as('signedIn');
diff --git a/packages/frontend/src/components/MkAbuseReport.stories.impl.ts b/idea/MkAbuseReport.stories.impl.ts
similarity index 88%
rename from packages/frontend/src/components/MkAbuseReport.stories.impl.ts
rename to idea/MkAbuseReport.stories.impl.ts
index cf09c96fd4..717bceb23d 100644
--- a/packages/frontend/src/components/MkAbuseReport.stories.impl.ts
+++ b/idea/MkAbuseReport.stories.impl.ts
@@ -7,8 +7,8 @@
 import { action } from '@storybook/addon-actions';
 import { StoryObj } from '@storybook/vue3';
 import { HttpResponse, http } from 'msw';
-import { abuseUserReport } from '../../.storybook/fakes.js';
-import { commonHandlers } from '../../.storybook/mocks.js';
+import { abuseUserReport } from '../packages/frontend/.storybook/fakes.js';
+import { commonHandlers } from '../packages/frontend/.storybook/mocks.js';
 import MkAbuseReport from './MkAbuseReport.vue';
 export const Default = {
 	render(args) {
diff --git a/idea/MkDisableSection.vue b/idea/MkDisableSection.vue
new file mode 100644
index 0000000000..360705071b
--- /dev/null
+++ b/idea/MkDisableSection.vue
@@ -0,0 +1,41 @@
+<!--
+SPDX-FileCopyrightText: syuilo and misskey-project
+SPDX-License-Identifier: AGPL-3.0-only
+-->
+
+<template>
+<div :class="[$style.root]">
+	<div :inert="disabled" :class="[{ [$style.disabled]: disabled }]">
+		<slot></slot>
+	</div>
+	<div v-if="disabled" :class="[$style.cover]"></div>
+</div>
+</template>
+
+<script lang="ts" setup>
+defineProps<{
+	disabled?: boolean;
+}>();
+</script>
+
+<style lang="scss" module>
+.root {
+	position: relative;
+}
+
+.disabled {
+	opacity: 0.7;
+}
+
+.cover {
+	position: absolute;
+	top: 0;
+	left: 0;
+	width: 100%;
+	height: 100%;
+	cursor: not-allowed;
+	--color: color(from var(--MI_THEME-error) srgb r g b / 0.25);
+	background-size: auto auto;
+	background-image: repeating-linear-gradient(135deg, transparent, transparent 10px, var(--color) 4px, var(--color) 14px);
+}
+</style>
diff --git a/idea/README.md b/idea/README.md
new file mode 100644
index 0000000000..f64d16800a
--- /dev/null
+++ b/idea/README.md
@@ -0,0 +1 @@
+使われなくなったけど消すのは勿体ない(将来使えるかもしれない)コードを入れておくとこ
diff --git a/locales/ar-SA.yml b/locales/ar-SA.yml
index 955d672c1d..de24ad4bb9 100644
--- a/locales/ar-SA.yml
+++ b/locales/ar-SA.yml
@@ -626,10 +626,7 @@ abuseReported: "أُرسل البلاغ، شكرًا لك"
 reporter: "المُبلّغ"
 reporteeOrigin: "أصل البلاغ"
 reporterOrigin: "أصل المُبلّغ"
-forwardReport: "وجّه البلاغ إلى المثيل البعيد"
-forwardReportIsAnonymous: "في المثيل البعيد سيظهر المبلّغ كحساب مجهول."
 send: "أرسل"
-abuseMarkAsResolved: "علّم البلاغ كمحلول"
 openInNewTab: "افتح في لسان جديد"
 defaultNavigationBehaviour: "سلوك الملاحة الافتراضي"
 editTheseSettingsMayBreakAccount: "تعديل هذه الإعدادات قد يسبب عطبًا لحسابك"
@@ -1255,15 +1252,12 @@ _theme:
     buttonBg: "خلفية الأزرار"
     buttonHoverBg: "خلفية الأزرار (عند التمرير فوقها)"
     inputBorder: "حواف حقل الإدخال"
-    listItemHoverBg: "خلفية عناصر القائمة (عند التمرير فوقها)"
     driveFolderBg: "خلفية مجلد قرص التخزين"
     messageBg: "خلفية المحادثة"
 _sfx:
   note: "الملاحظات"
   noteMy: "ملاحظتي"
   notification: "الإشعارات"
-  antenna: "الهوائيات"
-  channel: "إشعارات القنات"
 _ago:
   future: "المستقبَل"
   justNow: "اللحظة"
@@ -1535,6 +1529,7 @@ _notification:
     reaction: "التفاعل"
     receiveFollowRequest: "طلبات المتابعة"
     followRequestAccepted: "طلبات المتابعة المقبولة"
+    login: "لِج"
     app: "إشعارات التطبيقات المرتبطة"
   _actions:
     followBack: "تابعك بالمثل"
@@ -1566,6 +1561,10 @@ _webhookSettings:
   active: "مُفعّل"
   _events:
     reaction: "عند التفاعل"
+_abuseReport:
+  _notificationRecipient:
+    _recipientType:
+      mail: "البريد الإلكتروني "
 _moderationLogTypes:
   suspend: "علِق"
   deleteDriveFile: "حُذف الملف"
diff --git a/locales/bn-BD.yml b/locales/bn-BD.yml
index abcf07da83..0e761b0743 100644
--- a/locales/bn-BD.yml
+++ b/locales/bn-BD.yml
@@ -451,7 +451,6 @@ or: "অথবা"
 language: "ভাষা"
 uiLanguage: "UI এর ভাষা"
 aboutX: "{x} সম্পর্কে"
-disableDrawer: "ড্রয়ার মেনু প্রদর্শন করবেন না"
 noHistory: "কোনো ইতিহাস নেই"
 signinHistory: "প্রবেশ করার ইতিহাস"
 doing: "প্রক্রিয়া করছে..."
@@ -625,10 +624,7 @@ abuseReported: "আপনার অভিযোগটি দাখিল কর
 reporter: "অভিযোগকারী"
 reporteeOrigin: "অভিযোগটির উৎস"
 reporterOrigin: "অভিযোগকারীর উৎস"
-forwardReport: "রিমোট ইন্সত্যান্সে অভিযোগটি পাঠান"
-forwardReportIsAnonymous: "আপনার তথ্য রিমোট ইন্সত্যান্সে পাঠানো হবে না এবং একটি বেনামী সিস্টেম অ্যাকাউন্ট হিসাবে প্রদর্শিত হবে।"
 send: "পাঠান"
-abuseMarkAsResolved: "অভিযোগটিকে সমাধাকৃত হিসাবে চিহ্নিত করুন"
 openInNewTab: "নতুন ট্যাবে খুলুন"
 openInSideView: "সাইড ভিউতে খুলুন"
 defaultNavigationBehaviour: "ডিফল্ট নেভিগেশন"
@@ -1021,7 +1017,6 @@ _theme:
     buttonBg: "বাটনের পটভূমি"
     buttonHoverBg: "বাটনের পটভূমি (হভার)"
     inputBorder: "ইনপুট ফিল্ডের বর্ডার"
-    listItemHoverBg: "লিস্ট আইটেমের পটভূমি (হোভার)"
     driveFolderBg: "ড্রাইভ ফোল্ডারের পটভূমি"
     wallpaperOverlay: "ওয়ালপেপার ওভারলে"
     badge: "ব্যাজ"
@@ -1033,8 +1028,6 @@ _sfx:
   note: "নোটগুলি"
   noteMy: "নোট (আপনার)"
   notification: "বিজ্ঞপ্তি"
-  antenna: "অ্যান্টেনাগুলি"
-  channel: "চ্যানেলের বিজ্ঞপ্তি"
 _ago:
   future: "ভবিষ্যৎ"
   justNow: "এইমাত্র"
@@ -1316,6 +1309,7 @@ _notification:
     pollEnded: "পোল শেষ"
     receiveFollowRequest: "প্রাপ্ত অনুসরণের অনুরোধসমূহ"
     followRequestAccepted: "গৃহীত অনুসরণের অনুরোধসমূহ"
+    login: "প্রবেশ করুন"
     app: "লিঙ্ক করা অ্যাপ থেকে বিজ্ঞপ্তি"
   _actions:
     followBack: "ফলো ব্যাক করেছে"
@@ -1346,6 +1340,10 @@ _deck:
 _webhookSettings:
   name: "নাম"
   active: "চালু"
+_abuseReport:
+  _notificationRecipient:
+    _recipientType:
+      mail: "ইমেইল"
 _moderationLogTypes:
   suspend: "স্থগিত করা"
   resetPassword: "পাসওয়ার্ড রিসেট করুন"
diff --git a/locales/ca-ES.yml b/locales/ca-ES.yml
index 0345ee0326..748f6f03c0 100644
--- a/locales/ca-ES.yml
+++ b/locales/ca-ES.yml
@@ -2,12 +2,15 @@
 _lang_: "Català"
 headlineMisskey: "Una xarxa connectada per notes"
 introMisskey: "Benvingut! Misskey és un servei de microblogging descentralitzat de codi obert.\nCrea \"notes\" per compartir els teus pensaments amb tots els que t'envolten. 📡\nAmb \"reaccions\", també pots expressar ràpidament els teus sentiments sobre les notes de tothom. 👍\nExplorem un món nou! 🚀"
-poweredByMisskeyDescription: "{name} És un del serveis (anomenats instàncies de Misskey) que utilitzen la plataforma de codi obert <b>Misskey</b>."
+poweredByMisskeyDescription: "{name} És un dels serveis (anomenats instàncies de Misskey) que utilitzen la plataforma de codi obert <b>Misskey</b>."
 monthAndDay: "{day}/{month}"
 search: "Cercar"
 notifications: "Notificacions"
 username: "Nom d'usuari"
 password: "Contrasenya"
+initialPasswordForSetup: "Contrasenya inicial per la configuració inicial"
+initialPasswordIsIncorrect: "La contrasenya no és correcta."
+initialPasswordForSetupDescription: "Fes servir la contrasenya que has fet servir al fitxer de configuració, si tu mateix has instal·lat Misskey.\nSi fas servir una empresa d'allotjament de Misskey, fes servir la contrasenya que t'han donat.\nSi no has posat cap contrasenya deixar l'espai en blanc."
 forgotPassword: "Contrasenya oblidada"
 fetchingAsApObject: "Cercant en el Fediverse..."
 ok: "OK"
@@ -15,7 +18,7 @@ gotIt: "Ho he entès!"
 cancel: "Cancel·lar"
 noThankYou: "No, gràcies"
 enterUsername: "Introdueix el teu nom d'usuari"
-renotedBy: "Impulsat per {usuari}"
+renotedBy: "Impulsat per {user}"
 noNotes: "Cap nota"
 noNotifications: "Cap notificació"
 instance: "Servidor"
@@ -60,6 +63,7 @@ copyFileId: "Copiar ID d'arxiu"
 copyFolderId: "Copiar ID de carpeta"
 copyProfileUrl: "Copiar URL del perfil"
 searchUser: "Cercar un usuari"
+searchThisUsersNotes: "Cerca les publicacions de l'usuari"
 reply: "Respondre"
 loadMore: "Carregar més"
 showMore: "Veure més"
@@ -108,11 +112,14 @@ enterEmoji: "Introduir un emoji"
 renote: "Impulsa"
 unrenote: "Anul·la l'impuls"
 renoted: "S'ha impulsat"
+renotedToX: "Impulsat per {name}."
 cantRenote: "No es pot impulsar aquesta publicació"
 cantReRenote: "No es pot impulsar l'impuls."
 quote: "Cita"
 inChannelRenote: "Renotar només al Canal"
 inChannelQuote: "Citar només al Canal"
+renoteToChannel: "Impulsa a un canal"
+renoteToOtherChannel: "Impulsa a un altre canal"
 pinnedNote: "Nota fixada"
 pinned: "Fixar al perfil"
 you: "Tu"
@@ -151,6 +158,7 @@ editList: "Editar llista"
 selectChannel: "Selecciona un canal"
 selectAntenna: "Tria una antena"
 editAntenna: "Modificar antena"
+createAntenna: "Crea una antena"
 selectWidget: "Triar un giny"
 editWidgets: "Editar ginys"
 editWidgetsExit: "Fet"
@@ -177,6 +185,10 @@ addAccount: "Afegeix un compte"
 reloadAccountsList: "Recarregar la llista de contactes"
 loginFailed: "S'ha produït un error al accedir."
 showOnRemote: "Navega més en el perfil original"
+continueOnRemote: "Veure perfil original"
+chooseServerOnMisskeyHub: "Escull un servidor des del Hub de Misskey"
+specifyServerHost: "Especifica un servidor directament"
+inputHostName: "Introdueix el domini"
 general: "General"
 wallpaper: "Fons de Pantalla"
 setWallpaper: "Defineix el fons de pantalla"
@@ -187,6 +199,7 @@ followConfirm: "Estàs segur que vols deixar de seguir {name}?"
 proxyAccount: "Compte de proxy"
 proxyAccountDescription: "Un compte proxy és un compte que actua com a seguidor remot per als usuaris en determinades condicions. Per exemple, quan un usuari afegeix un usuari remot a la llista, l'activitat de l'usuari remot no es lliurarà al servidor si cap usuari local segueix aquest usuari, de manera que el compte proxy el seguirà."
 host: "Amfitrió"
+selectSelf: "Escollir manualment"
 selectUser: "Selecciona usuari/a"
 recipient: "Destinatari"
 annotation: "Comentaris"
@@ -202,6 +215,7 @@ perDay: "Per dia"
 stopActivityDelivery: "Deixa d'enviar activitats"
 blockThisInstance: "Deixa d'enviar activitats"
 silenceThisInstance: "Silencia aquesta instància "
+mediaSilenceThisInstance: "Silenciar els arxius d'aquesta instància "
 operations: "Accions"
 software: "Programari"
 version: "Versió"
@@ -223,6 +237,10 @@ blockedInstances: "Instàncies bloquejades"
 blockedInstancesDescription: "Llista els enllaços d'amfitrió de les instàncies que vols bloquejar separades per un salt de pàgina. Les instàncies llistades no podran comunicar-se amb aquesta instància."
 silencedInstances: "Instàncies silenciades"
 silencedInstancesDescription: "Llista els enllaços d'amfitrió de les instàncies que vols silenciar. Tots els comptes de les instàncies llistades s'establiran com silenciades i només podran fer sol·licitacions de seguiment, i no podran mencionar als comptes locals si no els segueixen. Això no afectarà les instàncies bloquejades."
+mediaSilencedInstances: "Instàncies amb els arxius silenciats"
+mediaSilencedInstancesDescription: "Llista els noms dels servidors que vulguis silenciar els arxius, un servidor per línia. Tots els comptes que pertanyin als servidors llistats seran tractats com sensibles i no podran fer servir emojis personalitzats. Això no tindrà efecte sobre els servidors blocats."
+federationAllowedHosts: "Llista de servidors federats"
+federationAllowedHostsDescription: "Llista dels servidors amb els quals es federa."
 muteAndBlock: "Silencia i bloca"
 mutedUsers: "Usuaris silenciats"
 blockedUsers: "Usuaris bloquejats"
@@ -313,6 +331,7 @@ selectFile: "Selecciona fitxers"
 selectFiles: "Selecciona fitxers"
 selectFolder: "Selecció de carpeta"
 selectFolders: "Selecció de carpeta"
+fileNotSelected: "Cap fitxer seleccionat"
 renameFile: "Canvia el nom del fitxer"
 folderName: "Nom de la carpeta"
 createFolder: "Crea una carpeta"
@@ -320,6 +339,7 @@ renameFolder: "Canvia el nom de la carpeta"
 deleteFolder: "Elimina la carpeta"
 folder: "Carpeta "
 addFile: "Afegeix un fitxer"
+showFile: "Mostrar fitxer"
 emptyDrive: "La teva unitat és buida"
 emptyFolder: "La carpeta està buida"
 unableToDelete: "No es pot eliminar"
@@ -434,6 +454,7 @@ totpDescription: "Escriu una contrasenya d'un sol us fent servir l'aplicació d'
 moderator: "Moderador/a"
 moderation: "Moderació"
 moderationNote: "Nota de moderació "
+moderationNoteDescription: "Pots escriure notes que es compartiran entre els moderadors."
 addModerationNote: "Afegir una nota de moderació "
 moderationLogs: "Registre de moderació "
 nUsersMentioned: "{n} usuaris mencionats"
@@ -468,10 +489,12 @@ retype: "Torneu a introduir-la"
 noteOf: "Publicació de: {user}"
 quoteAttached: "Frase adjunta"
 quoteQuestion: "Vols annexar-la com a cita?"
+attachAsFileQuestion: "El text copiat és massa llarg. Vols adjuntar-lo com un fitxer de text?"
 noMessagesYet: "Encara no hi ha missatges"
 newMessageExists: "Has rebut un nou missatge"
 onlyOneFileCanBeAttached: "Només pots adjuntar un fitxer a un missatge"
 signinRequired: "Si us plau, Registra't o inicia la sessió abans de continuar"
+signinOrContinueOnRemote: "Per continuar necessites moure el teu servidor o registrar-te / iniciar sessió en aquest servidor."
 invitations: "Convida"
 invitationCode: "Codi d'invitació"
 checking: "Comprovació en curs..."
@@ -493,7 +516,10 @@ uiLanguage: "Idioma de l'interfície"
 aboutX: "Respecte a {x}"
 emojiStyle: "Estil d'emoji"
 native: "Nadiu"
-disableDrawer: "No mostrar els menús en calaixos"
+menuStyle: "Estil de menú"
+style: "Estil"
+drawer: "Calaix"
+popup: "Emergent"
 showNoteActionsOnlyHover: "Només mostra accions de la nota en passar amb el cursor"
 showReactionsCount: "Mostra el nombre de reaccions a les publicacions"
 noHistory: "No hi ha un registre previ"
@@ -543,7 +569,7 @@ objectStorageUseSSLDesc: "Desactiva'l si no tens pensat fer servir HTTPS per les
 objectStorageUseProxy: "Connectar-se  mitjançant un Proxy"
 objectStorageUseProxyDesc: "Desactiva'l si no faràs servir un Proxy per les connexions de l'API"
 objectStorageSetPublicRead: "Configurar les pujades com públiques "
-s3ForcePathStyleDesc: "Si s3ForcePathStyle es troba activat el nom del dipòsit s'ha d'incloure a l'adreça URL en comtes del nom del host. Potser que necessitis activar-ho quan facis servir, per exemple, Minio a un servidor propi."
+s3ForcePathStyleDesc: "Si s3ForcePathStyle es troba activat el nom del cubell s'haurà d'especificar com a part de l'adreça URL en comptes del nom del servidor. Podria ser que necessitis activar aquesta opció quan facis servir serveis com ara l'allotjament a un servidor propi."
 serverLogs: "Registres del servidor"
 deleteAll: "Elimina-ho tot"
 showFixedPostForm: "Mostrar el formulari per escriure a l'inici de la línia de temps"
@@ -576,6 +602,8 @@ ascendingOrder: "Ascendent"
 descendingOrder: "Descendent"
 scratchpad: "Bloc de proves"
 scratchpadDescription: "El bloc de proves proporciona un entorn experimental per AiScript. Pot escriure i verificar els resultats que interactuen amb Misskey."
+uiInspector: "Inspector de la interfície"
+uiInspectorDescription: "Podeu visualitzar una llista d'elements UI presents en la memòria. Els components de la interfície d'usuari són generats per les funcions Ui:C:."
 output: "Sortida"
 script: "Script"
 disablePagesScript: "Desactivar AiScript a les pàgines "
@@ -692,10 +720,7 @@ abuseReported: "La teva denúncia s'ha enviat. Moltes gràcies."
 reporter: "Denunciant "
 reporteeOrigin: "Origen de la denúncia "
 reporterOrigin: "Origen del denunciant"
-forwardReport: "Transferir la denúncia a una instància remota"
-forwardReportIsAnonymous: "En lloc del teu compte, es farà servir un compte anònim com a denunciant al servidor remot."
 send: "Envia"
-abuseMarkAsResolved: "Marca la denúncia com a resolta"
 openInNewTab: "Obre a una pestanya nova"
 openInSideView: "Obre a una vista lateral"
 defaultNavigationBehaviour: "Navegació per defecte"
@@ -832,6 +857,7 @@ administration: "Administració"
 accounts: "Comptes"
 switch: "Canvia"
 noMaintainerInformationWarning: "La informació de l'administrador no s'ha configurat"
+noInquiryUrlWarning: "No s'ha desat l'URL de consulta."
 noBotProtectionWarning: "La protecció contra bots no s'ha configurat."
 configure: "Configurar"
 postToGallery: "Crear una nova publicació a la galeria"
@@ -896,6 +922,7 @@ followersVisibility: "Visibilitat dels seguidors"
 continueThread: "Veure la continuació del fil"
 deleteAccountConfirm: "Això eliminarà el teu compte irreversiblement. Procedir?"
 incorrectPassword: "Contrasenya incorrecta."
+incorrectTotp: "La contrasenya no és correcta, o ha caducat."
 voteConfirm: "Confirma el teu vot \"{choice}\""
 hide: "Amagar"
 useDrawerReactionPickerForMobile: "Mostrar el selector de reaccions com un calaix al mòbil "
@@ -920,6 +947,9 @@ oneHour: "1 hora"
 oneDay: "Un dia"
 oneWeek: "Una setmana"
 oneMonth: "Un mes"
+threeMonths: "3 mesos"
+oneYear: "1 any"
+threeDays: "3 dies"
 reflectMayTakeTime: "Això pot trigar una estona a tenir efecte"
 failedToFetchAccountInformation: "No es pot obtenir la informació del compte"
 rateLimitExceeded: "S'ha arribat al màxim de peticions"
@@ -1021,6 +1051,7 @@ thisPostMayBeAnnoyingHome: "Publicar a la línia de temps d'Inici"
 thisPostMayBeAnnoyingCancel: "Cancel·lar "
 thisPostMayBeAnnoyingIgnore: "Publicar de totes maneres"
 collapseRenotes: "Col·lapsar les renotes que ja has vist"
+collapseRenotesDescription: "Col·lapse les notes a les quals ja has reaccionat o que ja has renotat"
 internalServerError: "Error intern del servidor"
 internalServerErrorDescription: "El servidor ha fallat de manera inexplicable."
 copyErrorInfo: "Copiar la informació de l'error "
@@ -1059,6 +1090,7 @@ retryAllQueuesConfirmTitle: "Tornar a intentar-ho tot?"
 retryAllQueuesConfirmText: "Això farà que la càrrega del servidor augmenti temporalment."
 enableChartsForRemoteUser: "Generar gràfiques d'usuaris remots"
 enableChartsForFederatedInstances: "Generar gràfiques d'instàncies remotes"
+enableStatsForFederatedInstances: "Activa les estadístiques de les instàncies remotes federades"
 showClipButtonInNoteFooter: "Afegir \"Retall\" al menú d'acció de la nota"
 reactionsDisplaySize: "Mida de les reaccions"
 limitWidthOfReaction: "Limitar l'amplada màxima de la reacció i mostrar-les en una mida reduïda "
@@ -1094,6 +1126,8 @@ preservedUsernames: "Noms d'usuaris reservats"
 preservedUsernamesDescription: "Llistat de noms d'usuaris que no es poden fer servir separats per salts de linia. Aquests noms d'usuaris no estaran disponibles quan es creï un compte d'usuari normal, però els administradors els poden fer servir per crear comptes manualment. Per altre banda els comptes ja creats amb aquests noms d'usuari no es veure'n afectats."
 createNoteFromTheFile: "Compon una nota des d'aquest fitxer"
 archive: "Arxiu"
+archived: "Arxivat"
+unarchive: "Desarxivar"
 channelArchiveConfirmTitle: "Vols arxivar {name}?"
 channelArchiveConfirmDescription: "Un Canal arxivat no apareixerà a la llista de canals o als resultats de cerca. Tampoc es poden afegir noves entrades."
 thisChannelArchived: "Aquest Canal ha sigut arxivat."
@@ -1104,6 +1138,9 @@ preventAiLearning: "Descartar l'ús d'aprenentatge automàtic (IA Generativa)"
 preventAiLearningDescription: "Demanar els indexadors no fer servir els texts, imatges, etc. en cap conjunt de dades per alimentar l'aprenentatge automàtic (IA Predictiva/ Generativa). Això s'aconsegueix afegint la etiqueta \"noai\" com a resposta HTML al contingut corresponent. Prevenir aquest ús totalment pot ser que no sigui aconseguit, ja que molts indexadors poden obviar aquesta etiqueta."
 options: "Opcions"
 specifyUser: "Especificar usuari"
+lookupConfirm: "Vols fer una cerca?"
+openTagPageConfirm: "Vols obrir una pàgina d'etiquetes?"
+specifyHost: "Especifica un servidor"
 failedToPreviewUrl: "Vista prèvia no disponible"
 update: "Actualitzar"
 rolesThatCanBeUsedThisEmojiAsReaction: "Rols que poden fer servir aquest emoji com a reacció "
@@ -1172,7 +1209,10 @@ confirmShowRepliesAll: "Aquesta opció no té marxa enrere. Vols mostrar les tev
 confirmHideRepliesAll: "Aquesta opció no té marxa enrere. Vols ocultar les teves respostes a tots els usuaris que segueixes a la línia de temps?"
 externalServices: "Serveis externs"
 sourceCode: "Codi font"
+sourceCodeIsNotYetProvided: "El codi font encara no es troba disponible. Contacta amb l'administrador per solucionar aquest problema."
 repositoryUrl: "URL del repositori"
+repositoryUrlDescription: "Si estàs fent servir Misskey tal com és (sense cap canvi al codi font), introdueix https://github.com/misskey-dev/misskey"
+repositoryUrlOrTarballRequired: "Si no ofereixes cap repositori, publica un fitxer tarball. Dona una ullada a .config/example.yml per a més informació."
 feedback: "Opinió"
 feedbackUrl: "URL per a opinar"
 impressum: "Impressum"
@@ -1211,6 +1251,7 @@ showReplay: "Veure reproducció"
 replay: "Reproduir"
 replaying: "Reproduint"
 endReplay: "Tanca la redifusió"
+copyReplayData: "Copia les dades de la resposta"
 ranking: "Classificació"
 lastNDays: "Últims {n} dies"
 backToTitle: "Torna al títol"
@@ -1224,12 +1265,79 @@ gameRetry: "Torna a provar"
 notUsePleaseLeaveBlank: "Si no voleu usar-ho, deixeu-ho en blanc"
 useTotp: "Usa una contrasenya d'un sol ús"
 useBackupCode: "Usa un codi de recuperació"
+launchApp: "Inicia l'aplicació "
+useNativeUIForVideoAudioPlayer: "Fes servir la UI del navegador quan reprodueixis vídeo i àudio "
+keepOriginalFilename: "Desa el nom del fitxer original"
+keepOriginalFilenameDescription: "Si desactives aquesta opció els noms dels fitxers se substituiran per una cadena aleatòria quan carreguis nous fitxers de forma automàtica."
+noDescription: "No hi ha una descripció "
+alwaysConfirmFollow: "Confirma sempre els seguiments"
+inquiry: "Contacte"
+tryAgain: "Intenta-ho més tard."
+confirmWhenRevealingSensitiveMedia: "Confirmació quan revelis contingut sensible "
+sensitiveMediaRevealConfirm: "Aquest contingut potser sensible. Segur que ho vols revelar?"
+createdLists: "Llistes creades "
+createdAntennas: "Antenes creades"
+fromX: "De {x}"
+genEmbedCode: "Obtenir el codi per incrustar"
+noteOfThisUser: "Notes d'aquest usuari"
+clipNoteLimitExceeded: "No es poden afegir més notes a aquest clip."
+performance: "Rendiment"
+modified: "Modificat"
+discard: "Descarta"
+thereAreNChanges: "Hi ha(n) {n} canvi(s)"
+signinWithPasskey: "Inicia sessió amb Passkey"
+unknownWebAuthnKey: "Passkey desconeguda"
+passkeyVerificationFailed: "La verificació a fallat"
+passkeyVerificationSucceededButPasswordlessLoginDisabled: "La verificació de la passkey a estat correcta, però s'ha deshabilitat l'inici de sessió sense contrasenya."
+messageToFollower: "Missatge als meus seguidors"
+target: "Assumpte "
+testCaptchaWarning: "És una característica dissenyada per a la prova de CAPTCHA. <strong>No l'utilitzes en l'entorn real.</strong>"
+prohibitedWordsForNameOfUser: "Noms prohibits per escollir noms d'usuari "
+prohibitedWordsForNameOfUserDescription: "Si qualsevol d'aquestes paraules es troben a un nom d'usuari la creació de l'usuari no es durà a terme. Als moderadors no els afecta aquesta restricció."
+yourNameContainsProhibitedWords: "El nom conté paraules prohibides "
+yourNameContainsProhibitedWordsDescription: "Si de veritat vols fer servir aquest nom posat en contacte amb l'administrador."
+thisContentsAreMarkedAsSigninRequiredByAuthor: "L'autor requereix l'inici de sessió per poder veure"
+lockdown: "Bloquejat"
+pleaseSelectAccount: "Seleccionar un compte"
+_accountSettings:
+  requireSigninToViewContents: "És obligatori l'inici de sessió per poder veure el contingut"
+  requireSigninToViewContentsDescription1: "Es requereix l'inici de sessió per poder veure totes les notes i el contingut que has creat. Amb això esperem evitar que els rastrejadors recopilin informació."
+  requireSigninToViewContentsDescription2: "També es desactivaran les vistes prèvies d'URLS (OGP), la incrustació a pàgines web i la visualització des de servidors que no admetin la citació de notes."
+  requireSigninToViewContentsDescription3: "Aquestes restriccions pot ser que no s'apliquin als continguts federats en servidors remots."
+  makeNotesFollowersOnlyBefore: "Permetre que les notes antigues només es mostrin als seguidors."
+  makeNotesFollowersOnlyBeforeDescription: "Mentre aquesta funció estigui activada, les notes que hagin passat la data i hora fixada o hagi passat els temps establert seran visibles només per als teus seguidors. Quan es desactivi, també es restableix l'estat públic de la nota."
+  makeNotesHiddenBefore: "Fes que les notes antigues siguin privades"
+  makeNotesHiddenBeforeDescription: "Mentres aquesta funció estigui activada les notes que hagin superat una data i hora fixada o hagi passat el temps establert només seran visibles per a tu. Si la desactives es restablirà també l'estat públic de les notes."
+  mayNotEffectForFederatedNotes: "Això pot ser que no afecti les notes federades."
+  notesHavePassedSpecifiedPeriod: "Notes publicades durant un període de temps especificat."
+  notesOlderThanSpecifiedDateAndTime: "Notes més antigues de la data i temps especificat "
+_abuseUserReport:
+  forward: "Reenviar "
+  forwardDescription: "Reenvia l'informe a una altra instància com un compte del sistema anònima."
+  resolve: "Solució "
+  accept: "Acceptar "
+  reject: "Rebutjar"
+  resolveTutorial: "Si l'informe és legítim selecciona \"Acceptar\" per resoldre'l positivament. Però si l'informe no és legítim selecciona \"Rebutjar\" per resoldre'l negativament."
 _delivery:
+  status: "Estat d'entrega "
   stop: "Suspés"
+  resume: "Torna a enviar"
   _type:
     none: "S'està publicant"
+    manuallySuspended: "Suspendre manualment"
+    goneSuspended: "Servidor suspès perquè el servidor s'ha esborrat"
+    autoSuspendedForNotResponding: "Servidor suspès perquè el servidor no respon"
 _bubbleGame:
   howToPlay: "Com es juga"
+  hold: "Mantenir"
+  _score:
+    score: "Puntuació "
+    scoreYen: "Diners guanyats"
+    highScore: "Millor puntuació "
+    maxChain: "Nombre màxim de combos"
+    yen: "{yen}Ien"
+    estimatedQty: "{qty}peces"
+    scoreSweets: "{onigiriQtyWithUnit}ongiris"
   _howToPlay:
     section1: "Ajusta la posició i deixa caure l'objecte dintre la caixa."
     section2: "Quan dos objectes del mateix tipus es toquen, canviaran en un objecte diferent i guanyares punts."
@@ -1344,6 +1452,10 @@ _serverSettings:
   fanoutTimelineDescription: "Quan es troba activat millora bastant el rendiment quan es recuperen les línies de temps i redueix la carrega de la base de dades. Com a contrapunt, l'ús de memòria de Redis es veurà incrementada. Considera d'estabilitat aquesta opció en cas de tenir un servidor amb poca memòria o si tens problemes de inestabilitat."
   fanoutTimelineDbFallback: "Carregar de la base de dades"
   fanoutTimelineDbFallbackDescription: "Quan s'activa, la línia de temps fa servir la base de dades per consultes adicionals si la línia de temps no es troba a la memòria cau. Si és desactiva la càrrega del servidor és veure reduïda, però també és reduirà el nombre de línies de temps que és poden obtenir."
+  reactionsBufferingDescription: "Quan s'activa aquesta opció millora bastant el rendiment en recuperar les línies de temps reduint la càrrega de la base. Com a contrapunt, augmentarà  l'ús de memòria de Redís. Desactiva aquesta opció en cas de tenir un servidor amb poca memòria o si tens problemes d'inestabilitat."
+  inquiryUrl: "URL de consulta "
+  inquiryUrlDescription: "Escriu adreça URL per al formulari de consulta per al mantenidor del servidor o una pàgina web amb el contacte d'informació."
+  thisSettingWillAutomaticallyOffWhenModeratorsInactive: "Si no es detecta activitat per part del moderador durant un període de temps, aquesta opció es desactiva automàticament per evitar el correu brossa."
 _accountMigration:
   moveFrom: "Migrar un altre compte a aquest"
   moveFromSub: "Crear un àlies per un altre compte"
@@ -1651,6 +1763,7 @@ _role:
     gtlAvailable: "Pot veure la línia de temps global"
     ltlAvailable: "Pot veure la línia de temps local"
     canPublicNote: "Pot enviar notes públiques"
+    mentionMax: "Nombre màxim de mencions a una nota"
     canInvite: "Pot crear invitacions a la instància "
     inviteLimit: "Límit d'invitacions "
     inviteLimitCycle: "Temps de refresc de les invitacions"
@@ -1659,6 +1772,7 @@ _role:
     canManageAvatarDecorations: "Gestiona les decoracions dels avatars "
     driveCapacity: "Capacitat del disc"
     alwaysMarkNsfw: "Marca sempre els fitxers com a sensibles"
+    canUpdateBioMedia: "Permet l'edició d'una icona o un bàner"
     pinMax: "Nombre màxim de notes fixades"
     antennaMax: "Nombre màxim d'antenes"
     wordMuteMax: "Nombre màxim de caràcters permesos a les paraules silenciades"
@@ -1673,9 +1787,20 @@ _role:
     canSearchNotes: "Pot cercar notes"
     canUseTranslator: "Pot fer servir el traductor"
     avatarDecorationLimit: "Nombre màxim de decoracions que es poden aplicar els avatars"
+    canImportAntennas: "Autoritza la importació d'antenes "
+    canImportBlocking: "Autoritza la importació de bloquejats"
+    canImportFollowing: "Autoritza la importació de seguidors"
+    canImportMuting: "Autoritza la importació de silenciats"
+    canImportUserLists: "Autoritza la importació de llistes d'usuaris "
   _condition:
+    roleAssignedTo: "Assignat a rols manuals"
     isLocal: "Usuari local"
     isRemote: "Usuari remot"
+    isCat: "Usuaris gats"
+    isBot: "Usuaris bots"
+    isSuspended: "Usuari suspès"
+    isLocked: "Comptes privats"
+    isExplorable: "Fes que el compte aparegui a les cerques"
     createdLessThan: "Han passat menys de X a passat des de la creació del compte"
     createdMoreThan: "Han passat més de X des de la creació del compte"
     followersLessThanOrEq: "Té menys de X seguidors"
@@ -1745,6 +1870,7 @@ _plugin:
   installWarn: "Si us plau, no instal·lis afegits que no siguin de confiança."
   manage: "Gestionar els afegits"
   viewSource: "Veure l'origen "
+  viewLog: "Mostra el registre"
 _preferencesBackups:
   list: "Llista de còpies de seguretat"
   saveNew: "Fer una còpia de seguretat nova"
@@ -1774,6 +1900,8 @@ _aboutMisskey:
   contributors: "Col·laboradors principals"
   allContributors: "Tots els col·laboradors "
   source: "Codi font"
+  original: "Original"
+  thisIsModifiedVersion: "En {name} fa servir una versió modificada de Misskey."
   translation: "Tradueix Misskey"
   donate: "Fes un donatiu a Misskey"
   morePatrons: "També agraïm el suport d'altres col·laboradors que no surten en aquesta llista. Gràcies! 🥰"
@@ -1881,7 +2009,6 @@ _theme:
     buttonBg: "Fons botó "
     buttonHoverBg: "Fons botó (en passar-hi per sobre)"
     inputBorder: "Contorn del cap d'introducció "
-    listItemHoverBg: "Fons dels elements d'una llista"
     driveFolderBg: "Fons de la carpeta Disc"
     wallpaperOverlay: "Superposició del fons de pantalla "
     badge: "Insígnia "
@@ -1893,8 +2020,6 @@ _sfx:
   note: "Notes"
   noteMy: "Nota (per mi)"
   notification: "Notificacions"
-  antenna: "Antenes"
-  channel: "Notificacions dels canals"
   reaction: "Quan se selecciona una reacció "
 _soundSettings:
   driveFile: "Fer servir un fitxer d'àudio del disc"
@@ -1903,6 +2028,7 @@ _soundSettings:
   driveFileTypeWarnDescription: "Seleccionar un fitxer d'àudio "
   driveFileDurationWarn: "L'àudio és massa llarg"
   driveFileDurationWarnDescription: "Els àudios molt llargs pot interrompre l'ús de Misskey. Vols continuar?"
+  driveFileError: "El so no es pot carregar. Canvia la configuració"
 _ago:
   future: "Futur "
   justNow: "Ara mateix"
@@ -1955,6 +2081,7 @@ _2fa:
   backupCodesDescription: "Si l'aplicació d'autenticació no es pot utilitzar, es pot accedir al compte utilitzant els següents codis de còpia de seguretat. Assegura't de mantenir aquests codis en un lloc segur. Cada codi es pot utilitzar només una vegada."
   backupCodeUsedWarning: "Es va utilitzar un codi de còpia de seguretat. Si l'aplicació de certificació està disponible, reconfigura l'aplicació d'autenticació tan aviat com sigui possible."
   backupCodesExhaustedWarning: "Es van utilitzar tots els codis de còpia de seguretat. Si no es pot utilitzar l'aplicació d'autenticació, ja no es pot accedir al compte. Torna a registrar l'aplicació d'autenticació."
+  moreDetailedGuideHere: "Aquí tens una guia al detall"
 _permissions:
   "read:account": "Veure la informació del compte."
   "write:account": "Editar la informació del compte."
@@ -2028,22 +2155,76 @@ _permissions:
   "read:admin:emoji": "Veure emojis"
   "write:admin:queue": "Gestionar la cua de feines"
   "read:admin:queue": "Veure la cua de feines"
+  "write:admin:promo": "Gestiona les notes promocionals"
+  "write:admin:drive": "Gestiona el disc de l'usuari"
+  "read:admin:drive": "Veure la informació del disc de l'usuari"
+  "read:admin:stream": "Fes servir l'API sobre Websocket per l'administració"
+  "write:admin:ad": "Gestiona la publicitat"
+  "read:admin:ad": "Veure anuncis"
+  "write:invite-codes": "Crear codis d'invitació"
+  "read:invite-codes": "Obtenir codis d'invitació"
+  "write:clip-favorite": "Gestionar els clips favorits"
+  "read:clip-favorite": "Veure clips favorits"
+  "read:federation": "Veure dades de federació"
+  "write:report-abuse": "Informar d'un abús"
+_auth:
+  shareAccessTitle: "Concedeix permisos a l'aplicació"
+  shareAccess: "Vols que {name} pugui accedir al vostre compte?"
+  shareAccessAsk: "Segur que vols que aquesta aplicació pugui accedir al vostre compte?"
+  permission: "{name} demana els següents permisos"
+  permissionAsk: "Aquesta aplicació demana els següents permisos"
+  pleaseGoBack: "Si us plau, torna a l'aplicació"
+  callback: "Tornant a l'aplicació"
+  accepted: "Accés garantit"
+  denied: "Accés denegat"
+  scopeUser: "Opera com si fossis aquest usuari"
+  pleaseLogin: "Si us plau, identificat per autoritzar l'aplicació."
+  byClickingYouWillBeRedirectedToThisUrl: "Si es garanteix l'accés, seràs redirigit automàticament a la següent adreça URL"
 _antennaSources:
   all: "Totes les publicacions"
   homeTimeline: "Publicacions dels usuaris seguits"
   users: "Publicacions d'usuaris específics"
   userList: "Publicacions d'una llista d'usuaris"
+  userBlacklist: "Totes les notes excepte les d'un o alguns usuaris especificats"
+_weekday:
+  sunday: "Diumenge"
+  monday: "Dilluns"
+  tuesday: "Dimarts"
+  wednesday: "Dimecres"
+  thursday: "Dijous"
+  friday: "Divendres"
+  saturday: "Dissabte"
 _widgets:
   profile: "Perfil"
   instanceInfo: "Informació del fitxer d'instal·lació"
+  memo: "Notes adhesives"
   notifications: "Notificacions"
   timeline: "Línia de temps"
+  calendar: "Calendari"
+  trends: "Tendència"
+  clock: "Rellotge"
+  rss: "Lector RSS"
+  rssTicker: "RSS ticker"
   activity: "Activitat"
+  photos: "Fotografies"
+  digitalClock: "Rellotge digital"
+  unixClock: "Rellotge UNIX"
   federation: "Federació"
+  instanceCloud: "Núvol d'instàncies"
+  postForm: "Formulari de publicació"
+  slideshow: "Presentació"
   button: "Botó "
+  onlineUsers: "Usuaris actius"
   jobQueue: "Cua de tasques"
+  serverMetric: "Mètriques del servidor"
+  aiscript: "Consola AiScript"
+  aiscriptApp: "Aplicació AiScript"
+  aichan: "Ai"
+  userList: "Llistat d'usuaris"
   _userList:
     chooseList: "Tria una llista"
+  clicker: "Clicker"
+  birthdayFollowings: "Usuaris que fan l'aniversari avui"
 _cw:
   hide: "Amagar"
   show: "Carregar més"
@@ -2107,27 +2288,79 @@ _profile:
   changeBanner: "Canviar el bàner "
   verifiedLinkDescription: "Escrivint una adreça URL que enllaci a aquest perfil, una icona de propietat verificada es mostrarà al costat del camp."
   avatarDecorationMax: "Pot afegir un màxim de {max} decoracions."
+  followedMessage: "Missatge als nous seguidors"
+  followedMessageDescription: "Es pot configurar un missatge curt que es mostra a l'altra persona quan comença a seguir-te."
+  followedMessageDescriptionForLockedAccount: "Si comencen a seguir-te es mostra un missatge de quan es permet aquesta sol·licitud. "
 _exportOrImport:
   allNotes: "Totes les publicacions"
+  favoritedNotes: "Notes preferides"
   clips: "Retalls"
   followingList: "Seguint"
   muteList: "Silencia"
   blockingList: "Bloqueja"
   userLists: "Llistes"
+  excludeMutingUsers: "Exclou usuaris silenciats"
+  excludeInactiveUsers: "Exclou usuaris inactius"
+  withReplies: "Inclou a la línia de temps les respostes d'usuaris importats"
 _charts:
   federation: "Federació"
+  apRequest: "Peticions"
+  usersIncDec: "Diferència entre el nombre d'usuaris"
+  usersTotal: "Nombre total d'usuaris"
+  activeUsers: "Usuaris actius"
+  notesIncDec: "Diferència entre el nombre de notes"
+  localNotesIncDec: "Diferencia en el nombre de notes locals"
+  remoteNotesIncDec: "Diferencia en el nombre de notes remotes"
+  notesTotal: "Nombre total de notes"
+  filesIncDec: "Diferencia en el nombre de fitxers"
+  filesTotal: "Nombre total de fitxers"
+  storageUsageIncDec: "Diferencia en l'emmagatzematge usat"
+  storageUsageTotal: "Emmagatzematge usat"
+_instanceCharts:
+  requests: "Peticions"
+  users: "Diferència entre el nombre d'usuaris"
+  usersTotal: "Usuaris totals acumulats"
+  notes: "Diferència entre el nombre de notes"
+  notesTotal: "Notes totals acumulades"
+  ff: "Diferència en nombre d'usuaris seguits / seguidors"
+  ffTotal: "Nombre total acumulat d'usuaris seguits / seguidors"
+  cacheSize: "Diferència a la mida de la memòria cau"
+  cacheSizeTotal: "Total acumulat de la mida de la memòria cau"
+  files: "Diferència al nombre d'arxius"
+  filesTotal: "Nombre acumulatiu de fitxers"
 _timelines:
   home: "Inici"
   local: "Local"
   social: "Social"
   global: "Global"
 _play:
+  new: "Crear un guió"
+  edit: "Editar guió"
+  created: "Guió creat"
+  updated: "Guió editat"
+  deleted: "Guió esborrat"
+  pageSetting: "Configuració del guió"
+  editThisPage: "Edita aquest guió"
   viewSource: "Veure l'origen "
+  my: "Els meus guions"
+  liked: "Guions que m'han agradat"
   featured: "Popular"
   title: "Títol "
   script: "Script"
   summary: "Descripció"
+  visibilityDescription: ""
 _pages:
+  newPage: "pa"
+  editPage: "Editar la pàgina"
+  readPage: "Veure el codi font d'aquesta pàgina"
+  created: "La pàgina ha sigut creada correctament"
+  updated: "La pàgina s'ha editat correctament"
+  deleted: "La pàgina s'ha esborrat sense problemes"
+  pageSetting: "Configuració de la pàgina"
+  nameAlreadyExists: "L'adreça URL de la pàgina ja existeix"
+  invalidNameTitle: "L'adreça URL de la pàgina no és vàlida"
+  invalidNameText: "Assegurat que el títol de la pàgina no és buit"
+  editThisPage: "Editar la pàgina"
   viewSource: "Veure l'origen "
   viewPage: "Veure les teves pàgines "
   like: "M'agrada "
@@ -2150,6 +2383,7 @@ _pages:
   eyeCatchingImageSet: "Escull una miniatura"
   eyeCatchingImageRemove: "Esborrar la miniatura"
   chooseBlock: "Afegeix un bloc"
+  enterSectionTitle: "Escriu el títol de la secció"
   selectType: "Seleccionar tipus"
   contentBlocks: "Contingut"
   inputBlocks: "Entrada "
@@ -2160,6 +2394,8 @@ _pages:
     section: "Secció "
     image: "Imatges"
     button: "Botó "
+    dynamic: "Blocs dinàmics"
+    dynamicDescription: "Aquest bloc és antic. Ara en endavant fes servir {play}"
     note: "Incorporar una Nota"
     _note:
       id: "ID de la publicació"
@@ -2189,29 +2425,55 @@ _notification:
   sendTestNotification: "Enviar notificació de prova"
   notificationWillBeDisplayedLikeThis: "Les notificacions és veure'n així "
   reactedBySomeUsers: "Han reaccionat {n} usuaris"
+  likedBySomeUsers: "A {n} usuaris els hi agrada la teva nota"
   renotedBySomeUsers: "L'han impulsat {n} usuaris"
+  followedBySomeUsers: "Et segueixen {n} usuaris"
+  flushNotification: "Netejar notificacions"
+  exportOfXCompleted: "Completada l'exportació de {x}"
+  login: "Algú ha iniciat sessió "
   _types:
     all: "Tots"
+    note: "Notes noves"
     follow: "Seguint"
     mention: "Menció"
+    reply: "Respostes"
     renote: "Renotar"
     quote: "Citar"
     reaction: "Reaccions"
+    pollEnded: "Enquesta terminada"
+    receiveFollowRequest: "Rebuda una petició de seguiment"
+    followRequestAccepted: "Petició de seguiment acceptada"
+    roleAssigned: "Rol donat"
+    achievementEarned: "Assoliment desbloquejat"
+    exportCompleted: "Exportació completada"
+    login: "Iniciar sessió"
+    test: "Prova la notificació"
+    app: "Notificacions d'aplicacions"
   _actions:
     followBack: "t'ha seguit també"
     reply: "Respondre"
     renote: "Renotar"
 _deck:
+  alwaysShowMainColumn: "Mostrar sempre la columna principal"
   columnAlign: "Alinea les columnes"
   addColumn: "Afig una columna"
+  newNoteNotificationSettings: "Configuració de notificacions per a notes noves"
+  configureColumn: "Configuració de columnes"
   swapLeft: "Mou a l’esquerra"
   swapRight: "Mou a la dreta"
   swapUp: "Mou cap amunt"
   swapDown: "Mou cap avall"
+  stackLeft: "Pila a la columna esquerra"
   popRight: "Col·loca a la dreta"
   profile: "Perfil"
   newProfile: "Perfil nou"
   deleteProfile: "Elimina el perfil"
+  introduction: "Crea la interfície perfecta posant les columnes allà on vulguis!"
+  introduction2: "Fes clic al botó + de la dreta per afegir noves columnes sempre que vulguis."
+  widgetsIntroduction: "Selecciona \"Editar ginys\" a la columna del menú i afegeix un."
+  useSimpleUiForNonRootPages: "Usa una interfície senzilla per a les pàgines navegades"
+  usedAsMinWidthWhenFlexible: "L'amplada mínima es farà servir quan \"Ajust automàtic de l'amplada\" estigui activat"
+  flexible: "Ajust automàtic de l'amplada"
   _columns:
     main: "Principal"
     widgets: "Ginys"
@@ -2222,17 +2484,84 @@ _deck:
     channel: "Canals"
     mentions: "Mencions"
     direct: "Publicacions directes"
+    roleTimeline: "Línia de temps dels rols"
+_dialog:
+  charactersExceeded: "Has arribat al màxim de caràcters! Actualment és {current} de {max}"
+  charactersBelow: "Ets per sota del mínim de caràcters! Actualment és {current} de {min}"
+_disabledTimeline:
+  title: "Línia de tems desactivada"
+  description: "No pots fer servir aquesta línia de temps amb els teus rols actuals."
+_drivecleaner:
+  orderBySizeDesc: "Mida del fitxer descendent"
+  orderByCreatedAtAsc: "Data ascendent"
 _webhookSettings:
+  createWebhook: "Crear un Webhook"
+  modifyWebhook: "Modificar un Webhook"
   name: "Nom"
+  secret: "Secret"
+  trigger: "Activador"
   active: "Activat"
+  _events:
+    follow: "Quan se segueix a un usuari"
+    followed: "Quan et segueixen"
+    note: "Quan es publica una nota"
+    reply: "Quan es rep una resposta"
+    renote: "Quan es renoti"
+    reaction: "Quan es rep una reacció "
+    mention: "Quan et mencionen"
+  _systemEvents:
+    abuseReport: "Quan reps un nou informe de moderació "
+    abuseReportResolved: "Quan resols un informe de moderació "
+    userCreated: "Quan es crea un usuari"
+    inactiveModeratorsWarning: "Quan el compte d'un moderador no té activitat durant un temps"
+    inactiveModeratorsInvitationOnlyChanged: "Quan el compte d'un moderador no té activitat durant un temps, i el servidor es canvia a registre per invitacions"
+  deleteConfirm: "Segur que vols esborrar el webhook?"
+  testRemarks: "Si feu clic al botó a la dreta de l'interruptor, podeu enviar un webhook de prova amb dades dummy."
+_abuseReport:
+  _notificationRecipient:
+    createRecipient: "Afegeix un destinatari a l'informe de moderació "
+    modifyRecipient: "Editar un destinatari en l'informe de moderació "
+    recipientType: "Tipus de notificació "
+    _recipientType:
+      mail: "Correu electrònic"
+      webhook: "Webhook"
+      _captions:
+        mail: "Enviar un correu electrònic a tots els moderadors quan es rep un informe de moderació "
+        webhook: "Enviar una notificació al SystemWebhook quan es rebi o es resolgui un informe de moderació "
+    keywords: "Paraules clau"
+    notifiedUser: "Usuaris que s'han de notificar "
+    notifiedWebhook: "Webhook que s'ha de fer servir"
+    deleteConfirm: "Segur que vols esborrar el destinatari de l'informe de moderació?"
 _moderationLogTypes:
+  createRole: "Rol creat"
+  deleteRole: "Rol esborrat"
+  updateRole: "Rol actualitzat"
+  assignRole: "Assignat al rol"
+  unassignRole: "Esborrat del rol"
   suspend: "Suspèn"
+  unsuspend: "Suspensió treta"
+  addCustomEmoji: "Afegit emoji personalitzat"
+  updateCustomEmoji: "Actualitzat emoji personalitzat"
+  deleteCustomEmoji: "Esborrat emoji personalitzat"
+  updateServerSettings: "Configuració del servidor actualitzada"
+  updateUserNote: "Nota de moderació actualitzada"
+  deleteDriveFile: "Fitxer esborrat"
+  deleteNote: "Nota esborrada"
+  createGlobalAnnouncement: "Anunci global creat"
+  createUserAnnouncement: "Anunci individual creat"
+  updateGlobalAnnouncement: "Anunci global actualitzat"
+  updateUserAnnouncement: "Anunci individual actualitzat "
+  deleteGlobalAnnouncement: "Anunci global esborrat"
+  deleteUserAnnouncement: "Anunci individual esborrat "
   resetPassword: "Restableix la contrasenya"
   suspendRemoteInstance: "Servidor remot suspès "
   unsuspendRemoteInstance: "S'ha tret la suspensió del servidor remot"
+  updateRemoteInstanceNote: "Nota de moderació de la instància remota actualitzada"
   markSensitiveDriveFile: "Fitxer marcat com a sensible"
   unmarkSensitiveDriveFile: "S'ha tret la marca de sensible del fitxer"
   resolveAbuseReport: "Informe resolt"
+  forwardAbuseReport: "Informe reenviat"
+  updateAbuseReportNote: "Nota de moderació d'un informe actualitzat"
   createInvitation: "Crear codi d'invitació "
   createAd: "Anunci creat"
   deleteAd: "Anunci esborrat"
@@ -2242,6 +2571,16 @@ _moderationLogTypes:
   deleteAvatarDecoration: "S'ha esborrat la decoració de l'avatar "
   unsetUserAvatar: "Esborrar l'avatar d'aquest usuari"
   unsetUserBanner: "Esborrar el bàner d'aquest usuari"
+  createSystemWebhook: "Crear un SystemWebhook"
+  updateSystemWebhook: "Actualitzar SystemWebhook"
+  deleteSystemWebhook: "Esborrar SystemWebhook"
+  createAbuseReportNotificationRecipient: "Crear un destinatari per l'informe de moderació "
+  updateAbuseReportNotificationRecipient: "Actualitzar destinatari per l'informe de moderació "
+  deleteAbuseReportNotificationRecipient: "Esborrar destinatari de l'informe de moderació "
+  deleteAccount: "Esborrar el compte "
+  deletePage: "Esborrar la pàgina"
+  deleteFlash: "Esborrar el guió"
+  deleteGalleryPost: "Esborrar la publicació de la galeria"
 _fileViewer:
   title: "Detall del fitxer"
   type: "Tipus de fitxer"
@@ -2268,5 +2607,133 @@ _externalResourceInstaller:
   _errors:
     _invalidParams:
       title: "Paràmetres no vàlids "
+      description: "No hi ha suficient informació per carregar les dades del lloc extern. Confirma l'URL que hi ha escrita."
+    _resourceTypeNotSupported:
+      title: "El recurs extern no està suportat."
+      description: "Aquesta mena de recurs no està suportat. Contacta amb l'administrador."
+    _failedToFetch:
+      title: "Ha fallat l'obtenció de dades"
+      fetchErrorDescription: "Ha aparegut un error comunicant-se amb el lloc extern. Si després d'intentar-ho un altre cop no es resol, contacta amb l'administrador."
+      parseErrorDescription: "Ha aparegut un error processant les dades carregades del lloc extern. Contacta amb l'administrador."
+    _hashUnmatched:
+      title: "Ha fallat la verificació de les dades"
+      description: "Ha aparegut un error verificant les dades obtingudes. Com a mesura de seguretat la instal·lació no pot continuar. Contacta amb l'administrador."
+    _pluginParseFailed:
+      title: "Error d'AiScript"
+      description: "Les dades sol·licitades s'han obtingut correctament, però hem trobat un error durant el processament d'AiScript. Contacta amb l'autor de l'afegit. Detalls de l'error es pot veure a la consola JavaScript."
+    _pluginInstallFailed:
+      title: "La instal·lació de l'afegit a fallat"
+      description: "Ha aparegut un error durant la instal·lació de l'afegit. Intenta-ho una altra vegada. El detall de l'error es pot veure a la consola JavaScript."
+    _themeParseFailed:
+      title: "Ha fallat el processament del tema"
+      description: "Les dades sol·licitades s'han obtingut correctament, però hem trobat un error durant el processament del tema. Contacta amb l'autor de l'afegit. Detalls de l'error es pot veure a la consola JavaScript."
+    _themeInstallFailed:
+      title: "La instal·lació del tema a fallat"
+      description: "Ha aparegut un error durant la instal·lació del tema. Intenta-ho una altra vegada. El detall de l'error es pot veure a la consola JavaScript."
+_dataSaver:
+  _media:
+    title: "Carregant multimèdia "
+    description: "Desactiva la càrrega automàtica d'imatges i vídeos. Les imatges i els vídeos amagats es carregaran quan es faci clic a sobre."
+  _avatar:
+    title: "Avatars animats"
+    description: "Detenir l'animació dels avatars animats. Les imatges animades solen tenir un pes més gran que les imatges normals, reduint el tràfic disponible."
+  _urlPreview:
+    title: "Miniatures vista prèvia de l'URL"
+    description: "Les imatges en miniatura que serveixen com a vista prèvia de les URLs no es tornaran a carregar."
+  _code:
+    title: "Ressaltat del codi "
+    description: "Quan s'utilitza codi MFM, no es llegeix fins que es copiï. En els punts destacats del codi s'han de llegir els fitxers definits per a cada llengua que resulti alt, però no es poden llegir automàticament, per la qual cosa es poden reduir les quantitats de comunicació."
+_hemisphere:
+  N: "Hemisferi Nord "
+  S: "Hemisferi Sud"
+  caption: "El fan servir alguns clients per determinar l'estació de l'any."
 _reversi:
+  reversi: "Reversi"
+  gameSettings: "Opcions del joc"
+  chooseBoard: "Escull un taulell"
+  blackOrWhite: "Negres/Blanques"
+  blackIs: "{name} juga amb negres "
+  rules: "Regles"
+  thisGameIsStartedSoon: "El joc començarà en breu"
+  waitingForOther: "Esperant la tirada de l'oponent "
+  waitingForMe: "Esperant el teu torn"
+  waitingBoth: "Prepara't "
+  ready: "Preparat "
+  cancelReady: " No preparat "
+  opponentTurn: "Torn de l'oponent "
+  myTurn: "El teu torn"
+  turnOf: "Li toca a {name}"
+  pastTurnOf: "Torn de {name}"
+  surrender: "Rendeix-te"
+  surrendered: "T'has rendit"
+  timeout: "Temps esgotat"
+  drawn: "Empat"
+  won: "{name} ha guanyat"
+  black: "Negres"
+  white: "Blanques"
   total: "Total"
+  turnCount: "Torn {count}"
+  myGames: "Jugades"
+  allGames: "Totes les jugades"
+  ended: "Acabat"
+  playing: "Jugant"
+  isLlotheo: "Qui tingui menys pedres guanya (Llotheo)"
+  loopedMap: "Mapa de recursiu"
+  canPutEverywhere: "Les fitxes es poden posar a qualsevol lloc"
+  timeLimitForEachTurn: "Temps límit per jugada"
+  freeMatch: "Partida lliure"
+  lookingForPlayer: "Buscant contrincant..."
+  gameCanceled: "La partida s'ha cancel·lat "
+  shareToTlTheGameWhenStart: "Compartir la partida a la línia de temps quan comenci"
+  iStartedAGame: "La partida ha començat! #MisskeyReversi"
+  opponentHasSettingsChanged: "L'oponent h canviat la seva configuració "
+  allowIrregularRules: "Regles irregulars (totalment lliure)"
+  disallowIrregularRules: "Sense regles irregulars"
+  showBoardLabels: "Mostrar el número de línia i columna al tauler de joc"
+  useAvatarAsStone: "Fer servir els avatars dels usuaris com a fitxes"
+_offlineScreen:
+  title: "Fora de línia - No es pot connectar amb el servidor"
+  header: "Impossible connectar amb el servidor"
+_urlPreviewSetting:
+  title: "Configuració per a la previsualització de l'URL"
+  enable: "Activa la previsualització de l'URL"
+  timeout: "Temps màxim per carregar la previsualització de l'URL (ms)"
+  timeoutDescription: "Si l'obtenció de la previsualització triga més que el temps establert, no es generarà la vista prèvia."
+  maximumContentLength: "Longitud màxima del contingut (bytes)"
+  maximumContentLengthDescription: "Si la màxima longitud és més gran que aquest valor, la previsualització no es generarà."
+  requireContentLength: "Generar la previsualització només si es pot obtenir la longitud màxima "
+  requireContentLengthDescription: "Si l'altre servidor no proporciona la longitud màxima, la previsualització no es generarà."
+  userAgent: "User-Agent"
+  userAgentDescription: "Estableix l'User-Agent que és farà servir per a la recuperació de la vista prèvia. Si és deixa en blanc es farà servir l'User-Agent per defecte."
+  summaryProxy: "Proxy endpoints per generar vistes prèvies"
+  summaryProxyDescription: "La vista prèvia es genera fent servir Summaly proxy, no la genera el mateix Misskey."
+  summaryProxyDescription2: "Els següents paràmetres són passats al proxy com cadenes de consulta. Si el proxy no els admet, s'ignoren els valors configurats."
+_mediaControls:
+  pip: "Imatge sobre impressionada "
+  playbackRate: "Velocitat de reproducció "
+  loop: "Reproducció en bucle"
+_contextMenu:
+  title: "Menú contextual"
+  app: "Aplicació "
+  appWithShift: "Aplicació amb la tecla shift"
+  native: "Interfície del navegador"
+_embedCodeGen:
+  title: "Personalitza el codi per incrustar"
+  header: "Mostrar la capçalera"
+  autoload: "Carregar automàticament (no recomanat)"
+  maxHeight: "Alçada màxima"
+  maxHeightDescription: "0 anul·la la configuració màxima. Per evitar que continuï creixent verticalment, especifiqui qualsevol valor."
+  maxHeightWarn: "El límit màxim d'alçada és nul (0). Si això no és un canvi previst, estableix el màxim d'alçada a un cert valor."
+  previewIsNotActual: "La visualització és diferent de la que es mostra quan s'implanta."
+  rounded: "Angle recte"
+  border: "Afegeix un marc al contenidor"
+  applyToPreview: "Aplica a la vista prèvia"
+  generateCode: "Crea el codi per incrustar"
+  codeGenerated: "Codi generat"
+  codeGeneratedDescription: "Si us plau, enganxeu el codi generat al lloc web."
+_selfXssPrevention:
+  warning: "Advertència "
+  title: "\"Enganxa qualsevol cosa en aquesta finestra\"  És tot un engany."
+  description1: "Si posa alguna cosa al seu compte, un usuari malintencionat podria segrestar-la o robar-li les dades."
+  description2: "Si no entens que estàs fent %cpara ara mateix i tanca la finestra."
+  description3: "Per obtenir més informació. {link}"
diff --git a/locales/cs-CZ.yml b/locales/cs-CZ.yml
index c8a0b0cb28..caf6d6e163 100644
--- a/locales/cs-CZ.yml
+++ b/locales/cs-CZ.yml
@@ -471,7 +471,6 @@ uiLanguage: "Jazyk uživatelského rozhraní"
 aboutX: "O {x}"
 emojiStyle: "Styl emoji"
 native: "Výchozí"
-disableDrawer: "Nepoužívat šuplíkové menu"
 showNoteActionsOnlyHover: "Zobrazit akce poznámky jenom při naběhnutí myši"
 noHistory: "Žádná historie"
 signinHistory: "Historie přihlášení"
@@ -658,10 +657,7 @@ abuseReported: "Nahlášení bylo odesláno. Děkujeme převelice."
 reporter: "Nahlásil"
 reporteeOrigin: "Původ nahlášení"
 reporterOrigin: "Původ nahlasovače"
-forwardReport: "Přeposlat nahlášení do vzdálené instance"
-forwardReportIsAnonymous: "Místo vašeho účtu se ve vzdálené instanci zobrazí anonymní systémový účet jako nahlašovač."
 send: "Odeslat"
-abuseMarkAsResolved: "Označit nahlášení jako vyřešené"
 openInNewTab: "Otevřít v nové kartě"
 openInSideView: "Otevřít v bočním panelu"
 defaultNavigationBehaviour: "Výchozí chování navigace"
@@ -1633,7 +1629,6 @@ _theme:
     buttonBg: "Pozadí tlačítka"
     buttonHoverBg: "Pozadí tlačítka (Hover)"
     inputBorder: "Ohraničení vstupního pole"
-    listItemHoverBg: "Pozadí položky seznamu (Hover)"
     driveFolderBg: "Pozadí složky disku"
     wallpaperOverlay: "Překrytí tapety"
     badge: "Odznak"
@@ -1645,8 +1640,6 @@ _sfx:
   note: "Poznámky"
   noteMy: "Moje poznámka"
   notification: "Oznámení"
-  antenna: "Antény"
-  channel: "Oznámení kanálu"
 _ago:
   future: "Budoucí"
   justNow: "Teď"
@@ -1965,6 +1958,7 @@ _notification:
     receiveFollowRequest: "Obdržené žádosti o sledování"
     followRequestAccepted: "Přijaté žádosti o sledování"
     achievementEarned: "Úspěch odemčen"
+    login: "Přihlásit se"
     app: "Oznámení z propojených aplikací"
   _actions:
     followBack: "vás začal sledovat zpět"
@@ -2012,7 +2006,6 @@ _webhookSettings:
   createWebhook: "Vytvořit Webhook"
   name: "Jméno"
   secret: "Tajné"
-  events: "Události Webhook"
   active: "Zapnuto"
   _events:
     follow: "Při sledování uživatele"
@@ -2022,6 +2015,10 @@ _webhookSettings:
     renote: "Při renotaci poznámky"
     reaction: "Při obdržení reakce"
     mention: "Při zmínce"
+_abuseReport:
+  _notificationRecipient:
+    _recipientType:
+      mail: "Email"
 _moderationLogTypes:
   suspend: "Zmrazit"
   resetPassword: "Resetovat heslo"
diff --git a/locales/de-DE.yml b/locales/de-DE.yml
index 9e42e01252..4e2bd06934 100644
--- a/locales/de-DE.yml
+++ b/locales/de-DE.yml
@@ -491,7 +491,6 @@ uiLanguage: "Sprache der Benutzeroberfläche"
 aboutX: "Über {x}"
 emojiStyle: "Emoji-Stil"
 native: "Nativ"
-disableDrawer: "Keine ausfahrbaren Menüs verwenden"
 showNoteActionsOnlyHover: "Notizmenü nur bei Mouseover anzeigen"
 noHistory: "Kein Verlauf gefunden"
 signinHistory: "Anmeldungsverlauf"
@@ -687,10 +686,7 @@ abuseReported: "Deine Meldung wurde versendet. Vielen Dank."
 reporter: "Melder"
 reporteeOrigin: "Herkunft des Gemeldeten"
 reporterOrigin: "Herkunft des Meldenden"
-forwardReport: "Meldung an fremde Instanz weiterleiten"
-forwardReportIsAnonymous: "Anstatt deines Benutzerkontos wird bei der fremden Instanz ein anonymes Systemkonto als Melder angezeigt."
 send: "Senden"
-abuseMarkAsResolved: "Meldung als gelöst markieren"
 openInNewTab: "In neuem Tab öffnen"
 openInSideView: "In Seitenansicht öffnen"
 defaultNavigationBehaviour: "Standardnavigationsverhalten"
@@ -1788,7 +1784,6 @@ _theme:
     buttonBg: "Hintergrund von Schaltflächen"
     buttonHoverBg: "Hintergrund von Schaltflächen (Mouseover)"
     inputBorder: "Rahmen von Eingabefeldern"
-    listItemHoverBg: "Hintergrund von Listeneinträgen (Mouseover)"
     driveFolderBg: "Hintergrund von Drive-Ordnern"
     wallpaperOverlay: "Hintergrundbild-Overlay"
     badge: "Wappen"
@@ -1800,8 +1795,6 @@ _sfx:
   note: "Notizen"
   noteMy: "Meine Notizen"
   notification: "Benachrichtigungen"
-  antenna: "Antennen"
-  channel: "Kanalbenachrichtigung"
 _ago:
   future: "Zukunft"
   justNow: "Gerade eben"
@@ -2144,6 +2137,7 @@ _notification:
     receiveFollowRequest: "Erhaltene Follow-Anfragen"
     followRequestAccepted: "Akzeptierte Follow-Anfragen"
     achievementEarned: "Errungenschaft freigeschaltet"
+    login: "Anmelden"
     app: "Benachrichtigungen von Apps"
   _actions:
     followBack: "folgt dir nun auch"
@@ -2193,7 +2187,6 @@ _webhookSettings:
   createWebhook: "Webhook erstellen"
   name: "Name"
   secret: "Secret"
-  events: "Webhook-Ereignisse"
   active: "Aktiviert"
   _events:
     follow: "Wenn du jemandem folgst"
@@ -2203,6 +2196,10 @@ _webhookSettings:
     renote: "Wenn du ein Renote erhältst"
     reaction: "Wenn du eine Reaktion erhältst"
     mention: "Wenn du erwähnt wirst"
+_abuseReport:
+  _notificationRecipient:
+    _recipientType:
+      mail: "Email"
 _moderationLogTypes:
   createRole: "Rolle erstellt"
   deleteRole: "Rolle gelöscht"
diff --git a/locales/el-GR.yml b/locales/el-GR.yml
index 2098c7ef50..4657842ca5 100644
--- a/locales/el-GR.yml
+++ b/locales/el-GR.yml
@@ -301,8 +301,6 @@ _theme:
 _sfx:
   note: "Σημειώματα"
   notification: "Ειδοποιήσεις"
-  antenna: "Αντένες"
-  channel: "Ειδοποιήσεις καναλιών"
 _ago:
   future: "Μελλοντικό"
   justNow: "Μόλις τώρα"
@@ -380,6 +378,7 @@ _notification:
     renote: "Κοινοποίηση σημειώματος"
     quote: "Παράθεση"
     reaction: "Αντιδράσεις"
+    login: "Σύνδεση"
   _actions:
     reply: "Απάντηση"
     renote: "Κοινοποίηση σημειώματος"
diff --git a/locales/en-US.yml b/locales/en-US.yml
index c20a1ac7d8..8570addfa2 100644
--- a/locales/en-US.yml
+++ b/locales/en-US.yml
@@ -8,6 +8,9 @@ search: "Search"
 notifications: "Notifications"
 username: "Username"
 password: "Password"
+initialPasswordForSetup: "Initial password for setup"
+initialPasswordIsIncorrect: "Initial password for setup is incorrect"
+initialPasswordForSetupDescription: "Use the password you entered in the configuration file if you installed Misskey yourself.\n If you are using a Misskey hosting service, use the password provided.\n If you have not set a password, leave it blank to continue."
 forgotPassword: "Forgot password"
 fetchingAsApObject: "Fetching from the Fediverse..."
 ok: "OK"
@@ -60,6 +63,7 @@ copyFileId: "Copy file ID"
 copyFolderId: "Copy folder ID"
 copyProfileUrl: "Copy profile URL"
 searchUser: "Search for a user"
+searchThisUsersNotes: "Search this user’s notes"
 reply: "Reply"
 loadMore: "Load more"
 showMore: "Show more"
@@ -108,7 +112,7 @@ enterEmoji: "Enter an emoji"
 renote: "Renote"
 unrenote: "Remove renote"
 renoted: "Renoted."
-renotedToX: "Renote from {name} users。"
+renotedToX: "Renoted to {name}."
 cantRenote: "This post can't be renoted."
 cantReRenote: "A renote can't be renoted."
 quote: "Quote"
@@ -125,8 +129,8 @@ add: "Add"
 reaction: "Reactions"
 reactions: "Reactions"
 emojiPicker: "Emoji picker"
-pinnedEmojisForReactionSettingDescription: "Set the emojis which should be pinned and displayed immediately when reacting."
-pinnedEmojisSettingDescription: "Set the emojis to be pinned and displayed when viewing emoji picker"
+pinnedEmojisForReactionSettingDescription: "Set the emojis to be pinned and displayed when reacting."
+pinnedEmojisSettingDescription: "Set the emojis to be pinned and displayed when viewing emoji picker."
 emojiPickerDisplay: "Emoji picker display"
 overwriteFromPinnedEmojisForReaction: "Override from reaction settings"
 overwriteFromPinnedEmojis: "Override from general settings"
@@ -154,6 +158,7 @@ editList: "Edit list"
 selectChannel: "Select a channel"
 selectAntenna: "Select an antenna"
 editAntenna: "Edit antenna"
+createAntenna: "Create an antenna"
 selectWidget: "Select a widget"
 editWidgets: "Edit widgets"
 editWidgetsExit: "Done"
@@ -165,7 +170,7 @@ emojiUrl: "Emoji URL"
 addEmoji: "Add an emoji"
 settingGuide: "Recommended settings"
 cacheRemoteFiles: "Cache remote files"
-cacheRemoteFilesDescription: "When this setting is disabled, remote files are loaded directly from the remote instance. Disabling this will decrease storage usage, but increase traffic, as thumbnails will not be generated."
+cacheRemoteFilesDescription: "When this setting is disabled, remote files are loaded directly from the remote servers. Disabling this will decrease storage usage, but increase traffic, as thumbnails will not be generated."
 youCanCleanRemoteFilesCache: "You can clear the cache by clicking the 🗑️ button in the file management view."
 cacheRemoteSensitiveFiles: "Cache sensitive remote files"
 cacheRemoteSensitiveFilesDescription: "When this setting is disabled, sensitive remote files are loaded directly from the remote instance without caching."
@@ -180,6 +185,10 @@ addAccount: "Add account"
 reloadAccountsList: "Reload account list"
 loginFailed: "Failed to sign in"
 showOnRemote: "View on remote instance"
+continueOnRemote: "Continue on a remote server"
+chooseServerOnMisskeyHub: "Choose a server from the Misskey Hub"
+specifyServerHost: "Specify a server host directly"
+inputHostName: "Enter the domain"
 general: "General"
 wallpaper: "Wallpaper"
 setWallpaper: "Set wallpaper"
@@ -190,6 +199,7 @@ followConfirm: "Are you sure that you want to follow {name}?"
 proxyAccount: "Proxy account"
 proxyAccountDescription: "A proxy account is an account that acts as a remote follower for users under certain conditions. For example, when a user adds a remote user to the list, the remote user's activity will not be delivered to the instance if no local user is following that user, so the proxy account will follow instead."
 host: "Host"
+selectSelf: "Select myself"
 selectUser: "Select a user"
 recipient: "Recipient"
 annotation: "Comments"
@@ -205,6 +215,7 @@ perDay: "Per Day"
 stopActivityDelivery: "Stop sending activities"
 blockThisInstance: "Block this instance"
 silenceThisInstance: "Silence this instance"
+mediaSilenceThisInstance: "Media-silence this server"
 operations: "Operations"
 software: "Software"
 version: "Version"
@@ -225,7 +236,11 @@ clearCachedFilesConfirm: "Are you sure that you want to delete all cached remote
 blockedInstances: "Blocked Instances"
 blockedInstancesDescription: "List the hostnames of the instances you want to block separated by linebreaks. Listed instances will no longer be able to communicate with this instance."
 silencedInstances: "Silenced instances"
-silencedInstancesDescription: "List the hostnames of the instances that you want to silence. All accounts of the listed instances will be treated as silenced, can only make follow requests, and cannot mention local accounts if not followed. This will not affect blocked instances."
+silencedInstancesDescription: "List the host names of the servers that you want to silence, separated by a new line. All accounts belonging to the listed servers will be treated as silenced, and can only make follow requests, and cannot mention local accounts if not followed. This will not affect the blocked servers."
+mediaSilencedInstances: "Media-silenced servers"
+mediaSilencedInstancesDescription: "List the host names of the servers that you want to media-silence, separated by a new line. All accounts belonging to the listed servers will be treated as sensitive, and can't use custom emojis. This will not affect the blocked servers."
+federationAllowedHosts: "Federation allowed servers"
+federationAllowedHostsDescription: "Specify the hostnames of the servers you want to allow federation separated by line breaks."
 muteAndBlock: "Mutes and Blocks"
 mutedUsers: "Muted users"
 blockedUsers: "Blocked users"
@@ -323,6 +338,7 @@ renameFolder: "Rename this folder"
 deleteFolder: "Delete this folder"
 folder: "Folder"
 addFile: "Add a file"
+showFile: "Show files"
 emptyDrive: "Your Drive is empty"
 emptyFolder: "This folder is empty"
 unableToDelete: "Unable to delete"
@@ -387,7 +403,7 @@ mcaptcha: "mCaptcha"
 enableMcaptcha: "Enable mCaptcha"
 mcaptchaSiteKey: "Site key"
 mcaptchaSecretKey: "Secret key"
-mcaptchaInstanceUrl: "mCaptcha instance URL"
+mcaptchaInstanceUrl: "mCaptcha server URL"
 recaptcha: "reCAPTCHA"
 enableRecaptcha: "Enable reCAPTCHA"
 recaptchaSiteKey: "Site key"
@@ -437,6 +453,7 @@ totpDescription: "Use an authenticator app to enter one-time passwords"
 moderator: "Moderator"
 moderation: "Moderation"
 moderationNote: "Moderation note"
+moderationNoteDescription: "You can fill in notes that will be shared only among moderators."
 addModerationNote: "Add moderation note"
 moderationLogs: "Moderation logs"
 nUsersMentioned: "Mentioned by {n} users"
@@ -476,6 +493,7 @@ noMessagesYet: "No messages yet"
 newMessageExists: "There are new messages"
 onlyOneFileCanBeAttached: "You can only attach one file to a message"
 signinRequired: "Please register or sign in before continuing"
+signinOrContinueOnRemote: "To continue, you need to move your server or sign up / log in to this server."
 invitations: "Invites"
 invitationCode: "Invitation code"
 checking: "Checking..."
@@ -497,7 +515,10 @@ uiLanguage: "User interface language"
 aboutX: "About {x}"
 emojiStyle: "Emoji style"
 native: "Native"
-disableDrawer: "Don't use drawer-style menus"
+menuStyle: "Menu style"
+style: "Style"
+drawer: "Drawer"
+popup: "Pop up"
 showNoteActionsOnlyHover: "Only show note actions on hover"
 showReactionsCount: "See the number of reactions in notes"
 noHistory: "No history available"
@@ -580,6 +601,8 @@ ascendingOrder: "Ascending"
 descendingOrder: "Descending"
 scratchpad: "Scratchpad"
 scratchpadDescription: "The Scratchpad provides an environment for AiScript experiments. You can write, execute, and check the results of it interacting with Misskey in it."
+uiInspector: "UI inspector"
+uiInspectorDescription: "You can see the UI component server list on memory. UI component will be generated by Ui:C: function."
 output: "Output"
 script: "Script"
 disablePagesScript: "Disable AiScript on Pages"
@@ -696,10 +719,7 @@ abuseReported: "Your report has been sent. Thank you very much."
 reporter: "Reporter"
 reporteeOrigin: "Reportee Origin"
 reporterOrigin: "Reporter Origin"
-forwardReport: "Forward report to remote instance"
-forwardReportIsAnonymous: "Instead of your account, an anonymous system account will be displayed as reporter at the remote instance."
 send: "Send"
-abuseMarkAsResolved: "Mark report as resolved"
 openInNewTab: "Open in new tab"
 openInSideView: "Open in side view"
 defaultNavigationBehaviour: "Default navigation behavior"
@@ -836,6 +856,7 @@ administration: "Management"
 accounts: "Accounts"
 switch: "Switch"
 noMaintainerInformationWarning: "Maintainer information is not configured."
+noInquiryUrlWarning: "Inquiry URL isn’t set"
 noBotProtectionWarning: "Bot protection is not configured."
 configure: "Configure"
 postToGallery: "Create new gallery post"
@@ -900,6 +921,7 @@ followersVisibility: "Visibility of followers"
 continueThread: "View thread continuation"
 deleteAccountConfirm: "This will irreversibly delete your account. Proceed?"
 incorrectPassword: "Incorrect password."
+incorrectTotp: "The one-time password is incorrect or has expired."
 voteConfirm: "Confirm your vote for \"{choice}\"?"
 hide: "Hide"
 useDrawerReactionPickerForMobile: "Display reaction picker as drawer on mobile"
@@ -924,6 +946,9 @@ oneHour: "One hour"
 oneDay: "One day"
 oneWeek: "One week"
 oneMonth: "One month"
+threeMonths: "3 months"
+oneYear: "1 year"
+threeDays: "3 days"
 reflectMayTakeTime: "It may take some time for this to be reflected."
 failedToFetchAccountInformation: "Could not fetch account information"
 rateLimitExceeded: "Rate limit exceeded"
@@ -1025,6 +1050,7 @@ thisPostMayBeAnnoyingHome: "Post to home timeline"
 thisPostMayBeAnnoyingCancel: "Cancel"
 thisPostMayBeAnnoyingIgnore: "Post anyway"
 collapseRenotes: "Collapse renotes you've already seen"
+collapseRenotesDescription: "Collapse notes that you've reacted to or renoted before."
 internalServerError: "Internal Server Error"
 internalServerErrorDescription: "The server has run into an unexpected error."
 copyErrorInfo: "Copy error details"
@@ -1063,9 +1089,10 @@ retryAllQueuesConfirmTitle: "Really retry all?"
 retryAllQueuesConfirmText: "This will temporarily increase the server load."
 enableChartsForRemoteUser: "Generate remote user data charts"
 enableChartsForFederatedInstances: "Generate remote instance data charts"
+enableStatsForFederatedInstances: "Receive remote server stats"
 showClipButtonInNoteFooter: "Add \"Clip\" to note action menu"
 reactionsDisplaySize: "Reaction display size"
-limitWidthOfReaction: "Limits the maximum width of reactions and display them in reduced size."
+limitWidthOfReaction: "Limit the maximum width of reactions and display them in reduced size."
 noteIdOrUrl: "Note ID or URL"
 video: "Video"
 videos: "Videos"
@@ -1098,6 +1125,8 @@ preservedUsernames: "Reserved usernames"
 preservedUsernamesDescription: "List usernames to reserve separated by linebreaks. These will become unable during normal account creation, but can be used by administrators to manually create accounts. Already existing accounts using these usernames will not be affected."
 createNoteFromTheFile: "Compose note from this file"
 archive: "Archive"
+archived: "Archived"
+unarchive: "Unarchive"
 channelArchiveConfirmTitle: "Really archive {name}?"
 channelArchiveConfirmDescription: "An archived channel won't appear in the channel list or search results anymore. New posts can also not be added to it anymore."
 thisChannelArchived: "This channel has been archived."
@@ -1108,6 +1137,9 @@ preventAiLearning: "Reject usage in Machine Learning (Generative AI)"
 preventAiLearningDescription: "Requests crawlers to not use posted text or image material etc. in machine learning (Predictive / Generative AI) data sets. This is achieved by adding a \"noai\" HTML-Response flag to the respective content. A complete prevention can however not be achieved through this flag, as it may simply be ignored."
 options: "Options"
 specifyUser: "Specific user"
+lookupConfirm: "Do you want to look up?"
+openTagPageConfirm: "Do you want to open a hashtag page?"
+specifyHost: "Specific host"
 failedToPreviewUrl: "Could not preview"
 update: "Update"
 rolesThatCanBeUsedThisEmojiAsReaction: "Roles that can use this emoji as reaction"
@@ -1236,9 +1268,55 @@ launchApp: "Launch the app"
 useNativeUIForVideoAudioPlayer: "Use UI of browser when play video and audio"
 keepOriginalFilename: "Keep original file name"
 keepOriginalFilenameDescription: "If you turn off this setting, files names will be replaced with random string automatically when you upload files."
-noDescription: "There is not the explanation"
+noDescription: "There is no explanation"
 alwaysConfirmFollow: "Always confirm when following"
 inquiry: "Contact"
+tryAgain: "Please try again later"
+confirmWhenRevealingSensitiveMedia: "Confirm when revealing sensitive media"
+sensitiveMediaRevealConfirm: "This might be a sensitive media. Are you sure to reveal?"
+createdLists: "Created lists"
+createdAntennas: "Created antennas"
+fromX: "From {x}"
+genEmbedCode: "Generate embed code"
+noteOfThisUser: "Notes by this user"
+clipNoteLimitExceeded: "No more notes can be added to this clip."
+performance: "Performance"
+modified: "Modified"
+discard: "Discard"
+thereAreNChanges: "There are {n} change(s)"
+signinWithPasskey: "Sign in with Passkey"
+unknownWebAuthnKey: "Unknown Passkey"
+passkeyVerificationFailed: "Passkey verification has failed."
+passkeyVerificationSucceededButPasswordlessLoginDisabled: "Passkey verification has succeeded but password-less login is disabled."
+messageToFollower: "Message to followers"
+target: "Target"
+testCaptchaWarning: "This function is intended for CAPTCHA testing purposes.\n<strong>Do not use in a production environment.</strong>"
+prohibitedWordsForNameOfUser: "Prohibited words for user names"
+prohibitedWordsForNameOfUserDescription: "If any of the strings in this list are included in the user's name, the name will be denied. Users with moderator privileges are not affected by this restriction."
+yourNameContainsProhibitedWords: "Your name contains prohibited words"
+yourNameContainsProhibitedWordsDescription: "If you wish to use this name, please contact your server administrator."
+thisContentsAreMarkedAsSigninRequiredByAuthor: "Set by the author to require login to view"
+lockdown: "Lockdown"
+pleaseSelectAccount: "Select an account"
+_accountSettings:
+  requireSigninToViewContents: "Require sign-in to view contents"
+  requireSigninToViewContentsDescription1: "Require login to view all notes and other content you have created. This will have the effect of preventing crawlers from collecting your information."
+  requireSigninToViewContentsDescription2: "Content will not be displayed in URL previews (OGP), embedded in web pages, or on servers that don't support note quotes."
+  requireSigninToViewContentsDescription3: "These restrictions may not apply to federated content from other remote servers."
+  makeNotesFollowersOnlyBefore: "Make past notes to be displayed only to followers"
+  makeNotesFollowersOnlyBeforeDescription: "While this feature is enabled, only followers can see notes past the set date and time or have been visible for a set time. When it is deactivated, the note publication status will also be restored."
+  makeNotesHiddenBefore: "Make past notes private"
+  makeNotesHiddenBeforeDescription: "While this feature is enabled, notes that are past the set date and time or have been visible only to you. When it is deactivated, the note publication status will also be restored."
+  mayNotEffectForFederatedNotes: "Notes federated to a remote server may not be effective."
+  notesHavePassedSpecifiedPeriod: "Note that the specified time has passed"
+  notesOlderThanSpecifiedDateAndTime: "Notes before the specified date and time"
+_abuseUserReport:
+  forward: "Forward"
+  forwardDescription: "Forward the report to a remote server as an anonymous system account."
+  resolve: "Resolve"
+  accept: "Accept"
+  reject: "Reject"
+  resolveTutorial: "If the report is legitimate in content, select \"Accept\" to mark the case as resolved in the affirmative.\nIf the content of the report is not legitimate, select \"Reject\" to mark the case as resolved in the negative."
 _delivery:
   status: "Delivery status"
   stop: "Suspended"
@@ -1341,7 +1419,7 @@ _initialTutorial:
       _exampleNote:
         cw: "This will surely make you hungry!"
         note: "Just had a chocolate-glazed donut 🍩😋"
-      useCases: "This is used when following the server guidelines for necessary notes or for self-restriction of spoiler or sensitive text."
+      useCases: "This is used when following the server guidelines, for necessary notes, or for self-restriction of spoiler or sensitive text."
   _howToMakeAttachmentsSensitive:
     title: "How to Mark Attachments as Sensitive?"
     description: "For attachments that are required by server guidelines or that should not be left intact, add a \"sensitive\" flag."
@@ -1373,6 +1451,10 @@ _serverSettings:
   fanoutTimelineDescription: "Greatly increases performance of timeline retrieval and reduces load on the database when enabled. In exchange, memory usage of Redis will increase. Consider disabling this in case of low server memory or server instability."
   fanoutTimelineDbFallback: "Fallback to database"
   fanoutTimelineDbFallbackDescription: "When enabled, the timeline will fall back to the database for additional queries if the timeline is not cached. Disabling it further reduces the server load by eliminating the fallback process, but limits the range of timelines that can be retrieved."
+  reactionsBufferingDescription: "When enabled, performance during reaction creation will be greatly improved, reducing the load on the database. However, Redis memory usage will increase."
+  inquiryUrl: "Inquiry URL"
+  inquiryUrlDescription: "Specify a URL for the inquiry form to the server maintainer or a web page for the contact information."
+  thisSettingWillAutomaticallyOffWhenModeratorsInactive: "If no moderator activity is detected for a while, this setting will be automatically turned off to prevent spam."
 _accountMigration:
   moveFrom: "Migrate another account to this one"
   moveFromSub: "Create alias to another account"
@@ -1689,6 +1771,7 @@ _role:
     canManageAvatarDecorations: "Manage avatar decorations"
     driveCapacity: "Drive capacity"
     alwaysMarkNsfw: "Always mark files as NSFW"
+    canUpdateBioMedia: "Can edit an icon or a banner image"
     pinMax: "Maximum number of pinned notes"
     antennaMax: "Maximum number of antennas"
     wordMuteMax: "Maximum number of characters allowed in word mutes"
@@ -1703,6 +1786,11 @@ _role:
     canSearchNotes: "Usage of note search"
     canUseTranslator: "Translator usage"
     avatarDecorationLimit: "Maximum number of avatar decorations that can be applied"
+    canImportAntennas: "Allow importing antennas"
+    canImportBlocking: "Allow importing blocking"
+    canImportFollowing: "Allow importing following"
+    canImportMuting: "Allow importing muting"
+    canImportUserLists: "Allow importing lists"
   _condition:
     roleAssignedTo: "Assigned to manual roles"
     isLocal: "Local user"
@@ -1920,7 +2008,6 @@ _theme:
     buttonBg: "Button background"
     buttonHoverBg: "Button background (Hover)"
     inputBorder: "Input field border"
-    listItemHoverBg: "List item background (Hover)"
     driveFolderBg: "Drive folder background"
     wallpaperOverlay: "Wallpaper overlay"
     badge: "Badge"
@@ -1932,8 +2019,6 @@ _sfx:
   note: "New note"
   noteMy: "Own note"
   notification: "Notifications"
-  antenna: "Antennas"
-  channel: "Channel notifications"
   reaction: "On choosing a reaction"
 _soundSettings:
   driveFile: "Use an audio file in Drive."
@@ -1942,6 +2027,7 @@ _soundSettings:
   driveFileTypeWarnDescription: "Select an audio file"
   driveFileDurationWarn: "The audio is too long."
   driveFileDurationWarnDescription: "Long audio may disrupt using Misskey. Still continue?"
+  driveFileError: "It couldn't load the sound. Please change the setting."
 _ago:
   future: "Future"
   justNow: "Just now"
@@ -2059,7 +2145,7 @@ _permissions:
   "read:admin:invite-codes": "View invite codes"
   "write:admin:announcements": "Manage announcements"
   "read:admin:announcements": "View announcements"
-  "write:admin:avatar-decorations": "Manage avatar decorations"
+  "write:admin:avatar-decorations": "Can manage avatar decorations"
   "read:admin:avatar-decorations": "View avatar decorations"
   "write:admin:federation": "Manage federation data"
   "write:admin:account": "Manage user account"
@@ -2088,8 +2174,11 @@ _auth:
   permissionAsk: "This application requests the following permissions"
   pleaseGoBack: "Please go back to the application"
   callback: "Returning to the application"
+  accepted: "Access granted"
   denied: "Access denied"
+  scopeUser: "Operate as the following user"
   pleaseLogin: "Please log in to authorize applications."
+  byClickingYouWillBeRedirectedToThisUrl: "When access is granted, you will automatically be redirected to the following URL"
 _antennaSources:
   all: "All notes"
   homeTimeline: "Notes from followed users"
@@ -2198,6 +2287,9 @@ _profile:
   changeBanner: "Change banner"
   verifiedLinkDescription: "By entering an URL that contains a link to your profile here, an ownership verification icon can be displayed next to the field."
   avatarDecorationMax: "You can add up to {max} decorations."
+  followedMessage: "Message when you are followed"
+  followedMessageDescription: "You can set a short message to be displayed to the recipient when they follow you."
+  followedMessageDescriptionForLockedAccount: "If you have set up that follow requests require approval, this will be displayed when you grant a follow request."
 _exportOrImport:
   allNotes: "All notes"
   favoritedNotes: "Favorite notes"
@@ -2290,6 +2382,7 @@ _pages:
   eyeCatchingImageSet: "Set thumbnail"
   eyeCatchingImageRemove: "Delete thumbnail"
   chooseBlock: "Add a block"
+  enterSectionTitle: "Enter a section title"
   selectType: "Select a type"
   contentBlocks: "Content"
   inputBlocks: "Input"
@@ -2335,6 +2428,8 @@ _notification:
   renotedBySomeUsers: "Renote from {n} users"
   followedBySomeUsers: "Followed by {n} users"
   flushNotification: "Clear notifications"
+  exportOfXCompleted: "Export of {x} has been completed"
+  login: "Someone logged in"
   _types:
     all: "All"
     note: "New notes"
@@ -2349,6 +2444,9 @@ _notification:
     followRequestAccepted: "Accepted follow requests"
     roleAssigned: "Role given"
     achievementEarned: "Achievement unlocked"
+    exportCompleted: "The export has been completed"
+    login: "Sign In"
+    test: "Notification test"
     app: "Notifications from linked apps"
   _actions:
     followBack: "followed you back"
@@ -2358,6 +2456,7 @@ _deck:
   alwaysShowMainColumn: "Always show main column"
   columnAlign: "Align columns"
   addColumn: "Add column"
+  newNoteNotificationSettings: "Notification setting for new notes"
   configureColumn: "Column settings"
   swapLeft: "Swap with the left column"
   swapRight: "Swap with the right column"
@@ -2396,9 +2495,10 @@ _drivecleaner:
   orderByCreatedAtAsc: "Ascending Dates"
 _webhookSettings:
   createWebhook: "Create Webhook"
+  modifyWebhook: "Modify Webhook"
   name: "Name"
   secret: "Secret"
-  events: "Webhook Events"
+  trigger: "Trigger"
   active: "Enabled"
   _events:
     follow: "When following a user"
@@ -2408,6 +2508,29 @@ _webhookSettings:
     renote: "When renoted"
     reaction: "When receiving a reaction"
     mention: "When being mentioned"
+  _systemEvents:
+    abuseReport: "When received a new report"
+    abuseReportResolved: "When resolved report"
+    userCreated: "When user is created"
+    inactiveModeratorsWarning: "When moderators have been inactive for a while"
+    inactiveModeratorsInvitationOnlyChanged: "When a moderator has been inactive for a while, and the server is changed to invitation-only"
+  deleteConfirm: "Are you sure you want to delete the Webhook?"
+  testRemarks: "Click the button to the right of the switch to send a test Webhook with dummy data."
+_abuseReport:
+  _notificationRecipient:
+    createRecipient: "Add a recipient for reports"
+    modifyRecipient: "Edit a recipient for reports"
+    recipientType: "Notification type"
+    _recipientType:
+      mail: "Email"
+      webhook: "Webhook"
+      _captions:
+        mail: "Send the email to moderators' email addresses when you receive reports."
+        webhook: "Send a notification to System Webhook when you receive or resolve reports."
+    keywords: "Keywords"
+    notifiedUser: "Users to notify"
+    notifiedWebhook: "Webhook to use"
+    deleteConfirm: "Are you sure that you want to delete the notification recipient?"
 _moderationLogTypes:
   createRole: "Role created"
   deleteRole: "Role deleted"
@@ -2436,6 +2559,8 @@ _moderationLogTypes:
   markSensitiveDriveFile: "File marked as sensitive"
   unmarkSensitiveDriveFile: "File unmarked as sensitive"
   resolveAbuseReport: "Report resolved"
+  forwardAbuseReport: "Report forwarded"
+  updateAbuseReportNote: "Moderation note of a report updated"
   createInvitation: "Invite generated"
   createAd: "Ad created"
   deleteAd: "Ad deleted"
@@ -2443,8 +2568,18 @@ _moderationLogTypes:
   createAvatarDecoration: "Avatar decoration created"
   updateAvatarDecoration: "Avatar decoration updated"
   deleteAvatarDecoration: "Avatar decoration deleted"
-  unsetUserAvatar: "Unset this user's avatar"
-  unsetUserBanner: "Unset this user's banner"
+  unsetUserAvatar: "User avatar unset"
+  unsetUserBanner: "User banner unset"
+  createSystemWebhook: "System Webhook created"
+  updateSystemWebhook: "System Webhook updated"
+  deleteSystemWebhook: "System Webhook deleted"
+  createAbuseReportNotificationRecipient: "Recipient for reports created"
+  updateAbuseReportNotificationRecipient: "Recipient for reports updated"
+  deleteAbuseReportNotificationRecipient: "Recipient for reports deleted"
+  deleteAccount: "Account deleted"
+  deletePage: "Page deleted"
+  deleteFlash: "Play deleted"
+  deleteGalleryPost: "Gallery post deleted"
 _fileViewer:
   title: "File details"
   type: "File type"
@@ -2576,3 +2711,22 @@ _mediaControls:
   pip: "Picture in Picture"
   playbackRate: "Playback Speed"
   loop: "Loop playback"
+_contextMenu:
+  title: "Context menu"
+  app: "Application"
+  appWithShift: "Application with shift key"
+  native: "Native"
+_embedCodeGen:
+  title: "Customize embed code"
+  header: "Show header"
+  autoload: "Automatically load more (deprecated)"
+  maxHeight: "Max height"
+  maxHeightDescription: "Setting it to 0 disables the max height setting. Specify some value to prevent the widget from continuing to expand vertically."
+  maxHeightWarn: "The max height limit is disabled (0). If this was not intended, set the max height to some value."
+  previewIsNotActual: "The display differs from the actual embedding because it exceeds the range displayed on the preview screen."
+  rounded: "Make it rounded"
+  border: "Add a border to the outer frame"
+  applyToPreview: "Apply to the preview"
+  generateCode: "Generate embed code"
+  codeGenerated: "The code has been generated"
+  codeGeneratedDescription: "Paste the generated code into your website to embed the content."
diff --git a/locales/es-ES.yml b/locales/es-ES.yml
index 5c8249ded5..7731598152 100644
--- a/locales/es-ES.yml
+++ b/locales/es-ES.yml
@@ -8,6 +8,8 @@ search: "Buscar"
 notifications: "Notificaciones"
 username: "Nombre de usuario"
 password: "Contraseña"
+initialPasswordForSetup: "Contraseña para iniciar la inicialización"
+initialPasswordIsIncorrect: "La contraseña para iniciar la configuración inicial es incorrecta."
 forgotPassword: "Olvidé mi contraseña"
 fetchingAsApObject: "Buscando en el fediverso"
 ok: "OK"
@@ -60,6 +62,7 @@ copyFileId: "Copiar ID del archivo"
 copyFolderId: "Copiar ID de carpeta"
 copyProfileUrl: "Copiar la URL del perfil"
 searchUser: "Buscar un usuario"
+searchThisUsersNotes: ""
 reply: "Responder"
 loadMore: "Ver más"
 showMore: "Ver más"
@@ -108,11 +111,14 @@ enterEmoji: "Ingresar emojis"
 renote: "Renotar"
 unrenote: "Quitar renota"
 renoted: "Renotado"
+renotedToX: "{name} usuarios han renotado。"
 cantRenote: "No se puede renotar este post"
 cantReRenote: "No se puede renotar una renota"
 quote: "Citar"
 inChannelRenote: "Renota sólo del canal"
 inChannelQuote: "Cita sólo del canal"
+renoteToChannel: "Renotar a otro canal"
+renoteToOtherChannel: "Renotar a otro canal"
 pinnedNote: "Nota fijada"
 pinned: "Fijar al perfil"
 you: "Tú"
@@ -151,6 +157,7 @@ editList: "Editar lista"
 selectChannel: "Seleccionar canal"
 selectAntenna: "Seleccionar antena"
 editAntenna: "Editar antena"
+createAntenna: "Crear una antena"
 selectWidget: "Seleccionar widget"
 editWidgets: "Editar widgets"
 editWidgetsExit: "Terminar edición"
@@ -177,6 +184,10 @@ addAccount: "Agregar Cuenta"
 reloadAccountsList: "Recargar lista de cuentas"
 loginFailed: "Error al iniciar sesión."
 showOnRemote: "Ver en una instancia remota"
+continueOnRemote: "Ver en una instancia remota"
+chooseServerOnMisskeyHub: "Elegir un servidor en Misskey Hub"
+specifyServerHost: "Especifica una instancia directamente"
+inputHostName: "Introduzca el dominio"
 general: "General"
 wallpaper: "Fondo de pantalla"
 setWallpaper: "Establecer fondo de pantalla"
@@ -302,7 +313,7 @@ location: "Lugar"
 theme: "Tema"
 themeForLightMode: "Tema para usar en Modo Linterna"
 themeForDarkMode: "Tema para usar en Modo Oscuro"
-light: "Linterna"
+light: "Claro"
 dark: "Oscuro"
 lightThemes: "Tema claro"
 darkThemes: "Tema oscuro"
@@ -493,7 +504,8 @@ uiLanguage: "Idioma de visualización de la interfaz"
 aboutX: "Acerca de {x}"
 emojiStyle: "Estilo de emoji"
 native: "Nativo"
-disableDrawer: "No mostrar los menús en cajones"
+menuStyle: "Diseño del menú"
+style: "Diseño"
 showNoteActionsOnlyHover: "Mostrar acciones de la nota sólo al pasar el cursor"
 showReactionsCount: "Mostrar el número de reacciones en las notas"
 noHistory: "No hay datos en el historial"
@@ -692,10 +704,7 @@ abuseReported: "Se ha enviado el reporte. Muchas gracias."
 reporter: "Reportador"
 reporteeOrigin: "Reportar a"
 reporterOrigin: "Origen del reporte"
-forwardReport: "Transferir un informe a una instancia remota"
-forwardReportIsAnonymous: "No puede ver su información de la instancia remota y aparecerá como una cuenta anónima del sistema"
 send: "Enviar"
-abuseMarkAsResolved: "Marcar reporte como resuelto"
 openInNewTab: "Abrir en una Nueva Pestaña"
 openInSideView: "Abrir en una vista al costado"
 defaultNavigationBehaviour: "Navegación por defecto"
@@ -920,6 +929,9 @@ oneHour: "1 hora"
 oneDay: "1 día"
 oneWeek: "1 semana"
 oneMonth: "1 mes"
+threeMonths: "Tres meses"
+oneYear: "Un año"
+threeDays: "Tres días"
 reflectMayTakeTime: "Puede pasar un tiempo hasta que se reflejen los cambios"
 failedToFetchAccountInformation: "No se pudo obtener información de la cuenta"
 rateLimitExceeded: "Se excedió el límite de peticiones"
@@ -1094,6 +1106,8 @@ preservedUsernames: "Nombre de usuario reservado"
 preservedUsernamesDescription: "La lista de nombres de usuario para reservar tienen que separarse con saltos de línea.\nEstos estarán indisponibles durante la creación de cuentas, pero pueden ser usados para que los administradores puedan crear esas cuentas manualmente. Las cuentas existentes con esos nombres de usuario no se verán afectadas."
 createNoteFromTheFile: "Componer una nota desde éste archivo"
 archive: "Archivo"
+archived: "Archivado"
+unarchive: "Desarchivar"
 channelArchiveConfirmTitle: "¿Seguro de archivar {name}?"
 channelArchiveConfirmDescription: "Un canal archivado no aparecerá en la lista de canales ni en los resultados. Las nuevas publicaciones tampoco serán añadidas."
 thisChannelArchived: "El canal ha sido archivado."
@@ -1233,6 +1247,14 @@ useNativeUIForVideoAudioPlayer: "Usar la interfaz del navegador cuando se reprod
 keepOriginalFilename: "Mantener el nombre original del archivo"
 noDescription: "No hay descripción"
 alwaysConfirmFollow: "Confirmar siempre cuando se sigue a alguien"
+inquiry: "Contacto"
+tryAgain: "Por favor , inténtalo de nuevo"
+performance: "Rendimiento"
+unknownWebAuthnKey: "Esto no se ha registrado llave maestra."
+messageToFollower: "Mensaje a seguidores"
+_abuseUserReport:
+  accept: "Acepte"
+  reject: "repudio"
 _delivery:
   stop: "Suspendido"
   _type:
@@ -1908,7 +1930,6 @@ _theme:
     buttonBg: "Fondo de botón"
     buttonHoverBg: "Fondo de botón (hover)"
     inputBorder: "Borde de los campos de entrada"
-    listItemHoverBg: "Fondo de elemento de listas (hover)"
     driveFolderBg: "Fondo de capeta del drive"
     wallpaperOverlay: "Transparencia del fondo de pantalla"
     badge: "Medalla"
@@ -1920,8 +1941,6 @@ _sfx:
   note: "Notas"
   noteMy: "Nota (a mí mismo)"
   notification: "Notificaciones"
-  antenna: "Antena receptora"
-  channel: "Notificaciones del canal"
   reaction: "Al seleccionar una reacción"
 _soundSettings:
   driveFile: "Usar un archivo de audio en Drive"
@@ -2335,6 +2354,8 @@ _notification:
     followRequestAccepted: "El seguimiento fue aceptado"
     roleAssigned: "Rol asignado"
     achievementEarned: "Logro desbloqueado"
+    login: "Iniciar sesión"
+    test: "Pruebas de nofiticaciones"
     app: "Notificaciones desde aplicaciones"
   _actions:
     followBack: "Te sigue de vuelta"
@@ -2384,7 +2405,6 @@ _webhookSettings:
   createWebhook: "Crear Webhook"
   name: "Nombre"
   secret: "Secreto"
-  events: "Eventos de webhook"
   active: "Activado"
   _events:
     follow: "Cuando se sigue a alguien"
@@ -2394,6 +2414,14 @@ _webhookSettings:
     renote: "Cuando reciba un \"re-note\""
     reaction: "Cuando se recibe una reacción"
     mention: "Cuando hay una mención"
+  _systemEvents:
+    userCreated: "Cuando se crea el usuario."
+_abuseReport:
+  _notificationRecipient:
+    _recipientType:
+      mail: "Correo"
+      webhook: "Webhook"
+    keywords: "Palabras Clave"
 _moderationLogTypes:
   createRole: "Rol creado"
   deleteRole: "Rol eliminado"
@@ -2497,6 +2525,7 @@ _hemisphere:
   S: "Hemisferio sur"
 _reversi:
   reversi: "Reversi"
+  rules: "Reglas"
   won: "{name} ha ganado"
   total: "Total"
 _urlPreviewSetting:
diff --git a/locales/fr-FR.yml b/locales/fr-FR.yml
index 8d66c3d375..a7060c06fc 100644
--- a/locales/fr-FR.yml
+++ b/locales/fr-FR.yml
@@ -493,7 +493,6 @@ uiLanguage: "Langue d’affichage de l’interface"
 aboutX: "À propos de {x}"
 emojiStyle: "Style des émojis"
 native: "Natif"
-disableDrawer: "Les menus ne s'affichent pas dans le tiroir"
 showNoteActionsOnlyHover: "Afficher les actions de note uniquement au survol"
 showReactionsCount: "Afficher le nombre de réactions des notes"
 noHistory: "Pas d'historique"
@@ -692,10 +691,7 @@ abuseReported: "Le rapport est envoyé. Merci."
 reporter: "Signalé par"
 reporteeOrigin: "Origine du signalement"
 reporterOrigin: "Signalé par"
-forwardReport: "Transférer le signalement à l’instance distante"
-forwardReportIsAnonymous: "L'instance distante ne sera pas en mesure de voir vos informations et apparaîtra comme un compte anonyme du système."
 send: "Envoyer"
-abuseMarkAsResolved: "Marquer le signalement comme résolu"
 openInNewTab: "Ouvrir dans un nouvel onglet"
 openInSideView: "Ouvrir en vue latérale"
 defaultNavigationBehaviour: "Navigation par défaut"
@@ -1094,6 +1090,8 @@ preservedUsernames: "Noms d'utilisateur·rice réservés"
 preservedUsernamesDescription: "Énumérez les noms d'utilisateur à réserver, séparés par des nouvelles lignes. Les noms d'utilisateur spécifiés ici ne seront plus utilisables lors de la création d'un compte, sauf la création manuelle par un administrateur. De plus, les comptes existants ne seront pas affectés."
 createNoteFromTheFile: "Rédiger une note de ce fichier"
 archive: "Archive"
+archived: "Archivé"
+unarchive: "Annuler l'archivage"
 channelArchiveConfirmTitle: "Voulez-vous vraiment archiver {name} ?"
 channelArchiveConfirmDescription: "Une fois archivé, le canal n'apparaîtra plus dans la liste des canaux ni dans les résultats de recherche, et la publication des nouvelles notes sera impossible."
 thisChannelArchived: "Ce canal a été archivé."
@@ -1224,7 +1222,10 @@ enableHorizontalSwipe: "Glisser pour changer d'onglet"
 loading: "Chargement en cours"
 surrender: "Annuler"
 gameRetry: "Réessayer"
+launchApp: "Lancer l'app"
+inquiry: "Contact"
 _delivery:
+  status: "Statut de la diffusion"
   stop: "Suspendu·e"
   _type:
     none: "Publié"
@@ -1700,7 +1701,6 @@ _theme:
     buttonBg: "Arrière-plan du bouton"
     buttonHoverBg: "Arrière-plan du bouton (survolé)"
     inputBorder: "Cadre de la zone de texte"
-    listItemHoverBg: "Arrière-plan d'item de liste (survolé)"
     driveFolderBg: "Arrière-plan du dossier de disque"
     wallpaperOverlay: "Superposition de fond d'écran"
     badge: "Badge"
@@ -1712,8 +1712,6 @@ _sfx:
   note: "Nouvelle note"
   noteMy: "Ma note"
   notification: "Notifications"
-  antenna: "Réception de l’antenne"
-  channel: "Notifications de canal"
   reaction: "Lors de la sélection de la réaction"
 _soundSettings:
   driveFile: "Utiliser un effet sonore sur le Disque"
@@ -2035,6 +2033,7 @@ _notification:
     followRequestAccepted: "Demande d'abonnement acceptée"
     roleAssigned: "Rôle reçu"
     achievementEarned: "Déverrouillage d'accomplissement"
+    login: "Se connecter"
     app: "Notifications provenant des apps"
   _actions:
     followBack: "Suivre"
@@ -2073,6 +2072,10 @@ _drivecleaner:
 _webhookSettings:
   name: "Nom"
   active: "Activé"
+_abuseReport:
+  _notificationRecipient:
+    _recipientType:
+      mail: "E-mail "
 _moderationLogTypes:
   createRole: "Rôle créé"
   deleteRole: "Rôle supprimé"
diff --git a/locales/hu-HU.yml b/locales/hu-HU.yml
index 023a91494d..d0fdc027e9 100644
--- a/locales/hu-HU.yml
+++ b/locales/hu-HU.yml
@@ -1,5 +1,5 @@
 ---
-_lang_: "Japán"
+_lang_: "Magyar"
 monthAndDay: "{month}.{day}."
 search: "Keresés"
 notifications: "Értesítések"
@@ -96,6 +96,7 @@ _notification:
     renote: "Renote"
     quote: "Idézet"
     reaction: "Reakciók"
+    login: "Bejelentkezés"
   _actions:
     renote: "Renote"
 _deck:
diff --git a/locales/id-ID.yml b/locales/id-ID.yml
index 7f509afa50..5d51d2dc78 100644
--- a/locales/id-ID.yml
+++ b/locales/id-ID.yml
@@ -60,6 +60,7 @@ copyFileId: "Salin Berkas"
 copyFolderId: "Salin Folder"
 copyProfileUrl: "Salin Alamat Web Profil"
 searchUser: "Cari pengguna"
+searchThisUsersNotes: "Mencari catatan pengguna"
 reply: "Balas"
 loadMore: "Selebihnya"
 showMore: "Selebihnya"
@@ -154,6 +155,7 @@ editList: "Sunting daftar"
 selectChannel: "Pilih kanal"
 selectAntenna: "Pilih Antena"
 editAntenna: "Sunting antena"
+createAntenna: "Membuat antena."
 selectWidget: "Pilih gawit"
 editWidgets: "Sunting gawit"
 editWidgetsExit: "Selesai"
@@ -180,6 +182,10 @@ addAccount: "Tambahkan akun"
 reloadAccountsList: "Muat ulang daftar akun"
 loginFailed: "Gagal untuk masuk"
 showOnRemote: "Lihat profil asli"
+continueOnRemote: "Lihat di peladen asal"
+chooseServerOnMisskeyHub: "Pilih peladen dari Misskey Hub"
+specifyServerHost: "Tentukan domain peladen"
+inputHostName: "Masukkan nama domain"
 general: "Umum"
 wallpaper: "Wallpaper"
 setWallpaper: "Atur wallpaper"
@@ -190,6 +196,7 @@ followConfirm: "Apakah kamu yakin ingin mengikuti {name}?"
 proxyAccount: "Akun proksi"
 proxyAccountDescription: "Akun proksi merupakan sebuah akun yang bertindak sebagai pengikut instansi luar untuk pengguna dalam kondisi tertentu. Sebagai contoh, ketika pengguna menambahkan seorang pengguna instansi luar ke dalam daftar, aktivitas dari pengguna instansi luar tidak akan disampaikan ke instansi apabila tidak ada pengguna lokal yang mengikuti pengguna tersebut, dengan begitu akun proksilah yang akan mengikutinya."
 host: "Host"
+selectSelf: "Pilih diri sendiri"
 selectUser: "Pilih pengguna"
 recipient: "Penerima"
 annotation: "Keterangan konten"
@@ -226,6 +233,7 @@ blockedInstances: "Instansi terblokir"
 blockedInstancesDescription: "Daftar nama host dari instansi yang diperlukan untuk diblokir. Instansi yang didaftarkan tidak akan dapat berkomunikasi dengan instansi ini."
 silencedInstances: "Instansi yang disenyapkan"
 silencedInstancesDescription: "Daftar nama host dari instansi yang ingin kamu senyapkan. Semua akun dari instansi yang terdaftar akan diperlakukan sebagai disenyapkan. Hal ini membuat akun hanya dapat membuat permintaan mengikuti, dan tidak dapat menyebutkan akun lokal apabila tidak mengikuti. Hal ini tidak akan mempengaruhi instansi yang diblokir."
+federationAllowedHosts: "Server yang membolehkan federasi"
 muteAndBlock: "Bisukan / Blokir"
 mutedUsers: "Pengguna yang dibisukan"
 blockedUsers: "Pengguna yang diblokir"
@@ -316,6 +324,7 @@ selectFile: "Pilih berkas"
 selectFiles: "Pilih berkas"
 selectFolder: "Pilih folder"
 selectFolders: "Pilih folder"
+fileNotSelected: "Tidak ada file yang dipilih"
 renameFile: "Ubah nama berkas"
 folderName: "Nama folder"
 createFolder: "Buat folder"
@@ -323,6 +332,7 @@ renameFolder: "Ubah nama folder"
 deleteFolder: "Hapus folder"
 folder: "Folder"
 addFile: "Tambahkan berkas"
+showFile: "Tampilkan berkas"
 emptyDrive: "Drive kosong"
 emptyFolder: "Folder kosong"
 unableToDelete: "Tidak dapat menghapus"
@@ -497,7 +507,8 @@ uiLanguage: "Bahasa antarmuka pengguna"
 aboutX: "Tentang {x}"
 emojiStyle: "Gaya emoji"
 native: "Native"
-disableDrawer: "Jangan gunakan menu bergaya laci"
+menuStyle: "Gaya menu"
+style: "Gaya"
 showNoteActionsOnlyHover: "Hanya tampilkan aksi catatan saat ditunjuk"
 showReactionsCount: "Lihat jumlah reaksi dalam catatan"
 noHistory: "Tidak ada riwayat"
@@ -696,10 +707,7 @@ abuseReported: "Laporan kamu telah dikirimkan. Terima kasih."
 reporter: "Pelapor"
 reporteeOrigin: "Yang dilaporkan"
 reporterOrigin: "Pelapor"
-forwardReport: "Teruskan laporan ke instansi luar"
-forwardReportIsAnonymous: "Untuk melindungi privasi akun kamu, akun anonim dari sistem akan digunakan sebagai pelapor pada instansi luar."
 send: "Kirim"
-abuseMarkAsResolved: "Tandai laporan sebagai selesai"
 openInNewTab: "Buka di tab baru"
 openInSideView: "Buka di tampilan samping"
 defaultNavigationBehaviour: "Navigasi bawaan"
@@ -924,6 +932,9 @@ oneHour: "1 Jam"
 oneDay: "1 Hari"
 oneWeek: "1 Bulan"
 oneMonth: "satu bulan"
+threeMonths: "3 bulan"
+oneYear: "1 tahun"
+threeDays: "3 hari"
 reflectMayTakeTime: "Mungkin perlu beberapa saat untuk dicerminkan."
 failedToFetchAccountInformation: "Gagal untuk mendapatkan informasi akun"
 rateLimitExceeded: "Batas sudah terlampaui"
@@ -1098,6 +1109,7 @@ preservedUsernames: "Nama pengguna tercadangkan"
 preservedUsernamesDescription: "Daftar nama pengguna yang dicadangkan dipisah dengan baris baru. Nama pengguna berikut akan tidak dapat dipakai pada pembuatan akun normal, namun dapat digunakan oleh admin untuk membuat akun baru. Akun yang sudah ada dengan menggunakan nama pengguna ini tidak akan terpengaruh."
 createNoteFromTheFile: "Buat catatan dari berkas ini"
 archive: "Arsipkan"
+archived: "Diarsipkan"
 channelArchiveConfirmTitle: "Yakin untuk mengarsipkan {name}?"
 channelArchiveConfirmDescription: "Kanal yang diarsipkan tidak akan muncul pada daftar kanal atau hasil pencarian. Postingan baru juga tidak dapat ditambahkan lagi."
 thisChannelArchived: "Kanal ini telah diarsipkan."
@@ -1108,6 +1120,7 @@ preventAiLearning: "Tolak penggunaan Pembelajaran Mesin (AI Generatif)"
 preventAiLearningDescription: "Minta perayap web untuk tidak menggunakan materi teks atau gambar yang telah diposting ke dalam set data Pembelajaran Mesin (Prediktif / Generatif). Hal ini dicapai dengan menambahkan flag HTML-Response \"noai\" ke masing-masing konten. Pencegahan penuh mungkin tidak dapat dicapai dengan flag ini, karena juga dapat diabaikan begitu saja."
 options: "Opsi peran"
 specifyUser: "Pengguna spesifik"
+openTagPageConfirm: "Apakah ingin membuka laman tagar?"
 failedToPreviewUrl: "Tidak dapat dipratinjau"
 update: "Perbarui"
 rolesThatCanBeUsedThisEmojiAsReaction: "Peran yang dapat menggunakan emoji ini sebagai reaksi"
@@ -1239,6 +1252,19 @@ keepOriginalFilenameDescription: "Apabila pengaturan ini dimatikan, nama berkas
 noDescription: "Tidak ada deskripsi"
 alwaysConfirmFollow: "Selalu konfirmasi ketika mengikuti"
 inquiry: "Hubungi kami"
+tryAgain: "Silahkan coba lagi."
+createdLists: "Senarai yang dibuat"
+createdAntennas: "Antena yang dibuat"
+fromX: "Dari {x}"
+noteOfThisUser: "Catatan oleh pengguna ini"
+clipNoteLimitExceeded: "Klip ini tak bisa ditambahi lagi catatan."
+performance: "Kinerja"
+modified: "Diubah"
+thereAreNChanges: "Ada {n} perubahan"
+prohibitedWordsForNameOfUser: "Kata yang dilarang untuk nama pengguna"
+_abuseUserReport:
+  accept: "Setuju"
+  reject: "Tolak"
 _delivery:
   status: "Status pengiriman"
   stop: "Ditangguhkan"
@@ -1703,6 +1729,8 @@ _role:
     canSearchNotes: "Penggunaan pencarian catatan"
     canUseTranslator: "Penggunaan penerjemah"
     avatarDecorationLimit: "Jumlah maksimum dekorasi avatar yang dapat diterapkan"
+    canImportAntennas: "Izinkan mengimpor antena"
+    canImportUserLists: "Izinkan mengimpor senarai"
   _condition:
     roleAssignedTo: "Ditugaskan ke peran manual"
     isLocal: "Pengguna lokal"
@@ -1739,7 +1767,7 @@ _emailUnavailable:
   smtp: "Peladen alamat surel ini tidak merespon"
   banned: "Kamu tidak dapat mendaftar dengan alamat surel ini"
 _ffVisibility:
-  public: "Terbitkan"
+  public: "Publik"
   followers: "Tampil untuk pengikut saja"
   private: "Tersembunyi"
 _signup:
@@ -1920,7 +1948,6 @@ _theme:
     buttonBg: "Latar belakang tombol"
     buttonHoverBg: "Latar belakang tombol (Mengambang)"
     inputBorder: "Batas bidang masukan"
-    listItemHoverBg: "Latar belakang daftar item (Mengambang)"
     driveFolderBg: "Latar belakang folder drive"
     wallpaperOverlay: "Lapisan wallpaper"
     badge: "Lencana"
@@ -1932,8 +1959,6 @@ _sfx:
   note: "Catatan"
   noteMy: "Catatan (Saya)"
   notification: "Notifikasi"
-  antenna: "Penerimaan Antenna"
-  channel: "Notifikasi Kanal"
   reaction: "Ketika memilih reaksi"
 _soundSettings:
   driveFile: "Menggunakan berkas audio dalam Drive"
@@ -1942,6 +1967,7 @@ _soundSettings:
   driveFileTypeWarnDescription: "Pilih berkas audio"
   driveFileDurationWarn: "Audio ini terlalu panjang"
   driveFileDurationWarnDescription: "Audio panjang dapat mengganggu penggunaan Misskey. Masih ingin melanjutkan?"
+  driveFileError: "Tak bisa memuat audio. Mohon ubah pengaturan"
 _ago:
   future: "Masa depan"
   justNow: "Baru saja"
@@ -2349,6 +2375,7 @@ _notification:
     followRequestAccepted: "Permintaan mengikuti disetujui"
     roleAssigned: "Peran Diberikan"
     achievementEarned: "Pencapaian didapatkan"
+    login: "Masuk"
     app: "Notifikasi dari aplikasi tertaut"
   _actions:
     followBack: "Ikuti Kembali"
@@ -2396,9 +2423,9 @@ _drivecleaner:
   orderByCreatedAtAsc: "Tanggal (Naik)"
 _webhookSettings:
   createWebhook: "Buat Webhook"
+  modifyWebhook: "Sunting Webhook"
   name: "Nama"
   secret: "Secret"
-  events: "Webhook Events"
   active: "Aktif"
   _events:
     follow: "Ketika mengikuti pengguna"
@@ -2408,6 +2435,13 @@ _webhookSettings:
     renote: "Ketika direnote"
     reaction: "Ketika menerima reaksi"
     mention: "Ketika sedang disebut"
+  deleteConfirm: "Apakah kamu yakin ingin menghapus Webhook?"
+_abuseReport:
+  _notificationRecipient:
+    _recipientType:
+      mail: "Surel"
+      webhook: "Webhook"
+    keywords: "Kata kunci"
 _moderationLogTypes:
   createRole: "Peran telah dibuat"
   deleteRole: "Peran telah dihapus"
@@ -2445,6 +2479,7 @@ _moderationLogTypes:
   deleteAvatarDecoration: "Hapus dekorasi avatar"
   unsetUserAvatar: "Hapus avatar pengguna"
   unsetUserBanner: "Hapus banner pengguna"
+  deleteAccount: "Akun dihapus"
 _fileViewer:
   title: "Rincian berkas"
   type: "Jenis berkas"
diff --git a/locales/index.d.ts b/locales/index.d.ts
index 0b1b86d373..440f24ac84 100644
--- a/locales/index.d.ts
+++ b/locales/index.d.ts
@@ -48,6 +48,20 @@ export interface Locale extends ILocale {
      * パスワード
      */
     "password": string;
+    /**
+     * 初期設定開始用パスワード
+     */
+    "initialPasswordForSetup": string;
+    /**
+     * 初期設定開始用のパスワードが違います。
+     */
+    "initialPasswordIsIncorrect": string;
+    /**
+     * Misskeyを自分でインストールした場合は、設定ファイルに入力したパスワードを使用してください。
+     * Misskeyのホスティングサービスなどを使用している場合は、提供されたパスワードを使用してください。
+     * パスワードを設定していない場合は、空欄にしたまま続行してください。
+     */
+    "initialPasswordForSetupDescription": string;
     /**
      * パスワードを忘れた
      */
@@ -256,6 +270,10 @@ export interface Locale extends ILocale {
      * ユーザーを検索
      */
     "searchUser": string;
+    /**
+     * ユーザーのノートを検索
+     */
+    "searchThisUsersNotes": string;
     /**
      * 返信
      */
@@ -632,6 +650,10 @@ export interface Locale extends ILocale {
      * アンテナを編集
      */
     "editAntenna": string;
+    /**
+     * アンテナを作成
+     */
+    "createAntenna": string;
     /**
      * ウィジェットを選択
      */
@@ -736,6 +758,22 @@ export interface Locale extends ILocale {
      * リモートで表示
      */
     "showOnRemote": string;
+    /**
+     * リモートで続行
+     */
+    "continueOnRemote": string;
+    /**
+     * Misskey Hubからサーバーを選択
+     */
+    "chooseServerOnMisskeyHub": string;
+    /**
+     * サーバーのドメインを直接指定
+     */
+    "specifyServerHost": string;
+    /**
+     * ドメインを入力してください
+     */
+    "inputHostName": string;
     /**
      * 全般
      */
@@ -776,6 +814,10 @@ export interface Locale extends ILocale {
      * ホスト
      */
     "host": string;
+    /**
+     * 自分を選択
+     */
+    "selectSelf": string;
     /**
      * ユーザーを選択
      */
@@ -836,6 +878,10 @@ export interface Locale extends ILocale {
      * サーバーをサイレンス
      */
     "silenceThisInstance": string;
+    /**
+     * サーバーをメディアサイレンス
+     */
+    "mediaSilenceThisInstance": string;
     /**
      * 操作
      */
@@ -920,6 +966,22 @@ export interface Locale extends ILocale {
      * サイレンスしたいサーバーのホストを改行で区切って設定します。サイレンスされたサーバーに所属するアカウントはすべて「サイレンス」として扱われ、フォローがすべてリクエストになります。ブロックしたインスタンスには影響しません。
      */
     "silencedInstancesDescription": string;
+    /**
+     * メディアサイレンスしたサーバー
+     */
+    "mediaSilencedInstances": string;
+    /**
+     * メディアサイレンスしたいサーバーのホストを改行で区切って設定します。メディアサイレンスされたサーバーに所属するアカウントによるファイルはすべてセンシティブとして扱われ、カスタム絵文字が使用できないようになります。ブロックしたインスタンスには影響しません。
+     */
+    "mediaSilencedInstancesDescription": string;
+    /**
+     * 連合を許可するサーバー
+     */
+    "federationAllowedHosts": string;
+    /**
+     * 連合を許可するサーバーのホストを改行で区切って設定します。
+     */
+    "federationAllowedHostsDescription": string;
     /**
      * ミュートとブロック
      */
@@ -1312,6 +1374,10 @@ export interface Locale extends ILocale {
      * ファイルを追加
      */
     "addFile": string;
+    /**
+     * ファイルを表示
+     */
+    "showFile": string;
     /**
      * ドライブは空です
      */
@@ -1768,6 +1834,10 @@ export interface Locale extends ILocale {
      * モデレーションノート
      */
     "moderationNote": string;
+    /**
+     * モデレーター間でだけ共有されるメモを記入することができます。
+     */
+    "moderationNoteDescription": string;
     /**
      * モデレーションノートを追加する
      */
@@ -1921,9 +1991,13 @@ export interface Locale extends ILocale {
      */
     "onlyOneFileCanBeAttached": string;
     /**
-     * 続行する前に、サインアップまたはサインインが必要です
+     * 続行する前に、登録またはログインが必要です
      */
     "signinRequired": string;
+    /**
+     * 続行するには、お使いのサーバーに移動するか、このサーバーに登録・ログインする必要があります
+     */
+    "signinOrContinueOnRemote": string;
     /**
      * 招待
      */
@@ -2009,9 +2083,21 @@ export interface Locale extends ILocale {
      */
     "native": string;
     /**
-     * メニューをドロワーで表示しない
+     * メニューのスタイル
      */
-    "disableDrawer": string;
+    "menuStyle": string;
+    /**
+     * スタイル
+     */
+    "style": string;
+    /**
+     * ドロワー
+     */
+    "drawer": string;
+    /**
+     * ポップアップ
+     */
+    "popup": string;
     /**
      * ノートのアクションをホバー時のみ表示する
      */
@@ -2340,6 +2426,14 @@ export interface Locale extends ILocale {
      * スクラッチパッドは、AiScriptの実験環境を提供します。Misskeyと対話するコードの記述、実行、結果の確認ができます。
      */
     "scratchpadDescription": string;
+    /**
+     * UIインスペクター
+     */
+    "uiInspector": string;
+    /**
+     * メモリ上に存在しているUIコンポーネントのインスタンスの一覧を見ることができます。UIコンポーネントはUi:C:系関数により生成されます。
+     */
+    "uiInspectorDescription": string;
     /**
      * 出力
      */
@@ -2785,7 +2879,7 @@ export interface Locale extends ILocale {
      */
     "reportAbuseOf": ParameterizedString<"name">;
     /**
-     * 通報理由の詳細を記入してください。対象のノートがある場合はそのURLも記入してください。
+     * 通報理由の詳細を記入してください。対象のノートやページなどがある場合はそのURLも記入してください。
      */
     "fillAbuseReportDescription": string;
     /**
@@ -2804,22 +2898,10 @@ export interface Locale extends ILocale {
      * 通報元
      */
     "reporterOrigin": string;
-    /**
-     * リモートサーバーに通報を転送する
-     */
-    "forwardReport": string;
-    /**
-     * リモートサーバーからはあなたの情報は見れず、匿名のシステムアカウントとして表示されます。
-     */
-    "forwardReportIsAnonymous": string;
     /**
      * 送信
      */
     "send": string;
-    /**
-     * 対応済みにする
-     */
-    "abuseMarkAsResolved": string;
     /**
      * 新しいタブで開く
      */
@@ -3077,7 +3159,7 @@ export interface Locale extends ILocale {
      */
     "narrow": string;
     /**
-     * 設定はページリロード後に反映されます。今すぐリロードしますか?
+     * 設定はページリロード後に反映されます。
      */
     "reloadToApplySetting": string;
     /**
@@ -3624,6 +3706,10 @@ export interface Locale extends ILocale {
      * パスワードが間違っています。
      */
     "incorrectPassword": string;
+    /**
+     * ワンタイムパスワードが間違っているか、期限切れになっています。
+     */
+    "incorrectTotp": string;
     /**
      * 「{choice}」に投票しますか?
      */
@@ -3720,6 +3806,18 @@ export interface Locale extends ILocale {
      * 1ヶ月
      */
     "oneMonth": string;
+    /**
+     * 3ヶ月
+     */
+    "threeMonths": string;
+    /**
+     * 1年
+     */
+    "oneYear": string;
+    /**
+     * 3日
+     */
+    "threeDays": string;
     /**
      * 反映されるまで時間がかかる場合があります。
      */
@@ -4280,6 +4378,10 @@ export interface Locale extends ILocale {
      * リモートサーバーのチャートを生成
      */
     "enableChartsForFederatedInstances": string;
+    /**
+     * リモートサーバーの情報を取得
+     */
+    "enableStatsForFederatedInstances": string;
     /**
      * ノートのアクションにクリップを追加
      */
@@ -4420,6 +4522,14 @@ export interface Locale extends ILocale {
      * アーカイブ
      */
     "archive": string;
+    /**
+     * アーカイブ済み
+     */
+    "archived": string;
+    /**
+     * アーカイブ解除
+     */
+    "unarchive": string;
     /**
      * {name}をアーカイブしますか?
      */
@@ -4460,6 +4570,18 @@ export interface Locale extends ILocale {
      * ユーザー指定
      */
     "specifyUser": string;
+    /**
+     * 照会しますか?
+     */
+    "lookupConfirm": string;
+    /**
+     * ハッシュタグのページを開きますか?
+     */
+    "openTagPageConfirm": string;
+    /**
+     * ホスト指定
+     */
+    "specifyHost": string;
     /**
      * プレビューできません
      */
@@ -4984,6 +5106,191 @@ export interface Locale extends ILocale {
      * お問い合わせ
      */
     "inquiry": string;
+    /**
+     * もう一度お試しください。
+     */
+    "tryAgain": string;
+    /**
+     * センシティブなメディアを表示するとき確認する
+     */
+    "confirmWhenRevealingSensitiveMedia": string;
+    /**
+     * センシティブなメディアです。表示しますか?
+     */
+    "sensitiveMediaRevealConfirm": string;
+    /**
+     * 作成したリスト
+     */
+    "createdLists": string;
+    /**
+     * 作成したアンテナ
+     */
+    "createdAntennas": string;
+    /**
+     * {x}から
+     */
+    "fromX": ParameterizedString<"x">;
+    /**
+     * 埋め込みコードを生成
+     */
+    "genEmbedCode": string;
+    /**
+     * このユーザーのノート一覧
+     */
+    "noteOfThisUser": string;
+    /**
+     * これ以上このクリップにノートを追加できません。
+     */
+    "clipNoteLimitExceeded": string;
+    /**
+     * パフォーマンス
+     */
+    "performance": string;
+    /**
+     * 変更あり
+     */
+    "modified": string;
+    /**
+     * 破棄
+     */
+    "discard": string;
+    /**
+     * {n}件の変更があります
+     */
+    "thereAreNChanges": ParameterizedString<"n">;
+    /**
+     * パスキーでログイン
+     */
+    "signinWithPasskey": string;
+    /**
+     * 登録されていないパスキーです。
+     */
+    "unknownWebAuthnKey": string;
+    /**
+     * パスキーの検証に失敗しました。
+     */
+    "passkeyVerificationFailed": string;
+    /**
+     * パスキーの検証に成功しましたが、パスワードレスログインが無効になっています。
+     */
+    "passkeyVerificationSucceededButPasswordlessLoginDisabled": string;
+    /**
+     * フォロワーへのメッセージ
+     */
+    "messageToFollower": string;
+    /**
+     * 対象
+     */
+    "target": string;
+    /**
+     * CAPTCHAのテストを目的とした機能です。<strong>本番環境で使用しないでください。</strong>
+     */
+    "testCaptchaWarning": string;
+    /**
+     * 禁止ワード(ユーザーの名前)
+     */
+    "prohibitedWordsForNameOfUser": string;
+    /**
+     * このリストに含まれる文字列がユーザーの名前に含まれる場合、ユーザーの名前の変更を拒否します。モデレーター権限を持つユーザーはこの制限の影響を受けません。
+     */
+    "prohibitedWordsForNameOfUserDescription": string;
+    /**
+     * 変更しようとした名前に禁止された文字列が含まれています
+     */
+    "yourNameContainsProhibitedWords": string;
+    /**
+     * 名前に禁止されている文字列が含まれています。この名前を使用したい場合は、サーバー管理者にお問い合わせください。
+     */
+    "yourNameContainsProhibitedWordsDescription": string;
+    /**
+     * 投稿者により、表示にはログインが必要と設定されています
+     */
+    "thisContentsAreMarkedAsSigninRequiredByAuthor": string;
+    /**
+     * ロックダウン
+     */
+    "lockdown": string;
+    /**
+     * アカウントを選択してください
+     */
+    "pleaseSelectAccount": string;
+    /**
+     * 利用可能なロール
+     */
+    "availableRoles": string;
+    "_accountSettings": {
+        /**
+         * コンテンツの表示にログインを必須にする
+         */
+        "requireSigninToViewContents": string;
+        /**
+         * あなたが作成した全てのノートなどのコンテンツを表示するのにログインを必須にします。クローラーに情報が収集されるのを防ぐ効果が期待できます。
+         */
+        "requireSigninToViewContentsDescription1": string;
+        /**
+         * URLプレビュー(OGP)、Webページへの埋め込み、ノートの引用に対応していないサーバーからの表示も不可になります。
+         */
+        "requireSigninToViewContentsDescription2": string;
+        /**
+         * リモートサーバーに連合されたコンテンツでは、これらの制限が適用されない場合があります。
+         */
+        "requireSigninToViewContentsDescription3": string;
+        /**
+         * 過去のノートをフォロワーのみ表示可能にする
+         */
+        "makeNotesFollowersOnlyBefore": string;
+        /**
+         * この機能が有効になっている間、設定された日時より過去、または設定された時間を経過しているノートがフォロワーのみ表示可能になります。無効に戻すと、ノートの公開状態も元に戻ります。
+         */
+        "makeNotesFollowersOnlyBeforeDescription": string;
+        /**
+         * 過去のノートを非公開化する
+         */
+        "makeNotesHiddenBefore": string;
+        /**
+         * この機能が有効になっている間、設定された日時より過去、または設定された時間を経過しているノートが自分のみ表示可能(非公開化)になります。無効に戻すと、ノートの公開状態も元に戻ります。
+         */
+        "makeNotesHiddenBeforeDescription": string;
+        /**
+         * リモートサーバーに連合されたノートには効果が及ばない場合があります。
+         */
+        "mayNotEffectForFederatedNotes": string;
+        /**
+         * 指定した時間を経過しているノート
+         */
+        "notesHavePassedSpecifiedPeriod": string;
+        /**
+         * 指定した日時より前のノート
+         */
+        "notesOlderThanSpecifiedDateAndTime": string;
+    };
+    "_abuseUserReport": {
+        /**
+         * 転送
+         */
+        "forward": string;
+        /**
+         * 匿名のシステムアカウントとして、リモートサーバーに通報を転送します。
+         */
+        "forwardDescription": string;
+        /**
+         * 解決
+         */
+        "resolve": string;
+        /**
+         * 是認
+         */
+        "accept": string;
+        /**
+         * 否認
+         */
+        "reject": string;
+        /**
+         * 内容が正当である通報に対応した場合は「是認」を選択し、肯定的にケースが解決されたことをマークします。
+         * 内容が正当でない通報の場合は「否認」を選択し、否定的にケースが解決されたことをマークします。
+         */
+        "resolveTutorial": string;
+    };
     "_delivery": {
         /**
          * 配信状態
@@ -5475,6 +5782,10 @@ export interface Locale extends ILocale {
          * 有効にすると、タイムラインがキャッシュされていない場合にDBへ追加で問い合わせを行うフォールバック処理を行います。無効にすると、フォールバック処理を行わないことでさらにサーバーの負荷を軽減することができますが、タイムラインが取得できる範囲に制限が生じます。
          */
         "fanoutTimelineDbFallbackDescription": string;
+        /**
+         * 有効にすると、リアクション作成時のパフォーマンスが大幅に向上し、データベースへの負荷を軽減することが可能です。ただし、Redisのメモリ使用量は増加します。
+         */
+        "reactionsBufferingDescription": string;
         /**
          * 問い合わせ先URL
          */
@@ -5483,6 +5794,10 @@ export interface Locale extends ILocale {
          * サーバー運営者へのお問い合わせフォームのURLや、運営者の連絡先等が記載されたWebページのURLを指定します。
          */
         "inquiryUrlDescription": string;
+        /**
+         * 一定期間モデレーターのアクティビティが検出されなかった場合、スパム防止のためこの設定は自動でオフになります。
+         */
+        "thisSettingWillAutomaticallyOffWhenModeratorsInactive": string;
     };
     "_accountMigration": {
         /**
@@ -6594,6 +6909,10 @@ export interface Locale extends ILocale {
              * ファイルにNSFWを常に付与
              */
             "alwaysMarkNsfw": string;
+            /**
+             * アイコンとバナーの更新を許可
+             */
+            "canUpdateBioMedia": string;
             /**
              * ノートのピン留めの最大数
              */
@@ -6650,6 +6969,26 @@ export interface Locale extends ILocale {
              * アイコンデコレーションの最大取付個数
              */
             "avatarDecorationLimit": string;
+            /**
+             * アンテナのインポートを許可
+             */
+            "canImportAntennas": string;
+            /**
+             * ブロックのインポートを許可
+             */
+            "canImportBlocking": string;
+            /**
+             * フォローのインポートを許可
+             */
+            "canImportFollowing": string;
+            /**
+             * ミュートのインポートを許可
+             */
+            "canImportMuting": string;
+            /**
+             * リストのインポートを許可
+             */
+            "canImportUserLists": string;
         };
         "_condition": {
             /**
@@ -7468,10 +7807,6 @@ export interface Locale extends ILocale {
              * 入力ボックスの縁取り
              */
             "inputBorder": string;
-            /**
-             * リスト項目の背景 (ホバー)
-             */
-            "listItemHoverBg": string;
             /**
              * ドライブフォルダーの背景
              */
@@ -7515,14 +7850,6 @@ export interface Locale extends ILocale {
          * 通知
          */
         "notification": string;
-        /**
-         * アンテナ受信
-         */
-        "antenna": string;
-        /**
-         * チャンネル通知
-         */
-        "channel": string;
         /**
          * リアクション選択時
          */
@@ -7553,6 +7880,10 @@ export interface Locale extends ILocale {
          * 長い音声を使用するとMisskeyの使用に支障をきたす可能性があります。それでも続行しますか?
          */
         "driveFileDurationWarnDescription": string;
+        /**
+         * 音声が読み込めませんでした。設定を変更してください
+         */
+        "driveFileError": string;
     };
     "_ago": {
         /**
@@ -8125,14 +8456,26 @@ export interface Locale extends ILocale {
          * アプリケーションに戻っています
          */
         "callback": string;
+        /**
+         * アクセスを許可しました
+         */
+        "accepted": string;
         /**
          * アクセスを拒否しました
          */
         "denied": string;
+        /**
+         * 以下のユーザーとして操作しています
+         */
+        "scopeUser": string;
         /**
          * アプリケーションにアクセス許可を与えるには、ログインが必要です。
          */
         "pleaseLogin": string;
+        /**
+         * アクセスを許可すると、自動で以下のURLに遷移します
+         */
+        "byClickingYouWillBeRedirectedToThisUrl": string;
     };
     "_antennaSources": {
         /**
@@ -8545,6 +8888,18 @@ export interface Locale extends ILocale {
          * 最大{max}つまでデコレーションを付けられます。
          */
         "avatarDecorationMax": ParameterizedString<"max">;
+        /**
+         * フォローされた時のメッセージ
+         */
+        "followedMessage": string;
+        /**
+         * フォローされた時に相手に表示する短いメッセージを設定できます。
+         */
+        "followedMessageDescription": string;
+        /**
+         * フォローを承認制にしている場合、フォローリクエストを許可した時に表示されます。
+         */
+        "followedMessageDescriptionForLockedAccount": string;
     };
     "_exportOrImport": {
         /**
@@ -8901,6 +9256,10 @@ export interface Locale extends ILocale {
          * ブロックを追加
          */
         "chooseBlock": string;
+        /**
+         * セクションタイトルを入力
+         */
+        "enterSectionTitle": string;
         /**
          * 種類を選択
          */
@@ -8998,7 +9357,7 @@ export interface Locale extends ILocale {
          */
         "youGotQuote": ParameterizedString<"name">;
         /**
-         * {name}がRenoteしました
+         * {name}がリノートしました
          */
         "youRenoted": ParameterizedString<"name">;
         /**
@@ -9073,6 +9432,14 @@ export interface Locale extends ILocale {
          * 通知の履歴をリセットする
          */
         "flushNotification": string;
+        /**
+         * {x}のエクスポートが完了しました
+         */
+        "exportOfXCompleted": ParameterizedString<"x">;
+        /**
+         * ログインがありました
+         */
+        "login": string;
         "_types": {
             /**
              * すべて
@@ -9095,7 +9462,7 @@ export interface Locale extends ILocale {
              */
             "reply": string;
             /**
-             * Renote
+             * リノート
              */
             "renote": string;
             /**
@@ -9126,6 +9493,18 @@ export interface Locale extends ILocale {
              * 実績の獲得
              */
             "achievementEarned": string;
+            /**
+             * エクスポートが完了した
+             */
+            "exportCompleted": string;
+            /**
+             * ログイン
+             */
+            "login": string;
+            /**
+             * 通知のテスト
+             */
+            "test": string;
             /**
              * 連携アプリからの通知
              */
@@ -9141,7 +9520,7 @@ export interface Locale extends ILocale {
              */
             "reply": string;
             /**
-             * Renote
+             * リノート
              */
             "renote": string;
         };
@@ -9305,6 +9684,10 @@ export interface Locale extends ILocale {
          * Webhookを作成
          */
         "createWebhook": string;
+        /**
+         * Webhookを編集
+         */
+        "modifyWebhook": string;
         /**
          * 名前
          */
@@ -9314,9 +9697,9 @@ export interface Locale extends ILocale {
          */
         "secret": string;
         /**
-         * Webhookを実行するタイミング
+         * トリガー
          */
-        "events": string;
+        "trigger": string;
         /**
          * 有効
          */
@@ -9351,6 +9734,88 @@ export interface Locale extends ILocale {
              */
             "mention": string;
         };
+        "_systemEvents": {
+            /**
+             * ユーザーから通報があったとき
+             */
+            "abuseReport": string;
+            /**
+             * ユーザーからの通報を処理したとき
+             */
+            "abuseReportResolved": string;
+            /**
+             * ユーザーが作成されたとき
+             */
+            "userCreated": string;
+            /**
+             * モデレーターが一定期間非アクティブになったとき
+             */
+            "inactiveModeratorsWarning": string;
+            /**
+             * モデレーターが一定期間非アクティブだったため、システムにより招待制へと変更されたとき
+             */
+            "inactiveModeratorsInvitationOnlyChanged": string;
+        };
+        /**
+         * Webhookを削除しますか?
+         */
+        "deleteConfirm": string;
+        /**
+         * スイッチの右にあるボタンをクリックするとダミーのデータを使用したテスト用Webhookを送信できます。
+         */
+        "testRemarks": string;
+    };
+    "_abuseReport": {
+        "_notificationRecipient": {
+            /**
+             * 通報の通知先を追加
+             */
+            "createRecipient": string;
+            /**
+             * 通報の通知先を編集
+             */
+            "modifyRecipient": string;
+            /**
+             * 通知先の種類
+             */
+            "recipientType": string;
+            "_recipientType": {
+                /**
+                 * メール
+                 */
+                "mail": string;
+                /**
+                 * Webhook
+                 */
+                "webhook": string;
+                "_captions": {
+                    /**
+                     * モデレーター権限を持つユーザーのメールアドレスに通知を送ります(通報を受けた時のみ)
+                     */
+                    "mail": string;
+                    /**
+                     * 指定したSystemWebhookに通知を送ります(通報を受けた時と通報を解決した時にそれぞれ発信)
+                     */
+                    "webhook": string;
+                };
+            };
+            /**
+             * キーワード
+             */
+            "keywords": string;
+            /**
+             * 通知先ユーザー
+             */
+            "notifiedUser": string;
+            /**
+             * 使用するWebhook
+             */
+            "notifiedWebhook": string;
+            /**
+             * 通知先を削除しますか?
+             */
+            "deleteConfirm": string;
+        };
     };
     "_moderationLogTypes": {
         /**
@@ -9461,6 +9926,14 @@ export interface Locale extends ILocale {
          * 通報を解決
          */
         "resolveAbuseReport": string;
+        /**
+         * 通報を転送
+         */
+        "forwardAbuseReport": string;
+        /**
+         * 通報のモデレーションノート更新
+         */
+        "updateAbuseReportNote": string;
         /**
          * 招待コードを作成
          */
@@ -9497,6 +9970,46 @@ export interface Locale extends ILocale {
          * ユーザーのバナーを解除
          */
         "unsetUserBanner": string;
+        /**
+         * SystemWebhookを作成
+         */
+        "createSystemWebhook": string;
+        /**
+         * SystemWebhookを更新
+         */
+        "updateSystemWebhook": string;
+        /**
+         * SystemWebhookを削除
+         */
+        "deleteSystemWebhook": string;
+        /**
+         * 通報の通知先を作成
+         */
+        "createAbuseReportNotificationRecipient": string;
+        /**
+         * 通報の通知先を更新
+         */
+        "updateAbuseReportNotificationRecipient": string;
+        /**
+         * 通報の通知先を削除
+         */
+        "deleteAbuseReportNotificationRecipient": string;
+        /**
+         * アカウントを削除
+         */
+        "deleteAccount": string;
+        /**
+         * ページを削除
+         */
+        "deletePage": string;
+        /**
+         * Playを削除
+         */
+        "deleteFlash": string;
+        /**
+         * ギャラリーの投稿を削除
+         */
+        "deleteGalleryPost": string;
     };
     "_fileViewer": {
         /**
@@ -9667,7 +10180,7 @@ export interface Locale extends ILocale {
     "_dataSaver": {
         "_media": {
             /**
-             * メディアの読み込み
+             * メディアの読み込みを無効化
              */
             "title": string;
             /**
@@ -9677,7 +10190,7 @@ export interface Locale extends ILocale {
         };
         "_avatar": {
             /**
-             * アイコン画像
+             * アイコン画像のアニメーションを無効化
              */
             "title": string;
             /**
@@ -9687,7 +10200,7 @@ export interface Locale extends ILocale {
         };
         "_urlPreview": {
             /**
-             * URLプレビューのサムネイル
+             * URLプレビューのサムネイルを非表示
              */
             "title": string;
             /**
@@ -9697,7 +10210,7 @@ export interface Locale extends ILocale {
         };
         "_code": {
             /**
-             * コードハイライト
+             * コードハイライトを非表示
              */
             "title": string;
             /**
@@ -9972,6 +10485,100 @@ export interface Locale extends ILocale {
          */
         "loop": string;
     };
+    "_contextMenu": {
+        /**
+         * コンテキストメニュー
+         */
+        "title": string;
+        /**
+         * アプリケーション
+         */
+        "app": string;
+        /**
+         * Shiftキーでアプリケーション
+         */
+        "appWithShift": string;
+        /**
+         * ブラウザのUI
+         */
+        "native": string;
+    };
+    "_embedCodeGen": {
+        /**
+         * 埋め込みコードをカスタマイズ
+         */
+        "title": string;
+        /**
+         * ヘッダーを表示
+         */
+        "header": string;
+        /**
+         * 自動で続きを読み込む(非推奨)
+         */
+        "autoload": string;
+        /**
+         * 高さの最大値
+         */
+        "maxHeight": string;
+        /**
+         * 0で最大値の設定が無効になります。ウィジェットが縦に伸び続けるのを防ぐために、何らかの値に指定してください。
+         */
+        "maxHeightDescription": string;
+        /**
+         * 高さの最大値制限が無効(0)になっています。これが意図した変更ではない場合は、高さの最大値を何らかの値に設定してください。
+         */
+        "maxHeightWarn": string;
+        /**
+         * プレビュー画面で表示可能な範囲を超えたため、実際に埋め込んだ際とは表示が異なります。
+         */
+        "previewIsNotActual": string;
+        /**
+         * 角丸にする
+         */
+        "rounded": string;
+        /**
+         * 外枠に枠線をつける
+         */
+        "border": string;
+        /**
+         * プレビューに反映
+         */
+        "applyToPreview": string;
+        /**
+         * 埋め込みコードを作成
+         */
+        "generateCode": string;
+        /**
+         * コードが生成されました
+         */
+        "codeGenerated": string;
+        /**
+         * 生成されたコードをウェブサイトに貼り付けてご利用ください。
+         */
+        "codeGeneratedDescription": string;
+    };
+    "_selfXssPrevention": {
+        /**
+         * 警告
+         */
+        "warning": string;
+        /**
+         * 「この画面に何か貼り付けろ」はすべて詐欺です。
+         */
+        "title": string;
+        /**
+         * ここに何かを貼り付けると、悪意のあるユーザーにアカウントを乗っ取られたり、個人情報を盗まれたりする可能性があります。
+         */
+        "description1": string;
+        /**
+         * 貼り付けようとしているものが何なのかを正確に理解していない場合は、%c今すぐ作業を中止してこのウィンドウを閉じてください。
+         */
+        "description2": string;
+        /**
+         * 詳しくはこちらをご確認ください。 {link}
+         */
+        "description3": ParameterizedString<"link">;
+    };
 }
 declare const locales: {
     [lang: string]: Locale;
diff --git a/locales/index.js b/locales/index.js
index 650e552337..091d216dee 100644
--- a/locales/index.js
+++ b/locales/index.js
@@ -15,6 +15,7 @@ const merge = (...args) => args.reduce((a, c) => ({
 
 const languages = [
 	'ar-SA',
+	'ca-ES',
 	'cs-CZ',
 	'da-DK',
 	'de-DE',
@@ -52,7 +53,11 @@ const primaries = {
 const clean = (text) => text.replace(new RegExp(String.fromCodePoint(0x08), 'g'), '');
 
 export function build() {
-	const locales = languages.reduce((a, c) => (a[c] = yaml.load(clean(fs.readFileSync(new URL(`${c}.yml`, import.meta.url), 'utf-8'))) || {}, a), {});
+	// vitestの挙動を調整するため、一度ローカル変数化する必要がある
+	// https://github.com/vitest-dev/vitest/issues/3988#issuecomment-1686599577
+	// https://github.com/misskey-dev/misskey/pull/14057#issuecomment-2192833785
+	const metaUrl = import.meta.url;
+	const locales = languages.reduce((a, c) => (a[c] = yaml.load(clean(fs.readFileSync(new URL(`${c}.yml`, metaUrl), 'utf-8'))) || {}, a), {});
 
 	// 空文字列が入ることがあり、フォールバックが動作しなくなるのでプロパティごと消す
 	const removeEmpty = (obj) => {
diff --git a/locales/it-IT.yml b/locales/it-IT.yml
index 1d12a62cca..8fb6dcd6f2 100644
--- a/locales/it-IT.yml
+++ b/locales/it-IT.yml
@@ -1,6 +1,6 @@
 ---
 _lang_: "Italiano"
-headlineMisskey: "Rete collegata tramite note"
+headlineMisskey: "Rete collegata tramite Note"
 introMisskey: "Eccoci! Misskey è un servizio di microblogging decentralizzato, libero e aperto. \n\n📡 Puoi pubblicare «Note» per condividere ciò che sta succedendo o per dire a tutti qualcosa su di te. \n\n👍 Puoi reagire inviando emoji rapidi alle «Note» provenienti da altri profili nel Fediverso.\n\n🚀 Esplora un nuovo mondo insieme a noi!"
 poweredByMisskeyDescription: "{name} è uno dei servizi (chiamati istanze) che utilizzano la piattaforma open source <b>Misskey</b>."
 monthAndDay: "{day}/{month}"
@@ -8,6 +8,9 @@ search: "Cerca"
 notifications: "Notifiche"
 username: "Nome utente"
 password: "Password"
+initialPasswordForSetup: "Password iniziale, per avviare le impostazioni"
+initialPasswordIsIncorrect: "Password iniziale, sbagliata."
+initialPasswordForSetupDescription: "Se hai installato Misskey di persona, usa la password che hai indicato nel file di configurazione.\nSe stai utilizzando un servizio di hosting Misskey, usa la password fornita dal gestore.\nSe non hai una password preimpostata, lascia il campo vuoto e continua."
 forgotPassword: "Hai dimenticato la password?"
 fetchingAsApObject: "Recuperando dal Fediverso..."
 ok: "OK"
@@ -60,11 +63,12 @@ copyFileId: "Copia ID del file"
 copyFolderId: "Copia ID della cartella"
 copyProfileUrl: "Copia URL del profilo"
 searchUser: "Cerca profilo"
+searchThisUsersNotes: "Cerca le sue Note"
 reply: "Rispondi"
 loadMore: "Mostra di più"
 showMore: "Espandi"
 showLess: "Comprimi"
-youGotNewFollower: "Adesso ti segue"
+youGotNewFollower: "Hai un nuovo Follower"
 receiveFollowRequest: "Hai ricevuto una richiesta di follow"
 followRequestAccepted: "Ha accettato la tua richiesta di follow"
 mention: "Menzioni"
@@ -76,14 +80,14 @@ export: "Esporta"
 files: "Allegati"
 download: "Scarica"
 driveFileDeleteConfirm: "Vuoi davvero eliminare il file \"{name}\", e le Note a cui è stato allegato?"
-unfollowConfirm: "Vuoi davvero smettere di seguire {name}?"
+unfollowConfirm: "Vuoi davvero togliere il Following a {name}?"
 exportRequested: "Hai richiesto un'esportazione, e potrebbe volerci tempo. Quando sarà compiuta, il file verrà aggiunto direttamente al Drive."
 importRequested: "Hai richiesto un'importazione. Potrebbe richiedere un po' di tempo."
 lists: "Liste"
 noLists: "Nessuna lista"
 note: "Nota"
 notes: "Note"
-following: "Follow"
+following: "Following"
 followers: "Follower"
 followsYou: "Follower"
 createList: "Aggiungi una nuova lista"
@@ -102,17 +106,20 @@ defaultNoteVisibility: "Privacy predefinita delle note"
 follow: "Segui"
 followRequest: "Richiesta di follow"
 followRequests: "Richieste di follow"
-unfollow: "Smetti di seguire"
+unfollow: "Togli Following"
 followRequestPending: "Richiesta in approvazione"
 enterEmoji: "Inserisci emoji"
 renote: "Rinota"
 unrenote: "Elimina la Rinota"
 renoted: "Rinotata!"
+renotedToX: "Rinota da {name}."
 cantRenote: "È impossibile rinotare questa nota."
 cantReRenote: "È impossibile rinotare una Rinota."
 quote: "Citazione"
 inChannelRenote: "Rinota nel canale"
 inChannelQuote: "Cita nel canale"
+renoteToChannel: "Rinota al canale"
+renoteToOtherChannel: "Rinota a un altro canale"
 pinnedNote: "Nota in primo piano"
 pinned: "Fissa sul profilo"
 you: "Tu"
@@ -151,6 +158,7 @@ editList: "Modifica Lista"
 selectChannel: "Seleziona canale"
 selectAntenna: "Scegli un'antenna"
 editAntenna: "Modifica Antenna"
+createAntenna: "Crea Antenna"
 selectWidget: "Seleziona il riquadro"
 editWidgets: "Modifica i riquadri"
 editWidgetsExit: "Conferma le modifiche"
@@ -177,16 +185,21 @@ addAccount: "Aggiungi profilo"
 reloadAccountsList: "Ricarica l'elenco dei profili"
 loginFailed: "Accesso non riuscito"
 showOnRemote: "Leggi sull'istanza remota"
+continueOnRemote: "Continua da remoto"
+chooseServerOnMisskeyHub: "Scegli l'istanza sul sito Misskey Hub"
+specifyServerHost: "Indica l'indirizzo dell'istanza"
+inputHostName: "Digita il nome del dominio "
 general: "Generali"
 wallpaper: "Sfondo"
 setWallpaper: "Imposta sfondo"
 removeWallpaper: "Elimina lo sfondo"
 searchWith: "Cerca: {q}"
 youHaveNoLists: "Non hai ancora creato nessuna lista"
-followConfirm: "Vuoi seguire {name}?"
+followConfirm: "Confermi il Following a {name}?"
 proxyAccount: "Profilo proxy"
 proxyAccountDescription: "Un profilo proxy funziona come follower per i profili remoti, sotto certe condizioni. Ad esempio, quando un profilo locale ne inserisce uno remoto in una lista (senza seguirlo), se nessun altro segue quel profilo remoto, le attività non possono essere distribuite. Dunque, il profilo proxy le seguirà per tutti."
 host: "Host"
+selectSelf: "Segli me"
 selectUser: "Seleziona profilo"
 recipient: "Destinatario"
 annotation: "Annotazione preventiva"
@@ -202,6 +215,7 @@ perDay: "giornaliero"
 stopActivityDelivery: "Interrompi la distribuzione di attività"
 blockThisInstance: "Bloccare l'istanza"
 silenceThisInstance: "Silenziare l'istanza"
+mediaSilenceThisInstance: "Silenzia i media dell'istanza"
 operations: "Operazioni"
 software: "Software"
 version: "Versione"
@@ -223,6 +237,10 @@ blockedInstances: "Istanze bloccate"
 blockedInstancesDescription: "Elenca le istanze che vuoi bloccare, una per riga. Esse non potranno più interagire con la tua istanza."
 silencedInstances: "Istanze silenziate"
 silencedInstancesDescription: "Elenca i nomi host delle istanze che vuoi silenziare. Tutti i profili nelle istanze silenziate vengono trattati come tali. Possono solo inviare richieste di follow e menzionare soltanto i profili locali che seguono. Le istanze bloccate non sono interessate."
+mediaSilencedInstances: "Istanze coi media silenziati"
+mediaSilencedInstancesDescription: "Elenca i nomi host delle istanze di cui vuoi silenziare i media, uno per riga. Tutti gli allegati dei profili nelle istanze silenziate per via degli allegati espliciti, verranno impostati come tali, le emoji personalizzate non saranno disponibili. Le istanze bloccate sono escluse."
+federationAllowedHosts: "Server a cui consentire la federazione"
+federationAllowedHostsDescription: "Indica gli host dei server a cui è consentita la federazione, uno per ogni linea."
 muteAndBlock: "Silenziare e bloccare"
 mutedUsers: "Profili silenziati"
 blockedUsers: "Profili bloccati"
@@ -245,7 +263,7 @@ all: "Tutte"
 subscribing: "Iscrizione"
 publishing: "Pubblicazione"
 notResponding: "Nessuna risposta"
-instanceFollowing: "Seguiti dall'istanza"
+instanceFollowing: "Istanza Following"
 instanceFollowers: "Follower dell'istanza"
 instanceUsers: "Profili nell'istanza"
 changePassword: "Aggiorna Password"
@@ -313,6 +331,7 @@ selectFile: "Scelta allegato"
 selectFiles: "Scelta allegato"
 selectFolder: "Seleziona cartella"
 selectFolders: "Seleziona cartella"
+fileNotSelected: "Nessun file selezionato"
 renameFile: "Rinomina file"
 folderName: "Nome della cartella"
 createFolder: "Nuova cartella"
@@ -320,6 +339,7 @@ renameFolder: "Rinomina cartella"
 deleteFolder: "Elimina cartella"
 folder: "Cartella"
 addFile: "Allega"
+showFile: "Visualizza file"
 emptyDrive: "Il Drive è vuoto"
 emptyFolder: "La cartella è vuota"
 unableToDelete: "Eliminazione impossibile"
@@ -434,6 +454,7 @@ totpDescription: "Puoi autenticarti inserendo un codice OTP tramite la tua App d
 moderator: "Moderatore"
 moderation: "moderazione"
 moderationNote: "Promemoria di moderazione"
+moderationNoteDescription: "Puoi scrivere promemoria condivisi solo tra moderatori."
 addModerationNote: "Aggiungi promemoria di moderazione"
 moderationLogs: "Cronologia di moderazione"
 nUsersMentioned: "{n} profili ne parlano"
@@ -441,7 +462,7 @@ securityKeyAndPasskey: "Chiave di sicurezza e accesso"
 securityKey: "Chiave di sicurezza"
 lastUsed: "Ultima attività"
 lastUsedAt: "Uso più recente: {t}"
-unregister: "Annulla l'iscrizione"
+unregister: "Rimuovi autenticazione a due fattori (2FA/MFA)"
 passwordLessLogin: "Accedi senza password"
 passwordLessLoginDescription: "Accedi senza password, usando la chiave di sicurezza"
 resetPassword: "Ripristina la password"
@@ -468,10 +489,12 @@ retype: "Conferma"
 noteOf: "Note di {user}"
 quoteAttached: "Citazione allegata"
 quoteQuestion: "Vuoi aggiungere una citazione?"
+attachAsFileQuestion: "Il testo copiato eccede le dimensioni, vuoi allegarlo?"
 noMessagesYet: "Ancora nessuna chat"
 newMessageExists: "Hai ricevuto un nuovo messaggio"
 onlyOneFileCanBeAttached: "È possibile allegare al messaggio soltanto uno file"
 signinRequired: "Occorre avere un profilo registrato su questa istanza"
+signinOrContinueOnRemote: "Per continuare, devi accedere alla tua istanza o registrarti su questa e poi accedere"
 invitations: "Invita"
 invitationCode: "Codice di invito"
 checking: "Confermando"
@@ -493,7 +516,10 @@ uiLanguage: "Lingua di visualizzazione dell'interfaccia"
 aboutX: "Informazioni su {x}"
 emojiStyle: "Stile emoji"
 native: "Nativo"
-disableDrawer: "Non mostrare il menù sul drawer"
+menuStyle: "Stile menu"
+style: "Stile"
+drawer: "Drawer"
+popup: "Popup"
 showNoteActionsOnlyHover: "Mostra le azioni delle Note solo al passaggio del mouse"
 showReactionsCount: "Visualizza il numero di reazioni su una nota"
 noHistory: "Nessuna cronologia"
@@ -549,7 +575,7 @@ deleteAll: "Cancella cronologia"
 showFixedPostForm: "Visualizzare la finestra di pubblicazione in cima alla timeline"
 showFixedPostFormInChannel: "Per i canali, mostra il modulo di pubblicazione in cima alla timeline"
 withRepliesByDefaultForNewlyFollowed: "Quando segui nuovi profili, includi le risposte in TL come impostazione predefinita"
-newNoteRecived: "Nuove note da leggere"
+newNoteRecived: "Nuove Note da leggere"
 sounds: "Impostazioni suoni"
 sound: "Suono"
 listen: "Ascolta"
@@ -576,6 +602,8 @@ ascendingOrder: "Aumenta"
 descendingOrder: "Diminuisce"
 scratchpad: "ScratchPad"
 scratchpadDescription: "Lo Scratchpad offre un ambiente per esperimenti di AiScript. È possibile scrivere, eseguire e confermare i risultati dell'interazione del codice con Misskey."
+uiInspector: "UI Inspector"
+uiInspectorDescription: "Puoi visualizzare un elenco di elementi UI presenti in memoria. I componenti dell'interfaccia utente vengono generati dalle funzioni Ui:C:."
 output: "Uscita"
 script: "Script"
 disablePagesScript: "Disabilita AiScript nelle pagine"
@@ -587,7 +615,7 @@ unsetUserBannerConfirm: "Vuoi davvero rimuovere l'intestazione dal profilo?"
 deleteAllFiles: "Elimina tutti i file"
 deleteAllFilesConfirm: "Vuoi davvero eliminare tutti i file?"
 removeAllFollowing: "Annulla tutti i follow"
-removeAllFollowingDescription: "Cancella tutti i follows del server {host}. Per favore, esegui se, ad esempio, l'istanza non esiste più."
+removeAllFollowingDescription: "Togli il Following a tutti i profili su {host}. Utile, ad esempio, quando l'istanza non esiste più."
 userSuspended: "L'utente è in sospensione"
 userSilenced: "Profilo silenziato"
 yourAccountSuspendedTitle: "Questo profilo è sospeso"
@@ -660,7 +688,7 @@ hardWordMute: "Filtro parole forte"
 regexpError: "errore regex"
 regexpErrorDescription: "Si è verificato un errore nell'espressione regolare alla riga {line} della parola muta {tab}:"
 instanceMute: "Silenziare l'istanza"
-userSaysSomething: "{name} ha parlato"
+userSaysSomething: "{name} ha detto qualcosa"
 makeActive: "Attiva"
 display: "Visualizza"
 copy: "Copia"
@@ -675,7 +703,7 @@ notificationSetting: "Impostazioni notifiche"
 notificationSettingDesc: "Seleziona il tipo di notifiche da visualizzare."
 useGlobalSetting: "Usa impostazioni generali"
 useGlobalSettingDesc: "Quando attiva, verranno utilizzate le impostazioni notifiche del profilo. Altrimenti si possono segliere impostazioni personalizzate."
-other: "Ulteriori"
+other: "Eccetera"
 regenerateLoginToken: "Genera di nuovo un token di connessione"
 regenerateLoginTokenDescription: "Genera un nuovo token di autenticazione. Solitamente questa operazione non è necessaria: quando si genera un nuovo token, tutti i dispositivi vanno disconnessi."
 theKeywordWhenSearchingForCustomEmoji: "Questa sarà la parola chiave durante la ricerca di emoji personalizzate"
@@ -692,10 +720,7 @@ abuseReported: "La segnalazione è stata inviata. Grazie."
 reporter: "il corrispondente"
 reporteeOrigin: "Segnalazione a"
 reporterOrigin: "Segnalazione da"
-forwardReport: "Inoltro di un report a un'istanza remota."
-forwardReportIsAnonymous: "L'istanza remota non vedrà le tue informazioni, apparirai come profilo di sistema, anonimo."
 send: "Inviare"
-abuseMarkAsResolved: "Contrassegna la segnalazione come risolta"
 openInNewTab: "Apri in una nuova scheda"
 openInSideView: "Apri in vista laterale"
 defaultNavigationBehaviour: "Navigazione preimpostata"
@@ -722,7 +747,7 @@ repliesCount: "Numero di risposte inviate"
 renotesCount: "Numero di note che hai ricondiviso"
 repliedCount: "Numero di risposte ricevute"
 renotedCount: "Numero delle tue note ricondivise"
-followingCount: "Numero di profili seguiti"
+followingCount: "Numero di Following"
 followersCount: "Numero di profili che ti seguono"
 sentReactionsCount: "Numero di reazioni inviate"
 receivedReactionsCount: "Numero di reazioni ricevute"
@@ -817,7 +842,7 @@ onlineStatus: "Stato di connessione"
 hideOnlineStatus: "Modalità invisibile"
 hideOnlineStatusDescription: "Attivando questa opzione potresti ridurre l'usabilità di alcune funzioni, come la ricerca."
 online: "Online"
-active: "Attività"
+active: "Attivo"
 offline: "Offline"
 notRecommended: "Sconsigliato"
 botProtection: "Protezione contro i bot"
@@ -832,6 +857,7 @@ administration: "Gestione"
 accounts: "Profilo"
 switch: "Cambia"
 noMaintainerInformationWarning: "Mancano le informazioni sull'amministratore."
+noInquiryUrlWarning: "Non è stata impostata la URL di contatto"
 noBotProtectionWarning: "Non è stata impostata alcuna protezione dai Bot"
 configure: "Imposta"
 postToGallery: "Pubblicare nella galleria"
@@ -875,8 +901,8 @@ pubSub: "Publish/Subscribe del profilo"
 lastCommunication: "La comunicazione più recente"
 resolved: "Risolto"
 unresolved: "Non risolto"
-breakFollow: "Impedire di seguirmi"
-breakFollowConfirm: "Vuoi davvero che questo profilo smetta di seguirti?"
+breakFollow: "Rimuovi Follower"
+breakFollowConfirm: "Vuoi davvero togliere questo Follower?"
 itsOn: "Abilitato"
 itsOff: "Disabilitato"
 on: "Acceso"
@@ -891,11 +917,12 @@ makeReactionsPublicDescription: "La lista delle reazioni che avete fatto è a di
 classic: "Classico"
 muteThread: "Silenziare conversazione"
 unmuteThread: "Riattiva la conversazione"
-followingVisibility: "Visibilità dei profili seguiti"
+followingVisibility: "Visibilità dei Following"
 followersVisibility: "Visibilità dei profili che ti seguono"
 continueThread: "Altre conversazioni"
 deleteAccountConfirm: "Così verrà eliminato il profilo. Vuoi procedere?"
 incorrectPassword: "La password è errata."
+incorrectTotp: "Il codice OTP è sbagliato, oppure scaduto."
 voteConfirm: "Votare per「{choice}」?"
 hide: "Nascondere"
 useDrawerReactionPickerForMobile: "Mostra sul drawer da dispositivo mobile"
@@ -920,6 +947,9 @@ oneHour: "1 ora"
 oneDay: "1 giorno"
 oneWeek: "1 settimana"
 oneMonth: "Un mese"
+threeMonths: "3 mesi"
+oneYear: "1 anno"
+threeDays: "3 giorni"
 reflectMayTakeTime: "Potrebbe essere necessario un po' di tempo perché ciò abbia effetto."
 failedToFetchAccountInformation: "Impossibile recuperare le informazioni sul profilo"
 rateLimitExceeded: "Superato il limite di richieste."
@@ -938,7 +968,7 @@ driveCapOverrideLabel: "Modificare la capienza del Drive per questo profilo"
 driveCapOverrideCaption: "Se viene specificato meno di 0, viene annullato."
 requireAdminForView: "Per visualizzarli, è necessario aver effettuato l'accesso con un profilo amministratore."
 isSystemAccount: "Questi profili vengono creati e gestiti automaticamente dal sistema"
-typeToConfirm: "Per eseguire questa operazione, digitare {x}"
+typeToConfirm: "Digita {x} per continuare"
 deleteAccount: "Eliminazione profilo"
 document: "Documento"
 numberOfPageCache: "Numero di pagine cache"
@@ -993,7 +1023,7 @@ neverShow: "Non mostrare più"
 remindMeLater: "Rimanda"
 didYouLikeMisskey: "Ti piace Misskey?"
 pleaseDonate: "Misskey è il software libero utilizzato su {host}. Offrendo una donazione è più facile continuare a svilupparlo!"
-correspondingSourceIsAvailable: ""
+correspondingSourceIsAvailable: "Il codice sorgente corrispondente è disponibile su {anchor}."
 roles: "Ruoli"
 role: "Ruolo"
 noRole: "Ruolo non trovato"
@@ -1021,6 +1051,7 @@ thisPostMayBeAnnoyingHome: "Pubblica sulla timeline principale"
 thisPostMayBeAnnoyingCancel: "Annulla"
 thisPostMayBeAnnoyingIgnore: "Pubblica lo stesso"
 collapseRenotes: "Comprimi le Rinota già viste"
+collapseRenotesDescription: "Comprimi le Note con cui hai già interagito."
 internalServerError: "Errore interno del server"
 internalServerErrorDescription: "Si è verificato un errore imprevisto all'interno del server"
 copyErrorInfo: "Copia le informazioni sull'errore"
@@ -1059,6 +1090,7 @@ retryAllQueuesConfirmTitle: "Vuoi ritentare adesso?"
 retryAllQueuesConfirmText: "Potrebbe sovraccaricare il server temporaneamente."
 enableChartsForRemoteUser: "Abilita i grafici per i profili remoti"
 enableChartsForFederatedInstances: "Abilita i grafici per le istanze federate"
+enableStatsForFederatedInstances: "Informazioni statistiche sui server federati"
 showClipButtonInNoteFooter: "Aggiungi il bottone Clip tra le azioni delle Note"
 reactionsDisplaySize: "Grandezza delle reazioni"
 limitWidthOfReaction: "Limita la larghezza delle reazioni e ridimensionale"
@@ -1094,16 +1126,21 @@ preservedUsernames: "Nomi utente riservati"
 preservedUsernamesDescription: "Elenca, uno per linea, i nomi utente che non possono essere registrati durante la creazione del profilo. La restrizione non si applica agli amministratori. Inoltre, i profili già registrati sono esenti."
 createNoteFromTheFile: "Crea Nota da questo file"
 archive: "Archivio"
+archived: "Archiviato"
+unarchive: "Annulla archiviazione"
 channelArchiveConfirmTitle: "Vuoi davvero archiviare {name}?"
 channelArchiveConfirmDescription: "Un canale archiviato non compare nell'elenco canali, nemmeno nei risultati di ricerca. Non può ricevere nemmeno nuove Note."
 thisChannelArchived: "Questo canale è stato archiviato."
 displayOfNote: "Visualizzazione delle Note"
 initialAccountSetting: "Impostazioni iniziali del profilo"
-youFollowing: "Seguiti"
+youFollowing: "Following"
 preventAiLearning: "Impedisci l'apprendimento della IA"
 preventAiLearningDescription: "Aggiungendo il campo \"noai\" alla risposta HTML, si indica ai Robot esterni di non usare testi e allegati per addestrare sistemi di Machine Learning (IA predittiva/generativa). Anche se è impossibile sapere se la richiesta venga onorata o semplicemente ignorata."
 options: "Opzioni del ruolo"
 specifyUser: "Profilo specifico"
+lookupConfirm: "Vuoi davvero richiedere informazioni?"
+openTagPageConfirm: "Vuoi davvero aprire la pagina dell'hashtag?"
+specifyHost: "Specifica l'host"
 failedToPreviewUrl: "Anteprima non disponibile"
 update: "Aggiorna"
 rolesThatCanBeUsedThisEmojiAsReaction: "Ruoli che possono usare questa emoji come reazione"
@@ -1233,10 +1270,63 @@ useNativeUIForVideoAudioPlayer: "Riprodurre audio/video usando le funzionalità
 keepOriginalFilename: "Mantieni il nome file originale"
 keepOriginalFilenameDescription: "Disattivandola, i file verranno caricati usando nomi casuali."
 noDescription: "Manca la descrizione"
+alwaysConfirmFollow: "Richiedi conferma per i Follow"
+inquiry: "Contattaci"
+tryAgain: "Per favore riprova"
+confirmWhenRevealingSensitiveMedia: "Richiedi conferma prima di mostrare gli allegati espliciti"
+sensitiveMediaRevealConfirm: "Questo allegato è esplicito, vuoi vederlo?"
+createdLists: "Liste create"
+createdAntennas: "Antenne create"
+fromX: "Da {x}"
+genEmbedCode: "Ottieni il codice di incorporamento"
+noteOfThisUser: "Elenco di Note di questo profilo"
+clipNoteLimitExceeded: "Non è possibile aggiungere ulteriori Note a questa Clip."
+performance: "Prestazioni"
+modified: "Modificato"
+discard: "Scarta"
+thereAreNChanges: "Ci sono {n} cambiamenti"
+signinWithPasskey: "Accedi con passkey"
+unknownWebAuthnKey: "Questa è una passkey sconosciuta."
+passkeyVerificationFailed: "La verifica della passkey non è riuscita."
+passkeyVerificationSucceededButPasswordlessLoginDisabled: "La verifica della passkey è riuscita, ma l'accesso senza password è disabilitato."
+messageToFollower: "Messaggio ai follower"
+target: "Riferimento"
+testCaptchaWarning: "Questa funzione è destinata al test CAPTCHA. <strong>Da non utilizzare in ambiente di produzione.</strong>"
+prohibitedWordsForNameOfUser: "Parole proibite (nome utente)"
+prohibitedWordsForNameOfUserDescription: "Il sistema rifiuta di rinominare un utente, se il nome contiene qualsiasi parola nell'elenco. Sono esenti i profili con privilegi di moderazione."
+yourNameContainsProhibitedWords: "Il nome che hai scelto contiene una o più parole vietate"
+yourNameContainsProhibitedWordsDescription: "Se desideri comunque utilizzare questo nome, contatta l''amministrazione."
+thisContentsAreMarkedAsSigninRequiredByAuthor: "L'autore richiede di iscriversi per vedere il contenuto"
+lockdown: "Isolamento"
+pleaseSelectAccount: "Per favore, seleziona un profilo"
+_accountSettings:
+  requireSigninToViewContents: "Per vedere il contenuto, è necessaria l'iscrizione"
+  requireSigninToViewContentsDescription1: "Richiedere l'iscrizione per visualizzare tutte le Note e gli altri contenuti che hai creato. Probabilmente l'effetto è impedire la raccolta di informazioni da parte dei bot crawler."
+  requireSigninToViewContentsDescription2: "La visualizzazione verrà disabilitata a server che non supportano l'anteprima URL (OGP), all'incorporamento nelle pagine Web e alla citazione delle Note."
+  requireSigninToViewContentsDescription3: "Queste restrizioni potrebbero non applicarsi al contenuto federato su server remoti."
+  makeNotesFollowersOnlyBefore: "Rendi visibili solo ai Follower le Note pubblicate in precedenza"
+  makeNotesFollowersOnlyBeforeDescription: "Mentre questa funzione è abilitata, le Note antecedenti al momento impostato, saranno visibili solo ai profili Follower. Disabilitandola nuovamente, verrà ripristinata anche la visibilità pubblica della Nota."
+  makeNotesHiddenBefore: "Nascondi le Note pubblicate in precedenza"
+  makeNotesHiddenBeforeDescription: "Mentre questa funzione è abilitata, le Note antecedenti al momento impostato, saranno visibili soltanto a te (private). Disabilitandola nuovamente, verrà ripristinata anche la visibilità pubblica della Nota."
+  mayNotEffectForFederatedNotes: "Le Note già federate su server remoti potrebbero non essere modificate."
+  notesHavePassedSpecifiedPeriod: "Note antecedenti al periodo specificato"
+  notesOlderThanSpecifiedDateAndTime: "Note antecedenti al momento specificato"
+_abuseUserReport:
+  forward: "Inoltra"
+  forwardDescription: "Inoltra il report al server remoto, per mezzo di account di sistema, anonimo."
+  resolve: "Risolvi"
+  accept: "Approva"
+  reject: "Rifiuta"
+  resolveTutorial: "Se moderi una segnalazione legittima, scegli \"Approva\" per risolvere positivamente.\nSe la segnalazione non è legittima, seleziona \"Rifiuta\" per risolvere negativamente."
 _delivery:
+  status: "Stato della consegna"
   stop: "Sospensione"
+  resume: "Riprendi la consegna"
   _type:
     none: "Pubblicazione"
+    manuallySuspended: "Sospesa manualmente"
+    goneSuspended: "Sospensione server a causa dell'eliminazione"
+    autoSuspendedForNotResponding: "Sospensione del server a causa di mancata risposta"
 _bubbleGame:
   howToPlay: "Come giocare"
   hold: "Tieni"
@@ -1255,16 +1345,16 @@ _bubbleGame:
 _announcement:
   forExistingUsers: "Solo ai profili attuali"
   forExistingUsersDescription: "L'annuncio sarà visibile solo ai profili esistenti in questo momento. Se disabilitato, sarà visibile anche ai profili che verranno creati dopo la pubblicazione di questo annuncio."
-  needConfirmationToRead: "Richiede la conferma di lettura"
-  needConfirmationToReadDescription: "Sarà visualizzata una finestra di dialogo che richiede la conferma di lettura. Inoltre, non è soggetto a conferme di lettura massicce."
+  needConfirmationToRead: "Conferma di lettura obbligatoria"
+  needConfirmationToReadDescription: "I profili riceveranno una finestra di dialogo che richiede di accettare obbligatoriamente per procedere. Tale richiesta è esente da  \"conferma tutte\"."
   end: "Archivia l'annuncio"
   tooManyActiveAnnouncementDescription: "L'esperienza delle persone può peggiorare se ci sono troppi annunci attivi. Considera anche l'archiviazione degli annunci conclusi."
   readConfirmTitle: "Segnare come già letto?"
   readConfirmText: "Hai già letto \"{title}˝?"
   shouldNotBeUsedToPresentPermanentInfo: "Ti consigliamo di utilizzare gli annunci per pubblicare informazioni tempestive e limitate nel tempo, anziché informazioni importanti a lungo andare nel tempo, poiché potrebbero risultare difficili da ritrovare e peggiorare la fruibilità del servizio, specialmente alle nuove persone iscritte."
   dialogAnnouncementUxWarn: "Ti consigliamo di usarli con cautela, poiché è molto probabile che avere più di un annuncio in stile \"finestra di dialogo\" peggiori sensibilmente la fruibilità del servizio, specialmente alle nuove persone iscritte."
-  silence: "Silenziare gli annunci"
-  silenceDescription: "Se attivi questa opzione, non riceverai notifiche sugli annunci, evitando di contrassegnarle come già lette."
+  silence: "Annuncio silenzioso"
+  silenceDescription: "Attivando questa opzione, non invierai la notifica, evitando che debba essere contrassegnata come già letta."
 _initialAccountSetting:
   accountCreated: "Il tuo profilo è stato creato!"
   letsStartAccountSetup: "Per iniziare, impostiamo il tuo profilo."
@@ -1282,7 +1372,7 @@ _initialAccountSetting:
   skipAreYouSure: "Vuoi davvero saltare la configurazione iniziale?"
   laterAreYouSure: "Vuoi davvero rimandare la configurazione iniziale?"
 _initialTutorial:
-  launchTutorial: "Guarda il tutorial"
+  launchTutorial: "Inizia il tutorial"
   title: "Tutorial"
   wellDone: "Ottimo lavoro!"
   skipAreYouSure: "Vuoi davvero interrompere il tutorial?"
@@ -1292,13 +1382,13 @@ _initialTutorial:
   _note:
     title: "Cosa sono le Note?"
     description: "Gli status su Misskey sono chiamati \"Note\". Le Note sono elencate in ordine cronologico nelle timeline e vengono aggiornate in tempo reale."
-    reply: "Puoi rispondere alle Note. Puoi anche rispondere alle risposte e continuare i dialoghi come un conversazioni."
-    renote: "Puoi ri-condividere le Note, facendole rifluire sulla Timeline. Puoi anche aggiungere testo e citare altri profili."
-    reaction: "Puoi aggiungere una reazione. Nella pagina successiva spiegheremo i dettagli."
-    menu: "Puoi svolgere varie attività, come visualizzare i dettagli delle Note o copiare i collegamenti."
+    reply: "Puoi rispondere alle Note, alle altre risposte e dialogare in conversazioni."
+    renote: "Puoi ri-condividere le Note, ritorneranno sulla Timeline. Aggiungendo del testo, scriverai una Citazione."
+    reaction: "Puoi aggiungere una reazione. Nella pagina successiva ti spiego come."
+    menu: "Per altre attività, ad esempio, vedere i dettagli delle Note o copiare i collegamenti."
   _reaction:
     title: "Cosa sono le Reazioni?"
-    description: "Puoi reagire alle Note. Le sensazioni che non si riescono a trasmettere con i \"Mi piace\" si possono esprimere facilmente inviando una reazione."
+    description: "Reazioni alle Note. Le sensazioni che non si possono descrivere con \"Mi piace\" si esprimono facilmente con le reazioni."
     letsTryReacting: "Puoi aggiungere una Reazione cliccando il bottone \"+\" (più) della relativa Nota. Prova ad aggiungerne una a questa Nota di esempio!"
     reactToContinue: "Aggiungere la Reazione ti consentirà di procedere col tutorial."
     reactNotification: "Quando qualcuno reagisce alle tue Note, ricevi una notifica in tempo reale."
@@ -1306,12 +1396,12 @@ _initialTutorial:
   _timeline:
     title: "Come funziona la Timeline"
     description1: "Misskey fornisce alcune Timeline (sequenze cronologiche di Note). Una di queste potrebbe essere stata disattivata dagli amministratori."
-    home: "le Note provenienti dai profili che segui (follow)."
+    home: "le Note provenienti dai profili che segui (Following)."
     local: "tutte le Note pubblicate dai profili di questa istanza."
     social: "sia le Note della Timeline Home che quelle della Timeline Locale, insieme!"
     global: "le Note da pubblicate da tutte le altre istanze federate con la nostra."
     description2: "Nella parte superiore dello schermo, puoi scegliere una Timeline o l'altra in qualsiasi momento."
-    description3: "Ci sono anche sequenze temporali di elenchi, sequenze temporali di canali, ecc. Per ulteriori dettagli, consultare il {link}.\nPuoi vedere anche Timeline delle liste di profili (se ne hai create), canali, ecc... Per i dettagli, visita {link}."
+    description3: "Ci sono anche sequenze temporali di elenchi, sequenze temporali di canali, ecc. Per ulteriori dettagli, consultare la {link}.\nPuoi vedere anche Timeline delle liste di profili (se ne hai create), canali, ecc... Per i dettagli, c'è la {link}."
   _postNote:
     title: "La Nota e le sue impostazioni"
     description1: "Quando scrivi una Nota su Misskey, hai a disposizione varie opzioni. Il modulo di invio è simile a questo."
@@ -1344,7 +1434,7 @@ _initialTutorial:
     title: "Il tutorial è finito! 🎉"
     description: "Queste sono solamente alcune delle funzionalità principali di Misskey. Per ulteriori informazioni, {link}."
 _timelineDescription:
-  home: "Nella Timeline Home, la tua cronologia principale, puoi vedere le Note provenienti dai profili che segui (follow)."
+  home: "Nella Timeline Home, la tua cronologia principale, puoi vedere le Note provenienti dai profili che segui (Following)."
   local: "La Timeline Locale, è una cronologia di Note pubblicate da tutti i profili iscritti su questo server."
   social: "La Timeline Sociale, unisce in ordine cronologico l'elenco di Note presenti nella Timeline Home e quella Locale."
   global: "La Timeline Federata ti consente di vedere le Note pubblicate dai profili di tutti gli altri server federati a questo."
@@ -1362,11 +1452,15 @@ _serverSettings:
   fanoutTimelineDescription: "Attivando questa funzionalità migliori notevolmente la capacità delle Timeline di collezionare Note, riducendo il carico sul database. Tuttavia, aumenterà l'impiego di memoria RAM per Redis. Disattiva se il tuo server ha poca RAM o la funzionalità è irregolare."
   fanoutTimelineDbFallback: "Elaborazione dati alternativa"
   fanoutTimelineDbFallbackDescription: "Attivando l'elaborazione alternativa, verrà interrogato ulteriormente il database se la timeline non è nella cache. \nDisattivando, si può ridurre ulteriormente il carico del server, evitando l'elaborazione alternativa, ma limitando l'intervallo recuperabile delle timeline."
+  reactionsBufferingDescription: "Attivando questa opzione, puoi migliorare significativamente le prestazioni durante la creazione delle reazioni e ridurre il carico sul database. Tuttavia, aumenterà l'impiego di memoria Redis."
+  inquiryUrl: "URL di contatto"
+  inquiryUrlDescription: "Specificare l'URL al modulo di contatto, oppure le informazioni con i dati di contatto dell'amministrazione."
+  thisSettingWillAutomaticallyOffWhenModeratorsInactive: "Per prevenire SPAM, questa impostazione verrà disattivata automaticamente, se non si rileva alcuna attività di moderazione durante un certo periodo di tempo."
 _accountMigration:
   moveFrom: "Migra un altro profilo dentro a questo"
   moveFromSub: "Crea un alias verso un altro profilo remoto"
   moveFromLabel: "Profilo da cui migrare #{n}"
-  moveFromDescription: "Se desideri spostare i profili follower da un altro profilo a questo, devi prima creare un alias qui. Assicurati averlo creato PRIMA di eseguire l'attività! Inserisci l'indirizzo del profilo mittente in questo modo: @persona@istanza.it"
+  moveFromDescription: "Se desideri spostare i Follower da un altro profilo a questo, devi prima creare un alias qui. Assicurati averlo creato PRIMA di eseguire l'attività! Inserisci l'indirizzo del profilo mittente in questo modo: @persona@istanza.it"
   moveTo: "Migrare questo profilo verso un un altro"
   moveToLabel: "Profilo verso cui migrare"
   moveCannotBeUndone: "La migrazione è irreversibile, non può essere interrotta o annullata."
@@ -1375,7 +1469,7 @@ _accountMigration:
   startMigration: "Avvia la migrazione"
   migrationConfirm: "Vuoi davvero migrare questo profilo su {account}? L'azione è irreversibile e non potrai più utilizzare questo profilo nel suo stato originale.\nInoltre, assicurati di aver già creato un alias sull'account a cui ti stai trasferendo."
   movedAndCannotBeUndone: "Il tuo profilo è stato migrato.\nLa migrazione non può essere annullata."
-  postMigrationNote: "Questo profilo smetterà di seguire gli altri profili remoti a 24 ore dal termine della migrazione.\nSia i Follow che i Follower scenderanno a zero. I tuoi follower saranno comunque in grado di vedere le Note per soli follower, poiché non smetteranno di seguirti."
+  postMigrationNote: "Questo profilo smetterà di seguire gli altri profili remoti a 24 ore dal termine della migrazione.\nSia i Following che i Follower scenderanno a zero. I tuoi Follower saranno comunque in grado di vedere le Note per soli Follower, poiché non smetteranno di seguirti."
   movedTo: "Profilo verso cui migrare"
 _achievements:
   earnedAt: "Data di conseguimento"
@@ -1678,6 +1772,7 @@ _role:
     canManageAvatarDecorations: "Gestisce le decorazioni di immagini del profilo"
     driveCapacity: "Capienza del Drive"
     alwaysMarkNsfw: "Impostare sempre come esplicito (NSFW)"
+    canUpdateBioMedia: "Può aggiornare foto profilo e di testata"
     pinMax: "Quantità massima di Note in primo piano"
     antennaMax: "Quantità massima di Antenne"
     wordMuteMax: "Lunghezza massima del filtro parole"
@@ -1692,10 +1787,20 @@ _role:
     canSearchNotes: "Ricercare nelle Note"
     canUseTranslator: "Tradurre le Note"
     avatarDecorationLimit: "Numero massimo di decorazioni foto profilo installabili"
+    canImportAntennas: "Può importare Antenne"
+    canImportBlocking: "Può importare Blocchi"
+    canImportFollowing: "Può importare Following"
+    canImportMuting: "Può importare Silenziati"
+    canImportUserLists: "Può importare liste di Profili"
   _condition:
     roleAssignedTo: "Assegnato a ruoli manualmente"
     isLocal: "Profilo locale"
     isRemote: "Profilo remoto"
+    isCat: "È un gattino"
+    isBot: "È un bot"
+    isSuspended: "È sospeso"
+    isLocked: "È in stato privato"
+    isExplorable: "Autorizza la pubblicazione nei cataloghi"
     createdLessThan: "Profilo creato da meno di N"
     createdMoreThan: "Profilo creato da più di N"
     followersLessThanOrEq: "Profilo con N follower o meno"
@@ -1757,7 +1862,7 @@ _gallery:
   unlike: "Non mi piace più"
 _email:
   _follow:
-    title: "Adesso ti segue"
+    title: "Follower aggiuntivo"
   _receiveFollowRequest:
     title: "Hai ricevuto una richiesta di follow"
 _plugin:
@@ -1821,7 +1926,7 @@ _channel:
   removeBanner: "Rimuovi intestazione"
   featured: "Di tendenza"
   owned: "I miei canali"
-  following: "Seguiti"
+  following: "Following"
   usersCount: "{n} partecipanti"
   notesCount: "{n} note"
   nameAndDescription: "Nome e descrizione"
@@ -1904,7 +2009,6 @@ _theme:
     buttonBg: "Sfondo del pulsante"
     buttonHoverBg: "Sfondo del pulsante (sorvolato)"
     inputBorder: "Inquadra casella di testo"
-    listItemHoverBg: "Sfondo della voce di elenco (sorvolato)"
     driveFolderBg: "Sfondo della cartella di disco"
     wallpaperOverlay: "Sovrapposizione dello sfondo"
     badge: "Distintivo"
@@ -1916,8 +2020,6 @@ _sfx:
   note: "Nota"
   noteMy: "Mia nota"
   notification: "Notifiche"
-  antenna: "Ricezione dell'antenna"
-  channel: "Notifiche di canale"
   reaction: "Quando seleziono una reazione"
 _soundSettings:
   driveFile: "Suoni del Drive"
@@ -1926,6 +2028,7 @@ _soundSettings:
   driveFileTypeWarnDescription: "Per favore, scegli un file di tipo audio"
   driveFileDurationWarn: "La durata dell'audio è troppo lunga"
   driveFileDurationWarnDescription: "Scegliere un audio lungo potrebbe interferire con l'uso di Misskey. Vuoi continuare lo stesso?"
+  driveFileError: "Impossibile caricare l'audio. Si prega di modificare le impostazioni"
 _ago:
   future: "Futuro"
   justNow: "Adesso"
@@ -1989,7 +2092,7 @@ _permissions:
   "read:favorites": "Visualizza i tuoi preferiti"
   "write:favorites": "Gestisci i tuoi preferiti"
   "read:following": "Vedi le informazioni di follow"
-  "write:following": "Following di altri profili"
+  "write:following": "Aggiungere e togliere Following"
   "read:messaging": "Visualizzare la chat"
   "write:messaging": "Gestire la chat"
   "read:mutes": "Vedi i profili silenziati"
@@ -2072,11 +2175,14 @@ _auth:
   permissionAsk: "Questa app richiede le seguenti autorizzazioni:"
   pleaseGoBack: "Si prega di ritornare sulla app"
   callback: "Ritornando sulla app"
+  accepted: "Accesso concesso"
   denied: "Accesso negato"
+  scopeUser: "Sto funzionando per il seguente profilo"
   pleaseLogin: "Per favore accedi al tuo account per cambiare i permessi dell'applicazione"
+  byClickingYouWillBeRedirectedToThisUrl: "Consentendo l'accesso, si verrà reindirizzati presso questo indirizzo URL"
 _antennaSources:
   all: "Tutte le note"
-  homeTimeline: "Note dagli utenti che segui"
+  homeTimeline: "Note dai tuoi Following"
   users: "Note dagli utenti selezionati"
   userList: "Note dagli utenti della lista selezionata"
   userBlacklist: "Tutte le Note tranne quelle di uno o più profili specificati"
@@ -2118,7 +2224,7 @@ _widgets:
   _userList:
     chooseList: "Seleziona una lista"
   clicker: "Cliccaggio"
-  birthdayFollowings: "Chi nacque oggi"
+  birthdayFollowings: "Compleanni del giorno"
 _cw:
   hide: "Nascondere"
   show: "Continua la lettura..."
@@ -2182,11 +2288,14 @@ _profile:
   changeBanner: "Cambia intestazione"
   verifiedLinkDescription: "Puoi verificare il tuo profilo mostrando una icona. Devi inserire la URL alla pagina che contiene un link al tuo profilo."
   avatarDecorationMax: "Puoi aggiungere fino a {max} decorazioni."
+  followedMessage: "Messaggio, quando qualcuno ti segue"
+  followedMessageDescription: "Puoi impostare un breve messaggio da mostrare agli altri profili quando ti seguono."
+  followedMessageDescriptionForLockedAccount: "Quando approvi una richiesta di follow, verrà visualizzato questo testo."
 _exportOrImport:
   allNotes: "Tutte le note"
   favoritedNotes: "Note preferite"
   clips: "Clip"
-  followingList: "Follow"
+  followingList: "Following"
   muteList: "Elenco profili silenziati"
   blockingList: "Elenco profili bloccati"
   userLists: "Liste"
@@ -2274,6 +2383,7 @@ _pages:
   eyeCatchingImageSet: "Imposta un'immagine attraente"
   eyeCatchingImageRemove: "Elimina immagine attraente"
   chooseBlock: "Aggiungi blocco"
+  enterSectionTitle: "Inserisci il titolo della sezione"
   selectType: "Seleziona tipo"
   contentBlocks: "Contenuto"
   inputBlocks: "Blocchi di input"
@@ -2301,7 +2411,7 @@ _notification:
   youGotReply: "{name} ti ha risposto"
   youGotQuote: "{name} ha citato la tua Nota e ha detto"
   youRenoted: "{name} ha rinotato"
-  youWereFollowed: "Adesso ti segue"
+  youWereFollowed: "Follower aggiuntivo"
   youReceivedFollowRequest: "Hai ricevuto una richiesta di follow"
   yourFollowRequestAccepted: "La tua richiesta di follow è stata accettata"
   pollEnded: "Risultati del sondaggio."
@@ -2319,10 +2429,12 @@ _notification:
   renotedBySomeUsers: "{n} Rinota"
   followedBySomeUsers: "{n} follower"
   flushNotification: "Azzera le notifiche"
+  exportOfXCompleted: "Abbiamo completato l'esportazione di {x}"
+  login: "Autenticazione avvenuta"
   _types:
     all: "Tutto"
     note: "Nuove Note"
-    follow: "Nuovi profili follower"
+    follow: "Follower"
     mention: "Menzioni"
     reply: "Risposte"
     renote: "Rinota"
@@ -2333,15 +2445,19 @@ _notification:
     followRequestAccepted: "Richiesta di follow accettata"
     roleAssigned: "Ruolo concesso"
     achievementEarned: "Risultato raggiunto"
+    exportCompleted: "Esportazione completata"
+    login: "Accedi"
+    test: "Prova la notifica"
     app: "Notifiche da applicazioni"
   _actions:
-    followBack: "Segui"
+    followBack: "Following ricambiato"
     reply: "Rispondi"
     renote: "Rinota"
 _deck:
   alwaysShowMainColumn: "Mostra sempre la colonna principale"
   columnAlign: "Allineare colonne"
   addColumn: "Aggiungi colonna"
+  newNoteNotificationSettings: "Preferenze per le notifiche di nuove Note"
   configureColumn: "Impostazioni colonna"
   swapLeft: "Sposta a sinistra"
   swapRight: "Sposta a destra"
@@ -2380,18 +2496,42 @@ _drivecleaner:
   orderByCreatedAtAsc: "Dal più vecchio al più recente"
 _webhookSettings:
   createWebhook: "Creazione Webhook"
+  modifyWebhook: "Modifica Webhook"
   name: "Nome"
   secret: "Segreto"
-  events: "Quando eseguire il Webhook"
+  trigger: "Trigger"
   active: "Attivo"
   _events:
-    follow: "Quando segui un profilo"
+    follow: "Quando aggiungi Following"
     followed: "Quando ti segue un profilo"
     note: "Quando pubblichi una Nota"
     reply: "Quando rispondono ad una Nota"
     renote: "Quando la Nota è Rinotata"
     reaction: "Quando ricevo una reazione"
     mention: "Quando mi menzionano"
+  _systemEvents:
+    abuseReport: "Quando arriva una segnalazione"
+    abuseReportResolved: "Quando una segnalazione è risolta"
+    userCreated: "Quando viene creato un profilo"
+    inactiveModeratorsWarning: "Quando un profilo moderatore rimane inattivo per un determinato periodo"
+    inactiveModeratorsInvitationOnlyChanged: "Quando la moderazione è rimasta inattiva per un determinato periodo e il sistema è cambiato in modalità \"solo inviti\""
+  deleteConfirm: "Vuoi davvero eliminare il Webhook?"
+  testRemarks: "Clicca il bottone a destra dell'interruttore, per provare l'invio di un webhook con dati fittizi."
+_abuseReport:
+  _notificationRecipient:
+    createRecipient: "Aggiungi destinatario della segnalazione"
+    modifyRecipient: "Modifica destinatario della segnalazione"
+    recipientType: "Tipo di notifica"
+    _recipientType:
+      mail: "Email"
+      webhook: "Webhook"
+      _captions:
+        mail: "Quando ricevi un abuso, notifica l'amministrazione via email"
+        webhook: "Spedire una notifica al SystemWebhook specificato (sia quando si riceve una segnalazione, che quando viene risolta)"
+    keywords: "Parole chiave"
+    notifiedUser: "Profili da notificare"
+    notifiedWebhook: "Webhook da usare"
+    deleteConfirm: "Vuoi davvero rimuovere il destinatario della notifica?"
 _moderationLogTypes:
   createRole: "Ruolo creato"
   deleteRole: "Ruolo eliminato"
@@ -2420,6 +2560,8 @@ _moderationLogTypes:
   markSensitiveDriveFile: "File nel Drive segnato come esplicito"
   unmarkSensitiveDriveFile: "File nel Drive segnato come non esplicito"
   resolveAbuseReport: "Segnalazione risolta"
+  forwardAbuseReport: "Segnalazione inoltrata"
+  updateAbuseReportNote: "Ha aggiornato la segnalazione"
   createInvitation: "Genera codice di invito"
   createAd: "Banner creato"
   deleteAd: "Banner eliminato"
@@ -2429,6 +2571,16 @@ _moderationLogTypes:
   deleteAvatarDecoration: "Eliminazione decorazione della foto profilo"
   unsetUserAvatar: "Rimossa foto profilo"
   unsetUserBanner: "Rimossa intestazione profilo"
+  createSystemWebhook: "Crea un SystemWebhook"
+  updateSystemWebhook: "Modifica SystemWebhook"
+  deleteSystemWebhook: "Elimina SystemWebhook"
+  createAbuseReportNotificationRecipient: "Crea destinatario per le notifiche di segnalazioni"
+  updateAbuseReportNotificationRecipient: "Aggiorna destinatario notifiche di segnalazioni"
+  deleteAbuseReportNotificationRecipient: "Elimina destinatario notifiche di segnalazioni"
+  deleteAccount: "Quando viene eliminato un profilo"
+  deletePage: "Pagina eliminata"
+  deleteFlash: "Play eliminato"
+  deleteGalleryPost: "Eliminazione pubblicazione nella Galleria"
 _fileViewer:
   title: "Dettagli del file"
   type: "Tipo di file"
@@ -2554,7 +2706,34 @@ _urlPreviewSetting:
   userAgent: "User-Agent"
   userAgentDescription: "Definire con quale User-Agent si intende identificarsi durante l'acquisizione di un'anteprima. Se è vuoto, useremo il valore predefinito."
   summaryProxy: "Endpoint proxy che genera l'anteprima"
+  summaryProxyDescription: "Genera anteprime utilizzando un proxy Summaly anziché Misskey."
+  summaryProxyDescription2: "I parametri sono collegano al proxy come stringa query. Se il proxy non li supporta, verranno ignorati."
 _mediaControls:
   pip: "Sovraimpressione"
   playbackRate: "Velocità di riproduzione"
   loop: "Ripetizione infinita"
+_contextMenu:
+  title: "Menu contestuale"
+  app: "Applicazione"
+  appWithShift: "Applicazione Shift+Tasto"
+  native: "Interfaccia utente del browser"
+_embedCodeGen:
+  title: "Personalizza il codice di incorporamento"
+  header: "Mostra la testata"
+  autoload: "Carica automaticamente di più (sconsigliato)"
+  maxHeight: "Altezza massima"
+  maxHeightDescription: "Specifica un valore per evitare che continui a crescere verticalmente. Il valore 0 disabilita il limite d'altezza."
+  maxHeightWarn: "L'altezza massima è disabilitata (0). Se l'effetto è indesiderato, prova a impostare l'altezza massima a un valore specifico."
+  previewIsNotActual: "Poiché supera l'intervallo che può essere visualizzato in anteprima, la visualizzazione vera e propria sarà diversa quando effettivamente incorporata."
+  rounded: "Bordo arrotondato"
+  border: "Aggiungi un bordo al contenitore"
+  applyToPreview: "Applica all'anteprima"
+  generateCode: "Crea il codice di incorporamento"
+  codeGenerated: "Codice generato"
+  codeGeneratedDescription: "Incolla il codice appena generato sul tuo sito web."
+_selfXssPrevention:
+  warning: "Avviso"
+  title: "\"Incolla qualcosa su questa schermata\" è tutta una truffa."
+  description1: "Incollando qualcosa qui, malintenzionati potrebbero prendere il controllo del tuo profilo o rubare i tuoi dati personali."
+  description2: "Se non sai esattamente cosa stai facendo, %c smetti subito e chiudi questa finestra."
+  description3: "Per favore, controlla questo collegamento per avere maggiori dettagli. {link}"
diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index a89cfbd843..5d8e1a5e72 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -8,6 +8,9 @@ search: "検索"
 notifications: "通知"
 username: "ユーザー名"
 password: "パスワード"
+initialPasswordForSetup: "初期設定開始用パスワード"
+initialPasswordIsIncorrect: "初期設定開始用のパスワードが違います。"
+initialPasswordForSetupDescription: "Misskeyを自分でインストールした場合は、設定ファイルに入力したパスワードを使用してください。\nMisskeyのホスティングサービスなどを使用している場合は、提供されたパスワードを使用してください。\nパスワードを設定していない場合は、空欄にしたまま続行してください。"
 forgotPassword: "パスワードを忘れた"
 fetchingAsApObject: "連合に照会中"
 ok: "OK"
@@ -60,6 +63,7 @@ copyFileId: "ファイルIDをコピー"
 copyFolderId: "フォルダーIDをコピー"
 copyProfileUrl: "プロフィールURLをコピー"
 searchUser: "ユーザーを検索"
+searchThisUsersNotes: "ユーザーのノートを検索"
 reply: "返信"
 loadMore: "もっと見る"
 showMore: "もっと見る"
@@ -154,6 +158,7 @@ editList: "リストを編集"
 selectChannel: "チャンネルを選択"
 selectAntenna: "アンテナを選択"
 editAntenna: "アンテナを編集"
+createAntenna: "アンテナを作成"
 selectWidget: "ウィジェットを選択"
 editWidgets: "ウィジェットを編集"
 editWidgetsExit: "編集を終了"
@@ -180,6 +185,10 @@ addAccount: "アカウントを追加"
 reloadAccountsList: "アカウントリストの情報を更新"
 loginFailed: "ログインに失敗しました"
 showOnRemote: "リモートで表示"
+continueOnRemote: "リモートで続行"
+chooseServerOnMisskeyHub: "Misskey Hubからサーバーを選択"
+specifyServerHost: "サーバーのドメインを直接指定"
+inputHostName: "ドメインを入力してください"
 general: "全般"
 wallpaper: "壁紙"
 setWallpaper: "壁紙を設定"
@@ -190,6 +199,7 @@ followConfirm: "{name}をフォローしますか?"
 proxyAccount: "プロキシアカウント"
 proxyAccountDescription: "プロキシアカウントは、特定の条件下でユーザーのリモートフォローを代行するアカウントです。例えば、ユーザーがリモートユーザーをリストに入れたとき、リストに入れられたユーザーを誰もフォローしていないとアクティビティがサーバーに配達されないため、代わりにプロキシアカウントがフォローするようにします。"
 host: "ホスト"
+selectSelf: "自分を選択"
 selectUser: "ユーザーを選択"
 recipient: "宛先"
 annotation: "注釈"
@@ -205,6 +215,7 @@ perDay: "1日ごと"
 stopActivityDelivery: "アクティビティの配送を停止"
 blockThisInstance: "このサーバーをブロック"
 silenceThisInstance: "サーバーをサイレンス"
+mediaSilenceThisInstance: "サーバーをメディアサイレンス"
 operations: "操作"
 software: "ソフトウェア"
 version: "バージョン"
@@ -226,6 +237,10 @@ blockedInstances: "ブロックしたサーバー"
 blockedInstancesDescription: "ブロックしたいサーバーのホストを改行で区切って設定します。ブロックされたサーバーは、このインスタンスとやり取りできなくなります。"
 silencedInstances: "サイレンスしたサーバー"
 silencedInstancesDescription: "サイレンスしたいサーバーのホストを改行で区切って設定します。サイレンスされたサーバーに所属するアカウントはすべて「サイレンス」として扱われ、フォローがすべてリクエストになります。ブロックしたインスタンスには影響しません。"
+mediaSilencedInstances: "メディアサイレンスしたサーバー"
+mediaSilencedInstancesDescription: "メディアサイレンスしたいサーバーのホストを改行で区切って設定します。メディアサイレンスされたサーバーに所属するアカウントによるファイルはすべてセンシティブとして扱われ、カスタム絵文字が使用できないようになります。ブロックしたインスタンスには影響しません。"
+federationAllowedHosts: "連合を許可するサーバー"
+federationAllowedHostsDescription: "連合を許可するサーバーのホストを改行で区切って設定します。"
 muteAndBlock: "ミュートとブロック"
 mutedUsers: "ミュートしたユーザー"
 blockedUsers: "ブロックしたユーザー"
@@ -324,6 +339,7 @@ renameFolder: "フォルダー名を変更"
 deleteFolder: "フォルダーを削除"
 folder: "フォルダー"
 addFile: "ファイルを追加"
+showFile: "ファイルを表示"
 emptyDrive: "ドライブは空です"
 emptyFolder: "フォルダーは空です"
 unableToDelete: "削除できません"
@@ -438,6 +454,7 @@ totpDescription: "認証アプリを使ってワンタイムパスワードを
 moderator: "モデレーター"
 moderation: "モデレーション"
 moderationNote: "モデレーションノート"
+moderationNoteDescription: "モデレーター間でだけ共有されるメモを記入することができます。"
 addModerationNote: "モデレーションノートを追加する"
 moderationLogs: "モデログ"
 nUsersMentioned: "{n}人が投稿"
@@ -476,7 +493,8 @@ attachAsFileQuestion: "クリップボードのテキストが長いです。テ
 noMessagesYet: "まだチャットはありません"
 newMessageExists: "新しいメッセージがあります"
 onlyOneFileCanBeAttached: "メッセージに添付できるファイルはひとつです"
-signinRequired: "続行する前に、サインアップまたはサインインが必要です"
+signinRequired: "続行する前に、登録またはログインが必要です"
+signinOrContinueOnRemote: "続行するには、お使いのサーバーに移動するか、このサーバーに登録・ログインする必要があります"
 invitations: "招待"
 invitationCode: "招待コード"
 checking: "確認しています"
@@ -498,7 +516,10 @@ uiLanguage: "UIの表示言語"
 aboutX: "{x}について"
 emojiStyle: "絵文字のスタイル"
 native: "ネイティブ"
-disableDrawer: "メニューをドロワーで表示しない"
+menuStyle: "メニューのスタイル"
+style: "スタイル"
+drawer: "ドロワー"
+popup: "ポップアップ"
 showNoteActionsOnlyHover: "ノートのアクションをホバー時のみ表示する"
 showReactionsCount: "ノートのリアクション数を表示する"
 noHistory: "履歴はありません"
@@ -581,6 +602,8 @@ ascendingOrder: "昇順"
 descendingOrder: "降順"
 scratchpad: "スクラッチパッド"
 scratchpadDescription: "スクラッチパッドは、AiScriptの実験環境を提供します。Misskeyと対話するコードの記述、実行、結果の確認ができます。"
+uiInspector: "UIインスペクター"
+uiInspectorDescription: "メモリ上に存在しているUIコンポーネントのインスタンスの一覧を見ることができます。UIコンポーネントはUi:C:系関数により生成されます。"
 output: "出力"
 script: "スクリプト"
 disablePagesScript: "Pagesのスクリプトを無効にする"
@@ -692,15 +715,12 @@ abuseReports: "通報"
 reportAbuse: "通報"
 reportAbuseRenote: "リノートを通報"
 reportAbuseOf: "{name}を通報する"
-fillAbuseReportDescription: "通報理由の詳細を記入してください。対象のノートがある場合はそのURLも記入してください。"
+fillAbuseReportDescription: "通報理由の詳細を記入してください。対象のノートやページなどがある場合はそのURLも記入してください。"
 abuseReported: "内容が送信されました。ご報告ありがとうございました。"
 reporter: "通報者"
 reporteeOrigin: "通報先"
 reporterOrigin: "通報元"
-forwardReport: "リモートサーバーに通報を転送する"
-forwardReportIsAnonymous: "リモートサーバーからはあなたの情報は見れず、匿名のシステムアカウントとして表示されます。"
 send: "送信"
-abuseMarkAsResolved: "対応済みにする"
 openInNewTab: "新しいタブで開く"
 openInSideView: "サイドビューで開く"
 defaultNavigationBehaviour: "デフォルトのナビゲーション"
@@ -765,7 +785,7 @@ left: "左"
 center: "中央"
 wide: "広い"
 narrow: "狭い"
-reloadToApplySetting: "設定はページリロード後に反映されます。今すぐリロードしますか?"
+reloadToApplySetting: "設定はページリロード後に反映されます。"
 needReloadToApply: "反映には再起動が必要です。"
 showTitlebar: "タイトルバーを表示する"
 clearCache: "キャッシュをクリア"
@@ -902,6 +922,7 @@ followersVisibility: "フォロワーの公開範囲"
 continueThread: "さらにスレッドを見る"
 deleteAccountConfirm: "アカウントが削除されます。よろしいですか?"
 incorrectPassword: "パスワードが間違っています。"
+incorrectTotp: "ワンタイムパスワードが間違っているか、期限切れになっています。"
 voteConfirm: "「{choice}」に投票しますか?"
 hide: "隠す"
 useDrawerReactionPickerForMobile: "モバイルデバイスのときドロワーで表示"
@@ -926,6 +947,9 @@ oneHour: "1時間"
 oneDay: "1日"
 oneWeek: "1週間"
 oneMonth: "1ヶ月"
+threeMonths: "3ヶ月"
+oneYear: "1年"
+threeDays: "3日"
 reflectMayTakeTime: "反映されるまで時間がかかる場合があります。"
 failedToFetchAccountInformation: "アカウント情報の取得に失敗しました"
 rateLimitExceeded: "レート制限を超えました"
@@ -1066,6 +1090,7 @@ retryAllQueuesConfirmTitle: "今すぐ再試行しますか?"
 retryAllQueuesConfirmText: "一時的にサーバーの負荷が増大することがあります。"
 enableChartsForRemoteUser: "リモートユーザーのチャートを生成"
 enableChartsForFederatedInstances: "リモートサーバーのチャートを生成"
+enableStatsForFederatedInstances: "リモートサーバーの情報を取得"
 showClipButtonInNoteFooter: "ノートのアクションにクリップを追加"
 reactionsDisplaySize: "リアクションの表示サイズ"
 limitWidthOfReaction: "リアクションの最大横幅を制限し、縮小して表示する"
@@ -1101,6 +1126,8 @@ preservedUsernames: "予約ユーザー名"
 preservedUsernamesDescription: "予約するユーザー名を改行で列挙します。ここで指定されたユーザー名はアカウント作成時に使えなくなりますが、管理者によるアカウント作成時はこの制限を受けません。また、既に存在するアカウントも影響を受けません。"
 createNoteFromTheFile: "このファイルからノートを作成"
 archive: "アーカイブ"
+archived: "アーカイブ済み"
+unarchive: "アーカイブ解除"
 channelArchiveConfirmTitle: "{name}をアーカイブしますか?"
 channelArchiveConfirmDescription: "アーカイブすると、チャンネル一覧や検索結果に表示されなくなり、新たな書き込みもできなくなります。"
 thisChannelArchived: "このチャンネルはアーカイブされています。"
@@ -1111,6 +1138,9 @@ preventAiLearning: "生成AIによる学習を拒否"
 preventAiLearningDescription: "外部の文章生成AIや画像生成AIに対して、投稿したノートや画像などのコンテンツを学習の対象にしないように要求します。これはnoaiフラグをHTMLレスポンスに含めることによって実現されますが、この要求に従うかはそのAI次第であるため、学習を完全に防止するものではありません。"
 options: "オプション"
 specifyUser: "ユーザー指定"
+lookupConfirm: "照会しますか?"
+openTagPageConfirm: "ハッシュタグのページを開きますか?"
+specifyHost: "ホスト指定"
 failedToPreviewUrl: "プレビューできません"
 update: "更新"
 rolesThatCanBeUsedThisEmojiAsReaction: "リアクションとして使えるロール"
@@ -1242,6 +1272,55 @@ keepOriginalFilenameDescription: "この設定をオフにすると、アップ
 noDescription: "説明文はありません"
 alwaysConfirmFollow: "フォローの際常に確認する"
 inquiry: "お問い合わせ"
+tryAgain: "もう一度お試しください。"
+confirmWhenRevealingSensitiveMedia: "センシティブなメディアを表示するとき確認する"
+sensitiveMediaRevealConfirm: "センシティブなメディアです。表示しますか?"
+createdLists: "作成したリスト"
+createdAntennas: "作成したアンテナ"
+fromX: "{x}から"
+genEmbedCode: "埋め込みコードを生成"
+noteOfThisUser: "このユーザーのノート一覧"
+clipNoteLimitExceeded: "これ以上このクリップにノートを追加できません。"
+performance: "パフォーマンス"
+modified: "変更あり"
+discard: "破棄"
+thereAreNChanges: "{n}件の変更があります"
+signinWithPasskey: "パスキーでログイン"
+unknownWebAuthnKey: "登録されていないパスキーです。"
+passkeyVerificationFailed: "パスキーの検証に失敗しました。"
+passkeyVerificationSucceededButPasswordlessLoginDisabled: "パスキーの検証に成功しましたが、パスワードレスログインが無効になっています。"
+messageToFollower: "フォロワーへのメッセージ"
+target: "対象"
+testCaptchaWarning: "CAPTCHAのテストを目的とした機能です。<strong>本番環境で使用しないでください。</strong>"
+prohibitedWordsForNameOfUser: "禁止ワード(ユーザーの名前)"
+prohibitedWordsForNameOfUserDescription: "このリストに含まれる文字列がユーザーの名前に含まれる場合、ユーザーの名前の変更を拒否します。モデレーター権限を持つユーザーはこの制限の影響を受けません。"
+yourNameContainsProhibitedWords: "変更しようとした名前に禁止された文字列が含まれています"
+yourNameContainsProhibitedWordsDescription: "名前に禁止されている文字列が含まれています。この名前を使用したい場合は、サーバー管理者にお問い合わせください。"
+thisContentsAreMarkedAsSigninRequiredByAuthor: "投稿者により、表示にはログインが必要と設定されています"
+lockdown: "ロックダウン"
+pleaseSelectAccount: "アカウントを選択してください"
+availableRoles: "利用可能なロール"
+
+_accountSettings:
+  requireSigninToViewContents: "コンテンツの表示にログインを必須にする"
+  requireSigninToViewContentsDescription1: "あなたが作成した全てのノートなどのコンテンツを表示するのにログインを必須にします。クローラーに情報が収集されるのを防ぐ効果が期待できます。"
+  requireSigninToViewContentsDescription2: "URLプレビュー(OGP)、Webページへの埋め込み、ノートの引用に対応していないサーバーからの表示も不可になります。"
+  requireSigninToViewContentsDescription3: "リモートサーバーに連合されたコンテンツでは、これらの制限が適用されない場合があります。"
+  makeNotesFollowersOnlyBefore: "過去のノートをフォロワーのみ表示可能にする"
+  makeNotesFollowersOnlyBeforeDescription: "この機能が有効になっている間、設定された日時より過去、または設定された時間を経過しているノートがフォロワーのみ表示可能になります。無効に戻すと、ノートの公開状態も元に戻ります。"
+  makeNotesHiddenBefore: "過去のノートを非公開化する"
+  makeNotesHiddenBeforeDescription: "この機能が有効になっている間、設定された日時より過去、または設定された時間を経過しているノートが自分のみ表示可能(非公開化)になります。無効に戻すと、ノートの公開状態も元に戻ります。"
+  mayNotEffectForFederatedNotes: "リモートサーバーに連合されたノートには効果が及ばない場合があります。"
+  notesHavePassedSpecifiedPeriod: "指定した時間を経過しているノート"
+  notesOlderThanSpecifiedDateAndTime: "指定した日時より前のノート"
+
+_abuseUserReport:
+  forward: "転送"
+  forwardDescription: "匿名のシステムアカウントとして、リモートサーバーに通報を転送します。"
+  resolve: "解決"
+  accept: "是認"
+  reject: "否認"
+  resolveTutorial: "内容が正当である通報に対応した場合は「是認」を選択し、肯定的にケースが解決されたことをマークします。\n内容が正当でない通報の場合は「否認」を選択し、否定的にケースが解決されたことをマークします。"
 
 _delivery:
   status: "配信状態"
@@ -1384,8 +1463,10 @@ _serverSettings:
   fanoutTimelineDescription: "有効にすると、各種タイムラインを取得する際のパフォーマンスが大幅に向上し、データベースへの負荷を軽減することが可能です。ただし、Redisのメモリ使用量は増加します。サーバーのメモリ容量が少ない場合、または動作が不安定な場合は無効にすることができます。"
   fanoutTimelineDbFallback: "データベースへのフォールバック"
   fanoutTimelineDbFallbackDescription: "有効にすると、タイムラインがキャッシュされていない場合にDBへ追加で問い合わせを行うフォールバック処理を行います。無効にすると、フォールバック処理を行わないことでさらにサーバーの負荷を軽減することができますが、タイムラインが取得できる範囲に制限が生じます。"
+  reactionsBufferingDescription: "有効にすると、リアクション作成時のパフォーマンスが大幅に向上し、データベースへの負荷を軽減することが可能です。ただし、Redisのメモリ使用量は増加します。"
   inquiryUrl: "問い合わせ先URL"
   inquiryUrlDescription: "サーバー運営者へのお問い合わせフォームのURLや、運営者の連絡先等が記載されたWebページのURLを指定します。"
+  thisSettingWillAutomaticallyOffWhenModeratorsInactive: "一定期間モデレーターのアクティビティが検出されなかった場合、スパム防止のためこの設定は自動でオフになります。"
 
 _accountMigration:
   moveFrom: "別のアカウントからこのアカウントに移行"
@@ -1705,6 +1786,7 @@ _role:
     canManageAvatarDecorations: "アバターデコレーションの管理"
     driveCapacity: "ドライブ容量"
     alwaysMarkNsfw: "ファイルにNSFWを常に付与"
+    canUpdateBioMedia: "アイコンとバナーの更新を許可"
     pinMax: "ノートのピン留めの最大数"
     antennaMax: "アンテナの作成可能数"
     wordMuteMax: "ワードミュートの最大文字数"
@@ -1719,6 +1801,11 @@ _role:
     canSearchNotes: "ノート検索の利用"
     canUseTranslator: "翻訳機能の利用"
     avatarDecorationLimit: "アイコンデコレーションの最大取付個数"
+    canImportAntennas: "アンテナのインポートを許可"
+    canImportBlocking: "ブロックのインポートを許可"
+    canImportFollowing: "フォローのインポートを許可"
+    canImportMuting: "ミュートのインポートを許可"
+    canImportUserLists: "リストのインポートを許可"
   _condition:
     roleAssignedTo: "マニュアルロールにアサイン済み"
     isLocal: "ローカルユーザー"
@@ -1958,7 +2045,6 @@ _theme:
     buttonBg: "ボタンの背景"
     buttonHoverBg: "ボタンの背景 (ホバー)"
     inputBorder: "入力ボックスの縁取り"
-    listItemHoverBg: "リスト項目の背景 (ホバー)"
     driveFolderBg: "ドライブフォルダーの背景"
     wallpaperOverlay: "壁紙のオーバーレイ"
     badge: "バッジ"
@@ -1971,8 +2057,6 @@ _sfx:
   note: "ノート"
   noteMy: "ノート(自分)"
   notification: "通知"
-  antenna: "アンテナ受信"
-  channel: "チャンネル通知"
   reaction: "リアクション選択時"
 
 _soundSettings:
@@ -1982,6 +2066,7 @@ _soundSettings:
   driveFileTypeWarnDescription: "音声ファイルを選択してください"
   driveFileDurationWarn: "音声が長すぎます"
   driveFileDurationWarnDescription: "長い音声を使用するとMisskeyの使用に支障をきたす可能性があります。それでも続行しますか?"
+  driveFileError: "音声が読み込めませんでした。設定を変更してください"
 
 _ago:
   future: "未来"
@@ -2134,8 +2219,11 @@ _auth:
   permissionAsk: "このアプリは次の権限を要求しています"
   pleaseGoBack: "アプリケーションに戻ってやっていってください"
   callback: "アプリケーションに戻っています"
+  accepted: "アクセスを許可しました"
   denied: "アクセスを拒否しました"
+  scopeUser: "以下のユーザーとして操作しています"
   pleaseLogin: "アプリケーションにアクセス許可を与えるには、ログインが必要です。"
+  byClickingYouWillBeRedirectedToThisUrl: "アクセスを許可すると、自動で以下のURLに遷移します"
 
 _antennaSources:
   all: "全てのノート"
@@ -2252,6 +2340,9 @@ _profile:
   changeBanner: "バナー画像を変更"
   verifiedLinkDescription: "内容にURLを設定すると、リンク先のWebサイトに自分のプロフィールへのリンクが含まれている場合に所有者確認済みアイコンを表示させることができます。"
   avatarDecorationMax: "最大{max}つまでデコレーションを付けられます。"
+  followedMessage: "フォローされた時のメッセージ"
+  followedMessageDescription: "フォローされた時に相手に表示する短いメッセージを設定できます。"
+  followedMessageDescriptionForLockedAccount: "フォローを承認制にしている場合、フォローリクエストを許可した時に表示されます。"
 
 _exportOrImport:
   allNotes: "全てのノート"
@@ -2350,6 +2441,7 @@ _pages:
   eyeCatchingImageSet: "アイキャッチ画像を設定"
   eyeCatchingImageRemove: "アイキャッチ画像を削除"
   chooseBlock: "ブロックを追加"
+  enterSectionTitle: "セクションタイトルを入力"
   selectType: "種類を選択"
   contentBlocks: "コンテンツ"
   inputBlocks: "入力"
@@ -2379,7 +2471,7 @@ _notification:
   youGotMention: "{name}からのメンション"
   youGotReply: "{name}からのリプライ"
   youGotQuote: "{name}による引用"
-  youRenoted: "{name}がRenoteしました"
+  youRenoted: "{name}がリノートしました"
   youWereFollowed: "フォローされました"
   youReceivedFollowRequest: "フォローリクエストが来ました"
   yourFollowRequestAccepted: "フォローリクエストが承認されました"
@@ -2398,6 +2490,8 @@ _notification:
   renotedBySomeUsers: "{n}人がリノートしました"
   followedBySomeUsers: "{n}人にフォローされました"
   flushNotification: "通知の履歴をリセットする"
+  exportOfXCompleted: "{x}のエクスポートが完了しました"
+  login: "ログインがありました"
 
   _types:
     all: "すべて"
@@ -2405,7 +2499,7 @@ _notification:
     follow: "フォロー"
     mention: "メンション"
     reply: "リプライ"
-    renote: "Renote"
+    renote: "リノート"
     quote: "引用"
     reaction: "リアクション"
     pollEnded: "アンケートが終了"
@@ -2413,12 +2507,15 @@ _notification:
     followRequestAccepted: "フォローが受理された"
     roleAssigned: "ロールが付与された"
     achievementEarned: "実績の獲得"
+    exportCompleted: "エクスポートが完了した"
+    login: "ログイン"
+    test: "通知のテスト"
     app: "連携アプリからの通知"
 
   _actions:
     followBack: "フォローバック"
     reply: "返信"
-    renote: "Renote"
+    renote: "リノート"
 
 _deck:
   alwaysShowMainColumn: "常にメインカラムを表示"
@@ -2468,9 +2565,10 @@ _drivecleaner:
 
 _webhookSettings:
   createWebhook: "Webhookを作成"
+  modifyWebhook: "Webhookを編集"
   name: "名前"
   secret: "シークレット"
-  events: "Webhookを実行するタイミング"
+  trigger: "トリガー"
   active: "有効"
   _events:
     follow: "フォローしたとき"
@@ -2480,6 +2578,30 @@ _webhookSettings:
     renote: "Renoteされたとき"
     reaction: "リアクションがあったとき"
     mention: "メンションされたとき"
+  _systemEvents:
+    abuseReport: "ユーザーから通報があったとき"
+    abuseReportResolved: "ユーザーからの通報を処理したとき"
+    userCreated: "ユーザーが作成されたとき"
+    inactiveModeratorsWarning: "モデレーターが一定期間非アクティブになったとき"
+    inactiveModeratorsInvitationOnlyChanged: "モデレーターが一定期間非アクティブだったため、システムにより招待制へと変更されたとき"
+  deleteConfirm: "Webhookを削除しますか?"
+  testRemarks: "スイッチの右にあるボタンをクリックするとダミーのデータを使用したテスト用Webhookを送信できます。"
+
+_abuseReport:
+  _notificationRecipient:
+    createRecipient: "通報の通知先を追加"
+    modifyRecipient: "通報の通知先を編集"
+    recipientType: "通知先の種類"
+    _recipientType:
+      mail: "メール"
+      webhook: "Webhook"
+      _captions:
+        mail: "モデレーター権限を持つユーザーのメールアドレスに通知を送ります(通報を受けた時のみ)"
+        webhook: "指定したSystemWebhookに通知を送ります(通報を受けた時と通報を解決した時にそれぞれ発信)"
+    keywords: "キーワード"
+    notifiedUser: "通知先ユーザー"
+    notifiedWebhook: "使用するWebhook"
+    deleteConfirm: "通知先を削除しますか?"
 
 _moderationLogTypes:
   createRole: "ロールを作成"
@@ -2509,6 +2631,8 @@ _moderationLogTypes:
   markSensitiveDriveFile: "ファイルをセンシティブ付与"
   unmarkSensitiveDriveFile: "ファイルをセンシティブ解除"
   resolveAbuseReport: "通報を解決"
+  forwardAbuseReport: "通報を転送"
+  updateAbuseReportNote: "通報のモデレーションノート更新"
   createInvitation: "招待コードを作成"
   createAd: "広告を作成"
   deleteAd: "広告を削除"
@@ -2518,6 +2642,16 @@ _moderationLogTypes:
   deleteAvatarDecoration: "アイコンデコレーションを削除"
   unsetUserAvatar: "ユーザーのアイコンを解除"
   unsetUserBanner: "ユーザーのバナーを解除"
+  createSystemWebhook: "SystemWebhookを作成"
+  updateSystemWebhook: "SystemWebhookを更新"
+  deleteSystemWebhook: "SystemWebhookを削除"
+  createAbuseReportNotificationRecipient: "通報の通知先を作成"
+  updateAbuseReportNotificationRecipient: "通報の通知先を更新"
+  deleteAbuseReportNotificationRecipient: "通報の通知先を削除"
+  deleteAccount: "アカウントを削除"
+  deletePage: "ページを削除"
+  deleteFlash: "Playを削除"
+  deleteGalleryPost: "ギャラリーの投稿を削除"
 
 _fileViewer:
   title: "ファイルの詳細"
@@ -2572,16 +2706,16 @@ _externalResourceInstaller:
 
 _dataSaver:
   _media:
-    title: "メディアの読み込み"
+    title: "メディアの読み込みを無効化"
     description: "画像・動画が自動で読み込まれるのを防止します。隠れている画像・動画はタップすると読み込まれます。"
   _avatar:
-    title: "アイコン画像"
+    title: "アイコン画像のアニメーションを無効化"
     description: "アイコン画像のアニメーションが停止します。アニメーション画像は通常の画像よりファイルサイズが大きいことがあるので、データ通信量をさらに削減できます。"
   _urlPreview:
-    title: "URLプレビューのサムネイル"
+    title: "URLプレビューのサムネイルを非表示"
     description: "URLプレビューのサムネイル画像が読み込まれなくなります。"
   _code:
-    title: "コードハイライト"
+    title: "コードハイライトを非表示"
     description: "MFMなどでコードハイライト記法が使われている場合、タップするまで読み込まれなくなります。コードハイライトではハイライトする言語ごとにその定義ファイルを読み込む必要がありますが、それらが自動で読み込まれなくなるため、通信量の削減が見込めます。"
 
 _hemisphere:
@@ -2657,3 +2791,31 @@ _mediaControls:
   pip: "ピクチャインピクチャ"
   playbackRate: "再生速度"
   loop: "ループ再生"
+
+_contextMenu:
+  title: "コンテキストメニュー"
+  app: "アプリケーション"
+  appWithShift: "Shiftキーでアプリケーション"
+  native: "ブラウザのUI"
+
+_embedCodeGen:
+  title: "埋め込みコードをカスタマイズ"
+  header: "ヘッダーを表示"
+  autoload: "自動で続きを読み込む(非推奨)"
+  maxHeight: "高さの最大値"
+  maxHeightDescription: "0で最大値の設定が無効になります。ウィジェットが縦に伸び続けるのを防ぐために、何らかの値に指定してください。"
+  maxHeightWarn: "高さの最大値制限が無効(0)になっています。これが意図した変更ではない場合は、高さの最大値を何らかの値に設定してください。"
+  previewIsNotActual: "プレビュー画面で表示可能な範囲を超えたため、実際に埋め込んだ際とは表示が異なります。"
+  rounded: "角丸にする"
+  border: "外枠に枠線をつける"
+  applyToPreview: "プレビューに反映"
+  generateCode: "埋め込みコードを作成"
+  codeGenerated: "コードが生成されました"
+  codeGeneratedDescription: "生成されたコードをウェブサイトに貼り付けてご利用ください。"
+
+_selfXssPrevention:
+  warning: "警告"
+  title: "「この画面に何か貼り付けろ」はすべて詐欺です。"
+  description1: "ここに何かを貼り付けると、悪意のあるユーザーにアカウントを乗っ取られたり、個人情報を盗まれたりする可能性があります。"
+  description2: "貼り付けようとしているものが何なのかを正確に理解していない場合は、%c今すぐ作業を中止してこのウィンドウを閉じてください。"
+  description3: "詳しくはこちらをご確認ください。 {link}"
diff --git a/locales/ja-KS.yml b/locales/ja-KS.yml
index 7a33968e9e..50132c0645 100644
--- a/locales/ja-KS.yml
+++ b/locales/ja-KS.yml
@@ -8,6 +8,9 @@ search: "探す"
 notifications: "通知"
 username: "ユーザー名"
 password: "パスワード"
+initialPasswordForSetup: "初期設定開始用パスワード"
+initialPasswordIsIncorrect: "初期設定開始用のパスワードがちゃうで。"
+initialPasswordForSetupDescription: "Miskkeyを自分でインストールしたんやったら、設定ファイルに入れたパスワードを使ってや。\nホスティングサービスを使っとるんやったら、サービスから言われたやつを使うんやで。\n別に何も設定しとらんのやったら、何も入れずに空けといてな。"
 forgotPassword: "パスワード忘れたん?"
 fetchingAsApObject: "今ちと連合に照会しとるで"
 ok: "ええで"
@@ -60,6 +63,7 @@ copyFileId: "ファイルIDをコピー"
 copyFolderId: "フォルダーIDをコピー"
 copyProfileUrl: "プロフィールURLをコピー"
 searchUser: "ユーザーを探す"
+searchThisUsersNotes: "ユーザーのノートを検索"
 reply: "返事"
 loadMore: "まだまだあるで!"
 showMore: "まだまだあるで!"
@@ -108,11 +112,14 @@ enterEmoji: "絵文字を入れてや"
 renote: "リノート"
 unrenote: "リノートやめる"
 renoted: "リノートしたで。"
+renotedToX: "{name}にリノートしたで"
 cantRenote: "この投稿はリノートできへんっぽい。"
 cantReRenote: "リノート自体はリノートできへんで。"
 quote: "引用"
 inChannelRenote: "チャンネルの中でリノート"
 inChannelQuote: "チャンネル内引用"
+renoteToChannel: "チャンネルにリノート"
+renoteToOtherChannel: "他のチャンネルにリノート"
 pinnedNote: "ピン留めされとるノート"
 pinned: "ピン留めしとく"
 you: "あんた"
@@ -151,6 +158,7 @@ editList: "リストいじる"
 selectChannel: "チャンネルを選ぶ"
 selectAntenna: "アンテナを選ぶ"
 editAntenna: "アンテナいじる"
+createAntenna: "アンテナを作成"
 selectWidget: "ウィジェットを選ぶ"
 editWidgets: "ウィジェットをいじる"
 editWidgetsExit: "いじるのをやめる"
@@ -177,6 +185,10 @@ addAccount: "アカウントを追加"
 reloadAccountsList: "アカウントリストの情報を更新"
 loginFailed: "ログインに失敗してもうた…"
 showOnRemote: "リモートで見る"
+continueOnRemote: "リモートで続行"
+chooseServerOnMisskeyHub: "Misskey Hubからサーバーを選択"
+specifyServerHost: "サーバーのドメインを直接指定"
+inputHostName: "ドメインを入力せえや"
 general: "全般"
 wallpaper: "壁紙"
 setWallpaper: "壁紙を設定"
@@ -187,6 +199,7 @@ followConfirm: "{name}をフォローしてええか?"
 proxyAccount: "プロキシアカウント"
 proxyAccountDescription: "プロキシアカウントは、代わりにフォローしてくれるアカウントや。例えば、551に豚まんが無いときやったり、ユーザーがリモートユーザーをアカウントに入れたとき、リストに入れられたユーザーが誰からもフォローされてないと寂しいやん。寂しいし、アクティビティも配達されへんから、プロキシアカウントがフォローしてくれるで。ええやつやん…"
 host: "ホスト"
+selectSelf: "自分を選択"
 selectUser: "ユーザーを選ぶ"
 recipient: "宛先"
 annotation: "注釈"
@@ -202,6 +215,7 @@ perDay: "1日ごと"
 stopActivityDelivery: "アクティビティの配送をやめる"
 blockThisInstance: "このサーバーをブロックすんで"
 silenceThisInstance: "サーバーサイレンスすんで?"
+mediaSilenceThisInstance: "サーバーをメディアサイレンス"
 operations: "操作"
 software: "ソフトウェア"
 version: "バージョン"
@@ -223,6 +237,10 @@ blockedInstances: "ブロックしたサーバー"
 blockedInstancesDescription: "ブロックしたいサーバーのホストを改行で区切って設定してな。ブロックされてもうたサーバーとはもう金輪際やり取りできひんくなるで。"
 silencedInstances: "サーバーサイレンスされてんねん"
 silencedInstancesDescription: "サイレンスしたいサーバーのホストを改行で区切って設定すんで。サイレンスされたサーバーに所属するアカウントはすべて「サイレンス」として扱われ、フォローがすべてリクエストになり、フォロワーでないローカルアカウントにはメンションできなくなんねん。ブロックしたインスタンスには影響せーへんで。"
+mediaSilencedInstances: "メディアサイレンスしたサーバー"
+mediaSilencedInstancesDescription: "メディアサイレンスしたいサーバーのホストを改行で区切って設定するで。メディアサイレンスされたサーバーに所属するアカウントによるファイルはすべてセンシティブとして扱われてな、カスタム絵文字が使えへんようになるで。ブロックしたインスタンスには影響せえへんで。"
+federationAllowedHosts: "連合を許すサーバー"
+federationAllowedHostsDescription: "連合してもいいサーバーのホストを行ごとに区切って設定してや。"
 muteAndBlock: "ミュートとブロック"
 mutedUsers: "ミュートしとるユーザー"
 blockedUsers: "ブロックしとるユーザー"
@@ -313,6 +331,7 @@ selectFile: "ファイル選んでや"
 selectFiles: "ファイル選んでや"
 selectFolder: "フォルダ選んでや"
 selectFolders: "フォルダ選んでや"
+fileNotSelected: "ファイルが選択されてへんで"
 renameFile: "ファイル名をいらう"
 folderName: "フォルダー名"
 createFolder: "フォルダー作る"
@@ -320,6 +339,7 @@ renameFolder: "フォルダー名を変える"
 deleteFolder: "フォルダーをほかす"
 folder: "フォルダー"
 addFile: "ファイルを追加"
+showFile: "ファイル出す"
 emptyDrive: "ドライブは空っぽや"
 emptyFolder: "このフォルダーは空や"
 unableToDelete: "消せんかったわ"
@@ -434,6 +454,7 @@ totpDescription: "認証アプリ使うてワンタイムパスワードを入
 moderator: "モデレーター"
 moderation: "モデレーション"
 moderationNote: "モデレーションノート"
+moderationNoteDescription: "モデレーターの中だけで共有するメモを入れれるで。"
 addModerationNote: "モデレーションノートを追加するで"
 moderationLogs: "モデログ"
 nUsersMentioned: "{n}人が投稿"
@@ -468,10 +489,12 @@ retype: "もっかい入力"
 noteOf: "{user}はんのノート"
 quoteAttached: "引用付いとるで"
 quoteQuestion: "引用として添付してもええか?"
+attachAsFileQuestion: "クリップボードのテキストが長すぎるからテキストファイルとして添付してもええか?"
 noMessagesYet: "まだチャットはあらへんで"
 newMessageExists: "新しいメッセージがきたで"
 onlyOneFileCanBeAttached: "ごめんな、メッセージに添付できるファイルはひとつだけなんよ。"
 signinRequired: "ログインしてくれへん?"
+signinOrContinueOnRemote: "続行するには、お使いのサーバーに移動するか、このサーバーに登録・ログインする必要があるで"
 invitations: "来てや"
 invitationCode: "招待コード"
 checking: "確認しとるで"
@@ -493,7 +516,10 @@ uiLanguage: "UIの表示言語"
 aboutX: "{x}について"
 emojiStyle: "絵文字のスタイル"
 native: "ネイティブ"
-disableDrawer: "メニューをドロワーで表示せえへん"
+menuStyle: "メニューのスタイル"
+style: "スタイル"
+drawer: "ドロワー"
+popup: "ポップアップ"
 showNoteActionsOnlyHover: "ノートの操作部をホバー時のみ表示するで"
 showReactionsCount: "ノートのリアクション数を表示する"
 noHistory: "履歴はないわ。"
@@ -576,6 +602,8 @@ ascendingOrder: "小さい順"
 descendingOrder: "大きい順"
 scratchpad: "スクラッチパッド"
 scratchpadDescription: "スクラッチパッドではAiScriptを色々試すことができるんや。Misskeyに対して色々できるコードを書いて動かしてみたり、結果を見たりできるで。"
+uiInspector: "UIインスペクター"
+uiInspectorDescription: "メモリ上にあるUIコンポーネントのインスタンス一覧を見れるで。UIコンポーネントはUi:C:系関数で生成されるで。"
 output: "出力"
 script: "スクリプト"
 disablePagesScript: "Pagesのスクリプトを無効にしてや"
@@ -692,10 +720,7 @@ abuseReported: "無事内容が送信されたみたいやで。おおきに〜
 reporter: "通報者"
 reporteeOrigin: "通報先"
 reporterOrigin: "通報元"
-forwardReport: "リモートサーバーに通報を転送するで"
-forwardReportIsAnonymous: "リモートサーバーからはあんたの情報は見えんなって、匿名のシステムアカウントとして表示されるで。"
 send: "送信"
-abuseMarkAsResolved: "対応したで"
 openInNewTab: "新しいタブで開く"
 openInSideView: "サイドビューで開く"
 defaultNavigationBehaviour: "デフォルトのナビゲーション"
@@ -832,6 +857,7 @@ administration: "管理"
 accounts: "アカウント"
 switch: "切り替え"
 noMaintainerInformationWarning: "管理者情報が設定されてへんで"
+noInquiryUrlWarning: "問い合わせ先URLが設定されてへんで。"
 noBotProtectionWarning: "Botプロテクションが設定されてへんで。"
 configure: "設定する"
 postToGallery: "ギャラリーへ投稿"
@@ -896,6 +922,7 @@ followersVisibility: "フォロワーの公開範囲"
 continueThread: "さらにスレッドを見るで"
 deleteAccountConfirm: "アカウントを消すで?ええんか?"
 incorrectPassword: "パスワードがちゃうわ。"
+incorrectTotp: "ワンタイムパスワードが間違っとるか、期限が切れとるみたいやな。"
 voteConfirm: "「{choice}」に投票するんか?"
 hide: "隠す"
 useDrawerReactionPickerForMobile: "ケータイとかのときドロワーで表示するで"
@@ -1021,6 +1048,7 @@ thisPostMayBeAnnoyingHome: "ホームに投稿"
 thisPostMayBeAnnoyingCancel: "やめとく"
 thisPostMayBeAnnoyingIgnore: "このまま投稿"
 collapseRenotes: "見たことあるリノートは飛ばして表示するで"
+collapseRenotesDescription: "リアクションやリノートをしたことがあるノートをたたんで表示するで。"
 internalServerError: "サーバー内部エラー"
 internalServerErrorDescription: "サーバーでなんか変なこと起こっとるわ。"
 copyErrorInfo: "エラー情報をコピるで"
@@ -1059,6 +1087,7 @@ retryAllQueuesConfirmTitle: "もっかいやってみるか?"
 retryAllQueuesConfirmText: "一時的にサーバー重なるかもしれへんで。"
 enableChartsForRemoteUser: "リモートユーザーのチャートを作る"
 enableChartsForFederatedInstances: "リモートサーバーのチャートを作る"
+enableStatsForFederatedInstances: "リモートサーバの情報を取得"
 showClipButtonInNoteFooter: "ノートのアクションにクリップを追加"
 reactionsDisplaySize: "ツッコミの表示のでかさ"
 limitWidthOfReaction: "ツッコミの最大横幅を制限して、ちっさく表示するで"
@@ -1094,6 +1123,8 @@ preservedUsernames: "予約ユーザー名"
 preservedUsernamesDescription: "予約しとくユーザー名を行ごとに挙げるで。ここで指定されたユーザー名はアカウント作るときに使えへんくなるけど、管理者は例外や。あと、もうあるアカウントも例外やな。"
 createNoteFromTheFile: "このファイル使うてノート作るで"
 archive: "アーカイブ"
+archived: "アーカイブ済み"
+unarchive: "アーカイブ解除"
 channelArchiveConfirmTitle: "{name}をアーカイブしてええか?"
 channelArchiveConfirmDescription: "アーカイブしたら、チャンネル一覧とか検索結果からなくなるし、新しく書き込みもできへんなるで。"
 thisChannelArchived: "このチャンネル、アーカイブされとるで。"
@@ -1104,6 +1135,9 @@ preventAiLearning: "生成AIの学習に使わんといて"
 preventAiLearningDescription: "他の文章生成AIとか画像生成AIに、投稿したノートとか画像なんかを勝手に使わんように頼むで。具体的にはnoaiフラグをHTMLレスポンスに含めるんやけど、これ聞いてくれるんはAIの気分次第やから、使われる可能性もちょっとはあるな。"
 options: "オプション"
 specifyUser: "ユーザー指定"
+lookupConfirm: "照会するけどええか?"
+openTagPageConfirm: "ハッシュタグのページを開くんか?"
+specifyHost: "ホスト指定"
 failedToPreviewUrl: "プレビューできへん"
 update: "更新"
 rolesThatCanBeUsedThisEmojiAsReaction: "ツッコミとして使えるロール"
@@ -1235,10 +1269,46 @@ keepOriginalFilenameDescription: "この設定をオフにすると、アップ
 noDescription: "説明文はあらへんで"
 alwaysConfirmFollow: "フォローの際常に確認する"
 inquiry: "問い合わせ"
+tryAgain: "もう一度試しいや。"
+confirmWhenRevealingSensitiveMedia: "センシティブなメディアを表示するとき確認する"
+sensitiveMediaRevealConfirm: "センシティブなメディアやで。表示するんか?"
+createdLists: "作成したリスト"
+createdAntennas: "作成したアンテナ"
+fromX: "{x}から"
+genEmbedCode: "埋め込みコードを作る"
+noteOfThisUser: "このユーザーのノート全部"
+clipNoteLimitExceeded: "これ以上このクリップにノート追加でけへんわ。"
+performance: "パフォーマンス"
+modified: "変更あり"
+discard: "やめる"
+thereAreNChanges: "{n}個の変更があるみたいや"
+signinWithPasskey: "パスキーでログイン"
+unknownWebAuthnKey: "登録されてへんパスキーやな。"
+passkeyVerificationFailed: "パスキーの検証に失敗したで。"
+passkeyVerificationSucceededButPasswordlessLoginDisabled: "パスキーの検証は成功したんやけど、パスワードレスログインが無効になっとるわ。"
+messageToFollower: "フォロワーへのメッセージ"
+target: "対象"
+testCaptchaWarning: "CAPTCHAのテストを目的としてるで。<strong>絶対に本番環境で使わんといてな。絶対やで。</strong>"
+prohibitedWordsForNameOfUser: "禁止ワード(ユーザー名)"
+prohibitedWordsForNameOfUserDescription: "このリストの中にある文字列がユーザー名に入っとったら、その名前に変更できひんようになるで。モデレーター権限があるユーザーは除外や。"
+yourNameContainsProhibitedWords: "その名前は禁止した文字列が含まれとるで"
+yourNameContainsProhibitedWordsDescription: "その名前は禁止した文字列が含まれとるわ。どうしてもって言うなら、サーバー管理者に言うしかないで。"
+_abuseUserReport:
+  forward: "転送"
+  forwardDescription: "匿名のシステムアカウントってことにして、リモートサーバーに通報を転送するで。"
+  resolve: "解決"
+  accept: "ええよ"
+  reject: "あかんよ"
+  resolveTutorial: "内容がええなら「ええよ」を選ぶんや。肯定的に解決されたことにして記録するで。\n逆に、内容がだめなら「あかんよ」を選びいや。否定的に解決されたって記録しとくで。"
 _delivery:
+  status: "配信状態"
   stop: "配信せぇへん"
+  resume: "配信再開"
   _type:
     none: "配信しとる"
+    manuallySuspended: "手動停止中"
+    goneSuspended: "サーバー削除のため停止中"
+    autoSuspendedForNotResponding: "サーバー応答せえへんから停止中"
 _bubbleGame:
   howToPlay: "遊び方"
   hold: "ホールド"
@@ -1364,6 +1434,10 @@ _serverSettings:
   fanoutTimelineDescription: "入れると、おのおのタイムラインを取得するときにめちゃめちゃ動きが良うなって、データベースが軽くなるわ。でも、Redisのメモリ使う量が増えるから注意な。サーバーのメモリが足りんときとか、動きが変なときは切れるで。"
   fanoutTimelineDbFallback: "データベースにフォールバックする"
   fanoutTimelineDbFallbackDescription: "有効にしたら、タイムラインがキャッシュん中に入ってないときにDBにもっかい問い合わせるフォールバック処理ってのをやっとくで。切ったらフォールバック処理をやらんからサーバーはもっと軽くなんねんけど、タイムラインの取得範囲がちょっと減るで。"
+  reactionsBufferingDescription: "有効にしたら、リアクション作るときのパフォーマンスがすっごい上がって、データベースへの負荷が減るで。代わりに、Redisのメモリ使用は増えるで。"
+  inquiryUrl: "問い合わせ先URL"
+  inquiryUrlDescription: "サーバー運営者へのお問い合わせフォームのURLや、運営者の連絡先等が記載されたWebページのURLを指定するで。"
+  thisSettingWillAutomaticallyOffWhenModeratorsInactive: "一定期間モデレーターがおらんかったら、スパムを防ぐためにこの設定は勝手に切られるで。"
 _accountMigration:
   moveFrom: "別のアカウントからこのアカウントに引っ越す"
   moveFromSub: "別のアカウントへエイリアスを作る"
@@ -1680,6 +1754,7 @@ _role:
     canManageAvatarDecorations: "アバターを飾るモンの管理"
     driveCapacity: "ドライブ容量"
     alwaysMarkNsfw: "勝手にファイルにNSFWをくっつける"
+    canUpdateBioMedia: "アイコンとバナーの更新を許可"
     pinMax: "ノートピン留めできる数"
     antennaMax: "アンテナ作れる数"
     wordMuteMax: "ワードミュートの最大文字数"
@@ -1694,6 +1769,11 @@ _role:
     canSearchNotes: "ノート探せるかどうか"
     canUseTranslator: "翻訳使えるかどうか"
     avatarDecorationLimit: "アイコンデコのいっちばんつけれる数"
+    canImportAntennas: "アンテナのインポートを許す"
+    canImportBlocking: "ブロックのインポートを許す"
+    canImportFollowing: "フォローのインポートを許す"
+    canImportMuting: "ミュートのインポートを許す"
+    canImportUserLists: "リストのインポートを許す"
   _condition:
     roleAssignedTo: "マニュアルロールにアサイン済み"
     isLocal: "ローカルユーザー"
@@ -1911,7 +1991,6 @@ _theme:
     buttonBg: "ボタンの背景"
     buttonHoverBg: "ボタンの背景 (ホバー)"
     inputBorder: "入力ボックスの縁取り"
-    listItemHoverBg: "リスト項目の背景 (ホバー)"
     driveFolderBg: "ドライブフォルダーの背景"
     wallpaperOverlay: "壁紙のオーバーレイ"
     badge: "バッジ"
@@ -1923,8 +2002,6 @@ _sfx:
   note: "ノート"
   noteMy: "ノート(自分)"
   notification: "通知"
-  antenna: "アンテナ受信"
-  channel: "チャンネル通知"
   reaction: "ツッコミ選んどるとき"
 _soundSettings:
   driveFile: "ドライブん中の音使う"
@@ -1933,6 +2010,7 @@ _soundSettings:
   driveFileTypeWarnDescription: "音声ファイルを選びや"
   driveFileDurationWarn: "音が長すぎるわ"
   driveFileDurationWarnDescription: "長い音使うたらMisskey使うのに良うないかもしれへんで。それでもええか?"
+  driveFileError: "音声が読み込めへんかったで。設定を変更せえや"
 _ago:
   future: "未来"
   justNow: "ついさっき"
@@ -2189,6 +2267,9 @@ _profile:
   changeBanner: "バナー画像を変更するで"
   verifiedLinkDescription: "内容をURLに設定すると、リンク先のwebサイトに自分のプロフのリンクが含まれてる場合に所有者確認済みアイコンを表示させることができるで。"
   avatarDecorationMax: "最大{max}つまでデコつけれんで"
+  followedMessage: "フォローされたら返すメッセージ"
+  followedMessageDescription: "フォローされたときに相手に返す短めのメッセージを決めれるで。"
+  followedMessageDescriptionForLockedAccount: "フォローが承認制なら、フォローリクエストをOKしたときに見せるで。"
 _exportOrImport:
   allNotes: "全てのノート"
   favoritedNotes: "お気に入りにしたノート"
@@ -2281,6 +2362,7 @@ _pages:
   eyeCatchingImageSet: "アイキャッチ画像を設定"
   eyeCatchingImageRemove: "アイキャッチ画像を削除"
   chooseBlock: "ブロックを追加"
+  enterSectionTitle: "セクションタイトルを入れる"
   selectType: "種類を選択"
   contentBlocks: "コンテンツ"
   inputBlocks: "入力"
@@ -2326,13 +2408,15 @@ _notification:
   renotedBySomeUsers: "{n}人がリノートしたで"
   followedBySomeUsers: "{n}人にフォローされたで"
   flushNotification: "通知の履歴をリセットする"
+  exportOfXCompleted: "{x}のエクスポートが終わったわ"
+  login: "ログインしとったで"
   _types:
     all: "すべて"
     note: "あんたらの新規投稿"
     follow: "フォロー"
     mention: "メンション"
     reply: "リプライ"
-    renote: "Renote"
+    renote: "リノート"
     quote: "引用"
     reaction: "ツッコミ"
     pollEnded: "アンケートが終了したで"
@@ -2340,15 +2424,19 @@ _notification:
     followRequestAccepted: "フォローが受理されたで"
     roleAssigned: "ロールが付与された"
     achievementEarned: "実績の獲得"
+    exportCompleted: "エクスポート終わった"
+    login: "ログイン"
+    test: "通知テスト"
     app: "連携アプリからの通知や"
   _actions:
     followBack: "フォローバック"
     reply: "返事"
-    renote: "Renote"
+    renote: "リノート"
 _deck:
   alwaysShowMainColumn: "いつもメインカラムを表示"
   columnAlign: "カラムの寄せ"
   addColumn: "カラムを追加"
+  newNoteNotificationSettings: "新着ノート通知の設定"
   configureColumn: "カラムの設定"
   swapLeft: "左に移動"
   swapRight: "右に移動"
@@ -2387,9 +2475,10 @@ _drivecleaner:
   orderByCreatedAtAsc: "追加日の古い順"
 _webhookSettings:
   createWebhook: "Webhookをつくる"
+  modifyWebhook: "Webhookを編集"
   name: "名前"
   secret: "シークレット"
-  events: "Webhookを投げるタイミング"
+  trigger: "トリガー"
   active: "有効"
   _events:
     follow: "フォローしたとき~!"
@@ -2399,6 +2488,29 @@ _webhookSettings:
     renote: "リノートされるとき~!"
     reaction: "ツッコまれたとき~!"
     mention: "メンションがあるとき~!"
+  _systemEvents:
+    abuseReport: "ユーザーから通報があったとき"
+    abuseReportResolved: "ユーザーからの通報を処理したとき"
+    userCreated: "ユーザーが作成されたとき"
+    inactiveModeratorsWarning: "モデレーターがしばらくおらんかったとき"
+    inactiveModeratorsInvitationOnlyChanged: "モデレーターがしばらくおらんかったから、システムが招待制に変えたとき"
+  deleteConfirm: "ほんまにWebhookをほかしてもええんか?"
+  testRemarks: "スイッチ右のボタンを押すとダミーデータを使ったテスト用Webhookを送れるで。"
+_abuseReport:
+  _notificationRecipient:
+    createRecipient: "通報の通知先を追加"
+    modifyRecipient: "通報の通知先を編集"
+    recipientType: "通知先の種類"
+    _recipientType:
+      mail: "メール"
+      webhook: "Webhook"
+      _captions:
+        mail: "モデレーター権限を持つユーザーのメアドに通知を送るで(通報を受けた時のみ)"
+        webhook: "指定したSystemWebhookに通知を送るで(通報を受けた時と通報を解決した時にそれぞれ発信)"
+    keywords: "キーワード"
+    notifiedUser: "通知先ユーザー"
+    notifiedWebhook: "使用するWebhook"
+    deleteConfirm: "通知先を削除してもええか?"
 _moderationLogTypes:
   createRole: "ロールを追加すんで"
   deleteRole: "ロールほかす"
@@ -2427,6 +2539,8 @@ _moderationLogTypes:
   markSensitiveDriveFile: "ファイルをセンシティブ付与"
   unmarkSensitiveDriveFile: "ファイルをセンシティブ解除"
   resolveAbuseReport: "苦情を解決"
+  forwardAbuseReport: "通報を転送"
+  updateAbuseReportNote: "通報のモデレーションノート更新"
   createInvitation: "招待コード作る"
   createAd: "広告を作んで"
   deleteAd: "広告ほかす"
@@ -2436,6 +2550,16 @@ _moderationLogTypes:
   deleteAvatarDecoration: "アイコンデコレーションを削除"
   unsetUserAvatar: "この子のアイコン元に戻す"
   unsetUserBanner: "この子のバナー元に戻す"
+  createSystemWebhook: "SystemWebhookを作成"
+  updateSystemWebhook: "SystemWebhookを更新"
+  deleteSystemWebhook: "SystemWebhookを削除"
+  createAbuseReportNotificationRecipient: "通報の通知先作る"
+  updateAbuseReportNotificationRecipient: "通報の通知先更新"
+  deleteAbuseReportNotificationRecipient: "通報の通知先消す"
+  deleteAccount: "アカウント消す"
+  deletePage: "ページ消す"
+  deleteFlash: "Playをほかす"
+  deleteGalleryPost: "ギャラリーの投稿をほかす"
 _fileViewer:
   title: "ファイルの詳しい情報"
   type: "ファイルの種類"
@@ -2567,3 +2691,22 @@ _mediaControls:
   pip: "ピクチャインピクチャ"
   playbackRate: "再生速度"
   loop: "ループ再生"
+_contextMenu:
+  title: "コンテキストメニュー"
+  app: "アプリ"
+  appWithShift: "Shiftキーでアプリ"
+  native: "ブラウザのUI"
+_embedCodeGen:
+  title: "埋め込みコードをカスタム"
+  header: "ヘッダー出す"
+  autoload: "勝手に続きを読み込む(非推奨)"
+  maxHeight: "高さの最大値"
+  maxHeightDescription: "0は最大値を指定せえへんけど、ウィジェットが伸び続けるから絶対1以上にしといてや。"
+  maxHeightWarn: "高さの最大値が無効になっとるで。意図してへん変更なら、普通の値に戻してや。"
+  previewIsNotActual: "プレビュー画面で出せる範囲をはみ出したから、ホンマの表示とはちゃうとおもうで。"
+  rounded: "角丸める"
+  border: "外枠に枠線つける"
+  applyToPreview: "プレビューに反映"
+  generateCode: "埋め込みコード作る"
+  codeGenerated: "コード作ったで"
+  codeGeneratedDescription: "作ったコードはウェブサイトに貼っつけて使ってや。"
diff --git a/locales/kab-KAB.yml b/locales/kab-KAB.yml
index 22e24d3baa..d4aa36fa70 100644
--- a/locales/kab-KAB.yml
+++ b/locales/kab-KAB.yml
@@ -104,3 +104,7 @@ _deck:
   _columns:
     notifications: "Ilɣuyen"
     list: "Tibdarin"
+_abuseReport:
+  _notificationRecipient:
+    _recipientType:
+      mail: "Imayl"
diff --git a/locales/kn-IN.yml b/locales/kn-IN.yml
index b3ad46f2b1..222599572a 100644
--- a/locales/kn-IN.yml
+++ b/locales/kn-IN.yml
@@ -77,6 +77,8 @@ _profile:
   username: "ಬಳಕೆಹೆಸರು"
 _notification:
   youWereFollowed: "ಹಿಂಬಾಲಿಸಿದರು"
+  _types:
+    login: "ಪ್ರವೇಶ"
   _actions:
     reply: "ಉತ್ತರಿಸು"
 _deck:
diff --git a/locales/ko-GS.yml b/locales/ko-GS.yml
index 9466aff01f..1f7faba23a 100644
--- a/locales/ko-GS.yml
+++ b/locales/ko-GS.yml
@@ -468,7 +468,7 @@ tooShort: "억수로 짜립니다"
 tooLong: "억수로 집니다"
 passwordMatched: "맞십니다"
 passwordNotMatched: "안 맞십니다"
-signinWith: "{n}서 로그인"
+signinWith: "{x} 서 로그인"
 signinFailed: "로그인 몬 했십니다. 고 이름이랑 비밀번호 제대로 썼는가 확인해 주이소."
 or: "아니면"
 language: "언어"
@@ -476,7 +476,6 @@ uiLanguage: "UI 표시 언어"
 aboutX: "{x}에 대해서"
 emojiStyle: "이모지 모양"
 native: "기본"
-disableDrawer: "드로어 메뉴 쓰지 않기"
 showNoteActionsOnlyHover: "마우스 올맀을 때만 노트 액션 버턴 보이기"
 noHistory: "기록이 없십니다"
 signinHistory: "로그인 기록"
@@ -583,6 +582,9 @@ describeFile: "캡션 옇기"
 enterFileDescription: "캡션 서기"
 author: "맨던 사람"
 manage: "간리"
+large: "커게"
+medium: "엔갆게"
+small: "쪼맪게"
 emailServer: "전자우펜 서버"
 email: "전자우펜"
 emailAddress: "전자우펜 주소"
@@ -599,7 +601,6 @@ reportAbuseOf: "{name}님얼 신고하기"
 reporter: "신고한 사람"
 reporteeOrigin: "신고덴 사람"
 reporterOrigin: "신고한 곳"
-forwardReport: "웬겍 서버에 신고 보내기"
 waitingFor: "{x}(얼)럴 지달리고 잇십니다"
 random: "무작이"
 system: "시스템"
@@ -613,12 +614,14 @@ followersCount: "팔로워 수"
 noteFavoritesCount: "질겨찾기한 노트 수"
 clips: "클립 맨걸기"
 clearCache: "캐시 비우기"
+nUsers: "{n} 사용자"
 typingUsers: "{users} 님이 서고 잇어예"
 unlikeConfirm: "좋네예럴 무룹니꺼?"
 info: "정보"
 selectAccount: "계정 개리기"
 user: "사용자"
 administration: "간리"
+middle: "엔갆게"
 translatedFrom: "{x}서 번옉"
 on: "킴"
 off: "껌"
@@ -633,6 +636,7 @@ oneMonth: "한 달"
 file: "파일"
 typeToConfirm: "게속할라먼 {x}럴 누질라 주이소"
 pleaseSelect: "개리 주이소"
+remoteOnly: "웬겍만"
 tools: "도구"
 like: "좋네예!"
 unlike: "좋네예 무루기"
@@ -643,7 +647,10 @@ role: "옉할"
 noRole: "옉할이 어ᇝ십니다"
 thisPostMayBeAnnoyingCancel: "아이예"
 likeOnly: "좋네예마"
+hiddenTags: "수ᇚ훈 해시태그"
 myClips: "내 클립"
+preservedUsernames: "예약 사용자 이럼"
+specifyUser: "사용자 지정"
 icon: "아바타"
 replies: "답하기"
 renotes: "리노트"
@@ -709,6 +716,16 @@ _achievements:
       description: "0분 0초에 노트를 섰어예"
     _tutorialCompleted:
       description: "길라잡이럴 껕냇십니다"
+_role:
+  displayOrder: "보기 순서"
+  _priority:
+    middle: "엔갆게"
+  _options:
+    canHideAds: "강고 수ᇚ후기"
+  _condition:
+    isRemote: "웬겍 사용자"
+    isCat: "갱이 사용자"
+    isBot: "자동 사용자"
 _gallery:
   my: "내 걸"
   liked: "좋네예한 걸"
@@ -792,10 +809,13 @@ _notification:
   _types:
     follow: "팔로잉"
     mention: "멘션"
+    renote: "리노트"
     quote: "따오기"
     reaction: "반엉"
+    login: "로그인"
   _actions:
     reply: "답하기"
+    renote: "리노트"
 _deck:
   _columns:
     notifications: "알림"
@@ -805,6 +825,10 @@ _deck:
     mentions: "받언 멘션"
 _webhookSettings:
   name: "이럼"
+_abuseReport:
+  _notificationRecipient:
+    _recipientType:
+      mail: "전자우펜"
 _moderationLogTypes:
   suspend: "얼우기"
   deleteNote: "노트 뭉캐기"
diff --git a/locales/ko-KR.yml b/locales/ko-KR.yml
index 294a5a1520..414202adab 100644
--- a/locales/ko-KR.yml
+++ b/locales/ko-KR.yml
@@ -8,6 +8,9 @@ search: "검색"
 notifications: "알림"
 username: "유저명"
 password: "비밀번호"
+initialPasswordForSetup: "초기 설정용 비밀번호"
+initialPasswordIsIncorrect: "초기 설정용 비밀번호가 올바르지 않습니다."
+initialPasswordForSetupDescription: "Misskey를 직접 설치하는 경우, 설정 파일에 입력해둔 비밀번호를 사용하세요.\nMisskey 설치를 도와주는 호스팅 서비스 등을 사용하는 경우, 서비스 제공자로부터 받은 비밀번호를 사용하세요.\n비밀번호를 따로 설정하지 않은 경우, 아무것도 입력하지 않아도 됩니다."
 forgotPassword: "비밀번호 재설정"
 fetchingAsApObject: "연합에서 찾아보는 중"
 ok: "확인"
@@ -53,13 +56,14 @@ addToList: "리스트에 추가"
 addToAntenna: "안테나에 추가"
 sendMessage: "메시지 보내기"
 copyRSS: "RSS 복사"
-copyUsername: "사용자 이름 복사"
-copyUserId: "사용자 ID 복사"
+copyUsername: "유저명 복사"
+copyUserId: "유저 ID 복사"
 copyNoteId: "노트 ID 복사"
 copyFileId: "파일 ID 복사"
 copyFolderId: "폴더 ID 복사"
 copyProfileUrl: "프로필 URL 복사"
 searchUser: "사용자 검색"
+searchThisUsersNotes: "사용자의 노트 검색"
 reply: "답글"
 loadMore: "더 보기"
 showMore: "더 보기"
@@ -108,22 +112,25 @@ enterEmoji: "이모지 입력"
 renote: "리노트"
 unrenote: "리노트 취소"
 renoted: "리노트했습니다"
+renotedToX: "{name}명이 리노트했습니다."
 cantRenote: "이 게시물은 리노트 할 수 없습니다."
-cantReRenote: "리노트를 리노트할 수 없습니다."
+cantReRenote: "리노트를 리노트 할 수 없습니다."
 quote: "인용"
 inChannelRenote: "채널 내 리노트"
 inChannelQuote: "채널 내 인용"
+renoteToChannel: "채널에 리노트"
+renoteToOtherChannel: "다른 채널에 리노트"
 pinnedNote: "고정된 노트"
 pinned: "고정하기"
 you: "나"
 clickToShow: "클릭하여 보기"
 sensitive: "열람 주의"
 add: "추가"
-reaction: "반응"
-reactions: "반응"
+reaction: "리액션"
+reactions: "리액션"
 emojiPicker: "이모지 선택기"
-pinnedEmojisForReactionSettingDescription: "리액션을 할 때 프로필에 고정하여 표시할 이모지를 설정할 수 있습니다"
-pinnedEmojisSettingDescription: "이모지를 입력할 때 프로필에 고정하여 표시할 이모지를 설정할 수 있습니다"
+pinnedEmojisForReactionSettingDescription: "리액션을 할 때 이모지 선택기 상단에 표시할 이모지를 설정할 수 있습니다."
+pinnedEmojisSettingDescription: "이모지를 입력할 때 이모지 선택기 상단에 표시할 이모지를 설정할 수 있습니다."
 emojiPickerDisplay: "선택기 표시"
 overwriteFromPinnedEmojisForReaction: "리액션 설정을 덮어쓰기"
 overwriteFromPinnedEmojis: "일반 설정을 덮어쓰기"
@@ -136,7 +143,7 @@ unmarkAsSensitive: "열람주의 해제"
 enterFileName: "파일명을 입력"
 mute: "뮤트"
 unmute: "뮤트 해제"
-renoteMute: "리노트 뮤트하기"
+renoteMute: "리노트 뮤트"
 renoteUnmute: "리노트 뮤트 해제"
 block: "차단"
 unblock: "차단 해제"
@@ -151,6 +158,7 @@ editList: "리스트 편집"
 selectChannel: "채널 선택"
 selectAntenna: "안테나 선택"
 editAntenna: "안테나 편집"
+createAntenna: "안테나 만들기"
 selectWidget: "위젯 선택"
 editWidgets: "위젯 편집"
 editWidgetsExit: "편집 종료"
@@ -174,12 +182,16 @@ flagShowTimelineReplies: "타임라인에 노트의 답글을 표시하기"
 flagShowTimelineRepliesDescription: "이 설정을 활성화하면 타임라인에 다른 유저 간의 답글을 표시합니다."
 autoAcceptFollowed: "팔로우 중인 유저로부터의 팔로우 요청을 자동 수락"
 addAccount: "계정 추가"
-reloadAccountsList: "계정 리스트 정보 갱신"
+reloadAccountsList: "계정 목록 새로고침"
 loginFailed: "로그인에 실패했습니다"
 showOnRemote: "리모트에서 보기"
+continueOnRemote: "리모트에서 계속"
+chooseServerOnMisskeyHub: "Misskey Hub에서 서버 찾아보기"
+specifyServerHost: "서버 도메인 직접 지정"
+inputHostName: "도메인을 입력하세요"
 general: "일반"
 wallpaper: "배경"
-setWallpaper: "배경화면 설정"
+setWallpaper: "배경 설정"
 removeWallpaper: "배경 제거"
 searchWith: "검색: {q}"
 youHaveNoLists: "리스트가 없습니다"
@@ -187,7 +199,8 @@ followConfirm: "{name}님을 팔로우 하시겠습니까?"
 proxyAccount: "프록시 계정"
 proxyAccountDescription: "프록시 계정은 특정 조건 하에서 유저의 리모트 팔로우를 대행하는 계정입니다. 예를 들면, 유저가 리모트 유저를 리스트에 넣었을 때, 리스트에 들어간 유저를 아무도 팔로우한 적이 없다면 액티비티가 서버로 배달되지 않기 때문에, 대신 프록시 계정이 해당 유저를 팔로우하도록 합니다."
 host: "호스트"
-selectUser: "사용자 선택"
+selectSelf: "본인을 선택"
+selectUser: "유저 선택"
 recipient: "수신인"
 annotation: "내용에 대한 주석"
 federation: "연합"
@@ -202,6 +215,7 @@ perDay: "1일마다"
 stopActivityDelivery: "액티비티 보내지 않기"
 blockThisInstance: "이 서버를 차단"
 silenceThisInstance: "서버를 사일런스"
+mediaSilenceThisInstance: "서버의 미디어를 사일런스"
 operations: "작업"
 software: "소프트웨어"
 version: "버전"
@@ -223,6 +237,10 @@ blockedInstances: "차단된 서버"
 blockedInstancesDescription: "차단하려는 서버의 호스트 이름을 줄바꿈으로 구분하여 설정합니다. 차단된 인스턴스는 이 인스턴스와 통신할 수 없게 됩니다."
 silencedInstances: "사일런스한 서버"
 silencedInstancesDescription: "사일런스하려는 서버의 호스트명을 한 줄에 하나씩 입력합니다. 사일런스된 서버에 소속된 유저는 모두 '사일런스'된 상태로 취급되며, 이 서버로부터의 팔로우가 프로필 설정과 무관하게 승인제로 변경되고, 팔로워가 아닌 로컬 유저에게는 멘션할 수 없게 됩니다. 정지된 서버에는 적용되지 않습니다."
+mediaSilencedInstances: "미디어를 사일런스한 서버"
+mediaSilencedInstancesDescription: "미디어를 사일런스 하려는 서버의 호스트를 한 줄에 하나씩 입력합니다. 미디어가 사일런스된 서버의 유저가 업로드한 파일은 모두 민감한 미디어로 처리되며, 커스텀 이모지를 사용할 수 없게 됩니다. 또한, 차단한 인스턴스에는 적용되지 않습니다."
+federationAllowedHosts: "연합을 허가하는 서버"
+federationAllowedHostsDescription: "연합을 허가하는 서버의 호스트를 엔터로 구분해서 설정합니다."
 muteAndBlock: "뮤트 및 차단"
 mutedUsers: "뮤트한 유저"
 blockedUsers: "차단한 유저"
@@ -230,7 +248,7 @@ noUsers: "아무도 없습니다"
 editProfile: "프로필 수정"
 noteDeleteConfirm: "이 노트를 삭제하시겠습니까?"
 pinLimitExceeded: "더 이상 고정할 수 없습니다."
-intro: "Misskey의 설치를 완료했습니다! 관리자 계정을 만들어 주세요."
+intro: "Misskey의 설치가 완료되었습니다! 관리자 계정을 생성해주세요."
 done: "완료"
 processing: "처리중"
 preview: "미리보기"
@@ -247,7 +265,7 @@ publishing: "배포 중"
 notResponding: "응답 없음"
 instanceFollowing: "서버의 팔로잉"
 instanceFollowers: "서버의 팔로워"
-instanceUsers: "서버의 유저"
+instanceUsers: "서버의 사용자"
 changePassword: "비밀번호 변경"
 security: "보안"
 retypedNotMatch: "입력이 일치하지 않습니다."
@@ -263,12 +281,12 @@ lookup: "찾아보기"
 announcements: "공지사항"
 imageUrl: "이미지 URL"
 remove: "삭제"
-removed: "삭제하였습니다"
+removed: "삭제했습니다"
 removeAreYouSure: "\"{x}\" 을(를) 삭제하시겠습니까?"
 deleteAreYouSure: "\"{x}\" 을(를) 삭제하시겠습니까?"
 resetAreYouSure: "초기화 하시겠습니까?"
 areYouSure: "계속 진행하시겠습니까?"
-saved: "저장하였습니다"
+saved: "저장했습니다"
 messaging: "대화"
 upload: "업로드"
 keepOriginalUploading: "원본 이미지를 유지"
@@ -296,7 +314,7 @@ activity: "활동"
 images: "이미지"
 image: "이미지"
 birthday: "생일"
-yearsOld: "만 {age} 세"
+yearsOld: "{age}세"
 registeredDate: "등록일"
 location: "장소"
 theme: "테마"
@@ -313,6 +331,7 @@ selectFile: "파일 선택"
 selectFiles: "파일 선택"
 selectFolder: "폴더 선택"
 selectFolders: "폴더 선택"
+fileNotSelected: "파일을 선택하지 않았습니다"
 renameFile: "파일 이름 변경"
 folderName: "폴더 이름"
 createFolder: "폴더 만들기"
@@ -320,6 +339,7 @@ renameFolder: "폴더 이름 바꾸기"
 deleteFolder: "폴더 삭제"
 folder: "폴더"
 addFile: "파일 추가"
+showFile: "파일 표시하기"
 emptyDrive: "드라이브가 비어 있습니다"
 emptyFolder: "폴더가 비어 있습니다"
 unableToDelete: "삭제할 수 없습니다"
@@ -365,12 +385,12 @@ registration: "등록"
 enableRegistration: "신규 회원가입을 활성화"
 invite: "초대"
 driveCapacityPerLocalAccount: "로컬 유저 한 명당 드라이브 용량"
-driveCapacityPerRemoteAccount: "리모트 유저 한 명당 드라이브 용량"
+driveCapacityPerRemoteAccount: "원격 사용자별 드라이브 용량"
 inMb: "메가바이트 단위"
 bannerUrl: "배너 이미지 URL"
 backgroundImageUrl: "배경 이미지 URL"
 basicInfo: "기본 정보"
-pinnedUsers: "고정된 유저"
+pinnedUsers: "고정한 사용자"
 pinnedUsersDescription: "\"발견하기\" 페이지 등에 고정하고 싶은 유저를 한 줄에 한 명씩 적습니다."
 pinnedPages: "고정한 페이지"
 pinnedPagesDescription: "서버의 대문에 고정하고 싶은 페이지의 경로를 한 줄에 하나씩 적습니다."
@@ -434,16 +454,17 @@ totpDescription: "인증 앱을 사용하여 일회성 비밀번호 입력"
 moderator: "모더레이터"
 moderation: "조정"
 moderationNote: "조정 기록"
+moderationNoteDescription: "모더레이터 역할을 가진 유저만 보이는 메모를 적을 수 있습니다."
 addModerationNote: "조정 기록 추가하기"
 moderationLogs: "모더레이션 로그"
 nUsersMentioned: "{n}명이 언급함"
-securityKeyAndPasskey: "보안 키 또는 패스 키"
+securityKeyAndPasskey: "보안 키 또는 패스키"
 securityKey: "보안 키"
 lastUsed: "마지막 사용"
 lastUsedAt: "마지막 사용: {t}"
 unregister: "등록 해제"
 passwordLessLogin: "비밀번호 없이 로그인"
-passwordLessLoginDescription: "비밀번호를 사용하지 않고 보안 키 또는 패스 키 등으로만 로그인합니다."
+passwordLessLoginDescription: "비밀번호 없이 보안 키 또는 패스키만 사용해서 로그인합니다."
 resetPassword: "비밀번호 재설정"
 newPasswordIs: "새로운 비밀번호는 \"{password}\" 입니다"
 reduceUiAnimation: "UI의 애니메이션을 줄이기"
@@ -468,10 +489,12 @@ retype: "다시 입력"
 noteOf: "{user}의 노트"
 quoteAttached: "인용함"
 quoteQuestion: "인용해서 작성하시겠습니까?"
+attachAsFileQuestion: "붙여넣으려는 글이 너무 깁니다. 텍스트 파일로 첨부하시겠습니까?"
 noMessagesYet: "아직 대화가 없습니다"
 newMessageExists: "새 메시지가 있습니다"
 onlyOneFileCanBeAttached: "메시지에 첨부할 수 있는 파일은 하나까지입니다"
 signinRequired: "진행하기 전에 로그인을 해 주세요"
+signinOrContinueOnRemote: "계속하려면 사용하는 서버로 이동하거나 이 서버에 로그인해야 합니다."
 invitations: "초대"
 invitationCode: "초대 코드"
 checking: "확인하는 중입니다"
@@ -486,15 +509,18 @@ strongPassword: "강한 비밀번호"
 passwordMatched: "일치합니다"
 passwordNotMatched: "일치하지 않습니다"
 signinWith: "{x}로 로그인"
-signinFailed: "로그인할 수 없습니다. 사용자명과 비밀번호를 확인하여 주십시오."
+signinFailed: "로그인할 수 없습니다. 사용자 이름과 비밀번호를 확인해 주십시오."
 or: "혹은"
 language: "언어"
 uiLanguage: "UI 표시 언어"
 aboutX: "{x}에 대하여"
 emojiStyle: "이모지 스타일"
 native: "기본"
-disableDrawer: "드로어 메뉴를 사용하지 않기"
-showNoteActionsOnlyHover: "노트 액션 버튼을 마우스를 올렸을 때에만 표시"
+menuStyle: "메뉴 스타일"
+style: "스타일"
+drawer: "서랍"
+popup: "팝업"
+showNoteActionsOnlyHover: "마우스가 올라간 때에만 노트 동작 버튼을 표시하기"
 showReactionsCount: "노트의 반응 수를 표시하기"
 noHistory: "기록이 없습니다"
 signinHistory: "로그인 기록"
@@ -559,7 +585,7 @@ popout: "새 창으로 열기"
 volume: "음량"
 masterVolume: "마스터 볼륨"
 notUseSound: "음소거 하기"
-useSoundOnlyWhenActive: "Misskey가 활성화 되어져 있을 때만 소리 출력하기"
+useSoundOnlyWhenActive: "Misskey를 활성화한 때에만 소리를 출력하기"
 details: "자세히"
 chooseEmoji: "이모지 선택"
 unableToProcess: "작업을 완료할 수 없습니다"
@@ -576,6 +602,8 @@ ascendingOrder: "오름차순"
 descendingOrder: "내림차순"
 scratchpad: "스크래치 패드"
 scratchpadDescription: "스크래치 패드는 AiScript 의 테스트 환경을 제공합니다. Misskey 와 상호 작용하는 코드를 작성, 실행 및 결과를 확인할 수 있습니다."
+uiInspector: "UI 인스펙터"
+uiInspectorDescription: "메모리에 있는 UI 컴포넌트의 인스턴트 목록을 볼 수 있습니다. UI 컴포넌트는 Ui:C: 계열 함수로 만들어집니다."
 output: "출력"
 script: "스크립트"
 disablePagesScript: "Pages 에서 AiScript 를 사용하지 않음"
@@ -588,7 +616,7 @@ deleteAllFiles: "모든 파일 삭제"
 deleteAllFilesConfirm: "모든 파일을 삭제하시겠습니까?"
 removeAllFollowing: "모든 팔로잉 해제"
 removeAllFollowingDescription: "{host} 서버의 모든 팔로잉을 해제합니다. 해당 서버가 더 이상 존재하지 않는 경우 등에 실행해 주세요."
-userSuspended: "이 계정은 정지된 상태입니다."
+userSuspended: "이 사용자는 정지되었습니다."
 userSilenced: "이 계정은 사일런스된 상태입니다."
 yourAccountSuspendedTitle: "계정이 정지되었습니다"
 yourAccountSuspendedDescription: "이 계정은 서버의 이용 약관을 위반하거나, 기타 다른 이유로 인해 정지되었습니다. 자세한 사항은 관리자에게 문의해 주십시오. 계정을 새로 생성하지 마십시오."
@@ -692,10 +720,7 @@ abuseReported: "신고를 보냈습니다. 신고해 주셔서 감사합니다."
 reporter: "신고자"
 reporteeOrigin: "피신고자"
 reporterOrigin: "신고자"
-forwardReport: "리모트 서버에도 신고 내용 보내기"
-forwardReportIsAnonymous: "리모트 서버에서는 나의 정보를 볼 수 없으며, 익명의 시스템 계정으로 표시됩니다."
 send: "전송"
-abuseMarkAsResolved: "해결됨으로 표시"
 openInNewTab: "새 탭에서 열기"
 openInSideView: "사이드뷰로 열기"
 defaultNavigationBehaviour: "기본 탐색 동작"
@@ -752,7 +777,7 @@ experimentalFeatures: "실험실"
 experimental: "실험실"
 thisIsExperimentalFeature: "이 기능은 실험적인 기능입니다. 사양이 변경되거나 정상적으로 동작하지 않을 가능성이 있습니다."
 developer: "개발자"
-makeExplorable: "\"발견하기\"에 내 계정 보이기"
+makeExplorable: "계정을 쉽게 발견하도록 하기"
 makeExplorableDescription: "비활성화하면 \"발견하기\"에 나의 계정을 표시하지 않습니다."
 showGapBetweenNotesInTimeline: "타임라인의 노트 사이를 띄워서 표시"
 duplicate: "복제"
@@ -798,7 +823,7 @@ emailNotification: "메일 알림"
 publish: "게시"
 inChannelSearch: "채널에서 검색"
 useReactionPickerForContextMenu: "우클릭하여 리액션 선택기 열기"
-typingUsers: "{users} 님이 입력하고 있어요.."
+typingUsers: "{users}님이 입력 중"
 jumpToSpecifiedDate: "특정 날짜로 이동"
 showingPastTimeline: "과거의 타임라인을 표시하고 있어요"
 clear: "지우기"
@@ -832,6 +857,7 @@ administration: "관리"
 accounts: "계정"
 switch: "전환"
 noMaintainerInformationWarning: "관리자 정보가 설정되어 있지 않습니다."
+noInquiryUrlWarning: "문의처 주소를 설정하지 않았습니다."
 noBotProtectionWarning: "Bot 방어가 설정되어 있지 않습니다."
 configure: "설정하기"
 postToGallery: "갤러리에 업로드"
@@ -896,6 +922,7 @@ followersVisibility: "팔로워의 공개 범위"
 continueThread: "글타래 더 보기"
 deleteAccountConfirm: "계정이 삭제되고 되돌릴 수 없게 됩니다. 계속하시겠습니까? "
 incorrectPassword: "비밀번호가 올바르지 않습니다."
+incorrectTotp: "OTP 번호가 틀렸거나 유효기간이 만료되어 있을 수 있습니다."
 voteConfirm: "\"{choice}\"에 투표하시겠습니까?"
 hide: "숨기기"
 useDrawerReactionPickerForMobile: "모바일에서 드로어 메뉴로 표시"
@@ -1021,6 +1048,7 @@ thisPostMayBeAnnoyingHome: "홈에 게시"
 thisPostMayBeAnnoyingCancel: "그만두기"
 thisPostMayBeAnnoyingIgnore: "이대로 게시"
 collapseRenotes: "이미 본 리노트를 간략화하기"
+collapseRenotesDescription: "반응이나 리노트를 한 노트를 접어서 표시합니다."
 internalServerError: "내부 서버 오류"
 internalServerErrorDescription: "내부 서버에서 예기치 않은 오류가 발생했습니다."
 copyErrorInfo: "오류 정보 복사"
@@ -1059,6 +1087,7 @@ retryAllQueuesConfirmTitle: "지금 다시 시도하시겠습니까?"
 retryAllQueuesConfirmText: "일시적으로 서버의 부하가 증가할 수 있습니다."
 enableChartsForRemoteUser: "리모트 유저의 차트를 생성"
 enableChartsForFederatedInstances: "리모트 서버의 차트를 생성"
+enableStatsForFederatedInstances: "리모트 서버 정보 받아오기"
 showClipButtonInNoteFooter: "노트 동작에 클립을 추가"
 reactionsDisplaySize: "리액션 표시 크기"
 limitWidthOfReaction: "리액션의 최대 폭을 제한하고 작게 표시하기"
@@ -1090,10 +1119,12 @@ serverRules: "서버 규칙"
 pleaseConfirmBelowBeforeSignup: "이 서버에 가입하기 전에 아래 사항을 확인하여 주십시오."
 pleaseAgreeAllToContinue: "계속하시려면 모든 항목에 동의하십시오."
 continue: "계속"
-preservedUsernames: "예약된 사용자명"
+preservedUsernames: "예약한 사용자 이름"
 preservedUsernamesDescription: "예약할 사용자명을 한 줄에 하나씩 입력합니다. 여기에서 지정한 사용자명으로는 계정을 생성할 수 없게 됩니다. 단, 관리자 권한으로 계정을 생성할 때에는 해당되지 않으며, 이미 존재하는 계정도 영향을 받지 않습니다."
 createNoteFromTheFile: "이 파일로 노트를 작성"
 archive: "아카이브"
+archived: "아카이브 됨"
+unarchive: "보관 취소"
 channelArchiveConfirmTitle: "{name} 채널을 보존하시겠습니까?"
 channelArchiveConfirmDescription: "보존한 채널은 채널 목록과 검색 결과에 표시되지 않으며 새로운 노트도 작성할 수 없습니다."
 thisChannelArchived: "이 채널은 보존되었습니다."
@@ -1104,6 +1135,9 @@ preventAiLearning: "기계학습(생성형 AI)으로의 사용을 거부"
 preventAiLearningDescription: "외부의 문장 생성 AI나 이미지 생성 AI에 대해 제출한 노트나 이미지 등의 콘텐츠를 학습의 대상으로 사용하지 않도록 요구합니다. 다만, 이 요구사항을 지킬 의무는 없기 때문에 학습을 완전히 방지하는 것은 아닙니다."
 options: "옵션"
 specifyUser: "사용자 지정"
+lookupConfirm: "조회 할까요?"
+openTagPageConfirm: "해시태그의 페이지를 열까요?"
+specifyHost: "호스트 지정"
 failedToPreviewUrl: "미리 볼 수 없음"
 update: "업데이트"
 rolesThatCanBeUsedThisEmojiAsReaction: "이 이모지를 리액션으로 사용할 수 있는 역할"
@@ -1230,10 +1264,51 @@ useTotp: "일회용 비밀번호 사용"
 useBackupCode: "백업 코드 사용"
 launchApp: "앱 실행"
 useNativeUIForVideoAudioPlayer: "브라우저 UI에서 미디어 재생"
+keepOriginalFilename: "원본 파일 이름을 유지"
+keepOriginalFilenameDescription: "이 설정을 끄면 업로드를 할 때 파일 이름이 자동으로 무작위 문자열로 바뀝니다."
+noDescription: "설명문이 없습니다"
+alwaysConfirmFollow: "팔로우일 때 항상 확인하기"
+inquiry: "문의하기"
+tryAgain: "다시 시도해 주세요."
+confirmWhenRevealingSensitiveMedia: "민감한 미디어를 열 때 두 번 확인"
+sensitiveMediaRevealConfirm: "민감한 미디어입니다. 표시할까요?"
+createdLists: "만든 리스트"
+createdAntennas: "만든 안테나"
+fromX: "{x}부터"
+genEmbedCode: "임베디드 코드 만들기"
+noteOfThisUser: "이 유저의 노트 목록"
+clipNoteLimitExceeded: "더 이상 이 클립에 노트를 추가 할 수 없습니다."
+performance: "퍼포먼스"
+modified: "변경 있음"
+discard: "파기"
+thereAreNChanges: "{n}건 변경이 있습니다."
+signinWithPasskey: "패스키로 로그인"
+unknownWebAuthnKey: "등록되지 않은 패스키입니다."
+passkeyVerificationFailed: "패스키 검증을 실패했습니다."
+passkeyVerificationSucceededButPasswordlessLoginDisabled: "패스키를 검증했으나, 비밀번호 없이 로그인하기가 꺼져 있습니다."
+messageToFollower: "팔로워에 보낼 메시지"
+target: "대상"
+testCaptchaWarning: "CAPTCHA를 테스트하기 위한 기능입니다. <strong>실제 환경에서는 사용하지 마세요.</strong>"
+prohibitedWordsForNameOfUser: "금지 단어 (사용자 이름)"
+prohibitedWordsForNameOfUserDescription: "이 목록에 포함되는 키워드가 사용자 이름에 있는 경우, 일반 사용자는 이름을 바꿀 수 없습니다. 모더레이터 권한을 가진 사용자는 제한 대상에서 제외됩니다."
+yourNameContainsProhibitedWords: "바꾸려는 이름에 금지된 키워드가 포함되어 있습니다."
+yourNameContainsProhibitedWordsDescription: "이름에 금지된 키워드가 있습니다. 이름을 사용해야 하는 경우, 서버 관리자에 문의하세요."
+_abuseUserReport:
+  forward: "전달"
+  forwardDescription: "익명 시스템 계정을 사용하여 리모트 서버에 신고 내용을 전달할 수 있습니다."
+  resolve: "해결됨"
+  accept: "인용"
+  reject: "기각"
+  resolveTutorial: "적절한 신고 내용에 대응한 경우, \"인용\"을 선택하여 \"해결됨\"으로 기록합니다.\n적절하지 않은 신고를 받은 경우, \"기각\"을 선택하여 \"기각\"으로 기록합니다."
 _delivery:
+  status: "전송 상태"
   stop: "정지됨"
+  resume: "전송 다시 시작"
   _type:
     none: "배포 중"
+    manuallySuspended: "수동 정지 중"
+    goneSuspended: "서버 삭제를 이유로 정지 중"
+    autoSuspendedForNotResponding: "서버 응답 없음을 이유로 정지 중"
 _bubbleGame:
   howToPlay: "설명"
   hold: "홀드"
@@ -1359,6 +1434,10 @@ _serverSettings:
   fanoutTimelineDescription: "활성화하면 각종 타임라인을 가져올 때의 성능을 대폭 향상하며, 데이터베이스의 부하를 줄일 수 있습니다. 단, Redis의 메모리 사용량이 증가합니다. 서버의 메모리 용량이 작거나, 서비스가 불안정해지는 경우 비활성화할 수 있습니다."
   fanoutTimelineDbFallback: "데이터베이스를 예비로 사용하기"
   fanoutTimelineDbFallbackDescription: "활성화하면 타임라인의 캐시되어 있지 않은 부분에 대해 DB에 질의하여 정보를 가져옵니다. 비활성화하면 이를 실행하지 않음으로써 서버의 부하를 줄일 수 있지만, 타임라인에서 가져올 수 있는 게시물 범위가 한정됩니다."
+  reactionsBufferingDescription: "활성화 한 경우, 리액션 작성 퍼포먼스가 대폭 향상되어 DB의 부하를 줄일 수 있으나, Redis의 메모리 사용량이 많아집니다."
+  inquiryUrl: "문의처 URL"
+  inquiryUrlDescription: "서버 운영자에게 보내는 문의 양식의 URL이나 운영자의 연락처 등이 적힌 웹 페이지의 URL을 설정합니다."
+  thisSettingWillAutomaticallyOffWhenModeratorsInactive: "일정 기간동안 모더레이터의 활동이 감지되지 않는 경우, 스팸 방지를 위해 이 설정은 자동으로 꺼집니다."
 _accountMigration:
   moveFrom: "다른 계정에서 이 계정으로 이사"
   moveFromSub: "다른 계정에 대한 별칭을 생성"
@@ -1675,10 +1754,11 @@ _role:
     canManageAvatarDecorations: "아바타 꾸미기 관리"
     driveCapacity: "드라이브 용량"
     alwaysMarkNsfw: "파일을 항상 NSFW로 지정"
+    canUpdateBioMedia: "아바타 및 배너 이미지 변경 허용"
     pinMax: "고정할 수 있는 노트 수"
     antennaMax: "만들 수 있는 안테나 수"
     wordMuteMax: "단어 뮤트할 수 있는 문자 수"
-    webhookMax: "만들 수 있는 웹후크 수"
+    webhookMax: "만들 수 있는 Webhook 수"
     clipMax: "만들 수 있는 클립 수"
     noteEachClipsMax: "클립에 넣을 수 있는 노트 수"
     userListMax: "만들 수 있는 사용자 리스트 수"
@@ -1689,10 +1769,20 @@ _role:
     canSearchNotes: "노트 검색 이용 가능 여부"
     canUseTranslator: "번역 기능의 사용"
     avatarDecorationLimit: "아바타 장식의 최대 붙임 개수"
+    canImportAntennas: "안테나 가져오기 허용"
+    canImportBlocking: "차단 목록 가져오기 허용"
+    canImportFollowing: "팔로우 가져오기 허용"
+    canImportMuting: "뮤트 목록 가져오기 허용"
+    canImportUserLists: "리스트 목록 가져오기 허용"
   _condition:
     roleAssignedTo: "수동 역할에 이미 할당됨"
     isLocal: "로컬 사용자"
-    isRemote: "리모트 사용자"
+    isRemote: "원격 사용자"
+    isCat: "고양이 사용자"
+    isBot: "봇 사용자"
+    isSuspended: "정지된 사용자"
+    isLocked: "잠금 계정 사용자"
+    isExplorable: "‘계정을 쉽게 발견하도록 하기’를 활성화한 사용자"
     createdLessThan: "가입한 지 다음 일수 이내인 유저"
     createdMoreThan: "가입한 지 다음 일수 이상인 유저"
     followersLessThanOrEq: "팔로워 수가 다음 이하인 유저"
@@ -1901,7 +1991,6 @@ _theme:
     buttonBg: "버튼 배경"
     buttonHoverBg: "버튼 배경 (호버)"
     inputBorder: "입력 필드 테두리"
-    listItemHoverBg: "리스트 항목 배경 (호버)"
     driveFolderBg: "드라이브 폴더 배경"
     wallpaperOverlay: "배경화면 오버레이"
     badge: "배지"
@@ -1913,16 +2002,15 @@ _sfx:
   note: "새 노트"
   noteMy: "내 노트"
   notification: "알림"
-  antenna: "안테나 수신"
-  channel: "채널 알림"
   reaction: "리액션 선택"
 _soundSettings:
   driveFile: "드라이브에 있는 오디오를 사용"
   driveFileWarn: "드라이브에 있는 파일을 선택하세요."
-  driveFileTypeWarn: "이 파일은 지원되지 않습니다."
+  driveFileTypeWarn: "이 파이"
   driveFileTypeWarnDescription: "오디오 파일을 선택하세요."
   driveFileDurationWarn: "오디오가 너무 깁니다"
   driveFileDurationWarnDescription: "긴 오디오로 설정할 경우 미스키 사용에 지장이 갈 수도 있습니다. 그래도 괜찮습니까?"
+  driveFileError: "오디오를 불러올 수 없습니다. 설정을 바꿔주세요."
 _ago:
   future: "미래"
   justNow: "방금 전"
@@ -1975,6 +2063,7 @@ _2fa:
   backupCodesDescription: "인증 앱을 사용할 수 없게 된 경우 아래 백업 코드를 사용하여 계정에 액세스 할 수 있습니다.이 코드들은 반드시 안전한 장소에 보관하십시오.각 코드는 한 번만 사용할 수 있습니다."
   backupCodeUsedWarning: "백업 코드가 사용되었습니다.인증 앱을 사용할 수 없게 된 경우, 조속히 인증 앱을 다시 설정해 주십시오."
   backupCodesExhaustedWarning: "백업 코드가 모두 사용되었습니다.인증 앱을 사용할 수 없는 경우 더 이상 계정에 액세스하는 것이 불가능합니다.인증 앱을 다시 등록해 주세요."
+  moreDetailedGuideHere: "여기에 자세한 설명이 있습니다"
 _permissions:
   "read:account": "계정의 정보를 봅니다"
   "write:account": "계정의 정보를 변경합니다"
@@ -2163,7 +2252,7 @@ _postForm:
     c: "무엇을 생각하고 있나요?"
     d: "말하고 싶은 게 있나요?"
     e: "여기에 적어 주세요"
-    f: "글 쓰기를 기다려요…"
+    f: "작성해주시길 기다리고 있어요..."
 _profile:
   name: "이름"
   username: "사용자 이름"
@@ -2178,6 +2267,9 @@ _profile:
   changeBanner: "배너 이미지 변경"
   verifiedLinkDescription: "내용에 자신의 프로필로 향하는 링크가 포함된 페이지의 URL을 삽입하면 소유자 인증 마크가 표시됩니다."
   avatarDecorationMax: "최대 {max}개까지 장식을 할 수 있습니다."
+  followedMessage: "팔로우 받았을 때 메시지"
+  followedMessageDescription: "팔로우 받았을 때 상대방에게 보여줄 단문 메시지를 설정할 수 있습니다."
+  followedMessageDescriptionForLockedAccount: "팔로우를 승인제로 한 경우, 팔로우 요청을 수락했을 때 보여줍니다."
 _exportOrImport:
   allNotes: "모든 노트"
   favoritedNotes: "즐겨찾기한 노트"
@@ -2270,6 +2362,7 @@ _pages:
   eyeCatchingImageSet: "아이캐치 이미지를 설정"
   eyeCatchingImageRemove: "아이캐치 이미지를 삭제"
   chooseBlock: "블록 추가"
+  enterSectionTitle: "섹션 타이틀을 입력하기"
   selectType: "종류 선택"
   contentBlocks: "콘텐츠"
   inputBlocks: "입력"
@@ -2315,6 +2408,8 @@ _notification:
   renotedBySomeUsers: "{n}명이 리노트했습니다"
   followedBySomeUsers: "{n}명에게 팔로우됨"
   flushNotification: "알림 이력을 초기화"
+  exportOfXCompleted: "{x} 추출에 성공했습니다."
+  login: "로그인 알림이 있습니다"
   _types:
     all: "전부"
     note: "사용자의 새 글"
@@ -2329,6 +2424,9 @@ _notification:
     followRequestAccepted: "팔로우 요청이 승인되었을 때"
     roleAssigned: "역할이 부여 됨"
     achievementEarned: "도전 과제 획득"
+    exportCompleted: "추출을 성공함"
+    login: "로그인"
+    test: "알림 테스트"
     app: "연동된 앱을 통한 알림"
   _actions:
     followBack: "팔로우"
@@ -2338,6 +2436,7 @@ _deck:
   alwaysShowMainColumn: "메인 칼럼 항상 표시"
   columnAlign: "칼럼 정렬"
   addColumn: "칼럼 추가"
+  newNoteNotificationSettings: "새 노트 알림 설정"
   configureColumn: "칼럼 설정"
   swapLeft: "왼쪽으로 이동"
   swapRight: "오른쪽으로 이동"
@@ -2376,9 +2475,10 @@ _drivecleaner:
   orderByCreatedAtAsc: "등록일이 오래된 순"
 _webhookSettings:
   createWebhook: "Webhook 생성"
+  modifyWebhook: "Webhook 수정"
   name: "이름"
   secret: "시크릿"
-  events: "Webhook을 실행할 타이밍"
+  trigger: "트리거"
   active: "활성화"
   _events:
     follow: "누군가를 팔로우했을 때"
@@ -2388,6 +2488,29 @@ _webhookSettings:
     renote: "누군가 내 글을 리노트했을 때"
     reaction: "누군가 내 노트에 리액션했을 때"
     mention: "누군가 나를 멘션했을 때"
+  _systemEvents:
+    abuseReport: "유저롭"
+    abuseReportResolved: "받은 신고를 처리했을 때"
+    userCreated: "유저가 생성되었을 때"
+    inactiveModeratorsWarning: "모더레이터가 일정 기간동안 활동하지 않은 경우"
+    inactiveModeratorsInvitationOnlyChanged: "모더레이터가 일정 기간 활동하지 않아 시스템에 의해 초대제로 바뀐 경우"
+  deleteConfirm: "Webhook을 삭제할까요?"
+  testRemarks: "스위치 오른쪽에 있는 버튼을 클릭하여 더미 데이터를 사용한 테스트용 웹 훅을 보낼 수 있습니다."
+_abuseReport:
+  _notificationRecipient:
+    createRecipient: "신고 수신자 추가"
+    modifyRecipient: "신고 수신자 편집"
+    recipientType: "알림 종류"
+    _recipientType:
+      mail: "이메일"
+      webhook: "Webhook"
+      _captions:
+        mail: "모더레이터 권한을 가진 사용자의 이메일 주소에 알림을 보냅니다 (신고를 받은 때에만)"
+        webhook: "지정한 SystemWebhook에 알림을 보냅니다 (신고를 받은 때와 해결했을 때에 송신)"
+    keywords: "키워드"
+    notifiedUser: "알릴 사용자"
+    notifiedWebhook: "사용할 Webhook"
+    deleteConfirm: "수신자를 삭제하시겠습니까?"
 _moderationLogTypes:
   createRole: "역할 생성"
   deleteRole: "역할 삭제"
@@ -2403,7 +2526,7 @@ _moderationLogTypes:
   updateUserNote: "조정 기록 갱신"
   deleteDriveFile: "파일 삭제"
   deleteNote: "노트 삭제"
-  createGlobalAnnouncement: "모든 공지사항 만들기"
+  createGlobalAnnouncement: "전역 공지사항 생성"
   createUserAnnouncement: "사용자 공지사항 만들기"
   updateGlobalAnnouncement: "모든 공지사항 수정"
   updateUserAnnouncement: "사용자 공지사항 수정"
@@ -2416,6 +2539,8 @@ _moderationLogTypes:
   markSensitiveDriveFile: "파일에 열람주의를 설정"
   unmarkSensitiveDriveFile: "파일에 열람주의를 해제"
   resolveAbuseReport: "신고 처리"
+  forwardAbuseReport: "신고 전달"
+  updateAbuseReportNote: "신고 조정 노트 갱신"
   createInvitation: "초대 코드 생성"
   createAd: "광고 생성"
   deleteAd: "광고 삭제"
@@ -2425,6 +2550,16 @@ _moderationLogTypes:
   deleteAvatarDecoration: "아바타 장식 삭제"
   unsetUserAvatar: "유저 아바타 제거"
   unsetUserBanner: "유저 배너 제거"
+  createSystemWebhook: "SystemWebhook을 생성"
+  updateSystemWebhook: "SystemWebhook을 수정"
+  deleteSystemWebhook: "SystemWebhook을 삭제"
+  createAbuseReportNotificationRecipient: "신고 알림 수신자 생성"
+  updateAbuseReportNotificationRecipient: "신고 알림 수신자 편집"
+  deleteAbuseReportNotificationRecipient: "신고 알림 수신자 삭제"
+  deleteAccount: "계정을 삭제"
+  deletePage: "페이지를 삭제"
+  deleteFlash: "Play를 삭제"
+  deleteGalleryPost: "갤러리 포스트를 삭제"
 _fileViewer:
   title: "파일 상세"
   type: "파일 유형"
@@ -2545,7 +2680,7 @@ _urlPreviewSetting:
   timeoutDescription: "미리보기를 로딩하는데 걸리는 시간이 정한 시간보다 오래 걸리는 경우, 미리보기를 생성하지 않습니다."
   maximumContentLength: "Content-Length의 최대치 (byte)"
   maximumContentLengthDescription: "Content-Length가 이 값을 넘어서면 미리보기를 생성하지 않습니다."
-  requireContentLength: "Content-Length를 얻었을 때만 미리보기 만들기"
+  requireContentLength: "Content-Length를 받아온 경우에만 "
   requireContentLengthDescription: "상대 서버가 Content-Length를 되돌려주지 않는다면 미리보기를 만들지 않습니다."
   userAgent: "User-Agent"
   userAgentDescription: "미리보기를 얻을 때 사용한 User-Agent를 설정합니다. 비어 있다면 기본값의 User-Agent를 사용합니다."
@@ -2556,3 +2691,22 @@ _mediaControls:
   pip: "화면 속 화면"
   playbackRate: "재생 속도"
   loop: "반복 재생"
+_contextMenu:
+  title: "컨텍스트 메뉴"
+  app: "애플리케이션"
+  appWithShift: "Shift 키로 애플리케이션"
+  native: "브라우저의 UI"
+_embedCodeGen:
+  title: "임베디드 코드를 커스터마이즈"
+  header: "해더를 표시"
+  autoload: "자동으로 다음 코드를 실행 (비권장)"
+  maxHeight: "최대 높이"
+  maxHeightDescription: "최대 값을 무시하려면 0을 입력하세요. 위젯이 상하로 길어지는 것을 방지하려면, 임의의 값을 입력해 주세요."
+  maxHeightWarn: "높이 최대 값이 설정되어져 있지 않습니다(0). 의도적으로 설정 하지 않았다면 임의의 값을 설정해주세요."
+  previewIsNotActual: "미리보기로 표시할 수 있는 크기보다 큽니다. 실제로 넣은 코드의 표시가 다른 경우가 있습니다."
+  rounded: "외곽선을 둥글게 하기"
+  border: "외곽선에 테두리를 씌우기"
+  applyToPreview: "미리보기에 반영"
+  generateCode: "임베디드 코드를 만들기"
+  codeGenerated: "코드를 만들었습니다."
+  codeGeneratedDescription: "만들어진 코드를 웹 사이트에 붙여서 사용하세요."
diff --git a/locales/lo-LA.yml b/locales/lo-LA.yml
index 087bac3745..b100d0300f 100644
--- a/locales/lo-LA.yml
+++ b/locales/lo-LA.yml
@@ -18,15 +18,15 @@ enterUsername: "ປ້ອນຊື່ຜູ້ໃຊ້"
 renotedBy: "Renoted ໂດຍ {user}"
 noNotes: "ບໍ່ມີ note"
 noNotifications: "ບໍ່ມີການແຈ້ງເຕືອນ"
-instance: "ອີນສະແຕນ"
-settings: "ກຳນົດຄ່າ"
+instance: "ເຊີຟເວີຣ໌"
+settings: "ຕັ້ງຄ່າ"
 notificationSettings: "ຕັ້ງຄ່າການແຈ້ງເຕືອນ"
 basicSettings: "ການຕັ້ງຄ່າພື້ນຖານ"
 otherSettings: "ການຕັ້ງຄ່າອື່ນໆ"
-openInWindow: "ເປີດໃນປ່ອງຢ້ຽມ"
-profile: "ໂພຼຟາຍ"
+openInWindow: "ເປີດໃນ window"
+profile: "ໂປຣໄຟລ໌"
 timeline: "ໄທມ໌ໄລນ໌"
-noAccountDescription: "ຜູ້ໃຊ້ນີ້ຍັງບໍ່ໄດ້ຂຽນໃນຊີວະປະຫວັດຂອງເຂົາເຈົ້າເທື່ອ"
+noAccountDescription: "ຜູ້ໃຊ້ຄົນນີ້ຍັງບໍ່ໄດ້ຂຽນຄຳແນະນຳໂຕ"
 login: "ເຂົ້າ​ສູ່​ລະ​ບົບ"
 loggingIn: "ກຳລັງເຂົ້າສູ່ລະບົບ..."
 logout: "ອອກ​ຈາກ​ລະ​ບົບ"
@@ -37,7 +37,7 @@ users: "ຜູ້ໃຊ້"
 addUser: "ເພີ່ມຜູ້ໃຊ້"
 favorite: "ເພີ່ມໃສ່ລາຍການທີ່ມັກ"
 favorites: "ລາຍການທີ່ມັກ"
-unfavorite: "ລຶບອອກຈາກລາຍການທີ່ມັກ"
+unfavorite: "ເອົາອອກຈາກລາຍການທີ່ມັກ"
 favorited: "ເພີ່ມໃສ່ລາຍການທີ່ມັກແລ້ວ"
 alreadyFavorited: "ເພີ່ມເຂົ້າໃນລາຍການທີ່ມັກແລ້ວ."
 cantFavorite: "ບໍ່ສາມາດເພີ່ມໃສ່ລາຍການທີ່ມັກໄດ້."
@@ -48,41 +48,41 @@ copyLink: "ຄັດລອກລິ້ງ"
 copyLinkRenote: "ຄັດລອກລິ້ງຂອງ renote"
 delete: "ລຶບ"
 deleteAndEdit: "ລຶບ​ແລະ​ແກ້​ໄຂ​"
-deleteAndEditConfirm: "ເຈົ້າ​ແນ່​ໃຈ​ບໍ່? ທີ່ທ່ານຕ້ອງການທີ່ຈະລຶບ note ນີ້ ແລະແກ້ໄຂມັນ ທ່ານອາດຈະສູນເສຍ reaction, renote, ແລະການຕອບກັບທັງໝົດ"
+deleteAndEditConfirm: "ຕ້ອງການລຶບ note ນີ້ແລະແກ້ໄຂໃໝ່ແມ່ນບໍ່? reaction, renote ແລະການຕອບກັບຕໍ່ note ນີ້ ທັງເບິດຈະຖືກລຶບອອກ"
 addToList: "ເພີ່ມໃສ່ລາຍຊື່"
 addToAntenna: "ເພີ່ມໃສ່ເສົາອາກາດ"
 sendMessage: "ສົ່ງຂໍ້ຄວາມ"
-copyRSS: "ສຳເນົາ RSS"
-copyUsername: "ສຳເນົາຊື່ຜູ້ໃຊ້"
-copyUserId: "ສຳເນົາ ID ຜູ້ໃຊ້"
-copyNoteId: "ສຳເນົາ ID ບັນທຶກ"
-copyFileId: "ສຳເນົາ ID ໄຟລ໌"
-copyFolderId: "ສຳເນົາ ID ໂຟນເດີ"
-copyProfileUrl: "ສຳເນົາ URL ໂປຣໄຟລ໌"
+copyRSS: "ຄັດລອກ RSS"
+copyUsername: "ຄັດລອກຊື່ຜູ້ໃຊ້"
+copyUserId: "ຄັດລອກ ID ຜູ້ໃຊ້"
+copyNoteId: "ຄັດລອກ ID ຂອງ note"
+copyFileId: "ຄັດລອກ ID ໄຟລ໌"
+copyFolderId: "ຄັດລອກ ID ໂຟລ໌ເດີຣ໌"
+copyProfileUrl: "ຄັດລອກ URL ໂປຣໄຟລ໌"
 searchUser: "ຄົ້ນຫາຜູ້ໃຊ້"
-reply: "ຕອບ​ໄປ​ທີ"
+reply: "ຕອບ​ກັບ"
 loadMore: "ໂຫຼດເພີ່ມເຕີມ"
 showMore: "ໂຫຼດເພີ່ມເຕີມ"
 showLess: "ປິດ"
-youGotNewFollower: "ໄດ້ຕິດຕາມທ່ານ"
-receiveFollowRequest: "ປະຕິບັດຕາມຄໍາຮ້ອງຂໍທີ່ໄດ້ຮັບ"
-followRequestAccepted: "ຜູ້ຕິດຕາມໄດ້ຍອມຮັບຄໍາຮ້ອງຂໍຂອງທ່ານ"
-mention: "ກ່າວຖືງ"
-mentions: "ກ່າວເຖິງ"
+youGotNewFollower: "ໄດ້ຕິດຕາມເຈົ້າ"
+receiveFollowRequest: "ມີຄຳຂໍຕິດຕາມສົ່ງມາ"
+followRequestAccepted: "ການຕິດຕາມໄດ້ຮັບອນຸຍາດແລ້ວ"
+mention: "ເວົ້າເຖີງ"
+mentions: "ເວົ້າເຖີງເຈົ້າ"
 directNotes: "ໂພສ Direct note"
 importAndExport: "ນໍາເຂົ້າ / ສົ່ງອອກ"
 import: "ນຳເຂົ້າ"
 export: "ສົ່ງອອກ"
 files: "ໄຟລ໌"
 download: "ດາວໂຫລດ"
-driveFileDeleteConfirm: "ທ່ານແນ່ໃຈບໍ່ວ່າຕ້ອງການລຶບໄຟລ໌ \"{name}\"? note ທີ່ມີໄຟລ໌ແນບນີ້ຈະຖືກລຶບຖິ້ມ"
-unfollowConfirm: "ທ່ານແນ່ໃຈບໍ່ວ່າຕ້ອງການເຊົາຕິດຕາມ {name}?"
-exportRequested: "ໃນເວລາທີ່ທ່ານໄດ້ຮ້ອງຂໍການສົ່ງອອກ ມັນອາດຈະໃຊ້ເວລາບາງເວລາ ແລະມັນຈະຖືກເພີ່ມໃສ່ drive ຂອງທ່ານເມື່ອມັນສຳເລັດແລ້ວ"
-importRequested: "ໃນເວລາທີ່ທ່ານໄດ້ຮ້ອງຂໍການນໍາເຂົ້າ ມັນອາດຈະໃຊ້ເວລາບາງເວລາ"
+driveFileDeleteConfirm: "ຕ້ອງການລຶບໄຟລ໌ “{name}” ແມ່ນບໍ່? Note ທີ່ແນບມາກັບໄຟລ໌ນີ້ຈະຖືກລຶບອອກ"
+unfollowConfirm: "ຕ້ອງການເລີກຕິດຕາມ {name} ແມ່ນບໍ່?"
+exportRequested: "ເຈົ້າໄດ້ຮ້ອງຂໍການສົ່ງອອກ ອາດໃຊ້ເວລາຈັກໜ່ອຍ ເມື່ອແລ້ວຈະຖືກເພີ່ມໃສ່ drive"
+importRequested: "ເຈົ້າໄດ້ຮ້ອງຂໍການນຳເຂົ້າ ການດຳເນິນການນີ້ອາດໃຊ້ເວລາຈັກໜ່ອຍ"
 lists: "ລາຍການ"
-noLists: "ທ່ານ​ບໍ່​ມີ​ລາຍ​ການ​ໃດໆ​"
-note: "ບັນທຶກ"
-notes: "ບັນທຶກ"
+noLists: "ບໍ່​ມີ​ລາຍ​ການ​ໃດໆ​"
+note: "Note"
+notes: "Note"
 following: "ກຳລັງຕິດຕາມ"
 followers: "ຜູ້ຕິດຕາມ"
 followsYou: "ຕິດ​ຕາມ​ເຈົ້າ"
@@ -124,11 +124,11 @@ reactions: "reaction"
 attachCancel: "ເອົາໄຟລ໌ແນບ"
 mute: "ປີດສຽງ"
 unmute: "ເປີດສຽງ"
-block: "ບ໋ອກ"
-unblock: "ຍົກເລີກກາຮົບລັອກ"
+block: "ບລັອກ"
+unblock: "ເລີກບລັອກ"
 suspend: "ລະງັບ"
 unsuspend: "ເຊົາ​ລະ​ງັບ"
-selectList: "ເລືອກບັນຊີລາຍການ"
+selectList: "ເລືອກລາຍຊື່"
 editList: "ແກ້ໄຂລາຍຊື່"
 selectChannel: "ເລືອກຊ່ອງ"
 selectAntenna: "ເລືອກເສົາອາກາດ"
@@ -151,30 +151,30 @@ flagShowTimelineRepliesDescription: "ສະແດງການຕອບກັບ
 autoAcceptFollowed: "ອະນຸມັດອັດຕະໂນມັດຕາມຄຳຮ້ອງຂໍຈາກຜູ້ໃຊ້ທີ່ທ່ານກຳລັງຕິດຕາມຢູ່"
 addAccount: "ເພີ່ມບັນຊີ"
 loginFailed: "ການເຂົ້າສູ່ລະບົບບໍ່ສຳເລັດ"
-showOnRemote: "ເບິ່ງຢູ່ໃນຕົວຢ່າງໄລຍະໄກ"
+showOnRemote: "ເບິ່ງໃນເຊີຟເວີຣ໌ໄລຍະໄກ"
 general: "ທົ່ວໄປ"
 wallpaper: "ພາບພື້ນຫລັງ"
 setWallpaper: "ຕັ້ງເປັນພາບພື້ນຫຼັງ"
 removeWallpaper: "ລຶບຮູບວໍເປເປີອອກ"
 searchWith: "ຊອກຫາ: {q}"
-youHaveNoLists: "ທ່ານ​ບໍ່​ມີ​ລາຍ​ການ​ໃດໆ​"
+youHaveNoLists: "ເຈົ້າບໍ່ມີລາຍຊື່ໃດໆ"
 proxyAccount: "ບັນຊີພຣັອກຊີ"
-host: "ໂຮດສ"
+host: "ໂຮສຕ໌"
 selectUser: "ເລືອກຜູ້ໃຊ້"
 recipient: "ເຖິງ"
 annotation: "ຄຳເຫັນ"
 federation: "ສະຫະພັນ"
-instances: "ອີນສະແຕນ"
+instances: "ເຊີຟເວີຣ໌"
 registeredAt: "ລົງທະບຽນຢູ່"
 storageUsage: "ບ່ອນ​ຈັດ​ເກັບ​ຂໍ້​ມູນທີ່ໃຊ້"
-charts: "ອັນດັບເພງ"
+charts: "ແຜນພູມ"
 perHour: "ຕໍ່ຊົ່ວໂມງ"
 perDay: "ຕໍ່​ມື້"
 stopActivityDelivery: "ຢຸດເຊົາການສົ່ງກິດຈະກໍາ"
 blockThisInstance: "ຂັດຂວາງຕົວຢ່າງນີ້"
 operations: "ການດຳເນີນງານ"
 software: "ຊອບແວ"
-version: "ສະບັບ"
+version: "ເວີຣ໌ຊັນ"
 metadata: "Metadata"
 withNFiles: "{n} ໄຟລ໌(s)"
 monitor: "ຈໍພາບ"
@@ -199,15 +199,15 @@ federating: "ສະຫະພັນ"
 blocked: "ບລັອກແລ້ວ "
 suspended: "ໂຈະ"
 all: "ທັງໝົດ"
-subscribing: "ສະໝັກສະມາຊິກແລັວ"
-publishing: "ການ​ພິມ​ເຜີຍ​ແຜ່"
+subscribing: "ກຳລັງສະມັກສະມາຊິກ"
+publishing: "ກຳລັງ​ເຜີຍ​ແພ່"
 notResponding: "ບໍ່ຕອບສະໜອງ"
-instanceFollowing: "ກຳລັງຕິດຕາມສຸດຕົວຢ່າງ"
-instanceFollowers: "ຜູ້ຕິດຕາມຕົວຢ່າງ"
-instanceUsers: "ຜູ້​ຊົມ​ໃຊ້​ຂອງ​ຕົວ​ຢ່າງ​ນີ້​"
+instanceFollowing: "ກຳລັງຕິດຕາມບົນເຊີຟເວີຣ໌"
+instanceFollowers: "ຜູ້ຕິດຕາມຂອງເຊີຟເວີຣ໌"
+instanceUsers: "ຜູ້​ໃຊ້​ຂອງ​ເຊີຟເວີຣ໌ນີ້"
 changePassword: "ປ່ຽນ​ລະ​ຫັດ​ຜ່ານ"
 security: "ຄວາມປອດໄພ"
-retypedNotMatch: "ວັດສະດຸປ້ອນບໍ່ກົງກັນ"
+retypedNotMatch: "ປ້ອນຂໍ້ມູນບໍ່ກົງກັນ"
 currentPassword: "ລະຫັດຜ່ານປະຈຸບັນ"
 newPassword: "ລະຫັດຜ່ານໃໝ່"
 newPasswordRetype: "ໃສ່ລະຫັດຜ່ານໃໝ່ອີກເທື່ອໜຶ່ງ"
@@ -223,14 +223,14 @@ remove: "ລຶບ"
 removed: "ລຶບແລ້ວ"
 resetAreYouSure: "ຣີ​ເຊັດບໍ?"
 saved: "ບັນທຶກແລ້ວ"
-messaging: "ແຊ໋ດ"
+messaging: "ແຊັຕ"
 upload: "ອັບໂຫຼດ"
 keepOriginalUploading: "ຮັກສາຮູບພາບຕົ້ນສະບັບ"
 fromDrive: "ຈາກ Drive"
 fromUrl: "ຈາກ URL"
 uploadFromUrl: "ອັບໂຫຼດຈາກ URL"
 uploadFromUrlDescription: "URL ຂອງໄຟລ໌ທີ່ທ່ານຕ້ອງການອັບໂຫລດ"
-uploadFromUrlRequested: "ຮ້ອງຂໍການອັບໂຫລດ"
+uploadFromUrlRequested: "ຮ້ອງຂໍການອັບໂຫລດແລ້ວ"
 explore: "ສຳຫຼວດ"
 messageRead: "ອ່ານແລ້ວ"
 startMessaging: "ເລີ່ມການສົນທະນາໃໝ່"
@@ -244,47 +244,47 @@ images: "ຮູບພາບ"
 image: "ຮູບພາບ"
 birthday: "ວັນເກີດ"
 yearsOld: "{age} ປີ"
-registeredDate: "ວັນທີ່ເປັນສະມາຊິກ"
+registeredDate: "ວັນທີ່ລົງທະບຽນ"
 location: "ທີ່ຕັ້ງ"
-theme: "ແທ໋ມ"
-themeForLightMode: "ຮູບແບບສີສັນເພື່ອໃຊ້ໃນໂໝດແສງ"
-themeForDarkMode: "ຮູບແບບສີສັນທີ່ຈະໃຊ້ຢູ່ໃນໂໝດມືດ"
+theme: "Theme"
+themeForLightMode: "Theme ໃຊ້ໃນໂໝດສະຫວ່າງ"
+themeForDarkMode: "Theme ໃຊ້ໃນໂໝດມືດ"
 light: "ສະຫວ່າງ"
 dark: "ມືດ"
 lightThemes: "ຊຸດຮູບແບບສະຫວ່າງ"
 darkThemes: "ຮູບແບບສີສັນມືດ"
 syncDeviceDarkMode: "ຊິງຄ໌ໂໝດມືດກັບການຕັ້ງຄ່າທົ່ວອຸປະກອນ"
-drive: "ຂັບ"
+drive: "Drive"
 fileName: "ຊື່ໄຟລ໌"
 selectFile: "ເລືອກໄຟລ໌"
 selectFiles: "ເລືອກໄຟລ໌"
 selectFolder: "ເລືອກໂຟລເດີ"
 selectFolders: "ເລືອກໂຟລເດີ"
 renameFile: "ປ່ຽນຊື່ໄຟລ໌"
-folderName: "ຊື່ໂຟນເດີ"
+folderName: "ຊື່ໂຟລເດີຣ໌"
 createFolder: "​ສ້າງ​ໂຟ​ລ​ເດີ"
 renameFolder: "ປ່ຽນຊື່ໂຟນເດີນີ້"
 deleteFolder: "ລົບໂຟ​ລ​ເດີ​"
 addFile: "ເພີ່ມໄຟລ໌"
 emptyDrive: "Drive ຂອງທ່ານຫວ່າງເປົ່າ"
-emptyFolder: "ໂຟນເດີນີ້ເປົ່າຫວ່າງ"
+emptyFolder: "ໂຟລເດີຣ໌ນີ້ວ່າງເປົ່າ"
 unableToDelete: "ບໍ່​ສາ​ມາດລົບໄດ້"
 inputNewFileName: "ໃສ່ຊື່ໄຟລ໌ໃໝ່"
 inputNewDescription: "ໃສ່ຄຳບັນຍາຍໃໝ່"
 inputNewFolderName: "ໃສ່ຊື່ໂຟນເດີໃໝ່"
 circularReferenceFolder: "ໂຟນເດີປາຍທາງແມ່ນໂຟນເດີຍ່ອຍຂອງໂຟນເດີທີ່ທ່ານຕ້ອງການຍ້າຍ"
 rename: "ປ່ຽນຊື່"
-doNothing: "ບໍ່ສົນໃຈ"
-watch: "ເບິ່ງ"
-unwatch: "ຢຸດເບິ່ງ"
+doNothing: "ຢ່າມັນ"
+watch: "ເພັ່ງເລັງ"
+unwatch: "ຢຸດເພັ່ງເລັງ"
 accept: "ອະນຸຍາດ"
 reject: "ປະຕິເສດ"
 normal: "ປົກກະຕິ"
 instanceName: "ຊື່ເຊີເວີ້"
-instanceDescription: "ຄໍາອະທິບາຍຕົວຢ່າງ"
+instanceDescription: "ຄຳອະທິບາຍແນະນຳເຊີຟເວີຣ໌"
 maintainerName: "ຜູ້ດູແລ"
-maintainerEmail: "ອີເມວ admin"
-tosUrl: "ເງື່ອນໄຂການໃຫ້ບໍລິການ URL"
+maintainerEmail: "ອີເມລຜູ້ດູແລ"
+tosUrl: " URL ເງື່ອນໄຂການໃຫ້ບໍລິການ"
 thisYear: "ປີນີ້"
 thisMonth: "ເດືອນນີ້"
 today: "ມື້ນີ້"
@@ -292,34 +292,34 @@ dayX: "ວັນ {day}"
 monthX: "ເດືອນ {month}"
 yearX: "ປີ {year}"
 pages: "ໜ້າ"
-integration: "ຄວາມສຳພັນຂອງ"
+integration: "ເຊື່ອມໂຍງ"
 connectService: "ເຊື່ອມຕໍ່"
 disconnectService: "ຕັດການເຊື່ອມຕໍ່"
 enableLocalTimeline: "ເປີດໃຊ້ທາມລາຍທ້ອງຖິ່ນ"
 enableGlobalTimeline: "ເປີດໃຊ້ທາມລາຍທົ່ວໂລກ"
-disablingTimelinesInfo: "ຜູ້ເບິ່ງແຍງລະບົບ ແລະຜູ້ຄວບຄຸມຈະມີການເຂົ້າເຖິງທຸກກຳນົດເວລາ, ເຖິງແມ່ນວ່າຈະບໍ່ໄດ້ເປີດໃຊ້ງານກໍຕາມ"
+disablingTimelinesInfo: "ຜູ້ດູແລລະບບແລະຜູ້ຄວບຄຸມຈະສາມາດເຂົ້າເຖີງໄທມ໌ໄລນ໌ທັ້ງເບີດ ເຖີງວ່າຈະບໍ່ໄດ້ເປີດໃຊ້ງານກໍ່ຕາມ"
 registration: "ລົງທະບຽນ"
 enableRegistration: "ເປີດໃຊ້ການລົງທະບຽນຜູ້ໃຊ້ໃໝ່"
 invite: "ເຊີນ"
-driveCapacityPerLocalAccount: "ຄວາມອາດສາມາດຂັບຕໍ່ຜູ້ໃຊ້ທ້ອງຖິ່ນ"
-driveCapacityPerRemoteAccount: "ໄດຣຟ໌ຄວາມອາດສາມາດຕໍ່ຜູ້ໃຊ້ທາງໄກ"
+driveCapacityPerLocalAccount: "ຄວາມຈຸຂອງ drive ຕໍ່ຜູ້ໃຊ້ທ້ອງຖິ່ນ"
+driveCapacityPerRemoteAccount: "ຄວາມຈຸຂອງ drive ຕໍ່ຜູ້ໃຊ້ໄລຍະໄກ"
 basicInfo: "ຂໍ້ມຸນເບື້ອງຕົ້ນ"
-pinnedNotes: "ບັນທຶກທີ່ປັກໝຸດໄວ້"
-hcaptchaSiteKey: "ກະແຈໄຊທ໌"
-hcaptchaSecretKey: "ກະແຈລັບ"
-mcaptchaSiteKey: "ກະແຈໄຊທ໌"
-mcaptchaSecretKey: "ກະແຈລັບ"
+pinnedNotes: "Note ທີ່ປັກໝຸດໄວ້"
+hcaptchaSiteKey: "Site key"
+hcaptchaSecretKey: "Secret key"
+mcaptchaSiteKey: "Site key"
+mcaptchaSecretKey: "Secret Key"
 recaptcha: "reCAPTCHA"
-enableRecaptcha: "ເປີດໃຊ້ງານລີແຄ໋ບຈາ"
-recaptchaSiteKey: "ກະແຈໄຊທ໌"
-recaptchaSecretKey: "ກະແຈລັບ"
-turnstileSiteKey: "ກະແຈໄຊທ໌"
-turnstileSecretKey: "ກະແຈລັບ"
+enableRecaptcha: "ເປີດໃຊ້ງານ reCAPTCHA"
+recaptchaSiteKey: "Site key"
+recaptchaSecretKey: "Secret key"
+turnstileSiteKey: "Site key"
+turnstileSecretKey: "Secret key"
 name: "ຊື່"
 userList: "ລາຍການ"
 about: "ກ່ຽວກັບ"
 aboutMisskey: "ກ່ຽວກັບ Misskey"
-administrator: "ຜູ້ບໍລິຫານ"
+administrator: "ຜູ້ດູແລ"
 token: "ໂທເຄັນ"
 share: "ແບ່ງປັນ"
 notFound: "ບໍ່ພົບ"
@@ -332,27 +332,27 @@ title: "ຫົວຂໍ້"
 text: "ຂໍ້ຄວາມ"
 enable: "ເປີດໃຊ້"
 next: "ຕໍ່ໄປ"
-retype: "ເຂົ້າໄປອີກຄັ້ງ"
-quoteAttached: "ວົງຢືມ"
+retype: "ລອງພິມລະຫັດອີກເທື່ອໜຶ່ງ"
+quoteAttached: "ອ້າງອິງ"
 invitations: "ເຊີນ"
 unavailable: "ບໍ່​ສາ​ມາດ​ໃຊ້​ໄດ້"
 language: "ພາສາ"
 aboutX: "ກ່ຽວກັບ {x}"
 emojiStyle: "ຮູບແບບອີໂມຈິ"
 native: "ພາ​ສາ​ແມ່"
-noHistory: "​ບໍ່​ມີ​ລາຍ​ການ​ຢູ່​ບ່ອນ​ນີ້"
+noHistory: "​ບໍ່​ມີປະຫວັດ"
 doing: "ກຳລັງປະມວນຜົນ..."
 category: "ຫມວດຫມູ່"
-tags: "ແທ໋ກ"
+tags: "Aliases"
 createAccount: "ສ້າງບັນຊີ"
-existingAccount: "ທີ່ມີຢູ່"
-dashboard: "ໜ້າປັດ"
+existingAccount: "ບັນຊີທີ່ມີຢູ່ແລ້ວ"
+dashboard: "Dashboard"
 local: "ທ້ອງຖິ່ນ"
 numberOfDays: "ຈຳນວນມື້"
 objectStorageBucket: "Bucket"
 objectStoragePrefix: "Prefix"
 objectStorageEndpoint: "Endpoint"
-objectStorageRegion: "ພາກ​ພື້ນ"
+objectStorageRegion: "ພູມິພາກ"
 deleteAll: "ລຶບທັງໝົດ"
 sounds: "ສຽງ"
 sound: "ສຽງ"
@@ -365,11 +365,11 @@ state: "ສະຖານະ"
 sort: "ຈັດຮຽງໂດຍ"
 ascendingOrder: "ນ້ອຍໄປຫາໃຫຍ່"
 descendingOrder: "ໃຫຍ່ຫານ້ອຍ"
-output: "ຜົນຜະລິດ"
-script: "ບົດ​ຄວາມ"
+output: "Output"
+script: "Script"
 menu: "ເມນູ"
-rearrange: "ຈັດລຽງຄືນ"
-poll: "ການພູນ"
+rearrange: "ຈັດລຽງໃໝ່"
+poll: "Poll"
 description: "ລາຍລະອຽດ"
 author: "ຜູ້ຂຽນ"
 manage: "ການຈັດການ"
@@ -383,7 +383,7 @@ permission: "ການອະນຸຍາດ"
 notificationType: "​ປະເພດການ​ແຈ້ງ​ເຕືອນ"
 edit: "ແກ້ໄຂ"
 email: "ອີເມວ"
-smtpHost: "ໂຮດສ"
+smtpHost: "ໂຮສຕ໌"
 smtpUser: "ຊື່ຜູ້ໃຊ້"
 smtpPass: "ລະຫັດຜ່ານ"
 clearCache: "ລຶບລ້າງແຄສ"
@@ -393,12 +393,12 @@ administration: "ການຈັດການ"
 middle: "ປານກາງ"
 searchByGoogle: "ຄົ້ນຫາ"
 file: "ໄຟລ໌"
-replies: "ຕອບ​ໄປ​ທີ"
+replies: "ຕອບ​ກັບ"
 renotes: "Renote"
 _delivery:
   stop: "ໂຈະ"
   _type:
-    none: "ການ​ພິມ​ເຜີຍ​ແຜ່"
+    none: "ກຳລັງ​ເຜີຍ​ແພ່"
 _role:
   _priority:
     middle: "ປານກາງ"
@@ -416,8 +416,8 @@ _sfx:
 _2fa:
   renewTOTPCancel: "ບໍ່​ແມ່ນ​ຕອນ​ນີ້"
 _widgets:
-  profile: "ໂພຼຟາຍ"
-  instanceInfo: "ອີນສະແຕນ"
+  profile: "ໂປຣໄຟລ໌"
+  instanceInfo: "ຂໍ້ມູລເຊີຟເວີຣ໌"
   notifications: "ການແຈ້ງເຕືອນ"
   timeline: "​ເສັ້ນກຳ​ນົດ​ເວ​ລາ​"
   activity: "ກິດຈະກຳ"
@@ -436,28 +436,29 @@ _profile:
 _exportOrImport:
   followingList: "ກຳລັງຕິດຕາມ"
   muteList: "ປີດສຽງ"
-  blockingList: "ບ໋ອກ"
+  blockingList: "ບລັອກ"
   userLists: "ລາຍການ"
 _charts:
   federation: "ສະຫະພັນ"
 _timelines:
   home: "ໜ້າຫຼັກ"
 _play:
-  script: "ບົດ​ຄວາມ"
+  script: "Script"
   summary: "ລາຍລະອຽດ"
 _pages:
   blocks:
     image: "ຮູບພາບ"
 _notification:
-  youWereFollowed: "ໄດ້ຕິດຕາມທ່ານ"
+  youWereFollowed: "ໄດ້ຕິດຕາມເຈົ້າ"
   _types:
     follow: "ກຳລັງຕິດຕາມ"
-    mention: "ໄດ້ກ່າວມາ"
+    mention: "ໄດ້ກ່າວເຖິງ"
     renote: "Renote"
-    quote: "ລວມຂໍ້ຄວາມອ້າງອີງ"
-    reaction: "ປະຕິກິລິຍາ"
+    quote: "ອ້າງອີງ"
+    reaction: "Reaction"
+    login: "ເຂົ້າ​ສູ່​ລະ​ບົບ"
   _actions:
-    reply: "ຕອບ​ໄປ​ທີ"
+    reply: "ຕອບ​ກັບ"
     renote: "Renote"
 _deck:
   _columns:
@@ -465,8 +466,12 @@ _deck:
     tl: "​ເສັ້ນກຳ​ນົດ​ເວ​ລາ​"
     list: "ລາຍການ"
     channel: "ຊ່ອງ"
-    mentions: "ກ່າວເຖິງ"
+    mentions: "ກ່າວເຖິງເຈົ້າ"
 _webhookSettings:
   name: "ຊື່"
+_abuseReport:
+  _notificationRecipient:
+    _recipientType:
+      mail: "ອີເມວ"
 _moderationLogTypes:
   suspend: "ລະງັບ"
diff --git a/locales/nl-NL.yml b/locales/nl-NL.yml
index eb48cf72da..dde3035357 100644
--- a/locales/nl-NL.yml
+++ b/locales/nl-NL.yml
@@ -486,6 +486,7 @@ _notification:
     renote: "Herdelen"
     quote: "Quote"
     reaction: "Reacties"
+    login: "Inloggen"
   _actions:
     reply: "Antwoord"
     renote: "Herdelen"
diff --git a/locales/no-NO.yml b/locales/no-NO.yml
index 2b4c9b7776..c5f61db745 100644
--- a/locales/no-NO.yml
+++ b/locales/no-NO.yml
@@ -701,6 +701,7 @@ _notification:
     renote: "Renotes"
     quote: "Sitater"
     reaction: "Reaksjoner"
+    login: "Logg inn"
   _actions:
     reply: "Svar"
     renote: "Renote"
@@ -721,5 +722,9 @@ _deck:
     direct: "Direkte"
 _webhookSettings:
   name: "Navn"
+_abuseReport:
+  _notificationRecipient:
+    _recipientType:
+      mail: "E-post"
 _moderationLogTypes:
   suspend: "Suspender"
diff --git a/locales/pl-PL.yml b/locales/pl-PL.yml
index 9d75f7a9d7..d7afd57760 100644
--- a/locales/pl-PL.yml
+++ b/locales/pl-PL.yml
@@ -492,7 +492,6 @@ uiLanguage: "Język wyświetlania UI"
 aboutX: "O {x}"
 emojiStyle: "Styl emoji"
 native: "Natywny"
-disableDrawer: "Nie używaj menu w stylu szuflady"
 showNoteActionsOnlyHover: "Pokazuj akcje notatek tylko po najechaniu myszką"
 showReactionsCount: "Wyświetl liczbę reakcji na notatkę"
 noHistory: "Brak historii"
@@ -690,10 +689,7 @@ abuseReported: "Twoje zgłoszenie zostało wysłane. Dziękujemy."
 reporter: "Zgłaszający"
 reporteeOrigin: "Pochodzenie zgłoszonego"
 reporterOrigin: "Pochodzenie zgłaszającego"
-forwardReport: "Przekaż zgłoszenie do innej instancji"
-forwardReportIsAnonymous: "Zamiast twojego konta, anonimowe konto systemowe będzie wyświetlone jako zgłaszający na instancji zdalnej."
 send: "Wyślij"
-abuseMarkAsResolved: "Oznacz zgłoszenie jako rozwiązane"
 openInNewTab: "Otwórz w nowej karcie"
 openInSideView: "Otwórz w bocznym widoku"
 defaultNavigationBehaviour: "Domyślne zachowanie nawigacji"
@@ -1209,7 +1205,6 @@ _theme:
     buttonBg: "Tło przycisku"
     buttonHoverBg: "Tło przycisku (po najechaniu)"
     inputBorder: "Obramowanie pola wejścia"
-    listItemHoverBg: "Tło elementu listy (po najechaniu)"
     driveFolderBg: "Tło folderu na dysku"
     wallpaperOverlay: "Nakładka tapety"
     badge: "Odznaka"
@@ -1221,8 +1216,6 @@ _sfx:
   note: "Wpisy"
   noteMy: "Mój wpis"
   notification: "Powiadomienia"
-  antenna: "Anteny"
-  channel: "Powiadomienia kanału"
 _ago:
   future: "W przyszłości"
   justNow: "Przed chwilą"
@@ -1512,6 +1505,7 @@ _notification:
     reaction: "Reakcja"
     receiveFollowRequest: "Otrzymano prośbę o możliwość obserwacji"
     followRequestAccepted: "Przyjęto prośbę o możliwość obserwacji"
+    login: "Zaloguj się"
     app: "Powiadomienia z aplikacji"
   _actions:
     followBack: "zaobserwował cię z powrotem"
@@ -1546,7 +1540,6 @@ _webhookSettings:
   createWebhook: "Stwórz Webhook"
   name: "Nazwa"
   secret: "Sekret"
-  events: "Uruchomienie Webhooka"
   active: "Właczono"
   _events:
     follow: "Po zaobserwowaniu użytkownika"
@@ -1556,6 +1549,10 @@ _webhookSettings:
     renote: "Po udostępnieniu wpisu"
     reaction: "Po otrzymaniu reakcji"
     mention: "Po zostaniu wspomnianym"
+_abuseReport:
+  _notificationRecipient:
+    _recipientType:
+      mail: "Adres e-mail"
 _moderationLogTypes:
   suspend: "Zawieś"
   resetPassword: "Zresetuj hasło"
diff --git a/locales/pt-PT.yml b/locales/pt-PT.yml
index cfc576b6e1..9039fd2141 100644
--- a/locales/pt-PT.yml
+++ b/locales/pt-PT.yml
@@ -2,14 +2,14 @@
 _lang_: "Português"
 headlineMisskey: "Uma rede ligada por notas"
 introMisskey: "Bem-vindo! O Misskey é um serviço de microblog descentralizado de código aberto.\nCrie \"notas\" para compartilhar o que está acontecendo agora ou para se expressar com todos à sua volta 📡\nVocê também pode adicionar rapidamente reações às notas de outras pessoas usando a função \"Reações\" 👍\nVamos explorar um novo mundo 🚀"
-poweredByMisskeyDescription: "{name} é um dos servidores da plataforma de código aberto <b>Misskey</b>."
+poweredByMisskeyDescription: "{name} é uma instância da plataforma de código aberto <b>Misskey</b>."
 monthAndDay: "{day}/{month}"
 search: "Pesquisar"
 notifications: "Notificações"
 username: "Nome de usuário"
 password: "Senha"
 forgotPassword: "Esqueci-me da senha"
-fetchingAsApObject: "Buscando no Fediverso"
+fetchingAsApObject: "Buscando no Fediverso..."
 ok: "OK"
 gotIt: "Entendi"
 cancel: "Cancelar"
@@ -60,6 +60,7 @@ copyFileId: "Copiar o ID do arquivo"
 copyFolderId: "Copiar o ID da pasta"
 copyProfileUrl: "Copiar a URL do perfil"
 searchUser: "Pesquisar usuário"
+searchThisUsersNotes: "Pesquisar as notas desse usuário"
 reply: "Responder"
 loadMore: "Carregar mais"
 showMore: "Ver mais"
@@ -81,7 +82,7 @@ exportRequested: "A sua solicitação de exportação foi enviada. Isso pode lev
 importRequested: "A sua solicitação de importação foi enviada. Isso pode levar algum tempo."
 lists: "Listas"
 noLists: "Não possui nenhuma lista"
-note: "Post"
+note: "Publicar"
 notes: "Posts"
 following: "Seguindo"
 followers: "Seguidores"
@@ -99,7 +100,7 @@ enterListName: "Insira um nome para a lista"
 privacy: "Privacidade"
 makeFollowManuallyApprove: "Pedidos de seguidores precisam ser aprovados"
 defaultNoteVisibility: "Visibilidade padrão"
-follow: "Seguindo"
+follow: "Seguir"
 followRequest: "Enviar pedido de seguidor"
 followRequests: "Pedidos de seguidor"
 unfollow: "Deixar de seguir"
@@ -108,11 +109,14 @@ enterEmoji: "Inserir emoji"
 renote: "Repostar"
 unrenote: "Remover repostagem"
 renoted: "Repostado"
+renotedToX: "Repostar em {name}."
 cantRenote: "Não é possível repostar esta postagem"
 cantReRenote: "Não pode repostar este repost"
 quote: "Citar"
 inChannelRenote: "Repostar no canal"
 inChannelQuote: "Citar no canal"
+renoteToChannel: "Repostar em canal"
+renoteToOtherChannel: "Repostar em outro canal"
 pinnedNote: "Nota fixada"
 pinned: "Fixar no perfil"
 you: "Você"
@@ -121,9 +125,16 @@ sensitive: "Conteúdo sensível"
 add: "Adicionar"
 reaction: "Reações"
 reactions: "Reações"
+emojiPicker: "Seleção de emoji"
+pinnedEmojisForReactionSettingDescription: "Selecionar os emojis que serão fixados e exibidos ao reagir."
+pinnedEmojisSettingDescription: "Selecionar os emojis que serão fixos e exibidos na seleção de emoji."
+emojiPickerDisplay: "Janela de seleção de emoji"
+overwriteFromPinnedEmojisForReaction: "Sobrescrever as opções de reação"
+overwriteFromPinnedEmojis: "Sobrescrever as opções gerais"
 reactionSettingDescription2: "Arraste para reordenar, clique para excluir, pressione + para adicionar."
 rememberNoteVisibility: "Lembrar das configurações de visibilidade de notas"
 attachCancel: "Remover anexo"
+deleteFile: "Excluir arquivo"
 markAsSensitive: "Marcar como sensível"
 unmarkAsSensitive: "Desmarcar como sensível"
 enterFileName: "Digite o nome do arquivo"
@@ -144,6 +155,7 @@ editList: "Editar lista"
 selectChannel: "Selecionar canal"
 selectAntenna: "Selecione uma antena"
 editAntenna: "Editar antena"
+createAntenna: "Criar  uma antena"
 selectWidget: "Selecione um widget"
 editWidgets: "Editar widgets"
 editWidgetsExit: "Pronto"
@@ -170,16 +182,21 @@ addAccount: "Adicionar Conta"
 reloadAccountsList: "Recarregar lista de contas"
 loginFailed: "Falha ao logar"
 showOnRemote: "Exibir remotamente"
+continueOnRemote: ""
+chooseServerOnMisskeyHub: "Escolher um servidor da Misskey Hub"
+specifyServerHost: "Especificar uma instância diretamente"
+inputHostName: "Insira o domínio"
 general: "Geral"
 wallpaper: "Papel de parede"
 setWallpaper: "Definir papel de parede"
 removeWallpaper: "Remover papel de parede"
 searchWith: "Buscar: {q}"
 youHaveNoLists: "Não tem nenhuma lista"
-followConfirm: "Tem certeza que quer deixar de seguir {name}?"
+followConfirm: "Tem certeza que quer seguir {name}?"
 proxyAccount: "Conta proxy"
 proxyAccountDescription: "Uma conta de proxy é uma conta que assume o acompanhamento remoto de um usuário sob certas condições específicas. Por exemplo, quando um usuário inclui um usuário remoto em uma lista, mas ninguém na lista está seguindo o usuário remoto, a atividade não é entregue ao servidor. Nesse caso, a conta de proxy entra em ação para seguir o usuário remoto em vez disso."
 host: "Host"
+selectSelf: "Escolher manualmente"
 selectUser: "Selecionar usuário"
 recipient: "Destinatário"
 annotation: "Anotação"
@@ -194,6 +211,8 @@ perHour: "Por Hora"
 perDay: "Por dia"
 stopActivityDelivery: "Parar a entrega de atividades"
 blockThisInstance: "Bloquear esta instância"
+silenceThisInstance: "Silenciar essa instância"
+mediaSilenceThisInstance: "Silenciar a mídia dessa instância"
 operations: "Operações"
 software: "Software"
 version: "Versão"
@@ -213,6 +232,10 @@ clearCachedFiles: "Limpar o cache"
 clearCachedFilesConfirm: "Deseja excluir todos os arquivos remotos em cache?"
 blockedInstances: "Instância bloqueada"
 blockedInstancesDescription: "Configure os hosts dos servidores que deseja bloquear, separando-os por quebras de linha. Os servidores bloqueados não poderão interagir com este servidor, incluindo os subdomínios."
+silencedInstances: "Instâncias silenciadas"
+silencedInstancesDescription: "Liste o nome de hospedagem dos servidores que você deseja silenciar, separados por linha. Todas as contas desses servidores serão silenciada e poderão enviar solicitações para seguir, mas não poderão mencionar usuários locais sem segui-los. Isso não afetará servidores bloqueados."
+mediaSilencedInstances: "Instâncias com mídia silenciadas"
+mediaSilencedInstancesDescription: "Liste o nome de hospedagem dos servidores cuja mídia você deseja silenciar, separados por linha. Todas as contas desses servidores serão consideradas sensíveis e não poderão utilizar emojis personalizados. Isso não afetará servidores bloqueados."
 muteAndBlock: "Silenciar e bloquear"
 mutedUsers: "Usuários silenciados"
 blockedUsers: "Usuários bloqueados"
@@ -249,7 +272,7 @@ more: "Mais!"
 featured: "Destaques"
 usernameOrUserId: "Nome de usuário ou ID do usuário"
 noSuchUser: "Usuário não encontrado"
-lookup: "Buscando"
+lookup: "Consultar"
 announcements: "Avisos"
 imageUrl: "URL da imagem"
 remove: "Remover"
@@ -257,6 +280,7 @@ removed: "Removido"
 removeAreYouSure: "Deseja excluir \"{x}\"?"
 deleteAreYouSure: "Deseja excluir \"{x}\"?"
 resetAreYouSure: "Deseja reiniciar?"
+areYouSure: "Tem certeza?"
 saved: "Salvo"
 messaging: "Chat"
 upload: "Fazer upload"
@@ -272,7 +296,7 @@ explore: "Explorar"
 messageRead: "Lida"
 noMoreHistory: "Não existe histórico anterior"
 startMessaging: "Iniciar conversação"
-nUsersRead: "{n} Pessoas leem"
+nUsersRead: "{n} pessoas leram"
 agreeTo: "Eu concordo com {0}"
 agree: "Concordar"
 agreeBelow: "Eu concordo com o seguinte"
@@ -288,7 +312,7 @@ birthday: "Aniversário"
 yearsOld: "{age} anos"
 registeredDate: "Data de registro"
 location: "Localização"
-theme: "tema"
+theme: "Tema"
 themeForLightMode: "Temas usados ​​no modo de luz"
 themeForDarkMode: "Temas usados ​​no modo escuro"
 light: "Claro"
@@ -302,11 +326,13 @@ selectFile: "Selecione os arquivos"
 selectFiles: "Selecione os arquivos"
 selectFolder: "Selecionar uma pasta"
 selectFolders: "Selecionar uma pasta"
+fileNotSelected: "Nenhuma pasta selecionada"
 renameFile: "Renomear ficheiro"
 folderName: "Nome da pasta"
 createFolder: "Criar pasta"
 renameFolder: "Renomear Pasta"
 deleteFolder: "Excluir pasta"
+folder: "Pasta"
 addFile: "Adicionar arquivo"
 emptyDrive: "O drive está vazio"
 emptyFolder: "A pasta está vazia"
@@ -368,8 +394,11 @@ hcaptcha: "hCaptcha"
 enableHcaptcha: "Ativar hCaptcha"
 hcaptchaSiteKey: "Chave do sítio ‘web’"
 hcaptchaSecretKey: "Chave secreta"
+mcaptcha: "mCaptcha"
+enableMcaptcha: "Habilitar mCaptcha"
 mcaptchaSiteKey: "Chave do sítio ‘web’"
 mcaptchaSecretKey: "Chave secreta"
+mcaptchaInstanceUrl: "URL do servidor mCaptcha"
 recaptcha: "reCAPTCHA"
 enableRecaptcha: "Habilitar reCAPTCHA"
 recaptchaSiteKey: "Chave do sítio ‘web’"
@@ -385,6 +414,7 @@ name: "Nome"
 antennaSource: "Origem de entrada"
 antennaKeywords: "Palavras-chave recebidas"
 antennaExcludeKeywords: "Palavras-chave negativas"
+antennaExcludeBots: "Ignorar contas de bot"
 antennaKeywordsDescription: "Se você separá-lo com um espaço, será uma especificação AND, e se você separá-lo com uma quebra de linha, será uma especificação OR."
 notifyAntenna: "Notificar novas notas"
 withFileAntenna: "Apenas notas com arquivos anexados"
@@ -417,6 +447,9 @@ totp: "Aplicativo Autenticador"
 totpDescription: "Digite a senha de uso único informado pelo aplicativo autenticador"
 moderator: "Moderador"
 moderation: "Moderação"
+moderationNote: "Nota de moderação"
+addModerationNote: "Adicionar nota de moderação"
+moderationLogs: "Logs de moderação"
 nUsersMentioned: "Postado por {n} pessoas"
 securityKeyAndPasskey: "Chave de segurança / Chave de acesso"
 securityKey: "Chave de segurança"
@@ -449,10 +482,12 @@ retype: "Digite novamente"
 noteOf: "Publicação de {user}"
 quoteAttached: "Com citação"
 quoteQuestion: "Anexar como citação?"
+attachAsFileQuestion: "O texto na área de transferência é muito longo. Você gostaria de anexá-lo como um arquivo de texto?"
 noMessagesYet: "Sem conversas até o momento"
 newMessageExists: "Há uma nova mensagem"
 onlyOneFileCanBeAttached: "Apenas um arquivo pode ser anexado a uma mensagem"
 signinRequired: "É necessário se inscrever ou fazer login antes de continuar"
+signinOrContinueOnRemote: "Para continuar, você precisa mover o seu servidor ou entrar/cadastrar-se nesse servidor."
 invitations: "Convidar"
 invitationCode: "Código de convite"
 checking: "Verificando..."
@@ -474,8 +509,8 @@ uiLanguage: "Idioma de exibição da interface "
 aboutX: "Sobre {x}"
 emojiStyle: "Estilo de emojis"
 native: "Nativo"
-disableDrawer: "Não mostrar o menu em formato de gaveta"
 showNoteActionsOnlyHover: "Exibir as ações da nota somente ao passar o cursor sobre ela"
+showReactionsCount: "Ver o número de reações nas notas"
 noHistory: "Ainda não há histórico"
 signinHistory: "Histórico de acesso"
 enableAdvancedMfm: "Habilitar MFM avançado"
@@ -524,10 +559,11 @@ objectStorageUseProxy: "Usar proxy"
 objectStorageUseProxyDesc: "Se você não usa proxy para conexão de API, desative-o."
 objectStorageSetPublicRead: "Definir 'public-read' ao fazer o upload"
 s3ForcePathStyleDesc: "Ao habilitar s3ForcePathStyle, o nome do bucket é especificado como parte do caminho em vez de ser o nome do host na URL. Isso pode ser necessário ao usar serviços auto-hospedados como o Minio."
-serverLogs: "Registro do servidor"
+serverLogs: "Logs do servidor"
 deleteAll: "Excluir tudo"
 showFixedPostForm: "Exibir o formulário de postagem na parte superior da linha do tempo"
 showFixedPostFormInChannel: "Exibir o campo de postagem na parte superior da linha do tempo (canais)"
+withRepliesByDefaultForNewlyFollowed: "Incluir respostas por usuários recém-seguidos na linha do tempo por padrão"
 newNoteRecived: "Nova nota recebida"
 sounds: "Sons"
 sound: "Sons"
@@ -537,6 +573,8 @@ showInPage: "Ver na página"
 popout: "Sair"
 volume: "Volume"
 masterVolume: "volume principal"
+notUseSound: "Desabilitar som"
+useSoundOnlyWhenActive: "Apenas reproduzir sons quando Misskey estiver aberto."
 details: "Detalhes"
 chooseEmoji: "Selecione um emoji"
 unableToProcess: "Não é possível concluir a operação"
@@ -557,6 +595,10 @@ output: "Resultado"
 script: "Script"
 disablePagesScript: "Desabilitar scripts nas páginas"
 updateRemoteUser: "Atualizar informações do usuário remoto"
+unsetUserAvatar: "Remover avatar"
+unsetUserAvatarConfirm: "Você tem certeza de que deseja remover o avatar?"
+unsetUserBanner: "Remover banner"
+unsetUserBannerConfirm: "Você tem certeza de que deseja remover o banner?"
 deleteAllFiles: "Excluir todos os arquivos"
 deleteAllFilesConfirm: "Deseja excluir todos os arquivos?"
 removeAllFollowing: "Deseja remover todos os seguidores?"
@@ -607,6 +649,7 @@ medium: "Médio"
 small: "Pequeno"
 generateAccessToken: "Gerar token de acesso"
 permission: "Permissões"
+adminPermission: "Permissões de administrador"
 enableAll: "Habilitar tudo"
 disableAll: "Desabilitar tudo"
 tokenRequested: "Autorização de acesso à conta"
@@ -628,6 +671,7 @@ smtpSecure: "Use SSL/TLS implícito para conexões SMTP"
 smtpSecureInfo: "Desative esta opção ao utilizar STARTTLS."
 testEmail: "Testar envio de e-mail"
 wordMute: "Silenciar palavras"
+hardWordMute: "SIlenciamento pesado de palavra"
 regexpError: "Erro na expressão regular"
 regexpErrorDescription: "Ocorreu um erro na expressão regular na linha {line} da palavra mutada {tab}:"
 instanceMute: "Instâncias silenciadas"
@@ -649,12 +693,13 @@ useGlobalSettingDesc: "Ao ativar, serão utilizadas as configurações de notifi
 other: "Outros"
 regenerateLoginToken: "Gerar novo token de login"
 regenerateLoginTokenDescription: "Gera novamente o token interno usado para o login. Normalmente, isso não é necessário. Ao regenerar, você será desconectado de todos os dispositivos."
+theKeywordWhenSearchingForCustomEmoji: "Essa é a palavra-chave ao pesquisar por emojis personalizados"
 setMultipleBySeparatingWithSpace: "Você pode configurar vários itens separando-os por espaço."
 fileIdOrUrl: "ID do arquivo ou URL"
 behavior: "Comportamento"
 sample: "Exemplo"
 abuseReports: "Denúncias"
-reportAbuse: "Denúncias"
+reportAbuse: "Denunciar"
 reportAbuseRenote: "Reportar repostagem"
 reportAbuseOf: "Denunciar {name}"
 fillAbuseReportDescription: "Por favor, forneça detalhes sobre o motivo da denúncia. Se houver uma nota específica envolvida, inclua também a URL dela."
@@ -662,10 +707,7 @@ abuseReported: "Denúncia enviada. Obrigado por sua ajuda."
 reporter: "Denunciante"
 reporteeOrigin: "Origem da denúncia"
 reporterOrigin: "Origem do denunciante"
-forwardReport: "Encaminhar a denúncia para o servidor remoto"
-forwardReportIsAnonymous: "No servidor remoto, suas informações não serão visíveis, e você será apresentado como uma conta do sistema anônima."
 send: "Enviar"
-abuseMarkAsResolved: "Marcar denúncia como resolvida"
 openInNewTab: "Abrir em nova aba"
 openInSideView: "Abrir em visão lateral"
 defaultNavigationBehaviour: "Navegação padrão"
@@ -708,6 +750,7 @@ lockedAccountInfo: "Mesmo que você defina a aprovação para seguir, a menos qu
 alwaysMarkSensitive: "Marcar como sensível por padrão"
 loadRawImages: "Exibir as imagens originais ao invés de miniaturas"
 disableShowingAnimatedImages: "Não reproduzir imagens animadas"
+highlightSensitiveMedia: "Destacar mídia sensível"
 verificationEmailSent: "Um e-mail de confirmação foi enviado. Siga o link no e-mail para concluir a verificação."
 notSet: "Não definido"
 emailVerified: "O endereço de e-mail foi confirmado"
@@ -721,7 +764,7 @@ experimentalFeatures: "Funcionalidades Experimentais"
 experimental: "Experimental"
 thisIsExperimentalFeature: "Este é um recurso experimental. As funções podem mudar ou pode não funcionar corretamente."
 developer: "Programador"
-makeExplorable: "Deixe a sua conta mais fácil de encontrar."
+makeExplorable: "Deixe a sua conta encontrável em \"Explorar\"."
 makeExplorableDescription: "Se você desativá-lo, outros usuários não poderão encontrar a sua conta na aba Descoberta."
 showGapBetweenNotesInTimeline: "Mostrar um espaço entre as notas na linha de tempo"
 duplicate: "Duplicar"
@@ -796,11 +839,12 @@ switchAccount: "Trocar conta"
 enabled: "Ativado"
 disabled: "Desativado"
 quickAction: "Ações rápidas"
-user: "Usuários"
+user: "Usuário"
 administration: "Administrar"
 accounts: "Contas"
 switch: "Trocar"
 noMaintainerInformationWarning: "A informação de administrador não foi configurada."
+noInquiryUrlWarning: "URL de consulta não está definida"
 noBotProtectionWarning: "A proteção contra bots não foi configurada."
 configure: "Configurar"
 postToGallery: "Criar publicação em galeria"
@@ -860,6 +904,8 @@ makeReactionsPublicDescription: "Isto vai deixar o histórico de todas as suas r
 classic: "Clássico"
 muteThread: "Silenciar esta conversa"
 unmuteThread: "Desativar silêncio desta conversa"
+followingVisibility: "Visibilidade dos usuários seguidos"
+followersVisibility: "Visibilidade dos seguidores"
 continueThread: "Ver mais desta conversa"
 deleteAccountConfirm: "Deseja realmente excluir a conta?"
 incorrectPassword: "Senha inválida."
@@ -925,21 +971,33 @@ fast: "Rápido"
 sensitiveMediaDetection: "Detecção de conteúdo sensível"
 localOnly: "Apenas local"
 remoteOnly: "Apenas remoto"
+failedToUpload: "Falha ao enviar"
+cannotUploadBecauseInappropriate: "Esse arquivo não pôde ser enviado porque partes dele foram detectadas como potencialmente inapropriadas."
+cannotUploadBecauseNoFreeSpace: "Envio falhou devido à falta de capacidade no Drive."
 cannotUploadBecauseExceedsFileSizeLimit: "Não é possível realizar o upload deste arquivo porque ele excede o tamanho máximo permitido."
 beta: "Beta"
 enableAutoSensitive: "Marcar automaticamente como conteúdo sensível"
 enableAutoSensitiveDescription: "Quando disponível, a marcação de mídia sensível será automaticamente atribuído ao conteúdo de mídia usando aprendizado de máquina. Mesmo que você desative essa função, em alguns servidores, isso pode ser configurado automaticamente."
 activeEmailValidationDescription: "A validação do endereço de e-mail do usuário será realizada de forma mais rigorosa, considerando se é um endereço descartável ou se é possível realizar comunicação efetiva. Se desativado, apenas a validade do formato do endereço será verificada como uma sequência de caracteres."
+navbar: "Barra de navegação"
 shuffle: "Aleatório"
 account: "Contas"
 move: "Mover"
 pushNotification: "Notificações Push"
 subscribePushNotification: "Ativar notificações push"
 unsubscribePushNotification: "Desativar notificações push"
+pushNotificationAlreadySubscribed: "Notificações push já estão habilitadas"
+pushNotificationNotSupported: "Seu navegador ou instância não tem suporte às notificações push"
+sendPushNotificationReadMessage: "Apagar notificações push quando elas foram lidas"
+sendPushNotificationReadMessageCaption: "Pode aumentar o consumo de energia do dispositivo."
+windowMaximize: "Maximizar"
 windowMinimize: "Minimizar"
 windowRestore: "Restaurar"
 caption: "legenda"
+loggedInAsBot: "Atualmente conectado como bot"
 tools: "Ferramentas"
+cannotLoad: "Não foi possível carregar"
+numberOfProfileView: "Visualizações do perfil"
 like: "Curtir"
 unlike: "Remover curtida"
 numberOfLikes: "Número de curtidas"
@@ -948,6 +1006,7 @@ neverShow: "Não exibir novamente"
 remindMeLater: "Lembrar mais tarde"
 didYouLikeMisskey: "Você gostou do Misskey?"
 pleaseDonate: "O Misskey é um software gratuito utilizado por {host}. Para que possamos continuar o desenvolvimento, pedimos que considerem fazer doações. A sua contribuição é muito importante!"
+correspondingSourceIsAvailable: "O código-fonte correspondente está disponível em {anchor}"
 roles: "Cargos"
 role: "Cargo"
 noRole: "Nenhum cargo"
@@ -957,6 +1016,7 @@ assign: "Atribuir"
 unassign: "Remover"
 color: "Cor"
 manageCustomEmojis: "Gerenciar Emojis customizados"
+manageAvatarDecorations: "Gerenciar decorações de avatar"
 youCannotCreateAnymore: "Você atingiu o limite de criação."
 cannotPerformTemporary: "Ação temporariamente indisponível"
 cannotPerformTemporaryDescription: "Esta ação não pôde ser concluída devido ao excesso de pedidos em sucessão. Tente novamente em alguns momentos."
@@ -974,218 +1034,635 @@ thisPostMayBeAnnoyingHome: "Postar na linha do tempo inicial"
 thisPostMayBeAnnoyingCancel: "Cancelar"
 thisPostMayBeAnnoyingIgnore: "Postar mesmo assim"
 collapseRenotes: "Ocultar repostagens já visualizadas"
+collapseRenotesDescription: "Colapsar notas em que você reagiu ou repostou."
 internalServerError: "Erro interno de servidor"
+internalServerErrorDescription: "Houve um erro inesperado no servidor."
+copyErrorInfo: "Copiar detalhes de erro"
+joinThisServer: "Cadastrar-se na instância"
+exploreOtherServers: "Buscar outra instância"
+letsLookAtTimeline: "Dar uma olhada na linha do tempo"
+disableFederationConfirm: "Realmente desabilitar a federação?"
+disableFederationConfirmWarn: "Mesmo se defederado, publicações continuarão sendo públicas, a menos que seja definido o contrário. Você geralmente não precisa disso."
+disableFederationOk: "Desabilitar"
+invitationRequiredToRegister: "Essa instância é apenas para convidados. Você precisa inserir um código válido para se cadastrar."
 emailNotSupported: "O envio de e-mails não é suportado nesta instância"
+postToTheChannel: "Publicar ao canal"
+cannotBeChangedLater: "Isso não pode ser alterado."
+reactionAcceptance: "Aceitação de Reações"
 likeOnly: "Apenas curtidas"
 likeOnlyForRemote: "Tudo (somente curtidas remotas)"
+nonSensitiveOnly: "Apenas não-sensível"
 nonSensitiveOnlyForLocalLikeOnlyForRemote: "Apenas não sensíveis (somente curtidas remotas)"
 rolesAssignedToMe: "Cargos atribuídos a mim"
+resetPasswordConfirm: "Deseja realmente mudar a sua senha?"
+sensitiveWords: "Palavras sensíveis"
+sensitiveWordsDescription: "A visibilidade de todas as notas contendo as palavras configuradas será colocadas como \"Início\" automaticamente. Você pode listar várias delas separando-as por linha."
+sensitiveWordsDescription2: "Utilizar espaços irá criar expressões aditivas (AND) e cercar palavras-chave com barras irá transformá-las em expressões regulares (RegEx)"
+prohibitedWords: "Palavras proibidas"
+prohibitedWordsDescription: "Habilita um erro ao tentar publicar uma nota contendo as palavras escolhidas. Várias palavras podem ser escolhidas, separando-as por linha."
+prohibitedWordsDescription2: "Utilizar espaços irá criar expressões aditivas (AND) e cercar palavras-chave com barras irá transformá-las em expressões regulares (RegEx)"
+hiddenTags: "Hashtags escondidas"
+hiddenTagsDescription: "Selecione tags que não serão exibidas na lista de destaques. Várias tags podem ser escolhidas, separadas por linha."
+notesSearchNotAvailable: "A pesquisa de notas está indisponível."
+license: "Licença"
 unfavoriteConfirm: "Deseja realmente remover dos favoritos?"
+myClips: "Meus clipes"
 drivecleaner: "Limpeza do drive"
+retryAllQueuesNow: "Tentar novamente todas as pendências"
 retryAllQueuesConfirmTitle: "Gostaria de tentar novamente agora?"
+retryAllQueuesConfirmText: "Isso irá temporariamente aumentar a carga do servidor."
+enableChartsForRemoteUser: "Gerar gráficos estatísticos de usuários remotos"
+enableChartsForFederatedInstances: "Gerar gráficos estatísticos de instâncias remotas"
+showClipButtonInNoteFooter: "Adicionar \"Clip\" ao menu de ação de notas"
 reactionsDisplaySize: "Tamanho de exibição das reações"
+limitWidthOfReaction: "Limita o comprimento máximo de reações e as exibe em tamanho reduzido"
+noteIdOrUrl: "ID ou URL de nota"
+video: "Vídeo"
+videos: "Vídeos"
+audio: "Áudio"
+audioFiles: "Áudio"
+dataSaver: "Economia de Dados"
+accountMigration: "Migração da Conta"
+accountMoved: "Esse usuário moveu-se para uma nova conta:"
+accountMovedShort: "Essa conta foi migrada."
+operationForbidden: "Operação proibída"
+forceShowAds: "Sempre mostrar propagandas"
+addMemo: "Adicionar memorando"
+editMemo: "Editar memorando"
 reactionsList: "Reações"
 renotesList: "Repostagens"
+notificationDisplay: "Notificações"
 leftTop: "Superior esquerdo"
 rightTop: "Superior direito"
 leftBottom: "Inferior esquerdo"
 rightBottom: "Inferior direito"
+stackAxis: "Eixo de empilhamento"
 vertical: "Vertical"
 horizontal: "Exibir painel lateral inteiro"
 position: "Posição"
 serverRules: "Regras do servidor"
+pleaseConfirmBelowBeforeSignup: "Para cadastrar-se no servidor, você precisa ler e concordar como seguinte:"
+pleaseAgreeAllToContinue: "Você precisa concordar com todos os campos acima para continuar."
 continue: "Continuar"
+preservedUsernames: "Nomes de usuário reservados"
 preservedUsernamesDescription: "Liste os nomes de usuário que deseja reservar, separando-os por quebras de linha. Os nomes de usuário especificados aqui não poderão ser utilizados durante a criação de contas. No entanto, esta restrição não se aplica quando a conta é criada por um administrador. Além disso, as contas que já existem não serão afetadas."
+createNoteFromTheFile: "Compor nota a partir desse arquivo"
 archive: "Arquivo"
+archived: "Arquivado"
+unarchive: "Desarquivar"
 channelArchiveConfirmTitle: "Deseja realmente arquivar {name}?"
+channelArchiveConfirmDescription: "Um canal arquivado não irá aparecer na lista de canais e nem resultados de pesquisa. Novas publicações não poderão mais ser adicionadas."
+thisChannelArchived: "Esse canal foi arquivado."
+displayOfNote: "Exibição de nota"
+initialAccountSetting: "Configuração inicial do perfil"
 youFollowing: "Seguindo"
+preventAiLearning: "Rejeitar uso de Aprendizado de Máquina (IA Generativa)"
 preventAiLearningDescription: "Solicita-se que o conteúdo de notas e imagens enviadas não seja usado como objeto de aprendizado por sistemas externos de geração de texto ou imagens. Isso é alcançado incluindo a flag 'noai' na resposta HTML. No entanto, o cumprimento dessa solicitação depende do próprio sistema de IA, portanto, não é garantia total de prevenção de aprendizado."
 options: "Opções"
+specifyUser: "Usuário específico"
+lookupConfirm: "Deseja buscar?"
+openTagPageConfirm: "Deseja abrir a uma página de hashtag?"
+specifyHost: "Especificar um hospedeiro"
+failedToPreviewUrl: "Não foi possível carregar prévia"
+update: "Atualizar"
 rolesThatCanBeUsedThisEmojiAsReaction: "Cargos que podem utilizar este emoji como reação"
 rolesThatCanBeUsedThisEmojiAsReactionEmptyDescription: "Se nenhum cargo for especificado, qualquer pessoa pode usar este emoji como reação."
 rolesThatCanBeUsedThisEmojiAsReactionPublicRoleWarn: "Estes cargos devem ser públicos."
+cancelReactionConfirm: "Realmente excluir a sua reação?"
+changeReactionConfirm: "Realmente mudar a sua reação?"
+later: "Talvez mais tarde"
+goToMisskey: "Ao Misskey"
+additionalEmojiDictionary: "Dicionários adicionais de emoji"
+installed: "Instalado"
+branding: "Marca"
+enableServerMachineStats: "Publicar estatísticas do hardware do servidor"
+enableIdenticonGeneration: "Habilitar geração de identicon de usuário"
+turnOffToImprovePerformance: "Desligar isso pode melhorar o desempenho."
+createInviteCode: "Gerar convite"
+createWithOptions: "Criar com opções"
+createCount: "Número de convites"
+inviteCodeCreated: "Convite gerado"
+inviteLimitExceeded: "Você excedeu o limite de convites que podem ser gerados."
+createLimitRemaining: "Limite de convites: {limit}"
+inviteLimitResetCycle: "Esse limite irá tornar-se {limit} em {time}."
+expirationDate: "Data de expiração"
+noExpirationDate: "Sem expiração"
+inviteCodeUsedAt: "Código de convite usado em"
+registeredUserUsingInviteCode: "Convite usado por"
 waitingForMailAuth: "Verificação de e-mail pendente "
+inviteCodeCreator: "Convite criado por"
+usedAt: "Usado em"
+unused: "Não foi usado"
+used: "Usado"
+expired: "Expirado"
+doYouAgree: "Concorda?"
+beSureToReadThisAsItIsImportant: "Por favor, leia essa informação importante."
+iHaveReadXCarefullyAndAgree: "Eu li o texto \"{x}\" e concordo."
+dialog: "Diálogo"
 icon: "Avatar"
+forYou: "Para você"
+currentAnnouncements: "Anúncios atuais"
+pastAnnouncements: "Anúncios passados"
+youHaveUnreadAnnouncements: "Há anúncios não lidos."
+useSecurityKey: "Por favor, siga as instruções do seu navegador ou dispositivo para utilizar uma chave de acesso."
 replies: "Responder"
 renotes: "Repostar"
+loadReplies: "Mostrar respostas"
+loadConversation: "Mostrar conversa"
+pinnedList: "Lista fixada"
 keepScreenOn: "Manter a tela do dispositivo sempre ligada"
+verifiedLink: "A autoria do link foi verificada"
+notifyNotes: "Notificar sobre novas notas"
+unnotifyNotes: "Deixar de notificar sobre novas notas"
+authentication: "Autenticação"
+authenticationRequiredToContinue: "Por favor, autentique-se para continuar"
+dateAndTime: "Data e Hora"
+showRenotes: "Exibir reposts"
+edited: "Editado"
+notificationRecieveConfig: "Configurações de Notificação"
+mutualFollow: "Seguidor mútuo"
+followingOrFollower: "Seguidor ou usuário seguido"
+fileAttachedOnly: "Apenas notas com arquivos"
+showRepliesToOthersInTimeline: "Mostrar respostas aos outros na linha do tempo"
+hideRepliesToOthersInTimeline: "Esconder respostas dos outros na linha do tempo"
+showRepliesToOthersInTimelineAll: "Mostrar respostas aos outros,  mas apenas de quem você segue, na linha do tempo"
+hideRepliesToOthersInTimelineAll: "Esconder respostas de todos que você segue na linha do tempo"
+confirmShowRepliesAll: "Essa operação é irreversível. Você gostaria de mostrar respostas a todos que você segue na sua linha do tempo?"
+confirmHideRepliesAll: "Essa operação é irreversível. Você gostaria de esconder respostas a todos que você segue na sua linha do tempo?"
+externalServices: "Serviços Externos"
+sourceCode: "Código-fonte"
+sourceCodeIsNotYetProvided: "Código-fonte está indisponível. Contate o administrador para resolver esse problema."
+repositoryUrl: "URL do repositório"
+repositoryUrlDescription: "Se você estiver utilizando Misskey como está (sem mudanças no código-fonte), insira https://github.com/misskey-dev/misskey"
+repositoryUrlOrTarballRequired: "Se você não publicou um repositório, você precisa providenciar uma tarball em seu lugar. Veja .config/example.yml para mais informações."
+feedback: "Feedback"
+feedbackUrl: "Link para Feedback"
+impressum: "Impressum"
+impressumUrl: "URL de 'Impressum'"
+impressumDescription: "Em alguns países, como a Alemanha, a inclusão de informação de contato do operador de um serviço é legalmente exigida para websites comerciais."
+privacyPolicy: "Política de Privacidade"
+privacyPolicyUrl: "URL da Política de Privacidade"
+tosAndPrivacyPolicy: "Termos de Serviço e Política de Privacidade"
+avatarDecorations: "Decorações de avatar"
+attach: "Anexar"
+detach: "Remover"
+detachAll: "Remover Tudo"
+angle: "Ângulo"
 flip: "Inversão"
+showAvatarDecorations: "Exibir decorações de avatar"
+releaseToRefresh: "Solte para atualizar"
+refreshing: "Atualizando..."
+pullDownToRefresh: "Puxe para baixo para atualizar"
+disableStreamingTimeline: "Desabilitar atualizações em tempo real da linha do tempo"
+useGroupedNotifications: "Agrupar notificações"
+signupPendingError: "Houve um problema ao verificar o endereço de email. O link pode ter expirado."
+cwNotationRequired: "Se \"Esconder conteúdo\" está habilitado, uma descrição deve ser adicionada."
+doReaction: "Adicionar reação"
+code: "Código"
+reloadRequiredToApplySettings: "É necessário reiniciar para aplicar as configurações."
+remainingN: "Restante: {n}"
+overwriteContentConfirm: "Você tem certeza de que deseja sobrescrever o conteúdo atual?"
+seasonalScreenEffect: "Efeito de Tela Sazonal"
+decorate: "Decorar"
+addMfmFunction: "Adicionar MFM"
+enableQuickAddMfmFunction: "Exibir seleção avançada de MFM"
+bubbleGame: "Bubble Game"
+sfx: "Efeitos Sonoros"
+soundWillBePlayed: "Sons serão reproduzidos"
+showReplay: "Ver Replay"
+replay: "Replay"
+replaying: "Mostrando Replay"
+endReplay: "Sair do Replay"
+copyReplayData: "Copiar dados de Replay"
+ranking: "Ranking"
 lastNDays: "Últimos {n} dias"
+backToTitle: "Voltar à página inicial"
+hemisphere: "Onde você se localiza"
+withSensitive: "Incluir notas com arquivos sensíveis"
+userSaysSomethingSensitive: "Publicação de {name} contém conteúdo sensível"
+enableHorizontalSwipe: "Arraste para mudar de aba"
+loading: "Carregando"
 surrender: "Cancelar"
+gameRetry: "Tentar Novamente"
+notUsePleaseLeaveBlank: "Deixe em branco caso inutilizado"
+useTotp: "Digite a senha de uso único"
+useBackupCode: "Usar códigos de “backup”"
+launchApp: "Iniciar aplicação"
+useNativeUIForVideoAudioPlayer: "Utilizar UI do navegador ao reproduzir vídeo e áudio"
+keepOriginalFilename: "Manter nome original do arquivo"
+keepOriginalFilenameDescription: "Se você desabilitar essa opção, os nomes de arquivos serão substituídos por uma sequência aleatória ao enviar arquivos."
+noDescription: "Não há descrição"
+alwaysConfirmFollow: "Sempre confirmar ao seguir"
+inquiry: "Contato"
+tryAgain: "Por favor, tente novamente mais tarde"
+confirmWhenRevealingSensitiveMedia: "Confirmar ao revelar mídia sensível"
+sensitiveMediaRevealConfirm: "Essa mídia pode ser sensível. Deseja revelá-la?"
+createdLists: "Listas criadas"
+createdAntennas: "Antenas criadas"
+clipNoteLimitExceeded: "Não é possível adicionar mais notas ao clipe."
 _delivery:
+  status: "Estado de entrega"
   stop: "Suspenso"
+  resume: "Continuar entrega"
   _type:
     none: "Publicando"
+    manuallySuspended: "Suspenso manualmente"
+    goneSuspended: "Servidor foi suspenso devido ao seu apagamento"
+    autoSuspendedForNotResponding: "Servidor foi suspenso por não responder"
+_bubbleGame:
+  howToPlay: "Como jogar"
+  hold: "Próximos"
+  _score:
+    score: "Pontuação"
+    scoreYen: "Dinheiro recebido"
+    highScore: "Melhor pontuação"
+    maxChain: "Número máximo de encadeamentos"
+    yen: "{yen} Yen"
+    estimatedQty: "{qty} Peças"
+    scoreSweets: "{onigiriQtyWithUnit} Onigiri"
+  _howToPlay:
+    section1: "Ajuste a posição e solte o objeto na caixa."
+    section2: "Quando dois objetos do mesmo tipo tocam-se, eles tornam-se outro objeto e você ganha pontos."
+    section3: "O jogo acaba quando objetos transbordam da caixa. Busque uma pontuação alta ao fundir objetos enquanto evita transbordar a caixa."
+_announcement:
+  forExistingUsers: "Apenas aos usuários existente"
+  forExistingUsersDescription: "Se habilitado, esse anúncio será exibido apenas para usuários existentes no tempo de publicação. Se desabilitado, novos usuários também o receberão. "
+  needConfirmationToRead: "Exigir confirmação de leitura"
+  needConfirmationToReadDescription: "Um lembrete adicional será exibido para confirmar a leitura do anúncio. Esse anúncio também será excluído de qualquer forma de \"Marcar tudo como lido\"."
+  end: "Arquivar anúncio"
+  tooManyActiveAnnouncementDescription: "O excesso de anúncios pode atrapalhar a experiência do usuário. Considere arquivar anúncios obsoletos."
+  readConfirmTitle: "Marcar como lido?"
+  readConfirmText: "Isso marcará o conteúdo de \"{title}\" como lido."
+  shouldNotBeUsedToPresentPermanentInfo: "É preferível utilizar anúncios para publicar informações atuais e de curto prazo, e não informações que serão relevantes por muito tempo."
+  dialogAnnouncementUxWarn: "O uso de duas ou mais notificações de diálogo simultaneamente pode impactar significativamente a experiência de usuário. Portanto, utilize-as cuidadosamente."
+  silence: "Sem notificação"
+  silenceDescription: "Habilitar isso irá pular a notificação desse anúncio e o usuário não precisará lê-lo."
 _initialAccountSetting:
+  accountCreated: "A sua conta foi criada com sucesso!"
+  letsStartAccountSetup: "Em primeiro lugar, vamos configurar o seu perfil."
+  letsFillYourProfile: "Primeiramente, vamos configurar o seu perfil."
+  profileSetting: "Configurações do perfil"
+  privacySetting: "Configurações de privacidade"
+  theseSettingsCanEditLater: "Você pode alterar estas configurações mais tarde."
+  youCanEditMoreSettingsInSettingsPageLater: "Há mais configurações na página \"Configurações\". Não se esqueça de visitá-la mais tarde."
   followUsers: "Siga usuários que lhe interessam para criar a sua linha do tempo."
+  pushNotificationDescription: "Habilitar notificações push o possibilitará receber notificações de {name} diretamente no seu dispositivo."
+  initialAccountSettingCompleted: "Configuração de perfil completa!"
+  haveFun: "Aproveite {name}!"
+  youCanContinueTutorial: "Você pode iniciar um tutorial de como utilizar {name} (Misskey) ou pode sair da configuração e começar o uso imediatamente."
+  startTutorial: "Iniciar Tutorial"
+  skipAreYouSure: "Deseja pular a configuração de perfil?"
+  laterAreYouSure: "Deseja adiar a configuração de perfil?"
+_initialTutorial:
+  launchTutorial: "Iniciar Tutorial"
+  title: "Tutorial"
+  wellDone: "Ótimo!"
+  skipAreYouSure: "Sair do Tutorial?"
+  _landing:
+    title: "Bem-vindo ao Tutorial!"
+    description: "Aqui, você pode aprender o básico de como usar o Misskey e as suas funções."
+  _note:
+    title: "O que é uma Nota?"
+    description: "Publicações no Misskey chamam-se 'Notas'. Notas são organizadas cronologicamente na linha do tempo e atualizam em tempo real."
+    reply: "Clique nesse botão para responder a uma mensagem. Também é possível responder respostas, continuando a conversa como uma \"thread\"."
+    renote: "Você pode compartilhar essa nota na sua linha do tempo. Você também pode citá-la com os seus comentários."
+    reaction: "Você pode adicionar reações à nota. Mais detalhes serão explicados na próxima página."
+    menu: "Você pode ver detalhes da nota, copiar links e realizar outras ações."
+  _reaction:
+    title: "O que são Reações?"
+    description: "É possível reagir às notas com diversos emojis. Reações permitem que você expresse sutilezas que não são possíveis apenas com uma curtida."
+    letsTryReacting: "Reações podem ser adicionadas clicando no botão \"+\". Tente reagir à nota de exemplo."
+    reactToContinue: "Adicione uma reação para continuar."
+    reactNotification: "Você receberá notificações em tempo real quando alguém reagir à sua nota."
+    reactDone: "Você pode desfazer uma reação ao selecionar o botão \"-\"."
+  _timeline:
+    title: "O Conceito das Linhas do Tempo"
+    description1: "Misskey providencia diversas linhas do tempo baseadas na sua utilidade (algumas podem não estar disponíveis a partir das configurações da instância)."
+    home: "Você pode ver as notas das contas seguidas. "
+    local: "Você pode ver notas de todos os usuários dessa instância."
+    social: "Notas da linha do tempo Início e Local serão exibidas."
+    global: "Você pode ver notas de todos os servidores conectados."
+    description2: "Você pode alterar dentre as linhas do tempo no todo da tela a qualquer momento."
+    description3: "Adicionalmente, há \"listas\" e \"canais\". Para mais informações, acesse {link}."
+  _postNote:
+    title: "Opções de Postagem de Nota"
+    description1: "Ao postar uma nota no Misskey, diversas opções estão disponíveis. A ficha de publicação parece com isto: "
+    _visibility:
+      description: "Você pode limitar quem vê a sua nota."
+      public: "Sua nota será visível a todos os usuários."
+      home: "Publicar apenas na linha do tempo Início. Pessoas visitando seu perfil, seja seguindo ou por um repost poderão vê-los."
+      followers: "Visível apenas para seguidores. Apenas seguidores podem vê-la e mais ninguém, e ela não pode ser repostada pelos demais."
+      direct: "Visível apenas para usuários específicos, e o destinatário será notificado. Pode ser usado como uma alternativa às mensagens diretas."
+      doNotSendConfidencialOnDirect1: "Tenha cuidado ao enviar informações sensíveis!"
+      doNotSendConfidencialOnDirect2: "Administradores do servidor podem ver o que foi escrito. Cuidado, também, ao enviar notas diretas a usuários de servidores não confiáveis."
+      localOnly: "Publicar com essa opção não federará a nota com outros servidores. Usuários desses servidores não poderão ver essas notas diretamente, independente das opções de visibilidade acima. "
+    _cw:
+      title: "Aviso de Conteúdo"
+      description: "Ao invés do corpo do texto, o conteúdo escrito na caixa \"anotação\" será exibido. Apertar \"Carregar mais\" irá revelar o corpo."
+      _exampleNote:
+        cw: "Isso irá te esfomear!"
+        note: "Acabei de comer um donut coberto de chocolate! 🍩😋"
+      useCases: "Isso pode ser usado caso seja exigido, pelas diretrizes do servidor, o cuidado com algum tópico ou ao publicar conteúdo sensível ou spoilers."
+  _howToMakeAttachmentsSensitive:
+    title: "Como Marcar Anexos como Sensíveis?"
+    description: "Para anexos cujo conteúdo é considerado sensível pelas diretrizes do servidor ou quando pretende-se esconder o seu conteúdo, adicione o sinal \"sensível\"."
+    tryThisFile: "Tente marcar a imagem anexada como sensível!"
+    _exampleNote:
+      note: "Opa, me atrapalhei abrindo a tampa do natô..."
+    method: "Para marcar um anexo como sensível, clique na sua miniatura, abra o menu e clique \"Marcar como sensível\"."
+    sensitiveSucceeded: "Ao anexar arquivos, por favor atribua uma sensibilidade coerente com as diretrizes da instância."
+    doItToContinue: "Marque o anexo como sensível para prosseguir."
+  _done:
+    title: "Você completou o tutorial! 🎉"
+    description: "As funções apresentadas aqui são apenas uma pequena parte. Para um conhecimento mais detalhado do uso do Misskey, acesse {link}."
+_timelineDescription:
+  home: "Na linha do tempo Início, você verá notas dos usuários que você segue."
+  local: "Na linha do tempo Local, você verá notas de todos os usuários da instância."
+  social: "Na linha do tempo Social, você verá notas do Início e Local."
+  global: "Na linha do tempo Global, você verá notas de todas as instâncias conectadas."
+_serverRules:
+  description: "Um grupo de regras a ser exibido antes de um cadastro. É recomendado que se faça um resumo dos Termos de Serviço."
 _serverSettings:
   iconUrl: "URL do ícone"
+  appIconDescription: "Especifica o ícone utilizado quando {host} é exibido como um app."
+  appIconUsageExample: "Exemplo: Como PWA, ou quando exibido num marcador de páginas ou na tela inicial de um celular"
+  appIconStyleRecommendation: "Como o ícone pode ser cortado para um quadrado ou círculo, é recomendado adicionar um fundo colorido na imagem."
+  appIconResolutionMustBe: "A resolução mínima é {resolution}."
+  manifestJsonOverride: "Sobrescrever manifest.json"
+  shortName: "Abreviação"
+  shortNameDescription: "Uma abreviação do nome da instância que pode ser exibido caso o nome oficial completo seja muito longo."
+  fanoutTimelineDescription: "Melhora significativamente a performance do retorno da linha do tempo e reduz o impacto no banco de dados quando habilitado. Em contrapartida, o uso de memória do Redis aumentará. Considere desabilitar em casos de baixa disponibilidade de memória ou instabilidade do servidor."
+  fanoutTimelineDbFallback: "\"Fallback\" ao banco de dados"
+  fanoutTimelineDbFallbackDescription: "Quando habilitado, a linha do tempo irá recuar ao banco de dados caso consultas adicionais sejam feitas e ela não estiver em cache. Quando desabilitado, o impacto no servidor será reduzido ao eliminar o recuo, mas limita a quantidade de linhas do tempo que podem ser recebidas."
+  inquiryUrl: "URL de inquérito"
+  inquiryUrlDescription: "Especifique um URL para um formulário de inquérito para a administração ou uma página web com informações de contato."
 _accountMigration:
+  moveFrom: "Migrar outra conta para essa"
+  moveFromSub: "Criar um 'alias' a outra conta"
+  moveFromLabel: "Conta original #{n}"
   moveFromDescription: "Se você deseja migrar de outra conta para esta, é necessário criar um alias aqui. Por favor, insira a conta de origem da migração no seguinte formato: @username@server.example.com. Para excluir o alias, deixe o campo em branco e clique em salvar (não recomendado)."
+  moveTo: "Migrar dessa conta para outra"
+  moveToLabel: "Conta para a qual se mover:"
+  moveCannotBeUndone: "A migração de conta não pode ser desfeita."
   moveAccountDescription: "Você está migrando para uma nova conta.\n ・Seus seguidores irão automaticamente seguir a nova conta.\n ・Todas as suas conexões de seguidores nesta conta serão removidas.\n ・Você não poderá mais criar novas notas nesta conta.\n\nA migração dos seguidores é automática, mas a migração das pessoas que você segue deve ser feita manualmente. Antes de migrar, exporte quem você está seguindo nesta conta e, assim que migrar, importe essa lista na nova conta.\nO mesmo se aplica para listas, silenciamentos e bloqueios, que também devem ser migrados manualmente.\n\n(Esta descrição se refere ao comportamento do servidor Misskey v13.12.0 ou posterior. Outros softwares ActivityPub, como Mastodon, podem ter comportamentos diferentes.)"
   moveAccountHowTo: "Para realizar a migração da conta, primeiro crie um alias para esta conta no destino da migração. Após criar o alias, insira a conta de destino da migração no seguinte formato: @username@server.example.com."
+  startMigration: "Migrar"
   migrationConfirm: "Tem certeza de que deseja migrar esta conta para '{account}'? Uma vez migrada, não poderá ser desfeita e não será possível usar esta conta novamente em seu estado original."
+  movedAndCannotBeUndone: "Essa conta foi migrada. A migração não pode ser desfeita."
   postMigrationNote: "A remoção dos seguidores desta conta será realizada 24 horas após a operação de migração. O número de seguidores e seguidos desta conta se tornará zero. Os seguidores não serão removidos, portanto, eles continuarão a ver as postagens destinadas aos seguidores desta conta."
+  movedTo: "Conta para a qual se mover:"
 _achievements:
   earnedAt: "Data de aquisição"
   _types:
     _notes1:
       title: "Configurando o meu misskey"
-      description: "Postou uma nota pela primeira vez"
+      description: "Poste uma nota pela primeira vez"
       flavor: "Divirta-se com o Misskey!"
     _notes10:
       title: "Algumas notas"
-      description: "Postou 10 notas"
+      description: "Poste 10 notas"
     _notes100:
       title: "Um monte de notas"
-      description: "Postou 100 notas"
+      description: "Poste 100 notas"
     _notes500:
       title: "Coberto por notas"
-      description: "Postou 500 notas"
+      description: "Poste 500 notas"
     _notes1000:
       title: "Uma montanha de notas"
-      description: "Postou 1000 notas"
+      description: "Poste 1 000 notas"
     _notes5000:
       title: "Enxurrada de notas"
-      description: "Postou 5000 notas"
+      description: "Poste 5000 notas"
     _notes10000:
-      title: "Super nota"
-      description: "Postou 10000 notas"
+      title: "Supernota"
+      description: "Poste 10 000 notas"
     _notes20000:
       title: "Preciso... de mais... notas..."
-      description: "Postou 20000 notas"
+      description: "Poste 20 000 notas"
     _notes30000:
       title: "Notas, Notas, NOTAS!"
-      description: "Postou 30000 notas"
+      description: "Poste 30 000 notas"
     _notes40000:
       title: "Fábrica de notas"
-      description: "Postou 40000 notas"
+      description: "Poste 40 000 notas"
     _notes50000:
       title: "Planeta de notas"
-      description: "Postou 50000 notas"
+      description: "Poste 50 000 notas"
     _notes60000:
       title: "Quasar de notas"
-      description: "Postou 60000 notas"
+      description: "Poste 60 000 notas"
     _notes70000:
       title: "Buraco negro de notas"
-      description: "Postou 70000 notas"
+      description: "Poste 70 000 notas"
     _notes80000:
       title: "Galáxia de notas"
-      description: "Postou 80000 notas"
+      description: "Poste 80 000 notas"
     _notes90000:
       title: "Universo de notas"
-      description: "Postou 90000 notas"
+      description: "Poste 90 000 notas"
     _notes100000:
       title: "ALL YOUR NOTE ARE BELONG TO US"
-      description: "Postou 100000 notas"
+      description: "Poste 100 000 notas"
       flavor: "Você realmente tem muita coisa para escrever"
     _login3:
       title: "Iniciante I"
-      description: "Fez login por um total de 3 dias"
+      description: "Faça login por um total de 3 dias"
       flavor: "De hoje em diante, me chame apenas de Misskist"
     _login7:
       title: "Iniciante II"
-      description: "Fez login por um total de 7 dias"
+      description: "Faça login por um total de 7 dias"
       flavor: "Pegando o jeito da coisa?"
     _login15:
       title: "Iniciante III"
-      description: "Fez login por um total de 15 dias"
+      description: "Faça login por um total de 15 dias"
     _login30:
       title: "Misskist I"
-      description: "Fez login por um total de 30 dias"
+      description: "Faça login por um total de 30 dias"
     _login60:
       title: "Misskist II"
-      description: "Fez login por um total de 60 dias"
+      description: "Faça login por um total de 60 dias"
     _login100:
       title: "Misskist III"
-      description: "Fez login por um total de 100 dias"
+      description: "Faça login por um total de 100 dias"
       flavor: "Misskist violento"
     _login200:
       title: "Freguês I"
-      description: "Fez login por um total de 200 dias"
+      description: "Faça login por um total de 200 dias"
     _login300:
       title: "Freguês II"
-      description: "Fez login por um total de 300 dias"
+      description: "Faça login por um total de 300 dias"
     _login400:
       title: "Freguês III"
-      description: "Fez login por um total de 400 dias"
+      description: "Faça login por um total de 400 dias"
     _login500:
       title: "Veterano I"
-      description: "Fez login por um total de 500 dias"
+      description: "Faça login por um total de 500 dias"
       flavor: "Cavalheiros, tudo o que peço são notas"
     _login600:
       title: "Veterano II"
-      description: "Fez login por um total de 600 dias"
+      description: "Faça login por um total de 600 dias"
     _login700:
       title: "Veterano III"
-      description: "Fez login por um total de 700 dias"
+      description: "Faça login por um total de 700 dias"
     _login800:
-      title: "Mestre das notas I"
-      description: "Fez login por um total de 800 dias"
+      title: "Mestre das Notas I"
+      description: "Faça login por um total de 800 dias"
     _login900:
-      title: "Mestre das notas II"
-      description: "Fez login por um total de 900 dias"
+      title: "Mestre das Notas II"
+      description: "Faça login por um total de 900 dias"
     _login1000:
-      title: "Mestre das notas III"
-      description: "Fez login por um total de 1000 dias"
+      title: "Mestre das Notas III"
+      description: "Faça login por um total de 1 000 dias"
       flavor: "Obrigado por utilizar o Misskey!"
     _noteClipped1:
-      title: "Não posso deixar de adicionar ao clipe"
-      description: "Adicionou a um clipe a sua primeira nota"
+      title: "Preciso... clipar..."
+      description: "Adicione a um clipe a sua primeira nota"
     _noteFavorited1:
-      title: "Astrônomo amador"
-      description: "Adicionou uma nota aos favoritos pela primeira vez"
+      title: "Astrônomo Amador"
+      description: "Adicione uma nota aos favoritos pela primeira vez"
     _myNoteFavorited1:
       title: "Cabeça nas estrelas"
-      description: "Teve uma das suas notas adicionada aos favoritos de alguém"
+      description: "Tenha uma das suas notas adicionada aos favoritos de alguém"
     _profileFilled:
-      title: "Tudo pronto"
-      description: "Configurou o seu perfil"
+      title: "Tudo Pronto"
+      description: "Configure o seu perfil"
     _markedAsCat:
       title: "Eu Sou Um Gato"
-      description: "Marcou a sua conta como um gato"
+      description: "Marque a sua conta como um gato"
       flavor: "Ainda não tenho um nome."
     _following1:
       title: "Primeira vez seguindo alguém"
-      description: "Seguiu um usuário pela primeira vez"
+      description: "Siga um usuário pela primeira vez"
     _following10:
       title: "Circulando, circulando"
-      description: "Seguiu 10 usuários"
+      description: "Siga 10 usuários"
     _following50:
       title: "Muitos amigos"
-      description: "Seguiu 50 usuários"
+      description: "Siga 50 usuários"
     _following100:
-      title: "100 amigos"
-      description: "Seguiu 100 usuários"
+      title: "100 Amigos"
+      description: "Siga 100 usuários"
     _following300:
       title: "Sobrecarga de amigos"
-      description: "Seguiu 300 usuários"
+      description: "Siga 300 usuários"
     _followers1:
       title: "Primeiro seguidor"
-      description: "Ganhou o seu primeiro seguidor"
+      description: "Ganhe o seu primeiro seguidor"
     _followers10:
       title: "Sigam-me os bons!"
-      description: "Ganhou 10 seguidores"
+      description: "Ganhe 10 seguidores"
     _followers50:
       title: "Aos montes"
-      description: "Ganhou 50 seguidores"
+      description: "Ganhe 50 seguidores"
     _followers100:
       title: "Popular"
-      description: "Ganhou 100 seguidores"
+      description: "Ganhe 100 seguidores"
     _followers300:
       title: "Em fila única, por favor"
-      description: "Ganhou 300 seguidores"
+      description: "Ganhe 300 seguidores"
     _followers500:
       title: "Torre de celular"
-      description: "Ganhou 500 seguidores"
+      description: "Ganhe 500 seguidores"
     _followers1000:
       title: "Influencer"
-      description: "Ganhou 1000 seguidores"
+      description: "Ganhe 1 000 seguidores"
+    _collectAchievements30:
+      title: "Coletor de Conquistas"
+      description: "Ganhe 30 conquistas"
+    _viewAchievements3min:
+      title: "Curte Conquistas"
+      description: "Olhe para a sua lista de conquistas por pelo menos 3 minutos"
+    _iLoveMisskey:
+      title: "Eu Amo Misskey"
+      description: "Poste \"I ❤ #Misskey\""
+      flavor: "A equipe de desenvolvimento do Misskey aprecia profundamente o seu apoio!"
+    _foundTreasure:
+      title: "Caça ao Tesouro"
+      description: "Você achou o tesouro escondido"
+    _client30min:
+      title: "Pausinha"
+      description: "Deixe o Misskey aberto por pelo menos 30 minutos"
+    _client60min:
+      title: "Sem falta"
+      description: "Deixe o Misskey aberto por pelo menos 60 minutos"
     _noteDeletedWithin1min:
       title: "Deixa pra lá"
-      description: "Excluí a postagem dentro de 1 minuto após ter publicado"
+      description: "Exclua a postagem dentro de 1 minuto após a ter publicado"
+    _postedAtLateNight:
+      title: "Noturno"
+      description: "Poste uma nota tarde da noite"
+      flavor: "Tá na hora de ir dormir."
+    _postedAt0min0sec:
+      title: "Relógio Falante"
+      description: "Poste uma nota à meia-noite em ponto"
+      flavor: "Tic-Tac-Tic-Tac"
+    _selfQuote:
+      title: "Autorreferência"
+      description: "Cite sua própria nota"
+    _htl20npm:
+      title: "Linha do Tempo Fluida"
+      description: "Faça a velocidade da linha do tempo exceder 20 npm (notas por minuto)"
+    _viewInstanceChart:
+      title: "Analista"
+      description: "Veja os infográficos da instância"
+    _outputHelloWorldOnScratchpad:
+      title: "Olá, Mundo!"
+      description: "Produza \"hello world\" no Scratchpad"
+    _open3windows:
+      title: "Múlti-Janelas"
+      description: "Tenha ao mínimo 3 janelas abertas simultaneamente."
     _driveFolderCircularReference:
       title: "Referência circular"
+      description: "Tente criar uma pasta recursiva no Drive."
+    _reactWithoutRead:
+      title: "Você leu tudo isso?"
+      description: "Reaja a uma nota com mais de 100 caracteres dentro de 3 segundos após a sua publicação."
+    _clickedClickHere:
+      title: "Clique aqui"
+      description: "Você clicou aqui"
+    _justPlainLucky:
+      title: "Pura Sorte"
+      description: "Tem uma chance de ser obtido com uma probabilidade de 0.005% a cada 10 segundos."
+    _setNameToSyuilo:
+      title: "Complexo de Deus"
+      description: "Colocar seu nome como \"syuilo\""
+    _passedSinceAccountCreated1:
+      title: "Aniversário de Um Ano"
+      description: "Um ano passou-se desde a criação da conta"
+    _passedSinceAccountCreated2:
+      title: "Aniversário de Dois Anos"
+      description: "Dois anos passaram-se desde a criação da conta"
+    _passedSinceAccountCreated3:
+      title: "Aniversário de Três Anos"
+      description: "Três anos passaram-se desde a criação da conta"
+    _loggedInOnBirthday:
+      title: "Feliz Aniversário"
+      description: "Entre no dia do seu aniversário"
+    _loggedInOnNewYearsDay:
+      title: "Feliz Ano Novo!"
+      description: "Entre no primeiro dia do ano"
+      flavor: "Para outro ótimo ano nessa instância"
+    _cookieClicked:
+      title: "Um jogo onde você clica em cookies"
+      description: "Clicou o cookie"
+      flavor: "Pera, você tá no website correto?"
+    _brainDiver:
+      title: "Brain Diver"
+      description: "Poste o link do Brain Diver"
+      flavor: "Misskey-Misskey La-Tu-Ma"
+    _smashTestNotificationButton:
+      title: "Teste de Transbordamento"
+      description: "Ative o teste de notificações repetidamente dentro de um curto período de tempo"
+    _tutorialCompleted:
+      title: "Diploma de Ensino Fundamental Misskey"
+      description: "Complete o tutorial"
+    _bubbleGameExplodingHead:
+      title: "🤯"
+      description: "O maior objeto no Bubble Game"
+    _bubbleGameDoubleExplodingHead:
+      title: "🤯 Duplo"
+      description: "Dois dos maiores objetos do Bubble Game ao mesmo tempo."
+      flavor: "Dá para encher uma lancheira com esses 🤯🤯."
 _role:
   new: "Novo cargo"
   edit: "Editar cargo"
@@ -1195,8 +1672,10 @@ _role:
   descriptionOfPermission: "<b>Moderador</b> permite que você execute operações básicas relacionadas à moderação.\n<b>Administradores</b> podem alterar todas as configurações do servidor."
   assignTarget: "Atribuir"
   descriptionOfAssignTarget: "<b>Manual</b> para gerenciar manualmente quem está incluído neste cargo.\n<b>Condicional</b> define uma condição e os usuários que corresponderem a ela serão incluídos automaticamente."
-  manual: "Documentação"
+  manual: "Manual"
+  manualRoles: "Cargos manuais"
   conditional: "Condicional"
+  conditionalRoles: "Cargos condicionais"
   condition: "Condição"
   isConditionalRole: "Este é um cargo condicional."
   isPublic: "Cargo público"
@@ -1224,13 +1703,16 @@ _role:
     gtlAvailable: "Visualizar Linha do Tempo Global"
     ltlAvailable: "Visualizar Linha do Tempo Local"
     canPublicNote: "Permitir postagem pública"
+    mentionMax: "Número máximo de menções em uma nota"
     canInvite: "Permitir a criação de códigos de convites para a instância"
     inviteLimit: "Limite de códigos de convite"
     inviteLimitCycle: "Intervalo de emissão do código de convite"
     inviteExpirationTime: "Prazo de validade do código de convite"
     canManageCustomEmojis: "Permitir gerenciar emojis personalizados"
+    canManageAvatarDecorations: "Gerenciar decorações de avatar"
     driveCapacity: "Capacidade do drive"
     alwaysMarkNsfw: "Sempre marcar arquivos como NSFW"
+    canUpdateBioMedia: "Permitir a edição de ícone ou imagem do banner."
     pinMax: "Número máximo de notas fixadas"
     antennaMax: "Número máximo de antenas"
     wordMuteMax: "Número máximo de caracteres nas palavras silenciadas"
@@ -1243,9 +1725,17 @@ _role:
     descriptionOfRateLimitFactor: "Valores menores são menos restritivos, valores maiores são mais restritivos."
     canHideAds: "Permitir ocultar anúncios"
     canSearchNotes: "Permitir a busca de notas"
+    canUseTranslator: "Uso do tradutor"
+    avatarDecorationLimit: "Número máximo de decorações de avatar que podem ser aplicadas"
   _condition:
+    roleAssignedTo: "Atribuído a cargos manuais"
     isLocal: "Usuário local"
     isRemote: "Usuário remoto"
+    isCat: "Usuários Gatinho"
+    isBot: "Usuários Bot"
+    isSuspended: "Usuário suspenso"
+    isLocked: "Contas privadas"
+    isExplorable: "Encontrável em \"Explorar\""
     createdLessThan: "Menos de X passados desde a criação da conta"
     createdMoreThan: "Mais de X passados desde a criação da conta"
     followersLessThanOrEq: "Possui X ou menos seguidores"
@@ -1259,13 +1749,19 @@ _role:
     not: "Não ~ (Condicional)"
 _sensitiveMediaDetection:
   description: "Use o aprendizado de máquina para detectar automaticamente mídias sensíveis para moderação. Isso pode aumentar ligeiramente a carga no servidor."
+  sensitivity: "Detecção de sensibilidade"
   sensitivityDescription: "Ao reduzir a sensibilidade, as detecções incorretas (falsos positivos) diminuem. Ao aumentar a sensibilidade, as falhas de detecção (falsos negativos) diminuem."
+  setSensitiveFlagAutomatically: "Marcar como sensível"
+  setSensitiveFlagAutomaticallyDescription: "Os resultados da detecção interna serão mantidos mesmo se essa opção estiver desligada."
+  analyzeVideos: "Habilitar análise de vídeos"
+  analyzeVideosDescription: "Analisa vídeos em adição a imagens. Isso irá aumentar levemente a carga do servidor."
 _emailUnavailable:
   used: "O endereço de e-mail informado já está sendo utilizado"
   format: "Formado de e-mail inválido"
   disposable: "Endereços de e-mail descartáveis não devem ser utilizados"
   mx: "O servidor de informado é inválido"
   smtp: "O servidor de e-mail não está respondendo"
+  banned: "Você não pode se cadastrar com esse endereço de email"
 _ffVisibility:
   public: "Público"
   followers: "Visível apenas para seguidores"
@@ -1285,10 +1781,17 @@ _ad:
   back: "Voltar"
   reduceFrequencyOfThisAd: "Diminuir frequência deste anúncio"
   hide: "Não exibir anúncios"
+  timezoneinfo: "O dia da semana é determinado pelo fuso horário do servidor."
+  adsSettings: "Configurações de propaganda"
+  notesPerOneAd: "Intervalo de notas entre o anúncio nas atualizações em tempo real."
+  setZeroToDisable: "Selecione o valor 0 para desabilitar anúncios nas atualizações em tempo real."
+  adsTooClose: "O intervalo atual de anúncio pode impactar negativamente a experiência de usuário por ser muito baixo."
 _forgotPassword:
   enterEmail: "Por favor, insira o endereço de e-mail usado no cadastro de sua conta. Um link para redefinição de senha será enviado para esse endereço."
   ifNoEmail: "Caso você não tenha registrado um endereço de e-mail, por favor, entre em contato com o administrador."
+  contactAdmin: "Essa instância não possui suporte ao uso de endereços de email, contate seu administrador para mudar a sua senha."
 _gallery:
+  my: "Minha Galeria"
   liked: "Postagens curtidas"
   like: "Curtir"
   unlike: "Remover curtida"
@@ -1297,40 +1800,223 @@ _email:
     title: "Você tem um novo seguidor"
   _receiveFollowRequest:
     title: "Você recebeu um pedido de seguidor"
+_plugin:
+  install: "Instalar plugins"
+  installWarn: "Por favor, não instale plugins duvidosos."
+  manage: "Gerenciar plugins"
+  viewSource: "Ver código-fonte"
+  viewLog: "Mostrar registo"
 _preferencesBackups:
+  list: "Backups criados"
+  saveNew: "Salvar novo backup"
+  loadFile: "Carregar de arquivo"
+  apply: "Aplicar a este dispositivo"
+  save: "Salvar mudanças"
+  inputName: "Insira um nome para esse backup"
   cannotSave: "Não foi possível salvar"
+  nameAlreadyExists: "Um backup chamado \"{name}\" já existe. Por favor, insira outro nome."
   applyConfirm: "Deseja aplicar o backup '{name}' ao dispositivo atual? As configurações atuais do dispositivo serão perdidas."
+  saveConfirm: "Salvar backup como \"{name}\"?"
   deleteConfirm: "Deseja excluir {name}?"
+  renameConfirm: "Renomear esse backup de \"{old}\" para \"{new}\"?"
+  noBackups: "Não há backups. Você pode configurar suas configurações de cliente nesse servidor ao selecionar \"Criar novo backup\"."
+  createdAt: "Criado em: {date} {time}"
+  updatedAt: "Atualizado em: {date} {time}"
   cannotLoad: "Não foi possível carregar"
+  invalidFile: "Formato de arquivo inválido"
+_registry:
+  scope: "Escopo"
+  key: "Chave"
+  keys: "Chave"
+  domain: "Domínio"
+  createKey: "Criar chave"
+_aboutMisskey:
+  about: "Misskey é um software de código aberto desenvolvido por syulio desde 2014."
+  contributors: "Contribuidores principais"
+  allContributors: "Todos os contribuidores"
+  source: "Código-fonte"
+  original: "Original"
+  thisIsModifiedVersion: "{name} utiliza uma versão modificada do Misskey original."
+  translation: "Traduza o Misskey"
+  donate: "Doe para o Misskey"
+  morePatrons: "Nós apreciamos o apoio de vários outros apoiadores não listados aqui. Obrigado! 🥰"
+  patrons: "Apoiadores"
+  projectMembers: "Membros do projeto"
+_displayOfSensitiveMedia:
+  respect: "Esconder mídia marcada como sensível"
+  ignore: "Exibir mídia marcada como sensível"
+  force: "Esconder toda mídia"
+_instanceTicker:
+  none: "Nunca mostrar"
+  remote: "Mostrar para usuários remotos"
+  always: "Sempre mostrar"
+_serverDisconnectedBehavior:
+  reload: "Recarregar automaticamente"
+  dialog: "Exibir diálogo de aviso de conteúdo"
+  quiet: "Exibir aviso de conteúdo discreto"
 _channel:
+  create: "Criar canal"
+  edit: "Editar canal"
+  setBanner: "Definir banner"
+  removeBanner: "Remover banner"
   featured: "Destaques"
+  owned: "Autoral"
   following: "Seguindo"
   usersCount: "{n} usuários ativos"
   notesCount: "{n} notas"
   nameAndDescription: "Nome e descrição"
+  nameOnly: "Apenas o nome"
+  allowRenoteToExternal: "Permitir repostagens e citações de fora do canal"
 _menuDisplay:
   sideFull: "Exibir painel lateral inteiro"
+  sideIcon: "Lateral (Ícones)"
   top: "Exibir barra superior"
   hide: "Ocultar"
+_wordMute:
+  muteWords: "Palavras silenciadas"
+  muteWordsDescription: "Separe com espaços para uma condicional AND (&&) ou por linha para uma condicional OR (||)."
+  muteWordsDescription2: "Cercar palavras-chave com barras para usar expressões regulares (RegEx)."
 _instanceMute:
   instanceMuteDescription: "Todas as notas e repostagens do servidor configurado serão silenciados, incluindo respostas aos usuários do servidor mutado."
+  instanceMuteDescription2: "Separar por linha"
+  title: "Esconder notas das instâncias listadas. "
+  heading: "Lista de instâncias a serem silenciadas"
 _theme:
+  explore: "Explorar Temas"
+  install: "Instalar um tema"
+  manage: "Gerenciar temas"
+  code: "Código do tema"
   description: "Descrição"
+  installed: "{name} foi instalado"
+  installedThemes: "Temas instalados"
+  builtinThemes: "Temas nativos"
+  alreadyInstalled: "Esse tema já foi instalado"
+  invalid: "O formato desse tema é invalido"
+  make: "Fazer um tema"
+  base: "Base"
+  addConstant: "Adicionar constante"
+  constant: "Constante"
+  defaultValue: "Valor padrão"
+  color: "Cor"
+  refProp: "Referenciar uma propriedade"
+  refConst: "Referenciar uma constante"
+  key: "Chave"
+  func: "Funções"
+  funcKind: "Tipo de função"
+  argument: "Argumento"
+  basedProp: "Propriedade referenciada"
   alpha: "Opacidade"
+  darken: "Escurecer"
+  lighten: "Esclarecer"
+  inputConstantName: "Insira um nome para essa constante"
+  importInfo: "Se você inserir o código do tema aqui, você pode importá-lo no editor de temas"
   deleteConstantConfirm: "Confirma a exclusão da constante {const}?"
   keys:
+    accent: "Cor de destaque"
+    bg: "Plano de fundo"
+    fg: "Texto"
+    focus: "Foco"
+    indicator: "Indicador"
+    panel: "Painel"
+    shadow: "Sombra"
+    header: "Cabeçalho"
+    navBg: "Plano de fundo da barra lateral"
+    navFg: "Texto da barra lateral"
+    navHoverFg: "Texto da coluna lateral (Selecionado)"
+    navActive: "Texto da coluna lateral (Ativa)"
+    navIndicator: "Indicador da coluna lateral"
+    link: "Link"
+    hashtag: "Hashtag"
     mention: "Menção"
+    mentionMe: "Menciona (a mim)"
     renote: "Repostar"
+    modalBg: "Plano de fundo modal"
     divider: "Separador"
+    scrollbarHandle: "Alça da barra de rolagem (Selecionada)"
+    scrollbarHandleHover: "Alça da barra de rolagem (Selecionada)"
+    dateLabelFg: "Texto do rótulo de data"
+    infoBg: "Plano de fundo de informações"
+    infoFg: "Texto de informações"
+    infoWarnBg: "Plano de fundo de avisos"
+    infoWarnFg: "Texto de avisos"
+    toastBg: "Plano de fundo de notificações"
+    toastFg: "Texto da notificação"
+    buttonBg: "Plano de fundo de botão"
+    buttonHoverBg: "Plano de fundo de botão (Selecionado)"
+    inputBorder: "Borda de campo digitável"
+    driveFolderBg: "Plano de fundo da pasta no Drive"
+    wallpaperOverlay: "Sobreposição do papel de parede."
+    badge: "Emblema"
+    messageBg: "Plano de fundo do chat"
+    accentDarken: "Cor de destaque (Escurecida)"
+    accentLighten: "Cor de destaque (Esclarecida)"
+    fgHighlighted: "Texto Destacado"
 _sfx:
   note: "Posts"
+  noteMy: "Própria nota"
   notification: "Notificações"
+  reaction: "Ao selecionar uma reação"
+_soundSettings:
+  driveFile: "Usar um arquivo de áudio do Drive."
+  driveFileWarn: "Selecione um arquivo de áudio do Drive."
+  driveFileTypeWarn: "Esse arquivo não é compatível"
+  driveFileTypeWarnDescription: "Selecione um arquivo de áudio"
+  driveFileDurationWarn: "O áudio é muito longo."
+  driveFileDurationWarnDescription: "Áudios longos podem atrapalhar o funcionamento do Misskey. Deseja continuar?"
+  driveFileError: "Não foi possível carregar o som. Por favor, altere a configuração."
 _ago:
+  future: "Futuro"
+  justNow: "Agora mesmo"
+  secondsAgo: "{n}s atrás"
+  minutesAgo: "{n}m atrás"
+  hoursAgo: "{n}h atrás"
+  daysAgo: "{n}d atrás"
+  weeksAgo: "{n} semanas atrás"
+  monthsAgo: "{n} meses atrás"
+  yearsAgo: "{n} anos atrás"
   invalid: "Não há nada aqui"
+_timeIn:
+  seconds: "Em {n}s"
+  minutes: "Em {n}m"
+  hours: "Em {n}h"
+  days: "Em {n}d"
+  weeks: "Em {n} semanas"
+  months: "Em {n} meses"
+  years: "Em {n} anos"
+_time:
+  second: "Segundo(s)"
+  minute: "Minuto(s)"
+  hour: "Hora(s)"
+  day: "Dia(s)"
 _2fa:
+  alreadyRegistered: "Você já cadastrou um dispositivo de autenticação de dois fatores."
+  registerTOTP: "Cadastrar aplicativo autenticador"
+  step1: "Inicialmente, instale um aplicativo autenticador (como {a} ou {b}) em seu dispositivo."
+  step2: "Então, escaneie o código QR exibido na tela."
+  step2Uri: "Acesse o seguinte URI se você estiver utilizando um aplicativo no computador"
+  step3Title: "Insira o código de autenticação"
+  step3: "Insira o código de autenticação (token) providenciado pelo seu aplicativo para terminar a configuração."
+  setupCompleted: "Configuração completa"
+  step4: "De agora em diante, quaisquer solicitações de entrada pedirão pelo código."
+  securityKeyNotSupported: "O seu navegador não é compatível com chaves de segurança."
+  registerTOTPBeforeKey: "Por favor, configure um aplicativo autenticador para registrar uma chave de segurança."
   securityKeyInfo: "Além da autenticação por impressão digital ou PIN, você também pode configurar a autenticação por chaves de segurança de hardware compatível com FIDO2 para proteger ainda mais a sua conta."
+  registerSecurityKey: "Registre um código de segurança"
+  securityKeyName: "Insira um nome para a chave"
+  tapSecurityKey: "Por favor, siga as instruções do navegador para registrar o código de segurança"
+  removeKey: "Remover código de segurança"
   removeKeyConfirm: "Deseja excluir {name}?"
+  whyTOTPOnlyRenew: "O autenticador não pode ser removido enquanto há códigos de segurança registrados."
+  renewTOTP: "Reconfigurar autenticador"
+  renewTOTPConfirm: "Isso interromperá o funcionamento dos códigos de aplicativos anteriores "
+  renewTOTPOk: "Reconfigurar"
   renewTOTPCancel: "Não, obrigado"
+  checkBackupCodesBeforeCloseThisWizard: "Antes de fechar essa janela, anote os códigos de backup a seguir."
+  backupCodes: "Códigos de backup"
+  backupCodesDescription: "Você pode utilizar esses códigos para ganhar acesso à conta caso sua autenticação de dois fatores esteja indisponível. Cada código pode ser utilizado apenas uma vez. Por favor, guarde-os em um local seguro."
+  backupCodeUsedWarning: "Um código de backup foi utilizado. Por favor, reconfigure a autenticação de dois fatores o quanto antes, caso não consiga utilizá-la."
+  backupCodesExhaustedWarning: "Todos os códigos de backup foram utilizados. Caso perca acesso à autenticação de dois fatores, você perderá o acesso à conta. Por favor, reconfigure a autenticação de dois fatores."
+  moreDetailedGuideHere: "Aqui está um guia detalhado"
 _permissions:
   "read:account": "Visualizar informações da conta"
   "write:account": "Editar informações da conta"
@@ -1364,6 +2050,82 @@ _permissions:
   "write:gallery": "Editar sua galeria"
   "read:gallery-likes": "Visualizar a sua lista de curtidas da galeria"
   "write:gallery-likes": "Editar a sua lista de curtidas da galeria"
+  "read:flash": "Ver Play"
+  "write:flash": "Editar Plays"
+  "read:flash-likes": "Ver lista de Plays curtidas"
+  "write:flash-likes": "Editar lista de Plays curtidas"
+  "read:admin:abuse-user-reports": "Ver relatórios de usuário"
+  "write:admin:delete-account": "Excluir conta de usuário"
+  "write:admin:delete-all-files-of-a-user": "Excluir todos os arquivos de um usuário"
+  "read:admin:index-stats": "Ver estatísticas do índice do banco de dados"
+  "read:admin:table-stats": "Ver estatísticas da tabela do banco de dados"
+  "read:admin:user-ips": "Ver endereços IP do usuário"
+  "read:admin:meta": "Ver metadados da instância"
+  "write:admin:reset-password": "Mudar a senha do usuário"
+  "write:admin:resolve-abuse-user-report": "Resolver relatório de usuário"
+  "write:admin:send-email": "Enviar email"
+  "read:admin:server-info": "Ver informações do servidor"
+  "read:admin:show-moderation-log": "Ver log de moderação"
+  "read:admin:show-user": "Ver informações privadas do usuário"
+  "write:admin:suspend-user": "Suspender usuário"
+  "write:admin:unset-user-avatar": "Remover avatar do usuário"
+  "write:admin:unset-user-banner": "Remover banner do usuário"
+  "write:admin:unsuspend-user": "Cancelar a suspensão do usuário"
+  "write:admin:meta": "Gerenciar os metadados da instância"
+  "write:admin:user-note": "Gerenciar a nota de moderação"
+  "write:admin:roles": "Gerenciar cargos"
+  "read:admin:roles": "Ver cargos"
+  "write:admin:relays": "Gerenciar relays"
+  "read:admin:relays": "Ver relays"
+  "write:admin:invite-codes": "Gerenciar códigos de convite"
+  "read:admin:invite-codes": "Ver códigos de convite"
+  "write:admin:announcements": "Gerenciar anúncios"
+  "read:admin:announcements": "Ver anúncios"
+  "write:admin:avatar-decorations": "Gerenciar decorações de avatar"
+  "read:admin:avatar-decorations": "Ver decorações de avatar"
+  "write:admin:federation": "Gerenciar dados de federação"
+  "write:admin:account": "Gerenciar conta de usuário"
+  "read:admin:account": "Ver conta de usuário"
+  "write:admin:emoji": "Gerenciar emoji"
+  "read:admin:emoji": "Ver emoji"
+  "write:admin:queue": "Gerenciar trabalhos pendentes"
+  "read:admin:queue": "Ver informações de trabalhos pendentes"
+  "write:admin:promo": "Gerenciar notas de promoção"
+  "write:admin:drive": "Gerenciar Drive de usuário"
+  "read:admin:drive": "Ver informações de Drive de usuário"
+  "read:admin:stream": "Utilizar WebSocket API para Admin"
+  "write:admin:ad": "Gerenciar propagandas"
+  "read:admin:ad": "Ver propagandas"
+  "write:invite-codes": "Criar códigos de convite"
+  "read:invite-codes": "Obter códigos de convite"
+  "write:clip-favorite": "Gerenciar clipes favoritados"
+  "read:clip-favorite": "Ver Clipes favoritados"
+  "read:federation": "Ver dados de federação"
+  "write:report-abuse": "Reportar violação"
+_auth:
+  shareAccessTitle: "Conceder permissões do aplicativo"
+  shareAccess: "Você gostaria de autorizar \"{name}\" para acessar essa conta?"
+  shareAccessAsk: "Você tem certeza de que gostaria de conceder ao aplicativo o acesso à conta?"
+  permission: "{name} solicita as seguintes permissões"
+  permissionAsk: "O aplicativo solicita as seguintes permissões"
+  pleaseGoBack: "Por favor, volte ao aplicativo"
+  callback: "Retornando ao aplicativo"
+  denied: "Acesso negado"
+  pleaseLogin: "Por favor, entre para autorizar aplicativos."
+_antennaSources:
+  all: "Todas as notas"
+  homeTimeline: "Notas de usuários seguidos"
+  users: "Notas de usuários específicos"
+  userList: "Notas de uma lista específica de usuários"
+  userBlacklist: "Todas as notas, exceto as de um ou mais usuários específicos"
+_weekday:
+  sunday: "Domingo"
+  monday: "Segunda-feira"
+  tuesday: "Terça-feira"
+  wednesday: "Quarta-feira"
+  thursday: "Quinta-feira"
+  friday: "Sexta-feira"
+  saturday: "Sábado"
 _widgets:
   profile: "Perfil"
   instanceInfo: "Informações da instância"
@@ -1394,29 +2156,112 @@ _widgets:
   _userList:
     chooseList: "Selecione uma lista"
   clicker: "Clicker"
+  birthdayFollowings: "Usuários de aniversário hoje"
 _cw:
+  hide: "Esconder"
   show: "Carregar mais"
+  chars: "{count} caracteres"
+  files: "{count} arquivo(s)"
 _poll:
+  noOnlyOneChoice: "São necessárias, no mínimo, duas escolhas"
+  choiceN: "Escolha {n}"
+  noMore: "Você não pode adicionar mais escolhas"
   canMultipleVote: "Permitir múltipla seleção"
+  expiration: "Encerrar enquete"
+  infinite: "Nunca"
+  at: "Terminar em..."
+  after: "Terminar após..."
+  deadlineDate: "Data de término"
+  deadlineTime: "Tempo"
+  duration: "Duração"
+  votesCount: "{n} votos"
+  totalVotes: "{n} votos totais"
   vote: "Votar em enquetes"
+  showResult: "Ver resultados"
+  voted: "Votada"
+  closed: "Encerrada"
+  remainingDays: "{d} dia(s) {h} hora(s) restantes"
+  remainingHours: "{h} hora(s) {m} minuto(s) restantes"
+  remainingMinutes: "{m} minuto(s) {s} segundo(s) restantes"
+  remainingSeconds: "{s} segundo(s) restantes"
 _visibility:
+  public: "Público"
+  publicDescription: "Sua nota será visível para todos os usuários"
   home: "Início"
+  homeDescription: "Publicar apenas na linha do tempo Início"
   followers: "Seguidores"
   followersDescription: "Tornar visível apenas para os meus seguidores"
+  specified: "Mensagem Direta"
+  specifiedDescription: "Tornar visível apenas para usuários específicos"
+  disableFederation: "Defederar"
+  disableFederationDescription: "Não transmitir às outras instâncias"
+_postForm:
+  replyPlaceholder: "Responder a essa nota..."
+  quotePlaceholder: "Citar essa nota..."
+  channelPlaceholder: "Postar em canal..."
+  _placeholders:
+    a: "Como vão as coisas?"
+    b: "O que está rolando por aí?"
+    c: "No que está pensando?"
+    d: "Do que você quer falar?"
+    e: "Comece a digitar..."
+    f: "Esperando você digitar..."
 _profile:
   name: "Nome"
   username: "Nome de usuário"
+  description: "Bio"
+  youCanIncludeHashtags: "Você pode incluir hashtags em sua bio."
+  metadata: "Informações Adicionais"
+  metadataEdit: "Editar informações adicionais"
+  metadataDescription: "Aqui, você pode exibir campos adicionais de informação no seu perfil."
+  metadataLabel: "Rótulo"
+  metadataContent: "Conteúdo"
+  changeAvatar: "Mudar avatar"
+  changeBanner: "Mudar banner"
+  verifiedLinkDescription: "Ao inserir um URL que contém um link para essa conta, um ícone de verificação será exibido ao lado do campo"
+  avatarDecorationMax: "Você pode adicionar até {max} decorações."
 _exportOrImport:
+  allNotes: "Todas as notas"
   favoritedNotes: "Notas nos favoritos"
   clips: "Clipe"
   followingList: "Seguindo"
   muteList: "Silenciar"
   blockingList: "Bloquear"
   userLists: "Listas"
+  excludeMutingUsers: "Excluir usuários silenciados"
+  excludeInactiveUsers: "Excluir usuários inativos"
+  withReplies: "Incluir respostas de usuários importados na linha do tempo"
 _charts:
   federation: "União"
+  apRequest: "Solicitações"
+  usersIncDec: "Diferença no número de usuários"
+  usersTotal: "Número total de usuários"
+  activeUsers: "Usuários ativos"
+  notesIncDec: "Diferença no número de notas"
+  localNotesIncDec: "Diferença no número de notas locais"
+  remoteNotesIncDec: "Diferença no número de notas remotas"
+  notesTotal: "Número total de notas"
+  filesIncDec: "Diferença no número de arquivos"
+  filesTotal: "Número total de arquivos"
+  storageUsageIncDec: "Diferença no uso de armazenamento"
+  storageUsageTotal: "Uso total de armazenamento"
+_instanceCharts:
+  requests: "Solicitações"
+  users: "Diferença no número de usuários"
+  usersTotal: "Número cumulativo de usuários"
+  notes: "Diferença no número de notas"
+  notesTotal: "Número cumulativo de notas"
+  ff: "Diferença entre número de usuários seguidos/seguidores"
+  ffTotal: "Número cumulativo de usuários seguidos/seguidores"
+  cacheSize: "Diferença do tamanho do cache"
+  cacheSizeTotal: "Tamanho cumulativo do cache"
+  files: "Diferença no número de arquivos"
+  filesTotal: "Número cumulativo de arquivos"
 _timelines:
   home: "Início"
+  local: "Local"
+  social: "Social"
+  global: "Global"
 _play:
   new: "Criar  Play"
   edit: "Editar Play"
@@ -1425,18 +2270,66 @@ _play:
   deleted: "Play foi excluído"
   pageSetting: "Configurações do Play"
   editThisPage: "Editar este Play"
+  viewSource: "Ver fonte"
   my: "Meus Plays"
   liked: "Plays curtidos"
+  featured: "Popular"
+  title: "Título"
   script: "Script"
   summary: "Descrição"
+  visibilityDescription: "Pôr em privado significa que ele não será visível no perfil, mas qualquer um com o URL poderá acessar"
 _pages:
+  newPage: "Criar uma Página"
+  editPage: "Editar essa Página"
+  readPage: "Ver a fonte dessa Página"
+  created: "Página criada com sucesso"
+  updated: "Página atualizada com sucesso"
   deleted: "Página excluída com sucesso"
+  pageSetting: "Configurações da página"
+  nameAlreadyExists: "O URL de Página especificado já existe"
+  invalidNameTitle: "O URL de Página especificado é inválido"
+  invalidNameText: "Confira se o título da Página não está vazio"
+  editThisPage: "Editar essa Página"
+  viewSource: "Ver código-fonte"
   viewPage: "Visualizar as suas páginas"
   like: "Curtir"
   unlike: "Remover curtida"
+  my: "Minhas Páginas"
   liked: "Páginas curtidas"
+  featured: "Populares"
+  inspector: "Inspetor"
+  contents: "Conteúdo"
+  content: "Bloco da Página"
+  variables: "Variáveis"
+  title: "Título"
+  url: "URL da Página"
+  summary: "Resumo da página"
+  alignCenter: "Centralizar elementos"
+  hideTitleWhenPinned: "Esconder título da Página quando fixado em perfil"
+  font: "Fonte"
+  fontSerif: "Serif"
+  fontSansSerif: "Sans Serif"
+  eyeCatchingImageSet: "Escolher miniatura"
+  eyeCatchingImageRemove: "Excluir miniatura"
+  chooseBlock: "Adicionar bloco"
+  enterSectionTitle: "Insira um título à seção"
+  selectType: "Selecionar um tipo"
+  contentBlocks: "Conteúdo"
+  inputBlocks: "Inserir"
+  specialBlocks: "Especial"
   blocks:
+    text: "Texto"
+    textarea: "Área do texto"
+    section: "Seção"
     image: "imagem"
+    button: "Botão"
+    dynamic: "Blocos Dinâmicos"
+    dynamicDescription: "Esse bloco foi abolido. Por favor, use {play} de agora em diante."
+    note: "Nota embutida"
+    _note:
+      id: "ID da nota"
+      idDescription: "Você também pode colar o URL da nota aqui."
+      detailed: "Visão detalhada"
 _relayStatus:
   requesting: "Pendente"
   accepted: "Aprovado"
@@ -1451,18 +2344,35 @@ _notification:
   youReceivedFollowRequest: "Você recebeu um pedido de seguidor"
   yourFollowRequestAccepted: "Seu pedido de seguidor foi aceito"
   pollEnded: "Os resultados da enquete agora estão disponíveis"
+  newNote: "Nova nota"
+  unreadAntennaNote: "Antena {name}"
+  roleAssigned: "Cargo dado"
   emptyPushNotificationMessage: "As notificações de alerta foram atualizadas"
+  achievementEarned: "Conquista desbloqueada"
+  testNotification: "Notificação teste"
+  checkNotificationBehavior: "Verificar aparência da notificação"
+  sendTestNotification: "Enviar notificação de teste"
+  notificationWillBeDisplayedLikeThis: "Notificações se parecem com isso"
+  reactedBySomeUsers: "{n} usuários reagiram"
+  likedBySomeUsers: "{n} usuários gostaram da nota"
+  renotedBySomeUsers: "{n} usuários repostaram a nota"
+  followedBySomeUsers: "{n} usuários te seguiram"
+  flushNotification: "Limpar notificações"
   _types:
     all: "Todas"
+    note: "Novas notas"
     follow: "Seguindo"
     mention: "Menção"
     reply: "Respostas"
     renote: "Repostar"
-    quote: "Citar"
+    quote: "Citações"
     reaction: "Reações"
     pollEnded: "Enquetes terminando"
     receiveFollowRequest: "Recebeu pedidos de seguidor"
     followRequestAccepted: "Aceitou pedidos de seguidor"
+    roleAssigned: "Cargo dado"
+    achievementEarned: "Conquista desbloqueada"
+    login: "Iniciar sessão"
     app: "Notificações de aplicativos conectados"
   _actions:
     followBack: "te seguiu de volta"
@@ -1472,13 +2382,23 @@ _deck:
   alwaysShowMainColumn: "Sempre mostrar a coluna principal"
   columnAlign: "Alinhar colunas"
   addColumn: "Adicionar coluna"
+  newNoteNotificationSettings: "Opções de notificação para novas notas"
+  configureColumn: "Configurar coluna"
   swapLeft: "Trocar de posição com a coluna à esquerda"
   swapRight: "Trocar de posição com a coluna à direita"
   swapUp: "Trocar de posição com a coluna acima"
   swapDown: "Trocar de posição com a coluna abaixo"
+  stackLeft: "Empilhar na coluna à esquerda"
   popRight: "Acoplar coluna à direita"
   profile: "Perfil"
+  newProfile: "Novo perfil"
   deleteProfile: "Remover perfil"
+  introduction: "Crie a interface perfeita para você arranjando as colunas livremente!"
+  introduction2: "Clique no + à direita da tela para adicionar novas colunas quando quiser."
+  widgetsIntroduction: "Por favor, selecione \"Editar widgets\" no menu em coluna e adicione um widget."
+  useSimpleUiForNonRootPages: "Usar UI simples para páginas navegadas"
+  usedAsMinWidthWhenFlexible: "A largura mínima será usada para isso quando o \"Ajuste automático da largura\" estiver ativado"
+  flexible: "Ajuste automático da largura"
   _columns:
     main: "Principal"
     widgets: "Widgets"
@@ -1490,18 +2410,230 @@ _deck:
     mentions: "Menções"
     direct: "Notas diretas"
     roleTimeline: "Linha do tempo do cargo"
+_dialog:
+  charactersExceeded: "Você excedeu o limite de caracteres! Atualmente em {current} de {max}."
+  charactersBelow: "Você está abaixo do limite mínimo de caracteres! Atualmente em {current} of {min}."
+_disabledTimeline:
+  title: "Linha do tempo desabilitada"
+  description: "Você não pode acessar essa linha do tempo sob o seu cargo atual."
 _drivecleaner:
   orderBySizeDesc: "Tamanho descendente"
   orderByCreatedAtAsc: "Data ascendente"
 _webhookSettings:
+  createWebhook: "Criar Webhook"
+  modifyWebhook: "Modificar Webhook"
   name: "Nome"
+  secret: "Segredo"
+  trigger: "Gatilho"
   active: "Ativado"
   _events:
     follow: "Quando seguindo um usuário"
     followed: "Quando sendo seguido"
+    note: "Ao postar uma nota"
+    reply: "Quando receber uma resposta"
     renote: "Quando repostado"
+    reaction: "Quando receber uma reação"
+    mention: "Quando for mencionado"
+  _systemEvents:
+    abuseReport: "Quando receber um relatório de abuso"
+    abuseReportResolved: "Quando relatórios de abuso forem resolvidos "
+    userCreated: "Quando um usuário é criado"
+  deleteConfirm: "Você tem certeza de que deseja excluir o Webhook?"
+_abuseReport:
+  _notificationRecipient:
+    createRecipient: "Adicionar destinatário para relatórios de abuso"
+    modifyRecipient: "Editar destinatários para relatórios de abuso"
+    recipientType: "TIpo de notificação"
+    _recipientType:
+      mail: "E-mail"
+      webhook: "Webhook"
+      _captions:
+        mail: "Enviar o email aos endereços dos moderadores ao receber relatório de abuso."
+        webhook: "Enviar uma notificação ao SystemWebhook quando você receber um resolver um relatório de abuso."
+    keywords: "Palavras-chave"
+    notifiedUser: "Usuários para notificar"
+    notifiedWebhook: "Webhook usado"
+    deleteConfirm: "Você tem certeza de que quer excluir o destinatário da notificação?"
 _moderationLogTypes:
+  createRole: "Cargo criado"
+  deleteRole: "Cargo excluído"
+  updateRole: "Cargo atualizado"
+  assignRole: "Cargo atribuído"
+  unassignRole: "Cargo removido"
   suspend: "Suspender"
+  unsuspend: "Suspensão cancelada"
+  addCustomEmoji: "Emoji personalizado adicionado"
+  updateCustomEmoji: "Emoji personalizado atualizado"
+  deleteCustomEmoji: "Emoji personalizado removido"
+  updateServerSettings: "Configurações de servidor atualizadas"
+  updateUserNote: "Nota de moderação atualizada"
+  deleteDriveFile: "Arquivo excluído"
+  deleteNote: "Nota excluída"
+  createGlobalAnnouncement: "Anúncio global criado"
+  createUserAnnouncement: "Anúncio de usuário criado"
+  updateGlobalAnnouncement: "Anúncio global atualizado"
+  updateUserAnnouncement: "Anúncio de usuário atualizado"
+  deleteGlobalAnnouncement: "Anúncio global excluído"
+  deleteUserAnnouncement: "Anúncio de usuário excluído"
   resetPassword: "Redefinir senha"
+  suspendRemoteInstance: "Instância remota suspensa"
+  unsuspendRemoteInstance: "Suspensão de instância remota removida"
+  updateRemoteInstanceNote: "Nota de moderação atualizada para instância remota."
+  markSensitiveDriveFile: "Arquivo marcado como sensível"
+  unmarkSensitiveDriveFile: "Arquivo desmarcado como sensível"
+  resolveAbuseReport: "Relatório resolvido"
+  createInvitation: "Convite gerado"
+  createAd: "Propaganda criada"
+  deleteAd: "Propaganda excluída"
+  updateAd: "Propaganda atualizada"
+  createAvatarDecoration: "Decoração de avatar criada"
+  updateAvatarDecoration: "Decoração de avatar atualizada"
+  deleteAvatarDecoration: "Decoração de avatar removida"
+  unsetUserAvatar: "Remover avatar de usuário"
+  unsetUserBanner: "Remover banner de usuário"
+  createSystemWebhook: "Criar SystemWebhook"
+  updateSystemWebhook: "Atualizar SystemWebhook"
+  deleteSystemWebhook: "Remover SystemWebhook"
+  createAbuseReportNotificationRecipient: "Criar um destinatário para relatórios de abuso"
+  updateAbuseReportNotificationRecipient: "Atualizar destinatários para relatórios de abuso"
+  deleteAbuseReportNotificationRecipient: "Remover um destinatário para relatórios de abuso"
+  deleteAccount: "Remover conta"
+  deletePage: "Remover página"
+  deleteFlash: "Remover Play"
+  deleteGalleryPost: "Remover a publicação da galeria"
+_fileViewer:
+  title: "Detalhes do arquivo"
+  type: "Tipo de arquivo"
+  size: "Tamanho do arquivo"
+  url: "URL"
+  uploadedAt: "Adicionado em"
+  attachedNotes: "Notas anexadas"
+  thisPageCanBeSeenFromTheAuthor: "Essa página só pode ser vista pelo usuário que enviou esse arquivo."
+_externalResourceInstaller:
+  title: "Instalar de site externo"
+  checkVendorBeforeInstall: "Tenha certeza de que o distribuidor desse recurso é confiável antes da instalação."
+  _plugin:
+    title: "Deseja instalar esse plugin?"
+    metaTitle: "Informações do plugin"
+  _theme:
+    title: "Deseja instalar esse tema?"
+    metaTitle: "Informações do tema"
+  _meta:
+    base: "Paleta de cores base"
+  _vendorInfo:
+    title: "Informações do distribuidor"
+    endpoint: "Endpoint referenciado"
+    hashVerify: "Verificação de hashes"
+  _errors:
+    _invalidParams:
+      title: "Parâmetros inválidos"
+      description: "Não há informações suficientes para carregar dados do site externo. Por favor, confirme o URL inserido."
+    _resourceTypeNotSupported:
+      title: "Esse recurso externo é incompatível"
+      description: "Esse tipo de recuso externo é incompatível. Por favor, comunique o administrador do site."
+    _failedToFetch:
+      title: "Não foi possível obter dados"
+      fetchErrorDescription: "Houve um erro ao comunicar com o site externo. Se tentar novamente não resolver o problema, contate o administrador do site."
+      parseErrorDescription: "Houve um erro processando os dados do site externo. Por favor, contate o administrador do site."
+    _hashUnmatched:
+      title: "Verificação de dados falhou"
+      description: "Houve um erro verificando a integridade do conteúdo obtido. Como medida de segurança, a instalação foi interrompida. Por favor, contate o administrador do site."
+    _pluginParseFailed:
+      title: "Erro AiScript"
+      description: "Os dados solicitados foram obtidos com sucesso, mas houve um erro na leitura do AiScript. Por favor, contate o autor do plugin. Detalhes de erro podem ser vistos no console Javascript."
+    _pluginInstallFailed:
+      title: "A instalação do plugin falhou."
+      description: "Houve um problema na instalação do plugin. Por favor, tente novamente. Detalhes de erro podem ser vistos no console Javascript."
+    _themeParseFailed:
+      title: "Erro na leitura do tema"
+      description: "Os dados solicitados foram obtidos com sucesso, mas houve um erro na leitura do tema. Por favor, contate o autor do tema. Detalhes de erro podem ser vistos no console Javascript."
+    _themeInstallFailed:
+      title: "Falha ao instalar tema"
+      description: "Houve um problema na instalação do tema. Por favor, tente novamente. Detalhes do erro podem ser vistos no console Javascript."
+_dataSaver:
+  _media:
+    title: "Carregando mídia"
+    description: "Previne que mídia seja carregada automaticamente. Mídias escondidas serão carregadas quando selecionadas."
+  _avatar:
+    title: "Imagem do avatar"
+    description: "Parar animação de avatares. Imagens animadas podem ter um arquivo mais pesado do que imagens normais, potencialmente levando a reduções no tráfego de dados."
+  _urlPreview:
+    title: "Miniaturas na prévia de URLs"
+    description: "Miniaturas na prévia de URLs não serão mais carregadas."
+  _code:
+    title: "Destaque de código"
+    description: "Se as notações de formatação de código forem utilizadas em MFM, elas não irão carregar até serem selecionadas. Destaque de código exige baixar arquivos de alta definição para cada linguagem de programação. Logo, desabilitar o carregamento automático desses arquivos diminui a quantidade de informação comunicada."
+_hemisphere:
+  N: "Hemisfério Norte"
+  S: "Hemisfério Sul"
+  caption: "Utilizado em algumas configurações de aplicativo para determinar a estação do ano."
 _reversi:
+  reversi: "Reversi"
+  gameSettings: "Configurações de jogo"
+  chooseBoard: "Escolha um tabuleiro"
+  blackOrWhite: "Preto/Branco"
+  blackIs: "{name} é as peças Pretas"
+  rules: "Regras"
+  thisGameIsStartedSoon: "O jogo começará em breve"
+  waitingForOther: "Esperando o turno do oponente"
+  waitingForMe: "Esperando o seu turno"
+  waitingBoth: "Prepare-se"
+  ready: "Pronto"
+  cancelReady: "Não pronto"
+  opponentTurn: "Turno do oponente"
+  myTurn: "Seu turno"
+  turnOf: "É o turno de {name}"
+  pastTurnOf: "Turno de {name}"
+  surrender: "Desistir"
+  surrendered: "Desistiu"
+  timeout: "Fim do tempo"
+  drawn: "Empate"
+  won: "{name} venceu"
+  black: "Preto"
+  white: "Branco"
   total: "Total"
+  turnCount: "Turno {count}"
+  myGames: "Meus jogos"
+  allGames: "Todos os jogos"
+  ended: "Terminado"
+  playing: "Atualmente jogando"
+  isLlotheo: "Aquele com menos pedras vence (Llotheo)"
+  loopedMap: "Mapa em ‘loop’"
+  canPutEverywhere: "É possível pôr em qualquer lugar"
+  timeLimitForEachTurn: "Tempo limite por turno"
+  freeMatch: "Partida Livre"
+  lookingForPlayer: "À procura de adversários..."
+  gameCanceled: "A partida foi cancelada."
+  shareToTlTheGameWhenStart: "Compartilhar jogo na linha do tempo ao iniciar"
+  iStartedAGame: "O jogo começou! #MisskeyReversi"
+  opponentHasSettingsChanged: "O oponente alterou as configurações dele"
+  allowIrregularRules: "Regras irregulares (completamente livre)"
+  disallowIrregularRules: "Sem regras irregulares"
+  showBoardLabels: "Exibir numeração de linha e coluna no tabuleiro"
+  useAvatarAsStone: "Utilizar avatares de usuário como as pedras"
+_offlineScreen:
+  title: "Offline - não foi possível conectar ao servidor"
+  header: "Não foi possível conectar ao servidor"
+_urlPreviewSetting:
+  title: "Configurações da prévia de URL"
+  enable: "Habilitar prévia de URL"
+  timeout: "Tempo máximo para obter a prévia (ms)"
+  timeoutDescription: "Se demorar mais que esse valor para obter uma prévia, ela não será gerada."
+  maximumContentLength: "Content-Length máximo (em bytes)"
+  maximumContentLengthDescription: "Se o Content-Length for maior que esse valor, a prévia não será gerada."
+  requireContentLength: "Gerar previu apenas se houver cabeçalho Content-Length disponível na solicitação"
+  requireContentLengthDescription: "Se o outro servidor não retornar um cabeçalho Content-Length, a prévia não será gerada."
+  userAgent: "User-Agent"
+  userAgentDescription: "Define o User-Agent a ser usado ao gerar prévias. Se for deixado em branco, será usado o User-Agent padrão."
+  summaryProxy: "Endpoints do Proxy que geram prévias"
+  summaryProxyDescription: "Fora do Misskey, gerar prévias usando o Sumally Proxy."
+  summaryProxyDescription2: "Os parâmetros a seguir são vinculados ao proxy como um 'query string'. Se o proxy não os suportar, os valores serão ignorados."
+_mediaControls:
+  pip: "Picture-in-Picture"
+  playbackRate: "Velocidade de Reprodução"
+  loop: "Reprodução em Loop"
+_contextMenu:
+  title: "Menu de contexto"
+  app: "Aplicativo"
+  appWithShift: "Aplicativo com a tecla shift"
+  native: "Nativo"
diff --git a/locales/ro-RO.yml b/locales/ro-RO.yml
index 328d34405e..3cc09aa5c2 100644
--- a/locales/ro-RO.yml
+++ b/locales/ro-RO.yml
@@ -453,7 +453,6 @@ or: "Sau"
 language: "Limbă"
 uiLanguage: "Limba interfeței"
 aboutX: "Despre {x}"
-disableDrawer: "Nu folosi meniuri în stil sertar"
 noHistory: "Nu există istoric"
 signinHistory: "Istoric autentificări"
 doing: "Se procesează..."
@@ -626,10 +625,7 @@ abuseReported: "Raportul tău a fost trimis. Mulțumim."
 reporter: "Raportorul"
 reporteeOrigin: "Originea raportatului"
 reporterOrigin: "Originea raportorului"
-forwardReport: "Redirecționează raportul către instanța externă"
-forwardReportIsAnonymous: "În locul contului tău, va fi afișat un cont anonim, de sistem, ca raportor către instanța externă."
 send: "Trimite"
-abuseMarkAsResolved: "Marchează raportul ca rezolvat"
 openInNewTab: "Deschide în tab nou"
 openInSideView: "Deschide în vedere laterală"
 defaultNavigationBehaviour: "Comportament de navigare implicit"
@@ -715,6 +711,7 @@ _notification:
     renote: "Re-notează"
     quote: "Citează"
     reaction: "Reacție"
+    login: "Autentifică-te"
   _actions:
     reply: "Răspunde"
     renote: "Re-notează"
@@ -728,6 +725,10 @@ _deck:
     mentions: "Mențiuni"
 _webhookSettings:
   name: "Nume"
+_abuseReport:
+  _notificationRecipient:
+    _recipientType:
+      mail: "Email"
 _moderationLogTypes:
   suspend: "Suspendă"
   resetPassword: "Resetează parola"
diff --git a/locales/ru-RU.yml b/locales/ru-RU.yml
index 71f5cad601..8174675880 100644
--- a/locales/ru-RU.yml
+++ b/locales/ru-RU.yml
@@ -2,23 +2,26 @@
 _lang_: "Русский"
 headlineMisskey: "Сеть, сплетённая из заметок"
 introMisskey: "Добро пожаловать! Misskey — это децентрализованный сервис микроблогов с открытым исходным кодом.\nПишите «заметки» — делитесь со всеми происходящим вокруг или рассказывайте о себе 📡\nСтавьте «реакции» — выражайте свои чувства и эмоции от заметок других 👍\nОткройте для себя новый мир 🚀"
-poweredByMisskeyDescription: "{name} – сервис на платформе с открытым исходным кодом <b>Misskey</b>, называемый инстансом Misskey."
+poweredByMisskeyDescription: "{name} – сервис на платформе с открытым исходным кодом <b>Misskey</b>, называемый экземпляром Misskey."
 monthAndDay: "{day}.{month}"
 search: "Поиск"
 notifications: "Уведомления"
 username: "Имя пользователя"
 password: "Пароль"
+initialPasswordForSetup: "Пароль для начала настройки"
+initialPasswordIsIncorrect: "Пароль для запуска настройки неверен"
+initialPasswordForSetupDescription: "Если вы установили Misskey самостоятельно, используйте пароль, который вы указали в файле конфигурации.\nЕсли вы используете что-то вроде хостинга Misskey, используйте предоставленный пароль.\nЕсли вы не установили пароль, оставьте его пустым и продолжайте."
 forgotPassword: "Забыли пароль?"
 fetchingAsApObject: "Приём с других сайтов"
-ok: "Окей"
+ok: "Подтвердить"
 gotIt: "Ясно!"
 cancel: "Отмена"
 noThankYou: "Нет, спасибо"
 enterUsername: "Введите имя пользователя"
-renotedBy: "{user} делится"
+renotedBy: "{user} репостнул(а)"
 noNotes: "Нет ни одной заметки"
 noNotifications: "Нет уведомлений"
-instance: "Инстанс"
+instance: "Экземпляр"
 settings: "Настройки"
 notificationSettings: "Настройки уведомлений"
 basicSettings: "Основные настройки"
@@ -45,22 +48,24 @@ pin: "Закрепить в профиле"
 unpin: "Открепить от профиля"
 copyContent: "Скопировать содержимое"
 copyLink: "Скопировать ссылку"
+copyLinkRenote: "Скопировать ссылку на репост"
 delete: "Удалить"
 deleteAndEdit: "Удалить и отредактировать"
-deleteAndEditConfirm: "Удалить эту заметку и создать отредактированную? Все реакции, ссылки и ответы на существующую будут будут потеряны."
+deleteAndEditConfirm: "Удалить этот пост и отредактировать заново? Все реакции, репосты и ответы на него также будут удалены."
 addToList: "Добавить в список"
 addToAntenna: "Добавить к антенне"
 sendMessage: "Отправить сообщение"
 copyRSS: "Скопировать RSS"
 copyUsername: "Скопировать имя пользователя"
-copyUserId: "Скопировать идентификатор пользователя"
-copyNoteId: "Скопировать идентификатор заметки"
+copyUserId: "Скопировать ID пользователя"
+copyNoteId: "Скопировать ID поста"
 copyFileId: "Скопировать ID файла"
 copyFolderId: "Скопировать ID папки"
-copyProfileUrl: "Скопировать URL профиля "
+copyProfileUrl: "Скопировать ссылку на профиль"
 searchUser: "Поиск людей"
+searchThisUsersNotes: "Искать по заметкам пользователя"
 reply: "Ответ"
-loadMore: "Показать еще"
+loadMore: "Загрузить ещё"
 showMore: "Показать ещё"
 showLess: "Закрыть"
 youGotNewFollower: "Новый подписчик"
@@ -107,11 +112,14 @@ enterEmoji: "Введите эмодзи"
 renote: "Репост"
 unrenote: "Отмена репоста"
 renoted: "Репост совершён."
+renotedToX: "Репостнуть в {name}."
 cantRenote: "Это нельзя репостить."
 cantReRenote: "Невозможно репостить репост."
 quote: "Цитата"
 inChannelRenote: "В канале"
 inChannelQuote: "Заметки в канале"
+renoteToChannel: "Репостнуть в канал"
+renoteToOtherChannel: "Репостнуть в другой канал"
 pinnedNote: "Закреплённая заметка"
 pinned: "Закрепить в профиле"
 you: "Вы"
@@ -150,6 +158,7 @@ editList: "Редактировать список"
 selectChannel: "Выберите канал"
 selectAntenna: "Выберите антенну"
 editAntenna: "Редактировать антенну"
+createAntenna: "Создать антенну"
 selectWidget: "Выберите виджет"
 editWidgets: "Редактировать виджеты"
 editWidgetsExit: "Готово"
@@ -157,11 +166,12 @@ customEmojis: "Собственные эмодзи"
 emoji: "Эмодзи"
 emojis: "Эмодзи"
 emojiName: "Название эмодзи"
-emojiUrl: "URL эмодзи"
+emojiUrl: "Ссылка на эмодзи"
 addEmoji: "Добавить эмодзи"
 settingGuide: "Рекомендуемые настройки"
 cacheRemoteFiles: "Кешировать внешние файлы"
 cacheRemoteFilesDescription: "Когда эта настройка отключена, файлы с других сайтов будут загружаться прямо оттуда. Это сэкономит место на сервере, но увеличит трафик, так как не будут создаваться эскизы."
+youCanCleanRemoteFilesCache: "Вы можете очистить кэш, нажав на кнопку 🗑️ в меню управления файлами."
 cacheRemoteSensitiveFiles: "Кэшировать внешние файлы «не для всех»"
 cacheRemoteSensitiveFilesDescription: "Если отключено, файлы «не для всех» загружаются непосредственно с удалённых серверов, не кэшируясь."
 flagAsBot: "Аккаунт бота"
@@ -175,6 +185,10 @@ addAccount: "Добавить учётную запись"
 reloadAccountsList: "Обновить список учётных записей"
 loginFailed: "Неудачная попытка входа"
 showOnRemote: "Перейти к оригиналу на сайт"
+continueOnRemote: "Продолжить на удалённом сервере"
+chooseServerOnMisskeyHub: "Выбрать сервер с Misskey Hub"
+specifyServerHost: "Укажите сервер напрямую"
+inputHostName: "Введите домен"
 general: "Общее"
 wallpaper: "Обои"
 setWallpaper: "Установить обои"
@@ -185,6 +199,7 @@ followConfirm: "Подписаться на {name}?"
 proxyAccount: "Учётная запись прокси"
 proxyAccountDescription: "Учетная запись прокси предназначена служить подписчиком на пользователей с других сайтов. Например, если пользователь добавит кого-то с другого сайта а список, деятельность того не отобразится, пока никто с этого же сайта не подписан на него. Чтобы это стало возможным, на него подписывается прокси."
 host: "Хост"
+selectSelf: "Выбрать себя"
 selectUser: "Выберите пользователя"
 recipient: "Кому"
 annotation: "Описание"
@@ -199,6 +214,7 @@ perHour: "По часам"
 perDay: "По дням"
 stopActivityDelivery: "Остановить отправку обновлений активности"
 blockThisInstance: "Блокировать этот инстанс"
+silenceThisInstance: "Заглушить этот инстанс"
 operations: "Операции"
 software: "Программы"
 version: "Версия"
@@ -218,6 +234,8 @@ clearCachedFiles: "Очистить кэш"
 clearCachedFilesConfirm: "Удалить все закэшированные файлы с других сайтов?"
 blockedInstances: "Заблокированные инстансы"
 blockedInstancesDescription: "Введите список инстансов, которые хотите заблокировать. Они больше не смогут обмениваться с вашим инстансом."
+silencedInstances: "Заглушённые инстансы"
+federationAllowedHosts: "Серверы, поддерживающие федерацию"
 muteAndBlock: "Скрытие и блокировка"
 mutedUsers: "Скрытые пользователи"
 blockedUsers: "Заблокированные пользователи"
@@ -236,7 +254,7 @@ noJobs: "Нет заданий"
 federating: "Федерируется"
 blocked: "Заблокировано"
 suspended: "Заморожено"
-all: "Всё"
+all: "Все"
 subscribing: "Подписка"
 publishing: "Публикация"
 notResponding: "Нет ответа"
@@ -268,7 +286,7 @@ messaging: "Сообщения"
 upload: "Загрузить"
 keepOriginalUploading: "Сохранить исходное изображение"
 keepOriginalUploadingDescription: "Сохраняет исходную версию при загрузке изображений. Если выключить, то при загрузке браузер генерирует изображение для публикации."
-fromDrive: "С «диска»"
+fromDrive: "С Диска"
 fromUrl: "По ссылке"
 uploadFromUrl: "Загрузить по ссылке"
 uploadFromUrlDescription: "Ссылка на файл, который хотите загрузить"
@@ -308,6 +326,7 @@ selectFile: "Выберите файл"
 selectFiles: "Выберите файлы"
 selectFolder: "Выберите папку"
 selectFolders: "Выберите папки"
+fileNotSelected: "Файл не выбран"
 renameFile: "Переименовать файл"
 folderName: "Имя папки"
 createFolder: "Создать папку"
@@ -315,6 +334,7 @@ renameFolder: "Переименовать папку"
 deleteFolder: "Удалить папку"
 folder: "Папка"
 addFile: "Добавить файл"
+showFile: "Посмотреть файл"
 emptyDrive: "Диск пуст"
 emptyFolder: "Папка пуста"
 unableToDelete: "Удаление невозможно"
@@ -359,8 +379,8 @@ disablingTimelinesInfo: "У администраторов и модератор
 registration: "Регистрация"
 enableRegistration: "Разрешить регистрацию"
 invite: "Пригласить"
-driveCapacityPerLocalAccount: "Объём диска на одного локального пользователя"
-driveCapacityPerRemoteAccount: "Объём диска на одного пользователя с другого сайта"
+driveCapacityPerLocalAccount: "Объём Диска на одного локального пользователя"
+driveCapacityPerRemoteAccount: "Объём Диска на одного пользователя с другого экземпляра"
 inMb: "В мегабайтах"
 bannerUrl: "Ссылка на изображение в шапке"
 backgroundImageUrl: "Ссылка на фоновое изображение"
@@ -379,6 +399,7 @@ mcaptcha: "mCaptcha"
 enableMcaptcha: "Включить mCaptcha"
 mcaptchaSiteKey: "Ключ сайта"
 mcaptchaSecretKey: "Секретный ключ"
+mcaptchaInstanceUrl: "Ссылка на сервер mCaptcha"
 recaptcha: "reCAPTCHA"
 enableRecaptcha: "Включить reCAPTCHA"
 recaptchaSiteKey: "Ключ сайта"
@@ -393,7 +414,8 @@ manageAntennas: "Настройки антенн"
 name: "Название"
 antennaSource: "Источник антенны"
 antennaKeywords: "Ключевые слова"
-antennaExcludeKeywords: "Исключения"
+antennaExcludeKeywords: "Чёрный список слов"
+antennaExcludeBots: "Исключать ботов"
 antennaKeywordsDescription: "Пишите слова через пробел в одной строке, чтобы ловить их появление вместе; на отдельных строках располагайте слова, или группы слов, чтобы ловить любые из них."
 notifyAntenna: "Уведомлять о новых заметках"
 withFileAntenna: "Только заметки с вложениями"
@@ -426,6 +448,8 @@ totp: "Приложение-аутентификатор"
 totpDescription: "Описание приложения-аутентификатора"
 moderator: "Модератор"
 moderation: "Модерация"
+moderationNote: "Примечания модератора"
+moderationLogs: "Журнал модерации"
 nUsersMentioned: "Упомянуло пользователей: {n}"
 securityKeyAndPasskey: "Ключ безопасности и парольная фраза"
 securityKey: "Ключ безопасности"
@@ -458,10 +482,12 @@ retype: "Введите ещё раз"
 noteOf: "Что пишет {user}"
 quoteAttached: "Цитата"
 quoteQuestion: "Хотите добавить цитату?"
+attachAsFileQuestion: "Текста в буфере обмена слишком много. Прикрепить как текстовый файл?"
 noMessagesYet: "Пока ни одного сообщения"
 newMessageExists: "Новое сообщение"
 onlyOneFileCanBeAttached: "К сообщению можно прикрепить только один файл"
 signinRequired: "Пожалуйста, войдите"
+signinOrContinueOnRemote: "Чтобы продолжить, вам необходимо войти в аккаунт на своём сервере или зарегистрироваться / войти в аккаунт на этом."
 invitations: "Приглашения"
 invitationCode: "Код приглашения"
 checking: "Проверка"
@@ -471,7 +497,7 @@ usernameInvalidFormat: "Можно использовать только лат
 tooShort: "Слишком короткий"
 tooLong: "Слишком длинный"
 weakPassword: "Слабый пароль"
-normalPassword: "Годный пароль"
+normalPassword: "Хороший пароль"
 strongPassword: "Надёжный пароль"
 passwordMatched: "Совпали"
 passwordNotMatched: "Не совпадают"
@@ -483,8 +509,10 @@ uiLanguage: "Язык интерфейса"
 aboutX: "Описание {x}"
 emojiStyle: "Стиль эмодзи"
 native: "Системные"
-disableDrawer: "Не использовать выдвижные меню"
+menuStyle: "Стиль меню"
+style: "Стиль"
 showNoteActionsOnlyHover: "Показывать кнопки у заметок только при наведении"
+showReactionsCount: "Видеть количество реакций на заметках"
 noHistory: "История пока пуста"
 signinHistory: "Журнал посещений"
 enableAdvancedMfm: "Включить расширенный MFM"
@@ -547,7 +575,7 @@ popout: "Развернуть"
 volume: "Громкость"
 masterVolume: "Основная регулировка громкости"
 notUseSound: "Выключить звук"
-useSoundOnlyWhenActive: "Использовать звук, когда Misskey активен."
+useSoundOnlyWhenActive: "Воспроизводить звук только когда Misskey активен."
 details: "Подробнее"
 chooseEmoji: "Выберите эмодзи"
 unableToProcess: "Не удаётся завершить операцию"
@@ -601,7 +629,7 @@ poll: "Опрос"
 useCw: "Скрывать содержимое под предупреждением"
 enablePlayer: "Включить проигрыватель"
 disablePlayer: "Выключить проигрыватель"
-expandTweet: "Развернуть твит"
+expandTweet: "Развернуть заметку"
 themeEditor: "Редактор темы оформления"
 description: "Описание"
 describeFile: "Добавить подпись"
@@ -613,7 +641,7 @@ plugins: "Расширения"
 preferencesBackups: "Резервная копия"
 deck: "Пульт"
 undeck: "Покинуть пульт"
-useBlurEffectForModal: "Размывка под формой поверх всего"
+useBlurEffectForModal: "Размытие за формой ввода заметки"
 useFullReactionPicker: "Полнофункциональный выбор реакций"
 width: "Ширина"
 height: "Высота"
@@ -644,7 +672,7 @@ smtpSecure: "Использовать SSL/TLS для SMTP-соединений"
 smtpSecureInfo: "Выключите при использовании STARTTLS."
 testEmail: "Проверка доставки электронной почты"
 wordMute: "Скрытие слов"
-hardWordMute: ""
+hardWordMute: "Строгое скрытие слов"
 regexpError: "Ошибка в регулярном выражении"
 regexpErrorDescription: "В списке {tab} скрытых слов, в строке {line} обнаружена синтаксическая ошибка:"
 instanceMute: "Глушение инстансов"
@@ -680,10 +708,7 @@ abuseReported: "Жалоба отправлена. Большое спасибо
 reporter: "Сообщивший"
 reporteeOrigin: "О ком сообщено"
 reporterOrigin: "Кто сообщил"
-forwardReport: "Отправить жалобу на инстанс автора."
-forwardReportIsAnonymous: "Жалоба на удалённый инстанс будет отправлена анонимно. Вместо ваших данных у получателя будет отображена системная учётная запись."
 send: "Отправить"
-abuseMarkAsResolved: "Отметить жалобу как решённую"
 openInNewTab: "Открыть в новой вкладке"
 openInSideView: "Открывать в боковой колонке"
 defaultNavigationBehaviour: "Поведение навигации по умолчанию"
@@ -726,6 +751,7 @@ lockedAccountInfo: "Даже если вы вручную подтверждае
 alwaysMarkSensitive: "Отмечать файлы как «содержимое не для всех» по умолчанию"
 loadRawImages: "Сразу показывать изображения в полном размере"
 disableShowingAnimatedImages: "Не проигрывать анимацию"
+highlightSensitiveMedia: "Выделять содержимое не для всех"
 verificationEmailSent: "Вам отправлено письмо для подтверждения. Пройдите, пожалуйста, по ссылке из письма, чтобы завершить проверку."
 notSet: "Не настроено"
 emailVerified: "Адрес электронной почты подтверждён."
@@ -743,7 +769,7 @@ makeExplorable: "Опубликовать профиль в «Обзоре»."
 makeExplorableDescription: "Если выключить, ваш профиль не будет показан в разделе «Обзор»."
 showGapBetweenNotesInTimeline: "Показывать разделитель между заметками в ленте"
 duplicate: "Дубликат"
-left: "Влево"
+left: "Слева"
 center: "По центру"
 wide: "Толстый"
 narrow: "Тонкий"
@@ -822,7 +848,7 @@ noMaintainerInformationWarning: "Не заполнены сведения об 
 noBotProtectionWarning: "Ботозащита не настроена"
 configure: "Настроить"
 postToGallery: "Опубликовать в галерею"
-postToHashtag: "Написать заметку с этим хэштегом"
+postToHashtag: "Написать заметку с этим хештегом"
 gallery: "Галерея"
 recentPosts: "Недавние публикации"
 popularPosts: "Популярные публикации"
@@ -839,13 +865,13 @@ emailNotConfiguredWarning: "Не указан адрес электронной
 ratio: "Соотношение"
 previewNoteText: "Предварительный просмотр"
 customCss: "Индивидуальный CSS"
-customCssWarn: "Используйте эту настройку только если знаете, что делаете. Ошибки здесь чреваты тем, что сайт перестанет нормально работать у вас."
+customCssWarn: "Используйте эту настройку только если знаете, что делаете. Ошибки здесь чреваты тем, что у вас перестанет нормально работать сайт."
 global: "Всеобщая"
 squareAvatars: "Квадратные аватарки"
 sent: "Отправить"
 received: "Получено"
 searchResult: "Результаты поиска"
-hashtags: "Хэштег"
+hashtags: "Хештеги"
 troubleshooting: "Разрешение проблем"
 useBlurEffect: "Размытие в интерфейсе"
 learnMore: "Подробнее"
@@ -857,7 +883,7 @@ accountDeletionInProgress: "В настоящее время выполняет
 usernameInfo: "Имя, которое отличает вашу учетную запись от других на этом сервере. Вы можете использовать алфавит (a~z, A~Z), цифры (0~9) или символы подчеркивания (_). Имена пользователей не могут быть изменены позже."
 aiChanMode: "Режим Ай"
 devMode: "Режим разработчика"
-keepCw: "Сохраняйте Предупреждения о содержимом"
+keepCw: "Сохраняйте предупреждения о содержимом"
 pubSub: "Учётные записи Pub/Sub"
 lastCommunication: "Последнее сообщение"
 resolved: "Решено"
@@ -878,6 +904,8 @@ makeReactionsPublicDescription: "Список сделанных вами реа
 classic: "Классика"
 muteThread: "Скрыть цепочку"
 unmuteThread: "Отменить сокрытие цепочки"
+followingVisibility: "Видимость подписок"
+followersVisibility: "Видимость подписчиков"
 continueThread: "Показать следующие ответы"
 deleteAccountConfirm: "Учётная запись будет безвозвратно удалена. Подтверждаете?"
 incorrectPassword: "Пароль неверен."
@@ -987,6 +1015,7 @@ assign: "Назначить"
 unassign: "Отменить назначение"
 color: "Цвет"
 manageCustomEmojis: "Управлять пользовательскими эмодзи"
+manageAvatarDecorations: "Управление украшениями аватара"
 youCannotCreateAnymore: "Вы достигли лимита создания."
 cannotPerformTemporary: "Временно недоступен"
 cannotPerformTemporaryDescription: "Это действие временно невозможно выполнить из-за превышения лимита выполнения."
@@ -1003,7 +1032,8 @@ thisPostMayBeAnnoying: "Это сообщение может быть непри
 thisPostMayBeAnnoyingHome: "Этот пост может быть отправлен на главную"
 thisPostMayBeAnnoyingCancel: "Этот пост не может быть отменен."
 thisPostMayBeAnnoyingIgnore: "Этот пост может быть проигнорирован "
-collapseRenotes: "Свернуть репосты"
+collapseRenotes: "Сворачивать увиденные репосты"
+collapseRenotesDescription: "Сворачивать посты с которыми вы взаимодействовали."
 internalServerError: "Внутренняя ошибка сервера"
 internalServerErrorDescription: "Внутри сервера произошла непредвиденная ошибка."
 copyErrorInfo: "Скопировать код ошибки"
@@ -1027,7 +1057,10 @@ resetPasswordConfirm: "Сбросить пароль?"
 sensitiveWords: "Чувствительные слова"
 sensitiveWordsDescription: "Установите общедоступный диапазон заметки, содержащей заданное слово, на домашний. Можно сделать несколько настроек, разделив их переносами строк."
 sensitiveWordsDescription2: "Разделение пробелом создаёт спецификацию AND, а разделение косой чертой создаёт регулярное выражение."
+prohibitedWords: "Запрещённые слова"
+prohibitedWordsDescription: "Включает вывод ошибки при попытке опубликовать пост, содержащий указанное слово/набор слов.\nМножество слов может быть указано, разделяемые новой строкой."
 prohibitedWordsDescription2: "Разделение пробелом создаёт спецификацию AND, а разделение косой чертой создаёт регулярное выражение."
+hiddenTags: "Скрытые хештеги"
 notesSearchNotAvailable: "Поиск заметок недоступен"
 license: "Лицензия"
 unfavoriteConfirm: "Удалить избранное?"
@@ -1038,9 +1071,14 @@ retryAllQueuesConfirmTitle: "Хотите попробовать ещё раз?"
 retryAllQueuesConfirmText: "Нагрузка на сервер может увеличиться"
 enableChartsForRemoteUser: "Создание диаграмм для удалённых пользователей"
 enableChartsForFederatedInstances: "Создание диаграмм для удалённых серверов"
+showClipButtonInNoteFooter: "Показать кнопку добавления в подборку в меню действий с заметкой"
+reactionsDisplaySize: "Размер реакций"
+limitWidthOfReaction: "Ограничить максимальную ширину реакций и отображать их в уменьшенном размере."
 noteIdOrUrl: "ID или ссылка на заметку"
 video: "Видео"
 videos: "Видео"
+audio: "Звук"
+audioFiles: "Звуковые файлы"
 dataSaver: "Экономия трафика"
 accountMigration: "Перенос учётной записи"
 accountMoved: "Учётная запись перенесена"
@@ -1052,12 +1090,13 @@ editMemo: "Изменить памятку"
 reactionsList: "Список реакций"
 renotesList: "Репосты"
 notificationDisplay: "Отображение уведомлений"
-leftTop: "Влево вверх"
-rightTop: "Вправо вверх"
-leftBottom: "Влево вниз"
-rightBottom: "Вправо вниз"
-vertical: "Вертикальная"
-horizontal: "Сбоку"
+leftTop: "Слева вверху"
+rightTop: "Справа сверху"
+leftBottom: "Слева внизу"
+rightBottom: "Справа внизу"
+stackAxis: "Положение уведомлений"
+vertical: "Вертикально"
+horizontal: "Горизонтально"
 position: "Позиция"
 serverRules: "Правила сервера"
 pleaseConfirmBelowBeforeSignup: "Для регистрации на данном сервере, необходимо согласится с нижеследующими положениями."
@@ -1069,57 +1108,114 @@ createNoteFromTheFile: "Создать заметку из этого файла
 archive: "Архив"
 channelArchiveConfirmTitle: "Переместить {name} в архив?"
 channelArchiveConfirmDescription: "Архивированные каналы перестанут отображаться в списке каналов или результатах поиска. В них также нельзя будет добавлять новые записи."
+thisChannelArchived: "Этот канал находится в архиве."
 displayOfNote: "Отображение заметок"
 initialAccountSetting: "Настройка профиля"
 youFollowing: "Подписки"
 preventAiLearning: "Отказаться от использования в машинном обучении (Генеративный ИИ)"
+preventAiLearningDescription: "Запросить краулеров не использовать опубликованный текст или изображения и т.д. для машинного обучения (Прогнозирующий / Генеративный ИИ) датасетов. Это достигается путём добавления \"noai\" HTTP-заголовка в ответ на соответствующий контент. Полного предотвращения через этот заголовок не избежать, так как он может быть просто проигнорирован."
 options: "Настройки ролей"
 specifyUser: "Указанный пользователь"
+openTagPageConfirm: "Открыть страницу этого хештега?"
+specifyHost: "Указать сайт"
 failedToPreviewUrl: "Предварительный просмотр недоступен"
 update: "Обновить"
 rolesThatCanBeUsedThisEmojiAsReaction: "Роли тех, кому можно использовать эти эмодзи как реакцию"
 rolesThatCanBeUsedThisEmojiAsReactionEmptyDescription: "Если здесь ничего не указать, в качестве реакции эту эмодзи сможет использовать каждый."
+rolesThatCanBeUsedThisEmojiAsReactionPublicRoleWarn: "Эти роли должны быть общедоступными."
+cancelReactionConfirm: "Вы действительно хотите удалить свою реакцию?"
 later: "Позже"
 goToMisskey: "К Misskey"
 additionalEmojiDictionary: "Дополнительные словари эмодзи"
 installed: "Установлено"
 branding: "Бренд"
+enableServerMachineStats: "Опубликовать характеристики сервера"
 enableIdenticonGeneration: "Включить генерацию иконки пользователя"
 turnOffToImprovePerformance: "Отключение этого параметра может повысить производительность."
+createInviteCode: "Создать код приглашения"
+createCount: "Количество приглашений"
 expirationDate: "Дата истечения"
-unused: "Неиспользуемый"
+noExpirationDate: "Бессрочно"
+unused: "Неиспользованное"
+used: "Использован"
 expired: "Срок действия приглашения истёк"
 doYouAgree: "Согласны?"
 icon: "Аватар"
 replies: "Ответы"
 renotes: "Репост"
 loadReplies: "Показать ответы"
+pinnedList: "Закреплённый список"
+keepScreenOn: "Держать экран включённым"
+showRenotes: "Показывать репосты"
+mutualFollow: "Взаимные подписки"
+followingOrFollower: "Подписки или подписчики"
+fileAttachedOnly: "Только заметки с файлами"
+showRepliesToOthersInTimeline: "Показывать ответы в ленте"
+showRepliesToOthersInTimelineAll: "Показывать в ленте ответы пользователей, на которых вы подписаны"
+hideRepliesToOthersInTimelineAll: "Скрывать в ленте ответы пользователей, на которых вы подписаны"
 sourceCode: "Исходный код"
+sourceCodeIsNotYetProvided: "Исходный код пока не доступен. Свяжитесь с администратором, чтобы исправить эту проблему."
+repositoryUrl: "Ссылка на репозиторий"
+repositoryUrlDescription: "Если вы используете Misskey как есть (без изменений в исходном коде), введите https://github.com/misskey-dev/misskey"
+privacyPolicy: "Политика Конфиденциальности"
+privacyPolicyUrl: "Ссылка на Политику Конфиденциальности"
+attach: "Прикрепить"
+angle: "Угол"
 flip: "Переворот"
+disableStreamingTimeline: "Отключить обновление ленты в режиме реального времени"
+useGroupedNotifications: "Отображать уведомления сгруппировано"
+doReaction: "Добавить реакцию"
 code: "Код"
+remainingN: "Остаётся: {n}"
+seasonalScreenEffect: "Эффект времени года на экране"
+decorate: "Украсить"
+addMfmFunction: "Добавить MFM"
 lastNDays: "Последние {n} сут"
+hemisphere: "Место проживания"
+enableHorizontalSwipe: "Смахните в сторону, чтобы сменить вкладки"
 surrender: "Этот пост не может быть отменен."
+useNativeUIForVideoAudioPlayer: "Использовать интерфейс браузера при проигрывании видео и звука"
+keepOriginalFilename: "Сохранять исходное имя файла"
+keepOriginalFilenameDescription: "Если вы выключите данную настройку, имена файлов будут автоматически заменены случайной строкой при загрузке."
+alwaysConfirmFollow: "Всегда подтверждать подписку"
+inquiry: "Связаться"
 _delivery:
   stop: "Заморожено"
   _type:
     none: "Публикация"
+_announcement:
+  tooManyActiveAnnouncementDescription: "Большое количество оповещений может ухудшить пользовательский опыт. Рассмотрите архивирование неактуальных оповещений. "
 _initialAccountSetting:
   accountCreated: "Аккаунт успешно создан!"
   letsStartAccountSetup: "Давайте настроим вашу учётную запись."
   profileSetting: "Настройки профиля"
   privacySetting: "Настройки конфиденциальности"
   initialAccountSettingCompleted: "Первоначальная настройка успешно завершена!"
+  startTutorial: "Пройти Обучение"
   skipAreYouSure: "Пропустить настройку?"
 _initialTutorial:
+  launchTutorial: "Пройти обучение"
   _note:
     description: "Посты в Misskey называются 'Заметками.' Заметки отсортированы в хронологическом порядке в ленте и обновляются в режиме реального времени."
+  _reaction:
+    reactToContinue: "Добавьте реакцию, чтобы продолжить."
+  _postNote:
+    _visibility:
+      public: "Твоя заметка будет видна всем."
+      doNotSendConfidencialOnDirect2: "Администратор целевого сервера может видеть что вы отправляете. Будьте осторожны с конфиденциальной информацией, когда отправляете личные заметки пользователям с ненадёжных серверов."
 _timelineDescription:
   home: "В персональной ленте располагаются заметки тех, на которых вы подписаны."
-  local: "Местная лента показывает заметки всех пользователей этого сайта."
+  local: "Местная лента показывает заметки всех пользователей этого экземпляра."
   social: "В социальной ленте собирается всё, что есть в персональной и местной лентах."
-  global: "В глобальную ленту попадает вообще всё со связанных инстансов."
+  global: "В глобальную ленту попадает вообще всё со связанных экземпляров."
 _serverSettings:
   iconUrl: "Адрес на иконку роли"
+_accountMigration:
+  moveFrom: "Перенести другую учётную запись сюда"
+  moveTo: "Перенести учётную запись на другой сервер"
+  moveAccountDescription: "Это действие перенесёт ваш аккаунт на другой сервер.\n ・Подписчики с этого аккаунта автоматически подпишутся на новый\n ・Этот аккаунт отпишется от всех пользователей, на которых подписан сейчас\n ・Вы не сможете создавать новые заметки и т.д. на этом аккаунте\n\nТогда как перенос подписчиков происходит автоматически, вы должны будете подготовиться, сделав некоторые шаги, чтобы перенести список пользователей, на которых вы подписаны. Чтобы сделать это, экспортируйте список подписчиков в файл, который затем импортируете на новом аккаунте в меню настроек. То же самое необходимо будет сделать со списками, также как и со скрытыми и заблокированными пользователями.\n\n(Это объяснение применяется к Misskey v13.12.0 и выше. Другое ActivityPub программное обеспечение, такое, как Mastodon, может работать по-другому."
+  startMigration: "Перенести"
+  movedAndCannotBeUndone: "Аккаунт был перемещён. Это действие необратимо."
 _achievements:
   earnedAt: "Разблокировано в"
   _types:
@@ -1395,6 +1491,7 @@ _role:
     canPublicNote: "Может публиковать общедоступные заметки"
     canInvite: "Может создавать пригласительные коды"
     canManageCustomEmojis: "Управлять пользовательскими эмодзи"
+    canManageAvatarDecorations: "Управление украшениями аватара"
     driveCapacity: "Доступное пространство на «диске»"
     alwaysMarkNsfw: "Всегда отмечать файлы как «не для всех»"
     pinMax: "Доступное количество закреплённых заметок"
@@ -1505,6 +1602,11 @@ _aboutMisskey:
   donate: "Пожертвование на Misskey"
   morePatrons: "Большое спасибо и многим другим, кто принял участие в этом проекте! 🥰"
   patrons: "Материальная поддержка"
+  projectMembers: "Участники проекта"
+_displayOfSensitiveMedia:
+  respect: "Скрывать содержимое не для всех"
+  ignore: "Показывать содержимое не для всех"
+  force: "Скрывать всё содержимое"
 _instanceTicker:
   none: "Не показывать"
   remote: "Только для других сайтов"
@@ -1533,7 +1635,7 @@ _wordMute:
   muteWordsDescription: "Пишите слова через пробел в одной строке, чтобы фильтровать их появление вместе; а если хотите фильтровать любое из них, пишите в отдельных строках."
   muteWordsDescription2: "Здесь можно использовать регулярные выражения — просто заключите их между двумя дробными чертами (/)."
 _instanceMute:
-  instanceMuteDescription: "Заметки и репосты с указанных здесь инстансов, а также ответы пользователям оттуда же не будут отображаться."
+  instanceMuteDescription: "Любые активности, затрагивающие инстансы из данного списка, будут скрыты."
   instanceMuteDescription2: "Пишите каждый инстанс на отдельной строке"
   title: "Скрывает заметки с заданных инстансов."
   heading: "Список скрытых инстансов"
@@ -1582,7 +1684,7 @@ _theme:
     navActive: "Текст на боковой панели (активирован)"
     navIndicator: "Индикатор на боковой панели"
     link: "Ссылка"
-    hashtag: "Хэштег"
+    hashtag: "Хештег"
     mention: "Упоминание"
     mentionMe: "Упоминания вас"
     renote: "Репост"
@@ -1600,7 +1702,6 @@ _theme:
     buttonBg: "Фон кнопки"
     buttonHoverBg: "Текст кнопки"
     inputBorder: "Рамка поля ввода"
-    listItemHoverBg: "Фон пункта списка (под указателем)"
     driveFolderBg: "Фон папки «Диска»"
     wallpaperOverlay: "Слой обоев"
     badge: "Значок"
@@ -1612,8 +1713,10 @@ _sfx:
   note: "Заметки"
   noteMy: "Собственные заметки"
   notification: "Уведомления"
-  antenna: "Антенна"
-  channel: "Канал"
+  reaction: "При выборе реакции"
+_soundSettings:
+  driveFile: "Использовать аудиофайл с Диска."
+  driveFileWarn: "Выбрать аудиофайл с Диска."
 _ago:
   future: "Из будущего"
   justNow: "Только что"
@@ -1692,6 +1795,7 @@ _permissions:
   "write:gallery": "Редактирование галереи"
   "read:gallery-likes": "Просмотр списка понравившегося в галерее"
   "write:gallery-likes": "Изменение списка понравившегося в галерее"
+  "write:admin:reset-password": "Сбросить пароль пользователю"
 _auth:
   shareAccessTitle: "Разрешения для приложений"
   shareAccess: "Дать доступ для «{name}» к вашей учётной записи?"
@@ -1745,6 +1849,7 @@ _widgets:
   _userList:
     chooseList: "Выберите список"
   clicker: "Счётчик щелчков"
+  birthdayFollowings: "Пользователи, у которых сегодня день рождения"
 _cw:
   hide: "Спрятать"
   show: "Показать"
@@ -1798,7 +1903,7 @@ _profile:
   name: "Имя"
   username: "Имя пользователя"
   description: "О себе"
-  youCanIncludeHashtags: "Можете использовать здесь хэштеги"
+  youCanIncludeHashtags: "Можете использовать здесь хештеги."
   metadata: "Дополнительные сведения"
   metadataEdit: "Редактировать дополнительные сведения"
   metadataDescription: "Можно добавить до четырёх дополнительных граф в профиль."
@@ -1806,6 +1911,8 @@ _profile:
   metadataContent: "Содержимое"
   changeAvatar: "Поменять аватар"
   changeBanner: "Поменять изображение в шапке"
+  verifiedLinkDescription: "Указывая здесь URL, содержащий ссылку на профиль, иконка владения ресурсом может быть отображена рядом с полем"
+  avatarDecorationMax: "Вы можете добавить до {max} украшений."
 _exportOrImport:
   allNotes: "Все заметки\n"
   favoritedNotes: "Избранное"
@@ -1928,6 +2035,9 @@ _notification:
   unreadAntennaNote: "Антенна {name}"
   emptyPushNotificationMessage: "Обновлены push-уведомления"
   achievementEarned: "Получено достижение"
+  checkNotificationBehavior: "Проверить внешний вид уведомления"
+  sendTestNotification: "Отправить тестовое уведомление"
+  flushNotification: "Очистить уведомления"
   _types:
     all: "Все"
     follow: "Подписки"
@@ -1940,6 +2050,7 @@ _notification:
     receiveFollowRequest: "Получен запрос на подписку"
     followRequestAccepted: "Запрос на подписку одобрен"
     achievementEarned: "Получение достижений"
+    login: "Войти"
     app: "Уведомления из приложений"
   _actions:
     followBack: "отвечает взаимной подпиской"
@@ -1979,15 +2090,57 @@ _dialog:
 _disabledTimeline:
   title: "Лента отключена"
   description: "Ваша текущая роль не позволяет пользоваться этой лентой."
+_drivecleaner:
+  orderBySizeDesc: "Размеры файлов по убыванию"
+  orderByCreatedAtAsc: "По увеличению даты"
 _webhookSettings:
   createWebhook: "Создать вебхук"
+  modifyWebhook: "Изменить Вебхук"
   name: "Название"
+  secret: "Секрет"
+  trigger: "Условие срабатывания"
   active: "Вкл."
+  _events:
+    follow: "Когда подписались на пользователя"
+    followed: "Когда на вас подписались"
+    note: "Когда создали заметку"
+    reply: "Когда получили ответ на заметку"
+    renote: "Когда вас репостнули"
+    reaction: "Когда получили реакцию"
+    mention: "Когда вас упоминают"
+  _systemEvents:
+    abuseReport: "Когда приходит жалоба"
+    abuseReportResolved: "Когда разрешается жалоба"
+    userCreated: "Когда создан пользователь"
+  deleteConfirm: "Вы уверены, что хотите удалить этот Вебхук?"
+_abuseReport:
+  _notificationRecipient:
+    _recipientType:
+      mail: "Электронная почта"
+      webhook: "Вебхук"
+      _captions:
+        webhook: "Отправить уведомление Системному Вебхуку при получении или разрешении жалоб."
+    notifiedWebhook: "Используемый Вебхук"
 _moderationLogTypes:
   suspend: "Заморозить"
   addCustomEmoji: "Добавлено эмодзи"
   updateCustomEmoji: "Изменено эмодзи"
   deleteCustomEmoji: "Удалено эмодзи"
+  deleteDriveFile: "Файл удалён"
   resetPassword: "Сброс пароля:"
+  createInvitation: "Создать код приглашения"
+  createSystemWebhook: "Создать Системный Вебхук"
+  updateSystemWebhook: "Обновить Системый Вебхук"
+  deleteSystemWebhook: "Удалить Системный Вебхук"
+_fileViewer:
+  url: "Ссылка"
+  attachedNotes: "Закреплённые заметки"
+_dataSaver:
+  _code:
+    title: "Подсветка кода"
+_hemisphere:
+  N: "Северное полушарие"
+  S: "Южное полушарие"
+  caption: "Используется для некоторых настроек клиента для определения сезона."
 _reversi:
   total: "Всего"
diff --git a/locales/si-LK.yml b/locales/si-LK.yml
index e130d68ed8..c43f3d860d 100644
--- a/locales/si-LK.yml
+++ b/locales/si-LK.yml
@@ -17,3 +17,6 @@ _sfx:
   note: "නෝට්"
 _profile:
   username: "පරිශීලක නාමය"
+_notification:
+  _types:
+    login: "පිවිසෙන්න"
diff --git a/locales/sk-SK.yml b/locales/sk-SK.yml
index 52f6bf142c..60ce45a6b9 100644
--- a/locales/sk-SK.yml
+++ b/locales/sk-SK.yml
@@ -454,7 +454,6 @@ uiLanguage: "Jazyk používateľského prostredia"
 aboutX: "O {x}"
 emojiStyle: "Štýl emoji"
 native: "Natívne"
-disableDrawer: "Nepoužívať šuflíkové menu"
 showNoteActionsOnlyHover: "Ovládacie prvky poznámky sa zobrazujú len po nabehnutí myši"
 noHistory: "Žiadna história"
 signinHistory: "História prihlásení"
@@ -632,10 +631,7 @@ abuseReported: "Vaše nahlásenie je odoslané. Veľmi pekne ďakujeme."
 reporter: "Nahlásil"
 reporteeOrigin: "Pôvod nahláseného"
 reporterOrigin: "Pôvod nahlasovača"
-forwardReport: "Preposlať nahlásenie na server"
-forwardReportIsAnonymous: "Namiesto vášho účtu bude zobrazený anonymný systémový účet na vzdialenom serveri ako autor nahlásenia."
 send: "Poslať"
-abuseMarkAsResolved: "Označiť nahlásenia ako vyriešené"
 openInNewTab: "Otvoriť v novom tabe"
 openInSideView: "Otvoriť v bočnom paneli"
 defaultNavigationBehaviour: "Predvolené správanie navigácie"
@@ -1112,7 +1108,6 @@ _theme:
     buttonBg: "Pozadie tlačidla"
     buttonHoverBg: "Pozadie tlačidla (pod kurzorom)"
     inputBorder: "Okraj vstupného poľa"
-    listItemHoverBg: "Pozadie položky zoznamu (pod kurzorom)"
     driveFolderBg: "Pozadie priečinu disku"
     wallpaperOverlay: "Vrstvenie pozadia"
     badge: "Odznak"
@@ -1124,8 +1119,6 @@ _sfx:
   note: "Poznámky"
   noteMy: "Vlastná poznámka"
   notification: "Oznámenia"
-  antenna: "Antény"
-  channel: "Upozornenia kanála"
 _ago:
   future: "Budúcnosť"
   justNow: "Teraz"
@@ -1412,6 +1405,7 @@ _notification:
     pollEnded: "Hlasovanie skončilo"
     receiveFollowRequest: "Doručené žiadosti o sledovanie"
     followRequestAccepted: "Schválené žiadosti o sledovanie"
+    login: "Prihlásiť sa"
     app: "Oznámenia z prepojených aplikácií"
   _actions:
     followBack: "Sledovať späť\n"
@@ -1447,6 +1441,10 @@ _deck:
 _webhookSettings:
   name: "Názov"
   active: "Zapnuté"
+_abuseReport:
+  _notificationRecipient:
+    _recipientType:
+      mail: "Email"
 _moderationLogTypes:
   suspend: "Zmraziť"
   resetPassword: "Resetovať heslo"
diff --git a/locales/sv-SE.yml b/locales/sv-SE.yml
index 089dc3949f..5a0de660e8 100644
--- a/locales/sv-SE.yml
+++ b/locales/sv-SE.yml
@@ -512,7 +512,6 @@ _theme:
 _sfx:
   note: "Noter"
   notification: "Notifikationer"
-  antenna: "Antenner"
 _2fa:
   renewTOTPCancel: "Nej tack"
 _antennaSources:
@@ -563,6 +562,7 @@ _notification:
     renote: "Omnotera"
     quote: "Citat"
     reaction: "Reaktioner"
+    login: "Logga in"
   _actions:
     reply: "Svara"
     renote: "Omnotera"
@@ -577,6 +577,10 @@ _deck:
 _webhookSettings:
   name: "Namn"
   active: "Aktiverad"
+_abuseReport:
+  _notificationRecipient:
+    _recipientType:
+      mail: "E-post"
 _moderationLogTypes:
   suspend: "Suspendera"
   resetPassword: "Återställ Lösenord"
diff --git a/locales/th-TH.yml b/locales/th-TH.yml
index ab09ac4d5a..58cf8f068c 100644
--- a/locales/th-TH.yml
+++ b/locales/th-TH.yml
@@ -2,12 +2,15 @@
 _lang_: "ภาษาไทย"
 headlineMisskey: "เชื่อมต่อเครือข่ายโดยโน้ต"
 introMisskey: "ยินดีต้อนรับทุกคนจ้า! Misskey คือ ซอฟต์แวร์โอเพนซอร์สสำหรับบริการไมโครบล็อกกิ้ง (MicroBlogging) แบบกระจายศูนย์อำนาจ (Decentralized) \n\nเขียน “โน้ต (Note)” เพื่อส่งต่อเรื่องราวของคุณให้ทั้งโลกได้รับรู้📡\nและอย่าลืมที่จะ “รีแอคชั่น” กับเรื่องราวของคนอื่น ๆ ด้วยนะ! 👍\n\nท่องสำรวจโลกใบใหม่กันเถอะ🚀"
-poweredByMisskeyDescription: "{name} เป็นส่วนหนึ่งในบริการที่ถูกขับเคลื่อนโดยแพลตฟอร์มโอเพ่นซอร์ส <b>Misskey</b> (เรียกว่า \"อินสแตนซ์ Misskey\")"
+poweredByMisskeyDescription: "{name} เป็นหนึ่งในเซิร์ฟเวอร์ของแพลตฟอร์มโอเพ่นซอร์ส <b>Misskey</b>"
 monthAndDay: "{month}/{day}"
 search: "ค้นหา"
-notifications: "การเเจ้งเตือน"
+notifications: "เเจ้งเตือน"
 username: "ชื่อผู้ใช้"
 password: "รหัสผ่าน"
+initialPasswordForSetup: "รหัสผ่านเริ่มต้นสำหรับการตั้งค่า"
+initialPasswordIsIncorrect: "รหัสผ่านเริ่มต้นสำหรับตั้งค่านั้นไม่ถูกต้องค่ะ"
+initialPasswordForSetupDescription: "ถ้าหากคุณติดตั้ง Misskey เอง ให้ใช้รหัสผ่านที่คุณป้อนในไฟล์กำหนดค่า \nถ้าหากคุณกำลังใช้บริการโฮสต์ Misskey ให้ใช้รหัสผ่านที่ได้รับมา\nถ้ายังไม่มีรหัสผ่าน ให้ข้ามช่องรหัสผ่านไป แล้วกดต่อไป"
 forgotPassword: "ลืมรหัสผ่าน"
 fetchingAsApObject: "กำลังดึงข้อมูลจากสหพันธ์..."
 ok: "ตกลง"
@@ -18,7 +21,7 @@ enterUsername: "กรอกชื่อผู้ใช้"
 renotedBy: "รีโน้ตโดย {user}"
 noNotes: "ไม่มีโน้ต"
 noNotifications: "ไม่มีการแจ้งเตือน"
-instance: "อินสแตนซ์"
+instance: "เซิร์ฟเวอร์"
 settings: "การตั้งค่า"
 notificationSettings: "ตั้งค่าการแจ้งเตือน"
 basicSettings: "การตั้งค่าพื้นฐาน"
@@ -48,7 +51,7 @@ copyLink: "คัดลอกลิงก์"
 copyLinkRenote: "คัดลอกลิงก์รีโน้ต"
 delete: "ลบ"
 deleteAndEdit: "ลบและแก้ไข"
-deleteAndEditConfirm: "คุณต้องการลบโน้ตนี้และแก้ไขใหม่ใช่ไหม? รีแอคชั่น รีโน้ต และการตอบกลับต่อโน้ตนี้ทั้งหมดจะถูกลบออกด้วย"
+deleteAndEditConfirm: "ต้องการลบโน้ตนี้และแก้ไขใหม่ใช่ไหม? รีแอคชั่น รีโน้ต และการตอบกลับต่อโน้ตนี้ทั้งหมดจะถูกลบออกด้วย"
 addToList: "เพิ่มลงรายชื่อ"
 addToAntenna: "เพิ่มไปยังเสาอากาศ"
 sendMessage: "ส่งข้อความ"
@@ -60,6 +63,7 @@ copyFileId: "คัดลอกไฟล์ ID"
 copyFolderId: "คัดลอกโฟลเดอร์ ID"
 copyProfileUrl: "คัดลอกโปรไฟล์ URL"
 searchUser: "ค้นหาผู้ใช้"
+searchThisUsersNotes: "ค้นหาโน้ตของผู้ใช้"
 reply: "ตอบกลับ"
 loadMore: "แสดงเพิ่มเติม"
 showMore: "แสดงเพิ่มเติม"
@@ -68,7 +72,7 @@ youGotNewFollower: "ได้ติดตามคุณ"
 receiveFollowRequest: "มีคำขอติดตามส่งมาหา"
 followRequestAccepted: "การติดตามได้รับการอนุมัติแล้ว"
 mention: "กล่าวถึง"
-mentions: "พูดถึง"
+mentions: "กล่าวถึงคุณ"
 directNotes: "โพสต์แบบไดเร็กต์"
 importAndExport: "นำเข้า / ส่งออก"
 import: "นำเข้า"
@@ -92,7 +96,7 @@ error: "ผิดพลาด!"
 somethingHappened: "อุ๊ย ! มีอะไรบางอย่างผิดพลาด"
 retry: "ลองใหม่อีกครั้ง"
 pageLoadError: "เกิดข้อผิดพลาดในการโหลดหน้านี้"
-pageLoadErrorDescription: "โดยปกติแล้วมักจะเกิดจากข้อผิดพลาดของเครือข่ายหรือแคชของเบราว์เซอร์ ลองล้างแคชแล้วลองใหม่อีกครั้งหลังจากรอสักครู่ "
+pageLoadErrorDescription: "ปัญหานี้มักเกิดจากแคชของเครือข่ายหรือเบราว์เซอร์ ควรล้างแคช, รอสักครู่ แล้วลองใหม่อีกครั้ง"
 serverIsDead: "เซิร์ฟเวอร์นี้ไม่มีการตอบสนอง โปรดกรุณารอสักครู่แล้วลองใหม่อีกครั้ง"
 youShouldUpgradeClient: "หากต้องการดูหน้านี้ กรุณาโหลดหน้าใหม่เพื่ออัปเดตไคลเอ็นต์ของคุณ"
 enterListName: "ป้อนนามเรียกของรายชื่อชุดนี้"
@@ -108,11 +112,14 @@ enterEmoji: "พิมพ์เอโมจิ"
 renote: "รีโน้ต"
 unrenote: "เลิกรีโน้ต"
 renoted: "รีโน้ตแล้ว"
+renotedToX: "รีโน้ตให้ {name} แล้ว"
 cantRenote: "โพสต์นี้ไม่สามารถรีโน้ตใหม่ได้"
 cantReRenote: "รีโน้ตไม่สามารถรีโน้ตซ้ำได้"
 quote: "อ้างอิง"
 inChannelRenote: "รีโน้ตในช่องเท่านั้น"
 inChannelQuote: "อ้างอิงในช่องเท่านั้น"
+renoteToChannel: "รีโน้ตไปที่ช่อง"
+renoteToOtherChannel: "รีโน้ตไปยังช่องอื่น"
 pinnedNote: "โน้ตที่ปักหมุดไว้"
 pinned: "ปักหมุด"
 you: "คุณ"
@@ -151,6 +158,7 @@ editList: "แก้ไขรายชื่อ"
 selectChannel: "เลือกช่อง"
 selectAntenna: "เลือกเสาอากาศ"
 editAntenna: "แก้ไขเสาอากาศ"
+createAntenna: "สร้างเสาอากาศ"
 selectWidget: "เลือกวิดเจ็ต"
 editWidgets: "แก้ไขวิดเจ็ต"
 editWidgetsExit: "เรียบร้อย"
@@ -163,20 +171,24 @@ addEmoji: "แทรกเอโมจิ"
 settingGuide: "การตั้งค่าที่แนะนำ"
 cacheRemoteFiles: "แคชไฟล์ระยะไกล"
 cacheRemoteFilesDescription: "หากเปิดใช้งาน ไฟล์ระยะไกลจะถูกแคชไว้ ทำให้แสดงภาพเร็วขึ้น แต่ก็ใช้พื้นที่เก็บข้อมูลของเซิร์ฟเวอร์มากขึ้นเช่นกัน สำหรับขีดจำกัดที่ผู้ใช้ระยะไกลถูกแคชไว้จะขึ้นอยู่กับความจุไดรฟ์ตามบทบาทของเขา เมื่อเกินแล้วไฟล์เก่าจะถูกลบออกและเก็บเป็นลิงก์แทน หากปิดใช้งาน ไฟล์ระยะไกลจะถูกเก็บเป็นลิงก์ตั้งแต่ต้น เราแนะนำให้ตั้งค่า proxyRemoteFiles ใน default.yml เป็น true เพื่อสร้างธัมบ์เนลและปกป้องความเป็นส่วนตัวของผู้ใช้"
-youCanCleanRemoteFilesCache: "คุณสามารถล้างแคชได้โดยคลิกที่ปุ่ม 🗑️ ในมุมมองการจัดการไฟล์"
+youCanCleanRemoteFilesCache: "สามารถลบแคชทั้งหมดได้โดยใช้ปุ่ม 🗑️ ในหน้าการจัดการไฟล์"
 cacheRemoteSensitiveFiles: "แคชไฟล์ระยะไกลที่มีเนื้อหาละเอียดอ่อน"
-cacheRemoteSensitiveFilesDescription: "เมื่อปิดการใช้งานการตั้งค่านี้ ไฟล์ระยะไกลที่มีเครื่องหมายว่ามีเนื้อหาละเอียดอ่อนนั้นจะถูกโหลดโดยตรงจากอินสแตนซ์ระยะไกลโดยที่ไม่มีการแคช"
+cacheRemoteSensitiveFilesDescription: "เมื่อปิดการใช้งานการตั้งค่านี้ ไฟล์ระยะไกลที่มีเนื้อหาละเอียดอ่อนจะถูกโหลดโดยตรงจากเซิร์ฟเวอร์ระยะไกลโดยไม่มีการแคช"
 flagAsBot: "ทำเครื่องหมายบอกว่าบัญชีนี้เป็นบอต"
-flagAsBotDescription: "การเปิดใช้งานตัวเลือกนี้หากบัญชีนี้ถูกควบคุมโดยนักเขียนโปรแกรม หรือ ถ้าหากเปิดใช้งาน มันจะทำหน้าที่เป็นแฟล็กสำหรับนักพัฒนารายอื่นๆ และเพื่อป้องกันการโต้ตอบแบบไม่มีที่สิ้นสุดกับบอทตัวอื่นๆ และยังสามารถปรับเปลี่ยนระบบภายในของ Misskey เพื่อปฏิบัติต่อบัญชีนี้เป็นบอท"
+flagAsBotDescription: "เปิดใช้งานตัวเลือกนี้หากบัญชีนี้ถูกควบคุมโดยโปรแกรม เมื่อเปิดใช้งาน มันจะทำหน้าที่เป็นแฟล็กสำหรับนักพัฒนารายอื่นในการป้องกันการสร้างห่วงโซ่การโต้ตอบแบบอนันต์กับบอตตัวอื่น และปรับระบบภายในของ Misskey เพื่อจัดการบัญชีนี้ในฐานะบอต"
 flagAsCat: "เมี้ยววววววววววววววว!!!!!!!!!!!"
 flagAsCatDescription: "เหมียวเหมียวเมี้ยว??"
-flagShowTimelineReplies: "แสดงตอบกลับ ในไทม์ไลน์"
-flagShowTimelineRepliesDescription: "แสดงการตอบกลับของผู้ใช้งานไปยังโน้ตของผู้ใช้งานรายอื่นๆในไทม์ไลน์หากได้เปิดเอาไว้"
-autoAcceptFollowed: "อนุมัติคำขอติดตามโดยอัตโนมัติทันที จากผู้ใช้งานที่คุณกำลังติดตาม"
+flagShowTimelineReplies: "แสดงตอบกลับโน้ตลงไทม์ไลน์"
+flagShowTimelineRepliesDescription: "เมื่อเปิดใช้งาน จะแสดงการตอบกลับของผู้ใช้คนนั้นต่อโน้ตอื่นๆ ในไทม์ไลน์ด้วย"
+autoAcceptFollowed: "อนุมัติคำขอติดตามจากผู้ใช้ที่คุณติดตามอยู่โดยอัตโนมัติ"
 addAccount: "เพิ่มบัญชี"
 reloadAccountsList: "รีโหลดรายการบัญชีใหม่"
 loginFailed: "การเข้าสู่ระบบไม่สำเร็จ"
-showOnRemote: "ดูบนอินสแตนซ์ระยะไกล"
+showOnRemote: "ดูบนเซิร์ฟเวอร์ฝั่งระยะไกล"
+continueOnRemote: "ดำเนินการต่อบนเซิร์ฟเวอร์ฝั่งระยะไกล"
+chooseServerOnMisskeyHub: "เลือกเซิร์ฟเวอร์จาก Misskey Hub"
+specifyServerHost: "ระบุโดเมนของเซิร์ฟเวอร์โดยตรง"
+inputHostName: "โปรดป้อนโดเมน"
 general: "ทั่วไป"
 wallpaper: "ภาพพื้นหลัง"
 setWallpaper: "ตั้งค่าภาพพื้นหลัง"
@@ -185,23 +197,25 @@ searchWith: "ค้นหา: {q}"
 youHaveNoLists: "คุณไม่มีรายชื่อใดๆ "
 followConfirm: "ต้องการติดตาม {name} ใช่ไหม?"
 proxyAccount: "บัญชีพร็อกซี่"
-proxyAccountDescription: "บัญชีพร็อกซี่ คือ บัญชีที่จะทำหน้าที่เป็นผู้ติดตามระยะไกลสำหรับผู้ใช้งานที่อยู่ภายใต้ด้วยเงื่อนไขบางอย่าง ยกตัวอย่าง เช่น เมื่อมีผู้ใช้งานนั้นได้เพิ่มผู้ใช้งานจากระยะไกลลงในรายการ แต่กิจกรรมของผู้ใช้ในระยะไกลนั้นจะไม่ถูกส่งไปยังอินสแตนซ์หากไม่มีผู้ใช้งานในพื้นที่ติดตามผู้ใช้รายนั้น ดังนั้นบัญชีพร็อกซีนี้จะติดตามแทน"
+proxyAccountDescription: "บัญชีพร็อกซี คือ บัญชีที่ทำหน้าที่ติดตาม(ผู้ใช้)ระยะไกลภายใต้เงื่อนไขบางประการ ตัวอย่างเช่น เมื่อผู้ใช้ท้องถิ่นเพิ่มผู้ใช้ระยะไกลลงรายชื่อ หากไม่มีใครติดตามผู้ใช้ระยะไกลในรายชื่อนั้น กิจกรรมก็จะไม่ถูกส่งมายังเซิร์ฟเวอร์ ดังนั้นจึงมีบัญชีพร็อกซีไว้ติดตามผู้ใช้ระยะไกลเหล่านั้น"
 host: "โฮสต์"
+selectSelf: "เลือกตัวเอง"
 selectUser: "เลือกผู้ใช้งาน"
 recipient: "ผู้รับ"
 annotation: "หมายเหตุประกอบ"
 federation: "สหพันธ์"
-instances: "อินสแตนซ์"
+instances: "เซิร์ฟเวอร์"
 registeredAt: "วันที่ลงทะเบียน"
 latestRequestReceivedAt: "คำขอล่าสุดที่ได้รับ"
 latestStatus: "สถานะล่าสุด"
 storageUsage: "พื้นที่จัดเก็บข้อมูลที่ใช้ไป"
-charts: "โดดเด่น"
-perHour: "ทุกชั่วโมง"
+charts: "แผนภูมิ"
+perHour: "ต่อชั่วโมง"
 perDay: "ต่อวัน"
 stopActivityDelivery: "หยุดส่งกิจกรรม"
-blockThisInstance: "บล็อกอินสแตนซ์นี้"
-silenceThisInstance: "ปิดปากอินสแตนซ์นี้"
+blockThisInstance: "บล็อกเซิร์ฟเวอร์นี้"
+silenceThisInstance: "ปิดปากเซิร์ฟเวอร์นี้"
+mediaSilenceThisInstance: "ปิดปากสื่อของเซิร์ฟเวอร์นี้"
 operations: "ดำเนินการ"
 software: "ซอฟต์แวร์"
 version: "เวอร์ชั่น"
@@ -212,17 +226,21 @@ jobQueue: "คิวงาน"
 cpuAndMemory: "ซีพียู และ หน่วยความจำ"
 network: "เครือข่าย"
 disk: "ดิสก์"
-instanceInfo: "ข้อมูลอินสแตนซ์"
+instanceInfo: "ข้อมูลเซิร์ฟเวอร์"
 statistics: "สถิติการใช้งาน"
 clearQueue: "ล้างคิว"
 clearQueueConfirmTitle: "ต้องการล้างคิวใช่ไหม?"
 clearQueueConfirmText: "โพสต์ที่ยังค้างในคิวจะไม่ถูกจัดส่งอีกต่อไป โดยปกติแล้วการดำเนินการนี้ไม่จำเป็น"
 clearCachedFiles: "ล้างแคช"
 clearCachedFilesConfirm: "ต้องการลบไฟล์ระยะไกลที่แคชไว้ทั้งหมดใช่ไหม?"
-blockedInstances: "อินสแตนซ์ที่ถูกบล็อก"
-blockedInstancesDescription: "ระบุชื่อโฮสต์ของอินสแตนซ์ที่คุณต้องการบล็อก อินสแตนซ์ที่อยู่ในรายการนั้นจะไม่สามารถพูดคุยกับอินสแตนซ์นี้ได้อีกต่อไป"
-silencedInstances: "ปิดปากอินสแตนซ์นี้แล้ว"
-silencedInstancesDescription: "ตั้งค่ารายชื่อโฮสต์ของอินสแตนซ์ที่คุณต้องการปิดปาก บัญชีทั้งหมดของอินสแตนซ์ที่อยู่ในรายชื่อนั้นๆ จะถือว่าถูกปิดปากเช่นกัน ทำได้เฉพาะคำขอติดตามเท่านั้น และไม่สามารถกล่าวถึงบัญชีในพื้นที่ได้หากไม่ได้ติดตาม | สิ่งนี้ไม่มีผลต่ออินสแตนซ์ที่ถูกบล็อก"
+blockedInstances: "เซิร์ฟเวอร์ที่ถูกบล็อก"
+blockedInstancesDescription: "ระบุโฮสต์ของเซิร์ฟเวอร์ที่ต้องการบล็อก คั่นด้วยการขึ้นบรรทัดใหม่ เซิร์ฟเวอร์ที่ถูกบล็อกจะไม่สามารถติดต่อกับอินสแตนซ์นี้ได้"
+silencedInstances: "ปิดปากเซิร์ฟเวอร์นี้แล้ว"
+silencedInstancesDescription: "ระบุโฮสต์ของเซิร์ฟเวอร์ที่ต้องการปิดปาก คั่นด้วยการขึ้นบรรทัดใหม่, บัญชีทั้งหมดของเซิร์ฟเวอร์ดังกล่าวจะถือว่าถูกปิดปากเช่นกัน ทำได้เฉพาะคำขอติดตามเท่านั้น และไม่สามารถกล่าวถึงบัญชีในเซิร์ฟเวอร์นี้ได้หากไม่ได้ถูกติดตามกลับ | สิ่งนี้ไม่มีผลต่ออินสแตนซ์ที่ถูกบล็อก"
+mediaSilencedInstances: "เซิร์ฟเวอร์ที่ถูกปิดปากสื่อ"
+mediaSilencedInstancesDescription: "ระบุโฮสต์ของเซิร์ฟเวอร์ที่ต้องการปิดปากสื่อ คั่นด้วยการขึ้นบรรทัดใหม่, ไฟล์ที่ถูกส่งจากบัญชีของเซิร์ฟเวอร์ดังกล่าวจะถือว่าถูกปิดปาก แล้วจะถูกติดเครื่องหมายว่ามีเนื้อหาละเอียดอ่อน และเอโมจิแบบกำหนดเองก็จะใช้ไม่ได้ด้วย | สิ่งนี้ไม่มีผลต่ออินสแตนซ์ที่ถูกบล็อก"
+federationAllowedHosts: "เซิร์ฟเวอร์ที่เปิดให้บริการแบบเฟเดอเรชั่น"
+federationAllowedHostsDescription: "ระบุชื่อโฮสต์ของเซิร์ฟเวอร์ที่คุณต้องการอนุญาตให้เชื่อมต่อแบบเฟเดอเรชั่น โดยต้องเว้นวรรคแต่ละบรรทัด"
 muteAndBlock: "ปิดเสียงและบล็อก"
 mutedUsers: "ผู้ใช้ที่ถูกปิดเสียง"
 blockedUsers: "ผู้ใช้ที่ถูกบล็อก"
@@ -240,14 +258,14 @@ noCustomEmojis: "ไม่มีเอโมจิ"
 noJobs: "ไม่มีงาน"
 federating: "สหพันธ์"
 blocked: "ถูกบล็อก"
-suspended: "ถูกระงับ"
+suspended: "ระงับการส่ง"
 all: "ทั้งหมด"
-subscribing: "สมัครแล้ว"
+subscribing: "กำลังสมัครสมาชิก"
 publishing: "กำลังเผยแพร่"
 notResponding: "ไม่มีการตอบสนอง"
-instanceFollowing: "กำลังติดตามบนอินสแตนซ์"
-instanceFollowers: "ผู้ติดตามของอินสแตนซ์"
-instanceUsers: "ผู้ใช้งานของอินสแตนซ์นี้"
+instanceFollowing: "กำลังติดตามบนเซิร์ฟเวอร์"
+instanceFollowers: "ผู้ติดตามของเซิร์ฟเวอร์"
+instanceUsers: "ผู้ใช้ของเซิร์ฟเวอร์นี้"
 changePassword: "เปลี่ยนรหัสผ่าน"
 security: "ความปลอดภัย"
 retypedNotMatch: "ทั้งสองป้อนข้อมูลไม่สอดคล้องกัน"
@@ -284,20 +302,20 @@ messageRead: "อ่านแล้ว"
 noMoreHistory: "ไม่มีประวัติเพิ่มเติม"
 startMessaging: "เริ่มการสนทนา"
 nUsersRead: "อ่านโดย {n}"
-agreeTo: "ฉันยอมรับที่จะ {0}"
+agreeTo: "ฉันยอมรับ {0}"
 agree: "ยอมรับ"
-agreeBelow: "ฉันยอมรับถึงด้านล่าง"
+agreeBelow: "ยอมรับตามที่ระบุด้านล่าง"
 basicNotesBeforeCreateAccount: "หมายเหตุสำคัญ"
 termsOfService: "เงื่อนไขการให้บริการ"
 start: "เริ่ม"
-home: "หน้าแรก"
-remoteUserCaution: "ข้อมูลอาจไม่สมบูรณ์เนื่องจากผู้ใช้รายนี้มาจากอินสแตนซ์ระยะไกล"
+home: "หน้าหลัก"
+remoteUserCaution: "ข้อมูลอาจไม่สมบูรณ์เนื่องจากผู้ใช้รายนี้มาจากเซิร์ฟเวอร์ระยะไกล"
 activity: "กิจกรรม"
 images: "รูปภาพ"
 image: "รูปภาพ"
 birthday: "วันเกิด"
 yearsOld: "{age} ปี"
-registeredDate: "วันที่สมัครสมาชิก"
+registeredDate: "วันที่ลงทะเบียน"
 location: "ตำแหน่งที่ตั้ง"
 theme: "ธีม"
 themeForLightMode: "ธีมที่จะใช้ในโหมดสว่าง"
@@ -313,6 +331,7 @@ selectFile: "เลือกไฟล์"
 selectFiles: "เลือกไฟล์"
 selectFolder: "เลือกโฟลเดอร์"
 selectFolders: "เลือกโฟลเดอร์"
+fileNotSelected: "ยังไม่ได้เลือกไฟล์"
 renameFile: "เปลี่ยนชื่อไฟล์"
 folderName: "ชื่อโฟลเดอร์"
 createFolder: "สร้างโฟลเดอร์"
@@ -320,6 +339,7 @@ renameFolder: "เปลี่ยนชื่อโฟลเดอร์"
 deleteFolder: "ลบโฟลเดอร์"
 folder: "โฟลเดอร์"
 addFile: "เพิ่มไฟล์"
+showFile: "แสดงไฟล์"
 emptyDrive: "ไดรฟ์ของคุณว่างเปล่านะ"
 emptyFolder: "โฟลเดอร์นี้ว่างเปล่า"
 unableToDelete: "ไม่สามารถลบออกได้"
@@ -336,15 +356,15 @@ displayOfSensitiveMedia: "แสดงสื่อที่มีเนื้อ
 whenServerDisconnected: "เมื่อสูญเสียการเชื่อมต่อกับเซิร์ฟเวอร์"
 disconnectedFromServer: "การเชื่อมต่อเซิร์ฟเวอร์ถูกตัด"
 reload: "รีโหลด"
-doNothing: "เมิน"
+doNothing: "ช่างมัน"
 reloadConfirm: "รีโหลดเลยไหม?"
-watch: "ดู"
-unwatch: "หยุดดู"
+watch: "เพ่งเล็ง"
+unwatch: "เลิกเพ่งเล็ง"
 accept: "ยอมรับ"
 reject: "ปฏิเสธ"
 normal: "ปกติ"
-instanceName: "ชื่ออินสแตนซ์"
-instanceDescription: "คำอธิบายอินสแตนซ์"
+instanceName: "ชื่อเซิร์ฟเวอร์"
+instanceDescription: "คำอธิบายแนะนำเซิร์ฟเวอร์"
 maintainerName: "ผู้ดูแล"
 maintainerEmail: "อีเมลผู้ดูแลระบบ"
 tosUrl: "URL เงื่อนไขการให้บริการ"
@@ -355,16 +375,16 @@ dayX: "{day}"
 monthX: "เดือน {month}"
 yearX: "{year}"
 pages: "หน้าเพจ"
-integration: "รวบรวม"
+integration: "เชื่อมโยง"
 connectService: "เชื่อมต่อ"
 disconnectService: "ตัดการเชื่อมต่อ"
-enableLocalTimeline: "เปิดใช้งานไทม์ไลน์ในพื้นที่"
+enableLocalTimeline: "เปิดใช้งานไทม์ไลน์ท้องถิ่น"
 enableGlobalTimeline: "เปิดใช้งานไทม์ไลน์ทั่วโลก"
 disablingTimelinesInfo: "ผู้ดูแลระบบและผู้ควบคุมจะสามารถเข้าถึงไทม์ไลน์ทั้งหมด ถึงแม้ว่าจะไม่ได้เปิดใช้งานก็ตาม"
 registration: "ลงทะเบียน"
 enableRegistration: "เปิดใช้งานการลงทะเบียนผู้ใช้ใหม่"
 invite: "คำเชิญ"
-driveCapacityPerLocalAccount: "ความจุของไดรฟ์ต่อผู้ใช้ภายในเครื่อง"
+driveCapacityPerLocalAccount: "ความจุของไดรฟ์ต่อผู้ใช้ท้องถิ่น"
 driveCapacityPerRemoteAccount: "ความจุของไดรฟ์ต่อผู้ใช้ระยะไกล"
 inMb: "เป็นเมกะไบต์"
 bannerUrl: "URL รูปภาพแบนเนอร์"
@@ -373,7 +393,7 @@ basicInfo: "ข้อมูลเบื้องต้น"
 pinnedUsers: "ผู้ใช้ที่ถูกปักหมุด"
 pinnedUsersDescription: "ป้อนชื่อผู้ใช้ที่คุณต้องการปักหมุดในหน้า “ค้นพบ” ฯลฯ คั่นด้วยการขึ้นบรรทัดใหม่"
 pinnedPages: "หน้าเพจที่ปักหมุด"
-pinnedPagesDescription: "ป้อนเส้นทางของหน้าเพจที่คุณต้องการปักหมุดไว้ที่หน้าแรกของอินสแตนซ์นี้ คั่นด้วยขึ้นบรรทัดใหม่"
+pinnedPagesDescription: "ป้อนเส้นทางของหน้าเพจที่คุณต้องการปักหมุดไว้ที่หน้าแรกของเซิร์ฟเวอร์นี้ คั่นด้วยการขึ้นบรรทัดใหม่"
 pinnedClipId: "ID ของคลิปที่จะปักหมุด"
 pinnedNotes: "โน้ตที่ปักหมุดไว้"
 hcaptcha: "hCaptcha"
@@ -389,11 +409,11 @@ recaptcha: "reCAPTCHA"
 enableRecaptcha: "เปิดใช้ reCAPTCHA"
 recaptchaSiteKey: "คีย์ไซต์"
 recaptchaSecretKey: "คีย์ลับ"
-turnstile: "เทิร์น'สไทล"
-enableTurnstile: "เปิดใช้งาน เทิร์น'สไทล"
+turnstile: "Turnstile"
+enableTurnstile: "เปิดใช้งาน Turnstile"
 turnstileSiteKey: "คีย์ไซต์"
 turnstileSecretKey: "คีย์ลับ"
-avoidMultiCaptchaConfirm: "การใช้ระบบ Captcha หลายระบบอาจทำให้เกิดการรบกวนหรืออาจจะเกิดข้อผิดพลาดได้ หากต้องการที่จะปิดการใช้งานระบบ Captcha อื่น ๆ แนะนำให้ปิดตัวอื่นๆก่อน ถ้าหากคุณต้องการให้เปิดใช้งานต่อไป ให้ กด ยกเลิก"
+avoidMultiCaptchaConfirm: "การใช้ Captcha หลายตัวอาจทำให้เกิดการรบกวนหรือข้อผิดพลาดได้ ต้องการที่จะปิดการใช้งาน Captcha ตัวอื่นเลยไหม? หากต้องการให้เปิดใช้งานต่อไป ให้กดยกเลิก"
 antennas: "เสาอากาศ"
 manageAntennas: "จัดการเสาอากาศ"
 name: "ชื่อ"
@@ -401,7 +421,7 @@ antennaSource: "แหล่งเสาอากาศ"
 antennaKeywords: "คีย์เวิร์ดที่ควรฟัง"
 antennaExcludeKeywords: "คีย์เวิร์ดที่จะยกเว้น"
 antennaExcludeBots: "ยกเว้นบัญชีบอต"
-antennaKeywordsDescription: "คั่นด้วยช่องว่างสำหรับเงื่อนไข AND หรือด้วยการขึ้นบรรทัดใหม่สำหรับเงื่อนไข OR"
+antennaKeywordsDescription: "คั่นด้วยเว้นวรรคสำหรับเงื่อนไข AND, หรือขึ้นบรรทัดใหม่สำหรับเงื่อนไข OR"
 notifyAntenna: "แจ้งเตือนเกี่ยวกับโน้ตใหม่"
 withFileAntenna: "เฉพาะโน้ตที่มีไฟล์"
 enableServiceworker: "เปิดใช้งานการแจ้งเตือนแบบพุชไปยังเบราว์เซอร์ของคุณ"
@@ -418,7 +438,7 @@ unsilenceConfirm: "ต้องการเลิกปิดปากผู้
 popularUsers: "ผู้ใช้ที่เป็นที่นิยม"
 recentlyUpdatedUsers: "ผู้ใช้ที่เพิ่งใช้งานล่าสุด"
 recentlyRegisteredUsers: "ผู้ใช้ที่เข้าร่วมใหม่"
-recentlyDiscoveredUsers: "ผู้ใช้ที่เพิ่งค้นพบใหม่"
+recentlyDiscoveredUsers: "ผู้ใช้ที่เพิ่งค้นพบล่าสุด"
 exploreUsersCount: "มีผู้ใช้ {count} ราย"
 exploreFediverse: "สำรวจสหพันธ์"
 popularTags: "แท็กยอดนิยม"
@@ -434,8 +454,9 @@ totpDescription: "ใช้แอปยืนยันตัวตนเพื
 moderator: "ผู้ควบคุม"
 moderation: "การกลั่นกรอง"
 moderationNote: "โน้ตการกลั่นกรอง"
+moderationNoteDescription: "คุณสามารถใส่โน้ตส่วนตัวที่เฉพาะผู้ดูแลระบบเท่านั้นที่สามารถเข้าถึงได้"
 addModerationNote: "เพิ่มโน้ตการกลั่นกรอง"
-moderationLogs: "ปูมการแก้ไข"
+moderationLogs: "ปูมการควบคุมดูแล"
 nUsersMentioned: "กล่าวถึงโดยผู้ใช้ {n} ราย"
 securityKeyAndPasskey: "ความปลอดภัยและรหัสผ่าน"
 securityKey: "กุญแจความปลอดภัย"
@@ -468,44 +489,49 @@ retype: "พิมพ์รหัสอีกครั้ง"
 noteOf: "โน้ตของ {user}"
 quoteAttached: "อ้างอิง"
 quoteQuestion: "ต้องการที่จะแนบมันเพื่ออ้างอิงใช่ไหม?"
+attachAsFileQuestion: "ข้อความในคลิปบอร์ดยาวเกินไป คุณต้องการแนบเป็นไฟล์ข้อความหรือไม่?"
 noMessagesYet: "ยังไม่มีข้อความ"
 newMessageExists: "คุณมีข้อความใหม่"
 onlyOneFileCanBeAttached: "สามารถแนบไฟล์ได้เพียงไฟล์เดียวต่อ 1 ข้อความ"
-signinRequired: "กรุณาลงทะเบียนหรือลงชื่อเข้าใช้ก่อนดำเนินการต่อ"
+signinRequired: "ก่อนดำเนินการต่อ กรุณาลงทะเบียนหรือเข้าสู่ระบบ"
+signinOrContinueOnRemote: "เพื่อดำเนินการต่อได้ คุณต้องไปที่เซิร์ฟเวอร์ที่คุณใช้งานอยู่ หรือลงทะเบียน/เข้าสู่ระบบเซิร์ฟเวอร์นี้"
 invitations: "คำเชิญ"
 invitationCode: "รหัสเชิญ"
 checking: "Checking"
 available: "พร้อมใช้งาน"
 unavailable: "ไม่พร้อมใช้"
-usernameInvalidFormat: "คุณสามารถใช้อักษรตัวพิมพ์ใหญ่และตัวพิมพ์เล็ก ตัวเลข และขีดล่างได้นะ ( a-z , A-Z , 0-9 , รวมไปถึงอักษรพิเศษเช่น + * / , . - อื่นๆเป็นต้น )"
+usernameInvalidFormat: "สามารถใช้ a~z A~Z 0~9 และ _ ได้"
 tooShort: "สั้นเกินไปนะ"
 tooLong: "ยาวเกินไปนะ"
-weakPassword: "รหัสผ่าน แย่มาก"
+weakPassword: "รหัสผ่านแย่มาก"
 normalPassword: "รหัสผ่านปกติ"
 strongPassword: "รหัสผ่านรัดกุมมาก"
 passwordMatched: "ถูกต้อง!"
 passwordNotMatched: "ไม่ถูกต้อง"
-signinWith: "ลงชื่อเข้าใช้ด้วย {x}"
-signinFailed: "ไม่สามารถลงชื่อผู้เข้าใช้ได้ เนื่องจาก ชื่อผู้ใช้หรือรหัสผ่านที่คุณป้อนนั้นไม่ถูกต้องนะ"
+signinWith: "เข้าสู่ระบบด้วย {x}"
+signinFailed: "ไม่สามารถเข้าสู่ระบบได้ กรุณาตรวจสอบชื่อผู้ใช้และรหัสผ่าน"
 or: "หรือ"
 language: "ภาษา"
 uiLanguage: "ภาษาอินเทอร์เฟซผู้ใช้งาน"
 aboutX: "เกี่ยวกับ {x}"
-emojiStyle: "สไตล์เอโมจิ"
+emojiStyle: "สไตล์ของเอโมจิ"
 native: "ภาษาแม่"
-disableDrawer: "อย่าใช้ลิ้นชักสไตล์เมนู"
-showNoteActionsOnlyHover: "แสดงการดำเนินการเฉพาะโน้ตเมื่อโฮเวอร์"
+menuStyle: "สไตล์เมนู"
+style: "สไตล์"
+drawer: "ตัววาด"
+popup: "ป๊อปอัพ"
+showNoteActionsOnlyHover: "แสดงการดำเนินการโน้ตเมื่อโฮเวอร์(วางเมาส์เหนือ)เท่านั้น"
 showReactionsCount: "แสดงจำนวนรีแอกชั่นในโน้ต"
 noHistory: "ไม่มีประวัติ"
 signinHistory: "ประวัติการเข้าสู่ระบบ"
 enableAdvancedMfm: "เปิดใช้งาน MFM ขั้นสูง"
-enableAnimatedMfm: "เปิดการใช้งาน MFM ด้วยแอนิเมชั่น"
+enableAnimatedMfm: "เปิดการใช้งาน MFM แบบเคลื่อนไหว"
 doing: "กำลังประมวลผล......"
 category: "หมวดหมู่"
 tags: "นามแฝง"
 docSource: "ที่มาของเอกสารนี้"
 createAccount: "สร้างบัญชี"
-existingAccount: "บัญชีที่มีอยู่"
+existingAccount: "บัญชีที่มีอยู่แล้ว"
 regenerate: "สร้างอีกครั้ง"
 fontSize: "ขนาดตัวอักษร"
 mediaListWithOneImageAppearance: "ความสูงของรายการสื่อที่มีเพียงรูปเดียว"
@@ -513,11 +539,11 @@ limitTo: "จำกัดไว้ที่ {x}"
 noFollowRequests: "คุณไม่มีคำขอติดตามที่รอดำเนินการ"
 openImageInNewTab: "เปิดรูปภาพในแท็บใหม่"
 dashboard: "หน้ากระดานหลัก"
-local: "ในพื้นที่"
+local: "ท้องถิ่น"
 remote: "ระยะไกล"
 total: "รวมทั้งหมด"
-weekOverWeekChanges: "เปลี่ยนแปลงไปเมื่อสัปดาห์ที่แล้ว"
-dayOverDayChanges: "เปลี่ยนแปลงไปเมื่อวานนี้"
+weekOverWeekChanges: "เทียบกับสัปดาห์ก่อน"
+dayOverDayChanges: "เทียบกับเมื่อวาน"
 appearance: "ภาพลักษณ์"
 clientSettings: "การตั้งค่าไคลเอนต์"
 accountSettings: "ตั้งค่าบัญชี"
@@ -531,19 +557,19 @@ useObjectStorage: "ใช้การจัดเก็บในรูปแบ
 objectStorageBaseUrl: "Base URL"
 objectStorageBaseUrlDesc: "URL ที่ใช้เป็นข้อมูลอ้างอิง ระบุ URL ของ CDN หรือ Proxy ถ้าหากคุณใช้อย่างใดอย่างหนึ่ง\n สำหรับการใช้งาน S3 'https://<bucket>.s3.amazonaws.com' และสำหรับ GCS หรือบริการที่เทียบเท่าใช้ 'https://storage.googleapis.com/<bucket>', เป็นต้น"
 objectStorageBucket: "Bucket"
-objectStorageBucketDesc: "โปรดระบุชื่อที่เก็บข้อมูลที่ใช้กับผู้ให้บริการของคุณ"
+objectStorageBucketDesc: "โปรดระบุชื่อบัคเก็ตของบริการที่ใช้อยู่"
 objectStoragePrefix: "คำนำหน้า"
 objectStoragePrefixDesc: "ไฟล์ทั้งหมดจะถูกเก็บไว้ภายใต้ไดเร็กทอรีที่มีคำนำหน้านี้"
 objectStorageEndpoint: "ปลายทาง"
 objectStorageEndpointDesc: "เว้นว่างไว้หากคุณใช้ AWS S3 หรือระบุปลายทางเป็น '<host>' หรือ '<host>:<port>' ทั้งนี้ขึ้นอยู่กับผู้ให้บริการที่คุณใช้อยู่ด้วย"
 objectStorageRegion: "ภูมิภาค"
-objectStorageRegionDesc: "ระบุภูมิภาค เช่น 'xx-east-1' ถ้าหากบริการของคุณไม่ได้แยกความแตกต่างระหว่างภูมิภาคก็ให้ เว้นว่างไว้หรือป้อน 'us-east-1'"
+objectStorageRegionDesc: "ระบุภูมิภาค เช่น ‘xx-east-1’ หากบริการของคุณไม่แยกภูมิภาค ให้ระบุเป็น ‘us-east-1’ หรือเว้นวางไว้หากใช้ AWS configuration files / environment variables"
 objectStorageUseSSL: "ใช้ SSL"
 objectStorageUseSSLDesc: "ปิดการทำงานนี้ไว้ ถ้าหากคุณจะไม่ใช้ HTTPS สำหรับการเชื่อมต่อ API"
 objectStorageUseProxy: "เชื่อมต่อผ่านพร็อกซี"
 objectStorageUseProxyDesc: "ปิดสิ่งนี้ไว้ถ้าหากคุณจะไม่ใช้ Proxy สำหรับการเชื่อมต่อ API"
 objectStorageSetPublicRead: "ตั้งค่าเป็น “public-read” เมื่ออัปโหลด"
-s3ForcePathStyleDesc: "ถ้าหากเปิดใช้งาน s3ForcePathStyle ชื่อบัคเก็ตนั้นอาจจะต้องรวมอยู่ในเส้นทางของ URL ซึ่งตรงข้ามกับชื่อโฮสต์ของ URL คุณอาจจะต้องเปิดใช้งานการตั้งค่านี้เมื่อใช้บริการต่างๆ เช่น อินสแตนซ์ Minio ที่โฮสต์เองนะ"
+s3ForcePathStyleDesc: "เมื่อเปิดใช้งาน s3ForcePathStyle จะบังคับให้ ระบุชื่อบัคเก็ตเป็นส่วนหนึ่งของพาธ แทนที่จะเป็นชื่อโฮสต์ใน URL, อาจจำเป็นต้องเปิดใช้งานตัวเลือกนี้เมื่อใช้กับ Minio ที่โฮสต์เองหรือบริการที่คล้ายกัน"
 serverLogs: "ปูมของเซิร์ฟเวอร์"
 deleteAll: "ลบทั้งหมด"
 showFixedPostForm: "แสดงแบบฟอร์มการโพสต์ที่ด้านบนสุดของไทม์ไลน์"
@@ -575,7 +601,9 @@ sort: "เรียงลำดับ"
 ascendingOrder: "เรียงลำดับขึ้น"
 descendingOrder: "เรียงลำดับลง"
 scratchpad: "Scratchpad"
-scratchpadDescription: "Scratchpad เป็นการจัดเตรียมสภาพแวดล้อมสำหรับการทดลอง AiScript แต่คุณสามารถเขียน ดำเนินการ และตรวจสอบผลลัพธ์ของการโต้ตอบกับ Misskey มันได้ด้วยนะ"
+scratchpadDescription: "Scratchpad ให้สภาพแวดล้อมสำหรับการทดลอง AiScript คุณสามารถเขียนโค้ด/สั่งดำเนินการ/ตรวจสอบผลลัพธ์ ของการโต้ตอบกับ Misskey ได้"
+uiInspector: "ตัวตรวจสอบ UI"
+uiInspectorDescription: "คุณสามารถตรวจสอบรายชื่อเซิร์ฟเวอร์ที่เกี่ยวข้องกับส่วนประกอบอินเตอร์เฟซผู้ใช้ (UI) บนหน่วยความจำของระบบ ส่วนประกอบ UI เหล่านี้จะถูกสร้างขึ้นโดยฟังก์ชัน Ui:C:"
 output: "เอาท์พุต"
 script: "สคริปต์"
 disablePagesScript: "ปิดการใช้งาน AiScript บนเพจ"
@@ -587,15 +615,15 @@ unsetUserBannerConfirm: "ต้องการเลิกตั้งแบน
 deleteAllFiles: "ลบไฟล์ทั้งหมด"
 deleteAllFilesConfirm: "ต้องการลบไฟล์ทั้งหมดใช่ไหม?"
 removeAllFollowing: "เลิกติดตามผู้ใช้ที่ติดตามทั้งหมด"
-removeAllFollowingDescription: "เลิกติดตามทั้งหมดจาก {host} โปรดเรียกใช้สิ่งนี้เมื่ออินสแตนซ์ดังกล่าวได้สูญหายตายจากไปแล้ว"
+removeAllFollowingDescription: "จะเลิกติดตามทั้งหมดจาก {host} โปรดดำเนินการสิ่งนี้เมื่อเซิร์ฟเวอร์ดังกล่าวได้สูญหายตายจากไปแล้ว"
 userSuspended: "ผู้ใช้รายนี้ถูกระงับการใช้งาน"
 userSilenced: "ผู้ใช้รายนี้ถูกปิดปากอยู่"
 yourAccountSuspendedTitle: "บัญชีนี้นั้นถูกระงับ"
 yourAccountSuspendedDescription: "บัญชีนี้ถูกระงับ เนื่องจากละเมิดข้อกำหนดในการให้บริการของเซิร์ฟเวอร์หรืออาจจะละเมิดหลักเกณฑ์ชุมชน หรือ อาจจะโดนร้องเรียนเรื่องการละเมิดลิขสิทธิ์และอื่นๆอย่างต่อเนื่องซ้ำๆ หากคุณคิดว่าไม่ได้ทำผิดจริงๆหรือตัดสินผิดพลาด ได้โปรดกรุณาติดต่อผู้ดูแลระบบหากคุณต้องการทราบเหตุผลโดยละเอียดเพิ่มเติม และขอความกรุณาอย่าสร้างบัญชีใหม่"
 tokenRevoked: "โทเค็นไม่ถูกต้อง"
-tokenRevokedDescription: "โทเค็นนี้หมดอายุแล้วนะค่ะกรุณาเข้าสู่ระบบอีกครั้งนะ"
+tokenRevokedDescription: "โทเค็นการเข้าสู่ระบบหมดอายุ กรุณาเข้าสู่ระบบใหม่อีกครั้ง"
 accountDeleted: "ลบบัญชีแล้ว"
-accountDeletedDescription: "บัญชีนี้ถูกลบไปแล้วนะ"
+accountDeletedDescription: "บัญชีนี้ถูกลบแล้ว"
 menu: "เมนู"
 divider: "ตัวแบ่ง"
 addItem: "เพิ่มรายการ"
@@ -615,14 +643,14 @@ enablePlayer: "เปิดเครื่องเล่นวิดีโอ"
 disablePlayer: "ปิดเครื่องเล่นวิดีโอ"
 expandTweet: "ขยายทวีต"
 themeEditor: "ตัวแก้ไขธีม"
-description: "รายละเอียด"
+description: "คำอธิบาย"
 describeFile: "เพิ่มแคปชั่น"
 enterFileDescription: "ใส่แคปชั่น"
 author: "ผู้เขียน"
 leaveConfirm: "มีการเปลี่ยนแปลงที่ยังไม่ได้บันทึก ต้องการละทิ้งมันใช่ไหม?"
 manage: "การจัดการ"
 plugins: "ปลั๊กอิน"
-preferencesBackups: "ตั้งค่าการสำรองข้อมูล"
+preferencesBackups: "สำรองการตั้งค่า"
 deck: "เด็ค"
 undeck: "ออกจากเด็ค"
 useBlurEffectForModal: "ใช้เอฟเฟกต์เบลอสำหรับโมดอล"
@@ -632,21 +660,21 @@ height: "ความสูง"
 large: "ใหญ่"
 medium: "ปานกลาง"
 small: "เล็ก"
-generateAccessToken: "สร้างการเข้าถึงโทเค็น"
-permission: "การอนุญาต"
+generateAccessToken: "สร้างโทเค็นการเข้าถึง"
+permission: "สิทธิ์"
 adminPermission: "สิทธิ์ของผู้ดูแลระบบ"
 enableAll: "เปิดใช้งานทั้งหมด"
 disableAll: "ปิดการใช้งานทั้งหมด"
 tokenRequested: "ให้สิทธิ์การเข้าถึงบัญชี"
-pluginTokenRequestedDescription: "ปลั๊กอินนี้จะสามารถใช้การอนุญาตที่ตั้งค่าไว้ที่นี่นะ"
+pluginTokenRequestedDescription: "ปลั๊กอินนี้จะใช้สิทธิ์ตามที่ตั้งค่าไว้ที่นี่"
 notificationType: "ประเภทการแจ้งเตือน"
 edit: "แก้ไข"
-emailServer: "อีเมลเซิร์ฟเวอร์"
+emailServer: "เซิร์ฟเวอร์ของอีเมล"
 enableEmail: "เปิดใช้งานการกระจายอีเมล"
-emailConfigInfo: "ใช้เพื่อยืนยันอีเมลของคุณระหว่างการสมัครหรือถ้าหากคุณลืมรหัสผ่าน"
+emailConfigInfo: "ใช้สำหรับการยืนยันอีเมลหรือการรีเซ็ตรหัสผ่าน"
 email: "อีเมล"
 emailAddress: "ที่อยู่อีเมล"
-smtpConfig: "กำหนดค่าเซิร์ฟเวอร์ SMTP"
+smtpConfig: "ตั้งค่าเซิร์ฟเวอร์ SMTP"
 smtpHost: "โฮสต์"
 smtpPort: "พอร์ต"
 smtpUser: "ชื่อผู้ใช้"
@@ -656,10 +684,10 @@ smtpSecure: "ใช้โดยนัย SSL/TLS สำหรับการเ
 smtpSecureInfo: "ปิดสิ่งนี้เมื่อใช้ STARTTLS"
 testEmail: "ทดสอบการส่งอีเมล"
 wordMute: "ปิดเสียงคำ"
-hardWordMute: "ปิดเสียงคำยาก"
-regexpError: "ข้อผิดพลาดของนิพจน์ทั่วไป"
-regexpErrorDescription: "เกิดข้อผิดพลาดในนิพจน์ทั่วไปในบรรทัดที่ {line} ของการปิดเสียงคำ {tab} ของคุณ:"
-instanceMute: "ปิดเสียง อินสแตนซ์"
+hardWordMute: "ปิดเสียงคำแบบแข็งโป๊ก"
+regexpError: "เกิดข้อผิดพลาดใน regular expression"
+regexpErrorDescription: "เกิดข้อผิดพลาดใน regular expression บรรทัดที่ {line} ของการปิดเสียงคำ {tab} :"
+instanceMute: "ปิดเสียงเซิร์ฟเวอร์"
 userSaysSomething: "{name} พูดอะไรบางอย่าง"
 makeActive: "เปิดใช้งาน"
 display: "แสดงผล"
@@ -690,17 +718,14 @@ reportAbuseOf: "รายงาน {name}"
 fillAbuseReportDescription: "กรุณากรอกรายละเอียดเกี่ยวกับรายงานนี้ หากเป็นเรื่องเกี่ยวกับโน้ตโดยเฉพาะ ได้โปรดระบุ URL"
 abuseReported: "เราได้ส่งรายงานของคุณไปแล้ว ขอบคุณมากๆนะ"
 reporter: "ผู้รายงาน"
-reporteeOrigin: "รายงานต้นทาง"
+reporteeOrigin: "ปลายทางรายงาน"
 reporterOrigin: "แหล่งผู้รายงาน"
-forwardReport: "ส่งต่อรายงานไปยังอินสแตนซ์ระยะไกล"
-forwardReportIsAnonymous: "ข้อมูลของคุณจะไม่ปรากฏบนอินสแตนซ์ระยะไกลและปรากฏเป็นบัญชีระบบที่ไม่ระบุชื่อ"
 send: "ส่ง"
-abuseMarkAsResolved: "ทำเครื่องหมายรายงานว่าแก้ไขแล้ว"
 openInNewTab: "เปิดในแท็บใหม่"
 openInSideView: "เปิดในมุมมองด้านข้าง"
 defaultNavigationBehaviour: "พฤติกรรมการนำทางที่เป็นค่าเริ่มต้น"
 editTheseSettingsMayBreakAccount: "การแก้ไขการตั้งค่าเหล่านี้อาจทำให้บัญชีของคุณเสียหายนะ"
-instanceTicker: "ข้อมูลอินสแตนซ์ของโน้ต"
+instanceTicker: "ข้อมูลเซิร์ฟเวอร์ของโน้ต"
 waitingFor: "กำลังรอ {x}"
 random: "สุ่มค่า"
 system: "ระบบ"
@@ -739,7 +764,7 @@ alwaysMarkSensitive: "ทำเครื่องหมายว่ามีเ
 loadRawImages: "โหลดภาพต้นฉบับแทนการแสดงภาพขนาดย่อ"
 disableShowingAnimatedImages: "ไม่ต้องเล่นภาพเคลื่อนไหว"
 highlightSensitiveMedia: "ไฮไลท์สื่อที่มีเนื้อหาละเอียดอ่อน"
-verificationEmailSent: "ส่งอีเมลยืนยันแล้วนะ ได้โปรดกรุณาไปที่ลิงก์ที่รวมไว้เพื่อทำการตรวจสอบให้เสร็จสิ้น"
+verificationEmailSent: "ได้ส่งอีเมลยืนยันแล้ว กรุณาเข้าลิงก์ที่ระบุในอีเมลเพื่อทำการตั้งค่าให้เสร็จสิ้น"
 notSet: "ไม่ได้ตั้งค่า"
 emailVerified: "อีเมลได้รับการยืนยันแล้ว"
 noteFavoritesCount: "จำนวนโน้ตที่ชื่นชอบ"
@@ -750,7 +775,7 @@ useSystemFont: "ใช้ฟอนต์เริ่มต้นของระ
 clips: "คลิป"
 experimentalFeatures: "ฟังก์ชั่นทดสอบ"
 experimental: "ทดลอง"
-thisIsExperimentalFeature: "นี่คือฟีเจอร์ทดลองนะค่ะ ฟังก์ชันการทำงานบางอย่างอาจเปลี่ยนแปลงได้ และอาจไม่ทำงานหรือไม่เสถียรตามที่ตั้งใจไว้นะ"
+thisIsExperimentalFeature: "นี่เป็นฟีเจอร์ทดลอง ซึ่งอาจมีการเปลี่ยนแปลงการทำงาน และอาจไม่ทำงานตามที่ตั้งใจไว้"
 developer: "สำหรับนักพัฒนา"
 makeExplorable: "ทำให้บัญชีมองเห็นใน “สำรวจ”"
 makeExplorableDescription: "ถ้าหากคุณปิดการทำงานนี้ บัญชีของคุณนั้นจะไม่แสดงในส่วน “สำรวจ”"
@@ -761,14 +786,14 @@ center: "กึ่งกลาง"
 wide: "กว้าง"
 narrow: "ชิด"
 reloadToApplySetting: "การตั้งค่านี้จะมีผลหลังจากโหลดหน้าซ้ำเท่านั้น ต้องการที่จะโหลดใหม่เลยไหม?"
-needReloadToApply: "จำเป็นต้องโหลดซ้ำถึงจะมีผลนะ"
+needReloadToApply: "ต้องรีโหลดเพื่อให้การเปลี่ยนแปลงมีผล"
 showTitlebar: "แสดงแถบชื่อ"
 clearCache: "ล้างแคช"
-onlineUsersCount: "{n} ผู้ใช้คนนี้กำลังออนไลน์"
+onlineUsersCount: "{n} รายกำลังออนไลน์"
 nUsers: "{n} ผู้ใช้งาน"
 nNotes: "{n} โน้ต"
-sendErrorReports: "ส่งรายงานว่าข้อผิดพลาด"
-sendErrorReportsDescription: "เมื่อเปิดใช้งาน ข้อมูลข้อผิดพลาดโดยรายละเอียดนั้นจะถูกแชร์ให้กับ Misskey เมื่อเกิดปัญหา ซึ่งช่วยปรับปรุงคุณภาพของ Misskey\nซึ่งจะรวมถึงข้อมูล เช่น เวอร์ชั่นของระบบปฏิบัติการ เบราว์เซอร์ที่คุณใช้ กิจกรรมของคุณใน Misskey เป็นต้น"
+sendErrorReports: "ส่งรายงานข้อผิดพลาด"
+sendErrorReportsDescription: "เมื่อเปิดใช้งาน การแจ้งข้อผิดพลาดจะถูกแชร์กับ Misskey เมื่อเกิดปัญหา ซึ่งช่วยในการปรับปรุงคุณภาพของซอฟต์แวร์ ข้อมูลข้อผิดพลาดอาจรวมถึงเวอร์ชันของระบบปฏิบัติการ ประเภทของเบราว์เซอร์ และประวัติการใช้งาน ฯลฯ"
 myTheme: "ธีมของฉัน"
 backgroundColor: "สีพื้นหลัง"
 accentColor: "สีหลัก"
@@ -780,7 +805,7 @@ value: "ค่า"
 createdAt: "สร้างเมื่อ"
 updatedAt: "อัปเดตล่าสุด"
 saveConfirm: "บันทึกเปลี่ยนแปลงมั้ย?"
-deleteConfirm: "ลบจริงๆเหรอ?"
+deleteConfirm: "ต้องการลบใช่ไหม?"
 invalidValue: "ค่านี้ไม่ถูกต้อง"
 registry: "ทะเบียน"
 closeAccount: "ปิด บัญชี"
@@ -793,7 +818,7 @@ capacity: "ความจุ"
 inUse: "ใช้แล้ว"
 editCode: "แก้ไขโค้ด"
 apply: "นำไปใช้"
-receiveAnnouncementFromInstance: "รับการแจ้งเตือนจากอินสแตนซ์นี้"
+receiveAnnouncementFromInstance: "รับการแจ้งเตือนจากเซิร์ฟเวอร์นี้"
 emailNotification: "การแจ้งเตือนทางอีเมล"
 publish: "เผยแพร่"
 inChannelSearch: "ค้นหาในช่อง"
@@ -821,7 +846,7 @@ active: "ใช้งานอยู่"
 offline: "ออฟไลน์"
 notRecommended: "ไม่แนะนำ"
 botProtection: "การป้องกัน Bot"
-instanceBlocking: "อินสแตนซ์ที่ถูกบล็อก"
+instanceBlocking: "เซิร์ฟเวอร์ที่ถูกบล็อก/ปิดปาก"
 selectAccount: "เลือกบัญชี"
 switchAccount: "สลับบัญชีผู้ใช้"
 enabled: "เปิดใช้งาน"
@@ -831,9 +856,10 @@ user: "ผู้ใช้"
 administration: "การจัดการ"
 accounts: "บัญชีผู้ใช้"
 switch: "สลับ"
-noMaintainerInformationWarning: "ข้อมูลผู้ดูแลไม่ได้รับการกำหนดค่านะ"
-noBotProtectionWarning: "ไม่ได้กำหนดค่าการป้องกันบอทนะ"
-configure: "กำหนดค่า"
+noMaintainerInformationWarning: "ยังไม่ได้ตั้งค่าข้อมูลของผู้ดูแลระบบ"
+noInquiryUrlWarning: "ยังไม่ได้ตั้งค่า URL สำหรับการติดต่อสอบถาม"
+noBotProtectionWarning: "ยังไม่ได้ตั้งค่าการป้องกันบอต"
+configure: "ตั้งค่า"
 postToGallery: "สร้างโพสต์แกลเลอรี่ใหม่"
 postToHashtag: "โพสต์ไปที่แฮชแท็กนี้"
 gallery: "แกลเลอรี่"
@@ -870,7 +896,7 @@ accountDeletionInProgress: "กำลังดำเนินการลบบ
 usernameInfo: "ชื่อที่ระบุบัญชีของคุณจากผู้อื่นในเซิร์ฟเวอร์นี้ คุณสามารถใช้ตัวอักษร (a~z, A~Z), ตัวเลข (0~9) หรือขีดล่าง (_) ชื่อผู้ใช้ไม่สามารถเปลี่ยนแปลงได้ในภายหลัง"
 aiChanMode: "โหมด Ai "
 devMode: "โหมดนักพัฒนา"
-keepCw: "เก็บคำเตือนเนื้อหา"
+keepCw: "คงการเตือนเนื้อหาไว้"
 pubSub: "บัญชี Pub/Sub"
 lastCommunication: "การสื่อสารครั้งสุดท้ายล่าสุด"
 resolved: "คลี่คลายแล้ว"
@@ -896,6 +922,7 @@ followersVisibility: "การมองเห็นผู้ที่กำล
 continueThread: "ดูความต่อเนื่องเธรด"
 deleteAccountConfirm: "การดำเนินการนี้จะลบบัญชีของคุณอย่างถาวรเลยนะ แน่ใจหรอดำเนินการ?"
 incorrectPassword: "รหัสผ่านไม่ถูกต้อง"
+incorrectTotp: "รหัสยืนยันตัวตนแบบใช้ครั้งเดียวที่ท่านได้ระบุมานั้น ไม่ถูกต้องหรือหมดอายุลงแล้วค่ะ"
 voteConfirm: "ต้องการโหวต “{choice}” ใช่ไหม?"
 hide: "ซ่อน"
 useDrawerReactionPickerForMobile: "แสดง ตัวจิ้มรีแอคชั่น เป็นแบบลิ้นชัก เมื่อใช้บนมือถือ"
@@ -909,8 +936,8 @@ themeColor: "สีธีม"
 size: "ขนาด"
 numberOfColumn: "จำนวนคอลัมน์"
 searchByGoogle: "ค้นหา"
-instanceDefaultLightTheme: "ธีมสว่างตามค่าเริ่มต้นของอินสแตนซ์"
-instanceDefaultDarkTheme: "ธีมมืดตามค่าเริ่มต้นของอินสแตนซ์"
+instanceDefaultLightTheme: "ธีมสว่างตามค่าเริ่มต้นของเซิร์ฟเวอร์"
+instanceDefaultDarkTheme: "ธีมมืดตามค่าเริ่มต้นของเซิร์ฟเวอร์"
 instanceDefaultThemeDescription: "ป้อนรหัสธีมในรูปแบบออบเจ็กต์"
 mutePeriod: "ระยะเวลาปิดเสียง"
 period: "ระยะเวลา"
@@ -930,7 +957,7 @@ cropNo: "ใช้ตามที่เป็นอยู่"
 file: "ไฟล์"
 recentNHours: "ล่าสุด {n} ชั่วโมงที่แล้ว"
 recentNDays: "ล่าสุด {n} วันที่แล้ว"
-noEmailServerWarning: "ไม่ได้กำหนดค่าเซิร์ฟเวอร์อีเมลนี้"
+noEmailServerWarning: "ยังไม่ได้ตั้งค่าเซิร์ฟเวอร์ของอีเมล"
 thereIsUnresolvedAbuseReportWarning: "มีรายงานที่ยังไม่ได้แก้ไข"
 recommended: "แนะนำ"
 check: "ตรวจสอบ"
@@ -965,7 +992,7 @@ cannotUploadBecauseExceedsFileSizeLimit: "ไม่สามารถอัป
 beta: "เบต้า"
 enableAutoSensitive: "ทำเครื่องหมายว่ามีเนื้อหาที่ละเอียดอ่อนโดยอัตโนมัติ"
 enableAutoSensitiveDescription: "อนุญาตให้ตรวจหาและทำเครื่องหมายสื่อว่ามีเนื้อหาโดยละเอียดอ่อนโดยอัตโนมัติ ผ่าน Machine Learning หากเป็นไปได้ แม้ว่าคุณจะปิดคุณสมบัตินี้ ก็อาจถูกตั้งค่าโดยอัตโนมัติ ทั้งนี้ขึ้นอยู่กับเซิร์ฟเวอร์"
-activeEmailValidationDescription: "เปิดใช้งานการตรวจสอบที่อยู่อีเมลให้มีความเข้มงวดยิ่งขึ้น ซึ่งอาจจะรวมไปถึงการตรวจสอบที่อยู่อีเมล์ที่ใช้แล้วทิ้งและโดยให้พิจารณาว่าสามารถสื่อสารด้วยได้หรือไม่ เมื่อไม่เลือกระบบจะตรวจสอบเฉพาะรูปแบบของอีเมลเท่านั้น"
+activeEmailValidationDescription: "การตรวจสอบอีเมลของผู้ใช้จะเข้มงวดมากขึ้น โดยพิจารณาว่าเป็นอีเมลชั่วคราวหรือไม่ และสามารถติดต่อได้จริงหรือไม่ หากปิดการตรวจสอบนี้ จะตรวจสอบเพียงว่ารูปแบบอีเมลที่ถูกต้องหรือไม่เท่านั้น"
 navbar: "แถบนำทาง"
 shuffle: "สลับ"
 account: "บัญชีผู้ใช้"
@@ -974,7 +1001,7 @@ pushNotification: "การแจ้งเตือนแบบพุช"
 subscribePushNotification: "เปิดการแจ้งเตือนแบบพุช"
 unsubscribePushNotification: "ปิดการแจ้งเตือนแบบพุช"
 pushNotificationAlreadySubscribed: "การแจ้งเตือนแบบพุชได้เปิดใช้งานแล้ว"
-pushNotificationNotSupported: "เบราว์เซอร์หรืออินสแตนซ์ของคุณนั้นไม่รองรับการแจ้งเตือนแบบพุช"
+pushNotificationNotSupported: "เบราว์เซอร์หรือเซิร์ฟเวอร์ไม่รองรับการแจ้งเตือนแบบพุช"
 sendPushNotificationReadMessage: "ลบการแจ้งเตือนแบบพุชเมื่ออ่านการแจ้งเตือนหรือข้อความที่เกี่ยวข้องแล้ว"
 sendPushNotificationReadMessageCaption: "อาจทำให้อุปกรณ์ของคุณใช้พลังงานมากขึ้น"
 windowMaximize: "ขยายใหญ่สุด"
@@ -1017,36 +1044,37 @@ achievements: "ความสำเร็จ"
 gotInvalidResponseError: "การตอบสนองเซิร์ฟเวอร์ไม่ถูกต้อง"
 gotInvalidResponseErrorDescription: "เซิร์ฟเวอร์อาจไม่สามารถเข้าถึงได้หรืออาจจะกำลังอยู่ในระหว่างปรับปรุง กรุณาลองใหม่อีกครั้งในภายหลังนะคะ"
 thisPostMayBeAnnoying: "โน้ตนี้อาจจะเป็นการรบกวนผู้อื่นนะคะ"
-thisPostMayBeAnnoyingHome: "โพสต์ไปยังไทม์ไลน์หน้าแรก"
-thisPostMayBeAnnoyingCancel: "เลิก"
-thisPostMayBeAnnoyingIgnore: "โพสต์ยังไงก็แล้วแต่"
+thisPostMayBeAnnoyingHome: "โพสต์ลงไทม์ไลน์หลักเท่านั้น"
+thisPostMayBeAnnoyingCancel: "ยกเลิก"
+thisPostMayBeAnnoyingIgnore: "โพสต์ไปเลย ไม่ต้องปรับการมองเห็น"
 collapseRenotes: "ยุบรีโน้ตที่คุณเคยเห็นแล้ว"
+collapseRenotesDescription: "พับย่อโน้ตที่เคยตอบสนองหรือรีโน้ตแล้ว"
 internalServerError: "เซิร์ฟเวอร์ภายในเกิดข้อผิดพลาด"
-internalServerErrorDescription: "เซิร์ฟเวอร์รันค้นพบข้อผิดพลาดที่ไม่คาดคิด"
+internalServerErrorDescription: "เกิดข้อผิดพลาดที่ไม่คาดคิดภายในเซิร์ฟเวอร์"
 copyErrorInfo: "คัดลอกรายละเอียดข้อผิดพลาด"
-joinThisServer: "ลงชื่อสมัครใช้ในอินสแตนซ์นี้"
-exploreOtherServers: "มองหาอินสแตนซ์อื่น"
+joinThisServer: "ลงทะเบียนในเซิร์ฟเวอร์นี้"
+exploreOtherServers: "มองหาเซิร์ฟเวอร์อื่น"
 letsLookAtTimeline: "มาดูไทม์ไลน์กัน"
-disableFederationConfirm: "ปิดใช้งานสหพันธ์จริงๆหรอแน่ใจแล้วนะ?"
+disableFederationConfirm: "ปิดใช้งานสหพันธ์เลยใช่ไหม?"
 disableFederationConfirmWarn: "โพสต์จะยังคงเป็นสาธารณะต่อไป เว้นแต่จะตั้งค่าเป็นอย่างอื่น"
 disableFederationOk: "ปิดการใช้งาน"
-invitationRequiredToRegister: "อินสแตนซ์นี้เป็นแบบรับเชิญ เฉพาะผู้ที่มีรหัสเชิญเท่านั้นที่สามารถลงทะเบียนได้"
-emailNotSupported: "อินสแตนซ์นี้ไม่รองรับการส่งอีเมล"
+invitationRequiredToRegister: "เซิร์ฟเวอร์นี้เป็นแบบรับเชิญ เฉพาะผู้มีรหัสเชิญเท่านั้นถึงสามารถลงทะเบียนได้"
+emailNotSupported: "เซิร์ฟเวอร์นี้ไม่รองรับการส่งอีเมล"
 postToTheChannel: "โพสต์ลงช่อง"
 cannotBeChangedLater: "สิ่งนี้ไม่สามารถเปลี่ยนแปลงได้ในภายหลังนะ"
 reactionAcceptance: "การยอมรับรีแอคชั่น"
 likeOnly: "ที่ถูกใจเท่านั้น"
-likeOnlyForRemote: "ทั้งหมด (เฉพาะการถูกใจจากอินสแตนซ์ระยะไกล)"
+likeOnlyForRemote: "ทั้งหมด (เฉพาะการถูกใจจากเซิร์ฟเวอร์ระยะไกล)"
 nonSensitiveOnly: "เฉพาะไม่มีเนื้อหาละเอียดอ่อน"
 nonSensitiveOnlyForLocalLikeOnlyForRemote: "เฉพาะไม่มีเนื้อหาละเอียดอ่อน (เฉพาะการถูกใจจากระยะไกลเท่านั้น)"
 rolesAssignedToMe: "บทบาทที่ได้รับมอบหมายให้ฉัน"
-resetPasswordConfirm: "รีเซ็ตรหัสผ่านของคุณจริงๆหรอ?"
+resetPasswordConfirm: "ต้องการรีเซ็ตรหัสผ่านใช่ไหม?"
 sensitiveWords: "คำที่มีเนื้อหาละเอียดอ่อน"
-sensitiveWordsDescription: "การเปิดเผยโน้ตทั้งหมดที่มีคำที่กำหนดค่าไว้จะถูกตั้งค่าเป็น \"หน้าแรก\" โดยอัตโนมัติ คุณยังสามารถแสดงหลายรายการได้โดยแยกรายการโดยใช้ตัวแบ่งบรรทัดได้นะ"
-sensitiveWordsDescription2: "การใช้ช่องว่างนั้นอาจจะสร้างนิพจน์ AND และคำหลักที่มีเครื่องหมายทับล้อมรอบจะเปลี่ยนเป็นนิพจน์ทั่วไปนะ"
+sensitiveWordsDescription: "โน้ตที่มีคำที่ระบุไว้จะถูกตั้งค่าการมองเห็นของให้แสดงเฉพาะในหน้าหลักเท่านั้น คั่นคำด้วยการขึ้นบรรทัดใหม่"
+sensitiveWordsDescription2: "ถ้าแยกด้วยเว้นวรรคจะเป็นการระบุ AND และถ้าล้อมคำด้วยสแลช (/) จะเป็นการใช้ regular expression"
 prohibitedWords: "คำต้องห้าม"
 prohibitedWordsDescription: "จะแจ้งเตือนว่าเกิดข้อผิดพลาดเมื่อพยายามโพสต์โน้ตที่มีคำที่กำหนดไว้ สามารถตั้งได้หลายคำด้วยการขึ้นบรรทัดใหม่"
-prohibitedWordsDescription2: "การใช้ช่องว่างนั้นอาจจะสร้างนิพจน์ AND และคำหลักที่มีเครื่องหมายทับล้อมรอบจะเปลี่ยนเป็นนิพจน์ทั่วไปนะ"
+prohibitedWordsDescription2: "ถ้าแยกด้วยเว้นวรรคจะเป็นการระบุ AND และถ้าล้อมคำด้วยสแลช (/) จะเป็นการใช้ regular expression"
 hiddenTags: "แฮชแท็กที่ซ่อนอยู่"
 hiddenTagsDescription: "เลือกแท็กที่จะไม่แสดงในรายการเทรนด์ สามารถลงทะเบียนหลายแท็กได้โดยขึ้นบรรทัดใหม่"
 notesSearchNotAvailable: "การค้นหาโน้ตไม่พร้อมใช้งาน"
@@ -1058,7 +1086,8 @@ retryAllQueuesNow: "ลองเรียกใช้คิวทั้งหม
 retryAllQueuesConfirmTitle: "ลองใหม่ทั้งหมดจริงๆหรอแน่ใจนะ?"
 retryAllQueuesConfirmText: "สิ่งนี้จะเพิ่มการโหลดเซิร์ฟเวอร์ชั่วคราวนะ"
 enableChartsForRemoteUser: "สร้างแผนภูมิข้อมูลผู้ใช้ระยะไกล"
-enableChartsForFederatedInstances: "สร้างแผนภูมิข้อมูลอินสแตนซ์ระยะไกล"
+enableChartsForFederatedInstances: "สร้างแผนภูมิของเซิร์ฟเวอร์ระยะไกล"
+enableStatsForFederatedInstances: "ดึงข้อมูลสถิติจากเซิร์ฟเวอร์ที่อยู่ห่างไกล"
 showClipButtonInNoteFooter: "เพิ่ม “คลิป” ไปยังเมนูสั่งการของโน้ต"
 reactionsDisplaySize: "ขนาดของรีแอคชั่น"
 limitWidthOfReaction: "จำกัดความกว้างสูงสุดของรีแอคชั่นและแสดงให้เล็กลง"
@@ -1077,7 +1106,7 @@ addMemo: "เพิ่มเมโม"
 editMemo: "แก้ไขเมโม"
 reactionsList: "รายการรีแอคชั่น"
 renotesList: "รายการรีโน้ต"
-notificationDisplay: "การแจ้งเตือน"
+notificationDisplay: "การแสดงการแจ้งเตือน"
 leftTop: "บนซ้าย"
 rightTop: "บนขวา"
 leftBottom: "ล่างซ้าย"
@@ -1087,13 +1116,15 @@ vertical: "แนวตั้ง"
 horizontal: "แนวนอน"
 position: "ตำแหน่ง"
 serverRules: "กฎของเซิร์ฟเวอร์"
-pleaseConfirmBelowBeforeSignup: "โปรดยืนยันที่ด้านล่างก่อนสมัครใช้งาน"
+pleaseConfirmBelowBeforeSignup: "หากต้องการลงทะเบียนในเซิร์ฟเวอร์นี้ คุณต้องตรวจสอบและยอมรับสิ่งต่อไปนี้"
 pleaseAgreeAllToContinue: "คุณต้องยอมรับทุกช่องตรงด้านบนเพื่อดำเนินการต่อค่ะ"
 continue: "ดำเนินการต่อ"
 preservedUsernames: "ชื่อผู้ใช้ที่สงวนไว้"
 preservedUsernamesDescription: "ระบุชื่อผู้ใช้ที่จะสงวนชื่อไว้ คั่นด้วยการขึ้นบรรทัดใหม่ ชื่อผู้ใช้ที่ระบุที่นี่จะไม่สามารถใช้งานได้อีกต่อไปเมื่อสร้างบัญชีใหม่ ยกเว้นเมื่อผู้ดูแลระบบสร้างบัญชี นอกจากนี้ บัญชีที่มีอยู่แล้วจะไม่ได้รับผลกระทบ"
 createNoteFromTheFile: "เรียบเรียงโน้ตจากไฟล์นี้"
 archive: "เก็บถาวร"
+archived: "เก็บถาวรแล้ว"
+unarchive: "เลิกการเก็บถาวร"
 channelArchiveConfirmTitle: "ต้องการเก็บถาวรเจ้า {name} ใช่ไหม?"
 channelArchiveConfirmDescription: "เมื่อเก็บถาวรแล้ว จะไม่ปรากฏในรายการช่องหรือผลการค้นหาอีกต่อไป และจะไม่สามารถโพสต์ใหม่ได้อีกต่อไป"
 thisChannelArchived: "ช่องนี้ถูกเก็บถาวรแล้วนะ"
@@ -1104,6 +1135,9 @@ preventAiLearning: "ปฏิเสธการเรียนรู้ด้ว
 preventAiLearningDescription: "ส่งคำร้องขอไม่ให้ใช้ ข้อความในโน้ตที่โพสต์, หรือเนื้อหารูปภาพ ฯลฯ ในการเรียนรู้ของเครื่อง(machine learning) / Predictive AI / Generative AI โดยการเพิ่มแฟล็ก “noai” ลง HTML-Response ให้กับเนื้อหาที่เกี่ยวข้อง แต่ทั้งนี้ ไม่ได้ป้องกัน AI จากการเรียนรู้ได้อย่างสมบูรณ์ เนื่องจากมี AI บางตัวเท่านั้นที่จะเคารพคำขอดังกล่าว"
 options: "ตัวเลือกบทบาท"
 specifyUser: "ผู้ใช้เฉพาะ"
+lookupConfirm: "ต้องการเรียกดูข้อมูลใช่ไหม?"
+openTagPageConfirm: "ต้องการเปิดหน้าแฮชแท็กใช่ไหม?"
+specifyHost: "ระบุโฮสต์"
 failedToPreviewUrl: "ไม่สามารถดูตัวอย่างได้"
 update: "อัปเดต"
 rolesThatCanBeUsedThisEmojiAsReaction: "บทบาทที่สามารถใช้เอโมจินี้เป็นรีแอคชั่นได้"
@@ -1157,7 +1191,7 @@ notifyNotes: "แจ้งเตือนเกี่ยวกับโพสต
 unnotifyNotes: "หยุดการแจ้งเตือนเกี่ยวกับโน้ตใหม่"
 authentication: "การตรวจสอบสิทธิ์"
 authenticationRequiredToContinue: "กรุณายืนยันตัวตนทางอิเล็กทรอนิกส์เพื่อดำเนินการต่อ"
-dateAndTime: "เวลาประทับ"
+dateAndTime: "วันเวลา"
 showRenotes: "แสดงรีโน้ต"
 edited: "แก้ไขแล้ว"
 notificationRecieveConfig: "การตั้งค่าการแจ้งเตือน"
@@ -1168,11 +1202,11 @@ showRepliesToOthersInTimeline: "แสดงการตอบกลับผู
 hideRepliesToOthersInTimeline: "ไม่แสดงการตอบกลับผู้อื่นลงในไทม์ไลน์"
 showRepliesToOthersInTimelineAll: "รวมตอบกลับจากทุกคนที่คุณติดตามไว้ในไทม์ไลน์ของคุณ"
 hideRepliesToOthersInTimelineAll: "ซ่อนตอบกลับจากทุกคนที่คุณติดตามไปจากไทม์ไลน์ของคุณ"
-confirmShowRepliesAll: "การดำเนินการนี้ไม่สามารถย้อนกลับได้ คุณต้องการแสดงการตอบกลับผู้อื่นจากผู้ใช้ทุกคนที่คุณติดตามอยู่ในไทม์ไลน์ของคุณหรือไม่?"
-confirmHideRepliesAll: "การดำเนินการนี้ไม่สามารถย้อนกลับได้ คุณต้องการซ่อนการตอบกลับผู้อื่นจากผู้ใช้ทุกคนที่คุณติดตามอยู่ในไทม์ไลน์ของคุณหรือไม่?"
+confirmShowRepliesAll: "การดำเนินการนี้ไม่สามารถย้อนกลับได้ คุณต้องการแสดงการตอบกลับผู้อื่นจากผู้ใช้ทุกคนที่คุณติดตามอยู่ ใส่ลงไทม์ไลน์ใช่ไหม?"
+confirmHideRepliesAll: "การดำเนินการนี้ไม่สามารถย้อนกลับได้ คุณต้องการซ่อนการตอบกลับผู้อื่นจากผู้ใช้ทุกคนที่คุณติดตามอยู่ ไปจากไทม์ไลน์ใช่ไหม?"
 externalServices: "บริการภายนอก"
 sourceCode: "ซอร์สโค้ด"
-sourceCodeIsNotYetProvided: "ซอร์สโค้ดยังไม่พร้อมใช้งาน โปรดติดต่อผู้ดูแลระบบของคุณเพื่อแก้ไขปัญหานี้"
+sourceCodeIsNotYetProvided: "ซอร์สโค้ดยังไม่พร้อมใช้งาน โปรดติดต่อผู้ดูแลระบบเพื่อแก้ไขปัญหานี้"
 repositoryUrl: "URL ของ repository"
 repositoryUrlDescription: "หากมีที่เก็บซอร์สโค้ดที่เปิดเผยต่อสาธารณะ ให้ป้อน URL ที่เก็บซอร์สโค้ดนั้น แต่หากคุณใช้ Misskey ตามต้นฉบับ (ไม่มีการเปลี่ยนแปลงซอร์สโค้ด) ให้ป้อน https://github.com/misskey-dev/misskey"
 repositoryUrlOrTarballRequired: "หากคุณไม่มี repository สาธารณะ คุณจะต้องจัดเตรียม tarball แทน ดู .config/example.yml สำหรับรายละเอียด"
@@ -1227,7 +1261,7 @@ surrender: "ยอมแพ้"
 gameRetry: "เริ่มเกมใหม่"
 notUsePleaseLeaveBlank: "หากไม่ได้ใช้กรุณาเว้นว่างไว้"
 useTotp: "ใช้รหัสผ่านแบบใช้ครั้งเดียว (TOTP)"
-useBackupCode: "ใช้รหัสสำรอง"
+useBackupCode: "ใช้รหัสแบ๊กอัป"
 launchApp: "เริ่มแอป"
 useNativeUIForVideoAudioPlayer: "ใช้ UI ของเบราว์เซอร์เพื่อเล่นวิดีโอ/เสียง"
 keepOriginalFilename: "คงชื่อไฟล์เดิมไว้"
@@ -1235,13 +1269,49 @@ keepOriginalFilenameDescription: "หากปิดการตั้งค่
 noDescription: "ไม่มีข้อความอธิบาย"
 alwaysConfirmFollow: "แสดงข้อความยืนยันเมื่อกดติดตาม"
 inquiry: "ติดต่อเรา"
+tryAgain: "โปรดลองอีกครั้ง"
+confirmWhenRevealingSensitiveMedia: "ตรวจสอบก่อนแสดงสื่อที่มีเนื้อหาละเอียดอ่อน"
+sensitiveMediaRevealConfirm: "สื่อนี้มีเนื้อหาละเอียดอ่อน, ต้องการแสดงใช่ไหม?"
+createdLists: "รายชื่อที่ถูกสร้าง"
+createdAntennas: "เสาอากาศที่ถูกสร้าง"
+fromX: "จาก {x}"
+genEmbedCode: "สร้างรหัสฝัง"
+noteOfThisUser: "โน้ตโดยผู้ใช้นี้"
+clipNoteLimitExceeded: "ไม่สามารถเพิ่มโน้ตเพิ่มเติมในคลิปนี้ได้อีกแล้ว"
+performance: "ประสิทธิภาพ​"
+modified: "แก้ไข"
+discard: "ละทิ้ง"
+thereAreNChanges: "มีอยู่ {n} เปลี่ยนแปลง(s)"
+signinWithPasskey: "ลงชื่อเข้าใช้ด้วย Passkey"
+unknownWebAuthnKey: "พาสคีย์ไม่ถูกต้องค่ะ"
+passkeyVerificationFailed: "การยืนยันกุญแจดิจิทัลไม่สำเร็จค่ะ"
+passkeyVerificationSucceededButPasswordlessLoginDisabled: "การยืนยันพาสคีย์สำเร็จแล้ว แต่การลงชื่อเข้าใช้แบบไม่ต้องใส่รหัสผ่านถูกปิดใช้งานแล้ว"
+messageToFollower: "ข้อความถึงผู้ติดตาม"
+target: "เป้า"
+testCaptchaWarning: "ฟังก์ชันนี้มีไว้สำหรับทดสอบ CAPTCHA เท่านั้น\n<strong>ห้ามนำไปใช้ในระบบจริงโดยเด็ดขาด</strong>"
+prohibitedWordsForNameOfUser: "คำนี้ไม่สามารถใช้เป็นชื่อผู้ใช้ได้"
+prohibitedWordsForNameOfUserDescription: "หากมีสตริงใดๆ ในรายการนี้ปรากฏอยู่ในชื่อของผู้ใช้ ชื่อนั้นจะถูกปฏิเสธ ผู้ใช้ที่มีสิทธิ์แต่ผู้ดูแลระบบนั้นจะไม่ได้รับผลกระทบใดๆจากข้อจำกัดนี้ค่ะ"
+yourNameContainsProhibitedWords: "ชื่อของคุณนั้นมีคำที่ต้องห้าม"
+yourNameContainsProhibitedWordsDescription: "ถ้าหากคุณต้องการใช้ชื่อนี้ กรุณาติดต่อผู้ดูแลระบบของเซิร์ฟเวอร์นะค่ะ"
+_abuseUserReport:
+  forward: "ส่ง​ต่อ"
+  forwardDescription: "ส่งรายงานไปยังเซิร์ฟเวอร์ระยะไกลโดยใช้บัญชีระบบที่ไม่ระบุตัวตน"
+  resolve: "แก้ไข"
+  accept: "ยอมรับ"
+  reject: "ปฏิเสธ"
+  resolveTutorial: "ถ้าหากรายงานนี้มีเนื้อหาถูกต้อง ให้เลือก \"ยอมรับ\" เพื่อปิดเคสกรณีนี้โดยถือว่าได้รับการแก้ไขแล้ว\nถ้าหากเนื้อหาในรายงานนี้นั้นไม่ถูกต้อง ให้เลือก \"ปฏิเสธ\" เพื่อปิดเคสกรณีนี้โดยถือว่าไม่ได้รับการแก้ไข"
 _delivery:
-  stop: "ถูกระงับ"
+  status: "สถานะการจัดส่ง"
+  stop: "ระงับการส่ง"
+  resume: "จัดส่งต่อ"
   _type:
     none: "กำลังเผยแพร่"
+    manuallySuspended: "หยุดชั่วคราวด้วยตนเอง"
+    goneSuspended: "เซิร์ฟเวอร์ถูกระงับเนื่องจากมีการลบเซิร์ฟเวอร์นี้"
+    autoSuspendedForNotResponding: "เซิร์ฟเวอร์ถูกระงับเนื่องจากไม่ตอบสนอง"
 _bubbleGame:
   howToPlay: "วิธีเล่น"
-  hold: "หยุดชั่วคราว"
+  hold: "ถือไว้"
   _score:
     score: "คะแนน"
     scoreYen: "จำนวนเงินที่ได้รับ"
@@ -1255,27 +1325,27 @@ _bubbleGame:
     section2: "เมื่อวัตถุประเภทเดียวกันมารวมกัน พวกมันจะกลายเป็นวัตถุใหม่และคุณจะได้รับคะแนน"
     section3: "หากวัตถุล้นออกมาจากกล่อง เกมก็จะจบลง ตั้งเป้าทำคะแนนให้สูงด้วยการหลอมวัตถุต่าง ๆ โดยไม่ทำให้ล้นกล่อง!"
 _announcement:
-  forExistingUsers: "ผู้ใช้งานที่มีอยู่เท่านั้น"
-  forExistingUsersDescription: "การประกาศนี้จะแสดงต่อผู้ใช้ที่มีอยู่ ณ จุดที่เผยแพร่นั้นๆถ้าหากเปิดใช้งาน ถ้าหากปิดใช้งานผู้ที่กำลังสมัครใหม่หลังจากโพสต์แล้วนั้นก็จะเห็นเช่นกัน"
+  forExistingUsers: "ผู้ใช้งานที่มีอยู่ตอนนี้เท่านั้น"
+  forExistingUsersDescription: "หากเปิดใช้งาน การประกาศนี้จะแสดงเฉพาะกับผู้ใช้ที่สร้างบัญชีก่อน/ที่มีอยู่ในขณะที่สร้างประกาศนี้เท่านั้น หากปิดใช้งาน การประกาศนี้จะแสดงกับผู้ใช้ที่สร้างบัญชีหลังจากสร้างประกาศนี้ด้วย"
   needConfirmationToRead: "จำเป็นต้องยืนยันว่าอ่านแล้ว"
   needConfirmationToReadDescription: "กล่องโต้ตอบการยืนยันจะปรากฏขึ้นเมื่อจะทำเครื่องหมายว่าอ่านแล้ว นอกจากนี้ยังทำให้ประกาศนี้ยังไม่ถูกอ่านเมื่อใช้ฟังก์ชั่น “ทำเครื่องหมายฯ ทั้งหมดว่าอ่านแล้ว”"
   end: "เก็บประกาศ"
-  tooManyActiveAnnouncementDescription: "การมีประกาศที่ใช้งานมากเกินไปนั้นอาจจะทำให้ประสบการณ์ของผู้ใช้งานนั้นดูแย่ลง โปรดกรุณาพิจารณาการเก็บประกาศที่ล้าสมัยด้วยนะค่ะ"
+  tooManyActiveAnnouncementDescription: "เนื่องจากมีการประกาศที่ยังใช้งานอยู่จำนวนมาก อาจทำให้ UX ลดลง แนะนำให้พิจารณาการเก็บประกาศที่สิ้นสุดไปแล้ว"
   readConfirmTitle: "ทำเครื่องหมายว่าอ่านแล้วเลยไหม?"
   readConfirmText: "จะทำเครื่องหมายใส่ “{title}” ว่าอ่านแล้ว"
-  shouldNotBeUsedToPresentPermanentInfo: "เราขอแนะนำให้ใช้ประกาศเพื่อโพสต์ข้อมูลแบบ flow มากกว่าข้อมูลแบบ stock เนื่องจากมีแนวโน้มที่จะส่งผลเสียต่อ UX โดยเฉพาะสำหรับผู้ใช้ใหม่"
+  shouldNotBeUsedToPresentPermanentInfo: "เนื่องจากมีความเป็นไปได้สูงที่จะส่งผลเสียต่อง UX ของผู้ใช้ใหม่ จึงขอแนะนำให้ใช้ประกาศสำหรับข้อมูลที่ต้องการการตอบสนองในทันที ไม่ใช่ข้อมูลที่ต้องการแสดงตลอดเวลา"
   dialogAnnouncementUxWarn: "เราขอแนะนำให้ใช้ด้วยความระมัดระวัง เนื่องจากการแจ้งเตือนแบบกล่องโต้ตอบตั้งแต่ 2 รายการขึ้นไปพร้อมกันอาจส่งผลเสียต่อ UX ได้อย่างมาก"
   silence: "ไม่มีการแจ้งเตือน"
   silenceDescription: "หากเปิดใช้งาน จะไม่มีการแจ้งเตือนประกาศนี้ และผู้ใช้จะไม่จำเป็นต้องทำเครื่องหมายว่าอ่านแล้ว"
 _initialAccountSetting:
-  accountCreated: "คุณได้สร้างบัญชีของคุณสำเร็จเรียบร้อยแล้ว!"
+  accountCreated: "สร้างบัญชีเสร็จสมบูรณ์!"
   letsStartAccountSetup: "สำหรับผู้เริ่มต้นมาตั้งค่าโปรไฟล์ของคุณกันเถอะ"
   letsFillYourProfile: "ก่อนอื่นมาตั้งค่าโปรไฟล์ของคุณ"
   profileSetting: "ตั้งค่าโปรไฟล์"
   privacySetting: "ตั้งค่าความเป็นส่วนตัว"
   theseSettingsCanEditLater: "คุณสามารถเปลี่ยนการตั้งค่าเหล่านี้ได้ในภายหลังได้ตลอดเวลานะ"
-  youCanEditMoreSettingsInSettingsPageLater: "ยังมีการตั้งค่าอื่นๆ อีกมากมายที่คุณนั้นสามารถกำหนดค่าได้จาก \"การตั้งค่า\" เพื่อให้แน่ใจว่าได้เยี่ยมชมมันได้ภายหลังนะ"
-  followUsers: "ลองติดตามผู้ใช้บางคนที่คุณอาจจะสนใจเพื่อสร้างไทม์ไลน์ของคุณสิ !"
+  youCanEditMoreSettingsInSettingsPageLater: "สามารถตั้งค่าเพิ่มเติมได้ที่หน้า “การตั้งค่า” อย่าลืมไปเยี่ยมชมภายหลังด้วย"
+  followUsers: "ลองติดตามผู้ใช้ที่สนใจเพื่อสร้างไทม์ไลน์ดูสิ"
   pushNotificationDescription: "กำลังเปิดใช้งานการแจ้งเตือนแบบพุชจะช่วยให้คุณได้รับการแจ้งเตือนจาก {name} โดยตรงบนอุปกรณ์ของคุณนะ"
   initialAccountSettingCompleted: "ตั้งค่าโปรไฟล์เสร็จสมบูรณ์แล้ว!"
   haveFun: "ขอให้สนุกกับ {name}!"
@@ -1310,7 +1380,7 @@ _initialTutorial:
     description1: "Misskey มีหลายไทม์ไลน์ขึ้นอยู่กับวิธีการใช้งานของคุณ (บางไทม์ไลน์อาจไม่สามารถใช้ได้ขึ้นอยู่กับนโยบายของเซิร์ฟเวอร์)"
     home: "คุณสามารถดูโพสต์จากบัญชีที่คุณติดตามได้"
     local: "คุณสามารถดูโพสต์จากผู้ใช้ทั้งหมดบนเซิร์ฟเวอร์นี้"
-    social: "โพสต์จากทั้งไทม์ไลน์หน้าแรกและไทม์ไลน์ในพื้นที่ของคุณจะปรากฏขึ้น"
+    social: "จะแสดงโพสต์ทั้งจากไทม์ไลน์หลักและไทม์ไลน์ท้องถิ่น"
     global: "คุณสามารถดูโพสต์จากเซิร์ฟเวอร์ที่เชื่อมต่ออื่นๆ ทั้งหมดได้"
     description2: "คุณสามารถสลับระหว่างแต่ละไทม์ไลน์ได้ตลอดเวลาได้ที่บริเวณด้านบนของหน้าจอ"
     description3: "นอกจากนี้ยังมีรายการไทม์ไลน์ ไทม์ไลน์ของช่อง ฯลฯ โปรดดู {link} สำหรับรายละเอียดเพิ่มเติม"
@@ -1320,7 +1390,7 @@ _initialTutorial:
     _visibility:
       description: "คุณสามารถจำกัดผู้ที่สามารถดูโน้ตของคุณได้นะ"
       public: "โน้ตของคุณนั้นจะปรากฏแก่ผู้ใช้งานทุกคน"
-      home: "เผยแพร่บนไทม์ไลน์หน้าแรกเท่านั้น ผู้คนที่เข้าชมโปรไฟล์ของคุณ ผ่านผู้ติดตาม และผ่านการรีโน้ตสามารถเห็นได้"
+      home: "เผยแพร่บนไทม์ไลน์หลักเท่านั้น แต่ผู้ติดตาม ผู้ที่เข้ามาดูโปรไฟล์ และผู้ที่เห็นจากรีโน้ตยังสามารถดูโพสต์นี้ได้"
       followers: "มองเห็นได้เฉพาะผู้ติดตามเท่านั้น ไม่มีใครอื่นนอกจากตัวคุณเองที่สามารถรีโน้ตได้ และมีเพียงผู้ติดตามของคุณเท่านั้นที่สามารถดูได้"
       direct: "เปิดให้เห็นเฉพาะผู้ใช้ที่ระบุเท่านั้น และพวกเขาจะได้รับแจ้งเตือนด้วย คุณสามารถใช้มันแทนข้อความโดยตรง (dm)"
       doNotSendConfidencialOnDirect1: "โปรดใช้ความระมัดระวังในการส่งข้อมูลที่ละเอียดอ่อน"
@@ -1328,9 +1398,9 @@ _initialTutorial:
       localOnly: "การโพสต์ด้วย flag นี้จะไม่รวมโน้ตไปยังเซิร์ฟเวอร์อื่น ผู้ใช้บนเซิร์ฟเวอร์อื่นจะไม่สามารถดูโน้ตเหล่านี้ได้โดยตรง โดยไม่คำนึงถึงการตั้งค่าการแสดงผลข้างต้น"
     _cw:
       title: "คำเตือนเกี่ยวกับเนื้อหา"
-      description: "เนื้อหาที่เขียนด้วย “คำอธิบายประกอบ” จะแสดงแทนข้อความหลัก คลิก “ดูเพิ่มเติม” เพื่อแสดงข้อความเต็ม"
+      description: "เนื้อหาที่เขียนใน “คำอธิบายประกอบ” จะแสดงแทนเนื้อหาหลัก ต้องคลิก “ดูเพิ่มเติม” เพื่อให้เนื้อหาหลักแสดง"
       _exampleNote:
-        cw: "นี่อาจจะทำให้คุณหิวอย่างแน่นอน!"
+        cw: " ห้ามดู ระวังหิว"
         note: "เพิ่งไปกินโดนัทเคลือบช็อคโกแลตมา 🍩😋"
       useCases: "ใช้สิ่งนี้เพื่อระบุโน้ตที่ต้องตามแนวทางปฏิบัติของเซิร์ฟเวอร์ หรือเพื่อควบคุมการสปอยล์และข้อความที่ละเอียดอ่อนด้วยตนเอง"
   _howToMakeAttachmentsSensitive:
@@ -1346,17 +1416,17 @@ _initialTutorial:
     title: "บทเรียนจบลงแล้วจ้า เย่เย่เย่  🎉"
     description: "คุณสมบัติที่แนะนำในที่นี่เป็นเพียงบางส่วนเท่านั้น หากต้องการเรียนรู้เพิ่มเติมเกี่ยวกับวิธีใช้ Misskey โปรดไปที่ {link}"
 _timelineDescription:
-  home: "บนไทม์ไลน์หน้าแรก คุณสามารถดูโพสต์จากบัญชีที่คุณติดตามได้"
-  local: "ไทม์ไลน์ในพื้นที่ช่วยให้คุณเห็นโพสต์จากผู้ใช้ทั้งหมดบนเซิร์ฟเวอร์นี้"
-  social: "ไทม์ไลน์โซเชียลจะแสดงโพสต์จากทั้งไทม์ไลน์หน้าแรกและไทม์ไลน์ในพื้นที่"
+  home: "บนไทม์ไลน์หลัก คุณสามารถดูโพสต์จากบัญชีที่ติดตามอยู่ได้"
+  local: "ไทม์ไลน์ท้องถิ่นช่วยให้เห็นโพสต์จากผู้ใช้ทั้งหมดบนเซิร์ฟเวอร์นี้"
+  social: "ไทม์ไลน์โซเชียลจะแสดงโพสต์จากทั้งไทม์ไลน์หลักและไทม์ไลน์ท้องถิ่น"
   global: "ในไทม์ไลน์ทั่วโลก คุณสามารถดูโน้ตจากเซิร์ฟเวอร์ที่เชื่อมต่อทั้งหมดได้"
 _serverRules:
   description: "ชุดของกฎที่จะแสดงก่อนการลงทะเบียนเราขอแนะนำให้ตั้งค่าสรุปข้อกำหนดในการให้บริการ"
 _serverSettings:
   iconUrl: "URL ไอคอน"
   appIconDescription: "ระบุไอคอนที่จะใช้เมื่อ {host} แสดงเป็นแอป"
-  appIconUsageExample: "E.g. เป็น PWA หรือเมื่อแสดงผลเป็นบุ๊กมาร์กหน้าจอหลักบนโทรศัพท์"
-  appIconStyleRecommendation: "เนื่องจากไอคอนอาจถูกครอบตัดเป็นสี่เหลี่ยมจัตุรัสหรือวงกลม จึงแนะนำให้ใช้ไอคอนที่มีขอบสีรอบๆ เนื้อหา"
+  appIconUsageExample: "ตัวอย่างเช่น เมื่อถูกเพิ่มเป็น PWA หรือบุ๊กมาร์กบนหน้าจอหลักในสมาร์ทโฟน"
+  appIconStyleRecommendation: "เนื่องจากอาจถูกครอบตัดเป็นสี่เหลี่ยมหรือวงกลม จึงแนะนำให้ใช้ภาพที่เผื่อพื้นที่รอบๆ ตัวโลโก้ไอคอนไว้"
   appIconResolutionMustBe: "ความละเอียดขั้นต่ำไว้คือ {resolution}."
   manifestJsonOverride: "เขียนทับ manifest.json"
   shortName: "ชื่อย่อ"
@@ -1364,27 +1434,31 @@ _serverSettings:
   fanoutTimelineDescription: "เพิ่มประสิทธิภาพการดึงข้อมูลไทม์ไลน์อย่างมาก และลดภาระในฐานข้อมูลเมื่อเปิดใช้งาน ในทางกลับกัน การใช้หน่วยความจำของ Redis จะเพิ่มขึ้น ลองปิดการใช้งานนี้ในกรณีที่หน่วยความจำเซิร์ฟเวอร์เหลือน้อยหรือเซิร์ฟเวอร์ไม่เสถียร"
   fanoutTimelineDbFallback: "ฟอลแบ๊กกลับฐานข้อมูล"
   fanoutTimelineDbFallbackDescription: "เมื่อเปิดใช้งาน หากไม่ได้แคชไทม์ไลน์ ไทม์ไลน์จะฟอลแบ๊กไปยังฐานข้อมูลสำหรับการ query เพิ่มเติม การปิดใช้งานจะช่วยลดภาระของเซิร์ฟเวอร์ด้วยการกำจัดกระบวนฟอลแบ๊ก แต่มันก็จะจำกัดช่วงเวลาไทม์ไลน์ที่สามารถดึงข้อมูลได้"
+  reactionsBufferingDescription: "เมื่อเปิดใช้งานฟังก์ชันนี้ก็จะช่วยลด latency ในการสร้างปฏิกิริยา แต่อาจจะส่งผลให้ memory footprint ของ Redis เพิ่มขึ้นนะ"
+  inquiryUrl: "URL สำหรับการติดต่อสอบถาม"
+  inquiryUrlDescription: "ระบุ URL ของหน้าเว็บที่มีแบบฟอร์มสำหรับติดต่อผู้ดูแลเซิร์ฟเวอร์ หรือข้อมูลการติดต่อของผู้ดูแลเซิร์ฟเวอร์"
+  thisSettingWillAutomaticallyOffWhenModeratorsInactive: "ถ้าหากไม่มีการตรวจสอบจากผู้ดูแลระบบหรือไม่มีความเคลื่อนไหวมาเป็นระยะเวลาหนึ่ง ระบบจะทำการปิดใช้งานฟังก์ชันนี้โดยอัตโนมัติ เพื่อลดความเสี่ยงในการถูกโจมตีด้วยสแปมและอื่นๆ"
 _accountMigration:
-  moveFrom: "ย้ายข้อมูลบัญชีอื่นไปยังอีกบัญชีนี้หนึ่ง"
+  moveFrom: "ย้ายจากบัญชีอื่นมาที่บัญชีนี้"
   moveFromSub: "สร้างนามแฝงไปยังบัญชีอื่น"
   moveFromLabel: "บัญชีที่จะย้ายจาก #{n}"
-  moveFromDescription: "ถ้าหากคุณต้องการโอนข้อมูล คุณจำเป็นต้องสร้างบัญชีสำรองสำหรับการย้ายบัญชี  หลังจากนั้นป้อนบัญชีที่จะย้ายไปในรูปแบบต่อไปนี้: @person@instance.com"
-  moveTo: "ย้ายข้อมูลบัญชีนี้ไปยังบัญชีอีกหนึ่ง"
+  moveFromDescription: "หากต้องการโอนข้อมูลจากบัญชีอื่นมายังบัญชีนี้ จำเป็นต้องสร้างบัญชีนามแฝง (alias) ไว้ที่นี่ด้วย\nกรุณากรอกบัญชีเดิมในรูปแบบ: @username@server.example.com\nหากต้องการลบ alias, ให้เว้นว่างไว้แล้วบันทึก (ไม่แนะนำ)"
+  moveTo: "ย้ายบัญชีนี้ไปยังบัญชีใหม่"
   moveToLabel: "บัญชีที่จะย้ายไปที่:"
   moveCannotBeUndone: "ไม่สามารถยกเลิกการโอนย้ายบัญชีได้"
   moveAccountDescription: "การดำเนินการนี้จะย้ายบัญชีของคุณไปยังบัญชีอื่น\n・ผู้ที่กำลังติดตามคุณจากบัญชีนี้จะถูกย้ายไปยังบัญชีใหม่โดยอัตโนมัติ\n・บัญชีนี้จะเลิกติดตามผู้ใช้ทั้งหมดที่กำลังติดตามอยู่\n・คุณจะไม่สามารถสร้างโน้ต ฯลฯ ในบัญชีนี้ได้\n\nแม้ว่าการย้ายผู้ที่ติดตามคุณจะเป็นไปโดยอัตโนมัติ แต่คุณต้องเตรียมขั้นตอนบางอย่างด้วยตนเอง เพื่อย้ายรายชื่อผู้ใช้ที่คุณกำลังติดตาม โดยดำเนินการส่งออกรายชื่อแล้วค่อยนำเข้ามาภายหลังในเมนูการตั้งค่าของบัญชีใหม่ ใช้ขั้นตอนเดียวกันนี้ใช้รายชื่อผู้ใช้ที่ถูกปิดเสียงและถูกบล็อก\n\n(คำอธิบายนี้ใช้กับ Misskey v13.12.0 ขึ้นไป, ซอฟต์แวร์ ActivityPub อื่นๆ เช่น Mastodon อาจทำงานแตกต่างออกไป)"
-  moveAccountHowTo: "หากต้องการย้ายข้อมูลก่อนอื่นให้สร้างชื่อแทนสำหรับบัญชีนี้ ในบัญชีที่จะต้องการย้ายไป\nหลังจากที่คุณสร้างนามแฝงนั้นแล้ว ให้ป้อนบัญชีที่ต้องการจะย้ายไปในรูปแบบดังต่อไปนี้: @username@server.example.com"
+  moveAccountHowTo: "การย้ายบัญชีจะเริ่มต้นโดยการสร้างบัญชีนามแฝง (alias) ของบัญชีนี้ ณ บัญชีที่เป็นปลายทาง หลังจากสร้างนามแฝงแล้ว ให้ป้อนบัญชีปลายทางในรูปแบบดังนี้: @username@server.example.com"
   startMigration: "โอนย้าย"
   migrationConfirm: "ยืนยันการย้ายข้อมูลบัญชีนี้ไปที่ {account} เมื่อเริ่มแล้วจะไม่สามารถหยุดหรือนำกลับคืนมาได้ และคุณจะไม่สามารถใช้บัญชีนี้ในสถานะดั้งเดิมได้อีกต่อไป\n\nนอกจากนี้ คุณจำเป็นต้องสร้างบัญชีสำรองสำหรับการย้ายบัญชี"
-  movedAndCannotBeUndone: "\nบัญชีนี้ถูกโอนย้ายไปแล้ว\nไม่สามารถย้อนกลับโอนย้ายข้อมูลได้"
-  postMigrationNote: "บัญชีนี้จะถูกเลิกติดตามบัญชีทั้งหมดที่กำลังติดตามภายใน 24 ชั่วโมงหลังจากการย้ายข้อมูลนั้นเสร็จสิ้น ทั้งจำนวนผู้ติดตามและผู้ติดตามนั้นจะกลายเป็นศูนย์ เพื่อหลีกเลี่ยงป้องกันไม่ให้ผู้ติดตามของคุณนั้นไม่สามารถเห็นโพสต์เฉพาะผู้ติดตามของบัญชีนี้ได้ แต่อย่างไรก็ตามแล้วพวกเขาจะยังคงติดตามบัญชีนี้ต่อไป"
-  movedTo: "บัญชีที่จะย้ายไปที่:"
+  movedAndCannotBeUndone: "\nบัญชีนี้ถูกโอนย้ายไปแล้ว\nไม่สามารถยกเลิกการโอนย้ายได้"
+  postMigrationNote: "บัญชีนี้จะดำเนินการยกเลิกการติดตามทั้งหมดหลังจากการย้ายข้อมูลไปแล้ว 24 ชั่วโมง จำนวนกำลังติดตามและจำนวนผู้ติดตามของบัญชีนี้จะเป็น 0 และเพื่อหลีกเลี่ยงไม่ให้ผู้ติดตามคุณนั้นไม่สามารถเห็นโพสต์เฉพาะผู้ติดตามฯได้  การยกเลิกการติดตามจะไม่กระทบกับผู้ติดตามคุณ ดังนั้นผู้ติดตามคุณยังคงสามารถดูโพสต์ของบัญชีนี้ได้"
+  movedTo: "บัญชีที่จะย้ายไป:"
 _achievements:
   earnedAt: "ได้รับเมื่อ"
   _types:
     _notes1:
       title: "just setting up my msky"
-      description: "โพสต์โน้ตแรกของคุณ"
+      description: "โพสต์โน้ตเป็นครั้งแรก"
       flavor: "ขอให้มีช่วงเวลาที่ดีกับ Misskey นะคะ!"
     _notes10:
       title: "โน้ตไม่กี่ชิ้น"
@@ -1444,15 +1518,15 @@ _achievements:
       title: "มือใหม่ III"
       description: "เข้าสู่ระบบเป็นเวลารวม 15 วัน"
     _login30:
-      title: "มิสคิสท์ I"
+      title: "มิสคิสต์ I"
       description: "เข้าสู่ระบบเป็นเวลารวม 30 วัน"
     _login60:
-      title: "มิสคิสท์ II"
+      title: "มิสคิสต์ II"
       description: "เข้าสู่ระบบเป็นเวลารวม 60 วัน"
     _login100:
-      title: "มิสคิสท์ III"
+      title: "มิสคิสต์ III"
       description: "เข้าสู่ระบบเป็นเวลารวม 100 วัน"
-      flavor: "มิสคิสต์หัวรุนแรง"
+      flavor: "Violent Misskist (ทำไมเหมือนชื่อหนังสักเรื่องจังเลยนะ)"
     _login200:
       title: "ลูกค้าประจำ I"
       description: "เข้าสู่ระบบเป็นเวลารวม 200 วัน"
@@ -1484,19 +1558,19 @@ _achievements:
       flavor: "ขอบคุณที่ใช้ Misskey นะ !"
     _noteClipped1:
       title: "อดไม่ได้ที่จะต้องคลิปมันเอาไว้"
-      description: "คลิปโน้ตตัวแรกของคุณ"
+      description: "คลิปโน้ตเป็นครั้งแรก"
     _noteFavorited1:
       title: "สตาร์เกเซอร์"
-      description: "ชื่นชอบโน้ตแรกของคุณ"
+      description: "ใส่โน้ตเป็นรายการโปรดเป็นครั้งแรก"
     _myNoteFavorited1:
       title: "แสวงหาดวงดาว"
-      description: "มีคนอื่นๆที่ชื่นชอบหนึ่งในโน้ตของคุณ"
+      description: "โน้ตตัวเองถูกคนอื่นเพิ่มลงรายการโปรดของเขา"
     _profileFilled:
       title: "เตรียมตัวอย่างดี"
-      description: "ตั้งค่าโปรไฟล์ของคุณ"
+      description: "ตั้งค่าโปรไฟล์"
     _markedAsCat:
       title: "ฉันเป็นแมว"
-      description: "ทำเครื่องหมายบัญชีของคุณว่าเป็นแมว"
+      description: "ตั้งค่าบัญชีเป็นแมวเมี้ยวเมี้ยว"
       flavor: "แมวน้อยไร้ชื่อ"
     _following1:
       title: "ก้าวแรกสู่...กดติดตาม"
@@ -1539,7 +1613,7 @@ _achievements:
       description: "ได้รับความสำเร็จ 30 ครั้ง"
     _viewAchievements3min:
       title: "ชอบบรรลุความสําเร็จ"
-      description: "มองดูรายการความสำเร็จของคุณเป็นเวลาอย่างน้อย 3 นาที"
+      description: "มองดูรายการความสำเร็จเป็นเวลานานกว่า 3 นาที"
     _iLoveMisskey:
       title: "ฉันรัก Misskey"
       description: "โพสต์ “I ❤ #Misskey”"
@@ -1566,13 +1640,13 @@ _achievements:
       flavor: "โป๊ะ โป๊ะ โป๊ะ ปิ้งงงงง"
     _selfQuote:
       title: "อ้างอิงตนเอง"
-      description: "อ้างโน้ตของคุณเอง"
+      description: "อ้างอิงโน้ตตัวเอง"
     _htl20npm:
       title: "ไทม์ไลน์ไหล"
-      description: "มีการทำความเร็วของไทม์ไลน์หน้าแรกเกิน 20 npm (โน้ตต่อนาที)"
+      description: "มีการทำความเร็วของไทม์ไลน์หลักเกิน 20 npm (โน้ตต่อนาที)"
     _viewInstanceChart:
       title: "วิเคราะห์"
-      description: "ดูแผนภูมิอินสแตนซ์ของคุณ"
+      description: "ดูแผนภูมิของเซิร์ฟเวอร์"
     _outputHelloWorldOnScratchpad:
       title: "หวัดดีชาวโลก!"
       description: "เอาพุต \"hello world\" ใน Scratchpad"
@@ -1593,16 +1667,16 @@ _achievements:
       description: "มีโอกาสที่จะได้รับด้วยความน่าจะเป็นไปได้ 0.005% ทุก ๆ 10 วินาที"
     _setNameToSyuilo:
       title: "คอมเพล็กซ์ของพระเจ้า"
-      description: "ตั้งชื่อของคุณเป็น “syuilo”"
+      description: "ตั้งชื่อเป็น “syuilo”"
     _passedSinceAccountCreated1:
       title: "ครบรอบหนึ่งปี"
-      description: "ผ่านไปหนึ่งปีแล้วนะตั้งแต่บัญชีของคุณถูกสร้างขึ้นมาน่ะ"
+      description: "ผ่านไป 1 ปีนับตั้งแต่สร้างบัญชี"
     _passedSinceAccountCreated2:
       title: "ครบรอบสองปี"
-      description: "ผ่านไปสองปีแล้วนะตั้งแต่บัญชีของคุณถูกสร้างขึ้นมาน่ะ"
+      description: "ผ่านไป 2 ปีนับตั้งแต่สร้างบัญชี"
     _passedSinceAccountCreated3:
       title: "ครบรอบสามปี"
-      description: "ผ่านไปสามปีแล้วนะตั้งแต่บัญชีของคุณถูกสร้างขึ้นมาน่ะ"
+      description: "ผ่านไป 3 ปีนับตั้งแต่สร้างบัญชี"
     _loggedInOnBirthday:
       title: "สุขสันต์วันเกิด"
       description: "เข้าสู่ระบบในวันเกิดของคุณ"
@@ -1637,7 +1711,7 @@ _role:
   name: "ชื่อบทบาท"
   description: "คำอธิบายบทบาท"
   permission: "สิทธิ์ตามบทบาท"
-  descriptionOfPermission: "<b>ผู้ควบคุม</b> สามารถดำเนินการดูแลขั้นพื้นฐานได้\n<b>ผู้ดูแลระบบ</b> สามารถเปลี่ยนการตั้งค่าทั้งหมดของอินสแตนซ์ได้"
+  descriptionOfPermission: "<b>ผู้ควบคุม</b> สามารถดำเนินการดูแลขั้นพื้นฐานได้\n<b>ผู้ดูแลระบบ</b> สามารถเปลี่ยนการตั้งค่าทั้งหมดของเซิร์ฟเวอร์ได้"
   assignTarget: "มอบหมาย"
   descriptionOfAssignTarget: "แบบ<b>ปรับเอง</b> เพิ่มถอนบทบาทนี้แก่ผู้ใช้ด้วยตัวเอง\nแบบ<b>มีเงื่อนไข</b> เพิ่มถอนบทบาทนี้แก่ผู้ใช้โดยอัตโนมัติหากเข้าเงื่อนไขใดต่อไปนี้"
   manual: "ปรับเอง"
@@ -1650,8 +1724,8 @@ _role:
   descriptionOfIsPublic: "บทบาทจะปรากฏบนโปรไฟล์ของผู้ใช้และเปิดเผยต่อสาธารณะ (ทุกคนสามารถเห็นได้ว่าผู้ใช้รายนี้มีบทบาทนี้)"
   options: "ตัวเลือกบทบาท"
   policies: "นโยบาย"
-  baseRole: "เทมเพลตบทบาท"
-  useBaseValue: "ใช้ตามเทมเพลตบทบาท"
+  baseRole: "แม่แบบบทบาท"
+  useBaseValue: "ใช้ตามแม่แบบบทบาท"
   chooseRoleToAssign: "เลือกบทบาทที่ต้องการกำหนด"
   iconUrl: "URL ไอคอน"
   asBadge: "แสดงเป็นตรา"
@@ -1668,11 +1742,11 @@ _role:
     middle: "ปานกลาง"
     high: "สูง"
   _options:
-    gtlAvailable: "การดูไทม์ไลน์ทั่วโลก"
-    ltlAvailable: "การดูไทม์ไลน์ในท้องถิ่น"
+    gtlAvailable: "สามารถดูไทม์ไลน์ทั่วโลกได้"
+    ltlAvailable: "สามารถดูไทม์ไลน์ท้องถิ่นได้"
     canPublicNote: "สามารถโพสต์แบบสาธารณะ"
     mentionMax: "จำนวนการกล่าวถึงสูงสุดต่อโน้ต"
-    canInvite: "สร้างรหัสเชิญอินสแตนซ์"
+    canInvite: "สร้างรหัสเชิญเข้าเซิร์ฟเวอร์"
     inviteLimit: "จำกัดการเชิญ"
     inviteLimitCycle: "คูลดาวน์ในการเชิญ"
     inviteExpirationTime: "วันหมดอายุของรหัสการเชิญ"
@@ -1680,6 +1754,7 @@ _role:
     canManageAvatarDecorations: "จัดการตกแต่งอวตาร"
     driveCapacity: "ความจุของไดรฟ์"
     alwaysMarkNsfw: "ทำเครื่องหมายไฟล์ว่าเป็น NSFW เสมอ"
+    canUpdateBioMedia: "อนุญาตให้ปรับปรุงไอคอนและแบนเนอร์"
     pinMax: "จํานวนสูงสุดของโน้ตที่ปักหมุดไว้"
     antennaMax: "จำนวนสูงสุดของเสาอากาศ"
     wordMuteMax: "จำนวนอักขระสูงสุดที่อนุญาตในการปิดเสียงคำ"
@@ -1694,9 +1769,14 @@ _role:
     canSearchNotes: "การใช้การค้นหาโน้ต"
     canUseTranslator: "การใช้งานแปล"
     avatarDecorationLimit: "จำนวนการตกแต่งไอคอนสูงสุดที่สามารถติดตั้งได้"
+    canImportAntennas: "อนุญาตให้นำเข้าเสาอากาศ"
+    canImportBlocking: "อนุญาตให้นำเข้าการบล็อก"
+    canImportFollowing: "อนุญาตให้นำเข้ารายการต่อไปนี้"
+    canImportMuting: "อนุญาตให้นำเข้าการปิดกั้น"
+    canImportUserLists: "อนุญาตให้นำเข้ารายการ"
   _condition:
     roleAssignedTo: "มอบหมายให้มีบทบาทแบบทำมือ"
-    isLocal: "ผู้ใช้ในพื้นที่"
+    isLocal: "ผู้ใช้ท้องถิ่น"
     isRemote: "ผู้ใช้ระยะไกล"
     isCat: "ผู้ใช้ที่เป็นแมว"
     isBot: "ผู้ใช้ที่เป็นบอต"
@@ -1755,8 +1835,8 @@ _ad:
   adsTooClose: "เนื่องจากช่วงเวลาการแสดงโฆษณาสั้นมาก ประสบการณ์ผู้ใช้จึงอาจลดลงอย่างมาก"
 _forgotPassword:
   enterEmail: "ป้อนที่อยู่อีเมลที่คุณเคยใช้ในการลงทะเบียนไว้ ลิงก์ที่คุณสามารถรีเซ็ตรหัสผ่านได้นั้นจะถูกส่งไปนะ"
-  ifNoEmail: "ถ้าหากคุณไม่ได้ใช้อีเมลระหว่างการลงทะเบียน กรุณาติดต่อผู้ดูแลระบบอินสแตนซ์แทนนะ"
-  contactAdmin: "อินสแตนซ์นี้ไม่รองรับการใช้งานที่อยู่อีเมลนี้ กรุณาติดต่อผู้ดูแลระบบอินสแตนซ์เพื่อรีเซ็ตรหัสผ่านของคุณแทน"
+  ifNoEmail: "หากลงทะเบียนแบบไม่ใช้อีเมล โปรดติดต่อผู้ดูแลระบบ"
+  contactAdmin: "เนื่องจากเซิร์ฟเวอร์นี้ไม่รองรับการส่งอีเมล หากต้องการรีเซ็ตรหัสผ่าน กรุณาติดต่อผู้ดูแลระบบ"
 _gallery:
   my: "แกลลอรี่ของฉัน"
   liked: "โพสต์ที่ถูกใจ"
@@ -1774,23 +1854,23 @@ _plugin:
   viewSource: "ดูต้นฉบับ"
   viewLog: "แสดงปูม"
 _preferencesBackups:
-  list: "สร้างการสำรองข้อมูล"
-  saveNew: "บันทึกข้อมูลสำรองใหม่"
+  list: "การตั้งค่าที่สำรองไว้"
+  saveNew: "บันทึกการตั้งค่าสำรองใหม่"
   loadFile: "โหลดจากไฟล์"
   apply: "นำไปใช้กับอุปกรณ์นี้"
   save: "บันทึก"
-  inputName: "กรุณาป้อนชื่อสำหรับข้อมูลสำรองนี้"
+  inputName: "กรุณาป้อนชื่อการตั้งค่าสำรองนี้"
   cannotSave: "การบันทึกล้มเหลว"
-  nameAlreadyExists: "มีข้อมูลสำรองชื่อ \"{name}\" นี้อยู่แล้ว กรุณาป้อนชื่ออื่นนะ"
-  applyConfirm: "คุณต้องการใช้ข้อมูลสำรอง \"{name}\" กับอุปกรณ์นี้อย่างงั้นจริงหรอ การตั้งค่าที่มีอยู่ของอุปกรณ์นี้จะถูกเขียนทับนะ"
-  saveConfirm: "บันทึกข้อมูลสำรองเป็น {name} มั้ย?"
-  deleteConfirm: "ลบข้อมูลสำรอง {name} มั้ย?"
-  renameConfirm: "ต้องการเปลี่ยนชื่อข้อมูลสำรองจาก “{old}” เป็น “{new}” ใช่ไหม?"
-  noBackups: "ไม่มีข้อมูลสำรอง สามารถบันทึกการตั้งค่าไคลเอนต์ปัจจุบันไปยังเซิร์ฟเวอร์ด้วย “บันทึกข้อมูลสำรองใหม่”"
+  nameAlreadyExists: "มีการตั้งค่าสำรองชื่อ “{name}” อยู่แล้ว กรุณาป้อนชื่ออื่น"
+  applyConfirm: "ต้องการใช้การตั้งค่าสำรอง “{name}” กับอุปกรณ์นี้ใช่ไหม? การตั้งค่าที่มีอยู่บนอุปกรณ์นี้จะถูกเขียนทับ"
+  saveConfirm: "บันทึกการตั้งค่าสำรองเป็น {name} ใช่ไหม?"
+  deleteConfirm: "ต้องการลบ {name} ใช่ไหม?"
+  renameConfirm: "ต้องการเปลี่ยนชื่อจาก “{old}” เป็น “{new}” ใช่ไหม?"
+  noBackups: "ไม่มีการตั้งค่าสำรอง สามารถบันทึกการตั้งค่าไคลเอนต์ปัจจุบันไปยังเซิร์ฟเวอร์ด้วย “บันทึกการตั้งค่าสำรองใหม่”"
   createdAt: "สร้างเมื่อ: {date} {time}"
   updatedAt: "อัปเดตเมื่อ: {date} {time}"
   cannotLoad: "การโหลดล้มเหลว"
-  invalidFile: "รูปแบบไฟล์ไม่ถูกต้องนะ"
+  invalidFile: "รูปแบบไฟล์ไม่ถูกต้อง"
 _registry:
   scope: "สโคป"
   key: "คีย์"
@@ -1841,13 +1921,13 @@ _menuDisplay:
   hide: "ซ่อน"
 _wordMute:
   muteWords: "ปิดเสียงคำ"
-  muteWordsDescription: "คั่นด้วยช่องว่างสำหรับเงื่อนไข AND หรือด้วยการขึ้นบรรทัดใหม่สำหรับเงื่อนไข OR นะ"
+  muteWordsDescription: "คั่นด้วยเว้นวรรคสำหรับเงื่อนไข AND, หรือขึ้นบรรทัดใหม่สำหรับเงื่อนไข OR"
   muteWordsDescription2: "ล้อมรอบคีย์เวิร์ดด้วยเครื่องหมายทับเพื่อใช้นิพจน์ทั่วไป"
 _instanceMute:
-  instanceMuteDescription: "การดำเนินการนี้จะปิดเสียง\"โน้ต/รีโน้ต\"จากอินสแตนซ์ที่อยู่ในรายการ รวมถึงบันทึกของผู้ใช้ที่ตอบกลับผู้ใช้จากอินสแตนซ์ที่ปิดเสียง"
+  instanceMuteDescription: "ปิดเสียง “โน้ต/รีโน้ต” ทั้งหมดจากเซิร์ฟเวอร์ที่ระบุไว้ รวมถึงโน้ตของผู้ใช้ที่ตอบกลับผู้ใช้จากเซิร์ฟเวอร์ที่ถูกปิดเสียง"
   instanceMuteDescription2: "คั่นด้วยการขึ้นบรรทัดใหม่"
-  title: "ซ่อนโน้ตจากอินสแตนซ์ที่มีอยู่ในรายชื่อ"
-  heading: "รายชื่ออินสแตนซ์ที่ถูกปิดเสียง"
+  title: "ซ่อนโน้ตจากเซิร์ฟเวอร์ที่มีระบุไว้"
+  heading: "เซิร์ฟเวอร์ที่ถูกปิดเสียง"
 _theme:
   explore: "สำรวจธีม"
   install: "ติดตั้งธีม"
@@ -1911,7 +1991,6 @@ _theme:
     buttonBg: "ปุ่มพื้นหลัง"
     buttonHoverBg: "ปุ่มพื้นหลัง (โฮเวอร์)"
     inputBorder: "เส้นขอบของช่องป้อนข้อมูล"
-    listItemHoverBg: "รายการไอเทมพื้นหลัง (โฮเวอร์)"
     driveFolderBg: "พื้นหลังโฟลเดอร์ไดรฟ์"
     wallpaperOverlay: "วอลล์เปเปอร์ซ้อนทับ"
     badge: "ตรา"
@@ -1923,8 +2002,6 @@ _sfx:
   note: "โน้ต"
   noteMy: "โน้ตของตัวเอง"
   notification: "การเเจ้งเตือน"
-  antenna: "เสาอากาศ"
-  channel: "การแจ้งเตือนช่อง"
   reaction: "เมื่อเลือกรีแอคชั่น"
 _soundSettings:
   driveFile: "ใช้เสียงจากไดรฟ์"
@@ -1932,7 +2009,8 @@ _soundSettings:
   driveFileTypeWarn: "ไม่รองรับไฟล์นี้"
   driveFileTypeWarnDescription: "กรุณาเลือกไฟล์เสียง"
   driveFileDurationWarn: "เสียงยาวเกินไป"
-  driveFileDurationWarnDescription: "การใช้เสียงที่ยาวอาจรบกวนการใช้งาน Misskey, ต้องการดำเนินการต่อหรือไม่?"
+  driveFileDurationWarnDescription: "การใช้เสียงที่ยาว อาจรบกวนการใช้งาน Misskey, ต้องการดำเนินการต่อใช่ไหม?"
+  driveFileError: "ไม่สามารถโหลดไฟล์เสียงได้ กรุณาเปลี่ยนแปลงการตั้งค่า"
 _ago:
   future: "อนาคต"
   justNow: "เมื่อกี๊นี้"
@@ -1976,49 +2054,49 @@ _2fa:
   removeKey: "ลบคีย์ความปลอดภัยออก"
   removeKeyConfirm: "ลบข้อมูลสำรอง {name} มั้ย?"
   whyTOTPOnlyRenew: "ไม่สามารถลบแอปตัวรับรองความถูกต้องได้ตราบใดที่มีการลงทะเบียนคีย์ความปลอดภัยไว้แล้ว"
-  renewTOTP: "กำหนดค่าแอพตัวตรวจสอบสิทธิ์ใหม่"
+  renewTOTP: "ตั้งค่าแอปยืนยันตัวตน"
   renewTOTPConfirm: "วิธีการแบบนี้จะทําให้รหัสยืนยันจากแอพก่อนหน้าของคุณหยุดทํางานเลยนะ"
   renewTOTPOk: "ตั้งค่าคอนฟิกใหม่"
   renewTOTPCancel: "ไม่เป็นไร"
-  checkBackupCodesBeforeCloseThisWizard: "โปรดตรวจสอบรหัสสำรองด้านล่างก่อนที่จะปิดวิซาร์ดนี้"
-  backupCodes: "รหัสสำรองข้อมูล"
+  checkBackupCodesBeforeCloseThisWizard: "โปรดตรวจสอบรหัสแบ๊กอัปด้านล่างก่อนที่จะปิดวิซาร์ดนี้"
+  backupCodes: "รหัสแบ๊กอัป"
   backupCodesDescription: "หากแอปยืนยันตัวตนของคุณไม่พร้อมใช้งาน คุณสามารถใช้รหัสสำรองด้านล่างเพื่อเข้าถึงบัญชีของคุณได้ อย่าลืมเก็บรหัสเหล่านี้ไว้ในที่ปลอดภัย แต่ละรหัสสามารถใช้ได้เพียงครั้งเดียวเท่านั้น"
-  backupCodeUsedWarning: "มีการใช้รหัสสำรองแล้ว โปรดกรุณากำหนดค่าการตรวจสอบสิทธิ์แบบสองปัจจัยโดยเร็วที่สุดถ้าหากคุณยังไม่สามารถใช้งานได้อีก"
-  backupCodesExhaustedWarning: "รหัสสำรองทั้งหมดถูกใช้แล้ว ถ้าหากคุณยังสูญเสียการเข้าถึงแอปการตรวจสอบสิทธิ์แบบสองปัจจัยคุณจะยังไม่สามารถเข้าถึงบัญชีนี้ได้ กรุณากำหนดค่าการรับรองความถูกต้องด้วยการยืนยันสองชั้น"
+  backupCodeUsedWarning: "รหัสแบ๊กอัปถูกใช้งานแล้ว หากแอปพลิเคชันการยืนยันตัวตนไม่สามารถใช้งานได้ ให้รีบทำการตั้งค่าแอปฯใหม่โดยเร็วที่สุด"
+  backupCodesExhaustedWarning: "รหัสแบ๊กอัปทั้งหมดถูกใช้งานแล้ว หากยังไม่สามารถใช้แอปพลิเคชันการยืนยันตัวตนได้ก็จะไม่สามารถเข้าถึงบัญชีนี้ได้อีกต่อไป กรุณาลงทะเบียนแอปพลิเคชันการยืนยันตัวตนใหม่"
   moreDetailedGuideHere: "คลิกที่นี่เพื่อดูคำแนะนำโดยละเอียด"
 _permissions:
-  "read:account": "ดูข้อมูลบัญชีของคุณ"
-  "write:account": "แก้ไขข้อมูลบัญชีของคุณ"
-  "read:blocks": "ดูรายชื่อผู้ใช้ที่ถูกบล็อกของคุณ"
-  "write:blocks": "แก้ไขรายชื่อผู้ใช้ที่ถูกบล็อกของคุณ"
-  "read:drive": "เข้าถึงไฟล์และโฟลเดอร์ในไดรฟ์ของคุณ"
-  "write:drive": "แก้ไขหรือลบไฟล์และโฟลเดอร์ในไดรฟ์ของคุณ"
+  "read:account": "ดูข้อมูลบัญชี"
+  "write:account": "แก้ไขข้อมูลบัญชี"
+  "read:blocks": "ดูรายชื่อผู้ใช้ที่ถูกบล็อก"
+  "write:blocks": "แก้ไขรายชื่อผู้ใช้ที่ถูกบล็อก"
+  "read:drive": "เข้าถึงไดรฟ์"
+  "write:drive": "จัดการไดรฟ์"
   "read:favorites": "ดูรายการโปรด"
   "write:favorites": "แก้ไขรายการโปรด"
   "read:following": "ดูข้อมูลว่าใครที่คุณติดตาม"
   "write:following": "ติดตามหรือเลิกติดตามบัญชีอื่น"
-  "read:messaging": "ดูแชทของคุณ"
+  "read:messaging": "ดูแชท"
   "write:messaging": "เขียนหรือลบข้อความแชท"
-  "read:mutes": "ดูรายชื่อผู้ใช้ที่ปิดเสียงของคุณ"
+  "read:mutes": "ดูรายชื่อผู้ใช้ที่ถูกปิดเสียง"
   "write:mutes": "แก้ไขรายชื่อผู้ใช้ที่ถูกปิดเสียง"
   "write:notes": "เขียนหรือลบโน้ต"
-  "read:notifications": "ดูการแจ้งเตือนของคุณ"
-  "write:notifications": "จัดการแจ้งเตือนของคุณ"
-  "read:reactions": "ดูรีแอคชั่นของคุณ"
-  "write:reactions": "แก้ไขรีแอคชั่นของคุณ"
+  "read:notifications": "ดูการแจ้งเตือน"
+  "write:notifications": "จัดการแจ้งเตือน"
+  "read:reactions": "ดูรีแอคชั่น"
+  "write:reactions": "แก้ไขรีแอคชั่น"
   "write:votes": "โหวตบนสำรวจความคิดเห็น"
   "read:pages": "ดูหน้าเพจ"
-  "write:pages": "แก้ไขหรือลบเพจของคุณ"
+  "write:pages": "แก้ไขหรือลบเพจ"
   "read:page-likes": "ดูรายการเพจที่ถูกใจไว้"
   "write:page-likes": "แก้ไขรายการเพจที่ถูกใจ"
-  "read:user-groups": "ดูกลุ่มผู้ใช้ของคุณ"
-  "write:user-groups": "แก้ไขหรือลบกลุ่มผู้ใช้ของคุณ"
-  "read:channels": "ดูแชนแนลของคุณ"
-  "write:channels": "แก้ไขแชนแนลของคุณ"
+  "read:user-groups": "ดูกลุ่มผู้ใช้"
+  "write:user-groups": "แก้ไขหรือลบกลุ่มผู้ใช้"
+  "read:channels": "ดูช่อง"
+  "write:channels": "แก้ไขช่อง"
   "read:gallery": "ดูแกลเลอรี่"
-  "write:gallery": "แก้ไขแกลเลอรี่ของคุณ"
-  "read:gallery-likes": "ดูรายการโพสต์แกลเลอรีที่ถูกใจไว้"
-  "write:gallery-likes": "แก้ไขรายการโพสต์แกลเลอรีที่ถูกใจไว้"
+  "write:gallery": "แก้ไขแกลเลอรี"
+  "read:gallery-likes": "ดูแกลเลอรีที่ถูกใจไว้"
+  "write:gallery-likes": "จัดการแกลเลอรีที่ถูกใจไว้"
   "read:flash": "ดู Play"
   "write:flash": "แก้ไข Play"
   "read:flash-likes": "ดูรายการ  play ที่ถูกใจไว้"
@@ -2027,20 +2105,20 @@ _permissions:
   "write:admin:delete-account": "ลบบัญชีผู้ใช้"
   "write:admin:delete-all-files-of-a-user": "ลบไฟล์ทั้งหมดของผู้ใช้"
   "read:admin:index-stats": "ดูข้อมูลเกี่ยวกับดัชนีฐานข้อมูล"
-  "read:admin:table-stats": "ดูข้อมูลเกี่ยวกับตารางฐานข้อมูล"
+  "read:admin:table-stats": "ดูข้อมูลเกี่ยวกับตารางในฐานข้อมูล"
   "read:admin:user-ips": "ดูที่อยู่ IP ของผู้ใช้"
-  "read:admin:meta": "ดูข้อมูลเมตาของอินสแตนซ์"
+  "read:admin:meta": "ดูข้อมูลอภิพันธุ์ของอินสแตนซ์"
   "write:admin:reset-password": "รีเซ็ตรหัสผ่านของผู้ใช้"
   "write:admin:resolve-abuse-user-report": "แก้ไขรายงานจากผู้ใช้"
   "write:admin:send-email": "ส่งอีเมล"
   "read:admin:server-info": "ดูข้อมูลเซิร์ฟเวอร์"
-  "read:admin:show-moderation-log": "ดูปูมการแก้ไข"
+  "read:admin:show-moderation-log": "ดูปูมการควบคุมดูแล"
   "read:admin:show-user": "ดูข้อมูลส่วนตัวของผู้ใช้"
   "write:admin:suspend-user": "ระงับผู้ใช้"
   "write:admin:unset-user-avatar": "ลบอวตารผู้ใช้"
   "write:admin:unset-user-banner": "ลบแบนเนอร์ผู้ใช้"
   "write:admin:unsuspend-user": "ยกเลิกการระงับผู้ใช้"
-  "write:admin:meta": "จัดการข้อมูลเมตาของอินสแตนซ์"
+  "write:admin:meta": "จัดการข้อมูลอภิพันธุ์ของอินสแตนซ์"
   "write:admin:user-note": "จัดการโน้ตการกลั่นกรอง"
   "write:admin:roles": "จัดการบทบาท"
   "read:admin:roles": "ดูบทบาท"
@@ -2067,14 +2145,14 @@ _permissions:
   "read:admin:ad": "ดูโฆษณา"
   "write:invite-codes": "สร้างรหัสเชิญ"
   "read:invite-codes": "รับรหัสเชิญ"
-  "write:clip-favorite": "ควบคุมการถูกใจของคลิป"
-  "read:clip-favorite": "ดูการถูกใจของคลิป"
+  "write:clip-favorite": "จัดการคลิปที่ถูกใจ"
+  "read:clip-favorite": "ดูคลิปที่ถูกใจ"
   "read:federation": "รับข้อมูลเกี่ยวกับสหพันธ์"
   "write:report-abuse": "รายงานการละเมิด"
 _auth:
   shareAccessTitle: "การให้สิทธิ์แอปพลิเคชัน"
   shareAccess: "คุณต้องการอนุญาตให้ \"{name}\" เข้าถึงบัญชีนี้เลยมั้ย?"
-  shareAccessAsk: "ต้องการอนุญาตให้แอปพลิเคชันนี้เข้าถึงบัญชีของคุณหรือไม่?"
+  shareAccessAsk: "ต้องการอนุญาตให้แอปพลิเคชันนี้เข้าถึงบัญชีของคุณใช่ไหม?"
   permission: "{name} ได้ขอสิทธิ์การเข้าถึงดังต่อไปนี้"
   permissionAsk: "แอปพลิเคชันนี้ขอสิทธิ์ดังต่อไปนี้"
   pleaseGoBack: "กรุณากลับไปที่แอปพลิเคชัน"
@@ -2097,7 +2175,7 @@ _weekday:
   saturday: "วันเสาร์"
 _widgets:
   profile: "โปรไฟล์"
-  instanceInfo: "ข้อมูล อินสแตนซ์"
+  instanceInfo: "ข้อมูลเซิร์ฟเวอร์"
   memo: "โน้ตแปะ"
   notifications: "การเเจ้งเตือน"
   timeline: "ไทม์ไลน์"
@@ -2111,7 +2189,7 @@ _widgets:
   digitalClock: "นาฬิกาดิจิตอล"
   unixClock: "นาฬิกา UNIX"
   federation: "สหพันธ์"
-  instanceCloud: "อินสแตนซ์คลาวด์"
+  instanceCloud: "กลุ่มเมฆเซิร์ฟเวอร์"
   postForm: "แบบฟอร์มการโพสต์"
   slideshow: "แสดงภาพนิ่ง"
   button: "ปุ่ม"
@@ -2120,7 +2198,7 @@ _widgets:
   serverMetric: "ตัวชี้วัดเซิร์ฟเวอร์"
   aiscript: " คอนโซล AiScript"
   aiscriptApp: "แอป AiScript"
-  aichan: "ไอ"
+  aichan: "藍 (ไอ)"
   userList: "รายชื่อผู้ใช้"
   _userList:
     chooseList: "เลือกรายชื่อ"
@@ -2144,7 +2222,7 @@ _poll:
   deadlineTime: "เวลา"
   duration: "ระยะเวลา"
   votesCount: "{n} คะแนนเสียง"
-  totalVotes: "{n} คะแนนเสียงทั้งหมด"
+  totalVotes: "ทั้งหมด {n} คะแนนเสียง"
   vote: "โหวต"
   showResult: "ดูผลลัพธ์"
   voted: "โหวตแล้ว"
@@ -2156,14 +2234,14 @@ _poll:
 _visibility:
   public: "สาธารณะ"
   publicDescription: "โน้ตของคุณจะปรากฏแก่ผู้ใช้ทุกคน"
-  home: "หน้าแรก"
-  homeDescription: "โพสลงไทม์ไลน์ที่บ้านเท่านั้น"
+  home: "หน้าหลัก"
+  homeDescription: "โพสต์ลงไทม์ไลน์หลักเท่านั้น"
   followers: "ผู้ติดตาม"
   followersDescription: "เฉพาะผู้ติดตามเท่านั้นที่มองเห็นได้"
   specified: "ไดเร็ค"
   specifiedDescription: "ทำให้มองเห็นได้เฉพาะผู้ใช้ที่ระบุเท่านั้น"
-  disableFederation: "ไม่มีสหพันธ์"
-  disableFederationDescription: "อย่าส่งไปยังอินสแตนซ์อื่น"
+  disableFederation: "การปิดใช้งานสหพันธ์"
+  disableFederationDescription: "อย่าส่งข้อมูลไปยังเซิร์ฟเวอร์อื่น"
 _postForm:
   replyPlaceholder: "ตอบกลับโน้ตนี้..."
   quotePlaceholder: "อ้างโน้ตนี้..."
@@ -2189,6 +2267,9 @@ _profile:
   changeBanner: "เปลี่ยนแบนเนอร์"
   verifiedLinkDescription: "หากป้อน URL ที่มีลิงก์ไปยังโปรไฟล์ของคุณ ไอคอนการยืนยันความเป็นเจ้าของจะแสดงถัดจากฟิลด์นั้น ๆ"
   avatarDecorationMax: "คุณสามารถเพิ่มการตกแต่งได้สูงสุด {max}"
+  followedMessage: "ส่งข้อความเมื่อมีคนกดติดตาม"
+  followedMessageDescription: "ส่งข้อความเมื่อมีคนกดติดตามแล้ว"
+  followedMessageDescriptionForLockedAccount: "ถ้าหากคุณตั้งค่าให้คนอื่นต้องขออนุญาตก่อนที่จะติดตามคุณ ระบบจะขึ้นข้อความนี้ในตอนที่คุณอนุมัติให้เขาติดตาม"
 _exportOrImport:
   allNotes: "โน้ตทั้งหมด"
   favoritedNotes: "โน้ตที่ถูกใจไว้"
@@ -2199,37 +2280,37 @@ _exportOrImport:
   userLists: "รายชื่อ"
   excludeMutingUsers: "ยกเว้นผู้ใช้ที่ปิดเสียง"
   excludeInactiveUsers: "ยกเว้นผู้ใช้ที่ไม่ได้ใช้งาน"
-  withReplies: "รวมการตอบกลับจากผู้ใช้ที่นำเข้าไว้ในไทม์ไลน์"
+  withReplies: "รวมการตอบกลับจากผู้ใช้ที่ถูกนำเข้า ลงไทม์ไลน์"
 _charts:
   federation: "สหพันธ์"
   apRequest: "คำขอ"
-  usersIncDec: "ความแตกต่างของจำนวนผู้ใช้งาน"
+  usersIncDec: "การเพิ่มลดของจำนวนผู้ใช้"
   usersTotal: "จำนวนผู้ใช้งานทั้งหมด"
   activeUsers: "จำนวนผู้ใช้งานที่ยังมีความเคลื่อนไหวอยู่"
-  notesIncDec: "ความแตกต่างของจำนวนโน้ต"
-  localNotesIncDec: "ความแตกต่างของจำนวนโน้ตท้องถิ่น"
-  remoteNotesIncDec: "ความแตกต่างของจำนวนโน้ตระยะไกล"
+  notesIncDec: "การเพิ่มลดของจำนวนโน้ต"
+  localNotesIncDec: "การเพิ่มลดของจำนวนโน้ตท้องถิ่น"
+  remoteNotesIncDec: "การเพิ่มลดของจำนวนโน้ตระยะไกล"
   notesTotal: "จำนวนโน้ตทั้งหมด"
-  filesIncDec: "ความแตกต่างของจำนวนไฟล์"
+  filesIncDec: "การเพิ่มลดของจำนวนไฟล์"
   filesTotal: "จำนวนไฟล์ทั้งหมด"
-  storageUsageIncDec: "ความแตกต่างในการใช้พื้นที่เก็บข้อมูล"
+  storageUsageIncDec: "การเพิ่มลดในการใช้พื้นที่เก็บข้อมูล"
   storageUsageTotal: "การใช้พื้นที่เก็บข้อมูลทั้งหมด"
 _instanceCharts:
   requests: "คำขอ"
-  users: "ความแตกต่างของจำนวนผู้ใช้งาน"
+  users: "การเพิ่มลดของจำนวนผู้ใช้งาน"
   usersTotal: "จำนวนผู้ใช้งานสะสม"
-  notes: "ความแตกต่างของจำนวนโน้ต"
+  notes: "การเพิ่มลดของจำนวนโน้ต"
   notesTotal: "จำนวนโน้ตสะสม"
-  ff: "ความแตกต่างของจำนวนผู้ใช้ที่ติดตาม / ผู้ติดตาม"
-  ffTotal: "จำนวนผู้ใช้งานที่ติดตามสะสม / ผู้ติดตาม"
-  cacheSize: "ความแตกต่างในขนาดของแคช"
-  cacheSizeTotal: "ขนาดแคชรวมที่สะสม"
-  files: "ความแตกต่างของจำนวนไฟล์"
+  ff: "การเพิ่มลดของการติดตาม/ผู้ติดตาม"
+  ffTotal: "จำนวนสะสมของการติดตาม/ผู้ติดตาม"
+  cacheSize: "การเพิ่มลดขนาดของแคช"
+  cacheSizeTotal: "ขนาดแคชสะสม"
+  files: "การเพิ่มลดของจำนวนไฟล์"
   filesTotal: "จำนวนไฟล์สะสม"
 _timelines:
-  home: "หน้าแรก"
-  local: "ในพื้นที่"
-  social: "โซเชี่ยล"
+  home: "หน้าหลัก"
+  local: "ท้องถิ่น"
+  social: "โซเชียล"
   global: "ทั่วโลก"
 _play:
   new: "สร้าง Play"
@@ -2245,7 +2326,7 @@ _play:
   featured: "เป็นที่นิยม"
   title: "หัวข้อ"
   script: "สคริปต์"
-  summary: "รายละเอียด"
+  summary: "คำอธิบาย"
   visibilityDescription: "หากตั้งค่าเป็นส่วนตัว มันจะไม่ปรากฏในโปรไฟล์อีกต่อไป แต่ผู้ที่ทราบ URL ของมันจะยังสามารถเข้าถึงได้"
 _pages:
   newPage: "สร้างหน้าเพจใหม่"
@@ -2281,6 +2362,7 @@ _pages:
   eyeCatchingImageSet: "ตั้งค่าภาพขนาดย่อ"
   eyeCatchingImageRemove: "ลบภาพขนาดย่อ"
   chooseBlock: "เพิ่มบล็อค"
+  enterSectionTitle: "ป้อนชื่อหัวข้อ"
   selectType: "เลือกชนิด"
   contentBlocks: "เนื้อหา"
   inputBlocks: "ป้อนข้อมูล"
@@ -2326,6 +2408,8 @@ _notification:
   renotedBySomeUsers: "รีโน้ตจากผู้ใช้ {n} ราย"
   followedBySomeUsers: "มีผู้ติดตาม {n} ราย"
   flushNotification: "ล้างประวัติการแจ้งเตือน"
+  exportOfXCompleted: "การดำเนินการส่งออก {x} ได้เสร็จสิ้นลงแล้ว"
+  login: "มีคนล็อกอิน"
   _types:
     all: "ทั้งหมด"
     note: "โน้ตใหม่"
@@ -2333,13 +2417,16 @@ _notification:
     mention: "กล่าวถึง"
     reply: "ตอบกลับ"
     renote: "รีโน้ต"
-    quote: "อ้างคำพูด"
+    quote: "อ้างอิง"
     reaction: "รีแอคชั่น"
     pollEnded: "โพลสิ้นสุดแล้ว"
     receiveFollowRequest: "ได้รับคำร้องขอติดตาม"
     followRequestAccepted: "อนุมัติให้ติดตามแล้ว"
     roleAssigned: "ให้บทบาท"
     achievementEarned: "ปลดล็อกความสำเร็จแล้ว"
+    exportCompleted: "กระบวนการส่งออกข้อมูลได้เสร็จสิ้นสมบูรณ์แล้ว"
+    login: "เข้าสู่ระบบ"
+    test: "ทดสอบระบบแจ้งเตือน"
     app: "การแจ้งเตือนจากแอปที่มีลิงก์"
   _actions:
     followBack: "ติดตามกลับด้วย"
@@ -2349,6 +2436,7 @@ _deck:
   alwaysShowMainColumn: "แสดงคอลัมน์หลักเสมอ"
   columnAlign: "จัดแนวคอลัมน์"
   addColumn: "เพิ่มคอลัมน์"
+  newNoteNotificationSettings: "ตั้งค่าการแจ้งเตือนเมื่อมีโน้ตใหม่"
   configureColumn: "ตั้งค่าคอลัมน์"
   swapLeft: "ขยับไปทางซ้าย"
   swapRight: "ขยับไปทางขวา"
@@ -2373,7 +2461,7 @@ _deck:
     antenna: "เสาอากาศ"
     list: "รายการ"
     channel: "ช่อง"
-    mentions: "พูดถึง"
+    mentions: "กล่าวถึงคุณ"
     direct: "ไดเร็กต์"
     roleTimeline: "บทบาทไทม์ไลน์"
 _dialog:
@@ -2387,9 +2475,10 @@ _drivecleaner:
   orderByCreatedAtAsc: "วันที่จากน้อยไปหามาก"
 _webhookSettings:
   createWebhook: "สร้าง Webhook"
+  modifyWebhook: "แก้ไข Webhook"
   name: "ชื่อ"
   secret: "ความลับ"
-  events: "อีเว้นท์ Webhook"
+  trigger: "ทริกเกอร์"
   active: "เปิดใช้งาน"
   _events:
     follow: "เมื่อกำลังติดตามผู้ใช้"
@@ -2399,6 +2488,29 @@ _webhookSettings:
     renote: "รีโน้ตแล้วเมื่อ"
     reaction: "เมื่อได้รับรีแอคชั่น"
     mention: "เมื่อกำลังถูกกล่าวถึง"
+  _systemEvents:
+    abuseReport: "เมื่อมีการรายงานจากผู้ใช้"
+    abuseReportResolved: "เมื่อมีการจัดการกับการรายงานจากผู้ใช้"
+    userCreated: "เมื่อผู้ใช้ถูกสร้างขึ้น"
+    inactiveModeratorsWarning: "เมื่อผู้ดูแลระบบไม่ได้ใช้งานมานานระยะหนึ่ง"
+    inactiveModeratorsInvitationOnlyChanged: "เมื่อผู้ดูแลระบบที่ไม่ได้ใช้งานมานาน และเซิร์ฟเวอร์เปลี่ยนเป็นแบบเชิญเข้าร่วมเท่านั้น"
+  deleteConfirm: "ต้องการลบ Webhook ใช่ไหม?"
+  testRemarks: "คลิกปุ่มทางด้านขวาของสวิตช์เพื่อส่ง Webhook ทดสอบที่มีข้อมูลจำลอง"
+_abuseReport:
+  _notificationRecipient:
+    createRecipient: "เพิ่มปลายทางการแจ้งเตือนการรายงาน"
+    modifyRecipient: "แก้ไขปลายทางการแจ้งเตือนการรายงาน"
+    recipientType: "ประเภทของปลายทางการแจ้งเตือน\n"
+    _recipientType:
+      mail: "อีเมล"
+      webhook: "Webhook"
+      _captions:
+        mail: "ส่งการแจ้งเตือนไปยังที่อยู่อีเมลของผู้ควบคุม (เฉพาะเมื่อได้รับการรายงาน)"
+        webhook: "ส่งการแจ้งเตือนไปยัง SystemWebhook ที่กำหนด (จะส่งเมื่อได้รับการรายงานและเมื่อการรายงานได้รับการแก้ไข)"
+    keywords: "คีย์เวิร์ด"
+    notifiedUser: "ผู้ใช้ที่ได้รับการแจ้งเตือน"
+    notifiedWebhook: "Webhook ที่ใช้"
+    deleteConfirm: "ต้องการลบปลายทางการแจ้งเตือนใช่ไหม?"
 _moderationLogTypes:
   createRole: "สร้างบทบาทแล้ว"
   deleteRole: "ลบบทบาทแล้ว"
@@ -2421,12 +2533,14 @@ _moderationLogTypes:
   deleteGlobalAnnouncement: "ลบประกาศทั่วโลกออกแล้ว"
   deleteUserAnnouncement: "ลบประกาศผู้ใช้ออกแล้ว"
   resetPassword: "รีเซ็ตรหัสผ่าน"
-  suspendRemoteInstance: "ระงับอินสแตนซ์ระยะไกล"
-  unsuspendRemoteInstance: "เลิกระงับอินสแตนซ์ระยะไกล"
-  updateRemoteInstanceNote: "อัปเดตโน้ตการกลั่นกรองของอินสแตนซ์ระยะไกลแล้ว"
+  suspendRemoteInstance: "ระงับเซิร์ฟเวอร์ระยะไกล"
+  unsuspendRemoteInstance: "เลิกระงับเซิร์ฟเวอร์ระยะไกล"
+  updateRemoteInstanceNote: "อัปเดตโน้ตการกลั่นกรองสำหรับเซิร์ฟเวอร์ระยะไกลแล้ว"
   markSensitiveDriveFile: "ทำเครื่องหมายไฟล์ว่ามีเนื้อหาละเอียดอ่อน"
   unmarkSensitiveDriveFile: "ยกเลิกทำเครื่องหมายไฟล์ว่ามีเนื้อหาละเอียดอ่อน"
   resolveAbuseReport: "รายงานได้รับการแก้ไขแล้ว"
+  forwardAbuseReport: "ได้ส่งรายงานไปแล้ว"
+  updateAbuseReportNote: "โน้ตการกลั่นกรองที่รายงานไปนั้น ได้รับการอัปเดตแล้ว"
   createInvitation: "สร้างรหัสเชิญ"
   createAd: "สร้างโฆษณาแล้ว"
   deleteAd: "ลบโฆษณาออกแล้ว"
@@ -2436,6 +2550,16 @@ _moderationLogTypes:
   deleteAvatarDecoration: "ลบการตกแต่งไอคอนแล้ว"
   unsetUserAvatar: "ลบไอคอนผู้ใช้"
   unsetUserBanner: "ลบแบนเนอร์ผู้ใช้"
+  createSystemWebhook: "สร้าง SystemWebhook"
+  updateSystemWebhook: "อัปเดต SystemWebhook"
+  deleteSystemWebhook: "ลบ SystemWebhook"
+  createAbuseReportNotificationRecipient: "สร้างปลายทางการแจ้งเตือนการรายงาน"
+  updateAbuseReportNotificationRecipient: "อัปเดตปลายทางการแจ้งเตือนการรายงาน"
+  deleteAbuseReportNotificationRecipient: "ลบปลายทางการแจ้งเตือนการรายงาน"
+  deleteAccount: "บัญชีถูกลบไปแล้ว"
+  deletePage: "เพจถูกลบออกไปแล้ว"
+  deleteFlash: "Play ถูกลบออกไปแล้ว"
+  deleteGalleryPost: "โพสต์แกลเลอรี่ถูกลบออกแล้ว"
 _fileViewer:
   title: "รายละเอียดไฟล์"
   type: "ประเภทไฟล์"
@@ -2448,10 +2572,10 @@ _externalResourceInstaller:
   title: "ติดตั้งจากไซต์ภายนอก"
   checkVendorBeforeInstall: "โปรดตรวจสอบให้แน่ใจว่าแหล่งแจกหน่ายมีความน่าเชื่อถือก่อนทำการติดตั้ง"
   _plugin:
-    title: "ต้องการติดตั้งปลั๊กอินนี้หรือไม่?"
+    title: "ต้องการติดตั้งปลั๊กอินนี้ใช่ไหม?"
     metaTitle: "ข้อมูลส่วนเสริม"
   _theme:
-    title: "ต้องการติดตั้งธีมนี้หรือไม่?"
+    title: "ต้องการติดตั้งธีมนี้ใช่ไหม?"
     metaTitle: "ข้อมูลธีม"
   _meta:
     base: "โทนสีพื้นฐาน"
@@ -2487,7 +2611,7 @@ _externalResourceInstaller:
       description: "เกิดปัญหาระหว่างการติดตั้งธีม กรุณาลองอีกครั้ง. รายละเอียดข้อผิดพลาดสามารถดูได้ในคอนโซล Javascript"
 _dataSaver:
   _media:
-    title: "โหลดมีเดีย"
+    title: "โหลดสื่อ"
     description: "กันไม่ให้ภาพและวิดีโอโหลดโดยอัตโนมัติ แตะรูปภาพ/วิดีโอที่ซ่อนอยู่เพื่อโหลด"
   _avatar:
     title: "รูปไอคอน"
@@ -2567,3 +2691,22 @@ _mediaControls:
   pip: "รูปภาพในรูปภาม"
   playbackRate: "ความเร็วในการเล่น"
   loop: "เล่นวนซ้ำ"
+_contextMenu:
+  title: "เมนูเนื้อหา"
+  app: "แอปพลิเคชัน"
+  appWithShift: "แอปฟลิเคชันด้วยปุ่มยกแคร่ (Shift)"
+  native: "UI ของเบราว์เซอร์"
+_embedCodeGen:
+  title: "ปรับแต่งโค้ดฝัง"
+  header: "แสดงส่วนหัว"
+  autoload: "โหลดเพิ่มโดยอัตโนมัติ (เลิกใช้แล้ว)"
+  maxHeight: "ความสูงสุด"
+  maxHeightDescription: "หากถ้าตั้งค่าเป็น 0 จะทำให้ไม่มีการจำกัดความสูงของวิดเจ็ต แต่ควรตั้งค่าเป็นตัวเลขอื่นๆ เพื่อไม่ให้วิดเจ็ตยืดตัวลงไปเรื่อยๆ"
+  maxHeightWarn: "การจำกัดความสูงสูงสุดถูกปิดใช้งาน (0) หากไม่ได้ตั้งใจให้เป็นเช่นนี้ โปรดตั้งค่าความสูงสูงสุดให้เป็นค่าอื่นๆแทน"
+  previewIsNotActual: "การแสดงผลนั้นต่างจากการฝังจริงเพราะเกินขอบเขตที่แสดงบนหน้าจอตัวอย่างนะ"
+  rounded: "ทำให้มันกลม"
+  border: "เพิ่มขอบให้กับกรอบด้านนอก"
+  applyToPreview: "นำไปใช้กับการแสดงตัวอย่าง"
+  generateCode: "สร้างโค้ดสำหรับการฝัง"
+  codeGenerated: "รหัสถูกสร้างขึ้นแล้ว"
+  codeGeneratedDescription: "นำโค้ดที่สร้างแล้วไปวางในเว็บไซต์ของคุณเพื่อฝังเนื้อหา"
diff --git a/locales/tr-TR.yml b/locales/tr-TR.yml
index cf6729a81d..fe2f158ff6 100644
--- a/locales/tr-TR.yml
+++ b/locales/tr-TR.yml
@@ -446,6 +446,7 @@ _notification:
     reaction: "Tepkiler"
     receiveFollowRequest: "Takip isteği alındı"
     followRequestAccepted: "Takip isteği kabul edildi"
+    login: "Giriş Yap "
   _actions:
     reply: "yanıt"
     renote: "vazgeçme"
diff --git a/locales/ug-CN.yml b/locales/ug-CN.yml
index e48f64511c..fef26040a5 100644
--- a/locales/ug-CN.yml
+++ b/locales/ug-CN.yml
@@ -17,3 +17,6 @@ _2fa:
   renewTOTPCancel: "ئۇنى توختىتىڭ"
 _widgets:
   profile: "profile"
+_notification:
+  _types:
+    login: "كىرىش"
diff --git a/locales/uk-UA.yml b/locales/uk-UA.yml
index 661ecf19d7..f2262cd71f 100644
--- a/locales/uk-UA.yml
+++ b/locales/uk-UA.yml
@@ -452,7 +452,6 @@ language: "Мова"
 uiLanguage: "Мова інтерфейсу"
 aboutX: "Про {x}"
 native: "місцевий"
-disableDrawer: "Не використовувати висувні меню"
 noHistory: "Історія порожня"
 signinHistory: "Історія входів"
 enableAdvancedMfm: "Увімкнути розширений MFM"
@@ -631,10 +630,7 @@ abuseReported: "Дякуємо, вашу скаргу було відправл
 reporter: "Репортер"
 reporteeOrigin: "Про кого повідомлено"
 reporterOrigin: "Хто повідомив"
-forwardReport: "Переслати звіт на віддалений інстанс"
-forwardReportIsAnonymous: "Замість вашого облікового запису анонімний системний обліковий запис буде відображатися як доповідач на віддаленому інстансі"
 send: "Відправити"
-abuseMarkAsResolved: "Позначити скаргу як вирішену"
 openInNewTab: "Відкрити в новій вкладці"
 openInSideView: "Відкрити збоку"
 defaultNavigationBehaviour: "Поведінка навігації за замовчуванням"
@@ -1306,7 +1302,6 @@ _theme:
     buttonBg: "Фон кнопки"
     buttonHoverBg: "Фон кнопки (при наведенні)"
     inputBorder: "Край поля вводу"
-    listItemHoverBg: "Фон елементу в списку (при наведенні)"
     driveFolderBg: "Фон папки на диску"
     wallpaperOverlay: "Накладання шпалер"
     badge: "Значок"
@@ -1318,8 +1313,6 @@ _sfx:
   note: "Нотатки"
   noteMy: "Мої нотатки"
   notification: "Сповіщення"
-  antenna: "Прийом антени"
-  channel: "Повідомлення каналу"
 _ago:
   future: "Майбутнє"
   justNow: "Щойно"
@@ -1590,6 +1583,7 @@ _notification:
     reaction: "Реакції"
     receiveFollowRequest: "Запити на підписку"
     followRequestAccepted: "Прийняті підписки"
+    login: "Увійти"
     app: "Сповіщення від додатків"
   _actions:
     reply: "Відповісти"
@@ -1622,6 +1616,10 @@ _deck:
 _webhookSettings:
   name: "Ім'я"
   active: "Увімкнено"
+_abuseReport:
+  _notificationRecipient:
+    _recipientType:
+      mail: "E-mail"
 _moderationLogTypes:
   suspend: "Призупинити"
   resetPassword: "Скинути пароль"
diff --git a/locales/uz-UZ.yml b/locales/uz-UZ.yml
index 4a930626f4..37a550008a 100644
--- a/locales/uz-UZ.yml
+++ b/locales/uz-UZ.yml
@@ -471,7 +471,6 @@ uiLanguage: "Interfeys tili"
 aboutX: "{x} haqida"
 emojiStyle: "Emoji ko'rinishi"
 native: "Mahalliy"
-disableDrawer: "Slayd menyusidan foydalanmang"
 showNoteActionsOnlyHover: "Eslatma amallarini faqat sichqonchani olib borganda ko‘rsatish"
 noHistory: "Tarix yo'q"
 signinHistory: "kirish tarixi"
@@ -630,10 +629,7 @@ abuseReported: "Shikoyatingiz yetkazildi. Ma'lumot uchun rahmat."
 reporter: "Shikoyat qiluvchi"
 reporteeOrigin: "Xabarning kelib chiqishi"
 reporterOrigin: "Xabarchining joylashuvi"
-forwardReport: "Xabarni masofadagi serverga yuborish"
-forwardReportIsAnonymous: "Sizning yuborayotgan xabaringiz o'z akkountingiz emas balki anonim tarzda qoladi"
 send: "Yuborish"
-abuseMarkAsResolved: "Yuborilgan xabarni hal qilingan deb belgilash"
 openInNewTab: "Yangi tab da ochish"
 openInSideView: "Yon panelda ochish"
 defaultNavigationBehaviour: "Standart navigatsiya harakati"
@@ -1058,6 +1054,7 @@ _notification:
     quote: "Iqtibos keltirish"
     reaction: "Reaktsiyalar"
     receiveFollowRequest: "Qabul qilingan kuzatuv so'rovlari"
+    login: "Kirish"
   _actions:
     reply: "Javob berish"
     renote: "Qayta qayd qilish"
@@ -1089,6 +1086,10 @@ _webhookSettings:
   _events:
     renote: "Qayta qayd qilinganda"
     mention: "Eslanganda"
+_abuseReport:
+  _notificationRecipient:
+    _recipientType:
+      mail: "Email"
 _moderationLogTypes:
   suspend: "To'xtatish"
   resetPassword: "Parolni tiklash"
diff --git a/locales/vi-VN.yml b/locales/vi-VN.yml
index acc2e0c6a9..235497d844 100644
--- a/locales/vi-VN.yml
+++ b/locales/vi-VN.yml
@@ -376,6 +376,7 @@ mcaptcha: "mCaptcha"
 enableMcaptcha: "Bật mCaptcha"
 mcaptchaSiteKey: "Khóa của trang"
 mcaptchaSecretKey: "Khóa bí mật"
+mcaptchaInstanceUrl: "URL mCaptcha máy chủ"
 recaptcha: "reCAPTCHA"
 enableRecaptcha: "Bật reCAPTCHA"
 recaptchaSiteKey: "Khóa của trang"
@@ -426,6 +427,7 @@ moderator: "Kiểm duyệt viên"
 moderation: "Kiểm duyệt"
 moderationNote: "Ghi chú kiểm duyệt"
 addModerationNote: "Thêm ghi chú kiểm duyệt"
+moderationLogs: "Nhật kí quản trị"
 nUsersMentioned: "Dùng bởi {n} người"
 securityKeyAndPasskey: "Mã bảo mật・Passkey"
 securityKey: "Khóa bảo mật"
@@ -458,6 +460,7 @@ retype: "Nhập lại"
 noteOf: "Tút của {user}"
 quoteAttached: "Trích dẫn"
 quoteQuestion: "Trích dẫn lại?"
+attachAsFileQuestion: "Văn bản ở trong bộ nhớ tạm rất dài. Bạn có muốn đăng nó dưới dạng một tệp văn bản không?"
 noMessagesYet: "Chưa có tin nhắn"
 newMessageExists: "Bạn có tin nhắn mới"
 onlyOneFileCanBeAttached: "Bạn chỉ có thể đính kèm một tập tin"
@@ -483,7 +486,6 @@ uiLanguage: "Ngôn ngữ giao diện"
 aboutX: "Giới thiệu {x}"
 emojiStyle: "Kiểu cách Emoji"
 native: "Bản xứ"
-disableDrawer: "Không dùng menu thanh bên"
 showNoteActionsOnlyHover: "Chỉ hiển thị các hành động ghi chú khi di chuột"
 noHistory: "Không có dữ liệu"
 signinHistory: "Lịch sử đăng nhập"
@@ -673,10 +675,7 @@ abuseReported: "Báo cáo đã được gửi. Cảm ơn bạn nhiều."
 reporter: "Người báo cáo"
 reporteeOrigin: "Bị báo cáo"
 reporterOrigin: "Máy chủ người báo cáo"
-forwardReport: "Chuyển tiếp báo cáo cho máy chủ từ xa"
-forwardReportIsAnonymous: "Thay vì tài khoản của bạn, một tài khoản hệ thống ẩn danh sẽ được hiển thị dưới dạng người báo cáo ở máy chủ từ xa."
 send: "Gửi"
-abuseMarkAsResolved: "Đánh dấu đã xử lý"
 openInNewTab: "Mở trong tab mới"
 openInSideView: "Mở trong thanh bên"
 defaultNavigationBehaviour: "Thao tác điều hướng mặc định"
@@ -1547,7 +1546,6 @@ _theme:
     buttonBg: "Nền nút"
     buttonHoverBg: "Nền nút (Chạm)"
     inputBorder: "Đường viền khung soạn thảo"
-    listItemHoverBg: "Nền mục liệt kê (Chạm)"
     driveFolderBg: "Nền thư mục Ổ đĩa"
     wallpaperOverlay: "Lớp phủ hình nền"
     badge: "Huy hiệu"
@@ -1559,8 +1557,6 @@ _sfx:
   note: "Tút"
   noteMy: "Tút của tôi"
   notification: "Thông báo"
-  antenna: "Trạm phát sóng"
-  channel: "Kênh"
 _ago:
   future: "Tương lai"
   justNow: "Vừa xong"
@@ -1878,6 +1874,7 @@ _notification:
     receiveFollowRequest: "Yêu cầu theo dõi"
     followRequestAccepted: "Yêu cầu theo dõi được chấp nhận"
     achievementEarned: "Hoàn thành Achievement"
+    login: "Đăng nhập"
     app: "Từ app liên kết"
   _actions:
     followBack: "đã theo dõi lại bạn"
@@ -1917,11 +1914,14 @@ _webhookSettings:
   createWebhook: "Tạo Webhook"
   name: "Tên"
   secret: "Mã bí mật"
-  events: "Sự kiện Webhook"
   active: "Đã bật"
   _events:
     reaction: "Khi nhận được sự kiện"
     mention: "Khi có người nhắc tới bạn"
+_abuseReport:
+  _notificationRecipient:
+    _recipientType:
+      mail: "Email"
 _moderationLogTypes:
   suspend: "Vô hiệu hóa"
   resetPassword: "Đặt lại mật khẩu"
diff --git a/locales/zh-CN.yml b/locales/zh-CN.yml
index f92d997b5a..93804608c2 100644
--- a/locales/zh-CN.yml
+++ b/locales/zh-CN.yml
@@ -8,6 +8,9 @@ search: "搜索"
 notifications: "通知"
 username: "用户名"
 password: "密码"
+initialPasswordForSetup: "初始化密码"
+initialPasswordIsIncorrect: "初始化密码不正确"
+initialPasswordForSetupDescription: "如果是自己安装的 Misskey,请输入配置文件里设好的密码。\n如果使用的是 Misskey 的托管服务等,请输入服务商提供的密码。\n如果没有设置密码,请留空并继续。"
 forgotPassword: "忘记密码"
 fetchingAsApObject: "在联邦宇宙查询中..."
 ok: "OK"
@@ -60,6 +63,7 @@ copyFileId: "复制文件ID"
 copyFolderId: "复制文件夹ID"
 copyProfileUrl: "复制个人资料URL"
 searchUser: "搜索用户"
+searchThisUsersNotes: "搜索用户帖子"
 reply: "回复"
 loadMore: "查看更多"
 showMore: "查看更多"
@@ -89,7 +93,7 @@ followsYou: "正在关注你"
 createList: "创建列表"
 manageLists: "管理列表"
 error: "错误"
-somethingHappened: "出现了一些问题!"
+somethingHappened: "出错了"
 retry: "重试"
 pageLoadError: "页面加载失败。"
 pageLoadErrorDescription: "这通常是由于网络或浏览器缓存的原因。请清除缓存或等待片刻后重试。"
@@ -154,6 +158,7 @@ editList: "编辑列表"
 selectChannel: "选择频道"
 selectAntenna: "选择天线"
 editAntenna: "编辑天线"
+createAntenna: "创建天线"
 selectWidget: "选择小工具"
 editWidgets: "编辑部件"
 editWidgetsExit: "完成编辑"
@@ -165,7 +170,7 @@ emojiUrl: "emoji 地址"
 addEmoji: "添加表情符号"
 settingGuide: "推荐配置"
 cacheRemoteFiles: "缓存远程文件"
-cacheRemoteFilesDescription: "启用此设定时,将在此服务器上缓存远程文件。虽然可以加快图片显示的速度,但是相对的会消耗大量的服务器存储空间。用户角色内的网盘容量决定了这个远程用户能在服务器上保留保留多少缓存。当超出了这个限制时,旧的文件将从缓存中被删除,成为链接。当禁用此设定时,则是从一开始就将远程文件保留为链接。此时推荐将 default.yml 的 proxyRemoteFiles 设置为 true 以优化缩略图生成及保护用户隐私。"
+cacheRemoteFilesDescription: "启用此设定时,将在此服务器上缓存远程文件。虽然可以加快图片显示的速度,但是相对的会消耗大量的服务器存储空间。用户角色内的网盘容量决定了这个远程用户能在服务器上保留多少缓存。当超出了这个限制时,旧的文件将从缓存中被删除,成为链接。当禁用此设定时,则是从一开始就将远程文件保留为链接。此时推荐将 default.yml 的 proxyRemoteFiles 设置为 true 以优化缩略图生成及保护用户隐私。"
 youCanCleanRemoteFilesCache: "可以使用文件管理的🗑️按钮来删除所有的缓存。"
 cacheRemoteSensitiveFiles: "缓存远程敏感媒体文件"
 cacheRemoteSensitiveFilesDescription: "如果禁用这项设定,远程服务器的敏感媒体将不会被缓存,而是直接链接。"
@@ -180,6 +185,10 @@ addAccount: "添加账户"
 reloadAccountsList: "更新账户列表"
 loginFailed: "登录失败"
 showOnRemote: "转到所在服务器显示"
+continueOnRemote: "转到所在服务器继续"
+chooseServerOnMisskeyHub: "从 Misskey Hub 选择服务器"
+specifyServerHost: "直接输入服务器域名"
+inputHostName: "请输入域名"
 general: "常规设置"
 wallpaper: "壁纸"
 setWallpaper: "设置壁纸"
@@ -190,6 +199,7 @@ followConfirm: "你确定要关注 {name} 吗?"
 proxyAccount: "代理账户"
 proxyAccountDescription: "代理账户是在某些情况下替代用户进行远程关注用的账户。 例如说,当用户将一位远程用户放入一个列表中时,如果本地服务器上没有任何人关注这位远程用户,则这位远程用户的账户活动将不会被送到本地服务器上。作为替代,此时将使用代理账户进行关注。"
 host: "主机名"
+selectSelf: "选择自己"
 selectUser: "选择用户"
 recipient: "收件人"
 annotation: "注解"
@@ -203,8 +213,9 @@ charts: "图表"
 perHour: "每小时"
 perDay: "每天"
 stopActivityDelivery: "停止发送活动"
-blockThisInstance: "阻止此服务器向本服务器推流"
-silenceThisInstance: "使服务器静音"
+blockThisInstance: "封锁此服务器"
+silenceThisInstance: "静音此服务器"
+mediaSilenceThisInstance: "隐藏此服务器的媒体文件"
 operations: "操作"
 software: "软件"
 version: "版本"
@@ -223,9 +234,13 @@ clearQueueConfirmText: "未送达的帖子将不会被投递。 通常无需执
 clearCachedFiles: "清除缓存"
 clearCachedFilesConfirm: "确定要清除所有缓存的远程文件?"
 blockedInstances: "被封锁的服务器"
-blockedInstancesDescription: "设定要封锁的服务器,以换行来进行分割。被封锁的服务器将无法与本服务器进行交换通讯。子域名也同样会被封锁。"
+blockedInstancesDescription: "设定要封锁的服务器,以换行分隔。被封锁的服务器将无法与本服务器进行交换通讯。子域名也同样会被封锁。"
 silencedInstances: "被静音的服务器"
-silencedInstancesDescription: "设置要静音的服务器,以换行符分隔。被静音的服务器内所有的账户将默认处于「静音」状态,仅能发送关注请求,并且在未关注状态下无法提及本地账户。被阻止的实例不受影响。"
+silencedInstancesDescription: "设置要静音的服务器,以换行分隔。被静音的服务器内所有的账户将默认处于「静音」状态,仅能发送关注请求,并且在未关注状态下无法提及本地账户。被阻止的实例不受影响。"
+mediaSilencedInstances: "已隐藏媒体文件的服务器"
+mediaSilencedInstancesDescription: "设置要隐藏媒体文件的服务器,以换行分隔。被设置为隐藏媒体文件服务器内所有账号的文件均按照「敏感内容」处理,且将无法使用自定义表情符号。被阻止的实例不受影响。"
+federationAllowedHosts: "允许联合的服务器"
+federationAllowedHostsDescription: "设定允许联合的服务器,以换行分隔。"
 muteAndBlock: "静音/拉黑"
 mutedUsers: "已静音用户"
 blockedUsers: "已拉黑的用户"
@@ -243,7 +258,7 @@ noCustomEmojis: "没有自定义表情符号"
 noJobs: "没有任务"
 federating: "联合中"
 blocked: "已拉黑"
-suspended: "停止推流"
+suspended: "停止投递"
 all: "全部"
 subscribing: "已订阅"
 publishing: "投递中"
@@ -324,6 +339,7 @@ renameFolder: "重命名文件夹"
 deleteFolder: "删除文件夹"
 folder: "文件夹"
 addFile: "添加文件"
+showFile: "显示文件"
 emptyDrive: "网盘中无文件"
 emptyFolder: "此文件夹中无文件"
 unableToDelete: "无法删除"
@@ -351,7 +367,7 @@ instanceName: "服务器名称"
 instanceDescription: "服务器简介"
 maintainerName: "管理员名称"
 maintainerEmail: "管理员电子邮箱"
-tosUrl: "服务条款 URL"
+tosUrl: "服务条款地址"
 thisYear: "今年"
 thisMonth: "本月"
 today: "今天"
@@ -433,11 +449,12 @@ administrator: "管理员"
 token: "Token (令牌)"
 2fa: "双因素认证"
 setupOf2fa: "设置双因素认证"
-totp: "身份验证应用"
-totpDescription: "使用认证应用输入一次性密码。"
+totp: "验证器"
+totpDescription: "使用验证器输入一次性密码"
 moderator: "监察员"
 moderation: "管理"
 moderationNote: "管理笔记"
+moderationNoteDescription: "可以用来记录仅在管理员之间共享的笔记。"
 addModerationNote: "添加管理笔记"
 moderationLogs: "管理日志"
 nUsersMentioned: "{n} 被提到"
@@ -477,6 +494,7 @@ noMessagesYet: "现在没有新的聊天"
 newMessageExists: "新信息"
 onlyOneFileCanBeAttached: "只能添加一个附件"
 signinRequired: "请先登录"
+signinOrContinueOnRemote: "若要继续,需要转到您所使用的实例,或者在此服务器上注册或登录。"
 invitations: "邀请"
 invitationCode: "邀请码"
 checking: "正在确认"
@@ -498,7 +516,10 @@ uiLanguage: "显示语言"
 aboutX: "关于 {x}"
 emojiStyle: "表情符号的样式"
 native: "原生"
-disableDrawer: "不显示抽屉菜单"
+menuStyle: "菜单样式"
+style: "样式"
+drawer: "抽屉"
+popup: "弹窗"
 showNoteActionsOnlyHover: "仅在悬停时显示帖子操作"
 showReactionsCount: "显示帖子的回应数"
 noHistory: "没有历史记录"
@@ -581,6 +602,8 @@ ascendingOrder: "升序"
 descendingOrder: "降序"
 scratchpad: "AiScript 控制台"
 scratchpadDescription: "AiScript 控制台为 AiScript 提供了实验环境。您可以编写代码与 Misskey 交互,运行并查看结果。"
+uiInspector: "UI 检查器"
+uiInspectorDescription: "查看所有内存中由 UI 组件生成出的实例。UI 组件由 UI:C 系列函数所生成。"
 output: "输出"
 script: "脚本"
 disablePagesScript: "禁用页面脚本"
@@ -683,7 +706,7 @@ useGlobalSettingDesc: "启用时,将使用账户通知设置。关闭时,则
 other: "其他"
 regenerateLoginToken: "重新生成登录令牌"
 regenerateLoginTokenDescription: "重新生成用于登录的内部令牌。通常您不需要这样做。重新生成后,您将在所有设备上登出。"
-theKeywordWhenSearchingForCustomEmoji: "这将是搜素自定义表情符号时的关键词。"
+theKeywordWhenSearchingForCustomEmoji: "这将是搜索自定义表情符号时的关键词。"
 setMultipleBySeparatingWithSpace: "您可以使用空格分隔多个项目。"
 fileIdOrUrl: "文件 ID 或者 URL"
 behavior: "行为"
@@ -697,10 +720,7 @@ abuseReported: "内容已发送。感谢您提交信息。"
 reporter: "举报者"
 reporteeOrigin: "举报来源"
 reporterOrigin: "举报者来源"
-forwardReport: "将该举报信息转发给远程服务器"
-forwardReportIsAnonymous: "在远程实例上显示的报告者是匿名的系统账号,而不是您的账号。"
 send: "发送"
-abuseMarkAsResolved: "处理完毕"
 openInNewTab: "在新标签页中打开"
 openInSideView: "在侧边栏中打开"
 defaultNavigationBehaviour: "默认导航"
@@ -837,6 +857,7 @@ administration: "管理"
 accounts: "账户"
 switch: "切换"
 noMaintainerInformationWarning: "管理人员信息未设置。"
+noInquiryUrlWarning: "尚未设置联络地址。"
 noBotProtectionWarning: "Bot 防御未设置。"
 configure: "设置"
 postToGallery: "发送到图库"
@@ -848,7 +869,7 @@ shareWithNote: "在帖子中分享"
 ads: "广告"
 expiration: "截止时间"
 startingperiod: "开始时间"
-memo: "便笺"
+memo: "备注"
 priority: "优先级"
 high: "高"
 middle: "中"
@@ -901,6 +922,7 @@ followersVisibility: "关注者的公开范围"
 continueThread: "查看更多帖子"
 deleteAccountConfirm: "将要删除账户。是否确认?"
 incorrectPassword: "密码错误"
+incorrectTotp: "一次性密码不正确或已过期"
 voteConfirm: "确定投给 “{choice}” ?"
 hide: "隐藏"
 useDrawerReactionPickerForMobile: "在移动设备上使用抽屉显示"
@@ -925,6 +947,9 @@ oneHour: "1 小时"
 oneDay: "1 天"
 oneWeek: "1 周"
 oneMonth: "1 个月"
+threeMonths: "3 个月"
+oneYear: "1 年"
+threeDays: "3 天"
 reflectMayTakeTime: "可能需要一些时间才能体现出效果。"
 failedToFetchAccountInformation: "获取账户信息失败"
 rateLimitExceeded: "已超过速率限制"
@@ -1048,7 +1073,7 @@ nonSensitiveOnlyForLocalLikeOnlyForRemote: "仅限非敏感内容(远程仅点
 rolesAssignedToMe: "指派给自己的角色"
 resetPasswordConfirm: "确定重置密码?"
 sensitiveWords: "敏感词"
-sensitiveWordsDescription: "将包含设置词的帖子的可见范围设置为首页。可以通过用换行符分隔来设置多个。"
+sensitiveWordsDescription: "包含这些词的帖子将只在首页可见。可用换行来设定多个词。"
 sensitiveWordsDescription2: "AND 条件用空格分隔,正则表达式用斜线包裹。"
 prohibitedWords: "禁用词"
 prohibitedWordsDescription: "发布包含设定词汇的帖子时将出错。可用换行设定多个关键字"
@@ -1065,6 +1090,7 @@ retryAllQueuesConfirmTitle: "要再尝试一次吗?"
 retryAllQueuesConfirmText: "可能会使服务器负荷在一定时间内增加"
 enableChartsForRemoteUser: "生成远程用户的图表"
 enableChartsForFederatedInstances: "生成远程服务器的图表"
+enableStatsForFederatedInstances: "获取远程服务器的信息"
 showClipButtonInNoteFooter: "在贴文下方显示便签按钮"
 reactionsDisplaySize: "回应显示大小"
 limitWidthOfReaction: "限制回应的最大宽度,并将其缩小显示"
@@ -1100,6 +1126,8 @@ preservedUsernames: "保留的用户名"
 preservedUsernamesDescription: "列出需要保留的用户名,使用换行来作为分割。被指定的用户名在建立账户时无法使用,但由管理员所创建的账户不受该限制。此外,现有的账户也不会受到影响。"
 createNoteFromTheFile: "从文件创建帖子"
 archive: "归档"
+archived: "已归档"
+unarchive: "取消归档"
 channelArchiveConfirmTitle: "要将 {name} 归档吗?"
 channelArchiveConfirmDescription: "归档后,在频道列表与搜索结果中不会显示,也无法发布新的贴文。"
 thisChannelArchived: "该频道已被归档。"
@@ -1110,6 +1138,9 @@ preventAiLearning: "拒绝接受生成式 AI 的学习"
 preventAiLearningDescription: "要求文章生成 AI 或图像生成 AI 不能够以发布的帖子和图像等内容作为学习对象。这是通过在 HTML 响应中包含 noai 标志来实现的,这不能完全阻止 AI 学习你的发布内容,并不是所有 AI 都会遵守这类请求。"
 options: "选项"
 specifyUser: "用户指定"
+lookupConfirm: "确定查询?"
+openTagPageConfirm: "确定打开话题标签页面?"
+specifyHost: "指定主机名"
 failedToPreviewUrl: "无法预览"
 update: "更新"
 rolesThatCanBeUsedThisEmojiAsReaction: "可以使用表情作为回应的角色"
@@ -1172,15 +1203,15 @@ followingOrFollower: "关注中或关注者"
 fileAttachedOnly: "仅限媒体"
 showRepliesToOthersInTimeline: "在时间线中包含给别人的回复"
 hideRepliesToOthersInTimeline: "在时间线中隐藏给别人的回复"
-showRepliesToOthersInTimelineAll: "在时间线中包含现在关注的所有人的回复"
-hideRepliesToOthersInTimelineAll: "在时间线中隐藏现在关注的所有人的回复"
-confirmShowRepliesAll: "此操作不可撤销。确认要在时间线中包含现在关注的所有人的回复吗?"
-confirmHideRepliesAll: "此操作不可撤销。确认要在时间线中隐藏现在关注的所有人的回复吗?"
+showRepliesToOthersInTimelineAll: "在时间线中显示所有现在关注的人的回复"
+hideRepliesToOthersInTimelineAll: "在时间线中隐藏所有现在关注的人的回复"
+confirmShowRepliesAll: "此操作不可撤销。确认要在时间线中显示所有现在关注的人的回复吗?"
+confirmHideRepliesAll: "此操作不可撤销。确认要在时间线中隐藏所有现在关注的人的回复吗?"
 externalServices: "外部服务"
 sourceCode: "源代码"
 sourceCodeIsNotYetProvided: "还未提供源代码。要解决此问题请联系管理员。"
 repositoryUrl: "仓库地址"
-repositoryUrlDescription: "若源代码所在的仓库是公开的,请填入对应的 URL。若是按原样使用 Misskey(并未追加或者修改代码)的情况请填入 https://github.com/misskey-dev/misskey。"
+repositoryUrlDescription: "若源代码所在的仓库是公开的,请填入对应的 URL。若并未追加或者修改 Misskey 的代码,请填入 https://github.com/misskey-dev/misskey。"
 repositoryUrlOrTarballRequired: "若仓库并未公开,则需要提供 tarball 作为替代。详情请看 .config/example.yml。"
 feedback: "反馈"
 feedbackUrl: "反馈地址"
@@ -1241,6 +1272,52 @@ keepOriginalFilenameDescription: "若关闭此设置,上传文件时文件名
 noDescription: "没有描述"
 alwaysConfirmFollow: "总是确认关注"
 inquiry: "联系我们"
+tryAgain: "请再试一次"
+confirmWhenRevealingSensitiveMedia: "显示敏感内容前需要确认"
+sensitiveMediaRevealConfirm: "这是敏感内容。是否显示?"
+createdLists: "已创建的列表"
+createdAntennas: "已创建的天线"
+fromX: "从 {x}"
+genEmbedCode: "生成嵌入代码"
+noteOfThisUser: "此用户的帖子"
+clipNoteLimitExceeded: "无法再往此便签内添加更多帖子"
+performance: "性能"
+modified: "有变更"
+discard: "取消"
+thereAreNChanges: "有 {n} 处更改"
+signinWithPasskey: "使用通行密钥登录"
+unknownWebAuthnKey: "此通行密钥未注册。"
+passkeyVerificationFailed: "验证通行密钥失败。"
+passkeyVerificationSucceededButPasswordlessLoginDisabled: "通行密钥验证成功,但账户未开启无密码登录。"
+messageToFollower: "给关注者的消息"
+target: "对象"
+testCaptchaWarning: "此功能为测试 CAPTCHA 用。<strong>请勿在正式环境中使用。</strong>"
+prohibitedWordsForNameOfUser: "用户名中禁止的词"
+prohibitedWordsForNameOfUserDescription: "更改用户名时,如果用户名中包含此列表里的词汇,用户的改名请求将被拒绝。持有管理员权限的用户不受此限制。"
+yourNameContainsProhibitedWords: "目标用户名包含违禁词"
+yourNameContainsProhibitedWordsDescription: "用户名内含有违禁词。若想使用此用户名,请联系服务器管理员。"
+thisContentsAreMarkedAsSigninRequiredByAuthor: "根据发帖者的设定,需要登录才能显示"
+lockdown: "锁定"
+pleaseSelectAccount: "请选择帐户"
+_accountSettings:
+  requireSigninToViewContents: "需要登录才能显示内容"
+  requireSigninToViewContentsDescription1: "您发布的所有帖子将变成需要登入后才会显示。有望防止爬虫收集各种信息。"
+  requireSigninToViewContentsDescription2: "没有 URL 预览(OGP)、内嵌网页、引用帖子的功能的服务器也将无法显示。"
+  requireSigninToViewContentsDescription3: "这些限制可能不适用于联合到远程服务器的内容。"
+  makeNotesFollowersOnlyBefore: "可将过去的帖子设为仅关注者可见"
+  makeNotesFollowersOnlyBeforeDescription: "开启此设定时,超过设定的时间或日期后,帖子将变为仅关注者可见。关闭后帖子的公开状态将恢复成原本的设定。"
+  makeNotesHiddenBefore: "将过去的帖子设为私密"
+  makeNotesHiddenBeforeDescription: "开启此设定时,超过设定的时间或日期后,帖子将变为仅自己可见。关闭后帖子的公开状态将恢复成原本的设定。"
+  mayNotEffectForFederatedNotes: "与远程服务器联合的帖子在远端可能会没有效果。"
+  notesHavePassedSpecifiedPeriod: "超过指定时间的帖子"
+  notesOlderThanSpecifiedDateAndTime: "指定日期前的帖子"
+_abuseUserReport:
+  forward: "转发"
+  forwardDescription: "目标是匿名系统账户,将把举报转发给远程服务器。"
+  resolve: "解决"
+  accept: "确认"
+  reject: "拒绝"
+  resolveTutorial: "如果举报内容有理且已解决,选择「确认」将案件以肯定的态度标记为已解决。\n如果举报内容站不住脚,选择「拒绝」将案件以否定的态度标记为已解决。"
 _delivery:
   status: "投递状态"
   stop: "停止投递"
@@ -1375,6 +1452,10 @@ _serverSettings:
   fanoutTimelineDescription: "当启用时,可显著提高获取各种时间线时的性能,并减轻数据库的负荷。但是相对的 Redis 的内存使用量将会增加。如果服务器的内存不是很大,又或者运行不稳定的话可以把它关掉。"
   fanoutTimelineDbFallback: "回退到数据库"
   fanoutTimelineDbFallbackDescription: "当启用时,若时间线未被缓存,则将额外查询数据库。禁用该功能可通过不执行回退处理进一步减少服务器负载,但会限制可检索的时间线范围。"
+  reactionsBufferingDescription: "开启时可显著提高发送回应时的性能,及减轻数据库负荷。但 Redis 的内存用量会相应增加。"
+  inquiryUrl: "联络地址"
+  inquiryUrlDescription: "用来指定诸如向服务运营商咨询的论坛地址,或记载了运营商联系方式之类的网页地址。"
+  thisSettingWillAutomaticallyOffWhenModeratorsInactive: "若在一段时间内没有检测到管理活动,为防止垃圾信息,此设定将自动关闭。"
 _accountMigration:
   moveFrom: "从别的账号迁移到此账户"
   moveFromSub: "为另一个账户建立别名"
@@ -1574,7 +1655,7 @@ _achievements:
     _postedAt0min0sec:
       title: "报时"
       description: "在 0 点发布一篇帖子"
-      flavor: "嘣 嘣 嘣 Biu——!"
+      flavor: "嘟 · 嘟 · 嘟 · 哔——"
     _selfQuote:
       title: "自我引用"
       description: "引用了自己的帖子"
@@ -1623,8 +1704,8 @@ _achievements:
       flavor: "今年也请对本服务器多多指教!"
     _cookieClicked:
       title: "点击饼干小游戏"
-      description: "点击了可疑的饼干"
-      flavor: "是不是软件有问题?"
+      description: "点击了饼干"
+      flavor: "用错软件了?"
     _brainDiver:
       title: "Brain Diver"
       description: "发布了包含 Brain Diver 链接的帖子"
@@ -1641,6 +1722,7 @@ _achievements:
     _bubbleGameDoubleExplodingHead:
       title: "两个🤯"
       description: "你合成出了2个游戏里最大的Emoji"
+      flavor: "大约能 装满 这些便当盒 🤯 🤯 (比划)"
 _role:
   new: "创建角色"
   edit: "编辑角色"
@@ -1670,8 +1752,8 @@ _role:
   descriptionOfIsExplorable: "打开后将公开角色时间线。如果角色不是公开的,就无法公开时间线。"
   displayOrder: "显示顺序"
   descriptionOfDisplayOrder: "数字越大,显示位置越靠前。"
-  canEditMembersByModerator: "允许监察者编辑成员"
-  descriptionOfCanEditMembersByModerator: "如果选中,监察者和管理员都能够为用户分配/取消分配角色。如果未选中,则只有管理员可以执行此操作。"
+  canEditMembersByModerator: "允许监察员编辑成员"
+  descriptionOfCanEditMembersByModerator: "如果选中,监察员和管理员都能够为用户分配/取消分配角色。如果未选中,则只有管理员可以执行此操作。"
   priority: "优先级"
   _priority:
     low: "低"
@@ -1690,6 +1772,7 @@ _role:
     canManageAvatarDecorations: "管理头像挂件"
     driveCapacity: "网盘容量"
     alwaysMarkNsfw: "总是将文件标记为 NSFW"
+    canUpdateBioMedia: "可以更新头像和横幅"
     pinMax: "帖子置顶数量限制"
     antennaMax: "可创建的最大天线数量"
     wordMuteMax: "屏蔽词的字数限制"
@@ -1704,6 +1787,11 @@ _role:
     canSearchNotes: "是否可以搜索帖子"
     canUseTranslator: "使用翻译功能"
     avatarDecorationLimit: "可添加头像挂件的最大个数"
+    canImportAntennas: "允许导入天线"
+    canImportBlocking: "允许导入拉黑列表"
+    canImportFollowing: "允许导入关注列表"
+    canImportMuting: "允许导入屏蔽列表"
+    canImportUserLists: "允许导入用户列表"
   _condition:
     roleAssignedTo: "已分配给手动角色"
     isLocal: "是本地用户"
@@ -1921,7 +2009,6 @@ _theme:
     buttonBg: "按钮背景"
     buttonHoverBg: "按钮背景(悬停)"
     inputBorder: "输入框边框"
-    listItemHoverBg: "下拉列表项目背景(悬停)"
     driveFolderBg: "网盘的文件夹背景"
     wallpaperOverlay: "壁纸叠加层"
     badge: "徽章"
@@ -1933,8 +2020,6 @@ _sfx:
   note: "帖子"
   noteMy: "我的帖子"
   notification: "通知"
-  antenna: "天线接收"
-  channel: "频道通知"
   reaction: "选择回应时"
 _soundSettings:
   driveFile: "使用网盘内的音频"
@@ -1943,6 +2028,7 @@ _soundSettings:
   driveFileTypeWarnDescription: "请选择音频文件"
   driveFileDurationWarn: "音频过长"
   driveFileDurationWarnDescription: "使用长音频可能会影响 Misskey 的使用。即使这样也要继续吗?"
+  driveFileError: "无法读取声音。请更改设置。"
 _ago:
   future: "未来"
   justNow: "最近"
@@ -1969,7 +2055,7 @@ _time:
   day: "日"
 _2fa:
   alreadyRegistered: "此设备已被注册"
-  registerTOTP: "开始设置认证应用"
+  registerTOTP: "开始设置验证器"
   step1: "首先,在您的设备上安装验证应用,例如 {a} 或 {b}。"
   step2: "然后,扫描屏幕上显示的二维码。"
   step2Uri: "如果使用桌面应用程序的话,请输入下面的 URI"
@@ -1978,23 +2064,23 @@ _2fa:
   setupCompleted: "设置完成"
   step4: "从现在开始,任何登录操作都将要求您提供动态口令。"
   securityKeyNotSupported: "您的浏览器不支持安全密钥。"
-  registerTOTPBeforeKey: "要注册安全密钥或 Passkey,请先设置验证器应用程序。"
+  registerTOTPBeforeKey: "要注册安全密钥或 Passkey,请先设置验证器。"
   securityKeyInfo: "注册兼容 WebAuthn 的密钥,例如支持 FIDO2 的硬件安全密钥、设备上的生物识别功能、PIN 码以及 Passkey 等。"
   registerSecurityKey: "注册安全密钥或 Passkey"
   securityKeyName: "输入密钥名称"
   tapSecurityKey: "请按照浏览器说明操作来注册安全密钥或 Passkey。"
   removeKey: "删除安全密钥"
   removeKeyConfirm: "您确定要删除 {name} 吗?"
-  whyTOTPOnlyRenew: "如果注册了安全密钥,则无法取消验证器应用程序上的设置。"
-  renewTOTP: "重置验证器应用程序"
-  renewTOTPConfirm: "当前验证器应用程序的验证码将不再有效"
+  whyTOTPOnlyRenew: "当注册了安全密钥时,无法取消使用验证器。"
+  renewTOTP: "重置验证器"
+  renewTOTPConfirm: "当前验证器的验证码及备用代码已失效"
   renewTOTPOk: "重新配置"
   renewTOTPCancel: "不用,谢谢"
   checkBackupCodesBeforeCloseThisWizard: "在关闭此窗口前,请确认下面的备用代码"
   backupCodes: "备用代码"
-  backupCodesDescription: "如果无法使用认证应用,可以使用以下的备用代码来访问账户。请务必将这些代码保存在安全的地方。每个代码仅可使用一次。"
-  backupCodeUsedWarning: "已使用备用代码。如果无法使用认证应用,请尽快重新设定。"
-  backupCodesExhaustedWarning: "已使用完所有的备用代码。如果无法使用认证应用,将无法再访问您的账户。请再次设定认证应用。"
+  backupCodesDescription: "如果无法使用验证器,可以使用以下的备用代码来访问账户。请务必将这些代码保存在安全的地方。每个代码仅可使用一次。"
+  backupCodeUsedWarning: "已使用备用代码。若验证器无法使用,请尽快重置验证器。"
+  backupCodesExhaustedWarning: "已使用完所有的备用代码。若验证器无法使用,则无法再访问您的账户。请重置验证器。"
   moreDetailedGuideHere: "此处为详细指南"
 _permissions:
   "read:account": "查看账户信息"
@@ -2089,8 +2175,11 @@ _auth:
   permissionAsk: "这个应用程序需要以下权限"
   pleaseGoBack: "请返回到应用程序"
   callback: "回到应用程序"
+  accepted: "已允许访问"
   denied: "拒绝访问"
+  scopeUser: "以下面的用户进行操作"
   pleaseLogin: "在对应用进行授权许可之前,请先登录"
+  byClickingYouWillBeRedirectedToThisUrl: "允许访问后将会自动重定向到以下 URL"
 _antennaSources:
   all: "所有帖子"
   homeTimeline: "已关注用户的帖子"
@@ -2199,6 +2288,9 @@ _profile:
   changeBanner: "修改横幅"
   verifiedLinkDescription: "如果将内容设置为 URL,当链接所指向的网页内包含自己的个人资料链接时,可以显示一个已验证图标。"
   avatarDecorationMax: "最多可添加 {max} 个挂件"
+  followedMessage: "被关注时显示的消息"
+  followedMessageDescription: "可以设置被关注时向对方显示的短消息。"
+  followedMessageDescriptionForLockedAccount: "需要批准才能关注的情况下,消息是在请求被批准后显示。"
 _exportOrImport:
   allNotes: "所有帖子"
   favoritedNotes: "收藏的帖子"
@@ -2291,6 +2383,7 @@ _pages:
   eyeCatchingImageSet: "设置封面图片"
   eyeCatchingImageRemove: "删除封面图片"
   chooseBlock: "添加块"
+  enterSectionTitle: "输入会话标题"
   selectType: "选择类型"
   contentBlocks: "内容"
   inputBlocks: "输入"
@@ -2336,6 +2429,8 @@ _notification:
   renotedBySomeUsers: "{n} 人转发了"
   followedBySomeUsers: "被 {n} 人关注"
   flushNotification: "重置通知历史"
+  exportOfXCompleted: "已完成 {x} 个导出"
+  login: "有新的登录"
   _types:
     all: "全部"
     note: "用户的新帖子"
@@ -2350,6 +2445,9 @@ _notification:
     followRequestAccepted: "关注请求已通过"
     roleAssigned: "授予的角色"
     achievementEarned: "取得的成就"
+    exportCompleted: "已完成导出"
+    login: "登录"
+    test: "测试通知"
     app: "关联应用的通知"
   _actions:
     followBack: "回关"
@@ -2398,9 +2496,10 @@ _drivecleaner:
   orderByCreatedAtAsc: "按添加日期降序排列"
 _webhookSettings:
   createWebhook: "创建 Webhook"
+  modifyWebhook: "编辑 webhook"
   name: "名称"
   secret: "密钥"
-  events: "何时运行 Webhook"
+  trigger: "触发器"
   active: "已启用"
   _events:
     follow: "关注时"
@@ -2410,6 +2509,29 @@ _webhookSettings:
     renote: "被转发时"
     reaction: "被回应时"
     mention: "被提及时"
+  _systemEvents:
+    abuseReport: "当收到举报时"
+    abuseReportResolved: "当举报被处理时"
+    userCreated: "当用户被创建时"
+    inactiveModeratorsWarning: "当管理员在一段时间内不活跃时"
+    inactiveModeratorsInvitationOnlyChanged: "当因为管理员在一段时间内不活跃,导致服务器变为邀请制时"
+  deleteConfirm: "要删除 webhook 吗?"
+  testRemarks: "点击开关右侧的按钮,可以发送使用假数据的测试 Webhook。"
+_abuseReport:
+  _notificationRecipient:
+    createRecipient: "新建举报通知"
+    modifyRecipient: "编辑举报通知"
+    recipientType: "通知类型"
+    _recipientType:
+      mail: "邮箱"
+      webhook: "Webhook"
+      _captions:
+        mail: "当收到新举报时,向持有监察员权限的用户发送通知邮件"
+        webhook: "当收到新举报及举报被处理时,使用指定的 SystemWebhook 发送通知"
+    keywords: "关键字"
+    notifiedUser: "通知的用户"
+    notifiedWebhook: "使用的 webhook"
+    deleteConfirm: "要删除通知吗?"
 _moderationLogTypes:
   createRole: "创建角色"
   deleteRole: "删除角色"
@@ -2438,6 +2560,8 @@ _moderationLogTypes:
   markSensitiveDriveFile: "标记网盘文件为敏感媒体"
   unmarkSensitiveDriveFile: "取消标记网盘文件为敏感媒体"
   resolveAbuseReport: "处理举报"
+  forwardAbuseReport: "转发举报"
+  updateAbuseReportNote: "更新举报用管理笔记"
   createInvitation: "生成邀请码"
   createAd: "创建了广告"
   deleteAd: "删除了广告"
@@ -2447,6 +2571,16 @@ _moderationLogTypes:
   deleteAvatarDecoration: "删除头像挂件"
   unsetUserAvatar: "清除用户头像"
   unsetUserBanner: "清除用户横幅"
+  createSystemWebhook: "新建了 SystemWebhook"
+  updateSystemWebhook: "更新了 SystemWebhook"
+  deleteSystemWebhook: "删除了 SystemWebhook"
+  createAbuseReportNotificationRecipient: "新建了举报通知"
+  updateAbuseReportNotificationRecipient: "更新了举报通知"
+  deleteAbuseReportNotificationRecipient: "删除了举报通知"
+  deleteAccount: "删除了账户"
+  deletePage: "删除了页面"
+  deleteFlash: "删除了 Play"
+  deleteGalleryPost: "删除了图库稿件"
 _fileViewer:
   title: "文件信息"
   type: "文件类型"
@@ -2578,3 +2712,28 @@ _mediaControls:
   pip: "画中画"
   playbackRate: "播放速度"
   loop: "循环播放"
+_contextMenu:
+  title: "上下文菜单"
+  app: "应用"
+  appWithShift: "Shift 键应用"
+  native: "浏览器的用户界面"
+_embedCodeGen:
+  title: "自定义嵌入代码"
+  header: "显示标题"
+  autoload: "连续加载(不推荐)"
+  maxHeight: "最大高度"
+  maxHeightDescription: "若将最大值设为 0 则不限制最大高度。为防止小工具无限增高,建议设置一下。"
+  maxHeightWarn: "最大高度限制已禁用(0)。若这不是您想要的效果,请将最大高度设一个值。"
+  previewIsNotActual: "由于超出了预览画面可显示的范围,因此显示内容会与实际嵌入时有所不同。"
+  rounded: "圆角"
+  border: "外边框"
+  applyToPreview: "应用预览"
+  generateCode: "生成嵌入代码"
+  codeGenerated: "已生成代码"
+  codeGeneratedDescription: "将生成的代码贴到网站上来使用。"
+_selfXssPrevention:
+  warning: "警告"
+  title: "「在此处粘贴什么东西」是欺诈行为。"
+  description1: "如果在此处粘贴了什么,恶意用户可能会接管账户或者盗取个人资料。"
+  description2: "如果不能完全理解将要粘贴的内容,%c 请立即停止操作并关闭这个窗口。"
+  description3: "详情请看这里。{link}"
diff --git a/locales/zh-TW.yml b/locales/zh-TW.yml
index aac3f7662c..16afeed0f8 100644
--- a/locales/zh-TW.yml
+++ b/locales/zh-TW.yml
@@ -8,6 +8,9 @@ search: "搜尋"
 notifications: "通知"
 username: "使用者名稱"
 password: "密碼"
+initialPasswordForSetup: "初始設定用的密碼"
+initialPasswordIsIncorrect: "初始設定用的密碼錯誤。"
+initialPasswordForSetupDescription: "如果您自己安裝了 Misskey,請使用您在設定檔中輸入的密碼。\n如果您使用 Misskey 的託管服務之類的服務,請使用提供的密碼。\n如果您尚未設定密碼,請將其留空並繼續。"
 forgotPassword: "忘記密碼"
 fetchingAsApObject: "從聯邦宇宙取得中..."
 ok: "OK"
@@ -60,6 +63,7 @@ copyFileId: "複製檔案 ID"
 copyFolderId: "複製資料夾ID"
 copyProfileUrl: "複製個人資料網址"
 searchUser: "搜尋使用者"
+searchThisUsersNotes: "搜尋這個使用者的貼文"
 reply: "回覆"
 loadMore: "載入更多"
 showMore: "載入更多"
@@ -154,6 +158,7 @@ editList: "編輯清單"
 selectChannel: "選擇頻道"
 selectAntenna: "選擇天線"
 editAntenna: "編輯天線"
+createAntenna: "建立天線"
 selectWidget: "選擇小工具"
 editWidgets: "編輯小工具"
 editWidgetsExit: "完成"
@@ -180,6 +185,10 @@ addAccount: "新增帳戶"
 reloadAccountsList: "更新帳戶清單的資訊"
 loginFailed: "登入失敗"
 showOnRemote: "轉到所在實例顯示"
+continueOnRemote: "在遠端伺服器繼續"
+chooseServerOnMisskeyHub: "從 Misskey Hub 選擇伺服器"
+specifyServerHost: "直接指定伺服器網域"
+inputHostName: "請輸入域名"
 general: "一般"
 wallpaper: "桌布"
 setWallpaper: "設定桌布"
@@ -190,6 +199,7 @@ followConfirm: "你真的要追隨{name}嗎?"
 proxyAccount: "代理帳戶"
 proxyAccountDescription: "代理帳戶是在特定條件下充當遠端追隨者的帳戶。例如,當使用者新增遠端使用者至其列表時,若沒有本地使用者追隨該遠端使用者,則其活動將不會傳送至伺服器,此時便會由代理帳戶代為追隨以解決問題。"
 host: "主機"
+selectSelf: "選擇自己"
 selectUser: "選取使用者"
 recipient: "收件人"
 annotation: "註解"
@@ -205,6 +215,7 @@ perDay: "每日"
 stopActivityDelivery: "停止發送活動"
 blockThisInstance: "封鎖此伺服器"
 silenceThisInstance: "禁言此伺服器"
+mediaSilenceThisInstance: "將這個伺服器的媒體設為禁言"
 operations: "操作"
 software: "軟體"
 version: "版本"
@@ -226,6 +237,10 @@ blockedInstances: "已封鎖的伺服器"
 blockedInstancesDescription: "請逐行輸入需要封鎖的伺服器。已封鎖的伺服器將無法與本伺服器進行通訊。"
 silencedInstances: "被禁言的伺服器"
 silencedInstancesDescription: "設定要禁言的伺服器主機名稱,以換行分隔。隸屬於禁言伺服器的所有帳戶都將被視為「禁言帳戶」,只能發出「追隨請求」,而且無法提及未追隨的本地帳戶。這不會影響已封鎖的實例。"
+mediaSilencedInstances: "媒體被禁言的伺服器"
+mediaSilencedInstancesDescription: "設定您想要對媒體設定禁言的伺服器,以換行符號區隔。來自被媒體禁言的伺服器所屬帳戶的所有檔案都會被視為敏感檔案,且自訂表情符號不能使用。被封鎖的伺服器不受影響。"
+federationAllowedHosts: "允許聯邦通訊的伺服器"
+federationAllowedHostsDescription: "設定允許聯邦通訊的伺服器主機,以換行符號分隔。"
 muteAndBlock: "靜音和封鎖"
 mutedUsers: "被靜音的使用者"
 blockedUsers: "被封鎖的使用者"
@@ -243,10 +258,10 @@ noCustomEmojis: "沒有自訂的表情符號"
 noJobs: "沒有任務"
 federating: "聯邦運作中"
 blocked: "已封鎖"
-suspended: "已凍結"
+suspended: "停止發送"
 all: "全部"
 subscribing: "訂閱中"
-publishing: "直播中"
+publishing: "發送中"
 notResponding: "沒有回應"
 instanceFollowing: "追隨的伺服器"
 instanceFollowers: "伺服器的追隨者"
@@ -324,6 +339,7 @@ renameFolder: "重新命名資料夾"
 deleteFolder: "刪除資料夾"
 folder: "資料夾"
 addFile: "加入附件"
+showFile: "瀏覽文件"
 emptyDrive: "雲端硬碟為空"
 emptyFolder: "資料夾為空"
 unableToDelete: "無法刪除"
@@ -343,7 +359,7 @@ reload: "重新整理"
 doNothing: "無視"
 reloadConfirm: "確定要重新整理嗎?"
 watch: "關注"
-unwatch: "取消追隨"
+unwatch: "取消關注"
 accept: "接受"
 reject: "拒絕"
 normal: "正常"
@@ -438,6 +454,7 @@ totpDescription: "以驗證應用程式輸入一次性密碼"
 moderator: "審查員"
 moderation: "審查"
 moderationNote: "管理筆記"
+moderationNoteDescription: "您可以編寫僅在審查員之間共用的註解。"
 addModerationNote: "新增管理筆記"
 moderationLogs: "管理日誌"
 nUsersMentioned: "被 {n} 個人提及"
@@ -477,6 +494,7 @@ noMessagesYet: "沒有訊息"
 newMessageExists: "有新的訊息"
 onlyOneFileCanBeAttached: "只能加入一個附件"
 signinRequired: "請先登入"
+signinOrContinueOnRemote: "若要繼續,需前往您所在的伺服器,或者註冊並登入此伺服器"
 invitations: "邀請"
 invitationCode: "邀請碼"
 checking: "確認中"
@@ -498,8 +516,11 @@ uiLanguage: "介面語言"
 aboutX: "關於{x}"
 emojiStyle: "表情符號的風格"
 native: "原生"
-disableDrawer: "不顯示下拉式選單"
-showNoteActionsOnlyHover: "僅在游標停留時顯示貼文的操作選項"
+menuStyle: "選單風格"
+style: "風格"
+drawer: "側邊欄"
+popup: "彈出式視窗"
+showNoteActionsOnlyHover: "僅在游標停留時顯示貼文的"
 showReactionsCount: "顯示貼文的反應數目"
 noHistory: "沒有歷史紀錄"
 signinHistory: "登入歷史"
@@ -581,6 +602,8 @@ ascendingOrder: "昇冪"
 descendingOrder: "降冪"
 scratchpad: "暫存記憶體"
 scratchpadDescription: "AiScript 控制臺為 AiScript 的實驗環境。您可以在此編寫、執行和確認程式碼與 Misskey 互動的結果。"
+uiInspector: "UI 檢查"
+uiInspectorDescription: "您可以看到記憶體中存在的 UI 元件實例的清單。  UI 元件由 Ui:C: 系列函數產生。"
 output: "輸出"
 script: "腳本"
 disablePagesScript: "停用頁面的 AiScript 腳本"
@@ -697,10 +720,7 @@ abuseReported: "檢舉完成。感謝您的報告。"
 reporter: "檢舉者"
 reporteeOrigin: "檢舉來源"
 reporterOrigin: "檢舉者來源"
-forwardReport: "將報告轉送給遠端伺服器"
-forwardReportIsAnonymous: "在遠端實例上看不到您的資訊,顯示的報告者是匿名的系统帳戶。"
 send: "發送"
-abuseMarkAsResolved: "處理完畢"
 openInNewTab: "在新分頁中開啟"
 openInSideView: "在側欄中開啟"
 defaultNavigationBehaviour: "預設導航"
@@ -837,6 +857,7 @@ administration: "管理"
 accounts: "帳戶"
 switch: "切換"
 noMaintainerInformationWarning: "尚未設定管理員訊息。"
+noInquiryUrlWarning: "尚未設定聯絡表單網址。"
 noBotProtectionWarning: "尚未設定 Bot 防護。"
 configure: "設定"
 postToGallery: "發佈到相簿"
@@ -901,6 +922,7 @@ followersVisibility: "追隨者的可見性"
 continueThread: "查看更多貼文"
 deleteAccountConfirm: "將要刪除帳戶。是否確定?"
 incorrectPassword: "密碼錯誤。"
+incorrectTotp: "一次性密碼錯誤,或者已過期。"
 voteConfirm: "確定投給「{choice}」?"
 hide: "隱藏"
 useDrawerReactionPickerForMobile: "在移動設備上使用抽屜顯示"
@@ -925,6 +947,9 @@ oneHour: "一小時"
 oneDay: "一天"
 oneWeek: "一週"
 oneMonth: "一個月"
+threeMonths: "3 個月"
+oneYear: "1 年"
+threeDays: "3 日"
 reflectMayTakeTime: "可能需要一些時間才會出現效果。"
 failedToFetchAccountInformation: "取得帳戶資訊失敗"
 rateLimitExceeded: "已超過速率限制"
@@ -997,7 +1022,7 @@ show: "檢視"
 neverShow: "不再顯示"
 remindMeLater: "以後再說"
 didYouLikeMisskey: "您喜歡 Misskey 嗎?"
-pleaseDonate: "Misskey 是由 {host} 使用的免費軟體。請贊助我們,讓開發得以持續!"
+pleaseDonate: "Misskey是由{host}使用的免費軟體。請贊助我們,讓開發的工作能夠持續!"
 correspondingSourceIsAvailable: "對應的原始碼可以在 {anchor} 處找到。"
 roles: "角色"
 role: "角色"
@@ -1065,6 +1090,7 @@ retryAllQueuesConfirmTitle: "要現在重試嗎?"
 retryAllQueuesConfirmText: "伺服器的負荷可能會暫時增加。"
 enableChartsForRemoteUser: "生成遠端使用者的圖表"
 enableChartsForFederatedInstances: "生成遠端伺服器的圖表"
+enableStatsForFederatedInstances: "取得遠端伺服器資訊"
 showClipButtonInNoteFooter: "新增摘錄按鈕至貼文"
 reactionsDisplaySize: "反應的顯示尺寸"
 limitWidthOfReaction: "限制反應的最大寬度,並縮小顯示尺寸。"
@@ -1100,6 +1126,8 @@ preservedUsernames: "保留的使用者名稱"
 preservedUsernamesDescription: "換行列舉要保留的使用者名稱。此處出現的名稱將在註冊時禁用,但由管理者建立帳戶則不受此限。此外,既有的帳戶也不受影響。"
 createNoteFromTheFile: "由此檔案建立貼文"
 archive: "封存"
+archived: "已封存"
+unarchive: "取消封存"
 channelArchiveConfirmTitle: "要封存{name}嗎?"
 channelArchiveConfirmDescription: "封存後,將不會在頻道列表與搜尋結果中顯示,也無法發佈新貼文。"
 thisChannelArchived: "這個頻道已被封存。"
@@ -1110,6 +1138,9 @@ preventAiLearning: "拒絕接受生成式AI的訓練"
 preventAiLearningDescription: "要求站外生成式 AI 不使用您發佈的內容訓練模型。此功能會使伺服器於 HTML 回應新增「noai」標籤,而因為要視乎 AI 會否遵守該標籤,所以此功能無法完全阻止所有 AI 使用您的內容。"
 options: "選項"
 specifyUser: "指定使用者"
+lookupConfirm: "要查詢嗎?"
+openTagPageConfirm: "要開啟標籤的頁面嗎?"
+specifyHost: "指定主機"
 failedToPreviewUrl: "無法預覽"
 update: "更新"
 rolesThatCanBeUsedThisEmojiAsReaction: "可以使用此表情符號為反應的角色"
@@ -1168,8 +1199,8 @@ showRenotes: "顯示其他人的轉發貼文"
 edited: "已編輯"
 notificationRecieveConfig: "接受通知的設定"
 mutualFollow: "互相追隨"
-followingOrFollower: "追隨中或追隨者"
-fileAttachedOnly: "顯示包含附件的貼文"
+followingOrFollower: "追隨中或者追隨者"
+fileAttachedOnly: "只顯示包含附件的貼文"
 showRepliesToOthersInTimeline: "顯示給其他人的回覆"
 hideRepliesToOthersInTimeline: "在時間軸上隱藏給其他人的回覆"
 showRepliesToOthersInTimelineAll: "在時間軸包含追隨中所有人的回覆"
@@ -1239,14 +1270,60 @@ useNativeUIForVideoAudioPlayer: "使用瀏覽器的 UI 播放影片與音訊"
 keepOriginalFilename: "保留原始檔名"
 keepOriginalFilenameDescription: "如果關閉此設置,上傳時檔案名稱會自動替換為隨機字串。"
 noDescription: "沒有說明文字"
-alwaysConfirmFollow: "點擊追隨時總是顯示確認訊息"
+alwaysConfirmFollow: "跟隨時總是確認"
 inquiry: "聯絡我們"
+tryAgain: "請再試一次。"
+confirmWhenRevealingSensitiveMedia: "要顯示敏感媒體時需確認"
+sensitiveMediaRevealConfirm: "這是敏感媒體。確定要顯示嗎?"
+createdLists: "已建立的清單"
+createdAntennas: "已建立的天線"
+fromX: "自 {x}"
+genEmbedCode: "產生嵌入程式碼"
+noteOfThisUser: "這個使用者的貼文列表"
+clipNoteLimitExceeded: "沒辦法在這個摘錄中增加更多貼文了。"
+performance: "性能"
+modified: "已變更"
+discard: "取消"
+thereAreNChanges: "有 {n} 處的變更"
+signinWithPasskey: "使用密碼金鑰登入"
+unknownWebAuthnKey: "未註冊的金鑰。"
+passkeyVerificationFailed: "驗證金鑰失敗。"
+passkeyVerificationSucceededButPasswordlessLoginDisabled: "雖然驗證金鑰成功,但是無密碼登入的方式是停用的。"
+messageToFollower: "給追隨者的訊息"
+target: "目標 "
+testCaptchaWarning: "此功能用於 CAPTCHA 的測試。<strong>請勿在正式環境中使用。</strong>"
+prohibitedWordsForNameOfUser: "禁止使用的字詞(使用者名稱)"
+prohibitedWordsForNameOfUserDescription: "如果使用者名稱包含此清單中的任何字串,則拒絕重新命名使用者。 具有審查員權限的使用者不受此限制的影響。"
+yourNameContainsProhibitedWords: "您嘗試更改的名稱包含禁止的字串"
+yourNameContainsProhibitedWordsDescription: "名稱中包含禁止使用的字串。 如果您想使用此名稱,請聯絡您的伺服器管理員。"
+thisContentsAreMarkedAsSigninRequiredByAuthor: "作者將其設定為需要登入才能顯示。"
+lockdown: "鎖定"
+pleaseSelectAccount: "請選擇帳戶"
+_accountSettings:
+  requireSigninToViewContents: "須登入以顯示內容"
+  requireSigninToViewContentsDescription1: "必須登入才會顯示您建立的貼文等內容。可望有效防止資訊被爬蟲蒐集。"
+  requireSigninToViewContentsDescription2: "來自不支援 URL 預覽 (OGP)、 網頁嵌入和引用貼文的伺服器,也將停止顯示。"
+  requireSigninToViewContentsDescription3: "這些限制可能不適用於被聯邦發送至遠端伺服器的內容。"
+  makeNotesFollowersOnlyBefore: "讓過去的貼文僅對追隨者顯示"
+  makeNotesFollowersOnlyBeforeDescription: "啟用此功能後,超過設定的日期和時間或超過設定時間的貼文將僅對追隨者顯示。 如果您再次停用它,貼文的公開狀態也會恢復原狀。"
+  makeNotesHiddenBefore: "隱藏過去的貼文"
+  makeNotesHiddenBeforeDescription: "啟用此功能後,超過設定的日期和時間或超過設定時間的貼文將僅對自己顯示(私密化)。 如果您再次停用它,貼文的公開狀態也會恢復原狀。"
+  mayNotEffectForFederatedNotes: "聯邦發送至遠端伺服器的貼文可能會不受影響。"
+  notesHavePassedSpecifiedPeriod: "早於指定時間的貼文"
+  notesOlderThanSpecifiedDateAndTime: "指定時間和日期之前的貼文"
+_abuseUserReport:
+  forward: "轉發"
+  forwardDescription: "以匿名系統帳戶將檢舉轉發至遠端伺服器。"
+  resolve: "解決"
+  accept: "接受"
+  reject: "拒絕"
+  resolveTutorial: "如果您已回覆正當的檢舉,請選擇「接受」以將案件標記為已解決。\n 如果檢舉的內容不正當,請選擇「拒絕」將案件標記為已解決。"
 _delivery:
   status: "傳送狀態"
-  stop: "停止傳送"
-  resume: "恢復傳送"
+  stop: "停止發送"
+  resume: "恢復發送"
   _type:
-    none: "直播中"
+    none: "發送中"
     manuallySuspended: "手動暫停中"
     goneSuspended: "因為伺服器刪除所以暫停中"
     autoSuspendedForNotResponding: "因為伺服器沒有回應所以暫停中"
@@ -1375,8 +1452,10 @@ _serverSettings:
   fanoutTimelineDescription: "如果啟用的話,檢索各個時間軸的性能會顯著提昇,資料庫的負荷也會減少。不過,Redis 的記憶體使用量會增加。如果伺服器的記憶體容量比較少或者運行不穩定,可以停用。"
   fanoutTimelineDbFallback: "資料庫的回退"
   fanoutTimelineDbFallbackDescription: "若啟用,在時間軸沒有快取的情況下將執行回退處理以額外查詢資料庫。若停用,可以透過不執行回退處理來進一步減少伺服器的負荷,但會限制可取得的時間軸範圍。"
+  reactionsBufferingDescription: "啟用時,可以顯著提高建立反應時的效能並減少資料庫的負載。 但是,Redis 記憶體使用量會增加。"
   inquiryUrl: "聯絡表單網址"
-  inquiryUrlDescription: "指定伺服器運營者的聯絡表單網址或包含運營者聯絡資訊網頁的網址。"
+  inquiryUrlDescription: "指定伺服器運營者的聯絡表單網址,或包含運營者聯絡資訊網頁的網址。"
+  thisSettingWillAutomaticallyOffWhenModeratorsInactive: "為了防止 spam,如果一段期間內沒有偵測到審查員的活動,此設定將自動關閉。"
 _accountMigration:
   moveFrom: "從其他帳戶遷移到這個帳戶"
   moveFromSub: "為另一個帳戶建立別名"
@@ -1390,7 +1469,7 @@ _accountMigration:
   startMigration: "遷移"
   migrationConfirm: "確定要將這個帳戶遷移至 {account} 嗎?一旦遷移就無法撤銷,也就無法以原來的狀態使用這個帳戶。\n另外,請確認在要遷移到的帳戶已經建立了一個別名。"
   movedAndCannotBeUndone: "帳戶已遷移。\n遷移無法撤消。"
-  postMigrationNote: "在完成遷移的 24 小時後解除此帳戶的追隨。此帳戶的追隨中、追隨者數量變為 0。由於不會解除追隨者,你的追隨者仍然可以繼續檢視這個帳戶發布給追隨者的貼文。"
+  postMigrationNote: "取消追蹤此帳戶將在遷移操作後 24 小時執行。\n 此帳戶有 0 個關注者/關注者。 您的關注者仍然可以看到此帳戶的關注者帖子,因為您不會被取消關注。"
   movedTo: "要遷移到的帳戶:"
 _achievements:
   earnedAt: "獲得日期"
@@ -1510,7 +1589,7 @@ _achievements:
     _markedAsCat:
       title: "我是貓"
       description: "已將帳戶設定為貓"
-      flavor: "還沒有名字。"
+      flavor: "沒有名字。"
     _following1:
       title: "首次追隨"
       description: "首次追隨了"
@@ -1524,7 +1603,7 @@ _achievements:
       title: "一百位朋友"
       description: "追隨超過100人了"
     _following300:
-      title: "朋友過多"
+      title: "朋友太多"
       description: "追隨超過300人了"
     _followers1:
       title: "第一個追隨者"
@@ -1693,6 +1772,7 @@ _role:
     canManageAvatarDecorations: "管理頭像裝飾"
     driveCapacity: "雲端硬碟容量"
     alwaysMarkNsfw: "總是將檔案標記為NSFW"
+    canUpdateBioMedia: "允許更新大頭貼和橫幅"
     pinMax: "置頂貼文的最大數量"
     antennaMax: "可建立的天線數量"
     wordMuteMax: "靜音文字的最大字數"
@@ -1707,6 +1787,11 @@ _role:
     canSearchNotes: "可否搜尋貼文"
     canUseTranslator: "使用翻譯功能"
     avatarDecorationLimit: "頭像裝飾的最大設置量"
+    canImportAntennas: "允許匯入天線"
+    canImportBlocking: "允許匯入封鎖名單"
+    canImportFollowing: "允許匯入跟隨名單"
+    canImportMuting: "允許匯入靜音名單"
+    canImportUserLists: "允許匯入清單"
   _condition:
     roleAssignedTo: "手動指派角色完成"
     isLocal: "本地使用者"
@@ -1844,7 +1929,7 @@ _channel:
   following: "追隨中"
   usersCount: "有 {n} 人參與"
   notesCount: "有 {n} 篇貼文"
-  nameAndDescription: "名稱與說明"
+  nameAndDescription: "名稱"
   nameOnly: "僅名稱"
   allowRenoteToExternal: "允許在頻道外轉發和引用"
 _menuDisplay:
@@ -1924,7 +2009,6 @@ _theme:
     buttonBg: "按鈕背景"
     buttonHoverBg: "按鈕背景 (漂浮)"
     inputBorder: "輸入框邊框"
-    listItemHoverBg: "列表物品背景 (漂浮)"
     driveFolderBg: "雲端硬碟文件夾背景"
     wallpaperOverlay: "壁紙覆蓋層"
     badge: "徽章"
@@ -1936,8 +2020,6 @@ _sfx:
   note: "貼文"
   noteMy: "我的貼文"
   notification: "通知"
-  antenna: "天線接收"
-  channel: "頻道通知"
   reaction: "選擇反應時"
 _soundSettings:
   driveFile: "使用雲端硬碟的音效檔案"
@@ -1946,6 +2028,7 @@ _soundSettings:
   driveFileTypeWarnDescription: "請選擇音效檔案"
   driveFileDurationWarn: "音效太長了"
   driveFileDurationWarnDescription: "使用長音效檔可能會影響 Misskey 的使用體驗。仍要使用此檔案嗎?"
+  driveFileError: "無法載入語音。請變更設定"
 _ago:
   future: "未來"
   justNow: "剛剛"
@@ -2092,8 +2175,11 @@ _auth:
   permissionAsk: "此應用程式需要以下權限"
   pleaseGoBack: "請返回至應用程式"
   callback: "回到應用程式"
+  accepted: "已授予存取權限"
   denied: "拒絕訪問"
+  scopeUser: "以下列使用者身分操作"
   pleaseLogin: "必須登入以提供應用程式的存取權限。"
+  byClickingYouWillBeRedirectedToThisUrl: "如果授予存取權限,就會自動導向到以下的網址"
 _antennaSources:
   all: "全部貼文"
   homeTimeline: "來自已追隨使用者的貼文"
@@ -2202,6 +2288,9 @@ _profile:
   changeBanner: "變更橫幅圖像"
   verifiedLinkDescription: "如果輸入包含您個人資料的網站 URL,欄位旁邊將出現驗證圖示。"
   avatarDecorationMax: "最多可以設置 {max} 個裝飾。"
+  followedMessage: "被追隨時的訊息"
+  followedMessageDescription: "可以設定被追隨時顯示給對方的訊息。"
+  followedMessageDescriptionForLockedAccount: "如果追隨是需要審核的話,在允許追隨請求之後顯示。"
 _exportOrImport:
   allNotes: "所有貼文"
   favoritedNotes: "「我的最愛」貼文"
@@ -2217,7 +2306,7 @@ _charts:
   federation: "聯邦宇宙"
   apRequest: "請求"
   usersIncDec: "使用者增減"
-  usersTotal: "使用者總數"
+  usersTotal: "使用者合計"
   activeUsers: "活躍使用者"
   notesIncDec: "貼文増減"
   localNotesIncDec: "本地貼文増減"
@@ -2294,6 +2383,7 @@ _pages:
   eyeCatchingImageSet: "設定封面影像"
   eyeCatchingImageRemove: "刪除封面影像"
   chooseBlock: "新增方塊"
+  enterSectionTitle: "輸入區段的標題"
   selectType: "選擇類型"
   contentBlocks: "內容"
   inputBlocks: "輸入"
@@ -2339,13 +2429,15 @@ _notification:
   renotedBySomeUsers: "{n}人做了轉發"
   followedBySomeUsers: "被{n}人追隨了"
   flushNotification: "重置通知歷史紀錄"
+  exportOfXCompleted: "{x} 的匯出已完成。"
+  login: "已登入"
   _types:
     all: "全部 "
     note: "使用者的最新貼文"
     follow: "追隨中"
     mention: "提及"
     reply: "回覆"
-    renote: "轉發貼文"
+    renote: "轉發"
     quote: "引用"
     reaction: "反應"
     pollEnded: "問卷調查結束"
@@ -2353,6 +2445,9 @@ _notification:
     followRequestAccepted: "追隨請求已接受"
     roleAssigned: "已授予角色"
     achievementEarned: "獲得成就"
+    exportCompleted: "已完成匯出。"
+    login: "登入"
+    test: "通知測試"
     app: "應用程式通知"
   _actions:
     followBack: "追隨回去"
@@ -2401,9 +2496,10 @@ _drivecleaner:
   orderByCreatedAtAsc: "按新增日期降序排列"
 _webhookSettings:
   createWebhook: "建立 Webhook"
+  modifyWebhook: "編輯 Webhook"
   name: "名字"
   secret: "密鑰"
-  events: "何時運行 Webhook"
+  trigger: "觸發器"
   active: "已啟用"
   _events:
     follow: "當你追隨時"
@@ -2413,6 +2509,29 @@ _webhookSettings:
     renote: "當被轉發時"
     reaction: "當獲得反應時"
     mention: "當被提到時"
+  _systemEvents:
+    abuseReport: "當使用者檢舉時"
+    abuseReportResolved: "當處理了使用者的檢舉時"
+    userCreated: "使用者被新增時"
+    inactiveModeratorsWarning: "當審查員在一段時間內沒有活動時"
+    inactiveModeratorsInvitationOnlyChanged: "當審查員在一段時間內不活動時,系統會將模式變更為邀請制"
+  deleteConfirm: "請問是否要刪除 Webhook?"
+  testRemarks: "按下切換開關右側的按鈕,就會將假資料發送至 Webhook。"
+_abuseReport:
+  _notificationRecipient:
+    createRecipient: "新增接收檢舉的通知對象"
+    modifyRecipient: "編輯接收檢舉的通知對象"
+    recipientType: "通知對象的種類"
+    _recipientType:
+      mail: "電子郵件"
+      webhook: "Webhook"
+      _captions:
+        mail: "寄送到擁有監察員權限的使用者電子郵件地址(僅在收到檢舉時)"
+        webhook: "向指定的 SystemWebhook 發送通知(在收到檢舉和解決檢舉時發送)"
+    keywords: "關鍵字"
+    notifiedUser: "通知的使用者"
+    notifiedWebhook: "使用的 Webhook"
+    deleteConfirm: "確定要刪除通知對象嗎?"
 _moderationLogTypes:
   createRole: "新增角色"
   deleteRole: "刪除角色 "
@@ -2441,6 +2560,8 @@ _moderationLogTypes:
   markSensitiveDriveFile: "標記為敏感檔案"
   unmarkSensitiveDriveFile: "撤銷標記為敏感檔案"
   resolveAbuseReport: "解決檢舉"
+  forwardAbuseReport: "轉發檢舉"
+  updateAbuseReportNote: "更新檢舉的審查備註"
   createInvitation: "建立邀請碼"
   createAd: "建立廣告"
   deleteAd: "刪除廣告"
@@ -2450,6 +2571,16 @@ _moderationLogTypes:
   deleteAvatarDecoration: "刪除頭像裝飾"
   unsetUserAvatar: "移除使用者的大頭貼"
   unsetUserBanner: "移除使用者的橫幅圖像"
+  createSystemWebhook: "建立 SystemWebhook"
+  updateSystemWebhook: "更新 SystemWebhook"
+  deleteSystemWebhook: "刪除 SystemWebhook"
+  createAbuseReportNotificationRecipient: "建立接收檢舉的通知對象"
+  updateAbuseReportNotificationRecipient: "更新接收檢舉的通知對象"
+  deleteAbuseReportNotificationRecipient: "刪除接收檢舉的通知對象"
+  deleteAccount: "刪除帳戶"
+  deletePage: "刪除頁面"
+  deleteFlash: "刪除 Play"
+  deleteGalleryPost: "刪除相簿的貼文"
 _fileViewer:
   title: "檔案詳細資訊"
   type: "檔案類型 "
@@ -2581,3 +2712,28 @@ _mediaControls:
   pip: "畫中畫"
   playbackRate: "播放速度"
   loop: "循環播放"
+_contextMenu:
+  title: "內容功能表"
+  app: "應用程式"
+  appWithShift: "Shift 鍵應用程式"
+  native: "瀏覽器的使用者介面"
+_embedCodeGen:
+  title: "自訂嵌入程式碼"
+  header: "檢視標頭 "
+  autoload: "自動繼續載入(不建議)"
+  maxHeight: "最大高度"
+  maxHeightDescription: "設定為 0 時代表沒有最大值。請指定某個值以避免小工具持續在縱向延伸。"
+  maxHeightWarn: "最大高度限制已停用(0)。如果這個變更不是您想要的,請將最大高度設定為某個值。"
+  previewIsNotActual: "由於超出了預覽畫面可顯示的範圍,因此顯示內容會與實際嵌入時有所不同。"
+  rounded: "圓角"
+  border: "給外框加上邊框"
+  applyToPreview: "反映在預覽中"
+  generateCode: "建立嵌入程式碼"
+  codeGenerated: "已產生程式碼"
+  codeGeneratedDescription: "請將產生的程式碼貼到您的網站上。"
+_selfXssPrevention:
+  warning: "警告"
+  title: "「在此畫面貼上一些內容」完全是個騙局。"
+  description1: "如果您在此處貼上任何內容,惡意使用者可能會接管您的帳戶或竊取您的個人資訊。"
+  description2: "如果您不確切知道要貼上的內容,%c 請立即停止工作並關閉此視窗。"
+  description3: "細節請看這裡。{link}"
diff --git a/misskey-assets b/misskey-assets
deleted file mode 160000
index 0179793ec8..0000000000
--- a/misskey-assets
+++ /dev/null
@@ -1 +0,0 @@
-Subproject commit 0179793ec891856d6f37a3be16ba4c22f67a81b5
diff --git a/package.json b/package.json
index b1786e16f1..55ae092967 100644
--- a/package.json
+++ b/package.json
@@ -1,14 +1,16 @@
 {
 	"name": "misskey",
-	"version": "2024.5.0",
+	"version": "2024.10.2-alpha.2",
 	"codename": "nasubi",
 	"repository": {
 		"type": "git",
 		"url": "https://github.com/misskey-dev/misskey.git"
 	},
-	"packageManager": "pnpm@9.0.6",
+	"packageManager": "pnpm@9.6.0",
 	"workspaces": [
+		"packages/frontend-shared",
 		"packages/frontend",
+		"packages/frontend-embed",
 		"packages/backend",
 		"packages/sw",
 		"packages/misskey-js",
@@ -21,7 +23,7 @@
 		"build-assets": "node ./scripts/build-assets.mjs",
 		"build": "pnpm build-pre && pnpm -r build && pnpm build-assets",
 		"build-storybook": "pnpm --filter frontend build-storybook",
-		"build-misskey-js-with-types": "pnpm build-pre && pnpm --filter backend... --filter=!misskey-js build && pnpm --filter backend generate-api-json && ncp packages/backend/built/api.json packages/misskey-js/generator/api.json && pnpm --filter misskey-js update-autogen-code && pnpm --filter misskey-js build && pnpm --filter misskey-js api",
+		"build-misskey-js-with-types": "pnpm build-pre && pnpm --filter backend... --filter=!misskey-js build && pnpm --filter backend generate-api-json --no-build && ncp packages/backend/built/api.json packages/misskey-js/generator/api.json && pnpm --filter misskey-js update-autogen-code && pnpm --filter misskey-js build && pnpm --filter misskey-js api",
 		"start": "pnpm check:connect && cd packages/backend && node ./built/boot/entry.js",
 		"start:test": "cd packages/backend && cross-env NODE_ENV=test node ./built/boot/entry.js",
 		"init": "pnpm migrate",
@@ -35,6 +37,7 @@
 		"cy:open": "pnpm cypress open --browser --e2e --config-file=cypress.config.ts",
 		"cy:run": "pnpm cypress run",
 		"e2e": "pnpm start-server-and-test start:test http://localhost:61812 cy:run",
+		"e2e-dev-container": "cp ./.config/cypress-devcontainer.yml ./.config/test.yml && pnpm start-server-and-test start:test http://localhost:61812 cy:run",
 		"jest": "cd packages/backend && pnpm jest",
 		"jest-and-coverage": "cd packages/backend && pnpm jest-and-coverage",
 		"test": "pnpm -r test",
@@ -51,24 +54,26 @@
 		"cssnano": "6.1.2",
 		"execa": "8.0.1",
 		"fast-glob": "3.3.2",
-		"ignore-walk": "6.0.4",
+		"ignore-walk": "6.0.5",
 		"js-yaml": "4.1.0",
-		"postcss": "8.4.38",
+		"postcss": "8.4.47",
 		"tar": "6.2.1",
-		"terser": "5.30.3",
-		"typescript": "5.4.5",
-		"esbuild": "0.20.2",
-		"glob": "10.3.12"
+		"terser": "5.33.0",
+		"typescript": "5.6.2",
+		"esbuild": "0.23.1",
+		"glob": "11.0.0"
 	},
 	"devDependencies": {
-		"@types/node": "20.12.7",
-		"@typescript-eslint/eslint-plugin": "7.7.1",
-		"@typescript-eslint/parser": "7.7.1",
+		"@misskey-dev/eslint-plugin": "2.0.3",
+		"@types/node": "20.14.12",
+		"@typescript-eslint/eslint-plugin": "7.17.0",
+		"@typescript-eslint/parser": "7.17.0",
 		"cross-env": "7.0.3",
-		"cypress": "13.7.3",
-		"eslint": "8.57.0",
+		"cypress": "13.14.2",
+		"eslint": "9.8.0",
+		"globals": "15.9.0",
 		"ncp": "2.0.0",
-		"start-server-and-test": "2.0.3"
+		"start-server-and-test": "2.0.8"
 	},
 	"optionalDependencies": {
 		"@tensorflow/tfjs-core": "4.4.0"
diff --git a/packages/backend/.eslintignore b/packages/backend/.eslintignore
deleted file mode 100644
index 790eb90145..0000000000
--- a/packages/backend/.eslintignore
+++ /dev/null
@@ -1,4 +0,0 @@
-node_modules
-/built
-/.eslintrc.js
-/@types/**/*
diff --git a/packages/backend/.eslintrc.cjs b/packages/backend/.eslintrc.cjs
deleted file mode 100644
index f9fe4814e6..0000000000
--- a/packages/backend/.eslintrc.cjs
+++ /dev/null
@@ -1,32 +0,0 @@
-module.exports = {
-	parserOptions: {
-		tsconfigRootDir: __dirname,
-		project: ['./tsconfig.json', './test/tsconfig.json'],
-	},
-	extends: [
-		'../shared/.eslintrc.js',
-	],
-	rules: {
-		'import/order': ['warn', {
-			'groups': ['builtin', 'external', 'internal', 'parent', 'sibling', 'index', 'object', 'type'],
-			'pathGroups': [
-				{
-					'pattern': '@/**',
-					'group': 'external',
-					'position': 'after'
-				}
-			],
-		}],
-		'no-restricted-globals': [
-			'error',
-			{
-				'name': '__dirname',
-				'message': 'Not in ESModule. Use `import.meta.url` instead.'
-			},
-			{
-				'name': '__filename',
-				'message': 'Not in ESModule. Use `import.meta.url` instead.'
-			}
-	]
-	},
-};
diff --git a/packages/backend/assets/api-doc.html b/packages/backend/assets/api-doc.html
new file mode 100644
index 0000000000..19e0349d47
--- /dev/null
+++ b/packages/backend/assets/api-doc.html
@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+<html>
+	<head>
+		<title>Misskey API</title>
+		<meta charset="utf-8">
+		<meta name="viewport" content="width=device-width, initial-scale=1">
+		<style>
+			body {
+				margin: 0;
+				padding: 0;
+			}
+		</style>
+	</head>
+	<body>
+		<script
+			id="api-reference"
+			data-url="/api.json"></script>
+		<script src="https://cdn.jsdelivr.net/npm/@scalar/api-reference"></script>
+	</body>
+</html>
diff --git a/packages/backend/assets/embed.js b/packages/backend/assets/embed.js
new file mode 100644
index 0000000000..24fccc1b6c
--- /dev/null
+++ b/packages/backend/assets/embed.js
@@ -0,0 +1,31 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: MIT
+ */
+//@ts-check
+(() => {
+	/** @type {NodeListOf<HTMLIFrameElement>} */
+	const els = document.querySelectorAll('iframe[data-misskey-embed-id]');
+
+	window.addEventListener('message', function (event) {
+		els.forEach((el) => {
+			if (event.source !== el.contentWindow) {
+				return;
+			}
+
+			const id = el.dataset.misskeyEmbedId;
+
+			if (event.data.type === 'misskey:embed:ready') {
+				el.contentWindow?.postMessage({
+					type: 'misskey:embedParent:registerIframeId',
+					payload: {
+						iframeId: id,
+					}
+				}, '*');
+			}
+			if (event.data.type === 'misskey:embed:changeHeight' && event.data.iframeId === id) {
+				el.style.height = event.data.payload.height + 'px';
+			}
+		});
+	});
+})();
diff --git a/packages/backend/assets/redoc.html b/packages/backend/assets/redoc.html
deleted file mode 100644
index 2557b4532e..0000000000
--- a/packages/backend/assets/redoc.html
+++ /dev/null
@@ -1,24 +0,0 @@
-<!DOCTYPE html>
-<html>
-	<head>
-		<title>Misskey API</title>
-		<!-- needed for adaptive design -->
-		<meta charset="utf-8"/>
-		<meta name="viewport" content="width=device-width, initial-scale=1">
-		<link href="https://fonts.googleapis.com/css?family=Montserrat:300,400,700|Roboto:300,400,700" rel="stylesheet">
-
-		<!--
-		ReDoc doesn't change outer page styles
-		-->
-		<style>
-			body {
-				margin: 0;
-				padding: 0;
-			}
-		</style>
-	</head>
-	<body>
-		<redoc spec-url="/api.json" expand-responses="200" expand-single-schema-field="true"></redoc>
-		<script src="https://cdn.redoc.ly/redoc/v2.1.3/bundles/redoc.standalone.js" integrity="sha256-u4DgqzYXoArvNF/Ymw3puKexfOC6lYfw0sfmeliBJ1I=" crossorigin="anonymous"></script>
-	</body>
-</html>
diff --git a/packages/backend/assets/tabler-badges/login-2.png b/packages/backend/assets/tabler-badges/login-2.png
new file mode 100644
index 0000000000..f3ca8de3dd
Binary files /dev/null and b/packages/backend/assets/tabler-badges/login-2.png differ
diff --git a/packages/backend/eslint.config.js b/packages/backend/eslint.config.js
new file mode 100644
index 0000000000..ae7b2baf49
--- /dev/null
+++ b/packages/backend/eslint.config.js
@@ -0,0 +1,46 @@
+import tsParser from '@typescript-eslint/parser';
+import sharedConfig from '../shared/eslint.config.js';
+
+export default [
+	...sharedConfig,
+	{
+		ignores: ['**/node_modules', 'built', '@types/**/*', 'migration'],
+	},
+	{
+		files: ['**/*.ts', '**/*.tsx'],
+		languageOptions: {
+			parserOptions: {
+				parser: tsParser,
+				project: ['./tsconfig.json', './test/tsconfig.json', './test-federation/tsconfig.json'],
+				sourceType: 'module',
+				tsconfigRootDir: import.meta.dirname,
+			},
+		},
+		rules: {
+			'import/order': ['warn', {
+				groups: [
+					'builtin',
+					'external',
+					'internal',
+					'parent',
+					'sibling',
+					'index',
+					'object',
+					'type',
+				],
+				pathGroups: [{
+					pattern: '@/**',
+					group: 'external',
+					position: 'after',
+				}],
+			}],
+			'no-restricted-globals': ['error', {
+				name: '__dirname',
+				message: 'Not in ESModule. Use `import.meta.url` instead.',
+			}, {
+				name: '__filename',
+				message: 'Not in ESModule. Use `import.meta.url` instead.',
+			}],
+		},
+	},
+];
diff --git a/packages/backend/jest.config.fed.cjs b/packages/backend/jest.config.fed.cjs
new file mode 100644
index 0000000000..fae187bc23
--- /dev/null
+++ b/packages/backend/jest.config.fed.cjs
@@ -0,0 +1,13 @@
+/*
+ * For a detailed explanation regarding each configuration property and type check, visit:
+ * https://jestjs.io/docs/en/configuration.html
+ */
+
+const base = require('./jest.config.cjs');
+
+module.exports = {
+	...base,
+	testMatch: [
+		'<rootDir>/test-federation/test/**/*.test.ts',
+	],
+};
diff --git a/packages/backend/migration/1713656541000-abuse-report-notification.js b/packages/backend/migration/1713656541000-abuse-report-notification.js
new file mode 100644
index 0000000000..4a754f81e2
--- /dev/null
+++ b/packages/backend/migration/1713656541000-abuse-report-notification.js
@@ -0,0 +1,62 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+export class AbuseReportNotification1713656541000 {
+	name = 'AbuseReportNotification1713656541000'
+
+	async up(queryRunner) {
+		await queryRunner.query(`
+			CREATE TABLE "system_webhook" (
+				"id" varchar(32) NOT NULL,
+				"isActive" boolean NOT NULL DEFAULT true,
+				"updatedAt" timestamp with time zone NOT NULL DEFAULT CURRENT_TIMESTAMP,
+				"latestSentAt" timestamp with time zone NULL DEFAULT NULL,
+				"latestStatus" integer NULL DEFAULT NULL,
+				"name" varchar(255) NOT NULL,
+				"on" varchar(128) [] NOT NULL DEFAULT '{}'::character varying[],
+				"url" varchar(1024) NOT NULL,
+				"secret" varchar(1024) NOT NULL,
+				CONSTRAINT "PK_system_webhook_id" PRIMARY KEY ("id")
+			);
+			CREATE INDEX "IDX_system_webhook_isActive" ON "system_webhook" ("isActive");
+			CREATE INDEX "IDX_system_webhook_on" ON "system_webhook" USING gin ("on");
+
+			CREATE TABLE "abuse_report_notification_recipient" (
+				"id" varchar(32) NOT NULL,
+				"isActive" boolean NOT NULL DEFAULT true,
+				"updatedAt" timestamp with time zone NOT NULL DEFAULT CURRENT_TIMESTAMP,
+				"name" varchar(255) NOT NULL,
+				"method" varchar(64) NOT NULL,
+				"userId" varchar(32) NULL DEFAULT NULL,
+				"systemWebhookId" varchar(32) NULL DEFAULT NULL,
+				CONSTRAINT "PK_abuse_report_notification_recipient_id" PRIMARY KEY ("id"),
+				CONSTRAINT "FK_abuse_report_notification_recipient_userId1" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION,
+				CONSTRAINT "FK_abuse_report_notification_recipient_userId2" FOREIGN KEY ("userId") REFERENCES "user_profile"("userId") ON DELETE CASCADE ON UPDATE NO ACTION,
+				CONSTRAINT "FK_abuse_report_notification_recipient_systemWebhookId" FOREIGN KEY ("systemWebhookId") REFERENCES "system_webhook"("id") ON DELETE CASCADE ON UPDATE NO ACTION
+			);
+			CREATE INDEX "IDX_abuse_report_notification_recipient_isActive" ON "abuse_report_notification_recipient" ("isActive");
+			CREATE INDEX "IDX_abuse_report_notification_recipient_method" ON "abuse_report_notification_recipient" ("method");
+			CREATE INDEX "IDX_abuse_report_notification_recipient_userId" ON "abuse_report_notification_recipient" ("userId");
+			CREATE INDEX "IDX_abuse_report_notification_recipient_systemWebhookId" ON "abuse_report_notification_recipient" ("systemWebhookId");
+		`);
+	}
+
+	async down(queryRunner) {
+		await queryRunner.query(`
+			ALTER TABLE "abuse_report_notification_recipient" DROP CONSTRAINT "FK_abuse_report_notification_recipient_userId1";
+			ALTER TABLE "abuse_report_notification_recipient" DROP CONSTRAINT "FK_abuse_report_notification_recipient_userId2";
+			ALTER TABLE "abuse_report_notification_recipient" DROP CONSTRAINT "FK_abuse_report_notification_recipient_systemWebhookId";
+			DROP INDEX "IDX_abuse_report_notification_recipient_isActive";
+			DROP INDEX "IDX_abuse_report_notification_recipient_method";
+			DROP INDEX "IDX_abuse_report_notification_recipient_userId";
+			DROP INDEX "IDX_abuse_report_notification_recipient_systemWebhookId";
+			DROP TABLE "abuse_report_notification_recipient";
+
+			DROP INDEX "IDX_system_webhook_isActive";
+			DROP INDEX "IDX_system_webhook_on";
+			DROP TABLE "system_webhook";
+		`);
+	}
+}
diff --git a/packages/backend/migration/1716197366117-MediaSilenceForHosts.js b/packages/backend/migration/1716197366117-MediaSilenceForHosts.js
new file mode 100644
index 0000000000..10bb7f0255
--- /dev/null
+++ b/packages/backend/migration/1716197366117-MediaSilenceForHosts.js
@@ -0,0 +1,16 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+export class MediaSilenceForHosts1716197366117 {
+    name = 'MediaSilenceForHosts1716197366117'
+
+    async up(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "meta" ADD "mediaSilencedHosts" character varying(1024) array NOT NULL DEFAULT '{}'`);
+    }
+
+    async down(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "mediaSilencedHosts"`);
+    }
+}
diff --git a/packages/backend/migration/1721666053703-fixDriveUrl.js b/packages/backend/migration/1721666053703-fixDriveUrl.js
new file mode 100644
index 0000000000..d8512fb835
--- /dev/null
+++ b/packages/backend/migration/1721666053703-fixDriveUrl.js
@@ -0,0 +1,24 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+export class FixDriveUrl1721666053703 {
+    name = 'FixDriveUrl1721666053703'
+
+    async up(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "drive_file" ALTER COLUMN "url" TYPE character varying(1024), ALTER COLUMN "url" SET NOT NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "drive_file"."url" IS 'The URL of the DriveFile.'`);
+        await queryRunner.query(`ALTER TABLE "drive_file" ALTER COLUMN "uri" TYPE character varying(1024)`);
+        await queryRunner.query(`COMMENT ON COLUMN "drive_file"."uri" IS 'The URI of the DriveFile. it will be null when the DriveFile is local.'`);
+        await queryRunner.query(`ALTER TABLE "drive_file" ALTER COLUMN "src" TYPE character varying(1024)`);
+    }
+
+    async down(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "drive_file" ALTER COLUMN "src" TYPE character varying(512)`);
+        await queryRunner.query(`COMMENT ON COLUMN "drive_file"."uri" IS 'The URI of the DriveFile. it will be null when the DriveFile is local.'`);
+        await queryRunner.query(`ALTER TABLE "drive_file" ALTER COLUMN "uri" TYPE character varying(512)`);
+        await queryRunner.query(`COMMENT ON COLUMN "drive_file"."url" IS 'The URL of the DriveFile.'`);
+        await queryRunner.query(`ALTER TABLE "drive_file" ALTER COLUMN "url" TYPE character varying(512), ALTER COLUMN "url" SET NOT NULL`);
+    }
+}
diff --git a/packages/backend/migration/1723944246767-followedMessage.js b/packages/backend/migration/1723944246767-followedMessage.js
new file mode 100644
index 0000000000..fc9ad1cb85
--- /dev/null
+++ b/packages/backend/migration/1723944246767-followedMessage.js
@@ -0,0 +1,16 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+export class FollowedMessage1723944246767 {
+	name = 'FollowedMessage1723944246767';
+
+	async up(queryRunner) {
+		await queryRunner.query('ALTER TABLE "user_profile" ADD "followedMessage" character varying(256)');
+	}
+
+	async down(queryRunner) {
+		await queryRunner.query('ALTER TABLE "user_profile" DROP COLUMN "followedMessage"');
+	}
+}
diff --git a/packages/backend/migration/1726804538569-reactions-buffering.js b/packages/backend/migration/1726804538569-reactions-buffering.js
new file mode 100644
index 0000000000..bc19e9cc8a
--- /dev/null
+++ b/packages/backend/migration/1726804538569-reactions-buffering.js
@@ -0,0 +1,16 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+export class ReactionsBuffering1726804538569 {
+    name = 'ReactionsBuffering1726804538569'
+
+    async up(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "meta" ADD "enableReactionsBuffering" boolean NOT NULL DEFAULT false`);
+    }
+
+    async down(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "enableReactionsBuffering"`);
+    }
+}
diff --git a/packages/backend/migration/1727318020265-enableStatsForFederatedInstances.js b/packages/backend/migration/1727318020265-enableStatsForFederatedInstances.js
new file mode 100644
index 0000000000..4ff520172b
--- /dev/null
+++ b/packages/backend/migration/1727318020265-enableStatsForFederatedInstances.js
@@ -0,0 +1,16 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+export class EnableStatsForFederatedInstances1727318020265 {
+    name = 'EnableStatsForFederatedInstances1727318020265'
+
+    async up(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "meta" ADD "enableStatsForFederatedInstances" boolean NOT NULL DEFAULT true`);
+    }
+
+    async down(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "enableStatsForFederatedInstances"`);
+    }
+}
diff --git a/packages/backend/migration/1727491883993-user-score.js b/packages/backend/migration/1727491883993-user-score.js
new file mode 100644
index 0000000000..7292d5363c
--- /dev/null
+++ b/packages/backend/migration/1727491883993-user-score.js
@@ -0,0 +1,16 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+export class UserScore1727491883993 {
+    name = 'UserScore1727491883993'
+
+    async up(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "user" ADD "score" integer NOT NULL DEFAULT '0'`);
+    }
+
+    async down(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "score"`);
+    }
+}
diff --git a/packages/backend/migration/1727512908322-meta-federation.js b/packages/backend/migration/1727512908322-meta-federation.js
new file mode 100644
index 0000000000..52c24df4f7
--- /dev/null
+++ b/packages/backend/migration/1727512908322-meta-federation.js
@@ -0,0 +1,18 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+export class MetaFederation1727512908322 {
+    name = 'MetaFederation1727512908322'
+
+    async up(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "meta" ADD "federation" character varying(128) NOT NULL DEFAULT 'all'`);
+        await queryRunner.query(`ALTER TABLE "meta" ADD "federationHosts" character varying(1024) array NOT NULL DEFAULT '{}'`);
+    }
+
+    async down(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "federationHosts"`);
+        await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "federation"`);
+    }
+}
diff --git a/packages/backend/migration/1728085812127-refine-abuse-user-report.js b/packages/backend/migration/1728085812127-refine-abuse-user-report.js
new file mode 100644
index 0000000000..57cbfdcf6d
--- /dev/null
+++ b/packages/backend/migration/1728085812127-refine-abuse-user-report.js
@@ -0,0 +1,18 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+export class RefineAbuseUserReport1728085812127 {
+    name = 'RefineAbuseUserReport1728085812127'
+
+    async up(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "abuse_user_report" ADD "moderationNote" character varying(8192) NOT NULL DEFAULT ''`);
+        await queryRunner.query(`ALTER TABLE "abuse_user_report" ADD "resolvedAs" character varying(128)`);
+    }
+
+    async down(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "abuse_user_report" DROP COLUMN "resolvedAs"`);
+        await queryRunner.query(`ALTER TABLE "abuse_user_report" DROP COLUMN "moderationNote"`);
+    }
+}
diff --git a/packages/backend/migration/1728550878802-testcaptcha.js b/packages/backend/migration/1728550878802-testcaptcha.js
new file mode 100644
index 0000000000..d8d987c0c1
--- /dev/null
+++ b/packages/backend/migration/1728550878802-testcaptcha.js
@@ -0,0 +1,16 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+export class Testcaptcha1728550878802 {
+    name = 'Testcaptcha1728550878802'
+
+    async up(queryRunner) {
+			await queryRunner.query(`ALTER TABLE "meta" ADD "enableTestcaptcha" boolean NOT NULL DEFAULT false`);
+    }
+
+    async down(queryRunner) {
+			await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "enableTestcaptcha"`);
+    }
+}
diff --git a/packages/backend/migration/1728634286056-prohibitedWordsForNameOfUser.js b/packages/backend/migration/1728634286056-prohibitedWordsForNameOfUser.js
new file mode 100644
index 0000000000..36e698d120
--- /dev/null
+++ b/packages/backend/migration/1728634286056-prohibitedWordsForNameOfUser.js
@@ -0,0 +1,14 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+export class ProhibitedWordsForNameOfUser1728634286056 {
+		async up(queryRunner) {
+			await queryRunner.query(`ALTER TABLE "meta" ADD "prohibitedWordsForNameOfUser" character varying(1024) array NOT NULL DEFAULT '{}'`);
+		}
+
+		async down(queryRunner) {
+			await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "prohibitedWordsForNameOfUser"`);
+		}
+}
diff --git a/packages/backend/migration/1729333924409-signinRequiredForShowContents.js b/packages/backend/migration/1729333924409-signinRequiredForShowContents.js
new file mode 100644
index 0000000000..5d4d1fcce2
--- /dev/null
+++ b/packages/backend/migration/1729333924409-signinRequiredForShowContents.js
@@ -0,0 +1,16 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+export class SigninRequiredForShowContents1729333924409 {
+    name = 'SigninRequiredForShowContents1729333924409'
+
+    async up(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "user" ADD "requireSigninToViewContents" boolean NOT NULL DEFAULT false`);
+    }
+
+    async down(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "requireSigninToViewContents"`);
+    }
+}
diff --git a/packages/backend/migration/1729486255072-makeNotesHiddenBefore.js b/packages/backend/migration/1729486255072-makeNotesHiddenBefore.js
new file mode 100644
index 0000000000..5fe4886b04
--- /dev/null
+++ b/packages/backend/migration/1729486255072-makeNotesHiddenBefore.js
@@ -0,0 +1,18 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+export class MakeNotesHiddenBefore1729486255072 {
+    name = 'MakeNotesHiddenBefore1729486255072'
+
+    async up(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "user" ADD "makeNotesFollowersOnlyBefore" integer`);
+        await queryRunner.query(`ALTER TABLE "user" ADD "makeNotesHiddenBefore" integer`);
+    }
+
+    async down(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "makeNotesHiddenBefore"`);
+        await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "makeNotesFollowersOnlyBefore"`);
+    }
+}
diff --git a/packages/backend/package.json b/packages/backend/package.json
index e034f75dc5..0dd738a1e6 100644
--- a/packages/backend/package.json
+++ b/packages/backend/package.json
@@ -4,7 +4,7 @@
 	"private": true,
 	"type": "module",
 	"engines": {
-		"node": "^20.10.0"
+		"node": "^20.10.0 || ^22.0.0"
 	},
 	"scripts": {
 		"start": "node ./built/boot/entry.js",
@@ -19,19 +19,21 @@
 		"watch": "node ./scripts/watch.mjs",
 		"restart": "pnpm build && pnpm start",
 		"dev": "node ./scripts/dev.mjs",
-		"typecheck": "tsc --noEmit && tsc -p test --noEmit",
-		"eslint": "eslint --quiet \"src/**/*.ts\"",
+		"typecheck": "tsc --noEmit && tsc -p test --noEmit && tsc -p test-federation --noEmit",
+		"eslint": "eslint --quiet \"{src,test-federation}/**/*.ts\"",
 		"lint": "pnpm typecheck && pnpm eslint",
 		"jest": "cross-env NODE_ENV=test node --experimental-vm-modules --experimental-import-meta-resolve node_modules/jest/bin/jest.js --forceExit --config jest.config.unit.cjs",
 		"jest:e2e": "cross-env NODE_ENV=test node --experimental-vm-modules --experimental-import-meta-resolve node_modules/jest/bin/jest.js --forceExit --config jest.config.e2e.cjs",
+		"jest:fed": "node --experimental-vm-modules --experimental-import-meta-resolve node_modules/jest/bin/jest.js --forceExit --config jest.config.fed.cjs",
 		"jest-and-coverage": "cross-env NODE_ENV=test node --experimental-vm-modules --experimental-import-meta-resolve node_modules/jest/bin/jest.js --coverage --forceExit --config jest.config.unit.cjs",
 		"jest-and-coverage:e2e": "cross-env NODE_ENV=test node --experimental-vm-modules --experimental-import-meta-resolve node_modules/jest/bin/jest.js --coverage --forceExit --config jest.config.e2e.cjs",
 		"jest-clear": "cross-env NODE_ENV=test node --experimental-vm-modules --experimental-import-meta-resolve node_modules/jest/bin/jest.js --clearCache",
 		"test": "pnpm jest",
 		"test:e2e": "pnpm build && pnpm build:test && pnpm jest:e2e",
+		"test:fed": "pnpm jest:fed",
 		"test-and-coverage": "pnpm jest-and-coverage",
 		"test-and-coverage:e2e": "pnpm build && pnpm build:test && pnpm jest-and-coverage:e2e",
-		"generate-api-json": "pnpm build && node ./scripts/generate_api_json.js"
+		"generate-api-json": "node ./scripts/generate_api_json.js"
 	},
 	"optionalDependencies": {
 		"@swc/core-android-arm64": "1.3.11",
@@ -65,43 +67,43 @@
 		"utf-8-validate": "6.0.3"
 	},
 	"dependencies": {
-		"@aws-sdk/client-s3": "3.412.0",
-		"@aws-sdk/lib-storage": "3.412.0",
-		"@bull-board/api": "5.17.0",
-		"@bull-board/fastify": "5.17.0",
-		"@bull-board/ui": "5.17.0",
-		"@discordapp/twemoji": "15.0.3",
-		"@fastify/accepts": "4.3.0",
-		"@fastify/cookie": "9.3.1",
-		"@fastify/cors": "9.0.1",
-		"@fastify/express": "3.0.0",
-		"@fastify/http-proxy": "9.5.0",
-		"@fastify/multipart": "8.2.0",
-		"@fastify/static": "7.0.3",
-		"@fastify/view": "9.1.0",
+		"@aws-sdk/client-s3": "3.620.0",
+		"@aws-sdk/lib-storage": "3.620.0",
+		"@bull-board/api": "6.0.0",
+		"@bull-board/fastify": "6.0.0",
+		"@bull-board/ui": "6.0.0",
+		"@discordapp/twemoji": "15.1.0",
+		"@fastify/accepts": "5.0.1",
+		"@fastify/cookie": "10.0.1",
+		"@fastify/cors": "10.0.1",
+		"@fastify/express": "4.0.1",
+		"@fastify/http-proxy": "10.0.0",
+		"@fastify/multipart": "9.0.1",
+		"@fastify/static": "8.0.1",
+		"@fastify/view": "10.0.1",
 		"@misskey-dev/sharp-read-bmp": "1.2.0",
 		"@misskey-dev/summaly": "5.1.0",
-		"@napi-rs/canvas": "^0.1.52",
-		"@nestjs/common": "10.3.8",
-		"@nestjs/core": "10.3.8",
-		"@nestjs/testing": "10.3.8",
+		"@napi-rs/canvas": "0.1.56",
+		"@nestjs/common": "10.4.4",
+		"@nestjs/core": "10.4.4",
+		"@nestjs/testing": "10.4.4",
 		"@peertube/http-signature": "1.7.0",
-		"@sentry/node": "^8.5.0",
-		"@sentry/profiling-node": "^8.5.0",
-		"@simplewebauthn/server": "10.0.0",
+		"@sentry/node": "8.20.0",
+		"@sentry/profiling-node": "8.20.0",
+		"@simplewebauthn/server": "10.0.1",
 		"@sinonjs/fake-timers": "11.2.2",
 		"@smithy/node-http-handler": "2.5.0",
 		"@swc/cli": "0.3.12",
-		"@swc/core": "1.4.17",
+		"@swc/core": "1.6.6",
 		"@twemoji/parser": "15.1.1",
 		"accepts": "1.3.8",
-		"ajv": "8.13.0",
+		"ajv": "8.17.1",
 		"archiver": "7.0.1",
 		"async-mutex": "0.5.0",
 		"bcryptjs": "2.4.3",
 		"blurhash": "2.0.5",
-		"body-parser": "1.20.2",
-		"bullmq": "5.7.8",
+		"body-parser": "1.20.3",
+		"bullmq": "5.15.0",
 		"cacheable-lookup": "7.0.0",
 		"cbor": "9.0.2",
 		"chalk": "5.3.0",
@@ -112,27 +114,28 @@
 		"content-disposition": "0.5.4",
 		"date-fns": "2.30.0",
 		"deep-email-validator": "0.1.21",
-		"fastify": "4.26.2",
-		"fastify-raw-body": "4.3.0",
+		"fastify": "5.0.0",
+		"fastify-raw-body": "5.0.0",
 		"feed": "4.2.2",
-		"file-type": "19.0.0",
-		"fluent-ffmpeg": "2.1.2",
+		"file-type": "19.5.0",
+		"fluent-ffmpeg": "2.1.3",
 		"form-data": "4.0.0",
-		"got": "14.2.1",
-		"happy-dom": "10.0.3",
+		"got": "14.4.2",
+		"happy-dom": "15.7.4",
 		"hpagent": "1.2.0",
 		"htmlescape": "1.1.1",
 		"http-link-header": "1.1.3",
 		"ioredis": "5.4.1",
-		"ip-cidr": "3.1.0",
+		"ip-cidr": "4.0.2",
 		"ipaddr.js": "2.2.0",
-		"is-svg": "5.0.0",
+		"is-svg": "5.1.0",
 		"js-yaml": "4.1.0",
-		"jsdom": "24.0.0",
+		"jsdom": "24.1.1",
 		"json5": "2.2.3",
 		"jsonld": "8.3.2",
 		"jsrsasign": "11.1.0",
-		"meilisearch": "0.38.0",
+		"meilisearch": "0.42.0",
+		"juice": "11.0.0",
 		"mfm-js": "0.24.0",
 		"microformats-parser": "2.0.2",
 		"mime-types": "2.1.35",
@@ -142,84 +145,82 @@
 		"nanoid": "5.0.7",
 		"nested-property": "4.0.0",
 		"node-fetch": "3.3.2",
-		"nodemailer": "6.9.13",
+		"nodemailer": "6.9.15",
 		"nsfwjs": "2.4.2",
 		"oauth": "0.10.0",
 		"oauth2orize": "1.12.0",
 		"oauth2orize-pkce": "0.1.2",
 		"os-utils": "0.0.14",
-		"otpauth": "9.2.3",
+		"otpauth": "9.3.4",
 		"parse5": "7.1.2",
-		"pg": "8.11.5",
+		"pg": "8.13.0",
 		"pkce-challenge": "4.1.0",
 		"probe-image-size": "7.2.3",
 		"promise-limit": "2.7.0",
-		"pug": "3.0.2",
+		"pug": "3.0.3",
 		"punycode": "2.3.1",
-		"qrcode": "1.5.3",
+		"qrcode": "1.5.4",
 		"random-seed": "0.3.0",
 		"ratelimiter": "3.4.1",
-		"re2": "1.20.10",
+		"re2": "1.21.4",
 		"redis-lock": "0.1.4",
 		"reflect-metadata": "0.2.2",
 		"rename": "1.0.4",
 		"rss-parser": "3.13.0",
 		"rxjs": "7.8.1",
-		"sanitize-html": "2.13.0",
+		"sanitize-html": "2.13.1",
 		"secure-json-parse": "2.7.0",
-		"sharp": "0.33.3",
+		"sharp": "0.33.5",
 		"slacc": "0.0.10",
 		"strict-event-emitter-types": "2.0.0",
 		"stringz": "2.1.0",
-		"systeminformation": "5.22.7",
+		"systeminformation": "5.23.5",
 		"tinycolor2": "1.6.0",
 		"tmp": "0.2.3",
-		"tsc-alias": "1.8.8",
+		"tsc-alias": "1.8.10",
 		"tsconfig-paths": "4.2.0",
 		"typeorm": "0.3.20",
-		"typescript": "5.4.5",
+		"typescript": "5.6.2",
 		"ulid": "2.3.0",
 		"vary": "1.1.2",
 		"web-push": "3.6.7",
-		"ws": "8.17.0",
+		"ws": "8.18.0",
 		"xev": "3.0.2"
 	},
 	"devDependencies": {
 		"@jest/globals": "29.7.0",
-		"@misskey-dev/eslint-plugin": "1.0.0",
-		"@nestjs/platform-express": "10.3.8",
+		"@nestjs/platform-express": "10.4.4",
 		"@simplewebauthn/types": "10.0.0",
 		"@swc/jest": "0.2.36",
 		"@types/accepts": "1.3.7",
 		"@types/archiver": "6.0.2",
 		"@types/bcryptjs": "2.4.6",
 		"@types/body-parser": "1.19.5",
-		"@types/color-convert": "2.0.3",
+		"@types/color-convert": "2.0.4",
 		"@types/content-disposition": "0.5.8",
-		"@types/fluent-ffmpeg": "2.1.24",
-		"@types/htmlescape": "^1.1.3",
-		"@types/http-link-header": "1.0.5",
-		"@types/jest": "29.5.12",
+		"@types/fluent-ffmpeg": "2.1.26",
+		"@types/htmlescape": "1.1.3",
+		"@types/http-link-header": "1.0.7",
+		"@types/jest": "29.5.13",
 		"@types/js-yaml": "4.0.9",
-		"@types/jsdom": "21.1.6",
-		"@types/jsonld": "1.5.13",
+		"@types/jsdom": "21.1.7",
+		"@types/jsonld": "1.5.15",
 		"@types/jsrsasign": "10.5.14",
 		"@types/mime-types": "2.1.4",
 		"@types/ms": "0.7.34",
-		"@types/node": "20.12.7",
-		"@types/node-fetch": "3.0.3",
-		"@types/nodemailer": "6.4.15",
-		"@types/oauth": "0.9.4",
+		"@types/node": "20.14.12",
+		"@types/nodemailer": "6.4.16",
+		"@types/oauth": "0.9.5",
 		"@types/oauth2orize": "1.11.5",
 		"@types/oauth2orize-pkce": "0.1.2",
-		"@types/pg": "8.11.5",
+		"@types/pg": "8.11.10",
 		"@types/pug": "2.0.10",
 		"@types/punycode": "2.1.4",
 		"@types/qrcode": "1.5.5",
 		"@types/random-seed": "0.3.5",
 		"@types/ratelimiter": "3.4.6",
 		"@types/rename": "1.0.7",
-		"@types/sanitize-html": "2.11.0",
+		"@types/sanitize-html": "2.13.0",
 		"@types/semver": "7.5.8",
 		"@types/simple-oauth2": "5.0.7",
 		"@types/sinonjs__fake-timers": "8.1.5",
@@ -227,19 +228,18 @@
 		"@types/tmp": "0.2.6",
 		"@types/vary": "1.1.3",
 		"@types/web-push": "3.6.3",
-		"@types/ws": "8.5.10",
-		"@typescript-eslint/eslint-plugin": "7.7.1",
-		"@typescript-eslint/parser": "7.7.1",
-		"aws-sdk-client-mock": "3.0.1",
+		"@types/ws": "8.5.12",
+		"@typescript-eslint/eslint-plugin": "7.17.0",
+		"@typescript-eslint/parser": "7.17.0",
+		"aws-sdk-client-mock": "4.0.1",
 		"cross-env": "7.0.3",
-		"eslint": "8.57.0",
-		"eslint-plugin-import": "2.29.1",
-		"execa": "8.0.1",
-		"fkill": "^9.0.0",
+		"eslint-plugin-import": "2.30.0",
+		"execa": "9.4.0",
+		"fkill": "9.0.0",
 		"jest": "29.7.0",
 		"jest-mock": "29.7.0",
-		"nodemon": "3.1.0",
+		"nodemon": "3.1.7",
 		"pid-port": "1.0.0",
-		"simple-oauth2": "5.0.0"
+		"simple-oauth2": "5.1.0"
 	}
 }
diff --git a/packages/backend/scripts/check_connect.js b/packages/backend/scripts/check_connect.js
index ba25fd416c..bb149444b5 100644
--- a/packages/backend/scripts/check_connect.js
+++ b/packages/backend/scripts/check_connect.js
@@ -5,11 +5,52 @@
 
 import Redis from 'ioredis';
 import { loadConfig } from '../built/config.js';
+import { createPostgresDataSource } from '../built/postgres.js';
 
 const config = loadConfig();
-const redis = new Redis(config.redis);
 
-redis.on('connect', () => redis.disconnect());
-redis.on('error', (e) => {
-	throw e;
-});
+async function connectToPostgres() {
+	const source = createPostgresDataSource(config);
+	await source.initialize();
+	await source.destroy();
+}
+
+async function connectToRedis(redisOptions) {
+	return await new Promise(async (resolve, reject) => {
+		const redis = new Redis({
+			...redisOptions,
+			lazyConnect: true,
+			reconnectOnError: false,
+			showFriendlyErrorStack: true,
+		});
+		redis.on('error', e => reject(e));
+
+		try {
+			await redis.connect();
+			resolve();
+
+		} catch (e) {
+			reject(e);
+
+		} finally {
+			redis.disconnect(false);
+		}
+	});
+}
+
+// If not all of these are defined, the default one gets reused.
+// so we use a Set to only try connecting once to each **uniq** redis.
+const promises = Array
+	.from(new Set([
+		config.redis,
+		config.redisForPubsub,
+		config.redisForJobQueue,
+		config.redisForTimelines,
+		config.redisForReactions,
+	]))
+	.map(connectToRedis)
+	.concat([
+		connectToPostgres()
+	]);
+
+await Promise.allSettled(promises);
diff --git a/packages/backend/scripts/dev.mjs b/packages/backend/scripts/dev.mjs
index 2d0de0f916..a3e0558abd 100644
--- a/packages/backend/scripts/dev.mjs
+++ b/packages/backend/scripts/dev.mjs
@@ -30,6 +30,7 @@ function execStart() {
 
 async function killProc() {
 	if (backendProcess) {
+		backendProcess.catch(() => {}); // backendProcess.kill()によって発生する例外を無視するためにcatch()を呼び出す
 		backendProcess.kill();
 		await new Promise(resolve => backendProcess.on('exit', resolve));
 		backendProcess = undefined;
@@ -46,6 +47,7 @@ async function killProc() {
 		],
 		{
 			stdio: [process.stdin, process.stdout, process.stderr, 'ipc'],
+			serialization: "json",
 		})
 		.on('message', async (message) => {
 			if (message.type === 'exit') {
diff --git a/packages/backend/scripts/generate_api_json.js b/packages/backend/scripts/generate_api_json.js
index b4769ef801..798e243004 100644
--- a/packages/backend/scripts/generate_api_json.js
+++ b/packages/backend/scripts/generate_api_json.js
@@ -3,11 +3,34 @@
  * SPDX-License-Identifier: AGPL-3.0-only
  */
 
-import { loadConfig } from '../built/config.js'
-import { genOpenapiSpec } from '../built/server/api/openapi/gen-spec.js'
-import { writeFileSync } from "node:fs";
+import { execa } from 'execa';
+import { writeFileSync, existsSync } from "node:fs";
 
-const config = loadConfig();
-const spec = genOpenapiSpec(config, true);
+async function main() {
+	if (!process.argv.includes('--no-build')) {
+		await execa('pnpm', ['run', 'build'], {
+			stdout: process.stdout,
+			stderr: process.stderr,
+		});
+	}
 
-writeFileSync('./built/api.json', JSON.stringify(spec), 'utf-8');
+	if (!existsSync('./built')) {
+		throw new Error('`built` directory does not exist.');
+	}
+
+	/** @type {import('../src/config.js')} */
+	const { loadConfig } = await import('../built/config.js');
+
+	/** @type {import('../src/server/api/openapi/gen-spec.js')} */
+	const { genOpenapiSpec } = await import('../built/server/api/openapi/gen-spec.js');
+
+	const config = loadConfig();
+	const spec = genOpenapiSpec(config, true);
+
+	writeFileSync('./built/api.json', JSON.stringify(spec), 'utf-8');
+}
+
+main().catch(e => {
+	console.error(e);
+	process.exit(1);
+});
diff --git a/packages/backend/src/GlobalModule.ts b/packages/backend/src/GlobalModule.ts
index 09971e8ca0..6ae8ccfbb3 100644
--- a/packages/backend/src/GlobalModule.ts
+++ b/packages/backend/src/GlobalModule.ts
@@ -13,6 +13,8 @@ import { createPostgresDataSource } from './postgres.js';
 import { RepositoryModule } from './models/RepositoryModule.js';
 import { allSettled } from './misc/promise-tracker.js';
 import type { Provider, OnApplicationShutdown } from '@nestjs/common';
+import { MiMeta } from '@/models/Meta.js';
+import { GlobalEvents } from './core/GlobalEventService.js';
 
 const $config: Provider = {
 	provide: DI.config,
@@ -78,11 +80,76 @@ const $redisForTimelines: Provider = {
 	inject: [DI.config],
 };
 
+const $redisForReactions: Provider = {
+	provide: DI.redisForReactions,
+	useFactory: (config: Config) => {
+		return new Redis.Redis(config.redisForReactions);
+	},
+	inject: [DI.config],
+};
+
+const $meta: Provider = {
+	provide: DI.meta,
+	useFactory: async (db: DataSource, redisForSub: Redis.Redis) => {
+		const meta = await db.transaction(async transactionalEntityManager => {
+			// 過去のバグでレコードが複数出来てしまっている可能性があるので新しいIDを優先する
+			const metas = await transactionalEntityManager.find(MiMeta, {
+				order: {
+					id: 'DESC',
+				},
+			});
+
+			const meta = metas[0];
+
+			if (meta) {
+				return meta;
+			} else {
+				// metaが空のときfetchMetaが同時に呼ばれるとここが同時に呼ばれてしまうことがあるのでフェイルセーフなupsertを使う
+				const saved = await transactionalEntityManager
+					.upsert(
+						MiMeta,
+						{
+							id: 'x',
+						},
+						['id'],
+					)
+					.then((x) => transactionalEntityManager.findOneByOrFail(MiMeta, x.identifiers[0]));
+
+				return saved;
+			}
+		});
+
+		async function onMessage(_: string, data: string): Promise<void> {
+			const obj = JSON.parse(data);
+
+			if (obj.channel === 'internal') {
+				const { type, body } = obj.message as GlobalEvents['internal']['payload'];
+				switch (type) {
+					case 'metaUpdated': {
+						for (const key in body.after) {
+							(meta as any)[key] = (body.after as any)[key];
+						}
+						meta.proxyAccount = null; // joinなカラムは通常取ってこないので
+						break;
+					}
+					default:
+						break;
+				}
+			}
+		}
+
+		redisForSub.on('message', onMessage);
+
+		return meta;
+	},
+	inject: [DI.db, DI.redisForSub],
+};
+
 @Global()
 @Module({
 	imports: [RepositoryModule],
-	providers: [$config, $db, $meilisearch, $redis, $redisForPub, $redisForSub, $redisForTimelines],
-	exports: [$config, $db, $meilisearch, $redis, $redisForPub, $redisForSub, $redisForTimelines, RepositoryModule],
+	providers: [$config, $db, $meta, $meilisearch, $redis, $redisForPub, $redisForSub, $redisForTimelines, $redisForReactions],
+	exports: [$config, $db, $meta, $meilisearch, $redis, $redisForPub, $redisForSub, $redisForTimelines, $redisForReactions, RepositoryModule],
 })
 export class GlobalModule implements OnApplicationShutdown {
 	constructor(
@@ -91,6 +158,7 @@ export class GlobalModule implements OnApplicationShutdown {
 		@Inject(DI.redisForPub) private redisForPub: Redis.Redis,
 		@Inject(DI.redisForSub) private redisForSub: Redis.Redis,
 		@Inject(DI.redisForTimelines) private redisForTimelines: Redis.Redis,
+		@Inject(DI.redisForReactions) private redisForReactions: Redis.Redis,
 	) { }
 
 	public async dispose(): Promise<void> {
@@ -103,6 +171,7 @@ export class GlobalModule implements OnApplicationShutdown {
 			this.redisForPub.disconnect(),
 			this.redisForSub.disconnect(),
 			this.redisForTimelines.disconnect(),
+			this.redisForReactions.disconnect(),
 		]);
 	}
 
diff --git a/packages/backend/src/NestLogger.ts b/packages/backend/src/NestLogger.ts
index 80f1f7a024..d0be19664f 100644
--- a/packages/backend/src/NestLogger.ts
+++ b/packages/backend/src/NestLogger.ts
@@ -7,7 +7,7 @@ import { LoggerService } from '@nestjs/common';
 import Logger from '@/logger.js';
 
 const logger = new Logger('core', 'cyan');
-const nestLogger = logger.createSubLogger('nest', 'green', false);
+const nestLogger = logger.createSubLogger('nest', 'green');
 
 export class NestLogger implements LoggerService {
 	/**
diff --git a/packages/backend/src/boot/entry.ts b/packages/backend/src/boot/entry.ts
index 04c6ca9723..25375c3015 100644
--- a/packages/backend/src/boot/entry.ts
+++ b/packages/backend/src/boot/entry.ts
@@ -25,7 +25,7 @@ Error.stackTraceLimit = Infinity;
 EventEmitter.defaultMaxListeners = 128;
 
 const logger = new Logger('core', 'cyan');
-const clusterLogger = logger.createSubLogger('cluster', 'orange', false);
+const clusterLogger = logger.createSubLogger('cluster', 'orange');
 const ev = new Xev();
 
 //#region Events
diff --git a/packages/backend/src/boot/master.ts b/packages/backend/src/boot/master.ts
index 75e1a80cd1..4bc5c799cf 100644
--- a/packages/backend/src/boot/master.ts
+++ b/packages/backend/src/boot/master.ts
@@ -25,7 +25,7 @@ const _dirname = dirname(_filename);
 const meta = JSON.parse(fs.readFileSync(`${_dirname}/../../../../built/meta.json`, 'utf-8'));
 
 const logger = new Logger('core', 'cyan');
-const bootLogger = logger.createSubLogger('boot', 'magenta', false);
+const bootLogger = logger.createSubLogger('boot', 'magenta');
 
 const themeColor = chalk.hex('#86b300');
 
diff --git a/packages/backend/src/boot/worker.ts b/packages/backend/src/boot/worker.ts
index d4a7cd56e5..5d4a15b29f 100644
--- a/packages/backend/src/boot/worker.ts
+++ b/packages/backend/src/boot/worker.ts
@@ -4,13 +4,36 @@
  */
 
 import cluster from 'node:cluster';
+import * as Sentry from '@sentry/node';
+import { nodeProfilingIntegration } from '@sentry/profiling-node';
 import { envOption } from '@/env.js';
+import { loadConfig } from '@/config.js';
 import { jobQueue, server } from './common.js';
 
 /**
  * Init worker process
  */
 export async function workerMain() {
+	const config = loadConfig();
+
+	if (config.sentryForBackend) {
+		Sentry.init({
+			integrations: [
+				...(config.sentryForBackend.enableNodeProfiling ? [nodeProfilingIntegration()] : []),
+			],
+
+			// Performance Monitoring
+			tracesSampleRate: 1.0, //  Capture 100% of the transactions
+
+			// Set sampling rate for profiling - this is relative to tracesSampleRate
+			profilesSampleRate: 1.0,
+
+			maxBreadcrumbs: 0,
+
+			...config.sentryForBackend.options,
+		});
+	}
+
 	if (envOption.onlyServer) {
 		await server();
 	} else if (envOption.onlyQueue) {
diff --git a/packages/backend/src/config.ts b/packages/backend/src/config.ts
index 0ac521d409..42f1033b9d 100644
--- a/packages/backend/src/config.ts
+++ b/packages/backend/src/config.ts
@@ -23,7 +23,7 @@ type RedisOptionsSource = Partial<RedisOptions> & {
  * 設定ファイルの型
  */
 type Source = {
-	url: string;
+	url?: string;
 	port?: number;
 	socket?: string;
 	chmodSocket?: string;
@@ -31,9 +31,9 @@ type Source = {
 	db: {
 		host: string;
 		port: number;
-		db: string;
-		user: string;
-		pass: string;
+		db?: string;
+		user?: string;
+		pass?: string;
 		disableCache?: boolean;
 		extra?: { [x: string]: string };
 	};
@@ -49,6 +49,7 @@ type Source = {
 	redisForPubsub?: RedisOptionsSource;
 	redisForJobQueue?: RedisOptionsSource;
 	redisForTimelines?: RedisOptionsSource;
+	redisForReactions?: RedisOptionsSource;
 	meilisearch?: {
 		host: string;
 		port: string;
@@ -62,6 +63,8 @@ type Source = {
 
 	publishTarballInsteadOfProvideRepositoryUrl?: boolean;
 
+	setupPassword?: string;
+
 	proxy?: string;
 	proxySmtp?: string;
 	proxyBypassHosts?: string[];
@@ -133,7 +136,7 @@ export type Config = {
 	proxySmtp: string | undefined;
 	proxyBypassHosts: string[] | undefined;
 	allowedPrivateNetworks: string[] | undefined;
-	maxFileSize: number | undefined;
+	maxFileSize: number;
 	clusterLimit: number | undefined;
 	id: string;
 	outgoingAddress: string | undefined;
@@ -151,6 +154,7 @@ export type Config = {
 
 	version: string;
 	publishTarballInsteadOfProvideRepositoryUrl: boolean;
+	setupPassword: string | undefined;
 	host: string;
 	hostname: string;
 	scheme: string;
@@ -160,8 +164,10 @@ export type Config = {
 	authUrl: string;
 	driveUrl: string;
 	userAgent: string;
-	clientEntry: string;
-	clientManifestExists: boolean;
+	frontendEntry: string;
+	frontendManifestExists: boolean;
+	frontendEmbedEntry: string;
+	frontendEmbedManifestExists: boolean;
 	mediaProxy: string;
 	externalMediaProxyEnabled: boolean;
 	videoThumbnailGenerator: string | null;
@@ -169,6 +175,7 @@ export type Config = {
 	redisForPubsub: RedisOptions & RedisOptionsSource;
 	redisForJobQueue: RedisOptions & RedisOptionsSource;
 	redisForTimelines: RedisOptions & RedisOptionsSource;
+	redisForReactions: RedisOptions & RedisOptionsSource;
 	sentryForBackend: { options: Partial<Sentry.NodeOptions>; enableNodeProfiling: boolean; } | undefined;
 	sentryForFrontend: { options: Partial<Sentry.NodeOptions> } | undefined;
 	perChannelMaxNoteCacheCount: number;
@@ -196,19 +203,29 @@ const path = process.env.MISSKEY_CONFIG_YML
 
 export function loadConfig(): Config {
 	const meta = JSON.parse(fs.readFileSync(`${_dirname}/../../../built/meta.json`, 'utf-8'));
-	const clientManifestExists = fs.existsSync(_dirname + '/../../../built/_vite_/manifest.json');
-	const clientManifest = clientManifestExists ?
-		JSON.parse(fs.readFileSync(`${_dirname}/../../../built/_vite_/manifest.json`, 'utf-8'))
+
+	const frontendManifestExists = fs.existsSync(_dirname + '/../../../built/_frontend_vite_/manifest.json');
+	const frontendEmbedManifestExists = fs.existsSync(_dirname + '/../../../built/_frontend_embed_vite_/manifest.json');
+	const frontendManifest = frontendManifestExists ?
+		JSON.parse(fs.readFileSync(`${_dirname}/../../../built/_frontend_vite_/manifest.json`, 'utf-8'))
 		: { 'src/_boot_.ts': { file: 'src/_boot_.ts' } };
+	const frontendEmbedManifest = frontendEmbedManifestExists ?
+		JSON.parse(fs.readFileSync(`${_dirname}/../../../built/_frontend_embed_vite_/manifest.json`, 'utf-8'))
+		: { 'src/boot.ts': { file: 'src/boot.ts' } };
+
 	const config = yaml.load(fs.readFileSync(path, 'utf-8')) as Source;
 
-	const url = tryCreateUrl(config.url);
+	const url = tryCreateUrl(config.url ?? process.env.MISSKEY_URL ?? '');
 	const version = meta.version;
 	const host = url.host;
 	const hostname = url.hostname;
 	const scheme = url.protocol.replace(/:$/, '');
 	const wsScheme = scheme.replace('http', 'ws');
 
+	const dbDb = config.db.db ?? process.env.DATABASE_DB ?? '';
+	const dbUser = config.db.user ?? process.env.DATABASE_USER ?? '';
+	const dbPass = config.db.pass ?? process.env.DATABASE_PASSWORD ?? '';
+
 	const externalMediaProxy = config.mediaProxy ?
 		config.mediaProxy.endsWith('/') ? config.mediaProxy.substring(0, config.mediaProxy.length - 1) : config.mediaProxy
 		: null;
@@ -218,6 +235,7 @@ export function loadConfig(): Config {
 	return {
 		version,
 		publishTarballInsteadOfProvideRepositoryUrl: !!config.publishTarballInsteadOfProvideRepositoryUrl,
+		setupPassword: config.setupPassword,
 		url: url.origin,
 		port: config.port ?? parseInt(process.env.PORT ?? '', 10),
 		socket: config.socket,
@@ -231,7 +249,7 @@ export function loadConfig(): Config {
 		apiUrl: `${scheme}://${host}/api`,
 		authUrl: `${scheme}://${host}/auth`,
 		driveUrl: `${scheme}://${host}/files`,
-		db: config.db,
+		db: { ...config.db, db: dbDb, user: dbUser, pass: dbPass },
 		dbReplications: config.dbReplications,
 		dbSlaves: config.dbSlaves,
 		meilisearch: config.meilisearch,
@@ -239,6 +257,7 @@ export function loadConfig(): Config {
 		redisForPubsub: config.redisForPubsub ? convertRedisOptions(config.redisForPubsub, host) : redis,
 		redisForJobQueue: config.redisForJobQueue ? convertRedisOptions(config.redisForJobQueue, host) : redis,
 		redisForTimelines: config.redisForTimelines ? convertRedisOptions(config.redisForTimelines, host) : redis,
+		redisForReactions: config.redisForReactions ? convertRedisOptions(config.redisForReactions, host) : redis,
 		sentryForBackend: config.sentryForBackend,
 		sentryForFrontend: config.sentryForFrontend,
 		id: config.id,
@@ -246,7 +265,7 @@ export function loadConfig(): Config {
 		proxySmtp: config.proxySmtp,
 		proxyBypassHosts: config.proxyBypassHosts,
 		allowedPrivateNetworks: config.allowedPrivateNetworks,
-		maxFileSize: config.maxFileSize,
+		maxFileSize: config.maxFileSize ?? 262144000,
 		clusterLimit: config.clusterLimit,
 		outgoingAddress: config.outgoingAddress,
 		outgoingAddressFamily: config.outgoingAddressFamily,
@@ -259,15 +278,17 @@ export function loadConfig(): Config {
 		deliverJobMaxAttempts: config.deliverJobMaxAttempts,
 		inboxJobMaxAttempts: config.inboxJobMaxAttempts,
 		proxyRemoteFiles: config.proxyRemoteFiles,
-		signToActivityPubGet: config.signToActivityPubGet,
+		signToActivityPubGet: config.signToActivityPubGet ?? true,
 		mediaProxy: externalMediaProxy ?? internalMediaProxy,
 		externalMediaProxyEnabled: externalMediaProxy !== null && externalMediaProxy !== internalMediaProxy,
 		videoThumbnailGenerator: config.videoThumbnailGenerator ?
 			config.videoThumbnailGenerator.endsWith('/') ? config.videoThumbnailGenerator.substring(0, config.videoThumbnailGenerator.length - 1) : config.videoThumbnailGenerator
 			: null,
 		userAgent: `Misskey/${version} (${config.url})`,
-		clientEntry: clientManifest['src/_boot_.ts'],
-		clientManifestExists: clientManifestExists,
+		frontendEntry: frontendManifest['src/_boot_.ts'],
+		frontendManifestExists: frontendManifestExists,
+		frontendEmbedEntry: frontendEmbedManifest['src/boot.ts'],
+		frontendEmbedManifestExists: frontendEmbedManifestExists,
 		perChannelMaxNoteCacheCount: config.perChannelMaxNoteCacheCount ?? 1000,
 		perUserNotificationsMaxCount: config.perUserNotificationsMaxCount ?? 500,
 		deactivateAntennaThreshold: config.deactivateAntennaThreshold ?? (1000 * 60 * 60 * 24 * 7),
diff --git a/packages/backend/src/const.ts b/packages/backend/src/const.ts
index a238f4973a..e3a61861f4 100644
--- a/packages/backend/src/const.ts
+++ b/packages/backend/src/const.ts
@@ -8,6 +8,8 @@ export const MAX_NOTE_TEXT_LENGTH = 3000;
 export const USER_ONLINE_THRESHOLD = 1000 * 60 * 10; // 10min
 export const USER_ACTIVE_THRESHOLD = 1000 * 60 * 60 * 24 * 3; // 3days
 
+export const PER_NOTE_REACTION_USER_PAIR_CACHE_MAX = 16;
+
 //#region hard limits
 // If you change DB_* values, you must also change the DB schema.
 
diff --git a/packages/backend/src/core/AbuseReportNotificationService.ts b/packages/backend/src/core/AbuseReportNotificationService.ts
new file mode 100644
index 0000000000..25e265f2b1
--- /dev/null
+++ b/packages/backend/src/core/AbuseReportNotificationService.ts
@@ -0,0 +1,432 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Inject, Injectable, type OnApplicationShutdown } from '@nestjs/common';
+import { Brackets, In, IsNull, Not } from 'typeorm';
+import * as Redis from 'ioredis';
+import sanitizeHtml from 'sanitize-html';
+import { DI } from '@/di-symbols.js';
+import { bindThis } from '@/decorators.js';
+import { GlobalEvents, GlobalEventService } from '@/core/GlobalEventService.js';
+import type {
+	AbuseReportNotificationRecipientRepository,
+	MiAbuseReportNotificationRecipient,
+	MiAbuseUserReport,
+	MiMeta,
+	MiUser,
+} from '@/models/_.js';
+import { EmailService } from '@/core/EmailService.js';
+import { RoleService } from '@/core/RoleService.js';
+import { RecipientMethod } from '@/models/AbuseReportNotificationRecipient.js';
+import { ModerationLogService } from '@/core/ModerationLogService.js';
+import { SystemWebhookService } from '@/core/SystemWebhookService.js';
+import { UserEntityService } from '@/core/entities/UserEntityService.js';
+import { IdService } from './IdService.js';
+
+@Injectable()
+export class AbuseReportNotificationService implements OnApplicationShutdown {
+	constructor(
+		@Inject(DI.meta)
+		private meta: MiMeta,
+
+		@Inject(DI.abuseReportNotificationRecipientRepository)
+		private abuseReportNotificationRecipientRepository: AbuseReportNotificationRecipientRepository,
+
+		@Inject(DI.redisForSub)
+		private redisForSub: Redis.Redis,
+
+		private idService: IdService,
+		private roleService: RoleService,
+		private systemWebhookService: SystemWebhookService,
+		private emailService: EmailService,
+		private moderationLogService: ModerationLogService,
+		private globalEventService: GlobalEventService,
+		private userEntityService: UserEntityService,
+	) {
+		this.redisForSub.on('message', this.onMessage);
+	}
+
+	/**
+	 * 管理者用Redisイベントを用いて{@link abuseReports}の内容を管理者各位に通知する.
+	 * 通知先ユーザは{@link getModeratorIds}の取得結果に依る.
+	 *
+	 * @see RoleService.getModeratorIds
+	 * @see GlobalEventService.publishAdminStream
+	 */
+	@bindThis
+	public async notifyAdminStream(abuseReports: MiAbuseUserReport[]) {
+		if (abuseReports.length <= 0) {
+			return;
+		}
+
+		const moderatorIds = await this.roleService.getModeratorIds({
+			includeAdmins: true,
+			excludeExpire: true,
+		});
+
+		for (const moderatorId of moderatorIds) {
+			for (const abuseReport of abuseReports) {
+				this.globalEventService.publishAdminStream(
+					moderatorId,
+					'newAbuseUserReport',
+					{
+						id: abuseReport.id,
+						targetUserId: abuseReport.targetUserId,
+						reporterId: abuseReport.reporterId,
+						comment: abuseReport.comment,
+					},
+				);
+			}
+		}
+	}
+
+	/**
+	 * Mailを用いて{@link abuseReports}の内容を管理者各位に通知する.
+	 * メールアドレスの送信先は以下の通り.
+	 * - モデレータ権限所有者ユーザ(設定画面からメールアドレスの設定を行っているユーザに限る)
+	 * - metaテーブルに設定されているメールアドレス
+	 *
+	 * @see EmailService.sendEmail
+	 */
+	@bindThis
+	public async notifyMail(abuseReports: MiAbuseUserReport[]) {
+		if (abuseReports.length <= 0) {
+			return;
+		}
+
+		const recipientEMailAddresses = await this.fetchEMailRecipients().then(it => it
+			.filter(it => it.isActive && it.userProfile?.emailVerified)
+			.map(it => it.userProfile?.email)
+			.filter(x => x != null),
+		);
+
+		recipientEMailAddresses.push(
+			...(this.meta.email ? [this.meta.email] : []),
+		);
+
+		if (recipientEMailAddresses.length <= 0) {
+			return;
+		}
+
+		for (const mailAddress of recipientEMailAddresses) {
+			await Promise.all(
+				abuseReports.map(it => {
+					// TODO: 送信処理はJobQueue化したい
+					return this.emailService.sendEmail(
+						mailAddress,
+						'New Abuse Report',
+						sanitizeHtml(it.comment),
+						sanitizeHtml(it.comment),
+					);
+				}),
+			);
+		}
+	}
+
+	/**
+	 * SystemWebhookを用いて{@link abuseReports}の内容を管理者各位に通知する.
+	 * ここではJobQueueへのエンキューのみを行うため、即時実行されない.
+	 *
+	 * @see SystemWebhookService.enqueueSystemWebhook
+	 */
+	@bindThis
+	public async notifySystemWebhook(
+		abuseReports: MiAbuseUserReport[],
+		type: 'abuseReport' | 'abuseReportResolved',
+	) {
+		if (abuseReports.length <= 0) {
+			return;
+		}
+
+		const usersMap = await this.userEntityService.packMany(
+			[
+				...new Set([
+					...abuseReports.map(it => it.reporter ?? it.reporterId),
+					...abuseReports.map(it => it.targetUser ?? it.targetUserId),
+					...abuseReports.map(it => it.assignee ?? it.assigneeId),
+				].filter(x => x != null)),
+			],
+			null,
+			{ schema: 'UserLite' },
+		).then(it => new Map(it.map(it => [it.id, it])));
+		const convertedReports = abuseReports.map(it => {
+			return {
+				...it,
+				reporter: usersMap.get(it.reporterId),
+				targetUser: usersMap.get(it.targetUserId),
+				assignee: it.assigneeId ? usersMap.get(it.assigneeId) : null,
+			};
+		});
+
+		const recipientWebhookIds = await this.fetchWebhookRecipients()
+			.then(it => it
+				.filter(it => it.isActive && it.systemWebhookId && it.method === 'webhook')
+				.map(it => it.systemWebhookId)
+				.filter(x => x != null));
+		for (const webhookId of recipientWebhookIds) {
+			await Promise.all(
+				convertedReports.map(it => {
+					return this.systemWebhookService.enqueueSystemWebhook(
+						webhookId,
+						type,
+						it,
+					);
+				}),
+			);
+		}
+	}
+
+	/**
+	 * 通報の通知先一覧を取得する.
+	 *
+	 * @param {Object} [params] クエリの取得条件
+	 * @param {Object} [params.method] 取得する通知先の通知方法
+	 * @param {Object} [opts] 動作時の詳細なオプション
+	 * @param {boolean} [opts.removeUnauthorized] 副作用としてモデレータ権限を持たない送信先ユーザをDBから削除するかどうか(default: true)
+	 * @param {boolean} [opts.joinUser] 通知先のユーザ情報をJOINするかどうか(default: false)
+	 * @param {boolean} [opts.joinSystemWebhook] 通知先のSystemWebhook情報をJOINするかどうか(default: false)
+	 * @see removeUnauthorizedRecipientUsers
+	 */
+	@bindThis
+	public async fetchRecipients(
+		params?: {
+			ids?: MiAbuseReportNotificationRecipient['id'][],
+			method?: RecipientMethod[],
+		},
+		opts?: {
+			removeUnauthorized?: boolean,
+			joinUser?: boolean,
+			joinSystemWebhook?: boolean,
+		},
+	): Promise<MiAbuseReportNotificationRecipient[]> {
+		const query = this.abuseReportNotificationRecipientRepository.createQueryBuilder('recipient');
+
+		if (opts?.joinUser) {
+			query.innerJoinAndSelect('user', 'user', 'recipient.userId = user.id');
+			query.innerJoinAndSelect('recipient.userProfile', 'userProfile');
+		}
+
+		if (opts?.joinSystemWebhook) {
+			query.innerJoinAndSelect('recipient.systemWebhook', 'systemWebhook');
+		}
+
+		if (params?.ids) {
+			query.andWhere({ id: In(params.ids) });
+		}
+
+		if (params?.method) {
+			query.andWhere(new Brackets(qb => {
+				if (params.method?.includes('email')) {
+					qb.orWhere({ method: 'email', userId: Not(IsNull()) });
+				}
+				if (params.method?.includes('webhook')) {
+					qb.orWhere({ method: 'webhook', userId: IsNull() });
+				}
+			}));
+		}
+
+		const recipients = await query.getMany();
+		if (recipients.length <= 0) {
+			return [];
+		}
+
+		// アサイン有効期限切れはイベントで拾えないので、このタイミングでチェック及び削除(オプション)
+		return (opts?.removeUnauthorized ?? true)
+			? await this.removeUnauthorizedRecipientUsers(recipients)
+			: recipients;
+	}
+
+	/**
+	 * EMailの通知先一覧を取得する.
+	 * リレーション先の{@link MiUser}および{@link MiUserProfile}も同時に取得する.
+	 *
+	 * @param {Object} [opts]
+	 * @param {boolean} [opts.removeUnauthorized] 副作用としてモデレータ権限を持たない送信先ユーザをDBから削除するかどうか(default: true)
+	 * @see removeUnauthorizedRecipientUsers
+	 */
+	@bindThis
+	public async fetchEMailRecipients(opts?: {
+		removeUnauthorized?: boolean
+	}): Promise<MiAbuseReportNotificationRecipient[]> {
+		return this.fetchRecipients({ method: ['email'] }, { joinUser: true, ...opts });
+	}
+
+	/**
+	 * Webhookの通知先一覧を取得する.
+	 * リレーション先の{@link MiSystemWebhook}も同時に取得する.
+	 */
+	@bindThis
+	public fetchWebhookRecipients(): Promise<MiAbuseReportNotificationRecipient[]> {
+		return this.fetchRecipients({ method: ['webhook'] }, { joinSystemWebhook: true });
+	}
+
+	/**
+	 * 通知先を作成する.
+	 */
+	@bindThis
+	public async createRecipient(
+		params: {
+			isActive: MiAbuseReportNotificationRecipient['isActive'];
+			name: MiAbuseReportNotificationRecipient['name'];
+			method: MiAbuseReportNotificationRecipient['method'];
+			userId: MiAbuseReportNotificationRecipient['userId'];
+			systemWebhookId: MiAbuseReportNotificationRecipient['systemWebhookId'];
+		},
+		updater: MiUser,
+	): Promise<MiAbuseReportNotificationRecipient> {
+		const id = this.idService.gen();
+		await this.abuseReportNotificationRecipientRepository.insert({
+			...params,
+			id,
+		});
+
+		const created = await this.abuseReportNotificationRecipientRepository.findOneByOrFail({ id: id });
+
+		this.moderationLogService
+			.log(updater, 'createAbuseReportNotificationRecipient', {
+				recipientId: id,
+				recipient: created,
+			});
+
+		return created;
+	}
+
+	/**
+	 * 通知先を更新する.
+	 */
+	@bindThis
+	public async updateRecipient(
+		params: {
+			id: MiAbuseReportNotificationRecipient['id'];
+			isActive: MiAbuseReportNotificationRecipient['isActive'];
+			name: MiAbuseReportNotificationRecipient['name'];
+			method: MiAbuseReportNotificationRecipient['method'];
+			userId: MiAbuseReportNotificationRecipient['userId'];
+			systemWebhookId: MiAbuseReportNotificationRecipient['systemWebhookId'];
+		},
+		updater: MiUser,
+	): Promise<MiAbuseReportNotificationRecipient> {
+		const beforeEntity = await this.abuseReportNotificationRecipientRepository.findOneByOrFail({ id: params.id });
+
+		await this.abuseReportNotificationRecipientRepository.update(params.id, {
+			isActive: params.isActive,
+			updatedAt: new Date(),
+			name: params.name,
+			method: params.method,
+			userId: params.userId,
+			systemWebhookId: params.systemWebhookId,
+		});
+
+		const afterEntity = await this.abuseReportNotificationRecipientRepository.findOneByOrFail({ id: params.id });
+
+		this.moderationLogService
+			.log(updater, 'updateAbuseReportNotificationRecipient', {
+				recipientId: params.id,
+				before: beforeEntity,
+				after: afterEntity,
+			});
+
+		return afterEntity;
+	}
+
+	/**
+	 * 通知先を削除する.
+	 */
+	@bindThis
+	public async deleteRecipient(
+		id: MiAbuseReportNotificationRecipient['id'],
+		updater: MiUser,
+	) {
+		const entity = await this.abuseReportNotificationRecipientRepository.findBy({ id });
+
+		await this.abuseReportNotificationRecipientRepository.delete(id);
+
+		this.moderationLogService
+			.log(updater, 'deleteAbuseReportNotificationRecipient', {
+				recipientId: id,
+				recipient: entity,
+			});
+	}
+
+	/**
+	 * モデレータ権限を持たない(*1)通知先ユーザを削除する.
+	 *
+	 * *1: 以下の両方を満たすものの事を言う
+	 * - 通知先にユーザIDが設定されている
+	 * - 付与ロールにモデレータ権限がない or アサインの有効期限が切れている
+	 *
+	 * @param recipients 通知先一覧の配列
+	 * @returns {@lisk recipients}からモデレータ権限を持たない通知先を削除した配列
+	 */
+	@bindThis
+	private async removeUnauthorizedRecipientUsers(recipients: MiAbuseReportNotificationRecipient[]): Promise<MiAbuseReportNotificationRecipient[]> {
+		const userRecipients = recipients.filter(it => it.userId !== null);
+		const recipientUserIds = new Set(userRecipients.map(it => it.userId).filter(x => x != null));
+		if (recipientUserIds.size <= 0) {
+			// ユーザが通知先として設定されていない場合、この関数での処理を行うべきレコードが無い
+			return recipients;
+		}
+
+		// モデレータ権限の有無で通知先設定を振り分ける
+		const authorizedUserIds = await this.roleService.getModeratorIds({
+			includeAdmins: true,
+			excludeExpire: true,
+		});
+		const authorizedUserRecipients = Array.of<MiAbuseReportNotificationRecipient>();
+		const unauthorizedUserRecipients = Array.of<MiAbuseReportNotificationRecipient>();
+		for (const recipient of userRecipients) {
+			// eslint-disable-next-line
+			if (authorizedUserIds.includes(recipient.userId!)) {
+				authorizedUserRecipients.push(recipient);
+			} else {
+				unauthorizedUserRecipients.push(recipient);
+			}
+		}
+
+		// モデレータ権限を持たない通知先をDBから削除する
+		if (unauthorizedUserRecipients.length > 0) {
+			await this.abuseReportNotificationRecipientRepository.delete(unauthorizedUserRecipients.map(it => it.id));
+		}
+		const nonUserRecipients = recipients.filter(it => it.userId === null);
+		return [...nonUserRecipients, ...authorizedUserRecipients].sort((a, b) => a.id.localeCompare(b.id));
+	}
+
+	@bindThis
+	private async onMessage(_: string, data: string): Promise<void> {
+		const obj = JSON.parse(data);
+		if (obj.channel !== 'internal') {
+			return;
+		}
+
+		const { type } = obj.message as GlobalEvents['internal']['payload'];
+		switch (type) {
+			case 'roleUpdated':
+			case 'roleDeleted':
+			case 'userRoleUnassigned': {
+				// 場合によってはキャッシュ更新よりも先にここが呼ばれてしまう可能性があるのでnextTickで遅延実行
+				process.nextTick(async () => {
+					const recipients = await this.abuseReportNotificationRecipientRepository.findBy({
+						userId: Not(IsNull()),
+					});
+					await this.removeUnauthorizedRecipientUsers(recipients);
+				});
+				break;
+			}
+			default: {
+				break;
+			}
+		}
+	}
+
+	@bindThis
+	public dispose(): void {
+		this.redisForSub.off('message', this.onMessage);
+	}
+
+	@bindThis
+	public onApplicationShutdown(signal?: string | undefined): void {
+		this.dispose();
+	}
+}
diff --git a/packages/backend/src/core/AbuseReportService.ts b/packages/backend/src/core/AbuseReportService.ts
new file mode 100644
index 0000000000..0b022d3b08
--- /dev/null
+++ b/packages/backend/src/core/AbuseReportService.ts
@@ -0,0 +1,176 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Inject, Injectable } from '@nestjs/common';
+import { In } from 'typeorm';
+import { DI } from '@/di-symbols.js';
+import { bindThis } from '@/decorators.js';
+import type { AbuseUserReportsRepository, MiAbuseUserReport, MiUser, UsersRepository } from '@/models/_.js';
+import { AbuseReportNotificationService } from '@/core/AbuseReportNotificationService.js';
+import { QueueService } from '@/core/QueueService.js';
+import { InstanceActorService } from '@/core/InstanceActorService.js';
+import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
+import { ModerationLogService } from '@/core/ModerationLogService.js';
+import { IdService } from './IdService.js';
+
+@Injectable()
+export class AbuseReportService {
+	constructor(
+		@Inject(DI.abuseUserReportsRepository)
+		private abuseUserReportsRepository: AbuseUserReportsRepository,
+
+		@Inject(DI.usersRepository)
+		private usersRepository: UsersRepository,
+
+		private idService: IdService,
+		private abuseReportNotificationService: AbuseReportNotificationService,
+		private queueService: QueueService,
+		private instanceActorService: InstanceActorService,
+		private apRendererService: ApRendererService,
+		private moderationLogService: ModerationLogService,
+	) {
+	}
+
+	/**
+	 * ユーザからの通報をDBに記録し、その内容を下記の手段で管理者各位に通知する.
+	 * - 管理者用Redisイベント
+	 * - EMail(モデレータ権限所有者ユーザ+metaテーブルに設定されているメールアドレス)
+	 * - SystemWebhook
+	 *
+	 * @param params 通報内容. もし複数件の通報に対応した時のために、あらかじめ複数件を処理できる前提で考える
+	 * @see AbuseReportNotificationService.notify
+	 */
+	@bindThis
+	public async report(params: {
+		targetUserId: MiAbuseUserReport['targetUserId'],
+		targetUserHost: MiAbuseUserReport['targetUserHost'],
+		reporterId: MiAbuseUserReport['reporterId'],
+		reporterHost: MiAbuseUserReport['reporterHost'],
+		comment: string,
+	}[]) {
+		const entities = params.map(param => {
+			return {
+				id: this.idService.gen(),
+				targetUserId: param.targetUserId,
+				targetUserHost: param.targetUserHost,
+				reporterId: param.reporterId,
+				reporterHost: param.reporterHost,
+				comment: param.comment,
+			};
+		});
+
+		const reports = Array.of<MiAbuseUserReport>();
+		for (const entity of entities) {
+			const report = await this.abuseUserReportsRepository.insertOne(entity);
+			reports.push(report);
+		}
+
+		return Promise.all([
+			this.abuseReportNotificationService.notifyAdminStream(reports),
+			this.abuseReportNotificationService.notifySystemWebhook(reports, 'abuseReport'),
+			this.abuseReportNotificationService.notifyMail(reports),
+		]);
+	}
+
+	/**
+	 * 通報を解決し、その内容を下記の手段で管理者各位に通知する.
+	 * - SystemWebhook
+	 *
+	 * @param params 通報内容. もし複数件の通報に対応した時のために、あらかじめ複数件を処理できる前提で考える
+	 * @param moderator 通報を処理したユーザ
+	 * @see AbuseReportNotificationService.notify
+	 */
+	@bindThis
+	public async resolve(
+		params: {
+			reportId: string;
+			resolvedAs: MiAbuseUserReport['resolvedAs'];
+		}[],
+		moderator: MiUser,
+	) {
+		const paramsMap = new Map(params.map(it => [it.reportId, it]));
+		const reports = await this.abuseUserReportsRepository.findBy({
+			id: In(params.map(it => it.reportId)),
+		});
+
+		for (const report of reports) {
+			// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+			const ps = paramsMap.get(report.id)!;
+
+			await this.abuseUserReportsRepository.update(report.id, {
+				resolved: true,
+				assigneeId: moderator.id,
+				resolvedAs: ps.resolvedAs,
+			});
+
+			this.moderationLogService
+				.log(moderator, 'resolveAbuseReport', {
+					reportId: report.id,
+					report: report,
+					resolvedAs: ps.resolvedAs,
+				});
+		}
+
+		return this.abuseUserReportsRepository.findBy({ id: In(reports.map(it => it.id)) })
+			.then(reports => this.abuseReportNotificationService.notifySystemWebhook(reports, 'abuseReportResolved'));
+	}
+
+	@bindThis
+	public async forward(
+		reportId: MiAbuseUserReport['id'],
+		moderator: MiUser,
+	) {
+		const report = await this.abuseUserReportsRepository.findOneByOrFail({ id: reportId });
+
+		if (report.targetUserHost == null) {
+			throw new Error('The target user host is null.');
+		}
+
+		if (report.forwarded) {
+			throw new Error('The report has already been forwarded.');
+		}
+
+		await this.abuseUserReportsRepository.update(report.id, {
+			forwarded: true,
+		});
+
+		const actor = await this.instanceActorService.getInstanceActor();
+		const targetUser = await this.usersRepository.findOneByOrFail({ id: report.targetUserId });
+
+		const flag = this.apRendererService.renderFlag(actor, targetUser.uri!, report.comment);
+		const contextAssignedFlag = this.apRendererService.addContext(flag);
+		this.queueService.deliver(actor, contextAssignedFlag, targetUser.inbox, false);
+
+		this.moderationLogService
+			.log(moderator, 'forwardAbuseReport', {
+				reportId: report.id,
+				report: report,
+			});
+	}
+
+	@bindThis
+	public async update(
+		reportId: MiAbuseUserReport['id'],
+		params: {
+			moderationNote?: MiAbuseUserReport['moderationNote'];
+		},
+		moderator: MiUser,
+	) {
+		const report = await this.abuseUserReportsRepository.findOneByOrFail({ id: reportId });
+
+		await this.abuseUserReportsRepository.update(report.id, {
+			moderationNote: params.moderationNote,
+		});
+
+		if (params.moderationNote != null && report.moderationNote !== params.moderationNote) {
+			this.moderationLogService.log(moderator, 'updateAbuseReportNote', {
+				reportId: report.id,
+				report: report,
+				before: report.moderationNote,
+				after: params.moderationNote,
+			});
+		}
+	}
+}
diff --git a/packages/backend/src/core/AccountMoveService.ts b/packages/backend/src/core/AccountMoveService.ts
index b6b591d240..24d11f29ff 100644
--- a/packages/backend/src/core/AccountMoveService.ts
+++ b/packages/backend/src/core/AccountMoveService.ts
@@ -9,7 +9,7 @@ import { IsNull, In, MoreThan, Not } from 'typeorm';
 import { bindThis } from '@/decorators.js';
 import { DI } from '@/di-symbols.js';
 import type { MiLocalUser, MiRemoteUser, MiUser } from '@/models/User.js';
-import type { BlockingsRepository, FollowingsRepository, InstancesRepository, MutingsRepository, UserListMembershipsRepository, UsersRepository } from '@/models/_.js';
+import type { BlockingsRepository, FollowingsRepository, InstancesRepository, MiMeta, MutingsRepository, UserListMembershipsRepository, UsersRepository } from '@/models/_.js';
 import type { RelationshipJobData, ThinUser } from '@/queue/types.js';
 
 import { IdService } from '@/core/IdService.js';
@@ -22,13 +22,15 @@ import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
 import { UserEntityService } from '@/core/entities/UserEntityService.js';
 import { ProxyAccountService } from '@/core/ProxyAccountService.js';
 import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
-import { MetaService } from '@/core/MetaService.js';
 import InstanceChart from '@/core/chart/charts/instance.js';
 import PerUserFollowingChart from '@/core/chart/charts/per-user-following.js';
 
 @Injectable()
 export class AccountMoveService {
 	constructor(
+		@Inject(DI.meta)
+		private meta: MiMeta,
+
 		@Inject(DI.usersRepository)
 		private usersRepository: UsersRepository,
 
@@ -57,7 +59,6 @@ export class AccountMoveService {
 		private perUserFollowingChart: PerUserFollowingChart,
 		private federatedInstanceService: FederatedInstanceService,
 		private instanceChart: InstanceChart,
-		private metaService: MetaService,
 		private relayService: RelayService,
 		private queueService: QueueService,
 	) {
@@ -273,13 +274,15 @@ export class AccountMoveService {
 		}
 
 		// Update instance stats by decreasing remote followers count by the number of local followers who were following the old account.
-		if (this.userEntityService.isRemoteUser(oldAccount)) {
-			this.federatedInstanceService.fetch(oldAccount.host).then(async i => {
-				this.instancesRepository.decrement({ id: i.id }, 'followersCount', localFollowerIds.length);
-				if ((await this.metaService.fetch()).enableChartsForFederatedInstances) {
-					this.instanceChart.updateFollowers(i.host, false);
-				}
-			});
+		if (this.meta.enableStatsForFederatedInstances) {
+			if (this.userEntityService.isRemoteUser(oldAccount)) {
+				this.federatedInstanceService.fetchOrRegister(oldAccount.host).then(async i => {
+					this.instancesRepository.decrement({ id: i.id }, 'followersCount', localFollowerIds.length);
+					if (this.meta.enableChartsForFederatedInstances) {
+						this.instanceChart.updateFollowers(i.host, false);
+					}
+				});
+			}
 		}
 
 		// FIXME: expensive?
diff --git a/packages/backend/src/core/AnnouncementService.ts b/packages/backend/src/core/AnnouncementService.ts
index 40a9db01c0..d4fcf19439 100644
--- a/packages/backend/src/core/AnnouncementService.ts
+++ b/packages/backend/src/core/AnnouncementService.ts
@@ -209,6 +209,13 @@ export class AnnouncementService {
 			return;
 		}
 
+		const announcement = await this.announcementsRepository.findOneBy({ id: announcementId });
+		if (announcement != null && announcement.userId === user.id) {
+			await this.announcementsRepository.update(announcementId, {
+				isActive: false,
+			});
+		}
+
 		if ((await this.getUnreadAnnouncements(user)).length === 0) {
 			this.globalEventService.publishMainStream(user.id, 'readAllAnnouncements');
 		}
diff --git a/packages/backend/src/core/AntennaService.ts b/packages/backend/src/core/AntennaService.ts
index 793d8974b3..e827ffa68c 100644
--- a/packages/backend/src/core/AntennaService.ts
+++ b/packages/backend/src/core/AntennaService.ts
@@ -123,11 +123,14 @@ export class AntennaService implements OnApplicationShutdown {
 		if (antenna.src === 'home') {
 			// TODO
 		} else if (antenna.src === 'list') {
-			const listUsers = (await this.userListMembershipsRepository.findBy({
-				userListId: antenna.userListId!,
-			})).map(x => x.userId);
-
-			if (!listUsers.includes(note.userId)) return false;
+			if (antenna.userListId == null) return false;
+			const exists = await this.userListMembershipsRepository.exists({
+				where: {
+					userListId: antenna.userListId,
+					userId: note.userId,
+				},
+			});
+			if (!exists) return false;
 		} else if (antenna.src === 'users') {
 			const accts = antenna.users.map(x => {
 				const { username, host } = Acct.parse(x);
diff --git a/packages/backend/src/core/AvatarDecorationService.ts b/packages/backend/src/core/AvatarDecorationService.ts
index 8b54bbe012..4efd6122b1 100644
--- a/packages/backend/src/core/AvatarDecorationService.ts
+++ b/packages/backend/src/core/AvatarDecorationService.ts
@@ -29,7 +29,7 @@ export class AvatarDecorationService implements OnApplicationShutdown {
 		private moderationLogService: ModerationLogService,
 		private globalEventService: GlobalEventService,
 	) {
-		this.cache = new MemorySingleCache<MiAvatarDecoration[]>(1000 * 60 * 30);
+		this.cache = new MemorySingleCache<MiAvatarDecoration[]>(1000 * 60 * 30); // 30s
 
 		this.redisForSub.on('message', this.onMessage);
 	}
diff --git a/packages/backend/src/core/CacheService.ts b/packages/backend/src/core/CacheService.ts
index d008e7ec52..6725ebe75b 100644
--- a/packages/backend/src/core/CacheService.ts
+++ b/packages/backend/src/core/CacheService.ts
@@ -56,10 +56,10 @@ export class CacheService implements OnApplicationShutdown {
 	) {
 		//this.onMessage = this.onMessage.bind(this);
 
-		this.userByIdCache = new MemoryKVCache<MiUser>(Infinity);
-		this.localUserByNativeTokenCache = new MemoryKVCache<MiLocalUser | null>(Infinity);
-		this.localUserByIdCache = new MemoryKVCache<MiLocalUser>(Infinity);
-		this.uriPersonCache = new MemoryKVCache<MiUser | null>(Infinity);
+		this.userByIdCache = new MemoryKVCache<MiUser>(1000 * 60 * 5); // 5m
+		this.localUserByNativeTokenCache = new MemoryKVCache<MiLocalUser | null>(1000 * 60 * 5); // 5m
+		this.localUserByIdCache = new MemoryKVCache<MiLocalUser>(1000 * 60 * 5); // 5m
+		this.uriPersonCache = new MemoryKVCache<MiUser | null>(1000 * 60 * 5); // 5m
 
 		this.userProfileCache = new RedisKVCache<MiUserProfile>(this.redisClient, 'userProfile', {
 			lifetime: 1000 * 60 * 30, // 30m
@@ -135,14 +135,14 @@ export class CacheService implements OnApplicationShutdown {
 					if (user == null) {
 						this.userByIdCache.delete(body.id);
 						this.localUserByIdCache.delete(body.id);
-						for (const [k, v] of this.uriPersonCache.cache.entries()) {
+						for (const [k, v] of this.uriPersonCache.entries) {
 							if (v.value?.id === body.id) {
 								this.uriPersonCache.delete(k);
 							}
 						}
 					} else {
 						this.userByIdCache.set(user.id, user);
-						for (const [k, v] of this.uriPersonCache.cache.entries()) {
+						for (const [k, v] of this.uriPersonCache.entries) {
 							if (v.value?.id === user.id) {
 								this.uriPersonCache.set(k, user);
 							}
diff --git a/packages/backend/src/core/CaptchaService.ts b/packages/backend/src/core/CaptchaService.ts
index f6b7955cd2..206d0dbe0a 100644
--- a/packages/backend/src/core/CaptchaService.ts
+++ b/packages/backend/src/core/CaptchaService.ts
@@ -119,5 +119,18 @@ export class CaptchaService {
 			throw new Error(`turnstile-failed: ${errorCodes}`);
 		}
 	}
+
+	@bindThis
+	public async verifyTestcaptcha(response: string | null | undefined): Promise<void> {
+		if (response == null) {
+			throw new Error('testcaptcha-failed: no response provided');
+		}
+
+		const success = response === 'testcaptcha-passed';
+
+		if (!success) {
+			throw new Error('testcaptcha-failed');
+		}
+	}
 }
 
diff --git a/packages/backend/src/core/ClipService.ts b/packages/backend/src/core/ClipService.ts
index 9fd1ebad87..929a9db064 100644
--- a/packages/backend/src/core/ClipService.ts
+++ b/packages/backend/src/core/ClipService.ts
@@ -41,7 +41,7 @@ export class ClipService {
 		const currentCount = await this.clipsRepository.countBy({
 			userId: me.id,
 		});
-		if (currentCount > (await this.roleService.getUserPolicies(me.id)).clipLimit) {
+		if (currentCount >= (await this.roleService.getUserPolicies(me.id)).clipLimit) {
 			throw new ClipService.TooManyClipsError();
 		}
 
@@ -102,7 +102,7 @@ export class ClipService {
 		const currentCount = await this.clipNotesRepository.countBy({
 			clipId: clip.id,
 		});
-		if (currentCount > (await this.roleService.getUserPolicies(me.id)).noteEachClipsLimit) {
+		if (currentCount >= (await this.roleService.getUserPolicies(me.id)).noteEachClipsLimit) {
 			throw new ClipService.TooManyClipNotesError();
 		}
 
diff --git a/packages/backend/src/core/CoreModule.ts b/packages/backend/src/core/CoreModule.ts
index be80df6f1c..734d135648 100644
--- a/packages/backend/src/core/CoreModule.ts
+++ b/packages/backend/src/core/CoreModule.ts
@@ -5,6 +5,16 @@
 
 import { Module } from '@nestjs/common';
 import { FanoutTimelineEndpointService } from '@/core/FanoutTimelineEndpointService.js';
+import { AbuseReportService } from '@/core/AbuseReportService.js';
+import { SystemWebhookEntityService } from '@/core/entities/SystemWebhookEntityService.js';
+import {
+	AbuseReportNotificationRecipientEntityService,
+} from '@/core/entities/AbuseReportNotificationRecipientEntityService.js';
+import { AbuseReportNotificationService } from '@/core/AbuseReportNotificationService.js';
+import { SystemWebhookService } from '@/core/SystemWebhookService.js';
+import { UserSearchService } from '@/core/UserSearchService.js';
+import { WebhookTestService } from '@/core/WebhookTestService.js';
+import { FlashService } from '@/core/FlashService.js';
 import { AccountMoveService } from './AccountMoveService.js';
 import { AccountUpdateService } from './AccountUpdateService.js';
 import { AiService } from './AiService.js';
@@ -41,6 +51,7 @@ import { PollService } from './PollService.js';
 import { PushNotificationService } from './PushNotificationService.js';
 import { QueryService } from './QueryService.js';
 import { ReactionService } from './ReactionService.js';
+import { ReactionsBufferingService } from './ReactionsBufferingService.js';
 import { RelayService } from './RelayService.js';
 import { RoleService } from './RoleService.js';
 import { S3Service } from './S3Service.js';
@@ -53,10 +64,11 @@ import { UserFollowingService } from './UserFollowingService.js';
 import { UserKeypairService } from './UserKeypairService.js';
 import { UserListService } from './UserListService.js';
 import { UserMutingService } from './UserMutingService.js';
+import { UserRenoteMutingService } from './UserRenoteMutingService.js';
 import { UserSuspendService } from './UserSuspendService.js';
 import { UserAuthService } from './UserAuthService.js';
 import { VideoProcessingService } from './VideoProcessingService.js';
-import { WebhookService } from './WebhookService.js';
+import { UserWebhookService } from './UserWebhookService.js';
 import { ProxyAccountService } from './ProxyAccountService.js';
 import { UtilityService } from './UtilityService.js';
 import { FileInfoService } from './FileInfoService.js';
@@ -144,6 +156,8 @@ import type { Provider } from '@nestjs/common';
 
 //#region 文字列ベースでのinjection用(循環参照対応のため)
 const $LoggerService: Provider = { provide: 'LoggerService', useExisting: LoggerService };
+const $AbuseReportService: Provider = { provide: 'AbuseReportService', useExisting: AbuseReportService };
+const $AbuseReportNotificationService: Provider = { provide: 'AbuseReportNotificationService', useExisting: AbuseReportNotificationService };
 const $AccountMoveService: Provider = { provide: 'AccountMoveService', useExisting: AccountMoveService };
 const $AccountUpdateService: Provider = { provide: 'AccountUpdateService', useExisting: AccountUpdateService };
 const $AiService: Provider = { provide: 'AiService', useExisting: AiService };
@@ -181,6 +195,7 @@ const $ProxyAccountService: Provider = { provide: 'ProxyAccountService', useExis
 const $PushNotificationService: Provider = { provide: 'PushNotificationService', useExisting: PushNotificationService };
 const $QueryService: Provider = { provide: 'QueryService', useExisting: QueryService };
 const $ReactionService: Provider = { provide: 'ReactionService', useExisting: ReactionService };
+const $ReactionsBufferingService: Provider = { provide: 'ReactionsBufferingService', useExisting: ReactionsBufferingService };
 const $RelayService: Provider = { provide: 'RelayService', useExisting: RelayService };
 const $RoleService: Provider = { provide: 'RoleService', useExisting: RoleService };
 const $S3Service: Provider = { provide: 'S3Service', useExisting: S3Service };
@@ -193,12 +208,17 @@ const $UserFollowingService: Provider = { provide: 'UserFollowingService', useEx
 const $UserKeypairService: Provider = { provide: 'UserKeypairService', useExisting: UserKeypairService };
 const $UserListService: Provider = { provide: 'UserListService', useExisting: UserListService };
 const $UserMutingService: Provider = { provide: 'UserMutingService', useExisting: UserMutingService };
+const $UserRenoteMutingService: Provider = { provide: 'UserRenoteMutingService', useExisting: UserRenoteMutingService };
+const $UserSearchService: Provider = { provide: 'UserSearchService', useExisting: UserSearchService };
 const $UserSuspendService: Provider = { provide: 'UserSuspendService', useExisting: UserSuspendService };
 const $UserAuthService: Provider = { provide: 'UserAuthService', useExisting: UserAuthService };
 const $VideoProcessingService: Provider = { provide: 'VideoProcessingService', useExisting: VideoProcessingService };
-const $WebhookService: Provider = { provide: 'WebhookService', useExisting: WebhookService };
+const $UserWebhookService: Provider = { provide: 'UserWebhookService', useExisting: UserWebhookService };
+const $SystemWebhookService: Provider = { provide: 'SystemWebhookService', useExisting: SystemWebhookService };
+const $WebhookTestService: Provider = { provide: 'WebhookTestService', useExisting: WebhookTestService };
 const $UtilityService: Provider = { provide: 'UtilityService', useExisting: UtilityService };
 const $FileInfoService: Provider = { provide: 'FileInfoService', useExisting: FileInfoService };
+const $FlashService: Provider = { provide: 'FlashService', useExisting: FlashService };
 const $SearchService: Provider = { provide: 'SearchService', useExisting: SearchService };
 const $ClipService: Provider = { provide: 'ClipService', useExisting: ClipService };
 const $FeaturedService: Provider = { provide: 'FeaturedService', useExisting: FeaturedService };
@@ -225,6 +245,7 @@ const $ChartManagementService: Provider = { provide: 'ChartManagementService', u
 
 const $AbuseUserReportEntityService: Provider = { provide: 'AbuseUserReportEntityService', useExisting: AbuseUserReportEntityService };
 const $AnnouncementEntityService: Provider = { provide: 'AnnouncementEntityService', useExisting: AnnouncementEntityService };
+const $AbuseReportNotificationRecipientEntityService: Provider = { provide: 'AbuseReportNotificationRecipientEntityService', useExisting: AbuseReportNotificationRecipientEntityService };
 const $AntennaEntityService: Provider = { provide: 'AntennaEntityService', useExisting: AntennaEntityService };
 const $AppEntityService: Provider = { provide: 'AppEntityService', useExisting: AppEntityService };
 const $AuthSessionEntityService: Provider = { provide: 'AuthSessionEntityService', useExisting: AuthSessionEntityService };
@@ -258,6 +279,7 @@ const $FlashLikeEntityService: Provider = { provide: 'FlashLikeEntityService', u
 const $RoleEntityService: Provider = { provide: 'RoleEntityService', useExisting: RoleEntityService };
 const $ReversiGameEntityService: Provider = { provide: 'ReversiGameEntityService', useExisting: ReversiGameEntityService };
 const $MetaEntityService: Provider = { provide: 'MetaEntityService', useExisting: MetaEntityService };
+const $SystemWebhookEntityService: Provider = { provide: 'SystemWebhookEntityService', useExisting: SystemWebhookEntityService };
 
 const $ApAudienceService: Provider = { provide: 'ApAudienceService', useExisting: ApAudienceService };
 const $ApDbResolverService: Provider = { provide: 'ApDbResolverService', useExisting: ApDbResolverService };
@@ -285,6 +307,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
 	],
 	providers: [
 		LoggerService,
+		AbuseReportService,
+		AbuseReportNotificationService,
 		AccountMoveService,
 		AccountUpdateService,
 		AiService,
@@ -322,6 +346,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
 		PushNotificationService,
 		QueryService,
 		ReactionService,
+		ReactionsBufferingService,
 		RelayService,
 		RoleService,
 		S3Service,
@@ -334,12 +359,17 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
 		UserKeypairService,
 		UserListService,
 		UserMutingService,
+		UserRenoteMutingService,
+		UserSearchService,
 		UserSuspendService,
 		UserAuthService,
 		VideoProcessingService,
-		WebhookService,
+		UserWebhookService,
+		SystemWebhookService,
+		WebhookTestService,
 		UtilityService,
 		FileInfoService,
+		FlashService,
 		SearchService,
 		ClipService,
 		FeaturedService,
@@ -366,6 +396,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
 
 		AbuseUserReportEntityService,
 		AnnouncementEntityService,
+		AbuseReportNotificationRecipientEntityService,
 		AntennaEntityService,
 		AppEntityService,
 		AuthSessionEntityService,
@@ -399,6 +430,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
 		RoleEntityService,
 		ReversiGameEntityService,
 		MetaEntityService,
+		SystemWebhookEntityService,
 
 		ApAudienceService,
 		ApDbResolverService,
@@ -422,6 +454,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
 
 		//#region 文字列ベースでのinjection用(循環参照対応のため)
 		$LoggerService,
+		$AbuseReportService,
+		$AbuseReportNotificationService,
 		$AccountMoveService,
 		$AccountUpdateService,
 		$AiService,
@@ -459,6 +493,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
 		$PushNotificationService,
 		$QueryService,
 		$ReactionService,
+		$ReactionsBufferingService,
 		$RelayService,
 		$RoleService,
 		$S3Service,
@@ -471,12 +506,17 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
 		$UserKeypairService,
 		$UserListService,
 		$UserMutingService,
+		$UserRenoteMutingService,
+		$UserSearchService,
 		$UserSuspendService,
 		$UserAuthService,
 		$VideoProcessingService,
-		$WebhookService,
+		$UserWebhookService,
+		$SystemWebhookService,
+		$WebhookTestService,
 		$UtilityService,
 		$FileInfoService,
+		$FlashService,
 		$SearchService,
 		$ClipService,
 		$FeaturedService,
@@ -503,6 +543,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
 
 		$AbuseUserReportEntityService,
 		$AnnouncementEntityService,
+		$AbuseReportNotificationRecipientEntityService,
 		$AntennaEntityService,
 		$AppEntityService,
 		$AuthSessionEntityService,
@@ -536,6 +577,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
 		$RoleEntityService,
 		$ReversiGameEntityService,
 		$MetaEntityService,
+		$SystemWebhookEntityService,
 
 		$ApAudienceService,
 		$ApDbResolverService,
@@ -560,6 +602,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
 	exports: [
 		QueueModule,
 		LoggerService,
+		AbuseReportService,
+		AbuseReportNotificationService,
 		AccountMoveService,
 		AccountUpdateService,
 		AiService,
@@ -597,6 +641,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
 		PushNotificationService,
 		QueryService,
 		ReactionService,
+		ReactionsBufferingService,
 		RelayService,
 		RoleService,
 		S3Service,
@@ -609,12 +654,17 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
 		UserKeypairService,
 		UserListService,
 		UserMutingService,
+		UserRenoteMutingService,
+		UserSearchService,
 		UserSuspendService,
 		UserAuthService,
 		VideoProcessingService,
-		WebhookService,
+		UserWebhookService,
+		SystemWebhookService,
+		WebhookTestService,
 		UtilityService,
 		FileInfoService,
+		FlashService,
 		SearchService,
 		ClipService,
 		FeaturedService,
@@ -640,6 +690,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
 
 		AbuseUserReportEntityService,
 		AnnouncementEntityService,
+		AbuseReportNotificationRecipientEntityService,
 		AntennaEntityService,
 		AppEntityService,
 		AuthSessionEntityService,
@@ -673,6 +724,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
 		RoleEntityService,
 		ReversiGameEntityService,
 		MetaEntityService,
+		SystemWebhookEntityService,
 
 		ApAudienceService,
 		ApDbResolverService,
@@ -696,6 +748,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
 
 		//#region 文字列ベースでのinjection用(循環参照対応のため)
 		$LoggerService,
+		$AbuseReportService,
+		$AbuseReportNotificationService,
 		$AccountMoveService,
 		$AccountUpdateService,
 		$AiService,
@@ -733,6 +787,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
 		$PushNotificationService,
 		$QueryService,
 		$ReactionService,
+		$ReactionsBufferingService,
 		$RelayService,
 		$RoleService,
 		$S3Service,
@@ -745,10 +800,14 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
 		$UserKeypairService,
 		$UserListService,
 		$UserMutingService,
+		$UserRenoteMutingService,
+		$UserSearchService,
 		$UserSuspendService,
 		$UserAuthService,
 		$VideoProcessingService,
-		$WebhookService,
+		$UserWebhookService,
+		$SystemWebhookService,
+		$WebhookTestService,
 		$UtilityService,
 		$FileInfoService,
 		$SearchService,
@@ -776,6 +835,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
 
 		$AbuseUserReportEntityService,
 		$AnnouncementEntityService,
+		$AbuseReportNotificationRecipientEntityService,
 		$AntennaEntityService,
 		$AppEntityService,
 		$AuthSessionEntityService,
@@ -809,6 +869,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
 		$RoleEntityService,
 		$ReversiGameEntityService,
 		$MetaEntityService,
+		$SystemWebhookEntityService,
 
 		$ApAudienceService,
 		$ApDbResolverService,
diff --git a/packages/backend/src/core/CustomEmojiService.ts b/packages/backend/src/core/CustomEmojiService.ts
index 7e11b9cdca..4566113449 100644
--- a/packages/backend/src/core/CustomEmojiService.ts
+++ b/packages/backend/src/core/CustomEmojiService.ts
@@ -24,7 +24,7 @@ const parseEmojiStrRegexp = /^([-\w]+)(?:@([\w.-]+))?$/;
 
 @Injectable()
 export class CustomEmojiService implements OnApplicationShutdown {
-	private cache: MemoryKVCache<MiEmoji | null>;
+	private emojisCache: MemoryKVCache<MiEmoji | null>;
 	public localEmojisCache: RedisSingleCache<Map<string, MiEmoji>>;
 
 	constructor(
@@ -40,7 +40,7 @@ export class CustomEmojiService implements OnApplicationShutdown {
 		private moderationLogService: ModerationLogService,
 		private globalEventService: GlobalEventService,
 	) {
-		this.cache = new MemoryKVCache<MiEmoji | null>(1000 * 60 * 60 * 12);
+		this.emojisCache = new MemoryKVCache<MiEmoji | null>(1000 * 60 * 60 * 12); // 12h
 
 		this.localEmojisCache = new RedisSingleCache<Map<string, MiEmoji>>(this.redisClient, 'localEmojis', {
 			lifetime: 1000 * 60 * 30, // 30m
@@ -103,19 +103,33 @@ export class CustomEmojiService implements OnApplicationShutdown {
 	}
 
 	@bindThis
-	public async update(id: MiEmoji['id'], data: {
+	public async update(data: (
+		{ id: MiEmoji['id'], name?: string; } | { name: string; id?: MiEmoji['id'], }
+	) & {
 		driveFile?: MiDriveFile;
-		name?: string;
 		category?: string | null;
 		aliases?: string[];
 		license?: string | null;
 		isSensitive?: boolean;
 		localOnly?: boolean;
 		roleIdsThatCanBeUsedThisEmojiAsReaction?: MiRole['id'][];
-	}, moderator?: MiUser): Promise<void> {
-		const emoji = await this.emojisRepository.findOneByOrFail({ id: id });
-		const sameNameEmoji = await this.emojisRepository.findOneBy({ name: data.name, host: IsNull() });
-		if (sameNameEmoji != null && sameNameEmoji.id !== id) throw new Error('name already exists');
+	}, moderator?: MiUser): Promise<
+		null
+		| 'NO_SUCH_EMOJI'
+		| 'SAME_NAME_EMOJI_EXISTS'
+	> {
+		const emoji = data.id
+			? await this.getEmojiById(data.id)
+			: await this.getEmojiByName(data.name!);
+		if (emoji === null) return 'NO_SUCH_EMOJI';
+		const id = emoji.id;
+
+		// IDと絵文字名が両方指定されている場合は絵文字名の変更を行うため重複チェックが必要
+		const doNameUpdate = data.id && data.name && (data.name !== emoji.name);
+		if (doNameUpdate) {
+			const isDuplicate = await this.checkDuplicate(data.name!);
+			if (isDuplicate) return 'SAME_NAME_EMOJI_EXISTS';
+		}
 
 		await this.emojisRepository.update(emoji.id, {
 			updatedAt: new Date(),
@@ -135,7 +149,7 @@ export class CustomEmojiService implements OnApplicationShutdown {
 
 		const packed = await this.emojiEntityService.packDetailed(emoji.id);
 
-		if (emoji.name === data.name) {
+		if (!doNameUpdate) {
 			this.globalEventService.publishBroadcastStream('emojiUpdated', {
 				emojis: [packed],
 			});
@@ -157,6 +171,7 @@ export class CustomEmojiService implements OnApplicationShutdown {
 				after: updated,
 			});
 		}
+		return null;
 	}
 
 	@bindThis
@@ -334,7 +349,7 @@ export class CustomEmojiService implements OnApplicationShutdown {
 			host,
 		})) ?? null;
 
-		const emoji = await this.cache.fetch(`${name} ${host}`, queryOrNull);
+		const emoji = await this.emojisCache.fetch(`${name} ${host}`, queryOrNull);
 
 		if (emoji == null) return null;
 		return emoji.publicUrl || emoji.originalUrl; // || emoji.originalUrl してるのは後方互換性のため(publicUrlはstringなので??はだめ)
@@ -361,7 +376,7 @@ export class CustomEmojiService implements OnApplicationShutdown {
 	 */
 	@bindThis
 	public async prefetchEmojis(emojis: { name: string; host: string | null; }[]): Promise<void> {
-		const notCachedEmojis = emojis.filter(emoji => this.cache.get(`${emoji.name} ${emoji.host}`) == null);
+		const notCachedEmojis = emojis.filter(emoji => this.emojisCache.get(`${emoji.name} ${emoji.host}`) == null);
 		const emojisQuery: any[] = [];
 		const hosts = new Set(notCachedEmojis.map(e => e.host));
 		for (const host of hosts) {
@@ -376,7 +391,7 @@ export class CustomEmojiService implements OnApplicationShutdown {
 			select: ['name', 'host', 'originalUrl', 'publicUrl'],
 		}) : [];
 		for (const emoji of _emojis) {
-			this.cache.set(`${emoji.name} ${emoji.host}`, emoji);
+			this.emojisCache.set(`${emoji.name} ${emoji.host}`, emoji);
 		}
 	}
 
@@ -401,7 +416,7 @@ export class CustomEmojiService implements OnApplicationShutdown {
 
 	@bindThis
 	public dispose(): void {
-		this.cache.dispose();
+		this.emojisCache.dispose();
 	}
 
 	@bindThis
diff --git a/packages/backend/src/core/DeleteAccountService.ts b/packages/backend/src/core/DeleteAccountService.ts
index 79b614edba..7f1b8f3efb 100644
--- a/packages/backend/src/core/DeleteAccountService.ts
+++ b/packages/backend/src/core/DeleteAccountService.ts
@@ -4,12 +4,15 @@
  */
 
 import { Inject, Injectable } from '@nestjs/common';
-import type { UsersRepository } from '@/models/_.js';
+import { Not, IsNull } from 'typeorm';
+import type { FollowingsRepository, MiUser, UsersRepository } from '@/models/_.js';
 import { QueueService } from '@/core/QueueService.js';
-import { UserSuspendService } from '@/core/UserSuspendService.js';
 import { DI } from '@/di-symbols.js';
 import { bindThis } from '@/decorators.js';
 import { GlobalEventService } from '@/core/GlobalEventService.js';
+import { UserEntityService } from '@/core/entities/UserEntityService.js';
+import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
+import { ModerationLogService } from '@/core/ModerationLogService.js';
 
 @Injectable()
 export class DeleteAccountService {
@@ -17,9 +20,14 @@ export class DeleteAccountService {
 		@Inject(DI.usersRepository)
 		private usersRepository: UsersRepository,
 
-		private userSuspendService: UserSuspendService,
+		@Inject(DI.followingsRepository)
+		private followingsRepository: FollowingsRepository,
+
+		private userEntityService: UserEntityService,
+		private apRendererService: ApRendererService,
 		private queueService: QueueService,
 		private globalEventService: GlobalEventService,
+		private moderationLogService: ModerationLogService,
 	) {
 	}
 
@@ -27,16 +35,52 @@ export class DeleteAccountService {
 	public async deleteAccount(user: {
 		id: string;
 		host: string | null;
-	}): Promise<void> {
+	}, moderator?: MiUser): Promise<void> {
 		const _user = await this.usersRepository.findOneByOrFail({ id: user.id });
 		if (_user.isRoot) throw new Error('cannot delete a root account');
 
-		// 物理削除する前にDelete activityを送信する
-		await this.userSuspendService.doPostSuspend(user).catch(e => {});
+		if (moderator != null) {
+			this.moderationLogService.log(moderator, 'deleteAccount', {
+				userId: user.id,
+				userUsername: _user.username,
+				userHost: user.host,
+			});
+		}
 
-		this.queueService.createDeleteAccountJob(user, {
-			soft: false,
-		});
+		// 物理削除する前にDelete activityを送信する
+		if (this.userEntityService.isLocalUser(user)) {
+			// 知り得る全SharedInboxにDelete配信
+			const content = this.apRendererService.addContext(this.apRendererService.renderDelete(this.userEntityService.genLocalUserUri(user.id), user));
+
+			const queue: string[] = [];
+
+			const followings = await this.followingsRepository.find({
+				where: [
+					{ followerSharedInbox: Not(IsNull()) },
+					{ followeeSharedInbox: Not(IsNull()) },
+				],
+				select: ['followerSharedInbox', 'followeeSharedInbox'],
+			});
+
+			const inboxes = followings.map(x => x.followerSharedInbox ?? x.followeeSharedInbox);
+
+			for (const inbox of inboxes) {
+				if (inbox != null && !queue.includes(inbox)) queue.push(inbox);
+			}
+
+			for (const inbox of queue) {
+				this.queueService.deliver(user, content, inbox, true);
+			}
+
+			this.queueService.createDeleteAccountJob(user, {
+				soft: false,
+			});
+		} else {
+			// リモートユーザーの削除は、完全にDBから物理削除してしまうと再度連合してきてアカウントが復活する可能性があるため、soft指定する
+			this.queueService.createDeleteAccountJob(user, {
+				soft: true,
+			});
+		}
 
 		await this.usersRepository.update(user.id, {
 			isDeleted: true,
diff --git a/packages/backend/src/core/DownloadService.ts b/packages/backend/src/core/DownloadService.ts
index 21ae798f9f..93f4a38246 100644
--- a/packages/backend/src/core/DownloadService.ts
+++ b/packages/backend/src/core/DownloadService.ts
@@ -42,7 +42,7 @@ export class DownloadService {
 
 		const timeout = 30 * 1000;
 		const operationTimeout = 60 * 1000;
-		const maxSize = this.config.maxFileSize ?? 262144000;
+		const maxSize = this.config.maxFileSize;
 
 		const urlObj = new URL(url);
 		let filename = urlObj.pathname.split('/').pop() ?? 'untitled';
diff --git a/packages/backend/src/core/DriveService.ts b/packages/backend/src/core/DriveService.ts
index 37c5d1adf7..c332e5a0a8 100644
--- a/packages/backend/src/core/DriveService.ts
+++ b/packages/backend/src/core/DriveService.ts
@@ -11,11 +11,10 @@ import { sharpBmp } from '@misskey-dev/sharp-read-bmp';
 import { IsNull } from 'typeorm';
 import { DeleteObjectCommandInput, PutObjectCommandInput, NoSuchKey } from '@aws-sdk/client-s3';
 import { DI } from '@/di-symbols.js';
-import type { DriveFilesRepository, UsersRepository, DriveFoldersRepository, UserProfilesRepository } from '@/models/_.js';
+import type { DriveFilesRepository, UsersRepository, DriveFoldersRepository, UserProfilesRepository, MiMeta } from '@/models/_.js';
 import type { Config } from '@/config.js';
 import Logger from '@/logger.js';
 import type { MiRemoteUser, MiUser } from '@/models/User.js';
-import { MetaService } from '@/core/MetaService.js';
 import { MiDriveFile } from '@/models/DriveFile.js';
 import { IdService } from '@/core/IdService.js';
 import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error.js';
@@ -43,6 +42,7 @@ import { RoleService } from '@/core/RoleService.js';
 import { correctFilename } from '@/misc/correct-filename.js';
 import { isMimeImage } from '@/misc/is-mime-image.js';
 import { ModerationLogService } from '@/core/ModerationLogService.js';
+import { UtilityService } from '@/core/UtilityService.js';
 
 type AddFileArgs = {
 	/** User who wish to add file */
@@ -98,6 +98,9 @@ export class DriveService {
 		@Inject(DI.config)
 		private config: Config,
 
+		@Inject(DI.meta)
+		private meta: MiMeta,
+
 		@Inject(DI.usersRepository)
 		private usersRepository: UsersRepository,
 
@@ -114,7 +117,6 @@ export class DriveService {
 		private userEntityService: UserEntityService,
 		private driveFileEntityService: DriveFileEntityService,
 		private idService: IdService,
-		private metaService: MetaService,
 		private downloadService: DownloadService,
 		private internalStorageService: InternalStorageService,
 		private s3Service: S3Service,
@@ -127,6 +129,7 @@ export class DriveService {
 		private driveChart: DriveChart,
 		private perUserDriveChart: PerUserDriveChart,
 		private instanceChart: InstanceChart,
+		private utilityService: UtilityService,
 	) {
 		const logger = new Logger('drive', 'blue');
 		this.registerLogger = logger.createSubLogger('register', 'yellow');
@@ -147,9 +150,7 @@ export class DriveService {
 	// thunbnail, webpublic を必要なら生成
 		const alts = await this.generateAlts(path, type, !file.uri);
 
-		const meta = await this.metaService.fetch();
-
-		if (meta.useObjectStorage) {
+		if (this.meta.useObjectStorage) {
 		//#region ObjectStorage params
 			let [ext] = (name.match(/\.([a-zA-Z0-9_-]+)$/) ?? ['']);
 
@@ -168,11 +169,11 @@ export class DriveService {
 				ext = '';
 			}
 
-			const baseUrl = meta.objectStorageBaseUrl
-				?? `${ meta.objectStorageUseSSL ? 'https' : 'http' }://${ meta.objectStorageEndpoint }${ meta.objectStoragePort ? `:${meta.objectStoragePort}` : '' }/${ meta.objectStorageBucket }`;
+			const baseUrl = this.meta.objectStorageBaseUrl
+				?? `${ this.meta.objectStorageUseSSL ? 'https' : 'http' }://${ this.meta.objectStorageEndpoint }${ this.meta.objectStoragePort ? `:${this.meta.objectStoragePort}` : '' }/${ this.meta.objectStorageBucket }`;
 
 			// for original
-			const key = `${meta.objectStoragePrefix}/${randomUUID()}${ext}`;
+			const key = `${this.meta.objectStoragePrefix}/${randomUUID()}${ext}`;
 			const url = `${ baseUrl }/${ key }`;
 
 			// for alts
@@ -189,7 +190,7 @@ export class DriveService {
 			];
 
 			if (alts.webpublic) {
-				webpublicKey = `${meta.objectStoragePrefix}/webpublic-${randomUUID()}.${alts.webpublic.ext}`;
+				webpublicKey = `${this.meta.objectStoragePrefix}/webpublic-${randomUUID()}.${alts.webpublic.ext}`;
 				webpublicUrl = `${ baseUrl }/${ webpublicKey }`;
 
 				this.registerLogger.info(`uploading webpublic: ${webpublicKey}`);
@@ -197,7 +198,7 @@ export class DriveService {
 			}
 
 			if (alts.thumbnail) {
-				thumbnailKey = `${meta.objectStoragePrefix}/thumbnail-${randomUUID()}.${alts.thumbnail.ext}`;
+				thumbnailKey = `${this.meta.objectStoragePrefix}/thumbnail-${randomUUID()}.${alts.thumbnail.ext}`;
 				thumbnailUrl = `${ baseUrl }/${ thumbnailKey }`;
 
 				this.registerLogger.info(`uploading thumbnail: ${thumbnailKey}`);
@@ -374,10 +375,8 @@ export class DriveService {
 		if (type === 'image/apng') type = 'image/png';
 		if (!FILE_TYPE_BROWSERSAFE.includes(type)) type = 'application/octet-stream';
 
-		const meta = await this.metaService.fetch();
-
 		const params = {
-			Bucket: meta.objectStorageBucket,
+			Bucket: this.meta.objectStorageBucket,
 			Key: key,
 			Body: stream,
 			ContentType: type,
@@ -390,9 +389,9 @@ export class DriveService {
 			// 許可されているファイル形式でしか拡張子をつけない
 			ext ? correctFilename(filename, ext) : filename,
 		);
-		if (meta.objectStorageSetPublicRead) params.ACL = 'public-read';
+		if (this.meta.objectStorageSetPublicRead) params.ACL = 'public-read';
 
-		await this.s3Service.upload(meta, params)
+		await this.s3Service.upload(this.meta, params)
 			.then(
 				result => {
 					if ('Bucket' in result) { // CompleteMultipartUploadCommandOutput
@@ -458,32 +457,31 @@ export class DriveService {
 		ext = null,
 	}: AddFileArgs): Promise<MiDriveFile> {
 		let skipNsfwCheck = false;
-		const instance = await this.metaService.fetch();
 		const userRoleNSFW = user && (await this.roleService.getUserPolicies(user.id)).alwaysMarkNsfw;
 		if (user == null) {
 			skipNsfwCheck = true;
 		} else if (userRoleNSFW) {
 			skipNsfwCheck = true;
 		}
-		if (instance.sensitiveMediaDetection === 'none') skipNsfwCheck = true;
-		if (user && instance.sensitiveMediaDetection === 'local' && this.userEntityService.isRemoteUser(user)) skipNsfwCheck = true;
-		if (user && instance.sensitiveMediaDetection === 'remote' && this.userEntityService.isLocalUser(user)) skipNsfwCheck = true;
+		if (this.meta.sensitiveMediaDetection === 'none') skipNsfwCheck = true;
+		if (user && this.meta.sensitiveMediaDetection === 'local' && this.userEntityService.isRemoteUser(user)) skipNsfwCheck = true;
+		if (user && this.meta.sensitiveMediaDetection === 'remote' && this.userEntityService.isLocalUser(user)) skipNsfwCheck = true;
 
 		const info = await this.fileInfoService.getFileInfo(path, {
 			skipSensitiveDetection: skipNsfwCheck,
 			sensitiveThreshold: // 感度が高いほどしきい値は低くすることになる
-			instance.sensitiveMediaDetectionSensitivity === 'veryHigh' ? 0.1 :
-			instance.sensitiveMediaDetectionSensitivity === 'high' ? 0.3 :
-			instance.sensitiveMediaDetectionSensitivity === 'low' ? 0.7 :
-			instance.sensitiveMediaDetectionSensitivity === 'veryLow' ? 0.9 :
+			this.meta.sensitiveMediaDetectionSensitivity === 'veryHigh' ? 0.1 :
+			this.meta.sensitiveMediaDetectionSensitivity === 'high' ? 0.3 :
+			this.meta.sensitiveMediaDetectionSensitivity === 'low' ? 0.7 :
+			this.meta.sensitiveMediaDetectionSensitivity === 'veryLow' ? 0.9 :
 			0.5,
 			sensitiveThresholdForPorn: 0.75,
-			enableSensitiveMediaDetectionForVideos: instance.enableSensitiveMediaDetectionForVideos,
+			enableSensitiveMediaDetectionForVideos: this.meta.enableSensitiveMediaDetectionForVideos,
 		});
 		this.registerLogger.info(`${JSON.stringify(info)}`);
 
 		// 現状 false positive が多すぎて実用に耐えない
-		//if (info.porn && instance.disallowUploadWhenPredictedAsPorn) {
+		//if (info.porn && this.meta.disallowUploadWhenPredictedAsPorn) {
 		//	throw new IdentifiableError('282f77bf-5816-4f72-9264-aa14d8261a21', 'Detected as porn.');
 		//}
 
@@ -587,8 +585,9 @@ export class DriveService {
 			sensitive ?? false
 			: false;
 
+		if (user && this.utilityService.isMediaSilencedHost(this.meta.mediaSilencedHosts, user.host)) file.isSensitive = true;
 		if (info.sensitive && profile!.autoSensitive) file.isSensitive = true;
-		if (info.sensitive && instance.setSensitiveFlagAutomatically) file.isSensitive = true;
+		if (info.sensitive && this.meta.setSensitiveFlagAutomatically) file.isSensitive = true;
 		if (userRoleNSFW) file.isSensitive = true;
 
 		if (url !== null) {
@@ -649,7 +648,7 @@ export class DriveService {
 			// ローカルユーザーのみ
 			this.perUserDriveChart.update(file, true);
 		} else {
-			if ((await this.metaService.fetch()).enableChartsForFederatedInstances) {
+			if (this.meta.enableChartsForFederatedInstances) {
 				this.instanceChart.updateDrive(file, true);
 			}
 		}
@@ -795,7 +794,7 @@ export class DriveService {
 			// ローカルユーザーのみ
 			this.perUserDriveChart.update(file, false);
 		} else {
-			if ((await this.metaService.fetch()).enableChartsForFederatedInstances) {
+			if (this.meta.enableChartsForFederatedInstances) {
 				this.instanceChart.updateDrive(file, false);
 			}
 		}
@@ -817,14 +816,13 @@ export class DriveService {
 
 	@bindThis
 	public async deleteObjectStorageFile(key: string) {
-		const meta = await this.metaService.fetch();
 		try {
 			const param = {
-				Bucket: meta.objectStorageBucket,
+				Bucket: this.meta.objectStorageBucket,
 				Key: key,
 			} as DeleteObjectCommandInput;
 
-			await this.s3Service.delete(meta, param);
+			await this.s3Service.delete(this.meta, param);
 		} catch (err: any) {
 			if (err.name === 'NoSuchKey') {
 				this.deleteLogger.warn(`The object storage had no such key to delete: ${key}. Skipping this.`, err as Error);
diff --git a/packages/backend/src/core/EmailService.ts b/packages/backend/src/core/EmailService.ts
index 08f8f80a6e..a176474b95 100644
--- a/packages/backend/src/core/EmailService.ts
+++ b/packages/backend/src/core/EmailService.ts
@@ -5,14 +5,14 @@
 
 import { URLSearchParams } from 'node:url';
 import * as nodemailer from 'nodemailer';
+import juice from 'juice';
 import { Inject, Injectable } from '@nestjs/common';
 import { validate as validateEmail } from 'deep-email-validator';
-import { MetaService } from '@/core/MetaService.js';
 import { UtilityService } from '@/core/UtilityService.js';
 import { DI } from '@/di-symbols.js';
 import type { Config } from '@/config.js';
 import type Logger from '@/logger.js';
-import type { UserProfilesRepository } from '@/models/_.js';
+import type { MiMeta, UserProfilesRepository } from '@/models/_.js';
 import { LoggerService } from '@/core/LoggerService.js';
 import { bindThis } from '@/decorators.js';
 import { HttpRequestService } from '@/core/HttpRequestService.js';
@@ -25,10 +25,12 @@ export class EmailService {
 		@Inject(DI.config)
 		private config: Config,
 
+		@Inject(DI.meta)
+		private meta: MiMeta,
+
 		@Inject(DI.userProfilesRepository)
 		private userProfilesRepository: UserProfilesRepository,
 
-		private metaService: MetaService,
 		private loggerService: LoggerService,
 		private utilityService: UtilityService,
 		private httpRequestService: HttpRequestService,
@@ -38,35 +40,26 @@ export class EmailService {
 
 	@bindThis
 	public async sendEmail(to: string, subject: string, html: string, text: string) {
-		const meta = await this.metaService.fetch(true);
-
-		if (!meta.enableEmail) return;
+		if (!this.meta.enableEmail) return;
 
 		const iconUrl = `${this.config.url}/static-assets/mi-white.png`;
 		const emailSettingUrl = `${this.config.url}/settings/email`;
 
-		const enableAuth = meta.smtpUser != null && meta.smtpUser !== '';
+		const enableAuth = this.meta.smtpUser != null && this.meta.smtpUser !== '';
 
 		const transporter = nodemailer.createTransport({
-			host: meta.smtpHost,
-			port: meta.smtpPort,
-			secure: meta.smtpSecure,
+			host: this.meta.smtpHost,
+			port: this.meta.smtpPort,
+			secure: this.meta.smtpSecure,
 			ignoreTLS: !enableAuth,
 			proxy: this.config.proxySmtp,
 			auth: enableAuth ? {
-				user: meta.smtpUser,
-				pass: meta.smtpPass,
+				user: this.meta.smtpUser,
+				pass: this.meta.smtpPass,
 			} : undefined,
 		} as any);
 
-		try {
-			// TODO: htmlサニタイズ
-			const info = await transporter.sendMail({
-				from: meta.email!,
-				to: to,
-				subject: subject,
-				text: text,
-				html: `<!doctype html>
+		const htmlContent = `<!doctype html>
 <html>
 	<head>
 		<meta charset="utf-8">
@@ -131,7 +124,7 @@ export class EmailService {
 	<body>
 		<main>
 			<header>
-				<img src="${ meta.logoImageUrl ?? meta.iconUrl ?? iconUrl }"/>
+				<img src="${ this.meta.logoImageUrl ?? this.meta.iconUrl ?? iconUrl }"/>
 			</header>
 			<article>
 				<h1>${ subject }</h1>
@@ -145,7 +138,18 @@ export class EmailService {
 			<a href="${ this.config.url }">${ this.config.host }</a>
 		</nav>
 	</body>
-</html>`,
+</html>`;
+
+		const inlinedHtml = juice(htmlContent);
+
+		try {
+			// TODO: htmlサニタイズ
+			const info = await transporter.sendMail({
+				from: this.meta.email!,
+				to: to,
+				subject: subject,
+				text: text,
+				html: inlinedHtml,
 			});
 
 			this.logger.info(`Message sent: ${info.messageId}`);
@@ -160,8 +164,6 @@ export class EmailService {
 		available: boolean;
 		reason: null | 'used' | 'format' | 'disposable' | 'mx' | 'smtp' | 'banned' | 'network' | 'blacklist';
 	}> {
-		const meta = await this.metaService.fetch();
-
 		const exist = await this.userProfilesRepository.countBy({
 			emailVerified: true,
 			email: emailAddress,
@@ -179,11 +181,11 @@ export class EmailService {
 			reason?: string | null,
 		} = { valid: true, reason: null };
 
-		if (meta.enableActiveEmailValidation) {
-			if (meta.enableVerifymailApi && meta.verifymailAuthKey != null) {
-				validated = await this.verifyMail(emailAddress, meta.verifymailAuthKey);
-			} else if (meta.enableTruemailApi && meta.truemailInstance && meta.truemailAuthKey != null) {
-				validated = await this.trueMail(meta.truemailInstance, emailAddress, meta.truemailAuthKey);
+		if (this.meta.enableActiveEmailValidation) {
+			if (this.meta.enableVerifymailApi && this.meta.verifymailAuthKey != null) {
+				validated = await this.verifyMail(emailAddress, this.meta.verifymailAuthKey);
+			} else if (this.meta.enableTruemailApi && this.meta.truemailInstance && this.meta.truemailAuthKey != null) {
+				validated = await this.trueMail(this.meta.truemailInstance, emailAddress, this.meta.truemailAuthKey);
 			} else {
 				validated = await validateEmail({
 					email: emailAddress,
@@ -213,7 +215,7 @@ export class EmailService {
 		}
 
 		const emailDomain: string = emailAddress.split('@')[1];
-		const isBanned = this.utilityService.isBlockedHost(meta.bannedEmailDomains, emailDomain);
+		const isBanned = this.utilityService.isBlockedHost(this.meta.bannedEmailDomains, emailDomain);
 
 		if (isBanned) {
 			return {
diff --git a/packages/backend/src/core/FanoutTimelineEndpointService.ts b/packages/backend/src/core/FanoutTimelineEndpointService.ts
index d5058f37c2..b05af99c5e 100644
--- a/packages/backend/src/core/FanoutTimelineEndpointService.ts
+++ b/packages/backend/src/core/FanoutTimelineEndpointService.ts
@@ -55,9 +55,6 @@ export class FanoutTimelineEndpointService {
 
 	@bindThis
 	private async getMiNotes(ps: TimelineOptions): Promise<MiNote[]> {
-		let noteIds: string[];
-		let shouldFallbackToDb = false;
-
 		// 呼び出し元と以下の処理をシンプルにするためにdbFallbackを置き換える
 		if (!ps.useDbFallback) ps.dbFallback = () => Promise.resolve([]);
 
@@ -67,12 +64,11 @@ export class FanoutTimelineEndpointService {
 		const redisResult = await this.fanoutTimelineService.getMulti(ps.redisTimelines, ps.untilId, ps.sinceId);
 
 		// TODO: いい感じにgetMulti内でソート済だからuniqするときにredisResultが全てソート済なのを利用して再ソートを避けたい
-		const redisResultIds = Array.from(new Set(redisResult.flat(1)));
+		const redisResultIds = Array.from(new Set(redisResult.flat(1))).sort(idCompare);
 
-		redisResultIds.sort(idCompare);
-		noteIds = redisResultIds.slice(0, ps.limit);
-
-		shouldFallbackToDb = shouldFallbackToDb || (noteIds.length === 0);
+		let noteIds = redisResultIds.slice(0, ps.limit);
+		const oldestNoteId = ascending ? redisResultIds[0] : redisResultIds[redisResultIds.length - 1];
+		const shouldFallbackToDb = noteIds.length === 0 || ps.sinceId != null && ps.sinceId < oldestNoteId;
 
 		if (!shouldFallbackToDb) {
 			let filter = ps.noteFilter ?? (_note => true);
diff --git a/packages/backend/src/core/FederatedInstanceService.ts b/packages/backend/src/core/FederatedInstanceService.ts
index 6799f2c5bb..73bbf03b26 100644
--- a/packages/backend/src/core/FederatedInstanceService.ts
+++ b/packages/backend/src/core/FederatedInstanceService.ts
@@ -40,13 +40,14 @@ export class FederatedInstanceService implements OnApplicationShutdown {
 					firstRetrievedAt: new Date(parsed.firstRetrievedAt),
 					latestRequestReceivedAt: parsed.latestRequestReceivedAt ? new Date(parsed.latestRequestReceivedAt) : null,
 					infoUpdatedAt: parsed.infoUpdatedAt ? new Date(parsed.infoUpdatedAt) : null,
+					notRespondingSince: parsed.notRespondingSince ? new Date(parsed.notRespondingSince) : null,
 				};
 			},
 		});
 	}
 
 	@bindThis
-	public async fetch(host: string): Promise<MiInstance> {
+	public async fetchOrRegister(host: string): Promise<MiInstance> {
 		host = this.utilityService.toPuny(host);
 
 		const cached = await this.federatedInstanceCache.get(host);
@@ -69,6 +70,24 @@ export class FederatedInstanceService implements OnApplicationShutdown {
 		}
 	}
 
+	@bindThis
+	public async fetch(host: string): Promise<MiInstance | null> {
+		host = this.utilityService.toPuny(host);
+
+		const cached = await this.federatedInstanceCache.get(host);
+		if (cached !== undefined) return cached;
+
+		const index = await this.instancesRepository.findOneBy({ host });
+
+		if (index == null) {
+			this.federatedInstanceCache.set(host, null);
+			return null;
+		} else {
+			this.federatedInstanceCache.set(host, index);
+			return index;
+		}
+	}
+
 	@bindThis
 	public async update(id: MiInstance['id'], data: Partial<MiInstance>): Promise<void> {
 		const result = await this.instancesRepository.createQueryBuilder().update()
diff --git a/packages/backend/src/core/FetchInstanceMetadataService.ts b/packages/backend/src/core/FetchInstanceMetadataService.ts
index aa16468ecb..987999bce7 100644
--- a/packages/backend/src/core/FetchInstanceMetadataService.ts
+++ b/packages/backend/src/core/FetchInstanceMetadataService.ts
@@ -82,7 +82,7 @@ export class FetchInstanceMetadataService {
 
 		try {
 			if (!force) {
-				const _instance = await this.federatedInstanceService.fetch(host);
+				const _instance = await this.federatedInstanceService.fetchOrRegister(host);
 				const now = Date.now();
 				if (_instance && _instance.infoUpdatedAt && (now - _instance.infoUpdatedAt.getTime() < 1000 * 60 * 60 * 24)) {
 					// unlock at the finally caluse
diff --git a/packages/backend/src/core/FileInfoService.ts b/packages/backend/src/core/FileInfoService.ts
index 169285f033..6bd6cb8d9b 100644
--- a/packages/backend/src/core/FileInfoService.ts
+++ b/packages/backend/src/core/FileInfoService.ts
@@ -15,7 +15,7 @@ import isSvg from 'is-svg';
 import probeImageSize from 'probe-image-size';
 import { type predictionType } from 'nsfwjs';
 import { sharpBmp } from '@misskey-dev/sharp-read-bmp';
-import { encode } from 'blurhash';
+import * as blurhash from 'blurhash';
 import { createTempDir } from '@/misc/create-temp.js';
 import { AiService } from '@/core/AiService.js';
 import { LoggerService } from '@/core/LoggerService.js';
@@ -452,7 +452,7 @@ export class FileInfoService {
 	}
 
 	/**
-	 * Calculate average color of image
+	 * Calculate blurhash string of image
 	 */
 	@bindThis
 	private getBlurhash(path: string, type: string): Promise<string> {
@@ -467,7 +467,7 @@ export class FileInfoService {
 					let hash;
 
 					try {
-						hash = encode(new Uint8ClampedArray(buffer), info.width, info.height, 5, 5);
+						hash = blurhash.encode(new Uint8ClampedArray(buffer), info.width, info.height, 5, 5);
 					} catch (e) {
 						return reject(e);
 					}
diff --git a/packages/backend/src/core/FlashService.ts b/packages/backend/src/core/FlashService.ts
new file mode 100644
index 0000000000..2a98225382
--- /dev/null
+++ b/packages/backend/src/core/FlashService.ts
@@ -0,0 +1,40 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Inject, Injectable } from '@nestjs/common';
+import { DI } from '@/di-symbols.js';
+import { type FlashsRepository } from '@/models/_.js';
+
+/**
+ * MisskeyPlay関係のService
+ */
+@Injectable()
+export class FlashService {
+	constructor(
+		@Inject(DI.flashsRepository)
+		private flashRepository: FlashsRepository,
+	) {
+	}
+
+	/**
+	 * 人気のあるPlay一覧を取得する.
+	 */
+	public async featured(opts?: { offset?: number, limit: number }) {
+		const builder = this.flashRepository.createQueryBuilder('flash')
+			.andWhere('flash.likedCount > 0')
+			.andWhere('flash.visibility = :visibility', { visibility: 'public' })
+			.addOrderBy('flash.likedCount', 'DESC')
+			.addOrderBy('flash.updatedAt', 'DESC')
+			.addOrderBy('flash.id', 'DESC');
+
+		if (opts?.offset) {
+			builder.skip(opts.offset);
+		}
+
+		builder.take(opts?.limit ?? 10);
+
+		return await builder.getMany();
+	}
+}
diff --git a/packages/backend/src/core/GlobalEventService.ts b/packages/backend/src/core/GlobalEventService.ts
index 90efd63f3a..03646ff566 100644
--- a/packages/backend/src/core/GlobalEventService.ts
+++ b/packages/backend/src/core/GlobalEventService.ts
@@ -18,6 +18,7 @@ import type { MiAbuseUserReport } from '@/models/AbuseUserReport.js';
 import type { MiSignin } from '@/models/Signin.js';
 import type { MiPage } from '@/models/Page.js';
 import type { MiWebhook } from '@/models/Webhook.js';
+import type { MiSystemWebhook } from '@/models/SystemWebhook.js';
 import type { MiMeta } from '@/models/Meta.js';
 import { MiAvatarDecoration, MiReversiGame, MiRole, MiRoleAssignment } from '@/models/_.js';
 import type { Packed } from '@/misc/json-schema.js';
@@ -208,6 +209,10 @@ type SerializedAll<T> = {
 	[K in keyof T]: Serialized<T[K]>;
 };
 
+type UndefinedAsNullAll<T> = {
+	[K in keyof T]: T[K] extends undefined ? null : T[K];
+}
+
 export interface InternalEventTypes {
 	userChangeSuspendedState: { id: MiUser['id']; isSuspended: MiUser['isSuspended']; };
 	userChangeDeletedState: { id: MiUser['id']; isDeleted: MiUser['isDeleted']; };
@@ -227,13 +232,16 @@ export interface InternalEventTypes {
 	webhookCreated: MiWebhook;
 	webhookDeleted: MiWebhook;
 	webhookUpdated: MiWebhook;
+	systemWebhookCreated: MiSystemWebhook;
+	systemWebhookDeleted: MiSystemWebhook;
+	systemWebhookUpdated: MiSystemWebhook;
 	antennaCreated: MiAntenna;
 	antennaDeleted: MiAntenna;
 	antennaUpdated: MiAntenna;
 	avatarDecorationCreated: MiAvatarDecoration;
 	avatarDecorationDeleted: MiAvatarDecoration;
 	avatarDecorationUpdated: MiAvatarDecoration;
-	metaUpdated: MiMeta;
+	metaUpdated: { before?: MiMeta; after: MiMeta; };
 	followChannel: { userId: MiUser['id']; channelId: MiChannel['id']; };
 	unfollowChannel: { userId: MiUser['id']; channelId: MiChannel['id']; };
 	updateUserProfile: MiUserProfile;
@@ -243,43 +251,45 @@ export interface InternalEventTypes {
 	userListMemberRemoved: { userListId: MiUserList['id']; memberId: MiUser['id']; };
 }
 
+type EventTypesToEventPayload<T> = EventUnionFromDictionary<UndefinedAsNullAll<SerializedAll<T>>>;
+
 // name/messages(spec) pairs dictionary
 export type GlobalEvents = {
 	internal: {
 		name: 'internal';
-		payload: EventUnionFromDictionary<SerializedAll<InternalEventTypes>>;
+		payload: EventTypesToEventPayload<InternalEventTypes>;
 	};
 	broadcast: {
 		name: 'broadcast';
-		payload: EventUnionFromDictionary<SerializedAll<BroadcastTypes>>;
+		payload: EventTypesToEventPayload<BroadcastTypes>;
 	};
 	main: {
 		name: `mainStream:${MiUser['id']}`;
-		payload: EventUnionFromDictionary<SerializedAll<MainEventTypes>>;
+		payload: EventTypesToEventPayload<MainEventTypes>;
 	};
 	drive: {
 		name: `driveStream:${MiUser['id']}`;
-		payload: EventUnionFromDictionary<SerializedAll<DriveEventTypes>>;
+		payload: EventTypesToEventPayload<DriveEventTypes>;
 	};
 	note: {
 		name: `noteStream:${MiNote['id']}`;
-		payload: EventUnionFromDictionary<SerializedAll<NoteStreamEventTypes>>;
+		payload: EventTypesToEventPayload<NoteStreamEventTypes>;
 	};
 	userList: {
 		name: `userListStream:${MiUserList['id']}`;
-		payload: EventUnionFromDictionary<SerializedAll<UserListEventTypes>>;
+		payload: EventTypesToEventPayload<UserListEventTypes>;
 	};
 	roleTimeline: {
 		name: `roleTimelineStream:${MiRole['id']}`;
-		payload: EventUnionFromDictionary<SerializedAll<RoleTimelineEventTypes>>;
+		payload: EventTypesToEventPayload<RoleTimelineEventTypes>;
 	};
 	antenna: {
 		name: `antennaStream:${MiAntenna['id']}`;
-		payload: EventUnionFromDictionary<SerializedAll<AntennaEventTypes>>;
+		payload: EventTypesToEventPayload<AntennaEventTypes>;
 	};
 	admin: {
 		name: `adminStream:${MiUser['id']}`;
-		payload: EventUnionFromDictionary<SerializedAll<AdminEventTypes>>;
+		payload: EventTypesToEventPayload<AdminEventTypes>;
 	};
 	notes: {
 		name: 'notesStream';
@@ -287,11 +297,11 @@ export type GlobalEvents = {
 	};
 	reversi: {
 		name: `reversiStream:${MiUser['id']}`;
-		payload: EventUnionFromDictionary<SerializedAll<ReversiEventTypes>>;
+		payload: EventTypesToEventPayload<ReversiEventTypes>;
 	};
 	reversiGame: {
 		name: `reversiGameStream:${MiReversiGame['id']}`;
-		payload: EventUnionFromDictionary<SerializedAll<ReversiGameEventTypes>>;
+		payload: EventTypesToEventPayload<ReversiGameEventTypes>;
 	};
 };
 
diff --git a/packages/backend/src/core/HashtagService.ts b/packages/backend/src/core/HashtagService.ts
index eb192ee6da..793bbeecb1 100644
--- a/packages/backend/src/core/HashtagService.ts
+++ b/packages/backend/src/core/HashtagService.ts
@@ -10,16 +10,18 @@ import type { MiUser } from '@/models/User.js';
 import { normalizeForSearch } from '@/misc/normalize-for-search.js';
 import { IdService } from '@/core/IdService.js';
 import type { MiHashtag } from '@/models/Hashtag.js';
-import type { HashtagsRepository } from '@/models/_.js';
+import type { HashtagsRepository, MiMeta } from '@/models/_.js';
 import { UserEntityService } from '@/core/entities/UserEntityService.js';
 import { bindThis } from '@/decorators.js';
 import { FeaturedService } from '@/core/FeaturedService.js';
-import { MetaService } from '@/core/MetaService.js';
 import { UtilityService } from '@/core/UtilityService.js';
 
 @Injectable()
 export class HashtagService {
 	constructor(
+		@Inject(DI.meta)
+		private meta: MiMeta,
+
 		@Inject(DI.redis)
 		private redisClient: Redis.Redis, // TODO: 専用のRedisサーバーを設定できるようにする
 
@@ -29,7 +31,6 @@ export class HashtagService {
 		private userEntityService: UserEntityService,
 		private featuredService: FeaturedService,
 		private idService: IdService,
-		private metaService: MetaService,
 		private utilityService: UtilityService,
 	) {
 	}
@@ -160,10 +161,9 @@ export class HashtagService {
 
 	@bindThis
 	public async updateHashtagsRanking(hashtag: string, userId: MiUser['id']): Promise<void> {
-		const instance = await this.metaService.fetch();
-		const hiddenTags = instance.hiddenTags.map(t => normalizeForSearch(t));
+		const hiddenTags = this.meta.hiddenTags.map(t => normalizeForSearch(t));
 		if (hiddenTags.includes(hashtag)) return;
-		if (this.utilityService.isKeyWordIncluded(hashtag, instance.sensitiveWords)) return;
+		if (this.utilityService.isKeyWordIncluded(hashtag, this.meta.sensitiveWords)) return;
 
 		// YYYYMMDDHHmm (10分間隔)
 		const now = new Date();
diff --git a/packages/backend/src/core/LoggerService.ts b/packages/backend/src/core/LoggerService.ts
index 96d9b09992..f102461a50 100644
--- a/packages/backend/src/core/LoggerService.ts
+++ b/packages/backend/src/core/LoggerService.ts
@@ -15,7 +15,7 @@ export class LoggerService {
 	}
 
 	@bindThis
-	public getLogger(domain: string, color?: KEYWORD | undefined, store?: boolean) {
-		return new Logger(domain, color, store);
+	public getLogger(domain: string, color?: KEYWORD | undefined) {
+		return new Logger(domain, color);
 	}
 }
diff --git a/packages/backend/src/core/MetaService.ts b/packages/backend/src/core/MetaService.ts
index ec630f804e..3d88d0aefe 100644
--- a/packages/backend/src/core/MetaService.ts
+++ b/packages/backend/src/core/MetaService.ts
@@ -52,7 +52,7 @@ export class MetaService implements OnApplicationShutdown {
 			switch (type) {
 				case 'metaUpdated': {
 					this.cache = { // TODO: このあたりのデシリアライズ処理は各modelファイル内に関数としてexportしたい
-						...body,
+						...(body.after),
 						proxyAccount: null, // joinなカラムは通常取ってこないので
 					};
 					break;
@@ -141,7 +141,7 @@ export class MetaService implements OnApplicationShutdown {
 			});
 		}
 
-		this.globalEventService.publishInternalEvent('metaUpdated', updated);
+		this.globalEventService.publishInternalEvent('metaUpdated', { before, after: updated });
 
 		return updated;
 	}
diff --git a/packages/backend/src/core/MfmService.ts b/packages/backend/src/core/MfmService.ts
index 9786f8b8bb..edfb3aa4fc 100644
--- a/packages/backend/src/core/MfmService.ts
+++ b/packages/backend/src/core/MfmService.ts
@@ -13,10 +13,12 @@ import { intersperse } from '@/misc/prelude/array.js';
 import { normalizeForSearch } from '@/misc/normalize-for-search.js';
 import type { IMentionedRemoteUsers } from '@/models/Note.js';
 import { bindThis } from '@/decorators.js';
-import * as TreeAdapter from '../../node_modules/parse5/dist/tree-adapters/default.js';
+import type { DefaultTreeAdapterMap } from 'parse5';
 import type * as mfm from 'mfm-js';
 
-const treeAdapter = TreeAdapter.defaultTreeAdapter;
+const treeAdapter = parse5.defaultTreeAdapter;
+type Node = DefaultTreeAdapterMap['node'];
+type ChildNode = DefaultTreeAdapterMap['childNode'];
 
 const urlRegex = /^https?:\/\/[\w\/:%#@$&?!()\[\]~.,=+\-]+/;
 const urlRegexFull = /^https?:\/\/[\w\/:%#@$&?!()\[\]~.,=+\-]+$/;
@@ -46,7 +48,7 @@ export class MfmService {
 
 		return text.trim();
 
-		function getText(node: TreeAdapter.Node): string {
+		function getText(node: Node): string {
 			if (treeAdapter.isTextNode(node)) return node.value;
 			if (!treeAdapter.isElementNode(node)) return '';
 			if (node.nodeName === 'br') return '\n';
@@ -58,7 +60,7 @@ export class MfmService {
 			return '';
 		}
 
-		function appendChildren(childNodes: TreeAdapter.ChildNode[]): void {
+		function appendChildren(childNodes: ChildNode[]): void {
 			if (childNodes) {
 				for (const n of childNodes) {
 					analyze(n);
@@ -66,14 +68,16 @@ export class MfmService {
 			}
 		}
 
-		function analyze(node: TreeAdapter.Node) {
+		function analyze(node: Node) {
 			if (treeAdapter.isTextNode(node)) {
 				text += node.value;
 				return;
 			}
 
 			// Skip comment or document type node
-			if (!treeAdapter.isElementNode(node)) return;
+			if (!treeAdapter.isElementNode(node)) {
+				return;
+			}
 
 			switch (node.nodeName) {
 				case 'br': {
@@ -81,8 +85,7 @@ export class MfmService {
 					break;
 				}
 
-				case 'a':
-				{
+				case 'a': {
 					const txt = getText(node);
 					const rel = node.attrs.find(x => x.name === 'rel');
 					const href = node.attrs.find(x => x.name === 'href');
@@ -90,7 +93,7 @@ export class MfmService {
 					// ハッシュタグ
 					if (normalizedHashtagNames && href && normalizedHashtagNames.has(normalizeForSearch(txt))) {
 						text += txt;
-					// メンション
+						// メンション
 					} else if (txt.startsWith('@') && !(rel && rel.value.startsWith('me '))) {
 						const part = txt.split('@');
 
@@ -102,7 +105,7 @@ export class MfmService {
 						} else if (part.length === 3) {
 							text += txt;
 						}
-					// その他
+						// その他
 					} else {
 						const generateLink = () => {
 							if (!href && !txt) {
@@ -130,8 +133,7 @@ export class MfmService {
 					break;
 				}
 
-				case 'h1':
-				{
+				case 'h1': {
 					text += '【';
 					appendChildren(node.childNodes);
 					text += '】\n';
@@ -139,16 +141,14 @@ export class MfmService {
 				}
 
 				case 'b':
-				case 'strong':
-				{
+				case 'strong': {
 					text += '**';
 					appendChildren(node.childNodes);
 					text += '**';
 					break;
 				}
 
-				case 'small':
-				{
+				case 'small': {
 					text += '<small>';
 					appendChildren(node.childNodes);
 					text += '</small>';
@@ -156,8 +156,7 @@ export class MfmService {
 				}
 
 				case 's':
-				case 'del':
-				{
+				case 'del': {
 					text += '~~';
 					appendChildren(node.childNodes);
 					text += '~~';
@@ -165,8 +164,7 @@ export class MfmService {
 				}
 
 				case 'i':
-				case 'em':
-				{
+				case 'em': {
 					text += '<i>';
 					appendChildren(node.childNodes);
 					text += '</i>';
@@ -207,8 +205,7 @@ export class MfmService {
 				case 'h3':
 				case 'h4':
 				case 'h5':
-				case 'h6':
-				{
+				case 'h6': {
 					text += '\n\n';
 					appendChildren(node.childNodes);
 					break;
@@ -221,8 +218,7 @@ export class MfmService {
 				case 'article':
 				case 'li':
 				case 'dt':
-				case 'dd':
-				{
+				case 'dd': {
 					text += '\n';
 					appendChildren(node.childNodes);
 					break;
@@ -243,7 +239,7 @@ export class MfmService {
 			return null;
 		}
 
-		const { window } = new Window();
+		const { happyDOM, window } = new Window();
 
 		const doc = window.document;
 
@@ -410,7 +406,7 @@ export class MfmService {
 			mention: (node) => {
 				const a = doc.createElement('a');
 				const { username, host, acct } = node.props;
-				const remoteUserInfo = mentionedRemoteUsers.find(remoteUser => remoteUser.username === username && remoteUser.host === host);
+				const remoteUserInfo = mentionedRemoteUsers.find(remoteUser => remoteUser.username.toLowerCase() === username.toLowerCase() && remoteUser.host?.toLowerCase() === host?.toLowerCase());
 				a.setAttribute('href', remoteUserInfo ? (remoteUserInfo.url ? remoteUserInfo.url : remoteUserInfo.uri) : `${this.config.url}/${acct}`);
 				a.className = 'u-url mention';
 				a.textContent = acct;
@@ -461,6 +457,10 @@ export class MfmService {
 
 		appendChildren(nodes, body);
 
-		return new XMLSerializer().serializeToString(body);
+		const serialized = new XMLSerializer().serializeToString(body);
+
+		happyDOM.close().catch(err => {});
+
+		return serialized;
 	}
 }
diff --git a/packages/backend/src/core/ModerationLogService.ts b/packages/backend/src/core/ModerationLogService.ts
index 6c155c9a62..2c02af217d 100644
--- a/packages/backend/src/core/ModerationLogService.ts
+++ b/packages/backend/src/core/ModerationLogService.ts
@@ -9,7 +9,8 @@ import type { ModerationLogsRepository } from '@/models/_.js';
 import type { MiUser } from '@/models/User.js';
 import { IdService } from '@/core/IdService.js';
 import { bindThis } from '@/decorators.js';
-import { ModerationLogPayloads, moderationLogTypes } from '@/types.js';
+import type { ModerationLogPayloads } from '@/types.js';
+import { moderationLogTypes } from '@/types.js';
 
 @Injectable()
 export class ModerationLogService {
diff --git a/packages/backend/src/core/NoteCreateService.ts b/packages/backend/src/core/NoteCreateService.ts
index e5580f36d1..3647fa7231 100644
--- a/packages/backend/src/core/NoteCreateService.ts
+++ b/packages/backend/src/core/NoteCreateService.ts
@@ -8,13 +8,12 @@ import * as mfm from 'mfm-js';
 import { In, DataSource, IsNull, LessThan } from 'typeorm';
 import * as Redis from 'ioredis';
 import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
-import RE2 from 're2';
 import { extractMentions } from '@/misc/extract-mentions.js';
 import { extractCustomEmojisFromMfm } from '@/misc/extract-custom-emojis-from-mfm.js';
 import { extractHashtags } from '@/misc/extract-hashtags.js';
 import type { IMentionedRemoteUsers } from '@/models/Note.js';
 import { MiNote } from '@/models/Note.js';
-import type { ChannelFollowingsRepository, ChannelsRepository, FollowingsRepository, InstancesRepository, MiFollowing, MutingsRepository, NotesRepository, NoteThreadMutingsRepository, UserListMembershipsRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js';
+import type { ChannelFollowingsRepository, ChannelsRepository, FollowingsRepository, InstancesRepository, MiFollowing, MiMeta, MutingsRepository, NotesRepository, NoteThreadMutingsRepository, UserListMembershipsRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js';
 import type { MiDriveFile } from '@/models/DriveFile.js';
 import type { MiApp } from '@/models/App.js';
 import { concat } from '@/misc/prelude/array.js';
@@ -23,11 +22,8 @@ import type { MiUser, MiLocalUser, MiRemoteUser } from '@/models/User.js';
 import type { IPoll } from '@/models/Poll.js';
 import { MiPoll } from '@/models/Poll.js';
 import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error.js';
-import { checkWordMute } from '@/misc/check-word-mute.js';
 import type { MiChannel } from '@/models/Channel.js';
 import { normalizeForSearch } from '@/misc/normalize-for-search.js';
-import { MemorySingleCache } from '@/misc/cache.js';
-import type { MiUserProfile } from '@/models/UserProfile.js';
 import { RelayService } from '@/core/RelayService.js';
 import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
 import { DI } from '@/di-symbols.js';
@@ -38,7 +34,7 @@ import InstanceChart from '@/core/chart/charts/instance.js';
 import ActiveUsersChart from '@/core/chart/charts/active-users.js';
 import { GlobalEventService } from '@/core/GlobalEventService.js';
 import { NotificationService } from '@/core/NotificationService.js';
-import { WebhookService } from '@/core/WebhookService.js';
+import { UserWebhookService } from '@/core/UserWebhookService.js';
 import { HashtagService } from '@/core/HashtagService.js';
 import { AntennaService } from '@/core/AntennaService.js';
 import { QueueService } from '@/core/QueueService.js';
@@ -51,7 +47,6 @@ import { RemoteUserResolveService } from '@/core/RemoteUserResolveService.js';
 import { bindThis } from '@/decorators.js';
 import { DB_MAX_NOTE_TEXT_LENGTH } from '@/const.js';
 import { RoleService } from '@/core/RoleService.js';
-import { MetaService } from '@/core/MetaService.js';
 import { SearchService } from '@/core/SearchService.js';
 import { FeaturedService } from '@/core/FeaturedService.js';
 import { FanoutTimelineService } from '@/core/FanoutTimelineService.js';
@@ -59,8 +54,8 @@ import { UtilityService } from '@/core/UtilityService.js';
 import { UserBlockingService } from '@/core/UserBlockingService.js';
 import { isReply } from '@/misc/is-reply.js';
 import { trackPromise } from '@/misc/promise-tracker.js';
-import { isNotNull } from '@/misc/is-not-null.js';
 import { IdentifiableError } from '@/misc/identifiable-error.js';
+import { CollapsedQueue } from '@/misc/collapsed-queue.js';
 
 type NotificationType = 'reply' | 'renote' | 'quote' | 'mention';
 
@@ -152,11 +147,15 @@ type Option = {
 @Injectable()
 export class NoteCreateService implements OnApplicationShutdown {
 	#shutdownController = new AbortController();
+	private updateNotesCountQueue: CollapsedQueue<MiNote['id'], number>;
 
 	constructor(
 		@Inject(DI.config)
 		private config: Config,
 
+		@Inject(DI.meta)
+		private meta: MiMeta,
+
 		@Inject(DI.db)
 		private db: DataSource,
 
@@ -205,13 +204,12 @@ export class NoteCreateService implements OnApplicationShutdown {
 		private federatedInstanceService: FederatedInstanceService,
 		private hashtagService: HashtagService,
 		private antennaService: AntennaService,
-		private webhookService: WebhookService,
+		private webhookService: UserWebhookService,
 		private featuredService: FeaturedService,
 		private remoteUserResolveService: RemoteUserResolveService,
 		private apDeliverManagerService: ApDeliverManagerService,
 		private apRendererService: ApRendererService,
 		private roleService: RoleService,
-		private metaService: MetaService,
 		private searchService: SearchService,
 		private notesChart: NotesChart,
 		private perUserNotesChart: PerUserNotesChart,
@@ -219,7 +217,9 @@ export class NoteCreateService implements OnApplicationShutdown {
 		private instanceChart: InstanceChart,
 		private utilityService: UtilityService,
 		private userBlockingService: UserBlockingService,
-	) { }
+	) {
+		this.updateNotesCountQueue = new CollapsedQueue(process.env.NODE_ENV !== 'test' ? 60 * 1000 * 5 : 0, this.collapseNotesCount, this.performUpdateNotesCount);
+	}
 
 	@bindThis
 	public async create(user: {
@@ -252,10 +252,8 @@ export class NoteCreateService implements OnApplicationShutdown {
 		if (data.channel != null) data.visibleUsers = [];
 		if (data.channel != null) data.localOnly = true;
 
-		const meta = await this.metaService.fetch();
-
 		if (data.visibility === 'public' && data.channel == null) {
-			const sensitiveWords = meta.sensitiveWords;
+			const sensitiveWords = this.meta.sensitiveWords;
 			if (this.utilityService.isKeyWordIncluded(data.cw ?? data.text ?? '', sensitiveWords)) {
 				data.visibility = 'home';
 			} else if ((await this.roleService.getUserPolicies(user.id)).canPublicNote === false) {
@@ -263,17 +261,17 @@ export class NoteCreateService implements OnApplicationShutdown {
 			}
 		}
 
-		const hasProhibitedWords = await this.checkProhibitedWordsContain({
+		const hasProhibitedWords = this.checkProhibitedWordsContain({
 			cw: data.cw,
 			text: data.text,
 			pollChoices: data.poll?.choices,
-		}, meta.prohibitedWords);
+		}, this.meta.prohibitedWords);
 
 		if (hasProhibitedWords) {
 			throw new IdentifiableError('689ee33f-f97c-479a-ac49-1b9f8140af99', 'Note contains prohibited words');
 		}
 
-		const inSilencedInstance = this.utilityService.isSilencedHost(meta.silencedHosts, user.host);
+		const inSilencedInstance = this.utilityService.isSilencedHost(this.meta.silencedHosts, user.host);
 
 		if (data.visibility === 'public' && inSilencedInstance && user.host !== null) {
 			data.visibility = 'home';
@@ -365,6 +363,9 @@ export class NoteCreateService implements OnApplicationShutdown {
 			mentionedUsers = data.apMentions ?? await this.extractMentionedUsers(user, combinedTokens);
 		}
 
+		// if the host is media-silenced, custom emojis are not allowed
+		if (this.utilityService.isMediaSilencedHost(this.meta.mediaSilencedHosts, user.host)) emojis = [];
+
 		tags = tags.filter(tag => Array.from(tag).length <= 128).splice(0, 32);
 
 		if (data.reply && (user.id !== data.reply.userId) && !mentionedUsers.some(u => u.id === data.reply!.userId)) {
@@ -504,21 +505,21 @@ export class NoteCreateService implements OnApplicationShutdown {
 		host: MiUser['host'];
 		isBot: MiUser['isBot'];
 	}, data: Option, silent: boolean, tags: string[], mentionedUsers: MinimumUser[]) {
-		const meta = await this.metaService.fetch();
-
 		this.notesChart.update(note, true);
-		if (meta.enableChartsForRemoteUser || (user.host == null)) {
+		if (note.visibility !== 'specified' && (this.meta.enableChartsForRemoteUser || (user.host == null))) {
 			this.perUserNotesChart.update(user, note, true);
 		}
 
 		// Register host
-		if (this.userEntityService.isRemoteUser(user)) {
-			this.federatedInstanceService.fetch(user.host).then(async i => {
-				this.instancesRepository.increment({ id: i.id }, 'notesCount', 1);
-				if ((await this.metaService.fetch()).enableChartsForFederatedInstances) {
-					this.instanceChart.updateNote(i.host, note, true);
-				}
-			});
+		if (this.meta.enableStatsForFederatedInstances) {
+			if (this.userEntityService.isRemoteUser(user)) {
+				this.federatedInstanceService.fetchOrRegister(user.host).then(async i => {
+					this.updateNotesCountQueue.enqueue(i.id, 1);
+					if (this.meta.enableChartsForFederatedInstances) {
+						this.instanceChart.updateNote(i.host, note, true);
+					}
+				});
+			}
 		}
 
 		// ハッシュタグ更新
@@ -606,7 +607,7 @@ export class NoteCreateService implements OnApplicationShutdown {
 			this.webhookService.getActiveWebhooks().then(webhooks => {
 				webhooks = webhooks.filter(x => x.userId === user.id && x.on.includes('note'));
 				for (const webhook of webhooks) {
-					this.queueService.webhookDeliver(webhook, 'note', {
+					this.queueService.userWebhookDeliver(webhook, 'note', {
 						note: noteObj,
 					});
 				}
@@ -633,7 +634,7 @@ export class NoteCreateService implements OnApplicationShutdown {
 
 						const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === data.reply!.userId && x.on.includes('reply'));
 						for (const webhook of webhooks) {
-							this.queueService.webhookDeliver(webhook, 'reply', {
+							this.queueService.userWebhookDeliver(webhook, 'reply', {
 								note: noteObj,
 							});
 						}
@@ -656,7 +657,7 @@ export class NoteCreateService implements OnApplicationShutdown {
 
 					const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === data.renote!.userId && x.on.includes('renote'));
 					for (const webhook of webhooks) {
-						this.queueService.webhookDeliver(webhook, 'renote', {
+						this.queueService.userWebhookDeliver(webhook, 'renote', {
 							note: noteObj,
 						});
 					}
@@ -788,7 +789,7 @@ export class NoteCreateService implements OnApplicationShutdown {
 
 			const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === u.id && x.on.includes('mention'));
 			for (const webhook of webhooks) {
-				this.queueService.webhookDeliver(webhook, 'mention', {
+				this.queueService.userWebhookDeliver(webhook, 'mention', {
 					note: detailPackedNote,
 				});
 			}
@@ -839,7 +840,7 @@ export class NoteCreateService implements OnApplicationShutdown {
 		const mentions = extractMentions(tokens);
 		let mentionedUsers = (await Promise.all(mentions.map(m =>
 			this.remoteUserResolveService.resolveUser(m.username, m.host ?? user.host).catch(() => null),
-		))).filter(isNotNull);
+		))).filter(x => x != null);
 
 		// Drop duplicate users
 		mentionedUsers = mentionedUsers.filter((u, i, self) =>
@@ -851,15 +852,14 @@ export class NoteCreateService implements OnApplicationShutdown {
 
 	@bindThis
 	private async pushToTl(note: MiNote, user: { id: MiUser['id']; host: MiUser['host']; }) {
-		const meta = await this.metaService.fetch();
-		if (!meta.enableFanoutTimeline) return;
+		if (!this.meta.enableFanoutTimeline) return;
 
 		const r = this.redisForTimelines.pipeline();
 
 		if (note.channelId) {
 			this.fanoutTimelineService.push(`channelTimeline:${note.channelId}`, note.id, this.config.perChannelMaxNoteCacheCount, r);
 
-			this.fanoutTimelineService.push(`userTimelineWithChannel:${user.id}`, note.id, note.userHost == null ? meta.perLocalUserUserTimelineCacheMax : meta.perRemoteUserUserTimelineCacheMax, r);
+			this.fanoutTimelineService.push(`userTimelineWithChannel:${user.id}`, note.id, note.userHost == null ? this.meta.perLocalUserUserTimelineCacheMax : this.meta.perRemoteUserUserTimelineCacheMax, r);
 
 			const channelFollowings = await this.channelFollowingsRepository.find({
 				where: {
@@ -869,9 +869,9 @@ export class NoteCreateService implements OnApplicationShutdown {
 			});
 
 			for (const channelFollowing of channelFollowings) {
-				this.fanoutTimelineService.push(`homeTimeline:${channelFollowing.followerId}`, note.id, meta.perUserHomeTimelineCacheMax, r);
+				this.fanoutTimelineService.push(`homeTimeline:${channelFollowing.followerId}`, note.id, this.meta.perUserHomeTimelineCacheMax, r);
 				if (note.fileIds.length > 0) {
-					this.fanoutTimelineService.push(`homeTimelineWithFiles:${channelFollowing.followerId}`, note.id, meta.perUserHomeTimelineCacheMax / 2, r);
+					this.fanoutTimelineService.push(`homeTimelineWithFiles:${channelFollowing.followerId}`, note.id, this.meta.perUserHomeTimelineCacheMax / 2, r);
 				}
 			}
 		} else {
@@ -909,9 +909,9 @@ export class NoteCreateService implements OnApplicationShutdown {
 					if (!following.withReplies) continue;
 				}
 
-				this.fanoutTimelineService.push(`homeTimeline:${following.followerId}`, note.id, meta.perUserHomeTimelineCacheMax, r);
+				this.fanoutTimelineService.push(`homeTimeline:${following.followerId}`, note.id, this.meta.perUserHomeTimelineCacheMax, r);
 				if (note.fileIds.length > 0) {
-					this.fanoutTimelineService.push(`homeTimelineWithFiles:${following.followerId}`, note.id, meta.perUserHomeTimelineCacheMax / 2, r);
+					this.fanoutTimelineService.push(`homeTimelineWithFiles:${following.followerId}`, note.id, this.meta.perUserHomeTimelineCacheMax / 2, r);
 				}
 			}
 
@@ -928,22 +928,25 @@ export class NoteCreateService implements OnApplicationShutdown {
 					if (!userListMembership.withReplies) continue;
 				}
 
-				this.fanoutTimelineService.push(`userListTimeline:${userListMembership.userListId}`, note.id, meta.perUserListTimelineCacheMax, r);
+				this.fanoutTimelineService.push(`userListTimeline:${userListMembership.userListId}`, note.id, this.meta.perUserListTimelineCacheMax, r);
 				if (note.fileIds.length > 0) {
-					this.fanoutTimelineService.push(`userListTimelineWithFiles:${userListMembership.userListId}`, note.id, meta.perUserListTimelineCacheMax / 2, r);
+					this.fanoutTimelineService.push(`userListTimelineWithFiles:${userListMembership.userListId}`, note.id, this.meta.perUserListTimelineCacheMax / 2, r);
 				}
 			}
 
-			if (note.visibility !== 'specified' || !note.visibleUserIds.some(v => v === user.id)) { // 自分自身のHTL
-				this.fanoutTimelineService.push(`homeTimeline:${user.id}`, note.id, meta.perUserHomeTimelineCacheMax, r);
-				if (note.fileIds.length > 0) {
-					this.fanoutTimelineService.push(`homeTimelineWithFiles:${user.id}`, note.id, meta.perUserHomeTimelineCacheMax / 2, r);
+			// 自分自身のHTL
+			if (note.userHost == null) {
+				if (note.visibility !== 'specified' || !note.visibleUserIds.some(v => v === user.id)) {
+					this.fanoutTimelineService.push(`homeTimeline:${user.id}`, note.id, this.meta.perUserHomeTimelineCacheMax, r);
+					if (note.fileIds.length > 0) {
+						this.fanoutTimelineService.push(`homeTimelineWithFiles:${user.id}`, note.id, this.meta.perUserHomeTimelineCacheMax / 2, r);
+					}
 				}
 			}
 
 			// 自分自身以外への返信
 			if (isReply(note)) {
-				this.fanoutTimelineService.push(`userTimelineWithReplies:${user.id}`, note.id, note.userHost == null ? meta.perLocalUserUserTimelineCacheMax : meta.perRemoteUserUserTimelineCacheMax, r);
+				this.fanoutTimelineService.push(`userTimelineWithReplies:${user.id}`, note.id, note.userHost == null ? this.meta.perLocalUserUserTimelineCacheMax : this.meta.perRemoteUserUserTimelineCacheMax, r);
 
 				if (note.visibility === 'public' && note.userHost == null) {
 					this.fanoutTimelineService.push('localTimelineWithReplies', note.id, 300, r);
@@ -952,9 +955,9 @@ export class NoteCreateService implements OnApplicationShutdown {
 					}
 				}
 			} else {
-				this.fanoutTimelineService.push(`userTimeline:${user.id}`, note.id, note.userHost == null ? meta.perLocalUserUserTimelineCacheMax : meta.perRemoteUserUserTimelineCacheMax, r);
+				this.fanoutTimelineService.push(`userTimeline:${user.id}`, note.id, note.userHost == null ? this.meta.perLocalUserUserTimelineCacheMax : this.meta.perRemoteUserUserTimelineCacheMax, r);
 				if (note.fileIds.length > 0) {
-					this.fanoutTimelineService.push(`userTimelineWithFiles:${user.id}`, note.id, note.userHost == null ? meta.perLocalUserUserTimelineCacheMax / 2 : meta.perRemoteUserUserTimelineCacheMax / 2, r);
+					this.fanoutTimelineService.push(`userTimelineWithFiles:${user.id}`, note.id, note.userHost == null ? this.meta.perLocalUserUserTimelineCacheMax / 2 : this.meta.perRemoteUserUserTimelineCacheMax / 2, r);
 				}
 
 				if (note.visibility === 'public' && note.userHost == null) {
@@ -1013,9 +1016,9 @@ export class NoteCreateService implements OnApplicationShutdown {
 		}
 	}
 
-	public async checkProhibitedWordsContain(content: Parameters<UtilityService['concatNoteContentsForKeyWordCheck']>[0], prohibitedWords?: string[]) {
+	public checkProhibitedWordsContain(content: Parameters<UtilityService['concatNoteContentsForKeyWordCheck']>[0], prohibitedWords?: string[]) {
 		if (prohibitedWords == null) {
-			prohibitedWords = (await this.metaService.fetch()).prohibitedWords;
+			prohibitedWords = this.meta.prohibitedWords;
 		}
 
 		if (
@@ -1031,12 +1034,23 @@ export class NoteCreateService implements OnApplicationShutdown {
 	}
 
 	@bindThis
-	public dispose(): void {
-		this.#shutdownController.abort();
+	private collapseNotesCount(oldValue: number, newValue: number) {
+		return oldValue + newValue;
 	}
 
 	@bindThis
-	public onApplicationShutdown(signal?: string | undefined): void {
-		this.dispose();
+	private async performUpdateNotesCount(id: MiNote['id'], incrBy: number) {
+		await this.instancesRepository.increment({ id: id }, 'notesCount', incrBy);
+	}
+
+	@bindThis
+	public async dispose(): Promise<void> {
+		this.#shutdownController.abort();
+		await this.updateNotesCountQueue.performAllNow();
+	}
+
+	@bindThis
+	public async onApplicationShutdown(signal?: string | undefined): Promise<void> {
+		await this.dispose();
 	}
 }
diff --git a/packages/backend/src/core/NoteDeleteService.ts b/packages/backend/src/core/NoteDeleteService.ts
index 801ed02e00..4ecd2592b2 100644
--- a/packages/backend/src/core/NoteDeleteService.ts
+++ b/packages/backend/src/core/NoteDeleteService.ts
@@ -7,7 +7,7 @@ import { Brackets, In } from 'typeorm';
 import { Injectable, Inject } from '@nestjs/common';
 import type { MiUser, MiLocalUser, MiRemoteUser } from '@/models/User.js';
 import type { MiNote, IMentionedRemoteUsers } from '@/models/Note.js';
-import type { InstancesRepository, NotesRepository, UsersRepository } from '@/models/_.js';
+import type { InstancesRepository, MiMeta, NotesRepository, UsersRepository } from '@/models/_.js';
 import { RelayService } from '@/core/RelayService.js';
 import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
 import { DI } from '@/di-symbols.js';
@@ -19,9 +19,7 @@ import { GlobalEventService } from '@/core/GlobalEventService.js';
 import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
 import { ApDeliverManagerService } from '@/core/activitypub/ApDeliverManagerService.js';
 import { UserEntityService } from '@/core/entities/UserEntityService.js';
-import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
 import { bindThis } from '@/decorators.js';
-import { MetaService } from '@/core/MetaService.js';
 import { SearchService } from '@/core/SearchService.js';
 import { ModerationLogService } from '@/core/ModerationLogService.js';
 import { isQuote, isRenote } from '@/misc/is-renote.js';
@@ -32,6 +30,9 @@ export class NoteDeleteService {
 		@Inject(DI.config)
 		private config: Config,
 
+		@Inject(DI.meta)
+		private meta: MiMeta,
+
 		@Inject(DI.usersRepository)
 		private usersRepository: UsersRepository,
 
@@ -42,13 +43,11 @@ export class NoteDeleteService {
 		private instancesRepository: InstancesRepository,
 
 		private userEntityService: UserEntityService,
-		private noteEntityService: NoteEntityService,
 		private globalEventService: GlobalEventService,
 		private relayService: RelayService,
 		private federatedInstanceService: FederatedInstanceService,
 		private apRendererService: ApRendererService,
 		private apDeliverManagerService: ApDeliverManagerService,
-		private metaService: MetaService,
 		private searchService: SearchService,
 		private moderationLogService: ModerationLogService,
 		private notesChart: NotesChart,
@@ -92,7 +91,7 @@ export class NoteDeleteService {
 				this.deliverToConcerned(user, note, content);
 			}
 
-			// also deliever delete activity to cascaded notes
+			// also deliver delete activity to cascaded notes
 			const federatedLocalCascadingNotes = (cascadingNotes).filter(note => !note.localOnly && note.userHost == null); // filter out local-only notes
 			for (const cascadingNote of federatedLocalCascadingNotes) {
 				if (!cascadingNote.user) continue;
@@ -102,20 +101,20 @@ export class NoteDeleteService {
 			}
 			//#endregion
 
-			const meta = await this.metaService.fetch();
-
 			this.notesChart.update(note, false);
-			if (meta.enableChartsForRemoteUser || (user.host == null)) {
+			if (this.meta.enableChartsForRemoteUser || (user.host == null)) {
 				this.perUserNotesChart.update(user, note, false);
 			}
 
-			if (this.userEntityService.isRemoteUser(user)) {
-				this.federatedInstanceService.fetch(user.host).then(async i => {
-					this.instancesRepository.decrement({ id: i.id }, 'notesCount', 1);
-					if ((await this.metaService.fetch()).enableChartsForFederatedInstances) {
-						this.instanceChart.updateNote(i.host, note, false);
-					}
-				});
+			if (this.meta.enableStatsForFederatedInstances) {
+				if (this.userEntityService.isRemoteUser(user)) {
+					this.federatedInstanceService.fetchOrRegister(user.host).then(async i => {
+						this.instancesRepository.decrement({ id: i.id }, 'notesCount', 1);
+						if (this.meta.enableChartsForFederatedInstances) {
+							this.instanceChart.updateNote(i.host, note, false);
+						}
+					});
+				}
 			}
 		}
 
diff --git a/packages/backend/src/core/ProxyAccountService.ts b/packages/backend/src/core/ProxyAccountService.ts
index 71d663bf90..c3ff2a68d3 100644
--- a/packages/backend/src/core/ProxyAccountService.ts
+++ b/packages/backend/src/core/ProxyAccountService.ts
@@ -4,26 +4,25 @@
  */
 
 import { Inject, Injectable } from '@nestjs/common';
-import type { UsersRepository } from '@/models/_.js';
+import type { MiMeta, UsersRepository } from '@/models/_.js';
 import type { MiLocalUser } from '@/models/User.js';
 import { DI } from '@/di-symbols.js';
-import { MetaService } from '@/core/MetaService.js';
 import { bindThis } from '@/decorators.js';
 
 @Injectable()
 export class ProxyAccountService {
 	constructor(
+		@Inject(DI.meta)
+		private meta: MiMeta,
+
 		@Inject(DI.usersRepository)
 		private usersRepository: UsersRepository,
-
-		private metaService: MetaService,
 	) {
 	}
 
 	@bindThis
 	public async fetch(): Promise<MiLocalUser | null> {
-		const meta = await this.metaService.fetch();
-		if (meta.proxyAccountId == null) return null;
-		return await this.usersRepository.findOneByOrFail({ id: meta.proxyAccountId }) as MiLocalUser;
+		if (this.meta.proxyAccountId == null) return null;
+		return await this.usersRepository.findOneByOrFail({ id: this.meta.proxyAccountId }) as MiLocalUser;
 	}
 }
diff --git a/packages/backend/src/core/PushNotificationService.ts b/packages/backend/src/core/PushNotificationService.ts
index 6a845b951d..1479bb00d9 100644
--- a/packages/backend/src/core/PushNotificationService.ts
+++ b/packages/backend/src/core/PushNotificationService.ts
@@ -10,8 +10,7 @@ import { DI } from '@/di-symbols.js';
 import type { Config } from '@/config.js';
 import type { Packed } from '@/misc/json-schema.js';
 import { getNoteSummary } from '@/misc/get-note-summary.js';
-import type { MiSwSubscription, SwSubscriptionsRepository } from '@/models/_.js';
-import { MetaService } from '@/core/MetaService.js';
+import type { MiMeta, MiSwSubscription, SwSubscriptionsRepository } from '@/models/_.js';
 import { bindThis } from '@/decorators.js';
 import { RedisKVCache } from '@/misc/cache.js';
 
@@ -54,13 +53,14 @@ export class PushNotificationService implements OnApplicationShutdown {
 		@Inject(DI.config)
 		private config: Config,
 
+		@Inject(DI.meta)
+		private meta: MiMeta,
+
 		@Inject(DI.redis)
 		private redisClient: Redis.Redis,
 
 		@Inject(DI.swSubscriptionsRepository)
 		private swSubscriptionsRepository: SwSubscriptionsRepository,
-
-		private metaService: MetaService,
 	) {
 		this.subscriptionsCache = new RedisKVCache<MiSwSubscription[]>(this.redisClient, 'userSwSubscriptions', {
 			lifetime: 1000 * 60 * 60 * 1, // 1h
@@ -73,14 +73,12 @@ export class PushNotificationService implements OnApplicationShutdown {
 
 	@bindThis
 	public async pushNotification<T extends keyof PushNotificationsTypes>(userId: string, type: T, body: PushNotificationsTypes[T]) {
-		const meta = await this.metaService.fetch();
-
-		if (!meta.enableServiceWorker || meta.swPublicKey == null || meta.swPrivateKey == null) return;
+		if (!this.meta.enableServiceWorker || this.meta.swPublicKey == null || this.meta.swPrivateKey == null) return;
 
 		// アプリケーションの連絡先と、サーバーサイドの鍵ペアの情報を登録
 		push.setVapidDetails(this.config.url,
-			meta.swPublicKey,
-			meta.swPrivateKey);
+			this.meta.swPublicKey,
+			this.meta.swPrivateKey);
 
 		const subscriptions = await this.subscriptionsCache.fetch(userId);
 
diff --git a/packages/backend/src/core/QueueModule.ts b/packages/backend/src/core/QueueModule.ts
index 216734e9e5..b10b8e5899 100644
--- a/packages/backend/src/core/QueueModule.ts
+++ b/packages/backend/src/core/QueueModule.ts
@@ -7,10 +7,17 @@ import { Inject, Module, OnApplicationShutdown } from '@nestjs/common';
 import * as Bull from 'bullmq';
 import { DI } from '@/di-symbols.js';
 import type { Config } from '@/config.js';
-import { QUEUE, baseQueueOptions } from '@/queue/const.js';
+import { baseQueueOptions, QUEUE } from '@/queue/const.js';
 import { allSettled } from '@/misc/promise-tracker.js';
+import {
+	DeliverJobData,
+	EndedPollNotificationJobData,
+	InboxJobData,
+	RelationshipJobData,
+	UserWebhookDeliverJobData,
+	SystemWebhookDeliverJobData,
+} from '../queue/types.js';
 import type { Provider } from '@nestjs/common';
-import type { DeliverJobData, InboxJobData, EndedPollNotificationJobData, WebhookDeliverJobData, RelationshipJobData } from '../queue/types.js';
 
 export type SystemQueue = Bull.Queue<Record<string, unknown>>;
 export type EndedPollNotificationQueue = Bull.Queue<EndedPollNotificationJobData>;
@@ -19,7 +26,8 @@ export type InboxQueue = Bull.Queue<InboxJobData>;
 export type DbQueue = Bull.Queue;
 export type RelationshipQueue = Bull.Queue<RelationshipJobData>;
 export type ObjectStorageQueue = Bull.Queue;
-export type WebhookDeliverQueue = Bull.Queue<WebhookDeliverJobData>;
+export type UserWebhookDeliverQueue = Bull.Queue<UserWebhookDeliverJobData>;
+export type SystemWebhookDeliverQueue = Bull.Queue<SystemWebhookDeliverJobData>;
 
 const $system: Provider = {
 	provide: 'queue:system',
@@ -63,9 +71,15 @@ const $objectStorage: Provider = {
 	inject: [DI.config],
 };
 
-const $webhookDeliver: Provider = {
-	provide: 'queue:webhookDeliver',
-	useFactory: (config: Config) => new Bull.Queue(QUEUE.WEBHOOK_DELIVER, baseQueueOptions(config, QUEUE.WEBHOOK_DELIVER)),
+const $userWebhookDeliver: Provider = {
+	provide: 'queue:userWebhookDeliver',
+	useFactory: (config: Config) => new Bull.Queue(QUEUE.USER_WEBHOOK_DELIVER, baseQueueOptions(config, QUEUE.USER_WEBHOOK_DELIVER)),
+	inject: [DI.config],
+};
+
+const $systemWebhookDeliver: Provider = {
+	provide: 'queue:systemWebhookDeliver',
+	useFactory: (config: Config) => new Bull.Queue(QUEUE.SYSTEM_WEBHOOK_DELIVER, baseQueueOptions(config, QUEUE.SYSTEM_WEBHOOK_DELIVER)),
 	inject: [DI.config],
 };
 
@@ -80,7 +94,8 @@ const $webhookDeliver: Provider = {
 		$db,
 		$relationship,
 		$objectStorage,
-		$webhookDeliver,
+		$userWebhookDeliver,
+		$systemWebhookDeliver,
 	],
 	exports: [
 		$system,
@@ -90,7 +105,8 @@ const $webhookDeliver: Provider = {
 		$db,
 		$relationship,
 		$objectStorage,
-		$webhookDeliver,
+		$userWebhookDeliver,
+		$systemWebhookDeliver,
 	],
 })
 export class QueueModule implements OnApplicationShutdown {
@@ -102,7 +118,8 @@ export class QueueModule implements OnApplicationShutdown {
 		@Inject('queue:db') public dbQueue: DbQueue,
 		@Inject('queue:relationship') public relationshipQueue: RelationshipQueue,
 		@Inject('queue:objectStorage') public objectStorageQueue: ObjectStorageQueue,
-		@Inject('queue:webhookDeliver') public webhookDeliverQueue: WebhookDeliverQueue,
+		@Inject('queue:userWebhookDeliver') public userWebhookDeliverQueue: UserWebhookDeliverQueue,
+		@Inject('queue:systemWebhookDeliver') public systemWebhookDeliverQueue: SystemWebhookDeliverQueue,
 	) {}
 
 	public async dispose(): Promise<void> {
@@ -117,7 +134,8 @@ export class QueueModule implements OnApplicationShutdown {
 			this.dbQueue.close(),
 			this.relationshipQueue.close(),
 			this.objectStorageQueue.close(),
-			this.webhookDeliverQueue.close(),
+			this.userWebhookDeliverQueue.close(),
+			this.systemWebhookDeliverQueue.close(),
 		]);
 	}
 
diff --git a/packages/backend/src/core/QueueService.ts b/packages/backend/src/core/QueueService.ts
index c258a22927..37028026cc 100644
--- a/packages/backend/src/core/QueueService.ts
+++ b/packages/backend/src/core/QueueService.ts
@@ -8,15 +8,33 @@ import { Inject, Injectable } from '@nestjs/common';
 import type { IActivity } from '@/core/activitypub/type.js';
 import type { MiDriveFile } from '@/models/DriveFile.js';
 import type { MiWebhook, webhookEventTypes } from '@/models/Webhook.js';
+import type { MiSystemWebhook, SystemWebhookEventType } from '@/models/SystemWebhook.js';
 import type { Config } from '@/config.js';
 import { DI } from '@/di-symbols.js';
 import { bindThis } from '@/decorators.js';
 import type { Antenna } from '@/server/api/endpoints/i/import-antennas.js';
-import type { DbQueue, DeliverQueue, EndedPollNotificationQueue, InboxQueue, ObjectStorageQueue, RelationshipQueue, SystemQueue, WebhookDeliverQueue } from './QueueModule.js';
-import type { DbJobData, DeliverJobData, RelationshipJobData, ThinUser } from '../queue/types.js';
+import { ApRequestCreator } from '@/core/activitypub/ApRequestService.js';
+import type {
+	DbJobData,
+	DeliverJobData,
+	RelationshipJobData,
+	SystemWebhookDeliverJobData,
+	ThinUser,
+	UserWebhookDeliverJobData,
+} from '../queue/types.js';
+import type {
+	DbQueue,
+	DeliverQueue,
+	EndedPollNotificationQueue,
+	InboxQueue,
+	ObjectStorageQueue,
+	RelationshipQueue,
+	SystemQueue,
+	UserWebhookDeliverQueue,
+	SystemWebhookDeliverQueue,
+} from './QueueModule.js';
 import type httpSignature from '@peertube/http-signature';
 import type * as Bull from 'bullmq';
-import { ApRequestCreator } from '@/core/activitypub/ApRequestService.js';
 
 @Injectable()
 export class QueueService {
@@ -31,7 +49,8 @@ export class QueueService {
 		@Inject('queue:db') public dbQueue: DbQueue,
 		@Inject('queue:relationship') public relationshipQueue: RelationshipQueue,
 		@Inject('queue:objectStorage') public objectStorageQueue: ObjectStorageQueue,
-		@Inject('queue:webhookDeliver') public webhookDeliverQueue: WebhookDeliverQueue,
+		@Inject('queue:userWebhookDeliver') public userWebhookDeliverQueue: UserWebhookDeliverQueue,
+		@Inject('queue:systemWebhookDeliver') public systemWebhookDeliverQueue: SystemWebhookDeliverQueue,
 	) {
 		this.systemQueue.add('tickCharts', {
 		}, {
@@ -68,6 +87,19 @@ export class QueueService {
 			repeat: { pattern: '*/5 * * * *' },
 			removeOnComplete: true,
 		});
+
+		this.systemQueue.add('bakeBufferedReactions', {
+		}, {
+			repeat: { pattern: '0 0 * * *' },
+			removeOnComplete: true,
+		});
+
+		this.systemQueue.add('checkModeratorsActivity', {
+		}, {
+			// 毎時30分に起動
+			repeat: { pattern: '30 * * * *' },
+			removeOnComplete: true,
+		});
 	}
 
 	@bindThis
@@ -431,9 +463,18 @@ export class QueueService {
 		});
 	}
 
+	/**
+	 * @see UserWebhookDeliverJobData
+	 * @see UserWebhookDeliverProcessorService
+	 */
 	@bindThis
-	public webhookDeliver(webhook: MiWebhook, type: typeof webhookEventTypes[number], content: unknown) {
-		const data = {
+	public userWebhookDeliver(
+		webhook: MiWebhook,
+		type: typeof webhookEventTypes[number],
+		content: unknown,
+		opts?: { attempts?: number },
+	) {
+		const data: UserWebhookDeliverJobData = {
 			type,
 			content,
 			webhookId: webhook.id,
@@ -444,8 +485,39 @@ export class QueueService {
 			eventId: randomUUID(),
 		};
 
-		return this.webhookDeliverQueue.add(webhook.id, data, {
-			attempts: 4,
+		return this.userWebhookDeliverQueue.add(webhook.id, data, {
+			attempts: opts?.attempts ?? 4,
+			backoff: {
+				type: 'custom',
+			},
+			removeOnComplete: true,
+			removeOnFail: true,
+		});
+	}
+
+	/**
+	 * @see SystemWebhookDeliverJobData
+	 * @see SystemWebhookDeliverProcessorService
+	 */
+	@bindThis
+	public systemWebhookDeliver(
+		webhook: MiSystemWebhook,
+		type: SystemWebhookEventType,
+		content: unknown,
+		opts?: { attempts?: number },
+	) {
+		const data: SystemWebhookDeliverJobData = {
+			type,
+			content,
+			webhookId: webhook.id,
+			to: webhook.url,
+			secret: webhook.secret,
+			createdAt: Date.now(),
+			eventId: randomUUID(),
+		};
+
+		return this.systemWebhookDeliverQueue.add(webhook.id, data, {
+			attempts: opts?.attempts ?? 4,
 			backoff: {
 				type: 'custom',
 			},
diff --git a/packages/backend/src/core/ReactionService.ts b/packages/backend/src/core/ReactionService.ts
index cb0b079df0..6f9fe53937 100644
--- a/packages/backend/src/core/ReactionService.ts
+++ b/packages/backend/src/core/ReactionService.ts
@@ -4,9 +4,8 @@
  */
 
 import { Inject, Injectable } from '@nestjs/common';
-import * as Redis from 'ioredis';
 import { DI } from '@/di-symbols.js';
-import type { EmojisRepository, NoteReactionsRepository, UsersRepository, NotesRepository } from '@/models/_.js';
+import type { EmojisRepository, NoteReactionsRepository, UsersRepository, NotesRepository, MiMeta } from '@/models/_.js';
 import { IdentifiableError } from '@/misc/identifiable-error.js';
 import type { MiRemoteUser, MiUser } from '@/models/User.js';
 import type { MiNote } from '@/models/Note.js';
@@ -21,7 +20,6 @@ import { ApDeliverManagerService } from '@/core/activitypub/ApDeliverManagerServ
 import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
 import { UserEntityService } from '@/core/entities/UserEntityService.js';
 import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
-import { MetaService } from '@/core/MetaService.js';
 import { bindThis } from '@/decorators.js';
 import { UtilityService } from '@/core/UtilityService.js';
 import { UserBlockingService } from '@/core/UserBlockingService.js';
@@ -29,9 +27,11 @@ import { CustomEmojiService } from '@/core/CustomEmojiService.js';
 import { RoleService } from '@/core/RoleService.js';
 import { FeaturedService } from '@/core/FeaturedService.js';
 import { trackPromise } from '@/misc/promise-tracker.js';
+import { isQuote, isRenote } from '@/misc/is-renote.js';
+import { ReactionsBufferingService } from '@/core/ReactionsBufferingService.js';
+import { PER_NOTE_REACTION_USER_PAIR_CACHE_MAX } from '@/const.js';
 
 const FALLBACK = '\u2764';
-const PER_NOTE_REACTION_USER_PAIR_CACHE_MAX = 16;
 
 const legacies: Record<string, string> = {
 	'like': '👍',
@@ -70,8 +70,8 @@ const decodeCustomEmojiRegexp = /^:([\w+-]+)(?:@([\w.-]+))?:$/;
 @Injectable()
 export class ReactionService {
 	constructor(
-		@Inject(DI.redis)
-		private redisClient: Redis.Redis,
+		@Inject(DI.meta)
+		private meta: MiMeta,
 
 		@Inject(DI.usersRepository)
 		private usersRepository: UsersRepository,
@@ -86,12 +86,12 @@ export class ReactionService {
 		private emojisRepository: EmojisRepository,
 
 		private utilityService: UtilityService,
-		private metaService: MetaService,
 		private customEmojiService: CustomEmojiService,
 		private roleService: RoleService,
 		private userEntityService: UserEntityService,
 		private noteEntityService: NoteEntityService,
 		private userBlockingService: UserBlockingService,
+		private reactionsBufferingService: ReactionsBufferingService,
 		private idService: IdService,
 		private featuredService: FeaturedService,
 		private globalEventService: GlobalEventService,
@@ -117,11 +117,16 @@ export class ReactionService {
 			throw new IdentifiableError('68e9d2d1-48bf-42c2-b90a-b20e09fd3d48', 'Note not accessible for you.');
 		}
 
+		// Check if note is Renote
+		if (isRenote(note) && !isQuote(note)) {
+			throw new IdentifiableError('12c35529-3c79-4327-b1cc-e2cf63a71925', 'You cannot react to Renote.');
+		}
+
 		let reaction = _reaction ?? FALLBACK;
 
 		if (note.reactionAcceptance === 'likeOnly' || ((note.reactionAcceptance === 'likeOnlyForRemote' || note.reactionAcceptance === 'nonSensitiveOnlyForLocalLikeOnlyForRemote') && (user.host != null))) {
 			reaction = '\u2764';
-		} else if (_reaction) {
+		} else if (_reaction != null) {
 			const custom = reaction.match(isCustomEmojiRegexp);
 			if (custom) {
 				const reacterHost = this.utilityService.toPunyNullable(user.host);
@@ -142,6 +147,11 @@ export class ReactionService {
 						if ((note.reactionAcceptance === 'nonSensitiveOnly' || note.reactionAcceptance === 'nonSensitiveOnlyForLocalLikeOnlyForRemote') && emoji.isSensitive) {
 							reaction = FALLBACK;
 						}
+
+						// for media silenced host, custom emoji reactions are not allowed
+						if (reacterHost != null && this.utilityService.isMediaSilencedHost(this.meta.mediaSilencedHosts, reacterHost)) {
+							reaction = FALLBACK;
+						}
 					} else {
 						// リアクションとして使う権限がない
 						reaction = FALLBACK;
@@ -161,7 +171,6 @@ export class ReactionService {
 			reaction,
 		};
 
-		// Create reaction
 		try {
 			await this.noteReactionsRepository.insert(record);
 		} catch (e) {
@@ -185,16 +194,20 @@ export class ReactionService {
 		}
 
 		// Increment reactions count
-		const sql = `jsonb_set("reactions", '{${reaction}}', (COALESCE("reactions"->>'${reaction}', '0')::int + 1)::text::jsonb)`;
-		await this.notesRepository.createQueryBuilder().update()
-			.set({
-				reactions: () => sql,
-				...(note.reactionAndUserPairCache.length < PER_NOTE_REACTION_USER_PAIR_CACHE_MAX ? {
-					reactionAndUserPairCache: () => `array_append("reactionAndUserPairCache", '${user.id}/${reaction}')`,
-				} : {}),
-			})
-			.where('id = :id', { id: note.id })
-			.execute();
+		if (this.meta.enableReactionsBuffering) {
+			await this.reactionsBufferingService.create(note.id, user.id, reaction, note.reactionAndUserPairCache);
+		} else {
+			const sql = `jsonb_set("reactions", '{${reaction}}', (COALESCE("reactions"->>'${reaction}', '0')::int + 1)::text::jsonb)`;
+			await this.notesRepository.createQueryBuilder().update()
+				.set({
+					reactions: () => sql,
+					...(note.reactionAndUserPairCache.length < PER_NOTE_REACTION_USER_PAIR_CACHE_MAX ? {
+						reactionAndUserPairCache: () => `array_append("reactionAndUserPairCache", '${user.id}/${reaction}')`,
+					} : {}),
+				})
+				.where('id = :id', { id: note.id })
+				.execute();
+		}
 
 		// 30%の確率、セルフではない、3日以内に投稿されたノートの場合ハイライト用ランキング更新
 		if (
@@ -214,9 +227,7 @@ export class ReactionService {
 			}
 		}
 
-		const meta = await this.metaService.fetch();
-
-		if (meta.enableChartsForRemoteUser || (user.host == null)) {
+		if (this.meta.enableChartsForRemoteUser || (user.host == null)) {
 			this.perUserReactionsChart.update(user, note);
 		}
 
@@ -294,14 +305,18 @@ export class ReactionService {
 		}
 
 		// Decrement reactions count
-		const sql = `jsonb_set("reactions", '{${exist.reaction}}', (COALESCE("reactions"->>'${exist.reaction}', '0')::int - 1)::text::jsonb)`;
-		await this.notesRepository.createQueryBuilder().update()
-			.set({
-				reactions: () => sql,
-				reactionAndUserPairCache: () => `array_remove("reactionAndUserPairCache", '${user.id}/${exist.reaction}')`,
-			})
-			.where('id = :id', { id: note.id })
-			.execute();
+		if (this.meta.enableReactionsBuffering) {
+			await this.reactionsBufferingService.delete(note.id, user.id, exist.reaction);
+		} else {
+			const sql = `jsonb_set("reactions", '{${exist.reaction}}', (COALESCE("reactions"->>'${exist.reaction}', '0')::int - 1)::text::jsonb)`;
+			await this.notesRepository.createQueryBuilder().update()
+				.set({
+					reactions: () => sql,
+					reactionAndUserPairCache: () => `array_remove("reactionAndUserPairCache", '${user.id}/${exist.reaction}')`,
+				})
+				.where('id = :id', { id: note.id })
+				.execute();
+		}
 
 		this.globalEventService.publishNoteStream(note.id, 'unreacted', {
 			reaction: this.decodeReaction(exist.reaction).reaction,
@@ -323,8 +338,21 @@ export class ReactionService {
 	}
 
 	/**
-	 * 文字列タイプのレガシーな形式のリアクションを現在の形式に変換しつつ、
-	 * データベース上には存在する「0個のリアクションがついている」という情報を削除する。
+	 * - 文字列タイプのレガシーな形式のリアクションを現在の形式に変換する
+	 * - ローカルのリアクションのホストを `@.` にする(`decodeReaction()`の効果)
+	 */
+	@bindThis
+	public convertLegacyReaction(reaction: string): string {
+		reaction = this.decodeReaction(reaction).reaction;
+		if (Object.keys(legacies).includes(reaction)) return legacies[reaction];
+		return reaction;
+	}
+
+	// TODO: 廃止
+	/**
+	 * - 文字列タイプのレガシーな形式のリアクションを現在の形式に変換する
+	 * - ローカルのリアクションのホストを `@.` にする(`decodeReaction()`の効果)
+	 * - データベース上には存在する「0個のリアクションがついている」という情報を削除する
 	 */
 	@bindThis
 	public convertLegacyReactions(reactions: MiNote['reactions']): MiNote['reactions'] {
@@ -337,10 +365,7 @@ export class ReactionService {
 				return count > 0;
 			})
 			.map(([reaction, count]) => {
-				// unchecked indexed access
-				const convertedReaction = legacies[reaction] as string | undefined;
-
-				const key = this.decodeReaction(convertedReaction ?? reaction).reaction;
+				const key = this.convertLegacyReaction(reaction);
 
 				return [key, count] as const;
 			})
@@ -395,11 +420,4 @@ export class ReactionService {
 			host: undefined,
 		};
 	}
-
-	@bindThis
-	public convertLegacyReaction(reaction: string): string {
-		reaction = this.decodeReaction(reaction).reaction;
-		if (Object.keys(legacies).includes(reaction)) return legacies[reaction];
-		return reaction;
-	}
 }
diff --git a/packages/backend/src/core/ReactionsBufferingService.ts b/packages/backend/src/core/ReactionsBufferingService.ts
new file mode 100644
index 0000000000..b4207c5106
--- /dev/null
+++ b/packages/backend/src/core/ReactionsBufferingService.ts
@@ -0,0 +1,211 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Inject, Injectable } from '@nestjs/common';
+import * as Redis from 'ioredis';
+import { DI } from '@/di-symbols.js';
+import type { MiNote } from '@/models/Note.js';
+import { bindThis } from '@/decorators.js';
+import type { MiUser, NotesRepository } from '@/models/_.js';
+import type { Config } from '@/config.js';
+import { PER_NOTE_REACTION_USER_PAIR_CACHE_MAX } from '@/const.js';
+import type { GlobalEvents } from '@/core/GlobalEventService.js';
+import type { OnApplicationShutdown } from '@nestjs/common';
+
+const REDIS_DELTA_PREFIX = 'reactionsBufferDeltas';
+const REDIS_PAIR_PREFIX = 'reactionsBufferPairs';
+
+@Injectable()
+export class ReactionsBufferingService implements OnApplicationShutdown {
+	constructor(
+		@Inject(DI.config)
+		private config: Config,
+
+		@Inject(DI.redisForSub)
+		private redisForSub: Redis.Redis,
+
+		@Inject(DI.redisForReactions)
+		private redisForReactions: Redis.Redis, // TODO: 専用のRedisインスタンスにする
+
+		@Inject(DI.notesRepository)
+		private notesRepository: NotesRepository,
+	) {
+		this.redisForSub.on('message', this.onMessage);
+	}
+
+	@bindThis
+	private async onMessage(_: string, data: string) {
+		const obj = JSON.parse(data);
+
+		if (obj.channel === 'internal') {
+			const { type, body } = obj.message as GlobalEvents['internal']['payload'];
+			switch (type) {
+				case 'metaUpdated': {
+					// リアクションバッファリングが有効→無効になったら即bake
+					if (body.before != null && body.before.enableReactionsBuffering && !body.after.enableReactionsBuffering) {
+						this.bake();
+					}
+					break;
+				}
+				default:
+					break;
+			}
+		}
+	}
+
+	@bindThis
+	public async create(noteId: MiNote['id'], userId: MiUser['id'], reaction: string, currentPairs: string[]): Promise<void> {
+		const pipeline = this.redisForReactions.pipeline();
+		pipeline.hincrby(`${REDIS_DELTA_PREFIX}:${noteId}`, reaction, 1);
+		for (let i = 0; i < currentPairs.length; i++) {
+			pipeline.zadd(`${REDIS_PAIR_PREFIX}:${noteId}`, i, currentPairs[i]);
+		}
+		pipeline.zadd(`${REDIS_PAIR_PREFIX}:${noteId}`, Date.now(), `${userId}/${reaction}`);
+		pipeline.zremrangebyrank(`${REDIS_PAIR_PREFIX}:${noteId}`, 0, -(PER_NOTE_REACTION_USER_PAIR_CACHE_MAX + 1));
+		await pipeline.exec();
+	}
+
+	@bindThis
+	public async delete(noteId: MiNote['id'], userId: MiUser['id'], reaction: string): Promise<void> {
+		const pipeline = this.redisForReactions.pipeline();
+		pipeline.hincrby(`${REDIS_DELTA_PREFIX}:${noteId}`, reaction, -1);
+		pipeline.zrem(`${REDIS_PAIR_PREFIX}:${noteId}`, `${userId}/${reaction}`);
+		// TODO: 「消した要素一覧」も持っておかないとcreateされた時に上書きされて復活する
+		await pipeline.exec();
+	}
+
+	@bindThis
+	public async get(noteId: MiNote['id']): Promise<{
+		deltas: Record<string, number>;
+		pairs: ([MiUser['id'], string])[];
+	}> {
+		const pipeline = this.redisForReactions.pipeline();
+		pipeline.hgetall(`${REDIS_DELTA_PREFIX}:${noteId}`);
+		pipeline.zrange(`${REDIS_PAIR_PREFIX}:${noteId}`, 0, -1);
+		const results = await pipeline.exec();
+
+		const resultDeltas = results![0][1] as Record<string, string>;
+		const resultPairs = results![1][1] as string[];
+
+		const deltas = {} as Record<string, number>;
+		for (const [name, count] of Object.entries(resultDeltas)) {
+			deltas[name] = parseInt(count);
+		}
+
+		const pairs = resultPairs.map(x => x.split('/') as [MiUser['id'], string]);
+
+		return {
+			deltas,
+			pairs,
+		};
+	}
+
+	@bindThis
+	public async getMany(noteIds: MiNote['id'][]): Promise<Map<MiNote['id'], {
+		deltas: Record<string, number>;
+		pairs: ([MiUser['id'], string])[];
+	}>> {
+		const map = new Map<MiNote['id'], {
+			deltas: Record<string, number>;
+			pairs: ([MiUser['id'], string])[];
+		}>();
+
+		const pipeline = this.redisForReactions.pipeline();
+		for (const noteId of noteIds) {
+			pipeline.hgetall(`${REDIS_DELTA_PREFIX}:${noteId}`);
+			pipeline.zrange(`${REDIS_PAIR_PREFIX}:${noteId}`, 0, -1);
+		}
+		const results = await pipeline.exec();
+
+		const opsForEachNotes = 2;
+		for (let i = 0; i < noteIds.length; i++) {
+			const noteId = noteIds[i];
+			const resultDeltas = results![i * opsForEachNotes][1] as Record<string, string>;
+			const resultPairs = results![i * opsForEachNotes + 1][1] as string[];
+
+			const deltas = {} as Record<string, number>;
+			for (const [name, count] of Object.entries(resultDeltas)) {
+				deltas[name] = parseInt(count);
+			}
+
+			const pairs = resultPairs.map(x => x.split('/') as [MiUser['id'], string]);
+
+			map.set(noteId, {
+				deltas,
+				pairs,
+			});
+		}
+
+		return map;
+	}
+
+	// TODO: scanは重い可能性があるので、別途 bufferedNoteIds を直接Redis上に持っておいてもいいかもしれない
+	@bindThis
+	public async bake(): Promise<void> {
+		const bufferedNoteIds = [];
+		let cursor = '0';
+		do {
+			// https://github.com/redis/ioredis#transparent-key-prefixing
+			const result = await this.redisForReactions.scan(
+				cursor,
+				'MATCH',
+				`${this.config.redis.prefix}:${REDIS_DELTA_PREFIX}:*`,
+				'COUNT',
+				'1000');
+
+			cursor = result[0];
+			bufferedNoteIds.push(...result[1].map(x => x.replace(`${this.config.redis.prefix}:${REDIS_DELTA_PREFIX}:`, '')));
+		} while (cursor !== '0');
+
+		const bufferedMap = await this.getMany(bufferedNoteIds);
+
+		// clear
+		const pipeline = this.redisForReactions.pipeline();
+		for (const noteId of bufferedNoteIds) {
+			pipeline.del(`${REDIS_DELTA_PREFIX}:${noteId}`);
+			pipeline.del(`${REDIS_PAIR_PREFIX}:${noteId}`);
+		}
+		await pipeline.exec();
+
+		// TODO: SQL一個にまとめたい
+		for (const [noteId, buffered] of bufferedMap) {
+			const sql = Object.entries(buffered.deltas)
+				.map(([reaction, count]) =>
+					`jsonb_set("reactions", '{${reaction}}', (COALESCE("reactions"->>'${reaction}', '0')::int + ${count})::text::jsonb)`)
+				.join(' || ');
+
+			this.notesRepository.createQueryBuilder().update()
+				.set({
+					reactions: () => sql,
+					reactionAndUserPairCache: buffered.pairs.map(x => x.join('/')),
+				})
+				.where('id = :id', { id: noteId })
+				.execute();
+		}
+	}
+
+	@bindThis
+	public mergeReactions(src: MiNote['reactions'], delta: Record<string, number>): MiNote['reactions'] {
+		const reactions = { ...src };
+		for (const [name, count] of Object.entries(delta)) {
+			if (reactions[name] != null) {
+				reactions[name] += count;
+			} else {
+				reactions[name] = count;
+			}
+		}
+		return reactions;
+	}
+
+	@bindThis
+	public dispose(): void {
+		this.redisForSub.off('message', this.onMessage);
+	}
+
+	@bindThis
+	public onApplicationShutdown(signal?: string | undefined): void {
+		this.dispose();
+	}
+}
diff --git a/packages/backend/src/core/RelayService.ts b/packages/backend/src/core/RelayService.ts
index 8dd3d64f5b..db32114346 100644
--- a/packages/backend/src/core/RelayService.ts
+++ b/packages/backend/src/core/RelayService.ts
@@ -35,7 +35,7 @@ export class RelayService {
 		private createSystemUserService: CreateSystemUserService,
 		private apRendererService: ApRendererService,
 	) {
-		this.relaysCache = new MemorySingleCache<MiRelay[]>(1000 * 60 * 10);
+		this.relaysCache = new MemorySingleCache<MiRelay[]>(1000 * 60 * 10); // 10m
 	}
 
 	@bindThis
diff --git a/packages/backend/src/core/ReversiService.ts b/packages/backend/src/core/ReversiService.ts
index 7f939b99c7..51dca3da59 100644
--- a/packages/backend/src/core/ReversiService.ts
+++ b/packages/backend/src/core/ReversiService.ts
@@ -6,6 +6,7 @@
 import { Inject, Injectable } from '@nestjs/common';
 import * as Redis from 'ioredis';
 import { ModuleRef } from '@nestjs/core';
+import { reversiUpdateKeys } from 'misskey-js';
 import * as Reversi from 'misskey-reversi';
 import { IsNull, LessThan, MoreThan } from 'typeorm';
 import type {
@@ -399,7 +400,33 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
 	}
 
 	@bindThis
-	public async updateSettings(gameId: MiReversiGame['id'], user: MiUser, key: string, value: any) {
+	public isValidReversiUpdateKey(key: unknown): key is typeof reversiUpdateKeys[number] {
+		if (typeof key !== 'string') return false;
+		return (reversiUpdateKeys as string[]).includes(key);
+	}
+
+	@bindThis
+	public isValidReversiUpdateValue<K extends typeof reversiUpdateKeys[number]>(key: K, value: unknown): value is MiReversiGame[K] {
+		switch (key) {
+			case 'map':
+				return Array.isArray(value) && value.every(row => typeof row === 'string');
+			case 'bw':
+				return typeof value === 'string' && ['random', '1', '2'].includes(value);
+			case 'isLlotheo':
+				return typeof value === 'boolean';
+			case 'canPutEverywhere':
+				return typeof value === 'boolean';
+			case 'loopedBoard':
+				return typeof value === 'boolean';
+			case 'timeLimitForEachTurn':
+				return typeof value === 'number' && value >= 0;
+			default:
+				return false;
+		}
+	}
+
+	@bindThis
+	public async updateSettings<K extends typeof reversiUpdateKeys[number]>(gameId: MiReversiGame['id'], user: MiUser, key: K, value: MiReversiGame[K]) {
 		const game = await this.get(gameId);
 		if (game == null) throw new Error('game not found');
 		if (game.isStarted) return;
@@ -407,10 +434,6 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
 		if ((game.user1Id === user.id) && game.user1Ready) return;
 		if ((game.user2Id === user.id) && game.user2Ready) return;
 
-		if (!['map', 'bw', 'isLlotheo', 'canPutEverywhere', 'loopedBoard', 'timeLimitForEachTurn'].includes(key)) return;
-
-		// TODO: より厳格なバリデーション
-
 		const updatedGame = {
 			...game,
 			[key]: value,
diff --git a/packages/backend/src/core/RoleService.ts b/packages/backend/src/core/RoleService.ts
index d6eea70297..5af6b05942 100644
--- a/packages/backend/src/core/RoleService.ts
+++ b/packages/backend/src/core/RoleService.ts
@@ -8,6 +8,7 @@ import * as Redis from 'ioredis';
 import { In } from 'typeorm';
 import { ModuleRef } from '@nestjs/core';
 import type {
+	MiMeta,
 	MiRole,
 	MiRoleAssignment,
 	RoleAssignmentsRepository,
@@ -18,7 +19,6 @@ import { MemoryKVCache, MemorySingleCache } from '@/misc/cache.js';
 import type { MiUser } from '@/models/User.js';
 import { DI } from '@/di-symbols.js';
 import { bindThis } from '@/decorators.js';
-import { MetaService } from '@/core/MetaService.js';
 import { CacheService } from '@/core/CacheService.js';
 import type { RoleCondFormulaValue } from '@/models/Role.js';
 import { UserEntityService } from '@/core/entities/UserEntityService.js';
@@ -47,6 +47,7 @@ export type RolePolicies = {
 	canHideAds: boolean;
 	driveCapacityMb: number;
 	alwaysMarkNsfw: boolean;
+	canUpdateBioMedia: boolean;
 	pinLimit: number;
 	antennaLimit: number;
 	wordMuteLimit: number;
@@ -57,6 +58,11 @@ export type RolePolicies = {
 	userEachUserListsLimit: number;
 	rateLimitFactor: number;
 	avatarDecorationLimit: number;
+	canImportAntennas: boolean;
+	canImportBlocking: boolean;
+	canImportFollowing: boolean;
+	canImportMuting: boolean;
+	canImportUserLists: boolean;
 };
 
 export const DEFAULT_POLICIES: RolePolicies = {
@@ -75,6 +81,7 @@ export const DEFAULT_POLICIES: RolePolicies = {
 	canHideAds: false,
 	driveCapacityMb: 100,
 	alwaysMarkNsfw: false,
+	canUpdateBioMedia: true,
 	pinLimit: 5,
 	antennaLimit: 5,
 	wordMuteLimit: 200,
@@ -85,10 +92,16 @@ export const DEFAULT_POLICIES: RolePolicies = {
 	userEachUserListsLimit: 50,
 	rateLimitFactor: 1,
 	avatarDecorationLimit: 1,
+	canImportAntennas: true,
+	canImportBlocking: true,
+	canImportFollowing: true,
+	canImportMuting: true,
+	canImportUserLists: true,
 };
 
 @Injectable()
 export class RoleService implements OnApplicationShutdown, OnModuleInit {
+	private rootUserIdCache: MemorySingleCache<MiUser['id']>;
 	private rolesCache: MemorySingleCache<MiRole[]>;
 	private roleAssignmentByUserIdCache: MemoryKVCache<MiRoleAssignment[]>;
 	private notificationService: NotificationService;
@@ -99,8 +112,8 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
 	constructor(
 		private moduleRef: ModuleRef,
 
-		@Inject(DI.redis)
-		private redisClient: Redis.Redis,
+		@Inject(DI.meta)
+		private meta: MiMeta,
 
 		@Inject(DI.redisForTimelines)
 		private redisForTimelines: Redis.Redis,
@@ -117,7 +130,6 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
 		@Inject(DI.roleAssignmentsRepository)
 		private roleAssignmentsRepository: RoleAssignmentsRepository,
 
-		private metaService: MetaService,
 		private cacheService: CacheService,
 		private userEntityService: UserEntityService,
 		private globalEventService: GlobalEventService,
@@ -125,10 +137,9 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
 		private moderationLogService: ModerationLogService,
 		private fanoutTimelineService: FanoutTimelineService,
 	) {
-		//this.onMessage = this.onMessage.bind(this);
-
-		this.rolesCache = new MemorySingleCache<MiRole[]>(1000 * 60 * 60 * 1);
-		this.roleAssignmentByUserIdCache = new MemoryKVCache<MiRoleAssignment[]>(1000 * 60 * 60 * 1);
+		this.rootUserIdCache = new MemorySingleCache<MiUser['id']>(1000 * 60 * 60 * 24 * 7); // 1week. rootユーザのIDは不変なので長めに
+		this.rolesCache = new MemorySingleCache<MiRole[]>(1000 * 60 * 60); // 1h
+		this.roleAssignmentByUserIdCache = new MemoryKVCache<MiRoleAssignment[]>(1000 * 60 * 5); // 5m
 
 		this.redisForSub.on('message', this.onMessage);
 	}
@@ -339,8 +350,7 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
 
 	@bindThis
 	public async getUserPolicies(userId: MiUser['id'] | null): Promise<RolePolicies> {
-		const meta = await this.metaService.fetch();
-		const basePolicies = { ...DEFAULT_POLICIES, ...meta.policies };
+		const basePolicies = { ...DEFAULT_POLICIES, ...this.meta.policies };
 
 		if (userId == null) return basePolicies;
 
@@ -376,6 +386,7 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
 			canHideAds: calc('canHideAds', vs => vs.some(v => v === true)),
 			driveCapacityMb: calc('driveCapacityMb', vs => Math.max(...vs)),
 			alwaysMarkNsfw: calc('alwaysMarkNsfw', vs => vs.some(v => v === true)),
+			canUpdateBioMedia: calc('canUpdateBioMedia', vs => vs.some(v => v === true)),
 			pinLimit: calc('pinLimit', vs => Math.max(...vs)),
 			antennaLimit: calc('antennaLimit', vs => Math.max(...vs)),
 			wordMuteLimit: calc('wordMuteLimit', vs => Math.max(...vs)),
@@ -386,6 +397,11 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
 			userEachUserListsLimit: calc('userEachUserListsLimit', vs => Math.max(...vs)),
 			rateLimitFactor: calc('rateLimitFactor', vs => Math.max(...vs)),
 			avatarDecorationLimit: calc('avatarDecorationLimit', vs => Math.max(...vs)),
+			canImportAntennas: calc('canImportAntennas', vs => vs.some(v => v === true)),
+			canImportBlocking: calc('canImportBlocking', vs => vs.some(v => v === true)),
+			canImportFollowing: calc('canImportFollowing', vs => vs.some(v => v === true)),
+			canImportMuting: calc('canImportMuting', vs => vs.some(v => v === true)),
+			canImportUserLists: calc('canImportUserLists', vs => vs.some(v => v === true)),
 		};
 	}
 
@@ -402,31 +418,78 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
 	}
 
 	@bindThis
-	public async isExplorable(role: { id: MiRole['id']} | null): Promise<boolean> {
+	public async isExplorable(role: { id: MiRole['id'] } | null): Promise<boolean> {
 		if (role == null) return false;
 		const check = await this.rolesRepository.findOneBy({ id: role.id });
 		if (check == null) return false;
 		return check.isExplorable;
 	}
 
+	/**
+	 * モデレーター権限のロールが割り当てられているユーザID一覧を取得する.
+	 *
+	 * @param opts.includeAdmins 管理者権限も含めるか(デフォルト: true)
+	 * @param opts.includeRoot rootユーザも含めるか(デフォルト: false)
+	 * @param opts.excludeExpire 期限切れのロールを除外するか(デフォルト: false)
+	 */
 	@bindThis
-	public async getModeratorIds(includeAdmins = true): Promise<MiUser['id'][]> {
+	public async getModeratorIds(opts?: {
+		includeAdmins?: boolean,
+		includeRoot?: boolean,
+		excludeExpire?: boolean,
+	}): Promise<MiUser['id'][]> {
+		const includeAdmins = opts?.includeAdmins ?? true;
+		const includeRoot = opts?.includeRoot ?? false;
+		const excludeExpire = opts?.excludeExpire ?? false;
+
 		const roles = await this.rolesCache.fetch(() => this.rolesRepository.findBy({}));
-		const moderatorRoles = includeAdmins ? roles.filter(r => r.isModerator || r.isAdministrator) : roles.filter(r => r.isModerator);
-		const assigns = moderatorRoles.length > 0 ? await this.roleAssignmentsRepository.findBy({
-			roleId: In(moderatorRoles.map(r => r.id)),
-		}) : [];
-		// TODO: isRootなアカウントも含める
-		return assigns.map(a => a.userId);
+		const moderatorRoles = includeAdmins
+			? roles.filter(r => r.isModerator || r.isAdministrator)
+			: roles.filter(r => r.isModerator);
+
+		const assigns = moderatorRoles.length > 0
+			? await this.roleAssignmentsRepository.findBy({ roleId: In(moderatorRoles.map(r => r.id)) })
+			: [];
+
+		// Setを経由して重複を除去(ユーザIDは重複する可能性があるので)
+		const now = Date.now();
+		const resultSet = new Set(
+			assigns
+				.filter(it =>
+					(excludeExpire)
+						? (it.expiresAt == null || it.expiresAt.getTime() > now)
+						: true,
+				)
+				.map(a => a.userId),
+		);
+
+		if (includeRoot) {
+			const rootUserId = await this.rootUserIdCache.fetch(async () => {
+				const it = await this.usersRepository.createQueryBuilder('users')
+					.select('id')
+					.where({ isRoot: true })
+					.getRawOne<{ id: string }>();
+				// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+				return it!.id;
+			});
+			resultSet.add(rootUserId);
+		}
+
+		return [...resultSet].sort((x, y) => x.localeCompare(y));
 	}
 
 	@bindThis
-	public async getModerators(includeAdmins = true): Promise<MiUser[]> {
-		const ids = await this.getModeratorIds(includeAdmins);
-		const users = ids.length > 0 ? await this.usersRepository.findBy({
-			id: In(ids),
-		}) : [];
-		return users;
+	public async getModerators(opts?: {
+		includeAdmins?: boolean,
+		includeRoot?: boolean,
+		excludeExpire?: boolean,
+	}): Promise<MiUser[]> {
+		const ids = await this.getModeratorIds(opts);
+		return ids.length > 0
+			? await this.usersRepository.findBy({
+				id: In(ids),
+			})
+			: [];
 	}
 
 	@bindThis
@@ -484,14 +547,15 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
 
 		this.globalEventService.publishInternalEvent('userRoleAssigned', created);
 
-		if (role.isPublic) {
+		const user = await this.usersRepository.findOneByOrFail({ id: userId });
+
+		if (role.isPublic && user.host === null) {
 			this.notificationService.createNotification(userId, 'roleAssigned', {
 				roleId: roleId,
 			});
 		}
 
 		if (moderator) {
-			const user = await this.usersRepository.findOneByOrFail({ id: userId });
 			this.moderationLogService.log(moderator, 'assignRole', {
 				roleId: roleId,
 				roleName: role.name,
diff --git a/packages/backend/src/core/SignupService.ts b/packages/backend/src/core/SignupService.ts
index 5522ecd6cc..3865392b7f 100644
--- a/packages/backend/src/core/SignupService.ts
+++ b/packages/backend/src/core/SignupService.ts
@@ -8,7 +8,7 @@ import { Inject, Injectable } from '@nestjs/common';
 import bcrypt from 'bcryptjs';
 import { DataSource, IsNull } from 'typeorm';
 import { DI } from '@/di-symbols.js';
-import type { UsedUsernamesRepository, UsersRepository } from '@/models/_.js';
+import type { MiMeta, UsedUsernamesRepository, UsersRepository } from '@/models/_.js';
 import { MiUser } from '@/models/User.js';
 import { MiUserProfile } from '@/models/UserProfile.js';
 import { IdService } from '@/core/IdService.js';
@@ -20,7 +20,7 @@ import { InstanceActorService } from '@/core/InstanceActorService.js';
 import { bindThis } from '@/decorators.js';
 import UsersChart from '@/core/chart/charts/users.js';
 import { UtilityService } from '@/core/UtilityService.js';
-import { MetaService } from '@/core/MetaService.js';
+import { UserService } from '@/core/UserService.js';
 
 @Injectable()
 export class SignupService {
@@ -28,6 +28,9 @@ export class SignupService {
 		@Inject(DI.db)
 		private db: DataSource,
 
+		@Inject(DI.meta)
+		private meta: MiMeta,
+
 		@Inject(DI.usersRepository)
 		private usersRepository: UsersRepository,
 
@@ -35,9 +38,9 @@ export class SignupService {
 		private usedUsernamesRepository: UsedUsernamesRepository,
 
 		private utilityService: UtilityService,
+		private userService: UserService,
 		private userEntityService: UserEntityService,
 		private idService: IdService,
-		private metaService: MetaService,
 		private instanceActorService: InstanceActorService,
 		private usersChart: UsersChart,
 	) {
@@ -86,8 +89,7 @@ export class SignupService {
 		const isTheFirstUser = !await this.instanceActorService.realLocalUsersPresent();
 
 		if (!opts.ignorePreservedUsernames && !isTheFirstUser) {
-			const instance = await this.metaService.fetch(true);
-			const isPreserved = instance.preservedUsernames.map(x => x.toLowerCase()).includes(username.toLowerCase());
+			const isPreserved = this.meta.preservedUsernames.map(x => x.toLowerCase()).includes(username.toLowerCase());
 			if (isPreserved) {
 				throw new Error('USED_USERNAME');
 			}
@@ -149,6 +151,7 @@ export class SignupService {
 		});
 
 		this.usersChart.update(account, true);
+		this.userService.notifySystemWebhook(account, 'userCreated');
 
 		return { account, secret };
 	}
diff --git a/packages/backend/src/core/SystemWebhookService.ts b/packages/backend/src/core/SystemWebhookService.ts
new file mode 100644
index 0000000000..db6407dcb3
--- /dev/null
+++ b/packages/backend/src/core/SystemWebhookService.ts
@@ -0,0 +1,235 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Inject, Injectable } from '@nestjs/common';
+import * as Redis from 'ioredis';
+import type { MiUser, SystemWebhooksRepository } from '@/models/_.js';
+import { DI } from '@/di-symbols.js';
+import { bindThis } from '@/decorators.js';
+import { GlobalEvents, GlobalEventService } from '@/core/GlobalEventService.js';
+import { MiSystemWebhook, type SystemWebhookEventType } from '@/models/SystemWebhook.js';
+import { IdService } from '@/core/IdService.js';
+import { QueueService } from '@/core/QueueService.js';
+import { ModerationLogService } from '@/core/ModerationLogService.js';
+import { LoggerService } from '@/core/LoggerService.js';
+import Logger from '@/logger.js';
+import type { OnApplicationShutdown } from '@nestjs/common';
+
+@Injectable()
+export class SystemWebhookService implements OnApplicationShutdown {
+	private logger: Logger;
+	private activeSystemWebhooksFetched = false;
+	private activeSystemWebhooks: MiSystemWebhook[] = [];
+
+	constructor(
+		@Inject(DI.redisForSub)
+		private redisForSub: Redis.Redis,
+		@Inject(DI.systemWebhooksRepository)
+		private systemWebhooksRepository: SystemWebhooksRepository,
+		private idService: IdService,
+		private queueService: QueueService,
+		private moderationLogService: ModerationLogService,
+		private loggerService: LoggerService,
+		private globalEventService: GlobalEventService,
+	) {
+		this.redisForSub.on('message', this.onMessage);
+		this.logger = this.loggerService.getLogger('webhook');
+	}
+
+	@bindThis
+	public async fetchActiveSystemWebhooks() {
+		if (!this.activeSystemWebhooksFetched) {
+			this.activeSystemWebhooks = await this.systemWebhooksRepository.findBy({
+				isActive: true,
+			});
+			this.activeSystemWebhooksFetched = true;
+		}
+
+		return this.activeSystemWebhooks;
+	}
+
+	/**
+	 * SystemWebhook の一覧を取得する.
+	 */
+	@bindThis
+	public fetchSystemWebhooks(params?: {
+		ids?: MiSystemWebhook['id'][];
+		isActive?: MiSystemWebhook['isActive'];
+		on?: MiSystemWebhook['on'];
+	}): Promise<MiSystemWebhook[]> {
+		const query = this.systemWebhooksRepository.createQueryBuilder('systemWebhook');
+		if (params) {
+			if (params.ids && params.ids.length > 0) {
+				query.andWhere('systemWebhook.id IN (:...ids)', { ids: params.ids });
+			}
+			if (params.isActive !== undefined) {
+				query.andWhere('systemWebhook.isActive = :isActive', { isActive: params.isActive });
+			}
+			if (params.on && params.on.length > 0) {
+				query.andWhere(':on <@ systemWebhook.on', { on: params.on });
+			}
+		}
+
+		return query.getMany();
+	}
+
+	/**
+	 * SystemWebhook を作成する.
+	 */
+	@bindThis
+	public async createSystemWebhook(
+		params: {
+			isActive: MiSystemWebhook['isActive'];
+			name: MiSystemWebhook['name'];
+			on: MiSystemWebhook['on'];
+			url: MiSystemWebhook['url'];
+			secret: MiSystemWebhook['secret'];
+		},
+		updater: MiUser,
+	): Promise<MiSystemWebhook> {
+		const id = this.idService.gen();
+		await this.systemWebhooksRepository.insert({
+			...params,
+			id,
+		});
+
+		const webhook = await this.systemWebhooksRepository.findOneByOrFail({ id });
+		this.globalEventService.publishInternalEvent('systemWebhookCreated', webhook);
+		this.moderationLogService
+			.log(updater, 'createSystemWebhook', {
+				systemWebhookId: webhook.id,
+				webhook: webhook,
+			});
+
+		return webhook;
+	}
+
+	/**
+	 * SystemWebhook を更新する.
+	 */
+	@bindThis
+	public async updateSystemWebhook(
+		params: {
+			id: MiSystemWebhook['id'];
+			isActive: MiSystemWebhook['isActive'];
+			name: MiSystemWebhook['name'];
+			on: MiSystemWebhook['on'];
+			url: MiSystemWebhook['url'];
+			secret: MiSystemWebhook['secret'];
+		},
+		updater: MiUser,
+	): Promise<MiSystemWebhook> {
+		const beforeEntity = await this.systemWebhooksRepository.findOneByOrFail({ id: params.id });
+		await this.systemWebhooksRepository.update(beforeEntity.id, {
+			updatedAt: new Date(),
+			isActive: params.isActive,
+			name: params.name,
+			on: params.on,
+			url: params.url,
+			secret: params.secret,
+		});
+
+		const afterEntity = await this.systemWebhooksRepository.findOneByOrFail({ id: beforeEntity.id });
+		this.globalEventService.publishInternalEvent('systemWebhookUpdated', afterEntity);
+		this.moderationLogService
+			.log(updater, 'updateSystemWebhook', {
+				systemWebhookId: beforeEntity.id,
+				before: beforeEntity,
+				after: afterEntity,
+			});
+
+		return afterEntity;
+	}
+
+	/**
+	 * SystemWebhook を削除する.
+	 */
+	@bindThis
+	public async deleteSystemWebhook(id: MiSystemWebhook['id'], updater: MiUser) {
+		const webhook = await this.systemWebhooksRepository.findOneByOrFail({ id });
+		await this.systemWebhooksRepository.delete(id);
+
+		this.globalEventService.publishInternalEvent('systemWebhookDeleted', webhook);
+		this.moderationLogService
+			.log(updater, 'deleteSystemWebhook', {
+				systemWebhookId: webhook.id,
+				webhook,
+			});
+	}
+
+	/**
+	 * SystemWebhook をWebhook配送キューに追加する
+	 * @see QueueService.systemWebhookDeliver
+	 * // TODO: contentの型を厳格化する
+	 */
+	@bindThis
+	public async enqueueSystemWebhook<T extends SystemWebhookEventType>(
+		webhook: MiSystemWebhook | MiSystemWebhook['id'],
+		type: T,
+		content: unknown,
+	) {
+		const webhookEntity = typeof webhook === 'string'
+			? (await this.fetchActiveSystemWebhooks()).find(a => a.id === webhook)
+			: webhook;
+		if (!webhookEntity || !webhookEntity.isActive) {
+			this.logger.info(`SystemWebhook is not active or not found : ${webhook}`);
+			return;
+		}
+
+		if (!webhookEntity.on.includes(type)) {
+			this.logger.info(`SystemWebhook ${webhookEntity.id} is not listening to ${type}`);
+			return;
+		}
+
+		return this.queueService.systemWebhookDeliver(webhookEntity, type, content);
+	}
+
+	@bindThis
+	private async onMessage(_: string, data: string): Promise<void> {
+		const obj = JSON.parse(data);
+		if (obj.channel !== 'internal') {
+			return;
+		}
+
+		const { type, body } = obj.message as GlobalEvents['internal']['payload'];
+		switch (type) {
+			case 'systemWebhookCreated': {
+				if (body.isActive) {
+					this.activeSystemWebhooks.push(MiSystemWebhook.deserialize(body));
+				}
+				break;
+			}
+			case 'systemWebhookUpdated': {
+				if (body.isActive) {
+					const i = this.activeSystemWebhooks.findIndex(a => a.id === body.id);
+					if (i > -1) {
+						this.activeSystemWebhooks[i] = MiSystemWebhook.deserialize(body);
+					} else {
+						this.activeSystemWebhooks.push(MiSystemWebhook.deserialize(body));
+					}
+				} else {
+					this.activeSystemWebhooks = this.activeSystemWebhooks.filter(a => a.id !== body.id);
+				}
+				break;
+			}
+			case 'systemWebhookDeleted': {
+				this.activeSystemWebhooks = this.activeSystemWebhooks.filter(a => a.id !== body.id);
+				break;
+			}
+			default:
+				break;
+		}
+	}
+
+	@bindThis
+	public dispose(): void {
+		this.redisForSub.off('message', this.onMessage);
+	}
+
+	@bindThis
+	public onApplicationShutdown(signal?: string | undefined): void {
+		this.dispose();
+	}
+}
diff --git a/packages/backend/src/core/UserBlockingService.ts b/packages/backend/src/core/UserBlockingService.ts
index 96f389b54c..2f1310b8ef 100644
--- a/packages/backend/src/core/UserBlockingService.ts
+++ b/packages/backend/src/core/UserBlockingService.ts
@@ -16,7 +16,7 @@ import Logger from '@/logger.js';
 import { UserEntityService } from '@/core/entities/UserEntityService.js';
 import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
 import { LoggerService } from '@/core/LoggerService.js';
-import { WebhookService } from '@/core/WebhookService.js';
+import { UserWebhookService } from '@/core/UserWebhookService.js';
 import { bindThis } from '@/decorators.js';
 import { CacheService } from '@/core/CacheService.js';
 import { UserFollowingService } from '@/core/UserFollowingService.js';
@@ -46,7 +46,7 @@ export class UserBlockingService implements OnModuleInit {
 		private idService: IdService,
 		private queueService: QueueService,
 		private globalEventService: GlobalEventService,
-		private webhookService: WebhookService,
+		private webhookService: UserWebhookService,
 		private apRendererService: ApRendererService,
 		private loggerService: LoggerService,
 	) {
@@ -121,7 +121,7 @@ export class UserBlockingService implements OnModuleInit {
 
 				const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('unfollow'));
 				for (const webhook of webhooks) {
-					this.queueService.webhookDeliver(webhook, 'unfollow', {
+					this.queueService.userWebhookDeliver(webhook, 'unfollow', {
 						user: packed,
 					});
 				}
diff --git a/packages/backend/src/core/UserFollowingService.ts b/packages/backend/src/core/UserFollowingService.ts
index 406ea04031..8963003057 100644
--- a/packages/backend/src/core/UserFollowingService.ts
+++ b/packages/backend/src/core/UserFollowingService.ts
@@ -13,23 +13,20 @@ import PerUserFollowingChart from '@/core/chart/charts/per-user-following.js';
 import { GlobalEventService } from '@/core/GlobalEventService.js';
 import { IdService } from '@/core/IdService.js';
 import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error.js';
-import type { Packed } from '@/misc/json-schema.js';
 import InstanceChart from '@/core/chart/charts/instance.js';
 import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
-import { WebhookService } from '@/core/WebhookService.js';
+import { UserWebhookService } from '@/core/UserWebhookService.js';
 import { NotificationService } from '@/core/NotificationService.js';
 import { DI } from '@/di-symbols.js';
-import type { FollowingsRepository, FollowRequestsRepository, InstancesRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js';
+import type { FollowingsRepository, FollowRequestsRepository, InstancesRepository, MiMeta, UserProfilesRepository, UsersRepository } from '@/models/_.js';
 import { UserEntityService } from '@/core/entities/UserEntityService.js';
 import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
 import { bindThis } from '@/decorators.js';
 import { UserBlockingService } from '@/core/UserBlockingService.js';
-import { MetaService } from '@/core/MetaService.js';
 import { CacheService } from '@/core/CacheService.js';
 import type { Config } from '@/config.js';
 import { AccountMoveService } from '@/core/AccountMoveService.js';
 import { UtilityService } from '@/core/UtilityService.js';
-import { FanoutTimelineService } from '@/core/FanoutTimelineService.js';
 import type { ThinUser } from '@/queue/types.js';
 import Logger from '../logger.js';
 
@@ -58,6 +55,9 @@ export class UserFollowingService implements OnModuleInit {
 		@Inject(DI.config)
 		private config: Config,
 
+		@Inject(DI.meta)
+		private meta: MiMeta,
+
 		@Inject(DI.usersRepository)
 		private usersRepository: UsersRepository,
 
@@ -79,13 +79,11 @@ export class UserFollowingService implements OnModuleInit {
 		private idService: IdService,
 		private queueService: QueueService,
 		private globalEventService: GlobalEventService,
-		private metaService: MetaService,
 		private notificationService: NotificationService,
 		private federatedInstanceService: FederatedInstanceService,
-		private webhookService: WebhookService,
+		private webhookService: UserWebhookService,
 		private apRendererService: ApRendererService,
 		private accountMoveService: AccountMoveService,
-		private fanoutTimelineService: FanoutTimelineService,
 		private perUserFollowingChart: PerUserFollowingChart,
 		private instanceChart: InstanceChart,
 	) {
@@ -172,7 +170,7 @@ export class UserFollowingService implements OnModuleInit {
 			followee.isLocked ||
 			(followeeProfile.carefulBot && follower.isBot) ||
 			(this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee) && process.env.FORCE_FOLLOW_REMOTE_USER_FOR_TESTING !== 'true') ||
-			(this.userEntityService.isLocalUser(followee) && this.userEntityService.isRemoteUser(follower) && this.utilityService.isSilencedHost((await this.metaService.fetch()).silencedHosts, follower.host))
+			(this.userEntityService.isLocalUser(followee) && this.userEntityService.isRemoteUser(follower) && this.utilityService.isSilencedHost(this.meta.silencedHosts, follower.host))
 		) {
 			let autoAccept = false;
 
@@ -277,14 +275,19 @@ export class UserFollowingService implements OnModuleInit {
 				followeeId: followee.id,
 				followerId: follower.id,
 			});
-
-			// 通知を作成
-			this.notificationService.createNotification(follower.id, 'followRequestAccepted', {
-			}, followee.id);
 		}
 
 		if (alreadyFollowed) return;
 
+		// 通知を作成
+		if (follower.host === null) {
+			const profile = await this.cacheService.userProfileCache.fetch(followee.id);
+
+			this.notificationService.createNotification(follower.id, 'followRequestAccepted', {
+				message: profile.followedMessage,
+			}, followee.id);
+		}
+
 		this.globalEventService.publishInternalEvent('follow', { followerId: follower.id, followeeId: followee.id });
 
 		const [followeeUser, followerUser] = await Promise.all([
@@ -302,20 +305,22 @@ export class UserFollowingService implements OnModuleInit {
 			//#endregion
 
 			//#region Update instance stats
-			if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) {
-				this.federatedInstanceService.fetch(follower.host).then(async i => {
-					this.instancesRepository.increment({ id: i.id }, 'followingCount', 1);
-					if ((await this.metaService.fetch()).enableChartsForFederatedInstances) {
-						this.instanceChart.updateFollowing(i.host, true);
-					}
-				});
-			} else if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) {
-				this.federatedInstanceService.fetch(followee.host).then(async i => {
-					this.instancesRepository.increment({ id: i.id }, 'followersCount', 1);
-					if ((await this.metaService.fetch()).enableChartsForFederatedInstances) {
-						this.instanceChart.updateFollowers(i.host, true);
-					}
-				});
+			if (this.meta.enableStatsForFederatedInstances) {
+				if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) {
+					this.federatedInstanceService.fetchOrRegister(follower.host).then(async i => {
+						this.instancesRepository.increment({ id: i.id }, 'followingCount', 1);
+						if (this.meta.enableChartsForFederatedInstances) {
+							this.instanceChart.updateFollowing(i.host, true);
+						}
+					});
+				} else if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) {
+					this.federatedInstanceService.fetchOrRegister(followee.host).then(async i => {
+						this.instancesRepository.increment({ id: i.id }, 'followersCount', 1);
+						if (this.meta.enableChartsForFederatedInstances) {
+							this.instanceChart.updateFollowers(i.host, true);
+						}
+					});
+				}
 			}
 			//#endregion
 
@@ -331,7 +336,7 @@ export class UserFollowingService implements OnModuleInit {
 
 				const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('follow'));
 				for (const webhook of webhooks) {
-					this.queueService.webhookDeliver(webhook, 'follow', {
+					this.queueService.userWebhookDeliver(webhook, 'follow', {
 						user: packed,
 					});
 				}
@@ -345,7 +350,7 @@ export class UserFollowingService implements OnModuleInit {
 
 				const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === followee.id && x.on.includes('followed'));
 				for (const webhook of webhooks) {
-					this.queueService.webhookDeliver(webhook, 'followed', {
+					this.queueService.userWebhookDeliver(webhook, 'followed', {
 						user: packed,
 					});
 				}
@@ -398,7 +403,7 @@ export class UserFollowingService implements OnModuleInit {
 
 				const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('unfollow'));
 				for (const webhook of webhooks) {
-					this.queueService.webhookDeliver(webhook, 'unfollow', {
+					this.queueService.userWebhookDeliver(webhook, 'unfollow', {
 						user: packed,
 					});
 				}
@@ -434,20 +439,22 @@ export class UserFollowingService implements OnModuleInit {
 			//#endregion
 
 			//#region Update instance stats
-			if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) {
-				this.federatedInstanceService.fetch(follower.host).then(async i => {
-					this.instancesRepository.decrement({ id: i.id }, 'followingCount', 1);
-					if ((await this.metaService.fetch()).enableChartsForFederatedInstances) {
-						this.instanceChart.updateFollowing(i.host, false);
-					}
-				});
-			} else if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) {
-				this.federatedInstanceService.fetch(followee.host).then(async i => {
-					this.instancesRepository.decrement({ id: i.id }, 'followersCount', 1);
-					if ((await this.metaService.fetch()).enableChartsForFederatedInstances) {
-						this.instanceChart.updateFollowers(i.host, false);
-					}
-				});
+			if (this.meta.enableStatsForFederatedInstances) {
+				if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) {
+					this.federatedInstanceService.fetchOrRegister(follower.host).then(async i => {
+						this.instancesRepository.decrement({ id: i.id }, 'followingCount', 1);
+						if (this.meta.enableChartsForFederatedInstances) {
+							this.instanceChart.updateFollowing(i.host, false);
+						}
+					});
+				} else if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) {
+					this.federatedInstanceService.fetchOrRegister(followee.host).then(async i => {
+						this.instancesRepository.decrement({ id: i.id }, 'followersCount', 1);
+						if (this.meta.enableChartsForFederatedInstances) {
+							this.instanceChart.updateFollowers(i.host, false);
+						}
+					});
+				}
 			}
 			//#endregion
 
@@ -740,7 +747,7 @@ export class UserFollowingService implements OnModuleInit {
 
 		const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('unfollow'));
 		for (const webhook of webhooks) {
-			this.queueService.webhookDeliver(webhook, 'unfollow', {
+			this.queueService.userWebhookDeliver(webhook, 'unfollow', {
 				user: packedFollowee,
 			});
 		}
diff --git a/packages/backend/src/core/UserKeypairService.ts b/packages/backend/src/core/UserKeypairService.ts
index 51ac99179a..92d61cd103 100644
--- a/packages/backend/src/core/UserKeypairService.ts
+++ b/packages/backend/src/core/UserKeypairService.ts
@@ -25,7 +25,7 @@ export class UserKeypairService implements OnApplicationShutdown {
 	) {
 		this.cache = new RedisKVCache<MiUserKeypair>(this.redisClient, 'userKeypair', {
 			lifetime: 1000 * 60 * 60 * 24, // 24h
-			memoryCacheLifetime: Infinity,
+			memoryCacheLifetime: 1000 * 60 * 60, // 1h
 			fetcher: (key) => this.userKeypairsRepository.findOneByOrFail({ userId: key }),
 			toRedisConverter: (value) => JSON.stringify(value),
 			fromRedisConverter: (value) => JSON.parse(value),
diff --git a/packages/backend/src/core/UserListService.ts b/packages/backend/src/core/UserListService.ts
index bbdcfed738..6333356fe9 100644
--- a/packages/backend/src/core/UserListService.ts
+++ b/packages/backend/src/core/UserListService.ts
@@ -95,7 +95,7 @@ export class UserListService implements OnApplicationShutdown, OnModuleInit {
 		const currentCount = await this.userListMembershipsRepository.countBy({
 			userListId: list.id,
 		});
-		if (currentCount > (await this.roleService.getUserPolicies(me.id)).userEachUserListsLimit) {
+		if (currentCount >= (await this.roleService.getUserPolicies(me.id)).userEachUserListsLimit) {
 			throw new UserListService.TooManyUsersError();
 		}
 
diff --git a/packages/backend/src/core/UserRenoteMutingService.ts b/packages/backend/src/core/UserRenoteMutingService.ts
new file mode 100644
index 0000000000..bdc5e23f4b
--- /dev/null
+++ b/packages/backend/src/core/UserRenoteMutingService.ts
@@ -0,0 +1,52 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Inject, Injectable } from '@nestjs/common';
+import { In } from 'typeorm';
+import type { RenoteMutingsRepository } from '@/models/_.js';
+import type { MiRenoteMuting } from '@/models/RenoteMuting.js';
+
+import { IdService } from '@/core/IdService.js';
+import type { MiUser } from '@/models/User.js';
+import { DI } from '@/di-symbols.js';
+import { bindThis } from '@/decorators.js';
+import { CacheService } from '@/core/CacheService.js';
+
+@Injectable()
+export class UserRenoteMutingService {
+	constructor(
+		@Inject(DI.renoteMutingsRepository)
+		private renoteMutingsRepository: RenoteMutingsRepository,
+
+		private idService: IdService,
+		private cacheService: CacheService,
+	) {
+	}
+
+	@bindThis
+	public async mute(user: MiUser, target: MiUser, expiresAt: Date | null = null): Promise<void> {
+		await this.renoteMutingsRepository.insert({
+			id: this.idService.gen(),
+			muterId: user.id,
+			muteeId: target.id,
+		});
+
+		await this.cacheService.renoteMutingsCache.refresh(user.id);
+	}
+
+	@bindThis
+	public async unmute(mutings: MiRenoteMuting[]): Promise<void> {
+		if (mutings.length === 0) return;
+
+		await this.renoteMutingsRepository.delete({
+			id: In(mutings.map(m => m.id)),
+		});
+
+		const muterIds = [...new Set(mutings.map(m => m.muterId))];
+		for (const muterId of muterIds) {
+			await this.cacheService.renoteMutingsCache.refresh(muterId);
+		}
+	}
+}
diff --git a/packages/backend/src/core/UserSearchService.ts b/packages/backend/src/core/UserSearchService.ts
new file mode 100644
index 0000000000..0d03cf6ee0
--- /dev/null
+++ b/packages/backend/src/core/UserSearchService.ts
@@ -0,0 +1,205 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Inject, Injectable } from '@nestjs/common';
+import { Brackets, SelectQueryBuilder } from 'typeorm';
+import { DI } from '@/di-symbols.js';
+import { type FollowingsRepository, MiUser, type UsersRepository } from '@/models/_.js';
+import { bindThis } from '@/decorators.js';
+import { sqlLikeEscape } from '@/misc/sql-like-escape.js';
+import type { Config } from '@/config.js';
+import { UserEntityService } from '@/core/entities/UserEntityService.js';
+import { Packed } from '@/misc/json-schema.js';
+
+function defaultActiveThreshold() {
+	return new Date(Date.now() - 1000 * 60 * 60 * 24 * 30);
+}
+
+@Injectable()
+export class UserSearchService {
+	constructor(
+		@Inject(DI.config)
+		private config: Config,
+		@Inject(DI.usersRepository)
+		private usersRepository: UsersRepository,
+		@Inject(DI.followingsRepository)
+		private followingsRepository: FollowingsRepository,
+		private userEntityService: UserEntityService,
+	) {
+	}
+
+	/**
+	 * ユーザ名とホスト名によるユーザ検索を行う.
+	 *
+	 * - 検索結果には優先順位がつけられており、以下の順序で検索が行われる.
+	 *   1. フォローしているユーザのうち、一定期間以内(※)に更新されたユーザ
+	 *   2. フォローしているユーザのうち、一定期間以内に更新されていないユーザ
+	 *   3. フォローしていないユーザのうち、一定期間以内に更新されたユーザ
+	 *   4. フォローしていないユーザのうち、一定期間以内に更新されていないユーザ
+	 * - ログインしていない場合は、以下の順序で検索が行われる.
+	 *   1. 一定期間以内に更新されたユーザ
+	 *   2. 一定期間以内に更新されていないユーザ
+	 * - それぞれの検索結果はユーザ名の昇順でソートされる.
+	 * - 動作的には先に登場した検索結果の登場位置が優先される(条件的にユーザIDが重複することはないが).
+	 *   (1で既にヒットしていた場合、2, 3, 4でヒットしても無視される)
+	 * - ユーザ名とホスト名の検索条件はそれぞれ前方一致で検索される.
+	 * - ユーザ名の検索は大文字小文字を区別しない.
+	 * - ホスト名の検索は大文字小文字を区別しない.
+	 * - 検索結果は最大で {@link opts.limit} 件までとなる.
+	 *
+	 * ※一定期間とは {@link params.activeThreshold} で指定された日時から現在までの期間を指す.
+	 *
+	 * @param params 検索条件.
+	 * @param opts 関数の動作を制御するオプション.
+	 * @param me 検索を実行するユーザの情報. 未ログインの場合は指定しない.
+	 * @see {@link UserSearchService#buildSearchUserQueries}
+	 * @see {@link UserSearchService#buildSearchUserNoLoginQueries}
+	 */
+	@bindThis
+	public async search(
+		params: {
+			username?: string | null,
+			host?: string | null,
+			activeThreshold?: Date,
+		},
+		opts?: {
+			limit?: number,
+			detail?: boolean,
+		},
+		me?: MiUser | null,
+	): Promise<Packed<'User'>[]> {
+		const queries = me ? this.buildSearchUserQueries(me, params) : this.buildSearchUserNoLoginQueries(params);
+
+		let resultSet = new Set<MiUser['id']>();
+		const limit = opts?.limit ?? 10;
+		for (const query of queries) {
+			const ids = await query
+				.select('user.id')
+				.limit(limit - resultSet.size)
+				.orderBy('user.usernameLower', 'ASC')
+				.getRawMany<{ user_id: MiUser['id'] }>()
+				.then(res => res.map(x => x.user_id));
+
+			resultSet = new Set([...resultSet, ...ids]);
+			if (resultSet.size >= limit) {
+				break;
+			}
+		}
+
+		return this.userEntityService.packMany<'UserLite' | 'UserDetailed'>(
+			[...resultSet].slice(0, limit),
+			me,
+			{ schema: opts?.detail ? 'UserDetailed' : 'UserLite' },
+		);
+	}
+
+	/**
+	 * ログイン済みユーザによる検索実行時のクエリ一覧を構築する.
+	 * @param me
+	 * @param params
+	 * @private
+	 */
+	@bindThis
+	private buildSearchUserQueries(
+		me: MiUser,
+		params: {
+			username?: string | null,
+			host?: string | null,
+			activeThreshold?: Date,
+		},
+	) {
+		// デフォルト30日以内に更新されたユーザーをアクティブユーザーとする
+		const activeThreshold = params.activeThreshold ?? defaultActiveThreshold();
+
+		const followingUserQuery = this.followingsRepository.createQueryBuilder('following')
+			.select('following.followeeId')
+			.where('following.followerId = :followerId', { followerId: me.id });
+
+		const activeFollowingUsersQuery = this.generateUserQueryBuilder(params)
+			.andWhere(`user.id IN (${followingUserQuery.getQuery()})`)
+			.andWhere('user.updatedAt > :activeThreshold', { activeThreshold });
+		activeFollowingUsersQuery.setParameters(followingUserQuery.getParameters());
+
+		const inactiveFollowingUsersQuery = this.generateUserQueryBuilder(params)
+			.andWhere(`user.id IN (${followingUserQuery.getQuery()})`)
+			.andWhere(new Brackets(qb => {
+				qb
+					.where('user.updatedAt IS NULL')
+					.orWhere('user.updatedAt <= :activeThreshold', { activeThreshold });
+			}));
+		inactiveFollowingUsersQuery.setParameters(followingUserQuery.getParameters());
+
+		// 自分自身がヒットするとしたらここ
+		const activeUserQuery = this.generateUserQueryBuilder(params)
+			.andWhere(`user.id NOT IN (${followingUserQuery.getQuery()})`)
+			.andWhere('user.updatedAt > :activeThreshold', { activeThreshold });
+		activeUserQuery.setParameters(followingUserQuery.getParameters());
+
+		const inactiveUserQuery = this.generateUserQueryBuilder(params)
+			.andWhere(`user.id NOT IN (${followingUserQuery.getQuery()})`)
+			.andWhere('user.updatedAt <= :activeThreshold', { activeThreshold });
+		inactiveUserQuery.setParameters(followingUserQuery.getParameters());
+
+		return [activeFollowingUsersQuery, inactiveFollowingUsersQuery, activeUserQuery, inactiveUserQuery];
+	}
+
+	/**
+	 * ログインしていないユーザによる検索実行時のクエリ一覧を構築する.
+	 * @param params
+	 * @private
+	 */
+	@bindThis
+	private buildSearchUserNoLoginQueries(params: {
+		username?: string | null,
+		host?: string | null,
+		activeThreshold?: Date,
+	}) {
+		// デフォルト30日以内に更新されたユーザーをアクティブユーザーとする
+		const activeThreshold = params.activeThreshold ?? defaultActiveThreshold();
+
+		const activeUserQuery = this.generateUserQueryBuilder(params)
+			.andWhere(new Brackets(qb => {
+				qb
+					.where('user.updatedAt IS NULL')
+					.orWhere('user.updatedAt > :activeThreshold', { activeThreshold });
+			}));
+
+		const inactiveUserQuery = this.generateUserQueryBuilder(params)
+			.andWhere('user.updatedAt <= :activeThreshold', { activeThreshold });
+
+		return [activeUserQuery, inactiveUserQuery];
+	}
+
+	/**
+	 * ユーザ検索クエリで共通する抽出条件をあらかじめ設定したクエリビルダを生成する.
+	 * @param params
+	 * @private
+	 */
+	@bindThis
+	private generateUserQueryBuilder(params: {
+		username?: string | null,
+		host?: string | null,
+	}): SelectQueryBuilder<MiUser> {
+		const userQuery = this.usersRepository.createQueryBuilder('user');
+
+		if (params.username) {
+			userQuery.andWhere('user.usernameLower LIKE :username', { username: sqlLikeEscape(params.username.toLowerCase()) + '%' });
+		}
+
+		if (params.host) {
+			if (params.host === this.config.hostname || params.host === '.') {
+				userQuery.andWhere('user.host IS NULL');
+			} else {
+				userQuery.andWhere('user.host LIKE :host', {
+					host: sqlLikeEscape(params.host.toLowerCase()) + '%',
+				});
+			}
+		}
+
+		userQuery.andWhere('user.isSuspended = FALSE');
+
+		return userQuery;
+	}
+}
diff --git a/packages/backend/src/core/UserService.ts b/packages/backend/src/core/UserService.ts
index 72fa4d928d..9b1961c631 100644
--- a/packages/backend/src/core/UserService.ts
+++ b/packages/backend/src/core/UserService.ts
@@ -8,15 +8,18 @@ import type { FollowingsRepository, UsersRepository } from '@/models/_.js';
 import type { MiUser } from '@/models/User.js';
 import { DI } from '@/di-symbols.js';
 import { bindThis } from '@/decorators.js';
+import { SystemWebhookService } from '@/core/SystemWebhookService.js';
+import { UserEntityService } from '@/core/entities/UserEntityService.js';
 
 @Injectable()
 export class UserService {
 	constructor(
 		@Inject(DI.usersRepository)
 		private usersRepository: UsersRepository,
-
 		@Inject(DI.followingsRepository)
 		private followingsRepository: FollowingsRepository,
+		private systemWebhookService: SystemWebhookService,
+		private userEntityService: UserEntityService,
 	) {
 	}
 
@@ -50,4 +53,23 @@ export class UserService {
 			});
 		}
 	}
+
+	/**
+	 * SystemWebhookを用いてユーザに関する操作内容を管理者各位に通知する.
+	 * ここではJobQueueへのエンキューのみを行うため、即時実行されない.
+	 *
+	 * @see SystemWebhookService.enqueueSystemWebhook
+	 */
+	@bindThis
+	public async notifySystemWebhook(user: MiUser, type: 'userCreated') {
+		const packedUser = await this.userEntityService.pack(user, null, { schema: 'UserLite' });
+		const recipientWebhookIds = await this.systemWebhookService.fetchSystemWebhooks({ isActive: true, on: [type] });
+		for (const webhookId of recipientWebhookIds) {
+			await this.systemWebhookService.enqueueSystemWebhook(
+				webhookId,
+				type,
+				packedUser,
+			);
+		}
+	}
 }
diff --git a/packages/backend/src/core/UserSuspendService.ts b/packages/backend/src/core/UserSuspendService.ts
index d594a223f4..7920e58e36 100644
--- a/packages/backend/src/core/UserSuspendService.ts
+++ b/packages/backend/src/core/UserSuspendService.ts
@@ -5,7 +5,7 @@
 
 import { Inject, Injectable } from '@nestjs/common';
 import { Not, IsNull } from 'typeorm';
-import type { FollowingsRepository } from '@/models/_.js';
+import type { FollowingsRepository, FollowRequestsRepository, UsersRepository } from '@/models/_.js';
 import type { MiUser } from '@/models/User.js';
 import { QueueService } from '@/core/QueueService.js';
 import { GlobalEventService } from '@/core/GlobalEventService.js';
@@ -13,24 +13,75 @@ import { DI } from '@/di-symbols.js';
 import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
 import { UserEntityService } from '@/core/entities/UserEntityService.js';
 import { bindThis } from '@/decorators.js';
+import { RelationshipJobData } from '@/queue/types.js';
+import { ModerationLogService } from '@/core/ModerationLogService.js';
 
 @Injectable()
 export class UserSuspendService {
 	constructor(
+		@Inject(DI.usersRepository)
+		private usersRepository: UsersRepository,
+
 		@Inject(DI.followingsRepository)
 		private followingsRepository: FollowingsRepository,
 
+		@Inject(DI.followRequestsRepository)
+		private followRequestsRepository: FollowRequestsRepository,
+
 		private userEntityService: UserEntityService,
 		private queueService: QueueService,
 		private globalEventService: GlobalEventService,
 		private apRendererService: ApRendererService,
+		private moderationLogService: ModerationLogService,
 	) {
 	}
 
 	@bindThis
-	public async doPostSuspend(user: { id: MiUser['id']; host: MiUser['host'] }): Promise<void> {
+	public async suspend(user: MiUser, moderator: MiUser): Promise<void> {
+		await this.usersRepository.update(user.id, {
+			isSuspended: true,
+		});
+
+		this.moderationLogService.log(moderator, 'suspend', {
+			userId: user.id,
+			userUsername: user.username,
+			userHost: user.host,
+		});
+
+		(async () => {
+			await this.postSuspend(user).catch(e => {});
+			await this.unFollowAll(user).catch(e => {});
+		})();
+	}
+
+	@bindThis
+	public async unsuspend(user: MiUser, moderator: MiUser): Promise<void> {
+		await this.usersRepository.update(user.id, {
+			isSuspended: false,
+		});
+
+		this.moderationLogService.log(moderator, 'unsuspend', {
+			userId: user.id,
+			userUsername: user.username,
+			userHost: user.host,
+		});
+
+		(async () => {
+			await this.postUnsuspend(user).catch(e => {});
+		})();
+	}
+
+	@bindThis
+	private async postSuspend(user: { id: MiUser['id']; host: MiUser['host'] }): Promise<void> {
 		this.globalEventService.publishInternalEvent('userChangeSuspendedState', { id: user.id, isSuspended: true });
 
+		this.followRequestsRepository.delete({
+			followeeId: user.id,
+		});
+		this.followRequestsRepository.delete({
+			followerId: user.id,
+		});
+
 		if (this.userEntityService.isLocalUser(user)) {
 			// 知り得る全SharedInboxにDelete配信
 			const content = this.apRendererService.addContext(this.apRendererService.renderDelete(this.userEntityService.genLocalUserUri(user.id), user));
@@ -58,7 +109,7 @@ export class UserSuspendService {
 	}
 
 	@bindThis
-	public async doPostUnsuspend(user: MiUser): Promise<void> {
+	private async postUnsuspend(user: MiUser): Promise<void> {
 		this.globalEventService.publishInternalEvent('userChangeSuspendedState', { id: user.id, isSuspended: false });
 
 		if (this.userEntityService.isLocalUser(user)) {
@@ -86,4 +137,26 @@ export class UserSuspendService {
 			}
 		}
 	}
+
+	@bindThis
+	private async unFollowAll(follower: MiUser) {
+		const followings = await this.followingsRepository.find({
+			where: {
+				followerId: follower.id,
+				followeeId: Not(IsNull()),
+			},
+		});
+
+		const jobs: RelationshipJobData[] = [];
+		for (const following of followings) {
+			if (following.followeeId && following.followerId) {
+				jobs.push({
+					from: { id: following.followerId },
+					to: { id: following.followeeId },
+					silent: true,
+				});
+			}
+		}
+		this.queueService.createUnfollowJob(jobs);
+	}
 }
diff --git a/packages/backend/src/core/UserWebhookService.ts b/packages/backend/src/core/UserWebhookService.ts
new file mode 100644
index 0000000000..8a40a53688
--- /dev/null
+++ b/packages/backend/src/core/UserWebhookService.ts
@@ -0,0 +1,124 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Inject, Injectable } from '@nestjs/common';
+import * as Redis from 'ioredis';
+import { type WebhooksRepository } from '@/models/_.js';
+import { MiWebhook } from '@/models/Webhook.js';
+import { DI } from '@/di-symbols.js';
+import { bindThis } from '@/decorators.js';
+import { GlobalEvents } from '@/core/GlobalEventService.js';
+import type { OnApplicationShutdown } from '@nestjs/common';
+
+@Injectable()
+export class UserWebhookService implements OnApplicationShutdown {
+	private activeWebhooksFetched = false;
+	private activeWebhooks: MiWebhook[] = [];
+
+	constructor(
+		@Inject(DI.redisForSub)
+		private redisForSub: Redis.Redis,
+		@Inject(DI.webhooksRepository)
+		private webhooksRepository: WebhooksRepository,
+	) {
+		this.redisForSub.on('message', this.onMessage);
+	}
+
+	@bindThis
+	public async getActiveWebhooks() {
+		if (!this.activeWebhooksFetched) {
+			this.activeWebhooks = await this.webhooksRepository.findBy({
+				active: true,
+			});
+			this.activeWebhooksFetched = true;
+		}
+
+		return this.activeWebhooks;
+	}
+
+	/**
+	 * UserWebhook の一覧を取得する.
+	 */
+	@bindThis
+	public fetchWebhooks(params?: {
+		ids?: MiWebhook['id'][];
+		isActive?: MiWebhook['active'];
+		on?: MiWebhook['on'];
+	}): Promise<MiWebhook[]> {
+		const query = this.webhooksRepository.createQueryBuilder('webhook');
+		if (params) {
+			if (params.ids && params.ids.length > 0) {
+				query.andWhere('webhook.id IN (:...ids)', { ids: params.ids });
+			}
+			if (params.isActive !== undefined) {
+				query.andWhere('webhook.active = :isActive', { isActive: params.isActive });
+			}
+			if (params.on && params.on.length > 0) {
+				query.andWhere(':on <@ webhook.on', { on: params.on });
+			}
+		}
+
+		return query.getMany();
+	}
+
+	@bindThis
+	private async onMessage(_: string, data: string): Promise<void> {
+		const obj = JSON.parse(data);
+		if (obj.channel !== 'internal') {
+			return;
+		}
+
+		const { type, body } = obj.message as GlobalEvents['internal']['payload'];
+		switch (type) {
+			case 'webhookCreated': {
+				if (body.active) {
+					this.activeWebhooks.push({ // TODO: このあたりのデシリアライズ処理は各modelファイル内に関数としてexportしたい
+						...body,
+						latestSentAt: body.latestSentAt ? new Date(body.latestSentAt) : null,
+						user: null, // joinなカラムは通常取ってこないので
+					});
+				}
+				break;
+			}
+			case 'webhookUpdated': {
+				if (body.active) {
+					const i = this.activeWebhooks.findIndex(a => a.id === body.id);
+					if (i > -1) {
+						this.activeWebhooks[i] = { // TODO: このあたりのデシリアライズ処理は各modelファイル内に関数としてexportしたい
+							...body,
+							latestSentAt: body.latestSentAt ? new Date(body.latestSentAt) : null,
+							user: null, // joinなカラムは通常取ってこないので
+						};
+					} else {
+						this.activeWebhooks.push({ // TODO: このあたりのデシリアライズ処理は各modelファイル内に関数としてexportしたい
+							...body,
+							latestSentAt: body.latestSentAt ? new Date(body.latestSentAt) : null,
+							user: null, // joinなカラムは通常取ってこないので
+						});
+					}
+				} else {
+					this.activeWebhooks = this.activeWebhooks.filter(a => a.id !== body.id);
+				}
+				break;
+			}
+			case 'webhookDeleted': {
+				this.activeWebhooks = this.activeWebhooks.filter(a => a.id !== body.id);
+				break;
+			}
+			default:
+				break;
+		}
+	}
+
+	@bindThis
+	public dispose(): void {
+		this.redisForSub.off('message', this.onMessage);
+	}
+
+	@bindThis
+	public onApplicationShutdown(signal?: string | undefined): void {
+		this.dispose();
+	}
+}
diff --git a/packages/backend/src/core/UtilityService.ts b/packages/backend/src/core/UtilityService.ts
index 652e8f7449..86082ccdcd 100644
--- a/packages/backend/src/core/UtilityService.ts
+++ b/packages/backend/src/core/UtilityService.ts
@@ -10,12 +10,16 @@ import RE2 from 're2';
 import { DI } from '@/di-symbols.js';
 import type { Config } from '@/config.js';
 import { bindThis } from '@/decorators.js';
+import { MiMeta } from '@/models/Meta.js';
 
 @Injectable()
 export class UtilityService {
 	constructor(
 		@Inject(DI.config)
 		private config: Config,
+
+		@Inject(DI.meta)
+		private meta: MiMeta,
 	) {
 	}
 
@@ -42,6 +46,12 @@ export class UtilityService {
 		return silencedHosts.some(x => `.${host.toLowerCase()}`.endsWith(`.${x}`));
 	}
 
+	@bindThis
+	public isMediaSilencedHost(silencedHosts: string[] | undefined, host: string | null): boolean {
+		if (!silencedHosts || host == null) return false;
+		return silencedHosts.some(x => host.toLowerCase() === x);
+	}
+
 	@bindThis
 	public concatNoteContentsForKeyWordCheck(content: {
 		cw?: string | null;
@@ -99,4 +109,19 @@ export class UtilityService {
 		if (host == null) return null;
 		return toASCII(host.toLowerCase());
 	}
+
+	@bindThis
+	public isFederationAllowedHost(host: string): boolean {
+		if (this.meta.federation === 'none') return false;
+		if (this.meta.federation === 'specified' && !this.meta.federationHosts.some(x => `.${host.toLowerCase()}`.endsWith(`.${x}`))) return false;
+		if (this.isBlockedHost(this.meta.blockedHosts, host)) return false;
+
+		return true;
+	}
+
+	@bindThis
+	public isFederationAllowedUri(uri: string): boolean {
+		const host = this.extractDbHost(uri);
+		return this.isFederationAllowedHost(host);
+	}
 }
diff --git a/packages/backend/src/core/WebAuthnService.ts b/packages/backend/src/core/WebAuthnService.ts
index ec9f4484a4..75ab0a207c 100644
--- a/packages/backend/src/core/WebAuthnService.ts
+++ b/packages/backend/src/core/WebAuthnService.ts
@@ -12,10 +12,9 @@ import {
 } from '@simplewebauthn/server';
 import { AttestationFormat, isoCBOR, isoUint8Array } from '@simplewebauthn/server/helpers';
 import { DI } from '@/di-symbols.js';
-import type { UserSecurityKeysRepository } from '@/models/_.js';
+import type { MiMeta, UserSecurityKeysRepository } from '@/models/_.js';
 import type { Config } from '@/config.js';
 import { bindThis } from '@/decorators.js';
-import { MetaService } from '@/core/MetaService.js';
 import { MiUser } from '@/models/_.js';
 import { IdentifiableError } from '@/misc/identifiable-error.js';
 import type {
@@ -23,7 +22,6 @@ import type {
 	AuthenticatorTransportFuture,
 	CredentialDeviceType,
 	PublicKeyCredentialCreationOptionsJSON,
-	PublicKeyCredentialDescriptorFuture,
 	PublicKeyCredentialRequestOptionsJSON,
 	RegistrationResponseJSON,
 } from '@simplewebauthn/types';
@@ -31,33 +29,33 @@ import type {
 @Injectable()
 export class WebAuthnService {
 	constructor(
-		@Inject(DI.redis)
-		private redisClient: Redis.Redis,
-
 		@Inject(DI.config)
 		private config: Config,
 
+		@Inject(DI.meta)
+		private meta: MiMeta,
+
+		@Inject(DI.redis)
+		private redisClient: Redis.Redis,
+
 		@Inject(DI.userSecurityKeysRepository)
 		private userSecurityKeysRepository: UserSecurityKeysRepository,
-
-		private metaService: MetaService,
 	) {
 	}
 
 	@bindThis
-	public async getRelyingParty(): Promise<{ origin: string; rpId: string; rpName: string; rpIcon?: string; }> {
-		const instance = await this.metaService.fetch();
+	public getRelyingParty(): { origin: string; rpId: string; rpName: string; rpIcon?: string; } {
 		return {
 			origin: this.config.url,
 			rpId: this.config.hostname,
-			rpName: instance.name ?? this.config.host,
-			rpIcon: instance.iconUrl ?? undefined,
+			rpName: this.meta.name ?? this.config.host,
+			rpIcon: this.meta.iconUrl ?? undefined,
 		};
 	}
 
 	@bindThis
 	public async initiateRegistration(userId: MiUser['id'], userName: string, userDisplayName?: string): Promise<PublicKeyCredentialCreationOptionsJSON> {
-		const relyingParty = await this.getRelyingParty();
+		const relyingParty = this.getRelyingParty();
 		const keys = await this.userSecurityKeysRepository.findBy({
 			userId: userId,
 		});
@@ -104,7 +102,7 @@ export class WebAuthnService {
 
 		await this.redisClient.del(`webauthn:challenge:${userId}`);
 
-		const relyingParty = await this.getRelyingParty();
+		const relyingParty = this.getRelyingParty();
 
 		let verification;
 		try {
@@ -143,7 +141,7 @@ export class WebAuthnService {
 
 	@bindThis
 	public async initiateAuthentication(userId: MiUser['id']): Promise<PublicKeyCredentialRequestOptionsJSON> {
-		const relyingParty = await this.getRelyingParty();
+		const relyingParty = this.getRelyingParty();
 		const keys = await this.userSecurityKeysRepository.findBy({
 			userId: userId,
 		});
@@ -166,6 +164,86 @@ export class WebAuthnService {
 		return authenticationOptions;
 	}
 
+	/**
+	 * Initiate Passkey Auth (Without specifying user)
+	 * @returns authenticationOptions
+	 */
+	@bindThis
+	public async initiateSignInWithPasskeyAuthentication(context: string): Promise<PublicKeyCredentialRequestOptionsJSON> {
+		const relyingParty = await this.getRelyingParty();
+
+		const authenticationOptions = await generateAuthenticationOptions({
+			rpID: relyingParty.rpId,
+			userVerification: 'preferred',
+		});
+
+		await this.redisClient.setex(`webauthn:challenge:${context}`, 90, authenticationOptions.challenge);
+
+		return authenticationOptions;
+	}
+
+	/**
+	 * Verify Webauthn AuthenticationCredential
+	 * @throws IdentifiableError
+	 * @returns If the challenge is successful, return the user ID. Otherwise, return null.
+	 */
+	@bindThis
+	public async verifySignInWithPasskeyAuthentication(context: string, response: AuthenticationResponseJSON): Promise<MiUser['id'] | null> {
+		const challenge = await this.redisClient.get(`webauthn:challenge:${context}`);
+
+		if (!challenge) {
+			throw new IdentifiableError('2d16e51c-007b-4edd-afd2-f7dd02c947f6', `challenge '${context}' not found`);
+		}
+
+		await this.redisClient.del(`webauthn:challenge:${context}`);
+
+		const key = await this.userSecurityKeysRepository.findOneBy({
+			id: response.id,
+		});
+
+		if (!key) {
+			throw new IdentifiableError('36b96a7d-b547-412d-aeed-2d611cdc8cdc', 'Unknown Webauthn key');
+		}
+
+		const relyingParty = await this.getRelyingParty();
+
+		let verification;
+		try {
+			verification = await verifyAuthenticationResponse({
+				response: response,
+				expectedChallenge: challenge,
+				expectedOrigin: relyingParty.origin,
+				expectedRPID: relyingParty.rpId,
+				authenticator: {
+					credentialID: key.id,
+					credentialPublicKey: Buffer.from(key.publicKey, 'base64url'),
+					counter: key.counter,
+					transports: key.transports ? key.transports as AuthenticatorTransportFuture[] : undefined,
+				},
+				requireUserVerification: true,
+			});
+		} catch (error) {
+			throw new IdentifiableError('b18c89a7-5b5e-4cec-bb5b-0419f332d430', `verification failed: ${error}`);
+		}
+
+		const { verified, authenticationInfo } = verification;
+
+		if (!verified) {
+			return null;
+		}
+
+		await this.userSecurityKeysRepository.update({
+			id: response.id,
+		}, {
+			lastUsed: new Date(),
+			counter: authenticationInfo.newCounter,
+			credentialDeviceType: authenticationInfo.credentialDeviceType,
+			credentialBackedUp: authenticationInfo.credentialBackedUp,
+		});
+
+		return key.userId;
+	}
+
 	@bindThis
 	public async verifyAuthentication(userId: MiUser['id'], response: AuthenticationResponseJSON): Promise<boolean> {
 		const challenge = await this.redisClient.get(`webauthn:challenge:${userId}`);
@@ -209,7 +287,7 @@ export class WebAuthnService {
 			}
 		}
 
-		const relyingParty = await this.getRelyingParty();
+		const relyingParty = this.getRelyingParty();
 
 		let verification;
 		try {
diff --git a/packages/backend/src/core/WebhookService.ts b/packages/backend/src/core/WebhookService.ts
deleted file mode 100644
index 6be34977b0..0000000000
--- a/packages/backend/src/core/WebhookService.ts
+++ /dev/null
@@ -1,97 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and misskey-project
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-import { Inject, Injectable } from '@nestjs/common';
-import * as Redis from 'ioredis';
-import type { WebhooksRepository } from '@/models/_.js';
-import type { MiWebhook } from '@/models/Webhook.js';
-import { DI } from '@/di-symbols.js';
-import { bindThis } from '@/decorators.js';
-import type { GlobalEvents } from '@/core/GlobalEventService.js';
-import type { OnApplicationShutdown } from '@nestjs/common';
-
-@Injectable()
-export class WebhookService implements OnApplicationShutdown {
-	private webhooksFetched = false;
-	private webhooks: MiWebhook[] = [];
-
-	constructor(
-		@Inject(DI.redisForSub)
-		private redisForSub: Redis.Redis,
-
-		@Inject(DI.webhooksRepository)
-		private webhooksRepository: WebhooksRepository,
-	) {
-		//this.onMessage = this.onMessage.bind(this);
-		this.redisForSub.on('message', this.onMessage);
-	}
-
-	@bindThis
-	public async getActiveWebhooks() {
-		if (!this.webhooksFetched) {
-			this.webhooks = await this.webhooksRepository.findBy({
-				active: true,
-			});
-			this.webhooksFetched = true;
-		}
-
-		return this.webhooks;
-	}
-
-	@bindThis
-	private async onMessage(_: string, data: string): Promise<void> {
-		const obj = JSON.parse(data);
-
-		if (obj.channel === 'internal') {
-			const { type, body } = obj.message as GlobalEvents['internal']['payload'];
-			switch (type) {
-				case 'webhookCreated':
-					if (body.active) {
-						this.webhooks.push({ // TODO: このあたりのデシリアライズ処理は各modelファイル内に関数としてexportしたい
-							...body,
-							latestSentAt: body.latestSentAt ? new Date(body.latestSentAt) : null,
-							user: null, // joinなカラムは通常取ってこないので
-						});
-					}
-					break;
-				case 'webhookUpdated':
-					if (body.active) {
-						const i = this.webhooks.findIndex(a => a.id === body.id);
-						if (i > -1) {
-							this.webhooks[i] = { // TODO: このあたりのデシリアライズ処理は各modelファイル内に関数としてexportしたい
-								...body,
-								latestSentAt: body.latestSentAt ? new Date(body.latestSentAt) : null,
-								user: null, // joinなカラムは通常取ってこないので
-							};
-						} else {
-							this.webhooks.push({ // TODO: このあたりのデシリアライズ処理は各modelファイル内に関数としてexportしたい
-								...body,
-								latestSentAt: body.latestSentAt ? new Date(body.latestSentAt) : null,
-								user: null, // joinなカラムは通常取ってこないので
-							});
-						}
-					} else {
-						this.webhooks = this.webhooks.filter(a => a.id !== body.id);
-					}
-					break;
-				case 'webhookDeleted':
-					this.webhooks = this.webhooks.filter(a => a.id !== body.id);
-					break;
-				default:
-					break;
-			}
-		}
-	}
-
-	@bindThis
-	public dispose(): void {
-		this.redisForSub.off('message', this.onMessage);
-	}
-
-	@bindThis
-	public onApplicationShutdown(signal?: string | undefined): void {
-		this.dispose();
-	}
-}
diff --git a/packages/backend/src/core/WebhookTestService.ts b/packages/backend/src/core/WebhookTestService.ts
new file mode 100644
index 0000000000..c826a28963
--- /dev/null
+++ b/packages/backend/src/core/WebhookTestService.ts
@@ -0,0 +1,471 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Injectable } from '@nestjs/common';
+import { MiAbuseUserReport, MiNote, MiUser, MiWebhook } from '@/models/_.js';
+import { bindThis } from '@/decorators.js';
+import { MiSystemWebhook, type SystemWebhookEventType } from '@/models/SystemWebhook.js';
+import { SystemWebhookService } from '@/core/SystemWebhookService.js';
+import { Packed } from '@/misc/json-schema.js';
+import { type WebhookEventTypes } from '@/models/Webhook.js';
+import { UserWebhookService } from '@/core/UserWebhookService.js';
+import { QueueService } from '@/core/QueueService.js';
+import { ModeratorInactivityRemainingTime } from '@/queue/processors/CheckModeratorsActivityProcessorService.js';
+
+const oneDayMillis = 24 * 60 * 60 * 1000;
+
+type AbuseUserReportDto = Omit<MiAbuseUserReport, 'targetUser' | 'reporter' | 'assignee'> & {
+	targetUser: Packed<'UserLite'> | null,
+	reporter: Packed<'UserLite'> | null,
+	assignee: Packed<'UserLite'> | null,
+};
+
+function generateAbuseReport(override?: Partial<MiAbuseUserReport>): AbuseUserReportDto {
+	const result: MiAbuseUserReport = {
+		id: 'dummy-abuse-report1',
+		targetUserId: 'dummy-target-user',
+		targetUser: null,
+		reporterId: 'dummy-reporter-user',
+		reporter: null,
+		assigneeId: null,
+		assignee: null,
+		resolved: false,
+		forwarded: false,
+		comment: 'This is a dummy report for testing purposes.',
+		targetUserHost: null,
+		reporterHost: null,
+		resolvedAs: null,
+		moderationNote: 'foo',
+		...override,
+	};
+
+	return {
+		...result,
+		targetUser: result.targetUser ? toPackedUserLite(result.targetUser) : null,
+		reporter: result.reporter ? toPackedUserLite(result.reporter) : null,
+		assignee: result.assignee ? toPackedUserLite(result.assignee) : null,
+	};
+}
+
+function generateDummyUser(override?: Partial<MiUser>): MiUser {
+	return {
+		id: 'dummy-user-1',
+		updatedAt: new Date(Date.now() - oneDayMillis * 7),
+		lastFetchedAt: new Date(Date.now() - oneDayMillis * 5),
+		lastActiveDate: new Date(Date.now() - oneDayMillis * 3),
+		hideOnlineStatus: false,
+		username: 'dummy1',
+		usernameLower: 'dummy1',
+		name: 'DummyUser1',
+		followersCount: 10,
+		followingCount: 5,
+		movedToUri: null,
+		movedAt: null,
+		alsoKnownAs: null,
+		notesCount: 30,
+		avatarId: null,
+		avatar: null,
+		bannerId: null,
+		banner: null,
+		avatarUrl: null,
+		bannerUrl: null,
+		avatarBlurhash: null,
+		bannerBlurhash: null,
+		avatarDecorations: [],
+		tags: [],
+		isSuspended: false,
+		isLocked: false,
+		isBot: false,
+		isCat: true,
+		isRoot: false,
+		isExplorable: true,
+		isHibernated: false,
+		isDeleted: false,
+		requireSigninToViewContents: false,
+		makeNotesFollowersOnlyBefore: null,
+		makeNotesHiddenBefore: null,
+		emojis: [],
+		score: 0,
+		host: null,
+		inbox: null,
+		sharedInbox: null,
+		featured: null,
+		uri: null,
+		followersUri: null,
+		token: null,
+		...override,
+	};
+}
+
+function generateDummyNote(override?: Partial<MiNote>): MiNote {
+	return {
+		id: 'dummy-note-1',
+		replyId: null,
+		reply: null,
+		renoteId: null,
+		renote: null,
+		threadId: null,
+		text: 'This is a dummy note for testing purposes.',
+		name: null,
+		cw: null,
+		userId: 'dummy-user-1',
+		user: null,
+		localOnly: true,
+		reactionAcceptance: 'likeOnly',
+		renoteCount: 10,
+		repliesCount: 5,
+		clippedCount: 0,
+		reactions: {},
+		visibility: 'public',
+		uri: null,
+		url: null,
+		fileIds: [],
+		attachedFileTypes: [],
+		visibleUserIds: [],
+		mentions: [],
+		mentionedRemoteUsers: '[]',
+		reactionAndUserPairCache: [],
+		emojis: [],
+		tags: [],
+		hasPoll: false,
+		channelId: null,
+		channel: null,
+		userHost: null,
+		replyUserId: null,
+		replyUserHost: null,
+		renoteUserId: null,
+		renoteUserHost: null,
+		...override,
+	};
+}
+
+function toPackedNote(note: MiNote, detail = true, override?: Packed<'Note'>): Packed<'Note'> {
+	return {
+		id: note.id,
+		createdAt: new Date().toISOString(),
+		deletedAt: null,
+		text: note.text,
+		cw: note.cw,
+		userId: note.userId,
+		user: toPackedUserLite(note.user ?? generateDummyUser()),
+		replyId: note.replyId,
+		renoteId: note.renoteId,
+		isHidden: false,
+		visibility: note.visibility,
+		mentions: note.mentions,
+		visibleUserIds: note.visibleUserIds,
+		fileIds: note.fileIds,
+		files: [],
+		tags: note.tags,
+		poll: null,
+		emojis: note.emojis,
+		channelId: note.channelId,
+		channel: note.channel,
+		localOnly: note.localOnly,
+		reactionAcceptance: note.reactionAcceptance,
+		reactionEmojis: {},
+		reactions: {},
+		reactionCount: 0,
+		renoteCount: note.renoteCount,
+		repliesCount: note.repliesCount,
+		uri: note.uri ?? undefined,
+		url: note.url ?? undefined,
+		reactionAndUserPairCache: note.reactionAndUserPairCache,
+		...(detail ? {
+			clippedCount: note.clippedCount,
+			reply: note.reply ? toPackedNote(note.reply, false) : null,
+			renote: note.renote ? toPackedNote(note.renote, true) : null,
+			myReaction: null,
+		} : {}),
+		...override,
+	};
+}
+
+function toPackedUserLite(user: MiUser, override?: Packed<'UserLite'>): Packed<'UserLite'> {
+	return {
+		id: user.id,
+		name: user.name,
+		username: user.username,
+		host: user.host,
+		avatarUrl: user.avatarUrl,
+		avatarBlurhash: user.avatarBlurhash,
+		avatarDecorations: user.avatarDecorations.map(it => ({
+			id: it.id,
+			angle: it.angle,
+			flipH: it.flipH,
+			url: 'https://example.com/dummy-image001.png',
+			offsetX: it.offsetX,
+			offsetY: it.offsetY,
+		})),
+		isBot: user.isBot,
+		isCat: user.isCat,
+		emojis: user.emojis,
+		onlineStatus: 'active',
+		badgeRoles: [],
+		...override,
+	};
+}
+
+function toPackedUserDetailedNotMe(user: MiUser, override?: Packed<'UserDetailedNotMe'>): Packed<'UserDetailedNotMe'> {
+	return {
+		...toPackedUserLite(user),
+		url: null,
+		uri: null,
+		movedTo: null,
+		alsoKnownAs: [],
+		createdAt: new Date().toISOString(),
+		updatedAt: user.updatedAt?.toISOString() ?? null,
+		lastFetchedAt: user.lastFetchedAt?.toISOString() ?? null,
+		bannerUrl: user.bannerUrl,
+		bannerBlurhash: user.bannerBlurhash,
+		isLocked: user.isLocked,
+		isSilenced: false,
+		isSuspended: user.isSuspended,
+		description: null,
+		location: null,
+		birthday: null,
+		lang: null,
+		fields: [],
+		verifiedLinks: [],
+		followersCount: user.followersCount,
+		followingCount: user.followingCount,
+		notesCount: user.notesCount,
+		pinnedNoteIds: [],
+		pinnedNotes: [],
+		pinnedPageId: null,
+		pinnedPage: null,
+		publicReactions: true,
+		followersVisibility: 'public',
+		followingVisibility: 'public',
+		twoFactorEnabled: false,
+		usePasswordLessLogin: false,
+		securityKeys: false,
+		roles: [],
+		memo: null,
+		moderationNote: undefined,
+		isFollowing: false,
+		isFollowed: false,
+		hasPendingFollowRequestFromYou: false,
+		hasPendingFollowRequestToYou: false,
+		isBlocking: false,
+		isBlocked: false,
+		isMuted: false,
+		isRenoteMuted: false,
+		notify: 'none',
+		withReplies: true,
+		...override,
+	};
+}
+
+const dummyUser1 = generateDummyUser();
+const dummyUser2 = generateDummyUser({
+	id: 'dummy-user-2',
+	updatedAt: new Date(Date.now() - oneDayMillis * 30),
+	lastFetchedAt: new Date(Date.now() - oneDayMillis),
+	lastActiveDate: new Date(Date.now() - oneDayMillis),
+	username: 'dummy2',
+	usernameLower: 'dummy2',
+	name: 'DummyUser2',
+	followersCount: 40,
+	followingCount: 50,
+	notesCount: 900,
+});
+const dummyUser3 = generateDummyUser({
+	id: 'dummy-user-3',
+	updatedAt: new Date(Date.now() - oneDayMillis * 15),
+	lastFetchedAt: new Date(Date.now() - oneDayMillis * 2),
+	lastActiveDate: new Date(Date.now() - oneDayMillis * 2),
+	username: 'dummy3',
+	usernameLower: 'dummy3',
+	name: 'DummyUser3',
+	followersCount: 60,
+	followingCount: 70,
+	notesCount: 15900,
+});
+
+@Injectable()
+export class WebhookTestService {
+	public static NoSuchWebhookError = class extends Error {
+	};
+
+	constructor(
+		private userWebhookService: UserWebhookService,
+		private systemWebhookService: SystemWebhookService,
+		private queueService: QueueService,
+	) {
+	}
+
+	/**
+	 * UserWebhookのテスト送信を行う.
+	 * 送信されるペイロードはいずれもダミーの値で、実際にはデータベース上に存在しない.
+	 *
+	 * また、この関数経由で送信されるWebhookは以下の設定を無視する.
+	 * - Webhookそのものの有効・無効設定(active)
+	 * - 送信対象イベント(on)に関する設定
+	 */
+	@bindThis
+	public async testUserWebhook(
+		params: {
+			webhookId: MiWebhook['id'],
+			type: WebhookEventTypes,
+			override?: Partial<Omit<MiWebhook, 'id'>>,
+		},
+		sender: MiUser | null,
+	) {
+		const webhooks = await this.userWebhookService.fetchWebhooks({ ids: [params.webhookId] })
+			.then(it => it.filter(it => it.userId === sender?.id));
+		if (webhooks.length === 0) {
+			throw new WebhookTestService.NoSuchWebhookError();
+		}
+
+		const webhook = webhooks[0];
+		const send = (contents: unknown) => {
+			const merged = {
+				...webhook,
+				...params.override,
+			};
+
+			// テスト目的なのでUserWebhookServiceの機能を経由せず直接キューに追加する(チェック処理などをスキップする意図).
+			// また、Jobの試行回数も1回だけ.
+			this.queueService.userWebhookDeliver(merged, params.type, contents, { attempts: 1 });
+		};
+
+		const dummyNote1 = generateDummyNote({
+			userId: dummyUser1.id,
+			user: dummyUser1,
+		});
+		const dummyReply1 = generateDummyNote({
+			id: 'dummy-reply-1',
+			replyId: dummyNote1.id,
+			reply: dummyNote1,
+			userId: dummyUser1.id,
+			user: dummyUser1,
+		});
+		const dummyRenote1 = generateDummyNote({
+			id: 'dummy-renote-1',
+			renoteId: dummyNote1.id,
+			renote: dummyNote1,
+			userId: dummyUser2.id,
+			user: dummyUser2,
+			text: null,
+		});
+		const dummyMention1 = generateDummyNote({
+			id: 'dummy-mention-1',
+			userId: dummyUser1.id,
+			user: dummyUser1,
+			text: `@${dummyUser2.username} This is a mention to you.`,
+			mentions: [dummyUser2.id],
+		});
+
+		switch (params.type) {
+			case 'note': {
+				send(toPackedNote(dummyNote1));
+				break;
+			}
+			case 'reply': {
+				send(toPackedNote(dummyReply1));
+				break;
+			}
+			case 'renote': {
+				send(toPackedNote(dummyRenote1));
+				break;
+			}
+			case 'mention': {
+				send(toPackedNote(dummyMention1));
+				break;
+			}
+			case 'follow': {
+				send(toPackedUserDetailedNotMe(dummyUser1));
+				break;
+			}
+			case 'followed': {
+				send(toPackedUserLite(dummyUser2));
+				break;
+			}
+			case 'unfollow': {
+				send(toPackedUserDetailedNotMe(dummyUser3));
+				break;
+			}
+		}
+	}
+
+	/**
+	 * SystemWebhookのテスト送信を行う.
+	 * 送信されるペイロードはいずれもダミーの値で、実際にはデータベース上に存在しない.
+	 *
+	 * また、この関数経由で送信されるWebhookは以下の設定を無視する.
+	 * - Webhookそのものの有効・無効設定(isActive)
+	 * - 送信対象イベント(on)に関する設定
+	 */
+	@bindThis
+	public async testSystemWebhook(
+		params: {
+			webhookId: MiSystemWebhook['id'],
+			type: SystemWebhookEventType,
+			override?: Partial<Omit<MiSystemWebhook, 'id'>>,
+		},
+	) {
+		const webhooks = await this.systemWebhookService.fetchSystemWebhooks({ ids: [params.webhookId] });
+		if (webhooks.length === 0) {
+			throw new WebhookTestService.NoSuchWebhookError();
+		}
+
+		const webhook = webhooks[0];
+		const send = (contents: unknown) => {
+			const merged = {
+				...webhook,
+				...params.override,
+			};
+
+			// テスト目的なのでSystemWebhookServiceの機能を経由せず直接キューに追加する(チェック処理などをスキップする意図).
+			// また、Jobの試行回数も1回だけ.
+			this.queueService.systemWebhookDeliver(merged, params.type, contents, { attempts: 1 });
+		};
+
+		switch (params.type) {
+			case 'abuseReport': {
+				send(generateAbuseReport({
+					targetUserId: dummyUser1.id,
+					targetUser: dummyUser1,
+					reporterId: dummyUser2.id,
+					reporter: dummyUser2,
+				}));
+				break;
+			}
+			case 'abuseReportResolved': {
+				send(generateAbuseReport({
+					targetUserId: dummyUser1.id,
+					targetUser: dummyUser1,
+					reporterId: dummyUser2.id,
+					reporter: dummyUser2,
+					assigneeId: dummyUser3.id,
+					assignee: dummyUser3,
+					resolved: true,
+				}));
+				break;
+			}
+			case 'userCreated': {
+				send(toPackedUserLite(dummyUser1));
+				break;
+			}
+			case 'inactiveModeratorsWarning': {
+				const dummyTime: ModeratorInactivityRemainingTime = {
+					time: 100000,
+					asDays: 1,
+					asHours: 24,
+				};
+
+				send({
+					remainingTime: dummyTime,
+				});
+				break;
+			}
+			case 'inactiveModeratorsInvitationOnlyChanged': {
+				send({});
+				break;
+			}
+		}
+	}
+}
diff --git a/packages/backend/src/core/activitypub/ApAudienceService.ts b/packages/backend/src/core/activitypub/ApAudienceService.ts
index 0fccc7b950..5a5a76f7d6 100644
--- a/packages/backend/src/core/activitypub/ApAudienceService.ts
+++ b/packages/backend/src/core/activitypub/ApAudienceService.ts
@@ -8,7 +8,6 @@ import promiseLimit from 'promise-limit';
 import type { MiRemoteUser, MiUser } from '@/models/User.js';
 import { concat, unique } from '@/misc/prelude/array.js';
 import { bindThis } from '@/decorators.js';
-import { isNotNull } from '@/misc/is-not-null.js';
 import { getApIds } from './type.js';
 import { ApPersonService } from './models/ApPersonService.js';
 import type { ApObject } from './type.js';
@@ -41,7 +40,7 @@ export class ApAudienceService {
 		const limit = promiseLimit<MiUser | null>(2);
 		const mentionedUsers = (await Promise.all(
 			others.map(id => limit(() => this.apPersonService.resolvePerson(id, resolver).catch(() => null))),
-		)).filter(isNotNull);
+		)).filter(x => x != null);
 
 		if (toGroups.public.length > 0) {
 			return {
diff --git a/packages/backend/src/core/activitypub/ApDbResolverService.ts b/packages/backend/src/core/activitypub/ApDbResolverService.ts
index f6b70ead44..4192e8659a 100644
--- a/packages/backend/src/core/activitypub/ApDbResolverService.ts
+++ b/packages/backend/src/core/activitypub/ApDbResolverService.ts
@@ -54,8 +54,8 @@ export class ApDbResolverService implements OnApplicationShutdown {
 		private cacheService: CacheService,
 		private apPersonService: ApPersonService,
 	) {
-		this.publicKeyCache = new MemoryKVCache<MiUserPublickey | null>(Infinity);
-		this.publicKeyByUserIdCache = new MemoryKVCache<MiUserPublickey | null>(Infinity);
+		this.publicKeyCache = new MemoryKVCache<MiUserPublickey | null>(1000 * 60 * 60 * 12); // 12h
+		this.publicKeyByUserIdCache = new MemoryKVCache<MiUserPublickey | null>(1000 * 60 * 60 * 12); // 12h
 	}
 
 	@bindThis
diff --git a/packages/backend/src/core/activitypub/ApInboxService.ts b/packages/backend/src/core/activitypub/ApInboxService.ts
index d0d206760c..376c9c0151 100644
--- a/packages/backend/src/core/activitypub/ApInboxService.ts
+++ b/packages/backend/src/core/activitypub/ApInboxService.ts
@@ -17,18 +17,17 @@ import { NoteCreateService } from '@/core/NoteCreateService.js';
 import { concat, toArray, toSingle, unique } from '@/misc/prelude/array.js';
 import { AppLockService } from '@/core/AppLockService.js';
 import type Logger from '@/logger.js';
-import { MetaService } from '@/core/MetaService.js';
 import { IdService } from '@/core/IdService.js';
 import { StatusError } from '@/misc/status-error.js';
 import { UtilityService } from '@/core/UtilityService.js';
 import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
 import { UserEntityService } from '@/core/entities/UserEntityService.js';
 import { QueueService } from '@/core/QueueService.js';
-import type { UsersRepository, NotesRepository, FollowingsRepository, AbuseUserReportsRepository, FollowRequestsRepository } from '@/models/_.js';
+import type { UsersRepository, NotesRepository, FollowingsRepository, AbuseUserReportsRepository, FollowRequestsRepository, MiMeta } from '@/models/_.js';
 import { bindThis } from '@/decorators.js';
 import type { MiRemoteUser } from '@/models/User.js';
-import { isNotNull } from '@/misc/is-not-null.js';
 import { GlobalEventService } from '@/core/GlobalEventService.js';
+import { AbuseReportService } from '@/core/AbuseReportService.js';
 import { getApHrefNullable, getApId, getApIds, getApType, isAccept, isActor, isAdd, isAnnounce, isBlock, isCollection, isCollectionOrOrderedCollection, isCreate, isDelete, isFlag, isFollow, isLike, isMove, isPost, isReject, isRemove, isTombstone, isUndo, isUpdate, validActor, validPost } from './type.js';
 import { ApNoteService } from './models/ApNoteService.js';
 import { ApLoggerService } from './ApLoggerService.js';
@@ -48,6 +47,9 @@ export class ApInboxService {
 		@Inject(DI.config)
 		private config: Config,
 
+		@Inject(DI.meta)
+		private meta: MiMeta,
+
 		@Inject(DI.usersRepository)
 		private usersRepository: UsersRepository,
 
@@ -57,9 +59,6 @@ export class ApInboxService {
 		@Inject(DI.followingsRepository)
 		private followingsRepository: FollowingsRepository,
 
-		@Inject(DI.abuseUserReportsRepository)
-		private abuseUserReportsRepository: AbuseUserReportsRepository,
-
 		@Inject(DI.followRequestsRepository)
 		private followRequestsRepository: FollowRequestsRepository,
 
@@ -67,7 +66,7 @@ export class ApInboxService {
 		private noteEntityService: NoteEntityService,
 		private utilityService: UtilityService,
 		private idService: IdService,
-		private metaService: MetaService,
+		private abuseReportService: AbuseReportService,
 		private userFollowingService: UserFollowingService,
 		private apAudienceService: ApAudienceService,
 		private reactionService: ReactionService,
@@ -291,9 +290,8 @@ export class ApInboxService {
 			return;
 		}
 
-		// アナウンス先をブロックしてたら中断
-		const meta = await this.metaService.fetch();
-		if (this.utilityService.isBlockedHost(meta.blockedHosts, this.utilityService.extractDbHost(uri))) return;
+		// アナウンス先が許可されているかチェック
+		if (!this.utilityService.isFederationAllowedUri(uri)) return;
 
 		const unlock = await this.appLockService.getApLock(uri);
 
@@ -539,20 +537,19 @@ export class ApInboxService {
 		const userIds = uris
 			.filter(uri => uri.startsWith(this.config.url + '/users/'))
 			.map(uri => uri.split('/').at(-1))
-			.filter(isNotNull);
+			.filter(x => x != null);
 		const users = await this.usersRepository.findBy({
 			id: In(userIds),
 		});
 		if (users.length < 1) return 'skip';
 
-		await this.abuseUserReportsRepository.insert({
-			id: this.idService.gen(),
+		await this.abuseReportService.report([{
 			targetUserId: users[0].id,
 			targetUserHost: users[0].host,
 			reporterId: actor.id,
 			reporterHost: actor.host,
 			comment: `${activity.content}\n${JSON.stringify(uris, null, 2)}`,
-		});
+		}]);
 
 		return 'ok';
 	}
diff --git a/packages/backend/src/core/activitypub/ApMfmService.ts b/packages/backend/src/core/activitypub/ApMfmService.ts
index ab75b9abbd..4036d2794a 100644
--- a/packages/backend/src/core/activitypub/ApMfmService.ts
+++ b/packages/backend/src/core/activitypub/ApMfmService.ts
@@ -25,7 +25,7 @@ export class ApMfmService {
 	}
 
 	@bindThis
-	public getNoteHtml(note: MiNote, apAppend?: string) {
+	public getNoteHtml(note: Pick<MiNote, 'text' | 'mentionedRemoteUsers'>, apAppend?: string) {
 		let noMisskeyContent = false;
 		const srcMfm = (note.text ?? '') + (apAppend ?? '');
 
diff --git a/packages/backend/src/core/activitypub/ApRendererService.ts b/packages/backend/src/core/activitypub/ApRendererService.ts
index 4fc724b548..5617a29bab 100644
--- a/packages/backend/src/core/activitypub/ApRendererService.ts
+++ b/packages/backend/src/core/activitypub/ApRendererService.ts
@@ -26,7 +26,6 @@ import type { MiUserKeypair } from '@/models/UserKeypair.js';
 import type { UsersRepository, UserProfilesRepository, NotesRepository, DriveFilesRepository, PollsRepository } from '@/models/_.js';
 import { bindThis } from '@/decorators.js';
 import { CustomEmojiService } from '@/core/CustomEmojiService.js';
-import { isNotNull } from '@/misc/is-not-null.js';
 import { IdService } from '@/core/IdService.js';
 import { JsonLdService } from './JsonLdService.js';
 import { ApMfmService } from './ApMfmService.js';
@@ -317,7 +316,7 @@ export class ApRendererService {
 		const getPromisedFiles = async (ids: string[]): Promise<MiDriveFile[]> => {
 			if (ids.length === 0) return [];
 			const items = await this.driveFilesRepository.findBy({ id: In(ids) });
-			return ids.map(id => items.find(item => item.id === id)).filter(isNotNull);
+			return ids.map(id => items.find(item => item.id === id)).filter(x => x != null);
 		};
 
 		let inReplyTo;
@@ -495,6 +494,10 @@ export class ApRendererService {
 			name: user.name,
 			summary: profile.description ? this.mfmService.toHtml(mfm.parse(profile.description)) : null,
 			_misskey_summary: profile.description,
+			_misskey_followedMessage: profile.followedMessage,
+			_misskey_requireSigninToViewContents: user.requireSigninToViewContents,
+			_misskey_makeNotesFollowersOnlyBefore: user.makeNotesFollowersOnlyBefore,
+			_misskey_makeNotesHiddenBefore: user.makeNotesHiddenBefore,
 			icon: avatar ? this.renderImage(avatar) : null,
 			image: banner ? this.renderImage(banner) : null,
 			tag,
@@ -686,7 +689,7 @@ export class ApRendererService {
 		if (names.length === 0) return [];
 
 		const allEmojis = await this.customEmojiService.localEmojisCache.fetch();
-		const emojis = names.map(name => allEmojis.get(name)).filter(isNotNull);
+		const emojis = names.map(name => allEmojis.get(name)).filter(x => x != null);
 
 		return emojis;
 	}
diff --git a/packages/backend/src/core/activitypub/ApRequestService.ts b/packages/backend/src/core/activitypub/ApRequestService.ts
index 93ac8ce9a7..c7d19adfd5 100644
--- a/packages/backend/src/core/activitypub/ApRequestService.ts
+++ b/packages/backend/src/core/activitypub/ApRequestService.ts
@@ -6,6 +6,7 @@
 import * as crypto from 'node:crypto';
 import { URL } from 'node:url';
 import { Inject, Injectable } from '@nestjs/common';
+import { Window } from 'happy-dom';
 import { DI } from '@/di-symbols.js';
 import type { Config } from '@/config.js';
 import type { MiUser } from '@/models/User.js';
@@ -180,7 +181,8 @@ export class ApRequestService {
 	 * @param url URL to fetch
 	 */
 	@bindThis
-	public async signedGet(url: string, user: { id: MiUser['id'] }): Promise<unknown> {
+	public async signedGet(url: string, user: { id: MiUser['id'] }, followAlternate?: boolean): Promise<unknown> {
+		const _followAlternate = followAlternate ?? true;
 		const keypair = await this.userKeypairService.getUserKeypair(user.id);
 
 		const req = ApRequestCreator.createSignedGet({
@@ -198,9 +200,58 @@ export class ApRequestService {
 			headers: req.request.headers,
 		}, {
 			throwErrorWhenResponseNotOk: true,
-			validators: [validateContentTypeSetAsActivityPub],
 		});
 
+		//#region リクエスト先がhtmlかつactivity+jsonへのalternate linkタグがあるとき
+		const contentType = res.headers.get('content-type');
+
+		if (
+			res.ok &&
+			(contentType ?? '').split(';')[0].trimEnd().toLowerCase() === 'text/html' &&
+			_followAlternate === true
+		) {
+			const html = await res.text();
+			const { window, happyDOM } = new Window({
+				settings: {
+					disableJavaScriptEvaluation: true,
+					disableJavaScriptFileLoading: true,
+					disableCSSFileLoading: true,
+					disableComputedStyleRendering: true,
+					handleDisabledFileLoadingAsSuccess: true,
+					navigation: {
+						disableMainFrameNavigation: true,
+						disableChildFrameNavigation: true,
+						disableChildPageNavigation: true,
+						disableFallbackToSetURL: true,
+					},
+					timer: {
+						maxTimeout: 0,
+						maxIntervalTime: 0,
+						maxIntervalIterations: 0,
+					},
+				},
+			});
+			const document = window.document;
+			try {
+				document.documentElement.innerHTML = html;
+
+				const alternate = document.querySelector('head > link[rel="alternate"][type="application/activity+json"]');
+				if (alternate) {
+					const href = alternate.getAttribute('href');
+					if (href) {
+						return await this.signedGet(href, user, false);
+					}
+				}
+			} catch (e) {
+				// something went wrong parsing the HTML, ignore the whole thing
+			} finally {
+				happyDOM.close().catch(err => {});
+			}
+		}
+		//#endregion
+
+		validateContentTypeSetAsActivityPub(res);
+
 		return await res.json();
 	}
 }
diff --git a/packages/backend/src/core/activitypub/ApResolverService.ts b/packages/backend/src/core/activitypub/ApResolverService.ts
index bb3c40f093..ca35608d9b 100644
--- a/packages/backend/src/core/activitypub/ApResolverService.ts
+++ b/packages/backend/src/core/activitypub/ApResolverService.ts
@@ -7,9 +7,8 @@ import { Inject, Injectable } from '@nestjs/common';
 import { IsNull, Not } from 'typeorm';
 import type { MiLocalUser, MiRemoteUser } from '@/models/User.js';
 import { InstanceActorService } from '@/core/InstanceActorService.js';
-import type { NotesRepository, PollsRepository, NoteReactionsRepository, UsersRepository, FollowRequestsRepository } from '@/models/_.js';
+import type { NotesRepository, PollsRepository, NoteReactionsRepository, UsersRepository, FollowRequestsRepository, MiMeta } from '@/models/_.js';
 import type { Config } from '@/config.js';
-import { MetaService } from '@/core/MetaService.js';
 import { HttpRequestService } from '@/core/HttpRequestService.js';
 import { DI } from '@/di-symbols.js';
 import { UtilityService } from '@/core/UtilityService.js';
@@ -29,6 +28,7 @@ export class Resolver {
 
 	constructor(
 		private config: Config,
+		private meta: MiMeta,
 		private usersRepository: UsersRepository,
 		private notesRepository: NotesRepository,
 		private pollsRepository: PollsRepository,
@@ -36,7 +36,6 @@ export class Resolver {
 		private followRequestsRepository: FollowRequestsRepository,
 		private utilityService: UtilityService,
 		private instanceActorService: InstanceActorService,
-		private metaService: MetaService,
 		private apRequestService: ApRequestService,
 		private httpRequestService: HttpRequestService,
 		private apRendererService: ApRendererService,
@@ -94,8 +93,7 @@ export class Resolver {
 			return await this.resolveLocal(value);
 		}
 
-		const meta = await this.metaService.fetch();
-		if (this.utilityService.isBlockedHost(meta.blockedHosts, host)) {
+		if (!this.utilityService.isFederationAllowedHost(host)) {
 			throw new Error('Instance is blocked');
 		}
 
@@ -178,6 +176,9 @@ export class ApResolverService {
 		@Inject(DI.config)
 		private config: Config,
 
+		@Inject(DI.meta)
+		private meta: MiMeta,
+
 		@Inject(DI.usersRepository)
 		private usersRepository: UsersRepository,
 
@@ -195,7 +196,6 @@ export class ApResolverService {
 
 		private utilityService: UtilityService,
 		private instanceActorService: InstanceActorService,
-		private metaService: MetaService,
 		private apRequestService: ApRequestService,
 		private httpRequestService: HttpRequestService,
 		private apRendererService: ApRendererService,
@@ -208,6 +208,7 @@ export class ApResolverService {
 	public createResolver(): Resolver {
 		return new Resolver(
 			this.config,
+			this.meta,
 			this.usersRepository,
 			this.notesRepository,
 			this.pollsRepository,
@@ -215,7 +216,6 @@ export class ApResolverService {
 			this.followRequestsRepository,
 			this.utilityService,
 			this.instanceActorService,
-			this.metaService,
 			this.apRequestService,
 			this.httpRequestService,
 			this.apRendererService,
diff --git a/packages/backend/src/core/activitypub/misc/contexts.ts b/packages/backend/src/core/activitypub/misc/contexts.ts
index feb8c42c56..94cb0785cb 100644
--- a/packages/backend/src/core/activitypub/misc/contexts.ts
+++ b/packages/backend/src/core/activitypub/misc/contexts.ts
@@ -554,6 +554,10 @@ const extension_context_definition = {
 	'_misskey_reaction': 'misskey:_misskey_reaction',
 	'_misskey_votes': 'misskey:_misskey_votes',
 	'_misskey_summary': 'misskey:_misskey_summary',
+	'_misskey_followedMessage': 'misskey:_misskey_followedMessage',
+	'_misskey_requireSigninToViewContents': 'misskey:_misskey_requireSigninToViewContents',
+	'_misskey_makeNotesFollowersOnlyBefore': 'misskey:_misskey_makeNotesFollowersOnlyBefore',
+	'_misskey_makeNotesHiddenBefore': 'misskey:_misskey_makeNotesHiddenBefore',
 	'isCat': 'misskey:isCat',
 	// vcard
 	vcard: 'http://www.w3.org/2006/vcard/ns#',
diff --git a/packages/backend/src/core/activitypub/models/ApImageService.ts b/packages/backend/src/core/activitypub/models/ApImageService.ts
index 3691967270..e7ece87b01 100644
--- a/packages/backend/src/core/activitypub/models/ApImageService.ts
+++ b/packages/backend/src/core/activitypub/models/ApImageService.ts
@@ -5,10 +5,9 @@
 
 import { Inject, Injectable } from '@nestjs/common';
 import { DI } from '@/di-symbols.js';
-import type { DriveFilesRepository } from '@/models/_.js';
+import type { DriveFilesRepository, MiMeta } from '@/models/_.js';
 import type { MiRemoteUser } from '@/models/User.js';
 import type { MiDriveFile } from '@/models/DriveFile.js';
-import { MetaService } from '@/core/MetaService.js';
 import { truncate } from '@/misc/truncate.js';
 import { DB_MAX_IMAGE_COMMENT_LENGTH } from '@/const.js';
 import { DriveService } from '@/core/DriveService.js';
@@ -24,10 +23,12 @@ export class ApImageService {
 	private logger: Logger;
 
 	constructor(
+		@Inject(DI.meta)
+		private meta: MiMeta,
+
 		@Inject(DI.driveFilesRepository)
 		private driveFilesRepository: DriveFilesRepository,
 
-		private metaService: MetaService,
 		private apResolverService: ApResolverService,
 		private driveService: DriveService,
 		private apLoggerService: ApLoggerService,
@@ -63,12 +64,10 @@ export class ApImageService {
 
 		this.logger.info(`Creating the Image: ${image.url}`);
 
-		const instance = await this.metaService.fetch();
-
 		// Cache if remote file cache is on AND either
 		// 1. remote sensitive file is also on
 		// 2. or the image is not sensitive
-		const shouldBeCached = instance.cacheRemoteFiles && (instance.cacheRemoteSensitiveFiles || !image.sensitive);
+		const shouldBeCached = this.meta.cacheRemoteFiles && (this.meta.cacheRemoteSensitiveFiles || !image.sensitive);
 
 		const file = await this.driveService.uploadFromUrl({
 			url: image.url,
diff --git a/packages/backend/src/core/activitypub/models/ApMentionService.ts b/packages/backend/src/core/activitypub/models/ApMentionService.ts
index 0ced7e88af..2cd151fa04 100644
--- a/packages/backend/src/core/activitypub/models/ApMentionService.ts
+++ b/packages/backend/src/core/activitypub/models/ApMentionService.ts
@@ -8,7 +8,6 @@ import promiseLimit from 'promise-limit';
 import type { MiUser } from '@/models/_.js';
 import { toArray, unique } from '@/misc/prelude/array.js';
 import { bindThis } from '@/decorators.js';
-import { isNotNull } from '@/misc/is-not-null.js';
 import { isMention } from '../type.js';
 import { Resolver } from '../ApResolverService.js';
 import { ApPersonService } from './ApPersonService.js';
@@ -28,7 +27,7 @@ export class ApMentionService {
 		const limit = promiseLimit<MiUser | null>(2);
 		const mentionedUsers = (await Promise.all(
 			hrefs.map(x => limit(() => this.apPersonService.resolvePerson(x, resolver).catch(() => null))),
-		)).filter(isNotNull);
+		)).filter(x => x != null);
 
 		return mentionedUsers;
 	}
diff --git a/packages/backend/src/core/activitypub/models/ApNoteService.ts b/packages/backend/src/core/activitypub/models/ApNoteService.ts
index c6e6b3a1e8..2d333b3634 100644
--- a/packages/backend/src/core/activitypub/models/ApNoteService.ts
+++ b/packages/backend/src/core/activitypub/models/ApNoteService.ts
@@ -6,13 +6,12 @@
 import { forwardRef, Inject, Injectable } from '@nestjs/common';
 import { In } from 'typeorm';
 import { DI } from '@/di-symbols.js';
-import type { PollsRepository, EmojisRepository } from '@/models/_.js';
+import type { PollsRepository, EmojisRepository, MiMeta } from '@/models/_.js';
 import type { Config } from '@/config.js';
 import type { MiRemoteUser } from '@/models/User.js';
 import type { MiNote } from '@/models/Note.js';
 import { toArray, toSingle, unique } from '@/misc/prelude/array.js';
 import type { MiEmoji } from '@/models/Emoji.js';
-import { MetaService } from '@/core/MetaService.js';
 import { AppLockService } from '@/core/AppLockService.js';
 import type { MiDriveFile } from '@/models/DriveFile.js';
 import { NoteCreateService } from '@/core/NoteCreateService.js';
@@ -24,7 +23,6 @@ import { UtilityService } from '@/core/UtilityService.js';
 import { bindThis } from '@/decorators.js';
 import { checkHttps } from '@/misc/check-https.js';
 import { IdentifiableError } from '@/misc/identifiable-error.js';
-import { isNotNull } from '@/misc/is-not-null.js';
 import { getOneApId, getApId, getOneApHrefNullable, validPost, isEmoji, getApType } from '../type.js';
 import { ApLoggerService } from '../ApLoggerService.js';
 import { ApMfmService } from '../ApMfmService.js';
@@ -47,6 +45,9 @@ export class ApNoteService {
 		@Inject(DI.config)
 		private config: Config,
 
+		@Inject(DI.meta)
+		private meta: MiMeta,
+
 		@Inject(DI.pollsRepository)
 		private pollsRepository: PollsRepository,
 
@@ -66,7 +67,6 @@ export class ApNoteService {
 		private apMentionService: ApMentionService,
 		private apImageService: ApImageService,
 		private apQuestionService: ApQuestionService,
-		private metaService: MetaService,
 		private appLockService: AppLockService,
 		private pollService: PollService,
 		private noteCreateService: NoteCreateService,
@@ -79,9 +79,10 @@ export class ApNoteService {
 	@bindThis
 	public validateNote(object: IObject, uri: string): Error | null {
 		const expectHost = this.utilityService.extractDbHost(uri);
+		const apType = getApType(object);
 
-		if (!validPost.includes(getApType(object))) {
-			return new IdentifiableError('d450b8a9-48e4-4dab-ae36-f4db763fda7c', `invalid Note: invalid object type ${getApType(object)}`);
+		if (apType == null || !validPost.includes(apType)) {
+			return new IdentifiableError('d450b8a9-48e4-4dab-ae36-f4db763fda7c', `invalid Note: invalid object type ${apType ?? 'undefined'}`);
 		}
 
 		if (object.id && this.utilityService.extractDbHost(object.id) !== expectHost) {
@@ -182,7 +183,7 @@ export class ApNoteService {
 		/**
 		 * 禁止ワードチェック
 		 */
-		const hasProhibitedWords = await this.noteCreateService.checkProhibitedWordsContain({ cw, text, pollChoices: poll?.choices });
+		const hasProhibitedWords = this.noteCreateService.checkProhibitedWordsContain({ cw, text, pollChoices: poll?.choices });
 		if (hasProhibitedWords) {
 			throw new IdentifiableError('689ee33f-f97c-479a-ac49-1b9f8140af99', 'Note contains prohibited words');
 		}
@@ -253,7 +254,7 @@ export class ApNoteService {
 				}
 			};
 
-			const uris = unique([note._misskey_quote, note.quoteUrl].filter(isNotNull));
+			const uris = unique([note._misskey_quote, note.quoteUrl].filter(x => x != null));
 			const results = await Promise.all(uris.map(tryResolveNote));
 
 			quote = results.filter((x): x is { status: 'ok', res: MiNote } => x.status === 'ok').map(x => x.res).at(0);
@@ -335,9 +336,7 @@ export class ApNoteService {
 	public async resolveNote(value: string | IObject, options: { sentFrom?: URL, resolver?: Resolver } = {}): Promise<MiNote | null> {
 		const uri = getApId(value);
 
-		// ブロックしていたら中断
-		const meta = await this.metaService.fetch();
-		if (this.utilityService.isBlockedHost(meta.blockedHosts, this.utilityService.extractDbHost(uri))) {
+		if (!this.utilityService.isFederationAllowedUri(uri)) {
 			throw new StatusError('blocked host', 451);
 		}
 
diff --git a/packages/backend/src/core/activitypub/models/ApPersonService.ts b/packages/backend/src/core/activitypub/models/ApPersonService.ts
index 744b1ea683..c9de67b3a0 100644
--- a/packages/backend/src/core/activitypub/models/ApPersonService.ts
+++ b/packages/backend/src/core/activitypub/models/ApPersonService.ts
@@ -8,7 +8,7 @@ import promiseLimit from 'promise-limit';
 import { DataSource } from 'typeorm';
 import { ModuleRef } from '@nestjs/core';
 import { DI } from '@/di-symbols.js';
-import type { FollowingsRepository, InstancesRepository, UserProfilesRepository, UserPublickeysRepository, UsersRepository } from '@/models/_.js';
+import type { FollowingsRepository, InstancesRepository, MiMeta, UserProfilesRepository, UserPublickeysRepository, UsersRepository } from '@/models/_.js';
 import type { Config } from '@/config.js';
 import type { MiLocalUser, MiRemoteUser } from '@/models/User.js';
 import { MiUser } from '@/models/User.js';
@@ -34,11 +34,10 @@ import { StatusError } from '@/misc/status-error.js';
 import type { UtilityService } from '@/core/UtilityService.js';
 import type { UserEntityService } from '@/core/entities/UserEntityService.js';
 import { bindThis } from '@/decorators.js';
-import { MetaService } from '@/core/MetaService.js';
+import { RoleService } from '@/core/RoleService.js';
 import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
 import type { AccountMoveService } from '@/core/AccountMoveService.js';
 import { checkHttps } from '@/misc/check-https.js';
-import { isNotNull } from '@/misc/is-not-null.js';
 import { getApId, getApType, getOneApHrefNullable, isActor, isCollection, isCollectionOrOrderedCollection, isPropertyValue } from '../type.js';
 import { extractApHashtags } from './tag.js';
 import type { OnModuleInit } from '@nestjs/common';
@@ -46,9 +45,9 @@ import type { ApNoteService } from './ApNoteService.js';
 import type { ApMfmService } from '../ApMfmService.js';
 import type { ApResolverService, Resolver } from '../ApResolverService.js';
 import type { ApLoggerService } from '../ApLoggerService.js';
-// eslint-disable-next-line @typescript-eslint/consistent-type-imports
+
 import type { ApImageService } from './ApImageService.js';
-import type { IActor, IObject } from '../type.js';
+import type { IActor, ICollection, IObject, IOrderedCollection } from '../type.js';
 
 const nameLength = 128;
 const summaryLength = 2048;
@@ -62,7 +61,6 @@ export class ApPersonService implements OnModuleInit {
 	private driveFileEntityService: DriveFileEntityService;
 	private idService: IdService;
 	private globalEventService: GlobalEventService;
-	private metaService: MetaService;
 	private federatedInstanceService: FederatedInstanceService;
 	private fetchInstanceMetadataService: FetchInstanceMetadataService;
 	private cacheService: CacheService;
@@ -84,6 +82,9 @@ export class ApPersonService implements OnModuleInit {
 		@Inject(DI.config)
 		private config: Config,
 
+		@Inject(DI.meta)
+		private meta: MiMeta,
+
 		@Inject(DI.db)
 		private db: DataSource,
 
@@ -101,6 +102,8 @@ export class ApPersonService implements OnModuleInit {
 
 		@Inject(DI.followingsRepository)
 		private followingsRepository: FollowingsRepository,
+
+		private roleService: RoleService,
 	) {
 	}
 
@@ -110,7 +113,6 @@ export class ApPersonService implements OnModuleInit {
 		this.driveFileEntityService = this.moduleRef.get('DriveFileEntityService');
 		this.idService = this.moduleRef.get('IdService');
 		this.globalEventService = this.moduleRef.get('GlobalEventService');
-		this.metaService = this.moduleRef.get('MetaService');
 		this.federatedInstanceService = this.moduleRef.get('FederatedInstanceService');
 		this.fetchInstanceMetadataService = this.moduleRef.get('FetchInstanceMetadataService');
 		this.cacheService = this.moduleRef.get('CacheService');
@@ -230,6 +232,12 @@ export class ApPersonService implements OnModuleInit {
 		if (user == null) throw new Error('failed to create user: user is null');
 
 		const [avatar, banner] = await Promise.all([icon, image].map(img => {
+			// icon and image may be arrays
+			// see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-icon
+			if (Array.isArray(img)) {
+				img = img.find(item => item && item.url) ?? null;
+			}
+			
 			// if we have an explicitly missing image, return an
 			// explicitly-null set of values
 			if ((img == null) || (typeof img === 'object' && img.url == null)) {
@@ -239,6 +247,11 @@ export class ApPersonService implements OnModuleInit {
 			return this.apImageService.resolveImage(user, img).catch(() => null);
 		}));
 
+		if (((avatar != null && avatar.id != null) || (banner != null && banner.id != null))
+				&& !(await this.roleService.getUserPolicies(user.id)).canUpdateBioMedia) {
+			return {};
+		}
+
 		/*
 			we don't want to return nulls on errors! if the database fields
 			are already null, nothing changes; if the database has old
@@ -289,6 +302,21 @@ export class ApPersonService implements OnModuleInit {
 
 		const isBot = getApType(object) === 'Service' || getApType(object) === 'Application';
 
+		const [followingVisibility, followersVisibility] = await Promise.all(
+			[
+				this.isPublicCollection(person.following, resolver),
+				this.isPublicCollection(person.followers, resolver),
+			].map((p): Promise<'public' | 'private'> => p
+				.then(isPublic => isPublic ? 'public' : 'private')
+				.catch(err => {
+					if (!(err instanceof StatusError) || err.isRetryable) {
+						this.logger.error('error occurred while fetching following/followers collection', { stack: err });
+					}
+					return 'private';
+				}),
+			),
+		);
+
 		const bday = person['vcard:bday']?.match(/^\d{4}-\d{2}-\d{2}/);
 
 		const url = getOneApHrefNullable(person.url);
@@ -334,6 +362,9 @@ export class ApPersonService implements OnModuleInit {
 					tags,
 					isBot,
 					isCat: (person as any).isCat === true,
+					requireSigninToViewContents: (person as any).requireSigninToViewContents === true,
+					makeNotesFollowersOnlyBefore: (person as any).makeNotesFollowersOnlyBefore ?? null,
+					makeNotesHiddenBefore: (person as any).makeNotesHiddenBefore ?? null,
 					emojis,
 				})) as MiRemoteUser;
 
@@ -348,8 +379,11 @@ export class ApPersonService implements OnModuleInit {
 				await transactionalEntityManager.save(new MiUserProfile({
 					userId: user.id,
 					description: _description,
+					followedMessage: person._misskey_followedMessage != null ? truncate(person._misskey_followedMessage, 256) : null,
 					url,
 					fields,
+					followingVisibility,
+					followersVisibility,
 					birthday: bday?.[0] ?? null,
 					location: person['vcard:Address'] ?? null,
 					userHost: host,
@@ -383,13 +417,15 @@ export class ApPersonService implements OnModuleInit {
 		this.cacheService.uriPersonCache.set(user.uri, user);
 
 		// Register host
-		this.federatedInstanceService.fetch(host).then(async i => {
-			this.instancesRepository.increment({ id: i.id }, 'usersCount', 1);
-			this.fetchInstanceMetadataService.fetchInstanceMetadata(i);
-			if ((await this.metaService.fetch()).enableChartsForFederatedInstances) {
-				this.instanceChart.newUser(i.host);
-			}
-		});
+		if (this.meta.enableStatsForFederatedInstances) {
+			this.federatedInstanceService.fetchOrRegister(host).then(i => {
+				this.instancesRepository.increment({ id: i.id }, 'usersCount', 1);
+				if (this.meta.enableChartsForFederatedInstances) {
+					this.instanceChart.newUser(i.host);
+				}
+				this.fetchInstanceMetadataService.fetchInstanceMetadata(i);
+			});
+		}
 
 		this.usersChart.update(user, true);
 
@@ -457,6 +493,23 @@ export class ApPersonService implements OnModuleInit {
 
 		const tags = extractApHashtags(person.tag).map(normalizeForSearch).splice(0, 32);
 
+		const [followingVisibility, followersVisibility] = await Promise.all(
+			[
+				this.isPublicCollection(person.following, resolver),
+				this.isPublicCollection(person.followers, resolver),
+			].map((p): Promise<'public' | 'private' | undefined> => p
+				.then(isPublic => isPublic ? 'public' : 'private')
+				.catch(err => {
+					if (!(err instanceof StatusError) || err.isRetryable) {
+						this.logger.error('error occurred while fetching following/followers collection', { stack: err });
+						// Do not update the visibiility on transient errors.
+						return undefined;
+					}
+					return 'private';
+				}),
+			),
+		);
+
 		const bday = person['vcard:bday']?.match(/^\d{4}-\d{2}-\d{2}/);
 
 		const url = getOneApHrefNullable(person.url);
@@ -525,6 +578,9 @@ export class ApPersonService implements OnModuleInit {
 			url,
 			fields,
 			description: _description,
+			followedMessage: person._misskey_followedMessage != null ? truncate(person._misskey_followedMessage, 256) : null,
+			followingVisibility,
+			followersVisibility,
 			birthday: bday?.[0] ?? null,
 			location: person['vcard:Address'] ?? null,
 		});
@@ -637,7 +693,7 @@ export class ApPersonService implements OnModuleInit {
 
 			// とりあえずidを別の時間で生成して順番を維持
 			let td = 0;
-			for (const note of featuredNotes.filter(isNotNull)) {
+			for (const note of featuredNotes.filter(x => x != null)) {
 				td -= 1000;
 				transactionalEntityManager.insert(MiUserNotePining, {
 					id: this.idService.gen(Date.now() + td),
@@ -696,4 +752,16 @@ export class ApPersonService implements OnModuleInit {
 
 		return 'ok';
 	}
+
+	@bindThis
+	private async isPublicCollection(collection: string | ICollection | IOrderedCollection | undefined, resolver: Resolver): Promise<boolean> {
+		if (collection) {
+			const resolved = await resolver.resolveCollection(collection);
+			if (resolved.first || (resolved as ICollection).items || (resolved as IOrderedCollection).orderedItems) {
+				return true;
+			}
+		}
+
+		return false;
+	}
 }
diff --git a/packages/backend/src/core/activitypub/models/ApQuestionService.ts b/packages/backend/src/core/activitypub/models/ApQuestionService.ts
index d1936cfe1d..73004d10b0 100644
--- a/packages/backend/src/core/activitypub/models/ApQuestionService.ts
+++ b/packages/backend/src/core/activitypub/models/ApQuestionService.ts
@@ -10,7 +10,6 @@ import type { Config } from '@/config.js';
 import type { IPoll } from '@/models/Poll.js';
 import type Logger from '@/logger.js';
 import { bindThis } from '@/decorators.js';
-import { isNotNull } from '@/misc/is-not-null.js';
 import { isQuestion } from '../type.js';
 import { ApLoggerService } from '../ApLoggerService.js';
 import { ApResolverService } from '../ApResolverService.js';
@@ -52,7 +51,7 @@ export class ApQuestionService {
 
 		const choices = question[multiple ? 'anyOf' : 'oneOf']
 			?.map((x) => x.name)
-			.filter(isNotNull)
+			.filter(x => x != null)
 			?? [];
 
 		const votes = question[multiple ? 'anyOf' : 'oneOf']?.map((x) => x.replies?.totalItems ?? x._misskey_votes ?? 0);
@@ -75,10 +74,10 @@ export class ApQuestionService {
 
 		//#region このサーバーに既に登録されているか
 		const note = await this.notesRepository.findOneBy({ uri });
-		if (note == null) throw new Error('Question is not registed');
+		if (note == null) throw new Error('Question is not registered');
 
 		const poll = await this.pollsRepository.findOneBy({ noteId: note.id });
-		if (poll == null) throw new Error('Question is not registed');
+		if (poll == null) throw new Error('Question is not registered');
 		//#endregion
 
 		// resolve new Question object
diff --git a/packages/backend/src/core/activitypub/models/tag.ts b/packages/backend/src/core/activitypub/models/tag.ts
index e7ceec3262..f75cc45f7e 100644
--- a/packages/backend/src/core/activitypub/models/tag.ts
+++ b/packages/backend/src/core/activitypub/models/tag.ts
@@ -4,7 +4,6 @@
  */
 
 import { toArray } from '@/misc/prelude/array.js';
-import { isNotNull } from '@/misc/is-not-null.js';
 import { isHashtag } from '../type.js';
 import type { IObject, IApHashtag } from '../type.js';
 
@@ -16,7 +15,7 @@ export function extractApHashtags(tags: IObject | IObject[] | null | undefined):
 	return hashtags.map(tag => {
 		const m = tag.name.match(/^#(.+)/);
 		return m ? m[1] : null;
-	}).filter(isNotNull);
+	}).filter(x => x != null);
 }
 
 export function extractApHashtagObjects(tags: IObject | IObject[] | null | undefined): IApHashtag[] {
diff --git a/packages/backend/src/core/activitypub/type.ts b/packages/backend/src/core/activitypub/type.ts
index 5b6c6c8ca6..7496315f09 100644
--- a/packages/backend/src/core/activitypub/type.ts
+++ b/packages/backend/src/core/activitypub/type.ts
@@ -13,6 +13,10 @@ export interface IObject {
 	name?: string | null;
 	summary?: string;
 	_misskey_summary?: string;
+	_misskey_followedMessage?: string | null;
+	_misskey_requireSigninToViewContents?: boolean;
+	_misskey_makeNotesFollowersOnlyBefore?: number | null;
+	_misskey_makeNotesHiddenBefore?: number | null;
 	published?: string;
 	cc?: ApObject;
 	to?: ApObject;
@@ -60,11 +64,14 @@ export function getApId(value: string | IObject): string {
 
 /**
  * Get ActivityStreams Object type
+ *
+ * タイプ判定ができなかった場合に、あえてエラーではなくnullを返すようにしている。
+ * 詳細: https://github.com/misskey-dev/misskey/issues/14239
  */
-export function getApType(value: IObject): string {
+export function getApType(value: IObject): string | null {
 	if (typeof value.type === 'string') return value.type;
 	if (Array.isArray(value.type) && typeof value.type[0] === 'string') return value.type[0];
-	throw new Error('cannot detect type');
+	return null;
 }
 
 export function getOneApHrefNullable(value: ApObject | undefined): string | undefined {
@@ -97,19 +104,23 @@ export interface IActivity extends IObject {
 export interface ICollection extends IObject {
 	type: 'Collection';
 	totalItems: number;
-	items: ApObject;
+	first?: IObject | string;
+	items?: ApObject;
 }
 
 export interface IOrderedCollection extends IObject {
 	type: 'OrderedCollection';
 	totalItems: number;
-	orderedItems: ApObject;
+	first?: IObject | string;
+	orderedItems?: ApObject;
 }
 
 export const validPost = ['Note', 'Question', 'Article', 'Audio', 'Document', 'Image', 'Page', 'Video', 'Event'];
 
-export const isPost = (object: IObject): object is IPost =>
-	validPost.includes(getApType(object));
+export const isPost = (object: IObject): object is IPost => {
+	const type = getApType(object);
+	return type != null && validPost.includes(type);
+};
 
 export interface IPost extends IObject {
 	type: 'Note' | 'Question' | 'Article' | 'Audio' | 'Document' | 'Image' | 'Page' | 'Video' | 'Event';
@@ -156,8 +167,10 @@ export const isTombstone = (object: IObject): object is ITombstone =>
 
 export const validActor = ['Person', 'Service', 'Group', 'Organization', 'Application'];
 
-export const isActor = (object: IObject): object is IActor =>
-	validActor.includes(getApType(object));
+export const isActor = (object: IObject): object is IActor => {
+	const type = getApType(object);
+	return type != null && validActor.includes(type);
+};
 
 export interface IActor extends IObject {
 	type: 'Person' | 'Service' | 'Organization' | 'Group' | 'Application';
@@ -240,12 +253,16 @@ export interface IKey extends IObject {
 	publicKeyPem: string | Buffer;
 }
 
+export const validDocumentTypes = ['Audio', 'Document', 'Image', 'Page', 'Video'];
+
 export interface IApDocument extends IObject {
 	type: 'Audio' | 'Document' | 'Image' | 'Page' | 'Video';
 }
 
-export const isDocument = (object: IObject): object is IApDocument =>
-	['Audio', 'Document', 'Image', 'Page', 'Video'].includes(getApType(object));
+export const isDocument = (object: IObject): object is IApDocument => {
+	const type = getApType(object);
+	return type != null && validDocumentTypes.includes(type);
+};
 
 export interface IApImage extends IApDocument {
 	type: 'Image';
@@ -323,7 +340,10 @@ export const isAccept = (object: IObject): object is IAccept => getApType(object
 export const isReject = (object: IObject): object is IReject => getApType(object) === 'Reject';
 export const isAdd = (object: IObject): object is IAdd => getApType(object) === 'Add';
 export const isRemove = (object: IObject): object is IRemove => getApType(object) === 'Remove';
-export const isLike = (object: IObject): object is ILike => getApType(object) === 'Like' || getApType(object) === 'EmojiReaction' || getApType(object) === 'EmojiReact';
+export const isLike = (object: IObject): object is ILike => {
+	const type = getApType(object);
+	return type != null && ['Like', 'EmojiReaction', 'EmojiReact'].includes(type);
+};
 export const isAnnounce = (object: IObject): object is IAnnounce => getApType(object) === 'Announce';
 export const isBlock = (object: IObject): object is IBlock => getApType(object) === 'Block';
 export const isFlag = (object: IObject): object is IFlag => getApType(object) === 'Flag';
diff --git a/packages/backend/src/core/chart/ChartLoggerService.ts b/packages/backend/src/core/chart/ChartLoggerService.ts
index afc728d564..20815ea968 100644
--- a/packages/backend/src/core/chart/ChartLoggerService.ts
+++ b/packages/backend/src/core/chart/ChartLoggerService.ts
@@ -14,6 +14,6 @@ export class ChartLoggerService {
 	constructor(
 		private loggerService: LoggerService,
 	) {
-		this.logger = this.loggerService.getLogger('chart', 'white', process.env.NODE_ENV !== 'test');
+		this.logger = this.loggerService.getLogger('chart', 'white');
 	}
 }
diff --git a/packages/backend/src/core/chart/charts/federation.ts b/packages/backend/src/core/chart/charts/federation.ts
index 5e4555ee96..c9b43cc66d 100644
--- a/packages/backend/src/core/chart/charts/federation.ts
+++ b/packages/backend/src/core/chart/charts/federation.ts
@@ -5,10 +5,9 @@
 
 import { Injectable, Inject } from '@nestjs/common';
 import { DataSource } from 'typeorm';
-import type { FollowingsRepository, InstancesRepository } from '@/models/_.js';
+import type { FollowingsRepository, InstancesRepository, MiMeta } from '@/models/_.js';
 import { AppLockService } from '@/core/AppLockService.js';
 import { DI } from '@/di-symbols.js';
-import { MetaService } from '@/core/MetaService.js';
 import { bindThis } from '@/decorators.js';
 import Chart from '../core.js';
 import { ChartLoggerService } from '../ChartLoggerService.js';
@@ -24,13 +23,15 @@ export default class FederationChart extends Chart<typeof schema> { // eslint-di
 		@Inject(DI.db)
 		private db: DataSource,
 
+		@Inject(DI.meta)
+		private meta: MiMeta,
+
 		@Inject(DI.followingsRepository)
 		private followingsRepository: FollowingsRepository,
 
 		@Inject(DI.instancesRepository)
 		private instancesRepository: InstancesRepository,
 
-		private metaService: MetaService,
 		private appLockService: AppLockService,
 		private chartLoggerService: ChartLoggerService,
 	) {
@@ -43,11 +44,9 @@ export default class FederationChart extends Chart<typeof schema> { // eslint-di
 	}
 
 	protected async tickMinor(): Promise<Partial<KVs<typeof schema>>> {
-		const meta = await this.metaService.fetch();
-
 		const suspendedInstancesQuery = this.instancesRepository.createQueryBuilder('instance')
 			.select('instance.host')
-			.where('instance.isSuspended = true');
+			.where('instance.suspensionState != \'none\'');
 
 		const pubsubSubQuery = this.followingsRepository.createQueryBuilder('f')
 			.select('f.followerHost')
@@ -65,21 +64,21 @@ export default class FederationChart extends Chart<typeof schema> { // eslint-di
 			this.followingsRepository.createQueryBuilder('following')
 				.select('COUNT(DISTINCT following.followeeHost)')
 				.where('following.followeeHost IS NOT NULL')
-				.andWhere(meta.blockedHosts.length === 0 ? '1=1' : 'following.followeeHost NOT ILIKE ANY(ARRAY[:...blocked])', { blocked: meta.blockedHosts.flatMap(x => [x, `%.${x}`]) })
+				.andWhere(this.meta.blockedHosts.length === 0 ? '1=1' : 'following.followeeHost NOT ILIKE ALL(ARRAY[:...blocked])', { blocked: this.meta.blockedHosts.flatMap(x => [x, `%.${x}`]) })
 				.andWhere(`following.followeeHost NOT IN (${ suspendedInstancesQuery.getQuery() })`)
 				.getRawOne()
 				.then(x => parseInt(x.count, 10)),
 			this.followingsRepository.createQueryBuilder('following')
 				.select('COUNT(DISTINCT following.followerHost)')
 				.where('following.followerHost IS NOT NULL')
-				.andWhere(meta.blockedHosts.length === 0 ? '1=1' : 'following.followerHost NOT ILIKE ANY(ARRAY[:...blocked])', { blocked: meta.blockedHosts.flatMap(x => [x, `%.${x}`]) })
+				.andWhere(this.meta.blockedHosts.length === 0 ? '1=1' : 'following.followerHost NOT ILIKE ALL(ARRAY[:...blocked])', { blocked: this.meta.blockedHosts.flatMap(x => [x, `%.${x}`]) })
 				.andWhere(`following.followerHost NOT IN (${ suspendedInstancesQuery.getQuery() })`)
 				.getRawOne()
 				.then(x => parseInt(x.count, 10)),
 			this.followingsRepository.createQueryBuilder('following')
 				.select('COUNT(DISTINCT following.followeeHost)')
 				.where('following.followeeHost IS NOT NULL')
-				.andWhere(meta.blockedHosts.length === 0 ? '1=1' : 'following.followeeHost NOT ILIKE ANY(ARRAY[:...blocked])', { blocked: meta.blockedHosts.flatMap(x => [x, `%.${x}`]) })
+				.andWhere(this.meta.blockedHosts.length === 0 ? '1=1' : 'following.followeeHost NOT ILIKE ALL(ARRAY[:...blocked])', { blocked: this.meta.blockedHosts.flatMap(x => [x, `%.${x}`]) })
 				.andWhere(`following.followeeHost NOT IN (${ suspendedInstancesQuery.getQuery() })`)
 				.andWhere(`following.followeeHost IN (${ pubsubSubQuery.getQuery() })`)
 				.setParameters(pubsubSubQuery.getParameters())
@@ -88,16 +87,16 @@ export default class FederationChart extends Chart<typeof schema> { // eslint-di
 			this.instancesRepository.createQueryBuilder('instance')
 				.select('COUNT(instance.id)')
 				.where(`instance.host IN (${ subInstancesQuery.getQuery() })`)
-				.andWhere(meta.blockedHosts.length === 0 ? '1=1' : 'instance.host NOT ILIKE ANY(ARRAY[:...blocked])', { blocked: meta.blockedHosts.flatMap(x => [x, `%.${x}`]) })
-				.andWhere('instance.isSuspended = false')
+				.andWhere(this.meta.blockedHosts.length === 0 ? '1=1' : 'instance.host NOT ILIKE ALL(ARRAY[:...blocked])', { blocked: this.meta.blockedHosts.flatMap(x => [x, `%.${x}`]) })
+				.andWhere('instance.suspensionState = \'none\'')
 				.andWhere('instance.isNotResponding = false')
 				.getRawOne()
 				.then(x => parseInt(x.count, 10)),
 			this.instancesRepository.createQueryBuilder('instance')
 				.select('COUNT(instance.id)')
 				.where(`instance.host IN (${ pubInstancesQuery.getQuery() })`)
-				.andWhere(meta.blockedHosts.length === 0 ? '1=1' : 'instance.host NOT ILIKE ANY(ARRAY[:...blocked])', { blocked: meta.blockedHosts.flatMap(x => [x, `%.${x}`]) })
-				.andWhere('instance.isSuspended = false')
+				.andWhere(this.meta.blockedHosts.length === 0 ? '1=1' : 'instance.host NOT ILIKE ALL(ARRAY[:...blocked])', { blocked: this.meta.blockedHosts.flatMap(x => [x, `%.${x}`]) })
+				.andWhere('instance.suspensionState = \'none\'')
 				.andWhere('instance.isNotResponding = false')
 				.getRawOne()
 				.then(x => parseInt(x.count, 10)),
diff --git a/packages/backend/src/core/entities/AbuseReportNotificationRecipientEntityService.ts b/packages/backend/src/core/entities/AbuseReportNotificationRecipientEntityService.ts
new file mode 100644
index 0000000000..1e23c194c5
--- /dev/null
+++ b/packages/backend/src/core/entities/AbuseReportNotificationRecipientEntityService.ts
@@ -0,0 +1,87 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Inject, Injectable } from '@nestjs/common';
+import { In } from 'typeorm';
+import { DI } from '@/di-symbols.js';
+import type { AbuseReportNotificationRecipientRepository, MiAbuseReportNotificationRecipient } from '@/models/_.js';
+import { bindThis } from '@/decorators.js';
+import { UserEntityService } from '@/core/entities/UserEntityService.js';
+import { Packed } from '@/misc/json-schema.js';
+import { SystemWebhookEntityService } from '@/core/entities/SystemWebhookEntityService.js';
+
+@Injectable()
+export class AbuseReportNotificationRecipientEntityService {
+	constructor(
+		@Inject(DI.abuseReportNotificationRecipientRepository)
+		private abuseReportNotificationRecipientRepository: AbuseReportNotificationRecipientRepository,
+		private userEntityService: UserEntityService,
+		private systemWebhookEntityService: SystemWebhookEntityService,
+	) {
+	}
+
+	@bindThis
+	public async pack(
+		src: MiAbuseReportNotificationRecipient['id'] | MiAbuseReportNotificationRecipient,
+		opts?: {
+			users: Map<string, Packed<'UserLite'>>,
+			webhooks: Map<string, Packed<'SystemWebhook'>>,
+		},
+	): Promise<Packed<'AbuseReportNotificationRecipient'>> {
+		const recipient = typeof src === 'object'
+			? src
+			: await this.abuseReportNotificationRecipientRepository.findOneByOrFail({ id: src });
+		const user = recipient.userId
+			? (opts?.users.get(recipient.userId) ?? await this.userEntityService.pack<'UserLite'>(recipient.userId))
+			: undefined;
+		const webhook = recipient.systemWebhookId
+			? (opts?.webhooks.get(recipient.systemWebhookId) ?? await this.systemWebhookEntityService.pack(recipient.systemWebhookId))
+			: undefined;
+
+		return {
+			id: recipient.id,
+			isActive: recipient.isActive,
+			updatedAt: recipient.updatedAt.toISOString(),
+			name: recipient.name,
+			method: recipient.method,
+			userId: recipient.userId ?? undefined,
+			user: user,
+			systemWebhookId: recipient.systemWebhookId ?? undefined,
+			systemWebhook: webhook,
+		};
+	}
+
+	@bindThis
+	public async packMany(
+		src: MiAbuseReportNotificationRecipient['id'][] | MiAbuseReportNotificationRecipient[],
+	): Promise<Packed<'AbuseReportNotificationRecipient'>[]> {
+		const objs = src.filter((it): it is MiAbuseReportNotificationRecipient => typeof it === 'object');
+		const ids = src.filter((it): it is MiAbuseReportNotificationRecipient['id'] => typeof it === 'string');
+		if (ids.length > 0) {
+			objs.push(
+				...await this.abuseReportNotificationRecipientRepository.findBy({ id: In(ids) }),
+			);
+		}
+
+		const userIds = objs.map(it => it.userId).filter(x => x != null);
+		const users: Map<string, Packed<'UserLite'>> = (userIds.length > 0)
+			? await this.userEntityService.packMany(userIds)
+				.then(it => new Map(it.map(it => [it.id, it])))
+			: new Map();
+
+		const systemWebhookIds = objs.map(it => it.systemWebhookId).filter(x => x != null);
+		const systemWebhooks: Map<string, Packed<'SystemWebhook'>> = (systemWebhookIds.length > 0)
+			? await this.systemWebhookEntityService.packMany(systemWebhookIds)
+				.then(it => new Map(it.map(it => [it.id, it])))
+			: new Map();
+
+		return Promise
+			.all(
+				objs.map(it => this.pack(it, { users: users, webhooks: systemWebhooks })),
+			)
+			.then(it => it.sort((a, b) => a.id.localeCompare(b.id)));
+	}
+}
+
diff --git a/packages/backend/src/core/entities/AbuseUserReportEntityService.ts b/packages/backend/src/core/entities/AbuseUserReportEntityService.ts
index b0e1d1ab36..70ead890ab 100644
--- a/packages/backend/src/core/entities/AbuseUserReportEntityService.ts
+++ b/packages/backend/src/core/entities/AbuseUserReportEntityService.ts
@@ -10,7 +10,6 @@ import { awaitAll } from '@/misc/prelude/await-all.js';
 import type { MiAbuseUserReport } from '@/models/AbuseUserReport.js';
 import { bindThis } from '@/decorators.js';
 import { IdService } from '@/core/IdService.js';
-import { isNotNull } from '@/misc/is-not-null.js';
 import type { Packed } from '@/misc/json-schema.js';
 import { UserEntityService } from './UserEntityService.js';
 
@@ -54,6 +53,8 @@ export class AbuseUserReportEntityService {
 				schema: 'UserDetailedNotMe',
 			}) : null,
 			forwarded: report.forwarded,
+			resolvedAs: report.resolvedAs,
+			moderationNote: report.moderationNote,
 		});
 	}
 
@@ -63,7 +64,7 @@ export class AbuseUserReportEntityService {
 	) {
 		const _reporters = reports.map(({ reporter, reporterId }) => reporter ?? reporterId);
 		const _targetUsers = reports.map(({ targetUser, targetUserId }) => targetUser ?? targetUserId);
-		const _assignees = reports.map(({ assignee, assigneeId }) => assignee ?? assigneeId).filter(isNotNull);
+		const _assignees = reports.map(({ assignee, assigneeId }) => assignee ?? assigneeId).filter(x => x != null);
 		const _userMap = await this.userEntityService.packMany(
 			[..._reporters, ..._targetUsers, ..._assignees],
 			null,
diff --git a/packages/backend/src/core/entities/ClipEntityService.ts b/packages/backend/src/core/entities/ClipEntityService.ts
index 3855a28436..d915645906 100644
--- a/packages/backend/src/core/entities/ClipEntityService.ts
+++ b/packages/backend/src/core/entities/ClipEntityService.ts
@@ -53,7 +53,7 @@ export class ClipEntityService {
 			isPublic: clip.isPublic,
 			favoritedCount: await this.clipFavoritesRepository.countBy({ clipId: clip.id }),
 			isFavorited: meId ? await this.clipFavoritesRepository.exists({ where: { clipId: clip.id, userId: meId } }) : undefined,
-			notesCount: meId ? await this.clipNotesRepository.countBy({ clipId: clip.id }) : undefined,
+			notesCount: (meId === clip.userId) ? await this.clipNotesRepository.countBy({ clipId: clip.id }) : undefined,
 		});
 	}
 
diff --git a/packages/backend/src/core/entities/DriveFileEntityService.ts b/packages/backend/src/core/entities/DriveFileEntityService.ts
index 02ff2e7754..c485555f90 100644
--- a/packages/backend/src/core/entities/DriveFileEntityService.ts
+++ b/packages/backend/src/core/entities/DriveFileEntityService.ts
@@ -16,7 +16,6 @@ import { appendQuery, query } from '@/misc/prelude/url.js';
 import { deepClone } from '@/misc/clone.js';
 import { bindThis } from '@/decorators.js';
 import { isMimeImage } from '@/misc/is-mime-image.js';
-import { isNotNull } from '@/misc/is-not-null.js';
 import { IdService } from '@/core/IdService.js';
 import { UtilityService } from '../UtilityService.js';
 import { VideoProcessingService } from '../VideoProcessingService.js';
@@ -261,11 +260,11 @@ export class DriveFileEntityService {
 		files: MiDriveFile[],
 		options?: PackOptions,
 	): Promise<Packed<'DriveFile'>[]> {
-		const _user = files.map(({ user, userId }) => user ?? userId).filter(isNotNull);
+		const _user = files.map(({ user, userId }) => user ?? userId).filter(x => x != null);
 		const _userMap = await this.userEntityService.packMany(_user)
 			.then(users => new Map(users.map(user => [user.id, user])));
 		const items = await Promise.all(files.map(f => this.packNullable(f, options, f.userId ? { packedUser: _userMap.get(f.userId) } : {})));
-		return items.filter(isNotNull);
+		return items.filter(x => x != null);
 	}
 
 	@bindThis
@@ -290,6 +289,6 @@ export class DriveFileEntityService {
 	): Promise<Packed<'DriveFile'>[]> {
 		if (fileIds.length === 0) return [];
 		const filesMap = await this.packManyByIdsMap(fileIds, options);
-		return fileIds.map(id => filesMap.get(id)).filter(isNotNull);
+		return fileIds.map(id => filesMap.get(id)).filter(x => x != null);
 	}
 }
diff --git a/packages/backend/src/core/entities/FlashEntityService.ts b/packages/backend/src/core/entities/FlashEntityService.ts
index d110f7afc6..7b0150f5b6 100644
--- a/packages/backend/src/core/entities/FlashEntityService.ts
+++ b/packages/backend/src/core/entities/FlashEntityService.ts
@@ -5,10 +5,8 @@
 
 import { Inject, Injectable } from '@nestjs/common';
 import { DI } from '@/di-symbols.js';
-import type { FlashsRepository, FlashLikesRepository } from '@/models/_.js';
-import { awaitAll } from '@/misc/prelude/await-all.js';
+import type { FlashLikesRepository, FlashsRepository } from '@/models/_.js';
 import type { Packed } from '@/misc/json-schema.js';
-import type { } from '@/models/Blocking.js';
 import type { MiUser } from '@/models/User.js';
 import type { MiFlash } from '@/models/Flash.js';
 import { bindThis } from '@/decorators.js';
@@ -20,10 +18,8 @@ export class FlashEntityService {
 	constructor(
 		@Inject(DI.flashsRepository)
 		private flashsRepository: FlashsRepository,
-
 		@Inject(DI.flashLikesRepository)
 		private flashLikesRepository: FlashLikesRepository,
-
 		private userEntityService: UserEntityService,
 		private idService: IdService,
 	) {
@@ -34,24 +30,36 @@ export class FlashEntityService {
 		src: MiFlash['id'] | MiFlash,
 		me?: { id: MiUser['id'] } | null | undefined,
 		hint?: {
-			packedUser?: Packed<'UserLite'>
+			packedUser?: Packed<'UserLite'>,
+			likedFlashIds?: MiFlash['id'][],
 		},
 	): Promise<Packed<'Flash'>> {
 		const meId = me ? me.id : null;
 		const flash = typeof src === 'object' ? src : await this.flashsRepository.findOneByOrFail({ id: src });
 
-		return await awaitAll({
+		// { schema: 'UserDetailed' } すると無限ループするので注意
+		const user = hint?.packedUser ?? await this.userEntityService.pack(flash.user ?? flash.userId, me);
+
+		let isLiked = undefined;
+		if (meId) {
+			isLiked = hint?.likedFlashIds
+				? hint.likedFlashIds.includes(flash.id)
+				: await this.flashLikesRepository.exists({ where: { flashId: flash.id, userId: meId } });
+		}
+
+		return {
 			id: flash.id,
 			createdAt: this.idService.parse(flash.id).date.toISOString(),
 			updatedAt: flash.updatedAt.toISOString(),
 			userId: flash.userId,
-			user: hint?.packedUser ?? this.userEntityService.pack(flash.user ?? flash.userId, me), // { schema: 'UserDetailed' } すると無限ループするので注意
+			user: user,
 			title: flash.title,
 			summary: flash.summary,
 			script: flash.script,
+			visibility: flash.visibility,
 			likedCount: flash.likedCount,
-			isLiked: meId ? await this.flashLikesRepository.exists({ where: { flashId: flash.id, userId: meId } }) : undefined,
-		});
+			isLiked: isLiked,
+		};
 	}
 
 	@bindThis
@@ -62,7 +70,19 @@ export class FlashEntityService {
 		const _users = flashes.map(({ user, userId }) => user ?? userId);
 		const _userMap = await this.userEntityService.packMany(_users, me)
 			.then(users => new Map(users.map(u => [u.id, u])));
-		return Promise.all(flashes.map(flash => this.pack(flash, me, { packedUser: _userMap.get(flash.userId) })));
+		const _likedFlashIds = me
+			? await this.flashLikesRepository.createQueryBuilder('flashLike')
+				.select('flashLike.flashId')
+				.where('flashLike.userId = :userId', { userId: me.id })
+				.getRawMany<{ flashLike_flashId: string }>()
+				.then(likes => [...new Set(likes.map(like => like.flashLike_flashId))])
+			: [];
+		return Promise.all(
+			flashes.map(flash => this.pack(flash, me, {
+				packedUser: _userMap.get(flash.userId),
+				likedFlashIds: _likedFlashIds,
+			})),
+		);
 	}
 }
 
diff --git a/packages/backend/src/core/entities/InstanceEntityService.ts b/packages/backend/src/core/entities/InstanceEntityService.ts
index 9117b13914..284537b986 100644
--- a/packages/backend/src/core/entities/InstanceEntityService.ts
+++ b/packages/backend/src/core/entities/InstanceEntityService.ts
@@ -3,19 +3,22 @@
  * SPDX-License-Identifier: AGPL-3.0-only
  */
 
-import { Injectable } from '@nestjs/common';
+import { Inject, Injectable } from '@nestjs/common';
 import type { Packed } from '@/misc/json-schema.js';
 import type { MiInstance } from '@/models/Instance.js';
-import { MetaService } from '@/core/MetaService.js';
 import { bindThis } from '@/decorators.js';
 import { UtilityService } from '@/core/UtilityService.js';
 import { RoleService } from '@/core/RoleService.js';
 import { MiUser } from '@/models/User.js';
+import { DI } from '@/di-symbols.js';
+import { MiMeta } from '@/models/_.js';
 
 @Injectable()
 export class InstanceEntityService {
 	constructor(
-		private metaService: MetaService,
+		@Inject(DI.meta)
+		private meta: MiMeta,
+
 		private roleService: RoleService,
 
 		private utilityService: UtilityService,
@@ -27,7 +30,6 @@ export class InstanceEntityService {
 		instance: MiInstance,
 		me?: { id: MiUser['id']; } | null | undefined,
 	): Promise<Packed<'FederationInstance'>> {
-		const meta = await this.metaService.fetch();
 		const iAmModerator = me ? await this.roleService.isModerator(me as MiUser) : false;
 
 		return {
@@ -41,7 +43,7 @@ export class InstanceEntityService {
 			isNotResponding: instance.isNotResponding,
 			isSuspended: instance.suspensionState !== 'none',
 			suspensionState: instance.suspensionState,
-			isBlocked: this.utilityService.isBlockedHost(meta.blockedHosts, instance.host),
+			isBlocked: this.utilityService.isBlockedHost(this.meta.blockedHosts, instance.host),
 			softwareName: instance.softwareName,
 			softwareVersion: instance.softwareVersion,
 			openRegistrations: instance.openRegistrations,
@@ -49,7 +51,8 @@ export class InstanceEntityService {
 			description: instance.description,
 			maintainerName: instance.maintainerName,
 			maintainerEmail: instance.maintainerEmail,
-			isSilenced: this.utilityService.isSilencedHost(meta.silencedHosts, instance.host),
+			isSilenced: this.utilityService.isSilencedHost(this.meta.silencedHosts, instance.host),
+			isMediaSilenced: this.utilityService.isMediaSilencedHost(this.meta.mediaSilencedHosts, instance.host),
 			iconUrl: instance.iconUrl,
 			faviconUrl: instance.faviconUrl,
 			themeColor: instance.themeColor,
@@ -62,8 +65,9 @@ export class InstanceEntityService {
 	@bindThis
 	public packMany(
 		instances: MiInstance[],
+		me?: { id: MiUser['id']; } | null | undefined,
 	) {
-		return Promise.all(instances.map(x => this.pack(x)));
+		return Promise.all(instances.map(x => this.pack(x, me)));
 	}
 }
 
diff --git a/packages/backend/src/core/entities/InviteCodeEntityService.ts b/packages/backend/src/core/entities/InviteCodeEntityService.ts
index 26f57e1299..5d3e823a2a 100644
--- a/packages/backend/src/core/entities/InviteCodeEntityService.ts
+++ b/packages/backend/src/core/entities/InviteCodeEntityService.ts
@@ -12,7 +12,6 @@ import type { MiUser } from '@/models/User.js';
 import type { MiRegistrationTicket } from '@/models/RegistrationTicket.js';
 import { bindThis } from '@/decorators.js';
 import { IdService } from '@/core/IdService.js';
-import { isNotNull } from '@/misc/is-not-null.js';
 import { UserEntityService } from './UserEntityService.js';
 
 @Injectable()
@@ -59,8 +58,8 @@ export class InviteCodeEntityService {
 		tickets: MiRegistrationTicket[],
 		me: { id: MiUser['id'] },
 	) {
-		const _createdBys = tickets.map(({ createdBy, createdById }) => createdBy ?? createdById).filter(isNotNull);
-		const _usedBys = tickets.map(({ usedBy, usedById }) => usedBy ?? usedById).filter(isNotNull);
+		const _createdBys = tickets.map(({ createdBy, createdById }) => createdBy ?? createdById).filter(x => x != null);
+		const _usedBys = tickets.map(({ usedBy, usedById }) => usedBy ?? usedById).filter(x => x != null);
 		const _userMap = await this.userEntityService.packMany([..._createdBys, ..._usedBys], me)
 			.then(users => new Map(users.map(u => [u.id, u])));
 		return Promise.all(
diff --git a/packages/backend/src/core/entities/MetaEntityService.ts b/packages/backend/src/core/entities/MetaEntityService.ts
index 5dfec589e1..409dca3426 100644
--- a/packages/backend/src/core/entities/MetaEntityService.ts
+++ b/packages/backend/src/core/entities/MetaEntityService.ts
@@ -10,7 +10,6 @@ import type { Packed } from '@/misc/json-schema.js';
 import type { MiMeta } from '@/models/Meta.js';
 import type { AdsRepository } from '@/models/_.js';
 import { MAX_NOTE_TEXT_LENGTH } from '@/const.js';
-import { MetaService } from '@/core/MetaService.js';
 import { bindThis } from '@/decorators.js';
 import { UserEntityService } from '@/core/entities/UserEntityService.js';
 import { InstanceActorService } from '@/core/InstanceActorService.js';
@@ -24,11 +23,13 @@ export class MetaEntityService {
 		@Inject(DI.config)
 		private config: Config,
 
+		@Inject(DI.meta)
+		private meta: MiMeta,
+
 		@Inject(DI.adsRepository)
 		private adsRepository: AdsRepository,
 
 		private userEntityService: UserEntityService,
-		private metaService: MetaService,
 		private instanceActorService: InstanceActorService,
 	) { }
 
@@ -37,7 +38,7 @@ export class MetaEntityService {
 		let instance = meta;
 
 		if (!instance) {
-			instance = await this.metaService.fetch();
+			instance = this.meta;
 		}
 
 		const ads = await this.adsRepository.createQueryBuilder('ads')
@@ -50,6 +51,22 @@ export class MetaEntityService {
 			}))
 			.getMany();
 
+		// クライアントの手間を減らすためあらかじめJSONに変換しておく
+		let defaultLightTheme = null;
+		let defaultDarkTheme = null;
+		if (instance.defaultLightTheme) {
+			try {
+				defaultLightTheme = JSON.stringify(JSON5.parse(instance.defaultLightTheme));
+			} catch (e) {
+			}
+		}
+		if (instance.defaultDarkTheme) {
+			try {
+				defaultDarkTheme = JSON.stringify(JSON5.parse(instance.defaultDarkTheme));
+			} catch (e) {
+			}
+		}
+
 		const packed: Packed<'MetaLite'> = {
 			maintainerName: instance.maintainerName,
 			maintainerEmail: instance.maintainerEmail,
@@ -79,6 +96,7 @@ export class MetaEntityService {
 			recaptchaSiteKey: instance.recaptchaSiteKey,
 			enableTurnstile: instance.enableTurnstile,
 			turnstileSiteKey: instance.turnstileSiteKey,
+			enableTestcaptcha: instance.enableTestcaptcha,
 			swPublickey: instance.swPublicKey,
 			themeColor: instance.themeColor,
 			mascotImageUrl: instance.mascotImageUrl ?? '/assets/ai.png',
@@ -90,9 +108,8 @@ export class MetaEntityService {
 			backgroundImageUrl: instance.backgroundImageUrl,
 			logoImageUrl: instance.logoImageUrl,
 			maxNoteTextLength: MAX_NOTE_TEXT_LENGTH,
-			// クライアントの手間を減らすためあらかじめJSONに変換しておく
-			defaultLightTheme: instance.defaultLightTheme ? JSON.stringify(JSON5.parse(instance.defaultLightTheme)) : null,
-			defaultDarkTheme: instance.defaultDarkTheme ? JSON.stringify(JSON5.parse(instance.defaultDarkTheme)) : null,
+			defaultLightTheme,
+			defaultDarkTheme,
 			ads: ads.map(ad => ({
 				id: ad.id,
 				url: ad.url,
@@ -113,6 +130,8 @@ export class MetaEntityService {
 
 			mediaProxy: this.config.mediaProxy,
 			enableUrlPreview: instance.urlPreviewEnabled,
+			noteSearchableScope: (this.config.meilisearch == null || this.config.meilisearch.scope !== 'local') ? 'global' : 'local',
+			maxFileSize: this.config.maxFileSize,
 		};
 
 		return packed;
@@ -123,7 +142,7 @@ export class MetaEntityService {
 		let instance = meta;
 
 		if (!instance) {
-			instance = await this.metaService.fetch();
+			instance = this.meta;
 		}
 
 		const packed = await this.pack(instance);
diff --git a/packages/backend/src/core/entities/NoteEntityService.ts b/packages/backend/src/core/entities/NoteEntityService.ts
index 2ce72c50b8..96cc6b028e 100644
--- a/packages/backend/src/core/entities/NoteEntityService.ts
+++ b/packages/backend/src/core/entities/NoteEntityService.ts
@@ -11,30 +11,57 @@ import type { Packed } from '@/misc/json-schema.js';
 import { awaitAll } from '@/misc/prelude/await-all.js';
 import type { MiUser } from '@/models/User.js';
 import type { MiNote } from '@/models/Note.js';
-import type { MiNoteReaction } from '@/models/NoteReaction.js';
-import type { UsersRepository, NotesRepository, FollowingsRepository, PollsRepository, PollVotesRepository, NoteReactionsRepository, ChannelsRepository } from '@/models/_.js';
+import type { UsersRepository, NotesRepository, FollowingsRepository, PollsRepository, PollVotesRepository, NoteReactionsRepository, ChannelsRepository, MiMeta } from '@/models/_.js';
 import { bindThis } from '@/decorators.js';
-import { isNotNull } from '@/misc/is-not-null.js';
 import { DebounceLoader } from '@/misc/loader.js';
 import { IdService } from '@/core/IdService.js';
+import { ReactionsBufferingService } from '@/core/ReactionsBufferingService.js';
 import type { OnModuleInit } from '@nestjs/common';
 import type { CustomEmojiService } from '../CustomEmojiService.js';
 import type { ReactionService } from '../ReactionService.js';
 import type { UserEntityService } from './UserEntityService.js';
 import type { DriveFileEntityService } from './DriveFileEntityService.js';
 
+// is-renote.tsとよしなにリンク
+function isPureRenote(note: MiNote): note is MiNote & { renoteId: MiNote['id']; renote: MiNote } {
+	return (
+		note.renote != null &&
+		note.reply == null &&
+		note.text == null &&
+		note.cw == null &&
+		(note.fileIds == null || note.fileIds.length === 0) &&
+		!note.hasPoll
+	);
+}
+
+function getAppearNoteIds(notes: MiNote[]): Set<string> {
+	const appearNoteIds = new Set<string>();
+	for (const note of notes) {
+		if (isPureRenote(note)) {
+			appearNoteIds.add(note.renoteId);
+		} else {
+			appearNoteIds.add(note.id);
+		}
+	}
+	return appearNoteIds;
+}
+
 @Injectable()
 export class NoteEntityService implements OnModuleInit {
 	private userEntityService: UserEntityService;
 	private driveFileEntityService: DriveFileEntityService;
 	private customEmojiService: CustomEmojiService;
 	private reactionService: ReactionService;
+	private reactionsBufferingService: ReactionsBufferingService;
 	private idService: IdService;
 	private noteLoader = new DebounceLoader(this.findNoteOrFail);
 
 	constructor(
 		private moduleRef: ModuleRef,
 
+		@Inject(DI.meta)
+		private meta: MiMeta,
+
 		@Inject(DI.usersRepository)
 		private usersRepository: UsersRepository,
 
@@ -60,6 +87,8 @@ export class NoteEntityService implements OnModuleInit {
 		//private driveFileEntityService: DriveFileEntityService,
 		//private customEmojiService: CustomEmojiService,
 		//private reactionService: ReactionService,
+		//private reactionsBufferingService: ReactionsBufferingService,
+		//private idService: IdService,
 	) {
 	}
 
@@ -68,54 +97,85 @@ export class NoteEntityService implements OnModuleInit {
 		this.driveFileEntityService = this.moduleRef.get('DriveFileEntityService');
 		this.customEmojiService = this.moduleRef.get('CustomEmojiService');
 		this.reactionService = this.moduleRef.get('ReactionService');
+		this.reactionsBufferingService = this.moduleRef.get('ReactionsBufferingService');
 		this.idService = this.moduleRef.get('IdService');
 	}
 
 	@bindThis
-	private async hideNote(packedNote: Packed<'Note'>, meId: MiUser['id'] | null) {
+	private async hideNote(packedNote: Packed<'Note'>, meId: MiUser['id'] | null): Promise<void> {
+		// FIXME: このvisibility変更処理が当関数にあるのは若干不自然かもしれない(関数名を treatVisibility とかに変える手もある)
+		if (packedNote.visibility === 'public' || packedNote.visibility === 'home') {
+			const followersOnlyBefore = packedNote.user.makeNotesFollowersOnlyBefore;
+			if ((followersOnlyBefore != null)
+				&& (
+					(followersOnlyBefore <= 0 && (Date.now() - new Date(packedNote.createdAt).getTime() > 0 - (followersOnlyBefore * 1000)))
+					|| (followersOnlyBefore > 0 && (new Date(packedNote.createdAt).getTime() < followersOnlyBefore * 1000))
+				)
+			) {
+				packedNote.visibility = 'followers';
+			}
+		}
+
+		if (meId === packedNote.userId) return;
+
 		// TODO: isVisibleForMe を使うようにしても良さそう(型違うけど)
 		let hide = false;
 
-		// visibility が specified かつ自分が指定されていなかったら非表示
-		if (packedNote.visibility === 'specified') {
-			if (meId == null) {
-				hide = true;
-			} else if (meId === packedNote.userId) {
-				hide = false;
-			} else {
-				// 指定されているかどうか
-				const specified = packedNote.visibleUserIds!.some((id: any) => meId === id);
+		if (packedNote.user.requireSigninToViewContents && meId == null) {
+			hide = true;
+		}
 
-				if (specified) {
-					hide = false;
-				} else {
+		if (!hide) {
+			const hiddenBefore = packedNote.user.makeNotesHiddenBefore;
+			if ((hiddenBefore != null)
+				&& (
+					(hiddenBefore <= 0 && (Date.now() - new Date(packedNote.createdAt).getTime() > 0 - (hiddenBefore * 1000)))
+					|| (hiddenBefore > 0 && (new Date(packedNote.createdAt).getTime() < hiddenBefore * 1000))
+				)
+			) {
+				hide = true;
+			}
+		}
+
+		// visibility が specified かつ自分が指定されていなかったら非表示
+		if (!hide) {
+			if (packedNote.visibility === 'specified') {
+				if (meId == null) {
 					hide = true;
+				} else {
+					// 指定されているかどうか
+					const specified = packedNote.visibleUserIds!.some(id => meId === id);
+
+					if (!specified) {
+						hide = true;
+					}
 				}
 			}
 		}
 
 		// visibility が followers かつ自分が投稿者のフォロワーでなかったら非表示
-		if (packedNote.visibility === 'followers') {
-			if (meId == null) {
-				hide = true;
-			} else if (meId === packedNote.userId) {
-				hide = false;
-			} else if (packedNote.reply && (meId === packedNote.reply.userId)) {
-				// 自分の投稿に対するリプライ
-				hide = false;
-			} else if (packedNote.mentions && packedNote.mentions.some(id => meId === id)) {
-				// 自分へのメンション
-				hide = false;
-			} else {
-				// フォロワーかどうか
-				const isFollowing = await this.followingsRepository.exists({
-					where: {
-						followeeId: packedNote.userId,
-						followerId: meId,
-					},
-				});
+		if (!hide) {
+			if (packedNote.visibility === 'followers') {
+				if (meId == null) {
+					hide = true;
+				} else if (packedNote.reply && (meId === packedNote.reply.userId)) {
+					// 自分の投稿に対するリプライ
+					hide = false;
+				} else if (packedNote.mentions && packedNote.mentions.some(id => meId === id)) {
+					// 自分へのメンション
+					hide = false;
+				} else {
+					// フォロワーかどうか
+					// TODO: 当関数呼び出しごとにクエリが走るのは重そうだからなんとかする
+					const isFollowing = await this.followingsRepository.exists({
+						where: {
+							followeeId: packedNote.userId,
+							followerId: meId,
+						},
+					});
 
-				hide = !isFollowing;
+					hide = !isFollowing;
+				}
 			}
 		}
 
@@ -127,6 +187,7 @@ export class NoteEntityService implements OnModuleInit {
 			packedNote.poll = undefined;
 			packedNote.cw = null;
 			packedNote.isHidden = true;
+			// TODO: hiddenReason みたいなのを提供しても良さそう
 		}
 	}
 
@@ -221,7 +282,7 @@ export class NoteEntityService implements OnModuleInit {
 				return true;
 			} else {
 				// 指定されているかどうか
-				return note.visibleUserIds.some((id: any) => meId === id);
+				return note.visibleUserIds.some(id => meId === id);
 			}
 		}
 
@@ -276,7 +337,7 @@ export class NoteEntityService implements OnModuleInit {
 				packedFiles.set(k, v);
 			}
 		}
-		return fileIds.map(id => packedFiles.get(id)).filter(isNotNull);
+		return fileIds.map(id => packedFiles.get(id)).filter(x => x != null);
 	}
 
 	@bindThis
@@ -288,6 +349,7 @@ export class NoteEntityService implements OnModuleInit {
 			skipHide?: boolean;
 			withReactionAndUserPairCache?: boolean;
 			_hint_?: {
+				bufferedReactions: Map<MiNote['id'], { deltas: Record<string, number>; pairs: ([MiUser['id'], string])[] }> | null;
 				myReactions: Map<MiNote['id'], string | null>;
 				packedFiles: Map<MiNote['fileIds'][number], Packed<'DriveFile'> | null>;
 				packedUsers: Map<MiUser['id'], Packed<'UserLite'>>
@@ -304,6 +366,15 @@ export class NoteEntityService implements OnModuleInit {
 		const note = typeof src === 'object' ? src : await this.noteLoader.load(src);
 		const host = note.userHost;
 
+		const bufferedReactions = opts._hint_?.bufferedReactions != null
+			? (opts._hint_.bufferedReactions.get(note.id) ?? { deltas: {}, pairs: [] })
+			: this.meta.enableReactionsBuffering
+				? await this.reactionsBufferingService.get(note.id)
+				: { deltas: {}, pairs: [] };
+		const reactions = this.reactionService.convertLegacyReactions(this.reactionsBufferingService.mergeReactions(note.reactions, bufferedReactions.deltas ?? {}));
+
+		const reactionAndUserPairCache = note.reactionAndUserPairCache.concat(bufferedReactions.pairs.map(x => x.join('/')));
+
 		let text = note.text;
 
 		if (note.name && (note.url ?? note.uri)) {
@@ -316,7 +387,7 @@ export class NoteEntityService implements OnModuleInit {
 				: await this.channelsRepository.findOneBy({ id: note.channelId })
 			: null;
 
-		const reactionEmojiNames = Object.keys(note.reactions)
+		const reactionEmojiNames = Object.keys(reactions)
 			.filter(x => x.startsWith(':') && x.includes('@') && !x.includes('@.')) // リモートカスタム絵文字のみ
 			.map(x => this.reactionService.decodeReaction(x).reaction.replaceAll(':', ''));
 		const packedFiles = options?._hint_?.packedFiles;
@@ -335,10 +406,10 @@ export class NoteEntityService implements OnModuleInit {
 			visibleUserIds: note.visibility === 'specified' ? note.visibleUserIds : undefined,
 			renoteCount: note.renoteCount,
 			repliesCount: note.repliesCount,
-			reactionCount: Object.values(note.reactions).reduce((a, b) => a + b, 0),
-			reactions: this.reactionService.convertLegacyReactions(note.reactions),
+			reactionCount: Object.values(reactions).reduce((a, b) => a + b, 0),
+			reactions: reactions,
 			reactionEmojis: this.customEmojiService.populateEmojis(reactionEmojiNames, host),
-			reactionAndUserPairCache: opts.withReactionAndUserPairCache ? note.reactionAndUserPairCache : undefined,
+			reactionAndUserPairCache: opts.withReactionAndUserPairCache ? reactionAndUserPairCache : undefined,
 			emojis: host != null ? this.customEmojiService.populateEmojis(note.emojis, host) : undefined,
 			tags: note.tags.length > 0 ? note.tags : undefined,
 			fileIds: note.fileIds,
@@ -377,8 +448,12 @@ export class NoteEntityService implements OnModuleInit {
 
 				poll: note.hasPoll ? this.populatePoll(note, meId) : undefined,
 
-				...(meId && Object.keys(note.reactions).length > 0 ? {
-					myReaction: this.populateMyReaction(note, meId, options?._hint_),
+				...(meId && Object.keys(reactions).length > 0 ? {
+					myReaction: this.populateMyReaction({
+						id: note.id,
+						reactions: reactions,
+						reactionAndUserPairCache: reactionAndUserPairCache,
+					}, meId, options?._hint_),
 				} : {}),
 			} : {}),
 		});
@@ -401,6 +476,8 @@ export class NoteEntityService implements OnModuleInit {
 	) {
 		if (notes.length === 0) return [];
 
+		const bufferedReactions = this.meta.enableReactionsBuffering ? await this.reactionsBufferingService.getMany([...getAppearNoteIds(notes)]) : null;
+
 		const meId = me ? me.id : null;
 		const myReactionsMap = new Map<MiNote['id'], string | null>();
 		if (meId) {
@@ -410,24 +487,34 @@ export class NoteEntityService implements OnModuleInit {
 			const oldId = this.idService.gen(Date.now() - 2000);
 
 			for (const note of notes) {
-				if (note.renote && (note.text == null && note.fileIds.length === 0)) { // pure renote
-					const reactionsCount = Object.values(note.renote.reactions).reduce((a, b) => a + b, 0);
+				if (isPureRenote(note)) {
+					const reactionsCount = Object.values(this.reactionsBufferingService.mergeReactions(note.renote.reactions, bufferedReactions?.get(note.renote.id)?.deltas ?? {})).reduce((a, b) => a + b, 0);
 					if (reactionsCount === 0) {
 						myReactionsMap.set(note.renote.id, null);
-					} else if (reactionsCount <= note.renote.reactionAndUserPairCache.length) {
-						const pair = note.renote.reactionAndUserPairCache.find(p => p.startsWith(meId));
-						myReactionsMap.set(note.renote.id, pair ? pair.split('/')[1] : null);
+					} else if (reactionsCount <= note.renote.reactionAndUserPairCache.length + (bufferedReactions?.get(note.renote.id)?.pairs.length ?? 0)) {
+						const pairInBuffer = bufferedReactions?.get(note.renote.id)?.pairs.find(p => p[0] === meId);
+						if (pairInBuffer) {
+							myReactionsMap.set(note.renote.id, pairInBuffer[1]);
+						} else {
+							const pair = note.renote.reactionAndUserPairCache.find(p => p.startsWith(meId));
+							myReactionsMap.set(note.renote.id, pair ? pair.split('/')[1] : null);
+						}
 					} else {
 						idsNeedFetchMyReaction.add(note.renote.id);
 					}
 				} else {
 					if (note.id < oldId) {
-						const reactionsCount = Object.values(note.reactions).reduce((a, b) => a + b, 0);
+						const reactionsCount = Object.values(this.reactionsBufferingService.mergeReactions(note.reactions, bufferedReactions?.get(note.id)?.deltas ?? {})).reduce((a, b) => a + b, 0);
 						if (reactionsCount === 0) {
 							myReactionsMap.set(note.id, null);
-						} else if (reactionsCount <= note.reactionAndUserPairCache.length) {
-							const pair = note.reactionAndUserPairCache.find(p => p.startsWith(meId));
-							myReactionsMap.set(note.id, pair ? pair.split('/')[1] : null);
+						} else if (reactionsCount <= note.reactionAndUserPairCache.length + (bufferedReactions?.get(note.id)?.pairs.length ?? 0)) {
+							const pairInBuffer = bufferedReactions?.get(note.id)?.pairs.find(p => p[0] === meId);
+							if (pairInBuffer) {
+								myReactionsMap.set(note.id, pairInBuffer[1]);
+							} else {
+								const pair = note.reactionAndUserPairCache.find(p => p.startsWith(meId));
+								myReactionsMap.set(note.id, pair ? pair.split('/')[1] : null);
+							}
 						} else {
 							idsNeedFetchMyReaction.add(note.id);
 						}
@@ -449,12 +536,12 @@ export class NoteEntityService implements OnModuleInit {
 
 		await this.customEmojiService.prefetchEmojis(this.aggregateNoteEmojis(notes));
 		// TODO: 本当は renote とか reply がないのに renoteId とか replyId があったらここで解決しておく
-		const fileIds = notes.map(n => [n.fileIds, n.renote?.fileIds, n.reply?.fileIds]).flat(2).filter(isNotNull);
+		const fileIds = notes.map(n => [n.fileIds, n.renote?.fileIds, n.reply?.fileIds]).flat(2).filter(x => x != null);
 		const packedFiles = fileIds.length > 0 ? await this.driveFileEntityService.packManyByIdsMap(fileIds) : new Map();
 		const users = [
 			...notes.map(({ user, userId }) => user ?? userId),
-			...notes.map(({ replyUserId }) => replyUserId).filter(isNotNull),
-			...notes.map(({ renoteUserId }) => renoteUserId).filter(isNotNull),
+			...notes.map(({ replyUserId }) => replyUserId).filter(x => x != null),
+			...notes.map(({ renoteUserId }) => renoteUserId).filter(x => x != null),
 		];
 		const packedUsers = await this.userEntityService.packMany(users, me)
 			.then(users => new Map(users.map(u => [u.id, u])));
@@ -462,6 +549,7 @@ export class NoteEntityService implements OnModuleInit {
 		return await Promise.all(notes.map(n => this.pack(n, me, {
 			...options,
 			_hint_: {
+				bufferedReactions,
 				myReactions: myReactionsMap,
 				packedFiles,
 				packedUsers,
diff --git a/packages/backend/src/core/entities/NotificationEntityService.ts b/packages/backend/src/core/entities/NotificationEntityService.ts
index 94d56c883b..dff6968f9c 100644
--- a/packages/backend/src/core/entities/NotificationEntityService.ts
+++ b/packages/backend/src/core/entities/NotificationEntityService.ts
@@ -13,7 +13,6 @@ import type { MiGroupedNotification, MiNotification } from '@/models/Notificatio
 import type { MiNote } from '@/models/Note.js';
 import type { Packed } from '@/misc/json-schema.js';
 import { bindThis } from '@/decorators.js';
-import { isNotNull } from '@/misc/is-not-null.js';
 import { FilterUnionByProperty, groupedNotificationTypes } from '@/types.js';
 import { CacheService } from '@/core/CacheService.js';
 import { RoleEntityService } from './RoleEntityService.js';
@@ -60,7 +59,7 @@ export class NotificationEntityService implements OnModuleInit {
 	async #packInternal <T extends MiNotification | MiGroupedNotification> (
 		src: T,
 		meId: MiUser['id'],
-		// eslint-disable-next-line @typescript-eslint/ban-types
+		 
 		options: {
 			checkValidNotifier?: boolean;
 		},
@@ -103,7 +102,7 @@ export class NotificationEntityService implements OnModuleInit {
 					user,
 					reaction: reaction.reaction,
 				};
-			}))).filter(r => isNotNull(r.user));
+			}))).filter(r => r.user != null);
 			// if all users have been deleted, don't show this notification
 			if (reactions.length === 0) {
 				return null;
@@ -124,7 +123,7 @@ export class NotificationEntityService implements OnModuleInit {
 				}
 
 				return this.userEntityService.pack(userId, { id: meId });
-			}))).filter(isNotNull);
+			}))).filter(x => x != null);
 			// if all users have been deleted, don't show this notification
 			if (users.length === 0) {
 				return null;
@@ -160,9 +159,16 @@ export class NotificationEntityService implements OnModuleInit {
 			...(notification.type === 'roleAssigned' ? {
 				role: role,
 			} : {}),
+			...(notification.type === 'followRequestAccepted' ? {
+				message: notification.message,
+			} : {}),
 			...(notification.type === 'achievementEarned' ? {
 				achievement: notification.achievement,
 			} : {}),
+			...(notification.type === 'exportCompleted' ? {
+				exportedEntity: notification.exportedEntity,
+				fileId: notification.fileId,
+			} : {}),
 			...(notification.type === 'app' ? {
 				body: notification.customBody,
 				header: notification.customHeader,
@@ -181,7 +187,7 @@ export class NotificationEntityService implements OnModuleInit {
 
 		validNotifications = await this.#filterValidNotifier(validNotifications, meId);
 
-		const noteIds = validNotifications.map(x => 'noteId' in x ? x.noteId : null).filter(isNotNull);
+		const noteIds = validNotifications.map(x => 'noteId' in x ? x.noteId : null).filter(x => x != null);
 		const notes = noteIds.length > 0 ? await this.notesRepository.find({
 			where: { id: In(noteIds) },
 			relations: ['user', 'reply', 'reply.user', 'renote', 'renote.user'],
@@ -223,14 +229,14 @@ export class NotificationEntityService implements OnModuleInit {
 			);
 		});
 
-		return (await Promise.all(packPromises)).filter(isNotNull);
+		return (await Promise.all(packPromises)).filter(x => x != null);
 	}
 
 	@bindThis
 	public async pack(
 		src: MiNotification | MiGroupedNotification,
 		meId: MiUser['id'],
-		// eslint-disable-next-line @typescript-eslint/ban-types
+		 
 		options: {
 			checkValidNotifier?: boolean;
 		},
@@ -305,7 +311,7 @@ export class NotificationEntityService implements OnModuleInit {
 			this.cacheService.userProfileCache.fetch(meId).then(p => new Set(p.mutedInstances)),
 		]);
 
-		const notifierIds = notifications.map(notification => 'notifierId' in notification ? notification.notifierId : null).filter(isNotNull);
+		const notifierIds = notifications.map(notification => 'notifierId' in notification ? notification.notifierId : null).filter(x => x != null);
 		const notifiers = notifierIds.length > 0 ? await this.usersRepository.find({
 			where: { id: In(notifierIds) },
 		}) : [];
@@ -313,7 +319,7 @@ export class NotificationEntityService implements OnModuleInit {
 		const filteredNotifications = ((await Promise.all(notifications.map(async (notification) => {
 			const isValid = this.#validateNotifier(notification, userIdsWhoMeMuting, userMutedInstances, notifiers);
 			return isValid ? notification : null;
-		}))) as [T | null] ).filter(isNotNull);
+		}))) as [T | null] ).filter(x => x != null);
 
 		return filteredNotifications;
 	}
diff --git a/packages/backend/src/core/entities/PageEntityService.ts b/packages/backend/src/core/entities/PageEntityService.ts
index 142d9e81db..46bf51bb6d 100644
--- a/packages/backend/src/core/entities/PageEntityService.ts
+++ b/packages/backend/src/core/entities/PageEntityService.ts
@@ -14,7 +14,6 @@ import type { MiPage } from '@/models/Page.js';
 import type { MiDriveFile } from '@/models/DriveFile.js';
 import { bindThis } from '@/decorators.js';
 import { IdService } from '@/core/IdService.js';
-import { isNotNull } from '@/misc/is-not-null.js';
 import { UserEntityService } from './UserEntityService.js';
 import { DriveFileEntityService } from './DriveFileEntityService.js';
 
@@ -106,7 +105,7 @@ export class PageEntityService {
 			script: page.script,
 			eyeCatchingImageId: page.eyeCatchingImageId,
 			eyeCatchingImage: page.eyeCatchingImageId ? await this.driveFileEntityService.pack(page.eyeCatchingImageId) : null,
-			attachedFiles: this.driveFileEntityService.packMany((await Promise.all(attachedFiles)).filter(isNotNull)),
+			attachedFiles: this.driveFileEntityService.packMany((await Promise.all(attachedFiles)).filter(x => x != null)),
 			likedCount: page.likedCount,
 			isLiked: meId ? await this.pageLikesRepository.exists({ where: { pageId: page.id, userId: meId } }) : undefined,
 		});
diff --git a/packages/backend/src/core/entities/SystemWebhookEntityService.ts b/packages/backend/src/core/entities/SystemWebhookEntityService.ts
new file mode 100644
index 0000000000..e18734091c
--- /dev/null
+++ b/packages/backend/src/core/entities/SystemWebhookEntityService.ts
@@ -0,0 +1,74 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Inject, Injectable } from '@nestjs/common';
+import { In } from 'typeorm';
+import { DI } from '@/di-symbols.js';
+import type { MiSystemWebhook, SystemWebhooksRepository } from '@/models/_.js';
+import { bindThis } from '@/decorators.js';
+import { Packed } from '@/misc/json-schema.js';
+
+@Injectable()
+export class SystemWebhookEntityService {
+	constructor(
+		@Inject(DI.systemWebhooksRepository)
+		private systemWebhooksRepository: SystemWebhooksRepository,
+	) {
+	}
+
+	@bindThis
+	public async pack(
+		src: MiSystemWebhook['id'] | MiSystemWebhook,
+		opts?: {
+			webhooks: Map<string, MiSystemWebhook>
+		},
+	): Promise<Packed<'SystemWebhook'>> {
+		const webhook = typeof src === 'object'
+			? src
+			: opts?.webhooks.get(src) ?? await this.systemWebhooksRepository.findOneByOrFail({ id: src });
+
+		return {
+			id: webhook.id,
+			isActive: webhook.isActive,
+			updatedAt: webhook.updatedAt.toISOString(),
+			latestSentAt: webhook.latestSentAt?.toISOString() ?? null,
+			latestStatus: webhook.latestStatus,
+			name: webhook.name,
+			on: webhook.on,
+			url: webhook.url,
+			secret: webhook.secret,
+		};
+	}
+
+	@bindThis
+	public async packMany(src: MiSystemWebhook['id'][] | MiSystemWebhook[]): Promise<Packed<'SystemWebhook'>[]> {
+		if (src.length === 0) {
+			return [];
+		}
+
+		const webhooks = Array.of<MiSystemWebhook>();
+		webhooks.push(
+			...src.filter((it): it is MiSystemWebhook => typeof it === 'object'),
+		);
+
+		const ids = src.filter((it): it is MiSystemWebhook['id'] => typeof it === 'string');
+		if (ids.length > 0) {
+			webhooks.push(
+				...await this.systemWebhooksRepository.findBy({ id: In(ids) }),
+			);
+		}
+
+		return Promise
+			.all(
+				webhooks.map(x =>
+					this.pack(x, {
+						webhooks: new Map(webhooks.map(x => [x.id, x])),
+					}),
+				),
+			)
+			.then(it => it.sort((a, b) => a.id.localeCompare(b.id)));
+	}
+}
+
diff --git a/packages/backend/src/core/entities/UserEntityService.ts b/packages/backend/src/core/entities/UserEntityService.ts
index b80a1ec206..d3c087a153 100644
--- a/packages/backend/src/core/entities/UserEntityService.ts
+++ b/packages/backend/src/core/entities/UserEntityService.ts
@@ -47,7 +47,6 @@ import { IdService } from '@/core/IdService.js';
 import type { AnnouncementService } from '@/core/AnnouncementService.js';
 import type { CustomEmojiService } from '@/core/CustomEmojiService.js';
 import { AvatarDecorationService } from '@/core/AvatarDecorationService.js';
-import { isNotNull } from '@/misc/is-not-null.js';
 import type { OnModuleInit } from '@nestjs/common';
 import type { NoteEntityService } from './NoteEntityService.js';
 import type { DriveFileEntityService } from './DriveFileEntityService.js';
@@ -455,12 +454,12 @@ export class UserEntityService implements OnModuleInit {
 		}
 
 		const followingCount = profile == null ? null :
-			(profile.followingVisibility === 'public') || isMe ? user.followingCount :
+			(profile.followingVisibility === 'public') || isMe || iAmModerator ? user.followingCount :
 			(profile.followingVisibility === 'followers') && (relation && relation.isFollowing) ? user.followingCount :
 			null;
 
 		const followersCount = profile == null ? null :
-			(profile.followersVisibility === 'public') || isMe ? user.followersCount :
+			(profile.followersVisibility === 'public') || isMe || iAmModerator ? user.followersCount :
 			(profile.followersVisibility === 'followers') && (relation && relation.isFollowing) ? user.followersCount :
 			null;
 
@@ -491,6 +490,9 @@ export class UserEntityService implements OnModuleInit {
 			}))) : [],
 			isBot: user.isBot,
 			isCat: user.isCat,
+			requireSigninToViewContents: user.requireSigninToViewContents === false ? undefined : true,
+			makeNotesFollowersOnlyBefore: user.makeNotesFollowersOnlyBefore ?? undefined,
+			makeNotesHiddenBefore: user.makeNotesHiddenBefore ?? undefined,
 			instance: user.host ? this.federatedInstanceService.federatedInstanceCache.fetch(user.host).then(instance => instance ? {
 				name: instance.name,
 				softwareName: instance.softwareName,
@@ -502,11 +504,15 @@ export class UserEntityService implements OnModuleInit {
 			emojis: this.customEmojiService.populateEmojis(user.emojis, user.host),
 			onlineStatus: this.getOnlineStatus(user),
 			// パフォーマンス上の理由でローカルユーザーのみ
-			badgeRoles: user.host == null ? this.roleService.getUserBadgeRoles(user.id).then(rs => rs.sort((a, b) => b.displayOrder - a.displayOrder).map(r => ({
-				name: r.name,
-				iconUrl: r.iconUrl,
-				displayOrder: r.displayOrder,
-			}))) : undefined,
+			badgeRoles: user.host == null ? this.roleService.getUserBadgeRoles(user.id).then((rs) => rs
+				.filter((r) => r.isPublic || iAmModerator)
+				.sort((a, b) => b.displayOrder - a.displayOrder)
+				.map((r) => ({
+					name: r.name,
+					iconUrl: r.iconUrl,
+					displayOrder: r.displayOrder,
+				})),
+			) : undefined,
 
 			...(isDetailed ? {
 				url: profile!.url,
@@ -514,7 +520,7 @@ export class UserEntityService implements OnModuleInit {
 				movedTo: user.movedToUri ? this.apPersonService.resolvePerson(user.movedToUri).then(user => user.id).catch(() => null) : null,
 				alsoKnownAs: user.alsoKnownAs
 					? Promise.all(user.alsoKnownAs.map(uri => this.apPersonService.fetchPerson(uri).then(user => user?.id).catch(() => null)))
-						.then(xs => xs.length === 0 ? null : xs.filter(isNotNull))
+						.then(xs => xs.length === 0 ? null : xs.filter(x => x != null))
 					: null,
 				createdAt: this.idService.parse(user.id).date.toISOString(),
 				updatedAt: user.updatedAt ? user.updatedAt.toISOString() : null,
@@ -542,11 +548,6 @@ export class UserEntityService implements OnModuleInit {
 				publicReactions: this.isLocalUser(user) ? profile!.publicReactions : false, // https://github.com/misskey-dev/misskey/issues/12964
 				followersVisibility: profile!.followersVisibility,
 				followingVisibility: profile!.followingVisibility,
-				twoFactorEnabled: profile!.twoFactorEnabled,
-				usePasswordLessLogin: profile!.usePasswordLessLogin,
-				securityKeys: profile!.twoFactorEnabled
-					? this.userSecurityKeysRepository.countBy({ userId: user.id }).then(result => result >= 1)
-					: false,
 				roles: this.roleService.getUserRoles(user.id).then(roles => roles.filter(role => role.isPublic).sort((a, b) => b.displayOrder - a.displayOrder).map(role => ({
 					id: role.id,
 					name: role.name,
@@ -561,9 +562,18 @@ export class UserEntityService implements OnModuleInit {
 				moderationNote: iAmModerator ? (profile!.moderationNote ?? '') : undefined,
 			} : {}),
 
+			...(isDetailed && (isMe || iAmModerator) ? {
+				twoFactorEnabled: profile!.twoFactorEnabled,
+				usePasswordLessLogin: profile!.usePasswordLessLogin,
+				securityKeys: profile!.twoFactorEnabled
+					? this.userSecurityKeysRepository.countBy({ userId: user.id }).then(result => result >= 1)
+					: false,
+			} : {}),
+
 			...(isDetailed && isMe ? {
 				avatarId: user.avatarId,
 				bannerId: user.bannerId,
+				followedMessage: profile!.followedMessage,
 				isModerator: isModerator,
 				isAdmin: isAdmin,
 				injectFeaturedNote: profile!.injectFeaturedNote,
@@ -632,6 +642,7 @@ export class UserEntityService implements OnModuleInit {
 				isRenoteMuted: relation.isRenoteMuted,
 				notify: relation.following?.notify ?? 'none',
 				withReplies: relation.following?.withReplies ?? false,
+				followedMessage: relation.isFollowing ? profile!.followedMessage : undefined,
 			} : {}),
 		} as Promiseable<Packed<S>>;
 
diff --git a/packages/backend/src/daemons/ServerStatsService.ts b/packages/backend/src/daemons/ServerStatsService.ts
index 2c70344c94..d229efb123 100644
--- a/packages/backend/src/daemons/ServerStatsService.ts
+++ b/packages/backend/src/daemons/ServerStatsService.ts
@@ -3,13 +3,14 @@
  * SPDX-License-Identifier: AGPL-3.0-only
  */
 
-import { Injectable } from '@nestjs/common';
+import { Inject, Injectable } from '@nestjs/common';
 import si from 'systeminformation';
 import Xev from 'xev';
 import * as osUtils from 'os-utils';
 import { bindThis } from '@/decorators.js';
-import { MetaService } from '@/core/MetaService.js';
 import type { OnApplicationShutdown } from '@nestjs/common';
+import { MiMeta } from '@/models/_.js';
+import { DI } from '@/di-symbols.js';
 
 const ev = new Xev();
 
@@ -23,7 +24,8 @@ export class ServerStatsService implements OnApplicationShutdown {
 	private intervalId: NodeJS.Timeout | null = null;
 
 	constructor(
-		private metaService: MetaService,
+		@Inject(DI.meta)
+		private meta: MiMeta,
 	) {
 	}
 
@@ -32,7 +34,7 @@ export class ServerStatsService implements OnApplicationShutdown {
 	 */
 	@bindThis
 	public async start(): Promise<void> {
-		if (!(await this.metaService.fetch(true)).enableServerMachineStats) return;
+		if (!this.meta.enableServerMachineStats) return;
 
 		const log = [] as any[];
 
diff --git a/packages/backend/src/decorators.ts b/packages/backend/src/decorators.ts
index 21777657d1..42f925e125 100644
--- a/packages/backend/src/decorators.ts
+++ b/packages/backend/src/decorators.ts
@@ -10,8 +10,9 @@
  * The getter will return a .bind version of the function
  * and memoize the result against a symbol on the instance
  */
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
 export function bindThis(target: any, key: string, descriptor: any) {
-	let fn = descriptor.value;
+	const fn = descriptor.value;
 
 	if (typeof fn !== 'function') {
 		throw new TypeError(`@bindThis decorator can only be applied to methods not: ${typeof fn}`);
@@ -21,26 +22,18 @@ export function bindThis(target: any, key: string, descriptor: any) {
 		configurable: true,
 		get() {
 			// eslint-disable-next-line no-prototype-builtins
-			if (this === target.prototype || this.hasOwnProperty(key) ||
-        typeof fn !== 'function') {
+			if (this === target.prototype || this.hasOwnProperty(key)) {
 				return fn;
 			}
 
 			const boundFn = fn.bind(this);
-			Object.defineProperty(this, key, {
+			Reflect.defineProperty(this, key, {
+				value: boundFn,
 				configurable: true,
-				get() {
-					return boundFn;
-				},
-				set(value) {
-					fn = value;
-					delete this[key];
-				},
+				writable: true,
 			});
+
 			return boundFn;
 		},
-		set(value: any) {
-			fn = value;
-		},
 	};
 }
diff --git a/packages/backend/src/di-symbols.ts b/packages/backend/src/di-symbols.ts
index 919f4794a3..e599fc7b37 100644
--- a/packages/backend/src/di-symbols.ts
+++ b/packages/backend/src/di-symbols.ts
@@ -6,11 +6,13 @@
 export const DI = {
 	config: Symbol('config'),
 	db: Symbol('db'),
+	meta: Symbol('meta'),
 	meilisearch: Symbol('meilisearch'),
 	redis: Symbol('redis'),
 	redisForPub: Symbol('redisForPub'),
 	redisForSub: Symbol('redisForSub'),
 	redisForTimelines: Symbol('redisForTimelines'),
+	redisForReactions: Symbol('redisForReactions'),
 
 	//#region Repositories
 	usersRepository: Symbol('usersRepository'),
@@ -49,6 +51,7 @@ export const DI = {
 	swSubscriptionsRepository: Symbol('swSubscriptionsRepository'),
 	hashtagsRepository: Symbol('hashtagsRepository'),
 	abuseUserReportsRepository: Symbol('abuseUserReportsRepository'),
+	abuseReportNotificationRecipientRepository: Symbol('abuseReportNotificationRecipientRepository'),
 	registrationTicketsRepository: Symbol('registrationTicketsRepository'),
 	authSessionsRepository: Symbol('authSessionsRepository'),
 	accessTokensRepository: Symbol('accessTokensRepository'),
@@ -70,6 +73,7 @@ export const DI = {
 	channelFavoritesRepository: Symbol('channelFavoritesRepository'),
 	registryItemsRepository: Symbol('registryItemsRepository'),
 	webhooksRepository: Symbol('webhooksRepository'),
+	systemWebhooksRepository: Symbol('systemWebhooksRepository'),
 	adsRepository: Symbol('adsRepository'),
 	passwordResetRequestsRepository: Symbol('passwordResetRequestsRepository'),
 	retentionAggregationsRepository: Symbol('retentionAggregationsRepository'),
diff --git a/packages/backend/src/logger.ts b/packages/backend/src/logger.ts
index d4705af601..ff5363a425 100644
--- a/packages/backend/src/logger.ts
+++ b/packages/backend/src/logger.ts
@@ -22,31 +22,27 @@ type Level = 'error' | 'success' | 'warning' | 'debug' | 'info';
 export default class Logger {
 	private context: Context;
 	private parentLogger: Logger | null = null;
-	private store: boolean;
 
-	constructor(context: string, color?: KEYWORD, store = true) {
+	constructor(context: string, color?: KEYWORD) {
 		this.context = {
 			name: context,
 			color: color,
 		};
-		this.store = store;
 	}
 
 	@bindThis
-	public createSubLogger(context: string, color?: KEYWORD, store = true): Logger {
-		const logger = new Logger(context, color, store);
+	public createSubLogger(context: string, color?: KEYWORD): Logger {
+		const logger = new Logger(context, color);
 		logger.parentLogger = this;
 		return logger;
 	}
 
 	@bindThis
-	private log(level: Level, message: string, data?: Record<string, any> | null, important = false, subContexts: Context[] = [], store = true): void {
+	private log(level: Level, message: string, data?: Record<string, any> | null, important = false, subContexts: Context[] = []): void {
 		if (envOption.quiet) return;
-		if (!this.store) store = false;
-		if (level === 'debug') store = false;
 
 		if (this.parentLogger) {
-			this.parentLogger.log(level, message, data, important, [this.context].concat(subContexts), store);
+			this.parentLogger.log(level, message, data, important, [this.context].concat(subContexts));
 			return;
 		}
 
diff --git a/packages/backend/src/misc/cache.ts b/packages/backend/src/misc/cache.ts
index bba64a06ef..f9692ce5d5 100644
--- a/packages/backend/src/misc/cache.ts
+++ b/packages/backend/src/misc/cache.ts
@@ -7,23 +7,23 @@ import * as Redis from 'ioredis';
 import { bindThis } from '@/decorators.js';
 
 export class RedisKVCache<T> {
-	private redisClient: Redis.Redis;
-	private name: string;
-	private lifetime: number;
-	private memoryCache: MemoryKVCache<T>;
-	private fetcher: (key: string) => Promise<T>;
-	private toRedisConverter: (value: T) => string;
-	private fromRedisConverter: (value: string) => T | undefined;
+	private readonly lifetime: number;
+	private readonly memoryCache: MemoryKVCache<T>;
+	private readonly fetcher: (key: string) => Promise<T>;
+	private readonly toRedisConverter: (value: T) => string;
+	private readonly fromRedisConverter: (value: string) => T | undefined;
 
-	constructor(redisClient: RedisKVCache<T>['redisClient'], name: RedisKVCache<T>['name'], opts: {
-		lifetime: RedisKVCache<T>['lifetime'];
-		memoryCacheLifetime: number;
-		fetcher: RedisKVCache<T>['fetcher'];
-		toRedisConverter: RedisKVCache<T>['toRedisConverter'];
-		fromRedisConverter: RedisKVCache<T>['fromRedisConverter'];
-	}) {
-		this.redisClient = redisClient;
-		this.name = name;
+	constructor(
+		private redisClient: Redis.Redis,
+		private name: string,
+		opts: {
+			lifetime: RedisKVCache<T>['lifetime'];
+			memoryCacheLifetime: number;
+			fetcher: RedisKVCache<T>['fetcher'];
+			toRedisConverter: RedisKVCache<T>['toRedisConverter'];
+			fromRedisConverter: RedisKVCache<T>['fromRedisConverter'];
+		},
+	) {
 		this.lifetime = opts.lifetime;
 		this.memoryCache = new MemoryKVCache(opts.memoryCacheLifetime);
 		this.fetcher = opts.fetcher;
@@ -55,7 +55,13 @@ export class RedisKVCache<T> {
 
 		const cached = await this.redisClient.get(`kvcache:${this.name}:${key}`);
 		if (cached == null) return undefined;
-		return this.fromRedisConverter(cached);
+
+		const value = this.fromRedisConverter(cached);
+		if (value !== undefined) {
+			this.memoryCache.set(key, value);
+		}
+
+		return value;
 	}
 
 	@bindThis
@@ -66,6 +72,10 @@ export class RedisKVCache<T> {
 
 	/**
 	 * キャッシュがあればそれを返し、無ければfetcherを呼び出して結果をキャッシュ&返します
+	 * This awaits the call to Redis to ensure that the write succeeded, which is important for a few reasons:
+	 *   * Other code uses this to synchronize changes between worker processes. A failed write can internally de-sync the cluster.
+	 *   * Without an `await`, consecutive calls could race. An unlucky race could result in the older write overwriting the newer value.
+	 *   * Not awaiting here makes the entire cache non-consistent. The prevents many possible uses.
 	 */
 	@bindThis
 	public async fetch(key: string): Promise<T> {
@@ -77,14 +87,14 @@ export class RedisKVCache<T> {
 
 		// Cache MISS
 		const value = await this.fetcher(key);
-		this.set(key, value);
+		await this.set(key, value);
 		return value;
 	}
 
 	@bindThis
 	public async refresh(key: string) {
 		const value = await this.fetcher(key);
-		this.set(key, value);
+		await this.set(key, value);
 
 		// TODO: イベント発行して他プロセスのメモリキャッシュも更新できるようにする
 	}
@@ -101,23 +111,23 @@ export class RedisKVCache<T> {
 }
 
 export class RedisSingleCache<T> {
-	private redisClient: Redis.Redis;
-	private name: string;
-	private lifetime: number;
-	private memoryCache: MemorySingleCache<T>;
-	private fetcher: () => Promise<T>;
-	private toRedisConverter: (value: T) => string;
-	private fromRedisConverter: (value: string) => T | undefined;
+	private readonly lifetime: number;
+	private readonly memoryCache: MemorySingleCache<T>;
+	private readonly fetcher: () => Promise<T>;
+	private readonly toRedisConverter: (value: T) => string;
+	private readonly fromRedisConverter: (value: string) => T | undefined;
 
-	constructor(redisClient: RedisSingleCache<T>['redisClient'], name: RedisSingleCache<T>['name'], opts: {
-		lifetime: RedisSingleCache<T>['lifetime'];
-		memoryCacheLifetime: number;
-		fetcher: RedisSingleCache<T>['fetcher'];
-		toRedisConverter: RedisSingleCache<T>['toRedisConverter'];
-		fromRedisConverter: RedisSingleCache<T>['fromRedisConverter'];
-	}) {
-		this.redisClient = redisClient;
-		this.name = name;
+	constructor(
+		private redisClient: Redis.Redis,
+		private name: string,
+		opts: {
+			lifetime: number;
+			memoryCacheLifetime: number;
+			fetcher: RedisSingleCache<T>['fetcher'];
+			toRedisConverter: RedisSingleCache<T>['toRedisConverter'];
+			fromRedisConverter: RedisSingleCache<T>['fromRedisConverter'];
+		},
+	) {
 		this.lifetime = opts.lifetime;
 		this.memoryCache = new MemorySingleCache(opts.memoryCacheLifetime);
 		this.fetcher = opts.fetcher;
@@ -149,7 +159,13 @@ export class RedisSingleCache<T> {
 
 		const cached = await this.redisClient.get(`singlecache:${this.name}`);
 		if (cached == null) return undefined;
-		return this.fromRedisConverter(cached);
+
+		const value = this.fromRedisConverter(cached);
+		if (value !== undefined) {
+			this.memoryCache.set(value);
+		}
+
+		return value;
 	}
 
 	@bindThis
@@ -160,6 +176,10 @@ export class RedisSingleCache<T> {
 
 	/**
 	 * キャッシュがあればそれを返し、無ければfetcherを呼び出して結果をキャッシュ&返します
+	 * This awaits the call to Redis to ensure that the write succeeded, which is important for a few reasons:
+	 *   * Other code uses this to synchronize changes between worker processes. A failed write can internally de-sync the cluster.
+	 *   * Without an `await`, consecutive calls could race. An unlucky race could result in the older write overwriting the newer value.
+	 *   * Not awaiting here makes the entire cache non-consistent. The prevents many possible uses.
 	 */
 	@bindThis
 	public async fetch(): Promise<T> {
@@ -171,14 +191,14 @@ export class RedisSingleCache<T> {
 
 		// Cache MISS
 		const value = await this.fetcher();
-		this.set(value);
+		await this.set(value);
 		return value;
 	}
 
 	@bindThis
 	public async refresh() {
 		const value = await this.fetcher();
-		this.set(value);
+		await this.set(value);
 
 		// TODO: イベント発行して他プロセスのメモリキャッシュも更新できるようにする
 	}
@@ -187,22 +207,12 @@ export class RedisSingleCache<T> {
 // TODO: メモリ節約のためあまり参照されないキーを定期的に削除できるようにする?
 
 export class MemoryKVCache<T> {
-	/**
-	 * データを持つマップ
-	 * @deprecated これを直接操作するべきではない
-	 */
-	public cache: Map<string, { date: number; value: T; }>;
-	private lifetime: number;
-	private gcIntervalHandle: NodeJS.Timeout;
+	private readonly cache = new Map<string, { date: number; value: T; }>();
+	private readonly gcIntervalHandle = setInterval(() => this.gc(), 1000 * 60 * 3); // 3m
 
-	constructor(lifetime: MemoryKVCache<never>['lifetime']) {
-		this.cache = new Map();
-		this.lifetime = lifetime;
-
-		this.gcIntervalHandle = setInterval(() => {
-			this.gc();
-		}, 1000 * 60 * 3);
-	}
+	constructor(
+		private readonly lifetime: number,
+	) {}
 
 	@bindThis
 	/**
@@ -287,10 +297,14 @@ export class MemoryKVCache<T> {
 	@bindThis
 	public gc(): void {
 		const now = Date.now();
+
 		for (const [key, { date }] of this.cache.entries()) {
-			if ((now - date) > this.lifetime) {
-				this.cache.delete(key);
-			}
+			// The map is ordered from oldest to youngest.
+			// We can stop once we find an entry that's still active, because all following entries must *also* be active.
+			const age = now - date;
+			if (age < this.lifetime) break;
+
+			this.cache.delete(key);
 		}
 	}
 
@@ -298,16 +312,19 @@ export class MemoryKVCache<T> {
 	public dispose(): void {
 		clearInterval(this.gcIntervalHandle);
 	}
+
+	public get entries() {
+		return this.cache.entries();
+	}
 }
 
 export class MemorySingleCache<T> {
 	private cachedAt: number | null = null;
 	private value: T | undefined;
-	private lifetime: number;
 
-	constructor(lifetime: MemorySingleCache<never>['lifetime']) {
-		this.lifetime = lifetime;
-	}
+	constructor(
+		private lifetime: number,
+	) {}
 
 	@bindThis
 	public set(value: T): void {
diff --git a/packages/backend/src/misc/collapsed-queue.ts b/packages/backend/src/misc/collapsed-queue.ts
new file mode 100644
index 0000000000..5bc20a78ae
--- /dev/null
+++ b/packages/backend/src/misc/collapsed-queue.ts
@@ -0,0 +1,44 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+type Job<V> = {
+	value: V;
+	timer: NodeJS.Timeout;
+};
+
+// TODO: redis使えるようにする
+export class CollapsedQueue<K, V> {
+	private jobs: Map<K, Job<V>> = new Map();
+
+	constructor(
+		private timeout: number,
+		private collapse: (oldValue: V, newValue: V) => V,
+		private perform: (key: K, value: V) => Promise<void>,
+	) {}
+
+	enqueue(key: K, value: V) {
+		if (this.jobs.has(key)) {
+			const old = this.jobs.get(key)!;
+			const merged = this.collapse(old.value, value);
+			this.jobs.set(key, { ...old, value: merged });
+		} else {
+			const timer = setTimeout(() => {
+				const job = this.jobs.get(key)!;
+				this.jobs.delete(key);
+				this.perform(key, job.value);
+			}, this.timeout);
+			this.jobs.set(key, { value, timer });
+		}
+	}
+
+	async performAllNow() {
+		const entries = [...this.jobs.entries()];
+		this.jobs.clear();
+		for (const [_key, job] of entries) {
+			clearTimeout(job.timer);
+		}
+		await Promise.allSettled(entries.map(([key, job]) => this.perform(key, job.value)));
+	}
+}
diff --git a/packages/backend/src/misc/fastify-hook-handlers.ts b/packages/backend/src/misc/fastify-hook-handlers.ts
index 3e1c099e00..fa3ef0a267 100644
--- a/packages/backend/src/misc/fastify-hook-handlers.ts
+++ b/packages/backend/src/misc/fastify-hook-handlers.ts
@@ -8,7 +8,7 @@ import type { onRequestHookHandler } from 'fastify';
 export const handleRequestRedirectToOmitSearch: onRequestHookHandler = (request, reply, done) => {
 	const index = request.url.indexOf('?');
 	if (~index) {
-		reply.redirect(301, request.url.slice(0, index));
+		reply.redirect(request.url.slice(0, index), 301);
 	}
 	done();
 };
diff --git a/packages/backend/src/misc/is-not-null.ts b/packages/backend/src/misc/is-not-null.ts
deleted file mode 100644
index 8d9dc8bb39..0000000000
--- a/packages/backend/src/misc/is-not-null.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and misskey-project
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-export function isNotNull<T extends NonNullable<unknown>>(input: T | undefined | null): input is T {
-	return input != null;
-}
diff --git a/packages/backend/src/misc/is-renote.ts b/packages/backend/src/misc/is-renote.ts
index 48f821806c..f4bb329d80 100644
--- a/packages/backend/src/misc/is-renote.ts
+++ b/packages/backend/src/misc/is-renote.ts
@@ -6,6 +6,8 @@
 import type { MiNote } from '@/models/Note.js';
 import type { Packed } from '@/misc/json-schema.js';
 
+// NoteEntityService.isPureRenote とよしなにリンク
+
 type Renote =
 	MiNote & {
 		renoteId: NonNullable<MiNote['renoteId']>
diff --git a/packages/backend/src/misc/is-user-related.ts b/packages/backend/src/misc/is-user-related.ts
index 93c9b2b814..862d6e6a38 100644
--- a/packages/backend/src/misc/is-user-related.ts
+++ b/packages/backend/src/misc/is-user-related.ts
@@ -4,6 +4,10 @@
  */
 
 export function isUserRelated(note: any, userIds: Set<string>, ignoreAuthor = false): boolean {
+	if (!note) {
+		return false;
+	}
+
 	if (userIds.has(note.userId) && !ignoreAuthor) {
 		return true;
 	}
diff --git a/packages/backend/src/misc/json-schema.ts b/packages/backend/src/misc/json-schema.ts
index 41e5bfe9e4..040e36228c 100644
--- a/packages/backend/src/misc/json-schema.ts
+++ b/packages/backend/src/misc/json-schema.ts
@@ -4,12 +4,12 @@
  */
 
 import {
-	packedUserLiteSchema,
-	packedUserDetailedNotMeOnlySchema,
 	packedMeDetailedOnlySchema,
-	packedUserDetailedNotMeSchema,
 	packedMeDetailedSchema,
+	packedUserDetailedNotMeOnlySchema,
+	packedUserDetailedNotMeSchema,
 	packedUserDetailedSchema,
+	packedUserLiteSchema,
 	packedUserSchema,
 } from '@/models/json-schema/user.js';
 import { packedNoteSchema } from '@/models/json-schema/note.js';
@@ -25,7 +25,7 @@ import { packedBlockingSchema } from '@/models/json-schema/blocking.js';
 import { packedNoteReactionSchema } from '@/models/json-schema/note-reaction.js';
 import { packedHashtagSchema } from '@/models/json-schema/hashtag.js';
 import { packedInviteCodeSchema } from '@/models/json-schema/invite-code.js';
-import { packedPageSchema, packedPageBlockSchema } from '@/models/json-schema/page.js';
+import { packedPageBlockSchema, packedPageSchema } from '@/models/json-schema/page.js';
 import { packedNoteFavoriteSchema } from '@/models/json-schema/note-favorite.js';
 import { packedChannelSchema } from '@/models/json-schema/channel.js';
 import { packedAntennaSchema } from '@/models/json-schema/antenna.js';
@@ -38,25 +38,27 @@ import { packedFlashSchema } from '@/models/json-schema/flash.js';
 import { packedAnnouncementSchema } from '@/models/json-schema/announcement.js';
 import { packedSigninSchema } from '@/models/json-schema/signin.js';
 import {
-	packedRoleLiteSchema,
-	packedRoleSchema,
-	packedRolePoliciesSchema,
+	packedRoleCondFormulaFollowersOrFollowingOrNotesSchema,
 	packedRoleCondFormulaLogicsSchema,
-	packedRoleCondFormulaValueNot,
-	packedRoleCondFormulaValueIsLocalOrRemoteSchema,
 	packedRoleCondFormulaValueAssignedRoleSchema,
 	packedRoleCondFormulaValueCreatedSchema,
-	packedRoleCondFormulaFollowersOrFollowingOrNotesSchema,
+	packedRoleCondFormulaValueIsLocalOrRemoteSchema,
+	packedRoleCondFormulaValueNot,
 	packedRoleCondFormulaValueSchema,
 	packedRoleCondFormulaValueUserSettingBooleanSchema,
+	packedRoleLiteSchema,
+	packedRolePoliciesSchema,
+	packedRoleSchema,
 } from '@/models/json-schema/role.js';
 import { packedAdSchema } from '@/models/json-schema/ad.js';
-import { packedReversiGameLiteSchema, packedReversiGameDetailedSchema } from '@/models/json-schema/reversi-game.js';
+import { packedReversiGameDetailedSchema, packedReversiGameLiteSchema } from '@/models/json-schema/reversi-game.js';
 import {
-	packedMetaLiteSchema,
 	packedMetaDetailedOnlySchema,
 	packedMetaDetailedSchema,
+	packedMetaLiteSchema,
 } from '@/models/json-schema/meta.js';
+import { packedSystemWebhookSchema } from '@/models/json-schema/system-webhook.js';
+import { packedAbuseReportNotificationRecipientSchema } from '@/models/json-schema/abuse-report-notification-recipient.js';
 
 export const refs = {
 	UserLite: packedUserLiteSchema,
@@ -111,6 +113,8 @@ export const refs = {
 	MetaLite: packedMetaLiteSchema,
 	MetaDetailedOnly: packedMetaDetailedOnlySchema,
 	MetaDetailed: packedMetaDetailedSchema,
+	SystemWebhook: packedSystemWebhookSchema,
+	AbuseReportNotificationRecipient: packedAbuseReportNotificationRecipientSchema,
 };
 
 export type Packed<x extends keyof typeof refs> = SchemaType<typeof refs[x]>;
@@ -140,7 +144,9 @@ export interface Schema extends OfSchema {
 	readonly type?: TypeStringef;
 	readonly nullable?: boolean;
 	readonly optional?: boolean;
+	readonly prefixItems?: ReadonlyArray<Schema>;
 	readonly items?: Schema;
+	readonly unevaluatedItems?: Schema | boolean;
 	readonly properties?: Obj;
 	readonly required?: ReadonlyArray<Extract<keyof NonNullable<this['properties']>, string>>;
 	readonly description?: string;
@@ -194,6 +200,7 @@ type UnionSchemaType<a extends readonly any[], X extends Schema = a[number]> = X
 //type UnionObjectSchemaType<a extends readonly any[], X extends Schema = a[number]> = X extends any ? ObjectSchemaType<X> : never;
 type UnionObjType<s extends Obj, a extends readonly any[], X extends ReadonlyArray<keyof s> = a[number]> = X extends any ? ObjType<s, X> : never;
 type ArrayUnion<T> = T extends any ? Array<T> : never;
+type ArrayToTuple<X extends ReadonlyArray<Schema>> = { [K in keyof X]: SchemaType<X[K]> };
 
 type ObjectSchemaTypeDef<p extends Schema> =
 	p['ref'] extends keyof typeof refs ? Packed<p['ref']> :
@@ -228,6 +235,12 @@ export type SchemaTypeDef<p extends Schema> =
 			p['items']['allOf'] extends ReadonlyArray<Schema> ? UnionToIntersection<UnionSchemaType<NonNullable<p['items']['allOf']>>>[] :
 			never
 		) :
+		p['prefixItems'] extends ReadonlyArray<Schema> ? (
+			p['items'] extends NonNullable<Schema> ? [...ArrayToTuple<p['prefixItems']>, ...SchemaType<p['items']>[]] :
+			p['items'] extends false ? ArrayToTuple<p['prefixItems']> :
+			p['unevaluatedItems'] extends false ? ArrayToTuple<p['prefixItems']> :
+			[...ArrayToTuple<p['prefixItems']>, ...unknown[]]
+		) :
 		p['items'] extends NonNullable<Schema> ? SchemaType<p['items']>[] :
 		any[]
 	) :
diff --git a/packages/backend/src/misc/json-value.ts b/packages/backend/src/misc/json-value.ts
new file mode 100644
index 0000000000..bd7fe12058
--- /dev/null
+++ b/packages/backend/src/misc/json-value.ts
@@ -0,0 +1,12 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+export type JsonValue = JsonArray | JsonObject | string | number | boolean | null;
+export type JsonObject = {[K in string]?: JsonValue};
+export type JsonArray = JsonValue[];
+
+export function isJsonObject(value: JsonValue | undefined): value is JsonObject {
+	return typeof value === 'object' && value !== null && !Array.isArray(value);
+}
diff --git a/packages/backend/src/misc/prelude/array.ts b/packages/backend/src/misc/prelude/array.ts
index dbfe1fff18..f741a0c913 100644
--- a/packages/backend/src/misc/prelude/array.ts
+++ b/packages/backend/src/misc/prelude/array.ts
@@ -65,44 +65,6 @@ export function maximum(xs: number[]): number {
 	return Math.max(...xs);
 }
 
-/**
- * Splits an array based on the equivalence relation.
- * The concatenation of the result is equal to the argument.
- */
-export function groupBy<T>(f: EndoRelation<T>, xs: T[]): T[][] {
-	const groups = [] as T[][];
-	for (const x of xs) {
-		const lastGroup = groups.at(-1);
-		if (lastGroup !== undefined && f(lastGroup[0], x)) {
-			lastGroup.push(x);
-		} else {
-			groups.push([x]);
-		}
-	}
-	return groups;
-}
-
-/**
- * Splits an array based on the equivalence relation induced by the function.
- * The concatenation of the result is equal to the argument.
- */
-export function groupOn<T, S>(f: (x: T) => S, xs: T[]): T[][] {
-	return groupBy((a, b) => f(a) === f(b), xs);
-}
-
-export function groupByX<T>(collections: T[], keySelector: (x: T) => string) {
-	return collections.reduce((obj: Record<string, T[]>, item: T) => {
-		const key = keySelector(item);
-		if (!Object.prototype.hasOwnProperty.call(obj, key)) {
-			obj[key] = [];
-		}
-
-		obj[key].push(item);
-
-		return obj;
-	}, {});
-}
-
 /**
  * Compare two arrays by lexicographical order
  */
diff --git a/packages/backend/src/misc/prelude/maybe.ts b/packages/backend/src/misc/prelude/maybe.ts
deleted file mode 100644
index 1c58ccb9c7..0000000000
--- a/packages/backend/src/misc/prelude/maybe.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and misskey-project
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-export interface IMaybe<T> {
-	isJust(): this is IJust<T>;
-}
-
-export interface IJust<T> extends IMaybe<T> {
-	get(): T;
-}
-
-export function just<T>(value: T): IJust<T> {
-	return {
-		isJust: () => true,
-		get: () => value,
-	};
-}
-
-export function nothing<T>(): IMaybe<T> {
-	return {
-		isJust: () => false,
-	};
-}
diff --git a/packages/backend/src/misc/prelude/string.ts b/packages/backend/src/misc/prelude/string.ts
deleted file mode 100644
index 67ea529961..0000000000
--- a/packages/backend/src/misc/prelude/string.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and misskey-project
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-export function concat(xs: string[]): string {
-	return xs.join('');
-}
-
-export function capitalize(s: string): string {
-	return toUpperCase(s.charAt(0)) + toLowerCase(s.slice(1));
-}
-
-export function toUpperCase(s: string): string {
-	return s.toUpperCase();
-}
-
-export function toLowerCase(s: string): string {
-	return s.toLowerCase();
-}
diff --git a/packages/backend/src/models/AbuseReportNotificationRecipient.ts b/packages/backend/src/models/AbuseReportNotificationRecipient.ts
new file mode 100644
index 0000000000..fbff880afc
--- /dev/null
+++ b/packages/backend/src/models/AbuseReportNotificationRecipient.ts
@@ -0,0 +1,100 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Column, Entity, Index, JoinColumn, ManyToOne, PrimaryColumn } from 'typeorm';
+import { MiSystemWebhook } from '@/models/SystemWebhook.js';
+import { MiUserProfile } from '@/models/UserProfile.js';
+import { id } from './util/id.js';
+import { MiUser } from './User.js';
+
+/**
+ * 通報受信時に通知を送信する方法.
+ */
+export type RecipientMethod = 'email' | 'webhook';
+
+@Entity('abuse_report_notification_recipient')
+export class MiAbuseReportNotificationRecipient {
+	@PrimaryColumn(id())
+	public id: string;
+
+	/**
+	 * 有効かどうか.
+	 */
+	@Index()
+	@Column('boolean', {
+		default: true,
+	})
+	public isActive: boolean;
+
+	/**
+	 * 更新日時.
+	 */
+	@Column('timestamp with time zone', {
+		default: () => 'CURRENT_TIMESTAMP',
+	})
+	public updatedAt: Date;
+
+	/**
+	 * 通知設定名.
+	 */
+	@Column('varchar', {
+		length: 255,
+	})
+	public name: string;
+
+	/**
+	 * 通知方法.
+	 */
+	@Index()
+	@Column('varchar', {
+		length: 64,
+	})
+	public method: RecipientMethod;
+
+	/**
+	 * 通知先のユーザID.
+	 */
+	@Index()
+	@Column({
+		...id(),
+		nullable: true,
+	})
+	public userId: MiUser['id'] | null;
+
+	/**
+	 * 通知先のユーザ.
+	 */
+	@ManyToOne(type => MiUser, {
+		onDelete: 'CASCADE',
+	})
+	@JoinColumn({ name: 'userId', referencedColumnName: 'id', foreignKeyConstraintName: 'FK_abuse_report_notification_recipient_userId1' })
+	public user: MiUser | null;
+
+	/**
+	 * 通知先のユーザプロフィール.
+	 */
+	@ManyToOne(type => MiUserProfile, {})
+	@JoinColumn({ name: 'userId', referencedColumnName: 'userId', foreignKeyConstraintName: 'FK_abuse_report_notification_recipient_userId2' })
+	public userProfile: MiUserProfile | null;
+
+	/**
+	 * 通知先のシステムWebhookId.
+	 */
+	@Index()
+	@Column({
+		...id(),
+		nullable: true,
+	})
+	public systemWebhookId: string | null;
+
+	/**
+	 * 通知先のシステムWebhook.
+	 */
+	@ManyToOne(type => MiSystemWebhook, {
+		onDelete: 'CASCADE',
+	})
+	@JoinColumn()
+	public systemWebhook: MiSystemWebhook | null;
+}
diff --git a/packages/backend/src/models/AbuseUserReport.ts b/packages/backend/src/models/AbuseUserReport.ts
index 0615fd7eb5..cb5672e4ac 100644
--- a/packages/backend/src/models/AbuseUserReport.ts
+++ b/packages/backend/src/models/AbuseUserReport.ts
@@ -50,6 +50,9 @@ export class MiAbuseUserReport {
 	})
 	public resolved: boolean;
 
+	/**
+	 * リモートサーバーに転送したかどうか
+	 */
 	@Column('boolean', {
 		default: false,
 	})
@@ -60,6 +63,21 @@ export class MiAbuseUserReport {
 	})
 	public comment: string;
 
+	@Column('varchar', {
+		length: 8192, default: '',
+	})
+	public moderationNote: string;
+
+	/**
+	 * accept 是認 ... 通報内容が正当であり、肯定的に対応された
+	 * reject 否認 ... 通報内容が正当でなく、否定的に対応された
+	 * null ... その他
+	 */
+	@Column('varchar', {
+		length: 128, nullable: true,
+	})
+	public resolvedAs: 'accept' | 'reject' | null;
+
 	//#region Denormalized fields
 	@Index()
 	@Column('varchar', {
diff --git a/packages/backend/src/models/DriveFile.ts b/packages/backend/src/models/DriveFile.ts
index 438b32f79a..7b03e3e494 100644
--- a/packages/backend/src/models/DriveFile.ts
+++ b/packages/backend/src/models/DriveFile.ts
@@ -82,7 +82,7 @@ export class MiDriveFile {
 	public storedInternal: boolean;
 
 	@Column('varchar', {
-		length: 512,
+		length: 1024,
 		comment: 'The URL of the DriveFile.',
 	})
 	public url: string;
@@ -124,13 +124,13 @@ export class MiDriveFile {
 
 	@Index()
 	@Column('varchar', {
-		length: 512, nullable: true,
+		length: 1024, nullable: true,
 		comment: 'The URI of the DriveFile. it will be null when the DriveFile is local.',
 	})
 	public uri: string | null;
 
 	@Column('varchar', {
-		length: 512, nullable: true,
+		length: 1024, nullable: true,
 	})
 	public src: string | null;
 
diff --git a/packages/backend/src/models/Flash.ts b/packages/backend/src/models/Flash.ts
index a1469a0d94..5db7dca992 100644
--- a/packages/backend/src/models/Flash.ts
+++ b/packages/backend/src/models/Flash.ts
@@ -7,6 +7,9 @@ import { Entity, Index, JoinColumn, Column, PrimaryColumn, ManyToOne } from 'typ
 import { id } from './util/id.js';
 import { MiUser } from './User.js';
 
+export const flashVisibility = ['public', 'private'] as const;
+export type FlashVisibility = typeof flashVisibility[number];
+
 @Entity('flash')
 export class MiFlash {
 	@PrimaryColumn(id())
@@ -63,5 +66,5 @@ export class MiFlash {
 	@Column('varchar', {
 		length: 512, default: 'public',
 	})
-	public visibility: 'public' | 'private';
+	public visibility: FlashVisibility;
 }
diff --git a/packages/backend/src/models/Meta.ts b/packages/backend/src/models/Meta.ts
index ad306fcad6..ad5e31ad6f 100644
--- a/packages/backend/src/models/Meta.ts
+++ b/packages/backend/src/models/Meta.ts
@@ -81,11 +81,21 @@ export class MiMeta {
 	})
 	public prohibitedWords: string[];
 
+	@Column('varchar', {
+		length: 1024, array: true, default: '{}',
+	})
+	public prohibitedWordsForNameOfUser: string[];
+
 	@Column('varchar', {
 		length: 1024, array: true, default: '{}',
 	})
 	public silencedHosts: string[];
 
+	@Column('varchar', {
+		length: 1024, array: true, default: '{}',
+	})
+	public mediaSilencedHosts: string[];
+
 	@Column('varchar', {
 		length: 1024,
 		nullable: true,
@@ -253,6 +263,11 @@ export class MiMeta {
 	})
 	public turnstileSecretKey: string | null;
 
+	@Column('boolean', {
+		default: false,
+	})
+	public enableTestcaptcha: boolean;
+
 	// chaptcha系を追加した際にはnodeinfoのレスポンスに追加するのを忘れないようにすること
 
 	@Column('enum', {
@@ -514,6 +529,11 @@ export class MiMeta {
 	})
 	public enableChartsForFederatedInstances: boolean;
 
+	@Column('boolean', {
+		default: true,
+	})
+	public enableStatsForFederatedInstances: boolean;
+
 	@Column('boolean', {
 		default: false,
 	})
@@ -584,6 +604,11 @@ export class MiMeta {
 	})
 	public perUserListTimelineCacheMax: number;
 
+	@Column('boolean', {
+		default: false,
+	})
+	public enableReactionsBuffering: boolean;
+
 	@Column('integer', {
 		default: 0,
 	})
@@ -620,4 +645,17 @@ export class MiMeta {
 		nullable: true,
 	})
 	public urlPreviewUserAgent: string | null;
+
+	@Column('varchar', {
+		length: 128,
+		default: 'all',
+	})
+	public federation: 'all' | 'specified' | 'none';
+
+	@Column('varchar', {
+		length: 1024,
+		array: true,
+		default: '{}',
+	})
+	public federationHosts: string[];
 }
diff --git a/packages/backend/src/models/Notification.ts b/packages/backend/src/models/Notification.ts
index df88b99636..b7f8e94d69 100644
--- a/packages/backend/src/models/Notification.ts
+++ b/packages/backend/src/models/Notification.ts
@@ -3,10 +3,12 @@
  * SPDX-License-Identifier: AGPL-3.0-only
  */
 
+import { userExportableEntities } from '@/types.js';
 import { MiUser } from './User.js';
 import { MiNote } from './Note.js';
 import { MiAccessToken } from './AccessToken.js';
 import { MiRole } from './Role.js';
+import { MiDriveFile } from './DriveFile.js';
 
 export type MiNotification = {
 	type: 'note';
@@ -67,6 +69,7 @@ export type MiNotification = {
 	id: string;
 	createdAt: string;
 	notifierId: MiUser['id'];
+	message: string | null;
 } | {
 	type: 'roleAssigned';
 	id: string;
@@ -77,6 +80,16 @@ export type MiNotification = {
 	id: string;
 	createdAt: string;
 	achievement: string;
+} | {
+	type: 'exportCompleted';
+	id: string;
+	createdAt: string;
+	exportedEntity: typeof userExportableEntities[number];
+	fileId: MiDriveFile['id'];
+} | {
+	type: 'login';
+	id: string;
+	createdAt: string;
 } | {
 	type: 'app';
 	id: string;
@@ -85,7 +98,7 @@ export type MiNotification = {
 	/**
 	 * アプリ通知のbody
 	 */
-	customBody: string | null;
+	customBody: string;
 
 	/**
 	 * アプリ通知のheader
diff --git a/packages/backend/src/models/RepositoryModule.ts b/packages/backend/src/models/RepositoryModule.ts
index d3062d6b36..ea0f88baba 100644
--- a/packages/backend/src/models/RepositoryModule.ts
+++ b/packages/backend/src/models/RepositoryModule.ts
@@ -3,11 +3,83 @@
  * SPDX-License-Identifier: AGPL-3.0-only
  */
 
+import type { Provider } from '@nestjs/common';
 import { Module } from '@nestjs/common';
 import { DI } from '@/di-symbols.js';
-import { MiRepository, MiAbuseUserReport, MiAccessToken, MiAd, MiAnnouncement, MiAnnouncementRead, MiAntenna, MiApp, MiAuthSession, MiAvatarDecoration, MiBlocking, MiChannel, MiChannelFavorite, MiChannelFollowing, MiClip, MiClipFavorite, MiClipNote, MiDriveFile, MiDriveFolder, MiEmoji, MiFlash, MiFlashLike, MiFollowRequest, MiFollowing, MiGalleryLike, MiGalleryPost, MiHashtag, MiInstance, MiMeta, MiModerationLog, MiMuting, MiNote, MiNoteFavorite, MiNoteReaction, MiNoteThreadMuting, MiNoteUnread, MiPage, MiPageLike, MiPasswordResetRequest, MiPoll, MiPollVote, MiPromoNote, MiPromoRead, MiRegistrationTicket, MiRegistryItem, MiRelay, MiRenoteMuting, MiRetentionAggregation, MiRole, MiRoleAssignment, MiSignin, MiSwSubscription, MiUsedUsername, MiUser, MiUserIp, MiUserKeypair, MiUserList, MiUserListFavorite, MiUserListMembership, MiUserMemo, MiUserNotePining, MiUserPending, MiUserProfile, MiUserPublickey, MiUserSecurityKey, MiWebhook, MiBubbleGameRecord, MiReversiGame, miRepository } from './_.js';
+import {
+	MiAbuseReportNotificationRecipient,
+	MiAbuseUserReport,
+	MiAccessToken,
+	MiAd,
+	MiAnnouncement,
+	MiAnnouncementRead,
+	MiAntenna,
+	MiApp,
+	MiAuthSession,
+	MiAvatarDecoration,
+	MiBlocking,
+	MiBubbleGameRecord,
+	MiChannel,
+	MiChannelFavorite,
+	MiChannelFollowing,
+	MiClip,
+	MiClipFavorite,
+	MiClipNote,
+	MiDriveFile,
+	MiDriveFolder,
+	MiEmoji,
+	MiFlash,
+	MiFlashLike,
+	MiFollowing,
+	MiFollowRequest,
+	MiGalleryLike,
+	MiGalleryPost,
+	MiHashtag,
+	MiInstance,
+	MiMeta,
+	MiModerationLog,
+	MiMuting,
+	MiNote,
+	MiNoteFavorite,
+	MiNoteReaction,
+	MiNoteThreadMuting,
+	MiNoteUnread,
+	MiPage,
+	MiPageLike,
+	MiPasswordResetRequest,
+	MiPoll,
+	MiPollVote,
+	MiPromoNote,
+	MiPromoRead,
+	MiRegistrationTicket,
+	MiRegistryItem,
+	MiRelay,
+	MiRenoteMuting,
+	MiRepository,
+	miRepository,
+	MiRetentionAggregation,
+	MiReversiGame,
+	MiRole,
+	MiRoleAssignment,
+	MiSignin,
+	MiSwSubscription,
+	MiSystemWebhook,
+	MiUsedUsername,
+	MiUser,
+	MiUserIp,
+	MiUserKeypair,
+	MiUserList,
+	MiUserListFavorite,
+	MiUserListMembership,
+	MiUserMemo,
+	MiUserNotePining,
+	MiUserPending,
+	MiUserProfile,
+	MiUserPublickey,
+	MiUserSecurityKey,
+	MiWebhook
+} from './_.js';
 import type { DataSource } from 'typeorm';
-import type { Provider } from '@nestjs/common';
 
 const $usersRepository: Provider = {
 	provide: DI.usersRepository,
@@ -225,6 +297,12 @@ const $abuseUserReportsRepository: Provider = {
 	inject: [DI.db],
 };
 
+const $abuseReportNotificationRecipientRepository: Provider = {
+	provide: DI.abuseReportNotificationRecipientRepository,
+	useFactory: (db: DataSource) => db.getRepository(MiAbuseReportNotificationRecipient),
+	inject: [DI.db],
+};
+
 const $registrationTicketsRepository: Provider = {
 	provide: DI.registrationTicketsRepository,
 	useFactory: (db: DataSource) => db.getRepository(MiRegistrationTicket).extend(miRepository as MiRepository<MiRegistrationTicket>),
@@ -351,6 +429,12 @@ const $webhooksRepository: Provider = {
 	inject: [DI.db],
 };
 
+const $systemWebhooksRepository: Provider = {
+	provide: DI.systemWebhooksRepository,
+	useFactory: (db: DataSource) => db.getRepository(MiSystemWebhook),
+	inject: [DI.db],
+};
+
 const $adsRepository: Provider = {
 	provide: DI.adsRepository,
 	useFactory: (db: DataSource) => db.getRepository(MiAd).extend(miRepository as MiRepository<MiAd>),
@@ -412,8 +496,7 @@ const $reversiGamesRepository: Provider = {
 };
 
 @Module({
-	imports: [
-	],
+	imports: [],
 	providers: [
 		$usersRepository,
 		$notesRepository,
@@ -451,6 +534,7 @@ const $reversiGamesRepository: Provider = {
 		$swSubscriptionsRepository,
 		$hashtagsRepository,
 		$abuseUserReportsRepository,
+		$abuseReportNotificationRecipientRepository,
 		$registrationTicketsRepository,
 		$authSessionsRepository,
 		$accessTokensRepository,
@@ -472,6 +556,7 @@ const $reversiGamesRepository: Provider = {
 		$channelFavoritesRepository,
 		$registryItemsRepository,
 		$webhooksRepository,
+		$systemWebhooksRepository,
 		$adsRepository,
 		$passwordResetRequestsRepository,
 		$retentionAggregationsRepository,
@@ -520,6 +605,7 @@ const $reversiGamesRepository: Provider = {
 		$swSubscriptionsRepository,
 		$hashtagsRepository,
 		$abuseUserReportsRepository,
+		$abuseReportNotificationRecipientRepository,
 		$registrationTicketsRepository,
 		$authSessionsRepository,
 		$accessTokensRepository,
@@ -541,6 +627,7 @@ const $reversiGamesRepository: Provider = {
 		$channelFavoritesRepository,
 		$registryItemsRepository,
 		$webhooksRepository,
+		$systemWebhooksRepository,
 		$adsRepository,
 		$passwordResetRequestsRepository,
 		$retentionAggregationsRepository,
@@ -553,4 +640,5 @@ const $reversiGamesRepository: Provider = {
 		$reversiGamesRepository,
 	],
 })
-export class RepositoryModule {}
+export class RepositoryModule {
+}
diff --git a/packages/backend/src/models/SystemWebhook.ts b/packages/backend/src/models/SystemWebhook.ts
new file mode 100644
index 0000000000..1a7ce4962b
--- /dev/null
+++ b/packages/backend/src/models/SystemWebhook.ts
@@ -0,0 +1,104 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Column, Entity, Index, PrimaryColumn } from 'typeorm';
+import { Serialized } from '@/types.js';
+import { id } from './util/id.js';
+
+export const systemWebhookEventTypes = [
+	// ユーザからの通報を受けたとき
+	'abuseReport',
+	// 通報を処理したとき
+	'abuseReportResolved',
+	// ユーザが作成された時
+	'userCreated',
+	// モデレータが一定期間不在である警告
+	'inactiveModeratorsWarning',
+	// モデレータが一定期間不在のためシステムにより招待制へと変更された
+	'inactiveModeratorsInvitationOnlyChanged',
+] as const;
+export type SystemWebhookEventType = typeof systemWebhookEventTypes[number];
+
+@Entity('system_webhook')
+export class MiSystemWebhook {
+	@PrimaryColumn(id())
+	public id: string;
+
+	/**
+	 * 有効かどうか.
+	 */
+	@Index('IDX_system_webhook_isActive', { synchronize: false })
+	@Column('boolean', {
+		default: true,
+	})
+	public isActive: boolean;
+
+	/**
+	 * 更新日時.
+	 */
+	@Column('timestamp with time zone', {
+		default: () => 'CURRENT_TIMESTAMP',
+	})
+	public updatedAt: Date;
+
+	/**
+	 * 最後に送信された日時.
+	 */
+	@Column('timestamp with time zone', {
+		nullable: true,
+	})
+	public latestSentAt: Date | null;
+
+	/**
+	 * 最後に送信されたステータスコード
+	 */
+	@Column('integer', {
+		nullable: true,
+	})
+	public latestStatus: number | null;
+
+	/**
+	 * 通知設定名.
+	 */
+	@Column('varchar', {
+		length: 255,
+	})
+	public name: string;
+
+	/**
+	 * イベント種別.
+	 */
+	@Index('IDX_system_webhook_on', { synchronize: false })
+	@Column('varchar', {
+		length: 128,
+		array: true,
+		default: '{}',
+	})
+	public on: SystemWebhookEventType[];
+
+	/**
+	 * Webhook送信先のURL.
+	 */
+	@Column('varchar', {
+		length: 1024,
+	})
+	public url: string;
+
+	/**
+	 * Webhook検証用の値.
+	 */
+	@Column('varchar', {
+		length: 1024,
+	})
+	public secret: string;
+
+	static deserialize(obj: Serialized<MiSystemWebhook>): MiSystemWebhook {
+		return {
+			...obj,
+			updatedAt: new Date(obj.updatedAt),
+			latestSentAt: obj.latestSentAt ? new Date(obj.latestSentAt) : null,
+		};
+	}
+}
diff --git a/packages/backend/src/models/User.ts b/packages/backend/src/models/User.ts
index 9e2d7a3444..96de30c4c2 100644
--- a/packages/backend/src/models/User.ts
+++ b/packages/backend/src/models/User.ts
@@ -155,6 +155,11 @@ export class MiUser {
 	})
 	public tags: string[];
 
+	@Column('integer', {
+		default: 0,
+	})
+	public score: number;
+
 	@Column('boolean', {
 		default: false,
 		comment: 'Whether the User is suspended.',
@@ -197,6 +202,23 @@ export class MiUser {
 	})
 	public isHibernated: boolean;
 
+	@Column('boolean', {
+		default: false,
+	})
+	public requireSigninToViewContents: boolean;
+
+	// in sec, マイナスで相対時間
+	@Column('integer', {
+		nullable: true,
+	})
+	public makeNotesFollowersOnlyBefore: number | null;
+
+	// in sec, マイナスで相対時間
+	@Column('integer', {
+		nullable: true,
+	})
+	public makeNotesHiddenBefore: number | null;
+
 	// アカウントが削除されたかどうかのフラグだが、完全に削除される際は物理削除なので実質削除されるまでの「削除が進行しているかどうか」のフラグ
 	@Column('boolean', {
 		default: false,
@@ -289,5 +311,6 @@ export const localUsernameSchema = { type: 'string', pattern: /^\w{1,20}$/.toStr
 export const passwordSchema = { type: 'string', minLength: 1 } as const;
 export const nameSchema = { type: 'string', minLength: 1, maxLength: 50 } as const;
 export const descriptionSchema = { type: 'string', minLength: 1, maxLength: 1500 } as const;
+export const followedMessageSchema = { type: 'string', minLength: 1, maxLength: 256 } as const;
 export const locationSchema = { type: 'string', minLength: 1, maxLength: 50 } as const;
 export const birthdaySchema = { type: 'string', pattern: /^([0-9]{4})-([0-9]{2})-([0-9]{2})$/.toString().slice(1, -1) } as const;
diff --git a/packages/backend/src/models/UserProfile.ts b/packages/backend/src/models/UserProfile.ts
index 7dbe0b3717..5544555296 100644
--- a/packages/backend/src/models/UserProfile.ts
+++ b/packages/backend/src/models/UserProfile.ts
@@ -42,6 +42,14 @@ export class MiUserProfile {
 	})
 	public description: string | null;
 
+	// フォローされた際のメッセージ
+	@Column('varchar', {
+		length: 256, nullable: true,
+	})
+	public followedMessage: string | null;
+
+	// TODO: 鍵アカウントの場合の、フォローリクエスト受信時のメッセージも設定できるようにする
+
 	@Column('jsonb', {
 		default: [],
 	})
diff --git a/packages/backend/src/models/Webhook.ts b/packages/backend/src/models/Webhook.ts
index db24c03b3d..b4cab4edc8 100644
--- a/packages/backend/src/models/Webhook.ts
+++ b/packages/backend/src/models/Webhook.ts
@@ -8,6 +8,7 @@ import { id } from './util/id.js';
 import { MiUser } from './User.js';
 
 export const webhookEventTypes = ['mention', 'unfollow', 'follow', 'followed', 'note', 'reply', 'renote', 'reaction'] as const;
+export type WebhookEventTypes = typeof webhookEventTypes[number];
 
 @Entity('webhook')
 export class MiWebhook {
diff --git a/packages/backend/src/models/_.ts b/packages/backend/src/models/_.ts
index 2e6a41586e..c72bdaa727 100644
--- a/packages/backend/src/models/_.ts
+++ b/packages/backend/src/models/_.ts
@@ -11,6 +11,7 @@ import { RawSqlResultsToEntityTransformer } from 'typeorm/query-builder/transfor
 import { ObjectUtils } from 'typeorm/util/ObjectUtils.js';
 import { OrmUtils } from 'typeorm/util/OrmUtils.js';
 import { MiAbuseUserReport } from '@/models/AbuseUserReport.js';
+import { MiAbuseReportNotificationRecipient } from '@/models/AbuseReportNotificationRecipient.js';
 import { MiAccessToken } from '@/models/AccessToken.js';
 import { MiAd } from '@/models/Ad.js';
 import { MiAnnouncement } from '@/models/Announcement.js';
@@ -68,6 +69,7 @@ import { MiUserPublickey } from '@/models/UserPublickey.js';
 import { MiUserSecurityKey } from '@/models/UserSecurityKey.js';
 import { MiUserMemo } from '@/models/UserMemo.js';
 import { MiWebhook } from '@/models/Webhook.js';
+import { MiSystemWebhook } from '@/models/SystemWebhook.js';
 import { MiChannel } from '@/models/Channel.js';
 import { MiRetentionAggregation } from '@/models/RetentionAggregation.js';
 import { MiRole } from '@/models/Role.js';
@@ -80,34 +82,14 @@ import { MiReversiGame } from '@/models/ReversiGame.js';
 import type { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity.js';
 
 export interface MiRepository<T extends ObjectLiteral> {
-	createTableColumnNames(this: Repository<T> & MiRepository<T>, queryBuilder: InsertQueryBuilder<T>): string[];
-	createTableColumnNamesWithPrimaryKey(this: Repository<T> & MiRepository<T>, queryBuilder: InsertQueryBuilder<T>): string[];
+	createTableColumnNames(this: Repository<T> & MiRepository<T>): string[];
 	insertOne(this: Repository<T> & MiRepository<T>, entity: QueryDeepPartialEntity<T>, findOptions?: Pick<FindOneOptions<T>, 'relations'>): Promise<T>;
 	selectAliasColumnNames(this: Repository<T> & MiRepository<T>, queryBuilder: InsertQueryBuilder<T>, builder: SelectQueryBuilder<T>): void;
 }
 
 export const miRepository = {
-	createTableColumnNames(queryBuilder) {
-		// @ts-expect-error -- protected
-		const insertedColumns = queryBuilder.getInsertedColumns();
-		if (insertedColumns.length) {
-			return insertedColumns.map(column => column.databaseName);
-		}
-		if (!queryBuilder.expressionMap.mainAlias?.hasMetadata && !queryBuilder.expressionMap.insertColumns.length) {
-			// @ts-expect-error -- protected
-			const valueSets = queryBuilder.getValueSets();
-			if (valueSets.length === 1) {
-				return Object.keys(valueSets[0]);
-			}
-		}
-		return queryBuilder.expressionMap.insertColumns;
-	},
-	createTableColumnNamesWithPrimaryKey(queryBuilder) {
-		const columnNames = this.createTableColumnNames(queryBuilder);
-		if (!columnNames.includes('id')) {
-			columnNames.unshift('id');
-		}
-		return columnNames;
+	createTableColumnNames() {
+		return this.metadata.columns.filter(column => column.isSelect && !column.isVirtual).map(column => column.databaseName);
 	},
 	async insertOne(entity, findOptions?) {
 		const queryBuilder = this.createQueryBuilder().insert().values(entity);
@@ -115,7 +97,7 @@ export const miRepository = {
 		const mainAlias = queryBuilder.expressionMap.mainAlias!;
 		const name = mainAlias.name;
 		mainAlias.name = 't';
-		const columnNames = this.createTableColumnNamesWithPrimaryKey(queryBuilder);
+		const columnNames = this.createTableColumnNames();
 		queryBuilder.returning(columnNames.reduce((a, c) => `${a}, ${queryBuilder.escape(c)}`, '').slice(2));
 		const builder = this.createQueryBuilder().addCommonTableExpression(queryBuilder, 'cte', { columnNames });
 		// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
@@ -136,7 +118,7 @@ export const miRepository = {
 			selectOrAddSelect = (selection, selectionAliasName) => builder.addSelect(selection, selectionAliasName);
 			return builder.select(selection, selectionAliasName);
 		};
-		for (const columnName of this.createTableColumnNamesWithPrimaryKey(queryBuilder)) {
+		for (const columnName of this.createTableColumnNames()) {
 			selectOrAddSelect(`${builder.alias}.${columnName}`, `${builder.alias}_${columnName}`);
 		}
 	},
@@ -144,6 +126,7 @@ export const miRepository = {
 
 export {
 	MiAbuseUserReport,
+	MiAbuseReportNotificationRecipient,
 	MiAccessToken,
 	MiAd,
 	MiAnnouncement,
@@ -201,6 +184,7 @@ export {
 	MiUserPublickey,
 	MiUserSecurityKey,
 	MiWebhook,
+	MiSystemWebhook,
 	MiChannel,
 	MiRetentionAggregation,
 	MiRole,
@@ -213,6 +197,7 @@ export {
 };
 
 export type AbuseUserReportsRepository = Repository<MiAbuseUserReport> & MiRepository<MiAbuseUserReport>;
+export type AbuseReportNotificationRecipientRepository = Repository<MiAbuseReportNotificationRecipient> & MiRepository<MiAbuseReportNotificationRecipient>;
 export type AccessTokensRepository = Repository<MiAccessToken> & MiRepository<MiAccessToken>;
 export type AdsRepository = Repository<MiAd> & MiRepository<MiAd>;
 export type AnnouncementsRepository = Repository<MiAnnouncement> & MiRepository<MiAnnouncement>;
@@ -270,6 +255,7 @@ export type UserProfilesRepository = Repository<MiUserProfile> & MiRepository<Mi
 export type UserPublickeysRepository = Repository<MiUserPublickey> & MiRepository<MiUserPublickey>;
 export type UserSecurityKeysRepository = Repository<MiUserSecurityKey> & MiRepository<MiUserSecurityKey>;
 export type WebhooksRepository = Repository<MiWebhook> & MiRepository<MiWebhook>;
+export type SystemWebhooksRepository = Repository<MiSystemWebhook> & MiRepository<MiWebhook>;
 export type ChannelsRepository = Repository<MiChannel> & MiRepository<MiChannel>;
 export type RetentionAggregationsRepository = Repository<MiRetentionAggregation> & MiRepository<MiRetentionAggregation>;
 export type RolesRepository = Repository<MiRole> & MiRepository<MiRole>;
diff --git a/packages/backend/src/models/json-schema/abuse-report-notification-recipient.ts b/packages/backend/src/models/json-schema/abuse-report-notification-recipient.ts
new file mode 100644
index 0000000000..6215f0f5a2
--- /dev/null
+++ b/packages/backend/src/models/json-schema/abuse-report-notification-recipient.ts
@@ -0,0 +1,50 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+export const packedAbuseReportNotificationRecipientSchema = {
+	type: 'object',
+	properties: {
+		id: {
+			type: 'string',
+			optional: false, nullable: false,
+		},
+		isActive: {
+			type: 'boolean',
+			optional: false, nullable: false,
+		},
+		updatedAt: {
+			type: 'string',
+			format: 'date-time',
+			optional: false, nullable: false,
+		},
+		name: {
+			type: 'string',
+			optional: false, nullable: false,
+		},
+		method: {
+			type: 'string',
+			optional: false, nullable: false,
+			enum: ['email', 'webhook'],
+		},
+		userId: {
+			type: 'string',
+			optional: true, nullable: false,
+		},
+		user: {
+			type: 'object',
+			optional: true, nullable: false,
+			ref: 'UserLite',
+		},
+		systemWebhookId: {
+			type: 'string',
+			optional: true, nullable: false,
+		},
+		systemWebhook: {
+			type: 'object',
+			optional: true, nullable: false,
+			ref: 'SystemWebhook',
+		},
+	},
+} as const;
diff --git a/packages/backend/src/models/json-schema/drive-file.ts b/packages/backend/src/models/json-schema/drive-file.ts
index ca88cc0e39..5ee1561c50 100644
--- a/packages/backend/src/models/json-schema/drive-file.ts
+++ b/packages/backend/src/models/json-schema/drive-file.ts
@@ -20,7 +20,7 @@ export const packedDriveFileSchema = {
 		name: {
 			type: 'string',
 			optional: false, nullable: false,
-			example: 'lenna.jpg',
+			example: '192.jpg',
 		},
 		type: {
 			type: 'string',
diff --git a/packages/backend/src/models/json-schema/federation-instance.ts b/packages/backend/src/models/json-schema/federation-instance.ts
index ed40d405c6..912a0399d8 100644
--- a/packages/backend/src/models/json-schema/federation-instance.ts
+++ b/packages/backend/src/models/json-schema/federation-instance.ts
@@ -88,6 +88,10 @@ export const packedFederationInstanceSchema = {
 			type: 'boolean',
 			optional: false, nullable: false,
 		},
+		isMediaSilenced: {
+			type: 'boolean',
+			optional: false, nullable: false,
+		},
 		iconUrl: {
 			type: 'string',
 			optional: false, nullable: true,
diff --git a/packages/backend/src/models/json-schema/flash.ts b/packages/backend/src/models/json-schema/flash.ts
index 952df649ad..42b2172409 100644
--- a/packages/backend/src/models/json-schema/flash.ts
+++ b/packages/backend/src/models/json-schema/flash.ts
@@ -44,6 +44,11 @@ export const packedFlashSchema = {
 			type: 'string',
 			optional: false, nullable: false,
 		},
+		visibility: {
+			type: 'string',
+			optional: false, nullable: false,
+			enum: ['private', 'public'],
+		},
 		likedCount: {
 			type: 'number',
 			optional: false, nullable: true,
diff --git a/packages/backend/src/models/json-schema/meta.ts b/packages/backend/src/models/json-schema/meta.ts
index e7bc6356e5..e3fd63464a 100644
--- a/packages/backend/src/models/json-schema/meta.ts
+++ b/packages/backend/src/models/json-schema/meta.ts
@@ -115,6 +115,10 @@ export const packedMetaLiteSchema = {
 			type: 'string',
 			optional: false, nullable: true,
 		},
+		enableTestcaptcha: {
+			type: 'boolean',
+			optional: false, nullable: false,
+		},
 		swPublickey: {
 			type: 'string',
 			optional: false, nullable: true,
@@ -247,6 +251,16 @@ export const packedMetaLiteSchema = {
 			optional: false, nullable: false,
 			ref: 'RolePolicies',
 		},
+		noteSearchableScope: {
+			type: 'string',
+			enum: ['local', 'global'],
+			optional: false, nullable: false,
+			default: 'local',
+		},
+		maxFileSize: {
+			type: 'number',
+			optional: false, nullable: false,
+		},
 	},
 } as const;
 
diff --git a/packages/backend/src/models/json-schema/note.ts b/packages/backend/src/models/json-schema/note.ts
index 2641161c8b..432c096e48 100644
--- a/packages/backend/src/models/json-schema/note.ts
+++ b/packages/backend/src/models/json-schema/note.ts
@@ -204,6 +204,7 @@ export const packedNoteSchema = {
 		reactionAcceptance: {
 			type: 'string',
 			optional: false, nullable: true,
+			enum: ['likeOnly', 'likeOnlyForRemote', 'nonSensitiveOnly', 'nonSensitiveOnlyForLocalLikeOnlyForRemote', null],
 		},
 		reactionEmojis: {
 			type: 'object',
diff --git a/packages/backend/src/models/json-schema/notification.ts b/packages/backend/src/models/json-schema/notification.ts
index b4c4442758..cddaf4bc83 100644
--- a/packages/backend/src/models/json-schema/notification.ts
+++ b/packages/backend/src/models/json-schema/notification.ts
@@ -3,7 +3,8 @@
  * SPDX-License-Identifier: AGPL-3.0-only
  */
 
-import { notificationTypes } from '@/types.js';
+import { ACHIEVEMENT_TYPES } from '@/core/AchievementService.js';
+import { notificationTypes, userExportableEntities } from '@/types.js';
 
 const baseSchema = {
 	type: 'object',
@@ -266,6 +267,10 @@ export const packedNotificationSchema = {
 				optional: false, nullable: false,
 				format: 'id',
 			},
+			message: {
+				type: 'string',
+				optional: false, nullable: true,
+			},
 		},
 	}, {
 		type: 'object',
@@ -294,6 +299,37 @@ export const packedNotificationSchema = {
 			achievement: {
 				type: 'string',
 				optional: false, nullable: false,
+				enum: ACHIEVEMENT_TYPES,
+			},
+		},
+	}, {
+		type: 'object',
+		properties: {
+			...baseSchema.properties,
+			type: {
+				type: 'string',
+				optional: false, nullable: false,
+				enum: ['exportCompleted'],
+			},
+			exportedEntity: {
+				type: 'string',
+				optional: false, nullable: false,
+				enum: userExportableEntities,
+			},
+			fileId: {
+				type: 'string',
+				optional: false, nullable: false,
+				format: 'id',
+			},
+		},
+	}, {
+		type: 'object',
+		properties: {
+			...baseSchema.properties,
+			type: {
+				type: 'string',
+				optional: false, nullable: false,
+				enum: ['login'],
 			},
 		},
 	}, {
@@ -311,11 +347,11 @@ export const packedNotificationSchema = {
 			},
 			header: {
 				type: 'string',
-				optional: false, nullable: false,
+				optional: false, nullable: true,
 			},
 			icon: {
 				type: 'string',
-				optional: false, nullable: false,
+				optional: false, nullable: true,
 			},
 		},
 	}, {
diff --git a/packages/backend/src/models/json-schema/role.ts b/packages/backend/src/models/json-schema/role.ts
index d9987a70c3..3537de94c8 100644
--- a/packages/backend/src/models/json-schema/role.ts
+++ b/packages/backend/src/models/json-schema/role.ts
@@ -228,6 +228,10 @@ export const packedRolePoliciesSchema = {
 			type: 'boolean',
 			optional: false, nullable: false,
 		},
+		canUpdateBioMedia: {
+			type: 'boolean',
+			optional: false, nullable: false,
+		},
 		pinLimit: {
 			type: 'integer',
 			optional: false, nullable: false,
@@ -268,6 +272,26 @@ export const packedRolePoliciesSchema = {
 			type: 'integer',
 			optional: false, nullable: false,
 		},
+		canImportAntennas: {
+			type: 'boolean',
+			optional: false, nullable: false,
+		},
+		canImportBlocking: {
+			type: 'boolean',
+			optional: false, nullable: false,
+		},
+		canImportFollowing: {
+			type: 'boolean',
+			optional: false, nullable: false,
+		},
+		canImportMuting: {
+			type: 'boolean',
+			optional: false, nullable: false,
+		},
+		canImportUserLists: {
+			type: 'boolean',
+			optional: false, nullable: false,
+		},
 	},
 } as const;
 
diff --git a/packages/backend/src/models/json-schema/system-webhook.ts b/packages/backend/src/models/json-schema/system-webhook.ts
new file mode 100644
index 0000000000..d83065a743
--- /dev/null
+++ b/packages/backend/src/models/json-schema/system-webhook.ts
@@ -0,0 +1,54 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { systemWebhookEventTypes } from '@/models/SystemWebhook.js';
+
+export const packedSystemWebhookSchema = {
+	type: 'object',
+	properties: {
+		id: {
+			type: 'string',
+			optional: false, nullable: false,
+		},
+		isActive: {
+			type: 'boolean',
+			optional: false, nullable: false,
+		},
+		updatedAt: {
+			type: 'string',
+			format: 'date-time',
+			optional: false, nullable: false,
+		},
+		latestSentAt: {
+			type: 'string',
+			format: 'date-time',
+			optional: false, nullable: true,
+		},
+		latestStatus: {
+			type: 'number',
+			optional: false, nullable: true,
+		},
+		name: {
+			type: 'string',
+			optional: false, nullable: false,
+		},
+		on: {
+			type: 'array',
+			items: {
+				type: 'string',
+				optional: false, nullable: false,
+				enum: systemWebhookEventTypes,
+			},
+		},
+		url: {
+			type: 'string',
+			optional: false, nullable: false,
+		},
+		secret: {
+			type: 'string',
+			optional: false, nullable: false,
+		},
+	},
+} as const;
diff --git a/packages/backend/src/models/json-schema/user.ts b/packages/backend/src/models/json-schema/user.ts
index 947a9317d7..38631f907d 100644
--- a/packages/backend/src/models/json-schema/user.ts
+++ b/packages/backend/src/models/json-schema/user.ts
@@ -115,6 +115,18 @@ export const packedUserLiteSchema = {
 			type: 'boolean',
 			nullable: false, optional: true,
 		},
+		requireSigninToViewContents: {
+			type: 'boolean',
+			nullable: false, optional: true,
+		},
+		makeNotesFollowersOnlyBefore: {
+			type: 'number',
+			nullable: true, optional: true,
+		},
+		makeNotesHiddenBefore: {
+			type: 'number',
+			nullable: true, optional: true,
+		},
 		instance: {
 			type: 'object',
 			nullable: false, optional: true,
@@ -346,21 +358,6 @@ export const packedUserDetailedNotMeOnlySchema = {
 			nullable: false, optional: false,
 			enum: ['public', 'followers', 'private'],
 		},
-		twoFactorEnabled: {
-			type: 'boolean',
-			nullable: false, optional: false,
-			default: false,
-		},
-		usePasswordLessLogin: {
-			type: 'boolean',
-			nullable: false, optional: false,
-			default: false,
-		},
-		securityKeys: {
-			type: 'boolean',
-			nullable: false, optional: false,
-			default: false,
-		},
 		roles: {
 			type: 'array',
 			nullable: false, optional: false,
@@ -370,6 +367,10 @@ export const packedUserDetailedNotMeOnlySchema = {
 				ref: 'RoleLite',
 			},
 		},
+		followedMessage: {
+			type: 'string',
+			nullable: true, optional: true,
+		},
 		memo: {
 			type: 'string',
 			nullable: true, optional: false,
@@ -378,6 +379,18 @@ export const packedUserDetailedNotMeOnlySchema = {
 			type: 'string',
 			nullable: false, optional: true,
 		},
+		twoFactorEnabled: {
+			type: 'boolean',
+			nullable: false, optional: true,
+		},
+		usePasswordLessLogin: {
+			type: 'boolean',
+			nullable: false, optional: true,
+		},
+		securityKeys: {
+			type: 'boolean',
+			nullable: false, optional: true,
+		},
 		//#region relations
 		isFollowing: {
 			type: 'boolean',
@@ -437,6 +450,10 @@ export const packedMeDetailedOnlySchema = {
 			nullable: true, optional: false,
 			format: 'id',
 		},
+		followedMessage: {
+			type: 'string',
+			nullable: true, optional: false,
+		},
 		isModerator: {
 			type: 'boolean',
 			nullable: true, optional: false,
@@ -622,6 +639,21 @@ export const packedMeDetailedOnlySchema = {
 			nullable: false, optional: false,
 			ref: 'RolePolicies',
 		},
+		twoFactorEnabled: {
+			type: 'boolean',
+			nullable: false, optional: false,
+			default: false,
+		},
+		usePasswordLessLogin: {
+			type: 'boolean',
+			nullable: false, optional: false,
+			default: false,
+		},
+		securityKeys: {
+			type: 'boolean',
+			nullable: false, optional: false,
+			default: false,
+		},
 		//#region secrets
 		email: {
 			type: 'string',
diff --git a/packages/backend/src/postgres.ts b/packages/backend/src/postgres.ts
index 2d14537bbb..251a03c303 100644
--- a/packages/backend/src/postgres.ts
+++ b/packages/backend/src/postgres.ts
@@ -5,13 +5,12 @@
 
 // https://github.com/typeorm/typeorm/issues/2400
 import pg from 'pg';
-pg.types.setTypeParser(20, Number);
-
 import { DataSource, Logger } from 'typeorm';
 import * as highlight from 'cli-highlight';
 import { entities as charts } from '@/core/chart/entities.js';
 
 import { MiAbuseUserReport } from '@/models/AbuseUserReport.js';
+import { MiAbuseReportNotificationRecipient } from '@/models/AbuseReportNotificationRecipient.js';
 import { MiAccessToken } from '@/models/AccessToken.js';
 import { MiAd } from '@/models/Ad.js';
 import { MiAnnouncement } from '@/models/Announcement.js';
@@ -69,6 +68,7 @@ import { MiUserProfile } from '@/models/UserProfile.js';
 import { MiUserPublickey } from '@/models/UserPublickey.js';
 import { MiUserSecurityKey } from '@/models/UserSecurityKey.js';
 import { MiWebhook } from '@/models/Webhook.js';
+import { MiSystemWebhook } from '@/models/SystemWebhook.js';
 import { MiChannel } from '@/models/Channel.js';
 import { MiRetentionAggregation } from '@/models/RetentionAggregation.js';
 import { MiRole } from '@/models/Role.js';
@@ -83,9 +83,11 @@ import { Config } from '@/config.js';
 import MisskeyLogger from '@/logger.js';
 import { bindThis } from '@/decorators.js';
 
+pg.types.setTypeParser(20, Number);
+
 export const dbLogger = new MisskeyLogger('db');
 
-const sqlLogger = dbLogger.createSubLogger('sql', 'gray', false);
+const sqlLogger = dbLogger.createSubLogger('sql', 'gray');
 
 class MyCustomLogger implements Logger {
 	@bindThis
@@ -167,6 +169,7 @@ export const entities = [
 	MiHashtag,
 	MiSwSubscription,
 	MiAbuseUserReport,
+	MiAbuseReportNotificationRecipient,
 	MiRegistrationTicket,
 	MiSignin,
 	MiModerationLog,
@@ -185,6 +188,7 @@ export const entities = [
 	MiPasswordResetRequest,
 	MiUserPending,
 	MiWebhook,
+	MiSystemWebhook,
 	MiUserIp,
 	MiRetentionAggregation,
 	MiRole,
diff --git a/packages/backend/src/queue/QueueProcessorModule.ts b/packages/backend/src/queue/QueueProcessorModule.ts
index 8086158997..9044285bf6 100644
--- a/packages/backend/src/queue/QueueProcessorModule.ts
+++ b/packages/backend/src/queue/QueueProcessorModule.ts
@@ -6,13 +6,16 @@
 import { Module } from '@nestjs/common';
 import { CoreModule } from '@/core/CoreModule.js';
 import { GlobalModule } from '@/GlobalModule.js';
+import { CheckModeratorsActivityProcessorService } from '@/queue/processors/CheckModeratorsActivityProcessorService.js';
 import { QueueLoggerService } from './QueueLoggerService.js';
 import { QueueProcessorService } from './QueueProcessorService.js';
 import { DeliverProcessorService } from './processors/DeliverProcessorService.js';
 import { EndedPollNotificationProcessorService } from './processors/EndedPollNotificationProcessorService.js';
 import { InboxProcessorService } from './processors/InboxProcessorService.js';
-import { WebhookDeliverProcessorService } from './processors/WebhookDeliverProcessorService.js';
+import { UserWebhookDeliverProcessorService } from './processors/UserWebhookDeliverProcessorService.js';
+import { SystemWebhookDeliverProcessorService } from './processors/SystemWebhookDeliverProcessorService.js';
 import { CheckExpiredMutingsProcessorService } from './processors/CheckExpiredMutingsProcessorService.js';
+import { BakeBufferedReactionsProcessorService } from './processors/BakeBufferedReactionsProcessorService.js';
 import { CleanChartsProcessorService } from './processors/CleanChartsProcessorService.js';
 import { CleanProcessorService } from './processors/CleanProcessorService.js';
 import { CleanRemoteFilesProcessorService } from './processors/CleanRemoteFilesProcessorService.js';
@@ -50,6 +53,7 @@ import { RelationshipProcessorService } from './processors/RelationshipProcessor
 		ResyncChartsProcessorService,
 		CleanChartsProcessorService,
 		CheckExpiredMutingsProcessorService,
+		BakeBufferedReactionsProcessorService,
 		CleanProcessorService,
 		DeleteDriveFilesProcessorService,
 		ExportCustomEmojisProcessorService,
@@ -71,11 +75,14 @@ import { RelationshipProcessorService } from './processors/RelationshipProcessor
 		DeleteFileProcessorService,
 		CleanRemoteFilesProcessorService,
 		RelationshipProcessorService,
-		WebhookDeliverProcessorService,
+		UserWebhookDeliverProcessorService,
+		SystemWebhookDeliverProcessorService,
 		EndedPollNotificationProcessorService,
 		DeliverProcessorService,
 		InboxProcessorService,
 		AggregateRetentionProcessorService,
+		CheckExpiredMutingsProcessorService,
+		CheckModeratorsActivityProcessorService,
 		QueueProcessorService,
 	],
 	exports: [
diff --git a/packages/backend/src/queue/QueueProcessorService.ts b/packages/backend/src/queue/QueueProcessorService.ts
index ce999d9cef..6940e1c188 100644
--- a/packages/backend/src/queue/QueueProcessorService.ts
+++ b/packages/backend/src/queue/QueueProcessorService.ts
@@ -5,11 +5,14 @@
 
 import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
 import * as Bull from 'bullmq';
+import * as Sentry from '@sentry/node';
 import type { Config } from '@/config.js';
 import { DI } from '@/di-symbols.js';
 import type Logger from '@/logger.js';
 import { bindThis } from '@/decorators.js';
-import { WebhookDeliverProcessorService } from './processors/WebhookDeliverProcessorService.js';
+import { CheckModeratorsActivityProcessorService } from '@/queue/processors/CheckModeratorsActivityProcessorService.js';
+import { UserWebhookDeliverProcessorService } from './processors/UserWebhookDeliverProcessorService.js';
+import { SystemWebhookDeliverProcessorService } from './processors/SystemWebhookDeliverProcessorService.js';
 import { EndedPollNotificationProcessorService } from './processors/EndedPollNotificationProcessorService.js';
 import { DeliverProcessorService } from './processors/DeliverProcessorService.js';
 import { InboxProcessorService } from './processors/InboxProcessorService.js';
@@ -37,6 +40,7 @@ import { TickChartsProcessorService } from './processors/TickChartsProcessorServ
 import { ResyncChartsProcessorService } from './processors/ResyncChartsProcessorService.js';
 import { CleanChartsProcessorService } from './processors/CleanChartsProcessorService.js';
 import { CheckExpiredMutingsProcessorService } from './processors/CheckExpiredMutingsProcessorService.js';
+import { BakeBufferedReactionsProcessorService } from './processors/BakeBufferedReactionsProcessorService.js';
 import { CleanProcessorService } from './processors/CleanProcessorService.js';
 import { AggregateRetentionProcessorService } from './processors/AggregateRetentionProcessorService.js';
 import { QueueLoggerService } from './QueueLoggerService.js';
@@ -63,7 +67,7 @@ function getJobInfo(job: Bull.Job | undefined, increment = false): string {
 
 	// onActiveとかonCompletedのattemptsMadeがなぜか0始まりなのでインクリメントする
 	const currentAttempts = job.attemptsMade + (increment ? 1 : 0);
-	const maxAttempts = job.opts ? job.opts.attempts : 0;
+	const maxAttempts = job.opts.attempts ?? 0;
 
 	return `id=${job.id} attempts=${currentAttempts}/${maxAttempts} age=${formated}`;
 }
@@ -75,7 +79,8 @@ export class QueueProcessorService implements OnApplicationShutdown {
 	private dbQueueWorker: Bull.Worker;
 	private deliverQueueWorker: Bull.Worker;
 	private inboxQueueWorker: Bull.Worker;
-	private webhookDeliverQueueWorker: Bull.Worker;
+	private userWebhookDeliverQueueWorker: Bull.Worker;
+	private systemWebhookDeliverQueueWorker: Bull.Worker;
 	private relationshipQueueWorker: Bull.Worker;
 	private objectStorageQueueWorker: Bull.Worker;
 	private endedPollNotificationQueueWorker: Bull.Worker;
@@ -85,7 +90,8 @@ export class QueueProcessorService implements OnApplicationShutdown {
 		private config: Config,
 
 		private queueLoggerService: QueueLoggerService,
-		private webhookDeliverProcessorService: WebhookDeliverProcessorService,
+		private userWebhookDeliverProcessorService: UserWebhookDeliverProcessorService,
+		private systemWebhookDeliverProcessorService: SystemWebhookDeliverProcessorService,
 		private endedPollNotificationProcessorService: EndedPollNotificationProcessorService,
 		private deliverProcessorService: DeliverProcessorService,
 		private inboxProcessorService: InboxProcessorService,
@@ -114,220 +120,402 @@ export class QueueProcessorService implements OnApplicationShutdown {
 		private cleanChartsProcessorService: CleanChartsProcessorService,
 		private aggregateRetentionProcessorService: AggregateRetentionProcessorService,
 		private checkExpiredMutingsProcessorService: CheckExpiredMutingsProcessorService,
+		private bakeBufferedReactionsProcessorService: BakeBufferedReactionsProcessorService,
+		private checkModeratorsActivityProcessorService: CheckModeratorsActivityProcessorService,
 		private cleanProcessorService: CleanProcessorService,
 	) {
 		this.logger = this.queueLoggerService.logger;
 
-		function renderError(e: Error): any {
-			if (e) { // 何故かeがundefinedで来ることがある
-				return {
-					stack: e.stack,
-					message: e.message,
-					name: e.name,
-				};
-			} else {
-				return {
-					stack: '?',
-					message: '?',
-					name: '?',
-				};
+		function renderError(e?: Error) {
+			// 何故かeがundefinedで来ることがある
+			if (!e) return '?';
+
+			if (e instanceof Bull.UnrecoverableError || e.name === 'AbortError') {
+				return `${e.name}: ${e.message}`;
 			}
+
+			return {
+				stack: e.stack,
+				message: e.message,
+				name: e.name,
+			};
+		}
+
+		function renderJob(job?: Bull.Job) {
+			if (!job) return '?';
+
+			return {
+				name: job.name || undefined,
+				info: getJobInfo(job),
+				failedReason: job.failedReason || undefined,
+				data: job.data,
+			};
 		}
 
 		//#region system
-		this.systemQueueWorker = new Bull.Worker(QUEUE.SYSTEM, (job) => {
-			switch (job.name) {
-				case 'tickCharts': return this.tickChartsProcessorService.process();
-				case 'resyncCharts': return this.resyncChartsProcessorService.process();
-				case 'cleanCharts': return this.cleanChartsProcessorService.process();
-				case 'aggregateRetention': return this.aggregateRetentionProcessorService.process();
-				case 'checkExpiredMutings': return this.checkExpiredMutingsProcessorService.process();
-				case 'clean': return this.cleanProcessorService.process();
-				default: throw new Error(`unrecognized job type ${job.name} for system`);
-			}
-		}, {
-			...baseQueueOptions(this.config, QUEUE.SYSTEM),
-			autorun: false,
-		});
+		{
+			const processer = (job: Bull.Job) => {
+				switch (job.name) {
+					case 'tickCharts': return this.tickChartsProcessorService.process();
+					case 'resyncCharts': return this.resyncChartsProcessorService.process();
+					case 'cleanCharts': return this.cleanChartsProcessorService.process();
+					case 'aggregateRetention': return this.aggregateRetentionProcessorService.process();
+					case 'checkExpiredMutings': return this.checkExpiredMutingsProcessorService.process();
+					case 'bakeBufferedReactions': return this.bakeBufferedReactionsProcessorService.process();
+					case 'checkModeratorsActivity': return this.checkModeratorsActivityProcessorService.process();
+					case 'clean': return this.cleanProcessorService.process();
+					default: throw new Error(`unrecognized job type ${job.name} for system`);
+				}
+			};
 
-		const systemLogger = this.logger.createSubLogger('system');
+			this.systemQueueWorker = new Bull.Worker(QUEUE.SYSTEM, (job) => {
+				if (this.config.sentryForBackend) {
+					return Sentry.startSpan({ name: 'Queue: System: ' + job.name }, () => processer(job));
+				} else {
+					return processer(job);
+				}
+			}, {
+				...baseQueueOptions(this.config, QUEUE.SYSTEM),
+				autorun: false,
+			});
 
-		this.systemQueueWorker
-			.on('active', (job) => systemLogger.debug(`active id=${job.id}`))
-			.on('completed', (job, result) => systemLogger.debug(`completed(${result}) id=${job.id}`))
-			.on('failed', (job, err) => systemLogger.warn(`failed(${err.stack}) id=${job ? job.id : '-'}`, { job, e: renderError(err) }))
-			.on('error', (err: Error) => systemLogger.error(`error ${err.stack}`, { e: renderError(err) }))
-			.on('stalled', (jobId) => systemLogger.warn(`stalled id=${jobId}`));
+			const logger = this.logger.createSubLogger('system');
+
+			this.systemQueueWorker
+				.on('active', (job) => logger.debug(`active id=${job.id}`))
+				.on('completed', (job, result) => logger.debug(`completed(${result}) id=${job.id}`))
+				.on('failed', (job, err: Error) => {
+					logger.error(`failed(${err.name}: ${err.message}) id=${job?.id ?? '?'}`, { job: renderJob(job), e: renderError(err) });
+					if (config.sentryForBackend) {
+						Sentry.captureMessage(`Queue: System: ${job?.name ?? '?'}: ${err.name}: ${err.message}`, {
+							level: 'error',
+							extra: { job, err },
+						});
+					}
+				})
+				.on('error', (err: Error) => logger.error(`error ${err.name}: ${err.message}`, { e: renderError(err) }))
+				.on('stalled', (jobId) => logger.warn(`stalled id=${jobId}`));
+		}
 		//#endregion
 
 		//#region db
-		this.dbQueueWorker = new Bull.Worker(QUEUE.DB, (job) => {
-			switch (job.name) {
-				case 'deleteDriveFiles': return this.deleteDriveFilesProcessorService.process(job);
-				case 'exportCustomEmojis': return this.exportCustomEmojisProcessorService.process(job);
-				case 'exportNotes': return this.exportNotesProcessorService.process(job);
-				case 'exportClips': return this.exportClipsProcessorService.process(job);
-				case 'exportFavorites': return this.exportFavoritesProcessorService.process(job);
-				case 'exportFollowing': return this.exportFollowingProcessorService.process(job);
-				case 'exportMuting': return this.exportMutingProcessorService.process(job);
-				case 'exportBlocking': return this.exportBlockingProcessorService.process(job);
-				case 'exportUserLists': return this.exportUserListsProcessorService.process(job);
-				case 'exportAntennas': return this.exportAntennasProcessorService.process(job);
-				case 'importFollowing': return this.importFollowingProcessorService.process(job);
-				case 'importFollowingToDb': return this.importFollowingProcessorService.processDb(job);
-				case 'importMuting': return this.importMutingProcessorService.process(job);
-				case 'importBlocking': return this.importBlockingProcessorService.process(job);
-				case 'importBlockingToDb': return this.importBlockingProcessorService.processDb(job);
-				case 'importUserLists': return this.importUserListsProcessorService.process(job);
-				case 'importCustomEmojis': return this.importCustomEmojisProcessorService.process(job);
-				case 'importAntennas': return this.importAntennasProcessorService.process(job);
-				case 'deleteAccount': return this.deleteAccountProcessorService.process(job);
-				default: throw new Error(`unrecognized job type ${job.name} for db`);
-			}
-		}, {
-			...baseQueueOptions(this.config, QUEUE.DB),
-			autorun: false,
-		});
+		{
+			const processer = (job: Bull.Job) => {
+				switch (job.name) {
+					case 'deleteDriveFiles': return this.deleteDriveFilesProcessorService.process(job);
+					case 'exportCustomEmojis': return this.exportCustomEmojisProcessorService.process(job);
+					case 'exportNotes': return this.exportNotesProcessorService.process(job);
+					case 'exportClips': return this.exportClipsProcessorService.process(job);
+					case 'exportFavorites': return this.exportFavoritesProcessorService.process(job);
+					case 'exportFollowing': return this.exportFollowingProcessorService.process(job);
+					case 'exportMuting': return this.exportMutingProcessorService.process(job);
+					case 'exportBlocking': return this.exportBlockingProcessorService.process(job);
+					case 'exportUserLists': return this.exportUserListsProcessorService.process(job);
+					case 'exportAntennas': return this.exportAntennasProcessorService.process(job);
+					case 'importFollowing': return this.importFollowingProcessorService.process(job);
+					case 'importFollowingToDb': return this.importFollowingProcessorService.processDb(job);
+					case 'importMuting': return this.importMutingProcessorService.process(job);
+					case 'importBlocking': return this.importBlockingProcessorService.process(job);
+					case 'importBlockingToDb': return this.importBlockingProcessorService.processDb(job);
+					case 'importUserLists': return this.importUserListsProcessorService.process(job);
+					case 'importCustomEmojis': return this.importCustomEmojisProcessorService.process(job);
+					case 'importAntennas': return this.importAntennasProcessorService.process(job);
+					case 'deleteAccount': return this.deleteAccountProcessorService.process(job);
+					default: throw new Error(`unrecognized job type ${job.name} for db`);
+				}
+			};
 
-		const dbLogger = this.logger.createSubLogger('db');
+			this.dbQueueWorker = new Bull.Worker(QUEUE.DB, (job) => {
+				if (this.config.sentryForBackend) {
+					return Sentry.startSpan({ name: 'Queue: DB: ' + job.name }, () => processer(job));
+				} else {
+					return processer(job);
+				}
+			}, {
+				...baseQueueOptions(this.config, QUEUE.DB),
+				autorun: false,
+			});
 
-		this.dbQueueWorker
-			.on('active', (job) => dbLogger.debug(`active id=${job.id}`))
-			.on('completed', (job, result) => dbLogger.debug(`completed(${result}) id=${job.id}`))
-			.on('failed', (job, err) => dbLogger.warn(`failed(${err.stack}) id=${job ? job.id : '-'}`, { job, e: renderError(err) }))
-			.on('error', (err: Error) => dbLogger.error(`error ${err.stack}`, { e: renderError(err) }))
-			.on('stalled', (jobId) => dbLogger.warn(`stalled id=${jobId}`));
+			const logger = this.logger.createSubLogger('db');
+
+			this.dbQueueWorker
+				.on('active', (job) => logger.debug(`active id=${job.id}`))
+				.on('completed', (job, result) => logger.debug(`completed(${result}) id=${job.id}`))
+				.on('failed', (job, err) => {
+					logger.error(`failed(${err.name}: ${err.message}) id=${job?.id ?? '?'}`, { job: renderJob(job), e: renderError(err) });
+					if (config.sentryForBackend) {
+						Sentry.captureMessage(`Queue: DB: ${job?.name ?? '?'}: ${err.name}: ${err.message}`, {
+							level: 'error',
+							extra: { job, err },
+						});
+					}
+				})
+				.on('error', (err: Error) => logger.error(`error ${err.name}: ${err.message}`, { e: renderError(err) }))
+				.on('stalled', (jobId) => logger.warn(`stalled id=${jobId}`));
+		}
 		//#endregion
 
 		//#region deliver
-		this.deliverQueueWorker = new Bull.Worker(QUEUE.DELIVER, (job) => this.deliverProcessorService.process(job), {
-			...baseQueueOptions(this.config, QUEUE.DELIVER),
-			autorun: false,
-			concurrency: this.config.deliverJobConcurrency ?? 128,
-			limiter: {
-				max: this.config.deliverJobPerSec ?? 128,
-				duration: 1000,
-			},
-			settings: {
-				backoffStrategy: httpRelatedBackoff,
-			},
-		});
+		{
+			this.deliverQueueWorker = new Bull.Worker(QUEUE.DELIVER, (job) => {
+				if (this.config.sentryForBackend) {
+					return Sentry.startSpan({ name: 'Queue: Deliver' }, () => this.deliverProcessorService.process(job));
+				} else {
+					return this.deliverProcessorService.process(job);
+				}
+			}, {
+				...baseQueueOptions(this.config, QUEUE.DELIVER),
+				autorun: false,
+				concurrency: this.config.deliverJobConcurrency ?? 128,
+				limiter: {
+					max: this.config.deliverJobPerSec ?? 128,
+					duration: 1000,
+				},
+				settings: {
+					backoffStrategy: httpRelatedBackoff,
+				},
+			});
 
-		const deliverLogger = this.logger.createSubLogger('deliver');
+			const logger = this.logger.createSubLogger('deliver');
 
-		this.deliverQueueWorker
-			.on('active', (job) => deliverLogger.debug(`active ${getJobInfo(job, true)} to=${job.data.to}`))
-			.on('completed', (job, result) => deliverLogger.debug(`completed(${result}) ${getJobInfo(job, true)} to=${job.data.to}`))
-			.on('failed', (job, err) => deliverLogger.warn(`failed(${err.stack}) ${getJobInfo(job)} to=${job ? job.data.to : '-'}`))
-			.on('error', (err: Error) => deliverLogger.error(`error ${err.stack}`, { e: renderError(err) }))
-			.on('stalled', (jobId) => deliverLogger.warn(`stalled id=${jobId}`));
+			this.deliverQueueWorker
+				.on('active', (job) => logger.debug(`active ${getJobInfo(job, true)} to=${job.data.to}`))
+				.on('completed', (job, result) => logger.debug(`completed(${result}) ${getJobInfo(job, true)} to=${job.data.to}`))
+				.on('failed', (job, err) => {
+					logger.error(`failed(${err.name}: ${err.message}) ${getJobInfo(job)} to=${job ? job.data.to : '-'}`);
+					if (config.sentryForBackend) {
+						Sentry.captureMessage(`Queue: Deliver: ${err.name}: ${err.message}`, {
+							level: 'error',
+							extra: { job, err },
+						});
+					}
+				})
+				.on('error', (err: Error) => logger.error(`error ${err.name}: ${err.message}`, { e: renderError(err) }))
+				.on('stalled', (jobId) => logger.warn(`stalled id=${jobId}`));
+		}
 		//#endregion
 
 		//#region inbox
-		this.inboxQueueWorker = new Bull.Worker(QUEUE.INBOX, (job) => this.inboxProcessorService.process(job), {
-			...baseQueueOptions(this.config, QUEUE.INBOX),
-			autorun: false,
-			concurrency: this.config.inboxJobConcurrency ?? 16,
-			limiter: {
-				max: this.config.inboxJobPerSec ?? 32,
-				duration: 1000,
-			},
-			settings: {
-				backoffStrategy: httpRelatedBackoff,
-			},
-		});
+		{
+			this.inboxQueueWorker = new Bull.Worker(QUEUE.INBOX, (job) => {
+				if (this.config.sentryForBackend) {
+					return Sentry.startSpan({ name: 'Queue: Inbox' }, () => this.inboxProcessorService.process(job));
+				} else {
+					return this.inboxProcessorService.process(job);
+				}
+			}, {
+				...baseQueueOptions(this.config, QUEUE.INBOX),
+				autorun: false,
+				concurrency: this.config.inboxJobConcurrency ?? 16,
+				limiter: {
+					max: this.config.inboxJobPerSec ?? 32,
+					duration: 1000,
+				},
+				settings: {
+					backoffStrategy: httpRelatedBackoff,
+				},
+			});
 
-		const inboxLogger = this.logger.createSubLogger('inbox');
+			const logger = this.logger.createSubLogger('inbox');
 
-		this.inboxQueueWorker
-			.on('active', (job) => inboxLogger.debug(`active ${getJobInfo(job, true)}`))
-			.on('completed', (job, result) => inboxLogger.debug(`completed(${result}) ${getJobInfo(job, true)}`))
-			.on('failed', (job, err) => inboxLogger.warn(`failed(${err.stack}) ${getJobInfo(job)} activity=${job ? (job.data.activity ? job.data.activity.id : 'none') : '-'}`, { job, e: renderError(err) }))
-			.on('error', (err: Error) => inboxLogger.error(`error ${err.stack}`, { e: renderError(err) }))
-			.on('stalled', (jobId) => inboxLogger.warn(`stalled id=${jobId}`));
+			this.inboxQueueWorker
+				.on('active', (job) => logger.debug(`active ${getJobInfo(job, true)}`))
+				.on('completed', (job, result) => logger.debug(`completed(${result}) ${getJobInfo(job, true)}`))
+				.on('failed', (job, err) => {
+					logger.error(`failed(${err.name}: ${err.message}) ${getJobInfo(job)} activity=${job ? (job.data.activity ? job.data.activity.id : 'none') : '-'}`, { job: renderJob(job), e: renderError(err) });
+					if (config.sentryForBackend) {
+						Sentry.captureMessage(`Queue: Inbox: ${err.name}: ${err.message}`, {
+							level: 'error',
+							extra: { job, err },
+						});
+					}
+				})
+				.on('error', (err: Error) => logger.error(`error ${err.name}: ${err.message}`, { e: renderError(err) }))
+				.on('stalled', (jobId) => logger.warn(`stalled id=${jobId}`));
+		}
 		//#endregion
 
-		//#region webhook deliver
-		this.webhookDeliverQueueWorker = new Bull.Worker(QUEUE.WEBHOOK_DELIVER, (job) => this.webhookDeliverProcessorService.process(job), {
-			...baseQueueOptions(this.config, QUEUE.WEBHOOK_DELIVER),
-			autorun: false,
-			concurrency: 64,
-			limiter: {
-				max: 64,
-				duration: 1000,
-			},
-			settings: {
-				backoffStrategy: httpRelatedBackoff,
-			},
-		});
+		//#region user-webhook deliver
+		{
+			this.userWebhookDeliverQueueWorker = new Bull.Worker(QUEUE.USER_WEBHOOK_DELIVER, (job) => {
+				if (this.config.sentryForBackend) {
+					return Sentry.startSpan({ name: 'Queue: UserWebhookDeliver' }, () => this.userWebhookDeliverProcessorService.process(job));
+				} else {
+					return this.userWebhookDeliverProcessorService.process(job);
+				}
+			}, {
+				...baseQueueOptions(this.config, QUEUE.USER_WEBHOOK_DELIVER),
+				autorun: false,
+				concurrency: 64,
+				limiter: {
+					max: 64,
+					duration: 1000,
+				},
+				settings: {
+					backoffStrategy: httpRelatedBackoff,
+				},
+			});
 
-		const webhookLogger = this.logger.createSubLogger('webhook');
+			const logger = this.logger.createSubLogger('user-webhook');
 
-		this.webhookDeliverQueueWorker
-			.on('active', (job) => webhookLogger.debug(`active ${getJobInfo(job, true)} to=${job.data.to}`))
-			.on('completed', (job, result) => webhookLogger.debug(`completed(${result}) ${getJobInfo(job, true)} to=${job.data.to}`))
-			.on('failed', (job, err) => webhookLogger.warn(`failed(${err.stack}) ${getJobInfo(job)} to=${job ? job.data.to : '-'}`))
-			.on('error', (err: Error) => webhookLogger.error(`error ${err.stack}`, { e: renderError(err) }))
-			.on('stalled', (jobId) => webhookLogger.warn(`stalled id=${jobId}`));
+			this.userWebhookDeliverQueueWorker
+				.on('active', (job) => logger.debug(`active ${getJobInfo(job, true)} to=${job.data.to}`))
+				.on('completed', (job, result) => logger.debug(`completed(${result}) ${getJobInfo(job, true)} to=${job.data.to}`))
+				.on('failed', (job, err) => {
+					logger.error(`failed(${err.name}: ${err.message}) ${getJobInfo(job)} to=${job ? job.data.to : '-'}`);
+					if (config.sentryForBackend) {
+						Sentry.captureMessage(`Queue: UserWebhookDeliver: ${err.name}: ${err.message}`, {
+							level: 'error',
+							extra: { job, err },
+						});
+					}
+				})
+				.on('error', (err: Error) => logger.error(`error ${err.name}: ${err.message}`, { e: renderError(err) }))
+				.on('stalled', (jobId) => logger.warn(`stalled id=${jobId}`));
+		}
+		//#endregion
+
+		//#region system-webhook deliver
+		{
+			this.systemWebhookDeliverQueueWorker = new Bull.Worker(QUEUE.SYSTEM_WEBHOOK_DELIVER, (job) => {
+				if (this.config.sentryForBackend) {
+					return Sentry.startSpan({ name: 'Queue: SystemWebhookDeliver' }, () => this.systemWebhookDeliverProcessorService.process(job));
+				} else {
+					return this.systemWebhookDeliverProcessorService.process(job);
+				}
+			}, {
+				...baseQueueOptions(this.config, QUEUE.SYSTEM_WEBHOOK_DELIVER),
+				autorun: false,
+				concurrency: 16,
+				limiter: {
+					max: 16,
+					duration: 1000,
+				},
+				settings: {
+					backoffStrategy: httpRelatedBackoff,
+				},
+			});
+
+			const logger = this.logger.createSubLogger('system-webhook');
+
+			this.systemWebhookDeliverQueueWorker
+				.on('active', (job) => logger.debug(`active ${getJobInfo(job, true)} to=${job.data.to}`))
+				.on('completed', (job, result) => logger.debug(`completed(${result}) ${getJobInfo(job, true)} to=${job.data.to}`))
+				.on('failed', (job, err) => {
+					logger.error(`failed(${err.name}: ${err.message}) ${getJobInfo(job)} to=${job ? job.data.to : '-'}`);
+					if (config.sentryForBackend) {
+						Sentry.captureMessage(`Queue: SystemWebhookDeliver: ${err.name}: ${err.message}`, {
+							level: 'error',
+							extra: { job, err },
+						});
+					}
+				})
+				.on('error', (err: Error) => logger.error(`error ${err.name}: ${err.message}`, { e: renderError(err) }))
+				.on('stalled', (jobId) => logger.warn(`stalled id=${jobId}`));
+		}
 		//#endregion
 
 		//#region relationship
-		this.relationshipQueueWorker = new Bull.Worker(QUEUE.RELATIONSHIP, (job) => {
-			switch (job.name) {
-				case 'follow': return this.relationshipProcessorService.processFollow(job);
-				case 'unfollow': return this.relationshipProcessorService.processUnfollow(job);
-				case 'block': return this.relationshipProcessorService.processBlock(job);
-				case 'unblock': return this.relationshipProcessorService.processUnblock(job);
-				default: throw new Error(`unrecognized job type ${job.name} for relationship`);
-			}
-		}, {
-			...baseQueueOptions(this.config, QUEUE.RELATIONSHIP),
-			autorun: false,
-			concurrency: this.config.relationshipJobConcurrency ?? 16,
-			limiter: {
-				max: this.config.relationshipJobPerSec ?? 64,
-				duration: 1000,
-			},
-		});
+		{
+			const processer = (job: Bull.Job) => {
+				switch (job.name) {
+					case 'follow': return this.relationshipProcessorService.processFollow(job);
+					case 'unfollow': return this.relationshipProcessorService.processUnfollow(job);
+					case 'block': return this.relationshipProcessorService.processBlock(job);
+					case 'unblock': return this.relationshipProcessorService.processUnblock(job);
+					default: throw new Error(`unrecognized job type ${job.name} for relationship`);
+				}
+			};
 
-		const relationshipLogger = this.logger.createSubLogger('relationship');
+			this.relationshipQueueWorker = new Bull.Worker(QUEUE.RELATIONSHIP, (job) => {
+				if (this.config.sentryForBackend) {
+					return Sentry.startSpan({ name: 'Queue: Relationship: ' + job.name }, () => processer(job));
+				} else {
+					return processer(job);
+				}
+			}, {
+				...baseQueueOptions(this.config, QUEUE.RELATIONSHIP),
+				autorun: false,
+				concurrency: this.config.relationshipJobConcurrency ?? 16,
+				limiter: {
+					max: this.config.relationshipJobPerSec ?? 64,
+					duration: 1000,
+				},
+			});
 
-		this.relationshipQueueWorker
-			.on('active', (job) => relationshipLogger.debug(`active id=${job.id}`))
-			.on('completed', (job, result) => relationshipLogger.debug(`completed(${result}) id=${job.id}`))
-			.on('failed', (job, err) => relationshipLogger.warn(`failed(${err.stack}) id=${job ? job.id : '-'}`, { job, e: renderError(err) }))
-			.on('error', (err: Error) => relationshipLogger.error(`error ${err.stack}`, { e: renderError(err) }))
-			.on('stalled', (jobId) => relationshipLogger.warn(`stalled id=${jobId}`));
+			const logger = this.logger.createSubLogger('relationship');
+
+			this.relationshipQueueWorker
+				.on('active', (job) => logger.debug(`active id=${job.id}`))
+				.on('completed', (job, result) => logger.debug(`completed(${result}) id=${job.id}`))
+				.on('failed', (job, err) => {
+					logger.error(`failed(${err.name}: ${err.message}) id=${job?.id ?? '?'}`, { job: renderJob(job), e: renderError(err) });
+					if (config.sentryForBackend) {
+						Sentry.captureMessage(`Queue: Relationship: ${job?.name ?? '?'}: ${err.name}: ${err.message}`, {
+							level: 'error',
+							extra: { job, err },
+						});
+					}
+				})
+				.on('error', (err: Error) => logger.error(`error ${err.name}: ${err.message}`, { e: renderError(err) }))
+				.on('stalled', (jobId) => logger.warn(`stalled id=${jobId}`));
+		}
 		//#endregion
 
 		//#region object storage
-		this.objectStorageQueueWorker = new Bull.Worker(QUEUE.OBJECT_STORAGE, (job) => {
-			switch (job.name) {
-				case 'deleteFile': return this.deleteFileProcessorService.process(job);
-				case 'cleanRemoteFiles': return this.cleanRemoteFilesProcessorService.process(job);
-				default: throw new Error(`unrecognized job type ${job.name} for objectStorage`);
-			}
-		}, {
-			...baseQueueOptions(this.config, QUEUE.OBJECT_STORAGE),
-			autorun: false,
-			concurrency: 16,
-		});
+		{
+			const processer = (job: Bull.Job) => {
+				switch (job.name) {
+					case 'deleteFile': return this.deleteFileProcessorService.process(job);
+					case 'cleanRemoteFiles': return this.cleanRemoteFilesProcessorService.process(job);
+					default: throw new Error(`unrecognized job type ${job.name} for objectStorage`);
+				}
+			};
 
-		const objectStorageLogger = this.logger.createSubLogger('objectStorage');
+			this.objectStorageQueueWorker = new Bull.Worker(QUEUE.OBJECT_STORAGE, (job) => {
+				if (this.config.sentryForBackend) {
+					return Sentry.startSpan({ name: 'Queue: ObjectStorage: ' + job.name }, () => processer(job));
+				} else {
+					return processer(job);
+				}
+			}, {
+				...baseQueueOptions(this.config, QUEUE.OBJECT_STORAGE),
+				autorun: false,
+				concurrency: 16,
+			});
 
-		this.objectStorageQueueWorker
-			.on('active', (job) => objectStorageLogger.debug(`active id=${job.id}`))
-			.on('completed', (job, result) => objectStorageLogger.debug(`completed(${result}) id=${job.id}`))
-			.on('failed', (job, err) => objectStorageLogger.warn(`failed(${err.stack}) id=${job ? job.id : '-'}`, { job, e: renderError(err) }))
-			.on('error', (err: Error) => objectStorageLogger.error(`error ${err.stack}`, { e: renderError(err) }))
-			.on('stalled', (jobId) => objectStorageLogger.warn(`stalled id=${jobId}`));
+			const logger = this.logger.createSubLogger('objectStorage');
+
+			this.objectStorageQueueWorker
+				.on('active', (job) => logger.debug(`active id=${job.id}`))
+				.on('completed', (job, result) => logger.debug(`completed(${result}) id=${job.id}`))
+				.on('failed', (job, err) => {
+					logger.error(`failed(${err.name}: ${err.message}) id=${job?.id ?? '?'}`, { job: renderJob(job), e: renderError(err) });
+					if (config.sentryForBackend) {
+						Sentry.captureMessage(`Queue: ObjectStorage: ${job?.name ?? '?'}: ${err.name}: ${err.message}`, {
+							level: 'error',
+							extra: { job, err },
+						});
+					}
+				})
+				.on('error', (err: Error) => logger.error(`error ${err.name}: ${err.message}`, { e: renderError(err) }))
+				.on('stalled', (jobId) => logger.warn(`stalled id=${jobId}`));
+		}
 		//#endregion
 
 		//#region ended poll notification
-		this.endedPollNotificationQueueWorker = new Bull.Worker(QUEUE.ENDED_POLL_NOTIFICATION, (job) => this.endedPollNotificationProcessorService.process(job), {
-			...baseQueueOptions(this.config, QUEUE.ENDED_POLL_NOTIFICATION),
-			autorun: false,
-		});
+		{
+			this.endedPollNotificationQueueWorker = new Bull.Worker(QUEUE.ENDED_POLL_NOTIFICATION, (job) => {
+				if (this.config.sentryForBackend) {
+					return Sentry.startSpan({ name: 'Queue: EndedPollNotification' }, () => this.endedPollNotificationProcessorService.process(job));
+				} else {
+					return this.endedPollNotificationProcessorService.process(job);
+				}
+			}, {
+				...baseQueueOptions(this.config, QUEUE.ENDED_POLL_NOTIFICATION),
+				autorun: false,
+			});
+		}
 		//#endregion
 	}
 
@@ -338,7 +526,8 @@ export class QueueProcessorService implements OnApplicationShutdown {
 			this.dbQueueWorker.run(),
 			this.deliverQueueWorker.run(),
 			this.inboxQueueWorker.run(),
-			this.webhookDeliverQueueWorker.run(),
+			this.userWebhookDeliverQueueWorker.run(),
+			this.systemWebhookDeliverQueueWorker.run(),
 			this.relationshipQueueWorker.run(),
 			this.objectStorageQueueWorker.run(),
 			this.endedPollNotificationQueueWorker.run(),
@@ -352,7 +541,8 @@ export class QueueProcessorService implements OnApplicationShutdown {
 			this.dbQueueWorker.close(),
 			this.deliverQueueWorker.close(),
 			this.inboxQueueWorker.close(),
-			this.webhookDeliverQueueWorker.close(),
+			this.userWebhookDeliverQueueWorker.close(),
+			this.systemWebhookDeliverQueueWorker.close(),
 			this.relationshipQueueWorker.close(),
 			this.objectStorageQueueWorker.close(),
 			this.endedPollNotificationQueueWorker.close(),
diff --git a/packages/backend/src/queue/const.ts b/packages/backend/src/queue/const.ts
index 132e916612..67f689b618 100644
--- a/packages/backend/src/queue/const.ts
+++ b/packages/backend/src/queue/const.ts
@@ -14,7 +14,8 @@ export const QUEUE = {
 	DB: 'db',
 	RELATIONSHIP: 'relationship',
 	OBJECT_STORAGE: 'objectStorage',
-	WEBHOOK_DELIVER: 'webhookDeliver',
+	USER_WEBHOOK_DELIVER: 'userWebhookDeliver',
+	SYSTEM_WEBHOOK_DELIVER: 'systemWebhookDeliver',
 };
 
 export function baseQueueOptions(config: Config, queueName: typeof QUEUE[keyof typeof QUEUE]): Bull.QueueOptions {
diff --git a/packages/backend/src/queue/processors/BakeBufferedReactionsProcessorService.ts b/packages/backend/src/queue/processors/BakeBufferedReactionsProcessorService.ts
new file mode 100644
index 0000000000..d49c99f694
--- /dev/null
+++ b/packages/backend/src/queue/processors/BakeBufferedReactionsProcessorService.ts
@@ -0,0 +1,42 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Inject, Injectable } from '@nestjs/common';
+import type Logger from '@/logger.js';
+import { bindThis } from '@/decorators.js';
+import { ReactionsBufferingService } from '@/core/ReactionsBufferingService.js';
+import { QueueLoggerService } from '../QueueLoggerService.js';
+import type * as Bull from 'bullmq';
+import { MiMeta } from '@/models/_.js';
+import { DI } from '@/di-symbols.js';
+
+@Injectable()
+export class BakeBufferedReactionsProcessorService {
+	private logger: Logger;
+
+	constructor(
+		@Inject(DI.meta)
+		private meta: MiMeta,
+
+		private reactionsBufferingService: ReactionsBufferingService,
+		private queueLoggerService: QueueLoggerService,
+	) {
+		this.logger = this.queueLoggerService.logger.createSubLogger('bake-buffered-reactions');
+	}
+
+	@bindThis
+	public async process(): Promise<void> {
+		if (!this.meta.enableReactionsBuffering) {
+			this.logger.info('Reactions buffering is disabled. Skipping...');
+			return;
+		}
+
+		this.logger.info('Baking buffered reactions...');
+
+		await this.reactionsBufferingService.bake();
+
+		this.logger.succ('All buffered reactions baked.');
+	}
+}
diff --git a/packages/backend/src/queue/processors/CheckModeratorsActivityProcessorService.ts b/packages/backend/src/queue/processors/CheckModeratorsActivityProcessorService.ts
new file mode 100644
index 0000000000..87183cb342
--- /dev/null
+++ b/packages/backend/src/queue/processors/CheckModeratorsActivityProcessorService.ts
@@ -0,0 +1,292 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Inject, Injectable } from '@nestjs/common';
+import { In } from 'typeorm';
+import type Logger from '@/logger.js';
+import { bindThis } from '@/decorators.js';
+import { MetaService } from '@/core/MetaService.js';
+import { RoleService } from '@/core/RoleService.js';
+import { EmailService } from '@/core/EmailService.js';
+import { MiUser, type UserProfilesRepository } from '@/models/_.js';
+import { DI } from '@/di-symbols.js';
+import { SystemWebhookService } from '@/core/SystemWebhookService.js';
+import { AnnouncementService } from '@/core/AnnouncementService.js';
+import { QueueLoggerService } from '../QueueLoggerService.js';
+
+// モデレーターが不在と判断する日付の閾値
+const MODERATOR_INACTIVITY_LIMIT_DAYS = 7;
+// 警告通知やログ出力を行う残日数の閾値
+const MODERATOR_INACTIVITY_WARNING_REMAINING_DAYS = 2;
+// 期限から6時間ごとに通知を行う
+const MODERATOR_INACTIVITY_WARNING_NOTIFY_INTERVAL_HOURS = 6;
+const ONE_HOUR_MILLI_SEC = 1000 * 60 * 60;
+const ONE_DAY_MILLI_SEC = ONE_HOUR_MILLI_SEC * 24;
+
+export type ModeratorInactivityEvaluationResult = {
+	isModeratorsInactive: boolean;
+	inactiveModerators: MiUser[];
+	remainingTime: ModeratorInactivityRemainingTime;
+}
+
+export type ModeratorInactivityRemainingTime = {
+	time: number;
+	asHours: number;
+	asDays: number;
+};
+
+function generateModeratorInactivityMail(remainingTime: ModeratorInactivityRemainingTime) {
+	const subject = 'Moderator Inactivity Warning / モデレーター不在の通知';
+
+	const timeVariant = remainingTime.asDays === 0 ? `${remainingTime.asHours} hours` : `${remainingTime.asDays} days`;
+	const timeVariantJa = remainingTime.asDays === 0 ? `${remainingTime.asHours} 時間` : `${remainingTime.asDays} 日間`;
+	const message = [
+		'To Moderators,',
+		'',
+		`A moderator has been inactive for a period of time. If there are ${timeVariant} of inactivity left, it will switch to invitation only.`,
+		'If you do not wish to move to invitation only, you must log into Misskey and update your last active date and time.',
+		'',
+		'---------------',
+		'',
+		'To モデレーター各位',
+		'',
+		`モデレーターが一定期間活動していないようです。あと${timeVariantJa}活動していない状態が続くと招待制に切り替わります。`,
+		'招待制に切り替わることを望まない場合は、Misskeyにログインして最終アクティブ日時を更新してください。',
+		'',
+	];
+
+	const html = message.join('<br>');
+	const text = message.join('\n');
+
+	return {
+		subject,
+		html,
+		text,
+	};
+}
+
+function generateInvitationOnlyChangedMail() {
+	const subject = 'Change to Invitation-Only / 招待制に変更されました';
+
+	const message = [
+		'To Moderators,',
+		'',
+		`Changed to invitation only because no moderator activity was detected for ${MODERATOR_INACTIVITY_LIMIT_DAYS} days.`,
+		'To cancel the invitation only, you need to access the control panel.',
+		'',
+		'---------------',
+		'',
+		'To モデレーター各位',
+		'',
+		`モデレーターの活動が${MODERATOR_INACTIVITY_LIMIT_DAYS}日間検出されなかったため、招待制に変更されました。`,
+		'招待制を解除するには、コントロールパネルにアクセスする必要があります。',
+		'',
+	];
+
+	const html = message.join('<br>');
+	const text = message.join('\n');
+
+	return {
+		subject,
+		html,
+		text,
+	};
+}
+
+@Injectable()
+export class CheckModeratorsActivityProcessorService {
+	private logger: Logger;
+
+	constructor(
+		@Inject(DI.userProfilesRepository)
+		private userProfilesRepository: UserProfilesRepository,
+		private metaService: MetaService,
+		private roleService: RoleService,
+		private emailService: EmailService,
+		private announcementService: AnnouncementService,
+		private systemWebhookService: SystemWebhookService,
+		private queueLoggerService: QueueLoggerService,
+	) {
+		this.logger = this.queueLoggerService.logger.createSubLogger('check-moderators-activity');
+	}
+
+	@bindThis
+	public async process(): Promise<void> {
+		this.logger.info('start.');
+
+		const meta = await this.metaService.fetch(false);
+		if (!meta.disableRegistration) {
+			await this.processImpl();
+		} else {
+			this.logger.info('is already invitation only.');
+		}
+
+		this.logger.succ('finish.');
+	}
+
+	@bindThis
+	private async processImpl() {
+		const evaluateResult = await this.evaluateModeratorsInactiveDays();
+		if (evaluateResult.isModeratorsInactive) {
+			this.logger.warn(`The moderator has been inactive for ${MODERATOR_INACTIVITY_LIMIT_DAYS} days. We will move to invitation only.`);
+
+			await this.changeToInvitationOnly();
+			await this.notifyChangeToInvitationOnly();
+		} else {
+			const remainingTime = evaluateResult.remainingTime;
+			if (remainingTime.asDays <= MODERATOR_INACTIVITY_WARNING_REMAINING_DAYS) {
+				const timeVariant = remainingTime.asDays === 0 ? `${remainingTime.asHours} hours` : `${remainingTime.asDays} days`;
+				this.logger.warn(`A moderator has been inactive for a period of time. If you are inactive for an additional ${timeVariant}, it will switch to invitation only.`);
+
+				if (remainingTime.asHours % MODERATOR_INACTIVITY_WARNING_NOTIFY_INTERVAL_HOURS === 0) {
+					// ジョブの実行頻度と同等だと通知が多すぎるため期限から6時間ごとに通知する
+					// つまり、のこり2日を切ったら6時間ごとに通知が送られる
+					await this.notifyInactiveModeratorsWarning(remainingTime);
+				}
+			}
+		}
+	}
+
+	/**
+	 * モデレーターが不在であるかどうかを確認する。trueの場合はモデレーターが不在である。
+	 * isModerator, isAdministrator, isRootのいずれかがtrueのユーザを対象に、
+	 * {@link MiUser.lastActiveDate}の値が実行日時の{@link MODERATOR_INACTIVITY_LIMIT_DAYS}日前よりも古いユーザがいるかどうかを確認する。
+	 * {@link MiUser.lastActiveDate}がnullの場合は、そのユーザは確認の対象外とする。
+	 *
+	 * -----
+	 *
+	 * ### サンプルパターン
+	 * - 実行日時: 2022-01-30 12:00:00
+	 * - 判定基準: 2022-01-23 12:00:00(実行日時の{@link MODERATOR_INACTIVITY_LIMIT_DAYS}日前)
+	 *
+	 * #### パターン①
+	 * - モデレータA: lastActiveDate = 2022-01-20 00:00:00 ※アウト
+	 * - モデレータB: lastActiveDate = 2022-01-23 12:00:00 ※セーフ(判定基準と同値なのでギリギリ残り0日)
+	 * - モデレータC: lastActiveDate = 2022-01-23 11:59:59 ※アウト(残り-1日)
+	 * - モデレータD: lastActiveDate = null
+	 *
+	 * この場合、モデレータBのアクティビティのみ判定基準日よりも古くないため、モデレーターが在席と判断される。
+	 *
+	 * #### パターン②
+	 * - モデレータA: lastActiveDate = 2022-01-20 00:00:00 ※アウト
+	 * - モデレータB: lastActiveDate = 2022-01-22 12:00:00 ※アウト(残り-1日)
+	 * - モデレータC: lastActiveDate = 2022-01-23 11:59:59 ※アウト(残り-1日)
+	 * - モデレータD: lastActiveDate = null
+	 *
+	 * この場合、モデレータA, B, Cのアクティビティは判定基準日よりも古いため、モデレーターが不在と判断される。
+	 */
+	@bindThis
+	public async evaluateModeratorsInactiveDays(): Promise<ModeratorInactivityEvaluationResult> {
+		const today = new Date();
+		const inactivePeriod = new Date(today);
+		inactivePeriod.setDate(today.getDate() - MODERATOR_INACTIVITY_LIMIT_DAYS);
+
+		const moderators = await this.fetchModerators()
+			.then(it => it.filter(it => it.lastActiveDate != null));
+		const inactiveModerators = moderators
+			// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+			.filter(it => it.lastActiveDate!.getTime() < inactivePeriod.getTime());
+
+		// 残りの猶予を示したいので、最終アクティブ日時が一番若いモデレータの日数を基準に猶予を計算する
+		// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+		const newestLastActiveDate = new Date(Math.max(...moderators.map(it => it.lastActiveDate!.getTime())));
+		const remainingTime = newestLastActiveDate.getTime() - inactivePeriod.getTime();
+		const remainingTimeAsDays = Math.floor(remainingTime / ONE_DAY_MILLI_SEC);
+		const remainingTimeAsHours = Math.floor((remainingTime / ONE_HOUR_MILLI_SEC));
+
+		return {
+			isModeratorsInactive: inactiveModerators.length === moderators.length,
+			inactiveModerators,
+			remainingTime: {
+				time: remainingTime,
+				asHours: remainingTimeAsHours,
+				asDays: remainingTimeAsDays,
+			},
+		};
+	}
+
+	@bindThis
+	private async changeToInvitationOnly() {
+		await this.metaService.update({ disableRegistration: true });
+	}
+
+	@bindThis
+	public async notifyInactiveModeratorsWarning(remainingTime: ModeratorInactivityRemainingTime) {
+		// -- モデレータへのメール送信
+
+		const moderators = await this.fetchModerators();
+		const moderatorProfiles = await this.userProfilesRepository
+			.findBy({ userId: In(moderators.map(it => it.id)) })
+			.then(it => new Map(it.map(it => [it.userId, it])));
+
+		const mail = generateModeratorInactivityMail(remainingTime);
+		for (const moderator of moderators) {
+			const profile = moderatorProfiles.get(moderator.id);
+			if (profile && profile.email && profile.emailVerified) {
+				this.emailService.sendEmail(profile.email, mail.subject, mail.html, mail.text);
+			}
+		}
+
+		// -- SystemWebhook
+
+		const systemWebhooks = await this.systemWebhookService.fetchActiveSystemWebhooks()
+			.then(it => it.filter(it => it.on.includes('inactiveModeratorsWarning')));
+		for (const systemWebhook of systemWebhooks) {
+			this.systemWebhookService.enqueueSystemWebhook(
+				systemWebhook,
+				'inactiveModeratorsWarning',
+				{ remainingTime: remainingTime },
+			);
+		}
+	}
+
+	@bindThis
+	public async notifyChangeToInvitationOnly() {
+		// -- モデレータへのメールとお知らせ(個人向け)送信
+
+		const moderators = await this.fetchModerators();
+		const moderatorProfiles = await this.userProfilesRepository
+			.findBy({ userId: In(moderators.map(it => it.id)) })
+			.then(it => new Map(it.map(it => [it.userId, it])));
+
+		const mail = generateInvitationOnlyChangedMail();
+		for (const moderator of moderators) {
+			this.announcementService.create({
+				title: mail.subject,
+				text: mail.text,
+				forExistingUsers: true,
+				needConfirmationToRead: true,
+				userId: moderator.id,
+			});
+
+			const profile = moderatorProfiles.get(moderator.id);
+			if (profile && profile.email && profile.emailVerified) {
+				this.emailService.sendEmail(profile.email, mail.subject, mail.html, mail.text);
+			}
+		}
+
+		// -- SystemWebhook
+
+		const systemWebhooks = await this.systemWebhookService.fetchActiveSystemWebhooks()
+			.then(it => it.filter(it => it.on.includes('inactiveModeratorsInvitationOnlyChanged')));
+		for (const systemWebhook of systemWebhooks) {
+			this.systemWebhookService.enqueueSystemWebhook(
+				systemWebhook,
+				'inactiveModeratorsInvitationOnlyChanged',
+				{},
+			);
+		}
+	}
+
+	@bindThis
+	private async fetchModerators() {
+		// TODO: モデレーター以外にも特別な権限を持つユーザーがいる場合は考慮する
+		return this.roleService.getModerators({
+			includeAdmins: true,
+			includeRoot: true,
+			excludeExpire: true,
+		});
+	}
+}
diff --git a/packages/backend/src/queue/processors/DeliverProcessorService.ts b/packages/backend/src/queue/processors/DeliverProcessorService.ts
index b73195afc3..5a16496011 100644
--- a/packages/backend/src/queue/processors/DeliverProcessorService.ts
+++ b/packages/backend/src/queue/processors/DeliverProcessorService.ts
@@ -7,9 +7,8 @@ import { Inject, Injectable } from '@nestjs/common';
 import * as Bull from 'bullmq';
 import { Not } from 'typeorm';
 import { DI } from '@/di-symbols.js';
-import type { InstancesRepository } from '@/models/_.js';
+import type { InstancesRepository, MiMeta } from '@/models/_.js';
 import type Logger from '@/logger.js';
-import { MetaService } from '@/core/MetaService.js';
 import { ApRequestService } from '@/core/activitypub/ApRequestService.js';
 import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
 import { FetchInstanceMetadataService } from '@/core/FetchInstanceMetadataService.js';
@@ -31,10 +30,12 @@ export class DeliverProcessorService {
 	private latest: string | null;
 
 	constructor(
+		@Inject(DI.meta)
+		private meta: MiMeta,
+
 		@Inject(DI.instancesRepository)
 		private instancesRepository: InstancesRepository,
 
-		private metaService: MetaService,
 		private utilityService: UtilityService,
 		private federatedInstanceService: FederatedInstanceService,
 		private fetchInstanceMetadataService: FetchInstanceMetadataService,
@@ -45,16 +46,14 @@ export class DeliverProcessorService {
 		private queueLoggerService: QueueLoggerService,
 	) {
 		this.logger = this.queueLoggerService.logger.createSubLogger('deliver');
-		this.suspendedHostsCache = new MemorySingleCache<MiInstance[]>(1000 * 60 * 60);
+		this.suspendedHostsCache = new MemorySingleCache<MiInstance[]>(1000 * 60 * 60); // 1h
 	}
 
 	@bindThis
 	public async process(job: Bull.Job<DeliverJobData>): Promise<string> {
 		const { host } = new URL(job.data.to);
 
-		// ブロックしてたら中断
-		const meta = await this.metaService.fetch();
-		if (this.utilityService.isBlockedHost(meta.blockedHosts, this.utilityService.toPuny(host))) {
+		if (!this.utilityService.isFederationAllowedUri(job.data.to)) {
 			return 'skip (blocked)';
 		}
 
@@ -75,8 +74,17 @@ export class DeliverProcessorService {
 		try {
 			await this.apRequestService.signedPost(job.data.user, job.data.to, job.data.content, job.data.digest);
 
-			// Update stats
-			this.federatedInstanceService.fetch(host).then(i => {
+			this.apRequestChart.deliverSucc();
+			this.federationChart.deliverd(host, true);
+
+			// Update instance stats
+			process.nextTick(async () => {
+				const i = await (this.meta.enableStatsForFederatedInstances
+					? this.federatedInstanceService.fetchOrRegister(host)
+					: this.federatedInstanceService.fetch(host));
+
+				if (i == null) return;
+
 				if (i.isNotResponding) {
 					this.federatedInstanceService.update(i.id, {
 						isNotResponding: false,
@@ -84,19 +92,22 @@ export class DeliverProcessorService {
 					});
 				}
 
-				this.fetchInstanceMetadataService.fetchInstanceMetadata(i);
-				this.apRequestChart.deliverSucc();
-				this.federationChart.deliverd(i.host, true);
+				if (this.meta.enableStatsForFederatedInstances) {
+					this.fetchInstanceMetadataService.fetchInstanceMetadata(i);
+				}
 
-				if (meta.enableChartsForFederatedInstances) {
+				if (this.meta.enableChartsForFederatedInstances) {
 					this.instanceChart.requestSent(i.host, true);
 				}
 			});
 
 			return 'Success';
 		} catch (res) {
-			// Update stats
-			this.federatedInstanceService.fetch(host).then(i => {
+			this.apRequestChart.deliverFail();
+			this.federationChart.deliverd(host, false);
+
+			// Update instance stats
+			this.federatedInstanceService.fetchOrRegister(host).then(i => {
 				if (!i.isNotResponding) {
 					this.federatedInstanceService.update(i.id, {
 						isNotResponding: true,
@@ -109,12 +120,15 @@ export class DeliverProcessorService {
 							suspensionState: 'autoSuspendedForNotResponding',
 						});
 					}
+				} else {
+					// isNotRespondingがtrueでnotRespondingSinceがnullの場合はnotRespondingSinceをセット
+					// notRespondingSinceは新たな機能なので、それ以前のデータにはnotRespondingSinceがない場合がある
+					this.federatedInstanceService.update(i.id, {
+						notRespondingSince: new Date(),
+					});
 				}
 
-				this.apRequestChart.deliverFail();
-				this.federationChart.deliverd(i.host, false);
-
-				if (meta.enableChartsForFederatedInstances) {
+				if (this.meta.enableChartsForFederatedInstances) {
 					this.instanceChart.requestSent(i.host, false);
 				}
 			});
@@ -124,7 +138,7 @@ export class DeliverProcessorService {
 				if (!res.isRetryable) {
 					// 相手が閉鎖していることを明示しているため、配送停止する
 					if (job.data.isSharedInbox && res.statusCode === 410) {
-						this.federatedInstanceService.fetch(host).then(i => {
+						this.federatedInstanceService.fetchOrRegister(host).then(i => {
 							this.federatedInstanceService.update(i.id, {
 								suspensionState: 'goneSuspended',
 							});
diff --git a/packages/backend/src/queue/processors/EndedPollNotificationProcessorService.ts b/packages/backend/src/queue/processors/EndedPollNotificationProcessorService.ts
index 29c1f27bb1..34180e5f2b 100644
--- a/packages/backend/src/queue/processors/EndedPollNotificationProcessorService.ts
+++ b/packages/backend/src/queue/processors/EndedPollNotificationProcessorService.ts
@@ -7,6 +7,7 @@ import { Inject, Injectable } from '@nestjs/common';
 import { DI } from '@/di-symbols.js';
 import type { PollVotesRepository, NotesRepository } from '@/models/_.js';
 import type Logger from '@/logger.js';
+import { CacheService } from '@/core/CacheService.js';
 import { NotificationService } from '@/core/NotificationService.js';
 import { bindThis } from '@/decorators.js';
 import { QueueLoggerService } from '../QueueLoggerService.js';
@@ -24,6 +25,7 @@ export class EndedPollNotificationProcessorService {
 		@Inject(DI.pollVotesRepository)
 		private pollVotesRepository: PollVotesRepository,
 
+		private cacheService: CacheService,
 		private notificationService: NotificationService,
 		private queueLoggerService: QueueLoggerService,
 	) {
@@ -47,9 +49,12 @@ export class EndedPollNotificationProcessorService {
 		const userIds = [...new Set([note.userId, ...votes.map(v => v.userId)])];
 
 		for (const userId of userIds) {
-			this.notificationService.createNotification(userId, 'pollEnded', {
-				noteId: note.id,
-			});
+			const profile = await this.cacheService.userProfileCache.fetch(userId);
+			if (profile.userHost === null) {
+				this.notificationService.createNotification(userId, 'pollEnded', {
+					noteId: note.id,
+				});
+			}
 		}
 	}
 }
diff --git a/packages/backend/src/queue/processors/ExportAntennasProcessorService.ts b/packages/backend/src/queue/processors/ExportAntennasProcessorService.ts
index 88c4ea29c0..b3111865ad 100644
--- a/packages/backend/src/queue/processors/ExportAntennasProcessorService.ts
+++ b/packages/backend/src/queue/processors/ExportAntennasProcessorService.ts
@@ -14,6 +14,7 @@ import { DriveService } from '@/core/DriveService.js';
 import { bindThis } from '@/decorators.js';
 import { createTemp } from '@/misc/create-temp.js';
 import { UtilityService } from '@/core/UtilityService.js';
+import { NotificationService } from '@/core/NotificationService.js';
 import { QueueLoggerService } from '../QueueLoggerService.js';
 import type { DBExportAntennasData } from '../types.js';
 import type * as Bull from 'bullmq';
@@ -35,6 +36,7 @@ export class ExportAntennasProcessorService {
 		private driveService: DriveService,
 		private utilityService: UtilityService,
 		private queueLoggerService: QueueLoggerService,
+		private notificationService: NotificationService,
 	) {
 		this.logger = this.queueLoggerService.logger.createSubLogger('export-antennas');
 	}
@@ -95,6 +97,11 @@ export class ExportAntennasProcessorService {
 			const fileName = 'antennas-' + DateFormat(new Date(), 'yyyy-MM-dd-HH-mm-ss') + '.json';
 			const driveFile = await this.driveService.addFile({ user, path, name: fileName, force: true, ext: 'json' });
 			this.logger.succ('Exported to: ' + driveFile.id);
+
+			this.notificationService.createNotification(user.id, 'exportCompleted', {
+				exportedEntity: 'antenna',
+				fileId: driveFile.id,
+			});
 		} finally {
 			cleanup();
 		}
diff --git a/packages/backend/src/queue/processors/ExportBlockingProcessorService.ts b/packages/backend/src/queue/processors/ExportBlockingProcessorService.ts
index 6ec3c18786..ecc439db69 100644
--- a/packages/backend/src/queue/processors/ExportBlockingProcessorService.ts
+++ b/packages/backend/src/queue/processors/ExportBlockingProcessorService.ts
@@ -13,6 +13,7 @@ import type Logger from '@/logger.js';
 import { DriveService } from '@/core/DriveService.js';
 import { createTemp } from '@/misc/create-temp.js';
 import { UtilityService } from '@/core/UtilityService.js';
+import { NotificationService } from '@/core/NotificationService.js';
 import { bindThis } from '@/decorators.js';
 import { QueueLoggerService } from '../QueueLoggerService.js';
 import type * as Bull from 'bullmq';
@@ -30,6 +31,7 @@ export class ExportBlockingProcessorService {
 		private blockingsRepository: BlockingsRepository,
 
 		private utilityService: UtilityService,
+		private notificationService: NotificationService,
 		private driveService: DriveService,
 		private queueLoggerService: QueueLoggerService,
 	) {
@@ -109,6 +111,11 @@ export class ExportBlockingProcessorService {
 			const driveFile = await this.driveService.addFile({ user, path, name: fileName, force: true, ext: 'csv' });
 
 			this.logger.succ(`Exported to: ${driveFile.id}`);
+
+			this.notificationService.createNotification(user.id, 'exportCompleted', {
+				exportedEntity: 'blocking',
+				fileId: driveFile.id,
+			});
 		} finally {
 			cleanup();
 		}
diff --git a/packages/backend/src/queue/processors/ExportClipsProcessorService.ts b/packages/backend/src/queue/processors/ExportClipsProcessorService.ts
index 01eab26e96..583ddbb745 100644
--- a/packages/backend/src/queue/processors/ExportClipsProcessorService.ts
+++ b/packages/backend/src/queue/processors/ExportClipsProcessorService.ts
@@ -19,6 +19,7 @@ import { bindThis } from '@/decorators.js';
 import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
 import { Packed } from '@/misc/json-schema.js';
 import { IdService } from '@/core/IdService.js';
+import { NotificationService } from '@/core/NotificationService.js';
 import { QueueLoggerService } from '../QueueLoggerService.js';
 import type * as Bull from 'bullmq';
 import type { DbJobDataWithUser } from '../types.js';
@@ -43,6 +44,7 @@ export class ExportClipsProcessorService {
 		private driveService: DriveService,
 		private queueLoggerService: QueueLoggerService,
 		private idService: IdService,
+		private notificationService: NotificationService,
 	) {
 		this.logger = this.queueLoggerService.logger.createSubLogger('export-clips');
 	}
@@ -79,6 +81,11 @@ export class ExportClipsProcessorService {
 			const driveFile = await this.driveService.addFile({ user, path, name: fileName, force: true, ext: 'json' });
 
 			this.logger.succ(`Exported to: ${driveFile.id}`);
+
+			this.notificationService.createNotification(user.id, 'exportCompleted', {
+				exportedEntity: 'clip',
+				fileId: driveFile.id,
+			});
 		} finally {
 			cleanup();
 		}
diff --git a/packages/backend/src/queue/processors/ExportCustomEmojisProcessorService.ts b/packages/backend/src/queue/processors/ExportCustomEmojisProcessorService.ts
index e4eb4791bd..e237cd4975 100644
--- a/packages/backend/src/queue/processors/ExportCustomEmojisProcessorService.ts
+++ b/packages/backend/src/queue/processors/ExportCustomEmojisProcessorService.ts
@@ -16,6 +16,7 @@ import type Logger from '@/logger.js';
 import { DriveService } from '@/core/DriveService.js';
 import { createTemp, createTempDir } from '@/misc/create-temp.js';
 import { DownloadService } from '@/core/DownloadService.js';
+import { NotificationService } from '@/core/NotificationService.js';
 import { bindThis } from '@/decorators.js';
 import { QueueLoggerService } from '../QueueLoggerService.js';
 import type * as Bull from 'bullmq';
@@ -37,6 +38,7 @@ export class ExportCustomEmojisProcessorService {
 		private driveService: DriveService,
 		private downloadService: DownloadService,
 		private queueLoggerService: QueueLoggerService,
+		private notificationService: NotificationService,
 	) {
 		this.logger = this.queueLoggerService.logger.createSubLogger('export-custom-emojis');
 	}
@@ -134,6 +136,12 @@ export class ExportCustomEmojisProcessorService {
 				const driveFile = await this.driveService.addFile({ user, path: archivePath, name: fileName, force: true });
 
 				this.logger.succ(`Exported to: ${driveFile.id}`);
+
+				this.notificationService.createNotification(user.id, 'exportCompleted', {
+					exportedEntity: 'customEmoji',
+					fileId: driveFile.id,
+				});
+
 				cleanup();
 				archiveCleanup();
 				resolve();
diff --git a/packages/backend/src/queue/processors/ExportFavoritesProcessorService.ts b/packages/backend/src/queue/processors/ExportFavoritesProcessorService.ts
index 7bb626dd31..b81feece01 100644
--- a/packages/backend/src/queue/processors/ExportFavoritesProcessorService.ts
+++ b/packages/backend/src/queue/processors/ExportFavoritesProcessorService.ts
@@ -16,6 +16,7 @@ import type { MiPoll } from '@/models/Poll.js';
 import type { MiNote } from '@/models/Note.js';
 import { bindThis } from '@/decorators.js';
 import { IdService } from '@/core/IdService.js';
+import { NotificationService } from '@/core/NotificationService.js';
 import { QueueLoggerService } from '../QueueLoggerService.js';
 import type * as Bull from 'bullmq';
 import type { DbJobDataWithUser } from '../types.js';
@@ -37,6 +38,7 @@ export class ExportFavoritesProcessorService {
 		private driveService: DriveService,
 		private queueLoggerService: QueueLoggerService,
 		private idService: IdService,
+		private notificationService: NotificationService,
 	) {
 		this.logger = this.queueLoggerService.logger.createSubLogger('export-favorites');
 	}
@@ -123,6 +125,11 @@ export class ExportFavoritesProcessorService {
 			const driveFile = await this.driveService.addFile({ user, path, name: fileName, force: true, ext: 'json' });
 
 			this.logger.succ(`Exported to: ${driveFile.id}`);
+
+			this.notificationService.createNotification(user.id, 'exportCompleted', {
+				exportedEntity: 'favorite',
+				fileId: driveFile.id,
+			});
 		} finally {
 			cleanup();
 		}
diff --git a/packages/backend/src/queue/processors/ExportFollowingProcessorService.ts b/packages/backend/src/queue/processors/ExportFollowingProcessorService.ts
index 1cc80e66d7..903f962515 100644
--- a/packages/backend/src/queue/processors/ExportFollowingProcessorService.ts
+++ b/packages/backend/src/queue/processors/ExportFollowingProcessorService.ts
@@ -14,6 +14,7 @@ import { DriveService } from '@/core/DriveService.js';
 import { createTemp } from '@/misc/create-temp.js';
 import type { MiFollowing } from '@/models/Following.js';
 import { UtilityService } from '@/core/UtilityService.js';
+import { NotificationService } from '@/core/NotificationService.js';
 import { bindThis } from '@/decorators.js';
 import { QueueLoggerService } from '../QueueLoggerService.js';
 import type * as Bull from 'bullmq';
@@ -36,6 +37,7 @@ export class ExportFollowingProcessorService {
 		private utilityService: UtilityService,
 		private driveService: DriveService,
 		private queueLoggerService: QueueLoggerService,
+		private notificationService: NotificationService,
 	) {
 		this.logger = this.queueLoggerService.logger.createSubLogger('export-following');
 	}
@@ -113,6 +115,11 @@ export class ExportFollowingProcessorService {
 			const driveFile = await this.driveService.addFile({ user, path, name: fileName, force: true, ext: 'csv' });
 
 			this.logger.succ(`Exported to: ${driveFile.id}`);
+
+			this.notificationService.createNotification(user.id, 'exportCompleted', {
+				exportedEntity: 'following',
+				fileId: driveFile.id,
+			});
 		} finally {
 			cleanup();
 		}
diff --git a/packages/backend/src/queue/processors/ExportMutingProcessorService.ts b/packages/backend/src/queue/processors/ExportMutingProcessorService.ts
index 243b74f2c2..f9867ade29 100644
--- a/packages/backend/src/queue/processors/ExportMutingProcessorService.ts
+++ b/packages/backend/src/queue/processors/ExportMutingProcessorService.ts
@@ -13,6 +13,7 @@ import type Logger from '@/logger.js';
 import { DriveService } from '@/core/DriveService.js';
 import { createTemp } from '@/misc/create-temp.js';
 import { UtilityService } from '@/core/UtilityService.js';
+import { NotificationService } from '@/core/NotificationService.js';
 import { bindThis } from '@/decorators.js';
 import { QueueLoggerService } from '../QueueLoggerService.js';
 import type * as Bull from 'bullmq';
@@ -32,6 +33,7 @@ export class ExportMutingProcessorService {
 		private utilityService: UtilityService,
 		private driveService: DriveService,
 		private queueLoggerService: QueueLoggerService,
+		private notificationService: NotificationService,
 	) {
 		this.logger = this.queueLoggerService.logger.createSubLogger('export-muting');
 	}
@@ -110,6 +112,11 @@ export class ExportMutingProcessorService {
 			const driveFile = await this.driveService.addFile({ user, path, name: fileName, force: true, ext: 'csv' });
 
 			this.logger.succ(`Exported to: ${driveFile.id}`);
+
+			this.notificationService.createNotification(user.id, 'exportCompleted', {
+				exportedEntity: 'muting',
+				fileId: driveFile.id,
+			});
 		} finally {
 			cleanup();
 		}
diff --git a/packages/backend/src/queue/processors/ExportNotesProcessorService.ts b/packages/backend/src/queue/processors/ExportNotesProcessorService.ts
index c7611012d7..9e2b678219 100644
--- a/packages/backend/src/queue/processors/ExportNotesProcessorService.ts
+++ b/packages/backend/src/queue/processors/ExportNotesProcessorService.ts
@@ -18,6 +18,7 @@ import { bindThis } from '@/decorators.js';
 import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
 import { Packed } from '@/misc/json-schema.js';
 import { IdService } from '@/core/IdService.js';
+import { NotificationService } from '@/core/NotificationService.js';
 import { JsonArrayStream } from '@/misc/JsonArrayStream.js';
 import { FileWriterStream } from '@/misc/FileWriterStream.js';
 import { QueueLoggerService } from '../QueueLoggerService.js';
@@ -112,6 +113,7 @@ export class ExportNotesProcessorService {
 		private queueLoggerService: QueueLoggerService,
 		private driveFileEntityService: DriveFileEntityService,
 		private idService: IdService,
+		private notificationService: NotificationService,
 	) {
 		this.logger = this.queueLoggerService.logger.createSubLogger('export-notes');
 	}
@@ -150,6 +152,11 @@ export class ExportNotesProcessorService {
 			const driveFile = await this.driveService.addFile({ user, path, name: fileName, force: true, ext: 'json' });
 
 			this.logger.succ(`Exported to: ${driveFile.id}`);
+
+			this.notificationService.createNotification(user.id, 'exportCompleted', {
+				exportedEntity: 'note',
+				fileId: driveFile.id,
+			});
 		} finally {
 			cleanup();
 		}
diff --git a/packages/backend/src/queue/processors/ExportUserListsProcessorService.ts b/packages/backend/src/queue/processors/ExportUserListsProcessorService.ts
index ee87cff5d3..c483d79854 100644
--- a/packages/backend/src/queue/processors/ExportUserListsProcessorService.ts
+++ b/packages/backend/src/queue/processors/ExportUserListsProcessorService.ts
@@ -13,6 +13,7 @@ import type Logger from '@/logger.js';
 import { DriveService } from '@/core/DriveService.js';
 import { createTemp } from '@/misc/create-temp.js';
 import { UtilityService } from '@/core/UtilityService.js';
+import { NotificationService } from '@/core/NotificationService.js';
 import { bindThis } from '@/decorators.js';
 import { QueueLoggerService } from '../QueueLoggerService.js';
 import type * as Bull from 'bullmq';
@@ -35,6 +36,7 @@ export class ExportUserListsProcessorService {
 		private utilityService: UtilityService,
 		private driveService: DriveService,
 		private queueLoggerService: QueueLoggerService,
+		private notificationService: NotificationService,
 	) {
 		this.logger = this.queueLoggerService.logger.createSubLogger('export-user-lists');
 	}
@@ -89,6 +91,11 @@ export class ExportUserListsProcessorService {
 			const driveFile = await this.driveService.addFile({ user, path, name: fileName, force: true, ext: 'csv' });
 
 			this.logger.succ(`Exported to: ${driveFile.id}`);
+
+			this.notificationService.createNotification(user.id, 'exportCompleted', {
+				exportedEntity: 'userList',
+				fileId: driveFile.id,
+			});
 		} finally {
 			cleanup();
 		}
diff --git a/packages/backend/src/queue/processors/ImportCustomEmojisProcessorService.ts b/packages/backend/src/queue/processors/ImportCustomEmojisProcessorService.ts
index 171809d25c..9e1b8fee70 100644
--- a/packages/backend/src/queue/processors/ImportCustomEmojisProcessorService.ts
+++ b/packages/backend/src/queue/processors/ImportCustomEmojisProcessorService.ts
@@ -87,23 +87,30 @@ export class ImportCustomEmojisProcessorService {
 				await this.emojisRepository.delete({
 					name: emojiInfo.name,
 				});
-				const driveFile = await this.driveService.addFile({
-					user: null,
-					path: emojiPath,
-					name: record.fileName,
-					force: true,
-				});
-				await this.customEmojiService.add({
-					name: emojiInfo.name,
-					category: emojiInfo.category,
-					host: null,
-					aliases: emojiInfo.aliases,
-					driveFile,
-					license: emojiInfo.license,
-					isSensitive: emojiInfo.isSensitive,
-					localOnly: emojiInfo.localOnly,
-					roleIdsThatCanBeUsedThisEmojiAsReaction: [],
-				});
+				try {
+					const driveFile = await this.driveService.addFile({
+						user: null,
+						path: emojiPath,
+						name: record.fileName,
+						force: true,
+					});
+					await this.customEmojiService.add({
+						name: emojiInfo.name,
+						category: emojiInfo.category,
+						host: null,
+						aliases: emojiInfo.aliases,
+						driveFile,
+						license: emojiInfo.license,
+						isSensitive: emojiInfo.isSensitive,
+						localOnly: emojiInfo.localOnly,
+						roleIdsThatCanBeUsedThisEmojiAsReaction: [],
+					});
+				} catch (e) {
+					if (e instanceof Error || typeof e === 'string') {
+						this.logger.error(`couldn't import ${emojiPath} for ${emojiInfo.name}: ${e}`);
+					}
+					continue;
+				}
 			}
 
 			cleanup();
diff --git a/packages/backend/src/queue/processors/InboxProcessorService.ts b/packages/backend/src/queue/processors/InboxProcessorService.ts
index fa7009f8f5..95d764e4d8 100644
--- a/packages/backend/src/queue/processors/InboxProcessorService.ts
+++ b/packages/backend/src/queue/processors/InboxProcessorService.ts
@@ -4,11 +4,10 @@
  */
 
 import { URL } from 'node:url';
-import { Injectable } from '@nestjs/common';
+import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
 import httpSignature from '@peertube/http-signature';
 import * as Bull from 'bullmq';
 import type Logger from '@/logger.js';
-import { MetaService } from '@/core/MetaService.js';
 import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
 import { FetchInstanceMetadataService } from '@/core/FetchInstanceMetadataService.js';
 import InstanceChart from '@/core/chart/charts/instance.js';
@@ -26,16 +25,28 @@ import { JsonLdService } from '@/core/activitypub/JsonLdService.js';
 import { ApInboxService } from '@/core/activitypub/ApInboxService.js';
 import { bindThis } from '@/decorators.js';
 import { IdentifiableError } from '@/misc/identifiable-error.js';
+import { CollapsedQueue } from '@/misc/collapsed-queue.js';
+import { MiNote } from '@/models/Note.js';
+import { MiMeta } from '@/models/Meta.js';
+import { DI } from '@/di-symbols.js';
 import { QueueLoggerService } from '../QueueLoggerService.js';
 import type { InboxJobData } from '../types.js';
 
+type UpdateInstanceJob = {
+	latestRequestReceivedAt: Date,
+	shouldUnsuspend: boolean,
+};
+
 @Injectable()
-export class InboxProcessorService {
+export class InboxProcessorService implements OnApplicationShutdown {
 	private logger: Logger;
+	private updateInstanceQueue: CollapsedQueue<MiNote['id'], UpdateInstanceJob>;
 
 	constructor(
+		@Inject(DI.meta)
+		private meta: MiMeta,
+
 		private utilityService: UtilityService,
-		private metaService: MetaService,
 		private apInboxService: ApInboxService,
 		private federatedInstanceService: FederatedInstanceService,
 		private fetchInstanceMetadataService: FetchInstanceMetadataService,
@@ -48,6 +59,7 @@ export class InboxProcessorService {
 		private queueLoggerService: QueueLoggerService,
 	) {
 		this.logger = this.queueLoggerService.logger.createSubLogger('inbox');
+		this.updateInstanceQueue = new CollapsedQueue(process.env.NODE_ENV !== 'test' ? 60 * 1000 * 5 : 0, this.collapseUpdateInstanceJobs, this.performUpdateInstance);
 	}
 
 	@bindThis
@@ -63,9 +75,7 @@ export class InboxProcessorService {
 
 		const host = this.utilityService.toPuny(new URL(signature.keyId).hostname);
 
-		// ブロックしてたら中断
-		const meta = await this.metaService.fetch();
-		if (this.utilityService.isBlockedHost(meta.blockedHosts, host)) {
+		if (!this.utilityService.isFederationAllowedHost(host)) {
 			return `Blocked request: ${host}`;
 		}
 
@@ -164,9 +174,8 @@ export class InboxProcessorService {
 					throw new Bull.UnrecoverableError(`skip: LD-Signature user(${authUser.user.uri}) !== activity.actor(${activity.actor})`);
 				}
 
-				// ブロックしてたら中断
 				const ldHost = this.utilityService.extractDbHost(authUser.user.uri);
-				if (this.utilityService.isBlockedHost(meta.blockedHosts, ldHost)) {
+				if (!this.utilityService.isFederationAllowedHost(ldHost)) {
 					throw new Bull.UnrecoverableError(`Blocked request: ${ldHost}`);
 				}
 			} else {
@@ -183,23 +192,27 @@ export class InboxProcessorService {
 			}
 		}
 
-		// Update stats
-		this.federatedInstanceService.fetch(authUser.user.host).then(i => {
-			this.federatedInstanceService.update(i.id, {
+		this.apRequestChart.inbox();
+		this.federationChart.inbox(authUser.user.host);
+
+		// Update instance stats
+		process.nextTick(async () => {
+			const i = await (this.meta.enableStatsForFederatedInstances
+				? this.federatedInstanceService.fetchOrRegister(authUser.user.host)
+				: this.federatedInstanceService.fetch(authUser.user.host));
+
+			if (i == null) return;
+
+			this.updateInstanceQueue.enqueue(i.id, {
 				latestRequestReceivedAt: new Date(),
-				isNotResponding: false,
-				// もしサーバーが死んでるために配信が止まっていた場合には自動的に復活させてあげる
-				suspensionState: i.suspensionState === 'autoSuspendedForNotResponding' ? 'none' : undefined,
+				shouldUnsuspend: i.suspensionState === 'autoSuspendedForNotResponding',
 			});
 
-			this.fetchInstanceMetadataService.fetchInstanceMetadata(i);
-
-			this.apRequestChart.inbox();
-			this.federationChart.inbox(i.host);
-
-			if (meta.enableChartsForFederatedInstances) {
+			if (this.meta.enableChartsForFederatedInstances) {
 				this.instanceChart.requestReceived(i.host);
 			}
+
+			this.fetchInstanceMetadataService.fetchInstanceMetadata(i);
 		});
 
 		// アクティビティを処理
@@ -225,4 +238,36 @@ export class InboxProcessorService {
 		}
 		return 'ok';
 	}
+
+	@bindThis
+	public collapseUpdateInstanceJobs(oldJob: UpdateInstanceJob, newJob: UpdateInstanceJob) {
+		const latestRequestReceivedAt = oldJob.latestRequestReceivedAt < newJob.latestRequestReceivedAt
+			? newJob.latestRequestReceivedAt
+			: oldJob.latestRequestReceivedAt;
+		const shouldUnsuspend = oldJob.shouldUnsuspend || newJob.shouldUnsuspend;
+		return {
+			latestRequestReceivedAt,
+			shouldUnsuspend,
+		};
+	}
+
+	@bindThis
+	public async performUpdateInstance(id: string, job: UpdateInstanceJob) {
+		await this.federatedInstanceService.update(id, {
+			latestRequestReceivedAt: new Date(),
+			isNotResponding: false,
+			// もしサーバーが死んでるために配信が止まっていた場合には自動的に復活させてあげる
+			suspensionState: job.shouldUnsuspend ? 'none' : undefined,
+		});
+	}
+
+	@bindThis
+	public async dispose(): Promise<void> {
+		await this.updateInstanceQueue.performAllNow();
+	}
+
+	@bindThis
+	async onApplicationShutdown(signal?: string) {
+		await this.dispose();
+	}
 }
diff --git a/packages/backend/src/queue/processors/SystemWebhookDeliverProcessorService.ts b/packages/backend/src/queue/processors/SystemWebhookDeliverProcessorService.ts
new file mode 100644
index 0000000000..f6bef52684
--- /dev/null
+++ b/packages/backend/src/queue/processors/SystemWebhookDeliverProcessorService.ts
@@ -0,0 +1,87 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Inject, Injectable } from '@nestjs/common';
+import * as Bull from 'bullmq';
+import { DI } from '@/di-symbols.js';
+import type { SystemWebhooksRepository } from '@/models/_.js';
+import type { Config } from '@/config.js';
+import type Logger from '@/logger.js';
+import { HttpRequestService } from '@/core/HttpRequestService.js';
+import { StatusError } from '@/misc/status-error.js';
+import { bindThis } from '@/decorators.js';
+import { QueueLoggerService } from '../QueueLoggerService.js';
+import { SystemWebhookDeliverJobData } from '../types.js';
+
+@Injectable()
+export class SystemWebhookDeliverProcessorService {
+	private logger: Logger;
+
+	constructor(
+		@Inject(DI.config)
+		private config: Config,
+
+		@Inject(DI.systemWebhooksRepository)
+		private systemWebhooksRepository: SystemWebhooksRepository,
+
+		private httpRequestService: HttpRequestService,
+		private queueLoggerService: QueueLoggerService,
+	) {
+		this.logger = this.queueLoggerService.logger.createSubLogger('webhook');
+	}
+
+	@bindThis
+	public async process(job: Bull.Job<SystemWebhookDeliverJobData>): Promise<string> {
+		try {
+			this.logger.debug(`delivering ${job.data.webhookId}`);
+
+			const res = await this.httpRequestService.send(job.data.to, {
+				method: 'POST',
+				headers: {
+					'User-Agent': 'Misskey-Hooks',
+					'X-Misskey-Host': this.config.host,
+					'X-Misskey-Hook-Id': job.data.webhookId,
+					'X-Misskey-Hook-Secret': job.data.secret,
+					'Content-Type': 'application/json',
+				},
+				body: JSON.stringify({
+					server: this.config.url,
+					hookId: job.data.webhookId,
+					eventId: job.data.eventId,
+					createdAt: job.data.createdAt,
+					type: job.data.type,
+					body: job.data.content,
+				}),
+			});
+
+			this.systemWebhooksRepository.update({ id: job.data.webhookId }, {
+				latestSentAt: new Date(),
+				latestStatus: res.status,
+			});
+
+			return 'Success';
+		} catch (res) {
+			this.logger.error(res as Error);
+
+			this.systemWebhooksRepository.update({ id: job.data.webhookId }, {
+				latestSentAt: new Date(),
+				latestStatus: res instanceof StatusError ? res.statusCode : 1,
+			});
+
+			if (res instanceof StatusError) {
+				// 4xx
+				if (!res.isRetryable) {
+					throw new Bull.UnrecoverableError(`${res.statusCode} ${res.statusMessage}`);
+				}
+
+				// 5xx etc.
+				throw new Error(`${res.statusCode} ${res.statusMessage}`);
+			} else {
+				// DNS error, socket error, timeout ...
+				throw res;
+			}
+		}
+	}
+}
diff --git a/packages/backend/src/queue/processors/WebhookDeliverProcessorService.ts b/packages/backend/src/queue/processors/UserWebhookDeliverProcessorService.ts
similarity index 92%
rename from packages/backend/src/queue/processors/WebhookDeliverProcessorService.ts
rename to packages/backend/src/queue/processors/UserWebhookDeliverProcessorService.ts
index 8c260c0137..9ec630ef70 100644
--- a/packages/backend/src/queue/processors/WebhookDeliverProcessorService.ts
+++ b/packages/backend/src/queue/processors/UserWebhookDeliverProcessorService.ts
@@ -13,10 +13,10 @@ import { HttpRequestService } from '@/core/HttpRequestService.js';
 import { StatusError } from '@/misc/status-error.js';
 import { bindThis } from '@/decorators.js';
 import { QueueLoggerService } from '../QueueLoggerService.js';
-import type { WebhookDeliverJobData } from '../types.js';
+import { UserWebhookDeliverJobData } from '../types.js';
 
 @Injectable()
-export class WebhookDeliverProcessorService {
+export class UserWebhookDeliverProcessorService {
 	private logger: Logger;
 
 	constructor(
@@ -33,7 +33,7 @@ export class WebhookDeliverProcessorService {
 	}
 
 	@bindThis
-	public async process(job: Bull.Job<WebhookDeliverJobData>): Promise<string> {
+	public async process(job: Bull.Job<UserWebhookDeliverJobData>): Promise<string> {
 		try {
 			this.logger.debug(`delivering ${job.data.webhookId}`);
 
diff --git a/packages/backend/src/queue/types.ts b/packages/backend/src/queue/types.ts
index ce57ba745e..a4077a0547 100644
--- a/packages/backend/src/queue/types.ts
+++ b/packages/backend/src/queue/types.ts
@@ -106,7 +106,17 @@ export type EndedPollNotificationJobData = {
 	noteId: MiNote['id'];
 };
 
-export type WebhookDeliverJobData = {
+export type SystemWebhookDeliverJobData = {
+	type: string;
+	content: unknown;
+	webhookId: MiWebhook['id'];
+	to: string;
+	secret: string;
+	createdAt: number;
+	eventId: string;
+};
+
+export type UserWebhookDeliverJobData = {
 	type: string;
 	content: unknown;
 	webhookId: MiWebhook['id'];
diff --git a/packages/backend/src/server/FileServerService.ts b/packages/backend/src/server/FileServerService.ts
index 9db3aa1bfb..bf0a011699 100644
--- a/packages/backend/src/server/FileServerService.ts
+++ b/packages/backend/src/server/FileServerService.ts
@@ -53,7 +53,7 @@ export class FileServerService {
 		private internalStorageService: InternalStorageService,
 		private loggerService: LoggerService,
 	) {
-		this.logger = this.loggerService.getLogger('server', 'gray', false);
+		this.logger = this.loggerService.getLogger('server', 'gray');
 
 		//this.createServer = this.createServer.bind(this);
 	}
@@ -82,7 +82,7 @@ export class FileServerService {
 					.catch(err => this.errorHandler(request, reply, err));
 			});
 			fastify.get<{ Params: { key: string; } }>('/files/:key/*', async (request, reply) => {
-				return await reply.redirect(301, `${this.config.url}/files/${request.params.key}`);
+				return await reply.redirect(`${this.config.url}/files/${request.params.key}`, 301);
 			});
 			done();
 		});
@@ -147,12 +147,12 @@ export class FileServerService {
 						url.searchParams.set('static', '1');
 
 						file.cleanup();
-						return await reply.redirect(301, url.toString());
+						return await reply.redirect(url.toString(), 301);
 					} else if (file.mime.startsWith('video/')) {
 						const externalThumbnail = this.videoProcessingService.getExternalVideoThumbnailUrl(file.url);
 						if (externalThumbnail) {
 							file.cleanup();
-							return await reply.redirect(301, externalThumbnail);
+							return await reply.redirect(externalThumbnail, 301);
 						}
 
 						image = await this.videoProcessingService.generateVideoThumbnail(file.path);
@@ -167,7 +167,7 @@ export class FileServerService {
 						url.searchParams.set('url', file.url);
 
 						file.cleanup();
-						return await reply.redirect(301, url.toString());
+						return await reply.redirect(url.toString(), 301);
 					}
 				}
 
@@ -314,11 +314,17 @@ export class FileServerService {
 			}
 
 			return await reply.redirect(
-				301,
 				url.toString(),
+				301,
 			);
 		}
 
+		if (!request.headers['user-agent']) {
+			throw new StatusError('User-Agent is required', 400, 'User-Agent is required');
+		} else if (request.headers['user-agent'].toLowerCase().indexOf('misskey/') !== -1) {
+			throw new StatusError('Refusing to proxy a request from another proxy', 403, 'Proxy is recursive');
+		}
+
 		// Create temp file
 		const file = await this.getStreamAndTypeFromUrl(url);
 		if (file === '404') {
diff --git a/packages/backend/src/server/HealthServerService.ts b/packages/backend/src/server/HealthServerService.ts
index 2c3ed85925..5980609f02 100644
--- a/packages/backend/src/server/HealthServerService.ts
+++ b/packages/backend/src/server/HealthServerService.ts
@@ -27,6 +27,9 @@ export class HealthServerService {
 		@Inject(DI.redisForTimelines)
 		private redisForTimelines: Redis.Redis,
 
+		@Inject(DI.redisForReactions)
+		private redisForReactions: Redis.Redis,
+
 		@Inject(DI.db)
 		private db: DataSource,
 
@@ -43,6 +46,7 @@ export class HealthServerService {
 				this.redisForPub.ping(),
 				this.redisForSub.ping(),
 				this.redisForTimelines.ping(),
+				this.redisForReactions.ping(),
 				this.db.query('SELECT 1'),
 				...(this.meilisearch ? [this.meilisearch.health()] : []),
 			]).then(() => 200, () => 503));
diff --git a/packages/backend/src/server/NodeinfoServerService.ts b/packages/backend/src/server/NodeinfoServerService.ts
index cc18997fdc..9a641007ee 100644
--- a/packages/backend/src/server/NodeinfoServerService.ts
+++ b/packages/backend/src/server/NodeinfoServerService.ts
@@ -134,7 +134,7 @@ export class NodeinfoServerService {
 			return document;
 		};
 
-		const cache = new MemorySingleCache<Awaited<ReturnType<typeof nodeinfo2>>>(1000 * 60 * 10);
+		const cache = new MemorySingleCache<Awaited<ReturnType<typeof nodeinfo2>>>(1000 * 60 * 10); // 10m
 
 		fastify.get(nodeinfo2_1path, async (request, reply) => {
 			const base = await cache.fetch(() => nodeinfo2(21));
diff --git a/packages/backend/src/server/ServerModule.ts b/packages/backend/src/server/ServerModule.ts
index 12d5061985..3ab0b815f2 100644
--- a/packages/backend/src/server/ServerModule.ts
+++ b/packages/backend/src/server/ServerModule.ts
@@ -46,6 +46,7 @@ import { UserListChannelService } from './api/stream/channels/user-list.js';
 import { RoleTimelineChannelService } from './api/stream/channels/role-timeline.js';
 import { ReversiChannelService } from './api/stream/channels/reversi.js';
 import { ReversiGameChannelService } from './api/stream/channels/reversi-game.js';
+import { SigninWithPasskeyApiService } from './api/SigninWithPasskeyApiService.js';
 
 @Module({
 	imports: [
@@ -71,6 +72,7 @@ import { ReversiGameChannelService } from './api/stream/channels/reversi-game.js
 		AuthenticateService,
 		RateLimiterService,
 		SigninApiService,
+		SigninWithPasskeyApiService,
 		SigninService,
 		SignupApiService,
 		StreamingApiServerService,
diff --git a/packages/backend/src/server/ServerService.ts b/packages/backend/src/server/ServerService.ts
index 3572f16627..fd2bd3267d 100644
--- a/packages/backend/src/server/ServerService.ts
+++ b/packages/backend/src/server/ServerService.ts
@@ -13,7 +13,7 @@ import fastifyRawBody from 'fastify-raw-body';
 import { IsNull } from 'typeorm';
 import { GlobalEventService } from '@/core/GlobalEventService.js';
 import type { Config } from '@/config.js';
-import type { EmojisRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js';
+import type { EmojisRepository, MiMeta, UserProfilesRepository, UsersRepository } from '@/models/_.js';
 import { DI } from '@/di-symbols.js';
 import type Logger from '@/logger.js';
 import * as Acct from '@/misc/acct.js';
@@ -21,7 +21,6 @@ import { genIdenticon } from '@/misc/gen-identicon.js';
 import { UserEntityService } from '@/core/entities/UserEntityService.js';
 import { LoggerService } from '@/core/LoggerService.js';
 import { bindThis } from '@/decorators.js';
-import { MetaService } from '@/core/MetaService.js';
 import { ActivityPubServerService } from './ActivityPubServerService.js';
 import { NodeinfoServerService } from './NodeinfoServerService.js';
 import { ApiServerService } from './api/ApiServerService.js';
@@ -44,6 +43,9 @@ export class ServerService implements OnApplicationShutdown {
 		@Inject(DI.config)
 		private config: Config,
 
+		@Inject(DI.meta)
+		private meta: MiMeta,
+
 		@Inject(DI.usersRepository)
 		private usersRepository: UsersRepository,
 
@@ -53,7 +55,6 @@ export class ServerService implements OnApplicationShutdown {
 		@Inject(DI.emojisRepository)
 		private emojisRepository: EmojisRepository,
 
-		private metaService: MetaService,
 		private userEntityService: UserEntityService,
 		private apiServerService: ApiServerService,
 		private openApiServerService: OpenApiServerService,
@@ -68,7 +69,7 @@ export class ServerService implements OnApplicationShutdown {
 		private loggerService: LoggerService,
 		private oauth2ProviderService: OAuth2ProviderService,
 	) {
-		this.logger = this.loggerService.getLogger('server', 'gray', false);
+		this.logger = this.loggerService.getLogger('server', 'gray');
 	}
 
 	@bindThis
@@ -165,8 +166,8 @@ export class ServerService implements OnApplicationShutdown {
 			}
 
 			return await reply.redirect(
-				301,
 				url.toString(),
+				301,
 			);
 		});
 
@@ -193,7 +194,7 @@ export class ServerService implements OnApplicationShutdown {
 			reply.header('Content-Type', 'image/png');
 			reply.header('Cache-Control', 'public, max-age=86400');
 
-			if ((await this.metaService.fetch()).enableIdenticonGeneration) {
+			if (this.meta.enableIdenticonGeneration) {
 				return await genIdenticon(request.params.x);
 			} else {
 				return reply.redirect('/static-assets/avatar.png');
diff --git a/packages/backend/src/server/api/ApiCallService.ts b/packages/backend/src/server/api/ApiCallService.ts
index 271ef80554..aad833f126 100644
--- a/packages/backend/src/server/api/ApiCallService.ts
+++ b/packages/backend/src/server/api/ApiCallService.ts
@@ -13,8 +13,7 @@ import { getIpHash } from '@/misc/get-ip-hash.js';
 import type { MiLocalUser, MiUser } from '@/models/User.js';
 import type { MiAccessToken } from '@/models/AccessToken.js';
 import type Logger from '@/logger.js';
-import type { UserIpsRepository } from '@/models/_.js';
-import { MetaService } from '@/core/MetaService.js';
+import type { MiMeta, UserIpsRepository } from '@/models/_.js';
 import { createTemp } from '@/misc/create-temp.js';
 import { bindThis } from '@/decorators.js';
 import { RoleService } from '@/core/RoleService.js';
@@ -40,13 +39,15 @@ export class ApiCallService implements OnApplicationShutdown {
 	private userIpHistoriesClearIntervalId: NodeJS.Timeout;
 
 	constructor(
+		@Inject(DI.meta)
+		private meta: MiMeta,
+
 		@Inject(DI.config)
 		private config: Config,
 
 		@Inject(DI.userIpsRepository)
 		private userIpsRepository: UserIpsRepository,
 
-		private metaService: MetaService,
 		private authenticateService: AuthenticateService,
 		private rateLimiterService: RateLimiterService,
 		private roleService: RoleService,
@@ -64,6 +65,16 @@ export class ApiCallService implements OnApplicationShutdown {
 		let statusCode = err.httpStatusCode;
 		if (err.httpStatusCode === 401) {
 			reply.header('WWW-Authenticate', 'Bearer realm="Misskey"');
+		} else if (err.code === 'RATE_LIMIT_EXCEEDED') {
+			const info: unknown = err.info;
+			const unixEpochInSeconds = Date.now();
+			if (typeof(info) === 'object' && info && 'resetMs' in info && typeof(info.resetMs) === 'number') {
+				const cooldownInSeconds = Math.ceil((info.resetMs - unixEpochInSeconds) / 1000);
+				// もしかするとマイナスになる可能性がなくはないのでマイナスだったら0にしておく
+				reply.header('Retry-After', Math.max(cooldownInSeconds, 0).toString(10));
+			} else {
+				this.logger.warn(`rate limit information has unexpected type ${typeof(err.info?.reset)}`);
+			}
 		} else if (err.kind === 'client') {
 			reply.header('WWW-Authenticate', `Bearer realm="Misskey", error="invalid_request", error_description="${err.message}"`);
 			statusCode = statusCode ?? 400;
@@ -93,7 +104,7 @@ export class ApiCallService implements OnApplicationShutdown {
 		}
 	}
 
-	#onExecError(ep: IEndpoint, data: any, err: Error): void {
+	#onExecError(ep: IEndpoint, data: any, err: Error, userId?: MiUser['id']): void {
 		if (err instanceof ApiError || err instanceof AuthenticationError) {
 			throw err;
 		} else {
@@ -108,10 +119,13 @@ export class ApiCallService implements OnApplicationShutdown {
 					id: errId,
 				},
 			});
-			console.error(err, errId);
 
 			if (this.config.sentryForBackend) {
 				Sentry.captureMessage(`Internal error occurred in ${ep.name}: ${err.message}`, {
+					level: 'error',
+					user: {
+						id: userId,
+					},
 					extra: {
 						ep: ep.name,
 						ps: data,
@@ -186,9 +200,18 @@ export class ApiCallService implements OnApplicationShutdown {
 			return;
 		}
 
-		const [path] = await createTemp();
+		const [path, cleanup] = await createTemp();
 		await stream.pipeline(multipartData.file, fs.createWriteStream(path));
 
+		// ファイルサイズが制限を超えていた場合
+		// なお truncated はストリームを読み切ってからでないと機能しないため、stream.pipeline より後にある必要がある
+		if (multipartData.file.truncated) {
+			cleanup();
+			reply.code(413);
+			reply.send();
+			return;
+		}
+
 		const fields = {} as Record<string, unknown>;
 		for (const [k, v] of Object.entries(multipartData.fields)) {
 			fields[k] = typeof v === 'object' && 'value' in v ? v.value : undefined;
@@ -243,9 +266,8 @@ export class ApiCallService implements OnApplicationShutdown {
 	}
 
 	@bindThis
-	private async logIp(request: FastifyRequest, user: MiLocalUser) {
-		const meta = await this.metaService.fetch();
-		if (!meta.enableIpLogging) return;
+	private logIp(request: FastifyRequest, user: MiLocalUser) {
+		if (!this.meta.enableIpLogging) return;
 		const ip = request.ip;
 		const ips = this.userIpHistories.get(user.id);
 		if (ips == null || !ips.has(ip)) {
@@ -305,12 +327,17 @@ export class ApiCallService implements OnApplicationShutdown {
 			if (factor > 0) {
 				// Rate limit
 				await this.rateLimiterService.limit(limit as IEndpointMeta['limit'] & { key: NonNullable<string> }, limitActor, factor).catch(err => {
-					throw new ApiError({
-						message: 'Rate limit exceeded. Please try again later.',
-						code: 'RATE_LIMIT_EXCEEDED',
-						id: 'd5826d14-3982-4d2e-8011-b9e9f02499ef',
-						httpStatusCode: 429,
-					});
+					if ('info' in err) {
+						// errはLimiter.LimiterInfoであることが期待される
+						throw new ApiError({
+							message: 'Rate limit exceeded. Please try again later.',
+							code: 'RATE_LIMIT_EXCEEDED',
+							id: 'd5826d14-3982-4d2e-8011-b9e9f02499ef',
+							httpStatusCode: 429,
+						}, err.info);
+					} else {
+						throw new TypeError('information must be a rate-limiter information.');
+					}
 				});
 			}
 		}
@@ -410,9 +437,13 @@ export class ApiCallService implements OnApplicationShutdown {
 
 		// API invoking
 		if (this.config.sentryForBackend) {
-			return await Sentry.startSpan({ name: 'API: ' + ep.name }, () => ep.exec(data, user, token, file, request.ip, request.headers).catch((err: Error) => this.#onExecError(ep, data, err)));
+			return await Sentry.startSpan({
+				name: 'API: ' + ep.name,
+			}, () => ep.exec(data, user, token, file, request.ip, request.headers)
+				.catch((err: Error) => this.#onExecError(ep, data, err, user?.id)));
 		} else {
-			return await ep.exec(data, user, token, file, request.ip, request.headers).catch((err: Error) => this.#onExecError(ep, data, err));
+			return await ep.exec(data, user, token, file, request.ip, request.headers)
+				.catch((err: Error) => this.#onExecError(ep, data, err, user?.id));
 		}
 	}
 
diff --git a/packages/backend/src/server/api/ApiServerService.ts b/packages/backend/src/server/api/ApiServerService.ts
index 4a5935f930..3a8cb19f01 100644
--- a/packages/backend/src/server/api/ApiServerService.ts
+++ b/packages/backend/src/server/api/ApiServerService.ts
@@ -8,6 +8,7 @@ import cors from '@fastify/cors';
 import multipart from '@fastify/multipart';
 import fastifyCookie from '@fastify/cookie';
 import { ModuleRef } from '@nestjs/core';
+import { AuthenticationResponseJSON } from '@simplewebauthn/types';
 import type { Config } from '@/config.js';
 import type { InstancesRepository, AccessTokensRepository } from '@/models/_.js';
 import { DI } from '@/di-symbols.js';
@@ -17,6 +18,7 @@ import endpoints from './endpoints.js';
 import { ApiCallService } from './ApiCallService.js';
 import { SignupApiService } from './SignupApiService.js';
 import { SigninApiService } from './SigninApiService.js';
+import { SigninWithPasskeyApiService } from './SigninWithPasskeyApiService.js';
 import type { FastifyInstance, FastifyPluginOptions } from 'fastify';
 
 @Injectable()
@@ -37,6 +39,7 @@ export class ApiServerService {
 		private apiCallService: ApiCallService,
 		private signupApiService: SignupApiService,
 		private signinApiService: SigninApiService,
+		private signinWithPasskeyApiService: SigninWithPasskeyApiService,
 	) {
 		//this.createServer = this.createServer.bind(this);
 	}
@@ -49,7 +52,7 @@ export class ApiServerService {
 
 		fastify.register(multipart, {
 			limits: {
-				fileSize: this.config.maxFileSize ?? 262144000,
+				fileSize: this.config.maxFileSize,
 				files: 1,
 			},
 		});
@@ -115,21 +118,31 @@ export class ApiServerService {
 				'hcaptcha-response'?: string;
 				'g-recaptcha-response'?: string;
 				'turnstile-response'?: string;
+				'm-captcha-response'?: string;
+				'testcaptcha-response'?: string;
 			}
 		}>('/signup', (request, reply) => this.signupApiService.signup(request, reply));
 
 		fastify.post<{
 			Body: {
 				username: string;
-				password: string;
+				password?: string;
 				token?: string;
-				signature?: string;
-				authenticatorData?: string;
-				clientDataJSON?: string;
-				credentialId?: string;
-				challengeId?: string;
+				credential?: AuthenticationResponseJSON;
+				'hcaptcha-response'?: string;
+				'g-recaptcha-response'?: string;
+				'turnstile-response'?: string;
+				'm-captcha-response'?: string;
+				'testcaptcha-response'?: string;
 			};
-		}>('/signin', (request, reply) => this.signinApiService.signin(request, reply));
+		}>('/signin-flow', (request, reply) => this.signinApiService.signin(request, reply));
+
+		fastify.post<{
+			Body: {
+				credential?: AuthenticationResponseJSON;
+				context?: string;
+			};
+		}>('/signin-with-passkey', (request, reply) => this.signinWithPasskeyApiService.signin(request, reply));
 
 		fastify.post<{ Body: { code: string; } }>('/signup-pending', (request, reply) => this.signupApiService.signupPending(request, reply));
 
diff --git a/packages/backend/src/server/api/AuthenticateService.ts b/packages/backend/src/server/api/AuthenticateService.ts
index ddef8db987..690ff2e022 100644
--- a/packages/backend/src/server/api/AuthenticateService.ts
+++ b/packages/backend/src/server/api/AuthenticateService.ts
@@ -37,7 +37,7 @@ export class AuthenticateService implements OnApplicationShutdown {
 
 		private cacheService: CacheService,
 	) {
-		this.appCache = new MemoryKVCache<MiApp>(Infinity);
+		this.appCache = new MemoryKVCache<MiApp>(1000 * 60 * 60 * 24 * 7); // 1w
 	}
 
 	@bindThis
diff --git a/packages/backend/src/server/api/EndpointsModule.ts b/packages/backend/src/server/api/EndpointsModule.ts
index c645f4bcc6..3557fa40a5 100644
--- a/packages/backend/src/server/api/EndpointsModule.ts
+++ b/packages/backend/src/server/api/EndpointsModule.ts
@@ -6,8 +6,13 @@
 import { Module } from '@nestjs/common';
 
 import { CoreModule } from '@/core/CoreModule.js';
-import * as ep___admin_meta from './endpoints/admin/meta.js';
+import * as ep___admin_abuseReport_notificationRecipient_list from '@/server/api/endpoints/admin/abuse-report/notification-recipient/list.js';
+import * as ep___admin_abuseReport_notificationRecipient_show from '@/server/api/endpoints/admin/abuse-report/notification-recipient/show.js';
+import * as ep___admin_abuseReport_notificationRecipient_create from '@/server/api/endpoints/admin/abuse-report/notification-recipient/create.js';
+import * as ep___admin_abuseReport_notificationRecipient_update from '@/server/api/endpoints/admin/abuse-report/notification-recipient/update.js';
+import * as ep___admin_abuseReport_notificationRecipient_delete from '@/server/api/endpoints/admin/abuse-report/notification-recipient/delete.js';
 import * as ep___admin_abuseUserReports from './endpoints/admin/abuse-user-reports.js';
+import * as ep___admin_meta from './endpoints/admin/meta.js';
 import * as ep___admin_accounts_create from './endpoints/admin/accounts/create.js';
 import * as ep___admin_accounts_delete from './endpoints/admin/accounts/delete.js';
 import * as ep___admin_accounts_findByEmail from './endpoints/admin/accounts/find-by-email.js';
@@ -63,6 +68,8 @@ import * as ep___admin_relays_list from './endpoints/admin/relays/list.js';
 import * as ep___admin_relays_remove from './endpoints/admin/relays/remove.js';
 import * as ep___admin_resetPassword from './endpoints/admin/reset-password.js';
 import * as ep___admin_resolveAbuseUserReport from './endpoints/admin/resolve-abuse-user-report.js';
+import * as ep___admin_forwardAbuseUserReport from './endpoints/admin/forward-abuse-user-report.js';
+import * as ep___admin_updateAbuseUserReport from './endpoints/admin/update-abuse-user-report.js';
 import * as ep___admin_sendEmail from './endpoints/admin/send-email.js';
 import * as ep___admin_serverInfo from './endpoints/admin/server-info.js';
 import * as ep___admin_showModerationLogs from './endpoints/admin/show-moderation-logs.js';
@@ -82,6 +89,12 @@ import * as ep___admin_roles_assign from './endpoints/admin/roles/assign.js';
 import * as ep___admin_roles_unassign from './endpoints/admin/roles/unassign.js';
 import * as ep___admin_roles_updateDefaultPolicies from './endpoints/admin/roles/update-default-policies.js';
 import * as ep___admin_roles_users from './endpoints/admin/roles/users.js';
+import * as ep___admin_systemWebhook_create from './endpoints/admin/system-webhook/create.js';
+import * as ep___admin_systemWebhook_delete from './endpoints/admin/system-webhook/delete.js';
+import * as ep___admin_systemWebhook_list from './endpoints/admin/system-webhook/list.js';
+import * as ep___admin_systemWebhook_show from './endpoints/admin/system-webhook/show.js';
+import * as ep___admin_systemWebhook_update from './endpoints/admin/system-webhook/update.js';
+import * as ep___admin_systemWebhook_test from './endpoints/admin/system-webhook/test.js';
 import * as ep___announcements from './endpoints/announcements.js';
 import * as ep___announcements_show from './endpoints/announcements/show.js';
 import * as ep___antennas_create from './endpoints/antennas/create.js';
@@ -248,6 +261,7 @@ import * as ep___i_webhooks_show from './endpoints/i/webhooks/show.js';
 import * as ep___i_webhooks_list from './endpoints/i/webhooks/list.js';
 import * as ep___i_webhooks_update from './endpoints/i/webhooks/update.js';
 import * as ep___i_webhooks_delete from './endpoints/i/webhooks/delete.js';
+import * as ep___i_webhooks_test from './endpoints/i/webhooks/test.js';
 import * as ep___invite_create from './endpoints/invite/create.js';
 import * as ep___invite_delete from './endpoints/invite/delete.js';
 import * as ep___invite_list from './endpoints/invite/list.js';
@@ -381,6 +395,11 @@ import type { Provider } from '@nestjs/common';
 
 const $admin_meta: Provider = { provide: 'ep:admin/meta', useClass: ep___admin_meta.default };
 const $admin_abuseUserReports: Provider = { provide: 'ep:admin/abuse-user-reports', useClass: ep___admin_abuseUserReports.default };
+const $admin_abuseReport_notificationRecipient_list: Provider = { provide: 'ep:admin/abuse-report/notification-recipient/list', useClass: ep___admin_abuseReport_notificationRecipient_list.default };
+const $admin_abuseReport_notificationRecipient_show: Provider = { provide: 'ep:admin/abuse-report/notification-recipient/show', useClass: ep___admin_abuseReport_notificationRecipient_show.default };
+const $admin_abuseReport_notificationRecipient_create: Provider = { provide: 'ep:admin/abuse-report/notification-recipient/create', useClass: ep___admin_abuseReport_notificationRecipient_create.default };
+const $admin_abuseReport_notificationRecipient_update: Provider = { provide: 'ep:admin/abuse-report/notification-recipient/update', useClass: ep___admin_abuseReport_notificationRecipient_update.default };
+const $admin_abuseReport_notificationRecipient_delete: Provider = { provide: 'ep:admin/abuse-report/notification-recipient/delete', useClass: ep___admin_abuseReport_notificationRecipient_delete.default };
 const $admin_accounts_create: Provider = { provide: 'ep:admin/accounts/create', useClass: ep___admin_accounts_create.default };
 const $admin_accounts_delete: Provider = { provide: 'ep:admin/accounts/delete', useClass: ep___admin_accounts_delete.default };
 const $admin_accounts_findByEmail: Provider = { provide: 'ep:admin/accounts/find-by-email', useClass: ep___admin_accounts_findByEmail.default };
@@ -436,6 +455,8 @@ const $admin_relays_list: Provider = { provide: 'ep:admin/relays/list', useClass
 const $admin_relays_remove: Provider = { provide: 'ep:admin/relays/remove', useClass: ep___admin_relays_remove.default };
 const $admin_resetPassword: Provider = { provide: 'ep:admin/reset-password', useClass: ep___admin_resetPassword.default };
 const $admin_resolveAbuseUserReport: Provider = { provide: 'ep:admin/resolve-abuse-user-report', useClass: ep___admin_resolveAbuseUserReport.default };
+const $admin_forwardAbuseUserReport: Provider = { provide: 'ep:admin/forward-abuse-user-report', useClass: ep___admin_forwardAbuseUserReport.default };
+const $admin_updateAbuseUserReport: Provider = { provide: 'ep:admin/update-abuse-user-report', useClass: ep___admin_updateAbuseUserReport.default };
 const $admin_sendEmail: Provider = { provide: 'ep:admin/send-email', useClass: ep___admin_sendEmail.default };
 const $admin_serverInfo: Provider = { provide: 'ep:admin/server-info', useClass: ep___admin_serverInfo.default };
 const $admin_showModerationLogs: Provider = { provide: 'ep:admin/show-moderation-logs', useClass: ep___admin_showModerationLogs.default };
@@ -455,6 +476,12 @@ const $admin_roles_assign: Provider = { provide: 'ep:admin/roles/assign', useCla
 const $admin_roles_unassign: Provider = { provide: 'ep:admin/roles/unassign', useClass: ep___admin_roles_unassign.default };
 const $admin_roles_updateDefaultPolicies: Provider = { provide: 'ep:admin/roles/update-default-policies', useClass: ep___admin_roles_updateDefaultPolicies.default };
 const $admin_roles_users: Provider = { provide: 'ep:admin/roles/users', useClass: ep___admin_roles_users.default };
+const $admin_systemWebhook_create: Provider = { provide: 'ep:admin/system-webhook/create', useClass: ep___admin_systemWebhook_create.default };
+const $admin_systemWebhook_delete: Provider = { provide: 'ep:admin/system-webhook/delete', useClass: ep___admin_systemWebhook_delete.default };
+const $admin_systemWebhook_list: Provider = { provide: 'ep:admin/system-webhook/list', useClass: ep___admin_systemWebhook_list.default };
+const $admin_systemWebhook_show: Provider = { provide: 'ep:admin/system-webhook/show', useClass: ep___admin_systemWebhook_show.default };
+const $admin_systemWebhook_update: Provider = { provide: 'ep:admin/system-webhook/update', useClass: ep___admin_systemWebhook_update.default };
+const $admin_systemWebhook_test: Provider = { provide: 'ep:admin/system-webhook/test', useClass: ep___admin_systemWebhook_test.default };
 const $announcements: Provider = { provide: 'ep:announcements', useClass: ep___announcements.default };
 const $announcements_show: Provider = { provide: 'ep:announcements/show', useClass: ep___announcements_show.default };
 const $antennas_create: Provider = { provide: 'ep:antennas/create', useClass: ep___antennas_create.default };
@@ -621,6 +648,7 @@ const $i_webhooks_list: Provider = { provide: 'ep:i/webhooks/list', useClass: ep
 const $i_webhooks_show: Provider = { provide: 'ep:i/webhooks/show', useClass: ep___i_webhooks_show.default };
 const $i_webhooks_update: Provider = { provide: 'ep:i/webhooks/update', useClass: ep___i_webhooks_update.default };
 const $i_webhooks_delete: Provider = { provide: 'ep:i/webhooks/delete', useClass: ep___i_webhooks_delete.default };
+const $i_webhooks_test: Provider = { provide: 'ep:i/webhooks/test', useClass: ep___i_webhooks_test.default };
 const $invite_create: Provider = { provide: 'ep:invite/create', useClass: ep___invite_create.default };
 const $invite_delete: Provider = { provide: 'ep:invite/delete', useClass: ep___invite_delete.default };
 const $invite_list: Provider = { provide: 'ep:invite/list', useClass: ep___invite_list.default };
@@ -758,6 +786,11 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
 		ApiLoggerService,
 		$admin_meta,
 		$admin_abuseUserReports,
+		$admin_abuseReport_notificationRecipient_list,
+		$admin_abuseReport_notificationRecipient_show,
+		$admin_abuseReport_notificationRecipient_create,
+		$admin_abuseReport_notificationRecipient_update,
+		$admin_abuseReport_notificationRecipient_delete,
 		$admin_accounts_create,
 		$admin_accounts_delete,
 		$admin_accounts_findByEmail,
@@ -813,6 +846,8 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
 		$admin_relays_remove,
 		$admin_resetPassword,
 		$admin_resolveAbuseUserReport,
+		$admin_forwardAbuseUserReport,
+		$admin_updateAbuseUserReport,
 		$admin_sendEmail,
 		$admin_serverInfo,
 		$admin_showModerationLogs,
@@ -832,6 +867,12 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
 		$admin_roles_unassign,
 		$admin_roles_updateDefaultPolicies,
 		$admin_roles_users,
+		$admin_systemWebhook_create,
+		$admin_systemWebhook_delete,
+		$admin_systemWebhook_list,
+		$admin_systemWebhook_show,
+		$admin_systemWebhook_update,
+		$admin_systemWebhook_test,
 		$announcements,
 		$announcements_show,
 		$antennas_create,
@@ -998,6 +1039,7 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
 		$i_webhooks_show,
 		$i_webhooks_update,
 		$i_webhooks_delete,
+		$i_webhooks_test,
 		$invite_create,
 		$invite_delete,
 		$invite_list,
@@ -1129,6 +1171,11 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
 	exports: [
 		$admin_meta,
 		$admin_abuseUserReports,
+		$admin_abuseReport_notificationRecipient_list,
+		$admin_abuseReport_notificationRecipient_show,
+		$admin_abuseReport_notificationRecipient_create,
+		$admin_abuseReport_notificationRecipient_update,
+		$admin_abuseReport_notificationRecipient_delete,
 		$admin_accounts_create,
 		$admin_accounts_delete,
 		$admin_accounts_findByEmail,
@@ -1184,6 +1231,8 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
 		$admin_relays_remove,
 		$admin_resetPassword,
 		$admin_resolveAbuseUserReport,
+		$admin_forwardAbuseUserReport,
+		$admin_updateAbuseUserReport,
 		$admin_sendEmail,
 		$admin_serverInfo,
 		$admin_showModerationLogs,
@@ -1203,6 +1252,12 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
 		$admin_roles_unassign,
 		$admin_roles_updateDefaultPolicies,
 		$admin_roles_users,
+		$admin_systemWebhook_create,
+		$admin_systemWebhook_delete,
+		$admin_systemWebhook_list,
+		$admin_systemWebhook_show,
+		$admin_systemWebhook_update,
+		$admin_systemWebhook_test,
 		$announcements,
 		$announcements_show,
 		$antennas_create,
@@ -1369,6 +1424,7 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
 		$i_webhooks_show,
 		$i_webhooks_update,
 		$i_webhooks_delete,
+		$i_webhooks_test,
 		$invite_create,
 		$invite_delete,
 		$invite_list,
diff --git a/packages/backend/src/server/api/GetterService.ts b/packages/backend/src/server/api/GetterService.ts
index bff3ab96f3..444e6db744 100644
--- a/packages/backend/src/server/api/GetterService.ts
+++ b/packages/backend/src/server/api/GetterService.ts
@@ -39,6 +39,17 @@ export class GetterService {
 		return note;
 	}
 
+	@bindThis
+	public async getNoteWithUser(noteId: MiNote['id']) {
+		const note = await this.notesRepository.findOne({ where: { id: noteId }, relations: ['user'] });
+
+		if (note == null) {
+			throw new IdentifiableError('9725d0ce-ba28-4dde-95a7-2cbb2c15de24', 'No such note.');
+		}
+
+		return note;
+	}
+
 	/**
 	 * Get user for API processing
 	 */
diff --git a/packages/backend/src/server/api/RateLimiterService.ts b/packages/backend/src/server/api/RateLimiterService.ts
index 0439cdfe5e..52d73baa0a 100644
--- a/packages/backend/src/server/api/RateLimiterService.ts
+++ b/packages/backend/src/server/api/RateLimiterService.ts
@@ -32,11 +32,13 @@ export class RateLimiterService {
 
 	@bindThis
 	public limit(limitation: IEndpointMeta['limit'] & { key: NonNullable<string> }, actor: string, factor = 1) {
-		return new Promise<void>((ok, reject) => {
-			if (this.disabled) ok();
+		{
+			if (this.disabled) {
+				return Promise.resolve();
+			}
 
 			// Short-term limit
-			const min = (): void => {
+			const min = new Promise<void>((ok, reject) => {
 				const minIntervalLimiter = new Limiter({
 					id: `${actor}:${limitation.key}:min`,
 					duration: limitation.minInterval! * factor,
@@ -46,25 +48,25 @@ export class RateLimiterService {
 
 				minIntervalLimiter.get((err, info) => {
 					if (err) {
-						return reject('ERR');
+						return reject({ code: 'ERR', info });
 					}
 
 					this.logger.debug(`${actor} ${limitation.key} min remaining: ${info.remaining}`);
 
 					if (info.remaining === 0) {
-						reject('BRIEF_REQUEST_INTERVAL');
+						return reject({ code: 'BRIEF_REQUEST_INTERVAL', info });
 					} else {
 						if (hasLongTermLimit) {
-							max();
+							return max.then(ok, reject);
 						} else {
-							ok();
+							return ok();
 						}
 					}
 				});
-			};
+			});
 
 			// Long term limit
-			const max = (): void => {
+			const max = new Promise<void>((ok, reject) => {
 				const limiter = new Limiter({
 					id: `${actor}:${limitation.key}`,
 					duration: limitation.duration! * factor,
@@ -74,18 +76,18 @@ export class RateLimiterService {
 
 				limiter.get((err, info) => {
 					if (err) {
-						return reject('ERR');
+						return reject({ code: 'ERR', info });
 					}
 
 					this.logger.debug(`${actor} ${limitation.key} max remaining: ${info.remaining}`);
 
 					if (info.remaining === 0) {
-						reject('RATE_LIMIT_EXCEEDED');
+						return reject({ code: 'RATE_LIMIT_EXCEEDED', info });
 					} else {
-						ok();
+						return ok();
 					}
 				});
-			};
+			});
 
 			const hasShortTermLimit = typeof limitation.minInterval === 'number';
 
@@ -94,12 +96,12 @@ export class RateLimiterService {
 				typeof limitation.max === 'number';
 
 			if (hasShortTermLimit) {
-				min();
+				return min;
 			} else if (hasLongTermLimit) {
-				max();
+				return max;
 			} else {
-				ok();
+				return Promise.resolve();
 			}
-		});
+		}
 	}
 }
diff --git a/packages/backend/src/server/api/SigninApiService.ts b/packages/backend/src/server/api/SigninApiService.ts
index edac9b3beb..1d983ca4bc 100644
--- a/packages/backend/src/server/api/SigninApiService.ts
+++ b/packages/backend/src/server/api/SigninApiService.ts
@@ -5,12 +5,14 @@
 
 import { Inject, Injectable } from '@nestjs/common';
 import bcrypt from 'bcryptjs';
-import * as OTPAuth from 'otpauth';
 import { IsNull } from 'typeorm';
+import * as Misskey from 'misskey-js';
 import { DI } from '@/di-symbols.js';
 import type {
+	MiMeta,
 	SigninsRepository,
 	UserProfilesRepository,
+	UserSecurityKeysRepository,
 	UsersRepository,
 } from '@/models/_.js';
 import type { Config } from '@/config.js';
@@ -20,6 +22,8 @@ import { IdService } from '@/core/IdService.js';
 import { bindThis } from '@/decorators.js';
 import { WebAuthnService } from '@/core/WebAuthnService.js';
 import { UserAuthService } from '@/core/UserAuthService.js';
+import { CaptchaService } from '@/core/CaptchaService.js';
+import { FastifyReplyError } from '@/misc/fastify-reply-error.js';
 import { RateLimiterService } from './RateLimiterService.js';
 import { SigninService } from './SigninService.js';
 import type { AuthenticationResponseJSON } from '@simplewebauthn/types';
@@ -31,12 +35,18 @@ export class SigninApiService {
 		@Inject(DI.config)
 		private config: Config,
 
+		@Inject(DI.meta)
+		private meta: MiMeta,
+
 		@Inject(DI.usersRepository)
 		private usersRepository: UsersRepository,
 
 		@Inject(DI.userProfilesRepository)
 		private userProfilesRepository: UserProfilesRepository,
 
+		@Inject(DI.userSecurityKeysRepository)
+		private userSecurityKeysRepository: UserSecurityKeysRepository,
+
 		@Inject(DI.signinsRepository)
 		private signinsRepository: SigninsRepository,
 
@@ -45,6 +55,7 @@ export class SigninApiService {
 		private signinService: SigninService,
 		private userAuthService: UserAuthService,
 		private webAuthnService: WebAuthnService,
+		private captchaService: CaptchaService,
 	) {
 	}
 
@@ -53,9 +64,14 @@ export class SigninApiService {
 		request: FastifyRequest<{
 			Body: {
 				username: string;
-				password: string;
+				password?: string;
 				token?: string;
 				credential?: AuthenticationResponseJSON;
+				'hcaptcha-response'?: string;
+				'g-recaptcha-response'?: string;
+				'turnstile-response'?: string;
+				'm-captcha-response'?: string;
+				'testcaptcha-response'?: string;
 			};
 		}>,
 		reply: FastifyReply,
@@ -92,11 +108,6 @@ export class SigninApiService {
 			return;
 		}
 
-		if (typeof password !== 'string') {
-			reply.code(400);
-			return;
-		}
-
 		if (token != null && typeof token !== 'string') {
 			reply.code(400);
 			return;
@@ -121,11 +132,32 @@ export class SigninApiService {
 		}
 
 		const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id });
+		const securityKeysAvailable = await this.userSecurityKeysRepository.countBy({ userId: user.id }).then(result => result >= 1);
+
+		if (password == null) {
+			reply.code(200);
+			if (profile.twoFactorEnabled) {
+				return {
+					finished: false,
+					next: 'password',
+				} satisfies Misskey.entities.SigninFlowResponse;
+			} else {
+				return {
+					finished: false,
+					next: 'captcha',
+				} satisfies Misskey.entities.SigninFlowResponse;
+			}
+		}
+
+		if (typeof password !== 'string') {
+			reply.code(400);
+			return;
+		}
 
 		// Compare password
 		const same = await bcrypt.compare(password, profile.password!);
 
-		const fail = async (status?: number, failure?: { id: string }) => {
+		const fail = async (status?: number, failure?: { id: string; }) => {
 			// Append signin history
 			await this.signinsRepository.insert({
 				id: this.idService.gen(),
@@ -139,6 +171,38 @@ export class SigninApiService {
 		};
 
 		if (!profile.twoFactorEnabled) {
+			if (process.env.NODE_ENV !== 'test') {
+				if (this.meta.enableHcaptcha && this.meta.hcaptchaSecretKey) {
+					await this.captchaService.verifyHcaptcha(this.meta.hcaptchaSecretKey, body['hcaptcha-response']).catch(err => {
+						throw new FastifyReplyError(400, err);
+					});
+				}
+
+				if (this.meta.enableMcaptcha && this.meta.mcaptchaSecretKey && this.meta.mcaptchaSitekey && this.meta.mcaptchaInstanceUrl) {
+					await this.captchaService.verifyMcaptcha(this.meta.mcaptchaSecretKey, this.meta.mcaptchaSitekey, this.meta.mcaptchaInstanceUrl, body['m-captcha-response']).catch(err => {
+						throw new FastifyReplyError(400, err);
+					});
+				}
+
+				if (this.meta.enableRecaptcha && this.meta.recaptchaSecretKey) {
+					await this.captchaService.verifyRecaptcha(this.meta.recaptchaSecretKey, body['g-recaptcha-response']).catch(err => {
+						throw new FastifyReplyError(400, err);
+					});
+				}
+
+				if (this.meta.enableTurnstile && this.meta.turnstileSecretKey) {
+					await this.captchaService.verifyTurnstile(this.meta.turnstileSecretKey, body['turnstile-response']).catch(err => {
+						throw new FastifyReplyError(400, err);
+					});
+				}
+
+				if (this.meta.enableTestcaptcha) {
+					await this.captchaService.verifyTestcaptcha(body['testcaptcha-response']).catch(err => {
+						throw new FastifyReplyError(400, err);
+					});
+				}
+			}
+
 			if (same) {
 				return this.signinService.signin(request, reply, user);
 			} else {
@@ -180,7 +244,7 @@ export class SigninApiService {
 					id: '93b86c4b-72f9-40eb-9815-798928603d1e',
 				});
 			}
-		} else {
+		} else if (securityKeysAvailable) {
 			if (!same && !profile.usePasswordLessLogin) {
 				return await fail(403, {
 					id: '932c904e-9460-45b7-9ce6-7ed33be7eb2c',
@@ -190,7 +254,23 @@ export class SigninApiService {
 			const authRequest = await this.webAuthnService.initiateAuthentication(user.id);
 
 			reply.code(200);
-			return authRequest;
+			return {
+				finished: false,
+				next: 'passkey',
+				authRequest,
+			} satisfies Misskey.entities.SigninFlowResponse;
+		} else {
+			if (!same || !profile.twoFactorEnabled) {
+				return await fail(403, {
+					id: '932c904e-9460-45b7-9ce6-7ed33be7eb2c',
+				});
+			} else {
+				reply.code(200);
+				return {
+					finished: false,
+					next: 'totp',
+				} satisfies Misskey.entities.SigninFlowResponse;
+			}
 		}
 		// never get here
 	}
diff --git a/packages/backend/src/server/api/SigninService.ts b/packages/backend/src/server/api/SigninService.ts
index 70306c3113..640356b50c 100644
--- a/packages/backend/src/server/api/SigninService.ts
+++ b/packages/backend/src/server/api/SigninService.ts
@@ -4,13 +4,16 @@
  */
 
 import { Inject, Injectable } from '@nestjs/common';
+import * as Misskey from 'misskey-js';
 import { DI } from '@/di-symbols.js';
-import type { SigninsRepository } from '@/models/_.js';
+import type { SigninsRepository, UserProfilesRepository } from '@/models/_.js';
 import { IdService } from '@/core/IdService.js';
 import type { MiLocalUser } from '@/models/User.js';
 import { GlobalEventService } from '@/core/GlobalEventService.js';
 import { SigninEntityService } from '@/core/entities/SigninEntityService.js';
 import { bindThis } from '@/decorators.js';
+import { EmailService } from '@/core/EmailService.js';
+import { NotificationService } from '@/core/NotificationService.js';
 import type { FastifyRequest, FastifyReply } from 'fastify';
 
 @Injectable()
@@ -19,7 +22,12 @@ export class SigninService {
 		@Inject(DI.signinsRepository)
 		private signinsRepository: SigninsRepository,
 
+		@Inject(DI.userProfilesRepository)
+		private userProfilesRepository: UserProfilesRepository,
+
 		private signinEntityService: SigninEntityService,
+		private emailService: EmailService,
+		private notificationService: NotificationService,
 		private idService: IdService,
 		private globalEventService: GlobalEventService,
 	) {
@@ -28,7 +36,8 @@ export class SigninService {
 	@bindThis
 	public signin(request: FastifyRequest, reply: FastifyReply, user: MiLocalUser) {
 		setImmediate(async () => {
-			// Append signin history
+			this.notificationService.createNotification(user.id, 'login', {});
+
 			const record = await this.signinsRepository.insertOne({
 				id: this.idService.gen(),
 				userId: user.id,
@@ -37,15 +46,22 @@ export class SigninService {
 				success: true,
 			});
 
-			// Publish signin event
 			this.globalEventService.publishMainStream(user.id, 'signin', await this.signinEntityService.pack(record));
+
+			const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id });
+			if (profile.email && profile.emailVerified) {
+				this.emailService.sendEmail(profile.email, 'New login / ログインがありました',
+					'There is a new login. If you do not recognize this login, update the security status of your account, including changing your password. / 新しいログインがありました。このログインに心当たりがない場合は、パスワードを変更するなど、アカウントのセキュリティ状態を更新してください。',
+					'There is a new login. If you do not recognize this login, update the security status of your account, including changing your password. / 新しいログインがありました。このログインに心当たりがない場合は、パスワードを変更するなど、アカウントのセキュリティ状態を更新してください。');
+			}
 		});
 
 		reply.code(200);
 		return {
+			finished: true,
 			id: user.id,
-			i: user.token,
-		};
+			i: user.token!,
+		} satisfies Misskey.entities.SigninFlowResponse;
 	}
 }
 
diff --git a/packages/backend/src/server/api/SigninWithPasskeyApiService.ts b/packages/backend/src/server/api/SigninWithPasskeyApiService.ts
new file mode 100644
index 0000000000..9ba23c54e2
--- /dev/null
+++ b/packages/backend/src/server/api/SigninWithPasskeyApiService.ts
@@ -0,0 +1,173 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { randomUUID } from 'crypto';
+import { Inject, Injectable } from '@nestjs/common';
+import { IsNull } from 'typeorm';
+import { DI } from '@/di-symbols.js';
+import type {
+	SigninsRepository,
+	UserProfilesRepository,
+	UsersRepository,
+} from '@/models/_.js';
+import type { Config } from '@/config.js';
+import { getIpHash } from '@/misc/get-ip-hash.js';
+import type { MiLocalUser, MiUser } from '@/models/User.js';
+import { IdService } from '@/core/IdService.js';
+import { bindThis } from '@/decorators.js';
+import { WebAuthnService } from '@/core/WebAuthnService.js';
+import Logger from '@/logger.js';
+import { LoggerService } from '@/core/LoggerService.js';
+import type { IdentifiableError } from '@/misc/identifiable-error.js';
+import { RateLimiterService } from './RateLimiterService.js';
+import { SigninService } from './SigninService.js';
+import type { AuthenticationResponseJSON } from '@simplewebauthn/types';
+import type { FastifyReply, FastifyRequest } from 'fastify';
+
+@Injectable()
+export class SigninWithPasskeyApiService {
+	private logger: Logger;
+	constructor(
+		@Inject(DI.config)
+		private config: Config,
+
+		@Inject(DI.usersRepository)
+		private usersRepository: UsersRepository,
+
+		@Inject(DI.userProfilesRepository)
+		private userProfilesRepository: UserProfilesRepository,
+
+		@Inject(DI.signinsRepository)
+		private signinsRepository: SigninsRepository,
+
+		private idService: IdService,
+		private rateLimiterService: RateLimiterService,
+		private signinService: SigninService,
+		private webAuthnService: WebAuthnService,
+		private loggerService: LoggerService,
+	) {
+		this.logger = this.loggerService.getLogger('PasskeyAuth');
+	}
+
+	@bindThis
+	public async signin(
+		request: FastifyRequest<{
+			Body: {
+				credential?: AuthenticationResponseJSON;
+				context?: string;
+			};
+		}>,
+		reply: FastifyReply,
+	) {
+		reply.header('Access-Control-Allow-Origin', this.config.url);
+		reply.header('Access-Control-Allow-Credentials', 'true');
+
+		const body = request.body;
+		const credential = body['credential'];
+
+		function error(status: number, error: { id: string }) {
+			reply.code(status);
+			return { error };
+		}
+
+		const fail = async (userId: MiUser['id'], status?: number, failure?: { id: string }) => {
+			// Append signin history
+			await this.signinsRepository.insert({
+				id: this.idService.gen(),
+				userId: userId,
+				ip: request.ip,
+				headers: request.headers as any,
+				success: false,
+			});
+			return error(status ?? 500, failure ?? { id: '4e30e80c-e338-45a0-8c8f-44455efa3b76' });
+		};
+
+		try {
+			// Not more than 1 API call per 250ms and not more than 100 attempts per 30min
+			// NOTE: 1 Sign-in require 2 API calls
+			await this.rateLimiterService.limit({ key: 'signin-with-passkey', duration: 60 * 30 * 1000, max: 200, minInterval: 250 }, getIpHash(request.ip));
+		} catch (err) {
+			reply.code(429);
+			return {
+				error: {
+					message: 'Too many failed attempts to sign in. Try again later.',
+					code: 'TOO_MANY_AUTHENTICATION_FAILURES',
+					id: '22d05606-fbcf-421a-a2db-b32610dcfd1b',
+				},
+			};
+		}
+
+		// Initiate Passkey Auth challenge with context
+		if (!credential) {
+			const context = randomUUID();
+			this.logger.info(`Initiate Passkey challenge: context: ${context}`);
+			const authChallengeOptions = {
+				option: await this.webAuthnService.initiateSignInWithPasskeyAuthentication(context),
+				context: context,
+			};
+			reply.code(200);
+			return authChallengeOptions;
+		}
+
+		const context = body.context;
+		if (!context || typeof context !== 'string') {
+			// If try Authentication without context
+			return error(400, {
+				id: '1658cc2e-4495-461f-aee4-d403cdf073c1',
+			});
+		}
+
+		this.logger.debug(`Try Sign-in with Passkey: context: ${context}`);
+
+		let authorizedUserId: MiUser['id'] | null;
+		try {
+			authorizedUserId = await this.webAuthnService.verifySignInWithPasskeyAuthentication(context, credential);
+		} catch (err) {
+			this.logger.warn(`Passkey challenge Verify error! : ${err}`);
+			const errorId = (err as IdentifiableError).id;
+			return error(403, {
+				id: errorId,
+			});
+		}
+
+		if (!authorizedUserId) {
+			return error(403, {
+				id: '932c904e-9460-45b7-9ce6-7ed33be7eb2c',
+			});
+		}
+
+		// Fetch user
+		const user = await this.usersRepository.findOneBy({
+			id: authorizedUserId,
+			host: IsNull(),
+		}) as MiLocalUser | null;
+
+		if (user == null) {
+			return error(403, {
+				id: '652f899f-66d4-490e-993e-6606c8ec04c3',
+			});
+		}
+
+		if (user.isSuspended) {
+			return error(403, {
+				id: 'e03a5f46-d309-4865-9b69-56282d94e1eb',
+			});
+		}
+
+		const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id });
+
+		// Authentication was successful, but passwordless login is not enabled
+		if (!profile.usePasswordLessLogin) {
+			return await fail(user.id, 403, {
+				id: '2d84773e-f7b7-4d0b-8f72-bb69b584c912',
+			});
+		}
+
+		const signinResponse = this.signinService.signin(request, reply, user);
+		return {
+			signinResponse: signinResponse,
+		};
+	}
+}
diff --git a/packages/backend/src/server/api/SignupApiService.ts b/packages/backend/src/server/api/SignupApiService.ts
index 632b0c62bc..3ec5e5d3e6 100644
--- a/packages/backend/src/server/api/SignupApiService.ts
+++ b/packages/backend/src/server/api/SignupApiService.ts
@@ -7,9 +7,8 @@ import { Inject, Injectable } from '@nestjs/common';
 import bcrypt from 'bcryptjs';
 import { IsNull } from 'typeorm';
 import { DI } from '@/di-symbols.js';
-import type { RegistrationTicketsRepository, UsedUsernamesRepository, UserPendingsRepository, UserProfilesRepository, UsersRepository, MiRegistrationTicket } from '@/models/_.js';
+import type { RegistrationTicketsRepository, UsedUsernamesRepository, UserPendingsRepository, UserProfilesRepository, UsersRepository, MiRegistrationTicket, MiMeta } from '@/models/_.js';
 import type { Config } from '@/config.js';
-import { MetaService } from '@/core/MetaService.js';
 import { CaptchaService } from '@/core/CaptchaService.js';
 import { IdService } from '@/core/IdService.js';
 import { SignupService } from '@/core/SignupService.js';
@@ -28,6 +27,9 @@ export class SignupApiService {
 		@Inject(DI.config)
 		private config: Config,
 
+		@Inject(DI.meta)
+		private meta: MiMeta,
+
 		@Inject(DI.usersRepository)
 		private usersRepository: UsersRepository,
 
@@ -45,7 +47,6 @@ export class SignupApiService {
 
 		private userEntityService: UserEntityService,
 		private idService: IdService,
-		private metaService: MetaService,
 		private captchaService: CaptchaService,
 		private signupService: SignupService,
 		private signinService: SigninService,
@@ -66,37 +67,42 @@ export class SignupApiService {
 				'g-recaptcha-response'?: string;
 				'turnstile-response'?: string;
 				'm-captcha-response'?: string;
+				'testcaptcha-response'?: string;
 			}
 		}>,
 		reply: FastifyReply,
 	) {
 		const body = request.body;
 
-		const instance = await this.metaService.fetch(true);
-
 		// Verify *Captcha
 		// ただしテスト時はこの機構は障害となるため無効にする
 		if (process.env.NODE_ENV !== 'test') {
-			if (instance.enableHcaptcha && instance.hcaptchaSecretKey) {
-				await this.captchaService.verifyHcaptcha(instance.hcaptchaSecretKey, body['hcaptcha-response']).catch(err => {
+			if (this.meta.enableHcaptcha && this.meta.hcaptchaSecretKey) {
+				await this.captchaService.verifyHcaptcha(this.meta.hcaptchaSecretKey, body['hcaptcha-response']).catch(err => {
 					throw new FastifyReplyError(400, err);
 				});
 			}
 
-			if (instance.enableMcaptcha && instance.mcaptchaSecretKey && instance.mcaptchaSitekey && instance.mcaptchaInstanceUrl) {
-				await this.captchaService.verifyMcaptcha(instance.mcaptchaSecretKey, instance.mcaptchaSitekey, instance.mcaptchaInstanceUrl, body['m-captcha-response']).catch(err => {
+			if (this.meta.enableMcaptcha && this.meta.mcaptchaSecretKey && this.meta.mcaptchaSitekey && this.meta.mcaptchaInstanceUrl) {
+				await this.captchaService.verifyMcaptcha(this.meta.mcaptchaSecretKey, this.meta.mcaptchaSitekey, this.meta.mcaptchaInstanceUrl, body['m-captcha-response']).catch(err => {
 					throw new FastifyReplyError(400, err);
 				});
 			}
 
-			if (instance.enableRecaptcha && instance.recaptchaSecretKey) {
-				await this.captchaService.verifyRecaptcha(instance.recaptchaSecretKey, body['g-recaptcha-response']).catch(err => {
+			if (this.meta.enableRecaptcha && this.meta.recaptchaSecretKey) {
+				await this.captchaService.verifyRecaptcha(this.meta.recaptchaSecretKey, body['g-recaptcha-response']).catch(err => {
 					throw new FastifyReplyError(400, err);
 				});
 			}
 
-			if (instance.enableTurnstile && instance.turnstileSecretKey) {
-				await this.captchaService.verifyTurnstile(instance.turnstileSecretKey, body['turnstile-response']).catch(err => {
+			if (this.meta.enableTurnstile && this.meta.turnstileSecretKey) {
+				await this.captchaService.verifyTurnstile(this.meta.turnstileSecretKey, body['turnstile-response']).catch(err => {
+					throw new FastifyReplyError(400, err);
+				});
+			}
+
+			if (this.meta.enableTestcaptcha) {
+				await this.captchaService.verifyTestcaptcha(body['testcaptcha-response']).catch(err => {
 					throw new FastifyReplyError(400, err);
 				});
 			}
@@ -108,7 +114,7 @@ export class SignupApiService {
 		const invitationCode = body['invitationCode'];
 		const emailAddress = body['emailAddress'];
 
-		if (instance.emailRequiredForSignup) {
+		if (this.meta.emailRequiredForSignup) {
 			if (emailAddress == null || typeof emailAddress !== 'string') {
 				reply.code(400);
 				return;
@@ -123,7 +129,7 @@ export class SignupApiService {
 
 		let ticket: MiRegistrationTicket | null = null;
 
-		if (instance.disableRegistration) {
+		if (this.meta.disableRegistration) {
 			if (invitationCode == null || typeof invitationCode !== 'string') {
 				reply.code(400);
 				return;
@@ -144,7 +150,7 @@ export class SignupApiService {
 			}
 
 			// メアド認証が有効の場合
-			if (instance.emailRequiredForSignup) {
+			if (this.meta.emailRequiredForSignup) {
 				// メアド認証済みならエラー
 				if (ticket.usedBy) {
 					reply.code(400);
@@ -162,7 +168,7 @@ export class SignupApiService {
 			}
 		}
 
-		if (instance.emailRequiredForSignup) {
+		if (this.meta.emailRequiredForSignup) {
 			if (await this.usersRepository.exists({ where: { usernameLower: username.toLowerCase(), host: IsNull() } })) {
 				throw new FastifyReplyError(400, 'DUPLICATED_USERNAME');
 			}
@@ -172,7 +178,7 @@ export class SignupApiService {
 				throw new FastifyReplyError(400, 'USED_USERNAME');
 			}
 
-			const isPreserved = instance.preservedUsernames.map(x => x.toLowerCase()).includes(username.toLowerCase());
+			const isPreserved = this.meta.preservedUsernames.map(x => x.toLowerCase()).includes(username.toLowerCase());
 			if (isPreserved) {
 				throw new FastifyReplyError(400, 'DENIED_USERNAME');
 			}
diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts
index a38c62f35a..49b07d6ced 100644
--- a/packages/backend/src/server/api/endpoints.ts
+++ b/packages/backend/src/server/api/endpoints.ts
@@ -6,8 +6,18 @@
 import { permissions } from 'misskey-js';
 import type { KeyOf, Schema } from '@/misc/json-schema.js';
 
-import * as ep___admin_meta from './endpoints/admin/meta.js';
+import * as ep___admin_abuseReport_notificationRecipient_list
+	from '@/server/api/endpoints/admin/abuse-report/notification-recipient/list.js';
+import * as ep___admin_abuseReport_notificationRecipient_show
+	from '@/server/api/endpoints/admin/abuse-report/notification-recipient/show.js';
+import * as ep___admin_abuseReport_notificationRecipient_create
+	from '@/server/api/endpoints/admin/abuse-report/notification-recipient/create.js';
+import * as ep___admin_abuseReport_notificationRecipient_update
+	from '@/server/api/endpoints/admin/abuse-report/notification-recipient/update.js';
+import * as ep___admin_abuseReport_notificationRecipient_delete
+	from '@/server/api/endpoints/admin/abuse-report/notification-recipient/delete.js';
 import * as ep___admin_abuseUserReports from './endpoints/admin/abuse-user-reports.js';
+import * as ep___admin_meta from './endpoints/admin/meta.js';
 import * as ep___admin_accounts_create from './endpoints/admin/accounts/create.js';
 import * as ep___admin_accounts_delete from './endpoints/admin/accounts/delete.js';
 import * as ep___admin_accounts_findByEmail from './endpoints/admin/accounts/find-by-email.js';
@@ -44,7 +54,8 @@ import * as ep___admin_emoji_setCategoryBulk from './endpoints/admin/emoji/set-c
 import * as ep___admin_emoji_setLicenseBulk from './endpoints/admin/emoji/set-license-bulk.js';
 import * as ep___admin_emoji_update from './endpoints/admin/emoji/update.js';
 import * as ep___admin_federation_deleteAllFiles from './endpoints/admin/federation/delete-all-files.js';
-import * as ep___admin_federation_refreshRemoteInstanceMetadata from './endpoints/admin/federation/refresh-remote-instance-metadata.js';
+import * as ep___admin_federation_refreshRemoteInstanceMetadata
+	from './endpoints/admin/federation/refresh-remote-instance-metadata.js';
 import * as ep___admin_federation_removeAllFollowing from './endpoints/admin/federation/remove-all-following.js';
 import * as ep___admin_federation_updateInstance from './endpoints/admin/federation/update-instance.js';
 import * as ep___admin_getIndexStats from './endpoints/admin/get-index-stats.js';
@@ -63,6 +74,8 @@ import * as ep___admin_relays_list from './endpoints/admin/relays/list.js';
 import * as ep___admin_relays_remove from './endpoints/admin/relays/remove.js';
 import * as ep___admin_resetPassword from './endpoints/admin/reset-password.js';
 import * as ep___admin_resolveAbuseUserReport from './endpoints/admin/resolve-abuse-user-report.js';
+import * as ep___admin_forwardAbuseUserReport from './endpoints/admin/forward-abuse-user-report.js';
+import * as ep___admin_updateAbuseUserReport from './endpoints/admin/update-abuse-user-report.js';
 import * as ep___admin_sendEmail from './endpoints/admin/send-email.js';
 import * as ep___admin_serverInfo from './endpoints/admin/server-info.js';
 import * as ep___admin_showModerationLogs from './endpoints/admin/show-moderation-logs.js';
@@ -82,6 +95,12 @@ import * as ep___admin_roles_assign from './endpoints/admin/roles/assign.js';
 import * as ep___admin_roles_unassign from './endpoints/admin/roles/unassign.js';
 import * as ep___admin_roles_updateDefaultPolicies from './endpoints/admin/roles/update-default-policies.js';
 import * as ep___admin_roles_users from './endpoints/admin/roles/users.js';
+import * as ep___admin_systemWebhook_create from './endpoints/admin/system-webhook/create.js';
+import * as ep___admin_systemWebhook_delete from './endpoints/admin/system-webhook/delete.js';
+import * as ep___admin_systemWebhook_list from './endpoints/admin/system-webhook/list.js';
+import * as ep___admin_systemWebhook_show from './endpoints/admin/system-webhook/show.js';
+import * as ep___admin_systemWebhook_update from './endpoints/admin/system-webhook/update.js';
+import * as ep___admin_systemWebhook_test from './endpoints/admin/system-webhook/test.js';
 import * as ep___announcements from './endpoints/announcements.js';
 import * as ep___announcements_show from './endpoints/announcements/show.js';
 import * as ep___antennas_create from './endpoints/antennas/create.js';
@@ -248,6 +267,7 @@ import * as ep___i_webhooks_show from './endpoints/i/webhooks/show.js';
 import * as ep___i_webhooks_list from './endpoints/i/webhooks/list.js';
 import * as ep___i_webhooks_update from './endpoints/i/webhooks/update.js';
 import * as ep___i_webhooks_delete from './endpoints/i/webhooks/delete.js';
+import * as ep___i_webhooks_test from './endpoints/i/webhooks/test.js';
 import * as ep___invite_create from './endpoints/invite/create.js';
 import * as ep___invite_delete from './endpoints/invite/delete.js';
 import * as ep___invite_list from './endpoints/invite/list.js';
@@ -379,6 +399,11 @@ import * as ep___reversi_verify from './endpoints/reversi/verify.js';
 const eps = [
 	['admin/meta', ep___admin_meta],
 	['admin/abuse-user-reports', ep___admin_abuseUserReports],
+	['admin/abuse-report/notification-recipient/list', ep___admin_abuseReport_notificationRecipient_list],
+	['admin/abuse-report/notification-recipient/show', ep___admin_abuseReport_notificationRecipient_show],
+	['admin/abuse-report/notification-recipient/create', ep___admin_abuseReport_notificationRecipient_create],
+	['admin/abuse-report/notification-recipient/update', ep___admin_abuseReport_notificationRecipient_update],
+	['admin/abuse-report/notification-recipient/delete', ep___admin_abuseReport_notificationRecipient_delete],
 	['admin/accounts/create', ep___admin_accounts_create],
 	['admin/accounts/delete', ep___admin_accounts_delete],
 	['admin/accounts/find-by-email', ep___admin_accounts_findByEmail],
@@ -434,6 +459,8 @@ const eps = [
 	['admin/relays/remove', ep___admin_relays_remove],
 	['admin/reset-password', ep___admin_resetPassword],
 	['admin/resolve-abuse-user-report', ep___admin_resolveAbuseUserReport],
+	['admin/forward-abuse-user-report', ep___admin_forwardAbuseUserReport],
+	['admin/update-abuse-user-report', ep___admin_updateAbuseUserReport],
 	['admin/send-email', ep___admin_sendEmail],
 	['admin/server-info', ep___admin_serverInfo],
 	['admin/show-moderation-logs', ep___admin_showModerationLogs],
@@ -453,6 +480,12 @@ const eps = [
 	['admin/roles/unassign', ep___admin_roles_unassign],
 	['admin/roles/update-default-policies', ep___admin_roles_updateDefaultPolicies],
 	['admin/roles/users', ep___admin_roles_users],
+	['admin/system-webhook/create', ep___admin_systemWebhook_create],
+	['admin/system-webhook/delete', ep___admin_systemWebhook_delete],
+	['admin/system-webhook/list', ep___admin_systemWebhook_list],
+	['admin/system-webhook/show', ep___admin_systemWebhook_show],
+	['admin/system-webhook/update', ep___admin_systemWebhook_update],
+	['admin/system-webhook/test', ep___admin_systemWebhook_test],
 	['announcements', ep___announcements],
 	['announcements/show', ep___announcements_show],
 	['antennas/create', ep___antennas_create],
@@ -619,6 +652,7 @@ const eps = [
 	['i/webhooks/show', ep___i_webhooks_show],
 	['i/webhooks/update', ep___i_webhooks_update],
 	['i/webhooks/delete', ep___i_webhooks_delete],
+	['i/webhooks/test', ep___i_webhooks_test],
 	['invite/create', ep___invite_create],
 	['invite/delete', ep___invite_delete],
 	['invite/list', ep___invite_list],
@@ -873,8 +907,12 @@ export interface IEndpoint {
 const endpoints: IEndpoint[] = (eps as [string, any]).map(([name, ep]) => {
 	return {
 		name: name,
-		get meta() { return ep.meta ?? {}; },
-		get params() { return ep.paramDef; },
+		get meta() {
+			return ep.meta ?? {};
+		},
+		get params() {
+			return ep.paramDef;
+		},
 	};
 });
 
diff --git a/packages/backend/src/server/api/endpoints/admin/abuse-report/notification-recipient/create.ts b/packages/backend/src/server/api/endpoints/admin/abuse-report/notification-recipient/create.ts
new file mode 100644
index 0000000000..bdfbcba518
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/admin/abuse-report/notification-recipient/create.ts
@@ -0,0 +1,122 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Inject, Injectable } from '@nestjs/common';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import { ApiError } from '@/server/api/error.js';
+import {
+	AbuseReportNotificationRecipientEntityService,
+} from '@/core/entities/AbuseReportNotificationRecipientEntityService.js';
+import { AbuseReportNotificationService } from '@/core/AbuseReportNotificationService.js';
+import { DI } from '@/di-symbols.js';
+import type { UserProfilesRepository } from '@/models/_.js';
+
+export const meta = {
+	tags: ['admin', 'abuse-report', 'notification-recipient'],
+
+	requireCredential: true,
+	requireModerator: true,
+	secure: true,
+	kind: 'write:admin:abuse-report:notification-recipient',
+
+	res: {
+		type: 'object',
+		ref: 'AbuseReportNotificationRecipient',
+	},
+
+	errors: {
+		correlationCheckEmail: {
+			message: 'If "method" is email, "userId" must be set.',
+			code: 'CORRELATION_CHECK_EMAIL',
+			id: '348bb8ae-575a-6fe9-4327-5811999def8f',
+			httpStatusCode: 400,
+		},
+		correlationCheckWebhook: {
+			message: 'If "method" is webhook, "systemWebhookId" must be set.',
+			code: 'CORRELATION_CHECK_WEBHOOK',
+			id: 'b0c15051-de2d-29ef-260c-9585cddd701a',
+			httpStatusCode: 400,
+		},
+		emailAddressNotSet: {
+			message: 'Email address is not set.',
+			code: 'EMAIL_ADDRESS_NOT_SET',
+			id: '7cc1d85e-2f58-fc31-b644-3de8d0d3421f',
+			httpStatusCode: 400,
+		},
+	},
+} as const;
+
+export const paramDef = {
+	type: 'object',
+	properties: {
+		isActive: {
+			type: 'boolean',
+		},
+		name: {
+			type: 'string',
+			minLength: 1,
+			maxLength: 255,
+		},
+		method: {
+			type: 'string',
+			enum: ['email', 'webhook'],
+		},
+		userId: {
+			type: 'string',
+			format: 'misskey:id',
+		},
+		systemWebhookId: {
+			type: 'string',
+			format: 'misskey:id',
+		},
+	},
+	required: [
+		'isActive',
+		'name',
+		'method',
+	],
+} as const;
+
+@Injectable()
+export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
+	constructor(
+		@Inject(DI.userProfilesRepository)
+		private userProfilesRepository: UserProfilesRepository,
+		private abuseReportNotificationService: AbuseReportNotificationService,
+		private abuseReportNotificationRecipientEntityService: AbuseReportNotificationRecipientEntityService,
+	) {
+		super(meta, paramDef, async (ps, me) => {
+			if (ps.method === 'email') {
+				const userProfile = await this.userProfilesRepository.findOneBy({ userId: ps.userId });
+				if (!ps.userId || !userProfile) {
+					throw new ApiError(meta.errors.correlationCheckEmail);
+				}
+
+				if (!userProfile.email || !userProfile.emailVerified) {
+					throw new ApiError(meta.errors.emailAddressNotSet);
+				}
+			}
+
+			if (ps.method === 'webhook' && !ps.systemWebhookId) {
+				throw new ApiError(meta.errors.correlationCheckWebhook);
+			}
+
+			const userId = ps.method === 'email' ? ps.userId : null;
+			const systemWebhookId = ps.method === 'webhook' ? ps.systemWebhookId : null;
+			const result = await this.abuseReportNotificationService.createRecipient(
+				{
+					isActive: ps.isActive,
+					name: ps.name,
+					method: ps.method,
+					userId: userId ?? null,
+					systemWebhookId: systemWebhookId ?? null,
+				},
+				me,
+			);
+
+			return this.abuseReportNotificationRecipientEntityService.pack(result);
+		});
+	}
+}
diff --git a/packages/backend/src/server/api/endpoints/admin/abuse-report/notification-recipient/delete.ts b/packages/backend/src/server/api/endpoints/admin/abuse-report/notification-recipient/delete.ts
new file mode 100644
index 0000000000..b6dc44e09c
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/admin/abuse-report/notification-recipient/delete.ts
@@ -0,0 +1,44 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Injectable } from '@nestjs/common';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import { AbuseReportNotificationService } from '@/core/AbuseReportNotificationService.js';
+
+export const meta = {
+	tags: ['admin', 'abuse-report', 'notification-recipient'],
+
+	requireCredential: true,
+	requireModerator: true,
+	secure: true,
+	kind: 'write:admin:abuse-report:notification-recipient',
+} as const;
+
+export const paramDef = {
+	type: 'object',
+	properties: {
+		id: {
+			type: 'string',
+			format: 'misskey:id',
+		},
+	},
+	required: [
+		'id',
+	],
+} as const;
+
+@Injectable()
+export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
+	constructor(
+		private abuseReportNotificationService: AbuseReportNotificationService,
+	) {
+		super(meta, paramDef, async (ps, me) => {
+			await this.abuseReportNotificationService.deleteRecipient(
+				ps.id,
+				me,
+			);
+		});
+	}
+}
diff --git a/packages/backend/src/server/api/endpoints/admin/abuse-report/notification-recipient/list.ts b/packages/backend/src/server/api/endpoints/admin/abuse-report/notification-recipient/list.ts
new file mode 100644
index 0000000000..dad9161a8a
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/admin/abuse-report/notification-recipient/list.ts
@@ -0,0 +1,55 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Injectable } from '@nestjs/common';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import {
+	AbuseReportNotificationRecipientEntityService,
+} from '@/core/entities/AbuseReportNotificationRecipientEntityService.js';
+import { AbuseReportNotificationService } from '@/core/AbuseReportNotificationService.js';
+
+export const meta = {
+	tags: ['admin', 'abuse-report', 'notification-recipient'],
+
+	requireCredential: true,
+	requireModerator: true,
+	secure: true,
+	kind: 'read:admin:abuse-report:notification-recipient',
+
+	res: {
+		type: 'array',
+		items: {
+			type: 'object',
+			ref: 'AbuseReportNotificationRecipient',
+		},
+	},
+} as const;
+
+export const paramDef = {
+	type: 'object',
+	properties: {
+		method: {
+			type: 'array',
+			items: {
+				type: 'string',
+				enum: ['email', 'webhook'],
+			},
+		},
+	},
+	required: [],
+} as const;
+
+@Injectable()
+export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
+	constructor(
+		private abuseReportNotificationService: AbuseReportNotificationService,
+		private abuseReportNotificationRecipientEntityService: AbuseReportNotificationRecipientEntityService,
+	) {
+		super(meta, paramDef, async (ps) => {
+			const recipients = await this.abuseReportNotificationService.fetchRecipients({ method: ps.method });
+			return this.abuseReportNotificationRecipientEntityService.packMany(recipients);
+		});
+	}
+}
diff --git a/packages/backend/src/server/api/endpoints/admin/abuse-report/notification-recipient/show.ts b/packages/backend/src/server/api/endpoints/admin/abuse-report/notification-recipient/show.ts
new file mode 100644
index 0000000000..557798f946
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/admin/abuse-report/notification-recipient/show.ts
@@ -0,0 +1,64 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Injectable } from '@nestjs/common';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import {
+	AbuseReportNotificationRecipientEntityService,
+} from '@/core/entities/AbuseReportNotificationRecipientEntityService.js';
+import { AbuseReportNotificationService } from '@/core/AbuseReportNotificationService.js';
+import { ApiError } from '@/server/api/error.js';
+
+export const meta = {
+	tags: ['admin', 'abuse-report', 'notification-recipient'],
+
+	requireCredential: true,
+	requireModerator: true,
+	secure: true,
+	kind: 'read:admin:abuse-report:notification-recipient',
+
+	res: {
+		type: 'object',
+		ref: 'AbuseReportNotificationRecipient',
+	},
+
+	errors: {
+		noSuchRecipient: {
+			message: 'No such recipient.',
+			code: 'NO_SUCH_RECIPIENT',
+			id: '013de6a8-f757-04cb-4d73-cc2a7e3368e4',
+			kind: 'server',
+			httpStatusCode: 404,
+		},
+	},
+} as const;
+
+export const paramDef = {
+	type: 'object',
+	properties: {
+		id: {
+			type: 'string',
+			format: 'misskey:id',
+		},
+	},
+	required: ['id'],
+} as const;
+
+@Injectable()
+export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
+	constructor(
+		private abuseReportNotificationService: AbuseReportNotificationService,
+		private abuseReportNotificationRecipientEntityService: AbuseReportNotificationRecipientEntityService,
+	) {
+		super(meta, paramDef, async (ps) => {
+			const recipients = await this.abuseReportNotificationService.fetchRecipients({ ids: [ps.id] });
+			if (recipients.length === 0) {
+				throw new ApiError(meta.errors.noSuchRecipient);
+			}
+
+			return this.abuseReportNotificationRecipientEntityService.pack(recipients[0]);
+		});
+	}
+}
diff --git a/packages/backend/src/server/api/endpoints/admin/abuse-report/notification-recipient/update.ts b/packages/backend/src/server/api/endpoints/admin/abuse-report/notification-recipient/update.ts
new file mode 100644
index 0000000000..bd4b485217
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/admin/abuse-report/notification-recipient/update.ts
@@ -0,0 +1,128 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Inject, Injectable } from '@nestjs/common';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import { ApiError } from '@/server/api/error.js';
+import {
+	AbuseReportNotificationRecipientEntityService,
+} from '@/core/entities/AbuseReportNotificationRecipientEntityService.js';
+import { AbuseReportNotificationService } from '@/core/AbuseReportNotificationService.js';
+import { DI } from '@/di-symbols.js';
+import type { UserProfilesRepository } from '@/models/_.js';
+
+export const meta = {
+	tags: ['admin', 'abuse-report', 'notification-recipient'],
+
+	requireCredential: true,
+	requireModerator: true,
+	secure: true,
+	kind: 'write:admin:abuse-report:notification-recipient',
+
+	res: {
+		type: 'object',
+		ref: 'AbuseReportNotificationRecipient',
+	},
+
+	errors: {
+		correlationCheckEmail: {
+			message: 'If "method" is email, "userId" must be set.',
+			code: 'CORRELATION_CHECK_EMAIL',
+			id: '348bb8ae-575a-6fe9-4327-5811999def8f',
+			httpStatusCode: 400,
+		},
+		correlationCheckWebhook: {
+			message: 'If "method" is webhook, "systemWebhookId" must be set.',
+			code: 'CORRELATION_CHECK_WEBHOOK',
+			id: 'b0c15051-de2d-29ef-260c-9585cddd701a',
+			httpStatusCode: 400,
+		},
+		emailAddressNotSet: {
+			message: 'Email address is not set.',
+			code: 'EMAIL_ADDRESS_NOT_SET',
+			id: '7cc1d85e-2f58-fc31-b644-3de8d0d3421f',
+			httpStatusCode: 400,
+		},
+	},
+} as const;
+
+export const paramDef = {
+	type: 'object',
+	properties: {
+		id: {
+			type: 'string',
+			format: 'misskey:id',
+		},
+		isActive: {
+			type: 'boolean',
+		},
+		name: {
+			type: 'string',
+			minLength: 1,
+			maxLength: 255,
+		},
+		method: {
+			type: 'string',
+			enum: ['email', 'webhook'],
+		},
+		userId: {
+			type: 'string',
+			format: 'misskey:id',
+		},
+		systemWebhookId: {
+			type: 'string',
+			format: 'misskey:id',
+		},
+	},
+	required: [
+		'id',
+		'isActive',
+		'name',
+		'method',
+	],
+} as const;
+
+@Injectable()
+export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
+	constructor(
+		@Inject(DI.userProfilesRepository)
+		private userProfilesRepository: UserProfilesRepository,
+		private abuseReportNotificationService: AbuseReportNotificationService,
+		private abuseReportNotificationRecipientEntityService: AbuseReportNotificationRecipientEntityService,
+	) {
+		super(meta, paramDef, async (ps, me) => {
+			if (ps.method === 'email') {
+				const userProfile = await this.userProfilesRepository.findOneBy({ userId: ps.userId });
+				if (!ps.userId || !userProfile) {
+					throw new ApiError(meta.errors.correlationCheckEmail);
+				}
+
+				if (!userProfile.email || !userProfile.emailVerified) {
+					throw new ApiError(meta.errors.emailAddressNotSet);
+				}
+			}
+
+			if (ps.method === 'webhook' && !ps.systemWebhookId) {
+				throw new ApiError(meta.errors.correlationCheckWebhook);
+			}
+
+			const userId = ps.method === 'email' ? ps.userId : null;
+			const systemWebhookId = ps.method === 'webhook' ? ps.systemWebhookId : null;
+			const result = await this.abuseReportNotificationService.updateRecipient(
+				{
+					id: ps.id,
+					isActive: ps.isActive,
+					name: ps.name,
+					method: ps.method,
+					userId: userId ?? null,
+					systemWebhookId: systemWebhookId ?? null,
+				},
+				me,
+			);
+
+			return this.abuseReportNotificationRecipientEntityService.pack(result);
+		});
+	}
+}
diff --git a/packages/backend/src/server/api/endpoints/admin/abuse-user-reports.ts b/packages/backend/src/server/api/endpoints/admin/abuse-user-reports.ts
index cf3f257ca6..0dbfaae054 100644
--- a/packages/backend/src/server/api/endpoints/admin/abuse-user-reports.ts
+++ b/packages/backend/src/server/api/endpoints/admin/abuse-user-reports.ts
@@ -71,9 +71,22 @@ export const meta = {
 				},
 				assignee: {
 					type: 'object',
-					nullable: true, optional: true,
+					nullable: true, optional: false,
 					ref: 'UserDetailedNotMe',
 				},
+				forwarded: {
+					type: 'boolean',
+					nullable: false, optional: false,
+				},
+				resolvedAs: {
+					type: 'string',
+					nullable: true, optional: false,
+					enum: ['accept', 'reject', null],
+				},
+				moderationNote: {
+					type: 'string',
+					nullable: false, optional: false,
+				},
 			},
 		},
 	},
@@ -88,7 +101,6 @@ export const paramDef = {
 		state: { type: 'string', nullable: true, default: null },
 		reporterOrigin: { type: 'string', enum: ['combined', 'local', 'remote'], default: 'combined' },
 		targetUserOrigin: { type: 'string', enum: ['combined', 'local', 'remote'], default: 'combined' },
-		forwarded: { type: 'boolean', default: false },
 	},
 	required: [],
 } as const;
diff --git a/packages/backend/src/server/api/endpoints/admin/accounts/create.ts b/packages/backend/src/server/api/endpoints/admin/accounts/create.ts
index a7e8a3b018..d30131a62f 100644
--- a/packages/backend/src/server/api/endpoints/admin/accounts/create.ts
+++ b/packages/backend/src/server/api/endpoints/admin/accounts/create.ts
@@ -12,11 +12,27 @@ import { UserEntityService } from '@/core/entities/UserEntityService.js';
 import { InstanceActorService } from '@/core/InstanceActorService.js';
 import { localUsernameSchema, passwordSchema } from '@/models/User.js';
 import { DI } from '@/di-symbols.js';
+import type { Config } from '@/config.js';
+import { ApiError } from '@/server/api/error.js';
 import { Packed } from '@/misc/json-schema.js';
 
 export const meta = {
 	tags: ['admin'],
 
+	errors: {
+		accessDenied: {
+			message: 'Access denied.',
+			code: 'ACCESS_DENIED',
+			id: '1fb7cb09-d46a-4fff-b8df-057708cce513',
+		},
+
+		wrongInitialPassword: {
+			message: 'Initial password is incorrect.',
+			code: 'INCORRECT_INITIAL_PASSWORD',
+			id: '97147c55-1ae1-4f6f-91d6-e1c3e0e76d62',
+		},
+	},
+
 	res: {
 		type: 'object',
 		optional: false, nullable: false,
@@ -35,6 +51,7 @@ export const paramDef = {
 	properties: {
 		username: localUsernameSchema,
 		password: passwordSchema,
+		setupPassword: { type: 'string', nullable: true },
 	},
 	required: ['username', 'password'],
 } as const;
@@ -42,6 +59,9 @@ export const paramDef = {
 @Injectable()
 export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
 	constructor(
+		@Inject(DI.config)
+		private config: Config,
+
 		@Inject(DI.usersRepository)
 		private usersRepository: UsersRepository,
 
@@ -52,7 +72,23 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 		super(meta, paramDef, async (ps, _me, token) => {
 			const me = _me ? await this.usersRepository.findOneByOrFail({ id: _me.id }) : null;
 			const realUsers = await this.instanceActorService.realLocalUsersPresent();
-			if ((realUsers && !me?.isRoot) || token !== null) throw new Error('access denied');
+
+			if (!realUsers && me == null && token == null) {
+				// 初回セットアップの場合
+				if (this.config.setupPassword != null) {
+					// 初期パスワードが設定されている場合
+					if (ps.setupPassword !== this.config.setupPassword) {
+						// 初期パスワードが違う場合
+						throw new ApiError(meta.errors.wrongInitialPassword);
+					}
+				} else if (ps.setupPassword != null && ps.setupPassword.trim() !== '') {
+					// 初期パスワードが設定されていないのに初期パスワードが入力された場合
+					throw new ApiError(meta.errors.wrongInitialPassword);
+				}
+			} else if ((realUsers && !me?.isRoot) || token !== null) {
+				// 初回セットアップではなく、管理者でない場合 or 外部トークンを使用している場合
+				throw new ApiError(meta.errors.accessDenied);
+			}
 
 			const { account, secret } = await this.signupService.signup({
 				username: ps.username,
diff --git a/packages/backend/src/server/api/endpoints/admin/accounts/delete.ts b/packages/backend/src/server/api/endpoints/admin/accounts/delete.ts
index 4074e416b8..01dea703a3 100644
--- a/packages/backend/src/server/api/endpoints/admin/accounts/delete.ts
+++ b/packages/backend/src/server/api/endpoints/admin/accounts/delete.ts
@@ -7,9 +7,9 @@ import { Inject, Injectable } from '@nestjs/common';
 import { Endpoint } from '@/server/api/endpoint-base.js';
 import type { UsersRepository } from '@/models/_.js';
 import { QueueService } from '@/core/QueueService.js';
-import { UserSuspendService } from '@/core/UserSuspendService.js';
 import { DI } from '@/di-symbols.js';
 import { UserEntityService } from '@/core/entities/UserEntityService.js';
+import { DeleteAccountService } from '@/core/DeleteAccountService.js';
 
 export const meta = {
 	tags: ['admin'],
@@ -33,9 +33,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 		@Inject(DI.usersRepository)
 		private usersRepository: UsersRepository,
 
-		private userEntityService: UserEntityService,
-		private queueService: QueueService,
-		private userSuspendService: UserSuspendService,
+		private deleteAccoountService: DeleteAccountService,
 	) {
 		super(meta, paramDef, async (ps, me) => {
 			const user = await this.usersRepository.findOneBy({ id: ps.userId });
@@ -48,22 +46,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 				throw new Error('cannot delete a root account');
 			}
 
-			if (this.userEntityService.isLocalUser(user)) {
-				// 物理削除する前にDelete activityを送信する
-				await this.userSuspendService.doPostSuspend(user).catch(err => {});
-
-				this.queueService.createDeleteAccountJob(user, {
-					soft: false,
-				});
-			} else {
-				this.queueService.createDeleteAccountJob(user, {
-					soft: true, // リモートユーザーの削除は、完全にDBから物理削除してしまうと再度連合してきてアカウントが復活する可能性があるため、soft指定する
-				});
-			}
-
-			await this.usersRepository.update(user.id, {
-				isDeleted: true,
-			});
+			await this.deleteAccoountService.deleteAccount(user);
 		});
 	}
 }
diff --git a/packages/backend/src/server/api/endpoints/admin/ad/update.ts b/packages/backend/src/server/api/endpoints/admin/ad/update.ts
index 62358457ff..4e3d731aca 100644
--- a/packages/backend/src/server/api/endpoints/admin/ad/update.ts
+++ b/packages/backend/src/server/api/endpoints/admin/ad/update.ts
@@ -40,7 +40,7 @@ export const paramDef = {
 		startsAt: { type: 'integer' },
 		dayOfWeek: { type: 'integer' },
 	},
-	required: ['id', 'memo', 'url', 'imageUrl', 'place', 'priority', 'ratio', 'expiresAt', 'startsAt', 'dayOfWeek'],
+	required: ['id'],
 } as const;
 
 @Injectable()
@@ -63,8 +63,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 				ratio: ps.ratio,
 				memo: ps.memo,
 				imageUrl: ps.imageUrl,
-				expiresAt: new Date(ps.expiresAt),
-				startsAt: new Date(ps.startsAt),
+				expiresAt: ps.expiresAt ? new Date(ps.expiresAt) : undefined,
+				startsAt: ps.startsAt ? new Date(ps.startsAt) : undefined,
 				dayOfWeek: ps.dayOfWeek,
 			});
 
diff --git a/packages/backend/src/server/api/endpoints/admin/announcements/list.ts b/packages/backend/src/server/api/endpoints/admin/announcements/list.ts
index 87eaad31a3..7596bf44e3 100644
--- a/packages/backend/src/server/api/endpoints/admin/announcements/list.ts
+++ b/packages/backend/src/server/api/endpoints/admin/announcements/list.ts
@@ -69,6 +69,7 @@ export const paramDef = {
 		sinceId: { type: 'string', format: 'misskey:id' },
 		untilId: { type: 'string', format: 'misskey:id' },
 		userId: { type: 'string', format: 'misskey:id', nullable: true },
+		status: { type: 'string', enum: ['all', 'active', 'archived'], default: 'active' },
 	},
 	required: [],
 } as const;
@@ -87,7 +88,13 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 	) {
 		super(meta, paramDef, async (ps, me) => {
 			const query = this.queryService.makePaginationQuery(this.announcementsRepository.createQueryBuilder('announcement'), ps.sinceId, ps.untilId);
-			query.andWhere('announcement.isActive = true');
+
+			if (ps.status === 'archived') {
+				query.andWhere('announcement.isActive = false');
+			} else if (ps.status === 'active') {
+				query.andWhere('announcement.isActive = true');
+			}
+
 			if (ps.userId) {
 				query.andWhere('announcement.userId = :userId', { userId: ps.userId });
 			} else {
diff --git a/packages/backend/src/server/api/endpoints/admin/avatar-decorations/create.ts b/packages/backend/src/server/api/endpoints/admin/avatar-decorations/create.ts
index fd21309818..87d80cbe80 100644
--- a/packages/backend/src/server/api/endpoints/admin/avatar-decorations/create.ts
+++ b/packages/backend/src/server/api/endpoints/admin/avatar-decorations/create.ts
@@ -6,6 +6,7 @@
 import { Injectable } from '@nestjs/common';
 import { Endpoint } from '@/server/api/endpoint-base.js';
 import { AvatarDecorationService } from '@/core/AvatarDecorationService.js';
+import { IdService } from '@/core/IdService.js';
 
 export const meta = {
 	tags: ['admin'],
@@ -13,6 +14,49 @@ export const meta = {
 	requireCredential: true,
 	requireRolePolicy: 'canManageAvatarDecorations',
 	kind: 'write:admin:avatar-decorations',
+
+	res: {
+		type: 'object',
+		optional: false, nullable: false,
+		properties: {
+			id: {
+				type: 'string',
+				optional: false, nullable: false,
+				format: 'id',
+			},
+			createdAt: {
+				type: 'string',
+				optional: false, nullable: false,
+				format: 'date-time',
+			},
+			updatedAt: {
+				type: 'string',
+				optional: false, nullable: true,
+				format: 'date-time',
+			},
+			name: {
+				type: 'string',
+				optional: false, nullable: false,
+			},
+			description: {
+				type: 'string',
+				optional: false, nullable: false,
+			},
+			url: {
+				type: 'string',
+				optional: false, nullable: false,
+			},
+			roleIdsThatCanBeUsedThisDecoration: {
+				type: 'array',
+				optional: false, nullable: false,
+				items: {
+					type: 'string',
+					optional: false, nullable: false,
+					format: 'id',
+				},
+			},
+		},
+	},
 } as const;
 
 export const paramDef = {
@@ -32,14 +76,25 @@ export const paramDef = {
 export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
 	constructor(
 		private avatarDecorationService: AvatarDecorationService,
+		private idService: IdService,
 	) {
 		super(meta, paramDef, async (ps, me) => {
-			await this.avatarDecorationService.create({
+			const created = await this.avatarDecorationService.create({
 				name: ps.name,
 				description: ps.description,
 				url: ps.url,
 				roleIdsThatCanBeUsedThisDecoration: ps.roleIdsThatCanBeUsedThisDecoration,
 			}, me);
+
+			return {
+				id: created.id,
+				createdAt: this.idService.parse(created.id).date.toISOString(),
+				updatedAt: null,
+				name: created.name,
+				description: created.description,
+				url: created.url,
+				roleIdsThatCanBeUsedThisDecoration: created.roleIdsThatCanBeUsedThisDecoration,
+			};
 		});
 	}
 }
diff --git a/packages/backend/src/server/api/endpoints/admin/avatar-decorations/list.ts b/packages/backend/src/server/api/endpoints/admin/avatar-decorations/list.ts
index aee90023e1..d785f085ac 100644
--- a/packages/backend/src/server/api/endpoints/admin/avatar-decorations/list.ts
+++ b/packages/backend/src/server/api/endpoints/admin/avatar-decorations/list.ts
@@ -4,10 +4,7 @@
  */
 
 import { Inject, Injectable } from '@nestjs/common';
-import type { AnnouncementsRepository, AnnouncementReadsRepository } from '@/models/_.js';
-import type { MiAnnouncement } from '@/models/Announcement.js';
 import { Endpoint } from '@/server/api/endpoint-base.js';
-import { QueryService } from '@/core/QueryService.js';
 import { DI } from '@/di-symbols.js';
 import { IdService } from '@/core/IdService.js';
 import { AvatarDecorationService } from '@/core/AvatarDecorationService.js';
diff --git a/packages/backend/src/server/api/endpoints/admin/drive/show-file.ts b/packages/backend/src/server/api/endpoints/admin/drive/show-file.ts
index 459d8880fa..a7136d8c8c 100644
--- a/packages/backend/src/server/api/endpoints/admin/drive/show-file.ts
+++ b/packages/backend/src/server/api/endpoints/admin/drive/show-file.ts
@@ -61,7 +61,7 @@ export const meta = {
 			name: {
 				type: 'string',
 				optional: false, nullable: false,
-				example: 'lenna.jpg',
+				example: '192.jpg',
 			},
 			type: {
 				type: 'string',
diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/update.ts b/packages/backend/src/server/api/endpoints/admin/emoji/update.ts
index 22609a16a3..212cba5c5d 100644
--- a/packages/backend/src/server/api/endpoints/admin/emoji/update.ts
+++ b/packages/backend/src/server/api/endpoints/admin/emoji/update.ts
@@ -6,7 +6,7 @@
 import { Inject, Injectable } from '@nestjs/common';
 import { Endpoint } from '@/server/api/endpoint-base.js';
 import { CustomEmojiService } from '@/core/CustomEmojiService.js';
-import type { DriveFilesRepository } from '@/models/_.js';
+import type { DriveFilesRepository, MiEmoji } from '@/models/_.js';
 import { DI } from '@/di-symbols.js';
 import { ApiError } from '../../../error.js';
 
@@ -78,25 +78,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 				if (driveFile == null) throw new ApiError(meta.errors.noSuchFile);
 			}
 
-			let emojiId;
-			if (ps.id) {
-				emojiId = ps.id;
-				const emoji = await this.customEmojiService.getEmojiById(ps.id);
-				if (!emoji) throw new ApiError(meta.errors.noSuchEmoji);
-				if (ps.name && (ps.name !== emoji.name)) {
-					const isDuplicate = await this.customEmojiService.checkDuplicate(ps.name);
-					if (isDuplicate) throw new ApiError(meta.errors.sameNameEmojiExists);
-				}
-			} else {
-				if (!ps.name) throw new Error('Invalid Params unexpectedly passed. This is a BUG. Please report it to the development team.');
-				const emoji = await this.customEmojiService.getEmojiByName(ps.name);
-				if (!emoji) throw new ApiError(meta.errors.noSuchEmoji);
-				emojiId = emoji.id;
-			}
+			// JSON schemeのanyOfの型変換がうまくいっていないらしい
+			const required = { id: ps.id, name: ps.name } as 
+				| { id: MiEmoji['id']; name?: string }
+				| { id?: MiEmoji['id']; name: string };
 
-			await this.customEmojiService.update(emojiId, {
+			const error = await this.customEmojiService.update({
+				...required,
 				driveFile,
-				name: ps.name,
 				category: ps.category,
 				aliases: ps.aliases,
 				license: ps.license,
@@ -104,6 +93,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 				localOnly: ps.localOnly,
 				roleIdsThatCanBeUsedThisEmojiAsReaction: ps.roleIdsThatCanBeUsedThisEmojiAsReaction,
 			}, me);
+
+			switch (error) {
+				case null: return;
+				case 'NO_SUCH_EMOJI': throw new ApiError(meta.errors.noSuchEmoji);
+				case 'SAME_NAME_EMOJI_EXISTS': throw new ApiError(meta.errors.sameNameEmojiExists);
+			}
+			// 網羅性チェック
+			const mustBeNever: never = error;
 		});
 	}
 }
diff --git a/packages/backend/src/server/api/endpoints/admin/forward-abuse-user-report.ts b/packages/backend/src/server/api/endpoints/admin/forward-abuse-user-report.ts
new file mode 100644
index 0000000000..3e42c91fed
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/admin/forward-abuse-user-report.ts
@@ -0,0 +1,55 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Inject, Injectable } from '@nestjs/common';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import type { AbuseUserReportsRepository } from '@/models/_.js';
+import { DI } from '@/di-symbols.js';
+import { ApiError } from '@/server/api/error.js';
+import { AbuseReportService } from '@/core/AbuseReportService.js';
+
+export const meta = {
+	tags: ['admin'],
+
+	requireCredential: true,
+	requireModerator: true,
+	kind: 'write:admin:resolve-abuse-user-report',
+
+	errors: {
+		noSuchAbuseReport: {
+			message: 'No such abuse report.',
+			code: 'NO_SUCH_ABUSE_REPORT',
+			id: '8763e21b-d9bc-40be-acf6-54c1a6986493',
+			kind: 'server',
+			httpStatusCode: 404,
+		},
+	},
+} as const;
+
+export const paramDef = {
+	type: 'object',
+	properties: {
+		reportId: { type: 'string', format: 'misskey:id' },
+	},
+	required: ['reportId'],
+} as const;
+
+@Injectable()
+export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
+	constructor(
+		@Inject(DI.abuseUserReportsRepository)
+		private abuseUserReportsRepository: AbuseUserReportsRepository,
+		private abuseReportService: AbuseReportService,
+	) {
+		super(meta, paramDef, async (ps, me) => {
+			const report = await this.abuseUserReportsRepository.findOneBy({ id: ps.reportId });
+			if (!report) {
+				throw new ApiError(meta.errors.noSuchAbuseReport);
+			}
+
+			await this.abuseReportService.forward(report.id, me);
+		});
+	}
+}
diff --git a/packages/backend/src/server/api/endpoints/admin/meta.ts b/packages/backend/src/server/api/endpoints/admin/meta.ts
index eee02a7123..64e3cc33bd 100644
--- a/packages/backend/src/server/api/endpoints/admin/meta.ts
+++ b/packages/backend/src/server/api/endpoints/admin/meta.ts
@@ -69,6 +69,10 @@ export const meta = {
 				type: 'string',
 				optional: false, nullable: true,
 			},
+			enableTestcaptcha: {
+				type: 'boolean',
+				optional: false, nullable: false,
+			},
 			swPublickey: {
 				type: 'string',
 				optional: false, nullable: true,
@@ -128,6 +132,16 @@ export const meta = {
 					nullable: false,
 				},
 			},
+			mediaSilencedHosts: {
+				type: 'array',
+				optional: false,
+				nullable: false,
+				items: {
+					type: 'string',
+					optional: false,
+					nullable: false,
+				},
+			},
 			pinnedUsers: {
 				type: 'array',
 				optional: false, nullable: false,
@@ -163,6 +177,13 @@ export const meta = {
 					type: 'string',
 				},
 			},
+			prohibitedWordsForNameOfUser: {
+				type: 'array',
+				optional: false, nullable: false,
+				items: {
+					type: 'string',
+				},
+			},
 			bannedEmailDomains: {
 				type: 'array',
 				optional: true, nullable: false,
@@ -327,6 +348,10 @@ export const meta = {
 				type: 'boolean',
 				optional: false, nullable: false,
 			},
+			enableStatsForFederatedInstances: {
+				type: 'boolean',
+				optional: false, nullable: false,
+			},
 			enableServerMachineStats: {
 				type: 'boolean',
 				optional: false, nullable: false,
@@ -367,6 +392,10 @@ export const meta = {
 				type: 'number',
 				optional: false, nullable: false,
 			},
+			enableReactionsBuffering: {
+				type: 'boolean',
+				optional: false, nullable: false,
+			},
 			notesPerOneAd: {
 				type: 'number',
 				optional: false, nullable: false,
@@ -481,6 +510,18 @@ export const meta = {
 				type: 'string',
 				optional: false, nullable: true,
 			},
+			federation: {
+				type: 'string',
+				optional: false, nullable: false,
+			},
+			federationHosts: {
+				type: 'array',
+				optional: false, nullable: false,
+				items: {
+					type: 'string',
+					optional: false, nullable: false,
+				},
+			},
 		},
 	},
 } as const;
@@ -529,6 +570,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 				recaptchaSiteKey: instance.recaptchaSiteKey,
 				enableTurnstile: instance.enableTurnstile,
 				turnstileSiteKey: instance.turnstileSiteKey,
+				enableTestcaptcha: instance.enableTestcaptcha,
 				swPublickey: instance.swPublicKey,
 				themeColor: instance.themeColor,
 				mascotImageUrl: instance.mascotImageUrl,
@@ -552,8 +594,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 				hiddenTags: instance.hiddenTags,
 				blockedHosts: instance.blockedHosts,
 				silencedHosts: instance.silencedHosts,
+				mediaSilencedHosts: instance.mediaSilencedHosts,
 				sensitiveWords: instance.sensitiveWords,
 				prohibitedWords: instance.prohibitedWords,
+				prohibitedWordsForNameOfUser: instance.prohibitedWordsForNameOfUser,
 				preservedUsernames: instance.preservedUsernames,
 				hcaptchaSecretKey: instance.hcaptchaSecretKey,
 				mcaptchaSecretKey: instance.mcaptchaSecretKey,
@@ -595,6 +639,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 				truemailAuthKey: instance.truemailAuthKey,
 				enableChartsForRemoteUser: instance.enableChartsForRemoteUser,
 				enableChartsForFederatedInstances: instance.enableChartsForFederatedInstances,
+				enableStatsForFederatedInstances: instance.enableStatsForFederatedInstances,
 				enableServerMachineStats: instance.enableServerMachineStats,
 				enableIdenticonGeneration: instance.enableIdenticonGeneration,
 				bannedEmailDomains: instance.bannedEmailDomains,
@@ -606,6 +651,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 				perRemoteUserUserTimelineCacheMax: instance.perRemoteUserUserTimelineCacheMax,
 				perUserHomeTimelineCacheMax: instance.perUserHomeTimelineCacheMax,
 				perUserListTimelineCacheMax: instance.perUserListTimelineCacheMax,
+				enableReactionsBuffering: instance.enableReactionsBuffering,
 				notesPerOneAd: instance.notesPerOneAd,
 				summalyProxy: instance.urlPreviewSummaryProxyUrl,
 				urlPreviewEnabled: instance.urlPreviewEnabled,
@@ -614,6 +660,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 				urlPreviewRequireContentLength: instance.urlPreviewRequireContentLength,
 				urlPreviewUserAgent: instance.urlPreviewUserAgent,
 				urlPreviewSummaryProxyUrl: instance.urlPreviewSummaryProxyUrl,
+				federation: instance.federation,
+				federationHosts: instance.federationHosts,
 			};
 		});
 	}
diff --git a/packages/backend/src/server/api/endpoints/admin/queue/deliver-delayed.ts b/packages/backend/src/server/api/endpoints/admin/queue/deliver-delayed.ts
index 7a3410ffa7..f3e440b4cb 100644
--- a/packages/backend/src/server/api/endpoints/admin/queue/deliver-delayed.ts
+++ b/packages/backend/src/server/api/endpoints/admin/queue/deliver-delayed.ts
@@ -21,16 +21,15 @@ export const meta = {
 		items: {
 			type: 'array',
 			optional: false, nullable: false,
-			items: {
-				anyOf: [
-					{
-						type: 'string',
-					},
-					{
-						type: 'number',
-					},
-				],
-			},
+			prefixItems: [
+				{
+					type: 'string',
+				},
+				{
+					type: 'number',
+				},
+			],
+			unevaluatedItems: false,
 		},
 		example: [[
 			'example.com',
diff --git a/packages/backend/src/server/api/endpoints/admin/queue/inbox-delayed.ts b/packages/backend/src/server/api/endpoints/admin/queue/inbox-delayed.ts
index 305ae1af1d..e7589cba81 100644
--- a/packages/backend/src/server/api/endpoints/admin/queue/inbox-delayed.ts
+++ b/packages/backend/src/server/api/endpoints/admin/queue/inbox-delayed.ts
@@ -21,16 +21,15 @@ export const meta = {
 		items: {
 			type: 'array',
 			optional: false, nullable: false,
-			items: {
-				anyOf: [
-					{
-						type: 'string',
-					},
-					{
-						type: 'number',
-					},
-				],
-			},
+			prefixItems: [
+				{
+					type: 'string',
+				},
+				{
+					type: 'number',
+				},
+			],
+			unevaluatedItems: false,
 		},
 		example: [[
 			'example.com',
diff --git a/packages/backend/src/server/api/endpoints/admin/queue/stats.ts b/packages/backend/src/server/api/endpoints/admin/queue/stats.ts
index 9694b3fa40..d7f9e4eaa3 100644
--- a/packages/backend/src/server/api/endpoints/admin/queue/stats.ts
+++ b/packages/backend/src/server/api/endpoints/admin/queue/stats.ts
@@ -5,7 +5,7 @@
 
 import { Inject, Injectable } from '@nestjs/common';
 import { Endpoint } from '@/server/api/endpoint-base.js';
-import type { DbQueue, DeliverQueue, EndedPollNotificationQueue, InboxQueue, ObjectStorageQueue, SystemQueue, WebhookDeliverQueue } from '@/core/QueueModule.js';
+import type { DbQueue, DeliverQueue, EndedPollNotificationQueue, InboxQueue, ObjectStorageQueue, SystemQueue, UserWebhookDeliverQueue, SystemWebhookDeliverQueue } from '@/core/QueueModule.js';
 
 export const meta = {
 	tags: ['admin'],
@@ -53,7 +53,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 		@Inject('queue:inbox') public inboxQueue: InboxQueue,
 		@Inject('queue:db') public dbQueue: DbQueue,
 		@Inject('queue:objectStorage') public objectStorageQueue: ObjectStorageQueue,
-		@Inject('queue:webhookDeliver') public webhookDeliverQueue: WebhookDeliverQueue,
+		@Inject('queue:userWebhookDeliver') public userWebhookDeliverQueue: UserWebhookDeliverQueue,
+		@Inject('queue:systemWebhookDeliver') public systemWebhookDeliverQueue: SystemWebhookDeliverQueue,
 	) {
 		super(meta, paramDef, async (ps, me) => {
 			const deliverJobCounts = await this.deliverQueue.getJobCounts();
diff --git a/packages/backend/src/server/api/endpoints/admin/resolve-abuse-user-report.ts b/packages/backend/src/server/api/endpoints/admin/resolve-abuse-user-report.ts
index 8b0456068b..554d324ff2 100644
--- a/packages/backend/src/server/api/endpoints/admin/resolve-abuse-user-report.ts
+++ b/packages/backend/src/server/api/endpoints/admin/resolve-abuse-user-report.ts
@@ -5,12 +5,10 @@
 
 import { Inject, Injectable } from '@nestjs/common';
 import { Endpoint } from '@/server/api/endpoint-base.js';
-import type { UsersRepository, AbuseUserReportsRepository } from '@/models/_.js';
-import { InstanceActorService } from '@/core/InstanceActorService.js';
-import { QueueService } from '@/core/QueueService.js';
-import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
+import type { AbuseUserReportsRepository } from '@/models/_.js';
 import { DI } from '@/di-symbols.js';
-import { ModerationLogService } from '@/core/ModerationLogService.js';
+import { ApiError } from '@/server/api/error.js';
+import { AbuseReportService } from '@/core/AbuseReportService.js';
 
 export const meta = {
 	tags: ['admin'],
@@ -18,58 +16,41 @@ export const meta = {
 	requireCredential: true,
 	requireModerator: true,
 	kind: 'write:admin:resolve-abuse-user-report',
+
+	errors: {
+		noSuchAbuseReport: {
+			message: 'No such abuse report.',
+			code: 'NO_SUCH_ABUSE_REPORT',
+			id: 'ac3794dd-2ce4-d878-e546-73c60c06b398',
+			kind: 'server',
+			httpStatusCode: 404,
+		},
+	},
 } as const;
 
 export const paramDef = {
 	type: 'object',
 	properties: {
 		reportId: { type: 'string', format: 'misskey:id' },
-		forward: { type: 'boolean', default: false },
+		resolvedAs: { type: 'string', enum: ['accept', 'reject', null], nullable: true },
 	},
 	required: ['reportId'],
 } as const;
 
-// TODO: ロジックをサービスに切り出す
-
 @Injectable()
 export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
 	constructor(
-		@Inject(DI.usersRepository)
-		private usersRepository: UsersRepository,
-
 		@Inject(DI.abuseUserReportsRepository)
 		private abuseUserReportsRepository: AbuseUserReportsRepository,
-
-		private queueService: QueueService,
-		private instanceActorService: InstanceActorService,
-		private apRendererService: ApRendererService,
-		private moderationLogService: ModerationLogService,
+		private abuseReportService: AbuseReportService,
 	) {
 		super(meta, paramDef, async (ps, me) => {
 			const report = await this.abuseUserReportsRepository.findOneBy({ id: ps.reportId });
-
-			if (report == null) {
-				throw new Error('report not found');
+			if (!report) {
+				throw new ApiError(meta.errors.noSuchAbuseReport);
 			}
 
-			if (ps.forward && report.targetUserHost != null) {
-				const actor = await this.instanceActorService.getInstanceActor();
-				const targetUser = await this.usersRepository.findOneByOrFail({ id: report.targetUserId });
-
-				this.queueService.deliver(actor, this.apRendererService.addContext(this.apRendererService.renderFlag(actor, targetUser.uri!, report.comment)), targetUser.inbox, false);
-			}
-
-			await this.abuseUserReportsRepository.update(report.id, {
-				resolved: true,
-				assigneeId: me.id,
-				forwarded: ps.forward && report.targetUserHost != null,
-			});
-
-			this.moderationLogService.log(me, 'resolveAbuseReport', {
-				reportId: report.id,
-				report: report,
-				forwarded: ps.forward && report.targetUserHost != null,
-			});
+			await this.abuseReportService.resolve([{ reportId: report.id, resolvedAs: ps.resolvedAs ?? null }], me);
 		});
 	}
 }
diff --git a/packages/backend/src/server/api/endpoints/admin/roles/update-default-policies.ts b/packages/backend/src/server/api/endpoints/admin/roles/update-default-policies.ts
index d7209965db..5cf49670be 100644
--- a/packages/backend/src/server/api/endpoints/admin/roles/update-default-policies.ts
+++ b/packages/backend/src/server/api/endpoints/admin/roles/update-default-policies.ts
@@ -7,6 +7,7 @@ import { Injectable } from '@nestjs/common';
 import { Endpoint } from '@/server/api/endpoint-base.js';
 import { GlobalEventService } from '@/core/GlobalEventService.js';
 import { MetaService } from '@/core/MetaService.js';
+import { ModerationLogService } from '@/core/ModerationLogService.js';
 
 export const meta = {
 	tags: ['admin', 'role'],
@@ -33,12 +34,22 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 	constructor(
 		private metaService: MetaService,
 		private globalEventService: GlobalEventService,
+		private moderationLogService: ModerationLogService,
 	) {
-		super(meta, paramDef, async (ps) => {
+		super(meta, paramDef, async (ps, me) => {
+			const before = await this.metaService.fetch(true);
+
 			await this.metaService.update({
 				policies: ps.policies,
 			});
-			this.globalEventService.publishInternalEvent('policiesUpdated', ps.policies);
+
+			const after = await this.metaService.fetch(true);
+
+			this.globalEventService.publishInternalEvent('policiesUpdated', after.policies);
+			this.moderationLogService.log(me, 'updateServerSettings', {
+				before: before.policies,
+				after: after.policies,
+			});
 		});
 	}
 }
diff --git a/packages/backend/src/server/api/endpoints/admin/roles/update.ts b/packages/backend/src/server/api/endpoints/admin/roles/update.ts
index 5242e0be2f..465ad7aaaf 100644
--- a/packages/backend/src/server/api/endpoints/admin/roles/update.ts
+++ b/packages/backend/src/server/api/endpoints/admin/roles/update.ts
@@ -6,7 +6,6 @@
 import { Inject, Injectable } from '@nestjs/common';
 import { Endpoint } from '@/server/api/endpoint-base.js';
 import type { RolesRepository } from '@/models/_.js';
-import { GlobalEventService } from '@/core/GlobalEventService.js';
 import { DI } from '@/di-symbols.js';
 import { ApiError } from '@/server/api/error.js';
 import { RoleService } from '@/core/RoleService.js';
@@ -50,19 +49,6 @@ export const paramDef = {
 	},
 	required: [
 		'roleId',
-		'name',
-		'description',
-		'color',
-		'iconUrl',
-		'target',
-		'condFormula',
-		'isPublic',
-		'isModerator',
-		'isAdministrator',
-		'asBadge',
-		'canEditMembersByModerator',
-		'displayOrder',
-		'policies',
 	],
 } as const;
 
diff --git a/packages/backend/src/server/api/endpoints/admin/show-user.ts b/packages/backend/src/server/api/endpoints/admin/show-user.ts
index 5a1c05f41a..655bd32bce 100644
--- a/packages/backend/src/server/api/endpoints/admin/show-user.ts
+++ b/packages/backend/src/server/api/endpoints/admin/show-user.ts
@@ -31,6 +31,10 @@ export const meta = {
 				type: 'boolean',
 				optional: false, nullable: false,
 			},
+			followedMessage: {
+				type: 'string',
+				optional: false, nullable: true,
+			},
 			autoAcceptFollowed: {
 				type: 'boolean',
 				optional: false, nullable: false,
@@ -226,6 +230,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 			return {
 				email: profile.email,
 				emailVerified: profile.emailVerified,
+				followedMessage: profile.followedMessage,
 				autoAcceptFollowed: profile.autoAcceptFollowed,
 				noCrawle: profile.noCrawle,
 				preventAiLearning: profile.preventAiLearning,
diff --git a/packages/backend/src/server/api/endpoints/admin/show-users.ts b/packages/backend/src/server/api/endpoints/admin/show-users.ts
index 2fef9abbf9..2b2c8c60ab 100644
--- a/packages/backend/src/server/api/endpoints/admin/show-users.ts
+++ b/packages/backend/src/server/api/endpoints/admin/show-users.ts
@@ -71,13 +71,13 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 					break;
 				}
 				case 'moderator': {
-					const moderatorIds = await this.roleService.getModeratorIds(false);
+					const moderatorIds = await this.roleService.getModeratorIds({ includeAdmins: false });
 					if (moderatorIds.length === 0) return [];
 					query.where('user.id IN (:...moderatorIds)', { moderatorIds: moderatorIds });
 					break;
 				}
 				case 'adminOrModerator': {
-					const adminOrModeratorIds = await this.roleService.getModeratorIds();
+					const adminOrModeratorIds = await this.roleService.getModeratorIds({ includeAdmins: true });
 					if (adminOrModeratorIds.length === 0) return [];
 					query.where('user.id IN (:...adminOrModeratorIds)', { adminOrModeratorIds: adminOrModeratorIds });
 					break;
diff --git a/packages/backend/src/server/api/endpoints/admin/suspend-user.ts b/packages/backend/src/server/api/endpoints/admin/suspend-user.ts
index 8a946405cc..bea1bdc4ed 100644
--- a/packages/backend/src/server/api/endpoints/admin/suspend-user.ts
+++ b/packages/backend/src/server/api/endpoints/admin/suspend-user.ts
@@ -3,18 +3,12 @@
  * SPDX-License-Identifier: AGPL-3.0-only
  */
 
-import { IsNull, Not } from 'typeorm';
 import { Inject, Injectable } from '@nestjs/common';
 import { Endpoint } from '@/server/api/endpoint-base.js';
-import type { UsersRepository, FollowingsRepository } from '@/models/_.js';
-import type { MiUser } from '@/models/User.js';
-import type { RelationshipJobData } from '@/queue/types.js';
-import { ModerationLogService } from '@/core/ModerationLogService.js';
+import type { UsersRepository } from '@/models/_.js';
 import { UserSuspendService } from '@/core/UserSuspendService.js';
 import { DI } from '@/di-symbols.js';
-import { bindThis } from '@/decorators.js';
 import { RoleService } from '@/core/RoleService.js';
-import { QueueService } from '@/core/QueueService.js';
 
 export const meta = {
 	tags: ['admin'],
@@ -38,13 +32,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 		@Inject(DI.usersRepository)
 		private usersRepository: UsersRepository,
 
-		@Inject(DI.followingsRepository)
-		private followingsRepository: FollowingsRepository,
-
 		private userSuspendService: UserSuspendService,
 		private roleService: RoleService,
-		private moderationLogService: ModerationLogService,
-		private queueService: QueueService,
 	) {
 		super(meta, paramDef, async (ps, me) => {
 			const user = await this.usersRepository.findOneBy({ id: ps.userId });
@@ -57,42 +46,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 				throw new Error('cannot suspend moderator account');
 			}
 
-			await this.usersRepository.update(user.id, {
-				isSuspended: true,
-			});
-
-			this.moderationLogService.log(me, 'suspend', {
-				userId: user.id,
-				userUsername: user.username,
-				userHost: user.host,
-			});
-
-			(async () => {
-				await this.userSuspendService.doPostSuspend(user).catch(e => {});
-				await this.unFollowAll(user).catch(e => {});
-			})();
+			await this.userSuspendService.suspend(user, me);
 		});
 	}
-
-	@bindThis
-	private async unFollowAll(follower: MiUser) {
-		const followings = await this.followingsRepository.find({
-			where: {
-				followerId: follower.id,
-				followeeId: Not(IsNull()),
-			},
-		});
-
-		const jobs: RelationshipJobData[] = [];
-		for (const following of followings) {
-			if (following.followeeId && following.followerId) {
-				jobs.push({
-					from: { id: following.followerId },
-					to: { id: following.followeeId },
-					silent: true,
-				});
-			}
-		}
-		this.queueService.createUnfollowJob(jobs);
-	}
 }
diff --git a/packages/backend/src/server/api/endpoints/admin/system-webhook/create.ts b/packages/backend/src/server/api/endpoints/admin/system-webhook/create.ts
new file mode 100644
index 0000000000..28071e7a33
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/admin/system-webhook/create.ts
@@ -0,0 +1,85 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Injectable } from '@nestjs/common';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import { SystemWebhookEntityService } from '@/core/entities/SystemWebhookEntityService.js';
+import { systemWebhookEventTypes } from '@/models/SystemWebhook.js';
+import { SystemWebhookService } from '@/core/SystemWebhookService.js';
+
+export const meta = {
+	tags: ['admin', 'system-webhook'],
+
+	requireCredential: true,
+	requireModerator: true,
+	secure: true,
+	kind: 'write:admin:system-webhook',
+
+	res: {
+		type: 'object',
+		ref: 'SystemWebhook',
+	},
+} as const;
+
+export const paramDef = {
+	type: 'object',
+	properties: {
+		isActive: {
+			type: 'boolean',
+		},
+		name: {
+			type: 'string',
+			minLength: 1,
+			maxLength: 255,
+		},
+		on: {
+			type: 'array',
+			items: {
+				type: 'string',
+				enum: systemWebhookEventTypes,
+			},
+		},
+		url: {
+			type: 'string',
+			minLength: 1,
+			maxLength: 1024,
+		},
+		secret: {
+			type: 'string',
+			minLength: 1,
+			maxLength: 1024,
+		},
+	},
+	required: [
+		'isActive',
+		'name',
+		'on',
+		'url',
+		'secret',
+	],
+} as const;
+
+@Injectable()
+export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
+	constructor(
+		private systemWebhookService: SystemWebhookService,
+		private systemWebhookEntityService: SystemWebhookEntityService,
+	) {
+		super(meta, paramDef, async (ps, me) => {
+			const result = await this.systemWebhookService.createSystemWebhook(
+				{
+					isActive: ps.isActive,
+					name: ps.name,
+					on: ps.on,
+					url: ps.url,
+					secret: ps.secret,
+				},
+				me,
+			);
+
+			return this.systemWebhookEntityService.pack(result);
+		});
+	}
+}
diff --git a/packages/backend/src/server/api/endpoints/admin/system-webhook/delete.ts b/packages/backend/src/server/api/endpoints/admin/system-webhook/delete.ts
new file mode 100644
index 0000000000..9cdfc7e70f
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/admin/system-webhook/delete.ts
@@ -0,0 +1,44 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Injectable } from '@nestjs/common';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import { SystemWebhookService } from '@/core/SystemWebhookService.js';
+
+export const meta = {
+	tags: ['admin', 'system-webhook'],
+
+	requireCredential: true,
+	requireModerator: true,
+	secure: true,
+	kind: 'write:admin:system-webhook',
+} as const;
+
+export const paramDef = {
+	type: 'object',
+	properties: {
+		id: {
+			type: 'string',
+			format: 'misskey:id',
+		},
+	},
+	required: [
+		'id',
+	],
+} as const;
+
+@Injectable()
+export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
+	constructor(
+		private systemWebhookService: SystemWebhookService,
+	) {
+		super(meta, paramDef, async (ps, me) => {
+			await this.systemWebhookService.deleteSystemWebhook(
+				ps.id,
+				me,
+			);
+		});
+	}
+}
diff --git a/packages/backend/src/server/api/endpoints/admin/system-webhook/list.ts b/packages/backend/src/server/api/endpoints/admin/system-webhook/list.ts
new file mode 100644
index 0000000000..7a440a774e
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/admin/system-webhook/list.ts
@@ -0,0 +1,60 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Injectable } from '@nestjs/common';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import { SystemWebhookEntityService } from '@/core/entities/SystemWebhookEntityService.js';
+import { systemWebhookEventTypes } from '@/models/SystemWebhook.js';
+import { SystemWebhookService } from '@/core/SystemWebhookService.js';
+
+export const meta = {
+	tags: ['admin', 'system-webhook'],
+
+	requireCredential: true,
+	requireModerator: true,
+	secure: true,
+	kind: 'write:admin:system-webhook',
+
+	res: {
+		type: 'array',
+		items: {
+			type: 'object',
+			ref: 'SystemWebhook',
+		},
+	},
+} as const;
+
+export const paramDef = {
+	type: 'object',
+	properties: {
+		isActive: {
+			type: 'boolean',
+		},
+		on: {
+			type: 'array',
+			items: {
+				type: 'string',
+				enum: systemWebhookEventTypes,
+			},
+		},
+	},
+	required: [],
+} as const;
+
+@Injectable()
+export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
+	constructor(
+		private systemWebhookService: SystemWebhookService,
+		private systemWebhookEntityService: SystemWebhookEntityService,
+	) {
+		super(meta, paramDef, async (ps) => {
+			const webhooks = await this.systemWebhookService.fetchSystemWebhooks({
+				isActive: ps.isActive,
+				on: ps.on,
+			});
+			return this.systemWebhookEntityService.packMany(webhooks);
+		});
+	}
+}
diff --git a/packages/backend/src/server/api/endpoints/admin/system-webhook/show.ts b/packages/backend/src/server/api/endpoints/admin/system-webhook/show.ts
new file mode 100644
index 0000000000..75862c96a7
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/admin/system-webhook/show.ts
@@ -0,0 +1,62 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Injectable } from '@nestjs/common';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import { SystemWebhookEntityService } from '@/core/entities/SystemWebhookEntityService.js';
+import { ApiError } from '@/server/api/error.js';
+import { SystemWebhookService } from '@/core/SystemWebhookService.js';
+
+export const meta = {
+	tags: ['admin', 'system-webhook'],
+
+	requireCredential: true,
+	requireModerator: true,
+	secure: true,
+	kind: 'write:admin:system-webhook',
+
+	res: {
+		type: 'object',
+		ref: 'SystemWebhook',
+	},
+
+	errors: {
+		noSuchSystemWebhook: {
+			message: 'No such SystemWebhook.',
+			code: 'NO_SUCH_SYSTEM_WEBHOOK',
+			id: '38dd1ffe-04b4-6ff5-d8ba-4e6a6ae22c9d',
+			kind: 'server',
+			httpStatusCode: 404,
+		},
+	},
+} as const;
+
+export const paramDef = {
+	type: 'object',
+	properties: {
+		id: {
+			type: 'string',
+			format: 'misskey:id',
+		},
+	},
+	required: ['id'],
+} as const;
+
+@Injectable()
+export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
+	constructor(
+		private systemWebhookService: SystemWebhookService,
+		private systemWebhookEntityService: SystemWebhookEntityService,
+	) {
+		super(meta, paramDef, async (ps) => {
+			const webhooks = await this.systemWebhookService.fetchSystemWebhooks({ ids: [ps.id] });
+			if (webhooks.length === 0) {
+				throw new ApiError(meta.errors.noSuchSystemWebhook);
+			}
+
+			return this.systemWebhookEntityService.pack(webhooks[0]);
+		});
+	}
+}
diff --git a/packages/backend/src/server/api/endpoints/admin/system-webhook/test.ts b/packages/backend/src/server/api/endpoints/admin/system-webhook/test.ts
new file mode 100644
index 0000000000..fb2ddf4b44
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/admin/system-webhook/test.ts
@@ -0,0 +1,77 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Injectable } from '@nestjs/common';
+import ms from 'ms';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import { WebhookTestService } from '@/core/WebhookTestService.js';
+import { ApiError } from '@/server/api/error.js';
+import { systemWebhookEventTypes } from '@/models/SystemWebhook.js';
+
+export const meta = {
+	tags: ['webhooks'],
+
+	requireCredential: true,
+	requireModerator: true,
+	secure: true,
+	kind: 'read:admin:system-webhook',
+
+	limit: {
+		duration: ms('15min'),
+		max: 60,
+	},
+
+	errors: {
+		noSuchWebhook: {
+			message: 'No such webhook.',
+			code: 'NO_SUCH_WEBHOOK',
+			id: '0c52149c-e913-18f8-5dc7-74870bfe0cf9',
+		},
+	},
+} as const;
+
+export const paramDef = {
+	type: 'object',
+	properties: {
+		webhookId: {
+			type: 'string',
+			format: 'misskey:id',
+		},
+		type: {
+			type: 'string',
+			enum: systemWebhookEventTypes,
+		},
+		override: {
+			type: 'object',
+			properties: {
+				url: { type: 'string', nullable: false },
+				secret: { type: 'string', nullable: false },
+			},
+		},
+	},
+	required: ['webhookId', 'type'],
+} as const;
+
+@Injectable()
+export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
+	constructor(
+		private webhookTestService: WebhookTestService,
+	) {
+		super(meta, paramDef, async (ps) => {
+			try {
+				await this.webhookTestService.testSystemWebhook({
+					webhookId: ps.webhookId,
+					type: ps.type,
+					override: ps.override,
+				});
+			} catch (e) {
+				if (e instanceof WebhookTestService.NoSuchWebhookError) {
+					throw new ApiError(meta.errors.noSuchWebhook);
+				}
+				throw e;
+			}
+		});
+	}
+}
diff --git a/packages/backend/src/server/api/endpoints/admin/system-webhook/update.ts b/packages/backend/src/server/api/endpoints/admin/system-webhook/update.ts
new file mode 100644
index 0000000000..8d68bb8f87
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/admin/system-webhook/update.ts
@@ -0,0 +1,91 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Injectable } from '@nestjs/common';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import { SystemWebhookEntityService } from '@/core/entities/SystemWebhookEntityService.js';
+import { systemWebhookEventTypes } from '@/models/SystemWebhook.js';
+import { SystemWebhookService } from '@/core/SystemWebhookService.js';
+
+export const meta = {
+	tags: ['admin', 'system-webhook'],
+
+	requireCredential: true,
+	requireModerator: true,
+	secure: true,
+	kind: 'write:admin:system-webhook',
+
+	res: {
+		type: 'object',
+		ref: 'SystemWebhook',
+	},
+} as const;
+
+export const paramDef = {
+	type: 'object',
+	properties: {
+		id: {
+			type: 'string',
+			format: 'misskey:id',
+		},
+		isActive: {
+			type: 'boolean',
+		},
+		name: {
+			type: 'string',
+			minLength: 1,
+			maxLength: 255,
+		},
+		on: {
+			type: 'array',
+			items: {
+				type: 'string',
+				enum: systemWebhookEventTypes,
+			},
+		},
+		url: {
+			type: 'string',
+			minLength: 1,
+			maxLength: 1024,
+		},
+		secret: {
+			type: 'string',
+			minLength: 1,
+			maxLength: 1024,
+		},
+	},
+	required: [
+		'id',
+		'isActive',
+		'name',
+		'on',
+		'url',
+		'secret',
+	],
+} as const;
+
+@Injectable()
+export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
+	constructor(
+		private systemWebhookService: SystemWebhookService,
+		private systemWebhookEntityService: SystemWebhookEntityService,
+	) {
+		super(meta, paramDef, async (ps, me) => {
+			const result = await this.systemWebhookService.updateSystemWebhook(
+				{
+					id: ps.id,
+					isActive: ps.isActive,
+					name: ps.name,
+					on: ps.on,
+					url: ps.url,
+					secret: ps.secret,
+				},
+				me,
+			);
+
+			return this.systemWebhookEntityService.pack(result);
+		});
+	}
+}
diff --git a/packages/backend/src/server/api/endpoints/admin/unsuspend-user.ts b/packages/backend/src/server/api/endpoints/admin/unsuspend-user.ts
index 2c2b1bf6f5..b52c638cdb 100644
--- a/packages/backend/src/server/api/endpoints/admin/unsuspend-user.ts
+++ b/packages/backend/src/server/api/endpoints/admin/unsuspend-user.ts
@@ -6,7 +6,6 @@
 import { Inject, Injectable } from '@nestjs/common';
 import { Endpoint } from '@/server/api/endpoint-base.js';
 import type { UsersRepository } from '@/models/_.js';
-import { ModerationLogService } from '@/core/ModerationLogService.js';
 import { UserSuspendService } from '@/core/UserSuspendService.js';
 import { DI } from '@/di-symbols.js';
 
@@ -33,7 +32,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 		private usersRepository: UsersRepository,
 
 		private userSuspendService: UserSuspendService,
-		private moderationLogService: ModerationLogService,
 	) {
 		super(meta, paramDef, async (ps, me) => {
 			const user = await this.usersRepository.findOneBy({ id: ps.userId });
@@ -42,17 +40,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 				throw new Error('user not found');
 			}
 
-			await this.usersRepository.update(user.id, {
-				isSuspended: false,
-			});
-
-			this.moderationLogService.log(me, 'unsuspend', {
-				userId: user.id,
-				userUsername: user.username,
-				userHost: user.host,
-			});
-
-			this.userSuspendService.doPostUnsuspend(user);
+			await this.userSuspendService.unsuspend(user, me);
 		});
 	}
 }
diff --git a/packages/backend/src/server/api/endpoints/admin/update-abuse-user-report.ts b/packages/backend/src/server/api/endpoints/admin/update-abuse-user-report.ts
new file mode 100644
index 0000000000..73d4b843f0
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/admin/update-abuse-user-report.ts
@@ -0,0 +1,58 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Inject, Injectable } from '@nestjs/common';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import type { AbuseUserReportsRepository } from '@/models/_.js';
+import { DI } from '@/di-symbols.js';
+import { ApiError } from '@/server/api/error.js';
+import { AbuseReportService } from '@/core/AbuseReportService.js';
+
+export const meta = {
+	tags: ['admin'],
+
+	requireCredential: true,
+	requireModerator: true,
+	kind: 'write:admin:resolve-abuse-user-report',
+
+	errors: {
+		noSuchAbuseReport: {
+			message: 'No such abuse report.',
+			code: 'NO_SUCH_ABUSE_REPORT',
+			id: '15f51cf5-46d1-4b1d-a618-b35bcbed0662',
+			kind: 'server',
+			httpStatusCode: 404,
+		},
+	},
+} as const;
+
+export const paramDef = {
+	type: 'object',
+	properties: {
+		reportId: { type: 'string', format: 'misskey:id' },
+		moderationNote: { type: 'string' },
+	},
+	required: ['reportId'],
+} as const;
+
+@Injectable()
+export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
+	constructor(
+		@Inject(DI.abuseUserReportsRepository)
+		private abuseUserReportsRepository: AbuseUserReportsRepository,
+		private abuseReportService: AbuseReportService,
+	) {
+		super(meta, paramDef, async (ps, me) => {
+			const report = await this.abuseUserReportsRepository.findOneBy({ id: ps.reportId });
+			if (!report) {
+				throw new ApiError(meta.errors.noSuchAbuseReport);
+			}
+
+			await this.abuseReportService.update(report.id, {
+				moderationNote: ps.moderationNote,
+			}, me);
+		});
+	}
+}
diff --git a/packages/backend/src/server/api/endpoints/admin/update-meta.ts b/packages/backend/src/server/api/endpoints/admin/update-meta.ts
index 4e28ee6877..38ef0d1de8 100644
--- a/packages/backend/src/server/api/endpoints/admin/update-meta.ts
+++ b/packages/backend/src/server/api/endpoints/admin/update-meta.ts
@@ -46,6 +46,11 @@ export const paramDef = {
 				type: 'string',
 			},
 		},
+		prohibitedWordsForNameOfUser: {
+			type: 'array', nullable: true, items: {
+				type: 'string',
+			},
+		},
 		themeColor: { type: 'string', nullable: true, pattern: '^#[0-9a-fA-F]{6}$' },
 		mascotImageUrl: { type: 'string', nullable: true },
 		bannerUrl: { type: 'string', nullable: true },
@@ -78,6 +83,7 @@ export const paramDef = {
 		enableTurnstile: { type: 'boolean' },
 		turnstileSiteKey: { type: 'string', nullable: true },
 		turnstileSecretKey: { type: 'string', nullable: true },
+		enableTestcaptcha: { type: 'boolean' },
 		sensitiveMediaDetection: { type: 'string', enum: ['none', 'all', 'local', 'remote'] },
 		sensitiveMediaDetectionSensitivity: { type: 'string', enum: ['medium', 'low', 'high', 'veryLow', 'veryHigh'] },
 		setSensitiveFlagAutomatically: { type: 'boolean' },
@@ -130,6 +136,7 @@ export const paramDef = {
 		truemailAuthKey: { type: 'string', nullable: true },
 		enableChartsForRemoteUser: { type: 'boolean' },
 		enableChartsForFederatedInstances: { type: 'boolean' },
+		enableStatsForFederatedInstances: { type: 'boolean' },
 		enableServerMachineStats: { type: 'boolean' },
 		enableIdenticonGeneration: { type: 'boolean' },
 		serverRules: { type: 'array', items: { type: 'string' } },
@@ -142,6 +149,7 @@ export const paramDef = {
 		perRemoteUserUserTimelineCacheMax: { type: 'integer' },
 		perUserHomeTimelineCacheMax: { type: 'integer' },
 		perUserListTimelineCacheMax: { type: 'integer' },
+		enableReactionsBuffering: { type: 'boolean' },
 		notesPerOneAd: { type: 'integer' },
 		silencedHosts: {
 			type: 'array',
@@ -150,6 +158,13 @@ export const paramDef = {
 				type: 'string',
 			},
 		},
+		mediaSilencedHosts: {
+			type: 'array',
+			nullable: true,
+			items: {
+				type: 'string',
+			},
+		},
 		summalyProxy: {
 			type: 'string', nullable: true,
 			description: '[Deprecated] Use "urlPreviewSummaryProxyUrl" instead.',
@@ -160,6 +175,16 @@ export const paramDef = {
 		urlPreviewRequireContentLength: { type: 'boolean' },
 		urlPreviewUserAgent: { type: 'string', nullable: true },
 		urlPreviewSummaryProxyUrl: { type: 'string', nullable: true },
+		federation: {
+			type: 'string',
+			enum: ['all', 'none', 'specified'],
+		},
+		federationHosts: {
+			type: 'array',
+			items: {
+				type: 'string',
+			},
+		},
 	},
 	required: [],
 } as const;
@@ -195,6 +220,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 			if (Array.isArray(ps.prohibitedWords)) {
 				set.prohibitedWords = ps.prohibitedWords.filter(Boolean);
 			}
+			if (Array.isArray(ps.prohibitedWordsForNameOfUser)) {
+				set.prohibitedWordsForNameOfUser = ps.prohibitedWordsForNameOfUser.filter(Boolean);
+			}
 			if (Array.isArray(ps.silencedHosts)) {
 				let lastValue = '';
 				set.silencedHosts = ps.silencedHosts.sort().filter((h) => {
@@ -203,6 +231,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 					return h !== '' && h !== lv && !set.blockedHosts?.includes(h);
 				});
 			}
+			if (Array.isArray(ps.mediaSilencedHosts)) {
+				let lastValue = '';
+				set.mediaSilencedHosts = ps.mediaSilencedHosts.sort().filter((h) => {
+					const lv = lastValue;
+					lastValue = h;
+					return h !== '' && h !== lv && !set.blockedHosts?.includes(h);
+				});
+			}
 			if (ps.themeColor !== undefined) {
 				set.themeColor = ps.themeColor;
 			}
@@ -331,6 +367,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 				set.turnstileSecretKey = ps.turnstileSecretKey;
 			}
 
+			if (ps.enableTestcaptcha !== undefined) {
+				set.enableTestcaptcha = ps.enableTestcaptcha;
+			}
+
 			if (ps.sensitiveMediaDetection !== undefined) {
 				set.sensitiveMediaDetection = ps.sensitiveMediaDetection;
 			}
@@ -539,6 +579,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 				set.enableChartsForFederatedInstances = ps.enableChartsForFederatedInstances;
 			}
 
+			if (ps.enableStatsForFederatedInstances !== undefined) {
+				set.enableStatsForFederatedInstances = ps.enableStatsForFederatedInstances;
+			}
+
 			if (ps.enableServerMachineStats !== undefined) {
 				set.enableServerMachineStats = ps.enableServerMachineStats;
 			}
@@ -583,6 +627,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 				set.perUserListTimelineCacheMax = ps.perUserListTimelineCacheMax;
 			}
 
+			if (ps.enableReactionsBuffering !== undefined) {
+				set.enableReactionsBuffering = ps.enableReactionsBuffering;
+			}
+
 			if (ps.notesPerOneAd !== undefined) {
 				set.notesPerOneAd = ps.notesPerOneAd;
 			}
@@ -617,6 +665,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 				set.urlPreviewSummaryProxyUrl = value === '' ? null : value;
 			}
 
+			if (ps.federation !== undefined) {
+				set.federation = ps.federation;
+			}
+
+			if (Array.isArray(ps.federationHosts)) {
+				set.federationHosts = ps.federationHosts.filter(Boolean).map(x => x.toLowerCase());
+			}
+
 			const before = await this.metaService.fetch(true);
 
 			await this.metaService.update(set);
diff --git a/packages/backend/src/server/api/endpoints/antennas/create.ts b/packages/backend/src/server/api/endpoints/antennas/create.ts
index ec08198514..e0c8ddcc84 100644
--- a/packages/backend/src/server/api/endpoints/antennas/create.ts
+++ b/packages/backend/src/server/api/endpoints/antennas/create.ts
@@ -34,6 +34,12 @@ export const meta = {
 			code: 'TOO_MANY_ANTENNAS',
 			id: 'faf47050-e8b5-438c-913c-db2b1576fde4',
 		},
+
+		emptyKeyword: {
+			message: 'Either keywords or excludeKeywords is required.',
+			code: 'EMPTY_KEYWORD',
+			id: '53ee222e-1ddd-4f9a-92e5-9fb82ddb463a',
+		},
 	},
 
 	res: {
@@ -87,13 +93,13 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 	) {
 		super(meta, paramDef, async (ps, me) => {
 			if (ps.keywords.flat().every(x => x === '') && ps.excludeKeywords.flat().every(x => x === '')) {
-				throw new Error('either keywords or excludeKeywords is required.');
+				throw new ApiError(meta.errors.emptyKeyword);
 			}
 
 			const currentAntennasCount = await this.antennasRepository.countBy({
 				userId: me.id,
 			});
-			if (currentAntennasCount > (await this.roleService.getUserPolicies(me.id)).antennaLimit) {
+			if (currentAntennasCount >= (await this.roleService.getUserPolicies(me.id)).antennaLimit) {
 				throw new ApiError(meta.errors.tooManyAntennas);
 			}
 
diff --git a/packages/backend/src/server/api/endpoints/antennas/update.ts b/packages/backend/src/server/api/endpoints/antennas/update.ts
index 0c30bca9e0..10f26b1912 100644
--- a/packages/backend/src/server/api/endpoints/antennas/update.ts
+++ b/packages/backend/src/server/api/endpoints/antennas/update.ts
@@ -32,6 +32,12 @@ export const meta = {
 			code: 'NO_SUCH_USER_LIST',
 			id: '1c6b35c9-943e-48c2-81e4-2844989407f7',
 		},
+
+		emptyKeyword: {
+			message: 'Either keywords or excludeKeywords is required.',
+			code: 'EMPTY_KEYWORD',
+			id: '721aaff6-4e1b-4d88-8de6-877fae9f68c4',
+		},
 	},
 
 	res: {
@@ -85,7 +91,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 		super(meta, paramDef, async (ps, me) => {
 			if (ps.keywords && ps.excludeKeywords) {
 				if (ps.keywords.flat().every(x => x === '') && ps.excludeKeywords.flat().every(x => x === '')) {
-					throw new Error('either keywords or excludeKeywords is required.');
+					throw new ApiError(meta.errors.emptyKeyword);
 				}
 			}
 			// Fetch the antenna
diff --git a/packages/backend/src/server/api/endpoints/ap/show.ts b/packages/backend/src/server/api/endpoints/ap/show.ts
index d3c40dba59..c52608cefb 100644
--- a/packages/backend/src/server/api/endpoints/ap/show.ts
+++ b/packages/backend/src/server/api/endpoints/ap/show.ts
@@ -3,7 +3,7 @@
  * SPDX-License-Identifier: AGPL-3.0-only
  */
 
-import { Injectable } from '@nestjs/common';
+import { Inject, Injectable } from '@nestjs/common';
 import ms from 'ms';
 import { Endpoint } from '@/server/api/endpoint-base.js';
 import type { MiNote } from '@/models/Note.js';
@@ -12,7 +12,6 @@ import { isActor, isPost, getApId } from '@/core/activitypub/type.js';
 import type { SchemaType } from '@/misc/json-schema.js';
 import { ApResolverService } from '@/core/activitypub/ApResolverService.js';
 import { ApDbResolverService } from '@/core/activitypub/ApDbResolverService.js';
-import { MetaService } from '@/core/MetaService.js';
 import { ApPersonService } from '@/core/activitypub/models/ApPersonService.js';
 import { ApNoteService } from '@/core/activitypub/models/ApNoteService.js';
 import { UserEntityService } from '@/core/entities/UserEntityService.js';
@@ -91,7 +90,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 		private utilityService: UtilityService,
 		private userEntityService: UserEntityService,
 		private noteEntityService: NoteEntityService,
-		private metaService: MetaService,
 		private apResolverService: ApResolverService,
 		private apDbResolverService: ApDbResolverService,
 		private apPersonService: ApPersonService,
@@ -112,9 +110,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 	 */
 	@bindThis
 	private async fetchAny(uri: string, me: MiLocalUser | null | undefined): Promise<SchemaType<typeof meta['res']> | null> {
-	// ブロックしてたら中断
-		const fetchedMeta = await this.metaService.fetch();
-		if (this.utilityService.isBlockedHost(fetchedMeta.blockedHosts, this.utilityService.extractDbHost(uri))) return null;
+		if (!this.utilityService.isFederationAllowedUri(uri)) return null;
 
 		let local = await this.mergePack(me, ...await Promise.all([
 			this.apDbResolverService.getUserFromApId(uri),
diff --git a/packages/backend/src/server/api/endpoints/channels/timeline.ts b/packages/backend/src/server/api/endpoints/channels/timeline.ts
index 8c55673590..d4fd75e049 100644
--- a/packages/backend/src/server/api/endpoints/channels/timeline.ts
+++ b/packages/backend/src/server/api/endpoints/channels/timeline.ts
@@ -5,14 +5,12 @@
 
 import { Inject, Injectable } from '@nestjs/common';
 import { Endpoint } from '@/server/api/endpoint-base.js';
-import type { ChannelsRepository, NotesRepository } from '@/models/_.js';
+import type { ChannelsRepository, MiMeta, NotesRepository } from '@/models/_.js';
 import { QueryService } from '@/core/QueryService.js';
 import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
 import ActiveUsersChart from '@/core/chart/charts/active-users.js';
 import { DI } from '@/di-symbols.js';
 import { IdService } from '@/core/IdService.js';
-import { CacheService } from '@/core/CacheService.js';
-import { MetaService } from '@/core/MetaService.js';
 import { FanoutTimelineEndpointService } from '@/core/FanoutTimelineEndpointService.js';
 import { MiLocalUser } from '@/models/User.js';
 import { ApiError } from '../../error.js';
@@ -58,6 +56,9 @@ export const paramDef = {
 @Injectable()
 export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
 	constructor(
+		@Inject(DI.meta)
+		private serverSettings: MiMeta,
+
 		@Inject(DI.notesRepository)
 		private notesRepository: NotesRepository,
 
@@ -68,16 +69,12 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 		private noteEntityService: NoteEntityService,
 		private queryService: QueryService,
 		private fanoutTimelineEndpointService: FanoutTimelineEndpointService,
-		private cacheService: CacheService,
 		private activeUsersChart: ActiveUsersChart,
-		private metaService: MetaService,
 	) {
 		super(meta, paramDef, async (ps, me) => {
 			const untilId = ps.untilId ?? (ps.untilDate ? this.idService.gen(ps.untilDate!) : null);
 			const sinceId = ps.sinceId ?? (ps.sinceDate ? this.idService.gen(ps.sinceDate!) : null);
 
-			const serverSettings = await this.metaService.fetch();
-
 			const channel = await this.channelsRepository.findOneBy({
 				id: ps.channelId,
 			});
@@ -88,7 +85,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 
 			if (me) this.activeUsersChart.read(me);
 
-			if (!serverSettings.enableFanoutTimeline) {
+			if (!this.serverSettings.enableFanoutTimeline) {
 				return await this.noteEntityService.packMany(await this.getFromDb({ untilId, sinceId, limit: ps.limit, channelId: channel.id }, me), me);
 			}
 
diff --git a/packages/backend/src/server/api/endpoints/clips/update.ts b/packages/backend/src/server/api/endpoints/clips/update.ts
index 3b44ba81b3..603a3ccf3d 100644
--- a/packages/backend/src/server/api/endpoints/clips/update.ts
+++ b/packages/backend/src/server/api/endpoints/clips/update.ts
@@ -3,7 +3,7 @@
  * SPDX-License-Identifier: AGPL-3.0-only
  */
 
-import { Inject, Injectable } from '@nestjs/common';
+import { Injectable } from '@nestjs/common';
 import { Endpoint } from '@/server/api/endpoint-base.js';
 import { ClipEntityService } from '@/core/entities/ClipEntityService.js';
 import { ClipService } from '@/core/ClipService.js';
@@ -41,7 +41,7 @@ export const paramDef = {
 		isPublic: { type: 'boolean' },
 		description: { type: 'string', nullable: true, minLength: 1, maxLength: 2048 },
 	},
-	required: ['clipId', 'name'],
+	required: ['clipId'],
 } as const;
 
 @Injectable()
diff --git a/packages/backend/src/server/api/endpoints/drive.ts b/packages/backend/src/server/api/endpoints/drive.ts
index 7e9b0fa0e1..eb45e29f9e 100644
--- a/packages/backend/src/server/api/endpoints/drive.ts
+++ b/packages/backend/src/server/api/endpoints/drive.ts
@@ -5,7 +5,6 @@
 
 import { Injectable } from '@nestjs/common';
 import { Endpoint } from '@/server/api/endpoint-base.js';
-import { MetaService } from '@/core/MetaService.js';
 import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
 import { RoleService } from '@/core/RoleService.js';
 
@@ -41,14 +40,10 @@ export const paramDef = {
 @Injectable()
 export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
 	constructor(
-		private metaService: MetaService,
 		private driveFileEntityService: DriveFileEntityService,
 		private roleService: RoleService,
 	) {
 		super(meta, paramDef, async (ps, me) => {
-			const instance = await this.metaService.fetch(true);
-
-			// Calculate drive usage
 			const usage = await this.driveFileEntityService.calcDriveUsageOf(me.id);
 
 			const policies = await this.roleService.getUserPolicies(me.id);
diff --git a/packages/backend/src/server/api/endpoints/drive/files/attached-notes.ts b/packages/backend/src/server/api/endpoints/drive/files/attached-notes.ts
index 4670392025..b86059b5e7 100644
--- a/packages/backend/src/server/api/endpoints/drive/files/attached-notes.ts
+++ b/packages/backend/src/server/api/endpoints/drive/files/attached-notes.ts
@@ -10,6 +10,7 @@ import { QueryService } from '@/core/QueryService.js';
 import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
 import { DI } from '@/di-symbols.js';
 import { ApiError } from '../../../error.js';
+import { RoleService } from '@/core/RoleService.js';
 
 export const meta = {
 	tags: ['drive', 'notes'],
@@ -61,12 +62,13 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 
 		private noteEntityService: NoteEntityService,
 		private queryService: QueryService,
+		private roleService: RoleService,
 	) {
 		super(meta, paramDef, async (ps, me) => {
 			// Fetch file
 			const file = await this.driveFilesRepository.findOneBy({
 				id: ps.fileId,
-				userId: me.id,
+				userId: await this.roleService.isModerator(me) ? undefined : me.id,
 			});
 
 			if (file == null) {
diff --git a/packages/backend/src/server/api/endpoints/drive/files/create.ts b/packages/backend/src/server/api/endpoints/drive/files/create.ts
index 9c17f93ab2..74eb4dded7 100644
--- a/packages/backend/src/server/api/endpoints/drive/files/create.ts
+++ b/packages/backend/src/server/api/endpoints/drive/files/create.ts
@@ -4,14 +4,15 @@
  */
 
 import ms from 'ms';
-import { Injectable } from '@nestjs/common';
+import { Inject, Injectable } from '@nestjs/common';
 import { DB_MAX_IMAGE_COMMENT_LENGTH } from '@/const.js';
 import { IdentifiableError } from '@/misc/identifiable-error.js';
 import { Endpoint } from '@/server/api/endpoint-base.js';
 import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
-import { MetaService } from '@/core/MetaService.js';
 import { DriveService } from '@/core/DriveService.js';
 import { ApiError } from '../../../error.js';
+import { MiMeta } from '@/models/_.js';
+import { DI } from '@/di-symbols.js';
 
 export const meta = {
 	tags: ['drive'],
@@ -73,8 +74,10 @@ export const paramDef = {
 @Injectable()
 export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
 	constructor(
+		@Inject(DI.meta)
+		private serverSettings: MiMeta,
+
 		private driveFileEntityService: DriveFileEntityService,
-		private metaService: MetaService,
 		private driveService: DriveService,
 	) {
 		super(meta, paramDef, async (ps, me, _, file, cleanup, ip, headers) => {
@@ -91,8 +94,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 				}
 			}
 
-			const instance = await this.metaService.fetch();
-
 			try {
 				// Create file
 				const driveFile = await this.driveService.addFile({
@@ -103,8 +104,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 					folderId: ps.folderId,
 					force: ps.force,
 					sensitive: ps.isSensitive,
-					requestIp: instance.enableIpLogging ? ip : null,
-					requestHeaders: instance.enableIpLogging ? headers : null,
+					requestIp: this.serverSettings.enableIpLogging ? ip : null,
+					requestHeaders: this.serverSettings.enableIpLogging ? headers : null,
 				});
 				return await this.driveFileEntityService.pack(driveFile, { self: true });
 			} catch (err) {
diff --git a/packages/backend/src/server/api/endpoints/drive/folders/update.ts b/packages/backend/src/server/api/endpoints/drive/folders/update.ts
index 52b8b335b5..62b04e1df3 100644
--- a/packages/backend/src/server/api/endpoints/drive/folders/update.ts
+++ b/packages/backend/src/server/api/endpoints/drive/folders/update.ts
@@ -95,15 +95,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 
 					// Check if the circular reference will occur
 					const checkCircle = async (folderId: string): Promise<boolean> => {
-						// Fetch folder
-						const folder2 = await this.driveFoldersRepository.findOneBy({
+						const folder2 = await this.driveFoldersRepository.findOneByOrFail({
 							id: folderId,
 						});
 
-						if (folder2!.id === folder!.id) {
+						if (folder2.id === folder.id) {
 							return true;
-						} else if (folder2!.parentId) {
-							return await checkCircle(folder2!.parentId);
+						} else if (folder2.parentId) {
+							return await checkCircle(folder2.parentId);
 						} else {
 							return false;
 						}
diff --git a/packages/backend/src/server/api/endpoints/federation/instances.ts b/packages/backend/src/server/api/endpoints/federation/instances.ts
index 4ef4315fb3..41954129e6 100644
--- a/packages/backend/src/server/api/endpoints/federation/instances.ts
+++ b/packages/backend/src/server/api/endpoints/federation/instances.ts
@@ -117,9 +117,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 
 			if (typeof ps.suspended === 'boolean') {
 				if (ps.suspended) {
-					query.andWhere('instance.isSuspended = TRUE');
+					query.andWhere('instance.suspensionState != \'none\'');
 				} else {
-					query.andWhere('instance.isSuspended = FALSE');
+					query.andWhere('instance.suspensionState = \'none\'');
 				}
 			}
 
@@ -170,7 +170,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 
 			const instances = await query.limit(ps.limit).offset(ps.offset).getMany();
 
-			return await this.instanceEntityService.packMany(instances);
+			return await this.instanceEntityService.packMany(instances, me);
 		});
 	}
 }
diff --git a/packages/backend/src/server/api/endpoints/federation/stats.ts b/packages/backend/src/server/api/endpoints/federation/stats.ts
index bac54970ab..69900bff9a 100644
--- a/packages/backend/src/server/api/endpoints/federation/stats.ts
+++ b/packages/backend/src/server/api/endpoints/federation/stats.ts
@@ -107,9 +107,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 			const gotPubCount = topPubInstances.map(x => x.followingCount).reduce((a, b) => a + b, 0);
 
 			return await awaitAll({
-				topSubInstances: this.instanceEntityService.packMany(topSubInstances),
+				topSubInstances: this.instanceEntityService.packMany(topSubInstances, me),
 				otherFollowersCount: Math.max(0, allSubCount - gotSubCount),
-				topPubInstances: this.instanceEntityService.packMany(topPubInstances),
+				topPubInstances: this.instanceEntityService.packMany(topPubInstances, me),
 				otherFollowingCount: Math.max(0, allPubCount - gotPubCount),
 			});
 		});
diff --git a/packages/backend/src/server/api/endpoints/flash/delete.ts b/packages/backend/src/server/api/endpoints/flash/delete.ts
index d3d47e5deb..6912450abf 100644
--- a/packages/backend/src/server/api/endpoints/flash/delete.ts
+++ b/packages/backend/src/server/api/endpoints/flash/delete.ts
@@ -4,9 +4,11 @@
  */
 
 import { Inject, Injectable } from '@nestjs/common';
-import type { FlashsRepository } from '@/models/_.js';
+import type { FlashsRepository, UsersRepository } from '@/models/_.js';
 import { Endpoint } from '@/server/api/endpoint-base.js';
 import { DI } from '@/di-symbols.js';
+import { ModerationLogService } from '@/core/ModerationLogService.js';
+import { RoleService } from '@/core/RoleService.js';
 import { ApiError } from '../../error.js';
 
 export const meta = {
@@ -44,17 +46,35 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 	constructor(
 		@Inject(DI.flashsRepository)
 		private flashsRepository: FlashsRepository,
+
+		@Inject(DI.usersRepository)
+		private usersRepository: UsersRepository,
+
+		private moderationLogService: ModerationLogService,
+		private roleService: RoleService,
 	) {
 		super(meta, paramDef, async (ps, me) => {
 			const flash = await this.flashsRepository.findOneBy({ id: ps.flashId });
+
 			if (flash == null) {
 				throw new ApiError(meta.errors.noSuchFlash);
 			}
-			if (flash.userId !== me.id) {
+
+			if (!await this.roleService.isModerator(me) && flash.userId !== me.id) {
 				throw new ApiError(meta.errors.accessDenied);
 			}
 
 			await this.flashsRepository.delete(flash.id);
+
+			if (flash.userId !== me.id) {
+				const user = await this.usersRepository.findOneByOrFail({ id: flash.userId });
+				this.moderationLogService.log(me, 'deleteFlash', {
+					flashId: flash.id,
+					flashUserId: flash.userId,
+					flashUserUsername: user.username,
+					flash,
+				});
+			}
 		});
 	}
 }
diff --git a/packages/backend/src/server/api/endpoints/flash/featured.ts b/packages/backend/src/server/api/endpoints/flash/featured.ts
index c2d6ab5085..9a0cb461f2 100644
--- a/packages/backend/src/server/api/endpoints/flash/featured.ts
+++ b/packages/backend/src/server/api/endpoints/flash/featured.ts
@@ -8,6 +8,7 @@ import type { FlashsRepository } from '@/models/_.js';
 import { Endpoint } from '@/server/api/endpoint-base.js';
 import { FlashEntityService } from '@/core/entities/FlashEntityService.js';
 import { DI } from '@/di-symbols.js';
+import { FlashService } from '@/core/FlashService.js';
 
 export const meta = {
 	tags: ['flash'],
@@ -27,26 +28,25 @@ export const meta = {
 
 export const paramDef = {
 	type: 'object',
-	properties: {},
+	properties: {
+		offset: { type: 'integer', minimum: 0, default: 0 },
+		limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
+	},
 	required: [],
 } as const;
 
 @Injectable()
 export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
 	constructor(
-		@Inject(DI.flashsRepository)
-		private flashsRepository: FlashsRepository,
-
+		private flashService: FlashService,
 		private flashEntityService: FlashEntityService,
 	) {
 		super(meta, paramDef, async (ps, me) => {
-			const query = this.flashsRepository.createQueryBuilder('flash')
-				.andWhere('flash.likedCount > 0')
-				.orderBy('flash.likedCount', 'DESC');
-
-			const flashs = await query.limit(10).getMany();
-
-			return await this.flashEntityService.packMany(flashs, me);
+			const result = await this.flashService.featured({
+				offset: ps.offset,
+				limit: ps.limit,
+			});
+			return await this.flashEntityService.packMany(result, me);
 		});
 	}
 }
diff --git a/packages/backend/src/server/api/endpoints/gallery/posts/create.ts b/packages/backend/src/server/api/endpoints/gallery/posts/create.ts
index 46f8998810..504a9c789e 100644
--- a/packages/backend/src/server/api/endpoints/gallery/posts/create.ts
+++ b/packages/backend/src/server/api/endpoints/gallery/posts/create.ts
@@ -12,7 +12,6 @@ import type { MiDriveFile } from '@/models/DriveFile.js';
 import { IdService } from '@/core/IdService.js';
 import { GalleryPostEntityService } from '@/core/entities/GalleryPostEntityService.js';
 import { DI } from '@/di-symbols.js';
-import { isNotNull } from '@/misc/is-not-null.js';
 
 export const meta = {
 	tags: ['gallery'],
@@ -70,7 +69,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 					id: fileId,
 					userId: me.id,
 				}),
-			))).filter(isNotNull);
+			))).filter(x => x != null);
 
 			if (files.length === 0) {
 				throw new Error();
diff --git a/packages/backend/src/server/api/endpoints/gallery/posts/delete.ts b/packages/backend/src/server/api/endpoints/gallery/posts/delete.ts
index 527e3fb52d..b6b94db161 100644
--- a/packages/backend/src/server/api/endpoints/gallery/posts/delete.ts
+++ b/packages/backend/src/server/api/endpoints/gallery/posts/delete.ts
@@ -5,8 +5,10 @@
 
 import { Inject, Injectable } from '@nestjs/common';
 import { Endpoint } from '@/server/api/endpoint-base.js';
-import type { GalleryPostsRepository } from '@/models/_.js';
+import type { GalleryPostsRepository, UsersRepository } from '@/models/_.js';
 import { DI } from '@/di-symbols.js';
+import { ModerationLogService } from '@/core/ModerationLogService.js';
+import { RoleService } from '@/core/RoleService.js';
 import { ApiError } from '../../../error.js';
 
 export const meta = {
@@ -22,6 +24,12 @@ export const meta = {
 			code: 'NO_SUCH_POST',
 			id: 'ae52f367-4bd7-4ecd-afc6-5672fff427f5',
 		},
+
+		accessDenied: {
+			message: 'Access denied.',
+			code: 'ACCESS_DENIED',
+			id: 'c86e09de-1c48-43ac-a435-1c7e42ed4496',
+		},
 	},
 } as const;
 
@@ -38,18 +46,35 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 	constructor(
 		@Inject(DI.galleryPostsRepository)
 		private galleryPostsRepository: GalleryPostsRepository,
+
+		@Inject(DI.usersRepository)
+		private usersRepository: UsersRepository,
+
+		private moderationLogService: ModerationLogService,
+		private roleService: RoleService,
 	) {
 		super(meta, paramDef, async (ps, me) => {
-			const post = await this.galleryPostsRepository.findOneBy({
-				id: ps.postId,
-				userId: me.id,
-			});
+			const post = await this.galleryPostsRepository.findOneBy({ id: ps.postId });
 
 			if (post == null) {
 				throw new ApiError(meta.errors.noSuchPost);
 			}
 
+			if (!await this.roleService.isModerator(me) && post.userId !== me.id) {
+				throw new ApiError(meta.errors.accessDenied);
+			}
+
 			await this.galleryPostsRepository.delete(post.id);
+
+			if (post.userId !== me.id) {
+				const user = await this.usersRepository.findOneByOrFail({ id: post.userId });
+				this.moderationLogService.log(me, 'deleteGalleryPost', {
+					postId: post.id,
+					postUserId: post.userId,
+					postUserUsername: user.username,
+					post,
+				});
+			}
 		});
 	}
 }
diff --git a/packages/backend/src/server/api/endpoints/gallery/posts/update.ts b/packages/backend/src/server/api/endpoints/gallery/posts/update.ts
index 8bd83ff5ba..5243ee9603 100644
--- a/packages/backend/src/server/api/endpoints/gallery/posts/update.ts
+++ b/packages/backend/src/server/api/endpoints/gallery/posts/update.ts
@@ -10,7 +10,6 @@ import type { DriveFilesRepository, GalleryPostsRepository } from '@/models/_.js
 import type { MiDriveFile } from '@/models/DriveFile.js';
 import { GalleryPostEntityService } from '@/core/entities/GalleryPostEntityService.js';
 import { DI } from '@/di-symbols.js';
-import { isNotNull } from '@/misc/is-not-null.js';
 
 export const meta = {
 	tags: ['gallery'],
@@ -48,7 +47,7 @@ export const paramDef = {
 		} },
 		isSensitive: { type: 'boolean', default: false },
 	},
-	required: ['postId', 'title', 'fileIds'],
+	required: ['postId'],
 } as const;
 
 @Injectable()
@@ -63,15 +62,19 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 		private galleryPostEntityService: GalleryPostEntityService,
 	) {
 		super(meta, paramDef, async (ps, me) => {
-			const files = (await Promise.all(ps.fileIds.map(fileId =>
-				this.driveFilesRepository.findOneBy({
-					id: fileId,
-					userId: me.id,
-				}),
-			))).filter(isNotNull);
+			let files: Array<MiDriveFile> | undefined;
 
-			if (files.length === 0) {
-				throw new Error();
+			if (ps.fileIds) {
+				files = (await Promise.all(ps.fileIds.map(fileId =>
+					this.driveFilesRepository.findOneBy({
+						id: fileId,
+						userId: me.id,
+					}),
+				))).filter(x => x != null);
+
+				if (files.length === 0) {
+					throw new Error();
+				}
 			}
 
 			await this.galleryPostsRepository.update({
@@ -82,7 +85,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 				title: ps.title,
 				description: ps.description,
 				isSensitive: ps.isSensitive,
-				fileIds: files.map(file => file.id),
+				fileIds: files ? files.map(file => file.id) : undefined,
 			});
 
 			const post = await this.galleryPostsRepository.findOneByOrFail({ id: ps.postId });
diff --git a/packages/backend/src/server/api/endpoints/i/import-antennas.ts b/packages/backend/src/server/api/endpoints/i/import-antennas.ts
index b4661a93e2..bdf6c065e8 100644
--- a/packages/backend/src/server/api/endpoints/i/import-antennas.ts
+++ b/packages/backend/src/server/api/endpoints/i/import-antennas.ts
@@ -16,6 +16,7 @@ import { ApiError } from '../../error.js';
 export const meta = {
 	secure: true,
 	requireCredential: true,
+	requireRolePolicy: 'canImportAntennas',
 	prohibitMoved: true,
 
 	limit: {
@@ -78,7 +79,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
 			if (file.size === 0) throw new ApiError(meta.errors.emptyFile);
 			const antennas: (_Antenna & { userListAccts: string[] | null })[] = JSON.parse(await this.downloadService.downloadTextFile(file.url));
 			const currentAntennasCount = await this.antennasRepository.countBy({ userId: me.id });
-			if (currentAntennasCount + antennas.length > (await this.roleService.getUserPolicies(me.id)).antennaLimit) {
+			if (currentAntennasCount + antennas.length >= (await this.roleService.getUserPolicies(me.id)).antennaLimit) {
 				throw new ApiError(meta.errors.tooManyAntennas);
 			}
 			this.queueService.createImportAntennasJob(me, antennas);
diff --git a/packages/backend/src/server/api/endpoints/i/import-blocking.ts b/packages/backend/src/server/api/endpoints/i/import-blocking.ts
index 2606108539..d7bb6bcd22 100644
--- a/packages/backend/src/server/api/endpoints/i/import-blocking.ts
+++ b/packages/backend/src/server/api/endpoints/i/import-blocking.ts
@@ -15,6 +15,7 @@ import { ApiError } from '../../error.js';
 export const meta = {
 	secure: true,
 	requireCredential: true,
+	requireRolePolicy: 'canImportBlocking',
 	prohibitMoved: true,
 
 	limit: {
diff --git a/packages/backend/src/server/api/endpoints/i/import-following.ts b/packages/backend/src/server/api/endpoints/i/import-following.ts
index d5e824df27..e03192d8c6 100644
--- a/packages/backend/src/server/api/endpoints/i/import-following.ts
+++ b/packages/backend/src/server/api/endpoints/i/import-following.ts
@@ -15,6 +15,7 @@ import { ApiError } from '../../error.js';
 export const meta = {
 	secure: true,
 	requireCredential: true,
+	requireRolePolicy: 'canImportFollowing',
 	prohibitMoved: true,
 	limit: {
 		duration: ms('1hour'),
diff --git a/packages/backend/src/server/api/endpoints/i/import-muting.ts b/packages/backend/src/server/api/endpoints/i/import-muting.ts
index 0f5800404e..76b285bb7e 100644
--- a/packages/backend/src/server/api/endpoints/i/import-muting.ts
+++ b/packages/backend/src/server/api/endpoints/i/import-muting.ts
@@ -15,6 +15,7 @@ import { ApiError } from '../../error.js';
 export const meta = {
 	secure: true,
 	requireCredential: true,
+	requireRolePolicy: 'canImportMuting',
 	prohibitMoved: true,
 
 	limit: {
diff --git a/packages/backend/src/server/api/endpoints/i/import-user-lists.ts b/packages/backend/src/server/api/endpoints/i/import-user-lists.ts
index bacdd5c88f..76ecfd082c 100644
--- a/packages/backend/src/server/api/endpoints/i/import-user-lists.ts
+++ b/packages/backend/src/server/api/endpoints/i/import-user-lists.ts
@@ -15,6 +15,7 @@ import { ApiError } from '../../error.js';
 export const meta = {
 	secure: true,
 	requireCredential: true,
+	requireRolePolicy: 'canImportUserLists',
 	prohibitMoved: true,
 	limit: {
 		duration: ms('1hour'),
diff --git a/packages/backend/src/server/api/endpoints/i/update-email.ts b/packages/backend/src/server/api/endpoints/i/update-email.ts
index eea657ebbd..da1faee30d 100644
--- a/packages/backend/src/server/api/endpoints/i/update-email.ts
+++ b/packages/backend/src/server/api/endpoints/i/update-email.ts
@@ -7,7 +7,7 @@ import { Inject, Injectable } from '@nestjs/common';
 import ms from 'ms';
 import bcrypt from 'bcryptjs';
 import { Endpoint } from '@/server/api/endpoint-base.js';
-import type { UserProfilesRepository } from '@/models/_.js';
+import type { MiMeta, UserProfilesRepository } from '@/models/_.js';
 import { UserEntityService } from '@/core/entities/UserEntityService.js';
 import { EmailService } from '@/core/EmailService.js';
 import type { Config } from '@/config.js';
@@ -15,7 +15,6 @@ import { DI } from '@/di-symbols.js';
 import { GlobalEventService } from '@/core/GlobalEventService.js';
 import { L_CHARS, secureRndstr } from '@/misc/secure-rndstr.js';
 import { UserAuthService } from '@/core/UserAuthService.js';
-import { MetaService } from '@/core/MetaService.js';
 import { ApiError } from '../../error.js';
 
 export const meta = {
@@ -70,10 +69,12 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 		@Inject(DI.config)
 		private config: Config,
 
+		@Inject(DI.meta)
+		private serverSettings: MiMeta,
+
 		@Inject(DI.userProfilesRepository)
 		private userProfilesRepository: UserProfilesRepository,
 
-		private metaService: MetaService,
 		private userEntityService: UserEntityService,
 		private emailService: EmailService,
 		private userAuthService: UserAuthService,
@@ -105,7 +106,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 				if (!res.available) {
 					throw new ApiError(meta.errors.unavailable);
 				}
-			} else if ((await this.metaService.fetch()).emailRequiredForSignup) {
+			} else if (this.serverSettings.emailRequiredForSignup) {
 				throw new ApiError(meta.errors.emailRequired);
 			}
 
diff --git a/packages/backend/src/server/api/endpoints/i/update.ts b/packages/backend/src/server/api/endpoints/i/update.ts
index a8e702f328..2183beac7c 100644
--- a/packages/backend/src/server/api/endpoints/i/update.ts
+++ b/packages/backend/src/server/api/endpoints/i/update.ts
@@ -11,11 +11,10 @@ import { JSDOM } from 'jsdom';
 import { extractCustomEmojisFromMfm } from '@/misc/extract-custom-emojis-from-mfm.js';
 import { extractHashtags } from '@/misc/extract-hashtags.js';
 import * as Acct from '@/misc/acct.js';
-import type { UsersRepository, DriveFilesRepository, UserProfilesRepository, PagesRepository } from '@/models/_.js';
+import type { UsersRepository, DriveFilesRepository, MiMeta, UserProfilesRepository, PagesRepository } from '@/models/_.js';
 import type { MiLocalUser, MiUser } from '@/models/User.js';
-import { birthdaySchema, descriptionSchema, locationSchema, nameSchema } from '@/models/User.js';
+import { birthdaySchema, descriptionSchema, followedMessageSchema, locationSchema, nameSchema } from '@/models/User.js';
 import type { MiUserProfile } from '@/models/UserProfile.js';
-import { notificationTypes } from '@/types.js';
 import { normalizeForSearch } from '@/misc/normalize-for-search.js';
 import { langmap } from '@/misc/langmap.js';
 import { Endpoint } from '@/server/api/endpoint-base.js';
@@ -23,9 +22,10 @@ import { UserEntityService } from '@/core/entities/UserEntityService.js';
 import { GlobalEventService } from '@/core/GlobalEventService.js';
 import { UserFollowingService } from '@/core/UserFollowingService.js';
 import { AccountUpdateService } from '@/core/AccountUpdateService.js';
+import { UtilityService } from '@/core/UtilityService.js';
 import { HashtagService } from '@/core/HashtagService.js';
 import { DI } from '@/di-symbols.js';
-import { RoleService } from '@/core/RoleService.js';
+import { RolePolicies, RoleService } from '@/core/RoleService.js';
 import { CacheService } from '@/core/CacheService.js';
 import { RemoteUserResolveService } from '@/core/RemoteUserResolveService.js';
 import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
@@ -115,6 +115,13 @@ export const meta = {
 			code: 'RESTRICTED_BY_ROLE',
 			id: '8feff0ba-5ab5-585b-31f4-4df816663fad',
 		},
+
+		nameContainsProhibitedWords: {
+			message: 'Your new name contains prohibited words.',
+			code: 'YOUR_NAME_CONTAINS_PROHIBITED_WORDS',
+			id: '0b3f9f6a-2f4d-4b1f-9fb4-49d3a2fd7191',
+			httpStatusCode: 422,
+		},
 	},
 
 	res: {
@@ -134,6 +141,7 @@ export const paramDef = {
 	properties: {
 		name: { ...nameSchema, nullable: true },
 		description: { ...descriptionSchema, nullable: true },
+		followedMessage: { ...followedMessageSchema, nullable: true },
 		location: { ...locationSchema, nullable: true },
 		birthday: { ...birthdaySchema, nullable: true },
 		lang: { type: 'string', enum: [null, ...Object.keys(langmap)] as string[], nullable: true },
@@ -171,6 +179,9 @@ export const paramDef = {
 		autoAcceptFollowed: { type: 'boolean' },
 		noCrawle: { type: 'boolean' },
 		preventAiLearning: { type: 'boolean' },
+		requireSigninToViewContents: { type: 'boolean' },
+		makeNotesFollowersOnlyBefore: { type: 'integer', nullable: true },
+		makeNotesHiddenBefore: { type: 'integer', nullable: true },
 		isBot: { type: 'boolean' },
 		isCat: { type: 'boolean' },
 		injectFeaturedNote: { type: 'boolean' },
@@ -223,6 +234,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 		@Inject(DI.config)
 		private config: Config,
 
+		@Inject(DI.meta)
+		private instanceMeta: MiMeta,
+
 		@Inject(DI.usersRepository)
 		private usersRepository: UsersRepository,
 
@@ -247,6 +261,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 		private cacheService: CacheService,
 		private httpRequestService: HttpRequestService,
 		private avatarDecorationService: AvatarDecorationService,
+		private utilityService: UtilityService,
 	) {
 		super(meta, paramDef, async (ps, _user, token) => {
 			const user = await this.usersRepository.findOneByOrFail({ id: _user.id }) as MiLocalUser;
@@ -256,9 +271,18 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 			const profileUpdates = {} as Partial<MiUserProfile>;
 
 			const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id });
+			let policies: RolePolicies | null = null;
 
-			if (ps.name !== undefined) updates.name = ps.name;
+			if (ps.name !== undefined) {
+				if (ps.name === null) {
+					updates.name = null;
+				} else {
+					const trimmedName = ps.name.trim();
+					updates.name = trimmedName === '' ? null : trimmedName;
+				}
+			}
 			if (ps.description !== undefined) profileUpdates.description = ps.description;
+			if (ps.followedMessage !== undefined) profileUpdates.followedMessage = ps.followedMessage;
 			if (ps.lang !== undefined) profileUpdates.lang = ps.lang;
 			if (ps.location !== undefined) profileUpdates.location = ps.location;
 			if (ps.birthday !== undefined) profileUpdates.birthday = ps.birthday;
@@ -289,14 +313,16 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 			}
 
 			if (ps.mutedWords !== undefined) {
-				checkMuteWordCount(ps.mutedWords, (await this.roleService.getUserPolicies(user.id)).wordMuteLimit);
+				policies ??= await this.roleService.getUserPolicies(user.id);
+				checkMuteWordCount(ps.mutedWords, policies.wordMuteLimit);
 				validateMuteWordRegex(ps.mutedWords);
 
 				profileUpdates.mutedWords = ps.mutedWords;
 				profileUpdates.enableWordMute = ps.mutedWords.length > 0;
 			}
 			if (ps.hardMutedWords !== undefined) {
-				checkMuteWordCount(ps.hardMutedWords, (await this.roleService.getUserPolicies(user.id)).wordMuteLimit);
+				policies ??= await this.roleService.getUserPolicies(user.id);
+				checkMuteWordCount(ps.hardMutedWords, policies.wordMuteLimit);
 				validateMuteWordRegex(ps.hardMutedWords);
 				profileUpdates.hardMutedWords = ps.hardMutedWords;
 			}
@@ -311,17 +337,24 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 			if (typeof ps.autoAcceptFollowed === 'boolean') profileUpdates.autoAcceptFollowed = ps.autoAcceptFollowed;
 			if (typeof ps.noCrawle === 'boolean') profileUpdates.noCrawle = ps.noCrawle;
 			if (typeof ps.preventAiLearning === 'boolean') profileUpdates.preventAiLearning = ps.preventAiLearning;
+			if (typeof ps.requireSigninToViewContents === 'boolean') updates.requireSigninToViewContents = ps.requireSigninToViewContents;
+			if ((typeof ps.makeNotesFollowersOnlyBefore === 'number') || (ps.makeNotesFollowersOnlyBefore === null)) updates.makeNotesFollowersOnlyBefore = ps.makeNotesFollowersOnlyBefore;
+			if ((typeof ps.makeNotesHiddenBefore === 'number') || (ps.makeNotesHiddenBefore === null)) updates.makeNotesHiddenBefore = ps.makeNotesHiddenBefore;
 			if (typeof ps.isCat === 'boolean') updates.isCat = ps.isCat;
 			if (typeof ps.injectFeaturedNote === 'boolean') profileUpdates.injectFeaturedNote = ps.injectFeaturedNote;
 			if (typeof ps.receiveAnnouncementEmail === 'boolean') profileUpdates.receiveAnnouncementEmail = ps.receiveAnnouncementEmail;
 			if (typeof ps.alwaysMarkNsfw === 'boolean') {
-				if ((await roleService.getUserPolicies(user.id)).alwaysMarkNsfw) throw new ApiError(meta.errors.restrictedByRole);
+				policies ??= await this.roleService.getUserPolicies(user.id);
+				if (policies.alwaysMarkNsfw) throw new ApiError(meta.errors.restrictedByRole);
 				profileUpdates.alwaysMarkNsfw = ps.alwaysMarkNsfw;
 			}
 			if (typeof ps.autoSensitive === 'boolean') profileUpdates.autoSensitive = ps.autoSensitive;
 			if (ps.emailNotificationTypes !== undefined) profileUpdates.emailNotificationTypes = ps.emailNotificationTypes;
 
 			if (ps.avatarId) {
+				policies ??= await this.roleService.getUserPolicies(user.id);
+				if (!policies.canUpdateBioMedia) throw new ApiError(meta.errors.restrictedByRole);
+
 				const avatar = await this.driveFilesRepository.findOneBy({ id: ps.avatarId });
 
 				if (avatar == null || avatar.userId !== user.id) throw new ApiError(meta.errors.noSuchAvatar);
@@ -337,6 +370,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 			}
 
 			if (ps.bannerId) {
+				policies ??= await this.roleService.getUserPolicies(user.id);
+				if (!policies.canUpdateBioMedia) throw new ApiError(meta.errors.restrictedByRole);
+
 				const banner = await this.driveFilesRepository.findOneBy({ id: ps.bannerId });
 
 				if (banner == null || banner.userId !== user.id) throw new ApiError(meta.errors.noSuchBanner);
@@ -352,14 +388,15 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 			}
 
 			if (ps.avatarDecorations) {
+				policies ??= await this.roleService.getUserPolicies(user.id);
 				const decorations = await this.avatarDecorationService.getAll(true);
-				const [myRoles, myPolicies] = await Promise.all([this.roleService.getUserRoles(user.id), this.roleService.getUserPolicies(user.id)]);
+				const myRoles = await this.roleService.getUserRoles(user.id);
 				const allRoles = await this.roleService.getRoles();
 				const decorationIds = decorations
 					.filter(d => d.roleIdsThatCanBeUsedThisDecoration.filter(roleId => allRoles.some(r => r.id === roleId)).length === 0 || myRoles.some(r => d.roleIdsThatCanBeUsedThisDecoration.includes(r.id)))
 					.map(d => d.id);
 
-				if (ps.avatarDecorations.length > myPolicies.avatarDecorationLimit) throw new ApiError(meta.errors.restrictedByRole);
+				if (ps.avatarDecorations.length > policies.avatarDecorationLimit) throw new ApiError(meta.errors.restrictedByRole);
 
 				updates.avatarDecorations = ps.avatarDecorations.filter(d => decorationIds.includes(d.id)).map(d => ({
 					id: d.id,
@@ -430,6 +467,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 			const newFields = profileUpdates.fields === undefined ? profile.fields : profileUpdates.fields;
 
 			if (newName != null) {
+				let hasProhibitedWords = false;
+				if (!await this.roleService.isModerator(user)) {
+					hasProhibitedWords = this.utilityService.isKeyWordIncluded(newName, this.instanceMeta.prohibitedWordsForNameOfUser);
+				}
+				if (hasProhibitedWords) {
+					throw new ApiError(meta.errors.nameContainsProhibitedWords);
+				}
+
 				const tokens = mfm.parseSimple(newName);
 				emojis = emojis.concat(extractCustomEmojisFromMfm(tokens));
 			}
diff --git a/packages/backend/src/server/api/endpoints/i/webhooks/create.ts b/packages/backend/src/server/api/endpoints/i/webhooks/create.ts
index c692380288..6e84603f7a 100644
--- a/packages/backend/src/server/api/endpoints/i/webhooks/create.ts
+++ b/packages/backend/src/server/api/endpoints/i/webhooks/create.ts
@@ -13,6 +13,7 @@ import { DI } from '@/di-symbols.js';
 import { RoleService } from '@/core/RoleService.js';
 import { ApiError } from '@/server/api/error.js';
 
+// TODO: UserWebhook schemaの適用
 export const meta = {
 	tags: ['webhooks'],
 
@@ -85,7 +86,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 			const currentWebhooksCount = await this.webhooksRepository.countBy({
 				userId: me.id,
 			});
-			if (currentWebhooksCount > (await this.roleService.getUserPolicies(me.id)).webhookLimit) {
+			if (currentWebhooksCount >= (await this.roleService.getUserPolicies(me.id)).webhookLimit) {
 				throw new ApiError(meta.errors.tooManyWebhooks);
 			}
 
diff --git a/packages/backend/src/server/api/endpoints/i/webhooks/list.ts b/packages/backend/src/server/api/endpoints/i/webhooks/list.ts
index fe07afb2d0..394c178f2a 100644
--- a/packages/backend/src/server/api/endpoints/i/webhooks/list.ts
+++ b/packages/backend/src/server/api/endpoints/i/webhooks/list.ts
@@ -9,6 +9,7 @@ import { webhookEventTypes } from '@/models/Webhook.js';
 import type { WebhooksRepository } from '@/models/_.js';
 import { DI } from '@/di-symbols.js';
 
+// TODO: UserWebhook schemaの適用
 export const meta = {
 	tags: ['webhooks', 'account'],
 
diff --git a/packages/backend/src/server/api/endpoints/i/webhooks/show.ts b/packages/backend/src/server/api/endpoints/i/webhooks/show.ts
index 5ddb79caf2..4a0c09ff0c 100644
--- a/packages/backend/src/server/api/endpoints/i/webhooks/show.ts
+++ b/packages/backend/src/server/api/endpoints/i/webhooks/show.ts
@@ -10,6 +10,7 @@ import type { WebhooksRepository } from '@/models/_.js';
 import { DI } from '@/di-symbols.js';
 import { ApiError } from '../../../error.js';
 
+// TODO: UserWebhook schemaの適用
 export const meta = {
 	tags: ['webhooks'],
 
diff --git a/packages/backend/src/server/api/endpoints/i/webhooks/test.ts b/packages/backend/src/server/api/endpoints/i/webhooks/test.ts
new file mode 100644
index 0000000000..2bf6df9ce2
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/i/webhooks/test.ts
@@ -0,0 +1,76 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Injectable } from '@nestjs/common';
+import ms from 'ms';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import { webhookEventTypes } from '@/models/Webhook.js';
+import { WebhookTestService } from '@/core/WebhookTestService.js';
+import { ApiError } from '@/server/api/error.js';
+
+export const meta = {
+	tags: ['webhooks'],
+
+	requireCredential: true,
+	secure: true,
+	kind: 'read:account',
+
+	limit: {
+		duration: ms('15min'),
+		max: 60,
+	},
+
+	errors: {
+		noSuchWebhook: {
+			message: 'No such webhook.',
+			code: 'NO_SUCH_WEBHOOK',
+			id: '0c52149c-e913-18f8-5dc7-74870bfe0cf9',
+		},
+	},
+} as const;
+
+export const paramDef = {
+	type: 'object',
+	properties: {
+		webhookId: {
+			type: 'string',
+			format: 'misskey:id',
+		},
+		type: {
+			type: 'string',
+			enum: webhookEventTypes,
+		},
+		override: {
+			type: 'object',
+			properties: {
+				url: { type: 'string' },
+				secret: { type: 'string' },
+			},
+		},
+	},
+	required: ['webhookId', 'type'],
+} as const;
+
+@Injectable()
+export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
+	constructor(
+		private webhookTestService: WebhookTestService,
+	) {
+		super(meta, paramDef, async (ps, me) => {
+			try {
+				await this.webhookTestService.testUserWebhook({
+					webhookId: ps.webhookId,
+					type: ps.type,
+					override: ps.override,
+				}, me);
+			} catch (e) {
+				if (e instanceof WebhookTestService.NoSuchWebhookError) {
+					throw new ApiError(meta.errors.noSuchWebhook);
+				}
+				throw e;
+			}
+		});
+	}
+}
diff --git a/packages/backend/src/server/api/endpoints/i/webhooks/update.ts b/packages/backend/src/server/api/endpoints/i/webhooks/update.ts
index 6e380d76f8..07a25bd82a 100644
--- a/packages/backend/src/server/api/endpoints/i/webhooks/update.ts
+++ b/packages/backend/src/server/api/endpoints/i/webhooks/update.ts
@@ -34,13 +34,13 @@ export const paramDef = {
 		webhookId: { type: 'string', format: 'misskey:id' },
 		name: { type: 'string', minLength: 1, maxLength: 100 },
 		url: { type: 'string', minLength: 1, maxLength: 1024 },
-		secret: { type: 'string', maxLength: 1024, default: '' },
+		secret: { type: 'string', nullable: true, maxLength: 1024 },
 		on: { type: 'array', items: {
 			type: 'string', enum: webhookEventTypes,
 		} },
 		active: { type: 'boolean' },
 	},
-	required: ['webhookId', 'name', 'url', 'on', 'active'],
+	required: ['webhookId'],
 } as const;
 
 // TODO: ロジックをサービスに切り出す
@@ -66,7 +66,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 			await this.webhooksRepository.update(webhook.id, {
 				name: ps.name,
 				url: ps.url,
-				secret: ps.secret,
+				secret: ps.secret === null ? '' : ps.secret,
 				on: ps.on,
 				active: ps.active,
 			});
diff --git a/packages/backend/src/server/api/endpoints/invite/limit.ts b/packages/backend/src/server/api/endpoints/invite/limit.ts
index 2786bd98d5..2ffd41ae28 100644
--- a/packages/backend/src/server/api/endpoints/invite/limit.ts
+++ b/packages/backend/src/server/api/endpoints/invite/limit.ts
@@ -49,7 +49,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 			const policies = await this.roleService.getUserPolicies(me.id);
 
 			const count = policies.inviteLimit ? await this.registrationTicketsRepository.countBy({
-				id: MoreThan(this.idService.gen(Date.now() - (policies.inviteExpirationTime * 60 * 1000))),
+				id: MoreThan(this.idService.gen(Date.now() - (policies.inviteLimitCycle * 60 * 1000))),
 				createdById: me.id,
 			}) : null;
 
diff --git a/packages/backend/src/server/api/endpoints/notes/create.ts b/packages/backend/src/server/api/endpoints/notes/create.ts
index beb77ca7ab..253a360815 100644
--- a/packages/backend/src/server/api/endpoints/notes/create.ts
+++ b/packages/backend/src/server/api/endpoints/notes/create.ts
@@ -17,8 +17,6 @@ import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
 import { NoteCreateService } from '@/core/NoteCreateService.js';
 import { DI } from '@/di-symbols.js';
 import { isQuote, isRenote } from '@/misc/is-renote.js';
-import { MetaService } from '@/core/MetaService.js';
-import { UtilityService } from '@/core/UtilityService.js';
 import { IdentifiableError } from '@/misc/identifiable-error.js';
 import { ApiError } from '../../error.js';
 
diff --git a/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts b/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts
index 5acc9706d3..aed9065bf9 100644
--- a/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts
+++ b/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts
@@ -5,7 +5,7 @@
 
 import { Brackets } from 'typeorm';
 import { Inject, Injectable } from '@nestjs/common';
-import type { NotesRepository, ChannelFollowingsRepository } from '@/models/_.js';
+import type { NotesRepository, ChannelFollowingsRepository, MiMeta } from '@/models/_.js';
 import { Endpoint } from '@/server/api/endpoint-base.js';
 import ActiveUsersChart from '@/core/chart/charts/active-users.js';
 import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
@@ -16,7 +16,6 @@ import { CacheService } from '@/core/CacheService.js';
 import { FanoutTimelineName } from '@/core/FanoutTimelineService.js';
 import { QueryService } from '@/core/QueryService.js';
 import { UserFollowingService } from '@/core/UserFollowingService.js';
-import { MetaService } from '@/core/MetaService.js';
 import { MiLocalUser } from '@/models/User.js';
 import { FanoutTimelineEndpointService } from '@/core/FanoutTimelineEndpointService.js';
 import { ApiError } from '../../error.js';
@@ -74,6 +73,9 @@ export const paramDef = {
 @Injectable()
 export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
 	constructor(
+		@Inject(DI.meta)
+		private serverSettings: MiMeta,
+
 		@Inject(DI.notesRepository)
 		private notesRepository: NotesRepository,
 
@@ -87,7 +89,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 		private cacheService: CacheService,
 		private queryService: QueryService,
 		private userFollowingService: UserFollowingService,
-		private metaService: MetaService,
 		private fanoutTimelineEndpointService: FanoutTimelineEndpointService,
 	) {
 		super(meta, paramDef, async (ps, me) => {
@@ -101,9 +102,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 
 			if (ps.withReplies && ps.withFiles) throw new ApiError(meta.errors.bothWithRepliesAndWithFiles);
 
-			const serverSettings = await this.metaService.fetch();
-
-			if (!serverSettings.enableFanoutTimeline) {
+			if (!this.serverSettings.enableFanoutTimeline) {
 				const timeline = await this.getFromDb({
 					untilId,
 					sinceId,
@@ -139,9 +138,16 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 				timelineConfig = [
 					`homeTimeline:${me.id}`,
 					'localTimeline',
+					`localTimelineWithReplyTo:${me.id}`,
 				];
 			}
 
+			const [
+				followings,
+			] = await Promise.all([
+				this.cacheService.userFollowingsCache.fetch(me.id),
+			]);
+
 			const redisTimeline = await this.fanoutTimelineEndpointService.timeline({
 				untilId,
 				sinceId,
@@ -149,9 +155,16 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 				allowPartial: ps.allowPartial,
 				me,
 				redisTimelines: timelineConfig,
-				useDbFallback: serverSettings.enableFanoutTimelineDbFallback,
+				useDbFallback: this.serverSettings.enableFanoutTimelineDbFallback,
 				alwaysIncludeMyNotes: true,
 				excludePureRenotes: !ps.withRenotes,
+				noteFilter: note => {
+					if (note.reply && note.reply.visibility === 'followers') {
+						if (!Object.hasOwn(followings, note.reply.userId) && note.reply.userId !== me.id) return false;
+					}
+
+					return true;
+				},
 				dbFallback: async (untilId, sinceId, limit) => await this.getFromDb({
 					untilId,
 					sinceId,
diff --git a/packages/backend/src/server/api/endpoints/notes/local-timeline.ts b/packages/backend/src/server/api/endpoints/notes/local-timeline.ts
index be82b5a8a7..0b48f2c78b 100644
--- a/packages/backend/src/server/api/endpoints/notes/local-timeline.ts
+++ b/packages/backend/src/server/api/endpoints/notes/local-timeline.ts
@@ -5,16 +5,14 @@
 
 import { Brackets } from 'typeorm';
 import { Inject, Injectable } from '@nestjs/common';
-import type { NotesRepository } from '@/models/_.js';
+import type { MiMeta, NotesRepository } from '@/models/_.js';
 import { Endpoint } from '@/server/api/endpoint-base.js';
 import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
 import ActiveUsersChart from '@/core/chart/charts/active-users.js';
 import { DI } from '@/di-symbols.js';
 import { RoleService } from '@/core/RoleService.js';
 import { IdService } from '@/core/IdService.js';
-import { CacheService } from '@/core/CacheService.js';
 import { QueryService } from '@/core/QueryService.js';
-import { MetaService } from '@/core/MetaService.js';
 import { MiLocalUser } from '@/models/User.js';
 import { FanoutTimelineEndpointService } from '@/core/FanoutTimelineEndpointService.js';
 import { ApiError } from '../../error.js';
@@ -66,6 +64,9 @@ export const paramDef = {
 @Injectable()
 export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
 	constructor(
+		@Inject(DI.meta)
+		private serverSettings: MiMeta,
+
 		@Inject(DI.notesRepository)
 		private notesRepository: NotesRepository,
 
@@ -73,10 +74,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 		private roleService: RoleService,
 		private activeUsersChart: ActiveUsersChart,
 		private idService: IdService,
-		private cacheService: CacheService,
 		private fanoutTimelineEndpointService: FanoutTimelineEndpointService,
 		private queryService: QueryService,
-		private metaService: MetaService,
 	) {
 		super(meta, paramDef, async (ps, me) => {
 			const untilId = ps.untilId ?? (ps.untilDate ? this.idService.gen(ps.untilDate!) : null);
@@ -89,9 +88,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 
 			if (ps.withReplies && ps.withFiles) throw new ApiError(meta.errors.bothWithRepliesAndWithFiles);
 
-			const serverSettings = await this.metaService.fetch();
-
-			if (!serverSettings.enableFanoutTimeline) {
+			if (!this.serverSettings.enableFanoutTimeline) {
 				const timeline = await this.getFromDb({
 					untilId,
 					sinceId,
@@ -115,7 +112,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 				limit: ps.limit,
 				allowPartial: ps.allowPartial,
 				me,
-				useDbFallback: serverSettings.enableFanoutTimelineDbFallback,
+				useDbFallback: this.serverSettings.enableFanoutTimelineDbFallback,
 				redisTimelines:
 					ps.withFiles ? ['localTimelineWithFiles']
 					: ps.withReplies ? ['localTimeline', 'localTimelineWithReplies']
diff --git a/packages/backend/src/server/api/endpoints/notes/reactions/create.ts b/packages/backend/src/server/api/endpoints/notes/reactions/create.ts
index b9899608bf..0f0dcca605 100644
--- a/packages/backend/src/server/api/endpoints/notes/reactions/create.ts
+++ b/packages/backend/src/server/api/endpoints/notes/reactions/create.ts
@@ -36,6 +36,12 @@ export const meta = {
 			code: 'YOU_HAVE_BEEN_BLOCKED',
 			id: '20ef5475-9f38-4e4c-bd33-de6d979498ec',
 		},
+
+		cannotReactToRenote: {
+			message: 'You cannot react to Renote.',
+			code: 'CANNOT_REACT_TO_RENOTE',
+			id: 'eaccdc08-ddef-43fe-908f-d108faad57f5',
+		},
 	},
 } as const;
 
@@ -62,6 +68,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 			await this.reactionService.create(me, note, ps.reaction).catch(err => {
 				if (err.id === '51c42bb4-931a-456b-bff7-e5a8a70dd298') throw new ApiError(meta.errors.alreadyReacted);
 				if (err.id === 'e70412a4-7197-4726-8e74-f3e0deb92aa7') throw new ApiError(meta.errors.youHaveBeenBlocked);
+				if (err.id === '12c35529-3c79-4327-b1cc-e2cf63a71925') throw new ApiError(meta.errors.cannotReactToRenote);
 				throw err;
 			});
 			return;
diff --git a/packages/backend/src/server/api/endpoints/notes/show.ts b/packages/backend/src/server/api/endpoints/notes/show.ts
index adcda30a7d..11839bce36 100644
--- a/packages/backend/src/server/api/endpoints/notes/show.ts
+++ b/packages/backend/src/server/api/endpoints/notes/show.ts
@@ -26,6 +26,12 @@ export const meta = {
 			code: 'NO_SUCH_NOTE',
 			id: '24fcbfc6-2e37-42b6-8388-c29b3861a08d',
 		},
+
+		signinRequired: {
+			message: 'Signin required.',
+			code: 'SIGNIN_REQUIRED',
+			id: '8e75455b-738c-471d-9f80-62693f33372e',
+		},
 	},
 } as const;
 
@@ -44,11 +50,15 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 		private getterService: GetterService,
 	) {
 		super(meta, paramDef, async (ps, me) => {
-			const note = await this.getterService.getNote(ps.noteId).catch(err => {
+			const note = await this.getterService.getNoteWithUser(ps.noteId).catch(err => {
 				if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
 				throw err;
 			});
 
+			if (note.user!.requireSigninToViewContents && me == null) {
+				throw new ApiError(meta.errors.signinRequired);
+			}
+
 			return await this.noteEntityService.pack(note, me, {
 				detail: true,
 			});
diff --git a/packages/backend/src/server/api/endpoints/notes/timeline.ts b/packages/backend/src/server/api/endpoints/notes/timeline.ts
index 8b87908bd3..7cb11cc1eb 100644
--- a/packages/backend/src/server/api/endpoints/notes/timeline.ts
+++ b/packages/backend/src/server/api/endpoints/notes/timeline.ts
@@ -5,7 +5,7 @@
 
 import { Brackets } from 'typeorm';
 import { Inject, Injectable } from '@nestjs/common';
-import type { NotesRepository, ChannelFollowingsRepository } from '@/models/_.js';
+import type { NotesRepository, ChannelFollowingsRepository, MiMeta } from '@/models/_.js';
 import { Endpoint } from '@/server/api/endpoint-base.js';
 import { QueryService } from '@/core/QueryService.js';
 import ActiveUsersChart from '@/core/chart/charts/active-users.js';
@@ -15,7 +15,6 @@ import { IdService } from '@/core/IdService.js';
 import { CacheService } from '@/core/CacheService.js';
 import { UserFollowingService } from '@/core/UserFollowingService.js';
 import { MiLocalUser } from '@/models/User.js';
-import { MetaService } from '@/core/MetaService.js';
 import { FanoutTimelineEndpointService } from '@/core/FanoutTimelineEndpointService.js';
 
 export const meta = {
@@ -56,6 +55,9 @@ export const paramDef = {
 @Injectable()
 export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
 	constructor(
+		@Inject(DI.meta)
+		private serverSettings: MiMeta,
+
 		@Inject(DI.notesRepository)
 		private notesRepository: NotesRepository,
 
@@ -69,15 +71,12 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 		private fanoutTimelineEndpointService: FanoutTimelineEndpointService,
 		private userFollowingService: UserFollowingService,
 		private queryService: QueryService,
-		private metaService: MetaService,
 	) {
 		super(meta, paramDef, async (ps, me) => {
 			const untilId = ps.untilId ?? (ps.untilDate ? this.idService.gen(ps.untilDate!) : null);
 			const sinceId = ps.sinceId ?? (ps.sinceDate ? this.idService.gen(ps.sinceDate!) : null);
 
-			const serverSettings = await this.metaService.fetch();
-
-			if (!serverSettings.enableFanoutTimeline) {
+			if (!this.serverSettings.enableFanoutTimeline) {
 				const timeline = await this.getFromDb({
 					untilId,
 					sinceId,
@@ -108,13 +107,13 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 				limit: ps.limit,
 				allowPartial: ps.allowPartial,
 				me,
-				useDbFallback: serverSettings.enableFanoutTimelineDbFallback,
+				useDbFallback: this.serverSettings.enableFanoutTimelineDbFallback,
 				redisTimelines: ps.withFiles ? [`homeTimelineWithFiles:${me.id}`] : [`homeTimeline:${me.id}`],
 				alwaysIncludeMyNotes: true,
 				excludePureRenotes: !ps.withRenotes,
 				noteFilter: note => {
 					if (note.reply && note.reply.visibility === 'followers') {
-						if (!Object.hasOwn(followings, note.reply.userId)) return false;
+						if (!Object.hasOwn(followings, note.reply.userId) && note.reply.userId !== me.id) return false;
 					}
 
 					return true;
diff --git a/packages/backend/src/server/api/endpoints/notes/translate.ts b/packages/backend/src/server/api/endpoints/notes/translate.ts
index 38a9660aa2..e9a6a36b02 100644
--- a/packages/backend/src/server/api/endpoints/notes/translate.ts
+++ b/packages/backend/src/server/api/endpoints/notes/translate.ts
@@ -4,14 +4,15 @@
  */
 
 import { URLSearchParams } from 'node:url';
-import { Injectable } from '@nestjs/common';
+import { Inject, Injectable } from '@nestjs/common';
 import { Endpoint } from '@/server/api/endpoint-base.js';
 import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
-import { MetaService } from '@/core/MetaService.js';
 import { HttpRequestService } from '@/core/HttpRequestService.js';
 import { GetterService } from '@/server/api/GetterService.js';
 import { RoleService } from '@/core/RoleService.js';
 import { ApiError } from '../../error.js';
+import { MiMeta } from '@/models/_.js';
+import { DI } from '@/di-symbols.js';
 
 export const meta = {
 	tags: ['notes'],
@@ -59,9 +60,11 @@ export const paramDef = {
 @Injectable()
 export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
 	constructor(
+		@Inject(DI.meta)
+		private serverSettings: MiMeta,
+
 		private noteEntityService: NoteEntityService,
 		private getterService: GetterService,
-		private metaService: MetaService,
 		private httpRequestService: HttpRequestService,
 		private roleService: RoleService,
 	) {
@@ -84,9 +87,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 				return;
 			}
 
-			const instance = await this.metaService.fetch();
-
-			if (instance.deeplAuthKey == null) {
+			if (this.serverSettings.deeplAuthKey == null) {
 				throw new ApiError(meta.errors.unavailable);
 			}
 
@@ -94,11 +95,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 			if (targetLang.includes('-')) targetLang = targetLang.split('-')[0];
 
 			const params = new URLSearchParams();
-			params.append('auth_key', instance.deeplAuthKey);
+			params.append('auth_key', this.serverSettings.deeplAuthKey);
 			params.append('text', note.text);
 			params.append('target_lang', targetLang);
 
-			const endpoint = instance.deeplIsPro ? 'https://api.deepl.com/v2/translate' : 'https://api-free.deepl.com/v2/translate';
+			const endpoint = this.serverSettings.deeplIsPro ? 'https://api.deepl.com/v2/translate' : 'https://api-free.deepl.com/v2/translate';
 
 			const res = await this.httpRequestService.send(endpoint, {
 				method: 'POST',
diff --git a/packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts b/packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts
index 43877e61ef..6c7185c9eb 100644
--- a/packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts
+++ b/packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts
@@ -5,16 +5,14 @@
 
 import { Inject, Injectable } from '@nestjs/common';
 import { Brackets } from 'typeorm';
-import type { MiUserList, NotesRepository, UserListMembershipsRepository, UserListsRepository } from '@/models/_.js';
+import type { MiMeta, MiUserList, NotesRepository, UserListMembershipsRepository, UserListsRepository } from '@/models/_.js';
 import { Endpoint } from '@/server/api/endpoint-base.js';
 import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
 import ActiveUsersChart from '@/core/chart/charts/active-users.js';
 import { DI } from '@/di-symbols.js';
-import { CacheService } from '@/core/CacheService.js';
 import { IdService } from '@/core/IdService.js';
 import { QueryService } from '@/core/QueryService.js';
 import { MiLocalUser } from '@/models/User.js';
-import { MetaService } from '@/core/MetaService.js';
 import { FanoutTimelineEndpointService } from '@/core/FanoutTimelineEndpointService.js';
 import { ApiError } from '../../error.js';
 
@@ -69,6 +67,9 @@ export const paramDef = {
 @Injectable()
 export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
 	constructor(
+		@Inject(DI.meta)
+		private serverSettings: MiMeta,
+
 		@Inject(DI.notesRepository)
 		private notesRepository: NotesRepository,
 
@@ -80,11 +81,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 
 		private noteEntityService: NoteEntityService,
 		private activeUsersChart: ActiveUsersChart,
-		private cacheService: CacheService,
 		private idService: IdService,
 		private fanoutTimelineEndpointService: FanoutTimelineEndpointService,
 		private queryService: QueryService,
-		private metaService: MetaService,
 	) {
 		super(meta, paramDef, async (ps, me) => {
 			const untilId = ps.untilId ?? (ps.untilDate ? this.idService.gen(ps.untilDate!) : null);
@@ -99,9 +98,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 				throw new ApiError(meta.errors.noSuchList);
 			}
 
-			const serverSettings = await this.metaService.fetch();
-
-			if (!serverSettings.enableFanoutTimeline) {
+			if (!this.serverSettings.enableFanoutTimeline) {
 				const timeline = await this.getFromDb(list, {
 					untilId,
 					sinceId,
@@ -124,7 +121,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 				limit: ps.limit,
 				allowPartial: ps.allowPartial,
 				me,
-				useDbFallback: serverSettings.enableFanoutTimelineDbFallback,
+				useDbFallback: this.serverSettings.enableFanoutTimelineDbFallback,
 				redisTimelines: ps.withFiles ? [`userListTimelineWithFiles:${list.id}`] : [`userListTimeline:${list.id}`],
 				alwaysIncludeMyNotes: true,
 				excludePureRenotes: !ps.withRenotes,
diff --git a/packages/backend/src/server/api/endpoints/pages/delete.ts b/packages/backend/src/server/api/endpoints/pages/delete.ts
index aa2ba75a41..f2bc946788 100644
--- a/packages/backend/src/server/api/endpoints/pages/delete.ts
+++ b/packages/backend/src/server/api/endpoints/pages/delete.ts
@@ -4,9 +4,11 @@
  */
 
 import { Inject, Injectable } from '@nestjs/common';
-import type { PagesRepository } from '@/models/_.js';
+import type { PagesRepository, UsersRepository } from '@/models/_.js';
 import { Endpoint } from '@/server/api/endpoint-base.js';
 import { DI } from '@/di-symbols.js';
+import { ModerationLogService } from '@/core/ModerationLogService.js';
+import { RoleService } from '@/core/RoleService.js';
 import { ApiError } from '../../error.js';
 
 export const meta = {
@@ -44,17 +46,35 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 	constructor(
 		@Inject(DI.pagesRepository)
 		private pagesRepository: PagesRepository,
+
+		@Inject(DI.usersRepository)
+		private usersRepository: UsersRepository,
+
+		private moderationLogService: ModerationLogService,
+		private roleService: RoleService,
 	) {
 		super(meta, paramDef, async (ps, me) => {
 			const page = await this.pagesRepository.findOneBy({ id: ps.pageId });
+
 			if (page == null) {
 				throw new ApiError(meta.errors.noSuchPage);
 			}
-			if (page.userId !== me.id) {
+
+			if (!await this.roleService.isModerator(me) && page.userId !== me.id) {
 				throw new ApiError(meta.errors.accessDenied);
 			}
 
 			await this.pagesRepository.delete(page.id);
+
+			if (page.userId !== me.id) {
+				const user = await this.usersRepository.findOneByOrFail({ id: page.userId });
+				this.moderationLogService.log(me, 'deletePage', {
+					pageId: page.id,
+					pageUserId: page.userId,
+					pageUserUsername: user.username,
+					page,
+				});
+			}
 		});
 	}
 }
diff --git a/packages/backend/src/server/api/endpoints/pages/update.ts b/packages/backend/src/server/api/endpoints/pages/update.ts
index b8e5e70a25..f11bbbcb1a 100644
--- a/packages/backend/src/server/api/endpoints/pages/update.ts
+++ b/packages/backend/src/server/api/endpoints/pages/update.ts
@@ -70,7 +70,7 @@ export const paramDef = {
 		alignCenter: { type: 'boolean' },
 		hideTitleWhenPinned: { type: 'boolean' },
 	},
-	required: ['pageId', 'title', 'name', 'content', 'variables', 'script'],
+	required: ['pageId'],
 } as const;
 
 @Injectable()
@@ -91,9 +91,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 				throw new ApiError(meta.errors.accessDenied);
 			}
 
-			let eyeCatchingImage = null;
 			if (ps.eyeCatchingImageId != null) {
-				eyeCatchingImage = await this.driveFilesRepository.findOneBy({
+				const eyeCatchingImage = await this.driveFilesRepository.findOneBy({
 					id: ps.eyeCatchingImageId,
 					userId: me.id,
 				});
@@ -116,23 +115,15 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 			await this.pagesRepository.update(page.id, {
 				updatedAt: new Date(),
 				title: ps.title,
-				// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
-				name: ps.name === undefined ? page.name : ps.name,
+				name: ps.name,
 				summary: ps.summary === undefined ? page.summary : ps.summary,
 				content: ps.content,
 				variables: ps.variables,
 				script: ps.script,
-				// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
-				alignCenter: ps.alignCenter === undefined ? page.alignCenter : ps.alignCenter,
-				// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
-				hideTitleWhenPinned: ps.hideTitleWhenPinned === undefined ? page.hideTitleWhenPinned : ps.hideTitleWhenPinned,
-				// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
-				font: ps.font === undefined ? page.font : ps.font,
-				eyeCatchingImageId: ps.eyeCatchingImageId === null
-					? null
-					: ps.eyeCatchingImageId === undefined
-						? page.eyeCatchingImageId
-						: eyeCatchingImage!.id,
+				alignCenter: ps.alignCenter,
+				hideTitleWhenPinned: ps.hideTitleWhenPinned,
+				font: ps.font,
+				eyeCatchingImageId: ps.eyeCatchingImageId,
 			});
 		});
 	}
diff --git a/packages/backend/src/server/api/endpoints/pinned-users.ts b/packages/backend/src/server/api/endpoints/pinned-users.ts
index 784766bcb5..5b0b656c63 100644
--- a/packages/backend/src/server/api/endpoints/pinned-users.ts
+++ b/packages/backend/src/server/api/endpoints/pinned-users.ts
@@ -5,14 +5,12 @@
 
 import { IsNull } from 'typeorm';
 import { Inject, Injectable } from '@nestjs/common';
-import type { UsersRepository } from '@/models/_.js';
+import type { MiMeta, UsersRepository } from '@/models/_.js';
 import * as Acct from '@/misc/acct.js';
 import type { MiUser } from '@/models/User.js';
 import { Endpoint } from '@/server/api/endpoint-base.js';
-import { MetaService } from '@/core/MetaService.js';
 import { UserEntityService } from '@/core/entities/UserEntityService.js';
 import { DI } from '@/di-symbols.js';
-import { isNotNull } from '@/misc/is-not-null.js';
 
 export const meta = {
 	tags: ['users'],
@@ -39,21 +37,21 @@ export const paramDef = {
 @Injectable()
 export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
 	constructor(
+		@Inject(DI.meta)
+		private serverSettings: MiMeta,
+
 		@Inject(DI.usersRepository)
 		private usersRepository: UsersRepository,
 
-		private metaService: MetaService,
 		private userEntityService: UserEntityService,
 	) {
 		super(meta, paramDef, async (ps, me) => {
-			const meta = await this.metaService.fetch();
-
-			const users = await Promise.all(meta.pinnedUsers.map(acct => Acct.parse(acct)).map(acct => this.usersRepository.findOneBy({
+			const users = await Promise.all(this.serverSettings.pinnedUsers.map(acct => Acct.parse(acct)).map(acct => this.usersRepository.findOneBy({
 				usernameLower: acct.username.toLowerCase(),
 				host: acct.host ?? IsNull(),
 			})));
 
-			return await this.userEntityService.packMany(users.filter(isNotNull), me, { schema: 'UserDetailed' });
+			return await this.userEntityService.packMany(users.filter(x => x != null), me, { schema: 'UserDetailed' });
 		});
 	}
 }
diff --git a/packages/backend/src/server/api/endpoints/renote-mute/create.ts b/packages/backend/src/server/api/endpoints/renote-mute/create.ts
index 39bf0cc428..84a1f010d4 100644
--- a/packages/backend/src/server/api/endpoints/renote-mute/create.ts
+++ b/packages/backend/src/server/api/endpoints/renote-mute/create.ts
@@ -6,12 +6,11 @@
 import { Inject, Injectable } from '@nestjs/common';
 import ms from 'ms';
 import { Endpoint } from '@/server/api/endpoint-base.js';
-import { IdService } from '@/core/IdService.js';
-import type { RenoteMutingsRepository } from '@/models/_.js';
-import type { MiRenoteMuting } from '@/models/RenoteMuting.js';
 import { DI } from '@/di-symbols.js';
 import { GetterService } from '@/server/api/GetterService.js';
 import { ApiError } from '../../error.js';
+import { UserRenoteMutingService } from "@/core/UserRenoteMutingService.js";
+import type { RenoteMutingsRepository } from '@/models/_.js';
 
 export const meta = {
 	tags: ['account'],
@@ -62,7 +61,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 		private renoteMutingsRepository: RenoteMutingsRepository,
 
 		private getterService: GetterService,
-		private idService: IdService,
+		private userRenoteMutingService: UserRenoteMutingService,
 	) {
 		super(meta, paramDef, async (ps, me) => {
 			const muter = me;
@@ -79,21 +78,19 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 			});
 
 			// Check if already muting
-			const exist = await this.renoteMutingsRepository.findOneBy({
-				muterId: muter.id,
-				muteeId: mutee.id,
+			const exist = await this.renoteMutingsRepository.exists({
+				where: {
+					muterId: muter.id,
+					muteeId: mutee.id,
+				},
 			});
 
-			if (exist != null) {
+			if (exist === true) {
 				throw new ApiError(meta.errors.alreadyMuting);
 			}
 
 			// Create mute
-			await this.renoteMutingsRepository.insert({
-				id: this.idService.gen(),
-				muterId: muter.id,
-				muteeId: mutee.id,
-			} as MiRenoteMuting);
+			await this.userRenoteMutingService.mute(muter, mutee);
 		});
 	}
 }
diff --git a/packages/backend/src/server/api/endpoints/renote-mute/delete.ts b/packages/backend/src/server/api/endpoints/renote-mute/delete.ts
index 6e037cc07e..1a584b8404 100644
--- a/packages/backend/src/server/api/endpoints/renote-mute/delete.ts
+++ b/packages/backend/src/server/api/endpoints/renote-mute/delete.ts
@@ -5,10 +5,11 @@
 
 import { Inject, Injectable } from '@nestjs/common';
 import { Endpoint } from '@/server/api/endpoint-base.js';
-import type { RenoteMutingsRepository } from '@/models/_.js';
 import { DI } from '@/di-symbols.js';
 import { GetterService } from '@/server/api/GetterService.js';
 import { ApiError } from '../../error.js';
+import { UserRenoteMutingService } from "@/core/UserRenoteMutingService.js";
+import type { RenoteMutingsRepository } from '@/models/_.js';
 
 export const meta = {
 	tags: ['account'],
@@ -53,6 +54,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 		private renoteMutingsRepository: RenoteMutingsRepository,
 
 		private getterService: GetterService,
+		private userRenoteMutingService: UserRenoteMutingService,
 	) {
 		super(meta, paramDef, async (ps, me) => {
 			const muter = me;
@@ -79,9 +81,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 			}
 
 			// Delete mute
-			await this.renoteMutingsRepository.delete({
-				id: exist.id,
-			});
+			await this.userRenoteMutingService.unmute([exist]);
 		});
 	}
 }
diff --git a/packages/backend/src/server/api/endpoints/server-info.ts b/packages/backend/src/server/api/endpoints/server-info.ts
index c13802eb06..8301c85f2e 100644
--- a/packages/backend/src/server/api/endpoints/server-info.ts
+++ b/packages/backend/src/server/api/endpoints/server-info.ts
@@ -5,9 +5,10 @@
 
 import * as os from 'node:os';
 import si from 'systeminformation';
-import { Injectable } from '@nestjs/common';
+import { Inject, Injectable } from '@nestjs/common';
 import { Endpoint } from '@/server/api/endpoint-base.js';
-import { MetaService } from '@/core/MetaService.js';
+import { MiMeta } from '@/models/_.js';
+import { DI } from '@/di-symbols.js';
 
 export const meta = {
 	requireCredential: false,
@@ -73,10 +74,11 @@ export const paramDef = {
 @Injectable()
 export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
 	constructor(
-		private metaService: MetaService,
+		@Inject(DI.meta)
+		private serverSettings: MiMeta,
 	) {
 		super(meta, paramDef, async () => {
-			if (!(await this.metaService.fetch()).enableServerMachineStats) return {
+			if (!this.serverSettings.enableServerMachineStats) return {
 				machine: '?',
 				cpu: {
 					model: '?',
diff --git a/packages/backend/src/server/api/endpoints/sw/register.ts b/packages/backend/src/server/api/endpoints/sw/register.ts
index a9a33149f9..fd76df2d3c 100644
--- a/packages/backend/src/server/api/endpoints/sw/register.ts
+++ b/packages/backend/src/server/api/endpoints/sw/register.ts
@@ -5,9 +5,8 @@
 
 import { Inject, Injectable } from '@nestjs/common';
 import { IdService } from '@/core/IdService.js';
-import type { SwSubscriptionsRepository } from '@/models/_.js';
+import type { MiMeta, SwSubscriptionsRepository } from '@/models/_.js';
 import { Endpoint } from '@/server/api/endpoint-base.js';
-import { MetaService } from '@/core/MetaService.js';
 import { DI } from '@/di-symbols.js';
 import { PushNotificationService } from '@/core/PushNotificationService.js';
 
@@ -62,11 +61,13 @@ export const paramDef = {
 @Injectable()
 export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
 	constructor(
+		@Inject(DI.meta)
+		private serverSettings: MiMeta,
+
 		@Inject(DI.swSubscriptionsRepository)
 		private swSubscriptionsRepository: SwSubscriptionsRepository,
 
 		private idService: IdService,
-		private metaService: MetaService,
 		private pushNotificationService: PushNotificationService,
 	) {
 		super(meta, paramDef, async (ps, me) => {
@@ -78,12 +79,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 				publickey: ps.publickey,
 			});
 
-			const instance = await this.metaService.fetch(true);
-
 			if (exist != null) {
 				return {
 					state: 'already-subscribed' as const,
-					key: instance.swPublicKey,
+					key: this.serverSettings.swPublicKey,
 					userId: me.id,
 					endpoint: exist.endpoint,
 					sendReadMessage: exist.sendReadMessage,
@@ -103,7 +102,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 
 			return {
 				state: 'subscribed' as const,
-				key: instance.swPublicKey,
+				key: this.serverSettings.swPublicKey,
 				userId: me.id,
 				endpoint: ps.endpoint,
 				sendReadMessage: ps.sendReadMessage,
diff --git a/packages/backend/src/server/api/endpoints/username/available.ts b/packages/backend/src/server/api/endpoints/username/available.ts
index affb0996f1..4944be9b05 100644
--- a/packages/backend/src/server/api/endpoints/username/available.ts
+++ b/packages/backend/src/server/api/endpoints/username/available.ts
@@ -5,11 +5,10 @@
 
 import { IsNull } from 'typeorm';
 import { Inject, Injectable } from '@nestjs/common';
-import type { UsedUsernamesRepository, UsersRepository } from '@/models/_.js';
+import type { MiMeta, UsedUsernamesRepository, UsersRepository } from '@/models/_.js';
 import { Endpoint } from '@/server/api/endpoint-base.js';
 import { localUsernameSchema } from '@/models/User.js';
 import { DI } from '@/di-symbols.js';
-import { MetaService } from '@/core/MetaService.js';
 
 export const meta = {
 	tags: ['users'],
@@ -39,13 +38,14 @@ export const paramDef = {
 @Injectable()
 export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
 	constructor(
+		@Inject(DI.meta)
+		private serverSettings: MiMeta,
+
 		@Inject(DI.usersRepository)
 		private usersRepository: UsersRepository,
 
 		@Inject(DI.usedUsernamesRepository)
 		private usedUsernamesRepository: UsedUsernamesRepository,
-
-		private metaService: MetaService,
 	) {
 		super(meta, paramDef, async (ps, me) => {
 			const exist = await this.usersRepository.countBy({
@@ -55,8 +55,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 
 			const exist2 = await this.usedUsernamesRepository.countBy({ username: ps.username.toLowerCase() });
 
-			const meta = await this.metaService.fetch();
-			const isPreserved = meta.preservedUsernames.map(x => x.toLowerCase()).includes(ps.username.toLowerCase());
+			const isPreserved = this.serverSettings.preservedUsernames.map(x => x.toLowerCase()).includes(ps.username.toLowerCase());
 
 			return {
 				available: exist === 0 && exist2 === 0 && !isPreserved,
diff --git a/packages/backend/src/server/api/endpoints/users/followers.ts b/packages/backend/src/server/api/endpoints/users/followers.ts
index 7ce7734f53..a8b4319a61 100644
--- a/packages/backend/src/server/api/endpoints/users/followers.ts
+++ b/packages/backend/src/server/api/endpoints/users/followers.ts
@@ -11,6 +11,7 @@ import { QueryService } from '@/core/QueryService.js';
 import { FollowingEntityService } from '@/core/entities/FollowingEntityService.js';
 import { UtilityService } from '@/core/UtilityService.js';
 import { DI } from '@/di-symbols.js';
+import { RoleService } from '@/core/RoleService.js';
 import { ApiError } from '../../error.js';
 
 export const meta = {
@@ -81,6 +82,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 		private utilityService: UtilityService,
 		private followingEntityService: FollowingEntityService,
 		private queryService: QueryService,
+		private roleService: RoleService,
 	) {
 		super(meta, paramDef, async (ps, me) => {
 			const user = await this.usersRepository.findOneBy(ps.userId != null
@@ -93,23 +95,25 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 
 			const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id });
 
-			if (profile.followersVisibility === 'private') {
-				if (me == null || (me.id !== user.id)) {
-					throw new ApiError(meta.errors.forbidden);
-				}
-			} else if (profile.followersVisibility === 'followers') {
-				if (me == null) {
-					throw new ApiError(meta.errors.forbidden);
-				} else if (me.id !== user.id) {
-					const isFollowing = await this.followingsRepository.exists({
-						where: {
-							followeeId: user.id,
-							followerId: me.id,
-						},
-					});
-					if (!isFollowing) {
+			if (profile.followersVisibility !== 'public' && !await this.roleService.isModerator(me)) {
+				if (profile.followersVisibility === 'private') {
+					if (me == null || (me.id !== user.id)) {
 						throw new ApiError(meta.errors.forbidden);
 					}
+				} else if (profile.followersVisibility === 'followers') {
+					if (me == null) {
+						throw new ApiError(meta.errors.forbidden);
+					} else if (me.id !== user.id) {
+						const isFollowing = await this.followingsRepository.exists({
+							where: {
+								followeeId: user.id,
+								followerId: me.id,
+							},
+						});
+						if (!isFollowing) {
+							throw new ApiError(meta.errors.forbidden);
+						}
+					}
 				}
 			}
 
diff --git a/packages/backend/src/server/api/endpoints/users/following.ts b/packages/backend/src/server/api/endpoints/users/following.ts
index 6b3389f0b2..feda5bb353 100644
--- a/packages/backend/src/server/api/endpoints/users/following.ts
+++ b/packages/backend/src/server/api/endpoints/users/following.ts
@@ -12,6 +12,7 @@ import { QueryService } from '@/core/QueryService.js';
 import { FollowingEntityService } from '@/core/entities/FollowingEntityService.js';
 import { UtilityService } from '@/core/UtilityService.js';
 import { DI } from '@/di-symbols.js';
+import { RoleService } from '@/core/RoleService.js';
 import { ApiError } from '../../error.js';
 
 export const meta = {
@@ -90,6 +91,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 		private utilityService: UtilityService,
 		private followingEntityService: FollowingEntityService,
 		private queryService: QueryService,
+		private roleService: RoleService,
 	) {
 		super(meta, paramDef, async (ps, me) => {
 			const user = await this.usersRepository.findOneBy(ps.userId != null
@@ -102,23 +104,25 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 
 			const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id });
 
-			if (profile.followingVisibility === 'private') {
-				if (me == null || (me.id !== user.id)) {
-					throw new ApiError(meta.errors.forbidden);
-				}
-			} else if (profile.followingVisibility === 'followers') {
-				if (me == null) {
-					throw new ApiError(meta.errors.forbidden);
-				} else if (me.id !== user.id) {
-					const isFollowing = await this.followingsRepository.exists({
-						where: {
-							followeeId: user.id,
-							followerId: me.id,
-						},
-					});
-					if (!isFollowing) {
+			if (profile.followingVisibility !== 'public' && !await this.roleService.isModerator(me)) {
+				if (profile.followingVisibility === 'private') {
+					if (me == null || (me.id !== user.id)) {
 						throw new ApiError(meta.errors.forbidden);
 					}
+				} else if (profile.followingVisibility === 'followers') {
+					if (me == null) {
+						throw new ApiError(meta.errors.forbidden);
+					} else if (me.id !== user.id) {
+						const isFollowing = await this.followingsRepository.exists({
+							where: {
+								followeeId: user.id,
+								followerId: me.id,
+							},
+						});
+						if (!isFollowing) {
+							throw new ApiError(meta.errors.forbidden);
+						}
+					}
 				}
 			}
 
diff --git a/packages/backend/src/server/api/endpoints/users/lists/create-from-public.ts b/packages/backend/src/server/api/endpoints/users/lists/create-from-public.ts
index 8504da0209..7e44d501ab 100644
--- a/packages/backend/src/server/api/endpoints/users/lists/create-from-public.ts
+++ b/packages/backend/src/server/api/endpoints/users/lists/create-from-public.ts
@@ -100,7 +100,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 			const currentCount = await this.userListsRepository.countBy({
 				userId: me.id,
 			});
-			if (currentCount > (await this.roleService.getUserPolicies(me.id)).userListLimit) {
+			if (currentCount >= (await this.roleService.getUserPolicies(me.id)).userListLimit) {
 				throw new ApiError(meta.errors.tooManyUserLists);
 			}
 
diff --git a/packages/backend/src/server/api/endpoints/users/lists/create.ts b/packages/backend/src/server/api/endpoints/users/lists/create.ts
index 9378bde5cb..7daf05ba4e 100644
--- a/packages/backend/src/server/api/endpoints/users/lists/create.ts
+++ b/packages/backend/src/server/api/endpoints/users/lists/create.ts
@@ -61,7 +61,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 			const currentCount = await this.userListsRepository.countBy({
 				userId: me.id,
 			});
-			if (currentCount > (await this.roleService.getUserPolicies(me.id)).userListLimit) {
+			if (currentCount >= (await this.roleService.getUserPolicies(me.id)).userListLimit) {
 				throw new ApiError(meta.errors.tooManyUserLists);
 			}
 
diff --git a/packages/backend/src/server/api/endpoints/users/notes.ts b/packages/backend/src/server/api/endpoints/users/notes.ts
index cc76c12f1d..e9c334057e 100644
--- a/packages/backend/src/server/api/endpoints/users/notes.ts
+++ b/packages/backend/src/server/api/endpoints/users/notes.ts
@@ -5,14 +5,13 @@
 
 import { Brackets } from 'typeorm';
 import { Inject, Injectable } from '@nestjs/common';
-import type { NotesRepository } from '@/models/_.js';
+import type { MiMeta, NotesRepository } from '@/models/_.js';
 import { Endpoint } from '@/server/api/endpoint-base.js';
 import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
 import { DI } from '@/di-symbols.js';
 import { CacheService } from '@/core/CacheService.js';
 import { IdService } from '@/core/IdService.js';
 import { QueryService } from '@/core/QueryService.js';
-import { MetaService } from '@/core/MetaService.js';
 import { MiLocalUser } from '@/models/User.js';
 import { FanoutTimelineEndpointService } from '@/core/FanoutTimelineEndpointService.js';
 import { FanoutTimelineName } from '@/core/FanoutTimelineService.js';
@@ -43,6 +42,12 @@ export const meta = {
 			code: 'BOTH_WITH_REPLIES_AND_WITH_FILES',
 			id: '91c8cb9f-36ed-46e7-9ca2-7df96ed6e222',
 		},
+
+		signinRequired: {
+			message: 'Signin required.',
+			code: 'SIGNIN_REQUIRED',
+			id: 'd1588a9e-4b4d-4c07-807f-16f1486577a2',
+		},
 	},
 } as const;
 
@@ -67,6 +72,9 @@ export const paramDef = {
 @Injectable()
 export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
 	constructor(
+		@Inject(DI.meta)
+		private serverSettings: MiMeta,
+
 		@Inject(DI.notesRepository)
 		private notesRepository: NotesRepository,
 
@@ -75,15 +83,12 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 		private cacheService: CacheService,
 		private idService: IdService,
 		private fanoutTimelineEndpointService: FanoutTimelineEndpointService,
-		private metaService: MetaService,
 	) {
 		super(meta, paramDef, async (ps, me) => {
 			const untilId = ps.untilId ?? (ps.untilDate ? this.idService.gen(ps.untilDate!) : null);
 			const sinceId = ps.sinceId ?? (ps.sinceDate ? this.idService.gen(ps.sinceDate!) : null);
 			const isSelf = me && (me.id === ps.userId);
 
-			const serverSettings = await this.metaService.fetch();
-
 			if (ps.withReplies && ps.withFiles) throw new ApiError(meta.errors.bothWithRepliesAndWithFiles);
 
 			// early return if me is blocked by requesting user
@@ -94,7 +99,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 				}
 			}
 
-			if (!serverSettings.enableFanoutTimeline) {
+			if (!this.serverSettings.enableFanoutTimeline) {
 				const timeline = await this.getFromDb({
 					untilId,
 					sinceId,
diff --git a/packages/backend/src/server/api/endpoints/users/reactions.ts b/packages/backend/src/server/api/endpoints/users/reactions.ts
index aca883a052..7805ae3288 100644
--- a/packages/backend/src/server/api/endpoints/users/reactions.ts
+++ b/packages/backend/src/server/api/endpoints/users/reactions.ts
@@ -12,6 +12,7 @@ import { DI } from '@/di-symbols.js';
 import { CacheService } from '@/core/CacheService.js';
 import { UserEntityService } from '@/core/entities/UserEntityService.js';
 import { RoleService } from '@/core/RoleService.js';
+import { isUserRelated } from '@/misc/is-user-related.js';
 import { ApiError } from '../../error.js';
 
 export const meta = {
@@ -74,6 +75,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 		private roleService: RoleService,
 	) {
 		super(meta, paramDef, async (ps, me) => {
+			const userIdsWhoBlockingMe = me ? await this.cacheService.userBlockedCache.fetch(me.id) : new Set<string>();
 			const iAmModerator = me ? await this.roleService.isModerator(me) : false; // Moderators can see reactions of all users
 			if (!iAmModerator) {
 				const user = await this.cacheService.findUserById(ps.userId);
@@ -85,8 +87,15 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 				if ((me == null || me.id !== ps.userId) && !profile.publicReactions) {
 					throw new ApiError(meta.errors.reactionsNotPublic);
 				}
+
+				// early return if me is blocked by requesting user
+				if (userIdsWhoBlockingMe.has(ps.userId)) {
+					return [];
+				}
 			}
 
+			const userIdsWhoMeMuting = me ? await this.cacheService.userMutingsCache.fetch(me.id) : new Set<string>();
+
 			const query = this.queryService.makePaginationQuery(this.noteReactionsRepository.createQueryBuilder('reaction'),
 				ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
 				.andWhere('reaction.userId = :userId', { userId: ps.userId })
@@ -94,9 +103,15 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 
 			this.queryService.generateVisibilityQuery(query, me);
 
-			const reactions = await query
+			const reactions = (await query
 				.limit(ps.limit)
-				.getMany();
+				.getMany()).filter(reaction => {
+				if (reaction.note?.userId === ps.userId) return true; // we can see reactions to note of requesting user
+				if (me && isUserRelated(reaction.note, userIdsWhoBlockingMe)) return false;
+				if (me && isUserRelated(reaction.note, userIdsWhoMeMuting)) return false;
+
+				return true;
+			});
 
 			return await this.noteReactionEntityService.packMany(reactions, me, { withNote: true });
 		});
diff --git a/packages/backend/src/server/api/endpoints/users/report-abuse.ts b/packages/backend/src/server/api/endpoints/users/report-abuse.ts
index 48e14b68cc..5ff6de37d2 100644
--- a/packages/backend/src/server/api/endpoints/users/report-abuse.ts
+++ b/packages/backend/src/server/api/endpoints/users/report-abuse.ts
@@ -3,17 +3,11 @@
  * SPDX-License-Identifier: AGPL-3.0-only
  */
 
-import sanitizeHtml from 'sanitize-html';
-import { Inject, Injectable } from '@nestjs/common';
-import type { AbuseUserReportsRepository } from '@/models/_.js';
-import { IdService } from '@/core/IdService.js';
+import { Injectable } from '@nestjs/common';
 import { Endpoint } from '@/server/api/endpoint-base.js';
-import { GlobalEventService } from '@/core/GlobalEventService.js';
-import { MetaService } from '@/core/MetaService.js';
-import { EmailService } from '@/core/EmailService.js';
-import { DI } from '@/di-symbols.js';
 import { GetterService } from '@/server/api/GetterService.js';
 import { RoleService } from '@/core/RoleService.js';
+import { AbuseReportService } from '@/core/AbuseReportService.js';
 import { ApiError } from '../../error.js';
 
 export const meta = {
@@ -57,60 +51,32 @@ export const paramDef = {
 @Injectable()
 export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
 	constructor(
-		@Inject(DI.abuseUserReportsRepository)
-		private abuseUserReportsRepository: AbuseUserReportsRepository,
-
-		private idService: IdService,
-		private metaService: MetaService,
-		private emailService: EmailService,
 		private getterService: GetterService,
 		private roleService: RoleService,
-		private globalEventService: GlobalEventService,
+		private abuseReportService: AbuseReportService,
 	) {
 		super(meta, paramDef, async (ps, me) => {
 			// Lookup user
-			const user = await this.getterService.getUser(ps.userId).catch(err => {
+			const targetUser = await this.getterService.getUser(ps.userId).catch(err => {
 				if (err.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser);
 				throw err;
 			});
 
-			if (user.id === me.id) {
+			if (targetUser.id === me.id) {
 				throw new ApiError(meta.errors.cannotReportYourself);
 			}
 
-			if (await this.roleService.isAdministrator(user)) {
+			if (await this.roleService.isAdministrator(targetUser)) {
 				throw new ApiError(meta.errors.cannotReportAdmin);
 			}
 
-			const report = await this.abuseUserReportsRepository.insertOne({
-				id: this.idService.gen(),
-				targetUserId: user.id,
-				targetUserHost: user.host,
+			await this.abuseReportService.report([{
+				targetUserId: targetUser.id,
+				targetUserHost: targetUser.host,
 				reporterId: me.id,
 				reporterHost: null,
 				comment: ps.comment,
-			});
-
-			// Publish event to moderators
-			setImmediate(async () => {
-				const moderators = await this.roleService.getModerators();
-
-				for (const moderator of moderators) {
-					this.globalEventService.publishAdminStream(moderator.id, 'newAbuseUserReport', {
-						id: report.id,
-						targetUserId: report.targetUserId,
-						reporterId: report.reporterId,
-						comment: report.comment,
-					});
-				}
-
-				const meta = await this.metaService.fetch();
-				if (meta.email) {
-					this.emailService.sendEmail(meta.email, 'New abuse report',
-						sanitizeHtml(ps.comment),
-						sanitizeHtml(ps.comment));
-				}
-			});
+			}]);
 		});
 	}
 }
diff --git a/packages/backend/src/server/api/endpoints/users/search-by-username-and-host.ts b/packages/backend/src/server/api/endpoints/users/search-by-username-and-host.ts
index 7b3bdab327..8ff952dcb5 100644
--- a/packages/backend/src/server/api/endpoints/users/search-by-username-and-host.ts
+++ b/packages/backend/src/server/api/endpoints/users/search-by-username-and-host.ts
@@ -3,15 +3,9 @@
  * SPDX-License-Identifier: AGPL-3.0-only
  */
 
-import { Brackets } from 'typeorm';
-import { Inject, Injectable } from '@nestjs/common';
-import type { UsersRepository, FollowingsRepository } from '@/models/_.js';
-import type { Config } from '@/config.js';
-import type { MiUser } from '@/models/User.js';
+import { Injectable } from '@nestjs/common';
 import { Endpoint } from '@/server/api/endpoint-base.js';
-import { UserEntityService } from '@/core/entities/UserEntityService.js';
-import { DI } from '@/di-symbols.js';
-import { sqlLikeEscape } from '@/misc/sql-like-escape.js';
+import { UserSearchService } from '@/core/UserSearchService.js';
 
 export const meta = {
 	tags: ['users'],
@@ -49,89 +43,16 @@ export const paramDef = {
 @Injectable()
 export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
 	constructor(
-		@Inject(DI.config)
-		private config: Config,
-
-		@Inject(DI.usersRepository)
-		private usersRepository: UsersRepository,
-
-		@Inject(DI.followingsRepository)
-		private followingsRepository: FollowingsRepository,
-
-		private userEntityService: UserEntityService,
+		private userSearchService: UserSearchService,
 	) {
-		super(meta, paramDef, async (ps, me) => {
-			const setUsernameAndHostQuery = (query = this.usersRepository.createQueryBuilder('user')) => {
-				if (ps.username) {
-					query.andWhere('user.usernameLower LIKE :username', { username: sqlLikeEscape(ps.username.toLowerCase()) + '%' });
-				}
-
-				if (ps.host) {
-					if (ps.host === this.config.hostname || ps.host === '.') {
-						query.andWhere('user.host IS NULL');
-					} else {
-						query.andWhere('user.host LIKE :host', {
-							host: sqlLikeEscape(ps.host.toLowerCase()) + '%',
-						});
-					}
-				}
-
-				return query;
-			};
-
-			const activeThreshold = new Date(Date.now() - (1000 * 60 * 60 * 24 * 30)); // 30日
-
-			let users: MiUser[] = [];
-
-			if (me) {
-				const followingQuery = this.followingsRepository.createQueryBuilder('following')
-					.select('following.followeeId')
-					.where('following.followerId = :followerId', { followerId: me.id });
-
-				const query = setUsernameAndHostQuery()
-					.andWhere(`user.id IN (${ followingQuery.getQuery() })`)
-					.andWhere('user.id != :meId', { meId: me.id })
-					.andWhere('user.isSuspended = FALSE')
-					.andWhere(new Brackets(qb => {
-						qb
-							.where('user.updatedAt IS NULL')
-							.orWhere('user.updatedAt > :activeThreshold', { activeThreshold: activeThreshold });
-					}));
-
-				query.setParameters(followingQuery.getParameters());
-
-				users = await query
-					.orderBy('user.usernameLower', 'ASC')
-					.limit(ps.limit)
-					.getMany();
-
-				if (users.length < ps.limit) {
-					const otherQuery = setUsernameAndHostQuery()
-						.andWhere(`user.id NOT IN (${ followingQuery.getQuery() })`)
-						.andWhere('user.isSuspended = FALSE')
-						.andWhere('user.updatedAt IS NOT NULL');
-
-					otherQuery.setParameters(followingQuery.getParameters());
-
-					const otherUsers = await otherQuery
-						.orderBy('user.updatedAt', 'DESC')
-						.limit(ps.limit - users.length)
-						.getMany();
-
-					users = users.concat(otherUsers);
-				}
-			} else {
-				const query = setUsernameAndHostQuery()
-					.andWhere('user.isSuspended = FALSE')
-					.andWhere('user.updatedAt IS NOT NULL');
-
-				users = await query
-					.orderBy('user.updatedAt', 'DESC')
-					.limit(ps.limit - users.length)
-					.getMany();
-			}
-
-			return await this.userEntityService.packMany(users, me, { schema: ps.detail ? 'UserDetailed' : 'UserLite' });
+		super(meta, paramDef, (ps, me) => {
+			return this.userSearchService.search({
+				username: ps.username,
+				host: ps.host,
+			}, {
+				limit: ps.limit,
+				detail: ps.detail,
+			}, me);
 		});
 	}
 }
diff --git a/packages/backend/src/server/api/endpoints/users/search.ts b/packages/backend/src/server/api/endpoints/users/search.ts
index df9d9f6312..0b0136066d 100644
--- a/packages/backend/src/server/api/endpoints/users/search.ts
+++ b/packages/backend/src/server/api/endpoints/users/search.ts
@@ -57,88 +57,66 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 			const activeThreshold = new Date(Date.now() - (1000 * 60 * 60 * 24 * 30)); // 30日
 
 			ps.query = ps.query.trim();
-			const isUsername = ps.query.startsWith('@');
+			const isUsername = ps.query.startsWith('@') && !ps.query.includes(' ') && ps.query.indexOf('@', 1) === -1;
 
 			let users: MiUser[] = [];
 
-			if (isUsername) {
-				const usernameQuery = this.usersRepository.createQueryBuilder('user')
-					.where('user.usernameLower LIKE :username', { username: sqlLikeEscape(ps.query.replace('@', '').toLowerCase()) + '%' })
-					.andWhere(new Brackets(qb => {
-						qb
-							.where('user.updatedAt IS NULL')
-							.orWhere('user.updatedAt > :activeThreshold', { activeThreshold: activeThreshold });
-					}))
-					.andWhere('user.isSuspended = FALSE');
+			const nameQuery = this.usersRepository.createQueryBuilder('user')
+				.where(new Brackets(qb => {
+					qb.where('user.name ILIKE :query', { query: '%' + sqlLikeEscape(ps.query) + '%' });
 
-				if (ps.origin === 'local') {
-					usernameQuery.andWhere('user.host IS NULL');
-				} else if (ps.origin === 'remote') {
-					usernameQuery.andWhere('user.host IS NOT NULL');
-				}
-
-				users = await usernameQuery
-					.orderBy('user.updatedAt', 'DESC', 'NULLS LAST')
-					.limit(ps.limit)
-					.offset(ps.offset)
-					.getMany();
-			} else {
-				const nameQuery = this.usersRepository.createQueryBuilder('user')
-					.where(new Brackets(qb => {
-						qb.where('user.name ILIKE :query', { query: '%' + sqlLikeEscape(ps.query) + '%' });
-
-						// Also search username if it qualifies as username
-						if (this.userEntityService.validateLocalUsername(ps.query)) {
-							qb.orWhere('user.usernameLower LIKE :username', { username: '%' + sqlLikeEscape(ps.query.toLowerCase()) + '%' });
-						}
-					}))
-					.andWhere(new Brackets(qb => {
-						qb
-							.where('user.updatedAt IS NULL')
-							.orWhere('user.updatedAt > :activeThreshold', { activeThreshold: activeThreshold });
-					}))
-					.andWhere('user.isSuspended = FALSE');
-
-				if (ps.origin === 'local') {
-					nameQuery.andWhere('user.host IS NULL');
-				} else if (ps.origin === 'remote') {
-					nameQuery.andWhere('user.host IS NOT NULL');
-				}
-
-				users = await nameQuery
-					.orderBy('user.updatedAt', 'DESC', 'NULLS LAST')
-					.limit(ps.limit)
-					.offset(ps.offset)
-					.getMany();
-
-				if (users.length < ps.limit) {
-					const profQuery = this.userProfilesRepository.createQueryBuilder('prof')
-						.select('prof.userId')
-						.where('prof.description ILIKE :query', { query: '%' + sqlLikeEscape(ps.query) + '%' });
-
-					if (ps.origin === 'local') {
-						profQuery.andWhere('prof.userHost IS NULL');
-					} else if (ps.origin === 'remote') {
-						profQuery.andWhere('prof.userHost IS NOT NULL');
+					if (isUsername) {
+						qb.orWhere('user.usernameLower LIKE :username', { username: sqlLikeEscape(ps.query.replace('@', '').toLowerCase()) + '%' });
+					} else if (this.userEntityService.validateLocalUsername(ps.query)) { // Also search username if it qualifies as username
+						qb.orWhere('user.usernameLower LIKE :username', { username: '%' + sqlLikeEscape(ps.query.toLowerCase()) + '%' });
 					}
+				}))
+				.andWhere(new Brackets(qb => {
+					qb
+						.where('user.updatedAt IS NULL')
+						.orWhere('user.updatedAt > :activeThreshold', { activeThreshold: activeThreshold });
+				}))
+				.andWhere('user.isSuspended = FALSE');
 
-					const query = this.usersRepository.createQueryBuilder('user')
-						.where(`user.id IN (${ profQuery.getQuery() })`)
-						.andWhere(new Brackets(qb => {
-							qb
-								.where('user.updatedAt IS NULL')
-								.orWhere('user.updatedAt > :activeThreshold', { activeThreshold: activeThreshold });
-						}))
-						.andWhere('user.isSuspended = FALSE')
-						.setParameters(profQuery.getParameters());
+			if (ps.origin === 'local') {
+				nameQuery.andWhere('user.host IS NULL');
+			} else if (ps.origin === 'remote') {
+				nameQuery.andWhere('user.host IS NOT NULL');
+			}
 
-					users = users.concat(await query
-						.orderBy('user.updatedAt', 'DESC', 'NULLS LAST')
-						.limit(ps.limit)
-						.offset(ps.offset)
-						.getMany(),
-					);
+			users = await nameQuery
+				.orderBy('user.updatedAt', 'DESC', 'NULLS LAST')
+				.limit(ps.limit)
+				.offset(ps.offset)
+				.getMany();
+
+			if (users.length < ps.limit) {
+				const profQuery = this.userProfilesRepository.createQueryBuilder('prof')
+					.select('prof.userId')
+					.where('prof.description ILIKE :query', { query: '%' + sqlLikeEscape(ps.query) + '%' });
+
+				if (ps.origin === 'local') {
+					profQuery.andWhere('prof.userHost IS NULL');
+				} else if (ps.origin === 'remote') {
+					profQuery.andWhere('prof.userHost IS NOT NULL');
 				}
+
+				const query = this.usersRepository.createQueryBuilder('user')
+					.where(`user.id IN (${ profQuery.getQuery() })`)
+					.andWhere(new Brackets(qb => {
+						qb
+							.where('user.updatedAt IS NULL')
+							.orWhere('user.updatedAt > :activeThreshold', { activeThreshold: activeThreshold });
+					}))
+					.andWhere('user.isSuspended = FALSE')
+					.setParameters(profQuery.getParameters());
+
+				users = users.concat(await query
+					.orderBy('user.updatedAt', 'DESC', 'NULLS LAST')
+					.limit(ps.limit)
+					.offset(ps.offset)
+					.getMany(),
+				);
 			}
 
 			return await this.userEntityService.packMany(users, me, { schema: ps.detail ? 'UserDetailed' : 'UserLite' });
diff --git a/packages/backend/src/server/api/openapi/OpenApiServerService.ts b/packages/backend/src/server/api/openapi/OpenApiServerService.ts
index 5210e4d2bc..f124aa9f39 100644
--- a/packages/backend/src/server/api/openapi/OpenApiServerService.ts
+++ b/packages/backend/src/server/api/openapi/OpenApiServerService.ts
@@ -25,7 +25,7 @@ export class OpenApiServerService {
 	public createServer(fastify: FastifyInstance, _options: FastifyPluginOptions, done: (err?: Error) => void) {
 		fastify.get('/api-doc', async (_request, reply) => {
 			reply.header('Cache-Control', 'public, max-age=86400');
-			return await reply.sendFile('/redoc.html', staticAssets);
+			return await reply.sendFile('/api-doc.html', staticAssets);
 		});
 		fastify.get('/api.json', (_request, reply) => {
 			reply.header('Cache-Control', 'public, max-age=600');
diff --git a/packages/backend/src/server/api/openapi/gen-spec.ts b/packages/backend/src/server/api/openapi/gen-spec.ts
index 2a14270a24..efa47a6986 100644
--- a/packages/backend/src/server/api/openapi/gen-spec.ts
+++ b/packages/backend/src/server/api/openapi/gen-spec.ts
@@ -15,7 +15,6 @@ export function genOpenapiSpec(config: Config, includeSelfRef = false) {
 		info: {
 			version: config.version,
 			title: 'Misskey API',
-			'x-logo': { url: '/static-assets/api-doc.png' },
 		},
 
 		externalDocs: {
diff --git a/packages/backend/src/server/api/stream/Connection.ts b/packages/backend/src/server/api/stream/Connection.ts
index 41c0feccc7..0fb5238c78 100644
--- a/packages/backend/src/server/api/stream/Connection.ts
+++ b/packages/backend/src/server/api/stream/Connection.ts
@@ -14,10 +14,14 @@ import { CacheService } from '@/core/CacheService.js';
 import { MiFollowing, MiUserProfile } from '@/models/_.js';
 import type { StreamEventEmitter, GlobalEvents } from '@/core/GlobalEventService.js';
 import { ChannelFollowingService } from '@/core/ChannelFollowingService.js';
+import { isJsonObject } from '@/misc/json-value.js';
+import type { JsonObject, JsonValue } from '@/misc/json-value.js';
 import type { ChannelsService } from './ChannelsService.js';
 import type { EventEmitter } from 'events';
 import type Channel from './channel.js';
 
+const MAX_CHANNELS_PER_CONNECTION = 32;
+
 /**
  * Main stream connection
  */
@@ -28,7 +32,7 @@ export default class Connection {
 	private wsConnection: WebSocket.WebSocket;
 	public subscriber: StreamEventEmitter;
 	private channels: Channel[] = [];
-	private subscribingNotes: any = {};
+	private subscribingNotes: Partial<Record<string, number>> = {};
 	private cachedNotes: Packed<'Note'>[] = [];
 	public userProfile: MiUserProfile | null = null;
 	public following: Record<string, Pick<MiFollowing, 'withReplies'> | undefined> = {};
@@ -101,7 +105,7 @@ export default class Connection {
 	 */
 	@bindThis
 	private async onWsConnectionMessage(data: WebSocket.RawData) {
-		let obj: Record<string, any>;
+		let obj: JsonObject;
 
 		try {
 			obj = JSON.parse(data.toString());
@@ -151,7 +155,8 @@ export default class Connection {
 	}
 
 	@bindThis
-	private readNote(body: any) {
+	private readNote(body: JsonValue | undefined) {
+		if (!isJsonObject(body)) return;
 		const id = body.id;
 
 		const note = this.cachedNotes.find(n => n.id === id);
@@ -163,7 +168,7 @@ export default class Connection {
 	}
 
 	@bindThis
-	private onReadNotification(payload: any) {
+	private onReadNotification(payload: JsonValue | undefined) {
 		this.notificationService.readAllNotification(this.user!.id);
 	}
 
@@ -171,16 +176,15 @@ export default class Connection {
 	 * 投稿購読要求時
 	 */
 	@bindThis
-	private onSubscribeNote(payload: any) {
-		if (!payload.id) return;
+	private onSubscribeNote(payload: JsonValue | undefined) {
+		if (!isJsonObject(payload)) return;
+		if (!payload.id || typeof payload.id !== 'string') return;
 
-		if (this.subscribingNotes[payload.id] == null) {
-			this.subscribingNotes[payload.id] = 0;
-		}
+		const current = this.subscribingNotes[payload.id] ?? 0;
+		const updated = current + 1;
+		this.subscribingNotes[payload.id] = updated;
 
-		this.subscribingNotes[payload.id]++;
-
-		if (this.subscribingNotes[payload.id] === 1) {
+		if (updated === 1) {
 			this.subscriber.on(`noteStream:${payload.id}`, this.onNoteStreamMessage);
 		}
 	}
@@ -189,11 +193,15 @@ export default class Connection {
 	 * 投稿購読解除要求時
 	 */
 	@bindThis
-	private onUnsubscribeNote(payload: any) {
-		if (!payload.id) return;
+	private onUnsubscribeNote(payload: JsonValue | undefined) {
+		if (!isJsonObject(payload)) return;
+		if (!payload.id || typeof payload.id !== 'string') return;
 
-		this.subscribingNotes[payload.id]--;
-		if (this.subscribingNotes[payload.id] <= 0) {
+		const current = this.subscribingNotes[payload.id];
+		if (current == null) return;
+		const updated = current - 1;
+		this.subscribingNotes[payload.id] = updated;
+		if (updated <= 0) {
 			delete this.subscribingNotes[payload.id];
 			this.subscriber.off(`noteStream:${payload.id}`, this.onNoteStreamMessage);
 		}
@@ -212,17 +220,24 @@ export default class Connection {
 	 * チャンネル接続要求時
 	 */
 	@bindThis
-	private onChannelConnectRequested(payload: any) {
+	private onChannelConnectRequested(payload: JsonValue | undefined) {
+		if (!isJsonObject(payload)) return;
 		const { channel, id, params, pong } = payload;
-		this.connectChannel(id, params, channel, pong);
+		if (typeof id !== 'string') return;
+		if (typeof channel !== 'string') return;
+		if (typeof pong !== 'boolean' && typeof pong !== 'undefined' && pong !== null) return;
+		if (typeof params !== 'undefined' && !isJsonObject(params)) return;
+		this.connectChannel(id, params, channel, pong ?? undefined);
 	}
 
 	/**
 	 * チャンネル切断要求時
 	 */
 	@bindThis
-	private onChannelDisconnectRequested(payload: any) {
+	private onChannelDisconnectRequested(payload: JsonValue | undefined) {
+		if (!isJsonObject(payload)) return;
 		const { id } = payload;
+		if (typeof id !== 'string') return;
 		this.disconnectChannel(id);
 	}
 
@@ -230,7 +245,7 @@ export default class Connection {
 	 * クライアントにメッセージ送信
 	 */
 	@bindThis
-	public sendMessageToWs(type: string, payload: any) {
+	public sendMessageToWs(type: string, payload: JsonObject) {
 		this.wsConnection.send(JSON.stringify({
 			type: type,
 			body: payload,
@@ -241,7 +256,11 @@ export default class Connection {
 	 * チャンネルに接続
 	 */
 	@bindThis
-	public connectChannel(id: string, params: any, channel: string, pong = false) {
+	public connectChannel(id: string, params: JsonObject | undefined, channel: string, pong = false) {
+		if (this.channels.length >= MAX_CHANNELS_PER_CONNECTION) {
+			return;
+		}
+
 		const channelService = this.channelsService.getChannelService(channel);
 
 		if (channelService.requireCredential && this.user == null) {
@@ -288,7 +307,12 @@ export default class Connection {
 	 * @param data メッセージ
 	 */
 	@bindThis
-	private onChannelMessageRequested(data: any) {
+	private onChannelMessageRequested(data: JsonValue | undefined) {
+		if (!isJsonObject(data)) return;
+		if (typeof data.id !== 'string') return;
+		if (typeof data.type !== 'string') return;
+		if (typeof data.body === 'undefined') return;
+
 		const channel = this.channels.find(c => c.id === data.id);
 		if (channel != null && channel.onMessage != null) {
 			channel.onMessage(data.type, data.body);
diff --git a/packages/backend/src/server/api/stream/channel.ts b/packages/backend/src/server/api/stream/channel.ts
index a267d27fba..84cb552369 100644
--- a/packages/backend/src/server/api/stream/channel.ts
+++ b/packages/backend/src/server/api/stream/channel.ts
@@ -8,6 +8,7 @@ import { isInstanceMuted } from '@/misc/is-instance-muted.js';
 import { isUserRelated } from '@/misc/is-user-related.js';
 import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js';
 import type { Packed } from '@/misc/json-schema.js';
+import type { JsonObject, JsonValue } from '@/misc/json-value.js';
 import type Connection from './Connection.js';
 
 /**
@@ -81,10 +82,12 @@ export default abstract class Channel {
 		this.connection = connection;
 	}
 
+	public send(payload: { type: string, body: JsonValue }): void
+	public send(type: string, payload: JsonValue): void
 	@bindThis
-	public send(typeOrPayload: any, payload?: any) {
-		const type = payload === undefined ? typeOrPayload.type : typeOrPayload;
-		const body = payload === undefined ? typeOrPayload.body : payload;
+	public send(typeOrPayload: { type: string, body: JsonValue } | string, payload?: JsonValue) {
+		const type = payload === undefined ? (typeOrPayload as { type: string, body: JsonValue }).type : (typeOrPayload as string);
+		const body = payload === undefined ? (typeOrPayload as { type: string, body: JsonValue }).body : payload;
 
 		this.connection.sendMessageToWs('channel', {
 			id: this.id,
@@ -93,11 +96,11 @@ export default abstract class Channel {
 		});
 	}
 
-	public abstract init(params: any): void;
+	public abstract init(params: JsonObject): void;
 
 	public dispose?(): void;
 
-	public onMessage?(type: string, body: any): void;
+	public onMessage?(type: string, body: JsonValue): void;
 }
 
 export type MiChannelService<T extends boolean> = {
diff --git a/packages/backend/src/server/api/stream/channels/admin.ts b/packages/backend/src/server/api/stream/channels/admin.ts
index 92b6d2ac04..355d5dba21 100644
--- a/packages/backend/src/server/api/stream/channels/admin.ts
+++ b/packages/backend/src/server/api/stream/channels/admin.ts
@@ -5,6 +5,7 @@
 
 import { Injectable } from '@nestjs/common';
 import { bindThis } from '@/decorators.js';
+import type { JsonObject } from '@/misc/json-value.js';
 import Channel, { type MiChannelService } from '../channel.js';
 
 class AdminChannel extends Channel {
@@ -14,7 +15,7 @@ class AdminChannel extends Channel {
 	public static kind = 'read:admin:stream';
 
 	@bindThis
-	public async init(params: any) {
+	public async init(params: JsonObject) {
 		// Subscribe admin stream
 		this.subscriber.on(`adminStream:${this.user!.id}`, data => {
 			this.send(data);
diff --git a/packages/backend/src/server/api/stream/channels/antenna.ts b/packages/backend/src/server/api/stream/channels/antenna.ts
index 4a1d2dd109..53dc7f18b6 100644
--- a/packages/backend/src/server/api/stream/channels/antenna.ts
+++ b/packages/backend/src/server/api/stream/channels/antenna.ts
@@ -7,6 +7,7 @@ import { Injectable } from '@nestjs/common';
 import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
 import { bindThis } from '@/decorators.js';
 import type { GlobalEvents } from '@/core/GlobalEventService.js';
+import type { JsonObject } from '@/misc/json-value.js';
 import Channel, { type MiChannelService } from '../channel.js';
 
 class AntennaChannel extends Channel {
@@ -27,8 +28,9 @@ class AntennaChannel extends Channel {
 	}
 
 	@bindThis
-	public async init(params: any) {
-		this.antennaId = params.antennaId as string;
+	public async init(params: JsonObject) {
+		if (typeof params.antennaId !== 'string') return;
+		this.antennaId = params.antennaId;
 
 		// Subscribe stream
 		this.subscriber.on(`antennaStream:${this.antennaId}`, this.onEvent);
diff --git a/packages/backend/src/server/api/stream/channels/channel.ts b/packages/backend/src/server/api/stream/channels/channel.ts
index 140dd3dd9b..7108e0cd6e 100644
--- a/packages/backend/src/server/api/stream/channels/channel.ts
+++ b/packages/backend/src/server/api/stream/channels/channel.ts
@@ -8,6 +8,7 @@ import type { Packed } from '@/misc/json-schema.js';
 import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
 import { bindThis } from '@/decorators.js';
 import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js';
+import type { JsonObject } from '@/misc/json-value.js';
 import Channel, { type MiChannelService } from '../channel.js';
 
 class ChannelChannel extends Channel {
@@ -27,8 +28,9 @@ class ChannelChannel extends Channel {
 	}
 
 	@bindThis
-	public async init(params: any) {
-		this.channelId = params.channelId as string;
+	public async init(params: JsonObject) {
+		if (typeof params.channelId !== 'string') return;
+		this.channelId = params.channelId;
 
 		// Subscribe stream
 		this.subscriber.on('notesStream', this.onNote);
diff --git a/packages/backend/src/server/api/stream/channels/drive.ts b/packages/backend/src/server/api/stream/channels/drive.ts
index 0d9b486305..03768f3d23 100644
--- a/packages/backend/src/server/api/stream/channels/drive.ts
+++ b/packages/backend/src/server/api/stream/channels/drive.ts
@@ -5,6 +5,7 @@
 
 import { Injectable } from '@nestjs/common';
 import { bindThis } from '@/decorators.js';
+import type { JsonObject } from '@/misc/json-value.js';
 import Channel, { type MiChannelService } from '../channel.js';
 
 class DriveChannel extends Channel {
@@ -14,7 +15,7 @@ class DriveChannel extends Channel {
 	public static kind = 'read:account';
 
 	@bindThis
-	public async init(params: any) {
+	public async init(params: JsonObject) {
 		// Subscribe drive stream
 		this.subscriber.on(`driveStream:${this.user!.id}`, data => {
 			this.send(data);
diff --git a/packages/backend/src/server/api/stream/channels/global-timeline.ts b/packages/backend/src/server/api/stream/channels/global-timeline.ts
index 17116258d8..ed56fe0d40 100644
--- a/packages/backend/src/server/api/stream/channels/global-timeline.ts
+++ b/packages/backend/src/server/api/stream/channels/global-timeline.ts
@@ -10,6 +10,7 @@ import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
 import { bindThis } from '@/decorators.js';
 import { RoleService } from '@/core/RoleService.js';
 import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js';
+import type { JsonObject } from '@/misc/json-value.js';
 import Channel, { type MiChannelService } from '../channel.js';
 
 class GlobalTimelineChannel extends Channel {
@@ -32,12 +33,12 @@ class GlobalTimelineChannel extends Channel {
 	}
 
 	@bindThis
-	public async init(params: any) {
+	public async init(params: JsonObject) {
 		const policies = await this.roleService.getUserPolicies(this.user ? this.user.id : null);
 		if (!policies.gtlAvailable) return;
 
-		this.withRenotes = params.withRenotes ?? true;
-		this.withFiles = params.withFiles ?? false;
+		this.withRenotes = !!(params.withRenotes ?? true);
+		this.withFiles = !!(params.withFiles ?? false);
 
 		// Subscribe events
 		this.subscriber.on('notesStream', this.onNote);
diff --git a/packages/backend/src/server/api/stream/channels/hashtag.ts b/packages/backend/src/server/api/stream/channels/hashtag.ts
index 57bada5d9c..8105f15cb1 100644
--- a/packages/backend/src/server/api/stream/channels/hashtag.ts
+++ b/packages/backend/src/server/api/stream/channels/hashtag.ts
@@ -9,6 +9,7 @@ import type { Packed } from '@/misc/json-schema.js';
 import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
 import { bindThis } from '@/decorators.js';
 import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js';
+import type { JsonObject } from '@/misc/json-value.js';
 import Channel, { type MiChannelService } from '../channel.js';
 
 class HashtagChannel extends Channel {
@@ -28,11 +29,11 @@ class HashtagChannel extends Channel {
 	}
 
 	@bindThis
-	public async init(params: any) {
+	public async init(params: JsonObject) {
+		if (!Array.isArray(params.q)) return;
+		if (!params.q.every(x => Array.isArray(x) && x.every(y => typeof y === 'string'))) return;
 		this.q = params.q;
 
-		if (this.q == null) return;
-
 		// Subscribe stream
 		this.subscriber.on('notesStream', this.onNote);
 	}
diff --git a/packages/backend/src/server/api/stream/channels/home-timeline.ts b/packages/backend/src/server/api/stream/channels/home-timeline.ts
index 878a3180cb..66644ed58c 100644
--- a/packages/backend/src/server/api/stream/channels/home-timeline.ts
+++ b/packages/backend/src/server/api/stream/channels/home-timeline.ts
@@ -8,6 +8,7 @@ import type { Packed } from '@/misc/json-schema.js';
 import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
 import { bindThis } from '@/decorators.js';
 import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js';
+import type { JsonObject } from '@/misc/json-value.js';
 import Channel, { type MiChannelService } from '../channel.js';
 
 class HomeTimelineChannel extends Channel {
@@ -29,9 +30,9 @@ class HomeTimelineChannel extends Channel {
 	}
 
 	@bindThis
-	public async init(params: any) {
-		this.withRenotes = params.withRenotes ?? true;
-		this.withFiles = params.withFiles ?? false;
+	public async init(params: JsonObject) {
+		this.withRenotes = !!(params.withRenotes ?? true);
+		this.withFiles = !!(params.withFiles ?? false);
 
 		this.subscriber.on('notesStream', this.onNote);
 	}
@@ -59,7 +60,7 @@ class HomeTimelineChannel extends Channel {
 			const reply = note.reply;
 			if (this.following[note.userId]?.withReplies) {
 				// 自分のフォローしていないユーザーの visibility: followers な投稿への返信は弾く
-				if (reply.visibility === 'followers' && !Object.hasOwn(this.following, reply.userId)) return;
+				if (reply.visibility === 'followers' && !Object.hasOwn(this.following, reply.userId) && reply.userId !== this.user!.id) return;
 			} else {
 				// 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合
 				if (reply.userId !== this.user!.id && !isMe && reply.userId !== note.userId) return;
@@ -72,7 +73,7 @@ class HomeTimelineChannel extends Channel {
 			if (note.renote.reply) {
 				const reply = note.renote.reply;
 				// 自分のフォローしていないユーザーの visibility: followers な投稿への返信のリノートは弾く
-				if (reply.visibility === 'followers' && !Object.hasOwn(this.following, reply.userId)) return;
+				if (reply.visibility === 'followers' && !Object.hasOwn(this.following, reply.userId) && reply.userId !== this.user!.id) return;
 			}
 		}
 
diff --git a/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts b/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts
index 575d23d53c..75bd13221f 100644
--- a/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts
+++ b/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts
@@ -10,6 +10,7 @@ import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
 import { bindThis } from '@/decorators.js';
 import { RoleService } from '@/core/RoleService.js';
 import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js';
+import type { JsonObject } from '@/misc/json-value.js';
 import Channel, { type MiChannelService } from '../channel.js';
 
 class HybridTimelineChannel extends Channel {
@@ -34,13 +35,13 @@ class HybridTimelineChannel extends Channel {
 	}
 
 	@bindThis
-	public async init(params: any): Promise<void> {
+	public async init(params: JsonObject): Promise<void> {
 		const policies = await this.roleService.getUserPolicies(this.user ? this.user.id : null);
 		if (!policies.ltlAvailable) return;
 
-		this.withRenotes = params.withRenotes ?? true;
-		this.withReplies = params.withReplies ?? false;
-		this.withFiles = params.withFiles ?? false;
+		this.withRenotes = !!(params.withRenotes ?? true);
+		this.withReplies = !!(params.withReplies ?? false);
+		this.withFiles = !!(params.withFiles ?? false);
 
 		// Subscribe events
 		this.subscriber.on('notesStream', this.onNote);
@@ -75,14 +76,22 @@ class HybridTimelineChannel extends Channel {
 			const reply = note.reply;
 			if ((this.following[note.userId]?.withReplies ?? false) || this.withReplies) {
 				// 自分のフォローしていないユーザーの visibility: followers な投稿への返信は弾く
-				if (reply.visibility === 'followers' && !Object.hasOwn(this.following, reply.userId)) return;
+				if (reply.visibility === 'followers' && !Object.hasOwn(this.following, reply.userId) && reply.userId !== this.user!.id) return;
 			} else {
 				// 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合
 				if (reply.userId !== this.user!.id && !isMe && reply.userId !== note.userId) return;
 			}
 		}
 
-		if (isRenotePacked(note) && !isQuotePacked(note) && !this.withRenotes) return;
+		// 純粋なリノート(引用リノートでないリノート)の場合
+		if (isRenotePacked(note) && !isQuotePacked(note) && note.renote) {
+			if (!this.withRenotes) return;
+			if (note.renote.reply) {
+				const reply = note.renote.reply;
+				// 自分のフォローしていないユーザーの visibility: followers な投稿への返信のリノートは弾く
+				if (reply.visibility === 'followers' && !Object.hasOwn(this.following, reply.userId) && reply.userId !== this.user!.id) return;
+			}
+		}
 
 		if (this.user && note.renoteId && !note.text) {
 			if (note.renote && Object.keys(note.renote.reactions).length > 0) {
diff --git a/packages/backend/src/server/api/stream/channels/local-timeline.ts b/packages/backend/src/server/api/stream/channels/local-timeline.ts
index 442d08ae51..491029f5de 100644
--- a/packages/backend/src/server/api/stream/channels/local-timeline.ts
+++ b/packages/backend/src/server/api/stream/channels/local-timeline.ts
@@ -10,6 +10,7 @@ import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
 import { bindThis } from '@/decorators.js';
 import { RoleService } from '@/core/RoleService.js';
 import { isQuotePacked, isRenotePacked } from '@/misc/is-renote.js';
+import type { JsonObject } from '@/misc/json-value.js';
 import Channel, { type MiChannelService } from '../channel.js';
 
 class LocalTimelineChannel extends Channel {
@@ -33,13 +34,13 @@ class LocalTimelineChannel extends Channel {
 	}
 
 	@bindThis
-	public async init(params: any) {
+	public async init(params: JsonObject) {
 		const policies = await this.roleService.getUserPolicies(this.user ? this.user.id : null);
 		if (!policies.ltlAvailable) return;
 
-		this.withRenotes = params.withRenotes ?? true;
-		this.withReplies = params.withReplies ?? false;
-		this.withFiles = params.withFiles ?? false;
+		this.withRenotes = !!(params.withRenotes ?? true);
+		this.withReplies = !!(params.withReplies ?? false);
+		this.withFiles = !!(params.withFiles ?? false);
 
 		// Subscribe events
 		this.subscriber.on('notesStream', this.onNote);
diff --git a/packages/backend/src/server/api/stream/channels/main.ts b/packages/backend/src/server/api/stream/channels/main.ts
index a12976d69d..863d7f4c4e 100644
--- a/packages/backend/src/server/api/stream/channels/main.ts
+++ b/packages/backend/src/server/api/stream/channels/main.ts
@@ -7,6 +7,7 @@ import { Injectable } from '@nestjs/common';
 import { isInstanceMuted, isUserFromMutedInstance } from '@/misc/is-instance-muted.js';
 import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
 import { bindThis } from '@/decorators.js';
+import type { JsonObject } from '@/misc/json-value.js';
 import Channel, { type MiChannelService } from '../channel.js';
 
 class MainChannel extends Channel {
@@ -25,7 +26,7 @@ class MainChannel extends Channel {
 	}
 
 	@bindThis
-	public async init(params: any) {
+	public async init(params: JsonObject) {
 		// Subscribe main stream channel
 		this.subscriber.on(`mainStream:${this.user!.id}`, async data => {
 			switch (data.type) {
diff --git a/packages/backend/src/server/api/stream/channels/queue-stats.ts b/packages/backend/src/server/api/stream/channels/queue-stats.ts
index 061aa76904..91b62255b4 100644
--- a/packages/backend/src/server/api/stream/channels/queue-stats.ts
+++ b/packages/backend/src/server/api/stream/channels/queue-stats.ts
@@ -6,6 +6,8 @@
 import Xev from 'xev';
 import { Injectable } from '@nestjs/common';
 import { bindThis } from '@/decorators.js';
+import { isJsonObject } from '@/misc/json-value.js';
+import type { JsonObject, JsonValue } from '@/misc/json-value.js';
 import Channel, { type MiChannelService } from '../channel.js';
 
 const ev = new Xev();
@@ -22,19 +24,22 @@ class QueueStatsChannel extends Channel {
 	}
 
 	@bindThis
-	public async init(params: any) {
+	public async init(params: JsonObject) {
 		ev.addListener('queueStats', this.onStats);
 	}
 
 	@bindThis
-	private onStats(stats: any) {
+	private onStats(stats: JsonObject) {
 		this.send('stats', stats);
 	}
 
 	@bindThis
-	public onMessage(type: string, body: any) {
+	public onMessage(type: string, body: JsonValue) {
 		switch (type) {
 			case 'requestLog':
+				if (!isJsonObject(body)) return;
+				if (typeof body.id !== 'string') return;
+				if (typeof body.length !== 'number') return;
 				ev.once(`queueStatsLog:${body.id}`, statsLog => {
 					this.send('statsLog', statsLog);
 				});
diff --git a/packages/backend/src/server/api/stream/channels/reversi-game.ts b/packages/backend/src/server/api/stream/channels/reversi-game.ts
index f4a3a09367..7597a1cfa3 100644
--- a/packages/backend/src/server/api/stream/channels/reversi-game.ts
+++ b/packages/backend/src/server/api/stream/channels/reversi-game.ts
@@ -9,7 +9,10 @@ import { DI } from '@/di-symbols.js';
 import { bindThis } from '@/decorators.js';
 import { ReversiService } from '@/core/ReversiService.js';
 import { ReversiGameEntityService } from '@/core/entities/ReversiGameEntityService.js';
+import { isJsonObject } from '@/misc/json-value.js';
+import type { JsonObject, JsonValue } from '@/misc/json-value.js';
 import Channel, { type MiChannelService } from '../channel.js';
+import { reversiUpdateKeys } from 'misskey-js';
 
 class ReversiGameChannel extends Channel {
 	public readonly chName = 'reversiGame';
@@ -28,25 +31,42 @@ class ReversiGameChannel extends Channel {
 	}
 
 	@bindThis
-	public async init(params: any) {
-		this.gameId = params.gameId as string;
+	public async init(params: JsonObject) {
+		if (typeof params.gameId !== 'string') return;
+		this.gameId = params.gameId;
 
 		this.subscriber.on(`reversiGameStream:${this.gameId}`, this.send);
 	}
 
 	@bindThis
-	public onMessage(type: string, body: any) {
+	public onMessage(type: string, body: JsonValue) {
 		switch (type) {
-			case 'ready': this.ready(body); break;
-			case 'updateSettings': this.updateSettings(body.key, body.value); break;
-			case 'cancel': this.cancelGame(); break;
-			case 'putStone': this.putStone(body.pos, body.id); break;
+			case 'ready':
+				if (typeof body !== 'boolean') return;
+				this.ready(body);
+				break;
+			case 'updateSettings':
+				if (!isJsonObject(body)) return;
+				if (!this.reversiService.isValidReversiUpdateKey(body.key)) return;
+				if (!this.reversiService.isValidReversiUpdateValue(body.key, body.value)) return;
+
+				this.updateSettings(body.key, body.value);
+				break;
+			case 'cancel':
+				this.cancelGame();
+				break;
+			case 'putStone':
+				if (!isJsonObject(body)) return;
+				if (typeof body.pos !== 'number') return;
+				if (typeof body.id !== 'string') return;
+				this.putStone(body.pos, body.id);
+				break;
 			case 'claimTimeIsUp': this.claimTimeIsUp(); break;
 		}
 	}
 
 	@bindThis
-	private async updateSettings(key: string, value: any) {
+	private async updateSettings<K extends typeof reversiUpdateKeys[number]>(key: K, value: MiReversiGame[K]) {
 		if (this.user == null) return;
 
 		this.reversiService.updateSettings(this.gameId!, this.user, key, value);
diff --git a/packages/backend/src/server/api/stream/channels/reversi.ts b/packages/backend/src/server/api/stream/channels/reversi.ts
index 3998a0fd36..6e88939724 100644
--- a/packages/backend/src/server/api/stream/channels/reversi.ts
+++ b/packages/backend/src/server/api/stream/channels/reversi.ts
@@ -5,6 +5,7 @@
 
 import { Injectable } from '@nestjs/common';
 import { bindThis } from '@/decorators.js';
+import type { JsonObject } from '@/misc/json-value.js';
 import Channel, { type MiChannelService } from '../channel.js';
 
 class ReversiChannel extends Channel {
@@ -21,7 +22,7 @@ class ReversiChannel extends Channel {
 	}
 
 	@bindThis
-	public async init(params: any) {
+	public async init(params: JsonObject) {
 		this.subscriber.on(`reversiStream:${this.user!.id}`, this.send);
 	}
 
diff --git a/packages/backend/src/server/api/stream/channels/role-timeline.ts b/packages/backend/src/server/api/stream/channels/role-timeline.ts
index 6a4ad22460..fcfa26c38b 100644
--- a/packages/backend/src/server/api/stream/channels/role-timeline.ts
+++ b/packages/backend/src/server/api/stream/channels/role-timeline.ts
@@ -8,6 +8,7 @@ import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
 import { bindThis } from '@/decorators.js';
 import { RoleService } from '@/core/RoleService.js';
 import type { GlobalEvents } from '@/core/GlobalEventService.js';
+import type { JsonObject } from '@/misc/json-value.js';
 import Channel, { type MiChannelService } from '../channel.js';
 
 class RoleTimelineChannel extends Channel {
@@ -28,8 +29,9 @@ class RoleTimelineChannel extends Channel {
 	}
 
 	@bindThis
-	public async init(params: any) {
-		this.roleId = params.roleId as string;
+	public async init(params: JsonObject) {
+		if (typeof params.roleId !== 'string') return;
+		this.roleId = params.roleId;
 
 		this.subscriber.on(`roleTimelineStream:${this.roleId}`, this.onEvent);
 	}
diff --git a/packages/backend/src/server/api/stream/channels/server-stats.ts b/packages/backend/src/server/api/stream/channels/server-stats.ts
index eb4d8c9992..ec5352d12d 100644
--- a/packages/backend/src/server/api/stream/channels/server-stats.ts
+++ b/packages/backend/src/server/api/stream/channels/server-stats.ts
@@ -6,6 +6,8 @@
 import Xev from 'xev';
 import { Injectable } from '@nestjs/common';
 import { bindThis } from '@/decorators.js';
+import { isJsonObject } from '@/misc/json-value.js';
+import type { JsonObject, JsonValue } from '@/misc/json-value.js';
 import Channel, { type MiChannelService } from '../channel.js';
 
 const ev = new Xev();
@@ -22,19 +24,20 @@ class ServerStatsChannel extends Channel {
 	}
 
 	@bindThis
-	public async init(params: any) {
+	public async init(params: JsonObject) {
 		ev.addListener('serverStats', this.onStats);
 	}
 
 	@bindThis
-	private onStats(stats: any) {
+	private onStats(stats: JsonObject) {
 		this.send('stats', stats);
 	}
 
 	@bindThis
-	public onMessage(type: string, body: any) {
+	public onMessage(type: string, body: JsonValue) {
 		switch (type) {
 			case 'requestLog':
+				if (!isJsonObject(body)) return;
 				ev.once(`serverStatsLog:${body.id}`, statsLog => {
 					this.send('statsLog', statsLog);
 				});
diff --git a/packages/backend/src/server/api/stream/channels/user-list.ts b/packages/backend/src/server/api/stream/channels/user-list.ts
index 14b30a157c..4f38351e94 100644
--- a/packages/backend/src/server/api/stream/channels/user-list.ts
+++ b/packages/backend/src/server/api/stream/channels/user-list.ts
@@ -10,6 +10,7 @@ import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
 import { DI } from '@/di-symbols.js';
 import { bindThis } from '@/decorators.js';
 import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js';
+import type { JsonObject } from '@/misc/json-value.js';
 import Channel, { type MiChannelService } from '../channel.js';
 
 class UserListChannel extends Channel {
@@ -36,10 +37,11 @@ class UserListChannel extends Channel {
 	}
 
 	@bindThis
-	public async init(params: any) {
-		this.listId = params.listId as string;
-		this.withFiles = params.withFiles ?? false;
-		this.withRenotes = params.withRenotes ?? true;
+	public async init(params: JsonObject) {
+		if (typeof params.listId !== 'string') return;
+		this.listId = params.listId;
+		this.withFiles = !!(params.withFiles ?? false);
+		this.withRenotes = !!(params.withRenotes ?? true);
 
 		// Check existence and owner
 		const listExist = await this.userListsRepository.exists({
diff --git a/packages/backend/src/server/web/ClientServerService.ts b/packages/backend/src/server/web/ClientServerService.ts
index ab03489c0d..4860ef3e12 100644
--- a/packages/backend/src/server/web/ClientServerService.ts
+++ b/packages/backend/src/server/web/ClientServerService.ts
@@ -9,7 +9,7 @@ import { fileURLToPath } from 'node:url';
 import { Inject, Injectable } from '@nestjs/common';
 import { createBullBoard } from '@bull-board/api';
 import { BullMQAdapter } from '@bull-board/api/bullMQAdapter.js';
-import { FastifyAdapter } from '@bull-board/fastify';
+import { FastifyAdapter as BullBoardFastifyAdapter } from '@bull-board/fastify';
 import ms from 'ms';
 import sharp from 'sharp';
 import pug from 'pug';
@@ -24,8 +24,17 @@ import type { Config } from '@/config.js';
 import { getNoteSummary } from '@/misc/get-note-summary.js';
 import { DI } from '@/di-symbols.js';
 import * as Acct from '@/misc/acct.js';
-import { MetaService } from '@/core/MetaService.js';
-import type { DbQueue, DeliverQueue, EndedPollNotificationQueue, InboxQueue, ObjectStorageQueue, SystemQueue, WebhookDeliverQueue } from '@/core/QueueModule.js';
+import type {
+	DbQueue,
+	DeliverQueue,
+	EndedPollNotificationQueue,
+	InboxQueue,
+	ObjectStorageQueue,
+	RelationshipQueue,
+	SystemQueue,
+	UserWebhookDeliverQueue,
+	SystemWebhookDeliverQueue,
+} from '@/core/QueueModule.js';
 import { UserEntityService } from '@/core/entities/UserEntityService.js';
 import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
 import { PageEntityService } from '@/core/entities/PageEntityService.js';
@@ -52,7 +61,8 @@ const staticAssets = `${_dirname}/../../../assets/`;
 const clientAssets = `${_dirname}/../../../../frontend/assets/`;
 const assets = `${_dirname}/../../../../../built/_frontend_dist_/`;
 const swAssets = `${_dirname}/../../../../../built/_sw_dist_/`;
-const viteOut = `${_dirname}/../../../../../built/_vite_/`;
+const frontendViteOut = `${_dirname}/../../../../../built/_frontend_vite_/`;
+const frontendEmbedViteOut = `${_dirname}/../../../../../built/_frontend_embed_vite_/`;
 const tarball = `${_dirname}/../../../../../built/tarball/`;
 
 @Injectable()
@@ -63,6 +73,9 @@ export class ClientServerService {
 		@Inject(DI.config)
 		private config: Config,
 
+		@Inject(DI.meta)
+		private meta: MiMeta,
+
 		@Inject(DI.usersRepository)
 		private usersRepository: UsersRepository,
 
@@ -99,7 +112,6 @@ export class ClientServerService {
 		private clipEntityService: ClipEntityService,
 		private channelEntityService: ChannelEntityService,
 		private reversiGameEntityService: ReversiGameEntityService,
-		private metaService: MetaService,
 		private urlPreviewService: UrlPreviewService,
 		private feedService: FeedService,
 		private roleService: RoleService,
@@ -110,40 +122,40 @@ export class ClientServerService {
 		@Inject('queue:deliver') public deliverQueue: DeliverQueue,
 		@Inject('queue:inbox') public inboxQueue: InboxQueue,
 		@Inject('queue:db') public dbQueue: DbQueue,
+		@Inject('queue:relationship') public relationshipQueue: RelationshipQueue,
 		@Inject('queue:objectStorage') public objectStorageQueue: ObjectStorageQueue,
-		@Inject('queue:webhookDeliver') public webhookDeliverQueue: WebhookDeliverQueue,
+		@Inject('queue:userWebhookDeliver') public userWebhookDeliverQueue: UserWebhookDeliverQueue,
+		@Inject('queue:systemWebhookDeliver') public systemWebhookDeliverQueue: SystemWebhookDeliverQueue,
 	) {
 		//this.createServer = this.createServer.bind(this);
 	}
 
 	@bindThis
 	private async manifestHandler(reply: FastifyReply) {
-		const instance = await this.metaService.fetch(true);
-
 		let manifest = {
 			// 空文字列の場合右辺を使いたいため
 			// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
-			'short_name': instance.shortName || instance.name || this.config.host,
+			'short_name': this.meta.shortName || this.meta.name || this.config.host,
 			// 空文字列の場合右辺を使いたいため
 			// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
-			'name': instance.name || this.config.host,
+			'name': this.meta.name || this.config.host,
 			'start_url': '/',
 			'display': 'standalone',
 			'background_color': '#313a42',
 			// 空文字列の場合右辺を使いたいため
 			// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
-			'theme_color': instance.themeColor || '#86b300',
+			'theme_color': this.meta.themeColor || '#86b300',
 			'icons': [{
 				// 空文字列の場合右辺を使いたいため
 				// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
-				'src': instance.app192IconUrl || '/static-assets/icons/192.png',
+				'src': this.meta.app192IconUrl || '/static-assets/icons/192.png',
 				'sizes': '192x192',
 				'type': 'image/png',
 				'purpose': 'maskable',
 			}, {
 				// 空文字列の場合右辺を使いたいため
 				// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
-				'src': instance.app512IconUrl || '/static-assets/icons/512.png',
+				'src': this.meta.app512IconUrl || '/static-assets/icons/512.png',
 				'sizes': '512x512',
 				'type': 'image/png',
 				'purpose': 'maskable',
@@ -167,7 +179,7 @@ export class ClientServerService {
 
 		manifest = {
 			...manifest,
-			...JSON.parse(instance.manifestJsonOverride === '' ? '{}' : instance.manifestJsonOverride),
+			...JSON.parse(this.meta.manifestJsonOverride === '' ? '{}' : this.meta.manifestJsonOverride),
 		};
 
 		reply.header('Cache-Control', 'max-age=300');
@@ -229,7 +241,7 @@ export class ClientServerService {
 			}
 		});
 
-		const serverAdapter = new FastifyAdapter();
+		const bullBoardServerAdapter = new BullBoardFastifyAdapter();
 
 		createBullBoard({
 			queues: [
@@ -238,14 +250,16 @@ export class ClientServerService {
 				this.deliverQueue,
 				this.inboxQueue,
 				this.dbQueue,
+				this.relationshipQueue,
 				this.objectStorageQueue,
-				this.webhookDeliverQueue,
+				this.userWebhookDeliverQueue,
+				this.systemWebhookDeliverQueue,
 			].map(q => new BullMQAdapter(q)),
-			serverAdapter,
+			serverAdapter: bullBoardServerAdapter,
 		});
 
-		serverAdapter.setBasePath(bullBoardPath);
-		(fastify.register as any)(serverAdapter.registerPlugin(), { prefix: bullBoardPath });
+		bullBoardServerAdapter.setBasePath(bullBoardPath);
+		(fastify.register as any)(bullBoardServerAdapter.registerPlugin(), { prefix: bullBoardPath });
 		//#endregion
 
 		fastify.register(fastifyView, {
@@ -266,15 +280,22 @@ export class ClientServerService {
 		});
 
 		//#region vite assets
-		if (this.config.clientManifestExists) {
+		if (this.config.frontendEmbedManifestExists) {
 			fastify.register((fastify, options, done) => {
 				fastify.register(fastifyStatic, {
-					root: viteOut,
+					root: frontendViteOut,
 					prefix: '/vite/',
 					maxAge: ms('30 days'),
 					immutable: true,
 					decorateReply: false,
 				});
+				fastify.register(fastifyStatic, {
+					root: frontendEmbedViteOut,
+					prefix: '/embed_vite/',
+					maxAge: ms('30 days'),
+					immutable: true,
+					decorateReply: false,
+				});
 				fastify.addHook('onRequest', handleRequestRedirectToOmitSearch);
 				done();
 			});
@@ -285,6 +306,13 @@ export class ClientServerService {
 				prefix: '/vite',
 				rewritePrefix: '/vite',
 			});
+
+			const embedPort = (process.env.EMBED_VITE_PORT ?? '5174');
+			fastify.register(fastifyProxy, {
+				upstream: 'http://localhost:' + embedPort,
+				prefix: '/embed_vite',
+				rewritePrefix: '/embed_vite',
+			});
 		}
 		//#endregion
 
@@ -414,15 +442,20 @@ export class ClientServerService {
 		// Manifest
 		fastify.get('/manifest.json', async (request, reply) => await this.manifestHandler(reply));
 
+		// Embed Javascript
+		fastify.get('/embed.js', async (request, reply) => {
+			return await reply.sendFile('/embed.js', staticAssets, {
+				maxAge: ms('1 day'),
+			});
+		});
+
 		fastify.get('/robots.txt', async (request, reply) => {
 			return await reply.sendFile('/robots.txt', staticAssets);
 		});
 
 		// OpenSearch XML
 		fastify.get('/opensearch.xml', async (request, reply) => {
-			const meta = await this.metaService.fetch();
-
-			const name = meta.name ?? 'Misskey';
+			const name = this.meta.name ?? 'Misskey';
 			let content = '';
 			content += '<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/" xmlns:moz="http://www.mozilla.org/2006/browser/search/">';
 			content += `<ShortName>${name}</ShortName>`;
@@ -439,14 +472,13 @@ export class ClientServerService {
 		//#endregion
 
 		const renderBase = async (reply: FastifyReply, data: { [key: string]: any } = {}) => {
-			const meta = await this.metaService.fetch();
 			reply.header('Cache-Control', 'public, max-age=30');
 			return await reply.view('base', {
-				img: meta.bannerUrl,
+				img: this.meta.bannerUrl,
 				url: this.config.url,
-				title: meta.name ?? 'Misskey',
-				desc: meta.description,
-				...await this.generateCommonPugData(meta),
+				title: this.meta.name ?? 'Misskey',
+				desc: this.meta.description,
+				...await this.generateCommonPugData(this.meta),
 				...data,
 			});
 		};
@@ -524,7 +556,6 @@ export class ClientServerService {
 
 			if (user != null) {
 				const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id });
-				const meta = await this.metaService.fetch();
 				const me = profile.fields
 					? profile.fields
 						.filter(filed => filed.value != null && filed.value.match(/^https?:/))
@@ -540,7 +571,7 @@ export class ClientServerService {
 					user, profile, me,
 					avatarUrl: user.avatarUrl ?? this.userEntityService.getIdenticonUrl(user),
 					sub: request.params.sub,
-					...await this.generateCommonPugData(meta),
+					...await this.generateCommonPugData(this.meta),
 				});
 			} else {
 				// リモートユーザーなので
@@ -570,15 +601,17 @@ export class ClientServerService {
 		fastify.get<{ Params: { note: string; } }>('/notes/:note', async (request, reply) => {
 			vary(reply.raw, 'Accept');
 
-			const note = await this.notesRepository.findOneBy({
-				id: request.params.note,
-				visibility: In(['public', 'home']),
+			const note = await this.notesRepository.findOne({
+				where: {
+					id: request.params.note,
+					visibility: In(['public', 'home']),
+				},
+				relations: ['user'],
 			});
 
-			if (note) {
+			if (note && !note.user!.requireSigninToViewContents) {
 				const _note = await this.noteEntityService.pack(note);
 				const profile = await this.userProfilesRepository.findOneByOrFail({ userId: note.userId });
-				const meta = await this.metaService.fetch();
 				reply.header('Cache-Control', 'public, max-age=15');
 				if (profile.preventAiLearning) {
 					reply.header('X-Robots-Tag', 'noimageai');
@@ -590,7 +623,7 @@ export class ClientServerService {
 					avatarUrl: _note.user.avatarUrl,
 					// TODO: Let locale changeable by instance setting
 					summary: getNoteSummary(_note),
-					...await this.generateCommonPugData(meta),
+					...await this.generateCommonPugData(this.meta),
 				});
 			} else {
 				return await renderBase(reply);
@@ -615,7 +648,6 @@ export class ClientServerService {
 			if (page) {
 				const _page = await this.pageEntityService.pack(page);
 				const profile = await this.userProfilesRepository.findOneByOrFail({ userId: page.userId });
-				const meta = await this.metaService.fetch();
 				if (['public'].includes(page.visibility)) {
 					reply.header('Cache-Control', 'public, max-age=15');
 				} else {
@@ -629,7 +661,7 @@ export class ClientServerService {
 					page: _page,
 					profile,
 					avatarUrl: _page.user.avatarUrl,
-					...await this.generateCommonPugData(meta),
+					...await this.generateCommonPugData(this.meta),
 				});
 			} else {
 				return await renderBase(reply);
@@ -645,7 +677,6 @@ export class ClientServerService {
 			if (flash) {
 				const _flash = await this.flashEntityService.pack(flash);
 				const profile = await this.userProfilesRepository.findOneByOrFail({ userId: flash.userId });
-				const meta = await this.metaService.fetch();
 				reply.header('Cache-Control', 'public, max-age=15');
 				if (profile.preventAiLearning) {
 					reply.header('X-Robots-Tag', 'noimageai');
@@ -655,7 +686,7 @@ export class ClientServerService {
 					flash: _flash,
 					profile,
 					avatarUrl: _flash.user.avatarUrl,
-					...await this.generateCommonPugData(meta),
+					...await this.generateCommonPugData(this.meta),
 				});
 			} else {
 				return await renderBase(reply);
@@ -671,7 +702,6 @@ export class ClientServerService {
 			if (clip && clip.isPublic) {
 				const _clip = await this.clipEntityService.pack(clip);
 				const profile = await this.userProfilesRepository.findOneByOrFail({ userId: clip.userId });
-				const meta = await this.metaService.fetch();
 				reply.header('Cache-Control', 'public, max-age=15');
 				if (profile.preventAiLearning) {
 					reply.header('X-Robots-Tag', 'noimageai');
@@ -681,7 +711,7 @@ export class ClientServerService {
 					clip: _clip,
 					profile,
 					avatarUrl: _clip.user.avatarUrl,
-					...await this.generateCommonPugData(meta),
+					...await this.generateCommonPugData(this.meta),
 				});
 			} else {
 				return await renderBase(reply);
@@ -695,7 +725,6 @@ export class ClientServerService {
 			if (post) {
 				const _post = await this.galleryPostEntityService.pack(post);
 				const profile = await this.userProfilesRepository.findOneByOrFail({ userId: post.userId });
-				const meta = await this.metaService.fetch();
 				reply.header('Cache-Control', 'public, max-age=15');
 				if (profile.preventAiLearning) {
 					reply.header('X-Robots-Tag', 'noimageai');
@@ -705,7 +734,7 @@ export class ClientServerService {
 					post: _post,
 					profile,
 					avatarUrl: _post.user.avatarUrl,
-					...await this.generateCommonPugData(meta),
+					...await this.generateCommonPugData(this.meta),
 				});
 			} else {
 				return await renderBase(reply);
@@ -720,11 +749,10 @@ export class ClientServerService {
 
 			if (channel) {
 				const _channel = await this.channelEntityService.pack(channel);
-				const meta = await this.metaService.fetch();
 				reply.header('Cache-Control', 'public, max-age=15');
 				return await reply.view('channel', {
 					channel: _channel,
-					...await this.generateCommonPugData(meta),
+					...await this.generateCommonPugData(this.meta),
 				});
 			} else {
 				return await renderBase(reply);
@@ -739,11 +767,10 @@ export class ClientServerService {
 
 			if (game) {
 				const _game = await this.reversiGameEntityService.packDetail(game);
-				const meta = await this.metaService.fetch();
 				reply.header('Cache-Control', 'public, max-age=3600');
 				return await reply.view('reversi-game', {
 					game: _game,
-					...await this.generateCommonPugData(meta),
+					...await this.generateCommonPugData(this.meta),
 				});
 			} else {
 				return await renderBase(reply);
@@ -751,7 +778,7 @@ export class ClientServerService {
 		});
 		//#endregion
 
-		//region noindex pages
+		//#region noindex pages
 		// Tags
 		fastify.get<{ Params: { clip: string; } }>('/tags/:tag', async (request, reply) => {
 			return await renderBase(reply, { noindex: true });
@@ -761,21 +788,97 @@ export class ClientServerService {
 		fastify.get<{ Params: { clip: string; } }>('/user-tags/:tag', async (request, reply) => {
 			return await renderBase(reply, { noindex: true });
 		});
-		//endregion
+		//#endregion
+
+		//#region embed pages
+		fastify.get<{ Params: { user: string; } }>('/embed/user-timeline/:user', async (request, reply) => {
+			reply.removeHeader('X-Frame-Options');
+
+			const user = await this.usersRepository.findOneBy({
+				id: request.params.user,
+			});
+
+			if (user == null) return;
+			if (user.host != null) return;
+
+			const _user = await this.userEntityService.pack(user);
+
+			reply.header('Cache-Control', 'public, max-age=3600');
+			return await reply.view('base-embed', {
+				title: this.meta.name ?? 'Misskey',
+				...await this.generateCommonPugData(this.meta),
+				embedCtx: htmlSafeJsonStringify({
+					user: _user,
+				}),
+			});
+		});
+
+		fastify.get<{ Params: { note: string; } }>('/embed/notes/:note', async (request, reply) => {
+			reply.removeHeader('X-Frame-Options');
+
+			const note = await this.notesRepository.findOneBy({
+				id: request.params.note,
+			});
+
+			if (note == null) return;
+			if (note.visibility !== 'public') return;
+			if (note.userHost != null) return;
+
+			const _note = await this.noteEntityService.pack(note, null, { detail: true });
+
+			reply.header('Cache-Control', 'public, max-age=3600');
+			return await reply.view('base-embed', {
+				title: this.meta.name ?? 'Misskey',
+				...await this.generateCommonPugData(this.meta),
+				embedCtx: htmlSafeJsonStringify({
+					note: _note,
+				}),
+			});
+		});
+
+		fastify.get<{ Params: { clip: string; } }>('/embed/clips/:clip', async (request, reply) => {
+			reply.removeHeader('X-Frame-Options');
+
+			const clip = await this.clipsRepository.findOneBy({
+				id: request.params.clip,
+			});
+
+			if (clip == null) return;
+
+			const _clip = await this.clipEntityService.pack(clip);
+
+			reply.header('Cache-Control', 'public, max-age=3600');
+			return await reply.view('base-embed', {
+				title: this.meta.name ?? 'Misskey',
+				...await this.generateCommonPugData(this.meta),
+				embedCtx: htmlSafeJsonStringify({
+					clip: _clip,
+				}),
+			});
+		});
+
+		fastify.get('/embed/*', async (request, reply) => {
+			reply.removeHeader('X-Frame-Options');
+
+			reply.header('Cache-Control', 'public, max-age=3600');
+			return await reply.view('base-embed', {
+				title: this.meta.name ?? 'Misskey',
+				...await this.generateCommonPugData(this.meta),
+			});
+		});
 
 		fastify.get('/_info_card_', async (request, reply) => {
-			const meta = await this.metaService.fetch(true);
-
 			reply.removeHeader('X-Frame-Options');
 
 			return await reply.view('info-card', {
 				version: this.config.version,
 				host: this.config.host,
-				meta: meta,
+				meta: this.meta,
 				originalUsersCount: await this.usersRepository.countBy({ host: IsNull() }),
 				originalNotesCount: await this.notesRepository.countBy({ userHost: IsNull() }),
 			});
 		});
+		//#endregion
 
 		fastify.get('/bios', async (request, reply) => {
 			return await reply.view('bios', {
diff --git a/packages/backend/src/server/web/FeedService.ts b/packages/backend/src/server/web/FeedService.ts
index 10e3ed2682..9d810ddc84 100644
--- a/packages/backend/src/server/web/FeedService.ts
+++ b/packages/backend/src/server/web/FeedService.ts
@@ -14,6 +14,8 @@ import { UserEntityService } from '@/core/entities/UserEntityService.js';
 import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
 import { bindThis } from '@/decorators.js';
 import { IdService } from '@/core/IdService.js';
+import { MfmService } from "@/core/MfmService.js";
+import { parse as mfmParse } from 'mfm-js';
 
 @Injectable()
 export class FeedService {
@@ -33,6 +35,7 @@ export class FeedService {
 		private userEntityService: UserEntityService,
 		private driveFileEntityService: DriveFileEntityService,
 		private idService: IdService,
+		private mfmService: MfmService,
 	) {
 	}
 
@@ -76,13 +79,14 @@ export class FeedService {
 				id: In(note.fileIds),
 			}) : [];
 			const file = files.find(file => file.type.startsWith('image/'));
+			const text = note.text;
 
 			feed.addItem({
 				title: `New note by ${author.name}`,
 				link: `${this.config.url}/notes/${note.id}`,
 				date: this.idService.parse(note.id).date,
 				description: note.cw ?? undefined,
-				content: note.text ?? undefined,
+				content: text ? this.mfmService.toHtml(mfmParse(text), JSON.parse(note.mentionedRemoteUsers)) ?? undefined : undefined,
 				image: file ? this.driveFileEntityService.getPublicUrl(file) : undefined,
 			});
 		}
diff --git a/packages/backend/src/server/web/UrlPreviewService.ts b/packages/backend/src/server/web/UrlPreviewService.ts
index 8f8f08a305..5d493c2c46 100644
--- a/packages/backend/src/server/web/UrlPreviewService.ts
+++ b/packages/backend/src/server/web/UrlPreviewService.ts
@@ -8,7 +8,6 @@ import { summaly } from '@misskey-dev/summaly';
 import { SummalyResult } from '@misskey-dev/summaly/built/summary.js';
 import { DI } from '@/di-symbols.js';
 import type { Config } from '@/config.js';
-import { MetaService } from '@/core/MetaService.js';
 import { HttpRequestService } from '@/core/HttpRequestService.js';
 import type Logger from '@/logger.js';
 import { query } from '@/misc/prelude/url.js';
@@ -26,7 +25,9 @@ export class UrlPreviewService {
 		@Inject(DI.config)
 		private config: Config,
 
-		private metaService: MetaService,
+		@Inject(DI.meta)
+		private meta: MiMeta,
+
 		private httpRequestService: HttpRequestService,
 		private loggerService: LoggerService,
 	) {
@@ -62,9 +63,7 @@ export class UrlPreviewService {
 			return;
 		}
 
-		const meta = await this.metaService.fetch();
-
-		if (!meta.urlPreviewEnabled) {
+		if (!this.meta.urlPreviewEnabled) {
 			reply.code(403);
 			return {
 				error: new ApiError({
@@ -75,14 +74,14 @@ export class UrlPreviewService {
 			};
 		}
 
-		this.logger.info(meta.urlPreviewSummaryProxyUrl
+		this.logger.info(this.meta.urlPreviewSummaryProxyUrl
 			? `(Proxy) Getting preview of ${url}@${lang} ...`
 			: `Getting preview of ${url}@${lang} ...`);
 
 		try {
-			const summary = meta.urlPreviewSummaryProxyUrl
-				? await this.fetchSummaryFromProxy(url, meta, lang)
-				: await this.fetchSummary(url, meta, lang);
+			const summary = this.meta.urlPreviewSummaryProxyUrl
+				? await this.fetchSummaryFromProxy(url, this.meta, lang)
+				: await this.fetchSummary(url, this.meta, lang);
 
 			this.logger.succ(`Got preview of ${url}: ${summary.title}`);
 
diff --git a/packages/backend/src/server/web/boot.embed.js b/packages/backend/src/server/web/boot.embed.js
new file mode 100644
index 0000000000..48d1cd262b
--- /dev/null
+++ b/packages/backend/src/server/web/boot.embed.js
@@ -0,0 +1,219 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+'use strict';
+
+// ブロックの中に入れないと、定義した変数がブラウザのグローバルスコープに登録されてしまい邪魔なので
+(async () => {
+	window.onerror = (e) => {
+		console.error(e);
+		renderError('SOMETHING_HAPPENED');
+	};
+	window.onunhandledrejection = (e) => {
+		console.error(e);
+		renderError('SOMETHING_HAPPENED_IN_PROMISE');
+	};
+
+	let forceError = localStorage.getItem('forceError');
+	if (forceError != null) {
+		renderError('FORCED_ERROR', 'This error is forced by having forceError in local storage.');
+		return;
+	}
+
+	// パラメータに応じてsplashのスタイルを変更
+	const params = new URLSearchParams(location.search);
+	if (params.has('rounded') && params.get('rounded') === 'false') {
+		document.documentElement.classList.add('norounded');
+	}
+	if (params.has('border') && params.get('border') === 'false') {
+		document.documentElement.classList.add('noborder');
+	}
+
+	//#region Detect language & fetch translations
+	if (!localStorage.hasOwnProperty('locale')) {
+		const supportedLangs = LANGS;
+		let lang = localStorage.getItem('lang');
+		if (lang == null || !supportedLangs.includes(lang)) {
+			if (supportedLangs.includes(navigator.language)) {
+				lang = navigator.language;
+			} else {
+				lang = supportedLangs.find(x => x.split('-')[0] === navigator.language);
+
+				// Fallback
+				if (lang == null) lang = 'en-US';
+			}
+		}
+
+		const metaRes = await window.fetch('/api/meta', {
+			method: 'POST',
+			body: JSON.stringify({}),
+			credentials: 'omit',
+			cache: 'no-cache',
+			headers: {
+				'Content-Type': 'application/json',
+			},
+		});
+		if (metaRes.status !== 200) {
+			renderError('META_FETCH');
+			return;
+		}
+		const meta = await metaRes.json();
+		const v = meta.version;
+		if (v == null) {
+			renderError('META_FETCH_V');
+			return;
+		}
+
+		// for https://github.com/misskey-dev/misskey/issues/10202
+		if (lang == null || lang.toString == null || lang.toString() === 'null') {
+			console.error('invalid lang value detected!!!', typeof lang, lang);
+			lang = 'en-US';
+		}
+
+		const localRes = await window.fetch(`/assets/locales/${lang}.${v}.json`);
+		if (localRes.status === 200) {
+			localStorage.setItem('lang', lang);
+			localStorage.setItem('locale', await localRes.text());
+			localStorage.setItem('localeVersion', v);
+		} else {
+			renderError('LOCALE_FETCH');
+			return;
+		}
+	}
+	//#endregion
+
+	//#region Script
+	async function importAppScript() {
+		await import(`/embed_vite/${CLIENT_ENTRY}`)
+			.catch(async e => {
+				console.error(e);
+				renderError('APP_IMPORT');
+			});
+	}
+
+	// タイミングによっては、この時点でDOMの構築が済んでいる場合とそうでない場合とがある
+	if (document.readyState !== 'loading') {
+		importAppScript();
+	} else {
+		window.addEventListener('DOMContentLoaded', () => {
+			importAppScript();
+		});
+	}
+	//#endregion
+
+	async function addStyle(styleText) {
+		let css = document.createElement('style');
+		css.appendChild(document.createTextNode(styleText));
+		document.head.appendChild(css);
+	}
+
+	async function renderError(code) {
+		// Cannot set property 'innerHTML' of null を回避
+		if (document.readyState === 'loading') {
+			await new Promise(resolve => window.addEventListener('DOMContentLoaded', resolve));
+		}
+		document.body.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M12 12m-9 0a9 9 0 1 0 18 0a9 9 0 1 0 -18 0" /><path d="M12 9v4" /><path d="M12 16v.01" /></svg>
+		<div class="message">読み込みに失敗しました</div>
+		<div class="submessage">Failed to initialize Misskey</div>
+		<div class="submessage">Error Code: ${code}</div>
+		<button onclick="location.reload(!0)">
+			<div>リロード</div>
+			<div><small>Reload</small></div>
+		</button>`;
+		addStyle(`
+		#misskey_app,
+		#splash {
+			display: none !important;
+		}
+
+		html,
+		body {
+			margin: 0;
+		}
+
+		body {
+			position: relative;
+			color: #dee7e4;
+			font-family: Hiragino Maru Gothic Pro, BIZ UDGothic, Roboto, HelveticaNeue, Arial, sans-serif;
+			line-height: 1.35;
+			display: flex;
+			flex-direction: column;
+			align-items: center;
+			justify-content: center;
+			min-height: 100vh;
+			margin: 0;
+			padding: 24px;
+			box-sizing: border-box;
+			overflow: hidden;
+
+			border-radius: var(--radius, 12px);
+			border: 1px solid rgba(231, 255, 251, 0.14);
+		}
+
+		body::before {
+			content: '';
+			position: fixed;
+			top: 0;
+			left: 0;
+			width: 100%;
+			height: 100%;
+			background: #192320;
+			border-radius: var(--radius, 12px);
+			z-index: -1;
+		}
+
+		html.embed.norounded body,
+		html.embed.norounded body::before {
+			border-radius: 0;
+		}
+
+		html.embed.noborder body {
+			border: none;
+		}
+
+		.icon {
+			max-width: 60px;
+			width: 100%;
+			height: auto;
+			margin-bottom: 20px;
+			color: #dec340;
+		}
+
+		.message {
+			text-align: center;
+			font-size: 20px;
+			font-weight: 700;
+			margin-bottom: 20px;
+		}
+
+		.submessage {
+			text-align: center;
+			font-size: 90%;
+			margin-bottom: 7.5px;
+		}
+
+		.submessage:last-of-type {
+			margin-bottom: 20px;
+		}
+
+		button {
+			padding: 7px 14px;
+			min-width: 100px;
+			font-weight: 700;
+			font-family: Hiragino Maru Gothic Pro, BIZ UDGothic, Roboto, HelveticaNeue, Arial, sans-serif;
+			line-height: 1.35;
+			border-radius: 99rem;
+			background-color: #b4e900;
+			color: #192320;
+			border: none;
+			cursor: pointer;
+			-webkit-tap-highlight-color: transparent;
+		}
+
+		button:hover {
+			background-color: #c6ff03;
+		}`);
+	}
+})();
diff --git a/packages/backend/src/server/web/boot.js b/packages/backend/src/server/web/boot.js
index 396536948e..a04640d993 100644
--- a/packages/backend/src/server/web/boot.js
+++ b/packages/backend/src/server/web/boot.js
@@ -3,17 +3,6 @@
  * SPDX-License-Identifier: AGPL-3.0-only
  */
 
-/**
- * BOOT LOADER
- * サーバーからレスポンスされるHTMLに埋め込まれるスクリプトで、以下の役割を持ちます。
- * - 翻訳ファイルをフェッチする。
- * - バージョンに基づいて適切なメインスクリプトを読み込む。
- * - キャッシュされたコンパイル済みテーマを適用する。
- * - クライアントの設定値に基づいて対応するHTMLクラス等を設定する。
- * テーマをこの段階で設定するのは、メインスクリプトが読み込まれる間もテーマを適用したいためです。
- * 注: webpackは介さないため、このファイルではrequireやimportは使えません。
- */
-
 'use strict';
 
 // ブロックの中に入れないと、定義した変数がブラウザのグローバルスコープに登録されてしまい邪魔なので
@@ -29,7 +18,8 @@
 
 	let forceError = localStorage.getItem('forceError');
 	if (forceError != null) {
-		renderError('FORCED_ERROR', 'This error is forced by having forceError in local storage.')
+		renderError('FORCED_ERROR', 'This error is forced by having forceError in local storage.');
+		return;
 	}
 
 	//#region Detect language & fetch translations
@@ -108,7 +98,7 @@
 	const theme = localStorage.getItem('theme');
 	if (theme) {
 		for (const [k, v] of Object.entries(JSON.parse(theme))) {
-			document.documentElement.style.setProperty(`--${k}`, v.toString());
+			document.documentElement.style.setProperty(`--MI_THEME-${k}`, v.toString());
 
 			// HTMLの theme-color 適用
 			if (k === 'htmlThemeColor') {
@@ -155,12 +145,17 @@
 		document.head.appendChild(css);
 	}
 
-	function renderError(code, details) {
+	async function renderError(code, details) {
+		// Cannot set property 'innerHTML' of null を回避
+		if (document.readyState === 'loading') {
+			await new Promise(resolve => window.addEventListener('DOMContentLoaded', resolve));
+		}
+
 		let errorsElement = document.getElementById('errors');
 
 		if (!errorsElement) {
 			document.body.innerHTML = `
-			<svg class="icon-warning" xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-alert-triangle" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
+			<svg class="icon-warning" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
 				<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
 				<path d="M12 9v2m0 4v.01"></path>
 				<path d="M5 19h14a2 2 0 0 0 1.84 -2.75l-7.1 -12.25a2 2 0 0 0 -3.5 0l-7.1 12.25a2 2 0 0 0 1.75 2.75"></path>
@@ -170,10 +165,10 @@
 				<span class="button-label-big">Reload / リロード</span>
 			</button>
 			<p><b>The following actions may solve the problem. / 以下を行うと解決する可能性があります。</b></p>
-			<p>Clear the browser cache / ブラウザのキャッシュをクリアする</p>
 			<p>Update your os and browser / ブラウザおよびOSを最新バージョンに更新する</p>
 			<p>Disable an adblocker / アドブロッカーを無効にする</p>
-	 		<p>&#40;Tor Browser&#41; Set dom.webaudio.enabled to true / dom.webaudio.enabledをtrueに設定する</p>
+			<p>Clear the browser cache / ブラウザのキャッシュをクリアする</p>
+			<p>&#40;Tor Browser&#41; Set dom.webaudio.enabled to true / dom.webaudio.enabledをtrueに設定する</p>
 			<details style="color: #86b300;">
 				<summary>Other options / その他のオプション</summary>
 				<a href="/flush">
@@ -206,7 +201,7 @@
 		<summary>
 			<code>ERROR CODE: ${code}</code>
 		</summary>
-		<code>${JSON.stringify(details)}</code>`;
+		<code>${details.toString()} ${JSON.stringify(details)}</code>`;
 		errorsElement.appendChild(detailsElement);
 		addStyle(`
 		* {
@@ -314,6 +309,6 @@
 			#errorInfo {
 				width: 50%;
 			}
-		`)
+		}`);
 	}
 })();
diff --git a/packages/backend/src/server/web/style.css b/packages/backend/src/server/web/style.css
index e4723c24fd..5d81f2bed0 100644
--- a/packages/backend/src/server/web/style.css
+++ b/packages/backend/src/server/web/style.css
@@ -5,8 +5,8 @@
  */
 
 html {
-	background-color: var(--bg);
-	color: var(--fg);
+	background-color: var(--MI_THEME-bg);
+	color: var(--MI_THEME-fg);
 }
 
 #splash {
@@ -17,7 +17,7 @@ html {
 	width: 100vw;
 	height: 100vh;
 	cursor: wait;
-	background-color: var(--bg);
+	background-color: var(--MI_THEME-bg);
 	opacity: 1;
 	transition: opacity 0.5s ease;
 }
@@ -45,8 +45,9 @@ html {
 	width: 28px;
 	height: 28px;
 	transform: translateY(70px);
-	color: var(--accent);
+	color: var(--MI_THEME-accent);
 }
+
 #splashSpinner > .spinner {
 	position: absolute;
 	top: 0;
diff --git a/packages/backend/src/server/web/style.embed.css b/packages/backend/src/server/web/style.embed.css
new file mode 100644
index 0000000000..5e8786cc4e
--- /dev/null
+++ b/packages/backend/src/server/web/style.embed.css
@@ -0,0 +1,99 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+html {
+	background-color: var(--MI_THEME-bg);
+	color: var(--MI_THEME-fg);
+}
+
+html.embed {
+	box-sizing: border-box;
+	background-color: transparent;
+	color-scheme: light dark;
+	max-width: 500px;
+}
+
+#splash {
+	position: fixed;
+	z-index: 10000;
+	top: 0;
+	left: 0;
+	width: 100vw;
+	height: 100vh;
+	cursor: wait;
+	background-color: var(--MI_THEME-bg);
+	opacity: 1;
+	transition: opacity 0.5s ease;
+}
+
+html.embed #splash {
+	box-sizing: border-box;
+	min-height: 300px;
+	border-radius: var(--radius, 12px);
+	border: 1px solid var(--MI_THEME-divider, #e8e8e8);
+}
+
+html.embed.norounded #splash {
+	border-radius: 0;
+}
+
+html.embed.noborder #splash {
+	border: none;
+}
+
+#splashIcon {
+	position: absolute;
+	top: 0;
+	right: 0;
+	bottom: 0;
+	left: 0;
+	margin: auto;
+	width: 64px;
+	height: 64px;
+	pointer-events: none;
+}
+
+#splashSpinner {
+	position: absolute;
+	top: 0;
+	right: 0;
+	bottom: 0;
+	left: 0;
+	margin: auto;
+	display: inline-block;
+	width: 28px;
+	height: 28px;
+	transform: translateY(70px);
+	color: var(--MI_THEME-accent);
+}
+
+#splashSpinner > .spinner {
+	position: absolute;
+	top: 0;
+	left: 0;
+	width: 28px;
+	height: 28px;
+	fill-rule: evenodd;
+	clip-rule: evenodd;
+	stroke-linecap: round;
+	stroke-linejoin: round;
+	stroke-miterlimit: 1.5;
+}
+#splashSpinner > .spinner.bg {
+	opacity: 0.275;
+}
+#splashSpinner > .spinner.fg {
+	animation: splashSpinner 0.5s linear infinite;
+}
+
+@keyframes splashSpinner {
+	0% {
+		transform: rotate(0deg);
+	}
+	100% {
+		transform: rotate(360deg);
+	}
+}
diff --git a/packages/backend/src/server/web/views/base-embed.pug b/packages/backend/src/server/web/views/base-embed.pug
new file mode 100644
index 0000000000..baa0909676
--- /dev/null
+++ b/packages/backend/src/server/web/views/base-embed.pug
@@ -0,0 +1,72 @@
+block vars
+
+block loadClientEntry
+	- const entry = config.frontendEmbedEntry;
+
+doctype html
+
+html(class='embed')
+
+	head
+		meta(charset='utf-8')
+		meta(name='application-name' content='Misskey')
+		meta(name='referrer' content='origin')
+		meta(name='theme-color' content= themeColor || '#86b300')
+		meta(name='theme-color-orig' content= themeColor || '#86b300')
+		meta(property='og:site_name' content= instanceName || 'Misskey')
+		meta(property='instance_url' content= instanceUrl)
+		meta(name='viewport' content='width=device-width, initial-scale=1')
+		meta(name='format-detection' content='telephone=no,date=no,address=no,email=no,url=no')
+		link(rel='icon' href= icon || '/favicon.ico')
+		link(rel='apple-touch-icon' href= appleTouchIcon || '/apple-touch-icon.png')
+		link(rel='modulepreload' href=`/embed_vite/${entry.file}`)
+
+		if !config.frontendEmbedManifestExists
+				script(type="module" src="/embed_vite/@vite/client")
+
+		if Array.isArray(entry.css)
+			each href in entry.css
+				link(rel='stylesheet' href=`/embed_vite/${href}`)
+
+		title
+			block title
+				= title || 'Misskey'
+
+		block meta
+			meta(name='robots' content='noindex')
+
+		style
+			include ../style.embed.css
+
+		script.
+			var VERSION = "#{version}";
+			var CLIENT_ENTRY = "#{entry.file}";
+
+		script(type='application/json' id='misskey_meta' data-generated-at=now)
+			!= metaJson
+
+		script(type='application/json' id='misskey_embedCtx' data-generated-at=now)
+			!= embedCtx
+
+		script
+			include ../boot.embed.js
+
+	body
+		noscript: p
+			| JavaScriptを有効にしてください
+			br
+			| Please turn on your JavaScript
+		div#splash
+			img#splashIcon(src= icon || '/static-assets/splash.png')
+			div#splashSpinner
+				<svg class="spinner bg" viewBox="0 0 152 152" xmlns="http://www.w3.org/2000/svg">
+					<g transform="matrix(1,0,0,1,12,12)">
+						<circle cx="64" cy="64" r="64" style="fill:none;stroke:currentColor;stroke-width:24px;"/>
+					</g>
+				</svg>
+				<svg class="spinner fg" viewBox="0 0 152 152" xmlns="http://www.w3.org/2000/svg">
+					<g transform="matrix(1,0,0,1,12,12)">
+						<path d="M128,64C128,28.654 99.346,0 64,0C99.346,0 128,28.654 128,64Z" style="fill:none;stroke:currentColor;stroke-width:24px;"/>
+					</g>
+				</svg>
+		block content
diff --git a/packages/backend/src/server/web/views/base.pug b/packages/backend/src/server/web/views/base.pug
index ec1325e4e9..88714b2556 100644
--- a/packages/backend/src/server/web/views/base.pug
+++ b/packages/backend/src/server/web/views/base.pug
@@ -1,7 +1,7 @@
 block vars
 
 block loadClientEntry
-	- const clientEntry = config.clientEntry;
+	- const entry = config.frontendEntry;
 
 doctype html
 
@@ -28,6 +28,7 @@ html
 		meta(property='og:site_name' content= instanceName || 'Misskey')
 		meta(property='instance_url' content= instanceUrl)
 		meta(name='viewport' content='width=device-width, initial-scale=1')
+		meta(name='format-detection' content='telephone=no,date=no,address=no,email=no,url=no')
 		link(rel='icon' href= icon || '/favicon.ico')
 		link(rel='apple-touch-icon' href= appleTouchIcon || '/apple-touch-icon.png')
 		link(rel='manifest' href='/manifest.json')
@@ -35,15 +36,13 @@ html
 		link(rel='prefetch' href=serverErrorImageUrl)
 		link(rel='prefetch' href=infoImageUrl)
 		link(rel='prefetch' href=notFoundImageUrl)
-		//- https://github.com/misskey-dev/misskey/issues/9842
-		link(rel='stylesheet' href='/assets/tabler-icons/tabler-icons.min.css?v3.3.0')
-		link(rel='modulepreload' href=`/vite/${clientEntry.file}`)
+		link(rel='modulepreload' href=`/vite/${entry.file}`)
 
-		if !config.clientManifestExists
+		if !config.frontendManifestExists
 				script(type="module" src="/vite/@vite/client")
 
-		if Array.isArray(clientEntry.css)
-			each href in clientEntry.css
+		if Array.isArray(entry.css)
+			each href in entry.css
 				link(rel='stylesheet' href=`/vite/${href}`)
 
 		title
@@ -69,7 +68,7 @@ html
 
 		script.
 			var VERSION = "#{version}";
-			var CLIENT_ENTRY = "#{clientEntry.file}";
+			var CLIENT_ENTRY = "#{entry.file}";
 
 		script(type='application/json' id='misskey_meta' data-generated-at=now)
 			!= metaJson
diff --git a/packages/backend/src/types.ts b/packages/backend/src/types.ts
index 929070d0d2..df3cfee171 100644
--- a/packages/backend/src/types.ts
+++ b/packages/backend/src/types.ts
@@ -16,6 +16,8 @@
  * followRequestAccepted - 自分の送ったフォローリクエストが承認された
  * roleAssigned - ロールが付与された
  * achievementEarned - 実績を獲得
+ * exportCompleted - エクスポートが完了
+ * login - ログイン
  * app - アプリ通知
  * test - テスト通知(サーバー側)
  */
@@ -32,6 +34,8 @@ export const notificationTypes = [
 	'followRequestAccepted',
 	'roleAssigned',
 	'achievementEarned',
+	'exportCompleted',
+	'login',
 	'app',
 	'test',
 ] as const;
@@ -51,6 +55,20 @@ export const mutedNoteReasons = ['word', 'manual', 'spam', 'other'] as const;
 export const followingVisibilities = ['public', 'followers', 'private'] as const;
 export const followersVisibilities = ['public', 'followers', 'private'] as const;
 
+/**
+ * ユーザーがエクスポートできるものの種類
+ *
+ * (主にエクスポート完了通知で使用するものであり、既存のDBの名称等と必ずしも一致しない)
+ */
+export const userExportableEntities = ['antenna', 'blocking', 'clip', 'customEmoji', 'favorite', 'following', 'muting', 'note', 'userList'] as const;
+
+/**
+ * ユーザーがインポートできるものの種類
+ *
+ * (主にインポート完了通知で使用するものであり、既存のDBの名称等と必ずしも一致しない)
+ */
+export const userImportableEntities = ['antenna', 'blocking', 'customEmoji', 'following', 'muting', 'userList'] as const;
+
 export const moderationLogTypes = [
 	'updateServerSettings',
 	'suspend',
@@ -81,6 +99,8 @@ export const moderationLogTypes = [
 	'markSensitiveDriveFile',
 	'unmarkSensitiveDriveFile',
 	'resolveAbuseReport',
+	'forwardAbuseReport',
+	'updateAbuseReportNote',
 	'createInvitation',
 	'createAd',
 	'updateAd',
@@ -90,6 +110,16 @@ export const moderationLogTypes = [
 	'deleteAvatarDecoration',
 	'unsetUserAvatar',
 	'unsetUserBanner',
+	'createSystemWebhook',
+	'updateSystemWebhook',
+	'deleteSystemWebhook',
+	'createAbuseReportNotificationRecipient',
+	'updateAbuseReportNotificationRecipient',
+	'deleteAbuseReportNotificationRecipient',
+	'deleteAccount',
+	'deletePage',
+	'deleteFlash',
+	'deleteGalleryPost',
 ] as const;
 
 export type ModerationLogPayloads = {
@@ -239,7 +269,18 @@ export type ModerationLogPayloads = {
 	resolveAbuseReport: {
 		reportId: string;
 		report: any;
-		forwarded: boolean;
+		forwarded?: boolean;
+		resolvedAs?: string | null;
+	};
+	forwardAbuseReport: {
+		reportId: string;
+		report: any;
+	};
+	updateAbuseReportNote: {
+		reportId: string;
+		report: any;
+		before: string;
+		after: string;
 	};
 	createInvitation: {
 		invitations: any[];
@@ -282,6 +323,55 @@ export type ModerationLogPayloads = {
 		userHost: string | null;
 		fileId: string;
 	};
+	createSystemWebhook: {
+		systemWebhookId: string;
+		webhook: any;
+	};
+	updateSystemWebhook: {
+		systemWebhookId: string;
+		before: any;
+		after: any;
+	};
+	deleteSystemWebhook: {
+		systemWebhookId: string;
+		webhook: any;
+	};
+	createAbuseReportNotificationRecipient: {
+		recipientId: string;
+		recipient: any;
+	};
+	updateAbuseReportNotificationRecipient: {
+		recipientId: string;
+		before: any;
+		after: any;
+	};
+	deleteAbuseReportNotificationRecipient: {
+		recipientId: string;
+		recipient: any;
+	};
+	deleteAccount: {
+		userId: string;
+		userUsername: string;
+		userHost: string | null;
+	};
+	deletePage: {
+		pageId: string;
+		pageUserId: string;
+		pageUserUsername: string;
+		page: any;
+	};
+	deleteFlash: {
+		flashId: string;
+		flashUserId: string;
+		flashUserUsername: string;
+		flash: any;
+	};
+	deleteGalleryPost: {
+		postId: string;
+		postUserId: string;
+		postUserUsername: string;
+		post: any;
+	};
 };
 
 export type Serialized<T> = {
diff --git a/packages/backend/test-federation/.config/example.conf b/packages/backend/test-federation/.config/example.conf
new file mode 100644
index 0000000000..83d04eb39d
--- /dev/null
+++ b/packages/backend/test-federation/.config/example.conf
@@ -0,0 +1,70 @@
+# based on https://github.com/misskey-dev/misskey-hub/blob/7071f63a1c80ee35c71f0fd8a6d8722c118c7574/src/docs/admin/nginx.md
+
+# For WebSocket
+map $http_upgrade $connection_upgrade {
+	default upgrade;
+	'' close;
+}
+
+proxy_cache_path /tmp/nginx_cache levels=1:2 keys_zone=cache1:16m max_size=1g inactive=720m use_temp_path=off;
+
+server {
+	listen 80;
+	listen [::]:80;
+	server_name ${HOST};
+
+	# For SSL domain validation
+	root /var/www/html;
+	location /.well-known/acme-challenge/ { allow all; }
+	location /.well-known/pki-validation/ { allow all; }
+	location / { return 301 https://$server_name$request_uri; }
+}
+
+server {
+	listen 443 ssl;
+	listen [::]:443 ssl;
+	http2 on;
+	server_name ${HOST};
+
+	ssl_session_timeout 1d;
+	ssl_session_cache shared:ssl_session_cache:10m;
+	ssl_session_tickets off;
+
+	ssl_trusted_certificate /etc/nginx/certificates/rootCA.crt;
+	ssl_certificate /etc/nginx/certificates/$server_name.crt;
+	ssl_certificate_key /etc/nginx/certificates/$server_name.key;
+
+	# SSL protocol settings
+	ssl_protocols TLSv1.2 TLSv1.3;
+	ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
+	ssl_prefer_server_ciphers off;
+	ssl_stapling on;
+	ssl_stapling_verify on;
+
+	# Change to your upload limit
+	client_max_body_size 80m;
+
+	# Proxy to Node
+	location / {
+		proxy_pass http://misskey.${HOST}:3000;
+		proxy_set_header Host $host;
+		proxy_http_version 1.1;
+		proxy_redirect off;
+
+		# If it's behind another reverse proxy or CDN, remove the following.
+		proxy_set_header X-Real-IP $remote_addr;
+		proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+		proxy_set_header X-Forwarded-Proto https;
+
+		# For WebSocket
+		proxy_set_header Upgrade $http_upgrade;
+		proxy_set_header Connection $connection_upgrade;
+
+		# Cache settings
+		proxy_cache cache1;
+		proxy_cache_lock on;
+		proxy_cache_use_stale updating;
+		proxy_force_ranges on;
+		add_header X-Cache $upstream_cache_status;
+	}
+}
diff --git a/packages/backend/test-federation/.config/example.default.yml b/packages/backend/test-federation/.config/example.default.yml
new file mode 100644
index 0000000000..ff1760a5a6
--- /dev/null
+++ b/packages/backend/test-federation/.config/example.default.yml
@@ -0,0 +1,25 @@
+url: https://${HOST}/
+port: 3000
+db:
+  host: db.${HOST}
+  port: 5432
+  db: misskey
+  user: postgres
+  pass: postgres
+dbReplications: false
+redis:
+  host: redis.test
+  port: 6379
+id: 'aidx'
+proxyBypassHosts:
+  - api.deepl.com
+  - api-free.deepl.com
+  - www.recaptcha.net
+  - hcaptcha.com
+  - challenges.cloudflare.com
+proxyRemoteFiles: true
+signToActivityPubGet: true
+allowedPrivateNetworks: [
+  '127.0.0.1/32',
+  '172.20.0.0/16'
+]
diff --git a/packages/backend/test-federation/.config/example.docker.env b/packages/backend/test-federation/.config/example.docker.env
new file mode 100644
index 0000000000..a8af7cce49
--- /dev/null
+++ b/packages/backend/test-federation/.config/example.docker.env
@@ -0,0 +1,5 @@
+NODE_EXTRA_CA_CERTS=/usr/local/share/ca-certificates/rootCA.crt
+POSTGRES_DB=misskey
+POSTGRES_USER=postgres
+POSTGRES_PASSWORD=postgres
+MK_VERBOSE=true
diff --git a/packages/backend/test-federation/.gitignore b/packages/backend/test-federation/.gitignore
new file mode 100644
index 0000000000..e00f952cb5
--- /dev/null
+++ b/packages/backend/test-federation/.gitignore
@@ -0,0 +1,6 @@
+certificates
+volumes
+.env
+docker.env
+*.test.conf
+*.test.default.yml
diff --git a/packages/backend/test-federation/README.md b/packages/backend/test-federation/README.md
new file mode 100644
index 0000000000..967d51f085
--- /dev/null
+++ b/packages/backend/test-federation/README.md
@@ -0,0 +1,24 @@
+## test-federation
+Test federation between two Misskey servers: `a.test` and `b.test`.
+
+Before testing, you need to build the entire project, and change working directory to here:
+```sh
+pnpm build
+cd packages/backend/test-federation
+```
+
+First, you need to start servers by executing following commands:
+```sh
+bash ./setup.sh
+docker compose up --scale tester=0
+```
+
+Then you can run all tests by a following command:
+```sh
+docker compose run --no-deps --rm tester
+```
+
+For testing a specific file, run a following command:
+```sh
+docker compose run --no-deps --rm tester -- pnpm -F backend test:fed packages/backend/test-federation/test/user.test.ts
+```
diff --git a/packages/backend/test-federation/compose.a.yml b/packages/backend/test-federation/compose.a.yml
new file mode 100644
index 0000000000..6a305b404c
--- /dev/null
+++ b/packages/backend/test-federation/compose.a.yml
@@ -0,0 +1,64 @@
+services:
+  a.test:
+    extends:
+      file: ./compose.tpl.yml
+      service: nginx
+    depends_on:
+      misskey.a.test:
+        condition: service_healthy
+    networks:
+      - internal_network_a
+    volumes:
+      - type: bind
+        source: ./.config/a.test.conf
+        target: /etc/nginx/conf.d/a.test.conf
+        read_only: true
+      - type: bind
+        source: ./certificates/a.test.crt
+        target: /etc/nginx/certificates/a.test.crt
+        read_only: true
+      - type: bind
+        source: ./certificates/a.test.key
+        target: /etc/nginx/certificates/a.test.key
+        read_only: true
+
+  misskey.a.test:
+    extends:
+      file: ./compose.tpl.yml
+      service: misskey
+    depends_on:
+      db.a.test:
+        condition: service_healthy
+      redis.test:
+        condition: service_healthy
+      setup:
+        condition: service_completed_successfully
+    networks:
+      - internal_network_a
+    volumes:
+      - type: bind
+        source: ./.config/a.test.default.yml
+        target: /misskey/.config/default.yml
+        read_only: true
+
+  db.a.test:
+    extends:
+      file: ./compose.tpl.yml
+      service: db
+    networks:
+      - internal_network_a
+    volumes:
+      - type: bind
+        source: ./volumes/db.a
+        target: /var/lib/postgresql/data
+        bind:
+          create_host_path: true
+
+networks:
+  internal_network_a:
+    internal: true
+    driver: bridge
+    ipam:
+      config:
+        - subnet: 172.21.0.0/16
+          ip_range: 172.21.0.0/24
diff --git a/packages/backend/test-federation/compose.b.yml b/packages/backend/test-federation/compose.b.yml
new file mode 100644
index 0000000000..1158b53bae
--- /dev/null
+++ b/packages/backend/test-federation/compose.b.yml
@@ -0,0 +1,64 @@
+services:
+  b.test:
+    extends:
+      file: ./compose.tpl.yml
+      service: nginx
+    depends_on:
+      misskey.b.test:
+        condition: service_healthy
+    networks:
+      - internal_network_b
+    volumes:
+      - type: bind
+        source: ./.config/b.test.conf
+        target: /etc/nginx/conf.d/b.test.conf
+        read_only: true
+      - type: bind
+        source: ./certificates/b.test.crt
+        target: /etc/nginx/certificates/b.test.crt
+        read_only: true
+      - type: bind
+        source: ./certificates/b.test.key
+        target: /etc/nginx/certificates/b.test.key
+        read_only: true
+
+  misskey.b.test:
+    extends:
+      file: ./compose.tpl.yml
+      service: misskey
+    depends_on:
+      db.b.test:
+        condition: service_healthy
+      redis.test:
+        condition: service_healthy
+      setup:
+        condition: service_completed_successfully
+    networks:
+      - internal_network_b
+    volumes:
+      - type: bind
+        source: ./.config/b.test.default.yml
+        target: /misskey/.config/default.yml
+        read_only: true
+
+  db.b.test:
+    extends:
+      file: ./compose.tpl.yml
+      service: db
+    networks:
+      - internal_network_b
+    volumes:
+      - type: bind
+        source: ./volumes/db.b
+        target: /var/lib/postgresql/data
+        bind:
+          create_host_path: true
+
+networks:
+  internal_network_b:
+    internal: true
+    driver: bridge
+    ipam:
+      config:
+        - subnet: 172.22.0.0/16
+          ip_range: 172.22.0.0/24
diff --git a/packages/backend/test-federation/compose.override.yaml b/packages/backend/test-federation/compose.override.yaml
new file mode 100644
index 0000000000..60a7631ab5
--- /dev/null
+++ b/packages/backend/test-federation/compose.override.yaml
@@ -0,0 +1,117 @@
+services:
+  setup:
+    volumes:
+      - type: volume
+        source: node_modules
+        target: /misskey/node_modules
+      - type: volume
+        source: node_modules_backend
+        target: /misskey/packages/backend/node_modules
+      - type: volume
+        source: node_modules_misskey-js
+        target: /misskey/packages/misskey-js/node_modules
+      - type: volume
+        source: node_modules_misskey-reversi
+        target: /misskey/packages/misskey-reversi/node_modules
+
+  tester:
+    networks:
+      external_network:
+      internal_network:
+        ipv4_address: 172.20.1.1
+    volumes:
+      - type: volume
+        source: node_modules_dev
+        target: /misskey/node_modules
+      - type: volume
+        source: node_modules_backend_dev
+        target: /misskey/packages/backend/node_modules
+      - type: volume
+        source: node_modules_misskey-js_dev
+        target: /misskey/packages/misskey-js/node_modules
+
+  daemon:
+    networks:
+      - external_network
+      - internal_network_a
+      - internal_network_b
+    volumes:
+      - type: volume
+        source: node_modules_dev
+        target: /misskey/node_modules
+      - type: volume
+        source: node_modules_backend_dev
+        target: /misskey/packages/backend/node_modules
+
+  redis.test:
+    networks:
+      - internal_network_a
+      - internal_network_b
+
+  a.test:
+    networks:
+      - internal_network
+
+  misskey.a.test:
+    networks:
+      - external_network
+      - internal_network
+    volumes:
+      - type: volume
+        source: node_modules
+        target: /misskey/node_modules
+      - type: volume
+        source: node_modules_backend
+        target: /misskey/packages/backend/node_modules
+      - type: volume
+        source: node_modules_misskey-js
+        target: /misskey/packages/misskey-js/node_modules
+      - type: volume
+        source: node_modules_misskey-reversi
+        target: /misskey/packages/misskey-reversi/node_modules
+
+  b.test:
+    networks:
+      - internal_network
+
+  misskey.b.test:
+    networks:
+      - external_network
+      - internal_network
+    volumes:
+      - type: volume
+        source: node_modules
+        target: /misskey/node_modules
+      - type: volume
+        source: node_modules_backend
+        target: /misskey/packages/backend/node_modules
+      - type: volume
+        source: node_modules_misskey-js
+        target: /misskey/packages/misskey-js/node_modules
+      - type: volume
+        source: node_modules_misskey-reversi
+        target: /misskey/packages/misskey-reversi/node_modules
+
+networks:
+  external_network:
+    driver: bridge
+    ipam:
+      config:
+        - subnet: 172.23.0.0/16
+          ip_range: 172.23.0.0/24
+  internal_network:
+    internal: true
+    driver: bridge
+    ipam:
+      config:
+        - subnet: 172.20.0.0/16
+          ip_range: 172.20.0.0/24
+
+volumes:
+  node_modules:
+  node_modules_dev:
+  node_modules_backend:
+  node_modules_backend_dev:
+  node_modules_misskey-js:
+  node_modules_misskey-js_dev:
+  node_modules_misskey-reversi:
diff --git a/packages/backend/test-federation/compose.tpl.yml b/packages/backend/test-federation/compose.tpl.yml
new file mode 100644
index 0000000000..8c38f16919
--- /dev/null
+++ b/packages/backend/test-federation/compose.tpl.yml
@@ -0,0 +1,101 @@
+services:
+  nginx:
+    image: nginx:1.27
+    volumes:
+      - type: bind
+        source: ./certificates/rootCA.crt
+        target: /etc/nginx/certificates/rootCA.crt
+        read_only: true
+    healthcheck:
+      test: service nginx status
+      interval: 5s
+      retries: 20
+
+  misskey:
+    image: node:20
+    env_file:
+      - ./.config/docker.env
+    environment:
+      - NODE_ENV=production
+    volumes:
+      - type: bind
+        source: ../../../built
+        target: /misskey/built
+        read_only: true
+      - type: bind
+        source: ../assets
+        target: /misskey/packages/backend/assets
+        read_only: true
+      - type: bind
+        source: ../built
+        target: /misskey/packages/backend/built
+        read_only: true
+      - type: bind
+        source: ../migration
+        target: /misskey/packages/backend/migration
+        read_only: true
+      - type: bind
+        source: ../ormconfig.js
+        target: /misskey/packages/backend/ormconfig.js
+        read_only: true
+      - type: bind
+        source: ../package.json
+        target: /misskey/packages/backend/package.json
+        read_only: true
+      - type: bind
+        source: ../../misskey-js/built
+        target: /misskey/packages/misskey-js/built
+        read_only: true
+      - type: bind
+        source: ../../misskey-js/package.json
+        target: /misskey/packages/misskey-js/package.json
+        read_only: true
+      - type: bind
+        source: ../../misskey-reversi/built
+        target: /misskey/packages/misskey-reversi/built
+        read_only: true
+      - type: bind
+        source: ../../misskey-reversi/package.json
+        target: /misskey/packages/misskey-reversi/package.json
+        read_only: true
+      - type: bind
+        source: ../../../healthcheck.sh
+        target: /misskey/healthcheck.sh
+        read_only: true
+      - type: bind
+        source: ../../../package.json
+        target: /misskey/package.json
+        read_only: true
+      - type: bind
+        source: ../../../pnpm-lock.yaml
+        target: /misskey/pnpm-lock.yaml
+        read_only: true
+      - type: bind
+        source: ../../../pnpm-workspace.yaml
+        target: /misskey/pnpm-workspace.yaml
+        read_only: true
+      - type: bind
+        source: ./certificates/rootCA.crt
+        target: /usr/local/share/ca-certificates/rootCA.crt
+        read_only: true
+    working_dir: /misskey
+    command: >
+      bash -c "
+        corepack enable && corepack prepare
+        pnpm -F backend migrate
+        pnpm -F backend start
+      "
+    healthcheck:
+      test: bash /misskey/healthcheck.sh
+      interval: 5s
+      retries: 20
+
+  db:
+    image: postgres:15-alpine
+    env_file:
+      - ./.config/docker.env
+    volumes:
+    healthcheck:
+      test: pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB
+      interval: 5s
+      retries: 20
diff --git a/packages/backend/test-federation/compose.yml b/packages/backend/test-federation/compose.yml
new file mode 100644
index 0000000000..62d7e977c0
--- /dev/null
+++ b/packages/backend/test-federation/compose.yml
@@ -0,0 +1,133 @@
+include:
+  - ./compose.a.yml
+  - ./compose.b.yml
+
+services:
+  setup:
+    extends:
+      file: ./compose.tpl.yml
+      service: misskey
+    command: >
+      bash -c "
+        corepack enable && corepack prepare
+        pnpm -F backend i
+        pnpm -F misskey-js i
+        pnpm -F misskey-reversi i
+      "
+
+  tester:
+    image: node:20
+    depends_on:
+      a.test:
+        condition: service_healthy
+      b.test:
+        condition: service_healthy
+    environment:
+      - NODE_ENV=development
+      - NODE_EXTRA_CA_CERTS=/usr/local/share/ca-certificates/rootCA.crt
+    volumes:
+      - type: bind
+        source: ../package.json
+        target: /misskey/packages/backend/package.json
+        read_only: true
+      - type: bind
+        source: ../test/resources
+        target: /misskey/packages/backend/test/resources
+        read_only: true
+      - type: bind
+        source: ./test
+        target: /misskey/packages/backend/test-federation/test
+        read_only: true
+      - type: bind
+        source: ../jest.config.cjs
+        target: /misskey/packages/backend/jest.config.cjs
+        read_only: true
+      - type: bind
+        source: ../jest.config.fed.cjs
+        target: /misskey/packages/backend/jest.config.fed.cjs
+        read_only: true
+      - type: bind
+        source: ../../misskey-js/built
+        target: /misskey/packages/misskey-js/built
+        read_only: true
+      - type: bind
+        source: ../../misskey-js/package.json
+        target: /misskey/packages/misskey-js/package.json
+        read_only: true
+      - type: bind
+        source: ../../../package.json
+        target: /misskey/package.json
+        read_only: true
+      - type: bind
+        source: ../../../pnpm-lock.yaml
+        target: /misskey/pnpm-lock.yaml
+        read_only: true
+      - type: bind
+        source: ../../../pnpm-workspace.yaml
+        target: /misskey/pnpm-workspace.yaml
+        read_only: true
+      - type: bind
+        source: ./certificates/rootCA.crt
+        target: /usr/local/share/ca-certificates/rootCA.crt
+        read_only: true
+    working_dir: /misskey
+    entrypoint: >
+      bash -c '
+        corepack enable && corepack prepare
+        pnpm -F misskey-js i --frozen-lockfile
+        pnpm -F backend i --frozen-lockfile
+        exec "$0" "$@"
+      '
+    command: pnpm -F backend test:fed
+
+  daemon:
+    image: node:20
+    depends_on:
+      redis.test:
+        condition: service_healthy
+    volumes:
+      - type: bind
+        source: ../package.json
+        target: /misskey/packages/backend/package.json
+        read_only: true
+      - type: bind
+        source: ./daemon.ts
+        target: /misskey/packages/backend/test-federation/daemon.ts
+        read_only: true
+      - type: bind
+        source: ./tsconfig.json
+        target: /misskey/packages/backend/test-federation/tsconfig.json
+        read_only: true
+      - type: bind
+        source: ../../../package.json
+        target: /misskey/package.json
+        read_only: true
+      - type: bind
+        source: ../../../pnpm-lock.yaml
+        target: /misskey/pnpm-lock.yaml
+        read_only: true
+      - type: bind
+        source: ../../../pnpm-workspace.yaml
+        target: /misskey/pnpm-workspace.yaml
+        read_only: true
+    working_dir: /misskey
+    command: >
+      bash -c "
+        corepack enable && corepack prepare
+        pnpm -F backend i --frozen-lockfile
+        pnpm exec tsc -p ./packages/backend/test-federation
+        node ./packages/backend/test-federation/built/daemon.js
+      "
+
+  redis.test:
+    image: redis:7-alpine
+    volumes:
+      - type: bind
+        source: ./volumes/redis
+        target: /data
+        bind:
+          create_host_path: true
+    healthcheck:
+      test: redis-cli ping
+      interval: 5s
+      retries: 20
diff --git a/packages/backend/test-federation/daemon.ts b/packages/backend/test-federation/daemon.ts
new file mode 100644
index 0000000000..46b6963c79
--- /dev/null
+++ b/packages/backend/test-federation/daemon.ts
@@ -0,0 +1,38 @@
+import IPCIDR from 'ip-cidr';
+import { Redis } from 'ioredis';
+
+const TESTER_IP_ADDRESS = '172.20.1.1';
+
+/**
+ * This should be same as {@link file://./../src/misc/get-ip-hash.ts}.
+ */
+function getIpHash(ip: string) {
+	const prefix = IPCIDR.createAddress(ip).mask(64);
+	return `ip-${BigInt('0b' + prefix).toString(36)}`;
+}
+
+/**
+ * This prevents hitting rate limit when login.
+ */
+export async function purgeLimit(host: string, client: Redis) {
+	const ipHash = getIpHash(TESTER_IP_ADDRESS);
+	const key = `${host}:limit:${ipHash}:signin`;
+	const res = await client.zrange(key, 0, -1);
+	if (res.length !== 0) {
+		console.log(`${key} - ${JSON.stringify(res)}`);
+		await client.del(key);
+	}
+}
+
+console.log('Daemon started running');
+
+{
+	const redisClient = new Redis({
+		host: 'redis.test',
+	});
+
+	setInterval(() => {
+		purgeLimit('a.test', redisClient);
+		purgeLimit('b.test', redisClient);
+	}, 200);
+}
diff --git a/packages/backend/test-federation/eslint.config.js b/packages/backend/test-federation/eslint.config.js
new file mode 100644
index 0000000000..e3bcf4c0fe
--- /dev/null
+++ b/packages/backend/test-federation/eslint.config.js
@@ -0,0 +1,21 @@
+import globals from 'globals';
+import tsParser from '@typescript-eslint/parser';
+import sharedConfig from '../../shared/eslint.config.js';
+
+export default [
+	...sharedConfig,
+	{
+		files: ['**/*.ts', '**/*.tsx'],
+		languageOptions: {
+			globals: {
+				...globals.node,
+			},
+			parserOptions: {
+				parser: tsParser,
+				project: ['./tsconfig.json'],
+				sourceType: 'module',
+				tsconfigRootDir: import.meta.dirname,
+			},
+		},
+	},
+];
diff --git a/packages/backend/test-federation/setup.sh b/packages/backend/test-federation/setup.sh
new file mode 100644
index 0000000000..1bc3a2a87c
--- /dev/null
+++ b/packages/backend/test-federation/setup.sh
@@ -0,0 +1,35 @@
+#!/bin/bash
+mkdir certificates
+
+# rootCA
+openssl genrsa -des3 \
+  -passout pass:rootCA \
+  -out certificates/rootCA.key 4096
+openssl req -x509 -new -nodes -batch \
+  -key certificates/rootCA.key \
+  -sha256 \
+  -days 1024 \
+  -passin pass:rootCA \
+  -out certificates/rootCA.crt
+
+# domain
+function generate {
+  openssl req -new -newkey rsa:2048 -sha256 -nodes \
+    -keyout certificates/$1.key \
+    -subj "/CN=$1/emailAddress=admin@$1/C=JP/ST=/L=/O=Misskey Tester/OU=Some Unit" \
+    -out certificates/$1.csr
+  openssl x509 -req -sha256 \
+    -in certificates/$1.csr \
+    -CA certificates/rootCA.crt \
+    -CAkey certificates/rootCA.key \
+    -CAcreateserial \
+    -passin pass:rootCA \
+    -out certificates/$1.crt \
+    -days 500
+  if [ ! -f .config/docker.env ]; then cp .config/example.docker.env .config/docker.env; fi
+  if [ ! -f .config/$1.conf ]; then sed "s/\${HOST}/$1/g" .config/example.conf > .config/$1.conf; fi
+  if [ ! -f .config/$1.default.yml ]; then sed "s/\${HOST}/$1/g" .config/example.default.yml > .config/$1.default.yml; fi
+}
+
+generate a.test
+generate b.test
diff --git a/packages/backend/test-federation/test/abuse-report.test.ts b/packages/backend/test-federation/test/abuse-report.test.ts
new file mode 100644
index 0000000000..b54d6222b4
--- /dev/null
+++ b/packages/backend/test-federation/test/abuse-report.test.ts
@@ -0,0 +1,52 @@
+import { rejects, strictEqual } from 'node:assert';
+import * as Misskey from 'misskey-js';
+import { createAccount, createModerator, resolveRemoteUser, sleep, type LoginUser } from './utils.js';
+
+describe('Abuse report', () => {
+	describe('Forwarding report', () => {
+		let alice: LoginUser, bob: LoginUser, aModerator: LoginUser, bModerator: LoginUser;
+		let bobInA: Misskey.entities.UserDetailedNotMe, aliceInB: Misskey.entities.UserDetailedNotMe;
+
+		beforeAll(async () => {
+			[alice, bob] = await Promise.all([
+				createAccount('a.test'),
+				createAccount('b.test'),
+			]);
+
+			[aModerator, bModerator] = await Promise.all([
+				createModerator('a.test'),
+				createModerator('b.test'),
+			]);
+
+			[bobInA, aliceInB] = await Promise.all([
+				resolveRemoteUser('b.test', bob.id, alice),
+				resolveRemoteUser('a.test', alice.id, bob),
+			]);
+		});
+
+		test('Alice reports Bob, moderator in A forwards it, and B moderator receives it', async () => {
+			const comment = crypto.randomUUID();
+			await alice.client.request('users/report-abuse', { userId: bobInA.id, comment });
+			const reports = await aModerator.client.request('admin/abuse-user-reports', {});
+			const report = reports.filter(report => report.comment === comment)[0];
+			await aModerator.client.request('admin/forward-abuse-user-report', { reportId: report.id });
+			await sleep();
+
+			const reportsInB = await bModerator.client.request('admin/abuse-user-reports', {});
+			const reportInB = reportsInB.filter(report => report.comment.includes(comment))[0];
+			// NOTE: reporter is not Alice, and is not moderator in A
+			strictEqual(reportInB.reporter.url, 'https://a.test/@instance.actor');
+			strictEqual(reportInB.targetUserId, bob.id);
+
+			// NOTE: cannot forward multiple times
+			await rejects(
+				async () => await aModerator.client.request('admin/forward-abuse-user-report', { reportId: report.id }),
+				(err: any) => {
+					strictEqual(err.code, 'INTERNAL_ERROR');
+					strictEqual(err.info.e.message, 'The report has already been forwarded.');
+					return true;
+				},
+			);
+		});
+	});
+});
diff --git a/packages/backend/test-federation/test/block.test.ts b/packages/backend/test-federation/test/block.test.ts
new file mode 100644
index 0000000000..ef910eeaea
--- /dev/null
+++ b/packages/backend/test-federation/test/block.test.ts
@@ -0,0 +1,224 @@
+import { deepStrictEqual, rejects, strictEqual } from 'node:assert';
+import * as Misskey from 'misskey-js';
+import { assertNotificationReceived, createAccount, type LoginUser, resolveRemoteNote, resolveRemoteUser, sleep } from './utils.js';
+
+describe('Block', () => {
+	describe('Check follow', () => {
+		let alice: LoginUser, bob: LoginUser;
+		let bobInA: Misskey.entities.UserDetailedNotMe, aliceInB: Misskey.entities.UserDetailedNotMe;
+
+		beforeAll(async () => {
+			[alice, bob] = await Promise.all([
+				createAccount('a.test'),
+				createAccount('b.test'),
+			]);
+
+			[bobInA, aliceInB] = await Promise.all([
+				resolveRemoteUser('b.test', bob.id, alice),
+				resolveRemoteUser('a.test', alice.id, bob),
+			]);
+		});
+
+		test('Cannot follow if blocked', async () => {
+			await alice.client.request('blocking/create', { userId: bobInA.id });
+			await sleep();
+			await rejects(
+				async () => await bob.client.request('following/create', { userId: aliceInB.id }),
+				(err: any) => {
+					strictEqual(err.code, 'BLOCKED');
+					return true;
+				},
+			);
+
+			const following = await bob.client.request('users/following', { userId: bob.id });
+			strictEqual(following.length, 0);
+			const followers = await alice.client.request('users/followers', { userId: alice.id });
+			strictEqual(followers.length, 0);
+		});
+
+		// FIXME: this is invalid case
+		test('Cannot follow even if unblocked', async () => {
+			// unblock here
+			await alice.client.request('blocking/delete', { userId: bobInA.id });
+			await sleep();
+
+			// TODO: why still being blocked?
+			await rejects(
+				async () => await bob.client.request('following/create', { userId: aliceInB.id }),
+				(err: any) => {
+					strictEqual(err.code, 'BLOCKED');
+					return true;
+				},
+			);
+		});
+
+		test.skip('Can follow if unblocked', async () => {
+			await alice.client.request('blocking/delete', { userId: bobInA.id });
+			await sleep();
+
+			await bob.client.request('following/create', { userId: aliceInB.id });
+			await sleep();
+
+			const following = await bob.client.request('users/following', { userId: bob.id });
+			strictEqual(following.length, 1);
+			const followers = await alice.client.request('users/followers', { userId: alice.id });
+			strictEqual(followers.length, 1);
+		});
+
+		test.skip('Remove follower when block them', async () => {
+			test('before block', async () => {
+				const following = await bob.client.request('users/following', { userId: bob.id });
+				strictEqual(following.length, 1);
+				const followers = await alice.client.request('users/followers', { userId: alice.id });
+				strictEqual(followers.length, 1);
+			});
+
+			await alice.client.request('blocking/create', { userId: bobInA.id });
+			await sleep();
+
+			test('after block', async () => {
+				const following = await bob.client.request('users/following', { userId: bob.id });
+				strictEqual(following.length, 0);
+				const followers = await alice.client.request('users/followers', { userId: alice.id });
+				strictEqual(followers.length, 0);
+			});
+		});
+	});
+
+	describe('Check reply', () => {
+		let alice: LoginUser, bob: LoginUser;
+		let bobInA: Misskey.entities.UserDetailedNotMe, aliceInB: Misskey.entities.UserDetailedNotMe;
+
+		beforeAll(async () => {
+			[alice, bob] = await Promise.all([
+				createAccount('a.test'),
+				createAccount('b.test'),
+			]);
+
+			[bobInA, aliceInB] = await Promise.all([
+				resolveRemoteUser('b.test', bob.id, alice),
+				resolveRemoteUser('a.test', alice.id, bob),
+			]);
+		});
+
+		test('Cannot reply if blocked', async () => {
+			await alice.client.request('blocking/create', { userId: bobInA.id });
+			await sleep();
+
+			const note = (await alice.client.request('notes/create', { text: 'a' })).createdNote;
+			const resolvedNote = await resolveRemoteNote('a.test', note.id, bob);
+			await rejects(
+				async () => await bob.client.request('notes/create', { text: 'b', replyId: resolvedNote.id }),
+				(err: any) => {
+					strictEqual(err.code, 'YOU_HAVE_BEEN_BLOCKED');
+					return true;
+				},
+			);
+		});
+
+		test('Can reply if unblocked', async () => {
+			await alice.client.request('blocking/delete', { userId: bobInA.id });
+			await sleep();
+
+			const note = (await alice.client.request('notes/create', { text: 'a' })).createdNote;
+			const resolvedNote = await resolveRemoteNote('a.test', note.id, bob);
+			const reply = (await bob.client.request('notes/create', { text: 'b', replyId: resolvedNote.id })).createdNote;
+
+			await resolveRemoteNote('b.test', reply.id, alice);
+		});
+	});
+
+	describe('Check reaction', () => {
+		let alice: LoginUser, bob: LoginUser;
+		let bobInA: Misskey.entities.UserDetailedNotMe, aliceInB: Misskey.entities.UserDetailedNotMe;
+
+		beforeAll(async () => {
+			[alice, bob] = await Promise.all([
+				createAccount('a.test'),
+				createAccount('b.test'),
+			]);
+
+			[bobInA, aliceInB] = await Promise.all([
+				resolveRemoteUser('b.test', bob.id, alice),
+				resolveRemoteUser('a.test', alice.id, bob),
+			]);
+		});
+
+		test('Cannot reaction if blocked', async () => {
+			await alice.client.request('blocking/create', { userId: bobInA.id });
+			await sleep();
+
+			const note = (await alice.client.request('notes/create', { text: 'a' })).createdNote;
+			const resolvedNote = await resolveRemoteNote('a.test', note.id, bob);
+			await rejects(
+				async () => await bob.client.request('notes/reactions/create', { noteId: resolvedNote.id, reaction: '😅' }),
+				(err: any) => {
+					strictEqual(err.code, 'YOU_HAVE_BEEN_BLOCKED');
+					return true;
+				},
+			);
+		});
+
+		// FIXME: this is invalid case
+		test('Cannot reaction even if unblocked', async () => {
+			// unblock here
+			await alice.client.request('blocking/delete', { userId: bobInA.id });
+			await sleep();
+
+			const note = (await alice.client.request('notes/create', { text: 'a' })).createdNote;
+			const resolvedNote = await resolveRemoteNote('a.test', note.id, bob);
+
+			// TODO: why still being blocked?
+			await rejects(
+				async () => await bob.client.request('notes/reactions/create', { noteId: resolvedNote.id, reaction: '😅' }),
+				(err: any) => {
+					strictEqual(err.code, 'YOU_HAVE_BEEN_BLOCKED');
+					return true;
+				},
+			);
+		});
+
+		test.skip('Can reaction if unblocked', async () => {
+			await alice.client.request('blocking/delete', { userId: bobInA.id });
+			await sleep();
+
+			const note = (await alice.client.request('notes/create', { text: 'a' })).createdNote;
+			const resolvedNote = await resolveRemoteNote('a.test', note.id, bob);
+			await bob.client.request('notes/reactions/create', { noteId: resolvedNote.id, reaction: '😅' });
+
+			const _note = await alice.client.request('notes/show', { noteId: note.id });
+			deepStrictEqual(_note.reactions, { '😅': 1 });
+		});
+	});
+
+	describe('Check mention', () => {
+		let alice: LoginUser, bob: LoginUser;
+		let bobInA: Misskey.entities.UserDetailedNotMe, aliceInB: Misskey.entities.UserDetailedNotMe;
+
+		beforeAll(async () => {
+			[alice, bob] = await Promise.all([
+				createAccount('a.test'),
+				createAccount('b.test'),
+			]);
+
+			[bobInA, aliceInB] = await Promise.all([
+				resolveRemoteUser('b.test', bob.id, alice),
+				resolveRemoteUser('a.test', alice.id, bob),
+			]);
+		});
+
+		/** NOTE: You should mute the target to stop receiving notifications */
+		test('Can mention and notified even if blocked', async () => {
+			await alice.client.request('blocking/create', { userId: bobInA.id });
+			await sleep();
+
+			const text = `@${alice.username}@a.test plz unblock me!`;
+			await assertNotificationReceived(
+				'a.test', alice,
+				async () => await bob.client.request('notes/create', { text }),
+				notification => notification.type === 'mention' && notification.userId === bobInA.id && notification.note.text === text,
+				true,
+			);
+		});
+	});
+});
diff --git a/packages/backend/test-federation/test/drive.test.ts b/packages/backend/test-federation/test/drive.test.ts
new file mode 100644
index 0000000000..f755183b4d
--- /dev/null
+++ b/packages/backend/test-federation/test/drive.test.ts
@@ -0,0 +1,175 @@
+import assert, { strictEqual } from 'node:assert';
+import * as Misskey from 'misskey-js';
+import { createAccount, deepStrictEqualWithExcludedFields, fetchAdmin, type LoginUser, resolveRemoteNote, resolveRemoteUser, sleep, uploadFile } from './utils.js';
+
+const bAdmin = await fetchAdmin('b.test');
+
+describe('Drive', () => {
+	describe('Upload image in a.test and resolve from b.test', () => {
+		let uploader: LoginUser;
+
+		beforeAll(async () => {
+			uploader = await createAccount('a.test');
+		});
+
+		let image: Misskey.entities.DriveFile, imageInB: Misskey.entities.DriveFile;
+
+		describe('Upload', () => {
+			beforeAll(async () => {
+				image = await uploadFile('a.test', uploader);
+				const noteWithImage = (await uploader.client.request('notes/create', { fileIds: [image.id] })).createdNote;
+				const noteInB = await resolveRemoteNote('a.test', noteWithImage.id, bAdmin);
+				assert(noteInB.files != null);
+				strictEqual(noteInB.files.length, 1);
+				imageInB = noteInB.files[0];
+			});
+
+			test('Check consistency of DriveFile', () => {
+				// console.log(`a.test: ${JSON.stringify(image, null, '\t')}`);
+				// console.log(`b.test: ${JSON.stringify(imageInB, null, '\t')}`);
+
+				deepStrictEqualWithExcludedFields(image, imageInB, [
+					'id',
+					'createdAt',
+					'size',
+					'url',
+					'thumbnailUrl',
+					'userId',
+				]);
+			});
+		});
+
+		let updatedImage: Misskey.entities.DriveFile, updatedImageInB: Misskey.entities.DriveFile;
+
+		describe('Update', () => {
+			beforeAll(async () => {
+				updatedImage = await uploader.client.request('drive/files/update', {
+					fileId: image.id,
+					name: 'updated_192.jpg',
+					isSensitive: true,
+				});
+
+				updatedImageInB = await bAdmin.client.request('drive/files/show', {
+					fileId: imageInB.id,
+				});
+			});
+
+			test('Check consistency', () => {
+				// console.log(`a.test: ${JSON.stringify(updatedImage, null, '\t')}`);
+				// console.log(`b.test: ${JSON.stringify(updatedImageInB, null, '\t')}`);
+
+				// FIXME: not updated with `drive/files/update`
+				strictEqual(updatedImage.isSensitive, true);
+				strictEqual(updatedImage.name, 'updated_192.jpg');
+				strictEqual(updatedImageInB.isSensitive, false);
+				strictEqual(updatedImageInB.name, '192.jpg');
+			});
+		});
+
+		let reupdatedImageInB: Misskey.entities.DriveFile;
+
+		describe('Re-update with attaching to Note', () => {
+			beforeAll(async () => {
+				const noteWithUpdatedImage = (await uploader.client.request('notes/create', { fileIds: [updatedImage.id] })).createdNote;
+				const noteWithUpdatedImageInB = await resolveRemoteNote('a.test', noteWithUpdatedImage.id, bAdmin);
+				assert(noteWithUpdatedImageInB.files != null);
+				strictEqual(noteWithUpdatedImageInB.files.length, 1);
+				reupdatedImageInB = noteWithUpdatedImageInB.files[0];
+			});
+
+			test('Check consistency', () => {
+				// console.log(`b.test: ${JSON.stringify(reupdatedImageInB, null, '\t')}`);
+
+				// `isSensitive` is updated
+				strictEqual(reupdatedImageInB.isSensitive, true);
+				// FIXME: but `name` is not updated
+				strictEqual(reupdatedImageInB.name, '192.jpg');
+			});
+		});
+	});
+
+	describe('Sensitive flag', () => {
+		describe('isSensitive is federated in delivering to followers', () => {
+			let alice: LoginUser, bob: LoginUser;
+			let bobInA: Misskey.entities.UserDetailedNotMe, aliceInB: Misskey.entities.UserDetailedNotMe;
+
+			beforeAll(async () => {
+				[alice, bob] = await Promise.all([
+					createAccount('a.test'),
+					createAccount('b.test'),
+				]);
+
+				[bobInA, aliceInB] = await Promise.all([
+					resolveRemoteUser('b.test', bob.id, alice),
+					resolveRemoteUser('a.test', alice.id, bob),
+				]);
+
+				await bob.client.request('following/create', { userId: aliceInB.id });
+				await sleep();
+			});
+
+			test('Alice uploads sensitive image and it is shown as sensitive from Bob', async () => {
+				const file = await uploadFile('a.test', alice);
+				await alice.client.request('drive/files/update', { fileId: file.id, isSensitive: true });
+				await alice.client.request('notes/create', { text: 'sensitive', fileIds: [file.id] });
+				await sleep();
+
+				const notes = await bob.client.request('notes/timeline', {});
+				strictEqual(notes.length, 1);
+				const noteInB = notes[0];
+				assert(noteInB.files != null);
+				strictEqual(noteInB.files.length, 1);
+				strictEqual(noteInB.files[0].isSensitive, true);
+			});
+		});
+
+		describe('isSensitive is federated in resolving', () => {
+			let alice: LoginUser, bob: LoginUser;
+
+			beforeAll(async () => {
+				[alice, bob] = await Promise.all([
+					createAccount('a.test'),
+					createAccount('b.test'),
+				]);
+			});
+
+			test('Alice uploads sensitive image and it is shown as sensitive from Bob', async () => {
+				const file = await uploadFile('a.test', alice);
+				await alice.client.request('drive/files/update', { fileId: file.id, isSensitive: true });
+				const note = (await alice.client.request('notes/create', { text: 'sensitive', fileIds: [file.id] })).createdNote;
+
+				const noteInB = await resolveRemoteNote('a.test', note.id, bob);
+				assert(noteInB.files != null);
+				strictEqual(noteInB.files.length, 1);
+				strictEqual(noteInB.files[0].isSensitive, true);
+			});
+		});
+
+		/** @see https://github.com/misskey-dev/misskey/issues/12208 */
+		describe('isSensitive is federated in replying', () => {
+			let alice: LoginUser, bob: LoginUser;
+
+			beforeAll(async () => {
+				[alice, bob] = await Promise.all([
+					createAccount('a.test'),
+					createAccount('b.test'),
+				]);
+			});
+
+			test('Alice uploads sensitive image and it is shown as sensitive from Bob', async () => {
+				const bobNote = (await bob.client.request('notes/create', { text: 'I\'m Bob' })).createdNote;
+
+				const file = await uploadFile('a.test', alice);
+				await alice.client.request('drive/files/update', { fileId: file.id, isSensitive: true });
+				const bobNoteInA = await resolveRemoteNote('b.test', bobNote.id, alice);
+				const note = (await alice.client.request('notes/create', { text: 'sensitive', fileIds: [file.id], replyId: bobNoteInA.id })).createdNote;
+				await sleep();
+
+				const noteInB = await resolveRemoteNote('a.test', note.id, bob);
+				assert(noteInB.files != null);
+				strictEqual(noteInB.files.length, 1);
+				strictEqual(noteInB.files[0].isSensitive, true);
+			});
+		});
+	});
+});
diff --git a/packages/backend/test-federation/test/emoji.test.ts b/packages/backend/test-federation/test/emoji.test.ts
new file mode 100644
index 0000000000..3119ca6e4d
--- /dev/null
+++ b/packages/backend/test-federation/test/emoji.test.ts
@@ -0,0 +1,97 @@
+import assert, { deepStrictEqual, strictEqual } from 'assert';
+import * as Misskey from 'misskey-js';
+import { addCustomEmoji, createAccount, type LoginUser, resolveRemoteUser, sleep } from './utils.js';
+
+describe('Emoji', () => {
+	let alice: LoginUser, bob: LoginUser;
+	let bobInA: Misskey.entities.UserDetailedNotMe, aliceInB: Misskey.entities.UserDetailedNotMe;
+
+	beforeAll(async () => {
+		[alice, bob] = await Promise.all([
+			createAccount('a.test'),
+			createAccount('b.test'),
+		]);
+
+		[bobInA, aliceInB] = await Promise.all([
+			resolveRemoteUser('b.test', bob.id, alice),
+			resolveRemoteUser('a.test', alice.id, bob),
+		]);
+
+		await bob.client.request('following/create', { userId: aliceInB.id });
+		await sleep();
+	});
+
+	test('Custom emoji are delivered with Note delivery', async () => {
+		const emoji = await addCustomEmoji('a.test');
+		await alice.client.request('notes/create', { text: `I love :${emoji.name}:` });
+		await sleep();
+
+		const notes = await bob.client.request('notes/timeline', {});
+		const noteInB = notes[0];
+
+		strictEqual(noteInB.text, `I love \u200b:${emoji.name}:\u200b`);
+		assert(noteInB.emojis != null);
+		assert(emoji.name in noteInB.emojis);
+		strictEqual(noteInB.emojis[emoji.name], emoji.url);
+	});
+
+	test('Custom emoji are delivered with Reaction delivery', async () => {
+		const emoji = await addCustomEmoji('a.test');
+		const note = (await alice.client.request('notes/create', { text: 'a' })).createdNote;
+		await sleep();
+
+		await alice.client.request('notes/reactions/create', { noteId: note.id, reaction: `:${emoji.name}:` });
+		await sleep();
+
+		const noteInB = (await bob.client.request('notes/timeline', {}))[0];
+		deepStrictEqual(noteInB.reactions[`:${emoji.name}@a.test:`], 1);
+		deepStrictEqual(noteInB.reactionEmojis[`${emoji.name}@a.test`], emoji.url);
+	});
+
+	test('Custom emoji are delivered with Profile delivery', async () => {
+		const emoji = await addCustomEmoji('a.test');
+		const renewedAlice = await alice.client.request('i/update', { name: `:${emoji.name}:` });
+		await sleep();
+
+		const renewedaliceInB = await bob.client.request('users/show', { userId: aliceInB.id });
+		strictEqual(renewedaliceInB.name, renewedAlice.name);
+		assert(emoji.name in renewedaliceInB.emojis);
+		strictEqual(renewedaliceInB.emojis[emoji.name], emoji.url);
+	});
+
+	test('Local-only custom emoji aren\'t delivered with Note delivery', async () => {
+		const emoji = await addCustomEmoji('a.test', { localOnly: true });
+		await alice.client.request('notes/create', { text: `I love :${emoji.name}:` });
+		await sleep();
+
+		const notes = await bob.client.request('notes/timeline', {});
+		const noteInB = notes[0];
+
+		strictEqual(noteInB.text, `I love \u200b:${emoji.name}:\u200b`);
+		// deepStrictEqual(noteInB.emojis, {}); // TODO: this fails (why?)
+		deepStrictEqual({ ...noteInB.emojis }, {});
+	});
+
+	test('Local-only custom emoji aren\'t delivered with Reaction delivery', async () => {
+		const emoji = await addCustomEmoji('a.test', { localOnly: true });
+		const note = (await alice.client.request('notes/create', { text: 'a' })).createdNote;
+		await sleep();
+
+		await alice.client.request('notes/reactions/create', { noteId: note.id, reaction: `:${emoji.name}:` });
+		await sleep();
+
+		const noteInB = (await bob.client.request('notes/timeline', {}))[0];
+		deepStrictEqual({ ...noteInB.reactions }, { '❤': 1 });
+		deepStrictEqual({ ...noteInB.reactionEmojis }, {});
+	});
+
+	test('Local-only custom emoji aren\'t delivered with Profile delivery', async () => {
+		const emoji = await addCustomEmoji('a.test', { localOnly: true });
+		const renewedAlice = await alice.client.request('i/update', { name: `:${emoji.name}:` });
+		await sleep();
+
+		const renewedaliceInB = await bob.client.request('users/show', { userId: aliceInB.id });
+		strictEqual(renewedaliceInB.name, renewedAlice.name);
+		deepStrictEqual({ ...renewedaliceInB.emojis }, {});
+	});
+});
diff --git a/packages/backend/test-federation/test/move.test.ts b/packages/backend/test-federation/test/move.test.ts
new file mode 100644
index 0000000000..56a57de8a4
--- /dev/null
+++ b/packages/backend/test-federation/test/move.test.ts
@@ -0,0 +1,52 @@
+import assert, { strictEqual } from 'node:assert';
+import { createAccount, type LoginUser, sleep } from './utils.js';
+
+describe('Move', () => {
+	test('Minimum move', async () => {
+		const [alice, bob] = await Promise.all([
+			createAccount('a.test'),
+			createAccount('b.test'),
+		]);
+
+		await bob.client.request('i/update', { alsoKnownAs: [`@${alice.username}@a.test`] });
+		await alice.client.request('i/move', { moveToAccount: `@${bob.username}@b.test` });
+	});
+
+	/** @see https://github.com/misskey-dev/misskey/issues/11320 */
+	describe('Following relation is transferred after move', () => {
+		let alice: LoginUser, bob: LoginUser, carol: LoginUser;
+
+		beforeAll(async () => {
+			[alice, bob] = await Promise.all([
+				createAccount('a.test'),
+				createAccount('b.test'),
+			]);
+			carol = await createAccount('a.test');
+
+			// Follow @carol@a.test ==> @alice@a.test
+			await carol.client.request('following/create', { userId: alice.id });
+
+			// Move @alice@a.test ==> @bob@b.test
+			await bob.client.request('i/update', { alsoKnownAs: [`@${alice.username}@a.test`] });
+			await alice.client.request('i/move', { moveToAccount: `@${bob.username}@b.test` });
+			await sleep();
+		});
+
+		test('Check from follower', async () => {
+			const following = await carol.client.request('users/following', { userId: carol.id });
+			strictEqual(following.length, 2);
+			const followees = following.map(({ followee }) => followee);
+			assert(followees.every(followee => followee != null));
+			assert(followees.some(({ id, url }) => id === alice.id && url === null));
+			assert(followees.some(({ url }) => url === `https://b.test/@${bob.username}`));
+		});
+
+		test('Check from followee', async () => {
+			const followers = await bob.client.request('users/followers', { userId: bob.id });
+			strictEqual(followers.length, 1);
+			const follower = followers[0].follower;
+			assert(follower != null);
+			strictEqual(follower.url, `https://a.test/@${carol.username}`);
+		});
+	});
+});
diff --git a/packages/backend/test-federation/test/note.test.ts b/packages/backend/test-federation/test/note.test.ts
new file mode 100644
index 0000000000..bacc4cc54f
--- /dev/null
+++ b/packages/backend/test-federation/test/note.test.ts
@@ -0,0 +1,317 @@
+import assert, { rejects, strictEqual } from 'node:assert';
+import * as Misskey from 'misskey-js';
+import { addCustomEmoji, createAccount, createModerator, deepStrictEqualWithExcludedFields, type LoginUser, resolveRemoteNote, resolveRemoteUser, sleep, uploadFile } from './utils.js';
+
+describe('Note', () => {
+	let alice: LoginUser, bob: LoginUser;
+	let bobInA: Misskey.entities.UserDetailedNotMe, aliceInB: Misskey.entities.UserDetailedNotMe;
+
+	beforeAll(async () => {
+		[alice, bob] = await Promise.all([
+			createAccount('a.test'),
+			createAccount('b.test'),
+		]);
+
+		[bobInA, aliceInB] = await Promise.all([
+			resolveRemoteUser('b.test', bob.id, alice),
+			resolveRemoteUser('a.test', alice.id, bob),
+		]);
+	});
+
+	describe('Note content', () => {
+		test('Consistency of Public Note', async () => {
+			const image = await uploadFile('a.test', alice);
+			const note = (await alice.client.request('notes/create', {
+				text: 'I am Alice!',
+				fileIds: [image.id],
+				poll: {
+					choices: ['neko', 'inu'],
+					multiple: false,
+					expiredAfter: 60 * 60 * 1000,
+				},
+			})).createdNote;
+
+			const resolvedNote = await resolveRemoteNote('a.test', note.id, bob);
+			deepStrictEqualWithExcludedFields(note, resolvedNote, [
+				'id',
+				'emojis',
+				/** Consistency of files is checked at {@link file://./drive.test.ts}, so let's skip. */
+				'fileIds',
+				'files',
+				/** @see https://github.com/misskey-dev/misskey/issues/12409 */
+				'reactionAcceptance',
+				'userId',
+				'user',
+				'uri',
+			]);
+			strictEqual(aliceInB.id, resolvedNote.userId);
+		});
+
+		test('Consistency of reply', async () => {
+			const _replyedNote = (await alice.client.request('notes/create', {
+				text: 'a',
+			})).createdNote;
+			const note = (await alice.client.request('notes/create', {
+				text: 'b',
+				replyId: _replyedNote.id,
+			})).createdNote;
+			// NOTE: the repliedCount is incremented, so fetch again
+			const replyedNote = await alice.client.request('notes/show', { noteId: _replyedNote.id });
+			strictEqual(replyedNote.repliesCount, 1);
+
+			const resolvedNote = await resolveRemoteNote('a.test', note.id, bob);
+			deepStrictEqualWithExcludedFields(note, resolvedNote, [
+				'id',
+				'emojis',
+				'reactionAcceptance',
+				'replyId',
+				'reply',
+				'userId',
+				'user',
+				'uri',
+			]);
+			assert(resolvedNote.replyId != null);
+			assert(resolvedNote.reply != null);
+			deepStrictEqualWithExcludedFields(replyedNote, resolvedNote.reply, [
+				'id',
+				// TODO: why clippedCount loses consistency?
+				'clippedCount',
+				'emojis',
+				'userId',
+				'user',
+				'uri',
+				// flaky because this is parallelly incremented, so let's check it below
+				'repliesCount',
+			]);
+			strictEqual(aliceInB.id, resolvedNote.userId);
+
+			await sleep();
+
+			const resolvedReplyedNote = await bob.client.request('notes/show', { noteId: resolvedNote.replyId });
+			strictEqual(resolvedReplyedNote.repliesCount, 1);
+		});
+
+		test('Consistency of Renote', async () => {
+			// NOTE: the renoteCount is not incremented, so no need to fetch again
+			const renotedNote = (await alice.client.request('notes/create', {
+				text: 'a',
+			})).createdNote;
+			const note = (await alice.client.request('notes/create', {
+				text: 'b',
+				renoteId: renotedNote.id,
+			})).createdNote;
+
+			const resolvedNote = await resolveRemoteNote('a.test', note.id, bob);
+			deepStrictEqualWithExcludedFields(note, resolvedNote, [
+				'id',
+				'emojis',
+				'reactionAcceptance',
+				'renoteId',
+				'renote',
+				'userId',
+				'user',
+				'uri',
+			]);
+			assert(resolvedNote.renoteId != null);
+			assert(resolvedNote.renote != null);
+			deepStrictEqualWithExcludedFields(renotedNote, resolvedNote.renote, [
+				'id',
+				'emojis',
+				'userId',
+				'user',
+				'uri',
+			]);
+			strictEqual(aliceInB.id, resolvedNote.userId);
+		});
+	});
+
+	describe('Other props', () => {
+		test('localOnly', async () => {
+			const note = (await alice.client.request('notes/create', { text: 'a', localOnly: true })).createdNote;
+			rejects(
+				async () => await bob.client.request('ap/show', { uri: `https://a.test/notes/${note.id}` }),
+				(err: any) => {
+					/**
+					 * FIXME: this error is not handled
+					 * @see https://github.com/misskey-dev/misskey/issues/12736
+					 */
+					strictEqual(err.code, 'INTERNAL_ERROR');
+					return true;
+				},
+			);
+		});
+	});
+
+	describe('Deletion', () => {
+		describe('Check Delete consistency', () => {
+			let carol: LoginUser;
+
+			beforeAll(async () => {
+				carol = await createAccount('a.test');
+
+				await carol.client.request('following/create', { userId: bobInA.id });
+				await sleep();
+			});
+
+			test('Delete is derivered to followers', async () => {
+				const note = (await bob.client.request('notes/create', { text: 'I\'m Bob.' })).createdNote;
+				const noteInA = await resolveRemoteNote('b.test', note.id, carol);
+				await bob.client.request('notes/delete', { noteId: note.id });
+				await sleep();
+
+				await rejects(
+					async () => await carol.client.request('notes/show', { noteId: noteInA.id }),
+					(err: any) => {
+						strictEqual(err.code, 'NO_SUCH_NOTE');
+						return true;
+					},
+				);
+			});
+		});
+
+		describe('Deletion of remote user\'s note for moderation', () => {
+			let note: Misskey.entities.Note;
+
+			test('Alice post is deleted in B', async () => {
+				note = (await alice.client.request('notes/create', { text: 'Hello' })).createdNote;
+				const noteInB = await resolveRemoteNote('a.test', note.id, bob);
+				const bMod = await createModerator('b.test');
+				await bMod.client.request('notes/delete', { noteId: noteInB.id });
+				await rejects(
+					async () => await bob.client.request('notes/show', { noteId: noteInB.id }),
+					(err: any) => {
+						strictEqual(err.code, 'NO_SUCH_NOTE');
+						return true;
+					},
+				);
+			});
+
+			/**
+			 * FIXME: implement soft deletion as well as user?
+			 *        @see https://github.com/misskey-dev/misskey/issues/11437
+			 */
+			test.failing('Not found even if resolve again', async () => {
+				const noteInB = await resolveRemoteNote('a.test', note.id, bob);
+				await rejects(
+					async () => await bob.client.request('notes/show', { noteId: noteInB.id }),
+					(err: any) => {
+						strictEqual(err.code, 'NO_SUCH_NOTE');
+						return true;
+					},
+				);
+			});
+		});
+	});
+
+	describe('Reaction', () => {
+		describe('Consistency', () => {
+			test('Unicode reaction', async () => {
+				const note = (await alice.client.request('notes/create', { text: 'a' })).createdNote;
+				const resolvedNote = await resolveRemoteNote('a.test', note.id, bob);
+				const reaction = '😅';
+				await bob.client.request('notes/reactions/create', { noteId: resolvedNote.id, reaction });
+				await sleep();
+
+				const reactions = await alice.client.request('notes/reactions', { noteId: note.id });
+				strictEqual(reactions.length, 1);
+				strictEqual(reactions[0].type, reaction);
+				strictEqual(reactions[0].user.id, bobInA.id);
+			});
+
+			test('Custom emoji reaction', async () => {
+				const note = (await alice.client.request('notes/create', { text: 'a' })).createdNote;
+				const resolvedNote = await resolveRemoteNote('a.test', note.id, bob);
+				const emoji = await addCustomEmoji('b.test');
+				await bob.client.request('notes/reactions/create', { noteId: resolvedNote.id, reaction: `:${emoji.name}:` });
+				await sleep();
+
+				const reactions = await alice.client.request('notes/reactions', { noteId: note.id });
+				strictEqual(reactions.length, 1);
+				strictEqual(reactions[0].type, `:${emoji.name}@b.test:`);
+				strictEqual(reactions[0].user.id, bobInA.id);
+			});
+		});
+
+		describe('Acceptance', () => {
+			test('Even if likeOnly, remote users can react with custom emoji, but it is converted to like', async () => {
+				const note = (await alice.client.request('notes/create', { text: 'a', reactionAcceptance: 'likeOnly' })).createdNote;
+				const noteInB = await resolveRemoteNote('a.test', note.id, bob);
+				const emoji = await addCustomEmoji('b.test');
+				await bob.client.request('notes/reactions/create', { noteId: noteInB.id, reaction: `:${emoji.name}:` });
+				await sleep();
+
+				const reactions = await alice.client.request('notes/reactions', { noteId: note.id });
+				strictEqual(reactions.length, 1);
+				strictEqual(reactions[0].type, '❤');
+			});
+
+			/**
+			 * TODO: this may be unexpected behavior?
+			 *       @see https://github.com/misskey-dev/misskey/issues/12409
+			 */
+			test('Even if nonSensitiveOnly, remote users can react with sensitive emoji, and it is not converted', async () => {
+				const note = (await alice.client.request('notes/create', { text: 'a', reactionAcceptance: 'nonSensitiveOnly' })).createdNote;
+				const noteInB = await resolveRemoteNote('a.test', note.id, bob);
+				const emoji = await addCustomEmoji('b.test', { isSensitive: true });
+				await bob.client.request('notes/reactions/create', { noteId: noteInB.id, reaction: `:${emoji.name}:` });
+				await sleep();
+
+				const reactions = await alice.client.request('notes/reactions', { noteId: note.id });
+				strictEqual(reactions.length, 1);
+				strictEqual(reactions[0].type, `:${emoji.name}@b.test:`);
+			});
+		});
+	});
+
+	describe('Poll', () => {
+		describe('Any remote user\'s vote is delivered to the author', () => {
+			let carol: LoginUser;
+
+			beforeAll(async () => {
+				carol = await createAccount('a.test');
+			});
+
+			test('Bob creates poll and receives a vote from Carol', async () => {
+				const note = (await bob.client.request('notes/create', { poll: { choices: ['inu', 'neko'] } })).createdNote;
+				const noteInA = await resolveRemoteNote('b.test', note.id, carol);
+				await carol.client.request('notes/polls/vote', { noteId: noteInA.id, choice: 0 });
+				await sleep();
+
+				const noteAfterVote = await bob.client.request('notes/show', { noteId: note.id });
+				assert(noteAfterVote.poll != null);
+				strictEqual(noteAfterVote.poll.choices[0].votes, 1);
+				strictEqual(noteAfterVote.poll.choices[1].votes, 0);
+			});
+		});
+
+		describe('Local user\'s vote is delivered to the author\'s remote followers', () => {
+			let bobRemoteFollower: LoginUser, localVoter: LoginUser;
+
+			beforeAll(async () => {
+				[
+					bobRemoteFollower,
+					localVoter,
+				] = await Promise.all([
+					createAccount('a.test'),
+					createAccount('b.test'),
+				]);
+
+				await bobRemoteFollower.client.request('following/create', { userId: bobInA.id });
+				await sleep();
+			});
+
+			test('A vote in Bob\'s server is delivered to Bob\'s remote followers', async () => {
+				const note = (await bob.client.request('notes/create', { poll: { choices: ['inu', 'neko'] } })).createdNote;
+				// NOTE: resolve before voting
+				const noteInA = await resolveRemoteNote('b.test', note.id, bobRemoteFollower);
+				await localVoter.client.request('notes/polls/vote', { noteId: note.id, choice: 0 });
+				await sleep();
+
+				const noteAfterVote = await bobRemoteFollower.client.request('notes/show', { noteId: noteInA.id });
+				assert(noteAfterVote.poll != null);
+				strictEqual(noteAfterVote.poll.choices[0].votes, 1);
+				strictEqual(noteAfterVote.poll.choices[1].votes, 0);
+			});
+		});
+	});
+});
diff --git a/packages/backend/test-federation/test/notification.test.ts b/packages/backend/test-federation/test/notification.test.ts
new file mode 100644
index 0000000000..6d55353653
--- /dev/null
+++ b/packages/backend/test-federation/test/notification.test.ts
@@ -0,0 +1,107 @@
+import * as Misskey from 'misskey-js';
+import { assertNotificationReceived, createAccount, type LoginUser, resolveRemoteNote, resolveRemoteUser, sleep } from './utils.js';
+
+describe('Notification', () => {
+	let alice: LoginUser, bob: LoginUser;
+	let bobInA: Misskey.entities.UserDetailedNotMe, aliceInB: Misskey.entities.UserDetailedNotMe;
+
+	beforeAll(async () => {
+		[alice, bob] = await Promise.all([
+			createAccount('a.test'),
+			createAccount('b.test'),
+		]);
+
+		[bobInA, aliceInB] = await Promise.all([
+			resolveRemoteUser('b.test', bob.id, alice),
+			resolveRemoteUser('a.test', alice.id, bob),
+		]);
+	});
+
+	describe('Follow', () => {
+		test('Get notification when follow', async () => {
+			await assertNotificationReceived(
+				'b.test', bob,
+				async () => await bob.client.request('following/create', { userId: aliceInB.id }),
+				notification => notification.type === 'followRequestAccepted' && notification.userId === aliceInB.id,
+				true,
+			);
+
+			await bob.client.request('following/delete', { userId: aliceInB.id });
+			await sleep();
+		});
+
+		test('Get notification when get followed', async () => {
+			await assertNotificationReceived(
+				'a.test', alice,
+				async () => await bob.client.request('following/create', { userId: aliceInB.id }),
+				notification => notification.type === 'follow' && notification.userId === bobInA.id,
+				true,
+			);
+		});
+
+		afterAll(async () => await bob.client.request('following/delete', { userId: aliceInB.id }));
+	});
+
+	describe('Note', () => {
+		test('Get notification when get a reaction', async () => {
+			const note = (await alice.client.request('notes/create', { text: 'a' })).createdNote;
+			const noteInB = await resolveRemoteNote('a.test', note.id, bob);
+			const reaction = '😅';
+			await assertNotificationReceived(
+				'a.test', alice,
+				async () => await bob.client.request('notes/reactions/create', { noteId: noteInB.id, reaction }),
+				notification =>
+					notification.type === 'reaction' && notification.note.id === note.id && notification.userId === bobInA.id && notification.reaction === reaction,
+				true,
+			);
+		});
+
+		test('Get notification when replied', async () => {
+			const note = (await alice.client.request('notes/create', { text: 'a' })).createdNote;
+			const noteInB = await resolveRemoteNote('a.test', note.id, bob);
+			const text = crypto.randomUUID();
+			await assertNotificationReceived(
+				'a.test', alice,
+				async () => await bob.client.request('notes/create', { text, replyId: noteInB.id }),
+				notification =>
+					notification.type === 'reply' && notification.note.reply!.id === note.id && notification.userId === bobInA.id && notification.note.text === text,
+				true,
+			);
+		});
+
+		test('Get notification when renoted', async () => {
+			const note = (await alice.client.request('notes/create', { text: 'a' })).createdNote;
+			const noteInB = await resolveRemoteNote('a.test', note.id, bob);
+			await assertNotificationReceived(
+				'a.test', alice,
+				async () => await bob.client.request('notes/create', { renoteId: noteInB.id }),
+				notification =>
+					notification.type === 'renote' && notification.note.renote!.id === note.id && notification.userId === bobInA.id,
+				true,
+			);
+		});
+
+		test('Get notification when quoted', async () => {
+			const note = (await alice.client.request('notes/create', { text: 'a' })).createdNote;
+			const noteInB = await resolveRemoteNote('a.test', note.id, bob);
+			const text = crypto.randomUUID();
+			await assertNotificationReceived(
+				'a.test', alice,
+				async () => await bob.client.request('notes/create', { text, renoteId: noteInB.id }),
+				notification =>
+					notification.type === 'quote' && notification.note.renote!.id === note.id && notification.userId === bobInA.id && notification.note.text === text,
+				true,
+			);
+		});
+
+		test('Get notification when mentioned', async () => {
+			const text = `@${alice.username}@a.test`;
+			await assertNotificationReceived(
+				'a.test', alice,
+				async () => await bob.client.request('notes/create', { text }),
+				notification => notification.type === 'mention' && notification.userId === bobInA.id && notification.note.text === text,
+				true,
+			);
+		});
+	});
+});
diff --git a/packages/backend/test-federation/test/timeline.test.ts b/packages/backend/test-federation/test/timeline.test.ts
new file mode 100644
index 0000000000..2250bf4a42
--- /dev/null
+++ b/packages/backend/test-federation/test/timeline.test.ts
@@ -0,0 +1,328 @@
+import { strictEqual } from 'assert';
+import * as Misskey from 'misskey-js';
+import { createAccount, fetchAdmin, isNoteUpdatedEventFired, isFired, type LoginUser, type Request, resolveRemoteUser, sleep, createRole } from './utils.js';
+
+const bAdmin = await fetchAdmin('b.test');
+
+describe('Timeline', () => {
+	let alice: LoginUser, bob: LoginUser;
+	let bobInA: Misskey.entities.UserDetailedNotMe, aliceInB: Misskey.entities.UserDetailedNotMe;
+
+	beforeAll(async () => {
+		[alice, bob] = await Promise.all([
+			createAccount('a.test'),
+			createAccount('b.test'),
+		]);
+
+		[bobInA, aliceInB] = await Promise.all([
+			resolveRemoteUser('b.test', bob.id, alice),
+			resolveRemoteUser('a.test', alice.id, bob),
+		]);
+
+		await bob.client.request('following/create', { userId: aliceInB.id });
+		await sleep();
+	});
+
+	type TimelineChannel = keyof Misskey.Channels & (`${string}Timeline` | 'antenna' | 'userList' | 'hashtag');
+	type TimelineEndpoint = keyof Misskey.Endpoints & (`${string}timeline` | 'antennas/notes' | 'roles/notes' | 'notes/search-by-tag');
+	const timelineMap = new Map<TimelineChannel, TimelineEndpoint>([
+		['antenna', 'antennas/notes'],
+		['globalTimeline', 'notes/global-timeline'],
+		['homeTimeline', 'notes/timeline'],
+		['hybridTimeline', 'notes/hybrid-timeline'],
+		['localTimeline', 'notes/local-timeline'],
+		['roleTimeline', 'roles/notes'],
+		['hashtag', 'notes/search-by-tag'],
+		['userList', 'notes/user-list-timeline'],
+	]);
+
+	async function postAndCheckReception<C extends TimelineChannel>(
+		timelineChannel: C,
+		expect: boolean,
+		noteParams: Misskey.entities.NotesCreateRequest = {},
+		channelParams: Misskey.Channels[C]['params'] = {},
+	) {
+		let note: Misskey.entities.Note | undefined;
+		const text = noteParams.text ?? crypto.randomUUID();
+		const streamingFired = await isFired(
+			'b.test', bob, timelineChannel,
+			async () => {
+				note = (await alice.client.request('notes/create', { text, ...noteParams })).createdNote;
+			},
+			'note', msg => msg.text === text,
+			channelParams,
+		);
+		strictEqual(streamingFired, expect);
+
+		const endpoint = timelineMap.get(timelineChannel)!;
+		const params: Misskey.Endpoints[typeof endpoint]['req'] =
+			endpoint === 'antennas/notes' ? { antennaId: (channelParams as Misskey.Channels['antenna']['params']).antennaId } :
+			endpoint === 'notes/user-list-timeline' ? { listId: (channelParams as Misskey.Channels['userList']['params']).listId } :
+			endpoint === 'notes/search-by-tag' ? { query: (channelParams as Misskey.Channels['hashtag']['params']).q } :
+			endpoint === 'roles/notes' ? { roleId: (channelParams as Misskey.Channels['roleTimeline']['params']).roleId } :
+			{};
+
+		await sleep();
+		const notes = await (bob.client.request as Request)(endpoint, params);
+		const noteInB = notes.filter(({ uri }) => uri === `https://a.test/notes/${note!.id}`).pop();
+		const endpointFired = noteInB != null;
+		strictEqual(endpointFired, expect);
+
+		// Let's check Delete reception
+		if (expect) {
+			const streamingFired = await isNoteUpdatedEventFired(
+				'b.test', bob, noteInB!.id,
+				async () => await alice.client.request('notes/delete', { noteId: note!.id }),
+				msg => msg.type === 'deleted' && msg.id === noteInB!.id,
+			);
+			strictEqual(streamingFired, true);
+
+			await sleep();
+			const notes = await (bob.client.request as Request)(endpoint, params);
+			const endpointFired = notes.every(({ uri }) => uri !== `https://a.test/notes/${note!.id}`);
+			strictEqual(endpointFired, true);
+		}
+	}
+
+	describe('homeTimeline', () => {
+		// NOTE: narrowing scope intentionally to prevent mistakes by copy-and-paste
+		const homeTimeline = 'homeTimeline';
+
+		describe('Check reception of remote followee\'s Note', () => {
+			test('Receive remote followee\'s Note', async () => {
+				await postAndCheckReception(homeTimeline, true);
+			});
+
+			test('Receive remote followee\'s home-only Note', async () => {
+				await postAndCheckReception(homeTimeline, true, { visibility: 'home' });
+			});
+
+			test('Receive remote followee\'s followers-only Note', async () => {
+				await postAndCheckReception(homeTimeline, true, { visibility: 'followers' });
+			});
+
+			test('Receive remote followee\'s visible specified-only Note', async () => {
+				await postAndCheckReception(homeTimeline, true, { visibility: 'specified', visibleUserIds: [bobInA.id] });
+			});
+
+			test('Don\'t receive remote followee\'s localOnly Note', async () => {
+				await postAndCheckReception(homeTimeline, false, { localOnly: true });
+			});
+
+			test('Don\'t receive remote followee\'s invisible specified-only Note', async () => {
+				await postAndCheckReception(homeTimeline, false, { visibility: 'specified' });
+			});
+
+			/**
+			 * FIXME: can receive this
+			 * @see https://github.com/misskey-dev/misskey/issues/14083
+			 */
+			test.failing('Don\'t receive remote followee\'s invisible and mentioned specified-only Note', async () => {
+				await postAndCheckReception(homeTimeline, false, { text: `@${bob.username}@b.test Hello`, visibility: 'specified' });
+			});
+
+			/**
+			 * FIXME: cannot receive this
+			 * @see https://github.com/misskey-dev/misskey/issues/14084
+			 */
+			test.failing('Receive remote followee\'s visible specified-only reply to invisible specified-only Note', async () => {
+				const note = (await alice.client.request('notes/create', { text: 'a', visibility: 'specified' })).createdNote;
+				await postAndCheckReception(homeTimeline, true, { replyId: note.id, visibility: 'specified', visibleUserIds: [bobInA.id] });
+			});
+		});
+	});
+
+	describe('localTimeline', () => {
+		const localTimeline = 'localTimeline';
+
+		describe('Check reception of remote followee\'s Note', () => {
+			test('Don\'t receive remote followee\'s Note', async () => {
+				await postAndCheckReception(localTimeline, false);
+			});
+		});
+	});
+
+	describe('hybridTimeline', () => {
+		const hybridTimeline = 'hybridTimeline';
+
+		describe('Check reception of remote followee\'s Note', () => {
+			test('Receive remote followee\'s Note', async () => {
+				await postAndCheckReception(hybridTimeline, true);
+			});
+
+			test('Receive remote followee\'s home-only Note', async () => {
+				await postAndCheckReception(hybridTimeline, true, { visibility: 'home' });
+			});
+
+			test('Receive remote followee\'s followers-only Note', async () => {
+				await postAndCheckReception(hybridTimeline, true, { visibility: 'followers' });
+			});
+
+			test('Receive remote followee\'s visible specified-only Note', async () => {
+				await postAndCheckReception(hybridTimeline, true, { visibility: 'specified', visibleUserIds: [bobInA.id] });
+			});
+		});
+	});
+
+	describe('globalTimeline', () => {
+		const globalTimeline = 'globalTimeline';
+
+		describe('Check reception of remote followee\'s Note', () => {
+			test('Receive remote followee\'s Note', async () => {
+				await postAndCheckReception(globalTimeline, true);
+			});
+
+			test('Don\'t receive remote followee\'s home-only Note', async () => {
+				await postAndCheckReception(globalTimeline, false, { visibility: 'home' });
+			});
+
+			test('Don\'t receive remote followee\'s followers-only Note', async () => {
+				await postAndCheckReception(globalTimeline, false, { visibility: 'followers' });
+			});
+
+			test('Don\'t receive remote followee\'s visible specified-only Note', async () => {
+				await postAndCheckReception(globalTimeline, false, { visibility: 'specified', visibleUserIds: [bobInA.id] });
+			});
+		});
+	});
+
+	describe('userList', () => {
+		const userList = 'userList';
+
+		let list: Misskey.entities.UserList;
+
+		beforeAll(async () => {
+			list = await bob.client.request('users/lists/create', { name: 'Bob\'s List' });
+			await bob.client.request('users/lists/push', { listId: list.id, userId: aliceInB.id });
+			await sleep();
+		});
+
+		describe('Check reception of remote followee\'s Note', () => {
+			test('Receive remote followee\'s Note', async () => {
+				await postAndCheckReception(userList, true, {}, { listId: list.id });
+			});
+
+			test('Receive remote followee\'s home-only Note', async () => {
+				await postAndCheckReception(userList, true, { visibility: 'home' }, { listId: list.id });
+			});
+
+			test('Receive remote followee\'s followers-only Note', async () => {
+				await postAndCheckReception(userList, true, { visibility: 'followers' }, { listId: list.id });
+			});
+
+			test('Receive remote followee\'s visible specified-only Note', async () => {
+				await postAndCheckReception(userList, true, { visibility: 'specified', visibleUserIds: [bobInA.id] }, { listId: list.id });
+			});
+		});
+	});
+
+	describe('hashtag', () => {
+		const hashtag = 'hashtag';
+
+		describe('Check reception of remote followee\'s Note', () => {
+			test('Receive remote followee\'s Note', async () => {
+				const tag = crypto.randomUUID();
+				await postAndCheckReception(hashtag, true, { text: `#${tag}` }, { q: [[tag]] });
+			});
+
+			test('Receive remote followee\'s home-only Note', async () => {
+				const tag = crypto.randomUUID();
+				await postAndCheckReception(hashtag, true, { text: `#${tag}`, visibility: 'home' }, { q: [[tag]] });
+			});
+
+			test('Receive remote followee\'s followers-only Note', async () => {
+				const tag = crypto.randomUUID();
+				await postAndCheckReception(hashtag, true, { text: `#${tag}`, visibility: 'followers' }, { q: [[tag]] });
+			});
+
+			test('Receive remote followee\'s visible specified-only Note', async () => {
+				const tag = crypto.randomUUID();
+				await postAndCheckReception(hashtag, true, { text: `#${tag}`, visibility: 'specified', visibleUserIds: [bobInA.id] }, { q: [[tag]] });
+			});
+		});
+	});
+
+	describe('roleTimeline', () => {
+		const roleTimeline = 'roleTimeline';
+
+		let role: Misskey.entities.Role;
+
+		beforeAll(async () => {
+			role = await createRole('b.test', {
+				name: 'Remote Users',
+				description: 'Remote users are assigned to this role.',
+				condFormula: {
+					/** TODO: @see https://github.com/misskey-dev/misskey/issues/14169 */
+					type: 'isRemote' as never,
+				},
+			});
+			await sleep();
+		});
+
+		describe('Check reception of remote followee\'s Note', () => {
+			test('Receive remote followee\'s Note', async () => {
+				await postAndCheckReception(roleTimeline, true, {}, { roleId: role.id });
+			});
+
+			test('Don\'t receive remote followee\'s home-only Note', async () => {
+				await postAndCheckReception(roleTimeline, false, { visibility: 'home' }, { roleId: role.id });
+			});
+
+			test('Don\'t receive remote followee\'s followers-only Note', async () => {
+				await postAndCheckReception(roleTimeline, false, { visibility: 'followers' }, { roleId: role.id });
+			});
+
+			test('Don\'t receive remote followee\'s visible specified-only Note', async () => {
+				await postAndCheckReception(roleTimeline, false, { visibility: 'specified', visibleUserIds: [bobInA.id] }, { roleId: role.id });
+			});
+		});
+
+		afterAll(async () => {
+			await bAdmin.client.request('admin/roles/delete', { roleId: role.id });
+		});
+	});
+
+	// TODO: Cannot test
+	describe.skip('antenna', () => {
+		const antenna = 'antenna';
+
+		let bobAntenna: Misskey.entities.Antenna;
+
+		beforeAll(async () => {
+			bobAntenna = await bob.client.request('antennas/create', {
+				name: 'Bob\'s Egosurfing Antenna',
+				src: 'all',
+				keywords: [['Bob']],
+				excludeKeywords: [],
+				users: [],
+				caseSensitive: false,
+				localOnly: false,
+				withReplies: true,
+				withFile: true,
+			});
+			await sleep();
+		});
+
+		describe('Check reception of remote followee\'s Note', () => {
+			test('Receive remote followee\'s Note', async () => {
+				await postAndCheckReception(antenna, true, { text: 'I love Bob (1)' }, { antennaId: bobAntenna.id });
+			});
+
+			test('Don\'t receive remote followee\'s home-only Note', async () => {
+				await postAndCheckReception(antenna, false, { text: 'I love Bob (2)', visibility: 'home' }, { antennaId: bobAntenna.id });
+			});
+
+			test('Don\'t receive remote followee\'s followers-only Note', async () => {
+				await postAndCheckReception(antenna, false, { text: 'I love Bob (3)', visibility: 'followers' }, { antennaId: bobAntenna.id });
+			});
+
+			test('Don\'t receive remote followee\'s visible specified-only Note', async () => {
+				await postAndCheckReception(antenna, false, { text: 'I love Bob (4)', visibility: 'specified', visibleUserIds: [bobInA.id] }, { antennaId: bobAntenna.id });
+			});
+		});
+
+		afterAll(async () => {
+			await bob.client.request('antennas/delete', { antennaId: bobAntenna.id });
+		});
+	});
+});
diff --git a/packages/backend/test-federation/test/user.test.ts b/packages/backend/test-federation/test/user.test.ts
new file mode 100644
index 0000000000..76605e61d4
--- /dev/null
+++ b/packages/backend/test-federation/test/user.test.ts
@@ -0,0 +1,560 @@
+import assert, { rejects, strictEqual } from 'node:assert';
+import * as Misskey from 'misskey-js';
+import { createAccount, deepStrictEqualWithExcludedFields, fetchAdmin, type LoginUser, resolveRemoteNote, resolveRemoteUser, sleep } from './utils.js';
+
+const [aAdmin, bAdmin] = await Promise.all([
+	fetchAdmin('a.test'),
+	fetchAdmin('b.test'),
+]);
+
+describe('User', () => {
+	describe('Profile', () => {
+		describe('Consistency of profile', () => {
+			let alice: LoginUser;
+			let aliceWatcher: LoginUser;
+			let aliceWatcherInB: LoginUser;
+
+			beforeAll(async () => {
+				alice = await createAccount('a.test');
+				[
+					aliceWatcher,
+					aliceWatcherInB,
+				] = await Promise.all([
+					createAccount('a.test'),
+					createAccount('b.test'),
+				]);
+			});
+
+			test('Check consistency', async () => {
+				const aliceInA = await aliceWatcher.client.request('users/show', { userId: alice.id });
+				const resolved = await resolveRemoteUser('a.test', aliceInA.id, aliceWatcherInB);
+				const aliceInB = await aliceWatcherInB.client.request('users/show', { userId: resolved.id });
+
+				// console.log(`a.test: ${JSON.stringify(aliceInA, null, '\t')}`);
+				// console.log(`b.test: ${JSON.stringify(aliceInB, null, '\t')}`);
+
+				deepStrictEqualWithExcludedFields(aliceInA, aliceInB, [
+					'id',
+					'host',
+					'avatarUrl',
+					'instance',
+					'badgeRoles',
+					'url',
+					'uri',
+					'createdAt',
+					'lastFetchedAt',
+					'publicReactions',
+				]);
+			});
+		});
+
+		describe('ffVisibility is federated', () => {
+			let alice: LoginUser, bob: LoginUser;
+			let bobInA: Misskey.entities.UserDetailedNotMe, aliceInB: Misskey.entities.UserDetailedNotMe;
+
+			beforeAll(async () => {
+				[alice, bob] = await Promise.all([
+					createAccount('a.test'),
+					createAccount('b.test'),
+				]);
+
+				[bobInA, aliceInB] = await Promise.all([
+					resolveRemoteUser('b.test', bob.id, alice),
+					resolveRemoteUser('a.test', alice.id, bob),
+				]);
+
+				// NOTE: follow each other
+				await Promise.all([
+					alice.client.request('following/create', { userId: bobInA.id }),
+					bob.client.request('following/create', { userId: aliceInB.id }),
+				]);
+				await sleep();
+			});
+
+			test('Visibility set public by default', async () => {
+				for (const user of await Promise.all([
+					alice.client.request('users/show', { userId: bobInA.id }),
+					bob.client.request('users/show', { userId: aliceInB.id }),
+				])) {
+					strictEqual(user.followersVisibility, 'public');
+					strictEqual(user.followingVisibility, 'public');
+				}
+			});
+
+			/** FIXME: not working */
+			test.skip('Setting private for followersVisibility is federated', async () => {
+				await Promise.all([
+					alice.client.request('i/update', { followersVisibility: 'private' }),
+					bob.client.request('i/update', { followersVisibility: 'private' }),
+				]);
+				await sleep();
+
+				for (const user of await Promise.all([
+					alice.client.request('users/show', { userId: bobInA.id }),
+					bob.client.request('users/show', { userId: aliceInB.id }),
+				])) {
+					strictEqual(user.followersVisibility, 'private');
+					strictEqual(user.followingVisibility, 'public');
+				}
+			});
+
+			test.skip('Setting private for followingVisibility is federated', async () => {
+				await Promise.all([
+					alice.client.request('i/update', { followingVisibility: 'private' }),
+					bob.client.request('i/update', { followingVisibility: 'private' }),
+				]);
+				await sleep();
+
+				for (const user of await Promise.all([
+					alice.client.request('users/show', { userId: bobInA.id }),
+					bob.client.request('users/show', { userId: aliceInB.id }),
+				])) {
+					strictEqual(user.followersVisibility, 'private');
+					strictEqual(user.followingVisibility, 'private');
+				}
+			});
+		});
+
+		describe('isCat is federated', () => {
+			let alice: LoginUser, bob: LoginUser;
+			let bobInA: Misskey.entities.UserDetailedNotMe, aliceInB: Misskey.entities.UserDetailedNotMe;
+
+			beforeAll(async () => {
+				[alice, bob] = await Promise.all([
+					createAccount('a.test'),
+					createAccount('b.test'),
+				]);
+
+				[bobInA, aliceInB] = await Promise.all([
+					resolveRemoteUser('b.test', bob.id, alice),
+					resolveRemoteUser('a.test', alice.id, bob),
+				]);
+			});
+
+			test('Not isCat for default', () => {
+				strictEqual(aliceInB.isCat, false);
+			});
+
+			test('Becoming a cat is sent to their followers', async () => {
+				await bob.client.request('following/create', { userId: aliceInB.id });
+				await sleep();
+
+				await alice.client.request('i/update', { isCat: true });
+				await sleep();
+
+				const res = await bob.client.request('users/show', { userId: aliceInB.id });
+				strictEqual(res.isCat, true);
+			});
+		});
+
+		describe('Pinning Notes', () => {
+			let alice: LoginUser, bob: LoginUser;
+			let aliceInB: Misskey.entities.UserDetailedNotMe;
+
+			beforeAll(async () => {
+				[alice, bob] = await Promise.all([
+					createAccount('a.test'),
+					createAccount('b.test'),
+				]);
+				aliceInB = await resolveRemoteUser('a.test', alice.id, bob);
+
+				await bob.client.request('following/create', { userId: aliceInB.id });
+			});
+
+			test('Pinning localOnly Note is not delivered', async () => {
+				const note = (await alice.client.request('notes/create', { text: 'a', localOnly: true })).createdNote;
+				await alice.client.request('i/pin', { noteId: note.id });
+				await sleep();
+
+				const _aliceInB = await bob.client.request('users/show', { userId: aliceInB.id });
+				strictEqual(_aliceInB.pinnedNoteIds.length, 0);
+			});
+
+			test('Pinning followers-only Note is not delivered', async () => {
+				const note = (await alice.client.request('notes/create', { text: 'a', visibility: 'followers' })).createdNote;
+				await alice.client.request('i/pin', { noteId: note.id });
+				await sleep();
+
+				const _aliceInB = await bob.client.request('users/show', { userId: aliceInB.id });
+				strictEqual(_aliceInB.pinnedNoteIds.length, 0);
+			});
+
+			let pinnedNote: Misskey.entities.Note;
+
+			test('Pinning normal Note is delivered', async () => {
+				pinnedNote = (await alice.client.request('notes/create', { text: 'a' })).createdNote;
+				await alice.client.request('i/pin', { noteId: pinnedNote.id });
+				await sleep();
+
+				const _aliceInB = await bob.client.request('users/show', { userId: aliceInB.id });
+				strictEqual(_aliceInB.pinnedNoteIds.length, 1);
+				const pinnedNoteInB = await resolveRemoteNote('a.test', pinnedNote.id, bob);
+				strictEqual(_aliceInB.pinnedNotes[0].id, pinnedNoteInB.id);
+			});
+
+			test('Unpinning normal Note is delivered', async () => {
+				await alice.client.request('i/unpin', { noteId: pinnedNote.id });
+				await sleep();
+
+				const _aliceInB = await bob.client.request('users/show', { userId: aliceInB.id });
+				strictEqual(_aliceInB.pinnedNoteIds.length, 0);
+			});
+		});
+	});
+
+	describe('Follow / Unfollow', () => {
+		let alice: LoginUser, bob: LoginUser;
+		let bobInA: Misskey.entities.UserDetailedNotMe, aliceInB: Misskey.entities.UserDetailedNotMe;
+
+		beforeAll(async () => {
+			[alice, bob] = await Promise.all([
+				createAccount('a.test'),
+				createAccount('b.test'),
+			]);
+
+			[bobInA, aliceInB] = await Promise.all([
+				resolveRemoteUser('b.test', bob.id, alice),
+				resolveRemoteUser('a.test', alice.id, bob),
+			]);
+		});
+
+		describe('Follow a.test ==> b.test', () => {
+			beforeAll(async () => {
+				await alice.client.request('following/create', { userId: bobInA.id });
+
+				await sleep();
+			});
+
+			test('Check consistency with `users/following` and `users/followers` endpoints', async () => {
+				await Promise.all([
+					strictEqual(
+						(await alice.client.request('users/following', { userId: alice.id }))
+							.some(v => v.followeeId === bobInA.id),
+						true,
+					),
+					strictEqual(
+						(await bob.client.request('users/followers', { userId: bob.id }))
+							.some(v => v.followerId === aliceInB.id),
+						true,
+					),
+				]);
+			});
+		});
+
+		describe('Unfollow a.test ==> b.test', () => {
+			beforeAll(async () => {
+				await alice.client.request('following/delete', { userId: bobInA.id });
+
+				await sleep();
+			});
+
+			test('Check consistency with `users/following` and `users/followers` endpoints', async () => {
+				await Promise.all([
+					strictEqual(
+						(await alice.client.request('users/following', { userId: alice.id }))
+							.some(v => v.followeeId === bobInA.id),
+						false,
+					),
+					strictEqual(
+						(await bob.client.request('users/followers', { userId: bob.id }))
+							.some(v => v.followerId === aliceInB.id),
+						false,
+					),
+				]);
+			});
+		});
+	});
+
+	describe('Follow requests', () => {
+		let alice: LoginUser, bob: LoginUser;
+		let bobInA: Misskey.entities.UserDetailedNotMe, aliceInB: Misskey.entities.UserDetailedNotMe;
+
+		beforeAll(async () => {
+			[alice, bob] = await Promise.all([
+				createAccount('a.test'),
+				createAccount('b.test'),
+			]);
+
+			[bobInA, aliceInB] = await Promise.all([
+				resolveRemoteUser('b.test', bob.id, alice),
+				resolveRemoteUser('a.test', alice.id, bob),
+			]);
+
+			await alice.client.request('i/update', { isLocked: true });
+		});
+
+		describe('Send follow request from Bob to Alice and cancel', () => {
+			describe('Bob sends follow request to Alice', () => {
+				beforeAll(async () => {
+					await bob.client.request('following/create', { userId: aliceInB.id });
+					await sleep();
+				});
+
+				test('Alice should have a request', async () => {
+					const requests = await alice.client.request('following/requests/list', {});
+					strictEqual(requests.length, 1);
+					strictEqual(requests[0].followee.id, alice.id);
+					strictEqual(requests[0].follower.id, bobInA.id);
+				});
+			});
+
+			describe('Alice cancels it', () => {
+				beforeAll(async () => {
+					await bob.client.request('following/requests/cancel', { userId: aliceInB.id });
+					await sleep();
+				});
+
+				test('Alice should have no requests', async () => {
+					const requests = await alice.client.request('following/requests/list', {});
+					strictEqual(requests.length, 0);
+				});
+			});
+		});
+
+		describe('Send follow request from Bob to Alice and reject', () => {
+			beforeAll(async () => {
+				await bob.client.request('following/create', { userId: aliceInB.id });
+				await sleep();
+
+				await alice.client.request('following/requests/reject', { userId: bobInA.id });
+				await sleep();
+			});
+
+			test('Bob should have no requests', async () => {
+				await rejects(
+					async () => await bob.client.request('following/requests/cancel', { userId: aliceInB.id }),
+					(err: any) => {
+						strictEqual(err.code, 'FOLLOW_REQUEST_NOT_FOUND');
+						return true;
+					},
+				);
+			});
+
+			test('Bob doesn\'t follow Alice', async () => {
+				const following = await bob.client.request('users/following', { userId: bob.id });
+				strictEqual(following.length, 0);
+			});
+		});
+
+		describe('Send follow request from Bob to Alice and accept', () => {
+			beforeAll(async () => {
+				await bob.client.request('following/create', { userId: aliceInB.id });
+				await sleep();
+
+				await alice.client.request('following/requests/accept', { userId: bobInA.id });
+				await sleep();
+			});
+
+			test('Bob follows Alice', async () => {
+				const following = await bob.client.request('users/following', { userId: bob.id });
+				strictEqual(following.length, 1);
+				strictEqual(following[0].followeeId, aliceInB.id);
+				strictEqual(following[0].followerId, bob.id);
+			});
+		});
+	});
+
+	describe('Deletion', () => {
+		describe('Check Delete consistency', () => {
+			let alice: LoginUser, bob: LoginUser;
+			let bobInA: Misskey.entities.UserDetailedNotMe, aliceInB: Misskey.entities.UserDetailedNotMe;
+
+			beforeAll(async () => {
+				[alice, bob] = await Promise.all([
+					createAccount('a.test'),
+					createAccount('b.test'),
+				]);
+
+				[bobInA, aliceInB] = await Promise.all([
+					resolveRemoteUser('b.test', bob.id, alice),
+					resolveRemoteUser('a.test', alice.id, bob),
+				]);
+			});
+
+			test('Bob follows Alice, and Alice deleted themself', async () => {
+				await bob.client.request('following/create', { userId: aliceInB.id });
+				await sleep();
+
+				const followers = await alice.client.request('users/followers', { userId: alice.id });
+				strictEqual(followers.length, 1); // followed by Bob
+
+				await alice.client.request('i/delete-account', { password: alice.password });
+				await sleep();
+
+				const following = await bob.client.request('users/following', { userId: bob.id });
+				strictEqual(following.length, 0); // no following relation
+
+				await rejects(
+					async () => await bob.client.request('following/create', { userId: aliceInB.id }),
+					(err: any) => {
+						strictEqual(err.code, 'NO_SUCH_USER');
+						return true;
+					},
+				);
+			});
+		});
+
+		describe('Deletion of remote user for moderation', () => {
+			let alice: LoginUser, bob: LoginUser;
+			let bobInA: Misskey.entities.UserDetailedNotMe, aliceInB: Misskey.entities.UserDetailedNotMe;
+
+			beforeAll(async () => {
+				[alice, bob] = await Promise.all([
+					createAccount('a.test'),
+					createAccount('b.test'),
+				]);
+
+				[bobInA, aliceInB] = await Promise.all([
+					resolveRemoteUser('b.test', bob.id, alice),
+					resolveRemoteUser('a.test', alice.id, bob),
+				]);
+			});
+
+			test('Bob follows Alice, then Alice gets deleted in B server', async () => {
+				await bob.client.request('following/create', { userId: aliceInB.id });
+				await sleep();
+
+				const followers = await alice.client.request('users/followers', { userId: alice.id });
+				strictEqual(followers.length, 1); // followed by Bob
+
+				await bAdmin.client.request('admin/delete-account', { userId: aliceInB.id });
+				await sleep();
+
+				/**
+				 * FIXME: remote account is not deleted!
+				 *        @see https://github.com/misskey-dev/misskey/issues/14728
+				 */
+				const deletedAlice = await bob.client.request('users/show', { userId: aliceInB.id });
+				assert(deletedAlice.id, aliceInB.id);
+
+				// TODO: why still following relation?
+				const following = await bob.client.request('users/following', { userId: bob.id });
+				strictEqual(following.length, 1);
+				await rejects(
+					async () => await bob.client.request('following/create', { userId: aliceInB.id }),
+					(err: any) => {
+						strictEqual(err.code, 'ALREADY_FOLLOWING');
+						return true;
+					},
+				);
+			});
+
+			test('Alice tries to follow Bob, but it is not processed', async () => {
+				await alice.client.request('following/create', { userId: bobInA.id });
+				await sleep();
+
+				const following = await alice.client.request('users/following', { userId: alice.id });
+				strictEqual(following.length, 0); // Not following Bob because B server doesn't return Accept
+
+				const followers = await bob.client.request('users/followers', { userId: bob.id });
+				strictEqual(followers.length, 0); // Alice's Follow is not processed
+			});
+		});
+	});
+
+	describe('Suspension', () => {
+		describe('Check suspend/unsuspend consistency', () => {
+			let alice: LoginUser, bob: LoginUser;
+			let bobInA: Misskey.entities.UserDetailedNotMe, aliceInB: Misskey.entities.UserDetailedNotMe;
+
+			beforeAll(async () => {
+				[alice, bob] = await Promise.all([
+					createAccount('a.test'),
+					createAccount('b.test'),
+				]);
+
+				[bobInA, aliceInB] = await Promise.all([
+					resolveRemoteUser('b.test', bob.id, alice),
+					resolveRemoteUser('a.test', alice.id, bob),
+				]);
+			});
+
+			test('Bob follows Alice, and Alice gets suspended, there is no following relation, and Bob fails to follow again', async () => {
+				await bob.client.request('following/create', { userId: aliceInB.id });
+				await sleep();
+
+				const followers = await alice.client.request('users/followers', { userId: alice.id });
+				strictEqual(followers.length, 1); // followed by Bob
+
+				await aAdmin.client.request('admin/suspend-user', { userId: alice.id });
+				await sleep();
+
+				const following = await bob.client.request('users/following', { userId: bob.id });
+				strictEqual(following.length, 0); // no following relation
+
+				await rejects(
+					async () => await bob.client.request('following/create', { userId: aliceInB.id }),
+					(err: any) => {
+						strictEqual(err.code, 'NO_SUCH_USER');
+						return true;
+					},
+				);
+			});
+
+			test('Alice gets unsuspended, Bob succeeds in following Alice', async () => {
+				await aAdmin.client.request('admin/unsuspend-user', { userId: alice.id });
+				await sleep();
+
+				const followers = await alice.client.request('users/followers', { userId: alice.id });
+				strictEqual(followers.length, 1); // FIXME: followers are not deleted??
+
+				/**
+				 * FIXME: still rejected!
+				 *        seems to can't process Undo Delete activity because it is not implemented
+				 *        related @see https://github.com/misskey-dev/misskey/issues/13273
+				 */
+				await rejects(
+					async () => await bob.client.request('following/create', { userId: aliceInB.id }),
+					(err: any) => {
+						strictEqual(err.code, 'NO_SUCH_USER');
+						return true;
+					},
+				);
+
+				// FIXME: resolving also fails
+				await rejects(
+					async () => await resolveRemoteUser('a.test', alice.id, bob),
+					(err: any) => {
+						strictEqual(err.code, 'INTERNAL_ERROR');
+						return true;
+					},
+				);
+			});
+
+			/**
+			 * instead of simple unsuspension, let's tell existence by following from Alice
+			 */
+			test('Alice can follow Bob', async () => {
+				await alice.client.request('following/create', { userId: bobInA.id });
+				await sleep();
+
+				const bobFollowers = await bob.client.request('users/followers', { userId: bob.id });
+				strictEqual(bobFollowers.length, 1); // followed by Alice
+				assert(bobFollowers[0].follower != null);
+				const renewedaliceInB = bobFollowers[0].follower;
+				assert(aliceInB.username === renewedaliceInB.username);
+				assert(aliceInB.host === renewedaliceInB.host);
+				assert(aliceInB.id !== renewedaliceInB.id); // TODO: Same username and host, but their ids are different! Is it OK?
+
+				const following = await bob.client.request('users/following', { userId: bob.id });
+				strictEqual(following.length, 0); // following are deleted
+
+				// Bob tries to follow Alice
+				await bob.client.request('following/create', { userId: renewedaliceInB.id });
+				await sleep();
+
+				const aliceFollowers = await alice.client.request('users/followers', { userId: alice.id });
+				strictEqual(aliceFollowers.length, 1);
+
+				// FIXME: but resolving still fails ...
+				await rejects(
+					async () => await resolveRemoteUser('a.test', alice.id, bob),
+					(err: any) => {
+						strictEqual(err.code, 'INTERNAL_ERROR');
+						return true;
+					},
+				);
+			});
+		});
+	});
+});
diff --git a/packages/backend/test-federation/test/utils.ts b/packages/backend/test-federation/test/utils.ts
new file mode 100644
index 0000000000..093277cdb4
--- /dev/null
+++ b/packages/backend/test-federation/test/utils.ts
@@ -0,0 +1,307 @@
+import { deepStrictEqual, strictEqual } from 'assert';
+import { readFile } from 'fs/promises';
+import { dirname, join } from 'path';
+import { fileURLToPath } from 'url';
+import * as Misskey from 'misskey-js';
+import { WebSocket } from 'ws';
+
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = dirname(__filename);
+
+export const ADMIN_PARAMS = { username: 'admin', password: 'admin' };
+const ADMIN_CACHE = new Map<Host, SigninResponse>();
+
+await Promise.all([
+	fetchAdmin('a.test'),
+	fetchAdmin('b.test'),
+]);
+
+type SigninResponse = Omit<Misskey.entities.SigninFlowResponse & { finished: true }, 'finished'>;
+
+export type LoginUser = SigninResponse & {
+	client: Misskey.api.APIClient;
+	username: string;
+	password: string;
+}
+
+/** used for avoiding overload and some endpoints */
+export type Request = <
+	E extends keyof Misskey.Endpoints,
+	P extends Misskey.Endpoints[E]['req'],
+>(
+	endpoint: E,
+	params: P,
+	credential?: string | null,
+) => Promise<Misskey.api.SwitchCaseResponseType<E, P>>;
+
+type Host = 'a.test' | 'b.test';
+
+export async function sleep(ms = 200): Promise<void> {
+	return new Promise(resolve => setTimeout(resolve, ms));
+}
+
+async function signin(
+	host: Host,
+	params: Misskey.entities.SigninFlowRequest,
+): Promise<SigninResponse> {
+	// wait for a second to prevent hit rate limit
+	await sleep(1000);
+
+	return await (new Misskey.api.APIClient({ origin: `https://${host}` }).request as Request)('signin-flow', params)
+		.then(res => {
+			strictEqual(res.finished, true);
+			if (params.username === ADMIN_PARAMS.username) ADMIN_CACHE.set(host, res);
+			return res;
+		})
+		.then(({ id, i }) => ({ id, i }))
+		.catch(async err => {
+			if (err.code === 'TOO_MANY_AUTHENTICATION_FAILURES') {
+				await sleep(Math.random() * 2000);
+				return await signin(host, params);
+			}
+			throw err;
+		});
+}
+
+async function createAdmin(host: Host): Promise<Misskey.entities.SignupResponse | undefined> {
+	const client = new Misskey.api.APIClient({ origin: `https://${host}` });
+	return await client.request('admin/accounts/create', ADMIN_PARAMS).then(res => {
+		ADMIN_CACHE.set(host, {
+			id: res.id,
+			// @ts-expect-error FIXME: openapi-typescript generates incorrect response type for this endpoint, so ignore this
+			i: res.token,
+		});
+		return res as Misskey.entities.SignupResponse;
+	}).then(async res => {
+		await client.request('admin/roles/update-default-policies', {
+			policies: {
+				/** TODO: @see https://github.com/misskey-dev/misskey/issues/14169 */
+				rateLimitFactor: 0 as never,
+			},
+		}, res.token);
+		return res;
+	}).catch(err => {
+		if (err.info.e.message === 'access denied') return undefined;
+		throw err;
+	});
+}
+
+export async function fetchAdmin(host: Host): Promise<LoginUser> {
+	const admin = ADMIN_CACHE.get(host) ?? await signin(host, ADMIN_PARAMS)
+		.catch(async err => {
+			if (err.id === '6cc579cc-885d-43d8-95c2-b8c7fc963280') {
+				await createAdmin(host);
+				return await signin(host, ADMIN_PARAMS);
+			}
+			throw err;
+		});
+
+	return {
+		...admin,
+		client: new Misskey.api.APIClient({ origin: `https://${host}`, credential: admin.i }),
+		...ADMIN_PARAMS,
+	};
+}
+
+export async function createAccount(host: Host): Promise<LoginUser> {
+	const username = crypto.randomUUID().replaceAll('-', '').substring(0, 20);
+	const password = crypto.randomUUID().replaceAll('-', '');
+	const admin = await fetchAdmin(host);
+	await admin.client.request('admin/accounts/create', { username, password });
+	const signinRes = await signin(host, { username, password });
+
+	return {
+		...signinRes,
+		client: new Misskey.api.APIClient({ origin: `https://${host}`, credential: signinRes.i }),
+		username,
+		password,
+	};
+}
+
+export async function createModerator(host: Host): Promise<LoginUser> {
+	const user = await createAccount(host);
+	const role = await createRole(host, {
+		name: 'Moderator',
+		isModerator: true,
+	});
+	const admin = await fetchAdmin(host);
+	await admin.client.request('admin/roles/assign', { roleId: role.id, userId: user.id });
+	return user;
+}
+
+export async function createRole(
+	host: Host,
+	params: Partial<Misskey.entities.AdminRolesCreateRequest> = {},
+): Promise<Misskey.entities.Role> {
+	const admin = await fetchAdmin(host);
+	return await admin.client.request('admin/roles/create', {
+		name: 'Some role',
+		description: 'Role for testing',
+		color: null,
+		iconUrl: null,
+		target: 'conditional',
+		condFormula: {},
+		isPublic: true,
+		isModerator: false,
+		isAdministrator: false,
+		isExplorable: true,
+		asBadge: false,
+		canEditMembersByModerator: false,
+		displayOrder: 0,
+		policies: {},
+		...params,
+	});
+}
+
+export async function resolveRemoteUser(
+	host: Host,
+	id: string,
+	from: LoginUser,
+): Promise<Misskey.entities.UserDetailedNotMe> {
+	const uri = `https://${host}/users/${id}`;
+	return await from.client.request('ap/show', { uri })
+		.then(res => {
+			strictEqual(res.type, 'User');
+			strictEqual(res.object.uri, uri);
+			return res.object;
+		});
+}
+
+export async function resolveRemoteNote(
+	host: Host,
+	id: string,
+	from: LoginUser,
+): Promise<Misskey.entities.Note> {
+	const uri = `https://${host}/notes/${id}`;
+	return await from.client.request('ap/show', { uri })
+		.then(res => {
+			strictEqual(res.type, 'Note');
+			strictEqual(res.object.uri, uri);
+			return res.object;
+		});
+}
+
+export async function uploadFile(
+	host: Host,
+	user: { i: string },
+	path = '../../test/resources/192.jpg',
+): Promise<Misskey.entities.DriveFile> {
+	const filename = path.split('/').pop() ?? 'untitled';
+	const blob = new Blob([await readFile(join(__dirname, path))]);
+
+	const body = new FormData();
+	body.append('i', user.i);
+	body.append('force', 'true');
+	body.append('file', blob);
+	body.append('name', filename);
+
+	return await fetch(`https://${host}/api/drive/files/create`, { method: 'POST', body })
+		.then(async res => await res.json());
+}
+
+export async function addCustomEmoji(
+	host: Host,
+	param?: Partial<Misskey.entities.AdminEmojiAddRequest>,
+	path?: string,
+): Promise<Misskey.entities.EmojiDetailed> {
+	const admin = await fetchAdmin(host);
+	const name = crypto.randomUUID().replaceAll('-', '');
+	const file = await uploadFile(host, admin, path);
+	return await admin.client.request('admin/emoji/add', { name, fileId: file.id, ...param });
+}
+
+export function deepStrictEqualWithExcludedFields<T>(actual: T, expected: T, excludedFields: (keyof T)[]) {
+	const _actual = structuredClone(actual);
+	const _expected = structuredClone(expected);
+	for (const obj of [_actual, _expected]) {
+		for (const field of excludedFields) {
+			delete obj[field];
+		}
+	}
+	deepStrictEqual(_actual, _expected);
+}
+
+export async function isFired<C extends keyof Misskey.Channels, T extends keyof Misskey.Channels[C]['events']>(
+	host: Host,
+	user: { i: string },
+	channel: C,
+	trigger: () => Promise<unknown>,
+	type: T,
+	// @ts-expect-error TODO: why getting error here?
+	cond: (msg: Parameters<Misskey.Channels[C]['events'][T]>[0]) => boolean,
+	params?: Misskey.Channels[C]['params'],
+): Promise<boolean> {
+	return new Promise<boolean>(async (resolve, reject) => {
+		const stream = new Misskey.Stream(`wss://${host}`, { token: user.i }, { WebSocket });
+		const connection = stream.useChannel(channel, params);
+		connection.on(type as any, ((msg: any) => {
+			if (cond(msg)) {
+				stream.close();
+				clearTimeout(timer);
+				resolve(true);
+			}
+		}) as any);
+
+		let timer: NodeJS.Timeout | undefined;
+
+		await trigger().then(() => {
+			timer = setTimeout(() => {
+				stream.close();
+				resolve(false);
+			}, 500);
+		}).catch(err => {
+			stream.close();
+			clearTimeout(timer);
+			reject(err);
+		});
+	});
+};
+
+export async function isNoteUpdatedEventFired(
+	host: Host,
+	user: { i: string },
+	noteId: string,
+	trigger: () => Promise<unknown>,
+	cond: (msg: Parameters<Misskey.StreamEvents['noteUpdated']>[0]) => boolean,
+): Promise<boolean> {
+	return new Promise<boolean>(async (resolve, reject) => {
+		const stream = new Misskey.Stream(`wss://${host}`, { token: user.i }, { WebSocket });
+		stream.send('s', { id: noteId });
+		stream.on('noteUpdated', msg => {
+			if (cond(msg)) {
+				stream.close();
+				clearTimeout(timer);
+				resolve(true);
+			}
+		});
+
+		let timer: NodeJS.Timeout | undefined;
+
+		await trigger().then(() => {
+			timer = setTimeout(() => {
+				stream.close();
+				resolve(false);
+			}, 500);
+		}).catch(err => {
+			stream.close();
+			clearTimeout(timer);
+			reject(err);
+		});
+	});
+};
+
+export async function assertNotificationReceived(
+	receiverHost: Host,
+	receiver: LoginUser,
+	trigger: () => Promise<unknown>,
+	cond: (notification: Misskey.entities.Notification) => boolean,
+	expect: boolean,
+) {
+	const streamingFired = await isFired(receiverHost, receiver, 'main', trigger, 'notification', cond);
+	strictEqual(streamingFired, expect);
+
+	const endpointFired = await receiver.client.request('i/notifications', {})
+		// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
+		.then(([notification]) => notification != null ? cond(notification) : false);
+	strictEqual(endpointFired, expect);
+}
diff --git a/packages/backend/test-federation/tsconfig.json b/packages/backend/test-federation/tsconfig.json
new file mode 100644
index 0000000000..3a1cb3b9f3
--- /dev/null
+++ b/packages/backend/test-federation/tsconfig.json
@@ -0,0 +1,114 @@
+{
+	"compilerOptions": {
+		/* Visit https://aka.ms/tsconfig to read more about this file */
+
+		/* Projects */
+		// "incremental": true,                              /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
+		// "composite": true,                                /* Enable constraints that allow a TypeScript project to be used with project references. */
+		// "tsBuildInfoFile": "./.tsbuildinfo",              /* Specify the path to .tsbuildinfo incremental compilation file. */
+		// "disableSourceOfProjectReferenceRedirect": true,  /* Disable preferring source files instead of declaration files when referencing composite projects. */
+		// "disableSolutionSearching": true,                 /* Opt a project out of multi-project reference checking when editing. */
+		// "disableReferencedProjectLoad": true,             /* Reduce the number of projects loaded automatically by TypeScript. */
+
+		/* Language and Environment */
+		"target": "ESNext",                                  /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
+		// "lib": [],                                        /* Specify a set of bundled library declaration files that describe the target runtime environment. */
+		// "jsx": "preserve",                                /* Specify what JSX code is generated. */
+		// "experimentalDecorators": true,                   /* Enable experimental support for legacy experimental decorators. */
+		// "emitDecoratorMetadata": true,                    /* Emit design-type metadata for decorated declarations in source files. */
+		// "jsxFactory": "",                                 /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
+		// "jsxFragmentFactory": "",                         /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
+		// "jsxImportSource": "",                            /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
+		// "reactNamespace": "",                             /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
+		// "noLib": true,                                    /* Disable including any library files, including the default lib.d.ts. */
+		// "useDefineForClassFields": true,                  /* Emit ECMAScript-standard-compliant class fields. */
+		// "moduleDetection": "auto",                        /* Control what method is used to detect module-format JS files. */
+
+		/* Modules */
+		"module": "NodeNext",                                /* Specify what module code is generated. */
+		// "rootDir": "./",                                  /* Specify the root folder within your source files. */
+		// "moduleResolution": "node10",                     /* Specify how TypeScript looks up a file from a given module specifier. */
+		// "baseUrl": "./",                                  /* Specify the base directory to resolve non-relative module names. */
+		// "paths": {},                                      /* Specify a set of entries that re-map imports to additional lookup locations. */
+		// "rootDirs": [],                                   /* Allow multiple folders to be treated as one when resolving modules. */
+		// "typeRoots": [],                                  /* Specify multiple folders that act like './node_modules/@types'. */
+		// "types": [],                                      /* Specify type package names to be included without being referenced in a source file. */
+		// "allowUmdGlobalAccess": true,                     /* Allow accessing UMD globals from modules. */
+		// "moduleSuffixes": [],                             /* List of file name suffixes to search when resolving a module. */
+		// "allowImportingTsExtensions": true,               /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */
+		// "resolvePackageJsonExports": true,                /* Use the package.json 'exports' field when resolving package imports. */
+		// "resolvePackageJsonImports": true,                /* Use the package.json 'imports' field when resolving imports. */
+		// "customConditions": [],                           /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */
+		// "noUncheckedSideEffectImports": true,             /* Check side effect imports. */
+		// "resolveJsonModule": true,                        /* Enable importing .json files. */
+		// "allowArbitraryExtensions": true,                 /* Enable importing files with any extension, provided a declaration file is present. */
+		// "noResolve": true,                                /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */
+
+		/* JavaScript Support */
+		// "allowJs": true,                                  /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
+		// "checkJs": true,                                  /* Enable error reporting in type-checked JavaScript files. */
+		// "maxNodeModuleJsDepth": 1,                        /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
+
+		/* Emit */
+		// "declaration": true,                              /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
+		// "declarationMap": true,                           /* Create sourcemaps for d.ts files. */
+		// "emitDeclarationOnly": true,                      /* Only output d.ts files and not JavaScript files. */
+		// "sourceMap": true,                                /* Create source map files for emitted JavaScript files. */
+		// "inlineSourceMap": true,                          /* Include sourcemap files inside the emitted JavaScript. */
+		// "noEmit": true,                                   /* Disable emitting files from a compilation. */
+		// "outFile": "./",                                  /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
+		"outDir": "./built",                                 /* Specify an output folder for all emitted files. */
+		// "removeComments": true,                           /* Disable emitting comments. */
+		// "importHelpers": true,                            /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
+		// "downlevelIteration": true,                       /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
+		// "sourceRoot": "",                                 /* Specify the root path for debuggers to find the reference source code. */
+		// "mapRoot": "",                                    /* Specify the location where debugger should locate map files instead of generated locations. */
+		// "inlineSources": true,                            /* Include source code in the sourcemaps inside the emitted JavaScript. */
+		// "emitBOM": true,                                  /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
+		// "newLine": "crlf",                                /* Set the newline character for emitting files. */
+		// "stripInternal": true,                            /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
+		// "noEmitHelpers": true,                            /* Disable generating custom helper functions like '__extends' in compiled output. */
+		// "noEmitOnError": true,                            /* Disable emitting files if any type checking errors are reported. */
+		// "preserveConstEnums": true,                       /* Disable erasing 'const enum' declarations in generated code. */
+		// "declarationDir": "./",                           /* Specify the output directory for generated declaration files. */
+
+		/* Interop Constraints */
+		// "isolatedModules": true,                          /* Ensure that each file can be safely transpiled without relying on other imports. */
+		// "verbatimModuleSyntax": true,                     /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */
+		// "isolatedDeclarations": true,                     /* Require sufficient annotation on exports so other tools can trivially generate declaration files. */
+		// "allowSyntheticDefaultImports": true,             /* Allow 'import x from y' when a module doesn't have a default export. */
+		"esModuleInterop": true,                             /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
+		// "preserveSymlinks": true,                         /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
+		"forceConsistentCasingInFileNames": true,            /* Ensure that casing is correct in imports. */
+
+		/* Type Checking */
+		"strict": true,                                      /* Enable all strict type-checking options. */
+		// "noImplicitAny": true,                            /* Enable error reporting for expressions and declarations with an implied 'any' type. */
+		// "strictNullChecks": true,                         /* When type checking, take into account 'null' and 'undefined'. */
+		// "strictFunctionTypes": true,                      /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
+		// "strictBindCallApply": true,                      /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
+		// "strictPropertyInitialization": true,             /* Check for class properties that are declared but not set in the constructor. */
+		// "strictBuiltinIteratorReturn": true,              /* Built-in iterators are instantiated with a 'TReturn' type of 'undefined' instead of 'any'. */
+		// "noImplicitThis": true,                           /* Enable error reporting when 'this' is given the type 'any'. */
+		// "useUnknownInCatchVariables": true,               /* Default catch clause variables as 'unknown' instead of 'any'. */
+		// "alwaysStrict": true,                             /* Ensure 'use strict' is always emitted. */
+		// "noUnusedLocals": true,                           /* Enable error reporting when local variables aren't read. */
+		// "noUnusedParameters": true,                       /* Raise an error when a function parameter isn't read. */
+		// "exactOptionalPropertyTypes": true,               /* Interpret optional property types as written, rather than adding 'undefined'. */
+		// "noImplicitReturns": true,                        /* Enable error reporting for codepaths that do not explicitly return in a function. */
+		// "noFallthroughCasesInSwitch": true,               /* Enable error reporting for fallthrough cases in switch statements. */
+		// "noUncheckedIndexedAccess": true,                 /* Add 'undefined' to a type when accessed using an index. */
+		// "noImplicitOverride": true,                       /* Ensure overriding members in derived classes are marked with an override modifier. */
+		// "noPropertyAccessFromIndexSignature": true,       /* Enforces using indexed accessors for keys declared using an indexed type. */
+		// "allowUnusedLabels": true,                        /* Disable error reporting for unused labels. */
+		// "allowUnreachableCode": true,                     /* Disable error reporting for unreachable code. */
+
+		/* Completeness */
+		// "skipDefaultLibCheck": true,                      /* Skip type checking .d.ts files that are included with TypeScript. */
+		"skipLibCheck": true                                 /* Skip type checking all .d.ts files. */
+	},
+	"include": [
+		"daemon.ts",
+		"./test/**/*.ts"
+	]
+}
diff --git a/packages/backend/test-server/.eslintrc.cjs b/packages/backend/test-server/.eslintrc.cjs
deleted file mode 100644
index c261741a36..0000000000
--- a/packages/backend/test-server/.eslintrc.cjs
+++ /dev/null
@@ -1,32 +0,0 @@
-module.exports = {
-	parserOptions: {
-		tsconfigRootDir: __dirname,
-		project: ['./tsconfig.json'],
-	},
-	extends: [
-		'../../shared/.eslintrc.js',
-	],
-	rules: {
-		'import/order': ['warn', {
-			'groups': ['builtin', 'external', 'internal', 'parent', 'sibling', 'index', 'object', 'type'],
-			'pathGroups': [
-				{
-					'pattern': '@/**',
-					'group': 'external',
-					'position': 'after'
-				}
-			],
-		}],
-		'no-restricted-globals': [
-			'error',
-			{
-				'name': '__dirname',
-				'message': 'Not in ESModule. Use `import.meta.url` instead.'
-			},
-			{
-				'name': '__filename',
-				'message': 'Not in ESModule. Use `import.meta.url` instead.'
-			}
-	]
-	},
-};
diff --git a/packages/backend/test-server/entry.ts b/packages/backend/test-server/entry.ts
index 866a7e1f5b..04bf62d209 100644
--- a/packages/backend/test-server/entry.ts
+++ b/packages/backend/test-server/entry.ts
@@ -6,12 +6,16 @@ import { MainModule } from '@/MainModule.js';
 import { ServerService } from '@/server/ServerService.js';
 import { loadConfig } from '@/config.js';
 import { NestLogger } from '@/NestLogger.js';
+import { INestApplicationContext } from '@nestjs/common';
 
 const config = loadConfig();
 const originEnv = JSON.stringify(process.env);
 
 process.env.NODE_ENV = 'test';
 
+let app: INestApplicationContext;
+let serverService: ServerService;
+
 /**
  * テスト用のサーバインスタンスを起動する
  */
@@ -20,10 +24,10 @@ async function launch() {
 
 	console.log('starting application...');
 
-	const app = await NestFactory.createApplicationContext(MainModule, {
+	app = await NestFactory.createApplicationContext(MainModule, {
 		logger: new NestLogger(),
 	});
-	const serverService = app.get(ServerService);
+	serverService = app.get(ServerService);
 	await serverService.launch();
 
 	await startControllerEndpoints();
@@ -71,6 +75,20 @@ async function startControllerEndpoints(port = config.port + 1000) {
 
 	fastify.post<{ Body: { key?: string, value?: string } }>('/env-reset', async (req, res) => {
 		process.env = JSON.parse(originEnv);
+
+		await serverService.dispose();
+		await app.close();
+
+		await killTestServer();
+
+		console.log('starting application...');
+
+		app = await NestFactory.createApplicationContext(MainModule, {
+			logger: new NestLogger(),
+		});
+		serverService = app.get(ServerService);
+		await serverService.launch();
+
 		res.code(200).send({ success: true });
 	});
 
diff --git a/packages/backend/test-server/eslint.config.js b/packages/backend/test-server/eslint.config.js
new file mode 100644
index 0000000000..b9c16d469f
--- /dev/null
+++ b/packages/backend/test-server/eslint.config.js
@@ -0,0 +1,43 @@
+import tsParser from '@typescript-eslint/parser';
+import sharedConfig from '../../shared/eslint.config.js';
+
+export default [
+	...sharedConfig,
+	{
+		files: ['**/*.ts', '**/*.tsx'],
+		languageOptions: {
+			parserOptions: {
+				parser: tsParser,
+				project: ['./tsconfig.json'],
+				sourceType: 'module',
+				tsconfigRootDir: import.meta.dirname,
+			},
+		},
+		rules: {
+			'import/order': ['warn', {
+				groups: [
+					'builtin',
+					'external',
+					'internal',
+					'parent',
+					'sibling',
+					'index',
+					'object',
+					'type',
+				],
+				pathGroups: [{
+					pattern: '@/**',
+					group: 'external',
+					position: 'after',
+				}],
+			}],
+			'no-restricted-globals': ['error', {
+				name: '__dirname',
+				message: 'Not in ESModule. Use `import.meta.url` instead.',
+			}, {
+				name: '__filename',
+				message: 'Not in ESModule. Use `import.meta.url` instead.',
+			}],
+		},
+	},
+];
diff --git a/packages/backend/test/.eslintrc.cjs b/packages/backend/test/.eslintrc.cjs
deleted file mode 100644
index 41ecea0c3f..0000000000
--- a/packages/backend/test/.eslintrc.cjs
+++ /dev/null
@@ -1,11 +0,0 @@
-module.exports = {
-	parserOptions: {
-		tsconfigRootDir: __dirname,
-		project: ['./tsconfig.json'],
-	},
-	extends: ['../.eslintrc.cjs'],
-	env: {
-		node: true,
-		jest: true,
-	},
-};
diff --git a/packages/backend/test/docker-compose.yml b/packages/backend/test/compose.yml
similarity index 94%
rename from packages/backend/test/docker-compose.yml
rename to packages/backend/test/compose.yml
index f2d8990758..6593fc33dd 100644
--- a/packages/backend/test/docker-compose.yml
+++ b/packages/backend/test/compose.yml
@@ -1,5 +1,3 @@
-version: "3"
-
 services:
   redistest:
     image: redis:7
diff --git a/packages/backend/test/e2e/2fa.ts b/packages/backend/test/e2e/2fa.ts
index 13c56b88a6..48e1bababb 100644
--- a/packages/backend/test/e2e/2fa.ts
+++ b/packages/backend/test/e2e/2fa.ts
@@ -136,13 +136,7 @@ describe('2要素認証', () => {
 		keyName: string,
 		credentialId: Buffer,
 		requestOptions: PublicKeyCredentialRequestOptionsJSON,
-	}): {
-		username: string,
-		password: string,
-		credential: AuthenticationResponseJSON,
-		'g-recaptcha-response'?: string | null,
-		'hcaptcha-response'?: string | null,
-	} => {
+	}): misskey.entities.SigninFlowRequest => {
 		// AuthenticatorAssertionResponse.authenticatorData
 		// https://developer.mozilla.org/en-US/docs/Web/API/AuthenticatorAssertionResponse/authenticatorData
 		const authenticatorData = Buffer.concat([
@@ -202,17 +196,21 @@ describe('2要素認証', () => {
 		}, alice);
 		assert.strictEqual(doneResponse.status, 200);
 
-		const usersShowResponse = await api('users/show', {
-			username,
-		}, alice);
-		assert.strictEqual(usersShowResponse.status, 200);
-		assert.strictEqual(usersShowResponse.body.twoFactorEnabled, true);
+		const signinWithoutTokenResponse = await api('signin-flow', {
+			...signinParam(),
+		});
+		assert.strictEqual(signinWithoutTokenResponse.status, 200);
+		assert.deepStrictEqual(signinWithoutTokenResponse.body, {
+			finished: false,
+			next: 'totp',
+		});
 
-		const signinResponse = await api('signin', {
+		const signinResponse = await api('signin-flow', {
 			...signinParam(),
 			token: otpToken(registerResponse.body.secret),
 		});
 		assert.strictEqual(signinResponse.status, 200);
+		assert.strictEqual(signinResponse.body.finished, true);
 		assert.notEqual(signinResponse.body.i, undefined);
 
 		// 後片付け
@@ -248,32 +246,28 @@ describe('2要素認証', () => {
 			keyName,
 			credentialId,
 			creationOptions: registerKeyResponse.body,
-		}) as any, alice);
+		} as any) as any, alice);
 		assert.strictEqual(keyDoneResponse.status, 200);
 		assert.strictEqual(keyDoneResponse.body.id, credentialId.toString('base64url'));
 		assert.strictEqual(keyDoneResponse.body.name, keyName);
 
-		const usersShowResponse = await api('users/show', {
-			username,
-		});
-		assert.strictEqual(usersShowResponse.status, 200);
-		assert.strictEqual(usersShowResponse.body.securityKeys, true);
-
-		const signinResponse = await api('signin', {
+		const signinResponse = await api('signin-flow', {
 			...signinParam(),
 		});
 		assert.strictEqual(signinResponse.status, 200);
-		assert.strictEqual(signinResponse.body.i, undefined);
-		assert.notEqual(signinResponse.body.challenge, undefined);
-		assert.notEqual(signinResponse.body.allowCredentials, undefined);
-		assert.strictEqual(signinResponse.body.allowCredentials[0].id, credentialId.toString('base64url'));
+		assert.strictEqual(signinResponse.body.finished, false);
+		assert.strictEqual(signinResponse.body.next, 'passkey');
+		assert.notEqual(signinResponse.body.authRequest.challenge, undefined);
+		assert.notEqual(signinResponse.body.authRequest.allowCredentials, undefined);
+		assert.strictEqual(signinResponse.body.authRequest.allowCredentials && signinResponse.body.authRequest.allowCredentials[0]?.id, credentialId.toString('base64url'));
 
-		const signinResponse2 = await api('signin', signinWithSecurityKeyParam({
+		const signinResponse2 = await api('signin-flow', signinWithSecurityKeyParam({
 			keyName,
 			credentialId,
-			requestOptions: signinResponse.body,
+			requestOptions: signinResponse.body.authRequest,
 		}));
 		assert.strictEqual(signinResponse2.status, 200);
+		assert.strictEqual(signinResponse2.body.finished, true);
 		assert.notEqual(signinResponse2.body.i, undefined);
 
 		// 後片付け
@@ -307,7 +301,7 @@ describe('2要素認証', () => {
 			keyName,
 			credentialId,
 			creationOptions: registerKeyResponse.body,
-		}) as any, alice);
+		} as any) as any, alice);
 		assert.strictEqual(keyDoneResponse.status, 200);
 
 		const passwordLessResponse = await api('i/2fa/password-less', {
@@ -315,28 +309,30 @@ describe('2要素認証', () => {
 		}, alice);
 		assert.strictEqual(passwordLessResponse.status, 204);
 
-		const usersShowResponse = await api('users/show', {
-			username,
-		});
-		assert.strictEqual(usersShowResponse.status, 200);
-		assert.strictEqual(usersShowResponse.body.usePasswordLessLogin, true);
+		const iResponse = await api('i', {}, alice);
+		assert.strictEqual(iResponse.status, 200);
+		assert.strictEqual(iResponse.body.usePasswordLessLogin, true);
 
-		const signinResponse = await api('signin', {
+		const signinResponse = await api('signin-flow', {
 			...signinParam(),
 			password: '',
 		});
 		assert.strictEqual(signinResponse.status, 200);
-		assert.strictEqual(signinResponse.body.i, undefined);
+		assert.strictEqual(signinResponse.body.finished, false);
+		assert.strictEqual(signinResponse.body.next, 'passkey');
+		assert.notEqual(signinResponse.body.authRequest.challenge, undefined);
+		assert.notEqual(signinResponse.body.authRequest.allowCredentials, undefined);
 
-		const signinResponse2 = await api('signin', {
+		const signinResponse2 = await api('signin-flow', {
 			...signinWithSecurityKeyParam({
 				keyName,
 				credentialId,
-				requestOptions: signinResponse.body,
-			}),
+				requestOptions: signinResponse.body.authRequest,
+			} as any),
 			password: '',
 		});
 		assert.strictEqual(signinResponse2.status, 200);
+		assert.strictEqual(signinResponse2.body.finished, true);
 		assert.notEqual(signinResponse2.body.i, undefined);
 
 		// 後片付け
@@ -370,7 +366,7 @@ describe('2要素認証', () => {
 			keyName,
 			credentialId,
 			creationOptions: registerKeyResponse.body,
-		}) as any, alice);
+		} as any) as any, alice);
 		assert.strictEqual(keyDoneResponse.status, 200);
 
 		const renamedKey = 'other-key';
@@ -383,6 +379,7 @@ describe('2要素認証', () => {
 		const iResponse = await api('i', {
 		}, alice);
 		assert.strictEqual(iResponse.status, 200);
+		assert.ok(iResponse.body.securityKeysList);
 		const securityKeys = iResponse.body.securityKeysList.filter((s: { id: string; }) => s.id === credentialId.toString('base64url'));
 		assert.strictEqual(securityKeys.length, 1);
 		assert.strictEqual(securityKeys[0].name, renamedKey);
@@ -419,14 +416,15 @@ describe('2要素認証', () => {
 			keyName,
 			credentialId,
 			creationOptions: registerKeyResponse.body,
-		}) as any, alice);
+		} as any) as any, alice);
 		assert.strictEqual(keyDoneResponse.status, 200);
 
 		// テストの実行順によっては複数残ってるので全部消す
-		const iResponse = await api('i', {
+		const beforeIResponse = await api('i', {
 		}, alice);
-		assert.strictEqual(iResponse.status, 200);
-		for (const key of iResponse.body.securityKeysList) {
+		assert.strictEqual(beforeIResponse.status, 200);
+		assert.ok(beforeIResponse.body.securityKeysList);
+		for (const key of beforeIResponse.body.securityKeysList) {
 			const removeKeyResponse = await api('i/2fa/remove-key', {
 				token: otpToken(registerResponse.body.secret),
 				password,
@@ -435,17 +433,16 @@ describe('2要素認証', () => {
 			assert.strictEqual(removeKeyResponse.status, 200);
 		}
 
-		const usersShowResponse = await api('users/show', {
-			username,
-		});
-		assert.strictEqual(usersShowResponse.status, 200);
-		assert.strictEqual(usersShowResponse.body.securityKeys, false);
+		const afterIResponse = await api('i', {}, alice);
+		assert.strictEqual(afterIResponse.status, 200);
+		assert.strictEqual(afterIResponse.body.securityKeys, false);
 
-		const signinResponse = await api('signin', {
+		const signinResponse = await api('signin-flow', {
 			...signinParam(),
 			token: otpToken(registerResponse.body.secret),
 		});
 		assert.strictEqual(signinResponse.status, 200);
+		assert.strictEqual(signinResponse.body.finished, true);
 		assert.notEqual(signinResponse.body.i, undefined);
 
 		// 後片付け
@@ -466,11 +463,9 @@ describe('2要素認証', () => {
 		}, alice);
 		assert.strictEqual(doneResponse.status, 200);
 
-		const usersShowResponse = await api('users/show', {
-			username,
-		});
-		assert.strictEqual(usersShowResponse.status, 200);
-		assert.strictEqual(usersShowResponse.body.twoFactorEnabled, true);
+		const iResponse = await api('i', {}, alice);
+		assert.strictEqual(iResponse.status, 200);
+		assert.strictEqual(iResponse.body.twoFactorEnabled, true);
 
 		const unregisterResponse = await api('i/2fa/unregister', {
 			token: otpToken(registerResponse.body.secret),
@@ -478,10 +473,11 @@ describe('2要素認証', () => {
 		}, alice);
 		assert.strictEqual(unregisterResponse.status, 204);
 
-		const signinResponse = await api('signin', {
+		const signinResponse = await api('signin-flow', {
 			...signinParam(),
 		});
 		assert.strictEqual(signinResponse.status, 200);
+		assert.strictEqual(signinResponse.body.finished, true);
 		assert.notEqual(signinResponse.body.i, undefined);
 
 		// 後片付け
diff --git a/packages/backend/test/e2e/antennas.ts b/packages/backend/test/e2e/antennas.ts
index 101238b601..a544db955a 100644
--- a/packages/backend/test/e2e/antennas.ts
+++ b/packages/backend/test/e2e/antennas.ts
@@ -163,8 +163,7 @@ describe('アンテナ', () => {
 	});
 
 	test('が上限いっぱいまで作成できること', async () => {
-		// antennaLimit + 1まで作れるのがキモ
-		const response = await Promise.all([...Array(DEFAULT_POLICIES.antennaLimit + 1)].map(() => successfulApiCall({
+		const response = await Promise.all([...Array(DEFAULT_POLICIES.antennaLimit)].map(() => successfulApiCall({
 			endpoint: 'antennas/create',
 			parameters: { ...defaultParam },
 			user: alice,
@@ -229,6 +228,17 @@ describe('アンテナ', () => {
 		assert.deepStrictEqual(response, expected);
 	});
 
+	test('を作成する時キーワードが指定されていないとエラーになる', async () => {
+		await failedApiCall({
+			endpoint: 'antennas/create',
+			parameters: { ...defaultParam, keywords: [[]], excludeKeywords: [[]] },
+			user: alice
+		}, {
+			status: 400,
+			code: 'EMPTY_KEYWORD',
+			id: '53ee222e-1ddd-4f9a-92e5-9fb82ddb463a'
+		})
+	});
 	//#endregion
 	//#region 更新(antennas/update)
 
@@ -256,6 +266,18 @@ describe('アンテナ', () => {
 			id: '1c6b35c9-943e-48c2-81e4-2844989407f7',
 		});
 	});
+	test('を変更する時キーワードが指定されていないとエラーになる', async () => {
+		const antenna = await successfulApiCall({ endpoint: 'antennas/create', parameters: defaultParam, user: alice });
+		await failedApiCall({
+			endpoint: 'antennas/update',
+			parameters: { ...defaultParam, antennaId: antenna.id, keywords: [[]], excludeKeywords: [[]] },
+			user: alice
+		}, {
+			status: 400,
+			code: 'EMPTY_KEYWORD',
+			id: '721aaff6-4e1b-4d88-8de6-877fae9f68c4'
+		})
+	});
 
 	//#endregion
 	//#region 表示(antennas/show)
diff --git a/packages/backend/test/e2e/api-visibility.ts b/packages/backend/test/e2e/api-visibility.ts
index c61b0c2a86..2dd645d97a 100644
--- a/packages/backend/test/e2e/api-visibility.ts
+++ b/packages/backend/test/e2e/api-visibility.ts
@@ -410,21 +410,21 @@ describe('API visibility', () => {
 		test('[HTL] public-post が 自分が見れる', async () => {
 			const res = await api('notes/timeline', { limit: 100 }, alice);
 			assert.strictEqual(res.status, 200);
-			const notes = res.body.filter((n: any) => n.id === pub.id);
+			const notes = res.body.filter(n => n.id === pub.id);
 			assert.strictEqual(notes[0].text, 'x');
 		});
 
 		test('[HTL] public-post が 非フォロワーから見れない', async () => {
 			const res = await api('notes/timeline', { limit: 100 }, other);
 			assert.strictEqual(res.status, 200);
-			const notes = res.body.filter((n: any) => n.id === pub.id);
+			const notes = res.body.filter(n => n.id === pub.id);
 			assert.strictEqual(notes.length, 0);
 		});
 
 		test('[HTL] followers-post が フォロワーから見れる', async () => {
 			const res = await api('notes/timeline', { limit: 100 }, follower);
 			assert.strictEqual(res.status, 200);
-			const notes = res.body.filter((n: any) => n.id === fol.id);
+			const notes = res.body.filter(n => n.id === fol.id);
 			assert.strictEqual(notes[0].text, 'x');
 		});
 		//#endregion
@@ -433,21 +433,21 @@ describe('API visibility', () => {
 		test('[replies] followers-reply が フォロワーから見れる', async () => {
 			const res = await api('notes/replies', { noteId: tgt.id, limit: 100 }, follower);
 			assert.strictEqual(res.status, 200);
-			const notes = res.body.filter((n: any) => n.id === folR.id);
+			const notes = res.body.filter(n => n.id === folR.id);
 			assert.strictEqual(notes[0].text, 'x');
 		});
 
 		test('[replies] followers-reply が 非フォロワー (リプライ先ではない) から見れない', async () => {
 			const res = await api('notes/replies', { noteId: tgt.id, limit: 100 }, other);
 			assert.strictEqual(res.status, 200);
-			const notes = res.body.filter((n: any) => n.id === folR.id);
+			const notes = res.body.filter(n => n.id === folR.id);
 			assert.strictEqual(notes.length, 0);
 		});
 
 		test('[replies] followers-reply が 非フォロワー (リプライ先である) から見れる', async () => {
 			const res = await api('notes/replies', { noteId: tgt.id, limit: 100 }, target);
 			assert.strictEqual(res.status, 200);
-			const notes = res.body.filter((n: any) => n.id === folR.id);
+			const notes = res.body.filter(n => n.id === folR.id);
 			assert.strictEqual(notes[0].text, 'x');
 		});
 		//#endregion
@@ -456,14 +456,14 @@ describe('API visibility', () => {
 		test('[mentions] followers-reply が 非フォロワー (リプライ先である) から見れる', async () => {
 			const res = await api('notes/mentions', { limit: 100 }, target);
 			assert.strictEqual(res.status, 200);
-			const notes = res.body.filter((n: any) => n.id === folR.id);
+			const notes = res.body.filter(n => n.id === folR.id);
 			assert.strictEqual(notes[0].text, 'x');
 		});
 
 		test('[mentions] followers-mention が 非フォロワー (メンション先である) から見れる', async () => {
 			const res = await api('notes/mentions', { limit: 100 }, target);
 			assert.strictEqual(res.status, 200);
-			const notes = res.body.filter((n: any) => n.id === folM.id);
+			const notes = res.body.filter(n => n.id === folM.id);
 			assert.strictEqual(notes[0].text, '@target x');
 		});
 		//#endregion
diff --git a/packages/backend/test/e2e/block.ts b/packages/backend/test/e2e/block.ts
index e4f798498f..35b0e59383 100644
--- a/packages/backend/test/e2e/block.ts
+++ b/packages/backend/test/e2e/block.ts
@@ -6,7 +6,7 @@
 process.env.NODE_ENV = 'test';
 
 import * as assert from 'assert';
-import { api, post, signup } from '../utils.js';
+import { api, castAsError, post, signup } from '../utils.js';
 import type * as misskey from 'misskey-js';
 
 describe('Block', () => {
@@ -33,7 +33,7 @@ describe('Block', () => {
 		const res = await api('following/create', { userId: alice.id }, bob);
 
 		assert.strictEqual(res.status, 400);
-		assert.strictEqual(res.body.error.id, 'c4ab57cc-4e41-45e9-bfd9-584f61e35ce0');
+		assert.strictEqual(castAsError(res.body).error.id, 'c4ab57cc-4e41-45e9-bfd9-584f61e35ce0');
 	});
 
 	test('ブロックされているユーザーにリアクションできない', async () => {
@@ -42,7 +42,8 @@ describe('Block', () => {
 		const res = await api('notes/reactions/create', { noteId: note.id, reaction: '👍' }, bob);
 
 		assert.strictEqual(res.status, 400);
-		assert.strictEqual(res.body.error.id, '20ef5475-9f38-4e4c-bd33-de6d979498ec');
+		assert.ok(res.body);
+		assert.strictEqual(castAsError(res.body).error.id, '20ef5475-9f38-4e4c-bd33-de6d979498ec');
 	});
 
 	test('ブロックされているユーザーに返信できない', async () => {
@@ -51,7 +52,8 @@ describe('Block', () => {
 		const res = await api('notes/create', { replyId: note.id, text: 'yo' }, bob);
 
 		assert.strictEqual(res.status, 400);
-		assert.strictEqual(res.body.error.id, 'b390d7e1-8a5e-46ed-b625-06271cafd3d3');
+		assert.ok(res.body);
+		assert.strictEqual(castAsError(res.body).error.id, 'b390d7e1-8a5e-46ed-b625-06271cafd3d3');
 	});
 
 	test('ブロックされているユーザーのノートをRenoteできない', async () => {
@@ -60,7 +62,7 @@ describe('Block', () => {
 		const res = await api('notes/create', { renoteId: note.id, text: 'yo' }, bob);
 
 		assert.strictEqual(res.status, 400);
-		assert.strictEqual(res.body.error.id, 'b390d7e1-8a5e-46ed-b625-06271cafd3d3');
+		assert.strictEqual(castAsError(res.body).error.id, 'b390d7e1-8a5e-46ed-b625-06271cafd3d3');
 	});
 
 	// TODO: ユーザーリストに入れられないテスト
diff --git a/packages/backend/test/e2e/clips.ts b/packages/backend/test/e2e/clips.ts
index ba6f9d6a65..a130c3698d 100644
--- a/packages/backend/test/e2e/clips.ts
+++ b/packages/backend/test/e2e/clips.ts
@@ -79,14 +79,14 @@ describe('クリップ', () => {
 	};
 
 	const deleteClip = async (parameters: Misskey.entities.ClipsDeleteRequest, request: Partial<ApiRequest<'clips/delete'>> = {}): Promise<void> => {
-		return await successfulApiCall({
+		await successfulApiCall({
 			endpoint: 'clips/delete',
 			parameters,
 			user: alice,
 			...request,
 		}, {
 			status: 204,
-		}) as any as void;
+		});
 	};
 
 	const show = async (parameters: Misskey.entities.ClipsShowRequest, request: Partial<ApiRequest<'clips/show'>> = {}): Promise<Misskey.entities.Clip> => {
@@ -153,8 +153,7 @@ describe('クリップ', () => {
 	});
 
 	test('の作成はポリシーで定められた数以上はできない。', async () => {
-		// ポリシー + 1まで作れるという所がミソ
-		const clipLimit = DEFAULT_POLICIES.clipLimit + 1;
+		const clipLimit = DEFAULT_POLICIES.clipLimit;
 		for (let i = 0; i < clipLimit; i++) {
 			await create();
 		}
@@ -327,7 +326,7 @@ describe('クリップ', () => {
 	});
 
 	test('の一覧(clips/list)が取得できる(上限いっぱい)', async () => {
-		const clipLimit = DEFAULT_POLICIES.clipLimit + 1;
+		const clipLimit = DEFAULT_POLICIES.clipLimit;
 		const clips = await createMany({}, clipLimit);
 		const res = await list({
 			parameters: { limit: 1 }, // FIXME: 無視されて11全部返ってくる
@@ -455,25 +454,25 @@ describe('クリップ', () => {
 		let aliceClip: Misskey.entities.Clip;
 
 		const favorite = async (parameters: Misskey.entities.ClipsFavoriteRequest, request: Partial<ApiRequest<'clips/favorite'>> = {}): Promise<void> => {
-			return successfulApiCall({
+			await successfulApiCall({
 				endpoint: 'clips/favorite',
 				parameters,
 				user: alice,
 				...request,
 			}, {
 				status: 204,
-			}) as any as void;
+			});
 		};
 
 		const unfavorite = async (parameters: Misskey.entities.ClipsUnfavoriteRequest, request: Partial<ApiRequest<'clips/unfavorite'>> = {}): Promise<void> => {
-			return successfulApiCall({
+			await successfulApiCall({
 				endpoint: 'clips/unfavorite',
 				parameters,
 				user: alice,
 				...request,
 			}, {
 				status: 204,
-			}) as any as void;
+			});
 		};
 
 		const myFavorites = async (request: Partial<ApiRequest<'clips/my-favorites'>> = {}): Promise<Misskey.entities.Clip[]> => {
@@ -705,7 +704,7 @@ describe('クリップ', () => {
 
 		// TODO: 17000msくらいかかる...
 		test('をポリシーで定められた上限いっぱい(200)を超えて追加はできない。', async () => {
-			const noteLimit = DEFAULT_POLICIES.noteEachClipsLimit + 1;
+			const noteLimit = DEFAULT_POLICIES.noteEachClipsLimit;
 			const noteList = await Promise.all([...Array(noteLimit)].map((_, i) => post(alice, {
 				text: `test ${i}`,
 			}) as unknown)) as Misskey.entities.Note[];
diff --git a/packages/backend/test/e2e/drive.ts b/packages/backend/test/e2e/drive.ts
index 828c5200ef..43a73163eb 100644
--- a/packages/backend/test/e2e/drive.ts
+++ b/packages/backend/test/e2e/drive.ts
@@ -23,7 +23,7 @@ describe('Drive', () => {
 
 		const marker = Math.random().toString();
 
-		const url = 'https://raw.githubusercontent.com/misskey-dev/misskey/develop/packages/backend/test/resources/Lenna.jpg';
+		const url = 'https://raw.githubusercontent.com/misskey-dev/misskey/develop/packages/backend/test/resources/192.jpg';
 
 		const catcher = makeStreamCatcher(
 			alice,
@@ -41,14 +41,14 @@ describe('Drive', () => {
 		const file = await catcher;
 
 		assert.strictEqual(res.status, 204);
-		assert.strictEqual(file.name, 'Lenna.jpg');
+		assert.strictEqual(file.name, '192.jpg');
 		assert.strictEqual(file.type, 'image/jpeg');
 	});
 
 	test('ローカルからアップロードできる', async () => {
 		// APIレスポンスを直接使用するので utils.js uploadFile が通過することで成功とする
 
-		const res = await uploadFile(alice, { path: 'Lenna.jpg', name: 'テスト画像' });
+		const res = await uploadFile(alice, { path: '192.jpg', name: 'テスト画像' });
 
 		assert.strictEqual(res.body?.name, 'テスト画像.jpg');
 		assert.strictEqual(res.body.type, 'image/jpeg');
diff --git a/packages/backend/test/e2e/endpoints.ts b/packages/backend/test/e2e/endpoints.ts
index bc89dc37f4..b91d77c398 100644
--- a/packages/backend/test/e2e/endpoints.ts
+++ b/packages/backend/test/e2e/endpoints.ts
@@ -10,7 +10,7 @@ import * as assert from 'assert';
 // https://github.com/node-fetch/node-fetch/pull/1664
 import { Blob } from 'node-fetch';
 import { MiUser } from '@/models/_.js';
-import { api, initTestDb, post, signup, simpleGet, uploadFile } from '../utils.js';
+import { api, castAsError, initTestDb, post, signup, simpleGet, uploadFile } from '../utils.js';
 import type * as misskey from 'misskey-js';
 
 describe('Endpoints', () => {
@@ -66,9 +66,9 @@ describe('Endpoints', () => {
 		});
 	});
 
-	describe('signin', () => {
+	describe('signin-flow', () => {
 		test('間違ったパスワードでサインインできない', async () => {
-			const res = await api('signin', {
+			const res = await api('signin-flow', {
 				username: 'test1',
 				password: 'bar',
 			});
@@ -77,7 +77,7 @@ describe('Endpoints', () => {
 		});
 
 		test('クエリをインジェクションできない', async () => {
-			const res = await api('signin', {
+			const res = await api('signin-flow', {
 				username: 'test1',
 				// @ts-expect-error password must be string
 				password: {
@@ -89,7 +89,7 @@ describe('Endpoints', () => {
 		});
 
 		test('正しい情報でサインインできる', async () => {
-			const res = await api('signin', {
+			const res = await api('signin-flow', {
 				username: 'test1',
 				password: 'test1',
 			});
@@ -117,12 +117,21 @@ describe('Endpoints', () => {
 			assert.strictEqual(res.body.birthday, myBirthday);
 		});
 
-		test('名前を空白にできる', async () => {
+		test('名前を空白のみにした場合nullになる', async () => {
 			const res = await api('i/update', {
 				name: ' ',
 			}, alice);
 			assert.strictEqual(res.status, 200);
-			assert.strictEqual(res.body.name, ' ');
+			assert.strictEqual(res.body.name, null);
+		});
+
+		test('名前の前後に空白(ホワイトスペース)を入れてもトリムされる', async () => {
+			const res = await api('i/update', {
+				// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Lexical_grammar#white_space
+				name: ' あ い う \u0009\u000b\u000c\u0020\u00a0\u1680\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\ufeff',
+			}, alice);
+			assert.strictEqual(res.status, 200);
+			assert.strictEqual(res.body.name, 'あ い う');
 		});
 
 		test('誕生日の設定を削除できる', async () => {
@@ -155,7 +164,7 @@ describe('Endpoints', () => {
 
 			assert.strictEqual(res.status, 200);
 			assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
-			assert.strictEqual(res.body.id, alice.id);
+			assert.strictEqual((res.body as unknown as { id: string }).id, alice.id);
 		});
 
 		test('ユーザーが存在しなかったら怒る', async () => {
@@ -266,6 +275,68 @@ describe('Endpoints', () => {
 			assert.strictEqual(res.status, 400);
 		});
 
+		test('リノートにリアクションできない', async () => {
+			const bobNote = await post(bob, { text: 'hi' });
+			const bobRenote = await post(bob, { renoteId: bobNote.id });
+
+			const res = await api('notes/reactions/create', {
+				noteId: bobRenote.id,
+				reaction: '🚀',
+			}, alice);
+
+			assert.strictEqual(res.status, 400);
+			assert.ok(res.body);
+			assert.strictEqual(castAsError(res.body).error.code, 'CANNOT_REACT_TO_RENOTE');
+		});
+
+		test('引用にリアクションできる', async () => {
+			const bobNote = await post(bob, { text: 'hi' });
+			const bobRenote = await post(bob, { text: 'hi again', renoteId: bobNote.id });
+
+			const res = await api('notes/reactions/create', {
+				noteId: bobRenote.id,
+				reaction: '🚀',
+			}, alice);
+
+			assert.strictEqual(res.status, 204);
+		});
+
+		test('空文字列のリアクションは\u2764にフォールバックされる', async () => {
+			const bobNote = await post(bob, { text: 'hi' });
+
+			const res = await api('notes/reactions/create', {
+				noteId: bobNote.id,
+				reaction: '',
+			}, alice);
+
+			assert.strictEqual(res.status, 204);
+
+			const reaction = await api('notes/reactions', {
+				noteId: bobNote.id,
+			});
+
+			assert.strictEqual(reaction.body.length, 1);
+			assert.strictEqual(reaction.body[0].type, '\u2764');
+		});
+
+		test('絵文字ではない文字列のリアクションは\u2764にフォールバックされる', async () => {
+			const bobNote = await post(bob, { text: 'hi' });
+
+			const res = await api('notes/reactions/create', {
+				noteId: bobNote.id,
+				reaction: 'Hello!',
+			}, alice);
+
+			assert.strictEqual(res.status, 204);
+
+			const reaction = await api('notes/reactions', {
+				noteId: bobNote.id,
+			});
+
+			assert.strictEqual(reaction.body.length, 1);
+			assert.strictEqual(reaction.body[0].type, '\u2764');
+		});
+
 		test('空のパラメータで怒られる', async () => {
 			// @ts-expect-error param must not be empty
 			const res = await api('notes/reactions/create', {}, alice);
@@ -523,7 +594,7 @@ describe('Endpoints', () => {
 
 			assert.strictEqual(res.status, 200);
 			assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
-			assert.strictEqual(res.body!.name, 'Lenna.jpg');
+			assert.strictEqual(res.body!.name, '192.jpg');
 		});
 
 		test('ファイルに名前を付けられる', async () => {
@@ -993,7 +1064,7 @@ describe('Endpoints', () => {
 				userId: bob.id,
 			}, alice);
 			assert.strictEqual(res1.status, 204);
-			assert.strictEqual(res2.body?.memo, memo);
+			assert.strictEqual((res2.body as unknown as { memo: string })?.memo, memo);
 		});
 
 		test('自分に関するメモを更新できる', async () => {
@@ -1008,7 +1079,7 @@ describe('Endpoints', () => {
 				userId: alice.id,
 			}, alice);
 			assert.strictEqual(res1.status, 204);
-			assert.strictEqual(res2.body?.memo, memo);
+			assert.strictEqual((res2.body as unknown as { memo: string })?.memo, memo);
 		});
 
 		test('メモを削除できる', async () => {
@@ -1029,7 +1100,7 @@ describe('Endpoints', () => {
 			}, alice);
 
 			// memoには常に文字列かnullが入っている(5cac151)
-			assert.strictEqual(res.body.memo, null);
+			assert.strictEqual((res.body as unknown as { memo: string | null }).memo, null);
 		});
 
 		test('メモは個人ごとに独立して保存される', async () => {
@@ -1056,8 +1127,8 @@ describe('Endpoints', () => {
 				}, carol),
 			]);
 
-			assert.strictEqual(resAlice.body.memo, memoAliceToBob);
-			assert.strictEqual(resCarol.body.memo, memoCarolToBob);
+			assert.strictEqual((resAlice.body as unknown as { memo: string }).memo, memoAliceToBob);
+			assert.strictEqual((resCarol.body as unknown as { memo: string }).memo, memoCarolToBob);
 		});
 	});
 });
diff --git a/packages/backend/test/e2e/exports.ts b/packages/backend/test/e2e/exports.ts
index 80a5331a6d..4bcecc9716 100644
--- a/packages/backend/test/e2e/exports.ts
+++ b/packages/backend/test/e2e/exports.ts
@@ -61,14 +61,14 @@ describe('export-clips', () => {
 	});
 
 	test('basic export', async () => {
-		let res = await api('clips/create', {
+		const res1 = await api('clips/create', {
 			name: 'foo',
 			description: 'bar',
 		}, alice);
-		assert.strictEqual(res.status, 200);
+		assert.strictEqual(res1.status, 200);
 
-		res = await api('i/export-clips', {}, alice);
-		assert.strictEqual(res.status, 204);
+		const res2 = await api('i/export-clips', {}, alice);
+		assert.strictEqual(res2.status, 204);
 
 		const exported = await pollFirstDriveFile();
 		assert.strictEqual(exported[0].name, 'foo');
@@ -77,7 +77,7 @@ describe('export-clips', () => {
 	});
 
 	test('export with notes', async () => {
-		let res = await api('clips/create', {
+		const res = await api('clips/create', {
 			name: 'foo',
 			description: 'bar',
 		}, alice);
@@ -96,15 +96,15 @@ describe('export-clips', () => {
 		});
 
 		for (const note of [note1, note2]) {
-			res = await api('clips/add-note', {
+			const res2 = await api('clips/add-note', {
 				clipId: clip.id,
 				noteId: note.id,
 			}, alice);
-			assert.strictEqual(res.status, 204);
+			assert.strictEqual(res2.status, 204);
 		}
 
-		res = await api('i/export-clips', {}, alice);
-		assert.strictEqual(res.status, 204);
+		const res3 = await api('i/export-clips', {}, alice);
+		assert.strictEqual(res3.status, 204);
 
 		const exported = await pollFirstDriveFile();
 		assert.strictEqual(exported[0].name, 'foo');
@@ -116,19 +116,19 @@ describe('export-clips', () => {
 	});
 
 	test('multiple clips', async () => {
-		let res = await api('clips/create', {
+		const res1 = await api('clips/create', {
 			name: 'kawaii',
 			description: 'kawaii',
 		}, alice);
-		assert.strictEqual(res.status, 200);
-		const clip1 = res.body;
+		assert.strictEqual(res1.status, 200);
+		const clip1 = res1.body;
 
-		res = await api('clips/create', {
+		const res2 = await api('clips/create', {
 			name: 'yuri',
 			description: 'yuri',
 		}, alice);
-		assert.strictEqual(res.status, 200);
-		const clip2 = res.body;
+		assert.strictEqual(res2.status, 200);
+		const clip2 = res2.body;
 
 		const note1 = await post(alice, {
 			text: 'baz1',
@@ -138,20 +138,26 @@ describe('export-clips', () => {
 			text: 'baz2',
 		});
 
-		res = await api('clips/add-note', {
-			clipId: clip1.id,
-			noteId: note1.id,
-		}, alice);
-		assert.strictEqual(res.status, 204);
+		{
+			const res = await api('clips/add-note', {
+				clipId: clip1.id,
+				noteId: note1.id,
+			}, alice);
+			assert.strictEqual(res.status, 204);
+		}
 
-		res = await api('clips/add-note', {
-			clipId: clip2.id,
-			noteId: note2.id,
-		}, alice);
-		assert.strictEqual(res.status, 204);
+		{
+			const res = await api('clips/add-note', {
+				clipId: clip2.id,
+				noteId: note2.id,
+			}, alice);
+			assert.strictEqual(res.status, 204);
+		}
 
-		res = await api('i/export-clips', {}, alice);
-		assert.strictEqual(res.status, 204);
+		{
+			const res = await api('i/export-clips', {}, alice);
+			assert.strictEqual(res.status, 204);
+		}
 
 		const exported = await pollFirstDriveFile();
 		assert.strictEqual(exported[0].name, 'kawaii');
@@ -163,7 +169,7 @@ describe('export-clips', () => {
 	});
 
 	test('Clipping other user\'s note', async () => {
-		let res = await api('clips/create', {
+		const res = await api('clips/create', {
 			name: 'kawaii',
 			description: 'kawaii',
 		}, alice);
@@ -175,14 +181,14 @@ describe('export-clips', () => {
 			visibility: 'followers',
 		});
 
-		res = await api('clips/add-note', {
+		const res2 = await api('clips/add-note', {
 			clipId: clip.id,
 			noteId: note.id,
 		}, alice);
-		assert.strictEqual(res.status, 204);
+		assert.strictEqual(res2.status, 204);
 
-		res = await api('i/export-clips', {}, alice);
-		assert.strictEqual(res.status, 204);
+		const res3 = await api('i/export-clips', {}, alice);
+		assert.strictEqual(res3.status, 204);
 
 		const exported = await pollFirstDriveFile();
 		assert.strictEqual(exported[0].name, 'kawaii');
diff --git a/packages/backend/test/e2e/fetch-resource.ts b/packages/backend/test/e2e/fetch-resource.ts
index 4851ed14be..7efd688ec2 100644
--- a/packages/backend/test/e2e/fetch-resource.ts
+++ b/packages/backend/test/e2e/fetch-resource.ts
@@ -153,6 +153,23 @@ describe('Webリソース', () => {
 			path: path('nonexisting'),
 			status: 404,
 		}));
+
+		describe(' has entry such ', () => {
+			beforeEach(() => {
+				post(alice, { text: "**a**" })
+			});
+
+			test('MFMを含まない。', async () => {
+				const content = await simpleGet(path(alice.username), "*/*", undefined, res => res.text());
+				const _body: unknown = content.body;
+				// JSONフィードのときは改めて文字列化する
+				const body: string = typeof (_body) === "object" ? JSON.stringify(_body) : _body as string;
+
+				if (body.includes("**a**")) {
+					throw new Error("MFM shouldn't be included");
+				}
+			});
+		})
 	});
 
 	describe.each([{ path: '/api/foo' }])('$path', ({ path }) => {
diff --git a/packages/backend/test/e2e/move.ts b/packages/backend/test/e2e/move.ts
index 74cf61a785..fd798bdb25 100644
--- a/packages/backend/test/e2e/move.ts
+++ b/packages/backend/test/e2e/move.ts
@@ -7,19 +7,20 @@ import { INestApplicationContext } from '@nestjs/common';
 
 process.env.NODE_ENV = 'test';
 
+import { setTimeout } from 'node:timers/promises';
 import * as assert from 'assert';
 import { loadConfig } from '@/config.js';
 import { MiRepository, MiUser, UsersRepository, miRepository } from '@/models/_.js';
 import { secureRndstr } from '@/misc/secure-rndstr.js';
 import { jobQueue } from '@/boot/common.js';
-import { api, initTestDb, signup, sleep, successfulApiCall, uploadFile } from '../utils.js';
+import { api, castAsError, initTestDb, signup, successfulApiCall, uploadFile } from '../utils.js';
 import type * as misskey from 'misskey-js';
 
 describe('Account Move', () => {
 	let jq: INestApplicationContext;
 	let url: URL;
 
-	let root: any;
+	let root: misskey.entities.SignupResponse;
 	let alice: misskey.entities.SignupResponse;
 	let bob: misskey.entities.SignupResponse;
 	let carol: misskey.entities.SignupResponse;
@@ -92,8 +93,8 @@ describe('Account Move', () => {
 			}, bob);
 
 			assert.strictEqual(res.status, 400);
-			assert.strictEqual(res.body.error.code, 'NO_SUCH_USER');
-			assert.strictEqual(res.body.error.id, 'fcd2eef9-a9b2-4c4f-8624-038099e90aa5');
+			assert.strictEqual(castAsError(res.body).error.code, 'NO_SUCH_USER');
+			assert.strictEqual(castAsError(res.body).error.id, 'fcd2eef9-a9b2-4c4f-8624-038099e90aa5');
 		});
 
 		test('Unable to add duplicated aliases to alsoKnownAs', async () => {
@@ -102,8 +103,8 @@ describe('Account Move', () => {
 			}, bob);
 
 			assert.strictEqual(res.status, 400);
-			assert.strictEqual(res.body.error.code, 'INVALID_PARAM');
-			assert.strictEqual(res.body.error.id, '3d81ceae-475f-4600-b2a8-2bc116157532');
+			assert.strictEqual(castAsError(res.body).error.code, 'INVALID_PARAM');
+			assert.strictEqual(castAsError(res.body).error.id, '3d81ceae-475f-4600-b2a8-2bc116157532');
 		});
 
 		test('Unable to add itself', async () => {
@@ -112,8 +113,8 @@ describe('Account Move', () => {
 			}, bob);
 
 			assert.strictEqual(res.status, 400);
-			assert.strictEqual(res.body.error.code, 'FORBIDDEN_TO_SET_YOURSELF');
-			assert.strictEqual(res.body.error.id, '25c90186-4ab0-49c8-9bba-a1fa6c202ba4');
+			assert.strictEqual(castAsError(res.body).error.code, 'FORBIDDEN_TO_SET_YOURSELF');
+			assert.strictEqual(castAsError(res.body).error.id, '25c90186-4ab0-49c8-9bba-a1fa6c202ba4');
 		});
 
 		test('Unable to add a nonexisting local account to alsoKnownAs', async () => {
@@ -122,16 +123,16 @@ describe('Account Move', () => {
 			}, bob);
 
 			assert.strictEqual(res1.status, 400);
-			assert.strictEqual(res1.body.error.code, 'NO_SUCH_USER');
-			assert.strictEqual(res1.body.error.id, 'fcd2eef9-a9b2-4c4f-8624-038099e90aa5');
+			assert.strictEqual(castAsError(res1.body).error.code, 'NO_SUCH_USER');
+			assert.strictEqual(castAsError(res1.body).error.id, 'fcd2eef9-a9b2-4c4f-8624-038099e90aa5');
 
 			const res2 = await api('i/update', {
 				alsoKnownAs: ['@alice', 'nonexist'],
 			}, bob);
 
 			assert.strictEqual(res2.status, 400);
-			assert.strictEqual(res2.body.error.code, 'NO_SUCH_USER');
-			assert.strictEqual(res2.body.error.id, 'fcd2eef9-a9b2-4c4f-8624-038099e90aa5');
+			assert.strictEqual(castAsError(res2.body).error.code, 'NO_SUCH_USER');
+			assert.strictEqual(castAsError(res2.body).error.id, 'fcd2eef9-a9b2-4c4f-8624-038099e90aa5');
 		});
 
 		test('Able to add two existing local account to alsoKnownAs', async () => {
@@ -240,8 +241,8 @@ describe('Account Move', () => {
 			}, root);
 
 			assert.strictEqual(res.status, 400);
-			assert.strictEqual(res.body.error.code, 'NOT_ROOT_FORBIDDEN');
-			assert.strictEqual(res.body.error.id, '4362e8dc-731f-4ad8-a694-be2a88922a24');
+			assert.strictEqual(castAsError(res.body).error.code, 'NOT_ROOT_FORBIDDEN');
+			assert.strictEqual(castAsError(res.body).error.id, '4362e8dc-731f-4ad8-a694-be2a88922a24');
 		});
 
 		test('Unable to move to a nonexisting local account', async () => {
@@ -250,8 +251,8 @@ describe('Account Move', () => {
 			}, alice);
 
 			assert.strictEqual(res.status, 400);
-			assert.strictEqual(res.body.error.code, 'NO_SUCH_USER');
-			assert.strictEqual(res.body.error.id, 'fcd2eef9-a9b2-4c4f-8624-038099e90aa5');
+			assert.strictEqual(castAsError(res.body).error.code, 'NO_SUCH_USER');
+			assert.strictEqual(castAsError(res.body).error.id, 'fcd2eef9-a9b2-4c4f-8624-038099e90aa5');
 		});
 
 		test('Unable to move if alsoKnownAs is invalid', async () => {
@@ -260,8 +261,8 @@ describe('Account Move', () => {
 			}, alice);
 
 			assert.strictEqual(res.status, 400);
-			assert.strictEqual(res.body.error.code, 'DESTINATION_ACCOUNT_FORBIDS');
-			assert.strictEqual(res.body.error.id, 'b5c90186-4ab0-49c8-9bba-a1f766282ba4');
+			assert.strictEqual(castAsError(res.body).error.code, 'DESTINATION_ACCOUNT_FORBIDS');
+			assert.strictEqual(castAsError(res.body).error.id, 'b5c90186-4ab0-49c8-9bba-a1f766282ba4');
 		});
 
 		test('Relationships have been properly migrated', async () => {
@@ -271,43 +272,51 @@ describe('Account Move', () => {
 
 			assert.strictEqual(move.status, 200);
 
-			await sleep(1000 * 3); // wait for jobs to finish
+			await setTimeout(1000 * 3); // wait for jobs to finish
 
 			// Unfollow delayed?
 			const aliceFollowings = await api('users/following', {
 				userId: alice.id,
 			}, alice);
 			assert.strictEqual(aliceFollowings.status, 200);
+			assert.ok(aliceFollowings);
 			assert.strictEqual(aliceFollowings.body.length, 3);
 
 			const carolFollowings = await api('users/following', {
 				userId: carol.id,
 			}, carol);
 			assert.strictEqual(carolFollowings.status, 200);
+			assert.ok(carolFollowings);
 			assert.strictEqual(carolFollowings.body.length, 2);
 			assert.strictEqual(carolFollowings.body[0].followeeId, bob.id);
 			assert.strictEqual(carolFollowings.body[1].followeeId, alice.id);
 
 			const blockings = await api('blocking/list', {}, dave);
 			assert.strictEqual(blockings.status, 200);
+			assert.ok(blockings);
 			assert.strictEqual(blockings.body.length, 2);
 			assert.strictEqual(blockings.body[0].blockeeId, bob.id);
 			assert.strictEqual(blockings.body[1].blockeeId, alice.id);
 
 			const mutings = await api('mute/list', {}, dave);
 			assert.strictEqual(mutings.status, 200);
+			assert.ok(mutings);
 			assert.strictEqual(mutings.body.length, 2);
 			assert.strictEqual(mutings.body[0].muteeId, bob.id);
 			assert.strictEqual(mutings.body[1].muteeId, alice.id);
 
 			const rootLists = await api('users/lists/list', {}, root);
 			assert.strictEqual(rootLists.status, 200);
+			assert.ok(rootLists);
+			assert.ok(rootLists.body[0].userIds);
 			assert.strictEqual(rootLists.body[0].userIds.length, 2);
 			assert.ok(rootLists.body[0].userIds.find((id: string) => id === bob.id));
 			assert.ok(rootLists.body[0].userIds.find((id: string) => id === alice.id));
 
 			const eveLists = await api('users/lists/list', {}, eve);
 			assert.strictEqual(eveLists.status, 200);
+			assert.ok(eveLists);
+			assert.ok(eveLists.body[0].userIds);
 			assert.strictEqual(eveLists.body[0].userIds.length, 1);
 			assert.ok(eveLists.body[0].userIds.find((id: string) => id === bob.id));
 		});
@@ -330,7 +339,7 @@ describe('Account Move', () => {
 		});
 
 		test('Unfollowed after 10 sec (24 hours in production).', async () => {
-			await sleep(1000 * 8);
+			await setTimeout(1000 * 8);
 
 			const following = await api('users/following', {
 				userId: alice.id,
@@ -346,8 +355,8 @@ describe('Account Move', () => {
 			}, bob);
 
 			assert.strictEqual(res.status, 400);
-			assert.strictEqual(res.body.error.code, 'DESTINATION_ACCOUNT_FORBIDS');
-			assert.strictEqual(res.body.error.id, 'b5c90186-4ab0-49c8-9bba-a1f766282ba4');
+			assert.strictEqual(castAsError(res.body).error.code, 'DESTINATION_ACCOUNT_FORBIDS');
+			assert.strictEqual(castAsError(res.body).error.id, 'b5c90186-4ab0-49c8-9bba-a1f766282ba4');
 		});
 
 		test('Follow and follower counts are properly adjusted', async () => {
@@ -418,8 +427,9 @@ describe('Account Move', () => {
 		] as const)('Prohibit access after moving: %s', async (endpoint) => {
 			const res = await api(endpoint, {}, alice);
 			assert.strictEqual(res.status, 403);
-			assert.strictEqual(res.body.error.code, 'YOUR_ACCOUNT_MOVED');
-			assert.strictEqual(res.body.error.id, '56f20ec9-fd06-4fa5-841b-edd6d7d4fa31');
+			assert.ok(res.body);
+			assert.strictEqual(castAsError(res.body).error.code, 'YOUR_ACCOUNT_MOVED');
+			assert.strictEqual(castAsError(res.body).error.id, '56f20ec9-fd06-4fa5-841b-edd6d7d4fa31');
 		});
 
 		test('Prohibit access after moving: /antennas/update', async () => {
@@ -437,16 +447,19 @@ describe('Account Move', () => {
 			}, alice);
 
 			assert.strictEqual(res.status, 403);
-			assert.strictEqual(res.body.error.code, 'YOUR_ACCOUNT_MOVED');
-			assert.strictEqual(res.body.error.id, '56f20ec9-fd06-4fa5-841b-edd6d7d4fa31');
+			assert.ok(res.body);
+			assert.strictEqual(castAsError(res.body).error.code, 'YOUR_ACCOUNT_MOVED');
+			assert.strictEqual(castAsError(res.body).error.id, '56f20ec9-fd06-4fa5-841b-edd6d7d4fa31');
 		});
 
 		test('Prohibit access after moving: /drive/files/create', async () => {
+			// FIXME: 一旦逃げておく
 			const res = await uploadFile(alice);
 
 			assert.strictEqual(res.status, 403);
-			assert.strictEqual((res.body! as any as { error: misskey.api.APIError }).error.code, 'YOUR_ACCOUNT_MOVED');
-			assert.strictEqual((res.body! as any as { error: misskey.api.APIError }).error.id, '56f20ec9-fd06-4fa5-841b-edd6d7d4fa31');
+			assert.ok(res.body);
+			assert.strictEqual(castAsError(res.body).error.code, 'YOUR_ACCOUNT_MOVED');
+			assert.strictEqual(castAsError(res.body).error.id, '56f20ec9-fd06-4fa5-841b-edd6d7d4fa31');
 		});
 
 		test('Prohibit updating alsoKnownAs after moving', async () => {
@@ -455,8 +468,8 @@ describe('Account Move', () => {
 			}, alice);
 
 			assert.strictEqual(res.status, 403);
-			assert.strictEqual(res.body.error.code, 'YOUR_ACCOUNT_MOVED');
-			assert.strictEqual(res.body.error.id, '56f20ec9-fd06-4fa5-841b-edd6d7d4fa31');
+			assert.strictEqual(castAsError(res.body).error.code, 'YOUR_ACCOUNT_MOVED');
+			assert.strictEqual(castAsError(res.body).error.id, '56f20ec9-fd06-4fa5-841b-edd6d7d4fa31');
 		});
 	});
 });
diff --git a/packages/backend/test/e2e/mute.ts b/packages/backend/test/e2e/mute.ts
index 0e52c5decc..f37da288b7 100644
--- a/packages/backend/test/e2e/mute.ts
+++ b/packages/backend/test/e2e/mute.ts
@@ -47,8 +47,8 @@ describe('Mute', () => {
 
 		assert.strictEqual(res.status, 200);
 		assert.strictEqual(Array.isArray(res.body), true);
-		assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
-		assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false);
+		assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
+		assert.strictEqual(res.body.some(note => note.id === carolNote.id), false);
 	});
 
 	test('ミュートしているユーザーからメンションされても、hasUnreadMentions が true にならない', async () => {
@@ -92,9 +92,9 @@ describe('Mute', () => {
 
 			assert.strictEqual(res.status, 200);
 			assert.strictEqual(Array.isArray(res.body), true);
-			assert.strictEqual(res.body.some((note: any) => note.id === aliceNote.id), true);
-			assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
-			assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false);
+			assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true);
+			assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
+			assert.strictEqual(res.body.some(note => note.id === carolNote.id), false);
 		});
 
 		test('タイムラインにミュートしているユーザーの投稿のRenoteが含まれない', async () => {
@@ -108,9 +108,9 @@ describe('Mute', () => {
 
 			assert.strictEqual(res.status, 200);
 			assert.strictEqual(Array.isArray(res.body), true);
-			assert.strictEqual(res.body.some((note: any) => note.id === aliceNote.id), true);
-			assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
-			assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false);
+			assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true);
+			assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
+			assert.strictEqual(res.body.some(note => note.id === carolNote.id), false);
 		});
 	});
 
@@ -124,8 +124,8 @@ describe('Mute', () => {
 
 			assert.strictEqual(res.status, 200);
 			assert.strictEqual(Array.isArray(res.body), true);
-			assert.strictEqual(res.body.some((notification: any) => notification.userId === bob.id), true);
-			assert.strictEqual(res.body.some((notification: any) => notification.userId === carol.id), false);
+			assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === bob.id), true);
+			assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === carol.id), false);
 		});
 
 		test('通知にミュートしているユーザーからのリプライが含まれない', async () => {
@@ -138,8 +138,8 @@ describe('Mute', () => {
 			assert.strictEqual(res.status, 200);
 			assert.strictEqual(Array.isArray(res.body), true);
 
-			assert.strictEqual(res.body.some((notification: any) => notification.userId === bob.id), true);
-			assert.strictEqual(res.body.some((notification: any) => notification.userId === carol.id), false);
+			assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === bob.id), true);
+			assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === carol.id), false);
 		});
 
 		test('通知にミュートしているユーザーからのリプライが含まれない', async () => {
@@ -152,8 +152,8 @@ describe('Mute', () => {
 			assert.strictEqual(res.status, 200);
 			assert.strictEqual(Array.isArray(res.body), true);
 
-			assert.strictEqual(res.body.some((notification: any) => notification.userId === bob.id), true);
-			assert.strictEqual(res.body.some((notification: any) => notification.userId === carol.id), false);
+			assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === bob.id), true);
+			assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === carol.id), false);
 		});
 
 		test('通知にミュートしているユーザーからの引用リノートが含まれない', async () => {
@@ -166,8 +166,8 @@ describe('Mute', () => {
 			assert.strictEqual(res.status, 200);
 			assert.strictEqual(Array.isArray(res.body), true);
 
-			assert.strictEqual(res.body.some((notification: any) => notification.userId === bob.id), true);
-			assert.strictEqual(res.body.some((notification: any) => notification.userId === carol.id), false);
+			assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === bob.id), true);
+			assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === carol.id), false);
 		});
 
 		test('通知にミュートしているユーザーからのリノートが含まれない', async () => {
@@ -180,8 +180,8 @@ describe('Mute', () => {
 			assert.strictEqual(res.status, 200);
 			assert.strictEqual(Array.isArray(res.body), true);
 
-			assert.strictEqual(res.body.some((notification: any) => notification.userId === bob.id), true);
-			assert.strictEqual(res.body.some((notification: any) => notification.userId === carol.id), false);
+			assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === bob.id), true);
+			assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === carol.id), false);
 		});
 
 		test('通知にミュートしているユーザーからのフォロー通知が含まれない', async () => {
@@ -193,8 +193,8 @@ describe('Mute', () => {
 			assert.strictEqual(res.status, 200);
 			assert.strictEqual(Array.isArray(res.body), true);
 
-			assert.strictEqual(res.body.some((notification: any) => notification.userId === bob.id), true);
-			assert.strictEqual(res.body.some((notification: any) => notification.userId === carol.id), false);
+			assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === bob.id), true);
+			assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === carol.id), false);
 
 			await api('following/delete', { userId: alice.id }, bob);
 			await api('following/delete', { userId: alice.id }, carol);
@@ -210,8 +210,8 @@ describe('Mute', () => {
 			assert.strictEqual(res.status, 200);
 			assert.strictEqual(Array.isArray(res.body), true);
 
-			assert.strictEqual(res.body.some((notification: any) => notification.userId === bob.id), true);
-			assert.strictEqual(res.body.some((notification: any) => notification.userId === carol.id), false);
+			assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === bob.id), true);
+			assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === carol.id), false);
 
 			await api('following/delete', { userId: alice.id }, bob);
 			await api('following/delete', { userId: alice.id }, carol);
@@ -228,8 +228,8 @@ describe('Mute', () => {
 
 			assert.strictEqual(res.status, 200);
 			assert.strictEqual(Array.isArray(res.body), true);
-			assert.strictEqual(res.body.some((notification: any) => notification.userId === bob.id), true);
-			assert.strictEqual(res.body.some((notification: any) => notification.userId === carol.id), false);
+			assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === bob.id), true);
+			assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === carol.id), false);
 		});
 		test('通知にミュートしているユーザーからのリプライが含まれない', async () => {
 			const aliceNote = await post(alice, { text: 'hi' });
@@ -241,8 +241,8 @@ describe('Mute', () => {
 			assert.strictEqual(res.status, 200);
 			assert.strictEqual(Array.isArray(res.body), true);
 
-			assert.strictEqual(res.body.some((notification: any) => notification.userId === bob.id), true);
-			assert.strictEqual(res.body.some((notification: any) => notification.userId === carol.id), false);
+			assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === bob.id), true);
+			assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === carol.id), false);
 		});
 
 		test('通知にミュートしているユーザーからのリプライが含まれない', async () => {
@@ -255,8 +255,8 @@ describe('Mute', () => {
 			assert.strictEqual(res.status, 200);
 			assert.strictEqual(Array.isArray(res.body), true);
 
-			assert.strictEqual(res.body.some((notification: any) => notification.userId === bob.id), true);
-			assert.strictEqual(res.body.some((notification: any) => notification.userId === carol.id), false);
+			assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === bob.id), true);
+			assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === carol.id), false);
 		});
 
 		test('通知にミュートしているユーザーからの引用リノートが含まれない', async () => {
@@ -269,8 +269,8 @@ describe('Mute', () => {
 			assert.strictEqual(res.status, 200);
 			assert.strictEqual(Array.isArray(res.body), true);
 
-			assert.strictEqual(res.body.some((notification: any) => notification.userId === bob.id), true);
-			assert.strictEqual(res.body.some((notification: any) => notification.userId === carol.id), false);
+			assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === bob.id), true);
+			assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === carol.id), false);
 		});
 
 		test('通知にミュートしているユーザーからのリノートが含まれない', async () => {
@@ -283,8 +283,8 @@ describe('Mute', () => {
 			assert.strictEqual(res.status, 200);
 			assert.strictEqual(Array.isArray(res.body), true);
 
-			assert.strictEqual(res.body.some((notification: any) => notification.userId === bob.id), true);
-			assert.strictEqual(res.body.some((notification: any) => notification.userId === carol.id), false);
+			assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === bob.id), true);
+			assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === carol.id), false);
 		});
 
 		test('通知にミュートしているユーザーからのフォロー通知が含まれない', async () => {
@@ -296,8 +296,8 @@ describe('Mute', () => {
 			assert.strictEqual(res.status, 200);
 			assert.strictEqual(Array.isArray(res.body), true);
 
-			assert.strictEqual(res.body.some((notification: any) => notification.userId === bob.id), true);
-			assert.strictEqual(res.body.some((notification: any) => notification.userId === carol.id), false);
+			assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === bob.id), true);
+			assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === carol.id), false);
 
 			await api('following/delete', { userId: alice.id }, bob);
 			await api('following/delete', { userId: alice.id }, carol);
@@ -313,8 +313,8 @@ describe('Mute', () => {
 			assert.strictEqual(res.status, 200);
 			assert.strictEqual(Array.isArray(res.body), true);
 
-			assert.strictEqual(res.body.some((notification: any) => notification.userId === bob.id), true);
-			assert.strictEqual(res.body.some((notification: any) => notification.userId === carol.id), false);
+			assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === bob.id), true);
+			assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === carol.id), false);
 		});
 	});
 });
diff --git a/packages/backend/test/e2e/note.ts b/packages/backend/test/e2e/note.ts
index bda31d9640..5937eb9b49 100644
--- a/packages/backend/test/e2e/note.ts
+++ b/packages/backend/test/e2e/note.ts
@@ -3,16 +3,18 @@
  * SPDX-License-Identifier: AGPL-3.0-only
  */
 
+import type { Repository } from "typeorm";
+
 process.env.NODE_ENV = 'test';
 
 import * as assert from 'assert';
 import { MiNote } from '@/models/Note.js';
 import { MAX_NOTE_TEXT_LENGTH } from '@/const.js';
-import { api, initTestDb, post, role, signup, uploadFile, uploadUrl } from '../utils.js';
+import { api, castAsError, initTestDb, post, role, signup, uploadFile, uploadUrl } from '../utils.js';
 import type * as misskey from 'misskey-js';
 
 describe('Note', () => {
-	let Notes: any;
+	let Notes: Repository<MiNote>;
 
 	let root: misskey.entities.SignupResponse;
 	let alice: misskey.entities.SignupResponse;
@@ -41,7 +43,7 @@ describe('Note', () => {
 	});
 
 	test('ファイルを添付できる', async () => {
-		const file = await uploadUrl(alice, 'https://raw.githubusercontent.com/misskey-dev/misskey/develop/packages/backend/test/resources/Lenna.jpg');
+		const file = await uploadUrl(alice, 'https://raw.githubusercontent.com/misskey-dev/misskey/develop/packages/backend/test/resources/192.jpg');
 
 		const res = await api('notes/create', {
 			fileIds: [file.id],
@@ -53,7 +55,7 @@ describe('Note', () => {
 	}, 1000 * 10);
 
 	test('他人のファイルで怒られる', async () => {
-		const file = await uploadUrl(bob, 'https://raw.githubusercontent.com/misskey-dev/misskey/develop/packages/backend/test/resources/Lenna.jpg');
+		const file = await uploadUrl(bob, 'https://raw.githubusercontent.com/misskey-dev/misskey/develop/packages/backend/test/resources/192.jpg');
 
 		const res = await api('notes/create', {
 			text: 'test',
@@ -61,8 +63,8 @@ describe('Note', () => {
 		}, alice);
 
 		assert.strictEqual(res.status, 400);
-		assert.strictEqual(res.body.error.code, 'NO_SUCH_FILE');
-		assert.strictEqual(res.body.error.id, 'b6992544-63e7-67f0-fa7f-32444b1b5306');
+		assert.strictEqual(castAsError(res.body).error.code, 'NO_SUCH_FILE');
+		assert.strictEqual(castAsError(res.body).error.id, 'b6992544-63e7-67f0-fa7f-32444b1b5306');
 	}, 1000 * 10);
 
 	test('存在しないファイルで怒られる', async () => {
@@ -72,8 +74,8 @@ describe('Note', () => {
 		}, alice);
 
 		assert.strictEqual(res.status, 400);
-		assert.strictEqual(res.body.error.code, 'NO_SUCH_FILE');
-		assert.strictEqual(res.body.error.id, 'b6992544-63e7-67f0-fa7f-32444b1b5306');
+		assert.strictEqual(castAsError(res.body).error.code, 'NO_SUCH_FILE');
+		assert.strictEqual(castAsError(res.body).error.id, 'b6992544-63e7-67f0-fa7f-32444b1b5306');
 	});
 
 	test('不正なファイルIDで怒られる', async () => {
@@ -81,8 +83,8 @@ describe('Note', () => {
 			fileIds: ['kyoppie'],
 		}, alice);
 		assert.strictEqual(res.status, 400);
-		assert.strictEqual(res.body.error.code, 'NO_SUCH_FILE');
-		assert.strictEqual(res.body.error.id, 'b6992544-63e7-67f0-fa7f-32444b1b5306');
+		assert.strictEqual(castAsError(res.body).error.code, 'NO_SUCH_FILE');
+		assert.strictEqual(castAsError(res.body).error.id, 'b6992544-63e7-67f0-fa7f-32444b1b5306');
 	});
 
 	test('返信できる', async () => {
@@ -101,6 +103,7 @@ describe('Note', () => {
 		assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
 		assert.strictEqual(res.body.createdNote.text, alicePost.text);
 		assert.strictEqual(res.body.createdNote.replyId, alicePost.replyId);
+		assert.ok(res.body.createdNote.reply);
 		assert.strictEqual(res.body.createdNote.reply.text, bobPost.text);
 	});
 
@@ -118,6 +121,7 @@ describe('Note', () => {
 		assert.strictEqual(res.status, 200);
 		assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
 		assert.strictEqual(res.body.createdNote.renoteId, alicePost.renoteId);
+		assert.ok(res.body.createdNote.renote);
 		assert.strictEqual(res.body.createdNote.renote.text, bobPost.text);
 	});
 
@@ -137,6 +141,7 @@ describe('Note', () => {
 		assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
 		assert.strictEqual(res.body.createdNote.text, alicePost.text);
 		assert.strictEqual(res.body.createdNote.renoteId, alicePost.renoteId);
+		assert.ok(res.body.createdNote.renote);
 		assert.strictEqual(res.body.createdNote.renote.text, bobPost.text);
 	});
 
@@ -218,7 +223,7 @@ describe('Note', () => {
 		}, bob);
 
 		assert.strictEqual(bobReply.status, 400);
-		assert.strictEqual(bobReply.body.error.code, 'CANNOT_REPLY_TO_AN_INVISIBLE_NOTE');
+		assert.strictEqual(castAsError(bobReply.body).error.code, 'CANNOT_REPLY_TO_AN_INVISIBLE_NOTE');
 	});
 
 	test('visibility: specifiedなノートに対してvisibility: specifiedで返信できる', async () => {
@@ -256,7 +261,7 @@ describe('Note', () => {
 		}, bob);
 
 		assert.strictEqual(bobReply.status, 400);
-		assert.strictEqual(bobReply.body.error.code, 'CANNOT_REPLY_TO_SPECIFIED_VISIBILITY_NOTE_WITH_EXTENDED_VISIBILITY');
+		assert.strictEqual(castAsError(bobReply.body).error.code, 'CANNOT_REPLY_TO_SPECIFIED_VISIBILITY_NOTE_WITH_EXTENDED_VISIBILITY');
 	});
 
 	test('文字数ぎりぎりで怒られない', async () => {
@@ -333,6 +338,7 @@ describe('Note', () => {
 		assert.strictEqual(res.body.createdNote.text, post.text);
 
 		const noteDoc = await Notes.findOneBy({ id: res.body.createdNote.id });
+		assert.ok(noteDoc);
 		assert.deepStrictEqual(noteDoc.mentions, [bob.id]);
 	});
 
@@ -345,6 +351,7 @@ describe('Note', () => {
 
 			assert.strictEqual(res.status, 200);
 			assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
+			assert.ok(res.body.createdNote.files);
 			assert.strictEqual(res.body.createdNote.files.length, 1);
 			assert.strictEqual(res.body.createdNote.files[0].id, file.body!.id);
 		});
@@ -363,8 +370,9 @@ describe('Note', () => {
 
 			assert.strictEqual(res.status, 200);
 			assert.strictEqual(Array.isArray(res.body), true);
-			const myNote = res.body.find((note: { id: string; files: { id: string }[] }) => note.id === createdNote.body.createdNote.id);
-			assert.notEqual(myNote, null);
+			const myNote = res.body.find(note => note.id === createdNote.body.createdNote.id);
+			assert.ok(myNote);
+			assert.ok(myNote.files);
 			assert.strictEqual(myNote.files.length, 1);
 			assert.strictEqual(myNote.files[0].id, file.body!.id);
 		});
@@ -389,7 +397,9 @@ describe('Note', () => {
 			assert.strictEqual(res.status, 200);
 			assert.strictEqual(Array.isArray(res.body), true);
 			const myNote = res.body.find((note: { id: string }) => note.id === renoted.body.createdNote.id);
-			assert.notEqual(myNote, null);
+			assert.ok(myNote);
+			assert.ok(myNote.renote);
+			assert.ok(myNote.renote.files);
 			assert.strictEqual(myNote.renote.files.length, 1);
 			assert.strictEqual(myNote.renote.files[0].id, file.body!.id);
 		});
@@ -415,7 +425,9 @@ describe('Note', () => {
 			assert.strictEqual(res.status, 200);
 			assert.strictEqual(Array.isArray(res.body), true);
 			const myNote = res.body.find((note: { id: string }) => note.id === reply.body.createdNote.id);
-			assert.notEqual(myNote, null);
+			assert.ok(myNote);
+			assert.ok(myNote.reply);
+			assert.ok(myNote.reply.files);
 			assert.strictEqual(myNote.reply.files.length, 1);
 			assert.strictEqual(myNote.reply.files[0].id, file.body!.id);
 		});
@@ -446,7 +458,10 @@ describe('Note', () => {
 			assert.strictEqual(res.status, 200);
 			assert.strictEqual(Array.isArray(res.body), true);
 			const myNote = res.body.find((note: { id: string }) => note.id === renoted.body.createdNote.id);
-			assert.notEqual(myNote, null);
+			assert.ok(myNote);
+			assert.ok(myNote.renote);
+			assert.ok(myNote.renote.reply);
+			assert.ok(myNote.renote.reply.files);
 			assert.strictEqual(myNote.renote.reply.files.length, 1);
 			assert.strictEqual(myNote.renote.reply.files[0].id, file.body!.id);
 		});
@@ -474,7 +489,7 @@ describe('Note', () => {
 						priority: 0,
 						value: true,
 					},
-				} as any,
+				},
 			}, root);
 
 			assert.strictEqual(res.status, 200);
@@ -498,7 +513,7 @@ describe('Note', () => {
 			}, alice);
 
 			assert.strictEqual(liftnsfw.status, 400);
-			assert.strictEqual(liftnsfw.body.error.code, 'RESTRICTED_BY_ROLE');
+			assert.strictEqual(castAsError(liftnsfw.body).error.code, 'RESTRICTED_BY_ROLE');
 
 			const oldaddnsfw = await api('drive/files/update', {
 				fileId: file.body!.id,
@@ -710,7 +725,7 @@ describe('Note', () => {
 			}, alice);
 
 			assert.strictEqual(note1.status, 400);
-			assert.strictEqual(note1.body.error.code, 'CONTAINS_PROHIBITED_WORDS');
+			assert.strictEqual(castAsError(note1.body).error.code, 'CONTAINS_PROHIBITED_WORDS');
 		});
 
 		test('禁止ワードを含む投稿はエラーになる (正規表現)', async () => {
@@ -727,7 +742,7 @@ describe('Note', () => {
 			}, alice);
 
 			assert.strictEqual(note2.status, 400);
-			assert.strictEqual(note2.body.error.code, 'CONTAINS_PROHIBITED_WORDS');
+			assert.strictEqual(castAsError(note2.body).error.code, 'CONTAINS_PROHIBITED_WORDS');
 		});
 
 		test('禁止ワードを含む投稿はエラーになる (スペースアンド)', async () => {
@@ -744,7 +759,7 @@ describe('Note', () => {
 			}, alice);
 
 			assert.strictEqual(note2.status, 400);
-			assert.strictEqual(note2.body.error.code, 'CONTAINS_PROHIBITED_WORDS');
+			assert.strictEqual(castAsError(note2.body).error.code, 'CONTAINS_PROHIBITED_WORDS');
 		});
 
 		test('禁止ワードを含んでるリモートノートもエラーになる', async () => {
@@ -786,7 +801,7 @@ describe('Note', () => {
 						priority: 1,
 						value: 0,
 					},
-				} as any,
+				},
 			}, root);
 
 			assert.strictEqual(res.status, 200);
@@ -807,7 +822,7 @@ describe('Note', () => {
 			}, alice);
 
 			assert.strictEqual(note.status, 400);
-			assert.strictEqual(note.body.error.code, 'CONTAINS_TOO_MANY_MENTIONS');
+			assert.strictEqual(castAsError(note.body).error.code, 'CONTAINS_TOO_MANY_MENTIONS');
 
 			await api('admin/roles/unassign', {
 				userId: alice.id,
@@ -840,7 +855,7 @@ describe('Note', () => {
 						priority: 1,
 						value: 0,
 					},
-				} as any,
+				},
 			}, root);
 
 			assert.strictEqual(res.status, 200);
@@ -863,7 +878,7 @@ describe('Note', () => {
 			}, alice);
 
 			assert.strictEqual(note.status, 400);
-			assert.strictEqual(note.body.error.code, 'CONTAINS_TOO_MANY_MENTIONS');
+			assert.strictEqual(castAsError(note.body).error.code, 'CONTAINS_TOO_MANY_MENTIONS');
 
 			await api('admin/roles/unassign', {
 				userId: alice.id,
@@ -896,7 +911,7 @@ describe('Note', () => {
 						priority: 1,
 						value: 1,
 					},
-				} as any,
+				},
 			}, root);
 
 			assert.strictEqual(res.status, 200);
@@ -951,6 +966,7 @@ describe('Note', () => {
 
 			assert.strictEqual(deleteOneRes.status, 204);
 			let mainNote = await Notes.findOneBy({ id: mainNoteRes.body.createdNote.id });
+			assert.ok(mainNote);
 			assert.strictEqual(mainNote.repliesCount, 1);
 
 			const deleteTwoRes = await api('notes/delete', {
@@ -959,6 +975,7 @@ describe('Note', () => {
 
 			assert.strictEqual(deleteTwoRes.status, 204);
 			mainNote = await Notes.findOneBy({ id: mainNoteRes.body.createdNote.id });
+			assert.ok(mainNote);
 			assert.strictEqual(mainNote.repliesCount, 0);
 		});
 	});
@@ -980,7 +997,7 @@ describe('Note', () => {
 				}, alice);
 
 				assert.strictEqual(res.status, 400);
-				assert.strictEqual(res.body.error.code, 'UNAVAILABLE');
+				assert.strictEqual(castAsError(res.body).error.code, 'UNAVAILABLE');
 			});
 
 			afterAll(async () => {
@@ -992,7 +1009,7 @@ describe('Note', () => {
 			const res = await api('notes/translate', { noteId: 'foo', targetLang: 'ja' }, alice);
 
 			assert.strictEqual(res.status, 400);
-			assert.strictEqual(res.body.error.code, 'NO_SUCH_NOTE');
+			assert.strictEqual(castAsError(res.body).error.code, 'NO_SUCH_NOTE');
 		});
 
 		test('不可視なノートは翻訳できない', async () => {
@@ -1000,7 +1017,7 @@ describe('Note', () => {
 			const bobTranslateAttempt = await api('notes/translate', { noteId: aliceNote.id, targetLang: 'ja' }, bob);
 
 			assert.strictEqual(bobTranslateAttempt.status, 400);
-			assert.strictEqual(bobTranslateAttempt.body.error.code, 'CANNOT_TRANSLATE_INVISIBLE_NOTE');
+			assert.strictEqual(castAsError(bobTranslateAttempt.body).error.code, 'CANNOT_TRANSLATE_INVISIBLE_NOTE');
 		});
 
 		test('text: null なノートを翻訳すると空のレスポンスが返ってくる', async () => {
@@ -1016,7 +1033,7 @@ describe('Note', () => {
 
 			// NOTE: デフォルトでは登録されていないので落ちる
 			assert.strictEqual(res.status, 400);
-			assert.strictEqual(res.body.error.code, 'UNAVAILABLE');
+			assert.strictEqual(castAsError(res.body).error.code, 'UNAVAILABLE');
 		});
 	});
 });
diff --git a/packages/backend/test/e2e/renote-mute.ts b/packages/backend/test/e2e/renote-mute.ts
index 1abbb4f044..0f636b9ae2 100644
--- a/packages/backend/test/e2e/renote-mute.ts
+++ b/packages/backend/test/e2e/renote-mute.ts
@@ -6,7 +6,8 @@
 process.env.NODE_ENV = 'test';
 
 import * as assert from 'assert';
-import { api, post, signup, sleep, waitFire } from '../utils.js';
+import { setTimeout } from 'node:timers/promises';
+import { api, post, signup, waitFire } from '../utils.js';
 import type * as misskey from 'misskey-js';
 
 describe('Renote Mute', () => {
@@ -35,15 +36,15 @@ describe('Renote Mute', () => {
 		const carolNote = await post(carol, { text: 'hi' });
 
 		// redisに追加されるのを待つ
-		await sleep(100);
+		await setTimeout(100);
 
 		const res = await api('notes/local-timeline', {}, alice);
 
 		assert.strictEqual(res.status, 200);
 		assert.strictEqual(Array.isArray(res.body), true);
-		assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
-		assert.strictEqual(res.body.some((note: any) => note.id === carolRenote.id), false);
-		assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), true);
+		assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
+		assert.strictEqual(res.body.some(note => note.id === carolRenote.id), false);
+		assert.strictEqual(res.body.some(note => note.id === carolNote.id), true);
 	});
 
 	test('タイムラインにリノートミュートしているユーザーの引用が含まれる', async () => {
@@ -52,15 +53,15 @@ describe('Renote Mute', () => {
 		const carolNote = await post(carol, { text: 'hi' });
 
 		// redisに追加されるのを待つ
-		await sleep(100);
+		await setTimeout(100);
 
 		const res = await api('notes/local-timeline', {}, alice);
 
 		assert.strictEqual(res.status, 200);
 		assert.strictEqual(Array.isArray(res.body), true);
-		assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
-		assert.strictEqual(res.body.some((note: any) => note.id === carolRenote.id), true);
-		assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), true);
+		assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
+		assert.strictEqual(res.body.some(note => note.id === carolRenote.id), true);
+		assert.strictEqual(res.body.some(note => note.id === carolNote.id), true);
 	});
 
 	// #12956
@@ -69,14 +70,14 @@ describe('Renote Mute', () => {
 		const bobRenote = await post(bob, { renoteId: carolNote.id });
 
 		// redisに追加されるのを待つ
-		await sleep(100);
+		await setTimeout(100);
 
 		const res = await api('notes/local-timeline', {}, alice);
 
 		assert.strictEqual(res.status, 200);
 		assert.strictEqual(Array.isArray(res.body), true);
-		assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), true);
-		assert.strictEqual(res.body.some((note: any) => note.id === bobRenote.id), true);
+		assert.strictEqual(res.body.some(note => note.id === carolNote.id), true);
+		assert.strictEqual(res.body.some(note => note.id === bobRenote.id), true);
 	});
 
 	test('ストリームにリノートミュートしているユーザーのリノートが流れない', async () => {
diff --git a/packages/backend/test/e2e/streaming.ts b/packages/backend/test/e2e/streaming.ts
index b0a70074c6..72f26a38e0 100644
--- a/packages/backend/test/e2e/streaming.ts
+++ b/packages/backend/test/e2e/streaming.ts
@@ -34,6 +34,7 @@ describe('Streaming', () => {
 		let kyoko: misskey.entities.SignupResponse;
 		let chitose: misskey.entities.SignupResponse;
 		let kanako: misskey.entities.SignupResponse;
+		let erin: misskey.entities.SignupResponse;
 
 		// Remote users
 		let akari: misskey.entities.SignupResponse;
@@ -53,6 +54,7 @@ describe('Streaming', () => {
 			kyoko = await signup({ username: 'kyoko' });
 			chitose = await signup({ username: 'chitose' });
 			kanako = await signup({ username: 'kanako' });
+			erin = await signup({ username: 'erin' }); // erin:  A generic fifth participant
 
 			akari = await signup({ username: 'akari', host: 'example.com' });
 			chinatsu = await signup({ username: 'chinatsu', host: 'example.com' });
@@ -71,6 +73,12 @@ describe('Streaming', () => {
 			// Follow: kyoko => chitose
 			await api('following/create', { userId: chitose.id }, kyoko);
 
+			// Follow: erin <=> ayano each other.
+			// erin => ayano: withReplies: true
+			await api('following/create', { userId: ayano.id, withReplies: true }, erin);
+			// ayano => erin: withReplies: false
+			await api('following/create', { userId: erin.id, withReplies: false }, ayano);
+
 			// Mute: chitose => kanako
 			await api('mute/create', { userId: kanako.id }, chitose);
 
@@ -297,6 +305,28 @@ describe('Streaming', () => {
 
 				assert.strictEqual(fired, true);
 			});
+
+			test('withReplies: true のとき自分のfollowers投稿に対するリプライが流れる', async () => {
+				const erinNote = await post(erin, { text: 'hi', visibility: 'followers' });
+				const fired = await waitFire(
+					erin, 'homeTimeline',	// erin:home
+					() => api('notes/create', { text: 'hello', replyId: erinNote.id }, ayano),	// ayano reply to erin's followers post
+					msg => msg.type === 'note' && msg.body.userId === ayano.id,	// wait ayano
+				);
+
+				assert.strictEqual(fired, true);
+			});
+
+			test('withReplies: false でも自分の投稿に対するリプライが流れる', async () => {
+				const ayanoNote = await post(ayano, { text: 'hi', visibility: 'followers' });
+				const fired = await waitFire(
+					ayano, 'homeTimeline',	// ayano:home
+					() => api('notes/create', { text: 'hello', replyId: ayanoNote.id }, erin),	// erin reply to ayano's followers post
+					msg => msg.type === 'note' && msg.body.userId === erin.id,	// wait erin
+				);
+
+				assert.strictEqual(fired, true);
+			});
 		});	// Home
 
 		describe('Local Timeline', () => {
@@ -475,6 +505,38 @@ describe('Streaming', () => {
 
 				assert.strictEqual(fired, false);
 			});
+
+			test('withReplies: true のとき自分のfollowers投稿に対するリプライが流れる', async () => {
+				const erinNote = await post(erin, { text: 'hi', visibility: 'followers' });
+				const fired = await waitFire(
+					erin, 'homeTimeline',	// erin:home
+					() => api('notes/create', { text: 'hello', replyId: erinNote.id }, ayano),	// ayano reply to erin's followers post
+					msg => msg.type === 'note' && msg.body.userId === ayano.id,	// wait ayano
+				);
+
+				assert.strictEqual(fired, true);
+			});
+
+			test('withReplies: false でも自分の投稿に対するリプライが流れる', async () => {
+				const ayanoNote = await post(ayano, { text: 'hi', visibility: 'followers' });
+				const fired = await waitFire(
+					ayano, 'homeTimeline',	// ayano:home
+					() => api('notes/create', { text: 'hello', replyId: ayanoNote.id }, erin),	// erin reply to ayano's followers post
+					msg => msg.type === 'note' && msg.body.userId === erin.id,	// wait erin
+				);
+
+				assert.strictEqual(fired, true);
+			});
+
+			test('withReplies: true のフォローしていない人のfollowersノートに対するリプライが流れない', async () => {
+				const fired = await waitFire(
+					erin, 'homeTimeline',	// erin:home
+					() => api('notes/create', { text: 'hello', replyId: chitose.id }, ayano),	// ayano reply to chitose's post
+					msg => msg.type === 'note' && msg.body.userId === ayano.id,	// wait ayano
+				);
+
+				assert.strictEqual(fired, false);
+			});
 		});
 
 		describe('Global Timeline', () => {
diff --git a/packages/backend/test/e2e/synalio/abuse-report.ts b/packages/backend/test/e2e/synalio/abuse-report.ts
new file mode 100644
index 0000000000..c98d199f35
--- /dev/null
+++ b/packages/backend/test/e2e/synalio/abuse-report.ts
@@ -0,0 +1,354 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { entities } from 'misskey-js';
+import { beforeEach, describe, test } from '@jest/globals';
+import {
+	api,
+	captureWebhook,
+	randomString,
+	role,
+	signup,
+	startJobQueue,
+	UserToken,
+	WEBHOOK_HOST,
+} from '../../utils.js';
+import type { INestApplicationContext } from '@nestjs/common';
+
+describe('[シナリオ] ユーザ通報', () => {
+	let queue: INestApplicationContext;
+	let admin: entities.SignupResponse;
+	let alice: entities.SignupResponse;
+	let bob: entities.SignupResponse;
+
+	async function createSystemWebhook(args?: Partial<entities.AdminSystemWebhookCreateRequest>, credential?: UserToken): Promise<entities.AdminSystemWebhookCreateResponse> {
+		const res = await api(
+			'admin/system-webhook/create',
+			{
+				isActive: true,
+				name: randomString(),
+				on: ['abuseReport'],
+				url: WEBHOOK_HOST,
+				secret: randomString(),
+				...args,
+			},
+			credential ?? admin,
+		);
+		return res.body;
+	}
+
+	async function createAbuseReportNotificationRecipient(args?: Partial<entities.AdminAbuseReportNotificationRecipientCreateRequest>, credential?: UserToken): Promise<entities.AdminAbuseReportNotificationRecipientCreateResponse> {
+		const res = await api(
+			'admin/abuse-report/notification-recipient/create',
+			{
+				isActive: true,
+				name: randomString(),
+				method: 'webhook',
+				...args,
+			},
+			credential ?? admin,
+		);
+		return res.body;
+	}
+
+	async function createAbuseReport(args?: Partial<entities.UsersReportAbuseRequest>, credential?: UserToken): Promise<entities.EmptyResponse> {
+		const res = await api(
+			'users/report-abuse',
+			{
+				userId: alice.id,
+				comment: randomString(),
+				...args,
+			},
+			credential ?? admin,
+		);
+		return res.body;
+	}
+
+	async function resolveAbuseReport(args?: Partial<entities.AdminResolveAbuseUserReportRequest>, credential?: UserToken): Promise<entities.EmptyResponse> {
+		const res = await api(
+			'admin/resolve-abuse-user-report',
+			{
+				reportId: admin.id,
+				...args,
+			},
+			credential ?? admin,
+		);
+		return res.body;
+	}
+
+	// -------------------------------------------------------------------------------------------
+
+	beforeAll(async () => {
+		queue = await startJobQueue();
+		admin = await signup({ username: 'admin' });
+		alice = await signup({ username: 'alice' });
+		bob = await signup({ username: 'bob' });
+
+		await role(admin, { isAdministrator: true });
+	}, 1000 * 60 * 2);
+
+	afterAll(async () => {
+		await queue.close();
+	});
+
+	// -------------------------------------------------------------------------------------------
+
+	describe('SystemWebhook', () => {
+		beforeEach(async () => {
+			const webhooks = await api('admin/system-webhook/list', {}, admin);
+			for (const webhook of webhooks.body) {
+				await api('admin/system-webhook/delete', { id: webhook.id }, admin);
+			}
+		});
+
+		test('通報を受けた -> abuseReportが送出される', async () => {
+			const webhook = await createSystemWebhook({
+				on: ['abuseReport'],
+				isActive: true,
+			});
+			await createAbuseReportNotificationRecipient({ systemWebhookId: webhook.id });
+
+			// 通報(bob -> alice)
+			const abuse = {
+				userId: alice.id,
+				comment: randomString(),
+			};
+			const webhookBody = await captureWebhook(async () => {
+				await createAbuseReport(abuse, bob);
+			});
+
+			console.log(JSON.stringify(webhookBody, null, 2));
+
+			expect(webhookBody.hookId).toBe(webhook.id);
+			expect(webhookBody.type).toBe('abuseReport');
+			expect(webhookBody.body.targetUserId).toBe(alice.id);
+			expect(webhookBody.body.reporterId).toBe(bob.id);
+			expect(webhookBody.body.comment).toBe(abuse.comment);
+		});
+
+		test('通報を受けた -> abuseReportが送出される -> 解決 -> abuseReportResolvedが送出される', async () => {
+			const webhook = await createSystemWebhook({
+				on: ['abuseReport', 'abuseReportResolved'],
+				isActive: true,
+			});
+			await createAbuseReportNotificationRecipient({ systemWebhookId: webhook.id });
+
+			// 通報(bob -> alice)
+			const abuse = {
+				userId: alice.id,
+				comment: randomString(),
+			};
+			const webhookBody1 = await captureWebhook(async () => {
+				await createAbuseReport(abuse, bob);
+			});
+
+			console.log(JSON.stringify(webhookBody1, null, 2));
+			expect(webhookBody1.hookId).toBe(webhook.id);
+			expect(webhookBody1.type).toBe('abuseReport');
+			expect(webhookBody1.body.targetUserId).toBe(alice.id);
+			expect(webhookBody1.body.reporterId).toBe(bob.id);
+			expect(webhookBody1.body.assigneeId).toBeNull();
+			expect(webhookBody1.body.resolved).toBe(false);
+			expect(webhookBody1.body.comment).toBe(abuse.comment);
+
+			// 解決
+			const webhookBody2 = await captureWebhook(async () => {
+				await resolveAbuseReport({
+					reportId: webhookBody1.body.id,
+				}, admin);
+			});
+
+			console.log(JSON.stringify(webhookBody2, null, 2));
+			expect(webhookBody2.hookId).toBe(webhook.id);
+			expect(webhookBody2.type).toBe('abuseReportResolved');
+			expect(webhookBody2.body.targetUserId).toBe(alice.id);
+			expect(webhookBody2.body.reporterId).toBe(bob.id);
+			expect(webhookBody2.body.assigneeId).toBe(admin.id);
+			expect(webhookBody2.body.resolved).toBe(true);
+			expect(webhookBody2.body.comment).toBe(abuse.comment);
+		});
+
+		test('通報を受けた -> abuseReportが未許可の場合は送出されない', async () => {
+			const webhook = await createSystemWebhook({
+				on: [],
+				isActive: true,
+			});
+			await createAbuseReportNotificationRecipient({ systemWebhookId: webhook.id });
+
+			// 通報(bob -> alice)
+			const abuse = {
+				userId: alice.id,
+				comment: randomString(),
+			};
+			const webhookBody = await captureWebhook(async () => {
+				await createAbuseReport(abuse, bob);
+			}).catch(e => e.message);
+
+			expect(webhookBody).toBe('timeout');
+		});
+
+		test('通報を受けた -> abuseReportが未許可の場合は送出されない -> 解決 -> abuseReportResolvedが送出される', async () => {
+			const webhook = await createSystemWebhook({
+				on: ['abuseReportResolved'],
+				isActive: true,
+			});
+			await createAbuseReportNotificationRecipient({ systemWebhookId: webhook.id });
+
+			// 通報(bob -> alice)
+			const abuse = {
+				userId: alice.id,
+				comment: randomString(),
+			};
+			const webhookBody1 = await captureWebhook(async () => {
+				await createAbuseReport(abuse, bob);
+			}).catch(e => e.message);
+
+			expect(webhookBody1).toBe('timeout');
+
+			const abuseReportId = (await api('admin/abuse-user-reports', {}, admin)).body[0].id;
+
+			// 解決
+			const webhookBody2 = await captureWebhook(async () => {
+				await resolveAbuseReport({
+					reportId: abuseReportId,
+				}, admin);
+			});
+
+			console.log(JSON.stringify(webhookBody2, null, 2));
+			expect(webhookBody2.hookId).toBe(webhook.id);
+			expect(webhookBody2.type).toBe('abuseReportResolved');
+			expect(webhookBody2.body.targetUserId).toBe(alice.id);
+			expect(webhookBody2.body.reporterId).toBe(bob.id);
+			expect(webhookBody2.body.assigneeId).toBe(admin.id);
+			expect(webhookBody2.body.resolved).toBe(true);
+			expect(webhookBody2.body.comment).toBe(abuse.comment);
+		});
+
+		test('通報を受けた -> abuseReportが送出される -> 解決 -> abuseReportResolvedが未許可の場合は送出されない', async () => {
+			const webhook = await createSystemWebhook({
+				on: ['abuseReport'],
+				isActive: true,
+			});
+			await createAbuseReportNotificationRecipient({ systemWebhookId: webhook.id });
+
+			// 通報(bob -> alice)
+			const abuse = {
+				userId: alice.id,
+				comment: randomString(),
+			};
+			const webhookBody1 = await captureWebhook(async () => {
+				await createAbuseReport(abuse, bob);
+			});
+
+			console.log(JSON.stringify(webhookBody1, null, 2));
+			expect(webhookBody1.hookId).toBe(webhook.id);
+			expect(webhookBody1.type).toBe('abuseReport');
+			expect(webhookBody1.body.targetUserId).toBe(alice.id);
+			expect(webhookBody1.body.reporterId).toBe(bob.id);
+			expect(webhookBody1.body.assigneeId).toBeNull();
+			expect(webhookBody1.body.resolved).toBe(false);
+			expect(webhookBody1.body.comment).toBe(abuse.comment);
+
+			// 解決
+			const webhookBody2 = await captureWebhook(async () => {
+				await resolveAbuseReport({
+					reportId: webhookBody1.body.id,
+				}, admin);
+			}).catch(e => e.message);
+
+			expect(webhookBody2).toBe('timeout');
+		});
+
+		test('通報を受けた -> abuseReportが未許可の場合は送出されない -> 解決 -> abuseReportResolvedが未許可の場合は送出されない', async () => {
+			const webhook = await createSystemWebhook({
+				on: [],
+				isActive: true,
+			});
+			await createAbuseReportNotificationRecipient({ systemWebhookId: webhook.id });
+
+			// 通報(bob -> alice)
+			const abuse = {
+				userId: alice.id,
+				comment: randomString(),
+			};
+			const webhookBody1 = await captureWebhook(async () => {
+				await createAbuseReport(abuse, bob);
+			}).catch(e => e.message);
+
+			expect(webhookBody1).toBe('timeout');
+
+			const abuseReportId = (await api('admin/abuse-user-reports', {}, admin)).body[0].id;
+
+			// 解決
+			const webhookBody2 = await captureWebhook(async () => {
+				await resolveAbuseReport({
+					reportId: abuseReportId,
+				}, admin);
+			}).catch(e => e.message);
+
+			expect(webhookBody2).toBe('timeout');
+		});
+
+		test('通報を受けた -> Webhookが無効の場合は送出されない', async () => {
+			const webhook = await createSystemWebhook({
+				on: ['abuseReport', 'abuseReportResolved'],
+				isActive: false,
+			});
+			await createAbuseReportNotificationRecipient({ systemWebhookId: webhook.id });
+
+			// 通報(bob -> alice)
+			const abuse = {
+				userId: alice.id,
+				comment: randomString(),
+			};
+			const webhookBody1 = await captureWebhook(async () => {
+				await createAbuseReport(abuse, bob);
+			}).catch(e => e.message);
+
+			expect(webhookBody1).toBe('timeout');
+
+			const abuseReportId = (await api('admin/abuse-user-reports', {}, admin)).body[0].id;
+
+			// 解決
+			const webhookBody2 = await captureWebhook(async () => {
+				await resolveAbuseReport({
+					reportId: abuseReportId,
+				}, admin);
+			}).catch(e => e.message);
+
+			expect(webhookBody2).toBe('timeout');
+		});
+
+		test('通報を受けた -> 通知設定が無効の場合は送出されない', async () => {
+			const webhook = await createSystemWebhook({
+				on: ['abuseReport', 'abuseReportResolved'],
+				isActive: true,
+			});
+			await createAbuseReportNotificationRecipient({ systemWebhookId: webhook.id, isActive: false });
+
+			// 通報(bob -> alice)
+			const abuse = {
+				userId: alice.id,
+				comment: randomString(),
+			};
+			const webhookBody1 = await captureWebhook(async () => {
+				await createAbuseReport(abuse, bob);
+			}).catch(e => e.message);
+
+			expect(webhookBody1).toBe('timeout');
+
+			const abuseReportId = (await api('admin/abuse-user-reports', {}, admin)).body[0].id;
+
+			// 解決
+			const webhookBody2 = await captureWebhook(async () => {
+				await resolveAbuseReport({
+					reportId: abuseReportId,
+				}, admin);
+			}).catch(e => e.message);
+
+			expect(webhookBody2).toBe('timeout');
+		});
+	});
+});
diff --git a/packages/backend/test/e2e/synalio/user-create.ts b/packages/backend/test/e2e/synalio/user-create.ts
new file mode 100644
index 0000000000..cb0f68dfea
--- /dev/null
+++ b/packages/backend/test/e2e/synalio/user-create.ts
@@ -0,0 +1,130 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { setTimeout } from 'node:timers/promises';
+import { entities } from 'misskey-js';
+import { beforeEach, describe, test } from '@jest/globals';
+import {
+	api,
+	captureWebhook,
+	randomString,
+	role,
+	signup,
+	startJobQueue,
+	UserToken,
+	WEBHOOK_HOST,
+} from '../../utils.js';
+import type { INestApplicationContext } from '@nestjs/common';
+
+describe('[シナリオ] ユーザ作成', () => {
+	let queue: INestApplicationContext;
+	let admin: entities.SignupResponse;
+
+	async function createSystemWebhook(args?: Partial<entities.AdminSystemWebhookCreateRequest>, credential?: UserToken): Promise<entities.AdminSystemWebhookCreateResponse> {
+		const res = await api(
+			'admin/system-webhook/create',
+			{
+				isActive: true,
+				name: randomString(),
+				on: ['userCreated'],
+				url: WEBHOOK_HOST,
+				secret: randomString(),
+				...args,
+			},
+			credential ?? admin,
+		);
+		return res.body;
+	}
+
+	// -------------------------------------------------------------------------------------------
+
+	beforeAll(async () => {
+		queue = await startJobQueue();
+		admin = await signup({ username: 'admin' });
+
+		await role(admin, { isAdministrator: true });
+	}, 1000 * 60 * 2);
+
+	afterAll(async () => {
+		await queue.close();
+	});
+
+	// -------------------------------------------------------------------------------------------
+
+	describe('SystemWebhook', () => {
+		beforeEach(async () => {
+			const webhooks = await api('admin/system-webhook/list', {}, admin);
+			for (const webhook of webhooks.body) {
+				await api('admin/system-webhook/delete', { id: webhook.id }, admin);
+			}
+		});
+
+		test('ユーザが作成された -> userCreatedが送出される', async () => {
+			const webhook = await createSystemWebhook({
+				on: ['userCreated'],
+				isActive: true,
+			});
+
+			let alice: any = null;
+			const webhookBody = await captureWebhook(async () => {
+				alice = await signup({ username: 'alice' });
+			});
+
+			// webhookの送出後にいろいろやってるのでちょっと待つ必要がある
+			await setTimeout(2000);
+
+			console.log(alice);
+			console.log(JSON.stringify(webhookBody, null, 2));
+
+			expect(webhookBody.hookId).toBe(webhook.id);
+			expect(webhookBody.type).toBe('userCreated');
+
+			const body = webhookBody.body as entities.UserLite;
+			expect(alice.id).toBe(body.id);
+			expect(alice.name).toBe(body.name);
+			expect(alice.username).toBe(body.username);
+			expect(alice.host).toBe(body.host);
+			expect(alice.avatarUrl).toBe(body.avatarUrl);
+			expect(alice.avatarBlurhash).toBe(body.avatarBlurhash);
+			expect(alice.avatarDecorations).toEqual(body.avatarDecorations);
+			expect(alice.isBot).toBe(body.isBot);
+			expect(alice.isCat).toBe(body.isCat);
+			expect(alice.instance).toEqual(body.instance);
+			expect(alice.emojis).toEqual(body.emojis);
+			expect(alice.onlineStatus).toBe(body.onlineStatus);
+			expect(alice.badgeRoles).toEqual(body.badgeRoles);
+		});
+
+		test('ユーザ作成 -> userCreatedが未許可の場合は送出されない', async () => {
+			await createSystemWebhook({
+				on: [],
+				isActive: true,
+			});
+
+			let alice: any = null;
+			const webhookBody = await captureWebhook(async () => {
+				alice = await signup({ username: 'alice' });
+			}).catch(e => e.message);
+
+			expect(webhookBody).toBe('timeout');
+			expect(alice.id).not.toBeNull();
+		});
+
+		test('ユーザ作成 -> Webhookが無効の場合は送出されない', async () => {
+			await createSystemWebhook({
+				on: ['userCreated'],
+				isActive: false,
+			});
+
+			let alice: any = null;
+			const webhookBody = await captureWebhook(async () => {
+				alice = await signup({ username: 'alice' });
+			}).catch(e => e.message);
+
+			expect(webhookBody).toBe('timeout');
+			expect(alice.id).not.toBeNull();
+		});
+	});
+});
diff --git a/packages/backend/test/e2e/thread-mute.ts b/packages/backend/test/e2e/thread-mute.ts
index 53bb6eb765..1ac99df884 100644
--- a/packages/backend/test/e2e/thread-mute.ts
+++ b/packages/backend/test/e2e/thread-mute.ts
@@ -33,9 +33,9 @@ describe('Note thread mute', () => {
 
 		assert.strictEqual(res.status, 200);
 		assert.strictEqual(Array.isArray(res.body), true);
-		assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
-		assert.strictEqual(res.body.some((note: any) => note.id === carolReply.id), false);
-		assert.strictEqual(res.body.some((note: any) => note.id === carolReplyWithoutMention.id), false);
+		assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
+		assert.strictEqual(res.body.some(note => note.id === carolReply.id), false);
+		assert.strictEqual(res.body.some(note => note.id === carolReplyWithoutMention.id), false);
 	});
 
 	test('ミュートしているスレッドからメンションされても、hasUnreadMentions が true にならない', async () => {
@@ -93,8 +93,8 @@ describe('Note thread mute', () => {
 
 		assert.strictEqual(res.status, 200);
 		assert.strictEqual(Array.isArray(res.body), true);
-		assert.strictEqual(res.body.some((notification: any) => notification.note.id === carolReply.id), false);
-		assert.strictEqual(res.body.some((notification: any) => notification.note.id === carolReplyWithoutMention.id), false);
+		assert.strictEqual(res.body.some(notification => 'note' in notification && notification.note.id === carolReply.id), false);
+		assert.strictEqual(res.body.some(notification => 'note' in notification && notification.note.id === carolReplyWithoutMention.id), false);
 
 		// NOTE: bobの投稿はスレッドミュート前に行われたため通知に含まれていてもよい
 	});
diff --git a/packages/backend/test/e2e/timelines.ts b/packages/backend/test/e2e/timelines.ts
index 5487292afc..d12be2a9ac 100644
--- a/packages/backend/test/e2e/timelines.ts
+++ b/packages/backend/test/e2e/timelines.ts
@@ -7,17 +7,26 @@
 // pnpm jest -- e2e/timelines.ts
 
 import * as assert from 'assert';
-import { api, post, randomString, sendEnvUpdateRequest, signup, sleep, uploadUrl } from '../utils.js';
+import { setTimeout } from 'node:timers/promises';
+import { Redis } from 'ioredis';
+import { api, post, randomString, sendEnvUpdateRequest, signup, uploadUrl } from '../utils.js';
+import { loadConfig } from '@/config.js';
 
 function genHost() {
 	return randomString() + '.example.com';
 }
 
 function waitForPushToTl() {
-	return sleep(500);
+	return setTimeout(500);
 }
 
+let redisForTimelines: Redis;
+
 describe('Timelines', () => {
+	beforeAll(() => {
+		redisForTimelines = new Redis(loadConfig().redisForTimelines);
+	});
+
 	describe('Home TL', () => {
 		test.concurrent('自分の visibility: followers なノートが含まれる', async () => {
 			const [alice] = await Promise.all([signup()]);
@@ -28,15 +37,15 @@ describe('Timelines', () => {
 
 			const res = await api('notes/timeline', { limit: 100 }, alice);
 
-			assert.strictEqual(res.body.some((note: any) => note.id === aliceNote.id), true);
-			assert.strictEqual(res.body.find((note: any) => note.id === aliceNote.id).text, 'hi');
+			assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true);
+			assert.strictEqual(res.body.find(note => note.id === aliceNote.id)?.text, 'hi');
 		});
 
 		test.concurrent('フォローしているユーザーのノートが含まれる', async () => {
 			const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]);
 
 			await api('following/create', { userId: bob.id }, alice);
-			await sleep(1000);
+			await setTimeout(1000);
 			const bobNote = await post(bob, { text: 'hi' });
 			const carolNote = await post(carol, { text: 'hi' });
 
@@ -44,15 +53,15 @@ describe('Timelines', () => {
 
 			const res = await api('notes/timeline', { limit: 100 }, alice);
 
-			assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
-			assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false);
+			assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
+			assert.strictEqual(res.body.some(note => note.id === carolNote.id), false);
 		});
 
 		test.concurrent('フォローしているユーザーの visibility: followers なノートが含まれる', async () => {
 			const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]);
 
 			await api('following/create', { userId: bob.id }, alice);
-			await sleep(1000);
+			await setTimeout(1000);
 			const bobNote = await post(bob, { text: 'hi', visibility: 'followers' });
 			const carolNote = await post(carol, { text: 'hi' });
 
@@ -60,16 +69,16 @@ describe('Timelines', () => {
 
 			const res = await api('notes/timeline', { limit: 100 }, alice);
 
-			assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
-			assert.strictEqual(res.body.find((note: any) => note.id === bobNote.id).text, 'hi');
-			assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false);
+			assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
+			assert.strictEqual(res.body.find(note => note.id === bobNote.id)?.text, 'hi');
+			assert.strictEqual(res.body.some(note => note.id === carolNote.id), false);
 		});
 
 		test.concurrent('withReplies: false でフォローしているユーザーの他人への返信が含まれない', async () => {
 			const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]);
 
 			await api('following/create', { userId: bob.id }, alice);
-			await sleep(1000);
+			await setTimeout(1000);
 			const carolNote = await post(carol, { text: 'hi' });
 			const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id });
 
@@ -77,8 +86,8 @@ describe('Timelines', () => {
 
 			const res = await api('notes/timeline', { limit: 100 }, alice);
 
-			assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
-			assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false);
+			assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
+			assert.strictEqual(res.body.some(note => note.id === carolNote.id), false);
 		});
 
 		test.concurrent('withReplies: true でフォローしているユーザーの他人への返信が含まれる', async () => {
@@ -86,7 +95,7 @@ describe('Timelines', () => {
 
 			await api('following/create', { userId: bob.id }, alice);
 			await api('following/update', { userId: bob.id, withReplies: true }, alice);
-			await sleep(1000);
+			await setTimeout(1000);
 			const carolNote = await post(carol, { text: 'hi' });
 			const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id });
 
@@ -94,8 +103,8 @@ describe('Timelines', () => {
 
 			const res = await api('notes/timeline', { limit: 100 }, alice);
 
-			assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
-			assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false);
+			assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
+			assert.strictEqual(res.body.some(note => note.id === carolNote.id), false);
 		});
 
 		test.concurrent('withReplies: true でフォローしているユーザーの他人へのDM返信が含まれない', async () => {
@@ -103,7 +112,7 @@ describe('Timelines', () => {
 
 			await api('following/create', { userId: bob.id }, alice);
 			await api('following/update', { userId: bob.id, withReplies: true }, alice);
-			await sleep(1000);
+			await setTimeout(1000);
 			const carolNote = await post(carol, { text: 'hi' });
 			const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id, visibility: 'specified', visibleUserIds: [carolNote.id] });
 
@@ -111,16 +120,17 @@ describe('Timelines', () => {
 
 			const res = await api('notes/timeline', { limit: 100 }, alice);
 
-			assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
-			assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false);
+			assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
+			assert.strictEqual(res.body.some(note => note.id === carolNote.id), false);
 		});
 
 		test.concurrent('withReplies: true でフォローしているユーザーの他人の visibility: followers な投稿への返信が含まれない', async () => {
 			const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]);
 
+			await api('following/create', { userId: carol.id }, bob);
 			await api('following/create', { userId: bob.id }, alice);
 			await api('following/update', { userId: bob.id, withReplies: true }, alice);
-			await sleep(1000);
+			await setTimeout(1000);
 			const carolNote = await post(carol, { text: 'hi', visibility: 'followers' });
 			const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id });
 
@@ -128,8 +138,8 @@ describe('Timelines', () => {
 
 			const res = await api('notes/timeline', { limit: 100 }, alice);
 
-			assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
-			assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false);
+			assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
+			assert.strictEqual(res.body.some(note => note.id === carolNote.id), false);
 		});
 
 		test.concurrent('withReplies: true でフォローしているユーザーの行った別のフォローしているユーザーの visibility: followers な投稿への返信が含まれる', async () => {
@@ -139,7 +149,7 @@ describe('Timelines', () => {
 			await api('following/create', { userId: carol.id }, alice);
 			await api('following/create', { userId: carol.id }, bob);
 			await api('following/update', { userId: bob.id, withReplies: true }, alice);
-			await sleep(1000);
+			await setTimeout(1000);
 			const carolNote = await post(carol, { text: 'hi', visibility: 'followers' });
 			const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id });
 
@@ -147,9 +157,27 @@ describe('Timelines', () => {
 
 			const res = await api('notes/timeline', { limit: 100 }, alice);
 
+			assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
+			assert.strictEqual(res.body.some(note => note.id === carolNote.id), true);
+			assert.strictEqual(res.body.find(note => note.id === carolNote.id)?.text, 'hi');
+		});
+
+		test.concurrent('withReplies: true でフォローしているユーザーの自分の visibility: followers な投稿への返信が含まれる', async () => {
+			const [alice, bob] = await Promise.all([signup(), signup()]);
+
+			await api('following/create', { userId: bob.id }, alice);
+			await api('following/create', { userId: alice.id }, bob);
+			await api('following/update', { userId: bob.id, withReplies: true }, alice);
+			await setTimeout(1000);
+			const aliceNote = await post(alice, { text: 'hi', visibility: 'followers' });
+			const bobNote = await post(bob, { text: 'hi', replyId: aliceNote.id });
+
+			await waitForPushToTl();
+
+			const res = await api('notes/timeline', { limit: 100 }, alice);
+
 			assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
-			assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), true);
-			assert.strictEqual(res.body.find((note: any) => note.id === carolNote.id).text, 'hi');
+			assert.strictEqual(res.body.some((note: any) => note.id === aliceNote.id), true);
 		});
 
 		test.concurrent('withReplies: true でフォローしているユーザーの行った別のフォローしているユーザーの投稿への visibility: specified な返信が含まれない', async () => {
@@ -158,7 +186,7 @@ describe('Timelines', () => {
 			await api('following/create', { userId: bob.id }, alice);
 			await api('following/create', { userId: carol.id }, alice);
 			await api('following/update', { userId: bob.id, withReplies: true }, alice);
-			await sleep(1000);
+			await setTimeout(1000);
 			const carolNote = await post(carol, { text: 'hi' });
 			const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id, visibility: 'specified', visibleUserIds: [carolNote.id] });
 
@@ -166,15 +194,15 @@ describe('Timelines', () => {
 
 			const res = await api('notes/timeline', { limit: 100 }, alice);
 
-			assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
-			assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), true);
+			assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
+			assert.strictEqual(res.body.some(note => note.id === carolNote.id), true);
 		});
 
 		test.concurrent('withReplies: false でフォローしているユーザーのそのユーザー自身への返信が含まれる', async () => {
 			const [alice, bob] = await Promise.all([signup(), signup()]);
 
 			await api('following/create', { userId: bob.id }, alice);
-			await sleep(1000);
+			await setTimeout(1000);
 			const bobNote1 = await post(bob, { text: 'hi' });
 			const bobNote2 = await post(bob, { text: 'hi', replyId: bobNote1.id });
 
@@ -182,15 +210,15 @@ describe('Timelines', () => {
 
 			const res = await api('notes/timeline', { limit: 100 }, alice);
 
-			assert.strictEqual(res.body.some((note: any) => note.id === bobNote1.id), true);
-			assert.strictEqual(res.body.some((note: any) => note.id === bobNote2.id), true);
+			assert.strictEqual(res.body.some(note => note.id === bobNote1.id), true);
+			assert.strictEqual(res.body.some(note => note.id === bobNote2.id), true);
 		});
 
 		test.concurrent('withReplies: false でフォローしているユーザーからの自分への返信が含まれる', async () => {
 			const [alice, bob] = await Promise.all([signup(), signup()]);
 
 			await api('following/create', { userId: bob.id }, alice);
-			await sleep(1000);
+			await setTimeout(1000);
 			const aliceNote = await post(alice, { text: 'hi' });
 			const bobNote = await post(bob, { text: 'hi', replyId: aliceNote.id });
 
@@ -198,8 +226,8 @@ describe('Timelines', () => {
 
 			const res = await api('notes/timeline', { limit: 100 }, alice);
 
-			assert.strictEqual(res.body.some((note: any) => note.id === aliceNote.id), true);
-			assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
+			assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true);
+			assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
 		});
 
 		test.concurrent('自分の他人への返信が含まれる', async () => {
@@ -212,15 +240,15 @@ describe('Timelines', () => {
 
 			const res = await api('notes/timeline', { limit: 100 }, alice);
 
-			assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
-			assert.strictEqual(res.body.some((note: any) => note.id === aliceNote.id), true);
+			assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
+			assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true);
 		});
 
 		test.concurrent('フォローしているユーザーの他人の投稿のリノートが含まれる', async () => {
 			const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]);
 
 			await api('following/create', { userId: bob.id }, alice);
-			await sleep(1000);
+			await setTimeout(1000);
 			const carolNote = await post(carol, { text: 'hi' });
 			const bobNote = await post(bob, { renoteId: carolNote.id });
 
@@ -228,15 +256,15 @@ describe('Timelines', () => {
 
 			const res = await api('notes/timeline', { limit: 100 }, alice);
 
-			assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
-			assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false);
+			assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
+			assert.strictEqual(res.body.some(note => note.id === carolNote.id), false);
 		});
 
 		test.concurrent('[withRenotes: false] フォローしているユーザーの他人の投稿のリノートが含まれない', async () => {
 			const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]);
 
 			await api('following/create', { userId: bob.id }, alice);
-			await sleep(1000);
+			await setTimeout(1000);
 			const carolNote = await post(carol, { text: 'hi' });
 			const bobNote = await post(bob, { renoteId: carolNote.id });
 
@@ -246,15 +274,15 @@ describe('Timelines', () => {
 				withRenotes: false,
 			}, alice);
 
-			assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
-			assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false);
+			assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
+			assert.strictEqual(res.body.some(note => note.id === carolNote.id), false);
 		});
 
 		test.concurrent('[withRenotes: false] フォローしているユーザーの他人の投稿の引用が含まれる', async () => {
 			const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]);
 
 			await api('following/create', { userId: bob.id }, alice);
-			await sleep(1000);
+			await setTimeout(1000);
 			const carolNote = await post(carol, { text: 'hi' });
 			const bobNote = await post(bob, { text: 'hi', renoteId: carolNote.id });
 
@@ -264,22 +292,22 @@ describe('Timelines', () => {
 				withRenotes: false,
 			}, alice);
 
-			assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
-			assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false);
+			assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
+			assert.strictEqual(res.body.some(note => note.id === carolNote.id), false);
 		});
 
 		test.concurrent('フォローしているユーザーの他人への visibility: specified なノートが含まれない', async () => {
 			const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]);
 
 			await api('following/create', { userId: bob.id }, alice);
-			await sleep(1000);
+			await setTimeout(1000);
 			const bobNote = await post(bob, { text: 'hi', visibility: 'specified', visibleUserIds: [carol.id] });
 
 			await waitForPushToTl();
 
 			const res = await api('notes/timeline', { limit: 100 }, alice);
 
-			assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
+			assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
 		});
 
 		test.concurrent('フォローしているユーザーが行ったミュートしているユーザーのリノートが含まれない', async () => {
@@ -287,7 +315,7 @@ describe('Timelines', () => {
 
 			await api('following/create', { userId: bob.id }, alice);
 			await api('mute/create', { userId: carol.id }, alice);
-			await sleep(1000);
+			await setTimeout(1000);
 			const carolNote = await post(carol, { text: 'hi' });
 			const bobNote = await post(bob, { text: 'hi', renoteId: carolNote.id });
 
@@ -295,8 +323,8 @@ describe('Timelines', () => {
 
 			const res = await api('notes/timeline', { limit: 100 }, alice);
 
-			assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
-			assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false);
+			assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
+			assert.strictEqual(res.body.some(note => note.id === carolNote.id), false);
 		});
 
 		test.concurrent('withReplies: true でフォローしているユーザーが行ったミュートしているユーザーの投稿への返信が含まれない', async () => {
@@ -305,7 +333,7 @@ describe('Timelines', () => {
 			await api('following/create', { userId: bob.id }, alice);
 			await api('following/update', { userId: bob.id, withReplies: true }, alice);
 			await api('mute/create', { userId: carol.id }, alice);
-			await sleep(1000);
+			await setTimeout(1000);
 			const carolNote = await post(carol, { text: 'hi' });
 			const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id });
 
@@ -313,8 +341,8 @@ describe('Timelines', () => {
 
 			const res = await api('notes/timeline', { limit: 100 }, alice);
 
-			assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
-			assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false);
+			assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
+			assert.strictEqual(res.body.some(note => note.id === carolNote.id), false);
 		});
 
 		test.concurrent('フォローしているリモートユーザーのノートが含まれる', async () => {
@@ -329,7 +357,7 @@ describe('Timelines', () => {
 
 			const res = await api('notes/timeline', { limit: 100 }, alice);
 
-			assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
+			assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
 		});
 
 		test.concurrent('フォローしているリモートユーザーの visibility: home なノートが含まれる', async () => {
@@ -344,14 +372,14 @@ describe('Timelines', () => {
 
 			const res = await api('notes/timeline', { limit: 100 }, alice);
 
-			assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
+			assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
 		});
 
 		test.concurrent('[withFiles: true] フォローしているユーザーのファイル付きノートのみ含まれる', async () => {
 			const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]);
 
 			await api('following/create', { userId: bob.id }, alice);
-			await sleep(1000);
+			await setTimeout(1000);
 			const [bobFile, carolFile] = await Promise.all([
 				uploadUrl(bob, 'https://raw.githubusercontent.com/misskey-dev/assets/main/public/icon.png'),
 				uploadUrl(carol, 'https://raw.githubusercontent.com/misskey-dev/assets/main/public/icon.png'),
@@ -365,10 +393,10 @@ describe('Timelines', () => {
 
 			const res = await api('notes/timeline', { limit: 100, withFiles: true }, alice);
 
-			assert.strictEqual(res.body.some((note: any) => note.id === bobNote1.id), false);
-			assert.strictEqual(res.body.some((note: any) => note.id === bobNote2.id), true);
-			assert.strictEqual(res.body.some((note: any) => note.id === carolNote1.id), false);
-			assert.strictEqual(res.body.some((note: any) => note.id === carolNote2.id), false);
+			assert.strictEqual(res.body.some(note => note.id === bobNote1.id), false);
+			assert.strictEqual(res.body.some(note => note.id === bobNote2.id), true);
+			assert.strictEqual(res.body.some(note => note.id === carolNote1.id), false);
+			assert.strictEqual(res.body.some(note => note.id === carolNote2.id), false);
 		}, 1000 * 10);
 
 		test.concurrent('フォローしているユーザーのチャンネル投稿が含まれない', async () => {
@@ -376,14 +404,14 @@ describe('Timelines', () => {
 
 			const channel = await api('channels/create', { name: 'channel' }, bob).then(x => x.body);
 			await api('following/create', { userId: bob.id }, alice);
-			await sleep(1000);
+			await setTimeout(1000);
 			const bobNote = await post(bob, { text: 'hi', channelId: channel.id });
 
 			await waitForPushToTl();
 
 			const res = await api('notes/timeline', { limit: 100 }, alice);
 
-			assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
+			assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
 		});
 
 		test.concurrent('自分の visibility: specified なノートが含まれる', async () => {
@@ -395,23 +423,23 @@ describe('Timelines', () => {
 
 			const res = await api('notes/timeline', { limit: 100 }, alice);
 
-			assert.strictEqual(res.body.some((note: any) => note.id === aliceNote.id), true);
-			assert.strictEqual(res.body.find((note: any) => note.id === aliceNote.id).text, 'hi');
+			assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true);
+			assert.strictEqual(res.body.find(note => note.id === aliceNote.id)?.text, 'hi');
 		});
 
 		test.concurrent('フォローしているユーザーの自身を visibleUserIds に指定した visibility: specified なノートが含まれる', async () => {
 			const [alice, bob] = await Promise.all([signup(), signup()]);
 
 			await api('following/create', { userId: bob.id }, alice);
-			await sleep(1000);
+			await setTimeout(1000);
 			const bobNote = await post(bob, { text: 'hi', visibility: 'specified', visibleUserIds: [alice.id] });
 
 			await waitForPushToTl();
 
 			const res = await api('notes/timeline', { limit: 100 }, alice);
 
-			assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
-			assert.strictEqual(res.body.find((note: any) => note.id === bobNote.id).text, 'hi');
+			assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
+			assert.strictEqual(res.body.find(note => note.id === bobNote.id)?.text, 'hi');
 		});
 
 		test.concurrent('フォローしていないユーザーの自身を visibleUserIds に指定した visibility: specified なノートが含まれない', async () => {
@@ -423,21 +451,21 @@ describe('Timelines', () => {
 
 			const res = await api('notes/timeline', { limit: 100 }, alice);
 
-			assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
+			assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
 		});
 
 		test.concurrent('フォローしているユーザーの自身を visibleUserIds に指定していない visibility: specified なノートが含まれない', async () => {
 			const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]);
 
 			await api('following/create', { userId: bob.id }, alice);
-			await sleep(1000);
+			await setTimeout(1000);
 			const bobNote = await post(bob, { text: 'hi', visibility: 'specified', visibleUserIds: [carol.id] });
 
 			await waitForPushToTl();
 
 			const res = await api('notes/timeline', { limit: 100 }, alice);
 
-			assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
+			assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
 		});
 
 		test.concurrent('フォローしていないユーザーからの visibility: specified なノートに返信したときの自身のノートが含まれる', async () => {
@@ -450,8 +478,8 @@ describe('Timelines', () => {
 
 			const res = await api('notes/timeline', { limit: 100 }, alice);
 
-			assert.strictEqual(res.body.some((note: any) => note.id === aliceNote.id), true);
-			assert.strictEqual(res.body.find((note: any) => note.id === aliceNote.id).text, 'ok');
+			assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true);
+			assert.strictEqual(res.body.find(note => note.id === aliceNote.id)?.text, 'ok');
 		});
 
 		/* TODO
@@ -465,8 +493,8 @@ describe('Timelines', () => {
 
 			const res = await api('notes/timeline', { limit: 100 }, alice);
 
-			assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
-			assert.strictEqual(res.body.find((note: any) => note.id === bobNote.id).text, 'ok');
+			assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
+			assert.strictEqual(res.body.find(note => note.id === bobNote.id).text, 'ok');
 		});
 		*/
 
@@ -481,7 +509,45 @@ describe('Timelines', () => {
 
 			const res = await api('notes/timeline', { limit: 100 }, alice);
 
-			assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
+			assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
+		});
+
+		test.concurrent('FTT: ローカルユーザーの HTL にはプッシュされる', async () => {
+			const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]);
+
+			await api('following/create', {
+				userId: alice.id,
+			}, bob);
+
+			const aliceNote = await post(alice, { text: 'I\'m Alice.' });
+			const bobNote = await post(bob, { text: 'I\'m Bob.' });
+			const carolNote = await post(carol, { text: 'I\'m Carol.' });
+
+			await waitForPushToTl();
+
+			// NOTE: notes/timeline だと DB へのフォールバックが効くので Redis を直接見て確かめる
+			assert.strictEqual(await redisForTimelines.exists(`list:homeTimeline:${bob.id}`), 1);
+
+			const bobHTL = await redisForTimelines.lrange(`list:homeTimeline:${bob.id}`, 0, -1);
+			assert.strictEqual(bobHTL.includes(aliceNote.id), true);
+			assert.strictEqual(bobHTL.includes(bobNote.id), true);
+			assert.strictEqual(bobHTL.includes(carolNote.id), false);
+		});
+
+		test.concurrent('FTT: リモートユーザーの HTL にはプッシュされない', async () => {
+			const [alice, bob] = await Promise.all([signup(), signup({ host: genHost() })]);
+
+			await api('following/create', {
+				userId: alice.id,
+			}, bob);
+
+			await post(alice, { text: 'I\'m Alice.' });
+			await post(bob, { text: 'I\'m Bob.' });
+
+			await waitForPushToTl();
+
+			// NOTE: notes/timeline だと DB へのフォールバックが効くので Redis を直接見て確かめる
+			assert.strictEqual(await redisForTimelines.exists(`list:homeTimeline:${bob.id}`), 0);
 		});
 	});
 
@@ -496,8 +562,8 @@ describe('Timelines', () => {
 
 			const res = await api('notes/local-timeline', { limit: 100 }, alice);
 
-			assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
-			assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false);
+			assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
+			assert.strictEqual(res.body.some(note => note.id === carolNote.id), false);
 		});
 
 		test.concurrent('他人の他人への返信が含まれない', async () => {
@@ -510,8 +576,8 @@ describe('Timelines', () => {
 
 			const res = await api('notes/local-timeline', { limit: 100 }, alice);
 
-			assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
-			assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), true);
+			assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
+			assert.strictEqual(res.body.some(note => note.id === carolNote.id), true);
 		});
 
 		test.concurrent('他人のその人自身への返信が含まれる', async () => {
@@ -524,8 +590,8 @@ describe('Timelines', () => {
 
 			const res = await api('notes/local-timeline', { limit: 100 }, alice);
 
-			assert.strictEqual(res.body.some((note: any) => note.id === bobNote1.id), true);
-			assert.strictEqual(res.body.some((note: any) => note.id === bobNote2.id), true);
+			assert.strictEqual(res.body.some(note => note.id === bobNote1.id), true);
+			assert.strictEqual(res.body.some(note => note.id === bobNote2.id), true);
 		});
 
 		test.concurrent('チャンネル投稿が含まれない', async () => {
@@ -538,7 +604,7 @@ describe('Timelines', () => {
 
 			const res = await api('notes/local-timeline', { limit: 100 }, alice);
 
-			assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
+			assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
 		});
 
 		test.concurrent('リモートユーザーのノートが含まれない', async () => {
@@ -550,7 +616,7 @@ describe('Timelines', () => {
 
 			const res = await api('notes/local-timeline', { limit: 100 }, alice);
 
-			assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
+			assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
 		});
 
 		// 含まれても良いと思うけど実装が面倒なので含まれない
@@ -558,7 +624,7 @@ describe('Timelines', () => {
 			const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]);
 
 			await api('following/create', { userId: carol.id }, alice);
-			await sleep(1000);
+			await setTimeout(1000);
 			const carolNote = await post(carol, { text: 'hi', visibility: 'home' });
 			const bobNote = await post(bob, { text: 'hi' });
 
@@ -566,15 +632,15 @@ describe('Timelines', () => {
 
 			const res = await api('notes/local-timeline', { limit: 100 }, alice);
 
-			assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
-			assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false);
+			assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
+			assert.strictEqual(res.body.some(note => note.id === carolNote.id), false);
 		});
 
 		test.concurrent('ミュートしているユーザーのノートが含まれない', async () => {
 			const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]);
 
 			await api('mute/create', { userId: carol.id }, alice);
-			await sleep(1000);
+			await setTimeout(1000);
 			const carolNote = await post(carol, { text: 'hi' });
 			const bobNote = await post(bob, { text: 'hi' });
 
@@ -582,8 +648,8 @@ describe('Timelines', () => {
 
 			const res = await api('notes/local-timeline', { limit: 100 }, alice);
 
-			assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
-			assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false);
+			assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
+			assert.strictEqual(res.body.some(note => note.id === carolNote.id), false);
 		});
 
 		test.concurrent('フォローしているユーザーが行ったミュートしているユーザーのリノートが含まれない', async () => {
@@ -591,7 +657,7 @@ describe('Timelines', () => {
 
 			await api('following/create', { userId: bob.id }, alice);
 			await api('mute/create', { userId: carol.id }, alice);
-			await sleep(1000);
+			await setTimeout(1000);
 			const carolNote = await post(carol, { text: 'hi' });
 			const bobNote = await post(bob, { text: 'hi', renoteId: carolNote.id });
 
@@ -599,8 +665,8 @@ describe('Timelines', () => {
 
 			const res = await api('notes/local-timeline', { limit: 100 }, alice);
 
-			assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
-			assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false);
+			assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
+			assert.strictEqual(res.body.some(note => note.id === carolNote.id), false);
 		});
 
 		test.concurrent('withReplies: true でフォローしているユーザーが行ったミュートしているユーザーの投稿への返信が含まれない', async () => {
@@ -609,7 +675,7 @@ describe('Timelines', () => {
 			await api('following/create', { userId: bob.id }, alice);
 			await api('following/update', { userId: bob.id, withReplies: true }, alice);
 			await api('mute/create', { userId: carol.id }, alice);
-			await sleep(1000);
+			await setTimeout(1000);
 			const carolNote = await post(carol, { text: 'hi' });
 			const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id });
 
@@ -617,15 +683,15 @@ describe('Timelines', () => {
 
 			const res = await api('notes/local-timeline', { limit: 100 }, alice);
 
-			assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
-			assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false);
+			assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
+			assert.strictEqual(res.body.some(note => note.id === carolNote.id), false);
 		});
 
 		test.concurrent('withReplies: false でフォローしているユーザーからの自分への返信が含まれる', async () => {
 			const [alice, bob] = await Promise.all([signup(), signup()]);
 
 			await api('following/create', { userId: bob.id }, alice);
-			await sleep(1000);
+			await setTimeout(1000);
 			const aliceNote = await post(alice, { text: 'hi' });
 			const bobNote = await post(bob, { text: 'hi', replyId: aliceNote.id });
 
@@ -633,8 +699,23 @@ describe('Timelines', () => {
 
 			const res = await api('notes/local-timeline', { limit: 100 }, alice);
 
-			assert.strictEqual(res.body.some((note: any) => note.id === aliceNote.id), true);
-			assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
+			assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true);
+			assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
+		});
+
+		test.concurrent('withReplies: false でフォローしていないユーザーからの自分への返信が含まれる', async () => {
+			const [alice, bob] = await Promise.all([signup(), signup()]);
+
+			await setTimeout(1000);
+			const aliceNote = await post(alice, { text: 'hi' });
+			const bobNote = await post(bob, { text: 'hi', replyId: aliceNote.id });
+
+			await waitForPushToTl();
+
+			const res = await api('notes/local-timeline', { limit: 100 }, alice);
+
+			assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true);
+			assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
 		});
 
 		test.concurrent('[withReplies: true] 他人の他人への返信が含まれる', async () => {
@@ -647,7 +728,7 @@ describe('Timelines', () => {
 
 			const res = await api('notes/local-timeline', { limit: 100, withReplies: true }, alice);
 
-			assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
+			assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
 		});
 
 		test.concurrent('[withFiles: true] ファイル付きノートのみ含まれる', async () => {
@@ -661,8 +742,8 @@ describe('Timelines', () => {
 
 			const res = await api('notes/local-timeline', { limit: 100, withFiles: true }, alice);
 
-			assert.strictEqual(res.body.some((note: any) => note.id === bobNote1.id), false);
-			assert.strictEqual(res.body.some((note: any) => note.id === bobNote2.id), true);
+			assert.strictEqual(res.body.some(note => note.id === bobNote1.id), false);
+			assert.strictEqual(res.body.some(note => note.id === bobNote2.id), true);
 		}, 1000 * 10);
 	});
 
@@ -676,7 +757,7 @@ describe('Timelines', () => {
 
 			const res = await api('notes/hybrid-timeline', { limit: 100 }, alice);
 
-			assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
+			assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
 		});
 
 		test.concurrent('ローカルユーザーの visibility: home なノートが含まれない', async () => {
@@ -688,28 +769,28 @@ describe('Timelines', () => {
 
 			const res = await api('notes/hybrid-timeline', { limit: 100 }, alice);
 
-			assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
+			assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
 		});
 
 		test.concurrent('フォローしているローカルユーザーの visibility: home なノートが含まれる', async () => {
 			const [alice, bob] = await Promise.all([signup(), signup()]);
 
 			await api('following/create', { userId: bob.id }, alice);
-			await sleep(1000);
+			await setTimeout(1000);
 			const bobNote = await post(bob, { text: 'hi', visibility: 'home' });
 
 			await waitForPushToTl();
 
 			const res = await api('notes/hybrid-timeline', { limit: 100 }, alice);
 
-			assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
+			assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
 		});
 
 		test.concurrent('withReplies: false でフォローしているユーザーからの自分への返信が含まれる', async () => {
 			const [alice, bob] = await Promise.all([signup(), signup()]);
 
 			await api('following/create', { userId: bob.id }, alice);
-			await sleep(1000);
+			await setTimeout(1000);
 			const aliceNote = await post(alice, { text: 'hi' });
 			const bobNote = await post(bob, { text: 'hi', replyId: aliceNote.id });
 
@@ -717,8 +798,64 @@ describe('Timelines', () => {
 
 			const res = await api('notes/hybrid-timeline', { limit: 100 }, alice);
 
-			assert.strictEqual(res.body.some((note: any) => note.id === aliceNote.id), true);
+			assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true);
+			assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
+		});
+
+		test.concurrent('withReplies: true でフォローしているユーザーの他人の visibility: followers な投稿への返信が含まれない', async () => {
+			const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]);
+
+			await api('following/create', { userId: carol.id }, bob);
+			await api('following/create', { userId: bob.id }, alice);
+			await api('following/update', { userId: bob.id, withReplies: true }, alice);
+			await setTimeout(1000);
+			const carolNote = await post(carol, { text: 'hi', visibility: 'followers' });
+			const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id });
+
+			await waitForPushToTl();
+
+			const res = await api('notes/hybrid-timeline', { limit: 100 }, alice);
+
+			assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
+			assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false);
+		});
+
+		test.concurrent('withReplies: true でフォローしているユーザーの行った別のフォローしているユーザーの visibility: followers な投稿への返信が含まれる', async () => {
+			const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]);
+
+			await api('following/create', { userId: bob.id }, alice);
+			await api('following/create', { userId: carol.id }, alice);
+			await api('following/create', { userId: carol.id }, bob);
+			await api('following/update', { userId: bob.id, withReplies: true }, alice);
+			await setTimeout(1000);
+			const carolNote = await post(carol, { text: 'hi', visibility: 'followers' });
+			const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id });
+
+			await waitForPushToTl();
+
+			const res = await api('notes/hybrid-timeline', { limit: 100 }, alice);
+
 			assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
+			assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), true);
+			assert.strictEqual(res.body.find((note: any) => note.id === carolNote.id)?.text, 'hi');
+		});
+
+		test.concurrent('withReplies: true でフォローしているユーザーの自分の visibility: followers な投稿への返信が含まれる', async () => {
+			const [alice, bob] = await Promise.all([signup(), signup()]);
+
+			await api('following/create', { userId: bob.id }, alice);
+			await api('following/create', { userId: alice.id }, bob);
+			await api('following/update', { userId: bob.id, withReplies: true }, alice);
+			await setTimeout(1000);
+			const aliceNote = await post(alice, { text: 'hi', visibility: 'followers' });
+			const bobNote = await post(bob, { text: 'hi', replyId: aliceNote.id });
+
+			await waitForPushToTl();
+
+			const res = await api('notes/hybrid-timeline', { limit: 100 }, alice);
+
+			assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
+			assert.strictEqual(res.body.some((note: any) => note.id === aliceNote.id), true);
 		});
 
 		test.concurrent('他人の他人への返信が含まれない', async () => {
@@ -731,8 +868,8 @@ describe('Timelines', () => {
 
 			const res = await api('notes/hybrid-timeline', { limit: 100 }, alice);
 
-			assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
-			assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), true);
+			assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
+			assert.strictEqual(res.body.some(note => note.id === carolNote.id), true);
 		});
 
 		test.concurrent('リモートユーザーのノートが含まれない', async () => {
@@ -744,7 +881,7 @@ describe('Timelines', () => {
 
 			const res = await api('notes/local-timeline', { limit: 100 }, alice);
 
-			assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
+			assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
 		});
 
 		test.concurrent('フォローしているリモートユーザーのノートが含まれる', async () => {
@@ -759,7 +896,7 @@ describe('Timelines', () => {
 
 			const res = await api('notes/hybrid-timeline', { limit: 100 }, alice);
 
-			assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
+			assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
 		});
 
 		test.concurrent('フォローしているリモートユーザーの visibility: home なノートが含まれる', async () => {
@@ -774,7 +911,22 @@ describe('Timelines', () => {
 
 			const res = await api('notes/hybrid-timeline', { limit: 100 }, alice);
 
-			assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
+			assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
+		});
+
+		test.concurrent('withReplies: false でフォローしていないユーザーからの自分への返信が含まれる', async () => {
+			const [alice, bob] = await Promise.all([signup(), signup()]);
+
+			await setTimeout(1000);
+			const aliceNote = await post(alice, { text: 'hi' });
+			const bobNote = await post(bob, { text: 'hi', replyId: aliceNote.id });
+
+			await waitForPushToTl();
+
+			const res = await api('notes/local-timeline', { limit: 100 }, alice);
+
+			assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true);
+			assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
 		});
 
 		test.concurrent('[withReplies: true] 他人の他人への返信が含まれる', async () => {
@@ -787,7 +939,7 @@ describe('Timelines', () => {
 
 			const res = await api('notes/hybrid-timeline', { limit: 100, withReplies: true }, alice);
 
-			assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
+			assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
 		});
 
 		test.concurrent('[withFiles: true] ファイル付きノートのみ含まれる', async () => {
@@ -801,8 +953,8 @@ describe('Timelines', () => {
 
 			const res = await api('notes/hybrid-timeline', { limit: 100, withFiles: true }, alice);
 
-			assert.strictEqual(res.body.some((note: any) => note.id === bobNote1.id), false);
-			assert.strictEqual(res.body.some((note: any) => note.id === bobNote2.id), true);
+			assert.strictEqual(res.body.some(note => note.id === bobNote1.id), false);
+			assert.strictEqual(res.body.some(note => note.id === bobNote2.id), true);
 		}, 1000 * 10);
 	});
 
@@ -812,14 +964,14 @@ describe('Timelines', () => {
 
 			const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body);
 			await api('users/lists/push', { listId: list.id, userId: bob.id }, alice);
-			await sleep(1000);
+			await setTimeout(1000);
 			const bobNote = await post(bob, { text: 'hi' });
 
 			await waitForPushToTl();
 
 			const res = await api('notes/user-list-timeline', { listId: list.id }, alice);
 
-			assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
+			assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
 		});
 
 		test.concurrent('リスインしているフォローしていないユーザーの visibility: home なノートが含まれる', async () => {
@@ -827,14 +979,14 @@ describe('Timelines', () => {
 
 			const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body);
 			await api('users/lists/push', { listId: list.id, userId: bob.id }, alice);
-			await sleep(1000);
+			await setTimeout(1000);
 			const bobNote = await post(bob, { text: 'hi', visibility: 'home' });
 
 			await waitForPushToTl();
 
 			const res = await api('notes/user-list-timeline', { listId: list.id }, alice);
 
-			assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
+			assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
 		});
 
 		test.concurrent('リスインしているフォローしていないユーザーの visibility: followers なノートが含まれない', async () => {
@@ -842,14 +994,14 @@ describe('Timelines', () => {
 
 			const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body);
 			await api('users/lists/push', { listId: list.id, userId: bob.id }, alice);
-			await sleep(1000);
+			await setTimeout(1000);
 			const bobNote = await post(bob, { text: 'hi', visibility: 'followers' });
 
 			await waitForPushToTl();
 
 			const res = await api('notes/user-list-timeline', { listId: list.id }, alice);
 
-			assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
+			assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
 		});
 
 		test.concurrent('リスインしているフォローしていないユーザーの他人への返信が含まれない', async () => {
@@ -857,7 +1009,7 @@ describe('Timelines', () => {
 
 			const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body);
 			await api('users/lists/push', { listId: list.id, userId: bob.id }, alice);
-			await sleep(1000);
+			await setTimeout(1000);
 			const carolNote = await post(carol, { text: 'hi' });
 			const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id });
 
@@ -865,7 +1017,7 @@ describe('Timelines', () => {
 
 			const res = await api('notes/user-list-timeline', { listId: list.id }, alice);
 
-			assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
+			assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
 		});
 
 		test.concurrent('リスインしているフォローしていないユーザーのユーザー自身への返信が含まれる', async () => {
@@ -873,7 +1025,7 @@ describe('Timelines', () => {
 
 			const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body);
 			await api('users/lists/push', { listId: list.id, userId: bob.id }, alice);
-			await sleep(1000);
+			await setTimeout(1000);
 			const bobNote1 = await post(bob, { text: 'hi' });
 			const bobNote2 = await post(bob, { text: 'hi', replyId: bobNote1.id });
 
@@ -881,8 +1033,8 @@ describe('Timelines', () => {
 
 			const res = await api('notes/user-list-timeline', { listId: list.id }, alice);
 
-			assert.strictEqual(res.body.some((note: any) => note.id === bobNote1.id), true);
-			assert.strictEqual(res.body.some((note: any) => note.id === bobNote2.id), true);
+			assert.strictEqual(res.body.some(note => note.id === bobNote1.id), true);
+			assert.strictEqual(res.body.some(note => note.id === bobNote2.id), true);
 		});
 
 		test.concurrent('withReplies: false でリスインしているフォローしていないユーザーからの自分への返信が含まれる', async () => {
@@ -891,7 +1043,7 @@ describe('Timelines', () => {
 			const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body);
 			await api('users/lists/push', { listId: list.id, userId: bob.id }, alice);
 			await api('users/lists/update-membership', { listId: list.id, userId: bob.id, withReplies: false }, alice);
-			await sleep(1000);
+			await setTimeout(1000);
 			const aliceNote = await post(alice, { text: 'hi' });
 			const bobNote = await post(bob, { text: 'hi', replyId: aliceNote.id });
 
@@ -899,7 +1051,7 @@ describe('Timelines', () => {
 
 			const res = await api('notes/user-list-timeline', { listId: list.id }, alice);
 
-			assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
+			assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
 		});
 
 		test.concurrent('withReplies: false でリスインしているフォローしていないユーザーの他人への返信が含まれない', async () => {
@@ -908,7 +1060,7 @@ describe('Timelines', () => {
 			const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body);
 			await api('users/lists/push', { listId: list.id, userId: bob.id }, alice);
 			await api('users/lists/update-membership', { listId: list.id, userId: bob.id, withReplies: false }, alice);
-			await sleep(1000);
+			await setTimeout(1000);
 			const carolNote = await post(carol, { text: 'hi' });
 			const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id });
 
@@ -916,7 +1068,7 @@ describe('Timelines', () => {
 
 			const res = await api('notes/user-list-timeline', { listId: list.id }, alice);
 
-			assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
+			assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
 		});
 
 		test.concurrent('withReplies: true でリスインしているフォローしていないユーザーの他人への返信が含まれる', async () => {
@@ -925,7 +1077,7 @@ describe('Timelines', () => {
 			const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body);
 			await api('users/lists/push', { listId: list.id, userId: bob.id }, alice);
 			await api('users/lists/update-membership', { listId: list.id, userId: bob.id, withReplies: true }, alice);
-			await sleep(1000);
+			await setTimeout(1000);
 			const carolNote = await post(carol, { text: 'hi' });
 			const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id });
 
@@ -933,7 +1085,7 @@ describe('Timelines', () => {
 
 			const res = await api('notes/user-list-timeline', { listId: list.id }, alice);
 
-			assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
+			assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
 		});
 
 		test.concurrent('リスインしているフォローしているユーザーの visibility: home なノートが含まれる', async () => {
@@ -942,14 +1094,14 @@ describe('Timelines', () => {
 			await api('following/create', { userId: bob.id }, alice);
 			const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body);
 			await api('users/lists/push', { listId: list.id, userId: bob.id }, alice);
-			await sleep(1000);
+			await setTimeout(1000);
 			const bobNote = await post(bob, { text: 'hi', visibility: 'home' });
 
 			await waitForPushToTl();
 
 			const res = await api('notes/user-list-timeline', { listId: list.id }, alice);
 
-			assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
+			assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
 		});
 
 		test.concurrent('リスインしているフォローしているユーザーの visibility: followers なノートが含まれる', async () => {
@@ -958,15 +1110,15 @@ describe('Timelines', () => {
 			await api('following/create', { userId: bob.id }, alice);
 			const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body);
 			await api('users/lists/push', { listId: list.id, userId: bob.id }, alice);
-			await sleep(1000);
+			await setTimeout(1000);
 			const bobNote = await post(bob, { text: 'hi', visibility: 'followers' });
 
 			await waitForPushToTl();
 
 			const res = await api('notes/user-list-timeline', { listId: list.id }, alice);
 
-			assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
-			assert.strictEqual(res.body.find((note: any) => note.id === bobNote.id).text, 'hi');
+			assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
+			assert.strictEqual(res.body.find(note => note.id === bobNote.id)?.text, 'hi');
 		});
 
 		test.concurrent('リスインしている自分の visibility: followers なノートが含まれる', async () => {
@@ -974,15 +1126,15 @@ describe('Timelines', () => {
 
 			const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body);
 			await api('users/lists/push', { listId: list.id, userId: alice.id }, alice);
-			await sleep(1000);
+			await setTimeout(1000);
 			const aliceNote = await post(alice, { text: 'hi', visibility: 'followers' });
 
 			await waitForPushToTl();
 
 			const res = await api('notes/user-list-timeline', { listId: list.id }, alice);
 
-			assert.strictEqual(res.body.some((note: any) => note.id === aliceNote.id), true);
-			assert.strictEqual(res.body.find((note: any) => note.id === aliceNote.id).text, 'hi');
+			assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true);
+			assert.strictEqual(res.body.find(note => note.id === aliceNote.id)?.text, 'hi');
 		});
 
 		test.concurrent('リスインしているユーザーのチャンネルノートが含まれない', async () => {
@@ -991,14 +1143,14 @@ describe('Timelines', () => {
 			const channel = await api('channels/create', { name: 'channel' }, bob).then(x => x.body);
 			const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body);
 			await api('users/lists/push', { listId: list.id, userId: bob.id }, alice);
-			await sleep(1000);
+			await setTimeout(1000);
 			const bobNote = await post(bob, { text: 'hi', channelId: channel.id });
 
 			await waitForPushToTl();
 
 			const res = await api('notes/user-list-timeline', { listId: list.id }, alice);
 
-			assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
+			assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
 		});
 
 		test.concurrent('[withFiles: true] リスインしているユーザーのファイル付きノートのみ含まれる', async () => {
@@ -1014,8 +1166,8 @@ describe('Timelines', () => {
 
 			const res = await api('notes/user-list-timeline', { listId: list.id, withFiles: true }, alice);
 
-			assert.strictEqual(res.body.some((note: any) => note.id === bobNote1.id), false);
-			assert.strictEqual(res.body.some((note: any) => note.id === bobNote2.id), true);
+			assert.strictEqual(res.body.some(note => note.id === bobNote1.id), false);
+			assert.strictEqual(res.body.some(note => note.id === bobNote2.id), true);
 		}, 1000 * 10);
 
 		test.concurrent('リスインしているユーザーの自身宛ての visibility: specified なノートが含まれる', async () => {
@@ -1023,15 +1175,15 @@ describe('Timelines', () => {
 
 			const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body);
 			await api('users/lists/push', { listId: list.id, userId: bob.id }, alice);
-			await sleep(1000);
+			await setTimeout(1000);
 			const bobNote = await post(bob, { text: 'hi', visibility: 'specified', visibleUserIds: [alice.id] });
 
 			await waitForPushToTl();
 
 			const res = await api('notes/user-list-timeline', { listId: list.id }, alice);
 
-			assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
-			assert.strictEqual(res.body.find((note: any) => note.id === bobNote.id).text, 'hi');
+			assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
+			assert.strictEqual(res.body.find(note => note.id === bobNote.id)?.text, 'hi');
 		});
 
 		test.concurrent('リスインしているユーザーの自身宛てではない visibility: specified なノートが含まれない', async () => {
@@ -1040,14 +1192,14 @@ describe('Timelines', () => {
 			const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body);
 			await api('users/lists/push', { listId: list.id, userId: bob.id }, alice);
 			await api('users/lists/push', { listId: list.id, userId: carol.id }, alice);
-			await sleep(1000);
+			await setTimeout(1000);
 			const bobNote = await post(bob, { text: 'hi', visibility: 'specified', visibleUserIds: [carol.id] });
 
 			await waitForPushToTl();
 
 			const res = await api('notes/user-list-timeline', { listId: list.id }, alice);
 
-			assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
+			assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
 		});
 	});
 
@@ -1061,7 +1213,7 @@ describe('Timelines', () => {
 
 			const res = await api('users/notes', { userId: bob.id }, alice);
 
-			assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
+			assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
 		});
 
 		test.concurrent('フォローしていないユーザーの visibility: followers なノートが含まれない', async () => {
@@ -1073,22 +1225,22 @@ describe('Timelines', () => {
 
 			const res = await api('users/notes', { userId: bob.id }, alice);
 
-			assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
+			assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
 		});
 
 		test.concurrent('フォローしているユーザーの visibility: followers なノートが含まれる', async () => {
 			const [alice, bob] = await Promise.all([signup(), signup()]);
 
 			await api('following/create', { userId: bob.id }, alice);
-			await sleep(1000);
+			await setTimeout(1000);
 			const bobNote = await post(bob, { text: 'hi', visibility: 'followers' });
 
 			await waitForPushToTl();
 
 			const res = await api('users/notes', { userId: bob.id }, alice);
 
-			assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
-			assert.strictEqual(res.body.find((note: any) => note.id === bobNote.id).text, 'hi');
+			assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
+			assert.strictEqual(res.body.find(note => note.id === bobNote.id)?.text, 'hi');
 		});
 
 		test.concurrent('自身の visibility: followers なノートが含まれる', async () => {
@@ -1100,8 +1252,8 @@ describe('Timelines', () => {
 
 			const res = await api('users/notes', { userId: alice.id }, alice);
 
-			assert.strictEqual(res.body.some((note: any) => note.id === aliceNote.id), true);
-			assert.strictEqual(res.body.find((note: any) => note.id === aliceNote.id).text, 'hi');
+			assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true);
+			assert.strictEqual(res.body.find(note => note.id === aliceNote.id)?.text, 'hi');
 		});
 
 		test.concurrent('チャンネル投稿が含まれない', async () => {
@@ -1114,7 +1266,7 @@ describe('Timelines', () => {
 
 			const res = await api('users/notes', { userId: bob.id }, alice);
 
-			assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
+			assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
 		});
 
 		test.concurrent('[withReplies: false] 他人への返信が含まれない', async () => {
@@ -1128,8 +1280,8 @@ describe('Timelines', () => {
 
 			const res = await api('users/notes', { userId: bob.id }, alice);
 
-			assert.strictEqual(res.body.some((note: any) => note.id === bobNote1.id), true);
-			assert.strictEqual(res.body.some((note: any) => note.id === bobNote2.id), false);
+			assert.strictEqual(res.body.some(note => note.id === bobNote1.id), true);
+			assert.strictEqual(res.body.some(note => note.id === bobNote2.id), false);
 		});
 
 		test.concurrent('[withReplies: true] 他人への返信が含まれる', async () => {
@@ -1143,8 +1295,8 @@ describe('Timelines', () => {
 
 			const res = await api('users/notes', { userId: bob.id, withReplies: true }, alice);
 
-			assert.strictEqual(res.body.some((note: any) => note.id === bobNote1.id), true);
-			assert.strictEqual(res.body.some((note: any) => note.id === bobNote2.id), true);
+			assert.strictEqual(res.body.some(note => note.id === bobNote1.id), true);
+			assert.strictEqual(res.body.some(note => note.id === bobNote2.id), true);
 		});
 
 		test.concurrent('[withReplies: true] 他人への visibility: specified な返信が含まれない', async () => {
@@ -1158,8 +1310,8 @@ describe('Timelines', () => {
 
 			const res = await api('users/notes', { userId: bob.id, withReplies: true }, alice);
 
-			assert.strictEqual(res.body.some((note: any) => note.id === bobNote1.id), true);
-			assert.strictEqual(res.body.some((note: any) => note.id === bobNote2.id), false);
+			assert.strictEqual(res.body.some(note => note.id === bobNote1.id), true);
+			assert.strictEqual(res.body.some(note => note.id === bobNote2.id), false);
 		});
 
 		test.concurrent('[withFiles: true] ファイル付きノートのみ含まれる', async () => {
@@ -1173,8 +1325,8 @@ describe('Timelines', () => {
 
 			const res = await api('users/notes', { userId: bob.id, withFiles: true }, alice);
 
-			assert.strictEqual(res.body.some((note: any) => note.id === bobNote1.id), false);
-			assert.strictEqual(res.body.some((note: any) => note.id === bobNote2.id), true);
+			assert.strictEqual(res.body.some(note => note.id === bobNote1.id), false);
+			assert.strictEqual(res.body.some(note => note.id === bobNote2.id), true);
 		}, 1000 * 10);
 
 		test.concurrent('[withChannelNotes: true] チャンネル投稿が含まれる', async () => {
@@ -1187,7 +1339,7 @@ describe('Timelines', () => {
 
 			const res = await api('users/notes', { userId: bob.id, withChannelNotes: true }, alice);
 
-			assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
+			assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
 		});
 
 		test.concurrent('[withChannelNotes: true] 他人が取得した場合センシティブチャンネル投稿が含まれない', async () => {
@@ -1200,7 +1352,7 @@ describe('Timelines', () => {
 
 			const res = await api('users/notes', { userId: bob.id, withChannelNotes: true }, alice);
 
-			assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
+			assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
 		});
 
 		test.concurrent('[withChannelNotes: true] 自分が取得した場合センシティブチャンネル投稿が含まれる', async () => {
@@ -1213,14 +1365,14 @@ describe('Timelines', () => {
 
 			const res = await api('users/notes', { userId: bob.id, withChannelNotes: true }, bob);
 
-			assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
+			assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
 		});
 
 		test.concurrent('ミュートしているユーザーに関連する投稿が含まれない', async () => {
 			const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]);
 
 			await api('mute/create', { userId: carol.id }, alice);
-			await sleep(1000);
+			await setTimeout(1000);
 			const carolNote = await post(carol, { text: 'hi' });
 			const bobNote = await post(bob, { text: 'hi', renoteId: carolNote.id });
 
@@ -1228,14 +1380,14 @@ describe('Timelines', () => {
 
 			const res = await api('users/notes', { userId: bob.id }, alice);
 
-			assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
+			assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
 		});
 
 		test.concurrent('ミュートしていても userId に指定したユーザーの投稿が含まれる', async () => {
 			const [alice, bob] = await Promise.all([signup(), signup()]);
 
 			await api('mute/create', { userId: bob.id }, alice);
-			await sleep(1000);
+			await setTimeout(1000);
 			const bobNote1 = await post(bob, { text: 'hi' });
 			const bobNote2 = await post(bob, { text: 'hi', replyId: bobNote1.id });
 			const bobNote3 = await post(bob, { text: 'hi', renoteId: bobNote1.id });
@@ -1244,9 +1396,9 @@ describe('Timelines', () => {
 
 			const res = await api('users/notes', { userId: bob.id }, alice);
 
-			assert.strictEqual(res.body.some((note: any) => note.id === bobNote1.id), true);
-			assert.strictEqual(res.body.some((note: any) => note.id === bobNote2.id), true);
-			assert.strictEqual(res.body.some((note: any) => note.id === bobNote3.id), true);
+			assert.strictEqual(res.body.some(note => note.id === bobNote1.id), true);
+			assert.strictEqual(res.body.some(note => note.id === bobNote2.id), true);
+			assert.strictEqual(res.body.some(note => note.id === bobNote3.id), true);
 		});
 
 		test.concurrent('自身の visibility: specified なノートが含まれる', async () => {
@@ -1258,7 +1410,7 @@ describe('Timelines', () => {
 
 			const res = await api('users/notes', { userId: alice.id, withReplies: true }, alice);
 
-			assert.strictEqual(res.body.some((note: any) => note.id === aliceNote.id), true);
+			assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true);
 		});
 
 		test.concurrent('visibleUserIds に指定されてない visibility: specified なノートが含まれない', async () => {
@@ -1270,7 +1422,34 @@ describe('Timelines', () => {
 
 			const res = await api('users/notes', { userId: bob.id, withReplies: true }, alice);
 
-			assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
+			assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
+		});
+
+		/** @see https://github.com/misskey-dev/misskey/issues/14000 */
+		test.concurrent('FTT: sinceId にキャッシュより古いノートを指定しても、sinceId による絞り込みが正しく動作する', async () => {
+			const alice = await signup();
+			const noteSince = await post(alice, { text: 'Note where id will be `sinceId`.' });
+			const note1 = await post(alice, { text: '1' });
+			const note2 = await post(alice, { text: '2' });
+			await redisForTimelines.del('list:userTimeline:' + alice.id);
+			const note3 = await post(alice, { text: '3' });
+
+			const res = await api('users/notes', { userId: alice.id, sinceId: noteSince.id });
+			assert.deepStrictEqual(res.body, [note1, note2, note3]);
+		});
+
+		test.concurrent('FTT: sinceId にキャッシュより古いノートを指定しても、sinceId と untilId による絞り込みが正しく動作する', async () => {
+			const alice = await signup();
+			const noteSince = await post(alice, { text: 'Note where id will be `sinceId`.' });
+			const note1 = await post(alice, { text: '1' });
+			const note2 = await post(alice, { text: '2' });
+			await redisForTimelines.del('list:userTimeline:' + alice.id);
+			const note3 = await post(alice, { text: '3' });
+			const noteUntil = await post(alice, { text: 'Note where id will be `untilId`.' });
+			await post(alice, { text: '4' });
+
+			const res = await api('users/notes', { userId: alice.id, sinceId: noteSince.id, untilId: noteUntil.id });
+			assert.deepStrictEqual(res.body, [note3, note2, note1]);
 		});
 	});
 
diff --git a/packages/backend/test/e2e/user-notes.ts b/packages/backend/test/e2e/user-notes.ts
index 331e053935..cc07c5ae71 100644
--- a/packages/backend/test/e2e/user-notes.ts
+++ b/packages/backend/test/e2e/user-notes.ts
@@ -17,8 +17,8 @@ describe('users/notes', () => {
 
 	beforeAll(async () => {
 		alice = await signup({ username: 'alice' });
-		const jpg = await uploadUrl(alice, 'https://raw.githubusercontent.com/misskey-dev/misskey/develop/packages/backend/test/resources/Lenna.jpg');
-		const png = await uploadUrl(alice, 'https://raw.githubusercontent.com/misskey-dev/misskey/develop/packages/backend/test/resources/Lenna.png');
+		const jpg = await uploadUrl(alice, 'https://raw.githubusercontent.com/misskey-dev/misskey/develop/packages/backend/test/resources/192.jpg');
+		const png = await uploadUrl(alice, 'https://raw.githubusercontent.com/misskey-dev/misskey/develop/packages/backend/test/resources/192.png');
 		jpgNote = await post(alice, {
 			fileIds: [jpg.id],
 		});
diff --git a/packages/backend/test/e2e/users.ts b/packages/backend/test/e2e/users.ts
index 3458e06384..822ca14ae6 100644
--- a/packages/backend/test/e2e/users.ts
+++ b/packages/backend/test/e2e/users.ts
@@ -7,9 +7,9 @@ process.env.NODE_ENV = 'test';
 
 import * as assert from 'assert';
 import { inspect } from 'node:util';
-import { DEFAULT_POLICIES } from '@/core/RoleService.js';
 import { api, post, role, signup, successfulApiCall, uploadFile } from '../utils.js';
 import type * as misskey from 'misskey-js';
+import { DEFAULT_POLICIES } from '@/core/RoleService.js';
 
 describe('ユーザー', () => {
 	// エンティティとしてのユーザーを主眼においたテストを記述する
@@ -83,9 +83,6 @@ describe('ユーザー', () => {
 			publicReactions: user.publicReactions,
 			followingVisibility: user.followingVisibility,
 			followersVisibility: user.followersVisibility,
-			twoFactorEnabled: user.twoFactorEnabled,
-			usePasswordLessLogin: user.usePasswordLessLogin,
-			securityKeys: user.securityKeys,
 			roles: user.roles,
 			memo: user.memo,
 		});
@@ -105,6 +102,7 @@ describe('ユーザー', () => {
 			isRenoteMuted: user.isRenoteMuted ?? false,
 			notify: user.notify ?? 'none',
 			withReplies: user.withReplies ?? false,
+			followedMessage: user.isFollowing ? (user.followedMessage ?? null) : undefined,
 		});
 	};
 
@@ -114,6 +112,7 @@ describe('ユーザー', () => {
 			...userDetailedNotMe(user),
 			avatarId: user.avatarId,
 			bannerId: user.bannerId,
+			followedMessage: user.followedMessage,
 			isModerator: user.isModerator,
 			isAdmin: user.isAdmin,
 			injectFeaturedNote: user.injectFeaturedNote,
@@ -147,6 +146,9 @@ describe('ユーザー', () => {
 			achievements: user.achievements,
 			loggedInDays: user.loggedInDays,
 			policies: user.policies,
+			twoFactorEnabled: user.twoFactorEnabled,
+			usePasswordLessLogin: user.usePasswordLessLogin,
+			securityKeys: user.securityKeys,
 			...(security ? {
 				email: user.email,
 				emailVerified: user.emailVerified,
@@ -231,7 +233,7 @@ describe('ユーザー', () => {
 		rolePublic = await role(root, { isPublic: true, name: 'Public Role' });
 		await api('admin/roles/assign', { userId: userRolePublic.id, roleId: rolePublic.id }, root);
 		userRoleBadge = await signup({ username: 'userRoleBadge' });
-		roleBadge = await role(root, { asBadge: true, name: 'Badge Role' });
+		roleBadge = await role(root, { asBadge: true, name: 'Badge Role', isPublic: true });
 		await api('admin/roles/assign', { userId: userRoleBadge.id, roleId: roleBadge.id }, root);
 		userSilenced = await signup({ username: 'userSilenced' });
 		await post(userSilenced, { text: 'test' });
@@ -341,15 +343,13 @@ describe('ユーザー', () => {
 		assert.strictEqual(response.publicReactions, true);
 		assert.strictEqual(response.followingVisibility, 'public');
 		assert.strictEqual(response.followersVisibility, 'public');
-		assert.strictEqual(response.twoFactorEnabled, false);
-		assert.strictEqual(response.usePasswordLessLogin, false);
-		assert.strictEqual(response.securityKeys, false);
 		assert.deepStrictEqual(response.roles, []);
 		assert.strictEqual(response.memo, null);
 
 		// MeDetailedOnly
 		assert.strictEqual(response.avatarId, null);
 		assert.strictEqual(response.bannerId, null);
+		assert.strictEqual(response.followedMessage, null);
 		assert.strictEqual(response.isModerator, false);
 		assert.strictEqual(response.isAdmin, false);
 		assert.strictEqual(response.injectFeaturedNote, true);
@@ -382,6 +382,9 @@ describe('ユーザー', () => {
 		assert.deepStrictEqual(response.achievements, []);
 		assert.deepStrictEqual(response.loggedInDays, 0);
 		assert.deepStrictEqual(response.policies, DEFAULT_POLICIES);
+		assert.strictEqual(response.twoFactorEnabled, false);
+		assert.strictEqual(response.usePasswordLessLogin, false);
+		assert.strictEqual(response.securityKeys, false);
 		assert.notStrictEqual(response.email, undefined);
 		assert.strictEqual(response.emailVerified, false);
 		assert.deepStrictEqual(response.securityKeysList, []);
@@ -413,6 +416,8 @@ describe('ユーザー', () => {
 		{ parameters: () => ({ description: 'x'.repeat(1500) }) },
 		{ parameters: () => ({ description: 'x' }) },
 		{ parameters: () => ({ description: 'My description' }) },
+		{ parameters: () => ({ followedMessage: null }) },
+		{ parameters: () => ({ followedMessage: 'Thank you' }) },
 		{ parameters: () => ({ location: null }) },
 		{ parameters: () => ({ location: 'x'.repeat(50) }) },
 		{ parameters: () => ({ location: 'x' }) },
@@ -613,6 +618,9 @@ describe('ユーザー', () => {
 		{ label: 'Moderatorになっている', user: () => userModerator, me: () => userModerator, selector: (user: misskey.entities.MeDetailed) => user.isModerator },
 		// @ts-expect-error UserDetailedNotMe doesn't include isModerator
 		{ label: '自分以外から見たときはModeratorか判定できない', user: () => userModerator, selector: (user: misskey.entities.UserDetailedNotMe) => user.isModerator, expected: () => undefined },
+		{ label: '自分から見た場合に二要素認証関連のプロパティがセットされている', user: () => alice, me: () => alice, selector: (user: misskey.entities.MeDetailed) => user.twoFactorEnabled, expected: () => false },
+		{ label: '自分以外から見た場合に二要素認証関連のプロパティがセットされていない', user: () => alice, me: () => bob, selector: (user: misskey.entities.UserDetailedNotMe) => user.twoFactorEnabled, expected: () => undefined },
+		{ label: 'モデレーターから見た場合に二要素認証関連のプロパティがセットされている', user: () => alice, me: () => userModerator, selector: (user: misskey.entities.UserDetailedNotMe) => user.twoFactorEnabled, expected: () => false },
 		{ label: 'サイレンスになっている', user: () => userSilenced, selector: (user: misskey.entities.UserDetailed) => user.isSilenced },
 		// FIXME: 落ちる
 		//{ label: 'サスペンドになっている', user: () => userSuspended, selector: (user: misskey.entities.UserDetailed) => user.isSuspended },
@@ -655,7 +663,16 @@ describe('ユーザー', () => {
 			iconUrl: roleBadge.iconUrl,
 			displayOrder: roleBadge.displayOrder,
 		}]);
-		assert.deepStrictEqual(response.roles, []); // バッヂだからといってrolesが取れるとは限らない
+		assert.deepStrictEqual(response.roles, [{
+			id: roleBadge.id,
+			name: roleBadge.name,
+			color: roleBadge.color,
+			iconUrl: roleBadge.iconUrl,
+			description: roleBadge.description,
+			isModerator: roleBadge.isModerator,
+			isAdministrator: roleBadge.isAdministrator,
+			displayOrder: roleBadge.displayOrder,
+		}]);
 	});
 	test('をID指定のリスト形式で取得することができる(空)', async () => {
 		const parameters = { userIds: [] };
diff --git a/packages/backend/test/eslint.config.js b/packages/backend/test/eslint.config.js
new file mode 100644
index 0000000000..a0f43babad
--- /dev/null
+++ b/packages/backend/test/eslint.config.js
@@ -0,0 +1,22 @@
+import globals from 'globals';
+import tsParser from '@typescript-eslint/parser';
+import sharedConfig from '../../shared/eslint.config.js';
+
+export default [
+	...sharedConfig,
+	{
+		files: ['**/*.ts', '**/*.tsx'],
+		languageOptions: {
+			globals: {
+				...globals.node,
+				...globals.jest,
+			},
+			parserOptions: {
+				parser: tsParser,
+				project: ['./tsconfig.json'],
+				sourceType: 'module',
+				tsconfigRootDir: import.meta.dirname,
+			},
+		},
+	},
+];
diff --git a/packages/backend/test/jest.setup.ts b/packages/backend/test/jest.setup.ts
index 861bc6db66..7c6dd6a55f 100644
--- a/packages/backend/test/jest.setup.ts
+++ b/packages/backend/test/jest.setup.ts
@@ -6,8 +6,6 @@
 import { initTestDb, sendEnvResetRequest } from './utils.js';
 
 beforeAll(async () => {
-	await Promise.all([
-		initTestDb(false),
-		sendEnvResetRequest(),
-	]);
+		await initTestDb(false);
+		await sendEnvResetRequest();
 });
diff --git a/packages/backend/test/misc/mock-resolver.ts b/packages/backend/test/misc/mock-resolver.ts
index 3c7e796700..c8f3db8aac 100644
--- a/packages/backend/test/misc/mock-resolver.ts
+++ b/packages/backend/test/misc/mock-resolver.ts
@@ -17,6 +17,7 @@ import type { UtilityService } from '@/core/UtilityService.js';
 import { bindThis } from '@/decorators.js';
 import type {
 	FollowRequestsRepository,
+	MiMeta,
 	NoteReactionsRepository,
 	NotesRepository,
 	PollsRepository,
@@ -35,6 +36,7 @@ export class MockResolver extends Resolver {
 	constructor(loggerService: LoggerService) {
 		super(
 			{} as Config,
+			{} as MiMeta,
 			{} as UsersRepository,
 			{} as NotesRepository,
 			{} as PollsRepository,
@@ -42,7 +44,6 @@ export class MockResolver extends Resolver {
 			{} as FollowRequestsRepository,
 			{} as UtilityService,
 			{} as InstanceActorService,
-			{} as MetaService,
 			{} as ApRequestService,
 			{} as HttpRequestService,
 			{} as ApRendererService,
diff --git a/packages/backend/test/prelude/maybe.ts b/packages/backend/test/prelude/maybe.ts
deleted file mode 100644
index 16e92216d4..0000000000
--- a/packages/backend/test/prelude/maybe.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and misskey-project
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-import * as assert from 'assert';
-import { just, nothing } from '../../src/misc/prelude/maybe.js';
-
-describe('just', () => {
-	test('has a value', () => {
-		assert.deepStrictEqual(just(3).isJust(), true);
-	});
-
-	test('has the inverse called get', () => {
-		assert.deepStrictEqual(just(3).get(), 3);
-	});
-});
-
-describe('nothing', () => {
-	test('has no value', () => {
-		assert.deepStrictEqual(nothing().isJust(), false);
-	});
-});
diff --git a/packages/backend/test/resources/192.jpg b/packages/backend/test/resources/192.jpg
new file mode 100644
index 0000000000..76374628e0
Binary files /dev/null and b/packages/backend/test/resources/192.jpg differ
diff --git a/packages/backend/test/resources/192.png b/packages/backend/test/resources/192.png
new file mode 100644
index 0000000000..15fd1e3731
Binary files /dev/null and b/packages/backend/test/resources/192.png differ
diff --git a/packages/backend/test/resources/Lenna.jpg b/packages/backend/test/resources/Lenna.jpg
deleted file mode 100644
index 6b5b32281c..0000000000
Binary files a/packages/backend/test/resources/Lenna.jpg and /dev/null differ
diff --git a/packages/backend/test/resources/Lenna.png b/packages/backend/test/resources/Lenna.png
deleted file mode 100644
index 59ef68aabd..0000000000
Binary files a/packages/backend/test/resources/Lenna.png and /dev/null differ
diff --git a/packages/backend/test/unit/AbuseReportNotificationService.ts b/packages/backend/test/unit/AbuseReportNotificationService.ts
new file mode 100644
index 0000000000..235af29f0d
--- /dev/null
+++ b/packages/backend/test/unit/AbuseReportNotificationService.ts
@@ -0,0 +1,347 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { jest } from '@jest/globals';
+import { Test, TestingModule } from '@nestjs/testing';
+import { randomString } from '../utils.js';
+import { AbuseReportNotificationService } from '@/core/AbuseReportNotificationService.js';
+import {
+	AbuseReportNotificationRecipientRepository,
+	MiAbuseReportNotificationRecipient,
+	MiSystemWebhook,
+	MiUser,
+	SystemWebhooksRepository,
+	UserProfilesRepository,
+	UsersRepository,
+} from '@/models/_.js';
+import { DI } from '@/di-symbols.js';
+import { GlobalModule } from '@/GlobalModule.js';
+import { IdService } from '@/core/IdService.js';
+import { EmailService } from '@/core/EmailService.js';
+import { RoleService } from '@/core/RoleService.js';
+import { MetaService } from '@/core/MetaService.js';
+import { ModerationLogService } from '@/core/ModerationLogService.js';
+import { GlobalEventService } from '@/core/GlobalEventService.js';
+import { RecipientMethod } from '@/models/AbuseReportNotificationRecipient.js';
+import { SystemWebhookService } from '@/core/SystemWebhookService.js';
+import { UserEntityService } from '@/core/entities/UserEntityService.js';
+
+process.env.NODE_ENV = 'test';
+
+describe('AbuseReportNotificationService', () => {
+	let app: TestingModule;
+	let service: AbuseReportNotificationService;
+
+	// --------------------------------------------------------------------------------------
+
+	let usersRepository: UsersRepository;
+	let userProfilesRepository: UserProfilesRepository;
+	let systemWebhooksRepository: SystemWebhooksRepository;
+	let abuseReportNotificationRecipientRepository: AbuseReportNotificationRecipientRepository;
+	let idService: IdService;
+	let roleService: jest.Mocked<RoleService>;
+	let emailService: jest.Mocked<EmailService>;
+	let webhookService: jest.Mocked<SystemWebhookService>;
+
+	// --------------------------------------------------------------------------------------
+
+	let root: MiUser;
+	let alice: MiUser;
+	let bob: MiUser;
+	let systemWebhook1: MiSystemWebhook;
+	let systemWebhook2: MiSystemWebhook;
+
+	// --------------------------------------------------------------------------------------
+
+	async function createUser(data: Partial<MiUser> = {}) {
+		const user = await usersRepository
+			.insert({
+				id: idService.gen(),
+				...data,
+			})
+			.then(x => usersRepository.findOneByOrFail(x.identifiers[0]));
+
+		await userProfilesRepository.insert({
+			userId: user.id,
+		});
+
+		return user;
+	}
+
+	async function createWebhook(data: Partial<MiSystemWebhook> = {}) {
+		return systemWebhooksRepository
+			.insert({
+				id: idService.gen(),
+				name: randomString(),
+				on: ['abuseReport'],
+				url: 'https://example.com',
+				secret: randomString(),
+				...data,
+			})
+			.then(x => systemWebhooksRepository.findOneByOrFail(x.identifiers[0]));
+	}
+
+	async function createRecipient(data: Partial<MiAbuseReportNotificationRecipient> = {}) {
+		return abuseReportNotificationRecipientRepository
+			.insert({
+				id: idService.gen(),
+				isActive: true,
+				name: randomString(),
+				...data,
+			})
+			.then(x => abuseReportNotificationRecipientRepository.findOneByOrFail(x.identifiers[0]));
+	}
+
+	// --------------------------------------------------------------------------------------
+
+	beforeAll(async () => {
+		app = await Test
+			.createTestingModule({
+				imports: [
+					GlobalModule,
+				],
+				providers: [
+					AbuseReportNotificationService,
+					IdService,
+					{
+						provide: RoleService, useFactory: () => ({ getModeratorIds: jest.fn() }),
+					},
+					{
+						provide: SystemWebhookService, useFactory: () => ({ enqueueSystemWebhook: jest.fn() }),
+					},
+					{
+						provide: UserEntityService, useFactory: () => ({ pack: (v: any) => v }),
+					},
+					{
+						provide: EmailService, useFactory: () => ({ sendEmail: jest.fn() }),
+					},
+					{
+						provide: MetaService, useFactory: () => ({ fetch: jest.fn() }),
+					},
+					{
+						provide: ModerationLogService, useFactory: () => ({ log: () => Promise.resolve() }),
+					},
+					{
+						provide: GlobalEventService, useFactory: () => ({ publishAdminStream: jest.fn() }),
+					},
+				],
+			})
+			.compile();
+
+		usersRepository = app.get(DI.usersRepository);
+		userProfilesRepository = app.get(DI.userProfilesRepository);
+		systemWebhooksRepository = app.get(DI.systemWebhooksRepository);
+		abuseReportNotificationRecipientRepository = app.get(DI.abuseReportNotificationRecipientRepository);
+
+		service = app.get(AbuseReportNotificationService);
+		idService = app.get(IdService);
+		roleService = app.get(RoleService) as jest.Mocked<RoleService>;
+		emailService = app.get<EmailService>(EmailService) as jest.Mocked<EmailService>;
+		webhookService = app.get<SystemWebhookService>(SystemWebhookService) as jest.Mocked<SystemWebhookService>;
+
+		app.enableShutdownHooks();
+	});
+
+	beforeEach(async () => {
+		root = await createUser({ username: 'root', usernameLower: 'root', isRoot: true });
+		alice = await createUser({ username: 'alice', usernameLower: 'alice', isRoot: false });
+		bob = await createUser({ username: 'bob', usernameLower: 'bob', isRoot: false });
+		systemWebhook1 = await createWebhook();
+		systemWebhook2 = await createWebhook();
+
+		roleService.getModeratorIds.mockResolvedValue([root.id, alice.id, bob.id]);
+	});
+
+	afterEach(async () => {
+		emailService.sendEmail.mockClear();
+		webhookService.enqueueSystemWebhook.mockClear();
+
+		await usersRepository.delete({});
+		await userProfilesRepository.delete({});
+		await systemWebhooksRepository.delete({});
+		await abuseReportNotificationRecipientRepository.delete({});
+	});
+
+	afterAll(async () => {
+		await app.close();
+	});
+
+	// --------------------------------------------------------------------------------------
+
+	describe('createRecipient', () => {
+		test('作成成功1', async () => {
+			const params = {
+				isActive: true,
+				name: randomString(),
+				method: 'email' as RecipientMethod,
+				userId: alice.id,
+				systemWebhookId: null,
+			};
+
+			const recipient1 = await service.createRecipient(params, root);
+			expect(recipient1).toMatchObject(params);
+		});
+
+		test('作成成功2', async () => {
+			const params = {
+				isActive: true,
+				name: randomString(),
+				method: 'webhook' as RecipientMethod,
+				userId: null,
+				systemWebhookId: systemWebhook1.id,
+			};
+
+			const recipient1 = await service.createRecipient(params, root);
+			expect(recipient1).toMatchObject(params);
+		});
+	});
+
+	describe('updateRecipient', () => {
+		test('更新成功1', async () => {
+			const recipient1 = await createRecipient({
+				method: 'email',
+				userId: alice.id,
+			});
+
+			const params = {
+				id: recipient1.id,
+				isActive: false,
+				name: randomString(),
+				method: 'email' as RecipientMethod,
+				userId: bob.id,
+				systemWebhookId: null,
+			};
+
+			const recipient2 = await service.updateRecipient(params, root);
+			expect(recipient2).toMatchObject(params);
+		});
+
+		test('更新成功2', async () => {
+			const recipient1 = await createRecipient({
+				method: 'webhook',
+				systemWebhookId: systemWebhook1.id,
+			});
+
+			const params = {
+				id: recipient1.id,
+				isActive: false,
+				name: randomString(),
+				method: 'webhook' as RecipientMethod,
+				userId: null,
+				systemWebhookId: systemWebhook2.id,
+			};
+
+			const recipient2 = await service.updateRecipient(params, root);
+			expect(recipient2).toMatchObject(params);
+		});
+	});
+
+	describe('deleteRecipient', () => {
+		test('削除成功1', async () => {
+			const recipient1 = await createRecipient({
+				method: 'email',
+				userId: alice.id,
+			});
+
+			await service.deleteRecipient(recipient1.id, root);
+
+			await expect(abuseReportNotificationRecipientRepository.findOneBy({ id: recipient1.id })).resolves.toBeNull();
+		});
+	});
+
+	describe('fetchRecipients', () => {
+		async function create() {
+			const recipient1 = await createRecipient({
+				method: 'email',
+				userId: alice.id,
+			});
+			const recipient2 = await createRecipient({
+				method: 'email',
+				userId: bob.id,
+			});
+
+			const recipient3 = await createRecipient({
+				method: 'webhook',
+				systemWebhookId: systemWebhook1.id,
+			});
+			const recipient4 = await createRecipient({
+				method: 'webhook',
+				systemWebhookId: systemWebhook2.id,
+			});
+
+			return [recipient1, recipient2, recipient3, recipient4];
+		}
+
+		test('フィルタなし', async () => {
+			const [recipient1, recipient2, recipient3, recipient4] = await create();
+
+			const recipients = await service.fetchRecipients({});
+			expect(recipients).toEqual([recipient1, recipient2, recipient3, recipient4]);
+		});
+
+		test('フィルタなし(非モデレータは除外される)', async () => {
+			roleService.getModeratorIds.mockClear();
+			roleService.getModeratorIds.mockResolvedValue([root.id, bob.id]);
+
+			const [recipient1, recipient2, recipient3, recipient4] = await create();
+
+			const recipients = await service.fetchRecipients({});
+			// aliceはモデレータではないので除外される
+			expect(recipients).toEqual([recipient2, recipient3, recipient4]);
+		});
+
+		test('フィルタなし(非モデレータでも除外されないオプション設定)', async () => {
+			roleService.getModeratorIds.mockClear();
+			roleService.getModeratorIds.mockResolvedValue([root.id, bob.id]);
+
+			const [recipient1, recipient2, recipient3, recipient4] = await create();
+
+			const recipients = await service.fetchRecipients({}, { removeUnauthorized: false });
+			expect(recipients).toEqual([recipient1, recipient2, recipient3, recipient4]);
+		});
+
+		test('emailのみ', async () => {
+			const [recipient1, recipient2, recipient3, recipient4] = await create();
+
+			const recipients = await service.fetchRecipients({ method: ['email'] });
+			expect(recipients).toEqual([recipient1, recipient2]);
+		});
+
+		test('webhookのみ', async () => {
+			const [recipient1, recipient2, recipient3, recipient4] = await create();
+
+			const recipients = await service.fetchRecipients({ method: ['webhook'] });
+			expect(recipients).toEqual([recipient3, recipient4]);
+		});
+
+		test('すべて', async () => {
+			const [recipient1, recipient2, recipient3, recipient4] = await create();
+
+			const recipients = await service.fetchRecipients({ method: ['email', 'webhook'] });
+			expect(recipients).toEqual([recipient1, recipient2, recipient3, recipient4]);
+		});
+
+		test('ID指定', async () => {
+			const [recipient1, recipient2, recipient3, recipient4] = await create();
+
+			const recipients = await service.fetchRecipients({ ids: [recipient1.id, recipient3.id] });
+			expect(recipients).toEqual([recipient1, recipient3]);
+		});
+
+		test('ID指定(method=emailではないIDが混ざりこまない)', async () => {
+			const [recipient1, recipient2, recipient3, recipient4] = await create();
+
+			const recipients = await service.fetchRecipients({ ids: [recipient1.id, recipient3.id], method: ['email'] });
+			expect(recipients).toEqual([recipient1]);
+		});
+
+		test('ID指定(method=webhookではないIDが混ざりこまない)', async () => {
+			const [recipient1, recipient2, recipient3, recipient4] = await create();
+
+			const recipients = await service.fetchRecipients({ ids: [recipient1.id, recipient3.id], method: ['webhook'] });
+			expect(recipients).toEqual([recipient3]);
+		});
+	});
+});
diff --git a/packages/backend/test/unit/ApMfmService.ts b/packages/backend/test/unit/ApMfmService.ts
index 79cb81f5c9..e81a321c9b 100644
--- a/packages/backend/test/unit/ApMfmService.ts
+++ b/packages/backend/test/unit/ApMfmService.ts
@@ -23,10 +23,10 @@ describe('ApMfmService', () => {
 
 	describe('getNoteHtml', () => {
 		test('Do not provide _misskey_content for simple text', () => {
-			const note: MiNote = {
+			const note = {
 				text: 'テキスト #タグ @mention 🍊 :emoji: https://example.com',
 				mentionedRemoteUsers: '[]',
-			} as any;
+			};
 
 			const { content, noMisskeyContent } = apMfmService.getNoteHtml(note);
 
@@ -35,10 +35,10 @@ describe('ApMfmService', () => {
 		});
 
 		test('Provide _misskey_content for MFM', () => {
-			const note: MiNote = {
+			const note = {
 				text: '$[tada foo]',
 				mentionedRemoteUsers: '[]',
-			} as any;
+			};
 
 			const { content, noMisskeyContent } = apMfmService.getNoteHtml(note);
 
diff --git a/packages/backend/test/unit/FetchInstanceMetadataService.ts b/packages/backend/test/unit/FetchInstanceMetadataService.ts
index bf8f3ab0e3..1e3605aafc 100644
--- a/packages/backend/test/unit/FetchInstanceMetadataService.ts
+++ b/packages/backend/test/unit/FetchInstanceMetadataService.ts
@@ -8,6 +8,7 @@ process.env.NODE_ENV = 'test';
 import { jest } from '@jest/globals';
 import { Test } from '@nestjs/testing';
 import { Redis } from 'ioredis';
+import type { TestingModule } from '@nestjs/testing';
 import { GlobalModule } from '@/GlobalModule.js';
 import { FetchInstanceMetadataService } from '@/core/FetchInstanceMetadataService.js';
 import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
@@ -16,7 +17,6 @@ import { LoggerService } from '@/core/LoggerService.js';
 import { UtilityService } from '@/core/UtilityService.js';
 import { IdService } from '@/core/IdService.js';
 import { DI } from '@/di-symbols.js';
-import type { TestingModule } from '@nestjs/testing';
 
 function mockRedis() {
 	const hash = {} as any;
@@ -52,7 +52,7 @@ describe('FetchInstanceMetadataService', () => {
 				if (token === HttpRequestService) {
 					return { getJson: jest.fn(), getHtml: jest.fn(), send: jest.fn() };
 				} else if (token === FederatedInstanceService) {
-					return { fetch: jest.fn() };
+					return { fetchOrRegister: jest.fn() };
 				} else if (token === DI.redis) {
 					return mockRedis;
 				}
@@ -75,7 +75,7 @@ describe('FetchInstanceMetadataService', () => {
 	test('Lock and update', async () => {
 		redisClient.set = mockRedis();
 		const now = Date.now();
-		federatedInstanceService.fetch.mockResolvedValue({ infoUpdatedAt: { getTime: () => { return now - 10 * 1000 * 60 * 60 * 24; } } } as any);
+		federatedInstanceService.fetchOrRegister.mockResolvedValue({ infoUpdatedAt: { getTime: () => { return now - 10 * 1000 * 60 * 60 * 24; } } } as any);
 		httpRequestService.getJson.mockImplementation(() => { throw Error(); });
 		const tryLockSpy = jest.spyOn(fetchInstanceMetadataService, 'tryLock');
 		const unlockSpy = jest.spyOn(fetchInstanceMetadataService, 'unlock');
@@ -83,14 +83,14 @@ describe('FetchInstanceMetadataService', () => {
 		await fetchInstanceMetadataService.fetchInstanceMetadata({ host: 'example.com' } as any);
 		expect(tryLockSpy).toHaveBeenCalledTimes(1);
 		expect(unlockSpy).toHaveBeenCalledTimes(1);
-		expect(federatedInstanceService.fetch).toHaveBeenCalledTimes(1);
+		expect(federatedInstanceService.fetchOrRegister).toHaveBeenCalledTimes(1);
 		expect(httpRequestService.getJson).toHaveBeenCalled();
 	});
 
 	test('Lock and don\'t update', async () => {
 		redisClient.set = mockRedis();
 		const now = Date.now();
-		federatedInstanceService.fetch.mockResolvedValue({ infoUpdatedAt: { getTime: () => now } } as any);
+		federatedInstanceService.fetchOrRegister.mockResolvedValue({ infoUpdatedAt: { getTime: () => now } } as any);
 		httpRequestService.getJson.mockImplementation(() => { throw Error(); });
 		const tryLockSpy = jest.spyOn(fetchInstanceMetadataService, 'tryLock');
 		const unlockSpy = jest.spyOn(fetchInstanceMetadataService, 'unlock');
@@ -98,14 +98,14 @@ describe('FetchInstanceMetadataService', () => {
 		await fetchInstanceMetadataService.fetchInstanceMetadata({ host: 'example.com' } as any);
 		expect(tryLockSpy).toHaveBeenCalledTimes(1);
 		expect(unlockSpy).toHaveBeenCalledTimes(1);
-		expect(federatedInstanceService.fetch).toHaveBeenCalledTimes(1);
+		expect(federatedInstanceService.fetchOrRegister).toHaveBeenCalledTimes(1);
 		expect(httpRequestService.getJson).toHaveBeenCalledTimes(0);
 	});
 
 	test('Do nothing when lock not acquired', async () => {
 		redisClient.set = mockRedis();
 		const now = Date.now();
-		federatedInstanceService.fetch.mockResolvedValue({ infoUpdatedAt: { getTime: () => now - 10 * 1000 * 60 * 60 * 24 } } as any);
+		federatedInstanceService.fetchOrRegister.mockResolvedValue({ infoUpdatedAt: { getTime: () => now - 10 * 1000 * 60 * 60 * 24 } } as any);
 		httpRequestService.getJson.mockImplementation(() => { throw Error(); });
 		await fetchInstanceMetadataService.tryLock('example.com');
 		const tryLockSpy = jest.spyOn(fetchInstanceMetadataService, 'tryLock');
@@ -114,14 +114,14 @@ describe('FetchInstanceMetadataService', () => {
 		await fetchInstanceMetadataService.fetchInstanceMetadata({ host: 'example.com' } as any);
 		expect(tryLockSpy).toHaveBeenCalledTimes(1);
 		expect(unlockSpy).toHaveBeenCalledTimes(0);
-		expect(federatedInstanceService.fetch).toHaveBeenCalledTimes(0);
+		expect(federatedInstanceService.fetchOrRegister).toHaveBeenCalledTimes(0);
 		expect(httpRequestService.getJson).toHaveBeenCalledTimes(0);
 	});
 
 	test('Do when lock not acquired but forced', async () => {
 		redisClient.set = mockRedis();
 		const now = Date.now();
-		federatedInstanceService.fetch.mockResolvedValue({ infoUpdatedAt: { getTime: () => now - 10 * 1000 * 60 * 60 * 24 } } as any);
+		federatedInstanceService.fetchOrRegister.mockResolvedValue({ infoUpdatedAt: { getTime: () => now - 10 * 1000 * 60 * 60 * 24 } } as any);
 		httpRequestService.getJson.mockImplementation(() => { throw Error(); });
 		await fetchInstanceMetadataService.tryLock('example.com');
 		const tryLockSpy = jest.spyOn(fetchInstanceMetadataService, 'tryLock');
@@ -130,7 +130,7 @@ describe('FetchInstanceMetadataService', () => {
 		await fetchInstanceMetadataService.fetchInstanceMetadata({ host: 'example.com' } as any, true);
 		expect(tryLockSpy).toHaveBeenCalledTimes(0);
 		expect(unlockSpy).toHaveBeenCalledTimes(1);
-		expect(federatedInstanceService.fetch).toHaveBeenCalledTimes(0);
+		expect(federatedInstanceService.fetchOrRegister).toHaveBeenCalledTimes(0);
 		expect(httpRequestService.getJson).toHaveBeenCalled();
 	});
 });
diff --git a/packages/backend/test/unit/FileInfoService.ts b/packages/backend/test/unit/FileInfoService.ts
index 40d187f5a8..29bd03a201 100644
--- a/packages/backend/test/unit/FileInfoService.ts
+++ b/packages/backend/test/unit/FileInfoService.ts
@@ -12,7 +12,7 @@ import { ModuleMocker } from 'jest-mock';
 import { Test } from '@nestjs/testing';
 import { afterAll, beforeAll, describe, test } from '@jest/globals';
 import { GlobalModule } from '@/GlobalModule.js';
-import { FileInfoService } from '@/core/FileInfoService.js';
+import { FileInfo, FileInfoService } from '@/core/FileInfoService.js';
 //import { DI } from '@/di-symbols.js';
 import { AiService } from '@/core/AiService.js';
 import { LoggerService } from '@/core/LoggerService.js';
@@ -28,6 +28,15 @@ const moduleMocker = new ModuleMocker(global);
 describe('FileInfoService', () => {
 	let app: TestingModule;
 	let fileInfoService: FileInfoService;
+	const strip = (fileInfo: FileInfo): Omit<Partial<FileInfo>, 'warnings' | 'blurhash' | 'sensitive' | 'porn'> => {
+		const fi: Partial<FileInfo> = fileInfo;
+		delete fi.warnings;
+		delete fi.sensitive;
+		delete fi.blurhash;
+		delete fi.porn;
+		
+		return fi;
+	}
 
 	beforeAll(async () => {
 		app = await Test.createTestingModule({
@@ -63,11 +72,7 @@ describe('FileInfoService', () => {
 
 	test('Empty file', async () => {
 		const path = `${resources}/emptyfile`;
-		const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any;
-		delete info.warnings;
-		delete info.blurhash;
-		delete info.sensitive;
-		delete info.porn;
+		const info = strip(await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }));
 		assert.deepStrictEqual(info, {
 			size: 0,
 			md5: 'd41d8cd98f00b204e9800998ecf8427e',
@@ -83,32 +88,24 @@ describe('FileInfoService', () => {
 
 	describe('IMAGE', () => {
 		test('Generic JPEG', async () => {
-			const path = `${resources}/Lenna.jpg`;
-			const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any;
-			delete info.warnings;
-			delete info.blurhash;
-			delete info.sensitive;
-			delete info.porn;
+			const path = `${resources}/192.jpg`;
+			const info = strip(await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }));
 			assert.deepStrictEqual(info, {
-				size: 25360,
-				md5: '091b3f259662aa31e2ffef4519951168',
+				size: 5131,
+				md5: '8c9ed0677dd2b8f9f7472c3af247e5e3',
 				type: {
 					mime: 'image/jpeg',
 					ext: 'jpg',
 				},
-				width: 512,
-				height: 512,
+				width: 192,
+				height: 192,
 				orientation: undefined,
 			});
 		});
 
 		test('Generic APNG', async () => {
 			const path = `${resources}/anime.png`;
-			const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any;
-			delete info.warnings;
-			delete info.blurhash;
-			delete info.sensitive;
-			delete info.porn;
+			const info = strip(await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }));
 			assert.deepStrictEqual(info, {
 				size: 1868,
 				md5: '08189c607bea3b952704676bb3c979e0',
@@ -124,11 +121,7 @@ describe('FileInfoService', () => {
 
 		test('Generic AGIF', async () => {
 			const path = `${resources}/anime.gif`;
-			const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any;
-			delete info.warnings;
-			delete info.blurhash;
-			delete info.sensitive;
-			delete info.porn;
+			const info = strip(await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }));
 			assert.deepStrictEqual(info, {
 				size: 2248,
 				md5: '32c47a11555675d9267aee1a86571e7e',
@@ -144,11 +137,7 @@ describe('FileInfoService', () => {
 
 		test('PNG with alpha', async () => {
 			const path = `${resources}/with-alpha.png`;
-			const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any;
-			delete info.warnings;
-			delete info.blurhash;
-			delete info.sensitive;
-			delete info.porn;
+			const info = strip(await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }));
 			assert.deepStrictEqual(info, {
 				size: 3772,
 				md5: 'f73535c3e1e27508885b69b10cf6e991',
@@ -164,11 +153,7 @@ describe('FileInfoService', () => {
 
 		test('Generic SVG', async () => {
 			const path = `${resources}/image.svg`;
-			const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any;
-			delete info.warnings;
-			delete info.blurhash;
-			delete info.sensitive;
-			delete info.porn;
+			const info = strip(await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }));
 			assert.deepStrictEqual(info, {
 				size: 505,
 				md5: 'b6f52b4b021e7b92cdd04509c7267965',
@@ -185,11 +170,7 @@ describe('FileInfoService', () => {
 		test('SVG with XML definition', async () => {
 			// https://github.com/misskey-dev/misskey/issues/4413
 			const path = `${resources}/with-xml-def.svg`;
-			const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any;
-			delete info.warnings;
-			delete info.blurhash;
-			delete info.sensitive;
-			delete info.porn;
+			const info = strip(await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }));
 			assert.deepStrictEqual(info, {
 				size: 544,
 				md5: '4b7a346cde9ccbeb267e812567e33397',
@@ -205,11 +186,7 @@ describe('FileInfoService', () => {
 
 		test('Dimension limit', async () => {
 			const path = `${resources}/25000x25000.png`;
-			const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any;
-			delete info.warnings;
-			delete info.blurhash;
-			delete info.sensitive;
-			delete info.porn;
+			const info = strip(await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }));
 			assert.deepStrictEqual(info, {
 				size: 75933,
 				md5: '268c5dde99e17cf8fe09f1ab3f97df56',
@@ -225,11 +202,7 @@ describe('FileInfoService', () => {
 
 		test('Rotate JPEG', async () => {
 			const path = `${resources}/rotate.jpg`;
-			const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any;
-			delete info.warnings;
-			delete info.blurhash;
-			delete info.sensitive;
-			delete info.porn;
+			const info = strip(await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }));
 			assert.deepStrictEqual(info, {
 				size: 12624,
 				md5: '68d5b2d8d1d1acbbce99203e3ec3857e',
@@ -247,11 +220,7 @@ describe('FileInfoService', () => {
 	describe('AUDIO', () => {
 		test('MP3', async () => {
 			const path = `${resources}/kick_gaba7.mp3`;
-			const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any;
-			delete info.warnings;
-			delete info.blurhash;
-			delete info.sensitive;
-			delete info.porn;
+			const info = strip(await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }));
 			delete info.width;
 			delete info.height;
 			delete info.orientation;
@@ -267,11 +236,7 @@ describe('FileInfoService', () => {
 
 		test('WAV', async () => {
 			const path = `${resources}/kick_gaba7.wav`;
-			const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any;
-			delete info.warnings;
-			delete info.blurhash;
-			delete info.sensitive;
-			delete info.porn;
+			const info = strip(await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }));
 			delete info.width;
 			delete info.height;
 			delete info.orientation;
@@ -287,11 +252,7 @@ describe('FileInfoService', () => {
 
 		test('AAC', async () => {
 			const path = `${resources}/kick_gaba7.aac`;
-			const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any;
-			delete info.warnings;
-			delete info.blurhash;
-			delete info.sensitive;
-			delete info.porn;
+			const info = strip(await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }));
 			delete info.width;
 			delete info.height;
 			delete info.orientation;
@@ -307,11 +268,7 @@ describe('FileInfoService', () => {
 
 		test('FLAC', async () => {
 			const path = `${resources}/kick_gaba7.flac`;
-			const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any;
-			delete info.warnings;
-			delete info.blurhash;
-			delete info.sensitive;
-			delete info.porn;
+			const info = strip(await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }));
 			delete info.width;
 			delete info.height;
 			delete info.orientation;
@@ -327,11 +284,7 @@ describe('FileInfoService', () => {
 
 		test('MPEG-4 AUDIO (M4A)', async () => {
 			const path = `${resources}/kick_gaba7.m4a`;
-			const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any;
-			delete info.warnings;
-			delete info.blurhash;
-			delete info.sensitive;
-			delete info.porn;
+			const info = strip(await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }));
 			delete info.width;
 			delete info.height;
 			delete info.orientation;
@@ -347,11 +300,7 @@ describe('FileInfoService', () => {
 
 		test('WEBM AUDIO', async () => {
 			const path = `${resources}/kick_gaba7.webm`;
-			const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any;
-			delete info.warnings;
-			delete info.blurhash;
-			delete info.sensitive;
-			delete info.porn;
+			const info = strip(await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }));
 			delete info.width;
 			delete info.height;
 			delete info.orientation;
diff --git a/packages/backend/test/unit/FlashService.ts b/packages/backend/test/unit/FlashService.ts
new file mode 100644
index 0000000000..12ffaf3421
--- /dev/null
+++ b/packages/backend/test/unit/FlashService.ts
@@ -0,0 +1,152 @@
+/* eslint-disable @typescript-eslint/no-unused-vars */
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Test, TestingModule } from '@nestjs/testing';
+import { FlashService } from '@/core/FlashService.js';
+import { IdService } from '@/core/IdService.js';
+import { FlashsRepository, MiFlash, MiUser, UserProfilesRepository, UsersRepository } from '@/models/_.js';
+import { DI } from '@/di-symbols.js';
+import { GlobalModule } from '@/GlobalModule.js';
+
+describe('FlashService', () => {
+	let app: TestingModule;
+	let service: FlashService;
+
+	// --------------------------------------------------------------------------------------
+
+	let flashsRepository: FlashsRepository;
+	let usersRepository: UsersRepository;
+	let userProfilesRepository: UserProfilesRepository;
+	let idService: IdService;
+
+	// --------------------------------------------------------------------------------------
+
+	let root: MiUser;
+	let alice: MiUser;
+	let bob: MiUser;
+
+	// --------------------------------------------------------------------------------------
+
+	async function createFlash(data: Partial<MiFlash>) {
+		return flashsRepository.insert({
+			id: idService.gen(),
+			updatedAt: new Date(),
+			userId: root.id,
+			title: 'title',
+			summary: 'summary',
+			script: 'script',
+			permissions: [],
+			likedCount: 0,
+			...data,
+		}).then(x => flashsRepository.findOneByOrFail(x.identifiers[0]));
+	}
+
+	async function createUser(data: Partial<MiUser> = {}) {
+		const user = await usersRepository
+			.insert({
+				id: idService.gen(),
+				...data,
+			})
+			.then(x => usersRepository.findOneByOrFail(x.identifiers[0]));
+
+		await userProfilesRepository.insert({
+			userId: user.id,
+		});
+
+		return user;
+	}
+
+	// --------------------------------------------------------------------------------------
+
+	beforeEach(async () => {
+		app = await Test.createTestingModule({
+			imports: [
+				GlobalModule,
+			],
+			providers: [
+				FlashService,
+				IdService,
+			],
+		}).compile();
+
+		service = app.get(FlashService);
+
+		flashsRepository = app.get(DI.flashsRepository);
+		usersRepository = app.get(DI.usersRepository);
+		userProfilesRepository = app.get(DI.userProfilesRepository);
+		idService = app.get(IdService);
+
+		root = await createUser({ username: 'root', usernameLower: 'root', isRoot: true });
+		alice = await createUser({ username: 'alice', usernameLower: 'alice', isRoot: false });
+		bob = await createUser({ username: 'bob', usernameLower: 'bob', isRoot: false });
+	});
+
+	afterEach(async () => {
+		await usersRepository.delete({});
+		await userProfilesRepository.delete({});
+		await flashsRepository.delete({});
+	});
+
+	afterAll(async () => {
+		await app.close();
+	});
+
+	// --------------------------------------------------------------------------------------
+
+	describe('featured', () => {
+		test('should return featured flashes', async () => {
+			const flash1 = await createFlash({ likedCount: 1 });
+			const flash2 = await createFlash({ likedCount: 2 });
+			const flash3 = await createFlash({ likedCount: 3 });
+
+			const result = await service.featured({
+				offset: 0,
+				limit: 10,
+			});
+
+			expect(result).toEqual([flash3, flash2, flash1]);
+		});
+
+		test('should return featured flashes public visibility only', async () => {
+			const flash1 = await createFlash({ likedCount: 1, visibility: 'public' });
+			const flash2 = await createFlash({ likedCount: 2, visibility: 'public' });
+			const flash3 = await createFlash({ likedCount: 3, visibility: 'private' });
+
+			const result = await service.featured({
+				offset: 0,
+				limit: 10,
+			});
+
+			expect(result).toEqual([flash2, flash1]);
+		});
+
+		test('should return featured flashes with offset', async () => {
+			const flash1 = await createFlash({ likedCount: 1 });
+			const flash2 = await createFlash({ likedCount: 2 });
+			const flash3 = await createFlash({ likedCount: 3 });
+
+			const result = await service.featured({
+				offset: 1,
+				limit: 10,
+			});
+
+			expect(result).toEqual([flash2, flash1]);
+		});
+
+		test('should return featured flashes with limit', async () => {
+			const flash1 = await createFlash({ likedCount: 1 });
+			const flash2 = await createFlash({ likedCount: 2 });
+			const flash3 = await createFlash({ likedCount: 3 });
+
+			const result = await service.featured({
+				offset: 0,
+				limit: 2,
+			});
+
+			expect(result).toEqual([flash3, flash2]);
+		});
+	});
+});
diff --git a/packages/backend/test/unit/RoleService.ts b/packages/backend/test/unit/RoleService.ts
index ec441735d7..9c1b1008d6 100644
--- a/packages/backend/test/unit/RoleService.ts
+++ b/packages/backend/test/unit/RoleService.ts
@@ -3,17 +3,26 @@
  * SPDX-License-Identifier: AGPL-3.0-only
  */
 
-import { UserEntityService } from '@/core/entities/UserEntityService.js';
-
 process.env.NODE_ENV = 'test';
 
+import { setTimeout } from 'node:timers/promises';
 import { jest } from '@jest/globals';
 import { ModuleMocker } from 'jest-mock';
 import { Test } from '@nestjs/testing';
 import * as lolex from '@sinonjs/fake-timers';
+import type { TestingModule } from '@nestjs/testing';
+import type { MockFunctionMetadata } from 'jest-mock';
 import { GlobalModule } from '@/GlobalModule.js';
 import { RoleService } from '@/core/RoleService.js';
-import type { MiRole, MiUser, RoleAssignmentsRepository, RolesRepository, UsersRepository } from '@/models/_.js';
+import {
+	MiMeta,
+	MiRole,
+	MiRoleAssignment,
+	MiUser,
+	RoleAssignmentsRepository,
+	RolesRepository,
+	UsersRepository,
+} from '@/models/_.js';
 import { DI } from '@/di-symbols.js';
 import { MetaService } from '@/core/MetaService.js';
 import { genAidx } from '@/misc/id/aidx.js';
@@ -23,9 +32,7 @@ import { GlobalEventService } from '@/core/GlobalEventService.js';
 import { secureRndstr } from '@/misc/secure-rndstr.js';
 import { NotificationService } from '@/core/NotificationService.js';
 import { RoleCondFormulaValue } from '@/models/Role.js';
-import { sleep } from '../utils.js';
-import type { TestingModule } from '@nestjs/testing';
-import type { MockFunctionMetadata } from 'jest-mock';
+import { UserEntityService } from '@/core/entities/UserEntityService.js';
 
 const moduleMocker = new ModuleMocker(global);
 
@@ -35,31 +42,31 @@ describe('RoleService', () => {
 	let usersRepository: UsersRepository;
 	let rolesRepository: RolesRepository;
 	let roleAssignmentsRepository: RoleAssignmentsRepository;
-	let metaService: jest.Mocked<MetaService>;
+	let meta: jest.Mocked<MiMeta>;
 	let notificationService: jest.Mocked<NotificationService>;
 	let clock: lolex.InstalledClock;
 
-	function createUser(data: Partial<MiUser> = {}) {
+	async function createUser(data: Partial<MiUser> = {}) {
 		const un = secureRndstr(16);
-		return usersRepository.insert({
+		const x = await usersRepository.insert({
 			id: genAidx(Date.now()),
 			username: un,
 			usernameLower: un,
 			...data,
-		})
-			.then(x => usersRepository.findOneByOrFail(x.identifiers[0]));
+		});
+		return await usersRepository.findOneByOrFail(x.identifiers[0]);
 	}
 
-	function createRole(data: Partial<MiRole> = {}) {
-		return rolesRepository.insert({
+	async function createRole(data: Partial<MiRole> = {}) {
+		const x = await rolesRepository.insert({
 			id: genAidx(Date.now()),
 			updatedAt: new Date(),
 			lastUsedAt: new Date(),
 			name: '',
 			description: '',
 			...data,
-		})
-			.then(x => rolesRepository.findOneByOrFail(x.identifiers[0]));
+		});
+		return await rolesRepository.findOneByOrFail(x.identifiers[0]);
 	}
 
 	function createConditionalRole(condFormula: RoleCondFormulaValue, data: Partial<MiRole> = {}) {
@@ -71,6 +78,20 @@ describe('RoleService', () => {
 		});
 	}
 
+	async function assignRole(args: Partial<MiRoleAssignment>) {
+		const id = genAidx(Date.now());
+		const expiresAt = new Date();
+		expiresAt.setDate(expiresAt.getDate() + 1);
+
+		await roleAssignmentsRepository.insert({
+			id,
+			expiresAt,
+			...args,
+		});
+
+		return await roleAssignmentsRepository.findOneByOrFail({ id });
+	}
+
 	function aidx() {
 		return genAidx(Date.now());
 	}
@@ -122,7 +143,7 @@ describe('RoleService', () => {
 		rolesRepository = app.get<RolesRepository>(DI.rolesRepository);
 		roleAssignmentsRepository = app.get<RoleAssignmentsRepository>(DI.roleAssignmentsRepository);
 
-		metaService = app.get<MetaService>(MetaService) as jest.Mocked<MetaService>;
+		meta = app.get<MiMeta>(DI.meta) as jest.Mocked<MiMeta>;
 		notificationService = app.get<NotificationService>(NotificationService) as jest.Mocked<NotificationService>;
 
 		await roleService.onModuleInit();
@@ -144,11 +165,9 @@ describe('RoleService', () => {
 	describe('getUserPolicies', () => {
 		test('instance default policies', async () => {
 			const user = await createUser();
-			metaService.fetch.mockResolvedValue({
-				policies: {
-					canManageCustomEmojis: false,
-				},
-			} as any);
+			meta.policies = {
+				canManageCustomEmojis: false,
+			};
 
 			const result = await roleService.getUserPolicies(user.id);
 
@@ -157,11 +176,9 @@ describe('RoleService', () => {
 
 		test('instance default policies 2', async () => {
 			const user = await createUser();
-			metaService.fetch.mockResolvedValue({
-				policies: {
-					canManageCustomEmojis: true,
-				},
-			} as any);
+			meta.policies = {
+				canManageCustomEmojis: true,
+			};
 
 			const result = await roleService.getUserPolicies(user.id);
 
@@ -181,11 +198,9 @@ describe('RoleService', () => {
 				},
 			});
 			await roleService.assign(user.id, role.id);
-			metaService.fetch.mockResolvedValue({
-				policies: {
-					canManageCustomEmojis: false,
-				},
-			} as any);
+			meta.policies = {
+				canManageCustomEmojis: false,
+			};
 
 			const result = await roleService.getUserPolicies(user.id);
 
@@ -216,11 +231,9 @@ describe('RoleService', () => {
 			});
 			await roleService.assign(user.id, role1.id);
 			await roleService.assign(user.id, role2.id);
-			metaService.fetch.mockResolvedValue({
-				policies: {
-					driveCapacityMb: 50,
-				},
-			} as any);
+			meta.policies = {
+				driveCapacityMb: 50,
+			};
 
 			const result = await roleService.getUserPolicies(user.id);
 
@@ -240,11 +253,9 @@ describe('RoleService', () => {
 				},
 			});
 			await roleService.assign(user.id, role.id, new Date(Date.now() + (1000 * 60 * 60 * 24)));
-			metaService.fetch.mockResolvedValue({
-				policies: {
-					canManageCustomEmojis: false,
-				},
-			} as any);
+			meta.policies = {
+				canManageCustomEmojis: false,
+			};
 
 			const result = await roleService.getUserPolicies(user.id);
 			expect(result.canManageCustomEmojis).toBe(true);
@@ -258,13 +269,217 @@ describe('RoleService', () => {
 
 			// ストリーミング経由で反映されるまでちょっと待つ
 			clock.uninstall();
-			await sleep(100);
+			await setTimeout(100);
 
 			const resultAfter25hAgain = await roleService.getUserPolicies(user.id);
 			expect(resultAfter25hAgain.canManageCustomEmojis).toBe(true);
 		});
 	});
 
+	describe('getModeratorIds', () => {
+		test('includeAdmins = false, includeRoot = false, excludeExpire = false', async () => {
+			const [adminUser1, adminUser2, modeUser1, modeUser2, normalUser1, normalUser2, rootUser] = await Promise.all([
+				createUser(), createUser(), createUser(), createUser(), createUser(), createUser(), createUser({ isRoot: true }),
+			]);
+
+			const role1 = await createRole({ name: 'admin', isAdministrator: true });
+			const role2 = await createRole({ name: 'moderator', isModerator: true });
+			const role3 = await createRole({ name: 'normal' });
+
+			await Promise.all([
+				assignRole({ userId: adminUser1.id, roleId: role1.id }),
+				assignRole({ userId: adminUser2.id, roleId: role1.id, expiresAt: new Date(Date.now() - 1000) }),
+				assignRole({ userId: modeUser1.id, roleId: role2.id }),
+				assignRole({ userId: modeUser2.id, roleId: role2.id, expiresAt: new Date(Date.now() - 1000) }),
+				assignRole({ userId: normalUser1.id, roleId: role3.id }),
+				assignRole({ userId: normalUser2.id, roleId: role3.id, expiresAt: new Date(Date.now() - 1000) }),
+			]);
+
+			const result = await roleService.getModeratorIds({
+				includeAdmins: false,
+				includeRoot: false,
+				excludeExpire: false,
+			});
+			expect(result).toEqual([modeUser1.id, modeUser2.id]);
+		});
+
+		test('includeAdmins = false, includeRoot = false, excludeExpire = true', async () => {
+			const [adminUser1, adminUser2, modeUser1, modeUser2, normalUser1, normalUser2, rootUser] = await Promise.all([
+				createUser(), createUser(), createUser(), createUser(), createUser(), createUser(), createUser({ isRoot: true }),
+			]);
+
+			const role1 = await createRole({ name: 'admin', isAdministrator: true });
+			const role2 = await createRole({ name: 'moderator', isModerator: true });
+			const role3 = await createRole({ name: 'normal' });
+
+			await Promise.all([
+				assignRole({ userId: adminUser1.id, roleId: role1.id }),
+				assignRole({ userId: adminUser2.id, roleId: role1.id, expiresAt: new Date(Date.now() - 1000) }),
+				assignRole({ userId: modeUser1.id, roleId: role2.id }),
+				assignRole({ userId: modeUser2.id, roleId: role2.id, expiresAt: new Date(Date.now() - 1000) }),
+				assignRole({ userId: normalUser1.id, roleId: role3.id }),
+				assignRole({ userId: normalUser2.id, roleId: role3.id, expiresAt: new Date(Date.now() - 1000) }),
+			]);
+
+			const result = await roleService.getModeratorIds({
+				includeAdmins: false,
+				includeRoot: false,
+				excludeExpire: true,
+			});
+			expect(result).toEqual([modeUser1.id]);
+		});
+
+		test('includeAdmins = true, includeRoot = false, excludeExpire = false', async () => {
+			const [adminUser1, adminUser2, modeUser1, modeUser2, normalUser1, normalUser2, rootUser] = await Promise.all([
+				createUser(), createUser(), createUser(), createUser(), createUser(), createUser(), createUser({ isRoot: true }),
+			]);
+
+			const role1 = await createRole({ name: 'admin', isAdministrator: true });
+			const role2 = await createRole({ name: 'moderator', isModerator: true });
+			const role3 = await createRole({ name: 'normal' });
+
+			await Promise.all([
+				assignRole({ userId: adminUser1.id, roleId: role1.id }),
+				assignRole({ userId: adminUser2.id, roleId: role1.id, expiresAt: new Date(Date.now() - 1000) }),
+				assignRole({ userId: modeUser1.id, roleId: role2.id }),
+				assignRole({ userId: modeUser2.id, roleId: role2.id, expiresAt: new Date(Date.now() - 1000) }),
+				assignRole({ userId: normalUser1.id, roleId: role3.id }),
+				assignRole({ userId: normalUser2.id, roleId: role3.id, expiresAt: new Date(Date.now() - 1000) }),
+			]);
+
+			const result = await roleService.getModeratorIds({
+				includeAdmins: true,
+				includeRoot: false,
+				excludeExpire: false,
+			});
+			expect(result).toEqual([adminUser1.id, adminUser2.id, modeUser1.id, modeUser2.id]);
+		});
+
+		test('includeAdmins = true, includeRoot = false, excludeExpire = true', async () => {
+			const [adminUser1, adminUser2, modeUser1, modeUser2, normalUser1, normalUser2, rootUser] = await Promise.all([
+				createUser(), createUser(), createUser(), createUser(), createUser(), createUser(), createUser({ isRoot: true }),
+			]);
+
+			const role1 = await createRole({ name: 'admin', isAdministrator: true });
+			const role2 = await createRole({ name: 'moderator', isModerator: true });
+			const role3 = await createRole({ name: 'normal' });
+
+			await Promise.all([
+				assignRole({ userId: adminUser1.id, roleId: role1.id }),
+				assignRole({ userId: adminUser2.id, roleId: role1.id, expiresAt: new Date(Date.now() - 1000) }),
+				assignRole({ userId: modeUser1.id, roleId: role2.id }),
+				assignRole({ userId: modeUser2.id, roleId: role2.id, expiresAt: new Date(Date.now() - 1000) }),
+				assignRole({ userId: normalUser1.id, roleId: role3.id }),
+				assignRole({ userId: normalUser2.id, roleId: role3.id, expiresAt: new Date(Date.now() - 1000) }),
+			]);
+
+			const result = await roleService.getModeratorIds({
+				includeAdmins: true,
+				includeRoot: false,
+				excludeExpire: true,
+			});
+			expect(result).toEqual([adminUser1.id, modeUser1.id]);
+		});
+
+		test('includeAdmins = false, includeRoot = true, excludeExpire = false', async () => {
+			const [adminUser1, adminUser2, modeUser1, modeUser2, normalUser1, normalUser2, rootUser] = await Promise.all([
+				createUser(), createUser(), createUser(), createUser(), createUser(), createUser(), createUser({ isRoot: true }),
+			]);
+
+			const role1 = await createRole({ name: 'admin', isAdministrator: true });
+			const role2 = await createRole({ name: 'moderator', isModerator: true });
+			const role3 = await createRole({ name: 'normal' });
+
+			await Promise.all([
+				assignRole({ userId: adminUser1.id, roleId: role1.id }),
+				assignRole({ userId: adminUser2.id, roleId: role1.id, expiresAt: new Date(Date.now() - 1000) }),
+				assignRole({ userId: modeUser1.id, roleId: role2.id }),
+				assignRole({ userId: modeUser2.id, roleId: role2.id, expiresAt: new Date(Date.now() - 1000) }),
+				assignRole({ userId: normalUser1.id, roleId: role3.id }),
+				assignRole({ userId: normalUser2.id, roleId: role3.id, expiresAt: new Date(Date.now() - 1000) }),
+			]);
+
+			const result = await roleService.getModeratorIds({
+				includeAdmins: false,
+				includeRoot: true,
+				excludeExpire: false,
+			});
+			expect(result).toEqual([modeUser1.id, modeUser2.id, rootUser.id]);
+		});
+
+		test('root has moderator role', async () => {
+			const [adminUser1, modeUser1, normalUser1, rootUser] = await Promise.all([
+				createUser(), createUser(), createUser(), createUser({ isRoot: true }),
+			]);
+
+			const role1 = await createRole({ name: 'admin', isAdministrator: true });
+			const role2 = await createRole({ name: 'moderator', isModerator: true });
+			const role3 = await createRole({ name: 'normal' });
+
+			await Promise.all([
+				assignRole({ userId: adminUser1.id, roleId: role1.id }),
+				assignRole({ userId: modeUser1.id, roleId: role2.id }),
+				assignRole({ userId: rootUser.id, roleId: role2.id }),
+				assignRole({ userId: normalUser1.id, roleId: role3.id }),
+			]);
+
+			const result = await roleService.getModeratorIds({
+				includeAdmins: false,
+				includeRoot: true,
+				excludeExpire: false,
+			});
+			expect(result).toEqual([modeUser1.id, rootUser.id]);
+		});
+
+		test('root has administrator role', async () => {
+			const [adminUser1, modeUser1, normalUser1, rootUser] = await Promise.all([
+				createUser(), createUser(), createUser(), createUser({ isRoot: true }),
+			]);
+
+			const role1 = await createRole({ name: 'admin', isAdministrator: true });
+			const role2 = await createRole({ name: 'moderator', isModerator: true });
+			const role3 = await createRole({ name: 'normal' });
+
+			await Promise.all([
+				assignRole({ userId: adminUser1.id, roleId: role1.id }),
+				assignRole({ userId: rootUser.id, roleId: role1.id }),
+				assignRole({ userId: modeUser1.id, roleId: role2.id }),
+				assignRole({ userId: normalUser1.id, roleId: role3.id }),
+			]);
+
+			const result = await roleService.getModeratorIds({
+				includeAdmins: true,
+				includeRoot: true,
+				excludeExpire: false,
+			});
+			expect(result).toEqual([adminUser1.id, modeUser1.id, rootUser.id]);
+		});
+
+		test('root has moderator role(expire)', async () => {
+			const [adminUser1, modeUser1, normalUser1, rootUser] = await Promise.all([
+				createUser(), createUser(), createUser(), createUser({ isRoot: true }),
+			]);
+
+			const role1 = await createRole({ name: 'admin', isAdministrator: true });
+			const role2 = await createRole({ name: 'moderator', isModerator: true });
+			const role3 = await createRole({ name: 'normal' });
+
+			await Promise.all([
+				assignRole({ userId: adminUser1.id, roleId: role1.id }),
+				assignRole({ userId: modeUser1.id, roleId: role2.id, expiresAt: new Date(Date.now() - 1000) }),
+				assignRole({ userId: rootUser.id, roleId: role2.id, expiresAt: new Date(Date.now() - 1000) }),
+				assignRole({ userId: normalUser1.id, roleId: role3.id }),
+			]);
+
+			const result = await roleService.getModeratorIds({
+				includeAdmins: false,
+				includeRoot: true,
+				excludeExpire: true,
+			});
+			expect(result).toEqual([rootUser.id]);
+		});
+	});
+
 	describe('conditional role', () => {
 		test('~かつ~', async () => {
 			const [user1, user2, user3, user4] = await Promise.all([
@@ -697,7 +912,7 @@ describe('RoleService', () => {
 			await roleService.assign(user.id, role.id);
 
 			clock.uninstall();
-			await sleep(100);
+			await setTimeout(100);
 
 			const assignments = await roleAssignmentsRepository.find({
 				where: {
@@ -725,7 +940,7 @@ describe('RoleService', () => {
 			await roleService.assign(user.id, role.id);
 
 			clock.uninstall();
-			await sleep(100);
+			await setTimeout(100);
 
 			const assignments = await roleAssignmentsRepository.find({
 				where: {
diff --git a/packages/backend/test/unit/SigninWithPasskeyApiService.ts b/packages/backend/test/unit/SigninWithPasskeyApiService.ts
new file mode 100644
index 0000000000..bae2b88c60
--- /dev/null
+++ b/packages/backend/test/unit/SigninWithPasskeyApiService.ts
@@ -0,0 +1,182 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { IncomingHttpHeaders } from 'node:http';
+import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, jest, test } from '@jest/globals';
+import { Test, TestingModule } from '@nestjs/testing';
+import { FastifyReply, FastifyRequest } from 'fastify';
+import { AuthenticationResponseJSON } from '@simplewebauthn/types';
+import { HttpHeader } from 'fastify/types/utils.js';
+import { MockFunctionMetadata, ModuleMocker } from 'jest-mock';
+import { MiUser } from '@/models/User.js';
+import { MiUserProfile, UserProfilesRepository, UsersRepository } from '@/models/_.js';
+import { IdService } from '@/core/IdService.js';
+import { GlobalModule } from '@/GlobalModule.js';
+import { DI } from '@/di-symbols.js';
+import { CoreModule } from '@/core/CoreModule.js';
+import { SigninWithPasskeyApiService } from '@/server/api/SigninWithPasskeyApiService.js';
+import { RateLimiterService } from '@/server/api/RateLimiterService.js';
+import { WebAuthnService } from '@/core/WebAuthnService.js';
+import { SigninService } from '@/server/api/SigninService.js';
+import { IdentifiableError } from '@/misc/identifiable-error.js';
+
+const moduleMocker = new ModuleMocker(global);
+
+class FakeLimiter {
+	public async limit() {
+		return;
+	}
+}
+
+class FakeSigninService {
+	public signin(..._args: any): any {
+		return true;
+	}
+}
+
+class DummyFastifyReply {
+	public statusCode: number;
+	code(num: number): void {
+		this.statusCode = num;
+	}
+	header(_key: HttpHeader, _value: any): void {
+	}
+}
+class DummyFastifyRequest {
+	public ip: string;
+	public body: {credential: any, context: string};
+	public headers: IncomingHttpHeaders = { 'accept': 'application/json' };
+	constructor(body?: any) {
+		this.ip = '0.0.0.0';
+		this.body = body;
+	}
+}
+
+type ApiFastifyRequestType = FastifyRequest<{
+	Body: {
+		credential?: AuthenticationResponseJSON;
+		context?: string;
+	};
+}>;
+
+describe('SigninWithPasskeyApiService', () => {
+	let app: TestingModule;
+	let passkeyApiService: SigninWithPasskeyApiService;
+	let usersRepository: UsersRepository;
+	let userProfilesRepository: UserProfilesRepository;
+	let webAuthnService: WebAuthnService;
+	let idService: IdService;
+	let FakeWebauthnVerify: ()=>Promise<string>;
+
+	async function createUser(data: Partial<MiUser> = {}) {
+		const user = await usersRepository
+			.save({
+				...data,
+			});
+		return user;
+	}
+
+	async function createUserProfile(data: Partial<MiUserProfile> = {}) {
+		const userProfile = await userProfilesRepository
+			.save({ ...data },
+			);
+		return userProfile;
+	}
+
+	beforeAll(async () => {
+		app = await Test.createTestingModule({
+			imports: [GlobalModule, CoreModule],
+			providers: [
+				SigninWithPasskeyApiService, 
+				{ provide: RateLimiterService, useClass: FakeLimiter }, 
+				{ provide: SigninService, useClass: FakeSigninService },
+			],
+		}).useMocker((token) => {
+			if (typeof token === 'function') {
+				const mockMetadata = moduleMocker.getMetadata(token) as MockFunctionMetadata<any, any>;
+				const Mock = moduleMocker.generateFromMetadata(mockMetadata);
+				return new Mock();
+			}
+		}).compile();
+		passkeyApiService = app.get<SigninWithPasskeyApiService>(SigninWithPasskeyApiService);
+		usersRepository = app.get<UsersRepository>(DI.usersRepository);
+		userProfilesRepository = app.get<UserProfilesRepository>(DI.userProfilesRepository);
+		webAuthnService = app.get<WebAuthnService>(WebAuthnService);
+		idService = app.get<IdService>(IdService);
+	});
+
+	beforeEach(async () => {
+		const uid = idService.gen();
+		FakeWebauthnVerify = async () => {
+			return uid;
+		};
+		jest.spyOn(webAuthnService, 'verifySignInWithPasskeyAuthentication').mockImplementation(FakeWebauthnVerify);
+
+		const dummyUser = {
+			id: uid, username: uid, usernameLower: uid.toLocaleLowerCase(), uri: null, host: null,
+		 };
+		const dummyProfile = {
+			userId: uid,
+			password: 'qwerty',
+			usePasswordLessLogin: true,
+		};
+		await createUser(dummyUser);
+		await createUserProfile(dummyProfile);
+	});
+
+	afterAll(async () => {
+		await app.close();
+	});
+
+	describe('Get Passkey Options', () => {
+		it('Should return passkey Auth Options', async () => {
+			const req = new DummyFastifyRequest({}) as ApiFastifyRequestType;
+			const res = new DummyFastifyReply() as unknown as FastifyReply;
+			const res_body = await passkeyApiService.signin(req, res);
+			expect(res.statusCode).toBe(200);
+			expect((res_body as any).option).toBeDefined();
+			expect(typeof (res_body as any).context).toBe('string');
+		});
+	});
+	describe('Try Passkey Auth', () => {
+		it('Should Success', async () => {
+			const req = new DummyFastifyRequest({ context: 'auth-context', credential: { dummy: [] } }) as ApiFastifyRequestType;
+			const res = new DummyFastifyReply() as FastifyReply;
+			const res_body = await passkeyApiService.signin(req, res);
+			expect((res_body as any).signinResponse).toBeDefined();
+		});
+
+		it('Should return 400 Without Auth Context', async () => {
+			const req = new DummyFastifyRequest({ credential: { dummy: [] } }) as ApiFastifyRequestType;
+			const res = new DummyFastifyReply() as FastifyReply;
+			const res_body = await passkeyApiService.signin(req, res);
+			expect(res.statusCode).toBe(400);
+			expect((res_body as any).error?.id).toStrictEqual('1658cc2e-4495-461f-aee4-d403cdf073c1');
+		});
+
+		it('Should return 403 When Challenge Verify fail', async () => {
+			const req = new DummyFastifyRequest({ context: 'misskey-1234', credential: { dummy: [] } }) as ApiFastifyRequestType;
+			const res = new DummyFastifyReply() as FastifyReply;
+			jest.spyOn(webAuthnService, 'verifySignInWithPasskeyAuthentication')
+				.mockImplementation(async () => {
+					throw new IdentifiableError('THIS_ERROR_CODE_SHOULD_BE_FORWARDED');
+				});
+			const res_body = await passkeyApiService.signin(req, res);
+			expect(res.statusCode).toBe(403);
+			expect((res_body as any).error?.id).toStrictEqual('THIS_ERROR_CODE_SHOULD_BE_FORWARDED');
+		});
+
+		it('Should return 403 When The user not Enabled Passwordless login', async () => {
+			const req = new DummyFastifyRequest({ context: 'misskey-1234', credential: { dummy: [] } }) as ApiFastifyRequestType;
+			const res = new DummyFastifyReply() as FastifyReply;
+			const userId = await FakeWebauthnVerify();
+			const data = { userId: userId, usePasswordLessLogin: false };
+			await userProfilesRepository.update({ userId: userId }, data);
+			const res_body = await passkeyApiService.signin(req, res);
+			expect(res.statusCode).toBe(403);
+			expect((res_body as any).error?.id).toStrictEqual('2d84773e-f7b7-4d0b-8f72-bb69b584c912');
+		});
+	});
+});
diff --git a/packages/backend/test/unit/SystemWebhookService.ts b/packages/backend/test/unit/SystemWebhookService.ts
new file mode 100644
index 0000000000..5401dd74d8
--- /dev/null
+++ b/packages/backend/test/unit/SystemWebhookService.ts
@@ -0,0 +1,517 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { setTimeout } from 'node:timers/promises';
+import { afterEach, beforeEach, describe, expect, jest } from '@jest/globals';
+import { Test, TestingModule } from '@nestjs/testing';
+import { randomString } from '../utils.js';
+import { MiUser } from '@/models/User.js';
+import { MiSystemWebhook, SystemWebhookEventType } from '@/models/SystemWebhook.js';
+import { SystemWebhooksRepository, UsersRepository } from '@/models/_.js';
+import { IdService } from '@/core/IdService.js';
+import { GlobalModule } from '@/GlobalModule.js';
+import { ModerationLogService } from '@/core/ModerationLogService.js';
+import { GlobalEventService } from '@/core/GlobalEventService.js';
+import { DI } from '@/di-symbols.js';
+import { QueueService } from '@/core/QueueService.js';
+import { LoggerService } from '@/core/LoggerService.js';
+import { SystemWebhookService } from '@/core/SystemWebhookService.js';
+
+describe('SystemWebhookService', () => {
+	let app: TestingModule;
+	let service: SystemWebhookService;
+
+	// --------------------------------------------------------------------------------------
+
+	let usersRepository: UsersRepository;
+	let systemWebhooksRepository: SystemWebhooksRepository;
+	let idService: IdService;
+	let queueService: jest.Mocked<QueueService>;
+
+	// --------------------------------------------------------------------------------------
+
+	let root: MiUser;
+
+	// --------------------------------------------------------------------------------------
+
+	async function createUser(data: Partial<MiUser> = {}) {
+		return await usersRepository
+			.insert({
+				id: idService.gen(),
+				...data,
+			})
+			.then(x => usersRepository.findOneByOrFail(x.identifiers[0]));
+	}
+
+	async function createWebhook(data: Partial<MiSystemWebhook> = {}) {
+		return systemWebhooksRepository
+			.insert({
+				id: idService.gen(),
+				name: randomString(),
+				on: ['abuseReport'],
+				url: 'https://example.com',
+				secret: randomString(),
+				...data,
+			})
+			.then(x => systemWebhooksRepository.findOneByOrFail(x.identifiers[0]));
+	}
+
+	// --------------------------------------------------------------------------------------
+
+	async function beforeAllImpl() {
+		app = await Test
+			.createTestingModule({
+				imports: [
+					GlobalModule,
+				],
+				providers: [
+					SystemWebhookService,
+					IdService,
+					LoggerService,
+					GlobalEventService,
+					{
+						provide: QueueService, useFactory: () => ({ systemWebhookDeliver: jest.fn() }),
+					},
+					{
+						provide: ModerationLogService, useFactory: () => ({ log: () => Promise.resolve() }),
+					},
+				],
+			})
+			.compile();
+
+		usersRepository = app.get(DI.usersRepository);
+		systemWebhooksRepository = app.get(DI.systemWebhooksRepository);
+
+		service = app.get(SystemWebhookService);
+		idService = app.get(IdService);
+		queueService = app.get(QueueService) as jest.Mocked<QueueService>;
+
+		app.enableShutdownHooks();
+	}
+
+	async function afterAllImpl() {
+		await app.close();
+	}
+
+	async function beforeEachImpl() {
+		root = await createUser({ isRoot: true, username: 'root', usernameLower: 'root' });
+	}
+
+	async function afterEachImpl() {
+		await usersRepository.delete({});
+		await systemWebhooksRepository.delete({});
+	}
+
+	// --------------------------------------------------------------------------------------
+
+	describe('アプリを毎回作り直す必要のないグループ', () => {
+		beforeAll(beforeAllImpl);
+		afterAll(afterAllImpl);
+		beforeEach(beforeEachImpl);
+		afterEach(afterEachImpl);
+
+		describe('fetchSystemWebhooks', () => {
+			test('フィルタなし', async () => {
+				const webhook1 = await createWebhook({
+					isActive: true,
+					on: ['abuseReport'],
+				});
+				const webhook2 = await createWebhook({
+					isActive: false,
+					on: ['abuseReport'],
+				});
+				const webhook3 = await createWebhook({
+					isActive: true,
+					on: ['abuseReportResolved'],
+				});
+				const webhook4 = await createWebhook({
+					isActive: false,
+					on: [],
+				});
+
+				const fetchedWebhooks = await service.fetchSystemWebhooks();
+				expect(fetchedWebhooks).toEqual([webhook1, webhook2, webhook3, webhook4]);
+			});
+
+			test('activeのみ', async () => {
+				const webhook1 = await createWebhook({
+					isActive: true,
+					on: ['abuseReport'],
+				});
+				const webhook2 = await createWebhook({
+					isActive: false,
+					on: ['abuseReport'],
+				});
+				const webhook3 = await createWebhook({
+					isActive: true,
+					on: ['abuseReportResolved'],
+				});
+				const webhook4 = await createWebhook({
+					isActive: false,
+					on: [],
+				});
+
+				const fetchedWebhooks = await service.fetchSystemWebhooks({ isActive: true });
+				expect(fetchedWebhooks).toEqual([webhook1, webhook3]);
+			});
+
+			test('特定のイベントのみ', async () => {
+				const webhook1 = await createWebhook({
+					isActive: true,
+					on: ['abuseReport'],
+				});
+				const webhook2 = await createWebhook({
+					isActive: false,
+					on: ['abuseReport'],
+				});
+				const webhook3 = await createWebhook({
+					isActive: true,
+					on: ['abuseReportResolved'],
+				});
+				const webhook4 = await createWebhook({
+					isActive: false,
+					on: [],
+				});
+
+				const fetchedWebhooks = await service.fetchSystemWebhooks({ on: ['abuseReport'] });
+				expect(fetchedWebhooks).toEqual([webhook1, webhook2]);
+			});
+
+			test('activeな特定のイベントのみ', async () => {
+				const webhook1 = await createWebhook({
+					isActive: true,
+					on: ['abuseReport'],
+				});
+				const webhook2 = await createWebhook({
+					isActive: false,
+					on: ['abuseReport'],
+				});
+				const webhook3 = await createWebhook({
+					isActive: true,
+					on: ['abuseReportResolved'],
+				});
+				const webhook4 = await createWebhook({
+					isActive: false,
+					on: [],
+				});
+
+				const fetchedWebhooks = await service.fetchSystemWebhooks({ on: ['abuseReport'], isActive: true });
+				expect(fetchedWebhooks).toEqual([webhook1]);
+			});
+
+			test('ID指定', async () => {
+				const webhook1 = await createWebhook({
+					isActive: true,
+					on: ['abuseReport'],
+				});
+				const webhook2 = await createWebhook({
+					isActive: false,
+					on: ['abuseReport'],
+				});
+				const webhook3 = await createWebhook({
+					isActive: true,
+					on: ['abuseReportResolved'],
+				});
+				const webhook4 = await createWebhook({
+					isActive: false,
+					on: [],
+				});
+
+				const fetchedWebhooks = await service.fetchSystemWebhooks({ ids: [webhook1.id, webhook4.id] });
+				expect(fetchedWebhooks).toEqual([webhook1, webhook4]);
+			});
+
+			test('ID指定(他条件とANDになるか見たい)', async () => {
+				const webhook1 = await createWebhook({
+					isActive: true,
+					on: ['abuseReport'],
+				});
+				const webhook2 = await createWebhook({
+					isActive: false,
+					on: ['abuseReport'],
+				});
+				const webhook3 = await createWebhook({
+					isActive: true,
+					on: ['abuseReportResolved'],
+				});
+				const webhook4 = await createWebhook({
+					isActive: false,
+					on: [],
+				});
+
+				const fetchedWebhooks = await service.fetchSystemWebhooks({ ids: [webhook1.id, webhook4.id], isActive: false });
+				expect(fetchedWebhooks).toEqual([webhook4]);
+			});
+		});
+
+		describe('createSystemWebhook', () => {
+			test('作成成功	', async () => {
+				const params = {
+					isActive: true,
+					name: randomString(),
+					on: ['abuseReport'] as SystemWebhookEventType[],
+					url: 'https://example.com',
+					secret: randomString(),
+				};
+
+				const webhook = await service.createSystemWebhook(params, root);
+				expect(webhook).toMatchObject(params);
+			});
+		});
+
+		describe('updateSystemWebhook', () => {
+			test('更新成功', async () => {
+				const webhook = await createWebhook({
+					isActive: true,
+					on: ['abuseReport'],
+				});
+
+				const params = {
+					id: webhook.id,
+					isActive: false,
+					name: randomString(),
+					on: ['abuseReport'] as SystemWebhookEventType[],
+					url: randomString(),
+					secret: randomString(),
+				};
+
+				const updatedWebhook = await service.updateSystemWebhook(params, root);
+				expect(updatedWebhook).toMatchObject(params);
+			});
+		});
+
+		describe('deleteSystemWebhook', () => {
+			test('削除成功', async () => {
+				const webhook = await createWebhook({
+					isActive: true,
+					on: ['abuseReport'],
+				});
+
+				await service.deleteSystemWebhook(webhook.id, root);
+
+				await expect(systemWebhooksRepository.findOneBy({ id: webhook.id })).resolves.toBeNull();
+			});
+		});
+	});
+
+	describe('アプリを毎回作り直す必要があるグループ', () => {
+		beforeEach(async () => {
+			await beforeAllImpl();
+			await beforeEachImpl();
+		});
+
+		afterEach(async () => {
+			await afterEachImpl();
+			await afterAllImpl();
+		});
+
+		describe('enqueueSystemWebhook', () => {
+			test('キューに追加成功', async () => {
+				const webhook = await createWebhook({
+					isActive: true,
+					on: ['abuseReport'],
+				});
+				await service.enqueueSystemWebhook(webhook.id, 'abuseReport', { foo: 'bar' } as any);
+
+				expect(queueService.systemWebhookDeliver).toHaveBeenCalled();
+			});
+
+			test('非アクティブなWebhookはキューに追加されない', async () => {
+				const webhook = await createWebhook({
+					isActive: false,
+					on: ['abuseReport'],
+				});
+				await service.enqueueSystemWebhook(webhook.id, 'abuseReport', { foo: 'bar' } as any);
+
+				expect(queueService.systemWebhookDeliver).not.toHaveBeenCalled();
+			});
+
+			test('未許可のイベント種別が渡された場合はWebhookはキューに追加されない', async () => {
+				const webhook1 = await createWebhook({
+					isActive: true,
+					on: [],
+				});
+				const webhook2 = await createWebhook({
+					isActive: true,
+					on: ['abuseReportResolved'],
+				});
+				await service.enqueueSystemWebhook(webhook1.id, 'abuseReport', { foo: 'bar' } as any);
+				await service.enqueueSystemWebhook(webhook2.id, 'abuseReport', { foo: 'bar' } as any);
+
+				expect(queueService.systemWebhookDeliver).not.toHaveBeenCalled();
+			});
+		});
+
+		describe('fetchActiveSystemWebhooks', () => {
+			describe('systemWebhookCreated', () => {
+				test('ActiveなWebhookが追加された時、キャッシュに追加されている', async () => {
+					const webhook = await service.createSystemWebhook(
+						{
+							isActive: true,
+							name: randomString(),
+							on: ['abuseReport'],
+							url: 'https://example.com',
+							secret: randomString(),
+						},
+						root,
+					);
+
+					// redisでの配信経由で更新されるのでちょっと待つ
+					await setTimeout(500);
+
+					const fetchedWebhooks = await service.fetchActiveSystemWebhooks();
+					expect(fetchedWebhooks).toEqual([webhook]);
+				});
+
+				test('NotActiveなWebhookが追加された時、キャッシュに追加されていない', async () => {
+					const webhook = await service.createSystemWebhook(
+						{
+							isActive: false,
+							name: randomString(),
+							on: ['abuseReport'],
+							url: 'https://example.com',
+							secret: randomString(),
+						},
+						root,
+					);
+
+					// redisでの配信経由で更新されるのでちょっと待つ
+					await setTimeout(500);
+
+					const fetchedWebhooks = await service.fetchActiveSystemWebhooks();
+					expect(fetchedWebhooks).toEqual([]);
+				});
+			});
+
+			describe('systemWebhookUpdated', () => {
+				test('ActiveなWebhookが編集された時、キャッシュに反映されている', async () => {
+					const id = idService.gen();
+					await createWebhook({ id });
+					// キャッシュ作成
+					const webhook1 = await service.fetchActiveSystemWebhooks();
+					// 読み込まれていることをチェック
+					expect(webhook1.length).toEqual(1);
+					expect(webhook1[0].id).toEqual(id);
+
+					const webhook2 = await service.updateSystemWebhook(
+						{
+							id,
+							isActive: true,
+							name: randomString(),
+							on: ['abuseReport'],
+							url: 'https://example.com',
+							secret: randomString(),
+						},
+						root,
+					);
+
+					// redisでの配信経由で更新されるのでちょっと待つ
+					await setTimeout(500);
+
+					const fetchedWebhooks = await service.fetchActiveSystemWebhooks();
+					expect(fetchedWebhooks).toEqual([webhook2]);
+				});
+
+				test('NotActiveなWebhookが編集された時、キャッシュに追加されない', async () => {
+					const id = idService.gen();
+					await createWebhook({ id, isActive: false });
+					// キャッシュ作成
+					const webhook1 = await service.fetchActiveSystemWebhooks();
+					// 読み込まれていないことをチェック
+					expect(webhook1.length).toEqual(0);
+
+					const webhook2 = await service.updateSystemWebhook(
+						{
+							id,
+							isActive: false,
+							name: randomString(),
+							on: ['abuseReport'],
+							url: 'https://example.com',
+							secret: randomString(),
+						},
+						root,
+					);
+
+					// redisでの配信経由で更新されるのでちょっと待つ
+					await setTimeout(500);
+
+					const fetchedWebhooks = await service.fetchActiveSystemWebhooks();
+					expect(fetchedWebhooks.length).toEqual(0);
+				});
+
+				test('NotActiveなWebhookがActiveにされた時、キャッシュに追加されている', async () => {
+					const id = idService.gen();
+					const baseWebhook = await createWebhook({ id, isActive: false });
+					// キャッシュ作成
+					const webhook1 = await service.fetchActiveSystemWebhooks();
+					// 読み込まれていないことをチェック
+					expect(webhook1.length).toEqual(0);
+
+					const webhook2 = await service.updateSystemWebhook(
+						{
+							...baseWebhook,
+							isActive: true,
+						},
+						root,
+					);
+
+					// redisでの配信経由で更新されるのでちょっと待つ
+					await setTimeout(500);
+
+					const fetchedWebhooks = await service.fetchActiveSystemWebhooks();
+					expect(fetchedWebhooks).toEqual([webhook2]);
+				});
+
+				test('ActiveなWebhookがNotActiveにされた時、キャッシュから削除されている', async () => {
+					const id = idService.gen();
+					const baseWebhook = await createWebhook({ id, isActive: true });
+					// キャッシュ作成
+					const webhook1 = await service.fetchActiveSystemWebhooks();
+					// 読み込まれていることをチェック
+					expect(webhook1.length).toEqual(1);
+					expect(webhook1[0].id).toEqual(id);
+
+					const webhook2 = await service.updateSystemWebhook(
+						{
+							...baseWebhook,
+							isActive: false,
+						},
+						root,
+					);
+
+					// redisでの配信経由で更新されるのでちょっと待つ
+					await setTimeout(500);
+
+					const fetchedWebhooks = await service.fetchActiveSystemWebhooks();
+					expect(fetchedWebhooks.length).toEqual(0);
+				});
+			});
+
+			describe('systemWebhookDeleted', () => {
+				test('キャッシュから削除されている', async () => {
+					const id = idService.gen();
+					const baseWebhook = await createWebhook({ id, isActive: true });
+					// キャッシュ作成
+					const webhook1 = await service.fetchActiveSystemWebhooks();
+					// 読み込まれていることをチェック
+					expect(webhook1.length).toEqual(1);
+					expect(webhook1[0].id).toEqual(id);
+
+					const webhook2 = await service.deleteSystemWebhook(
+						id,
+						root,
+					);
+
+					// redisでの配信経由で更新されるのでちょっと待つ
+					await setTimeout(500);
+
+					const fetchedWebhooks = await service.fetchActiveSystemWebhooks();
+					expect(fetchedWebhooks.length).toEqual(0);
+				});
+			});
+		});
+	});
+});
diff --git a/packages/backend/test/unit/UserSearchService.ts b/packages/backend/test/unit/UserSearchService.ts
new file mode 100644
index 0000000000..7ea325d420
--- /dev/null
+++ b/packages/backend/test/unit/UserSearchService.ts
@@ -0,0 +1,265 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Test, TestingModule } from '@nestjs/testing';
+import { describe, jest, test } from '@jest/globals';
+import { In } from 'typeorm';
+import { UserSearchService } from '@/core/UserSearchService.js';
+import { FollowingsRepository, MiUser, UserProfilesRepository, UsersRepository } from '@/models/_.js';
+import { IdService } from '@/core/IdService.js';
+import { GlobalModule } from '@/GlobalModule.js';
+import { DI } from '@/di-symbols.js';
+import { UserEntityService } from '@/core/entities/UserEntityService.js';
+
+describe('UserSearchService', () => {
+	let app: TestingModule;
+	let service: UserSearchService;
+
+	let usersRepository: UsersRepository;
+	let followingsRepository: FollowingsRepository;
+	let idService: IdService;
+	let userProfilesRepository: UserProfilesRepository;
+
+	let root: MiUser;
+	let alice: MiUser;
+	let alyce: MiUser;
+	let alycia: MiUser;
+	let alysha: MiUser;
+	let alyson: MiUser;
+	let alyssa: MiUser;
+	let bob: MiUser;
+	let bobbi: MiUser;
+	let bobbie: MiUser;
+	let bobby: MiUser;
+
+	async function createUser(data: Partial<MiUser> = {}) {
+		const user = await usersRepository
+			.insert({
+				id: idService.gen(),
+				...data,
+			})
+			.then(x => usersRepository.findOneByOrFail(x.identifiers[0]));
+
+		await userProfilesRepository.insert({
+			userId: user.id,
+		});
+
+		return user;
+	}
+
+	async function createFollowings(follower: MiUser, followees: MiUser[]) {
+		for (const followee of followees) {
+			await followingsRepository.insert({
+				id: idService.gen(),
+				followerId: follower.id,
+				followeeId: followee.id,
+			});
+		}
+	}
+
+	async function setActive(users: MiUser[]) {
+		for (const user of users) {
+			await usersRepository.update(user.id, {
+				updatedAt: new Date(),
+			});
+		}
+	}
+
+	async function setInactive(users: MiUser[]) {
+		for (const user of users) {
+			await usersRepository.update(user.id, {
+				updatedAt: new Date(0),
+			});
+		}
+	}
+
+	async function setSuspended(users: MiUser[]) {
+		for (const user of users) {
+			await usersRepository.update(user.id, {
+				isSuspended: true,
+			});
+		}
+	}
+
+	beforeAll(async () => {
+		app = await Test
+			.createTestingModule({
+				imports: [
+					GlobalModule,
+				],
+				providers: [
+					UserSearchService,
+					{
+						provide: UserEntityService, useFactory: jest.fn(() => ({
+							// とりあえずIDが返れば確認が出来るので
+							packMany: (value: any) => value,
+						})),
+					},
+					IdService,
+				],
+			})
+			.compile();
+
+		await app.init();
+
+		usersRepository = app.get(DI.usersRepository);
+		userProfilesRepository = app.get(DI.userProfilesRepository);
+		followingsRepository = app.get(DI.followingsRepository);
+
+		service = app.get(UserSearchService);
+		idService = app.get(IdService);
+	});
+
+	beforeEach(async () => {
+		root = await createUser({ username: 'root', usernameLower: 'root', isRoot: true });
+		alice = await createUser({ username: 'Alice', usernameLower: 'alice' });
+		alyce = await createUser({ username: 'Alyce', usernameLower: 'alyce' });
+		alycia = await createUser({ username: 'Alycia', usernameLower: 'alycia' });
+		alysha = await createUser({ username: 'Alysha', usernameLower: 'alysha' });
+		alyson = await createUser({ username: 'Alyson', usernameLower: 'alyson', host: 'example.com' });
+		alyssa = await createUser({ username: 'Alyssa', usernameLower: 'alyssa', host: 'example.com' });
+		bob = await createUser({ username: 'Bob', usernameLower: 'bob' });
+		bobbi = await createUser({ username: 'Bobbi', usernameLower: 'bobbi' });
+		bobbie = await createUser({ username: 'Bobbie', usernameLower: 'bobbie', host: 'example.com' });
+		bobby = await createUser({ username: 'Bobby', usernameLower: 'bobby', host: 'example.com' });
+	});
+
+	afterEach(async () => {
+		await usersRepository.delete({});
+	});
+
+	afterAll(async () => {
+		await app.close();
+	});
+
+	describe('search', () => {
+		test('フォロー中のアクティブユーザのうち、"al"から始まる人が全員ヒットする', async () => {
+			await createFollowings(root, [alice, alyce, alycia, alysha, alyson, alyssa, bob, bobbi, bobbie, bobby]);
+			await setActive([alice, alyce, alyssa, bob, bobbi, bobbie, bobby]);
+			await setInactive([alycia, alysha, alyson]);
+
+			const result = await service.search(
+				{ username: 'al' },
+				{ limit: 100 },
+				root,
+			);
+
+			// alycia, alysha, alysonは非アクティブなので後ろに行く
+			expect(result).toEqual([alice, alyce, alyssa, alycia, alysha, alyson].map(x => x.id));
+		});
+
+		test('フォロー中の非アクティブユーザのうち、"al"から始まる人が全員ヒットする', async () => {
+			await createFollowings(root, [alycia, alysha, alyson, alyssa, bob, bobbi, bobbie, bobby]);
+			await setInactive([alice, alyce, alycia, alysha, alyson, alyssa, bob, bobbi, bobbie, bobby]);
+
+			const result = await service.search(
+				{ username: 'al' },
+				{ limit: 100 },
+				root,
+			);
+
+			// alice, alyceはフォローしていないので後ろに行く
+			expect(result).toEqual([alycia, alysha, alyson, alyssa, alice, alyce].map(x => x.id));
+		});
+
+		test('フォローしていないアクティブユーザのうち、"al"から始まる人が全員ヒットする', async () => {
+			await setActive([alysha, alyson, alyssa, bob, bobbi, bobbie, bobby]);
+			await setInactive([alice, alyce, alycia]);
+
+			const result = await service.search(
+				{ username: 'al' },
+				{ limit: 100 },
+				root,
+			);
+
+			// alice, alyce, alyciaは非アクティブなので後ろに行く
+			expect(result).toEqual([alysha, alyson, alyssa, alice, alyce, alycia].map(x => x.id));
+		});
+
+		test('フォローしていない非アクティブユーザのうち、"al"から始まる人が全員ヒットする', async () => {
+			await setInactive([alice, alyce, alycia, alysha, alyson, alyssa, bob, bobbi, bobbie, bobby]);
+
+			const result = await service.search(
+				{ username: 'al' },
+				{ limit: 100 },
+				root,
+			);
+
+			expect(result).toEqual([alice, alyce, alycia, alysha, alyson, alyssa].map(x => x.id));
+		});
+
+		test('フォロー(アクティブ)、フォロー(非アクティブ)、非フォロー(アクティブ)、非フォロー(非アクティブ)混在時の優先順位度確認', async () => {
+			await createFollowings(root, [alyson, alyssa, bob, bobbi, bobbie]);
+			await setActive([root, alyssa, bob, bobbi, alyce, alycia]);
+			await setInactive([alyson, alice, alysha, bobbie, bobby]);
+
+			const result = await service.search(
+				{ },
+				{ limit: 100 },
+				root,
+			);
+
+			// 見る用
+			// const users = await usersRepository.findBy({ id: In(result) }).then(it => new Map(it.map(x => [x.id, x])));
+			// console.log(result.map(x => users.get(x as any)).map(it => it?.username));
+
+			// フォローしててアクティブなので先頭: alyssa, bob, bobbi
+			// フォローしてて非アクティブなので次: alyson, bobbie
+			// フォローしてないけどアクティブなので次: alyce, alycia, root(アルファベット順的にここになる)
+			// フォローしてないし非アクティブなので最後: alice, alysha, bobby
+			expect(result).toEqual([alyssa, bob, bobbi, alyson, bobbie, alyce, alycia, root, alice, alysha, bobby].map(x => x.id));
+		});
+
+		test('[非ログイン] アクティブユーザのうち、"al"から始まる人が全員ヒットする', async () => {
+			await setActive([alysha, alyson, alyssa, bob, bobbi, bobbie, bobby]);
+			await setInactive([alice, alyce, alycia]);
+
+			const result = await service.search(
+				{ username: 'al' },
+				{ limit: 100 },
+			);
+
+			// alice, alyce, alyciaは非アクティブなので後ろに行く
+			expect(result).toEqual([alysha, alyson, alyssa, alice, alyce, alycia].map(x => x.id));
+		});
+
+		test('[非ログイン] 非アクティブユーザのうち、"al"から始まる人が全員ヒットする', async () => {
+			await setInactive([alice, alyce, alycia, alysha, alyson, alyssa, bob, bobbi, bobbie, bobby]);
+
+			const result = await service.search(
+				{ username: 'al' },
+				{ limit: 100 },
+			);
+
+			expect(result).toEqual([alice, alyce, alycia, alysha, alyson, alyssa].map(x => x.id));
+		});
+
+		test('フォロー中のアクティブユーザのうち、"al"から始まり"example.com"にいる人が全員ヒットする', async () => {
+			await createFollowings(root, [alice, alyce, alycia, alysha, alyson, alyssa, bob, bobbi, bobbie, bobby]);
+			await setActive([alice, alyce, alycia, alysha, alyson, alyssa, bob, bobbi, bobbie, bobby]);
+
+			const result = await service.search(
+				{ username: 'al', host: 'exam' },
+				{ limit: 100 },
+				root,
+			);
+
+			expect(result).toEqual([alyson, alyssa].map(x => x.id));
+		});
+
+		test('サスペンド済みユーザは出ない', async () => {
+			await setActive([alice, alyce, alycia, alysha, alyson, alyssa, bob, bobbi, bobbie, bobby]);
+			await setSuspended([alice, alyce, alycia]);
+
+			const result = await service.search(
+				{ username: 'al' },
+				{ limit: 100 },
+				root,
+			);
+
+			expect(result).toEqual([alysha, alyson, alyssa].map(x => x.id));
+		});
+	});
+});
diff --git a/packages/backend/test/unit/UserWebhookService.ts b/packages/backend/test/unit/UserWebhookService.ts
new file mode 100644
index 0000000000..0e88835a02
--- /dev/null
+++ b/packages/backend/test/unit/UserWebhookService.ts
@@ -0,0 +1,245 @@
+
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { afterEach, beforeEach, describe, expect, jest } from '@jest/globals';
+import { Test, TestingModule } from '@nestjs/testing';
+import { randomString } from '../utils.js';
+import { MiUser } from '@/models/User.js';
+import { MiWebhook, UsersRepository, WebhooksRepository } from '@/models/_.js';
+import { IdService } from '@/core/IdService.js';
+import { GlobalModule } from '@/GlobalModule.js';
+import { GlobalEventService } from '@/core/GlobalEventService.js';
+import { DI } from '@/di-symbols.js';
+import { QueueService } from '@/core/QueueService.js';
+import { LoggerService } from '@/core/LoggerService.js';
+import { UserWebhookService } from '@/core/UserWebhookService.js';
+
+describe('UserWebhookService', () => {
+	let app: TestingModule;
+	let service: UserWebhookService;
+
+	// --------------------------------------------------------------------------------------
+
+	let usersRepository: UsersRepository;
+	let userWebhooksRepository: WebhooksRepository;
+	let idService: IdService;
+	let queueService: jest.Mocked<QueueService>;
+
+	// --------------------------------------------------------------------------------------
+
+	let root: MiUser;
+
+	// --------------------------------------------------------------------------------------
+
+	async function createUser(data: Partial<MiUser> = {}) {
+		return await usersRepository
+			.insert({
+				id: idService.gen(),
+				...data,
+			})
+			.then(x => usersRepository.findOneByOrFail(x.identifiers[0]));
+	}
+
+	async function createWebhook(data: Partial<MiWebhook> = {}) {
+		return userWebhooksRepository
+			.insert({
+				id: idService.gen(),
+				name: randomString(),
+				on: ['mention'],
+				url: 'https://example.com',
+				secret: randomString(),
+				userId: root.id,
+				...data,
+			})
+			.then(x => userWebhooksRepository.findOneByOrFail(x.identifiers[0]));
+	}
+
+	// --------------------------------------------------------------------------------------
+
+	async function beforeAllImpl() {
+		app = await Test
+			.createTestingModule({
+				imports: [
+					GlobalModule,
+				],
+				providers: [
+					UserWebhookService,
+					IdService,
+					LoggerService,
+					GlobalEventService,
+					{
+						provide: QueueService, useFactory: () => ({ systemWebhookDeliver: jest.fn() }),
+					},
+				],
+			})
+			.compile();
+
+		usersRepository = app.get(DI.usersRepository);
+		userWebhooksRepository = app.get(DI.webhooksRepository);
+
+		service = app.get(UserWebhookService);
+		idService = app.get(IdService);
+		queueService = app.get(QueueService) as jest.Mocked<QueueService>;
+
+		app.enableShutdownHooks();
+	}
+
+	async function afterAllImpl() {
+		await app.close();
+	}
+
+	async function beforeEachImpl() {
+		root = await createUser({ isRoot: true, username: 'root', usernameLower: 'root' });
+	}
+
+	async function afterEachImpl() {
+		await usersRepository.delete({});
+		await userWebhooksRepository.delete({});
+	}
+
+	// --------------------------------------------------------------------------------------
+
+	describe('アプリを毎回作り直す必要のないグループ', () => {
+		beforeAll(beforeAllImpl);
+		afterAll(afterAllImpl);
+		beforeEach(beforeEachImpl);
+		afterEach(afterEachImpl);
+
+		describe('fetchSystemWebhooks', () => {
+			test('フィルタなし', async () => {
+				const webhook1 = await createWebhook({
+					active: true,
+					on: ['mention'],
+				});
+				const webhook2 = await createWebhook({
+					active: false,
+					on: ['mention'],
+				});
+				const webhook3 = await createWebhook({
+					active: true,
+					on: ['reply'],
+				});
+				const webhook4 = await createWebhook({
+					active: false,
+					on: [],
+				});
+
+				const fetchedWebhooks = await service.fetchWebhooks();
+				expect(fetchedWebhooks).toEqual([webhook1, webhook2, webhook3, webhook4]);
+			});
+
+			test('activeのみ', async () => {
+				const webhook1 = await createWebhook({
+					active: true,
+					on: ['mention'],
+				});
+				const webhook2 = await createWebhook({
+					active: false,
+					on: ['mention'],
+				});
+				const webhook3 = await createWebhook({
+					active: true,
+					on: ['reply'],
+				});
+				const webhook4 = await createWebhook({
+					active: false,
+					on: [],
+				});
+
+				const fetchedWebhooks = await service.fetchWebhooks({ isActive: true });
+				expect(fetchedWebhooks).toEqual([webhook1, webhook3]);
+			});
+
+			test('特定のイベントのみ', async () => {
+				const webhook1 = await createWebhook({
+					active: true,
+					on: ['mention'],
+				});
+				const webhook2 = await createWebhook({
+					active: false,
+					on: ['mention'],
+				});
+				const webhook3 = await createWebhook({
+					active: true,
+					on: ['reply'],
+				});
+				const webhook4 = await createWebhook({
+					active: false,
+					on: [],
+				});
+
+				const fetchedWebhooks = await service.fetchWebhooks({ on: ['mention'] });
+				expect(fetchedWebhooks).toEqual([webhook1, webhook2]);
+			});
+
+			test('activeな特定のイベントのみ', async () => {
+				const webhook1 = await createWebhook({
+					active: true,
+					on: ['mention'],
+				});
+				const webhook2 = await createWebhook({
+					active: false,
+					on: ['mention'],
+				});
+				const webhook3 = await createWebhook({
+					active: true,
+					on: ['reply'],
+				});
+				const webhook4 = await createWebhook({
+					active: false,
+					on: [],
+				});
+
+				const fetchedWebhooks = await service.fetchWebhooks({ on: ['mention'], isActive: true });
+				expect(fetchedWebhooks).toEqual([webhook1]);
+			});
+
+			test('ID指定', async () => {
+				const webhook1 = await createWebhook({
+					active: true,
+					on: ['mention'],
+				});
+				const webhook2 = await createWebhook({
+					active: false,
+					on: ['mention'],
+				});
+				const webhook3 = await createWebhook({
+					active: true,
+					on: ['reply'],
+				});
+				const webhook4 = await createWebhook({
+					active: false,
+					on: [],
+				});
+
+				const fetchedWebhooks = await service.fetchWebhooks({ ids: [webhook1.id, webhook4.id] });
+				expect(fetchedWebhooks).toEqual([webhook1, webhook4]);
+			});
+
+			test('ID指定(他条件とANDになるか見たい)', async () => {
+				const webhook1 = await createWebhook({
+					active: true,
+					on: ['mention'],
+				});
+				const webhook2 = await createWebhook({
+					active: false,
+					on: ['mention'],
+				});
+				const webhook3 = await createWebhook({
+					active: true,
+					on: ['reply'],
+				});
+				const webhook4 = await createWebhook({
+					active: false,
+					on: [],
+				});
+
+				const fetchedWebhooks = await service.fetchWebhooks({ ids: [webhook1.id, webhook4.id], isActive: false });
+				expect(fetchedWebhooks).toEqual([webhook4]);
+			});
+		});
+	});
+});
diff --git a/packages/backend/test/unit/WebhookTestService.ts b/packages/backend/test/unit/WebhookTestService.ts
new file mode 100644
index 0000000000..5e63b86f8f
--- /dev/null
+++ b/packages/backend/test/unit/WebhookTestService.ts
@@ -0,0 +1,225 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Test, TestingModule } from '@nestjs/testing';
+import { beforeAll, describe, jest } from '@jest/globals';
+import { WebhookTestService } from '@/core/WebhookTestService.js';
+import { UserWebhookService } from '@/core/UserWebhookService.js';
+import { SystemWebhookService } from '@/core/SystemWebhookService.js';
+import { GlobalModule } from '@/GlobalModule.js';
+import { MiSystemWebhook, MiUser, MiWebhook, UserProfilesRepository, UsersRepository } from '@/models/_.js';
+import { IdService } from '@/core/IdService.js';
+import { DI } from '@/di-symbols.js';
+import { QueueService } from '@/core/QueueService.js';
+
+describe('WebhookTestService', () => {
+	let app: TestingModule;
+	let service: WebhookTestService;
+
+	// --------------------------------------------------------------------------------------
+
+	let usersRepository: UsersRepository;
+	let userProfilesRepository: UserProfilesRepository;
+	let queueService: jest.Mocked<QueueService>;
+	let userWebhookService: jest.Mocked<UserWebhookService>;
+	let systemWebhookService: jest.Mocked<SystemWebhookService>;
+	let idService: IdService;
+
+	let root: MiUser;
+	let alice: MiUser;
+
+	async function createUser(data: Partial<MiUser> = {}) {
+		const user = await usersRepository
+			.insert({
+				id: idService.gen(),
+				...data,
+			})
+			.then(x => usersRepository.findOneByOrFail(x.identifiers[0]));
+
+		await userProfilesRepository.insert({
+			userId: user.id,
+		});
+
+		return user;
+	}
+
+	// --------------------------------------------------------------------------------------
+
+	beforeAll(async () => {
+		app = await Test.createTestingModule({
+			imports: [
+				GlobalModule,
+			],
+			providers: [
+				WebhookTestService,
+				IdService,
+				{
+					provide: QueueService, useFactory: () => ({
+						systemWebhookDeliver: jest.fn(),
+						userWebhookDeliver: jest.fn(),
+					}),
+				},
+				{
+					provide: UserWebhookService, useFactory: () => ({
+						fetchWebhooks: jest.fn(),
+					}),
+				},
+				{
+					provide: SystemWebhookService, useFactory: () => ({
+						fetchSystemWebhooks: jest.fn(),
+					}),
+				},
+			],
+		}).compile();
+
+		usersRepository = app.get(DI.usersRepository);
+		userProfilesRepository = app.get(DI.userProfilesRepository);
+
+		service = app.get(WebhookTestService);
+		idService = app.get(IdService);
+		queueService = app.get(QueueService) as jest.Mocked<QueueService>;
+		userWebhookService = app.get(UserWebhookService) as jest.Mocked<UserWebhookService>;
+		systemWebhookService = app.get(SystemWebhookService) as jest.Mocked<SystemWebhookService>;
+
+		app.enableShutdownHooks();
+	});
+
+	beforeEach(async () => {
+		root = await createUser({ username: 'root', usernameLower: 'root', isRoot: true });
+		alice = await createUser({ username: 'alice', usernameLower: 'alice', isRoot: false });
+
+		userWebhookService.fetchWebhooks.mockReturnValue(Promise.resolve([
+			{ id: 'dummy-webhook', active: true, userId: alice.id } as MiWebhook,
+		]));
+		systemWebhookService.fetchSystemWebhooks.mockReturnValue(Promise.resolve([
+			{ id: 'dummy-webhook', isActive: true } as MiSystemWebhook,
+		]));
+	});
+
+	afterEach(async () => {
+		queueService.systemWebhookDeliver.mockClear();
+		queueService.userWebhookDeliver.mockClear();
+		userWebhookService.fetchWebhooks.mockClear();
+		systemWebhookService.fetchSystemWebhooks.mockClear();
+
+		await usersRepository.delete({});
+		await userProfilesRepository.delete({});
+	});
+
+	afterAll(async () => {
+		await app.close();
+	});
+
+	// --------------------------------------------------------------------------------------
+
+	describe('testUserWebhook', () => {
+		test('note', async () => {
+			await service.testUserWebhook({ webhookId: 'dummy-webhook', type: 'note' }, alice);
+
+			const calls = queueService.userWebhookDeliver.mock.calls[0];
+			expect((calls[0] as any).id).toBe('dummy-webhook');
+			expect(calls[1]).toBe('note');
+			expect((calls[2] as any).id).toBe('dummy-note-1');
+		});
+
+		test('reply', async () => {
+			await service.testUserWebhook({ webhookId: 'dummy-webhook', type: 'reply' }, alice);
+
+			const calls = queueService.userWebhookDeliver.mock.calls[0];
+			expect((calls[0] as any).id).toBe('dummy-webhook');
+			expect(calls[1]).toBe('reply');
+			expect((calls[2] as any).id).toBe('dummy-reply-1');
+		});
+
+		test('renote', async () => {
+			await service.testUserWebhook({ webhookId: 'dummy-webhook', type: 'renote' }, alice);
+
+			const calls = queueService.userWebhookDeliver.mock.calls[0];
+			expect((calls[0] as any).id).toBe('dummy-webhook');
+			expect(calls[1]).toBe('renote');
+			expect((calls[2] as any).id).toBe('dummy-renote-1');
+		});
+
+		test('mention', async () => {
+			await service.testUserWebhook({ webhookId: 'dummy-webhook', type: 'mention' }, alice);
+
+			const calls = queueService.userWebhookDeliver.mock.calls[0];
+			expect((calls[0] as any).id).toBe('dummy-webhook');
+			expect(calls[1]).toBe('mention');
+			expect((calls[2] as any).id).toBe('dummy-mention-1');
+		});
+
+		test('follow', async () => {
+			await service.testUserWebhook({ webhookId: 'dummy-webhook', type: 'follow' }, alice);
+
+			const calls = queueService.userWebhookDeliver.mock.calls[0];
+			expect((calls[0] as any).id).toBe('dummy-webhook');
+			expect(calls[1]).toBe('follow');
+			expect((calls[2] as any).id).toBe('dummy-user-1');
+		});
+
+		test('followed', async () => {
+			await service.testUserWebhook({ webhookId: 'dummy-webhook', type: 'followed' }, alice);
+
+			const calls = queueService.userWebhookDeliver.mock.calls[0];
+			expect((calls[0] as any).id).toBe('dummy-webhook');
+			expect(calls[1]).toBe('followed');
+			expect((calls[2] as any).id).toBe('dummy-user-2');
+		});
+
+		test('unfollow', async () => {
+			await service.testUserWebhook({ webhookId: 'dummy-webhook', type: 'unfollow' }, alice);
+
+			const calls = queueService.userWebhookDeliver.mock.calls[0];
+			expect((calls[0] as any).id).toBe('dummy-webhook');
+			expect(calls[1]).toBe('unfollow');
+			expect((calls[2] as any).id).toBe('dummy-user-3');
+		});
+
+		describe('NoSuchWebhookError', () => {
+			test('user not match', async () => {
+				userWebhookService.fetchWebhooks.mockClear();
+				userWebhookService.fetchWebhooks.mockReturnValue(Promise.resolve([
+					{ id: 'dummy-webhook', active: true } as MiWebhook,
+				]));
+
+				await expect(service.testUserWebhook({ webhookId: 'dummy-webhook', type: 'note' }, root))
+					.rejects.toThrow(WebhookTestService.NoSuchWebhookError);
+			});
+		});
+	});
+
+	describe('testSystemWebhook', () => {
+		test('abuseReport', async () => {
+			await service.testSystemWebhook({ webhookId: 'dummy-webhook', type: 'abuseReport' });
+
+			const calls = queueService.systemWebhookDeliver.mock.calls[0];
+			expect((calls[0] as any).id).toBe('dummy-webhook');
+			expect(calls[1]).toBe('abuseReport');
+			expect((calls[2] as any).id).toBe('dummy-abuse-report1');
+			expect((calls[2] as any).resolved).toBe(false);
+		});
+
+		test('abuseReportResolved', async () => {
+			await service.testSystemWebhook({ webhookId: 'dummy-webhook', type: 'abuseReportResolved' });
+
+			const calls = queueService.systemWebhookDeliver.mock.calls[0];
+			expect((calls[0] as any).id).toBe('dummy-webhook');
+			expect(calls[1]).toBe('abuseReportResolved');
+			expect((calls[2] as any).id).toBe('dummy-abuse-report1');
+			expect((calls[2] as any).resolved).toBe(true);
+		});
+
+		test('userCreated', async () => {
+			await service.testSystemWebhook({ webhookId: 'dummy-webhook', type: 'userCreated' });
+
+			const calls = queueService.systemWebhookDeliver.mock.calls[0];
+			expect((calls[0] as any).id).toBe('dummy-webhook');
+			expect(calls[1]).toBe('userCreated');
+			expect((calls[2] as any).id).toBe('dummy-user-1');
+		});
+	});
+});
diff --git a/packages/backend/test/unit/activitypub.ts b/packages/backend/test/unit/activitypub.ts
index 6962608106..2fc08aec91 100644
--- a/packages/backend/test/unit/activitypub.ts
+++ b/packages/backend/test/unit/activitypub.ts
@@ -20,10 +20,10 @@ import { CoreModule } from '@/core/CoreModule.js';
 import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
 import { LoggerService } from '@/core/LoggerService.js';
 import type { IActor, IApDocument, ICollection, IObject, IPost } from '@/core/activitypub/type.js';
-import { MiMeta, MiNote } from '@/models/_.js';
+import { MiMeta, MiNote, UserProfilesRepository } from '@/models/_.js';
+import { DI } from '@/di-symbols.js';
 import { secureRndstr } from '@/misc/secure-rndstr.js';
 import { DownloadService } from '@/core/DownloadService.js';
-import { MetaService } from '@/core/MetaService.js';
 import type { MiRemoteUser } from '@/models/User.js';
 import { genAidx } from '@/misc/id/aidx.js';
 import { MockResolver } from '../misc/mock-resolver.js';
@@ -86,6 +86,7 @@ async function createRandomRemoteUser(
 }
 
 describe('ActivityPub', () => {
+	let userProfilesRepository: UserProfilesRepository;
 	let imageService: ApImageService;
 	let noteService: ApNoteService;
 	let personService: ApPersonService;
@@ -105,7 +106,14 @@ describe('ActivityPub', () => {
 		sensitiveWords: [] as string[],
 		prohibitedWords: [] as string[],
 	} as MiMeta;
-	let meta = metaInitial;
+	const meta = { ...metaInitial };
+
+	function updateMeta(newMeta: Partial<MiMeta>): void {
+		for (const key in meta) {
+			delete (meta as any)[key];
+		}
+		Object.assign(meta, newMeta);
+	}
 
 	beforeAll(async () => {
 		const app = await Test.createTestingModule({
@@ -118,15 +126,14 @@ describe('ActivityPub', () => {
 					};
 				},
 			})
-			.overrideProvider(MetaService).useValue({
-				async fetch(): Promise<MiMeta> {
-					return meta;
-				},
-			}).compile();
+			.overrideProvider(DI.meta).useFactory({ factory: () => meta })
+			.compile();
 
 		await app.init();
 		app.enableShutdownHooks();
 
+		userProfilesRepository = app.get(DI.userProfilesRepository);
+
 		noteService = app.get<ApNoteService>(ApNoteService);
 		personService = app.get<ApPersonService>(ApPersonService);
 		rendererService = app.get<ApRendererService>(ApRendererService);
@@ -205,6 +212,53 @@ describe('ActivityPub', () => {
 		});
 	});
 
+	describe('Collection visibility', () => {
+		test('Public following/followers', async () => {
+			const actor = createRandomActor();
+			actor.following = {
+				id: `${actor.id}/following`,
+				type: 'OrderedCollection',
+				totalItems: 0,
+				first: `${actor.id}/following?page=1`,
+			};
+			actor.followers = `${actor.id}/followers`;
+
+			resolver.register(actor.id, actor);
+			resolver.register(actor.followers, {
+				id: actor.followers,
+				type: 'OrderedCollection',
+				totalItems: 0,
+				first: `${actor.followers}?page=1`,
+			});
+
+			const user = await personService.createPerson(actor.id, resolver);
+			const userProfile = await userProfilesRepository.findOneByOrFail({ userId: user.id });
+
+			assert.deepStrictEqual(userProfile.followingVisibility, 'public');
+			assert.deepStrictEqual(userProfile.followersVisibility, 'public');
+		});
+
+		test('Private following/followers', async () => {
+			const actor = createRandomActor();
+			actor.following = {
+				id: `${actor.id}/following`,
+				type: 'OrderedCollection',
+				totalItems: 0,
+				// first: …
+			};
+			actor.followers = `${actor.id}/followers`;
+
+			resolver.register(actor.id, actor);
+			//resolver.register(actor.followers, { … });
+
+			const user = await personService.createPerson(actor.id, resolver);
+			const userProfile = await userProfilesRepository.findOneByOrFail({ userId: user.id });
+
+			assert.deepStrictEqual(userProfile.followingVisibility, 'private');
+			assert.deepStrictEqual(userProfile.followersVisibility, 'private');
+		});
+	});
+
 	describe('Renderer', () => {
 		test('Render an announce with visibility: followers', () => {
 			rendererService.renderAnnounce('https://example.com/notes/00example', {
@@ -316,7 +370,7 @@ describe('ActivityPub', () => {
 		});
 
 		test('cacheRemoteFiles=false disables caching', async () => {
-			meta = { ...metaInitial, cacheRemoteFiles: false };
+			updateMeta({ ...metaInitial, cacheRemoteFiles: false });
 
 			const imageObject: IApDocument = {
 				type: 'Document',
@@ -345,7 +399,7 @@ describe('ActivityPub', () => {
 		});
 
 		test('cacheRemoteSensitiveFiles=false only affects sensitive files', async () => {
-			meta = { ...metaInitial, cacheRemoteSensitiveFiles: false };
+			updateMeta({ ...metaInitial, cacheRemoteSensitiveFiles: false });
 
 			const imageObject: IApDocument = {
 				type: 'Document',
diff --git a/packages/backend/test/unit/entities/UserEntityService.ts b/packages/backend/test/unit/entities/UserEntityService.ts
index ee16d421c4..e4f42809f8 100644
--- a/packages/backend/test/unit/entities/UserEntityService.ts
+++ b/packages/backend/test/unit/entities/UserEntityService.ts
@@ -4,10 +4,10 @@
  */
 
 import { Test, TestingModule } from '@nestjs/testing';
+import type { MiUser } from '@/models/User.js';
 import { UserEntityService } from '@/core/entities/UserEntityService.js';
 import { GlobalModule } from '@/GlobalModule.js';
 import { CoreModule } from '@/core/CoreModule.js';
-import type { MiUser } from '@/models/User.js';
 import { secureRndstr } from '@/misc/secure-rndstr.js';
 import { genAidx } from '@/misc/id/aidx.js';
 import {
@@ -49,6 +49,7 @@ import { ApLoggerService } from '@/core/activitypub/ApLoggerService.js';
 import { AccountMoveService } from '@/core/AccountMoveService.js';
 import { ReactionService } from '@/core/ReactionService.js';
 import { NotificationService } from '@/core/NotificationService.js';
+import { ReactionsBufferingService } from '@/core/ReactionsBufferingService.js';
 
 process.env.NODE_ENV = 'test';
 
@@ -169,6 +170,7 @@ describe('UserEntityService', () => {
 				ApLoggerService,
 				AccountMoveService,
 				ReactionService,
+				ReactionsBufferingService,
 				NotificationService,
 			];
 
diff --git a/packages/backend/test/unit/queue/processors/CheckModeratorsActivityProcessorService.ts b/packages/backend/test/unit/queue/processors/CheckModeratorsActivityProcessorService.ts
new file mode 100644
index 0000000000..1506283a3c
--- /dev/null
+++ b/packages/backend/test/unit/queue/processors/CheckModeratorsActivityProcessorService.ts
@@ -0,0 +1,379 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { jest } from '@jest/globals';
+import { Test, TestingModule } from '@nestjs/testing';
+import * as lolex from '@sinonjs/fake-timers';
+import { addHours, addSeconds, subDays, subHours, subSeconds } from 'date-fns';
+import { CheckModeratorsActivityProcessorService } from '@/queue/processors/CheckModeratorsActivityProcessorService.js';
+import { MiSystemWebhook, MiUser, MiUserProfile, UserProfilesRepository, UsersRepository } from '@/models/_.js';
+import { IdService } from '@/core/IdService.js';
+import { RoleService } from '@/core/RoleService.js';
+import { GlobalModule } from '@/GlobalModule.js';
+import { MetaService } from '@/core/MetaService.js';
+import { DI } from '@/di-symbols.js';
+import { QueueLoggerService } from '@/queue/QueueLoggerService.js';
+import { EmailService } from '@/core/EmailService.js';
+import { SystemWebhookService } from '@/core/SystemWebhookService.js';
+import { AnnouncementService } from '@/core/AnnouncementService.js';
+
+const baseDate = new Date(Date.UTC(2000, 11, 15, 12, 0, 0));
+
+describe('CheckModeratorsActivityProcessorService', () => {
+	let app: TestingModule;
+	let clock: lolex.InstalledClock;
+	let service: CheckModeratorsActivityProcessorService;
+
+	// --------------------------------------------------------------------------------------
+
+	let usersRepository: UsersRepository;
+	let userProfilesRepository: UserProfilesRepository;
+	let idService: IdService;
+	let roleService: jest.Mocked<RoleService>;
+	let announcementService: jest.Mocked<AnnouncementService>;
+	let emailService: jest.Mocked<EmailService>;
+	let systemWebhookService: jest.Mocked<SystemWebhookService>;
+
+	let systemWebhook1: MiSystemWebhook;
+	let systemWebhook2: MiSystemWebhook;
+	let systemWebhook3: MiSystemWebhook;
+
+	// --------------------------------------------------------------------------------------
+
+	async function createUser(data: Partial<MiUser> = {}, profile: Partial<MiUserProfile> = {}): Promise<MiUser> {
+		const id = idService.gen();
+		const user = await usersRepository
+			.insert({
+				id: id,
+				username: `user_${id}`,
+				usernameLower: `user_${id}`.toLowerCase(),
+				...data,
+			})
+			.then(x => usersRepository.findOneByOrFail(x.identifiers[0]));
+
+		await userProfilesRepository.insert({
+			userId: user.id,
+			...profile,
+		});
+
+		return user;
+	}
+
+	function crateSystemWebhook(data: Partial<MiSystemWebhook> = {}): MiSystemWebhook {
+		return {
+			id: idService.gen(),
+			isActive: true,
+			updatedAt: new Date(),
+			latestSentAt: null,
+			latestStatus: null,
+			name: 'test',
+			url: 'https://example.com',
+			secret: 'test',
+			on: [],
+			...data,
+		};
+	}
+
+	function mockModeratorRole(users: MiUser[]) {
+		roleService.getModerators.mockReset();
+		roleService.getModerators.mockResolvedValue(users);
+	}
+
+	// --------------------------------------------------------------------------------------
+
+	beforeAll(async () => {
+		app = await Test
+			.createTestingModule({
+				imports: [
+					GlobalModule,
+				],
+				providers: [
+					CheckModeratorsActivityProcessorService,
+					IdService,
+					{
+						provide: RoleService, useFactory: () => ({ getModerators: jest.fn() }),
+					},
+					{
+						provide: MetaService, useFactory: () => ({ fetch: jest.fn() }),
+					},
+					{
+						provide: AnnouncementService, useFactory: () => ({ create: jest.fn() }),
+					},
+					{
+						provide: EmailService, useFactory: () => ({ sendEmail: jest.fn() }),
+					},
+					{
+						provide: SystemWebhookService, useFactory: () => ({
+							fetchActiveSystemWebhooks: jest.fn(),
+							enqueueSystemWebhook: jest.fn(),
+						}),
+					},
+					{
+						provide: QueueLoggerService, useFactory: () => ({
+							logger: ({
+								createSubLogger: () => ({
+									info: jest.fn(),
+									warn: jest.fn(),
+									succ: jest.fn(),
+								}),
+							}),
+						}),
+					},
+				],
+			})
+			.compile();
+
+		usersRepository = app.get(DI.usersRepository);
+		userProfilesRepository = app.get(DI.userProfilesRepository);
+
+		service = app.get(CheckModeratorsActivityProcessorService);
+		idService = app.get(IdService);
+		roleService = app.get(RoleService) as jest.Mocked<RoleService>;
+		announcementService = app.get(AnnouncementService) as jest.Mocked<AnnouncementService>;
+		emailService = app.get(EmailService) as jest.Mocked<EmailService>;
+		systemWebhookService = app.get(SystemWebhookService) as jest.Mocked<SystemWebhookService>;
+
+		app.enableShutdownHooks();
+	});
+
+	beforeEach(async () => {
+		clock = lolex.install({
+			now: new Date(baseDate),
+			shouldClearNativeTimers: true,
+		});
+
+		systemWebhook1 = crateSystemWebhook({ on: ['inactiveModeratorsWarning'] });
+		systemWebhook2 = crateSystemWebhook({ on: ['inactiveModeratorsWarning', 'inactiveModeratorsInvitationOnlyChanged'] });
+		systemWebhook3 = crateSystemWebhook({ on: ['abuseReport'] });
+
+		emailService.sendEmail.mockReturnValue(Promise.resolve());
+		announcementService.create.mockReturnValue(Promise.resolve({} as never));
+		systemWebhookService.fetchActiveSystemWebhooks.mockResolvedValue([systemWebhook1, systemWebhook2, systemWebhook3]);
+		systemWebhookService.enqueueSystemWebhook.mockReturnValue(Promise.resolve({} as never));
+	});
+
+	afterEach(async () => {
+		clock.uninstall();
+		await usersRepository.delete({});
+		await userProfilesRepository.delete({});
+		roleService.getModerators.mockReset();
+		announcementService.create.mockReset();
+		emailService.sendEmail.mockReset();
+		systemWebhookService.enqueueSystemWebhook.mockReset();
+	});
+
+	afterAll(async () => {
+		await app.close();
+	});
+
+	// --------------------------------------------------------------------------------------
+
+	describe('evaluateModeratorsInactiveDays', () => {
+		test('[isModeratorsInactive] inactiveなモデレーターがいても他のモデレーターがアクティブなら"運営が非アクティブ"としてみなされない', async () => {
+			const [user1, user2, user3, user4] = await Promise.all([
+				// 期限よりも1秒新しいタイミングでアクティブ化(セーフ)
+				createUser({ lastActiveDate: subDays(addSeconds(baseDate, 1), 7) }),
+				// 期限ちょうどにアクティブ化(セーフ)
+				createUser({ lastActiveDate: subDays(baseDate, 7) }),
+				// 期限よりも1秒古いタイミングでアクティブ化(アウト)
+				createUser({ lastActiveDate: subDays(subSeconds(baseDate, 1), 7) }),
+				// 対象外
+				createUser({ lastActiveDate: null }),
+			]);
+
+			mockModeratorRole([user1, user2, user3, user4]);
+
+			const result = await service.evaluateModeratorsInactiveDays();
+			expect(result.isModeratorsInactive).toBe(false);
+			expect(result.inactiveModerators).toEqual([user3]);
+		});
+
+		test('[isModeratorsInactive] 全員非アクティブなら"運営が非アクティブ"としてみなされる', async () => {
+			const [user1, user2] = await Promise.all([
+				// 期限よりも1秒古いタイミングでアクティブ化(アウト)
+				createUser({ lastActiveDate: subDays(subSeconds(baseDate, 1), 7) }),
+				// 対象外
+				createUser({ lastActiveDate: null }),
+			]);
+
+			mockModeratorRole([user1, user2]);
+
+			const result = await service.evaluateModeratorsInactiveDays();
+			expect(result.isModeratorsInactive).toBe(true);
+			expect(result.inactiveModerators).toEqual([user1]);
+		});
+
+		test('[remainingTime] 猶予まで24時間ある場合、猶予1日として計算される', async () => {
+			const [user1, user2] = await Promise.all([
+				createUser({ lastActiveDate: subDays(baseDate, 8) }),
+				// 猶予はこのユーザ基準で計算される想定。
+				// 期限まで残り24時間->猶予1日として計算されるはずである
+				createUser({ lastActiveDate: subDays(baseDate, 6) }),
+			]);
+
+			mockModeratorRole([user1, user2]);
+
+			const result = await service.evaluateModeratorsInactiveDays();
+			expect(result.isModeratorsInactive).toBe(false);
+			expect(result.inactiveModerators).toEqual([user1]);
+			expect(result.remainingTime.asDays).toBe(1);
+			expect(result.remainingTime.asHours).toBe(24);
+		});
+
+		test('[remainingTime] 猶予まで25時間ある場合、猶予1日として計算される', async () => {
+			const [user1, user2] = await Promise.all([
+				createUser({ lastActiveDate: subDays(baseDate, 8) }),
+				// 猶予はこのユーザ基準で計算される想定。
+				// 期限まで残り25時間->猶予1日として計算されるはずである
+				createUser({ lastActiveDate: subDays(addHours(baseDate, 1), 6) }),
+			]);
+
+			mockModeratorRole([user1, user2]);
+
+			const result = await service.evaluateModeratorsInactiveDays();
+			expect(result.isModeratorsInactive).toBe(false);
+			expect(result.inactiveModerators).toEqual([user1]);
+			expect(result.remainingTime.asDays).toBe(1);
+			expect(result.remainingTime.asHours).toBe(25);
+		});
+
+		test('[remainingTime] 猶予まで23時間ある場合、猶予0日として計算される', async () => {
+			const [user1, user2] = await Promise.all([
+				createUser({ lastActiveDate: subDays(baseDate, 8) }),
+				// 猶予はこのユーザ基準で計算される想定。
+				// 期限まで残り23時間->猶予0日として計算されるはずである
+				createUser({ lastActiveDate: subDays(subHours(baseDate, 1), 6) }),
+			]);
+
+			mockModeratorRole([user1, user2]);
+
+			const result = await service.evaluateModeratorsInactiveDays();
+			expect(result.isModeratorsInactive).toBe(false);
+			expect(result.inactiveModerators).toEqual([user1]);
+			expect(result.remainingTime.asDays).toBe(0);
+			expect(result.remainingTime.asHours).toBe(23);
+		});
+
+		test('[remainingTime] 期限ちょうどの場合、猶予0日として計算される', async () => {
+			const [user1, user2] = await Promise.all([
+				createUser({ lastActiveDate: subDays(baseDate, 8) }),
+				// 猶予はこのユーザ基準で計算される想定。
+				// 期限ちょうど->猶予0日として計算されるはずである
+				createUser({ lastActiveDate: subDays(baseDate, 7) }),
+			]);
+
+			mockModeratorRole([user1, user2]);
+
+			const result = await service.evaluateModeratorsInactiveDays();
+			expect(result.isModeratorsInactive).toBe(false);
+			expect(result.inactiveModerators).toEqual([user1]);
+			expect(result.remainingTime.asDays).toBe(0);
+			expect(result.remainingTime.asHours).toBe(0);
+		});
+
+		test('[remainingTime] 期限より1時間超過している場合、猶予-1日として計算される', async () => {
+			const [user1, user2] = await Promise.all([
+				createUser({ lastActiveDate: subDays(baseDate, 8) }),
+				// 猶予はこのユーザ基準で計算される想定。
+				// 期限より1時間超過->猶予-1日として計算されるはずである
+				createUser({ lastActiveDate: subDays(subHours(baseDate, 1), 7) }),
+			]);
+
+			mockModeratorRole([user1, user2]);
+
+			const result = await service.evaluateModeratorsInactiveDays();
+			expect(result.isModeratorsInactive).toBe(true);
+			expect(result.inactiveModerators).toEqual([user1, user2]);
+			expect(result.remainingTime.asDays).toBe(-1);
+			expect(result.remainingTime.asHours).toBe(-1);
+		});
+
+		test('[remainingTime] 期限より25時間超過している場合、猶予-2日として計算される', async () => {
+			const [user1, user2] = await Promise.all([
+				createUser({ lastActiveDate: subDays(baseDate, 10) }),
+				// 猶予はこのユーザ基準で計算される想定。
+				// 期限より1時間超過->猶予-1日として計算されるはずである
+				createUser({ lastActiveDate: subDays(subHours(baseDate, 25), 7) }),
+			]);
+
+			mockModeratorRole([user1, user2]);
+
+			const result = await service.evaluateModeratorsInactiveDays();
+			expect(result.isModeratorsInactive).toBe(true);
+			expect(result.inactiveModerators).toEqual([user1, user2]);
+			expect(result.remainingTime.asDays).toBe(-2);
+			expect(result.remainingTime.asHours).toBe(-25);
+		});
+	});
+
+	describe('notifyInactiveModeratorsWarning', () => {
+		test('[notification + mail] 通知はモデレータ全員に発信され、メールはメールアドレスが存在+認証済みの場合のみ', async () => {
+			const [user1, user2, user3, user4, root] = await Promise.all([
+				createUser({}, { email: 'user1@example.com', emailVerified: true }),
+				createUser({}, { email: 'user2@example.com', emailVerified: false }),
+				createUser({}, { email: null, emailVerified: false }),
+				createUser({}, { email: 'user4@example.com', emailVerified: true }),
+				createUser({ isRoot: true }, { email: 'root@example.com', emailVerified: true }),
+			]);
+
+			mockModeratorRole([user1, user2, user3, root]);
+			await service.notifyInactiveModeratorsWarning({ time: 1, asDays: 0, asHours: 0 });
+
+			expect(emailService.sendEmail).toHaveBeenCalledTimes(2);
+			expect(emailService.sendEmail.mock.calls[0][0]).toBe('user1@example.com');
+			expect(emailService.sendEmail.mock.calls[1][0]).toBe('root@example.com');
+		});
+
+		test('[systemWebhook] "inactiveModeratorsWarning"が有効なSystemWebhookに対して送信される', async () => {
+			const [user1] = await Promise.all([
+				createUser({}, { email: 'user1@example.com', emailVerified: true }),
+			]);
+
+			mockModeratorRole([user1]);
+			await service.notifyInactiveModeratorsWarning({ time: 1, asDays: 0, asHours: 0 });
+
+			expect(systemWebhookService.enqueueSystemWebhook).toHaveBeenCalledTimes(2);
+			expect(systemWebhookService.enqueueSystemWebhook.mock.calls[0][0]).toEqual(systemWebhook1);
+			expect(systemWebhookService.enqueueSystemWebhook.mock.calls[1][0]).toEqual(systemWebhook2);
+		});
+	});
+
+	describe('notifyChangeToInvitationOnly', () => {
+		test('[notification + mail] 通知はモデレータ全員に発信され、メールはメールアドレスが存在+認証済みの場合のみ', async () => {
+			const [user1, user2, user3, user4, root] = await Promise.all([
+				createUser({}, { email: 'user1@example.com', emailVerified: true }),
+				createUser({}, { email: 'user2@example.com', emailVerified: false }),
+				createUser({}, { email: null, emailVerified: false }),
+				createUser({}, { email: 'user4@example.com', emailVerified: true }),
+				createUser({ isRoot: true }, { email: 'root@example.com', emailVerified: true }),
+			]);
+
+			mockModeratorRole([user1, user2, user3, root]);
+			await service.notifyChangeToInvitationOnly();
+
+			expect(announcementService.create).toHaveBeenCalledTimes(4);
+			expect(announcementService.create.mock.calls[0][0].userId).toBe(user1.id);
+			expect(announcementService.create.mock.calls[1][0].userId).toBe(user2.id);
+			expect(announcementService.create.mock.calls[2][0].userId).toBe(user3.id);
+			expect(announcementService.create.mock.calls[3][0].userId).toBe(root.id);
+
+			expect(emailService.sendEmail).toHaveBeenCalledTimes(2);
+			expect(emailService.sendEmail.mock.calls[0][0]).toBe('user1@example.com');
+			expect(emailService.sendEmail.mock.calls[1][0]).toBe('root@example.com');
+		});
+
+		test('[systemWebhook] "inactiveModeratorsInvitationOnlyChanged"が有効なSystemWebhookに対して送信される', async () => {
+			const [user1] = await Promise.all([
+				createUser({}, { email: 'user1@example.com', emailVerified: true }),
+			]);
+
+			mockModeratorRole([user1]);
+			await service.notifyChangeToInvitationOnly();
+
+			expect(systemWebhookService.enqueueSystemWebhook).toHaveBeenCalledTimes(1);
+			expect(systemWebhookService.enqueueSystemWebhook.mock.calls[0][0]).toEqual(systemWebhook2);
+		});
+	});
+});
diff --git a/packages/backend/test/utils.ts b/packages/backend/test/utils.ts
index 86814fffe0..26de19eaf1 100644
--- a/packages/backend/test/utils.ts
+++ b/packages/backend/test/utils.ts
@@ -12,11 +12,14 @@ import WebSocket, { ClientOptions } from 'ws';
 import fetch, { File, RequestInit, type Headers } from 'node-fetch';
 import { DataSource } from 'typeorm';
 import { JSDOM } from 'jsdom';
-import { DEFAULT_POLICIES } from '@/core/RoleService.js';
-import { validateContentTypeSetAsActivityPub } from '@/core/activitypub/misc/validator.js';
+import { type Response } from 'node-fetch';
+import Fastify from 'fastify';
 import { entities } from '../src/postgres.js';
 import { loadConfig } from '../src/config.js';
 import type * as misskey from 'misskey-js';
+import { DEFAULT_POLICIES } from '@/core/RoleService.js';
+import { validateContentTypeSetAsActivityPub } from '@/core/activitypub/misc/validator.js';
+import { ApiError } from '@/server/api/error.js';
 
 export { server as startServer, jobQueue as startJobQueue } from '@/boot/common.js';
 
@@ -25,11 +28,23 @@ export interface UserToken {
 	bearer?: boolean;
 }
 
+export type SystemWebhookPayload = {
+	server: string;
+	hookId: string;
+	eventId: string;
+	createdAt: string;
+	type: string;
+	body: any;
+}
+
 const config = loadConfig();
 export const port = config.port;
 export const origin = config.url;
 export const host = new URL(config.url).host;
 
+export const WEBHOOK_HOST = 'http://localhost:15080';
+export const WEBHOOK_PORT = 15080;
+
 export const cookie = (me: UserToken): string => {
 	return `token=${me.token};`;
 };
@@ -47,27 +62,28 @@ export const successfulApiCall = async <E extends keyof misskey.Endpoints, P ext
 	const res = await api(endpoint, parameters, user);
 	const status = assertion.status ?? (res.body == null ? 204 : 200);
 	assert.strictEqual(res.status, status, inspect(res.body, { depth: 5, colors: true }));
-	return res.body;
+
+	return res.body as misskey.api.SwitchCaseResponseType<E, P>;
 };
 
-export const failedApiCall = async <T, E extends keyof misskey.Endpoints, P extends misskey.Endpoints[E]['req']>(request: ApiRequest<E, P>, assertion: {
+export const failedApiCall = async <E extends keyof misskey.Endpoints, P extends misskey.Endpoints[E]['req']>(request: ApiRequest<E, P>, assertion: {
 	status: number,
 	code: string,
 	id: string
-}): Promise<T> => {
+}): Promise<void> => {
 	const { endpoint, parameters, user } = request;
 	const { status, code, id } = assertion;
 	const res = await api(endpoint, parameters, user);
 	assert.strictEqual(res.status, status, inspect(res.body));
-	assert.strictEqual(res.body.error.code, code, inspect(res.body));
-	assert.strictEqual(res.body.error.id, id, inspect(res.body));
-	return res.body;
+	assert.ok(res.body);
+	assert.strictEqual(castAsError(res.body as any).error.code, code, inspect(res.body));
+	assert.strictEqual(castAsError(res.body as any).error.id, id, inspect(res.body));
 };
 
-export const api = async <E extends keyof misskey.Endpoints>(path: E, params: misskey.Endpoints[E]['req'], me?: UserToken): Promise<{
+export const api = async <E extends keyof misskey.Endpoints, P extends misskey.Endpoints[E]['req']>(path: E, params: P, me?: UserToken): Promise<{
 	status: number,
 	headers: Headers,
-	body: any
+	body: misskey.api.SwitchCaseResponseType<E, P>
 }> => {
 	const bodyAuth: Record<string, string> = {};
 	const headers: Record<string, string> = {
@@ -88,13 +104,14 @@ export const api = async <E extends keyof misskey.Endpoints>(path: E, params: mi
 	});
 
 	const body = res.headers.get('content-type') === 'application/json; charset=utf-8'
-		? await res.json()
+		? await res.json() as misskey.api.SwitchCaseResponseType<E, P>
 		: null;
 
 	return {
 		status: res.status,
 		headers: res.headers,
-		body,
+		// FIXME: removing this non-null assertion: requires better typing around empty response.
+		body: body!,
 	};
 };
 
@@ -140,7 +157,8 @@ export const post = async (user: UserToken, params: misskey.Endpoints['notes/cre
 
 	const res = await api('notes/create', q, user);
 
-	return res.body ? res.body.createdNote : null;
+	// FIXME: the return type should reflect this fact.
+	return (res.body ? res.body.createdNote : null)!;
 };
 
 export const createAppToken = async (user: UserToken, permissions: (typeof misskey.permissions)[number][]) => {
@@ -296,7 +314,7 @@ export const uploadFile = async (user?: UserToken, { path, name, blob }: UploadO
 	body: misskey.entities.DriveFile | null
 }> => {
 	const absPath = path == null
-		? new URL('resources/Lenna.jpg', import.meta.url)
+		? new URL('resources/192.jpg', import.meta.url)
 		: isAbsolute(path.toString())
 			? new URL(path)
 			: new URL(path, new URL('resources/', import.meta.url));
@@ -454,7 +472,7 @@ export type SimpleGetResponse = {
 	type: string | null,
 	location: string | null
 };
-export const simpleGet = async (path: string, accept = '*/*', cookie: any = undefined): Promise<SimpleGetResponse> => {
+export const simpleGet = async (path: string, accept = '*/*', cookie: any = undefined, bodyExtractor: (res: Response) => Promise<string | null> = _ => Promise.resolve(null)): Promise<SimpleGetResponse> => {
 	const res = await relativeFetch(path, {
 		headers: {
 			Accept: accept,
@@ -482,7 +500,7 @@ export const simpleGet = async (path: string, accept = '*/*', cookie: any = unde
 	const body =
 		jsonTypes.includes(res.headers.get('content-type') ?? '') ? await res.json() :
 		htmlTypes.includes(res.headers.get('content-type') ?? '') ? new JSDOM(await res.text()) :
-		null;
+		await bodyExtractor(res);
 
 	return {
 		status: res.status,
@@ -604,14 +622,6 @@ export async function initTestDb(justBorrow = false, initEntities?: any[]) {
 	return db;
 }
 
-export function sleep(msec: number) {
-	return new Promise<void>(res => {
-		setTimeout(() => {
-			res();
-		}, msec);
-	});
-}
-
 export async function sendEnvUpdateRequest(params: { key: string, value?: string }) {
 	const res = await fetch(
 		`http://localhost:${port + 1000}/env`,
@@ -642,3 +652,43 @@ export async function sendEnvResetRequest() {
 		throw new Error('server env update failed.');
 	}
 }
+
+// 与えられた値を強制的にエラーとみなす。この関数は型安全性を破壊するため、異常系のアサーション以外で用いられるべきではない。
+// FIXME(misskey-js): misskey-jsがエラー情報を公開するようになったらこの関数を廃止する
+export function castAsError(obj: Record<string, unknown>): { error: ApiError } {
+	return obj as { error: ApiError };
+}
+
+export async function captureWebhook<T = SystemWebhookPayload>(postAction: () => Promise<void>, port = WEBHOOK_PORT): Promise<T> {
+	const fastify = Fastify();
+
+	let timeoutHandle: NodeJS.Timeout | null = null;
+	const result = await new Promise<string>(async (resolve, reject) => {
+		fastify.all('/', async (req, res) => {
+			timeoutHandle && clearTimeout(timeoutHandle);
+
+			const body = JSON.stringify(req.body);
+			res.status(200).send('ok');
+			await fastify.close();
+			resolve(body);
+		});
+
+		await fastify.listen({ port });
+
+		timeoutHandle = setTimeout(async () => {
+			await fastify.close();
+			reject(new Error('timeout'));
+		}, 3000);
+
+		try {
+			await postAction();
+		} catch (e) {
+			await fastify.close();
+			reject(e);
+		}
+	});
+
+	await fastify.close();
+
+	return JSON.parse(result) as T;
+}
diff --git a/packages/frontend-embed/.gitignore b/packages/frontend-embed/.gitignore
new file mode 100644
index 0000000000..1aa0ac14e8
--- /dev/null
+++ b/packages/frontend-embed/.gitignore
@@ -0,0 +1 @@
+/storybook-static
diff --git a/packages/frontend-embed/@types/global.d.ts b/packages/frontend-embed/@types/global.d.ts
new file mode 100644
index 0000000000..1025d1bedb
--- /dev/null
+++ b/packages/frontend-embed/@types/global.d.ts
@@ -0,0 +1,23 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+type FIXME = any;
+
+declare const _LANGS_: string[][];
+declare const _VERSION_: string;
+declare const _ENV_: string;
+declare const _DEV_: boolean;
+declare const _PERF_PREFIX_: string;
+declare const _DATA_TRANSFER_DRIVE_FILE_: string;
+declare const _DATA_TRANSFER_DRIVE_FOLDER_: string;
+declare const _DATA_TRANSFER_DECK_COLUMN_: string;
+
+// for dev-mode
+declare const _LANGS_FULL_: string[][];
+
+// TagCanvas
+interface Window {
+	TagCanvas: any;
+}
diff --git a/packages/frontend-embed/@types/theme.d.ts b/packages/frontend-embed/@types/theme.d.ts
new file mode 100644
index 0000000000..6ac1037493
--- /dev/null
+++ b/packages/frontend-embed/@types/theme.d.ts
@@ -0,0 +1,12 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+declare module '@@/themes/*.json5' {
+	import { Theme } from '@/theme.js';
+
+	const theme: Theme;
+
+	export default theme;
+}
diff --git a/packages/frontend-embed/assets/dummy.png b/packages/frontend-embed/assets/dummy.png
new file mode 100644
index 0000000000..39332b0c1b
Binary files /dev/null and b/packages/frontend-embed/assets/dummy.png differ
diff --git a/packages/frontend-embed/eslint.config.js b/packages/frontend-embed/eslint.config.js
new file mode 100644
index 0000000000..dd8f03dac5
--- /dev/null
+++ b/packages/frontend-embed/eslint.config.js
@@ -0,0 +1,95 @@
+import globals from 'globals';
+import tsParser from '@typescript-eslint/parser';
+import parser from 'vue-eslint-parser';
+import pluginVue from 'eslint-plugin-vue';
+import pluginMisskey from '@misskey-dev/eslint-plugin';
+import sharedConfig from '../shared/eslint.config.js';
+
+export default [
+	...sharedConfig,
+	{
+		files: ['src/**/*.vue'],
+		...pluginMisskey.configs.typescript,
+	},
+	...pluginVue.configs['flat/recommended'],
+	{
+		files: ['src/**/*.{ts,vue}'],
+		languageOptions: {
+			globals: {
+				...Object.fromEntries(Object.entries(globals.node).map(([key]) => [key, 'off'])),
+				...globals.browser,
+
+				// Node.js
+				module: false,
+				require: false,
+				__dirname: false,
+
+				// Misskey
+				_DEV_: false,
+				_LANGS_: false,
+				_VERSION_: false,
+				_ENV_: false,
+				_PERF_PREFIX_: false,
+				_DATA_TRANSFER_DRIVE_FILE_: false,
+				_DATA_TRANSFER_DRIVE_FOLDER_: false,
+				_DATA_TRANSFER_DECK_COLUMN_: false,
+			},
+			parser,
+			parserOptions: {
+				extraFileExtensions: ['.vue'],
+				parser: tsParser,
+				project: ['./tsconfig.json'],
+				sourceType: 'module',
+				tsconfigRootDir: import.meta.dirname,
+			},
+		},
+		rules: {
+			'@typescript-eslint/no-empty-interface': ['error', {
+				allowSingleExtends: true,
+			}],
+			// window の禁止理由: グローバルスコープと衝突し、予期せぬ結果を招くため
+			// e の禁止理由: error や event など、複数のキーワードの頭文字であり分かりにくいため
+			'id-denylist': ['error', 'window', 'e'],
+			'no-shadow': ['warn'],
+			'vue/attributes-order': ['error', {
+				alphabetical: false,
+			}],
+			'vue/no-use-v-if-with-v-for': ['error', {
+				allowUsingIterationVar: false,
+			}],
+			'vue/no-ref-as-operand': 'error',
+			'vue/no-multi-spaces': ['error', {
+				ignoreProperties: false,
+			}],
+			'vue/no-v-html': 'warn',
+			'vue/order-in-components': 'error',
+			'vue/html-indent': ['warn', 'tab', {
+				attribute: 1,
+				baseIndent: 0,
+				closeBracket: 0,
+				alignAttributesVertically: true,
+				ignores: [],
+			}],
+			'vue/html-closing-bracket-spacing': ['warn', {
+				startTag: 'never',
+				endTag: 'never',
+				selfClosingTag: 'never',
+			}],
+			'vue/multi-word-component-names': 'warn',
+			'vue/require-v-for-key': 'warn',
+			'vue/no-unused-components': 'warn',
+			'vue/no-unused-vars': 'warn',
+			'vue/no-dupe-keys': 'warn',
+			'vue/valid-v-for': 'warn',
+			'vue/return-in-computed-property': 'warn',
+			'vue/no-setup-props-reactivity-loss': 'warn',
+			'vue/max-attributes-per-line': 'off',
+			'vue/html-self-closing': 'off',
+			'vue/singleline-html-element-content-newline': 'off',
+			'vue/v-on-event-hyphenation': ['error', 'never', {
+				autofix: true,
+			}],
+			'vue/attribute-hyphenation': ['error', 'never'],
+		},
+	},
+];
diff --git a/packages/frontend-embed/package.json b/packages/frontend-embed/package.json
new file mode 100644
index 0000000000..cb62191c3b
--- /dev/null
+++ b/packages/frontend-embed/package.json
@@ -0,0 +1,72 @@
+{
+	"name": "frontend-embed",
+	"private": true,
+	"type": "module",
+	"scripts": {
+		"watch": "vite",
+		"dev": "vite --config vite.config.local-dev.ts --debug hmr",
+		"build": "vite build",
+		"typecheck": "vue-tsc --noEmit",
+		"eslint": "eslint --quiet \"src/**/*.{ts,vue}\"",
+		"lint": "pnpm typecheck && pnpm eslint"
+	},
+	"dependencies": {
+		"@discordapp/twemoji": "15.1.0",
+		"@rollup/plugin-json": "6.1.0",
+		"@rollup/plugin-replace": "5.0.7",
+		"@rollup/pluginutils": "5.1.2",
+		"@tabler/icons-webfont": "3.3.0",
+		"@twemoji/parser": "15.1.1",
+		"@vitejs/plugin-vue": "5.1.4",
+		"@vue/compiler-sfc": "3.5.11",
+		"astring": "1.9.0",
+		"buraha": "0.0.1",
+		"estree-walker": "3.0.3",
+		"mfm-js": "0.24.0",
+		"misskey-js": "workspace:*",
+		"frontend-shared": "workspace:*",
+		"punycode": "2.3.1",
+		"rollup": "4.22.5",
+		"sass": "1.79.4",
+		"shiki": "1.21.0",
+		"tinycolor2": "1.6.0",
+		"tsc-alias": "1.8.10",
+		"tsconfig-paths": "4.2.0",
+		"typescript": "5.6.2",
+		"uuid": "10.0.0",
+		"json5": "2.2.3",
+		"vite": "5.4.8",
+		"vue": "3.5.11"
+	},
+	"devDependencies": {
+		"@misskey-dev/summaly": "5.1.0",
+		"@testing-library/vue": "8.1.0",
+		"@types/estree": "1.0.6",
+		"@types/micromatch": "4.0.9",
+		"@types/node": "20.14.12",
+		"@types/punycode": "2.1.4",
+		"@types/tinycolor2": "1.4.6",
+		"@types/uuid": "10.0.0",
+		"@types/ws": "8.5.12",
+		"@typescript-eslint/eslint-plugin": "7.17.0",
+		"@typescript-eslint/parser": "7.17.0",
+		"@vitest/coverage-v8": "1.6.0",
+		"@vue/runtime-core": "3.5.11",
+		"acorn": "8.12.1",
+		"cross-env": "7.0.3",
+		"eslint-plugin-import": "2.31.0",
+		"eslint-plugin-vue": "9.28.0",
+		"fast-glob": "3.3.2",
+		"happy-dom": "10.0.3",
+		"intersection-observer": "0.12.2",
+		"micromatch": "4.0.8",
+		"msw": "2.3.4",
+		"nodemon": "3.1.7",
+		"prettier": "3.3.3",
+		"start-server-and-test": "2.0.8",
+		"vite-plugin-turbosnap": "1.0.3",
+		"vue-component-type-helpers": "2.1.6",
+		"vue-eslint-parser": "9.4.3",
+		"vue-tsc": "2.1.6"
+	}
+}
diff --git a/packages/frontend-embed/src/boot.ts b/packages/frontend-embed/src/boot.ts
new file mode 100644
index 0000000000..8ab4ab32e6
--- /dev/null
+++ b/packages/frontend-embed/src/boot.ts
@@ -0,0 +1,163 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+// https://vitejs.dev/config/build-options.html#build-modulepreload
+import 'vite/modulepreload-polyfill';
+
+import '@tabler/icons-webfont/dist/tabler-icons.scss';
+
+import '@/style.scss';
+import { createApp, defineAsyncComponent } from 'vue';
+import defaultLightTheme from '@@/themes/l-light.json5';
+import defaultDarkTheme from '@@/themes/d-dark.json5';
+import { MediaProxy } from '@@/js/media-proxy.js';
+import { applyTheme, assertIsTheme } from '@/theme.js';
+import { fetchCustomEmojis } from '@/custom-emojis.js';
+import { DI } from '@/di.js';
+import { serverMetadata } from '@/server-metadata.js';
+import { url } from '@@/js/config.js';
+import { parseEmbedParams } from '@@/js/embed-page.js';
+import { postMessageToParentWindow, setIframeId } from '@/post-message.js';
+import { serverContext } from '@/server-context.js';
+import { i18n } from '@/i18n.js';
+
+import type { Theme } from '@/theme.js';
+
+console.log('Misskey Embed');
+
+//#region Embedパラメータの取得・パース
+const params = new URLSearchParams(location.search);
+const embedParams = parseEmbedParams(params);
+if (_DEV_) console.log(embedParams);
+//#endregion
+
+//#region テーマ
+function parseThemeOrNull(theme: string | null): Theme | null {
+	if (theme == null) return null;
+	try {
+		const parsed = JSON.parse(theme);
+		if (assertIsTheme(parsed)) {
+			return parsed;
+		} else {
+			return null;
+		}
+	} catch (err) {
+		return null;
+	}
+}
+
+const lightTheme = parseThemeOrNull(serverMetadata.defaultLightTheme) ?? defaultLightTheme;
+const darkTheme = parseThemeOrNull(serverMetadata.defaultDarkTheme) ?? defaultDarkTheme;
+
+if (embedParams.colorMode === 'dark') {
+	applyTheme(darkTheme);
+} else if (embedParams.colorMode === 'light') {
+	applyTheme(lightTheme);
+} else {
+	if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
+		applyTheme(darkTheme);
+	} else {
+		applyTheme(lightTheme);
+	}
+	window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (mql) => {
+		if (mql.matches) {
+			applyTheme(darkTheme);
+		} else {
+			applyTheme(lightTheme);
+		}
+	});
+}
+//#endregion
+
+// サイズの制限
+document.documentElement.style.maxWidth = '500px';
+
+// iframeIdの設定
+function setIframeIdHandler(event: MessageEvent) {
+	if (event.data?.type === 'misskey:embedParent:registerIframeId' && event.data.payload?.iframeId != null) {
+		setIframeId(event.data.payload.iframeId);
+		window.removeEventListener('message', setIframeIdHandler);
+	}
+}
+
+window.addEventListener('message', setIframeIdHandler);
+
+try {
+	await fetchCustomEmojis();
+} catch (err) { /* empty */ }
+
+const app = createApp(
+	defineAsyncComponent(() => import('@/ui.vue')),
+);
+
+app.provide(DI.mediaProxy, new MediaProxy(serverMetadata, url));
+
+app.provide(DI.serverMetadata, serverMetadata);
+
+app.provide(DI.serverContext, serverContext);
+
+app.provide(DI.embedParams, embedParams);
+
+// https://github.com/misskey-dev/misskey/pull/8575#issuecomment-1114239210
+// なぜか2回実行されることがあるため、mountするdivを1つに制限する
+const rootEl = ((): HTMLElement => {
+	const MISSKEY_MOUNT_DIV_ID = 'misskey_app';
+
+	const currentRoot = document.getElementById(MISSKEY_MOUNT_DIV_ID);
+
+	if (currentRoot) {
+		console.warn('multiple import detected');
+		return currentRoot;
+	}
+
+	const root = document.createElement('div');
+	root.id = MISSKEY_MOUNT_DIV_ID;
+	document.body.appendChild(root);
+	return root;
+})();
+
+postMessageToParentWindow('misskey:embed:ready');
+
+app.mount(rootEl);
+
+// boot.jsのやつを解除
+window.onerror = null;
+window.onunhandledrejection = null;
+
+removeSplash();
+
+//#region Self-XSS 対策メッセージ
+console.log(
+	`%c${i18n.ts._selfXssPrevention.warning}`,
+	'color: #f00; background-color: #ff0; font-size: 36px; padding: 4px;',
+);
+console.log(
+	`%c${i18n.ts._selfXssPrevention.title}`,
+	'color: #f00; font-weight: 900; font-family: "Hiragino Sans W9", "Hiragino Kaku Gothic ProN", sans-serif; font-size: 24px;',
+);
+console.log(
+	`%c${i18n.ts._selfXssPrevention.description1}`,
+	'font-size: 16px; font-weight: 700;',
+);
+console.log(
+	`%c${i18n.ts._selfXssPrevention.description2}`,
+	'font-size: 16px;',
+	'font-size: 20px; font-weight: 700; color: #f00;',
+);
+console.log(i18n.tsx._selfXssPrevention.description3({ link: 'https://misskey-hub.net/docs/for-users/resources/self-xss/' }));
+//#endregion
+
+function removeSplash() {
+	const splash = document.getElementById('splash');
+	if (splash) {
+		splash.style.opacity = '0';
+		splash.style.pointerEvents = 'none';
+
+		// transitionendイベントが発火しない場合があるため
+		window.setTimeout(() => {
+			splash.remove();
+		}, 1000);
+	}
+}
diff --git a/packages/frontend-embed/src/components/EmA.vue b/packages/frontend-embed/src/components/EmA.vue
new file mode 100644
index 0000000000..1c236b9a35
--- /dev/null
+++ b/packages/frontend-embed/src/components/EmA.vue
@@ -0,0 +1,21 @@
+<!--
+SPDX-FileCopyrightText: syuilo and misskey-project
+SPDX-License-Identifier: AGPL-3.0-only
+-->
+
+<template>
+<a :href="to" target="_blank" rel="noopener">
+	<slot></slot>
+</a>
+</template>
+
+<script lang="ts" setup>
+import { } from 'vue';
+
+const props = withDefaults(defineProps<{
+	to: string;
+	activeClass?: null | string;
+}>(), {
+	activeClass: null,
+});
+</script>
diff --git a/packages/frontend-embed/src/components/EmAcct.vue b/packages/frontend-embed/src/components/EmAcct.vue
new file mode 100644
index 0000000000..6856b8272e
--- /dev/null
+++ b/packages/frontend-embed/src/components/EmAcct.vue
@@ -0,0 +1,24 @@
+<!--
+SPDX-FileCopyrightText: syuilo and misskey-project
+SPDX-License-Identifier: AGPL-3.0-only
+-->
+
+<template>
+<span>
+	<span>@{{ user.username }}</span>
+	<span v-if="user.host || detail" style="opacity: 0.5;">@{{ user.host || host }}</span>
+</span>
+</template>
+
+<script lang="ts" setup>
+import * as Misskey from 'misskey-js';
+import { toUnicode } from 'punycode/';
+import { host as hostRaw } from '@@/js/config.js';
+
+defineProps<{
+	user: Misskey.entities.UserLite;
+	detail?: boolean;
+}>();
+
+const host = toUnicode(hostRaw);
+</script>
diff --git a/packages/frontend-embed/src/components/EmAvatar.vue b/packages/frontend-embed/src/components/EmAvatar.vue
new file mode 100644
index 0000000000..58c35c8ef0
--- /dev/null
+++ b/packages/frontend-embed/src/components/EmAvatar.vue
@@ -0,0 +1,250 @@
+<!--
+SPDX-FileCopyrightText: syuilo and misskey-project
+SPDX-License-Identifier: AGPL-3.0-only
+-->
+
+<template>
+<component :is="link ? EmA : 'span'" v-bind="bound" class="_noSelect" :class="[$style.root, { [$style.cat]: user.isCat }]">
+	<EmImgWithBlurhash :class="$style.inner" :src="url" :hash="user.avatarBlurhash" :cover="true" :onlyAvgColor="true"/>
+	<div v-if="user.isCat" :class="[$style.ears]">
+		<div :class="$style.earLeft">
+			<div v-if="false" :class="$style.layer">
+				<div :class="$style.plot" :style="{ backgroundImage: `url(${JSON.stringify(url)})` }"/>
+				<div :class="$style.plot" :style="{ backgroundImage: `url(${JSON.stringify(url)})` }"/>
+				<div :class="$style.plot" :style="{ backgroundImage: `url(${JSON.stringify(url)})` }"/>
+			</div>
+		</div>
+		<div :class="$style.earRight">
+			<div v-if="false" :class="$style.layer">
+				<div :class="$style.plot" :style="{ backgroundImage: `url(${JSON.stringify(url)})` }"/>
+				<div :class="$style.plot" :style="{ backgroundImage: `url(${JSON.stringify(url)})` }"/>
+				<div :class="$style.plot" :style="{ backgroundImage: `url(${JSON.stringify(url)})` }"/>
+			</div>
+		</div>
+	</div>
+	<img
+		v-for="decoration in user.avatarDecorations"
+		:class="[$style.decoration]"
+		:src="getDecorationUrl(decoration)"
+		:style="{
+			rotate: getDecorationAngle(decoration),
+			scale: getDecorationScale(decoration),
+			translate: getDecorationOffset(decoration),
+		}"
+		alt=""
+	>
+</component>
+</template>
+
+<script lang="ts" setup>
+import { computed } from 'vue';
+import * as Misskey from 'misskey-js';
+import EmImgWithBlurhash from './EmImgWithBlurhash.vue';
+import EmA from './EmA.vue';
+import { userPage } from '@/utils.js';
+
+const props = withDefaults(defineProps<{
+	user: Misskey.entities.User;
+	link?: boolean;
+	preview?: boolean;
+	indicator?: boolean;
+}>(), {
+	link: false,
+	preview: false,
+	indicator: false,
+});
+
+const emit = defineEmits<{
+	(ev: 'click', v: MouseEvent): void;
+}>();
+
+const bound = computed(() => props.link
+	? { to: userPage(props.user) }
+	: {});
+
+const url = computed(() => {
+	if (props.user.avatarUrl == null) return null;
+	return props.user.avatarUrl;
+});
+
+function getDecorationUrl(decoration: Omit<Misskey.entities.UserDetailed['avatarDecorations'][number], 'id'>) {
+	return decoration.url;
+}
+
+function getDecorationAngle(decoration: Omit<Misskey.entities.UserDetailed['avatarDecorations'][number], 'id'>) {
+	const angle = decoration.angle ?? 0;
+	return angle === 0 ? undefined : `${angle * 360}deg`;
+}
+
+function getDecorationScale(decoration: Omit<Misskey.entities.UserDetailed['avatarDecorations'][number], 'id'>) {
+	const scaleX = decoration.flipH ? -1 : 1;
+	return scaleX === 1 ? undefined : `${scaleX} 1`;
+}
+
+function getDecorationOffset(decoration: Omit<Misskey.entities.UserDetailed['avatarDecorations'][number], 'id'>) {
+	const offsetX = decoration.offsetX ?? 0;
+	const offsetY = decoration.offsetY ?? 0;
+	return offsetX === 0 && offsetY === 0 ? undefined : `${offsetX * 100}% ${offsetY * 100}%`;
+}
+</script>
+
+<style lang="scss" module>
+.root {
+	position: relative;
+	display: inline-block;
+	vertical-align: bottom;
+	flex-shrink: 0;
+	border-radius: 100%;
+	line-height: 16px;
+}
+
+.inner {
+	position: absolute;
+	bottom: 0;
+	left: 0;
+	right: 0;
+	top: 0;
+	border-radius: 100%;
+	z-index: 1;
+	overflow: clip;
+	object-fit: cover;
+	width: 100%;
+	height: 100%;
+}
+
+.indicator {
+	position: absolute;
+	z-index: 2;
+	bottom: 0;
+	left: 0;
+	width: 20%;
+	height: 20%;
+}
+
+.cat {
+	> .ears {
+		contain: strict;
+		position: absolute;
+		top: -50%;
+		left: -50%;
+		width: 100%;
+		height: 100%;
+		padding: 50%;
+		pointer-events: none;
+
+		> .earLeft,
+		> .earRight {
+			contain: strict;
+			display: inline-block;
+			height: 50%;
+			width: 50%;
+			background: currentColor;
+
+			&::after {
+				contain: strict;
+				content: '';
+				display: block;
+				width: 60%;
+				height: 60%;
+				margin: 20%;
+				background: #df548f;
+			}
+
+			> .layer {
+				contain: strict;
+				position: absolute;
+				top: 0;
+				width: 280%;
+				height: 280%;
+
+				> .plot {
+					contain: strict;
+					position: absolute;
+					width: 100%;
+					height: 100%;
+					clip-path: path('M0 0H1V1H0z');
+					transform: scale(32767);
+					transform-origin: 0 0;
+					opacity: 0.5;
+
+					&:first-child {
+						opacity: 1;
+					}
+
+					&:last-child {
+						opacity: calc(1 / 3);
+					}
+				}
+			}
+		}
+
+		> .earLeft {
+			transform: rotate(37.5deg) skew(30deg);
+
+			&, &::after {
+				border-radius: 25% 75% 75%;
+			}
+
+			> .layer {
+				left: 0;
+				transform:
+					skew(-30deg)
+					rotate(-37.5deg)
+					translate(-2.82842712475%, /* -2 * sqrt(2) */
+										-38.5857864376%); /* 40 - 2 * sqrt(2) */
+
+				> .plot {
+					background-position: 20% 10%; /* ~= 37.5deg */
+
+					&:first-child {
+						background-position-x: 21%;
+					}
+
+					&:last-child {
+						background-position-y: 11%;
+					}
+				}
+			}
+		}
+
+		> .earRight {
+			transform: rotate(-37.5deg) skew(-30deg);
+
+			&, &::after {
+				border-radius: 75% 25% 75% 75%;
+			}
+
+			> .layer {
+				right: 0;
+				transform:
+					skew(30deg)
+					rotate(37.5deg)
+					translate(2.82842712475%, /* 2 * sqrt(2) */
+										-38.5857864376%); /* 40 - 2 * sqrt(2) */
+
+				> .plot {
+					position: absolute;
+					background-position: 80% 10%; /* ~= 37.5deg */
+
+					&:first-child {
+						background-position-x: 79%;
+					}
+
+					&:last-child {
+						background-position-y: 11%;
+					}
+				}
+			}
+		}
+	}
+}
+
+.decoration {
+	position: absolute;
+	z-index: 1;
+	top: -50%;
+	left: -50%;
+	width: 200%;
+	pointer-events: none;
+}
+</style>
diff --git a/packages/frontend-embed/src/components/EmCustomEmoji.vue b/packages/frontend-embed/src/components/EmCustomEmoji.vue
new file mode 100644
index 0000000000..59b670cdc6
--- /dev/null
+++ b/packages/frontend-embed/src/components/EmCustomEmoji.vue
@@ -0,0 +1,99 @@
+<!--
+SPDX-FileCopyrightText: syuilo and misskey-project
+SPDX-License-Identifier: AGPL-3.0-only
+-->
+
+<template>
+<img
+	v-if="errored && fallbackToImage"
+	:class="[$style.root, { [$style.normal]: normal, [$style.noStyle]: noStyle }]"
+	src="/client-assets/dummy.png"
+	:title="alt"
+/>
+<span v-else-if="errored">:{{ customEmojiName }}:</span>
+<img
+	v-else
+	:class="[$style.root, { [$style.normal]: normal, [$style.noStyle]: noStyle }]"
+	:src="url"
+	:alt="alt"
+	:title="alt"
+	decoding="async"
+	@error="errored = true"
+	@load="errored = false"
+/>
+</template>
+
+<script lang="ts" setup>
+import { computed, inject, ref } from 'vue';
+import { customEmojisMap } from '@/custom-emojis.js';
+
+import { DI } from '@/di.js';
+
+const mediaProxy = inject(DI.mediaProxy)!;
+
+const props = defineProps<{
+	name: string;
+	normal?: boolean;
+	noStyle?: boolean;
+	host?: string | null;
+	url?: string;
+	useOriginalSize?: boolean;
+	fallbackToImage?: boolean;
+}>();
+
+const customEmojiName = computed(() => (props.name[0] === ':' ? props.name.substring(1, props.name.length - 1) : props.name).replace('@.', ''));
+const isLocal = computed(() => !props.host && (customEmojiName.value.endsWith('@.') || !customEmojiName.value.includes('@')));
+
+const rawUrl = computed(() => {
+	if (props.url) {
+		return props.url;
+	}
+	if (isLocal.value) {
+		return customEmojisMap.get(customEmojiName.value)?.url ?? null;
+	}
+	return props.host ? `/emoji/${customEmojiName.value}@${props.host}.webp` : `/emoji/${customEmojiName.value}.webp`;
+});
+
+const url = computed(() => {
+	if (rawUrl.value == null) return undefined;
+
+	const proxied =
+		(rawUrl.value.startsWith('/emoji/') || (props.useOriginalSize && isLocal.value))
+			? rawUrl.value
+			: mediaProxy.getProxiedImageUrl(
+				rawUrl.value,
+				props.useOriginalSize ? undefined : 'emoji',
+				false,
+				true,
+			);
+	return proxied;
+});
+
+const alt = computed(() => `:${customEmojiName.value}:`);
+const errored = ref(url.value == null);
+</script>
+
+<style lang="scss" module>
+.root {
+	height: 2em;
+	vertical-align: middle;
+	transition: transform 0.2s ease;
+
+	&:hover {
+		transform: scale(1.2);
+	}
+}
+
+.normal {
+	height: 1.25em;
+	vertical-align: -0.25em;
+
+	&:hover {
+		transform: none;
+	}
+}
+
+.noStyle {
+	height: auto !important;
+}
+</style>
diff --git a/packages/frontend-embed/src/components/EmEmoji.vue b/packages/frontend-embed/src/components/EmEmoji.vue
new file mode 100644
index 0000000000..224979707b
--- /dev/null
+++ b/packages/frontend-embed/src/components/EmEmoji.vue
@@ -0,0 +1,26 @@
+<!--
+SPDX-FileCopyrightText: syuilo and misskey-project
+SPDX-License-Identifier: AGPL-3.0-only
+-->
+
+<template>
+<img :class="$style.root" :src="url" :alt="props.emoji" decoding="async"/>
+</template>
+
+<script lang="ts" setup>
+import { computed } from 'vue';
+import { char2twemojiFilePath } from '@@/js/emoji-base.js';
+
+const props = defineProps<{
+	emoji: string;
+}>();
+
+const url = computed(() => char2twemojiFilePath(props.emoji));
+</script>
+
+<style lang="scss" module>
+.root {
+	height: 1.25em;
+	vertical-align: -0.25em;
+}
+</style>
diff --git a/packages/frontend-embed/src/components/EmError.vue b/packages/frontend-embed/src/components/EmError.vue
new file mode 100644
index 0000000000..d376b29a7f
--- /dev/null
+++ b/packages/frontend-embed/src/components/EmError.vue
@@ -0,0 +1,43 @@
+<!--
+SPDX-FileCopyrightText: syuilo and misskey-project
+SPDX-License-Identifier: AGPL-3.0-only
+-->
+
+<template>
+<div :class="$style.root">
+	<p :class="$style.text"><i class="ti ti-alert-triangle"></i> {{ i18n.ts.somethingHappened }}</p>
+	<button class="_buttonGray _buttonRounded" :class="$style.button" @click="() => emit('retry')">{{ i18n.ts.retry }}</button>
+</div>
+</template>
+
+<script lang="ts" setup>
+import { i18n } from '@/i18n.js';
+
+const emit = defineEmits<{
+	(ev: 'retry'): void;
+}>();
+</script>
+
+<style lang="scss" module>
+.root {
+	padding: 32px;
+	text-align: center;
+  align-items: center;
+}
+
+.text {
+	margin: 0 0 8px 0;
+}
+
+.button {
+	margin: 0 auto;
+}
+
+.img {
+	vertical-align: bottom;
+  width: 128px;
+	height: 128px;
+	margin-bottom: 16px;
+	border-radius: 16px;
+}
+</style>
diff --git a/packages/frontend-embed/src/components/EmImgWithBlurhash.vue b/packages/frontend-embed/src/components/EmImgWithBlurhash.vue
new file mode 100644
index 0000000000..bf976c71ae
--- /dev/null
+++ b/packages/frontend-embed/src/components/EmImgWithBlurhash.vue
@@ -0,0 +1,240 @@
+<!--
+SPDX-FileCopyrightText: syuilo and misskey-project
+SPDX-License-Identifier: AGPL-3.0-only
+-->
+
+<template>
+<div ref="root" :class="['chromatic-ignore', $style.root, { [$style.cover]: cover }]" :title="title ?? ''">
+	<canvas v-show="hide" key="canvas" ref="canvas" :class="$style.canvas" :width="canvasWidth" :height="canvasHeight" :title="title ?? undefined" tabindex="-1"/>
+	<img v-show="!hide" key="img" ref="img" :height="imgHeight ?? undefined" :width="imgWidth ?? undefined" :class="$style.img" :src="src ?? undefined" :title="title ?? undefined" :alt="alt ?? undefined" loading="eager" decoding="async" tabindex="-1"/>
+</div>
+</template>
+
+<script lang="ts">
+import DrawBlurhash from '@/workers/draw-blurhash?worker';
+import TestWebGL2 from '@/workers/test-webgl2?worker';
+import { WorkerMultiDispatch } from '@@/js/worker-multi-dispatch.js';
+import { extractAvgColorFromBlurhash } from '@@/js/extract-avg-color-from-blurhash.js';
+
+const canvasPromise = new Promise<WorkerMultiDispatch | HTMLCanvasElement>(resolve => {
+	// テスト環境で Web Worker インスタンスは作成できない
+	if (import.meta.env.MODE === 'test') {
+		const canvas = document.createElement('canvas');
+		canvas.width = 64;
+		canvas.height = 64;
+		resolve(canvas);
+		return;
+	}
+	const testWorker = new TestWebGL2();
+	testWorker.addEventListener('message', event => {
+		if (event.data.result) {
+			const workers = new WorkerMultiDispatch(
+				() => new DrawBlurhash(),
+				Math.min(navigator.hardwareConcurrency - 1, 4),
+			);
+			resolve(workers);
+			if (_DEV_) console.log('WebGL2 in worker is supported!');
+		} else {
+			const canvas = document.createElement('canvas');
+			canvas.width = 64;
+			canvas.height = 64;
+			resolve(canvas);
+			if (_DEV_) console.log('WebGL2 in worker is not supported...');
+		}
+		testWorker.terminate();
+	});
+});
+</script>
+
+<script lang="ts" setup>
+import { computed, nextTick, onMounted, onUnmounted, shallowRef, watch, ref } from 'vue';
+import { v4 as uuid } from 'uuid';
+import { render } from 'buraha';
+
+const props = withDefaults(defineProps<{
+	src?: string | null;
+	hash?: string | null;
+	alt?: string | null;
+	title?: string | null;
+	height?: number;
+	width?: number;
+	cover?: boolean;
+	forceBlurhash?: boolean;
+	onlyAvgColor?: boolean; // 軽量化のためにBlurhashを使わずに平均色だけを描画
+}>(), {
+	src: null,
+	alt: '',
+	title: null,
+	height: 64,
+	width: 64,
+	cover: true,
+	forceBlurhash: false,
+	onlyAvgColor: false,
+});
+
+const viewId = uuid();
+const canvas = shallowRef<HTMLCanvasElement>();
+const root = shallowRef<HTMLDivElement>();
+const img = shallowRef<HTMLImageElement>();
+const loaded = ref(false);
+const canvasWidth = ref(64);
+const canvasHeight = ref(64);
+const imgWidth = ref(props.width);
+const imgHeight = ref(props.height);
+const bitmapTmp = ref<CanvasImageSource | undefined>();
+const hide = computed(() => !loaded.value || props.forceBlurhash);
+
+function waitForDecode() {
+	if (props.src != null && props.src !== '') {
+		nextTick()
+			.then(() => img.value?.decode())
+			.then(() => {
+				loaded.value = true;
+			}, error => {
+				console.log('Error occurred during decoding image', img.value, error);
+			});
+	} else {
+		loaded.value = false;
+	}
+}
+
+watch([() => props.width, () => props.height, root], () => {
+	const ratio = props.width / props.height;
+	if (ratio > 1) {
+		canvasWidth.value = Math.round(64 * ratio);
+		canvasHeight.value = 64;
+	} else {
+		canvasWidth.value = 64;
+		canvasHeight.value = Math.round(64 / ratio);
+	}
+
+	const clientWidth = root.value?.clientWidth ?? 300;
+	imgWidth.value = clientWidth;
+	imgHeight.value = Math.round(clientWidth / ratio);
+}, {
+	immediate: true,
+});
+
+function drawImage(bitmap: CanvasImageSource) {
+	// canvasがない(mountedされていない)場合はTmpに保存しておく
+	if (!canvas.value) {
+		bitmapTmp.value = bitmap;
+		return;
+	}
+
+	// canvasがあれば描画する
+	bitmapTmp.value = undefined;
+	const ctx = canvas.value.getContext('2d');
+	if (!ctx) return;
+	ctx.drawImage(bitmap, 0, 0, canvasWidth.value, canvasHeight.value);
+}
+
+function drawAvg() {
+	if (!canvas.value) return;
+
+	const color = (props.hash != null && extractAvgColorFromBlurhash(props.hash)) || '#888';
+
+	const ctx = canvas.value.getContext('2d');
+	if (!ctx) return;
+
+	// avgColorでお茶をにごす
+	ctx.beginPath();
+	ctx.fillStyle = color;
+	ctx.fillRect(0, 0, canvasWidth.value, canvasHeight.value);
+}
+
+async function draw() {
+	if (import.meta.env.MODE === 'test' && props.hash == null) return;
+
+	drawAvg();
+
+	if (props.hash == null) return;
+
+	if (props.onlyAvgColor) return;
+
+	const work = await canvasPromise;
+	if (work instanceof WorkerMultiDispatch) {
+		work.postMessage(
+			{
+				id: viewId,
+				hash: props.hash,
+			},
+			undefined,
+		);
+	} else {
+		try {
+			render(props.hash, work);
+			drawImage(work);
+		} catch (error) {
+			console.error('Error occurred during drawing blurhash', error);
+		}
+	}
+}
+
+function workerOnMessage(event: MessageEvent) {
+	if (event.data.id !== viewId) return;
+	drawImage(event.data.bitmap as ImageBitmap);
+}
+
+canvasPromise.then(work => {
+	if (work instanceof WorkerMultiDispatch) {
+		work.addListener(workerOnMessage);
+	}
+
+	draw();
+});
+
+watch(() => props.src, () => {
+	waitForDecode();
+});
+
+watch(() => props.hash, () => {
+	draw();
+});
+
+onMounted(() => {
+	// drawImageがmountedより先に呼ばれている場合はここで描画する
+	if (bitmapTmp.value) {
+		drawImage(bitmapTmp.value);
+	}
+	waitForDecode();
+});
+
+onUnmounted(() => {
+	canvasPromise.then(work => {
+		if (work instanceof WorkerMultiDispatch) {
+			work.removeListener(workerOnMessage);
+		}
+	});
+});
+</script>
+
+<style lang="scss" module>
+.root {
+	position: relative;
+	width: 100%;
+	height: 100%;
+
+	&.cover {
+		> .canvas,
+		> .img {
+			object-fit: cover;
+		}
+	}
+}
+
+.canvas,
+.img {
+	display: block;
+	width: 100%;
+	height: 100%;
+}
+
+.canvas {
+	object-fit: contain;
+}
+
+.img {
+	object-fit: contain;
+}
+</style>
diff --git a/packages/frontend-embed/src/components/EmInstanceTicker.vue b/packages/frontend-embed/src/components/EmInstanceTicker.vue
new file mode 100644
index 0000000000..4a116e317a
--- /dev/null
+++ b/packages/frontend-embed/src/components/EmInstanceTicker.vue
@@ -0,0 +1,87 @@
+<!--
+SPDX-FileCopyrightText: syuilo and misskey-project
+SPDX-License-Identifier: AGPL-3.0-only
+-->
+
+<template>
+<div :class="$style.root" :style="bg">
+	<img v-if="faviconUrl" :class="$style.icon" :src="faviconUrl"/>
+	<div :class="$style.name">{{ instance.name }}</div>
+</div>
+</template>
+
+<script lang="ts" setup>
+import { computed, inject } from 'vue';
+
+import { DI } from '@/di.js';
+
+const serverMetadata = inject(DI.serverMetadata)!;
+const mediaProxy = inject(DI.mediaProxy)!;
+
+const props = defineProps<{
+	instance?: {
+		faviconUrl?: string | null
+		name?: string | null
+		themeColor?: string | null
+	}
+}>();
+
+// if no instance data is given, this is for the local instance
+const instance = props.instance ?? {
+	name: serverMetadata.name,
+	themeColor: (document.querySelector('meta[name="theme-color-orig"]') as HTMLMetaElement)?.content,
+};
+
+const faviconUrl = computed(() => props.instance ? mediaProxy.getProxiedImageUrlNullable(props.instance.faviconUrl, 'preview') : mediaProxy.getProxiedImageUrlNullable(serverMetadata.iconUrl, 'preview') ?? '/favicon.ico');
+
+const themeColor = props.instance?.themeColor ?? serverMetadata.themeColor ?? '#777777';
+
+const bg = {
+	background: `linear-gradient(90deg, ${themeColor}, ${themeColor}00)`,
+};
+</script>
+
+<style lang="scss" module>
+$height: 2ex;
+
+.root {
+	display: flex;
+	align-items: center;
+	height: $height;
+	border-radius: 4px 0 0 4px;
+	overflow: clip;
+	color: #fff;
+	text-shadow: /* .866 ≈ sin(60deg) */
+		1px 0 1px #000,
+		.866px .5px 1px #000,
+		.5px .866px 1px #000,
+		0 1px 1px #000,
+		-.5px .866px 1px #000,
+		-.866px .5px 1px #000,
+		-1px 0 1px #000,
+		-.866px -.5px 1px #000,
+		-.5px -.866px 1px #000,
+		0 -1px 1px #000,
+		.5px -.866px 1px #000,
+		.866px -.5px 1px #000;
+	mask-image: linear-gradient(90deg,
+		rgb(0,0,0),
+		rgb(0,0,0) calc(100% - 16px),
+		rgba(0,0,0,0) 100%
+	);
+}
+
+.icon {
+	height: $height;
+	flex-shrink: 0;
+}
+
+.name {
+	margin-left: 4px;
+	line-height: 1;
+	font-size: 0.9em;
+	font-weight: bold;
+	white-space: nowrap;
+	overflow: visible;
+}
+</style>
diff --git a/packages/frontend-embed/src/components/EmLink.vue b/packages/frontend-embed/src/components/EmLink.vue
new file mode 100644
index 0000000000..aec9b33072
--- /dev/null
+++ b/packages/frontend-embed/src/components/EmLink.vue
@@ -0,0 +1,40 @@
+<!--
+SPDX-FileCopyrightText: syuilo and misskey-project
+SPDX-License-Identifier: AGPL-3.0-only
+-->
+
+<template>
+<component
+	:is="self ? EmA : 'a'" ref="el" style="word-break: break-all;" class="_link" :[attr]="self ? url.substring(local.length) : url" :rel="rel ?? 'nofollow noopener'" :target="target"
+	:title="url"
+>
+	<slot></slot>
+	<i v-if="target === '_blank'" class="ti ti-external-link" :class="$style.icon"></i>
+</component>
+</template>
+
+<script lang="ts" setup>
+import { ref } from 'vue';
+import EmA from './EmA.vue';
+import { url as local } from '@@/js/config.js';
+
+const props = withDefaults(defineProps<{
+	url: string;
+	rel?: null | string;
+}>(), {
+});
+
+const self = props.url.startsWith(local);
+const attr = self ? 'to' : 'href';
+const target = self ? null : '_blank';
+
+const el = ref<HTMLElement | { $el: HTMLElement }>();
+
+</script>
+
+<style lang="scss" module>
+.icon {
+	padding-left: 2px;
+	font-size: .9em;
+}
+</style>
diff --git a/packages/frontend-embed/src/components/EmLoading.vue b/packages/frontend-embed/src/components/EmLoading.vue
new file mode 100644
index 0000000000..47d797606b
--- /dev/null
+++ b/packages/frontend-embed/src/components/EmLoading.vue
@@ -0,0 +1,112 @@
+<!--
+SPDX-FileCopyrightText: syuilo and misskey-project
+SPDX-License-Identifier: AGPL-3.0-only
+-->
+
+<template>
+<div :class="[$style.root, { [$style.inline]: inline, [$style.colored]: colored, [$style.mini]: mini, [$style.em]: em }]">
+	<div :class="$style.container">
+		<svg :class="[$style.spinner, $style.bg]" viewBox="0 0 168 168" xmlns="http://www.w3.org/2000/svg">
+			<g transform="matrix(1.125,0,0,1.125,12,12)">
+				<circle cx="64" cy="64" r="64" style="fill:none;stroke:currentColor;stroke-width:21.33px;"/>
+			</g>
+		</svg>
+		<svg :class="[$style.spinner, $style.fg, { [$style.static]: static }]" viewBox="0 0 168 168" xmlns="http://www.w3.org/2000/svg">
+			<g transform="matrix(1.125,0,0,1.125,12,12)">
+				<path d="M128,64C128,28.654 99.346,0 64,0C99.346,0 128,28.654 128,64Z" style="fill:none;stroke:currentColor;stroke-width:21.33px;"/>
+			</g>
+		</svg>
+	</div>
+</div>
+</template>
+
+<script lang="ts" setup>
+import { } from 'vue';
+
+const props = withDefaults(defineProps<{
+	static?: boolean;
+	inline?: boolean;
+	colored?: boolean;
+	mini?: boolean;
+	em?: boolean;
+}>(), {
+	static: false,
+	inline: false,
+	colored: true,
+	mini: false,
+	em: false,
+});
+</script>
+
+<style lang="scss" module>
+@keyframes spinner {
+	0% {
+		transform: rotate(0deg);
+	}
+	100% {
+		transform: rotate(360deg);
+	}
+}
+
+.root {
+	padding: 32px;
+	text-align: center;
+	cursor: wait;
+
+	--size: 38px;
+
+	&.colored {
+		color: var(--MI_THEME-accent);
+	}
+
+	&.inline {
+		display: inline;
+		padding: 0;
+		--size: 32px;
+	}
+
+	&.mini {
+		padding: 16px;
+		--size: 32px;
+	}
+
+	&.em {
+		display: inline-block;
+		vertical-align: middle;
+		padding: 0;
+		--size: 1em;
+	}
+}
+
+.container {
+	position: relative;
+	width: var(--size);
+	height: var(--size);
+	margin: 0 auto;
+}
+
+.spinner {
+	position: absolute;
+	top: 0;
+	left: 0;
+	width: var(--size);
+	height: var(--size);
+	fill-rule: evenodd;
+	clip-rule: evenodd;
+	stroke-linecap: round;
+	stroke-linejoin: round;
+	stroke-miterlimit: 1.5;
+}
+
+.bg {
+	opacity: 0.275;
+}
+
+.fg {
+	animation: spinner 0.5s linear infinite;
+
+	&.static {
+		animation-play-state: paused;
+	}
+}
+</style>
diff --git a/packages/frontend-embed/src/components/EmMediaBanner.vue b/packages/frontend-embed/src/components/EmMediaBanner.vue
new file mode 100644
index 0000000000..cf4a4c53b5
--- /dev/null
+++ b/packages/frontend-embed/src/components/EmMediaBanner.vue
@@ -0,0 +1,55 @@
+<!--
+SPDX-FileCopyrightText: syuilo and misskey-project
+SPDX-License-Identifier: AGPL-3.0-only
+-->
+
+<template>
+<a :href="href" target="_blank" :class="$style.root">
+	<div :class="$style.label">
+		<template v-if="media.type.startsWith('audio')"><i class="ti ti-music"></i> {{ i18n.ts.audio }}</template>
+		<template v-else><i class="ti ti-file"></i> {{ i18n.ts.file }}</template>
+	</div>
+	<div :class="$style.go">
+		<i class="ti ti-chevron-right"></i>
+	</div>
+</a>
+</template>
+
+<script setup lang="ts">
+import * as Misskey from 'misskey-js';
+import { i18n } from '@/i18n.js';
+
+defineProps<{
+	media: Misskey.entities.DriveFile;
+	href: string;
+}>();
+</script>
+
+<style lang="scss" module>
+.root {
+	box-sizing: border-box;
+	display: flex;
+	align-items: center;
+	width: 100%;
+	padding: var(--MI-margin);
+	margin-top: 4px;
+	border: 1px solid var(--MI_THEME-inputBorder);
+	border-radius: var(--MI-radius);
+	background-color: var(--MI_THEME-panel);
+	transition: background-color .1s, border-color .1s;
+
+	&:hover {
+		text-decoration: none;
+		border-color: var(--MI_THEME-inputBorderHover);
+		background-color: var(--MI_THEME-buttonHoverBg);
+	}
+}
+
+.label {
+	font-size: .9em;
+}
+
+.go {
+	margin-left: auto;
+}
+</style>
diff --git a/packages/frontend-embed/src/components/EmMediaImage.vue b/packages/frontend-embed/src/components/EmMediaImage.vue
new file mode 100644
index 0000000000..d711020a74
--- /dev/null
+++ b/packages/frontend-embed/src/components/EmMediaImage.vue
@@ -0,0 +1,162 @@
+<!--
+SPDX-FileCopyrightText: syuilo and misskey-project
+SPDX-License-Identifier: AGPL-3.0-only
+-->
+
+<template>
+<div :class="[hide ? $style.hidden : $style.visible]" @click="onclick">
+	<a
+		:title="image.name"
+		:class="$style.imageContainer"
+		:href="href ?? image.url"
+		target="_blank"
+		rel="noopener"
+	>
+		<ImgWithBlurhash
+			:hash="image.blurhash"
+			:src="hide ? null : url"
+			:forceBlurhash="hide"
+			:cover="hide || cover"
+			:alt="image.comment || image.name"
+			:title="image.comment || image.name"
+			:width="image.properties.width"
+			:height="image.properties.height"
+			:style="hide ? 'filter: brightness(0.7);' : null"
+		/>
+	</a>
+	<template v-if="hide">
+		<div :class="$style.hiddenText">
+			<div :class="$style.hiddenTextWrapper">
+				<b v-if="image.isSensitive" style="display: block;"><i class="ti ti-eye-exclamation"></i> {{ i18n.ts.sensitive }}</b>
+				<b v-else style="display: block;"><i class="ti ti-photo"></i> {{ i18n.ts.image }}</b>
+				<span style="display: block;">{{ i18n.ts.clickToShow }}</span>
+			</div>
+		</div>
+	</template>
+	<div :class="$style.indicators">
+		<div v-if="['image/gif', 'image/apng'].includes(image.type)" :class="$style.indicator">GIF</div>
+		<div v-if="image.comment" :class="$style.indicator">ALT</div>
+		<div v-if="image.isSensitive" :class="$style.indicator" style="color: var(--MI_THEME-warn);" :title="i18n.ts.sensitive"><i class="ti ti-eye-exclamation"></i></div>
+	</div>
+	<i v-if="!hide" class="ti ti-eye-off" :class="$style.hide" @click.stop="hide = true"></i>
+</div>
+</template>
+
+<script lang="ts" setup>
+import { ref, computed } from 'vue';
+import * as Misskey from 'misskey-js';
+import ImgWithBlurhash from '@/components/EmImgWithBlurhash.vue';
+import { i18n } from '@/i18n.js';
+
+const props = withDefaults(defineProps<{
+	image: Misskey.entities.DriveFile;
+	href?: string;
+	raw?: boolean;
+	cover?: boolean;
+}>(), {
+	cover: false,
+});
+
+const hide = ref(props.image.isSensitive);
+
+const url = computed(() => (props.raw)
+	? props.image.url
+	: props.image.thumbnailUrl,
+);
+
+async function onclick(ev: MouseEvent) {
+	if (hide.value) {
+		ev.stopPropagation();
+		hide.value = false;
+	}
+}
+</script>
+
+<style lang="scss" module>
+.hidden {
+	position: relative;
+}
+
+.hiddenText {
+	position: absolute;
+	left: 0;
+	top: 0;
+	width: 100%;
+	height: 100%;
+	z-index: 1;
+	display: flex;
+	justify-content: center;
+	align-items: center;
+	cursor: pointer;
+}
+
+.hide {
+	display: block;
+	position: absolute;
+	border-radius: 6px;
+	background-color: var(--MI_THEME-fg);
+	color: var(--MI_THEME-accentLighten);
+	font-size: 12px;
+	opacity: .5;
+	padding: 5px 8px;
+	text-align: center;
+	cursor: pointer;
+	top: 12px;
+	right: 12px;
+}
+
+.hiddenTextWrapper {
+	display: table-cell;
+	text-align: center;
+	font-size: 0.8em;
+	color: #fff;
+}
+
+.visible {
+	position: relative;
+	//box-shadow: 0 0 0 1px var(--MI_THEME-divider) inset;
+	background: var(--MI_THEME-bg);
+	background-size: 16px 16px;
+}
+
+html[data-color-scheme=dark] .visible {
+	--c: rgb(255 255 255 / 2%);
+	background-image: linear-gradient(45deg, var(--c) 16.67%, var(--MI_THEME-bg) 16.67%, var(--MI_THEME-bg) 50%, var(--c) 50%, var(--c) 66.67%, var(--MI_THEME-bg) 66.67%, var(--MI_THEME-bg) 100%);
+}
+
+html[data-color-scheme=light] .visible {
+	--c: rgb(0 0 0 / 2%);
+	background-image: linear-gradient(45deg, var(--c) 16.67%, var(--MI_THEME-bg) 16.67%, var(--MI_THEME-bg) 50%, var(--c) 50%, var(--c) 66.67%, var(--MI_THEME-bg) 66.67%, var(--MI_THEME-bg) 100%);
+}
+
+.imageContainer {
+	display: block;
+	overflow: hidden;
+	width: 100%;
+	height: 100%;
+	background-position: center;
+	background-size: contain;
+	background-repeat: no-repeat;
+}
+
+.indicators {
+	display: inline-flex;
+	position: absolute;
+	top: 10px;
+	left: 10px;
+	pointer-events: none;
+	opacity: .5;
+	gap: 6px;
+}
+
+.indicator {
+	/* Hardcode to black because either --MI_THEME-bg or --MI_THEME-fg makes it hard to read in dark/light mode */
+	background-color: black;
+	border-radius: 6px;
+	color: var(--MI_THEME-accentLighten);
+	display: inline-block;
+	font-weight: bold;
+	font-size: 0.8em;
+	padding: 2px 5px;
+}
+</style>
diff --git a/packages/frontend-embed/src/components/EmMediaList.vue b/packages/frontend-embed/src/components/EmMediaList.vue
new file mode 100644
index 0000000000..0b2d835abe
--- /dev/null
+++ b/packages/frontend-embed/src/components/EmMediaList.vue
@@ -0,0 +1,146 @@
+<!--
+SPDX-FileCopyrightText: syuilo and misskey-project
+SPDX-License-Identifier: AGPL-3.0-only
+-->
+
+<template>
+<div>
+	<div v-for="media in mediaList.filter(media => !previewable(media))" :key="media.id" :class="$style.banner">
+		<XBanner :media="media" :href="originalEntityUrl"/>
+	</div>
+	<div v-if="mediaList.filter(media => previewable(media)).length > 0" :class="$style.container">
+		<div
+			:class="[
+				$style.medias,
+				count === 1 ? [$style.n1] : count === 2 ? $style.n2 : count === 3 ? $style.n3 : count === 4 ? $style.n4 : $style.nMany,
+			]"
+		>
+			<div v-for="media in mediaList.filter(media => previewable(media))" :class="$style.media">
+				<XVideo v-if="media.type.startsWith('video')" :key="`video:${media.id}`" :class="$style.mediaInner" :video="media" :href="originalEntityUrl"/>
+				<XImage v-else-if="media.type.startsWith('image')" :key="`image:${media.id}`" :class="$style.mediaInner" class="image" :image="media" :raw="raw" :href="originalEntityUrl"/>
+			</div>
+		</div>
+	</div>
+</div>
+</template>
+
+<script lang="ts" setup>
+import { computed } from 'vue';
+import * as Misskey from 'misskey-js';
+import XBanner from './EmMediaBanner.vue';
+import XImage from './EmMediaImage.vue';
+import XVideo from './EmMediaVideo.vue';
+import { FILE_TYPE_BROWSERSAFE } from '@@/js/const.js';
+
+const props = defineProps<{
+	mediaList: Misskey.entities.DriveFile[];
+	raw?: boolean;
+
+	/** 埋め込みページ用 親要素の正規URL */
+	originalEntityUrl: string;
+}>();
+
+const count = computed(() => props.mediaList.filter(media => previewable(media)).length);
+
+const previewable = (file: Misskey.entities.DriveFile): boolean => {
+	if (file.type === 'image/svg+xml') return true; // svgのwebpublic/thumbnailはpngなのでtrue
+	// FILE_TYPE_BROWSERSAFEに適合しないものはブラウザで表示するのに不適切
+	return (file.type.startsWith('video') || file.type.startsWith('image')) && FILE_TYPE_BROWSERSAFE.includes(file.type);
+};
+</script>
+
+<style lang="scss" module>
+.container {
+	position: relative;
+	width: 100%;
+	margin-top: 4px;
+}
+
+.medias {
+	display: grid;
+	grid-gap: 8px;
+
+	height: 100%;
+	width: 100%;
+
+	&.n1 {
+		grid-template-rows: 1fr;
+
+		// default but fallback (expand)
+		min-height: 64px;
+		max-height: clamp(
+			64px,
+			50cqh,
+			min(360px, 50vh)
+		);
+
+		&.n116_9 {
+			min-height: initial;
+			max-height: initial;
+			aspect-ratio: 16 / 9; // fallback
+		}
+
+		&.n11_1{
+			min-height: initial;
+			max-height: initial;
+			aspect-ratio: 1 / 1; // fallback
+		}
+
+		&.n12_3 {
+			min-height: initial;
+			max-height: initial;
+			aspect-ratio: 2 / 3; // fallback
+		}
+	}
+
+	&.n2 {
+		aspect-ratio: 16/9;
+		grid-template-columns: 1fr 1fr;
+		grid-template-rows: 1fr;
+	}
+
+	&.n3 {
+		aspect-ratio: 16/9;
+		grid-template-columns: 1fr 0.5fr;
+		grid-template-rows: 1fr 1fr;
+
+		> .media:nth-child(1) {
+			grid-row: 1 / 3;
+		}
+
+		> .media:nth-child(3) {
+			grid-column: 2 / 3;
+			grid-row: 2 / 3;
+		}
+	}
+
+	&.n4 {
+		aspect-ratio: 16/9;
+		grid-template-columns: 1fr 1fr;
+		grid-template-rows: 1fr 1fr;
+	}
+
+	&.nMany {
+		grid-template-columns: 1fr 1fr;
+
+		> .media {
+			aspect-ratio: 16/9;
+		}
+	}
+}
+
+.media {
+	overflow: hidden; // clipにするとバグる
+	border-radius: 8px;
+	position: relative;
+
+	>.mediaInner {
+		width: 100%;
+		height: 100%;
+	}
+}
+
+.banner {
+	position: relative;
+}
+</style>
diff --git a/packages/frontend-embed/src/components/EmMediaVideo.vue b/packages/frontend-embed/src/components/EmMediaVideo.vue
new file mode 100644
index 0000000000..e2779bdee4
--- /dev/null
+++ b/packages/frontend-embed/src/components/EmMediaVideo.vue
@@ -0,0 +1,64 @@
+<!--
+SPDX-FileCopyrightText: syuilo and misskey-project
+SPDX-License-Identifier: AGPL-3.0-only
+-->
+
+<template>
+<a :href="href" target="_blank" :class="$style.root">
+	<img v-if="!video.isSensitive && video.thumbnailUrl" :class="$style.thumbnail" :src="video.thumbnailUrl">
+	<div :class="$style.videoOverlayPlayButton"><i class="ti ti-player-play-filled"></i></div>
+</a>
+</template>
+
+<script setup lang="ts">
+import * as Misskey from 'misskey-js';
+
+defineProps<{
+	video: Misskey.entities.DriveFile;
+	href: string;
+}>();
+</script>
+
+<style lang="scss" module>
+.root {
+	position: relative;
+	box-sizing: border-box;
+	display: flex;
+	align-items: center;
+	justify-content: center;
+	width: 100%;
+	height: auto;
+	aspect-ratio: 16 / 9;
+	padding: var(--MI-margin);
+	border: 1px solid var(--MI_THEME-divider);
+	border-radius: var(--MI-radius);
+	background-color: #000;
+
+	&:hover {
+		text-decoration: none;
+	}
+}
+
+.thumbnail {
+	position: absolute;
+	top: 0;
+	left: 0;
+	width: 100%;
+	height: 100%;
+	object-fit: cover;
+}
+
+.videoOverlayPlayButton {
+	background: var(--MI_THEME-accent);
+	color: #fff;
+	padding: 1rem;
+	border-radius: 99rem;
+
+	font-size: 1rem;
+	line-height: 1rem;
+
+	&:focus-visible {
+		outline: none;
+	}
+}
+</style>
diff --git a/packages/frontend-embed/src/components/EmMention.vue b/packages/frontend-embed/src/components/EmMention.vue
new file mode 100644
index 0000000000..a71364237d
--- /dev/null
+++ b/packages/frontend-embed/src/components/EmMention.vue
@@ -0,0 +1,46 @@
+<!--
+SPDX-FileCopyrightText: syuilo and misskey-project
+SPDX-License-Identifier: AGPL-3.0-only
+-->
+
+<template>
+<MkA :class="[$style.root]" :to="url" :style="{ background: bgCss }">
+	<span>
+		<span>@{{ username }}</span>
+		<span v-if="(host != localHost)" :class="$style.host">@{{ toUnicode(host) }}</span>
+	</span>
+</MkA>
+</template>
+
+<script lang="ts" setup>
+import { toUnicode } from 'punycode';
+import { } from 'vue';
+import tinycolor from 'tinycolor2';
+import { host as localHost } from '@@/js/config.js';
+
+const props = defineProps<{
+	username: string;
+	host: string;
+}>();
+
+const canonical = props.host === localHost ? `@${props.username}` : `@${props.username}@${toUnicode(props.host)}`;
+
+const url = `/${canonical}`;
+
+const bg = tinycolor(getComputedStyle(document.documentElement).getPropertyValue('--MI_THEME-mention'));
+bg.setAlpha(0.1);
+const bgCss = bg.toRgbString();
+</script>
+
+<style lang="scss" module>
+.root {
+	display: inline-block;
+	padding: 4px 8px 4px 4px;
+	border-radius: 999px;
+	color: var(--MI_THEME-mention);
+}
+
+.host {
+	opacity: 0.5;
+}
+</style>
diff --git a/packages/frontend-embed/src/components/EmMfm.ts b/packages/frontend-embed/src/components/EmMfm.ts
new file mode 100644
index 0000000000..cae2feb8fb
--- /dev/null
+++ b/packages/frontend-embed/src/components/EmMfm.ts
@@ -0,0 +1,454 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { VNode, h, SetupContext, provide } from 'vue';
+import * as mfm from 'mfm-js';
+import * as Misskey from 'misskey-js';
+import { host } from '@@/js/config.js';
+import EmUrl from '@/components/EmUrl.vue';
+import EmTime from '@/components/EmTime.vue';
+import EmLink from '@/components/EmLink.vue';
+import EmMention from '@/components/EmMention.vue';
+import EmEmoji from '@/components/EmEmoji.vue';
+import EmCustomEmoji from '@/components/EmCustomEmoji.vue';
+import EmA from '@/components/EmA.vue';
+
+function safeParseFloat(str: unknown): number | null {
+	if (typeof str !== 'string' || str === '') return null;
+	const num = parseFloat(str);
+	if (isNaN(num)) return null;
+	return num;
+}
+
+const QUOTE_STYLE = `
+display: block;
+margin: 8px;
+padding: 6px 0 6px 12px;
+color: var(--MI_THEME-fg);
+border-left: solid 3px var(--MI_THEME-fg);
+opacity: 0.7;
+`.split('\n').join(' ');
+
+type MfmProps = {
+	text: string;
+	plain?: boolean;
+	nowrap?: boolean;
+	author?: Misskey.entities.UserLite;
+	isNote?: boolean;
+	emojiUrls?: Record<string, string>;
+	rootScale?: number;
+	nyaize?: boolean | 'respect';
+	parsedNodes?: mfm.MfmNode[] | null;
+};
+
+type MfmEvents = {
+	clickEv(id: string): void;
+};
+
+// eslint-disable-next-line import/no-default-export
+export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEvents>['emit'] }) {
+	const isNote = props.isNote ?? true;
+	const shouldNyaize = props.nyaize ? props.nyaize === 'respect' ? props.author?.isCat : false : false;
+
+	// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
+	if (props.text == null || props.text === '') return;
+
+	const rootAst = props.parsedNodes ?? (props.plain ? mfm.parseSimple : mfm.parse)(props.text);
+
+	const validTime = (t: string | boolean | null | undefined) => {
+		if (t == null) return null;
+		if (typeof t === 'boolean') return null;
+		return t.match(/^\-?[0-9.]+s$/) ? t : null;
+	};
+
+	const validColor = (c: unknown): string | null => {
+		if (typeof c !== 'string') return null;
+		return c.match(/^[0-9a-f]{3,6}$/i) ? c : null;
+	};
+
+	const useAnim = true;
+
+	/**
+	 * Gen Vue Elements from MFM AST
+	 * @param ast MFM AST
+	 * @param scale How times large the text is
+	 * @param disableNyaize Whether nyaize is disabled or not
+	 */
+	const genEl = (ast: mfm.MfmNode[], scale: number, disableNyaize = false) => ast.map((token): VNode | string | (VNode | string)[] => {
+		switch (token.type) {
+			case 'text': {
+				let text = token.props.text.replace(/(\r\n|\n|\r)/g, '\n');
+				if (!disableNyaize && shouldNyaize) {
+					text = Misskey.nyaize(text);
+				}
+
+				if (!props.plain) {
+					const res: (VNode | string)[] = [];
+					for (const t of text.split('\n')) {
+						res.push(h('br'));
+						res.push(t);
+					}
+					res.shift();
+					return res;
+				} else {
+					return [text.replace(/\n/g, ' ')];
+				}
+			}
+
+			case 'bold': {
+				return [h('b', genEl(token.children, scale))];
+			}
+
+			case 'strike': {
+				return [h('del', genEl(token.children, scale))];
+			}
+
+			case 'italic': {
+				return h('i', {
+					style: 'font-style: oblique;',
+				}, genEl(token.children, scale));
+			}
+
+			case 'fn': {
+				// TODO: CSSを文字列で組み立てていくと token.props.args.~~~ 経由でCSSインジェクションできるのでよしなにやる
+				let style: string | undefined;
+				switch (token.props.name) {
+					case 'tada': {
+						const speed = validTime(token.props.args.speed) ?? '1s';
+						const delay = validTime(token.props.args.delay) ?? '0s';
+						style = 'font-size: 150%;' + (useAnim ? `animation: global-tada ${speed} linear infinite both; animation-delay: ${delay};` : '');
+						break;
+					}
+					case 'jelly': {
+						const speed = validTime(token.props.args.speed) ?? '1s';
+						const delay = validTime(token.props.args.delay) ?? '0s';
+						style = (useAnim ? `animation: mfm-rubberBand ${speed} linear infinite both; animation-delay: ${delay};` : '');
+						break;
+					}
+					case 'twitch': {
+						const speed = validTime(token.props.args.speed) ?? '0.5s';
+						const delay = validTime(token.props.args.delay) ?? '0s';
+						style = useAnim ? `animation: mfm-twitch ${speed} ease infinite; animation-delay: ${delay};` : '';
+						break;
+					}
+					case 'shake': {
+						const speed = validTime(token.props.args.speed) ?? '0.5s';
+						const delay = validTime(token.props.args.delay) ?? '0s';
+						style = useAnim ? `animation: mfm-shake ${speed} ease infinite; animation-delay: ${delay};` : '';
+						break;
+					}
+					case 'spin': {
+						const direction =
+							token.props.args.left ? 'reverse' :
+							token.props.args.alternate ? 'alternate' :
+							'normal';
+						const anime =
+							token.props.args.x ? 'mfm-spinX' :
+							token.props.args.y ? 'mfm-spinY' :
+							'mfm-spin';
+						const speed = validTime(token.props.args.speed) ?? '1.5s';
+						const delay = validTime(token.props.args.delay) ?? '0s';
+						style = useAnim ? `animation: ${anime} ${speed} linear infinite; animation-direction: ${direction}; animation-delay: ${delay};` : '';
+						break;
+					}
+					case 'jump': {
+						const speed = validTime(token.props.args.speed) ?? '0.75s';
+						const delay = validTime(token.props.args.delay) ?? '0s';
+						style = useAnim ? `animation: mfm-jump ${speed} linear infinite; animation-delay: ${delay};` : '';
+						break;
+					}
+					case 'bounce': {
+						const speed = validTime(token.props.args.speed) ?? '0.75s';
+						const delay = validTime(token.props.args.delay) ?? '0s';
+						style = useAnim ? `animation: mfm-bounce ${speed} linear infinite; transform-origin: center bottom; animation-delay: ${delay};` : '';
+						break;
+					}
+					case 'flip': {
+						const transform =
+							(token.props.args.h && token.props.args.v) ? 'scale(-1, -1)' :
+							token.props.args.v ? 'scaleY(-1)' :
+							'scaleX(-1)';
+						style = `transform: ${transform};`;
+						break;
+					}
+					case 'x2': {
+						return h('span', {
+							class: 'mfm-x2',
+						}, genEl(token.children, scale * 2));
+					}
+					case 'x3': {
+						return h('span', {
+							class: 'mfm-x3',
+						}, genEl(token.children, scale * 3));
+					}
+					case 'x4': {
+						return h('span', {
+							class: 'mfm-x4',
+						}, genEl(token.children, scale * 4));
+					}
+					case 'font': {
+						const family =
+							token.props.args.serif ? 'serif' :
+							token.props.args.monospace ? 'monospace' :
+							token.props.args.cursive ? 'cursive' :
+							token.props.args.fantasy ? 'fantasy' :
+							token.props.args.emoji ? 'emoji' :
+							token.props.args.math ? 'math' :
+							null;
+						if (family) style = `font-family: ${family};`;
+						break;
+					}
+					case 'blur': {
+						return h('span', {
+							class: '_mfm_blur_',
+						}, genEl(token.children, scale));
+					}
+					case 'rainbow': {
+						if (!useAnim) {
+							return h('span', {
+								class: '_mfm_rainbow_fallback_',
+							}, genEl(token.children, scale));
+						}
+						const speed = validTime(token.props.args.speed) ?? '1s';
+						const delay = validTime(token.props.args.delay) ?? '0s';
+						style = `animation: mfm-rainbow ${speed} linear infinite; animation-delay: ${delay};`;
+						break;
+					}
+					case 'sparkle': {
+						return genEl(token.children, scale);
+					}
+					case 'rotate': {
+						const degrees = safeParseFloat(token.props.args.deg) ?? 90;
+						style = `transform: rotate(${degrees}deg); transform-origin: center center;`;
+						break;
+					}
+					case 'position': {
+						const x = safeParseFloat(token.props.args.x) ?? 0;
+						const y = safeParseFloat(token.props.args.y) ?? 0;
+						style = `transform: translateX(${x}em) translateY(${y}em);`;
+						break;
+					}
+					case 'scale': {
+						const x = Math.min(safeParseFloat(token.props.args.x) ?? 1, 5);
+						const y = Math.min(safeParseFloat(token.props.args.y) ?? 1, 5);
+						style = `transform: scale(${x}, ${y});`;
+						scale = scale * Math.max(x, y);
+						break;
+					}
+					case 'fg': {
+						let color = validColor(token.props.args.color);
+						color = color ?? 'f00';
+						style = `color: #${color}; overflow-wrap: anywhere;`;
+						break;
+					}
+					case 'bg': {
+						let color = validColor(token.props.args.color);
+						color = color ?? 'f00';
+						style = `background-color: #${color}; overflow-wrap: anywhere;`;
+						break;
+					}
+					case 'border': {
+						let color = validColor(token.props.args.color);
+						color = color ? `#${color}` : 'var(--MI_THEME-accent)';
+						let b_style = token.props.args.style;
+						if (
+							typeof b_style !== 'string' ||
+							!['hidden', 'dotted', 'dashed', 'solid', 'double', 'groove', 'ridge', 'inset', 'outset']
+								.includes(b_style)
+						) b_style = 'solid';
+						const width = safeParseFloat(token.props.args.width) ?? 1;
+						const radius = safeParseFloat(token.props.args.radius) ?? 0;
+						style = `border: ${width}px ${b_style} ${color}; border-radius: ${radius}px;${token.props.args.noclip ? '' : ' overflow: clip;'}`;
+						break;
+					}
+					case 'ruby': {
+						if (token.children.length === 1) {
+							const child = token.children[0];
+							let text = child.type === 'text' ? child.props.text : '';
+							if (!disableNyaize && shouldNyaize) {
+								text = Misskey.nyaize(text);
+							}
+							return h('ruby', {}, [text.split(' ')[0], h('rt', text.split(' ')[1])]);
+						} else {
+							const rt = token.children.at(-1)!;
+							let text = rt.type === 'text' ? rt.props.text : '';
+							if (!disableNyaize && shouldNyaize) {
+								text = Misskey.nyaize(text);
+							}
+							return h('ruby', {}, [...genEl(token.children.slice(0, token.children.length - 1), scale), h('rt', text.trim())]);
+						}
+					}
+					case 'unixtime': {
+						const child = token.children[0];
+						const unixtime = parseInt(child.type === 'text' ? child.props.text : '');
+						return h('span', {
+							style: 'display: inline-block; font-size: 90%; border: solid 1px var(--MI_THEME-divider); border-radius: 999px; padding: 4px 10px 4px 6px;',
+						}, [
+							h('i', {
+								class: 'ti ti-clock',
+								style: 'margin-right: 0.25em;',
+							}),
+							h(EmTime, {
+								key: Math.random(),
+								time: unixtime * 1000,
+								mode: 'detail',
+							}),
+						]);
+					}
+					case 'clickable': {
+						return h('span', { onClick(ev: MouseEvent): void {
+							ev.stopPropagation();
+							ev.preventDefault();
+							const clickEv = typeof token.props.args.ev === 'string' ? token.props.args.ev : '';
+							emit('clickEv', clickEv);
+						} }, genEl(token.children, scale));
+					}
+				}
+				if (style === undefined) {
+					return h('span', {}, ['$[', token.props.name, ' ', ...genEl(token.children, scale), ']']);
+				} else {
+					return h('span', {
+						style: 'display: inline-block; ' + style,
+					}, genEl(token.children, scale));
+				}
+			}
+
+			case 'small': {
+				return [h('small', {
+					style: 'opacity: 0.7;',
+				}, genEl(token.children, scale))];
+			}
+
+			case 'center': {
+				return [h('div', {
+					style: 'text-align:center;',
+				}, genEl(token.children, scale))];
+			}
+
+			case 'url': {
+				return [h(EmUrl, {
+					key: Math.random(),
+					url: token.props.url,
+					rel: 'nofollow noopener',
+				})];
+			}
+
+			case 'link': {
+				return [h(EmLink, {
+					key: Math.random(),
+					url: token.props.url,
+					rel: 'nofollow noopener',
+				}, genEl(token.children, scale, true))];
+			}
+
+			case 'mention': {
+				return [h(EmMention, {
+					key: Math.random(),
+					host: (token.props.host == null && props.author && props.author.host != null ? props.author.host : token.props.host) ?? host,
+					username: token.props.username,
+				})];
+			}
+
+			case 'hashtag': {
+				return [h(EmA, {
+					key: Math.random(),
+					to: isNote ? `/tags/${encodeURIComponent(token.props.hashtag)}` : `/user-tags/${encodeURIComponent(token.props.hashtag)}`,
+					style: 'color:var(--MI_THEME-hashtag);',
+				}, `#${token.props.hashtag}`)];
+			}
+
+			case 'blockCode': {
+				return [h('code', {
+					key: Math.random(),
+					lang: token.props.lang ?? undefined,
+				}, token.props.code)];
+			}
+
+			case 'inlineCode': {
+				return [h('code', {
+					key: Math.random(),
+				}, token.props.code)];
+			}
+
+			case 'quote': {
+				if (!props.nowrap) {
+					return [h('div', {
+						style: QUOTE_STYLE,
+					}, genEl(token.children, scale, true))];
+				} else {
+					return [h('span', {
+						style: QUOTE_STYLE,
+					}, genEl(token.children, scale, true))];
+				}
+			}
+
+			case 'emojiCode': {
+				if (props.author?.host == null) {
+					return [h(EmCustomEmoji, {
+						key: Math.random(),
+						name: token.props.name,
+						normal: props.plain,
+						host: null,
+						useOriginalSize: scale >= 2.5,
+						fallbackToImage: false,
+					})];
+				} else {
+					// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
+					if (props.emojiUrls && (props.emojiUrls[token.props.name] == null)) {
+						return [h('span', `:${token.props.name}:`)];
+					} else {
+						return [h(EmCustomEmoji, {
+							key: Math.random(),
+							name: token.props.name,
+							url: props.emojiUrls && props.emojiUrls[token.props.name],
+							normal: props.plain,
+							host: props.author.host,
+							useOriginalSize: scale >= 2.5,
+						})];
+					}
+				}
+			}
+
+			case 'unicodeEmoji': {
+				return [h(EmEmoji, {
+					key: Math.random(),
+					emoji: token.props.emoji,
+					menu: props.enableEmojiMenu,
+					menuReaction: props.enableEmojiMenuReaction,
+				})];
+			}
+
+			case 'mathInline': {
+				return [h('code', token.props.formula)];
+			}
+
+			case 'mathBlock': {
+				return [h('code', token.props.formula)];
+			}
+
+			case 'search': {
+				return [h('div', {
+					key: Math.random(),
+				}, token.props.query)];
+			}
+
+			case 'plain': {
+				return [h('span', genEl(token.children, scale, true))];
+			}
+
+			default: {
+				// eslint-disable-next-line @typescript-eslint/no-explicit-any
+				console.error('unrecognized ast type:', (token as any).type);
+
+				return [];
+			}
+		}
+	}).flat(Infinity) as (VNode | string)[];
+
+	return h('span', {
+		// https://codeday.me/jp/qa/20190424/690106.html
+		style: props.nowrap ? 'white-space: pre; word-wrap: normal; overflow: hidden; text-overflow: ellipsis;' : 'white-space: pre-wrap;',
+	}, genEl(rootAst, props.rootScale ?? 1));
+}
diff --git a/packages/frontend-embed/src/components/EmNote.vue b/packages/frontend-embed/src/components/EmNote.vue
new file mode 100644
index 0000000000..f5b064c293
--- /dev/null
+++ b/packages/frontend-embed/src/components/EmNote.vue
@@ -0,0 +1,605 @@
+<!--
+SPDX-FileCopyrightText: syuilo and misskey-project
+SPDX-License-Identifier: AGPL-3.0-only
+-->
+
+<template>
+<div
+	v-show="!isDeleted"
+	ref="rootEl"
+	:class="[$style.root]"
+	:tabindex="isDeleted ? '-1' : '0'"
+>
+	<EmNoteSub v-if="appearNote.reply" :note="appearNote.reply" :class="$style.replyTo"/>
+	<div v-if="pinned" :class="$style.tip"><i class="ti ti-pin"></i> {{ i18n.ts.pinnedNote }}</div>
+	<!--<div v-if="appearNote._prId_" class="tip"><i class="ti ti-speakerphone"></i> {{ i18n.ts.promotion }}<button class="_textButton hide" @click="readPromo()">{{ i18n.ts.hideThisNote }} <i class="ti ti-x"></i></button></div>-->
+	<!--<div v-if="appearNote._featuredId_" class="tip"><i class="ti ti-bolt"></i> {{ i18n.ts.featured }}</div>-->
+	<div v-if="isRenote" :class="$style.renote">
+		<div v-if="note.channel" :class="$style.colorBar" :style="{ background: note.channel.color }"></div>
+		<EmAvatar :class="$style.renoteAvatar" :user="note.user" link/>
+		<i class="ti ti-repeat" style="margin-right: 4px;"></i>
+		<I18n :src="i18n.ts.renotedBy" tag="span" :class="$style.renoteText">
+			<template #user>
+				<EmA :class="$style.renoteUserName" :to="userPage(note.user)">
+					<EmUserName :user="note.user"/>
+				</EmA>
+			</template>
+		</I18n>
+		<div :class="$style.renoteInfo">
+			<button ref="renoteTime" :class="$style.renoteTime" class="_button">
+				<i class="ti ti-dots" :class="$style.renoteMenu"></i>
+				<EmTime :time="note.createdAt"/>
+			</button>
+			<span v-if="note.visibility !== 'public'" style="margin-left: 0.5em;" :title="i18n.ts._visibility[note.visibility]">
+				<i v-if="note.visibility === 'home'" class="ti ti-home"></i>
+				<i v-else-if="note.visibility === 'followers'" class="ti ti-lock"></i>
+				<i v-else-if="note.visibility === 'specified'" ref="specified" class="ti ti-mail"></i>
+			</span>
+			<span v-if="note.localOnly" style="margin-left: 0.5em;" :title="i18n.ts._visibility['disableFederation']"><i class="ti ti-rocket-off"></i></span>
+			<span v-if="note.channel" style="margin-left: 0.5em;" :title="note.channel.name"><i class="ti ti-device-tv"></i></span>
+		</div>
+	</div>
+	<article :class="$style.article">
+		<div v-if="appearNote.channel" :class="$style.colorBar" :style="{ background: appearNote.channel.color }"></div>
+		<EmAvatar :class="$style.avatar" :user="appearNote.user" link/>
+		<div :class="$style.main">
+			<EmNoteHeader :note="appearNote" :mini="true"/>
+			<EmInstanceTicker v-if="appearNote.user.instance != null" :instance="appearNote.user.instance"/>
+			<div style="container-type: inline-size;">
+				<p v-if="appearNote.cw != null" :class="$style.cw">
+					<EmMfm v-if="appearNote.cw != ''" style="margin-right: 8px;" :text="appearNote.cw" :author="appearNote.user" :nyaize="'respect'"/>
+					<button style="display: block; width: 100%; margin: 4px 0;" class="_buttonGray _buttonRounded" @click="showContent = !showContent">{{ showContent ? i18n.ts._cw.hide : i18n.ts._cw.show }}</button>
+				</p>
+				<div v-show="appearNote.cw == null || showContent" :class="[{ [$style.contentCollapsed]: collapsed }]">
+					<div :class="$style.text">
+						<span v-if="appearNote.isHidden" style="opacity: 0.5">({{ i18n.ts.private }})</span>
+						<EmA v-if="appearNote.replyId" :class="$style.replyIcon" :to="`/notes/${appearNote.replyId}`"><i class="ti ti-arrow-back-up"></i></EmA>
+						<EmMfm
+							v-if="appearNote.text"
+							:parsedNodes="parsed"
+							:text="appearNote.text"
+							:author="appearNote.user"
+							:nyaize="'respect'"
+							:emojiUrls="appearNote.emojis"
+							:enableEmojiMenu="!true"
+							:enableEmojiMenuReaction="true"
+						/>
+					</div>
+					<div v-if="appearNote.files && appearNote.files.length > 0">
+						<EmMediaList :mediaList="appearNote.files" :originalEntityUrl="`${url}/notes/${appearNote.id}`"/>
+					</div>
+					<EmPoll v-if="appearNote.poll" :noteId="appearNote.id" :poll="appearNote.poll" :readOnly="true" :class="$style.poll"/>
+					<div v-if="appearNote.renote" :class="$style.quote"><EmNoteSimple :note="appearNote.renote" :class="$style.quoteNote"/></div>
+					<button v-if="isLong && collapsed" :class="$style.collapsed" class="_button" @click="collapsed = false">
+						<span :class="$style.collapsedLabel">{{ i18n.ts.showMore }}</span>
+					</button>
+					<button v-else-if="isLong && !collapsed" :class="$style.showLess" class="_button" @click="collapsed = true">
+						<span :class="$style.showLessLabel">{{ i18n.ts.showLess }}</span>
+					</button>
+				</div>
+				<EmA v-if="appearNote.channel && !inChannel" :class="$style.channel" :to="`/channels/${appearNote.channel.id}`"><i class="ti ti-device-tv"></i> {{ appearNote.channel.name }}</EmA>
+			</div>
+			<EmReactionsViewer v-if="appearNote.reactionAcceptance !== 'likeOnly'" :note="appearNote" :maxNumber="16">
+				<template #more>
+					<EmA :to="`/notes/${appearNote.id}/reactions`" :class="[$style.reactionOmitted]">{{ i18n.ts.more }}</EmA>
+				</template>
+			</EmReactionsViewer>
+			<footer :class="$style.footer">
+				<a :href="`/notes/${appearNote.id}`" target="_blank" rel="noopener" :class="[$style.footerButton, $style.footerButtonLink]" class="_button">
+					<i class="ti ti-arrow-back-up"></i>
+				</a>
+				<a :href="`/notes/${appearNote.id}`" target="_blank" rel="noopener" :class="[$style.footerButton, $style.footerButtonLink]" class="_button">
+					<i class="ti ti-repeat"></i>
+				</a>
+				<a :href="`/notes/${appearNote.id}`" target="_blank" rel="noopener" :class="[$style.footerButton, $style.footerButtonLink]" class="_button">
+					<i v-if="appearNote.reactionAcceptance === 'likeOnly'" class="ti ti-heart"></i>
+					<i v-else class="ti ti-plus"></i>
+				</a>
+				<a :href="`/notes/${appearNote.id}`" target="_blank" rel="noopener" :class="[$style.footerButton, $style.footerButtonLink]" class="_button">
+					<i class="ti ti-dots"></i>
+				</a>
+			</footer>
+		</div>
+	</article>
+</div>
+</template>
+
+<script lang="ts" setup>
+import { computed, inject, ref, shallowRef } from 'vue';
+import * as mfm from 'mfm-js';
+import * as Misskey from 'misskey-js';
+import { shouldCollapsed } from '@@/js/collapsed.js';
+import { url } from '@@/js/config.js';
+import I18n from '@/components/I18n.vue';
+import EmNoteSub from '@/components/EmNoteSub.vue';
+import EmNoteHeader from '@/components/EmNoteHeader.vue';
+import EmNoteSimple from '@/components/EmNoteSimple.vue';
+import EmInstanceTicker from '@/components/EmInstanceTicker.vue';
+import EmReactionsViewer from '@/components/EmReactionsViewer.vue';
+import EmMediaList from '@/components/EmMediaList.vue';
+import EmPoll from '@/components/EmPoll.vue';
+import EmMfm from '@/components/EmMfm.js';
+import EmA from '@/components/EmA.vue';
+import EmAvatar from '@/components/EmAvatar.vue';
+import EmUserName from '@/components/EmUserName.vue';
+import EmTime from '@/components/EmTime.vue';
+import { userPage } from '@/utils.js';
+import { i18n } from '@/i18n.js';
+
+function getAppearNote(note: Misskey.entities.Note) {
+	return Misskey.note.isPureRenote(note) ? note.renote : note;
+}
+
+const props = withDefaults(defineProps<{
+	note: Misskey.entities.Note;
+	pinned?: boolean;
+}>(), {
+});
+
+const emit = defineEmits<{
+	(ev: 'reaction', emoji: string): void;
+	(ev: 'removeReaction', emoji: string): void;
+}>();
+
+const inChannel = inject('inChannel', null);
+
+const note = ref((props.note));
+
+const isRenote = Misskey.note.isPureRenote(note.value);
+
+const rootEl = shallowRef<HTMLElement>();
+const renoteTime = shallowRef<HTMLElement>();
+const appearNote = computed(() => getAppearNote(note.value));
+const showContent = ref(false);
+const parsed = computed(() => appearNote.value.text ? mfm.parse(appearNote.value.text) : null);
+const isLong = shouldCollapsed(appearNote.value, []);
+const collapsed = ref(appearNote.value.cw == null && isLong);
+const isDeleted = ref(false);
+</script>
+
+<style lang="scss" module>
+.root {
+	position: relative;
+	transition: box-shadow 0.1s ease;
+	font-size: 1.05em;
+	overflow: clip;
+	contain: content;
+	content-visibility: auto;
+  contain-intrinsic-size: 0 150px;
+
+	&:focus-visible {
+		outline: none;
+
+		&::after {
+			content: "";
+			pointer-events: none;
+			display: block;
+			position: absolute;
+			z-index: 10;
+			top: 0;
+			left: 0;
+			right: 0;
+			bottom: 0;
+			margin: auto;
+			width: calc(100% - 8px);
+			height: calc(100% - 8px);
+			border: dashed 2px var(--MI_THEME-focus);
+			border-radius: var(--MI-radius);
+			box-sizing: border-box;
+		}
+	}
+
+	.footer {
+		position: relative;
+		z-index: 1;
+	}
+
+	&:hover > .article > .main > .footer > .footerButton {
+		opacity: 1;
+	}
+
+	&.showActionsOnlyHover {
+		.footer {
+			visibility: hidden;
+			position: absolute;
+			top: 12px;
+			right: 12px;
+			padding: 0 4px;
+			margin-bottom: 0 !important;
+			background: var(--MI_THEME-popup);
+			border-radius: 8px;
+			box-shadow: 0px 4px 32px var(--MI_THEME-shadow);
+		}
+
+		.footerButton {
+			font-size: 90%;
+
+			&:not(:last-child) {
+				margin-right: 0;
+			}
+		}
+	}
+
+	&.showActionsOnlyHover:hover {
+		.footer {
+			visibility: visible;
+		}
+	}
+}
+
+.tip {
+	display: flex;
+	align-items: center;
+	padding: 16px 32px 8px 32px;
+	line-height: 24px;
+	font-size: 90%;
+	white-space: pre;
+	color: #d28a3f;
+}
+
+.tip + .article {
+	padding-top: 8px;
+}
+
+.replyTo {
+	opacity: 0.7;
+	padding-bottom: 0;
+}
+
+.renote {
+	position: relative;
+	display: flex;
+	align-items: center;
+	padding: 16px 32px 8px 32px;
+	line-height: 28px;
+	white-space: pre;
+	color: var(--MI_THEME-renote);
+
+	& + .article {
+		padding-top: 8px;
+	}
+
+	> .colorBar {
+		height: calc(100% - 6px);
+	}
+}
+
+.renoteAvatar {
+	flex-shrink: 0;
+	display: inline-block;
+	width: 28px;
+	height: 28px;
+	margin: 0 8px 0 0;
+}
+
+.renoteText {
+	overflow: hidden;
+	flex-shrink: 1;
+	text-overflow: ellipsis;
+	white-space: nowrap;
+}
+
+.renoteUserName {
+	font-weight: bold;
+}
+
+.renoteInfo {
+	margin-left: auto;
+	font-size: 0.9em;
+}
+
+.renoteTime {
+	flex-shrink: 0;
+	color: inherit;
+}
+
+.renoteMenu {
+	margin-right: 4px;
+}
+
+.collapsedRenoteTarget {
+	display: flex;
+	align-items: center;
+	line-height: 28px;
+	white-space: pre;
+	padding: 0 32px 18px;
+}
+
+.collapsedRenoteTargetAvatar {
+	flex-shrink: 0;
+	display: inline-block;
+	width: 28px;
+	height: 28px;
+	margin: 0 8px 0 0;
+}
+
+.collapsedRenoteTargetText {
+	overflow: hidden;
+	flex-shrink: 1;
+	text-overflow: ellipsis;
+	white-space: nowrap;
+	font-size: 90%;
+	opacity: 0.7;
+	cursor: pointer;
+
+	&:hover {
+		text-decoration: underline;
+	}
+}
+
+.article {
+	position: relative;
+	display: flex;
+	padding: 28px 32px;
+}
+
+.colorBar {
+	position: absolute;
+	top: 8px;
+	left: 8px;
+	width: 5px;
+	height: calc(100% - 16px);
+	border-radius: 999px;
+	pointer-events: none;
+}
+
+.avatar {
+	flex-shrink: 0;
+	display: block !important;
+	margin: 0 14px 0 0;
+	width: 58px;
+	height: 58px;
+	position: sticky !important;
+	top: calc(22px + var(--MI-stickyTop, 0px));
+	left: 0;
+}
+
+.main {
+	flex: 1;
+	min-width: 0;
+}
+
+.cw {
+	cursor: default;
+	display: block;
+	margin: 0;
+	padding: 0;
+	overflow-wrap: break-word;
+}
+
+.showLess {
+	width: 100%;
+	margin-top: 14px;
+	position: sticky;
+	bottom: calc(var(--MI-stickyBottom, 0px) + 14px);
+}
+
+.showLessLabel {
+	display: inline-block;
+	background: var(--MI_THEME-popup);
+	padding: 6px 10px;
+	font-size: 0.8em;
+	border-radius: 999px;
+	box-shadow: 0 2px 6px rgb(0 0 0 / 20%);
+}
+
+.contentCollapsed {
+	position: relative;
+	max-height: 9em;
+	overflow: clip;
+}
+
+.collapsed {
+	display: block;
+	position: absolute;
+	bottom: 0;
+	left: 0;
+	z-index: 2;
+	width: 100%;
+	height: 64px;
+	background: linear-gradient(0deg, var(--MI_THEME-panel), color(from var(--MI_THEME-panel) srgb r g b / 0));
+
+	&:hover > .collapsedLabel {
+		background: var(--MI_THEME-panelHighlight);
+	}
+}
+
+.collapsedLabel {
+	display: inline-block;
+	background: var(--MI_THEME-panel);
+	padding: 6px 10px;
+	font-size: 0.8em;
+	border-radius: 999px;
+	box-shadow: 0 2px 6px rgb(0 0 0 / 20%);
+}
+
+.text {
+	overflow-wrap: break-word;
+}
+
+.replyIcon {
+	color: var(--MI_THEME-accent);
+	margin-right: 0.5em;
+}
+
+.translation {
+	border: solid 0.5px var(--MI_THEME-divider);
+	border-radius: var(--MI-radius);
+	padding: 12px;
+	margin-top: 8px;
+}
+
+.urlPreview {
+	margin-top: 8px;
+}
+
+.poll {
+	font-size: 80%;
+}
+
+.quote {
+	padding: 8px 0;
+}
+
+.quoteNote {
+	padding: 16px;
+	border: dashed 1px var(--MI_THEME-renote);
+	border-radius: 8px;
+	overflow: clip;
+}
+
+.channel {
+	opacity: 0.7;
+	font-size: 80%;
+}
+
+.footer {
+	margin-bottom: -14px;
+}
+
+.footerButton {
+	margin: 0;
+	padding: 8px;
+	opacity: 0.7;
+
+	&:not(:last-child) {
+		margin-right: 28px;
+	}
+
+	&:hover {
+		color: var(--MI_THEME-fgHighlighted);
+	}
+}
+
+.footerButtonLink:hover,
+.footerButtonLink:focus,
+.footerButtonLink:active {
+	text-decoration: none;
+}
+
+.footerButtonCount {
+	display: inline;
+	margin: 0 0 0 8px;
+	opacity: 0.7;
+}
+
+@container (max-width: 580px) {
+	.root {
+		font-size: 0.95em;
+	}
+
+	.renote {
+		padding: 12px 26px 0 26px;
+	}
+
+	.article {
+		padding: 24px 26px;
+	}
+
+	.avatar {
+		width: 50px;
+		height: 50px;
+	}
+}
+
+@container (max-width: 500px) {
+	.root {
+		font-size: 0.9em;
+	}
+
+	.renote {
+		padding: 10px 22px 0 22px;
+	}
+
+	.article {
+		padding: 20px 22px;
+	}
+
+	.footer {
+		margin-bottom: -8px;
+	}
+}
+
+@container (max-width: 480px) {
+	.renote {
+		padding: 8px 16px 0 16px;
+	}
+
+	.tip {
+		padding: 8px 16px 0 16px;
+	}
+
+	.collapsedRenoteTarget {
+		padding: 0 16px 9px;
+		margin-top: 4px;
+	}
+
+	.article {
+		padding: 14px 16px;
+	}
+}
+
+@container (max-width: 450px) {
+	.avatar {
+		margin: 0 10px 0 0;
+		width: 46px;
+		height: 46px;
+		top: calc(14px + var(--MI-stickyTop, 0px));
+	}
+}
+
+@container (max-width: 400px) {
+	.root:not(.showActionsOnlyHover) {
+		.footerButton {
+			&:not(:last-child) {
+				margin-right: 18px;
+			}
+		}
+	}
+}
+
+@container (max-width: 350px) {
+	.root:not(.showActionsOnlyHover) {
+		.footerButton {
+			&:not(:last-child) {
+				margin-right: 12px;
+			}
+		}
+	}
+
+	.colorBar {
+		top: 6px;
+		left: 6px;
+		width: 4px;
+		height: calc(100% - 12px);
+	}
+}
+
+@container (max-width: 300px) {
+	.avatar {
+		width: 44px;
+		height: 44px;
+	}
+
+	.root:not(.showActionsOnlyHover) {
+		.footerButton {
+			&:not(:last-child) {
+				margin-right: 8px;
+			}
+		}
+	}
+}
+
+@container (max-width: 250px) {
+	.quoteNote {
+		padding: 12px;
+	}
+}
+
+.reactionOmitted {
+	display: inline-block;
+	margin-left: 8px;
+	opacity: .8;
+	font-size: 95%;
+}
+</style>
diff --git a/packages/frontend-embed/src/components/EmNoteDetailed.vue b/packages/frontend-embed/src/components/EmNoteDetailed.vue
new file mode 100644
index 0000000000..b39b47c065
--- /dev/null
+++ b/packages/frontend-embed/src/components/EmNoteDetailed.vue
@@ -0,0 +1,490 @@
+<!--
+SPDX-FileCopyrightText: syuilo and misskey-project
+SPDX-License-Identifier: AGPL-3.0-only
+-->
+
+<template>
+<div
+	v-show="!isDeleted"
+	ref="rootEl"
+	:class="$style.root"
+>
+	<EmNoteSub v-if="appearNote.reply" :note="appearNote.reply" :class="$style.replyTo"/>
+	<div v-if="isRenote" :class="$style.renote">
+		<EmAvatar :class="$style.renoteAvatar" :user="note.user" link/>
+		<i class="ti ti-repeat" style="margin-right: 4px;"></i>
+		<span :class="$style.renoteText">
+			<I18n :src="i18n.ts.renotedBy" tag="span">
+				<template #user>
+					<EmA :class="$style.renoteName" :to="userPage(note.user)">
+						<EmUserName :user="note.user"/>
+					</EmA>
+				</template>
+			</I18n>
+		</span>
+		<div :class="$style.renoteInfo">
+			<div class="$style.renoteTime">
+				<EmTime :time="note.createdAt"/>
+			</div>
+			<span v-if="note.visibility !== 'public'" style="margin-left: 0.5em;" :title="i18n.ts._visibility[note.visibility]">
+				<i v-if="note.visibility === 'home'" class="ti ti-home"></i>
+				<i v-else-if="note.visibility === 'followers'" class="ti ti-lock"></i>
+				<i v-else-if="note.visibility === 'specified'" ref="specified" class="ti ti-mail"></i>
+			</span>
+			<span v-if="note.localOnly" style="margin-left: 0.5em;" :title="i18n.ts._visibility['disableFederation']"><i class="ti ti-rocket-off"></i></span>
+		</div>
+	</div>
+	<article :class="$style.note">
+		<header :class="$style.noteHeader">
+			<EmAvatar :class="$style.noteHeaderAvatar" :user="appearNote.user" indicator link/>
+			<div :class="$style.noteHeaderBody">
+				<div :class="$style.noteHeaderBodyUpper">
+					<div style="min-width: 0;">
+						<div class="_nowrap">
+							<EmA :class="$style.noteHeaderName" :to="userPage(appearNote.user)">
+								<EmUserName :nowrap="true" :user="appearNote.user"/>
+							</EmA>
+							<span v-if="appearNote.user.isBot" :class="$style.isBot">bot</span>
+						</div>
+						<div :class="$style.noteHeaderUsername"><EmAcct :user="appearNote.user"/></div>
+					</div>
+					<div :class="$style.noteHeaderInfo">
+						<a :href="url" :class="$style.noteHeaderInstanceIconLink" target="_blank" rel="noopener noreferrer">
+							<img :src="serverMetadata.iconUrl || '/favicon.ico'" alt="" :class="$style.noteHeaderInstanceIcon"/>
+						</a>
+					</div>
+				</div>
+				<EmInstanceTicker v-if="appearNote.user.instance != null" :instance="appearNote.user.instance"/>
+			</div>
+		</header>
+		<div :class="[$style.noteContent, { [$style.contentCollapsed]: collapsed }]">
+			<p v-if="appearNote.cw != null" :class="$style.cw">
+				<EmMfm v-if="appearNote.cw != ''" style="margin-right: 8px;" :text="appearNote.cw" :author="appearNote.user" :nyaize="'respect'"/>
+				<button style="display: block; width: 100%; margin: 4px 0;" class="_buttonGray _buttonRounded" @click="showContent = !showContent">{{ showContent ? i18n.ts._cw.hide : i18n.ts._cw.show }}</button>
+			</p>
+			<div v-show="appearNote.cw == null || showContent">
+				<span v-if="appearNote.isHidden" style="opacity: 0.5">({{ i18n.ts.private }})</span>
+				<EmA v-if="appearNote.replyId" :class="$style.noteReplyTarget" :to="`/notes/${appearNote.replyId}`"><i class="ti ti-arrow-back-up"></i></EmA>
+				<EmMfm
+					v-if="appearNote.text"
+					:parsedNodes="parsed"
+					:text="appearNote.text"
+					:author="appearNote.user"
+					:nyaize="'respect'"
+					:emojiUrls="appearNote.emojis"
+				/>
+				<a v-if="appearNote.renote != null" :class="$style.rn">RN:</a>
+				<div v-if="appearNote.files && appearNote.files.length > 0">
+					<EmMediaList :mediaList="appearNote.files" :originalEntityUrl="`${url}/notes/${appearNote.id}`"/>
+				</div>
+				<EmPoll v-if="appearNote.poll" ref="pollViewer" :noteId="appearNote.id" :poll="appearNote.poll" :readOnly="true" :class="$style.poll"/>
+				<div v-if="appearNote.renote" :class="$style.quote"><EmNoteSimple :note="appearNote.renote" :class="$style.quoteNote"/></div>
+				<button v-if="isLong && collapsed" :class="$style.collapsed" class="_button" @click="collapsed = false">
+					<span :class="$style.collapsedLabel">{{ i18n.ts.showMore }}</span>
+				</button>
+				<button v-else-if="isLong && !collapsed" :class="$style.showLess" class="_button" @click="collapsed = true">
+					<span :class="$style.showLessLabel">{{ i18n.ts.showLess }}</span>
+				</button>
+			</div>
+			<EmA v-if="appearNote.channel && !inChannel" :class="$style.channel" :to="`/channels/${appearNote.channel.id}`"><i class="ti ti-device-tv"></i> {{ appearNote.channel.name }}</EmA>
+		</div>
+		<footer>
+			<div :class="$style.noteFooterInfo">
+				<span v-if="appearNote.visibility !== 'public'" style="display: inline-block; margin-right: 0.5em;" :title="i18n.ts._visibility[appearNote.visibility]">
+					<i v-if="appearNote.visibility === 'home'" class="ti ti-home"></i>
+					<i v-else-if="appearNote.visibility === 'followers'" class="ti ti-lock"></i>
+					<i v-else-if="appearNote.visibility === 'specified'" ref="specified" class="ti ti-mail"></i>
+				</span>
+				<span v-if="appearNote.localOnly" style="display: inline-block; margin-right: 0.5em;" :title="i18n.ts._visibility['disableFederation']"><i class="ti ti-rocket-off"></i></span>
+				<EmA :to="notePage(appearNote)">
+					<EmTime :time="appearNote.createdAt" mode="detail" colored/>
+				</EmA>
+			</div>
+			<EmReactionsViewer v-if="appearNote.reactionAcceptance !== 'likeOnly'" ref="reactionsViewer" :maxNumber="16" :note="appearNote">
+				<template #more>
+					<EmA :to="`/notes/${appearNote.id}`" :class="[$style.reactionOmitted]">{{ i18n.ts.more }}</EmA>
+				</template>
+			</EmReactionsViewer>
+			<a :href="`/notes/${appearNote.id}`" target="_blank" rel="noopener" :class="[$style.noteFooterButton, $style.footerButtonLink]" class="_button">
+				<i class="ti ti-arrow-back-up"></i>
+			</a>
+			<a :href="`/notes/${appearNote.id}`" target="_blank" rel="noopener" :class="[$style.noteFooterButton, $style.footerButtonLink]" class="_button">
+				<i class="ti ti-repeat"></i>
+				<p v-if="appearNote.renoteCount > 0" :class="$style.noteFooterButtonCount">{{ (appearNote.renoteCount) }}</p>
+			</a>
+			<a :href="`/notes/${appearNote.id}`" target="_blank" rel="noopener" :class="[$style.noteFooterButton, $style.footerButtonLink]" class="_button">
+				<i v-if="appearNote.reactionAcceptance === 'likeOnly'" class="ti ti-heart"></i>
+				<i v-else class="ti ti-plus"></i>
+				<p v-if="(appearNote.reactionAcceptance === 'likeOnly') && appearNote.reactionCount > 0" :class="$style.noteFooterButtonCount">{{ (appearNote.reactionCount) }}</p>
+			</a>
+			<a :href="`/notes/${appearNote.id}`" target="_blank" rel="noopener" :class="[$style.noteFooterButton, $style.footerButtonLink]" class="_button">
+				<i class="ti ti-dots"></i>
+			</a>
+		</footer>
+	</article>
+</div>
+</template>
+
+<script lang="ts" setup>
+import { computed, inject, ref } from 'vue';
+import * as mfm from 'mfm-js';
+import * as Misskey from 'misskey-js';
+import I18n from '@/components/I18n.vue';
+import EmMediaList from '@/components/EmMediaList.vue';
+import EmNoteSub from '@/components/EmNoteSub.vue';
+import EmNoteSimple from '@/components/EmNoteSimple.vue';
+import EmInstanceTicker from '@/components/EmInstanceTicker.vue';
+import EmReactionsViewer from '@/components/EmReactionsViewer.vue';
+import EmPoll from '@/components/EmPoll.vue';
+import EmA from '@/components/EmA.vue';
+import EmAvatar from '@/components/EmAvatar.vue';
+import EmTime from '@/components/EmTime.vue';
+import EmUserName from '@/components/EmUserName.vue';
+import EmAcct from '@/components/EmAcct.vue';
+import { userPage } from '@/utils.js';
+import { notePage } from '@/utils.js';
+import { i18n } from '@/i18n.js';
+import { DI } from '@/di.js';
+import { shouldCollapsed } from '@@/js/collapsed.js';
+import { url } from '@@/js/config.js';
+import EmMfm from '@/components/EmMfm.js';
+
+const props = defineProps<{
+	note: Misskey.entities.Note;
+}>();
+
+const serverMetadata = inject(DI.serverMetadata)!;
+
+const inChannel = inject('inChannel', null);
+
+const note = ref(props.note);
+
+const isRenote = (
+	note.value.renote != null &&
+	note.value.reply == null &&
+	note.value.text == null &&
+	note.value.cw == null &&
+	note.value.fileIds && note.value.fileIds.length === 0 &&
+	note.value.poll == null
+);
+
+const appearNote = computed(() => isRenote ? note.value.renote as Misskey.entities.Note : note.value);
+const showContent = ref(false);
+const isDeleted = ref(false);
+const parsed = appearNote.value.text ? mfm.parse(appearNote.value.text) : null;
+const isLong = shouldCollapsed(appearNote.value, []);
+const collapsed = ref(appearNote.value.cw == null && isLong);
+</script>
+
+<style lang="scss" module>
+.root {
+	position: relative;
+	transition: box-shadow 0.1s ease;
+	overflow: clip;
+	contain: content;
+}
+
+.replyTo {
+	opacity: 0.7;
+	padding-bottom: 0;
+}
+
+.renote {
+	display: flex;
+	align-items: center;
+	padding: 16px 32px 8px 32px;
+	line-height: 28px;
+	white-space: pre;
+	color: var(--MI_THEME-renote);
+}
+
+.renoteAvatar {
+	flex-shrink: 0;
+	display: inline-block;
+	width: 28px;
+	height: 28px;
+	margin: 0 8px 0 0;
+	border-radius: 6px;
+}
+
+.renoteText {
+	overflow: hidden;
+	flex-shrink: 1;
+	text-overflow: ellipsis;
+	white-space: nowrap;
+}
+
+.renoteName {
+	font-weight: bold;
+}
+
+.renoteInfo {
+	margin-left: auto;
+	font-size: 0.9em;
+}
+
+.renoteTime {
+	flex-shrink: 0;
+	color: inherit;
+}
+
+.renote + .note {
+	padding-top: 8px;
+}
+
+.note {
+	padding: 24px 32px 16px;
+	font-size: 1.2em;
+
+	&:hover > .main > .footer > .button {
+		opacity: 1;
+	}
+}
+
+.noteHeader {
+	display: flex;
+	position: relative;
+	margin-bottom: 16px;
+	align-items: center;
+}
+
+.noteHeaderAvatar {
+	display: block;
+	flex-shrink: 0;
+	width: 50px;
+	height: 50px;
+}
+
+.noteHeaderBody {
+	flex: 1;
+	display: flex;
+	min-width: 0;
+	flex-direction: column;
+	justify-content: center;
+	padding-left: 16px;
+	font-size: 0.95em;
+}
+
+.noteHeaderBodyUpper {
+	display: flex;
+	min-width: 0;
+}
+
+.noteHeaderName {
+	font-weight: bold;
+	line-height: 1.3;
+}
+
+.isBot {
+	display: inline-block;
+	margin: 0 0.5em;
+	padding: 4px 6px;
+	font-size: 80%;
+	line-height: 1;
+	border: solid 0.5px var(--MI_THEME-divider);
+	border-radius: 4px;
+}
+
+.noteHeaderInfo {
+	margin-left: auto;
+	display: flex;
+	gap: 0.5em;
+	align-items: center;
+}
+
+.noteHeaderInstanceIconLink {
+	display: inline-block;
+	margin-left: 4px;
+}
+
+.noteHeaderInstanceIcon {
+	width: 32px;
+	height: 32px;
+	border-radius: 4px;
+}
+
+.noteHeaderUsername {
+	margin-bottom: 2px;
+	line-height: 1.3;
+	word-wrap: anywhere;
+}
+
+.noteContent {
+	container-type: inline-size;
+	overflow-wrap: break-word;
+}
+
+.cw {
+	cursor: default;
+	display: block;
+	margin: 0;
+	padding: 0;
+	overflow-wrap: break-word;
+}
+
+.noteReplyTarget {
+	color: var(--MI_THEME-accent);
+	margin-right: 0.5em;
+}
+
+.rn {
+	margin-left: 4px;
+	font-style: oblique;
+	color: var(--MI_THEME-renote);
+}
+
+.reactionOmitted {
+	display: inline-block;
+	margin-left: 8px;
+	opacity: .8;
+	font-size: 95%;
+}
+
+.poll {
+	font-size: 80%;
+}
+
+.quote {
+	padding: 8px 0;
+}
+
+.quoteNote {
+	padding: 16px;
+	border: dashed 1px var(--MI_THEME-renote);
+	border-radius: 8px;
+	overflow: clip;
+}
+
+.channel {
+	opacity: 0.7;
+	font-size: 80%;
+}
+
+.showLess {
+	width: 100%;
+	margin-top: 14px;
+	position: sticky;
+	bottom: calc(var(--MI-stickyBottom, 0px) + 14px);
+}
+
+.showLessLabel {
+	display: inline-block;
+	background: var(--MI_THEME-popup);
+	padding: 6px 10px;
+	font-size: 0.8em;
+	border-radius: 999px;
+	box-shadow: 0 2px 6px rgb(0 0 0 / 20%);
+}
+
+.contentCollapsed {
+	position: relative;
+	max-height: 9em;
+	overflow: clip;
+}
+
+.collapsed {
+	display: block;
+	position: absolute;
+	bottom: 0;
+	left: 0;
+	z-index: 2;
+	width: 100%;
+	height: 64px;
+	background: linear-gradient(0deg, var(--MI_THEME-panel), var(--MI_THEME-X15));
+
+	&:hover > .collapsedLabel {
+		background: var(--MI_THEME-panelHighlight);
+	}
+}
+
+.collapsedLabel {
+	display: inline-block;
+	background: var(--MI_THEME-panel);
+	padding: 6px 10px;
+	font-size: 0.8em;
+	border-radius: 999px;
+	box-shadow: 0 2px 6px rgb(0 0 0 / 20%);
+}
+
+.noteFooterInfo {
+	margin: 16px 0;
+	opacity: 0.7;
+	font-size: 0.9em;
+}
+
+.noteFooterButton {
+	margin: 0;
+	padding: 8px;
+	opacity: 0.7;
+
+	&:not(:last-child) {
+		margin-right: 28px;
+	}
+
+	&:hover {
+		color: var(--MI_THEME-fgHighlighted);
+	}
+}
+
+.footerButtonLink:hover,
+.footerButtonLink:focus,
+.footerButtonLink:active {
+	text-decoration: none;
+}
+
+.noteFooterButtonCount {
+	display: inline;
+	margin: 0 0 0 8px;
+	opacity: 0.7;
+
+	&.reacted {
+		color: var(--MI_THEME-accent);
+	}
+}
+
+@container (max-width: 500px) {
+	.root {
+		font-size: 0.9em;
+	}
+}
+
+@container (max-width: 450px) {
+	.renote {
+		padding: 8px 16px 0 16px;
+	}
+
+	.note {
+		padding: 16px;
+	}
+
+	.noteHeaderAvatar {
+		width: 50px;
+		height: 50px;
+	}
+}
+
+@container (max-width: 350px) {
+	.noteFooterButton {
+		&:not(:last-child) {
+			margin-right: 18px;
+		}
+	}
+}
+
+@container (max-width: 300px) {
+	.root {
+		font-size: 0.825em;
+	}
+
+	.noteHeaderAvatar {
+		width: 50px;
+		height: 50px;
+	}
+
+	.noteFooterButton {
+		&:not(:last-child) {
+			margin-right: 12px;
+		}
+	}
+}
+</style>
diff --git a/packages/frontend-embed/src/components/EmNoteHeader.vue b/packages/frontend-embed/src/components/EmNoteHeader.vue
new file mode 100644
index 0000000000..85b4aac071
--- /dev/null
+++ b/packages/frontend-embed/src/components/EmNoteHeader.vue
@@ -0,0 +1,104 @@
+<!--
+SPDX-FileCopyrightText: syuilo and misskey-project
+SPDX-License-Identifier: AGPL-3.0-only
+-->
+
+<template>
+<header :class="$style.root">
+	<EmA :class="$style.name" :to="userPage(note.user)">
+		<EmUserName :user="note.user"/>
+	</EmA>
+	<div v-if="note.user.isBot" :class="$style.isBot">bot</div>
+	<div :class="$style.username"><EmAcct :user="note.user"/></div>
+	<div v-if="note.user.badgeRoles" :class="$style.badgeRoles">
+		<img v-for="(role, i) in note.user.badgeRoles" :key="i" :class="$style.badgeRole" :src="role.iconUrl!"/>
+	</div>
+	<div :class="$style.info">
+		<EmA :to="notePage(note)">
+			<EmTime :time="note.createdAt" colored/>
+		</EmA>
+		<span v-if="note.visibility !== 'public'" style="margin-left: 0.5em;">
+			<i v-if="note.visibility === 'home'" class="ti ti-home"></i>
+			<i v-else-if="note.visibility === 'followers'" class="ti ti-lock"></i>
+			<i v-else-if="note.visibility === 'specified'" ref="specified" class="ti ti-mail"></i>
+		</span>
+		<span v-if="note.localOnly" style="margin-left: 0.5em;"><i class="ti ti-rocket-off"></i></span>
+		<span v-if="note.channel" style="margin-left: 0.5em;" :title="note.channel.name"><i class="ti ti-device-tv"></i></span>
+	</div>
+</header>
+</template>
+
+<script lang="ts" setup>
+import { } from 'vue';
+import * as Misskey from 'misskey-js';
+import { notePage } from '@/utils.js';
+import { userPage } from '@/utils.js';
+import EmA from '@/components/EmA.vue';
+import EmUserName from '@/components/EmUserName.vue';
+import EmAcct from '@/components/EmAcct.vue';
+import EmTime from '@/components/EmTime.vue';
+
+defineProps<{
+	note: Misskey.entities.Note;
+}>();
+</script>
+
+<style lang="scss" module>
+.root {
+	display: flex;
+	align-items: baseline;
+	white-space: nowrap;
+}
+
+.name {
+	flex-shrink: 1;
+	display: block;
+	margin: 0 .5em 0 0;
+	padding: 0;
+	overflow: hidden;
+	font-size: 1em;
+	font-weight: bold;
+	text-decoration: none;
+	text-overflow: ellipsis;
+
+	&:hover {
+		text-decoration: underline;
+	}
+}
+
+.isBot {
+	flex-shrink: 0;
+	align-self: center;
+	margin: 0 .5em 0 0;
+	padding: 1px 6px;
+	font-size: 80%;
+	border: solid 0.5px var(--MI_THEME-divider);
+	border-radius: 3px;
+}
+
+.username {
+	flex-shrink: 9999999;
+	margin: 0 .5em 0 0;
+	overflow: hidden;
+	text-overflow: ellipsis;
+}
+
+.info {
+	flex-shrink: 0;
+	margin-left: auto;
+	font-size: 0.9em;
+}
+
+.badgeRoles {
+	margin: 0 .5em 0 0;
+}
+
+.badgeRole {
+	height: 1.3em;
+	vertical-align: -20%;
+
+	& + .badgeRole {
+		margin-left: 0.2em;
+	}
+}
+</style>
diff --git a/packages/frontend-embed/src/components/EmNoteSimple.vue b/packages/frontend-embed/src/components/EmNoteSimple.vue
new file mode 100644
index 0000000000..b9aaf3fa4a
--- /dev/null
+++ b/packages/frontend-embed/src/components/EmNoteSimple.vue
@@ -0,0 +1,106 @@
+<!--
+SPDX-FileCopyrightText: syuilo and misskey-project
+SPDX-License-Identifier: AGPL-3.0-only
+-->
+
+<template>
+<div :class="$style.root">
+	<EmAvatar :class="$style.avatar" :user="note.user" link preview/>
+	<div :class="$style.main">
+		<EmNoteHeader :class="$style.header" :note="note" :mini="true"/>
+		<div>
+			<p v-if="note.cw != null" :class="$style.cw">
+				<EmMfm v-if="note.cw != ''" style="margin-right: 8px;" :text="note.cw" :author="note.user" :nyaize="'respect'" :emojiUrls="note.emojis"/>
+				<button style="display: block; width: 100%;" class="_buttonGray _buttonRounded" @click="showContent = !showContent">{{ showContent ? i18n.ts._cw.hide : i18n.ts._cw.show }}</button>
+			</p>
+			<div v-show="note.cw == null || showContent">
+				<EmSubNoteContent :class="$style.text" :note="note"/>
+			</div>
+		</div>
+	</div>
+</div>
+</template>
+
+<script lang="ts" setup>
+import { ref } from 'vue';
+import * as Misskey from 'misskey-js';
+import { i18n } from '@/i18n.js';
+import EmAvatar from '@/components/EmAvatar.vue';
+import EmNoteHeader from '@/components/EmNoteHeader.vue';
+import EmSubNoteContent from '@/components/EmSubNoteContent.vue';
+import EmMfm from '@/components/EmMfm.js';
+
+const props = defineProps<{
+	note: Misskey.entities.Note;
+}>();
+
+const showContent = ref(false);
+</script>
+
+<style lang="scss" module>
+.root {
+	display: flex;
+	margin: 0;
+	padding: 0;
+	font-size: 0.95em;
+}
+
+.avatar {
+	flex-shrink: 0;
+	display: block;
+	margin: 0 10px 0 0;
+	width: 34px;
+	height: 34px;
+	border-radius: 8px;
+	position: sticky !important;
+	top: calc(16px + var(--MI-stickyTop, 0px));
+	left: 0;
+}
+
+.main {
+	flex: 1;
+	min-width: 0;
+}
+
+.header {
+	margin-bottom: 2px;
+}
+
+.cw {
+	cursor: default;
+	display: block;
+	margin: 0;
+	padding: 0;
+	overflow-wrap: break-word;
+}
+
+.text {
+	cursor: default;
+	margin: 0;
+	padding: 0;
+}
+
+@container (min-width: 250px) {
+	.avatar {
+		margin: 0 10px 0 0;
+		width: 40px;
+		height: 40px;
+	}
+}
+
+@container (min-width: 350px) {
+	.avatar {
+		margin: 0 10px 0 0;
+		width: 44px;
+		height: 44px;
+	}
+}
+
+@container (min-width: 500px) {
+	.avatar {
+		margin: 0 12px 0 0;
+		width: 48px;
+		height: 48px;
+	}
+}
+</style>
diff --git a/packages/frontend-embed/src/components/EmNoteSub.vue b/packages/frontend-embed/src/components/EmNoteSub.vue
new file mode 100644
index 0000000000..59be8608e0
--- /dev/null
+++ b/packages/frontend-embed/src/components/EmNoteSub.vue
@@ -0,0 +1,151 @@
+<!--
+SPDX-FileCopyrightText: syuilo and misskey-project
+SPDX-License-Identifier: AGPL-3.0-only
+-->
+
+<template>
+<div :class="[$style.root, { [$style.children]: depth > 1 }]">
+	<div :class="$style.main">
+		<div v-if="note.channel" :class="$style.colorBar" :style="{ background: note.channel.color }"></div>
+		<EmAvatar :class="$style.avatar" :user="note.user" link preview/>
+		<div :class="$style.body">
+			<EmNoteHeader :class="$style.header" :note="note" :mini="true"/>
+			<div>
+				<p v-if="note.cw != null" :class="$style.cw">
+					<EmMfm v-if="note.cw != ''" style="margin-right: 8px;" :text="note.cw" :author="note.user" :nyaize="'respect'"/>
+					<button style="display: block; width: 100%;" class="_buttonGray _buttonRounded" @click="showContent = !showContent">{{ showContent ? i18n.ts._cw.hide : i18n.ts._cw.show }}</button>
+				</p>
+				<div v-show="note.cw == null || showContent">
+					<EmSubNoteContent :class="$style.text" :note="note"/>
+				</div>
+			</div>
+		</div>
+	</div>
+	<template v-if="depth < 5">
+		<EmNoteSub v-for="reply in replies" :key="reply.id" :note="reply" :class="$style.reply" :detail="true" :depth="depth + 1"/>
+	</template>
+	<div v-else :class="$style.more">
+		<EmA class="_link" :to="notePage(note)">{{ i18n.ts.continueThread }} <i class="ti ti-chevron-double-right"></i></EmA>
+	</div>
+</div>
+</template>
+
+<script lang="ts" setup>
+import { ref } from 'vue';
+import * as Misskey from 'misskey-js';
+import EmA from '@/components/EmA.vue';
+import EmAvatar from '@/components/EmAvatar.vue';
+import EmNoteHeader from '@/components/EmNoteHeader.vue';
+import EmSubNoteContent from '@/components/EmSubNoteContent.vue';
+import { notePage } from '@/utils.js';
+import { misskeyApi } from '@/misskey-api.js';
+import { i18n } from '@/i18n.js';
+import EmMfm from '@/components/EmMfm.js';
+
+const props = withDefaults(defineProps<{
+	note: Misskey.entities.Note;
+	detail?: boolean;
+
+	// how many notes are in between this one and the note being viewed in detail
+	depth?: number;
+}>(), {
+	depth: 1,
+});
+
+const showContent = ref(false);
+const replies = ref<Misskey.entities.Note[]>([]);
+
+if (props.detail) {
+	misskeyApi('notes/children', {
+		noteId: props.note.id,
+		limit: 5,
+	}).then(res => {
+		replies.value = res;
+	});
+}
+</script>
+
+<style lang="scss" module>
+.root {
+	padding: 16px 32px;
+	font-size: 0.9em;
+	position: relative;
+
+	&.children {
+		padding: 10px 0 0 16px;
+		font-size: 1em;
+	}
+}
+
+.main {
+	display: flex;
+}
+
+.colorBar {
+	position: absolute;
+	top: 8px;
+	left: 8px;
+	width: 5px;
+	height: calc(100% - 8px);
+	border-radius: 999px;
+	pointer-events: none;
+}
+
+.avatar {
+	flex-shrink: 0;
+	display: block;
+	margin: 0 8px 0 0;
+	width: 38px;
+	height: 38px;
+	border-radius: 8px;
+}
+
+.body {
+	flex: 1;
+	min-width: 0;
+}
+
+.header {
+	margin-bottom: 2px;
+}
+
+.cw {
+	cursor: default;
+	display: block;
+	margin: 0;
+	padding: 0;
+	overflow-wrap: break-word;
+}
+
+.text {
+	margin: 0;
+	padding: 0;
+}
+
+.reply, .more {
+	border-left: solid 0.5px var(--MI_THEME-divider);
+	margin-top: 10px;
+}
+
+.more {
+	padding: 10px 0 0 16px;
+}
+
+@container (max-width: 450px) {
+	.root {
+		padding: 14px 16px;
+
+		&.children {
+			padding: 10px 0 0 8px;
+		}
+	}
+}
+
+.muted {
+	text-align: center;
+	padding: 8px !important;
+	border: 1px solid var(--MI_THEME-divider);
+	margin: 8px 8px 0 8px;
+	border-radius: 8px;
+}
+</style>
diff --git a/packages/frontend-embed/src/components/EmNotes.vue b/packages/frontend-embed/src/components/EmNotes.vue
new file mode 100644
index 0000000000..4211261e19
--- /dev/null
+++ b/packages/frontend-embed/src/components/EmNotes.vue
@@ -0,0 +1,52 @@
+<!--
+SPDX-FileCopyrightText: syuilo and misskey-project
+SPDX-License-Identifier: AGPL-3.0-only
+-->
+
+<template>
+<EmPagination ref="pagingComponent" :pagination="pagination" :disableAutoLoad="disableAutoLoad">
+	<template #empty>
+		<div class="_fullinfo">
+			<div>{{ i18n.ts.noNotes }}</div>
+		</div>
+	</template>
+
+	<template #default="{ items: notes }">
+		<div :class="[$style.root]">
+			<EmNote v-for="note in notes" :key="note._featuredId_ || note._prId_ || note.id" :class="$style.note" :note="note"/>
+		</div>
+	</template>
+</EmPagination>
+</template>
+
+<script lang="ts" setup>
+import { useTemplateRef } from 'vue';
+import EmNote from '@/components/EmNote.vue';
+import EmPagination, { Paging } from '@/components/EmPagination.vue';
+import { i18n } from '@/i18n.js';
+
+withDefaults(defineProps<{
+	pagination: Paging;
+	noGap?: boolean;
+	disableAutoLoad?: boolean;
+	ad?: boolean;
+}>(), {
+	ad: true,
+});
+
+const pagingComponent = useTemplateRef('pagingComponent');
+
+defineExpose({
+	pagingComponent,
+});
+</script>
+
+<style lang="scss" module>
+.root {
+	background: var(--MI_THEME-panel);
+}
+
+.note {
+	border-bottom: 0.5px solid var(--MI_THEME-divider);
+}
+</style>
diff --git a/packages/frontend-embed/src/components/EmPagination.vue b/packages/frontend-embed/src/components/EmPagination.vue
new file mode 100644
index 0000000000..5d5317a912
--- /dev/null
+++ b/packages/frontend-embed/src/components/EmPagination.vue
@@ -0,0 +1,504 @@
+<!--
+SPDX-FileCopyrightText: syuilo and misskey-project
+SPDX-License-Identifier: AGPL-3.0-only
+-->
+
+<template>
+<EmLoading v-if="fetching"/>
+
+<EmError v-else-if="error" @retry="init()"/>
+
+<div v-else-if="empty" key="_empty_" class="empty">
+	<slot name="empty">
+		<div class="_fullinfo">
+			<div>{{ i18n.ts.nothing }}</div>
+		</div>
+	</slot>
+</div>
+
+<div v-else ref="rootEl">
+	<div v-show="pagination.reversed && more" key="_more_" class="_margin">
+		<button v-if="!moreFetching" class="_buttonPrimary" :class="$style.more" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }" @click="fetchMoreAhead">
+			{{ i18n.ts.loadMore }}
+		</button>
+		<EmLoading v-else class="loading"/>
+	</div>
+	<slot :items="Array.from(items.values())" :fetching="fetching || moreFetching"></slot>
+	<div v-show="!pagination.reversed && more" key="_more_" class="_margin">
+		<button v-if="!moreFetching" class="_buttonRounded _buttonPrimary" :class="$style.more" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }" @click="fetchMore">
+			{{ i18n.ts.loadMore }}
+		</button>
+		<EmLoading v-else class="loading"/>
+	</div>
+</div>
+</template>
+
+<script lang="ts">
+import { computed, ComputedRef, isRef, nextTick, onActivated, onBeforeMount, onBeforeUnmount, onDeactivated, ref, shallowRef, watch } from 'vue';
+import * as Misskey from 'misskey-js';
+import { useDocumentVisibility } from '@@/js/use-document-visibility.js';
+import { onScrollTop, isTopVisible, getBodyScrollHeight, getScrollContainer, onScrollBottom, scrollToBottom, scroll, isBottomVisible } from '@@/js/scroll.js';
+import { misskeyApi } from '@/misskey-api.js';
+import { i18n } from '@/i18n.js';
+
+const SECOND_FETCH_LIMIT = 30;
+const TOLERANCE = 16;
+const APPEAR_MINIMUM_INTERVAL = 600;
+
+export type Paging<E extends keyof Misskey.Endpoints = keyof Misskey.Endpoints> = {
+	endpoint: E;
+	limit: number;
+	params?: Misskey.Endpoints[E]['req'] | ComputedRef<Misskey.Endpoints[E]['req']>;
+
+	/**
+	 * 検索APIのような、ページング不可なエンドポイントを利用する場合
+	 * (そのようなAPIをこの関数で使うのは若干矛盾してるけど)
+	 */
+	noPaging?: boolean;
+
+	/**
+	 * items 配列の中身を逆順にする(新しい方が最後)
+	 */
+	reversed?: boolean;
+
+	offsetMode?: boolean;
+
+	pageEl?: HTMLElement;
+};
+
+type MisskeyEntity = {
+	id: string;
+	createdAt: string;
+	_shouldInsertAd_?: boolean;
+	[x: string]: any;
+};
+
+type MisskeyEntityMap = Map<string, MisskeyEntity>;
+
+function arrayToEntries(entities: MisskeyEntity[]): [string, MisskeyEntity][] {
+	return entities.map(en => [en.id, en]);
+}
+
+function concatMapWithArray(map: MisskeyEntityMap, entities: MisskeyEntity[]): MisskeyEntityMap {
+	return new Map([...map, ...arrayToEntries(entities)]);
+}
+
+</script>
+<script lang="ts" setup>
+import EmError from '@/components/EmError.vue';
+import EmLoading from '@/components/EmLoading.vue';
+
+const props = withDefaults(defineProps<{
+	pagination: Paging;
+	disableAutoLoad?: boolean;
+	displayLimit?: number;
+}>(), {
+	displayLimit: 20,
+});
+
+const emit = defineEmits<{
+	(ev: 'queue', count: number): void;
+	(ev: 'status', error: boolean): void;
+}>();
+
+const rootEl = shallowRef<HTMLElement>();
+
+// 遡り中かどうか
+const backed = ref(false);
+
+const scrollRemove = ref<(() => void) | null>(null);
+
+/**
+ * 表示するアイテムのソース
+ * 最新が0番目
+ */
+const items = ref<MisskeyEntityMap>(new Map());
+
+/**
+ * タブが非アクティブなどの場合に更新を貯めておく
+ * 最新が0番目
+ */
+const queue = ref<MisskeyEntityMap>(new Map());
+
+const offset = ref(0);
+
+/**
+ * 初期化中かどうか(trueならEmLoadingで全て隠す)
+ */
+const fetching = ref(true);
+
+const moreFetching = ref(false);
+const more = ref(false);
+const preventAppearFetchMore = ref(false);
+const preventAppearFetchMoreTimer = ref<number | null>(null);
+const isBackTop = ref(false);
+const empty = computed(() => items.value.size === 0);
+const error = ref(false);
+
+const contentEl = computed(() => props.pagination.pageEl ?? rootEl.value);
+const scrollableElement = computed(() => contentEl.value ? getScrollContainer(contentEl.value) : document.body);
+
+const visibility = useDocumentVisibility();
+
+let isPausingUpdate = false;
+let timerForSetPause: number | null = null;
+const BACKGROUND_PAUSE_WAIT_SEC = 10;
+
+// 先頭が表示されているかどうかを検出
+// https://qiita.com/mkataigi/items/0154aefd2223ce23398e
+const scrollObserver = ref<IntersectionObserver>();
+
+watch([() => props.pagination.reversed, scrollableElement], () => {
+	if (scrollObserver.value) scrollObserver.value.disconnect();
+
+	scrollObserver.value = new IntersectionObserver(entries => {
+		backed.value = entries[0].isIntersecting;
+	}, {
+		root: scrollableElement.value,
+		rootMargin: props.pagination.reversed ? '-100% 0px 100% 0px' : '100% 0px -100% 0px',
+		threshold: 0.01,
+	});
+}, { immediate: true });
+
+watch(rootEl, () => {
+	scrollObserver.value?.disconnect();
+	nextTick(() => {
+		if (rootEl.value) scrollObserver.value?.observe(rootEl.value);
+	});
+});
+
+watch([backed, contentEl], () => {
+	if (!backed.value) {
+		if (!contentEl.value) return;
+
+		scrollRemove.value = (props.pagination.reversed ? onScrollBottom : onScrollTop)(contentEl.value, executeQueue, TOLERANCE);
+	} else {
+		if (scrollRemove.value) scrollRemove.value();
+		scrollRemove.value = null;
+	}
+});
+
+// パラメータに何らかの変更があった際、再読込したい(チャンネル等のIDが変わったなど)
+watch(() => [props.pagination.endpoint, props.pagination.params], init, { deep: true });
+
+watch(queue, (a, b) => {
+	if (a.size === 0 && b.size === 0) return;
+	emit('queue', queue.value.size);
+}, { deep: true });
+
+watch(error, (n, o) => {
+	if (n === o) return;
+	emit('status', n);
+});
+
+async function init(): Promise<void> {
+	items.value = new Map();
+	queue.value = new Map();
+	fetching.value = true;
+	const params = props.pagination.params ? isRef(props.pagination.params) ? props.pagination.params.value : props.pagination.params : {};
+	await misskeyApi<MisskeyEntity[]>(props.pagination.endpoint, {
+		...params,
+		limit: props.pagination.limit ?? 10,
+		allowPartial: true,
+	}).then(res => {
+		for (let i = 0; i < res.length; i++) {
+			const item = res[i];
+			if (i === 3) item._shouldInsertAd_ = true;
+		}
+
+		if (res.length === 0 || props.pagination.noPaging) {
+			concatItems(res);
+			more.value = false;
+		} else {
+			if (props.pagination.reversed) moreFetching.value = true;
+			concatItems(res);
+			more.value = true;
+		}
+
+		offset.value = res.length;
+		error.value = false;
+		fetching.value = false;
+	}, err => {
+		error.value = true;
+		fetching.value = false;
+	});
+}
+
+const reload = (): Promise<void> => {
+	return init();
+};
+
+const fetchMore = async (): Promise<void> => {
+	if (!more.value || fetching.value || moreFetching.value || items.value.size === 0) return;
+	moreFetching.value = true;
+	const params = props.pagination.params ? isRef(props.pagination.params) ? props.pagination.params.value : props.pagination.params : {};
+	await misskeyApi<MisskeyEntity[]>(props.pagination.endpoint, {
+		...params,
+		limit: SECOND_FETCH_LIMIT,
+		...(props.pagination.offsetMode ? {
+			offset: offset.value,
+		} : {
+			untilId: Array.from(items.value.keys()).at(-1),
+		}),
+	}).then(res => {
+		for (let i = 0; i < res.length; i++) {
+			const item = res[i];
+			if (i === 10) item._shouldInsertAd_ = true;
+		}
+
+		const reverseConcat = _res => {
+			const oldHeight = scrollableElement.value ? scrollableElement.value.scrollHeight : getBodyScrollHeight();
+			const oldScroll = scrollableElement.value ? scrollableElement.value.scrollTop : window.scrollY;
+
+			items.value = concatMapWithArray(items.value, _res);
+
+			return nextTick(() => {
+				if (scrollableElement.value) {
+					scroll(scrollableElement.value, { top: oldScroll + (scrollableElement.value.scrollHeight - oldHeight), behavior: 'instant' });
+				} else {
+					window.scroll({ top: oldScroll + (getBodyScrollHeight() - oldHeight), behavior: 'instant' });
+				}
+
+				return nextTick();
+			});
+		};
+
+		if (res.length === 0) {
+			if (props.pagination.reversed) {
+				reverseConcat(res).then(() => {
+					more.value = false;
+					moreFetching.value = false;
+				});
+			} else {
+				items.value = concatMapWithArray(items.value, res);
+				more.value = false;
+				moreFetching.value = false;
+			}
+		} else {
+			if (props.pagination.reversed) {
+				reverseConcat(res).then(() => {
+					more.value = true;
+					moreFetching.value = false;
+				});
+			} else {
+				items.value = concatMapWithArray(items.value, res);
+				more.value = true;
+				moreFetching.value = false;
+			}
+		}
+		offset.value += res.length;
+	}, err => {
+		moreFetching.value = false;
+	});
+};
+
+const fetchMoreAhead = async (): Promise<void> => {
+	if (!more.value || fetching.value || moreFetching.value || items.value.size === 0) return;
+	moreFetching.value = true;
+	const params = props.pagination.params ? isRef(props.pagination.params) ? props.pagination.params.value : props.pagination.params : {};
+	await misskeyApi<MisskeyEntity[]>(props.pagination.endpoint, {
+		...params,
+		limit: SECOND_FETCH_LIMIT,
+		...(props.pagination.offsetMode ? {
+			offset: offset.value,
+		} : {
+			sinceId: Array.from(items.value.keys()).at(-1),
+		}),
+	}).then(res => {
+		if (res.length === 0) {
+			items.value = concatMapWithArray(items.value, res);
+			more.value = false;
+		} else {
+			items.value = concatMapWithArray(items.value, res);
+			more.value = true;
+		}
+		offset.value += res.length;
+		moreFetching.value = false;
+	}, err => {
+		moreFetching.value = false;
+	});
+};
+
+/**
+ * Appear(IntersectionObserver)によってfetchMoreが呼ばれる場合、
+ * APPEAR_MINIMUM_INTERVALミリ秒以内に2回fetchMoreが呼ばれるのを防ぐ
+ */
+const fetchMoreApperTimeoutFn = (): void => {
+	preventAppearFetchMore.value = false;
+	preventAppearFetchMoreTimer.value = null;
+};
+const fetchMoreAppearTimeout = (): void => {
+	preventAppearFetchMore.value = true;
+	preventAppearFetchMoreTimer.value = window.setTimeout(fetchMoreApperTimeoutFn, APPEAR_MINIMUM_INTERVAL);
+};
+
+const appearFetchMore = async (): Promise<void> => {
+	if (preventAppearFetchMore.value) return;
+	await fetchMore();
+	fetchMoreAppearTimeout();
+};
+
+const appearFetchMoreAhead = async (): Promise<void> => {
+	if (preventAppearFetchMore.value) return;
+	await fetchMoreAhead();
+	fetchMoreAppearTimeout();
+};
+
+const isTop = (): boolean => isBackTop.value || (props.pagination.reversed ? isBottomVisible : isTopVisible)(contentEl.value!, TOLERANCE);
+
+watch(visibility, () => {
+	if (visibility.value === 'hidden') {
+		timerForSetPause = window.setTimeout(() => {
+			isPausingUpdate = true;
+			timerForSetPause = null;
+		},
+		BACKGROUND_PAUSE_WAIT_SEC * 1000);
+	} else { // 'visible'
+		if (timerForSetPause) {
+			clearTimeout(timerForSetPause);
+			timerForSetPause = null;
+		} else {
+			isPausingUpdate = false;
+			if (isTop()) {
+				executeQueue();
+			}
+		}
+	}
+});
+
+/**
+ * 最新のものとして1つだけアイテムを追加する
+ * ストリーミングから降ってきたアイテムはこれで追加する
+ * @param item アイテム
+ */
+const prepend = (item: MisskeyEntity): void => {
+	if (items.value.size === 0) {
+		items.value.set(item.id, item);
+		fetching.value = false;
+		return;
+	}
+
+	if (isTop() && !isPausingUpdate) unshiftItems([item]);
+	else prependQueue(item);
+};
+
+/**
+ * 新着アイテムをitemsの先頭に追加し、displayLimitを適用する
+ * @param newItems 新しいアイテムの配列
+ */
+function unshiftItems(newItems: MisskeyEntity[]) {
+	const length = newItems.length + items.value.size;
+	items.value = new Map([...arrayToEntries(newItems), ...items.value].slice(0, props.displayLimit));
+
+	if (length >= props.displayLimit) more.value = true;
+}
+
+/**
+ * 古いアイテムをitemsの末尾に追加し、displayLimitを適用する
+ * @param oldItems 古いアイテムの配列
+ */
+function concatItems(oldItems: MisskeyEntity[]) {
+	const length = oldItems.length + items.value.size;
+	items.value = new Map([...items.value, ...arrayToEntries(oldItems)].slice(0, props.displayLimit));
+
+	if (length >= props.displayLimit) more.value = true;
+}
+
+function executeQueue() {
+	unshiftItems(Array.from(queue.value.values()));
+	queue.value = new Map();
+}
+
+function prependQueue(newItem: MisskeyEntity) {
+	queue.value = new Map([[newItem.id, newItem], ...queue.value].slice(0, props.displayLimit) as [string, MisskeyEntity][]);
+}
+
+/*
+ * アイテムを末尾に追加する(使うの?)
+ */
+const appendItem = (item: MisskeyEntity): void => {
+	items.value.set(item.id, item);
+};
+
+const removeItem = (id: string) => {
+	items.value.delete(id);
+	queue.value.delete(id);
+};
+
+const updateItem = (id: MisskeyEntity['id'], replacer: (old: MisskeyEntity) => MisskeyEntity): void => {
+	const item = items.value.get(id);
+	if (item) items.value.set(id, replacer(item));
+
+	const queueItem = queue.value.get(id);
+	if (queueItem) queue.value.set(id, replacer(queueItem));
+};
+
+onActivated(() => {
+	isBackTop.value = false;
+});
+
+onDeactivated(() => {
+	isBackTop.value = props.pagination.reversed ? window.scrollY >= (rootEl.value ? rootEl.value.scrollHeight - window.innerHeight : 0) : window.scrollY === 0;
+});
+
+function toBottom() {
+	scrollToBottom(contentEl.value!);
+}
+
+onBeforeMount(() => {
+	init().then(() => {
+		if (props.pagination.reversed) {
+			nextTick(() => {
+				setTimeout(toBottom, 800);
+
+				// scrollToBottomでmoreFetchingボタンが画面外まで出るまで
+				// more = trueを遅らせる
+				setTimeout(() => {
+					moreFetching.value = false;
+				}, 2000);
+			});
+		}
+	});
+});
+
+onBeforeUnmount(() => {
+	if (timerForSetPause) {
+		clearTimeout(timerForSetPause);
+		timerForSetPause = null;
+	}
+	if (preventAppearFetchMoreTimer.value) {
+		clearTimeout(preventAppearFetchMoreTimer.value);
+		preventAppearFetchMoreTimer.value = null;
+	}
+	scrollObserver.value?.disconnect();
+});
+
+defineExpose({
+	items,
+	queue,
+	backed: backed.value,
+	more,
+	reload,
+	prepend,
+	append: appendItem,
+	removeItem,
+	updateItem,
+});
+</script>
+
+<style lang="scss" module>
+.transition_fade_enterActive,
+.transition_fade_leaveActive {
+	transition: opacity 0.125s ease;
+}
+.transition_fade_enterFrom,
+.transition_fade_leaveTo {
+	opacity: 0;
+}
+
+.more {
+	display: block;
+	margin-left: auto;
+	margin-right: auto;
+}
+</style>
diff --git a/packages/frontend-embed/src/components/EmPoll.vue b/packages/frontend-embed/src/components/EmPoll.vue
new file mode 100644
index 0000000000..d197e094c6
--- /dev/null
+++ b/packages/frontend-embed/src/components/EmPoll.vue
@@ -0,0 +1,82 @@
+<!--
+SPDX-FileCopyrightText: syuilo and misskey-project
+SPDX-License-Identifier: AGPL-3.0-only
+-->
+
+<template>
+<div>
+	<ul :class="$style.choices">
+		<li v-for="(choice, i) in poll.choices" :key="i" :class="$style.choice">
+			<div :class="$style.bg" :style="{ 'width': `${choice.votes / total * 100}%` }"></div>
+			<span :class="$style.fg">
+				<template v-if="choice.isVoted"><i class="ti ti-check" style="margin-right: 4px; color: var(--MI_THEME-accent);"></i></template>
+				<EmMfm :text="choice.text" :plain="true"/>
+				<span style="margin-left: 4px; opacity: 0.7;">({{ i18n.tsx._poll.votesCount({ n: choice.votes }) }})</span>
+			</span>
+		</li>
+	</ul>
+	<p :class="$style.info">
+		<span>{{ i18n.tsx._poll.totalVotes({ n: total }) }}</span>
+	</p>
+</div>
+</template>
+
+<script lang="ts" setup>
+import { computed } from 'vue';
+import * as Misskey from 'misskey-js';
+import { i18n } from '@/i18n.js';
+import EmMfm from '@/components/EmMfm.js';
+
+function sum(xs: number[]): number {
+	return xs.reduce((a, b) => a + b, 0);
+}
+
+const props = defineProps<{
+	noteId: string;
+	poll: NonNullable<Misskey.entities.Note['poll']>;
+}>();
+
+const total = computed(() => sum(props.poll.choices.map(x => x.votes)));
+</script>
+
+<style lang="scss" module>
+.choices {
+	display: block;
+	margin: 0;
+	padding: 0;
+	list-style: none;
+}
+
+.choice {
+	display: block;
+	position: relative;
+	margin: 4px 0;
+	padding: 4px;
+	//border: solid 0.5px var(--MI_THEME-divider);
+	background: var(--MI_THEME-accentedBg);
+	border-radius: 4px;
+	overflow: clip;
+}
+
+.bg {
+	position: absolute;
+	top: 0;
+	left: 0;
+	height: 100%;
+	background: var(--MI_THEME-accent);
+	background: linear-gradient(90deg,var(--MI_THEME-buttonGradateA),var(--MI_THEME-buttonGradateB));
+	transition: width 1s ease;
+}
+
+.fg {
+	position: relative;
+	display: inline-block;
+	padding: 3px 5px;
+	background: var(--MI_THEME-panel);
+	border-radius: 3px;
+}
+
+.info {
+	color: var(--MI_THEME-fg);
+}
+</style>
diff --git a/packages/frontend-embed/src/components/EmReactionIcon.vue b/packages/frontend-embed/src/components/EmReactionIcon.vue
new file mode 100644
index 0000000000..5c38ecb0ed
--- /dev/null
+++ b/packages/frontend-embed/src/components/EmReactionIcon.vue
@@ -0,0 +1,23 @@
+<!--
+SPDX-FileCopyrightText: syuilo and misskey-project
+SPDX-License-Identifier: AGPL-3.0-only
+-->
+
+<template>
+<EmCustomEmoji v-if="reaction[0] === ':'" ref="elRef" :name="reaction" :normal="true" :noStyle="noStyle" :url="emojiUrl" :fallbackToImage="true"/>
+<EmEmoji v-else ref="elRef" :emoji="reaction" :normal="true" :noStyle="noStyle"/>
+</template>
+
+<script lang="ts" setup>
+import { } from 'vue';
+import EmCustomEmoji from './EmCustomEmoji.vue';
+import EmEmoji from './EmEmoji.vue';
+
+const props = defineProps<{
+	reaction: string;
+	noStyle?: boolean;
+	emojiUrl?: string;
+	withTooltip?: boolean;
+}>();
+
+</script>
diff --git a/packages/frontend-embed/src/components/EmReactionsViewer.reaction.vue b/packages/frontend-embed/src/components/EmReactionsViewer.reaction.vue
new file mode 100644
index 0000000000..2ebff489fd
--- /dev/null
+++ b/packages/frontend-embed/src/components/EmReactionsViewer.reaction.vue
@@ -0,0 +1,99 @@
+<!--
+SPDX-FileCopyrightText: syuilo and misskey-project
+SPDX-License-Identifier: AGPL-3.0-only
+-->
+
+<template>
+<button
+	class="_button"
+	:class="[$style.root, { [$style.reacted]: note.myReaction == reaction }]"
+>
+	<EmReactionIcon :class="$style.limitWidth" :reaction="reaction" :emojiUrl="note.reactionEmojis[reaction.substring(1, reaction.length - 1)]"/>
+	<span :class="$style.count">{{ count }}</span>
+</button>
+</template>
+
+<script lang="ts" setup>
+import { } from 'vue';
+import * as Misskey from 'misskey-js';
+import EmReactionIcon from '@/components/EmReactionIcon.vue';
+
+const props = defineProps<{
+	reaction: string;
+	count: number;
+	isInitial: boolean;
+	note: Misskey.entities.Note;
+}>();
+</script>
+
+<style lang="scss" module>
+.root {
+	display: inline-flex;
+	height: 42px;
+	margin: 2px;
+	padding: 0 6px;
+	font-size: 1.5em;
+	border-radius: 6px;
+	align-items: center;
+	justify-content: center;
+
+	&.canToggle {
+		background: var(--MI_THEME-buttonBg);
+
+		&:hover {
+			background: rgba(0, 0, 0, 0.1);
+		}
+	}
+
+	&:not(.canToggle) {
+		cursor: default;
+	}
+
+	&.small {
+		height: 32px;
+		font-size: 1em;
+		border-radius: 4px;
+
+		> .count {
+			font-size: 0.9em;
+			line-height: 32px;
+		}
+	}
+
+	&.large {
+		height: 52px;
+		font-size: 2em;
+		border-radius: 8px;
+
+		> .count {
+			font-size: 0.6em;
+			line-height: 52px;
+		}
+	}
+
+	&.reacted, &.reacted:hover {
+		background: var(--MI_THEME-accentedBg);
+		color: var(--MI_THEME-accent);
+		box-shadow: 0 0 0 1px var(--MI_THEME-accent) inset;
+
+		> .count {
+			color: var(--MI_THEME-accent);
+		}
+
+		> .icon {
+			filter: drop-shadow(0 0 2px rgba(0, 0, 0, 0.5));
+		}
+	}
+}
+
+.limitWidth {
+	max-width: 70px;
+	object-fit: contain;
+}
+
+.count {
+	font-size: 0.7em;
+	line-height: 42px;
+	margin: 0 0 0 4px;
+}
+</style>
diff --git a/packages/frontend-embed/src/components/EmReactionsViewer.vue b/packages/frontend-embed/src/components/EmReactionsViewer.vue
new file mode 100644
index 0000000000..014dd1c935
--- /dev/null
+++ b/packages/frontend-embed/src/components/EmReactionsViewer.vue
@@ -0,0 +1,104 @@
+<!--
+SPDX-FileCopyrightText: syuilo and misskey-project
+SPDX-License-Identifier: AGPL-3.0-only
+-->
+
+<template>
+<div :class="$style.root">
+	<XReaction v-for="[reaction, count] in reactions" :key="reaction" :reaction="reaction" :count="count" :isInitial="initialReactions.has(reaction)" :note="note" @reactionToggled="onMockToggleReaction"/>
+	<slot v-if="hasMoreReactions" name="more"></slot>
+</div>
+</template>
+
+<script lang="ts" setup>
+import * as Misskey from 'misskey-js';
+import { inject, watch, ref } from 'vue';
+import XReaction from '@/components/EmReactionsViewer.reaction.vue';
+
+const props = withDefaults(defineProps<{
+	note: Misskey.entities.Note;
+	maxNumber?: number;
+}>(), {
+	maxNumber: Infinity,
+});
+
+const mock = inject<boolean>('mock', false);
+
+const emit = defineEmits<{
+	(ev: 'mockUpdateMyReaction', emoji: string, delta: number): void;
+}>();
+
+const initialReactions = new Set(Object.keys(props.note.reactions));
+
+const reactions = ref<[string, number][]>([]);
+const hasMoreReactions = ref(false);
+
+if (props.note.myReaction && !Object.keys(reactions.value).includes(props.note.myReaction)) {
+	reactions.value[props.note.myReaction] = props.note.reactions[props.note.myReaction];
+}
+
+function onMockToggleReaction(emoji: string, count: number) {
+	if (!mock) return;
+
+	const i = reactions.value.findIndex((item) => item[0] === emoji);
+	if (i < 0) return;
+
+	emit('mockUpdateMyReaction', emoji, (count - reactions.value[i][1]));
+}
+
+watch([() => props.note.reactions, () => props.maxNumber], ([newSource, maxNumber]) => {
+	let newReactions: [string, number][] = [];
+	hasMoreReactions.value = Object.keys(newSource).length > maxNumber;
+
+	for (let i = 0; i < reactions.value.length; i++) {
+		const reaction = reactions.value[i][0];
+		if (reaction in newSource && newSource[reaction] !== 0) {
+			reactions.value[i][1] = newSource[reaction];
+			newReactions.push(reactions.value[i]);
+		}
+	}
+
+	const newReactionsNames = newReactions.map(([x]) => x);
+	newReactions = [
+		...newReactions,
+		...Object.entries(newSource)
+			.sort(([, a], [, b]) => b - a)
+			.filter(([y], i) => i < maxNumber && !newReactionsNames.includes(y)),
+	];
+
+	newReactions = newReactions.slice(0, props.maxNumber);
+
+	if (props.note.myReaction && !newReactions.map(([x]) => x).includes(props.note.myReaction)) {
+		newReactions.push([props.note.myReaction, newSource[props.note.myReaction]]);
+	}
+
+	reactions.value = newReactions;
+}, { immediate: true, deep: true });
+</script>
+
+<style lang="scss" module>
+.transition_x_move,
+.transition_x_enterActive,
+.transition_x_leaveActive {
+	transition: opacity 0.2s cubic-bezier(0,.5,.5,1), transform 0.2s cubic-bezier(0,.5,.5,1) !important;
+}
+.transition_x_enterFrom,
+.transition_x_leaveTo {
+	opacity: 0;
+	transform: scale(0.7);
+}
+.transition_x_leaveActive {
+	position: absolute;
+}
+
+.root {
+	display: flex;
+	flex-wrap: wrap;
+	align-items: center;
+	margin: 4px -2px 0 -2px;
+
+	&:empty {
+		display: none;
+	}
+}
+</style>
diff --git a/packages/frontend-embed/src/components/EmSubNoteContent.vue b/packages/frontend-embed/src/components/EmSubNoteContent.vue
new file mode 100644
index 0000000000..61815ddfd8
--- /dev/null
+++ b/packages/frontend-embed/src/components/EmSubNoteContent.vue
@@ -0,0 +1,114 @@
+<!--
+SPDX-FileCopyrightText: syuilo and misskey-project
+SPDX-License-Identifier: AGPL-3.0-only
+-->
+
+<template>
+<div :class="[$style.root, { [$style.collapsed]: collapsed }]">
+	<div>
+		<span v-if="note.isHidden" style="opacity: 0.5">({{ i18n.ts.private }})</span>
+		<span v-if="note.deletedAt" style="opacity: 0.5">({{ i18n.ts.deletedNote }})</span>
+		<EmA v-if="note.replyId" :class="$style.reply" :to="`/notes/${note.replyId}`"><i class="ti ti-arrow-back-up"></i></EmA>
+		<EmMfm v-if="note.text" :text="note.text" :author="note.user" :nyaize="'respect'" :emojiUrls="note.emojis"/>
+		<EmA v-if="note.renoteId" :class="$style.rp" :to="`/notes/${note.renoteId}`">RN: ...</EmA>
+	</div>
+	<details v-if="note.files && note.files.length > 0">
+		<summary>({{ i18n.tsx.withNFiles({ n: note.files.length }) }})</summary>
+		<EmMediaList :mediaList="note.files" :originalEntityUrl="`${url}/notes/${note.id}`"/>
+	</details>
+	<details v-if="note.poll">
+		<summary>{{ i18n.ts.poll }}</summary>
+		<EmPoll :noteId="note.id" :poll="note.poll"/>
+	</details>
+	<button v-if="isLong && collapsed" :class="$style.fade" class="_button" @click="collapsed = false">
+		<span :class="$style.fadeLabel">{{ i18n.ts.showMore }}</span>
+	</button>
+	<button v-else-if="isLong && !collapsed" :class="$style.showLess" class="_button" @click="collapsed = true">
+		<span :class="$style.showLessLabel">{{ i18n.ts.showLess }}</span>
+	</button>
+</div>
+</template>
+
+<script lang="ts" setup>
+import { ref } from 'vue';
+import * as Misskey from 'misskey-js';
+import EmMediaList from '@/components/EmMediaList.vue';
+import EmPoll from '@/components/EmPoll.vue';
+import { i18n } from '@/i18n.js';
+import { url } from '@@/js/config.js';
+import { shouldCollapsed } from '@@/js/collapsed.js';
+import EmA from '@/components/EmA.vue';
+import EmMfm from '@/components/EmMfm.js';
+
+const props = defineProps<{
+	note: Misskey.entities.Note;
+}>();
+
+const isLong = shouldCollapsed(props.note, []);
+
+const collapsed = ref(isLong);
+</script>
+
+<style lang="scss" module>
+.root {
+	overflow-wrap: break-word;
+
+	&.collapsed {
+		position: relative;
+		max-height: 9em;
+		overflow: clip;
+
+		> .fade {
+			display: block;
+			position: absolute;
+			bottom: 0;
+			left: 0;
+			width: 100%;
+			height: 64px;
+			background: linear-gradient(0deg, var(--MI_THEME-panel), color(from var(--MI_THEME-panel) srgb r g b / 0));
+
+			> .fadeLabel {
+				display: inline-block;
+				background: var(--MI_THEME-panel);
+				padding: 6px 10px;
+				font-size: 0.8em;
+				border-radius: 999px;
+				box-shadow: 0 2px 6px rgb(0 0 0 / 20%);
+			}
+
+			&:hover {
+				> .fadeLabel {
+					background: var(--MI_THEME-panelHighlight);
+				}
+			}
+		}
+	}
+}
+
+.reply {
+	margin-right: 6px;
+	color: var(--MI_THEME-accent);
+}
+
+.rp {
+	margin-left: 4px;
+	font-style: oblique;
+	color: var(--MI_THEME-renote);
+}
+
+.showLess {
+	width: 100%;
+	margin-top: 14px;
+	position: sticky;
+	bottom: calc(var(--MI-stickyBottom, 0px) + 14px);
+}
+
+.showLessLabel {
+	display: inline-block;
+	background: var(--MI_THEME-popup);
+	padding: 6px 10px;
+	font-size: 0.8em;
+	border-radius: 999px;
+	box-shadow: 0 2px 6px rgb(0 0 0 / 20%);
+}
+</style>
diff --git a/packages/frontend-embed/src/components/EmTime.vue b/packages/frontend-embed/src/components/EmTime.vue
new file mode 100644
index 0000000000..7902e18483
--- /dev/null
+++ b/packages/frontend-embed/src/components/EmTime.vue
@@ -0,0 +1,107 @@
+<!--
+SPDX-FileCopyrightText: syuilo and misskey-project
+SPDX-License-Identifier: AGPL-3.0-only
+-->
+
+<template>
+<time :title="absolute" :class="{ [$style.old1]: colored && (ago > 60 * 60 * 24 * 90), [$style.old2]: colored && (ago > 60 * 60 * 24 * 180) }">
+	<template v-if="invalid">{{ i18n.ts._ago.invalid }}</template>
+	<template v-else-if="mode === 'relative'">{{ relative }}</template>
+	<template v-else-if="mode === 'absolute'">{{ absolute }}</template>
+	<template v-else-if="mode === 'detail'">{{ absolute }} ({{ relative }})</template>
+</time>
+</template>
+
+<script lang="ts" setup>
+import { onMounted, onUnmounted, ref, computed } from 'vue';
+import { i18n } from '@/i18n.js';
+import { dateTimeFormat } from '@@/js/intl-const.js';
+
+const props = withDefaults(defineProps<{
+	time: Date | string | number | null;
+	origin?: Date | null;
+	mode?: 'relative' | 'absolute' | 'detail';
+	colored?: boolean;
+}>(), {
+	origin: null,
+	mode: 'relative',
+});
+
+function getDateSafe(n: Date | string | number) {
+	try {
+		if (n instanceof Date) {
+			return n;
+		}
+		return new Date(n);
+	} catch (err) {
+		return {
+			getTime: () => NaN,
+		};
+	}
+}
+
+// eslint-disable-next-line vue/no-setup-props-reactivity-loss
+const _time = props.time == null ? NaN : getDateSafe(props.time).getTime();
+const invalid = Number.isNaN(_time);
+const absolute = !invalid ? dateTimeFormat.format(_time) : i18n.ts._ago.invalid;
+
+// eslint-disable-next-line vue/no-setup-props-reactivity-loss
+const now = ref(props.origin?.getTime() ?? Date.now());
+const ago = computed(() => (now.value - _time) / 1000/*ms*/);
+
+const relative = computed<string>(() => {
+	if (props.mode === 'absolute') return ''; // absoluteではrelativeを使わないので計算しない
+	if (invalid) return i18n.ts._ago.invalid;
+
+	return (
+		ago.value >= 31536000 ? i18n.tsx._ago.yearsAgo({ n: Math.round(ago.value / 31536000).toString() }) :
+		ago.value >= 2592000 ? i18n.tsx._ago.monthsAgo({ n: Math.round(ago.value / 2592000).toString() }) :
+		ago.value >= 604800 ? i18n.tsx._ago.weeksAgo({ n: Math.round(ago.value / 604800).toString() }) :
+		ago.value >= 86400 ? i18n.tsx._ago.daysAgo({ n: Math.round(ago.value / 86400).toString() }) :
+		ago.value >= 3600 ? i18n.tsx._ago.hoursAgo({ n: Math.round(ago.value / 3600).toString() }) :
+		ago.value >= 60 ? i18n.tsx._ago.minutesAgo({ n: (~~(ago.value / 60)).toString() }) :
+		ago.value >= 10 ? i18n.tsx._ago.secondsAgo({ n: (~~(ago.value % 60)).toString() }) :
+		ago.value >= -3 ? i18n.ts._ago.justNow :
+		ago.value < -31536000 ? i18n.tsx._timeIn.years({ n: Math.round(-ago.value / 31536000).toString() }) :
+		ago.value < -2592000 ? i18n.tsx._timeIn.months({ n: Math.round(-ago.value / 2592000).toString() }) :
+		ago.value < -604800 ? i18n.tsx._timeIn.weeks({ n: Math.round(-ago.value / 604800).toString() }) :
+		ago.value < -86400 ? i18n.tsx._timeIn.days({ n: Math.round(-ago.value / 86400).toString() }) :
+		ago.value < -3600 ? i18n.tsx._timeIn.hours({ n: Math.round(-ago.value / 3600).toString() }) :
+		ago.value < -60 ? i18n.tsx._timeIn.minutes({ n: (~~(-ago.value / 60)).toString() }) :
+		i18n.tsx._timeIn.seconds({ n: (~~(-ago.value % 60)).toString() })
+	);
+});
+
+let tickId: number;
+let currentInterval: number;
+
+function tick() {
+	now.value = Date.now();
+	const nextInterval = ago.value < 60 ? 10000 : ago.value < 3600 ? 60000 : 180000;
+
+	if (currentInterval !== nextInterval) {
+		if (tickId) window.clearInterval(tickId);
+		currentInterval = nextInterval;
+		tickId = window.setInterval(tick, nextInterval);
+	}
+}
+
+if (!invalid && props.origin === null && (props.mode === 'relative' || props.mode === 'detail')) {
+	onMounted(() => {
+		tick();
+	});
+	onUnmounted(() => {
+		if (tickId) window.clearInterval(tickId);
+	});
+}
+</script>
+
+<style lang="scss" module>
+.old1 {
+	color: var(--MI_THEME-warn);
+}
+
+.old1.old2 {
+	color: var(--MI_THEME-error);
+}
+</style>
diff --git a/packages/frontend-embed/src/components/EmTimelineContainer.vue b/packages/frontend-embed/src/components/EmTimelineContainer.vue
new file mode 100644
index 0000000000..60fd67ced9
--- /dev/null
+++ b/packages/frontend-embed/src/components/EmTimelineContainer.vue
@@ -0,0 +1,39 @@
+<!--
+SPDX-FileCopyrightText: syuilo and misskey-project
+SPDX-License-Identifier: AGPL-3.0-only
+-->
+
+<template>
+<div :class="$style.timelineRoot">
+	<div v-if="showHeader" :class="$style.header"><slot name="header"></slot></div>
+	<div :class="$style.body"><slot name="body"></slot></div>
+</div>
+</template>
+
+<script setup lang="ts">
+withDefaults(defineProps<{
+	showHeader?: boolean;
+}>(), {
+	showHeader: true,
+});
+</script>
+
+<style module lang="scss">
+.timelineRoot {
+	background-color: var(--MI_THEME-panel);
+	height: 100%;
+	max-height: var(--embedMaxHeight, none);
+	display: flex;
+	flex-direction: column;
+}
+
+.header {
+	flex-shrink: 0;
+	border-bottom: 1px solid var(--MI_THEME-divider);
+}
+
+.body {
+	flex-grow: 1;
+	overflow-y: auto;
+}
+</style>
diff --git a/packages/frontend-embed/src/components/EmUrl.vue b/packages/frontend-embed/src/components/EmUrl.vue
new file mode 100644
index 0000000000..94424cab28
--- /dev/null
+++ b/packages/frontend-embed/src/components/EmUrl.vue
@@ -0,0 +1,96 @@
+<!--
+SPDX-FileCopyrightText: syuilo and misskey-project
+SPDX-License-Identifier: AGPL-3.0-only
+-->
+
+<template>
+<component
+	:is="self ? EmA : 'a'" ref="el" :class="$style.root" class="_link" :[attr]="self ? props.url.substring(local.length) : props.url" :rel="rel ?? 'nofollow noopener'" :target="target"
+	@contextmenu.stop="() => {}"
+>
+	<template v-if="!self">
+		<span :class="$style.schema">{{ schema }}//</span>
+		<span :class="$style.hostname">{{ hostname }}</span>
+		<span v-if="port != ''">:{{ port }}</span>
+	</template>
+	<template v-if="pathname === '/' && self">
+		<span :class="$style.self">{{ hostname }}</span>
+	</template>
+	<span v-if="pathname != ''" :class="$style.pathname">{{ self ? pathname.substring(1) : pathname }}</span>
+	<span :class="$style.query">{{ query }}</span>
+	<span :class="$style.hash">{{ hash }}</span>
+	<i v-if="target === '_blank'" :class="$style.icon" class="ti ti-external-link"></i>
+</component>
+</template>
+
+<script lang="ts" setup>
+import { ref } from 'vue';
+import { toUnicode as decodePunycode } from 'punycode/';
+import EmA from './EmA.vue';
+import { url as local } from '@@/js/config.js';
+
+function safeURIDecode(str: string): string {
+	try {
+		return decodeURIComponent(str);
+	} catch {
+		return str;
+	}
+}
+
+const props = withDefaults(defineProps<{
+	url: string;
+	rel?: string;
+	showUrlPreview?: boolean;
+}>(), {
+	showUrlPreview: true,
+});
+
+const self = props.url.startsWith(local);
+const url = new URL(props.url);
+if (!['http:', 'https:'].includes(url.protocol)) throw new Error('invalid url');
+const el = ref();
+
+const schema = url.protocol;
+const hostname = decodePunycode(url.hostname);
+const port = url.port;
+const pathname = safeURIDecode(url.pathname);
+const query = safeURIDecode(url.search);
+const hash = safeURIDecode(url.hash);
+const attr = self ? 'to' : 'href';
+const target = self ? null : '_blank';
+</script>
+
+<style lang="scss" module>
+.root {
+	word-break: break-all;
+}
+
+.icon {
+	padding-left: 2px;
+	font-size: .9em;
+}
+
+.self {
+	font-weight: bold;
+}
+
+.schema {
+	opacity: 0.5;
+}
+
+.hostname {
+	font-weight: bold;
+}
+
+.pathname {
+	opacity: 0.8;
+}
+
+.query {
+	opacity: 0.5;
+}
+
+.hash {
+	font-style: italic;
+}
+</style>
diff --git a/packages/frontend-embed/src/components/EmUserName.vue b/packages/frontend-embed/src/components/EmUserName.vue
new file mode 100644
index 0000000000..c0c7c443ca
--- /dev/null
+++ b/packages/frontend-embed/src/components/EmUserName.vue
@@ -0,0 +1,21 @@
+<!--
+SPDX-FileCopyrightText: syuilo and misskey-project
+SPDX-License-Identifier: AGPL-3.0-only
+-->
+
+<template>
+<EmMfm :text="user.name ?? user.username" :author="user" :plain="true" :nowrap="nowrap" :emojiUrls="user.emojis"/>
+</template>
+
+<script lang="ts" setup>
+import { } from 'vue';
+import * as Misskey from 'misskey-js';
+import EmMfm from './EmMfm.js';
+
+const props = withDefaults(defineProps<{
+	user: Misskey.entities.User;
+	nowrap?: boolean;
+}>(), {
+	nowrap: true,
+});
+</script>
diff --git a/packages/frontend-embed/src/components/I18n.vue b/packages/frontend-embed/src/components/I18n.vue
new file mode 100644
index 0000000000..b621110ec9
--- /dev/null
+++ b/packages/frontend-embed/src/components/I18n.vue
@@ -0,0 +1,51 @@
+<!--
+SPDX-FileCopyrightText: syuilo and misskey-project
+SPDX-License-Identifier: AGPL-3.0-only
+-->
+
+<template>
+<render/>
+</template>
+
+<script setup lang="ts" generic="T extends string | ParameterizedString">
+import { computed, h } from 'vue';
+import type { ParameterizedString } from '../../../../locales/index.js';
+
+const props = withDefaults(defineProps<{
+	src: T;
+	tag?: string;
+	// eslint-disable-next-line vue/require-default-prop
+	textTag?: string;
+}>(), {
+	tag: 'span',
+});
+
+const slots = defineSlots<T extends ParameterizedString<infer R> ? { [K in R]: () => unknown } : NonNullable<unknown>>();
+
+const parsed = computed(() => {
+	let str = props.src as string;
+	const value: (string | { arg: string; })[] = [];
+	for (;;) {
+		const nextBracketOpen = str.indexOf('{');
+		const nextBracketClose = str.indexOf('}');
+
+		if (nextBracketOpen === -1) {
+			value.push(str);
+			break;
+		} else {
+			if (nextBracketOpen > 0) value.push(str.substring(0, nextBracketOpen));
+			value.push({
+				arg: str.substring(nextBracketOpen + 1, nextBracketClose),
+			});
+		}
+
+		str = str.substring(nextBracketClose + 1);
+	}
+
+	return value;
+});
+
+const render = () => {
+	return h(props.tag, parsed.value.map(x => typeof x === 'string' ? (props.textTag ? h(props.textTag, x) : x) : slots[x.arg]()));
+};
+</script>
diff --git a/packages/frontend-embed/src/custom-emojis.ts b/packages/frontend-embed/src/custom-emojis.ts
new file mode 100644
index 0000000000..d5b40885c1
--- /dev/null
+++ b/packages/frontend-embed/src/custom-emojis.ts
@@ -0,0 +1,61 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { shallowRef, watch } from 'vue';
+import * as Misskey from 'misskey-js';
+import { misskeyApi, misskeyApiGet } from '@/misskey-api.js';
+
+function get(key: string) {
+	const value = localStorage.getItem(key);
+	if (value === null) return null;
+	return JSON.parse(value);
+}
+
+function set(key: string, value: any) {
+	localStorage.setItem(key, JSON.stringify(value));
+}
+
+const storageCache = await get('emojis');
+export const customEmojis = shallowRef<Misskey.entities.EmojiSimple[]>(Array.isArray(storageCache) ? storageCache : []);
+
+export const customEmojisMap = new Map<string, Misskey.entities.EmojiSimple>();
+watch(customEmojis, emojis => {
+	customEmojisMap.clear();
+	for (const emoji of emojis) {
+		customEmojisMap.set(emoji.name, emoji);
+	}
+}, { immediate: true });
+
+export async function fetchCustomEmojis(force = false) {
+	const now = Date.now();
+
+	let res;
+	if (force) {
+		res = await misskeyApi('emojis', {});
+	} else {
+		const lastFetchedAt = await get('lastEmojisFetchedAt');
+		if (lastFetchedAt && (now - lastFetchedAt) < 1000 * 60 * 60) return;
+		res = await misskeyApiGet('emojis', {});
+	}
+
+	customEmojis.value = res.emojis;
+	set('emojis', res.emojis);
+	set('lastEmojisFetchedAt', now);
+}
+
+let cachedTags;
+export function getCustomEmojiTags() {
+	if (cachedTags) return cachedTags;
+
+	const tags = new Set();
+	for (const emoji of customEmojis.value) {
+		for (const tag of emoji.aliases) {
+			tags.add(tag);
+		}
+	}
+	const res = Array.from(tags);
+	cachedTags = res;
+	return res;
+}
diff --git a/packages/frontend-embed/src/di.ts b/packages/frontend-embed/src/di.ts
new file mode 100644
index 0000000000..22f6276630
--- /dev/null
+++ b/packages/frontend-embed/src/di.ts
@@ -0,0 +1,17 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import type { InjectionKey } from 'vue';
+import * as Misskey from 'misskey-js';
+import { MediaProxy } from '@@/js/media-proxy.js';
+import type { ParsedEmbedParams } from '@@/js/embed-page.js';
+import type { ServerContext } from '@/server-context.js';
+
+export const DI = {
+	serverMetadata: Symbol() as InjectionKey<Misskey.entities.MetaDetailed>,
+	embedParams: Symbol() as InjectionKey<ParsedEmbedParams>,
+	serverContext: Symbol() as InjectionKey<ServerContext>,
+	mediaProxy: Symbol() as InjectionKey<MediaProxy>,
+};
diff --git a/packages/frontend-embed/src/i18n.ts b/packages/frontend-embed/src/i18n.ts
new file mode 100644
index 0000000000..6ad503b089
--- /dev/null
+++ b/packages/frontend-embed/src/i18n.ts
@@ -0,0 +1,15 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { markRaw } from 'vue';
+import { I18n } from '@@/js/i18n.js';
+import type { Locale } from '../../../locales/index.js';
+import { locale } from '@@/js/config.js';
+
+export const i18n = markRaw(new I18n<Locale>(locale, _DEV_));
+
+export function updateI18n(newLocale: Locale) {
+	i18n.locale = newLocale;
+}
diff --git a/packages/frontend-embed/src/index.html b/packages/frontend-embed/src/index.html
new file mode 100644
index 0000000000..47b0b0e84e
--- /dev/null
+++ b/packages/frontend-embed/src/index.html
@@ -0,0 +1,36 @@
+<!--
+  SPDX-FileCopyrightText: syuilo and misskey-project
+  SPDX-License-Identifier: AGPL-3.0-only
+-->
+
+<!--
+  開発モードのviteはこのファイルを起点にサーバーを起動します。
+  このファイルに書かれた [t]js のリンクと (s)cssのリンクと、その依存関係にあるファイルはビルドされます
+-->
+
+<!DOCTYPE html>
+<html>
+<head>
+	<meta charset="UTF-8" />
+	<title>[DEV] Loading...</title>
+	<!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
+	<meta
+		http-equiv="Content-Security-Policy"
+		content="default-src 'self' https://newassets.hcaptcha.com/ https://challenges.cloudflare.com/ http://localhost:7493/;
+			worker-src 'self';
+			script-src 'self' 'unsafe-eval' https://*.hcaptcha.com https://challenges.cloudflare.com https://esm.sh;
+			style-src 'self' 'unsafe-inline';
+			img-src 'self' data: blob: www.google.com xn--931a.moe localhost:3000 localhost:5173 127.0.0.1:5173 127.0.0.1:3000;
+			media-src 'self' localhost:3000 localhost:5173 127.0.0.1:5173 127.0.0.1:3000;
+			connect-src 'self' localhost:3000 localhost:5173 127.0.0.1:5173 127.0.0.1:3000 https://newassets.hcaptcha.com;
+			frame-src *;"
+	/>
+	<meta property="og:site_name" content="[DEV BUILD] Misskey" />
+	<meta name="viewport" content="width=device-width, initial-scale=1">
+</head>
+
+<body>
+<div id="misskey_app"></div>
+<script type="module" src="./boot.ts"></script>
+</body>
+</html>
diff --git a/packages/frontend-embed/src/misskey-api.ts b/packages/frontend-embed/src/misskey-api.ts
new file mode 100644
index 0000000000..0d3c679359
--- /dev/null
+++ b/packages/frontend-embed/src/misskey-api.ts
@@ -0,0 +1,99 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import * as Misskey from 'misskey-js';
+import { ref } from 'vue';
+import { apiUrl } from '@@/js/config.js';
+
+export const pendingApiRequestsCount = ref(0);
+
+// Implements Misskey.api.ApiClient.request
+export function misskeyApi<
+	ResT = void,
+	E extends keyof Misskey.Endpoints = keyof Misskey.Endpoints,
+	P extends Misskey.Endpoints[E]['req'] = Misskey.Endpoints[E]['req'],
+	_ResT = ResT extends void ? Misskey.api.SwitchCaseResponseType<E, P> : ResT,
+>(
+	endpoint: E,
+	data: P = {} as any,
+	signal?: AbortSignal,
+): Promise<_ResT> {
+	if (endpoint.includes('://')) throw new Error('invalid endpoint');
+	pendingApiRequestsCount.value++;
+
+	const onFinally = () => {
+		pendingApiRequestsCount.value--;
+	};
+
+	const promise = new Promise<_ResT>((resolve, reject) => {
+		// Send request
+		window.fetch(`${apiUrl}/${endpoint}`, {
+			method: 'POST',
+			body: JSON.stringify(data),
+			credentials: 'omit',
+			cache: 'no-cache',
+			headers: {
+				'Content-Type': 'application/json',
+			},
+			signal,
+		}).then(async (res) => {
+			const body = res.status === 204 ? null : await res.json();
+
+			if (res.status === 200) {
+				resolve(body);
+			} else if (res.status === 204) {
+				resolve(undefined as _ResT); // void -> undefined
+			} else {
+				reject(body.error);
+			}
+		}).catch(reject);
+	});
+
+	promise.then(onFinally, onFinally);
+
+	return promise;
+}
+
+// Implements Misskey.api.ApiClient.request
+export function misskeyApiGet<
+	ResT = void,
+	E extends keyof Misskey.Endpoints = keyof Misskey.Endpoints,
+	P extends Misskey.Endpoints[E]['req'] = Misskey.Endpoints[E]['req'],
+	_ResT = ResT extends void ? Misskey.api.SwitchCaseResponseType<E, P> : ResT,
+>(
+	endpoint: E,
+	data: P = {} as any,
+): Promise<_ResT> {
+	pendingApiRequestsCount.value++;
+
+	const onFinally = () => {
+		pendingApiRequestsCount.value--;
+	};
+
+	const query = new URLSearchParams(data as any);
+
+	const promise = new Promise<_ResT>((resolve, reject) => {
+		// Send request
+		window.fetch(`${apiUrl}/${endpoint}?${query}`, {
+			method: 'GET',
+			credentials: 'omit',
+			cache: 'default',
+		}).then(async (res) => {
+			const body = res.status === 204 ? null : await res.json();
+
+			if (res.status === 200) {
+				resolve(body);
+			} else if (res.status === 204) {
+				resolve(undefined as _ResT); // void -> undefined
+			} else {
+				reject(body.error);
+			}
+		}).catch(reject);
+	});
+
+	promise.then(onFinally, onFinally);
+
+	return promise;
+}
diff --git a/packages/frontend-embed/src/pages/clip.vue b/packages/frontend-embed/src/pages/clip.vue
new file mode 100644
index 0000000000..f4d4e8cf6f
--- /dev/null
+++ b/packages/frontend-embed/src/pages/clip.vue
@@ -0,0 +1,143 @@
+<!--
+SPDX-FileCopyrightText: syuilo and misskey-project
+SPDX-License-Identifier: AGPL-3.0-only
+-->
+
+<template>
+<div>
+	<EmTimelineContainer v-if="clip" :showHeader="embedParams.header">
+		<template #header>
+			<div :class="$style.clipHeader">
+				<div :class="$style.headerClipIconRoot">
+					<i class="ti ti-paperclip"></i>
+				</div>
+				<div :class="$style.headerTitle" @click="top">
+					<div class="_nowrap"><a :href="`/clips/${clip.id}`" target="_blank" rel="noopener">{{ clip.name }}</a></div>
+					<div :class="$style.sub">{{ i18n.tsx.fromX({ x: instanceName }) }}</div>
+				</div>
+				<a :href="url" :class="$style.instanceIconLink" target="_blank" rel="noopener noreferrer">
+					<img
+						:class="$style.instanceIcon"
+						:src="serverMetadata.iconUrl || '/favicon.ico'"
+					/>
+				</a>
+			</div>
+		</template>
+		<template #body>
+			<EmNotes
+				ref="notesEl"
+				:pagination="pagination"
+				:disableAutoLoad="!embedParams.autoload"
+				:noGap="true"
+				:ad="false"
+			/>
+		</template>
+	</EmTimelineContainer>
+	<XNotFound v-else/>
+</div>
+</template>
+
+<script setup lang="ts">
+import { ref, computed, inject, useTemplateRef } from 'vue';
+import * as Misskey from 'misskey-js';
+import { scrollToTop } from '@@/js/scroll.js';
+import { url, instanceName } from '@@/js/config.js';
+import { isLink } from '@@/js/is-link.js';
+import { defaultEmbedParams } from '@@/js/embed-page.js';
+import type { Paging } from '@/components/EmPagination.vue';
+import EmNotes from '@/components/EmNotes.vue';
+import XNotFound from '@/pages/not-found.vue';
+import EmTimelineContainer from '@/components/EmTimelineContainer.vue';
+import { misskeyApi } from '@/misskey-api.js';
+import { i18n } from '@/i18n.js';
+import { assertServerContext } from '@/server-context.js';
+import { DI } from '@/di.js';
+
+const props = defineProps<{
+	clipId: string;
+}>();
+
+const embedParams = inject(DI.embedParams, defaultEmbedParams);
+
+const serverMetadata = inject(DI.serverMetadata)!;
+
+const serverContext = inject(DI.serverContext)!;
+
+const clip = ref<Misskey.entities.Clip | null>();
+
+if (assertServerContext(serverContext, 'clip')) {
+	clip.value = serverContext.clip;
+} else {
+	clip.value = await misskeyApi('clips/show', {
+		clipId: props.clipId,
+	}).catch(() => {
+		return null;
+	});
+}
+
+const pagination = computed(() => ({
+	endpoint: 'clips/notes',
+	params: {
+		clipId: props.clipId,
+	},
+} as Paging));
+
+const notesEl = useTemplateRef('notesEl');
+
+function top(ev: MouseEvent) {
+	const target = ev.target as HTMLElement | null;
+	if (target && isLink(target)) return;
+
+	if (notesEl.value) {
+		scrollToTop(notesEl.value.$el as HTMLElement, { behavior: 'smooth' });
+	}
+}
+</script>
+
+<style lang="scss" module>
+.clipHeader {
+	padding: 8px 16px;
+	display: flex;
+	min-width: 0;
+	align-items: center;
+	gap: var(--MI-margin);
+	overflow: hidden;
+
+	.headerClipIconRoot {
+		flex-shrink: 0;
+		width: 32px;
+		height: 32px;
+		line-height: 32px;
+		font-size: 14px;
+		text-align: center;
+		background-color: var(--MI_THEME-accentedBg);
+		color: var(--MI_THEME-accent);
+		border-radius: 50%;
+	}
+
+	.headerTitle {
+		flex-grow: 1;
+		font-weight: 700;
+		line-height: 1.1;
+		min-width: 0;
+
+		.sub {
+			font-size: 0.8em;
+			font-weight: 400;
+			opacity: 0.7;
+		}
+	}
+
+	.instanceIconLink {
+		flex-shrink: 0;
+		display: block;
+		margin-left: auto;
+		height: 24px;
+	}
+
+	.instanceIcon {
+		height: 24px;
+		border-radius: 3px;
+	}
+}
+</style>
diff --git a/packages/frontend-embed/src/pages/not-found.vue b/packages/frontend-embed/src/pages/not-found.vue
new file mode 100644
index 0000000000..bbb03b4e64
--- /dev/null
+++ b/packages/frontend-embed/src/pages/not-found.vue
@@ -0,0 +1,24 @@
+<!--
+SPDX-FileCopyrightText: syuilo and misskey-project
+SPDX-License-Identifier: AGPL-3.0-only
+-->
+
+<template>
+<div>
+	<div class="_fullinfo">
+		<img :src="notFoundImageUrl" class="_ghost"/>
+		<div>{{ i18n.ts.notFoundDescription }}</div>
+	</div>
+</div>
+</template>
+
+<script lang="ts" setup>
+import { inject, computed } from 'vue';
+import { DEFAULT_NOT_FOUND_IMAGE_URL } from '@@/js/const.js';
+import { DI } from '@/di.js';
+import { i18n } from '@/i18n.js';
+
+const serverMetadata = inject(DI.serverMetadata)!;
+
+const notFoundImageUrl = computed(() => serverMetadata?.notFoundImageUrl ?? DEFAULT_NOT_FOUND_IMAGE_URL);
+</script>
diff --git a/packages/frontend-embed/src/pages/note.vue b/packages/frontend-embed/src/pages/note.vue
new file mode 100644
index 0000000000..e879430286
--- /dev/null
+++ b/packages/frontend-embed/src/pages/note.vue
@@ -0,0 +1,52 @@
+<!--
+SPDX-FileCopyrightText: syuilo and misskey-project
+SPDX-License-Identifier: AGPL-3.0-only
+-->
+
+<template>
+<div :class="$style.noteEmbedRoot">
+	<EmNoteDetailed v-if="note && !prohibited" :note="note"/>
+	<XNotFound v-else/>
+</div>
+</template>
+
+<script setup lang="ts">
+import { inject, ref } from 'vue';
+import * as Misskey from 'misskey-js';
+import EmNoteDetailed from '@/components/EmNoteDetailed.vue';
+import XNotFound from '@/pages/not-found.vue';
+import { DI } from '@/di.js';
+import { misskeyApi } from '@/misskey-api.js';
+import { assertServerContext } from '@/server-context';
+
+const props = defineProps<{
+	noteId: string;
+}>();
+
+const serverContext = inject(DI.serverContext)!;
+
+const note = ref<Misskey.entities.Note | null>(null);
+
+const prohibited = ref(false);
+
+if (assertServerContext(serverContext, 'note')) {
+	note.value = serverContext.note;
+} else {
+	note.value = await misskeyApi('notes/show', {
+		noteId: props.noteId,
+	}).catch(() => {
+		return null;
+	});
+}
+
+if (note.value?.url != null || note.value?.uri != null) {
+	// リモートサーバーのノートは弾く
+	prohibited.value = true;
+}
+</script>
+
+<style lang="scss" module>
+.noteEmbedRoot {
+	background-color: var(--MI_THEME-panel);
+}
+</style>
diff --git a/packages/frontend-embed/src/pages/tag.vue b/packages/frontend-embed/src/pages/tag.vue
new file mode 100644
index 0000000000..4b00ae7c2d
--- /dev/null
+++ b/packages/frontend-embed/src/pages/tag.vue
@@ -0,0 +1,126 @@
+<!--
+SPDX-FileCopyrightText: syuilo and misskey-project
+SPDX-License-Identifier: AGPL-3.0-only
+-->
+
+<template>
+<div>
+	<EmTimelineContainer v-if="tag" :showHeader="embedParams.header">
+		<template #header>
+			<div :class="$style.clipHeader">
+				<div :class="$style.headerClipIconRoot">
+					<i class="ti ti-hash"></i>
+				</div>
+				<div :class="$style.headerTitle" @click="top">
+					<div class="_nowrap"><a :href="`/tags/${tag}`" target="_blank" rel="noopener">#{{ tag }}</a></div>
+					<div :class="$style.sub">{{ i18n.tsx.fromX({ x: instanceName }) }}</div>
+				</div>
+				<a :href="url" :class="$style.instanceIconLink" target="_blank" rel="noopener noreferrer">
+					<img
+						:class="$style.instanceIcon"
+						:src="serverMetadata.iconUrl || '/favicon.ico'"
+					/>
+				</a>
+			</div>
+		</template>
+		<template #body>
+			<EmNotes
+				ref="notesEl"
+				:pagination="pagination"
+				:disableAutoLoad="!embedParams.autoload"
+				:noGap="true"
+				:ad="false"
+			/>
+		</template>
+	</EmTimelineContainer>
+	<XNotFound v-else/>
+</div>
+</template>
+
+<script setup lang="ts">
+import { computed, inject, useTemplateRef } from 'vue';
+import { scrollToTop } from '@@/js/scroll.js';
+import type { Paging } from '@/components/EmPagination.vue';
+import EmNotes from '@/components/EmNotes.vue';
+import XNotFound from '@/pages/not-found.vue';
+import EmTimelineContainer from '@/components/EmTimelineContainer.vue';
+import { i18n } from '@/i18n.js';
+import { url, instanceName } from '@@/js/config.js';
+import { isLink } from '@@/js/is-link.js';
+import { DI } from '@/di.js';
+import { defaultEmbedParams } from '@@/js/embed-page.js';
+
+const props = defineProps<{
+	tag: string;
+}>();
+
+const serverMetadata = inject(DI.serverMetadata)!;
+
+const embedParams = inject(DI.embedParams, defaultEmbedParams);
+
+const pagination = computed(() => ({
+	endpoint: 'notes/search-by-tag',
+	params: {
+		tag: props.tag,
+	},
+} as Paging));
+
+const notesEl = useTemplateRef('notesEl');
+
+function top(ev: MouseEvent) {
+	const target = ev.target as HTMLElement | null;
+	if (target && isLink(target)) return;
+
+	if (notesEl.value) {
+		scrollToTop(notesEl.value.$el as HTMLElement, { behavior: 'smooth' });
+	}
+}
+</script>
+
+<style lang="scss" module>
+.clipHeader {
+	padding: 8px 16px;
+	display: flex;
+	min-width: 0;
+	align-items: center;
+	gap: var(--MI-margin);
+	overflow: hidden;
+
+	.headerClipIconRoot {
+		flex-shrink: 0;
+		width: 32px;
+		height: 32px;
+		line-height: 32px;
+		font-size: 14px;
+		text-align: center;
+		background-color: var(--MI_THEME-accentedBg);
+		color: var(--MI_THEME-accent);
+		border-radius: 50%;
+	}
+
+	.headerTitle {
+		flex-grow: 1;
+		font-weight: 700;
+		line-height: 1.1;
+		min-width: 0;
+
+		.sub {
+			font-size: 0.8em;
+			font-weight: 400;
+			opacity: 0.7;
+		}
+	}
+
+	.instanceIconLink {
+		flex-shrink: 0;
+		display: block;
+		margin-left: auto;
+		height: 24px;
+	}
+
+	.instanceIcon {
+		height: 24px;
+		border-radius: 3px;
+	}
+}
+</style>
diff --git a/packages/frontend-embed/src/pages/user-timeline.vue b/packages/frontend-embed/src/pages/user-timeline.vue
new file mode 100644
index 0000000000..348b1a7622
--- /dev/null
+++ b/packages/frontend-embed/src/pages/user-timeline.vue
@@ -0,0 +1,158 @@
+<!--
+SPDX-FileCopyrightText: syuilo and misskey-project
+SPDX-License-Identifier: AGPL-3.0-only
+-->
+
+<template>
+<div>
+	<EmTimelineContainer v-if="user && !prohibited" :showHeader="embedParams.header">
+		<template #header>
+			<div :class="$style.userHeader">
+				<a :href="`/@${user.username}`" target="_blank" rel="noopener noreferrer" :class="$style.avatarLink">
+					<EmAvatar :class="$style.avatar" :user="user"/>
+				</a>
+				<div :class="$style.headerTitle" @click="top">
+					<I18n :src="i18n.ts.noteOf" tag="div" class="_nowrap">
+						<template #user>
+							<a v-if="user != null" :href="`/@${user.username}`" target="_blank" rel="noopener noreferrer">
+								<EmUserName :user="user"/>
+							</a>
+							<span v-else>{{ i18n.ts.user }}</span>
+						</template>
+					</I18n>
+					<div :class="$style.sub">{{ i18n.tsx.fromX({ x: instanceName }) }}</div>
+				</div>
+				<a :href="url" :class="$style.instanceIconLink" target="_blank" rel="noopener noreferrer">
+					<img
+						:class="$style.instanceIcon"
+						:src="serverMetadata.iconUrl || '/favicon.ico'"
+					/>
+				</a>
+			</div>
+		</template>
+		<template #body>
+			<EmNotes
+				ref="notesEl"
+				:pagination="pagination"
+				:disableAutoLoad="!embedParams.autoload"
+				:noGap="true"
+				:ad="false"
+			/>
+		</template>
+	</EmTimelineContainer>
+	<XNotFound v-else/>
+</div>
+</template>
+
+<script setup lang="ts">
+import { ref, computed, inject, useTemplateRef } from 'vue';
+import * as Misskey from 'misskey-js';
+import { url, instanceName } from '@@/js/config.js';
+import { defaultEmbedParams } from '@@/js/embed-page.js';
+import type { Paging } from '@/components/EmPagination.vue';
+import EmNotes from '@/components/EmNotes.vue';
+import EmAvatar from '@/components/EmAvatar.vue';
+import EmUserName from '@/components/EmUserName.vue';
+import I18n from '@/components/I18n.vue';
+import XNotFound from '@/pages/not-found.vue';
+import EmTimelineContainer from '@/components/EmTimelineContainer.vue';
+import { scrollToTop } from '@@/js/scroll.js';
+import { isLink } from '@@/js/is-link.js';
+import { misskeyApi } from '@/misskey-api.js';
+import { i18n } from '@/i18n.js';
+import { assertServerContext } from '@/server-context.js';
+import { DI } from '@/di.js';
+
+const props = defineProps<{
+	userId: string;
+}>();
+
+const embedParams = inject(DI.embedParams, defaultEmbedParams);
+
+const serverMetadata = inject(DI.serverMetadata)!;
+
+const serverContext = inject(DI.serverContext)!;
+
+const user = ref<Misskey.entities.UserLite | null>();
+
+const prohibited = ref(false);
+
+if (assertServerContext(serverContext, 'user')) {
+	user.value = serverContext.user;
+} else {
+	user.value = await misskeyApi('users/show', {
+		userId: props.userId,
+	}).catch(() => {
+		return null;
+	});
+}
+
+if (user.value?.host != null) {
+	// リモートサーバーのユーザーは弾く
+	prohibited.value = true;
+}
+
+const pagination = computed(() => ({
+	endpoint: 'users/notes',
+	params: {
+		userId: user.value?.id,
+	},
+} as Paging));
+
+const notesEl = useTemplateRef('notesEl');
+
+function top(ev: MouseEvent) {
+	const target = ev.target as HTMLElement | null;
+	if (target && isLink(target)) return;
+
+	if (notesEl.value) {
+		scrollToTop(notesEl.value.$el as HTMLElement, { behavior: 'smooth' });
+	}
+}
+</script>
+
+<style lang="scss" module>
+.userHeader {
+	padding: 8px 16px;
+	display: flex;
+	min-width: 0;
+	align-items: center;
+	gap: var(--MI-margin);
+	overflow: hidden;
+
+	.avatarLink {
+		display: block;
+	}
+
+	.avatar {
+		display: inline-block;
+		width: 32px;
+		height: 32px;
+	}
+
+	.headerTitle {
+		flex-grow: 1;
+		font-weight: 700;
+		line-height: 1.1;
+		min-width: 0;
+
+		.sub {
+			font-size: 0.8em;
+			font-weight: 400;
+			opacity: 0.7;
+		}
+	}
+
+	.instanceIconLink {
+		flex-shrink: 0;
+		display: block;
+		margin-left: auto;
+		height: 24px;
+	}
+
+	.instanceIcon {
+		height: 24px;
+		border-radius: 3px;
+	}
+}
+</style>
diff --git a/packages/frontend-embed/src/post-message.ts b/packages/frontend-embed/src/post-message.ts
new file mode 100644
index 0000000000..fd8eb8a5d2
--- /dev/null
+++ b/packages/frontend-embed/src/post-message.ts
@@ -0,0 +1,49 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+export const postMessageEventTypes = [
+	'misskey:embed:ready',
+	'misskey:embed:changeHeight',
+] as const;
+
+export type PostMessageEventType = typeof postMessageEventTypes[number];
+
+export interface PostMessageEventPayload extends Record<PostMessageEventType, any> {
+	'misskey:embed:ready': undefined;
+	'misskey:embed:changeHeight': {
+		height: number;
+	};
+}
+
+export type MiPostMessageEvent<T extends PostMessageEventType = PostMessageEventType> = {
+	type: T;
+	iframeId?: string;
+	payload?: PostMessageEventPayload[T];
+}
+
+let defaultIframeId: string | null = null;
+
+export function setIframeId(id: string): void {
+	if (defaultIframeId != null) return;
+
+	if (_DEV_) console.log('setIframeId', id);
+	defaultIframeId = id;
+}
+
+/**
+ * 親フレームにイベントを送信
+ */
+export function postMessageToParentWindow<T extends PostMessageEventType = PostMessageEventType>(type: T, payload?: PostMessageEventPayload[T], iframeId: string | null = null): void {
+	let _iframeId = iframeId;
+	if (_iframeId == null) {
+		_iframeId = defaultIframeId;
+	}
+	if (_DEV_) console.log('postMessageToParentWindow', type, _iframeId, payload);
+	window.parent.postMessage({
+		type,
+		iframeId: _iframeId,
+		payload,
+	}, '*');
+}
diff --git a/packages/frontend-embed/src/server-context.ts b/packages/frontend-embed/src/server-context.ts
new file mode 100644
index 0000000000..a84a1a726a
--- /dev/null
+++ b/packages/frontend-embed/src/server-context.ts
@@ -0,0 +1,21 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+import * as Misskey from 'misskey-js';
+
+const providedContextEl = document.getElementById('misskey_embedCtx');
+
+export type ServerContext = {
+	clip?: Misskey.entities.Clip;
+	note?: Misskey.entities.Note;
+	user?: Misskey.entities.UserLite;
+} | null;
+
+// NOTE: devモードのときしか embedCtx が null になることは無い
+export const serverContext: ServerContext = (providedContextEl && providedContextEl.textContent) ? JSON.parse(providedContextEl.textContent) : null;
+
+export function assertServerContext<K extends keyof NonNullable<ServerContext>>(ctx: ServerContext, entity: K): ctx is Required<Pick<NonNullable<ServerContext>, K>> {
+	if (ctx == null) return false;
+	return entity in ctx;
+}
diff --git a/packages/frontend-embed/src/server-metadata.ts b/packages/frontend-embed/src/server-metadata.ts
new file mode 100644
index 0000000000..6c94aacd48
--- /dev/null
+++ b/packages/frontend-embed/src/server-metadata.ts
@@ -0,0 +1,16 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import * as Misskey from 'misskey-js';
+import { misskeyApi } from '@/misskey-api.js';
+
+const providedMetaEl = document.getElementById('misskey_meta');
+
+const _serverMetadata: Misskey.entities.MetaDetailed | null = (providedMetaEl && providedMetaEl.textContent) ? JSON.parse(providedMetaEl.textContent) : null;
+
+// NOTE: devモードのときしか _serverMetadata が null になることは無い
+export const serverMetadata: Misskey.entities.MetaDetailed = _serverMetadata ?? await misskeyApi('meta', {
+	detail: true,
+});
diff --git a/packages/frontend-embed/src/style.scss b/packages/frontend-embed/src/style.scss
new file mode 100644
index 0000000000..2e43cfd20a
--- /dev/null
+++ b/packages/frontend-embed/src/style.scss
@@ -0,0 +1,453 @@
+@charset "utf-8";
+
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+:root {
+	--MI-radius: 12px;
+	--MI-marginFull: 14px;
+	--MI-marginHalf: 10px;
+
+	--MI-margin: var(--MI-marginFull);
+}
+
+html {
+	background-color: transparent;
+	color-scheme: light dark;
+	color: var(--MI_THEME-fg);
+	accent-color: var(--MI_THEME-accent);
+	overflow: clip;
+	overflow-wrap: break-word;
+	font-family: 'Hiragino Maru Gothic Pro', "BIZ UDGothic", Roboto, HelveticaNeue, Arial, sans-serif;
+	font-size: 14px;
+	line-height: 1.35;
+	text-size-adjust: 100%;
+	tab-size: 2;
+	-webkit-text-size-adjust: 100%;
+
+	&, * {
+		scrollbar-color: var(--MI_THEME-scrollbarHandle) transparent;
+		scrollbar-width: thin;
+
+		&::-webkit-scrollbar {
+			width: 6px;
+			height: 6px;
+		}
+
+		&::-webkit-scrollbar-track {
+			background: inherit;
+		}
+
+		&::-webkit-scrollbar-thumb {
+			background: var(--MI_THEME-scrollbarHandle);
+
+			&:hover {
+				background: var(--MI_THEME-scrollbarHandleHover);
+			}
+
+			&:active {
+				background: var(--MI_THEME-accent);
+			}
+		}
+	}
+}
+
+html, body {
+	height: 100%;
+	touch-action: manipulation;
+	margin: 0;
+	padding: 0;
+	scroll-behavior: smooth;
+}
+
+#misskey_app {
+	height: 100%;
+}
+
+a {
+	text-decoration: none;
+	cursor: pointer;
+	color: inherit;
+	tap-highlight-color: transparent;
+	-webkit-tap-highlight-color: transparent;
+	-webkit-touch-callout: none;
+
+	&:focus-visible {
+		outline-offset: 2px;
+	}
+
+	&:hover {
+		text-decoration: underline;
+	}
+
+	&[target="_blank"] {
+		-webkit-touch-callout: default;
+	}
+}
+
+rt {
+	white-space: initial;
+}
+
+:focus-visible {
+	outline: var(--MI_THEME-focus) solid 2px;
+	outline-offset: -2px;
+
+	&:hover {
+		text-decoration: none;
+	}
+}
+
+.ti {
+	width: 1.28em;
+	vertical-align: -12%;
+	line-height: 1em;
+
+	&::before {
+		font-size: 128%;
+	}
+}
+
+.ti-fw {
+	display: inline-block;
+	text-align: center;
+}
+
+._nowrap {
+	white-space: pre !important;
+	word-wrap: normal !important; // https://codeday.me/jp/qa/20190424/690106.html
+	overflow: hidden;
+	text-overflow: ellipsis;
+}
+
+._button {
+	user-select: none;
+	-webkit-user-select: none;
+	-webkit-touch-callout: none;
+	appearance: none;
+	display: inline-block;
+	padding: 0;
+	margin: 0; // for Safari
+	background: none;
+	border: none;
+	cursor: pointer;
+	color: inherit;
+	touch-action: manipulation;
+	tap-highlight-color: transparent;
+	-webkit-tap-highlight-color: transparent;
+	font-size: 1em;
+	font-family: inherit;
+	line-height: inherit;
+	max-width: 100%;
+
+	&:disabled {
+		opacity: 0.5;
+		cursor: default;
+	}
+}
+
+._buttonGray {
+	@extend ._button;
+	background: var(--MI_THEME-buttonBg);
+
+	&:not(:disabled):hover {
+		background: var(--MI_THEME-buttonHoverBg);
+	}
+}
+
+._buttonPrimary {
+	@extend ._button;
+	color: var(--MI_THEME-fgOnAccent);
+	background: var(--MI_THEME-accent);
+
+	&:not(:disabled):hover {
+		background: hsl(from var(--MI_THEME-accent) h s calc(l + 5));
+	}
+
+	&:not(:disabled):active {
+		background: hsl(from var(--MI_THEME-accent) h s calc(l - 5));
+	}
+}
+
+._buttonGradate {
+	@extend ._buttonPrimary;
+	color: var(--MI_THEME-fgOnAccent);
+	background: linear-gradient(90deg, var(--MI_THEME-buttonGradateA), var(--MI_THEME-buttonGradateB));
+
+	&:not(:disabled):hover {
+		background: linear-gradient(90deg, hsl(from var(--MI_THEME-accent) h s calc(l + 5)), hsl(from var(--MI_THEME-accent) h s calc(l + 5)));
+	}
+
+	&:not(:disabled):active {
+		background: linear-gradient(90deg, hsl(from var(--MI_THEME-accent) h s calc(l + 5)), hsl(from var(--MI_THEME-accent) h s calc(l + 5)));
+	}
+}
+
+._buttonRounded {
+	font-size: 0.95em;
+	padding: 0.5em 1em;
+	min-width: 100px;
+	border-radius: 99rem;
+
+	&._buttonPrimary,
+	&._buttonGradate {
+		font-weight: 700;
+	}
+}
+
+._help {
+	color: var(--MI_THEME-accent);
+	cursor: help;
+}
+
+._textButton {
+	@extend ._button;
+	color: var(--MI_THEME-accent);
+
+	&:focus-visible {
+		outline-offset: 2px;
+	}
+
+	&:not(:disabled):hover {
+		text-decoration: underline;
+	}
+}
+
+._panel {
+	background: var(--MI_THEME-panel);
+	border-radius: var(--MI-radius);
+	overflow: clip;
+}
+
+._margin {
+	margin: var(--MI-margin) 0;
+}
+
+._gaps_m {
+	display: flex;
+	flex-direction: column;
+	gap: 1.5em;
+}
+
+._gaps_s {
+	display: flex;
+	flex-direction: column;
+	gap: 0.75em;
+}
+
+._gaps {
+	display: flex;
+	flex-direction: column;
+	gap: var(--MI-margin);
+}
+
+._buttons {
+	display: flex;
+	gap: 8px;
+	flex-wrap: wrap;
+}
+
+._buttonsCenter {
+	@extend ._buttons;
+
+	justify-content: center;
+}
+
+._borderButton {
+	@extend ._button;
+	display: block;
+	width: 100%;
+	padding: 10px;
+	box-sizing: border-box;
+	text-align: center;
+	border: solid 0.5px var(--MI_THEME-divider);
+	border-radius: var(--MI-radius);
+
+	&:active {
+		border-color: var(--MI_THEME-accent);
+	}
+}
+
+._popup {
+	background: var(--MI_THEME-popup);
+	border-radius: var(--MI-radius);
+	contain: content;
+}
+
+._acrylic {
+	background: var(--MI_THEME-acrylicPanel);
+	-webkit-backdrop-filter: var(--MI-blur, blur(15px));
+	backdrop-filter: var(--MI-blur, blur(15px));
+}
+
+._fullinfo {
+	padding: 64px 32px;
+	text-align: center;
+
+	> img {
+		vertical-align: bottom;
+		height: 128px;
+		margin-bottom: 16px;
+		border-radius: 16px;
+	}
+}
+
+._link {
+	color: var(--MI_THEME-link);
+}
+
+._caption {
+	font-size: 0.8em;
+	opacity: 0.7;
+}
+
+._monospace {
+	font-family: Fira code, Fira Mono, Consolas, Menlo, Courier, monospace !important;
+}
+
+// MFM -----------------------------
+
+._mfm_blur_ {
+	filter: blur(6px);
+	transition: filter 0.3s;
+
+	&:hover {
+		filter: blur(0px);
+	}
+}
+
+.mfm-x2 {
+	--mfm-zoom-size: 200%;
+}
+
+.mfm-x3 {
+	--mfm-zoom-size: 400%;
+}
+
+.mfm-x4 {
+	--mfm-zoom-size: 600%;
+}
+
+.mfm-x2, .mfm-x3, .mfm-x4 {
+	font-size: var(--mfm-zoom-size);
+
+	.mfm-x2, .mfm-x3, .mfm-x4 {
+		/* only half effective */
+		font-size: calc(var(--mfm-zoom-size) / 2 + 50%);
+
+		.mfm-x2, .mfm-x3, .mfm-x4 {
+			/* disabled */
+			font-size: 100%;
+		}
+	}
+}
+
+._mfm_rainbow_fallback_ {
+	background-image: linear-gradient(to right, rgb(255, 0, 0) 0%, rgb(255, 165, 0) 17%, rgb(255, 255, 0) 33%, rgb(0, 255, 0) 50%, rgb(0, 255, 255) 67%, rgb(0, 0, 255) 83%, rgb(255, 0, 255) 100%);
+	-webkit-background-clip: text;
+	background-clip: text;
+	color: transparent;
+}
+
+@keyframes mfm-spin {
+	0% { transform: rotate(0deg); }
+	100% { transform: rotate(360deg); }
+}
+
+@keyframes mfm-spinX {
+	0% { transform: perspective(128px) rotateX(0deg); }
+	100% { transform: perspective(128px) rotateX(360deg); }
+}
+
+@keyframes mfm-spinY {
+	0% { transform: perspective(128px) rotateY(0deg); }
+	100% { transform: perspective(128px) rotateY(360deg); }
+}
+
+@keyframes mfm-jump {
+	0% { transform: translateY(0); }
+	25% { transform: translateY(-16px); }
+	50% { transform: translateY(0); }
+	75% { transform: translateY(-8px); }
+	100% { transform: translateY(0); }
+}
+
+@keyframes mfm-bounce {
+	0% { transform: translateY(0) scale(1, 1); }
+	25% { transform: translateY(-16px) scale(1, 1); }
+	50% { transform: translateY(0) scale(1, 1); }
+	75% { transform: translateY(0) scale(1.5, 0.75); }
+	100% { transform: translateY(0) scale(1, 1); }
+}
+
+// const val = () => `translate(${Math.floor(Math.random() * 20) - 10}px, ${Math.floor(Math.random() * 20) - 10}px)`;
+// let css = '';
+// for (let i = 0; i <= 100; i += 5) { css += `${i}% { transform: ${val()} }\n`; }
+@keyframes mfm-twitch {
+	0% { transform: translate(7px, -2px) }
+	5% { transform: translate(-3px, 1px) }
+	10% { transform: translate(-7px, -1px) }
+	15% { transform: translate(0px, -1px) }
+	20% { transform: translate(-8px, 6px) }
+	25% { transform: translate(-4px, -3px) }
+	30% { transform: translate(-4px, -6px) }
+	35% { transform: translate(-8px, -8px) }
+	40% { transform: translate(4px, 6px) }
+	45% { transform: translate(-3px, 1px) }
+	50% { transform: translate(2px, -10px) }
+	55% { transform: translate(-7px, 0px) }
+	60% { transform: translate(-2px, 4px) }
+	65% { transform: translate(3px, -8px) }
+	70% { transform: translate(6px, 7px) }
+	75% { transform: translate(-7px, -2px) }
+	80% { transform: translate(-7px, -8px) }
+	85% { transform: translate(9px, 3px) }
+	90% { transform: translate(-3px, -2px) }
+	95% { transform: translate(-10px, 2px) }
+	100% { transform: translate(-2px, -6px) }
+}
+
+// const val = () => `translate(${Math.floor(Math.random() * 6) - 3}px, ${Math.floor(Math.random() * 6) - 3}px) rotate(${Math.floor(Math.random() * 24) - 12}deg)`;
+// let css = '';
+// for (let i = 0; i <= 100; i += 5) { css += `${i}% { transform: ${val()} }\n`; }
+@keyframes mfm-shake {
+	0% { transform: translate(-3px, -1px) rotate(-8deg) }
+	5% { transform: translate(0px, -1px) rotate(-10deg) }
+	10% { transform: translate(1px, -3px) rotate(0deg) }
+	15% { transform: translate(1px, 1px) rotate(11deg) }
+	20% { transform: translate(-2px, 1px) rotate(1deg) }
+	25% { transform: translate(-1px, -2px) rotate(-2deg) }
+	30% { transform: translate(-1px, 2px) rotate(-3deg) }
+	35% { transform: translate(2px, 1px) rotate(6deg) }
+	40% { transform: translate(-2px, -3px) rotate(-9deg) }
+	45% { transform: translate(0px, -1px) rotate(-12deg) }
+	50% { transform: translate(1px, 2px) rotate(10deg) }
+	55% { transform: translate(0px, -3px) rotate(8deg) }
+	60% { transform: translate(1px, -1px) rotate(8deg) }
+	65% { transform: translate(0px, -1px) rotate(-7deg) }
+	70% { transform: translate(-1px, -3px) rotate(6deg) }
+	75% { transform: translate(0px, -2px) rotate(4deg) }
+	80% { transform: translate(-2px, -1px) rotate(3deg) }
+	85% { transform: translate(1px, -3px) rotate(-10deg) }
+	90% { transform: translate(1px, 0px) rotate(3deg) }
+	95% { transform: translate(-2px, 0px) rotate(-3deg) }
+	100% { transform: translate(2px, 1px) rotate(2deg) }
+}
+
+@keyframes mfm-rubberBand {
+	from { transform: scale3d(1, 1, 1); }
+	30% { transform: scale3d(1.25, 0.75, 1); }
+	40% { transform: scale3d(0.75, 1.25, 1); }
+	50% { transform: scale3d(1.15, 0.85, 1); }
+	65% { transform: scale3d(0.95, 1.05, 1); }
+	75% { transform: scale3d(1.05, 0.95, 1); }
+	to { transform: scale3d(1, 1, 1); }
+}
+
+@keyframes mfm-rainbow {
+	0% { filter: hue-rotate(0deg) contrast(150%) saturate(150%); }
+	100% { filter: hue-rotate(360deg) contrast(150%) saturate(150%); }
+}
diff --git a/packages/frontend-embed/src/theme.ts b/packages/frontend-embed/src/theme.ts
new file mode 100644
index 0000000000..4664ad4880
--- /dev/null
+++ b/packages/frontend-embed/src/theme.ts
@@ -0,0 +1,108 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import tinycolor from 'tinycolor2';
+import lightTheme from '@@/themes/_light.json5';
+import darkTheme from '@@/themes/_dark.json5';
+import type { BundledTheme } from 'shiki/themes';
+
+export type Theme = {
+	id: string;
+	name: string;
+	author: string;
+	desc?: string;
+	base?: 'dark' | 'light';
+	props: Record<string, string>;
+	codeHighlighter?: {
+		base: BundledTheme;
+		overrides?: Record<string, any>;
+	} | {
+		base: '_none_';
+		overrides: Record<string, any>;
+	};
+};
+
+let timeout: number | null = null;
+
+export function assertIsTheme(theme: Record<string, unknown>): theme is Theme {
+	return typeof theme === 'object' && theme !== null && 'id' in theme && 'name' in theme && 'author' in theme && 'props' in theme;
+}
+
+export function applyTheme(theme: Theme, persist = true) {
+	if (timeout) window.clearTimeout(timeout);
+
+	document.documentElement.classList.add('_themeChanging_');
+
+	timeout = window.setTimeout(() => {
+		document.documentElement.classList.remove('_themeChanging_');
+	}, 1000);
+
+	const colorScheme = theme.base === 'dark' ? 'dark' : 'light';
+
+	document.documentElement.dataset.colorScheme = colorScheme;
+
+	// Deep copy
+	const _theme = JSON.parse(JSON.stringify(theme));
+
+	if (_theme.base) {
+		const base = [lightTheme, darkTheme].find(x => x.id === _theme.base);
+		if (base) _theme.props = Object.assign({}, base.props, _theme.props);
+	}
+
+	const props = compile(_theme);
+
+	for (const tag of document.head.children) {
+		if (tag.tagName === 'META' && tag.getAttribute('name') === 'theme-color') {
+			tag.setAttribute('content', props['htmlThemeColor']);
+			break;
+		}
+	}
+
+	for (const [k, v] of Object.entries(props)) {
+		document.documentElement.style.setProperty(`--MI_THEME-${k}`, v.toString());
+	}
+
+	// iframeを正常に透過させるために、cssのcolor-schemeは `light dark;` 固定にしてある。style.scss参照
+}
+
+function compile(theme: Theme): Record<string, string> {
+	function getColor(val: string): tinycolor.Instance {
+		if (val[0] === '@') { // ref (prop)
+			return getColor(theme.props[val.substring(1)]);
+		} else if (val[0] === '$') { // ref (const)
+			return getColor(theme.props[val]);
+		} else if (val[0] === ':') { // func
+			const parts = val.split('<');
+			const func = parts.shift().substring(1);
+			const arg = parseFloat(parts.shift());
+			const color = getColor(parts.join('<'));
+
+			switch (func) {
+				case 'darken': return color.darken(arg);
+				case 'lighten': return color.lighten(arg);
+				case 'alpha': return color.setAlpha(arg);
+				case 'hue': return color.spin(arg);
+				case 'saturate': return color.saturate(arg);
+			}
+		}
+
+		// other case
+		return tinycolor(val);
+	}
+
+	const props = {};
+
+	for (const [k, v] of Object.entries(theme.props)) {
+		if (k.startsWith('$')) continue; // ignore const
+
+		props[k] = v.startsWith('"') ? v.replace(/^"\s*/, '') : genValue(getColor(v));
+	}
+
+	return props;
+}
+
+function genValue(c: tinycolor.Instance): string {
+	return c.toRgbString();
+}
diff --git a/packages/frontend-embed/src/ui.vue b/packages/frontend-embed/src/ui.vue
new file mode 100644
index 0000000000..4ba5968a91
--- /dev/null
+++ b/packages/frontend-embed/src/ui.vue
@@ -0,0 +1,110 @@
+<!--
+SPDX-FileCopyrightText: syuilo and misskey-project
+SPDX-License-Identifier: AGPL-3.0-only
+-->
+
+<template>
+<div
+	ref="rootEl"
+	:class="[
+		$style.rootForEmbedPage,
+		{
+			[$style.rounded]: embedRounded,
+			[$style.noBorder]: embedNoBorder,
+		}
+	]"
+	:style="maxHeight > 0 ? { maxHeight: `${maxHeight}px`, '--embedMaxHeight': `${maxHeight}px` } : {}"
+>
+	<div
+		:class="$style.routerViewContainer"
+	>
+		<Suspense :timeout="0">
+			<EmNotePage v-if="page === 'notes'" :noteId="contentId"/>
+			<EmUserTimelinePage v-else-if="page === 'user-timeline'" :userId="contentId"/>
+			<EmClipPage v-else-if="page === 'clips'" :clipId="contentId"/>
+			<EmTagPage v-else-if="page === 'tags'" :tag="contentId"/>
+			<XNotFound v-else/>
+			<template #fallback>
+				<EmLoading/>
+			</template>
+		</Suspense>
+	</div>
+</div>
+</template>
+
+<script lang="ts" setup>
+import { ref, shallowRef, onMounted, onUnmounted, inject } from 'vue';
+import { postMessageToParentWindow } from '@/post-message.js';
+import { DI } from '@/di.js';
+import { defaultEmbedParams } from '@@/js/embed-page.js';
+import EmNotePage from '@/pages/note.vue';
+import EmUserTimelinePage from '@/pages/user-timeline.vue';
+import EmClipPage from '@/pages/clip.vue';
+import EmTagPage from '@/pages/tag.vue';
+import XNotFound from '@/pages/not-found.vue';
+import EmLoading from '@/components/EmLoading.vue';
+
+function safeURIDecode(str: string): string {
+	try {
+		return decodeURIComponent(str);
+	} catch {
+		return str;
+	}
+}
+
+const page = location.pathname.split('/')[2];
+const contentId = safeURIDecode(location.pathname.split('/')[3]);
+if (_DEV_) console.log(page, contentId);
+
+const embedParams = inject(DI.embedParams, defaultEmbedParams);
+
+//#region Embed Style
+const embedRounded = ref(embedParams.rounded);
+const embedNoBorder = ref(!embedParams.border);
+const maxHeight = ref(embedParams.maxHeight ?? 0);
+//#endregion
+
+//#region Embed Resizer
+const rootEl = shallowRef<HTMLElement | null>(null);
+
+let previousHeight = 0;
+const resizeObserver = new ResizeObserver(async () => {
+	const height = rootEl.value!.scrollHeight + (embedNoBorder.value ? 0 : 2); // border 上下1px
+	if (Math.abs(previousHeight - height) < 1) return; // 1px未満の変化は無視
+	postMessageToParentWindow('misskey:embed:changeHeight', {
+		height: (maxHeight.value > 0 && height > maxHeight.value) ? maxHeight.value : height,
+	});
+	previousHeight = height;
+});
+onMounted(() => {
+	resizeObserver.observe(rootEl.value!);
+});
+onUnmounted(() => {
+	resizeObserver.disconnect();
+});
+//#endregion
+</script>
+
+<style lang="scss" module>
+.rootForEmbedPage {
+	box-sizing: border-box;
+	border: 1px solid var(--MI_THEME-divider);
+	background-color: var(--MI_THEME-bg);
+	overflow: hidden;
+	position: relative;
+	height: auto;
+
+	&.rounded {
+		border-radius: var(--MI-radius);
+	}
+
+	&.noBorder {
+		border: none;
+	}
+}
+
+.routerViewContainer {
+	container-type: inline-size;
+	max-height: var(--embedMaxHeight, none);
+}
+</style>
diff --git a/packages/frontend-embed/src/utils.ts b/packages/frontend-embed/src/utils.ts
new file mode 100644
index 0000000000..939648aa38
--- /dev/null
+++ b/packages/frontend-embed/src/utils.ts
@@ -0,0 +1,23 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import * as Misskey from 'misskey-js';
+import { url } from '@@/js/config.js';
+
+export const acct = (user: Misskey.Acct) => {
+	return Misskey.acct.toString(user);
+};
+
+export const userName = (user: Misskey.entities.User) => {
+	return user.name || user.username;
+};
+
+export const userPage = (user: Misskey.Acct, path?: string, absolute = false) => {
+	return `${absolute ? url : ''}/@${acct(user)}${(path ? `/${path}` : '')}`;
+};
+
+export const notePage = (note: Misskey.entities.Note) => {
+	return `/notes/${note.id}`;
+};
diff --git a/packages/frontend-embed/src/workers/draw-blurhash.ts b/packages/frontend-embed/src/workers/draw-blurhash.ts
new file mode 100644
index 0000000000..22de6cd3a8
--- /dev/null
+++ b/packages/frontend-embed/src/workers/draw-blurhash.ts
@@ -0,0 +1,22 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { render } from 'buraha';
+
+const canvas = new OffscreenCanvas(64, 64);
+
+onmessage = (event) => {
+	// console.log(event.data);
+	if (!('id' in event.data && typeof event.data.id === 'string')) {
+		return;
+	}
+	if (!('hash' in event.data && typeof event.data.hash === 'string')) {
+		return;
+	}
+
+	render(event.data.hash, canvas);
+	const bitmap = canvas.transferToImageBitmap();
+	postMessage({ id: event.data.id, bitmap });
+};
diff --git a/packages/frontend-embed/src/workers/test-webgl2.ts b/packages/frontend-embed/src/workers/test-webgl2.ts
new file mode 100644
index 0000000000..b203ebe666
--- /dev/null
+++ b/packages/frontend-embed/src/workers/test-webgl2.ts
@@ -0,0 +1,14 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+const canvas = globalThis.OffscreenCanvas && new OffscreenCanvas(1, 1);
+// 環境によってはOffscreenCanvasが存在しないため
+// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
+const gl = canvas?.getContext('webgl2');
+if (gl) {
+	postMessage({ result: true });
+} else {
+	postMessage({ result: false });
+}
diff --git a/packages/frontend-embed/src/workers/tsconfig.json b/packages/frontend-embed/src/workers/tsconfig.json
new file mode 100644
index 0000000000..8ee8930465
--- /dev/null
+++ b/packages/frontend-embed/src/workers/tsconfig.json
@@ -0,0 +1,5 @@
+{
+	"compilerOptions": {
+		"lib": ["esnext", "webworker"],
+	}
+}
diff --git a/packages/frontend-embed/tsconfig.json b/packages/frontend-embed/tsconfig.json
new file mode 100644
index 0000000000..3701343623
--- /dev/null
+++ b/packages/frontend-embed/tsconfig.json
@@ -0,0 +1,53 @@
+{
+	"compilerOptions": {
+		"allowJs": true,
+		"noEmitOnError": false,
+		"noImplicitAny": false,
+		"noImplicitReturns": true,
+		"noUnusedParameters": false,
+		"noUnusedLocals": false,
+		"noFallthroughCasesInSwitch": true,
+		"declaration": false,
+		"sourceMap": false,
+		"target": "ES2022",
+		"module": "nodenext",
+		"moduleResolution": "nodenext",
+		"removeComments": false,
+		"noLib": false,
+		"strict": true,
+		"strictNullChecks": true,
+		"experimentalDecorators": true,
+		"resolveJsonModule": true,
+		"allowSyntheticDefaultImports": true,
+		"isolatedModules": true,
+		"useDefineForClassFields": true,
+		"baseUrl": ".",
+		"paths": {
+			"@/*": ["./src/*"],
+			"@@/*": ["../frontend-shared/*"]
+		},
+		"typeRoots": [
+			"./@types",
+			"./node_modules/@types",
+			"./node_modules/@vue-macros",
+			"./node_modules"
+		],
+		"types": [
+			"vite/client",
+		],
+		"lib": [
+			"esnext",
+			"dom",
+			"dom.iterable"
+		],
+		"jsx": "preserve"
+	},
+	"compileOnSave": false,
+	"include": [
+		"./**/*.ts",
+		"./**/*.vue"
+	],
+	"exclude": [
+		".storybook/**/*"
+	]
+}
diff --git a/packages/frontend-embed/vite.config.local-dev.ts b/packages/frontend-embed/vite.config.local-dev.ts
new file mode 100644
index 0000000000..bf2f478887
--- /dev/null
+++ b/packages/frontend-embed/vite.config.local-dev.ts
@@ -0,0 +1,96 @@
+import dns from 'dns';
+import { readFile } from 'node:fs/promises';
+import type { IncomingMessage } from 'node:http';
+import { defineConfig } from 'vite';
+import type { UserConfig } from 'vite';
+import * as yaml from 'js-yaml';
+import locales from '../../locales/index.js';
+import { getConfig } from './vite.config.js';
+
+dns.setDefaultResultOrder('ipv4first');
+
+const defaultConfig = getConfig();
+
+const { port } = yaml.load(await readFile('../../.config/default.yml', 'utf-8'));
+
+const httpUrl = `http://localhost:${port}/`;
+const websocketUrl = `ws://localhost:${port}/`;
+
+// activitypubリクエストはProxyを通し、それ以外はViteの開発サーバーを返す
+function varyHandler(req: IncomingMessage) {
+	if (req.headers.accept?.includes('application/activity+json')) {
+		return null;
+	}
+	return '/index.html';
+}
+
+const devConfig: UserConfig = {
+	// 基本の設定は vite.config.js から引き継ぐ
+	...defaultConfig,
+	root: 'src',
+	publicDir: '../assets',
+	base: '/embed',
+	server: {
+		host: 'localhost',
+		port: 5174,
+		proxy: {
+			'/api': {
+				changeOrigin: true,
+				target: httpUrl,
+			},
+			'/assets': httpUrl,
+			'/static-assets': httpUrl,
+			'/client-assets': httpUrl,
+			'/files': httpUrl,
+			'/twemoji': httpUrl,
+			'/fluent-emoji': httpUrl,
+			'/sw.js': httpUrl,
+			'/streaming': {
+				target: websocketUrl,
+				ws: true,
+			},
+			'/favicon.ico': httpUrl,
+			'/robots.txt': httpUrl,
+			'/embed.js': httpUrl,
+			'/identicon': {
+				target: httpUrl,
+				rewrite(path) {
+					return path.replace('@localhost:5173', '');
+				},
+			},
+			'/url': httpUrl,
+			'/proxy': httpUrl,
+			'/_info_card_': httpUrl,
+			'/bios': httpUrl,
+			'/cli': httpUrl,
+			'/inbox': httpUrl,
+			'/emoji/': httpUrl,
+			'/notes': {
+				target: httpUrl,
+				bypass: varyHandler,
+			},
+			'/users': {
+				target: httpUrl,
+				bypass: varyHandler,
+			},
+			'/.well-known': {
+				target: httpUrl,
+			},
+		},
+	},
+	build: {
+		...defaultConfig.build,
+		rollupOptions: {
+			...defaultConfig.build?.rollupOptions,
+			input: 'index.html',
+		},
+	},
+
+	define: {
+		...defaultConfig.define,
+		_LANGS_FULL_: JSON.stringify(Object.entries(locales)),
+	},
+};
+
+export default defineConfig(({ command, mode }) => devConfig);
+
diff --git a/packages/frontend-embed/vite.config.ts b/packages/frontend-embed/vite.config.ts
new file mode 100644
index 0000000000..2dbee488c5
--- /dev/null
+++ b/packages/frontend-embed/vite.config.ts
@@ -0,0 +1,161 @@
+import path from 'path';
+import pluginVue from '@vitejs/plugin-vue';
+import { type UserConfig, defineConfig } from 'vite';
+
+import locales from '../../locales/index.js';
+import meta from '../../package.json';
+import packageInfo from './package.json' with { type: 'json' };
+import pluginJson5 from './vite.json5.js';
+
+const extensions = ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.json', '.json5', '.svg', '.sass', '.scss', '.css', '.vue'];
+
+/**
+ * Misskeyのフロントエンドにバンドルせず、CDNなどから別途読み込むリソースを記述する。
+ * CDNを使わずにバンドルしたい場合、以下の配列から該当要素を削除orコメントアウトすればOK
+ */
+const externalPackages = [
+	// shiki(コードブロックのシンタックスハイライトで使用中)はテーマ・言語の定義の容量が大きいため、それらはCDNから読み込む
+	{
+		name: 'shiki',
+		match: /^shiki\/(?<subPkg>(langs|themes))$/,
+		path(id: string, pattern: RegExp): string {
+			const match = pattern.exec(id)?.groups;
+			return match
+				? `https://esm.sh/shiki@${packageInfo.dependencies.shiki}/${match['subPkg']}`
+				: id;
+		},
+	},
+];
+
+const hash = (str: string, seed = 0): number => {
+	let h1 = 0xdeadbeef ^ seed,
+		h2 = 0x41c6ce57 ^ seed;
+	for (let i = 0, ch; i < str.length; i++) {
+		ch = str.charCodeAt(i);
+		h1 = Math.imul(h1 ^ ch, 2654435761);
+		h2 = Math.imul(h2 ^ ch, 1597334677);
+	}
+
+	h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507) ^ Math.imul(h2 ^ (h2 >>> 13), 3266489909);
+	h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507) ^ Math.imul(h1 ^ (h1 >>> 13), 3266489909);
+
+	return 4294967296 * (2097151 & h2) + (h1 >>> 0);
+};
+
+const BASE62_DIGITS = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
+
+function toBase62(n: number): string {
+	if (n === 0) {
+		return '0';
+	}
+	let result = '';
+	while (n > 0) {
+		result = BASE62_DIGITS[n % BASE62_DIGITS.length] + result;
+		n = Math.floor(n / BASE62_DIGITS.length);
+	}
+
+	return result;
+}
+
+export function getConfig(): UserConfig {
+	return {
+		base: '/embed_vite/',
+
+		server: {
+			port: 5174,
+		},
+
+		plugins: [
+			pluginVue(),
+			pluginJson5(),
+		],
+
+		resolve: {
+			extensions,
+			alias: {
+				'@/': __dirname + '/src/',
+				'@@/': __dirname + '/../frontend-shared/',
+				'/client-assets/': __dirname + '/assets/',
+				'/static-assets/': __dirname + '/../backend/assets/'
+			},
+		},
+
+		css: {
+			modules: {
+				generateScopedName(name, filename, _css): string {
+					const id = (path.relative(__dirname, filename.split('?')[0]) + '-' + name).replace(/[\\\/\.\?&=]/g, '-').replace(/(src-|vue-)/g, '');
+					if (process.env.NODE_ENV === 'production') {
+						return 'x' + toBase62(hash(id)).substring(0, 4);
+					} else {
+						return id;
+					}
+				},
+			},
+			preprocessorOptions: {
+				scss: {
+					api: 'modern-compiler',
+				},
+			},
+		},
+
+		define: {
+			_VERSION_: JSON.stringify(meta.version),
+			_LANGS_: JSON.stringify(Object.entries(locales).map(([k, v]) => [k, v._lang_])),
+			_ENV_: JSON.stringify(process.env.NODE_ENV),
+			_DEV_: process.env.NODE_ENV !== 'production',
+			_PERF_PREFIX_: JSON.stringify('Misskey:'),
+			__VUE_OPTIONS_API__: false,
+			__VUE_PROD_DEVTOOLS__: false,
+		},
+
+		build: {
+			target: [
+				'chrome116',
+				'firefox116',
+				'safari16',
+			],
+			manifest: 'manifest.json',
+			rollupOptions: {
+				input: {
+					app: './src/boot.ts',
+				},
+				external: externalPackages.map(p => p.match),
+				output: {
+					manualChunks: {
+						vue: ['vue'],
+					},
+					chunkFileNames: process.env.NODE_ENV === 'production' ? '[hash:8].js' : '[name]-[hash:8].js',
+					assetFileNames: process.env.NODE_ENV === 'production' ? '[hash:8][extname]' : '[name]-[hash:8][extname]',
+					paths(id) {
+						for (const p of externalPackages) {
+							if (p.match.test(id)) {
+								return p.path(id, p.match);
+							}
+						}
+
+						return id;
+					},
+				},
+			},
+			cssCodeSplit: true,
+			outDir: __dirname + '/../../built/_frontend_embed_vite_',
+			assetsDir: '.',
+			emptyOutDir: false,
+			sourcemap: process.env.NODE_ENV === 'development',
+			reportCompressedSize: false,
+
+			// https://vitejs.dev/guide/dep-pre-bundling.html#monorepos-and-linked-dependencies
+			commonjsOptions: {
+				include: [/misskey-js/, /node_modules/],
+			},
+		},
+
+		worker: {
+			format: 'es',
+		},
+	};
+}
+
+const config = defineConfig(({ command, mode }) => getConfig());
+
+export default config;
diff --git a/packages/frontend-embed/vite.json5.ts b/packages/frontend-embed/vite.json5.ts
new file mode 100644
index 0000000000..87b67c2142
--- /dev/null
+++ b/packages/frontend-embed/vite.json5.ts
@@ -0,0 +1,48 @@
+// Original: https://github.com/rollup/plugins/tree/8835dd2aed92f408d7dc72d7cc25a9728e16face/packages/json
+
+import JSON5 from 'json5';
+import { Plugin } from 'rollup';
+import { createFilter, dataToEsm } from '@rollup/pluginutils';
+import { RollupJsonOptions } from '@rollup/plugin-json';
+
+// json5 extends SyntaxError with additional fields (without subclassing)
+// https://github.com/json5/json5/blob/de344f0619bda1465a6e25c76f1c0c3dda8108d9/lib/parse.js#L1111-L1112
+interface Json5SyntaxError extends SyntaxError {
+	lineNumber: number;
+	columnNumber: number;
+}
+
+export default function json5(options: RollupJsonOptions = {}): Plugin {
+	const filter = createFilter(options.include, options.exclude);
+	const indent = 'indent' in options ? options.indent : '\t';
+
+	return {
+		name: 'json5',
+
+		// eslint-disable-next-line no-shadow
+		transform(json, id) {
+			if (id.slice(-6) !== '.json5' || !filter(id)) return null;
+
+			try {
+				const parsed = JSON5.parse(json);
+				return {
+					code: dataToEsm(parsed, {
+						preferConst: options.preferConst,
+						compact: options.compact,
+						namedExports: options.namedExports,
+						indent,
+					}),
+					map: { mappings: '' },
+				};
+			} catch (err) {
+				if (!(err instanceof SyntaxError)) {
+					throw err;
+				}
+				const message = 'Could not parse JSON5 file';
+				const { lineNumber, columnNumber } = err as Json5SyntaxError;
+				this.warn({ message, id, loc: { line: lineNumber, column: columnNumber } });
+				return null;
+			}
+		},
+	};
+}
diff --git a/packages/frontend-embed/vue-shims.d.ts b/packages/frontend-embed/vue-shims.d.ts
new file mode 100644
index 0000000000..eba994772d
--- /dev/null
+++ b/packages/frontend-embed/vue-shims.d.ts
@@ -0,0 +1,6 @@
+/* eslint-disable */
+declare module "*.vue" {
+	import { defineComponent } from "vue";
+	const component: ReturnType<typeof defineComponent>;
+	export default component;
+}
diff --git a/packages/frontend-shared/.gitignore b/packages/frontend-shared/.gitignore
new file mode 100644
index 0000000000..5f6be09d7c
--- /dev/null
+++ b/packages/frontend-shared/.gitignore
@@ -0,0 +1,2 @@
+/storybook-static
+js-built
diff --git a/packages/frontend-shared/@types/global.d.ts b/packages/frontend-shared/@types/global.d.ts
new file mode 100644
index 0000000000..4b8d679e75
--- /dev/null
+++ b/packages/frontend-shared/@types/global.d.ts
@@ -0,0 +1,25 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+type FIXME = any;
+
+declare const _LANGS_: string[][];
+declare const _VERSION_: string;
+declare const _ENV_: string;
+declare const _DEV_: boolean;
+declare const _PERF_PREFIX_: string;
+declare const _DATA_TRANSFER_DRIVE_FILE_: string;
+declare const _DATA_TRANSFER_DRIVE_FOLDER_: string;
+declare const _DATA_TRANSFER_DECK_COLUMN_: string;
+
+// for dev-mode
+declare const _LANGS_FULL_: string[][];
+
+// TagCanvas
+interface Window {
+	// eslint-disable-next-line @typescript-eslint/no-explicit-any
+	TagCanvas: any;
+}
diff --git a/packages/frontend-shared/build.js b/packages/frontend-shared/build.js
new file mode 100644
index 0000000000..17b6da8d30
--- /dev/null
+++ b/packages/frontend-shared/build.js
@@ -0,0 +1,106 @@
+import fs from 'node:fs';
+import { fileURLToPath } from 'node:url';
+import { dirname } from 'node:path';
+import * as esbuild from 'esbuild';
+import { build } from 'esbuild';
+import { globSync } from 'glob';
+import { execa } from 'execa';
+
+const _filename = fileURLToPath(import.meta.url);
+const _dirname = dirname(_filename);
+const _package = JSON.parse(fs.readFileSync(_dirname + '/package.json', 'utf-8'));
+
+const entryPoints = globSync('./js/**/**.{ts,tsx}');
+
+/** @type {import('esbuild').BuildOptions} */
+const options = {
+	entryPoints,
+	minify: process.env.NODE_ENV === 'production',
+	outdir: './js-built',
+	target: 'es2022',
+	platform: 'browser',
+	format: 'esm',
+	sourcemap: 'linked',
+};
+
+// js-built配下をすべて削除する
+fs.rmSync('./js-built', { recursive: true, force: true });
+
+if (process.argv.map(arg => arg.toLowerCase()).includes('--watch')) {
+	await watchSrc();
+} else {
+	await buildSrc();
+}
+
+async function buildSrc() {
+	console.log(`[${_package.name}] start building...`);
+
+	await build(options)
+		.then(() => {
+			console.log(`[${_package.name}] build succeeded.`);
+		})
+		.catch((err) => {
+			process.stderr.write(err.stderr);
+			process.exit(1);
+		});
+
+	if (process.env.NODE_ENV === 'production') {
+		console.log(`[${_package.name}] skip building d.ts because NODE_ENV is production.`);
+	} else {
+		await buildDts();
+	}
+
+	fs.copyFileSync('./js/emojilist.json', './js-built/emojilist.json');
+
+	console.log(`[${_package.name}] finish building.`);
+}
+
+function buildDts() {
+	return execa(
+		'tsc',
+		[
+			'--project', 'tsconfig.json',
+			'--outDir', 'js-built',
+			'--declaration', 'true',
+			'--emitDeclarationOnly', 'true',
+		],
+		{
+			stdout: process.stdout,
+			stderr: process.stderr,
+		},
+	);
+}
+
+async function watchSrc() {
+	const plugins = [{
+		name: 'gen-dts',
+		setup(build) {
+			build.onStart(() => {
+				console.log(`[${_package.name}] detect changed...`);
+			});
+			build.onEnd(async result => {
+				if (result.errors.length > 0) {
+					console.error(`[${_package.name}] watch build failed:`, result);
+					return;
+				}
+				await buildDts();
+			});
+		},
+	}];
+
+	console.log(`[${_package.name}] start watching...`);
+
+	const context = await esbuild.context({ ...options, plugins });
+	await context.watch();
+
+	await new Promise((resolve, reject) => {
+		process.on('SIGHUP', resolve);
+		process.on('SIGINT', resolve);
+		process.on('SIGTERM', resolve);
+		process.on('uncaughtException', reject);
+		process.on('exit', resolve);
+	}).finally(async () => {
+		await context.dispose();
+		console.log(`[${_package.name}] finish watching.`);
+	});
+}
diff --git a/packages/frontend-shared/eslint.config.js b/packages/frontend-shared/eslint.config.js
new file mode 100644
index 0000000000..cd4641a270
--- /dev/null
+++ b/packages/frontend-shared/eslint.config.js
@@ -0,0 +1,100 @@
+import globals from 'globals';
+import tsParser from '@typescript-eslint/parser';
+import parser from 'vue-eslint-parser';
+import pluginVue from 'eslint-plugin-vue';
+import pluginMisskey from '@misskey-dev/eslint-plugin';
+import sharedConfig from '../shared/eslint.config.js';
+
+// eslint-disable-next-line import/no-default-export
+export default [
+	...sharedConfig,
+	{
+		files: ['**/*.vue'],
+		...pluginMisskey.configs.typescript,
+	},
+	...pluginVue.configs['flat/recommended'],
+	{
+		files: [
+			'@types/**/*.ts',
+			'js/**/*.ts',
+			'**/*.vue',
+		],
+		languageOptions: {
+			globals: {
+				...Object.fromEntries(Object.entries(globals.node).map(([key]) => [key, 'off'])),
+				...globals.browser,
+
+				// Node.js
+				module: false,
+				require: false,
+				__dirname: false,
+
+				// Misskey
+				_DEV_: false,
+				_LANGS_: false,
+				_VERSION_: false,
+				_ENV_: false,
+				_PERF_PREFIX_: false,
+				_DATA_TRANSFER_DRIVE_FILE_: false,
+				_DATA_TRANSFER_DRIVE_FOLDER_: false,
+				_DATA_TRANSFER_DECK_COLUMN_: false,
+			},
+			parser,
+			parserOptions: {
+				extraFileExtensions: ['.vue'],
+				parser: tsParser,
+				project: ['./tsconfig.json'],
+				sourceType: 'module',
+				tsconfigRootDir: import.meta.dirname,
+			},
+		},
+		rules: {
+			'@typescript-eslint/no-empty-interface': ['error', {
+				allowSingleExtends: true,
+			}],
+			// window の禁止理由: グローバルスコープと衝突し、予期せぬ結果を招くため
+			// e の禁止理由: error や event など、複数のキーワードの頭文字であり分かりにくいため
+			'id-denylist': ['error', 'window', 'e'],
+			'no-shadow': ['warn'],
+			'vue/attributes-order': ['error', {
+				alphabetical: false,
+			}],
+			'vue/no-use-v-if-with-v-for': ['error', {
+				allowUsingIterationVar: false,
+			}],
+			'vue/no-ref-as-operand': 'error',
+			'vue/no-multi-spaces': ['error', {
+				ignoreProperties: false,
+			}],
+			'vue/no-v-html': 'warn',
+			'vue/order-in-components': 'error',
+			'vue/html-indent': ['warn', 'tab', {
+				attribute: 1,
+				baseIndent: 0,
+				closeBracket: 0,
+				alignAttributesVertically: true,
+				ignores: [],
+			}],
+			'vue/html-closing-bracket-spacing': ['warn', {
+				startTag: 'never',
+				endTag: 'never',
+				selfClosingTag: 'never',
+			}],
+			'vue/multi-word-component-names': 'warn',
+			'vue/require-v-for-key': 'warn',
+			'vue/no-unused-components': 'warn',
+			'vue/no-unused-vars': 'warn',
+			'vue/no-dupe-keys': 'warn',
+			'vue/valid-v-for': 'warn',
+			'vue/return-in-computed-property': 'warn',
+			'vue/no-setup-props-reactivity-loss': 'warn',
+			'vue/max-attributes-per-line': 'off',
+			'vue/html-self-closing': 'off',
+			'vue/singleline-html-element-content-newline': 'off',
+			'vue/v-on-event-hyphenation': ['error', 'never', {
+				autofix: true,
+			}],
+			'vue/attribute-hyphenation': ['error', 'never'],
+		},
+	},
+];
diff --git a/packages/frontend/src/scripts/collapsed.ts b/packages/frontend-shared/js/collapsed.ts
similarity index 86%
rename from packages/frontend/src/scripts/collapsed.ts
rename to packages/frontend-shared/js/collapsed.ts
index 4ec88a3c65..af1f88cb73 100644
--- a/packages/frontend/src/scripts/collapsed.ts
+++ b/packages/frontend-shared/js/collapsed.ts
@@ -7,7 +7,7 @@ import * as Misskey from 'misskey-js';
 
 export function shouldCollapsed(note: Misskey.entities.Note, urls: string[]): boolean {
 	const collapsed = note.cw == null && (
-		note.text != null && (
+		(note.text != null && (
 			(note.text.includes('$[x2')) ||
 			(note.text.includes('$[x3')) ||
 			(note.text.includes('$[x4')) ||
@@ -15,7 +15,7 @@ export function shouldCollapsed(note: Misskey.entities.Note, urls: string[]): bo
 			(note.text.split('\n').length > 9) ||
 			(note.text.length > 500) ||
 			(urls.length >= 4)
-		) || note.files.length >= 5
+		)) || (note.files != null && note.files.length >= 5)
 	);
 
 	return collapsed;
diff --git a/packages/frontend/src/config.ts b/packages/frontend-shared/js/config.ts
similarity index 51%
rename from packages/frontend/src/config.ts
rename to packages/frontend-shared/js/config.ts
index 277dfc12aa..ae1dcae10b 100644
--- a/packages/frontend/src/config.ts
+++ b/packages/frontend-shared/js/config.ts
@@ -3,8 +3,9 @@
  * SPDX-License-Identifier: AGPL-3.0-only
  */
 
-import { miLocalStorage } from '@/local-storage.js';
+import type { Locale } from '../../../locales/index.js';
 
+// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
 const address = new URL(document.querySelector<HTMLMetaElement>('meta[property="instance_url"]')?.content || location.href);
 const siteName = document.querySelector<HTMLMetaElement>('meta[property="og:site_name"]')?.content;
 
@@ -13,15 +14,15 @@ export const hostname = address.hostname;
 export const url = address.origin;
 export const apiUrl = location.origin + '/api';
 export const wsOrigin = location.origin;
-export const lang = miLocalStorage.getItem('lang') ?? 'en-US';
+export const lang = localStorage.getItem('lang') ?? 'en-US';
 export const langs = _LANGS_;
-const preParseLocale = miLocalStorage.getItem('locale');
-export let locale = preParseLocale ? JSON.parse(preParseLocale) : null;
+const preParseLocale = localStorage.getItem('locale');
+export let locale: Locale = preParseLocale ? JSON.parse(preParseLocale) : null;
 export const version = _VERSION_;
-export const instanceName = siteName === 'Misskey' || siteName == null ? host : siteName;
-export const ui = miLocalStorage.getItem('ui');
-export const debug = miLocalStorage.getItem('debug') === 'true';
+export const instanceName = (siteName === 'Misskey' || siteName == null) ? host : siteName;
+export const ui = localStorage.getItem('ui');
+export const debug = localStorage.getItem('debug') === 'true';
 
-export function updateLocale(newLocale): void {
+export function updateLocale(newLocale: Locale): void {
 	locale = newLocale;
 }
diff --git a/packages/frontend/src/const.ts b/packages/frontend-shared/js/const.ts
similarity index 93%
rename from packages/frontend/src/const.ts
rename to packages/frontend-shared/js/const.ts
index 9e41926a97..4fe5cbb205 100644
--- a/packages/frontend/src/const.ts
+++ b/packages/frontend-shared/js/const.ts
@@ -67,6 +67,9 @@ export const notificationTypes = [
 	'followRequestAccepted',
 	'roleAssigned',
 	'achievementEarned',
+	'exportCompleted',
+	'login',
+	'test',
 	'app',
 ] as const;
 export const obsoleteNotificationTypes = ['pollVote', 'groupInvited'] as const;
@@ -87,6 +90,7 @@ export const ROLE_POLICIES = [
 	'canHideAds',
 	'driveCapacityMb',
 	'alwaysMarkNsfw',
+	'canUpdateBioMedia',
 	'pinLimit',
 	'antennaLimit',
 	'wordMuteLimit',
@@ -97,6 +101,11 @@ export const ROLE_POLICIES = [
 	'userEachUserListsLimit',
 	'rateLimitFactor',
 	'avatarDecorationLimit',
+	'canImportAntennas',
+	'canImportBlocking',
+	'canImportFollowing',
+	'canImportMuting',
+	'canImportUserLists',
 ] as const;
 
 // なんか動かない
@@ -126,7 +135,7 @@ export const MFM_PARAMS: Record<typeof MFM_TAGS[number], string[]> = {
 	position: ['x=', 'y='],
 	fg: ['color='],
 	bg: ['color='],
-  border: ['width=', 'style=', 'color=', 'radius=', 'noclip'],
+	border: ['width=', 'style=', 'color=', 'radius=', 'noclip'],
 	font: ['serif', 'monospace', 'cursive', 'fantasy', 'emoji', 'math'],
 	blur: [],
 	rainbow: ['speed=', 'delay='],
diff --git a/packages/frontend-shared/js/embed-page.ts b/packages/frontend-shared/js/embed-page.ts
new file mode 100644
index 0000000000..d5555a98c3
--- /dev/null
+++ b/packages/frontend-shared/js/embed-page.ts
@@ -0,0 +1,97 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+//#region Embed関連の定義
+
+/** 埋め込みの対象となるエンティティ(/embed/xxx の xxx の部分と対応させる) */
+const embeddableEntities = [
+	'notes',
+	'user-timeline',
+	'clips',
+	'tags',
+] as const;
+
+/** 埋め込みの対象となるエンティティ */
+export type EmbeddableEntity = typeof embeddableEntities[number];
+
+/** 内部でスクロールがあるページ */
+export const embedRouteWithScrollbar: EmbeddableEntity[] = [
+	'clips',
+	'tags',
+	'user-timeline',
+];
+
+/** 埋め込みコードのパラメータ */
+export type EmbedParams = {
+	maxHeight?: number;
+	colorMode?: 'light' | 'dark';
+	rounded?: boolean;
+	border?: boolean;
+	autoload?: boolean;
+	header?: boolean;
+};
+
+/** 正規化されたパラメータ */
+export type ParsedEmbedParams = Required<Omit<EmbedParams, 'maxHeight' | 'colorMode'>> & Pick<EmbedParams, 'maxHeight' | 'colorMode'>;
+
+/** パラメータのデフォルトの値 */
+export const defaultEmbedParams = {
+	maxHeight: undefined,
+	colorMode: undefined,
+	rounded: true,
+	border: true,
+	autoload: false,
+	header: true,
+} as const satisfies EmbedParams;
+
+//#endregion
+
+/**
+ * パラメータを正規化する(埋め込みページ初期化用)
+ * @param searchParams URLSearchParamsもしくはクエリ文字列
+ * @returns 正規化されたパラメータ
+ */
+export function parseEmbedParams(searchParams: URLSearchParams | string): ParsedEmbedParams {
+	let _searchParams: URLSearchParams;
+	if (typeof searchParams === 'string') {
+		_searchParams = new URLSearchParams(searchParams);
+	} else if (searchParams instanceof URLSearchParams) {
+		_searchParams = searchParams;
+	} else {
+		throw new Error('searchParams must be URLSearchParams or string');
+	}
+
+	function convertBoolean(value: string | null): boolean | undefined {
+		if (value === 'true') {
+			return true;
+		} else if (value === 'false') {
+			return false;
+		}
+		return undefined;
+	}
+
+	function convertNumber(value: string | null): number | undefined {
+		if (value != null && !isNaN(Number(value))) {
+			return Number(value);
+		}
+		return undefined;
+	}
+
+	function convertColorMode(value: string | null): 'light' | 'dark' | undefined {
+		if (value != null && ['light', 'dark'].includes(value)) {
+			return value as 'light' | 'dark';
+		}
+		return undefined;
+	}
+
+	return {
+		maxHeight: convertNumber(_searchParams.get('maxHeight')) ?? defaultEmbedParams.maxHeight,
+		colorMode: convertColorMode(_searchParams.get('colorMode')) ?? defaultEmbedParams.colorMode,
+		rounded: convertBoolean(_searchParams.get('rounded')) ?? defaultEmbedParams.rounded,
+		border: convertBoolean(_searchParams.get('border')) ?? defaultEmbedParams.border,
+		autoload: convertBoolean(_searchParams.get('autoload')) ?? defaultEmbedParams.autoload,
+		header: convertBoolean(_searchParams.get('header')) ?? defaultEmbedParams.header,
+	};
+}
diff --git a/packages/frontend/src/scripts/emoji-base.ts b/packages/frontend-shared/js/emoji-base.ts
similarity index 87%
rename from packages/frontend/src/scripts/emoji-base.ts
rename to packages/frontend-shared/js/emoji-base.ts
index a01540a3e4..5fbbc4ea84 100644
--- a/packages/frontend/src/scripts/emoji-base.ts
+++ b/packages/frontend-shared/js/emoji-base.ts
@@ -19,7 +19,7 @@ export function char2fluentEmojiFilePath(char: string): string {
 	// Fluent Emojiは国旗非対応 https://github.com/microsoft/fluentui-emoji/issues/25
 	if (codes[0]?.startsWith('1f1')) return char2twemojiFilePath(char);
 	if (!codes.includes('200d')) codes = codes.filter(x => x !== 'fe0f');
-	codes = codes.filter(x => x && x.length);
-	const fileName = codes.map(x => x!.padStart(4, '0')).join('-');
+	codes = codes.filter(x => x != null && x.length > 0);
+	const fileName = (codes as string[]).map(x => x.padStart(4, '0')).join('-');
 	return `${fluentEmojiPngBase}/${fileName}.png`;
 }
diff --git a/packages/frontend/src/emojilist.json b/packages/frontend-shared/js/emojilist.json
similarity index 100%
rename from packages/frontend/src/emojilist.json
rename to packages/frontend-shared/js/emojilist.json
diff --git a/packages/frontend/src/scripts/emojilist.ts b/packages/frontend-shared/js/emojilist.ts
similarity index 96%
rename from packages/frontend/src/scripts/emojilist.ts
rename to packages/frontend-shared/js/emojilist.ts
index 6565feba97..bde30a864f 100644
--- a/packages/frontend/src/scripts/emojilist.ts
+++ b/packages/frontend-shared/js/emojilist.ts
@@ -12,12 +12,12 @@ export type UnicodeEmojiDef = {
 }
 
 // initial converted from https://github.com/muan/emojilib/commit/242fe68be86ed6536843b83f7e32f376468b38fb
-import _emojilist from '../emojilist.json';
+import _emojilist from './emojilist.json';
 
 export const emojilist: UnicodeEmojiDef[] = _emojilist.map(x => ({
 	name: x[1] as string,
 	char: x[0] as string,
-	category: unicodeEmojiCategories[x[2]],
+	category: unicodeEmojiCategories[x[2] as number],
 }));
 
 const unicodeEmojisMap = new Map<string, UnicodeEmojiDef>(
diff --git a/packages/frontend/src/scripts/extract-avg-color-from-blurhash.ts b/packages/frontend-shared/js/extract-avg-color-from-blurhash.ts
similarity index 100%
rename from packages/frontend/src/scripts/extract-avg-color-from-blurhash.ts
rename to packages/frontend-shared/js/extract-avg-color-from-blurhash.ts
diff --git a/packages/frontend/src/scripts/i18n.ts b/packages/frontend-shared/js/i18n.ts
similarity index 78%
rename from packages/frontend/src/scripts/i18n.ts
rename to packages/frontend-shared/js/i18n.ts
index c2f44a33cc..18232691fa 100644
--- a/packages/frontend/src/scripts/i18n.ts
+++ b/packages/frontend-shared/js/i18n.ts
@@ -2,7 +2,10 @@
  * SPDX-FileCopyrightText: syuilo and misskey-project
  * SPDX-License-Identifier: AGPL-3.0-only
  */
-import type { ILocale, ParameterizedString } from '../../../../locales/index.js';
+import type { ILocale, ParameterizedString } from '../../../locales/index.js';
+
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+type TODO = any;
 
 type FlattenKeys<T extends ILocale, TPrediction> = keyof {
 	[K in keyof T as T[K] extends ILocale
@@ -32,15 +35,18 @@ type Tsx<T extends ILocale> = {
 
 export class I18n<T extends ILocale> {
 	private tsxCache?: Tsx<T>;
+	private devMode: boolean;
+
+	constructor(public locale: T, devMode = false) {
+		this.devMode = devMode;
 
-	constructor(public locale: T) {
 		//#region BIND
 		this.t = this.t.bind(this);
 		//#endregion
 	}
 
 	public get ts(): T {
-		if (_DEV_) {
+		if (this.devMode) {
 			class Handler<TTarget extends ILocale> implements ProxyHandler<TTarget> {
 				get(target: TTarget, p: string | symbol): unknown {
 					const value = target[p as keyof TTarget];
@@ -72,7 +78,7 @@ export class I18n<T extends ILocale> {
 	}
 
 	public get tsx(): Tsx<T> {
-		if (_DEV_) {
+		if (this.devMode) {
 			if (this.tsxCache) {
 				return this.tsxCache;
 			}
@@ -113,7 +119,7 @@ export class I18n<T extends ILocale> {
 							return () => value;
 						}
 
-						return (arg) => {
+						return (arg: TODO) => {
 							let str = quasis[0];
 
 							for (let i = 0; i < expressions.length; i++) {
@@ -137,7 +143,6 @@ export class I18n<T extends ILocale> {
 			return this.tsxCache = new Proxy(this.locale, new Handler()) as unknown as Tsx<T>;
 		}
 
-		// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
 		if (this.tsxCache) {
 			return this.tsxCache;
 		}
@@ -153,7 +158,7 @@ export class I18n<T extends ILocale> {
 				const value = target[k as keyof typeof target];
 
 				if (typeof value === 'object') {
-					result[k] = build(value as ILocale);
+					(result as TODO)[k] = build(value as ILocale);
 				} else if (typeof value === 'string') {
 					const quasis: string[] = [];
 					const expressions: string[] = [];
@@ -180,7 +185,7 @@ export class I18n<T extends ILocale> {
 						continue;
 					}
 
-					result[k] = (arg) => {
+					(result as TODO)[k] = (arg: TODO) => {
 						let str = quasis[0];
 
 						for (let i = 0; i < expressions.length; i++) {
@@ -209,9 +214,9 @@ export class I18n<T extends ILocale> {
 		let str: string | ParameterizedString | ILocale = this.locale;
 
 		for (const k of key.split('.')) {
-			str = str[k];
+			str = (str as TODO)[k];
 
-			if (_DEV_) {
+			if (this.devMode) {
 				if (typeof str === 'undefined') {
 					console.error(`Unexpected locale key: ${key}`);
 					return key;
@@ -220,7 +225,7 @@ export class I18n<T extends ILocale> {
 		}
 
 		if (args) {
-			if (_DEV_) {
+			if (this.devMode) {
 				const missing = Array.from((str as string).matchAll(/\{(\w+)\}/g), ([, parameter]) => parameter).filter(parameter => !Object.hasOwn(args, parameter));
 
 				if (missing.length) {
@@ -231,7 +236,7 @@ export class I18n<T extends ILocale> {
 			for (const [k, v] of Object.entries(args)) {
 				const search = `{${k}}`;
 
-				if (_DEV_) {
+				if (this.devMode) {
 					if (!(str as string).includes(search)) {
 						console.error(`Unexpected locale parameter: ${k} at ${key}`);
 					}
@@ -244,51 +249,3 @@ export class I18n<T extends ILocale> {
 		return str;
 	}
 }
-
-if (import.meta.vitest) {
-	const { describe, expect, it } = import.meta.vitest;
-
-	describe('i18n', () => {
-		it('t', () => {
-			const i18n = new I18n({
-				foo: 'foo',
-				bar: {
-					baz: 'baz',
-					qux: 'qux {0}' as unknown as ParameterizedString<'0'>,
-					quux: 'quux {0} {1}' as unknown as ParameterizedString<'0' | '1'>,
-				},
-			});
-
-			expect(i18n.t('foo')).toBe('foo');
-			expect(i18n.t('bar.baz')).toBe('baz');
-			expect(i18n.tsx.bar.qux({ 0: 'hoge' })).toBe('qux hoge');
-			expect(i18n.tsx.bar.quux({ 0: 'hoge', 1: 'fuga' })).toBe('quux hoge fuga');
-		});
-		it('ts', () => {
-			const i18n = new I18n({
-				foo: 'foo',
-				bar: {
-					baz: 'baz',
-					qux: 'qux {0}' as unknown as ParameterizedString<'0'>,
-					quux: 'quux {0} {1}' as unknown as ParameterizedString<'0' | '1'>,
-				},
-			});
-
-			expect(i18n.ts.foo).toBe('foo');
-			expect(i18n.ts.bar.baz).toBe('baz');
-		});
-		it('tsx', () => {
-			const i18n = new I18n({
-				foo: 'foo',
-				bar: {
-					baz: 'baz',
-					qux: 'qux {0}' as unknown as ParameterizedString<'0'>,
-					quux: 'quux {0} {1}' as unknown as ParameterizedString<'0' | '1'>,
-				},
-			});
-
-			expect(i18n.tsx.bar.qux({ 0: 'hoge' })).toBe('qux hoge');
-			expect(i18n.tsx.bar.quux({ 0: 'hoge', 1: 'fuga' })).toBe('quux hoge fuga');
-		});
-	});
-}
diff --git a/packages/frontend-shared/js/intl-const.ts b/packages/frontend-shared/js/intl-const.ts
new file mode 100644
index 0000000000..33b65b6e9b
--- /dev/null
+++ b/packages/frontend-shared/js/intl-const.ts
@@ -0,0 +1,51 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { lang } from '@@/js/config.js';
+
+// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
+export const versatileLang = (lang ?? 'ja-JP').replace('ja-KS', 'ja-JP');
+
+let _dateTimeFormat: Intl.DateTimeFormat;
+try {
+	_dateTimeFormat = new Intl.DateTimeFormat(versatileLang, {
+		year: 'numeric',
+		month: 'numeric',
+		day: 'numeric',
+		hour: 'numeric',
+		minute: 'numeric',
+		second: 'numeric',
+	});
+} catch (err) {
+	console.warn(err);
+	if (_DEV_) console.log('[Intl] Fallback to en-US');
+
+	// Fallback to en-US
+	_dateTimeFormat = new Intl.DateTimeFormat('en-US', {
+		year: 'numeric',
+		month: 'numeric',
+		day: 'numeric',
+		hour: 'numeric',
+		minute: 'numeric',
+		second: 'numeric',
+	});
+}
+export const dateTimeFormat = _dateTimeFormat;
+
+export const timeZone = dateTimeFormat.resolvedOptions().timeZone;
+
+export const hemisphere = /^(australia|pacific|antarctica|indian)\//i.test(timeZone) ? 'S' : 'N';
+
+let _numberFormat: Intl.NumberFormat;
+try {
+	_numberFormat = new Intl.NumberFormat(versatileLang);
+} catch (err) {
+	console.warn(err);
+	if (_DEV_) console.log('[Intl] Fallback to en-US');
+
+	// Fallback to en-US
+	_numberFormat = new Intl.NumberFormat('en-US');
+}
+export const numberFormat = _numberFormat;
diff --git a/packages/frontend-shared/js/is-link.ts b/packages/frontend-shared/js/is-link.ts
new file mode 100644
index 0000000000..946f86400e
--- /dev/null
+++ b/packages/frontend-shared/js/is-link.ts
@@ -0,0 +1,12 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+export function isLink(el: HTMLElement) {
+	if (el.tagName === 'A') return true;
+	if (el.parentElement) {
+		return isLink(el.parentElement);
+	}
+	return false;
+}
diff --git a/packages/frontend-shared/js/media-proxy.ts b/packages/frontend-shared/js/media-proxy.ts
new file mode 100644
index 0000000000..2837870c9a
--- /dev/null
+++ b/packages/frontend-shared/js/media-proxy.ts
@@ -0,0 +1,63 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import * as Misskey from 'misskey-js';
+import { query } from './url.js';
+
+export class MediaProxy {
+	private serverMetadata: Misskey.entities.MetaDetailed;
+	private url: string;
+
+	constructor(serverMetadata: Misskey.entities.MetaDetailed, url: string) {
+		this.serverMetadata = serverMetadata;
+		this.url = url;
+	}
+
+	public getProxiedImageUrl(imageUrl: string, type?: 'preview' | 'emoji' | 'avatar', mustOrigin = false, noFallback = false): string {
+		const localProxy = `${this.url}/proxy`;
+		let _imageUrl = imageUrl;
+
+		if (imageUrl.startsWith(this.serverMetadata.mediaProxy + '/') || imageUrl.startsWith('/proxy/') || imageUrl.startsWith(localProxy + '/')) {
+			// もう既にproxyっぽそうだったらurlを取り出す
+			_imageUrl = (new URL(imageUrl)).searchParams.get('url') ?? imageUrl;
+		}
+
+		return `${mustOrigin ? localProxy : this.serverMetadata.mediaProxy}/${
+			type === 'preview' ? 'preview.webp'
+			: 'image.webp'
+		}?${query({
+			url: _imageUrl,
+			...(!noFallback ? { 'fallback': '1' } : {}),
+			...(type ? { [type]: '1' } : {}),
+			...(mustOrigin ? { origin: '1' } : {}),
+		})}`;
+	}
+
+	public getProxiedImageUrlNullable(imageUrl: string | null | undefined, type?: 'preview'): string | null {
+		if (imageUrl == null) return null;
+		return this.getProxiedImageUrl(imageUrl, type);
+	}
+
+	public getStaticImageUrl(baseUrl: string): string {
+		const u = baseUrl.startsWith('http') ? new URL(baseUrl) : new URL(baseUrl, this.url);
+
+		if (u.href.startsWith(`${this.url}/emoji/`)) {
+			// もう既にemojiっぽそうだったらsearchParams付けるだけ
+			u.searchParams.set('static', '1');
+			return u.href;
+		}
+
+		if (u.href.startsWith(this.serverMetadata.mediaProxy + '/')) {
+			// もう既にproxyっぽそうだったらsearchParams付けるだけ
+			u.searchParams.set('static', '1');
+			return u.href;
+		}
+
+		return `${this.serverMetadata.mediaProxy}/static.webp?${query({
+			url: u.href,
+			static: '1',
+		})}`;
+	}
+}
diff --git a/packages/frontend/src/scripts/scroll.ts b/packages/frontend-shared/js/scroll.ts
similarity index 75%
rename from packages/frontend/src/scripts/scroll.ts
rename to packages/frontend-shared/js/scroll.ts
index 8edb6fca05..4f2e9105c3 100644
--- a/packages/frontend/src/scripts/scroll.ts
+++ b/packages/frontend-shared/js/scroll.ts
@@ -23,24 +23,40 @@ export function getStickyTop(el: HTMLElement, container: HTMLElement | null = nu
 	return getStickyTop(el.parentElement, container, newTop);
 }
 
+export function getStickyBottom(el: HTMLElement, container: HTMLElement | null = null, bottom = 0) {
+	if (!el.parentElement) return bottom;
+	const data = el.dataset.stickyContainerFooterHeight;
+	const newBottom = data ? Number(data) + bottom : bottom;
+	if (el === container) return newBottom;
+	return getStickyBottom(el.parentElement, container, newBottom);
+}
+
 export function getScrollPosition(el: HTMLElement | null): number {
 	const container = getScrollContainer(el);
 	return container == null ? window.scrollY : container.scrollTop;
 }
 
-export function onScrollTop(el: HTMLElement, cb: () => unknown, tolerance = 1, once = false) {
+export function onScrollTop(el: HTMLElement, cb: (topVisible: boolean) => unknown, tolerance = 1, once = false) {
 	// とりあえず評価してみる
-	if (el.isConnected && isTopVisible(el)) {
-		cb();
+	const firstTopVisible = isTopVisible(el);
+	if (el.isConnected && firstTopVisible) {
+		cb(firstTopVisible);
 		if (once) return null;
 	}
 
 	const container = getScrollContainer(el) ?? window;
 
-	const onScroll = ev => {
+	// 以下のケースにおいて、cbが何度も呼び出されてしまって具合が悪いので1回呼んだら以降は無視するようにする
+	// - スクロールイベントは1回のスクロールで複数回発生することがある
+	// - toleranceの範囲内に収まる程度の微量なスクロールが発生した
+	let prevTopVisible = firstTopVisible;
+	const onScroll = () => {
 		if (!document.body.contains(el)) return;
-		if (isTopVisible(el, tolerance)) {
-			cb();
+
+		const topVisible = isTopVisible(el, tolerance);
+		if (topVisible !== prevTopVisible) {
+			prevTopVisible = topVisible;
+			cb(topVisible);
 			if (once) removeListener();
 		}
 	};
@@ -61,7 +77,7 @@ export function onScrollBottom(el: HTMLElement, cb: () => unknown, tolerance = 1
 	}
 
 	const containerOrWindow = container ?? window;
-	const onScroll = ev => {
+	const onScroll = () => {
 		if (!document.body.contains(el)) return;
 		if (isBottomVisible(el, 1, container)) {
 			cb();
@@ -118,6 +134,7 @@ export function scrollToBottom(
 
 export function isTopVisible(el: HTMLElement, tolerance = 1): boolean {
 	const scrollTop = getScrollPosition(el);
+	if (_DEV_) console.log(scrollTop, tolerance, scrollTop <= tolerance);
 	return scrollTop <= tolerance;
 }
 
diff --git a/packages/frontend/src/scripts/url.ts b/packages/frontend-shared/js/url.ts
similarity index 55%
rename from packages/frontend/src/scripts/url.ts
rename to packages/frontend-shared/js/url.ts
index e3072b3b7d..eb830b1eea 100644
--- a/packages/frontend/src/scripts/url.ts
+++ b/packages/frontend-shared/js/url.ts
@@ -8,16 +8,21 @@
  * 2. プロパティがundefinedの時はクエリを付けない
  * (new URLSearchParams(obj)ではそこまで丁寧なことをしてくれない)
  */
-export function query(obj: Record<string, any>): string {
+export function query(obj: Record<string, string | number | boolean>): string {
 	const params = Object.entries(obj)
-		.filter(([, v]) => Array.isArray(v) ? v.length : v !== undefined)
-		.reduce((a, [k, v]) => (a[k] = v, a), {} as Record<string, any>);
+		.filter(([, v]) => Array.isArray(v) ? v.length : v !== undefined) // eslint-disable-line @typescript-eslint/no-unnecessary-condition
+		.reduce<Record<string, string | number | boolean>>((a, [k, v]) => (a[k] = v, a), {});
 
 	return Object.entries(params)
 		.map((p) => `${p[0]}=${encodeURIComponent(p[1])}`)
 		.join('&');
 }
 
-export function appendQuery(url: string, query: string): string {
-	return `${url}${/\?/.test(url) ? url.endsWith('?') ? '' : '&' : '?'}${query}`;
+export function appendQuery(url: string, queryString: string): string {
+	return `${url}${/\?/.test(url) ? url.endsWith('?') ? '' : '&' : '?'}${queryString}`;
+}
+
+export function extractDomain(url: string) {
+	const match = url.match(/^(?:https?:)?(?:\/\/)?(?:[^@\n]+@)?([^:\/\n]+)/im);
+	return match ? match[1] : null;
 }
diff --git a/packages/frontend/src/scripts/use-document-visibility.ts b/packages/frontend-shared/js/use-document-visibility.ts
similarity index 85%
rename from packages/frontend/src/scripts/use-document-visibility.ts
rename to packages/frontend-shared/js/use-document-visibility.ts
index a8f4d5e03a..b1197e68da 100644
--- a/packages/frontend/src/scripts/use-document-visibility.ts
+++ b/packages/frontend-shared/js/use-document-visibility.ts
@@ -3,7 +3,8 @@
  * SPDX-License-Identifier: AGPL-3.0-only
  */
 
-import { onMounted, onUnmounted, ref, Ref } from 'vue';
+import { onMounted, onUnmounted, ref } from 'vue';
+import type { Ref } from 'vue';
 
 export function useDocumentVisibility(): Ref<DocumentVisibilityState> {
 	const visibility = ref(document.visibilityState);
diff --git a/packages/frontend/src/scripts/use-interval.ts b/packages/frontend-shared/js/use-interval.ts
similarity index 100%
rename from packages/frontend/src/scripts/use-interval.ts
rename to packages/frontend-shared/js/use-interval.ts
diff --git a/packages/frontend/src/scripts/worker-multi-dispatch.ts b/packages/frontend-shared/js/worker-multi-dispatch.ts
similarity index 84%
rename from packages/frontend/src/scripts/worker-multi-dispatch.ts
rename to packages/frontend-shared/js/worker-multi-dispatch.ts
index 6b3fcd9383..5d393ed1ed 100644
--- a/packages/frontend/src/scripts/worker-multi-dispatch.ts
+++ b/packages/frontend-shared/js/worker-multi-dispatch.ts
@@ -3,16 +3,18 @@
  * SPDX-License-Identifier: AGPL-3.0-only
  */
 
-function defaultUseWorkerNumber(prev: number, totalWorkers: number) {
+function defaultUseWorkerNumber(prev: number) {
 	return prev + 1;
 }
 
-export class WorkerMultiDispatch<POST = any, RETURN = any> {
+type WorkerNumberGetter = (prev: number, totalWorkers: number) => number;
+
+export class WorkerMultiDispatch<POST = unknown, RETURN = unknown> {
 	private symbol = Symbol('WorkerMultiDispatch');
 	private workers: Worker[] = [];
 	private terminated = false;
 	private prevWorkerNumber = 0;
-	private getUseWorkerNumber = defaultUseWorkerNumber;
+	private getUseWorkerNumber: WorkerNumberGetter;
 	private finalizationRegistry: FinalizationRegistry<symbol>;
 
 	constructor(workerConstructor: () => Worker, concurrency: number, getUseWorkerNumber = defaultUseWorkerNumber) {
@@ -29,7 +31,7 @@ export class WorkerMultiDispatch<POST = any, RETURN = any> {
 		if (_DEV_) console.log('WorkerMultiDispatch: Created', this);
 	}
 
-	public postMessage(message: POST, options?: Transferable[] | StructuredSerializeOptions, useWorkerNumber: typeof defaultUseWorkerNumber = this.getUseWorkerNumber) {
+	public postMessage(message: POST, options?: Transferable[] | StructuredSerializeOptions, useWorkerNumber: WorkerNumberGetter = this.getUseWorkerNumber) {
 		let workerNumber = useWorkerNumber(this.prevWorkerNumber, this.workers.length);
 		workerNumber = Math.abs(Math.round(workerNumber)) % this.workers.length;
 		if (_DEV_) console.log('WorkerMultiDispatch: Posting message to worker', workerNumber, useWorkerNumber);
@@ -46,12 +48,14 @@ export class WorkerMultiDispatch<POST = any, RETURN = any> {
 		return workerNumber;
 	}
 
+	// eslint-disable-next-line @typescript-eslint/no-explicit-any
 	public addListener(callback: (this: Worker, ev: MessageEvent<RETURN>) => any, options?: boolean | AddEventListenerOptions) {
 		this.workers.forEach(worker => {
 			worker.addEventListener('message', callback, options);
 		});
 	}
 
+	// eslint-disable-next-line @typescript-eslint/no-explicit-any
 	public removeListener(callback: (this: Worker, ev: MessageEvent<RETURN>) => any, options?: boolean | AddEventListenerOptions) {
 		this.workers.forEach(worker => {
 			worker.removeEventListener('message', callback, options);
diff --git a/packages/frontend-shared/package.json b/packages/frontend-shared/package.json
new file mode 100644
index 0000000000..9981d10dd2
--- /dev/null
+++ b/packages/frontend-shared/package.json
@@ -0,0 +1,39 @@
+{
+	"name": "frontend-shared",
+	"type": "module",
+	"main": "./js-built/index.js",
+	"types": "./js-built/index.d.ts",
+	"exports": {
+		".": {
+			"import": "./js-built/index.js",
+			"types": "./js-built/index.d.ts"
+		},
+		"./*": {
+			"import": "./js-built/*",
+			"types": "./js-built/*"
+		}
+	},
+	"scripts": {
+		"build": "node ./build.js",
+		"watch": "nodemon -w package.json -e json --exec \"node ./build.js --watch\"",
+		"eslint": "eslint './**/*.{js,jsx,ts,tsx}'",
+		"typecheck": "tsc --noEmit",
+		"lint": "pnpm typecheck && pnpm eslint"
+	},
+	"devDependencies": {
+		"@types/node": "20.14.12",
+		"@typescript-eslint/eslint-plugin": "7.17.0",
+		"@typescript-eslint/parser": "7.17.0",
+		"esbuild": "0.23.0",
+		"eslint-plugin-vue": "9.27.0",
+		"typescript": "5.5.4",
+		"vue-eslint-parser": "9.4.3"
+	},
+	"files": [
+		"js-built"
+	],
+	"dependencies": {
+		"misskey-js": "workspace:*",
+		"vue": "3.4.37"
+	}
+}
diff --git a/packages/frontend/src/themes/_dark.json5 b/packages/frontend-shared/themes/_dark.json5
similarity index 85%
rename from packages/frontend/src/themes/_dark.json5
rename to packages/frontend-shared/themes/_dark.json5
index c82a956868..5271785e62 100644
--- a/packages/frontend/src/themes/_dark.json5
+++ b/packages/frontend-shared/themes/_dark.json5
@@ -13,6 +13,7 @@
 		accentDarken: ':darken<10<@accent',
 		accentLighten: ':lighten<10<@accent',
 		accentedBg: ':alpha<0.15<@accent',
+		love: '#dd2e44',
 		focus: ':alpha<0.3<@accent',
 		bg: '#000',
 		acrylicBg: ':alpha<0.5<@bg',
@@ -29,7 +30,7 @@
 		panelHeaderBg: ':lighten<3<@panel',
 		panelHeaderFg: '@fg',
 		panelHeaderDivider: 'rgba(0, 0, 0, 0)',
-		panelBorder: '" solid 1px var(--divider)',
+		panelBorder: '" solid 1px var(--MI_THEME-divider)',
 		acrylicPanel: ':alpha<0.5<@panel',
 		windowHeader: ':alpha<0.85<@panel',
 		popup: ':lighten<3<@panel',
@@ -53,18 +54,19 @@
 		infoFg: '#fff',
 		infoWarnBg: '#42321c',
 		infoWarnFg: '#ffbd3e',
-		switchBg: 'rgba(255, 255, 255, 0.15)',
-		buttonBg: 'rgba(255, 255, 255, 0.05)',
-		buttonHoverBg: 'rgba(255, 255, 255, 0.1)',
+		folderHeaderBg: 'rgba(255, 255, 255, 0.05)',
+		folderHeaderHoverBg: 'rgba(255, 255, 255, 0.1)',
+		buttonBg: ':lighten<5<@panel',
+		buttonHoverBg: ':lighten<10<@panel',
 		buttonGradateA: '@accent',
 		buttonGradateB: ':hue<20<@accent',
+		switchBg: 'rgba(255, 255, 255, 0.15)',
 		switchOffBg: 'rgba(255, 255, 255, 0.1)',
 		switchOffFg: ':alpha<0.8<@fg',
 		switchOnBg: '@accentedBg',
 		switchOnFg: '@accent',
 		inputBorder: 'rgba(255, 255, 255, 0.1)',
 		inputBorderHover: 'rgba(255, 255, 255, 0.2)',
-		listItemHoverBg: 'rgba(255, 255, 255, 0.03)',
 		driveFolderBg: ':alpha<0.3<@accent',
 		wallpaperOverlay: 'rgba(0, 0, 0, 0.5)',
 		badge: '#31b1ce',
@@ -77,22 +79,14 @@
 		codeBoolean: '#c59eff',
 		deckBg: '#000',
 		htmlThemeColor: '@bg',
-		X2: ':darken<2<@panel',
 		X3: 'rgba(255, 255, 255, 0.05)',
 		X4: 'rgba(255, 255, 255, 0.1)',
 		X5: 'rgba(255, 255, 255, 0.05)',
 		X6: 'rgba(255, 255, 255, 0.15)',
 		X7: 'rgba(255, 255, 255, 0.05)',
-		X8: ':lighten<5<@accent',
-		X9: ':darken<5<@accent',
-		X10: ':alpha<0.4<@accent',
 		X11: 'rgba(0, 0, 0, 0.3)',
 		X12: 'rgba(255, 255, 255, 0.1)',
 		X13: 'rgba(255, 255, 255, 0.15)',
-		X14: ':alpha<0.5<@navBg',
-		X15: ':alpha<0<@panel',
-		X16: ':alpha<0.7<@panel',
-		X17: ':alpha<0.8<@bg',
 	},
 
 	codeHighlighter: {
diff --git a/packages/frontend/src/themes/_light.json5 b/packages/frontend-shared/themes/_light.json5
similarity index 86%
rename from packages/frontend/src/themes/_light.json5
rename to packages/frontend-shared/themes/_light.json5
index 63bc030916..be331ce58f 100644
--- a/packages/frontend/src/themes/_light.json5
+++ b/packages/frontend-shared/themes/_light.json5
@@ -13,6 +13,7 @@
 		accentDarken: ':darken<10<@accent',
 		accentLighten: ':lighten<10<@accent',
 		accentedBg: ':alpha<0.15<@accent',
+		love: '#dd2e44',
 		focus: ':alpha<0.3<@accent',
 		bg: '#fff',
 		acrylicBg: ':alpha<0.5<@bg',
@@ -29,7 +30,7 @@
 		panelHeaderBg: ':lighten<3<@panel',
 		panelHeaderFg: '@fg',
 		panelHeaderDivider: 'rgba(0, 0, 0, 0)',
-		panelBorder: '" solid 1px var(--divider)',
+		panelBorder: '" solid 1px var(--MI_THEME-divider)',
 		acrylicPanel: ':alpha<0.5<@panel',
 		windowHeader: ':alpha<0.85<@panel',
 		popup: ':lighten<3<@panel',
@@ -53,18 +54,19 @@
 		infoFg: '#72818a',
 		infoWarnBg: '#fff0db',
 		infoWarnFg: '#8f6e31',
-		switchBg: 'rgba(0, 0, 0, 0.15)',
-		buttonBg: 'rgba(0, 0, 0, 0.05)',
-		buttonHoverBg: 'rgba(0, 0, 0, 0.1)',
+		folderHeaderBg: 'rgba(0, 0, 0, 0.05)',
+		folderHeaderHoverBg: 'rgba(0, 0, 0, 0.1)',
+		buttonBg: ':darken<5<@panel',
+		buttonHoverBg: ':darken<10<@panel',
 		buttonGradateA: '@accent',
 		buttonGradateB: ':hue<20<@accent',
+		switchBg: 'rgba(0, 0, 0, 0.15)',
 		switchOffBg: 'rgba(0, 0, 0, 0.1)',
 		switchOffFg: '@panel',
 		switchOnBg: '@accent',
 		switchOnFg: '@fgOnAccent',
 		inputBorder: 'rgba(0, 0, 0, 0.1)',
 		inputBorderHover: 'rgba(0, 0, 0, 0.2)',
-		listItemHoverBg: 'rgba(0, 0, 0, 0.03)',
 		driveFolderBg: ':alpha<0.3<@accent',
 		wallpaperOverlay: 'rgba(255, 255, 255, 0.5)',
 		badge: '#31b1ce',
@@ -77,22 +79,14 @@
 		codeBoolean: '#62b70c',
 		deckBg: ':darken<3<@bg',
 		htmlThemeColor: '@bg',
-		X2: ':darken<2<@panel',
 		X3: 'rgba(0, 0, 0, 0.05)',
 		X4: 'rgba(0, 0, 0, 0.1)',
 		X5: 'rgba(0, 0, 0, 0.05)',
 		X6: 'rgba(0, 0, 0, 0.25)',
 		X7: 'rgba(0, 0, 0, 0.05)',
-		X8: ':lighten<5<@accent',
-		X9: ':darken<5<@accent',
-		X10: ':alpha<0.4<@accent',
 		X11: 'rgba(0, 0, 0, 0.1)',
 		X12: 'rgba(0, 0, 0, 0.1)',
 		X13: 'rgba(0, 0, 0, 0.15)',
-		X14: ':alpha<0.5<@navBg',
-		X15: ':alpha<0<@panel',
-		X16: ':alpha<0.7<@panel',
-		X17: ':alpha<0.8<@bg',
 	},
 
 	codeHighlighter: {
diff --git a/packages/frontend/src/themes/d-astro.json5 b/packages/frontend-shared/themes/d-astro.json5
similarity index 82%
rename from packages/frontend/src/themes/d-astro.json5
rename to packages/frontend-shared/themes/d-astro.json5
index fee25cc4a4..4422526a33 100644
--- a/packages/frontend/src/themes/d-astro.json5
+++ b/packages/frontend-shared/themes/d-astro.json5
@@ -25,7 +25,6 @@
 		mention: '#ffd152',
 		modalBg: 'rgba(0, 0, 0, 0.5)',
 		success: '#86b300',
-		buttonBg: 'rgba(255, 255, 255, 0.05)',
 		acrylicBg: ':alpha<0.5<@bg',
 		indicator: '@accent',
 		mentionMe: '#fb5d38',
@@ -37,12 +36,11 @@
 		dateLabelFg: '@fg',
 		inputBorder: 'rgba(255, 255, 255, 0.1)',
 		inputBorderHover: 'rgba(255, 255, 255, 0.2)',
-		panelBorder: '" solid 1px var(--divider)',
+		panelBorder: '" solid 1px var(--MI_THEME-divider)',
 		accentDarken: ':darken<10<@accent',
 		acrylicPanel: ':alpha<0.5<@panel',
 		navIndicator: '@accent',
 		accentLighten: ':lighten<10<@accent',
-		buttonHoverBg: 'rgba(255, 255, 255, 0.1)',
 		buttonGradateA: '@accent',
 		buttonGradateB: ':hue<-20<@accent',
 		driveFolderBg: ':alpha<0.3<@accent',
@@ -52,25 +50,17 @@
 		htmlThemeColor: '@bg',
 		fgOnWhite: '@accent',
 		panelHighlight: ':lighten<3<@panel',
-		listItemHoverBg: 'rgba(255, 255, 255, 0.03)',
 		scrollbarHandle: 'rgba(255, 255, 255, 0.2)',
 		wallpaperOverlay: 'rgba(0, 0, 0, 0.5)',
 		panelHeaderDivider: 'rgba(0, 0, 0, 0)',
 		scrollbarHandleHover: 'rgba(255, 255, 255, 0.4)',
-		X2: ':darken<2<@panel',
 		X3: 'rgba(255, 255, 255, 0.05)',
 		X4: 'rgba(255, 255, 255, 0.1)',
 		X5: 'rgba(255, 255, 255, 0.05)',
 		X6: 'rgba(255, 255, 255, 0.15)',
 		X7: 'rgba(255, 255, 255, 0.05)',
-		X8: ':lighten<5<@accent',
-		X9: ':darken<5<@accent',
-		X10: ':alpha<0.4<@accent',
 		X11: 'rgba(0, 0, 0, 0.3)',
 		X12: 'rgba(255, 255, 255, 0.1)',
 		X13: 'rgba(255, 255, 255, 0.15)',
-		X14: ':alpha<0.5<@navBg',
-		X15: ':alpha<0<@panel',
-		X16: ':alpha<0.7<@panel',
 	},
 }
diff --git a/packages/frontend/src/themes/d-botanical.json5 b/packages/frontend-shared/themes/d-botanical.json5
similarity index 100%
rename from packages/frontend/src/themes/d-botanical.json5
rename to packages/frontend-shared/themes/d-botanical.json5
diff --git a/packages/frontend/src/themes/d-cherry.json5 b/packages/frontend-shared/themes/d-cherry.json5
similarity index 100%
rename from packages/frontend/src/themes/d-cherry.json5
rename to packages/frontend-shared/themes/d-cherry.json5
diff --git a/packages/frontend/src/themes/d-dark.json5 b/packages/frontend-shared/themes/d-dark.json5
similarity index 100%
rename from packages/frontend/src/themes/d-dark.json5
rename to packages/frontend-shared/themes/d-dark.json5
diff --git a/packages/frontend/src/themes/d-future.json5 b/packages/frontend-shared/themes/d-future.json5
similarity index 100%
rename from packages/frontend/src/themes/d-future.json5
rename to packages/frontend-shared/themes/d-future.json5
diff --git a/packages/frontend/src/themes/d-green-lime.json5 b/packages/frontend-shared/themes/d-green-lime.json5
similarity index 100%
rename from packages/frontend/src/themes/d-green-lime.json5
rename to packages/frontend-shared/themes/d-green-lime.json5
diff --git a/packages/frontend/src/themes/d-green-orange.json5 b/packages/frontend-shared/themes/d-green-orange.json5
similarity index 100%
rename from packages/frontend/src/themes/d-green-orange.json5
rename to packages/frontend-shared/themes/d-green-orange.json5
diff --git a/packages/frontend/src/themes/d-ice.json5 b/packages/frontend-shared/themes/d-ice.json5
similarity index 100%
rename from packages/frontend/src/themes/d-ice.json5
rename to packages/frontend-shared/themes/d-ice.json5
diff --git a/packages/frontend/src/themes/d-persimmon.json5 b/packages/frontend-shared/themes/d-persimmon.json5
similarity index 100%
rename from packages/frontend/src/themes/d-persimmon.json5
rename to packages/frontend-shared/themes/d-persimmon.json5
diff --git a/packages/frontend/src/themes/d-u0.json5 b/packages/frontend-shared/themes/d-u0.json5
similarity index 89%
rename from packages/frontend/src/themes/d-u0.json5
rename to packages/frontend-shared/themes/d-u0.json5
index 3bd0b9483c..fb707c74c3 100644
--- a/packages/frontend/src/themes/d-u0.json5
+++ b/packages/frontend-shared/themes/d-u0.json5
@@ -3,14 +3,11 @@
 	base: 'dark',
 	name: 'Mi U0 Dark',
 	props: {
-		X2: ':darken<2<@panel',
 		X3: 'rgba(255, 255, 255, 0.05)',
 		X4: 'rgba(255, 255, 255, 0.1)',
 		X5: 'rgba(255, 255, 255, 0.05)',
 		X6: 'rgba(255, 255, 255, 0.15)',
 		X7: 'rgba(255, 255, 255, 0.05)',
-		X8: ':lighten<5<@accent',
-		X9: ':darken<5<@accent',
 		bg: '#172426',
 		fg: '#dadada',
 		X10: ':alpha<0.4<@accent',
@@ -41,7 +38,6 @@
 		mention: '@accent',
 		modalBg: 'rgba(0, 0, 0, 0.5)',
 		success: '#86b300',
-		buttonBg: 'rgba(255, 255, 255, 0.05)',
 		switchBg: 'rgba(255, 255, 255, 0.15)',
 		acrylicBg: ':alpha<0.5<@bg',
 		indicator: '@accent',
@@ -59,12 +55,11 @@
 		codeBoolean: '#c59eff',
 		dateLabelFg: '@fg',
 		inputBorder: 'rgba(255, 255, 255, 0.1)',
-		panelBorder: '" solid 1px var(--divider)',
+		panelBorder: '" solid 1px var(--MI_THEME-divider)',
 		accentDarken: ':darken<10<@accent',
 		acrylicPanel: ':alpha<0.5<@panel',
 		navIndicator: '@indicator',
 		accentLighten: ':lighten<10<@accent',
-		buttonHoverBg: 'rgba(255, 255, 255, 0.1)',
 		driveFolderBg: ':alpha<0.3<@accent',
 		fgHighlighted: ':lighten<3<@fg',
 		fgTransparent: ':alpha<0.5<@fg',
@@ -74,7 +69,6 @@
 		buttonGradateB: ':hue<20<@accent',
 		htmlThemeColor: '@bg',
 		panelHighlight: ':lighten<3<@panel',
-		listItemHoverBg: 'rgba(255, 255, 255, 0.03)',
 		scrollbarHandle: 'rgba(255, 255, 255, 0.2)',
 		inputBorderHover: 'rgba(255, 255, 255, 0.2)',
 		wallpaperOverlay: 'rgba(0, 0, 0, 0.5)',
diff --git a/packages/frontend/src/themes/l-apricot.json5 b/packages/frontend-shared/themes/l-apricot.json5
similarity index 100%
rename from packages/frontend/src/themes/l-apricot.json5
rename to packages/frontend-shared/themes/l-apricot.json5
diff --git a/packages/frontend/src/themes/l-botanical.json5 b/packages/frontend-shared/themes/l-botanical.json5
similarity index 100%
rename from packages/frontend/src/themes/l-botanical.json5
rename to packages/frontend-shared/themes/l-botanical.json5
diff --git a/packages/frontend/src/themes/l-cherry.json5 b/packages/frontend-shared/themes/l-cherry.json5
similarity index 100%
rename from packages/frontend/src/themes/l-cherry.json5
rename to packages/frontend-shared/themes/l-cherry.json5
diff --git a/packages/frontend/src/themes/l-coffee.json5 b/packages/frontend-shared/themes/l-coffee.json5
similarity index 100%
rename from packages/frontend/src/themes/l-coffee.json5
rename to packages/frontend-shared/themes/l-coffee.json5
diff --git a/packages/frontend/src/themes/l-light.json5 b/packages/frontend-shared/themes/l-light.json5
similarity index 100%
rename from packages/frontend/src/themes/l-light.json5
rename to packages/frontend-shared/themes/l-light.json5
diff --git a/packages/frontend/src/themes/l-rainy.json5 b/packages/frontend-shared/themes/l-rainy.json5
similarity index 100%
rename from packages/frontend/src/themes/l-rainy.json5
rename to packages/frontend-shared/themes/l-rainy.json5
diff --git a/packages/frontend/src/themes/l-sushi.json5 b/packages/frontend-shared/themes/l-sushi.json5
similarity index 100%
rename from packages/frontend/src/themes/l-sushi.json5
rename to packages/frontend-shared/themes/l-sushi.json5
diff --git a/packages/frontend/src/themes/l-u0.json5 b/packages/frontend-shared/themes/l-u0.json5
similarity index 92%
rename from packages/frontend/src/themes/l-u0.json5
rename to packages/frontend-shared/themes/l-u0.json5
index dbc777d493..7062e7fe5b 100644
--- a/packages/frontend/src/themes/l-u0.json5
+++ b/packages/frontend-shared/themes/l-u0.json5
@@ -3,14 +3,11 @@
 	base: 'light',
 	name: 'Mi U0 Light',
 	props: {
-		X2: ':darken<2<@panel',
 		X3: 'rgba(255, 255, 255, 0.05)',
 		X4: 'rgba(255, 255, 255, 0.1)',
 		X5: 'rgba(255, 255, 255, 0.05)',
 		X6: 'rgba(255, 255, 255, 0.15)',
 		X7: 'rgba(255, 255, 255, 0.05)',
-		X8: ':lighten<5<@accent',
-		X9: ':darken<5<@accent',
 		bg: '#e7e7eb',
 		fg: '#5f5f5f',
 		X10: ':alpha<0.4<@accent',
@@ -59,7 +56,7 @@
 		codeBoolean: '#c59eff',
 		dateLabelFg: '@fg',
 		inputBorder: 'rgba(255, 255, 255, 0.1)',
-		panelBorder: '" solid 1px var(--divider)',
+		panelBorder: '" solid 1px var(--MI_THEME-divider)',
 		accentDarken: ':darken<10<@accent',
 		acrylicPanel: ':alpha<0.5<@panel',
 		navIndicator: '@indicator',
@@ -74,7 +71,6 @@
 		buttonGradateB: ':hue<20<@accent',
 		htmlThemeColor: '@bg',
 		panelHighlight: ':lighten<3<@panel',
-		listItemHoverBg: 'rgba(255, 255, 255, 0.03)',
 		scrollbarHandle: '#74747433',
 		inputBorderHover: 'rgba(255, 255, 255, 0.2)',
 		wallpaperOverlay: 'rgba(0, 0, 0, 0.5)',
diff --git a/packages/frontend/src/themes/l-vivid.json5 b/packages/frontend-shared/themes/l-vivid.json5
similarity index 82%
rename from packages/frontend/src/themes/l-vivid.json5
rename to packages/frontend-shared/themes/l-vivid.json5
index 3368855b5e..39768d4ac6 100644
--- a/packages/frontend/src/themes/l-vivid.json5
+++ b/packages/frontend-shared/themes/l-vivid.json5
@@ -28,7 +28,6 @@
 		mention: '@accent',
 		modalBg: 'rgba(0, 0, 0, 0.3)',
 		success: '#86b300',
-		buttonBg: 'rgba(0, 0, 0, 0.05)',
 		acrylicBg: ':alpha<0.5<@bg',
 		indicator: '@accent',
 		mentionMe: '@mention',
@@ -40,12 +39,11 @@
 		dateLabelFg: '@fg',
 		inputBorder: 'rgba(0, 0, 0, 0.1)',
 		inputBorderHover: 'rgba(0, 0, 0, 0.2)',
-		panelBorder: '" solid 1px var(--divider)',
+		panelBorder: '" solid 1px var(--MI_THEME-divider)',
 		accentDarken: ':darken<10<@accent',
 		acrylicPanel: ':alpha<0.5<@panel',
 		navIndicator: '@accent',
 		accentLighten: ':lighten<10<@accent',
-		buttonHoverBg: 'rgba(0, 0, 0, 0.1)',
 		driveFolderBg: ':alpha<0.3<@accent',
 		fgHighlighted: ':darken<3<@fg',
 		fgTransparent: ':alpha<0.5<@fg',
@@ -54,27 +52,18 @@
 		panelHeaderFg: '@fg',
 		htmlThemeColor: '@bg',
 		panelHighlight: ':darken<3<@panel',
-		listItemHoverBg: 'rgba(0, 0, 0, 0.03)',
 		scrollbarHandle: 'rgba(0, 0, 0, 0.2)',
 		wallpaperOverlay: 'rgba(255, 255, 255, 0.5)',
 		fgTransparentWeak: ':alpha<0.75<@fg',
 		panelHeaderDivider: '@divider',
 		scrollbarHandleHover: 'rgba(0, 0, 0, 0.4)',
-		X2: ':darken<2<@panel',
 		X3: 'rgba(0, 0, 0, 0.05)',
 		X4: 'rgba(0, 0, 0, 0.1)',
 		X5: 'rgba(0, 0, 0, 0.05)',
 		X6: 'rgba(0, 0, 0, 0.25)',
 		X7: 'rgba(0, 0, 0, 0.05)',
-		X8: ':lighten<5<@accent',
-		X9: ':darken<5<@accent',
-		X10: ':alpha<0.4<@accent',
 		X11: 'rgba(0, 0, 0, 0.1)',
 		X12: 'rgba(0, 0, 0, 0.1)',
 		X13: 'rgba(0, 0, 0, 0.15)',
-		X14: ':alpha<0.5<@navBg',
-		X15: ':alpha<0<@panel',
-		X16: ':alpha<0.7<@panel',
-		X17: ':alpha<0.8<@bg',
 	},
 }
diff --git a/packages/frontend-shared/tsconfig.json b/packages/frontend-shared/tsconfig.json
new file mode 100644
index 0000000000..09a8ff76aa
--- /dev/null
+++ b/packages/frontend-shared/tsconfig.json
@@ -0,0 +1,41 @@
+{
+	"$schema": "https://json.schemastore.org/tsconfig",
+	"compilerOptions": {
+		"target": "ES2022",
+		"module": "nodenext",
+		"moduleResolution": "nodenext",
+		"declaration": true,
+		"declarationMap": true,
+		"sourceMap": false,
+		"outDir": "./js-built/",
+		"removeComments": true,
+		"resolveJsonModule": true,
+		"strict": true,
+		"strictFunctionTypes": true,
+		"strictNullChecks": true,
+		"experimentalDecorators": true,
+		"noImplicitReturns": true,
+		"esModuleInterop": true,
+		"baseUrl": ".",
+		"paths": {
+			"@/*": ["./*"],
+			"@@/*": ["./*"]
+		},
+		"typeRoots": [
+			"./@types",
+			"./node_modules/@types"
+		],
+		"lib": [
+			"esnext",
+			"dom"
+		]
+	},
+	"include": [
+		"@types/**/*.ts",
+		"js/**/*"
+	],
+	"exclude": [
+		"node_modules",
+		"test/**/*"
+	]
+}
diff --git a/packages/frontend/.eslintrc.cjs b/packages/frontend/.eslintrc.cjs
deleted file mode 100644
index 20f88dc078..0000000000
--- a/packages/frontend/.eslintrc.cjs
+++ /dev/null
@@ -1,82 +0,0 @@
-module.exports = {
-	root: true,
-	env: {
-		'node': false,
-	},
-	parser: 'vue-eslint-parser',
-	parserOptions: {
-		'parser': '@typescript-eslint/parser',
-		tsconfigRootDir: __dirname,
-		project: ['./tsconfig.json'],
-		extraFileExtensions: ['.vue'],
-	},
-	extends: [
-		'../shared/.eslintrc.js',
-		'plugin:vue/vue3-recommended',
-	],
-	rules: {
-		'@typescript-eslint/no-empty-interface': [
-			'error',
-			{
-				'allowSingleExtends': true,
-			},
-		],
-		// window の禁止理由: グローバルスコープと衝突し、予期せぬ結果を招くため
-		// e の禁止理由: error や event など、複数のキーワードの頭文字であり分かりにくいため
-		'id-denylist': ['error', 'window', 'e'],
-		'no-shadow': ['warn'],
-		'vue/attributes-order': ['error', {
-			'alphabetical': false,
-		}],
-		'vue/no-use-v-if-with-v-for': ['error', {
-			'allowUsingIterationVar': false,
-		}],
-		'vue/no-ref-as-operand': 'error',
-		'vue/no-multi-spaces': ['error', {
-			'ignoreProperties': false,
-		}],
-		'vue/no-v-html': 'warn',
-		'vue/order-in-components': 'error',
-		'vue/html-indent': ['warn', 'tab', {
-			'attribute': 1,
-			'baseIndent': 0,
-			'closeBracket': 0,
-			'alignAttributesVertically': true,
-			'ignores': [],
-		}],
-		'vue/html-closing-bracket-spacing': ['warn', {
-			'startTag': 'never',
-			'endTag': 'never',
-			'selfClosingTag': 'never',
-		}],
-		'vue/multi-word-component-names': 'warn',
-		'vue/require-v-for-key': 'warn',
-		'vue/no-unused-components': 'warn',
-		'vue/no-unused-vars': 'warn',
-		'vue/no-dupe-keys': 'warn',
-		'vue/valid-v-for': 'warn',
-		'vue/return-in-computed-property': 'warn',
-		'vue/no-setup-props-destructure': 'warn',
-		'vue/max-attributes-per-line': 'off',
-		'vue/html-self-closing': 'off',
-		'vue/singleline-html-element-content-newline': 'off',
-		'vue/v-on-event-hyphenation': ['error', 'never', { autofix: true }],
-		'vue/attribute-hyphenation': ['error', 'never'],
-	},
-	globals: {
-		// Node.js
-		'module': false,
-		'require': false,
-		'__dirname': false,
-
-		// Misskey
-		'_DEV_': false,
-		'_LANGS_': false,
-		'_VERSION_': false,
-		'_ENV_': false,
-		'_PERF_PREFIX_': false,
-		'_DATA_TRANSFER_DRIVE_FILE_': false,
-		'_DATA_TRANSFER_DRIVE_FOLDER_': false,
-		'_DATA_TRANSFER_DECK_COLUMN_': false,
-	},
-};
diff --git a/packages/frontend/.storybook/changes.ts b/packages/frontend/.storybook/changes.ts
index 7c70972e1e..1299910499 100644
--- a/packages/frontend/.storybook/changes.ts
+++ b/packages/frontend/.storybook/changes.ts
@@ -47,14 +47,12 @@ await fs.readFile(
 				)
 			)
 			.map((path) => path.replace(/(?:(?<=\.stories)\.(?:impl|meta)|\.msw)(?=\.ts$)/g, ''))
-			.map((path) => (path.startsWith('.') ? path : `./${path}`))
 	);
 	if (
 		micromatch(Array.from(modules), [
 			'../../assets/**',
 			'../../fluent-emojis/**',
 			'../../locales/ja-JP.yml',
-			'../../misskey-assets/**',
 			'assets/**',
 			'public/**',
 			'../../pnpm-lock.yaml',
diff --git a/packages/frontend/.storybook/charts.ts b/packages/frontend/.storybook/charts.ts
new file mode 100644
index 0000000000..5015012a82
--- /dev/null
+++ b/packages/frontend/.storybook/charts.ts
@@ -0,0 +1,48 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { DefaultBodyType, HttpResponse, HttpResponseResolver, JsonBodyType, PathParams, http } from 'msw';
+import seedrandom from 'seedrandom';
+import { action } from '@storybook/addon-actions';
+
+function getChartArray(seed: string, limit: number, option?: { accumulate?: boolean, mul?: number }): number[] {
+	const rng = seedrandom(seed);
+	const max = Math.floor(option?.mul ?? 250 * rng());
+	let accumulation = 0;
+	const array: number[] = [];
+	for (let i = 0; i < limit; i++) {
+		const num = Math.floor((max + 1) * rng());
+		if (option?.accumulate) {
+			accumulation += num;
+			array.unshift(accumulation);
+		} else {
+			array.push(num);
+		}
+	}
+	return array;
+}
+
+export function getChartResolver(fields: string[], option?: { accumulate?: boolean, mulMap?: Record<string, number> }): HttpResponseResolver<PathParams, DefaultBodyType, JsonBodyType> {
+	return ({ request }) => {
+		action(`GET ${request.url}`)();
+		const limitParam = new URL(request.url).searchParams.get('limit');
+		const limit = limitParam ? parseInt(limitParam) : 30;
+		const res = {};
+		for (const field of fields) {
+			const layers = field.split('.');
+			let current = res;
+			while (layers.length > 1) {
+				const currentKey = layers.shift()!;
+				if (current[currentKey] == null) current[currentKey] = {};
+				current = current[currentKey];
+			}
+			current[layers[0]] = getChartArray(field, limit, {
+				accumulate: option?.accumulate,
+				mul: option?.mulMap != null && field in option.mulMap ? option.mulMap[field] : undefined,
+			});
+		}
+		return HttpResponse.json(res);
+	};
+}
diff --git a/packages/frontend/.storybook/fakes.ts b/packages/frontend/.storybook/fakes.ts
index 3a24ccb248..fc3b0334e4 100644
--- a/packages/frontend/.storybook/fakes.ts
+++ b/packages/frontend/.storybook/fakes.ts
@@ -3,6 +3,7 @@
  * SPDX-License-Identifier: AGPL-3.0-only
  */
 
+import { AISCRIPT_VERSION } from '@syuilo/aiscript';
 import type { entities } from 'misskey-js'
 
 export function abuseUserReport() {
@@ -22,6 +23,55 @@ export function abuseUserReport() {
 	};
 }
 
+export function channel(id = 'somechannelid', name = 'Some Channel', bannerUrl: string | null = 'https://github.com/misskey-dev/misskey/blob/master/packages/frontend/assets/fedi.jpg?raw=true'): entities.Channel {
+	return {
+		id,
+		createdAt: '2016-12-28T22:49:51.000Z',
+		lastNotedAt: '2016-12-28T22:49:51.000Z',
+		name,
+		description: null,
+		userId: null,
+		bannerUrl,
+		pinnedNoteIds: [],
+		color: '#000',
+		isArchived: false,
+		usersCount: 1,
+		notesCount: 1,
+		isSensitive: false,
+		allowRenoteToExternal: false,
+	};
+}
+
+export function clip(id = 'someclipid', name = 'Some Clip'): entities.Clip {
+	return {
+		id,
+		createdAt: '2016-12-28T22:49:51.000Z',
+		lastClippedAt: null,
+		userId: 'someuserid',
+		user: userLite(),
+		notesCount: undefined,
+		name,
+		description: 'Some clip description',
+		isPublic: false,
+		favoritedCount: 0,
+	};
+}
+
+export function emojiDetailed(id = 'someemojiid', name = 'some_emoji'): entities.EmojiDetailed {
+	return {
+		id,
+		aliases: ['alias1', 'alias2'],
+		name,
+		category: 'emojiCategory',
+		host: null,
+		url: '/client-assets/about-icon.png',
+		license: null,
+		isSensitive: false,
+		localOnly: false,
+		roleIdsThatCanBeUsedThisEmojiAsReaction: ['roleId1', 'roleId2'],
+	};
+}
+
 export function galleryPost(isSensitive = false) {
 	return {
 		id: 'somepostid',
@@ -65,7 +115,99 @@ export function file(isSensitive = false) {
 	};
 }
 
-export function userDetailed(id = 'someuserid', username = 'miskist', host = 'misskey-hub.net', name = 'Misskey User'): entities.UserDetailed {
+const script = `/// @ ${AISCRIPT_VERSION}
+
+var name = ""
+
+Ui:render([
+	Ui:C:textInput({
+		label: "Your name"
+		onInput: @(v) { name = v }
+	})
+	Ui:C:button({
+		text: "Hello"
+		onClick: @() {
+			Mk:dialog(null, \`Hello, {name}!\`)
+		}
+	})
+])
+`;
+
+export function flash(): entities.Flash {
+	return {
+		id: 'someflashid',
+		createdAt: '2016-12-28T22:49:51.000Z',
+		updatedAt: '2016-12-28T22:49:51.000Z',
+		userId: 'someuserid',
+		user: userLite(),
+		title: 'Some Play title',
+		summary: 'Some Play summary',
+		script,
+		visibility: 'public',
+		likedCount: 0,
+		isLiked: false,
+	};
+}
+
+export function folder(id = 'somefolderid', name = 'Some Folder', parentId: string | null = null): entities.DriveFolder {
+	return {
+		id,
+		createdAt: '2016-12-28T22:49:51.000Z',
+		name,
+		parentId,
+	};
+}
+
+export function federationInstance(): entities.FederationInstance {
+	return {
+		id: 'someinstanceid',
+		firstRetrievedAt: '2021-01-01T00:00:00.000Z',
+		host: 'misskey-hub.net',
+		usersCount: 10,
+		notesCount: 20,
+		followingCount: 5,
+		followersCount: 15,
+		isNotResponding: false,
+		isSuspended: false,
+		suspensionState: 'none',
+		isBlocked: false,
+		softwareName: 'misskey',
+		softwareVersion: '2024.5.0',
+		openRegistrations: false,
+		name: 'Misskey Hub',
+		description: '',
+		maintainerName: '',
+		maintainerEmail: '',
+		isSilenced: false,
+		iconUrl: 'https://github.com/misskey-dev/misskey/blob/master/packages/frontend/assets/about-icon.png?raw=true',
+		faviconUrl: '',
+		themeColor: '',
+		infoUpdatedAt: '',
+		latestRequestReceivedAt: '',
+	};
+}
+
+export function note(id = 'somenoteid'): entities.Note {
+	return {
+		id,
+		createdAt: '2016-12-28T22:49:51.000Z',
+		deletedAt: null,
+		text: 'some note',
+		cw: null,
+		userId: 'someuserid',
+		user: userLite(),
+		visibility: 'public',
+		reactionAcceptance: 'nonSensitiveOnly',
+		reactionEmojis: {},
+		reactions: {},
+		myReaction: null,
+		reactionCount: 0,
+		renoteCount: 0,
+		repliesCount: 0,
+	};
+}
+
+export function userLite(id = 'someuserid', username = 'miskist', host: entities.UserDetailed['host'] = 'misskey-hub.net', name: entities.UserDetailed['name'] = 'Misskey User'): entities.UserLite {
 	return {
 		id,
 		username,
@@ -76,6 +218,12 @@ export function userDetailed(id = 'someuserid', username = 'miskist', host = 'mi
 		avatarBlurhash: 'eQFRshof5NWBRi},juayfPju53WB?0ofs;s*a{ofjuay^SoMEJR%ay',
 		avatarDecorations: [],
 		emojis: {},
+	};
+}
+
+export function userDetailed(id = 'someuserid', username = 'miskist', host: entities.UserDetailed['host'] = 'misskey-hub.net', name: entities.UserDetailed['name'] = 'Misskey User'): entities.UserDetailed {
+	return {
+		...userLite(id, username, host, name),
 		bannerBlurhash: 'eQA^IW^-MH8w9tE8I=S^o{$*R4RikXtSxutRozjEnNR.RQadoyozog',
 		bannerUrl: 'https://github.com/misskey-dev/misskey/blob/master/packages/frontend/assets/fedi.jpg?raw=true',
 		birthday: '2014-06-20',
@@ -126,7 +274,7 @@ export function userDetailed(id = 'someuserid', username = 'miskist', host = 'mi
 		movedTo: null,
 		alsoKnownAs: null,
 		notify: 'none',
-		memo: null
+		memo: null,
 	};
 }
 
diff --git a/packages/frontend/.storybook/generate.tsx b/packages/frontend/.storybook/generate.tsx
index d74c83a500..f2bdc631d2 100644
--- a/packages/frontend/.storybook/generate.tsx
+++ b/packages/frontend/.storybook/generate.tsx
@@ -397,14 +397,28 @@ function toStories(component: string): Promise<string> {
 	const globs = await Promise.all([
 		glob('src/components/global/Mk*.vue'),
 		glob('src/components/global/RouterView.vue'),
-		glob('src/components/Mk{A,B}*.vue'),
-		glob('src/components/MkDigitalClock.vue'),
+		glob('src/components/MkAbuseReportWindow.vue'),
+		glob('src/components/MkAccountMoved.vue'),
+		glob('src/components/MkAchievements.vue'),
+		glob('src/components/MkAnalogClock.vue'),
+		glob('src/components/MkAnimBg.vue'),
+		glob('src/components/MkAnnouncementDialog.vue'),
+		glob('src/components/MkAntennaEditor.vue'),
+		glob('src/components/MkAntennaEditorDialog.vue'),
+		glob('src/components/MkAsUi.vue'),
+		glob('src/components/MkAutocomplete.vue'),
+		glob('src/components/MkAvatars.vue'),
+		glob('src/components/Mk[B-E]*.vue'),
+		glob('src/components/MkFlashPreview.vue'),
 		glob('src/components/MkGalleryPostPreview.vue'),
 		glob('src/components/MkSignupServerRules.vue'),
 		glob('src/components/MkUserSetupDialog.vue'),
 		glob('src/components/MkUserSetupDialog.*.vue'),
+		glob('src/components/MkInstanceCardMini.vue'),
 		glob('src/components/MkInviteCode.vue'),
+		glob('src/pages/admin/overview.ap-requests.vue'),
 		glob('src/pages/user/home.vue'),
+		glob('src/pages/search.vue'),
 	]);
 	const components = globs.flat();
 	await Promise.all(components.map(async (component) => {
diff --git a/packages/frontend/.storybook/main.ts b/packages/frontend/.storybook/main.ts
index d3822942cd..9f318cf449 100644
--- a/packages/frontend/.storybook/main.ts
+++ b/packages/frontend/.storybook/main.ts
@@ -15,6 +15,7 @@ const _dirname = fileURLToPath(new URL('.', import.meta.url));
 
 const config = {
 	stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'],
+	staticDirs: [{ from: '../assets', to: '/client-assets' }],
 	addons: [
 		getAbsolutePath('@storybook/addon-essentials'),
 		getAbsolutePath('@storybook/addon-interactions'),
diff --git a/packages/frontend/.storybook/preload-theme.ts b/packages/frontend/.storybook/preload-theme.ts
index fb93d7be13..e5573f2ac3 100644
--- a/packages/frontend/.storybook/preload-theme.ts
+++ b/packages/frontend/.storybook/preload-theme.ts
@@ -30,7 +30,7 @@ const keys = [
 	'd-u0',
 ]
 
-await Promise.all(keys.map((key) => readFile(new URL(`../src/themes/${key}.json5`, import.meta.url), 'utf8'))).then((sources) => {
+await Promise.all(keys.map((key) => readFile(new URL(`../../frontend-shared/themes/${key}.json5`, import.meta.url), 'utf8'))).then((sources) => {
 	writeFile(
 		new URL('./themes.ts', import.meta.url),
 		`export default ${JSON.stringify(
diff --git a/packages/frontend/.storybook/preview.ts b/packages/frontend/.storybook/preview.ts
index 982a2979ac..d000a28232 100644
--- a/packages/frontend/.storybook/preview.ts
+++ b/packages/frontend/.storybook/preview.ts
@@ -3,11 +3,11 @@
  * SPDX-License-Identifier: AGPL-3.0-only
  */
 
-import { FORCE_REMOUNT } from '@storybook/core-events';
+import { FORCE_RE_RENDER, FORCE_REMOUNT } from '@storybook/core-events';
 import { addons } from '@storybook/preview-api';
 import { type Preview, setup } from '@storybook/vue3';
 import isChromatic from 'chromatic/isChromatic';
-import { initialize, mswDecorator } from 'msw-storybook-addon';
+import { initialize, mswLoader } from 'msw-storybook-addon';
 import { userDetailed } from './fakes.js';
 import locale from './locale.js';
 import { commonHandlers, onUnhandledRequest } from './mocks.js';
@@ -16,7 +16,7 @@ import '../src/style.scss';
 
 const appInitialized = Symbol();
 
-let lastStory = null;
+let lastStory: string | null = null;
 let moduleInitialized = false;
 let unobserve = () => {};
 let misskeyOS = null;
@@ -110,7 +110,7 @@ const preview = {
 				}).catch(() => {});
 				Promise.all([resetIndexedDBPromise, resetDefaultStorePromise]).then(() => {
 					initLocalStorage();
-					channel.emit(FORCE_REMOUNT, { storyId: context.id });
+					channel.emit(FORCE_RE_RENDER, { storyId: context.id });
 				});
 			}
 			const story = Story();
@@ -122,7 +122,6 @@ const preview = {
 			}
 			return story;
 		},
-		mswDecorator,
 		(Story, context) => {
 			return {
 				setup() {
@@ -137,6 +136,7 @@ const preview = {
 			};
 		},
 	],
+	loaders: [mswLoader],
 	parameters: {
 		controls: {
 			exclude: /^__/,
diff --git a/packages/frontend/@types/theme.d.ts b/packages/frontend/@types/theme.d.ts
index 0a7281898d..70afc356c1 100644
--- a/packages/frontend/@types/theme.d.ts
+++ b/packages/frontend/@types/theme.d.ts
@@ -3,7 +3,7 @@
  * SPDX-License-Identifier: AGPL-3.0-only
  */
 
-declare module '@/themes/*.json5' {
+declare module '@@/themes/*.json5' {
 	import { Theme } from '@/scripts/theme.js';
 
 	const theme: Theme;
diff --git a/packages/frontend/assets/testcaptcha.png b/packages/frontend/assets/testcaptcha.png
new file mode 100644
index 0000000000..9bfd252b51
Binary files /dev/null and b/packages/frontend/assets/testcaptcha.png differ
diff --git a/packages/frontend/eslint.config.js b/packages/frontend/eslint.config.js
new file mode 100644
index 0000000000..dd8f03dac5
--- /dev/null
+++ b/packages/frontend/eslint.config.js
@@ -0,0 +1,95 @@
+import globals from 'globals';
+import tsParser from '@typescript-eslint/parser';
+import parser from 'vue-eslint-parser';
+import pluginVue from 'eslint-plugin-vue';
+import pluginMisskey from '@misskey-dev/eslint-plugin';
+import sharedConfig from '../shared/eslint.config.js';
+
+export default [
+	...sharedConfig,
+	{
+		files: ['src/**/*.vue'],
+		...pluginMisskey.configs.typescript,
+	},
+	...pluginVue.configs['flat/recommended'],
+	{
+		files: ['src/**/*.{ts,vue}'],
+		languageOptions: {
+			globals: {
+				...Object.fromEntries(Object.entries(globals.node).map(([key]) => [key, 'off'])),
+				...globals.browser,
+
+				// Node.js
+				module: false,
+				require: false,
+				__dirname: false,
+
+				// Misskey
+				_DEV_: false,
+				_LANGS_: false,
+				_VERSION_: false,
+				_ENV_: false,
+				_PERF_PREFIX_: false,
+				_DATA_TRANSFER_DRIVE_FILE_: false,
+				_DATA_TRANSFER_DRIVE_FOLDER_: false,
+				_DATA_TRANSFER_DECK_COLUMN_: false,
+			},
+			parser,
+			parserOptions: {
+				extraFileExtensions: ['.vue'],
+				parser: tsParser,
+				project: ['./tsconfig.json'],
+				sourceType: 'module',
+				tsconfigRootDir: import.meta.dirname,
+			},
+		},
+		rules: {
+			'@typescript-eslint/no-empty-interface': ['error', {
+				allowSingleExtends: true,
+			}],
+			// window の禁止理由: グローバルスコープと衝突し、予期せぬ結果を招くため
+			// e の禁止理由: error や event など、複数のキーワードの頭文字であり分かりにくいため
+			'id-denylist': ['error', 'window', 'e'],
+			'no-shadow': ['warn'],
+			'vue/attributes-order': ['error', {
+				alphabetical: false,
+			}],
+			'vue/no-use-v-if-with-v-for': ['error', {
+				allowUsingIterationVar: false,
+			}],
+			'vue/no-ref-as-operand': 'error',
+			'vue/no-multi-spaces': ['error', {
+				ignoreProperties: false,
+			}],
+			'vue/no-v-html': 'warn',
+			'vue/order-in-components': 'error',
+			'vue/html-indent': ['warn', 'tab', {
+				attribute: 1,
+				baseIndent: 0,
+				closeBracket: 0,
+				alignAttributesVertically: true,
+				ignores: [],
+			}],
+			'vue/html-closing-bracket-spacing': ['warn', {
+				startTag: 'never',
+				endTag: 'never',
+				selfClosingTag: 'never',
+			}],
+			'vue/multi-word-component-names': 'warn',
+			'vue/require-v-for-key': 'warn',
+			'vue/no-unused-components': 'warn',
+			'vue/no-unused-vars': 'warn',
+			'vue/no-dupe-keys': 'warn',
+			'vue/valid-v-for': 'warn',
+			'vue/return-in-computed-property': 'warn',
+			'vue/no-setup-props-reactivity-loss': 'warn',
+			'vue/max-attributes-per-line': 'off',
+			'vue/html-self-closing': 'off',
+			'vue/singleline-html-element-content-newline': 'off',
+			'vue/v-on-event-hyphenation': ['error', 'never', {
+				autofix: true,
+			}],
+			'vue/attribute-hyphenation': ['error', 'never'],
+		},
+	},
+];
diff --git a/packages/frontend/package.json b/packages/frontend/package.json
index 56b824c0c5..3226a554a9 100644
--- a/packages/frontend/package.json
+++ b/packages/frontend/package.json
@@ -17,35 +17,35 @@
 		"lint": "pnpm typecheck && pnpm eslint"
 	},
 	"dependencies": {
-		"@discordapp/twemoji": "15.0.3",
+		"@discordapp/twemoji": "15.1.0",
 		"@github/webauthn-json": "2.1.1",
 		"@mcaptcha/vanilla-glue": "0.1.0-alpha-3",
 		"@misskey-dev/browser-image-resizer": "2024.1.0",
 		"@rollup/plugin-json": "6.1.0",
-		"@rollup/plugin-replace": "5.0.5",
-		"@rollup/pluginutils": "5.1.0",
-		"@syuilo/aiscript": "0.18.0",
+		"@rollup/plugin-replace": "5.0.7",
+		"@rollup/pluginutils": "5.1.2",
+		"@syuilo/aiscript": "0.19.0",
 		"@tabler/icons-webfont": "3.3.0",
 		"@twemoji/parser": "15.1.1",
-		"@vitejs/plugin-vue": "5.0.4",
-		"@vue/compiler-sfc": "3.4.26",
-		"aiscript-vscode": "github:aiscript-dev/aiscript-vscode#v0.1.9",
-		"astring": "1.8.6",
+		"@vitejs/plugin-vue": "5.1.4",
+		"@vue/compiler-sfc": "3.5.11",
+		"aiscript-vscode": "github:aiscript-dev/aiscript-vscode#v0.1.11",
+		"astring": "1.9.0",
 		"broadcast-channel": "7.0.0",
 		"buraha": "0.0.1",
 		"canvas-confetti": "1.9.3",
-		"chart.js": "4.4.2",
+		"chart.js": "4.4.4",
 		"chartjs-adapter-date-fns": "3.0.0",
 		"chartjs-chart-matrix": "2.0.1",
 		"chartjs-plugin-gradient": "0.6.1",
 		"chartjs-plugin-zoom": "2.0.1",
-		"chromatic": "11.3.0",
-		"compare-versions": "6.1.0",
-		"cropperjs": "2.0.0-beta.5",
+		"chromatic": "11.11.0",
+		"compare-versions": "6.1.1",
+		"cropperjs": "2.0.0-rc.2",
 		"date-fns": "2.30.0",
-		"escape-regexp": "0.0.1",
 		"estree-walker": "3.0.3",
 		"eventemitter3": "5.0.1",
+		"frontend-shared": "workspace:*",
 		"idb-keyval": "6.2.1",
 		"insert-text-at-cursor": "0.3.0",
 		"is-file-animated": "1.0.2",
@@ -55,87 +55,87 @@
 		"misskey-bubble-game": "workspace:*",
 		"misskey-js": "workspace:*",
 		"misskey-reversi": "workspace:*",
-		"photoswipe": "5.4.3",
+		"photoswipe": "5.4.4",
 		"punycode": "2.3.1",
-		"rollup": "4.17.2",
-		"sanitize-html": "2.13.0",
-		"sass": "1.76.0",
-		"shiki": "1.4.0",
+		"rollup": "4.22.5",
+		"sanitize-html": "2.13.1",
+		"sass": "1.79.3",
+		"shiki": "1.21.0",
 		"strict-event-emitter-types": "2.0.0",
 		"textarea-caret": "3.1.0",
-		"three": "0.164.1",
-		"throttle-debounce": "5.0.0",
+		"three": "0.169.0",
+		"throttle-debounce": "5.0.2",
 		"tinycolor2": "1.6.0",
-		"tsc-alias": "1.8.8",
+		"tsc-alias": "1.8.10",
 		"tsconfig-paths": "4.2.0",
-		"typescript": "5.4.5",
-		"uuid": "9.0.1",
-		"v-code-diff": "1.11.0",
-		"vite": "5.2.11",
-		"vue": "3.4.26",
+		"typescript": "5.6.2",
+		"uuid": "10.0.0",
+		"v-code-diff": "1.13.1",
+		"vite": "5.4.8",
+		"vue": "3.5.11",
 		"vuedraggable": "next"
 	},
 	"devDependencies": {
-		"@misskey-dev/eslint-plugin": "1.0.0",
 		"@misskey-dev/summaly": "5.1.0",
-		"@storybook/addon-actions": "8.0.9",
-		"@storybook/addon-essentials": "8.0.9",
-		"@storybook/addon-interactions": "8.0.9",
-		"@storybook/addon-links": "8.0.9",
-		"@storybook/addon-mdx-gfm": "8.0.9",
-		"@storybook/addon-storysource": "8.0.9",
-		"@storybook/blocks": "8.0.9",
-		"@storybook/components": "8.0.9",
-		"@storybook/core-events": "8.0.9",
-		"@storybook/manager-api": "8.0.9",
-		"@storybook/preview-api": "8.0.9",
-		"@storybook/react": "8.0.9",
-		"@storybook/react-vite": "8.0.9",
-		"@storybook/test": "8.0.9",
-		"@storybook/theming": "8.0.9",
-		"@storybook/types": "8.0.9",
-		"@storybook/vue3": "8.0.9",
-		"@storybook/vue3-vite": "8.0.9",
-		"@testing-library/vue": "8.0.3",
-		"@types/escape-regexp": "0.0.3",
-		"@types/estree": "1.0.5",
-		"@types/matter-js": "0.19.6",
-		"@types/micromatch": "4.0.7",
-		"@types/node": "20.12.7",
+		"@storybook/addon-actions": "8.3.4",
+		"@storybook/addon-essentials": "8.3.4",
+		"@storybook/addon-interactions": "8.3.4",
+		"@storybook/addon-links": "8.3.4",
+		"@storybook/addon-mdx-gfm": "8.3.4",
+		"@storybook/addon-storysource": "8.3.4",
+		"@storybook/blocks": "8.3.4",
+		"@storybook/components": "8.3.4",
+		"@storybook/core-events": "8.3.4",
+		"@storybook/manager-api": "8.3.4",
+		"@storybook/preview-api": "8.3.4",
+		"@storybook/react": "8.3.4",
+		"@storybook/react-vite": "8.3.4",
+		"@storybook/test": "8.3.4",
+		"@storybook/theming": "8.3.4",
+		"@storybook/types": "8.3.4",
+		"@storybook/vue3": "8.3.4",
+		"@storybook/vue3-vite": "8.3.4",
+		"@testing-library/vue": "8.1.0",
+		"@types/canvas-confetti": "^1.6.4",
+		"@types/estree": "1.0.6",
+		"@types/matter-js": "0.19.7",
+		"@types/micromatch": "4.0.9",
+		"@types/node": "20.14.12",
 		"@types/punycode": "2.1.4",
-		"@types/sanitize-html": "2.11.0",
+		"@types/sanitize-html": "2.13.0",
+		"@types/seedrandom": "3.0.8",
 		"@types/throttle-debounce": "5.0.2",
 		"@types/tinycolor2": "1.4.6",
-		"@types/uuid": "9.0.8",
-		"@types/ws": "8.5.10",
-		"@typescript-eslint/eslint-plugin": "7.7.1",
-		"@typescript-eslint/parser": "7.7.1",
-		"@vitest/coverage-v8": "0.34.6",
-		"@vue/runtime-core": "3.4.26",
-		"acorn": "8.11.3",
+		"@types/uuid": "10.0.0",
+		"@types/ws": "8.5.12",
+		"@typescript-eslint/eslint-plugin": "7.17.0",
+		"@typescript-eslint/parser": "7.17.0",
+		"@vitest/coverage-v8": "1.6.0",
+		"@vue/runtime-core": "3.5.11",
+		"acorn": "8.12.1",
 		"cross-env": "7.0.3",
-		"cypress": "13.8.1",
-		"eslint": "8.57.0",
-		"eslint-plugin-import": "2.29.1",
-		"eslint-plugin-vue": "9.25.0",
+		"cypress": "13.15.0",
+		"eslint-plugin-import": "2.31.0",
+		"eslint-plugin-vue": "9.28.0",
 		"fast-glob": "3.3.2",
 		"happy-dom": "10.0.3",
 		"intersection-observer": "0.12.2",
-		"micromatch": "4.0.5",
-		"msw": "2.2.14",
-		"msw-storybook-addon": "2.0.1",
-		"nodemon": "3.1.0",
-		"prettier": "3.2.5",
+		"micromatch": "4.0.8",
+		"msw": "2.4.9",
+		"msw-storybook-addon": "2.0.3",
+		"nodemon": "3.1.7",
+		"prettier": "3.3.3",
 		"react": "18.3.1",
 		"react-dom": "18.3.1",
-		"start-server-and-test": "2.0.3",
-		"storybook": "8.0.9",
+		"seedrandom": "3.0.5",
+		"start-server-and-test": "2.0.8",
+		"storybook": "8.3.4",
 		"storybook-addon-misskey-theme": "github:misskey-dev/storybook-addon-misskey-theme",
 		"vite-plugin-turbosnap": "1.0.3",
-		"vitest": "0.34.6",
+		"vitest": "1.6.0",
 		"vitest-fetch-mock": "0.2.2",
-		"vue-component-type-helpers": "2.0.16",
-		"vue-eslint-parser": "9.4.2",
-		"vue-tsc": "2.0.16"
+		"vue-component-type-helpers": "2.1.6",
+		"vue-eslint-parser": "9.4.3",
+		"vue-tsc": "2.1.6"
 	}
 }
diff --git a/packages/frontend/src/_boot_.ts b/packages/frontend/src/_boot_.ts
index 875353f8a4..c90cc6bdd0 100644
--- a/packages/frontend/src/_boot_.ts
+++ b/packages/frontend/src/_boot_.ts
@@ -6,11 +6,13 @@
 // https://vitejs.dev/config/build-options.html#build-modulepreload
 import 'vite/modulepreload-polyfill';
 
+import '@tabler/icons-webfont/dist/tabler-icons.scss';
+
 import '@/style.scss';
 import { mainBoot } from '@/boot/main-boot.js';
 import { subBoot } from '@/boot/sub-boot.js';
 
-const subBootPaths = ['/share', '/auth', '/miauth', '/signup-complete'];
+const subBootPaths = ['/share', '/auth', '/miauth', '/oauth', '/signup-complete'];
 
 if (subBootPaths.some(i => location.pathname === i || location.pathname.startsWith(i + '/'))) {
 	subBoot();
diff --git a/packages/frontend/src/_dev_boot_.ts b/packages/frontend/src/_dev_boot_.ts
index 7c6e537fbc..f312765dcf 100644
--- a/packages/frontend/src/_dev_boot_.ts
+++ b/packages/frontend/src/_dev_boot_.ts
@@ -3,11 +3,6 @@
  * SPDX-License-Identifier: AGPL-3.0-only
  */
 
-// devモードで起動される際(index.htmlを使うとき)はrouterが暴発してしまってうまく読み込めない。
-// よって、devモードとして起動されるときはビルド時に組み込む形としておく。
-// (pnpm start時はpugファイルの中で静的リソースとして読み込むようになっており、この問題は起こっていない)
-import '@tabler/icons-webfont/dist/tabler-icons.scss';
-
 await main();
 
 import('@/_boot_.js');
@@ -48,7 +43,7 @@ async function main() {
 	const theme = localStorage.getItem('theme');
 	if (theme) {
 		for (const [k, v] of Object.entries(JSON.parse(theme))) {
-			document.documentElement.style.setProperty(`--${k}`, v.toString());
+			document.documentElement.style.setProperty(`--MI_THEME-${k}`, v.toString());
 
 			// HTMLの theme-color 適用
 			if (k === 'htmlThemeColor') {
diff --git a/packages/frontend/src/account.ts b/packages/frontend/src/account.ts
index 7f20e0b1a2..36186ecac1 100644
--- a/packages/frontend/src/account.ts
+++ b/packages/frontend/src/account.ts
@@ -5,12 +5,12 @@
 
 import { defineAsyncComponent, reactive, ref } from 'vue';
 import * as Misskey from 'misskey-js';
+import { apiUrl } from '@@/js/config.js';
+import type { MenuItem, MenuButton } from '@/types/menu.js';
 import { showSuspendedDialog } from '@/scripts/show-suspended-dialog.js';
 import { i18n } from '@/i18n.js';
 import { miLocalStorage } from '@/local-storage.js';
-import { MenuButton } from '@/types/menu.js';
 import { del, get, set } from '@/scripts/idb-proxy.js';
-import { apiUrl } from '@/config.js';
 import { waiting, popup, popupMenu, success, alert } from '@/os.js';
 import { misskeyApi } from '@/scripts/misskey-api.js';
 import { unisonReload, reloadChannel } from '@/scripts/unison-reload.js';
@@ -120,7 +120,7 @@ function fetchAccount(token: string, id?: string, forceShowDialog?: boolean): Pr
 				res.json().then(done2, fail2);
 			}))
 			.then(async res => {
-				if (res.error) {
+				if ('error' in res) {
 					if (res.error.id === 'a8c724b3-6e9c-4b46-b1a8-bc3ed6258370') {
 						// SUSPENDED
 						if (forceShowDialog || $i && (token === $i.token || id === $i.id)) {
@@ -165,7 +165,18 @@ function fetchAccount(token: string, id?: string, forceShowDialog?: boolean): Pr
 	});
 }
 
-export function updateAccount(accountData: Partial<Account>) {
+export function updateAccount(accountData: Account) {
+	if (!$i) return;
+	for (const key of Object.keys($i)) {
+		delete $i[key];
+	}
+	for (const [key, value] of Object.entries(accountData)) {
+		$i[key] = value;
+	}
+	miLocalStorage.setItem('account', JSON.stringify($i));
+}
+
+export function updateAccountPartial(accountData: Partial<Account>) {
 	if (!$i) return;
 	for (const [key, value] of Object.entries(accountData)) {
 		$i[key] = value;
@@ -184,10 +195,12 @@ export async function refreshAccount() {
 
 export async function login(token: Account['token'], redirect?: string) {
 	const showing = ref(true);
-	popup(defineAsyncComponent(() => import('@/components/MkWaitingDialog.vue')), {
+	const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkWaitingDialog.vue')), {
 		success: false,
 		showing: showing,
-	}, {}, 'closed');
+	}, {
+		closed: () => dispose(),
+	});
 	if (_DEV_) console.log('logging as token ', token);
 	const me = await fetchAccount(token, undefined, true)
 		.catch(reason => {
@@ -222,24 +235,6 @@ export async function openAccountMenu(opts: {
 }, ev: MouseEvent) {
 	if (!$i) return;
 
-	function showSigninDialog() {
-		popup(defineAsyncComponent(() => import('@/components/MkSigninDialog.vue')), {}, {
-			done: res => {
-				addAccount(res.id, res.i);
-				success();
-			},
-		}, 'closed');
-	}
-
-	function createAccount() {
-		popup(defineAsyncComponent(() => import('@/components/MkSignupDialog.vue')), {}, {
-			done: res => {
-				addAccount(res.id, res.i);
-				switchAccountWithToken(res.i);
-			},
-		}, 'closed');
-	}
-
 	async function switchAccount(account: Misskey.entities.UserDetailed) {
 		const storedAccounts = await getAccounts();
 		const found = storedAccounts.find(x => x.id === account.id);
@@ -284,36 +279,98 @@ export async function openAccountMenu(opts: {
 		});
 	}));
 
+	const menuItems: MenuItem[] = [];
+
 	if (opts.withExtraOperation) {
-		popupMenu([...[{
-			type: 'link' as const,
+		menuItems.push({
+			type: 'link',
 			text: i18n.ts.profile,
-			to: `/@${ $i.username }`,
+			to: `/@${$i.username}`,
 			avatar: $i,
-		}, { type: 'divider' as const }, ...(opts.includeCurrentAccount ? [createItem($i)] : []), ...accountItemPromises, {
-			type: 'parent' as const,
+		}, {
+			type: 'divider',
+		});
+
+		if (opts.includeCurrentAccount) {
+			menuItems.push(createItem($i));
+		}
+
+		menuItems.push(...accountItemPromises);
+
+		menuItems.push({
+			type: 'parent',
 			icon: 'ti ti-plus',
 			text: i18n.ts.addAccount,
 			children: [{
 				text: i18n.ts.existingAccount,
-				action: () => { showSigninDialog(); },
+				action: () => {
+					getAccountWithSigninDialog().then(res => {
+						if (res != null) {
+							success();
+						}
+					});
+				},
 			}, {
 				text: i18n.ts.createAccount,
-				action: () => { createAccount(); },
+				action: () => {
+					getAccountWithSignupDialog().then(res => {
+						if (res != null) {
+							switchAccountWithToken(res.token);
+						}
+					});
+				},
 			}],
 		}, {
-			type: 'link' as const,
+			type: 'link',
 			icon: 'ti ti-users',
 			text: i18n.ts.manageAccounts,
 			to: '/settings/accounts',
-		}]], ev.currentTarget ?? ev.target, {
-			align: 'left',
 		});
 	} else {
-		popupMenu([...(opts.includeCurrentAccount ? [createItem($i)] : []), ...accountItemPromises], ev.currentTarget ?? ev.target, {
-			align: 'left',
-		});
+		if (opts.includeCurrentAccount) {
+			menuItems.push(createItem($i));
+		}
+
+		menuItems.push(...accountItemPromises);
 	}
+
+	popupMenu(menuItems, ev.currentTarget ?? ev.target, {
+		align: 'left',
+	});
+}
+
+export function getAccountWithSigninDialog(): Promise<{ id: string, token: string } | null> {
+	return new Promise((resolve) => {
+		const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkSigninDialog.vue')), {}, {
+			done: async (res: Misskey.entities.SigninFlowResponse & { finished: true }) => {
+				await addAccount(res.id, res.i);
+				resolve({ id: res.id, token: res.i });
+			},
+			cancelled: () => {
+				resolve(null);
+			},
+			closed: () => {
+				dispose();
+			},
+		});
+	});
+}
+
+export function getAccountWithSignupDialog(): Promise<{ id: string, token: string } | null> {
+	return new Promise((resolve) => {
+		const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkSignupDialog.vue')), {}, {
+			done: async (res: Misskey.entities.SignupResponse) => {
+				await addAccount(res.id, res.token);
+				resolve({ id: res.id, token: res.token });
+			},
+			cancelled: () => {
+				resolve(null);
+			},
+			closed: () => {
+				dispose();
+			},
+		});
+	});
 }
 
 if (_DEV_) {
diff --git a/packages/frontend/src/boot/common.ts b/packages/frontend/src/boot/common.ts
index d86ae18ffe..90ae49ee59 100644
--- a/packages/frontend/src/boot/common.ts
+++ b/packages/frontend/src/boot/common.ts
@@ -5,13 +5,13 @@
 
 import { computed, watch, version as vueVersion, App } from 'vue';
 import { compareVersions } from 'compare-versions';
+import { version, lang, updateLocale, locale } from '@@/js/config.js';
 import widgets from '@/widgets/index.js';
 import directives from '@/directives/index.js';
 import components from '@/components/index.js';
-import { version, lang, updateLocale, locale } from '@/config.js';
 import { applyTheme } from '@/scripts/theme.js';
 import { isDeviceDarkmode } from '@/scripts/is-device-darkmode.js';
-import { updateI18n } from '@/i18n.js';
+import { updateI18n, i18n } from '@/i18n.js';
 import { $i, refreshAccount, login } from '@/account.js';
 import { defaultStore, ColdDeviceStorage } from '@/store.js';
 import { fetchInstance, instance } from '@/instance.js';
@@ -22,7 +22,8 @@ import { getAccountFromId } from '@/scripts/get-account-from-id.js';
 import { deckStore } from '@/ui/deck/deck-store.js';
 import { miLocalStorage } from '@/local-storage.js';
 import { fetchCustomEmojis } from '@/custom-emojis.js';
-import { setupRouter } from '@/router/definition.js';
+import { setupRouter } from '@/router/main.js';
+import { createMainRouter } from '@/router/definition.js';
 
 export async function common(createVue: () => App<Element>) {
 	console.info(`Misskey v${version}`);
@@ -145,10 +146,9 @@ export async function common(createVue: () => App<Element>) {
 	// NOTE: この処理は必ずクライアント更新チェック処理より後に来ること(テーマ再構築のため)
 	watch(defaultStore.reactiveState.darkMode, (darkMode) => {
 		applyTheme(darkMode ? ColdDeviceStorage.get('darkTheme') : ColdDeviceStorage.get('lightTheme'));
-		document.documentElement.dataset.colorMode = darkMode ? 'dark' : 'light';
 	}, { immediate: miLocalStorage.getItem('theme') == null });
 
-	document.documentElement.dataset.colorMode = defaultStore.state.darkMode ? 'dark' : 'light';
+	document.documentElement.dataset.colorScheme = defaultStore.state.darkMode ? 'dark' : 'light';
 
 	const darkTheme = computed(ColdDeviceStorage.makeGetterSetter('darkTheme'));
 	const lightTheme = computed(ColdDeviceStorage.makeGetterSetter('lightTheme'));
@@ -182,24 +182,18 @@ export async function common(createVue: () => App<Element>) {
 			if (instance.defaultLightTheme != null) ColdDeviceStorage.set('lightTheme', JSON.parse(instance.defaultLightTheme));
 			if (instance.defaultDarkTheme != null) ColdDeviceStorage.set('darkTheme', JSON.parse(instance.defaultDarkTheme));
 			defaultStore.set('themeInitial', false);
-		} else {
-			if (defaultStore.state.darkMode) {
-				applyTheme(darkTheme.value);
-			} else {
-				applyTheme(lightTheme.value);
-			}
 		}
 	});
 
 	watch(defaultStore.reactiveState.useBlurEffectForModal, v => {
-		document.documentElement.style.setProperty('--modalBgFilter', v ? 'blur(4px)' : 'none');
+		document.documentElement.style.setProperty('--MI-modalBgFilter', v ? 'blur(4px)' : 'none');
 	}, { immediate: true });
 
 	watch(defaultStore.reactiveState.useBlurEffect, v => {
 		if (v) {
-			document.documentElement.style.removeProperty('--blur');
+			document.documentElement.style.removeProperty('--MI-blur');
 		} else {
-			document.documentElement.style.setProperty('--blur', 'none');
+			document.documentElement.style.setProperty('--MI-blur', 'none');
 		}
 	}, { immediate: true });
 
@@ -239,7 +233,7 @@ export async function common(createVue: () => App<Element>) {
 
 	const app = createVue();
 
-	setupRouter(app);
+	setupRouter(app, createMainRouter);
 
 	if (_DEV_) {
 		app.config.performance = true;
@@ -275,6 +269,27 @@ export async function common(createVue: () => App<Element>) {
 
 	removeSplash();
 
+	//#region Self-XSS 対策メッセージ
+	console.log(
+		`%c${i18n.ts._selfXssPrevention.warning}`,
+		'color: #f00; background-color: #ff0; font-size: 36px; padding: 4px;',
+	);
+	console.log(
+		`%c${i18n.ts._selfXssPrevention.title}`,
+		'color: #f00; font-weight: 900; font-family: "Hiragino Sans W9", "Hiragino Kaku Gothic ProN", sans-serif; font-size: 24px;',
+	);
+	console.log(
+		`%c${i18n.ts._selfXssPrevention.description1}`,
+		'font-size: 16px; font-weight: 700;',
+	);
+	console.log(
+		`%c${i18n.ts._selfXssPrevention.description2}`,
+		'font-size: 16px;',
+		'font-size: 20px; font-weight: 700; color: #f00;',
+	);
+	console.log(i18n.tsx._selfXssPrevention.description3({ link: 'https://misskey-hub.net/docs/for-users/resources/self-xss/' }));
+	//#endregion
+
 	return {
 		isClientUpdated,
 		app,
diff --git a/packages/frontend/src/boot/main-boot.ts b/packages/frontend/src/boot/main-boot.ts
index 5cb19f388a..2bf9029479 100644
--- a/packages/frontend/src/boot/main-boot.ts
+++ b/packages/frontend/src/boot/main-boot.ts
@@ -4,16 +4,16 @@
  */
 
 import { createApp, defineAsyncComponent, markRaw } from 'vue';
+import { ui } from '@@/js/config.js';
 import { common } from './common.js';
-import { ui } from '@/config.js';
+import type * as Misskey from 'misskey-js';
 import { i18n } from '@/i18n.js';
 import { alert, confirm, popup, post, toast } from '@/os.js';
 import { useStream } from '@/stream.js';
 import * as sound from '@/scripts/sound.js';
-import { $i, signout, updateAccount } from '@/account.js';
+import { $i, signout, updateAccountPartial } from '@/account.js';
 import { instance } from '@/instance.js';
 import { ColdDeviceStorage, defaultStore } from '@/store.js';
-import { makeHotkey } from '@/scripts/hotkey.js';
 import { reactionPicker } from '@/scripts/reaction-picker.js';
 import { miLocalStorage } from '@/local-storage.js';
 import { claimAchievement, claimedAchievements } from '@/scripts/achievements.js';
@@ -21,6 +21,8 @@ import { initializeSw } from '@/scripts/initialize-sw.js';
 import { deckStore } from '@/ui/deck/deck-store.js';
 import { emojiPicker } from '@/scripts/emoji-picker.js';
 import { mainRouter } from '@/router/main.js';
+import { type Keymap, makeHotkey } from '@/scripts/hotkey.js';
+import { addCustomEmoji, removeCustomEmojis, updateCustomEmojis } from '@/custom-emojis.js';
 
 export async function mainBoot() {
 	const { isClientUpdated } = await common(() => createApp(
@@ -35,7 +37,9 @@ export async function mainBoot() {
 	emojiPicker.init();
 
 	if (isClientUpdated && $i) {
-		popup(defineAsyncComponent(() => import('@/components/MkUpdated.vue')), {}, {}, 'closed');
+		const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkUpdated.vue')), {}, {
+			closed: () => dispose(),
+		});
 	}
 
 	const stream = useStream();
@@ -59,6 +63,18 @@ export async function mainBoot() {
 		}
 	});
 
+	stream.on('emojiAdded', emojiData => {
+		addCustomEmoji(emojiData.emoji);
+	});
+
+	stream.on('emojiUpdated', emojiData => {
+		updateCustomEmojis(emojiData.emojis);
+	});
+
+	stream.on('emojiDeleted', emojiData => {
+		removeCustomEmojis(emojiData.emojis);
+	});
+
 	for (const plugin of ColdDeviceStorage.get('plugins').filter(p => p.active)) {
 		import('@/plugin.js').then(async ({ install }) => {
 			// Workaround for https://bugs.webkit.org/show_bug.cgi?id=242740
@@ -67,14 +83,6 @@ export async function mainBoot() {
 		});
 	}
 
-	const hotkeys = {
-		'd': (): void => {
-			defaultStore.set('darkMode', !defaultStore.state.darkMode);
-		},
-		's': (): void => {
-			mainRouter.push('/search');
-		},
-	};
 	try {
 		if (defaultStore.state.enableSeasonalScreenEffect) {
 			const month = new Date().getMonth() + 1;
@@ -96,36 +104,41 @@ export async function mainBoot() {
 					}).render();
 				}
 			}
-		}	
+		}
 	} catch (error) {
 		// console.error(error);
 		console.error('Failed to initialise the seasonal screen effect canvas context:', error);
 	}
 
 	if ($i) {
-		// only add post shortcuts if logged in
-		hotkeys['p|n'] = post;
-
 		defaultStore.loaded.then(() => {
 			if (defaultStore.state.accountSetupWizard !== -1) {
-				popup(defineAsyncComponent(() => import('@/components/MkUserSetupDialog.vue')), {}, {}, 'closed');
+				const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkUserSetupDialog.vue')), {}, {
+					closed: () => dispose(),
+				});
 			}
 		});
 
 		for (const announcement of ($i.unreadAnnouncements ?? []).filter(x => x.display === 'dialog')) {
-			popup(defineAsyncComponent(() => import('@/components/MkAnnouncementDialog.vue')), {
+			const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkAnnouncementDialog.vue')), {
 				announcement,
-			}, {}, 'closed');
+			}, {
+				closed: () => dispose(),
+			});
 		}
 
-		stream.on('announcementCreated', (ev) => {
+		function onAnnouncementCreated (ev: { announcement: Misskey.entities.Announcement }) {
 			const announcement = ev.announcement;
 			if (announcement.display === 'dialog') {
-				popup(defineAsyncComponent(() => import('@/components/MkAnnouncementDialog.vue')), {
+				const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkAnnouncementDialog.vue')), {
 					announcement,
-				}, {}, 'closed');
+				}, {
+					closed: () => dispose(),
+				});
 			}
-		});
+		}
+
+		stream.on('announcementCreated', onAnnouncementCreated);
 
 		if ($i.isDeleted) {
 			alert({
@@ -217,43 +230,84 @@ export async function mainBoot() {
 			claimAchievement('collectAchievements30');
 		}
 
-		window.setInterval(() => {
-			if (Math.floor(Math.random() * 20000) === 0) {
-				claimAchievement('justPlainLucky');
+		if (!claimedAchievements.includes('justPlainLucky')) {
+			let justPlainLuckyTimer: number | null = null;
+			let lastVisibilityChangedAt = Date.now();
+
+			function claimPlainLucky() {
+				if (document.visibilityState !== 'visible') {
+					if (justPlainLuckyTimer != null) window.clearTimeout(justPlainLuckyTimer);
+					return;
+				}
+
+				if (Math.floor(Math.random() * 20000) === 0) {
+					claimAchievement('justPlainLucky');
+				} else {
+					justPlainLuckyTimer = window.setTimeout(claimPlainLucky, 1000 * 10);
+				}
 			}
-		}, 1000 * 10);
 
-		window.setTimeout(() => {
-			claimAchievement('client30min');
-		}, 1000 * 60 * 30);
+			window.addEventListener('visibilitychange', () => {
+				const now = Date.now();
 
-		window.setTimeout(() => {
-			claimAchievement('client60min');
-		}, 1000 * 60 * 60);
+				if (document.visibilityState === 'visible') {
+					// タブを高速で切り替えたら取得処理が何度も走るのを防ぐ
+					if ((now - lastVisibilityChangedAt) < 1000 * 10) {
+						justPlainLuckyTimer = window.setTimeout(claimPlainLucky, 1000 * 10);
+					} else {
+						claimPlainLucky();
+					}
+				} else if (justPlainLuckyTimer != null) {
+					window.clearTimeout(justPlainLuckyTimer);
+					justPlainLuckyTimer = null;
+				}
 
-		const lastUsed = miLocalStorage.getItem('lastUsed');
-		if (lastUsed) {
-			const lastUsedDate = parseInt(lastUsed, 10);
-			// 二時間以上前なら
-			if (Date.now() - lastUsedDate > 1000 * 60 * 60 * 2) {
-				toast(i18n.tsx.welcomeBackWithName({
-					name: $i.name || $i.username,
-				}));
-			}
+				lastVisibilityChangedAt = now;
+			}, { passive: true });
+
+			claimPlainLucky();
 		}
-		miLocalStorage.setItem('lastUsed', Date.now().toString());
+
+		if (!claimedAchievements.includes('client30min')) {
+			window.setTimeout(() => {
+				claimAchievement('client30min');
+			}, 1000 * 60 * 30);
+		}
+
+		if (!claimedAchievements.includes('client60min')) {
+			window.setTimeout(() => {
+				claimAchievement('client60min');
+			}, 1000 * 60 * 60);
+		}
+
+		// 邪魔
+		//const lastUsed = miLocalStorage.getItem('lastUsed');
+		//if (lastUsed) {
+		//	const lastUsedDate = parseInt(lastUsed, 10);
+		//	// 二時間以上前なら
+		//	if (Date.now() - lastUsedDate > 1000 * 60 * 60 * 2) {
+		//		toast(i18n.tsx.welcomeBackWithName({
+		//			name: $i.name || $i.username,
+		//		}));
+		//	}
+		//}
+		//miLocalStorage.setItem('lastUsed', Date.now().toString());
 
 		const latestDonationInfoShownAt = miLocalStorage.getItem('latestDonationInfoShownAt');
 		const neverShowDonationInfo = miLocalStorage.getItem('neverShowDonationInfo');
 		if (neverShowDonationInfo !== 'true' && (createdAt.getTime() < (Date.now() - (1000 * 60 * 60 * 24 * 3))) && !location.pathname.startsWith('/miauth')) {
 			if (latestDonationInfoShownAt == null || (new Date(latestDonationInfoShownAt).getTime() < (Date.now() - (1000 * 60 * 60 * 24 * 30)))) {
-				popup(defineAsyncComponent(() => import('@/components/MkDonation.vue')), {}, {}, 'closed');
+				const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkDonation.vue')), {}, {
+					closed: () => dispose(),
+				});
 			}
 		}
 
 		const modifiedVersionMustProminentlyOfferInAgplV3Section13Read = miLocalStorage.getItem('modifiedVersionMustProminentlyOfferInAgplV3Section13Read');
 		if (modifiedVersionMustProminentlyOfferInAgplV3Section13Read !== 'true' && instance.repositoryUrl !== 'https://github.com/misskey-dev/misskey') {
-			popup(defineAsyncComponent(() => import('@/components/MkSourceCodeAvailablePopup.vue')), {}, {}, 'closed');
+			const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkSourceCodeAvailablePopup.vue')), {}, {
+				closed: () => dispose(),
+			});
 		}
 
 		if ('Notification' in window) {
@@ -267,11 +321,11 @@ export async function mainBoot() {
 
 		// 自分の情報が更新されたとき
 		main.on('meUpdated', i => {
-			updateAccount(i);
+			updateAccountPartial(i);
 		});
 
 		main.on('readAllNotifications', () => {
-			updateAccount({
+			updateAccountPartial({
 				hasUnreadNotification: false,
 				unreadNotificationsCount: 0,
 			});
@@ -279,41 +333,44 @@ export async function mainBoot() {
 
 		main.on('unreadNotification', () => {
 			const unreadNotificationsCount = ($i?.unreadNotificationsCount ?? 0) + 1;
-			updateAccount({
+			updateAccountPartial({
 				hasUnreadNotification: true,
 				unreadNotificationsCount,
 			});
 		});
 
 		main.on('unreadMention', () => {
-			updateAccount({ hasUnreadMentions: true });
+			updateAccountPartial({ hasUnreadMentions: true });
 		});
 
 		main.on('readAllUnreadMentions', () => {
-			updateAccount({ hasUnreadMentions: false });
+			updateAccountPartial({ hasUnreadMentions: false });
 		});
 
 		main.on('unreadSpecifiedNote', () => {
-			updateAccount({ hasUnreadSpecifiedNotes: true });
+			updateAccountPartial({ hasUnreadSpecifiedNotes: true });
 		});
 
 		main.on('readAllUnreadSpecifiedNotes', () => {
-			updateAccount({ hasUnreadSpecifiedNotes: false });
+			updateAccountPartial({ hasUnreadSpecifiedNotes: false });
 		});
 
 		main.on('readAllAntennas', () => {
-			updateAccount({ hasUnreadAntenna: false });
+			updateAccountPartial({ hasUnreadAntenna: false });
 		});
 
 		main.on('unreadAntenna', () => {
-			updateAccount({ hasUnreadAntenna: true });
+			updateAccountPartial({ hasUnreadAntenna: true });
 			sound.playMisskeySfx('antenna');
 		});
 
 		main.on('readAllAnnouncements', () => {
-			updateAccount({ hasUnreadAnnouncement: false });
+			updateAccountPartial({ hasUnreadAnnouncement: false });
 		});
 
+		// 個人宛てお知らせが発行されたとき
+		main.on('announcementCreated', onAnnouncementCreated);
+
 		// トークンが再生成されたとき
 		// このままではMisskeyが利用できないので強制的にサインアウトさせる
 		main.on('myTokenRegenerated', () => {
@@ -322,7 +379,19 @@ export async function mainBoot() {
 	}
 
 	// shortcut
-	document.addEventListener('keydown', makeHotkey(hotkeys));
+	const keymap = {
+		'p|n': () => {
+			if ($i == null) return;
+			post();
+		},
+		'd': () => {
+			defaultStore.set('darkMode', !defaultStore.state.darkMode);
+		},
+		's': () => {
+			mainRouter.push('/search');
+		},
+	} as const satisfies Keymap;
+	document.addEventListener('keydown', makeHotkey(keymap), { passive: false });
 
 	initializeSw();
 }
diff --git a/packages/frontend/src/boot/sub-boot.ts b/packages/frontend/src/boot/sub-boot.ts
index 017457822b..35c84d5568 100644
--- a/packages/frontend/src/boot/sub-boot.ts
+++ b/packages/frontend/src/boot/sub-boot.ts
@@ -5,9 +5,12 @@
 
 import { createApp, defineAsyncComponent } from 'vue';
 import { common } from './common.js';
+import { emojiPicker } from '@/scripts/emoji-picker.js';
 
 export async function subBoot() {
 	const { isClientUpdated } = await common(() => createApp(
 		defineAsyncComponent(() => import('@/ui/minimum.vue')),
 	));
+
+	emojiPicker.init();
 }
diff --git a/packages/frontend/src/components/MkAbuseReport.vue b/packages/frontend/src/components/MkAbuseReport.vue
index a28e7c2559..e48b6ef781 100644
--- a/packages/frontend/src/components/MkAbuseReport.vue
+++ b/packages/frontend/src/components/MkAbuseReport.vue
@@ -4,112 +4,151 @@ SPDX-License-Identifier: AGPL-3.0-only
 -->
 
 <template>
-<div class="bcekxzvu _margin _panel">
-	<div class="target">
-		<MkA v-user-preview="report.targetUserId" class="info" :to="`/admin/user/${report.targetUserId}`" :behavior="'window'">
-			<MkAvatar class="avatar" :user="report.targetUser" indicator/>
-			<div class="names">
-				<MkUserName class="name" :user="report.targetUser"/>
-				<MkAcct class="acct" :user="report.targetUser" style="display: block;"/>
-			</div>
-		</MkA>
-		<MkKeyValue>
-			<template #key>{{ i18n.ts.registeredDate }}</template>
-			<template #value>{{ dateString(report.targetUser.createdAt) }} (<MkTime :time="report.targetUser.createdAt"/>)</template>
-		</MkKeyValue>
-	</div>
-	<div class="detail">
-		<div>
-			<Mfm :text="report.comment" :linkNavigationBehavior="'window'"/>
+<MkFolder>
+	<template #icon>
+		<i v-if="report.resolved && report.resolvedAs === 'accept'" class="ti ti-check" style="color: var(--MI_THEME-success)"></i>
+		<i v-else-if="report.resolved && report.resolvedAs === 'reject'" class="ti ti-x" style="color: var(--MI_THEME-error)"></i>
+		<i v-else-if="report.resolved" class="ti ti-slash"></i>
+		<i v-else class="ti ti-exclamation-circle" style="color: var(--MI_THEME-warn)"></i>
+	</template>
+	<template #label><MkAcct :user="report.targetUser"/> (by <MkAcct :user="report.reporter"/>)</template>
+	<template #caption>{{ report.comment }}</template>
+	<template #suffix><MkTime :time="report.createdAt"/></template>
+	<template #footer>
+		<div class="_buttons">
+			<template v-if="!report.resolved">
+				<MkButton @click="resolve('accept')"><i class="ti ti-check" style="color: var(--MI_THEME-success)"></i> {{ i18n.ts._abuseUserReport.resolve }} ({{ i18n.ts._abuseUserReport.accept }})</MkButton>
+				<MkButton @click="resolve('reject')"><i class="ti ti-x" style="color: var(--MI_THEME-error)"></i> {{ i18n.ts._abuseUserReport.resolve }} ({{ i18n.ts._abuseUserReport.reject }})</MkButton>
+				<MkButton @click="resolve(null)"><i class="ti ti-slash"></i> {{ i18n.ts._abuseUserReport.resolve }} ({{ i18n.ts.other }})</MkButton>
+			</template>
+			<template v-if="report.targetUser.host != null">
+				<MkButton :disabled="report.forwarded" primary @click="forward"><i class="ti ti-corner-up-right"></i> {{ i18n.ts._abuseUserReport.forward }}</MkButton>
+				<div v-tooltip:dialog="i18n.ts._abuseUserReport.forwardDescription" class="_button _help"><i class="ti ti-help-circle"></i></div>
+			</template>
+			<button class="_button" style="margin-left: auto; width: 34px;" @click="showMenu"><i class="ti ti-dots"></i></button>
 		</div>
-		<hr/>
-		<div>{{ i18n.ts.reporter }}: <MkA :to="`/admin/user/${report.reporter.id}`" class="_link" :behavior="'window'">@{{ report.reporter.username }}</MkA></div>
+	</template>
+
+	<div class="_gaps_s">
+		<MkFolder :withSpacer="false">
+			<template #icon><MkAvatar :user="report.targetUser" style="width: 18px; height: 18px;"/></template>
+			<template #label>{{ i18n.ts.target }}: <MkAcct :user="report.targetUser"/></template>
+			<template #suffix>#{{ report.targetUserId.toUpperCase() }}</template>
+
+			<div style="container-type: inline-size;">
+				<RouterView :router="targetRouter"/>
+			</div>
+		</MkFolder>
+
+		<MkFolder :defaultOpen="true">
+			<template #icon><i class="ti ti-message-2"></i></template>
+			<template #label>{{ i18n.ts.details }}</template>
+			<div class="_gaps_s">
+				<Mfm :text="report.comment" :linkNavigationBehavior="'window'"/>
+			</div>
+		</MkFolder>
+
+		<MkFolder :withSpacer="false">
+			<template #icon><MkAvatar :user="report.reporter" style="width: 18px; height: 18px;"/></template>
+			<template #label>{{ i18n.ts.reporter }}: <MkAcct :user="report.reporter"/></template>
+			<template #suffix>#{{ report.reporterId.toUpperCase() }}</template>
+
+			<div style="container-type: inline-size;">
+				<RouterView :router="reporterRouter"/>
+			</div>
+		</MkFolder>
+
+		<MkFolder :defaultOpen="false">
+			<template #icon><i class="ti ti-message-2"></i></template>
+			<template #label>{{ i18n.ts.moderationNote }}</template>
+			<template #suffix>{{ moderationNote.length > 0 ? '...' : i18n.ts.none }}</template>
+			<div class="_gaps_s">
+				<MkTextarea v-model="moderationNote" manualSave>
+					<template #caption>{{ i18n.ts.moderationNoteDescription }}</template>
+				</MkTextarea>
+			</div>
+		</MkFolder>
+
 		<div v-if="report.assignee">
 			{{ i18n.ts.moderator }}:
 			<MkAcct :user="report.assignee"/>
 		</div>
-		<div><MkTime :time="report.createdAt"/></div>
-		<div class="action">
-			<MkSwitch v-model="forward" :disabled="report.targetUser.host == null || report.resolved">
-				{{ i18n.ts.forwardReport }}
-				<template #caption>{{ i18n.ts.forwardReportIsAnonymous }}</template>
-			</MkSwitch>
-			<MkButton v-if="!report.resolved" primary @click="resolve">{{ i18n.ts.abuseMarkAsResolved }}</MkButton>
-		</div>
 	</div>
-</div>
+</MkFolder>
 </template>
 
 <script lang="ts" setup>
-import { ref } from 'vue';
+import { provide, ref, watch } from 'vue';
+import * as Misskey from 'misskey-js';
 import MkButton from '@/components/MkButton.vue';
 import MkSwitch from '@/components/MkSwitch.vue';
 import MkKeyValue from '@/components/MkKeyValue.vue';
 import * as os from '@/os.js';
 import { i18n } from '@/i18n.js';
 import { dateString } from '@/filters/date.js';
+import MkFolder from '@/components/MkFolder.vue';
+import RouterView from '@/components/global/RouterView.vue';
+import { useRouterFactory } from '@/router/supplier';
+import MkTextarea from '@/components/MkTextarea.vue';
+import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
 
 const props = defineProps<{
-	report: any;
+	report: Misskey.entities.AdminAbuseUserReportsResponse[number];
 }>();
 
 const emit = defineEmits<{
 	(ev: 'resolved', reportId: string): void;
 }>();
 
-const forward = ref(props.report.forwarded);
+const routerFactory = useRouterFactory();
+const targetRouter = routerFactory(`/admin/user/${props.report.targetUserId}`);
+targetRouter.init();
+const reporterRouter = routerFactory(`/admin/user/${props.report.reporterId}`);
+reporterRouter.init();
 
-function resolve() {
-	os.apiWithDialog('admin/resolve-abuse-user-report', {
-		forward: forward.value,
+const moderationNote = ref(props.report.moderationNote ?? '');
+
+watch(moderationNote, async () => {
+	os.apiWithDialog('admin/update-abuse-user-report', {
 		reportId: props.report.id,
+		moderationNote: moderationNote.value,
+	}).then(() => {
+	});
+});
+
+function resolve(resolvedAs) {
+	os.apiWithDialog('admin/resolve-abuse-user-report', {
+		reportId: props.report.id,
+		resolvedAs,
 	}).then(() => {
 		emit('resolved', props.report.id);
 	});
 }
+
+function forward() {
+	os.apiWithDialog('admin/forward-abuse-user-report', {
+		reportId: props.report.id,
+	}).then(() => {
+
+	});
+}
+
+function showMenu(ev: MouseEvent) {
+	os.popupMenu([{
+		icon: 'ti ti-id',
+		text: 'Copy ID',
+		action: () => {
+			copyToClipboard(props.report.id);
+		},
+	}, {
+		icon: 'ti ti-json',
+		text: 'Copy JSON',
+		action: () => {
+			copyToClipboard(JSON.stringify(props.report, null, '\t'));
+		},
+	}], ev.currentTarget ?? ev.target);
+}
 </script>
 
-<style lang="scss" scoped>
-.bcekxzvu {
-	display: flex;
-
-	> .target {
-		width: 35%;
-		box-sizing: border-box;
-		text-align: left;
-		padding: 24px;
-		border-right: solid 1px var(--divider);
-
-		> .info {
-			display: flex;
-			box-sizing: border-box;
-			align-items: center;
-			padding: 14px;
-			border-radius: 8px;
-			--c: rgb(255 196 0 / 15%);
-			background-image: linear-gradient(45deg, var(--c) 16.67%, transparent 16.67%, transparent 50%, var(--c) 50%, var(--c) 66.67%, transparent 66.67%, transparent 100%);
-			background-size: 16px 16px;
-
-			> .avatar {
-				width: 42px;
-				height: 42px;
-			}
-
-			> .names {
-				margin-left: 0.3em;
-				padding: 0 8px;
-				flex: 1;
-
-				> .name {
-					font-weight: bold;
-				}
-			}
-		}
-	}
-
-	> .detail {
-		flex: 1;
-		padding: 24px;
-	}
-}
+<style lang="scss" module>
 </style>
diff --git a/packages/frontend/src/components/MkAbuseReportWindow.vue b/packages/frontend/src/components/MkAbuseReportWindow.vue
index b09c7bb3fb..a634a748e9 100644
--- a/packages/frontend/src/components/MkAbuseReportWindow.vue
+++ b/packages/frontend/src/components/MkAbuseReportWindow.vue
@@ -39,7 +39,7 @@ import * as os from '@/os.js';
 import { i18n } from '@/i18n.js';
 
 const props = defineProps<{
-	user: Misskey.entities.UserDetailed;
+	user: Misskey.entities.UserLite;
 	initialComment?: string;
 }>();
 
diff --git a/packages/frontend/src/components/MkAccountMoved.vue b/packages/frontend/src/components/MkAccountMoved.vue
index 6c0774b634..0839955d9d 100644
--- a/packages/frontend/src/components/MkAccountMoved.vue
+++ b/packages/frontend/src/components/MkAccountMoved.vue
@@ -16,7 +16,7 @@ import { ref } from 'vue';
 import * as Misskey from 'misskey-js';
 import MkMention from './MkMention.vue';
 import { i18n } from '@/i18n.js';
-import { host as localHost } from '@/config.js';
+import { host as localHost } from '@@/js/config.js';
 import { misskeyApi } from '@/scripts/misskey-api.js';
 
 const user = ref<Misskey.entities.UserLite>();
@@ -32,9 +32,9 @@ misskeyApi('users/show', { userId: props.movedTo }).then(u => user.value = u);
 .root {
 	padding: 16px;
 	font-size: 90%;
-	background: var(--infoWarnBg);
-	color: var(--error);
-	border-radius: var(--radius);
+	background: var(--MI_THEME-infoWarnBg);
+	color: var(--MI_THEME-error);
+	border-radius: var(--MI-radius);
 }
 
 .link {
diff --git a/packages/frontend/src/components/MkAchievements.vue b/packages/frontend/src/components/MkAchievements.vue
index 5d103fa789..c8134416b5 100644
--- a/packages/frontend/src/components/MkAchievements.vue
+++ b/packages/frontend/src/components/MkAchievements.vue
@@ -153,7 +153,7 @@ onMounted(() => {
 		background: linear-gradient(0deg, #ffee20, #eb7018);
 	}
 
-	&:before {
+	&::before {
 		content: "";
 		display: block;
 		position: absolute;
@@ -173,7 +173,7 @@ onMounted(() => {
 		background: linear-gradient(0deg, #e1e1e1, #7c7c7c);
 	}
 
-	&:before {
+	&::before {
 		content: "";
 		display: block;
 		position: absolute;
diff --git a/packages/frontend/src/components/MkAnalogClock.vue b/packages/frontend/src/components/MkAnalogClock.vue
index 835efbd6cd..c8fa6246e0 100644
--- a/packages/frontend/src/components/MkAnalogClock.vue
+++ b/packages/frontend/src/components/MkAnalogClock.vue
@@ -193,12 +193,12 @@ tick();
 
 function calcColors() {
 	const computedStyle = getComputedStyle(document.documentElement);
-	const dark = tinycolor(computedStyle.getPropertyValue('--bg')).isDark();
-	const accent = tinycolor(computedStyle.getPropertyValue('--accent')).toHexString();
+	const dark = tinycolor(computedStyle.getPropertyValue('--MI_THEME-bg')).isDark();
+	const accent = tinycolor(computedStyle.getPropertyValue('--MI_THEME-accent')).toHexString();
 	majorGraduationColor.value = dark ? 'rgba(255, 255, 255, 0.3)' : 'rgba(0, 0, 0, 0.3)';
 	//minorGraduationColor = dark ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
 	sHandColor.value = dark ? 'rgba(255, 255, 255, 0.5)' : 'rgba(0, 0, 0, 0.3)';
-	mHandColor.value = tinycolor(computedStyle.getPropertyValue('--fg')).toHexString();
+	mHandColor.value = tinycolor(computedStyle.getPropertyValue('--MI_THEME-fg')).toHexString();
 	hHandColor.value = accent;
 	nowColor.value = accent;
 }
diff --git a/packages/frontend/src/components/MkAnnouncementDialog.vue b/packages/frontend/src/components/MkAnnouncementDialog.vue
index f27694658e..3045a47585 100644
--- a/packages/frontend/src/components/MkAnnouncementDialog.vue
+++ b/packages/frontend/src/components/MkAnnouncementDialog.vue
@@ -9,9 +9,9 @@ SPDX-License-Identifier: AGPL-3.0-only
 		<div :class="$style.header">
 			<span :class="$style.icon">
 				<i v-if="announcement.icon === 'info'" class="ti ti-info-circle"></i>
-				<i v-else-if="announcement.icon === 'warning'" class="ti ti-alert-triangle" style="color: var(--warn);"></i>
-				<i v-else-if="announcement.icon === 'error'" class="ti ti-circle-x" style="color: var(--error);"></i>
-				<i v-else-if="announcement.icon === 'success'" class="ti ti-check" style="color: var(--success);"></i>
+				<i v-else-if="announcement.icon === 'warning'" class="ti ti-alert-triangle" style="color: var(--MI_THEME-warn);"></i>
+				<i v-else-if="announcement.icon === 'error'" class="ti ti-circle-x" style="color: var(--MI_THEME-error);"></i>
+				<i v-else-if="announcement.icon === 'success'" class="ti ti-check" style="color: var(--MI_THEME-success);"></i>
 			</span>
 			<span :class="$style.title">{{ announcement.title }}</span>
 		</div>
@@ -29,7 +29,7 @@ import { misskeyApi } from '@/scripts/misskey-api.js';
 import MkModal from '@/components/MkModal.vue';
 import MkButton from '@/components/MkButton.vue';
 import { i18n } from '@/i18n.js';
-import { $i, updateAccount } from '@/account.js';
+import { $i, updateAccountPartial } from '@/account.js';
 
 const props = withDefaults(defineProps<{
 	announcement: Misskey.entities.Announcement;
@@ -51,7 +51,7 @@ async function ok() {
 
 	modal.value?.close();
 	misskeyApi('i/read-announcement', { announcementId: props.announcement.id });
-	updateAccount({
+	updateAccountPartial({
 		unreadAnnouncements: $i!.unreadAnnouncements.filter(a => a.id !== props.announcement.id),
 	});
 }
@@ -83,8 +83,8 @@ onMounted(() => {
 	min-width: 320px;
 	max-width: 480px;
 	box-sizing: border-box;
-	background: var(--panel);
-	border-radius: var(--radius);
+	background: var(--MI_THEME-panel);
+	border-radius: var(--MI-radius);
 }
 
 .header {
diff --git a/packages/frontend/src/components/MkAntennaEditor.stories.impl.ts b/packages/frontend/src/components/MkAntennaEditor.stories.impl.ts
new file mode 100644
index 0000000000..1749e07a4e
--- /dev/null
+++ b/packages/frontend/src/components/MkAntennaEditor.stories.impl.ts
@@ -0,0 +1,62 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+/* eslint-disable @typescript-eslint/explicit-function-return-type */
+import { action } from '@storybook/addon-actions';
+import { StoryObj } from '@storybook/vue3';
+import { HttpResponse, http } from 'msw';
+import { commonHandlers } from '../../.storybook/mocks.js';
+import MkAntennaEditor from './MkAntennaEditor.vue';
+export const Default = {
+	render(args) {
+		return {
+			components: {
+				MkAntennaEditor,
+			},
+			setup() {
+				return {
+					args,
+				};
+			},
+			computed: {
+				props() {
+					return {
+						...this.args,
+					};
+				},
+				events() {
+					return {
+						created: action('created'),
+						updated: action('updated'),
+						deleted: action('deleted'),
+					};
+				},
+			},
+			template: '<MkAntennaEditor v-bind="props" v-on="events" />',
+		};
+	},
+	args: {
+	},
+	parameters: {
+		layout: 'fullscreen',
+		msw: {
+			handlers: [
+				...commonHandlers,
+				http.post('/api/antennas/create', async ({ request }) => {
+					action('POST /api/antennas/create')(await request.json());
+					return HttpResponse.json({});
+				}),
+				http.post('/api/antennas/update', async ({ request }) => {
+					action('POST /api/antennas/update')(await request.json());
+					return HttpResponse.json({});
+				}),
+				http.post('/api/antennas/delete', async ({ request }) => {
+					action('POST /api/antennas/delete')(await request.json());
+					return HttpResponse.json();
+				}),
+			],
+		},
+	},
+} satisfies StoryObj<typeof MkAntennaEditor>;
diff --git a/packages/frontend/src/pages/my-antennas/editor.vue b/packages/frontend/src/components/MkAntennaEditor.vue
similarity index 62%
rename from packages/frontend/src/pages/my-antennas/editor.vue
rename to packages/frontend/src/components/MkAntennaEditor.vue
index 2949bfc02c..e622d57f1e 100644
--- a/packages/frontend/src/pages/my-antennas/editor.vue
+++ b/packages/frontend/src/components/MkAntennaEditor.vue
@@ -41,8 +41,10 @@ SPDX-License-Identifier: AGPL-3.0-only
 			<MkSwitch v-model="withFile">{{ i18n.ts.withFileAntenna }}</MkSwitch>
 		</div>
 		<div :class="$style.actions">
-			<MkButton inline primary @click="saveAntenna()"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton>
-			<MkButton v-if="antenna.id != null" inline danger @click="deleteAntenna()"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton>
+			<div class="_buttons">
+				<MkButton inline primary @click="saveAntenna()"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton>
+				<MkButton v-if="initialAntenna.id != null" inline danger @click="deleteAntenna()"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton>
+			</div>
 		</div>
 	</div>
 </MkSpacer>
@@ -59,28 +61,53 @@ import MkSwitch from '@/components/MkSwitch.vue';
 import * as os from '@/os.js';
 import { misskeyApi } from '@/scripts/misskey-api.js';
 import { i18n } from '@/i18n.js';
+import { deepMerge } from '@/scripts/merge.js';
+import type { DeepPartial } from '@/scripts/merge.js';
+
+type PartialAllowedAntenna = Omit<Misskey.entities.Antenna, 'id' | 'createdAt' | 'updatedAt'> & {
+	id?: string;
+	createdAt?: string;
+	updatedAt?: string;
+};
 
 const props = defineProps<{
-	antenna: Misskey.entities.Antenna
+	antenna?: DeepPartial<PartialAllowedAntenna>;
 }>();
 
+const initialAntenna = deepMerge<PartialAllowedAntenna>(props.antenna ?? {}, {
+	name: '',
+	src: 'all',
+	userListId: null,
+	users: [],
+	keywords: [],
+	excludeKeywords: [],
+	excludeBots: false,
+	withReplies: false,
+	caseSensitive: false,
+	localOnly: false,
+	withFile: false,
+	isActive: true,
+	hasUnreadNote: false,
+	notify: false,
+});
+
 const emit = defineEmits<{
-	(ev: 'created'): void,
-	(ev: 'updated'): void,
+	(ev: 'created', newAntenna: Misskey.entities.Antenna): void,
+	(ev: 'updated', editedAntenna: Misskey.entities.Antenna): void,
 	(ev: 'deleted'): void,
 }>();
 
-const name = ref<string>(props.antenna.name);
-const src = ref<Misskey.entities.AntennasCreateRequest['src']>(props.antenna.src);
-const userListId = ref<string | null>(props.antenna.userListId);
-const users = ref<string>(props.antenna.users.join('\n'));
-const keywords = ref<string>(props.antenna.keywords.map(x => x.join(' ')).join('\n'));
-const excludeKeywords = ref<string>(props.antenna.excludeKeywords.map(x => x.join(' ')).join('\n'));
-const caseSensitive = ref<boolean>(props.antenna.caseSensitive);
-const localOnly = ref<boolean>(props.antenna.localOnly);
-const excludeBots = ref<boolean>(props.antenna.excludeBots);
-const withReplies = ref<boolean>(props.antenna.withReplies);
-const withFile = ref<boolean>(props.antenna.withFile);
+const name = ref<string>(initialAntenna.name);
+const src = ref<Misskey.entities.AntennasCreateRequest['src']>(initialAntenna.src);
+const userListId = ref<string | null>(initialAntenna.userListId);
+const users = ref<string>(initialAntenna.users.join('\n'));
+const keywords = ref<string>(initialAntenna.keywords.map(x => x.join(' ')).join('\n'));
+const excludeKeywords = ref<string>(initialAntenna.excludeKeywords.map(x => x.join(' ')).join('\n'));
+const caseSensitive = ref<boolean>(initialAntenna.caseSensitive);
+const localOnly = ref<boolean>(initialAntenna.localOnly);
+const excludeBots = ref<boolean>(initialAntenna.excludeBots);
+const withReplies = ref<boolean>(initialAntenna.withReplies);
+const withFile = ref<boolean>(initialAntenna.withFile);
 const userLists = ref<Misskey.entities.UserList[] | null>(null);
 
 watch(() => src.value, async () => {
@@ -104,24 +131,26 @@ async function saveAntenna() {
 		excludeKeywords: excludeKeywords.value.trim().split('\n').map(x => x.trim().split(' ')),
 	};
 
-	if (props.antenna.id == null) {
-		await os.apiWithDialog('antennas/create', antennaData);
-		emit('created');
+	if (initialAntenna.id == null) {
+		const res = await os.apiWithDialog('antennas/create', antennaData);
+		emit('created', res);
 	} else {
-		await os.apiWithDialog('antennas/update', { ...antennaData, antennaId: props.antenna.id });
-		emit('updated');
+		const res = await os.apiWithDialog('antennas/update', { ...antennaData, antennaId: initialAntenna.id });
+		emit('updated', res);
 	}
 }
 
 async function deleteAntenna() {
+	if (initialAntenna.id == null) return;
+
 	const { canceled } = await os.confirm({
 		type: 'warning',
-		text: i18n.tsx.removeAreYouSure({ x: props.antenna.name }),
+		text: i18n.tsx.removeAreYouSure({ x: initialAntenna.name }),
 	});
 	if (canceled) return;
 
 	await misskeyApi('antennas/delete', {
-		antennaId: props.antenna.id,
+		antennaId: initialAntenna.id,
 	});
 
 	os.success();
@@ -131,7 +160,7 @@ async function deleteAntenna() {
 function addUser() {
 	os.selectUser({ includeSelf: true }).then(user => {
 		users.value = users.value.trim();
-		users.value += '\n@' + Misskey.acct.toString(user as any);
+		users.value += '\n@' + Misskey.acct.toString(user);
 		users.value = users.value.trim();
 	});
 }
@@ -141,6 +170,6 @@ function addUser() {
 .actions {
 	margin-top: 16px;
 	padding: 24px 0;
-	border-top: solid 0.5px var(--divider);
+	border-top: solid 0.5px var(--MI_THEME-divider);
 }
 </style>
diff --git a/packages/frontend/src/components/MkAntennaEditorDialog.stories.impl.ts b/packages/frontend/src/components/MkAntennaEditorDialog.stories.impl.ts
new file mode 100644
index 0000000000..1c6ca83b47
--- /dev/null
+++ b/packages/frontend/src/components/MkAntennaEditorDialog.stories.impl.ts
@@ -0,0 +1,63 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+/* eslint-disable @typescript-eslint/explicit-function-return-type */
+import { action } from '@storybook/addon-actions';
+import { StoryObj } from '@storybook/vue3';
+import { HttpResponse, http } from 'msw';
+import { commonHandlers } from '../../.storybook/mocks.js';
+import MkAntennaEditorDialog from './MkAntennaEditorDialog.vue';
+export const Default = {
+	render(args) {
+		return {
+			components: {
+				MkAntennaEditorDialog,
+			},
+			setup() {
+				return {
+					args,
+				};
+			},
+			computed: {
+				props() {
+					return {
+						...this.args,
+					};
+				},
+				events() {
+					return {
+						created: action('created'),
+						updated: action('updated'),
+						deleted: action('deleted'),
+						closed: action('closed'),
+					};
+				},
+			},
+			template: '<MkAntennaEditorDialog v-bind="props" v-on="events" />',
+		};
+	},
+	args: {
+	},
+	parameters: {
+		layout: 'centered',
+		msw: {
+			handlers: [
+				...commonHandlers,
+				http.post('/api/antennas/create', async ({ request }) => {
+					action('POST /api/antennas/create')(await request.json());
+					return HttpResponse.json({});
+				}),
+				http.post('/api/antennas/update', async ({ request }) => {
+					action('POST /api/antennas/update')(await request.json());
+					return HttpResponse.json({});
+				}),
+				http.post('/api/antennas/delete', async ({ request }) => {
+					action('POST /api/antennas/delete')(await request.json());
+					return HttpResponse.json();
+				}),
+			],
+		},
+	},
+} satisfies StoryObj<typeof MkAntennaEditorDialog>;
diff --git a/packages/frontend/src/components/MkAntennaEditorDialog.vue b/packages/frontend/src/components/MkAntennaEditorDialog.vue
new file mode 100644
index 0000000000..6d815d29f3
--- /dev/null
+++ b/packages/frontend/src/components/MkAntennaEditorDialog.vue
@@ -0,0 +1,63 @@
+<!--
+SPDX-FileCopyrightText: syuilo and misskey-project
+SPDX-License-Identifier: AGPL-3.0-only
+-->
+
+<template>
+<MkModalWindow
+	ref="dialog"
+	:withOkButton="false"
+	:width="500"
+	:height="550"
+	@close="close()"
+	@closed="emit('closed')"
+>
+	<template #header>{{ antenna == null ? i18n.ts.createAntenna : i18n.ts.editAntenna }}</template>
+	<XAntennaEditor
+		:antenna="antenna"
+		@created="onAntennaCreated"
+		@updated="onAntennaUpdated"
+		@deleted="onAntennaDeleted"
+	/>
+</MkModalWindow>
+</template>
+
+<script lang="ts" setup>
+import { shallowRef } from 'vue';
+import * as Misskey from 'misskey-js';
+import MkModalWindow from '@/components/MkModalWindow.vue';
+import XAntennaEditor from '@/components/MkAntennaEditor.vue';
+import { i18n } from '@/i18n.js';
+
+defineProps<{
+	antenna?: Misskey.entities.Antenna;
+}>();
+
+const emit = defineEmits<{
+	(ev: 'created', newAntenna: Misskey.entities.Antenna): void,
+	(ev: 'updated', editedAntenna: Misskey.entities.Antenna): void,
+	(ev: 'deleted'): void,
+	(ev: 'closed'): void,
+}>();
+
+const dialog = shallowRef<InstanceType<typeof MkModalWindow>>();
+
+function onAntennaCreated(newAntenna: Misskey.entities.Antenna) {
+	emit('created', newAntenna);
+	dialog.value?.close();
+}
+
+function onAntennaUpdated(editedAntenna: Misskey.entities.Antenna) {
+	emit('updated', editedAntenna);
+	dialog.value?.close();
+}
+
+function onAntennaDeleted() {
+	emit('deleted');
+	dialog.value?.close();
+}
+
+function close() {
+	dialog.value?.close();
+}
+</script>
diff --git a/packages/frontend/src/components/MkAsUi.vue b/packages/frontend/src/components/MkAsUi.vue
index 18e8e7542e..e52ab5ccad 100644
--- a/packages/frontend/src/components/MkAsUi.vue
+++ b/packages/frontend/src/components/MkAsUi.vue
@@ -54,7 +54,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 			<MkAsUi v-if="!g(child).hidden" :component="g(child)" :components="props.components" :size="size"/>
 		</template>
 	</MkFolder>
-	<div v-else-if="c.type === 'container'" :class="[$style.container, { [$style.fontSerif]: c.font === 'serif', [$style.fontMonospace]: c.font === 'monospace' }]" :style="{ textAlign: c.align, backgroundColor: c.bgColor, color: c.fgColor, borderWidth: c.borderWidth ? `${c.borderWidth}px` : 0, borderColor: c.borderColor ?? 'var(--divider)', padding: c.padding ? `${c.padding}px` : 0, borderRadius: c.rounded ? '8px' : 0 }">
+	<div v-else-if="c.type === 'container'" :class="[$style.container, { [$style.fontSerif]: c.font === 'serif', [$style.fontMonospace]: c.font === 'monospace' }]" :style="containerStyle">
 		<template v-for="child in c.children" :key="child">
 			<MkAsUi v-if="!g(child).hidden" :component="g(child)" :components="props.components" :size="size" :align="c.align"/>
 		</template>
@@ -63,7 +63,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { Ref, ref } from 'vue';
+import { Ref, ref, computed } from 'vue';
 import * as os from '@/os.js';
 import MkButton from '@/components/MkButton.vue';
 import MkInput from '@/components/MkInput.vue';
@@ -97,6 +97,29 @@ function g(id) {
 	} as AsUiRoot;
 }
 
+const containerStyle = computed(() => {
+	if (c.type !== 'container') return undefined;
+
+	// width, color, styleのうち一つでも指定があれば、枠線がちゃんと表示されるようにwidthとstyleのデフォルト値を設定
+	// radiusは単に角を丸める用途もあるため除外
+	const isBordered = c.borderWidth ?? c.borderColor ?? c.borderStyle;
+
+	const border = isBordered ? {
+		borderWidth: c.borderWidth ?? '1px',
+		borderColor: c.borderColor ?? 'var(--MI_THEME-divider)',
+		borderStyle: c.borderStyle ?? 'solid',
+	} : undefined;
+
+	return {
+		textAlign: c.align,
+		backgroundColor: c.bgColor,
+		color: c.fgColor,
+		padding: c.padding ? `${c.padding}px` : 0,
+		borderRadius: (c.borderRadius ?? (c.rounded ? 8 : 0)) + 'px',
+		...border,
+	};
+});
+
 const valueForSwitch = ref('default' in c && typeof c.default === 'boolean' ? c.default : false);
 
 function onSwitchUpdate(v) {
@@ -142,7 +165,7 @@ function openPostForm() {
 }
 
 .postForm {
-	background: var(--bg);
+	background: var(--MI_THEME-bg);
 	border-radius: 8px;
 }
 </style>
diff --git a/packages/backend/src/misc/prelude/math.ts b/packages/frontend/src/components/MkAuthConfirm.stories.impl.ts
similarity index 53%
rename from packages/backend/src/misc/prelude/math.ts
rename to packages/frontend/src/components/MkAuthConfirm.stories.impl.ts
index 38556def2d..0adc44e204 100644
--- a/packages/backend/src/misc/prelude/math.ts
+++ b/packages/frontend/src/components/MkAuthConfirm.stories.impl.ts
@@ -3,6 +3,5 @@
  * SPDX-License-Identifier: AGPL-3.0-only
  */
 
-export function gcd(a: number, b: number): number {
-	return b === 0 ? a : gcd(b, a % b);
-}
+import MkAuthConfirm from './MkAuthConfirm.vue';
+void MkAuthConfirm;
diff --git a/packages/frontend/src/components/MkAuthConfirm.vue b/packages/frontend/src/components/MkAuthConfirm.vue
new file mode 100644
index 0000000000..f78d2d38f0
--- /dev/null
+++ b/packages/frontend/src/components/MkAuthConfirm.vue
@@ -0,0 +1,450 @@
+<!--
+SPDX-FileCopyrightText: syuilo and misskey-project
+SPDX-License-Identifier: AGPL-3.0-only
+-->
+
+<template>
+<div :class="$style.wrapper">
+	<Transition
+		mode="out-in"
+		:enterActiveClass="$style.transition_enterActive"
+		:leaveActiveClass="$style.transition_leaveActive"
+		:enterFromClass="$style.transition_enterFrom"
+		:leaveToClass="$style.transition_leaveTo"
+
+		:inert="_waiting"
+	>
+		<div v-if="phase === 'accountSelect'" key="accountSelect" :class="$style.root" class="_gaps">
+			<div :class="$style.header" class="_gaps_s">
+				<div :class="$style.iconFallback">
+					<i class="ti ti-user"></i>
+				</div>
+				<div :class="$style.headerText">{{ i18n.ts.pleaseSelectAccount }}</div>
+			</div>
+			<div>
+				<div :class="$style.accountSelectorLabel">{{ i18n.ts.selectAccount }}</div>
+				<div :class="$style.accountSelectorList">
+					<template v-for="[id, user] in users">
+						<input :id="'account-' + id" v-model="selectedUser" type="radio" name="accountSelector" :value="id" :class="$style.accountSelectorRadio"/>
+						<label :for="'account-' + id" :class="$style.accountSelectorItem">
+							<MkAvatar :user="user" :class="$style.accountSelectorAvatar"/>
+							<div :class="$style.accountSelectorBody">
+								<MkUserName :user="user" :class="$style.accountSelectorName"/>
+								<MkAcct :user="user" :class="$style.accountSelectorAcct"/>
+							</div>
+						</label>
+					</template>
+					<button class="_button" :class="[$style.accountSelectorItem, $style.accountSelectorAddAccountRoot]" @click="clickAddAccount">
+						<div :class="[$style.accountSelectorAvatar, $style.accountSelectorAddAccountAvatar]">
+							<i class="ti ti-user-plus"></i>
+						</div>
+						<div :class="[$style.accountSelectorBody, $style.accountSelectorName]">{{ i18n.ts.addAccount }}</div>
+					</button>
+				</div>
+			</div>
+			<div class="_buttonsCenter">
+				<MkButton rounded gradate :disabled="selectedUser === null" @click="clickChooseAccount">{{ i18n.ts.continue }} <i class="ti ti-arrow-right"></i></MkButton>
+			</div>
+		</div>
+		<div v-else-if="phase === 'consent'" key="consent" :class="$style.root" class="_gaps">
+			<div :class="$style.header" class="_gaps_s">
+				<img v-if="icon" :class="$style.icon" :src="getProxiedImageUrl(icon, 'preview')"/>
+				<div v-else :class="$style.iconFallback">
+					<i class="ti ti-apps"></i>
+				</div>
+				<div :class="$style.headerText">{{ name ? i18n.tsx._auth.shareAccess({ name }) : i18n.ts._auth.shareAccessAsk }}</div>
+			</div>
+			<div v-if="permissions && permissions.length > 0" class="_gaps_s" :class="$style.permissionRoot">
+				<div>{{ name ? i18n.tsx._auth.permission({ name }) : i18n.ts._auth.permissionAsk }}</div>
+				<div :class="$style.permissionListWrapper">
+					<ul :class="$style.permissionList">
+						<li v-for="p in permissions" :key="p">{{ i18n.ts._permissions[p] }}</li>
+					</ul>
+				</div>
+			</div>
+			<slot name="consentAdditionalInfo"></slot>
+			<div>
+				<div :class="$style.accountSelectorLabel">
+					{{ i18n.ts._auth.scopeUser }} <button class="_textButton" @click="clickBackToAccountSelect">{{ i18n.ts.switchAccount }}</button>
+				</div>
+				<div :class="$style.accountSelectorList">
+					<div :class="[$style.accountSelectorItem, $style.static]">
+						<MkAvatar :user="users.get(selectedUser!)!" :class="$style.accountSelectorAvatar"/>
+						<div :class="$style.accountSelectorBody">
+							<MkUserName :user="users.get(selectedUser!)!" :class="$style.accountSelectorName"/>
+							<MkAcct :user="users.get(selectedUser!)!" :class="$style.accountSelectorAcct"/>
+						</div>
+					</div>
+				</div>
+			</div>
+			<div class="_buttonsCenter">
+				<MkButton rounded @click="clickCancel">{{ i18n.ts.reject }}</MkButton>
+				<MkButton rounded gradate @click="clickAccept">{{ i18n.ts.accept }}</MkButton>
+			</div>
+		</div>
+		<div v-else-if="phase === 'success'" key="success" :class="$style.root" class="_gaps_s">
+			<div :class="$style.header" class="_gaps_s">
+				<div :class="$style.iconFallback">
+					<i class="ti ti-check"></i>
+				</div>
+				<div :class="$style.headerText">{{ i18n.ts._auth.accepted }}</div>
+				<div :class="$style.headerTextSub">{{ i18n.ts._auth.pleaseGoBack }}</div>
+			</div>
+		</div>
+		<div v-else-if="phase === 'denied'" key="denied" :class="$style.root" class="_gaps_s">
+			<div :class="$style.header" class="_gaps_s">
+				<div :class="$style.iconFallback">
+					<i class="ti ti-x"></i>
+				</div>
+				<div :class="$style.headerText">{{ i18n.ts._auth.denied }}</div>
+			</div>
+		</div>
+		<div v-else-if="phase === 'failed'" key="failed" :class="$style.root" class="_gaps_s">
+			<div :class="$style.header" class="_gaps_s">
+				<div :class="$style.iconFallback">
+					<i class="ti ti-x"></i>
+				</div>
+				<div :class="$style.headerText">{{ i18n.ts.somethingHappened }}</div>
+			</div>
+		</div>
+	</Transition>
+	<div v-if="_waiting" :class="$style.waitingRoot">
+		<MkLoading/>
+	</div>
+</div>
+</template>
+
+<script setup lang="ts">
+import { ref, computed } from 'vue';
+import * as Misskey from 'misskey-js';
+
+import MkButton from '@/components/MkButton.vue';
+
+import { $i, getAccounts, getAccountWithSigninDialog, getAccountWithSignupDialog } from '@/account.js';
+import { i18n } from '@/i18n.js';
+import * as os from '@/os.js';
+import { getProxiedImageUrl } from '@/scripts/media-proxy.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
+
+const props = defineProps<{
+	name?: string;
+	icon?: string;
+	permissions?: (typeof Misskey.permissions[number])[];
+	manualWaiting?: boolean;
+	waitOnDeny?: boolean;
+}>();
+
+const emit = defineEmits<{
+	(ev: 'accept', token: string): void;
+	(ev: 'deny', token: string): void;
+}>();
+
+const waiting = ref(true);
+const _waiting = computed(() => waiting.value || props.manualWaiting);
+const phase = ref<'accountSelect' | 'consent' | 'success' | 'denied' | 'failed'>('accountSelect');
+
+const selectedUser = ref<string | null>(null);
+
+const users = ref(new Map<string, Misskey.entities.UserDetailed & { token: string; }>());
+
+async function init() {
+	waiting.value = true;
+
+	users.value.clear();
+
+	if ($i) {
+		users.value.set($i.id, $i);
+	}
+
+	const accounts = await getAccounts();
+
+	const accountIdsToFetch = accounts.map(a => a.id).filter(id => !users.value.has(id));
+
+	if (accountIdsToFetch.length > 0) {
+		const usersRes = await misskeyApi('users/show', {
+			userIds: accountIdsToFetch,
+		});
+
+		for (const user of usersRes) {
+			if (users.value.has(user.id)) continue;
+
+			users.value.set(user.id, {
+				...user,
+				token: accounts.find(a => a.id === user.id)!.token,
+			});
+		}
+	}
+
+	waiting.value = false;
+}
+
+init();
+
+function clickAddAccount(ev: MouseEvent) {
+	selectedUser.value = null;
+
+	os.popupMenu([{
+		text: i18n.ts.existingAccount,
+		action: () => {
+			getAccountWithSigninDialog().then(async (res) => {
+				if (res != null) {
+					os.success();
+					await init();
+					if (users.value.has(res.id)) {
+						selectedUser.value = res.id;
+					}
+				}
+			});
+		},
+	}, {
+		text: i18n.ts.createAccount,
+		action: () => {
+			getAccountWithSignupDialog().then(async (res) => {
+				if (res != null) {
+					os.success();
+					await init();
+					if (users.value.has(res.id)) {
+						selectedUser.value = res.id;
+					}
+				}
+			});
+		},
+	}], ev.currentTarget ?? ev.target);
+}
+
+function clickChooseAccount() {
+	if (selectedUser.value === null) return;
+
+	phase.value = 'consent';
+}
+
+function clickBackToAccountSelect() {
+	selectedUser.value = null;
+	phase.value = 'accountSelect';
+}
+
+function clickCancel() {
+	if (selectedUser.value === null) return;
+
+	const user = users.value.get(selectedUser.value)!;
+
+	const token = user.token;
+
+	if (props.waitOnDeny) {
+		waiting.value = true;
+	}
+	emit('deny', token);
+}
+
+async function clickAccept() {
+	if (selectedUser.value === null) return;
+
+	const user = users.value.get(selectedUser.value)!;
+
+	const token = user.token;
+
+	waiting.value = true;
+	emit('accept', token);
+}
+
+function showUI(state: 'success' | 'denied' | 'failed') {
+	phase.value = state;
+	waiting.value = false;
+}
+
+defineExpose({
+	showUI,
+});
+</script>
+
+<style lang="scss" module>
+.transition_enterActive,
+.transition_leaveActive {
+	transition: opacity 0.3s cubic-bezier(0,0,.35,1), transform 0.3s cubic-bezier(0,0,.35,1);
+}
+.transition_enterFrom {
+	opacity: 0;
+	transform: translateX(50px);
+}
+.transition_leaveTo {
+	opacity: 0;
+	transform: translateX(-50px);
+}
+
+.wrapper {
+	overflow-x: hidden;
+	overflow-x: clip;
+
+	position: relative;
+	width: 100%;
+	height: 100%;
+}
+
+.waitingRoot {
+	position: absolute;
+	top: 0;
+	left: 0;
+	width: 100%;
+	height: 100%;
+	background-color: color-mix(in srgb, var(--MI_THEME-panel), transparent 50%);
+	display: flex;
+	justify-content: center;
+	align-items: center;
+	z-index: 1;
+	cursor: wait;
+}
+
+.root {
+	position: relative;
+	box-sizing: border-box;
+	width: 100%;
+	padding: 48px 24px;
+}
+
+.header {
+	margin: 0 auto;
+	max-width: 320px;
+}
+
+.icon,
+.iconFallback {
+	display: block;
+	margin: 0 auto;
+	width: 54px;
+	height: 54px;
+}
+
+.icon {
+	border-radius: 50%;
+	border: 1px solid var(--MI_THEME-divider);
+	background-color: #fff;
+	object-fit: contain;
+}
+
+.iconFallback {
+	border-radius: 50%;
+	background-color: var(--MI_THEME-accentedBg);
+	color: var(--MI_THEME-accent);
+	text-align: center;
+	line-height: 54px;
+	font-size: 18px;
+}
+
+.headerText,
+.headerTextSub {
+	text-align: center;
+	word-break: normal;
+	word-break: auto-phrase;
+}
+
+.headerText {
+	font-size: 16px;
+	font-weight: 700;
+}
+
+.permissionRoot {
+	padding: 16px;
+	border-radius: var(--MI-radius);
+	background-color: var(--MI_THEME-bg);
+}
+
+.permissionListWrapper {
+	max-height: 350px;
+	overflow-y: auto;
+	padding: 12px;
+	border-radius: var(--MI-radius);
+	background-color: var(--MI_THEME-panel);
+}
+
+.permissionList {
+	margin: 0 0 0 1.5em;
+	padding: 0;
+	font-size: 90%;
+}
+
+.accountSelectorLabel {
+	font-size: 0.85em;
+	opacity: 0.7;
+	margin-bottom: 8px;
+}
+
+.accountSelectorList {
+	border-radius: var(--MI-radius);
+	border: 1px solid var(--MI_THEME-divider);
+	overflow: hidden;
+	overflow: clip;
+}
+
+.accountSelectorRadio {
+	position: absolute;
+	clip: rect(0, 0, 0, 0);
+	pointer-events: none;
+
+	&:focus-visible + .accountSelectorItem {
+		outline: 2px solid var(--MI_THEME-accent);
+		outline-offset: -4px;
+	}
+
+	&:checked:focus-visible + .accountSelectorItem {
+		outline-color: #fff;
+	}
+
+	&:checked + .accountSelectorItem {
+		background: var(--MI_THEME-accent);
+		color: #fff;
+	}
+}
+
+.accountSelectorItem {
+	display: flex;
+	align-items: center;
+	padding: 8px;
+	font-size: 14px;
+	-webkit-tap-highlight-color: transparent;
+	cursor: pointer;
+
+	&:hover {
+		background: var(--MI_THEME-buttonHoverBg);
+	}
+
+	&.static {
+		cursor: unset;
+
+		&:hover {
+			background: none;
+		}
+	}
+}
+
+.accountSelectorAddAccountRoot {
+	width: 100%;
+}
+
+.accountSelectorBody {
+	padding: 0 8px;
+	min-width: 0;
+}
+
+.accountSelectorAvatar {
+	width: 45px;
+	height: 45px;
+}
+
+.accountSelectorAddAccountAvatar {
+	background-color: var(--MI_THEME-accentedBg);
+	color: var(--MI_THEME-accent);
+	font-size: 16px;
+	line-height: 45px;
+	text-align: center;
+	border-radius: 50%;
+}
+
+.accountSelectorName {
+	display: block;
+	font-weight: bold;
+}
+
+.accountSelectorAcct {
+	opacity: 0.5;
+}
+</style>
diff --git a/packages/frontend/src/components/MkAutocomplete.vue b/packages/frontend/src/components/MkAutocomplete.vue
index 932c4ecb2e..0ea4566d4e 100644
--- a/packages/frontend/src/components/MkAutocomplete.vue
+++ b/packages/frontend/src/components/MkAutocomplete.vue
@@ -46,17 +46,17 @@ SPDX-License-Identifier: AGPL-3.0-only
 <script lang="ts">
 import { markRaw, ref, shallowRef, computed, onUpdated, onMounted, onBeforeUnmount, nextTick, watch } from 'vue';
 import sanitizeHtml from 'sanitize-html';
+import { emojilist, getEmojiName } from '@@/js/emojilist.js';
 import contains from '@/scripts/contains.js';
-import { char2twemojiFilePath, char2fluentEmojiFilePath } from '@/scripts/emoji-base.js';
+import { char2twemojiFilePath, char2fluentEmojiFilePath } from '@@/js/emoji-base.js';
 import { acct } from '@/filters/user.js';
 import * as os from '@/os.js';
 import { misskeyApi } from '@/scripts/misskey-api.js';
 import { defaultStore } from '@/store.js';
-import { emojilist, getEmojiName } from '@/scripts/emojilist.js';
 import { i18n } from '@/i18n.js';
 import { miLocalStorage } from '@/local-storage.js';
 import { customEmojis } from '@/custom-emojis.js';
-import { MFM_TAGS, MFM_PARAMS } from '@/const.js';
+import { MFM_TAGS, MFM_PARAMS } from '@@/js/const.js';
 import { searchEmoji, EmojiDef } from '@/scripts/search-emoji.js';
 
 const lib = emojilist.filter(x => x.category !== 'flags');
@@ -407,16 +407,16 @@ onBeforeUnmount(() => {
 	text-overflow: ellipsis;
 
 	&:hover {
-		background: var(--X3);
+		background: var(--MI_THEME-X3);
 	}
 
 	&[data-selected='true'] {
-		background: var(--accent);
+		background: var(--MI_THEME-accent);
 		color: #fff !important;
 	}
 
 	&:active {
-		background: var(--accentDarken);
+		background: var(--MI_THEME-accentDarken);
 		color: #fff !important;
 	}
 }
diff --git a/packages/frontend/src/components/MkButton.vue b/packages/frontend/src/components/MkButton.vue
index 3489255b91..311facb4aa 100644
--- a/packages/frontend/src/components/MkButton.vue
+++ b/packages/frontend/src/components/MkButton.vue
@@ -11,6 +11,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 	:type="type"
 	:name="name"
 	:value="value"
+	:disabled="disabled"
 	@click="emit('click', $event)"
 	@mousedown="onMousedown"
 >
@@ -55,6 +56,7 @@ const props = defineProps<{
 	asLike?: boolean;
 	name?: string;
 	value?: string;
+	disabled?: boolean;
 }>();
 
 const emit = defineEmits<{
@@ -127,7 +129,7 @@ function onMousedown(evt: MouseEvent): void {
 	font-size: 95%;
 	box-shadow: none;
 	text-decoration: none;
-	background: var(--buttonBg);
+	background: var(--MI_THEME-buttonBg);
 	border-radius: 5px;
 	overflow: clip;
 	box-sizing: border-box;
@@ -138,11 +140,11 @@ function onMousedown(evt: MouseEvent): void {
 	}
 
 	&:not(:disabled):hover {
-		background: var(--buttonHoverBg);
+		background: var(--MI_THEME-buttonHoverBg);
 	}
 
 	&:not(:disabled):active {
-		background: var(--buttonHoverBg);
+		background: var(--MI_THEME-buttonHoverBg);
 	}
 
 	&.small {
@@ -165,15 +167,15 @@ function onMousedown(evt: MouseEvent): void {
 
 	&.primary {
 		font-weight: bold;
-		color: var(--fgOnAccent) !important;
-		background: var(--accent);
+		color: var(--MI_THEME-fgOnAccent) !important;
+		background: var(--MI_THEME-accent);
 
 		&:not(:disabled):hover {
-			background: var(--X8);
+			background: hsl(from var(--MI_THEME-accent) h s calc(l + 5));
 		}
 
 		&:not(:disabled):active {
-			background: var(--X8);
+			background: hsl(from var(--MI_THEME-accent) h s calc(l + 5));
 		}
 	}
 
@@ -214,19 +216,20 @@ function onMousedown(evt: MouseEvent): void {
 
 	&.gradate {
 		font-weight: bold;
-		color: var(--fgOnAccent) !important;
-		background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB));
+		color: var(--MI_THEME-fgOnAccent) !important;
+		background: linear-gradient(90deg, var(--MI_THEME-buttonGradateA), var(--MI_THEME-buttonGradateB));
 
 		&:not(:disabled):hover {
-			background: linear-gradient(90deg, var(--X8), var(--X8));
+			background: linear-gradient(90deg, hsl(from var(--MI_THEME-accent) h s calc(l + 5)), hsl(from var(--MI_THEME-accent) h s calc(l + 5)));
 		}
 
 		&:not(:disabled):active {
-			background: linear-gradient(90deg, var(--X8), var(--X8));
+			background: linear-gradient(90deg, hsl(from var(--MI_THEME-accent) h s calc(l + 5)), hsl(from var(--MI_THEME-accent) h s calc(l + 5)));
 		}
 	}
 
 	&.danger {
+		font-weight: bold;
 		color: #ff2a2a;
 
 		&.primary {
@@ -244,11 +247,10 @@ function onMousedown(evt: MouseEvent): void {
 	}
 
 	&:disabled {
-		opacity: 0.7;
+		opacity: 0.5;
 	}
 
 	&:focus-visible {
-		outline: solid 2px var(--focus);
 		outline-offset: 2px;
 	}
 
diff --git a/packages/frontend/src/components/MkCaptcha.vue b/packages/frontend/src/components/MkCaptcha.vue
index c64bb47e77..264cf9af06 100644
--- a/packages/frontend/src/components/MkCaptcha.vue
+++ b/packages/frontend/src/components/MkCaptcha.vue
@@ -10,6 +10,17 @@ SPDX-License-Identifier: AGPL-3.0-only
 		<div id="mcaptcha__widget-container" class="m-captcha-style"></div>
 		<div ref="captchaEl"></div>
 	</div>
+	<div v-if="props.provider == 'testcaptcha'" style="background: #eee; border: solid 1px #888; padding: 8px; color: #000; max-width: 320px; display: flex; gap: 10px; align-items: center; box-shadow: 2px 2px 6px #0004; border-radius: 4px;">
+		<img src="/client-assets/testcaptcha.png" style="width: 60px; height: 60px; "/>
+		<div v-if="testcaptchaPassed">
+			<div style="color: green;">Test captcha passed!</div>
+		</div>
+		<div v-else>
+			<div style="font-size: 13px; margin-bottom: 4px;">Type "ai-chan-kawaii" to pass captcha</div>
+			<input v-model="testcaptchaInput" data-cy-testcaptcha-input/>
+			<button type="button" data-cy-testcaptcha-submit @click="testcaptchaSubmit">Submit</button>
+		</div>
+	</div>
 	<div v-else ref="captchaEl"></div>
 </div>
 </template>
@@ -29,7 +40,7 @@ export type Captcha = {
 	getResponse(id: string): string;
 };
 
-export type CaptchaProvider = 'hcaptcha' | 'recaptcha' | 'turnstile' | 'mcaptcha';
+export type CaptchaProvider = 'hcaptcha' | 'recaptcha' | 'turnstile' | 'mcaptcha' | 'testcaptcha';
 
 type CaptchaContainer = {
 	readonly [_ in CaptchaProvider]?: Captcha;
@@ -54,12 +65,16 @@ const available = ref(false);
 
 const captchaEl = shallowRef<HTMLDivElement | undefined>();
 
+const testcaptchaInput = ref('');
+const testcaptchaPassed = ref(false);
+
 const variable = computed(() => {
 	switch (props.provider) {
 		case 'hcaptcha': return 'hcaptcha';
 		case 'recaptcha': return 'grecaptcha';
 		case 'turnstile': return 'turnstile';
 		case 'mcaptcha': return 'mcaptcha';
+		case 'testcaptcha': return 'testcaptcha';
 	}
 });
 
@@ -71,6 +86,7 @@ const src = computed(() => {
 		case 'recaptcha': return 'https://www.recaptcha.net/recaptcha/api.js?render=explicit';
 		case 'turnstile': return 'https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit';
 		case 'mcaptcha': return null;
+		case 'testcaptcha': return null;
 	}
 });
 
@@ -78,7 +94,7 @@ const scriptId = computed(() => `script-${props.provider}`);
 
 const captcha = computed<Captcha>(() => window[variable.value] || {} as unknown as Captcha);
 
-if (loaded || props.provider === 'mcaptcha') {
+if (loaded || props.provider === 'mcaptcha' || props.provider === 'testcaptcha') {
 	available.value = true;
 } else if (src.value !== null) {
 	(document.getElementById(scriptId.value) ?? document.head.appendChild(Object.assign(document.createElement('script'), {
@@ -91,6 +107,8 @@ if (loaded || props.provider === 'mcaptcha') {
 
 function reset() {
 	if (captcha.value.reset) captcha.value.reset();
+	testcaptchaPassed.value = false;
+	testcaptchaInput.value = '';
 }
 
 async function requestRender() {
@@ -99,12 +117,11 @@ async function requestRender() {
 			sitekey: props.sitekey,
 			theme: defaultStore.state.darkMode ? 'dark' : 'light',
 			callback: callback,
-			'expired-callback': callback,
-			'error-callback': callback,
+			'expired-callback': () => callback(undefined),
+			'error-callback': () => callback(undefined),
 		});
 	} else if (props.provider === 'mcaptcha' && props.instanceUrl && props.sitekey) {
 		const { default: Widget } = await import('@mcaptcha/vanilla-glue');
-		// @ts-expect-error avoid typecheck error
 		new Widget({
 			siteKey: {
 				instanceUrl: new URL(props.instanceUrl),
@@ -128,6 +145,12 @@ function onReceivedMessage(message: MessageEvent) {
 	}
 }
 
+function testcaptchaSubmit() {
+	testcaptchaPassed.value = testcaptchaInput.value === 'ai-chan-kawaii';
+	callback(testcaptchaPassed.value ? 'testcaptcha-passed' : undefined);
+	if (!testcaptchaPassed.value) testcaptchaInput.value = '';
+}
+
 onMounted(() => {
 	if (available.value) {
 		window.addEventListener('message', onReceivedMessage);
diff --git a/packages/frontend/src/components/MkChannelFollowButton.stories.impl.ts b/packages/frontend/src/components/MkChannelFollowButton.stories.impl.ts
new file mode 100644
index 0000000000..b9770670dc
--- /dev/null
+++ b/packages/frontend/src/components/MkChannelFollowButton.stories.impl.ts
@@ -0,0 +1,71 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+/* eslint-disable @typescript-eslint/explicit-function-return-type */
+/* eslint-disable import/no-default-export */
+import { StoryObj } from '@storybook/vue3';
+import { HttpResponse, http } from 'msw';
+import { action } from '@storybook/addon-actions';
+import { expect, userEvent, within } from '@storybook/test';
+import { channel } from '../../.storybook/fakes.js';
+import { commonHandlers } from '../../.storybook/mocks.js';
+import MkChannelFollowButton from './MkChannelFollowButton.vue';
+import { i18n } from '@/i18n.js';
+
+function sleep(ms: number) {
+	return new Promise(resolve => setTimeout(resolve, ms));
+}
+
+export const Default = {
+	render(args) {
+		return {
+			components: {
+				MkChannelFollowButton,
+			},
+			setup() {
+				return {
+					args,
+				};
+			},
+			computed: {
+				props() {
+					return {
+						...this.args,
+					};
+				},
+			},
+			template: '<MkChannelFollowButton v-bind="props" />',
+		};
+	},
+	args: {
+		channel: channel(),
+		full: true,
+	},
+	async play({ canvasElement }) {
+		const canvas = within(canvasElement);
+		const buttonElement = canvas.getByRole<HTMLButtonElement>('button');
+		await expect(buttonElement).toHaveTextContent(i18n.ts.follow);
+		await userEvent.click(buttonElement);
+		await sleep(1000);
+		await expect(buttonElement).toHaveTextContent(i18n.ts.unfollow);
+		await userEvent.click(buttonElement);
+	},
+	parameters: {
+		layout: 'centered',
+		msw: {
+			handlers: [
+				...commonHandlers,
+				http.post('/api/channels/follow', async ({ request }) => {
+					action('POST /api/channels/follow')(await request.json());
+					return HttpResponse.json({});
+				}),
+				http.post('/api/channels/unfollow', async ({ request }) => {
+					action('POST /api/channels/unfollow')(await request.json());
+					return HttpResponse.json({});
+				}),
+			],
+		},
+	},
+} satisfies StoryObj<typeof MkChannelFollowButton>;
diff --git a/packages/frontend/src/components/MkChannelFollowButton.vue b/packages/frontend/src/components/MkChannelFollowButton.vue
index 6b1b380e41..d4e4f6179a 100644
--- a/packages/frontend/src/components/MkChannelFollowButton.vue
+++ b/packages/frontend/src/components/MkChannelFollowButton.vue
@@ -26,17 +26,18 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <script lang="ts" setup>
 import { ref } from 'vue';
+import * as Misskey from 'misskey-js';
 import { misskeyApi } from '@/scripts/misskey-api.js';
 import { i18n } from '@/i18n.js';
 
 const props = withDefaults(defineProps<{
-	channel: Record<string, any>;
+	channel: Misskey.entities.Channel;
 	full?: boolean;
 }>(), {
 	full: false,
 });
 
-const isFollowing = ref<boolean>(props.channel.isFollowing);
+const isFollowing = ref(props.channel.isFollowing);
 const wait = ref(false);
 
 async function onClick() {
@@ -67,9 +68,9 @@ async function onClick() {
 	position: relative;
 	display: inline-block;
 	font-weight: bold;
-	color: var(--accent);
+	color: var(--MI_THEME-accent);
 	background: transparent;
-	border: solid 1px var(--accent);
+	border: solid 1px var(--MI_THEME-accent);
 	padding: 0;
 	height: 31px;
 	font-size: 16px;
@@ -86,17 +87,7 @@ async function onClick() {
 	}
 
 	&:focus-visible {
-		&:after {
-			content: "";
-			pointer-events: none;
-			position: absolute;
-			top: -5px;
-			right: -5px;
-			bottom: -5px;
-			left: -5px;
-			border: 2px solid var(--focus);
-			border-radius: 32px;
-		}
+		outline-offset: 2px;
 	}
 
 	&:hover {
@@ -108,17 +99,17 @@ async function onClick() {
 	}
 
 	&.active {
-		color: var(--fgOnAccent);
-		background: var(--accent);
+		color: var(--MI_THEME-fgOnAccent);
+		background: var(--MI_THEME-accent);
 
 		&:hover {
-			background: var(--accentLighten);
-			border-color: var(--accentLighten);
+			background: var(--MI_THEME-accentLighten);
+			border-color: var(--MI_THEME-accentLighten);
 		}
 
 		&:active {
-			background: var(--accentDarken);
-			border-color: var(--accentDarken);
+			background: var(--MI_THEME-accentDarken);
+			border-color: var(--MI_THEME-accentDarken);
 		}
 	}
 
diff --git a/packages/frontend/src/components/MkChannelList.stories.impl.ts b/packages/frontend/src/components/MkChannelList.stories.impl.ts
new file mode 100644
index 0000000000..f69b20c049
--- /dev/null
+++ b/packages/frontend/src/components/MkChannelList.stories.impl.ts
@@ -0,0 +1,65 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+/* eslint-disable @typescript-eslint/explicit-function-return-type */
+/* eslint-disable import/no-default-export */
+import { StoryObj } from '@storybook/vue3';
+import { HttpResponse, http } from 'msw';
+import { action } from '@storybook/addon-actions';
+import { channel } from '../../.storybook/fakes.js';
+import { commonHandlers } from '../../.storybook/mocks.js';
+import MkChannelList from './MkChannelList.vue';
+export const Default = {
+	render(args) {
+		return {
+			components: {
+				MkChannelList,
+			},
+			setup() {
+				return {
+					args,
+				};
+			},
+			computed: {
+				props() {
+					return {
+						...this.args,
+					};
+				},
+			},
+			template: '<MkChannelList v-bind="props" />',
+		};
+	},
+	args: {
+		pagination: {
+			endpoint: 'channels/search',
+			limit: 10,
+		},
+	},
+	parameters: {
+		chromatic: {
+			// NOTE: ロードが終わるまで待つ
+			delay: 3000,
+		},
+		layout: 'fullscreen',
+		msw: {
+			handlers: [
+				...commonHandlers,
+				http.post('/api/channels/search', async ({ request, params }) => {
+					action('POST /api/channels/search')(await request.json());
+					return HttpResponse.json(params.untilId === 'lastchannel' ? [] : [
+						channel(),
+						channel('lastchannel', 'Last Channel', null),
+					]);
+				}),
+			],
+		},
+	},
+	decorators: [
+		() => ({
+			template: '<div style="display: flex; align-items: center; justify-content: center; height: 100vh"><div style="max-width: 700px; width: 100%; margin: 3rem"><story/></div></div>',
+		}),
+	],
+} satisfies StoryObj<typeof MkChannelList>;
diff --git a/packages/frontend/src/components/MkChannelPreview.stories.impl.ts b/packages/frontend/src/components/MkChannelPreview.stories.impl.ts
new file mode 100644
index 0000000000..de0193c78f
--- /dev/null
+++ b/packages/frontend/src/components/MkChannelPreview.stories.impl.ts
@@ -0,0 +1,43 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+/* eslint-disable @typescript-eslint/explicit-function-return-type */
+/* eslint-disable import/no-default-export */
+import { StoryObj } from '@storybook/vue3';
+import { channel } from '../../.storybook/fakes.js';
+import MkChannelPreview from './MkChannelPreview.vue';
+export const Default = {
+	render(args) {
+		return {
+			components: {
+				MkChannelPreview,
+			},
+			setup() {
+				return {
+					args,
+				};
+			},
+			computed: {
+				props() {
+					return {
+						...this.args,
+					};
+				},
+			},
+			template: '<MkChannelPreview v-bind="props" />',
+		};
+	},
+	args: {
+		channel: channel(),
+	},
+	parameters: {
+		layout: 'fullscreen',
+	},
+	decorators: [
+		() => ({
+			template: '<div style="display: flex; align-items: center; justify-content: center; height: 100vh"><div style="max-width: 700px; width: 100%; margin: 3rem"><story/></div></div>',
+		}),
+	],
+} satisfies StoryObj<typeof MkChannelPreview>;
diff --git a/packages/frontend/src/components/MkChannelPreview.vue b/packages/frontend/src/components/MkChannelPreview.vue
index 4ff64dc4ba..c470042b79 100644
--- a/packages/frontend/src/components/MkChannelPreview.vue
+++ b/packages/frontend/src/components/MkChannelPreview.vue
@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <template>
 <div style="position: relative;">
-	<MkA :to="`/channels/${channel.id}`" class="eftoefju _panel" tabindex="-1" @click="updateLastReadedAt">
+	<MkA :to="`/channels/${channel.id}`" class="eftoefju _panel" @click="updateLastReadedAt">
 		<div class="banner" :style="bannerStyle">
 			<div class="fade"></div>
 			<div class="name"><i class="ti ti-device-tv"></i> {{ channel.name }}</div>
@@ -47,11 +47,12 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <script lang="ts" setup>
 import { computed, ref, watch } from 'vue';
+import * as Misskey from 'misskey-js';
 import { i18n } from '@/i18n.js';
 import { miLocalStorage } from '@/local-storage.js';
 
 const props = defineProps<{
-	channel: Record<string, any>;
+	channel: Misskey.entities.Channel;
 }>();
 
 const getLastReadedAt = (): number | null => {
@@ -80,6 +81,7 @@ const bannerStyle = computed(() => {
 <style lang="scss" scoped>
 .eftoefju {
 	display: block;
+	position: relative;
 	overflow: hidden;
 	width: 100%;
 
@@ -87,6 +89,22 @@ const bannerStyle = computed(() => {
 		text-decoration: none;
 	}
 
+	&:focus-within {
+		outline: none;
+
+		&::after {
+			content: '';
+			position: absolute;
+			top: 0;
+			left: 0;
+			width: 100%;
+			height: 100%;
+			border-radius: inherit;
+			pointer-events: none;
+			box-shadow: inset 0 0 0 2px var(--MI_THEME-focus);
+		}
+	}
+
 	> .banner {
 		position: relative;
 		width: 100%;
@@ -100,7 +118,7 @@ const bannerStyle = computed(() => {
 			left: 0;
 			width: 100%;
 			height: 64px;
-			background: linear-gradient(0deg, var(--panel), var(--X15));
+			background: linear-gradient(0deg, var(--MI_THEME-panel), color(from var(--MI_THEME-panel) srgb r g b / 0));
 		}
 
 		> .name {
@@ -131,7 +149,7 @@ const bannerStyle = computed(() => {
 			bottom: 16px;
 			left: 16px;
 			background: rgba(0, 0, 0, 0.7);
-			color: var(--warn);
+			color: var(--MI_THEME-warn);
 			border-radius: 6px;
 			font-weight: bold;
 			font-size: 1em;
@@ -150,7 +168,7 @@ const bannerStyle = computed(() => {
 
 	> footer {
 		padding: 12px 16px;
-		border-top: solid 0.5px var(--divider);
+		border-top: solid 0.5px var(--MI_THEME-divider);
 
 		> span {
 			opacity: 0.7;
@@ -196,8 +214,8 @@ const bannerStyle = computed(() => {
 	top: 0;
 	right: 0;
 	transform: translate(25%, -25%);
-	background-color: var(--accent);
-	border: solid var(--bg) 4px;
+	background-color: var(--MI_THEME-accent);
+	border: solid var(--MI_THEME-bg) 4px;
 	border-radius: 100%;
 	width: 1.5rem;
 	height: 1.5rem;
diff --git a/packages/frontend/src/components/MkChart.stories.impl.ts b/packages/frontend/src/components/MkChart.stories.impl.ts
new file mode 100644
index 0000000000..1bcb9c30d8
--- /dev/null
+++ b/packages/frontend/src/components/MkChart.stories.impl.ts
@@ -0,0 +1,80 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+/* eslint-disable @typescript-eslint/explicit-function-return-type */
+/* eslint-disable import/no-default-export */
+import { StoryObj } from '@storybook/vue3';
+import { http } from 'msw';
+import { commonHandlers } from '../../.storybook/mocks.js';
+import { getChartResolver } from '../../.storybook/charts.js';
+import MkChart from './MkChart.vue';
+
+const Base = {
+	render(args) {
+		return {
+			components: {
+				MkChart,
+			},
+			setup() {
+				return {
+					args,
+				};
+			},
+			computed: {
+				props() {
+					return {
+						...this.args,
+					};
+				},
+			},
+			template: '<MkChart v-bind="props" />',
+		};
+	},
+	args: {
+		src: 'federation',
+		span: 'hour',
+		nowForChromatic: 1716263640000,
+	},
+	parameters: {
+		layout: 'centered',
+		msw: {
+			handlers: [
+				...commonHandlers,
+				http.get('/api/charts/federation', getChartResolver(
+					['deliveredInstances', 'inboxInstances', 'stalled', 'sub', 'pub', 'pubsub', 'subActive', 'pubActive'],
+				)),
+				http.get('/api/charts/notes', getChartResolver(
+					['local.total', 'remote.total'],
+					{ accumulate: true },
+				)),
+				http.get('/api/charts/drive', getChartResolver(
+					['local.incSize', 'local.decSize', 'remote.incSize', 'remote.decSize'],
+					{ mulMap: { 'local.incSize': 1e7, 'local.decSize': 5e6, 'remote.incSize': 1e6, 'remote.decSize': 5e5 } },
+				)),
+			],
+		},
+	},
+} satisfies StoryObj<typeof MkChart>;
+export const FederationChart = {
+	...Base,
+	args: {
+		...Base.args,
+		src: 'federation',
+	},
+} satisfies StoryObj<typeof MkChart>;
+export const NotesTotalChart = {
+	...Base,
+	args: {
+		...Base.args,
+		src: 'notes-total',
+	},
+} satisfies StoryObj<typeof MkChart>;
+export const DriveChart = {
+	...Base,
+	args: {
+		...Base.args,
+		src: 'drive',
+	},
+} satisfies StoryObj<typeof MkChart>;
diff --git a/packages/frontend/src/components/MkChart.vue b/packages/frontend/src/components/MkChart.vue
index 04b6d2f29c..d05f4921f6 100644
--- a/packages/frontend/src/components/MkChart.vue
+++ b/packages/frontend/src/components/MkChart.vue
@@ -13,14 +13,46 @@ SPDX-License-Identifier: AGPL-3.0-only
 </div>
 </template>
 
+<script lang="ts">
+export type ChartSrc =
+	| 'federation'
+	| 'ap-request'
+	| 'users'
+	| 'users-total'
+	| 'active-users'
+	| 'notes'
+	| 'local-notes'
+	| 'remote-notes'
+	| 'notes-total'
+	| 'drive'
+	| 'drive-files'
+	| 'instance-requests'
+	| 'instance-users'
+	| 'instance-users-total'
+	| 'instance-notes'
+	| 'instance-notes-total'
+	| 'instance-ff'
+	| 'instance-ff-total'
+	| 'instance-drive-usage'
+	| 'instance-drive-usage-total'
+	| 'instance-drive-files'
+	| 'instance-drive-files-total'
+	| 'per-user-notes'
+	| 'per-user-pv'
+	| 'per-user-following'
+	| 'per-user-followers'
+	| 'per-user-drive';
+</script>
+
 <script lang="ts" setup>
 /* eslint-disable id-denylist --
   Chart.js has a `data` attribute in most chart definitions, which triggers the
   id-denylist violation when setting it. This is causing about 60+ lint issues.
   As this is part of Chart.js's API it makes sense to disable the check here.
 */
-import { onMounted, ref, shallowRef, watch, PropType } from 'vue';
+import { onMounted, ref, shallowRef, watch } from 'vue';
 import { Chart } from 'chart.js';
+import * as Misskey from 'misskey-js';
 import { misskeyApiGet } from '@/scripts/misskey-api.js';
 import { defaultStore } from '@/store.js';
 import { useChartTooltip } from '@/scripts/use-chart-tooltip.js';
@@ -34,44 +66,34 @@ import MkChartLegend from '@/components/MkChartLegend.vue';
 
 initChart();
 
-const props = defineProps({
-	src: {
-		type: String,
-		required: true,
-	},
-	args: {
-		type: Object,
-		required: false,
-	},
-	limit: {
-		type: Number,
-		required: false,
-		default: 90,
-	},
-	span: {
-		type: String as PropType<'hour' | 'day'>,
-		required: true,
-	},
-	detailed: {
-		type: Boolean,
-		required: false,
-		default: false,
-	},
-	stacked: {
-		type: Boolean,
-		required: false,
-		default: false,
-	},
-	bar: {
-		type: Boolean,
-		required: false,
-		default: false,
-	},
-	aspectRatio: {
-		type: Number,
-		required: false,
-		default: null,
-	},
+const props = withDefaults(defineProps<{
+	src: ChartSrc;
+	args?: {
+		host?: string;
+		user?: Misskey.entities.UserLite;
+		withoutAll?: boolean;
+	};
+	limit?: number;
+	span: 'hour' | 'day';
+	detailed?: boolean;
+	stacked?: boolean;
+	bar?: boolean;
+	aspectRatio?: number | null;
+	nowForChromatic?: number;
+}>(), {
+	args: undefined,
+	limit: 90,
+	detailed: false,
+	stacked: false,
+	bar: false,
+	aspectRatio: null,
+
+	/**
+	 * @desc Overwrites current date to fix background lines of chart.
+	 * @ignore Only used for Chromatic. Don't use this for production.
+	 * @see https://github.com/misskey-dev/misskey/pull/13830#issuecomment-2155886151
+	 */
+	nowForChromatic: undefined,
 });
 
 const legendEl = shallowRef<InstanceType<typeof MkChartLegend>>();
@@ -94,7 +116,8 @@ const getColor = (i) => {
 	return colorSets[i % colorSets.length];
 };
 
-const now = new Date();
+// eslint-disable-next-line vue/no-setup-props-reactivity-loss
+const now = props.nowForChromatic != null ? new Date(props.nowForChromatic) : new Date();
 let chartInstance: Chart | null = null;
 let chartData: {
 	series: {
@@ -840,8 +863,8 @@ onMounted(() => {
 	left: 0;
 	width: 100%;
 	height: 100%;
-	-webkit-backdrop-filter: var(--blur, blur(12px));
-	backdrop-filter: var(--blur, blur(12px));
+	-webkit-backdrop-filter: var(--MI-blur, blur(12px));
+	backdrop-filter: var(--MI-blur, blur(12px));
 	display: flex;
 	justify-content: center;
 	align-items: center;
diff --git a/packages/frontend/src/components/MkChartLegend.stories.impl.ts b/packages/frontend/src/components/MkChartLegend.stories.impl.ts
new file mode 100644
index 0000000000..06146e20e4
--- /dev/null
+++ b/packages/frontend/src/components/MkChartLegend.stories.impl.ts
@@ -0,0 +1,7 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import MkChartLegend from './MkChartLegend.vue';
+void MkChartLegend;
diff --git a/packages/frontend/src/components/MkChartLegend.vue b/packages/frontend/src/components/MkChartLegend.vue
index 6eb2009784..574cde9da4 100644
--- a/packages/frontend/src/components/MkChartLegend.vue
+++ b/packages/frontend/src/components/MkChartLegend.vue
@@ -53,11 +53,11 @@ defineExpose({
 		> .item {
 			font-size: 85%;
 			padding: 4px 12px 4px 8px;
-			border: solid 1px var(--divider);
+			border: solid 1px var(--MI_THEME-divider);
 			border-radius: 999px;
 
 			&:hover {
-				border-color: var(--inputBorderHover);
+				border-color: var(--MI_THEME-inputBorderHover);
 			}
 
 			&.disabled {
diff --git a/packages/frontend/src/components/MkChartTooltip.stories.impl.ts b/packages/frontend/src/components/MkChartTooltip.stories.impl.ts
new file mode 100644
index 0000000000..289a9e9f27
--- /dev/null
+++ b/packages/frontend/src/components/MkChartTooltip.stories.impl.ts
@@ -0,0 +1,7 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import MkChartTooltip from './MkChartTooltip.vue';
+void MkChartTooltip;
diff --git a/packages/frontend/src/components/MkClickerGame.stories.impl.ts b/packages/frontend/src/components/MkClickerGame.stories.impl.ts
new file mode 100644
index 0000000000..36313f965d
--- /dev/null
+++ b/packages/frontend/src/components/MkClickerGame.stories.impl.ts
@@ -0,0 +1,77 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+/* eslint-disable @typescript-eslint/explicit-function-return-type */
+/* eslint-disable import/no-default-export */
+import { StoryObj } from '@storybook/vue3';
+import { HttpResponse, http } from 'msw';
+import { action } from '@storybook/addon-actions';
+import { expect, userEvent, within } from '@storybook/test';
+import { commonHandlers } from '../../.storybook/mocks.js';
+import MkClickerGame from './MkClickerGame.vue';
+
+function sleep(ms: number) {
+	return new Promise(resolve => setTimeout(resolve, ms));
+}
+
+export const Default = {
+	render(args) {
+		return {
+			components: {
+				MkClickerGame,
+			},
+			setup() {
+				return {
+					args,
+				};
+			},
+			computed: {
+				props() {
+					return {
+						...this.args,
+					};
+				},
+			},
+			template: '<MkClickerGame v-bind="props" />',
+		};
+	},
+	async play({ canvasElement }) {
+		await sleep(1000);
+		const canvas = within(canvasElement);
+		const count = canvas.getByTestId('count');
+		await expect(count).toHaveTextContent('0');
+		const buttonElement = canvas.getByRole<HTMLButtonElement>('button');
+		await userEvent.click(buttonElement);
+		await expect(count).toHaveTextContent('1');
+	},
+	parameters: {
+		layout: 'centered',
+		msw: {
+			handlers: [
+				...commonHandlers,
+				http.post('/api/i/registry/get', async ({ request }) => {
+					action('POST /api/i/registry/get')(await request.json());
+					return HttpResponse.json({
+						error: {
+							message: 'No such key.',
+							code: 'NO_SUCH_KEY',
+							id: 'ac3ed68a-62f0-422b-a7bc-d5e09e8f6a6a',
+						},
+					}, {
+						status: 400,
+					});
+				}),
+				http.post('/api/i/registry/set', async ({ request }) => {
+					action('POST /api/i/registry/set')(await request.json());
+					return HttpResponse.json(undefined, { status: 204 });
+				}),
+				http.post('/api/i/claim-achievement', async ({ request }) => {
+					action('POST /api/i/claim-achievement')(await request.json());
+					return HttpResponse.json(undefined, { status: 204 });
+				}),
+			],
+		},
+	},
+} satisfies StoryObj<typeof MkClickerGame>;
diff --git a/packages/frontend/src/components/MkClickerGame.vue b/packages/frontend/src/components/MkClickerGame.vue
index 23046bf345..9a0a9fba05 100644
--- a/packages/frontend/src/components/MkClickerGame.vue
+++ b/packages/frontend/src/components/MkClickerGame.vue
@@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 <div>
 	<div v-if="game.ready" :class="$style.game">
 		<div :class="$style.cps" class="">{{ number(cps) }}cps</div>
-		<div :class="$style.count" class=""><i class="ti ti-cookie" style="font-size: 70%;"></i> {{ number(cookies) }}</div>
+		<div :class="$style.count" class="" data-testid="count"><i class="ti ti-cookie" style="font-size: 70%;"></i> {{ number(cookies) }}</div>
 		<button v-click-anime class="_button" @click="onClick">
 			<img src="/client-assets/cookie.png" :class="$style.img">
 		</button>
@@ -22,7 +22,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { computed, onMounted, onUnmounted, ref } from 'vue';
 import MkPlusOneEffect from '@/components/MkPlusOneEffect.vue';
 import * as os from '@/os.js';
-import { useInterval } from '@/scripts/use-interval.js';
+import { useInterval } from '@@/js/use-interval.js';
 import * as game from '@/scripts/clicker-game.js';
 import number from '@/filters/number.js';
 import { claimAchievement } from '@/scripts/achievements.js';
@@ -35,7 +35,9 @@ const prevCookies = ref(0);
 function onClick(ev: MouseEvent) {
 	const x = ev.clientX;
 	const y = ev.clientY;
-	os.popup(MkPlusOneEffect, { x, y }, {}, 'end');
+	const { dispose } = os.popup(MkPlusOneEffect, { x, y }, {
+		end: () => dispose(),
+	});
 
 	saveData.value!.cookies++;
 	saveData.value!.totalCookies++;
diff --git a/packages/frontend/src/components/MkClipPreview.stories.impl.ts b/packages/frontend/src/components/MkClipPreview.stories.impl.ts
new file mode 100644
index 0000000000..62503fb98a
--- /dev/null
+++ b/packages/frontend/src/components/MkClipPreview.stories.impl.ts
@@ -0,0 +1,44 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+/* eslint-disable @typescript-eslint/explicit-function-return-type */
+/* eslint-disable import/no-default-export */
+import { StoryObj } from '@storybook/vue3';
+import { clip } from '../../.storybook/fakes.js';
+import MkClipPreview from './MkClipPreview.vue';
+export const Default = {
+	render(args) {
+		return {
+			components: {
+				MkClipPreview,
+			},
+			setup() {
+				return {
+					args,
+				};
+			},
+			computed: {
+				props() {
+					return {
+						...this.args,
+					};
+				},
+			},
+			template: '<MkClipPreview v-bind="props" />',
+		};
+	},
+	args: {
+		clip: clip(),
+		noUserInfo: false,
+	},
+	parameters: {
+		layout: 'fullscreen',
+	},
+	decorators: [
+		() => ({
+			template: '<div style="display: flex; align-items: center; justify-content: center; height: 100vh"><div style="max-width: 700px; width: 100%; margin: 3rem"><story/></div></div>',
+		}),
+	],
+} satisfies StoryObj<typeof MkClipPreview>;
diff --git a/packages/frontend/src/components/MkClipPreview.vue b/packages/frontend/src/components/MkClipPreview.vue
index 6299a28e9f..5b09ec90dd 100644
--- a/packages/frontend/src/components/MkClipPreview.vue
+++ b/packages/frontend/src/components/MkClipPreview.vue
@@ -12,10 +12,12 @@ SPDX-License-Identifier: AGPL-3.0-only
 			<div v-if="clip.lastClippedAt">{{ i18n.ts.updatedAt }}: <MkTime :time="clip.lastClippedAt" mode="detail"/></div>
 			<div v-if="clip.notesCount != null">{{ i18n.ts.notesCount }}: {{ number(clip.notesCount) }} / {{ $i?.policies.noteEachClipsLimit }} ({{ i18n.tsx.remainingN({ n: remaining }) }})</div>
 		</div>
-		<div :class="$style.divider"></div>
-		<div>
-			<MkAvatar :user="clip.user" :class="$style.userAvatar" indicator link preview/> <MkUserName :user="clip.user" :nowrap="false"/>
-		</div>
+		<template v-if="!props.noUserInfo">
+			<div :class="$style.divider"></div>
+			<div>
+				<MkAvatar :user="clip.user" :class="$style.userAvatar" indicator link preview/> <MkUserName :user="clip.user" :nowrap="false"/>
+			</div>
+		</template>
 	</div>
 </MkA>
 </template>
@@ -27,9 +29,12 @@ import { i18n } from '@/i18n.js';
 import { $i } from '@/account.js';
 import number from '@/filters/number.js';
 
-const props = defineProps<{
+const props = withDefaults(defineProps<{
 	clip: Misskey.entities.Clip;
-}>();
+	noUserInfo?: boolean;
+}>(), {
+	noUserInfo: false,
+});
 
 const remaining = computed(() => {
 	return ($i?.policies && props.clip.notesCount != null) ? ($i.policies.noteEachClipsLimit - props.clip.notesCount) : i18n.ts.unknown;
@@ -40,9 +45,17 @@ const remaining = computed(() => {
 .link {
 	display: block;
 
+	&:focus-visible {
+		outline: none;
+
+		.root {
+			box-shadow: inset 0 0 0 2px var(--MI_THEME-focus);
+		}
+	}
+
 	&:hover {
 		text-decoration: none;
-		color: var(--accent);
+		color: var(--MI_THEME-accent);
 	}
 }
 
@@ -52,7 +65,7 @@ const remaining = computed(() => {
 
 .divider {
 	height: 1px;
-	background: var(--divider);
+	background: var(--MI_THEME-divider);
 }
 
 .description {
diff --git a/packages/frontend/src/components/MkCode.core.stories.impl.ts b/packages/frontend/src/components/MkCode.core.stories.impl.ts
new file mode 100644
index 0000000000..91990fffd5
--- /dev/null
+++ b/packages/frontend/src/components/MkCode.core.stories.impl.ts
@@ -0,0 +1,7 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import MkCode_core from './MkCode.core.vue';
+void MkCode_core;
diff --git a/packages/frontend/src/components/MkCode.core.vue b/packages/frontend/src/components/MkCode.core.vue
index c0e7df5dac..0d7a67eaec 100644
--- a/packages/frontend/src/components/MkCode.core.vue
+++ b/packages/frontend/src/components/MkCode.core.vue
@@ -77,7 +77,7 @@ watch(() => props.lang, (to) => {
 	margin: .5em 0;
 	overflow: auto;
 	border-radius: 8px;
-	border: 1px solid var(--divider);
+	border: 1px solid var(--MI_THEME-divider);
 	font-family: Consolas, Monaco, Andale Mono, Ubuntu Mono, monospace;
 
 	color: var(--shiki-fallback);
diff --git a/packages/frontend/src/components/MkCode.stories.impl.ts b/packages/frontend/src/components/MkCode.stories.impl.ts
new file mode 100644
index 0000000000..b7e53e8e35
--- /dev/null
+++ b/packages/frontend/src/components/MkCode.stories.impl.ts
@@ -0,0 +1,44 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+/* eslint-disable @typescript-eslint/explicit-function-return-type */
+/* eslint-disable import/no-default-export */
+import { StoryObj } from '@storybook/vue3';
+import MkCode from './MkCode.vue';
+const code = `for (let i, 100) {
+	<: if (i % 15 == 0) "FizzBuzz"
+		elif (i % 3 == 0) "Fizz"
+		elif (i % 5 == 0) "Buzz"
+		else i
+}`;
+export const Default = {
+	render(args) {
+		return {
+			components: {
+				MkCode,
+			},
+			setup() {
+				return {
+					args,
+				};
+			},
+			computed: {
+				props() {
+					return {
+						...this.args,
+					};
+				},
+			},
+			template: '<MkCode v-bind="props" />',
+		};
+	},
+	args: {
+		code,
+		lang: 'is',
+	},
+	parameters: {
+		layout: 'centered',
+	},
+} satisfies StoryObj<typeof MkCode>;
diff --git a/packages/frontend/src/components/MkCode.vue b/packages/frontend/src/components/MkCode.vue
index a3c80e743b..cb82bfd98b 100644
--- a/packages/frontend/src/components/MkCode.vue
+++ b/packages/frontend/src/components/MkCode.vue
@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <template>
 <div :class="$style.codeBlockRoot">
-	<button :class="$style.codeBlockCopyButton" class="_button" @click="copy">
+	<button v-if="copyButton" :class="$style.codeBlockCopyButton" class="_button" @click="copy">
 		<i class="ti ti-copy"></i>
 	</button>
 	<Suspense>
@@ -30,14 +30,19 @@ import * as os from '@/os.js';
 import MkLoading from '@/components/global/MkLoading.vue';
 import { defaultStore } from '@/store.js';
 import { i18n } from '@/i18n.js';
-import copyToClipboard from '@/scripts/copy-to-clipboard.js';
+import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
 
-const props = defineProps<{
+const props = withDefaults(defineProps<{
 	code: string;
+	forceShow?: boolean;
+	copyButton?: boolean;
 	lang?: string;
-}>();
+}>(), {
+	copyButton: true,
+	forceShow: false,
+});
 
-const show = ref(!defaultStore.state.dataSaver.code);
+const show = ref(props.forceShow === true ? true : !defaultStore.state.dataSaver.code);
 
 const XCode = defineAsyncComponent(() => import('@/components/MkCode.core.vue'));
 
@@ -66,7 +71,7 @@ function copy() {
 .codeBlockFallbackRoot {
 	display: block;
 	overflow-wrap: anywhere;
-	background: var(--bg);
+	background: var(--MI_THEME-bg);
 	padding: 1em;
 	margin: .5em 0;
 	overflow: auto;
@@ -89,8 +94,8 @@ function copy() {
 	border-radius: 8px;
 	padding: 24px;
 	margin-top: 4px;
-	color: var(--fg);
-	background: var(--bg);
+	color: var(--MI_THEME-fg);
+	background: var(--MI_THEME-bg);
 }
 
 .codePlaceholderContainer {
diff --git a/packages/frontend/src/components/MkCodeEditor.stories.impl.ts b/packages/frontend/src/components/MkCodeEditor.stories.impl.ts
new file mode 100644
index 0000000000..5c410c4886
--- /dev/null
+++ b/packages/frontend/src/components/MkCodeEditor.stories.impl.ts
@@ -0,0 +1,62 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+/* eslint-disable @typescript-eslint/explicit-function-return-type */
+/* eslint-disable import/no-default-export */
+import { StoryObj } from '@storybook/vue3';
+import { action } from '@storybook/addon-actions';
+import MkCodeEditor from './MkCodeEditor.vue';
+const code = `for (let i, 100) {
+	<: if (i % 15 == 0) "FizzBuzz"
+		elif (i % 3 == 0) "Fizz"
+		elif (i % 5 == 0) "Buzz"
+		else i
+}`;
+export const Default = {
+	render(args) {
+		return {
+			components: {
+				MkCodeEditor,
+			},
+			data() {
+				return {
+					code,
+				};
+			},
+			setup() {
+				return {
+					args,
+				};
+			},
+			computed: {
+				props() {
+					return {
+						...this.args,
+					};
+				},
+				events() {
+					return {
+						'change': action('change'),
+						'keydown': action('keydown'),
+						'enter': action('enter'),
+						'update:modelValue': action('update:modelValue'),
+					};
+				},
+			},
+			template: '<MkCodeEditor v-model="code" v-bind="props" v-on="events" />',
+		};
+	},
+	args: {
+		lang: 'aiscript',
+	},
+	parameters: {
+		layout: 'fullscreen',
+	},
+	decorators: [
+		() => ({
+			template: '<div style="display: flex; align-items: center; justify-content: center; height: 100vh"><div style="max-width: 800px; width: 100%; margin: 3rem"><Suspense><story/></Suspense></div></div>',
+		}),
+	],
+} satisfies StoryObj<typeof MkCodeEditor>;
diff --git a/packages/frontend/src/components/MkCodeEditor.vue b/packages/frontend/src/components/MkCodeEditor.vue
index afd9132a12..5bf2301e72 100644
--- a/packages/frontend/src/components/MkCodeEditor.vue
+++ b/packages/frontend/src/components/MkCodeEditor.vue
@@ -140,7 +140,7 @@ watch(v, newValue => {
 .caption {
 	font-size: 0.85em;
 	padding: 8px 0 0 0;
-	color: var(--fgTransparentWeak);
+	color: var(--MI_THEME-fgTransparentWeak);
 
 	&:empty {
 		display: none;
@@ -160,17 +160,17 @@ watch(v, newValue => {
 	margin: 0;
 	border-radius: 6px;
 	padding: 0;
-	color: var(--fg);
-	border: solid 1px var(--panel);
+	color: var(--MI_THEME-fg);
+	border: solid 1px var(--MI_THEME-panel);
 	transition: border-color 0.1s ease-out;
 	font-family: Consolas, Monaco, Andale Mono, Ubuntu Mono, monospace;
 	&:hover {
-		border-color: var(--inputBorderHover) !important;
+		border-color: var(--MI_THEME-inputBorderHover) !important;
 	}
 }
 
 .focused.codeEditorRoot {
-	border-color: var(--accent) !important;
+	border-color: var(--MI_THEME-accent) !important;
 	border-radius: 6px;
 }
 
@@ -196,7 +196,7 @@ watch(v, newValue => {
 	resize: none;
 	text-align: left;
 	color: transparent;
-	caret-color: var(--fg);
+	caret-color: var(--MI_THEME-fg);
 	background-color: transparent;
 	border: 0;
 	border-radius: 6px;
@@ -211,6 +211,6 @@ watch(v, newValue => {
 }
 
 .textarea::selection {
-	color: var(--bg);
+	color: var(--MI_THEME-bg);
 }
 </style>
diff --git a/packages/frontend/src/components/MkCodeInline.stories.impl.ts b/packages/frontend/src/components/MkCodeInline.stories.impl.ts
new file mode 100644
index 0000000000..51d4d106ff
--- /dev/null
+++ b/packages/frontend/src/components/MkCodeInline.stories.impl.ts
@@ -0,0 +1,37 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+/* eslint-disable @typescript-eslint/explicit-function-return-type */
+/* eslint-disable import/no-default-export */
+import { StoryObj } from '@storybook/vue3';
+import MkCodeInline from './MkCodeInline.vue';
+export const Default = {
+	render(args) {
+		return {
+			components: {
+				MkCodeInline,
+			},
+			setup() {
+				return {
+					args,
+				};
+			},
+			computed: {
+				props() {
+					return {
+						...this.args,
+					};
+				},
+			},
+			template: '<MkCodeInline v-bind="props"/>',
+		};
+	},
+	args: {
+		code: '<: "Hello, world!"',
+	},
+	parameters: {
+		layout: 'centered',
+	},
+} satisfies StoryObj<typeof MkCodeInline>;
diff --git a/packages/frontend/src/components/MkCodeInline.vue b/packages/frontend/src/components/MkCodeInline.vue
index 6add80d1bc..04b6e54108 100644
--- a/packages/frontend/src/components/MkCodeInline.vue
+++ b/packages/frontend/src/components/MkCodeInline.vue
@@ -18,7 +18,7 @@ const props = defineProps<{
 	display: inline-block;
 	font-family: Consolas, Monaco, Andale Mono, Ubuntu Mono, monospace;
 	overflow-wrap: anywhere;
-	background: var(--bg);
+	background: var(--MI_THEME-bg);
 	padding: .1em;
 	border-radius: .3em;
 }
diff --git a/packages/frontend/src/components/MkColorInput.stories.impl.ts b/packages/frontend/src/components/MkColorInput.stories.impl.ts
new file mode 100644
index 0000000000..61383e2cae
--- /dev/null
+++ b/packages/frontend/src/components/MkColorInput.stories.impl.ts
@@ -0,0 +1,50 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+/* eslint-disable @typescript-eslint/explicit-function-return-type */
+/* eslint-disable import/no-default-export */
+import { StoryObj } from '@storybook/vue3';
+import { action } from '@storybook/addon-actions';
+import MkColorInput from './MkColorInput.vue';
+export const Default = {
+	render(args) {
+		return {
+			components: {
+				MkColorInput,
+			},
+			data() {
+				return {
+					color: '#cccccc',
+				};
+			},
+			setup() {
+				return {
+					args,
+				};
+			},
+			computed: {
+				props() {
+					return {
+						...this.args,
+					};
+				},
+				events() {
+					return {
+						'update:modelValue': action('update:modelValue'),
+					};
+				},
+			},
+			template: '<MkColorInput v-model="color" v-bind="props" v-on="events" />',
+		};
+	},
+	parameters: {
+		layout: 'fullscreen',
+	},
+	decorators: [
+		() => ({
+			template: '<div style="display: flex; align-items: center; justify-content: center; height: 100vh"><div style="max-width: 800px; width: 100%; margin: 3rem"><story/></div></div>',
+		}),
+	],
+} satisfies StoryObj<typeof MkColorInput>;
diff --git a/packages/frontend/src/components/MkColorInput.vue b/packages/frontend/src/components/MkColorInput.vue
index f5c580789b..55a32664de 100644
--- a/packages/frontend/src/components/MkColorInput.vue
+++ b/packages/frontend/src/components/MkColorInput.vue
@@ -60,7 +60,7 @@ const onInput = () => {
 .caption {
 	font-size: 0.85em;
 	padding: 8px 0 0 0;
-	color: var(--fgTransparentWeak);
+	color: var(--MI_THEME-fgTransparentWeak);
 
 	&:empty {
 		display: none;
@@ -72,8 +72,8 @@ const onInput = () => {
 
 	&.focused {
 		> .inputCore {
-			border-color: var(--accent) !important;
-			//box-shadow: 0 0 0 4px var(--focus);
+			border-color: var(--MI_THEME-accent) !important;
+			//box-shadow: 0 0 0 4px var(--MI_THEME-focus);
 		}
 	}
 
@@ -98,9 +98,9 @@ const onInput = () => {
 	font: inherit;
 	font-weight: normal;
 	font-size: 1em;
-	color: var(--fg);
-	background: var(--panel);
-	border: solid 1px var(--panel);
+	color: var(--MI_THEME-fg);
+	background: var(--MI_THEME-panel);
+	border: solid 1px var(--MI_THEME-panel);
 	border-radius: 6px;
 	outline: none;
 	box-shadow: none;
@@ -108,7 +108,7 @@ const onInput = () => {
 	transition: border-color 0.1s ease-out;
 
 	&:hover {
-		border-color: var(--inputBorderHover) !important;
+		border-color: var(--MI_THEME-inputBorderHover) !important;
 	}
 }
 </style>
diff --git a/packages/frontend/src/components/MkContainer.stories.impl.ts b/packages/frontend/src/components/MkContainer.stories.impl.ts
new file mode 100644
index 0000000000..72a7659521
--- /dev/null
+++ b/packages/frontend/src/components/MkContainer.stories.impl.ts
@@ -0,0 +1,7 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import MkContainer from './MkContainer.vue';
+void MkContainer;
diff --git a/packages/frontend/src/components/MkContainer.vue b/packages/frontend/src/components/MkContainer.vue
index a399acd47f..f513795c56 100644
--- a/packages/frontend/src/components/MkContainer.vue
+++ b/packages/frontend/src/components/MkContainer.vue
@@ -64,26 +64,30 @@ const showBody = ref(props.expanded);
 const ignoreOmit = ref(false);
 const omitted = ref(false);
 
-function enter(el) {
+function enter(el: Element) {
+	if (!(el instanceof HTMLElement)) return;
 	const elementHeight = el.getBoundingClientRect().height;
-	el.style.height = 0;
+	el.style.height = '0';
 	el.offsetHeight; // reflow
-	el.style.height = Math.min(elementHeight, props.maxHeight ?? Infinity) + 'px';
+	el.style.height = `${Math.min(elementHeight, props.maxHeight ?? Infinity)}px`;
 }
 
-function afterEnter(el) {
-	el.style.height = null;
+function afterEnter(el: Element) {
+	if (!(el instanceof HTMLElement)) return;
+	el.style.height = '';
 }
 
-function leave(el) {
+function leave(el: Element) {
+	if (!(el instanceof HTMLElement)) return;
 	const elementHeight = el.getBoundingClientRect().height;
-	el.style.height = elementHeight + 'px';
+	el.style.height = `${elementHeight}px`;
 	el.offsetHeight; // reflow
-	el.style.height = 0;
+	el.style.height = '0';
 }
 
-function afterLeave(el) {
-	el.style.height = null;
+function afterLeave(el: Element) {
+	if (!(el instanceof HTMLElement)) return;
+	el.style.height = '';
 }
 
 const calcOmit = () => {
@@ -165,11 +169,11 @@ onUnmounted(() => {
 
 .header {
 	position: sticky;
-	top: var(--stickyTop, 0px);
+	top: var(--MI-stickyTop, 0px);
 	left: 0;
-	color: var(--panelHeaderFg);
-	background: var(--panelHeaderBg);
-	border-bottom: solid 0.5px var(--panelHeaderDivider);
+	color: var(--MI_THEME-panelHeaderFg);
+	background: var(--MI_THEME-panelHeaderBg);
+	border-bottom: solid 0.5px var(--MI_THEME-panelHeaderDivider);
 	z-index: 2;
 	line-height: 1.4em;
 }
@@ -201,7 +205,7 @@ onUnmounted(() => {
 }
 
 .content {
-	--stickyTop: 0px;
+	--MI-stickyTop: 0px;
 
 	&.omitted {
 		position: relative;
@@ -216,11 +220,11 @@ onUnmounted(() => {
 			left: 0;
 			width: 100%;
 			height: 64px;
-			background: linear-gradient(0deg, var(--panel), var(--X15));
+			background: linear-gradient(0deg, var(--MI_THEME-panel), color(from var(--MI_THEME-panel) srgb r g b / 0));
 
 			> .fadeLabel {
 				display: inline-block;
-				background: var(--panel);
+				background: var(--MI_THEME-panel);
 				padding: 6px 10px;
 				font-size: 0.8em;
 				border-radius: 999px;
@@ -229,7 +233,7 @@ onUnmounted(() => {
 
 			&:hover {
 				> .fadeLabel {
-					background: var(--panelHighlight);
+					background: var(--MI_THEME-panelHighlight);
 				}
 			}
 		}
diff --git a/packages/frontend/src/components/MkContextMenu.stories.impl.ts b/packages/frontend/src/components/MkContextMenu.stories.impl.ts
new file mode 100644
index 0000000000..1ff0f51bd4
--- /dev/null
+++ b/packages/frontend/src/components/MkContextMenu.stories.impl.ts
@@ -0,0 +1,58 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+/* eslint-disable @typescript-eslint/explicit-function-return-type */
+/* eslint-disable import/no-default-export */
+import { StoryObj } from '@storybook/vue3';
+import { userEvent, within } from '@storybook/test';
+import MkContextMenu from './MkContextMenu.vue';
+import * as os from '@/os.js';
+export const Empty = {
+	render(args) {
+		return {
+			setup() {
+				return {
+					args,
+				};
+			},
+			computed: {
+				props() {
+					return {
+						...this.args,
+					};
+				},
+			},
+			methods: {
+				onContextmenu(ev: MouseEvent) {
+					os.contextMenu(args.items, ev);
+				},
+			},
+			template: '<div @contextmenu.stop="onContextmenu">Right Click Here</div>',
+		};
+	},
+	args: {
+		items: [],
+	},
+	async play({ canvasElement }) {
+		const canvas = within(canvasElement);
+		const target = canvas.getByText('Right Click Here');
+		await userEvent.pointer({ keys: '[MouseRight>]', target });
+	},
+	parameters: {
+		layout: 'centered',
+	},
+} satisfies StoryObj<typeof MkContextMenu>;
+export const SomeTabs = {
+	...Empty,
+	args: {
+		items: [
+			{
+				text: 'Home',
+				icon: 'ti ti-home',
+				action() {},
+			},
+		],
+	},
+} satisfies StoryObj<typeof MkContextMenu>;
diff --git a/packages/frontend/src/components/MkContextMenu.vue b/packages/frontend/src/components/MkContextMenu.vue
index a807742bb9..f51fefa0c0 100644
--- a/packages/frontend/src/components/MkContextMenu.vue
+++ b/packages/frontend/src/components/MkContextMenu.vue
@@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 	:leaveToClass="defaultStore.state.animation ? $style.transition_fade_leaveTo : ''"
 >
 	<div ref="rootEl" :class="$style.root" :style="{ zIndex }" @contextmenu.prevent.stop="() => {}">
-		<MkMenu :items="items" :align="'left'" @close="$emit('closed')"/>
+		<MkMenu :items="items" :align="'left'" @close="emit('closed')"/>
 	</div>
 </Transition>
 </template>
@@ -20,7 +20,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 <script lang="ts" setup>
 import { onMounted, onBeforeUnmount, shallowRef, ref } from 'vue';
 import MkMenu from './MkMenu.vue';
-import { MenuItem } from '@/types/menu.js';
+import type { MenuItem } from '@/types/menu.js';
 import contains from '@/scripts/contains.js';
 import { defaultStore } from '@/store.js';
 import * as os from '@/os.js';
diff --git a/packages/frontend/src/components/MkCropperDialog.stories.impl.ts b/packages/frontend/src/components/MkCropperDialog.stories.impl.ts
new file mode 100644
index 0000000000..ce13093975
--- /dev/null
+++ b/packages/frontend/src/components/MkCropperDialog.stories.impl.ts
@@ -0,0 +1,75 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+/* eslint-disable @typescript-eslint/explicit-function-return-type */
+/* eslint-disable import/no-default-export */
+import { StoryObj } from '@storybook/vue3';
+import { HttpResponse, http } from 'msw';
+import { action } from '@storybook/addon-actions';
+import { file } from '../../.storybook/fakes.js';
+import { commonHandlers } from '../../.storybook/mocks.js';
+import MkCropperDialog from './MkCropperDialog.vue';
+export const Default = {
+	render(args) {
+		return {
+			components: {
+				MkCropperDialog,
+			},
+			setup() {
+				return {
+					args,
+				};
+			},
+			computed: {
+				props() {
+					return {
+						...this.args,
+					};
+				},
+				events() {
+					return {
+						'ok': action('ok'),
+						'cancel': action('cancel'),
+						'closed': action('closed'),
+					};
+				},
+			},
+			template: '<MkCropperDialog v-bind="props" v-on="events" />',
+		};
+	},
+	args: {
+		file: file(),
+		aspectRatio: NaN,
+	},
+	parameters: {
+		chromatic: {
+			// NOTE: ロードが終わるまで待つ
+			delay: 3000,
+		},
+		layout: 'centered',
+		msw: {
+			handlers: [
+				...commonHandlers,
+				http.get('/proxy/image.webp', async ({ request }) => {
+					const url = new URL(request.url).searchParams.get('url');
+					if (url === 'https://github.com/misskey-dev/misskey/blob/master/packages/frontend/assets/fedi.jpg?raw=true') {
+						const image = await (await fetch('client-assets/fedi.jpg')).blob();
+						return new HttpResponse(image, {
+							headers: {
+								'Content-Type': 'image/jpeg',
+							},
+						});
+					} else {
+						return new HttpResponse(null, { status: 404 });
+					}
+				}),
+				http.post('/api/drive/files/create', async ({ request }) => {
+					action('POST /api/drive/files/create')(await request.formData());
+					return HttpResponse.json(file());
+				}),
+			],
+		},
+	},
+} satisfies StoryObj<typeof MkCropperDialog>;
diff --git a/packages/frontend/src/components/MkCropperDialog.vue b/packages/frontend/src/components/MkCropperDialog.vue
index 54f6f39c9d..0186cfc2c0 100644
--- a/packages/frontend/src/components/MkCropperDialog.vue
+++ b/packages/frontend/src/components/MkCropperDialog.vue
@@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 	:withOkButton="true"
 	@close="cancel()"
 	@ok="ok()"
-	@closed="$emit('closed')"
+	@closed="emit('closed')"
 >
 	<template #header>{{ i18n.ts.cropImage }}</template>
 	<template #default="{ width, height }">
@@ -39,7 +39,7 @@ import MkModalWindow from '@/components/MkModalWindow.vue';
 import * as os from '@/os.js';
 import { $i } from '@/account.js';
 import { defaultStore } from '@/store.js';
-import { apiUrl } from '@/config.js';
+import { apiUrl } from '@@/js/config.js';
 import { i18n } from '@/i18n.js';
 import { getProxiedImageUrl } from '@/scripts/media-proxy.js';
 
@@ -125,7 +125,7 @@ onMounted(() => {
 	const computedStyle = getComputedStyle(document.documentElement);
 
 	const selection = cropper.getCropperSelection()!;
-	selection.themeColor = tinycolor(computedStyle.getPropertyValue('--accent')).toHexString();
+	selection.themeColor = tinycolor(computedStyle.getPropertyValue('--MI_THEME-accent')).toHexString();
 	selection.aspectRatio = props.aspectRatio;
 	selection.initialAspectRatio = props.aspectRatio;
 	selection.outlined = true;
@@ -170,8 +170,8 @@ onMounted(() => {
 		display: flex;
 		align-items: center;
 		justify-content: center;
-		-webkit-backdrop-filter: var(--blur, blur(10px));
-		backdrop-filter: var(--blur, blur(10px));
+		-webkit-backdrop-filter: var(--MI-blur, blur(10px));
+		backdrop-filter: var(--MI-blur, blur(10px));
 		background: rgba(0, 0, 0, 0.5);
 	}
 
diff --git a/packages/frontend/src/components/MkCustomEmojiDetailedDialog.stories.impl.ts b/packages/frontend/src/components/MkCustomEmojiDetailedDialog.stories.impl.ts
new file mode 100644
index 0000000000..8a05e06311
--- /dev/null
+++ b/packages/frontend/src/components/MkCustomEmojiDetailedDialog.stories.impl.ts
@@ -0,0 +1,38 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+/* eslint-disable @typescript-eslint/explicit-function-return-type */
+/* eslint-disable import/no-default-export */
+import { StoryObj } from '@storybook/vue3';
+import { emojiDetailed } from '../../.storybook/fakes.js';
+import MkCustomEmojiDetailedDialog from './MkCustomEmojiDetailedDialog.vue';
+export const Default = {
+	render(args) {
+		return {
+			components: {
+				MkCustomEmojiDetailedDialog,
+			},
+			setup() {
+				return {
+					args,
+				};
+			},
+			computed: {
+				props() {
+					return {
+						...this.args,
+					};
+				},
+			},
+			template: '<MkCustomEmojiDetailedDialog v-bind="props" />',
+		};
+	},
+	args: {
+		emoji: emojiDetailed(),
+	},
+	parameters: {
+		layout: 'centered',
+	},
+} satisfies StoryObj<typeof MkCustomEmojiDetailedDialog>;
diff --git a/packages/frontend/src/components/MkCustomEmojiDetailedDialog.vue b/packages/frontend/src/components/MkCustomEmojiDetailedDialog.vue
index c7f1288729..ecbee864dc 100644
--- a/packages/frontend/src/components/MkCustomEmojiDetailedDialog.vue
+++ b/packages/frontend/src/components/MkCustomEmojiDetailedDialog.vue
@@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 -->
 
 <template>
-<MkModalWindow ref="dialogEl" @close="cancel()" @closed="$emit('closed')">
+<MkModalWindow ref="dialogEl" @close="cancel()" @closed="emit('closed')">
 	<template #header>:{{ emoji.name }}:</template>
 	<template #default>
 		<MkSpacer>
@@ -85,8 +85,8 @@ function cancel() {
 .emojiImgWrapper {
   max-width: 100%;
   height: 40cqh;
-  background-image: repeating-linear-gradient(45deg, transparent, transparent 8px, var(--X5) 8px, var(--X5) 14px);
-  border-radius: var(--radius);
+  background-image: repeating-linear-gradient(45deg, transparent, transparent 8px, var(--MI_THEME-X5) 8px, var(--MI_THEME-X5) 14px);
+  border-radius: var(--MI-radius);
   margin: auto;
   overflow-y: hidden;
 }
@@ -101,8 +101,8 @@ function cancel() {
   display: inline-block;
   word-break: break-all;
   padding: 3px 10px;
-  background-color: var(--X5);
-  border: solid 1px var(--divider);
-  border-radius: var(--radius);
+  background-color: var(--MI_THEME-X5);
+  border: solid 1px var(--MI_THEME-divider);
+  border-radius: var(--MI-radius);
 }
 </style>
diff --git a/packages/frontend/src/components/MkCwButton.stories.impl.ts b/packages/frontend/src/components/MkCwButton.stories.impl.ts
new file mode 100644
index 0000000000..5d6ea56da9
--- /dev/null
+++ b/packages/frontend/src/components/MkCwButton.stories.impl.ts
@@ -0,0 +1,79 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+/* eslint-disable @typescript-eslint/explicit-function-return-type */
+/* eslint-disable import/no-default-export */
+import { StoryObj } from '@storybook/vue3';
+import { action } from '@storybook/addon-actions';
+import { expect, userEvent, within } from '@storybook/test';
+import { file } from '../../.storybook/fakes.js';
+import MkCwButton from './MkCwButton.vue';
+import { i18n } from '@/i18n.js';
+
+export const Default = {
+	render(args) {
+		return {
+			components: {
+				MkCwButton,
+			},
+			data() {
+				return {
+					showContent: false,
+				};
+			},
+			setup() {
+				return {
+					args,
+				};
+			},
+			computed: {
+				props() {
+					return {
+						...this.args,
+					};
+				},
+				events() {
+					return {
+						'update:modelValue': action('update:modelValue'),
+					};
+				},
+			},
+			template: '<MkCwButton v-model="showContent" v-bind="props" v-on="events" />',
+		};
+	},
+	args: {
+		text: 'Some CW content',
+	},
+	async play({ canvasElement }) {
+		const canvas = within(canvasElement);
+		const buttonElement = canvas.getByRole<HTMLButtonElement>('button');
+		await expect(buttonElement).toHaveTextContent(i18n.ts._cw.show);
+		await expect(buttonElement).toHaveTextContent(i18n.tsx._cw.chars({ count: 15 }));
+		await userEvent.click(buttonElement);
+		await expect(buttonElement).toHaveTextContent(i18n.ts._cw.hide);
+		await userEvent.click(buttonElement);
+	},
+	parameters: {
+		chromatic: {
+			// NOTE: テストが終わるまで待つ
+			delay: 5000,
+		},
+		layout: 'centered',
+	},
+} satisfies StoryObj<typeof MkCwButton>;
+export const IncludesTextAndDriveFile = {
+	...Default,
+	args: {
+		text: 'Some CW content',
+		files: [file()],
+	},
+	async play({ canvasElement }) {
+		const canvas = within(canvasElement);
+		const buttonElement = canvas.getByRole<HTMLButtonElement>('button');
+		await expect(buttonElement).toHaveTextContent(i18n.tsx._cw.chars({ count: 15 }));
+		await expect(buttonElement).toHaveTextContent(' / ');
+		await expect(buttonElement).toHaveTextContent(i18n.tsx._cw.files({ count: 1 }));
+	},
+} satisfies StoryObj<typeof MkCwButton>;
diff --git a/packages/frontend/src/components/MkCwButton.vue b/packages/frontend/src/components/MkCwButton.vue
index a2cb3185f4..b5f6e78b6c 100644
--- a/packages/frontend/src/components/MkCwButton.vue
+++ b/packages/frontend/src/components/MkCwButton.vue
@@ -45,11 +45,11 @@ function toggle() {
 .label {
 	margin-left: 4px;
 
-	&:before {
+	&::before {
 		content: '(';
 	}
 
-	&:after {
+	&::after {
 		content: ')';
 	}
 }
diff --git a/packages/frontend/src/components/MkDateSeparatedList.stories.impl.ts b/packages/frontend/src/components/MkDateSeparatedList.stories.impl.ts
new file mode 100644
index 0000000000..0e5635754c
--- /dev/null
+++ b/packages/frontend/src/components/MkDateSeparatedList.stories.impl.ts
@@ -0,0 +1,7 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import MkDateSeparatedList from './MkDateSeparatedList.vue';
+void MkDateSeparatedList;
diff --git a/packages/frontend/src/components/MkDateSeparatedList.vue b/packages/frontend/src/components/MkDateSeparatedList.vue
index 85e131cf9b..9c75f91cb2 100644
--- a/packages/frontend/src/components/MkDateSeparatedList.vue
+++ b/packages/frontend/src/components/MkDateSeparatedList.vue
@@ -9,6 +9,7 @@ import MkAd from '@/components/global/MkAd.vue';
 import { isDebuggerEnabled, stackTraceInstances } from '@/debug.js';
 import { i18n } from '@/i18n.js';
 import * as os from '@/os.js';
+import { instance } from '@/instance.js';
 import { defaultStore } from '@/store.js';
 import { MisskeyEntity } from '@/types/date-separated-list.js';
 
@@ -43,9 +44,9 @@ export default defineComponent({
 	setup(props, { slots, expose }) {
 		const $style = useCssModule(); // カスタムレンダラなので使っても大丈夫
 
-		function getDateText(time: string) {
-			const date = new Date(time).getDate();
-			const month = new Date(time).getMonth() + 1;
+		function getDateText(dateInstance: Date) {
+			const date = dateInstance.getDate();
+			const month = dateInstance.getMonth() + 1;
 			return i18n.tsx.monthAndDay({
 				month: month.toString(),
 				day: date.toString(),
@@ -62,9 +63,16 @@ export default defineComponent({
 			})[0];
 			if (el.key == null && item.id) el.key = item.id;
 
+			const date = new Date(item.createdAt);
+			const nextDate = props.items[i + 1] ? new Date(props.items[i + 1].createdAt) : null;
+
 			if (
 				i !== props.items.length - 1 &&
-				new Date(item.createdAt).getDate() !== new Date(props.items[i + 1].createdAt).getDate()
+				nextDate != null && (
+					date.getFullYear() !== nextDate.getFullYear() ||
+					date.getMonth() !== nextDate.getMonth() ||
+					date.getDate() !== nextDate.getDate()
+				)
 			) {
 				const separator = h('div', {
 					class: $style['separator'],
@@ -78,12 +86,12 @@ export default defineComponent({
 						h('i', {
 							class: `ti ti-chevron-up ${$style['date-1-icon']}`,
 						}),
-						getDateText(item.createdAt),
+						getDateText(date),
 					]),
 					h('span', {
 						class: $style['date-2'],
 					}, [
-						getDateText(props.items[i + 1].createdAt),
+						getDateText(nextDate),
 						h('i', {
 							class: `ti ti-chevron-down ${$style['date-2-icon']}`,
 						}),
@@ -92,11 +100,13 @@ export default defineComponent({
 
 				return [el, separator];
 			} else {
-				if (props.ad && item._shouldInsertAd_) {
-					return [h(MkAd, {
+				if (props.ad && instance.ads.length > 0 && item._shouldInsertAd_) {
+					return [h('div', {
 						key: item.id + ':ad',
+						class: $style['ad-wrapper'],
+					}, [h(MkAd, {
 						prefer: ['horizontal', 'horizontal-big'],
-					}), el];
+					})]), el];
 				} else {
 					return el;
 				}
@@ -118,19 +128,19 @@ export default defineComponent({
 			return children;
 		};
 
-		function onBeforeLeave(element: Element) {
-			const el = element as HTMLElement;
+		function onBeforeLeave(el: Element) {
+			if (!(el instanceof HTMLElement)) return;
 			el.style.top = `${el.offsetTop}px`;
 			el.style.left = `${el.offsetLeft}px`;
 		}
 
-		function onLeaveCancelled(element: Element) {
-			const el = element as HTMLElement;
+		function onLeaveCancelled(el: Element) {
+			if (!(el instanceof HTMLElement)) return;
 			el.style.top = '';
 			el.style.left = '';
 		}
 
-		// eslint-disable-next-line vue/no-setup-props-destructure
+		// eslint-disable-next-line vue/no-setup-props-reactivity-loss
 		const classes = {
 			[$style['date-separated-list']]: true,
 			[$style['date-separated-list-nogap']]: props.noGap,
@@ -175,7 +185,7 @@ export default defineComponent({
 	}
 
 	&:not(.date-separated-list-nogap) > *:not(:last-child) {
-		margin-bottom: var(--margin);
+		margin-bottom: var(--MI-margin);
 	}
 }
 
@@ -187,7 +197,7 @@ export default defineComponent({
 		box-shadow: none;
 
 		&:not(:last-child) {
-			border-bottom: solid 0.5px var(--divider);
+			border-bottom: solid 0.5px var(--MI_THEME-divider);
 		}
 	}
 }
@@ -228,7 +238,7 @@ export default defineComponent({
 	line-height: 32px;
 	text-align: center;
 	font-size: 12px;
-	color: var(--dateLabelFg);
+	color: var(--MI_THEME-dateLabelFg);
 }
 
 .date-1 {
@@ -246,5 +256,11 @@ export default defineComponent({
 .date-2-icon {
 	margin-left: 8px;
 }
+
+.ad-wrapper {
+	padding: 8px;
+	background-size: auto auto;
+	background-image: repeating-linear-gradient(45deg, transparent, transparent 8px, var(--MI_THEME-bg) 8px, var(--MI_THEME-bg) 14px);
+}
 </style>
 
diff --git a/packages/frontend/src/components/MkDialog.stories.impl.ts b/packages/frontend/src/components/MkDialog.stories.impl.ts
new file mode 100644
index 0000000000..2d8d3661f2
--- /dev/null
+++ b/packages/frontend/src/components/MkDialog.stories.impl.ts
@@ -0,0 +1,159 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { action } from '@storybook/addon-actions';
+import { expect, userEvent, waitFor, within } from '@storybook/test';
+import { StoryObj } from '@storybook/vue3';
+import { i18n } from '@/i18n.js';
+import MkDialog from './MkDialog.vue';
+const Base = {
+	render(args) {
+		return {
+			components: {
+				MkDialog,
+			},
+			setup() {
+				return {
+					args,
+				};
+			},
+			computed: {
+				props() {
+					return {
+						...this.args,
+					};
+				},
+				events() {
+					return {
+						done: action('done'),
+						closed: action('closed'),
+					};
+				},
+			},
+			template: '<MkDialog v-bind="props" v-on="events" />',
+		};
+	},
+	args: {
+		text: 'Hello, world!',
+	},
+	parameters: {
+		layout: 'centered',
+	},
+} satisfies StoryObj<typeof MkDialog>;
+export const Success = {
+	...Base,
+	args: {
+		...Base.args,
+		type: 'success',
+	},
+} satisfies StoryObj<typeof MkDialog>;
+export const Error = {
+	...Base,
+	args: {
+		...Base.args,
+		type: 'error',
+	},
+} satisfies StoryObj<typeof MkDialog>;
+export const Warning = {
+	...Base,
+	args: {
+		...Base.args,
+		type: 'warning',
+	},
+} satisfies StoryObj<typeof MkDialog>;
+export const Info = {
+	...Base,
+	args: {
+		...Base.args,
+		type: 'info',
+	},
+} satisfies StoryObj<typeof MkDialog>;
+export const Question = {
+	...Base,
+	args: {
+		...Base.args,
+		type: 'question',
+	},
+} satisfies StoryObj<typeof MkDialog>;
+export const Waiting = {
+	...Base,
+	args: {
+		...Base.args,
+		type: 'waiting',
+	},
+} satisfies StoryObj<typeof MkDialog>;
+export const DialogWithActions = {
+	...Question,
+	args: {
+		...Question.args,
+		text: i18n.ts.areYouSure,
+		actions: [
+			{
+				text: i18n.ts.yes,
+				primary: true,
+				callback() {
+					action('YES')();
+				},
+			},
+			{
+				text: i18n.ts.no,
+				callback() {
+					action('NO')();
+				},
+			},
+		],
+	},
+} satisfies StoryObj<typeof MkDialog>;
+export const DialogWithDangerActions = {
+	...Warning,
+	args: {
+		...Warning.args,
+		text: i18n.ts.resetAreYouSure,
+		actions: [
+			{
+				text: i18n.ts.yes,
+				danger: true,
+				primary: true,
+				callback() {
+					action('YES')();
+				},
+			},
+			{
+				text: i18n.ts.no,
+				callback() {
+					action('NO')();
+				},
+			},
+		],
+	},
+} satisfies StoryObj<typeof MkDialog>;
+export const DialogWithInput = {
+	...Question,
+	args: {
+		...Question.args,
+		title: 'Hello, world!',
+		text: undefined,
+		input: {
+			placeholder: i18n.ts.inputMessageHere,
+			type: 'text',
+			default: null,
+			minLength: 2,
+			maxLength: 3,
+		},
+	},
+	async play({ canvasElement }) {
+		const canvas = within(canvasElement);
+		await expect(canvasElement).toHaveTextContent(i18n.tsx._dialog.charactersBelow({ current: 0, min: 2 }));
+		const okButton = canvas.getByRole('button', { name: i18n.ts.ok });
+		await expect(okButton).toBeDisabled();
+		const input = canvas.getByRole<HTMLInputElement>('combobox');
+		await waitFor(() => userEvent.hover(input));
+		await waitFor(() => userEvent.click(input));
+		await waitFor(() => userEvent.type(input, 'M'));
+		await expect(canvasElement).toHaveTextContent(i18n.tsx._dialog.charactersBelow({ current: 1, min: 2 }));
+		await waitFor(() => userEvent.type(input, 'i'));
+		await expect(okButton).toBeEnabled();
+	},
+} satisfies StoryObj<typeof MkDialog>;
diff --git a/packages/frontend/src/components/MkDialog.vue b/packages/frontend/src/components/MkDialog.vue
index c52404a319..b095a1cd4a 100644
--- a/packages/frontend/src/components/MkDialog.vue
+++ b/packages/frontend/src/components/MkDialog.vue
@@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 -->
 
 <template>
-<MkModal ref="modal" :preferType="'dialog'" :zPriority="'high'" @click="done(true)" @closed="emit('closed')">
+<MkModal ref="modal" :preferType="'dialog'" :zPriority="'high'" @click="done(true)" @closed="emit('closed')" @esc="cancel()">
 	<div :class="$style.root">
 		<div v-if="icon" :class="$style.icon">
 			<i :class="icon"></i>
@@ -36,11 +36,16 @@ SPDX-License-Identifier: AGPL-3.0-only
 		</MkInput>
 		<MkSelect v-if="select" v-model="selectedValue" autofocus>
 			<template v-if="select.items">
-				<option v-for="item in select.items" :value="item.value">{{ item.text }}</option>
+				<template v-for="item in select.items">
+					<optgroup v-if="'sectionTitle' in item" :label="item.sectionTitle">
+						<option v-for="subItem in item.items" :value="subItem.value">{{ subItem.text }}</option>
+					</optgroup>
+					<option v-else :value="item.value">{{ item.text }}</option>
+				</template>
 			</template>
 		</MkSelect>
 		<div v-if="(showOkButton || showCancelButton) && !actions" :class="$style.buttons">
-			<MkButton v-if="showOkButton" data-cy-modal-dialog-ok inline primary rounded :autofocus="!input && !select" :disabled="okButtonDisabledReason" @click="ok">{{ okText ?? ((showCancelButton || input || select) ? i18n.ts.ok : i18n.ts.gotIt) }}</MkButton>
+			<MkButton v-if="showOkButton" data-cy-modal-dialog-ok inline primary rounded :autofocus="!input && !select" :disabled="okButtonDisabledReason != null" @click="ok">{{ okText ?? ((showCancelButton || input || select) ? i18n.ts.ok : i18n.ts.gotIt) }}</MkButton>
 			<MkButton v-if="showCancelButton || input || select" data-cy-modal-dialog-cancel inline rounded @click="cancel">{{ cancelText ?? i18n.ts.cancel }}</MkButton>
 		</div>
 		<div v-if="actions" :class="$style.buttons">
@@ -51,7 +56,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { onBeforeUnmount, onMounted, ref, shallowRef, computed } from 'vue';
+import { ref, shallowRef, computed } from 'vue';
 import MkModal from '@/components/MkModal.vue';
 import MkButton from '@/components/MkButton.vue';
 import MkInput from '@/components/MkInput.vue';
@@ -67,11 +72,16 @@ type Input = {
 	maxLength?: number;
 };
 
+type SelectItem = {
+	value: any;
+	text: string;
+};
+
 type Select = {
-	items: {
-		value: any;
-		text: string;
-	}[];
+	items: (SelectItem | {
+		sectionTitle: string;
+		items: SelectItem[];
+	})[];
 	default: string | null;
 };
 
@@ -88,7 +98,7 @@ const props = withDefaults(defineProps<{
 		text: string;
 		primary?: boolean,
 		danger?: boolean,
-		callback: (...args: any[]) => void;
+		callback: (...args: unknown[]) => void;
 	}[];
 	showOkButton?: boolean;
 	showCancelButton?: boolean;
@@ -156,10 +166,6 @@ function onBgClick() {
 	if (props.cancelableByBgClick) cancel();
 }
 */
-function onKeydown(evt: KeyboardEvent) {
-	if (evt.key === 'Escape') cancel();
-}
-
 function onInputKeydown(evt: KeyboardEvent) {
 	if (evt.key === 'Enter' && okButtonDisabledReason.value === null) {
 		evt.preventDefault();
@@ -167,14 +173,6 @@ function onInputKeydown(evt: KeyboardEvent) {
 		ok();
 	}
 }
-
-onMounted(() => {
-	document.addEventListener('keydown', onKeydown);
-});
-
-onBeforeUnmount(() => {
-	document.removeEventListener('keydown', onKeydown);
-});
 </script>
 
 <style lang="scss" module>
@@ -186,7 +184,7 @@ onBeforeUnmount(() => {
 	max-width: 480px;
 	box-sizing: border-box;
 	text-align: center;
-	background: var(--panel);
+	background: var(--MI_THEME-panel);
 	border-radius: 16px;
 }
 
@@ -208,15 +206,15 @@ onBeforeUnmount(() => {
 }
 
 .type_success {
-	color: var(--success);
+	color: var(--MI_THEME-success);
 }
 
 .type_error {
-	color: var(--error);
+	color: var(--MI_THEME-error);
 }
 
 .type_warning {
-	color: var(--warn);
+	color: var(--MI_THEME-warn);
 }
 
 .title {
diff --git a/packages/backend/src/misc/prelude/symbol.ts b/packages/frontend/src/components/MkDivider.stories.impl.ts
similarity index 64%
rename from packages/backend/src/misc/prelude/symbol.ts
rename to packages/frontend/src/components/MkDivider.stories.impl.ts
index 7e8d39bdb6..a593111987 100644
--- a/packages/backend/src/misc/prelude/symbol.ts
+++ b/packages/frontend/src/components/MkDivider.stories.impl.ts
@@ -3,4 +3,5 @@
  * SPDX-License-Identifier: AGPL-3.0-only
  */
 
-export const fallback = Symbol('fallback');
+import MkDivider from './MkDivider.vue';
+void MkDivider;
diff --git a/packages/frontend/src/components/MkDivider.vue b/packages/frontend/src/components/MkDivider.vue
new file mode 100644
index 0000000000..f72f091383
--- /dev/null
+++ b/packages/frontend/src/components/MkDivider.vue
@@ -0,0 +1,32 @@
+<!--
+SPDX-FileCopyrightText: syuilo and misskey-project
+SPDX-License-Identifier: AGPL-3.0-only
+-->
+
+<template>
+<div
+	class="default" :style="[
+		marginTopBottom ? { marginTop: marginTopBottom, marginBottom: marginTopBottom } : {},
+		marginLeftRight ? { marginLeft: marginLeftRight, marginRight: marginLeftRight } : {},
+		borderStyle ? { borderStyle: borderStyle } : {},
+		borderWidth ? { borderWidth: borderWidth } : {},
+		borderColor ? { borderColor: borderColor } : {},
+	]"
+/>
+</template>
+
+<script setup lang="ts">
+defineProps<{
+	marginTopBottom?: string;
+	marginLeftRight?: string;
+	borderStyle?: string;
+	borderWidth?: string;
+	borderColor?: string;
+}>();
+</script>
+
+<style scoped lang="scss">
+.default {
+	border-top: solid 0.5px var(--MI_THEME-divider);
+}
+</style>
diff --git a/packages/frontend/src/components/MkDonation.stories.impl.ts b/packages/frontend/src/components/MkDonation.stories.impl.ts
new file mode 100644
index 0000000000..27d6b7df6c
--- /dev/null
+++ b/packages/frontend/src/components/MkDonation.stories.impl.ts
@@ -0,0 +1,54 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { action } from '@storybook/addon-actions';
+import { StoryObj } from '@storybook/vue3';
+import { onBeforeUnmount } from 'vue';
+import MkDonation from './MkDonation.vue';
+import { instance } from '@/instance.js';
+export const Default = {
+	render(args) {
+		return {
+			components: {
+				MkDonation,
+			},
+			setup() {
+				return {
+					args,
+				};
+			},
+			computed: {
+				props() {
+					return {
+						...this.args,
+					};
+				},
+				events() {
+					return {
+						closed: action('closed'),
+					};
+				},
+			},
+			template: '<MkDonation v-bind="props" v-on="events" />',
+		};
+	},
+	args: {
+		// @ts-expect-error name is used for mocking instance
+		name: 'Misskey Hub',
+	},
+	decorators: [
+		(_, { args }) => ({
+			setup() {
+				// @ts-expect-error name is used for mocking instance
+				instance.name = args.name;
+				onBeforeUnmount(() => instance.name = null);
+			},
+			template: '<story/>',
+		}),
+	],
+	parameters: {
+		layout: 'centered',
+	},
+} satisfies StoryObj<typeof MkDonation>;
diff --git a/packages/frontend/src/components/MkDonation.vue b/packages/frontend/src/components/MkDonation.vue
index 434fc81582..0e0da64750 100644
--- a/packages/frontend/src/components/MkDonation.vue
+++ b/packages/frontend/src/components/MkDonation.vue
@@ -38,7 +38,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 <script lang="ts" setup>
 import MkButton from '@/components/MkButton.vue';
 import MkLink from '@/components/MkLink.vue';
-import { host } from '@/config.js';
+import { host } from '@@/js/config.js';
 import { i18n } from '@/i18n.js';
 import * as os from '@/os.js';
 import { miLocalStorage } from '@/local-storage.js';
@@ -65,12 +65,12 @@ function neverShow() {
 .root {
 	position: fixed;
 	z-index: v-bind(zIndex);
-	bottom: var(--margin);
+	bottom: var(--MI-margin);
 	left: 0;
 	right: 0;
 	margin: auto;
 	box-sizing: border-box;
-	width: calc(100% - (var(--margin) * 2));
+	width: calc(100% - (var(--MI-margin) * 2));
 	max-width: 500px;
 	display: flex;
 }
@@ -79,7 +79,7 @@ function neverShow() {
 	text-align: center;
 	padding-top: 25px;
 	width: 100px;
-	color: var(--accent);
+	color: var(--MI_THEME-accent);
 }
 @media (max-width: 500px) {
 	.icon {
diff --git a/packages/frontend/src/components/MkDrive.file.stories.impl.ts b/packages/frontend/src/components/MkDrive.file.stories.impl.ts
new file mode 100644
index 0000000000..5f6e6a0667
--- /dev/null
+++ b/packages/frontend/src/components/MkDrive.file.stories.impl.ts
@@ -0,0 +1,48 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { action } from '@storybook/addon-actions';
+import { StoryObj } from '@storybook/vue3';
+import MkDrive_file from './MkDrive.file.vue';
+import { file } from '../../.storybook/fakes.js';
+export const Default = {
+	render(args) {
+		return {
+			components: {
+				MkDrive_file,
+			},
+			setup() {
+				return {
+					args,
+				};
+			},
+			computed: {
+				props() {
+					return {
+						...this.args,
+					};
+				},
+				events() {
+					return {
+						chosen: action('chosen'),
+						dragstart: action('dragstart'),
+						dragend: action('dragend'),
+					};
+				},
+			},
+			template: '<MkDrive_file v-bind="props" v-on="events" />',
+		};
+	},
+	args: {
+		file: file(),
+	},
+	parameters: {
+		chromatic: {
+			// NOTE: ロードが終わるまで待つ
+			delay: 3000,
+		},
+		layout: 'centered',
+	},
+} satisfies StoryObj<typeof MkDrive_file>;
diff --git a/packages/frontend/src/components/MkDrive.file.vue b/packages/frontend/src/components/MkDrive.file.vue
index 4106b0a436..e45c3bd9ce 100644
--- a/packages/frontend/src/components/MkDrive.file.vue
+++ b/packages/frontend/src/components/MkDrive.file.vue
@@ -115,14 +115,14 @@ function onDragend() {
 		background: rgba(#000, 0.05);
 
 		> .label {
-			&:before,
-			&:after {
+			&::before,
+			&::after {
 				background: #0b65a5;
 			}
 
 			&.red {
-				&:before,
-				&:after {
+				&::before,
+				&::after {
 					background: #c12113;
 				}
 			}
@@ -133,14 +133,14 @@ function onDragend() {
 		background: rgba(#000, 0.1);
 
 		> .label {
-			&:before,
-			&:after {
+			&::before,
+			&::after {
 				background: #0b588c;
 			}
 
 			&.red {
-				&:before,
-				&:after {
+				&::before,
+				&::after {
 					background: #ce2212;
 				}
 			}
@@ -148,19 +148,19 @@ function onDragend() {
 	}
 
 	&.isSelected {
-		background: var(--accent);
+		background: var(--MI_THEME-accent);
 
 		&:hover {
-			background: var(--accentLighten);
+			background: var(--MI_THEME-accentLighten);
 		}
 
 		&:active {
-			background: var(--accentDarken);
+			background: var(--MI_THEME-accentDarken);
 		}
 
 		> .label {
-			&:before,
-			&:after {
+			&::before,
+			&::after {
 				display: none;
 			}
 		}
@@ -181,8 +181,8 @@ function onDragend() {
 	left: 0;
 	pointer-events: none;
 
-	&:before,
-	&:after {
+	&::before,
+	&::after {
 		content: "";
 		display: block;
 		position: absolute;
@@ -190,14 +190,14 @@ function onDragend() {
 		background: #0c7ac9;
 	}
 
-	&:before {
+	&::before {
 		top: 0;
 		left: 57px;
 		width: 28px;
 		height: 8px;
 	}
 
-	&:after {
+	&::after {
 		top: 57px;
 		left: 0;
 		width: 8px;
@@ -205,8 +205,8 @@ function onDragend() {
 	}
 
 	&.red {
-		&:before,
-		&:after {
+		&::before,
+		&::after {
 			background: #c12113;
 		}
 	}
@@ -244,7 +244,7 @@ function onDragend() {
 	font-size: 0.8em;
 	text-align: center;
 	word-break: break-all;
-	color: var(--fg);
+	color: var(--MI_THEME-fg);
 	overflow: hidden;
 }
 </style>
diff --git a/packages/frontend/src/components/MkDrive.folder.stories.impl.ts b/packages/frontend/src/components/MkDrive.folder.stories.impl.ts
new file mode 100644
index 0000000000..5f8ef48520
--- /dev/null
+++ b/packages/frontend/src/components/MkDrive.folder.stories.impl.ts
@@ -0,0 +1,70 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { action } from '@storybook/addon-actions';
+import { StoryObj } from '@storybook/vue3';
+import { http, HttpResponse } from 'msw';
+import * as Misskey from 'misskey-js';
+import MkDrive_folder from './MkDrive.folder.vue';
+import { folder } from '../../.storybook/fakes.js';
+import { commonHandlers } from '../../.storybook/mocks.js';
+export const Default = {
+	render(args) {
+		return {
+			components: {
+				MkDrive_folder,
+			},
+			setup() {
+				return {
+					args,
+				};
+			},
+			computed: {
+				props() {
+					return {
+						...this.args,
+					};
+				},
+				events() {
+					return {
+						chosen: action('chosen'),
+						move: action('move'),
+						upload: action('upload'),
+						removeFile: action('removeFile'),
+						removeFolder: action('removeFolder'),
+						dragstart: action('dragstart'),
+						dragend: action('dragend'),
+					};
+				},
+			},
+			template: '<MkDrive_folder v-bind="props" v-on="events" />',
+		};
+	},
+	args: {
+		folder: folder(),
+	},
+	parameters: {
+		layout: 'centered',
+		msw: {
+			handlers: [
+				...commonHandlers,
+				http.post('/api/drive/folders/delete', async ({ request }) => {
+					action('POST /api/drive/folders/delete')(await request.json());
+					return HttpResponse.json(undefined, { status: 204 });
+				}),
+				http.post('/api/drive/folders/update', async ({ request }) => {
+					const req = await request.json() as Misskey.entities.DriveFoldersUpdateRequest;
+					action('POST /api/drive/folders/update')(req);
+					return HttpResponse.json({
+						...folder(),
+						id: req.folderId,
+						name: req.name ?? folder().name,
+						parentId: req.parentId ?? folder().parentId,
+					});
+				}),
+			],
+		},
+	},
+} satisfies StoryObj<typeof MkDrive_folder>;
diff --git a/packages/frontend/src/components/MkDrive.folder.vue b/packages/frontend/src/components/MkDrive.folder.vue
index 8da0d78f35..44e3b59ade 100644
--- a/packages/frontend/src/components/MkDrive.folder.vue
+++ b/packages/frontend/src/components/MkDrive.folder.vue
@@ -27,20 +27,22 @@ SPDX-License-Identifier: AGPL-3.0-only
 	<p v-if="defaultStore.state.uploadFolder == folder.id" :class="$style.upload">
 		{{ i18n.ts.uploadFolder }}
 	</p>
-	<button v-if="selectMode" class="_button" :class="[$style.checkbox, { [$style.checked]: isSelected }]" @click.prevent.stop="checkboxClicked"></button>
+	<button v-if="selectMode" class="_button" :class="$style.checkboxWrapper" @click.prevent.stop="checkboxClicked">
+		<div :class="[$style.checkbox, { [$style.checked]: isSelected }]"></div>
+	</button>
 </div>
 </template>
 
 <script lang="ts" setup>
 import { computed, defineAsyncComponent, ref } from 'vue';
 import * as Misskey from 'misskey-js';
+import type { MenuItem } from '@/types/menu.js';
 import * as os from '@/os.js';
 import { misskeyApi } from '@/scripts/misskey-api.js';
 import { i18n } from '@/i18n.js';
 import { defaultStore } from '@/store.js';
 import { claimAchievement } from '@/scripts/achievements.js';
-import copyToClipboard from '@/scripts/copy-to-clipboard.js';
-import { MenuItem } from '@/types/menu.js';
+import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
 
 const props = withDefaults(defineProps<{
 	folder: Misskey.entities.DriveFolder;
@@ -53,6 +55,7 @@ const props = withDefaults(defineProps<{
 
 const emit = defineEmits<{
 	(ev: 'chosen', v: Misskey.entities.DriveFolder): void;
+	(ev: 'unchose', v: Misskey.entities.DriveFolder): void;
 	(ev: 'move', v: Misskey.entities.DriveFolder): void;
 	(ev: 'upload', file: File, folder: Misskey.entities.DriveFolder);
 	(ev: 'removeFile', v: Misskey.entities.DriveFile['id']): void;
@@ -68,7 +71,11 @@ const isDragging = ref(false);
 const title = computed(() => props.folder.name);
 
 function checkboxClicked() {
-	emit('chosen', props.folder);
+	if (props.isSelected) {
+		emit('unchose', props.folder);
+	} else {
+		emit('chosen', props.folder);
+	}
 }
 
 function onClick() {
@@ -222,6 +229,17 @@ function rename() {
 	});
 }
 
+function move() {
+	os.selectDriveFolder(false).then(folder => {
+		if (folder[0] && folder[0].id === props.folder.id) return;
+
+		misskeyApi('drive/folders/update', {
+			folderId: props.folder.id,
+			parentId: folder[0] ? folder[0].id : null,
+		});
+	});
+}
+
 function deleteFolder() {
 	misskeyApi('drive/folders/delete', {
 		folderId: props.folder.id,
@@ -257,15 +275,20 @@ function onContextmenu(ev: MouseEvent) {
 		text: i18n.ts.openInWindow,
 		icon: 'ti ti-app-window',
 		action: () => {
-			os.popup(defineAsyncComponent(() => import('@/components/MkDriveWindow.vue')), {
+			const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkDriveWindow.vue')), {
 				initialFolder: props.folder,
 			}, {
-			}, 'closed');
+				closed: () => dispose(),
+			});
 		},
 	}, { type: 'divider' }, {
 		text: i18n.ts.rename,
 		icon: 'ti ti-forms',
 		action: rename,
+	}, {
+		text: i18n.ts.move,
+		icon: 'ti ti ti-folder-symlink',
+		action: move,
 	}, { type: 'divider' }, {
 		text: i18n.ts.delete,
 		icon: 'ti ti-trash',
@@ -290,12 +313,12 @@ function onContextmenu(ev: MouseEvent) {
 	position: relative;
 	padding: 8px;
 	height: 64px;
-	background: var(--driveFolderBg);
+	background: var(--MI_THEME-driveFolderBg);
 	border-radius: 4px;
 	cursor: pointer;
 
 	&.draghover {
-		&:after {
+		&::after {
 			content: "";
 			pointer-events: none;
 			position: absolute;
@@ -303,30 +326,55 @@ function onContextmenu(ev: MouseEvent) {
 			right: -4px;
 			bottom: -4px;
 			left: -4px;
-			border: 2px dashed var(--focus);
+			border: 2px dashed var(--MI_THEME-focus);
 			border-radius: 4px;
 		}
 	}
 }
 
-.checkbox {
+.checkboxWrapper {
 	position: absolute;
-	bottom: 8px;
-	right: 8px;
-	width: 16px;
-	height: 16px;
-	background: #fff;
-	border: solid 1px #000;
+	border-radius: 50%;
+	bottom: 2px;
+	right: 2px;
+	padding: 8px;
+	box-sizing: border-box;
 
-	&.checked {
-		background: var(--accent);
+	> .checkbox {
+		position: relative;
+		width: 18px;
+		height: 18px;
+		background: #fff;
+		border: solid 2px var(--MI_THEME-divider);
+		border-radius: 4px;
+		box-sizing: border-box;
+
+		&.checked {
+			border-color: var(--MI_THEME-accent);
+			background: var(--MI_THEME-accent);
+
+			&::after {
+				content: "\ea5e";
+				font-family: 'tabler-icons';
+				position: absolute;
+				top: 50%;
+				left: 50%;
+				transform: translate(-50%, -50%);
+				color: #fff;
+				font-size: 12px;
+				line-height: 22px;
+			}
+		}
+	}
+
+	&:hover {
+		background: var(--MI_THEME-accentedBg);
 	}
 }
 
 .name {
 	margin: 0;
 	font-size: 0.9em;
-	color: var(--desktopDriveFolderFg);
 }
 
 .icon {
@@ -339,6 +387,5 @@ function onContextmenu(ev: MouseEvent) {
 	margin: 4px 4px;
 	font-size: 0.8em;
 	text-align: right;
-	color: var(--desktopDriveFolderFg);
 }
 </style>
diff --git a/packages/frontend/src/components/MkDrive.navFolder.stories.impl.ts b/packages/frontend/src/components/MkDrive.navFolder.stories.impl.ts
new file mode 100644
index 0000000000..9d49f24fa4
--- /dev/null
+++ b/packages/frontend/src/components/MkDrive.navFolder.stories.impl.ts
@@ -0,0 +1,7 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import MkDrive_navFolder from './MkDrive.navFolder.vue';
+void MkDrive_navFolder;
diff --git a/packages/frontend/src/components/MkDrive.stories.impl.ts b/packages/frontend/src/components/MkDrive.stories.impl.ts
new file mode 100644
index 0000000000..fe20e61415
--- /dev/null
+++ b/packages/frontend/src/components/MkDrive.stories.impl.ts
@@ -0,0 +1,82 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { action } from '@storybook/addon-actions';
+import { StoryObj } from '@storybook/vue3';
+import { http, HttpResponse } from 'msw';
+import * as Misskey from 'misskey-js';
+import MkDrive from './MkDrive.vue';
+import { file, folder } from '../../.storybook/fakes.js';
+import { commonHandlers } from '../../.storybook/mocks.js';
+export const Default = {
+	render(args) {
+		return {
+			components: {
+				MkDrive,
+			},
+			setup() {
+				return {
+					args,
+				};
+			},
+			computed: {
+				props() {
+					return {
+						...this.args,
+					};
+				},
+				events() {
+					return {
+						selected: action('selected'),
+						'change-selection': action('change-selection'),
+						'move-root': action('move-root'),
+						cd: action('cd'),
+						'open-folder': action('open-folder'),
+					};
+				},
+			},
+			template: '<MkDrive v-bind="props" v-on="events" />',
+		};
+	},
+	parameters: {
+		chromatic: {
+			// NOTE: ロードが終わるまで待つ
+			delay: 3000,
+		},
+		layout: 'centered',
+		msw: {
+			handlers: [
+				...commonHandlers,
+				http.post('/api/drive/files', async ({ request }) => {
+					action('POST /api/drive/files')(await request.json());
+					return HttpResponse.json([file()]);
+				}),
+				http.post('/api/drive/folders', async ({ request }) => {
+					action('POST /api/drive/folders')(await request.json());
+					return HttpResponse.json([folder(crypto.randomUUID())]);
+				}),
+				http.post('/api/drive/folders/create', async ({ request }) => {
+					const req = await request.json() as Misskey.entities.DriveFoldersCreateRequest;
+					action('POST /api/drive/folders/create')(req);
+					return HttpResponse.json(folder(crypto.randomUUID(), req.name, req.parentId));
+				}),
+				http.post('/api/drive/folders/delete', async ({ request }) => {
+					action('POST /api/drive/folders/delete')(await request.json());
+					return HttpResponse.json(undefined, { status: 204 });
+				}),
+				http.post('/api/drive/folders/update', async ({ request }) => {
+					const req = await request.json() as Misskey.entities.DriveFoldersUpdateRequest;
+					action('POST /api/drive/folders/update')(req);
+					return HttpResponse.json({
+						...folder(),
+						id: req.folderId,
+						name: req.name ?? folder().name,
+						parentId: req.parentId ?? folder().parentId,
+					});
+				}),
+			]
+		},
+	},
+} satisfies StoryObj<typeof MkDrive>;
diff --git a/packages/frontend/src/components/MkDrive.vue b/packages/frontend/src/components/MkDrive.vue
index a9717b4fb7..8be6d6f53d 100644
--- a/packages/frontend/src/components/MkDrive.vue
+++ b/packages/frontend/src/components/MkDrive.vue
@@ -52,6 +52,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 					:selectMode="select === 'folder'"
 					:isSelected="selectedFolders.some(x => x.id === f.id)"
 					@chosen="chooseFolder"
+					@unchose="unchoseFolder"
 					@move="move"
 					@upload="upload"
 					@removeFile="removeFile"
@@ -156,7 +157,12 @@ const ilFilesObserver = new IntersectionObserver(
 	(entries) => entries.some((entry) => entry.isIntersecting) && !fetching.value && moreFiles.value && fetchMoreFiles(),
 );
 
+const sortModeSelect = ref<NonNullable<Misskey.entities.DriveFilesRequest['sort']>>('+createdAt');
+
 watch(folder, () => emit('cd', folder.value));
+watch(sortModeSelect, () => {
+	fetch();
+});
 
 function onStreamDriveFileCreated(file: Misskey.entities.DriveFile) {
 	addFile(file, true);
@@ -192,7 +198,7 @@ function onStreamDriveFolderDeleted(folderId: string) {
 	removeFolder(folderId);
 }
 
-function onDragover(ev: DragEvent): any {
+function onDragover(ev: DragEvent) {
 	if (!ev.dataTransfer) return;
 
 	// ドラッグ元が自分自身の所有するアイテムだったら
@@ -237,7 +243,7 @@ function onDragleave() {
 	draghover.value = false;
 }
 
-function onDrop(ev: DragEvent): any {
+function onDrop(ev: DragEvent) {
 	draghover.value = false;
 
 	if (!ev.dataTransfer) return;
@@ -326,7 +332,7 @@ function createFolder() {
 		title: i18n.ts.createFolder,
 		placeholder: i18n.ts.folderName,
 	}).then(({ canceled, result: name }) => {
-		if (canceled) return;
+		if (canceled || name == null) return;
 		misskeyApi('drive/folders/create', {
 			name: name,
 			parentId: folder.value ? folder.value.id : undefined,
@@ -428,6 +434,11 @@ function chooseFolder(folderToChoose: Misskey.entities.DriveFolder) {
 	}
 }
 
+function unchoseFolder(folderToUnchose: Misskey.entities.DriveFolder) {
+	selectedFolders.value = selectedFolders.value.filter(f => f.id !== folderToUnchose.id);
+	emit('change-selection', selectedFolders.value);
+}
+
 function move(target?: Misskey.entities.DriveFolder | Misskey.entities.DriveFolder['id' | 'parentId']) {
 	if (!target) {
 		goRoot();
@@ -552,6 +563,7 @@ async function fetch() {
 		folderId: folder.value ? folder.value.id : null,
 		type: props.type,
 		limit: filesMax + 1,
+		sort: sortModeSelect.value,
 	}).then(fetchedFiles => {
 		if (fetchedFiles.length === filesMax + 1) {
 			moreFiles.value = true;
@@ -601,6 +613,7 @@ function fetchMoreFiles() {
 		type: props.type,
 		untilId: files.value.at(-1)?.id,
 		limit: max + 1,
+		sort: sortModeSelect.value,
 	}).then(files => {
 		if (files.length === max + 1) {
 			moreFiles.value = true;
@@ -614,7 +627,9 @@ function fetchMoreFiles() {
 }
 
 function getMenu() {
-	const menu: MenuItem[] = [{
+	const menu: MenuItem[] = [];
+
+	menu.push({
 		type: 'switch',
 		text: i18n.ts.keepOriginalUploading,
 		ref: keepOriginal,
@@ -632,19 +647,62 @@ function getMenu() {
 	}, { type: 'divider' }, {
 		text: folder.value ? folder.value.name : i18n.ts.drive,
 		type: 'label',
-	}, folder.value ? {
-		text: i18n.ts.renameFolder,
-		icon: 'ti ti-forms',
-		action: () => { if (folder.value) renameFolder(folder.value); },
-	} : undefined, folder.value ? {
-		text: i18n.ts.deleteFolder,
-		icon: 'ti ti-trash',
-		action: () => { deleteFolder(folder.value as Misskey.entities.DriveFolder); },
-	} : undefined, {
+	});
+
+	menu.push({
+		type: 'parent',
+		text: i18n.ts.sort,
+		icon: 'ti ti-arrows-sort',
+		children: [{
+			text: `${i18n.ts.registeredDate} (${i18n.ts.descendingOrder})`,
+			icon: 'ti ti-sort-descending-letters',
+			action: () => { sortModeSelect.value = '+createdAt'; },
+			active: sortModeSelect.value === '+createdAt',
+		}, {
+			text: `${i18n.ts.registeredDate} (${i18n.ts.ascendingOrder})`,
+			icon: 'ti ti-sort-ascending-letters',
+			action: () => { sortModeSelect.value = '-createdAt'; },
+			active: sortModeSelect.value === '-createdAt',
+		}, {
+			text: `${i18n.ts.size} (${i18n.ts.descendingOrder})`,
+			icon: 'ti ti-sort-descending-letters',
+			action: () => { sortModeSelect.value = '+size'; },
+			active: sortModeSelect.value === '+size',
+		}, {
+			text: `${i18n.ts.size} (${i18n.ts.ascendingOrder})`,
+			icon: 'ti ti-sort-ascending-letters',
+			action: () => { sortModeSelect.value = '-size'; },
+			active: sortModeSelect.value === '-size',
+		}, {
+			text: `${i18n.ts.name} (${i18n.ts.descendingOrder})`,
+			icon: 'ti ti-sort-descending-letters',
+			action: () => { sortModeSelect.value = '+name'; },
+			active: sortModeSelect.value === '+name',
+		}, {
+			text: `${i18n.ts.name} (${i18n.ts.ascendingOrder})`,
+			icon: 'ti ti-sort-ascending-letters',
+			action: () => { sortModeSelect.value = '-name'; },
+			active: sortModeSelect.value === '-name',
+		}],
+	});
+
+	if (folder.value) {
+		menu.push({
+			text: i18n.ts.renameFolder,
+			icon: 'ti ti-forms',
+			action: () => { if (folder.value) renameFolder(folder.value); },
+		}, {
+			text: i18n.ts.deleteFolder,
+			icon: 'ti ti-trash',
+			action: () => { deleteFolder(folder.value as Misskey.entities.DriveFolder); },
+		});
+	}
+
+	menu.push({
 		text: i18n.ts.createFolder,
 		icon: 'ti ti-folder-plus',
 		action: () => { createFolder(); },
-	}];
+	});
 
 	return menu;
 }
@@ -707,7 +765,7 @@ onBeforeUnmount(() => {
 	box-sizing: border-box;
 	overflow: auto;
 	font-size: 0.9em;
-	box-shadow: 0 1px 0 var(--divider);
+	box-shadow: 0 1px 0 var(--MI_THEME-divider);
 	user-select: none;
 }
 
@@ -754,7 +812,7 @@ onBeforeUnmount(() => {
 .main {
 	flex: 1;
 	overflow: auto;
-	padding: var(--margin);
+	padding: var(--MI-margin);
 	user-select: none;
 
 	&.fetching {
@@ -801,7 +859,7 @@ onBeforeUnmount(() => {
 	top: 38px;
 	width: 100%;
 	height: calc(100% - 38px);
-	border: dashed 2px var(--focus);
+	border: dashed 2px var(--MI_THEME-focus);
 	pointer-events: none;
 }
 </style>
diff --git a/packages/frontend/src/components/MkDriveFileThumbnail.stories.impl.ts b/packages/frontend/src/components/MkDriveFileThumbnail.stories.impl.ts
new file mode 100644
index 0000000000..3fa24d7edb
--- /dev/null
+++ b/packages/frontend/src/components/MkDriveFileThumbnail.stories.impl.ts
@@ -0,0 +1,41 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { StoryObj } from '@storybook/vue3';
+import MkDriveFileThumbnail from './MkDriveFileThumbnail.vue';
+import { file } from '../../.storybook/fakes.js';
+export const Default = {
+	render(args) {
+		return {
+			components: {
+				MkDriveFileThumbnail,
+			},
+			setup() {
+				return {
+					args,
+				};
+			},
+			computed: {
+				props() {
+					return {
+						...this.args,
+					};
+				},
+			},
+			template: '<MkDriveFileThumbnail v-bind="props" />',
+		};
+	},
+	args: {
+		file: file(),
+		fit: 'contain',
+	},
+	parameters: {
+		chromatic: {
+			// NOTE: ロードが終わるまで待つ
+			delay: 3000,
+		},
+		layout: 'centered',
+	},
+} satisfies StoryObj<typeof MkDriveFileThumbnail>;
diff --git a/packages/frontend/src/components/MkDriveFileThumbnail.vue b/packages/frontend/src/components/MkDriveFileThumbnail.vue
index 706c9a55c7..3410a915c3 100644
--- a/packages/frontend/src/components/MkDriveFileThumbnail.vue
+++ b/packages/frontend/src/components/MkDriveFileThumbnail.vue
@@ -4,7 +4,13 @@ SPDX-License-Identifier: AGPL-3.0-only
 -->
 
 <template>
-<div ref="thumbnail" :class="$style.root">
+<div
+	ref="thumbnail"
+	:class="[
+		$style.root,
+		{ [$style.sensitiveHighlight]: highlightWhenSensitive && file.isSensitive },
+	]"
+>
 	<ImgWithBlurhash v-if="isThumbnailAvailable" :hash="file.blurhash" :src="file.thumbnailUrl" :alt="file.name" :title="file.name" :cover="fit !== 'contain'"/>
 	<i v-else-if="is === 'image'" class="ti ti-photo" :class="$style.icon"></i>
 	<i v-else-if="is === 'video'" class="ti ti-video" :class="$style.icon"></i>
@@ -26,7 +32,8 @@ import ImgWithBlurhash from '@/components/MkImgWithBlurhash.vue';
 
 const props = defineProps<{
 	file: Misskey.entities.DriveFile;
-	fit: string;
+	fit: 'cover' | 'contain';
+	highlightWhenSensitive?: boolean;
 }>();
 
 const is = computed(() => {
@@ -62,11 +69,23 @@ const isThumbnailAvailable = computed(() => {
 .root {
 	position: relative;
 	display: flex;
-	background: var(--panel);
+	background: var(--MI_THEME-panel);
 	border-radius: 8px;
 	overflow: clip;
 }
 
+.sensitiveHighlight::after {
+	content: "";
+	position: absolute;
+	top: 0;
+	left: 0;
+	width: 100%;
+	height: 100%;
+	pointer-events: none;
+	border-radius: inherit;
+	box-shadow: inset 0 0 0 4px var(--MI_THEME-warn);
+}
+
 .iconSub {
 	position: absolute;
 	width: 30%;
diff --git a/packages/frontend/src/components/MkDriveSelectDialog.stories.impl.ts b/packages/frontend/src/components/MkDriveSelectDialog.stories.impl.ts
new file mode 100644
index 0000000000..fe8f705165
--- /dev/null
+++ b/packages/frontend/src/components/MkDriveSelectDialog.stories.impl.ts
@@ -0,0 +1,7 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import MkDriveSelectDialog from './MkDriveSelectDialog.vue';
+void MkDriveSelectDialog;
diff --git a/packages/frontend/src/components/MkDriveWindow.stories.impl.ts b/packages/frontend/src/components/MkDriveWindow.stories.impl.ts
new file mode 100644
index 0000000000..faa1f7fd5f
--- /dev/null
+++ b/packages/frontend/src/components/MkDriveWindow.stories.impl.ts
@@ -0,0 +1,7 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import MkDriveWindow from './MkDriveWindow.vue';
+void MkDriveWindow;
diff --git a/packages/frontend/src/components/MkEmbedCodeGenDialog.vue b/packages/frontend/src/components/MkEmbedCodeGenDialog.vue
new file mode 100644
index 0000000000..6e9eb75920
--- /dev/null
+++ b/packages/frontend/src/components/MkEmbedCodeGenDialog.vue
@@ -0,0 +1,414 @@
+<!--
+SPDX-FileCopyrightText: syuilo and misskey-project
+SPDX-License-Identifier: AGPL-3.0-only
+-->
+
+<template>
+<MkModalWindow
+	ref="dialogEl"
+	:width="1000"
+	:height="600"
+	:scroll="false"
+	:withOkButton="false"
+	@close="cancel()"
+	@closed="emit('closed')"
+>
+	<template #header><i class="ti ti-code"></i> {{ i18n.ts._embedCodeGen.title }}</template>
+
+	<div :class="$style.embedCodeGenRoot">
+		<Transition
+			mode="out-in"
+			:enterActiveClass="$style.transition_x_enterActive"
+			:leaveActiveClass="$style.transition_x_leaveActive"
+			:enterFromClass="$style.transition_x_enterFrom"
+			:leaveToClass="$style.transition_x_leaveTo"
+		>
+			<div v-if="phase === 'input'" key="input" :class="$style.embedCodeGenInputRoot">
+				<div
+					:class="$style.embedCodeGenPreviewRoot"
+				>
+					<MkLoading v-if="iframeLoading" :class="$style.embedCodeGenPreviewSpinner"/>
+					<div :class="$style.embedCodeGenPreviewWrapper">
+						<div class="_acrylic" :class="$style.embedCodeGenPreviewTitle">{{ i18n.ts.preview }}</div>
+						<div ref="resizerRootEl" :class="$style.embedCodeGenPreviewResizerRoot" inert>
+							<div
+								:class="$style.embedCodeGenPreviewResizer"
+								:style="{ transform: iframeStyle }"
+							>
+								<iframe
+									ref="iframeEl"
+									:src="embedPreviewUrl"
+									:class="$style.embedCodeGenPreviewIframe"
+									:style="{ height: `${iframeHeight}px` }"
+									@load="iframeOnLoad"
+								></iframe>
+							</div>
+						</div>
+					</div>
+				</div>
+				<div :class="$style.embedCodeGenSettings" class="_gaps">
+					<MkInput v-if="isEmbedWithScrollbar" v-model="maxHeight" type="number" :min="0">
+						<template #label>{{ i18n.ts._embedCodeGen.maxHeight }}</template>
+						<template #suffix>px</template>
+						<template #caption>{{ i18n.ts._embedCodeGen.maxHeightDescription }}</template>
+					</MkInput>
+					<MkSelect v-model="colorMode">
+						<template #label>{{ i18n.ts.theme }}</template>
+						<option value="auto">{{ i18n.ts.syncDeviceDarkMode }}</option>
+						<option value="light">{{ i18n.ts.light }}</option>
+						<option value="dark">{{ i18n.ts.dark }}</option>
+					</MkSelect>
+					<MkSwitch v-if="isEmbedWithScrollbar" v-model="header">{{ i18n.ts._embedCodeGen.header }}</MkSwitch>
+					<MkSwitch v-model="rounded">{{ i18n.ts._embedCodeGen.rounded }}</MkSwitch>
+					<MkSwitch v-model="border">{{ i18n.ts._embedCodeGen.border }}</MkSwitch>
+					<MkInfo v-if="isEmbedWithScrollbar && (!maxHeight || maxHeight <= 0)" warn>{{ i18n.ts._embedCodeGen.maxHeightWarn }}</MkInfo>
+					<MkInfo v-if="typeof maxHeight === 'number' && (maxHeight <= 0 || maxHeight > 700)">{{ i18n.ts._embedCodeGen.previewIsNotActual }}</MkInfo>
+					<div class="_buttons">
+						<MkButton :disabled="iframeLoading" @click="applyToPreview">{{ i18n.ts._embedCodeGen.applyToPreview }}</MkButton>
+						<MkButton :disabled="iframeLoading" primary @click="generate">{{ i18n.ts._embedCodeGen.generateCode }} <i class="ti ti-arrow-right"></i></MkButton>
+					</div>
+				</div>
+			</div>
+			<div v-else-if="phase === 'result'" key="result" :class="$style.embedCodeGenResultRoot">
+				<div :class="$style.embedCodeGenResultWrapper" class="_gaps">
+					<div class="_gaps_s">
+						<div :class="$style.embedCodeGenResultHeadingIcon"><i class="ti ti-check"></i></div>
+						<div :class="$style.embedCodeGenResultHeading">{{ i18n.ts._embedCodeGen.codeGenerated }}</div>
+						<div :class="$style.embedCodeGenResultDescription">{{ i18n.ts._embedCodeGen.codeGeneratedDescription }}</div>
+					</div>
+					<div class="_gaps_s">
+						<MkCode :code="result" lang="html" :forceShow="true" :copyButton="false" :class="$style.embedCodeGenResultCode"/>
+						<MkButton :class="$style.embedCodeGenResultButtons" rounded primary @click="doCopy"><i class="ti ti-copy"></i> {{ i18n.ts.copy }}</MkButton>
+					</div>
+					<MkButton :class="$style.embedCodeGenResultButtons" rounded transparent @click="close">{{ i18n.ts.close }}</MkButton>
+				</div>
+			</div>
+		</Transition>
+	</div>
+</MkModalWindow>
+</template>
+
+<script setup lang="ts">
+import { shallowRef, ref, computed, nextTick, onMounted, onDeactivated, onUnmounted } from 'vue';
+import { url } from '@@/js/config.js';
+import { embedRouteWithScrollbar } from '@@/js/embed-page.js';
+import type { EmbeddableEntity, EmbedParams } from '@@/js/embed-page.js';
+import MkModalWindow from '@/components/MkModalWindow.vue';
+
+import MkInput from '@/components/MkInput.vue';
+import MkSelect from '@/components/MkSelect.vue';
+import MkSwitch from '@/components/MkSwitch.vue';
+import MkButton from '@/components/MkButton.vue';
+
+import MkCode from '@/components/MkCode.vue';
+import MkInfo from '@/components/MkInfo.vue';
+
+import * as os from '@/os.js';
+import { i18n } from '@/i18n.js';
+import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
+import { normalizeEmbedParams, getEmbedCode } from '@/scripts/get-embed-code.js';
+
+const emit = defineEmits<{
+	(ev: 'ok'): void;
+	(ev: 'cancel'): void;
+	(ev: 'closed'): void;
+}>();
+
+const props = defineProps<{
+	entity: EmbeddableEntity;
+	id: string;
+	params?: EmbedParams;
+}>();
+
+//#region Modalの制御
+const dialogEl = shallowRef<InstanceType<typeof MkModalWindow>>();
+
+function cancel() {
+	emit('cancel');
+	dialogEl.value?.close();
+}
+
+function close() {
+	dialogEl.value?.close();
+}
+
+const phase = ref<'input' | 'result'>('input');
+//#endregion
+
+//#region 埋め込みURL生成・カスタマイズ
+
+// 本URL生成用params
+const paramsForUrl = computed<EmbedParams>(() => ({
+	header: header.value,
+	maxHeight: typeof maxHeight.value === 'number' ? Math.max(0, maxHeight.value) : undefined,
+	colorMode: colorMode.value === 'auto' ? undefined : colorMode.value,
+	rounded: rounded.value,
+	border: border.value,
+}));
+
+// プレビュー用params(手動で更新を掛けるのでref)
+const paramsForPreview = ref<EmbedParams>(props.params ?? {});
+
+const embedPreviewUrl = computed(() => {
+	const paramClass = new URLSearchParams(normalizeEmbedParams(paramsForPreview.value));
+	if (paramClass.has('maxHeight')) {
+		const maxHeight = parseInt(paramClass.get('maxHeight')!);
+		paramClass.set('maxHeight', maxHeight === 0 ? '500' : Math.min(maxHeight, 700).toString()); // プレビューであまりにも縮小されると見づらいため、700pxまでに制限
+	}
+	return `${url}/embed/${props.entity}/${props.id}${paramClass.toString() ? '?' + paramClass.toString() : ''}`;
+});
+
+const isEmbedWithScrollbar = computed(() => embedRouteWithScrollbar.includes(props.entity));
+const header = ref(props.params?.header ?? true);
+const maxHeight = ref(props.params?.maxHeight !== 0 ? props.params?.maxHeight ?? undefined : 500);
+
+const colorMode = ref<'light' | 'dark' | 'auto'>(props.params?.colorMode ?? 'auto');
+const rounded = ref(props.params?.rounded ?? true);
+const border = ref(props.params?.border ?? true);
+
+function applyToPreview() {
+	const currentPreviewUrl = embedPreviewUrl.value;
+
+	paramsForPreview.value = {
+		header: header.value,
+		maxHeight: typeof maxHeight.value === 'number' ? Math.max(0, maxHeight.value) : undefined,
+		colorMode: colorMode.value === 'auto' ? undefined : colorMode.value,
+		rounded: rounded.value,
+		border: border.value,
+	};
+
+	nextTick(() => {
+		if (currentPreviewUrl === embedPreviewUrl.value) {
+			// URLが変わらなくてもリロード
+			iframeEl.value?.contentWindow?.location.reload();
+		}
+	});
+}
+
+const result = ref('');
+
+function generate() {
+	result.value = getEmbedCode(`/embed/${props.entity}/${props.id}`, paramsForUrl.value);
+	phase.value = 'result';
+}
+
+function doCopy() {
+	copyToClipboard(result.value);
+	os.success();
+}
+//#endregion
+
+//#region プレビューのリサイズ
+const resizerRootEl = shallowRef<HTMLDivElement>();
+const iframeLoading = ref(true);
+const iframeEl = shallowRef<HTMLIFrameElement>();
+const iframeHeight = ref(0);
+const iframeScale = ref(1);
+const iframeStyle = computed(() => {
+	return `translate(-50%, -50%) scale(${iframeScale.value})`;
+});
+const resizeObserver = new ResizeObserver(() => {
+	calcScale();
+});
+
+function iframeOnLoad() {
+	iframeEl.value?.contentWindow?.addEventListener('beforeunload', () => {
+		iframeLoading.value = true;
+		nextTick(() => {
+			iframeHeight.value = 0;
+			iframeScale.value = 1;
+		});
+	});
+}
+
+function windowEventHandler(event: MessageEvent) {
+	if (event.source !== iframeEl.value?.contentWindow) {
+		return;
+	}
+	if (event.data.type === 'misskey:embed:ready') {
+		iframeEl.value!.contentWindow?.postMessage({
+			type: 'misskey:embedParent:registerIframeId',
+			payload: {
+				iframeId: 'embedCodeGen', // 同じタイミングで複数のembed iframeがある際の区別用なのでここではなんでもいい
+			},
+		});
+	}
+	if (event.data.type === 'misskey:embed:changeHeight') {
+		iframeHeight.value = event.data.payload.height;
+		nextTick(() => {
+			calcScale();
+			iframeLoading.value = false; // 初回の高さ変更まで待つ
+		});
+	}
+}
+
+function calcScale() {
+	if (!resizerRootEl.value) return;
+	const previewWidth = resizerRootEl.value.clientWidth - 40; // 左右の余白 20pxずつ
+	const previewHeight = resizerRootEl.value.clientHeight - 40; // 上下の余白 20pxずつ
+	const iframeWidth = 500;
+	const scale = Math.min(previewWidth / iframeWidth, previewHeight / iframeHeight.value, 1); // 拡大はしないので1を上限に
+	iframeScale.value = scale;
+}
+
+onMounted(() => {
+	window.addEventListener('message', windowEventHandler);
+	if (!resizerRootEl.value) return;
+	resizeObserver.observe(resizerRootEl.value);
+});
+
+function reset() {
+	window.removeEventListener('message', windowEventHandler);
+	resizeObserver.disconnect();
+
+	// プレビューのリセット
+	iframeHeight.value = 0;
+	iframeScale.value = 1;
+	iframeLoading.value = true;
+	result.value = '';
+	phase.value = 'input';
+}
+
+onDeactivated(() => {
+	reset();
+});
+
+onUnmounted(() => {
+	reset();
+});
+//#endregion
+</script>
+
+<style module>
+.transition_x_enterActive,
+.transition_x_leaveActive {
+	transition: opacity 0.3s cubic-bezier(0,0,.35,1), transform 0.3s cubic-bezier(0,0,.35,1);
+}
+.transition_x_enterFrom {
+	opacity: 0;
+	transform: translateX(50px);
+}
+.transition_x_leaveTo {
+	opacity: 0;
+	transform: translateX(-50px);
+}
+
+.embedCodeGenRoot {
+	container-type: inline-size;
+	height: 100%;
+}
+
+.embedCodeGenInputRoot {
+	height: 100%;
+	display: grid;
+	grid-template-columns: 1fr 400px;
+}
+
+.embedCodeGenPreviewRoot {
+	position: relative;
+	background-color: var(--MI_THEME-bg);
+	background-size: auto auto;
+	background-image: repeating-linear-gradient(135deg, transparent, transparent 6px, var(--MI_THEME-panel) 6px, var(--MI_THEME-panel) 12px);
+	cursor: not-allowed;
+}
+
+.embedCodeGenPreviewWrapper {
+	display: flex;
+	flex-direction: column;
+	height: 100%;
+	pointer-events: none;
+	user-select: none;
+	-webkit-user-drag: none;
+}
+
+.embedCodeGenPreviewTitle {
+	position: absolute;
+	z-index: 100;
+	top: 8px;
+	left: 8px;
+	padding: 6px 10px;
+	border-radius: 6px;
+	font-size: 85%;
+}
+
+.embedCodeGenPreviewSpinner {
+	position: absolute;
+	top: 50%;
+	left: 50%;
+	transform: translate(-50%, -50%);
+	pointer-events: none;
+	user-select: none;
+	-webkit-user-drag: none;
+}
+
+.embedCodeGenPreviewResizerRoot {
+	position: relative;
+	flex: 1 0;
+}
+
+.embedCodeGenPreviewResizer {
+	position: absolute;
+	top: 50%;
+	left: 50%;
+}
+
+.embedCodeGenPreviewIframe {
+	display: block;
+	border: none;
+	width: 500px;
+	color-scheme: light dark;
+}
+
+.embedCodeGenSettings {
+	padding: 24px;
+	overflow-y: scroll;
+}
+
+.embedCodeGenResultRoot {
+	box-sizing: border-box;
+	padding: 24px;
+	height: 100%;
+	max-width: 700px;
+	margin: 0 auto;
+	display: flex;
+	align-items: center;
+}
+
+.embedCodeGenResultHeading {
+	text-align: center;
+	font-size: 1.2em;
+}
+
+.embedCodeGenResultHeadingIcon {
+	margin: 0 auto;
+	background-color: var(--MI_THEME-accentedBg);
+	color: var(--MI_THEME-accent);
+	text-align: center;
+	height: 64px;
+	width: 64px;
+	font-size: 24px;
+	line-height: 64px;
+	border-radius: 50%;
+}
+
+.embedCodeGenResultDescription {
+	text-align: center;
+	white-space: pre-wrap;
+}
+
+.embedCodeGenResultWrapper,
+.embedCodeGenResultCode {
+	width: 100%;
+}
+
+.embedCodeGenResultButtons {
+	margin: 0 auto;
+}
+
+@container (max-width: 800px) {
+	.embedCodeGenInputRoot {
+		grid-template-columns: 1fr;
+		grid-template-rows: 1fr 1fr;
+	}
+}
+</style>
diff --git a/packages/frontend/src/components/MkEmojiPicker.section.stories.impl.ts b/packages/frontend/src/components/MkEmojiPicker.section.stories.impl.ts
new file mode 100644
index 0000000000..69aef577de
--- /dev/null
+++ b/packages/frontend/src/components/MkEmojiPicker.section.stories.impl.ts
@@ -0,0 +1,7 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import MkEmojiPicker_section from './MkEmojiPicker.section.vue';
+void MkEmojiPicker_section;
diff --git a/packages/frontend/src/components/MkEmojiPicker.section.vue b/packages/frontend/src/components/MkEmojiPicker.section.vue
index c13164c296..b418ed3ae6 100644
--- a/packages/frontend/src/components/MkEmojiPicker.section.vue
+++ b/packages/frontend/src/components/MkEmojiPicker.section.vue
@@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 <template>
 <!-- このコンポーネントの要素のclassは親から利用されるのでむやみに弄らないこと -->
 <!-- フォルダの中にはカスタム絵文字だけ(Unicode絵文字もこっち) -->
-<section v-if="!hasChildSection" v-panel style="border-radius: 6px; border-bottom: 0.5px solid var(--divider);">
+<section v-if="!hasChildSection" v-panel style="border-radius: 6px; border-bottom: 0.5px solid var(--MI_THEME-divider);">
 	<header class="_acrylic" @click="shown = !shown">
 		<i class="toggle ti-fw" :class="shown ? 'ti ti-chevron-down' : 'ti ti-chevron-up'"></i> <slot></slot> (<i class="ti ti-icons"></i>:{{ emojis.length }})
 	</header>
@@ -26,7 +26,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 	</div>
 </section>
 <!-- フォルダの中にはカスタム絵文字やフォルダがある -->
-<section v-else v-panel style="border-radius: 6px; border-bottom: 0.5px solid var(--divider);">
+<section v-else v-panel style="border-radius: 6px; border-bottom: 0.5px solid var(--MI_THEME-divider);">
 	<header class="_acrylic" @click="shown = !shown">
 		<i class="toggle ti-fw" :class="shown ? 'ti ti-chevron-down' : 'ti ti-chevron-up'"></i> <slot></slot> (<i class="ti ti-folder ti-fw"></i>:{{ customEmojiTree?.length }} <i class="ti ti-icons ti-fw"></i>:{{ emojis.length }})
 	</header>
@@ -62,7 +62,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <script lang="ts" setup>
 import { ref, computed, Ref } from 'vue';
-import { CustomEmojiFolderTree, getEmojiName } from '@/scripts/emojilist.js';
+import { CustomEmojiFolderTree, getEmojiName } from '@@/js/emojilist.js';
 import { i18n } from '@/i18n.js';
 import { customEmojis } from '@/custom-emojis.js';
 import MkEmojiPickerSection from '@/components/MkEmojiPicker.section.vue';
@@ -90,7 +90,7 @@ function computeButtonTitle(ev: MouseEvent): void {
 	elm.title = getEmojiName(emoji);
 }
 
-function nestedChosen(emoji: any, ev: MouseEvent) {
+function nestedChosen(emoji: string, ev: MouseEvent) {
 	emit('chosen', emoji, ev);
 }
 </script>
diff --git a/packages/frontend/src/components/MkEmojiPicker.stories.impl.ts b/packages/frontend/src/components/MkEmojiPicker.stories.impl.ts
new file mode 100644
index 0000000000..d38d8de808
--- /dev/null
+++ b/packages/frontend/src/components/MkEmojiPicker.stories.impl.ts
@@ -0,0 +1,54 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { action } from '@storybook/addon-actions';
+import { expect, userEvent, waitFor, within } from '@storybook/test';
+import { StoryObj } from '@storybook/vue3';
+import { i18n } from '@/i18n.js';
+import MkEmojiPicker from './MkEmojiPicker.vue';
+export const Default = {
+	render(args) {
+		return {
+			components: {
+				MkEmojiPicker,
+			},
+			setup() {
+				return {
+					args,
+				};
+			},
+			computed: {
+				props() {
+					return {
+						...this.args,
+					};
+				},
+				events() {
+					return {
+						chosen: action('chosen'),
+					};
+				},
+			},
+			template: '<MkEmojiPicker v-bind="props" v-on="events" />',
+		};
+	},
+	async play({ canvasElement }) {
+		const canvas = within(canvasElement);
+		const faceSection = canvas.getByText(/face/i);
+		await waitFor(() => userEvent.click(faceSection));
+		const grinning = canvasElement.querySelector('[data-emoji="😀"]');
+		await expect(grinning).toBeInTheDocument();
+		if (grinning == null) throw new Error(); // NOTE: not called
+		await waitFor(() => userEvent.click(grinning));
+		const recentUsedSection = canvas.getByText(new RegExp(i18n.ts.recentUsed)).parentElement;
+		await expect(recentUsedSection).toBeInTheDocument();
+		if (recentUsedSection == null) throw new Error(); // NOTE: not called
+		await expect(within(recentUsedSection).getByAltText('😀')).toBeInTheDocument();
+		await expect(within(recentUsedSection).queryByAltText('😬')).toEqual(null);
+	},
+	parameters: {
+		layout: 'centered',
+	},
+} satisfies StoryObj<typeof MkEmojiPicker>;
diff --git a/packages/frontend/src/components/MkEmojiPicker.vue b/packages/frontend/src/components/MkEmojiPicker.vue
index 8a6bef54d8..8187d991e7 100644
--- a/packages/frontend/src/components/MkEmojiPicker.vue
+++ b/packages/frontend/src/components/MkEmojiPicker.vue
@@ -5,7 +5,19 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <template>
 <div class="omfetrab" :class="['s' + size, 'w' + width, 'h' + height, { asDrawer, asWindow }]" :style="{ maxHeight: maxHeight ? maxHeight + 'px' : undefined }">
-	<input ref="searchEl" :value="q" class="search" data-prevent-emoji-insert :class="{ filled: q != null && q != '' }" :placeholder="i18n.ts.search" type="search" autocapitalize="off" @input="input()" @paste.stop="paste" @keydown.stop.prevent.enter="onEnter">
+	<input
+		ref="searchEl"
+		:value="q"
+		class="search"
+		data-prevent-emoji-insert
+		:class="{ filled: q != null && q != '' }"
+		:placeholder="i18n.ts.search"
+		type="search"
+		autocapitalize="off"
+		@input="input()"
+		@paste.stop="paste"
+		@keydown="onKeydown"
+	>
 	<!-- FirefoxのTabフォーカスが想定外の挙動となるためtabindex="-1"を追加 https://github.com/misskey-dev/misskey/issues/10744 -->
 	<div ref="emojisEl" class="emojis" tabindex="-1">
 		<section class="result">
@@ -105,7 +117,6 @@ SPDX-License-Identifier: AGPL-3.0-only
 <script lang="ts" setup>
 import { ref, shallowRef, computed, watch, onMounted } from 'vue';
 import * as Misskey from 'misskey-js';
-import XSection from '@/components/MkEmojiPicker.section.vue';
 import {
 	emojilist,
 	emojiCharByCategory,
@@ -114,7 +125,8 @@ import {
 	getEmojiName,
 	CustomEmojiFolderTree,
 	getUnicodeEmoji,
-} from '@/scripts/emojilist.js';
+} from '@@/js/emojilist.js';
+import XSection from '@/components/MkEmojiPicker.section.vue';
 import MkRippleEffect from '@/components/MkRippleEffect.vue';
 import * as os from '@/os.js';
 import { isTouchUsing } from '@/scripts/touch.js';
@@ -139,6 +151,7 @@ const props = withDefaults(defineProps<{
 
 const emit = defineEmits<{
 	(ev: 'chosen', v: string): void;
+	(ev: 'esc'): void;
 }>();
 
 const searchEl = shallowRef<HTMLInputElement>();
@@ -396,13 +409,15 @@ function computeButtonTitle(ev: MouseEvent): void {
 	elm.title = getEmojiName(emoji);
 }
 
-function chosen(emoji: any, ev?: MouseEvent) {
+function chosen(emoji: string | Misskey.entities.EmojiSimple | UnicodeEmojiDef, ev?: MouseEvent) {
 	const el = ev && (ev.currentTarget ?? ev.target) as HTMLElement | null | undefined;
 	if (el) {
 		const rect = el.getBoundingClientRect();
 		const x = rect.left + (el.offsetWidth / 2);
 		const y = rect.top + (el.offsetHeight / 2);
-		os.popup(MkRippleEffect, { x, y }, {}, 'end');
+		const { dispose } = os.popup(MkRippleEffect, { x, y }, {
+			end: () => dispose(),
+		});
 	}
 
 	const key = getKey(emoji);
@@ -411,7 +426,7 @@ function chosen(emoji: any, ev?: MouseEvent) {
 	// 最近使った絵文字更新
 	if (!pinned.value?.includes(key)) {
 		let recents = defaultStore.state.recentlyUsedEmojis;
-		recents = recents.filter((emoji: any) => emoji !== key);
+		recents = recents.filter((emoji) => emoji !== key);
 		recents.unshift(key);
 		defaultStore.set('recentlyUsedEmojis', recents.splice(0, 32));
 	}
@@ -431,9 +446,18 @@ function paste(event: ClipboardEvent): void {
 	}
 }
 
-function onEnter(ev: KeyboardEvent) {
+function onKeydown(ev: KeyboardEvent) {
 	if (ev.isComposing || ev.key === 'Process' || ev.keyCode === 229) return;
-	done();
+	if (ev.key === 'Enter') {
+		ev.preventDefault();
+		ev.stopPropagation();
+		done();
+	}
+	if (ev.key === 'Escape') {
+		ev.preventDefault();
+		ev.stopPropagation();
+		emit('esc');
+	}
 }
 
 function done(query?: string): boolean | void {
@@ -556,7 +580,7 @@ defineExpose({
 
 						&:disabled {
 							cursor: not-allowed;
-							background: linear-gradient(-45deg, transparent 0% 48%, var(--X6) 48% 52%, transparent 52% 100%);
+							background: linear-gradient(-45deg, transparent 0% 48%, var(--MI_THEME-X6) 48% 52%, transparent 52% 100%);
 							opacity: 1;
 
 							> .emoji {
@@ -587,10 +611,11 @@ defineExpose({
 						width: auto;
 						height: auto;
 						min-width: 0;
+						padding: 0;
 
 						&:disabled {
 							cursor: not-allowed;
-							background: linear-gradient(-45deg, transparent 0% 48%, var(--X6) 48% 52%, transparent 52% 100%);
+							background: linear-gradient(-45deg, transparent 0% 48%, var(--MI_THEME-X6) 48% 52%, transparent 52% 100%);
 							opacity: 1;
 
 							> .emoji {
@@ -613,7 +638,7 @@ defineExpose({
 		outline: none;
 		border: none;
 		background: transparent;
-		color: var(--fg);
+		color: var(--MI_THEME-fg);
 
 		&:not(:focus):not(.filled) {
 			margin-bottom: env(safe-area-inset-bottom, 0px);
@@ -622,7 +647,7 @@ defineExpose({
 		&:not(.filled) {
 			order: 1;
 			z-index: 2;
-			box-shadow: 0px -1px 0 0px var(--divider);
+			box-shadow: 0px -1px 0 0px var(--MI_THEME-divider);
 		}
 	}
 
@@ -633,11 +658,11 @@ defineExpose({
 		> .tab {
 			flex: 1;
 			height: 38px;
-			border-top: solid 0.5px var(--divider);
+			border-top: solid 0.5px var(--MI_THEME-divider);
 
 			&.active {
-				border-top: solid 1px var(--accent);
-				color: var(--accent);
+				border-top: solid 1px var(--MI_THEME-accent);
+				color: var(--MI_THEME-accent);
 			}
 		}
 	}
@@ -656,7 +681,7 @@ defineExpose({
 		> .group {
 			&:not(.index) {
 				padding: 4px 0 8px 0;
-				border-top: solid 0.5px var(--divider);
+				border-top: solid 0.5px var(--MI_THEME-divider);
 			}
 
 			> header {
@@ -683,7 +708,7 @@ defineExpose({
 				cursor: pointer;
 
 				&:hover {
-					color: var(--accent);
+					color: var(--MI_THEME-accent);
 				}
 			}
 
@@ -693,30 +718,25 @@ defineExpose({
 
 				> .item {
 					position: relative;
-					padding: 0;
+					padding: 0 3px;
 					width: var(--eachSize);
 					height: var(--eachSize);
 					contain: strict;
 					border-radius: 4px;
 					font-size: 24px;
 
-					&:focus-visible {
-						outline: solid 2px var(--focus);
-						z-index: 1;
-					}
-
 					&:hover {
 						background: rgba(0, 0, 0, 0.05);
 					}
 
 					&:active {
-						background: var(--accent);
+						background: var(--MI_THEME-accent);
 						box-shadow: inset 0 0.15em 0.3em rgba(27, 31, 35, 0.15);
 					}
 
 					&:disabled {
 						cursor: not-allowed;
-						background: linear-gradient(-45deg, transparent 0% 48%, var(--X6) 48% 52%, transparent 52% 100%);
+						background: linear-gradient(-45deg, transparent 0% 48%, var(--MI_THEME-X6) 48% 52%, transparent 52% 100%);
 						opacity: 1;
 
 						> .emoji {
@@ -737,7 +757,7 @@ defineExpose({
 			}
 
 			&.result {
-				border-bottom: solid 0.5px var(--divider);
+				border-bottom: solid 0.5px var(--MI_THEME-divider);
 
 				&:empty {
 					display: none;
diff --git a/packages/frontend/src/components/MkEmojiPickerDialog.stories.impl.ts b/packages/frontend/src/components/MkEmojiPickerDialog.stories.impl.ts
new file mode 100644
index 0000000000..131087ad45
--- /dev/null
+++ b/packages/frontend/src/components/MkEmojiPickerDialog.stories.impl.ts
@@ -0,0 +1,7 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import MkEmojiPickerDialog from './MkEmojiPickerDialog.vue';
+void MkEmojiPickerDialog;
diff --git a/packages/frontend/src/components/MkEmojiPickerDialog.vue b/packages/frontend/src/components/MkEmojiPickerDialog.vue
index adcea839ee..21c712b441 100644
--- a/packages/frontend/src/components/MkEmojiPickerDialog.vue
+++ b/packages/frontend/src/components/MkEmojiPickerDialog.vue
@@ -8,11 +8,13 @@ SPDX-License-Identifier: AGPL-3.0-only
 	ref="modal"
 	v-slot="{ type, maxHeight }"
 	:zPriority="'middle'"
-	:preferType="defaultStore.state.emojiPickerUseDrawerForMobile === false ? 'popup' : 'auto'"
+	:preferType="defaultStore.state.emojiPickerStyle"
+	:hasInteractionWithOtherFocusTrappedEls="true"
 	:transparentBg="true"
 	:manualShowing="manualShowing"
 	:src="src"
 	@click="modal?.close()"
+	@esc="modal?.close()"
 	@opening="opening"
 	@close="emit('close')"
 	@closed="emit('closed')"
@@ -28,6 +30,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 		:asDrawer="type === 'drawer'"
 		:max-height="maxHeight"
 		@chosen="chosen"
+		@esc="modal?.close()"
 	/>
 </MkModal>
 </template>
diff --git a/packages/frontend/src/components/MkExtensionInstaller.stories.impl.ts b/packages/frontend/src/components/MkExtensionInstaller.stories.impl.ts
new file mode 100644
index 0000000000..6763f7c546
--- /dev/null
+++ b/packages/frontend/src/components/MkExtensionInstaller.stories.impl.ts
@@ -0,0 +1,83 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { StoryObj } from '@storybook/vue3';
+import MkExtensionInstaller from './MkExtensionInstaller.vue';
+import lightTheme from '@@/themes/_light.json5';
+
+export const Plugin = {
+	render(args) {
+		return {
+			components: {
+				MkExtensionInstaller,
+			},
+			setup() {
+				return {
+					args,
+				};
+			},
+			computed: {
+				props() {
+					return {
+						...this.args,
+					};
+				},
+			},
+			template: '<MkExtensionInstaller v-bind="props" />',
+		};
+	},
+	args: {
+		extension: {
+			type: 'plugin',
+			raw: '"do nothing"',
+			meta: {
+				name: 'do nothing plugin',
+				version: '1.0',
+				author: 'syuilo and misskey-project',
+				description: 'a plugin that does nothing',
+				permissions: ['read:account'],
+				config: {
+					'doNothing': true,
+				},
+			},
+		},
+	},
+	parameters: {
+		layout: 'centered',
+	},
+} satisfies StoryObj<typeof MkExtensionInstaller>;
+
+export const Theme = {
+	render(args) {
+		return {
+			components: {
+				MkExtensionInstaller,
+			},
+			setup() {
+				return {
+					args,
+				};
+			},
+			computed: {
+				props() {
+					return {
+						...this.args,
+					};
+				},
+			},
+			template: '<MkExtensionInstaller v-bind="props" />',
+		};
+	},
+	args: {
+		extension: {
+			type: 'theme',
+			raw: JSON.stringify(lightTheme),
+			meta: lightTheme,
+		},
+	},
+	parameters: {
+		layout: 'centered',
+	},
+} satisfies StoryObj<typeof MkExtensionInstaller>;
diff --git a/packages/frontend/src/components/MkExtensionInstaller.vue b/packages/frontend/src/components/MkExtensionInstaller.vue
new file mode 100644
index 0000000000..d59b20435e
--- /dev/null
+++ b/packages/frontend/src/components/MkExtensionInstaller.vue
@@ -0,0 +1,146 @@
+<!--
+SPDX-FileCopyrightText: syuilo and misskey-project
+SPDX-License-Identifier: AGPL-3.0-only
+-->
+
+<template>
+<div class="_gaps_m" :class="$style.extInstallerRoot">
+	<div :class="$style.extInstallerIconWrapper">
+		<i v-if="isPlugin" class="ti ti-plug"></i>
+		<i v-else-if="isTheme" class="ti ti-palette"></i>
+		<!-- 拡張用? -->
+		<i v-else class="ti ti-download"></i>
+	</div>
+	<h2 :class="$style.extInstallerTitle">{{ i18n.ts._externalResourceInstaller[`_${extension.type}`].title }}</h2>
+	<div :class="$style.extInstallerNormDesc">{{ i18n.ts._externalResourceInstaller.checkVendorBeforeInstall }}</div>
+	<MkInfo v-if="isPlugin" :warn="true">{{ i18n.ts._plugin.installWarn }}</MkInfo>
+	<FormSection>
+		<template #label>{{ i18n.ts._externalResourceInstaller[`_${extension.type}`].metaTitle }}</template>
+		<div class="_gaps_s">
+			<FormSplit>
+				<MkKeyValue>
+					<template #key>{{ i18n.ts.name }}</template>
+					<template #value>{{ extension.meta.name }}</template>
+				</MkKeyValue>
+				<MkKeyValue>
+					<template #key>{{ i18n.ts.author }}</template>
+					<template #value>{{ extension.meta.author }}</template>
+				</MkKeyValue>
+			</FormSplit>
+			<MkKeyValue v-if="isPlugin">
+				<template #key>{{ i18n.ts.description }}</template>
+				<template #value>{{ extension.meta.description ?? i18n.ts.none }}</template>
+			</MkKeyValue>
+			<MkKeyValue v-if="isPlugin">
+				<template #key>{{ i18n.ts.version }}</template>
+				<template #value>{{ extension.meta.version }}</template>
+			</MkKeyValue>
+			<MkKeyValue v-if="isPlugin">
+				<template #key>{{ i18n.ts.permission }}</template>
+				<template #value>
+					<ul v-if="extension.meta.permissions && extension.meta.permissions.length > 0" :class="$style.extInstallerKVList">
+						<li v-for="permission in extension.meta.permissions" :key="permission">{{ i18n.ts._permissions[permission] }}</li>
+					</ul>
+					<template v-else>{{ i18n.ts.none }}</template>
+				</template>
+			</MkKeyValue>
+			<MkKeyValue v-if="isTheme">
+				<template #key>{{ i18n.ts._externalResourceInstaller._meta.base }}</template>
+				<template #value>{{ i18n.ts[extension.meta.base ?? 'none'] }}</template>
+			</MkKeyValue>
+			<MkFolder>
+				<template #icon><i class="ti ti-code"></i></template>
+				<template #label>{{ i18n.ts._plugin.viewSource }}</template>
+
+				<MkCode :code="extension.raw"/>
+			</MkFolder>
+		</div>
+	</FormSection>
+	<slot name="additionalInfo"/>
+	<div class="_buttonsCenter">
+		<MkButton primary @click="emits('confirm')"><i class="ti ti-check"></i> {{ i18n.ts.install }}</MkButton>
+	</div>
+</div>
+</template>
+
+<script lang="ts">
+export type Extension = {
+	type: 'plugin';
+	raw: string;
+	meta: {
+		name: string;
+		version: string;
+		author: string;
+		description?: string;
+		permissions?: string[];
+		config?: Record<string, unknown>;
+	};
+} | {
+	type: 'theme';
+	raw: string;
+	meta: {
+		name: string;
+		author: string;
+		base?: 'light' | 'dark';
+	};
+};
+</script>
+<script lang="ts" setup>
+import { computed } from 'vue';
+import MkButton from '@/components/MkButton.vue';
+import FormSection from '@/components/form/section.vue';
+import FormSplit from '@/components/form/split.vue';
+import MkCode from '@/components/MkCode.vue';
+import MkInfo from '@/components/MkInfo.vue';
+import MkFolder from '@/components/MkFolder.vue';
+import MkKeyValue from '@/components/MkKeyValue.vue';
+import { i18n } from '@/i18n.js';
+
+const isPlugin = computed(() => props.extension.type === 'plugin');
+const isTheme = computed(() => props.extension.type === 'theme');
+
+const props = defineProps<{
+	extension: Extension;
+}>();
+
+const emits = defineEmits<{
+	(ev: 'confirm'): void;
+}>();
+</script>
+
+<style lang="scss" module>
+.extInstallerRoot {
+	border-radius: var(--MI-radius);
+	background: var(--MI_THEME-panel);
+	padding: 1.5rem;
+}
+
+.extInstallerIconWrapper {
+	width: 48px;
+	height: 48px;
+	font-size: 24px;
+	line-height: 48px;
+	text-align: center;
+	border-radius: 50%;
+	margin-left: auto;
+	margin-right: auto;
+
+	background-color: var(--MI_THEME-accentedBg);
+	color: var(--MI_THEME-accent);
+}
+
+.extInstallerTitle {
+	font-size: 1.2rem;
+	text-align: center;
+	margin: 0;
+}
+
+.extInstallerNormDesc {
+	text-align: center;
+}
+
+.extInstallerKVList {
+	margin-top: 0;
+	margin-bottom: 0;
+}
+</style>
diff --git a/packages/frontend/src/components/MkFileListForAdmin.vue b/packages/frontend/src/components/MkFileListForAdmin.vue
index 30822ef655..d5d32ebb28 100644
--- a/packages/frontend/src/components/MkFileListForAdmin.vue
+++ b/packages/frontend/src/components/MkFileListForAdmin.vue
@@ -14,7 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 			class="file _button"
 		>
 			<div v-if="file.isSensitive" class="sensitive-label">{{ i18n.ts.sensitive }}</div>
-			<MkDriveFileThumbnail class="thumbnail" :file="file" fit="contain"/>
+			<MkDriveFileThumbnail class="thumbnail" :file="file" fit="contain" :highlightWhenSensitive="true"/>
 			<div v-if="viewMode === 'list'" class="body">
 				<div>
 					<small style="opacity: 0.7;">{{ file.name }}</small>
@@ -66,7 +66,7 @@ const props = defineProps<{
 			align-items: center;
 
 			&:hover {
-				color: var(--accent);
+				color: var(--MI_THEME-accent);
 			}
 
 			> .thumbnail {
diff --git a/packages/frontend/src/components/MkFlashPreview.stories.impl.ts b/packages/frontend/src/components/MkFlashPreview.stories.impl.ts
new file mode 100644
index 0000000000..fa5288b73d
--- /dev/null
+++ b/packages/frontend/src/components/MkFlashPreview.stories.impl.ts
@@ -0,0 +1,53 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { StoryObj } from '@storybook/vue3';
+import MkFlashPreview from './MkFlashPreview.vue';
+import { flash } from './../../.storybook/fakes.js';
+export const Public = {
+	render(args) {
+		return {
+			components: {
+				MkFlashPreview,
+			},
+			setup() {
+				return {
+					args,
+				};
+			},
+			computed: {
+				props() {
+					return {
+						...this.args,
+					};
+				},
+			},
+			template: '<MkFlashPreview v-bind="props" />',
+		};
+	},
+	args: {
+		flash: {
+			...flash(),
+			visibility: 'public',
+		},
+	},
+	parameters: {
+		layout: 'fullscreen',
+	},
+	decorators: [
+		() => ({
+			template: '<div style="display: flex; align-items: center; justify-content: center; height: 100vh"><div style="max-width: 700px; width: 100%; margin: 3rem"><story/></div></div>',
+		}),
+	],
+} satisfies StoryObj<typeof MkFlashPreview>;
+export const Private = {
+	...Public,
+	args: {
+		flash: {
+			...flash(),
+			visibility: 'private',
+		},
+	},
+} satisfies StoryObj<typeof MkFlashPreview>;
diff --git a/packages/frontend/src/components/MkFlashPreview.vue b/packages/frontend/src/components/MkFlashPreview.vue
index c5dd877971..b7278ac742 100644
--- a/packages/frontend/src/components/MkFlashPreview.vue
+++ b/packages/frontend/src/components/MkFlashPreview.vue
@@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 -->
 
 <template>
-<MkA :to="`/play/${flash.id}`" class="vhpxefrk _panel" tabindex="-1">
+<MkA :to="`/play/${flash.id}`" class="vhpxefrk _panel" :class="[{ gray: flash.visibility === 'private' }]">
 	<article>
 		<header>
 			<h1 :title="flash.title">{{ flash.title }}</h1>
@@ -22,11 +22,11 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <script lang="ts" setup>
 import { } from 'vue';
+import * as Misskey from 'misskey-js';
 import { userName } from '@/filters/user.js';
 
 const props = defineProps<{
-	//flash: Misskey.entities.Flash;
-	flash: any;
+	flash: Misskey.entities.Flash;
 }>();
 </script>
 
@@ -36,7 +36,11 @@ const props = defineProps<{
 
 	&:hover {
 		text-decoration: none;
-		color: var(--accent);
+		color: var(--MI_THEME-accent);
+	}
+
+	&:focus-visible {
+		outline-offset: -2px;
 	}
 
 	> article {
@@ -79,7 +83,6 @@ const props = defineProps<{
 			> p {
 				display: inline-block;
 				margin: 0;
-				color: var(--urlPreviewInfo);
 				font-size: 0.8em;
 				line-height: 16px;
 				vertical-align: top;
@@ -87,6 +90,12 @@ const props = defineProps<{
 		}
 	}
 
+	&:global(.gray) {
+		--c: var(--MI_THEME-bg);
+		background-image: linear-gradient(45deg, var(--c) 16.67%, transparent 16.67%, transparent 50%, var(--c) 50%, var(--c) 66.67%, transparent 66.67%, transparent 100%);
+		background-size: 16px 16px;
+	}
+
 	@media (max-width: 700px) {
 	}
 
diff --git a/packages/frontend/src/components/MkFoldableSection.vue b/packages/frontend/src/components/MkFoldableSection.vue
index f10d58b38a..fb1b5220fb 100644
--- a/packages/frontend/src/components/MkFoldableSection.vue
+++ b/packages/frontend/src/components/MkFoldableSection.vue
@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <template>
 <div ref="rootEl" :class="$style.root">
-	<header :class="$style.header" class="_button" :style="{ background: bg }" @click="showBody = !showBody">
+	<header :class="$style.header" class="_button" @click="showBody = !showBody">
 		<div :class="$style.title"><div><slot name="header"></slot></div></div>
 		<div :class="$style.divider"></div>
 		<button class="_button" :class="$style.button">
@@ -32,21 +32,23 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <script lang="ts" setup>
 import { onMounted, ref, shallowRef, watch } from 'vue';
-import tinycolor from 'tinycolor2';
 import { miLocalStorage } from '@/local-storage.js';
 import { defaultStore } from '@/store.js';
+import { getBgColor } from '@/scripts/get-bg-color.js';
 
 const miLocalStoragePrefix = 'ui:folder:' as const;
 
 const props = withDefaults(defineProps<{
 	expanded?: boolean;
-	persistKey?: string;
+	persistKey?: string | null;
 }>(), {
 	expanded: true,
+	persistKey: null,
 });
 
-const rootEl = shallowRef<HTMLDivElement>();
-const bg = ref<string>();
+const rootEl = shallowRef<HTMLElement>();
+const parentBg = ref<string | null>(null);
+// eslint-disable-next-line vue/no-setup-props-reactivity-loss
 const showBody = ref((props.persistKey && miLocalStorage.getItem(`${miLocalStoragePrefix}${props.persistKey}`)) ? (miLocalStorage.getItem(`${miLocalStoragePrefix}${props.persistKey}`) === 't') : props.expanded);
 
 watch(showBody, () => {
@@ -55,47 +57,34 @@ watch(showBody, () => {
 	}
 });
 
-function enter(element: Element) {
-	const el = element as HTMLElement;
+function enter(el: Element) {
+	if (!(el instanceof HTMLElement)) return;
 	const elementHeight = el.getBoundingClientRect().height;
 	el.style.height = '0';
 	el.offsetHeight; // reflow
-	el.style.height = elementHeight + 'px';
+	el.style.height = `${elementHeight}px`;
 }
 
-function afterEnter(element: Element) {
-	const el = element as HTMLElement;
-	el.style.height = 'unset';
+function afterEnter(el: Element) {
+	if (!(el instanceof HTMLElement)) return;
+	el.style.height = '';
 }
 
-function leave(element: Element) {
-	const el = element as HTMLElement;
+function leave(el: Element) {
+	if (!(el instanceof HTMLElement)) return;
 	const elementHeight = el.getBoundingClientRect().height;
-	el.style.height = elementHeight + 'px';
+	el.style.height = `${elementHeight}px`;
 	el.offsetHeight; // reflow
 	el.style.height = '0';
 }
 
-function afterLeave(element: Element) {
-	const el = element as HTMLElement;
-	el.style.height = 'unset';
+function afterLeave(el: Element) {
+	if (!(el instanceof HTMLElement)) return;
+	el.style.height = '';
 }
 
 onMounted(() => {
-	function getParentBg(el?: HTMLElement | null): string {
-		if (el == null || el.tagName === 'BODY') return 'var(--bg)';
-		const background = el.style.background || el.style.backgroundColor;
-		if (background) {
-			return background;
-		} else {
-			return getParentBg(el.parentElement);
-		}
-	}
-
-	const rawBg = getParentBg(rootEl.value);
-	const _bg = tinycolor(rawBg.startsWith('var(') ? getComputedStyle(document.documentElement).getPropertyValue(rawBg.slice(4, -1)) : rawBg);
-	_bg.setAlpha(0.85);
-	bg.value = _bg.toRgbString();
+	parentBg.value = getBgColor(rootEl.value?.parentElement);
 });
 </script>
 
@@ -118,9 +107,10 @@ onMounted(() => {
 	position: relative;
 	z-index: 10;
 	position: sticky;
-	top: var(--stickyTop, 0px);
-	-webkit-backdrop-filter: var(--blur, blur(8px));
-	backdrop-filter: var(--blur, blur(20px));
+	top: var(--MI-stickyTop, 0px);
+	-webkit-backdrop-filter: var(--MI-blur, blur(8px));
+	backdrop-filter: var(--MI-blur, blur(20px));
+	background-color: color(from v-bind("parentBg ?? 'var(--bg)'") srgb r g b / 0.85);
 }
 
 .title {
@@ -134,7 +124,7 @@ onMounted(() => {
 	flex: 1;
 	margin: auto;
 	height: 1px;
-	background: var(--divider);
+	background: var(--MI_THEME-divider);
 }
 
 .button {
diff --git a/packages/frontend/src/components/MkFolder.vue b/packages/frontend/src/components/MkFolder.vue
index 9b8eb19a11..7bdc06a8b4 100644
--- a/packages/frontend/src/components/MkFolder.vue
+++ b/packages/frontend/src/components/MkFolder.vue
@@ -7,10 +7,10 @@ SPDX-License-Identifier: AGPL-3.0-only
 <div ref="rootEl" :class="$style.root" role="group" :aria-expanded="opened">
 	<MkStickyContainer>
 		<template #header>
-			<div :class="[$style.header, { [$style.opened]: opened }]" class="_button" role="button" data-cy-folder-header @click="toggle">
+			<button :class="[$style.header, { [$style.opened]: opened }]" class="_button" role="button" data-cy-folder-header @click="toggle">
 				<div :class="$style.headerIcon"><slot name="icon"></slot></div>
 				<div :class="$style.headerText">
-					<div>
+					<div :class="$style.headerTextMain">
 						<MkCondensedLine :minScale="2 / 3"><slot name="label"></slot></MkCondensedLine>
 					</div>
 					<div :class="$style.headerTextSub">
@@ -22,7 +22,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 					<i v-if="opened" class="ti ti-chevron-up icon"></i>
 					<i v-else class="ti ti-chevron-down icon"></i>
 				</div>
-			</div>
+			</button>
 		</template>
 
 		<div v-if="openedAtLeastOnce" :class="[$style.body, { [$style.bgSame]: bgSame }]" :style="{ maxHeight: maxHeight ? `${maxHeight}px` : undefined, overflow: maxHeight ? `auto` : undefined }" :aria-hidden="!opened">
@@ -38,9 +38,15 @@ SPDX-License-Identifier: AGPL-3.0-only
 			>
 				<KeepAlive>
 					<div v-show="opened">
-						<MkSpacer :marginMin="14" :marginMax="22">
+						<MkSpacer v-if="withSpacer" :marginMin="14" :marginMax="22">
 							<slot></slot>
 						</MkSpacer>
+						<div v-else>
+							<slot></slot>
+						</div>
+						<div v-if="$slots.footer" :class="$style.footer">
+							<slot name="footer"></slot>
+						</div>
 					</div>
 				</KeepAlive>
 			</Transition>
@@ -50,51 +56,49 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { nextTick, onMounted, shallowRef, ref } from 'vue';
+import { nextTick, onMounted, ref, shallowRef } from 'vue';
 import { defaultStore } from '@/store.js';
+import { getBgColor } from '@/scripts/get-bg-color.js';
 
 const props = withDefaults(defineProps<{
 	defaultOpen?: boolean;
 	maxHeight?: number | null;
+	withSpacer?: boolean;
 }>(), {
 	defaultOpen: false,
 	maxHeight: null,
+	withSpacer: true,
 });
 
-const getBgColor = (el: HTMLElement) => {
-	const style = window.getComputedStyle(el);
-	if (style.backgroundColor && !['rgba(0, 0, 0, 0)', 'rgba(0,0,0,0)', 'transparent'].includes(style.backgroundColor)) {
-		return style.backgroundColor;
-	} else {
-		return el.parentElement ? getBgColor(el.parentElement) : 'transparent';
-	}
-};
-
 const rootEl = shallowRef<HTMLElement>();
 const bgSame = ref(false);
 const opened = ref(props.defaultOpen);
 const openedAtLeastOnce = ref(props.defaultOpen);
 
-function enter(el) {
+function enter(el: Element) {
+	if (!(el instanceof HTMLElement)) return;
 	const elementHeight = el.getBoundingClientRect().height;
-	el.style.height = 0;
+	el.style.height = '0';
 	el.offsetHeight; // reflow
-	el.style.height = Math.min(elementHeight, props.maxHeight ?? Infinity) + 'px';
+	el.style.height = `${Math.min(elementHeight, props.maxHeight ?? Infinity)}px`;
 }
 
-function afterEnter(el) {
-	el.style.height = null;
+function afterEnter(el: Element) {
+	if (!(el instanceof HTMLElement)) return;
+	el.style.height = '';
 }
 
-function leave(el) {
+function leave(el: Element) {
+	if (!(el instanceof HTMLElement)) return;
 	const elementHeight = el.getBoundingClientRect().height;
-	el.style.height = elementHeight + 'px';
+	el.style.height = `${elementHeight}px`;
 	el.offsetHeight; // reflow
-	el.style.height = 0;
+	el.style.height = '0';
 }
 
-function afterLeave(el) {
-	el.style.height = null;
+function afterLeave(el: Element) {
+	if (!(el instanceof HTMLElement)) return;
+	el.style.height = '';
 }
 
 function toggle() {
@@ -109,8 +113,8 @@ function toggle() {
 
 onMounted(() => {
 	const computedStyle = getComputedStyle(document.documentElement);
-	const parentBg = getBgColor(rootEl.value!.parentElement!);
-	const myBg = computedStyle.getPropertyValue('--panel');
+	const parentBg = getBgColor(rootEl.value?.parentElement) ?? 'transparent';
+	const myBg = computedStyle.getPropertyValue('--MI_THEME-panel');
 	bgSame.value = parentBg === myBg;
 });
 </script>
@@ -136,20 +140,24 @@ onMounted(() => {
 	width: 100%;
 	box-sizing: border-box;
 	padding: 9px 12px 9px 12px;
-	background: var(--buttonBg);
-	-webkit-backdrop-filter: var(--blur, blur(15px));
-	backdrop-filter: var(--blur, blur(15px));
+	background: var(--MI_THEME-folderHeaderBg);
+	-webkit-backdrop-filter: var(--MI-blur, blur(15px));
+	backdrop-filter: var(--MI-blur, blur(15px));
 	border-radius: 6px;
 	transition: border-radius 0.3s;
 
 	&:hover {
 		text-decoration: none;
-		background: var(--buttonHoverBg);
+		background: var(--MI_THEME-folderHeaderHoverBg);
+	}
+
+	&:focus-within {
+		outline-offset: 2px;
 	}
 
 	&.active {
-		color: var(--accent);
-		background: var(--buttonHoverBg);
+		color: var(--MI_THEME-accent);
+		background: var(--MI_THEME-folderHeaderHoverBg);
 	}
 
 	&.opened {
@@ -163,7 +171,7 @@ onMounted(() => {
 }
 
 .headerLower {
-	color: var(--fgTransparentWeak);
+	color: var(--MI_THEME-fgTransparentWeak);
 	font-size: .85em;
 	padding-left: 4px;
 }
@@ -190,14 +198,20 @@ onMounted(() => {
 	padding-right: 12px;
 }
 
+.headerTextMain,
 .headerTextSub {
-	color: var(--fgTransparentWeak);
+	width: fit-content;
+	max-width: 100%;
+}
+
+.headerTextSub {
+	color: var(--MI_THEME-fgTransparentWeak);
 	font-size: .85em;
 }
 
 .headerRight {
 	margin-left: auto;
-	color: var(--fgTransparentWeak);
+	color: var(--MI_THEME-fgTransparentWeak);
 	white-space: nowrap;
 }
 
@@ -206,12 +220,26 @@ onMounted(() => {
 }
 
 .body {
-	background: var(--panel);
+	background: var(--MI_THEME-panel);
 	border-radius: 0 0 6px 6px;
 	container-type: inline-size;
 
 	&.bgSame {
-		background: var(--bg);
+		background: var(--MI_THEME-bg);
 	}
 }
+
+.footer {
+	position: sticky !important;
+	z-index: 1;
+	bottom: var(--MI-stickyBottom, 0px);
+	left: 0;
+	padding: 12px;
+	background: var(--MI_THEME-acrylicBg);
+	-webkit-backdrop-filter: var(--MI-blur, blur(15px));
+	backdrop-filter: var(--MI-blur, blur(15px));
+	background-size: auto auto;
+	background-image: repeating-linear-gradient(135deg, transparent, transparent 5px, var(--MI_THEME-panel) 5px, var(--MI_THEME-panel) 10px);
+	border-radius: 0 0 6px 6px;
+}
 </style>
diff --git a/packages/frontend/src/components/MkFollowButton.vue b/packages/frontend/src/components/MkFollowButton.vue
index 636e61db8f..cc07175907 100644
--- a/packages/frontend/src/components/MkFollowButton.vue
+++ b/packages/frontend/src/components/MkFollowButton.vue
@@ -19,7 +19,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 			<span v-if="full" :class="$style.text">{{ i18n.ts.processing }}</span><MkLoading :em="true" :colored="false"/>
 		</template>
 		<template v-else-if="isFollowing">
-			<span v-if="full" :class="$style.text">{{ i18n.ts.unfollow }}</span><i class="ti ti-minus"></i>
+			<span v-if="full" :class="$style.text">{{ i18n.ts.youFollowing }}</span><i class="ti ti-minus"></i>
 		</template>
 		<template v-else-if="!isFollowing && user.isLocked">
 			<span v-if="full" :class="$style.text">{{ i18n.ts.followRequest }}</span><i class="ti ti-plus"></i>
@@ -37,11 +37,13 @@ SPDX-License-Identifier: AGPL-3.0-only
 <script lang="ts" setup>
 import { onBeforeUnmount, onMounted, ref } from 'vue';
 import * as Misskey from 'misskey-js';
+import { host } from '@@/js/config.js';
 import * as os from '@/os.js';
 import { misskeyApi } from '@/scripts/misskey-api.js';
 import { useStream } from '@/stream.js';
 import { i18n } from '@/i18n.js';
 import { claimAchievement } from '@/scripts/achievements.js';
+import { pleaseLogin } from '@/scripts/please-login.js';
 import { $i } from '@/account.js';
 import { defaultStore } from '@/store.js';
 
@@ -63,7 +65,7 @@ const hasPendingFollowRequestFromYou = ref(props.user.hasPendingFollowRequestFro
 const wait = ref(false);
 const connection = useStream().useChannel('main');
 
-if (props.user.isFollowing == null) {
+if (props.user.isFollowing == null && $i) {
 	misskeyApi('users/show', {
 		userId: props.user.id,
 	})
@@ -78,6 +80,8 @@ function onFollowChange(user: Misskey.entities.UserDetailed) {
 }
 
 async function onClick() {
+	pleaseLogin({ openOnRemote: { type: 'web', path: `/@${props.user.username}@${props.user.host ?? host}` } });
+
 	wait.value = true;
 
 	try {
@@ -121,6 +125,8 @@ async function onClick() {
 				});
 				hasPendingFollowRequestFromYou.value = true;
 
+				if ($i == null) return;
+
 				claimAchievement('following1');
 
 				if ($i.followingCount >= 10) {
@@ -159,8 +165,8 @@ onBeforeUnmount(() => {
 	position: relative;
 	display: inline-block;
 	font-weight: bold;
-	color: var(--fgOnWhite);
-	border: solid 1px var(--accent);
+	color: var(--MI_THEME-fgOnWhite);
+	border: solid 1px var(--MI_THEME-accent);
 	padding: 0;
 	height: 31px;
 	font-size: 16px;
@@ -183,17 +189,7 @@ onBeforeUnmount(() => {
 	}
 
 	&:focus-visible {
-		&:after {
-			content: "";
-			pointer-events: none;
-			position: absolute;
-			top: -5px;
-			right: -5px;
-			bottom: -5px;
-			left: -5px;
-			border: 2px solid var(--focus);
-			border-radius: 32px;
-		}
+		outline-offset: 2px;
 	}
 
 	&:hover {
@@ -205,17 +201,17 @@ onBeforeUnmount(() => {
 	}
 
 	&.active {
-		color: var(--fgOnAccent);
-		background: var(--accent);
+		color: var(--MI_THEME-fgOnAccent);
+		background: var(--MI_THEME-accent);
 
 		&:hover {
-			background: var(--accentLighten);
-			border-color: var(--accentLighten);
+			background: var(--MI_THEME-accentLighten);
+			border-color: var(--MI_THEME-accentLighten);
 		}
 
 		&:active {
-			background: var(--accentDarken);
-			border-color: var(--accentDarken);
+			background: var(--MI_THEME-accentDarken);
+			border-color: var(--MI_THEME-accentDarken);
 		}
 	}
 
diff --git a/packages/frontend/src/components/MkFormDialog.file.vue b/packages/frontend/src/components/MkFormDialog.file.vue
index 9360594236..ecb6cf882b 100644
--- a/packages/frontend/src/components/MkFormDialog.file.vue
+++ b/packages/frontend/src/components/MkFormDialog.file.vue
@@ -66,6 +66,6 @@ function selectButton(ev: MouseEvent) {
 <style module>
 .fileNotSelected {
 	font-weight: 700;
-	color: var(--infoWarnFg);
+	color: var(--MI_THEME-infoWarnFg);
 }
 </style>
diff --git a/packages/frontend/src/components/MkFormDialog.vue b/packages/frontend/src/components/MkFormDialog.vue
index 124f114111..a639eae208 100644
--- a/packages/frontend/src/components/MkFormDialog.vue
+++ b/packages/frontend/src/components/MkFormDialog.vue
@@ -13,7 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 	@click="cancel()"
 	@ok="ok()"
 	@close="cancel()"
-	@closed="$emit('closed')"
+	@closed="emit('closed')"
 >
 	<template #header>
 		{{ title }}
diff --git a/packages/frontend/src/components/MkFormFooter.vue b/packages/frontend/src/components/MkFormFooter.vue
new file mode 100644
index 0000000000..f409f6ce50
--- /dev/null
+++ b/packages/frontend/src/components/MkFormFooter.vue
@@ -0,0 +1,49 @@
+<!--
+SPDX-FileCopyrightText: syuilo and misskey-project
+SPDX-License-Identifier: AGPL-3.0-only
+-->
+
+<template>
+<div :class="$style.root">
+	<div :class="$style.text">{{ i18n.tsx.thereAreNChanges({ n: form.modifiedCount.value }) }}</div>
+	<div style="margin-left: auto;" class="_buttons">
+		<MkButton danger rounded @click="form.discard"><i class="ti ti-x"></i> {{ i18n.ts.discard }}</MkButton>
+		<MkButton primary rounded @click="form.save"><i class="ti ti-check"></i> {{ i18n.ts.save }}</MkButton>
+	</div>
+</div>
+</template>
+
+<script lang="ts" setup>
+import { } from 'vue';
+import MkButton from './MkButton.vue';
+import { i18n } from '@/i18n.js';
+
+const props = defineProps<{
+	form: {
+		modifiedCount: {
+			value: number;
+		};
+		discard: () => void;
+		save: () => void;
+	};
+}>();
+</script>
+
+<style lang="scss" module>
+.root {
+	display: flex;
+	align-items: center;
+}
+
+.text {
+	color: var(--MI_THEME-warn);
+	font-size: 90%;
+	animation: modified-blink 2s infinite;
+}
+
+@keyframes modified-blink {
+	0% { opacity: 1; }
+	50% { opacity: 0.5; }
+	100% { opacity: 1; }
+}
+</style>
diff --git a/packages/frontend/src/components/MkFukidashi.vue b/packages/frontend/src/components/MkFukidashi.vue
new file mode 100644
index 0000000000..8b1c56fca4
--- /dev/null
+++ b/packages/frontend/src/components/MkFukidashi.vue
@@ -0,0 +1,100 @@
+<!--
+SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-License-Identifier: AGPL-3.0-only
+-->
+
+<template>
+<div
+	:class="[
+		$style.root,
+		tail === 'left' ? $style.left : $style.right,
+		negativeMargin === true && $style.negativeMargin,
+		shadow === true && $style.shadow,
+	]"
+>
+	<div :class="$style.bg">
+		<svg v-if="tail !== 'none'" :class="$style.tail" version="1.1" viewBox="0 0 14.597 14.58" xmlns="http://www.w3.org/2000/svg">
+			<g transform="translate(-173.71 -87.184)">
+				<path d="m188.19 87.657c-1.469 2.3218-3.9315 3.8312-6.667 4.0865-2.2309-1.7379-4.9781-2.6816-7.8061-2.6815h-5.1e-4v12.702h12.702v-5.1e-4c2e-5 -1.9998-0.47213-3.9713-1.378-5.754 2.0709-1.6834 3.2732-4.2102 3.273-6.8791-6e-5 -0.49375-0.0413-0.98662-0.1235-1.4735z" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round" stroke-width=".33225" style="paint-order:stroke fill markers"/>
+			</g>
+		</svg>
+		<div :class="$style.content">
+			<slot></slot>
+		</div>
+	</div>
+</div>
+</template>
+
+<script setup lang="ts">
+withDefaults(defineProps<{
+	tail?: 'left' | 'right' | 'none';
+	negativeMargin?: boolean;
+	shadow?: boolean;
+}>(), {
+	tail: 'right',
+	negativeMargin: false,
+	shadow: false,
+});
+</script>
+
+<style module lang="scss">
+.root {
+	--fukidashi-radius: var(--MI-radius);
+	--fukidashi-bg: var(--MI_THEME-panel);
+
+	position: relative;
+	display: inline-block;
+	min-height: calc(var(--fukidashi-radius) * 2);
+	padding-top: calc(var(--fukidashi-radius) * .13);
+
+	&.shadow {
+		filter: drop-shadow(0 4px 32px var(--MI_THEME-shadow));
+	}
+
+	&.left {
+		padding-left: calc(var(--fukidashi-radius) * .13);
+
+		&.negativeMargin {
+			margin-left: calc(calc(var(--fukidashi-radius) * .13) * -1);
+		}
+	}
+
+	&.right {
+		padding-right: calc(var(--fukidashi-radius) * .13);
+
+		&.negativeMargin {
+			margin-right: calc(calc(var(--fukidashi-radius) * .13) * -1);
+		}
+	}
+}
+
+.bg {
+	width: 100%;
+	height: 100%;
+	background: var(--fukidashi-bg);
+	border-radius: var(--fukidashi-radius);
+}
+
+.content {
+	position: relative;
+	padding: 8px 12px;
+}
+
+.tail {
+	position: absolute;
+	top: 0;
+	display: block;
+	width: calc(var(--fukidashi-radius) * 1.13);
+	height: auto;
+	fill: var(--fukidashi-bg);
+}
+
+.left .tail {
+	left: 0;
+	transform: rotateY(180deg);
+}
+
+.right .tail {
+	right: 0;
+}
+</style>
diff --git a/packages/frontend/src/components/MkGalleryPostPreview.vue b/packages/frontend/src/components/MkGalleryPostPreview.vue
index 47cccd9b7c..22f8355acf 100644
--- a/packages/frontend/src/components/MkGalleryPostPreview.vue
+++ b/packages/frontend/src/components/MkGalleryPostPreview.vue
@@ -75,7 +75,7 @@ function leaveHover(): void {
 
 	&:hover {
 		text-decoration: none;
-		color: var(--accent);
+		color: var(--MI_THEME-accent);
 
 		> .thumbnail {
 			transform: scale(1.1);
@@ -83,7 +83,7 @@ function leaveHover(): void {
 
 		> article {
 			> footer {
-				&:before {
+				&::before {
 					opacity: 1;
 				}
 			}
@@ -139,7 +139,7 @@ function leaveHover(): void {
 			text-shadow: 0 0 8px #000;
 			background: linear-gradient(transparent, rgba(0, 0, 0, 0.7));
 
-			&:before {
+			&::before {
 				content: "";
 				display: block;
 				position: absolute;
diff --git a/packages/frontend/src/components/MkGoogle.vue b/packages/frontend/src/components/MkGoogle.vue
index 2988d77fe3..2eaee5b115 100644
--- a/packages/frontend/src/components/MkGoogle.vue
+++ b/packages/frontend/src/components/MkGoogle.vue
@@ -39,7 +39,7 @@ const search = () => {
 	width: 100%;
 	height: 40px;
 	font-size: 16px;
-	border: solid 1px var(--divider);
+	border: solid 1px var(--MI_THEME-divider);
 	border-radius: 4px 0 0 4px;
 	-webkit-appearance: textfield;
 }
@@ -48,7 +48,7 @@ const search = () => {
 	flex-shrink: 0;
 	margin: 0;
 	padding: 0 16px;
-	border: solid 1px var(--divider);
+	border: solid 1px var(--MI_THEME-divider);
 	border-left: none;
 	border-radius: 0 4px 4px 0;
 
diff --git a/packages/frontend/src/components/MkImgWithBlurhash.vue b/packages/frontend/src/components/MkImgWithBlurhash.vue
index 4e3fafe845..c04d0864fb 100644
--- a/packages/frontend/src/components/MkImgWithBlurhash.vue
+++ b/packages/frontend/src/components/MkImgWithBlurhash.vue
@@ -14,8 +14,8 @@ SPDX-License-Identifier: AGPL-3.0-only
 		:enterToClass="defaultStore.state.animation && props.transition?.enterToClass || undefined"
 		:leaveFromClass="defaultStore.state.animation && props.transition?.leaveFromClass || undefined"
 	>
-		<canvas v-show="hide" key="canvas" ref="canvas" :class="$style.canvas" :width="canvasWidth" :height="canvasHeight" :title="title ?? undefined"/>
-		<img v-show="!hide" key="img" ref="img" :height="imgHeight" :width="imgWidth" :class="$style.img" :src="src ?? undefined" :title="title ?? undefined" :alt="alt ?? undefined" loading="eager" decoding="async"/>
+		<canvas v-show="hide" key="canvas" ref="canvas" :class="$style.canvas" :width="canvasWidth" :height="canvasHeight" :title="title ?? undefined" tabindex="-1"/>
+		<img v-show="!hide" key="img" ref="img" :height="imgHeight ?? undefined" :width="imgWidth ?? undefined" :class="$style.img" :src="src ?? undefined" :title="title ?? undefined" :alt="alt ?? undefined" loading="eager" decoding="async" tabindex="-1"/>
 	</TransitionGroup>
 </div>
 </template>
@@ -23,8 +23,8 @@ SPDX-License-Identifier: AGPL-3.0-only
 <script lang="ts">
 import DrawBlurhash from '@/workers/draw-blurhash?worker';
 import TestWebGL2 from '@/workers/test-webgl2?worker';
-import { WorkerMultiDispatch } from '@/scripts/worker-multi-dispatch.js';
-import { extractAvgColorFromBlurhash } from '@/scripts/extract-avg-color-from-blurhash.js';
+import { WorkerMultiDispatch } from '@@/js/worker-multi-dispatch.js';
+import { extractAvgColorFromBlurhash } from '@@/js/extract-avg-color-from-blurhash.js';
 
 const canvasPromise = new Promise<WorkerMultiDispatch | HTMLCanvasElement>(resolve => {
 	// テスト環境で Web Worker インスタンスは作成できない
@@ -151,22 +151,26 @@ function drawImage(bitmap: CanvasImageSource) {
 }
 
 function drawAvg() {
-	if (!canvas.value || !props.hash) return;
+	if (!canvas.value) return;
+
+	const color = (props.hash != null && extractAvgColorFromBlurhash(props.hash)) || '#888';
 
 	const ctx = canvas.value.getContext('2d');
 	if (!ctx) return;
 
 	// avgColorでお茶をにごす
 	ctx.beginPath();
-	ctx.fillStyle = extractAvgColorFromBlurhash(props.hash) ?? '#888';
+	ctx.fillStyle = color;
 	ctx.fillRect(0, 0, canvasWidth.value, canvasHeight.value);
 }
 
 async function draw() {
-	if (props.hash == null) return;
+	if (import.meta.env.MODE === 'test' && props.hash == null) return;
 
 	drawAvg();
 
+	if (props.hash == null) return;
+
 	if (props.onlyAvgColor) return;
 
 	const work = await canvasPromise;
diff --git a/packages/frontend/src/components/MkInfo.vue b/packages/frontend/src/components/MkInfo.vue
index 33e65ccc4e..90ca1b5a9d 100644
--- a/packages/frontend/src/components/MkInfo.vue
+++ b/packages/frontend/src/components/MkInfo.vue
@@ -36,14 +36,14 @@ function close() {
   align-items: center;
 	padding: 12px 14px;
 	font-size: 90%;
-	background: var(--infoBg);
-	color: var(--infoFg);
-	border-radius: var(--radius);
+	background: var(--MI_THEME-infoBg);
+	color: var(--MI_THEME-infoFg);
+	border-radius: var(--MI-radius);
 	white-space: pre-wrap;
 
 	&.warn {
-		background: var(--infoWarnBg);
-		color: var(--infoWarnFg);
+		background: var(--MI_THEME-infoWarnBg);
+		color: var(--MI_THEME-infoWarnFg);
 	}
 }
 
diff --git a/packages/frontend/src/components/MkInput.vue b/packages/frontend/src/components/MkInput.vue
index 88ef4635e6..08817fd6a8 100644
--- a/packages/frontend/src/components/MkInput.vue
+++ b/packages/frontend/src/components/MkInput.vue
@@ -44,16 +44,16 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { onMounted, onUnmounted, nextTick, ref, shallowRef, watch, computed, toRefs } from 'vue';
+import { onMounted, onUnmounted, nextTick, ref, shallowRef, watch, computed, toRefs, InputHTMLAttributes } from 'vue';
 import { debounce } from 'throttle-debounce';
 import MkButton from '@/components/MkButton.vue';
-import { useInterval } from '@/scripts/use-interval.js';
+import { useInterval } from '@@/js/use-interval.js';
 import { i18n } from '@/i18n.js';
 import { Autocomplete, SuggestionType } from '@/scripts/autocomplete.js';
 
 const props = defineProps<{
 	modelValue: string | number | null;
-	type?: 'text' | 'number' | 'password' | 'email' | 'url' | 'date' | 'time' | 'search' | 'datetime-local';
+	type?: InputHTMLAttributes['type'];
 	required?: boolean;
 	readonly?: boolean;
 	disabled?: boolean;
@@ -64,8 +64,8 @@ const props = defineProps<{
 	mfmAutocomplete?: boolean | SuggestionType[],
 	autocapitalize?: string;
 	spellcheck?: boolean;
-	inputmode?: 'none' | 'text' | 'search' | 'email' | 'url' | 'numeric' | 'tel' | 'decimal';
-	step?: any;
+	inputmode?: InputHTMLAttributes['inputmode'];
+	step?: InputHTMLAttributes['step'];
 	datalist?: string[];
 	min?: number;
 	max?: number;
@@ -79,7 +79,7 @@ const props = defineProps<{
 const emit = defineEmits<{
 	(ev: 'change', _ev: KeyboardEvent): void;
 	(ev: 'keydown', _ev: KeyboardEvent): void;
-	(ev: 'enter'): void;
+	(ev: 'enter', _ev: KeyboardEvent): void;
 	(ev: 'update:modelValue', value: string | number): void;
 }>();
 
@@ -111,7 +111,7 @@ const onKeydown = (ev: KeyboardEvent) => {
 	emit('keydown', ev);
 
 	if (ev.code === 'Enter') {
-		emit('enter');
+		emit('enter', ev);
 	}
 };
 
@@ -199,7 +199,7 @@ defineExpose({
 .caption {
 	font-size: 0.85em;
 	padding: 8px 0 0 0;
-	color: var(--fgTransparentWeak);
+	color: var(--MI_THEME-fgTransparentWeak);
 
 	&:empty {
 		display: none;
@@ -216,8 +216,8 @@ defineExpose({
 
 	&.focused {
 		> .inputCore {
-			border-color: var(--accent) !important;
-			//box-shadow: 0 0 0 4px var(--focus);
+			border-color: var(--MI_THEME-accent) !important;
+			//box-shadow: 0 0 0 4px var(--MI_THEME-focus);
 		}
 	}
 
@@ -242,9 +242,9 @@ defineExpose({
 	font: inherit;
 	font-weight: normal;
 	font-size: 1em;
-	color: var(--fg);
-	background: var(--panel);
-	border: solid 1px var(--panel);
+	color: var(--MI_THEME-fg);
+	background: var(--MI_THEME-panel);
+	border: solid 1px var(--MI_THEME-panel);
 	border-radius: 6px;
 	outline: none;
 	box-shadow: none;
@@ -252,7 +252,7 @@ defineExpose({
 	transition: border-color 0.1s ease-out;
 
 	&:hover {
-		border-color: var(--inputBorderHover) !important;
+		border-color: var(--MI_THEME-inputBorderHover) !important;
 	}
 }
 
diff --git a/packages/frontend/src/components/MkInstanceCardMini.stories.impl.ts b/packages/frontend/src/components/MkInstanceCardMini.stories.impl.ts
new file mode 100644
index 0000000000..9e8de9d878
--- /dev/null
+++ b/packages/frontend/src/components/MkInstanceCardMini.stories.impl.ts
@@ -0,0 +1,65 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+/* eslint-disable @typescript-eslint/explicit-function-return-type */
+import { StoryObj } from '@storybook/vue3';
+import { HttpResponse, http } from 'msw';
+import { federationInstance } from '../../.storybook/fakes.js';
+import { commonHandlers } from '../../.storybook/mocks.js';
+import { getChartResolver } from '../../.storybook/charts.js';
+import MkInstanceCardMini from './MkInstanceCardMini.vue';
+
+export const Default = {
+	render(args) {
+		return {
+			components: {
+				MkInstanceCardMini,
+			},
+			setup() {
+				return {
+					args,
+				};
+			},
+			computed: {
+				props() {
+					return {
+						...this.args,
+					};
+				},
+			},
+			template: '<MkInstanceCardMini v-bind="props" />',
+		};
+	},
+	args: {
+		instance: federationInstance(),
+	},
+	parameters: {
+		layout: 'centered',
+		msw: {
+			handlers: [
+				...commonHandlers,
+				http.get('/undefined/preview.webp', async ({ request }) => {
+					const urlStr = new URL(request.url).searchParams.get('url');
+					if (urlStr == null) {
+						return new HttpResponse(null, { status: 404 });
+					}
+					const url = new URL(urlStr);
+
+					if (url.href.startsWith('https://github.com/misskey-dev/misskey/blob/master/packages/frontend/assets/')) {
+						const image = await (await fetch(`client-assets/${url.pathname.split('/').pop()}`)).blob();
+						return new HttpResponse(image, {
+							headers: {
+								'Content-Type': 'image/jpeg',
+							},
+						});
+					} else {
+						return new HttpResponse(null, { status: 404 });
+					}
+				}),
+				http.get('/api/charts/instance', getChartResolver(['requests.received'])),
+			],
+		},
+	},
+} satisfies StoryObj<typeof MkInstanceCardMini>;
diff --git a/packages/frontend/src/components/MkInstanceCardMini.vue b/packages/frontend/src/components/MkInstanceCardMini.vue
index e26aef0f69..b0601cf7f9 100644
--- a/packages/frontend/src/components/MkInstanceCardMini.vue
+++ b/packages/frontend/src/components/MkInstanceCardMini.vue
@@ -29,8 +29,8 @@ const chartValues = ref<number[] | null>(null);
 
 misskeyApiGet('charts/instance', { host: props.instance.host, limit: 16 + 1, span: 'day' }).then(res => {
 	// 今日のぶんの値はまだ途中の値であり、それも含めると大抵の場合前日よりも下降しているようなグラフになってしまうため今日は弾く
-	res['requests.received'].splice(0, 1);
-	chartValues.value = res['requests.received'];
+	res.requests.received.splice(0, 1);
+	chartValues.value = res.requests.received;
 });
 
 function getInstanceIcon(instance): string {
@@ -46,7 +46,7 @@ function getInstanceIcon(instance): string {
 	display: flex;
 	align-items: center;
 	padding: 16px;
-	background: var(--panel);
+	background: var(--MI_THEME-panel);
 	border-radius: 8px;
 
 	> :global(.icon) {
@@ -62,7 +62,7 @@ function getInstanceIcon(instance): string {
 		flex: 1;
 		overflow: hidden;
 		font-size: 0.9em;
-		color: var(--fg);
+		color: var(--MI_THEME-fg);
 		padding-right: 8px;
 
 		> :global(.host) {
@@ -109,7 +109,7 @@ function getInstanceIcon(instance): string {
 	}
 
 	&:global(.gray) {
-		--c: var(--bg);
+		--c: var(--MI_THEME-bg);
 		background-image: linear-gradient(45deg, var(--c) 16.67%, transparent 16.67%, transparent 50%, var(--c) 50%, var(--c) 66.67%, transparent 66.67%, transparent 100%);
 		background-size: 16px 16px;
 	}
diff --git a/packages/frontend/src/components/MkInstanceStats.vue b/packages/frontend/src/components/MkInstanceStats.vue
index d74c885041..8ccbf61e48 100644
--- a/packages/frontend/src/components/MkInstanceStats.vue
+++ b/packages/frontend/src/components/MkInstanceStats.vue
@@ -121,7 +121,7 @@ function createDoughnut(chartEl, tooltip, data) {
 			labels: data.map(x => x.name),
 			datasets: [{
 				backgroundColor: data.map(x => x.color),
-				borderColor: getComputedStyle(document.documentElement).getPropertyValue('--panel'),
+				borderColor: getComputedStyle(document.documentElement).getPropertyValue('--MI_THEME-panel'),
 				borderWidth: 2,
 				hoverOffset: 0,
 				data: data.map(x => x.value),
@@ -256,8 +256,8 @@ onMounted(() => {
 				flex: 1;
 				min-width: 0;
 				position: relative;
-				background: var(--panel);
-				border-radius: var(--radius);
+				background: var(--MI_THEME-panel);
+				border-radius: var(--MI-radius);
 				padding: 24px;
 				max-height: 300px;
 
diff --git a/packages/frontend/src/components/MkInstanceTicker.vue b/packages/frontend/src/components/MkInstanceTicker.vue
index 82c82199b5..fae22baa3f 100644
--- a/packages/frontend/src/components/MkInstanceTicker.vue
+++ b/packages/frontend/src/components/MkInstanceTicker.vue
@@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <script lang="ts" setup>
 import { computed } from 'vue';
-import { instanceName } from '@/config.js';
+import { instanceName } from '@@/js/config.js';
 import { instance as Instance } from '@/instance.js';
 import { getProxiedImageUrlNullable } from '@/scripts/media-proxy.js';
 
diff --git a/packages/frontend/src/components/MkInviteCode.vue b/packages/frontend/src/components/MkInviteCode.vue
index 1c6f412dc1..1a71f6574f 100644
--- a/packages/frontend/src/components/MkInviteCode.vue
+++ b/packages/frontend/src/components/MkInviteCode.vue
@@ -8,11 +8,17 @@ SPDX-License-Identifier: AGPL-3.0-only
 	<template #label>{{ invite.code }}</template>
 	<template #suffix>
 		<span v-if="invite.used">{{ i18n.ts.used }}</span>
-		<span v-else-if="isExpired" style="color: var(--error)">{{ i18n.ts.expired }}</span>
-		<span v-else style="color: var(--success)">{{ i18n.ts.unused }}</span>
+		<span v-else-if="isExpired" style="color: var(--MI_THEME-error)">{{ i18n.ts.expired }}</span>
+		<span v-else style="color: var(--MI_THEME-success)">{{ i18n.ts.unused }}</span>
+	</template>
+	<template #footer>
+		<div class="_buttons">
+			<MkButton v-if="!invite.used && !isExpired" primary rounded @click="copyInviteCode()"><i class="ti ti-copy"></i> {{ i18n.ts.copy }}</MkButton>
+			<MkButton v-if="!invite.used || moderator" danger rounded @click="deleteCode()"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton>
+		</div>
 	</template>
 
-	<div class="_gaps_s" :class="$style.root">
+	<div :class="$style.root">
 		<div :class="$style.items">
 			<div>
 				<div :class="$style.label">{{ i18n.ts.invitationCode }}</div>
@@ -49,10 +55,6 @@ SPDX-License-Identifier: AGPL-3.0-only
 				<div><MkTime :time="invite.createdAt" mode="absolute"/></div>
 			</div>
 		</div>
-		<div :class="$style.buttons">
-			<MkButton v-if="!invite.used && !isExpired" primary rounded @click="copyInviteCode()"><i class="ti ti-copy"></i> {{ i18n.ts.copy }}</MkButton>
-			<MkButton v-if="!invite.used || moderator" danger rounded @click="deleteCode()"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton>
-		</div>
 	</div>
 </MkFolder>
 </template>
@@ -62,7 +64,7 @@ import { computed } from 'vue';
 import * as Misskey from 'misskey-js';
 import MkFolder from '@/components/MkFolder.vue';
 import MkButton from '@/components/MkButton.vue';
-import copyToClipboard from '@/scripts/copy-to-clipboard.js';
+import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
 import { i18n } from '@/i18n.js';
 import * as os from '@/os.js';
 
@@ -121,9 +123,4 @@ function copyInviteCode() {
 	width: var(--height);
 	height: var(--height);
 }
-
-.buttons {
-	display: flex;
-	gap: 8px;
-}
 </style>
diff --git a/packages/frontend/src/components/MkKeyValue.vue b/packages/frontend/src/components/MkKeyValue.vue
index 20b1ef2be2..50c9e16e5e 100644
--- a/packages/frontend/src/components/MkKeyValue.vue
+++ b/packages/frontend/src/components/MkKeyValue.vue
@@ -17,7 +17,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <script lang="ts" setup>
 import { } from 'vue';
-import copyToClipboard from '@/scripts/copy-to-clipboard.js';
+import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
 import * as os from '@/os.js';
 import { i18n } from '@/i18n.js';
 
diff --git a/packages/frontend/src/components/MkLaunchPad.vue b/packages/frontend/src/components/MkLaunchPad.vue
index f9d4334c4c..32c1a2d172 100644
--- a/packages/frontend/src/components/MkLaunchPad.vue
+++ b/packages/frontend/src/components/MkLaunchPad.vue
@@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 -->
 
 <template>
-<MkModal ref="modal" v-slot="{ type, maxHeight }" :preferType="preferedModalType" :anchor="anchor" :transparentBg="true" :src="src" @click="modal?.close()" @closed="emit('closed')">
+<MkModal ref="modal" v-slot="{ type, maxHeight }" :preferType="preferedModalType" :anchor="anchor" :transparentBg="true" :src="src" @click="modal?.close()" @closed="emit('closed')" @esc="modal?.close()">
 	<div class="szkkfdyq _popup _shadow" :class="{ asDrawer: type === 'drawer' }" :style="{ maxHeight: maxHeight ? maxHeight + 'px' : '' }">
 		<div class="main">
 			<template v-for="item in items" :key="item.text">
@@ -12,13 +12,13 @@ SPDX-License-Identifier: AGPL-3.0-only
 					<i class="icon" :class="item.icon"></i>
 					<div class="text">{{ item.text }}</div>
 					<span v-if="item.indicate && item.indicateValue" class="_indicateCounter indicatorWithValue">{{ item.indicateValue }}</span>
-					<span v-else-if="item.indicate" class="indicator"><i class="_indicatorCircle"></i></span>
+					<span v-else-if="item.indicate" class="indicator _blink"><i class="_indicatorCircle"></i></span>
 				</button>
 				<MkA v-else v-click-anime :to="item.to" class="item" @click.passive="close()">
 					<i class="icon" :class="item.icon"></i>
 					<div class="text">{{ item.text }}</div>
 					<span v-if="item.indicate && item.indicateValue" class="_indicateCounter indicatorWithValue">{{ item.indicateValue }}</span>
-					<span v-else-if="item.indicate" class="indicator"><i class="_indicatorCircle"></i></span>
+					<span v-else-if="item.indicate" class="indicator _blink"><i class="_indicatorCircle"></i></span>
 				</MkA>
 			</template>
 		</div>
@@ -105,8 +105,8 @@ function close() {
 			box-sizing: border-box;
 
 			&:hover {
-				color: var(--accent);
-				background: var(--accentedBg);
+				color: var(--MI_THEME-accent);
+				background: var(--MI_THEME-accentedBg);
 				text-decoration: none;
 			}
 
@@ -137,9 +137,8 @@ function close() {
 				position: absolute;
 				top: 32px;
 				left: 32px;
-				color: var(--indicator);
+				color: var(--MI_THEME-indicator);
 				font-size: 8px;
-				animation: global-blink 1s infinite;
 
 				@media (max-width: 500px) {
 					top: 16px;
diff --git a/packages/frontend/src/components/MkLink.vue b/packages/frontend/src/components/MkLink.vue
index 5d54a58e97..bda2161eb8 100644
--- a/packages/frontend/src/components/MkLink.vue
+++ b/packages/frontend/src/components/MkLink.vue
@@ -16,7 +16,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <script lang="ts" setup>
 import { defineAsyncComponent, ref } from 'vue';
-import { url as local } from '@/config.js';
+import { url as local } from '@@/js/config.js';
 import { useTooltip } from '@/scripts/use-tooltip.js';
 import * as os from '@/os.js';
 import { isEnabledUrlPreview } from '@/instance.js';
@@ -37,11 +37,13 @@ const el = ref<HTMLElement | { $el: HTMLElement }>();
 
 if (isEnabledUrlPreview.value) {
 	useTooltip(el, (showing) => {
-		os.popup(defineAsyncComponent(() => import('@/components/MkUrlPreviewPopup.vue')), {
+		const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkUrlPreviewPopup.vue')), {
 			showing,
 			url: props.url,
 			source: el.value instanceof HTMLElement ? el.value : el.value?.$el,
-		}, {}, 'closed');
+		}, {
+			closed: () => dispose(),
+		});
 	});
 }
 </script>
diff --git a/packages/frontend/src/components/MkMediaAudio.vue b/packages/frontend/src/components/MkMediaAudio.vue
index 5d2edf467e..8b713b2734 100644
--- a/packages/frontend/src/components/MkMediaAudio.vue
+++ b/packages/frontend/src/components/MkMediaAudio.vue
@@ -15,7 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 	@contextmenu.stop
 	@keydown.stop
 >
-	<button v-if="hide" :class="$style.hidden" @click="hide = false">
+	<button v-if="hide" :class="$style.hidden" @click="show">
 		<div :class="$style.hiddenTextWrapper">
 			<b v-if="audio.isSensitive" style="display: block;"><i class="ti ti-eye-exclamation"></i> {{ i18n.ts.sensitive }}{{ defaultStore.state.dataSaver.media ? ` (${i18n.ts.audio}${audio.size ? ' ' + bytes(audio.size) : ''})` : '' }}</b>
 			<b v-else style="display: block;"><i class="ti ti-music"></i> {{ defaultStore.state.dataSaver.media && audio.size ? bytes(audio.size) : i18n.ts.audio }}</b>
@@ -39,23 +39,37 @@ SPDX-License-Identifier: AGPL-3.0-only
 		<audio
 			ref="audioEl"
 			preload="metadata"
+			@keydown.prevent="() => {}"
 		>
 			<source :src="audio.url">
 		</audio>
 		<div :class="[$style.controlsChild, $style.controlsLeft]">
-			<button class="_button" :class="$style.controlButton" @click="togglePlayPause">
+			<button
+				:class="['_button', $style.controlButton]"
+				tabindex="-1"
+				@click.stop="togglePlayPause"
+			>
 				<i v-if="isPlaying" class="ti ti-player-pause-filled"></i>
 				<i v-else class="ti ti-player-play-filled"></i>
 			</button>
 		</div>
 		<div :class="[$style.controlsChild, $style.controlsRight]">
-			<button class="_button" :class="$style.controlButton" @click="showMenu">
+			<button
+				:class="['_button', $style.controlButton]"
+				tabindex="-1"
+				@click.stop="() => {}"
+				@mousedown.prevent.stop="showMenu"
+			>
 				<i class="ti ti-settings"></i>
 			</button>
 		</div>
 		<div :class="[$style.controlsChild, $style.controlsTime]">{{ hms(elapsedTimeMs) }}</div>
 		<div :class="[$style.controlsChild, $style.controlsVolume]">
-			<button class="_button" :class="$style.controlButton" @click="toggleMute">
+			<button
+				:class="['_button', $style.controlButton]"
+				tabindex="-1"
+				@click.stop="toggleMute"
+			>
 				<i v-if="volume === 0" class="ti ti-volume-3"></i>
 				<i v-else class="ti ti-volume"></i>
 			</button>
@@ -80,6 +94,7 @@ import type { MenuItem } from '@/types/menu.js';
 import { defaultStore } from '@/store.js';
 import { i18n } from '@/i18n.js';
 import * as os from '@/os.js';
+import { type Keymap } from '@/scripts/hotkey.js';
 import bytes from '@/filters/bytes.js';
 import { hms } from '@/filters/hms.js';
 import MkMediaRange from '@/components/MkMediaRange.vue';
@@ -90,32 +105,44 @@ const props = defineProps<{
 }>();
 
 const keymap = {
-	'up': () => {
-		if (hasFocus() && audioEl.value) {
-			volume.value = Math.min(volume.value + 0.1, 1);
-		}
+	'up': {
+		allowRepeat: true,
+		callback: () => {
+			if (hasFocus() && audioEl.value) {
+				volume.value = Math.min(volume.value + 0.1, 1);
+			}
+		},
 	},
-	'down': () => {
-		if (hasFocus() && audioEl.value) {
-			volume.value = Math.max(volume.value - 0.1, 0);
-		}
+	'down': {
+		allowRepeat: true,
+		callback: () => {
+			if (hasFocus() && audioEl.value) {
+				volume.value = Math.max(volume.value - 0.1, 0);
+			}
+		},
 	},
-	'left': () => {
-		if (hasFocus() && audioEl.value) {
-			audioEl.value.currentTime = Math.max(audioEl.value.currentTime - 5, 0);
-		}
+	'left': {
+		allowRepeat: true,
+		callback: () => {
+			if (hasFocus() && audioEl.value) {
+				audioEl.value.currentTime = Math.max(audioEl.value.currentTime - 5, 0);
+			}
+		},
 	},
-	'right': () => {
-		if (hasFocus() && audioEl.value) {
-			audioEl.value.currentTime = Math.min(audioEl.value.currentTime + 5, audioEl.value.duration);
-		}
+	'right': {
+		allowRepeat: true,
+		callback: () => {
+			if (hasFocus() && audioEl.value) {
+				audioEl.value.currentTime = Math.min(audioEl.value.currentTime + 5, audioEl.value.duration);
+			}
+		},
 	},
 	'space': () => {
 		if (hasFocus()) {
 			togglePlayPause();
 		}
 	},
-};
+} as const satisfies Keymap;
 
 // PlayerElもしくはその子要素にフォーカスがあるかどうか
 function hasFocus() {
@@ -126,16 +153,26 @@ function hasFocus() {
 const playerEl = shallowRef<HTMLDivElement>();
 const audioEl = shallowRef<HTMLAudioElement>();
 
-// eslint-disable-next-line vue/no-setup-props-destructure
+// eslint-disable-next-line vue/no-setup-props-reactivity-loss
 const hide = ref((defaultStore.state.nsfw === 'force' || defaultStore.state.dataSaver.media) ? true : (props.audio.isSensitive && defaultStore.state.nsfw !== 'ignore'));
 
+async function show() {
+	if (props.audio.isSensitive && defaultStore.state.confirmWhenRevealingSensitiveMedia) {
+		const { canceled } = await os.confirm({
+			type: 'question',
+			text: i18n.ts.sensitiveMediaRevealConfirm,
+		});
+		if (canceled) return;
+	}
+
+	hide.value = false;
+}
+
 // Menu
 const menuShowing = ref(false);
 
 function showMenu(ev: MouseEvent) {
-	let menu: MenuItem[] = [];
-
-	menu = [
+	const menu: MenuItem[] = [
 		// TODO: 再生キューに追加
 		{
 			type: 'switch',
@@ -183,7 +220,7 @@ function showMenu(ev: MouseEvent) {
 		menu.push({
 			type: 'divider',
 		}, {
-			type: 'link' as const,
+			type: 'link',
 			text: i18n.ts._fileViewer.title,
 			icon: 'ti ti-info-circle',
 			to: `/my/drive/file/${props.audio.id}`,
@@ -354,11 +391,11 @@ onDeactivated(() => {
 .audioContainer {
 	container-type: inline-size;
 	position: relative;
-	border: .5px solid var(--divider);
-	border-radius: var(--radius);
+	border: .5px solid var(--MI_THEME-divider);
+	border-radius: var(--MI-radius);
 	overflow: clip;
 
-	&:focus {
+	&:focus-visible {
 		outline: none;
 	}
 }
@@ -375,7 +412,7 @@ onDeactivated(() => {
 		height: 100%;
 		pointer-events: none;
 		border-radius: inherit;
-		box-shadow: inset 0 0 0 4px var(--warn);
+		box-shadow: inset 0 0 0 4px var(--MI_THEME-warn);
 	}
 }
 
@@ -417,12 +454,16 @@ onDeactivated(() => {
 
 	.controlButton {
 		padding: 6px;
-		border-radius: calc(var(--radius) / 2);
+		border-radius: calc(var(--MI-radius) / 2);
 		font-size: 1.05rem;
 
 		&:hover {
-			color: var(--accent);
-			background-color: var(--accentedBg);
+			color: var(--MI_THEME-accent);
+			background-color: var(--MI_THEME-accentedBg);
+		}
+
+		&:focus-visible {
+			outline: none;
 		}
 	}
 }
diff --git a/packages/frontend/src/components/MkMediaBanner.vue b/packages/frontend/src/components/MkMediaBanner.vue
index a219848b7f..3e521e0a03 100644
--- a/packages/frontend/src/components/MkMediaBanner.vue
+++ b/packages/frontend/src/components/MkMediaBanner.vue
@@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 <template>
 <div :class="$style.root">
 	<MkMediaAudio v-if="media.type.startsWith('audio') && media.type !== 'audio/midi'" :audio="media"/>
-	<div v-else-if="media.isSensitive && hide" :class="$style.sensitive" @click="hide = false">
+	<div v-else-if="media.isSensitive && hide" :class="$style.sensitive" @click="show">
 		<span style="font-size: 1.6em;"><i class="ti ti-alert-triangle"></i></span>
 		<b>{{ i18n.ts.sensitive }}</b>
 		<span>{{ i18n.ts.clickToShow }}</span>
@@ -24,24 +24,30 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { shallowRef, watch, ref } from 'vue';
+import { ref } from 'vue';
 import * as Misskey from 'misskey-js';
 import { i18n } from '@/i18n.js';
+import { defaultStore } from '@/store.js';
+import * as os from '@/os.js';
 import MkMediaAudio from '@/components/MkMediaAudio.vue';
 
-const props = withDefaults(defineProps<{
+const props = defineProps<{
 	media: Misskey.entities.DriveFile;
-}>(), {
-});
+}>();
 
-const audioEl = shallowRef<HTMLAudioElement>();
 const hide = ref(true);
 
-watch(audioEl, () => {
-	if (audioEl.value) {
-		audioEl.value.volume = 0.3;
+async function show() {
+	if (props.media.isSensitive && defaultStore.state.confirmWhenRevealingSensitiveMedia) {
+		const { canceled } = await os.confirm({
+			type: 'question',
+			text: i18n.ts.sensitiveMediaRevealConfirm,
+		});
+		if (canceled) return;
 	}
-});
+
+	hide.value = false;
+}
 </script>
 
 <style lang="scss" module>
@@ -62,7 +68,6 @@ watch(audioEl, () => {
 }
 
 .download {
-	background: var(--noteAttachedFile);
 }
 
 .sensitive {
diff --git a/packages/frontend/src/components/MkMediaImage.vue b/packages/frontend/src/components/MkMediaImage.vue
index 82f36fe5c4..ec85569df5 100644
--- a/packages/frontend/src/components/MkMediaImage.vue
+++ b/packages/frontend/src/components/MkMediaImage.vue
@@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 -->
 
 <template>
-<div :class="[hide ? $style.hidden : $style.visible, (image.isSensitive && defaultStore.state.highlightSensitiveMedia) && $style.sensitive]" :style="darkMode ? '--c: rgb(255 255 255 / 2%);' : '--c: rgb(0 0 0 / 2%);'" @click="onclick">
+<div :class="[hide ? $style.hidden : $style.visible, (image.isSensitive && defaultStore.state.highlightSensitiveMedia) && $style.sensitive]" @click="onclick">
 	<component
 		:is="disableImageLink ? 'div' : 'a'"
 		v-bind="disableImageLink ? {
@@ -42,7 +42,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 		<div :class="$style.indicators">
 			<div v-if="['image/gif', 'image/apng'].includes(image.type)" :class="$style.indicator">GIF</div>
 			<div v-if="image.comment" :class="$style.indicator">ALT</div>
-			<div v-if="image.isSensitive" :class="$style.indicator" style="color: var(--warn);" :title="i18n.ts.sensitive"><i class="ti ti-eye-exclamation"></i></div>
+			<div v-if="image.isSensitive" :class="$style.indicator" style="color: var(--MI_THEME-warn);" :title="i18n.ts.sensitive"><i class="ti ti-eye-exclamation"></i></div>
 		</div>
 		<button :class="$style.menu" class="_button" @click.stop="showMenu"><i class="ti ti-dots" style="vertical-align: middle;"></i></button>
 		<i class="ti ti-eye-off" :class="$style.hide" @click.stop="hide = true"></i>
@@ -53,6 +53,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 <script lang="ts" setup>
 import { watch, ref, computed } from 'vue';
 import * as Misskey from 'misskey-js';
+import type { MenuItem } from '@/types/menu.js';
 import { getStaticImageUrl } from '@/scripts/media-proxy.js';
 import bytes from '@/filters/bytes.js';
 import ImgWithBlurhash from '@/components/MkImgWithBlurhash.vue';
@@ -74,7 +75,6 @@ const props = withDefaults(defineProps<{
 });
 
 const hide = ref(true);
-const darkMode = ref<boolean>(defaultStore.state.darkMode);
 
 const url = computed(() => (props.raw || defaultStore.state.loadRawImages)
 	? props.image.url
@@ -83,11 +83,21 @@ const url = computed(() => (props.raw || defaultStore.state.loadRawImages)
 		: props.image.thumbnailUrl,
 );
 
-function onclick() {
+async function onclick(ev: MouseEvent) {
 	if (!props.controls) {
 		return;
 	}
+
 	if (hide.value) {
+		ev.stopPropagation();
+		if (props.image.isSensitive && defaultStore.state.confirmWhenRevealingSensitiveMedia) {
+			const { canceled } = await os.confirm({
+				type: 'question',
+				text: i18n.ts.sensitiveMediaRevealConfirm,
+			});
+			if (canceled) return;
+		}
+
 		hide.value = false;
 	}
 }
@@ -101,27 +111,39 @@ watch(() => props.image, () => {
 });
 
 function showMenu(ev: MouseEvent) {
-	os.popupMenu([{
+	const menuItems: MenuItem[] = [];
+
+	menuItems.push({
 		text: i18n.ts.hide,
 		icon: 'ti ti-eye-off',
 		action: () => {
 			hide.value = true;
 		},
-	}, ...(iAmModerator ? [{
-		text: i18n.ts.markAsSensitive,
-		icon: 'ti ti-eye-exclamation',
-		danger: true,
-		action: () => {
-			os.apiWithDialog('drive/files/update', { fileId: props.image.id, isSensitive: true });
-		},
-	}] : []), ...($i?.id === props.image.userId ? [{
-		type: 'divider' as const,
-	}, {
-		type: 'link' as const,
-		text: i18n.ts._fileViewer.title,
-		icon: 'ti ti-info-circle',
-		to: `/my/drive/file/${props.image.id}`,
-	}] : [])], ev.currentTarget ?? ev.target);
+	});
+
+	if (iAmModerator) {
+		menuItems.push({
+			text: i18n.ts.markAsSensitive,
+			icon: 'ti ti-eye-exclamation',
+			danger: true,
+			action: () => {
+				os.apiWithDialog('drive/files/update', { fileId: props.image.id, isSensitive: true });
+			},
+		});
+	}
+
+	if ($i?.id === props.image.userId) {
+		menuItems.push({
+			type: 'divider',
+		}, {
+			type: 'link',
+			text: i18n.ts._fileViewer.title,
+			icon: 'ti ti-info-circle',
+			to: `/my/drive/file/${props.image.id}`,
+		});
+	}
+
+	os.popupMenu(menuItems, ev.currentTarget ?? ev.target);
 }
 
 </script>
@@ -143,7 +165,7 @@ function showMenu(ev: MouseEvent) {
 		height: 100%;
 		pointer-events: none;
 		border-radius: inherit;
-		box-shadow: inset 0 0 0 4px var(--warn);
+		box-shadow: inset 0 0 0 4px var(--MI_THEME-warn);
 	}
 }
 
@@ -164,8 +186,8 @@ function showMenu(ev: MouseEvent) {
 	display: block;
 	position: absolute;
 	border-radius: 6px;
-	background-color: var(--fg);
-	color: var(--accentLighten);
+	background-color: var(--MI_THEME-fg);
+	color: var(--MI_THEME-accentLighten);
 	font-size: 12px;
 	opacity: .5;
 	padding: 5px 8px;
@@ -184,19 +206,28 @@ function showMenu(ev: MouseEvent) {
 
 .visible {
 	position: relative;
-	//box-shadow: 0 0 0 1px var(--divider) inset;
-	background: var(--bg);
-	background-image: linear-gradient(45deg, var(--c) 16.67%, var(--bg) 16.67%, var(--bg) 50%, var(--c) 50%, var(--c) 66.67%, var(--bg) 66.67%, var(--bg) 100%);
+	//box-shadow: 0 0 0 1px var(--MI_THEME-divider) inset;
+	background: var(--MI_THEME-bg);
 	background-size: 16px 16px;
 }
 
+html[data-color-scheme=dark] .visible {
+	--c: rgb(255 255 255 / 2%);
+	background-image: linear-gradient(45deg, var(--c) 16.67%, var(--MI_THEME-bg) 16.67%, var(--MI_THEME-bg) 50%, var(--c) 50%, var(--c) 66.67%, var(--MI_THEME-bg) 66.67%, var(--MI_THEME-bg) 100%);
+}
+
+html[data-color-scheme=light] .visible {
+	--c: rgb(0 0 0 / 2%);
+	background-image: linear-gradient(45deg, var(--c) 16.67%, var(--MI_THEME-bg) 16.67%, var(--MI_THEME-bg) 50%, var(--c) 50%, var(--c) 66.67%, var(--MI_THEME-bg) 66.67%, var(--MI_THEME-bg) 100%);
+}
+
 .menu {
 	display: block;
 	position: absolute;
 	border-radius: 999px;
 	background-color: rgba(0, 0, 0, 0.3);
-	-webkit-backdrop-filter: var(--blur, blur(15px));
-	backdrop-filter: var(--blur, blur(15px));
+	-webkit-backdrop-filter: var(--MI-blur, blur(15px));
+	backdrop-filter: var(--MI-blur, blur(15px));
 	color: #fff;
 	font-size: 0.8em;
 	width: 28px;
@@ -227,10 +258,10 @@ function showMenu(ev: MouseEvent) {
 }
 
 .indicator {
-	/* Hardcode to black because either --bg or --fg makes it hard to read in dark/light mode */
+	/* Hardcode to black because either --MI_THEME-bg or --MI_THEME-fg makes it hard to read in dark/light mode */
 	background-color: black;
 	border-radius: 6px;
-	color: var(--accentLighten);
+	color: var(--MI_THEME-accentLighten);
 	display: inline-block;
 	font-weight: bold;
 	font-size: 0.8em;
diff --git a/packages/frontend/src/components/MkMediaList.vue b/packages/frontend/src/components/MkMediaList.vue
index b1321a8ef9..32766f2029 100644
--- a/packages/frontend/src/components/MkMediaList.vue
+++ b/packages/frontend/src/components/MkMediaList.vue
@@ -37,8 +37,9 @@ import XBanner from '@/components/MkMediaBanner.vue';
 import XImage from '@/components/MkMediaImage.vue';
 import XVideo from '@/components/MkMediaVideo.vue';
 import * as os from '@/os.js';
-import { FILE_TYPE_BROWSERSAFE } from '@/const.js';
+import { FILE_TYPE_BROWSERSAFE } from '@@/js/const.js';
 import { defaultStore } from '@/store.js';
+import { focusParent } from '@/scripts/focus.js';
 
 const props = defineProps<{
 	mediaList: Misskey.entities.DriveFile[];
@@ -49,7 +50,9 @@ const gallery = shallowRef<HTMLDivElement>();
 const pswpZIndex = os.claimZIndex('middle');
 document.documentElement.style.setProperty('--mk-pswp-root-z-index', pswpZIndex.toString());
 const count = computed(() => props.mediaList.filter(media => previewable(media)).length);
-let lightbox: PhotoSwipeLightbox | null;
+let lightbox: PhotoSwipeLightbox | null = null;
+
+let activeEl: HTMLElement | null = null;
 
 const popstateHandler = (): void => {
 	if (lightbox?.pswp && lightbox.pswp.isOpen === true) {
@@ -60,7 +63,7 @@ const popstateHandler = (): void => {
 async function calcAspectRatio() {
 	if (!gallery.value) return;
 
-	let img = props.mediaList[0];
+	const img = props.mediaList[0];
 
 	if (props.mediaList.length !== 1 || !(img.properties.width && img.properties.height)) {
 		gallery.value.style.aspectRatio = '';
@@ -131,18 +134,17 @@ onMounted(() => {
 		bgOpacity: 1,
 		showAnimationDuration: 100,
 		hideAnimationDuration: 100,
+		returnFocus: false,
 		pswpModule: PhotoSwipe,
 	});
 
-	lightbox.on('itemData', (ev) => {
-		const { itemData } = ev;
-
+	lightbox.addFilter('itemData', (itemData) => {
 		// element is children
 		const { element } = itemData;
 
 		const id = element?.dataset.id;
 		const file = props.mediaList.find(media => media.id === id);
-		if (!file) return;
+		if (!file) return itemData;
 
 		itemData.src = file.url;
 		itemData.w = Number(file.properties.width);
@@ -154,44 +156,54 @@ onMounted(() => {
 		itemData.alt = file.comment ?? file.name;
 		itemData.comment = file.comment ?? file.name;
 		itemData.thumbCropped = true;
+
+		return itemData;
 	});
 
 	lightbox.on('uiRegister', () => {
 		lightbox?.pswp?.ui?.registerElement({
 			name: 'altText',
-			className: 'pwsp__alt-text-container',
+			className: 'pswp__alt-text-container',
 			appendTo: 'wrapper',
-			onInit: (el, pwsp) => {
-				let textBox = document.createElement('p');
-				textBox.className = 'pwsp__alt-text _acrylic';
+			onInit: (el, pswp) => {
+				const textBox = document.createElement('p');
+				textBox.className = 'pswp__alt-text _acrylic';
 				el.appendChild(textBox);
 
-				pwsp.on('change', () => {
-					textBox.textContent = pwsp.currSlide?.data.comment;
+				pswp.on('change', () => {
+					textBox.textContent = pswp.currSlide?.data.comment;
 				});
 			},
 		});
 	});
 
-	lightbox.init();
-
-	window.addEventListener('popstate', popstateHandler);
-
-	lightbox.on('beforeOpen', () => {
+	lightbox.on('afterInit', () => {
+		activeEl = document.activeElement instanceof HTMLElement ? document.activeElement : null;
+		focusParent(activeEl, true, true);
+		lightbox?.pswp?.element?.focus({
+			preventScroll: true,
+		});
 		history.pushState(null, '', '#pswp');
 	});
 
-	lightbox.on('close', () => {
+	lightbox.on('destroy', () => {
+		focusParent(activeEl, true, false);
+		activeEl = null;
 		if (window.location.hash === '#pswp') {
 			history.back();
 		}
 	});
+
+	window.addEventListener('popstate', popstateHandler);
+
+	lightbox.init();
 });
 
 onUnmounted(() => {
 	window.removeEventListener('popstate', popstateHandler);
 	lightbox?.destroy();
 	lightbox = null;
+	activeEl = null;
 });
 
 const previewable = (file: Misskey.entities.DriveFile): boolean => {
@@ -199,6 +211,16 @@ const previewable = (file: Misskey.entities.DriveFile): boolean => {
 	// FILE_TYPE_BROWSERSAFEに適合しないものはブラウザで表示するのに不適切
 	return (file.type.startsWith('video') || file.type.startsWith('image')) && FILE_TYPE_BROWSERSAFE.includes(file.type);
 };
+
+const openGallery = () => {
+	if (props.mediaList.filter(media => previewable(media)).length > 0) {
+		lightbox?.loadAndOpen(0);
+	}
+};
+
+defineExpose({
+	openGallery,
+});
 </script>
 
 <style lang="scss" module>
@@ -288,17 +310,17 @@ const previewable = (file: Misskey.entities.DriveFile): boolean => {
 
 :global(.pswp) {
 	--pswp-root-z-index: var(--mk-pswp-root-z-index, 2000700) !important;
-	--pswp-bg: var(--modalBg) !important;
+	--pswp-bg: var(--MI_THEME-modalBg) !important;
 }
 </style>
 
 <style lang="scss">
 .pswp__bg {
-	background: var(--modalBg);
-	backdrop-filter: var(--modalBgFilter);
+	background: var(--MI_THEME-modalBg);
+	backdrop-filter: var(--MI-modalBgFilter);
 }
 
-.pwsp__alt-text-container {
+.pswp__alt-text-container {
 	display: flex;
 	flex-direction: row;
 	align-items: center;
@@ -312,15 +334,15 @@ const previewable = (file: Misskey.entities.DriveFile): boolean => {
 	max-width: 800px;
 }
 
-.pwsp__alt-text {
-	color: var(--fg);
+.pswp__alt-text {
+	color: var(--MI_THEME-fg);
 	margin: 0 auto;
 	text-align: center;
-	padding: var(--margin);
-	border-radius: var(--radius);
+	padding: var(--MI-margin);
+	border-radius: var(--MI-radius);
 	max-height: 8em;
 	overflow-y: auto;
-	text-shadow: var(--bg) 0 0 10px, var(--bg) 0 0 3px, var(--bg) 0 0 3px;
+	text-shadow: var(--MI_THEME-bg) 0 0 10px, var(--MI_THEME-bg) 0 0 3px, var(--MI_THEME-bg) 0 0 3px;
 	white-space: pre-line;
 }
 </style>
diff --git a/packages/frontend/src/components/MkMediaRange.vue b/packages/frontend/src/components/MkMediaRange.vue
index 86ed8ba2cf..df7505b0c3 100644
--- a/packages/frontend/src/components/MkMediaRange.vue
+++ b/packages/frontend/src/components/MkMediaRange.vue
@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <!-- Media系専用のinput range -->
 <template>
-<div :style="sliderBgWhite ? '--sliderBg: rgba(255,255,255,.25);' : '--sliderBg: var(--scrollbarHandle);'">
+<div :style="sliderBgWhite ? '--sliderBg: rgba(255,255,255,.25);' : '--sliderBg: var(--MI_THEME-scrollbarHandle);'">
 	<div :class="$style.controlsSeekbar">
 		<progress v-if="buffer !== undefined" :class="$style.buffer" :value="isNaN(buffer) ? 0 : buffer" min="0" max="1">{{ Math.round(buffer * 100) }}% buffered</progress>
 		<input v-model="model" :class="$style.seek" :style="`--value: ${modelValue * 100}%;`" type="range" min="0" max="1" step="any" @change="emit('dragEnded', modelValue)"/>
@@ -48,7 +48,7 @@ const modelValue = computed({
 	background: transparent;
 	border: 0;
 	border-radius: 26px;
-	color: var(--accent);
+	color: var(--MI_THEME-accent);
 	display: block;
 	height: 19px;
 	margin: 0;
diff --git a/packages/frontend/src/components/MkMediaVideo.vue b/packages/frontend/src/components/MkMediaVideo.vue
index 1e3868bc36..d3a12ca734 100644
--- a/packages/frontend/src/components/MkMediaVideo.vue
+++ b/packages/frontend/src/components/MkMediaVideo.vue
@@ -18,7 +18,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 	@contextmenu.stop
 	@keydown.stop
 >
-	<button v-if="hide" :class="$style.hidden" @click="hide = false">
+	<button v-if="hide" :class="$style.hidden" @click="show">
 		<div :class="$style.hiddenTextWrapper">
 			<b v-if="video.isSensitive" style="display: block;"><i class="ti ti-eye-exclamation"></i> {{ i18n.ts.sensitive }}{{ defaultStore.state.dataSaver.media ? ` (${i18n.ts.video}${video.size ? ' ' + bytes(video.size) : ''})` : '' }}</b>
 			<b v-else style="display: block;"><i class="ti ti-movie"></i> {{ defaultStore.state.dataSaver.media && video.size ? bytes(video.size) : i18n.ts.video }}</b>
@@ -42,7 +42,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 		<i class="ti ti-eye-off" :class="$style.hide" @click="hide = true"></i>
 		<div :class="$style.indicators">
 			<div v-if="video.comment" :class="$style.indicator">ALT</div>
-			<div v-if="video.isSensitive" :class="$style.indicator" style="color: var(--warn);" :title="i18n.ts.sensitive"><i class="ti ti-eye-exclamation"></i></div>
+			<div v-if="video.isSensitive" :class="$style.indicator" style="color: var(--MI_THEME-warn);" :title="i18n.ts.sensitive"><i class="ti ti-eye-exclamation"></i></div>
 		</div>
 	</div>
 
@@ -67,7 +67,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 		<i class="ti ti-eye-off" :class="$style.hide" @click="hide = true"></i>
 		<div :class="$style.indicators">
 			<div v-if="video.comment" :class="$style.indicator">ALT</div>
-			<div v-if="video.isSensitive" :class="$style.indicator" style="color: var(--warn);" :title="i18n.ts.sensitive"><i class="ti ti-eye-exclamation"></i></div>
+			<div v-if="video.isSensitive" :class="$style.indicator" style="color: var(--MI_THEME-warn);" :title="i18n.ts.sensitive"><i class="ti ti-eye-exclamation"></i></div>
 		</div>
 		<div :class="$style.videoControls" @click.self="togglePlayPause">
 			<div :class="[$style.controlsChild, $style.controlsLeft]">
@@ -112,6 +112,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { ref, shallowRef, computed, watch, onDeactivated, onActivated, onMounted } from 'vue';
 import * as Misskey from 'misskey-js';
 import type { MenuItem } from '@/types/menu.js';
+import { type Keymap } from '@/scripts/hotkey.js';
 import bytes from '@/filters/bytes.js';
 import { hms } from '@/filters/hms.js';
 import { defaultStore } from '@/store.js';
@@ -127,32 +128,44 @@ const props = defineProps<{
 }>();
 
 const keymap = {
-	'up': () => {
-		if (hasFocus() && videoEl.value) {
-			volume.value = Math.min(volume.value + 0.1, 1);
-		}
+	'up': {
+		allowRepeat: true,
+		callback: () => {
+			if (hasFocus() && videoEl.value) {
+				volume.value = Math.min(volume.value + 0.1, 1);
+			}
+		},
 	},
-	'down': () => {
-		if (hasFocus() && videoEl.value) {
-			volume.value = Math.max(volume.value - 0.1, 0);
-		}
+	'down': {
+		allowRepeat: true,
+		callback: () => {
+			if (hasFocus() && videoEl.value) {
+				volume.value = Math.max(volume.value - 0.1, 0);
+			}
+		},
 	},
-	'left': () => {
-		if (hasFocus() && videoEl.value) {
-			videoEl.value.currentTime = Math.max(videoEl.value.currentTime - 5, 0);
-		}
+	'left': {
+		allowRepeat: true,
+		callback: () => {
+			if (hasFocus() && videoEl.value) {
+				videoEl.value.currentTime = Math.max(videoEl.value.currentTime - 5, 0);
+			}
+		},
 	},
-	'right': () => {
-		if (hasFocus() && videoEl.value) {
-			videoEl.value.currentTime = Math.min(videoEl.value.currentTime + 5, videoEl.value.duration);
-		}
+	'right': {
+		allowRepeat: true,
+		callback: () => {
+			if (hasFocus() && videoEl.value) {
+				videoEl.value.currentTime = Math.min(videoEl.value.currentTime + 5, videoEl.value.duration);
+			}
+		},
 	},
 	'space': () => {
 		if (hasFocus()) {
 			togglePlayPause();
 		}
 	},
-};
+} as const satisfies Keymap;
 
 // PlayerElもしくはその子要素にフォーカスがあるかどうか
 function hasFocus() {
@@ -160,16 +173,26 @@ function hasFocus() {
 	return playerEl.value === document.activeElement || playerEl.value.contains(document.activeElement);
 }
 
-// eslint-disable-next-line vue/no-setup-props-destructure
+// eslint-disable-next-line vue/no-setup-props-reactivity-loss
 const hide = ref((defaultStore.state.nsfw === 'force' || defaultStore.state.dataSaver.media) ? true : (props.video.isSensitive && defaultStore.state.nsfw !== 'ignore'));
 
+async function show() {
+	if (props.video.isSensitive && defaultStore.state.confirmWhenRevealingSensitiveMedia) {
+		const { canceled } = await os.confirm({
+			type: 'question',
+			text: i18n.ts.sensitiveMediaRevealConfirm,
+		});
+		if (canceled) return;
+	}
+
+	hide.value = false;
+}
+
 // Menu
 const menuShowing = ref(false);
 
 function showMenu(ev: MouseEvent) {
-	let menu: MenuItem[] = [];
-
-	menu = [
+	const menu: MenuItem[] = [
 		// TODO: 再生キューに追加
 		{
 			type: 'switch',
@@ -222,7 +245,7 @@ function showMenu(ev: MouseEvent) {
 		menu.push({
 			type: 'divider',
 		}, {
-			type: 'link' as const,
+			type: 'link',
 			text: i18n.ts._fileViewer.title,
 			icon: 'ti ti-info-circle',
 			to: `/my/drive/file/${props.video.id}`,
@@ -468,7 +491,7 @@ onDeactivated(() => {
 	position: relative;
 	overflow: clip;
 
-	&:focus {
+	&:focus-visible {
 		outline: none;
 	}
 }
@@ -485,7 +508,7 @@ onDeactivated(() => {
 		height: 100%;
 		pointer-events: none;
 		border-radius: inherit;
-		box-shadow: inset 0 0 0 4px var(--warn);
+		box-shadow: inset 0 0 0 4px var(--MI_THEME-warn);
 	}
 }
 
@@ -500,10 +523,10 @@ onDeactivated(() => {
 }
 
 .indicator {
-	/* Hardcode to black because either --bg or --fg makes it hard to read in dark/light mode */
+	/* Hardcode to black because either --MI_THEME-bg or --MI_THEME-fg makes it hard to read in dark/light mode */
 	background-color: black;
 	border-radius: 6px;
-	color: var(--accentLighten);
+	color: var(--MI_THEME-accentLighten);
 	display: inline-block;
 	font-weight: bold;
 	font-size: 0.8em;
@@ -514,8 +537,8 @@ onDeactivated(() => {
 	display: block;
 	position: absolute;
 	border-radius: 6px;
-	background-color: var(--fg);
-	color: var(--accentLighten);
+	background-color: var(--MI_THEME-fg);
+	color: var(--MI_THEME-accentLighten);
 	font-size: 12px;
 	opacity: .5;
 	padding: 5px 8px;
@@ -569,12 +592,16 @@ onDeactivated(() => {
 	opacity: 0;
 	transition: opacity .4s ease-in-out;
 
-	background: var(--accent);
+	background: var(--MI_THEME-accent);
 	color: #fff;
 	padding: 1rem;
 	border-radius: 99rem;
 
 	font-size: 1.1rem;
+
+	&:focus-visible {
+		outline: none;
+	}
 }
 
 .videoLoading {
@@ -631,12 +658,16 @@ onDeactivated(() => {
 
 	.controlButton {
 		padding: 6px;
-		border-radius: calc(var(--radius) / 2);
+		border-radius: calc(var(--MI-radius) / 2);
 		transition: background-color .2s ease-in-out;
 		font-size: 1.05rem;
 
 		&:hover {
-			background-color: var(--accent);
+			background-color: var(--MI_THEME-accent);
+		}
+
+		&:focus-visible {
+			outline: none;
 		}
 	}
 }
diff --git a/packages/frontend/src/components/MkMention.vue b/packages/frontend/src/components/MkMention.vue
index bfb49a416e..ac2d3f4398 100644
--- a/packages/frontend/src/components/MkMention.vue
+++ b/packages/frontend/src/components/MkMention.vue
@@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 -->
 
 <template>
-<MkA v-user-preview="canonical" :class="[$style.root, { [$style.isMe]: isMe }]" :to="url" :style="{ background: bgCss }" :behavior="navigationBehavior">
+<MkA v-user-preview="canonical" :class="[$style.root, { [$style.isMe]: isMe }]" :to="url" :behavior="navigationBehavior">
 	<img :class="$style.icon" :src="avatarUrl" alt="">
 	<span>
 		<span>@{{ username }}</span>
@@ -16,8 +16,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 <script lang="ts" setup>
 import { toUnicode } from 'punycode';
 import { computed } from 'vue';
-import tinycolor from 'tinycolor2';
-import { host as localHost } from '@/config.js';
+import { host as localHost } from '@@/js/config.js';
 import { $i } from '@/account.js';
 import { defaultStore } from '@/store.js';
 import { getStaticImageUrl } from '@/scripts/media-proxy.js';
@@ -37,11 +36,7 @@ const isMe = $i && (
 	`@${props.username}@${toUnicode(props.host)}` === `@${$i.username}@${toUnicode(localHost)}`.toLowerCase()
 );
 
-const bg = tinycolor(getComputedStyle(document.documentElement).getPropertyValue(isMe ? '--mentionMe' : '--mention'));
-bg.setAlpha(0.1);
-const bgCss = bg.toRgbString();
-
-const avatarUrl = computed(() => defaultStore.state.disableShowingAnimatedImages
+const avatarUrl = computed(() => defaultStore.state.disableShowingAnimatedImages || defaultStore.state.dataSaver.avatar
 	? getStaticImageUrl(`/avatar/@${props.username}@${props.host}`)
 	: `/avatar/@${props.username}@${props.host}`,
 );
@@ -52,10 +47,12 @@ const avatarUrl = computed(() => defaultStore.state.disableShowingAnimatedImages
 	display: inline-block;
 	padding: 4px 8px 4px 4px;
 	border-radius: 999px;
-	color: var(--mention);
+	color: var(--MI_THEME-mention);
+	background: color(from var(--MI_THEME-mention) srgb r g b / 0.1);
 
 	&.isMe {
-		color: var(--mentionMe);
+		color: var(--MI_THEME-mentionMe);
+		background: color(from var(--MI_THEME-mentionMe) srgb r g b / 0.1);
 	}
 }
 
diff --git a/packages/frontend/src/components/MkMenu.child.vue b/packages/frontend/src/components/MkMenu.child.vue
index dfb6d34618..086573ba6d 100644
--- a/packages/frontend/src/components/MkMenu.child.vue
+++ b/packages/frontend/src/components/MkMenu.child.vue
@@ -10,16 +10,15 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { nextTick, onMounted, onUnmounted, shallowRef, watch } from 'vue';
+import { nextTick, onMounted, onUnmounted, provide, shallowRef, watch } from 'vue';
 import MkMenu from './MkMenu.vue';
-import { MenuItem } from '@/types/menu.js';
+import type { MenuItem } from '@/types/menu.js';
 
 const props = defineProps<{
 	items: MenuItem[];
 	targetElement: HTMLElement;
 	rootElement: HTMLElement;
 	width?: number;
-	viaKeyboard?: boolean;
 }>();
 
 const emit = defineEmits<{
@@ -27,6 +26,8 @@ const emit = defineEmits<{
 	(ev: 'actioned'): void;
 }>();
 
+provide('isNestingMenu', true);
+
 const el = shallowRef<HTMLElement>();
 const align = 'left';
 
diff --git a/packages/frontend/src/components/MkMenu.vue b/packages/frontend/src/components/MkMenu.vue
index d91239b9e2..13a65e411f 100644
--- a/packages/frontend/src/components/MkMenu.vue
+++ b/packages/frontend/src/components/MkMenu.vue
@@ -4,44 +4,97 @@ SPDX-License-Identifier: AGPL-3.0-only
 -->
 
 <template>
-<div role="menu">
+<div
+	role="menu"
+	:class="{
+		[$style.root]: true,
+		[$style.center]: align === 'center',
+		[$style.big]: big,
+		[$style.asDrawer]: asDrawer,
+	}"
+	@focusin.passive.stop="() => {}"
+>
 	<div
-		ref="itemsEl" v-hotkey="keymap"
+		ref="itemsEl"
+		v-hotkey="keymap"
+		tabindex="0"
 		class="_popup _shadow"
-		:class="[$style.root, { [$style.center]: align === 'center', [$style.asDrawer]: asDrawer }]"
-		:style="{ width: (width && !asDrawer) ? width + 'px' : '', maxHeight: maxHeight ? maxHeight + 'px' : '' }"
-		@contextmenu.self="e => e.preventDefault()"
+		:class="$style.menu"
+		:style="{
+			width: (width && !asDrawer) ? `${width}px` : '',
+			maxHeight: maxHeight ? `min(${maxHeight}px, calc(100dvh - 32px))` : 'calc(100dvh - 32px)',
+		}"
+		@keydown.stop="() => {}"
+		@contextmenu.self.prevent="() => {}"
 	>
-		<template v-for="(item, i) in (items2 ?? [])">
-			<div v-if="item.type === 'divider'" role="separator" :class="$style.divider"></div>
-			<span v-else-if="item.type === 'label'" role="menuitem" :class="[$style.label, $style.item]">
+		<template v-for="item in (items2 ?? [])">
+			<div v-if="item.type === 'divider'" role="separator" tabindex="-1" :class="$style.divider"></div>
+			<span v-else-if="item.type === 'label'" role="menuitem" tabindex="-1" :class="[$style.label, $style.item]">
 				<span style="opacity: 0.7;">{{ item.text }}</span>
 			</span>
-			<span v-else-if="item.type === 'pending'" role="menuitem" :tabindex="i" :class="[$style.pending, $style.item]">
+			<span v-else-if="item.type === 'pending'" role="menuitem" tabindex="0" :class="[$style.pending, $style.item]">
 				<span><MkEllipsis/></span>
 			</span>
-			<MkA v-else-if="item.type === 'link'" role="menuitem" :to="item.to" :tabindex="i" class="_button" :class="$style.item" @click.passive="close(true)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
+			<MkA
+				v-else-if="item.type === 'link'"
+				role="menuitem"
+				tabindex="0"
+				:class="['_button', $style.item]"
+				:to="item.to"
+				@click.passive="close(true)"
+				@mouseenter.passive="onItemMouseEnter"
+				@mouseleave.passive="onItemMouseLeave"
+			>
 				<i v-if="item.icon" class="ti-fw" :class="[$style.icon, item.icon]"></i>
 				<MkAvatar v-if="item.avatar" :user="item.avatar" :class="$style.avatar"/>
 				<div :class="$style.item_content">
 					<span :class="$style.item_content_text">{{ item.text }}</span>
-					<span v-if="item.indicate" :class="$style.indicator"><i class="_indicatorCircle"></i></span>
+					<span v-if="item.indicate" :class="$style.indicator" class="_blink"><i class="_indicatorCircle"></i></span>
 				</div>
 			</MkA>
-			<a v-else-if="item.type === 'a'" role="menuitem" :href="item.href" :target="item.target" :download="item.download" :tabindex="i" class="_button" :class="$style.item" @click="close(true)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
+			<a
+				v-else-if="item.type === 'a'"
+				role="menuitem"
+				tabindex="0"
+				:class="['_button', $style.item]"
+				:href="item.href"
+				:target="item.target"
+				:rel="item.target === '_blank' ? 'noopener noreferrer' : undefined"
+				:download="item.download"
+				@click.passive="close(true)"
+				@mouseenter.passive="onItemMouseEnter"
+				@mouseleave.passive="onItemMouseLeave"
+			>
 				<i v-if="item.icon" class="ti-fw" :class="[$style.icon, item.icon]"></i>
 				<div :class="$style.item_content">
 					<span :class="$style.item_content_text">{{ item.text }}</span>
-					<span v-if="item.indicate" :class="$style.indicator"><i class="_indicatorCircle"></i></span>
+					<span v-if="item.indicate" :class="$style.indicator" class="_blink"><i class="_indicatorCircle"></i></span>
 				</div>
 			</a>
-			<button v-else-if="item.type === 'user'" role="menuitem" :tabindex="i" class="_button" :class="[$style.item, { [$style.active]: item.active }]" :disabled="item.active" @click="clicked(item.action, $event)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
+			<button
+				v-else-if="item.type === 'user'"
+				role="menuitem"
+				tabindex="0"
+				:class="['_button', $style.item, { [$style.active]: item.active }]"
+				@click.prevent="item.active ? close(false) : clicked(item.action, $event)"
+				@mouseenter.passive="onItemMouseEnter"
+				@mouseleave.passive="onItemMouseLeave"
+			>
 				<MkAvatar :user="item.user" :class="$style.avatar"/><MkUserName :user="item.user"/>
 				<div v-if="item.indicate" :class="$style.item_content">
-					<span :class="$style.indicator"><i class="_indicatorCircle"></i></span>
+					<span :class="$style.indicator" class="_blink"><i class="_indicatorCircle"></i></span>
 				</div>
 			</button>
-			<button v-else-if="item.type === 'switch'" role="menuitemcheckbox" :tabindex="i" class="_button" :class="[$style.item, $style.switch, { [$style.switchDisabled]: item.disabled } ]" @click="switchItem(item)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
+			<button
+				v-else-if="item.type === 'switch'"
+				role="menuitemcheckbox"
+				tabindex="0"
+				:class="['_button', $style.item]"
+				:disabled="unref(item.disabled)"
+				@click.prevent="switchItem(item)"
+				@mouseenter.passive="onItemMouseEnter"
+				@mouseleave.passive="onItemMouseLeave"
+			>
 				<i v-if="item.icon" class="ti-fw" :class="[$style.icon, item.icon]"></i>
 				<MkSwitchButton v-else :class="$style.switchButton" :checked="item.ref" :disabled="item.disabled" @toggle="switchItem(item)"/>
 				<div :class="$style.item_content">
@@ -49,55 +102,89 @@ SPDX-License-Identifier: AGPL-3.0-only
 					<MkSwitchButton v-if="item.icon" :class="[$style.switchButton, $style.caret]" :checked="item.ref" :disabled="item.disabled" @toggle="switchItem(item)"/>
 				</div>
 			</button>
-			<button v-else-if="item.type === 'radio'" class="_button" role="menuitem" :tabindex="i" :class="[$style.item, $style.parent, { [$style.childShowing]: childShowingItem === item }]" @mouseenter="preferClick ? null : showRadioOptions(item, $event)" @click="!preferClick ? null : showRadioOptions(item, $event)">
+			<button
+				v-else-if="item.type === 'radio'"
+				role="menuitem"
+				tabindex="0"
+				:class="['_button', $style.item, $style.parent, { [$style.active]: childShowingItem === item }]"
+				:disabled="unref(item.disabled)"
+				@mouseenter.prevent="preferClick ? null : showRadioOptions(item, $event)"
+				@keydown.enter.prevent="preferClick ? null : showRadioOptions(item, $event)"
+				@click.prevent="!preferClick ? null : showRadioOptions(item, $event)"
+			>
 				<i v-if="item.icon" class="ti-fw" :class="[$style.icon, item.icon]" style="pointer-events: none;"></i>
 				<div :class="$style.item_content">
 					<span :class="$style.item_content_text" style="pointer-events: none;">{{ item.text }}</span>
 					<span :class="$style.caret" style="pointer-events: none;"><i class="ti ti-chevron-right ti-fw"></i></span>
 				</div>
 			</button>
-			<button v-else-if="item.type === 'radioOption'" :tabindex="i" class="_button" role="menuitem" :class="[$style.item, { [$style.radioActive]: item.active }]" @click="clicked(item.action, $event, false)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
+			<button
+				v-else-if="item.type === 'radioOption'"
+				role="menuitemradio"
+				tabindex="0"
+				:class="['_button', $style.item, $style.radio, { [$style.active]: unref(item.active) }]"
+				@click.prevent="unref(item.active) ? null : clicked(item.action, $event, false)"
+				@mouseenter.passive="onItemMouseEnter"
+				@mouseleave.passive="onItemMouseLeave"
+			>
 				<div :class="$style.icon">
-					<span :class="[$style.radio, { [$style.radioChecked]: item.active }]"></span>
+					<span :class="[$style.radioIcon, { [$style.radioChecked]: unref(item.active) }]"></span>
 				</div>
 				<div :class="$style.item_content">
 					<span :class="$style.item_content_text">{{ item.text }}</span>
 				</div>
 			</button>
-			<button v-else-if="item.type === 'parent'" class="_button" role="menuitem" :tabindex="i" :class="[$style.item, $style.parent, { [$style.childShowing]: childShowingItem === item }]" @mouseenter="preferClick ? null : showChildren(item, $event)" @click="!preferClick ? null : showChildren(item, $event)">
+			<button
+				v-else-if="item.type === 'parent'"
+				role="menuitem"
+				tabindex="0"
+				:class="['_button', $style.item, $style.parent, { [$style.active]: childShowingItem === item }]"
+				@mouseenter.prevent="preferClick ? null : showChildren(item, $event)"
+				@keydown.enter.prevent="preferClick ? null : showChildren(item, $event)"
+				@click.prevent="!preferClick ? null : showChildren(item, $event)"
+			>
 				<i v-if="item.icon" class="ti-fw" :class="[$style.icon, item.icon]" style="pointer-events: none;"></i>
 				<div :class="$style.item_content">
 					<span :class="$style.item_content_text" style="pointer-events: none;">{{ item.text }}</span>
 					<span :class="$style.caret" style="pointer-events: none;"><i class="ti ti-chevron-right ti-fw"></i></span>
 				</div>
 			</button>
-			<button v-else :tabindex="i" class="_button" role="menuitem" :class="[$style.item, { [$style.danger]: item.danger, [$style.active]: getValue(item.active) }]" :disabled="getValue(item.active)" @click="clicked(item.action, $event)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
+			<button
+				v-else role="menuitem"
+				tabindex="0"
+				:class="['_button', $style.item, { [$style.danger]: item.danger, [$style.active]: unref(item.active) }]"
+				@click.prevent="unref(item.active) ? close(false) : clicked(item.action, $event)"
+				@mouseenter.passive="onItemMouseEnter"
+				@mouseleave.passive="onItemMouseLeave"
+			>
 				<i v-if="item.icon" class="ti-fw" :class="[$style.icon, item.icon]"></i>
 				<MkAvatar v-if="item.avatar" :user="item.avatar" :class="$style.avatar"/>
 				<div :class="$style.item_content">
 					<span :class="$style.item_content_text">{{ item.text }}</span>
-					<span v-if="item.indicate" :class="$style.indicator"><i class="_indicatorCircle"></i></span>
+					<span v-if="item.indicate" :class="$style.indicator" class="_blink"><i class="_indicatorCircle"></i></span>
 				</div>
 			</button>
 		</template>
-		<span v-if="items2 == null || items2.length === 0" :class="[$style.none, $style.item]">
+		<span v-if="items2 == null || items2.length === 0" tabindex="-1" :class="[$style.none, $style.item]">
 			<span>{{ i18n.ts.none }}</span>
 		</span>
 	</div>
 	<div v-if="childMenu">
-		<XChild ref="child" :items="childMenu" :targetElement="childTarget!" :rootElement="itemsEl!" showing @actioned="childActioned" @close="close(false)"/>
+		<XChild ref="child" :items="childMenu" :targetElement="childTarget!" :rootElement="itemsEl!" @actioned="childActioned" @closed="closeChild"/>
 	</div>
 </div>
 </template>
 
 <script lang="ts">
-import { ComputedRef, computed, defineAsyncComponent, isRef, nextTick, onBeforeUnmount, onMounted, ref, shallowRef, watch } from 'vue';
-import { focusPrev, focusNext } from '@/scripts/focus.js';
+import { computed, defineAsyncComponent, inject, nextTick, onBeforeUnmount, onMounted, ref, shallowRef, unref, watch } from 'vue';
 import MkSwitchButton from '@/components/MkSwitch.button.vue';
 import { MenuItem, InnerMenuItem, MenuPending, MenuAction, MenuSwitch, MenuRadio, MenuRadioOption, MenuParent } from '@/types/menu.js';
 import * as os from '@/os.js';
 import { i18n } from '@/i18n.js';
 import { isTouchUsing } from '@/scripts/touch.js';
+import { type Keymap } from '@/scripts/hotkey.js';
+import { isFocusable } from '@/scripts/focus.js';
+import { getNodeOrNull } from '@/scripts/get-dom-node-or-null.js';
 
 const childrenCache = new WeakMap<MenuParent, MenuItem[]>();
 </script>
@@ -107,7 +194,6 @@ const XChild = defineAsyncComponent(() => import('./MkMenu.child.vue'));
 
 const props = defineProps<{
 	items: MenuItem[];
-	viaKeyboard?: boolean;
 	asDrawer?: boolean;
 	align?: 'center' | string;
 	width?: number;
@@ -119,17 +205,30 @@ const emit = defineEmits<{
 	(ev: 'hide'): void;
 }>();
 
-const itemsEl = shallowRef<HTMLDivElement>();
+const big = isTouchUsing;
+
+const isNestingMenu = inject<boolean>('isNestingMenu', false);
+
+const itemsEl = shallowRef<HTMLElement>();
 
 const items2 = ref<InnerMenuItem[]>();
 
 const child = shallowRef<InstanceType<typeof XChild>>();
 
-const keymap = computed(() => ({
-	'up|k|shift+tab': focusUp,
-	'down|j|tab': focusDown,
-	'esc': close,
-}));
+const keymap = {
+	'up|k|shift+tab': {
+		allowRepeat: true,
+		callback: () => focusUp(),
+	},
+	'down|j|tab': {
+		allowRepeat: true,
+		callback: () => focusDown(),
+	},
+	'esc': {
+		allowRepeat: true,
+		callback: () => close(false),
+	},
+} as const satisfies Keymap;
 
 const childShowingItem = ref<MenuItem | null>();
 
@@ -167,25 +266,19 @@ function childActioned() {
 	close(true);
 }
 
-const onGlobalMousedown = (event: MouseEvent) => {
-	if (childTarget.value && (event.target === childTarget.value || childTarget.value.contains(event.target as Node))) return;
-	if (child.value && child.value.checkHit(event)) return;
-	closeChild();
-};
-
 let childCloseTimer: null | number = null;
 
-function onItemMouseEnter(item) {
+function onItemMouseEnter() {
 	childCloseTimer = window.setTimeout(() => {
 		closeChild();
 	}, 300);
 }
 
-function onItemMouseLeave(item) {
+function onItemMouseLeave() {
 	if (childCloseTimer) window.clearTimeout(childCloseTimer);
 }
 
-async function showRadioOptions(item: MenuRadio, ev: MouseEvent) {
+async function showRadioOptions(item: MenuRadio, ev: Event) {
 	const children: MenuItem[] = Object.keys(item.options).map<MenuRadioOption>(key => {
 		const value = item.options[key];
 		return {
@@ -200,7 +293,7 @@ async function showRadioOptions(item: MenuRadio, ev: MouseEvent) {
 
 	if (props.asDrawer) {
 		os.popupMenu(children, ev.currentTarget ?? ev.target).finally(() => {
-			emit('close');
+			close(false);
 		});
 		emit('hide');
 	} else {
@@ -210,7 +303,9 @@ async function showRadioOptions(item: MenuRadio, ev: MouseEvent) {
 	}
 }
 
-async function showChildren(item: MenuParent, ev: MouseEvent) {
+async function showChildren(item: MenuParent, ev: Event) {
+	ev.stopPropagation();
+
 	const children: MenuItem[] = await (async () => {
 		if (childrenCache.has(item)) {
 			return childrenCache.get(item)!;
@@ -227,7 +322,7 @@ async function showChildren(item: MenuParent, ev: MouseEvent) {
 
 	if (props.asDrawer) {
 		os.popupMenu(children, ev.currentTarget ?? ev.target).finally(() => {
-			emit('close');
+			close(false);
 		});
 		emit('hide');
 	} else {
@@ -246,15 +341,11 @@ function clicked(fn: MenuAction, ev: MouseEvent, doClose = true) {
 }
 
 function close(actioned = false) {
-	emit('close', actioned);
-}
-
-function focusUp() {
-	focusPrev(document.activeElement);
-}
-
-function focusDown() {
-	focusNext(document.activeElement);
+	disposeHandlers();
+	nextTick(() => {
+		closeChild();
+		emit('close', actioned);
+	});
 }
 
 function switchItem(item: MenuSwitch & { ref: any }) {
@@ -262,30 +353,134 @@ function switchItem(item: MenuSwitch & { ref: any }) {
 	item.ref = !item.ref;
 }
 
-function getValue<T>(item?: ComputedRef<T> | T) {
-	return isRef(item) ? item.value : item;
+function focusUp() {
+	if (disposed) return;
+	if (!itemsEl.value?.contains(document.activeElement)) return;
+
+	const focusableElements = Array.from(itemsEl.value.children).filter(isFocusable);
+	const activeIndex = focusableElements.findIndex(el => el === document.activeElement);
+	const targetIndex = (activeIndex !== -1 && activeIndex !== 0) ? (activeIndex - 1) : (focusableElements.length - 1);
+	const targetElement = focusableElements.at(targetIndex) ?? itemsEl.value;
+
+	targetElement.focus();
 }
 
-onMounted(() => {
-	if (props.viaKeyboard) {
-		nextTick(() => {
-			if (itemsEl.value) focusNext(itemsEl.value.children[0], true, false);
-		});
+function focusDown() {
+	if (disposed) return;
+	if (!itemsEl.value?.contains(document.activeElement)) return;
+
+	const focusableElements = Array.from(itemsEl.value.children).filter(isFocusable);
+	const activeIndex = focusableElements.findIndex(el => el === document.activeElement);
+	const targetIndex = (activeIndex !== -1 && activeIndex !== (focusableElements.length - 1)) ? (activeIndex + 1) : 0;
+	const targetElement = focusableElements.at(targetIndex) ?? itemsEl.value;
+
+	targetElement.focus();
+}
+
+const onGlobalFocusin = (ev: FocusEvent) => {
+	if (disposed) return;
+	if (itemsEl.value?.parentElement?.contains(getNodeOrNull(ev.target))) return;
+	nextTick(() => {
+		if (itemsEl.value != null && isFocusable(itemsEl.value)) {
+			itemsEl.value.focus({ preventScroll: true });
+			nextTick(() => focusDown());
+		}
+	});
+};
+
+const onGlobalMousedown = (ev: MouseEvent) => {
+	if (disposed) return;
+	if (childTarget.value?.contains(getNodeOrNull(ev.target))) return;
+	if (child.value?.checkHit(ev)) return;
+	closeChild();
+};
+
+const setupHandlers = () => {
+	if (!isNestingMenu) {
+		document.addEventListener('focusin', onGlobalFocusin, { passive: true });
 	}
-
-	// TODO: アクティブな要素までスクロール
-	//itemsEl.scrollTo();
-
 	document.addEventListener('mousedown', onGlobalMousedown, { passive: true });
+};
+
+let disposed = false;
+
+const disposeHandlers = () => {
+	disposed = true;
+	if (!isNestingMenu) {
+		document.removeEventListener('focusin', onGlobalFocusin);
+	}
+	document.removeEventListener('mousedown', onGlobalMousedown);
+};
+
+onMounted(() => {
+	setupHandlers();
+
+	if (!isNestingMenu) {
+		nextTick(() => itemsEl.value?.focus({ preventScroll: true }));
+	}
 });
 
 onBeforeUnmount(() => {
-	document.removeEventListener('mousedown', onGlobalMousedown);
+	disposeHandlers();
 });
 </script>
 
 <style lang="scss" module>
 .root {
+	&.center {
+		> .menu {
+			> .item {
+				text-align: center;
+			}
+		}
+	}
+
+	&.big:not(.asDrawer) {
+		> .menu {
+			min-width: 230px;
+
+			> .item {
+				padding: 6px 20px;
+				font-size: 0.95em;
+				line-height: 24px;
+			}
+		}
+	}
+
+	&.asDrawer {
+		max-width: 600px;
+		margin: auto;
+
+		> .menu {
+			padding: 12px 0 max(env(safe-area-inset-bottom, 0px), 12px) 0;
+			width: 100%;
+			border-radius: 24px;
+			border-bottom-right-radius: 0;
+			border-bottom-left-radius: 0;
+
+			> .item {
+				font-size: 1em;
+				padding: 12px 24px;
+
+				&::before {
+					width: calc(100% - 24px);
+					border-radius: 12px;
+				}
+
+				> .icon {
+					margin-right: 14px;
+					width: 24px;
+				}
+			}
+
+			> .divider {
+				margin: 12px 0;
+			}
+		}
+	}
+}
+
+.menu {
 	padding: 8px 0;
 	box-sizing: border-box;
 	max-width: 100vw;
@@ -293,37 +488,8 @@ onBeforeUnmount(() => {
 	overflow: auto;
 	overscroll-behavior: contain;
 
-	&.center {
-		> .item {
-			text-align: center;
-		}
-	}
-
-	&.asDrawer {
-		padding: 12px 0 max(env(safe-area-inset-bottom, 0px), 12px) 0;
-		width: 100%;
-		border-radius: 24px;
-		border-bottom-right-radius: 0;
-		border-bottom-left-radius: 0;
-
-		> .item {
-			font-size: 1em;
-			padding: 12px 24px;
-
-			&:before {
-				width: calc(100% - 24px);
-				border-radius: 12px;
-			}
-
-			> .icon {
-				margin-right: 14px;
-				width: 24px;
-			}
-		}
-
-		> .divider {
-			margin: 12px 0;
-		}
+	&:focus-visible {
+		outline: none;
 	}
 }
 
@@ -340,8 +506,10 @@ onBeforeUnmount(() => {
 	text-align: left;
 	overflow: hidden;
 	text-overflow: ellipsis;
+	text-decoration: none !important;
+	color: var(--menuFg, var(--MI_THEME-fg));
 
-	&:before {
+	&::before {
 		content: "";
 		display: block;
 		position: absolute;
@@ -355,56 +523,56 @@ onBeforeUnmount(() => {
 		border-radius: 6px;
 	}
 
-	&:not(:disabled):hover {
-		color: var(--accent);
-		text-decoration: none;
+	&:focus-visible {
+		outline: none;
 
-		&:before {
-			background: var(--accentedBg);
+		&:not(:hover):not(:active)::before {
+			outline: var(--MI_THEME-focus) solid 2px;
+			outline-offset: -2px;
 		}
 	}
 
+	&:not(:disabled) {
+		&:hover,
+		&:focus-visible:active,
+		&:focus-visible.active {
+			color: var(--menuHoverFg, var(--MI_THEME-accent));
+
+			&::before {
+				background-color: var(--menuHoverBg, var(--MI_THEME-accentedBg));
+			}
+		}
+
+		&:not(:focus-visible):active,
+		&:not(:focus-visible).active {
+			color: var(--menuActiveFg, var(--MI_THEME-fgOnAccent));
+
+			&::before {
+				background-color: var(--menuActiveBg, var(--MI_THEME-accent));
+			}
+		}
+	}
+
+	&:disabled {
+		cursor: not-allowed;
+	}
+
 	&.danger {
-		color: #ff2a2a;
-
-		&:hover {
-			color: #fff;
-
-			&:before {
-				background: #ff4242;
-			}
-		}
-
-		&:active {
-			color: #fff;
-
-			&:before {
-				background: #d42e2e !important;
-			}
-		}
+		--menuFg: #ff2a2a;
+		--menuHoverFg: #fff;
+		--menuHoverBg: #ff4242;
+		--menuActiveFg: #fff;
+		--menuActiveBg: #d42e2e;
 	}
 
-	&:active,
-	&.active {
-		color: var(--fgOnAccent) !important;
-		opacity: 1;
-
-		&:before {
-			background: var(--accent) !important;
-		}
+	&.radio {
+		--menuActiveFg: var(--MI_THEME-accent);
+		--menuActiveBg: var(--MI_THEME-accentedBg);
 	}
 
-	&.radioActive {
-		color: var(--accent) !important;
-		opacity: 1;
-
-		&:before {
-			background-color: var(--accentedBg) !important;
-		}
-	}
-
-	&:not(:active):focus-visible {
-		box-shadow: 0 0 0 2px var(--focus) inset;
+	&.parent {
+		--menuActiveFg: var(--MI_THEME-accent);
+		--menuActiveBg: var(--MI_THEME-accentedBg);
 	}
 
 	&.label {
@@ -422,22 +590,6 @@ onBeforeUnmount(() => {
 		pointer-events: none;
 		opacity: 0.7;
 	}
-
-	&.parent {
-		pointer-events: auto;
-		display: flex;
-		align-items: center;
-		cursor: default;
-
-		&.childShowing {
-			color: var(--accent);
-			text-decoration: none;
-
-			&:before {
-				background: var(--accentedBg);
-			}
-		}
-	}
 }
 
 .item_content {
@@ -456,18 +608,6 @@ onBeforeUnmount(() => {
 	overflow: hidden;
 }
 
-.switch {
-	position: relative;
-	display: flex;
-	transition: all 0.2s ease;
-	user-select: none;
-	cursor: pointer;
-}
-
-.switchDisabled {
-	cursor: not-allowed;
-}
-
 .switchButton {
 	margin-left: -2px;
 	--height: 1.35em;
@@ -479,14 +619,6 @@ onBeforeUnmount(() => {
 	text-overflow: ellipsis;
 }
 
-.switchInput {
-	position: absolute;
-	width: 0;
-	height: 0;
-	opacity: 0;
-	margin: 0;
-}
-
 .icon {
 	margin-right: 8px;
 	line-height: 1;
@@ -505,28 +637,27 @@ onBeforeUnmount(() => {
 .indicator {
 	display: flex;
 	align-items: center;
-	color: var(--indicator);
+	color: var(--MI_THEME-indicator);
 	font-size: 12px;
-	animation: global-blink 1s infinite;
 }
 
 .divider {
 	margin: 8px 0;
-	border-top: solid 0.5px var(--divider);
+	border-top: solid 0.5px var(--MI_THEME-divider);
 }
 
-.radio {
+.radioIcon {
 	display: inline-block;
 	position: relative;
 	width: 1em;
 	height: 1em;
-	vertical-align: -.125em;
+	vertical-align: -0.125em;
 	border-radius: 50%;
-	border: solid 2px var(--divider);
-	background-color: var(--panel);
+	border: solid 2px var(--MI_THEME-divider);
+	background-color: var(--MI_THEME-panel);
 
 	&.radioChecked {
-		border-color: var(--accent);
+		border-color: var(--MI_THEME-accent);
 
 		&::after {
 			content: "";
@@ -538,7 +669,7 @@ onBeforeUnmount(() => {
 			width: 50%;
 			height: 50%;
 			border-radius: 50%;
-			background-color: var(--accent);
+			background-color: var(--MI_THEME-accent);
 		}
 	}
 }
diff --git a/packages/frontend/src/components/MkMiniChart.vue b/packages/frontend/src/components/MkMiniChart.vue
index f2f2bf47a8..7ea585ecc2 100644
--- a/packages/frontend/src/components/MkMiniChart.vue
+++ b/packages/frontend/src/components/MkMiniChart.vue
@@ -34,7 +34,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { watch, ref } from 'vue';
 import { v4 as uuid } from 'uuid';
 import tinycolor from 'tinycolor2';
-import { useInterval } from '@/scripts/use-interval.js';
+import { useInterval } from '@@/js/use-interval.js';
 
 const props = defineProps<{
 	src: number[];
@@ -48,7 +48,7 @@ const polygonPoints = ref('');
 const headX = ref<number | null>(null);
 const headY = ref<number | null>(null);
 const clock = ref<number | null>(null);
-const accent = tinycolor(getComputedStyle(document.documentElement).getPropertyValue('--accent'));
+const accent = tinycolor(getComputedStyle(document.documentElement).getPropertyValue('--MI_THEME-accent'));
 const color = accent.toRgbString();
 
 function draw(): void {
diff --git a/packages/frontend/src/components/MkModal.vue b/packages/frontend/src/components/MkModal.vue
index 9e69ab2207..c766a33823 100644
--- a/packages/frontend/src/components/MkModal.vue
+++ b/packages/frontend/src/components/MkModal.vue
@@ -30,9 +30,9 @@ SPDX-License-Identifier: AGPL-3.0-only
 		[$style.transition_modal_leaveTo]: transitionName === 'modal',
 		[$style.transition_send_leaveTo]: transitionName === 'send',
 	})"
-	:duration="transitionDuration" appear @afterLeave="emit('closed')" @enter="emit('opening')" @afterEnter="onOpened"
+	:duration="transitionDuration" appear @afterLeave="onClosed" @enter="emit('opening')" @afterEnter="onOpened"
 >
-	<div v-show="manualShowing != null ? manualShowing : showing" v-hotkey.global="keymap" :class="[$style.root, { [$style.drawer]: type === 'drawer', [$style.dialog]: type === 'dialog', [$style.popup]: type === 'popup' }]" :style="{ zIndex, pointerEvents: (manualShowing != null ? manualShowing : showing) ? 'auto' : 'none', '--transformOrigin': transformOrigin }">
+	<div v-show="manualShowing != null ? manualShowing : showing" ref="modalRootEl" v-hotkey.global="keymap" :class="[$style.root, { [$style.drawer]: type === 'drawer', [$style.dialog]: type === 'dialog', [$style.popup]: type === 'popup' }]" :style="{ zIndex, pointerEvents: (manualShowing != null ? manualShowing : showing) ? 'auto' : 'none', '--transformOrigin': transformOrigin }">
 		<div data-cy-bg :data-cy-transparent="isEnableBgTransparent" class="_modalBg" :class="[$style.bg, { [$style.bgTransparent]: isEnableBgTransparent }]" :style="{ zIndex }" @click="onBgClick" @mousedown="onBgClick" @contextmenu.prevent.stop="() => {}"></div>
 		<div ref="content" :class="[$style.content, { [$style.fixed]: fixed }]" :style="{ zIndex }" @click.self="onBgClick">
 			<slot :max-height="maxHeight" :type="type"></slot>
@@ -47,6 +47,9 @@ import * as os from '@/os.js';
 import { isTouchUsing } from '@/scripts/touch.js';
 import { defaultStore } from '@/store.js';
 import { deviceKind } from '@/scripts/device-kind.js';
+import { type Keymap } from '@/scripts/hotkey.js';
+import { focusTrap } from '@/scripts/focus-trap.js';
+import { focusParent } from '@/scripts/focus.js';
 
 function getFixedContainer(el: Element | null): Element | null {
 	if (el == null || el.tagName === 'BODY') return null;
@@ -68,6 +71,8 @@ const props = withDefaults(defineProps<{
 	zPriority?: 'low' | 'middle' | 'high';
 	noOverlap?: boolean;
 	transparentBg?: boolean;
+	hasInteractionWithOtherFocusTrappedEls?: boolean;
+	returnFocusTo?: HTMLElement | null;
 }>(), {
 	manualShowing: null,
 	src: null,
@@ -76,6 +81,8 @@ const props = withDefaults(defineProps<{
 	zPriority: 'low',
 	noOverlap: true,
 	transparentBg: false,
+	hasInteractionWithOtherFocusTrappedEls: false,
+	returnFocusTo: null,
 });
 
 const emit = defineEmits<{
@@ -93,12 +100,13 @@ const maxHeight = ref<number>();
 const fixed = ref(false);
 const transformOrigin = ref('center');
 const showing = ref(true);
+const modalRootEl = shallowRef<HTMLElement>();
 const content = shallowRef<HTMLElement>();
 const zIndex = os.claimZIndex(props.zPriority);
 const useSendAnime = ref(false);
 const type = computed<ModalTypes>(() => {
 	if (props.preferType === 'auto') {
-		if (!defaultStore.state.disableDrawer && isTouchUsing && deviceKind === 'smartphone') {
+		if ((defaultStore.state.menuStyle === 'drawer') || (defaultStore.state.menuStyle === 'auto' && isTouchUsing && deviceKind === 'smartphone')) {
 			return 'drawer';
 		} else {
 			return props.src != null ? 'popup' : 'dialog';
@@ -131,6 +139,7 @@ const transitionDuration = computed((() =>
 					: 0
 ));
 
+let releaseFocusTrap: (() => void) | null = null;
 let contentClicking = false;
 
 function close(opts: { useSendAnimation?: boolean } = {}) {
@@ -154,8 +163,11 @@ if (type.value === 'drawer') {
 }
 
 const keymap = {
-	'esc': () => emit('esc'),
-};
+	'esc': {
+		allowRepeat: true,
+		callback: () => emit('esc'),
+	},
+} as const satisfies Keymap;
 
 const MARGIN = 16;
 const SCROLLBAR_THICKNESS = 16;
@@ -292,6 +304,10 @@ const onOpened = () => {
 	}, { passive: true });
 };
 
+const onClosed = () => {
+	emit('closed');
+};
+
 const alignObserver = new ResizeObserver((entries, observer) => {
 	align();
 });
@@ -309,6 +325,20 @@ onMounted(() => {
 		align();
 	}, { immediate: true });
 
+	watch([showing, () => props.manualShowing], ([showing, manualShowing]) => {
+		if (manualShowing === true || (manualShowing == null && showing === true)) {
+			if (modalRootEl.value != null) {
+				const { release } = focusTrap(modalRootEl.value, props.hasInteractionWithOtherFocusTrappedEls);
+
+				releaseFocusTrap = release;
+				modalRootEl.value.focus();
+			}
+		} else {
+			releaseFocusTrap?.();
+			focusParent(props.returnFocusTo ?? props.src, true, false);
+		}
+	}, { immediate: true });
+
 	nextTick(() => {
 		alignObserver.observe(content.value!);
 	});
diff --git a/packages/frontend/src/components/MkModalWindow.vue b/packages/frontend/src/components/MkModalWindow.vue
index d3657afa94..f06cfffee4 100644
--- a/packages/frontend/src/components/MkModalWindow.vue
+++ b/packages/frontend/src/components/MkModalWindow.vue
@@ -4,15 +4,15 @@ SPDX-License-Identifier: AGPL-3.0-only
 -->
 
 <template>
-<MkModal ref="modal" :preferType="'dialog'" @click="onBgClick" @closed="$emit('closed')">
-	<div ref="rootEl" :class="$style.root" :style="{ width: `${width}px`, height: `min(${height}px, 100%)` }" @keydown="onKeydown">
+<MkModal ref="modal" :preferType="'dialog'" @click="onBgClick" @closed="emit('closed')" @esc="emit('esc')">
+	<div ref="rootEl" :class="$style.root" :style="{ width: `${width}px`, height: `min(${height}px, 100%)` }">
 		<div ref="headerEl" :class="$style.header">
-			<button v-if="withOkButton" :class="$style.headerButton" class="_button" @click="$emit('close')"><i class="ti ti-x"></i></button>
+			<button v-if="withOkButton && withCloseButton" :class="$style.headerButton" class="_button" @click="emit('close')"><i class="ti ti-x"></i></button>
 			<span :class="$style.title">
 				<slot name="header"></slot>
 			</span>
-			<button v-if="!withOkButton" :class="$style.headerButton" class="_button" data-cy-modal-window-close @click="$emit('close')"><i class="ti ti-x"></i></button>
-			<button v-if="withOkButton" :class="$style.headerButton" class="_button" :disabled="okButtonDisabled" @click="$emit('ok')"><i class="ti ti-check"></i></button>
+			<button v-if="!withOkButton && withCloseButton" :class="$style.headerButton" class="_button" data-cy-modal-window-close @click="emit('close')"><i class="ti ti-x"></i></button>
+			<button v-if="withOkButton" :class="$style.headerButton" class="_button" :disabled="okButtonDisabled" @click="emit('ok')"><i class="ti ti-check"></i></button>
 		</div>
 		<div :class="$style.body">
 			<slot :width="bodyWidth" :height="bodyHeight"></slot>
@@ -26,12 +26,14 @@ import { onMounted, onUnmounted, shallowRef, ref } from 'vue';
 import MkModal from './MkModal.vue';
 
 const props = withDefaults(defineProps<{
-	withOkButton: boolean;
-	okButtonDisabled: boolean;
-	width: number;
-	height: number;
+	withOkButton?: boolean;
+	withCloseButton?: boolean;
+	okButtonDisabled?: boolean;
+	width?: number;
+	height?: number;
 }>(), {
 	withOkButton: false,
+	withCloseButton: true,
 	okButtonDisabled: false,
 	width: 400,
 	height: 500,
@@ -42,6 +44,7 @@ const emit = defineEmits<{
 	(event: 'close'): void;
 	(event: 'closed'): void;
 	(event: 'ok'): void;
+	(event: 'esc'): void;
 }>();
 
 const modal = shallowRef<InstanceType<typeof MkModal>>();
@@ -50,21 +53,13 @@ const headerEl = shallowRef<HTMLElement>();
 const bodyWidth = ref(0);
 const bodyHeight = ref(0);
 
-const close = () => {
+function close() {
 	modal.value?.close();
-};
+}
 
-const onBgClick = () => {
+function onBgClick() {
 	emit('click');
-};
-
-const onKeydown = (evt) => {
-	if (evt.which === 27) { // Esc
-		evt.preventDefault();
-		evt.stopPropagation();
-		close();
-	}
-};
+}
 
 const ro = new ResizeObserver((entries, observer) => {
 	if (rootEl.value == null || headerEl.value == null) return;
@@ -95,39 +90,39 @@ defineExpose({
 	display: flex;
 	flex-direction: column;
 	contain: content;
-	border-radius: var(--radius);
+	border-radius: var(--MI-radius);
 
 	--root-margin: 24px;
 
+	--MI_THEME-headerHeight: 46px;
+	--MI_THEME-headerHeightNarrow: 42px;
+
 	@media (max-width: 500px) {
 		--root-margin: 16px;
 	}
-
-	--headerHeight: 46px;
-	--headerHeightNarrow: 42px;
 }
 
 .header {
 	display: flex;
 	flex-shrink: 0;
-	background: var(--windowHeader);
-	-webkit-backdrop-filter: var(--blur, blur(15px));
-	backdrop-filter: var(--blur, blur(15px));
+	background: var(--MI_THEME-windowHeader);
+	-webkit-backdrop-filter: var(--MI-blur, blur(15px));
+	backdrop-filter: var(--MI-blur, blur(15px));
 }
 
 .headerButton {
-	height: var(--headerHeight);
-	width: var(--headerHeight);
+	height: var(--MI_THEME-headerHeight);
+	width: var(--MI_THEME-headerHeight);
 
 	@media (max-width: 500px) {
-		height: var(--headerHeightNarrow);
-		width: var(--headerHeightNarrow);
+		height: var(--MI_THEME-headerHeightNarrow);
+		width: var(--MI_THEME-headerHeightNarrow);
 	}
 }
 
 .title {
 	flex: 1;
-	line-height: var(--headerHeight);
+	line-height: var(--MI_THEME-headerHeight);
 	padding-left: 32px;
 	font-weight: bold;
 	white-space: nowrap;
@@ -136,7 +131,7 @@ defineExpose({
 	pointer-events: none;
 
 	@media (max-width: 500px) {
-		line-height: var(--headerHeightNarrow);
+		line-height: var(--MI_THEME-headerHeightNarrow);
 		padding-left: 16px;
 	}
 }
@@ -148,7 +143,7 @@ defineExpose({
 .body {
 	flex: 1;
 	overflow: auto;
-	background: var(--panel);
+	background: var(--MI_THEME-panel);
 	container-type: size;
 }
 </style>
diff --git a/packages/frontend/src/components/MkNote.vue b/packages/frontend/src/components/MkNote.vue
index 22b1691a86..cf0d0787b1 100644
--- a/packages/frontend/src/components/MkNote.vue
+++ b/packages/frontend/src/components/MkNote.vue
@@ -9,8 +9,8 @@ SPDX-License-Identifier: AGPL-3.0-only
 	v-show="!isDeleted"
 	ref="rootEl"
 	v-hotkey="keymap"
-	:class="[$style.root, { [$style.showActionsOnlyHover]: defaultStore.state.showNoteActionsOnlyHover }]"
-	:tabindex="!isDeleted ? '-1' : undefined"
+	:class="[$style.root, { [$style.showActionsOnlyHover]: defaultStore.state.showNoteActionsOnlyHover, [$style.skipRender]: defaultStore.state.skipNoteRender }]"
+	:tabindex="isDeleted ? '-1' : '0'"
 >
 	<MkNoteSub v-if="appearNote.reply && !renoteCollapsed" :note="appearNote.reply" :class="$style.replyTo"/>
 	<div v-if="pinned" :class="$style.tip"><i class="ti ti-pin"></i> {{ i18n.ts.pinnedNote }}</div>
@@ -28,7 +28,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 			</template>
 		</I18n>
 		<div :class="$style.renoteInfo">
-			<button ref="renoteTime" :class="$style.renoteTime" class="_button" @click="showRenoteMenu()">
+			<button ref="renoteTime" :class="$style.renoteTime" class="_button" @mousedown.prevent="showRenoteMenu()">
 				<i class="ti ti-dots" :class="$style.renoteMenu"></i>
 				<MkTime :time="note.createdAt"/>
 			</button>
@@ -53,7 +53,14 @@ SPDX-License-Identifier: AGPL-3.0-only
 			<MkInstanceTicker v-if="showTicker" :instance="appearNote.user.instance"/>
 			<div style="container-type: inline-size;">
 				<p v-if="appearNote.cw != null" :class="$style.cw">
-					<Mfm v-if="appearNote.cw != ''" style="margin-right: 8px;" :text="appearNote.cw" :author="appearNote.user" :nyaize="'respect'"/>
+					<Mfm
+						v-if="appearNote.cw != ''"
+						:text="appearNote.cw"
+						:author="appearNote.user"
+						:nyaize="'respect'"
+						:enableEmojiMenu="true"
+						:enableEmojiMenuReaction="true"
+					/>
 					<MkCwButton v-model="showContent" :text="appearNote.text" :renote="appearNote.renote" :files="appearNote.files" :poll="appearNote.poll" style="margin: 4px 0;"/>
 				</p>
 				<div v-show="appearNote.cw == null || showContent" :class="[{ [$style.contentCollapsed]: collapsed }]">
@@ -79,7 +86,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 						</div>
 					</div>
 					<div v-if="appearNote.files && appearNote.files.length > 0">
-						<MkMediaList :mediaList="appearNote.files"/>
+						<MkMediaList ref="galleryEl" :mediaList="appearNote.files"/>
 					</div>
 					<MkPoll v-if="appearNote.poll" :noteId="appearNote.id" :poll="appearNote.poll" :class="$style.poll"/>
 					<div v-if="isEnabledUrlPreview">
@@ -110,7 +117,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 					ref="renoteButton"
 					:class="$style.footerButton"
 					class="_button"
-					@mousedown="renote()"
+					@mousedown.prevent="renote()"
 				>
 					<i class="ti ti-repeat"></i>
 					<p v-if="appearNote.renoteCount > 0" :class="$style.footerButtonCount">{{ number(appearNote.renoteCount) }}</p>
@@ -119,16 +126,16 @@ SPDX-License-Identifier: AGPL-3.0-only
 					<i class="ti ti-ban"></i>
 				</button>
 				<button ref="reactButton" :class="$style.footerButton" class="_button" @click="toggleReact()">
-					<i v-if="appearNote.reactionAcceptance === 'likeOnly' && appearNote.myReaction != null" class="ti ti-heart-filled" style="color: var(--eventReactionHeart);"></i>
-					<i v-else-if="appearNote.myReaction != null" class="ti ti-minus" style="color: var(--accent);"></i>
+					<i v-if="appearNote.reactionAcceptance === 'likeOnly' && appearNote.myReaction != null" class="ti ti-heart-filled" style="color: var(--MI_THEME-love);"></i>
+					<i v-else-if="appearNote.myReaction != null" class="ti ti-minus" style="color: var(--MI_THEME-accent);"></i>
 					<i v-else-if="appearNote.reactionAcceptance === 'likeOnly'" class="ti ti-heart"></i>
 					<i v-else class="ti ti-plus"></i>
 					<p v-if="(appearNote.reactionAcceptance === 'likeOnly' || defaultStore.state.showReactionsCount) && appearNote.reactionCount > 0" :class="$style.footerButtonCount">{{ number(appearNote.reactionCount) }}</p>
 				</button>
-				<button v-if="defaultStore.state.showClipButtonInNoteFooter" ref="clipButton" :class="$style.footerButton" class="_button" @mousedown="clip()">
+				<button v-if="defaultStore.state.showClipButtonInNoteFooter" ref="clipButton" :class="$style.footerButton" class="_button" @mousedown.prevent="clip()">
 					<i class="ti ti-paperclip"></i>
 				</button>
-				<button ref="menuButton" :class="$style.footerButton" class="_button" @mousedown="showMenu()">
+				<button ref="menuButton" :class="$style.footerButton" class="_button" @mousedown.prevent="showMenu()">
 					<i class="ti ti-dots"></i>
 				</button>
 			</footer>
@@ -163,6 +170,10 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { computed, inject, onMounted, ref, shallowRef, Ref, watch, provide } from 'vue';
 import * as mfm from 'mfm-js';
 import * as Misskey from 'misskey-js';
+import { isLink } from '@@/js/is-link.js';
+import { shouldCollapsed } from '@@/js/collapsed.js';
+import { host } from '@@/js/config.js';
+import type { MenuItem } from '@/types/menu.js';
 import MkNoteSub from '@/components/MkNoteSub.vue';
 import MkNoteHeader from '@/components/MkNoteHeader.vue';
 import MkNoteSimple from '@/components/MkNoteSimple.vue';
@@ -174,8 +185,7 @@ import MkPoll from '@/components/MkPoll.vue';
 import MkUsersTooltip from '@/components/MkUsersTooltip.vue';
 import MkUrlPreview from '@/components/MkUrlPreview.vue';
 import MkInstanceTicker from '@/components/MkInstanceTicker.vue';
-import { pleaseLogin } from '@/scripts/please-login.js';
-import { focusPrev, focusNext } from '@/scripts/focus.js';
+import { pleaseLogin, type OpenOnRemoteOptions } from '@/scripts/please-login.js';
 import { checkWordMute } from '@/scripts/check-word-mute.js';
 import { userPage } from '@/filters/user.js';
 import number from '@/filters/number.js';
@@ -193,11 +203,12 @@ import { deepClone } from '@/scripts/clone.js';
 import { useTooltip } from '@/scripts/use-tooltip.js';
 import { claimAchievement } from '@/scripts/achievements.js';
 import { getNoteSummary } from '@/scripts/get-note-summary.js';
-import { MenuItem } from '@/types/menu.js';
 import MkRippleEffect from '@/components/MkRippleEffect.vue';
 import { showMovedDialog } from '@/scripts/show-moved-dialog.js';
-import { shouldCollapsed } from '@/scripts/collapsed.js';
 import { isEnabledUrlPreview } from '@/instance.js';
+import { type Keymap } from '@/scripts/hotkey.js';
+import { focusPrev, focusNext } from '@/scripts/focus.js';
+import { getAppearNote } from '@/scripts/get-appear-note.js';
 
 const props = withDefaults(defineProps<{
 	note: Misskey.entities.Note;
@@ -216,6 +227,7 @@ const emit = defineEmits<{
 }>();
 
 const inTimeline = inject<boolean>('inTimeline', false);
+const tl_withSensitive = inject<Ref<boolean>>('tl_withSensitive', ref(true));
 const inChannel = inject('inChannel', null);
 const currentClip = inject<Ref<Misskey.entities.Clip> | null>('currentClip', null);
 
@@ -240,14 +252,7 @@ if (noteViewInterruptors.length > 0) {
 	});
 }
 
-const isRenote = (
-	note.value.renote != null &&
-	note.value.reply == null &&
-	note.value.text == null &&
-	note.value.cw == null &&
-	note.value.fileIds && note.value.fileIds.length === 0 &&
-	note.value.poll == null
-);
+const isRenote = Misskey.note.isPureRenote(note.value);
 
 const rootEl = shallowRef<HTMLElement>();
 const menuButton = shallowRef<HTMLElement>();
@@ -255,7 +260,8 @@ const renoteButton = shallowRef<HTMLElement>();
 const renoteTime = shallowRef<HTMLElement>();
 const reactButton = shallowRef<HTMLElement>();
 const clipButton = shallowRef<HTMLElement>();
-const appearNote = computed(() => isRenote ? note.value.renote as Misskey.entities.Note : note.value);
+const appearNote = computed(() => getAppearNote(note.value));
+const galleryEl = shallowRef<InstanceType<typeof MkMediaList>>();
 const isMyRenote = $i && ($i.id === note.value.userId);
 const showContent = ref(false);
 const parsed = computed(() => appearNote.value.text ? mfm.parse(appearNote.value.text) : null);
@@ -276,33 +282,79 @@ const renoteCollapsed = ref(
 	),
 );
 
+const pleaseLoginContext = computed<OpenOnRemoteOptions>(() => ({
+	type: 'lookup',
+	url: `https://${host}/notes/${appearNote.value.id}`,
+}));
+
 /* Overload FunctionにLintが対応していないのでコメントアウト
 function checkMute(noteToCheck: Misskey.entities.Note, mutedWords: Array<string | string[]> | undefined | null, checkOnly: true): boolean;
 function checkMute(noteToCheck: Misskey.entities.Note, mutedWords: Array<string | string[]> | undefined | null, checkOnly: false): boolean | 'sensitiveMute';
 */
 function checkMute(noteToCheck: Misskey.entities.Note, mutedWords: Array<string | string[]> | undefined | null, checkOnly = false): boolean | 'sensitiveMute' {
-	if (mutedWords == null) return false;
-
-	if (checkWordMute(noteToCheck, $i, mutedWords)) return true;
-	if (noteToCheck.reply && checkWordMute(noteToCheck.reply, $i, mutedWords)) return true;
-	if (noteToCheck.renote && checkWordMute(noteToCheck.renote, $i, mutedWords)) return true;
+	if (mutedWords != null) {
+		if (checkWordMute(noteToCheck, $i, mutedWords)) return true;
+		if (noteToCheck.reply && checkWordMute(noteToCheck.reply, $i, mutedWords)) return true;
+		if (noteToCheck.renote && checkWordMute(noteToCheck.renote, $i, mutedWords)) return true;
+	}
 
 	if (checkOnly) return false;
 
-	if (inTimeline && !defaultStore.state.tl.filter.withSensitive && noteToCheck.files?.some((v) => v.isSensitive)) return 'sensitiveMute';
+	if (inTimeline && tl_withSensitive.value === false && noteToCheck.files?.some((v) => v.isSensitive)) {
+		return 'sensitiveMute';
+	}
+
 	return false;
 }
 
 const keymap = {
-	'r': () => reply(true),
-	'e|a|plus': () => react(true),
-	'q': () => renote(true),
-	'up|k|shift+tab': focusBefore,
-	'down|j|tab': focusAfter,
-	'esc': blur,
-	'm|o': () => showMenu(true),
-	's': () => showContent.value !== showContent.value,
-};
+	'r': () => {
+		if (renoteCollapsed.value) return;
+		reply();
+	},
+	'e|a|plus': () => {
+		if (renoteCollapsed.value) return;
+		react();
+	},
+	'q': () => {
+		if (renoteCollapsed.value) return;
+		renote();
+	},
+	'm': () => {
+		if (renoteCollapsed.value) return;
+		showMenu();
+	},
+	'c': () => {
+		if (renoteCollapsed.value) return;
+		if (!defaultStore.state.showClipButtonInNoteFooter) return;
+		clip();
+	},
+	'o': () => {
+		if (renoteCollapsed.value) return;
+		galleryEl.value?.openGallery();
+	},
+	'v|enter': () => {
+		if (renoteCollapsed.value) {
+			renoteCollapsed.value = false;
+		} else if (appearNote.value.cw != null) {
+			showContent.value = !showContent.value;
+		} else if (isLong) {
+			collapsed.value = !collapsed.value;
+		}
+	},
+	'esc': {
+		allowRepeat: true,
+		callback: () => blur(),
+	},
+	'up|k|shift+tab': {
+		allowRepeat: true,
+		callback: () => focusBefore(),
+	},
+	'down|j|tab': {
+		allowRepeat: true,
+		callback: () => focusAfter(),
+	},
+} as const satisfies Keymap;
 
 provide('react', (reaction: string) => {
 	misskeyApi('notes/reactions/create', {
@@ -335,12 +387,14 @@ if (!props.mock) {
 
 		if (users.length < 1) return;
 
-		os.popup(MkUsersTooltip, {
+		const { dispose } = os.popup(MkUsersTooltip, {
 			showing,
 			users,
 			count: appearNote.value.renoteCount,
 			targetElement: renoteButton.value,
-		}, {}, 'closed');
+		}, {
+			closed: () => dispose(),
+		});
 	});
 
 	if (appearNote.value.reactionAcceptance === 'likeOnly') {
@@ -355,19 +409,21 @@ if (!props.mock) {
 
 			if (users.length < 1) return;
 
-			os.popup(MkReactionsViewerDetails, {
+			const { dispose } = os.popup(MkReactionsViewerDetails, {
 				showing,
 				reaction: '❤️',
 				users,
 				count: appearNote.value.reactionCount,
 				targetElement: reactButton.value!,
-			}, {}, 'closed');
+			}, {
+				closed: () => dispose(),
+			});
 		});
 	}
 }
 
 function renote(viaKeyboard = false) {
-	pleaseLogin();
+	pleaseLogin({ openOnRemote: pleaseLoginContext.value });
 	showMovedDialog();
 
 	const { menu } = getRenoteMenu({ note: note.value, renoteButton, mock: props.mock });
@@ -376,22 +432,21 @@ function renote(viaKeyboard = false) {
 	});
 }
 
-function reply(viaKeyboard = false): void {
-	pleaseLogin();
+function reply(): void {
+	pleaseLogin({ openOnRemote: pleaseLoginContext.value });
 	if (props.mock) {
 		return;
 	}
 	os.post({
 		reply: appearNote.value,
 		channel: appearNote.value.channel,
-		animation: !viaKeyboard,
 	}).then(() => {
 		focus();
 	});
 }
 
-function react(viaKeyboard = false): void {
-	pleaseLogin();
+function react(): void {
+	pleaseLogin({ openOnRemote: pleaseLoginContext.value });
 	showMovedDialog();
 	if (appearNote.value.reactionAcceptance === 'likeOnly') {
 		sound.playMisskeySfx('reaction');
@@ -409,7 +464,9 @@ function react(viaKeyboard = false): void {
 			const rect = el.getBoundingClientRect();
 			const x = rect.left + (el.offsetWidth / 2);
 			const y = rect.top + (el.offsetHeight / 2);
-			os.popup(MkRippleEffect, { x, y }, {}, 'end');
+			const { dispose } = os.popup(MkRippleEffect, { x, y }, {
+				end: () => dispose(),
+			});
 		}
 	} else {
 		blur();
@@ -461,16 +518,6 @@ function onContextmenu(ev: MouseEvent): void {
 		return;
 	}
 
-	const isLink = (el: HTMLElement): boolean => {
-		if (el.tagName === 'A') return true;
-		// 再生速度の選択などのために、Audio要素のコンテキストメニューはブラウザデフォルトとする。
-		if (el.tagName === 'AUDIO') return true;
-		if (el.parentElement) {
-			return isLink(el.parentElement);
-		}
-		return false;
-	};
-
 	if (ev.target && isLink(ev.target as HTMLElement)) return;
 	if (window.getSelection()?.toString() !== '') return;
 
@@ -483,18 +530,16 @@ function onContextmenu(ev: MouseEvent): void {
 	}
 }
 
-function showMenu(viaKeyboard = false): void {
+function showMenu(): void {
 	if (props.mock) {
 		return;
 	}
 
 	const { menu, cleanup } = getNoteMenu({ note: note.value, translating, translation, isDeleted, currentClip: currentClip?.value });
-	os.popupMenu(menu, menuButton.value, {
-		viaKeyboard,
-	}).then(focus).finally(cleanup);
+	os.popupMenu(menu, menuButton.value).then(focus).finally(cleanup);
 }
 
-async function clip() {
+async function clip(): Promise<void> {
 	if (props.mock) {
 		return;
 	}
@@ -502,7 +547,7 @@ async function clip() {
 	os.popupMenu(await getNoteClipMenu({ note: note.value, isDeleted, currentClip: currentClip?.value }), clipButton.value).then(focus);
 }
 
-function showRenoteMenu(viaKeyboard = false): void {
+function showRenoteMenu(): void {
 	if (props.mock) {
 		return;
 	}
@@ -522,23 +567,19 @@ function showRenoteMenu(viaKeyboard = false): void {
 	}
 
 	if (isMyRenote) {
-		pleaseLogin();
+		pleaseLogin({ openOnRemote: pleaseLoginContext.value });
 		os.popupMenu([
 			getCopyNoteLinkMenu(note.value, i18n.ts.copyLinkRenote),
 			{ type: 'divider' },
 			getUnrenote(),
-		], renoteTime.value, {
-			viaKeyboard: viaKeyboard,
-		});
+		], renoteTime.value);
 	} else {
 		os.popupMenu([
 			getCopyNoteLinkMenu(note.value, i18n.ts.copyLinkRenote),
 			{ type: 'divider' },
 			getAbuseNoteMenu(note.value, i18n.ts.reportAbuseRenote),
 			($i?.isModerator || $i?.isAdmin) ? getUnrenote() : undefined,
-		], renoteTime.value, {
-			viaKeyboard: viaKeyboard,
-		});
+		], renoteTime.value);
 	}
 }
 
@@ -551,11 +592,11 @@ function blur() {
 }
 
 function focusBefore() {
-	focusPrev(rootEl.value ?? null);
+	focusPrev(rootEl.value);
 }
 
 function focusAfter() {
-	focusNext(rootEl.value ?? null);
+	focusNext(rootEl.value);
 }
 
 function readPromo() {
@@ -582,18 +623,10 @@ function emitUpdReaction(emoji: string, delta: number) {
 	overflow: clip;
 	contain: content;
 
-	// これらの指定はパフォーマンス向上には有効だが、ノートの高さは一定でないため、
-	// 下の方までスクロールすると上のノートの高さがここで決め打ちされたものに変化し、表示しているノートの位置が変わってしまう
-	// ノートがマウントされたときに自身の高さを取得し contain-intrinsic-size を設定しなおせばほぼ解決できそうだが、
-	// 今度はその処理自体がパフォーマンス低下の原因にならないか懸念される。また、被リアクションでも高さは変化するため、やはり多少のズレは生じる
-	// 一度レンダリングされた要素はブラウザがよしなにサイズを覚えておいてくれるような実装になるまで待った方が良さそう(なるのか?)
-	//content-visibility: auto;
-  //contain-intrinsic-size: 0 128px;
-
 	&:focus-visible {
 		outline: none;
 
-		&:after {
+		&::after {
 			content: "";
 			pointer-events: none;
 			display: block;
@@ -606,8 +639,8 @@ function emitUpdReaction(emoji: string, delta: number) {
 			margin: auto;
 			width: calc(100% - 8px);
 			height: calc(100% - 8px);
-			border: dashed 1px var(--focus);
-			border-radius: var(--radius);
+			border: dashed 2px var(--MI_THEME-focus);
+			border-radius: var(--MI-radius);
 			box-sizing: border-box;
 		}
 	}
@@ -629,9 +662,9 @@ function emitUpdReaction(emoji: string, delta: number) {
 			right: 12px;
 			padding: 0 4px;
 			margin-bottom: 0 !important;
-			background: var(--popup);
+			background: var(--MI_THEME-popup);
 			border-radius: 8px;
-			box-shadow: 0px 4px 32px var(--shadow);
+			box-shadow: 0px 4px 32px var(--MI_THEME-shadow);
 		}
 
 		.footerButton {
@@ -650,6 +683,11 @@ function emitUpdReaction(emoji: string, delta: number) {
 	}
 }
 
+.skipRender {
+	content-visibility: auto;
+	contain-intrinsic-size: 0 150px;
+}
+
 .tip {
 	display: flex;
 	align-items: center;
@@ -676,7 +714,7 @@ function emitUpdReaction(emoji: string, delta: number) {
 	padding: 16px 32px 8px 32px;
 	line-height: 28px;
 	white-space: pre;
-	color: var(--renote);
+	color: var(--MI_THEME-renote);
 
 	& + .article {
 		padding-top: 8px;
@@ -773,7 +811,7 @@ function emitUpdReaction(emoji: string, delta: number) {
 	width: 58px;
 	height: 58px;
 	position: sticky !important;
-	top: calc(22px + var(--stickyTop, 0px));
+	top: calc(22px + var(--MI-stickyTop, 0px));
 	left: 0;
 }
 
@@ -794,12 +832,12 @@ function emitUpdReaction(emoji: string, delta: number) {
 	width: 100%;
 	margin-top: 14px;
 	position: sticky;
-	bottom: calc(var(--stickyBottom, 0px) + 14px);
+	bottom: calc(var(--MI-stickyBottom, 0px) + 14px);
 }
 
 .showLessLabel {
 	display: inline-block;
-	background: var(--popup);
+	background: var(--MI_THEME-popup);
 	padding: 6px 10px;
 	font-size: 0.8em;
 	border-radius: 999px;
@@ -820,16 +858,16 @@ function emitUpdReaction(emoji: string, delta: number) {
 	z-index: 2;
 	width: 100%;
 	height: 64px;
-	background: linear-gradient(0deg, var(--panel), var(--X15));
+	background: linear-gradient(0deg, var(--MI_THEME-panel), color(from var(--MI_THEME-panel) srgb r g b / 0));
 
 	&:hover > .collapsedLabel {
-		background: var(--panelHighlight);
+		background: var(--MI_THEME-panelHighlight);
 	}
 }
 
 .collapsedLabel {
 	display: inline-block;
-	background: var(--panel);
+	background: var(--MI_THEME-panel);
 	padding: 6px 10px;
 	font-size: 0.8em;
 	border-radius: 999px;
@@ -841,13 +879,13 @@ function emitUpdReaction(emoji: string, delta: number) {
 }
 
 .replyIcon {
-	color: var(--accent);
+	color: var(--MI_THEME-accent);
 	margin-right: 0.5em;
 }
 
 .translation {
-	border: solid 0.5px var(--divider);
-	border-radius: var(--radius);
+	border: solid 0.5px var(--MI_THEME-divider);
+	border-radius: var(--MI-radius);
 	padding: 12px;
 	margin-top: 8px;
 }
@@ -866,7 +904,7 @@ function emitUpdReaction(emoji: string, delta: number) {
 
 .quoteNote {
 	padding: 16px;
-	border: dashed 1px var(--renote);
+	border: dashed 1px var(--MI_THEME-renote);
 	border-radius: 8px;
 	overflow: clip;
 }
@@ -890,7 +928,7 @@ function emitUpdReaction(emoji: string, delta: number) {
 	}
 
 	&:hover {
-		color: var(--fgHighlighted);
+		color: var(--MI_THEME-fgHighlighted);
 	}
 }
 
@@ -961,7 +999,7 @@ function emitUpdReaction(emoji: string, delta: number) {
 		margin: 0 10px 0 0;
 		width: 46px;
 		height: 46px;
-		top: calc(14px + var(--stickyTop, 0px));
+		top: calc(14px + var(--MI-stickyTop, 0px));
 	}
 }
 
diff --git a/packages/frontend/src/components/MkNoteDetailed.vue b/packages/frontend/src/components/MkNoteDetailed.vue
index ed1c0a9e96..e0473dce5e 100644
--- a/packages/frontend/src/components/MkNoteDetailed.vue
+++ b/packages/frontend/src/components/MkNoteDetailed.vue
@@ -10,6 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 	ref="rootEl"
 	v-hotkey="keymap"
 	:class="$style.root"
+	:tabindex="isDeleted ? '-1' : '0'"
 >
 	<div v-if="appearNote.reply && appearNote.reply.replyId">
 		<div v-if="!conversationLoaded" style="padding: 16px">
@@ -31,7 +32,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 			</I18n>
 		</span>
 		<div :class="$style.renoteInfo">
-			<button ref="renoteTime" class="_button" :class="$style.renoteTime" @click="showRenoteMenu()">
+			<button ref="renoteTime" class="_button" :class="$style.renoteTime" @mousedown.prevent="showRenoteMenu()">
 				<i v-if="isMyRenote" class="ti ti-dots" style="margin-right: 4px;"></i>
 				<MkTime :time="note.createdAt"/>
 			</button>
@@ -67,7 +68,14 @@ SPDX-License-Identifier: AGPL-3.0-only
 		</header>
 		<div :class="$style.noteContent">
 			<p v-if="appearNote.cw != null" :class="$style.cw">
-				<Mfm v-if="appearNote.cw != ''" style="margin-right: 8px;" :text="appearNote.cw" :author="appearNote.user" :nyaize="'respect'"/>
+				<Mfm
+					v-if="appearNote.cw != ''"
+					:text="appearNote.cw"
+					:author="appearNote.user"
+					:nyaize="'respect'"
+					:enableEmojiMenu="true"
+					:enableEmojiMenuReaction="true"
+				/>
 				<MkCwButton v-model="showContent" :text="appearNote.text" :renote="appearNote.renote" :files="appearNote.files" :poll="appearNote.poll"/>
 			</p>
 			<div v-show="appearNote.cw == null || showContent">
@@ -92,7 +100,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 					</div>
 				</div>
 				<div v-if="appearNote.files && appearNote.files.length > 0">
-					<MkMediaList :mediaList="appearNote.files"/>
+					<MkMediaList ref="galleryEl" :mediaList="appearNote.files"/>
 				</div>
 				<MkPoll v-if="appearNote.poll" ref="pollViewer" :noteId="appearNote.id" :poll="appearNote.poll" :class="$style.poll"/>
 				<div v-if="isEnabledUrlPreview">
@@ -118,7 +126,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 				ref="renoteButton"
 				class="_button"
 				:class="$style.noteFooterButton"
-				@mousedown="renote()"
+				@mousedown.prevent="renote()"
 			>
 				<i class="ti ti-repeat"></i>
 				<p v-if="appearNote.renoteCount > 0" :class="$style.noteFooterButtonCount">{{ number(appearNote.renoteCount) }}</p>
@@ -127,16 +135,16 @@ SPDX-License-Identifier: AGPL-3.0-only
 				<i class="ti ti-ban"></i>
 			</button>
 			<button ref="reactButton" :class="$style.noteFooterButton" class="_button" @click="toggleReact()">
-				<i v-if="appearNote.reactionAcceptance === 'likeOnly' && appearNote.myReaction != null" class="ti ti-heart-filled" style="color: var(--eventReactionHeart);"></i>
-				<i v-else-if="appearNote.myReaction != null" class="ti ti-minus" style="color: var(--accent);"></i>
+				<i v-if="appearNote.reactionAcceptance === 'likeOnly' && appearNote.myReaction != null" class="ti ti-heart-filled" style="color: var(--MI_THEME-love);"></i>
+				<i v-else-if="appearNote.myReaction != null" class="ti ti-minus" style="color: var(--MI_THEME-accent);"></i>
 				<i v-else-if="appearNote.reactionAcceptance === 'likeOnly'" class="ti ti-heart"></i>
 				<i v-else class="ti ti-plus"></i>
 				<p v-if="(appearNote.reactionAcceptance === 'likeOnly' || defaultStore.state.showReactionsCount) && appearNote.reactionCount > 0" :class="$style.noteFooterButtonCount">{{ number(appearNote.reactionCount) }}</p>
 			</button>
-			<button v-if="defaultStore.state.showClipButtonInNoteFooter" ref="clipButton" class="_button" :class="$style.noteFooterButton" @mousedown="clip()">
+			<button v-if="defaultStore.state.showClipButtonInNoteFooter" ref="clipButton" class="_button" :class="$style.noteFooterButton" @mousedown.prevent="clip()">
 				<i class="ti ti-paperclip"></i>
 			</button>
-			<button ref="menuButton" class="_button" :class="$style.noteFooterButton" @mousedown="showMenu()">
+			<button ref="menuButton" class="_button" :class="$style.noteFooterButton" @mousedown.prevent="showMenu()">
 				<i class="ti ti-dots"></i>
 			</button>
 		</footer>
@@ -198,6 +206,8 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { computed, inject, onMounted, provide, ref, shallowRef } from 'vue';
 import * as mfm from 'mfm-js';
 import * as Misskey from 'misskey-js';
+import { isLink } from '@@/js/is-link.js';
+import { host } from '@@/js/config.js';
 import MkNoteSub from '@/components/MkNoteSub.vue';
 import MkNoteSimple from '@/components/MkNoteSimple.vue';
 import MkReactionsViewer from '@/components/MkReactionsViewer.vue';
@@ -208,7 +218,7 @@ import MkPoll from '@/components/MkPoll.vue';
 import MkUsersTooltip from '@/components/MkUsersTooltip.vue';
 import MkUrlPreview from '@/components/MkUrlPreview.vue';
 import MkInstanceTicker from '@/components/MkInstanceTicker.vue';
-import { pleaseLogin } from '@/scripts/please-login.js';
+import { pleaseLogin, type OpenOnRemoteOptions } from '@/scripts/please-login.js';
 import { checkWordMute } from '@/scripts/check-word-mute.js';
 import { userPage } from '@/filters/user.js';
 import { notePage } from '@/filters/note.js';
@@ -233,6 +243,8 @@ import MkPagination, { type Paging } from '@/components/MkPagination.vue';
 import MkReactionIcon from '@/components/MkReactionIcon.vue';
 import MkButton from '@/components/MkButton.vue';
 import { isEnabledUrlPreview } from '@/instance.js';
+import { getAppearNote } from '@/scripts/get-appear-note.js';
+import { type Keymap } from '@/scripts/hotkey.js';
 
 const props = withDefaults(defineProps<{
 	note: Misskey.entities.Note;
@@ -264,14 +276,7 @@ if (noteViewInterruptors.length > 0) {
 	});
 }
 
-const isRenote = (
-	note.value.renote != null &&
-	note.value.reply == null &&
-	note.value.text == null &&
-	note.value.cw == null &&
-	note.value.fileIds && note.value.fileIds.length === 0 &&
-	note.value.poll == null
-);
+const isRenote = Misskey.note.isPureRenote(note.value);
 
 const rootEl = shallowRef<HTMLElement>();
 const menuButton = shallowRef<HTMLElement>();
@@ -279,7 +284,8 @@ const renoteButton = shallowRef<HTMLElement>();
 const renoteTime = shallowRef<HTMLElement>();
 const reactButton = shallowRef<HTMLElement>();
 const clipButton = shallowRef<HTMLElement>();
-const appearNote = computed(() => isRenote ? note.value.renote as Misskey.entities.Note : note.value);
+const appearNote = computed(() => getAppearNote(note.value));
+const galleryEl = shallowRef<InstanceType<typeof MkMediaList>>();
 const isMyRenote = $i && ($i.id === note.value.userId);
 const showContent = ref(false);
 const isDeleted = ref(false);
@@ -293,14 +299,31 @@ const conversation = ref<Misskey.entities.Note[]>([]);
 const replies = ref<Misskey.entities.Note[]>([]);
 const canRenote = computed(() => ['public', 'home'].includes(appearNote.value.visibility) || appearNote.value.userId === $i?.id);
 
+const pleaseLoginContext = computed<OpenOnRemoteOptions>(() => ({
+	type: 'lookup',
+	url: `https://${host}/notes/${appearNote.value.id}`,
+}));
+
 const keymap = {
-	'r': () => reply(true),
-	'e|a|plus': () => react(true),
-	'q': () => renote(true),
-	'esc': blur,
-	'm|o': () => showMenu(true),
-	's': () => showContent.value !== showContent.value,
-};
+	'r': () => reply(),
+	'e|a|plus': () => react(),
+	'q': () => renote(),
+	'm': () => showMenu(),
+	'c': () => {
+		if (!defaultStore.state.showClipButtonInNoteFooter) return;
+		clip();
+	},
+	'o': () => galleryEl.value?.openGallery(),
+	'v|enter': () => {
+		if (appearNote.value.cw != null) {
+			showContent.value = !showContent.value;
+		}
+	},
+	'esc': {
+		allowRepeat: true,
+		callback: () => blur(),
+	},
+} as const satisfies Keymap;
 
 provide('react', (reaction: string) => {
 	misskeyApi('notes/reactions/create', {
@@ -346,12 +369,14 @@ useTooltip(renoteButton, async (showing) => {
 
 	if (users.length < 1) return;
 
-	os.popup(MkUsersTooltip, {
+	const { dispose } = os.popup(MkUsersTooltip, {
 		showing,
 		users,
 		count: appearNote.value.renoteCount,
 		targetElement: renoteButton.value,
-	}, {}, 'closed');
+	}, {
+		closed: () => dispose(),
+	});
 });
 
 if (appearNote.value.reactionAcceptance === 'likeOnly') {
@@ -366,40 +391,39 @@ if (appearNote.value.reactionAcceptance === 'likeOnly') {
 
 		if (users.length < 1) return;
 
-		os.popup(MkReactionsViewerDetails, {
+		const { dispose } = os.popup(MkReactionsViewerDetails, {
 			showing,
 			reaction: '❤️',
 			users,
 			count: appearNote.value.reactionCount,
 			targetElement: reactButton.value!,
-		}, {}, 'closed');
+		}, {
+			closed: () => dispose(),
+		});
 	});
 }
 
-function renote(viaKeyboard = false) {
-	pleaseLogin();
+function renote() {
+	pleaseLogin({ openOnRemote: pleaseLoginContext.value });
 	showMovedDialog();
 
 	const { menu } = getRenoteMenu({ note: note.value, renoteButton });
-	os.popupMenu(menu, renoteButton.value, {
-		viaKeyboard,
-	});
+	os.popupMenu(menu, renoteButton.value);
 }
 
-function reply(viaKeyboard = false): void {
-	pleaseLogin();
+function reply(): void {
+	pleaseLogin({ openOnRemote: pleaseLoginContext.value });
 	showMovedDialog();
 	os.post({
 		reply: appearNote.value,
 		channel: appearNote.value.channel,
-		animation: !viaKeyboard,
 	}).then(() => {
 		focus();
 	});
 }
 
-function react(viaKeyboard = false): void {
-	pleaseLogin();
+function react(): void {
+	pleaseLogin({ openOnRemote: pleaseLoginContext.value });
 	showMovedDialog();
 	if (appearNote.value.reactionAcceptance === 'likeOnly') {
 		sound.playMisskeySfx('reaction');
@@ -408,12 +432,14 @@ function react(viaKeyboard = false): void {
 			noteId: appearNote.value.id,
 			reaction: '❤️',
 		});
-		const el = reactButton.value as HTMLElement | null | undefined;
+		const el = reactButton.value;
 		if (el) {
 			const rect = el.getBoundingClientRect();
 			const x = rect.left + (el.offsetWidth / 2);
 			const y = rect.top + (el.offsetHeight / 2);
-			os.popup(MkRippleEffect, { x, y }, {}, 'end');
+			const { dispose } = os.popup(MkRippleEffect, { x, y }, {
+				end: () => dispose(),
+			});
 		}
 	} else {
 		blur();
@@ -450,14 +476,6 @@ function toggleReact() {
 }
 
 function onContextmenu(ev: MouseEvent): void {
-	const isLink = (el: HTMLElement): boolean => {
-		if (el.tagName === 'A') return true;
-		if (el.parentElement) {
-			return isLink(el.parentElement);
-		}
-		return false;
-	};
-
 	if (ev.target && isLink(ev.target as HTMLElement)) return;
 	if (window.getSelection()?.toString() !== '') return;
 
@@ -470,20 +488,18 @@ function onContextmenu(ev: MouseEvent): void {
 	}
 }
 
-function showMenu(viaKeyboard = false): void {
+function showMenu(): void {
 	const { menu, cleanup } = getNoteMenu({ note: note.value, translating, translation, isDeleted });
-	os.popupMenu(menu, menuButton.value, {
-		viaKeyboard,
-	}).then(focus).finally(cleanup);
+	os.popupMenu(menu, menuButton.value).then(focus).finally(cleanup);
 }
 
-async function clip() {
+async function clip(): Promise<void> {
 	os.popupMenu(await getNoteClipMenu({ note: note.value, isDeleted }), clipButton.value).then(focus);
 }
 
-function showRenoteMenu(viaKeyboard = false): void {
+function showRenoteMenu(): void {
 	if (!isMyRenote) return;
-	pleaseLogin();
+	pleaseLogin({ openOnRemote: pleaseLoginContext.value });
 	os.popupMenu([{
 		text: i18n.ts.unrenote,
 		icon: 'ti ti-trash',
@@ -494,9 +510,7 @@ function showRenoteMenu(viaKeyboard = false): void {
 			});
 			isDeleted.value = true;
 		},
-	}], renoteTime.value, {
-		viaKeyboard: viaKeyboard,
-	});
+	}], renoteTime.value);
 }
 
 function focus() {
@@ -538,6 +552,28 @@ function loadConversation() {
 	transition: box-shadow 0.1s ease;
 	overflow: clip;
 	contain: content;
+
+	&:focus-visible {
+		outline: none;
+
+		&::after {
+			content: "";
+			pointer-events: none;
+			display: block;
+			position: absolute;
+			z-index: 10;
+			top: 0;
+			left: 0;
+			right: 0;
+			bottom: 0;
+			margin: auto;
+			width: calc(100% - 8px);
+			height: calc(100% - 8px);
+			border: dashed 2px var(--MI_THEME-focus);
+			border-radius: var(--MI-radius);
+			box-sizing: border-box;
+		}
+	}
 }
 
 .replyTo {
@@ -555,7 +591,7 @@ function loadConversation() {
 	padding: 16px 32px 8px 32px;
 	line-height: 28px;
 	white-space: pre;
-	color: var(--renote);
+	color: var(--MI_THEME-renote);
 }
 
 .renoteAvatar {
@@ -635,7 +671,7 @@ function loadConversation() {
 	padding: 4px 6px;
 	font-size: 80%;
 	line-height: 1;
-	border: solid 0.5px var(--divider);
+	border: solid 0.5px var(--MI_THEME-divider);
 	border-radius: 4px;
 }
 
@@ -663,19 +699,19 @@ function loadConversation() {
 }
 
 .noteReplyTarget {
-	color: var(--accent);
+	color: var(--MI_THEME-accent);
 	margin-right: 0.5em;
 }
 
 .rn {
 	margin-left: 4px;
 	font-style: oblique;
-	color: var(--renote);
+	color: var(--MI_THEME-renote);
 }
 
 .translation {
-	border: solid 0.5px var(--divider);
-	border-radius: var(--radius);
+	border: solid 0.5px var(--MI_THEME-divider);
+	border-radius: var(--MI-radius);
 	padding: 12px;
 	margin-top: 8px;
 }
@@ -690,7 +726,7 @@ function loadConversation() {
 
 .quoteNote {
 	padding: 16px;
-	border: dashed 1px var(--renote);
+	border: dashed 1px var(--MI_THEME-renote);
 	border-radius: 8px;
 	overflow: clip;
 }
@@ -716,7 +752,7 @@ function loadConversation() {
 	}
 
 	&:hover {
-		color: var(--fgHighlighted);
+		color: var(--MI_THEME-fgHighlighted);
 	}
 }
 
@@ -726,17 +762,17 @@ function loadConversation() {
 	opacity: 0.7;
 
 	&.reacted {
-		color: var(--accent);
+		color: var(--MI_THEME-accent);
 	}
 }
 
 .reply:not(:first-child) {
-	border-top: solid 0.5px var(--divider);
+	border-top: solid 0.5px var(--MI_THEME-divider);
 }
 
 .tabs {
-	border-top: solid 0.5px var(--divider);
-	border-bottom: solid 0.5px var(--divider);
+	border-top: solid 0.5px var(--MI_THEME-divider);
+	border-bottom: solid 0.5px var(--MI_THEME-divider);
 	display: flex;
 }
 
@@ -748,7 +784,7 @@ function loadConversation() {
 }
 
 .tabActive {
-	border-bottom: solid 2px var(--accent);
+	border-bottom: solid 2px var(--MI_THEME-accent);
 }
 
 .tab_renotes {
@@ -768,12 +804,12 @@ function loadConversation() {
 
 .reactionTab {
 	padding: 4px 6px;
-	border: solid 1px var(--divider);
+	border: solid 1px var(--MI_THEME-divider);
 	border-radius: 6px;
 }
 
 .reactionTabActive {
-	border-color: var(--accent);
+	border-color: var(--MI_THEME-accent);
 }
 
 @container (max-width: 500px) {
diff --git a/packages/frontend/src/components/MkNoteHeader.vue b/packages/frontend/src/components/MkNoteHeader.vue
index be5829d92f..750e32a9ff 100644
--- a/packages/frontend/src/components/MkNoteHeader.vue
+++ b/packages/frontend/src/components/MkNoteHeader.vue
@@ -40,6 +40,7 @@ import * as Misskey from 'misskey-js';
 import { i18n } from '@/i18n.js';
 import { notePage } from '@/filters/note.js';
 import { userPage } from '@/filters/user.js';
+import { defaultStore } from '@/store.js';
 
 defineProps<{
 	note: Misskey.entities.Note;
@@ -77,7 +78,7 @@ const mock = inject<boolean>('mock', false);
 	margin: 0 .5em 0 0;
 	padding: 1px 6px;
 	font-size: 80%;
-	border: solid 0.5px var(--divider);
+	border: solid 0.5px var(--MI_THEME-divider);
 	border-radius: 3px;
 }
 
diff --git a/packages/frontend/src/components/MkNotePreview.vue b/packages/frontend/src/components/MkNotePreview.vue
index cc2f770cda..c4479bb0d6 100644
--- a/packages/frontend/src/components/MkNotePreview.vue
+++ b/packages/frontend/src/components/MkNotePreview.vue
@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <template>
 <div :class="$style.root">
-	<MkAvatar :class="$style.avatar" :user="user" link preview/>
+	<MkAvatar :class="$style.avatar" :user="user"/>
 	<div :class="$style.main">
 		<div :class="$style.header">
 			<MkUserName :user="user" :nowrap="true"/>
diff --git a/packages/frontend/src/components/MkNoteSimple.vue b/packages/frontend/src/components/MkNoteSimple.vue
index c3f3c42b42..e684cf2a30 100644
--- a/packages/frontend/src/components/MkNoteSimple.vue
+++ b/packages/frontend/src/components/MkNoteSimple.vue
@@ -51,7 +51,7 @@ const showContent = ref(false);
 	height: 34px;
 	border-radius: 8px;
 	position: sticky !important;
-	top: calc(16px + var(--stickyTop, 0px));
+	top: calc(16px + var(--MI-stickyTop, 0px));
 	left: 0;
 }
 
diff --git a/packages/frontend/src/components/MkNoteSub.vue b/packages/frontend/src/components/MkNoteSub.vue
index 829b37e7a7..e4bade309b 100644
--- a/packages/frontend/src/components/MkNoteSub.vue
+++ b/packages/frontend/src/components/MkNoteSub.vue
@@ -135,7 +135,7 @@ if (props.detail) {
 }
 
 .reply, .more {
-	border-left: solid 0.5px var(--divider);
+	border-left: solid 0.5px var(--MI_THEME-divider);
 	margin-top: 10px;
 }
 
@@ -156,7 +156,7 @@ if (props.detail) {
 .muted {
 	text-align: center;
 	padding: 8px !important;
-	border: 1px solid var(--divider);
+	border: 1px solid var(--MI_THEME-divider);
 	margin: 8px 8px 0 8px;
 	border-radius: 8px;
 }
diff --git a/packages/frontend/src/components/MkNotes.vue b/packages/frontend/src/components/MkNotes.vue
index 0856c146ba..1c17c6b691 100644
--- a/packages/frontend/src/components/MkNotes.vue
+++ b/packages/frontend/src/components/MkNotes.vue
@@ -56,17 +56,17 @@ defineExpose({
 .root {
 	&.noGap {
 		> .notes {
-			background: var(--panel);
+			background: var(--MI_THEME-panel);
 		}
 	}
 
 	&:not(.noGap) {
 		> .notes {
-			background: var(--bg);
+			background: var(--MI_THEME-bg);
 
 			.note {
-				background: var(--panel);
-				border-radius: var(--radius);
+				background: var(--MI_THEME-panel);
+				border-radius: var(--MI-radius);
 			}
 		}
 	}
diff --git a/packages/frontend/src/components/MkNotification.vue b/packages/frontend/src/components/MkNotification.vue
index 73cd7cd5b3..093bdb8b4c 100644
--- a/packages/frontend/src/components/MkNotification.vue
+++ b/packages/frontend/src/components/MkNotification.vue
@@ -6,14 +6,14 @@ SPDX-License-Identifier: AGPL-3.0-only
 <template>
 <div :class="$style.root">
 	<div :class="$style.head">
-		<MkAvatar v-if="['pollEnded', 'note'].includes(notification.type) && notification.note" :class="$style.icon" :user="notification.note.user" link preview/>
-		<MkAvatar v-else-if="['roleAssigned', 'achievementEarned'].includes(notification.type)" :class="$style.icon" :user="$i" link preview/>
+		<MkAvatar v-if="['pollEnded', 'note'].includes(notification.type) && 'note' in notification" :class="$style.icon" :user="notification.note.user" link preview/>
+		<MkAvatar v-else-if="['roleAssigned', 'achievementEarned', 'exportCompleted', 'login'].includes(notification.type)" :class="$style.icon" :user="$i" link preview/>
 		<div v-else-if="notification.type === 'reaction:grouped' && notification.note.reactionAcceptance === 'likeOnly'" :class="[$style.icon, $style.icon_reactionGroupHeart]"><i class="ti ti-heart" style="line-height: 1;"></i></div>
 		<div v-else-if="notification.type === 'reaction:grouped'" :class="[$style.icon, $style.icon_reactionGroup]"><i class="ti ti-plus" style="line-height: 1;"></i></div>
 		<div v-else-if="notification.type === 'renote:grouped'" :class="[$style.icon, $style.icon_renoteGroup]"><i class="ti ti-repeat" style="line-height: 1;"></i></div>
 		<img v-else-if="notification.type === 'test'" :class="$style.icon" :src="infoImageUrl"/>
-		<MkAvatar v-else-if="notification.user" :class="$style.icon" :user="notification.user" link preview/>
-		<img v-else-if="notification.icon" :class="[$style.icon, $style.icon_app]" :src="notification.icon" alt=""/>
+		<MkAvatar v-else-if="'user' in notification" :class="$style.icon" :user="notification.user" link preview/>
+		<img v-else-if="'icon' in notification && notification.icon != null" :class="[$style.icon, $style.icon_app]" :src="notification.icon" alt=""/>
 		<div
 			:class="[$style.subIcon, {
 				[$style.t_follow]: notification.type === 'follow',
@@ -25,6 +25,8 @@ SPDX-License-Identifier: AGPL-3.0-only
 				[$style.t_quote]: notification.type === 'quote',
 				[$style.t_pollEnded]: notification.type === 'pollEnded',
 				[$style.t_achievementEarned]: notification.type === 'achievementEarned',
+				[$style.t_exportCompleted]: notification.type === 'exportCompleted',
+				[$style.t_login]: notification.type === 'login',
 				[$style.t_roleAssigned]: notification.type === 'roleAssigned' && notification.role.iconUrl == null,
 			}]"
 		>
@@ -37,6 +39,8 @@ SPDX-License-Identifier: AGPL-3.0-only
 			<i v-else-if="notification.type === 'quote'" class="ti ti-quote"></i>
 			<i v-else-if="notification.type === 'pollEnded'" class="ti ti-chart-arrows"></i>
 			<i v-else-if="notification.type === 'achievementEarned'" class="ti ti-medal"></i>
+			<i v-else-if="notification.type === 'exportCompleted'" class="ti ti-archive"></i>
+			<i v-else-if="notification.type === 'login'" class="ti ti-login-2"></i>
 			<template v-else-if="notification.type === 'roleAssigned'">
 				<img v-if="notification.role.iconUrl" style="height: 1.3em; vertical-align: -22%;" :src="notification.role.iconUrl" alt=""/>
 				<i v-else class="ti ti-badges"></i>
@@ -46,7 +50,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 				:withTooltip="true"
 				:reaction="notification.reaction.replace(/^:(\w+):$/, ':$1@.:')"
 				:noStyle="true"
-				style="width: 100%; height: 100%;"
+				style="width: 100%; height: 100% !important; object-fit: contain;"
 			/>
 		</div>
 	</div>
@@ -56,7 +60,9 @@ SPDX-License-Identifier: AGPL-3.0-only
 			<span v-else-if="notification.type === 'note'">{{ i18n.ts._notification.newNote }}: <MkUserName :user="notification.note.user"/></span>
 			<span v-else-if="notification.type === 'roleAssigned'">{{ i18n.ts._notification.roleAssigned }}</span>
 			<span v-else-if="notification.type === 'achievementEarned'">{{ i18n.ts._notification.achievementEarned }}</span>
+			<span v-else-if="notification.type === 'login'">{{ i18n.ts._notification.login }}</span>
 			<span v-else-if="notification.type === 'test'">{{ i18n.ts._notification.testNotification }}</span>
+			<span v-else-if="notification.type === 'exportCompleted'">{{ i18n.tsx._notification.exportOfXCompleted({ x: exportEntityName[notification.exportedEntity] }) }}</span>
 			<MkA v-else-if="notification.type === 'follow' || notification.type === 'mention' || notification.type === 'reply' || notification.type === 'renote' || notification.type === 'quote' || notification.type === 'reaction' || notification.type === 'receiveFollowRequest' || notification.type === 'followRequestAccepted'" v-user-preview="notification.user.id" :class="$style.headerName" :to="userPage(notification.user)"><MkUserName :user="notification.user"/></MkA>
 			<span v-else-if="notification.type === 'reaction:grouped' && notification.note.reactionAcceptance === 'likeOnly'">{{ i18n.tsx._notification.likedBySomeUsers({ n: getActualReactedUsersCount(notification) }) }}</span>
 			<span v-else-if="notification.type === 'reaction:grouped'">{{ i18n.tsx._notification.reactedBySomeUsers({ n: getActualReactedUsersCount(notification) }) }}</span>
@@ -98,10 +104,20 @@ SPDX-License-Identifier: AGPL-3.0-only
 			<MkA v-else-if="notification.type === 'achievementEarned'" :class="$style.text" to="/my/achievements">
 				{{ i18n.ts._achievements._types['_' + notification.achievement].title }}
 			</MkA>
+			<MkA v-else-if="notification.type === 'exportCompleted'" :class="$style.text" :to="`/my/drive/file/${notification.fileId}`">
+				{{ i18n.ts.showFile }}
+			</MkA>
 			<template v-else-if="notification.type === 'follow'">
 				<span :class="$style.text" style="opacity: 0.6;">{{ i18n.ts.youGotNewFollower }}</span>
 			</template>
-			<span v-else-if="notification.type === 'followRequestAccepted'" :class="$style.text" style="opacity: 0.6;">{{ i18n.ts.followRequestAccepted }}</span>
+			<template v-else-if="notification.type === 'followRequestAccepted'">
+				<div :class="$style.text" style="opacity: 0.6;">{{ i18n.ts.followRequestAccepted }}</div>
+				<div v-if="notification.message" :class="$style.text" style="opacity: 0.6; font-style: oblique;">
+					<i class="ti ti-quote" :class="$style.quote"></i>
+					<span>{{ notification.message }}</span>
+					<i class="ti ti-quote" :class="$style.quote"></i>
+				</div>
+			</template>
 			<template v-else-if="notification.type === 'receiveFollowRequest'">
 				<span :class="$style.text" style="opacity: 0.6;">{{ i18n.ts.receiveFollowRequest }}</span>
 				<div v-if="full && !followRequestDone" :class="$style.followRequestCommands">
@@ -122,7 +138,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 							:withTooltip="true"
 							:reaction="reaction.reaction.replace(/^:(\w+):$/, ':$1@.:')"
 							:noStyle="true"
-							style="width: 100%; height: 100%;"
+							style="width: 100%; height: 100% !important; object-fit: contain;"
 						/>
 					</div>
 				</div>
@@ -161,16 +177,30 @@ const props = withDefaults(defineProps<{
 	full: false,
 });
 
+type ExportCompletedNotification = Misskey.entities.Notification & { type: 'exportCompleted' };
+
+const exportEntityName = {
+	antenna: i18n.ts.antennas,
+	blocking: i18n.ts.blockedUsers,
+	clip: i18n.ts.clips,
+	customEmoji: i18n.ts.customEmojis,
+	favorite: i18n.ts.favorites,
+	following: i18n.ts.following,
+	muting: i18n.ts.mutedUsers,
+	note: i18n.ts.notes,
+	userList: i18n.ts.lists,
+} as const satisfies Record<ExportCompletedNotification['exportedEntity'], string>;
+
 const followRequestDone = ref(false);
 
 const acceptFollowRequest = () => {
-	if (props.notification.user == null) return;
+	if (!('user' in props.notification)) return;
 	followRequestDone.value = true;
 	misskeyApi('following/requests/accept', { userId: props.notification.user.id });
 };
 
 const rejectFollowRequest = () => {
-	if (props.notification.user == null) return;
+	if (!('user' in props.notification)) return;
 	followRequestDone.value = true;
 	misskeyApi('following/requests/reject', { userId: props.notification.user.id });
 };
@@ -190,6 +220,17 @@ function getActualReactedUsersCount(notification: Misskey.entities.Notification)
 	overflow-wrap: break-word;
 	display: flex;
 	contain: content;
+	content-visibility: auto;
+	contain-intrinsic-size: 0 100px;
+
+	--eventFollow: #36aed2;
+	--eventRenote: #36d298;
+	--eventReply: #007aff;
+	--eventReactionHeart: var(--MI_THEME-love);
+	--eventReaction: #e99a0b;
+	--eventAchievement: #cb9a11;
+	--eventLogin: #007aff;
+	--eventOther: #88a6b7;
 }
 
 .head {
@@ -245,8 +286,8 @@ function getActualReactedUsersCount(notification: Misskey.entities.Notification)
 	height: 20px;
 	box-sizing: border-box;
 	border-radius: 100%;
-	background: var(--panel);
-	box-shadow: 0 0 0 3px var(--panel);
+	background: var(--MI_THEME-panel);
+	box-shadow: 0 0 0 3px var(--MI_THEME-panel);
 	font-size: 11px;
 	text-align: center;
 	color: #fff;
@@ -298,12 +339,24 @@ function getActualReactedUsersCount(notification: Misskey.entities.Notification)
 	pointer-events: none;
 }
 
+.t_exportCompleted {
+	padding: 3px;
+	background: var(--eventOther);
+	pointer-events: none;
+}
+
 .t_roleAssigned {
 	padding: 3px;
 	background: var(--eventOther);
 	pointer-events: none;
 }
 
+.t_login {
+	padding: 3px;
+	background: var(--eventLogin);
+	pointer-events: none;
+}
+
 .tail {
 	flex: 1;
 	min-width: 0;
@@ -343,7 +396,7 @@ function getActualReactedUsersCount(notification: Misskey.entities.Notification)
 	margin-right: 4px;
 	position: relative;
 
-	&:before {
+	&::before {
 		position: absolute;
 		transform: rotate(180deg);
 	}
@@ -386,8 +439,8 @@ function getActualReactedUsersCount(notification: Misskey.entities.Notification)
 	height: 20px;
 	box-sizing: border-box;
 	border-radius: 100%;
-	background: var(--panel);
-	box-shadow: 0 0 0 3px var(--panel);
+	background: var(--MI_THEME-panel);
+	box-shadow: 0 0 0 3px var(--MI_THEME-panel);
 	font-size: 11px;
 	text-align: center;
 	color: #fff;
diff --git a/packages/frontend/src/components/MkNotificationSelectWindow.vue b/packages/frontend/src/components/MkNotificationSelectWindow.vue
index 71b38d99ed..d07827d11a 100644
--- a/packages/frontend/src/components/MkNotificationSelectWindow.vue
+++ b/packages/frontend/src/components/MkNotificationSelectWindow.vue
@@ -35,7 +35,7 @@ import MkSwitch from './MkSwitch.vue';
 import MkInfo from './MkInfo.vue';
 import MkButton from './MkButton.vue';
 import MkModalWindow from '@/components/MkModalWindow.vue';
-import { notificationTypes } from '@/const.js';
+import { notificationTypes } from '@@/js/const.js';
 import { i18n } from '@/i18n.js';
 
 type TypesMap = Record<typeof notificationTypes[number], Ref<boolean>>
@@ -53,7 +53,7 @@ const props = withDefaults(defineProps<{
 
 const dialog = shallowRef<InstanceType<typeof MkModalWindow>>();
 
-const typesMap: TypesMap = notificationTypes.reduce((p, t) => ({ ...p, [t]: ref<boolean>(!props.excludeTypes.includes(t)) }), {} as any);
+const typesMap = notificationTypes.reduce((p, t) => ({ ...p, [t]: ref<boolean>(!props.excludeTypes.includes(t)) }), {} as TypesMap);
 
 function ok() {
 	emit('done', {
diff --git a/packages/frontend/src/components/MkNotifications.vue b/packages/frontend/src/components/MkNotifications.vue
index 389987338d..5a6ada474a 100644
--- a/packages/frontend/src/components/MkNotifications.vue
+++ b/packages/frontend/src/components/MkNotifications.vue
@@ -31,7 +31,7 @@ import MkDateSeparatedList from '@/components/MkDateSeparatedList.vue';
 import MkNote from '@/components/MkNote.vue';
 import { useStream } from '@/stream.js';
 import { i18n } from '@/i18n.js';
-import { notificationTypes } from '@/const.js';
+import { notificationTypes } from '@@/js/const.js';
 import { infoImageUrl } from '@/instance.js';
 import { defaultStore } from '@/store.js';
 import MkPullToRefresh from '@/components/MkPullToRefresh.vue';
@@ -106,6 +106,6 @@ defineExpose({
 
 <style lang="scss" module>
 .list {
-	background: var(--panel);
+	background: var(--MI_THEME-panel);
 }
 </style>
diff --git a/packages/frontend/src/components/MkNumberDiff.vue b/packages/frontend/src/components/MkNumberDiff.vue
index 1825cc5405..80c634fdce 100644
--- a/packages/frontend/src/components/MkNumberDiff.vue
+++ b/packages/frontend/src/components/MkNumberDiff.vue
@@ -24,11 +24,11 @@ const isZero = computed(() => props.value === 0);
 
 <style lang="scss" module>
 .isPlus {
-	color: var(--success);
+	color: var(--MI_THEME-success);
 }
 
 .isMinus {
-	color: var(--error);
+	color: var(--MI_THEME-error);
 }
 
 .isZero {
diff --git a/packages/frontend/src/components/MkObjectView.value.vue b/packages/frontend/src/components/MkObjectView.value.vue
index 870599aa94..7fa8c23c6c 100644
--- a/packages/frontend/src/components/MkObjectView.value.vue
+++ b/packages/frontend/src/components/MkObjectView.value.vue
@@ -39,7 +39,7 @@ import number from '@/filters/number.js';
 import XValue from '@/components/MkObjectView.value.vue';
 
 const props = defineProps<{
-	value: any;
+	value: unknown;
 }>();
 
 const collapsed = reactive({});
@@ -50,19 +50,19 @@ if (isObject(props.value)) {
 	}
 }
 
-function isObject(v): boolean {
+function isObject(v: unknown): v is Record<PropertyKey, unknown> {
 	return typeof v === 'object' && !Array.isArray(v) && v !== null;
 }
 
-function isArray(v): boolean {
+function isArray(v: unknown): v is unknown[] {
 	return Array.isArray(v);
 }
 
-function isEmpty(v): boolean {
+function isEmpty(v: unknown): v is Record<PropertyKey, never> | never[] {
 	return (isArray(v) && v.length === 0) || (isObject(v) && Object.keys(v).length === 0);
 }
 
-function collapsable(v): boolean {
+function collapsable(v: unknown): boolean {
 	return (isObject(v) || isArray(v)) && !isEmpty(v);
 }
 </script>
@@ -78,7 +78,7 @@ function collapsable(v): boolean {
 
 	> .boolean {
 		display: inline;
-		color: var(--codeBoolean);
+		color: var(--MI_THEME-codeBoolean);
 
 		&.true {
 			font-weight: bold;
@@ -91,12 +91,12 @@ function collapsable(v): boolean {
 
 	> .string {
 		display: inline;
-		color: var(--codeString);
+		color: var(--MI_THEME-codeString);
 	}
 
 	> .number {
 		display: inline;
-		color: var(--codeNumber);
+		color: var(--MI_THEME-codeNumber);
 	}
 
 	> .array.empty {
@@ -127,7 +127,7 @@ function collapsable(v): boolean {
 
 			> .toggle {
 				width: 16px;
-				color: var(--accent);
+				color: var(--MI_THEME-accent);
 				visibility: hidden;
 
 				&.visible {
diff --git a/packages/frontend/src/components/MkOmit.vue b/packages/frontend/src/components/MkOmit.vue
index 100a025653..a05176e2f4 100644
--- a/packages/frontend/src/components/MkOmit.vue
+++ b/packages/frontend/src/components/MkOmit.vue
@@ -47,7 +47,7 @@ onUnmounted(() => {
 
 <style lang="scss" module>
 .content {
-	--stickyTop: 0px;
+	--MI-stickyTop: 0px;
 
 	&.omitted {
 		position: relative;
@@ -62,11 +62,11 @@ onUnmounted(() => {
 			left: 0;
 			width: 100%;
 			height: 64px;
-			background: linear-gradient(0deg, var(--panel), var(--X15));
+			background: linear-gradient(0deg, var(--MI_THEME-panel), color(from var(--MI_THEME-panel) srgb r g b / 0));
 
 			> .fadeLabel {
 				display: inline-block;
-				background: var(--panel);
+				background: var(--MI_THEME-panel);
 				padding: 6px 10px;
 				font-size: 0.8em;
 				border-radius: 999px;
@@ -75,7 +75,7 @@ onUnmounted(() => {
 
 			&:hover {
 				> .fadeLabel {
-					background: var(--panelHighlight);
+					background: var(--MI_THEME-panelHighlight);
 				}
 			}
 		}
diff --git a/packages/frontend/src/components/MkPagePreview.vue b/packages/frontend/src/components/MkPagePreview.vue
index f6dc00698c..35a37a1f7d 100644
--- a/packages/frontend/src/components/MkPagePreview.vue
+++ b/packages/frontend/src/components/MkPagePreview.vue
@@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 -->
 
 <template>
-<MkA :to="`/@${page.user.username}/pages/${page.name}`" class="vhpxefrj" tabindex="-1">
+<MkA :to="`/@${page.user.username}/pages/${page.name}`" class="vhpxefrj">
 	<div v-if="page.eyeCatchingImage" class="thumbnail">
 		<MediaImage
 			:image="page.eyeCatchingImage"
@@ -42,7 +42,7 @@ const props = defineProps<{
 .eyeCatchingImageRoot {
 	width: 100%;
 	height: 200px;
-	border-radius: var(--radius) var(--radius) 0 0;
+	border-radius: var(--MI-radius) var(--MI-radius) 0 0;
 	overflow: hidden;
 }
 </style>
@@ -50,22 +50,39 @@ const props = defineProps<{
 <style lang="scss" scoped>
 .vhpxefrj {
 	display: block;
+	position: relative;
 
 	&:hover {
 		text-decoration: none;
-		color: var(--accent);
+		color: var(--MI_THEME-accent);
+	}
+
+	&:focus-within {
+		outline: none;
+
+		&::after {
+			content: "";
+			position: absolute;
+			top: 0;
+			left: 0;
+			width: 100%;
+			height: 100%;
+			border-radius: var(--MI-radius);
+			pointer-events: none;
+			box-shadow: inset 0 0 0 2px var(--MI_THEME-focus);
+		}
 	}
 
 	> .thumbnail {
 		& + article {
-			border-radius: 0 0 var(--radius) var(--radius);
+			border-radius: 0 0 var(--MI-radius) var(--MI-radius);
 		}
 	}
 
 	> article {
-		background-color: var(--panel);
+		background-color: var(--MI_THEME-panel);
 		padding: 16px;
-		border-radius: var(--radius);
+		border-radius: var(--MI-radius);
 
 		> header {
 			margin-bottom: 8px;
@@ -98,7 +115,6 @@ const props = defineProps<{
 			> p {
 				display: inline-block;
 				margin: 0;
-				color: var(--urlPreviewInfo);
 				font-size: 0.8em;
 				line-height: 16px;
 				vertical-align: top;
diff --git a/packages/frontend/src/components/MkPageWindow.vue b/packages/frontend/src/components/MkPageWindow.vue
index aa4509b14b..9547423227 100644
--- a/packages/frontend/src/components/MkPageWindow.vue
+++ b/packages/frontend/src/components/MkPageWindow.vue
@@ -13,7 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 	:buttonsLeft="buttonsLeft"
 	:buttonsRight="buttonsRight"
 	:contextmenu="contextmenu"
-	@closed="$emit('closed')"
+	@closed="emit('closed')"
 >
 	<template #header>
 		<template v-if="pageMetadata">
@@ -30,17 +30,17 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <script lang="ts" setup>
 import { computed, onMounted, onUnmounted, provide, ref, shallowRef } from 'vue';
+import { url } from '@@/js/config.js';
+import { getScrollContainer } from '@@/js/scroll.js';
 import RouterView from '@/components/global/RouterView.vue';
 import MkWindow from '@/components/MkWindow.vue';
 import { popout as _popout } from '@/scripts/popout.js';
-import copyToClipboard from '@/scripts/copy-to-clipboard.js';
-import { url } from '@/config.js';
+import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
 import { useScrollPositionManager } from '@/nirax.js';
 import { i18n } from '@/i18n.js';
 import { PageMetadata, provideMetadataReceiver, provideReactiveMetadata } from '@/scripts/page-metadata.js';
 import { openingWindowsCount } from '@/os.js';
 import { claimAchievement } from '@/scripts/achievements.js';
-import { getScrollContainer } from '@/scripts/scroll.js';
 import { useRouterFactory } from '@/router/supplier.js';
 import { mainRouter } from '@/router/main.js';
 
@@ -48,7 +48,7 @@ const props = defineProps<{
 	initialPath: string;
 }>();
 
-defineEmits<{
+const emit = defineEmits<{
 	(ev: 'closed'): void;
 }>();
 
@@ -58,7 +58,7 @@ const windowRouter = routerFactory(props.initialPath);
 const contents = shallowRef<HTMLElement | null>(null);
 const pageMetadata = ref<null | PageMetadata>(null);
 const windowEl = shallowRef<InstanceType<typeof MkWindow>>();
-const history = ref<{ path: string; key: any; }[]>([{
+const history = ref<{ path: string; key: string; }[]>([{
 	path: windowRouter.getCurrentPath(),
 	key: windowRouter.getCurrentKey(),
 }]);
@@ -179,8 +179,8 @@ defineExpose({
 	overscroll-behavior: contain;
 
 	min-height: 100%;
-	background: var(--bg);
+	background: var(--MI_THEME-bg);
 
-	--margin: var(--marginHalf);
+	--MI-margin: var(--MI-marginHalf);
 }
 </style>
diff --git a/packages/frontend/src/components/MkPagination.vue b/packages/frontend/src/components/MkPagination.vue
index 62a85389ad..ea299c319e 100644
--- a/packages/frontend/src/components/MkPagination.vue
+++ b/packages/frontend/src/components/MkPagination.vue
@@ -45,10 +45,10 @@ SPDX-License-Identifier: AGPL-3.0-only
 <script lang="ts">
 import { computed, ComputedRef, isRef, nextTick, onActivated, onBeforeMount, onBeforeUnmount, onDeactivated, ref, shallowRef, watch } from 'vue';
 import * as Misskey from 'misskey-js';
+import { useDocumentVisibility } from '@@/js/use-document-visibility.js';
+import { onScrollTop, isTopVisible, getBodyScrollHeight, getScrollContainer, onScrollBottom, scrollToBottom, scroll, isBottomVisible } from '@@/js/scroll.js';
 import * as os from '@/os.js';
 import { misskeyApi } from '@/scripts/misskey-api.js';
-import { onScrollTop, isTopVisible, getBodyScrollHeight, getScrollContainer, onScrollBottom, scrollToBottom, scroll, isBottomVisible } from '@/scripts/scroll.js';
-import { useDocumentVisibility } from '@/scripts/use-document-visibility.js';
 import { defaultStore } from '@/store.js';
 import { MisskeyEntity } from '@/types/date-separated-list.js';
 import { i18n } from '@/i18n.js';
@@ -125,8 +125,6 @@ const items = ref<MisskeyEntityMap>(new Map());
  */
 const queue = ref<MisskeyEntityMap>(new Map());
 
-const offset = ref(0);
-
 /**
  * 初期化中かどうか(trueならMkLoadingで全て隠す)
  */
@@ -179,7 +177,9 @@ watch([backed, contentEl], () => {
 	if (!backed.value) {
 		if (!contentEl.value) return;
 
-		scrollRemove.value = (props.pagination.reversed ? onScrollBottom : onScrollTop)(contentEl.value, executeQueue, TOLERANCE);
+		scrollRemove.value = props.pagination.reversed
+			? onScrollBottom(contentEl.value, executeQueue, TOLERANCE)
+			: onScrollTop(contentEl.value, (topVisible) => { if (topVisible) executeQueue(); }, TOLERANCE);
 	} else {
 		if (scrollRemove.value) scrollRemove.value();
 		scrollRemove.value = null;
@@ -223,7 +223,6 @@ async function init(): Promise<void> {
 			more.value = true;
 		}
 
-		offset.value = res.length;
 		error.value = false;
 		fetching.value = false;
 	}, err => {
@@ -244,7 +243,7 @@ const fetchMore = async (): Promise<void> => {
 		...params,
 		limit: SECOND_FETCH_LIMIT,
 		...(props.pagination.offsetMode ? {
-			offset: offset.value,
+			offset: items.value.size,
 		} : {
 			untilId: Array.from(items.value.keys()).at(-1),
 		}),
@@ -294,7 +293,6 @@ const fetchMore = async (): Promise<void> => {
 				moreFetching.value = false;
 			}
 		}
-		offset.value += res.length;
 	}, err => {
 		moreFetching.value = false;
 	});
@@ -308,7 +306,7 @@ const fetchMoreAhead = async (): Promise<void> => {
 		...params,
 		limit: SECOND_FETCH_LIMIT,
 		...(props.pagination.offsetMode ? {
-			offset: offset.value,
+			offset: items.value.size,
 		} : {
 			sinceId: Array.from(items.value.keys()).at(-1),
 		}),
@@ -320,7 +318,6 @@ const fetchMoreAhead = async (): Promise<void> => {
 			items.value = concatMapWithArray(items.value, res);
 			more.value = true;
 		}
-		offset.value += res.length;
 		moreFetching.value = false;
 	}, err => {
 		moreFetching.value = false;
diff --git a/packages/frontend/src/components/MkPoll.vue b/packages/frontend/src/components/MkPoll.vue
index a98690f1c3..e70ac7ff1a 100644
--- a/packages/frontend/src/components/MkPoll.vue
+++ b/packages/frontend/src/components/MkPoll.vue
@@ -9,7 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 		<li v-for="(choice, i) in poll.choices" :key="i" :class="$style.choice" @click="vote(i)">
 			<div :class="$style.bg" :style="{ 'width': `${showResult ? (choice.votes / total * 100) : 0}%` }"></div>
 			<span :class="$style.fg">
-				<template v-if="choice.isVoted"><i class="ti ti-check" style="margin-right: 4px; color: var(--accent);"></i></template>
+				<template v-if="choice.isVoted"><i class="ti ti-check" style="margin-right: 4px; color: var(--MI_THEME-accent);"></i></template>
 				<Mfm :text="choice.text" :plain="true"/>
 				<span v-if="showResult" style="margin-left: 4px; opacity: 0.7;">({{ i18n.tsx._poll.votesCount({ n: choice.votes }) }})</span>
 			</span>
@@ -29,12 +29,14 @@ SPDX-License-Identifier: AGPL-3.0-only
 <script lang="ts" setup>
 import { computed, ref } from 'vue';
 import * as Misskey from 'misskey-js';
+import { host } from '@@/js/config.js';
+import { useInterval } from '@@/js/use-interval.js';
+import type { OpenOnRemoteOptions } from '@/scripts/please-login.js';
 import { sum } from '@/scripts/array.js';
 import { pleaseLogin } from '@/scripts/please-login.js';
 import * as os from '@/os.js';
 import { misskeyApi } from '@/scripts/misskey-api.js';
 import { i18n } from '@/i18n.js';
-import { useInterval } from '@/scripts/use-interval.js';
 
 const props = defineProps<{
 	noteId: string;
@@ -60,6 +62,11 @@ const timer = computed(() => i18n.tsx._poll[
 
 const showResult = ref(props.readOnly || isVoted.value);
 
+const pleaseLoginContext = computed<OpenOnRemoteOptions>(() => ({
+	type: 'lookup',
+	url: `https://${host}/notes/${props.noteId}`,
+}));
+
 // 期限付きアンケート
 if (props.poll.expiresAt) {
 	const tick = () => {
@@ -76,10 +83,10 @@ if (props.poll.expiresAt) {
 }
 
 const vote = async (id) => {
-	pleaseLogin();
-
 	if (props.readOnly || closed.value || isVoted.value) return;
 
+	pleaseLogin({ openOnRemote: pleaseLoginContext.value });
+
 	const { canceled } = await os.confirm({
 		type: 'question',
 		text: i18n.tsx.voteConfirm({ choice: props.poll.choices[id].text }),
@@ -107,8 +114,8 @@ const vote = async (id) => {
 	position: relative;
 	margin: 4px 0;
 	padding: 4px;
-	//border: solid 0.5px var(--divider);
-	background: var(--accentedBg);
+	//border: solid 0.5px var(--MI_THEME-divider);
+	background: var(--MI_THEME-accentedBg);
 	border-radius: 4px;
 	overflow: clip;
 	cursor: pointer;
@@ -119,8 +126,8 @@ const vote = async (id) => {
 	top: 0;
 	left: 0;
 	height: 100%;
-	background: var(--accent);
-	background: linear-gradient(90deg,var(--buttonGradateA),var(--buttonGradateB));
+	background: var(--MI_THEME-accent);
+	background: linear-gradient(90deg,var(--MI_THEME-buttonGradateA),var(--MI_THEME-buttonGradateB));
 	transition: width 1s ease;
 }
 
@@ -128,17 +135,17 @@ const vote = async (id) => {
 	position: relative;
 	display: inline-block;
 	padding: 3px 5px;
-	background: var(--panel);
+	background: var(--MI_THEME-panel);
 	border-radius: 3px;
 }
 
 .info {
-	color: var(--fg);
+	color: var(--MI_THEME-fg);
 }
 
 .done {
 	.choice {
-		cursor: default;
+		cursor: initial;
 	}
 }
 </style>
diff --git a/packages/frontend/src/components/MkPollEditor.vue b/packages/frontend/src/components/MkPollEditor.vue
index db74354bbb..3726ddf822 100644
--- a/packages/frontend/src/components/MkPollEditor.vue
+++ b/packages/frontend/src/components/MkPollEditor.vue
@@ -37,7 +37,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 				</MkInput>
 			</section>
 			<section v-else-if="expiration === 'after'">
-				<MkInput v-model="after" small type="number" class="input">
+				<MkInput v-model="after" small type="number" min="1" class="input">
 					<template #label>{{ i18n.ts._poll.duration }}</template>
 				</MkInput>
 				<MkSelect v-model="unit" small>
diff --git a/packages/frontend/src/components/MkPopupMenu.vue b/packages/frontend/src/components/MkPopupMenu.vue
index be0b07612a..df664e49f7 100644
--- a/packages/frontend/src/components/MkPopupMenu.vue
+++ b/packages/frontend/src/components/MkPopupMenu.vue
@@ -4,8 +4,8 @@ SPDX-License-Identifier: AGPL-3.0-only
 -->
 
 <template>
-<MkModal ref="modal" v-slot="{ type, maxHeight }" :manualShowing="manualShowing" :zPriority="'high'" :src="src" :transparentBg="true" @click="click" @close="onModalClose" @closed="onModalClosed">
-	<MkMenu :items="items" :align="align" :width="width" :max-height="maxHeight" :asDrawer="type === 'drawer'" :class="{ [$style.drawer]: type === 'drawer' }" @close="onMenuClose" @hide="hide"/>
+<MkModal ref="modal" v-slot="{ type, maxHeight }" :manualShowing="manualShowing" :zPriority="'high'" :src="src" :transparentBg="true" :returnFocusTo="returnFocusTo" @click="click" @close="onModalClose" @closed="onModalClosed">
+	<MkMenu :items="items" :align="align" :width="width" :max-height="maxHeight" :asDrawer="type === 'drawer'" :returnFocusTo="returnFocusTo" :class="{ [$style.drawer]: type === 'drawer' }" @close="onMenuClose" @hide="hide"/>
 </MkModal>
 </template>
 
@@ -13,14 +13,14 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { ref, shallowRef } from 'vue';
 import MkModal from './MkModal.vue';
 import MkMenu from './MkMenu.vue';
-import { MenuItem } from '@/types/menu.js';
+import type { MenuItem } from '@/types/menu.js';
 
 defineProps<{
 	items: MenuItem[];
 	align?: 'center' | string;
 	width?: number;
-	viaKeyboard?: boolean;
-	src?: any;
+	src?: HTMLElement | null;
+	returnFocusTo?: HTMLElement | null;
 }>();
 
 const emit = defineEmits<{
diff --git a/packages/frontend/src/components/MkPostForm.vue b/packages/frontend/src/components/MkPostForm.vue
index 71d53c39ff..7cf07f4ef3 100644
--- a/packages/frontend/src/components/MkPostForm.vue
+++ b/packages/frontend/src/components/MkPostForm.vue
@@ -65,10 +65,10 @@ SPDX-License-Identifier: AGPL-3.0-only
 		</div>
 	</div>
 	<MkInfo v-if="hasNotSpecifiedMentions" warn :class="$style.hasNotSpecifiedMentions">{{ i18n.ts.notSpecifiedMentionWarning }} - <button class="_textButton" @click="addMissingMention()">{{ i18n.ts.add }}</button></MkInfo>
-	<input v-show="useCw" ref="cwInputEl" v-model="cw" :class="$style.cw" :placeholder="i18n.ts.annotation" @keydown="onKeydown">
+	<input v-show="useCw" ref="cwInputEl" v-model="cw" :class="$style.cw" :placeholder="i18n.ts.annotation" @keydown="onKeydown" @keyup="onKeyup" @compositionend="onCompositionEnd">
 	<div :class="[$style.textOuter, { [$style.withCw]: useCw }]">
 		<div v-if="channel" :class="$style.colorBar" :style="{ background: channel.color }"></div>
-		<textarea ref="textareaEl" v-model="text" :class="[$style.text]" :disabled="posting || posted" :readonly="textAreaReadOnly" :placeholder="placeholder" data-cy-post-form-text @keydown="onKeydown" @paste="onPaste" @compositionupdate="onCompositionUpdate" @compositionend="onCompositionEnd"/>
+		<textarea ref="textareaEl" v-model="text" :class="[$style.text]" :disabled="posting || posted" :readonly="textAreaReadOnly" :placeholder="placeholder" data-cy-post-form-text @keydown="onKeydown" @keyup="onKeyup" @paste="onPaste" @compositionupdate="onCompositionUpdate" @compositionend="onCompositionEnd"/>
 		<div v-if="maxTextLength - textLength < 100" :class="['_acrylic', $style.textCount, { [$style.textOver]: textLength > maxTextLength }]">{{ maxTextLength - textLength }}</div>
 	</div>
 	<input v-show="withHashtags" ref="hashtagsInputEl" v-model="hashtags" :class="$style.hashtags" :placeholder="i18n.ts.hashtags" list="hashtags">
@@ -105,11 +105,11 @@ import * as mfm from 'mfm-js';
 import * as Misskey from 'misskey-js';
 import insertTextAtCursor from 'insert-text-at-cursor';
 import { toASCII } from 'punycode/';
+import { host, url } from '@@/js/config.js';
 import MkNoteSimple from '@/components/MkNoteSimple.vue';
 import MkNotePreview from '@/components/MkNotePreview.vue';
 import XPostFormAttaches from '@/components/MkPostFormAttaches.vue';
 import MkPollEditor, { type PollEditorModelValue } from '@/components/MkPollEditor.vue';
-import { host, url } from '@/config.js';
 import { erase, unique } from '@/scripts/array.js';
 import { extractMentions } from '@/scripts/extract-mentions.js';
 import { formatTimeString } from '@/scripts/format-time-string.js';
@@ -129,25 +129,13 @@ import { miLocalStorage } from '@/local-storage.js';
 import { claimAchievement } from '@/scripts/achievements.js';
 import { emojiPicker } from '@/scripts/emoji-picker.js';
 import { mfmFunctionPicker } from '@/scripts/mfm-function-picker.js';
+import type { PostFormProps } from '@/types/post-form.js';
 
 const $i = signinRequired();
 
 const modal = inject('modal');
 
-const props = withDefaults(defineProps<{
-	reply?: Misskey.entities.Note;
-	renote?: Misskey.entities.Note;
-	channel?: Misskey.entities.Channel; // TODO
-	mention?: Misskey.entities.User;
-	specified?: Misskey.entities.UserDetailed;
-	initialText?: string;
-	initialCw?: string;
-	initialVisibility?: (typeof Misskey.noteVisibilities)[number];
-	initialFiles?: Misskey.entities.DriveFile[];
-	initialLocalOnly?: boolean;
-	initialVisibleUsers?: Misskey.entities.UserDetailed[];
-	initialNote?: Misskey.entities.Note;
-	instant?: boolean;
+const props = withDefaults(defineProps<PostFormProps & {
 	fixed?: boolean;
 	autofocus?: boolean;
 	freezeAfterPosted?: boolean;
@@ -201,6 +189,7 @@ const recentHashtags = ref(JSON.parse(miLocalStorage.getItem('hashtags') ?? '[]'
 const imeText = ref('');
 const showingOptions = ref(false);
 const textAreaReadOnly = ref(false);
+const justEndedComposition = ref(false);
 
 const draftKey = computed((): string => {
 	let key = props.channel ? `channel:${props.channel.id}` : '';
@@ -245,7 +234,7 @@ const submitText = computed((): string => {
 });
 
 const textLength = computed((): number => {
-	return (text.value + imeText.value).trim().length;
+	return (text.value + imeText.value).length;
 });
 
 const maxTextLength = computed((): number => {
@@ -259,7 +248,7 @@ const canPost = computed((): boolean => {
 			1 <= files.value.length ||
 			poll.value != null ||
 			props.renote != null ||
-			(props.reply != null && quoteId.value != null)
+			quoteId.value != null
 		) &&
 		(textLength.value <= maxTextLength.value) &&
 		(!poll.value || poll.value.choices.length >= 2);
@@ -367,6 +356,8 @@ function watchForDraft() {
 	watch(files, () => saveDraft(), { deep: true });
 	watch(visibility, () => saveDraft());
 	watch(localOnly, () => saveDraft());
+	watch(quoteId, () => saveDraft());
+	watch(reactionAcceptance, () => saveDraft());
 }
 
 function checkMissingMention() {
@@ -463,7 +454,7 @@ function setVisibility() {
 		return;
 	}
 
-	os.popup(defineAsyncComponent(() => import('@/components/MkVisibilityPicker.vue')), {
+	const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkVisibilityPicker.vue')), {
 		currentVisibility: visibility.value,
 		isSilenced: $i.isSilenced,
 		localOnly: localOnly.value,
@@ -476,7 +467,8 @@ function setVisibility() {
 				defaultStore.set('visibility', visibility.value);
 			}
 		},
-	}, 'closed');
+		closed: () => dispose(),
+	});
 }
 
 async function toggleLocalOnly() {
@@ -569,7 +561,14 @@ function clear() {
 
 function onKeydown(ev: KeyboardEvent) {
 	if (ev.key === 'Enter' && (ev.ctrlKey || ev.metaKey) && canPost.value) post();
-	if (ev.key === 'Escape') emit('esc');
+
+	// justEndedComposition.value is for Safari, which keyDown occurs after compositionend.
+	// ev.isComposing is for another browsers.
+	if (ev.key === 'Escape' && !justEndedComposition.value && !ev.isComposing) emit('esc');
+}
+
+function onKeyup(ev: KeyboardEvent) {
+	justEndedComposition.value = false;
 }
 
 function onCompositionUpdate(ev: CompositionEvent) {
@@ -578,6 +577,7 @@ function onCompositionUpdate(ev: CompositionEvent) {
 
 function onCompositionEnd(ev: CompositionEvent) {
 	imeText.value = '';
+	justEndedComposition.value = true;
 }
 
 async function onPaste(ev: ClipboardEvent) {
@@ -624,8 +624,8 @@ async function onPaste(ev: ClipboardEvent) {
 				return;
 			}
 
-			const fileName = formatTimeString(new Date(), defaultStore.state.pastedFileName).replace(/{{number}}/g, "0");
-			const file = new File([paste], `${fileName}.txt`, { type: "text/plain" });
+			const fileName = formatTimeString(new Date(), defaultStore.state.pastedFileName).replace(/{{number}}/g, '0');
+			const file = new File([paste], `${fileName}.txt`, { type: 'text/plain' });
 			upload(file, `${fileName}.txt`);
 		});
 	}
@@ -701,6 +701,8 @@ function saveDraft() {
 			files: files.value,
 			poll: poll.value,
 			visibleUserIds: visibility.value === 'specified' ? visibleUsers.value.map(x => x.id) : undefined,
+			quoteId: quoteId.value,
+			reactionAcceptance: reactionAcceptance.value,
 		},
 	};
 
@@ -731,7 +733,9 @@ async function post(ev?: MouseEvent) {
 			const rect = el.getBoundingClientRect();
 			const x = rect.left + (el.offsetWidth / 2);
 			const y = rect.top + (el.offsetHeight / 2);
-			os.popup(MkRippleEffect, { x, y }, {}, 'end');
+			const { dispose } = os.popup(MkRippleEffect, { x, y }, {
+				end: () => dispose(),
+			});
 		}
 	}
 
@@ -905,10 +909,23 @@ async function insertEmoji(ev: MouseEvent) {
 	textAreaReadOnly.value = true;
 	const target = ev.currentTarget ?? ev.target;
 	if (target == null) return;
+
+	// emojiPickerはダイアログが閉じずにtextareaとやりとりするので、
+	// focustrapをかけているとinsertTextAtCursorが効かない
+	// そのため、投稿フォームのテキストに直接注入する
+	// See: https://github.com/misskey-dev/misskey/pull/14282
+	//      https://github.com/misskey-dev/misskey/issues/14274
+
+	let pos = textareaEl.value?.selectionStart ?? 0;
+	let posEnd = textareaEl.value?.selectionEnd ?? text.value.length;
 	emojiPicker.show(
 		target as HTMLElement,
 		emoji => {
-			insertTextAtCursor(textareaEl.value, emoji);
+			const textBefore = text.value.substring(0, pos);
+			const textAfter = text.value.substring(posEnd);
+			text.value = textBefore + emoji + textAfter;
+			pos += emoji.length;
+			posEnd += emoji.length;
 		},
 		() => {
 			textAreaReadOnly.value = false;
@@ -933,8 +950,8 @@ function showActions(ev: MouseEvent) {
 			action.handler({
 				text: text.value,
 				cw: cw.value,
-			}, (key, value: any) => {
-				if (typeof key !== 'string') return;
+			}, (key, value) => {
+				if (typeof key !== 'string' || typeof value !== 'string') return;
 				if (key === 'text') { text.value = value; }
 				if (key === 'cw') { useCw.value = value !== null; cw.value = value; }
 			});
@@ -994,6 +1011,8 @@ onMounted(() => {
 						users.forEach(u => pushVisibleUser(u));
 					});
 				}
+				quoteId.value = draft.data.quoteId;
+				reactionAcceptance.value = draft.data.reactionAcceptance;
 			}
 		}
 
@@ -1001,9 +1020,11 @@ onMounted(() => {
 		if (props.initialNote) {
 			const init = props.initialNote;
 			text.value = init.text ? init.text : '';
-			files.value = init.files ?? [];
-			cw.value = init.cw ?? null;
 			useCw.value = init.cw != null;
+			cw.value = init.cw ?? null;
+			visibility.value = init.visibility;
+			localOnly.value = init.localOnly ?? false;
+			files.value = init.files ?? [];
 			if (init.poll) {
 				poll.value = {
 					choices: init.poll.choices.map(x => x.text),
@@ -1012,9 +1033,13 @@ onMounted(() => {
 					expiredAfter: null,
 				};
 			}
-			visibility.value = init.visibility;
-			localOnly.value = init.localOnly ?? false;
+			if (init.visibleUserIds) {
+				misskeyApi('users/show', { userIds: init.visibleUserIds }).then(users => {
+					users.forEach(u => pushVisibleUser(u));
+				});
+			}
 			quoteId.value = init.renote ? init.renote.id : null;
+			reactionAcceptance.value = init.reactionAcceptance;
 		}
 
 		nextTick(() => watchForDraft());
@@ -1087,6 +1112,15 @@ defineExpose({
 	margin: 12px 12px 12px 6px;
 	vertical-align: bottom;
 
+	&:focus-visible {
+		outline: none;
+
+		.submitInner {
+			outline: 2px solid var(--MI_THEME-fgOnAccent);
+			outline-offset: -4px;
+		}
+	}
+
 	&:disabled {
 		opacity: 0.7;
 	}
@@ -1097,13 +1131,13 @@ defineExpose({
 
 	&:not(:disabled):hover {
 		> .inner {
-			background: linear-gradient(90deg, var(--X8), var(--X8));
+			background: linear-gradient(90deg, hsl(from var(--MI_THEME-accent) h s calc(l + 5)), hsl(from var(--MI_THEME-accent) h s calc(l + 5)));
 		}
 	}
 
 	&:not(:disabled):active {
 		> .inner {
-			background: linear-gradient(90deg, var(--X8), var(--X8));
+			background: linear-gradient(90deg, hsl(from var(--MI_THEME-accent) h s calc(l + 5)), hsl(from var(--MI_THEME-accent) h s calc(l + 5)));
 		}
 	}
 }
@@ -1125,8 +1159,8 @@ defineExpose({
 	border-radius: 6px;
 	min-width: 90px;
 	box-sizing: border-box;
-	color: var(--fgOnAccent);
-	background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB));
+	color: var(--MI_THEME-fgOnAccent);
+	background: linear-gradient(90deg, var(--MI_THEME-buttonGradateA), var(--MI_THEME-buttonGradateB));
 }
 
 .headerRightItem {
@@ -1135,7 +1169,7 @@ defineExpose({
 	border-radius: 6px;
 
 	&:hover {
-		background: var(--X5);
+		background: var(--MI_THEME-X5);
 	}
 
 	&:disabled {
@@ -1170,6 +1204,15 @@ defineExpose({
 	min-height: 75px;
 	max-height: 150px;
 	overflow: auto;
+	background-size: auto auto;
+}
+
+html[data-color-scheme=dark] .preview {
+	background-image: repeating-linear-gradient(135deg, transparent, transparent 5px, #0004 5px, #0004 10px);
+}
+
+html[data-color-scheme=light] .preview {
+	background-image: repeating-linear-gradient(135deg, transparent, transparent 5px, #00000005 5px, #00000005 10px);
 }
 
 .targetNote {
@@ -1178,7 +1221,7 @@ defineExpose({
 
 .withQuote {
 	margin: 0 0 8px 0;
-	color: var(--accent);
+	color: var(--MI_THEME-accent);
 }
 
 .toSpecified {
@@ -1198,7 +1241,7 @@ defineExpose({
 	margin-right: 14px;
 	padding: 8px 0 8px 8px;
 	border-radius: 8px;
-	background: var(--X4);
+	background: var(--MI_THEME-X4);
 }
 
 .hasNotSpecifiedMentions {
@@ -1217,7 +1260,7 @@ defineExpose({
 	border: none;
 	border-radius: 0;
 	background: transparent;
-	color: var(--fg);
+	color: var(--MI_THEME-fg);
 	font-family: inherit;
 
 	&:focus {
@@ -1232,14 +1275,14 @@ defineExpose({
 .cw {
 	z-index: 1;
 	padding-bottom: 8px;
-	border-bottom: solid 0.5px var(--divider);
+	border-bottom: solid 0.5px var(--MI_THEME-divider);
 }
 
 .hashtags {
 	z-index: 1;
 	padding-top: 8px;
 	padding-bottom: 8px;
-	border-top: solid 0.5px var(--divider);
+	border-top: solid 0.5px var(--MI_THEME-divider);
 }
 
 .textOuter {
@@ -1265,7 +1308,7 @@ defineExpose({
 	right: 2px;
 	padding: 4px 6px;
 	font-size: .9em;
-	color: var(--warn);
+	color: var(--MI_THEME-warn);
 	border-radius: 6px;
 	min-width: 1.6em;
 	text-align: center;
@@ -1309,16 +1352,16 @@ defineExpose({
 	border-radius: 6px;
 
 	&:hover {
-		background: var(--X5);
+		background: var(--MI_THEME-X5);
 	}
 
 	&.footerButtonActive {
-		color: var(--accent);
+		color: var(--MI_THEME-accent);
 	}
 }
 
 .previewButtonActive {
-	color: var(--accent);
+	color: var(--MI_THEME-accent);
 }
 
 @container (max-width: 500px) {
diff --git a/packages/frontend/src/components/MkPostFormAttaches.vue b/packages/frontend/src/components/MkPostFormAttaches.vue
index 95eb367318..56e026aa3c 100644
--- a/packages/frontend/src/components/MkPostFormAttaches.vue
+++ b/packages/frontend/src/components/MkPostFormAttaches.vue
@@ -6,8 +6,15 @@ SPDX-License-Identifier: AGPL-3.0-only
 <template>
 <div v-show="props.modelValue.length != 0" :class="$style.root">
 	<Sortable :modelValue="props.modelValue" :class="$style.files" itemKey="id" :animation="150" :delay="100" :delayOnTouchOnly="true" @update:modelValue="v => emit('update:modelValue', v)">
-		<template #item="{element}">
-			<div :class="$style.file" @click="showFileMenu(element, $event)" @contextmenu.prevent="showFileMenu(element, $event)">
+		<template #item="{ element }">
+			<div
+				:class="$style.file"
+				role="button"
+				tabindex="0"
+				@click="showFileMenu(element, $event)"
+				@keydown.space.enter="showFileMenu(element, $event)"
+				@contextmenu.prevent="showFileMenu(element, $event)"
+			>
 				<MkDriveFileThumbnail :data-id="element.id" :class="$style.thumbnail" :file="element" fit="cover"/>
 				<div v-if="element.isSensitive" :class="$style.sensitive">
 					<i class="ti ti-eye-exclamation" style="margin: auto;"></i>
@@ -26,18 +33,19 @@ import MkDriveFileThumbnail from '@/components/MkDriveFileThumbnail.vue';
 import * as os from '@/os.js';
 import { misskeyApi } from '@/scripts/misskey-api.js';
 import { i18n } from '@/i18n.js';
+import type { MenuItem } from '@/types/menu.js';
 
 const Sortable = defineAsyncComponent(() => import('vuedraggable').then(x => x.default));
 
 const props = defineProps<{
-	modelValue: any[];
+	modelValue: Misskey.entities.DriveFile[];
 	detachMediaFn?: (id: string) => void;
 }>();
 
 const mock = inject<boolean>('mock', false);
 
 const emit = defineEmits<{
-	(ev: 'update:modelValue', value: any[]): void;
+	(ev: 'update:modelValue', value: Misskey.entities.DriveFile[]): void;
 	(ev: 'detach', id: string): void;
 	(ev: 'changeSensitive', file: Misskey.entities.DriveFile, isSensitive: boolean): void;
 	(ev: 'changeName', file: Misskey.entities.DriveFile, newName: string): void;
@@ -63,7 +71,7 @@ async function detachAndDeleteMedia(file: Misskey.entities.DriveFile) {
 
 	const { canceled } = await os.confirm({
 		type: 'warning',
-		text: i18n.t('driveFileDeleteConfirm', { name: file.name }),
+		text: i18n.tsx.driveFileDeleteConfirm({ name: file.name }),
 	});
 
 	if (canceled) return;
@@ -105,10 +113,10 @@ async function rename(file) {
 	});
 }
 
-async function describe(file) {
+async function describe(file: Misskey.entities.DriveFile) {
 	if (mock) return;
 
-	os.popup(defineAsyncComponent(() => import('@/components/MkFileCaptionEditWindow.vue')), {
+	const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkFileCaptionEditWindow.vue')), {
 		default: file.comment !== null ? file.comment : '',
 		file: file,
 	}, {
@@ -121,7 +129,8 @@ async function describe(file) {
 				file.comment = comment;
 			});
 		},
-	}, 'closed');
+		closed: () => dispose(),
+	});
 }
 
 async function crop(file: Misskey.entities.DriveFile): Promise<void> {
@@ -131,11 +140,14 @@ async function crop(file: Misskey.entities.DriveFile): Promise<void> {
 	emit('replaceFile', file, newFile);
 }
 
-function showFileMenu(file: Misskey.entities.DriveFile, ev: MouseEvent): void {
+function showFileMenu(file: Misskey.entities.DriveFile, ev: MouseEvent | KeyboardEvent): void {
 	if (menuShowing) return;
 
 	const isImage = file.type.startsWith('image/');
-	os.popupMenu([{
+
+	const menuItems: MenuItem[] = [];
+
+	menuItems.push({
 		text: i18n.ts.renameFile,
 		icon: 'ti ti-forms',
 		action: () => { rename(file); },
@@ -147,11 +159,17 @@ function showFileMenu(file: Misskey.entities.DriveFile, ev: MouseEvent): void {
 		text: i18n.ts.describeFile,
 		icon: 'ti ti-text-caption',
 		action: () => { describe(file); },
-	}, ...isImage ? [{
-		text: i18n.ts.cropImage,
-		icon: 'ti ti-crop',
-		action: () : void => { crop(file); },
-	}] : [], {
+	});
+
+	if (isImage) {
+		menuItems.push({
+			text: i18n.ts.cropImage,
+			icon: 'ti ti-crop',
+			action: () : void => { crop(file); },
+		});
+	}
+
+	menuItems.push({
 		type: 'divider',
 	}, {
 		text: i18n.ts.attachCancel,
@@ -162,7 +180,9 @@ function showFileMenu(file: Misskey.entities.DriveFile, ev: MouseEvent): void {
 		icon: 'ti ti-trash',
 		danger: true,
 		action: () => { detachAndDeleteMedia(file); },
-	}], ev.currentTarget ?? ev.target).then(() => menuShowing = false);
+	});
+
+	os.popupMenu(menuItems, ev.currentTarget ?? ev.target).then(() => menuShowing = false);
 	menuShowing = true;
 }
 </script>
@@ -186,13 +206,17 @@ function showFileMenu(file: Misskey.entities.DriveFile, ev: MouseEvent): void {
 	border-radius: 4px;
 	overflow: hidden;
 	cursor: move;
+
+	&:focus-visible {
+		outline-offset: 4px;
+	}
 }
 
 .thumbnail {
 	width: 100%;
 	height: 100%;
 	z-index: 1;
-	color: var(--fg);
+	color: var(--MI_THEME-fg);
 }
 
 .sensitive {
diff --git a/packages/frontend/src/components/MkPostFormDialog.vue b/packages/frontend/src/components/MkPostFormDialog.vue
index ac37cb31bc..32d8df1504 100644
--- a/packages/frontend/src/components/MkPostFormDialog.vue
+++ b/packages/frontend/src/components/MkPostFormDialog.vue
@@ -4,30 +4,18 @@ SPDX-License-Identifier: AGPL-3.0-only
 -->
 
 <template>
-<MkModal ref="modal" :preferType="'dialog'" @click="modal?.close()" @closed="onModalClosed()">
+<MkModal ref="modal" :preferType="'dialog'" @click="modal?.close()" @closed="onModalClosed()" @esc="modal?.close()">
 	<MkPostForm ref="form" :class="$style.form" v-bind="props" autofocus freezeAfterPosted @posted="onPosted" @cancel="modal?.close()" @esc="modal?.close()"/>
 </MkModal>
 </template>
 
 <script lang="ts" setup>
 import { shallowRef } from 'vue';
-import * as Misskey from 'misskey-js';
 import MkModal from '@/components/MkModal.vue';
 import MkPostForm from '@/components/MkPostForm.vue';
+import type { PostFormProps } from '@/types/post-form.js';
 
-const props = withDefaults(defineProps<{
-	reply?: Misskey.entities.Note;
-	renote?: Misskey.entities.Note;
-	channel?: any; // TODO
-	mention?: Misskey.entities.User;
-	specified?: Misskey.entities.UserDetailed;
-	initialText?: string;
-	initialCw?: string;
-	initialVisibility?: (typeof Misskey.noteVisibilities)[number];
-	initialFiles?: Misskey.entities.DriveFile[];
-	initialLocalOnly?: boolean;
-	initialVisibleUsers?: Misskey.entities.UserDetailed[];
-	initialNote?: Misskey.entities.Note;
+const props = withDefaults(defineProps<PostFormProps & {
 	instant?: boolean;
 	fixed?: boolean;
 	autofocus?: boolean;
diff --git a/packages/frontend/src/components/MkPreview.vue b/packages/frontend/src/components/MkPreview.vue
new file mode 100644
index 0000000000..6efd99d14b
--- /dev/null
+++ b/packages/frontend/src/components/MkPreview.vue
@@ -0,0 +1,150 @@
+<!--
+SPDX-FileCopyrightText: syuilo and misskey-project
+SPDX-License-Identifier: AGPL-3.0-only
+-->
+
+<template>
+<div :class="$style.preview">
+	<div>
+		<MkInput v-model="text">
+			<template #label>Text</template>
+		</MkInput>
+		<MkSwitch v-model="flag" :class="$style.preview__content1__switch_button">
+			<span>Switch is now {{ flag ? 'on' : 'off' }}</span>
+		</MkSwitch>
+		<div :class="$style.preview__content1__input">
+			<MkRadio v-model="radio" value="misskey">Misskey</MkRadio>
+			<MkRadio v-model="radio" value="mastodon">Mastodon</MkRadio>
+			<MkRadio v-model="radio" value="pleroma">Pleroma</MkRadio>
+		</div>
+		<div :class="$style.preview__content1__button">
+		<MkButton inline>This is</MkButton>
+		<MkButton inline primary>the button</MkButton>
+		</div>
+	</div>
+	<div :class="$style.preview__content2" style="pointer-events: none;">
+		<Mfm :text="mfm"/>
+	</div>
+	<div :class="$style.preview__content3">
+		<MkButton inline primary @click="openMenu">Open menu</MkButton>
+		<MkButton inline primary @click="openDialog">Open dialog</MkButton>
+		<MkButton inline primary @click="openForm">Open form</MkButton>
+		<MkButton inline primary @click="openDrive">Open drive</MkButton>
+	</div>
+</div>
+</template>
+
+<script lang="ts" setup>
+import { ref } from 'vue';
+import MkButton from '@/components/MkButton.vue';
+import MkInput from '@/components/MkInput.vue';
+import MkSwitch from '@/components/MkSwitch.vue';
+import MkTextarea from '@/components/MkTextarea.vue';
+import MkRadio from '@/components/MkRadio.vue';
+import * as os from '@/os.js';
+import * as config from '@@/js/config.js';
+import { $i } from '@/account.js';
+
+const text = ref('');
+const flag = ref(true);
+const radio = ref('misskey');
+const mfm = ref(`Hello world! This is an @example mention. BTW you are @${$i ? $i.username : 'guest'}.\nAlso, here is ${config.url} and [example link](${config.url}). for more details, see https://example.com.\nAs you know #misskey is open-source software.`);
+
+const openDialog = async () => {
+	await os.alert({
+		type: 'warning',
+		title: 'Oh my Aichan',
+		text: 'Lorem ipsum dolor sit amet, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.',
+	});
+};
+
+const openForm = async () => {
+	await os.form('Example form', {
+		foo: {
+			type: 'boolean',
+			default: true,
+			label: 'This is a boolean property',
+		},
+		bar: {
+			type: 'number',
+			default: 300,
+			label: 'This is a number property',
+		},
+		baz: {
+			type: 'string',
+			default: 'Misskey makes you happy.',
+			label: 'This is a string property',
+		},
+	});
+};
+
+const openDrive = async () => {
+	await os.selectDriveFile(false);
+};
+
+const selectUser = async () => {
+	await os.selectUser();
+};
+
+const openMenu = async (ev: Event) => {
+	os.popupMenu([{
+		type: 'label',
+		text: 'Fruits',
+	}, {
+		text: 'Create some apples',
+		action: () => {},
+	}, {
+		text: 'Read some oranges',
+		action: () => {},
+	}, {
+		text: 'Update some melons',
+		action: () => {},
+	}, {
+		text: 'Delete some bananas',
+		danger: true,
+		action: () => {},
+	}], ev.currentTarget ?? ev.target);
+};
+</script>
+
+<style lang="scss" module>
+.preview {
+	padding: 16px;
+
+	&__content1 {
+
+		&__switch_button {
+			padding: 16px 0 8px 0;
+		}
+
+		&__input {
+			padding: 8px 0 8px 0;
+
+			div {
+				margin: 0 8px 8px 0;
+			}
+		}
+
+		&__button {
+			padding: 4px 0 8px 0;
+
+			button {
+				margin: 0 8px 8px 0;
+			}
+		}
+	}
+
+	&__content2 {
+		padding: 8px 0 8px 0;
+	}
+
+	&__content3 {
+		padding: 8px 0 8px 0;
+
+		button {
+			margin: 0 8px 8px 0;
+
+		}
+	}
+}
+</style>
diff --git a/packages/frontend/src/components/MkPullToRefresh.vue b/packages/frontend/src/components/MkPullToRefresh.vue
index e0d0b561be..4fb4c6fe56 100644
--- a/packages/frontend/src/components/MkPullToRefresh.vue
+++ b/packages/frontend/src/components/MkPullToRefresh.vue
@@ -25,7 +25,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 <script lang="ts" setup>
 import { onMounted, onUnmounted, ref, shallowRef } from 'vue';
 import { i18n } from '@/i18n.js';
-import { getScrollContainer } from '@/scripts/scroll.js';
+import { getScrollContainer } from '@@/js/scroll.js';
 import { isHorizontalSwipeSwiping } from '@/scripts/touch.js';
 
 const SCROLL_STOP = 10;
diff --git a/packages/frontend/src/components/MkRadio.vue b/packages/frontend/src/components/MkRadio.vue
index 6676e3bf5b..f16c8f6c2a 100644
--- a/packages/frontend/src/components/MkRadio.vue
+++ b/packages/frontend/src/components/MkRadio.vue
@@ -9,6 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 	:class="[$style.root, { [$style.disabled]: disabled, [$style.checked]: checked }]"
 	:aria-checked="checked"
 	:aria-disabled="disabled"
+	role="checkbox"
 	@click="toggle"
 >
 	<input
@@ -23,17 +24,17 @@ SPDX-License-Identifier: AGPL-3.0-only
 </div>
 </template>
 
-<script lang="ts" setup>
+<script lang="ts" setup generic="T extends unknown">
 import { computed } from 'vue';
 
 const props = defineProps<{
-	modelValue: any;
-	value: any;
+	modelValue: T;
+	value: T;
 	disabled?: boolean;
 }>();
 
 const emit = defineEmits<{
-	(ev: 'update:modelValue', value: any): void;
+	(ev: 'update:modelValue', value: T): void;
 }>();
 
 const checked = computed(() => props.modelValue === props.value);
@@ -52,9 +53,9 @@ function toggle(): void {
 	cursor: pointer;
 	padding: 7px 10px;
 	min-width: 60px;
-	background-color: var(--panel);
+	background-color: var(--MI_THEME-panel);
 	background-clip: padding-box !important;
-	border: solid 1px var(--panel);
+	border: solid 1px var(--MI_THEME-panel);
 	border-radius: 6px;
 	font-size: 90%;
 	transition: all 0.2s;
@@ -66,20 +67,25 @@ function toggle(): void {
 	}
 
 	&:hover {
-		border-color: var(--inputBorderHover) !important;
+		border-color: var(--MI_THEME-inputBorderHover) !important;
+	}
+
+	&:focus-within {
+		outline: none;
+		box-shadow: 0 0 0 2px var(--MI_THEME-focus);
 	}
 
 	&.checked {
-		background-color: var(--accentedBg) !important;
-		border-color: var(--accentedBg) !important;
-		color: var(--accent);
+		background-color: var(--MI_THEME-accentedBg) !important;
+		border-color: var(--MI_THEME-accentedBg) !important;
+		color: var(--MI_THEME-accent);
 		cursor: default !important;
 
 		> .button {
-			border-color: var(--accent);
+			border-color: var(--MI_THEME-accent);
 
-			&:after {
-				background-color: var(--accent);
+			&::after {
+				background-color: var(--MI_THEME-accent);
 				transform: scale(1);
 				opacity: 1;
 			}
@@ -100,11 +106,11 @@ function toggle(): void {
 	width: 14px;
 	height: 14px;
 	background: none;
-	border: solid 2px var(--inputBorder);
+	border: solid 2px var(--MI_THEME-inputBorder);
 	border-radius: 100%;
 	transition: inherit;
 
-	&:after {
+	&::after {
 		content: '';
 		display: block;
 		position: absolute;
diff --git a/packages/frontend/src/components/MkRadios.vue b/packages/frontend/src/components/MkRadios.vue
index 549438f61b..af81eb814d 100644
--- a/packages/frontend/src/components/MkRadios.vue
+++ b/packages/frontend/src/components/MkRadios.vue
@@ -29,6 +29,9 @@ export default defineComponent({
 		// なぜかFragmentになることがあるため
 		if (options.length === 1 && options[0].props == null) options = options[0].children as VNode[];
 
+		// vnodeのうちv-if=falseなものを除外する(trueになるものはoptionなど他typeになる)
+		options = options.filter(vnode => !(typeof vnode.type === 'symbol' && vnode.type.description === 'v-cmt' && vnode.children === 'v-if'));
+
 		return () => h('div', {
 			class: 'novjtcto',
 		}, [
@@ -40,6 +43,7 @@ export default defineComponent({
 			}, options.map(option => h(MkRadio, {
 				key: option.key as string,
 				value: option.props?.value,
+				disabled: option.props?.disabled,
 				modelValue: value.value,
 				'onUpdate:modelValue': _v => value.value = _v,
 			}, () => option.children)),
@@ -73,7 +77,7 @@ export default defineComponent({
 	> .caption {
 		font-size: 0.85em;
 		padding: 8px 0 0 0;
-		color: var(--fgTransparentWeak);
+		color: var(--MI_THEME-fgTransparentWeak);
 
 		&:empty {
 			display: none;
diff --git a/packages/frontend/src/components/MkRange.vue b/packages/frontend/src/components/MkRange.vue
index 15f8128e98..264b559222 100644
--- a/packages/frontend/src/components/MkRange.vue
+++ b/packages/frontend/src/components/MkRange.vue
@@ -5,7 +5,9 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <template>
 <div class="timctyfi" :class="{ disabled, easing }">
-	<div class="label"><slot name="label"></slot></div>
+	<div class="label">
+		<slot name="label"></slot>
+	</div>
 	<div v-adaptive-border class="body">
 		<div ref="containerEl" class="container">
 			<div class="track">
@@ -14,15 +16,25 @@ SPDX-License-Identifier: AGPL-3.0-only
 			<div v-if="steps && showTicks" class="ticks">
 				<div v-for="i in (steps + 1)" class="tick" :style="{ left: (((i - 1) / steps) * 100) + '%' }"></div>
 			</div>
-			<div ref="thumbEl" v-tooltip="textConverter(finalValue)" class="thumb" :style="{ left: thumbPosition + 'px' }" @mousedown="onMousedown" @touchstart="onMousedown"></div>
+			<div
+				ref="thumbEl"
+				class="thumb"
+				:style="{ left: thumbPosition + 'px' }"
+				@mouseenter.passive="onMouseenter"
+				@mousedown="onMousedown"
+				@touchstart="onMousedown"
+			></div>
 		</div>
 	</div>
-	<div class="caption"><slot name="caption"></slot></div>
+	<div class="caption">
+		<slot name="caption"></slot>
+	</div>
 </div>
 </template>
 
 <script lang="ts" setup>
-import { computed, defineAsyncComponent, onMounted, onUnmounted, ref, watch, shallowRef } from 'vue';
+import { computed, defineAsyncComponent, onMounted, onUnmounted, ref, shallowRef, watch } from 'vue';
+import { isTouchUsing } from '@/scripts/touch.js';
 import * as os from '@/os.js';
 
 const props = withDefaults(defineProps<{
@@ -101,17 +113,43 @@ const steps = computed(() => {
 	}
 });
 
-const onMousedown = (ev: MouseEvent | TouchEvent) => {
-	ev.preventDefault();
+const tooltipForDragShowing = ref(false);
+const tooltipForHoverShowing = ref(false);
 
-	const tooltipShowing = ref(true);
-	os.popup(defineAsyncComponent(() => import('@/components/MkTooltip.vue')), {
-		showing: tooltipShowing,
+function onMouseenter() {
+	if (isTouchUsing) return;
+
+	tooltipForHoverShowing.value = true;
+
+	const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkTooltip.vue')), {
+		showing: computed(() => tooltipForHoverShowing.value && !tooltipForDragShowing.value),
 		text: computed(() => {
 			return props.textConverter(finalValue.value);
 		}),
 		targetElement: thumbEl,
-	}, {}, 'closed');
+	}, {
+		closed: () => dispose(),
+	});
+
+	thumbEl.value!.addEventListener('mouseleave', () => {
+		tooltipForHoverShowing.value = false;
+	}, { once: true, passive: true });
+}
+
+function onMousedown(ev: MouseEvent | TouchEvent) {
+	ev.preventDefault();
+
+	tooltipForDragShowing.value = true;
+
+	const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkTooltip.vue')), {
+		showing: tooltipForDragShowing,
+		text: computed(() => {
+			return props.textConverter(finalValue.value);
+		}),
+		targetElement: thumbEl,
+	}, {
+		closed: () => dispose(),
+	});
 
 	const style = document.createElement('style');
 	style.appendChild(document.createTextNode('* { cursor: grabbing !important; } body * { pointer-events: none !important; }'));
@@ -135,7 +173,7 @@ const onMousedown = (ev: MouseEvent | TouchEvent) => {
 
 	const onMouseup = () => {
 		document.head.removeChild(style);
-		tooltipShowing.value = false;
+		tooltipForDragShowing.value = false;
 		window.removeEventListener('mousemove', onDrag);
 		window.removeEventListener('touchmove', onDrag);
 		window.removeEventListener('mouseup', onMouseup);
@@ -152,7 +190,7 @@ const onMousedown = (ev: MouseEvent | TouchEvent) => {
 	window.addEventListener('touchmove', onDrag);
 	window.addEventListener('mouseup', onMouseup, { once: true });
 	window.addEventListener('touchend', onMouseup, { once: true });
-};
+}
 </script>
 
 <style lang="scss" scoped>
@@ -174,7 +212,7 @@ const onMousedown = (ev: MouseEvent | TouchEvent) => {
 	> .caption {
 		font-size: 0.85em;
 		padding: 8px 0 0 0;
-		color: var(--fgTransparentWeak);
+		color: var(--MI_THEME-fgTransparentWeak);
 
 		&:empty {
 			display: none;
@@ -186,8 +224,8 @@ const onMousedown = (ev: MouseEvent | TouchEvent) => {
 
 	> .body {
 		padding: 7px 12px;
-		background: var(--panel);
-		border: solid 1px var(--panel);
+		background: var(--MI_THEME-panel);
+		border: solid 1px var(--MI_THEME-panel);
 		border-radius: 6px;
 
 		> .container {
@@ -212,7 +250,7 @@ const onMousedown = (ev: MouseEvent | TouchEvent) => {
 					top: 0;
 					left: 0;
 					height: 100%;
-					background: var(--accent);
+					background: var(--MI_THEME-accent);
 					opacity: 0.5;
 				}
 			}
@@ -234,7 +272,7 @@ const onMousedown = (ev: MouseEvent | TouchEvent) => {
 					width: $tickWidth;
 					height: 3px;
 					margin-left: - math.div($tickWidth, 2);
-					background: var(--divider);
+					background: var(--MI_THEME-divider);
 					border-radius: 999px;
 				}
 			}
@@ -244,11 +282,11 @@ const onMousedown = (ev: MouseEvent | TouchEvent) => {
 				width: $thumbWidth;
 				height: $thumbHeight;
 				cursor: grab;
-				background: var(--accent);
+				background: var(--MI_THEME-accent);
 				border-radius: 999px;
 
 				&:hover {
-					background: var(--accentLighten);
+					background: var(--MI_THEME-accentLighten);
 				}
 			}
 		}
@@ -259,12 +297,12 @@ const onMousedown = (ev: MouseEvent | TouchEvent) => {
 			> .container {
 				> .track {
 					> .highlight {
-						transition: width 0.2s cubic-bezier(0,0,0,1);
+						transition: width 0.2s cubic-bezier(0, 0, 0, 1);
 					}
 				}
 
 				> .thumb {
-					transition: left 0.2s cubic-bezier(0,0,0,1);
+					transition: left 0.2s cubic-bezier(0, 0, 0, 1);
 				}
 			}
 		}
diff --git a/packages/frontend/src/components/MkReactionEffect.vue b/packages/frontend/src/components/MkReactionEffect.vue
index 361e246e9f..5a59a5e055 100644
--- a/packages/frontend/src/components/MkReactionEffect.vue
+++ b/packages/frontend/src/components/MkReactionEffect.vue
@@ -60,7 +60,7 @@ onMounted(() => {
 	right: 0;
 	bottom: 0;
 	margin: auto;
-	color: var(--accent);
+	color: var(--MI_THEME-accent);
 	font-size: 18px;
 	font-weight: bold;
 	transform: translateY(-30px);
diff --git a/packages/frontend/src/components/MkReactionIcon.vue b/packages/frontend/src/components/MkReactionIcon.vue
index 068a2968db..c0cbd8a65d 100644
--- a/packages/frontend/src/components/MkReactionIcon.vue
+++ b/packages/frontend/src/components/MkReactionIcon.vue
@@ -24,11 +24,13 @@ const elRef = shallowRef();
 
 if (props.withTooltip) {
 	useTooltip(elRef, (showing) => {
-		os.popup(defineAsyncComponent(() => import('@/components/MkReactionTooltip.vue')), {
+		const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkReactionTooltip.vue')), {
 			showing,
 			reaction: props.reaction.replace(/^:(\w+):$/, ':$1@.:'),
 			targetElement: elRef.value.$el,
-		}, {}, 'closed');
+		}, {
+			closed: () => dispose(),
+		});
 	});
 }
 </script>
diff --git a/packages/frontend/src/components/MkReactionTooltip.vue b/packages/frontend/src/components/MkReactionTooltip.vue
index 15409a216a..77ca841ad0 100644
--- a/packages/frontend/src/components/MkReactionTooltip.vue
+++ b/packages/frontend/src/components/MkReactionTooltip.vue
@@ -36,6 +36,7 @@ const emit = defineEmits<{
 .icon {
 	display: block;
 	width: 60px;
+	max-height: 60px;
 	font-size: 60px; // unicodeな絵文字についてはwidthが効かないため
 	margin: 0 auto;
 	object-fit: contain;
diff --git a/packages/frontend/src/components/MkReactionsViewer.details.vue b/packages/frontend/src/components/MkReactionsViewer.details.vue
index 8b5e6efdf3..d24e0b15bf 100644
--- a/packages/frontend/src/components/MkReactionsViewer.details.vue
+++ b/packages/frontend/src/components/MkReactionsViewer.details.vue
@@ -23,14 +23,15 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <script lang="ts" setup>
 import { } from 'vue';
+import * as Misskey from 'misskey-js';
+import { getEmojiName } from '@@/js/emojilist.js';
 import MkTooltip from './MkTooltip.vue';
 import MkReactionIcon from '@/components/MkReactionIcon.vue';
-import { getEmojiName } from '@/scripts/emojilist.js';
 
 defineProps<{
 	showing: boolean;
 	reaction: string;
-	users: any[]; // TODO
+	users: Misskey.entities.UserLite[];
 	count: number;
 	targetElement: HTMLElement;
 }>();
@@ -57,12 +58,13 @@ function getReactionName(reaction: string): string {
 	max-width: 100px;
 	padding-right: 10px;
 	text-align: center;
-	border-right: solid 0.5px var(--divider);
+	border-right: solid 0.5px var(--MI_THEME-divider);
 }
 
 .reactionIcon {
 	display: block;
 	width: 60px;
+	max-height: 60px;
 	font-size: 60px; // unicodeな絵文字についてはwidthが効かないため
 	object-fit: contain;
 	margin: 0 auto;
@@ -81,6 +83,7 @@ function getReactionName(reaction: string): string {
 }
 
 .user {
+	display: flex;
 	line-height: 24px;
 	padding-top: 4px;
 	white-space: nowrap;
diff --git a/packages/frontend/src/components/MkReactionsViewer.reaction.vue b/packages/frontend/src/components/MkReactionsViewer.reaction.vue
index c41811febe..b65038aadc 100644
--- a/packages/frontend/src/components/MkReactionsViewer.reaction.vue
+++ b/packages/frontend/src/components/MkReactionsViewer.reaction.vue
@@ -20,6 +20,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 <script lang="ts" setup>
 import { computed, inject, onMounted, shallowRef, watch } from 'vue';
 import * as Misskey from 'misskey-js';
+import { getUnicodeEmoji } from '@@/js/emojilist.js';
 import MkCustomEmojiDetailedDialog from './MkCustomEmojiDetailedDialog.vue';
 import XDetails from '@/components/MkReactionsViewer.details.vue';
 import MkReactionIcon from '@/components/MkReactionIcon.vue';
@@ -34,7 +35,6 @@ import { i18n } from '@/i18n.js';
 import * as sound from '@/scripts/sound.js';
 import { checkReactionPermissions } from '@/scripts/check-reaction-permissions.js';
 import { customEmojisMap } from '@/custom-emojis.js';
-import { getUnicodeEmoji } from '@/scripts/emojilist.js';
 
 const props = defineProps<{
 	reaction: string;
@@ -114,10 +114,12 @@ async function menu(ev) {
 		text: i18n.ts.info,
 		icon: 'ti ti-info-circle',
 		action: async () => {
-			os.popup(MkCustomEmojiDetailedDialog, {
+			const { dispose } = os.popup(MkCustomEmojiDetailedDialog, {
 				emoji: await misskeyApiGet('emoji', {
 					name: props.reaction.replace(/:/g, '').replace(/@\./, ''),
 				}),
+			}, {
+				closed: () => dispose(),
 			});
 		},
 	}], ev.currentTarget ?? ev.target);
@@ -129,7 +131,9 @@ function anime() {
 	const rect = buttonEl.value.getBoundingClientRect();
 	const x = rect.left + 16;
 	const y = rect.top + (buttonEl.value.offsetHeight / 2);
-	os.popup(MkReactionEffect, { reaction: props.reaction, x, y }, {}, 'end');
+	const { dispose } = os.popup(MkReactionEffect, { reaction: props.reaction, x, y }, {
+		end: () => dispose(),
+	});
 }
 
 watch(() => props.count, (newCount, oldCount) => {
@@ -151,13 +155,15 @@ if (!mock) {
 
 		const users = reactions.map(x => x.user);
 
-		os.popup(XDetails, {
+		const { dispose } = os.popup(XDetails, {
 			showing,
 			reaction: props.reaction,
 			users,
 			count: props.count,
 			targetElement: buttonEl.value,
-		}, {}, 'closed');
+		}, {
+			closed: () => dispose(),
+		});
 	}, 100);
 }
 </script>
@@ -174,7 +180,7 @@ if (!mock) {
 	justify-content: center;
 
 	&.canToggle {
-		background: var(--buttonBg);
+		background: var(--MI_THEME-buttonBg);
 
 		&:hover {
 			background: rgba(0, 0, 0, 0.1);
@@ -208,12 +214,12 @@ if (!mock) {
 	}
 
 	&.reacted, &.reacted:hover {
-		background: var(--accentedBg);
-		color: var(--accent);
-		box-shadow: 0 0 0 1px var(--accent) inset;
+		background: var(--MI_THEME-accentedBg);
+		color: var(--MI_THEME-accent);
+		box-shadow: 0 0 0 1px var(--MI_THEME-accent) inset;
 
 		> .count {
-			color: var(--accent);
+			color: var(--MI_THEME-accent);
 		}
 
 		> .icon {
diff --git a/packages/frontend/src/components/MkRemoteCaution.vue b/packages/frontend/src/components/MkRemoteCaution.vue
index f1050d26e6..a56a4b1671 100644
--- a/packages/frontend/src/components/MkRemoteCaution.vue
+++ b/packages/frontend/src/components/MkRemoteCaution.vue
@@ -19,14 +19,14 @@ defineProps<{
 .root {
 	font-size: 0.8em;
 	padding: 16px;
-	background: var(--infoWarnBg);
-	color: var(--infoWarnFg);
-	border-radius: var(--radius);
+	background: var(--MI_THEME-infoWarnBg);
+	color: var(--MI_THEME-infoWarnFg);
+	border-radius: var(--MI-radius);
 	overflow: clip;
 }
 
 .link {
 	margin-left: 4px;
-	color: var(--accent);
+	color: var(--MI_THEME-accent);
 }
 </style>
diff --git a/packages/frontend/src/components/MkRetentionLineChart.vue b/packages/frontend/src/components/MkRetentionLineChart.vue
index c3daa9c9a4..d41793b0fa 100644
--- a/packages/frontend/src/components/MkRetentionLineChart.vue
+++ b/packages/frontend/src/components/MkRetentionLineChart.vue
@@ -44,7 +44,7 @@ onMounted(async () => {
 
 	const vLineColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
 
-	const accent = tinycolor(getComputedStyle(document.documentElement).getPropertyValue('--accent'));
+	const accent = tinycolor(getComputedStyle(document.documentElement).getPropertyValue('--MI_THEME-accent'));
 	const color = accent.toHex();
 
 	if (chartEl.value == null) return;
diff --git a/packages/frontend/src/components/MkRippleEffect.vue b/packages/frontend/src/components/MkRippleEffect.vue
index ee5bb73ebf..2949cf156d 100644
--- a/packages/frontend/src/components/MkRippleEffect.vue
+++ b/packages/frontend/src/components/MkRippleEffect.vue
@@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 <template>
 <div :class="$style.root" :style="{ zIndex, top: `${y - 64}px`, left: `${x - 64}px` }">
 	<svg width="128" height="128" viewBox="0 0 128 128" xmlns="http://www.w3.org/2000/svg">
-		<circle fill="none" cx="64" cy="64" style="stroke: var(--accent);">
+		<circle fill="none" cx="64" cy="64" style="stroke: var(--MI_THEME-accent);">
 			<animate
 				attributeName="r"
 				begin="0s" dur="0.5s"
@@ -27,7 +27,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 			/>
 		</circle>
 		<g fill="none" fill-rule="evenodd">
-			<circle v-for="(particle, i) in particles" :key="i" :fill="particle.color" style="stroke: var(--accent);">
+			<circle v-for="(particle, i) in particles" :key="i" :fill="particle.color" style="stroke: var(--MI_THEME-accent);">
 				<animate
 					attributeName="r"
 					begin="0s" dur="0.8s"
diff --git a/packages/frontend/src/components/MkRolePreview.vue b/packages/frontend/src/components/MkRolePreview.vue
index c1b922198f..3f14c5b5e0 100644
--- a/packages/frontend/src/components/MkRolePreview.vue
+++ b/packages/frontend/src/components/MkRolePreview.vue
@@ -4,25 +4,32 @@ SPDX-License-Identifier: AGPL-3.0-only
 -->
 
 <template>
-<MkA v-adaptive-bg :to="forModeration ? `/admin/roles/${role.id}` : `/roles/${role.id}`" class="_panel" :class="$style.root" tabindex="-1" :style="{ '--color': role.color }">
-	<div :class="$style.title">
-		<span :class="$style.icon">
-			<template v-if="role.iconUrl">
-				<img :class="$style.badge" :src="role.iconUrl"/>
+<MkA :to="forModeration ? `/admin/roles/${role.id}` : `/roles/${role.id}`" :class="$style.root" tabindex="-1" :style="{ '--color': role.color }">
+	<template v-if="forModeration">
+		<i v-if="role.isPublic" class="ti ti-world" :class="$style.icon" style="color: var(--MI_THEME-success)"></i>
+		<i v-else class="ti ti-lock" :class="$style.icon" style="color: var(--MI_THEME-warn)"></i>
+	</template>
+
+	<div v-adaptive-bg class="_panel" :class="$style.body">
+		<div :class="$style.bodyTitle">
+			<span :class="$style.bodyIcon">
+				<template v-if="role.iconUrl">
+					<img :class="$style.bodyBadge" :src="role.iconUrl"/>
+				</template>
+				<template v-else>
+					<i v-if="role.isAdministrator" class="ti ti-crown" style="color: var(--MI_THEME-accent);"></i>
+					<i v-else-if="role.isModerator" class="ti ti-shield" style="color: var(--MI_THEME-accent);"></i>
+					<i v-else class="ti ti-user" style="opacity: 0.7;"></i>
+				</template>
+			</span>
+			<span :class="$style.bodyName">{{ role.name }}</span>
+			<template v-if="detailed">
+				<span v-if="role.target === 'manual'" :class="$style.bodyUsers">{{ role.usersCount }} users</span>
+				<span v-else-if="role.target === 'conditional'" :class="$style.bodyUsers">? users</span>
 			</template>
-			<template v-else>
-				<i v-if="role.isAdministrator" class="ti ti-crown" style="color: var(--accent);"></i>
-				<i v-else-if="role.isModerator" class="ti ti-shield" style="color: var(--accent);"></i>
-				<i v-else class="ti ti-user" style="opacity: 0.7;"></i>
-			</template>
-		</span>
-		<span :class="$style.name">{{ role.name }}</span>
-		<template v-if="detailed">
-			<span v-if="role.target === 'manual'" :class="$style.users">{{ role.usersCount }} users</span>
-			<span v-else-if="role.target === 'conditional'" :class="$style.users">({{ i18n.ts._role.conditional }})</span>
-		</template>
+		</div>
+		<div :class="$style.bodyDescription">{{ role.description }}</div>
 	</div>
-	<div :class="$style.description">{{ role.description }}</div>
 </MkA>
 </template>
 
@@ -42,34 +49,44 @@ const props = withDefaults(defineProps<{
 
 <style lang="scss" module>
 .root {
-	display: block;
-	padding: 16px 20px;
-	border-left: solid 6px var(--color);
-}
-
-.title {
 	display: flex;
+	align-items: center;
 }
 
 .icon {
+	margin: 0 12px;
+}
+
+.body {
+	display: block;
+	padding: 16px 20px;
+	flex: 1;
+	border-left: solid 6px var(--color);
+}
+
+.bodyTitle {
+	display: flex;
+}
+
+.bodyIcon {
 	margin-right: 8px;
 }
 
-.badge {
+.bodyBadge {
 	height: 1.3em;
 	vertical-align: -20%;
 }
 
-.name {
+.bodyName {
 	font-weight: bold;
 }
 
-.users {
+.bodyUsers {
 	margin-left: auto;
 	opacity: 0.7;
 }
 
-.description {
+.bodyDescription {
 	opacity: 0.7;
 	font-size: 85%;
 }
diff --git a/packages/frontend/src/components/MkSelect.vue b/packages/frontend/src/components/MkSelect.vue
index 358d9b1f4b..eeadd49936 100644
--- a/packages/frontend/src/components/MkSelect.vue
+++ b/packages/frontend/src/components/MkSelect.vue
@@ -6,66 +6,67 @@ SPDX-License-Identifier: AGPL-3.0-only
 <template>
 <div>
 	<div :class="$style.label" @click="focus"><slot name="label"></slot></div>
-	<div ref="container" :class="[$style.input, { [$style.inline]: inline, [$style.disabled]: disabled, [$style.focused]: focused }]" @mousedown.prevent="show">
+	<div
+		ref="container"
+		tabindex="0"
+		:class="[$style.input, { [$style.inline]: inline, [$style.disabled]: disabled, [$style.focused]: focused || opening }]"
+		@focus="focused = true"
+		@blur="focused = false"
+		@mousedown.prevent="show"
+		@keydown.space.enter="show"
+	>
 		<div ref="prefixEl" :class="$style.prefix"><slot name="prefix"></slot></div>
-		<select
+		<div
 			ref="inputEl"
-			v-model="v"
 			v-adaptive-border
+			tabindex="-1"
 			:class="$style.inputCore"
 			:disabled="disabled"
 			:required="required"
 			:readonly="readonly"
 			:placeholder="placeholder"
-			@focus="focused = true"
-			@blur="focused = false"
-			@input="onInput"
+			@mousedown.prevent="() => {}"
+			@keydown.prevent="() => {}"
 		>
-			<slot></slot>
-		</select>
+			<div style="pointer-events: none;">{{ currentValueText ?? '' }}</div>
+			<div style="display: none;">
+				<slot></slot>
+			</div>
+		</div>
 		<div ref="suffixEl" :class="$style.suffix"><i class="ti ti-chevron-down" :class="[$style.chevron, { [$style.chevronOpening]: opening }]"></i></div>
 	</div>
 	<div :class="$style.caption"><slot name="caption"></slot></div>
-
-	<MkButton v-if="manualSave && changed" primary :class="$style.save" @click="updated"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton>
 </div>
 </template>
 
 <script lang="ts" setup>
 import { onMounted, nextTick, ref, watch, computed, toRefs, VNode, useSlots, VNodeChild } from 'vue';
-import MkButton from '@/components/MkButton.vue';
+import { useInterval } from '@@/js/use-interval.js';
+import type { MenuItem } from '@/types/menu.js';
 import * as os from '@/os.js';
-import { useInterval } from '@/scripts/use-interval.js';
-import { i18n } from '@/i18n.js';
-import { MenuItem } from '@/types/menu.js';
 
 const props = defineProps<{
-	modelValue: string | null;
+	modelValue: string | number | null;
 	required?: boolean;
 	readonly?: boolean;
 	disabled?: boolean;
 	placeholder?: string;
 	autofocus?: boolean;
 	inline?: boolean;
-	manualSave?: boolean;
 	small?: boolean;
 	large?: boolean;
 }>();
 
 const emit = defineEmits<{
-	(ev: 'changeByUser', value: string | null): void;
-	(ev: 'update:modelValue', value: string | null): void;
+	(ev: 'update:modelValue', value: string | number | null): void;
 }>();
 
 const slots = useSlots();
 
 const { modelValue, autofocus } = toRefs(props);
-const v = ref(modelValue.value);
 const focused = ref(false);
 const opening = ref(false);
-const changed = ref(false);
-const invalid = ref(false);
-const filled = computed(() => v.value !== '' && v.value != null);
+const currentValueText = ref<string | null>(null);
 const inputEl = ref<HTMLObjectElement | null>(null);
 const prefixEl = ref<HTMLElement | null>(null);
 const suffixEl = ref<HTMLElement | null>(null);
@@ -75,27 +76,7 @@ const height =
 	props.large ? 39 :
 	36;
 
-const focus = () => inputEl.value?.focus();
-const onInput = (ev) => {
-	changed.value = true;
-};
-
-const updated = () => {
-	changed.value = false;
-	emit('update:modelValue', v.value);
-};
-
-watch(modelValue, newValue => {
-	v.value = newValue;
-});
-
-watch(v, () => {
-	if (!props.manualSave) {
-		updated();
-	}
-
-	invalid.value = inputEl.value?.validity.badInput ?? true;
-});
+const focus = () => container.value?.focus();
 
 // このコンポーネントが作成された時、非表示状態である場合がある
 // 非表示状態だと要素の幅などは0になってしまうので、定期的に計算する
@@ -125,8 +106,35 @@ onMounted(() => {
 	});
 });
 
+watch(modelValue, () => {
+	const scanOptions = (options: VNodeChild[]) => {
+		for (const vnode of options) {
+			if (typeof vnode !== 'object' || vnode === null || Array.isArray(vnode)) continue;
+			if (vnode.type === 'optgroup') {
+				const optgroup = vnode;
+				if (Array.isArray(optgroup.children)) scanOptions(optgroup.children);
+			} else if (Array.isArray(vnode.children)) { // 何故かフラグメントになってくることがある
+				const fragment = vnode;
+				if (Array.isArray(fragment.children)) scanOptions(fragment.children);
+			} else if (vnode.props == null) { // v-if で条件が false のときにこうなる
+				// nop?
+			} else {
+				const option = vnode;
+				if (option.props?.value === modelValue.value) {
+					currentValueText.value = option.children as string;
+					break;
+				}
+			}
+		}
+	};
+
+	scanOptions(slots.default!());
+}, { immediate: true });
+
 function show() {
-	focused.value = true;
+	if (opening.value) return;
+	focus();
+
 	opening.value = true;
 
 	const menu: MenuItem[] = [];
@@ -135,11 +143,9 @@ function show() {
 	const pushOption = (option: VNode) => {
 		menu.push({
 			text: option.children as string,
-			active: computed(() => v.value === option.props?.value),
+			active: computed(() => modelValue.value === option.props?.value),
 			action: () => {
-				v.value = option.props?.value;
-				changed.value = true;
-				emit('changeByUser', v.value);
+				emit('update:modelValue', option.props?.value);
 			},
 		});
 	};
@@ -173,8 +179,6 @@ function show() {
 		onClosing: () => {
 			opening.value = false;
 		},
-	}).then(() => {
-		focused.value = false;
 	});
 }
 </script>
@@ -193,7 +197,7 @@ function show() {
 .caption {
 	font-size: 0.85em;
 	padding: 8px 0 0 0;
-	color: var(--fgTransparentWeak);
+	color: var(--MI_THEME-fgTransparentWeak);
 
 	&:empty {
 		display: none;
@@ -211,8 +215,8 @@ function show() {
 
 	&.focused {
 		> .inputCore {
-			border-color: var(--accent) !important;
-			//box-shadow: 0 0 0 4px var(--focus);
+			border-color: var(--MI_THEME-accent) !important;
+			//box-shadow: 0 0 0 4px var(--MI_THEME-focus);
 		}
 	}
 
@@ -225,9 +229,13 @@ function show() {
 		}
 	}
 
+	&:focus {
+		outline: none;
+	}
+
 	&:hover {
 		> .inputCore {
-			border-color: var(--inputBorderHover) !important;
+			border-color: var(--MI_THEME-inputBorderHover) !important;
 		}
 	}
 }
@@ -235,7 +243,8 @@ function show() {
 .inputCore {
 	appearance: none;
 	-webkit-appearance: none;
-	display: block;
+	display: flex;
+	align-items: center;
 	height: v-bind("height + 'px'");
 	width: 100%;
 	margin: 0;
@@ -243,9 +252,9 @@ function show() {
 	font: inherit;
 	font-weight: normal;
 	font-size: 1em;
-	color: var(--fg);
-	background: var(--panel);
-	border: solid 1px var(--panel);
+	color: var(--MI_THEME-fg);
+	background: var(--MI_THEME-panel);
+	border: solid 1px var(--MI_THEME-panel);
 	border-radius: 6px;
 	outline: none;
 	box-shadow: none;
diff --git a/packages/frontend/src/components/MkSignin.input.vue b/packages/frontend/src/components/MkSignin.input.vue
new file mode 100644
index 0000000000..34c22abc31
--- /dev/null
+++ b/packages/frontend/src/components/MkSignin.input.vue
@@ -0,0 +1,206 @@
+<!--
+SPDX-FileCopyrightText: syuilo and misskey-project
+SPDX-License-Identifier: AGPL-3.0-only
+-->
+
+<template>
+<div :class="$style.wrapper" data-cy-signin-page-input>
+	<div :class="$style.root">
+		<div :class="$style.avatar">
+			<i class="ti ti-user"></i>
+		</div>
+
+		<!-- ログイン画面メッセージ -->
+		<MkInfo v-if="message">
+			{{ message }}
+		</MkInfo>
+
+		<!-- 外部サーバーへの転送 -->
+		<div v-if="openOnRemote" class="_gaps_m">
+			<div class="_gaps_s">
+				<MkButton type="button" rounded primary style="margin: 0 auto;" @click="openRemote(openOnRemote)">
+					{{ i18n.ts.continueOnRemote }} <i class="ti ti-external-link"></i>
+				</MkButton>
+				<button type="button" class="_button" :class="$style.instanceManualSelectButton" @click="specifyHostAndOpenRemote(openOnRemote)">
+					{{ i18n.ts.specifyServerHost }}
+				</button>
+			</div>
+			<div :class="$style.orHr">
+				<p :class="$style.orMsg">{{ i18n.ts.or }}</p>
+			</div>
+		</div>
+
+		<!-- username入力 -->
+		<form class="_gaps_s" @submit.prevent="emit('usernameSubmitted', username)">
+			<MkInput v-model="username" :placeholder="i18n.ts.username" type="text" pattern="^[a-zA-Z0-9_]+$" :spellcheck="false" autocomplete="username webauthn" autofocus required data-cy-signin-username>
+				<template #prefix>@</template>
+				<template #suffix>@{{ host }}</template>
+			</MkInput>
+			<MkButton type="submit" large primary rounded style="margin: 0 auto;" data-cy-signin-page-input-continue>{{ i18n.ts.continue }} <i class="ti ti-arrow-right"></i></MkButton>
+		</form>
+
+		<!-- パスワードレスログイン -->
+		<div :class="$style.orHr">
+			<p :class="$style.orMsg">{{ i18n.ts.or }}</p>
+		</div>
+		<div>
+			<MkButton type="submit" style="margin: auto auto;" large rounded primary gradate @click="emit('passkeyClick', $event)">
+				<i class="ti ti-device-usb" style="font-size: medium;"></i>{{ i18n.ts.signinWithPasskey }}
+			</MkButton>
+		</div>
+	</div>
+</div>
+</template>
+
+<script setup lang="ts">
+import { ref } from 'vue';
+import { toUnicode } from 'punycode/';
+
+import { query, extractDomain } from '@@/js/url.js';
+import { host as configHost } from '@@/js/config.js';
+import type { OpenOnRemoteOptions } from '@/scripts/please-login.js';
+import { i18n } from '@/i18n.js';
+import * as os from '@/os.js';
+
+import MkButton from '@/components/MkButton.vue';
+import MkInput from '@/components/MkInput.vue';
+import MkInfo from '@/components/MkInfo.vue';
+
+const props = withDefaults(defineProps<{
+	message?: string,
+	openOnRemote?: OpenOnRemoteOptions,
+}>(), {
+	message: '',
+	openOnRemote: undefined,
+});
+
+const emit = defineEmits<{
+	(ev: 'usernameSubmitted', v: string): void;
+	(ev: 'passkeyClick', v: MouseEvent): void;
+}>();
+
+const host = toUnicode(configHost);
+
+const username = ref('');
+
+//#region Open on remote
+function openRemote(options: OpenOnRemoteOptions, targetHost?: string): void {
+	switch (options.type) {
+		case 'web':
+		case 'lookup': {
+			let _path: string;
+
+			if (options.type === 'lookup') {
+				// TODO: v2024.7.0以降が浸透してきたら正式なURLに変更する▼
+				// _path = `/lookup?uri=${encodeURIComponent(_path)}`;
+				_path = `/authorize-follow?acct=${encodeURIComponent(options.url)}`;
+			} else {
+				_path = options.path;
+			}
+
+			if (targetHost) {
+				window.open(`https://${targetHost}${_path}`, '_blank', 'noopener');
+			} else {
+				window.open(`https://misskey-hub.net/mi-web/?path=${encodeURIComponent(_path)}`, '_blank', 'noopener');
+			}
+			break;
+		}
+		case 'share': {
+			const params = query(options.params);
+			if (targetHost) {
+				window.open(`https://${targetHost}/share?${params}`, '_blank', 'noopener');
+			} else {
+				window.open(`https://misskey-hub.net/share/?${params}`, '_blank', 'noopener');
+			}
+			break;
+		}
+	}
+}
+
+async function specifyHostAndOpenRemote(options: OpenOnRemoteOptions): Promise<void> {
+	const { canceled, result: hostTemp } = await os.inputText({
+		title: i18n.ts.inputHostName,
+		placeholder: 'misskey.example.com',
+	});
+
+	if (canceled) return;
+
+	let targetHost: string | null = hostTemp;
+
+	// ドメイン部分だけを取り出す
+	targetHost = extractDomain(targetHost ?? '');
+	if (targetHost == null) {
+		os.alert({
+			type: 'error',
+			title: i18n.ts.invalidValue,
+			text: i18n.ts.tryAgain,
+		});
+		return;
+	}
+	openRemote(options, targetHost);
+}
+//#endregion
+</script>
+
+<style lang="scss" module>
+.root {
+	display: flex;
+	flex-direction: column;
+	gap: 20px;
+}
+
+.wrapper {
+	display: flex;
+	align-items: center;
+	width: 100%;
+	min-height: 336px;
+
+	> .root {
+		width: 100%;
+	}
+}
+
+.avatar {
+	margin: 0 auto;
+	background-color: color-mix(in srgb, var(--MI_THEME-fg), transparent 85%);
+	color: color-mix(in srgb, var(--MI_THEME-fg), transparent 25%);
+	text-align: center;
+	height: 64px;
+	width: 64px;
+	font-size: 24px;
+	line-height: 64px;
+	border-radius: 50%;
+}
+
+.instanceManualSelectButton {
+	display: block;
+	text-align: center;
+	opacity: .7;
+	font-size: .8em;
+
+	&:hover {
+		text-decoration: underline;
+	}
+}
+
+.orHr {
+	position: relative;
+	margin: .4em auto;
+	width: 100%;
+	height: 1px;
+	background: var(--MI_THEME-divider);
+}
+
+.orMsg {
+	position: absolute;
+	top: -.6em;
+	display: inline-block;
+	padding: 0 1em;
+	background: var(--MI_THEME-panel);
+	font-size: 0.8em;
+	color: var(--MI_THEME-fgOnPanel);
+	margin: 0;
+	left: 50%;
+	transform: translateX(-50%);
+}
+</style>
diff --git a/packages/frontend/src/components/MkSignin.passkey.vue b/packages/frontend/src/components/MkSignin.passkey.vue
new file mode 100644
index 0000000000..e5a56ab66d
--- /dev/null
+++ b/packages/frontend/src/components/MkSignin.passkey.vue
@@ -0,0 +1,92 @@
+<!--
+SPDX-FileCopyrightText: syuilo and misskey-project
+SPDX-License-Identifier: AGPL-3.0-only
+-->
+
+<template>
+<div :class="$style.wrapper">
+	<div class="_gaps" :class="$style.root">
+		<div class="_gaps_s">
+			<div :class="$style.passkeyIcon">
+				<i class="ti ti-fingerprint"></i>
+			</div>
+			<div :class="$style.passkeyDescription">{{ i18n.ts.useSecurityKey }}</div>
+		</div>
+
+		<MkButton large primary rounded :disabled="queryingKey" style="margin: 0 auto;" @click="queryKey">{{ i18n.ts.retry }}</MkButton>
+
+		<MkButton v-if="isPerformingPasswordlessLogin !== true" transparent rounded :disabled="queryingKey" style="margin: 0 auto;" @click="emit('useTotp')">{{ i18n.ts.useTotp }}</MkButton>
+	</div>
+</div>
+</template>
+
+<script setup lang="ts">
+import { ref, onMounted } from 'vue';
+import { get as webAuthnRequest } from '@github/webauthn-json/browser-ponyfill';
+
+import { i18n } from '@/i18n.js';
+
+import MkButton from '@/components/MkButton.vue';
+
+import type { AuthenticationPublicKeyCredential } from '@github/webauthn-json/browser-ponyfill';
+
+const props = defineProps<{
+	credentialRequest: CredentialRequestOptions;
+	isPerformingPasswordlessLogin?: boolean;
+}>();
+
+const emit = defineEmits<{
+	(ev: 'done', credential: AuthenticationPublicKeyCredential): void;
+	(ev: 'useTotp'): void;
+}>();
+
+const queryingKey = ref(true);
+
+async function queryKey() {
+	queryingKey.value = true;
+	await webAuthnRequest(props.credentialRequest)
+		.catch(() => {
+			return Promise.reject(null);
+		})
+		.then((credential) => {
+			emit('done', credential);
+		})
+		.finally(() => {
+			queryingKey.value = false;
+		});
+}
+
+onMounted(() => {
+	queryKey();
+});
+</script>
+
+<style lang="scss" module>
+.wrapper {
+	display: flex;
+	align-items: center;
+	width: 100%;
+	min-height: 336px;
+
+	> .root {
+		width: 100%;
+	}
+}
+
+.passkeyIcon {
+	margin: 0 auto;
+	background-color: var(--MI_THEME-accentedBg);
+	color: var(--MI_THEME-accent);
+	text-align: center;
+	height: 64px;
+	width: 64px;
+	font-size: 24px;
+	line-height: 64px;
+	border-radius: 50%;
+}
+
+.passkeyDescription {
+	text-align: center;
+	font-size: 1.1em;
+}
+</style>
diff --git a/packages/frontend/src/components/MkSignin.password.vue b/packages/frontend/src/components/MkSignin.password.vue
new file mode 100644
index 0000000000..cd003a39df
--- /dev/null
+++ b/packages/frontend/src/components/MkSignin.password.vue
@@ -0,0 +1,188 @@
+<!--
+SPDX-FileCopyrightText: syuilo and misskey-project
+SPDX-License-Identifier: AGPL-3.0-only
+-->
+
+<template>
+<div :class="$style.wrapper" data-cy-signin-page-password>
+	<div class="_gaps" :class="$style.root">
+		<div :class="$style.avatar" :style="{ backgroundImage: user ? `url('${user.avatarUrl}')` : undefined }"></div>
+		<div :class="$style.welcomeBackMessage">
+			<I18n :src="i18n.ts.welcomeBackWithName" tag="span">
+				<template #name><Mfm :text="user.name ?? user.username" :plain="true"/></template>
+			</I18n>
+		</div>
+
+		<!-- password入力 -->
+		<form class="_gaps_s" @submit.prevent="onSubmit">
+			<!-- ブラウザ オートコンプリート用 -->
+			<input type="hidden" name="username" autocomplete="username" :value="user.username">
+
+			<MkInput v-model="password" :placeholder="i18n.ts.password" type="password" autocomplete="current-password webauthn" :withPasswordToggle="true" required autofocus data-cy-signin-password>
+				<template #prefix><i class="ti ti-lock"></i></template>
+				<template #caption><button class="_textButton" type="button" @click="resetPassword">{{ i18n.ts.forgotPassword }}</button></template>
+			</MkInput>
+
+			<div v-if="needCaptcha">
+				<MkCaptcha v-if="instance.enableHcaptcha" ref="hcaptcha" v-model="hCaptchaResponse" provider="hcaptcha" :sitekey="instance.hcaptchaSiteKey"/>
+				<MkCaptcha v-if="instance.enableMcaptcha" ref="mcaptcha" v-model="mCaptchaResponse" provider="mcaptcha" :sitekey="instance.mcaptchaSiteKey" :instanceUrl="instance.mcaptchaInstanceUrl"/>
+				<MkCaptcha v-if="instance.enableRecaptcha" ref="recaptcha" v-model="reCaptchaResponse" provider="recaptcha" :sitekey="instance.recaptchaSiteKey"/>
+				<MkCaptcha v-if="instance.enableTurnstile" ref="turnstile" v-model="turnstileResponse" provider="turnstile" :sitekey="instance.turnstileSiteKey"/>
+				<MkCaptcha v-if="instance.enableTestcaptcha" ref="testcaptcha" v-model="testcaptchaResponse" provider="testcaptcha"/>
+			</div>
+
+			<MkButton type="submit" :disabled="needCaptcha && captchaFailed" large primary rounded style="margin: 0 auto;" data-cy-signin-page-password-continue>{{ i18n.ts.continue }} <i class="ti ti-arrow-right"></i></MkButton>
+		</form>
+	</div>
+</div>
+</template>
+
+<script lang="ts">
+export type PwResponse = {
+	password: string;
+	captcha: {
+		hCaptchaResponse: string | null;
+		mCaptchaResponse: string | null;
+		reCaptchaResponse: string | null;
+		turnstileResponse: string | null;
+		testcaptchaResponse: string | null;
+	};
+};
+</script>
+
+<script setup lang="ts">
+import { ref, computed, useTemplateRef, defineAsyncComponent } from 'vue';
+import * as Misskey from 'misskey-js';
+
+import { instance } from '@/instance.js';
+import { i18n } from '@/i18n.js';
+import * as os from '@/os.js';
+
+import MkButton from '@/components/MkButton.vue';
+import MkInput from '@/components/MkInput.vue';
+import MkCaptcha from '@/components/MkCaptcha.vue';
+
+const props = defineProps<{
+	user: Misskey.entities.UserDetailed;
+	needCaptcha: boolean;
+}>();
+
+const emit = defineEmits<{
+	(ev: 'passwordSubmitted', v: PwResponse): void;
+}>();
+
+const password = ref('');
+
+const hCaptcha = useTemplateRef('hcaptcha');
+const mCaptcha = useTemplateRef('mcaptcha');
+const reCaptcha = useTemplateRef('recaptcha');
+const turnstile = useTemplateRef('turnstile');
+const testcaptcha = useTemplateRef('testcaptcha');
+
+const hCaptchaResponse = ref<string | null>(null);
+const mCaptchaResponse = ref<string | null>(null);
+const reCaptchaResponse = ref<string | null>(null);
+const turnstileResponse = ref<string | null>(null);
+const testcaptchaResponse = ref<string | null>(null);
+
+const captchaFailed = computed((): boolean => {
+	return (
+		(instance.enableHcaptcha && !hCaptchaResponse.value) ||
+		(instance.enableMcaptcha && !mCaptchaResponse.value) ||
+		(instance.enableRecaptcha && !reCaptchaResponse.value) ||
+		(instance.enableTurnstile && !turnstileResponse.value) ||
+		(instance.enableTestcaptcha && !testcaptchaResponse.value)
+	);
+});
+
+function resetPassword(): void {
+	const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkForgotPassword.vue')), {}, {
+		closed: () => dispose(),
+	});
+}
+
+function onSubmit() {
+	emit('passwordSubmitted', {
+		password: password.value,
+		captcha: {
+			hCaptchaResponse: hCaptchaResponse.value,
+			mCaptchaResponse: mCaptchaResponse.value,
+			reCaptchaResponse: reCaptchaResponse.value,
+			turnstileResponse: turnstileResponse.value,
+			testcaptchaResponse: testcaptchaResponse.value,
+		},
+	});
+}
+
+function resetCaptcha() {
+	hCaptcha.value?.reset();
+	mCaptcha.value?.reset();
+	reCaptcha.value?.reset();
+	turnstile.value?.reset();
+	testcaptcha.value?.reset();
+}
+
+defineExpose({
+	resetCaptcha,
+});
+</script>
+
+<style lang="scss" module>
+.wrapper {
+	display: flex;
+	align-items: center;
+	width: 100%;
+	min-height: 336px;
+
+	> .root {
+		width: 100%;
+	}
+}
+
+.avatar {
+	margin: 0 auto 0 auto;
+	width: 64px;
+	height: 64px;
+	background: #ddd;
+	background-position: center;
+	background-size: cover;
+	border-radius: 100%;
+}
+
+.welcomeBackMessage {
+	text-align: center;
+	font-size: 1.1em;
+}
+
+.instanceManualSelectButton {
+	display: block;
+	text-align: center;
+	opacity: .7;
+	font-size: .8em;
+
+	&:hover {
+		text-decoration: underline;
+	}
+}
+
+.orHr {
+	position: relative;
+	margin: .4em auto;
+	width: 100%;
+	height: 1px;
+	background: var(--MI_THEME-divider);
+}
+
+.orMsg {
+	position: absolute;
+	top: -.6em;
+	display: inline-block;
+	padding: 0 1em;
+	background: var(--MI_THEME-panel);
+	font-size: 0.8em;
+	color: var(--MI_THEME-fgOnPanel);
+	margin: 0;
+	left: 50%;
+	transform: translateX(-50%);
+}
+</style>
diff --git a/packages/frontend/src/components/MkSignin.totp.vue b/packages/frontend/src/components/MkSignin.totp.vue
new file mode 100644
index 0000000000..670b8057c2
--- /dev/null
+++ b/packages/frontend/src/components/MkSignin.totp.vue
@@ -0,0 +1,74 @@
+<!--
+SPDX-FileCopyrightText: syuilo and misskey-project
+SPDX-License-Identifier: AGPL-3.0-only
+-->
+
+<template>
+<div :class="$style.wrapper">
+	<div class="_gaps" :class="$style.root">
+		<div class="_gaps_s">
+			<div :class="$style.totpIcon">
+				<i class="ti ti-key"></i>
+			</div>
+			<div :class="$style.totpDescription">{{ i18n.ts['2fa'] }}</div>
+		</div>
+
+		<!-- totp入力 -->
+		<form class="_gaps_s" @submit.prevent="emit('totpSubmitted', token)">
+			<MkInput v-model="token" type="text" :pattern="isBackupCode ? '^[A-Z0-9]{32}$' :'^[0-9]{6}$'" autocomplete="one-time-code" required autofocus :spellcheck="false" :inputmode="isBackupCode ? undefined : 'numeric'">
+				<template #label>{{ i18n.ts.token }} ({{ i18n.ts['2fa'] }})</template>
+				<template #prefix><i v-if="isBackupCode" class="ti ti-key"></i><i v-else class="ti ti-123"></i></template>
+				<template #caption><button class="_textButton" type="button" @click="isBackupCode = !isBackupCode">{{ isBackupCode ? i18n.ts.useTotp : i18n.ts.useBackupCode }}</button></template>
+			</MkInput>
+
+			<MkButton type="submit" large primary rounded style="margin: 0 auto;">{{ i18n.ts.continue }} <i class="ti ti-arrow-right"></i></MkButton>
+		</form>
+	</div>
+</div>
+</template>
+
+<script setup lang="ts">
+import { ref } from 'vue';
+
+import { i18n } from '@/i18n.js';
+
+import MkButton from '@/components/MkButton.vue';
+import MkInput from '@/components/MkInput.vue';
+
+const emit = defineEmits<{
+	(ev: 'totpSubmitted', token: string): void;
+}>();
+
+const token = ref('');
+const isBackupCode = ref(false);
+</script>
+
+<style lang="scss" module>
+.wrapper {
+	display: flex;
+	align-items: center;
+	width: 100%;
+	min-height: 336px;
+
+	> .root {
+		width: 100%;
+	}
+}
+
+.totpIcon {
+	margin: 0 auto;
+	background-color: var(--MI_THEME-accentedBg);
+	color: var(--MI_THEME-accent);
+	text-align: center;
+	height: 64px;
+	width: 64px;
+	font-size: 24px;
+	line-height: 64px;
+	border-radius: 50%;
+}
+
+.totpDescription {
+	text-align: center;
+	font-size: 1.1em;
+}
+</style>
diff --git a/packages/frontend/src/components/MkSignin.vue b/packages/frontend/src/components/MkSignin.vue
index 970aff825d..776ee20e36 100644
--- a/packages/frontend/src/components/MkSignin.vue
+++ b/packages/frontend/src/components/MkSignin.vue
@@ -4,177 +4,288 @@ SPDX-License-Identifier: AGPL-3.0-only
 -->
 
 <template>
-<form :class="{ signing, totpLogin }" @submit.prevent="onSubmit">
-	<div class="_gaps_m">
-		<div v-show="withAvatar" :class="$style.avatar" :style="{ backgroundImage: user ? `url('${ user.avatarUrl }')` : undefined, marginBottom: message ? '1.5em' : undefined }"></div>
-		<MkInfo v-if="message">
-			{{ message }}
-		</MkInfo>
-		<div v-if="!totpLogin" class="normal-signin _gaps_m">
-			<MkInput v-model="username" :placeholder="i18n.ts.username" type="text" pattern="^[a-zA-Z0-9_]+$" :spellcheck="false" autocomplete="username webauthn" autofocus required data-cy-signin-username @update:modelValue="onUsernameChange">
-				<template #prefix>@</template>
-				<template #suffix>@{{ host }}</template>
-			</MkInput>
-			<MkInput v-if="!user || user && !user.usePasswordLessLogin" v-model="password" :placeholder="i18n.ts.password" type="password" autocomplete="current-password webauthn" :withPasswordToggle="true" required data-cy-signin-password>
-				<template #prefix><i class="ti ti-lock"></i></template>
-				<template #caption><button class="_textButton" type="button" @click="resetPassword">{{ i18n.ts.forgotPassword }}</button></template>
-			</MkInput>
-			<MkButton type="submit" large primary rounded :disabled="signing" style="margin: 0 auto;">{{ signing ? i18n.ts.loggingIn : i18n.ts.login }}</MkButton>
-		</div>
-		<div v-if="totpLogin" class="2fa-signin" :class="{ securityKeys: user && user.securityKeys }">
-			<div v-if="user && user.securityKeys" class="twofa-group tap-group">
-				<p>{{ i18n.ts.useSecurityKey }}</p>
-				<MkButton v-if="!queryingKey" @click="queryKey">
-					{{ i18n.ts.retry }}
-				</MkButton>
-			</div>
-			<div v-if="user && user.securityKeys" class="or-hr">
-				<p class="or-msg">{{ i18n.ts.or }}</p>
-			</div>
-			<div class="twofa-group totp-group _gaps">
-				<MkInput v-if="user && user.usePasswordLessLogin" v-model="password" type="password" autocomplete="current-password" :withPasswordToggle="true" required>
-					<template #label>{{ i18n.ts.password }}</template>
-					<template #prefix><i class="ti ti-lock"></i></template>
-				</MkInput>
-				<MkInput v-model="token" type="text" :pattern="isBackupCode ? '^[A-Z0-9]{32}$' :'^[0-9]{6}$'" autocomplete="one-time-code" required :spellcheck="false" :inputmode="isBackupCode ? undefined : 'numeric'">
-					<template #label>{{ i18n.ts.token }} ({{ i18n.ts['2fa'] }})</template>
-					<template #prefix><i v-if="isBackupCode" class="ti ti-key"></i><i v-else class="ti ti-123"></i></template>
-					<template #caption><button class="_textButton" type="button" @click="isBackupCode = !isBackupCode">{{ isBackupCode ? i18n.ts.useTotp : i18n.ts.useBackupCode }}</button></template>
-				</MkInput>
-				<MkButton type="submit" :disabled="signing" large primary rounded style="margin: 0 auto;">{{ signing ? i18n.ts.loggingIn : i18n.ts.login }}</MkButton>
-			</div>
-		</div>
+<div :class="$style.signinRoot">
+	<Transition
+		mode="out-in"
+		:enterActiveClass="$style.transition_enterActive"
+		:leaveActiveClass="$style.transition_leaveActive"
+		:enterFromClass="$style.transition_enterFrom"
+		:leaveToClass="$style.transition_leaveTo"
+
+		:inert="waiting"
+	>
+		<!-- 1. 外部サーバーへの転送・username入力・パスキー -->
+		<XInput
+			v-if="page === 'input'"
+			key="input"
+			:message="message"
+			:openOnRemote="openOnRemote"
+
+			@usernameSubmitted="onUsernameSubmitted"
+			@passkeyClick="onPasskeyLogin"
+		/>
+
+		<!-- 2. パスワード入力 -->
+		<XPassword
+			v-else-if="page === 'password'"
+			key="password"
+			ref="passwordPageEl"
+
+			:user="userInfo!"
+			:needCaptcha="needCaptcha"
+
+			@passwordSubmitted="onPasswordSubmitted"
+		/>
+
+		<!-- 3. ワンタイムパスワード -->
+		<XTotp
+			v-else-if="page === 'totp'"
+			key="totp"
+
+			@totpSubmitted="onTotpSubmitted"
+		/>
+
+		<!-- 4. パスキー -->
+		<XPasskey
+			v-else-if="page === 'passkey'"
+			key="passkey"
+
+			:credentialRequest="credentialRequest!"
+			:isPerformingPasswordlessLogin="doingPasskeyFromInputPage"
+
+			@done="onPasskeyDone"
+			@useTotp="onUseTotp"
+		/>
+	</Transition>
+	<div v-if="waiting" :class="$style.waitingRoot">
+		<MkLoading/>
 	</div>
-</form>
+</div>
 </template>
 
-<script lang="ts" setup>
-import { defineAsyncComponent, ref } from 'vue';
-import { toUnicode } from 'punycode/';
+<script setup lang="ts">
+import { nextTick, onBeforeUnmount, ref, shallowRef, useTemplateRef } from 'vue';
 import * as Misskey from 'misskey-js';
-import { supported as webAuthnSupported, get as webAuthnRequest, parseRequestOptionsFromJSON } from '@github/webauthn-json/browser-ponyfill';
-import { showSuspendedDialog } from '@/scripts/show-suspended-dialog.js';
-import MkButton from '@/components/MkButton.vue';
-import MkInput from '@/components/MkInput.vue';
-import MkInfo from '@/components/MkInfo.vue';
-import { host as configHost } from '@/config.js';
-import * as os from '@/os.js';
+import { supported as webAuthnSupported, parseRequestOptionsFromJSON } from '@github/webauthn-json/browser-ponyfill';
+
+import type { AuthenticationPublicKeyCredential } from '@github/webauthn-json/browser-ponyfill';
+import type { OpenOnRemoteOptions } from '@/scripts/please-login.js';
 import { misskeyApi } from '@/scripts/misskey-api.js';
+import { showSuspendedDialog } from '@/scripts/show-suspended-dialog.js';
 import { login } from '@/account.js';
 import { i18n } from '@/i18n.js';
+import * as os from '@/os.js';
 
-const signing = ref(false);
-const user = ref<Misskey.entities.UserDetailed | null>(null);
-const username = ref('');
-const password = ref('');
-const token = ref('');
-const host = ref(toUnicode(configHost));
-const totpLogin = ref(false);
-const isBackupCode = ref(false);
-const queryingKey = ref(false);
-const credentialRequest = ref<CredentialRequestOptions | null>(null);
+import XInput from '@/components/MkSignin.input.vue';
+import XPassword, { type PwResponse } from '@/components/MkSignin.password.vue';
+import XTotp from '@/components/MkSignin.totp.vue';
+import XPasskey from '@/components/MkSignin.passkey.vue';
 
 const emit = defineEmits<{
-	(ev: 'login', v: any): void;
+	(ev: 'login', v: Misskey.entities.SigninFlowResponse & { finished: true }): void;
 }>();
 
-const props = defineProps({
-	withAvatar: {
-		type: Boolean,
-		required: false,
-		default: true,
-	},
-	autoSet: {
-		type: Boolean,
-		required: false,
-		default: false,
-	},
-	message: {
-		type: String,
-		required: false,
-		default: '',
-	},
+const props = withDefaults(defineProps<{
+	autoSet?: boolean;
+	message?: string,
+	openOnRemote?: OpenOnRemoteOptions,
+}>(), {
+	autoSet: false,
+	message: '',
+	openOnRemote: undefined,
 });
 
-function onUsernameChange(): void {
-	misskeyApi('users/show', {
-		username: username.value,
-	}).then(userResponse => {
-		user.value = userResponse;
-	}, () => {
-		user.value = null;
+const page = ref<'input' | 'password' | 'totp' | 'passkey'>('input');
+const waiting = ref(false);
+
+const passwordPageEl = useTemplateRef('passwordPageEl');
+const needCaptcha = ref(false);
+
+const userInfo = ref<null | Misskey.entities.UserDetailed>(null);
+const password = ref('');
+
+//#region Passkey Passwordless
+const credentialRequest = shallowRef<CredentialRequestOptions | null>(null);
+const passkeyContext = ref('');
+const doingPasskeyFromInputPage = ref(false);
+
+function onPasskeyLogin(): void {
+	if (webAuthnSupported()) {
+		doingPasskeyFromInputPage.value = true;
+		waiting.value = true;
+		misskeyApi('signin-with-passkey', {})
+			.then((res) => {
+				passkeyContext.value = res.context ?? '';
+				credentialRequest.value = parseRequestOptionsFromJSON({
+					publicKey: res.option,
+				});
+
+				page.value = 'passkey';
+				waiting.value = false;
+			})
+			.catch(onSigninApiError);
+	}
+}
+
+function onPasskeyDone(credential: AuthenticationPublicKeyCredential): void {
+	waiting.value = true;
+
+	if (doingPasskeyFromInputPage.value) {
+		misskeyApi('signin-with-passkey', {
+			credential: credential.toJSON(),
+			context: passkeyContext.value,
+		}).then((res) => {
+			if (res.signinResponse == null) {
+				onSigninApiError();
+				return;
+			}
+			emit('login', res.signinResponse);
+		}).catch(onSigninApiError);
+	} else if (userInfo.value != null) {
+		tryLogin({
+			username: userInfo.value.username,
+			password: password.value,
+			credential: credential.toJSON(),
+		});
+	}
+}
+
+function onUseTotp(): void {
+	page.value = 'totp';
+}
+//#endregion
+
+async function onUsernameSubmitted(username: string) {
+	waiting.value = true;
+
+	userInfo.value = await misskeyApi('users/show', {
+		username,
+	}).catch(() => null);
+
+	await tryLogin({
+		username,
 	});
 }
 
-function onLogin(res: any): Promise<void> | void {
-	if (props.autoSet) {
-		return login(res.i);
-	}
-}
+async function onPasswordSubmitted(pw: PwResponse) {
+	waiting.value = true;
+	password.value = pw.password;
 
-async function queryKey(): Promise<void> {
-	if (credentialRequest.value == null) return;
-	queryingKey.value = true;
-	await webAuthnRequest(credentialRequest.value)
-		.catch(() => {
-			queryingKey.value = false;
-			return Promise.reject(null);
-		}).then(credential => {
-			credentialRequest.value = null;
-			queryingKey.value = false;
-			signing.value = true;
-			return misskeyApi('signin', {
-				username: username.value,
-				password: password.value,
-				credential: credential.toJSON(),
-			});
-		}).then(res => {
-			emit('login', res);
-			return onLogin(res);
-		}).catch(err => {
-			if (err === null) return;
-			os.alert({
-				type: 'error',
-				text: i18n.ts.signinFailed,
-			});
-			signing.value = false;
+	if (userInfo.value == null) {
+		await os.alert({
+			type: 'error',
+			title: i18n.ts.noSuchUser,
+			text: i18n.ts.signinFailed,
 		});
-}
-
-function onSubmit(): void {
-	signing.value = true;
-	if (!totpLogin.value && user.value && user.value.twoFactorEnabled) {
-		if (webAuthnSupported() && user.value.securityKeys) {
-			misskeyApi('signin', {
-				username: username.value,
-				password: password.value,
-			}).then(res => {
-				totpLogin.value = true;
-				signing.value = false;
-				credentialRequest.value = parseRequestOptionsFromJSON({
-					publicKey: res,
-				});
-			})
-				.then(() => queryKey())
-				.catch(loginFailed);
-		} else {
-			totpLogin.value = true;
-			signing.value = false;
-		}
+		waiting.value = false;
+		return;
 	} else {
-		misskeyApi('signin', {
-			username: username.value,
-			password: password.value,
-			token: user.value?.twoFactorEnabled ? token.value : undefined,
-		}).then(res => {
-			emit('login', res);
-			onLogin(res);
-		}).catch(loginFailed);
+		await tryLogin({
+			username: userInfo.value.username,
+			password: pw.password,
+			'hcaptcha-response': pw.captcha.hCaptchaResponse,
+			'm-captcha-response': pw.captcha.mCaptchaResponse,
+			'g-recaptcha-response': pw.captcha.reCaptchaResponse,
+			'turnstile-response': pw.captcha.turnstileResponse,
+			'testcaptcha-response': pw.captcha.testcaptchaResponse,
+		});
 	}
 }
 
-function loginFailed(err: any): void {
-	switch (err.id) {
+async function onTotpSubmitted(token: string) {
+	waiting.value = true;
+
+	if (userInfo.value == null) {
+		await os.alert({
+			type: 'error',
+			title: i18n.ts.noSuchUser,
+			text: i18n.ts.signinFailed,
+		});
+		waiting.value = false;
+		return;
+	} else {
+		await tryLogin({
+			username: userInfo.value.username,
+			password: password.value,
+			token,
+		});
+	}
+}
+
+async function tryLogin(req: Partial<Misskey.entities.SigninFlowRequest>): Promise<Misskey.entities.SigninFlowResponse> {
+	const _req = {
+		username: req.username ?? userInfo.value?.username,
+		...req,
+	};
+
+	function assertIsSigninFlowRequest(x: Partial<Misskey.entities.SigninFlowRequest>): x is Misskey.entities.SigninFlowRequest {
+		return x.username != null;
+	}
+
+	if (!assertIsSigninFlowRequest(_req)) {
+		throw new Error('Invalid request');
+	}
+
+	return await misskeyApi('signin-flow', _req).then(async (res) => {
+		if (res.finished) {
+			emit('login', res);
+			await onLoginSucceeded(res);
+		} else {
+			switch (res.next) {
+				case 'captcha': {
+					needCaptcha.value = true;
+					page.value = 'password';
+					break;
+				}
+				case 'password': {
+					needCaptcha.value = false;
+					page.value = 'password';
+					break;
+				}
+				case 'totp': {
+					page.value = 'totp';
+					break;
+				}
+				case 'passkey': {
+					if (webAuthnSupported()) {
+						credentialRequest.value = parseRequestOptionsFromJSON({
+							publicKey: res.authRequest,
+						});
+						page.value = 'passkey';
+					} else {
+						page.value = 'totp';
+					}
+					break;
+				}
+			}
+
+			if (doingPasskeyFromInputPage.value === true) {
+				doingPasskeyFromInputPage.value = false;
+				page.value = 'input';
+				password.value = '';
+			}
+			passwordPageEl.value?.resetCaptcha();
+			nextTick(() => {
+				waiting.value = false;
+			});
+		}
+		return res;
+	}).catch((err) => {
+		onSigninApiError(err);
+		return Promise.reject(err);
+	});
+}
+
+async function onLoginSucceeded(res: Misskey.entities.SigninFlowResponse & { finished: true }) {
+	if (props.autoSet) {
+		await login(res.i);
+	}
+}
+
+function onSigninApiError(err?: any): void {
+	const id = err?.id ?? null;
+
+	switch (id) {
 		case '6cc579cc-885d-43d8-95c2-b8c7fc963280': {
 			os.alert({
 				type: 'error',
@@ -203,6 +314,46 @@ function loginFailed(err: any): void {
 			});
 			break;
 		}
+		case 'cdf1235b-ac71-46d4-a3a6-84ccce48df6f': {
+			os.alert({
+				type: 'error',
+				title: i18n.ts.loginFailed,
+				text: i18n.ts.incorrectTotp,
+			});
+			break;
+		}
+		case '36b96a7d-b547-412d-aeed-2d611cdc8cdc': {
+			os.alert({
+				type: 'error',
+				title: i18n.ts.loginFailed,
+				text: i18n.ts.unknownWebAuthnKey,
+			});
+			break;
+		}
+		case '93b86c4b-72f9-40eb-9815-798928603d1e': {
+			os.alert({
+				type: 'error',
+				title: i18n.ts.loginFailed,
+				text: i18n.ts.passkeyVerificationFailed,
+			});
+			break;
+		}
+		case 'b18c89a7-5b5e-4cec-bb5b-0419f332d430': {
+			os.alert({
+				type: 'error',
+				title: i18n.ts.loginFailed,
+				text: i18n.ts.passkeyVerificationFailed,
+			});
+			break;
+		}
+		case '2d84773e-f7b7-4d0b-8f72-bb69b584c912': {
+			os.alert({
+				type: 'error',
+				title: i18n.ts.loginFailed,
+				text: i18n.ts.passkeyVerificationSucceededButPasswordlessLoginDisabled,
+			});
+			break;
+		}
 		default: {
 			console.error(err);
 			os.alert({
@@ -213,24 +364,55 @@ function loginFailed(err: any): void {
 		}
 	}
 
-	totpLogin.value = false;
-	signing.value = false;
+	if (doingPasskeyFromInputPage.value === true) {
+		doingPasskeyFromInputPage.value = false;
+		page.value = 'input';
+		password.value = '';
+	}
+	passwordPageEl.value?.resetCaptcha();
+	nextTick(() => {
+		waiting.value = false;
+	});
 }
 
-function resetPassword(): void {
-	os.popup(defineAsyncComponent(() => import('@/components/MkForgotPassword.vue')), {}, {
-	}, 'closed');
-}
+onBeforeUnmount(() => {
+	password.value = '';
+	needCaptcha.value = false;
+	userInfo.value = null;
+});
 </script>
 
 <style lang="scss" module>
-.avatar {
-	margin: 0 auto 0 auto;
-	width: 64px;
-	height: 64px;
-	background: #ddd;
-	background-position: center;
-	background-size: cover;
-	border-radius: 100%;
+.transition_enterActive,
+.transition_leaveActive {
+	transition: opacity 0.3s cubic-bezier(0,0,.35,1), transform 0.3s cubic-bezier(0,0,.35,1);
+}
+.transition_enterFrom {
+	opacity: 0;
+	transform: translateX(50px);
+}
+.transition_leaveTo {
+	opacity: 0;
+	transform: translateX(-50px);
+}
+
+.signinRoot {
+	overflow-x: hidden;
+	overflow-x: clip;
+
+	position: relative;
+}
+
+.waitingRoot {
+	position: absolute;
+	top: 0;
+	left: 0;
+	width: 100%;
+	height: 100%;
+	background-color: color-mix(in srgb, var(--MI_THEME-panel), transparent 50%);
+	display: flex;
+	justify-content: center;
+	align-items: center;
+	z-index: 1;
 }
 </style>
diff --git a/packages/frontend/src/components/MkSigninDialog.vue b/packages/frontend/src/components/MkSigninDialog.vue
index 33355bb99e..676a336ec7 100644
--- a/packages/frontend/src/components/MkSigninDialog.vue
+++ b/packages/frontend/src/components/MkSigninDialog.vue
@@ -4,50 +4,104 @@ SPDX-License-Identifier: AGPL-3.0-only
 -->
 
 <template>
-<MkModalWindow
-	ref="dialog"
-	:width="370"
-	:height="400"
-	@close="onClose"
+<MkModal
+	ref="modal"
+	:preferType="'dialog'"
+	@click="onClose"
 	@closed="emit('closed')"
 >
-	<template #header>{{ i18n.ts.login }}</template>
-
-	<MkSpacer :marginMin="20" :marginMax="28">
-		<MkSignin :autoSet="autoSet" :message="message" @login="onLogin"/>
-	</MkSpacer>
-</MkModalWindow>
+	<div :class="$style.root">
+		<div :class="$style.header">
+			<div :class="$style.headerText"><i class="ti ti-login-2"></i> {{ i18n.ts.login }}</div>
+			<button :class="$style.closeButton" class="_button" @click="onClose"><i class="ti ti-x"></i></button>
+		</div>
+		<div :class="$style.content">
+			<MkSignin :autoSet="autoSet" :message="message" :openOnRemote="openOnRemote" @login="onLogin"/>
+		</div>
+	</div>
+</MkModal>
 </template>
 
 <script lang="ts" setup>
+import * as Misskey from 'misskey-js';
 import { shallowRef } from 'vue';
+import type { OpenOnRemoteOptions } from '@/scripts/please-login.js';
 import MkSignin from '@/components/MkSignin.vue';
-import MkModalWindow from '@/components/MkModalWindow.vue';
+import MkModal from '@/components/MkModal.vue';
 import { i18n } from '@/i18n.js';
 
 withDefaults(defineProps<{
 	autoSet?: boolean;
 	message?: string,
+	openOnRemote?: OpenOnRemoteOptions,
 }>(), {
 	autoSet: false,
 	message: '',
+	openOnRemote: undefined,
 });
 
 const emit = defineEmits<{
-	(ev: 'done', v: any): void;
+	(ev: 'done', v: Misskey.entities.SigninFlowResponse & { finished: true }): void;
 	(ev: 'closed'): void;
 	(ev: 'cancelled'): void;
 }>();
 
-const dialog = shallowRef<InstanceType<typeof MkModalWindow>>();
+const modal = shallowRef<InstanceType<typeof MkModal>>();
 
 function onClose() {
 	emit('cancelled');
-	if (dialog.value) dialog.value.close();
+	if (modal.value) modal.value.close();
 }
 
-function onLogin(res) {
+function onLogin(res: Misskey.entities.SigninFlowResponse & { finished: true }) {
 	emit('done', res);
-	if (dialog.value) dialog.value.close();
+	if (modal.value) modal.value.close();
 }
 </script>
+
+<style lang="scss" module>
+.root {
+	overflow: auto;
+	margin: auto;
+	position: relative;
+	width: 100%;
+	max-width: 400px;
+	height: 100%;
+	max-height: 450px;
+	box-sizing: border-box;
+	background: var(--MI_THEME-panel);
+	border-radius: var(--MI-radius);
+}
+
+.header {
+	position: sticky;
+	top: 0;
+	left: 0;
+	width: 100%;
+	height: 50px;
+	box-sizing: border-box;
+	display: flex;
+	align-items: center;
+	font-weight: bold;
+	backdrop-filter: var(--MI-blur, blur(15px));
+	background: var(--MI_THEME-acrylicBg);
+	z-index: 1;
+}
+
+.headerText {
+	padding: 0 20px;
+	box-sizing: border-box;
+}
+
+.closeButton {
+	margin-left: auto;
+	padding: 16px;
+	font-size: 16px;
+	line-height: 16px;
+}
+
+.content {
+	padding: 32px;
+	box-sizing: border-box;
+}
+</style>
diff --git a/packages/frontend/src/components/MkSignupDialog.form.vue b/packages/frontend/src/components/MkSignupDialog.form.vue
index 5f08e416c1..3d1c44fc90 100644
--- a/packages/frontend/src/components/MkSignupDialog.form.vue
+++ b/packages/frontend/src/components/MkSignupDialog.form.vue
@@ -21,12 +21,12 @@ SPDX-License-Identifier: AGPL-3.0-only
 				<template #caption>
 					<div><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.cannotBeChangedLater }}</div>
 					<span v-if="usernameState === 'wait'" style="color:#999"><MkLoading :em="true"/> {{ i18n.ts.checking }}</span>
-					<span v-else-if="usernameState === 'ok'" style="color: var(--success)"><i class="ti ti-check ti-fw"></i> {{ i18n.ts.available }}</span>
-					<span v-else-if="usernameState === 'unavailable'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.unavailable }}</span>
-					<span v-else-if="usernameState === 'error'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.error }}</span>
-					<span v-else-if="usernameState === 'invalid-format'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.usernameInvalidFormat }}</span>
-					<span v-else-if="usernameState === 'min-range'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.tooShort }}</span>
-					<span v-else-if="usernameState === 'max-range'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.tooLong }}</span>
+					<span v-else-if="usernameState === 'ok'" style="color: var(--MI_THEME-success)"><i class="ti ti-check ti-fw"></i> {{ i18n.ts.available }}</span>
+					<span v-else-if="usernameState === 'unavailable'" style="color: var(--MI_THEME-error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.unavailable }}</span>
+					<span v-else-if="usernameState === 'error'" style="color: var(--MI_THEME-error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.error }}</span>
+					<span v-else-if="usernameState === 'invalid-format'" style="color: var(--MI_THEME-error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.usernameInvalidFormat }}</span>
+					<span v-else-if="usernameState === 'min-range'" style="color: var(--MI_THEME-error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.tooShort }}</span>
+					<span v-else-if="usernameState === 'max-range'" style="color: var(--MI_THEME-error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.tooLong }}</span>
 				</template>
 			</MkInput>
 			<MkInput v-if="instance.emailRequiredForSignup" v-model="email" :debounce="true" type="email" :spellcheck="false" required data-cy-signup-email @update:modelValue="onChangeEmail">
@@ -34,38 +34,39 @@ SPDX-License-Identifier: AGPL-3.0-only
 				<template #prefix><i class="ti ti-mail"></i></template>
 				<template #caption>
 					<span v-if="emailState === 'wait'" style="color:#999"><MkLoading :em="true"/> {{ i18n.ts.checking }}</span>
-					<span v-else-if="emailState === 'ok'" style="color: var(--success)"><i class="ti ti-check ti-fw"></i> {{ i18n.ts.available }}</span>
-					<span v-else-if="emailState === 'unavailable:used'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts._emailUnavailable.used }}</span>
-					<span v-else-if="emailState === 'unavailable:format'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts._emailUnavailable.format }}</span>
-					<span v-else-if="emailState === 'unavailable:disposable'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts._emailUnavailable.disposable }}</span>
-					<span v-else-if="emailState === 'unavailable:banned'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts._emailUnavailable.banned }}</span>
-					<span v-else-if="emailState === 'unavailable:mx'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts._emailUnavailable.mx }}</span>
-					<span v-else-if="emailState === 'unavailable:smtp'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts._emailUnavailable.smtp }}</span>
-					<span v-else-if="emailState === 'unavailable'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.unavailable }}</span>
-					<span v-else-if="emailState === 'error'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.error }}</span>
+					<span v-else-if="emailState === 'ok'" style="color: var(--MI_THEME-success)"><i class="ti ti-check ti-fw"></i> {{ i18n.ts.available }}</span>
+					<span v-else-if="emailState === 'unavailable:used'" style="color: var(--MI_THEME-error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts._emailUnavailable.used }}</span>
+					<span v-else-if="emailState === 'unavailable:format'" style="color: var(--MI_THEME-error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts._emailUnavailable.format }}</span>
+					<span v-else-if="emailState === 'unavailable:disposable'" style="color: var(--MI_THEME-error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts._emailUnavailable.disposable }}</span>
+					<span v-else-if="emailState === 'unavailable:banned'" style="color: var(--MI_THEME-error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts._emailUnavailable.banned }}</span>
+					<span v-else-if="emailState === 'unavailable:mx'" style="color: var(--MI_THEME-error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts._emailUnavailable.mx }}</span>
+					<span v-else-if="emailState === 'unavailable:smtp'" style="color: var(--MI_THEME-error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts._emailUnavailable.smtp }}</span>
+					<span v-else-if="emailState === 'unavailable'" style="color: var(--MI_THEME-error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.unavailable }}</span>
+					<span v-else-if="emailState === 'error'" style="color: var(--MI_THEME-error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.error }}</span>
 				</template>
 			</MkInput>
 			<MkInput v-model="password" type="password" autocomplete="new-password" required data-cy-signup-password @update:modelValue="onChangePassword">
 				<template #label>{{ i18n.ts.password }}</template>
 				<template #prefix><i class="ti ti-lock"></i></template>
 				<template #caption>
-					<span v-if="passwordStrength == 'low'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.weakPassword }}</span>
-					<span v-if="passwordStrength == 'medium'" style="color: var(--warn)"><i class="ti ti-check ti-fw"></i> {{ i18n.ts.normalPassword }}</span>
-					<span v-if="passwordStrength == 'high'" style="color: var(--success)"><i class="ti ti-check ti-fw"></i> {{ i18n.ts.strongPassword }}</span>
+					<span v-if="passwordStrength == 'low'" style="color: var(--MI_THEME-error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.weakPassword }}</span>
+					<span v-if="passwordStrength == 'medium'" style="color: var(--MI_THEME-warn)"><i class="ti ti-check ti-fw"></i> {{ i18n.ts.normalPassword }}</span>
+					<span v-if="passwordStrength == 'high'" style="color: var(--MI_THEME-success)"><i class="ti ti-check ti-fw"></i> {{ i18n.ts.strongPassword }}</span>
 				</template>
 			</MkInput>
 			<MkInput v-model="retypedPassword" type="password" autocomplete="new-password" required data-cy-signup-password-retype @update:modelValue="onChangePasswordRetype">
 				<template #label>{{ i18n.ts.password }} ({{ i18n.ts.retype }})</template>
 				<template #prefix><i class="ti ti-lock"></i></template>
 				<template #caption>
-					<span v-if="passwordRetypeState == 'match'" style="color: var(--success)"><i class="ti ti-check ti-fw"></i> {{ i18n.ts.passwordMatched }}</span>
-					<span v-if="passwordRetypeState == 'not-match'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.passwordNotMatched }}</span>
+					<span v-if="passwordRetypeState == 'match'" style="color: var(--MI_THEME-success)"><i class="ti ti-check ti-fw"></i> {{ i18n.ts.passwordMatched }}</span>
+					<span v-if="passwordRetypeState == 'not-match'" style="color: var(--MI_THEME-error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.passwordNotMatched }}</span>
 				</template>
 			</MkInput>
 			<MkCaptcha v-if="instance.enableHcaptcha" ref="hcaptcha" v-model="hCaptchaResponse" :class="$style.captcha" provider="hcaptcha" :sitekey="instance.hcaptchaSiteKey"/>
 			<MkCaptcha v-if="instance.enableMcaptcha" ref="mcaptcha" v-model="mCaptchaResponse" :class="$style.captcha" provider="mcaptcha" :sitekey="instance.mcaptchaSiteKey" :instanceUrl="instance.mcaptchaInstanceUrl"/>
 			<MkCaptcha v-if="instance.enableRecaptcha" ref="recaptcha" v-model="reCaptchaResponse" :class="$style.captcha" provider="recaptcha" :sitekey="instance.recaptchaSiteKey"/>
 			<MkCaptcha v-if="instance.enableTurnstile" ref="turnstile" v-model="turnstileResponse" :class="$style.captcha" provider="turnstile" :sitekey="instance.turnstileSiteKey"/>
+			<MkCaptcha v-if="instance.enableTestcaptcha" ref="testcaptcha" v-model="testcaptchaResponse" :class="$style.captcha" provider="testcaptcha"/>
 			<MkButton type="submit" :disabled="shouldDisableSubmitting" large gradate rounded data-cy-signup-submit style="margin: 0 auto;">
 				<template v-if="submitting">
 					<MkLoading :em="true" :colored="false"/>
@@ -81,10 +82,10 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { ref, computed } from 'vue';
 import { toUnicode } from 'punycode/';
 import * as Misskey from 'misskey-js';
+import * as config from '@@/js/config.js';
 import MkButton from './MkButton.vue';
 import MkInput from './MkInput.vue';
 import MkCaptcha, { type Captcha } from '@/components/MkCaptcha.vue';
-import * as config from '@/config.js';
 import * as os from '@/os.js';
 import { misskeyApi } from '@/scripts/misskey-api.js';
 import { login } from '@/account.js';
@@ -98,15 +99,17 @@ const props = withDefaults(defineProps<{
 });
 
 const emit = defineEmits<{
-	(ev: 'signup', user: Misskey.entities.SigninResponse): void;
+	(ev: 'signup', user: Misskey.entities.SignupResponse): void;
 	(ev: 'signupEmailPending'): void;
 }>();
 
 const host = toUnicode(config.host);
 
 const hcaptcha = ref<Captcha | undefined>();
+const mcaptcha = ref<Captcha | undefined>();
 const recaptcha = ref<Captcha | undefined>();
 const turnstile = ref<Captcha | undefined>();
+const testcaptcha = ref<Captcha | undefined>();
 
 const username = ref<string>('');
 const password = ref<string>('');
@@ -122,6 +125,7 @@ const hCaptchaResponse = ref<string | null>(null);
 const mCaptchaResponse = ref<string | null>(null);
 const reCaptchaResponse = ref<string | null>(null);
 const turnstileResponse = ref<string | null>(null);
+const testcaptchaResponse = ref<string | null>(null);
 const usernameAbortController = ref<null | AbortController>(null);
 const emailAbortController = ref<null | AbortController>(null);
 
@@ -131,6 +135,7 @@ const shouldDisableSubmitting = computed((): boolean => {
 		instance.enableMcaptcha && !mCaptchaResponse.value ||
 		instance.enableRecaptcha && !reCaptchaResponse.value ||
 		instance.enableTurnstile && !turnstileResponse.value ||
+		instance.enableTestcaptcha && !testcaptchaResponse.value ||
 		instance.emailRequiredForSignup && emailState.value !== 'ok' ||
 		usernameState.value !== 'ok' ||
 		passwordRetypeState.value !== 'match';
@@ -249,18 +254,31 @@ async function onSubmit(): Promise<void> {
 	if (submitting.value) return;
 	submitting.value = true;
 
-	try {
-		await misskeyApi('signup', {
-			username: username.value,
-			password: password.value,
-			emailAddress: email.value,
-			invitationCode: invitationCode.value,
-			'hcaptcha-response': hCaptchaResponse.value,
-			'm-captcha-response': mCaptchaResponse.value,
-			'g-recaptcha-response': reCaptchaResponse.value,
-			'turnstile-response': turnstileResponse.value,
-		});
-		if (instance.emailRequiredForSignup) {
+	const signupPayload: Misskey.entities.SignupRequest = {
+		username: username.value,
+		password: password.value,
+		emailAddress: email.value,
+		invitationCode: invitationCode.value,
+		'hcaptcha-response': hCaptchaResponse.value,
+		'm-captcha-response': mCaptchaResponse.value,
+		'g-recaptcha-response': reCaptchaResponse.value,
+		'turnstile-response': turnstileResponse.value,
+		'testcaptcha-response': testcaptchaResponse.value,
+	};
+
+	const res = await fetch(`${config.apiUrl}/signup`, {
+		method: 'POST',
+		headers: {
+			'Content-Type': 'application/json',
+		},
+		body: JSON.stringify(signupPayload),
+	}).catch(() => {
+		onSignupApiError();
+		return null;
+	});
+
+	if (res) {
+		if (res.status === 204 || instance.emailRequiredForSignup) {
 			os.alert({
 				type: 'success',
 				title: i18n.ts._signup.almostThere,
@@ -268,27 +286,32 @@ async function onSubmit(): Promise<void> {
 			});
 			emit('signupEmailPending');
 		} else {
-			const res = await misskeyApi('signin', {
-				username: username.value,
-				password: password.value,
-			});
-			emit('signup', res);
+			const resJson = (await res.json()) as Misskey.entities.SignupResponse;
+			if (_DEV_) console.log(resJson);
+
+			emit('signup', resJson);
 
 			if (props.autoSet) {
-				return login(res.i);
+				await login(resJson.token);
 			}
 		}
-	} catch {
-		submitting.value = false;
-		hcaptcha.value?.reset?.();
-		recaptcha.value?.reset?.();
-		turnstile.value?.reset?.();
-
-		os.alert({
-			type: 'error',
-			text: i18n.ts.somethingHappened,
-		});
 	}
+
+	submitting.value = false;
+}
+
+function onSignupApiError() {
+	submitting.value = false;
+	hcaptcha.value?.reset?.();
+	mcaptcha.value?.reset?.();
+	recaptcha.value?.reset?.();
+	turnstile.value?.reset?.();
+	testcaptcha.value?.reset?.();
+
+	os.alert({
+		type: 'error',
+		text: i18n.ts.somethingHappened,
+	});
 }
 </script>
 
@@ -297,8 +320,8 @@ async function onSubmit(): Promise<void> {
 	padding: 16px;
 	text-align: center;
 	font-size: 26px;
-	background-color: var(--accentedBg);
-	color: var(--accent);
+	background-color: var(--MI_THEME-accentedBg);
+	color: var(--MI_THEME-accent);
 }
 
 .captcha {
diff --git a/packages/frontend/src/components/MkSignupDialog.rules.vue b/packages/frontend/src/components/MkSignupDialog.rules.vue
index 59a3651cd4..e2a06dd91f 100644
--- a/packages/frontend/src/components/MkSignupDialog.rules.vue
+++ b/packages/frontend/src/components/MkSignupDialog.rules.vue
@@ -21,7 +21,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 			<MkFolder v-if="availableServerRules" :defaultOpen="true">
 				<template #label>{{ i18n.ts.serverRules }}</template>
-				<template #suffix><i v-if="agreeServerRules" class="ti ti-check" style="color: var(--success)"></i></template>
+				<template #suffix><i v-if="agreeServerRules" class="ti ti-check" style="color: var(--MI_THEME-success)"></i></template>
 
 				<ol class="_gaps_s" :class="$style.rules">
 					<li v-for="item in instance.serverRules" :class="$style.rule"><div :class="$style.ruleText" v-html="item"></div></li>
@@ -32,7 +32,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 			<MkFolder v-if="availableTos || availablePrivacyPolicy" :defaultOpen="true">
 				<template #label>{{ tosPrivacyPolicyLabel }}</template>
-				<template #suffix><i v-if="agreeTosAndPrivacyPolicy" class="ti ti-check" style="color: var(--success)"></i></template>
+				<template #suffix><i v-if="agreeTosAndPrivacyPolicy" class="ti ti-check" style="color: var(--MI_THEME-success)"></i></template>
 				<div class="_gaps_s">
 					<div v-if="availableTos"><a :href="instance.tosUrl ?? undefined" class="_link" target="_blank">{{ i18n.ts.termsOfService }} <i class="ti ti-external-link"></i></a></div>
 					<div v-if="availablePrivacyPolicy"><a :href="instance.privacyPolicyUrl ?? undefined" class="_link" target="_blank">{{ i18n.ts.privacyPolicy }} <i class="ti ti-external-link"></i></a></div>
@@ -43,7 +43,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 			<MkFolder :defaultOpen="true">
 				<template #label>{{ i18n.ts.basicNotesBeforeCreateAccount }}</template>
-				<template #suffix><i v-if="agreeNote" class="ti ti-check" style="color: var(--success)"></i></template>
+				<template #suffix><i v-if="agreeNote" class="ti ti-check" style="color: var(--MI_THEME-success)"></i></template>
 
 				<a href="https://misskey-hub.net/docs/for-users/onboarding/warning/" class="_link" target="_blank">{{ i18n.ts.basicNotesBeforeCreateAccount }} <i class="ti ti-external-link"></i></a>
 
@@ -150,8 +150,8 @@ async function updateAgreeNote(v: boolean) {
 	padding: 16px;
 	text-align: center;
 	font-size: 26px;
-	background-color: var(--accentedBg);
-	color: var(--accent);
+	background-color: var(--MI_THEME-accentedBg);
+	color: var(--MI_THEME-accent);
 }
 
 .rules {
@@ -170,14 +170,14 @@ async function updateAgreeNote(v: boolean) {
 		flex-shrink: 0;
 		display: flex;
 		position: sticky;
-		top: calc(var(--stickyTop, 0px) + 8px);
+		top: calc(var(--MI-stickyTop, 0px) + 8px);
 		counter-increment: item;
 		content: counter(item);
 		width: 32px;
 		height: 32px;
 		line-height: 32px;
-		background-color: var(--accentedBg);
-		color: var(--accent);
+		background-color: var(--MI_THEME-accentedBg);
+		color: var(--MI_THEME-accent);
 		font-size: 13px;
 		font-weight: bold;
 		align-items: center;
diff --git a/packages/frontend/src/components/MkSignupDialog.vue b/packages/frontend/src/components/MkSignupDialog.vue
index 97310d32a6..6fb9d77837 100644
--- a/packages/frontend/src/components/MkSignupDialog.vue
+++ b/packages/frontend/src/components/MkSignupDialog.vue
@@ -8,8 +8,8 @@ SPDX-License-Identifier: AGPL-3.0-only
 	ref="dialog"
 	:width="500"
 	:height="600"
-	@close="dialog?.close()"
-	@closed="$emit('closed')"
+	@close="onClose"
+	@closed="emit('closed')"
 >
 	<template #header>{{ i18n.ts.signup }}</template>
 
@@ -22,7 +22,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 			:leaveToClass="$style.transition_x_leaveTo"
 		>
 			<template v-if="!isAcceptedServerRule">
-				<XServerRules @done="isAcceptedServerRule = true" @cancel="dialog?.close()"/>
+				<XServerRules @done="isAcceptedServerRule = true" @cancel="onClose"/>
 			</template>
 			<template v-else>
 				<XSignup :autoSet="autoSet" @signup="onSignup" @signupEmailPending="onSignupEmailPending"/>
@@ -47,7 +47,8 @@ const props = withDefaults(defineProps<{
 });
 
 const emit = defineEmits<{
-	(ev: 'done', res: Misskey.entities.SigninResponse): void;
+	(ev: 'done', res: Misskey.entities.SignupResponse): void;
+	(ev: 'cancelled'): void;
 	(ev: 'closed'): void;
 }>();
 
@@ -55,7 +56,12 @@ const dialog = shallowRef<InstanceType<typeof MkModalWindow>>();
 
 const isAcceptedServerRule = ref(false);
 
-function onSignup(res: Misskey.entities.SigninResponse) {
+function onClose() {
+	emit('cancelled');
+	dialog.value?.close();
+}
+
+function onSignup(res: Misskey.entities.SignupResponse) {
 	emit('done', res);
 	dialog.value?.close();
 }
diff --git a/packages/frontend/src/components/MkSourceCodeAvailablePopup.vue b/packages/frontend/src/components/MkSourceCodeAvailablePopup.vue
index 80f3a6709c..4c197ed43e 100644
--- a/packages/frontend/src/components/MkSourceCodeAvailablePopup.vue
+++ b/packages/frontend/src/components/MkSourceCodeAvailablePopup.vue
@@ -41,7 +41,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <script lang="ts" setup>
 import MkButton from '@/components/MkButton.vue';
-import { host } from '@/config.js';
+import { host } from '@@/js/config.js';
 import { i18n } from '@/i18n.js';
 import { instance } from '@/instance.js';
 import { miLocalStorage } from '@/local-storage.js';
@@ -63,12 +63,12 @@ function close() {
 .root {
 	position: fixed;
 	z-index: v-bind(zIndex);
-	bottom: var(--margin);
+	bottom: var(--MI-margin);
 	left: 0;
 	right: 0;
 	margin: auto;
 	box-sizing: border-box;
-	width: calc(100% - (var(--margin) * 2));
+	width: calc(100% - (var(--MI-margin) * 2));
 	max-width: 500px;
 	display: flex;
 }
@@ -77,7 +77,7 @@ function close() {
 	text-align: center;
 	padding-top: 25px;
 	width: 100px;
-	color: var(--accent);
+	color: var(--MI_THEME-accent);
 }
 @media (max-width: 500px) {
 	.icon {
diff --git a/packages/frontend/src/components/MkSubNoteContent.vue b/packages/frontend/src/components/MkSubNoteContent.vue
index 9a07826f1a..9e02884b8c 100644
--- a/packages/frontend/src/components/MkSubNoteContent.vue
+++ b/packages/frontend/src/components/MkSubNoteContent.vue
@@ -35,7 +35,7 @@ import * as Misskey from 'misskey-js';
 import MkMediaList from '@/components/MkMediaList.vue';
 import MkPoll from '@/components/MkPoll.vue';
 import { i18n } from '@/i18n.js';
-import { shouldCollapsed } from '@/scripts/collapsed.js';
+import { shouldCollapsed } from '@@/js/collapsed.js';
 
 const props = defineProps<{
 	note: Misskey.entities.Note;
@@ -62,11 +62,11 @@ const collapsed = ref(isLong);
 			left: 0;
 			width: 100%;
 			height: 64px;
-			background: linear-gradient(0deg, var(--panel), var(--X15));
+			background: linear-gradient(0deg, var(--MI_THEME-panel), color(from var(--MI_THEME-panel) srgb r g b / 0));
 
 			> .fadeLabel {
 				display: inline-block;
-				background: var(--panel);
+				background: var(--MI_THEME-panel);
 				padding: 6px 10px;
 				font-size: 0.8em;
 				border-radius: 999px;
@@ -75,7 +75,7 @@ const collapsed = ref(isLong);
 
 			&:hover {
 				> .fadeLabel {
-					background: var(--panelHighlight);
+					background: var(--MI_THEME-panelHighlight);
 				}
 			}
 		}
@@ -84,25 +84,25 @@ const collapsed = ref(isLong);
 
 .reply {
 	margin-right: 6px;
-	color: var(--accent);
+	color: var(--MI_THEME-accent);
 }
 
 .rp {
 	margin-left: 4px;
 	font-style: oblique;
-	color: var(--renote);
+	color: var(--MI_THEME-renote);
 }
 
 .showLess {
 	width: 100%;
 	margin-top: 14px;
 	position: sticky;
-	bottom: calc(var(--stickyBottom, 0px) + 14px);
+	bottom: calc(var(--MI-stickyBottom, 0px) + 14px);
 }
 
 .showLessLabel {
 	display: inline-block;
-	background: var(--popup);
+	background: var(--MI_THEME-popup);
 	padding: 6px 10px;
 	font-size: 0.8em;
 	border-radius: 999px;
diff --git a/packages/frontend/src/components/MkSuperMenu.vue b/packages/frontend/src/components/MkSuperMenu.vue
index 3023f63e5d..0caaed6f39 100644
--- a/packages/frontend/src/components/MkSuperMenu.vue
+++ b/packages/frontend/src/components/MkSuperMenu.vue
@@ -10,15 +10,15 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 		<div class="items">
 			<template v-for="(item, i) in group.items">
-				<a v-if="item.type === 'a'" :href="item.href" :target="item.target" :tabindex="i" class="_button item" :class="{ danger: item.danger, active: item.active }">
+				<a v-if="item.type === 'a'" :href="item.href" :target="item.target" class="_button item" :class="{ danger: item.danger, active: item.active }">
 					<span v-if="item.icon" class="icon"><i :class="item.icon" class="ti-fw"></i></span>
 					<span class="text">{{ item.text }}</span>
 				</a>
-				<button v-else-if="item.type === 'button'" :tabindex="i" class="_button item" :class="{ danger: item.danger, active: item.active }" :disabled="item.active" @click="ev => item.action(ev)">
+				<button v-else-if="item.type === 'button'" class="_button item" :class="{ danger: item.danger, active: item.active }" :disabled="item.active" @click="ev => item.action(ev)">
 					<span v-if="item.icon" class="icon"><i :class="item.icon" class="ti-fw"></i></span>
 					<span class="text">{{ item.text }}</span>
 				</button>
-				<MkA v-else :to="item.to" :tabindex="i" class="_button item" :class="{ danger: item.danger, active: item.active }">
+				<MkA v-else :to="item.to" class="_button item" :class="{ danger: item.danger, active: item.active }">
 					<span v-if="item.icon" class="icon"><i :class="item.icon" class="ti-fw"></i></span>
 					<span class="text">{{ item.text }}</span>
 				</MkA>
@@ -28,11 +28,38 @@ SPDX-License-Identifier: AGPL-3.0-only
 </div>
 </template>
 
-<script lang="ts" setup>
-import { } from 'vue';
+<script lang="ts">
+export type SuperMenuDef = {
+	title?: string;
+	items: ({
+		type: 'a';
+		href: string;
+		target?: string;
+		icon?: string;
+		text: string;
+		danger?: boolean;
+		active?: boolean;
+	} | {
+		type: 'button';
+		icon?: string;
+		text: string;
+		danger?: boolean;
+		active?: boolean;
+		action: (ev: MouseEvent) => void;
+	} | {
+		type: 'link';
+		to: string;
+		icon?: string;
+		text: string;
+		danger?: boolean;
+		active?: boolean;
+	})[];
+};
+</script>
 
+<script lang="ts" setup>
 defineProps<{
-	def: any[];
+	def: SuperMenuDef[];
 	grid?: boolean;
 }>();
 </script>
@@ -43,7 +70,7 @@ defineProps<{
 		& + .group {
 			margin-top: 16px;
 			padding-top: 16px;
-			border-top: solid 0.5px var(--divider);
+			border-top: solid 0.5px var(--MI_THEME-divider);
 		}
 
 		> .title {
@@ -64,16 +91,20 @@ defineProps<{
 
 				&:hover {
 					text-decoration: none;
-					background: var(--panelHighlight);
+					background: var(--MI_THEME-panelHighlight);
+				}
+
+				&:focus-visible {
+					outline-offset: -2px;
 				}
 
 				&.active {
-					color: var(--accent);
-					background: var(--accentedBg);
+					color: var(--MI_THEME-accent);
+					background: var(--MI_THEME-accentedBg);
 				}
 
 				&.danger {
-					color: var(--error);
+					color: var(--MI_THEME-error);
 				}
 
 				> .icon {
@@ -96,14 +127,14 @@ defineProps<{
 
 	&.grid {
 		> .group {
+			margin-left: 0;
+			margin-right: 0;
+
 			& + .group {
 				padding-top: 0;
 				border-top: none;
 			}
 
-			margin-left: 0;
-			margin-right: 0;
-
 			> .title {
 				font-size: 1em;
 				opacity: 0.7;
@@ -124,10 +155,10 @@ defineProps<{
 					&:hover {
 						text-decoration: none;
 						background: none;
-						color: var(--accent);
+						color: var(--MI_THEME-accent);
 
 						> .icon {
-							background: var(--accentedBg);
+							background: var(--MI_THEME-accentedBg);
 						}
 					}
 
@@ -140,7 +171,7 @@ defineProps<{
 						width: 60px;
 						height: 60px;
 						aspect-ratio: 1;
-						background: var(--panel);
+						background: var(--MI_THEME-panel);
 						border-radius: 100%;
 					}
 
diff --git a/packages/frontend/src/components/MkSwitch.button.vue b/packages/frontend/src/components/MkSwitch.button.vue
index 226908e221..fd8ed0992e 100644
--- a/packages/frontend/src/components/MkSwitch.button.vue
+++ b/packages/frontend/src/components/MkSwitch.button.vue
@@ -51,9 +51,9 @@ const toggle = () => {
 	width: calc(var(--height) * 1.6);
 	height: calc(var(--height) + 2px); // 枠線
 	outline: none;
-	background: var(--switchOffBg);
+	background: var(--MI_THEME-switchOffBg);
 	background-clip: content-box;
-	border: solid 1px var(--switchOffBg);
+	border: solid 1px var(--MI_THEME-switchOffBg);
 	border-radius: 999px;
 	cursor: pointer;
 	transition: inherit;
@@ -61,8 +61,8 @@ const toggle = () => {
 }
 
 .buttonChecked {
-	background-color: var(--switchOnBg) !important;
-	border-color: var(--switchOnBg) !important;
+	background-color: var(--MI_THEME-switchOnBg) !important;
+	border-color: var(--MI_THEME-switchOnBg) !important;
 }
 
 .buttonDisabled {
@@ -80,12 +80,12 @@ const toggle = () => {
 
 	&:not(.knobChecked) {
 		left: 3px;
-		background: var(--switchOffFg);
+		background: var(--MI_THEME-switchOffFg);
 	}
 }
 
 .knobChecked {
 	left: calc(calc(100% - var(--height)) + 3px);
-	background: var(--switchOnFg);
+	background: var(--MI_THEME-switchOnFg);
 }
 </style>
diff --git a/packages/frontend/src/components/MkSwitch.vue b/packages/frontend/src/components/MkSwitch.vue
index a19b45448b..5e6029ee40 100644
--- a/packages/frontend/src/components/MkSwitch.vue
+++ b/packages/frontend/src/components/MkSwitch.vue
@@ -10,10 +10,10 @@ SPDX-License-Identifier: AGPL-3.0-only
 		type="checkbox"
 		:disabled="disabled"
 		:class="$style.input"
-		@keydown.enter="toggle"
+		@click="toggle"
 	>
-	<XButton :checked="checked" :disabled="disabled" @toggle="toggle"/>
-	<span :class="$style.body">
+	<XButton :class="$style.toggle" :checked="checked" :disabled="disabled" @toggle="toggle"/>
+	<span v-if="!noBody" :class="$style.body">
 		<!-- TODO: 無名slotの方は廃止 -->
 		<span :class="$style.label">
 			<span @click="toggle">
@@ -34,16 +34,19 @@ const props = defineProps<{
 	modelValue: boolean | Ref<boolean>;
 	disabled?: boolean;
 	helpText?: string;
+	noBody?: boolean;
 }>();
 
 const emit = defineEmits<{
 	(ev: 'update:modelValue', v: boolean): void;
+	(ev: 'change', v: boolean): void;
 }>();
 
 const checked = toRefs(props).modelValue;
 const toggle = () => {
 	if (props.disabled) return;
 	emit('update:modelValue', !checked.value);
+	emit('change', !checked.value);
 };
 </script>
 
@@ -56,7 +59,7 @@ const toggle = () => {
 
 	&:hover {
 		> .button {
-			border-color: var(--inputBorderHover) !important;
+			border-color: var(--MI_THEME-inputBorderHover) !important;
 		}
 	}
 
@@ -72,13 +75,19 @@ const toggle = () => {
 	height: 0;
 	opacity: 0;
 	margin: 0;
+
+	&:focus-visible ~ .toggle {
+		outline: 2px solid var(--MI_THEME-focus);
+		outline-offset: 2px;
+	}
 }
+
 .body {
 	margin-left: 12px;
 	margin-top: 2px;
 	display: block;
 	transition: inherit;
-	color: var(--fg);
+	color: var(--MI_THEME-fg);
 }
 
 .label {
@@ -90,7 +99,7 @@ const toggle = () => {
 
 .caption {
 	margin: 8px 0 0 0;
-	color: var(--fgTransparentWeak);
+	color: var(--MI_THEME-fgTransparentWeak);
 	font-size: 0.85em;
 
 	&:empty {
diff --git a/packages/frontend/src/components/MkSystemWebhookEditor.impl.ts b/packages/frontend/src/components/MkSystemWebhookEditor.impl.ts
new file mode 100644
index 0000000000..19e4eea733
--- /dev/null
+++ b/packages/frontend/src/components/MkSystemWebhookEditor.impl.ts
@@ -0,0 +1,47 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { defineAsyncComponent } from 'vue';
+import * as Misskey from 'misskey-js';
+import * as os from '@/os.js';
+
+export type SystemWebhookEventType = Misskey.entities.SystemWebhook['on'][number];
+
+export type MkSystemWebhookEditorProps = {
+	mode: 'create' | 'edit';
+	id?: string;
+	requiredEvents?: SystemWebhookEventType[];
+};
+
+export type MkSystemWebhookResult = {
+	id?: string;
+	isActive: boolean;
+	name: string;
+	on: SystemWebhookEventType[];
+	url: string;
+	secret: string;
+};
+
+export async function showSystemWebhookEditorDialog(props: MkSystemWebhookEditorProps): Promise<MkSystemWebhookResult | null> {
+	const { result } = await new Promise<{ result: MkSystemWebhookResult | null }>(async resolve => {
+		const { dispose } = os.popup(
+			defineAsyncComponent(() => import('@/components/MkSystemWebhookEditor.vue')),
+			props,
+			{
+				submitted: (ev: MkSystemWebhookResult) => {
+					resolve({ result: ev });
+				},
+				canceled: () => {
+					resolve({ result: null });
+				},
+				closed: () => {
+					dispose();
+				},
+			},
+		);
+	});
+
+	return result;
+}
diff --git a/packages/frontend/src/components/MkSystemWebhookEditor.vue b/packages/frontend/src/components/MkSystemWebhookEditor.vue
new file mode 100644
index 0000000000..485d003f93
--- /dev/null
+++ b/packages/frontend/src/components/MkSystemWebhookEditor.vue
@@ -0,0 +1,312 @@
+<!--
+SPDX-FileCopyrightText: syuilo and misskey-project
+SPDX-License-Identifier: AGPL-3.0-only
+-->
+
+<template>
+<MkModalWindow
+	ref="dialogEl"
+	:width="450"
+	:height="590"
+	:canClose="true"
+	:withOkButton="false"
+	:okButtonDisabled="false"
+	@click="onCancelClicked"
+	@close="onCancelClicked"
+	@closed="emit('closed')"
+>
+	<template #header>
+		{{ mode === 'create' ? i18n.ts._webhookSettings.createWebhook : i18n.ts._webhookSettings.modifyWebhook }}
+	</template>
+
+	<div style="display: flex; flex-direction: column; min-height: 100%;">
+		<MkSpacer :marginMin="20" :marginMax="28" style="flex-grow: 1;">
+			<MkLoading v-if="loading !== 0"/>
+			<div v-else :class="$style.root" class="_gaps_m">
+				<MkInput v-model="title">
+					<template #label>{{ i18n.ts._webhookSettings.name }}</template>
+				</MkInput>
+				<MkInput v-model="url">
+					<template #label>URL</template>
+				</MkInput>
+				<MkInput v-model="secret">
+					<template #label>{{ i18n.ts._webhookSettings.secret }}</template>
+				</MkInput>
+				<MkFolder :defaultOpen="true">
+					<template #label>{{ i18n.ts._webhookSettings.trigger }}</template>
+
+					<div class="_gaps">
+						<div class="_gaps_s">
+							<div :class="$style.switchBox">
+								<MkSwitch v-model="events.abuseReport" :disabled="disabledEvents.abuseReport">
+									<template #label>{{ i18n.ts._webhookSettings._systemEvents.abuseReport }}</template>
+								</MkSwitch>
+								<MkButton v-show="mode === 'edit'" transparent :class="$style.testButton" :disabled="!(isActive && events.abuseReport)" @click="test('abuseReport')"><i class="ti ti-send"></i></MkButton>
+							</div>
+							<div :class="$style.switchBox">
+								<MkSwitch v-model="events.abuseReportResolved" :disabled="disabledEvents.abuseReportResolved">
+									<template #label>{{ i18n.ts._webhookSettings._systemEvents.abuseReportResolved }}</template>
+								</MkSwitch>
+								<MkButton v-show="mode === 'edit'" transparent :class="$style.testButton" :disabled="!(isActive && events.abuseReportResolved)" @click="test('abuseReportResolved')"><i class="ti ti-send"></i></MkButton>
+							</div>
+							<div :class="$style.switchBox">
+								<MkSwitch v-model="events.userCreated" :disabled="disabledEvents.userCreated">
+									<template #label>{{ i18n.ts._webhookSettings._systemEvents.userCreated }}</template>
+								</MkSwitch>
+								<MkButton v-show="mode === 'edit'" transparent :class="$style.testButton" :disabled="!(isActive && events.userCreated)" @click="test('userCreated')"><i class="ti ti-send"></i></MkButton>
+							</div>
+							<div :class="$style.switchBox">
+								<MkSwitch v-model="events.inactiveModeratorsWarning" :disabled="disabledEvents.inactiveModeratorsWarning">
+									<template #label>{{ i18n.ts._webhookSettings._systemEvents.inactiveModeratorsWarning }}</template>
+								</MkSwitch>
+								<MkButton v-show="mode === 'edit'" transparent :class="$style.testButton" :disabled="!(isActive && events.inactiveModeratorsWarning)" @click="test('inactiveModeratorsWarning')"><i class="ti ti-send"></i></MkButton>
+							</div>
+							<div :class="$style.switchBox">
+								<MkSwitch v-model="events.inactiveModeratorsInvitationOnlyChanged" :disabled="disabledEvents.inactiveModeratorsInvitationOnlyChanged">
+									<template #label>{{ i18n.ts._webhookSettings._systemEvents.inactiveModeratorsInvitationOnlyChanged }}</template>
+								</MkSwitch>
+								<MkButton v-show="mode === 'edit'" transparent :class="$style.testButton" :disabled="!(isActive && events.inactiveModeratorsInvitationOnlyChanged)" @click="test('inactiveModeratorsInvitationOnlyChanged')"><i class="ti ti-send"></i></MkButton>
+							</div>
+						</div>
+
+						<div v-show="mode === 'edit'" :class="$style.description">
+							{{ i18n.ts._webhookSettings.testRemarks }}
+						</div>
+					</div>
+				</MkFolder>
+
+				<MkSwitch v-model="isActive">
+					<template #label>{{ i18n.ts.enable }}</template>
+				</MkSwitch>
+			</div>
+		</MkSpacer>
+		<div :class="$style.footer" class="_buttonsCenter">
+			<MkButton primary rounded :disabled="disableSubmitButton" @click="onSubmitClicked">
+				<i class="ti ti-check"></i>
+				{{ i18n.ts.ok }}
+			</MkButton>
+			<MkButton rounded @click="onCancelClicked"><i class="ti ti-x"></i> {{ i18n.ts.cancel }}</MkButton>
+		</div>
+	</div>
+</MkModalWindow>
+</template>
+
+<script setup lang="ts">
+import { computed, onMounted, ref, shallowRef, toRefs } from 'vue';
+import * as Misskey from 'misskey-js';
+import MkInput from '@/components/MkInput.vue';
+import MkSwitch from '@/components/MkSwitch.vue';
+import {
+	MkSystemWebhookEditorProps,
+	MkSystemWebhookResult,
+	SystemWebhookEventType,
+} from '@/components/MkSystemWebhookEditor.impl.js';
+import { i18n } from '@/i18n.js';
+import MkButton from '@/components/MkButton.vue';
+import { misskeyApi } from '@/scripts/misskey-api.js';
+import MkModalWindow from '@/components/MkModalWindow.vue';
+import MkFolder from '@/components/MkFolder.vue';
+import * as os from '@/os.js';
+
+type EventType = {
+	abuseReport: boolean;
+	abuseReportResolved: boolean;
+	userCreated: boolean;
+	inactiveModeratorsWarning: boolean;
+	inactiveModeratorsInvitationOnlyChanged: boolean;
+}
+
+const emit = defineEmits<{
+	(ev: 'submitted', result: MkSystemWebhookResult): void;
+	(ev: 'canceled'): void;
+	(ev: 'closed'): void;
+}>();
+
+const dialogEl = shallowRef<InstanceType<typeof MkModalWindow>>();
+
+const props = defineProps<MkSystemWebhookEditorProps>();
+
+const { mode, id, requiredEvents } = toRefs(props);
+
+const loading = ref<number>(0);
+
+const title = ref<string>('');
+const url = ref<string>('');
+const secret = ref<string>('');
+const events = ref<EventType>({
+	abuseReport: true,
+	abuseReportResolved: true,
+	userCreated: true,
+	inactiveModeratorsWarning: true,
+	inactiveModeratorsInvitationOnlyChanged: true,
+});
+const isActive = ref<boolean>(true);
+
+const disabledEvents = ref<EventType>({
+	abuseReport: false,
+	abuseReportResolved: false,
+	userCreated: false,
+	inactiveModeratorsWarning: false,
+	inactiveModeratorsInvitationOnlyChanged: false,
+});
+
+const disableSubmitButton = computed(() => {
+	if (!title.value) {
+		return true;
+	}
+	if (!url.value) {
+		return true;
+	}
+	if (!secret.value) {
+		return true;
+	}
+
+	return false;
+});
+
+async function onSubmitClicked() {
+	await loadingScope(async () => {
+		const params = {
+			isActive: isActive.value,
+			name: title.value,
+			url: url.value,
+			secret: secret.value,
+			on: Object.keys(events.value).filter(ev => events.value[ev as keyof EventType]) as SystemWebhookEventType[],
+		};
+
+		try {
+			switch (mode.value) {
+				case 'create': {
+					const result = await misskeyApi('admin/system-webhook/create', params);
+					dialogEl.value?.close();
+					emit('submitted', result);
+					break;
+				}
+				case 'edit': {
+					// eslint-disable-next-line
+					const result = await misskeyApi('admin/system-webhook/update', { id: id.value!, ...params });
+					dialogEl.value?.close();
+					emit('submitted', result);
+					break;
+				}
+			}
+			// eslint-disable-next-line
+		} catch (ex: any) {
+			const msg = ex.message ?? i18n.ts.internalServerErrorDescription;
+			await os.alert({ type: 'error', title: i18n.ts.error, text: msg });
+			dialogEl.value?.close();
+			emit('canceled');
+		}
+	});
+}
+
+function onCancelClicked() {
+	dialogEl.value?.close();
+	emit('canceled');
+}
+
+async function loadingScope<T>(fn: () => Promise<T>): Promise<T> {
+	loading.value++;
+	try {
+		return await fn();
+	} finally {
+		loading.value--;
+	}
+}
+
+async function test(type: Misskey.entities.SystemWebhook['on'][number]): Promise<void> {
+	if (!id.value) {
+		return Promise.resolve();
+	}
+
+	await os.apiWithDialog('admin/system-webhook/test', {
+		webhookId: id.value,
+		type,
+		override: {
+			secret: secret.value,
+			url: url.value,
+		},
+	});
+}
+
+onMounted(async () => {
+	await loadingScope(async () => {
+		switch (mode.value) {
+			case 'edit': {
+				if (!id.value) {
+					throw new Error('id is required');
+				}
+
+				try {
+					const res = await misskeyApi('admin/system-webhook/show', { id: id.value });
+
+					title.value = res.name;
+					url.value = res.url;
+					secret.value = res.secret;
+					isActive.value = res.isActive;
+					for (const ev of Object.keys(events.value)) {
+						events.value[ev] = res.on.includes(ev as SystemWebhookEventType);
+					}
+					// eslint-disable-next-line @typescript-eslint/no-explicit-any
+				} catch (ex: any) {
+					const msg = ex.message ?? i18n.ts.internalServerErrorDescription;
+					await os.alert({ type: 'error', title: i18n.ts.error, text: msg });
+					dialogEl.value?.close();
+					emit('canceled');
+				}
+				break;
+			}
+		}
+
+		for (const ev of requiredEvents.value ?? []) {
+			disabledEvents.value[ev] = true;
+		}
+	});
+});
+</script>
+
+<style module lang="scss">
+.root {
+	display: flex;
+	flex-direction: column;
+	justify-content: center;
+	align-items: stretch;
+}
+
+.footer {
+	position: sticky;
+	z-index: 10000;
+	bottom: 0;
+	left: 0;
+	padding: 12px;
+	border-top: solid 0.5px var(--MI_THEME-divider);
+	background: var(--MI_THEME-acrylicBg);
+	-webkit-backdrop-filter: var(--MI-blur, blur(15px));
+	backdrop-filter: var(--MI-blur, blur(15px));
+}
+
+.switchBox {
+	display: flex;
+	align-items: center;
+	justify-content: start;
+
+	.testButton {
+		$buttonSize: 28px;
+		padding: 0;
+		width: $buttonSize;
+		min-width: $buttonSize;
+		max-width: $buttonSize;
+		height: $buttonSize;
+		margin-left: auto;
+		line-height: normal;
+		font-size: 90%;
+		border-radius: 9999px;
+	}
+}
+
+.description {
+	font-size: 0.85em;
+	padding: 8px 0 0 0;
+	color: var(--MI_THEME-fgTransparentWeak);
+}
+</style>
diff --git a/packages/frontend/src/components/MkTab.vue b/packages/frontend/src/components/MkTab.vue
index f2d0c95013..f557ffa5dc 100644
--- a/packages/frontend/src/components/MkTab.vue
+++ b/packages/frontend/src/components/MkTab.vue
@@ -47,13 +47,13 @@ export default defineComponent({
 		}
 
 		&.active {
-			color: var(--accent);
-			background: var(--accentedBg);
+			color: var(--MI_THEME-accent);
+			background: var(--MI_THEME-accentedBg);
 		}
 
 		&:not(.active):hover {
-			color: var(--fgHighlighted);
-			background: var(--panelHighlight);
+			color: var(--MI_THEME-fgHighlighted);
+			background: var(--MI_THEME-panelHighlight);
 		}
 
 		&:not(:first-child) {
diff --git a/packages/frontend/src/components/MkTagCloud.vue b/packages/frontend/src/components/MkTagCloud.vue
index 6b9c181597..87aa046963 100644
--- a/packages/frontend/src/components/MkTagCloud.vue
+++ b/packages/frontend/src/components/MkTagCloud.vue
@@ -33,7 +33,7 @@ watch(available, () => {
 	try {
 		window.TagCanvas.Start(idForCanvas, idForTags, {
 			textColour: '#ffffff',
-			outlineColour: tinycolor(computedStyle.getPropertyValue('--accent')).toHexString(),
+			outlineColour: tinycolor(computedStyle.getPropertyValue('--MI_THEME-accent')).toHexString(),
 			outlineRadius: 10,
 			initial: [-0.030, -0.010],
 			frontSelect: true,
diff --git a/packages/frontend/src/components/MkTextarea.vue b/packages/frontend/src/components/MkTextarea.vue
index 59490c552a..d1a6e1ebbf 100644
--- a/packages/frontend/src/components/MkTextarea.vue
+++ b/packages/frontend/src/components/MkTextarea.vue
@@ -159,7 +159,7 @@ onUnmounted(() => {
 .caption {
 	font-size: 0.85em;
 	padding: 8px 0 0 0;
-	color: var(--fgTransparentWeak);
+	color: var(--MI_THEME-fgTransparentWeak);
 
 	&:empty {
 		display: none;
@@ -179,9 +179,9 @@ onUnmounted(() => {
 	font: inherit;
 	font-weight: normal;
 	font-size: 1em;
-	color: var(--fg);
-	background: var(--panel);
-	border: solid 1px var(--panel);
+	color: var(--MI_THEME-fg);
+	background: var(--MI_THEME-panel);
+	border: solid 1px var(--MI_THEME-panel);
 	border-radius: 6px;
 	outline: none;
 	box-shadow: none;
@@ -189,13 +189,13 @@ onUnmounted(() => {
 	transition: border-color 0.1s ease-out;
 
 	&:hover {
-		border-color: var(--inputBorderHover) !important;
+		border-color: var(--MI_THEME-inputBorderHover) !important;
 	}
 }
 
 .focused {
 	> .textarea {
-		border-color: var(--accent) !important;
+		border-color: var(--MI_THEME-accent) !important;
 	}
 }
 
@@ -226,7 +226,7 @@ onUnmounted(() => {
 
 .mfmPreview {
   padding: 12px;
-  border-radius: var(--radius);
+  border-radius: var(--MI-radius);
   box-sizing: border-box;
   min-height: 130px;
 	pointer-events: none;
diff --git a/packages/frontend/src/components/MkTimeline.vue b/packages/frontend/src/components/MkTimeline.vue
index 03dccb18e9..fb8eb4ae37 100644
--- a/packages/frontend/src/components/MkTimeline.vue
+++ b/packages/frontend/src/components/MkTimeline.vue
@@ -19,6 +19,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 <script lang="ts" setup>
 import { computed, watch, onUnmounted, provide, ref, shallowRef } from 'vue';
 import * as Misskey from 'misskey-js';
+import type { BasicTimelineType } from '@/timelines.js';
 import MkNotes from '@/components/MkNotes.vue';
 import MkPullToRefresh from '@/components/MkPullToRefresh.vue';
 import { useStream } from '@/stream.js';
@@ -29,7 +30,7 @@ import { defaultStore } from '@/store.js';
 import { Paging } from '@/components/MkPagination.vue';
 
 const props = withDefaults(defineProps<{
-	src: 'home' | 'local' | 'social' | 'global' | 'mentions' | 'directs' | 'list' | 'antenna' | 'channel' | 'role';
+	src: BasicTimelineType | 'mentions' | 'directs' | 'list' | 'antenna' | 'channel' | 'role';
 	list?: string;
 	antenna?: string;
 	channel?: string;
@@ -37,10 +38,12 @@ const props = withDefaults(defineProps<{
 	sound?: boolean;
 	withRenotes?: boolean;
 	withReplies?: boolean;
+	withSensitive?: boolean;
 	onlyFiles?: boolean;
 }>(), {
 	withRenotes: true,
 	withReplies: false,
+	withSensitive: true,
 	onlyFiles: false,
 });
 
@@ -50,6 +53,7 @@ const emit = defineEmits<{
 }>();
 
 provide('inTimeline', true);
+provide('tl_withSensitive', computed(() => props.withSensitive));
 provide('inChannel', computed(() => props.src === 'channel'));
 
 type TimelineQueryType = {
@@ -247,6 +251,9 @@ function refreshEndpointAndChannel() {
 // IDが切り替わったら切り替え先のTLを表示させたい
 watch(() => [props.list, props.antenna, props.channel, props.role, props.withRenotes], refreshEndpointAndChannel);
 
+// withSensitiveはクライアントで完結する処理のため、単にリロードするだけでOK
+watch(() => props.withSensitive, reloadTimeline);
+
 // 初回表示用
 refreshEndpointAndChannel();
 
diff --git a/packages/frontend/src/components/MkTokenGenerateWindow.vue b/packages/frontend/src/components/MkTokenGenerateWindow.vue
index b32066c950..73aef68964 100644
--- a/packages/frontend/src/components/MkTokenGenerateWindow.vue
+++ b/packages/frontend/src/components/MkTokenGenerateWindow.vue
@@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 	:okButtonDisabled="false"
 	:canClose="false"
 	@close="dialog?.close()"
-	@closed="$emit('closed')"
+	@closed="emit('closed')"
 	@ok="ok()"
 >
 	<template #header>{{ title || i18n.ts.generateAccessToken }}</template>
@@ -136,15 +136,15 @@ function enableAll(): void {
 .adminPermissions {
 	margin: 8px -6px 0;
 	padding: 24px 6px 6px;
-	border: 2px solid var(--error);
-	border-radius: calc(var(--radius) / 2);
+	border: 2px solid var(--MI_THEME-error);
+	border-radius: calc(var(--MI-radius) / 2);
 }
 
 .adminPermissionsHeader {
 	margin: -34px 0 6px 12px;
 	padding: 0 4px;
 	width: fit-content;
-	color: var(--error);
-	background: var(--panel);
+	color: var(--MI_THEME-error);
+	background: var(--MI_THEME-panel);
 }
 </style>
diff --git a/packages/frontend/src/components/MkTooltip.vue b/packages/frontend/src/components/MkTooltip.vue
index a3620aab68..10365d29b1 100644
--- a/packages/frontend/src/components/MkTooltip.vue
+++ b/packages/frontend/src/components/MkTooltip.vue
@@ -110,7 +110,7 @@ onUnmounted(() => {
 	box-sizing: border-box;
 	text-align: center;
 	border-radius: 4px;
-	border: solid 0.5px var(--divider);
+	border: solid 0.5px var(--MI_THEME-divider);
 	pointer-events: none;
 	transform-origin: center center;
 }
diff --git a/packages/frontend/src/components/MkTutorialDialog.Note.vue b/packages/frontend/src/components/MkTutorialDialog.Note.vue
index 2a26d22dc2..b26a01737e 100644
--- a/packages/frontend/src/components/MkTutorialDialog.Note.vue
+++ b/packages/frontend/src/components/MkTutorialDialog.Note.vue
@@ -18,7 +18,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 	<div style="text-align: center; padding: 0 16px;">{{ i18n.ts._initialTutorial._reaction.description }}</div>
 	<div>{{ i18n.ts._initialTutorial._reaction.letsTryReacting }}</div>
 	<MkNote :class="$style.exampleNoteRoot" :note="exampleNote" :mock="true" @reaction="addReaction" @removeReaction="removeReaction"/>
-	<div v-if="onceReacted"><b style="color: var(--accent);"><i class="ti ti-check"></i> {{ i18n.ts._initialTutorial.wellDone }}</b> {{ i18n.ts._initialTutorial._reaction.reactNotification }}<br>{{ i18n.ts._initialTutorial._reaction.reactDone }}</div>
+	<div v-if="onceReacted"><b style="color: var(--MI_THEME-accent);"><i class="ti ti-check"></i> {{ i18n.ts._initialTutorial.wellDone }}</b> {{ i18n.ts._initialTutorial._reaction.reactNotification }}<br>{{ i18n.ts._initialTutorial._reaction.reactDone }}</div>
 </div>
 </template>
 
@@ -105,13 +105,13 @@ function removeReaction(emoji) {
 
 <style lang="scss" module>
 .exampleNoteRoot {
-	border-radius: var(--radius);
-	border: var(--panelBorder);
-	background: var(--panel);
+	border-radius: var(--MI-radius);
+	border: var(--MI_THEME-panelBorder);
+	background: var(--MI_THEME-panel);
 }
 
 .divider {
 	height: 1px;
-	background: var(--divider);
+	background: var(--MI_THEME-divider);
 }
 </style>
diff --git a/packages/frontend/src/components/MkTutorialDialog.PostNote.vue b/packages/frontend/src/components/MkTutorialDialog.PostNote.vue
index e1d88b5e5c..6559307a94 100644
--- a/packages/frontend/src/components/MkTutorialDialog.PostNote.vue
+++ b/packages/frontend/src/components/MkTutorialDialog.PostNote.vue
@@ -81,14 +81,14 @@ const exampleCWNote = reactive<Misskey.entities.Note>({
 <style lang="scss" module>
 .exampleRoot {
 	max-width: none!important;
-	border-radius: var(--radius);
-	border: var(--panelBorder);
-	background: var(--panel);
+	border-radius: var(--MI-radius);
+	border: var(--MI_THEME-panelBorder);
+	background: var(--MI_THEME-panel);
 }
 
 .divider {
 	height: 1px;
-	background: var(--divider);
+	background: var(--MI_THEME-divider);
 }
 
 .image {
@@ -101,11 +101,11 @@ const exampleCWNote = reactive<Misskey.entities.Note>({
 	display: block;
 	width: 100%;
 	height: 40px;
-	color: var(--fgOnAccent);
+	color: var(--MI_THEME-fgOnAccent);
 	font-weight: bold;
 	text-align: left;
 
-	&:before {
+	&::before {
 		content: "";
 		display: block;
 		width: calc(100% - 38px);
@@ -117,7 +117,7 @@ const exampleCWNote = reactive<Misskey.entities.Note>({
 		right: 0;
 		bottom: 0;
 		border-radius: 999px;
-		background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB));
+		background: linear-gradient(90deg, var(--MI_THEME-buttonGradateA), var(--MI_THEME-buttonGradateB));
 	}
 
 }
diff --git a/packages/frontend/src/components/MkTutorialDialog.Sensitive.vue b/packages/frontend/src/components/MkTutorialDialog.Sensitive.vue
index 7ae48dcd15..f7b60fbc45 100644
--- a/packages/frontend/src/components/MkTutorialDialog.Sensitive.vue
+++ b/packages/frontend/src/components/MkTutorialDialog.Sensitive.vue
@@ -15,7 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 		:initialNote="exampleNote"
 		@fileChangeSensitive="doSucceeded"
 	></MkPostForm>
-	<div v-if="onceSucceeded"><b style="color: var(--accent);"><i class="ti ti-check"></i> {{ i18n.ts._initialTutorial.wellDone }}</b> {{ i18n.ts._initialTutorial._howToMakeAttachmentsSensitive.sensitiveSucceeded }}</div>
+	<div v-if="onceSucceeded"><b style="color: var(--MI_THEME-accent);"><i class="ti ti-check"></i> {{ i18n.ts._initialTutorial.wellDone }}</b> {{ i18n.ts._initialTutorial._howToMakeAttachmentsSensitive.sensitiveSucceeded }}</div>
 	<MkFolder>
 		<template #label>{{ i18n.ts.previewNoteText }}</template>
 		<MkNote :mock="true" :note="exampleNote" :class="$style.exampleRoot"></MkNote>
@@ -91,14 +91,14 @@ const exampleNote = reactive<Misskey.entities.Note>({
 
 <style lang="scss" module>
 .exampleRoot {
-	border-radius: var(--radius);
-	border: var(--panelBorder);
-	background: var(--panel);
+	border-radius: var(--MI-radius);
+	border: var(--MI_THEME-panelBorder);
+	background: var(--MI_THEME-panel);
 }
 
 .divider {
 	height: 1px;
-	background: var(--divider);
+	background: var(--MI_THEME-divider);
 }
 
 .image {
@@ -111,11 +111,11 @@ const exampleNote = reactive<Misskey.entities.Note>({
 	display: block;
 	width: 100%;
 	height: 40px;
-	color: var(--fgOnAccent);
+	color: var(--MI_THEME-fgOnAccent);
 	font-weight: bold;
 	text-align: left;
 
-	&:before {
+	&::before {
 		content: "";
 		display: block;
 		width: calc(100% - 38px);
@@ -127,7 +127,7 @@ const exampleNote = reactive<Misskey.entities.Note>({
 		right: 0;
 		bottom: 0;
 		border-radius: 999px;
-		background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB));
+		background: linear-gradient(90deg, var(--MI_THEME-buttonGradateA), var(--MI_THEME-buttonGradateB));
 	}
 
 }
diff --git a/packages/frontend/src/components/MkTutorialDialog.Timeline.vue b/packages/frontend/src/components/MkTutorialDialog.Timeline.vue
index 57f26e86a7..b203110ef0 100644
--- a/packages/frontend/src/components/MkTutorialDialog.Timeline.vue
+++ b/packages/frontend/src/components/MkTutorialDialog.Timeline.vue
@@ -7,10 +7,9 @@ SPDX-License-Identifier: AGPL-3.0-only
 <div class="_gaps">
 	<div style="text-align: center; padding: 0 16px;">{{ i18n.ts._initialTutorial._timeline.description1 }}</div>
 	<div class="_gaps_s">
-		<div><i class="ti ti-home"></i> <b>{{ i18n.ts._timelines.home }}</b> … {{ i18n.ts._initialTutorial._timeline.home }}</div>
-		<div><i class="ti ti-planet"></i> <b>{{ i18n.ts._timelines.local }}</b> … {{ i18n.ts._initialTutorial._timeline.local }}</div>
-		<div><i class="ti ti-universe"></i> <b>{{ i18n.ts._timelines.social }}</b> … {{ i18n.ts._initialTutorial._timeline.social }}</div>
-		<div><i class="ti ti-whirl"></i> <b>{{ i18n.ts._timelines.global }}</b> … {{ i18n.ts._initialTutorial._timeline.global }}</div>
+		<div v-for="tl in basicTimelineTypes">
+			<i :class="basicTimelineIconClass(tl)"></i> <b>{{ i18n.ts._timelines[tl] }}</b> … {{ i18n.ts._initialTutorial._timeline[tl] }}
+		</div>
 	</div>
 	<div class="_gaps_s">
 		<div>{{ i18n.ts._initialTutorial._timeline.description2 }}</div>
@@ -22,24 +21,24 @@ SPDX-License-Identifier: AGPL-3.0-only
 			<a href="https://misskey-hub.net/docs/for-users/features/timeline/" target="_blank" class="_link">{{ i18n.ts.help }}</a>
 		</template>
 	</I18n>
-
 </div>
 </template>
 
 <script setup lang="ts">
 import { i18n } from '@/i18n.js';
+import { basicTimelineIconClass, basicTimelineTypes } from '@/timelines.js';
 </script>
 
 <style lang="scss" module>
 .exampleNoteRoot {
-	border-radius: var(--radius);
-	border: var(--panelBorder);
-	background: var(--panel);
+	border-radius: var(--MI-radius);
+	border: var(--MI_THEME-panelBorder);
+	background: var(--MI_THEME-panel);
 }
 
 .divider {
 	height: 1px;
-	background: var(--divider);
+	background: var(--MI_THEME-divider);
 }
 
 .image {
@@ -52,11 +51,11 @@ import { i18n } from '@/i18n.js';
 	display: block;
 	width: 100%;
 	height: 40px;
-	color: var(--fgOnAccent);
+	color: var(--MI_THEME-fgOnAccent);
 	font-weight: bold;
 	text-align: left;
 
-	&:before {
+	&::before {
 		content: "";
 		display: block;
 		width: calc(100% - 38px);
@@ -68,7 +67,7 @@ import { i18n } from '@/i18n.js';
 		right: 0;
 		bottom: 0;
 		border-radius: 999px;
-		background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB));
+		background: linear-gradient(90deg, var(--MI_THEME-buttonGradateA), var(--MI_THEME-buttonGradateB));
 	}
 
 }
diff --git a/packages/frontend/src/components/MkTutorialDialog.vue b/packages/frontend/src/components/MkTutorialDialog.vue
index d2711e4ec5..11d7c8dc4d 100644
--- a/packages/frontend/src/components/MkTutorialDialog.vue
+++ b/packages/frontend/src/components/MkTutorialDialog.vue
@@ -31,7 +31,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 					<MkAnimBg style="position: absolute; top: 0;" :scale="1.5"/>
 					<MkSpacer :marginMin="20" :marginMax="28">
 						<div class="_gaps" style="text-align: center;">
-							<i class="ti ti-confetti" style="display: block; margin: auto; font-size: 3em; color: var(--accent);"></i>
+							<i class="ti ti-confetti" style="display: block; margin: auto; font-size: 3em; color: var(--MI_THEME-accent);"></i>
 							<div style="font-size: 120%;">{{ i18n.ts._initialTutorial._landing.title }}</div>
 							<div>{{ i18n.ts._initialTutorial._landing.description }}</div>
 							<MkButton primary rounded gradate style="margin: 16px auto 0 auto;" @click="page++">{{ i18n.ts._initialTutorial.launchTutorial }} <i class="ti ti-arrow-right"></i></MkButton>
@@ -126,7 +126,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 					<MkAnimBg style="position: absolute; top: 0;" :scale="1.5"/>
 					<MkSpacer :marginMin="20" :marginMax="28">
 						<div class="_gaps" style="text-align: center;">
-							<i class="ti ti-check" style="display: block; margin: auto; font-size: 3em; color: var(--accent);"></i>
+							<i class="ti ti-check" style="display: block; margin: auto; font-size: 3em; color: var(--MI_THEME-accent);"></i>
 							<div style="font-size: 120%;">{{ i18n.ts._initialTutorial._done.title }}</div>
 							<I18n :src="i18n.ts._initialTutorial._done.description" tag="div" style="padding: 0 16px;">
 								<template #link>
@@ -158,7 +158,7 @@ import XSensitive from '@/components/MkTutorialDialog.Sensitive.vue';
 import MkAnimBg from '@/components/MkAnimBg.vue';
 import { i18n } from '@/i18n.js';
 import { instance } from '@/instance.js';
-import { host } from '@/config.js';
+import { host } from '@@/js/config.js';
 import { claimAchievement } from '@/scripts/achievements.js';
 import * as os from '@/os.js';
 
@@ -172,7 +172,7 @@ const emit = defineEmits<{
 
 const dialog = shallowRef<InstanceType<typeof MkModalWindow>>();
 
-// eslint-disable-next-line vue/no-setup-props-destructure
+// eslint-disable-next-line vue/no-setup-props-reactivity-loss
 const page = ref(props.initialPage ?? 0);
 
 watch(page, (to) => {
@@ -223,7 +223,7 @@ async function close(skip: boolean) {
 
 .progressBarValue {
 	height: 100%;
-	background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB));
+	background: linear-gradient(90deg, var(--MI_THEME-buttonGradateA), var(--MI_THEME-buttonGradateB));
 	transition: all 0.5s cubic-bezier(0,.5,.5,1);
 }
 
@@ -253,7 +253,7 @@ async function close(skip: boolean) {
 	left: 0;
 	flex-shrink: 0;
 	padding: 12px;
-	border-top: solid 0.5px var(--divider);
+	border-top: solid 0.5px var(--MI_THEME-divider);
 	-webkit-backdrop-filter: blur(15px);
 	backdrop-filter: blur(15px);
 }
diff --git a/packages/frontend/src/components/MkUpdated.vue b/packages/frontend/src/components/MkUpdated.vue
index 188cc37f41..c937b4ce59 100644
--- a/packages/frontend/src/components/MkUpdated.vue
+++ b/packages/frontend/src/components/MkUpdated.vue
@@ -19,7 +19,7 @@ import { onMounted, shallowRef } from 'vue';
 import MkModal from '@/components/MkModal.vue';
 import MkButton from '@/components/MkButton.vue';
 import MkSparkle from '@/components/MkSparkle.vue';
-import { version } from '@/config.js';
+import { version } from '@@/js/config.js';
 import { i18n } from '@/i18n.js';
 import { confetti } from '@/scripts/confetti.js';
 
@@ -46,8 +46,8 @@ onMounted(() => {
 	max-width: 480px;
 	box-sizing: border-box;
 	text-align: center;
-	background: var(--panel);
-	border-radius: var(--radius);
+	background: var(--MI_THEME-panel);
+	border-radius: var(--MI-radius);
 }
 
 .title {
diff --git a/packages/frontend/src/components/MkUrlPreview.vue b/packages/frontend/src/components/MkUrlPreview.vue
index 6954f1f6ff..f0da8fd3f2 100644
--- a/packages/frontend/src/components/MkUrlPreview.vue
+++ b/packages/frontend/src/components/MkUrlPreview.vue
@@ -15,7 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 			scrolling="no"
 			:allow="player.allow == null ? 'autoplay;encrypted-media;fullscreen' : player.allow.filter(x => ['autoplay', 'clipboard-write', 'fullscreen', 'encrypted-media', 'picture-in-picture', 'web-share'].includes(x)).join(';')"
 			:class="$style.playerIframe"
-			:src="player.url + (player.url.match(/\?/) ? '&autoplay=1&auto_play=1' : '?autoplay=1&auto_play=1')"
+			:src="transformPlayerUrl(player.url)"
 			:style="{ border: 0 }"
 		></iframe>
 		<span v-else>invalid url</span>
@@ -84,13 +84,14 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <script lang="ts" setup>
 import { defineAsyncComponent, onDeactivated, onUnmounted, ref } from 'vue';
+import { url as local } from '@@/js/config.js';
+import { versatileLang } from '@@/js/intl-const.js';
 import type { summaly } from '@misskey-dev/summaly';
-import { url as local } from '@/config.js';
 import { i18n } from '@/i18n.js';
 import * as os from '@/os.js';
 import { deviceKind } from '@/scripts/device-kind.js';
 import MkButton from '@/components/MkButton.vue';
-import { versatileLang } from '@/scripts/intl-const.js';
+import { transformPlayerUrl } from '@/scripts/player-url-transform.js';
 import { defaultStore } from '@/store.js';
 
 type SummalyResult = Awaited<ReturnType<typeof summaly>>;
@@ -179,7 +180,7 @@ window.fetch(`/url?url=${encodeURIComponent(requestUrl.href)}&lang=${versatileLa
 		sensitive.value = info.sensitive ?? false;
 	});
 
-function adjustTweetHeight(message: any) {
+function adjustTweetHeight(message: MessageEvent) {
 	if (message.origin !== 'https://platform.twitter.com') return;
 	const embed = message.data?.['twttr.embed'];
 	if (embed?.method !== 'twttr.private.resize') return;
@@ -188,16 +189,20 @@ function adjustTweetHeight(message: any) {
 	if (height) tweetHeight.value = height;
 }
 
-const openPlayer = (): void => {
-	os.popup(defineAsyncComponent(() => import('@/components/MkYouTubePlayer.vue')), {
+function openPlayer(): void {
+	const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkYouTubePlayer.vue')), {
 		url: requestUrl.href,
+	}, {
+		closed: () => {
+			dispose();
+		},
 	});
-};
+}
 
-(window as any).addEventListener('message', adjustTweetHeight);
+window.addEventListener('message', adjustTweetHeight);
 
 onUnmounted(() => {
-	(window as any).removeEventListener('message', adjustTweetHeight);
+	window.removeEventListener('message', adjustTweetHeight);
 });
 </script>
 
@@ -216,7 +221,7 @@ onUnmounted(() => {
 	height: 1.5em;
 	padding: 0;
 	margin: 0;
-	color: var(--fg);
+	color: var(--MI_THEME-fg);
 	background: rgba(128, 128, 128, 0.2);
 	opacity: 0.7;
 
@@ -237,7 +242,7 @@ onUnmounted(() => {
 	position: relative;
 	display: block;
 	font-size: 14px;
-	box-shadow: 0 0 0 1px var(--divider);
+	box-shadow: 0 0 0 1px var(--MI_THEME-divider);
 	border-radius: 8px;
 	overflow: clip;
 
@@ -267,7 +272,7 @@ onUnmounted(() => {
 	height: 100%;
 	background-position: center;
 	background-size: cover;
-	background-color: var(--bg);
+	background-color: var(--MI_THEME-bg);
 	display: flex;
 	justify-content: center;
 	align-items: center;
@@ -314,7 +319,6 @@ onUnmounted(() => {
 .siteName {
 	display: inline-block;
 	margin: 0;
-	color: var(--urlPreviewInfo);
 	font-size: 0.8em;
 	line-height: 16px;
 	vertical-align: top;
diff --git a/packages/frontend/src/components/MkUserAnnouncementEditDialog.vue b/packages/frontend/src/components/MkUserAnnouncementEditDialog.vue
index 3c5f563aa0..fe499fabbf 100644
--- a/packages/frontend/src/components/MkUserAnnouncementEditDialog.vue
+++ b/packages/frontend/src/components/MkUserAnnouncementEditDialog.vue
@@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 	ref="dialog"
 	:width="400"
 	@close="dialog?.close()"
-	@closed="$emit('closed')"
+	@closed="emit('closed')"
 >
 	<template v-if="announcement" #header>:{{ announcement.title }}:</template>
 	<template v-else #header>New announcement</template>
@@ -25,9 +25,9 @@ SPDX-License-Identifier: AGPL-3.0-only
 				<MkRadios v-model="icon">
 					<template #label>{{ i18n.ts.icon }}</template>
 					<option value="info"><i class="ti ti-info-circle"></i></option>
-					<option value="warning"><i class="ti ti-alert-triangle" style="color: var(--warn);"></i></option>
-					<option value="error"><i class="ti ti-circle-x" style="color: var(--error);"></i></option>
-					<option value="success"><i class="ti ti-check" style="color: var(--success);"></i></option>
+					<option value="warning"><i class="ti ti-alert-triangle" style="color: var(--MI_THEME-warn);"></i></option>
+					<option value="error"><i class="ti ti-circle-x" style="color: var(--MI_THEME-error);"></i></option>
+					<option value="success"><i class="ti ti-check" style="color: var(--MI_THEME-success);"></i></option>
 				</MkRadios>
 				<MkRadios v-model="display">
 					<template #label>{{ i18n.ts.display }}</template>
@@ -62,9 +62,16 @@ import MkTextarea from '@/components/MkTextarea.vue';
 import MkSwitch from '@/components/MkSwitch.vue';
 import MkRadios from '@/components/MkRadios.vue';
 
+type AdminAnnouncementType = Misskey.entities.AdminAnnouncementsCreateRequest & { id: string; }
+
 const props = defineProps<{
 	user: Misskey.entities.User,
-	announcement?: Misskey.entities.Announcement,
+	announcement?: Required<AdminAnnouncementType>,
+}>();
+
+const emit = defineEmits<{
+	(ev: 'done', v: { deleted?: boolean; updated?: AdminAnnouncementType; created?: AdminAnnouncementType; }): void,
+	(ev: 'closed'): void
 }>();
 
 const dialog = ref<InstanceType<typeof MkModalWindow> | null>(null);
@@ -74,11 +81,6 @@ const icon = ref(props.announcement ? props.announcement.icon : 'info');
 const display = ref(props.announcement ? props.announcement.display : 'dialog');
 const needConfirmationToRead = ref(props.announcement ? props.announcement.needConfirmationToRead : false);
 
-const emit = defineEmits<{
-	(ev: 'done', v: { deleted?: boolean; updated?: any; created?: any }): void,
-	(ev: 'closed'): void
-}>();
-
 async function done() {
 	const params = {
 		title: title.value,
@@ -88,7 +90,7 @@ async function done() {
 		display: display.value,
 		needConfirmationToRead: needConfirmationToRead.value,
 		userId: props.user.id,
-	};
+	} satisfies Misskey.entities.AdminAnnouncementsCreateRequest;
 
 	if (props.announcement) {
 		await os.apiWithDialog('admin/announcements/update', {
@@ -141,8 +143,8 @@ async function del() {
 	bottom: 0;
 	left: 0;
 	padding: 12px;
-	border-top: solid 0.5px var(--divider);
-	-webkit-backdrop-filter: var(--blur, blur(15px));
-	backdrop-filter: var(--blur, blur(15px));
+	border-top: solid 0.5px var(--MI_THEME-divider);
+	-webkit-backdrop-filter: var(--MI-blur, blur(15px));
+	backdrop-filter: var(--MI-blur, blur(15px));
 }
 </style>
diff --git a/packages/frontend/src/components/MkUserCardMini.vue b/packages/frontend/src/components/MkUserCardMini.vue
index d3e77b2818..7a2e878931 100644
--- a/packages/frontend/src/components/MkUserCardMini.vue
+++ b/packages/frontend/src/components/MkUserCardMini.vue
@@ -23,7 +23,7 @@ import { acct } from '@/filters/user.js';
 
 const props = withDefaults(defineProps<{
 	user: Misskey.entities.User;
-	withChart: boolean;
+	withChart?: boolean;
 }>(), {
 	withChart: true,
 });
@@ -49,7 +49,7 @@ $bodyInfoHieght: 16px;
 	display: flex;
 	align-items: center;
 	padding: 16px;
-	background: var(--panel);
+	background: var(--MI_THEME-panel);
 	border-radius: 8px;
 }
 
@@ -64,7 +64,7 @@ $bodyInfoHieght: 16px;
 	flex: 1;
 	overflow: hidden;
 	font-size: 0.9em;
-	color: var(--fg);
+	color: var(--MI_THEME-fg);
 	padding-right: 8px;
 }
 
diff --git a/packages/frontend/src/components/MkUserInfo.vue b/packages/frontend/src/components/MkUserInfo.vue
index f247ba8fdd..0164515a8a 100644
--- a/packages/frontend/src/components/MkUserInfo.vue
+++ b/packages/frontend/src/components/MkUserInfo.vue
@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <template>
 <div class="_panel" :class="$style.root">
-	<div :class="$style.banner" :style="user.bannerUrl ? `background-image: url(${user.bannerUrl})` : ''"></div>
+	<div :class="$style.banner" :style="user.bannerUrl ? `background-image: url(${defaultStore.state.disableShowingAnimatedImages ? getStaticImageUrl(user.bannerUrl) : user.bannerUrl})` : ''"></div>
 	<MkAvatar :class="$style.avatar" :user="user" indicator/>
 	<div :class="$style.title">
 		<MkA :class="$style.name" :to="userPage(user)"><MkUserName :user="user" :nowrap="false"/></MkA>
@@ -29,7 +29,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 			<p :class="$style.statusItemLabel">{{ i18n.ts.followers }}</p><span :class="$style.statusItemValue">{{ number(user.followersCount) }}</span>
 		</div>
 	</div>
-	<MkFollowButton v-if="$i && user.id != $i.id" :class="$style.follow" :user="user" mini/>
+	<MkFollowButton v-if="user.id != $i?.id" :class="$style.follow" :user="user" mini/>
 </div>
 </template>
 
@@ -41,6 +41,8 @@ import { userPage } from '@/filters/user.js';
 import { i18n } from '@/i18n.js';
 import { $i } from '@/account.js';
 import { isFollowingVisibleForMe, isFollowersVisibleForMe } from '@/scripts/isFfVisibleForMe.js';
+import { getStaticImageUrl } from '@/scripts/media-proxy.js';
+import { defaultStore } from '@/store.js';
 
 defineProps<{
 	user: Misskey.entities.UserDetailed;
@@ -67,7 +69,7 @@ defineProps<{
 	z-index: 2;
 	width: 58px;
 	height: 58px;
-	border: solid 4px var(--panel);
+	border: solid 4px var(--MI_THEME-panel);
 }
 
 .title {
@@ -88,7 +90,7 @@ defineProps<{
 	margin: 0;
 	line-height: 16px;
 	font-size: 0.8em;
-	color: var(--fg);
+	color: var(--MI_THEME-fg);
 	opacity: 0.7;
 }
 
@@ -106,7 +108,7 @@ defineProps<{
 .description {
 	padding: 16px;
 	font-size: 0.8em;
-	border-top: solid 0.5px var(--divider);
+	border-top: solid 0.5px var(--MI_THEME-divider);
 }
 
 .mfm {
@@ -118,7 +120,7 @@ defineProps<{
 
 .status {
 	padding: 10px 16px;
-	border-top: solid 0.5px var(--divider);
+	border-top: solid 0.5px var(--MI_THEME-divider);
 }
 
 .statusItem {
@@ -129,12 +131,12 @@ defineProps<{
 .statusItemLabel {
 	margin: 0;
 	font-size: 0.7em;
-	color: var(--fg);
+	color: var(--MI_THEME-fg);
 }
 
 .statusItemValue {
 	font-size: 1em;
-	color: var(--accent);
+	color: var(--MI_THEME-accent);
 }
 
 .follow {
diff --git a/packages/frontend/src/components/MkUserList.vue b/packages/frontend/src/components/MkUserList.vue
index 17a9254d01..8b4afd7994 100644
--- a/packages/frontend/src/components/MkUserList.vue
+++ b/packages/frontend/src/components/MkUserList.vue
@@ -39,6 +39,6 @@ const props = withDefaults(defineProps<{
 .root {
 	display: grid;
 	grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
-	grid-gap: var(--margin);
+	grid-gap: var(--MI-margin);
 }
 </style>
diff --git a/packages/frontend/src/components/MkUserOnlineIndicator.vue b/packages/frontend/src/components/MkUserOnlineIndicator.vue
index c39a900bcf..5cebeea2f4 100644
--- a/packages/frontend/src/components/MkUserOnlineIndicator.vue
+++ b/packages/frontend/src/components/MkUserOnlineIndicator.vue
@@ -36,7 +36,7 @@ const text = computed(() => {
 
 <style lang="scss" module>
 .root {
-	box-shadow: 0 0 0 3px var(--panel);
+	box-shadow: 0 0 0 3px var(--MI_THEME-panel);
 	border-radius: 120%; // Blinkのバグか知らんけど、100%ぴったりにすると何故か若干楕円でレンダリングされる
 
 	&.status_online {
diff --git a/packages/frontend/src/components/MkUserPopup.vue b/packages/frontend/src/components/MkUserPopup.vue
index 41b27a1afb..740202f28b 100644
--- a/packages/frontend/src/components/MkUserPopup.vue
+++ b/packages/frontend/src/components/MkUserPopup.vue
@@ -13,12 +13,12 @@ SPDX-License-Identifier: AGPL-3.0-only
 >
 	<div v-if="showing" :class="$style.root" class="_popup _shadow" :style="{ zIndex, top: top + 'px', left: left + 'px' }" @mouseover="() => { emit('mouseover'); }" @mouseleave="() => { emit('mouseleave'); }">
 		<div v-if="user != null">
-			<div :class="$style.banner" :style="user.bannerUrl ? `background-image: url(${user.bannerUrl})` : ''">
+			<div :class="$style.banner" :style="user.bannerUrl ? `background-image: url(${defaultStore.state.disableShowingAnimatedImages ? getStaticImageUrl(user.bannerUrl) : user.bannerUrl})` : ''">
 				<span v-if="$i && $i.id != user.id && user.isFollowed" :class="$style.followed">{{ i18n.ts.followsYou }}</span>
 			</div>
 			<svg viewBox="0 0 128 128" :class="$style.avatarBack">
 				<g transform="matrix(1.6,0,0,1.6,-38.4,-51.2)">
-					<path d="M64,32C81.661,32 96,46.339 96,64C95.891,72.184 104,72 104,72C104,72 74.096,80 64,80C52.755,80 24,72 24,72C24,72 31.854,72.018 32,64C32,46.339 46.339,32 64,32Z" style="fill: var(--popup);"/>
+					<path d="M64,32C81.661,32 96,46.339 96,64C95.891,72.184 104,72 104,72C104,72 74.096,80 64,80C52.755,80 24,72 24,72C24,72 31.854,72.018 32,64C32,46.339 46.339,32 64,32Z" style="fill: var(--MI_THEME-popup);"/>
 				</g>
 			</svg>
 			<MkAvatar :class="$style.avatar" :user="user" indicator/>
@@ -67,6 +67,7 @@ import { i18n } from '@/i18n.js';
 import { defaultStore } from '@/store.js';
 import { $i } from '@/account.js';
 import { isFollowingVisibleForMe, isFollowersVisibleForMe } from '@/scripts/isFfVisibleForMe.js';
+import { getStaticImageUrl } from '@/scripts/media-proxy.js';
 
 const props = defineProps<{
 	showing: boolean;
@@ -196,8 +197,8 @@ onMounted(() => {
 	padding: 16px 26px;
 	font-size: 0.8em;
 	text-align: center;
-	border-top: solid 1px var(--divider);
-	border-bottom: solid 1px var(--divider);
+	border-top: solid 1px var(--MI_THEME-divider);
+	border-bottom: solid 1px var(--MI_THEME-divider);
 }
 
 .mfm {
@@ -219,7 +220,7 @@ onMounted(() => {
 
 .statusItemLabel {
 	font-size: 0.7em;
-	color: var(--fgTransparentWeak);
+	color: var(--MI_THEME-fgTransparentWeak);
 }
 
 .menu {
@@ -227,7 +228,7 @@ onMounted(() => {
 	top: 8px;
 	right: 44px;
 	padding: 6px;
-	background: var(--panel);
+	background: var(--MI_THEME-panel);
 	border-radius: 999px;
 }
 
diff --git a/packages/frontend/src/components/MkUserSelectDialog.vue b/packages/frontend/src/components/MkUserSelectDialog.vue
index cc3ea78ffe..764bf74f21 100644
--- a/packages/frontend/src/components/MkUserSelectDialog.vue
+++ b/packages/frontend/src/components/MkUserSelectDialog.vue
@@ -11,7 +11,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 	@click="cancel()"
 	@close="cancel()"
 	@ok="ok()"
-	@closed="$emit('closed')"
+	@closed="emit('closed')"
 >
 	<template #header>{{ i18n.ts.selectUser }}</template>
 	<div>
@@ -61,7 +61,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { onMounted, ref } from 'vue';
+import { onMounted, ref, shallowRef } from 'vue';
 import * as Misskey from 'misskey-js';
 import MkInput from '@/components/MkInput.vue';
 import FormSplit from '@/components/form/split.vue';
@@ -70,7 +70,7 @@ import { misskeyApi } from '@/scripts/misskey-api.js';
 import { defaultStore } from '@/store.js';
 import { i18n } from '@/i18n.js';
 import { $i } from '@/account.js';
-import { host as currentHost, hostname } from '@/config.js';
+import { host as currentHost, hostname } from '@@/js/config.js';
 
 const emit = defineEmits<{
 	(ev: 'ok', selected: Misskey.entities.UserDetailed): void;
@@ -91,7 +91,7 @@ const host = ref('');
 const users = ref<Misskey.entities.UserLite[]>([]);
 const recentUsers = ref<Misskey.entities.UserDetailed[]>([]);
 const selected = ref<Misskey.entities.UserLite | null>(null);
-const dialogEl = ref();
+const dialogEl = shallowRef<InstanceType<typeof MkModalWindow>>();
 
 function search() {
 	if (username.value === '' && host.value === '') {
@@ -122,7 +122,7 @@ async function ok() {
 	});
 	emit('ok', user);
 
-	dialogEl.value.close();
+	dialogEl.value?.close();
 
 	// 最近使ったユーザー更新
 	let recents = defaultStore.state.recentlyUsedUsers;
@@ -133,7 +133,7 @@ async function ok() {
 
 function cancel() {
 	emit('cancel');
-	dialogEl.value.close();
+	dialogEl.value?.close();
 }
 
 onMounted(() => {
@@ -195,11 +195,11 @@ onMounted(() => {
 	font-size: 14px;
 
 	&:hover {
-		background: var(--X7);
+		background: var(--MI_THEME-X7);
 	}
 
 	&.selected {
-		background: var(--accent);
+		background: var(--MI_THEME-accent);
 		color: #fff;
 	}
 }
diff --git a/packages/frontend/src/components/MkUserSetupDialog.Follow.vue b/packages/frontend/src/components/MkUserSetupDialog.Follow.vue
index 1524ea0ec9..5153c06139 100644
--- a/packages/frontend/src/components/MkUserSetupDialog.Follow.vue
+++ b/packages/frontend/src/components/MkUserSetupDialog.Follow.vue
@@ -62,7 +62,7 @@ const popularUsers: Paging = {
 .users {
 	display: grid;
 	grid-template-columns: repeat(auto-fill, minmax(230px, 1fr));
-	grid-gap: var(--margin);
+	grid-gap: var(--MI-margin);
 	justify-content: center;
 }
 </style>
diff --git a/packages/frontend/src/components/MkUserSetupDialog.Profile.vue b/packages/frontend/src/components/MkUserSetupDialog.Profile.vue
index 3194641cdb..7cb48f6afb 100644
--- a/packages/frontend/src/components/MkUserSetupDialog.Profile.vue
+++ b/packages/frontend/src/components/MkUserSetupDialog.Profile.vue
@@ -51,6 +51,11 @@ watch(name, () => {
 		// 空文字列をnullにしたいので??は使うな
 		// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
 		name: name.value || null,
+	}, undefined, {
+		'0b3f9f6a-2f4d-4b1f-9fb4-49d3a2fd7191': {
+			title: i18n.ts.yourNameContainsProhibitedWords,
+			text: i18n.ts.yourNameContainsProhibitedWordsDescription,
+		},
 	});
 });
 
diff --git a/packages/frontend/src/components/MkUserSetupDialog.User.vue b/packages/frontend/src/components/MkUserSetupDialog.User.vue
index bb9af676e2..004edab630 100644
--- a/packages/frontend/src/components/MkUserSetupDialog.User.vue
+++ b/packages/frontend/src/components/MkUserSetupDialog.User.vue
@@ -61,7 +61,7 @@ async function follow() {
 	z-index: 2;
 	width: 58px;
 	height: 58px;
-	border: solid 4px var(--panel);
+	border: solid 4px var(--MI_THEME-panel);
 }
 
 .title {
@@ -82,7 +82,7 @@ async function follow() {
 	margin: 0;
 	line-height: 16px;
 	font-size: 0.8em;
-	color: var(--fg);
+	color: var(--MI_THEME-fg);
 	opacity: 0.7;
 }
 
@@ -99,7 +99,7 @@ async function follow() {
 }
 
 .footer {
-	border-top: solid 0.5px var(--divider);
+	border-top: solid 0.5px var(--MI_THEME-divider);
 	padding: 16px;
 }
 </style>
diff --git a/packages/frontend/src/components/MkUserSetupDialog.vue b/packages/frontend/src/components/MkUserSetupDialog.vue
index 1d376382ca..b7261129ef 100644
--- a/packages/frontend/src/components/MkUserSetupDialog.vue
+++ b/packages/frontend/src/components/MkUserSetupDialog.vue
@@ -35,7 +35,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 					<MkAnimBg style="position: absolute; top: 0;" :scale="1.5"/>
 					<MkSpacer :marginMin="20" :marginMax="28">
 						<div class="_gaps" style="text-align: center;">
-							<i class="ti ti-confetti" style="display: block; margin: auto; font-size: 3em; color: var(--accent);"></i>
+							<i class="ti ti-confetti" style="display: block; margin: auto; font-size: 3em; color: var(--MI_THEME-accent);"></i>
 							<div style="font-size: 120%;">{{ i18n.ts._initialAccountSetting.accountCreated }}</div>
 							<div>{{ i18n.ts._initialAccountSetting.letsStartAccountSetup }}</div>
 							<MkButton primary rounded gradate style="margin: 16px auto 0 auto;" data-cy-user-setup-continue @click="page++">{{ i18n.ts._initialAccountSetting.profileSetting }} <i class="ti ti-arrow-right"></i></MkButton>
@@ -91,7 +91,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 				<div :class="$style.centerPage">
 					<MkSpacer :marginMin="20" :marginMax="28">
 						<div class="_gaps" style="text-align: center;">
-							<i class="ti ti-bell-ringing-2" style="display: block; margin: auto; font-size: 3em; color: var(--accent);"></i>
+							<i class="ti ti-bell-ringing-2" style="display: block; margin: auto; font-size: 3em; color: var(--MI_THEME-accent);"></i>
 							<div style="font-size: 120%;">{{ i18n.ts.pushNotification }}</div>
 							<div style="padding: 0 16px;">{{ i18n.tsx._initialAccountSetting.pushNotificationDescription({ name: instance.name ?? host }) }}</div>
 							<MkPushNotificationAllowButton primary showOnlyToRegister style="margin: 0 auto;"/>
@@ -108,7 +108,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 					<MkAnimBg style="position: absolute; top: 0;" :scale="1.5"/>
 					<MkSpacer :marginMin="20" :marginMax="28">
 						<div class="_gaps" style="text-align: center;">
-							<i class="ti ti-check" style="display: block; margin: auto; font-size: 3em; color: var(--accent);"></i>
+							<i class="ti ti-check" style="display: block; margin: auto; font-size: 3em; color: var(--MI_THEME-accent);"></i>
 							<div style="font-size: 120%;">{{ i18n.ts._initialAccountSetting.initialAccountSettingCompleted }}</div>
 							<div>{{ i18n.tsx._initialAccountSetting.youCanContinueTutorial({ name: instance.name ?? host }) }}</div>
 							<div class="_buttonsCenter" style="margin-top: 16px;">
@@ -137,7 +137,7 @@ import XPrivacy from '@/components/MkUserSetupDialog.Privacy.vue';
 import MkAnimBg from '@/components/MkAnimBg.vue';
 import { i18n } from '@/i18n.js';
 import { instance } from '@/instance.js';
-import { host } from '@/config.js';
+import { host } from '@@/js/config.js';
 import MkPushNotificationAllowButton from '@/components/MkPushNotificationAllowButton.vue';
 import { defaultStore } from '@/store.js';
 import * as os from '@/os.js';
@@ -148,7 +148,7 @@ const emit = defineEmits<{
 
 const dialog = shallowRef<InstanceType<typeof MkModalWindow>>();
 
-// eslint-disable-next-line vue/no-setup-props-destructure
+// eslint-disable-next-line vue/no-setup-props-reactivity-loss
 const page = ref(defaultStore.state.accountSetupWizard);
 
 watch(page, () => {
@@ -176,9 +176,11 @@ function setupComplete() {
 function launchTutorial() {
 	setupComplete();
 	nextTick(() => {
-		os.popup(defineAsyncComponent(() => import('@/components/MkTutorialDialog.vue')), {
+		const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkTutorialDialog.vue')), {
 			initialPage: 1,
-		}, {}, 'closed');
+		}, {
+			closed: () => dispose(),
+		});
 	});
 }
 
@@ -221,7 +223,7 @@ async function later(later: boolean) {
 
 .progressBarValue {
 	height: 100%;
-	background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB));
+	background: linear-gradient(90deg, var(--MI_THEME-buttonGradateA), var(--MI_THEME-buttonGradateB));
 	transition: all 0.5s cubic-bezier(0,.5,.5,1);
 }
 
@@ -250,7 +252,7 @@ async function later(later: boolean) {
 	left: 0;
 	flex-shrink: 0;
 	padding: 12px;
-	border-top: solid 0.5px var(--divider);
+	border-top: solid 0.5px var(--MI_THEME-divider);
 	-webkit-backdrop-filter: blur(15px);
 	backdrop-filter: blur(15px);
 }
diff --git a/packages/frontend/src/components/MkUsersTooltip.vue b/packages/frontend/src/components/MkUsersTooltip.vue
index 054a503257..0cb7f22e93 100644
--- a/packages/frontend/src/components/MkUsersTooltip.vue
+++ b/packages/frontend/src/components/MkUsersTooltip.vue
@@ -16,12 +16,12 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { } from 'vue';
+import * as Misskey from 'misskey-js';
 import MkTooltip from './MkTooltip.vue';
 
 defineProps<{
 	showing: boolean;
-	users: any[]; // TODO
+	users: Misskey.entities.UserLite[];
 	count: number;
 	targetElement: HTMLElement;
 }>();
diff --git a/packages/frontend/src/components/MkVisibilityPicker.vue b/packages/frontend/src/components/MkVisibilityPicker.vue
index 5ecd41bfdf..650e639c4f 100644
--- a/packages/frontend/src/components/MkVisibilityPicker.vue
+++ b/packages/frontend/src/components/MkVisibilityPicker.vue
@@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 -->
 
 <template>
-<MkModal ref="modal" v-slot="{ type }" :zPriority="'high'" :src="src" @click="modal?.close()" @closed="emit('closed')">
+<MkModal ref="modal" v-slot="{ type }" :zPriority="'high'" :src="src" @click="modal?.close()" @closed="emit('closed')" @esc="modal?.close()">
 	<div class="_popup" :class="{ [$style.root]: true, [$style.asDrawer]: type === 'drawer' }">
 		<div :class="[$style.label, $style.item]">
 			{{ i18n.ts.visibility }}
@@ -124,7 +124,7 @@ function choose(visibility: typeof Misskey.noteVisibilities[number]): void {
 	}
 
 	&.active {
-		color: var(--accent);
+		color: var(--MI_THEME-accent);
 	}
 }
 
diff --git a/packages/frontend/src/components/MkVisitorDashboard.ActiveUsersChart.vue b/packages/frontend/src/components/MkVisitorDashboard.ActiveUsersChart.vue
index cab42cd59d..d098dad9a1 100644
--- a/packages/frontend/src/components/MkVisitorDashboard.ActiveUsersChart.vue
+++ b/packages/frontend/src/components/MkVisitorDashboard.ActiveUsersChart.vue
@@ -62,7 +62,7 @@ async function renderChart() {
 	const vLineColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
 
 	const computedStyle = getComputedStyle(document.documentElement);
-	const accent = tinycolor(computedStyle.getPropertyValue('--accent')).toHexString();
+	const accent = tinycolor(computedStyle.getPropertyValue('--MI_THEME-accent')).toHexString();
 
 	const colorRead = accent;
 	const colorWrite = '#2ecc71';
diff --git a/packages/frontend/src/components/MkVisitorDashboard.vue b/packages/frontend/src/components/MkVisitorDashboard.vue
index f7963f9938..97c765d81c 100644
--- a/packages/frontend/src/components/MkVisitorDashboard.vue
+++ b/packages/frontend/src/components/MkVisitorDashboard.vue
@@ -23,7 +23,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 			</div>
 			<div class="_gaps_s" :class="$style.mainActions">
 				<MkButton :class="$style.mainAction" full rounded gradate data-cy-signup style="margin-right: 12px;" @click="signup()">{{ i18n.ts.joinThisServer }}</MkButton>
-				<MkButton :class="$style.mainAction" full rounded @click="exploreOtherServers()">{{ i18n.ts.exploreOtherServers }}</MkButton>
+				<MkButton :class="$style.mainAction" full rounded link to="https://misskey-hub.net/servers/">{{ i18n.ts.exploreOtherServers }}</MkButton>
 				<MkButton :class="$style.mainAction" full rounded data-cy-signin @click="signin()">{{ i18n.ts.login }}</MkButton>
 			</div>
 		</div>
@@ -58,14 +58,15 @@ import XSignupDialog from '@/components/MkSignupDialog.vue';
 import MkButton from '@/components/MkButton.vue';
 import MkTimeline from '@/components/MkTimeline.vue';
 import MkInfo from '@/components/MkInfo.vue';
-import { instanceName } from '@/config.js';
+import { instanceName } from '@@/js/config.js';
 import * as os from '@/os.js';
 import { misskeyApi } from '@/scripts/misskey-api.js';
 import { i18n } from '@/i18n.js';
 import { instance } from '@/instance.js';
 import MkNumber from '@/components/MkNumber.vue';
 import XActiveUsersChart from '@/components/MkVisitorDashboard.ActiveUsersChart.vue';
-import { openInstanceMenu } from '@/ui/_common_/common';
+import { openInstanceMenu } from '@/ui/_common_/common.js';
+import type { MenuItem } from '@/types/menu.js';
 
 const stats = ref<Misskey.entities.StatsResponse | null>(null);
 
@@ -74,24 +75,24 @@ misskeyApi('stats', {}).then((res) => {
 });
 
 function signin() {
-	os.popup(XSigninDialog, {
+	const { dispose } = os.popup(XSigninDialog, {
 		autoSet: true,
-	}, {}, 'closed');
+	}, {
+		closed: () => dispose(),
+	});
 }
 
 function signup() {
-	os.popup(XSignupDialog, {
+	const { dispose } = os.popup(XSignupDialog, {
 		autoSet: true,
-	}, {}, 'closed');
+	}, {
+		closed: () => dispose(),
+	});
 }
 
-function showMenu(ev) {
+function showMenu(ev: MouseEvent) {
 	openInstanceMenu(ev);
 }
-
-function exploreOtherServers() {
-	window.open('https://misskey-hub.net/servers/', '_blank', 'noopener');
-}
 </script>
 
 <style lang="scss" module>
@@ -105,8 +106,8 @@ function exploreOtherServers() {
 
 .panel {
 	position: relative;
-	background: var(--panel);
-	border-radius: var(--radius);
+	background: var(--MI_THEME-panel);
+	border-radius: var(--MI-radius);
 	box-shadow: 0 12px 32px rgb(0 0 0 / 25%);
 }
 
@@ -177,14 +178,14 @@ function exploreOtherServers() {
 }
 
 .statsItemLabel {
-	color: var(--fgTransparentWeak);
+	color: var(--MI_THEME-fgTransparentWeak);
 	font-size: 0.9em;
 }
 
 .statsItemCount {
 	font-weight: bold;
 	font-size: 1.2em;
-	color: var(--accent);
+	color: var(--MI_THEME-accent);
 }
 
 .tl {
@@ -193,7 +194,7 @@ function exploreOtherServers() {
 
 .tlHeader {
 	padding: 12px 16px;
-	border-bottom: solid 1px var(--divider);
+	border-bottom: solid 1px var(--MI_THEME-divider);
 }
 
 .tlBody {
diff --git a/packages/frontend/src/components/MkWaitingDialog.vue b/packages/frontend/src/components/MkWaitingDialog.vue
index 60b75b6d30..34fa6b0723 100644
--- a/packages/frontend/src/components/MkWaitingDialog.vue
+++ b/packages/frontend/src/components/MkWaitingDialog.vue
@@ -47,8 +47,8 @@ watch(() => props.showing, () => {
 	padding: 32px;
 	box-sizing: border-box;
 	text-align: center;
-	background: var(--panel);
-	border-radius: var(--radius);
+	background: var(--MI_THEME-panel);
+	border-radius: var(--MI-radius);
 	width: 250px;
 
 	&.iconOnly {
@@ -65,7 +65,7 @@ watch(() => props.showing, () => {
 	font-size: 32px;
 
 	&.success {
-		color: var(--accent);
+		color: var(--MI_THEME-accent);
 	}
 
 	&.waiting {
diff --git a/packages/frontend/src/components/MkWidgets.vue b/packages/frontend/src/components/MkWidgets.vue
index 7550edd120..ba619f6063 100644
--- a/packages/frontend/src/components/MkWidgets.vue
+++ b/packages/frontend/src/components/MkWidgets.vue
@@ -7,12 +7,12 @@ SPDX-License-Identifier: AGPL-3.0-only
 <div :class="$style.root">
 	<template v-if="edit">
 		<header :class="$style.editHeader">
-			<MkSelect v-model="widgetAdderSelected" style="margin-bottom: var(--margin)" data-cy-widget-select>
+			<MkSelect v-model="widgetAdderSelected" style="margin-bottom: var(--MI-margin)" data-cy-widget-select>
 				<template #label>{{ i18n.ts.selectWidget }}</template>
 				<option v-for="widget in widgetDefs" :key="widget" :value="widget">{{ i18n.ts._widgets[widget] }}</option>
 			</MkSelect>
 			<MkButton inline primary data-cy-widget-add @click="addWidget"><i class="ti ti-plus"></i> {{ i18n.ts.add }}</MkButton>
-			<MkButton inline @click="$emit('exit')">{{ i18n.ts.close }}</MkButton>
+			<MkButton inline @click="emit('exit')">{{ i18n.ts.close }}</MkButton>
 		</header>
 		<Sortable
 			:modelValue="props.widgets"
@@ -57,6 +57,7 @@ import MkButton from '@/components/MkButton.vue';
 import { widgets as widgetDefs } from '@/widgets/index.js';
 import * as os from '@/os.js';
 import { i18n } from '@/i18n.js';
+import { isLink } from '@@/js/is-link.js';
 
 const Sortable = defineAsyncComponent(() => import('vuedraggable').then(x => x.default));
 
@@ -98,13 +99,6 @@ const updateWidget = (id, data) => {
 
 function onContextmenu(widget: Widget, ev: MouseEvent) {
 	const element = ev.target as HTMLElement | null;
-	const isLink = (el: HTMLElement): boolean => {
-		if (el.tagName === 'A') return true;
-		if (el.parentElement) {
-			return isLink(el.parentElement);
-		}
-		return false;
-	};
 	if (element && isLink(element)) return;
 	if (element && (['INPUT', 'TEXTAREA', 'IMG', 'VIDEO', 'CANVAS'].includes(element.tagName) || element.attributes['contenteditable'])) return;
 	if (window.getSelection()?.toString() !== '') return;
@@ -129,7 +123,7 @@ function onContextmenu(widget: Widget, ev: MouseEvent) {
 
 .widget {
 	contain: content;
-	margin: var(--margin) 0;
+	margin: var(--MI-margin) 0;
 
 	&:first-of-type {
 		margin-top: 0;
diff --git a/packages/frontend/src/components/MkWindow.vue b/packages/frontend/src/components/MkWindow.vue
index 303e49de00..2953f656d4 100644
--- a/packages/frontend/src/components/MkWindow.vue
+++ b/packages/frontend/src/components/MkWindow.vue
@@ -10,7 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 	:enterFromClass="defaultStore.state.animation ? $style.transition_window_enterFrom : ''"
 	:leaveToClass="defaultStore.state.animation ? $style.transition_window_leaveTo : ''"
 	appear
-	@afterLeave="$emit('closed')"
+	@afterLeave="emit('closed')"
 >
 	<div v-if="showing" ref="rootEl" :class="[$style.root, { [$style.maximized]: maximized }]">
 		<div :class="$style.body" class="_shadow" @mousedown="onBodyMousedown" @keydown="onKeydown">
@@ -54,12 +54,19 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <script lang="ts" setup>
 import { onBeforeUnmount, onMounted, provide, shallowRef, ref } from 'vue';
+import type { MenuItem } from '@/types/menu.js';
 import contains from '@/scripts/contains.js';
 import * as os from '@/os.js';
-import { MenuItem } from '@/types/menu.js';
 import { i18n } from '@/i18n.js';
 import { defaultStore } from '@/store.js';
 
+type WindowButton = {
+	title: string;
+	icon: string;
+	onClick: () => void;
+	highlighted?: boolean;
+};
+
 const minHeight = 50;
 const minWidth = 250;
 
@@ -87,8 +94,8 @@ const props = withDefaults(defineProps<{
 	mini?: boolean;
 	front?: boolean;
 	contextmenu?: MenuItem[] | null;
-	buttonsLeft?: any[];
-	buttonsRight?: any[];
+	buttonsLeft?: WindowButton[];
+	buttonsRight?: WindowButton[];
 }>(), {
 	initialWidth: 400,
 	initialHeight: null,
@@ -484,6 +491,10 @@ defineExpose({
 }
 
 .root {
+	// universal.vueとかで直接--MI-stickyBottomが定義されていたりするのでリセット
+	--MI-stickyTop: 0;
+	--MI-stickyBottom: 0;
+
 	position: fixed;
 	top: 0;
 	left: 0;
@@ -502,28 +513,28 @@ defineExpose({
 	contain: content;
 	width: 100%;
 	height: 100%;
-	border-radius: var(--radius);
+	border-radius: var(--MI-radius);
 }
 
 .header {
 	--height: 39px;
 
-	&.mini {
-		--height: 32px;
-	}
-
 	display: flex;
 	position: relative;
 	z-index: 1;
 	flex-shrink: 0;
 	user-select: none;
 	height: var(--height);
-	background: var(--windowHeader);
-	-webkit-backdrop-filter: var(--blur, blur(15px));
-	backdrop-filter: var(--blur, blur(15px));
-	//border-bottom: solid 1px var(--divider);
+	background: var(--MI_THEME-windowHeader);
+	-webkit-backdrop-filter: var(--MI-blur, blur(15px));
+	backdrop-filter: var(--MI-blur, blur(15px));
+	//border-bottom: solid 1px var(--MI_THEME-divider);
 	font-size: 90%;
 	font-weight: bold;
+
+	&.mini {
+		--height: 32px;
+	}
 }
 
 .headerButton {
@@ -531,11 +542,11 @@ defineExpose({
 	width: var(--height);
 
 	&:hover {
-		color: var(--fgHighlighted);
+		color: var(--MI_THEME-fgHighlighted);
 	}
 
 	&.highlighted {
-		color: var(--accent);
+		color: var(--MI_THEME-accent);
 	}
 }
 
@@ -560,7 +571,7 @@ defineExpose({
 .content {
 	flex: 1;
 	overflow: auto;
-	background: var(--panel);
+	background: var(--MI_THEME-panel);
 	container-type: size;
 }
 
diff --git a/packages/frontend/src/components/MkYouTubePlayer.vue b/packages/frontend/src/components/MkYouTubePlayer.vue
index 1fad222fc5..1122976436 100644
--- a/packages/frontend/src/components/MkYouTubePlayer.vue
+++ b/packages/frontend/src/components/MkYouTubePlayer.vue
@@ -13,7 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 	<div class="poamfof">
 		<Transition :name="defaultStore.state.animation ? 'fade' : ''" mode="out-in">
 			<div v-if="player.url && (player.url.startsWith('http://') || player.url.startsWith('https://'))" class="player">
-				<iframe v-if="!fetching" :src="player.url + (player.url.match(/\?/) ? '&autoplay=1&auto_play=1' : '?autoplay=1&auto_play=1')" frameborder="0" allow="autoplay; encrypted-media" allowfullscreen/>
+				<iframe v-if="!fetching" :src="transformPlayerUrl(player.url)" frameborder="0" allow="autoplay; encrypted-media" allowfullscreen></iframe>
 			</div>
 			<span v-else>invalid url</span>
 		</Transition>
@@ -26,7 +26,8 @@ SPDX-License-Identifier: AGPL-3.0-only
 <script lang="ts" setup>
 import { ref } from 'vue';
 import MkWindow from '@/components/MkWindow.vue';
-import { versatileLang } from '@/scripts/intl-const.js';
+import { versatileLang } from '@@/js/intl-const.js';
+import { transformPlayerUrl } from '@/scripts/player-url-transform.js';
 import { defaultStore } from '@/store.js';
 
 const props = defineProps<{
diff --git a/packages/frontend/src/components/form/link.vue b/packages/frontend/src/components/form/link.vue
index e76ed9a849..8fa9e4affb 100644
--- a/packages/frontend/src/components/form/link.vue
+++ b/packages/frontend/src/components/form/link.vue
@@ -51,18 +51,18 @@ const props = defineProps<{
 	width: 100%;
 	box-sizing: border-box;
 	padding: 10px 14px;
-	background: var(--buttonBg);
+	background: var(--MI_THEME-folderHeaderBg);
 	border-radius: 6px;
 	font-size: 0.9em;
 
 	&:hover {
 		text-decoration: none;
-		background: var(--buttonHoverBg);
+		background: var(--MI_THEME-folderHeaderHoverBg);
 	}
 
 	&.active {
-		color: var(--accent);
-		background: var(--buttonHoverBg);
+		color: var(--MI_THEME-accent);
+		background: var(--MI_THEME-folderHeaderHoverBg);
 	}
 }
 
@@ -70,7 +70,7 @@ const props = defineProps<{
 	margin-right: 0.75em;
 	flex-shrink: 0;
 	text-align: center;
-	color: var(--fgTransparentWeak);
+	color: var(--MI_THEME-fgTransparentWeak);
 
 	&:empty {
 		display: none;
diff --git a/packages/frontend/src/components/form/section.vue b/packages/frontend/src/components/form/section.vue
index ad37daa265..5fca3acc31 100644
--- a/packages/frontend/src/components/form/section.vue
+++ b/packages/frontend/src/components/form/section.vue
@@ -21,8 +21,8 @@ defineProps<{
 
 <style lang="scss" module>
 .root {
-	border-top: solid 0.5px var(--divider);
-	//border-bottom: solid 0.5px var(--divider);
+	border-top: solid 0.5px var(--MI_THEME-divider);
+	//border-bottom: solid 0.5px var(--MI_THEME-divider);
 }
 
 .rootFirst {
@@ -49,7 +49,7 @@ defineProps<{
 
 .description {
 	font-size: 0.85em;
-	color: var(--fgTransparentWeak);
+	color: var(--MI_THEME-fgTransparentWeak);
 	margin: 0 0 8px 0;
 }
 </style>
diff --git a/packages/frontend/src/components/form/slot.vue b/packages/frontend/src/components/form/slot.vue
index f54db0ca82..da94b7abbb 100644
--- a/packages/frontend/src/components/form/slot.vue
+++ b/packages/frontend/src/components/form/slot.vue
@@ -35,7 +35,7 @@ function focus() {
 .caption {
 	font-size: 0.85em;
 	padding: 8px 0 0 0;
-	color: var(--fgTransparentWeak);
+	color: var(--MI_THEME-fgTransparentWeak);
 
 	&:empty {
 		display: none;
diff --git a/packages/frontend/src/components/form/suspense.vue b/packages/frontend/src/components/form/suspense.vue
index 5226c61d68..821f07510b 100644
--- a/packages/frontend/src/components/form/suspense.vue
+++ b/packages/frontend/src/components/form/suspense.vue
@@ -18,19 +18,19 @@ SPDX-License-Identifier: AGPL-3.0-only
 </div>
 </template>
 
-<script lang="ts" setup>
+<script lang="ts" setup generic="T extends unknown">
 import { ref, watch } from 'vue';
 import MkButton from '@/components/MkButton.vue';
 import { i18n } from '@/i18n.js';
 
 const props = defineProps<{
-	p: () => Promise<any>;
+	p: () => Promise<T>;
 }>();
 
 const pending = ref(true);
 const resolved = ref(false);
 const rejected = ref(false);
-const result = ref<any>(null);
+const result = ref<T | null>(null);
 
 const process = () => {
 	if (props.p == null) {
diff --git a/packages/frontend/src/components/global/MkA.stories.impl.ts b/packages/frontend/src/components/global/MkA.stories.impl.ts
index c1d8cf0ca6..02e5a7f98c 100644
--- a/packages/frontend/src/components/global/MkA.stories.impl.ts
+++ b/packages/frontend/src/components/global/MkA.stories.impl.ts
@@ -35,12 +35,10 @@ export const Default = {
 		// FIXME: 通るけどその後落ちるのでコメントアウト
 		// await expect(a.href).toMatch(/^https?:\/\/.*#test$/);
 		await userEvent.pointer({ keys: '[MouseRight]', target: a });
-		await tick();
 		const menu = canvas.getByRole('menu');
 		await expect(menu).toBeInTheDocument();
 		await userEvent.click(a);
 		a.blur();
-		await tick();
 		await expect(menu).not.toBeInTheDocument();
 	},
 	args: {
diff --git a/packages/frontend/src/components/global/MkA.vue b/packages/frontend/src/components/global/MkA.vue
index d1e9113c48..87fa9c8252 100644
--- a/packages/frontend/src/components/global/MkA.vue
+++ b/packages/frontend/src/components/global/MkA.vue
@@ -16,8 +16,8 @@ export type MkABehavior = 'window' | 'browser' | null;
 <script lang="ts" setup>
 import { computed, inject, shallowRef } from 'vue';
 import * as os from '@/os.js';
-import copyToClipboard from '@/scripts/copy-to-clipboard.js';
-import { url } from '@/config.js';
+import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
+import { url } from '@@/js/config.js';
 import { i18n } from '@/i18n.js';
 import { useRouter } from '@/router/supplier.js';
 
diff --git a/packages/frontend/src/components/global/MkAcct.vue b/packages/frontend/src/components/global/MkAcct.vue
index 8cb082585b..9a1ac3aca2 100644
--- a/packages/frontend/src/components/global/MkAcct.vue
+++ b/packages/frontend/src/components/global/MkAcct.vue
@@ -4,11 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 -->
 
 <template>
-<MkCondensedLine v-if="defaultStore.state.enableCondensedLineForAcct" :minScale="2 / 3">
-	<span>@{{ user.username }}</span>
-	<span v-if="user.host || detail || defaultStore.state.showFullAcct" style="opacity: 0.5;">@{{ user.host || host }}</span>
-</MkCondensedLine>
-<span v-else>
+<span>
 	<span>@{{ user.username }}</span>
 	<span v-if="user.host || detail || defaultStore.state.showFullAcct" style="opacity: 0.5;">@{{ user.host || host }}</span>
 </span>
@@ -17,11 +13,11 @@ SPDX-License-Identifier: AGPL-3.0-only
 <script lang="ts" setup>
 import * as Misskey from 'misskey-js';
 import { toUnicode } from 'punycode/';
-import { host as hostRaw } from '@/config.js';
+import { host as hostRaw } from '@@/js/config.js';
 import { defaultStore } from '@/store.js';
 
 defineProps<{
-	user: Misskey.entities.User;
+	user: Misskey.entities.UserLite;
 	detail?: boolean;
 }>();
 
diff --git a/packages/frontend/src/components/global/MkAd.stories.impl.ts b/packages/frontend/src/components/global/MkAd.stories.impl.ts
index aef26ab92d..8c0b7ef52f 100644
--- a/packages/frontend/src/components/global/MkAd.stories.impl.ts
+++ b/packages/frontend/src/components/global/MkAd.stories.impl.ts
@@ -9,12 +9,6 @@ import { StoryObj } from '@storybook/vue3';
 import MkAd from './MkAd.vue';
 import { i18n } from '@/i18n.js';
 
-let lock: Promise<undefined> | undefined;
-
-function sleep(ms: number) {
-	return new Promise(resolve => setTimeout(resolve, ms));
-}
-
 const common = {
 	render(args) {
 		return {
@@ -37,56 +31,41 @@ const common = {
 		};
 	},
 	async play({ canvasElement, args }) {
-		if (lock) {
-			console.warn('This test is unexpectedly running twice in parallel, fix it!');
-			console.warn('See also: https://github.com/misskey-dev/misskey/issues/11267');
-			await lock;
+		const canvas = within(canvasElement);
+		const a = canvas.getByRole<HTMLAnchorElement>('link');
+		// FIXME: 通るけどその後落ちるのでコメントアウト
+		// await expect(a.href).toMatch(/^https?:\/\/.*#test$/);
+		const img = within(a).getByRole('img');
+		await expect(img).toBeInTheDocument();
+		let buttons = canvas.getAllByRole<HTMLButtonElement>('button');
+		await expect(buttons).toHaveLength(1);
+		const i = buttons[0];
+		await expect(i).toBeInTheDocument();
+		await userEvent.click(i);
+		await expect(canvasElement).toHaveTextContent(i18n.ts._ad.back);
+		await expect(a).not.toBeInTheDocument();
+		await expect(i).not.toBeInTheDocument();
+		buttons = canvas.getAllByRole<HTMLButtonElement>('button');
+		const hasReduceFrequency = args.specify?.ratio !== 0;
+		await expect(buttons).toHaveLength(hasReduceFrequency ? 2 : 1);
+		const reduce = hasReduceFrequency ? buttons[0] : null;
+		const back = buttons[hasReduceFrequency ? 1 : 0];
+		if (reduce) {
+			await expect(reduce).toBeInTheDocument();
+			await expect(reduce).toHaveTextContent(i18n.ts._ad.reduceFrequencyOfThisAd);
 		}
-
-		let resolve: (value?: any) => void;
-		lock = new Promise(r => resolve = r);
-
-		try {
-			// NOTE: sleep しないと何故か落ちる
-			await sleep(100);
-			const canvas = within(canvasElement);
-			const a = canvas.getByRole<HTMLAnchorElement>('link');
-			// await expect(a.href).toMatch(/^https?:\/\/.*#test$/);
-			const img = within(a).getByRole('img');
-			await expect(img).toBeInTheDocument();
-			let buttons = canvas.getAllByRole<HTMLButtonElement>('button');
-			await expect(buttons).toHaveLength(1);
-			const i = buttons[0];
-			await expect(i).toBeInTheDocument();
-			await userEvent.click(i);
-			await expect(canvasElement).toHaveTextContent(i18n.ts._ad.back);
-			await expect(a).not.toBeInTheDocument();
-			await expect(i).not.toBeInTheDocument();
-			buttons = canvas.getAllByRole<HTMLButtonElement>('button');
-			const hasReduceFrequency = args.specify?.ratio !== 0;
-			await expect(buttons).toHaveLength(hasReduceFrequency ? 2 : 1);
-			const reduce = hasReduceFrequency ? buttons[0] : null;
-			const back = buttons[hasReduceFrequency ? 1 : 0];
-			if (reduce) {
-				await expect(reduce).toBeInTheDocument();
-				await expect(reduce).toHaveTextContent(i18n.ts._ad.reduceFrequencyOfThisAd);
-			}
-			await expect(back).toBeInTheDocument();
-			await expect(back).toHaveTextContent(i18n.ts._ad.back);
-			await userEvent.click(back);
-			await waitFor(() => expect(canvas.queryByRole('img')).toBeTruthy());
-			if (reduce) {
-				await expect(reduce).not.toBeInTheDocument();
-			}
-			await expect(back).not.toBeInTheDocument();
-			const aAgain = canvas.getByRole<HTMLAnchorElement>('link');
-			await expect(aAgain).toBeInTheDocument();
-			const imgAgain = within(aAgain).getByRole('img');
-			await expect(imgAgain).toBeInTheDocument();
-		} finally {
-			resolve!();
-			lock = undefined;
+		await expect(back).toBeInTheDocument();
+		await expect(back).toHaveTextContent(i18n.ts._ad.back);
+		await userEvent.click(back);
+		await waitFor(() => expect(canvas.queryByRole('img')).toBeTruthy());
+		if (reduce) {
+			await expect(reduce).not.toBeInTheDocument();
 		}
+		await expect(back).not.toBeInTheDocument();
+		const aAgain = canvas.getByRole<HTMLAnchorElement>('link');
+		await expect(aAgain).toBeInTheDocument();
+		const imgAgain = within(aAgain).getByRole('img');
+		await expect(imgAgain).toBeInTheDocument();
 	},
 	args: {
 		prefer: [],
diff --git a/packages/frontend/src/components/global/MkAd.vue b/packages/frontend/src/components/global/MkAd.vue
index bdaa8a809f..08a78c8d81 100644
--- a/packages/frontend/src/components/global/MkAd.vue
+++ b/packages/frontend/src/components/global/MkAd.vue
@@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 -->
 
 <template>
-<div v-if="chosen && !shouldHide" :class="$style.root">
+<div v-if="chosen && !shouldHide">
 	<div
 		v-if="!showMenu"
 		:class="[$style.main, {
@@ -30,12 +30,10 @@ SPDX-License-Identifier: AGPL-3.0-only
 		</component>
 	</div>
 	<div v-else :class="$style.menu">
-		<div :class="$style.menuContainer">
-			<div>Ads by {{ host }}</div>
-			<!--<MkButton class="button" primary>{{ i18n.ts._ad.like }}</MkButton>-->
-			<MkButton v-if="chosen.ratio !== 0" :class="$style.menuButton" @click="reduceFrequency">{{ i18n.ts._ad.reduceFrequencyOfThisAd }}</MkButton>
-			<button class="_textButton" @click="toggleMenu">{{ i18n.ts._ad.back }}</button>
-		</div>
+		<div>Ads by {{ host }}</div>
+		<!--<MkButton class="button" primary>{{ i18n.ts._ad.like }}</MkButton>-->
+		<MkButton v-if="chosen.ratio !== 0" :class="$style.menuButton" @click="reduceFrequency">{{ i18n.ts._ad.reduceFrequencyOfThisAd }}</MkButton>
+		<button class="_textButton" @click="toggleMenu">{{ i18n.ts._ad.back }}</button>
 	</div>
 </div>
 <div v-else></div>
@@ -43,9 +41,9 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <script lang="ts" setup>
 import { ref, computed } from 'vue';
+import { url as local, host } from '@@/js/config.js';
 import { i18n } from '@/i18n.js';
 import { instance } from '@/instance.js';
-import { url as local, host } from '@/config.js';
 import MkButton from '@/components/MkButton.vue';
 import { defaultStore } from '@/store.js';
 import * as os from '@/os.js';
@@ -122,11 +120,6 @@ function reduceFrequency(): void {
 </script>
 
 <style lang="scss" module>
-.root {
-	background-size: auto auto;
-	background-image: repeating-linear-gradient(45deg, transparent, transparent 8px, var(--ad) 8px, var(--ad) 14px );
-}
-
 .main {
 	text-align: center;
 
@@ -139,8 +132,6 @@ function reduceFrequency(): void {
 	}
 
 	&.form_horizontal {
-		padding: 8px;
-
 		> .link,
 		> .link > .img {
 			max-width: min(600px, 100%);
@@ -149,8 +140,6 @@ function reduceFrequency(): void {
 	}
 
 	&.form_horizontalBig {
-		padding: 8px;
-
 		> .link,
 		> .link > .img {
 			max-width: min(600px, 100%);
@@ -191,7 +180,7 @@ function reduceFrequency(): void {
 	right: 1px;
 	display: grid;
 	place-content: center;
-	background: var(--panel);
+	background: var(--MI_THEME-panel);
 	border-radius: 100%;
 	padding: 2px;
 }
@@ -202,15 +191,12 @@ function reduceFrequency(): void {
 }
 
 .menu {
-	padding: 8px;
 	text-align: center;
-}
-
-.menuContainer {
 	padding: 8px;
 	margin: 0 auto;
 	max-width: 400px;
-	border: solid 1px var(--divider);
+	background: var(--MI_THEME-panel);
+	border: solid 1px var(--MI_THEME-divider);
 }
 
 .menuButton {
diff --git a/packages/frontend/src/components/global/MkAvatar.vue b/packages/frontend/src/components/global/MkAvatar.vue
index e8e1bc696b..35c07bc80c 100644
--- a/packages/frontend/src/components/global/MkAvatar.vue
+++ b/packages/frontend/src/components/global/MkAvatar.vue
@@ -26,7 +26,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 	<template v-if="showDecoration">
 		<img
 			v-for="decoration in decorations ?? user.avatarDecorations"
-			:class="[$style.decoration]"
+			:class="[$style.decoration, { [$style.decorationBlink]: decoration.blink }]"
 			:src="getDecorationUrl(decoration)"
 			:style="{
 				rotate: getDecorationAngle(decoration),
@@ -42,10 +42,10 @@ SPDX-License-Identifier: AGPL-3.0-only
 <script lang="ts" setup>
 import { watch, ref, computed } from 'vue';
 import * as Misskey from 'misskey-js';
+import { extractAvgColorFromBlurhash } from '@@/js/extract-avg-color-from-blurhash.js';
 import MkImgWithBlurhash from '../MkImgWithBlurhash.vue';
 import MkA from './MkA.vue';
 import { getStaticImageUrl } from '@/scripts/media-proxy.js';
-import { extractAvgColorFromBlurhash } from '@/scripts/extract-avg-color-from-blurhash.js';
 import { acct, userPage } from '@/filters/user.js';
 import MkUserOnlineIndicator from '@/components/MkUserOnlineIndicator.vue';
 import { defaultStore } from '@/store.js';
@@ -60,7 +60,7 @@ const props = withDefaults(defineProps<{
 	link?: boolean;
 	preview?: boolean;
 	indicator?: boolean;
-	decorations?: Omit<Misskey.entities.UserDetailed['avatarDecorations'][number], 'id'>[];
+	decorations?: (Omit<Misskey.entities.UserDetailed['avatarDecorations'][number], 'id'> & { blink?: boolean; })[];
 	forceShowDecoration?: boolean;
 }>(), {
 	target: null,
@@ -330,4 +330,17 @@ watch(() => props.user.avatarBlurhash, () => {
 	width: 200%;
 	pointer-events: none;
 }
+
+.decorationBlink {
+	animation: blink 1s infinite;
+}
+
+@keyframes blink {
+	0%, 100% {
+		filter: brightness(2);
+	}
+	50% {
+		filter: brightness(1);
+	}
+}
 </style>
diff --git a/packages/frontend/src/components/global/MkCondensedLine.vue b/packages/frontend/src/components/global/MkCondensedLine.vue
index 7c4957d77f..473d444c16 100644
--- a/packages/frontend/src/components/global/MkCondensedLine.vue
+++ b/packages/frontend/src/components/global/MkCondensedLine.vue
@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <template>
 <span :class="$style.container">
-	<span ref="content" :class="$style.content">
+	<span ref="content" :class="$style.content" :style="{ maxWidth: `${100 / minScale}%` }">
 		<slot/>
 	</span>
 </span>
diff --git a/packages/frontend/src/components/global/MkCustomEmoji.vue b/packages/frontend/src/components/global/MkCustomEmoji.vue
index 6123835340..66f82a7898 100644
--- a/packages/frontend/src/components/global/MkCustomEmoji.vue
+++ b/packages/frontend/src/components/global/MkCustomEmoji.vue
@@ -31,10 +31,11 @@ import { defaultStore } from '@/store.js';
 import { customEmojisMap } from '@/custom-emojis.js';
 import * as os from '@/os.js';
 import { misskeyApiGet } from '@/scripts/misskey-api.js';
-import copyToClipboard from '@/scripts/copy-to-clipboard.js';
+import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
 import * as sound from '@/scripts/sound.js';
 import { i18n } from '@/i18n.js';
 import MkCustomEmojiDetailedDialog from '@/components/MkCustomEmojiDetailedDialog.vue';
+import type { MenuItem } from '@/types/menu.js';
 
 const props = defineProps<{
 	name: string;
@@ -85,7 +86,9 @@ const errored = ref(url.value == null);
 
 function onClick(ev: MouseEvent) {
 	if (props.menu) {
-		os.popupMenu([{
+		const menuItems: MenuItem[] = [];
+
+		menuItems.push({
 			type: 'label',
 			text: `:${props.name}:`,
 		}, {
@@ -95,26 +98,34 @@ function onClick(ev: MouseEvent) {
 				copyToClipboard(`:${props.name}:`);
 				os.success();
 			},
-		}, ...(props.menuReaction && react ? [{
-			text: i18n.ts.doReaction,
-			icon: 'ti ti-plus',
-			action: () => {
-				react(`:${props.name}:`);
-				sound.playMisskeySfx('reaction');
-			},
-		}] : []), {
+		});
+
+		if (props.menuReaction && react) {
+			menuItems.push({
+				text: i18n.ts.doReaction,
+				icon: 'ti ti-plus',
+				action: () => {
+					react(`:${props.name}:`);
+					sound.playMisskeySfx('reaction');
+				},
+			});
+		}
+
+		menuItems.push({
 			text: i18n.ts.info,
 			icon: 'ti ti-info-circle',
 			action: async () => {
-				os.popup(MkCustomEmojiDetailedDialog, {
+				const { dispose } = os.popup(MkCustomEmojiDetailedDialog, {
 					emoji: await misskeyApiGet('emoji', {
 						name: customEmojiName.value,
 					}),
 				}, {
-					anchor: ev.target,
+					closed: () => dispose(),
 				});
 			},
-		}], ev.currentTarget ?? ev.target);
+		});
+
+		os.popupMenu(menuItems, ev.currentTarget ?? ev.target);
 	}
 }
 </script>
diff --git a/packages/frontend/src/components/global/MkEmoji.vue b/packages/frontend/src/components/global/MkEmoji.vue
index 65f75642b2..f0acd3bc27 100644
--- a/packages/frontend/src/components/global/MkEmoji.vue
+++ b/packages/frontend/src/components/global/MkEmoji.vue
@@ -10,13 +10,14 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <script lang="ts" setup>
 import { computed, inject } from 'vue';
-import { char2fluentEmojiFilePath, char2twemojiFilePath } from '@/scripts/emoji-base.js';
+import { colorizeEmoji, getEmojiName } from '@@/js/emojilist.js';
+import { char2fluentEmojiFilePath, char2twemojiFilePath } from '@@/js/emoji-base.js';
 import { defaultStore } from '@/store.js';
-import { colorizeEmoji, getEmojiName } from '@/scripts/emojilist.js';
 import * as os from '@/os.js';
-import copyToClipboard from '@/scripts/copy-to-clipboard.js';
+import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
 import * as sound from '@/scripts/sound.js';
 import { i18n } from '@/i18n.js';
+import type { MenuItem } from '@/types/menu.js';
 
 const props = defineProps<{
 	emoji: string;
@@ -39,7 +40,9 @@ function computeTitle(event: PointerEvent): void {
 
 function onClick(ev: MouseEvent) {
 	if (props.menu) {
-		os.popupMenu([{
+		const menuItems: MenuItem[] = [];
+
+		menuItems.push({
 			type: 'label',
 			text: props.emoji,
 		}, {
@@ -49,14 +52,20 @@ function onClick(ev: MouseEvent) {
 				copyToClipboard(props.emoji);
 				os.success();
 			},
-		}, ...(props.menuReaction && react ? [{
-			text: i18n.ts.doReaction,
-			icon: 'ti ti-plus',
-			action: () => {
-				react(props.emoji);
-				sound.playMisskeySfx('reaction');
-			},
-		}] : [])], ev.currentTarget ?? ev.target);
+		});
+
+		if (props.menuReaction && react) {
+			menuItems.push({
+				text: i18n.ts.doReaction,
+				icon: 'ti ti-plus',
+				action: () => {
+					react(props.emoji);
+					sound.playMisskeySfx('reaction');
+				},
+			});
+		}
+
+		os.popupMenu(menuItems, ev.currentTarget ?? ev.target);
 	}
 }
 </script>
diff --git a/packages/frontend/src/components/global/MkLoading.vue b/packages/frontend/src/components/global/MkLoading.vue
index 49d8ace37b..47d797606b 100644
--- a/packages/frontend/src/components/global/MkLoading.vue
+++ b/packages/frontend/src/components/global/MkLoading.vue
@@ -56,7 +56,7 @@ const props = withDefaults(defineProps<{
 	--size: 38px;
 
 	&.colored {
-		color: var(--accent);
+		color: var(--MI_THEME-accent);
 	}
 
 	&.inline {
diff --git a/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.stories.impl.ts b/packages/frontend/src/components/global/MkMfm.stories.impl.ts
similarity index 78%
rename from packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.stories.impl.ts
rename to packages/frontend/src/components/global/MkMfm.stories.impl.ts
index 730351f795..1daf7a29cb 100644
--- a/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.stories.impl.ts
+++ b/packages/frontend/src/components/global/MkMfm.stories.impl.ts
@@ -2,16 +2,15 @@
  * SPDX-FileCopyrightText: syuilo and misskey-project
  * SPDX-License-Identifier: AGPL-3.0-only
  */
-
-/* eslint-disable @typescript-eslint/explicit-function-return-type */
+ 
 import { StoryObj } from '@storybook/vue3';
 import { expect, within } from '@storybook/test';
-import MkMisskeyFlavoredMarkdown from './MkMisskeyFlavoredMarkdown.js';
+import MkMfm from './MkMfm.js';
 export const Default = {
 	render(args) {
 		return {
 			components: {
-				MkMisskeyFlavoredMarkdown,
+				MkMfm,
 			},
 			setup() {
 				return {
@@ -25,7 +24,7 @@ export const Default = {
 					};
 				},
 			},
-			template: '<MkMisskeyFlavoredMarkdown v-bind="props" />',
+			template: '<MkMfm v-bind="props" />',
 		};
 	},
 	async play({ canvasElement, args }) {
@@ -54,25 +53,25 @@ export const Default = {
 	parameters: {
 		layout: 'centered',
 	},
-} satisfies StoryObj<typeof MkMisskeyFlavoredMarkdown>;
+} satisfies StoryObj<typeof MkMfm>;
 export const Plain = {
 	...Default,
 	args: {
 		...Default.args,
 		plain: true,
 	},
-} satisfies StoryObj<typeof MkMisskeyFlavoredMarkdown>;
+} satisfies StoryObj<typeof MkMfm>;
 export const Nowrap = {
 	...Default,
 	args: {
 		...Default.args,
 		nowrap: true,
 	},
-} satisfies StoryObj<typeof MkMisskeyFlavoredMarkdown>;
+} satisfies StoryObj<typeof MkMfm>;
 export const IsNotNote = {
 	...Default,
 	args: {
 		...Default.args,
 		isNote: false,
 	},
-} satisfies StoryObj<typeof MkMisskeyFlavoredMarkdown>;
+} satisfies StoryObj<typeof MkMfm>;
diff --git a/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.ts b/packages/frontend/src/components/global/MkMfm.ts
similarity index 92%
rename from packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.ts
rename to packages/frontend/src/components/global/MkMfm.ts
index cab8d9c704..0d138d1f1c 100644
--- a/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.ts
+++ b/packages/frontend/src/components/global/MkMfm.ts
@@ -6,6 +6,7 @@
 import { VNode, h, SetupContext, provide } from 'vue';
 import * as mfm from 'mfm-js';
 import * as Misskey from 'misskey-js';
+import { host } from '@@/js/config.js';
 import MkUrl from '@/components/global/MkUrl.vue';
 import MkTime from '@/components/global/MkTime.vue';
 import MkLink from '@/components/MkLink.vue';
@@ -17,17 +18,21 @@ import MkCodeInline from '@/components/MkCodeInline.vue';
 import MkGoogle from '@/components/MkGoogle.vue';
 import MkSparkle from '@/components/MkSparkle.vue';
 import MkA, { MkABehavior } from '@/components/global/MkA.vue';
-import { host } from '@/config.js';
 import { defaultStore } from '@/store.js';
-import { nyaize as doNyaize } from '@/scripts/nyaize.js';
-import { safeParseFloat } from '@/scripts/safe-parse.js';
+
+function safeParseFloat(str: unknown): number | null {
+	if (typeof str !== 'string' || str === '') return null;
+	const num = parseFloat(str);
+	if (isNaN(num)) return null;
+	return num;
+}
 
 const QUOTE_STYLE = `
 display: block;
 margin: 8px;
 padding: 6px 0 6px 12px;
-color: var(--fg);
-border-left: solid 3px var(--fg);
+color: var(--MI_THEME-fg);
+border-left: solid 3px var(--MI_THEME-fg);
 opacity: 0.7;
 `.split('\n').join(' ');
 
@@ -52,7 +57,8 @@ type MfmEvents = {
 
 // eslint-disable-next-line import/no-default-export
 export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEvents>['emit'] }) {
-	provide('linkNavigationBehavior', props.linkNavigationBehavior);
+	// こうしたいところだけど functional component 内では provide は使えない
+	//provide('linkNavigationBehavior', props.linkNavigationBehavior);
 
 	const isNote = props.isNote ?? true;
 	const shouldNyaize = props.nyaize ? props.nyaize === 'respect' ? props.author?.isCat : false : false;
@@ -65,7 +71,7 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEven
 	const validTime = (t: string | boolean | null | undefined) => {
 		if (t == null) return null;
 		if (typeof t === 'boolean') return null;
-		return t.match(/^[0-9.]+s$/) ? t : null;
+		return t.match(/^\-?[0-9.]+s$/) ? t : null;
 	};
 
 	const validColor = (c: unknown): string | null => {
@@ -86,7 +92,7 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEven
 			case 'text': {
 				let text = token.props.text.replace(/(\r\n|\n|\r)/g, '\n');
 				if (!disableNyaize && shouldNyaize) {
-					text = doNyaize(text);
+					text = Misskey.nyaize(text);
 				}
 
 				if (!props.plain) {
@@ -264,7 +270,7 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEven
 					}
 					case 'border': {
 						let color = validColor(token.props.args.color);
-						color = color ? `#${color}` : 'var(--accent)';
+						color = color ? `#${color}` : 'var(--MI_THEME-accent)';
 						let b_style = token.props.args.style;
 						if (
 							typeof b_style !== 'string' ||
@@ -281,14 +287,14 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEven
 							const child = token.children[0];
 							let text = child.type === 'text' ? child.props.text : '';
 							if (!disableNyaize && shouldNyaize) {
-								text = doNyaize(text);
+								text = Misskey.nyaize(text);
 							}
 							return h('ruby', {}, [text.split(' ')[0], h('rt', text.split(' ')[1])]);
 						} else {
 							const rt = token.children.at(-1)!;
 							let text = rt.type === 'text' ? rt.props.text : '';
 							if (!disableNyaize && shouldNyaize) {
-								text = doNyaize(text);
+								text = Misskey.nyaize(text);
 							}
 							return h('ruby', {}, [...genEl(token.children.slice(0, token.children.length - 1), scale), h('rt', text.trim())]);
 						}
@@ -297,7 +303,7 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEven
 						const child = token.children[0];
 						const unixtime = parseInt(child.type === 'text' ? child.props.text : '');
 						return h('span', {
-							style: 'display: inline-block; font-size: 90%; border: solid 1px var(--divider); border-radius: 999px; padding: 4px 10px 4px 6px;',
+							style: 'display: inline-block; font-size: 90%; border: solid 1px var(--MI_THEME-divider); border-radius: 999px; padding: 4px 10px 4px 6px;',
 						}, [
 							h('i', {
 								class: 'ti ti-clock',
@@ -345,6 +351,7 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEven
 					key: Math.random(),
 					url: token.props.url,
 					rel: 'nofollow noopener',
+					navigationBehavior: props.linkNavigationBehavior,
 				})];
 			}
 
@@ -353,6 +360,7 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEven
 					key: Math.random(),
 					url: token.props.url,
 					rel: 'nofollow noopener',
+					navigationBehavior: props.linkNavigationBehavior,
 				}, genEl(token.children, scale, true))];
 			}
 
@@ -361,6 +369,7 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEven
 					key: Math.random(),
 					host: (token.props.host == null && props.author && props.author.host != null ? props.author.host : token.props.host) ?? host,
 					username: token.props.username,
+					navigationBehavior: props.linkNavigationBehavior,
 				})];
 			}
 
@@ -368,7 +377,8 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEven
 				return [h(MkA, {
 					key: Math.random(),
 					to: isNote ? `/tags/${encodeURIComponent(token.props.hashtag)}` : `/user-tags/${encodeURIComponent(token.props.hashtag)}`,
-					style: 'color:var(--hashtag);',
+					style: 'color:var(--MI_THEME-hashtag);',
+					behavior: props.linkNavigationBehavior,
 				}, `#${token.props.hashtag}`)];
 			}
 
@@ -400,7 +410,6 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEven
 			}
 
 			case 'emojiCode': {
-				// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
 				if (props.author?.host == null) {
 					return [h(MkCustomEmoji, {
 						key: Math.random(),
@@ -458,8 +467,8 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEven
 			}
 
 			default: {
-				// eslint-disable-next-line @typescript-eslint/no-explicit-any
-				console.error('unrecognized ast type:', (token as any).type);
+				// @ts-expect-error 存在しないASTタイプ
+				console.error('unrecognized ast type:', token.type);
 
 				return [];
 			}
diff --git a/packages/frontend/src/components/global/MkPageHeader.tabs.vue b/packages/frontend/src/components/global/MkPageHeader.tabs.vue
index fcc46cc345..aaef8b8fca 100644
--- a/packages/frontend/src/components/global/MkPageHeader.tabs.vue
+++ b/packages/frontend/src/components/global/MkPageHeader.tabs.vue
@@ -53,7 +53,7 @@ export type Tab = {
 </script>
 
 <script lang="ts" setup>
-import { onMounted, onUnmounted, watch, nextTick, shallowRef } from 'vue';
+import { nextTick, onMounted, onUnmounted, shallowRef, watch } from 'vue';
 import { defaultStore } from '@/store.js';
 
 const props = withDefaults(defineProps<{
@@ -120,14 +120,14 @@ function onTabWheel(ev: WheelEvent) {
 
 let entering = false;
 
-async function enter(element: Element) {
+async function enter(el: Element) {
+	if (!(el instanceof HTMLElement)) return;
 	entering = true;
-	const el = element as HTMLElement;
 	const elementWidth = el.getBoundingClientRect().width;
 	el.style.width = '0';
 	el.style.paddingLeft = '0';
-	el.offsetWidth; // force reflow
-	el.style.width = elementWidth + 'px';
+	el.offsetWidth; // reflow
+	el.style.width = `${elementWidth}px`;
 	el.style.paddingLeft = '';
 	nextTick(() => {
 		entering = false;
@@ -136,22 +136,23 @@ async function enter(element: Element) {
 	setTimeout(renderTab, 170);
 }
 
-function afterEnter(element: Element) {
-	//el.style.width = '';
+function afterEnter(el: Element) {
+	if (!(el instanceof HTMLElement)) return;
+	// element.style.width = '';
 }
 
-async function leave(element: Element) {
-	const el = element as HTMLElement;
+async function leave(el: Element) {
+	if (!(el instanceof HTMLElement)) return;
 	const elementWidth = el.getBoundingClientRect().width;
-	el.style.width = elementWidth + 'px';
+	el.style.width = `${elementWidth}px`;
 	el.style.paddingLeft = '';
-	el.offsetWidth; // force reflow
+	el.offsetWidth; // reflow
 	el.style.width = '0';
 	el.style.paddingLeft = '0';
 }
 
-function afterLeave(element: Element) {
-	const el = element as HTMLElement;
+function afterLeave(el: Element) {
+	if (!(el instanceof HTMLElement)) return;
 	el.style.width = '';
 }
 
@@ -247,7 +248,7 @@ onUnmounted(() => {
 	position: absolute;
 	bottom: 0;
 	height: 3px;
-	background: var(--accent);
+	background: var(--MI_THEME-accent);
 	border-radius: 999px;
 	transition: none;
 	pointer-events: none;
diff --git a/packages/frontend/src/components/global/MkPageHeader.vue b/packages/frontend/src/components/global/MkPageHeader.vue
index f16d951679..aa4be69b2c 100644
--- a/packages/frontend/src/components/global/MkPageHeader.vue
+++ b/packages/frontend/src/components/global/MkPageHeader.vue
@@ -44,7 +44,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { onMounted, onUnmounted, ref, inject, shallowRef, computed } from 'vue';
 import tinycolor from 'tinycolor2';
 import XTabs, { Tab } from './MkPageHeader.tabs.vue';
-import { scrollToTop } from '@/scripts/scroll.js';
+import { scrollToTop } from '@@/js/scroll.js';
 import { globalEvents } from '@/events.js';
 import { injectReactiveMetadata } from '@/scripts/page-metadata.js';
 import { $i, openAccountMenu as openAccountMenu_ } from '@/account.js';
@@ -99,7 +99,7 @@ function onTabClick(): void {
 }
 
 const calcBg = () => {
-	const rawBg = 'var(--bg)';
+	const rawBg = 'var(--MI_THEME-bg)';
 	const tinyBg = tinycolor(rawBg.startsWith('var(') ? getComputedStyle(document.documentElement).getPropertyValue(rawBg.slice(4, -1)) : rawBg);
 	tinyBg.setAlpha(0.85);
 	bg.value = tinyBg.toRgbString();
@@ -130,9 +130,9 @@ onUnmounted(() => {
 
 <style lang="scss" module>
 .root {
-	-webkit-backdrop-filter: var(--blur, blur(15px));
-	backdrop-filter: var(--blur, blur(15px));
-	border-bottom: solid 0.5px var(--divider);
+	-webkit-backdrop-filter: var(--MI-blur, blur(15px));
+	backdrop-filter: var(--MI-blur, blur(15px));
+	border-bottom: solid 0.5px var(--MI_THEME-divider);
 	width: 100%;
 }
 
@@ -145,7 +145,7 @@ onUnmounted(() => {
 .upper {
 	--height: 50px;
 	display: flex;
-	gap: var(--margin);
+	gap: var(--MI-margin);
 	height: var(--height);
 
 	.tabs:first-child {
@@ -230,7 +230,7 @@ onUnmounted(() => {
 	}
 
 	&.highlighted {
-		color: var(--accent);
+		color: var(--MI_THEME-accent);
 	}
 }
 
diff --git a/packages/frontend/src/components/global/MkStickyContainer.vue b/packages/frontend/src/components/global/MkStickyContainer.vue
index 89993e1b8e..1aebf487bb 100644
--- a/packages/frontend/src/components/global/MkStickyContainer.vue
+++ b/packages/frontend/src/components/global/MkStickyContainer.vue
@@ -5,27 +5,30 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <template>
 <div ref="rootEl">
-	<div ref="headerEl">
+	<div ref="headerEl" :class="$style.header">
 		<slot name="header"></slot>
 	</div>
-	<div ref="bodyEl" :data-sticky-container-header-height="headerHeight">
+	<div
+		:class="$style.body"
+		:data-sticky-container-header-height="headerHeight"
+		:data-sticky-container-footer-height="footerHeight"
+	>
 		<slot></slot>
 	</div>
-	<div ref="footerEl">
+	<div ref="footerEl" :class="$style.footer">
 		<slot name="footer"></slot>
 	</div>
 </div>
 </template>
 
 <script lang="ts" setup>
-import { onMounted, onUnmounted, provide, inject, Ref, ref, watch, shallowRef } from 'vue';
+import { onMounted, onUnmounted, provide, inject, Ref, ref, watch, useTemplateRef } from 'vue';
 
-import { CURRENT_STICKY_BOTTOM, CURRENT_STICKY_TOP } from '@/const.js';
+import { CURRENT_STICKY_BOTTOM, CURRENT_STICKY_TOP } from '@@/js/const.js';
 
-const rootEl = shallowRef<HTMLElement>();
-const headerEl = shallowRef<HTMLElement>();
-const footerEl = shallowRef<HTMLElement>();
-const bodyEl = shallowRef<HTMLElement>();
+const rootEl = useTemplateRef('rootEl');
+const headerEl = useTemplateRef('headerEl');
+const footerEl = useTemplateRef('footerEl');
 
 const headerHeight = ref<string | undefined>();
 const childStickyTop = ref(0);
@@ -62,31 +65,11 @@ onMounted(() => {
 
 	watch([parentStickyTop, parentStickyBottom], calc);
 
-	watch(childStickyTop, () => {
-		if (bodyEl.value == null) return;
-		bodyEl.value.style.setProperty('--stickyTop', `${childStickyTop.value}px`);
-	}, {
-		immediate: true,
-	});
-
-	watch(childStickyBottom, () => {
-		if (bodyEl.value == null) return;
-		bodyEl.value.style.setProperty('--stickyBottom', `${childStickyBottom.value}px`);
-	}, {
-		immediate: true,
-	});
-
 	if (headerEl.value != null) {
-		headerEl.value.style.position = 'sticky';
-		headerEl.value.style.top = 'var(--stickyTop, 0)';
-		headerEl.value.style.zIndex = '1000';
 		observer.observe(headerEl.value);
 	}
 
 	if (footerEl.value != null) {
-		footerEl.value.style.position = 'sticky';
-		footerEl.value.style.bottom = 'var(--stickyBottom, 0)';
-		footerEl.value.style.zIndex = '1000';
 		observer.observe(footerEl.value);
 	}
 });
@@ -96,6 +79,27 @@ onUnmounted(() => {
 });
 
 defineExpose({
-	rootEl: rootEl,
+	rootEl,
 });
 </script>
+
+<style lang='scss' module>
+.body {
+	position: relative;
+	z-index: 0;
+	--MI-stickyTop: v-bind("childStickyTop + 'px'");
+	--MI-stickyBottom: v-bind("childStickyBottom + 'px'");
+}
+
+.header {
+	position: sticky;
+	top: var(--MI-stickyTop, 0);
+	z-index: 1;
+}
+
+.footer {
+	position: sticky;
+	bottom: var(--MI-stickyBottom, 0);
+	z-index: 1;
+}
+</style>
diff --git a/packages/frontend/src/components/global/MkTime.stories.impl.ts b/packages/frontend/src/components/global/MkTime.stories.impl.ts
index ffd4a849a2..ccf7f200b5 100644
--- a/packages/frontend/src/components/global/MkTime.stories.impl.ts
+++ b/packages/frontend/src/components/global/MkTime.stories.impl.ts
@@ -8,7 +8,7 @@ import { expect } from '@storybook/test';
 import { StoryObj } from '@storybook/vue3';
 import MkTime from './MkTime.vue';
 import { i18n } from '@/i18n.js';
-import { dateTimeFormat } from '@/scripts/intl-const.js';
+import { dateTimeFormat } from '@@/js/intl-const.js';
 const now = new Date('2023-04-01T00:00:00.000Z');
 const future = new Date('2024-04-01T00:00:00.000Z');
 const oneHourAgo = new Date(now.getTime() - 3600000);
diff --git a/packages/frontend/src/components/global/MkTime.vue b/packages/frontend/src/components/global/MkTime.vue
index 23fe99bd9c..f600f7eed2 100644
--- a/packages/frontend/src/components/global/MkTime.vue
+++ b/packages/frontend/src/components/global/MkTime.vue
@@ -16,7 +16,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 import isChromatic from 'chromatic/isChromatic';
 import { onMounted, onUnmounted, ref, computed } from 'vue';
 import { i18n } from '@/i18n.js';
-import { dateTimeFormat } from '@/scripts/intl-const.js';
+import { dateTimeFormat } from '@@/js/intl-const.js';
 
 const props = withDefaults(defineProps<{
 	time: Date | string | number | null;
@@ -41,12 +41,12 @@ function getDateSafe(n: Date | string | number) {
 	}
 }
 
-// eslint-disable-next-line vue/no-setup-props-destructure
+// eslint-disable-next-line vue/no-setup-props-reactivity-loss
 const _time = props.time == null ? NaN : getDateSafe(props.time).getTime();
 const invalid = Number.isNaN(_time);
 const absolute = !invalid ? dateTimeFormat.format(_time) : i18n.ts._ago.invalid;
 
-// eslint-disable-next-line vue/no-setup-props-destructure
+// eslint-disable-next-line vue/no-setup-props-reactivity-loss
 const now = ref(props.origin?.getTime() ?? Date.now());
 const ago = computed(() => (now.value - _time) / 1000/*ms*/);
 
@@ -99,10 +99,10 @@ if (!invalid && props.origin === null && (props.mode === 'relative' || props.mod
 
 <style lang="scss" module>
 .old1 {
-	color: var(--warn);
+	color: var(--MI_THEME-warn);
 }
 
 .old1.old2 {
-	color: var(--error);
+	color: var(--MI_THEME-error);
 }
 </style>
diff --git a/packages/frontend/src/components/global/MkUrl.vue b/packages/frontend/src/components/global/MkUrl.vue
index 9d4cd559d9..e789251659 100644
--- a/packages/frontend/src/components/global/MkUrl.vue
+++ b/packages/frontend/src/components/global/MkUrl.vue
@@ -27,13 +27,20 @@ SPDX-License-Identifier: AGPL-3.0-only
 <script lang="ts" setup>
 import { defineAsyncComponent, ref } from 'vue';
 import { toUnicode as decodePunycode } from 'punycode/';
-import { url as local } from '@/config.js';
+import { url as local } from '@@/js/config.js';
 import * as os from '@/os.js';
 import { useTooltip } from '@/scripts/use-tooltip.js';
-import { safeURIDecode } from '@/scripts/safe-uri-decode.js';
 import { isEnabledUrlPreview } from '@/instance.js';
 import { MkABehavior } from '@/components/global/MkA.vue';
 
+function safeURIDecode(str: string): string {
+	try {
+		return decodeURIComponent(str);
+	} catch {
+		return str;
+	}
+}
+
 const props = withDefaults(defineProps<{
 	url: string;
 	rel?: string;
@@ -50,11 +57,13 @@ const el = ref();
 
 if (props.showUrlPreview && isEnabledUrlPreview.value) {
 	useTooltip(el, (showing) => {
-		os.popup(defineAsyncComponent(() => import('@/components/MkUrlPreviewPopup.vue')), {
+		const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkUrlPreviewPopup.vue')), {
 			showing,
 			url: props.url,
 			source: el.value instanceof HTMLElement ? el.value : el.value?.$el,
-		}, {}, 'closed');
+		}, {
+			closed: () => dispose(),
+		});
 	});
 }
 
diff --git a/packages/frontend/src/components/global/RouterView.vue b/packages/frontend/src/components/global/RouterView.vue
index 06cb30eff1..38bdfc52d4 100644
--- a/packages/frontend/src/components/global/RouterView.vue
+++ b/packages/frontend/src/components/global/RouterView.vue
@@ -27,6 +27,7 @@ import MkLoadingPage from '@/pages/_loading_.vue';
 
 const props = defineProps<{
 	router?: IRouter;
+	nested?: boolean;
 }>();
 
 const router = props.router ?? inject('router');
@@ -39,6 +40,8 @@ const currentDepth = inject('routerCurrentDepth', 0);
 provide('routerCurrentDepth', currentDepth + 1);
 
 function resolveNested(current: Resolved, d = 0): Resolved | null {
+	if (!props.nested) return current;
+
 	if (d === currentDepth) {
 		return current;
 	} else {
@@ -53,14 +56,14 @@ function resolveNested(current: Resolved, d = 0): Resolved | null {
 const current = resolveNested(router.current)!;
 const currentPageComponent = shallowRef('component' in current.route ? current.route.component : MkLoadingPage);
 const currentPageProps = ref(current.props);
-const key = ref(current.route.path + JSON.stringify(Object.fromEntries(current.props)));
+const key = ref(router.getCurrentKey() + JSON.stringify(Object.fromEntries(current.props)));
 
 function onChange({ resolved, key: newKey }) {
 	const current = resolveNested(resolved);
 	if (current == null || 'redirect' in current.route) return;
 	currentPageComponent.value = current.route.component;
 	currentPageProps.value = current.props;
-	key.value = current.route.path + JSON.stringify(Object.fromEntries(current.props));
+	key.value = newKey + JSON.stringify(Object.fromEntries(current.props));
 
 	nextTick(() => {
 		// ページ遷移完了後に再びキャッシュを有効化
diff --git a/packages/frontend/src/components/index.ts b/packages/frontend/src/components/index.ts
index 44d8d59941..b36625ed1b 100644
--- a/packages/frontend/src/components/index.ts
+++ b/packages/frontend/src/components/index.ts
@@ -5,7 +5,7 @@
 
 import { App } from 'vue';
 
-import Mfm from './global/MkMisskeyFlavoredMarkdown.js';
+import Mfm from './global/MkMfm.js';
 import MkA from './global/MkA.vue';
 import MkAcct from './global/MkAcct.vue';
 import MkAvatar from './global/MkAvatar.vue';
diff --git a/packages/frontend/src/components/page/page.dynamic.vue b/packages/frontend/src/components/page/page.dynamic.vue
index 8c511a690d..c2449931c1 100644
--- a/packages/frontend/src/components/page/page.dynamic.vue
+++ b/packages/frontend/src/components/page/page.dynamic.vue
@@ -27,9 +27,9 @@ const props = defineProps<{
 
 <style lang="scss" module>
 .root {
-	border: 1px solid var(--divider);
-	border-radius: var(--radius);
-	padding: var(--margin);
+	border: 1px solid var(--MI_THEME-divider);
+	border-radius: var(--MI-radius);
+	padding: var(--MI-margin);
 	text-align: center;
 }
 
diff --git a/packages/frontend/src/components/page/page.image.vue b/packages/frontend/src/components/page/page.image.vue
index fc1ce9fc7b..69443ce7dd 100644
--- a/packages/frontend/src/components/page/page.image.vue
+++ b/packages/frontend/src/components/page/page.image.vue
@@ -28,8 +28,8 @@ onMounted(() => {
 
 <style lang="scss" module>
 .root {
-	border: 1px solid var(--divider);
-	border-radius: var(--radius);
+	border: 1px solid var(--MI_THEME-divider);
+	border-radius: var(--MI-radius);
 	overflow: hidden;
 }
 .mediaList {
diff --git a/packages/frontend/src/components/page/page.note.vue b/packages/frontend/src/components/page/page.note.vue
index b5ba407806..84436e7adb 100644
--- a/packages/frontend/src/components/page/page.note.vue
+++ b/packages/frontend/src/components/page/page.note.vue
@@ -35,7 +35,7 @@ onMounted(() => {
 
 <style lang="scss" module>
 .root {
-	border: 1px solid var(--divider);
-	border-radius: var(--radius);
+	border: 1px solid var(--MI_THEME-divider);
+	border-radius: var(--MI-radius);
 }
 </style>
diff --git a/packages/frontend/src/custom-emojis.ts b/packages/frontend/src/custom-emojis.ts
index 9da3582e1a..0d03282cee 100644
--- a/packages/frontend/src/custom-emojis.ts
+++ b/packages/frontend/src/custom-emojis.ts
@@ -6,7 +6,6 @@
 import { shallowRef, computed, markRaw, watch } from 'vue';
 import * as Misskey from 'misskey-js';
 import { misskeyApi, misskeyApiGet } from '@/scripts/misskey-api.js';
-import { useStream } from '@/stream.js';
 import { get, set } from '@/scripts/idb-proxy.js';
 
 const storageCache = await get('emojis');
@@ -29,23 +28,20 @@ watch(customEmojis, emojis => {
 	}
 }, { immediate: true });
 
-// TODO: ここら辺副作用なのでいい感じにする
-const stream = useStream();
-
-stream.on('emojiAdded', emojiData => {
-	customEmojis.value = [emojiData.emoji, ...customEmojis.value];
+export function addCustomEmoji(emoji: Misskey.entities.EmojiSimple) {
+	customEmojis.value = [emoji, ...customEmojis.value];
 	set('emojis', customEmojis.value);
-});
+}
 
-stream.on('emojiUpdated', emojiData => {
-	customEmojis.value = customEmojis.value.map(item => emojiData.emojis.find(search => search.name === item.name) as Misskey.entities.EmojiSimple ?? item);
+export function updateCustomEmojis(emojis: Misskey.entities.EmojiSimple[]) {
+	customEmojis.value = customEmojis.value.map(item => emojis.find(search => search.name === item.name) ?? item);
 	set('emojis', customEmojis.value);
-});
+}
 
-stream.on('emojiDeleted', emojiData => {
-	customEmojis.value = customEmojis.value.filter(item => !emojiData.emojis.some(search => search.name === item.name));
+export function removeCustomEmojis(emojis: Misskey.entities.EmojiSimple[]) {
+	customEmojis.value = customEmojis.value.filter(item => !emojis.some(search => search.name === item.name));
 	set('emojis', customEmojis.value);
-});
+}
 
 export async function fetchCustomEmojis(force = false) {
 	const now = Date.now();
diff --git a/packages/frontend/src/directives/adaptive-bg.ts b/packages/frontend/src/directives/adaptive-bg.ts
index 23fd1bddf4..f88996019f 100644
--- a/packages/frontend/src/directives/adaptive-bg.ts
+++ b/packages/frontend/src/directives/adaptive-bg.ts
@@ -4,24 +4,16 @@
  */
 
 import { Directive } from 'vue';
+import { getBgColor } from '@/scripts/get-bg-color.js';
 
 export default {
 	mounted(src, binding, vn) {
-		const getBgColor = (el: HTMLElement) => {
-			const style = window.getComputedStyle(el);
-			if (style.backgroundColor && !['rgba(0, 0, 0, 0)', 'rgba(0,0,0,0)', 'transparent'].includes(style.backgroundColor)) {
-				return style.backgroundColor;
-			} else {
-				return el.parentElement ? getBgColor(el.parentElement) : 'transparent';
-			}
-		};
-
-		const parentBg = getBgColor(src.parentElement);
+		const parentBg = getBgColor(src.parentElement) ?? 'transparent';
 
 		const myBg = window.getComputedStyle(src).backgroundColor;
 
 		if (parentBg === myBg) {
-			src.style.backgroundColor = 'var(--bg)';
+			src.style.backgroundColor = 'var(--MI_THEME-bg)';
 		} else {
 			src.style.backgroundColor = myBg;
 		}
diff --git a/packages/frontend/src/directives/adaptive-border.ts b/packages/frontend/src/directives/adaptive-border.ts
index b436075fcd..1305f312bd 100644
--- a/packages/frontend/src/directives/adaptive-border.ts
+++ b/packages/frontend/src/directives/adaptive-border.ts
@@ -4,24 +4,16 @@
  */
 
 import { Directive } from 'vue';
+import { getBgColor } from '@/scripts/get-bg-color.js';
 
 export default {
 	mounted(src, binding, vn) {
-		const getBgColor = (el: HTMLElement) => {
-			const style = window.getComputedStyle(el);
-			if (style.backgroundColor && !['rgba(0, 0, 0, 0)', 'rgba(0,0,0,0)', 'transparent'].includes(style.backgroundColor)) {
-				return style.backgroundColor;
-			} else {
-				return el.parentElement ? getBgColor(el.parentElement) : 'transparent';
-			}
-		};
-
-		const parentBg = getBgColor(src.parentElement);
+		const parentBg = getBgColor(src.parentElement) ?? 'transparent';
 
 		const myBg = window.getComputedStyle(src).backgroundColor;
 
 		if (parentBg === myBg) {
-			src.style.borderColor = 'var(--divider)';
+			src.style.borderColor = 'var(--MI_THEME-divider)';
 		} else {
 			src.style.borderColor = myBg;
 		}
diff --git a/packages/frontend/src/directives/follow-append.ts b/packages/frontend/src/directives/follow-append.ts
index f200f242ed..615dd99fa8 100644
--- a/packages/frontend/src/directives/follow-append.ts
+++ b/packages/frontend/src/directives/follow-append.ts
@@ -4,7 +4,7 @@
  */
 
 import { Directive } from 'vue';
-import { getScrollContainer, getScrollPosition } from '@/scripts/scroll.js';
+import { getScrollContainer, getScrollPosition } from '@@/js/scroll.js';
 
 export default {
 	mounted(src, binding, vn) {
diff --git a/packages/frontend/src/directives/hotkey.ts b/packages/frontend/src/directives/hotkey.ts
index b082b6edf2..0e5c7ede24 100644
--- a/packages/frontend/src/directives/hotkey.ts
+++ b/packages/frontend/src/directives/hotkey.ts
@@ -4,7 +4,7 @@
  */
 
 import { Directive } from 'vue';
-import { makeHotkey } from '../scripts/hotkey.js';
+import { makeHotkey } from '@/scripts/hotkey.js';
 
 export default {
 	mounted(el, binding) {
@@ -13,9 +13,9 @@ export default {
 		el._keyHandler = makeHotkey(binding.value);
 
 		if (el._hotkey_global) {
-			document.addEventListener('keydown', el._keyHandler);
+			document.addEventListener('keydown', el._keyHandler, { passive: false });
 		} else {
-			el.addEventListener('keydown', el._keyHandler);
+			el.addEventListener('keydown', el._keyHandler, { passive: false });
 		}
 	},
 
diff --git a/packages/frontend/src/directives/panel.ts b/packages/frontend/src/directives/panel.ts
index bbcc220e09..aa26b94d0b 100644
--- a/packages/frontend/src/directives/panel.ts
+++ b/packages/frontend/src/directives/panel.ts
@@ -4,26 +4,18 @@
  */
 
 import { Directive } from 'vue';
+import { getBgColor } from '@/scripts/get-bg-color.js';
 
 export default {
 	mounted(src, binding, vn) {
-		const getBgColor = (el: HTMLElement) => {
-			const style = window.getComputedStyle(el);
-			if (style.backgroundColor && !['rgba(0, 0, 0, 0)', 'rgba(0,0,0,0)', 'transparent'].includes(style.backgroundColor)) {
-				return style.backgroundColor;
-			} else {
-				return el.parentElement ? getBgColor(el.parentElement) : 'transparent';
-			}
-		};
+		const parentBg = getBgColor(src.parentElement) ?? 'transparent';
 
-		const parentBg = getBgColor(src.parentElement);
-
-		const myBg = getComputedStyle(document.documentElement).getPropertyValue('--panel');
+		const myBg = getComputedStyle(document.documentElement).getPropertyValue('--MI_THEME-panel');
 
 		if (parentBg === myBg) {
-			src.style.backgroundColor = 'var(--bg)';
+			src.style.backgroundColor = 'var(--MI_THEME-bg)';
 		} else {
-			src.style.backgroundColor = 'var(--panel)';
+			src.style.backgroundColor = 'var(--MI_THEME-panel)';
 		}
 	},
 } as Directive;
diff --git a/packages/frontend/src/directives/ripple.ts b/packages/frontend/src/directives/ripple.ts
index 2d724f771e..a043ff212d 100644
--- a/packages/frontend/src/directives/ripple.ts
+++ b/packages/frontend/src/directives/ripple.ts
@@ -17,7 +17,9 @@ export default {
 			const x = rect.left + (el.offsetWidth / 2);
 			const y = rect.top + (el.offsetHeight / 2);
 
-			popup(MkRippleEffect, { x, y }, {}, 'end');
+			const { dispose } = popup(MkRippleEffect, { x, y }, {
+				end: () => dispose(),
+			});
 		});
 	},
 };
diff --git a/packages/frontend/src/directives/tooltip.ts b/packages/frontend/src/directives/tooltip.ts
index b1c1b19907..251ce5675f 100644
--- a/packages/frontend/src/directives/tooltip.ts
+++ b/packages/frontend/src/directives/tooltip.ts
@@ -51,13 +51,15 @@ export default {
 			if (self.text == null) return;
 
 			const showing = ref(true);
-			popup(defineAsyncComponent(() => import('@/components/MkTooltip.vue')), {
+			const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkTooltip.vue')), {
 				showing,
 				text: self.text,
 				asMfm: binding.modifiers.mfm,
 				direction: binding.modifiers.left ? 'left' : binding.modifiers.right ? 'right' : binding.modifiers.top ? 'top' : binding.modifiers.bottom ? 'bottom' : 'top',
 				targetElement: el,
-			}, {}, 'closed');
+			}, {
+				closed: () => dispose(),
+			});
 
 			self._close = () => {
 				showing.value = false;
diff --git a/packages/frontend/src/directives/user-preview.ts b/packages/frontend/src/directives/user-preview.ts
index 7a008a4486..278d842d09 100644
--- a/packages/frontend/src/directives/user-preview.ts
+++ b/packages/frontend/src/directives/user-preview.ts
@@ -35,7 +35,7 @@ export class UserPreview {
 
 		const showing = ref(true);
 
-		popup(defineAsyncComponent(() => import('@/components/MkUserPopup.vue')), {
+		const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkUserPopup.vue')), {
 			showing,
 			q: this.user,
 			source: this.el,
@@ -47,7 +47,8 @@ export class UserPreview {
 				window.clearTimeout(this.showTimer);
 				this.hideTimer = window.setTimeout(this.close, 500);
 			},
-		}, 'closed');
+			closed: () => dispose(),
+		});
 
 		this.promise = {
 			cancel: () => {
diff --git a/packages/frontend/src/filters/date.ts b/packages/frontend/src/filters/date.ts
index 2ffe93e868..d13d1a5e42 100644
--- a/packages/frontend/src/filters/date.ts
+++ b/packages/frontend/src/filters/date.ts
@@ -3,7 +3,7 @@
  * SPDX-License-Identifier: AGPL-3.0-only
  */
 
-import { dateTimeFormat } from '@/scripts/intl-const.js';
+import { dateTimeFormat } from '@@/js/intl-const.js';
 
 export default (d: Date | number | undefined) => dateTimeFormat.format(d);
 export const dateString = (d: string) => dateTimeFormat.format(new Date(d));
diff --git a/packages/frontend/src/filters/number.ts b/packages/frontend/src/filters/number.ts
index 2e7cc60ff4..10fb64deb4 100644
--- a/packages/frontend/src/filters/number.ts
+++ b/packages/frontend/src/filters/number.ts
@@ -3,6 +3,6 @@
  * SPDX-License-Identifier: AGPL-3.0-only
  */
 
-import { numberFormat } from '@/scripts/intl-const.js';
+import { numberFormat } from '@@/js/intl-const.js';
 
 export default n => n == null ? 'N/A' : numberFormat.format(n);
diff --git a/packages/frontend/src/filters/user.ts b/packages/frontend/src/filters/user.ts
index b713d41789..d9bc316764 100644
--- a/packages/frontend/src/filters/user.ts
+++ b/packages/frontend/src/filters/user.ts
@@ -4,9 +4,9 @@
  */
 
 import * as Misskey from 'misskey-js';
-import { url } from '@/config.js';
+import { url } from '@@/js/config.js';
 
-export const acct = (user: misskey.Acct) => {
+export const acct = (user: Misskey.Acct) => {
 	return Misskey.acct.toString(user);
 };
 
@@ -14,6 +14,6 @@ export const userName = (user: Misskey.entities.User) => {
 	return user.name || user.username;
 };
 
-export const userPage = (user: misskey.Acct, path?, absolute = false) => {
+export const userPage = (user: Misskey.Acct, path?: string, absolute = false) => {
 	return `${absolute ? url : ''}/@${acct(user)}${(path ? `/${path}` : '')}`;
 };
diff --git a/packages/frontend/src/i18n.ts b/packages/frontend/src/i18n.ts
index cc9faddb20..6ad503b089 100644
--- a/packages/frontend/src/i18n.ts
+++ b/packages/frontend/src/i18n.ts
@@ -4,13 +4,12 @@
  */
 
 import { markRaw } from 'vue';
+import { I18n } from '@@/js/i18n.js';
 import type { Locale } from '../../../locales/index.js';
-import { locale } from '@/config.js';
-import { I18n } from '@/scripts/i18n.js';
+import { locale } from '@@/js/config.js';
 
-export const i18n = markRaw(new I18n<Locale>(locale));
+export const i18n = markRaw(new I18n<Locale>(locale, _DEV_));
 
 export function updateI18n(newLocale: Locale) {
-	// @ts-expect-error -- private field
 	i18n.locale = newLocale;
 }
diff --git a/packages/frontend/src/instance.ts b/packages/frontend/src/instance.ts
index 6847321d6c..71cb42b30c 100644
--- a/packages/frontend/src/instance.ts
+++ b/packages/frontend/src/instance.ts
@@ -7,7 +7,7 @@ import { computed, reactive } from 'vue';
 import * as Misskey from 'misskey-js';
 import { misskeyApi } from '@/scripts/misskey-api.js';
 import { miLocalStorage } from '@/local-storage.js';
-import { DEFAULT_INFO_IMAGE_URL, DEFAULT_NOT_FOUND_IMAGE_URL, DEFAULT_SERVER_ERROR_IMAGE_URL } from '@/const.js';
+import { DEFAULT_INFO_IMAGE_URL, DEFAULT_NOT_FOUND_IMAGE_URL, DEFAULT_SERVER_ERROR_IMAGE_URL } from '@@/js/const.js';
 
 // TODO: 他のタブと永続化されたstateを同期
 
diff --git a/packages/frontend/src/local-storage.ts b/packages/frontend/src/local-storage.ts
index 8029bca68d..5b8ba77e01 100644
--- a/packages/frontend/src/local-storage.ts
+++ b/packages/frontend/src/local-storage.ts
@@ -3,7 +3,7 @@
  * SPDX-License-Identifier: AGPL-3.0-only
  */
 
-type Keys =
+export type Keys =
 	'v' |
 	'lastVersion' |
 	'instance' |
@@ -38,12 +38,22 @@ type Keys =
 	`aiscript:${string}` |
 	'lastEmojisFetchedAt' | // DEPRECATED, stored in indexeddb (13.9.0~)
 	'emojis' | // DEPRECATED, stored in indexeddb (13.9.0~);
-	`channelLastReadedAt:${string}`
+	`channelLastReadedAt:${string}` |
+	`idbfallback::${string}`
+
+// セッション毎に廃棄されるLocalStorage代替(セーフモードなどで使用できそう)
+//const safeSessionStorage = new Map<Keys, string>();
 
 export const miLocalStorage = {
-	getItem: (key: Keys): string | null => window.localStorage.getItem(key),
-	setItem: (key: Keys, value: string): void => window.localStorage.setItem(key, value),
-	removeItem: (key: Keys): void => window.localStorage.removeItem(key),
+	getItem: (key: Keys): string | null => {
+		return window.localStorage.getItem(key);
+	},
+	setItem: (key: Keys, value: string): void => {
+		window.localStorage.setItem(key, value);
+	},
+	removeItem: (key: Keys): void => {
+		window.localStorage.removeItem(key);
+	},
 	getItemAsJson: (key: Keys): any | undefined => {
 		const item = miLocalStorage.getItem(key);
 		if (item === null) {
@@ -51,5 +61,7 @@ export const miLocalStorage = {
 		}
 		return JSON.parse(item);
 	},
-	setItemAsJson: (key: Keys, value: any): void => window.localStorage.setItem(key, JSON.stringify(value)),
+	setItemAsJson: (key: Keys, value: any): void => {
+		miLocalStorage.setItem(key, JSON.stringify(value));
+	},
 };
diff --git a/packages/frontend/src/navbar.ts b/packages/frontend/src/navbar.ts
index d7910935fa..ac730f8021 100644
--- a/packages/frontend/src/navbar.ts
+++ b/packages/frontend/src/navbar.ts
@@ -11,7 +11,7 @@ import { openInstanceMenu, openToolsMenu } from '@/ui/_common_/common.js';
 import { lookup } from '@/scripts/lookup.js';
 import * as os from '@/os.js';
 import { i18n } from '@/i18n.js';
-import { ui } from '@/config.js';
+import { ui } from '@@/js/config.js';
 import { unisonReload } from '@/scripts/unison-reload.js';
 
 export const navbarItemDef = reactive({
@@ -125,7 +125,7 @@ export const navbarItemDef = reactive({
 	ui: {
 		title: i18n.ts.switchUi,
 		icon: 'ti ti-devices',
-		action: (ev) => {
+		action: (ev: MouseEvent) => {
 			os.popupMenu([{
 				text: i18n.ts.default,
 				active: ui === 'default' || ui === null,
diff --git a/packages/frontend/src/nirax.ts b/packages/frontend/src/nirax.ts
index 6a8ea09ed6..965bd6f0bc 100644
--- a/packages/frontend/src/nirax.ts
+++ b/packages/frontend/src/nirax.ts
@@ -7,7 +7,14 @@
 
 import { Component, onMounted, shallowRef, ShallowRef } from 'vue';
 import { EventEmitter } from 'eventemitter3';
-import { safeURIDecode } from '@/scripts/safe-uri-decode.js';
+
+function safeURIDecode(str: string): string {
+	try {
+		return decodeURIComponent(str);
+	} catch {
+		return str;
+	}
+}
 
 interface RouteDefBase {
 	path: string;
@@ -29,6 +36,8 @@ interface RouteDefWithRedirect extends RouteDefBase {
 
 export type RouteDef = RouteDefWithComponent | RouteDefWithRedirect;
 
+export type RouterFlag = 'forcePage';
+
 type ParsedPath = (string | {
 	name: string;
 	startsWith?: string;
@@ -100,7 +109,7 @@ export interface IRouter extends EventEmitter<RouterEvent> {
 	current: Resolved;
 	currentRef: ShallowRef<Resolved>;
 	currentRoute: ShallowRef<RouteDef>;
-	navHook: ((path: string, flag?: any) => boolean) | null;
+	navHook: ((path: string, flag?: RouterFlag) => boolean) | null;
 
 	/**
 	 * ルートの初期化(eventListenerの定義後に必ず呼び出すこと)
@@ -109,11 +118,11 @@ export interface IRouter extends EventEmitter<RouterEvent> {
 
 	resolve(path: string): Resolved | null;
 
-	getCurrentPath(): any;
+	getCurrentPath(): string;
 
 	getCurrentKey(): string;
 
-	push(path: string, flag?: any): void;
+	push(path: string, flag?: RouterFlag): void;
 
 	replace(path: string, key?: string | null): void;
 
@@ -190,7 +199,7 @@ export class Router extends EventEmitter<RouterEvent> implements IRouter {
 	private currentKey = Date.now().toString();
 	private redirectCount = 0;
 
-	public navHook: ((path: string, flag?: any) => boolean) | null = null;
+	public navHook: ((path: string, flag?: RouterFlag) => boolean) | null = null;
 
 	constructor(routes: Router['routes'], currentPath: Router['currentPath'], isLoggedIn: boolean, notFoundPageComponent: Component) {
 		super();
@@ -397,7 +406,7 @@ export class Router extends EventEmitter<RouterEvent> implements IRouter {
 		return this.currentKey;
 	}
 
-	public push(path: string, flag?: any) {
+	public push(path: string, flag?: RouterFlag) {
 		const beforePath = this.currentPath;
 		if (path === beforePath) {
 			this.emit('same');
diff --git a/packages/frontend/src/os.ts b/packages/frontend/src/os.ts
index f656a52371..ea1b673de9 100644
--- a/packages/frontend/src/os.ts
+++ b/packages/frontend/src/os.ts
@@ -5,12 +5,14 @@
 
 // TODO: なんでもかんでもos.tsに突っ込むのやめたいのでよしなに分割する
 
-import { Component, markRaw, Ref, ref, defineAsyncComponent } from 'vue';
+import { Component, markRaw, Ref, ref, defineAsyncComponent, nextTick } from 'vue';
 import { EventEmitter } from 'eventemitter3';
 import * as Misskey from 'misskey-js';
 import type { ComponentProps as CP } from 'vue-component-type-helpers';
 import type { Form, GetFormResultType } from '@/scripts/form.js';
+import type { MenuItem } from '@/types/menu.js';
 import { misskeyApi } from '@/scripts/misskey-api.js';
+import { defaultStore } from '@/store.js';
 import { i18n } from '@/i18n.js';
 import MkPostFormDialog from '@/components/MkPostFormDialog.vue';
 import MkWaitingDialog from '@/components/MkWaitingDialog.vue';
@@ -21,16 +23,20 @@ import MkPasswordDialog from '@/components/MkPasswordDialog.vue';
 import MkEmojiPickerDialog from '@/components/MkEmojiPickerDialog.vue';
 import MkPopupMenu from '@/components/MkPopupMenu.vue';
 import MkContextMenu from '@/components/MkContextMenu.vue';
-import { MenuItem } from '@/types/menu.js';
-import copyToClipboard from '@/scripts/copy-to-clipboard.js';
+import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
+import { pleaseLogin } from '@/scripts/please-login.js';
 import { showMovedDialog } from '@/scripts/show-moved-dialog.js';
+import { getHTMLElementOrNull } from '@/scripts/get-dom-node-or-null.js';
+import { focusParent } from '@/scripts/focus.js';
+import type { PostFormProps } from '@/types/post-form.js';
 
 export const openingWindowsCount = ref(0);
 
-export const apiWithDialog = (<E extends keyof Misskey.Endpoints = keyof Misskey.Endpoints, P extends Misskey.Endpoints[E]['req'] = Misskey.Endpoints[E]['req']>(
+export const apiWithDialog = (<E extends keyof Misskey.Endpoints, P extends Misskey.Endpoints[E]['req'] = Misskey.Endpoints[E]['req']>(
 	endpoint: E,
-	data: P = {} as any,
+	data: P,
 	token?: string | null | undefined,
+	customErrors?: Record<string, { title?: string; text: string; }>,
 ) => {
 	const promise = misskeyApi(endpoint, data, token);
 	promiseDialog(promise, null, async (err) => {
@@ -73,6 +79,9 @@ export const apiWithDialog = (<E extends keyof Misskey.Endpoints = keyof Misskey
 		} else if (err.message.startsWith('Unexpected token')) {
 			title = i18n.ts.gotInvalidResponseError;
 			text = i18n.ts.gotInvalidResponseErrorDescription;
+		} else if (customErrors && customErrors[err.id] != null) {
+			title = customErrors[err.id].title;
+			text = customErrors[err.id].text;
 		}
 		alert({
 			type: 'error',
@@ -82,11 +91,11 @@ export const apiWithDialog = (<E extends keyof Misskey.Endpoints = keyof Misskey
 	});
 
 	return promise;
-}) as typeof misskeyApi;
+});
 
 export function promiseDialog<T extends Promise<any>>(
 	promise: T,
-	onSuccess?: ((res: any) => void) | null,
+	onSuccess?: ((res: Awaited<T>) => void) | null,
 	onFailure?: ((err: Misskey.api.APIError) => void) | null,
 	text?: string,
 ): T {
@@ -116,22 +125,24 @@ export function promiseDialog<T extends Promise<any>>(
 	});
 
 	// NOTE: dynamic importすると挙動がおかしくなる(showingの変更が伝播しない)
-	popup(MkWaitingDialog, {
+	const { dispose } = popup(MkWaitingDialog, {
 		success: success,
 		showing: showing,
 		text: text,
-	}, {}, 'closed');
+	}, {
+		closed: () => dispose(),
+	});
 
 	return promise;
 }
 
 let popupIdCount = 0;
-export const popups = ref([]) as Ref<{
+export const popups = ref<{
 	id: number;
 	component: Component;
 	props: Record<string, any>;
 	events: Record<string, any>;
-}[]>;
+}[]>([]);
 
 const zIndexes = {
 	veryLow: 500000,
@@ -166,28 +177,24 @@ type EmitsExtractor<T> = {
 	[K in keyof T as K extends `onVnode${string}` ? never : K extends `on${infer E}` ? Uncapitalize<E> : K extends string ? never : K]: T[K];
 };
 
-export async function popup<T extends Component>(
+export function popup<T extends Component>(
 	component: T,
 	props: ComponentProps<T>,
 	events: ComponentEmit<T> = {} as ComponentEmit<T>,
-	disposeEvent?: keyof ComponentEmit<T>,
-): Promise<{ dispose: () => void }> {
+): { dispose: () => void } {
 	markRaw(component);
 
 	const id = ++popupIdCount;
 	const dispose = () => {
 		// このsetTimeoutが無いと挙動がおかしくなる(autocompleteが閉じなくなる)。Vueのバグ?
 		window.setTimeout(() => {
-			popups.value = popups.value.filter(popup => popup.id !== id);
+			popups.value = popups.value.filter(p => p.id !== id);
 		}, 0);
 	};
 	const state = {
 		component,
 		props,
-		events: disposeEvent ? {
-			...events,
-			[disposeEvent]: dispose,
-		} : events,
+		events,
 		id,
 	};
 
@@ -199,15 +206,19 @@ export async function popup<T extends Component>(
 }
 
 export function pageWindow(path: string) {
-	popup(MkPageWindow, {
+	const { dispose } = popup(MkPageWindow, {
 		initialPath: path,
-	}, {}, 'closed');
+	}, {
+		closed: () => dispose(),
+	});
 }
 
 export function toast(message: string) {
-	popup(MkToast, {
+	const { dispose } = popup(MkToast, {
 		message,
-	}, {}, 'closed');
+	}, {
+		closed: () => dispose(),
+	});
 }
 
 export function alert(props: {
@@ -216,11 +227,12 @@ export function alert(props: {
 	text?: string;
 }): Promise<void> {
 	return new Promise(resolve => {
-		popup(MkDialog, props, {
+		const { dispose } = popup(MkDialog, props, {
 			done: () => {
 				resolve();
 			},
-		}, 'closed');
+			closed: () => dispose(),
+		});
 	});
 }
 
@@ -232,14 +244,15 @@ export function confirm(props: {
 	cancelText?: string;
 }): Promise<{ canceled: boolean }> {
 	return new Promise(resolve => {
-		popup(MkDialog, {
+		const { dispose } = popup(MkDialog, {
 			...props,
 			showCancelButton: true,
 		}, {
 			done: result => {
 				resolve(result ? result : { canceled: true });
 			},
-		}, 'closed');
+			closed: () => dispose(),
+		});
 	});
 }
 
@@ -261,7 +274,7 @@ export function actions<T extends {
 	canceled: false; result: T[number]['value'];
 }> {
 	return new Promise(resolve => {
-		popup(MkDialog, {
+		const { dispose } = popup(MkDialog, {
 			...props,
 			actions: props.actions.map(a => ({
 				text: a.text,
@@ -275,7 +288,8 @@ export function actions<T extends {
 			done: result => {
 				resolve(result ? result : { canceled: true });
 			},
-		}, 'closed');
+			closed: () => dispose(),
+		});
 	});
 }
 
@@ -323,7 +337,7 @@ export function inputText(props: {
 	canceled: false; result: string | null;
 }> {
 	return new Promise(resolve => {
-		popup(MkDialog, {
+		const { dispose } = popup(MkDialog, {
 			title: props.title,
 			text: props.text,
 			input: {
@@ -338,7 +352,8 @@ export function inputText(props: {
 			done: result => {
 				resolve(result ? result : { canceled: true });
 			},
-		}, 'closed');
+			closed: () => dispose(),
+		});
 	});
 }
 
@@ -377,7 +392,7 @@ export function inputNumber(props: {
 	canceled: false; result: number | null;
 }> {
 	return new Promise(resolve => {
-		popup(MkDialog, {
+		const { dispose } = popup(MkDialog, {
 			title: props.title,
 			text: props.text,
 			input: {
@@ -390,7 +405,8 @@ export function inputNumber(props: {
 			done: result => {
 				resolve(result ? result : { canceled: true });
 			},
-		}, 'closed');
+			closed: () => dispose(),
+		});
 	});
 }
 
@@ -405,7 +421,7 @@ export function inputDate(props: {
 	canceled: false; result: Date;
 }> {
 	return new Promise(resolve => {
-		popup(MkDialog, {
+		const { dispose } = popup(MkDialog, {
 			title: props.title,
 			text: props.text,
 			input: {
@@ -417,7 +433,8 @@ export function inputDate(props: {
 			done: result => {
 				resolve(result ? { result: new Date(result.result), canceled: false } : { result: undefined, canceled: true });
 			},
-		}, 'closed');
+			closed: () => dispose(),
+		});
 	});
 }
 
@@ -427,67 +444,74 @@ export function authenticateDialog(): Promise<{
 	canceled: false; result: { password: string; token: string | null; };
 }> {
 	return new Promise(resolve => {
-		popup(MkPasswordDialog, {}, {
+		const { dispose } = popup(MkPasswordDialog, {}, {
 			done: result => {
 				resolve(result ? { canceled: false, result } : { canceled: true, result: undefined });
 			},
-		}, 'closed');
+			closed: () => dispose(),
+		});
 	});
 }
 
+type SelectItem<C> = {
+	value: C;
+	text: string;
+};
+
 // default が指定されていたら result は null になり得ないことを保証する overload function
-export function select<C = any>(props: {
+export function select<C = unknown>(props: {
 	title?: string;
 	text?: string;
 	default: string;
-	items: {
-		value: C;
-		text: string;
-	}[];
+	items: (SelectItem<C> | {
+		sectionTitle: string;
+		items: SelectItem<C>[];
+	} | undefined)[];
 }): Promise<{
 	canceled: true; result: undefined;
 } | {
 	canceled: false; result: C;
 }>;
-export function select<C = any>(props: {
+export function select<C = unknown>(props: {
 	title?: string;
 	text?: string;
 	default?: string | null;
-	items: {
-		value: C;
-		text: string;
-	}[];
+	items: (SelectItem<C> | {
+		sectionTitle: string;
+		items: SelectItem<C>[];
+	} | undefined)[];
 }): Promise<{
 	canceled: true; result: undefined;
 } | {
 	canceled: false; result: C | null;
 }>;
-export function select<C = any>(props: {
+export function select<C = unknown>(props: {
 	title?: string;
 	text?: string;
 	default?: string | null;
-	items: {
-		value: C;
-		text: string;
-	}[];
+	items: (SelectItem<C> | {
+		sectionTitle: string;
+		items: SelectItem<C>[];
+	} | undefined)[];
 }): Promise<{
 	canceled: true; result: undefined;
 } | {
 	canceled: false; result: C | null;
 }> {
 	return new Promise(resolve => {
-		popup(MkDialog, {
+		const { dispose } = popup(MkDialog, {
 			title: props.title,
 			text: props.text,
 			select: {
-				items: props.items,
+				items: props.items.filter(x => x !== undefined),
 				default: props.default ?? null,
 			},
 		}, {
 			done: result => {
 				resolve(result ? result : { canceled: true });
 			},
-		}, 'closed');
+			closed: () => dispose(),
+		});
 	});
 }
 
@@ -497,53 +521,57 @@ export function success(): Promise<void> {
 		window.setTimeout(() => {
 			showing.value = false;
 		}, 1000);
-		popup(MkWaitingDialog, {
+		const { dispose } = popup(MkWaitingDialog, {
 			success: true,
 			showing: showing,
 		}, {
 			done: () => resolve(),
-		}, 'closed');
+			closed: () => dispose(),
+		});
 	});
 }
 
 export function waiting(): Promise<void> {
 	return new Promise(resolve => {
 		const showing = ref(true);
-		popup(MkWaitingDialog, {
+		const { dispose } = popup(MkWaitingDialog, {
 			success: false,
 			showing: showing,
 		}, {
 			done: () => resolve(),
-		}, 'closed');
+			closed: () => dispose(),
+		});
 	});
 }
 
 export function form<F extends Form>(title: string, f: F): Promise<{ canceled: true, result?: undefined } | { canceled?: false, result: GetFormResultType<F> }> {
 	return new Promise(resolve => {
-		popup(defineAsyncComponent(() => import('@/components/MkFormDialog.vue')), { title, form: f }, {
+		const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkFormDialog.vue')), { title, form: f }, {
 			done: result => {
 				resolve(result);
 			},
-		}, 'closed');
+			closed: () => dispose(),
+		});
 	});
 }
 
 export async function selectUser(opts: { includeSelf?: boolean; localOnly?: boolean; } = {}): Promise<Misskey.entities.UserDetailed> {
 	return new Promise(resolve => {
-		popup(defineAsyncComponent(() => import('@/components/MkUserSelectDialog.vue')), {
+		const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkUserSelectDialog.vue')), {
 			includeSelf: opts.includeSelf,
 			localOnly: opts.localOnly,
 		}, {
 			ok: user => {
 				resolve(user);
 			},
-		}, 'closed');
+			closed: () => dispose(),
+		});
 	});
 }
 
 export async function selectDriveFile(multiple: boolean): Promise<Misskey.entities.DriveFile[]> {
 	return new Promise(resolve => {
-		popup(defineAsyncComponent(() => import('@/components/MkDriveSelectDialog.vue')), {
+		const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkDriveSelectDialog.vue')), {
 			type: 'file',
 			multiple,
 		}, {
@@ -552,13 +580,14 @@ export async function selectDriveFile(multiple: boolean): Promise<Misskey.entiti
 					resolve(files);
 				}
 			},
-		}, 'closed');
+			closed: () => dispose(),
+		});
 	});
 }
 
 export async function selectDriveFolder(multiple: boolean): Promise<Misskey.entities.DriveFolder[]> {
 	return new Promise(resolve => {
-		popup(defineAsyncComponent(() => import('@/components/MkDriveSelectDialog.vue')), {
+		const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkDriveSelectDialog.vue')), {
 			type: 'folder',
 			multiple,
 		}, {
@@ -567,20 +596,22 @@ export async function selectDriveFolder(multiple: boolean): Promise<Misskey.enti
 					resolve(folders);
 				}
 			},
-		}, 'closed');
+			closed: () => dispose(),
+		});
 	});
 }
 
 export async function pickEmoji(src: HTMLElement, opts: ComponentProps<typeof MkEmojiPickerDialog>): Promise<string> {
 	return new Promise(resolve => {
-		popup(MkEmojiPickerDialog, {
+		const { dispose } = popup(MkEmojiPickerDialog, {
 			src,
 			...opts,
 		}, {
 			done: emoji => {
 				resolve(emoji);
 			},
-		}, 'closed');
+			closed: () => dispose(),
+		});
 	});
 }
 
@@ -589,7 +620,7 @@ export async function cropImage(image: Misskey.entities.DriveFile, options: {
 	uploadFolder?: string | null;
 }): Promise<Misskey.entities.DriveFile> {
 	return new Promise(resolve => {
-		popup(defineAsyncComponent(() => import('@/components/MkCropperDialog.vue')), {
+		const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkCropperDialog.vue')), {
 			file: image,
 			aspectRatio: options.aspectRatio,
 			uploadFolder: options.uploadFolder,
@@ -597,73 +628,90 @@ export async function cropImage(image: Misskey.entities.DriveFile, options: {
 			ok: x => {
 				resolve(x);
 			},
-		}, 'closed');
+			closed: () => dispose(),
+		});
 	});
 }
 
 export function popupMenu(items: MenuItem[], src?: HTMLElement | EventTarget | null, options?: {
 	align?: string;
 	width?: number;
-	viaKeyboard?: boolean;
 	onClosing?: () => void;
 }): Promise<void> {
-	return new Promise(resolve => {
-		let dispose;
-		popup(MkPopupMenu, {
+	let returnFocusTo = getHTMLElementOrNull(src) ?? getHTMLElementOrNull(document.activeElement);
+	return new Promise(resolve => nextTick(() => {
+		const { dispose } = popup(MkPopupMenu, {
 			items,
 			src,
 			width: options?.width,
 			align: options?.align,
-			viaKeyboard: options?.viaKeyboard,
+			returnFocusTo,
 		}, {
 			closed: () => {
 				resolve();
 				dispose();
+				returnFocusTo = null;
 			},
 			closing: () => {
-				if (options?.onClosing) options.onClosing();
+				options?.onClosing?.();
 			},
-		}).then(res => {
-			dispose = res.dispose;
 		});
-	});
+	}));
 }
 
 export function contextMenu(items: MenuItem[], ev: MouseEvent): Promise<void> {
+	if (
+		defaultStore.state.contextMenu === 'native' ||
+		(defaultStore.state.contextMenu === 'appWithShift' && !ev.shiftKey)
+	) {
+		return Promise.resolve();
+	}
+
+	let returnFocusTo = getHTMLElementOrNull(ev.currentTarget ?? ev.target) ?? getHTMLElementOrNull(document.activeElement);
 	ev.preventDefault();
-	return new Promise(resolve => {
-		let dispose;
-		popup(MkContextMenu, {
+	return new Promise(resolve => nextTick(() => {
+		const { dispose } = popup(MkContextMenu, {
 			items,
 			ev,
 		}, {
 			closed: () => {
 				resolve();
 				dispose();
+
+				// MkModalを通していないのでここでフォーカスを戻す処理を行う
+				if (returnFocusTo != null) {
+					focusParent(returnFocusTo, true, false);
+					returnFocusTo = null;
+				}
 			},
-		}).then(res => {
-			dispose = res.dispose;
 		});
-	});
+	}));
 }
 
-export function post(props: Record<string, any> = {}): Promise<void> {
-	showMovedDialog();
+export function post(props: PostFormProps = {}): Promise<void> {
+	pleaseLogin({
+		openOnRemote: (props.initialText || props.initialNote ? {
+			type: 'share',
+			params: {
+				text: props.initialText ?? props.initialNote?.text ?? '',
+				visibility: props.initialVisibility ?? props.initialNote?.visibility ?? 'public',
+				localOnly: (props.initialLocalOnly || props.initialNote?.localOnly) ? '1' : '0',
+			},
+		} : undefined),
+	});
 
+	showMovedDialog();
 	return new Promise(resolve => {
 		// NOTE: MkPostFormDialogをdynamic importするとiOSでテキストエリアに自動フォーカスできない
 		// NOTE: ただ、dynamic importしない場合、MkPostFormDialogインスタンスが使いまわされ、
 		//       Vueが渡されたコンポーネントに内部的に__propsというプロパティを生やす影響で、
 		//       複数のpost formを開いたときに場合によってはエラーになる
 		//       もちろん複数のpost formを開けること自体Misskeyサイドのバグなのだが
-		let dispose;
-		popup(MkPostFormDialog, props, {
+		const { dispose } = popup(MkPostFormDialog, props, {
 			closed: () => {
 				resolve();
 				dispose();
 			},
-		}).then(res => {
-			dispose = res.dispose;
 		});
 	});
 }
diff --git a/packages/frontend/src/pages/_error_.vue b/packages/frontend/src/pages/_error_.vue
index c04f399c6d..f09a8e4285 100644
--- a/packages/frontend/src/pages/_error_.vue
+++ b/packages/frontend/src/pages/_error_.vue
@@ -29,7 +29,7 @@ import { ref, computed } from 'vue';
 import * as Misskey from 'misskey-js';
 import MkButton from '@/components/MkButton.vue';
 import MkLink from '@/components/MkLink.vue';
-import { version } from '@/config.js';
+import { version } from '@@/js/config.js';
 import { misskeyApi } from '@/scripts/misskey-api.js';
 import { unisonReload } from '@/scripts/unison-reload.js';
 import { i18n } from '@/i18n.js';
diff --git a/packages/frontend/src/pages/about-misskey.vue b/packages/frontend/src/pages/about-misskey.vue
index b55ae220d8..fbbfb6ea61 100644
--- a/packages/frontend/src/pages/about-misskey.vue
+++ b/packages/frontend/src/pages/about-misskey.vue
@@ -72,10 +72,6 @@ SPDX-License-Identifier: AGPL-3.0-only
 							<img src="https://avatars.githubusercontent.com/u/4439005?v=4" :class="$style.contributorAvatar">
 							<span :class="$style.contributorUsername">@syuilo</span>
 						</a>
-						<a href="https://github.com/tamaina" target="_blank" :class="$style.contributor">
-							<img src="https://avatars.githubusercontent.com/u/7973572?v=4" :class="$style.contributorAvatar">
-							<span :class="$style.contributorUsername">@tamaina</span>
-						</a>
 						<a href="https://github.com/acid-chicken" target="_blank" :class="$style.contributor">
 							<img src="https://avatars.githubusercontent.com/u/20679825?v=4" :class="$style.contributorAvatar">
 							<span :class="$style.contributorUsername">@acid-chicken</span>
@@ -102,13 +98,16 @@ SPDX-License-Identifier: AGPL-3.0-only
 					<template #label>Special thanks</template>
 					<div style="display:grid;grid-template-columns:repeat(auto-fill, minmax(130px, 1fr));grid-gap:24px;align-items:center;">
 						<div>
-							<a style="display: inline-block;" class="masknetwork" title="Mask Network" href="https://mask.io/" target="_blank"><img style="width: 100%;" src="https://misskey-hub.net/sponsors/masknetwork.png" alt="Mask Network"></a>
+							<a style="display: inline-block;" class="masknetwork" title="Mask Network" href="https://mask.io/" target="_blank"><img style="width: 100%;" src="https://assets.misskey-hub.net/sponsors/masknetwork.png" alt="Mask Network"></a>
 						</div>
 						<div>
-							<a style="display: inline-block;" class="xserver" title="XServer" href="https://www.xserver.ne.jp/" target="_blank"><img style="width: 100%;" src="https://misskey-hub.net/sponsors/xserver.png" alt="XServer"></a>
+							<a style="display: inline-block;" class="xserver" title="XServer" href="https://www.xserver.ne.jp/" target="_blank"><img style="width: 100%;" src="https://assets.misskey-hub.net/sponsors/xserver.png" alt="XServer"></a>
 						</div>
 						<div>
-							<a style="display: inline-block;" class="skeb" title="Skeb" href="https://skeb.jp/" target="_blank"><img style="width: 100%;" src="https://misskey-hub.net/sponsors/skeb.svg" alt="Skeb"></a>
+							<a style="display: inline-block;" class="skeb" title="Skeb" href="https://skeb.jp/" target="_blank"><img style="width: 100%;" src="https://assets.misskey-hub.net/sponsors/skeb.svg" alt="Skeb"></a>
+						</div>
+						<div>
+							<a style="display: inline-block;" class="pepabo" title="GMO Pepabo" href="https://pepabo.com/" target="_blank"><img style="width: 100%;" src="https://assets.misskey-hub.net/sponsors/gmo_pepabo.svg" alt="GMO Pepabo"></a>
 						</div>
 					</div>
 				</FormSection>
@@ -133,7 +132,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <script lang="ts" setup>
 import { nextTick, onBeforeUnmount, ref, shallowRef, computed } from 'vue';
-import { version } from '@/config.js';
+import { version } from '@@/js/config.js';
 import FormLink from '@/components/form/link.vue';
 import FormSection from '@/components/form/section.vue';
 import MkButton from '@/components/MkButton.vue';
@@ -240,6 +239,39 @@ const patronsWithIcon = [{
 }, {
 	name: '越貝鯛丸',
 	icon: 'https://assets.misskey-hub.net/patrons/86c7374de37849b882d8ebbc833dc968.jpg',
+}, {
+	name: '☔あめ🍬(灬˘╰╯˘灬)',
+	icon: 'https://assets.misskey-hub.net/patrons/676eea72d4884d3f89aababbb62533fb.jpg',
+}, {
+	name: '貯水よび',
+	icon: 'https://assets.misskey-hub.net/patrons/2974506d53244bbe94a67707b27099e2.jpg',
+}, {
+	name: 'はるかさ',
+	icon: 'https://assets.misskey-hub.net/patrons/26ce2432739a400aa3aa0de0ef67a107.jpg',
+}, {
+	name: '天鈴のあ',
+	icon: 'https://assets.misskey-hub.net/patrons/995cdbb00bd6421184461a883adfe1d9.jpg',
+}, {
+	name: 'えとゔぁす',
+	icon: 'https://assets.misskey-hub.net/patrons/2578f441b82a44cfaa55ba83a318b26e.jpg',
+}, {
+	name: 'Soli',
+	icon: 'https://assets.misskey-hub.net/patrons/448070c81ebd41eda4ea2328291b2efe.jpg',
+}, {
+	name: 'ささくれりょう',
+	icon: 'https://assets.misskey-hub.net/patrons/cf55022cee6c41da8e70a43587aaad9a.jpg',
+}, {
+	name: 'Macop',
+	icon: 'https://assets.misskey-hub.net/patrons/ee052bf550014d36a643ce3dce595640.jpg',
+}, {
+	name: 'なっかあ',
+	icon: 'https://assets.misskey-hub.net/patrons/c2f5f3e394e74a64912284a2f4ca710e.jpg',
+}, {
+	name: '如月ユカ',
+	icon: 'https://assets.misskey-hub.net/patrons/f24a042076a041b6811a2f124eb620ca.jpg',
+}, {
+	name: 'Yatoigawa',
+	icon: 'https://assets.misskey-hub.net/patrons/505e3568885a4a488431a8f22b4553d0.jpg',
 }];
 
 const patrons = [
@@ -344,6 +376,10 @@ const patrons = [
 	'SHO SEKIGUCHI',
 	'塩キャベツ',
 	'はとぽぷさん',
+	'100の人 (エスパー・イーシア)',
+	'ケモナーのケシン',
+	'こまつぶり',
+	'まゆつな空高',
 ];
 
 const thereIsTreasure = ref($i && !claimedAchievements.includes('foundTreasure'));
@@ -413,7 +449,7 @@ definePageMetadata(() => ({
 .znqjceqz {
 	> .about {
 		position: relative;
-		border-radius: var(--radius);
+		border-radius: var(--MI-radius);
 
 		> .treasure {
 			position: absolute;
@@ -501,17 +537,17 @@ definePageMetadata(() => ({
 	display: flex;
 	align-items: center;
 	padding: 12px;
-	background: var(--buttonBg);
+	background: var(--MI_THEME-buttonBg);
 	border-radius: 6px;
 
 	&:hover {
 		text-decoration: none;
-		background: var(--buttonHoverBg);
+		background: var(--MI_THEME-buttonHoverBg);
 	}
 
 	&.active {
-		color: var(--accent);
-		background: var(--buttonHoverBg);
+		color: var(--MI_THEME-accent);
+		background: var(--MI_THEME-buttonHoverBg);
 	}
 }
 
@@ -534,7 +570,7 @@ definePageMetadata(() => ({
 	display: flex;
 	align-items: center;
 	padding: 12px;
-	background: var(--buttonBg);
+	background: var(--MI_THEME-buttonBg);
 	border-radius: 6px;
 }
 
diff --git a/packages/frontend/src/pages/about.federation.vue b/packages/frontend/src/pages/about.federation.vue
index 24e96b4f4e..0a7cb8a50b 100644
--- a/packages/frontend/src/pages/about.federation.vue
+++ b/packages/frontend/src/pages/about.federation.vue
@@ -10,7 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 			<template #prefix><i class="ti ti-search"></i></template>
 			<template #label>{{ i18n.ts.host }}</template>
 		</MkInput>
-		<FormSplit style="margin-top: var(--margin);">
+		<FormSplit style="margin-top: var(--MI-margin);">
 			<MkSelect v-model="state">
 				<template #label>{{ i18n.ts.state }}</template>
 				<option value="all">{{ i18n.ts.all }}</option>
@@ -71,9 +71,9 @@ const pagination = {
 		sort: sort.value,
 		host: host.value !== '' ? host.value : null,
 		...(
-			state.value === 'federating' ? { federating: true } :
-			state.value === 'subscribing' ? { subscribing: true } :
-			state.value === 'publishing' ? { publishing: true } :
+			state.value === 'federating' ? { federating: true, suspended: false, blocked: false } :
+			state.value === 'subscribing' ? { subscribing: true, suspended: false, blocked: false } :
+			state.value === 'publishing' ? { publishing: true, suspended: false, blocked: false } :
 			state.value === 'suspended' ? { suspended: true } :
 			state.value === 'blocked' ? { blocked: true } :
 			state.value === 'silenced' ? { silenced: true } :
diff --git a/packages/frontend/src/pages/about.overview.vue b/packages/frontend/src/pages/about.overview.vue
new file mode 100644
index 0000000000..e5e57c05c4
--- /dev/null
+++ b/packages/frontend/src/pages/about.overview.vue
@@ -0,0 +1,205 @@
+<!--
+SPDX-FileCopyrightText: syuilo and misskey-project
+SPDX-License-Identifier: AGPL-3.0-only
+-->
+
+<template>
+<div class="_gaps_m">
+	<div :class="$style.banner" :style="{ backgroundImage: `url(${ instance.bannerUrl })` }">
+		<div style="overflow: clip;">
+			<img :src="instance.iconUrl ?? instance.faviconUrl ?? '/favicon.ico'" alt="" :class="$style.bannerIcon"/>
+			<div :class="$style.bannerName">
+				<b>{{ instance.name ?? host }}</b>
+			</div>
+		</div>
+	</div>
+
+	<MkKeyValue>
+		<template #key>{{ i18n.ts.description }}</template>
+		<template #value><div v-html="instance.description"></div></template>
+	</MkKeyValue>
+
+	<FormSection>
+		<div class="_gaps_m">
+			<MkKeyValue :copy="version">
+				<template #key>Misskey</template>
+				<template #value>{{ version }}</template>
+			</MkKeyValue>
+			<div v-html="i18n.tsx.poweredByMisskeyDescription({ name: instance.name ?? host })">
+			</div>
+			<FormLink to="/about-misskey">
+				<template #icon><i class="ti ti-info-circle"></i></template>
+				{{ i18n.ts.aboutMisskey }}
+			</FormLink>
+			<FormLink v-if="instance.repositoryUrl || instance.providesTarball" :to="instance.repositoryUrl || `/tarball/misskey-${version}.tar.gz`" external>
+				<template #icon><i class="ti ti-code"></i></template>
+				{{ i18n.ts.sourceCode }}
+			</FormLink>
+			<MkInfo v-else warn>
+				{{ i18n.ts.sourceCodeIsNotYetProvided }}
+			</MkInfo>
+		</div>
+	</FormSection>
+
+	<FormSection>
+		<div class="_gaps_m">
+			<FormSplit>
+				<MkKeyValue :copy="instance.maintainerName">
+					<template #key>{{ i18n.ts.administrator }}</template>
+					<template #value>
+						<template v-if="instance.maintainerName">{{ instance.maintainerName }}</template>
+						<span v-else style="opacity: 0.7;">({{ i18n.ts.none }})</span>
+					</template>
+				</MkKeyValue>
+				<MkKeyValue :copy="instance.maintainerEmail">
+					<template #key>{{ i18n.ts.contact }}</template>
+					<template #value>
+						<template v-if="instance.maintainerEmail">{{ instance.maintainerEmail }}</template>
+						<span v-else style="opacity: 0.7;">({{ i18n.ts.none }})</span>
+					</template>
+				</MkKeyValue>
+				<MkKeyValue>
+					<template #key>{{ i18n.ts.inquiry }}</template>
+					<template #value>
+						<MkLink v-if="instance.inquiryUrl" :url="instance.inquiryUrl" target="_blank">{{ instance.inquiryUrl }}</MkLink>
+						<span v-else style="opacity: 0.7;">({{ i18n.ts.none }})</span>
+					</template>
+				</MkKeyValue>
+			</FormSplit>
+			<div class="_gaps_s">
+				<FormLink v-if="instance.impressumUrl" :to="instance.impressumUrl" external>
+					<template #icon><i class="ti ti-user-shield"></i></template>
+					<template #default>{{ i18n.ts.impressum }}</template>
+				</FormLink>
+				<MkFolder v-if="instance.serverRules.length > 0">
+					<template #icon><i class="ti ti-checkup-list"></i></template>
+					<template #label>{{ i18n.ts.serverRules }}</template>
+					<ol class="_gaps_s" :class="$style.rules">
+						<li v-for="item in instance.serverRules" :key="item" :class="$style.rule">
+							<div :class="$style.ruleText" v-html="item"></div>
+						</li>
+					</ol>
+				</MkFolder>
+				<FormLink v-if="instance.tosUrl" :to="instance.tosUrl" external>
+					<template #icon><i class="ti ti-license"></i></template>
+					<template #default>{{ i18n.ts.termsOfService }}</template>
+				</FormLink>
+				<FormLink v-if="instance.privacyPolicyUrl" :to="instance.privacyPolicyUrl" external>
+					<template #icon><i class="ti ti-shield-lock"></i></template>
+					<template #default>{{ i18n.ts.privacyPolicy }}</template>
+				</FormLink>
+				<FormLink v-if="instance.feedbackUrl" :to="instance.feedbackUrl" external>
+					<template #icon><i class="ti ti-message"></i></template>
+					<template #default>{{ i18n.ts.feedback }}</template>
+				</FormLink>
+			</div>
+		</div>
+	</FormSection>
+
+	<FormSuspense v-slot="{ result: stats }" :p="initStats">
+		<FormSection>
+			<template #label>{{ i18n.ts.statistics }}</template>
+			<FormSplit>
+				<MkKeyValue>
+					<template #key>{{ i18n.ts.users }}</template>
+					<template #value>{{ number(stats.originalUsersCount) }}</template>
+				</MkKeyValue>
+				<MkKeyValue>
+					<template #key>{{ i18n.ts.notes }}</template>
+					<template #value>{{ number(stats.originalNotesCount) }}</template>
+				</MkKeyValue>
+			</FormSplit>
+		</FormSection>
+	</FormSuspense>
+
+	<FormSection>
+		<template #label>Well-known resources</template>
+		<div class="_gaps_s">
+			<FormLink to="/.well-known/host-meta" external>host-meta</FormLink>
+			<FormLink to="/.well-known/host-meta.json" external>host-meta.json</FormLink>
+			<FormLink to="/.well-known/nodeinfo" external>nodeinfo</FormLink>
+			<FormLink to="/robots.txt" external>robots.txt</FormLink>
+			<FormLink to="/manifest.json" external>manifest.json</FormLink>
+		</div>
+	</FormSection>
+</div>
+</template>
+
+<script lang="ts" setup>
+import { host, version } from '@@/js/config.js';
+import { i18n } from '@/i18n.js';
+import { instance } from '@/instance.js';
+import number from '@/filters/number.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
+import FormLink from '@/components/form/link.vue';
+import FormSection from '@/components/form/section.vue';
+import FormSplit from '@/components/form/split.vue';
+import FormSuspense from '@/components/form/suspense.vue';
+import MkFolder from '@/components/MkFolder.vue';
+import MkKeyValue from '@/components/MkKeyValue.vue';
+import MkLink from '@/components/MkLink.vue';
+
+const initStats = () => misskeyApi('stats', {});
+</script>
+
+<style lang="scss" module>
+.banner {
+	text-align: center;
+	border-radius: 10px;
+	overflow: clip;
+	background-color: var(--MI_THEME-panel);
+	background-size: cover;
+	background-position: center center;
+}
+
+.bannerIcon {
+	display: block;
+	margin: 16px auto 0 auto;
+	height: 64px;
+	border-radius: 8px;
+}
+
+.bannerName {
+	display: block;
+	padding: 16px;
+	color: #fff;
+	text-shadow: 0 0 8px #000;
+	background: linear-gradient(transparent, rgba(0, 0, 0, 0.7));
+}
+
+.rules {
+	counter-reset: item;
+	list-style: none;
+	padding: 0;
+	margin: 0;
+}
+
+.rule {
+	display: flex;
+	gap: 8px;
+	word-break: break-word;
+
+	&::before {
+		flex-shrink: 0;
+		display: flex;
+		position: sticky;
+		top: calc(var(--MI-stickyTop, 0px) + 8px);
+		counter-increment: item;
+		content: counter(item);
+		width: 32px;
+		height: 32px;
+		line-height: 32px;
+		background-color: var(--MI_THEME-accentedBg);
+		color: var(--MI_THEME-accent);
+		font-size: 13px;
+		font-weight: bold;
+		align-items: center;
+		justify-content: center;
+		border-radius: 999px;
+	}
+}
+
+.ruleText {
+	padding-top: 6px;
+}
+</style>
diff --git a/packages/frontend/src/pages/about.vue b/packages/frontend/src/pages/about.vue
index 324d1c11de..8dfeb6d2a7 100644
--- a/packages/frontend/src/pages/about.vue
+++ b/packages/frontend/src/pages/about.vue
@@ -8,113 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 	<template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template>
 	<MkHorizontalSwipe v-model:tab="tab" :tabs="headerTabs">
 		<MkSpacer v-if="tab === 'overview'" :contentMax="600" :marginMin="20">
-			<div class="_gaps_m">
-				<div :class="$style.banner" :style="{ backgroundImage: `url(${ instance.bannerUrl })` }">
-					<div style="overflow: clip;">
-						<img :src="instance.iconUrl ?? instance.faviconUrl ?? '/favicon.ico'" alt="" :class="$style.bannerIcon"/>
-						<div :class="$style.bannerName">
-							<b>{{ instance.name ?? host }}</b>
-						</div>
-					</div>
-				</div>
-
-				<MkKeyValue>
-					<template #key>{{ i18n.ts.description }}</template>
-					<template #value><div v-html="instance.description"></div></template>
-				</MkKeyValue>
-
-				<FormSection>
-					<div class="_gaps_m">
-						<MkKeyValue :copy="version">
-							<template #key>Misskey</template>
-							<template #value>{{ version }}</template>
-						</MkKeyValue>
-						<div v-html="i18n.tsx.poweredByMisskeyDescription({ name: instance.name ?? host })">
-						</div>
-						<FormLink to="/about-misskey">
-							<template #icon><i class="ti ti-info-circle"></i></template>
-							{{ i18n.ts.aboutMisskey }}
-						</FormLink>
-						<FormLink v-if="instance.repositoryUrl || instance.providesTarball" :to="instance.repositoryUrl || `/tarball/misskey-${version}.tar.gz`" external>
-							<template #icon><i class="ti ti-code"></i></template>
-							{{ i18n.ts.sourceCode }}
-						</FormLink>
-						<MkInfo v-else warn>
-							{{ i18n.ts.sourceCodeIsNotYetProvided }}
-						</MkInfo>
-					</div>
-				</FormSection>
-
-				<FormSection>
-					<div class="_gaps_m">
-						<FormSplit>
-							<MkKeyValue>
-								<template #key>{{ i18n.ts.administrator }}</template>
-								<template #value>{{ instance.maintainerName }}</template>
-							</MkKeyValue>
-							<MkKeyValue>
-								<template #key>{{ i18n.ts.contact }}</template>
-								<template #value>{{ instance.maintainerEmail }}</template>
-							</MkKeyValue>
-						</FormSplit>
-						<FormLink v-if="instance.impressumUrl" :to="instance.impressumUrl" external>
-							<template #icon><i class="ti ti-user-shield"></i></template>
-							{{ i18n.ts.impressum }}
-						</FormLink>
-						<div class="_gaps_s">
-							<MkFolder v-if="instance.serverRules.length > 0">
-								<template #label>
-									<i class="ti ti-checkup-list"></i>
-									{{ i18n.ts.serverRules }}
-								</template>
-
-								<ol class="_gaps_s" :class="$style.rules">
-									<li v-for="(item, index) in instance.serverRules" :key="index" :class="$style.rule"><div :class="$style.ruleText" v-html="item"></div></li>
-								</ol>
-							</MkFolder>
-							<FormLink v-if="instance.tosUrl" :to="instance.tosUrl" external>
-								<template #icon><i class="ti ti-license"></i></template>
-								{{ i18n.ts.termsOfService }}
-							</FormLink>
-							<FormLink v-if="instance.privacyPolicyUrl" :to="instance.privacyPolicyUrl" external>
-								<template #icon><i class="ti ti-shield-lock"></i></template>
-								{{ i18n.ts.privacyPolicy }}
-							</FormLink>
-							<FormLink v-if="instance.feedbackUrl" :to="instance.feedbackUrl" external>
-								<template #icon><i class="ti ti-message"></i></template>
-								{{ i18n.ts.feedback }}
-							</FormLink>
-						</div>
-					</div>
-				</FormSection>
-
-				<FormSuspense :p="initStats">
-					<FormSection>
-						<template #label>{{ i18n.ts.statistics }}</template>
-						<FormSplit>
-							<MkKeyValue>
-								<template #key>{{ i18n.ts.users }}</template>
-								<template #value>{{ number(stats.originalUsersCount) }}</template>
-							</MkKeyValue>
-							<MkKeyValue>
-								<template #key>{{ i18n.ts.notes }}</template>
-								<template #value>{{ number(stats.originalNotesCount) }}</template>
-							</MkKeyValue>
-						</FormSplit>
-					</FormSection>
-				</FormSuspense>
-
-				<FormSection>
-					<template #label>Well-known resources</template>
-					<div class="_gaps_s">
-						<FormLink :to="`/.well-known/host-meta`" external>host-meta</FormLink>
-						<FormLink :to="`/.well-known/host-meta.json`" external>host-meta.json</FormLink>
-						<FormLink :to="`/.well-known/nodeinfo`" external>nodeinfo</FormLink>
-						<FormLink :to="`/robots.txt`" external>robots.txt</FormLink>
-						<FormLink :to="`/manifest.json`" external>manifest.json</FormLink>
-					</div>
-				</FormSection>
-			</div>
+			<XOverview/>
 		</MkSpacer>
 		<MkSpacer v-else-if="tab === 'emojis'" :contentMax="1000" :marginMin="20">
 			<XEmojis/>
@@ -130,26 +24,16 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { computed, watch, ref } from 'vue';
-import * as Misskey from 'misskey-js';
-import XEmojis from './about.emojis.vue';
-import XFederation from './about.federation.vue';
-import { version, host } from '@/config.js';
-import FormLink from '@/components/form/link.vue';
-import FormSection from '@/components/form/section.vue';
-import FormSuspense from '@/components/form/suspense.vue';
-import FormSplit from '@/components/form/split.vue';
-import MkFolder from '@/components/MkFolder.vue';
-import MkKeyValue from '@/components/MkKeyValue.vue';
-import MkInfo from '@/components/MkInfo.vue';
-import MkInstanceStats from '@/components/MkInstanceStats.vue';
-import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue';
-import { misskeyApi } from '@/scripts/misskey-api.js';
-import number from '@/filters/number.js';
+import { computed, defineAsyncComponent, ref, watch } from 'vue';
 import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
 import { claimAchievement } from '@/scripts/achievements.js';
-import { instance } from '@/instance.js';
+import { definePageMetadata } from '@/scripts/page-metadata.js';
+import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue';
+
+const XOverview = defineAsyncComponent(() => import('@/pages/about.overview.vue'));
+const XEmojis = defineAsyncComponent(() => import('@/pages/about.emojis.vue'));
+const XFederation = defineAsyncComponent(() => import('@/pages/about.federation.vue'));
+const MkInstanceStats = defineAsyncComponent(() => import('@/components/MkInstanceStats.vue'));
 
 const props = withDefaults(defineProps<{
 	initialTab?: string;
@@ -157,7 +41,6 @@ const props = withDefaults(defineProps<{
 	initialTab: 'overview',
 });
 
-const stats = ref<Misskey.entities.StatsResponse | null>(null);
 const tab = ref(props.initialTab);
 
 watch(tab, () => {
@@ -166,11 +49,6 @@ watch(tab, () => {
 	}
 });
 
-const initStats = () => misskeyApi('stats', {
-}).then((res) => {
-	stats.value = res;
-});
-
 const headerActions = computed(() => []);
 
 const headerTabs = computed(() => [{
@@ -195,64 +73,3 @@ definePageMetadata(() => ({
 	icon: 'ti ti-info-circle',
 }));
 </script>
-
-<style lang="scss" module>
-.banner {
-	text-align: center;
-	border-radius: 10px;
-	overflow: clip;
-	background-size: cover;
-	background-position: center center;
-}
-
-.bannerIcon {
-	display: block;
-	margin: 16px auto 0 auto;
-	height: 64px;
-	border-radius: 8px;
-}
-
-.bannerName {
-	display: block;
-	padding: 16px;
-	color: #fff;
-	text-shadow: 0 0 8px #000;
-	background: linear-gradient(transparent, rgba(0, 0, 0, 0.7));
-}
-
-.rules {
-	counter-reset: item;
-	list-style: none;
-	padding: 0;
-	margin: 0;
-}
-
-.rule {
-	display: flex;
-	gap: 8px;
-	word-break: break-word;
-
-	&::before {
-		flex-shrink: 0;
-		display: flex;
-		position: sticky;
-		top: calc(var(--stickyTop, 0px) + 8px);
-		counter-increment: item;
-		content: counter(item);
-		width: 32px;
-		height: 32px;
-		line-height: 32px;
-		background-color: var(--accentedBg);
-		color: var(--accent);
-		font-size: 13px;
-		font-weight: bold;
-		align-items: center;
-		justify-content: center;
-		border-radius: 999px;
-	}
-}
-
-.ruleText {
-	padding-top: 6px;
-}
-</style>
diff --git a/packages/frontend/src/pages/admin-file.vue b/packages/frontend/src/pages/admin-file.vue
index d8311186ab..60f6be51d4 100644
--- a/packages/frontend/src/pages/admin-file.vue
+++ b/packages/frontend/src/pages/admin-file.vue
@@ -44,6 +44,9 @@ SPDX-License-Identifier: AGPL-3.0-only
 				<MkButton danger @click="del"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton>
 			</div>
 		</div>
+		<div v-else-if="tab === 'notes' && info" class="_gaps_m">
+			<XNotes :fileId="fileId"/>
+		</div>
 		<div v-else-if="tab === 'ip' && info" class="_gaps_m">
 			<MkInfo v-if="!iAmAdmin" warn>{{ i18n.ts.requireAdminForView }}</MkInfo>
 			<MkKeyValue v-if="info.requestIp" class="_monospace" :copy="info.requestIp" oneline>
@@ -67,7 +70,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { computed, ref } from 'vue';
+import { computed, defineAsyncComponent, ref } from 'vue';
 import * as Misskey from 'misskey-js';
 import MkButton from '@/components/MkButton.vue';
 import MkSwitch from '@/components/MkSwitch.vue';
@@ -88,6 +91,7 @@ const tab = ref('overview');
 const file = ref<Misskey.entities.DriveFile | null>(null);
 const info = ref<Misskey.entities.AdminDriveShowFileResponse | null>(null);
 const isSensitive = ref<boolean>(false);
+const XNotes = defineAsyncComponent(() => import('./drive.file.notes.vue'));
 
 const props = defineProps<{
 	fileId: string,
@@ -131,6 +135,10 @@ const headerTabs = computed(() => [{
 	title: i18n.ts.overview,
 	icon: 'ti ti-info-circle',
 }, iAmModerator ? {
+	key: 'notes',
+	title: i18n.ts._fileViewer.attachedNotes,
+	icon: 'ti ti-pencil',
+} : null, iAmModerator ? {
 	key: 'ip',
 	title: 'IP',
 	icon: 'ti ti-password',
diff --git a/packages/frontend/src/pages/admin-user.vue b/packages/frontend/src/pages/admin-user.vue
index f57aa51b5b..948e7a3cce 100644
--- a/packages/frontend/src/pages/admin-user.vue
+++ b/packages/frontend/src/pages/admin-user.vue
@@ -22,7 +22,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 					</div>
 				</div>
 
-				<MkInfo v-if="user.username.includes('.')">{{ i18n.ts.isSystemAccount }}</MkInfo>
+				<MkInfo v-if="['instance.actor', 'relay.actor'].includes(user.username)">{{ i18n.ts.isSystemAccount }}</MkInfo>
 
 				<FormLink v-if="user.host" :to="`/instance-info/${user.host}`">{{ i18n.ts.instanceInfo }}</FormLink>
 
@@ -53,6 +53,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 				<MkTextarea v-model="moderationNote" manualSave>
 					<template #label>{{ i18n.ts.moderationNote }}</template>
+					<template #caption>{{ i18n.ts.moderationNoteDescription }}</template>
 				</MkTextarea>
 
 				<!--
@@ -152,15 +153,21 @@ SPDX-License-Identifier: AGPL-3.0-only
 			<div v-else-if="tab === 'announcements'" class="_gaps">
 				<MkButton primary rounded @click="createAnnouncement"><i class="ti ti-plus"></i> {{ i18n.ts.new }}</MkButton>
 
+				<MkSelect v-model="announcementsStatus">
+					<template #label>{{ i18n.ts.filter }}</template>
+					<option value="active">{{ i18n.ts.active }}</option>
+					<option value="archived">{{ i18n.ts.archived }}</option>
+				</MkSelect>
+
 				<MkPagination :pagination="announcementsPagination">
 					<template #default="{ items }">
 						<div class="_gaps_s">
 							<div v-for="announcement in items" :key="announcement.id" v-panel :class="$style.announcementItem" @click="editAnnouncement(announcement)">
 								<span style="margin-right: 0.5em;">
 									<i v-if="announcement.icon === 'info'" class="ti ti-info-circle"></i>
-									<i v-else-if="announcement.icon === 'warning'" class="ti ti-alert-triangle" style="color: var(--warn);"></i>
-									<i v-else-if="announcement.icon === 'error'" class="ti ti-circle-x" style="color: var(--error);"></i>
-									<i v-else-if="announcement.icon === 'success'" class="ti ti-check" style="color: var(--success);"></i>
+									<i v-else-if="announcement.icon === 'warning'" class="ti ti-alert-triangle" style="color: var(--MI_THEME-warn);"></i>
+									<i v-else-if="announcement.icon === 'error'" class="ti ti-circle-x" style="color: var(--MI_THEME-error);"></i>
+									<i v-else-if="announcement.icon === 'success'" class="ti ti-check" style="color: var(--MI_THEME-success);"></i>
 								</span>
 								<span>{{ announcement.title }}</span>
 								<span v-if="announcement.reads > 0" style="margin-left: auto; opacity: 0.7;">{{ i18n.ts.messageRead }}</span>
@@ -205,6 +212,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 <script lang="ts" setup>
 import { computed, defineAsyncComponent, watch, ref } from 'vue';
 import * as Misskey from 'misskey-js';
+import { url } from '@@/js/config.js';
 import MkChart from '@/components/MkChart.vue';
 import MkObjectView from '@/components/MkObjectView.vue';
 import MkTextarea from '@/components/MkTextarea.vue';
@@ -220,7 +228,6 @@ import MkFileListForAdmin from '@/components/MkFileListForAdmin.vue';
 import MkInfo from '@/components/MkInfo.vue';
 import * as os from '@/os.js';
 import { misskeyApi } from '@/scripts/misskey-api.js';
-import { url } from '@/config.js';
 import { acct } from '@/filters/user.js';
 import { definePageMetadata } from '@/scripts/page-metadata.js';
 import { i18n } from '@/i18n.js';
@@ -253,11 +260,15 @@ const filesPagination = {
 		userId: props.userId,
 	})),
 };
+
+const announcementsStatus = ref<'active' | 'archived'>('active');
+
 const announcementsPagination = {
 	endpoint: 'admin/announcements/list' as const,
 	limit: 10,
 	params: computed(() => ({
 		userId: props.userId,
+		status: announcementsStatus.value,
 	})),
 };
 const expandedRoles = ref([]);
@@ -464,16 +475,20 @@ function toggleRoleItem(role) {
 }
 
 function createAnnouncement() {
-	os.popup(defineAsyncComponent(() => import('@/components/MkUserAnnouncementEditDialog.vue')), {
+	const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkUserAnnouncementEditDialog.vue')), {
 		user: user.value,
-	}, {}, 'closed');
+	}, {
+		closed: () => dispose(),
+	});
 }
 
 function editAnnouncement(announcement) {
-	os.popup(defineAsyncComponent(() => import('@/components/MkUserAnnouncementEditDialog.vue')), {
+	const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkUserAnnouncementEditDialog.vue')), {
 		user: user.value,
 		announcement,
-	}, {}, 'closed');
+	}, {
+		closed: () => dispose(),
+	});
 }
 
 watch(() => props.userId, () => {
@@ -577,18 +592,18 @@ definePageMetadata(() => ({
 			}
 
 			> .suspended {
-				color: var(--error);
-				border-color: var(--error);
+				color: var(--MI_THEME-error);
+				border-color: var(--MI_THEME-error);
 			}
 
 			> .silenced {
-				color: var(--warn);
-				border-color: var(--warn);
+				color: var(--MI_THEME-warn);
+				border-color: var(--MI_THEME-warn);
 			}
 
 			> .moderator {
-				color: var(--success);
-				border-color: var(--success);
+				color: var(--MI_THEME-success);
+				border-color: var(--MI_THEME-success);
 			}
 		}
 	}
@@ -635,7 +650,7 @@ definePageMetadata(() => ({
 .roleItemSub {
 	padding: 6px 12px;
 	font-size: 85%;
-	color: var(--fgTransparentWeak);
+	color: var(--MI_THEME-fgTransparentWeak);
 }
 
 .roleUnassign {
diff --git a/packages/frontend/src/pages/admin/RolesEditorFormula.vue b/packages/frontend/src/pages/admin/RolesEditorFormula.vue
index f001a4ac20..4762ef3f97 100644
--- a/packages/frontend/src/pages/admin/RolesEditorFormula.vue
+++ b/packages/frontend/src/pages/admin/RolesEditorFormula.vue
@@ -155,12 +155,12 @@ function removeSelf() {
 }
 
 .item {
-	border: solid 2px var(--divider);
-	border-radius: var(--radius);
+	border: solid 2px var(--MI_THEME-divider);
+	border-radius: var(--MI-radius);
 	padding: 12px;
 
 	&:hover {
-		border-color: var(--accent);
+		border-color: var(--MI_THEME-accent);
 	}
 }
 </style>
diff --git a/packages/frontend/src/pages/admin/_header_.vue b/packages/frontend/src/pages/admin/_header_.vue
index c5a9609e6e..9b1bf51f58 100644
--- a/packages/frontend/src/pages/admin/_header_.vue
+++ b/packages/frontend/src/pages/admin/_header_.vue
@@ -24,8 +24,8 @@ SPDX-License-Identifier: AGPL-3.0-only
 	<div class="buttons right">
 		<template v-if="actions">
 			<template v-for="action in actions">
-				<MkButton v-if="action.asFullButton" class="fullButton" primary @click.stop="action.handler"><i :class="action.icon" style="margin-right: 6px;"></i>{{ action.text }}</MkButton>
-				<button v-else v-tooltip.noDelay="action.text" class="_button button" :class="{ highlighted: action.highlighted }" @click.stop="action.handler" @touchstart="preventDrag"><i :class="action.icon"></i></button>
+				<MkButton v-if="action.asFullButton" class="fullButton" primary :disabled="action.disabled" @click.stop="action.handler"><i :class="action.icon" style="margin-right: 6px;"></i>{{ action.text }}</MkButton>
+				<button v-else v-tooltip.noDelay="action.text" class="_button button" :class="{ highlighted: action.highlighted }" :disabled="action.disabled" @click.stop="action.handler" @touchstart="preventDrag"><i :class="action.icon"></i></button>
 			</template>
 		</template>
 	</div>
@@ -36,7 +36,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { computed, onMounted, onUnmounted, ref, shallowRef, watch, nextTick } from 'vue';
 import tinycolor from 'tinycolor2';
 import { popupMenu } from '@/os.js';
-import { scrollToTop } from '@/scripts/scroll.js';
+import { scrollToTop } from '@@/js/scroll.js';
 import MkButton from '@/components/MkButton.vue';
 import { globalEvents } from '@/events.js';
 import { injectReactiveMetadata } from '@/scripts/page-metadata.js';
@@ -56,6 +56,7 @@ const props = defineProps<{
 		text: string;
 		icon: string;
 		asFullButton?: boolean;
+		disabled?: boolean;
 		handler: (ev: MouseEvent) => void;
 	}[];
 	thin?: boolean;
@@ -118,7 +119,7 @@ function onTabClick(tab: Tab, ev: MouseEvent): void {
 }
 
 const calcBg = () => {
-	const rawBg = pageMetadata.value?.bg ?? 'var(--bg)';
+	const rawBg = pageMetadata.value?.bg ?? 'var(--MI_THEME-bg)';
 	const tinyBg = tinycolor(rawBg.startsWith('var(') ? getComputedStyle(document.documentElement).getPropertyValue(rawBg.slice(4, -1)) : rawBg);
 	tinyBg.setAlpha(0.85);
 	bg.value = tinyBg.toRgbString();
@@ -155,8 +156,8 @@ onUnmounted(() => {
 	--height: 60px;
 	display: flex;
 	width: 100%;
-	-webkit-backdrop-filter: var(--blur, blur(15px));
-	backdrop-filter: var(--blur, blur(15px));
+	-webkit-backdrop-filter: var(--MI-blur, blur(15px));
+	backdrop-filter: var(--MI-blur, blur(15px));
 
 	> .buttons {
 		--margin: 8px;
@@ -188,7 +189,7 @@ onUnmounted(() => {
 			}
 
 			&.highlighted {
-				color: var(--accent);
+				color: var(--MI_THEME-accent);
 			}
 		}
 
@@ -285,7 +286,7 @@ onUnmounted(() => {
 			position: absolute;
 			bottom: 0;
 			height: 3px;
-			background: var(--accent);
+			background: var(--MI_THEME-accent);
 			border-radius: 999px;
 			transition: all 0.2s ease;
 			pointer-events: none;
diff --git a/packages/frontend/src/pages/admin/abuse-report/notification-recipient.editor.vue b/packages/frontend/src/pages/admin/abuse-report/notification-recipient.editor.vue
new file mode 100644
index 0000000000..eef24afd32
--- /dev/null
+++ b/packages/frontend/src/pages/admin/abuse-report/notification-recipient.editor.vue
@@ -0,0 +1,321 @@
+<!--
+SPDX-FileCopyrightText: syuilo and misskey-project
+SPDX-License-Identifier: AGPL-3.0-only
+-->
+
+<template>
+<MkModalWindow
+	ref="dialogEl"
+	:width="400"
+	:height="490"
+	:withOkButton="false"
+	:okButtonDisabled="false"
+	@close="onCancelClicked"
+	@closed="emit('closed')"
+>
+	<template #header>
+		{{ mode === 'create' ? i18n.ts._abuseReport._notificationRecipient.createRecipient : i18n.ts._abuseReport._notificationRecipient.modifyRecipient }}
+	</template>
+	<div v-if="loading === 0" style="display: flex; flex-direction: column; min-height: 100%;">
+		<MkSpacer :marginMin="20" :marginMax="28" style="flex-grow: 1;">
+			<div :class="$style.root" class="_gaps_m">
+				<MkInput v-model="title">
+					<template #label>{{ i18n.ts.title }}</template>
+				</MkInput>
+				<MkSelect v-model="method">
+					<template #label>{{ i18n.ts._abuseReport._notificationRecipient.recipientType }}</template>
+					<option value="email">{{ i18n.ts._abuseReport._notificationRecipient._recipientType.mail }}</option>
+					<option value="webhook">{{ i18n.ts._abuseReport._notificationRecipient._recipientType.webhook }}</option>
+					<template #caption>
+						{{ methodCaption }}
+					</template>
+				</MkSelect>
+				<div>
+					<MkSelect v-if="method === 'email'" v-model="userId">
+						<template #label>{{ i18n.ts._abuseReport._notificationRecipient.notifiedUser }}</template>
+						<option v-for="user in moderators" :key="user.id" :value="user.id">
+							{{ user.name ? `${user.name}(${user.username})` : user.username }}
+						</option>
+					</MkSelect>
+					<div v-else-if="method === 'webhook'" :class="$style.systemWebhook">
+						<MkSelect v-model="systemWebhookId" style="flex: 1">
+							<template #label>{{ i18n.ts._abuseReport._notificationRecipient.notifiedWebhook }}</template>
+							<option v-for="webhook in systemWebhooks" :key="webhook.id ?? undefined" :value="webhook.id">
+								{{ webhook.name }}
+							</option>
+						</MkSelect>
+						<MkButton rounded :class="$style.systemWebhookEditButton" @click="onEditSystemWebhookClicked">
+							<span v-if="systemWebhookId === null" class="ti ti-plus" style="line-height: normal"/>
+							<span v-else class="ti ti-settings" style="line-height: normal"/>
+						</MkButton>
+					</div>
+				</div>
+
+				<MkDivider/>
+
+				<MkSwitch v-model="isActive">
+					<template #label>{{ i18n.ts.enable }}</template>
+				</MkSwitch>
+			</div>
+		</MkSpacer>
+
+		<div :class="$style.footer" class="_buttonsCenter">
+			<MkButton primary rounded :disabled="disableSubmitButton" @click="onSubmitClicked"><i class="ti ti-check"></i> {{ i18n.ts.ok }}</MkButton>
+			<MkButton rounded @click="onCancelClicked"><i class="ti ti-x"></i> {{ i18n.ts.cancel }}</MkButton>
+		</div>
+	</div>
+	<div v-else>
+		<MkLoading/>
+	</div>
+</MkModalWindow>
+</template>
+
+<script lang="ts" setup>
+import { computed, onMounted, ref, shallowRef, toRefs } from 'vue';
+import { entities } from 'misskey-js';
+import MkButton from '@/components/MkButton.vue';
+import MkModalWindow from '@/components/MkModalWindow.vue';
+import { i18n } from '@/i18n.js';
+import MkInput from '@/components/MkInput.vue';
+import { misskeyApi } from '@/scripts/misskey-api.js';
+import MkSelect from '@/components/MkSelect.vue';
+import { MkSystemWebhookResult, showSystemWebhookEditorDialog } from '@/components/MkSystemWebhookEditor.impl.js';
+import MkSwitch from '@/components/MkSwitch.vue';
+import MkDivider from '@/components/MkDivider.vue';
+import * as os from '@/os.js';
+
+type NotificationRecipientMethod = 'email' | 'webhook';
+
+const emit = defineEmits<{
+	(ev: 'submitted'): void;
+	(ev: 'canceled'): void;
+	(ev: 'closed'): void;
+}>();
+
+const props = defineProps<{
+	mode: 'create' | 'edit';
+	id?: string;
+}>();
+
+const { mode, id } = toRefs(props);
+
+const dialogEl = shallowRef<InstanceType<typeof MkModalWindow>>();
+
+const loading = ref<number>(0);
+
+const title = ref<string>('');
+const method = ref<NotificationRecipientMethod>('email');
+const userId = ref<string | null>(null);
+const systemWebhookId = ref<string | null>(null);
+const isActive = ref<boolean>(true);
+
+const moderators = ref<entities.User[]>([]);
+const systemWebhooks = ref<(entities.SystemWebhook | { id: null, name: string })[]>([]);
+
+const methodCaption = computed(() => {
+	switch (method.value) {
+		case 'email': {
+			return i18n.ts._abuseReport._notificationRecipient._recipientType._captions.mail;
+		}
+		case 'webhook': {
+			return i18n.ts._abuseReport._notificationRecipient._recipientType._captions.webhook;
+		}
+		default: {
+			return '';
+		}
+	}
+});
+
+const disableSubmitButton = computed(() => {
+	if (!title.value) {
+		return true;
+	}
+
+	switch (method.value) {
+		case 'email': {
+			return userId.value === null;
+		}
+		case 'webhook': {
+			return systemWebhookId.value === null;
+		}
+		default: {
+			return true;
+		}
+	}
+});
+
+async function onSubmitClicked() {
+	await loadingScope(async () => {
+		const _userId = (method.value === 'email') ? userId.value : null;
+		const _systemWebhookId = (method.value === 'webhook') ? systemWebhookId.value : null;
+		const params = {
+			isActive: isActive.value,
+			name: title.value,
+			method: method.value,
+			userId: _userId ?? undefined,
+			systemWebhookId: _systemWebhookId ?? undefined,
+		};
+
+		try {
+			switch (mode.value) {
+				case 'create': {
+					await misskeyApi('admin/abuse-report/notification-recipient/create', params);
+					break;
+				}
+				case 'edit': {
+					// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+					await misskeyApi('admin/abuse-report/notification-recipient/update', { id: id.value!, ...params });
+					break;
+				}
+			}
+
+			dialogEl.value?.close();
+			emit('submitted');
+			// eslint-disable-next-line @typescript-eslint/no-explicit-any
+		} catch (ex: any) {
+			const msg = ex.message ?? i18n.ts.internalServerErrorDescription;
+			await os.alert({ type: 'error', title: i18n.ts.error, text: msg });
+			dialogEl.value?.close();
+			emit('canceled');
+		}
+	});
+}
+
+function onCancelClicked() {
+	dialogEl.value?.close();
+	emit('canceled');
+}
+
+async function onEditSystemWebhookClicked() {
+	let result: MkSystemWebhookResult | null;
+	if (systemWebhookId.value === null) {
+		result = await showSystemWebhookEditorDialog({
+			mode: 'create',
+		});
+	} else {
+		result = await showSystemWebhookEditorDialog({
+			mode: 'edit',
+			id: systemWebhookId.value,
+		});
+	}
+	if (!result) {
+		return;
+	}
+
+	await fetchSystemWebhooks();
+	systemWebhookId.value = result.id ?? null;
+}
+
+async function fetchSystemWebhooks() {
+	await loadingScope(async () => {
+		systemWebhooks.value = [
+			{ id: null, name: i18n.ts.createNew },
+			...await misskeyApi('admin/system-webhook/list', { }),
+		];
+	});
+}
+
+async function fetchModerators() {
+	await loadingScope(async () => {
+		const users = Array.of<entities.User>();
+		for (; ;) {
+			const res = await misskeyApi('admin/show-users', {
+				limit: 100,
+				state: 'adminOrModerator',
+				origin: 'local',
+				offset: users.length,
+			});
+
+			if (res.length === 0) {
+				break;
+			}
+
+			users.push(...res);
+		}
+
+		moderators.value = users;
+	});
+}
+
+async function loadingScope<T>(fn: () => Promise<T>): Promise<T> {
+	loading.value++;
+	try {
+		return await fn();
+	} finally {
+		loading.value--;
+	}
+}
+
+onMounted(async () => {
+	await loadingScope(async () => {
+		await fetchModerators();
+		await fetchSystemWebhooks();
+
+		if (mode.value === 'edit') {
+			if (!id.value) {
+				throw new Error('id is required');
+			}
+
+			try {
+				const res = await misskeyApi('admin/abuse-report/notification-recipient/show', { id: id.value });
+
+				title.value = res.name;
+				method.value = res.method;
+				userId.value = res.userId ?? null;
+				systemWebhookId.value = res.systemWebhookId ?? null;
+				isActive.value = res.isActive;
+				// eslint-disable-next-line
+			} catch (ex: any) {
+				const msg = ex.message ?? i18n.ts.internalServerErrorDescription;
+				await os.alert({ type: 'error', title: i18n.ts.error, text: msg });
+				dialogEl.value?.close();
+				emit('canceled');
+			}
+		} else {
+			userId.value = moderators.value[0]?.id ?? null;
+			systemWebhookId.value = systemWebhooks.value[0]?.id ?? null;
+		}
+	});
+});
+
+</script>
+
+<style lang="scss" module>
+.root {
+	display: flex;
+	flex-direction: column;
+	justify-content: flex-start;
+	align-items: stretch;
+}
+
+.footer {
+	position: sticky;
+	z-index: 10000;
+	bottom: 0;
+	left: 0;
+	padding: 12px;
+	border-top: solid 0.5px var(--MI_THEME-divider);
+	background: var(--MI_THEME-acrylicBg);
+	-webkit-backdrop-filter: var(--MI-blur, blur(15px));
+	backdrop-filter: var(--MI-blur, blur(15px));
+}
+
+.systemWebhook {
+	display: flex;
+	flex-direction: row;
+	justify-content: stretch;
+	align-items: flex-end;
+	gap: 8px;
+}
+
+.systemWebhookEditButton {
+	min-width: 0;
+	min-height: 0;
+	width: 34px;
+	height: 34px;
+	flex-shrink: 0;
+	box-sizing: border-box;
+	margin: 1px 0;
+	padding: 6px;
+}
+</style>
diff --git a/packages/frontend/src/pages/admin/abuse-report/notification-recipient.item.vue b/packages/frontend/src/pages/admin/abuse-report/notification-recipient.item.vue
new file mode 100644
index 0000000000..36d586bd23
--- /dev/null
+++ b/packages/frontend/src/pages/admin/abuse-report/notification-recipient.item.vue
@@ -0,0 +1,114 @@
+<!--
+SPDX-FileCopyrightText: syuilo and misskey-project
+SPDX-License-Identifier: AGPL-3.0-only
+-->
+
+<template>
+<div :class="$style.root" class="_panel _gaps_s">
+	<div :class="$style.rightDivider" style="width: 80px;"><span :class="`ti ${methodIcon}`"/> {{ methodName }}</div>
+	<div :class="$style.rightDivider" style="flex: 0.5">{{ entity.name }}</div>
+	<div :class="$style.rightDivider" style="flex: 1">
+		<div v-if="method === 'email' && user">
+			{{
+				`${i18n.ts._abuseReport._notificationRecipient.notifiedUser}: ` + ((user.name) ? `${user.name}(${user.username})` : user.username)
+			}}
+		</div>
+		<div v-if="method === 'webhook' && systemWebhook">
+			{{ `${i18n.ts._abuseReport._notificationRecipient.notifiedWebhook}: ` + systemWebhook.name }}
+		</div>
+	</div>
+	<div :class="$style.recipientButtons" style="margin-left: auto">
+		<button :class="$style.recipientButton" @click="onEditButtonClicked()">
+			<span class="ti ti-settings"/>
+		</button>
+		<button :class="$style.recipientButton" @click="onDeleteButtonClicked()">
+			<span class="ti ti-trash"/>
+		</button>
+	</div>
+</div>
+</template>
+
+<script setup lang="ts">
+import { entities } from 'misskey-js';
+import { computed, toRefs } from 'vue';
+import { i18n } from '@/i18n.js';
+
+const emit = defineEmits<{
+	(ev: 'edit', id: entities.AbuseReportNotificationRecipient['id']): void;
+	(ev: 'delete', id: entities.AbuseReportNotificationRecipient['id']): void;
+}>();
+
+const props = defineProps<{
+	entity: entities.AbuseReportNotificationRecipient;
+}>();
+
+const { entity } = toRefs(props);
+
+const method = computed(() => entity.value.method);
+const user = computed(() => entity.value.user);
+const systemWebhook = computed(() => entity.value.systemWebhook);
+const methodIcon = computed(() => {
+	switch (entity.value.method) {
+		case 'email':
+			return 'ti-mail';
+		case 'webhook':
+			return 'ti-webhook';
+		default:
+			return 'ti-help';
+	}
+});
+const methodName = computed(() => {
+	switch (entity.value.method) {
+		case 'email':
+			return i18n.ts._abuseReport._notificationRecipient._recipientType.mail;
+		case 'webhook':
+			return i18n.ts._abuseReport._notificationRecipient._recipientType.webhook;
+		default:
+			return '不明';
+	}
+});
+
+function onEditButtonClicked() {
+	emit('edit', entity.value.id);
+}
+
+function onDeleteButtonClicked() {
+	emit('delete', entity.value.id);
+}
+</script>
+
+<style module lang="scss">
+.root {
+	display: flex;
+	flex-direction: row;
+	justify-content: center;
+	align-items: center;
+	padding: 4px 8px;
+}
+
+.rightDivider {
+	border-right: 0.5px solid var(--MI_THEME-divider);
+}
+
+.recipientButtons {
+	display: flex;
+	flex-direction: row;
+	justify-content: center;
+	align-items: center;
+	margin-right: -4;
+}
+
+.recipientButton {
+	background-color: transparent;
+	border: none;
+	border-radius: 9999px;
+	box-sizing: border-box;
+	margin-top: -2px;
+	margin-bottom: -2px;
+	padding: 8px;
+
+	&:hover {
+		background-color: var(--MI_THEME-buttonBg);
+	}
+}
+</style>
diff --git a/packages/frontend/src/pages/admin/abuse-report/notification-recipient.vue b/packages/frontend/src/pages/admin/abuse-report/notification-recipient.vue
new file mode 100644
index 0000000000..f5249261be
--- /dev/null
+++ b/packages/frontend/src/pages/admin/abuse-report/notification-recipient.vue
@@ -0,0 +1,177 @@
+<!--
+SPDX-FileCopyrightText: syuilo and misskey-project
+SPDX-License-Identifier: AGPL-3.0-only
+-->
+
+<template>
+<MkStickyContainer>
+	<template #header>
+		<XHeader :actions="headerActions" :tabs="headerTabs"/>
+	</template>
+
+	<MkSpacer :contentMax="900">
+		<div :class="$style.root" class="_gaps_m">
+			<div :class="$style.addButton">
+				<MkButton primary @click="onAddButtonClicked">
+					<span class="ti ti-plus"/> {{ i18n.ts._abuseReport._notificationRecipient.createRecipient }}
+				</MkButton>
+			</div>
+			<div :class="$style.subMenus" class="_gaps_s">
+				<MkSelect v-model="filterMethod" style="flex: 1">
+					<template #label>{{ i18n.ts._abuseReport._notificationRecipient.recipientType }}</template>
+					<option :value="null">-</option>
+					<option :value="'email'">{{ i18n.ts._abuseReport._notificationRecipient._recipientType.mail }}</option>
+					<option :value="'webhook'">{{ i18n.ts._abuseReport._notificationRecipient._recipientType.webhook }}</option>
+				</MkSelect>
+				<MkInput v-model="filterText" type="search" style="flex: 1">
+					<template #label>{{ i18n.ts._abuseReport._notificationRecipient.keywords }}</template>
+				</MkInput>
+			</div>
+
+			<MkDivider/>
+
+			<div :class="$style.recipients" class="_gaps_s">
+				<XRecipient
+					v-for="r in filteredRecipients"
+					:key="r.id"
+					:entity="r"
+					@edit="onEditButtonClicked"
+					@delete="onDeleteButtonClicked"
+				/>
+			</div>
+		</div>
+	</MkSpacer>
+</MkStickyContainer>
+</template>
+
+<script setup lang="ts">
+import { entities } from 'misskey-js';
+import { computed, defineAsyncComponent, onMounted, ref } from 'vue';
+import XRecipient from './notification-recipient.item.vue';
+import XHeader from '@/pages/admin/_header_.vue';
+import { misskeyApi } from '@/scripts/misskey-api.js';
+import MkInput from '@/components/MkInput.vue';
+import MkSelect from '@/components/MkSelect.vue';
+import MkButton from '@/components/MkButton.vue';
+import * as os from '@/os.js';
+import MkDivider from '@/components/MkDivider.vue';
+import { i18n } from '@/i18n.js';
+
+const recipients = ref<entities.AbuseReportNotificationRecipient[]>([]);
+
+const filterMethod = ref<string | null>(null);
+const filterText = ref<string>('');
+
+const filteredRecipients = computed(() => {
+	const method = filterMethod.value;
+	const text = filterText.value.trim().length === 0 ? null : filterText.value;
+
+	return recipients.value.filter(it => {
+		if (method ?? text) {
+			if (text) {
+				const keywords = [it.name, it.systemWebhook?.name, it.user?.name, it.user?.username];
+				if (keywords.filter(k => k?.includes(text)).length !== 0) {
+					return true;
+				}
+			}
+
+			if (method) {
+				return it.method.includes(method);
+			}
+
+			return false;
+		}
+
+		return true;
+	});
+});
+const headerActions = computed(() => []);
+const headerTabs = computed(() => []);
+
+async function onAddButtonClicked() {
+	await showEditor('create');
+}
+
+async function onEditButtonClicked(id: string) {
+	await showEditor('edit', id);
+}
+
+async function onDeleteButtonClicked(id: string) {
+	const res = await os.confirm({
+		type: 'warning',
+		title: i18n.ts._abuseReport._notificationRecipient.deleteConfirm,
+	});
+	if (!res.canceled) {
+		await misskeyApi('admin/abuse-report/notification-recipient/delete', { id: id });
+		await fetchRecipients();
+	}
+}
+
+async function showEditor(mode: 'create' | 'edit', id?: string) {
+	const { needLoad } = await new Promise<{ needLoad: boolean }>(async resolve => {
+		const { dispose } = os.popup(
+			defineAsyncComponent(() => import('./notification-recipient.editor.vue')),
+			{
+				mode,
+				id,
+			},
+			{
+				submitted: () => {
+					resolve({ needLoad: true });
+				},
+				canceled: () => {
+					resolve({ needLoad: false });
+				},
+				closed: () => {
+					dispose();
+				},
+			},
+		);
+	});
+
+	if (needLoad) {
+		await fetchRecipients();
+	}
+}
+
+async function fetchRecipients() {
+	const result = await misskeyApi('admin/abuse-report/notification-recipient/list', {
+		method: ['email', 'webhook'],
+	});
+
+	recipients.value = result.sort((a, b) => (a.method + a.id).localeCompare(b.method + b.id));
+}
+
+onMounted(async () => {
+	await fetchRecipients();
+});
+</script>
+
+<style module lang="scss">
+.root {
+	display: flex;
+	flex-direction: column;
+	justify-content: center;
+	align-items: stretch;
+}
+
+.addButton {
+	display: flex;
+	justify-content: flex-end;
+	gap: 8px;
+}
+
+.subMenus {
+	display: flex;
+	flex-direction: row;
+	justify-content: space-between;
+	align-items: flex-end;
+}
+
+.recipients {
+	display: flex;
+	flex-direction: column;
+	justify-content: flex-start;
+	align-items: stretch;
+}
+</style>
diff --git a/packages/frontend/src/pages/admin/abuses.vue b/packages/frontend/src/pages/admin/abuses.vue
index d2f4a4b531..22173bb888 100644
--- a/packages/frontend/src/pages/admin/abuses.vue
+++ b/packages/frontend/src/pages/admin/abuses.vue
@@ -7,30 +7,37 @@ SPDX-License-Identifier: AGPL-3.0-only
 <MkStickyContainer>
 	<template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template>
 	<MkSpacer :contentMax="900">
-		<div>
-			<div class="reports">
-				<div class="">
-					<div class="inputs" style="display: flex;">
-						<MkSelect v-model="state" style="margin: 0; flex: 1;">
-							<template #label>{{ i18n.ts.state }}</template>
-							<option value="all">{{ i18n.ts.all }}</option>
-							<option value="unresolved">{{ i18n.ts.unresolved }}</option>
-							<option value="resolved">{{ i18n.ts.resolved }}</option>
-						</MkSelect>
-						<MkSelect v-model="targetUserOrigin" style="margin: 0; flex: 1;">
-							<template #label>{{ i18n.ts.reporteeOrigin }}</template>
-							<option value="combined">{{ i18n.ts.all }}</option>
-							<option value="local">{{ i18n.ts.local }}</option>
-							<option value="remote">{{ i18n.ts.remote }}</option>
-						</MkSelect>
-						<MkSelect v-model="reporterOrigin" style="margin: 0; flex: 1;">
-							<template #label>{{ i18n.ts.reporterOrigin }}</template>
-							<option value="combined">{{ i18n.ts.all }}</option>
-							<option value="local">{{ i18n.ts.local }}</option>
-							<option value="remote">{{ i18n.ts.remote }}</option>
-						</MkSelect>
-					</div>
-					<!-- TODO
+		<div :class="$style.root" class="_gaps">
+			<div :class="$style.subMenus" class="_gaps">
+				<MkButton link to="/admin/abuse-report-notification-recipient" primary>{{ i18n.ts.notificationSetting }}</MkButton>
+			</div>
+
+			<MkInfo v-if="!defaultStore.reactiveState.abusesTutorial.value" closable @close="closeTutorial()">
+				{{ i18n.ts._abuseUserReport.resolveTutorial }}
+			</MkInfo>
+
+			<div :class="$style.inputs" class="_gaps">
+				<MkSelect v-model="state" style="margin: 0; flex: 1;">
+					<template #label>{{ i18n.ts.state }}</template>
+					<option value="all">{{ i18n.ts.all }}</option>
+					<option value="unresolved">{{ i18n.ts.unresolved }}</option>
+					<option value="resolved">{{ i18n.ts.resolved }}</option>
+				</MkSelect>
+				<MkSelect v-model="targetUserOrigin" style="margin: 0; flex: 1;">
+					<template #label>{{ i18n.ts.reporteeOrigin }}</template>
+					<option value="combined">{{ i18n.ts.all }}</option>
+					<option value="local">{{ i18n.ts.local }}</option>
+					<option value="remote">{{ i18n.ts.remote }}</option>
+				</MkSelect>
+				<MkSelect v-model="reporterOrigin" style="margin: 0; flex: 1;">
+					<template #label>{{ i18n.ts.reporterOrigin }}</template>
+					<option value="combined">{{ i18n.ts.all }}</option>
+					<option value="local">{{ i18n.ts.local }}</option>
+					<option value="remote">{{ i18n.ts.remote }}</option>
+				</MkSelect>
+			</div>
+
+			<!-- TODO
 			<div class="inputs" style="display: flex; padding-top: 1.2em;">
 				<MkInput v-model="searchUsername" style="margin: 0; flex: 1;" type="text" :spellcheck="false">
 					<span>{{ i18n.ts.username }}</span>
@@ -41,11 +48,11 @@ SPDX-License-Identifier: AGPL-3.0-only
 			</div>
 			-->
 
-					<MkPagination v-slot="{items}" ref="reports" :pagination="pagination" style="margin-top: var(--margin);">
-						<XAbuseReport v-for="report in items" :key="report.id" :report="report" @resolved="resolved"/>
-					</MkPagination>
+			<MkPagination v-slot="{items}" ref="reports" :pagination="pagination">
+				<div class="_gaps">
+					<XAbuseReport v-for="report in items" :key="report.id" :report="report" @resolved="resolved"/>
 				</div>
-			</div>
+			</MkPagination>
 		</div>
 	</MkSpacer>
 </MkStickyContainer>
@@ -53,13 +60,15 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <script lang="ts" setup>
 import { computed, shallowRef, ref } from 'vue';
-
 import XHeader from './_header_.vue';
 import MkSelect from '@/components/MkSelect.vue';
 import MkPagination from '@/components/MkPagination.vue';
 import XAbuseReport from '@/components/MkAbuseReport.vue';
 import { i18n } from '@/i18n.js';
 import { definePageMetadata } from '@/scripts/page-metadata.js';
+import MkButton from '@/components/MkButton.vue';
+import MkInfo from '@/components/MkInfo.vue';
+import { defaultStore } from '@/store.js';
 
 const reports = shallowRef<InstanceType<typeof MkPagination>>();
 
@@ -80,7 +89,11 @@ const pagination = {
 };
 
 function resolved(reportId) {
-	reports.value.removeItem(reportId);
+	reports.value?.removeItem(reportId);
+}
+
+function closeTutorial() {
+	defaultStore.set('abusesTutorial', false);
 }
 
 const headerActions = computed(() => []);
@@ -92,3 +105,26 @@ definePageMetadata(() => ({
 	icon: 'ti ti-exclamation-circle',
 }));
 </script>
+
+<style module lang="scss">
+.root {
+	display: flex;
+	flex-direction: column;
+	justify-content: center;
+	align-items: stretch;
+}
+
+.subMenus {
+	display: flex;
+	flex-direction: row;
+	justify-content: flex-end;
+	align-items: center;
+}
+
+.inputs {
+	display: flex;
+	flex-direction: row;
+	justify-content: space-between;
+	align-items: center;
+}
+</style>
diff --git a/packages/frontend/src/pages/admin/ads.vue b/packages/frontend/src/pages/admin/ads.vue
index bd442ccc69..0d67359e47 100644
--- a/packages/frontend/src/pages/admin/ads.vue
+++ b/packages/frontend/src/pages/admin/ads.vue
@@ -65,18 +65,18 @@ SPDX-License-Identifier: AGPL-3.0-only
 				<MkTextarea v-model="ad.memo">
 					<template #label>{{ i18n.ts.memo }}</template>
 				</MkTextarea>
-				<div class="buttons">
-					<MkButton class="button" inline primary style="margin-right: 12px;" @click="save(ad)">
+				<div class="_buttons">
+					<MkButton inline primary style="margin-right: 12px;" @click="save(ad)">
 						<i
 							class="ti ti-device-floppy"
 						></i> {{ i18n.ts.save }}
 					</MkButton>
-					<MkButton class="button" inline danger @click="remove(ad)">
+					<MkButton inline danger @click="remove(ad)">
 						<i class="ti ti-trash"></i> {{ i18n.ts.remove }}
 					</MkButton>
 				</div>
 			</div>
-			<MkButton class="button" @click="more()">
+			<MkButton @click="more()">
 				<i class="ti ti-reload"></i>{{ i18n.ts.more }}
 			</MkButton>
 		</div>
@@ -266,7 +266,7 @@ definePageMetadata(() => ({
 	padding: 32px;
 
 	&:not(:last-child) {
-		margin-bottom: var(--margin);
+		margin-bottom: var(--MI-margin);
 	}
 }
 .input {
diff --git a/packages/frontend/src/pages/admin/announcements.vue b/packages/frontend/src/pages/admin/announcements.vue
index e7fb62ec1d..e420586017 100644
--- a/packages/frontend/src/pages/admin/announcements.vue
+++ b/packages/frontend/src/pages/admin/announcements.vue
@@ -11,70 +11,85 @@ SPDX-License-Identifier: AGPL-3.0-only
 			<MkInfo>{{ i18n.ts._announcement.shouldNotBeUsedToPresentPermanentInfo }}</MkInfo>
 			<MkInfo v-if="announcements.length > 5" warn>{{ i18n.ts._announcement.tooManyActiveAnnouncementDescription }}</MkInfo>
 
-			<MkFolder v-for="announcement in announcements" :key="announcement.id ?? announcement._id" :defaultOpen="announcement.id == null">
-				<template #label>{{ announcement.title }}</template>
-				<template #icon>
-					<i v-if="announcement.icon === 'info'" class="ti ti-info-circle"></i>
-					<i v-else-if="announcement.icon === 'warning'" class="ti ti-alert-triangle" style="color: var(--warn);"></i>
-					<i v-else-if="announcement.icon === 'error'" class="ti ti-circle-x" style="color: var(--error);"></i>
-					<i v-else-if="announcement.icon === 'success'" class="ti ti-check" style="color: var(--success);"></i>
-				</template>
-				<template #caption>{{ announcement.text }}</template>
+			<MkSelect v-model="announcementsStatus">
+				<template #label>{{ i18n.ts.filter }}</template>
+				<option value="active">{{ i18n.ts.active }}</option>
+				<option value="archived">{{ i18n.ts.archived }}</option>
+			</MkSelect>
 
-				<div class="_gaps_m">
-					<MkInput v-model="announcement.title">
-						<template #label>{{ i18n.ts.title }}</template>
-					</MkInput>
-					<MkTextarea v-model="announcement.text" mfmAutocomplete :mfmPreview="true">
-						<template #label>{{ i18n.ts.text }}</template>
-					</MkTextarea>
-					<MkInput v-model="announcement.imageUrl" type="url">
-						<template #label>{{ i18n.ts.imageUrl }}</template>
-					</MkInput>
-					<MkRadios v-model="announcement.icon">
-						<template #label>{{ i18n.ts.icon }}</template>
-						<option value="info"><i class="ti ti-info-circle"></i></option>
-						<option value="warning"><i class="ti ti-alert-triangle" style="color: var(--warn);"></i></option>
-						<option value="error"><i class="ti ti-circle-x" style="color: var(--error);"></i></option>
-						<option value="success"><i class="ti ti-check" style="color: var(--success);"></i></option>
-					</MkRadios>
-					<MkRadios v-model="announcement.display">
-						<template #label>{{ i18n.ts.display }}</template>
-						<option value="normal">{{ i18n.ts.normal }}</option>
-						<option value="banner">{{ i18n.ts.banner }}</option>
-						<option value="dialog">{{ i18n.ts.dialog }}</option>
-					</MkRadios>
-					<MkInfo v-if="announcement.display === 'dialog'" warn>{{ i18n.ts._announcement.dialogAnnouncementUxWarn }}</MkInfo>
-					<MkSwitch v-model="announcement.forExistingUsers" :helpText="i18n.ts._announcement.forExistingUsersDescription">
-						{{ i18n.ts._announcement.forExistingUsers }}
-					</MkSwitch>
-					<MkSwitch v-model="announcement.silence" :helpText="i18n.ts._announcement.silenceDescription">
-						{{ i18n.ts._announcement.silence }}
-					</MkSwitch>
-					<MkSwitch v-model="announcement.needConfirmationToRead" :helpText="i18n.ts._announcement.needConfirmationToReadDescription">
-						{{ i18n.ts._announcement.needConfirmationToRead }}
-					</MkSwitch>
-					<p v-if="announcement.reads">{{ i18n.tsx.nUsersRead({ n: announcement.reads }) }}</p>
-					<div class="buttons _buttons">
-						<MkButton class="button" inline primary @click="save(announcement)"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton>
-						<MkButton v-if="announcement.id != null" class="button" inline @click="archive(announcement)"><i class="ti ti-check"></i> {{ i18n.ts._announcement.end }} ({{ i18n.ts.archive }})</MkButton>
-						<MkButton v-if="announcement.id != null" class="button" inline danger @click="del(announcement)"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton>
+			<MkLoading v-if="loading"/>
+
+			<template v-else>
+				<MkFolder v-for="announcement in announcements" :key="announcement.id ?? announcement._id" :defaultOpen="announcement.id == null">
+					<template #label>{{ announcement.title }}</template>
+					<template #icon>
+						<i v-if="announcement.icon === 'info'" class="ti ti-info-circle"></i>
+						<i v-else-if="announcement.icon === 'warning'" class="ti ti-alert-triangle" style="color: var(--MI_THEME-warn);"></i>
+						<i v-else-if="announcement.icon === 'error'" class="ti ti-circle-x" style="color: var(--MI_THEME-error);"></i>
+						<i v-else-if="announcement.icon === 'success'" class="ti ti-check" style="color: var(--MI_THEME-success);"></i>
+					</template>
+					<template #caption>{{ announcement.text }}</template>
+					<template #footer>
+						<div class="_buttons">
+							<MkButton rounded primary @click="save(announcement)"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton>
+							<MkButton v-if="announcement.id != null && announcement.isActive" rounded @click="archive(announcement)"><i class="ti ti-check"></i> {{ i18n.ts._announcement.end }} ({{ i18n.ts.archive }})</MkButton>
+							<MkButton v-if="announcement.id != null && !announcement.isActive" rounded @click="unarchive(announcement)"><i class="ti ti-restore"></i> {{ i18n.ts.unarchive }}</MkButton>
+							<MkButton v-if="announcement.id != null" rounded danger @click="del(announcement)"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton>
+						</div>
+					</template>
+
+					<div class="_gaps">
+						<MkInput v-model="announcement.title">
+							<template #label>{{ i18n.ts.title }}</template>
+						</MkInput>
+						<MkTextarea v-model="announcement.text" mfmAutocomplete :mfmPreview="true">
+							<template #label>{{ i18n.ts.text }}</template>
+						</MkTextarea>
+						<MkInput v-model="announcement.imageUrl" type="url">
+							<template #label>{{ i18n.ts.imageUrl }}</template>
+						</MkInput>
+						<MkRadios v-model="announcement.icon">
+							<template #label>{{ i18n.ts.icon }}</template>
+							<option value="info"><i class="ti ti-info-circle"></i></option>
+							<option value="warning"><i class="ti ti-alert-triangle" style="color: var(--MI_THEME-warn);"></i></option>
+							<option value="error"><i class="ti ti-circle-x" style="color: var(--MI_THEME-error);"></i></option>
+							<option value="success"><i class="ti ti-check" style="color: var(--MI_THEME-success);"></i></option>
+						</MkRadios>
+						<MkRadios v-model="announcement.display">
+							<template #label>{{ i18n.ts.display }}</template>
+							<option value="normal">{{ i18n.ts.normal }}</option>
+							<option value="banner">{{ i18n.ts.banner }}</option>
+							<option value="dialog">{{ i18n.ts.dialog }}</option>
+						</MkRadios>
+						<MkInfo v-if="announcement.display === 'dialog'" warn>{{ i18n.ts._announcement.dialogAnnouncementUxWarn }}</MkInfo>
+						<MkSwitch v-model="announcement.forExistingUsers" :helpText="i18n.ts._announcement.forExistingUsersDescription">
+							{{ i18n.ts._announcement.forExistingUsers }}
+						</MkSwitch>
+						<MkSwitch v-model="announcement.silence" :helpText="i18n.ts._announcement.silenceDescription">
+							{{ i18n.ts._announcement.silence }}
+						</MkSwitch>
+						<MkSwitch v-model="announcement.needConfirmationToRead" :helpText="i18n.ts._announcement.needConfirmationToReadDescription">
+							{{ i18n.ts._announcement.needConfirmationToRead }}
+						</MkSwitch>
+						<p v-if="announcement.reads">{{ i18n.tsx.nUsersRead({ n: announcement.reads }) }}</p>
 					</div>
-				</div>
-			</MkFolder>
-			<MkButton class="button" @click="more()">
-				<i class="ti ti-reload"></i>{{ i18n.ts.more }}
-			</MkButton>
+				</MkFolder>
+				<MkLoading v-if="loadingMore"/>
+				<MkButton @click="more()">
+					<i class="ti ti-reload"></i>{{ i18n.ts.more }}
+				</MkButton>
+			</template>
 		</div>
 	</MkSpacer>
 </MkStickyContainer>
 </template>
 
 <script lang="ts" setup>
-import { ref, computed } from 'vue';
+import { ref, computed, watch } from 'vue';
 import XHeader from './_header_.vue';
 import MkButton from '@/components/MkButton.vue';
 import MkInput from '@/components/MkInput.vue';
+import MkSelect from '@/components/MkSelect.vue';
 import MkSwitch from '@/components/MkSwitch.vue';
 import MkRadios from '@/components/MkRadios.vue';
 import MkInfo from '@/components/MkInfo.vue';
@@ -85,11 +100,22 @@ import { definePageMetadata } from '@/scripts/page-metadata.js';
 import MkFolder from '@/components/MkFolder.vue';
 import MkTextarea from '@/components/MkTextarea.vue';
 
+const announcementsStatus = ref<'active' | 'archived'>('active');
+
+const loading = ref(true);
+const loadingMore = ref(false);
+
 const announcements = ref<any[]>([]);
 
-misskeyApi('admin/announcements/list').then(announcementResponse => {
-	announcements.value = announcementResponse;
-});
+watch(announcementsStatus, (to) => {
+	loading.value = true;
+	misskeyApi('admin/announcements/list', {
+		status: to,
+	}).then(announcementResponse => {
+		announcements.value = announcementResponse;
+		loading.value = false;
+	});
+}, { immediate: true });
 
 function add() {
 	announcements.value.unshift({
@@ -125,6 +151,14 @@ async function archive(announcement) {
 	refresh();
 }
 
+async function unarchive(announcement) {
+	await os.apiWithDialog('admin/announcements/update', {
+		...announcement,
+		isActive: true,
+	});
+	refresh();
+}
+
 async function save(announcement) {
 	if (announcement.id == null) {
 		await os.apiWithDialog('admin/announcements/create', announcement);
@@ -135,24 +169,32 @@ async function save(announcement) {
 }
 
 function more() {
-	misskeyApi('admin/announcements/list', { untilId: announcements.value.reduce((acc, announcement) => announcement.id != null ? announcement : acc).id }).then(announcementResponse => {
+	loadingMore.value = true;
+	misskeyApi('admin/announcements/list', {
+		status: announcementsStatus.value,
+		untilId: announcements.value.reduce((acc, announcement) => announcement.id != null ? announcement : acc).id,
+	}).then(announcementResponse => {
 		announcements.value = announcements.value.concat(announcementResponse);
+		loadingMore.value = false;
 	});
 }
 
 function refresh() {
-	misskeyApi('admin/announcements/list').then(announcementResponse => {
+	loading.value = true;
+	misskeyApi('admin/announcements/list', {
+		status: announcementsStatus.value,
+	}).then(announcementResponse => {
 		announcements.value = announcementResponse;
+		loading.value = false;
 	});
 }
 
-refresh();
-
 const headerActions = computed(() => [{
 	asFullButton: true,
 	icon: 'ti ti-plus',
 	text: i18n.ts.add,
 	handler: add,
+	disabled: announcementsStatus.value === 'archived',
 }]);
 
 const headerTabs = computed(() => []);
diff --git a/packages/frontend/src/pages/admin/bot-protection.vue b/packages/frontend/src/pages/admin/bot-protection.vue
index 73c5e1919f..d07add4408 100644
--- a/packages/frontend/src/pages/admin/bot-protection.vue
+++ b/packages/frontend/src/pages/admin/bot-protection.vue
@@ -4,145 +4,156 @@ SPDX-License-Identifier: AGPL-3.0-only
 -->
 
 <template>
-<div>
-	<FormSuspense :p="init">
-		<div class="_gaps_m">
-			<MkRadios v-model="provider">
-				<option :value="null">{{ i18n.ts.none }} ({{ i18n.ts.notRecommended }})</option>
-				<option value="hcaptcha">hCaptcha</option>
-				<option value="mcaptcha">mCaptcha</option>
-				<option value="recaptcha">reCAPTCHA</option>
-				<option value="turnstile">Turnstile</option>
-			</MkRadios>
+<MkFolder>
+	<template #icon><i class="ti ti-shield"></i></template>
+	<template #label>{{ i18n.ts.botProtection }}</template>
+	<template v-if="botProtectionForm.savedState.provider === 'hcaptcha'" #suffix>hCaptcha</template>
+	<template v-else-if="botProtectionForm.savedState.provider === 'mcaptcha'" #suffix>mCaptcha</template>
+	<template v-else-if="botProtectionForm.savedState.provider === 'recaptcha'" #suffix>reCAPTCHA</template>
+	<template v-else-if="botProtectionForm.savedState.provider === 'turnstile'" #suffix>Turnstile</template>
+	<template v-else-if="botProtectionForm.savedState.provider === 'testcaptcha'" #suffix>testCaptcha</template>
+	<template v-else #suffix>{{ i18n.ts.none }} ({{ i18n.ts.notRecommended }})</template>
+	<template v-if="botProtectionForm.modified.value" #footer>
+		<MkFormFooter :form="botProtectionForm"/>
+	</template>
 
-			<template v-if="provider === 'hcaptcha'">
-				<MkInput v-model="hcaptchaSiteKey">
-					<template #prefix><i class="ti ti-key"></i></template>
-					<template #label>{{ i18n.ts.hcaptchaSiteKey }}</template>
-				</MkInput>
-				<MkInput v-model="hcaptchaSecretKey">
-					<template #prefix><i class="ti ti-key"></i></template>
-					<template #label>{{ i18n.ts.hcaptchaSecretKey }}</template>
-				</MkInput>
-				<FormSlot>
-					<template #label>{{ i18n.ts.preview }}</template>
-					<MkCaptcha provider="hcaptcha" :sitekey="hcaptchaSiteKey || '10000000-ffff-ffff-ffff-000000000001'"/>
-				</FormSlot>
-			</template>
-			<template v-else-if="provider === 'mcaptcha'">
-				<MkInput v-model="mcaptchaSiteKey">
-					<template #prefix><i class="ti ti-key"></i></template>
-					<template #label>{{ i18n.ts.mcaptchaSiteKey }}</template>
-				</MkInput>
-				<MkInput v-model="mcaptchaSecretKey">
-					<template #prefix><i class="ti ti-key"></i></template>
-					<template #label>{{ i18n.ts.mcaptchaSecretKey }}</template>
-				</MkInput>
-				<MkInput v-model="mcaptchaInstanceUrl">
-					<template #prefix><i class="ti ti-link"></i></template>
-					<template #label>{{ i18n.ts.mcaptchaInstanceUrl }}</template>
-				</MkInput>
-				<FormSlot v-if="mcaptchaSiteKey && mcaptchaInstanceUrl">
-					<template #label>{{ i18n.ts.preview }}</template>
-					<MkCaptcha provider="mcaptcha" :sitekey="mcaptchaSiteKey" :instanceUrl="mcaptchaInstanceUrl"/>
-				</FormSlot>
-			</template>
-			<template v-else-if="provider === 'recaptcha'">
-				<MkInput v-model="recaptchaSiteKey">
-					<template #prefix><i class="ti ti-key"></i></template>
-					<template #label>{{ i18n.ts.recaptchaSiteKey }}</template>
-				</MkInput>
-				<MkInput v-model="recaptchaSecretKey">
-					<template #prefix><i class="ti ti-key"></i></template>
-					<template #label>{{ i18n.ts.recaptchaSecretKey }}</template>
-				</MkInput>
-				<FormSlot v-if="recaptchaSiteKey">
-					<template #label>{{ i18n.ts.preview }}</template>
-					<MkCaptcha provider="recaptcha" :sitekey="recaptchaSiteKey"/>
-				</FormSlot>
-			</template>
-			<template v-else-if="provider === 'turnstile'">
-				<MkInput v-model="turnstileSiteKey">
-					<template #prefix><i class="ti ti-key"></i></template>
-					<template #label>{{ i18n.ts.turnstileSiteKey }}</template>
-				</MkInput>
-				<MkInput v-model="turnstileSecretKey">
-					<template #prefix><i class="ti ti-key"></i></template>
-					<template #label>{{ i18n.ts.turnstileSecretKey }}</template>
-				</MkInput>
-				<FormSlot>
-					<template #label>{{ i18n.ts.preview }}</template>
-					<MkCaptcha provider="turnstile" :sitekey="turnstileSiteKey || '1x00000000000000000000AA'"/>
-				</FormSlot>
-			</template>
+	<div class="_gaps_m">
+		<MkRadios v-model="botProtectionForm.state.provider">
+			<option :value="null">{{ i18n.ts.none }} ({{ i18n.ts.notRecommended }})</option>
+			<option value="hcaptcha">hCaptcha</option>
+			<option value="mcaptcha">mCaptcha</option>
+			<option value="recaptcha">reCAPTCHA</option>
+			<option value="turnstile">Turnstile</option>
+			<option value="testcaptcha">testCaptcha</option>
+		</MkRadios>
 
-			<MkButton primary @click="save"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton>
-		</div>
-	</FormSuspense>
-</div>
+		<template v-if="botProtectionForm.state.provider === 'hcaptcha'">
+			<MkInput v-model="botProtectionForm.state.hcaptchaSiteKey">
+				<template #prefix><i class="ti ti-key"></i></template>
+				<template #label>{{ i18n.ts.hcaptchaSiteKey }}</template>
+			</MkInput>
+			<MkInput v-model="botProtectionForm.state.hcaptchaSecretKey">
+				<template #prefix><i class="ti ti-key"></i></template>
+				<template #label>{{ i18n.ts.hcaptchaSecretKey }}</template>
+			</MkInput>
+			<FormSlot>
+				<template #label>{{ i18n.ts.preview }}</template>
+				<MkCaptcha provider="hcaptcha" :sitekey="botProtectionForm.state.hcaptchaSiteKey || '10000000-ffff-ffff-ffff-000000000001'"/>
+			</FormSlot>
+		</template>
+		<template v-else-if="botProtectionForm.state.provider === 'mcaptcha'">
+			<MkInput v-model="botProtectionForm.state.mcaptchaSiteKey">
+				<template #prefix><i class="ti ti-key"></i></template>
+				<template #label>{{ i18n.ts.mcaptchaSiteKey }}</template>
+			</MkInput>
+			<MkInput v-model="botProtectionForm.state.mcaptchaSecretKey">
+				<template #prefix><i class="ti ti-key"></i></template>
+				<template #label>{{ i18n.ts.mcaptchaSecretKey }}</template>
+			</MkInput>
+			<MkInput v-model="botProtectionForm.state.mcaptchaInstanceUrl">
+				<template #prefix><i class="ti ti-link"></i></template>
+				<template #label>{{ i18n.ts.mcaptchaInstanceUrl }}</template>
+			</MkInput>
+			<FormSlot v-if="botProtectionForm.state.mcaptchaSiteKey && botProtectionForm.state.mcaptchaInstanceUrl">
+				<template #label>{{ i18n.ts.preview }}</template>
+				<MkCaptcha provider="mcaptcha" :sitekey="botProtectionForm.state.mcaptchaSiteKey" :instanceUrl="botProtectionForm.state.mcaptchaInstanceUrl"/>
+			</FormSlot>
+		</template>
+		<template v-else-if="botProtectionForm.state.provider === 'recaptcha'">
+			<MkInput v-model="botProtectionForm.state.recaptchaSiteKey">
+				<template #prefix><i class="ti ti-key"></i></template>
+				<template #label>{{ i18n.ts.recaptchaSiteKey }}</template>
+			</MkInput>
+			<MkInput v-model="botProtectionForm.state.recaptchaSecretKey">
+				<template #prefix><i class="ti ti-key"></i></template>
+				<template #label>{{ i18n.ts.recaptchaSecretKey }}</template>
+			</MkInput>
+			<FormSlot v-if="botProtectionForm.state.recaptchaSiteKey">
+				<template #label>{{ i18n.ts.preview }}</template>
+				<MkCaptcha provider="recaptcha" :sitekey="botProtectionForm.state.recaptchaSiteKey"/>
+			</FormSlot>
+		</template>
+		<template v-else-if="botProtectionForm.state.provider === 'turnstile'">
+			<MkInput v-model="botProtectionForm.state.turnstileSiteKey">
+				<template #prefix><i class="ti ti-key"></i></template>
+				<template #label>{{ i18n.ts.turnstileSiteKey }}</template>
+			</MkInput>
+			<MkInput v-model="botProtectionForm.state.turnstileSecretKey">
+				<template #prefix><i class="ti ti-key"></i></template>
+				<template #label>{{ i18n.ts.turnstileSecretKey }}</template>
+			</MkInput>
+			<FormSlot>
+				<template #label>{{ i18n.ts.preview }}</template>
+				<MkCaptcha provider="turnstile" :sitekey="botProtectionForm.state.turnstileSiteKey || '1x00000000000000000000AA'"/>
+			</FormSlot>
+		</template>
+		<template v-else-if="botProtectionForm.state.provider === 'testcaptcha'">
+			<MkInfo warn><span v-html="i18n.ts.testCaptchaWarning"></span></MkInfo>
+			<FormSlot>
+				<template #label>{{ i18n.ts.preview }}</template>
+				<MkCaptcha provider="testcaptcha"/>
+			</FormSlot>
+		</template>
+	</div>
+</MkFolder>
 </template>
 
 <script lang="ts" setup>
 import { defineAsyncComponent, ref } from 'vue';
-import type { CaptchaProvider } from '@/components/MkCaptcha.vue';
 import MkRadios from '@/components/MkRadios.vue';
 import MkInput from '@/components/MkInput.vue';
-import MkButton from '@/components/MkButton.vue';
-import FormSuspense from '@/components/form/suspense.vue';
 import FormSlot from '@/components/form/slot.vue';
 import * as os from '@/os.js';
 import { misskeyApi } from '@/scripts/misskey-api.js';
 import { fetchInstance } from '@/instance.js';
 import { i18n } from '@/i18n.js';
+import { useForm } from '@/scripts/use-form.js';
+import MkFormFooter from '@/components/MkFormFooter.vue';
+import MkFolder from '@/components/MkFolder.vue';
+import MkInfo from '@/components/MkInfo.vue';
 
 const MkCaptcha = defineAsyncComponent(() => import('@/components/MkCaptcha.vue'));
 
-const provider = ref<CaptchaProvider | null>(null);
-const hcaptchaSiteKey = ref<string | null>(null);
-const hcaptchaSecretKey = ref<string | null>(null);
-const mcaptchaSiteKey = ref<string | null>(null);
-const mcaptchaSecretKey = ref<string | null>(null);
-const mcaptchaInstanceUrl = ref<string | null>(null);
-const recaptchaSiteKey = ref<string | null>(null);
-const recaptchaSecretKey = ref<string | null>(null);
-const turnstileSiteKey = ref<string | null>(null);
-const turnstileSecretKey = ref<string | null>(null);
+const meta = await misskeyApi('admin/meta');
 
-async function init() {
-	const meta = await misskeyApi('admin/meta');
-	hcaptchaSiteKey.value = meta.hcaptchaSiteKey;
-	hcaptchaSecretKey.value = meta.hcaptchaSecretKey;
-	mcaptchaSiteKey.value = meta.mcaptchaSiteKey;
-	mcaptchaSecretKey.value = meta.mcaptchaSecretKey;
-	mcaptchaInstanceUrl.value = meta.mcaptchaInstanceUrl;
-	recaptchaSiteKey.value = meta.recaptchaSiteKey;
-	recaptchaSecretKey.value = meta.recaptchaSecretKey;
-	turnstileSiteKey.value = meta.turnstileSiteKey;
-	turnstileSecretKey.value = meta.turnstileSecretKey;
-
-	provider.value = meta.enableHcaptcha ? 'hcaptcha' :
-		meta.enableRecaptcha ? 'recaptcha' :
-		meta.enableTurnstile ? 'turnstile' :
-		meta.enableMcaptcha ? 'mcaptcha' : null;
-}
-
-function save() {
-	os.apiWithDialog('admin/update-meta', {
-		enableHcaptcha: provider.value === 'hcaptcha',
-		hcaptchaSiteKey: hcaptchaSiteKey.value,
-		hcaptchaSecretKey: hcaptchaSecretKey.value,
-		enableMcaptcha: provider.value === 'mcaptcha',
-		mcaptchaSiteKey: mcaptchaSiteKey.value,
-		mcaptchaSecretKey: mcaptchaSecretKey.value,
-		mcaptchaInstanceUrl: mcaptchaInstanceUrl.value,
-		enableRecaptcha: provider.value === 'recaptcha',
-		recaptchaSiteKey: recaptchaSiteKey.value,
-		recaptchaSecretKey: recaptchaSecretKey.value,
-		enableTurnstile: provider.value === 'turnstile',
-		turnstileSiteKey: turnstileSiteKey.value,
-		turnstileSecretKey: turnstileSecretKey.value,
-	}).then(() => {
-		fetchInstance(true);
+const botProtectionForm = useForm({
+	provider: meta.enableHcaptcha
+		? 'hcaptcha'
+		: meta.enableRecaptcha
+			? 'recaptcha'
+			: meta.enableTurnstile
+				? 'turnstile'
+				: meta.enableMcaptcha
+					? 'mcaptcha'
+					: meta.enableTestcaptcha
+						? 'testcaptcha'
+						: null,
+	hcaptchaSiteKey: meta.hcaptchaSiteKey,
+	hcaptchaSecretKey: meta.hcaptchaSecretKey,
+	mcaptchaSiteKey: meta.mcaptchaSiteKey,
+	mcaptchaSecretKey: meta.mcaptchaSecretKey,
+	mcaptchaInstanceUrl: meta.mcaptchaInstanceUrl,
+	recaptchaSiteKey: meta.recaptchaSiteKey,
+	recaptchaSecretKey: meta.recaptchaSecretKey,
+	turnstileSiteKey: meta.turnstileSiteKey,
+	turnstileSecretKey: meta.turnstileSecretKey,
+}, async (state) => {
+	await os.apiWithDialog('admin/update-meta', {
+		enableHcaptcha: state.provider === 'hcaptcha',
+		hcaptchaSiteKey: state.hcaptchaSiteKey,
+		hcaptchaSecretKey: state.hcaptchaSecretKey,
+		enableMcaptcha: state.provider === 'mcaptcha',
+		mcaptchaSiteKey: state.mcaptchaSiteKey,
+		mcaptchaSecretKey: state.mcaptchaSecretKey,
+		mcaptchaInstanceUrl: state.mcaptchaInstanceUrl,
+		enableRecaptcha: state.provider === 'recaptcha',
+		recaptchaSiteKey: state.recaptchaSiteKey,
+		recaptchaSecretKey: state.recaptchaSecretKey,
+		enableTurnstile: state.provider === 'turnstile',
+		turnstileSiteKey: state.turnstileSiteKey,
+		turnstileSecretKey: state.turnstileSecretKey,
+		enableTestcaptcha: state.provider === 'testcaptcha',
 	});
-}
+	fetchInstance(true);
+});
 </script>
diff --git a/packages/frontend/src/pages/admin/branding.vue b/packages/frontend/src/pages/admin/branding.vue
index fe1b7c561d..95f82c1f24 100644
--- a/packages/frontend/src/pages/admin/branding.vue
+++ b/packages/frontend/src/pages/admin/branding.vue
@@ -117,7 +117,7 @@ import { i18n } from '@/i18n.js';
 import { definePageMetadata } from '@/scripts/page-metadata.js';
 import MkButton from '@/components/MkButton.vue';
 import MkColorInput from '@/components/MkColorInput.vue';
-import { host } from '@/config.js';
+import { host } from '@@/js/config.js';
 
 const iconUrl = ref<string | null>(null);
 const app192IconUrl = ref<string | null>(null);
@@ -183,7 +183,7 @@ definePageMetadata(() => ({
 
 <style lang="scss" module>
 .footer {
-	-webkit-backdrop-filter: var(--blur, blur(15px));
-	backdrop-filter: var(--blur, blur(15px));
+	-webkit-backdrop-filter: var(--MI-blur, blur(15px));
+	backdrop-filter: var(--MI-blur, blur(15px));
 }
 </style>
diff --git a/packages/frontend/src/pages/admin/email-settings.vue b/packages/frontend/src/pages/admin/email-settings.vue
index 4a858887f3..5b60e67dac 100644
--- a/packages/frontend/src/pages/admin/email-settings.vue
+++ b/packages/frontend/src/pages/admin/email-settings.vue
@@ -138,7 +138,7 @@ definePageMetadata(() => ({
 
 <style lang="scss" module>
 .footer {
-	-webkit-backdrop-filter: var(--blur, blur(15px));
-	backdrop-filter: var(--blur, blur(15px));
+	-webkit-backdrop-filter: var(--MI-blur, blur(15px));
+	backdrop-filter: var(--MI-blur, blur(15px));
 }
 </style>
diff --git a/packages/frontend/src/pages/admin/external-services.vue b/packages/frontend/src/pages/admin/external-services.vue
index e0b82eb02e..91f41166e9 100644
--- a/packages/frontend/src/pages/admin/external-services.vue
+++ b/packages/frontend/src/pages/admin/external-services.vue
@@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 	<template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template>
 	<MkSpacer :contentMax="700" :marginMin="16" :marginMax="32">
 		<FormSuspense :p="init">
-			<FormSection>
+			<MkFolder>
 				<template #label>DeepL Translation</template>
 
 				<div class="_gaps_m">
@@ -19,17 +19,11 @@ SPDX-License-Identifier: AGPL-3.0-only
 					<MkSwitch v-model="deeplIsPro">
 						<template #label>Pro account</template>
 					</MkSwitch>
+					<MkButton primary @click="save_deepl">Save</MkButton>
 				</div>
-			</FormSection>
+			</MkFolder>
 		</FormSuspense>
 	</MkSpacer>
-	<template #footer>
-		<div :class="$style.footer">
-			<MkSpacer :contentMax="700" :marginMin="16" :marginMax="16">
-				<MkButton primary rounded @click="save"><i class="ti ti-check"></i> {{ i18n.ts.save }}</MkButton>
-			</MkSpacer>
-		</div>
-	</template>
 </MkStickyContainer>
 </template>
 
@@ -40,12 +34,12 @@ import MkInput from '@/components/MkInput.vue';
 import MkButton from '@/components/MkButton.vue';
 import MkSwitch from '@/components/MkSwitch.vue';
 import FormSuspense from '@/components/form/suspense.vue';
-import FormSection from '@/components/form/section.vue';
 import * as os from '@/os.js';
 import { misskeyApi } from '@/scripts/misskey-api.js';
 import { fetchInstance } from '@/instance.js';
 import { i18n } from '@/i18n.js';
 import { definePageMetadata } from '@/scripts/page-metadata.js';
+import MkFolder from '@/components/MkFolder.vue';
 
 const deeplAuthKey = ref<string>('');
 const deeplIsPro = ref<boolean>(false);
@@ -56,7 +50,7 @@ async function init() {
 	deeplIsPro.value = meta.deeplIsPro;
 }
 
-function save() {
+function save_deepl() {
 	os.apiWithDialog('admin/update-meta', {
 		deeplAuthKey: deeplAuthKey.value,
 		deeplIsPro: deeplIsPro.value,
@@ -74,10 +68,3 @@ definePageMetadata(() => ({
 	icon: 'ti ti-link',
 }));
 </script>
-
-<style lang="scss" module>
-.footer {
-	-webkit-backdrop-filter: var(--blur, blur(15px));
-	backdrop-filter: var(--blur, blur(15px));
-}
-</style>
diff --git a/packages/frontend/src/pages/admin/federation.vue b/packages/frontend/src/pages/admin/federation.vue
index 0aaa398584..e7b9fd8621 100644
--- a/packages/frontend/src/pages/admin/federation.vue
+++ b/packages/frontend/src/pages/admin/federation.vue
@@ -14,7 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 						<template #prefix><i class="ti ti-search"></i></template>
 						<template #label>{{ i18n.ts.host }}</template>
 					</MkInput>
-					<FormSplit style="margin-top: var(--margin);">
+					<FormSplit style="margin-top: var(--MI-margin);">
 						<MkSelect v-model="state">
 							<template #label>{{ i18n.ts.state }}</template>
 							<option value="all">{{ i18n.ts.all }}</option>
@@ -80,9 +80,9 @@ const pagination = {
 		sort: sort.value,
 		host: host.value !== '' ? host.value : null,
 		...(
-			state.value === 'federating' ? { federating: true } :
-			state.value === 'subscribing' ? { subscribing: true } :
-			state.value === 'publishing' ? { publishing: true } :
+			state.value === 'federating' ? { federating: true, suspended: false, blocked: false } :
+			state.value === 'subscribing' ? { subscribing: true, suspended: false, blocked: false } :
+			state.value === 'publishing' ? { publishing: true, suspended: false, blocked: false } :
 			state.value === 'suspended' ? { suspended: true } :
 			state.value === 'blocked' ? { blocked: true } :
 			state.value === 'silenced' ? { silenced: true } :
diff --git a/packages/frontend/src/pages/admin/files.vue b/packages/frontend/src/pages/admin/files.vue
index 5132b85c64..4cc859227f 100644
--- a/packages/frontend/src/pages/admin/files.vue
+++ b/packages/frontend/src/pages/admin/files.vue
@@ -9,7 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 		<template #header><XHeader :actions="headerActions"/></template>
 		<MkSpacer :contentMax="900">
 			<div class="_gaps">
-				<div class="inputs" style="display: flex; gap: var(--margin); flex-wrap: wrap;">
+				<div class="inputs" style="display: flex; gap: var(--MI-margin); flex-wrap: wrap;">
 					<MkSelect v-model="origin" style="margin: 0; flex: 1;">
 						<template #label>{{ i18n.ts.instance }}</template>
 						<option value="combined">{{ i18n.ts.all }}</option>
@@ -20,7 +20,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 						<template #label>{{ i18n.ts.host }}</template>
 					</MkInput>
 				</div>
-				<div class="inputs" style="display: flex; gap: var(--margin); flex-wrap: wrap;">
+				<div class="inputs" style="display: flex; gap: var(--MI-margin); flex-wrap: wrap;">
 					<MkInput v-model="userId" :debounce="true" type="search" style="margin: 0; flex: 1;">
 						<template #label>User ID</template>
 					</MkInput>
diff --git a/packages/frontend/src/pages/admin/index.vue b/packages/frontend/src/pages/admin/index.vue
index 794feae202..fd15ae1d66 100644
--- a/packages/frontend/src/pages/admin/index.vue
+++ b/packages/frontend/src/pages/admin/index.vue
@@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 <div ref="el" class="hiyeyicy" :class="{ wide: !narrow }">
 	<div v-if="!narrow || currentPage?.route.name == null" class="nav">
 		<MkSpacer :contentMax="700" :marginMin="16">
-			<div class="lxpfedzu">
+			<div class="lxpfedzu _gaps">
 				<div class="banner">
 					<img :src="instance.iconUrl || '/favicon.ico'" alt="" class="icon"/>
 				</div>
@@ -15,7 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 				<div class="_gaps_s">
 					<MkInfo v-if="thereIsUnresolvedAbuseReport" warn>{{ i18n.ts.thereIsUnresolvedAbuseReportWarning }} <MkA to="/admin/abuses" class="_link">{{ i18n.ts.check }}</MkA></MkInfo>
 					<MkInfo v-if="noMaintainerInformation" warn>{{ i18n.ts.noMaintainerInformationWarning }} <MkA to="/admin/settings" class="_link">{{ i18n.ts.configure }}</MkA></MkInfo>
-					<MkInfo v-if="noInquiryUrl" warn>{{ i18n.ts.noInquiryUrlWarning }} <MkA to="/admin/moderation" class="_link">{{ i18n.ts.configure }}</MkA></MkInfo>
+					<MkInfo v-if="noInquiryUrl" warn>{{ i18n.ts.noInquiryUrlWarning }} <MkA to="/admin/settings" class="_link">{{ i18n.ts.configure }}</MkA></MkInfo>
 					<MkInfo v-if="noBotProtection" warn>{{ i18n.ts.noBotProtectionWarning }} <MkA to="/admin/security" class="_link">{{ i18n.ts.configure }}</MkA></MkInfo>
 					<MkInfo v-if="noEmailServer" warn>{{ i18n.ts.noEmailServerWarning }} <MkA to="/admin/email-settings" class="_link">{{ i18n.ts.configure }}</MkA></MkInfo>
 				</div>
@@ -25,7 +25,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 		</MkSpacer>
 	</div>
 	<div v-if="!(narrow && currentPage?.route.name == null)" class="main">
-		<RouterView/>
+		<RouterView nested/>
 	</div>
 </div>
 </template>
@@ -61,10 +61,10 @@ const narrow = ref(false);
 const view = ref(null);
 const el = ref<HTMLDivElement | null>(null);
 const pageProps = ref({});
-let noMaintainerInformation = isEmpty(instance.maintainerName) || isEmpty(instance.maintainerEmail);
-let noBotProtection = !instance.disableRegistration && !instance.enableHcaptcha && !instance.enableRecaptcha && !instance.enableTurnstile;
-let noEmailServer = !instance.enableEmail;
-let noInquiryUrl = isEmpty(instance.inquiryUrl);
+const noMaintainerInformation = computed(() => isEmpty(instance.maintainerName) || isEmpty(instance.maintainerEmail));
+const noBotProtection = computed(() => !instance.disableRegistration && !instance.enableHcaptcha && !instance.enableRecaptcha && !instance.enableTurnstile && !instance.enableMcaptcha);
+const noEmailServer = computed(() => !instance.enableEmail);
+const noInquiryUrl = computed(() => isEmpty(instance.inquiryUrl));
 const thereIsUnresolvedAbuseReport = ref(false);
 const currentPage = computed(() => router.currentRef.value.child);
 
@@ -199,26 +199,21 @@ const menuDef = computed(() => [{
 		text: i18n.ts.relays,
 		to: '/admin/relays',
 		active: currentPage.value?.route.name === 'relays',
-	}, {
-		icon: 'ti ti-ban',
-		text: i18n.ts.instanceBlocking,
-		to: '/admin/instance-block',
-		active: currentPage.value?.route.name === 'instance-block',
-	}, {
-		icon: 'ti ti-ghost',
-		text: i18n.ts.proxyAccount,
-		to: '/admin/proxy-account',
-		active: currentPage.value?.route.name === 'proxy-account',
 	}, {
 		icon: 'ti ti-link',
 		text: i18n.ts.externalServices,
 		to: '/admin/external-services',
 		active: currentPage.value?.route.name === 'external-services',
 	}, {
-		icon: 'ti ti-adjustments',
-		text: i18n.ts.other,
-		to: '/admin/other-settings',
-		active: currentPage.value?.route.name === 'other-settings',
+		icon: 'ti ti-webhook',
+		text: 'Webhook',
+		to: '/admin/system-webhook',
+		active: currentPage.value?.route.name === 'system-webhook',
+	}, {
+		icon: 'ti ti-bolt',
+		text: i18n.ts.performance,
+		to: '/admin/performance',
+		active: currentPage.value?.route.name === 'performance',
 	}],
 }, {
 	title: i18n.ts.info,
@@ -230,25 +225,22 @@ const menuDef = computed(() => [{
 	}],
 }]);
 
-watch(narrow.value, () => {
-	if (currentPage.value?.route.name == null && !narrow.value) {
-		router.push('/admin/overview');
-	}
-});
-
 onMounted(() => {
-	ro.observe(el.value);
-
-	narrow.value = el.value.offsetWidth < NARROW_THRESHOLD;
+	if (el.value != null) {
+		ro.observe(el.value);
+		narrow.value = el.value.offsetWidth < NARROW_THRESHOLD;
+	}
 	if (currentPage.value?.route.name == null && !narrow.value) {
-		router.push('/admin/overview');
+		router.replace('/admin/overview');
 	}
 });
 
 onActivated(() => {
-	narrow.value = el.value.offsetWidth < NARROW_THRESHOLD;
+	if (el.value != null) {
+		narrow.value = el.value.offsetWidth < NARROW_THRESHOLD;
+	}
 	if (currentPage.value?.route.name == null && !narrow.value) {
-		router.push('/admin/overview');
+		router.replace('/admin/overview');
 	}
 });
 
@@ -339,7 +331,7 @@ defineExpose({
 			width: 32%;
 			max-width: 280px;
 			box-sizing: border-box;
-			border-right: solid 0.5px var(--divider);
+			border-right: solid 0.5px var(--MI_THEME-divider);
 			overflow: auto;
 			height: 100%;
 		}
diff --git a/packages/frontend/src/pages/admin/instance-block.vue b/packages/frontend/src/pages/admin/instance-block.vue
deleted file mode 100644
index 6b14bd42c2..0000000000
--- a/packages/frontend/src/pages/admin/instance-block.vue
+++ /dev/null
@@ -1,73 +0,0 @@
-<!--
-SPDX-FileCopyrightText: syuilo and misskey-project
-SPDX-License-Identifier: AGPL-3.0-only
--->
-
-<template>
-<MkStickyContainer>
-	<template #header><XHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template>
-	<MkSpacer :contentMax="700" :marginMin="16" :marginMax="32">
-		<FormSuspense :p="init">
-			<MkTextarea v-if="tab === 'block'" v-model="blockedHosts">
-				<span>{{ i18n.ts.blockedInstances }}</span>
-				<template #caption>{{ i18n.ts.blockedInstancesDescription }}</template>
-			</MkTextarea>
-			<MkTextarea v-else-if="tab === 'silence'" v-model="silencedHosts" class="_formBlock">
-				<span>{{ i18n.ts.silencedInstances }}</span>
-				<template #caption>{{ i18n.ts.silencedInstancesDescription }}</template>
-			</MkTextarea>
-			<MkButton primary @click="save"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton>
-		</FormSuspense>
-	</MkSpacer>
-</MkStickyContainer>
-</template>
-
-<script lang="ts" setup>
-import { ref, computed } from 'vue';
-import XHeader from './_header_.vue';
-import MkButton from '@/components/MkButton.vue';
-import MkTextarea from '@/components/MkTextarea.vue';
-import FormSuspense from '@/components/form/suspense.vue';
-import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
-import { fetchInstance } from '@/instance.js';
-import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
-
-const blockedHosts = ref<string>('');
-const silencedHosts = ref<string>('');
-const tab = ref('block');
-
-async function init() {
-	const meta = await misskeyApi('admin/meta');
-	blockedHosts.value = meta.blockedHosts.join('\n');
-	silencedHosts.value = meta.silencedHosts.join('\n');
-}
-
-function save() {
-	os.apiWithDialog('admin/update-meta', {
-		blockedHosts: blockedHosts.value.split('\n') || [],
-		silencedHosts: silencedHosts.value.split('\n') || [],
-
-	}).then(() => {
-		fetchInstance(true);
-	});
-}
-
-const headerActions = computed(() => []);
-
-const headerTabs = computed(() => [{
-	key: 'block',
-	title: i18n.ts.block,
-	icon: 'ti ti-ban',
-}, {
-	key: 'silence',
-	title: i18n.ts.silence,
-	icon: 'ti ti-eye-off',
-}]);
-
-definePageMetadata(() => ({
-	title: i18n.ts.instanceBlocking,
-	icon: 'ti ti-ban',
-}));
-</script>
diff --git a/packages/frontend/src/pages/admin/invites.vue b/packages/frontend/src/pages/admin/invites.vue
index 95727fb14c..9cb430b0fe 100644
--- a/packages/frontend/src/pages/admin/invites.vue
+++ b/packages/frontend/src/pages/admin/invites.vue
@@ -19,7 +19,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 					<MkInput v-if="!noExpirationDate" v-model="expiresAt" type="datetime-local">
 						<template #label>{{ i18n.ts.expirationDate }}</template>
 					</MkInput>
-					<MkInput v-model="createCount" type="number">
+					<MkInput v-model="createCount" type="number" min="1">
 						<template #label>{{ i18n.ts.createCount }}</template>
 					</MkInput>
 					<MkButton primary rounded @click="createWithOptions">{{ i18n.ts.create }}</MkButton>
diff --git a/packages/frontend/src/pages/admin/moderation.vue b/packages/frontend/src/pages/admin/moderation.vue
index a75799696d..5d8a581b2e 100644
--- a/packages/frontend/src/pages/admin/moderation.vue
+++ b/packages/frontend/src/pages/admin/moderation.vue
@@ -10,61 +10,115 @@ SPDX-License-Identifier: AGPL-3.0-only
 		<MkSpacer :contentMax="700" :marginMin="16" :marginMax="32">
 			<FormSuspense :p="init">
 				<div class="_gaps_m">
-					<MkSwitch v-model="enableRegistration">
+					<MkSwitch v-model="enableRegistration" @change="onChange_enableRegistration">
 						<template #label>{{ i18n.ts.enableRegistration }}</template>
+						<template #caption>{{ i18n.ts._serverSettings.thisSettingWillAutomaticallyOffWhenModeratorsInactive }}</template>
 					</MkSwitch>
 
-					<MkSwitch v-model="emailRequiredForSignup">
+					<MkSwitch v-model="emailRequiredForSignup" @change="onChange_emailRequiredForSignup">
 						<template #label>{{ i18n.ts.emailRequiredForSignup }}</template>
 					</MkSwitch>
 
 					<FormLink to="/admin/server-rules">{{ i18n.ts.serverRules }}</FormLink>
 
-					<MkInput v-model="tosUrl" type="url">
-						<template #prefix><i class="ti ti-link"></i></template>
-						<template #label>{{ i18n.ts.tosUrl }}</template>
-					</MkInput>
-
-					<MkInput v-model="privacyPolicyUrl" type="url">
-						<template #prefix><i class="ti ti-link"></i></template>
-						<template #label>{{ i18n.ts.privacyPolicyUrl }}</template>
-					</MkInput>
-
-					<MkInput v-model="inquiryUrl" type="url">
-						<template #prefix><i class="ti ti-link"></i></template>
-						<template #label>{{ i18n.ts._serverSettings.inquiryUrl }}</template>
-						<template #caption>{{ i18n.ts._serverSettings.inquiryUrlDescription }}</template>
-					</MkInput>
-
-					<MkTextarea v-model="preservedUsernames">
+					<MkFolder>
+						<template #icon><i class="ti ti-lock-star"></i></template>
 						<template #label>{{ i18n.ts.preservedUsernames }}</template>
-						<template #caption>{{ i18n.ts.preservedUsernamesDescription }}</template>
-					</MkTextarea>
 
-					<MkTextarea v-model="sensitiveWords">
+						<div class="_gaps">
+							<MkTextarea v-model="preservedUsernames">
+								<template #caption>{{ i18n.ts.preservedUsernamesDescription }}</template>
+							</MkTextarea>
+							<MkButton primary @click="save_preservedUsernames">{{ i18n.ts.save }}</MkButton>
+						</div>
+					</MkFolder>
+
+					<MkFolder>
+						<template #icon><i class="ti ti-message-exclamation"></i></template>
 						<template #label>{{ i18n.ts.sensitiveWords }}</template>
-						<template #caption>{{ i18n.ts.sensitiveWordsDescription }}<br>{{ i18n.ts.sensitiveWordsDescription2 }}</template>
-					</MkTextarea>
 
-					<MkTextarea v-model="prohibitedWords">
+						<div class="_gaps">
+							<MkTextarea v-model="sensitiveWords">
+								<template #caption>{{ i18n.ts.sensitiveWordsDescription }}<br>{{ i18n.ts.sensitiveWordsDescription2 }}</template>
+							</MkTextarea>
+							<MkButton primary @click="save_sensitiveWords">{{ i18n.ts.save }}</MkButton>
+						</div>
+					</MkFolder>
+
+					<MkFolder>
+						<template #icon><i class="ti ti-message-x"></i></template>
 						<template #label>{{ i18n.ts.prohibitedWords }}</template>
-						<template #caption>{{ i18n.ts.prohibitedWordsDescription }}<br>{{ i18n.ts.prohibitedWordsDescription2 }}</template>
-					</MkTextarea>
 
-					<MkTextarea v-model="hiddenTags">
+						<div class="_gaps">
+							<MkTextarea v-model="prohibitedWords">
+								<template #caption>{{ i18n.ts.prohibitedWordsDescription }}<br>{{ i18n.ts.prohibitedWordsDescription2 }}</template>
+							</MkTextarea>
+							<MkButton primary @click="save_prohibitedWords">{{ i18n.ts.save }}</MkButton>
+						</div>
+					</MkFolder>
+
+					<MkFolder>
+						<template #icon><i class="ti ti-user-x"></i></template>
+						<template #label>{{ i18n.ts.prohibitedWordsForNameOfUser }}</template>
+
+						<div class="_gaps">
+							<MkTextarea v-model="prohibitedWordsForNameOfUser">
+								<template #caption>{{ i18n.ts.prohibitedWordsForNameOfUserDescription }}<br>{{ i18n.ts.prohibitedWordsDescription2 }}</template>
+							</MkTextarea>
+							<MkButton primary @click="save_prohibitedWordsForNameOfUser">{{ i18n.ts.save }}</MkButton>
+						</div>
+					</MkFolder>
+
+					<MkFolder>
+						<template #icon><i class="ti ti-eye-off"></i></template>
 						<template #label>{{ i18n.ts.hiddenTags }}</template>
-						<template #caption>{{ i18n.ts.hiddenTagsDescription }}</template>
-					</MkTextarea>
+
+						<div class="_gaps">
+							<MkTextarea v-model="hiddenTags">
+								<template #caption>{{ i18n.ts.hiddenTagsDescription }}</template>
+							</MkTextarea>
+							<MkButton primary @click="save_hiddenTags">{{ i18n.ts.save }}</MkButton>
+						</div>
+					</MkFolder>
+
+					<MkFolder>
+						<template #icon><i class="ti ti-eye-off"></i></template>
+						<template #label>{{ i18n.ts.silencedInstances }}</template>
+
+						<div class="_gaps">
+							<MkTextarea v-model="silencedHosts">
+								<template #caption>{{ i18n.ts.silencedInstancesDescription }}</template>
+							</MkTextarea>
+							<MkButton primary @click="save_silencedHosts">{{ i18n.ts.save }}</MkButton>
+						</div>
+					</MkFolder>
+
+					<MkFolder>
+						<template #icon><i class="ti ti-eye-off"></i></template>
+						<template #label>{{ i18n.ts.mediaSilencedInstances }}</template>
+
+						<div class="_gaps">
+							<MkTextarea v-model="mediaSilencedHosts">
+								<template #caption>{{ i18n.ts.mediaSilencedInstancesDescription }}</template>
+							</MkTextarea>
+							<MkButton primary @click="save_mediaSilencedHosts">{{ i18n.ts.save }}</MkButton>
+						</div>
+					</MkFolder>
+
+					<MkFolder>
+						<template #icon><i class="ti ti-ban"></i></template>
+						<template #label>{{ i18n.ts.blockedInstances }}</template>
+
+						<div class="_gaps">
+							<MkTextarea v-model="blockedHosts">
+								<template #caption>{{ i18n.ts.blockedInstancesDescription }}</template>
+							</MkTextarea>
+							<MkButton primary @click="save_blockedHosts">{{ i18n.ts.save }}</MkButton>
+						</div>
+					</MkFolder>
 				</div>
 			</FormSuspense>
 		</MkSpacer>
-		<template #footer>
-			<div :class="$style.footer">
-				<MkSpacer :contentMax="700" :marginMin="16" :marginMax="16">
-					<MkButton primary rounded @click="save"><i class="ti ti-check"></i> {{ i18n.ts.save }}</MkButton>
-				</MkSpacer>
-			</div>
-		</template>
 	</MkStickyContainer>
 </div>
 </template>
@@ -83,16 +137,18 @@ import { i18n } from '@/i18n.js';
 import { definePageMetadata } from '@/scripts/page-metadata.js';
 import MkButton from '@/components/MkButton.vue';
 import FormLink from '@/components/form/link.vue';
+import MkFolder from '@/components/MkFolder.vue';
 
 const enableRegistration = ref<boolean>(false);
 const emailRequiredForSignup = ref<boolean>(false);
 const sensitiveWords = ref<string>('');
 const prohibitedWords = ref<string>('');
+const prohibitedWordsForNameOfUser = ref<string>('');
 const hiddenTags = ref<string>('');
 const preservedUsernames = ref<string>('');
-const tosUrl = ref<string | null>(null);
-const privacyPolicyUrl = ref<string | null>(null);
-const inquiryUrl = ref<string | null>(null);
+const blockedHosts = ref<string>('');
+const silencedHosts = ref<string>('');
+const mediaSilencedHosts = ref<string>('');
 
 async function init() {
 	const meta = await misskeyApi('admin/meta');
@@ -100,29 +156,94 @@ async function init() {
 	emailRequiredForSignup.value = meta.emailRequiredForSignup;
 	sensitiveWords.value = meta.sensitiveWords.join('\n');
 	prohibitedWords.value = meta.prohibitedWords.join('\n');
+	prohibitedWordsForNameOfUser.value = meta.prohibitedWordsForNameOfUser.join('\n');
 	hiddenTags.value = meta.hiddenTags.join('\n');
 	preservedUsernames.value = meta.preservedUsernames.join('\n');
-	tosUrl.value = meta.tosUrl;
-	privacyPolicyUrl.value = meta.privacyPolicyUrl;
-	inquiryUrl.value = meta.inquiryUrl;
+	blockedHosts.value = meta.blockedHosts.join('\n');
+	silencedHosts.value = meta.silencedHosts?.join('\n') ?? '';
+	mediaSilencedHosts.value = meta.mediaSilencedHosts.join('\n');
 }
 
-function save() {
+function onChange_enableRegistration(value: boolean) {
+	os.apiWithDialog('admin/update-meta', {
+		disableRegistration: !value,
+	}).then(() => {
+		fetchInstance(true);
+	});
+}
+
+function onChange_emailRequiredForSignup(value: boolean) {
+	os.apiWithDialog('admin/update-meta', {
+		emailRequiredForSignup: value,
+	}).then(() => {
+		fetchInstance(true);
+	});
+}
+
+function save_preservedUsernames() {
 	os.apiWithDialog('admin/update-meta', {
-		disableRegistration: !enableRegistration.value,
-		emailRequiredForSignup: emailRequiredForSignup.value,
-		tosUrl: tosUrl.value,
-		privacyPolicyUrl: privacyPolicyUrl.value,
-		inquiryUrl: inquiryUrl.value,
-		sensitiveWords: sensitiveWords.value.split('\n'),
-		prohibitedWords: prohibitedWords.value.split('\n'),
-		hiddenTags: hiddenTags.value.split('\n'),
 		preservedUsernames: preservedUsernames.value.split('\n'),
 	}).then(() => {
 		fetchInstance(true);
 	});
 }
 
+function save_sensitiveWords() {
+	os.apiWithDialog('admin/update-meta', {
+		sensitiveWords: sensitiveWords.value.split('\n'),
+	}).then(() => {
+		fetchInstance(true);
+	});
+}
+
+function save_prohibitedWords() {
+	os.apiWithDialog('admin/update-meta', {
+		prohibitedWords: prohibitedWords.value.split('\n'),
+	}).then(() => {
+		fetchInstance(true);
+	});
+}
+
+function save_prohibitedWordsForNameOfUser() {
+	os.apiWithDialog('admin/update-meta', {
+		prohibitedWordsForNameOfUser: prohibitedWordsForNameOfUser.value.split('\n'),
+	}).then(() => {
+		fetchInstance(true);
+	});
+}
+
+function save_hiddenTags() {
+	os.apiWithDialog('admin/update-meta', {
+		hiddenTags: hiddenTags.value.split('\n'),
+	}).then(() => {
+		fetchInstance(true);
+	});
+}
+
+function save_blockedHosts() {
+	os.apiWithDialog('admin/update-meta', {
+		blockedHosts: blockedHosts.value.split('\n') || [],
+	}).then(() => {
+		fetchInstance(true);
+	});
+}
+
+function save_silencedHosts() {
+	os.apiWithDialog('admin/update-meta', {
+		silencedHosts: silencedHosts.value.split('\n') || [],
+	}).then(() => {
+		fetchInstance(true);
+	});
+}
+
+function save_mediaSilencedHosts() {
+	os.apiWithDialog('admin/update-meta', {
+		mediaSilencedHosts: mediaSilencedHosts.value.split('\n') || [],
+	}).then(() => {
+		fetchInstance(true);
+	});
+}
+
 const headerTabs = computed(() => []);
 
 definePageMetadata(() => ({
@@ -130,10 +251,3 @@ definePageMetadata(() => ({
 	icon: 'ti ti-shield',
 }));
 </script>
-
-<style lang="scss" module>
-.footer {
-	-webkit-backdrop-filter: var(--blur, blur(15px));
-	backdrop-filter: var(--blur, blur(15px));
-}
-</style>
diff --git a/packages/frontend/src/pages/admin/modlog.ModLog.vue b/packages/frontend/src/pages/admin/modlog.ModLog.vue
index e33c882721..1e144394fb 100644
--- a/packages/frontend/src/pages/admin/modlog.ModLog.vue
+++ b/packages/frontend/src/pages/admin/modlog.ModLog.vue
@@ -8,9 +8,39 @@ SPDX-License-Identifier: AGPL-3.0-only
 	<template #label>
 		<b
 			:class="{
-				[$style.logGreen]: ['createRole', 'addCustomEmoji', 'createGlobalAnnouncement', 'createUserAnnouncement', 'createAd', 'createInvitation', 'createAvatarDecoration'].includes(log.type),
-				[$style.logYellow]: ['markSensitiveDriveFile', 'resetPassword'].includes(log.type),
-				[$style.logRed]: ['suspend', 'deleteRole', 'suspendRemoteInstance', 'deleteGlobalAnnouncement', 'deleteUserAnnouncement', 'deleteCustomEmoji', 'deleteNote', 'deleteDriveFile', 'deleteAd', 'deleteAvatarDecoration'].includes(log.type)
+				[$style.logGreen]: [
+					'createRole',
+					'addCustomEmoji',
+					'createGlobalAnnouncement',
+					'createUserAnnouncement',
+					'createAd',
+					'createInvitation',
+					'createAvatarDecoration',
+					'createSystemWebhook',
+					'createAbuseReportNotificationRecipient',
+				].includes(log.type),
+				[$style.logYellow]: [
+					'markSensitiveDriveFile',
+					'resetPassword',
+					'suspendRemoteInstance',
+				].includes(log.type),
+				[$style.logRed]: [
+					'suspend',
+					'deleteRole',
+					'deleteGlobalAnnouncement',
+					'deleteUserAnnouncement',
+					'deleteCustomEmoji',
+					'deleteNote',
+					'deleteDriveFile',
+					'deleteAd',
+					'deleteAvatarDecoration',
+					'deleteSystemWebhook',
+					'deleteAbuseReportNotificationRecipient',
+					'deleteAccount',
+					'deletePage',
+					'deleteFlash',
+					'deleteGalleryPost',
+				].includes(log.type)
 			}"
 		>{{ i18n.ts._moderationLogTypes[log.type] }}</b>
 		<span v-if="log.type === 'updateUserNote'">: @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }}</span>
@@ -40,6 +70,16 @@ SPDX-License-Identifier: AGPL-3.0-only
 		<span v-else-if="log.type === 'createAvatarDecoration'">: {{ log.info.avatarDecoration.name }}</span>
 		<span v-else-if="log.type === 'updateAvatarDecoration'">: {{ log.info.before.name }}</span>
 		<span v-else-if="log.type === 'deleteAvatarDecoration'">: {{ log.info.avatarDecoration.name }}</span>
+		<span v-else-if="log.type === 'createSystemWebhook'">: {{ log.info.webhook.name }}</span>
+		<span v-else-if="log.type === 'updateSystemWebhook'">: {{ log.info.before.name }}</span>
+		<span v-else-if="log.type === 'deleteSystemWebhook'">: {{ log.info.webhook.name }}</span>
+		<span v-else-if="log.type === 'createAbuseReportNotificationRecipient'">: {{ log.info.recipient.name }}</span>
+		<span v-else-if="log.type === 'updateAbuseReportNotificationRecipient'">: {{ log.info.before.name }}</span>
+		<span v-else-if="log.type === 'deleteAbuseReportNotificationRecipient'">: {{ log.info.recipient.name }}</span>
+		<span v-else-if="log.type === 'deleteAccount'">: @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }}</span>
+		<span v-else-if="log.type === 'deletePage'">: @{{ log.info.pageUserUsername }}</span>
+		<span v-else-if="log.type === 'deleteFlash'">: @{{ log.info.flashUserUsername }}</span>
+		<span v-else-if="log.type === 'deleteGalleryPost'">: @{{ log.info.postUserUsername }}</span>
 	</template>
 	<template #icon>
 		<MkAvatar :user="log.user" :class="$style.avatar"/>
@@ -49,7 +89,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 	</template>
 
 	<div>
-		<div style="display: flex; gap: var(--margin); flex-wrap: wrap;">
+		<div style="display: flex; gap: var(--MI-margin); flex-wrap: wrap;">
 			<div style="flex: 1;">{{ i18n.ts.moderator }}: <MkA :to="`/admin/user/${log.userId}`" class="_link">@{{ log.user?.username }}</MkA></div>
 			<div style="flex: 1;">{{ i18n.ts.dateAndTime }}: <MkTime :time="log.createdAt" mode="detail"/></div>
 		</div>
@@ -111,7 +151,21 @@ SPDX-License-Identifier: AGPL-3.0-only
 			</div>
 		</template>
 		<template v-else-if="log.type === 'updateRemoteInstanceNote'">
-			<div>{{ i18n.ts.user }}: {{ log.info.userId }}</div>
+			<div :class="$style.diff">
+				<CodeDiff :context="5" :hideHeader="true" :oldString="log.info.before ?? ''" :newString="log.info.after ?? ''" maxHeight="300px"/>
+			</div>
+		</template>
+		<template v-else-if="log.type === 'updateSystemWebhook'">
+			<div :class="$style.diff">
+				<CodeDiff :context="5" :hideHeader="true" :oldString="JSON5.stringify(log.info.before, null, '\t')" :newString="JSON5.stringify(log.info.after, null, '\t')" language="javascript" maxHeight="300px"/>
+			</div>
+		</template>
+		<template v-else-if="log.type === 'updateAbuseReportNotificationRecipient'">
+			<div :class="$style.diff">
+				<CodeDiff :context="5" :hideHeader="true" :oldString="JSON5.stringify(log.info.before, null, '\t')" :newString="JSON5.stringify(log.info.after, null, '\t')" language="javascript" maxHeight="300px"/>
+			</div>
+		</template>
+		<template v-else-if="log.type === 'updateAbuseReportNote'">
 			<div :class="$style.diff">
 				<CodeDiff :context="5" :hideHeader="true" :oldString="log.info.before ?? ''" :newString="log.info.after ?? ''" maxHeight="300px"/>
 			</div>
@@ -151,14 +205,14 @@ const props = defineProps<{
 }
 
 .logYellow {
-	color: var(--warn);
+	color: var(--MI_THEME-warn);
 }
 
 .logRed {
-	color: var(--error);
+	color: var(--MI_THEME-error);
 }
 
 .logGreen {
-	color: var(--success);
+	color: var(--MI_THEME-success);
 }
 </style>
diff --git a/packages/frontend/src/pages/admin/modlog.vue b/packages/frontend/src/pages/admin/modlog.vue
index 8590ee1651..c9eaf07531 100644
--- a/packages/frontend/src/pages/admin/modlog.vue
+++ b/packages/frontend/src/pages/admin/modlog.vue
@@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 	<template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template>
 	<MkSpacer :contentMax="900">
 		<div>
-			<div style="display: flex; gap: var(--margin); flex-wrap: wrap;">
+			<div style="display: flex; gap: var(--MI-margin); flex-wrap: wrap;">
 				<MkSelect v-model="type" style="margin: 0; flex: 1;">
 					<template #label>{{ i18n.ts.type }}</template>
 					<option :value="null">{{ i18n.ts.all }}</option>
@@ -19,10 +19,10 @@ SPDX-License-Identifier: AGPL-3.0-only
 				</MkInput>
 			</div>
 
-			<MkPagination v-slot="{items}" ref="logs" :pagination="pagination" style="margin-top: var(--margin);">
-				<div class="_gaps_s">
-					<XModLog v-for="item in items" :key="item.id" :log="item"/>
-				</div>
+			<MkPagination v-slot="{items}" ref="logs" :pagination="pagination" style="margin-top: var(--MI-margin);">
+				<MkDateSeparatedList v-slot="{ item }" :items="items" :noGap="false" style="--MI-margin: 8px;">
+					<XModLog :key="item.id" :log="item"/>
+				</MkDateSeparatedList>
 			</MkPagination>
 		</div>
 	</MkSpacer>
@@ -39,6 +39,7 @@ import MkInput from '@/components/MkInput.vue';
 import MkPagination from '@/components/MkPagination.vue';
 import { i18n } from '@/i18n.js';
 import { definePageMetadata } from '@/scripts/page-metadata.js';
+import MkDateSeparatedList from '@/components/MkDateSeparatedList.vue';
 
 const logs = shallowRef<InstanceType<typeof MkPagination>>();
 
diff --git a/packages/frontend/src/pages/admin/object-storage.vue b/packages/frontend/src/pages/admin/object-storage.vue
index 5fddb715cd..d5a664934c 100644
--- a/packages/frontend/src/pages/admin/object-storage.vue
+++ b/packages/frontend/src/pages/admin/object-storage.vue
@@ -157,7 +157,7 @@ definePageMetadata(() => ({
 
 <style lang="scss" module>
 .footer {
-	-webkit-backdrop-filter: var(--blur, blur(15px));
-	backdrop-filter: var(--blur, blur(15px));
+	-webkit-backdrop-filter: var(--MI-blur, blur(15px));
+	backdrop-filter: var(--MI-blur, blur(15px));
 }
 </style>
diff --git a/packages/frontend/src/pages/admin/other-settings.vue b/packages/frontend/src/pages/admin/other-settings.vue
deleted file mode 100644
index 345cf333b5..0000000000
--- a/packages/frontend/src/pages/admin/other-settings.vue
+++ /dev/null
@@ -1,93 +0,0 @@
-<!--
-SPDX-FileCopyrightText: syuilo and misskey-project
-SPDX-License-Identifier: AGPL-3.0-only
--->
-
-<template>
-<MkStickyContainer>
-	<template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template>
-	<MkSpacer :contentMax="700" :marginMin="16" :marginMax="32">
-		<FormSuspense :p="init">
-			<div class="_gaps">
-				<div class="_panel" style="padding: 16px;">
-					<MkSwitch v-model="enableServerMachineStats">
-						<template #label>{{ i18n.ts.enableServerMachineStats }}</template>
-						<template #caption>{{ i18n.ts.turnOffToImprovePerformance }}</template>
-					</MkSwitch>
-				</div>
-
-				<div class="_panel" style="padding: 16px;">
-					<MkSwitch v-model="enableIdenticonGeneration">
-						<template #label>{{ i18n.ts.enableIdenticonGeneration }}</template>
-						<template #caption>{{ i18n.ts.turnOffToImprovePerformance }}</template>
-					</MkSwitch>
-				</div>
-
-				<div class="_panel" style="padding: 16px;">
-					<MkSwitch v-model="enableChartsForRemoteUser">
-						<template #label>{{ i18n.ts.enableChartsForRemoteUser }}</template>
-						<template #caption>{{ i18n.ts.turnOffToImprovePerformance }}</template>
-					</MkSwitch>
-				</div>
-
-				<div class="_panel" style="padding: 16px;">
-					<MkSwitch v-model="enableChartsForFederatedInstances">
-						<template #label>{{ i18n.ts.enableChartsForFederatedInstances }}</template>
-						<template #caption>{{ i18n.ts.turnOffToImprovePerformance }}</template>
-					</MkSwitch>
-				</div>
-			</div>
-		</FormSuspense>
-	</MkSpacer>
-</MkStickyContainer>
-</template>
-
-<script lang="ts" setup>
-import { ref, computed } from 'vue';
-import XHeader from './_header_.vue';
-import FormSuspense from '@/components/form/suspense.vue';
-import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
-import { fetchInstance } from '@/instance.js';
-import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
-import MkSwitch from '@/components/MkSwitch.vue';
-
-const enableServerMachineStats = ref<boolean>(false);
-const enableIdenticonGeneration = ref<boolean>(false);
-const enableChartsForRemoteUser = ref<boolean>(false);
-const enableChartsForFederatedInstances = ref<boolean>(false);
-
-async function init() {
-	const meta = await misskeyApi('admin/meta');
-	enableServerMachineStats.value = meta.enableServerMachineStats;
-	enableIdenticonGeneration.value = meta.enableIdenticonGeneration;
-	enableChartsForRemoteUser.value = meta.enableChartsForRemoteUser;
-	enableChartsForFederatedInstances.value = meta.enableChartsForFederatedInstances;
-}
-
-function save() {
-	os.apiWithDialog('admin/update-meta', {
-		enableServerMachineStats: enableServerMachineStats.value,
-		enableIdenticonGeneration: enableIdenticonGeneration.value,
-		enableChartsForRemoteUser: enableChartsForRemoteUser.value,
-		enableChartsForFederatedInstances: enableChartsForFederatedInstances.value,
-	}).then(() => {
-		fetchInstance(true);
-	});
-}
-
-const headerActions = computed(() => [{
-	asFullButton: true,
-	icon: 'ti ti-check',
-	text: i18n.ts.save,
-	handler: save,
-}]);
-
-const headerTabs = computed(() => []);
-
-definePageMetadata(() => ({
-	title: i18n.ts.other,
-	icon: 'ti ti-adjustments',
-}));
-</script>
diff --git a/packages/frontend/src/pages/admin/overview.ap-requests.stories.impl.ts b/packages/frontend/src/pages/admin/overview.ap-requests.stories.impl.ts
new file mode 100644
index 0000000000..584cd3e4d9
--- /dev/null
+++ b/packages/frontend/src/pages/admin/overview.ap-requests.stories.impl.ts
@@ -0,0 +1,41 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { StoryObj } from '@storybook/vue3';
+import { http, HttpResponse } from 'msw';
+import { action } from '@storybook/addon-actions';
+import { commonHandlers } from '../../../.storybook/mocks.js';
+import overview_ap_requests from './overview.ap-requests.vue';
+export const Default = {
+	render(args) {
+		return {
+			components: {
+				overview_ap_requests,
+			},
+			setup() {
+				return {
+					args,
+				};
+			},
+			template: '<overview_ap_requests />',
+		};
+	},
+	parameters: {
+		layout: 'fullscreen',
+		msw: {
+			handlers: [
+				...commonHandlers,
+				http.post('/api/charts/ap-request', async ({ request }) => {
+					action('POST /api/charts/ap-request')(await request.json());
+					return HttpResponse.json({
+						deliverFailed: [0, 0, 0, 2, 0, 0, 1, 0, 0, 2, 0, 0, 0, 0, 0, 0, 2, 0, 1, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 0, 1, 0, 0, 0, 3, 1, 1, 2, 0, 0],
+						deliverSucceeded: [0, 1, 51, 34, 136, 189, 51, 17, 17, 34, 1, 17, 18, 51, 34, 68, 287, 0, 17, 33, 32, 96, 96, 0, 49, 64, 0, 32, 0, 32, 81, 48, 65, 1, 16, 50, 90, 148, 33, 43, 72, 127, 17, 138, 78, 91, 78, 91, 13, 52],
+						inboxReceived: [507, 1173, 1096, 871, 958, 937, 908, 1026, 956, 909, 807, 1002, 832, 995, 1039, 1047, 1109, 930, 711, 835, 764, 679, 835, 958, 634, 654, 691, 895, 811, 676, 1044, 1389, 1318, 863, 887, 952, 1011, 1061, 592, 900, 611, 595, 604, 562, 607, 621, 854, 666, 1197, 644],
+					});
+				}),
+			],
+		},
+	},
+} satisfies StoryObj<typeof overview_ap_requests>;
diff --git a/packages/frontend/src/pages/admin/overview.ap-requests.vue b/packages/frontend/src/pages/admin/overview.ap-requests.vue
index d4c83f21b6..570fcddc07 100644
--- a/packages/frontend/src/pages/admin/overview.ap-requests.vue
+++ b/packages/frontend/src/pages/admin/overview.ap-requests.vue
@@ -23,6 +23,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { onMounted, shallowRef, ref } from 'vue';
 import { Chart } from 'chart.js';
 import gradient from 'chartjs-plugin-gradient';
+import isChromatic from 'chromatic';
 import { misskeyApi } from '@/scripts/misskey-api.js';
 import { useChartTooltip } from '@/scripts/use-chart-tooltip.js';
 import { chartVLine } from '@/scripts/chart-vline.js';
@@ -41,7 +42,7 @@ const { handler: externalTooltipHandler } = useChartTooltip();
 const { handler: externalTooltipHandler2 } = useChartTooltip();
 
 onMounted(async () => {
-	const now = new Date();
+	const now = isChromatic() ? new Date('2024-08-31T10:00:00Z') : new Date();
 
 	const getDate = (ago: number) => {
 		const y = now.getFullYear();
@@ -51,14 +52,14 @@ onMounted(async () => {
 		return new Date(y, m, d - ago);
 	};
 
-	const format = (arr) => {
+	const format = (arr: number[]) => {
 		return arr.map((v, i) => ({
 			x: getDate(i).getTime(),
 			y: v,
 		}));
 	};
 
-	const formatMinus = (arr) => {
+	const formatMinus = (arr: number[]) => {
 		return arr.map((v, i) => ({
 			x: getDate(i).getTime(),
 			y: -v,
@@ -78,7 +79,6 @@ onMounted(async () => {
 		type: 'line',
 		data: {
 			datasets: [{
-				stack: 'a',
 				parsing: false,
 				label: 'Out: Succ',
 				data: format(raw.deliverSucceeded).slice().reverse(),
@@ -92,7 +92,6 @@ onMounted(async () => {
 				fill: true,
 				clip: 8,
 			}, {
-				stack: 'a',
 				parsing: false,
 				label: 'Out: Fail',
 				data: formatMinus(raw.deliverFailed).slice().reverse(),
@@ -137,7 +136,6 @@ onMounted(async () => {
 					min: getDate(chartLimit).getTime(),
 				},
 				y: {
-					stacked: true,
 					position: 'left',
 					suggestedMax: 10,
 					grid: {
@@ -171,6 +169,9 @@ onMounted(async () => {
 						duration: 0,
 					},
 					external: externalTooltipHandler,
+					callbacks: {
+						label: context => `${context.dataset.label}: ${Math.abs(context.parsed.y)}`,
+					},
 				},
 				gradient,
 			},
@@ -277,7 +278,7 @@ onMounted(async () => {
 				padding: 16px;
 
 				&:first-child {
-					border-bottom: solid 0.5px var(--divider);
+					border-bottom: solid 0.5px var(--MI_THEME-divider);
 				}
 			}
 		}
diff --git a/packages/frontend/src/pages/admin/overview.federation.vue b/packages/frontend/src/pages/admin/overview.federation.vue
index 022b392d2d..0896859f3c 100644
--- a/packages/frontend/src/pages/admin/overview.federation.vue
+++ b/packages/frontend/src/pages/admin/overview.federation.vue
@@ -151,8 +151,8 @@ onMounted(async () => {
 					height: 100%;
 					aspect-ratio: 1;
 					margin-right: 12px;
-					background: var(--accentedBg);
-					color: var(--accent);
+					background: var(--MI_THEME-accentedBg);
+					color: var(--MI_THEME-accent);
 					border-radius: 10px;
 				}
 
diff --git a/packages/frontend/src/pages/admin/overview.instances.vue b/packages/frontend/src/pages/admin/overview.instances.vue
index a09db2a6d5..292e2e1dbc 100644
--- a/packages/frontend/src/pages/admin/overview.instances.vue
+++ b/packages/frontend/src/pages/admin/overview.instances.vue
@@ -20,7 +20,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { ref } from 'vue';
 import { misskeyApi } from '@/scripts/misskey-api.js';
 import * as Misskey from 'misskey-js';
-import { useInterval } from '@/scripts/use-interval.js';
+import { useInterval } from '@@/js/use-interval.js';
 import MkInstanceCardMini from '@/components/MkInstanceCardMini.vue';
 import { defaultStore } from '@/store.js';
 
diff --git a/packages/frontend/src/pages/admin/overview.pie.vue b/packages/frontend/src/pages/admin/overview.pie.vue
index c7a9f2a702..a21ec6c464 100644
--- a/packages/frontend/src/pages/admin/overview.pie.vue
+++ b/packages/frontend/src/pages/admin/overview.pie.vue
@@ -41,7 +41,7 @@ onMounted(() => {
 			labels: props.data.map(x => x.name),
 			datasets: [{
 				backgroundColor: props.data.map(x => x.color),
-				borderColor: getComputedStyle(document.documentElement).getPropertyValue('--panel'),
+				borderColor: getComputedStyle(document.documentElement).getPropertyValue('--MI_THEME-panel'),
 				borderWidth: 2,
 				hoverOffset: 0,
 				data: props.data.map(x => x.value),
diff --git a/packages/frontend/src/pages/admin/overview.queue.vue b/packages/frontend/src/pages/admin/overview.queue.vue
index c7478f252a..de6b254412 100644
--- a/packages/frontend/src/pages/admin/overview.queue.vue
+++ b/packages/frontend/src/pages/admin/overview.queue.vue
@@ -36,7 +36,9 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <script lang="ts" setup>
 import { markRaw, onMounted, onUnmounted, ref, shallowRef } from 'vue';
+import * as Misskey from 'misskey-js';
 import XChart from './overview.queue.chart.vue';
+import type { ApQueueDomain } from '@/pages/admin/queue.vue';
 import number from '@/filters/number.js';
 import { useStream } from '@/stream.js';
 
@@ -52,10 +54,10 @@ const chartDelayed = shallowRef<InstanceType<typeof XChart>>();
 const chartWaiting = shallowRef<InstanceType<typeof XChart>>();
 
 const props = defineProps<{
-	domain: string;
+	domain: ApQueueDomain;
 }>();
 
-const onStats = (stats) => {
+function onStats(stats: Misskey.entities.QueueStats) {
 	activeSincePrevTick.value = stats[props.domain].activeSincePrevTick;
 	active.value = stats[props.domain].active;
 	delayed.value = stats[props.domain].delayed;
@@ -65,13 +67,13 @@ const onStats = (stats) => {
 	chartActive.value.pushData(stats[props.domain].active);
 	chartDelayed.value.pushData(stats[props.domain].delayed);
 	chartWaiting.value.pushData(stats[props.domain].waiting);
-};
+}
 
-const onStatsLog = (statsLog) => {
-	const dataProcess = [];
-	const dataActive = [];
-	const dataDelayed = [];
-	const dataWaiting = [];
+function onStatsLog(statsLog: Misskey.entities.QueueStatsLog) {
+	const dataProcess: Misskey.entities.QueueStats[ApQueueDomain]['activeSincePrevTick'][] = [];
+	const dataActive: Misskey.entities.QueueStats[ApQueueDomain]['active'][] = [];
+	const dataDelayed: Misskey.entities.QueueStats[ApQueueDomain]['delayed'][] = [];
+	const dataWaiting: Misskey.entities.QueueStats[ApQueueDomain]['waiting'][] = [];
 
 	for (const stats of [...statsLog].reverse()) {
 		dataProcess.push(stats[props.domain].activeSincePrevTick);
@@ -84,7 +86,7 @@ const onStatsLog = (statsLog) => {
 	chartActive.value.setData(dataActive);
 	chartDelayed.value.setData(dataDelayed);
 	chartWaiting.value.setData(dataWaiting);
-};
+}
 
 onMounted(() => {
 	connection.on('stats', onStats);
@@ -117,8 +119,8 @@ onUnmounted(() => {
 			> .chart {
 				min-width: 0;
 				padding: 16px;
-				background: var(--panel);
-				border-radius: var(--radius);
+				background: var(--MI_THEME-panel);
+				border-radius: var(--MI-radius);
 
 				> .title {
 					font-size: 0.85em;
diff --git a/packages/frontend/src/pages/admin/overview.stats.vue b/packages/frontend/src/pages/admin/overview.stats.vue
index 0f4707f08d..222e9f4673 100644
--- a/packages/frontend/src/pages/admin/overview.stats.vue
+++ b/packages/frontend/src/pages/admin/overview.stats.vue
@@ -114,8 +114,8 @@ onMounted(async () => {
 				height: 100%;
 				aspect-ratio: 1;
 				margin-right: 12px;
-				background: var(--accentedBg);
-				color: var(--accent);
+				background: var(--MI_THEME-accentedBg);
+				color: var(--MI_THEME-accent);
 				border-radius: 10px;
 			}
 
diff --git a/packages/frontend/src/pages/admin/overview.users.vue b/packages/frontend/src/pages/admin/overview.users.vue
index 408be88d47..8c9d7a8197 100644
--- a/packages/frontend/src/pages/admin/overview.users.vue
+++ b/packages/frontend/src/pages/admin/overview.users.vue
@@ -20,7 +20,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { ref } from 'vue';
 import { misskeyApi } from '@/scripts/misskey-api.js';
 import * as Misskey from 'misskey-js';
-import { useInterval } from '@/scripts/use-interval.js';
+import { useInterval } from '@@/js/use-interval.js';
 import MkUserCardMini from '@/components/MkUserCardMini.vue';
 import { defaultStore } from '@/store.js';
 
@@ -47,14 +47,14 @@ useInterval(fetch, 1000 * 60, {
 .root {
 	&:global {
 		> .users {
-			.chart-move {
-				transition: transform 1s ease;
-			}
-
 			display: grid;
 			grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
 			grid-gap: 12px;
 
+			.chart-move {
+				transition: transform 1s ease;
+			}
+
 			> .user:hover {
 				text-decoration: none;
 			}
diff --git a/packages/frontend/src/pages/admin/performance.vue b/packages/frontend/src/pages/admin/performance.vue
new file mode 100644
index 0000000000..12338f0bf9
--- /dev/null
+++ b/packages/frontend/src/pages/admin/performance.vue
@@ -0,0 +1,209 @@
+<!--
+SPDX-FileCopyrightText: syuilo and misskey-project
+SPDX-License-Identifier: AGPL-3.0-only
+-->
+
+<template>
+<MkStickyContainer>
+	<template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template>
+	<MkSpacer :contentMax="700" :marginMin="16" :marginMax="32">
+		<div class="_gaps">
+			<div class="_panel" style="padding: 16px;">
+				<MkSwitch v-model="enableServerMachineStats" @change="onChange_enableServerMachineStats">
+					<template #label>{{ i18n.ts.enableServerMachineStats }}</template>
+					<template #caption>{{ i18n.ts.turnOffToImprovePerformance }}</template>
+				</MkSwitch>
+			</div>
+
+			<div class="_panel" style="padding: 16px;">
+				<MkSwitch v-model="enableIdenticonGeneration" @change="onChange_enableIdenticonGeneration">
+					<template #label>{{ i18n.ts.enableIdenticonGeneration }}</template>
+					<template #caption>{{ i18n.ts.turnOffToImprovePerformance }}</template>
+				</MkSwitch>
+			</div>
+
+			<div class="_panel" style="padding: 16px;">
+				<MkSwitch v-model="enableChartsForRemoteUser" @change="onChange_enableChartsForRemoteUser">
+					<template #label>{{ i18n.ts.enableChartsForRemoteUser }}</template>
+					<template #caption>{{ i18n.ts.turnOffToImprovePerformance }}</template>
+				</MkSwitch>
+			</div>
+
+			<div class="_panel" style="padding: 16px;">
+				<MkSwitch v-model="enableStatsForFederatedInstances" @change="onChange_enableStatsForFederatedInstances">
+					<template #label>{{ i18n.ts.enableStatsForFederatedInstances }}</template>
+					<template #caption>{{ i18n.ts.turnOffToImprovePerformance }}</template>
+				</MkSwitch>
+			</div>
+
+			<div class="_panel" style="padding: 16px;">
+				<MkSwitch v-model="enableChartsForFederatedInstances" @change="onChange_enableChartsForFederatedInstances">
+					<template #label>{{ i18n.ts.enableChartsForFederatedInstances }}</template>
+					<template #caption>{{ i18n.ts.turnOffToImprovePerformance }}</template>
+				</MkSwitch>
+			</div>
+
+			<MkFolder :defaultOpen="true">
+				<template #icon><i class="ti ti-bolt"></i></template>
+				<template #label>Misskey® Fan-out Timeline Technology™ (FTT)</template>
+				<template v-if="fttForm.savedState.enableFanoutTimeline" #suffix>Enabled</template>
+				<template v-else #suffix>Disabled</template>
+				<template v-if="fttForm.modified.value" #footer>
+					<MkFormFooter :form="fttForm"/>
+				</template>
+
+				<div class="_gaps">
+					<MkSwitch v-model="fttForm.state.enableFanoutTimeline">
+						<template #label>{{ i18n.ts.enable }}<span v-if="fttForm.modifiedStates.enableFanoutTimeline" class="_modified">{{ i18n.ts.modified }}</span></template>
+						<template #caption>
+							<div>{{ i18n.ts._serverSettings.fanoutTimelineDescription }}</div>
+							<div><MkLink target="_blank" url="https://misskey-hub.net/docs/for-admin/features/ftt/">{{ i18n.ts.details }}</MkLink></div>
+						</template>
+					</MkSwitch>
+
+					<template v-if="fttForm.state.enableFanoutTimeline">
+						<MkSwitch v-model="fttForm.state.enableFanoutTimelineDbFallback">
+							<template #label>{{ i18n.ts._serverSettings.fanoutTimelineDbFallback }}<span v-if="fttForm.modifiedStates.enableFanoutTimelineDbFallback" class="_modified">{{ i18n.ts.modified }}</span></template>
+							<template #caption>{{ i18n.ts._serverSettings.fanoutTimelineDbFallbackDescription }}</template>
+						</MkSwitch>
+
+						<MkInput v-model="fttForm.state.perLocalUserUserTimelineCacheMax" type="number">
+							<template #label>perLocalUserUserTimelineCacheMax<span v-if="fttForm.modifiedStates.perLocalUserUserTimelineCacheMax" class="_modified">{{ i18n.ts.modified }}</span></template>
+						</MkInput>
+
+						<MkInput v-model="fttForm.state.perRemoteUserUserTimelineCacheMax" type="number">
+							<template #label>perRemoteUserUserTimelineCacheMax<span v-if="fttForm.modifiedStates.perRemoteUserUserTimelineCacheMax" class="_modified">{{ i18n.ts.modified }}</span></template>
+						</MkInput>
+
+						<MkInput v-model="fttForm.state.perUserHomeTimelineCacheMax" type="number">
+							<template #label>perUserHomeTimelineCacheMax<span v-if="fttForm.modifiedStates.perUserHomeTimelineCacheMax" class="_modified">{{ i18n.ts.modified }}</span></template>
+						</MkInput>
+
+						<MkInput v-model="fttForm.state.perUserListTimelineCacheMax" type="number">
+							<template #label>perUserListTimelineCacheMax<span v-if="fttForm.modifiedStates.perUserListTimelineCacheMax" class="_modified">{{ i18n.ts.modified }}</span></template>
+						</MkInput>
+					</template>
+				</div>
+			</MkFolder>
+
+			<MkFolder :defaultOpen="true">
+				<template #icon><i class="ti ti-bolt"></i></template>
+				<template #label>Misskey® Reactions Boost Technology™ (RBT)<span class="_beta">{{ i18n.ts.beta }}</span></template>
+				<template v-if="rbtForm.savedState.enableReactionsBuffering" #suffix>Enabled</template>
+				<template v-else #suffix>Disabled</template>
+				<template v-if="rbtForm.modified.value" #footer>
+					<MkFormFooter :form="rbtForm"/>
+				</template>
+
+				<div class="_gaps_m">
+					<MkSwitch v-model="rbtForm.state.enableReactionsBuffering">
+						<template #label>{{ i18n.ts.enable }}<span v-if="rbtForm.modifiedStates.enableReactionsBuffering" class="_modified">{{ i18n.ts.modified }}</span></template>
+						<template #caption>{{ i18n.ts._serverSettings.reactionsBufferingDescription }}</template>
+					</MkSwitch>
+				</div>
+			</MkFolder>
+		</div>
+	</MkSpacer>
+</MkStickyContainer>
+</template>
+
+<script lang="ts" setup>
+import { ref, computed } from 'vue';
+import XHeader from './_header_.vue';
+import * as os from '@/os.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
+import { fetchInstance } from '@/instance.js';
+import { i18n } from '@/i18n.js';
+import { definePageMetadata } from '@/scripts/page-metadata.js';
+import MkSwitch from '@/components/MkSwitch.vue';
+import MkFolder from '@/components/MkFolder.vue';
+import MkInput from '@/components/MkInput.vue';
+import MkLink from '@/components/MkLink.vue';
+import { useForm } from '@/scripts/use-form.js';
+import MkFormFooter from '@/components/MkFormFooter.vue';
+
+const meta = await misskeyApi('admin/meta');
+
+const enableServerMachineStats = ref(meta.enableServerMachineStats);
+const enableIdenticonGeneration = ref(meta.enableIdenticonGeneration);
+const enableChartsForRemoteUser = ref(meta.enableChartsForRemoteUser);
+const enableStatsForFederatedInstances = ref(meta.enableStatsForFederatedInstances);
+const enableChartsForFederatedInstances = ref(meta.enableChartsForFederatedInstances);
+
+function onChange_enableServerMachineStats(value: boolean) {
+	os.apiWithDialog('admin/update-meta', {
+		enableServerMachineStats: value,
+	}).then(() => {
+		fetchInstance(true);
+	});
+}
+
+function onChange_enableIdenticonGeneration(value: boolean) {
+	os.apiWithDialog('admin/update-meta', {
+		enableIdenticonGeneration: value,
+	}).then(() => {
+		fetchInstance(true);
+	});
+}
+
+function onChange_enableChartsForRemoteUser(value: boolean) {
+	os.apiWithDialog('admin/update-meta', {
+		enableChartsForRemoteUser: value,
+	}).then(() => {
+		fetchInstance(true);
+	});
+}
+
+function onChange_enableStatsForFederatedInstances(value: boolean) {
+	os.apiWithDialog('admin/update-meta', {
+		enableStatsForFederatedInstances: value,
+	}).then(() => {
+		fetchInstance(true);
+	});
+}
+
+function onChange_enableChartsForFederatedInstances(value: boolean) {
+	os.apiWithDialog('admin/update-meta', {
+		enableChartsForFederatedInstances: value,
+	}).then(() => {
+		fetchInstance(true);
+	});
+}
+
+const fttForm = useForm({
+	enableFanoutTimeline: meta.enableFanoutTimeline,
+	enableFanoutTimelineDbFallback: meta.enableFanoutTimelineDbFallback,
+	perLocalUserUserTimelineCacheMax: meta.perLocalUserUserTimelineCacheMax,
+	perRemoteUserUserTimelineCacheMax: meta.perRemoteUserUserTimelineCacheMax,
+	perUserHomeTimelineCacheMax: meta.perUserHomeTimelineCacheMax,
+	perUserListTimelineCacheMax: meta.perUserListTimelineCacheMax,
+}, async (state) => {
+	await os.apiWithDialog('admin/update-meta', {
+		enableFanoutTimeline: state.enableFanoutTimeline,
+		enableFanoutTimelineDbFallback: state.enableFanoutTimelineDbFallback,
+		perLocalUserUserTimelineCacheMax: state.perLocalUserUserTimelineCacheMax,
+		perRemoteUserUserTimelineCacheMax: state.perRemoteUserUserTimelineCacheMax,
+		perUserHomeTimelineCacheMax: state.perUserHomeTimelineCacheMax,
+		perUserListTimelineCacheMax: state.perUserListTimelineCacheMax,
+	});
+	fetchInstance(true);
+});
+
+const rbtForm = useForm({
+	enableReactionsBuffering: meta.enableReactionsBuffering,
+}, async (state) => {
+	await os.apiWithDialog('admin/update-meta', {
+		enableReactionsBuffering: state.enableReactionsBuffering,
+	});
+	fetchInstance(true);
+});
+
+const headerActions = computed(() => []);
+
+const headerTabs = computed(() => []);
+
+definePageMetadata(() => ({
+	title: i18n.ts.other,
+	icon: 'ti ti-adjustments',
+}));
+</script>
diff --git a/packages/frontend/src/pages/admin/proxy-account.vue b/packages/frontend/src/pages/admin/proxy-account.vue
deleted file mode 100644
index 81db9f1da9..0000000000
--- a/packages/frontend/src/pages/admin/proxy-account.vue
+++ /dev/null
@@ -1,71 +0,0 @@
-<!--
-SPDX-FileCopyrightText: syuilo and misskey-project
-SPDX-License-Identifier: AGPL-3.0-only
--->
-
-<template>
-<MkStickyContainer>
-	<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
-	<MkSpacer :contentMax="700" :marginMin="16" :marginMax="32">
-		<FormSuspense :p="init">
-			<MkInfo>{{ i18n.ts.proxyAccountDescription }}</MkInfo>
-			<MkKeyValue>
-				<template #key>{{ i18n.ts.proxyAccount }}</template>
-				<template #value>{{ proxyAccount ? `@${proxyAccount.username}` : i18n.ts.none }}</template>
-			</MkKeyValue>
-
-			<MkButton primary @click="chooseProxyAccount">{{ i18n.ts.selectAccount }}</MkButton>
-		</FormSuspense>
-	</MkSpacer>
-</MkStickyContainer>
-</template>
-
-<script lang="ts" setup>
-import { ref, computed } from 'vue';
-import * as Misskey from 'misskey-js';
-import MkKeyValue from '@/components/MkKeyValue.vue';
-import MkButton from '@/components/MkButton.vue';
-import MkInfo from '@/components/MkInfo.vue';
-import FormSuspense from '@/components/form/suspense.vue';
-import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
-import { fetchInstance } from '@/instance.js';
-import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
-
-const proxyAccount = ref<Misskey.entities.UserDetailed | null>(null);
-const proxyAccountId = ref<string | null>(null);
-
-async function init() {
-	const meta = await misskeyApi('admin/meta');
-	proxyAccountId.value = meta.proxyAccountId;
-	if (proxyAccountId.value) {
-		proxyAccount.value = await misskeyApi('users/show', { userId: proxyAccountId.value });
-	}
-}
-
-function chooseProxyAccount() {
-	os.selectUser({ localOnly: true }).then(user => {
-		proxyAccount.value = user;
-		proxyAccountId.value = user.id;
-		save();
-	});
-}
-
-function save() {
-	os.apiWithDialog('admin/update-meta', {
-		proxyAccountId: proxyAccountId.value,
-	}).then(() => {
-		fetchInstance(true);
-	});
-}
-
-const headerActions = computed(() => []);
-
-const headerTabs = computed(() => []);
-
-definePageMetadata(() => ({
-	title: i18n.ts.proxyAccount,
-	icon: 'ti ti-ghost',
-}));
-</script>
diff --git a/packages/frontend/src/pages/admin/queue.chart.vue b/packages/frontend/src/pages/admin/queue.chart.vue
index 8d3fe35320..7c171ba0e1 100644
--- a/packages/frontend/src/pages/admin/queue.chart.vue
+++ b/packages/frontend/src/pages/admin/queue.chart.vue
@@ -49,7 +49,9 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <script lang="ts" setup>
 import { markRaw, onMounted, onUnmounted, ref, shallowRef } from 'vue';
+import * as Misskey from 'misskey-js';
 import XChart from './queue.chart.chart.vue';
+import type { ApQueueDomain } from '@/pages/admin/queue.vue';
 import number from '@/filters/number.js';
 import { misskeyApi } from '@/scripts/misskey-api.js';
 import { useStream } from '@/stream.js';
@@ -62,17 +64,17 @@ const activeSincePrevTick = ref(0);
 const active = ref(0);
 const delayed = ref(0);
 const waiting = ref(0);
-const jobs = ref<(string | number)[][]>([]);
+const jobs = ref<Misskey.Endpoints[`admin/queue/${ApQueueDomain}-delayed`]['res']>([]);
 const chartProcess = shallowRef<InstanceType<typeof XChart>>();
 const chartActive = shallowRef<InstanceType<typeof XChart>>();
 const chartDelayed = shallowRef<InstanceType<typeof XChart>>();
 const chartWaiting = shallowRef<InstanceType<typeof XChart>>();
 
 const props = defineProps<{
-	domain: string;
+	domain: ApQueueDomain;
 }>();
 
-const onStats = (stats) => {
+function onStats(stats: Misskey.entities.QueueStats) {
 	activeSincePrevTick.value = stats[props.domain].activeSincePrevTick;
 	active.value = stats[props.domain].active;
 	delayed.value = stats[props.domain].delayed;
@@ -82,13 +84,13 @@ const onStats = (stats) => {
 	chartActive.value.pushData(stats[props.domain].active);
 	chartDelayed.value.pushData(stats[props.domain].delayed);
 	chartWaiting.value.pushData(stats[props.domain].waiting);
-};
+}
 
-const onStatsLog = (statsLog) => {
-	const dataProcess = [];
-	const dataActive = [];
-	const dataDelayed = [];
-	const dataWaiting = [];
+function onStatsLog(statsLog: Misskey.entities.QueueStatsLog) {
+	const dataProcess: Misskey.entities.QueueStats[ApQueueDomain]['activeSincePrevTick'][] = [];
+	const dataActive: Misskey.entities.QueueStats[ApQueueDomain]['active'][] = [];
+	const dataDelayed: Misskey.entities.QueueStats[ApQueueDomain]['delayed'][] = [];
+	const dataWaiting: Misskey.entities.QueueStats[ApQueueDomain]['waiting'][] = [];
 
 	for (const stats of [...statsLog].reverse()) {
 		dataProcess.push(stats[props.domain].activeSincePrevTick);
@@ -101,14 +103,12 @@ const onStatsLog = (statsLog) => {
 	chartActive.value.setData(dataActive);
 	chartDelayed.value.setData(dataDelayed);
 	chartWaiting.value.setData(dataWaiting);
-};
+}
 
 onMounted(() => {
-	if (props.domain === 'inbox' || props.domain === 'deliver') {
-		misskeyApi(`admin/queue/${props.domain}-delayed`).then(result => {
-			jobs.value = result;
-		});
-	}
+	misskeyApi(`admin/queue/${props.domain}-delayed`).then(result => {
+		jobs.value = result;
+	});
 
 	connection.on('stats', onStats);
 	connection.on('statsLog', onStatsLog);
@@ -135,8 +135,8 @@ onUnmounted(() => {
 .chart {
 	min-width: 0;
 	padding: 16px;
-	background: var(--panel);
-	border-radius: var(--radius);
+	background: var(--MI_THEME-panel);
+	border-radius: var(--MI-radius);
 }
 
 .chartTitle {
diff --git a/packages/frontend/src/pages/admin/queue.vue b/packages/frontend/src/pages/admin/queue.vue
index 8d77d927d7..512039242e 100644
--- a/packages/frontend/src/pages/admin/queue.vue
+++ b/packages/frontend/src/pages/admin/queue.vue
@@ -16,16 +16,18 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { ref, computed } from 'vue';
+import { ref, computed, type Ref } from 'vue';
 import XQueue from './queue.chart.vue';
 import XHeader from './_header_.vue';
 import * as os from '@/os.js';
-import * as config from '@/config.js';
+import * as config from '@@/js/config.js';
 import { i18n } from '@/i18n.js';
 import { definePageMetadata } from '@/scripts/page-metadata.js';
 import MkButton from '@/components/MkButton.vue';
 
-const tab = ref('deliver');
+export type ApQueueDomain = 'deliver' | 'inbox';
+
+const tab: Ref<ApQueueDomain> = ref('deliver');
 
 function clear() {
 	os.confirm({
diff --git a/packages/frontend/src/pages/admin/relays.vue b/packages/frontend/src/pages/admin/relays.vue
index 04982eea1f..17e99e6593 100644
--- a/packages/frontend/src/pages/admin/relays.vue
+++ b/packages/frontend/src/pages/admin/relays.vue
@@ -11,8 +11,8 @@ SPDX-License-Identifier: AGPL-3.0-only
 			<div v-for="relay in relays" :key="relay.inbox" class="relaycxt _panel" style="padding: 16px;">
 				<div>{{ relay.inbox }}</div>
 				<div style="margin: 8px 0;">
-					<i v-if="relay.status === 'accepted'" class="ti ti-check" :class="$style.icon" style="color: var(--success);"></i>
-					<i v-else-if="relay.status === 'rejected'" class="ti ti-ban" :class="$style.icon" style="color: var(--error);"></i>
+					<i v-if="relay.status === 'accepted'" class="ti ti-check" :class="$style.icon" style="color: var(--MI_THEME-success);"></i>
+					<i v-else-if="relay.status === 'rejected'" class="ti ti-ban" :class="$style.icon" style="color: var(--MI_THEME-error);"></i>
 					<i v-else class="ti ti-clock" :class="$style.icon"></i>
 					<span>{{ i18n.ts._relayStatus[relay.status] }}</span>
 				</div>
diff --git a/packages/frontend/src/pages/admin/roles.edit.vue b/packages/frontend/src/pages/admin/roles.edit.vue
index 60f06d50ba..2b4006c3f7 100644
--- a/packages/frontend/src/pages/admin/roles.edit.vue
+++ b/packages/frontend/src/pages/admin/roles.edit.vue
@@ -95,7 +95,7 @@ definePageMetadata(() => ({
 
 <style lang="scss" module>
 .footer {
-	-webkit-backdrop-filter: var(--blur, blur(15px));
-	backdrop-filter: var(--blur, blur(15px));
+	-webkit-backdrop-filter: var(--MI-blur, blur(15px));
+	backdrop-filter: var(--MI-blur, blur(15px));
 }
 </style>
diff --git a/packages/frontend/src/pages/admin/roles.editor.vue b/packages/frontend/src/pages/admin/roles.editor.vue
index eb8a59b34f..ae01432d0c 100644
--- a/packages/frontend/src/pages/admin/roles.editor.vue
+++ b/packages/frontend/src/pages/admin/roles.editor.vue
@@ -378,6 +378,26 @@ SPDX-License-Identifier: AGPL-3.0-only
 				</div>
 			</MkFolder>
 
+			<MkFolder v-if="matchQuery([i18n.ts._role._options.canUpdateBioMedia, 'canUpdateBioMedia'])">
+				<template #label>{{ i18n.ts._role._options.canUpdateBioMedia }}</template>
+				<template #suffix>
+					<span v-if="role.policies.canUpdateBioMedia.useDefault" :class="$style.useDefaultLabel">{{ i18n.ts._role.useBaseValue }}</span>
+					<span v-else>{{ role.policies.canUpdateBioMedia.value ? i18n.ts.yes : i18n.ts.no }}</span>
+					<span :class="$style.priorityIndicator"><i :class="getPriorityIcon(role.policies.canUpdateBioMedia)"></i></span>
+				</template>
+				<div class="_gaps">
+					<MkSwitch v-model="role.policies.canUpdateBioMedia.useDefault" :readonly="readonly">
+						<template #label>{{ i18n.ts._role.useBaseValue }}</template>
+					</MkSwitch>
+					<MkSwitch v-model="role.policies.canUpdateBioMedia.value" :disabled="role.policies.canUpdateBioMedia.useDefault" :readonly="readonly">
+						<template #label>{{ i18n.ts.enable }}</template>
+					</MkSwitch>
+					<MkRange v-model="role.policies.canUpdateBioMedia.priority" :min="0" :max="2" :step="1" easing :textConverter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''">
+						<template #label>{{ i18n.ts._role.priority }}</template>
+					</MkRange>
+				</div>
+			</MkFolder>
+
 			<MkFolder v-if="matchQuery([i18n.ts._role._options.pinMax, 'pinLimit'])">
 				<template #label>{{ i18n.ts._role._options.pinMax }}</template>
 				<template #suffix>
@@ -570,6 +590,106 @@ SPDX-License-Identifier: AGPL-3.0-only
 					</MkRange>
 				</div>
 			</MkFolder>
+
+			<MkFolder v-if="matchQuery([i18n.ts._role._options.canImportAntennas, 'canImportAntennas'])">
+				<template #label>{{ i18n.ts._role._options.canImportAntennas }}</template>
+				<template #suffix>
+					<span v-if="role.policies.canImportAntennas.useDefault" :class="$style.useDefaultLabel">{{ i18n.ts._role.useBaseValue }}</span>
+					<span v-else>{{ role.policies.canImportAntennas.value ? i18n.ts.yes : i18n.ts.no }}</span>
+					<span :class="$style.priorityIndicator"><i :class="getPriorityIcon(role.policies.canImportAntennas)"></i></span>
+				</template>
+				<div class="_gaps">
+					<MkSwitch v-model="role.policies.canImportAntennas.useDefault" :readonly="readonly">
+						<template #label>{{ i18n.ts._role.useBaseValue }}</template>
+					</MkSwitch>
+					<MkSwitch v-model="role.policies.canImportAntennas.value" :disabled="role.policies.canImportAntennas.useDefault" :readonly="readonly">
+						<template #label>{{ i18n.ts.enable }}</template>
+					</MkSwitch>
+					<MkRange v-model="role.policies.canImportAntennas.priority" :min="0" :max="2" :step="1" easing :textConverter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''">
+						<template #label>{{ i18n.ts._role.priority }}</template>
+					</MkRange>
+				</div>
+			</MkFolder>
+
+			<MkFolder v-if="matchQuery([i18n.ts._role._options.canImportBlocking, 'canImportBlocking'])">
+				<template #label>{{ i18n.ts._role._options.canImportBlocking }}</template>
+				<template #suffix>
+					<span v-if="role.policies.canImportBlocking.useDefault" :class="$style.useDefaultLabel">{{ i18n.ts._role.useBaseValue }}</span>
+					<span v-else>{{ role.policies.canImportBlocking.value ? i18n.ts.yes : i18n.ts.no }}</span>
+					<span :class="$style.priorityIndicator"><i :class="getPriorityIcon(role.policies.canImportBlocking)"></i></span>
+				</template>
+				<div class="_gaps">
+					<MkSwitch v-model="role.policies.canImportBlocking.useDefault" :readonly="readonly">
+						<template #label>{{ i18n.ts._role.useBaseValue }}</template>
+					</MkSwitch>
+					<MkSwitch v-model="role.policies.canImportBlocking.value" :disabled="role.policies.canImportBlocking.useDefault" :readonly="readonly">
+						<template #label>{{ i18n.ts.enable }}</template>
+					</MkSwitch>
+					<MkRange v-model="role.policies.canImportBlocking.priority" :min="0" :max="2" :step="1" easing :textConverter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''">
+						<template #label>{{ i18n.ts._role.priority }}</template>
+					</MkRange>
+				</div>
+			</MkFolder>
+
+			<MkFolder v-if="matchQuery([i18n.ts._role._options.canImportFollowing, 'canImportFollowing'])">
+				<template #label>{{ i18n.ts._role._options.canImportFollowing }}</template>
+				<template #suffix>
+					<span v-if="role.policies.canImportFollowing.useDefault" :class="$style.useDefaultLabel">{{ i18n.ts._role.useBaseValue }}</span>
+					<span v-else>{{ role.policies.canImportFollowing.value ? i18n.ts.yes : i18n.ts.no }}</span>
+					<span :class="$style.priorityIndicator"><i :class="getPriorityIcon(role.policies.canImportFollowing)"></i></span>
+				</template>
+				<div class="_gaps">
+					<MkSwitch v-model="role.policies.canImportFollowing.useDefault" :readonly="readonly">
+						<template #label>{{ i18n.ts._role.useBaseValue }}</template>
+					</MkSwitch>
+					<MkSwitch v-model="role.policies.canImportFollowing.value" :disabled="role.policies.canImportFollowing.useDefault" :readonly="readonly">
+						<template #label>{{ i18n.ts.enable }}</template>
+					</MkSwitch>
+					<MkRange v-model="role.policies.canImportFollowing.priority" :min="0" :max="2" :step="1" easing :textConverter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''">
+						<template #label>{{ i18n.ts._role.priority }}</template>
+					</MkRange>
+				</div>
+			</MkFolder>
+
+			<MkFolder v-if="matchQuery([i18n.ts._role._options.canImportMuting, 'canImportMuting'])">
+				<template #label>{{ i18n.ts._role._options.canImportMuting }}</template>
+				<template #suffix>
+					<span v-if="role.policies.canImportMuting.useDefault" :class="$style.useDefaultLabel">{{ i18n.ts._role.useBaseValue }}</span>
+					<span v-else>{{ role.policies.canImportMuting.value ? i18n.ts.yes : i18n.ts.no }}</span>
+					<span :class="$style.priorityIndicator"><i :class="getPriorityIcon(role.policies.canImportMuting)"></i></span>
+				</template>
+				<div class="_gaps">
+					<MkSwitch v-model="role.policies.canImportMuting.useDefault" :readonly="readonly">
+						<template #label>{{ i18n.ts._role.useBaseValue }}</template>
+					</MkSwitch>
+					<MkSwitch v-model="role.policies.canImportMuting.value" :disabled="role.policies.canImportMuting.useDefault" :readonly="readonly">
+						<template #label>{{ i18n.ts.enable }}</template>
+					</MkSwitch>
+					<MkRange v-model="role.policies.canImportMuting.priority" :min="0" :max="2" :step="1" easing :textConverter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''">
+						<template #label>{{ i18n.ts._role.priority }}</template>
+					</MkRange>
+				</div>
+			</MkFolder>
+
+			<MkFolder v-if="matchQuery([i18n.ts._role._options.canImportUserLists, 'canImportUserLists'])">
+				<template #label>{{ i18n.ts._role._options.canImportUserLists }}</template>
+				<template #suffix>
+					<span v-if="role.policies.canImportUserLists.useDefault" :class="$style.useDefaultLabel">{{ i18n.ts._role.useBaseValue }}</span>
+					<span v-else>{{ role.policies.canImportUserLists.value ? i18n.ts.yes : i18n.ts.no }}</span>
+					<span :class="$style.priorityIndicator"><i :class="getPriorityIcon(role.policies.canImportUserLists)"></i></span>
+				</template>
+				<div class="_gaps">
+					<MkSwitch v-model="role.policies.canImportUserLists.useDefault" :readonly="readonly">
+						<template #label>{{ i18n.ts._role.useBaseValue }}</template>
+					</MkSwitch>
+					<MkSwitch v-model="role.policies.canImportUserLists.value" :disabled="role.policies.canImportUserLists.useDefault" :readonly="readonly">
+						<template #label>{{ i18n.ts.enable }}</template>
+					</MkSwitch>
+					<MkRange v-model="role.policies.canImportUserLists.priority" :min="0" :max="2" :step="1" easing :textConverter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''">
+						<template #label>{{ i18n.ts._role.priority }}</template>
+					</MkRange>
+				</div>
+			</MkFolder>
 		</div>
 	</FormSlot>
 </div>
@@ -588,7 +708,7 @@ import MkSwitch from '@/components/MkSwitch.vue';
 import MkRange from '@/components/MkRange.vue';
 import FormSlot from '@/components/form/slot.vue';
 import { i18n } from '@/i18n.js';
-import { ROLE_POLICIES } from '@/const.js';
+import { ROLE_POLICIES } from '@@/js/const.js';
 import { instance } from '@/instance.js';
 import { deepClone } from '@/scripts/clone.js';
 
diff --git a/packages/frontend/src/pages/admin/roles.role.vue b/packages/frontend/src/pages/admin/roles.role.vue
index 8b3c906d8a..1c237a69b4 100644
--- a/packages/frontend/src/pages/admin/roles.role.vue
+++ b/packages/frontend/src/pages/admin/roles.role.vue
@@ -184,7 +184,7 @@ definePageMetadata(() => ({
 .userItemSub {
 	padding: 6px 12px;
 	font-size: 85%;
-	color: var(--fgTransparentWeak);
+	color: var(--MI_THEME-fgTransparentWeak);
 }
 
 .userItemMainBody {
diff --git a/packages/frontend/src/pages/admin/roles.vue b/packages/frontend/src/pages/admin/roles.vue
index 9753d9f6cb..b1cbdad137 100644
--- a/packages/frontend/src/pages/admin/roles.vue
+++ b/packages/frontend/src/pages/admin/roles.vue
@@ -11,6 +11,9 @@ SPDX-License-Identifier: AGPL-3.0-only
 			<div class="_gaps">
 				<MkFolder>
 					<template #label>{{ i18n.ts._role.baseRole }}</template>
+					<template #footer>
+						<MkButton primary rounded @click="updateBaseRole">{{ i18n.ts.save }}</MkButton>
+					</template>
 					<div class="_gaps_s">
 						<MkInput v-model="baseRoleQ" type="search">
 							<template #prefix><i class="ti ti-search"></i></template>
@@ -134,6 +137,14 @@ SPDX-License-Identifier: AGPL-3.0-only
 							</MkSwitch>
 						</MkFolder>
 
+						<MkFolder v-if="matchQuery([i18n.ts._role._options.canUpdateBioMedia, 'canUpdateBioMedia'])">
+							<template #label>{{ i18n.ts._role._options.canUpdateBioMedia }}</template>
+							<template #suffix>{{ policies.canUpdateBioMedia ? i18n.ts.yes : i18n.ts.no }}</template>
+							<MkSwitch v-model="policies.canUpdateBioMedia">
+								<template #label>{{ i18n.ts.enable }}</template>
+							</MkSwitch>
+						</MkFolder>
+
 						<MkFolder v-if="matchQuery([i18n.ts._role._options.pinMax, 'pinLimit'])">
 							<template #label>{{ i18n.ts._role._options.pinMax }}</template>
 							<template #suffix>{{ policies.pinLimit }}</template>
@@ -206,7 +217,45 @@ SPDX-License-Identifier: AGPL-3.0-only
 							</MkInput>
 						</MkFolder>
 
-						<MkButton primary rounded @click="updateBaseRole">{{ i18n.ts.save }}</MkButton>
+						<MkFolder v-if="matchQuery([i18n.ts._role._options.canImportAntennas, 'canImportAntennas'])">
+							<template #label>{{ i18n.ts._role._options.canImportAntennas }}</template>
+							<template #suffix>{{ policies.canImportAntennas ? i18n.ts.yes : i18n.ts.no }}</template>
+							<MkSwitch v-model="policies.canImportAntennas">
+								<template #label>{{ i18n.ts.enable }}</template>
+							</MkSwitch>
+						</MkFolder>
+
+						<MkFolder v-if="matchQuery([i18n.ts._role._options.canImportBlocking, 'canImportBlocking'])">
+							<template #label>{{ i18n.ts._role._options.canImportBlocking }}</template>
+							<template #suffix>{{ policies.canImportBlocking ? i18n.ts.yes : i18n.ts.no }}</template>
+							<MkSwitch v-model="policies.canImportBlocking">
+								<template #label>{{ i18n.ts.enable }}</template>
+							</MkSwitch>
+						</MkFolder>
+
+						<MkFolder v-if="matchQuery([i18n.ts._role._options.canImportFollowing, 'canImportFollowing'])">
+							<template #label>{{ i18n.ts._role._options.canImportFollowing }}</template>
+							<template #suffix>{{ policies.canImportFollowing ? i18n.ts.yes : i18n.ts.no }}</template>
+							<MkSwitch v-model="policies.canImportFollowing">
+								<template #label>{{ i18n.ts.enable }}</template>
+							</MkSwitch>
+						</MkFolder>
+
+						<MkFolder v-if="matchQuery([i18n.ts._role._options.canImportMuting, 'canImportMuting'])">
+							<template #label>{{ i18n.ts._role._options.canImportMuting }}</template>
+							<template #suffix>{{ policies.canImportMuting ? i18n.ts.yes : i18n.ts.no }}</template>
+							<MkSwitch v-model="policies.canImportMuting">
+								<template #label>{{ i18n.ts.enable }}</template>
+							</MkSwitch>
+						</MkFolder>
+
+						<MkFolder v-if="matchQuery([i18n.ts._role._options.canImportUserLists, 'canImportUserList'])">
+							<template #label>{{ i18n.ts._role._options.canImportUserLists }}</template>
+							<template #suffix>{{ policies.canImportUserLists ? i18n.ts.yes : i18n.ts.no }}</template>
+							<MkSwitch v-model="policies.canImportUserLists">
+								<template #label>{{ i18n.ts.enable }}</template>
+							</MkSwitch>
+						</MkFolder>
 					</div>
 				</MkFolder>
 				<MkButton primary rounded @click="create"><i class="ti ti-plus"></i> {{ i18n.ts._role.new }}</MkButton>
@@ -232,6 +281,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <script lang="ts" setup>
 import { computed, reactive, ref } from 'vue';
+import { ROLE_POLICIES } from '@@/js/const.js';
 import XHeader from './_header_.vue';
 import MkInput from '@/components/MkInput.vue';
 import MkFolder from '@/components/MkFolder.vue';
@@ -243,9 +293,8 @@ import * as os from '@/os.js';
 import { misskeyApi } from '@/scripts/misskey-api.js';
 import { i18n } from '@/i18n.js';
 import { definePageMetadata } from '@/scripts/page-metadata.js';
-import { instance } from '@/instance.js';
+import { instance, fetchInstance } from '@/instance.js';
 import MkFoldableSection from '@/components/MkFoldableSection.vue';
-import { ROLE_POLICIES } from '@/const.js';
 import { useRouter } from '@/router/supplier.js';
 
 const router = useRouter();
@@ -267,6 +316,7 @@ async function updateBaseRole() {
 	await os.apiWithDialog('admin/roles/update-default-policies', {
 		policies,
 	});
+	fetchInstance(true);
 }
 
 function create() {
diff --git a/packages/frontend/src/pages/admin/security.vue b/packages/frontend/src/pages/admin/security.vue
index 9bccee89a5..975a4a1265 100644
--- a/packages/frontend/src/pages/admin/security.vue
+++ b/packages/frontend/src/pages/admin/security.vue
@@ -7,119 +7,115 @@ SPDX-License-Identifier: AGPL-3.0-only
 <MkStickyContainer>
 	<template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template>
 	<MkSpacer :contentMax="700" :marginMin="16" :marginMax="32">
-		<FormSuspense :p="init">
-			<div class="_gaps_m">
-				<MkFolder>
-					<template #icon><i class="ti ti-shield"></i></template>
-					<template #label>{{ i18n.ts.botProtection }}</template>
-					<template v-if="enableHcaptcha" #suffix>hCaptcha</template>
-					<template v-else-if="enableMcaptcha" #suffix>mCaptcha</template>
-					<template v-else-if="enableRecaptcha" #suffix>reCAPTCHA</template>
-					<template v-else-if="enableTurnstile" #suffix>Turnstile</template>
-					<template v-else #suffix>{{ i18n.ts.none }} ({{ i18n.ts.notRecommended }})</template>
+		<div class="_gaps_m">
+			<XBotProtection/>
 
-					<XBotProtection/>
-				</MkFolder>
+			<MkFolder>
+				<template #icon><i class="ti ti-eye-off"></i></template>
+				<template #label>{{ i18n.ts.sensitiveMediaDetection }}</template>
+				<template v-if="sensitiveMediaDetectionForm.savedState.sensitiveMediaDetection === 'all'" #suffix>{{ i18n.ts.all }}</template>
+				<template v-else-if="sensitiveMediaDetectionForm.savedState.sensitiveMediaDetection === 'local'" #suffix>{{ i18n.ts.localOnly }}</template>
+				<template v-else-if="sensitiveMediaDetectionForm.savedState.sensitiveMediaDetection === 'remote'" #suffix>{{ i18n.ts.remoteOnly }}</template>
+				<template v-else #suffix>{{ i18n.ts.none }}</template>
+				<template v-if="sensitiveMediaDetectionForm.modified.value" #footer>
+					<MkFormFooter :form="sensitiveMediaDetectionForm"/>
+				</template>
 
-				<MkFolder>
-					<template #icon><i class="ti ti-eye-off"></i></template>
-					<template #label>{{ i18n.ts.sensitiveMediaDetection }}</template>
-					<template v-if="sensitiveMediaDetection === 'all'" #suffix>{{ i18n.ts.all }}</template>
-					<template v-else-if="sensitiveMediaDetection === 'local'" #suffix>{{ i18n.ts.localOnly }}</template>
-					<template v-else-if="sensitiveMediaDetection === 'remote'" #suffix>{{ i18n.ts.remoteOnly }}</template>
-					<template v-else #suffix>{{ i18n.ts.none }}</template>
+				<div class="_gaps_m">
+					<span>{{ i18n.ts._sensitiveMediaDetection.description }}</span>
 
-					<div class="_gaps_m">
-						<span>{{ i18n.ts._sensitiveMediaDetection.description }}</span>
+					<MkRadios v-model="sensitiveMediaDetectionForm.state.sensitiveMediaDetection">
+						<option value="none">{{ i18n.ts.none }}</option>
+						<option value="all">{{ i18n.ts.all }}</option>
+						<option value="local">{{ i18n.ts.localOnly }}</option>
+						<option value="remote">{{ i18n.ts.remoteOnly }}</option>
+					</MkRadios>
 
-						<MkRadios v-model="sensitiveMediaDetection">
-							<option value="none">{{ i18n.ts.none }}</option>
-							<option value="all">{{ i18n.ts.all }}</option>
-							<option value="local">{{ i18n.ts.localOnly }}</option>
-							<option value="remote">{{ i18n.ts.remoteOnly }}</option>
-						</MkRadios>
+					<MkRange v-model="sensitiveMediaDetectionForm.state.sensitiveMediaDetectionSensitivity" :min="0" :max="4" :step="1" :textConverter="(v) => `${v + 1}`">
+						<template #label>{{ i18n.ts._sensitiveMediaDetection.sensitivity }}</template>
+						<template #caption>{{ i18n.ts._sensitiveMediaDetection.sensitivityDescription }}</template>
+					</MkRange>
 
-						<MkRange v-model="sensitiveMediaDetectionSensitivity" :min="0" :max="4" :step="1" :textConverter="(v) => `${v + 1}`">
-							<template #label>{{ i18n.ts._sensitiveMediaDetection.sensitivity }}</template>
-							<template #caption>{{ i18n.ts._sensitiveMediaDetection.sensitivityDescription }}</template>
-						</MkRange>
+					<MkSwitch v-model="sensitiveMediaDetectionForm.state.enableSensitiveMediaDetectionForVideos">
+						<template #label>{{ i18n.ts._sensitiveMediaDetection.analyzeVideos }}<span class="_beta">{{ i18n.ts.beta }}</span></template>
+						<template #caption>{{ i18n.ts._sensitiveMediaDetection.analyzeVideosDescription }}</template>
+					</MkSwitch>
 
-						<MkSwitch v-model="enableSensitiveMediaDetectionForVideos">
-							<template #label>{{ i18n.ts._sensitiveMediaDetection.analyzeVideos }}<span class="_beta">{{ i18n.ts.beta }}</span></template>
-							<template #caption>{{ i18n.ts._sensitiveMediaDetection.analyzeVideosDescription }}</template>
-						</MkSwitch>
+					<MkSwitch v-model="sensitiveMediaDetectionForm.state.setSensitiveFlagAutomatically">
+						<template #label>{{ i18n.ts._sensitiveMediaDetection.setSensitiveFlagAutomatically }} ({{ i18n.ts.notRecommended }})</template>
+						<template #caption>{{ i18n.ts._sensitiveMediaDetection.setSensitiveFlagAutomaticallyDescription }}</template>
+					</MkSwitch>
 
-						<MkSwitch v-model="setSensitiveFlagAutomatically">
-							<template #label>{{ i18n.ts._sensitiveMediaDetection.setSensitiveFlagAutomatically }} ({{ i18n.ts.notRecommended }})</template>
-							<template #caption>{{ i18n.ts._sensitiveMediaDetection.setSensitiveFlagAutomaticallyDescription }}</template>
-						</MkSwitch>
+					<!-- 現状 false positive が多すぎて実用に耐えない
+					<MkSwitch v-model="disallowUploadWhenPredictedAsPorn">
+						<template #label>{{ i18n.ts._sensitiveMediaDetection.disallowUploadWhenPredictedAsPorn }}</template>
+					</MkSwitch>
+					-->
+				</div>
+			</MkFolder>
 
-						<!-- 現状 false positive が多すぎて実用に耐えない
-						<MkSwitch v-model="disallowUploadWhenPredictedAsPorn">
-							<template #label>{{ i18n.ts._sensitiveMediaDetection.disallowUploadWhenPredictedAsPorn }}</template>
-						</MkSwitch>
-						-->
+			<MkFolder>
+				<template #label>Active Email Validation</template>
+				<template v-if="emailValidationForm.savedState.enableActiveEmailValidation" #suffix>Enabled</template>
+				<template v-else #suffix>Disabled</template>
+				<template v-if="emailValidationForm.modified.value" #footer>
+					<MkFormFooter :form="emailValidationForm"/>
+				</template>
 
-						<MkButton primary @click="save"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton>
-					</div>
-				</MkFolder>
+				<div class="_gaps_m">
+					<span>{{ i18n.ts.activeEmailValidationDescription }}</span>
+					<MkSwitch v-model="emailValidationForm.state.enableActiveEmailValidation">
+						<template #label>Enable</template>
+					</MkSwitch>
+					<MkSwitch v-model="emailValidationForm.state.enableVerifymailApi">
+						<template #label>Use Verifymail.io API</template>
+					</MkSwitch>
+					<MkInput v-model="emailValidationForm.state.verifymailAuthKey">
+						<template #prefix><i class="ti ti-key"></i></template>
+						<template #label>Verifymail.io API Auth Key</template>
+					</MkInput>
+					<MkSwitch v-model="emailValidationForm.state.enableTruemailApi">
+						<template #label>Use TrueMail API</template>
+					</MkSwitch>
+					<MkInput v-model="emailValidationForm.state.truemailInstance">
+						<template #prefix><i class="ti ti-key"></i></template>
+						<template #label>TrueMail API Instance</template>
+					</MkInput>
+					<MkInput v-model="emailValidationForm.state.truemailAuthKey">
+						<template #prefix><i class="ti ti-key"></i></template>
+						<template #label>TrueMail API Auth Key</template>
+					</MkInput>
+				</div>
+			</MkFolder>
 
-				<MkFolder>
-					<template #label>Active Email Validation</template>
-					<template v-if="enableActiveEmailValidation" #suffix>Enabled</template>
-					<template v-else #suffix>Disabled</template>
+			<MkFolder>
+				<template #label>Banned Email Domains</template>
+				<template v-if="bannedEmailDomainsForm.modified.value" #footer>
+					<MkFormFooter :form="bannedEmailDomainsForm"/>
+				</template>
 
-					<div class="_gaps_m">
-						<span>{{ i18n.ts.activeEmailValidationDescription }}</span>
-						<MkSwitch v-model="enableActiveEmailValidation">
-							<template #label>Enable</template>
-						</MkSwitch>
-						<MkSwitch v-model="enableVerifymailApi">
-							<template #label>Use Verifymail.io API</template>
-						</MkSwitch>
-						<MkInput v-model="verifymailAuthKey">
-							<template #prefix><i class="ti ti-key"></i></template>
-							<template #label>Verifymail.io API Auth Key</template>
-						</MkInput>
-						<MkSwitch v-model="enableTruemailApi">
-							<template #label>Use TrueMail API</template>
-						</MkSwitch>
-						<MkInput v-model="truemailInstance">
-							<template #prefix><i class="ti ti-key"></i></template>
-							<template #label>TrueMail API Instance</template>
-						</MkInput>
-						<MkInput v-model="truemailAuthKey">
-							<template #prefix><i class="ti ti-key"></i></template>
-							<template #label>TrueMail API Auth Key</template>
-						</MkInput>
-						<MkButton primary @click="save"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton>
-					</div>
-				</MkFolder>
+				<div class="_gaps_m">
+					<MkTextarea v-model="bannedEmailDomainsForm.state.bannedEmailDomains">
+						<template #label>Banned Email Domains List</template>
+					</MkTextarea>
+				</div>
+			</MkFolder>
 
-				<MkFolder>
-					<template #label>Banned Email Domains</template>
+			<MkFolder>
+				<template #label>Log IP address</template>
+				<template v-if="ipLoggingForm.savedState.enableIpLogging" #suffix>Enabled</template>
+				<template v-else #suffix>Disabled</template>
+				<template v-if="ipLoggingForm.modified.value" #footer>
+					<MkFormFooter :form="ipLoggingForm"/>
+				</template>
 
-					<div class="_gaps_m">
-						<MkTextarea v-model="bannedEmailDomains">
-							<template #label>Banned Email Domains List</template>
-						</MkTextarea>
-						<MkButton primary @click="save"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton>
-					</div>
-				</MkFolder>
-
-				<MkFolder>
-					<template #label>Log IP address</template>
-					<template v-if="enableIpLogging" #suffix>Enabled</template>
-					<template v-else #suffix>Disabled</template>
-
-					<div class="_gaps_m">
-						<MkSwitch v-model="enableIpLogging" @update:modelValue="save">
-							<template #label>Enable</template>
-						</MkSwitch>
-					</div>
-				</MkFolder>
-			</div>
-		</FormSuspense>
+				<div class="_gaps_m">
+					<MkSwitch v-model="ipLoggingForm.state.enableIpLogging">
+						<template #label>Enable</template>
+					</MkSwitch>
+				</div>
+			</MkFolder>
+		</div>
 	</MkSpacer>
 </MkStickyContainer>
 </template>
@@ -131,83 +127,80 @@ import XHeader from './_header_.vue';
 import MkFolder from '@/components/MkFolder.vue';
 import MkRadios from '@/components/MkRadios.vue';
 import MkSwitch from '@/components/MkSwitch.vue';
-import FormSuspense from '@/components/form/suspense.vue';
 import MkRange from '@/components/MkRange.vue';
 import MkInput from '@/components/MkInput.vue';
-import MkButton from '@/components/MkButton.vue';
 import MkTextarea from '@/components/MkTextarea.vue';
 import * as os from '@/os.js';
 import { misskeyApi } from '@/scripts/misskey-api.js';
 import { fetchInstance } from '@/instance.js';
 import { i18n } from '@/i18n.js';
 import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { useForm } from '@/scripts/use-form.js';
+import MkFormFooter from '@/components/MkFormFooter.vue';
 
-const enableHcaptcha = ref<boolean>(false);
-const enableMcaptcha = ref<boolean>(false);
-const enableRecaptcha = ref<boolean>(false);
-const enableTurnstile = ref<boolean>(false);
-const sensitiveMediaDetection = ref<string>('none');
-const sensitiveMediaDetectionSensitivity = ref<number>(0);
-const setSensitiveFlagAutomatically = ref<boolean>(false);
-const enableSensitiveMediaDetectionForVideos = ref<boolean>(false);
-const enableIpLogging = ref<boolean>(false);
-const enableActiveEmailValidation = ref<boolean>(false);
-const enableVerifymailApi = ref<boolean>(false);
-const verifymailAuthKey = ref<string | null>(null);
-const enableTruemailApi = ref<boolean>(false);
-const truemailInstance = ref<string | null>(null);
-const truemailAuthKey = ref<string | null>(null);
-const bannedEmailDomains = ref<string>('');
+const meta = await misskeyApi('admin/meta');
 
-async function init() {
-	const meta = await misskeyApi('admin/meta');
-	enableHcaptcha.value = meta.enableHcaptcha;
-	enableMcaptcha.value = meta.enableMcaptcha;
-	enableRecaptcha.value = meta.enableRecaptcha;
-	enableTurnstile.value = meta.enableTurnstile;
-	sensitiveMediaDetection.value = meta.sensitiveMediaDetection;
-	sensitiveMediaDetectionSensitivity.value =
-		meta.sensitiveMediaDetectionSensitivity === 'veryLow' ? 0 :
-		meta.sensitiveMediaDetectionSensitivity === 'low' ? 1 :
-		meta.sensitiveMediaDetectionSensitivity === 'medium' ? 2 :
-		meta.sensitiveMediaDetectionSensitivity === 'high' ? 3 :
-		meta.sensitiveMediaDetectionSensitivity === 'veryHigh' ? 4 : 0;
-	setSensitiveFlagAutomatically.value = meta.setSensitiveFlagAutomatically;
-	enableSensitiveMediaDetectionForVideos.value = meta.enableSensitiveMediaDetectionForVideos;
-	enableIpLogging.value = meta.enableIpLogging;
-	enableActiveEmailValidation.value = meta.enableActiveEmailValidation;
-	enableVerifymailApi.value = meta.enableVerifymailApi;
-	verifymailAuthKey.value = meta.verifymailAuthKey;
-	enableTruemailApi.value = meta.enableTruemailApi;
-	truemailInstance.value = meta.truemailInstance;
-	truemailAuthKey.value = meta.truemailAuthKey;
-	bannedEmailDomains.value = meta.bannedEmailDomains?.join('\n') || '';
-}
-
-function save() {
-	os.apiWithDialog('admin/update-meta', {
-		sensitiveMediaDetection: sensitiveMediaDetection.value,
+const sensitiveMediaDetectionForm = useForm({
+	sensitiveMediaDetection: meta.sensitiveMediaDetection,
+	sensitiveMediaDetectionSensitivity: meta.sensitiveMediaDetectionSensitivity === 'veryLow' ? 0 :
+	meta.sensitiveMediaDetectionSensitivity === 'low' ? 1 :
+	meta.sensitiveMediaDetectionSensitivity === 'medium' ? 2 :
+	meta.sensitiveMediaDetectionSensitivity === 'high' ? 3 :
+	meta.sensitiveMediaDetectionSensitivity === 'veryHigh' ? 4 : 0,
+	setSensitiveFlagAutomatically: meta.setSensitiveFlagAutomatically,
+	enableSensitiveMediaDetectionForVideos: meta.enableSensitiveMediaDetectionForVideos,
+}, async (state) => {
+	await os.apiWithDialog('admin/update-meta', {
+		sensitiveMediaDetection: state.sensitiveMediaDetection,
 		sensitiveMediaDetectionSensitivity:
-			sensitiveMediaDetectionSensitivity.value === 0 ? 'veryLow' :
-			sensitiveMediaDetectionSensitivity.value === 1 ? 'low' :
-			sensitiveMediaDetectionSensitivity.value === 2 ? 'medium' :
-			sensitiveMediaDetectionSensitivity.value === 3 ? 'high' :
-			sensitiveMediaDetectionSensitivity.value === 4 ? 'veryHigh' :
+			state.sensitiveMediaDetectionSensitivity === 0 ? 'veryLow' :
+			state.sensitiveMediaDetectionSensitivity === 1 ? 'low' :
+			state.sensitiveMediaDetectionSensitivity === 2 ? 'medium' :
+			state.sensitiveMediaDetectionSensitivity === 3 ? 'high' :
+			state.sensitiveMediaDetectionSensitivity === 4 ? 'veryHigh' :
 			0,
-		setSensitiveFlagAutomatically: setSensitiveFlagAutomatically.value,
-		enableSensitiveMediaDetectionForVideos: enableSensitiveMediaDetectionForVideos.value,
-		enableIpLogging: enableIpLogging.value,
-		enableActiveEmailValidation: enableActiveEmailValidation.value,
-		enableVerifymailApi: enableVerifymailApi.value,
-		verifymailAuthKey: verifymailAuthKey.value,
-		enableTruemailApi: enableTruemailApi.value,
-		truemailInstance: truemailInstance.value,
-		truemailAuthKey: truemailAuthKey.value,
-		bannedEmailDomains: bannedEmailDomains.value.split('\n'),
-	}).then(() => {
-		fetchInstance(true);
+		setSensitiveFlagAutomatically: state.setSensitiveFlagAutomatically,
+		enableSensitiveMediaDetectionForVideos: state.enableSensitiveMediaDetectionForVideos,
 	});
-}
+	fetchInstance(true);
+});
+
+const ipLoggingForm = useForm({
+	enableIpLogging: meta.enableIpLogging,
+}, async (state) => {
+	await os.apiWithDialog('admin/update-meta', {
+		enableIpLogging: state.enableIpLogging,
+	});
+	fetchInstance(true);
+});
+
+const emailValidationForm = useForm({
+	enableActiveEmailValidation: meta.enableActiveEmailValidation,
+	enableVerifymailApi: meta.enableVerifymailApi,
+	verifymailAuthKey: meta.verifymailAuthKey,
+	enableTruemailApi: meta.enableTruemailApi,
+	truemailInstance: meta.truemailInstance,
+	truemailAuthKey: meta.truemailAuthKey,
+}, async (state) => {
+	await os.apiWithDialog('admin/update-meta', {
+		enableActiveEmailValidation: state.enableActiveEmailValidation,
+		enableVerifymailApi: state.enableVerifymailApi,
+		verifymailAuthKey: state.verifymailAuthKey,
+		enableTruemailApi: state.enableTruemailApi,
+		truemailInstance: state.truemailInstance,
+		truemailAuthKey: state.truemailAuthKey,
+	});
+	fetchInstance(true);
+});
+
+const bannedEmailDomainsForm = useForm({
+	bannedEmailDomains: meta.bannedEmailDomains?.join('\n') || '',
+}, async (state) => {
+	await os.apiWithDialog('admin/update-meta', {
+		bannedEmailDomains: state.bannedEmailDomains.split('\n'),
+	});
+	fetchInstance(true);
+});
 
 const headerActions = computed(() => []);
 
diff --git a/packages/frontend/src/pages/admin/server-rules.vue b/packages/frontend/src/pages/admin/server-rules.vue
index ff9b8d6299..552958af6f 100644
--- a/packages/frontend/src/pages/admin/server-rules.vue
+++ b/packages/frontend/src/pages/admin/server-rules.vue
@@ -76,7 +76,7 @@ definePageMetadata(() => ({
 <style lang="scss" module>
 .item {
 	display: block;
-	color: var(--navFg);
+	color: var(--MI_THEME-navFg);
 }
 
 .itemHeader {
@@ -96,8 +96,8 @@ definePageMetadata(() => ({
 
 .itemNumber {
 	display: flex;
-	background-color: var(--accentedBg);
-	color: var(--accent);
+	background-color: var(--MI_THEME-accentedBg);
+	color: var(--MI_THEME-accent);
 	font-size: 14px;
 	font-weight: bold;
 	width: 28px;
@@ -117,12 +117,12 @@ definePageMetadata(() => ({
 .itemRemove {
 	width: 40px;
 	height: 40px;
-	color: var(--error);
+	color: var(--MI_THEME-error);
 	margin-left: auto;
 	border-radius: 6px;
 
 	&:hover {
-		background: var(--X5);
+		background: var(--MI_THEME-X5);
 	}
 }
 
diff --git a/packages/frontend/src/pages/admin/settings.vue b/packages/frontend/src/pages/admin/settings.vue
index 6f45c212ec..ea7603a45a 100644
--- a/packages/frontend/src/pages/admin/settings.vue
+++ b/packages/frontend/src/pages/admin/settings.vue
@@ -8,173 +8,191 @@ SPDX-License-Identifier: AGPL-3.0-only
 	<MkStickyContainer>
 		<template #header><XHeader :tabs="headerTabs"/></template>
 		<MkSpacer :contentMax="700" :marginMin="16" :marginMax="32">
-			<FormSuspense :p="init">
-				<div class="_gaps_m">
-					<MkInput v-model="name">
-						<template #label>{{ i18n.ts.instanceName }}</template>
-					</MkInput>
+			<div class="_gaps_m">
+				<MkFolder :defaultOpen="true">
+					<template #icon><i class="ti ti-info-circle"></i></template>
+					<template #label>{{ i18n.ts.info }}</template>
+					<template v-if="infoForm.modified.value" #footer>
+						<MkFormFooter :form="infoForm"/>
+					</template>
 
-					<MkInput v-model="shortName">
-						<template #label>{{ i18n.ts._serverSettings.shortName }} ({{ i18n.ts.optional }})</template>
-						<template #caption>{{ i18n.ts._serverSettings.shortNameDescription }}</template>
-					</MkInput>
-
-					<MkTextarea v-model="description">
-						<template #label>{{ i18n.ts.instanceDescription }}</template>
-					</MkTextarea>
-
-					<FormSplit :minWidth="300">
-						<MkInput v-model="maintainerName">
-							<template #label>{{ i18n.ts.maintainerName }}</template>
+					<div class="_gaps">
+						<MkInput v-model="infoForm.state.name">
+							<template #label>{{ i18n.ts.instanceName }}<span v-if="infoForm.modifiedStates.name" class="_modified">{{ i18n.ts.modified }}</span></template>
 						</MkInput>
 
-						<MkInput v-model="maintainerEmail" type="email">
-							<template #prefix><i class="ti ti-mail"></i></template>
-							<template #label>{{ i18n.ts.maintainerEmail }}</template>
+						<MkInput v-model="infoForm.state.shortName">
+							<template #label>{{ i18n.ts._serverSettings.shortName }} ({{ i18n.ts.optional }})<span v-if="infoForm.modifiedStates.shortName" class="_modified">{{ i18n.ts.modified }}</span></template>
+							<template #caption>{{ i18n.ts._serverSettings.shortNameDescription }}</template>
 						</MkInput>
-					</FormSplit>
 
-					<MkInput v-model="repositoryUrl" type="url">
-						<template #label>{{ i18n.ts.repositoryUrl }}</template>
-						<template #prefix><i class="ti ti-link"></i></template>
-						<template #caption>{{ i18n.ts.repositoryUrlDescription }}</template>
-					</MkInput>
+						<MkTextarea v-model="infoForm.state.description">
+							<template #label>{{ i18n.ts.instanceDescription }}<span v-if="infoForm.modifiedStates.description" class="_modified">{{ i18n.ts.modified }}</span></template>
+						</MkTextarea>
 
-					<MkInfo v-if="!instance.providesTarball && !repositoryUrl" warn>
-						{{ i18n.ts.repositoryUrlOrTarballRequired }}
-					</MkInfo>
+						<FormSplit :minWidth="300">
+							<MkInput v-model="infoForm.state.maintainerName">
+								<template #label>{{ i18n.ts.maintainerName }}<span v-if="infoForm.modifiedStates.maintainerName" class="_modified">{{ i18n.ts.modified }}</span></template>
+							</MkInput>
 
-					<MkInput v-model="impressumUrl" type="url">
-						<template #label>{{ i18n.ts.impressumUrl }}</template>
-						<template #prefix><i class="ti ti-link"></i></template>
-						<template #caption>{{ i18n.ts.impressumDescription }}</template>
-					</MkInput>
+							<MkInput v-model="infoForm.state.maintainerEmail" type="email">
+								<template #label>{{ i18n.ts.maintainerEmail }}<span v-if="infoForm.modifiedStates.maintainerEmail" class="_modified">{{ i18n.ts.modified }}</span></template>
+								<template #prefix><i class="ti ti-mail"></i></template>
+							</MkInput>
+						</FormSplit>
 
-					<MkTextarea v-model="pinnedUsers">
-						<template #label>{{ i18n.ts.pinnedUsers }}</template>
+						<MkInput v-model="infoForm.state.tosUrl" type="url">
+							<template #label>{{ i18n.ts.tosUrl }}<span v-if="infoForm.modifiedStates.tosUrl" class="_modified">{{ i18n.ts.modified }}</span></template>
+							<template #prefix><i class="ti ti-link"></i></template>
+						</MkInput>
+
+						<MkInput v-model="infoForm.state.privacyPolicyUrl" type="url">
+							<template #label>{{ i18n.ts.privacyPolicyUrl }}<span v-if="infoForm.modifiedStates.privacyPolicyUrl" class="_modified">{{ i18n.ts.modified }}</span></template>
+							<template #prefix><i class="ti ti-link"></i></template>
+						</MkInput>
+
+						<MkInput v-model="infoForm.state.inquiryUrl" type="url">
+							<template #label>{{ i18n.ts._serverSettings.inquiryUrl }}<span v-if="infoForm.modifiedStates.inquiryUrl" class="_modified">{{ i18n.ts.modified }}</span></template>
+							<template #caption>{{ i18n.ts._serverSettings.inquiryUrlDescription }}</template>
+							<template #prefix><i class="ti ti-link"></i></template>
+						</MkInput>
+
+						<MkInput v-model="infoForm.state.repositoryUrl" type="url">
+							<template #label>{{ i18n.ts.repositoryUrl }}<span v-if="infoForm.modifiedStates.repositoryUrl" class="_modified">{{ i18n.ts.modified }}</span></template>
+							<template #caption>{{ i18n.ts.repositoryUrlDescription }}</template>
+							<template #prefix><i class="ti ti-link"></i></template>
+						</MkInput>
+
+						<MkInfo v-if="!instance.providesTarball && !infoForm.state.repositoryUrl" warn>
+							{{ i18n.ts.repositoryUrlOrTarballRequired }}
+						</MkInfo>
+
+						<MkInput v-model="infoForm.state.impressumUrl" type="url">
+							<template #label>{{ i18n.ts.impressumUrl }}<span v-if="infoForm.modifiedStates.impressumUrl" class="_modified">{{ i18n.ts.modified }}</span></template>
+							<template #caption>{{ i18n.ts.impressumDescription }}</template>
+							<template #prefix><i class="ti ti-link"></i></template>
+						</MkInput>
+					</div>
+				</MkFolder>
+
+				<MkFolder>
+					<template #icon><i class="ti ti-user-star"></i></template>
+					<template #label>{{ i18n.ts.pinnedUsers }}</template>
+					<template v-if="pinnedUsersForm.modified.value" #footer>
+						<MkFormFooter :form="pinnedUsersForm"/>
+					</template>
+
+					<MkTextarea v-model="pinnedUsersForm.state.pinnedUsers">
+						<template #label>{{ i18n.ts.pinnedUsers }}<span v-if="pinnedUsersForm.modifiedStates.pinnedUsers" class="_modified">{{ i18n.ts.modified }}</span></template>
 						<template #caption>{{ i18n.ts.pinnedUsersDescription }}</template>
 					</MkTextarea>
+				</MkFolder>
 
-					<FormSection>
-						<template #label>{{ i18n.ts.files }}</template>
+				<MkFolder>
+					<template #icon><i class="ti ti-cloud"></i></template>
+					<template #label>{{ i18n.ts.files }}</template>
+					<template v-if="filesForm.modified.value" #footer>
+						<MkFormFooter :form="filesForm"/>
+					</template>
 
-						<div class="_gaps_m">
-							<MkSwitch v-model="cacheRemoteFiles">
-								<template #label>{{ i18n.ts.cacheRemoteFiles }}</template>
-								<template #caption>{{ i18n.ts.cacheRemoteFilesDescription }}{{ i18n.ts.youCanCleanRemoteFilesCache }}</template>
+					<div class="_gaps">
+						<MkSwitch v-model="filesForm.state.cacheRemoteFiles">
+							<template #label>{{ i18n.ts.cacheRemoteFiles }}<span v-if="filesForm.modifiedStates.cacheRemoteFiles" class="_modified">{{ i18n.ts.modified }}</span></template>
+							<template #caption>{{ i18n.ts.cacheRemoteFilesDescription }}{{ i18n.ts.youCanCleanRemoteFilesCache }}</template>
+						</MkSwitch>
+
+						<template v-if="filesForm.state.cacheRemoteFiles">
+							<MkSwitch v-model="filesForm.state.cacheRemoteSensitiveFiles">
+								<template #label>{{ i18n.ts.cacheRemoteSensitiveFiles }}<span v-if="filesForm.modifiedStates.cacheRemoteSensitiveFiles" class="_modified">{{ i18n.ts.modified }}</span></template>
+								<template #caption>{{ i18n.ts.cacheRemoteSensitiveFilesDescription }}</template>
 							</MkSwitch>
+						</template>
+					</div>
+				</MkFolder>
 
-							<template v-if="cacheRemoteFiles">
-								<MkSwitch v-model="cacheRemoteSensitiveFiles">
-									<template #label>{{ i18n.ts.cacheRemoteSensitiveFiles }}</template>
-									<template #caption>{{ i18n.ts.cacheRemoteSensitiveFilesDescription }}</template>
-								</MkSwitch>
-							</template>
-						</div>
-					</FormSection>
+				<MkFolder>
+					<template #icon><i class="ti ti-world-cog"></i></template>
+					<template #label>ServiceWorker</template>
+					<template v-if="serviceWorkerForm.modified.value" #footer>
+						<MkFormFooter :form="serviceWorkerForm"/>
+					</template>
 
-					<FormSection>
-						<template #label>ServiceWorker</template>
+					<div class="_gaps">
+						<MkSwitch v-model="serviceWorkerForm.state.enableServiceWorker">
+							<template #label>{{ i18n.ts.enableServiceworker }}<span v-if="serviceWorkerForm.modifiedStates.enableServiceWorker" class="_modified">{{ i18n.ts.modified }}</span></template>
+							<template #caption>{{ i18n.ts.serviceworkerInfo }}</template>
+						</MkSwitch>
 
-						<div class="_gaps_m">
-							<MkSwitch v-model="enableServiceWorker">
-								<template #label>{{ i18n.ts.enableServiceworker }}</template>
-								<template #caption>{{ i18n.ts.serviceworkerInfo }}</template>
-							</MkSwitch>
-
-							<template v-if="enableServiceWorker">
-								<MkInput v-model="swPublicKey">
-									<template #prefix><i class="ti ti-key"></i></template>
-									<template #label>Public key</template>
-								</MkInput>
-
-								<MkInput v-model="swPrivateKey">
-									<template #prefix><i class="ti ti-key"></i></template>
-									<template #label>Private key</template>
-								</MkInput>
-							</template>
-						</div>
-					</FormSection>
-
-					<FormSection>
-						<template #label>Misskey® Fan-out Timeline Technology™ (FTT)</template>
-
-						<div class="_gaps_m">
-							<MkSwitch v-model="enableFanoutTimeline">
-								<template #label>{{ i18n.ts.enable }}</template>
-								<template #caption>{{ i18n.ts._serverSettings.fanoutTimelineDescription }}</template>
-							</MkSwitch>
-
-							<MkSwitch v-model="enableFanoutTimelineDbFallback">
-								<template #label>{{ i18n.ts._serverSettings.fanoutTimelineDbFallback }}</template>
-								<template #caption>{{ i18n.ts._serverSettings.fanoutTimelineDbFallbackDescription }}</template>
-							</MkSwitch>
-
-							<MkInput v-model="perLocalUserUserTimelineCacheMax" type="number">
-								<template #label>perLocalUserUserTimelineCacheMax</template>
+						<template v-if="serviceWorkerForm.state.enableServiceWorker">
+							<MkInput v-model="serviceWorkerForm.state.swPublicKey">
+								<template #label>Public key<span v-if="serviceWorkerForm.modifiedStates.swPublicKey" class="_modified">{{ i18n.ts.modified }}</span></template>
+								<template #prefix><i class="ti ti-key"></i></template>
 							</MkInput>
 
-							<MkInput v-model="perRemoteUserUserTimelineCacheMax" type="number">
-								<template #label>perRemoteUserUserTimelineCacheMax</template>
+							<MkInput v-model="serviceWorkerForm.state.swPrivateKey">
+								<template #label>Private key<span v-if="serviceWorkerForm.modifiedStates.swPrivateKey" class="_modified">{{ i18n.ts.modified }}</span></template>
+								<template #prefix><i class="ti ti-key"></i></template>
 							</MkInput>
+						</template>
+					</div>
+				</MkFolder>
 
-							<MkInput v-model="perUserHomeTimelineCacheMax" type="number">
-								<template #label>perUserHomeTimelineCacheMax</template>
-							</MkInput>
+				<MkFolder>
+					<template #icon><i class="ti ti-ad"></i></template>
+					<template #label>{{ i18n.ts._ad.adsSettings }}</template>
+					<template v-if="adForm.modified.value" #footer>
+						<MkFormFooter :form="adForm"/>
+					</template>
 
-							<MkInput v-model="perUserListTimelineCacheMax" type="number">
-								<template #label>perUserListTimelineCacheMax</template>
+					<div class="_gaps">
+						<div class="_gaps_s">
+							<MkInput v-model="adForm.state.notesPerOneAd" :min="0" type="number">
+								<template #label>{{ i18n.ts._ad.notesPerOneAd }}<span v-if="adForm.modifiedStates.notesPerOneAd" class="_modified">{{ i18n.ts.modified }}</span></template>
+								<template #caption>{{ i18n.ts._ad.setZeroToDisable }}</template>
 							</MkInput>
+							<MkInfo v-if="adForm.state.notesPerOneAd > 0 && adForm.state.notesPerOneAd < 20" :warn="true">
+								{{ i18n.ts._ad.adsTooClose }}
+							</MkInfo>
 						</div>
-					</FormSection>
+					</div>
+				</MkFolder>
 
-					<FormSection>
-						<template #label>{{ i18n.ts._ad.adsSettings }}</template>
+				<MkFolder>
+					<template #icon><i class="ti ti-world-search"></i></template>
+					<template #label>{{ i18n.ts._urlPreviewSetting.title }}</template>
+					<template v-if="urlPreviewForm.modified.value" #footer>
+						<MkFormFooter :form="urlPreviewForm"/>
+					</template>
 
-						<div class="_gaps_m">
-							<div class="_gaps_s">
-								<MkInput v-model="notesPerOneAd" :min="0" type="number">
-									<template #label>{{ i18n.ts._ad.notesPerOneAd }}</template>
-									<template #caption>{{ i18n.ts._ad.setZeroToDisable }}</template>
-								</MkInput>
-								<MkInfo v-if="notesPerOneAd > 0 && notesPerOneAd < 20" :warn="true">
-									{{ i18n.ts._ad.adsTooClose }}
-								</MkInfo>
-							</div>
-						</div>
-					</FormSection>
+					<div class="_gaps">
+						<MkSwitch v-model="urlPreviewForm.state.urlPreviewEnabled">
+							<template #label>{{ i18n.ts._urlPreviewSetting.enable }}<span v-if="urlPreviewForm.modifiedStates.urlPreviewEnabled" class="_modified">{{ i18n.ts.modified }}</span></template>
+						</MkSwitch>
 
-					<FormSection>
-						<template #label>{{ i18n.ts._urlPreviewSetting.title }}</template>
-
-						<div class="_gaps_m">
-							<MkSwitch v-model="urlPreviewEnabled">
-								<template #label>{{ i18n.ts._urlPreviewSetting.enable }}</template>
-							</MkSwitch>
-
-							<MkSwitch v-model="urlPreviewRequireContentLength">
-								<template #label>{{ i18n.ts._urlPreviewSetting.requireContentLength }}</template>
+						<template v-if="urlPreviewForm.state.urlPreviewEnabled">
+							<MkSwitch v-model="urlPreviewForm.state.urlPreviewRequireContentLength">
+								<template #label>{{ i18n.ts._urlPreviewSetting.requireContentLength }}<span v-if="urlPreviewForm.modifiedStates.urlPreviewRequireContentLength" class="_modified">{{ i18n.ts.modified }}</span></template>
 								<template #caption>{{ i18n.ts._urlPreviewSetting.requireContentLengthDescription }}</template>
 							</MkSwitch>
 
-							<MkInput v-model="urlPreviewMaximumContentLength" type="number">
-								<template #label>{{ i18n.ts._urlPreviewSetting.maximumContentLength }}</template>
+							<MkInput v-model="urlPreviewForm.state.urlPreviewMaximumContentLength" type="number">
+								<template #label>{{ i18n.ts._urlPreviewSetting.maximumContentLength }}<span v-if="urlPreviewForm.modifiedStates.urlPreviewMaximumContentLength" class="_modified">{{ i18n.ts.modified }}</span></template>
 								<template #caption>{{ i18n.ts._urlPreviewSetting.maximumContentLengthDescription }}</template>
 							</MkInput>
 
-							<MkInput v-model="urlPreviewTimeout" type="number">
-								<template #label>{{ i18n.ts._urlPreviewSetting.timeout }}</template>
+							<MkInput v-model="urlPreviewForm.state.urlPreviewTimeout" type="number">
+								<template #label>{{ i18n.ts._urlPreviewSetting.timeout }}<span v-if="urlPreviewForm.modifiedStates.urlPreviewTimeout" class="_modified">{{ i18n.ts.modified }}</span></template>
 								<template #caption>{{ i18n.ts._urlPreviewSetting.timeoutDescription }}</template>
 							</MkInput>
 
-							<MkInput v-model="urlPreviewUserAgent" type="text">
-								<template #label>{{ i18n.ts._urlPreviewSetting.userAgent }}</template>
+							<MkInput v-model="urlPreviewForm.state.urlPreviewUserAgent" type="text">
+								<template #label>{{ i18n.ts._urlPreviewSetting.userAgent }}<span v-if="urlPreviewForm.modifiedStates.urlPreviewUserAgent" class="_modified">{{ i18n.ts.modified }}</span></template>
 								<template #caption>{{ i18n.ts._urlPreviewSetting.userAgentDescription }}</template>
 							</MkInput>
 
 							<div>
-								<MkInput v-model="urlPreviewSummaryProxyUrl" type="text">
-									<template #label>{{ i18n.ts._urlPreviewSetting.summaryProxy }}</template>
+								<MkInput v-model="urlPreviewForm.state.urlPreviewSummaryProxyUrl" type="text">
+									<template #label>{{ i18n.ts._urlPreviewSetting.summaryProxy }}<span v-if="urlPreviewForm.modifiedStates.urlPreviewSummaryProxyUrl" class="_modified">{{ i18n.ts.modified }}</span></template>
 									<template #caption>[{{ i18n.ts.notUsePleaseLeaveBlank }}] {{ i18n.ts._urlPreviewSetting.summaryProxyDescription }}</template>
 								</MkInput>
 
@@ -188,18 +206,51 @@ SPDX-License-Identifier: AGPL-3.0-only
 									</ul>
 								</div>
 							</div>
-						</div>
-					</FormSection>
-				</div>
-			</FormSuspense>
-		</MkSpacer>
-		<template #footer>
-			<div :class="$style.footer">
-				<MkSpacer :contentMax="700" :marginMin="16" :marginMax="16">
-					<MkButton primary rounded @click="save"><i class="ti ti-check"></i> {{ i18n.ts.save }}</MkButton>
-				</MkSpacer>
+						</template>
+					</div>
+				</MkFolder>
+
+				<MkFolder>
+					<template #icon><i class="ti ti-planet"></i></template>
+					<template #label>{{ i18n.ts.federation }}</template>
+					<template v-if="federationForm.savedState.federation === 'all'" #suffix>{{ i18n.ts.all }}</template>
+					<template v-else-if="federationForm.savedState.federation === 'specified'" #suffix>{{ i18n.ts.specifyHost }}</template>
+					<template v-else-if="federationForm.savedState.federation === 'none'" #suffix>{{ i18n.ts.none }}</template>
+					<template v-if="federationForm.modified.value" #footer>
+						<MkFormFooter :form="federationForm"/>
+					</template>
+
+					<div class="_gaps">
+						<MkRadios v-model="federationForm.state.federation">
+							<template #label>{{ i18n.ts.behavior }}<span v-if="federationForm.modifiedStates.federation" class="_modified">{{ i18n.ts.modified }}</span></template>
+							<option value="all">{{ i18n.ts.all }}</option>
+							<option value="specified">{{ i18n.ts.specifyHost }}</option>
+							<option value="none">{{ i18n.ts.none }}</option>
+						</MkRadios>
+
+						<MkTextarea v-if="federationForm.state.federation === 'specified'" v-model="federationForm.state.federationHosts">
+							<template #label>{{ i18n.ts.federationAllowedHosts }}<span v-if="federationForm.modifiedStates.federationHosts" class="_modified">{{ i18n.ts.modified }}</span></template>
+							<template #caption>{{ i18n.ts.federationAllowedHostsDescription }}</template>
+						</MkTextarea>
+					</div>
+				</MkFolder>
+
+				<MkFolder>
+					<template #icon><i class="ti ti-ghost"></i></template>
+					<template #label>{{ i18n.ts.proxyAccount }}</template>
+
+					<div class="_gaps">
+						<MkInfo>{{ i18n.ts.proxyAccountDescription }}</MkInfo>
+						<MkKeyValue>
+							<template #key>{{ i18n.ts.proxyAccount }}</template>
+							<template #value>{{ proxyAccount ? `@${proxyAccount.username}` : i18n.ts.none }}</template>
+						</MkKeyValue>
+
+						<MkButton primary @click="chooseProxyAccount">{{ i18n.ts.selectAccount }}</MkButton>
+					</div>
+				</MkFolder>
 			</div>
-		</template>
+		</MkSpacer>
 	</MkStickyContainer>
 </div>
 </template>
@@ -211,9 +262,7 @@ import MkSwitch from '@/components/MkSwitch.vue';
 import MkInput from '@/components/MkInput.vue';
 import MkTextarea from '@/components/MkTextarea.vue';
 import MkInfo from '@/components/MkInfo.vue';
-import FormSection from '@/components/form/section.vue';
 import FormSplit from '@/components/form/split.vue';
-import FormSuspense from '@/components/form/suspense.vue';
 import * as os from '@/os.js';
 import { misskeyApi } from '@/scripts/misskey-api.js';
 import { fetchInstance, instance } from '@/instance.js';
@@ -221,96 +270,123 @@ import { i18n } from '@/i18n.js';
 import { definePageMetadata } from '@/scripts/page-metadata.js';
 import MkButton from '@/components/MkButton.vue';
 import MkFolder from '@/components/MkFolder.vue';
-import MkSelect from '@/components/MkSelect.vue';
+import MkKeyValue from '@/components/MkKeyValue.vue';
+import { useForm } from '@/scripts/use-form.js';
+import MkFormFooter from '@/components/MkFormFooter.vue';
+import MkRadios from '@/components/MkRadios.vue';
 
-const name = ref<string | null>(null);
-const shortName = ref<string | null>(null);
-const description = ref<string | null>(null);
-const maintainerName = ref<string | null>(null);
-const maintainerEmail = ref<string | null>(null);
-const repositoryUrl = ref<string | null>(null);
-const impressumUrl = ref<string | null>(null);
-const pinnedUsers = ref<string>('');
-const cacheRemoteFiles = ref<boolean>(false);
-const cacheRemoteSensitiveFiles = ref<boolean>(false);
-const enableServiceWorker = ref<boolean>(false);
-const swPublicKey = ref<string | null>(null);
-const swPrivateKey = ref<string | null>(null);
-const enableFanoutTimeline = ref<boolean>(false);
-const enableFanoutTimelineDbFallback = ref<boolean>(false);
-const perLocalUserUserTimelineCacheMax = ref<number>(0);
-const perRemoteUserUserTimelineCacheMax = ref<number>(0);
-const perUserHomeTimelineCacheMax = ref<number>(0);
-const perUserListTimelineCacheMax = ref<number>(0);
-const notesPerOneAd = ref<number>(0);
-const urlPreviewEnabled = ref<boolean>(true);
-const urlPreviewTimeout = ref<number>(10000);
-const urlPreviewMaximumContentLength = ref<number>(1024 * 1024 * 10);
-const urlPreviewRequireContentLength = ref<boolean>(true);
-const urlPreviewUserAgent = ref<string | null>(null);
-const urlPreviewSummaryProxyUrl = ref<string | null>(null);
+const meta = await misskeyApi('admin/meta');
 
-async function init(): Promise<void> {
-	const meta = await misskeyApi('admin/meta');
-	name.value = meta.name;
-	shortName.value = meta.shortName;
-	description.value = meta.description;
-	maintainerName.value = meta.maintainerName;
-	maintainerEmail.value = meta.maintainerEmail;
-	repositoryUrl.value = meta.repositoryUrl;
-	impressumUrl.value = meta.impressumUrl;
-	pinnedUsers.value = meta.pinnedUsers.join('\n');
-	cacheRemoteFiles.value = meta.cacheRemoteFiles;
-	cacheRemoteSensitiveFiles.value = meta.cacheRemoteSensitiveFiles;
-	enableServiceWorker.value = meta.enableServiceWorker;
-	swPublicKey.value = meta.swPublickey;
-	swPrivateKey.value = meta.swPrivateKey;
-	enableFanoutTimeline.value = meta.enableFanoutTimeline;
-	enableFanoutTimelineDbFallback.value = meta.enableFanoutTimelineDbFallback;
-	perLocalUserUserTimelineCacheMax.value = meta.perLocalUserUserTimelineCacheMax;
-	perRemoteUserUserTimelineCacheMax.value = meta.perRemoteUserUserTimelineCacheMax;
-	perUserHomeTimelineCacheMax.value = meta.perUserHomeTimelineCacheMax;
-	perUserListTimelineCacheMax.value = meta.perUserListTimelineCacheMax;
-	notesPerOneAd.value = meta.notesPerOneAd;
-	urlPreviewEnabled.value = meta.urlPreviewEnabled;
-	urlPreviewTimeout.value = meta.urlPreviewTimeout;
-	urlPreviewMaximumContentLength.value = meta.urlPreviewMaximumContentLength;
-	urlPreviewRequireContentLength.value = meta.urlPreviewRequireContentLength;
-	urlPreviewUserAgent.value = meta.urlPreviewUserAgent;
-	urlPreviewSummaryProxyUrl.value = meta.urlPreviewSummaryProxyUrl;
-}
+const proxyAccount = ref(meta.proxyAccountId ? await misskeyApi('users/show', { userId: meta.proxyAccountId }) : null);
 
-async function save() {
+const infoForm = useForm({
+	name: meta.name ?? '',
+	shortName: meta.shortName ?? '',
+	description: meta.description ?? '',
+	maintainerName: meta.maintainerName ?? '',
+	maintainerEmail: meta.maintainerEmail ?? '',
+	tosUrl: meta.tosUrl ?? '',
+	privacyPolicyUrl: meta.privacyPolicyUrl ?? '',
+	inquiryUrl: meta.inquiryUrl ?? '',
+	repositoryUrl: meta.repositoryUrl ?? '',
+	impressumUrl: meta.impressumUrl ?? '',
+}, async (state) => {
 	await os.apiWithDialog('admin/update-meta', {
-		name: name.value,
-		shortName: shortName.value === '' ? null : shortName.value,
-		description: description.value,
-		maintainerName: maintainerName.value,
-		maintainerEmail: maintainerEmail.value,
-		repositoryUrl: repositoryUrl.value,
-		impressumUrl: impressumUrl.value,
-		pinnedUsers: pinnedUsers.value.split('\n'),
-		cacheRemoteFiles: cacheRemoteFiles.value,
-		cacheRemoteSensitiveFiles: cacheRemoteSensitiveFiles.value,
-		enableServiceWorker: enableServiceWorker.value,
-		swPublicKey: swPublicKey.value,
-		swPrivateKey: swPrivateKey.value,
-		enableFanoutTimeline: enableFanoutTimeline.value,
-		enableFanoutTimelineDbFallback: enableFanoutTimelineDbFallback.value,
-		perLocalUserUserTimelineCacheMax: perLocalUserUserTimelineCacheMax.value,
-		perRemoteUserUserTimelineCacheMax: perRemoteUserUserTimelineCacheMax.value,
-		perUserHomeTimelineCacheMax: perUserHomeTimelineCacheMax.value,
-		perUserListTimelineCacheMax: perUserListTimelineCacheMax.value,
-		notesPerOneAd: notesPerOneAd.value,
-		urlPreviewEnabled: urlPreviewEnabled.value,
-		urlPreviewTimeout: urlPreviewTimeout.value,
-		urlPreviewMaximumContentLength: urlPreviewMaximumContentLength.value,
-		urlPreviewRequireContentLength: urlPreviewRequireContentLength.value,
-		urlPreviewUserAgent: urlPreviewUserAgent.value,
-		urlPreviewSummaryProxyUrl: urlPreviewSummaryProxyUrl.value,
+		name: state.name,
+		shortName: state.shortName === '' ? null : state.shortName,
+		description: state.description,
+		maintainerName: state.maintainerName,
+		maintainerEmail: state.maintainerEmail,
+		tosUrl: state.tosUrl,
+		privacyPolicyUrl: state.privacyPolicyUrl,
+		inquiryUrl: state.inquiryUrl,
+		repositoryUrl: state.repositoryUrl,
+		impressumUrl: state.impressumUrl,
 	});
-
 	fetchInstance(true);
+});
+
+const pinnedUsersForm = useForm({
+	pinnedUsers: meta.pinnedUsers.join('\n'),
+}, async (state) => {
+	await os.apiWithDialog('admin/update-meta', {
+		pinnedUsers: state.pinnedUsers.split('\n'),
+	});
+	fetchInstance(true);
+});
+
+const filesForm = useForm({
+	cacheRemoteFiles: meta.cacheRemoteFiles,
+	cacheRemoteSensitiveFiles: meta.cacheRemoteSensitiveFiles,
+}, async (state) => {
+	await os.apiWithDialog('admin/update-meta', {
+		cacheRemoteFiles: state.cacheRemoteFiles,
+		cacheRemoteSensitiveFiles: state.cacheRemoteSensitiveFiles,
+	});
+	fetchInstance(true);
+});
+
+const serviceWorkerForm = useForm({
+	enableServiceWorker: meta.enableServiceWorker,
+	swPublicKey: meta.swPublickey ?? '',
+	swPrivateKey: meta.swPrivateKey ?? '',
+}, async (state) => {
+	await os.apiWithDialog('admin/update-meta', {
+		enableServiceWorker: state.enableServiceWorker,
+		swPublicKey: state.swPublicKey,
+		swPrivateKey: state.swPrivateKey,
+	});
+	fetchInstance(true);
+});
+
+const adForm = useForm({
+	notesPerOneAd: meta.notesPerOneAd,
+}, async (state) => {
+	await os.apiWithDialog('admin/update-meta', {
+		notesPerOneAd: state.notesPerOneAd,
+	});
+	fetchInstance(true);
+});
+
+const urlPreviewForm = useForm({
+	urlPreviewEnabled: meta.urlPreviewEnabled,
+	urlPreviewTimeout: meta.urlPreviewTimeout,
+	urlPreviewMaximumContentLength: meta.urlPreviewMaximumContentLength,
+	urlPreviewRequireContentLength: meta.urlPreviewRequireContentLength,
+	urlPreviewUserAgent: meta.urlPreviewUserAgent ?? '',
+	urlPreviewSummaryProxyUrl: meta.urlPreviewSummaryProxyUrl ?? '',
+}, async (state) => {
+	await os.apiWithDialog('admin/update-meta', {
+		urlPreviewEnabled: state.urlPreviewEnabled,
+		urlPreviewTimeout: state.urlPreviewTimeout,
+		urlPreviewMaximumContentLength: state.urlPreviewMaximumContentLength,
+		urlPreviewRequireContentLength: state.urlPreviewRequireContentLength,
+		urlPreviewUserAgent: state.urlPreviewUserAgent,
+		urlPreviewSummaryProxyUrl: state.urlPreviewSummaryProxyUrl,
+	});
+	fetchInstance(true);
+});
+
+const federationForm = useForm({
+	federation: meta.federation,
+	federationHosts: meta.federationHosts.join('\n'),
+}, async (state) => {
+	await os.apiWithDialog('admin/update-meta', {
+		federation: state.federation,
+		federationHosts: state.federationHosts.split('\n'),
+	});
+	fetchInstance(true);
+});
+
+function chooseProxyAccount() {
+	os.selectUser({ localOnly: true }).then(user => {
+		proxyAccount.value = user;
+		os.apiWithDialog('admin/update-meta', {
+			proxyAccountId: user.id,
+		}).then(() => {
+			fetchInstance(true);
+		});
+	});
 }
 
 const headerTabs = computed(() => []);
@@ -322,13 +398,8 @@ definePageMetadata(() => ({
 </script>
 
 <style lang="scss" module>
-.footer {
-	-webkit-backdrop-filter: var(--blur, blur(15px));
-	backdrop-filter: var(--blur, blur(15px));
-}
-
 .subCaption {
 	font-size: 0.85em;
-	color: var(--fgTransparentWeak);
+	color: var(--MI_THEME-fgTransparentWeak);
 }
 </style>
diff --git a/packages/frontend/src/pages/admin/system-webhook.item.vue b/packages/frontend/src/pages/admin/system-webhook.item.vue
new file mode 100644
index 0000000000..45f0fff107
--- /dev/null
+++ b/packages/frontend/src/pages/admin/system-webhook.item.vue
@@ -0,0 +1,80 @@
+<!--
+SPDX-FileCopyrightText: syuilo and misskey-project
+SPDX-License-Identifier: AGPL-3.0-only
+-->
+
+<template>
+<MkFolder>
+	<template #label>{{ entity.name || entity.url }}</template>
+	<template v-if="entity.name != null && entity.name != ''" #caption>{{ entity.url }}</template>
+	<template #icon>
+		<i v-if="!entity.isActive" class="ti ti-player-pause"/>
+		<i v-else-if="entity.latestStatus === null" class="ti ti-circle"/>
+		<i
+			v-else-if="[200, 201, 204].includes(entity.latestStatus)"
+			class="ti ti-check"
+			:style="{ color: 'var(--MI_THEME-success)' }"
+		/>
+		<i v-else class="ti ti-alert-triangle" :style="{ color: 'var(--MI_THEME-error)' }"/>
+	</template>
+	<template #suffix>
+		<MkTime v-if="entity.latestSentAt" :time="entity.latestSentAt" style="margin-right: 8px"/>
+		<span v-else>-</span>
+	</template>
+	<template #footer>
+		<div class="_buttons">
+			<MkButton @click="onEditClick">
+				<i class="ti ti-settings"></i> {{ i18n.ts.edit }}
+			</MkButton>
+			<MkButton danger @click="onDeleteClick">
+				<i class="ti ti-trash"></i> {{ i18n.ts.delete }}
+			</MkButton>
+		</div>
+	</template>
+
+	<div class="_gaps">
+		<MkKeyValue>
+			<template #key>latestStatus</template>
+			<template #value>{{ entity.latestStatus ?? '-' }}</template>
+		</MkKeyValue>
+	</div>
+</MkFolder>
+</template>
+
+<script lang="ts" setup>
+import { entities } from 'misskey-js';
+import { toRefs } from 'vue';
+import MkFolder from '@/components/MkFolder.vue';
+import { i18n } from '@/i18n.js';
+import MkButton from '@/components/MkButton.vue';
+import MkKeyValue from '@/components/MkKeyValue.vue';
+
+const emit = defineEmits<{
+	(ev: 'edit', value: entities.SystemWebhook): void;
+	(ev: 'delete', value: entities.SystemWebhook): void;
+}>();
+
+const props = defineProps<{
+	entity: entities.SystemWebhook;
+}>();
+
+const { entity } = toRefs(props);
+
+function onEditClick() {
+	emit('edit', entity.value);
+}
+
+function onDeleteClick() {
+	emit('delete', entity.value);
+}
+
+</script>
+
+<style module lang="scss">
+.icon {
+	margin-right: 0.75em;
+	flex-shrink: 0;
+	text-align: center;
+	color: var(--MI_THEME-fgTransparentWeak);
+}
+</style>
diff --git a/packages/frontend/src/pages/admin/system-webhook.vue b/packages/frontend/src/pages/admin/system-webhook.vue
new file mode 100644
index 0000000000..c59abda24a
--- /dev/null
+++ b/packages/frontend/src/pages/admin/system-webhook.vue
@@ -0,0 +1,93 @@
+<!--
+SPDX-FileCopyrightText: syuilo and misskey-project
+SPDX-License-Identifier: AGPL-3.0-only
+-->
+
+<template>
+<MkStickyContainer>
+	<template #header>
+		<XHeader :actions="headerActions" :tabs="headerTabs"/>
+	</template>
+
+	<MkSpacer :contentMax="900">
+		<div class="_gaps_m">
+			<MkButton primary @click="onCreateWebhookClicked">
+				<i class="ti ti-plus"></i> {{ i18n.ts._webhookSettings.createWebhook }}
+			</MkButton>
+
+			<FormSection>
+				<div class="_gaps">
+					<XItem v-for="item in webhooks" :key="item.id" :entity="item" @edit="onEditButtonClicked" @delete="onDeleteButtonClicked"/>
+				</div>
+			</FormSection>
+		</div>
+	</MkSpacer>
+</MkStickyContainer>
+</template>
+
+<script lang="ts" setup>
+import { computed, onMounted, ref } from 'vue';
+import { entities } from 'misskey-js';
+import XItem from './system-webhook.item.vue';
+import FormSection from '@/components/form/section.vue';
+import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { i18n } from '@/i18n.js';
+import XHeader from '@/pages/admin/_header_.vue';
+import MkButton from '@/components/MkButton.vue';
+import { misskeyApi } from '@/scripts/misskey-api.js';
+import { showSystemWebhookEditorDialog } from '@/components/MkSystemWebhookEditor.impl.js';
+import * as os from '@/os.js';
+
+const webhooks = ref<entities.SystemWebhook[]>([]);
+
+const headerActions = computed(() => []);
+const headerTabs = computed(() => []);
+
+async function onCreateWebhookClicked() {
+	await showSystemWebhookEditorDialog({
+		mode: 'create',
+	});
+
+	await fetchWebhooks();
+}
+
+async function onEditButtonClicked(webhook: entities.SystemWebhook) {
+	await showSystemWebhookEditorDialog({
+		mode: 'edit',
+		id: webhook.id,
+	});
+
+	await fetchWebhooks();
+}
+
+async function onDeleteButtonClicked(webhook: entities.SystemWebhook) {
+	const result = await os.confirm({
+		type: 'warning',
+		title: i18n.ts._webhookSettings.deleteConfirm,
+	});
+	if (!result.canceled) {
+		await misskeyApi('admin/system-webhook/delete', {
+			id: webhook.id,
+		});
+		await fetchWebhooks();
+	}
+}
+
+async function fetchWebhooks() {
+	const result = await misskeyApi('admin/system-webhook/list', {});
+	webhooks.value = result.sort((a, b) => a.id.localeCompare(b.id));
+}
+
+onMounted(async () => {
+	await fetchWebhooks();
+});
+
+definePageMetadata(() => ({
+	title: 'SystemWebhook',
+	icon: 'ti ti-webhook',
+}));
+</script>
+
+<style module lang="scss">
+
+</style>
diff --git a/packages/frontend/src/pages/admin/users.vue b/packages/frontend/src/pages/admin/users.vue
index 7d87b97a36..870c3ce88b 100644
--- a/packages/frontend/src/pages/admin/users.vue
+++ b/packages/frontend/src/pages/admin/users.vue
@@ -33,11 +33,11 @@ SPDX-License-Identifier: AGPL-3.0-only
 					</MkSelect>
 				</div>
 				<div :class="$style.inputs">
-					<MkInput v-model="searchUsername" style="flex: 1;" type="text" :spellcheck="false" @update:modelValue="$refs.users.reload()">
+					<MkInput v-model="searchUsername" style="flex: 1;" type="text" :spellcheck="false">
 						<template #prefix>@</template>
 						<template #label>{{ i18n.ts.username }}</template>
 					</MkInput>
-					<MkInput v-model="searchHost" style="flex: 1;" type="text" :spellcheck="false" :disabled="pagination.params.origin === 'local'" @update:modelValue="$refs.users.reload()">
+					<MkInput v-model="searchHost" style="flex: 1;" type="text" :spellcheck="false" :disabled="pagination.params.origin === 'local'">
 						<template #prefix>@</template>
 						<template #label>{{ i18n.ts.host }}</template>
 					</MkInput>
@@ -99,19 +99,19 @@ async function addUser() {
 	const { canceled: canceled1, result: username } = await os.inputText({
 		title: i18n.ts.username,
 	});
-	if (canceled1) return;
+	if (canceled1 || username == null) return;
 
 	const { canceled: canceled2, result: password } = await os.inputText({
 		title: i18n.ts.password,
 		type: 'password',
 	});
-	if (canceled2) return;
+	if (canceled2 || password == null) return;
 
 	os.apiWithDialog('admin/accounts/create', {
 		username: username,
 		password: password,
 	}).then(res => {
-		paginationComponent.value.reload();
+		paginationComponent.value?.reload();
 	});
 }
 
diff --git a/packages/frontend/src/pages/announcement.vue b/packages/frontend/src/pages/announcement.vue
index 85ae9062d4..01c29cf02d 100644
--- a/packages/frontend/src/pages/announcement.vue
+++ b/packages/frontend/src/pages/announcement.vue
@@ -20,9 +20,9 @@ SPDX-License-Identifier: AGPL-3.0-only
 					<span v-if="$i && !announcement.silence && !announcement.isRead" style="margin-right: 0.5em;">🆕</span>
 					<span style="margin-right: 0.5em;">
 						<i v-if="announcement.icon === 'info'" class="ti ti-info-circle"></i>
-						<i v-else-if="announcement.icon === 'warning'" class="ti ti-alert-triangle" style="color: var(--warn);"></i>
-						<i v-else-if="announcement.icon === 'error'" class="ti ti-circle-x" style="color: var(--error);"></i>
-						<i v-else-if="announcement.icon === 'success'" class="ti ti-check" style="color: var(--success);"></i>
+						<i v-else-if="announcement.icon === 'warning'" class="ti ti-alert-triangle" style="color: var(--MI_THEME-warn);"></i>
+						<i v-else-if="announcement.icon === 'error'" class="ti ti-circle-x" style="color: var(--MI_THEME-error);"></i>
+						<i v-else-if="announcement.icon === 'success'" class="ti ti-check" style="color: var(--MI_THEME-success);"></i>
 					</span>
 					<Mfm :text="announcement.title"/>
 				</div>
@@ -55,7 +55,7 @@ import * as os from '@/os.js';
 import { misskeyApi } from '@/scripts/misskey-api.js';
 import { i18n } from '@/i18n.js';
 import { definePageMetadata } from '@/scripts/page-metadata.js';
-import { $i, updateAccount } from '@/account.js';
+import { $i, updateAccountPartial } from '@/account.js';
 import { defaultStore } from '@/store.js';
 
 const props = defineProps<{
@@ -90,7 +90,7 @@ async function read(target: Misskey.entities.Announcement): Promise<void> {
 	target.isRead = true;
 	await misskeyApi('i/read-announcement', { announcementId: target.id });
 	if ($i) {
-		updateAccount({
+		updateAccountPartial({
 			unreadAnnouncements: $i.unreadAnnouncements.filter((a: { id: string; }) => a.id !== target.id),
 		});
 	}
@@ -109,6 +109,15 @@ definePageMetadata(() => ({
 </script>
 
 <style lang="scss" module>
+.fadeEnterActive,
+.fadeLeaveActive {
+	transition: opacity 0.125s ease;
+}
+.fadeEnterFrom,
+.fadeLeaveTo {
+	opacity: 0;
+}
+
 .announcement {
 	padding: 16px;
 }
diff --git a/packages/frontend/src/pages/announcements.vue b/packages/frontend/src/pages/announcements.vue
index e50b208775..75c0fd98dc 100644
--- a/packages/frontend/src/pages/announcements.vue
+++ b/packages/frontend/src/pages/announcements.vue
@@ -17,9 +17,9 @@ SPDX-License-Identifier: AGPL-3.0-only
 							<span v-if="$i && !announcement.silence && !announcement.isRead" style="margin-right: 0.5em;">🆕</span>
 							<span style="margin-right: 0.5em;">
 								<i v-if="announcement.icon === 'info'" class="ti ti-info-circle"></i>
-								<i v-else-if="announcement.icon === 'warning'" class="ti ti-alert-triangle" style="color: var(--warn);"></i>
-								<i v-else-if="announcement.icon === 'error'" class="ti ti-circle-x" style="color: var(--error);"></i>
-								<i v-else-if="announcement.icon === 'success'" class="ti ti-check" style="color: var(--success);"></i>
+								<i v-else-if="announcement.icon === 'warning'" class="ti ti-alert-triangle" style="color: var(--MI_THEME-warn);"></i>
+								<i v-else-if="announcement.icon === 'error'" class="ti ti-circle-x" style="color: var(--MI_THEME-error);"></i>
+								<i v-else-if="announcement.icon === 'success'" class="ti ti-check" style="color: var(--MI_THEME-success);"></i>
 							</span>
 							<MkA :to="`/announcements/${announcement.id}`"><span>{{ announcement.title }}</span></MkA>
 						</div>
@@ -56,7 +56,7 @@ import * as os from '@/os.js';
 import { misskeyApi } from '@/scripts/misskey-api.js';
 import { i18n } from '@/i18n.js';
 import { definePageMetadata } from '@/scripts/page-metadata.js';
-import { $i, updateAccount } from '@/account.js';
+import { $i, updateAccountPartial } from '@/account.js';
 
 const paginationCurrent = {
 	endpoint: 'announcements' as const,
@@ -94,7 +94,7 @@ async function read(target) {
 		return a;
 	});
 	misskeyApi('i/read-announcement', { announcementId: target.id });
-	updateAccount({
+	updateAccountPartial({
 		unreadAnnouncements: $i!.unreadAnnouncements.filter(a => a.id !== target.id),
 	});
 }
diff --git a/packages/frontend/src/pages/antenna-timeline.vue b/packages/frontend/src/pages/antenna-timeline.vue
index 273250d1d0..a01bafd996 100644
--- a/packages/frontend/src/pages/antenna-timeline.vue
+++ b/packages/frontend/src/pages/antenna-timeline.vue
@@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 <MkStickyContainer>
 	<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
 	<MkSpacer :contentMax="800">
-		<div ref="rootEl" v-hotkey.global="keymap">
+		<div ref="rootEl">
 			<div v-if="queue > 0" :class="$style.new"><button class="_buttonPrimary" :class="$style.newButton" @click="top()">{{ i18n.ts.newNoteRecived }}</button></div>
 			<div :class="$style.tl">
 				<MkTimeline
@@ -27,7 +27,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { computed, watch, ref, shallowRef } from 'vue';
 import * as Misskey from 'misskey-js';
 import MkTimeline from '@/components/MkTimeline.vue';
-import { scroll } from '@/scripts/scroll.js';
+import { scroll } from '@@/js/scroll.js';
 import * as os from '@/os.js';
 import { misskeyApi } from '@/scripts/misskey-api.js';
 import { definePageMetadata } from '@/scripts/page-metadata.js';
@@ -44,9 +44,6 @@ const antenna = ref<Misskey.entities.Antenna | null>(null);
 const queue = ref(0);
 const rootEl = shallowRef<HTMLElement>();
 const tlEl = shallowRef<InstanceType<typeof MkTimeline>>();
-const keymap = computed(() => ({
-	't': focus,
-}));
 
 function queueUpdated(q) {
 	queue.value = q;
@@ -100,26 +97,26 @@ definePageMetadata(() => ({
 <style lang="scss" module>
 .new {
 	position: sticky;
-	top: calc(var(--stickyTop, 0px) + 16px);
+	top: calc(var(--MI-stickyTop, 0px) + 16px);
 	z-index: 1000;
 	width: 100%;
 	margin: calc(-0.675em - 8px) 0;
 
 	&:first-child {
-		margin-top: calc(-0.675em - 8px - var(--margin));
+		margin-top: calc(-0.675em - 8px - var(--MI-margin));
 	}
 }
 
 .newButton {
 	display: block;
-	margin: var(--margin) auto 0 auto;
+	margin: var(--MI-margin) auto 0 auto;
 	padding: 8px 16px;
 	border-radius: 32px;
 }
 
 .tl {
-	background: var(--bg);
-	border-radius: var(--radius);
+	background: var(--MI_THEME-bg);
+	border-radius: var(--MI-radius);
 	overflow: clip;
 }
 </style>
diff --git a/packages/frontend/src/pages/avatar-decoration-edit-dialog.vue b/packages/frontend/src/pages/avatar-decoration-edit-dialog.vue
new file mode 100644
index 0000000000..a834f1c5fd
--- /dev/null
+++ b/packages/frontend/src/pages/avatar-decoration-edit-dialog.vue
@@ -0,0 +1,220 @@
+<!--
+SPDX-FileCopyrightText: syuilo and misskey-project
+SPDX-License-Identifier: AGPL-3.0-only
+-->
+
+<template>
+<MkWindow
+	ref="windowEl"
+	:initialWidth="400"
+	:initialHeight="500"
+	:canResize="true"
+	@close="windowEl?.close()"
+	@closed="emit('closed')"
+>
+	<template v-if="avatarDecoration" #header>{{ avatarDecoration.name }}</template>
+	<template v-else #header>New decoration</template>
+
+	<div style="display: flex; flex-direction: column; min-height: 100%;">
+		<MkSpacer :marginMin="20" :marginMax="28" style="flex-grow: 1;">
+			<div class="_gaps_m">
+				<div :class="$style.preview">
+					<div :class="[$style.previewItem, $style.light]">
+						<MkAvatar style="width: 60px; height: 60px;" :user="$i" :decorations="url != '' ? [{ url }] : []" forceShowDecoration/>
+					</div>
+					<div :class="[$style.previewItem, $style.dark]">
+						<MkAvatar style="width: 60px; height: 60px;" :user="$i" :decorations="url != '' ? [{ url }] : []" forceShowDecoration/>
+					</div>
+				</div>
+				<MkInput v-model="name">
+					<template #label>{{ i18n.ts.name }}</template>
+				</MkInput>
+				<MkInput v-model="url">
+					<template #label>{{ i18n.ts.imageUrl }}</template>
+				</MkInput>
+				<MkTextarea v-model="description">
+					<template #label>{{ i18n.ts.description }}</template>
+				</MkTextarea>
+				<MkFolder>
+					<template #label>{{ i18n.ts.availableRoles }}</template>
+					<template #suffix>{{ rolesThatCanBeUsedThisDecoration.length === 0 ? i18n.ts.all : rolesThatCanBeUsedThisDecoration.length }}</template>
+
+					<div class="_gaps">
+						<MkButton rounded @click="addRole"><i class="ti ti-plus"></i> {{ i18n.ts.add }}</MkButton>
+
+						<div v-for="role in rolesThatCanBeUsedThisDecoration" :key="role.id" :class="$style.roleItem">
+							<MkRolePreview :class="$style.role" :role="role" :forModeration="true" :detailed="false" style="pointer-events: none;"/>
+							<button v-if="role.target === 'manual'" class="_button" :class="$style.roleUnassign" @click="removeRole(role, $event)"><i class="ti ti-x"></i></button>
+							<button v-else class="_button" :class="$style.roleUnassign" disabled><i class="ti ti-ban"></i></button>
+						</div>
+					</div>
+				</MkFolder>
+				<MkButton v-if="avatarDecoration" danger @click="del()"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton>
+			</div>
+		</MkSpacer>
+		<div :class="$style.footer">
+			<MkButton primary rounded style="margin: 0 auto;" @click="done"><i class="ti ti-check"></i> {{ props.avatarDecoration ? i18n.ts.update : i18n.ts.create }}</MkButton>
+		</div>
+	</div>
+</MkWindow>
+</template>
+
+<script lang="ts" setup>
+import { computed, watch, ref } from 'vue';
+import * as Misskey from 'misskey-js';
+import MkWindow from '@/components/MkWindow.vue';
+import MkButton from '@/components/MkButton.vue';
+import MkInput from '@/components/MkInput.vue';
+import MkInfo from '@/components/MkInfo.vue';
+import MkFolder from '@/components/MkFolder.vue';
+import * as os from '@/os.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
+import { i18n } from '@/i18n.js';
+import MkSwitch from '@/components/MkSwitch.vue';
+import MkRolePreview from '@/components/MkRolePreview.vue';
+import MkTextarea from '@/components/MkTextarea.vue';
+import { signinRequired } from '@/account.js';
+
+const $i = signinRequired();
+
+const props = defineProps<{
+	avatarDecoration?: any,
+}>();
+
+const emit = defineEmits<{
+	(ev: 'done', v: { deleted?: boolean; updated?: any; created?: any }): void,
+	(ev: 'closed'): void
+}>();
+
+const windowEl = ref<InstanceType<typeof MkWindow> | null>(null);
+const url = ref<string>(props.avatarDecoration ? props.avatarDecoration.url : '');
+const name = ref<string>(props.avatarDecoration ? props.avatarDecoration.name : '');
+const description = ref<string>(props.avatarDecoration ? props.avatarDecoration.description : '');
+const roleIdsThatCanBeUsedThisDecoration = ref(props.avatarDecoration ? props.avatarDecoration.roleIdsThatCanBeUsedThisDecoration : []);
+const rolesThatCanBeUsedThisDecoration = ref<Misskey.entities.Role[]>([]);
+
+watch(roleIdsThatCanBeUsedThisDecoration, async () => {
+	rolesThatCanBeUsedThisDecoration.value = (await Promise.all(roleIdsThatCanBeUsedThisDecoration.value.map((id) => misskeyApi('admin/roles/show', { roleId: id }).catch(() => null)))).filter(x => x != null);
+}, { immediate: true });
+
+async function addRole() {
+	const roles = await misskeyApi('admin/roles/list');
+	const currentRoleIds = rolesThatCanBeUsedThisDecoration.value.map(x => x.id);
+
+	const { canceled, result: role } = await os.select({
+		items: roles.filter(r => r.isPublic).filter(r => !currentRoleIds.includes(r.id)).map(r => ({ text: r.name, value: r })),
+	});
+	if (canceled || role == null) return;
+
+	rolesThatCanBeUsedThisDecoration.value.push(role);
+}
+
+async function removeRole(role, ev) {
+	rolesThatCanBeUsedThisDecoration.value = rolesThatCanBeUsedThisDecoration.value.filter(x => x.id !== role.id);
+}
+
+async function done() {
+	const params = {
+		url: url.value,
+		name: name.value,
+		description: description.value,
+		roleIdsThatCanBeUsedThisDecoration: rolesThatCanBeUsedThisDecoration.value.map(x => x.id),
+	};
+
+	if (props.avatarDecoration) {
+		await os.apiWithDialog('admin/avatar-decorations/update', {
+			id: props.avatarDecoration.id,
+			...params,
+		});
+
+		emit('done', {
+			updated: {
+				id: props.avatarDecoration.id,
+				...params,
+			},
+		});
+
+		windowEl.value?.close();
+	} else {
+		const created = await os.apiWithDialog('admin/avatar-decorations/create', params);
+
+		emit('done', {
+			created: created,
+		});
+
+		windowEl.value?.close();
+	}
+}
+
+async function del() {
+	const { canceled } = await os.confirm({
+		type: 'warning',
+		text: i18n.tsx.removeAreYouSure({ x: name.value }),
+	});
+	if (canceled) return;
+
+	misskeyApi('admin/avatar-decorations/delete', {
+		id: props.avatarDecoration.id,
+	}).then(() => {
+		emit('done', {
+			deleted: true,
+		});
+		windowEl.value?.close();
+	});
+}
+</script>
+
+<style lang="scss" module>
+.preview {
+	display: grid;
+	place-items: center;
+	grid-template-columns: 1fr 1fr;
+	grid-template-rows: 1fr;
+	gap: var(--MI-margin);
+}
+
+.previewItem {
+	width: 100%;
+	height: 100%;
+	min-height: 160px;
+	display: flex;
+	align-items: center;
+	justify-content: center;
+	border-radius: var(--MI-radius);
+
+	&.light {
+		background: #eee;
+	}
+
+	&.dark {
+		background: #222;
+	}
+}
+
+.roleItem {
+	display: flex;
+}
+
+.role {
+	flex: 1;
+}
+
+.roleUnassign {
+	width: 32px;
+	height: 32px;
+	margin-left: 8px;
+	align-self: center;
+}
+
+.footer {
+	position: sticky;
+	z-index: 10000;
+	bottom: 0;
+	left: 0;
+	padding: 12px;
+	border-top: solid 0.5px var(--MI_THEME-divider);
+	background: var(--MI_THEME-acrylicBg);
+	-webkit-backdrop-filter: var(--MI-blur, blur(15px));
+	backdrop-filter: var(--MI-blur, blur(15px));
+}
+</style>
diff --git a/packages/frontend/src/pages/avatar-decorations.vue b/packages/frontend/src/pages/avatar-decorations.vue
index ad9ec3c4ee..a5cafb1678 100644
--- a/packages/frontend/src/pages/avatar-decorations.vue
+++ b/packages/frontend/src/pages/avatar-decorations.vue
@@ -5,78 +5,39 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <template>
 <MkStickyContainer>
-	<template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template>
+	<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
 	<MkSpacer :contentMax="900">
 		<div class="_gaps">
-			<MkFolder v-for="avatarDecoration in avatarDecorations" :key="avatarDecoration.id ?? avatarDecoration._id" :defaultOpen="avatarDecoration.id == null">
-				<template #label>{{ avatarDecoration.name }}</template>
-				<template #caption>{{ avatarDecoration.description }}</template>
-
-				<div class="_gaps_m">
-					<MkInput v-model="avatarDecoration.name">
-						<template #label>{{ i18n.ts.name }}</template>
-					</MkInput>
-					<MkTextarea v-model="avatarDecoration.description">
-						<template #label>{{ i18n.ts.description }}</template>
-					</MkTextarea>
-					<MkInput v-model="avatarDecoration.url">
-						<template #label>{{ i18n.ts.imageUrl }}</template>
-					</MkInput>
-					<div class="buttons _buttons">
-						<MkButton class="button" inline primary @click="save(avatarDecoration)"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton>
-						<MkButton v-if="avatarDecoration.id != null" class="button" inline danger @click="del(avatarDecoration)"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton>
-					</div>
+			<div :class="$style.decorations">
+				<div
+					v-for="avatarDecoration in avatarDecorations"
+					:key="avatarDecoration.id"
+					v-panel
+					:class="$style.decoration"
+					@click="edit(avatarDecoration)"
+				>
+					<div :class="$style.decorationName"><MkCondensedLine :minScale="0.5">{{ avatarDecoration.name }}</MkCondensedLine></div>
+					<MkAvatar style="width: 60px; height: 60px;" :user="$i" :decorations="[{ url: avatarDecoration.url }]" forceShowDecoration/>
 				</div>
-			</MkFolder>
+			</div>
 		</div>
 	</MkSpacer>
 </MkStickyContainer>
 </template>
 
 <script lang="ts" setup>
-import { ref, computed } from 'vue';
+import { ref, computed, defineAsyncComponent } from 'vue';
 import * as Misskey from 'misskey-js';
-import MkButton from '@/components/MkButton.vue';
-import MkInput from '@/components/MkInput.vue';
-import MkTextarea from '@/components/MkTextarea.vue';
+import { signinRequired } from '@/account.js';
 import * as os from '@/os.js';
 import { misskeyApi } from '@/scripts/misskey-api.js';
 import { i18n } from '@/i18n.js';
 import { definePageMetadata } from '@/scripts/page-metadata.js';
-import MkFolder from '@/components/MkFolder.vue';
+
+const $i = signinRequired();
 
 const avatarDecorations = ref<Misskey.entities.AdminAvatarDecorationsListResponse>([]);
 
-function add() {
-	avatarDecorations.value.unshift({
-		_id: Math.random().toString(36),
-		id: null,
-		name: '',
-		description: '',
-		url: '',
-	});
-}
-
-function del(avatarDecoration) {
-	os.confirm({
-		type: 'warning',
-		text: i18n.tsx.deleteAreYouSure({ x: avatarDecoration.name }),
-	}).then(({ canceled }) => {
-		if (canceled) return;
-		avatarDecorations.value = avatarDecorations.value.filter(x => x !== avatarDecoration);
-		misskeyApi('admin/avatar-decorations/delete', avatarDecoration);
-	});
-}
-
-async function save(avatarDecoration) {
-	if (avatarDecoration.id == null) {
-		await os.apiWithDialog('admin/avatar-decorations/create', avatarDecoration);
-		load();
-	} else {
-		os.apiWithDialog('admin/avatar-decorations/update', avatarDecoration);
-	}
-}
-
 function load() {
 	misskeyApi('admin/avatar-decorations/list').then(_avatarDecorations => {
 		avatarDecorations.value = _avatarDecorations;
@@ -85,6 +46,37 @@ function load() {
 
 load();
 
+async function add(ev: MouseEvent) {
+	const { dispose } = os.popup(defineAsyncComponent(() => import('./avatar-decoration-edit-dialog.vue')), {
+	}, {
+		done: result => {
+			if (result.created) {
+				avatarDecorations.value.unshift(result.created);
+			}
+		},
+		closed: () => dispose(),
+	});
+}
+
+function edit(avatarDecoration) {
+	const { dispose } = os.popup(defineAsyncComponent(() => import('./avatar-decoration-edit-dialog.vue')), {
+		avatarDecoration: avatarDecoration,
+	}, {
+		done: result => {
+			if (result.updated) {
+				const index = avatarDecorations.value.findIndex(x => x.id === avatarDecoration.id);
+				avatarDecorations.value[index] = {
+					...avatarDecorations.value[index],
+					...result.updated,
+				};
+			} else if (result.deleted) {
+				avatarDecorations.value = avatarDecorations.value.filter(x => x.id !== avatarDecoration.id);
+			}
+		},
+		closed: () => dispose(),
+	});
+}
+
 const headerActions = computed(() => [{
 	asFullButton: true,
 	icon: 'ti ti-plus',
@@ -99,3 +91,28 @@ definePageMetadata(() => ({
 	icon: 'ti ti-sparkles',
 }));
 </script>
+
+<style lang="scss" module>
+.decorations {
+	display: grid;
+	grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
+	grid-gap: 12px;
+}
+
+.decoration {
+	cursor: pointer;
+	padding: 16px 16px 28px 16px;
+	border-radius: 8px;
+	text-align: center;
+	font-size: 90%;
+	overflow: clip;
+	contain: content;
+}
+
+.decorationName {
+	position: relative;
+	z-index: 10;
+	font-weight: bold;
+	margin-bottom: 20px;
+}
+</style>
diff --git a/packages/frontend/src/pages/channel-editor.vue b/packages/frontend/src/pages/channel-editor.vue
index d3f4a65b89..6d8274a55c 100644
--- a/packages/frontend/src/pages/channel-editor.vue
+++ b/packages/frontend/src/pages/channel-editor.vue
@@ -216,7 +216,7 @@ definePageMetadata(() => ({
 	text-overflow: ellipsis;
 	overflow: hidden;
 	white-space: nowrap;
-	color: var(--navFg);
+	color: var(--MI_THEME-navFg);
 }
 
 .pinnedNoteRemove {
diff --git a/packages/frontend/src/pages/channel.vue b/packages/frontend/src/pages/channel.vue
index a895df76e8..b61054118d 100644
--- a/packages/frontend/src/pages/channel.vue
+++ b/packages/frontend/src/pages/channel.vue
@@ -82,7 +82,7 @@ import { i18n } from '@/i18n.js';
 import { definePageMetadata } from '@/scripts/page-metadata.js';
 import { deviceKind } from '@/scripts/device-kind.js';
 import MkNotes from '@/components/MkNotes.vue';
-import { url } from '@/config.js';
+import { url } from '@@/js/config.js';
 import { favoritedChannelsCache } from '@/cache.js';
 import MkButton from '@/components/MkButton.vue';
 import MkInput from '@/components/MkInput.vue';
@@ -93,7 +93,7 @@ import MkFoldableSection from '@/components/MkFoldableSection.vue';
 import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue';
 import { PageHeaderItem } from '@/types/page-header.js';
 import { isSupportShare } from '@/scripts/navigator.js';
-import copyToClipboard from '@/scripts/copy-to-clipboard.js';
+import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
 import { miLocalStorage } from '@/local-storage.js';
 import { useRouter } from '@/router/supplier.js';
 
@@ -269,14 +269,14 @@ definePageMetadata(() => ({
 
 <style lang="scss" module>
 .main {
-	min-height: calc(100cqh - (var(--stickyTop, 0px) + var(--stickyBottom, 0px)));
+	min-height: calc(100cqh - (var(--MI-stickyTop, 0px) + var(--MI-stickyBottom, 0px)));
 }
 
 .footer {
-	-webkit-backdrop-filter: var(--blur, blur(15px));
-	backdrop-filter: var(--blur, blur(15px));
-	background: var(--acrylicBg);
-	border-top: solid 0.5px var(--divider);
+	-webkit-backdrop-filter: var(--MI-blur, blur(15px));
+	backdrop-filter: var(--MI-blur, blur(15px));
+	background: var(--MI_THEME-acrylicBg);
+	border-top: solid 0.5px var(--MI_THEME-divider);
 }
 
 .bannerContainer {
@@ -310,7 +310,7 @@ definePageMetadata(() => ({
 	left: 0;
 	width: 100%;
 	height: 64px;
-	background: linear-gradient(0deg, var(--panel), var(--X15));
+	background: linear-gradient(0deg, var(--MI_THEME-panel), color(from var(--MI_THEME-panel) srgb r g b / 0));
 }
 
 .bannerStatus {
@@ -335,7 +335,7 @@ definePageMetadata(() => ({
 	bottom: 16px;
 	left: 16px;
 	background: rgba(0, 0, 0, 0.7);
-	color: var(--warn);
+	color: var(--MI_THEME-warn);
 	border-radius: 6px;
 	font-weight: bold;
 	font-size: 1em;
diff --git a/packages/frontend/src/pages/clip.vue b/packages/frontend/src/pages/clip.vue
index fd64a55c65..7b1737fece 100644
--- a/packages/frontend/src/pages/clip.vue
+++ b/packages/frontend/src/pages/clip.vue
@@ -39,11 +39,13 @@ import { i18n } from '@/i18n.js';
 import * as os from '@/os.js';
 import { misskeyApi } from '@/scripts/misskey-api.js';
 import { definePageMetadata } from '@/scripts/page-metadata.js';
-import { url } from '@/config.js';
+import { url } from '@@/js/config.js';
 import MkButton from '@/components/MkButton.vue';
 import { clipsCache } from '@/cache.js';
 import { isSupportShare } from '@/scripts/navigator.js';
-import copyToClipboard from '@/scripts/copy-to-clipboard.js';
+import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
+import { genEmbedCode } from '@/scripts/get-embed-code.js';
+import type { MenuItem } from '@/types/menu.js';
 
 const props = defineProps<{
 	clipId: string,
@@ -127,21 +129,41 @@ const headerActions = computed(() => clip.value && isOwned.value ? [{
 		clipsCache.delete();
 	},
 }, ...(clip.value.isPublic ? [{
-	icon: 'ti ti-link',
-	text: i18n.ts.copyUrl,
-	handler: async (): Promise<void> => {
-		copyToClipboard(`${url}/clips/${clip.value.id}`);
-		os.success();
-	},
-}] : []), ...(clip.value.isPublic && isSupportShare() ? [{
 	icon: 'ti ti-share',
 	text: i18n.ts.share,
-	handler: async (): Promise<void> => {
-		navigator.share({
-			title: clip.value.name,
-			text: clip.value.description,
-			url: `${url}/clips/${clip.value.id}`,
+	handler: (ev: MouseEvent): void => {
+		const menuItems: MenuItem[] = [];
+
+		menuItems.push({
+			icon: 'ti ti-link',
+			text: i18n.ts.copyUrl,
+			action: () => {
+				copyToClipboard(`${url}/clips/${clip.value!.id}`);
+				os.success();
+			},
+		}, {
+			icon: 'ti ti-code',
+			text: i18n.ts.genEmbedCode,
+			action: () => {
+				genEmbedCode('clips', clip.value!.id);
+			},
 		});
+
+		if (isSupportShare()) {
+			menuItems.push({
+				icon: 'ti ti-share',
+				text: i18n.ts.share,
+				action: async () => {
+					navigator.share({
+						title: clip.value!.name,
+						text: clip.value!.description ?? '',
+						url: `${url}/clips/${clip.value!.id}`,
+					});
+				},
+			});
+		}
+
+		os.popupMenu(menuItems, ev.currentTarget ?? ev.target);
 	},
 }] : []), {
 	icon: 'ti ti-trash',
@@ -176,7 +198,7 @@ definePageMetadata(() => ({
 .user {
 	--height: 32px;
 	padding: 16px;
-	border-top: solid 0.5px var(--divider);
+	border-top: solid 0.5px var(--MI_THEME-divider);
 	line-height: var(--height);
 }
 
diff --git a/packages/frontend/src/pages/contact.vue b/packages/frontend/src/pages/contact.vue
index bcdcf43275..1f2bee5a77 100644
--- a/packages/frontend/src/pages/contact.vue
+++ b/packages/frontend/src/pages/contact.vue
@@ -7,18 +7,26 @@ SPDX-License-Identifier: AGPL-3.0-only
 <MkStickyContainer>
 	<template #header><MkPageHeader/></template>
 	<MkSpacer :contentMax="600" :marginMin="20">
-		<div class="_gaps">
-			<MkKeyValue>
-				<template #key>{{ i18n.ts.inquiry }}</template>
+		<div class="_gaps_m">
+			<MkKeyValue :copy="instance.maintainerName">
+				<template #key>{{ i18n.ts.administrator }}</template>
 				<template #value>
-					<MkLink :url="instance.inquiryUrl" target="_blank">{{ instance.inquiryUrl }}</MkLink>
+					<template v-if="instance.maintainerName">{{ instance.maintainerName }}</template>
+					<span v-else style="opacity: 0.7;">({{ i18n.ts.none }})</span>
 				</template>
 			</MkKeyValue>
-
-			<MkKeyValue>
-				<template #key>{{ i18n.ts.email }}</template>
+			<MkKeyValue :copy="instance.maintainerEmail">
+				<template #key>{{ i18n.ts.contact }}</template>
 				<template #value>
-					<div>{{ instance.maintainerEmail }}</div>
+					<template v-if="instance.maintainerEmail">{{ instance.maintainerEmail }}</template>
+					<span v-else style="opacity: 0.7;">({{ i18n.ts.none }})</span>
+				</template>
+			</MkKeyValue>
+			<MkKeyValue :copy="instance.inquiryUrl">
+				<template #key>{{ i18n.ts.inquiry }}</template>
+				<template #value>
+					<MkLink v-if="instance.inquiryUrl" :url="instance.inquiryUrl" target="_blank">{{ instance.inquiryUrl }}</MkLink>
+					<span v-else style="opacity: 0.7;">({{ i18n.ts.none }})</span>
 				</template>
 			</MkKeyValue>
 		</div>
@@ -28,8 +36,8 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <script lang="ts" setup>
 import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
 import { instance } from '@/instance.js';
+import { definePageMetadata } from '@/scripts/page-metadata.js';
 import MkKeyValue from '@/components/MkKeyValue.vue';
 import MkLink from '@/components/MkLink.vue';
 
diff --git a/packages/frontend/src/pages/custom-emojis-manager.vue b/packages/frontend/src/pages/custom-emojis-manager.vue
index 3e2332e408..cae3f3ede9 100644
--- a/packages/frontend/src/pages/custom-emojis-manager.vue
+++ b/packages/frontend/src/pages/custom-emojis-manager.vue
@@ -116,7 +116,7 @@ const selectAll = () => {
 	if (selectedEmojis.value.length > 0) {
 		selectedEmojis.value = [];
 	} else {
-		selectedEmojis.value = Array.from(emojisPaginationComponent.value.items.values(), item => item.id);
+		selectedEmojis.value = Array.from(emojisPaginationComponent.value?.items.values(), item => item.id);
 	}
 };
 
@@ -129,31 +129,33 @@ const toggleSelect = (emoji) => {
 };
 
 const add = async (ev: MouseEvent) => {
-	os.popup(defineAsyncComponent(() => import('./emoji-edit-dialog.vue')), {
+	const { dispose } = os.popup(defineAsyncComponent(() => import('./emoji-edit-dialog.vue')), {
 	}, {
 		done: result => {
 			if (result.created) {
-				emojisPaginationComponent.value.prepend(result.created);
+				emojisPaginationComponent.value?.prepend(result.created);
 			}
 		},
-	}, 'closed');
+		closed: () => dispose(),
+	});
 };
 
 const edit = (emoji) => {
-	os.popup(defineAsyncComponent(() => import('./emoji-edit-dialog.vue')), {
+	const { dispose } = os.popup(defineAsyncComponent(() => import('./emoji-edit-dialog.vue')), {
 		emoji: emoji,
 	}, {
 		done: result => {
 			if (result.updated) {
-				emojisPaginationComponent.value.updateItem(result.updated.id, (oldEmoji: any) => ({
+				emojisPaginationComponent.value?.updateItem(result.updated.id, (oldEmoji) => ({
 					...oldEmoji,
 					...result.updated,
 				}));
 			} else if (result.deleted) {
-				emojisPaginationComponent.value.removeItem(emoji.id);
+				emojisPaginationComponent.value?.removeItem(emoji.id);
 			}
 		},
-	}, 'closed');
+		closed: () => dispose(),
+	});
 };
 
 const importEmoji = (emoji) => {
@@ -224,7 +226,7 @@ const setCategoryBulk = async () => {
 		ids: selectedEmojis.value,
 		category: result,
 	});
-	emojisPaginationComponent.value.reload();
+	emojisPaginationComponent.value?.reload();
 };
 
 const setLicenseBulk = async () => {
@@ -236,43 +238,43 @@ const setLicenseBulk = async () => {
 		ids: selectedEmojis.value,
 		license: result,
 	});
-	emojisPaginationComponent.value.reload();
+	emojisPaginationComponent.value?.reload();
 };
 
 const addTagBulk = async () => {
 	const { canceled, result } = await os.inputText({
 		title: 'Tag',
 	});
-	if (canceled) return;
+	if (canceled || result == null) return;
 	await os.apiWithDialog('admin/emoji/add-aliases-bulk', {
 		ids: selectedEmojis.value,
 		aliases: result.split(' '),
 	});
-	emojisPaginationComponent.value.reload();
+	emojisPaginationComponent.value?.reload();
 };
 
 const removeTagBulk = async () => {
 	const { canceled, result } = await os.inputText({
 		title: 'Tag',
 	});
-	if (canceled) return;
+	if (canceled || result == null) return;
 	await os.apiWithDialog('admin/emoji/remove-aliases-bulk', {
 		ids: selectedEmojis.value,
 		aliases: result.split(' '),
 	});
-	emojisPaginationComponent.value.reload();
+	emojisPaginationComponent.value?.reload();
 };
 
 const setTagBulk = async () => {
 	const { canceled, result } = await os.inputText({
 		title: 'Tag',
 	});
-	if (canceled) return;
+	if (canceled || result == null) return;
 	await os.apiWithDialog('admin/emoji/set-aliases-bulk', {
 		ids: selectedEmojis.value,
 		aliases: result.split(' '),
 	});
-	emojisPaginationComponent.value.reload();
+	emojisPaginationComponent.value?.reload();
 };
 
 const delBulk = async () => {
@@ -284,7 +286,7 @@ const delBulk = async () => {
 	await os.apiWithDialog('admin/emoji/delete-bulk', {
 		ids: selectedEmojis.value,
 	});
-	emojisPaginationComponent.value.reload();
+	emojisPaginationComponent.value?.reload();
 };
 
 const headerActions = computed(() => [{
@@ -315,33 +317,34 @@ definePageMetadata(() => ({
 .ogwlenmc {
 	> .local {
 		.empty {
-			margin: var(--margin);
+			margin: var(--MI-margin);
 		}
 
 		.ldhfsamy {
 			display: grid;
 			grid-template-columns: repeat(auto-fill, minmax(190px, 1fr));
 			grid-gap: 12px;
-			margin: var(--margin) 0;
+			margin: var(--MI-margin) 0;
 
 			> .emoji {
 				display: flex;
 				align-items: center;
 				padding: 11px;
 				text-align: left;
-				border: solid 1px var(--panel);
+				border: solid 1px var(--MI_THEME-panel);
 
 				&:hover {
-					border-color: var(--inputBorderHover);
+					border-color: var(--MI_THEME-inputBorderHover);
 				}
 
 				&.selected {
-					border-color: var(--accent);
+					border-color: var(--MI_THEME-accent);
 				}
 
 				> .img {
 					width: 42px;
 					height: 42px;
+					object-fit: contain;
 				}
 
 				> .body {
@@ -366,14 +369,14 @@ definePageMetadata(() => ({
 
 	> .remote {
 		.empty {
-			margin: var(--margin);
+			margin: var(--MI-margin);
 		}
 
 		.ldhfsamy {
 			display: grid;
 			grid-template-columns: repeat(auto-fill, minmax(190px, 1fr));
 			grid-gap: 12px;
-			margin: var(--margin) 0;
+			margin: var(--MI-margin) 0;
 
 			> .emoji {
 				display: flex;
@@ -382,12 +385,13 @@ definePageMetadata(() => ({
 				text-align: left;
 
 				&:hover {
-					color: var(--accent);
+					color: var(--MI_THEME-accent);
 				}
 
 				> .img {
 					width: 32px;
 					height: 32px;
+					object-fit: contain;
 				}
 
 				> .body {
diff --git a/packages/frontend/src/pages/drive.file.info.vue b/packages/frontend/src/pages/drive.file.info.vue
index 8077edff5f..dfcc82c77b 100644
--- a/packages/frontend/src/pages/drive.file.info.vue
+++ b/packages/frontend/src/pages/drive.file.info.vue
@@ -37,11 +37,17 @@ SPDX-License-Identifier: AGPL-3.0-only
 				</button>
 			</div>
 		</div>
-		<div>
-			<button class="_button" :class="$style.fileAltEditBtn" @click="describe()">
+		<div class="_gaps_s">
+			<button class="_button" :class="$style.kvEditBtn" @click="move()">
 				<MkKeyValue>
+					<template #key>{{ i18n.ts.folder }}</template>
+					<template #value>{{ folderHierarchy.join(' > ') }}<i class="ti ti-pencil" :class="$style.kvEditIcon"></i></template>
+				</MkKeyValue>
+			</button>
+			<button class="_button" :class="$style.kvEditBtn" @click="describe()">
+				<MkKeyValue :class="$style.multiline">
 					<template #key>{{ i18n.ts.description }}</template>
-					<template #value>{{ file.comment ? file.comment : `(${i18n.ts.none})` }}<i class="ti ti-pencil" :class="$style.fileAltEditIcon"></i></template>
+					<template #value>{{ file.comment ? file.comment : `(${i18n.ts.none})` }}<i class="ti ti-pencil" :class="$style.kvEditIcon"></i></template>
 				</MkKeyValue>
 			</button>
 			<MkKeyValue :class="$style.fileMetaDataChildren">
@@ -90,6 +96,18 @@ const props = defineProps<{
 
 const fetching = ref(true);
 const file = ref<Misskey.entities.DriveFile>();
+const folderHierarchy = computed(() => {
+	if (!file.value) return [i18n.ts.drive];
+	const folderNames = [i18n.ts.drive];
+
+	function get(folder: Misskey.entities.DriveFolder) {
+		if (folder.parent) get(folder.parent);
+		folderNames.push(folder.name);
+	}
+
+	if (file.value.folder) get(file.value.folder);
+	return folderNames;
+});
 const isImage = computed(() => file.value?.type.startsWith('image/'));
 
 async function fetch() {
@@ -122,6 +140,19 @@ function crop() {
 	});
 }
 
+function move() {
+	if (!file.value) return;
+
+	os.selectDriveFolder(false).then(folder => {
+		misskeyApi('drive/files/update', {
+			fileId: file.value.id,
+			folderId: folder[0] ? folder[0].id : null,
+		}).then(async () => {
+			await fetch();
+		});
+	});
+}
+
 function toggleSensitive() {
 	if (!file.value) return;
 
@@ -160,7 +191,7 @@ function rename() {
 function describe() {
 	if (!file.value) return;
 
-	os.popup(defineAsyncComponent(() => import('@/components/MkFileCaptionEditWindow.vue')), {
+	const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkFileCaptionEditWindow.vue')), {
 		default: file.value.comment ?? '',
 		file: file.value,
 	}, {
@@ -172,7 +203,8 @@ function describe() {
 				await fetch();
 			});
 		},
-	}, 'closed');
+		closed: () => dispose(),
+	});
 }
 
 async function deleteFile() {
@@ -199,8 +231,8 @@ onMounted(async () => {
 <style lang="scss" module>
 
 .filePreviewRoot {
-	background: var(--panel);
-	border-radius: var(--radius);
+	background: var(--MI_THEME-panel);
+	border-radius: var(--MI-radius);
 	// MkMediaList 内の上部マージン 4px
 	padding: calc(1rem - 4px) 1rem 1rem;
 }
@@ -230,9 +262,10 @@ onMounted(async () => {
 
 		&:hover,
 		&:focus-visible {
-			background-color: var(--accentedBg);
-			color: var(--accent);
+			background-color: var(--MI_THEME-accentedBg);
+			color: var(--MI_THEME-accent);
 			text-decoration: none;
+			outline: none;
 		}
 
 		&.danger {
@@ -252,7 +285,7 @@ onMounted(async () => {
 	align-items: center;
 	min-width: 0;
 	font-weight: 700;
-	border-radius: var(--radius);
+	border-radius: var(--MI-radius);
 	font-size: .8rem;
 
 	>.fileNameEditIcon {
@@ -266,12 +299,12 @@ onMounted(async () => {
 	}
 
 	&:hover {
-		background-color: var(--accentedBg);
+		background-color: var(--MI_THEME-accentedBg);
 
 		>.fileName,
 		>.fileNameEditIcon {
 			visibility: visible;
-			color: var(--accent);
+			color: var(--MI_THEME-accent);
 		}
 	}
 }
@@ -280,14 +313,18 @@ onMounted(async () => {
 	padding: .5rem 1rem;
 }
 
-.fileAltEditBtn {
+.multiline {
+	white-space: pre-wrap;
+}
+
+.kvEditBtn {
 	text-align: start;
 	display: block;
 	width: 100%;
 	padding: .5rem 1rem;
-	border-radius: var(--radius);
+	border-radius: var(--MI-radius);
 
-	.fileAltEditIcon {
+	.kvEditIcon {
 		display: inline-block;
 		color: transparent;
 		visibility: hidden;
@@ -295,11 +332,11 @@ onMounted(async () => {
 	}
 
 	&:hover {
-		color: var(--accent);
-		background-color: var(--accentedBg);
+		color: var(--MI_THEME-accent);
+		background-color: var(--MI_THEME-accentedBg);
 
-		.fileAltEditIcon {
-			color: var(--accent);
+		.kvEditIcon {
+			color: var(--MI_THEME-accent);
 			visibility: visible;
 		}
 	}
diff --git a/packages/frontend/src/pages/drop-and-fusion.game.vue b/packages/frontend/src/pages/drop-and-fusion.game.vue
index eba5b92154..fb4d599c28 100644
--- a/packages/frontend/src/pages/drop-and-fusion.game.vue
+++ b/packages/frontend/src/pages/drop-and-fusion.game.vue
@@ -111,7 +111,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 			<div v-if="replaying" class="_woodenFrame">
 				<div class="_woodenFrameInner">
 					<div style="background: #0004;">
-						<div style="height: 10px; background: var(--accent); will-change: width;" :style="{ width: `${(currentFrame / endedAtFrame) * 100}%` }"></div>
+						<div style="height: 10px; background: var(--MI_THEME-accent); will-change: width;" :style="{ width: `${(currentFrame / endedAtFrame) * 100}%` }"></div>
 					</div>
 				</div>
 				<div class="_woodenFrameInner">
@@ -205,12 +205,12 @@ import { claimAchievement } from '@/scripts/achievements.js';
 import { defaultStore } from '@/store.js';
 import { misskeyApi } from '@/scripts/misskey-api.js';
 import { i18n } from '@/i18n.js';
-import { useInterval } from '@/scripts/use-interval.js';
-import { apiUrl } from '@/config.js';
+import { useInterval } from '@@/js/use-interval.js';
+import { apiUrl } from '@@/js/config.js';
 import { $i } from '@/account.js';
 import * as sound from '@/scripts/sound.js';
 import MkRange from '@/components/MkRange.vue';
-import copyToClipboard from '@/scripts/copy-to-clipboard.js';
+import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
 
 type FrontendMonoDefinition = {
 	id: string;
@@ -1008,8 +1008,18 @@ function attachGameEvents() {
 		const domX = rect.left + (x * viewScale);
 		const domY = rect.top + (y * viewScale);
 		const scoreUnit = getScoreUnit(props.gameMode);
-		os.popup(MkRippleEffect, { x: domX, y: domY }, {}, 'end');
-		os.popup(MkPlusOneEffect, { x: domX, y: domY, value: scoreDelta + (scoreUnit === 'pt' ? '' : scoreUnit) }, {}, 'end');
+
+		{
+			const { dispose } = os.popup(MkRippleEffect, { x: domX, y: domY }, {
+				end: () => dispose(),
+			});
+		}
+
+		{
+			const { dispose } = os.popup(MkPlusOneEffect, { x: domX, y: domY, value: scoreDelta + (scoreUnit === 'pt' ? '' : scoreUnit) }, {
+				end: () => dispose(),
+			});
+		}
 
 		if (nextMono) {
 			const def = monoDefinitions.value.find(x => x.id === nextMono.id)!;
diff --git a/packages/frontend/src/pages/emoji-edit-dialog.vue b/packages/frontend/src/pages/emoji-edit-dialog.vue
index 16769ef360..3765319b25 100644
--- a/packages/frontend/src/pages/emoji-edit-dialog.vue
+++ b/packages/frontend/src/pages/emoji-edit-dialog.vue
@@ -8,15 +8,15 @@ SPDX-License-Identifier: AGPL-3.0-only
 	ref="windowEl"
 	:initialWidth="400"
 	:initialHeight="500"
-	:canResize="false"
-	@close="windowEl.close()"
-	@closed="$emit('closed')"
+	:canResize="true"
+	@close="windowEl?.close()"
+	@closed="emit('closed')"
 >
 	<template v-if="emoji" #header>:{{ emoji.name }}:</template>
 	<template v-else #header>New emoji</template>
 
-	<div>
-		<MkSpacer :marginMin="20" :marginMax="28">
+	<div style="display: flex; flex-direction: column; min-height: 100%;">
+		<MkSpacer :marginMin="20" :marginMax="28" style="flex-grow: 1;">
 			<div class="_gaps_m">
 				<div v-if="imgUrl != null" :class="$style.imgs">
 					<div style="background: #000;" :class="$style.imgContainer">
@@ -95,14 +95,19 @@ import { selectFile } from '@/scripts/select-file.js';
 import MkRolePreview from '@/components/MkRolePreview.vue';
 
 const props = defineProps<{
-	emoji?: any,
+	emoji?: Misskey.entities.EmojiDetailed,
+}>();
+
+const emit = defineEmits<{
+	(ev: 'done', v: { deleted?: boolean; updated?: Misskey.entities.AdminEmojiUpdateRequest; created?: Misskey.entities.AdminEmojiUpdateRequest }): void,
+	(ev: 'closed'): void
 }>();
 
 const windowEl = ref<InstanceType<typeof MkWindow> | null>(null);
 const name = ref<string>(props.emoji ? props.emoji.name : '');
-const category = ref<string>(props.emoji ? props.emoji.category : '');
+const category = ref<string>(props.emoji?.category ? props.emoji.category : '');
 const aliases = ref<string>(props.emoji ? props.emoji.aliases.join(' ') : '');
-const license = ref<string>(props.emoji ? (props.emoji.license ?? '') : '');
+const license = ref<string>(props.emoji?.license ? props.emoji.license : '');
 const isSensitive = ref(props.emoji ? props.emoji.isSensitive : false);
 const localOnly = ref(props.emoji ? props.emoji.localOnly : false);
 const roleIdsThatCanBeUsedThisEmojiAsReaction = ref(props.emoji ? props.emoji.roleIdsThatCanBeUsedThisEmojiAsReaction : []);
@@ -115,12 +120,7 @@ watch(roleIdsThatCanBeUsedThisEmojiAsReaction, async () => {
 
 const imgUrl = computed(() => file.value ? file.value.url : props.emoji ? `/emoji/${props.emoji.name}.webp` : null);
 
-const emit = defineEmits<{
-	(ev: 'done', v: { deleted?: boolean; updated?: any; created?: any }): void,
-	(ev: 'closed'): void
-}>();
-
-async function changeImage(ev) {
+async function changeImage(ev: Event) {
 	file.value = await selectFile(ev.currentTarget ?? ev.target, null);
 	const candidate = file.value.name.replace(/\.(.+)$/, '');
 	if (candidate.match(/^[a-z0-9_]+$/)) {
@@ -140,7 +140,7 @@ async function addRole() {
 	rolesThatCanBeUsedThisEmojiAsReaction.value.push(role);
 }
 
-async function removeRole(role, ev) {
+async function removeRole(role: Misskey.entities.RoleLite, ev: Event) {
 	rolesThatCanBeUsedThisEmojiAsReaction.value = rolesThatCanBeUsedThisEmojiAsReaction.value.filter(x => x.id !== role.id);
 }
 
@@ -172,7 +172,7 @@ async function done() {
 			},
 		});
 
-		windowEl.value.close();
+		windowEl.value?.close();
 	} else {
 		const created = await os.apiWithDialog('admin/emoji/add', params);
 
@@ -180,11 +180,12 @@ async function done() {
 			created: created,
 		});
 
-		windowEl.value.close();
+		windowEl.value?.close();
 	}
 }
 
 async function del() {
+	if (!props.emoji) return;
 	const { canceled } = await os.confirm({
 		type: 'warning',
 		text: i18n.tsx.removeAreYouSure({ x: name.value }),
@@ -197,7 +198,7 @@ async function del() {
 		emit('done', {
 			deleted: true,
 		});
-		windowEl.value.close();
+		windowEl.value?.close();
 	});
 }
 </script>
@@ -239,11 +240,13 @@ async function del() {
 
 .footer {
 	position: sticky;
+	z-index: 10000;
 	bottom: 0;
 	left: 0;
 	padding: 12px;
-	border-top: solid 0.5px var(--divider);
-	-webkit-backdrop-filter: var(--blur, blur(15px));
-	backdrop-filter: var(--blur, blur(15px));
+	border-top: solid 0.5px var(--MI_THEME-divider);
+	background: var(--MI_THEME-acrylicBg);
+	-webkit-backdrop-filter: var(--MI-blur, blur(15px));
+	backdrop-filter: var(--MI-blur, blur(15px));
 }
 </style>
diff --git a/packages/frontend/src/pages/emojis.emoji.vue b/packages/frontend/src/pages/emojis.emoji.vue
index 5301a08521..fcd22155b7 100644
--- a/packages/frontend/src/pages/emojis.emoji.vue
+++ b/packages/frontend/src/pages/emojis.emoji.vue
@@ -14,10 +14,10 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import * as os from '@/os.js';
 import * as Misskey from 'misskey-js';
+import * as os from '@/os.js';
 import { misskeyApiGet } from '@/scripts/misskey-api.js';
-import copyToClipboard from '@/scripts/copy-to-clipboard.js';
+import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
 import { i18n } from '@/i18n.js';
 import MkCustomEmojiDetailedDialog from '@/components/MkCustomEmojiDetailedDialog.vue';
 
@@ -40,12 +40,12 @@ function menu(ev) {
 		text: i18n.ts.info,
 		icon: 'ti ti-info-circle',
 		action: async () => {
-			os.popup(MkCustomEmojiDetailedDialog, {
+			const { dispose } = os.popup(MkCustomEmojiDetailedDialog, {
 				emoji: await misskeyApiGet('emoji', {
 					name: props.emoji.name,
-				})
+				}),
 			}, {
-				anchor: ev.target,
+				closed: () => dispose(),
 			});
 		},
 	}], ev.currentTarget ?? ev.target);
@@ -58,11 +58,11 @@ function menu(ev) {
 	align-items: center;
 	padding: 12px;
 	text-align: left;
-	background: var(--panel);
+	background: var(--MI_THEME-panel);
 	border-radius: 8px;
 
 	&:hover {
-		border-color: var(--accent);
+		border-color: var(--MI_THEME-accent);
 	}
 }
 
diff --git a/packages/frontend/src/pages/explore.featured.vue b/packages/frontend/src/pages/explore.featured.vue
index cfdb235d3a..8b16a88ff3 100644
--- a/packages/frontend/src/pages/explore.featured.vue
+++ b/packages/frontend/src/pages/explore.featured.vue
@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <template>
 <MkSpacer :contentMax="800">
-	<MkTab v-model="tab" style="margin-bottom: var(--margin);">
+	<MkTab v-model="tab" style="margin-bottom: var(--MI-margin);">
 		<option value="notes">{{ i18n.ts.notes }}</option>
 		<option value="polls">{{ i18n.ts.poll }}</option>
 	</MkTab>
diff --git a/packages/frontend/src/pages/explore.users.vue b/packages/frontend/src/pages/explore.users.vue
index e9608ae94e..c9acfec04f 100644
--- a/packages/frontend/src/pages/explore.users.vue
+++ b/packages/frontend/src/pages/explore.users.vue
@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <template>
 <MkSpacer :contentMax="1200">
-	<MkTab v-model="origin" style="margin-bottom: var(--margin);">
+	<MkTab v-model="origin" style="margin-bottom: var(--MI-margin);">
 		<option value="local">{{ i18n.ts.local }}</option>
 		<option value="remote">{{ i18n.ts.remote }}</option>
 	</MkTab>
diff --git a/packages/frontend/src/pages/favorites.vue b/packages/frontend/src/pages/favorites.vue
index c3d4cae4aa..6716566101 100644
--- a/packages/frontend/src/pages/favorites.vue
+++ b/packages/frontend/src/pages/favorites.vue
@@ -46,7 +46,7 @@ definePageMetadata(() => ({
 
 <style lang="scss" module>
 .note {
-	background: var(--panel);
-	border-radius: var(--radius);
+	background: var(--MI_THEME-panel);
+	border-radius: var(--MI-radius);
 }
 </style>
diff --git a/packages/frontend/src/pages/flash/flash-edit.vue b/packages/frontend/src/pages/flash/flash-edit.vue
index 3445da26a2..d84ec4873b 100644
--- a/packages/frontend/src/pages/flash/flash-edit.vue
+++ b/packages/frontend/src/pages/flash/flash-edit.vue
@@ -11,6 +11,12 @@ SPDX-License-Identifier: AGPL-3.0-only
 			<MkInput v-model="title">
 				<template #label>{{ i18n.ts._play.title }}</template>
 			</MkInput>
+			<MkSelect v-model="visibility">
+				<template #label>{{ i18n.ts.visibility }}</template>
+				<template #caption>{{ i18n.ts._play.visibilityDescription }}</template>
+				<option :key="'public'" :value="'public'">{{ i18n.ts.public }}</option>
+				<option :key="'private'" :value="'private'">{{ i18n.ts.private }}</option>
+			</MkSelect>
 			<MkTextarea v-model="summary" :mfmAutocomplete="true" :mfmPreview="true">
 				<template #label>{{ i18n.ts._play.summary }}</template>
 			</MkTextarea>
@@ -18,25 +24,26 @@ SPDX-License-Identifier: AGPL-3.0-only
 			<MkCodeEditor v-model="script" lang="is">
 				<template #label>{{ i18n.ts._play.script }}</template>
 			</MkCodeEditor>
-			<MkSelect v-model="visibility">
-				<template #label>{{ i18n.ts.visibility }}</template>
-				<template #caption>{{ i18n.ts._play.visibilityDescription }}</template>
-				<option :key="'public'" :value="'public'">{{ i18n.ts.public }}</option>
-				<option :key="'private'" :value="'private'">{{ i18n.ts.private }}</option>
-			</MkSelect>
-			<div class="_buttons">
-				<MkButton primary @click="save"><i class="ti ti-check"></i> {{ i18n.ts.save }}</MkButton>
-				<MkButton @click="show"><i class="ti ti-eye"></i> {{ i18n.ts.show }}</MkButton>
-				<MkButton v-if="flash" danger @click="del"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton>
-			</div>
 		</div>
 	</MkSpacer>
+	<template #footer>
+		<div :class="$style.footer">
+			<MkSpacer>
+				<div class="_buttons">
+					<MkButton primary @click="save"><i class="ti ti-check"></i> {{ i18n.ts.save }}</MkButton>
+					<MkButton @click="show"><i class="ti ti-eye"></i> {{ i18n.ts.show }}</MkButton>
+					<MkButton v-if="flash" danger @click="del"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton>
+				</div>
+			</MkSpacer>
+		</div>
+	</template>
 </MkStickyContainer>
 </template>
 
 <script lang="ts" setup>
 import { computed, ref } from 'vue';
 import * as Misskey from 'misskey-js';
+import { AISCRIPT_VERSION } from '@syuilo/aiscript';
 import MkButton from '@/components/MkButton.vue';
 import * as os from '@/os.js';
 import { misskeyApi } from '@/scripts/misskey-api.js';
@@ -48,7 +55,7 @@ import MkInput from '@/components/MkInput.vue';
 import MkSelect from '@/components/MkSelect.vue';
 import { useRouter } from '@/router/supplier.js';
 
-const PRESET_DEFAULT = `/// @ 0.18.0
+const PRESET_DEFAULT = `/// @ ${AISCRIPT_VERSION}
 
 var name = ""
 
@@ -66,7 +73,7 @@ Ui:render([
 ])
 `;
 
-const PRESET_OMIKUJI = `/// @ 0.18.0
+const PRESET_OMIKUJI = `/// @ ${AISCRIPT_VERSION}
 // ユーザーごとに日替わりのおみくじのプリセット
 
 // 選択肢
@@ -109,7 +116,7 @@ Ui:render([
 ])
 `;
 
-const PRESET_SHUFFLE = `/// @ 0.18.0
+const PRESET_SHUFFLE = `/// @ ${AISCRIPT_VERSION}
 // 巻き戻し可能な文字シャッフルのプリセット
 
 let string = "ペペロンチーノ"
@@ -188,7 +195,7 @@ var cursor = 0
 do()
 `;
 
-const PRESET_QUIZ = `/// @ 0.18.0
+const PRESET_QUIZ = `/// @ ${AISCRIPT_VERSION}
 let title = '地理クイズ'
 
 let qas = [{
@@ -301,7 +308,7 @@ qaEls.push(Ui:C:container({
 Ui:render(qaEls)
 `;
 
-const PRESET_TIMELINE = `/// @ 0.18.0
+const PRESET_TIMELINE = `/// @ ${AISCRIPT_VERSION}
 // APIリクエストを行いローカルタイムラインを表示するプリセット
 
 @fetch() {
@@ -368,7 +375,6 @@ const props = defineProps<{
 }>();
 
 const flash = ref<Misskey.entities.Flash | null>(null);
-const visibility = ref<'private' | 'public'>('public');
 
 if (props.id) {
 	flash.value = await misskeyApi('flash/show', {
@@ -379,6 +385,7 @@ if (props.id) {
 const title = ref(flash.value?.title ?? 'New Play');
 const summary = ref(flash.value?.summary ?? '');
 const permissions = ref(flash.value?.permissions ?? []);
+const visibility = ref<'private' | 'public'>(flash.value?.visibility ?? 'public');
 const script = ref(flash.value?.script ?? PRESET_DEFAULT);
 
 function selectPreset(ev: MouseEvent) {
@@ -458,3 +465,10 @@ definePageMetadata(() => ({
 	title: flash.value ? `${i18n.ts._play.edit}: ${flash.value.title}` : i18n.ts._play.new,
 }));
 </script>
+<style lang="scss" module>
+.footer {
+	backdrop-filter: var(--MI-blur, blur(15px));
+	background: var(--MI_THEME-acrylicBg);
+	border-top: solid .5px var(--MI_THEME-divider);
+}
+</style>
diff --git a/packages/frontend/src/pages/flash/flash-index.vue b/packages/frontend/src/pages/flash/flash-index.vue
index f63a799365..2b85489706 100644
--- a/packages/frontend/src/pages/flash/flash-index.vue
+++ b/packages/frontend/src/pages/flash/flash-index.vue
@@ -55,7 +55,8 @@ const tab = ref('featured');
 
 const featuredFlashsPagination = {
 	endpoint: 'flash/featured' as const,
-	noPaging: true,
+	limit: 5,
+	offsetMode: true,
 };
 const myFlashsPagination = {
 	endpoint: 'flash/my' as const,
diff --git a/packages/frontend/src/pages/flash/flash.vue b/packages/frontend/src/pages/flash/flash.vue
index 40499fde0e..3b982a405e 100644
--- a/packages/frontend/src/pages/flash/flash.vue
+++ b/packages/frontend/src/pages/flash/flash.vue
@@ -23,6 +23,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 								<MkButton v-else v-tooltip="i18n.ts.like" asLike class="button" rounded @click="like()"><i class="ti ti-heart"></i><span v-if="flash?.likedCount && flash.likedCount > 0" style="margin-left: 6px;">{{ flash.likedCount }}</span></MkButton>
 								<MkButton v-tooltip="i18n.ts.copyLink" class="button" rounded @click="copyLink"><i class="ti ti-link ti-fw"></i></MkButton>
 								<MkButton v-tooltip="i18n.ts.share" class="button" rounded @click="share"><i class="ti ti-share ti-fw"></i></MkButton>
+								<MkButton v-if="$i && $i.id !== flash.user.id" class="button" rounded @mousedown="showMenu"><i class="ti ti-dots ti-fw"></i></MkButton>
 							</div>
 						</div>
 					</div>
@@ -50,7 +51,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 						<div><i class="ti ti-clock"></i> {{ i18n.ts.createdAt }}: <MkTime :time="flash.createdAt" mode="detail"/></div>
 					</div>
 				</div>
-				<MkA v-if="$i && $i.id === flash.userId" :to="`/play/${flash.id}/edit`" style="color: var(--accent);">{{ i18n.ts._play.editThisPage }}</MkA>
+				<MkA v-if="$i && $i.id === flash.userId" :to="`/play/${flash.id}/edit`" style="color: var(--MI_THEME-accent);">{{ i18n.ts._play.editThisPage }}</MkA>
 				<MkAd :prefer="['horizontal', 'horizontal-big']"/>
 			</div>
 			<MkError v-else-if="error" @retry="fetchFlash()"/>
@@ -61,13 +62,13 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { computed, onDeactivated, onUnmounted, Ref, ref, watch, shallowRef } from 'vue';
+import { computed, onDeactivated, onUnmounted, Ref, ref, watch, shallowRef, defineAsyncComponent } from 'vue';
 import * as Misskey from 'misskey-js';
 import { Interpreter, Parser, values } from '@syuilo/aiscript';
 import MkButton from '@/components/MkButton.vue';
 import * as os from '@/os.js';
 import { misskeyApi } from '@/scripts/misskey-api.js';
-import { url } from '@/config.js';
+import { url } from '@@/js/config.js';
 import { i18n } from '@/i18n.js';
 import { definePageMetadata } from '@/scripts/page-metadata.js';
 import MkAsUi from '@/components/MkAsUi.vue';
@@ -78,7 +79,9 @@ import MkCode from '@/components/MkCode.vue';
 import { defaultStore } from '@/store.js';
 import { $i } from '@/account.js';
 import { isSupportShare } from '@/scripts/navigator.js';
-import copyToClipboard from '@/scripts/copy-to-clipboard.js';
+import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
+import type { MenuItem } from '@/types/menu.js';
+import { pleaseLogin } from '@/scripts/please-login.js';
 
 const props = defineProps<{
 	id: string;
@@ -101,18 +104,23 @@ function fetchFlash() {
 function share(ev: MouseEvent) {
 	if (!flash.value) return;
 
-	os.popupMenu([
-		{
-			text: i18n.ts.shareWithNote,
-			icon: 'ti ti-pencil',
-			action: shareWithNote,
-		},
-		...(isSupportShare() ? [{
+	const menuItems: MenuItem[] = [];
+
+	menuItems.push({
+		text: i18n.ts.shareWithNote,
+		icon: 'ti ti-pencil',
+		action: shareWithNote,
+	});
+
+	if (isSupportShare()) {
+		menuItems.push({
 			text: i18n.ts.share,
 			icon: 'ti ti-share',
 			action: shareWithNavigator,
-		}] : []),
-	], ev.currentTarget ?? ev.target);
+		});
+	}
+
+	os.popupMenu(menuItems, ev.currentTarget ?? ev.target);
 }
 
 function copyLink() {
@@ -143,6 +151,7 @@ function shareWithNote() {
 
 function like() {
 	if (!flash.value) return;
+	pleaseLogin();
 
 	os.apiWithDialog('flash/like', {
 		flashId: flash.value.id,
@@ -154,6 +163,7 @@ function like() {
 
 async function unlike() {
 	if (!flash.value) return;
+	pleaseLogin();
 
 	const confirm = await os.confirm({
 		type: 'warning',
@@ -226,6 +236,53 @@ async function run() {
 	}
 }
 
+function reportAbuse() {
+	if (!flash.value) return;
+
+	const pageUrl = `${url}/play/${flash.value.id}`;
+
+	const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkAbuseReportWindow.vue')), {
+		user: flash.value.user,
+		initialComment: `Play: ${pageUrl}\n-----\n`,
+	}, {
+		closed: () => dispose(),
+	});
+}
+
+function showMenu(ev: MouseEvent) {
+	if (!flash.value) return;
+
+	const menu: MenuItem[] = [
+		...($i && $i.id !== flash.value.userId ? [
+			{
+				icon: 'ti ti-exclamation-circle',
+				text: i18n.ts.reportAbuse,
+				action: reportAbuse,
+			},
+			...($i.isModerator || $i.isAdmin ? [
+				{
+					type: 'divider' as const,
+				},
+				{
+					icon: 'ti ti-trash',
+					text: i18n.ts.delete,
+					danger: true,
+					action: () => os.confirm({
+						type: 'warning',
+						text: i18n.ts.deleteConfirm,
+					}).then(({ canceled }) => {
+						if (canceled || !flash.value) return;
+
+						os.apiWithDialog('flash/delete', { flashId: flash.value.id });
+					}),
+				},
+			] : []),
+		] : []),
+	];
+
+	os.popupMenu(menu, ev.currentTarget ?? ev.target);
+}
+
 function reset() {
 	if (aiscript.value) aiscript.value.abort();
 	started.value = false;
@@ -310,7 +367,7 @@ definePageMetadata(() => ({
 				justify-content: center;
 				gap: 12px;
 				padding: 16px;
-				border-bottom: 1px solid var(--divider);
+				border-bottom: 1px solid var(--MI_THEME-divider);
 
 				&:last-child {
 					border-bottom: none;
diff --git a/packages/frontend/src/pages/follow-requests.vue b/packages/frontend/src/pages/follow-requests.vue
index 8991af8086..a840d0d0b3 100644
--- a/packages/frontend/src/pages/follow-requests.vue
+++ b/packages/frontend/src/pages/follow-requests.vue
@@ -55,13 +55,13 @@ const pagination = {
 
 function accept(user) {
 	misskeyApi('following/requests/accept', { userId: user.id }).then(() => {
-		paginationComponent.value.reload();
+		paginationComponent.value?.reload();
 	});
 }
 
 function reject(user) {
 	misskeyApi('following/requests/reject', { userId: user.id }).then(() => {
-		paginationComponent.value.reload();
+		paginationComponent.value?.reload();
 	});
 }
 
diff --git a/packages/frontend/src/pages/follow.vue b/packages/frontend/src/pages/follow.vue
deleted file mode 100644
index 247b0ac639..0000000000
--- a/packages/frontend/src/pages/follow.vue
+++ /dev/null
@@ -1,71 +0,0 @@
-<!--
-SPDX-FileCopyrightText: syuilo and misskey-project
-SPDX-License-Identifier: AGPL-3.0-only
--->
-
-<template>
-<div>
-</div>
-</template>
-
-<script lang="ts" setup>
-import { } from 'vue';
-import * as Misskey from 'misskey-js';
-import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
-import { i18n } from '@/i18n.js';
-import { defaultStore } from '@/store.js';
-import { mainRouter } from '@/router/main.js';
-
-async function follow(user): Promise<void> {
-	const { canceled } = await os.confirm({
-		type: 'question',
-		text: i18n.tsx.followConfirm({ name: user.name || user.username }),
-	});
-
-	if (canceled) {
-		window.close();
-		return;
-	}
-
-	os.apiWithDialog('following/create', {
-		userId: user.id,
-		withReplies: defaultStore.state.defaultWithReplies,
-	});
-	user.withReplies = defaultStore.state.defaultWithReplies;
-}
-
-const acct = new URL(location.href).searchParams.get('acct');
-if (acct == null) {
-	throw new Error('acct required');
-}
-
-let promise;
-
-if (acct.startsWith('https://')) {
-	promise = misskeyApi('ap/show', {
-		uri: acct,
-	});
-	promise.then(res => {
-		if (res.type === 'User') {
-			follow(res.object);
-		} else if (res.type === 'Note') {
-			mainRouter.push(`/notes/${res.object.id}`);
-		} else {
-			os.alert({
-				type: 'error',
-				text: 'Not a user',
-			}).then(() => {
-				window.close();
-			});
-		}
-	});
-} else {
-	promise = misskeyApi('users/show', Misskey.acct.parse(acct));
-	promise.then(user => {
-		follow(user);
-	});
-}
-
-os.promiseDialog(promise, null, null, i18n.ts.fetchingAsApObject);
-</script>
diff --git a/packages/frontend/src/pages/gallery/edit.vue b/packages/frontend/src/pages/gallery/edit.vue
index a68a7e5c41..70f8b2c31d 100644
--- a/packages/frontend/src/pages/gallery/edit.vue
+++ b/packages/frontend/src/pages/gallery/edit.vue
@@ -141,7 +141,7 @@ definePageMetadata(() => ({
 		top: 8px;
 		left: 9px;
 		padding: 8px;
-		background: var(--panel);
+		background: var(--MI_THEME-panel);
 	}
 
 	> .remove {
@@ -149,7 +149,7 @@ definePageMetadata(() => ({
 		top: 8px;
 		right: 9px;
 		padding: 8px;
-		background: var(--panel);
+		background: var(--MI_THEME-panel);
 	}
 }
 </style>
diff --git a/packages/frontend/src/pages/gallery/index.vue b/packages/frontend/src/pages/gallery/index.vue
index e0e187f2ce..f396fd2c0c 100644
--- a/packages/frontend/src/pages/gallery/index.vue
+++ b/packages/frontend/src/pages/gallery/index.vue
@@ -130,6 +130,6 @@ definePageMetadata(() => ({
 	display: grid;
 	grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
 	grid-gap: 12px;
-	margin: 0 var(--margin);
+	margin: 0 var(--MI-margin);
 }
 </style>
diff --git a/packages/frontend/src/pages/gallery/post.vue b/packages/frontend/src/pages/gallery/post.vue
index 615675225d..feb4c60611 100644
--- a/packages/frontend/src/pages/gallery/post.vue
+++ b/packages/frontend/src/pages/gallery/post.vue
@@ -31,6 +31,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 								<button v-tooltip="i18n.ts.shareWithNote" v-click-anime class="_button" @click="shareWithNote"><i class="ti ti-repeat ti-fw"></i></button>
 								<button v-tooltip="i18n.ts.copyLink" v-click-anime class="_button" @click="copyLink"><i class="ti ti-link ti-fw"></i></button>
 								<button v-if="isSupportShare()" v-tooltip="i18n.ts.share" v-click-anime class="_button" @click="share"><i class="ti ti-share ti-fw"></i></button>
+								<button v-if="$i && $i.id !== post.user.id" v-click-anime class="_button" @mousedown="showMenu"><i class="ti ti-dots ti-fw"></i></button>
 							</div>
 						</div>
 						<div class="user">
@@ -62,7 +63,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { computed, watch, ref } from 'vue';
+import { computed, watch, ref, defineAsyncComponent } from 'vue';
 import * as Misskey from 'misskey-js';
 import MkButton from '@/components/MkButton.vue';
 import * as os from '@/os.js';
@@ -71,14 +72,15 @@ import MkContainer from '@/components/MkContainer.vue';
 import MkPagination from '@/components/MkPagination.vue';
 import MkGalleryPostPreview from '@/components/MkGalleryPostPreview.vue';
 import MkFollowButton from '@/components/MkFollowButton.vue';
-import { url } from '@/config.js';
+import { url } from '@@/js/config.js';
 import { i18n } from '@/i18n.js';
 import { definePageMetadata } from '@/scripts/page-metadata.js';
 import { defaultStore } from '@/store.js';
 import { $i } from '@/account.js';
 import { isSupportShare } from '@/scripts/navigator.js';
-import copyToClipboard from '@/scripts/copy-to-clipboard.js';
+import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
 import { useRouter } from '@/router/supplier.js';
+import type { MenuItem } from '@/types/menu.js';
 
 const router = useRouter();
 
@@ -153,13 +155,56 @@ function edit() {
 	router.push(`/gallery/${post.value.id}/edit`);
 }
 
+function reportAbuse() {
+	if (!post.value) return;
+
+	const pageUrl = `${url}/gallery/${post.value.id}`;
+
+	const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkAbuseReportWindow.vue')), {
+		user: post.value.user,
+		initialComment: `Post: ${pageUrl}\n-----\n`,
+	}, {
+		closed: () => dispose(),
+	});
+}
+
+function showMenu(ev: MouseEvent) {
+	if (!post.value) return;
+
+	const menuItems: MenuItem[] = [];
+
+	if ($i && $i.id !== post.value.userId) {
+		menuItems.push({
+			icon: 'ti ti-exclamation-circle',
+			text: i18n.ts.reportAbuse,
+			action: reportAbuse,
+		});
+
+		if ($i.isModerator || $i.isAdmin) {
+			menuItems.push({
+				type: 'divider',
+			}, {
+				icon: 'ti ti-trash',
+				text: i18n.ts.delete,
+				danger: true,
+				action: () => os.confirm({
+					type: 'warning',
+					text: i18n.ts.deleteConfirm,
+				}).then(({ canceled }) => {
+					if (canceled || !post.value) return;
+
+					os.apiWithDialog('gallery/posts/delete', { postId: post.value.id });
+				}),
+			});
+		}
+	}
+
+	os.popupMenu(menuItems, ev.currentTarget ?? ev.target);
+}
+
 watch(() => props.postId, fetchPost, { immediate: true });
 
-const headerActions = computed(() => [{
-	icon: 'ti ti-pencil',
-	text: i18n.ts.edit,
-	handler: edit,
-}]);
+const headerActions = computed(() => []);
 
 const headerTabs = computed(() => []);
 
@@ -217,14 +262,14 @@ definePageMetadata(() => ({
 			align-items: center;
 			margin-top: 16px;
 			padding: 16px 0 0 0;
-			border-top: solid 0.5px var(--divider);
+			border-top: solid 0.5px var(--MI_THEME-divider);
 
 			> .like {
 				> .button {
-					--accent: rgb(241 97 132);
-					--X8: rgb(241 92 128);
-					--buttonBg: rgb(216 71 106 / 5%);
-					--buttonHoverBg: rgb(216 71 106 / 10%);
+					--MI_THEME-accent: rgb(241 97 132);
+					--MI_THEME-X8: rgb(241 92 128);
+					--MI_THEME-buttonBg: rgb(216 71 106 / 5%);
+					--MI_THEME-buttonHoverBg: rgb(216 71 106 / 10%);
 					color: #ff002f;
 
 					::v-deep(.count) {
@@ -241,7 +286,7 @@ definePageMetadata(() => ({
 					margin: 0 8px;
 
 					&:hover {
-						color: var(--fgHighlighted);
+						color: var(--MI_THEME-fgHighlighted);
 					}
 				}
 			}
@@ -250,7 +295,7 @@ definePageMetadata(() => ({
 		> .user {
 			margin-top: 16px;
 			padding: 16px 0 0 0;
-			border-top: solid 0.5px var(--divider);
+			border-top: solid 0.5px var(--MI_THEME-divider);
 			display: flex;
 			align-items: center;
 			flex-wrap: wrap;
@@ -276,7 +321,7 @@ definePageMetadata(() => ({
 	display: grid;
 	grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
 	grid-gap: 12px;
-	margin: var(--margin);
+	margin: var(--MI-margin);
 
 	> .post {
 
diff --git a/packages/frontend/src/pages/games.vue b/packages/frontend/src/pages/games.vue
index afd6df1ad9..998b8be0f3 100644
--- a/packages/frontend/src/pages/games.vue
+++ b/packages/frontend/src/pages/games.vue
@@ -8,12 +8,12 @@ SPDX-License-Identifier: AGPL-3.0-only
 	<template #header><MkPageHeader/></template>
 	<MkSpacer :contentMax="800">
 		<div class="_gaps">
-			<div class="_panel">
+			<div class="_panel" :class="$style.link">
 				<MkA to="/bubble-game">
 					<img src="/client-assets/drop-and-fusion/logo.png" style="display: block; max-width: 100%; max-height: 200px; margin: auto;"/>
 				</MkA>
 			</div>
-			<div class="_panel">
+			<div class="_panel" :class="$style.link">
 				<MkA to="/reversi">
 					<img src="/client-assets/reversi/logo.png" style="display: block; max-width: 100%; max-height: 200px; margin: auto;"/>
 				</MkA>
@@ -32,3 +32,10 @@ definePageMetadata(() => ({
 	icon: 'ti ti-device-gamepad',
 }));
 </script>
+
+<style module>
+.link:focus-within {
+	outline: 2px solid var(--MI_THEME-focus);
+	outline-offset: -2px;
+}
+</style>
diff --git a/packages/frontend/src/pages/install-extensions.vue b/packages/frontend/src/pages/install-extensions.vue
index 4bee437f65..6d68ed83b4 100644
--- a/packages/frontend/src/pages/install-extensions.vue
+++ b/packages/frontend/src/pages/install-extensions.vue
@@ -8,76 +8,26 @@ SPDX-License-Identifier: AGPL-3.0-only
 	<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
 	<MkSpacer :contentMax="500">
 		<MkLoading v-if="uiPhase === 'fetching'"/>
-		<div v-else-if="uiPhase === 'confirm' && data" class="_gaps_m" :class="$style.extInstallerRoot">
-			<div :class="$style.extInstallerIconWrapper">
-				<i v-if="data.type === 'plugin'" class="ti ti-plug"></i>
-				<i v-else-if="data.type === 'theme'" class="ti ti-palette"></i>
-				<i v-else class="ti ti-download"></i>
-			</div>
-			<h2 :class="$style.extInstallerTitle">{{ i18n.ts._externalResourceInstaller[`_${data.type}`].title }}</h2>
-			<div :class="$style.extInstallerNormDesc">{{ i18n.ts._externalResourceInstaller.checkVendorBeforeInstall }}</div>
-			<MkInfo v-if="data.type === 'plugin'" :warn="true">{{ i18n.ts._plugin.installWarn }}</MkInfo>
-			<FormSection>
-				<template #label>{{ i18n.ts._externalResourceInstaller[`_${data.type}`].metaTitle }}</template>
-				<div class="_gaps_s">
-					<FormSplit>
+		<MkExtensionInstaller v-else-if="uiPhase === 'confirm' && data" :extension="data" @confirm="install()">
+			<template #additionalInfo>
+				<FormSection>
+					<template #label>{{ i18n.ts._externalResourceInstaller._vendorInfo.title }}</template>
+					<div class="_gaps_s">
 						<MkKeyValue>
-							<template #key>{{ i18n.ts.name }}</template>
-							<template #value>{{ data.meta?.name }}</template>
+							<template #key>{{ i18n.ts._externalResourceInstaller._vendorInfo.endpoint }}</template>
+							<template #value><MkUrl :url="url" :showUrlPreview="false"></MkUrl></template>
 						</MkKeyValue>
 						<MkKeyValue>
-							<template #key>{{ i18n.ts.author }}</template>
-							<template #value>{{ data.meta?.author }}</template>
+							<template #key>{{ i18n.ts._externalResourceInstaller._vendorInfo.hashVerify }}</template>
+							<template #value>
+								<!-- この画面が出ている時点でハッシュの検証には成功している -->
+								<i class="ti ti-check" style="color: var(--MI_THEME-accent)"></i>
+							</template>
 						</MkKeyValue>
-					</FormSplit>
-					<MkKeyValue v-if="data.type === 'plugin'">
-						<template #key>{{ i18n.ts.description }}</template>
-						<template #value>{{ data.meta?.description }}</template>
-					</MkKeyValue>
-					<MkKeyValue v-if="data.type === 'plugin'">
-						<template #key>{{ i18n.ts.version }}</template>
-						<template #value>{{ data.meta?.version }}</template>
-					</MkKeyValue>
-					<MkKeyValue v-if="data.type === 'plugin'">
-						<template #key>{{ i18n.ts.permission }}</template>
-						<template #value>
-							<ul :class="$style.extInstallerKVList">
-								<li v-for="permission in data.meta?.permissions" :key="permission">{{ i18n.ts._permissions[permission] }}</li>
-							</ul>
-						</template>
-					</MkKeyValue>
-					<MkKeyValue v-if="data.type === 'theme' && data.meta?.base">
-						<template #key>{{ i18n.ts._externalResourceInstaller._meta.base }}</template>
-						<template #value>{{ i18n.ts[data.meta.base] }}</template>
-					</MkKeyValue>
-					<MkFolder>
-						<template #icon><i class="ti ti-code"></i></template>
-						<template #label>{{ i18n.ts._plugin.viewSource }}</template>
-
-						<MkCode :code="data.raw ?? ''"/>
-					</MkFolder>
-				</div>
-			</FormSection>
-			<FormSection>
-				<template #label>{{ i18n.ts._externalResourceInstaller._vendorInfo.title }}</template>
-				<div class="_gaps_s">
-					<MkKeyValue>
-						<template #key>{{ i18n.ts._externalResourceInstaller._vendorInfo.endpoint }}</template>
-						<template #value><MkUrl :url="url ?? ''" :showUrlPreview="false"></MkUrl></template>
-					</MkKeyValue>
-					<MkKeyValue>
-						<template #key>{{ i18n.ts._externalResourceInstaller._vendorInfo.hashVerify }}</template>
-						<template #value>
-							<!--この画面が出ている時点でハッシュの検証には成功している-->
-							<i class="ti ti-check" style="color: var(--accent)"></i>
-						</template>
-					</MkKeyValue>
-				</div>
-			</FormSection>
-			<div class="_buttonsCenter">
-				<MkButton primary @click="install()"><i class="ti ti-check"></i> {{ i18n.ts.install }}</MkButton>
-			</div>
-		</div>
+					</div>
+				</FormSection>
+			</template>
+		</MkExtensionInstaller>
 		<div v-else-if="uiPhase === 'error'" class="_gaps_m" :class="[$style.extInstallerRoot, $style.error]">
 			<div :class="$style.extInstallerIconWrapper">
 				<i class="ti ti-circle-x"></i>
@@ -96,14 +46,11 @@ SPDX-License-Identifier: AGPL-3.0-only
 <script lang="ts" setup>
 import { ref, computed, onActivated, onDeactivated, nextTick } from 'vue';
 import MkLoading from '@/components/global/MkLoading.vue';
+import MkExtensionInstaller, { type Extension } from '@/components/MkExtensionInstaller.vue';
 import MkButton from '@/components/MkButton.vue';
-import FormSection from '@/components/form/section.vue';
-import FormSplit from '@/components/form/split.vue';
-import MkCode from '@/components/MkCode.vue';
-import MkUrl from '@/components/global/MkUrl.vue';
-import MkInfo from '@/components/MkInfo.vue';
-import MkFolder from '@/components/MkFolder.vue';
 import MkKeyValue from '@/components/MkKeyValue.vue';
+import MkUrl from '@/components/global/MkUrl.vue';
+import FormSection from '@/components/form/section.vue';
 import * as os from '@/os.js';
 import { misskeyApi } from '@/scripts/misskey-api.js';
 import { AiScriptPluginMeta, parsePluginMeta, installPlugin } from '@/scripts/install-plugin.js';
@@ -124,24 +71,7 @@ const errorKV = ref<{
 const url = ref<string | null>(null);
 const hash = ref<string | null>(null);
 
-const data = ref<{
-	type: 'plugin' | 'theme';
-	raw: string;
-	meta?: {
-		// Plugin & Theme Common
-		name: string;
-		author: string;
-
-		// Plugin
-		description?: string;
-		version?: string;
-		permissions?: string[];
-		config?: Record<string, any>;
-
-		// Theme
-		base?: 'light' | 'dark';
-	};
-} | null>(null);
+const data = ref<Extension | null>(null);
 
 function goBack(): void {
 	history.back();
@@ -227,7 +157,7 @@ async function fetch() {
 				data.value = {
 					type: 'theme',
 					meta: {
-						description,
+						// description, // 使用されていない
 						...meta,
 					},
 					raw: res.data,
@@ -320,8 +250,8 @@ definePageMetadata(() => ({
 
 <style lang="scss" module>
 .extInstallerRoot {
-	border-radius: var(--radius);
-	background: var(--panel);
+	border-radius: var(--MI-radius);
+	background: var(--MI_THEME-panel);
 	padding: 1.5rem;
 }
 
@@ -335,8 +265,8 @@ definePageMetadata(() => ({
 	margin-left: auto;
 	margin-right: auto;
 
-	background-color: var(--accentedBg);
-	color: var(--accent);
+	background-color: var(--MI_THEME-accentedBg);
+	color: var(--MI_THEME-accent);
 }
 
 .error .extInstallerIconWrapper {
@@ -353,9 +283,4 @@ definePageMetadata(() => ({
 .extInstallerNormDesc {
 	text-align: center;
 }
-
-.extInstallerKVList {
-	margin-top: 0;
-	margin-bottom: 0;
-}
 </style>
diff --git a/packages/frontend/src/pages/instance-info.vue b/packages/frontend/src/pages/instance-info.vue
index 26797ba85e..6cec3f9d45 100644
--- a/packages/frontend/src/pages/instance-info.vue
+++ b/packages/frontend/src/pages/instance-info.vue
@@ -47,9 +47,11 @@ SPDX-License-Identifier: AGPL-3.0-only
 						<MkButton v-if="suspensionState !== 'none'" :disabled="!instance" @click="resumeDelivery">{{ i18n.ts._delivery.resume }}</MkButton>
 						<MkSwitch v-model="isBlocked" :disabled="!meta || !instance" @update:modelValue="toggleBlock">{{ i18n.ts.blockThisInstance }}</MkSwitch>
 						<MkSwitch v-model="isSilenced" :disabled="!meta || !instance" @update:modelValue="toggleSilenced">{{ i18n.ts.silenceThisInstance }}</MkSwitch>
+						<MkSwitch v-model="isMediaSilenced" :disabled="!meta || !instance" @update:modelValue="toggleMediaSilenced">{{ i18n.ts.mediaSilenceThisInstance }}</MkSwitch>
 						<MkButton @click="refreshMetadata"><i class="ti ti-refresh"></i> Refresh metadata</MkButton>
 						<MkTextarea v-model="moderationNote" manualSave>
 							<template #label>{{ i18n.ts.moderationNote }}</template>
+							<template #caption>{{ i18n.ts.moderationNoteDescription }}</template>
 						</MkTextarea>
 					</div>
 				</FormSection>
@@ -133,7 +135,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 <script lang="ts" setup>
 import { ref, computed, watch } from 'vue';
 import * as Misskey from 'misskey-js';
-import MkChart from '@/components/MkChart.vue';
+import MkChart, { type ChartSrc } from '@/components/MkChart.vue';
 import MkObjectView from '@/components/MkObjectView.vue';
 import FormLink from '@/components/form/link.vue';
 import MkLink from '@/components/MkLink.vue';
@@ -149,7 +151,7 @@ import { iAmModerator, iAmAdmin } from '@/account.js';
 import { definePageMetadata } from '@/scripts/page-metadata.js';
 import { i18n } from '@/i18n.js';
 import MkUserCardMini from '@/components/MkUserCardMini.vue';
-import MkPagination from '@/components/MkPagination.vue';
+import MkPagination, { type Paging } from '@/components/MkPagination.vue';
 import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue';
 import { getProxiedImageUrlNullable } from '@/scripts/media-proxy.js';
 import { dateString } from '@/filters/date.js';
@@ -161,17 +163,18 @@ const props = defineProps<{
 
 const tab = ref('overview');
 
-const chartSrc = ref('instance-requests');
+const chartSrc = ref<ChartSrc>('instance-requests');
 const meta = ref<Misskey.entities.AdminMetaResponse | null>(null);
 const instance = ref<Misskey.entities.FederationInstance | null>(null);
 const suspensionState = ref<'none' | 'manuallySuspended' | 'goneSuspended' | 'autoSuspendedForNotResponding'>('none');
 const isBlocked = ref(false);
 const isSilenced = ref(false);
+const isMediaSilenced = ref(false);
 const faviconUrl = ref<string | null>(null);
 const moderationNote = ref('');
 
 const usersPagination = {
-	endpoint: iAmModerator ? 'admin/show-users' : 'users' as const,
+	endpoint: iAmModerator ? 'admin/show-users' : 'users',
 	limit: 10,
 	params: {
 		sort: '+updatedAt',
@@ -179,11 +182,14 @@ const usersPagination = {
 		hostname: props.host,
 	},
 	offsetMode: true,
-};
+} satisfies Paging;
 
-watch(moderationNote, async () => {
-	await misskeyApi('admin/federation/update-instance', { host: instance.value.host, moderationNote: moderationNote.value });
-});
+if (iAmModerator) {
+	watch(moderationNote, async () => {
+		if (instance.value == null) return;
+		await misskeyApi('admin/federation/update-instance', { host: instance.value.host, moderationNote: moderationNote.value });
+	});
+}
 
 async function fetch(): Promise<void> {
 	if (iAmAdmin) {
@@ -195,11 +201,13 @@ async function fetch(): Promise<void> {
 	suspensionState.value = instance.value?.suspensionState ?? 'none';
 	isBlocked.value = instance.value?.isBlocked ?? false;
 	isSilenced.value = instance.value?.isSilenced ?? false;
+	isMediaSilenced.value = instance.value?.isMediaSilenced ?? false;
 	faviconUrl.value = getProxiedImageUrlNullable(instance.value?.faviconUrl, 'preview') ?? getProxiedImageUrlNullable(instance.value?.iconUrl, 'preview');
-	moderationNote.value = instance.value?.moderationNote;
+	moderationNote.value = instance.value?.moderationNote ?? '';
 }
 
 async function toggleBlock(): Promise<void> {
+	if (!iAmAdmin) return;
 	if (!meta.value) throw new Error('No meta?');
 	if (!instance.value) throw new Error('No instance?');
 	const { host } = instance.value;
@@ -209,6 +217,7 @@ async function toggleBlock(): Promise<void> {
 }
 
 async function toggleSilenced(): Promise<void> {
+	if (!iAmAdmin) return;
 	if (!meta.value) throw new Error('No meta?');
 	if (!instance.value) throw new Error('No instance?');
 	const { host } = instance.value;
@@ -218,7 +227,19 @@ async function toggleSilenced(): Promise<void> {
 	});
 }
 
+async function toggleMediaSilenced(): Promise<void> {
+	if (!iAmAdmin) return;
+	if (!meta.value) throw new Error('No meta?');
+	if (!instance.value) throw new Error('No instance?');
+	const { host } = instance.value;
+	const mediaSilencedHosts = meta.value.mediaSilencedHosts ?? [];
+	await misskeyApi('admin/update-meta', {
+		mediaSilencedHosts: isMediaSilenced.value ? mediaSilencedHosts.concat([host]) : mediaSilencedHosts.filter(x => x !== host),
+	});
+}
+
 async function stopDelivery(): Promise<void> {
+	if (!iAmModerator) return;
 	if (!instance.value) throw new Error('No instance?');
 	suspensionState.value = 'manuallySuspended';
 	await misskeyApi('admin/federation/update-instance', {
@@ -228,6 +249,7 @@ async function stopDelivery(): Promise<void> {
 }
 
 async function resumeDelivery(): Promise<void> {
+	if (!iAmModerator) return;
 	if (!instance.value) throw new Error('No instance?');
 	suspensionState.value = 'none';
 	await misskeyApi('admin/federation/update-instance', {
@@ -237,6 +259,7 @@ async function resumeDelivery(): Promise<void> {
 }
 
 function refreshMetadata(): void {
+	if (!iAmModerator) return;
 	if (!instance.value) throw new Error('No instance?');
 	misskeyApi('admin/federation/refresh-remote-instance-metadata', {
 		host: instance.value.host,
diff --git a/packages/frontend/src/pages/invite.vue b/packages/frontend/src/pages/invite.vue
index 25e56d2b8d..3f6ae27b89 100644
--- a/packages/frontend/src/pages/invite.vue
+++ b/packages/frontend/src/pages/invite.vue
@@ -5,10 +5,8 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <template>
 <MkStickyContainer>
-	<template #header>
-		<MkPageHeader/>
-	</template>
-	<MKSpacer v-if="!instance.disableRegistration || !($i && ($i.isAdmin || $i.policies.canInvite))" :contentMax="1200">
+	<template #header><MkPageHeader/></template>
+	<MkSpacer v-if="!instance.disableRegistration || !($i && ($i.isAdmin || $i.policies.canInvite))" :contentMax="1200">
 		<div :class="$style.root">
 			<img :class="$style.img" :src="serverErrorImageUrl" class="_ghost"/>
 			<div :class="$style.text">
@@ -16,7 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 				{{ i18n.ts.nothing }}
 			</div>
 		</div>
-	</MKSpacer>
+	</MkSpacer>
 	<MkSpacer v-else :contentMax="800">
 		<div class="_gaps_m" style="text-align: center;">
 			<div v-if="resetCycle && inviteLimit">{{ i18n.tsx.inviteLimitResetCycle({ time: resetCycle, limit: inviteLimit }) }}</div>
diff --git a/packages/frontend/src/pages/list.vue b/packages/frontend/src/pages/list.vue
index 954246ff93..0ff1854154 100644
--- a/packages/frontend/src/pages/list.vue
+++ b/packages/frontend/src/pages/list.vue
@@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 <template>
 <MkStickyContainer>
 	<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
-	<MKSpacer v-if="!(typeof error === 'undefined')" :contentMax="1200">
+	<MkSpacer v-if="error != null" :contentMax="1200">
 		<div :class="$style.root">
 			<img :class="$style.img" :src="serverErrorImageUrl" class="_ghost"/>
 			<p :class="$style.text">
@@ -14,7 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 				{{ i18n.ts.nothing }}
 			</p>
 		</div>
-	</MKSpacer>
+	</MkSpacer>
 	<MkSpacer v-else-if="list" :contentMax="700" :class="$style.main">
 		<div v-if="list" class="members _margin">
 			<div :class="$style.member_text">{{ i18n.ts.members }}</div>
@@ -50,7 +50,7 @@ const props = defineProps<{
 }>();
 
 const list = ref<Misskey.entities.UserList | null>(null);
-const error = ref();
+const error = ref<unknown | null>(null);
 const users = ref<Misskey.entities.UserDetailed[]>([]);
 
 function fetchList(): void {
@@ -108,7 +108,7 @@ definePageMetadata(() => ({
 </script>
 <style lang="scss" module>
 .main {
-	min-height: calc(100cqh - (var(--stickyTop, 0px) + var(--stickyBottom, 0px)));
+	min-height: calc(100cqh - (var(--MI-stickyTop, 0px) + var(--MI-stickyBottom, 0px)));
 }
 
 .userItem {
diff --git a/packages/frontend/src/pages/lookup.vue b/packages/frontend/src/pages/lookup.vue
new file mode 100644
index 0000000000..6f10c69640
--- /dev/null
+++ b/packages/frontend/src/pages/lookup.vue
@@ -0,0 +1,97 @@
+<!--
+SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-License-Identifier: AGPL-3.0-only
+-->
+
+<template>
+<MkStickyContainer>
+	<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
+	<MkSpacer :contentMax="800">
+		<div v-if="state === 'done'" class="_buttonsCenter">
+			<MkButton @click="close">{{ i18n.ts.close }}</MkButton>
+			<MkButton @click="goToMisskey">{{ i18n.ts.goToMisskey }}</MkButton>
+		</div>
+		<div v-else class="_fullInfo">
+			<MkLoading/>
+		</div>
+	</MkSpacer>
+</MkStickyContainer>
+</template>
+
+<script lang="ts" setup>
+import { computed, ref } from 'vue';
+import * as Misskey from 'misskey-js';
+import * as os from '@/os.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
+import { i18n } from '@/i18n.js';
+import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { mainRouter } from '@/router/main.js';
+import MkButton from '@/components/MkButton.vue';
+
+const state = ref<'fetching' | 'done'>('fetching');
+
+function fetch() {
+	const params = new URL(location.href).searchParams;
+
+	// acctのほうはdeprecated
+	let uri = params.get('uri') ?? params.get('acct');
+	if (uri == null) {
+		state.value = 'done';
+		return;
+	}
+
+	let promise: Promise<unknown>;
+
+	if (uri.startsWith('https://')) {
+		promise = misskeyApi('ap/show', {
+			uri,
+		});
+		promise.then(res => {
+			if (res.type === 'User') {
+				mainRouter.replace(res.object.host ? `/@${res.object.username}@${res.object.host}` : `/@${res.object.username}`);
+			} else if (res.type === 'Note') {
+				mainRouter.replace(`/notes/${res.object.id}`);
+			} else {
+				os.alert({
+					type: 'error',
+					text: 'Not a user',
+				});
+			}
+		});
+	} else {
+		if (uri.startsWith('acct:')) {
+			uri = uri.slice(5);
+		}
+		promise = misskeyApi('users/show', Misskey.acct.parse(uri));
+		promise.then(user => {
+			mainRouter.replace(user.host ? `/@${user.username}@${user.host}` : `/@${user.username}`);
+		});
+	}
+
+	os.promiseDialog(promise, null, null, i18n.ts.fetchingAsApObject);
+}
+
+function close(): void {
+	window.close();
+
+	// 閉じなければ100ms後タイムラインに
+	window.setTimeout(() => {
+		location.href = '/';
+	}, 100);
+}
+
+function goToMisskey(): void {
+	location.href = '/';
+}
+
+fetch();
+
+const headerActions = computed(() => []);
+
+const headerTabs = computed(() => []);
+
+definePageMetadata({
+	title: i18n.ts.lookup,
+	icon: 'ti ti-world-search',
+});
+</script>
diff --git a/packages/frontend/src/pages/miauth.vue b/packages/frontend/src/pages/miauth.vue
index ffaf739ed0..e89dd5c4a5 100644
--- a/packages/frontend/src/pages/miauth.vue
+++ b/packages/frontend/src/pages/miauth.vue
@@ -4,95 +4,79 @@ SPDX-License-Identifier: AGPL-3.0-only
 -->
 
 <template>
-<MkStickyContainer>
-	<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
-	<MkSpacer :contentMax="800">
-		<div v-if="$i">
-			<div v-if="state == 'waiting'">
-				<MkLoading/>
-			</div>
-			<div v-if="state == 'denied'">
-				<p>{{ i18n.ts._auth.denied }}</p>
-			</div>
-			<div v-else-if="state == 'accepted'" class="accepted">
-				<p v-if="callback">{{ i18n.ts._auth.callback }}<MkEllipsis/></p>
-				<p v-else>{{ i18n.ts._auth.pleaseGoBack }}</p>
-			</div>
-			<div v-else>
-				<div v-if="_permissions.length > 0">
-					<p v-if="name">{{ i18n.tsx._auth.permission({ name }) }}</p>
-					<p v-else>{{ i18n.ts._auth.permissionAsk }}</p>
-					<ul>
-						<li v-for="p in _permissions" :key="p">{{ i18n.ts._permissions[p] }}</li>
-					</ul>
-				</div>
-				<div v-if="name">{{ i18n.tsx._auth.shareAccess({ name }) }}</div>
-				<div v-else>{{ i18n.ts._auth.shareAccessAsk }}</div>
-				<div :class="$style.buttons">
-					<MkButton inline @click="deny">{{ i18n.ts.cancel }}</MkButton>
-					<MkButton inline primary @click="accept">{{ i18n.ts.accept }}</MkButton>
-				</div>
-			</div>
+<div>
+	<MkAnimBg style="position: fixed; top: 0;"/>
+	<div :class="$style.formContainer">
+		<div :class="$style.form">
+			<MkAuthConfirm
+				ref="authRoot"
+				:name="name"
+				:icon="icon || undefined"
+				:permissions="_permissions"
+				@accept="onAccept"
+				@deny="onDeny"
+			>
+				<template #consentAdditionalInfo>
+					<div v-if="callback != null" class="_gaps_s" :class="$style.redirectRoot">
+						<div>{{ i18n.ts._auth.byClickingYouWillBeRedirectedToThisUrl }}</div>
+						<div class="_monospace" :class="$style.redirectUrl">{{ callback }}</div>
+					</div>
+				</template>
+			</MkAuthConfirm>
 		</div>
-		<div v-else>
-			<p :class="$style.loginMessage">{{ i18n.ts._auth.pleaseLogin }}</p>
-			<MkSignin @login="onLogin"/>
-		</div>
-	</MkSpacer>
-</MkStickyContainer>
+	</div>
+</div>
 </template>
 
 <script lang="ts" setup>
-import { ref, computed } from 'vue';
-import MkSignin from '@/components/MkSignin.vue';
-import MkButton from '@/components/MkButton.vue';
-import { misskeyApi } from '@/scripts/misskey-api.js';
-import { $i, login } from '@/account.js';
+import { computed, useTemplateRef } from 'vue';
+import * as Misskey from 'misskey-js';
+
+import MkAnimBg from '@/components/MkAnimBg.vue';
+import MkAuthConfirm from '@/components/MkAuthConfirm.vue';
+
 import { i18n } from '@/i18n.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
 import { definePageMetadata } from '@/scripts/page-metadata.js';
 
 const props = defineProps<{
 	session: string;
 	callback?: string;
-	name: string;
-	icon: string;
-	permission: string; // コンマ区切り
+	name?: string;
+	icon?: string;
+	permission?: string; // コンマ区切り
 }>();
 
-const _permissions = props.permission ? props.permission.split(',') : [];
+const _permissions = computed(() => {
+	return (props.permission ? props.permission.split(',').filter((p): p is typeof Misskey.permissions[number] => (Misskey.permissions as readonly string[]).includes(p)) : []);
+});
 
-const state = ref<string | null>(null);
+const authRoot = useTemplateRef('authRoot');
 
-async function accept(): Promise<void> {
-	state.value = 'waiting';
+async function onAccept(token: string) {
 	await misskeyApi('miauth/gen-token', {
 		session: props.session,
 		name: props.name,
 		iconUrl: props.icon,
-		permission: _permissions,
+		permission: _permissions.value,
+	}, token).catch(() => {
+		authRoot.value?.showUI('failed');
 	});
 
-	state.value = 'accepted';
-	if (props.callback) {
+	if (props.callback && props.callback !== '') {
 		const cbUrl = new URL(props.callback);
 		if (['javascript:', 'file:', 'data:', 'mailto:', 'tel:'].includes(cbUrl.protocol)) throw new Error('invalid url');
 		cbUrl.searchParams.set('session', props.session);
-		location.href = cbUrl.href;
+		location.href = cbUrl.toString();
+	} else {
+		authRoot.value?.showUI('success');
 	}
 }
 
-function deny(): void {
-	state.value = 'denied';
+function onDeny() {
+	authRoot.value?.showUI('denied');
 }
 
-function onLogin(res): void {
-	login(res.i);
-}
-
-const headerActions = computed(() => []);
-
-const headerTabs = computed(() => []);
-
 definePageMetadata(() => ({
 	title: 'MiAuth',
 	icon: 'ti ti-apps',
@@ -100,15 +84,38 @@ definePageMetadata(() => ({
 </script>
 
 <style lang="scss" module>
-.buttons {
-	margin-top: 16px;
-	display: flex;
-	gap: 8px;
-	flex-wrap: wrap;
+.formContainer {
+	min-height: 100svh;
+	padding: 32px 32px calc(env(safe-area-inset-bottom, 0px) + 32px) 32px;
+	box-sizing: border-box;
+	display: grid;
+	place-content: center;
 }
 
-.loginMessage {
-	text-align: center;
-	margin: 8px 0 24px;
+.form {
+	position: relative;
+	z-index: 10;
+	border-radius: var(--MI-radius);
+	background-color: var(--MI_THEME-panel);
+	box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1);
+	overflow: clip;
+	max-width: 500px;
+	width: calc(100vw - 64px);
+	height: min(65svh, calc(100svh - calc(env(safe-area-inset-bottom, 0px) + 64px)));
+	overflow-y: scroll;
+}
+
+.redirectRoot {
+	padding: 16px;
+	border-radius: var(--MI-radius);
+	background-color: var(--MI_THEME-bg);
+}
+
+.redirectUrl {
+	font-size: 90%;
+	padding: 12px;
+	border-radius: var(--MI-radius);
+	background-color: var(--MI_THEME-panel);
+	overflow-x: scroll;
 }
 </style>
diff --git a/packages/frontend/src/pages/my-antennas/create.vue b/packages/frontend/src/pages/my-antennas/create.vue
index 2d026d2fa9..2b8518747f 100644
--- a/packages/frontend/src/pages/my-antennas/create.vue
+++ b/packages/frontend/src/pages/my-antennas/create.vue
@@ -4,43 +4,33 @@ SPDX-License-Identifier: AGPL-3.0-only
 -->
 
 <template>
-<div>
-	<XAntenna :antenna="draft" @created="onAntennaCreated"/>
-</div>
+<MkStickyContainer>
+	<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
+
+	<MkAntennaEditor @created="onAntennaCreated"/>
+</MkStickyContainer>
 </template>
 
 <script lang="ts" setup>
-import { ref } from 'vue';
-import XAntenna from './editor.vue';
+import { computed } from 'vue';
 import { i18n } from '@/i18n.js';
 import { definePageMetadata } from '@/scripts/page-metadata.js';
 import { antennasCache } from '@/cache.js';
 import { useRouter } from '@/router/supplier.js';
+import MkAntennaEditor from '@/components/MkAntennaEditor.vue';
 
 const router = useRouter();
 
-const draft = ref({
-	name: '',
-	src: 'all',
-	userListId: null,
-	users: [],
-	keywords: [],
-	excludeKeywords: [],
-	excludeBots: false,
-	withReplies: false,
-	caseSensitive: false,
-	localOnly: false,
-	withFile: false,
-	notify: false,
-});
-
 function onAntennaCreated() {
 	antennasCache.delete();
 	router.push('/my/antennas');
 }
 
+const headerActions = computed(() => []);
+const headerTabs = computed(() => []);
+
 definePageMetadata(() => ({
-	title: i18n.ts.manageAntennas,
+	title: i18n.ts.createAntenna,
 	icon: 'ti ti-antenna',
 }));
 </script>
diff --git a/packages/frontend/src/pages/my-antennas/edit.vue b/packages/frontend/src/pages/my-antennas/edit.vue
index 9471be8575..9f927cd1a0 100644
--- a/packages/frontend/src/pages/my-antennas/edit.vue
+++ b/packages/frontend/src/pages/my-antennas/edit.vue
@@ -4,15 +4,17 @@ SPDX-License-Identifier: AGPL-3.0-only
 -->
 
 <template>
-<div class="">
-	<XAntenna v-if="antenna" :antenna="antenna" @updated="onAntennaUpdated"/>
-</div>
+<MkStickyContainer>
+	<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
+
+	<MkAntennaEditor v-if="antenna" :antenna="antenna" @updated="onAntennaUpdated"/>
+</MkStickyContainer>
 </template>
 
 <script lang="ts" setup>
-import { ref } from 'vue';
+import { ref, computed } from 'vue';
 import * as Misskey from 'misskey-js';
-import XAntenna from './editor.vue';
+import MkAntennaEditor from '@/components/MkAntennaEditor.vue';
 import { misskeyApi } from '@/scripts/misskey-api.js';
 import { i18n } from '@/i18n.js';
 import { definePageMetadata } from '@/scripts/page-metadata.js';
@@ -36,8 +38,11 @@ misskeyApi('antennas/show', { antennaId: props.antennaId }).then((antennaRespons
 	antenna.value = antennaResponse;
 });
 
+const headerActions = computed(() => []);
+const headerTabs = computed(() => []);
+
 definePageMetadata(() => ({
-	title: i18n.ts.manageAntennas,
+	title: i18n.ts.editAntenna,
 	icon: 'ti ti-antenna',
 }));
 </script>
diff --git a/packages/frontend/src/pages/my-antennas/index.vue b/packages/frontend/src/pages/my-antennas/index.vue
index 21c96348f0..f387740728 100644
--- a/packages/frontend/src/pages/my-antennas/index.vue
+++ b/packages/frontend/src/pages/my-antennas/index.vue
@@ -73,11 +73,11 @@ onActivated(() => {
 .antenna {
 	display: block;
 	padding: 16px;
-	border: solid 1px var(--divider);
+	border: solid 1px var(--MI_THEME-divider);
 	border-radius: 6px;
 
 	&:hover {
-		border: solid 1px var(--accent);
+		border: solid 1px var(--MI_THEME-accent);
 		text-decoration: none;
 	}
 }
diff --git a/packages/frontend/src/pages/my-clips/index.vue b/packages/frontend/src/pages/my-clips/index.vue
index 1a0d7177fc..acf37a9a2f 100644
--- a/packages/frontend/src/pages/my-clips/index.vue
+++ b/packages/frontend/src/pages/my-clips/index.vue
@@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 				<MkButton primary rounded class="add" @click="create"><i class="ti ti-plus"></i> {{ i18n.ts.add }}</MkButton>
 
 				<MkPagination v-slot="{ items }" ref="pagingComponent" :pagination="pagination" class="_gaps">
-					<MkClipPreview v-for="item in items" :key="item.id" :clip="item"/>
+					<MkClipPreview v-for="item in items" :key="item.id" :clip="item" :noUserInfo="true"/>
 				</MkPagination>
 			</div>
 			<div v-else-if="tab === 'favorites'" key="favorites" class="_gaps">
@@ -77,15 +77,15 @@ async function create() {
 
 	clipsCache.delete();
 
-	pagingComponent.value.reload();
+	pagingComponent.value?.reload();
 }
 
 function onClipCreated() {
-	pagingComponent.value.reload();
+	pagingComponent.value?.reload();
 }
 
 function onClipDeleted() {
-	pagingComponent.value.reload();
+	pagingComponent.value?.reload();
 }
 
 const headerActions = computed(() => []);
diff --git a/packages/frontend/src/pages/my-lists/index.vue b/packages/frontend/src/pages/my-lists/index.vue
index 82fde284c1..6cbcca73c2 100644
--- a/packages/frontend/src/pages/my-lists/index.vue
+++ b/packages/frontend/src/pages/my-lists/index.vue
@@ -85,12 +85,12 @@ onActivated(() => {
 .list {
 	display: block;
 	padding: 16px;
-	border: solid 1px var(--divider);
+	border: solid 1px var(--MI_THEME-divider);
 	border-radius: 6px;
 	margin-bottom: 8px;
 
 	&:hover {
-		border: solid 1px var(--accent);
+		border: solid 1px var(--MI_THEME-accent);
 		text-decoration: none;
 	}
 }
diff --git a/packages/frontend/src/pages/my-lists/list.vue b/packages/frontend/src/pages/my-lists/list.vue
index 7492b099ea..69e404bd85 100644
--- a/packages/frontend/src/pages/my-lists/list.vue
+++ b/packages/frontend/src/pages/my-lists/list.vue
@@ -110,7 +110,7 @@ function addUser() {
 			listId: list.value.id,
 			userId: user.id,
 		}).then(() => {
-			paginationEl.value.reload();
+			paginationEl.value?.reload();
 		});
 	});
 }
@@ -126,29 +126,34 @@ async function removeUser(item, ev) {
 				listId: list.value.id,
 				userId: item.userId,
 			}).then(() => {
-				paginationEl.value.removeItem(item.id);
+				paginationEl.value?.removeItem(item.id);
 			});
 		},
 	}], ev.currentTarget ?? ev.target);
 }
 
 async function showMembershipMenu(item, ev) {
+	const withRepliesRef = ref(item.withReplies);
+
 	os.popupMenu([{
-		text: item.withReplies ? i18n.ts.hideRepliesToOthersInTimeline : i18n.ts.showRepliesToOthersInTimeline,
-		icon: item.withReplies ? 'ti ti-messages-off' : 'ti ti-messages',
-		action: async () => {
-			misskeyApi('users/lists/update-membership', {
-				listId: list.value.id,
-				userId: item.userId,
-				withReplies: !item.withReplies,
-			}).then(() => {
-				paginationEl.value.updateItem(item.id, (old) => ({
-					...old,
-					withReplies: !item.withReplies,
-				}));
-			});
-		},
+		type: 'switch',
+		text: i18n.ts.showRepliesToOthersInTimeline,
+		icon: 'ti ti-messages',
+		ref: withRepliesRef,
 	}], ev.currentTarget ?? ev.target);
+
+	watch(withRepliesRef, withReplies => {
+		misskeyApi('users/lists/update-membership', {
+			listId: list.value!.id,
+			userId: item.userId,
+			withReplies,
+		}).then(() => {
+			paginationEl.value!.updateItem(item.id, (old) => ({
+				...old,
+				withReplies,
+			}));
+		});
+	});
 }
 
 async function deleteList() {
@@ -194,7 +199,7 @@ definePageMetadata(() => ({
 
 <style lang="scss" module>
 .main {
-	min-height: calc(100cqh - (var(--stickyTop, 0px) + var(--stickyBottom, 0px)));
+	min-height: calc(100cqh - (var(--MI-stickyTop, 0px) + var(--MI-stickyBottom, 0px)));
 }
 
 .userItem {
@@ -229,8 +234,8 @@ definePageMetadata(() => ({
 }
 
 .footer {
-	-webkit-backdrop-filter: var(--blur, blur(15px));
-	backdrop-filter: var(--blur, blur(15px));
-	border-top: solid 0.5px var(--divider);
+	-webkit-backdrop-filter: var(--MI-blur, blur(15px));
+	backdrop-filter: var(--MI-blur, blur(15px));
+	border-top: solid 0.5px var(--MI_THEME-divider);
 }
 </style>
diff --git a/packages/frontend/src/pages/not-found.vue b/packages/frontend/src/pages/not-found.vue
index 93a792c42f..6a2d01b6fa 100644
--- a/packages/frontend/src/pages/not-found.vue
+++ b/packages/frontend/src/pages/not-found.vue
@@ -24,7 +24,7 @@ const props = defineProps<{
 }>();
 
 if (props.showLoginPopup) {
-	pleaseLogin('/');
+	pleaseLogin({ path: '/' });
 }
 
 const headerActions = computed(() => []);
diff --git a/packages/frontend/src/pages/note.vue b/packages/frontend/src/pages/note.vue
index 97f32d35cd..454ee3c6bc 100644
--- a/packages/frontend/src/pages/note.vue
+++ b/packages/frontend/src/pages/note.vue
@@ -61,6 +61,7 @@ import { i18n } from '@/i18n.js';
 import { dateString } from '@/filters/date.js';
 import MkClipPreview from '@/components/MkClipPreview.vue';
 import { defaultStore } from '@/store.js';
+import { pleaseLogin } from '@/scripts/please-login.js';
 
 const props = defineProps<{
 	noteId: string;
@@ -128,6 +129,11 @@ function fetchNote() {
 			});
 		}
 	}).catch(err => {
+		if (err.id === '8e75455b-738c-471d-9f80-62693f33372e') {
+			pleaseLogin({
+				message: i18n.ts.thisContentsAreMarkedAsSigninRequiredByAuthor,
+			});
+		}
 		error.value = err;
 	});
 }
@@ -170,11 +176,11 @@ definePageMetadata(() => ({
 }
 
 .loadNext {
-	margin-bottom: var(--margin);
+	margin-bottom: var(--MI-margin);
 }
 
 .loadPrev {
-	margin-top: var(--margin);
+	margin-top: var(--MI-margin);
 }
 
 .loadButton {
@@ -182,7 +188,7 @@ definePageMetadata(() => ({
 }
 
 .note {
-	border-radius: var(--radius);
-	background: var(--panel);
+	border-radius: var(--MI-radius);
+	background: var(--MI_THEME-panel);
 }
 </style>
diff --git a/packages/frontend/src/pages/notifications.vue b/packages/frontend/src/pages/notifications.vue
index 28f5838296..46ee501c76 100644
--- a/packages/frontend/src/pages/notifications.vue
+++ b/packages/frontend/src/pages/notifications.vue
@@ -30,7 +30,7 @@ import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue';
 import * as os from '@/os.js';
 import { i18n } from '@/i18n.js';
 import { definePageMetadata } from '@/scripts/page-metadata.js';
-import { notificationTypes } from '@/const.js';
+import { notificationTypes } from '@@/js/const.js';
 
 const tab = ref('all');
 const includeTypes = ref<string[] | null>(null);
@@ -102,7 +102,7 @@ definePageMetadata(() => ({
 
 <style module lang="scss">
 .notifications {
-	border-radius: var(--radius);
+	border-radius: var(--MI-radius);
 	overflow: clip;
 }
 </style>
diff --git a/packages/frontend/src/pages/oauth.vue b/packages/frontend/src/pages/oauth.vue
index 733e34eb2c..8719a769e5 100644
--- a/packages/frontend/src/pages/oauth.vue
+++ b/packages/frontend/src/pages/oauth.vue
@@ -4,40 +4,28 @@ SPDX-License-Identifier: AGPL-3.0-only
 -->
 
 <template>
-<MkStickyContainer>
-	<template #header><MkPageHeader/></template>
-	<MkSpacer :contentMax="800">
-		<div v-if="$i">
-			<div v-if="permissions.length > 0">
-				<p v-if="name">{{ i18n.tsx._auth.permission({ name }) }}</p>
-				<p v-else>{{ i18n.ts._auth.permissionAsk }}</p>
-				<ul>
-					<li v-for="p in permissions" :key="p">{{ i18n.ts._permissions[p] }}</li>
-				</ul>
-			</div>
-			<div v-if="name">{{ i18n.tsx._auth.shareAccess({ name }) }}</div>
-			<div v-else>{{ i18n.ts._auth.shareAccessAsk }}</div>
-			<form :class="$style.buttons" action="/oauth/decision" accept-charset="utf-8" method="post">
-				<input name="login_token" type="hidden" :value="$i.token"/>
-				<input name="transaction_id" type="hidden" :value="transactionIdMeta?.content"/>
-				<MkButton inline name="cancel" value="cancel">{{ i18n.ts.cancel }}</MkButton>
-				<MkButton inline primary>{{ i18n.ts.accept }}</MkButton>
-			</form>
+<div>
+	<MkAnimBg style="position: fixed; top: 0;"/>
+	<div :class="$style.formContainer">
+		<div :class="$style.form">
+			<MkAuthConfirm
+				ref="authRoot"
+				:name="name"
+				:permissions="permissions"
+				:waitOnDeny="true"
+				@accept="onAccept"
+				@deny="onDeny"
+			/>
 		</div>
-		<div v-else>
-			<p :class="$style.loginMessage">{{ i18n.ts._auth.pleaseLogin }}</p>
-			<MkSignin @login="onLogin"/>
-		</div>
-	</MkSpacer>
-</MkStickyContainer>
+	</div>
+</div>
 </template>
 
 <script lang="ts" setup>
-import MkSignin from '@/components/MkSignin.vue';
-import MkButton from '@/components/MkButton.vue';
-import { $i, login } from '@/account.js';
-import { i18n } from '@/i18n.js';
+import * as Misskey from 'misskey-js';
+import MkAnimBg from '@/components/MkAnimBg.vue';
 import { definePageMetadata } from '@/scripts/page-metadata.js';
+import MkAuthConfirm from '@/components/MkAuthConfirm.vue';
 
 const transactionIdMeta = document.querySelector<HTMLMetaElement>('meta[name="misskey:oauth:transaction-id"]');
 if (transactionIdMeta) {
@@ -45,10 +33,44 @@ if (transactionIdMeta) {
 }
 
 const name = document.querySelector<HTMLMetaElement>('meta[name="misskey:oauth:client-name"]')?.content;
-const permissions = document.querySelector<HTMLMetaElement>('meta[name="misskey:oauth:scope"]')?.content.split(' ') ?? [];
+const permissions = document.querySelector<HTMLMetaElement>('meta[name="misskey:oauth:scope"]')?.content.split(' ').filter((p): p is typeof Misskey.permissions[number] => (Misskey.permissions as readonly string[]).includes(p)) ?? [];
 
-function onLogin(res): void {
-	login(res.i);
+function doPost(token: string, decision: 'accept' | 'deny') {
+	const form = document.createElement('form');
+	form.action = '/oauth/decision';
+	form.method = 'post';
+	form.acceptCharset = 'utf-8';
+
+	const loginToken = document.createElement('input');
+	loginToken.type = 'hidden';
+	loginToken.name = 'login_token';
+	loginToken.value = token;
+	form.appendChild(loginToken);
+
+	const transactionId = document.createElement('input');
+	transactionId.type = 'hidden';
+	transactionId.name = 'transaction_id';
+	transactionId.value = transactionIdMeta?.content ?? '';
+	form.appendChild(transactionId);
+
+	if (decision === 'deny') {
+		const cancel = document.createElement('input');
+		cancel.type = 'hidden';
+		cancel.name = 'cancel';
+		cancel.value = 'cancel';
+		form.appendChild(cancel);
+	}
+
+	document.body.appendChild(form);
+	form.submit();
+}
+
+function onAccept(token: string) {
+	doPost(token, 'accept');
+}
+
+function onDeny(token: string) {
+	doPost(token, 'deny');
 }
 
 definePageMetadata(() => ({
@@ -58,15 +80,24 @@ definePageMetadata(() => ({
 </script>
 
 <style lang="scss" module>
-.buttons {
-	margin-top: 16px;
-	display: flex;
-	gap: 8px;
-	flex-wrap: wrap;
+.formContainer {
+	min-height: 100svh;
+	padding: 32px 32px calc(env(safe-area-inset-bottom, 0px) + 32px) 32px;
+	box-sizing: border-box;
+	display: grid;
+	place-content: center;
 }
 
-.loginMessage {
-	text-align: center;
-	margin: 8px 0 24px;
+.form {
+	position: relative;
+	z-index: 10;
+	border-radius: var(--MI-radius);
+	background-color: var(--MI_THEME-panel);
+	box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1);
+	overflow: clip;
+	max-width: 500px;
+	width: calc(100vw - 64px);
+	height: min(65svh, calc(100svh - calc(env(safe-area-inset-bottom, 0px) + 64px)));
+	overflow-y: scroll;
 }
 </style>
diff --git a/packages/frontend/src/pages/page-editor/common.ts b/packages/frontend/src/pages/page-editor/common.ts
new file mode 100644
index 0000000000..420c8fc967
--- /dev/null
+++ b/packages/frontend/src/pages/page-editor/common.ts
@@ -0,0 +1,15 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { i18n } from '@/i18n.js';
+
+export function getPageBlockList() {
+	return [
+		{ value: 'section', text: i18n.ts._pages.blocks.section },
+		{ value: 'text', text: i18n.ts._pages.blocks.text },
+		{ value: 'image', text: i18n.ts._pages.blocks.image },
+		{ value: 'note', text: i18n.ts._pages.blocks.note },
+	];
+}
diff --git a/packages/frontend/src/pages/page-editor/els/page-editor.el.image.vue b/packages/frontend/src/pages/page-editor/els/page-editor.el.image.vue
index 1cfe7a6d2d..c3ad6657b0 100644
--- a/packages/frontend/src/pages/page-editor/els/page-editor.el.image.vue
+++ b/packages/frontend/src/pages/page-editor/els/page-editor.el.image.vue
@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <template>
 <!-- eslint-disable vue/no-mutating-props -->
-<XContainer :draggable="true" @remove="() => $emit('remove')">
+<XContainer :draggable="true" @remove="() => emit('remove')">
 	<template #header><i class="ti ti-photo"></i> {{ i18n.ts._pages.blocks.image }}</template>
 	<template #func>
 		<button @click="choose()">
@@ -30,11 +30,12 @@ import { misskeyApi } from '@/scripts/misskey-api.js';
 import { i18n } from '@/i18n.js';
 
 const props = defineProps<{
-	modelValue: any
+	modelValue: Misskey.entities.PageBlock & { type: 'image' };
 }>();
 
 const emit = defineEmits<{
-	(ev: 'update:modelValue', value: any): void;
+	(ev: 'update:modelValue', value: Misskey.entities.PageBlock & { type: 'image' }): void;
+	(ev: 'remove'): void;
 }>();
 
 const file = ref<Misskey.entities.DriveFile | null>(null);
diff --git a/packages/frontend/src/pages/page-editor/els/page-editor.el.note.vue b/packages/frontend/src/pages/page-editor/els/page-editor.el.note.vue
index 0a28386986..36e03b4790 100644
--- a/packages/frontend/src/pages/page-editor/els/page-editor.el.note.vue
+++ b/packages/frontend/src/pages/page-editor/els/page-editor.el.note.vue
@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <template>
 <!-- eslint-disable vue/no-mutating-props -->
-<XContainer :draggable="true" @remove="() => $emit('remove')">
+<XContainer :draggable="true" @remove="() => emit('remove')">
 	<template #header><i class="ti ti-note"></i> {{ i18n.ts._pages.blocks.note }}</template>
 
 	<section style="padding: 16px;" class="_gaps_s">
@@ -34,19 +34,24 @@ import { misskeyApi } from '@/scripts/misskey-api.js';
 import { i18n } from '@/i18n.js';
 
 const props = defineProps<{
-	modelValue: any
+	modelValue: Misskey.entities.PageBlock & { type: 'note' };
 }>();
 
 const emit = defineEmits<{
-	(ev: 'update:modelValue', value: any): void;
+	(ev: 'update:modelValue', value: Misskey.entities.PageBlock & { type: 'note' }): void;
 }>();
 
-const id = ref<any>(props.modelValue.note);
+const id = ref(props.modelValue.note);
 const note = ref<Misskey.entities.Note | null>(null);
 
 watch(id, async () => {
 	if (id.value && (id.value.startsWith('http://') || id.value.startsWith('https://'))) {
-		id.value = (id.value.endsWith('/') ? id.value.slice(0, -1) : id.value).split('/').pop();
+		id.value = (id.value.endsWith('/') ? id.value.slice(0, -1) : id.value).split('/').pop() ?? null;
+	}
+
+	if (!id.value) {
+		note.value = null;
+		return;
 	}
 
 	emit('update:modelValue', {
diff --git a/packages/frontend/src/pages/page-editor/els/page-editor.el.section.vue b/packages/frontend/src/pages/page-editor/els/page-editor.el.section.vue
index 47e9c08c2c..3fed07f7e8 100644
--- a/packages/frontend/src/pages/page-editor/els/page-editor.el.section.vue
+++ b/packages/frontend/src/pages/page-editor/els/page-editor.el.section.vue
@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <template>
 <!-- eslint-disable vue/no-mutating-props -->
-<XContainer :draggable="true" @remove="() => $emit('remove')">
+<XContainer :draggable="true" @remove="() => emit('remove')">
 	<template #header><i class="ti ti-note"></i> {{ props.modelValue.title }}</template>
 	<template #func>
 		<button class="_button" @click="rename()">
@@ -21,25 +21,26 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-/* eslint-disable vue/no-mutating-props */
+ 
 import { defineAsyncComponent, inject, onMounted, watch, ref } from 'vue';
+import * as Misskey from 'misskey-js';
 import { v4 as uuid } from 'uuid';
 import XContainer from '../page-editor.container.vue';
 import * as os from '@/os.js';
 import { i18n } from '@/i18n.js';
 import { deepClone } from '@/scripts/clone.js';
 import MkButton from '@/components/MkButton.vue';
+import { getPageBlockList } from '@/pages/page-editor/common.js';
 
 const XBlocks = defineAsyncComponent(() => import('../page-editor.blocks.vue'));
 
-const props = withDefaults(defineProps<{
-	modelValue: any,
-}>(), {
-	modelValue: {},
-});
+const props = defineProps<{
+	modelValue: Misskey.entities.PageBlock & { type: 'section'; },
+}>();
 
 const emit = defineEmits<{
-	(ev: 'update:modelValue', value: any): void;
+	(ev: 'update:modelValue', value: Misskey.entities.PageBlock & { type: 'section' }): void;
+	(ev: 'remove'): void;
 }>();
 
 const children = ref(deepClone(props.modelValue.children ?? []));
@@ -53,11 +54,9 @@ watch(children, () => {
 	deep: true,
 });
 
-const getPageBlockList = inject<(any) => any>('getPageBlockList');
-
 async function rename() {
 	const { canceled, result: title } = await os.inputText({
-		title: 'Enter title',
+		title: i18n.ts._pages.enterSectionTitle,
 		default: props.modelValue.title,
 	});
 	if (canceled) return;
diff --git a/packages/frontend/src/pages/page-editor/els/page-editor.el.text.vue b/packages/frontend/src/pages/page-editor/els/page-editor.el.text.vue
index 14c3e6845e..5795b46c00 100644
--- a/packages/frontend/src/pages/page-editor/els/page-editor.el.text.vue
+++ b/packages/frontend/src/pages/page-editor/els/page-editor.el.text.vue
@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <template>
 <!-- eslint-disable vue/no-mutating-props -->
-<XContainer :draggable="true" @remove="() => $emit('remove')">
+<XContainer :draggable="true" @remove="() => emit('remove')">
 	<template #header><i class="ti ti-align-left"></i> {{ i18n.ts._pages.blocks.text }}</template>
 
 	<section>
@@ -15,18 +15,19 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-/* eslint-disable vue/no-mutating-props */
+ 
 import { watch, ref, shallowRef, onMounted, onUnmounted } from 'vue';
+import * as Misskey from 'misskey-js';
 import XContainer from '../page-editor.container.vue';
 import { i18n } from '@/i18n.js';
 import { Autocomplete } from '@/scripts/autocomplete.js';
 
 const props = defineProps<{
-	modelValue: any
+	modelValue: Misskey.entities.PageBlock & { type: 'text' }
 }>();
 
 const emit = defineEmits<{
-	(ev: 'update:modelValue', value: any): void;
+	(ev: 'update:modelValue', value: Misskey.entities.PageBlock & { type: 'text' }): void;
 }>();
 
 let autocomplete: Autocomplete;
@@ -63,7 +64,7 @@ onUnmounted(() => {
 	box-shadow: none;
 	padding: 16px;
 	background: transparent;
-	color: var(--fg);
+	color: var(--MI_THEME-fg);
 	font-size: 14px;
 	box-sizing: border-box;
 }
diff --git a/packages/frontend/src/pages/page-editor/page-editor.blocks.vue b/packages/frontend/src/pages/page-editor/page-editor.blocks.vue
index 4967e73000..f191320180 100644
--- a/packages/frontend/src/pages/page-editor/page-editor.blocks.vue
+++ b/packages/frontend/src/pages/page-editor/page-editor.blocks.vue
@@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 -->
 
 <template>
-<Sortable :modelValue="modelValue" tag="div" itemKey="id" handle=".drag-handle" :group="{ name: 'blocks' }" :animation="150" :swapThreshold="0.5" @update:modelValue="v => $emit('update:modelValue', v)">
+<Sortable :modelValue="modelValue" tag="div" itemKey="id" handle=".drag-handle" :group="{ name: 'blocks' }" :animation="150" :swapThreshold="0.5" @update:modelValue="v => emit('update:modelValue', v)">
 	<template #item="{element}">
 		<div :class="$style.item">
 			<!-- divが無いとエラーになる https://github.com/SortableJS/vue.draggable.next/issues/189 -->
diff --git a/packages/frontend/src/pages/page-editor/page-editor.container.vue b/packages/frontend/src/pages/page-editor/page-editor.container.vue
index f2081c452c..a96c2c2a77 100644
--- a/packages/frontend/src/pages/page-editor/page-editor.container.vue
+++ b/packages/frontend/src/pages/page-editor/page-editor.container.vue
@@ -60,12 +60,12 @@ function remove() {
 .cpjygsrt {
 	position: relative;
 	overflow: hidden;
-	background: var(--panel);
-	border: solid 2px var(--X12);
+	background: var(--MI_THEME-panel);
+	border: solid 2px var(--MI_THEME-X12);
 	border-radius: 8px;
 
 	&:hover {
-		border: solid 2px var(--X13);
+		border: solid 2px var(--MI_THEME-X13);
 	}
 
 	&.warn {
diff --git a/packages/frontend/src/pages/page-editor/page-editor.vue b/packages/frontend/src/pages/page-editor/page-editor.vue
index af32fd2274..ddb808390c 100644
--- a/packages/frontend/src/pages/page-editor/page-editor.vue
+++ b/packages/frontend/src/pages/page-editor/page-editor.vue
@@ -69,7 +69,7 @@ import MkButton from '@/components/MkButton.vue';
 import MkSelect from '@/components/MkSelect.vue';
 import MkSwitch from '@/components/MkSwitch.vue';
 import MkInput from '@/components/MkInput.vue';
-import { url } from '@/config.js';
+import { url } from '@@/js/config.js';
 import * as os from '@/os.js';
 import { misskeyApi } from '@/scripts/misskey-api.js';
 import { selectFile } from '@/scripts/select-file.js';
@@ -77,6 +77,7 @@ import { i18n } from '@/i18n.js';
 import { definePageMetadata } from '@/scripts/page-metadata.js';
 import { $i } from '@/account.js';
 import { mainRouter } from '@/router/main.js';
+import { getPageBlockList } from '@/pages/page-editor/common.js';
 
 const props = defineProps<{
 	initPageId?: string;
@@ -101,7 +102,6 @@ const alignCenter = ref(false);
 const hideTitleWhenPinned = ref(false);
 
 provide('readonly', readonly.value);
-provide('getPageBlockList', getPageBlockList);
 
 watch(eyeCatchingImageId, async () => {
 	if (eyeCatchingImageId.value == null) {
@@ -216,15 +216,6 @@ async function add() {
 	content.value.push({ id, type });
 }
 
-function getPageBlockList() {
-	return [
-		{ value: 'section', text: i18n.ts._pages.blocks.section },
-		{ value: 'text', text: i18n.ts._pages.blocks.text },
-		{ value: 'image', text: i18n.ts._pages.blocks.image },
-		{ value: 'note', text: i18n.ts._pages.blocks.note },
-	];
-}
-
 function setEyeCatchingImage(img) {
 	selectFile(img.currentTarget ?? img.target, null).then(file => {
 		eyeCatchingImageId.value = file.id;
diff --git a/packages/frontend/src/pages/page.vue b/packages/frontend/src/pages/page.vue
index e73d032000..a1bec52f18 100644
--- a/packages/frontend/src/pages/page.vue
+++ b/packages/frontend/src/pages/page.vue
@@ -62,8 +62,10 @@ SPDX-License-Identifier: AGPL-3.0-only
 							<MkButton v-else v-tooltip="i18n.ts._pages.like" class="button" asLike @click="like()"><i class="ti ti-heart"></i><span v-if="page.likedCount > 0" class="count">{{ page.likedCount }}</span></MkButton>
 						</div>
 						<div :class="$style.other">
+							<MkA v-if="page.userId === $i?.id" v-tooltip="i18n.ts._pages.editThisPage" :to="`/pages/edit/${page.id}`" class="_button" :class="$style.generalActionButton"><i class="ti ti-pencil ti-fw"></i></MkA>
 							<button v-tooltip="i18n.ts.copyLink" class="_button" :class="$style.generalActionButton" @click="copyLink"><i class="ti ti-link ti-fw"></i></button>
 							<button v-tooltip="i18n.ts.share" class="_button" :class="$style.generalActionButton" @click="share"><i class="ti ti-share ti-fw"></i></button>
+							<button v-if="$i" v-click-anime class="_button" :class="$style.generalActionButton" @mousedown="showMenu"><i class="ti ti-dots ti-fw"></i></button>
 						</div>
 					</div>
 					<div :class="$style.pageUser">
@@ -78,14 +80,6 @@ SPDX-License-Identifier: AGPL-3.0-only
 						<div><i class="ti ti-clock"></i> {{ i18n.ts.createdAt }}: <MkTime :time="page.createdAt" mode="detail"/></div>
 						<div v-if="page.createdAt != page.updatedAt"><i class="ti ti-clock-edit"></i> {{ i18n.ts.updatedAt }}: <MkTime :time="page.updatedAt" mode="detail"/></div>
 					</div>
-					<div :class="$style.pageLinks">
-						<MkA v-if="!$i || $i.id !== page.userId" :to="`/@${username}/pages/${pageName}/view-source`" class="link">{{ i18n.ts._pages.viewSource }}</MkA>
-						<template v-if="$i && $i.id === page.userId">
-							<MkA :to="`/pages/edit/${page.id}`" class="link">{{ i18n.ts._pages.editThisPage }}</MkA>
-							<button v-if="$i.pinnedPageId === page.id" class="link _textButton" @click="pin(false)">{{ i18n.ts.unpin }}</button>
-							<button v-else class="link _textButton" @click="pin(true)">{{ i18n.ts.pin }}</button>
-						</template>
-					</div>
 				</div>
 				<MkAd :prefer="['horizontal', 'horizontal-big']"/>
 				<MkContainer :max-height="300" :foldable="true" class="other">
@@ -104,13 +98,13 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { computed, watch, ref } from 'vue';
+import { computed, watch, ref, defineAsyncComponent } from 'vue';
 import * as Misskey from 'misskey-js';
 import XPage from '@/components/page/page.vue';
 import MkButton from '@/components/MkButton.vue';
 import * as os from '@/os.js';
 import { misskeyApi } from '@/scripts/misskey-api.js';
-import { url } from '@/config.js';
+import { url } from '@@/js/config.js';
 import MkMediaImage from '@/components/MkMediaImage.vue';
 import MkImgWithBlurhash from '@/components/MkImgWithBlurhash.vue';
 import MkFollowButton from '@/components/MkFollowButton.vue';
@@ -125,7 +119,11 @@ import { $i } from '@/account.js';
 import { isSupportShare } from '@/scripts/navigator.js';
 import { instance } from '@/instance.js';
 import { getStaticImageUrl } from '@/scripts/media-proxy.js';
-import copyToClipboard from '@/scripts/copy-to-clipboard.js';
+import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
+import { useRouter } from '@/router/supplier.js';
+import type { MenuItem } from '@/types/menu.js';
+
+const router = useRouter();
 
 const props = defineProps<{
 	pageName: string;
@@ -167,18 +165,23 @@ function fetchPage() {
 function share(ev: MouseEvent) {
 	if (!page.value) return;
 
-	os.popupMenu([
-		{
-			text: i18n.ts.shareWithNote,
-			icon: 'ti ti-pencil',
-			action: shareWithNote,
-		},
-		...(isSupportShare() ? [{
+	const menuItems: MenuItem[] = [];
+
+	menuItems.push({
+		text: i18n.ts.shareWithNote,
+		icon: 'ti ti-pencil',
+		action: shareWithNote,
+	});
+
+	if (isSupportShare()) {
+		menuItems.push({
 			text: i18n.ts.share,
 			icon: 'ti ti-share',
 			action: shareWithNavigator,
-		}] : []),
-	], ev.currentTarget ?? ev.target);
+		});
+	}
+
+	os.popupMenu(menuItems, ev.currentTarget ?? ev.target);
 }
 
 function copyLink() {
@@ -242,6 +245,77 @@ function pin(pin) {
 	});
 }
 
+function reportAbuse() {
+	if (!page.value) return;
+
+	const pageUrl = `${url}/@${props.username}/pages/${props.pageName}`;
+
+	const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkAbuseReportWindow.vue')), {
+		user: page.value.user,
+		initialComment: `Page: ${pageUrl}\n-----\n`,
+	}, {
+		closed: () => dispose(),
+	});
+}
+
+function showMenu(ev: MouseEvent) {
+	if (!page.value) return;
+
+	const menuItems: MenuItem[] = [];
+
+	if ($i && $i.id === page.value.userId) {
+		menuItems.push({
+			icon: 'ti ti-pencil',
+			text: i18n.ts.editThisPage,
+			action: () => router.push(`/pages/edit/${page.value.id}`),
+		});
+
+		if ($i.pinnedPageId === page.value.id) {
+			menuItems.push({
+				icon: 'ti ti-pinned-off',
+				text: i18n.ts.unpin,
+				action: () => pin(false),
+			});
+		} else {
+			menuItems.push({
+				icon: 'ti ti-pin',
+				text: i18n.ts.pin,
+				action: () => pin(true),
+			});
+		}
+	} else if ($i && $i.id !== page.value.userId) {
+		menuItems.push({
+				icon: 'ti ti-code',
+				text: i18n.ts._pages.viewSource,
+				action: () => router.push(`/@${props.username}/pages/${props.pageName}/view-source`),
+		}, {
+			icon: 'ti ti-exclamation-circle',
+			text: i18n.ts.reportAbuse,
+			action: reportAbuse,
+		});
+
+		if ($i.isModerator || $i.isAdmin) {
+			menuItems.push({
+				type: 'divider',
+			}, {
+				icon: 'ti ti-trash',
+				text: i18n.ts.delete,
+				danger: true,
+				action: () => os.confirm({
+					type: 'warning',
+					text: i18n.ts.deleteConfirm,
+				}).then(({ canceled }) => {
+					if (canceled || !page.value) return;
+
+					os.apiWithDialog('pages/delete', { pageId: page.value.id });
+				}),
+			});
+		}
+	}
+
+	os.popupMenu(menuItems, ev.currentTarget ?? ev.target);
+}
+
 watch(() => path.value, fetchPage, { immediate: true });
 
 const headerActions = computed(() => []);
@@ -283,23 +357,24 @@ definePageMetadata(() => ({
 
 	&:hover,
 	&:focus-visible {
-		background-color: var(--accentedBg);
-		color: var(--accent);
+		background-color: var(--MI_THEME-accentedBg);
+		color: var(--MI_THEME-accent);
 		text-decoration: none;
+		outline: none;
 	}
 }
 
 .pageMain {
-	border-radius: var(--radius);
+	border-radius: var(--MI-radius);
 	padding: 2rem;
-	background: var(--panel);
+	background: var(--MI_THEME-panel);
 	box-sizing: border-box;
 }
 
 .pageBanner {
 	width: calc(100% + 4rem);
 	margin: -2rem -2rem 1.5rem;
-	border-radius: var(--radius) var(--radius) 0 0;
+	border-radius: var(--MI-radius) var(--MI-radius) 0 0;
 	overflow: hidden;
 	position: relative;
 
@@ -324,7 +399,7 @@ definePageMetadata(() => ({
 		}
 
 		.pageBannerBgFallback2 {
-			background-color: var(--accentedBg);
+			background-color: var(--MI_THEME-accentedBg);
 		}
 
 		&::after {
@@ -334,7 +409,7 @@ definePageMetadata(() => ({
 			bottom: 0;
 			width: 100%;
 			height: 100px;
-			background: linear-gradient(0deg, var(--panel), transparent);
+			background: linear-gradient(0deg, var(--MI_THEME-panel), transparent);
 		}
 	}
 
@@ -358,7 +433,7 @@ definePageMetadata(() => ({
 		h1 {
 			font-size: 2rem;
 			font-weight: 700;
-			color: var(--fg);
+			color: var(--MI_THEME-fg);
 			margin: 0;
 		}
 
@@ -371,20 +446,19 @@ definePageMetadata(() => ({
 		.pageBannerTitleUser {
 			--height: 32px;
 			flex-shrink: 0;
+			line-height: var(--height);
 
 			.avatar {
 				height: var(--height);
 				width: var(--height);
 			}
-
-			line-height: var(--height);
 		}
 
 		.pageBannerTitleSubActions {
 			flex-shrink: 0;
 			display: flex;
 			align-items: center;
-			gap: var(--marginHalf);
+			gap: var(--MI-marginHalf);
 			margin-left: auto;
 		}
 	}
@@ -398,14 +472,14 @@ definePageMetadata(() => ({
 	display: flex;
 	align-items: center;
 
-	border-top: 1px solid var(--divider);
+	border-top: 1px solid var(--MI_THEME-divider);
 	padding-top: 1.5rem;
 	margin-bottom: 1.5rem;
 
 	> .other {
 		margin-left: auto;
 		display: flex;
-		gap: var(--marginHalf);
+		gap: var(--MI-marginHalf);
 	}
 }
 
@@ -413,7 +487,7 @@ definePageMetadata(() => ({
 	display: flex;
 	align-items: center;
 
-	border-top: 1px solid var(--divider);
+	border-top: 1px solid var(--MI_THEME-divider);
 	padding-top: 1.5rem;
 	margin-bottom: 1.5rem;
 
@@ -452,14 +526,14 @@ definePageMetadata(() => ({
 	display: flex;
 	align-items: center;
 	flex-wrap: wrap;
-	gap: var(--marginHalf);
+	gap: var(--MI-marginHalf);
 }
 
 .relatedPagesRoot {
-	padding: var(--margin);
+	padding: var(--MI-margin);
 }
 
 .relatedPagesItem > article {
-	background-color: var(--panelHighlight) !important;
+	background-color: var(--MI_THEME-panelHighlight) !important;
 }
 </style>
diff --git a/packages/frontend/src/pages/preview.vue b/packages/frontend/src/pages/preview.vue
new file mode 100644
index 0000000000..8e07b190aa
--- /dev/null
+++ b/packages/frontend/src/pages/preview.vue
@@ -0,0 +1,26 @@
+<!--
+SPDX-FileCopyrightText: syuilo and misskey-project
+SPDX-License-Identifier: AGPL-3.0-only
+-->
+
+<template>
+<div>
+	<MkSample/>
+</div>
+</template>
+
+<script lang="ts" setup>
+import { computed } from 'vue';
+import MkSample from '@/components/MkPreview.vue';
+import { i18n } from '@/i18n.js';
+import { definePageMetadata } from '@/scripts/page-metadata.js';
+
+const headerActions = computed(() => []);
+
+const headerTabs = computed(() => []);
+
+definePageMetadata(computed(() => ({
+	title: i18n.ts.preview,
+	icon: 'ti ti-eye',
+})));
+</script>
diff --git a/packages/frontend/src/pages/registry.keys.vue b/packages/frontend/src/pages/registry.keys.vue
index bac1d2bb70..4cacbd0906 100644
--- a/packages/frontend/src/pages/registry.keys.vue
+++ b/packages/frontend/src/pages/registry.keys.vue
@@ -52,7 +52,7 @@ const props = defineProps<{
 
 const scope = computed(() => props.path ? props.path.split('/') : []);
 
-const keys = ref<any>(null);
+const keys = ref<[string, string][]>([]);
 
 function fetchKeys() {
 	misskeyApi('i/registry/keys-with-type', {
diff --git a/packages/frontend/src/pages/reset-password.vue b/packages/frontend/src/pages/reset-password.vue
index 6b67a9cc87..6d24029535 100644
--- a/packages/frontend/src/pages/reset-password.vue
+++ b/packages/frontend/src/pages/reset-password.vue
@@ -44,7 +44,9 @@ async function save() {
 
 onMounted(() => {
 	if (props.token == null) {
-		os.popup(defineAsyncComponent(() => import('@/components/MkForgotPassword.vue')), {}, {}, 'closed');
+		const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkForgotPassword.vue')), {}, {
+			closed: () => dispose(),
+		});
 		mainRouter.push('/');
 	}
 });
diff --git a/packages/frontend/src/pages/reversi/game.board.vue b/packages/frontend/src/pages/reversi/game.board.vue
index 175ea62411..429f502133 100644
--- a/packages/frontend/src/pages/reversi/game.board.vue
+++ b/packages/frontend/src/pages/reversi/game.board.vue
@@ -149,9 +149,9 @@ import MkButton from '@/components/MkButton.vue';
 import MkFolder from '@/components/MkFolder.vue';
 import MkSwitch from '@/components/MkSwitch.vue';
 import { deepClone } from '@/scripts/clone.js';
-import { useInterval } from '@/scripts/use-interval.js';
+import { useInterval } from '@@/js/use-interval.js';
 import { signinRequired } from '@/account.js';
-import { url } from '@/config.js';
+import { url } from '@@/js/config.js';
 import { i18n } from '@/i18n.js';
 import { misskeyApi } from '@/scripts/misskey-api.js';
 import { userPage } from '@/filters/user.js';
@@ -169,7 +169,7 @@ const props = defineProps<{
 const showBoardLabels = ref<boolean>(false);
 const useAvatarAsStone = ref<boolean>(true);
 const autoplaying = ref<boolean>(false);
-// eslint-disable-next-line vue/no-setup-props-destructure
+// eslint-disable-next-line vue/no-setup-props-reactivity-loss
 const game = ref<Misskey.entities.ReversiGameDetailed & { logs: Reversi.Serializer.SerializedLog[] }>(deepClone(props.game));
 const logPos = ref<number>(game.value.logs.length);
 const engine = shallowRef<Reversi.Game>(Reversi.Serializer.restoreGame({
@@ -504,7 +504,7 @@ $gap: 4px;
 .boardInner {
 	padding: 32px;
 
-	background: var(--panel);
+	background: var(--MI_THEME-panel);
 	box-shadow: 0 0 2px 1px #ce8a5c, inset 0 0 1px 1px #693410;
 	border-radius: 8px;
 }
@@ -574,34 +574,34 @@ $gap: 4px;
 	transition: border 0.25s ease, opacity 0.25s ease;
 
 	&.boardCell_empty {
-		border: solid 2px var(--divider);
+		border: solid 2px var(--MI_THEME-divider);
 	}
 
 	&.boardCell_empty.boardCell_can {
-		border-color: var(--accent);
+		border-color: var(--MI_THEME-accent);
 		opacity: 0.5;
 	}
 
 	&.boardCell_empty.boardCell_myTurn {
-		border-color: var(--divider);
+		border-color: var(--MI_THEME-divider);
 		opacity: 1;
 
 		&.boardCell_can {
-			border-color: var(--accent);
+			border-color: var(--MI_THEME-accent);
 			cursor: pointer;
 
 			&:hover {
-				background: var(--accent);
+				background: var(--MI_THEME-accent);
 			}
 		}
 	}
 
 	&.boardCell_prev {
-		box-shadow: 0 0 0 4px var(--accent);
+		box-shadow: 0 0 0 4px var(--MI_THEME-accent);
 	}
 
 	&.boardCell_isEnded {
-		border-color: var(--divider);
+		border-color: var(--MI_THEME-divider);
 	}
 
 	&.boardCell_none {
diff --git a/packages/frontend/src/pages/reversi/game.setting.vue b/packages/frontend/src/pages/reversi/game.setting.vue
index 31c0003130..437a1a2294 100644
--- a/packages/frontend/src/pages/reversi/game.setting.vue
+++ b/packages/frontend/src/pages/reversi/game.setting.vue
@@ -17,7 +17,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 				</template>
 				<template v-else>
 					<div class="_panel">
-						<div style="display: flex; align-items: center; padding: 16px; border-bottom: solid 1px var(--divider);">
+						<div style="display: flex; align-items: center; padding: 16px; border-bottom: solid 1px var(--MI_THEME-divider);">
 							<div>{{ mapName }}</div>
 							<MkButton style="margin-left: auto;" @click="chooseMap">{{ i18n.ts._reversi.chooseBoard }}</MkButton>
 						</div>
@@ -87,7 +87,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 		<div :class="$style.footer">
 			<MkSpacer :contentMax="700" :marginMin="16" :marginMax="16">
 				<div style="text-align: center;" class="_gaps_s">
-					<div v-if="opponentHasSettingsChanged" style="color: var(--warn);">{{ i18n.ts._reversi.opponentHasSettingsChanged }}</div>
+					<div v-if="opponentHasSettingsChanged" style="color: var(--MI_THEME-warn);">{{ i18n.ts._reversi.opponentHasSettingsChanged }}</div>
 					<div>
 						<template v-if="isReady && isOpReady">{{ i18n.ts._reversi.thisGameIsStartedSoon }}<MkEllipsis/></template>
 						<template v-if="isReady && !isOpReady">{{ i18n.ts._reversi.waitingForOther }}<MkEllipsis/></template>
@@ -121,7 +121,7 @@ import MkRadios from '@/components/MkRadios.vue';
 import MkSwitch from '@/components/MkSwitch.vue';
 import MkFolder from '@/components/MkFolder.vue';
 import * as os from '@/os.js';
-import { MenuItem } from '@/types/menu.js';
+import type { MenuItem } from '@/types/menu.js';
 import { useRouter } from '@/router/supplier.js';
 
 const $i = signinRequired();
@@ -132,7 +132,7 @@ const mapCategories = Array.from(new Set(Object.values(Reversi.maps).map(x => x.
 
 const props = defineProps<{
 	game: Misskey.entities.ReversiGameDetailed;
-	connection: Misskey.ChannelConnection;
+	connection: Misskey.ChannelConnection<Misskey.Channels['reversiGame']>;
 }>();
 
 const shareWhenStart = defineModel<boolean>('shareWhenStart', { default: false });
@@ -217,14 +217,14 @@ function onChangeReadyStates(states) {
 	game.value.user2Ready = states.user2;
 }
 
-function updateSettings(key: keyof Misskey.entities.ReversiGameDetailed) {
+function updateSettings(key: typeof Misskey.reversiUpdateKeys[number]) {
 	props.connection.send('updateSettings', {
 		key: key,
 		value: game.value[key],
 	});
 }
 
-function onUpdateSettings({ userId, key, value }: { userId: string; key: keyof Misskey.entities.ReversiGameDetailed; value: any; }) {
+function onUpdateSettings<K extends typeof Misskey.reversiUpdateKeys[number]>({ userId, key, value }: { userId: string; key: K; value: Misskey.entities.ReversiGameDetailed[K]; }) {
 	if (userId === $i.id) return;
 	if (game.value[key] === value) return;
 	game.value[key] = value;
@@ -273,14 +273,14 @@ onUnmounted(() => {
 	width: 300px;
 	height: 300px;
 	margin: 0 auto;
-	color: var(--fg);
+	color: var(--MI_THEME-fg);
 }
 
 .boardCell {
 	display: grid;
 	place-items: center;
 	background: transparent;
-	border: solid 2px var(--divider);
+	border: solid 2px var(--MI_THEME-divider);
 	border-radius: 6px;
 	overflow: clip;
 	cursor: pointer;
@@ -290,9 +290,9 @@ onUnmounted(() => {
 }
 
 .footer {
-	-webkit-backdrop-filter: var(--blur, blur(15px));
-	backdrop-filter: var(--blur, blur(15px));
-	background: var(--acrylicBg);
-	border-top: solid 0.5px var(--divider);
+	-webkit-backdrop-filter: var(--MI-blur, blur(15px));
+	backdrop-filter: var(--MI-blur, blur(15px));
+	background: var(--MI_THEME-acrylicBg);
+	border-top: solid 0.5px var(--MI_THEME-divider);
 }
 </style>
diff --git a/packages/frontend/src/pages/reversi/game.vue b/packages/frontend/src/pages/reversi/game.vue
index eadc51881c..10ea3717ab 100644
--- a/packages/frontend/src/pages/reversi/game.vue
+++ b/packages/frontend/src/pages/reversi/game.vue
@@ -20,8 +20,9 @@ import { useStream } from '@/stream.js';
 import { signinRequired } from '@/account.js';
 import { useRouter } from '@/router/supplier.js';
 import * as os from '@/os.js';
+import { url } from '@@/js/config.js';
 import { i18n } from '@/i18n.js';
-import { useInterval } from '@/scripts/use-interval.js';
+import { useInterval } from '@@/js/use-interval.js';
 
 const $i = signinRequired();
 
@@ -44,7 +45,7 @@ function start(_game: Misskey.entities.ReversiGameDetailed) {
 
 	if (shareWhenStart.value) {
 		misskeyApi('notes/create', {
-			text: i18n.ts._reversi.iStartedAGame + '\n' + location.href,
+			text: `${i18n.ts._reversi.iStartedAGame}\n${url}/reversi/g/${props.gameId}`,
 			visibility: 'home',
 		});
 	}
diff --git a/packages/frontend/src/pages/reversi/index.vue b/packages/frontend/src/pages/reversi/index.vue
index 51a03e4418..d608a2411c 100644
--- a/packages/frontend/src/pages/reversi/index.vue
+++ b/packages/frontend/src/pages/reversi/index.vue
@@ -36,13 +36,13 @@ SPDX-License-Identifier: AGPL-3.0-only
 					<div :class="$style.gamePreviews">
 						<MkA v-for="g in items" :key="g.id" v-panel :class="[$style.gamePreview, !g.isStarted && !g.isEnded && $style.gamePreviewWaiting, g.isStarted && !g.isEnded && $style.gamePreviewActive]" tabindex="-1" :to="`/reversi/g/${g.id}`">
 							<div :class="$style.gamePreviewPlayers">
-								<span v-if="g.winnerId === g.user1Id" style="margin-right: 0.75em; color: var(--accent); font-weight: bold;"><i class="ti ti-trophy"></i></span>
+								<span v-if="g.winnerId === g.user1Id" style="margin-right: 0.75em; color: var(--MI_THEME-accent); font-weight: bold;"><i class="ti ti-trophy"></i></span>
 								<span v-if="g.winnerId === g.user2Id" style="margin-right: 0.75em; visibility: hidden;"><i class="ti ti-x"></i></span>
 								<MkAvatar :class="$style.gamePreviewPlayersAvatar" :user="g.user1"/>
 								<span style="margin: 0 1em;">vs</span>
 								<MkAvatar :class="$style.gamePreviewPlayersAvatar" :user="g.user2"/>
 								<span v-if="g.winnerId === g.user1Id" style="margin-left: 0.75em; visibility: hidden;"><i class="ti ti-x"></i></span>
-								<span v-if="g.winnerId === g.user2Id" style="margin-left: 0.75em; color: var(--accent); font-weight: bold;"><i class="ti ti-trophy"></i></span>
+								<span v-if="g.winnerId === g.user2Id" style="margin-left: 0.75em; color: var(--MI_THEME-accent); font-weight: bold;"><i class="ti ti-trophy"></i></span>
 							</div>
 							<div :class="$style.gamePreviewFooter">
 								<span v-if="g.isStarted && !g.isEnded" :class="$style.gamePreviewStatusActive">{{ i18n.ts._reversi.playing }}</span>
@@ -63,13 +63,13 @@ SPDX-License-Identifier: AGPL-3.0-only
 					<div :class="$style.gamePreviews">
 						<MkA v-for="g in items" :key="g.id" v-panel :class="[$style.gamePreview, !g.isStarted && !g.isEnded && $style.gamePreviewWaiting, g.isStarted && !g.isEnded && $style.gamePreviewActive]" tabindex="-1" :to="`/reversi/g/${g.id}`">
 							<div :class="$style.gamePreviewPlayers">
-								<span v-if="g.winnerId === g.user1Id" style="margin-right: 0.75em; color: var(--accent); font-weight: bold;"><i class="ti ti-trophy"></i></span>
+								<span v-if="g.winnerId === g.user1Id" style="margin-right: 0.75em; color: var(--MI_THEME-accent); font-weight: bold;"><i class="ti ti-trophy"></i></span>
 								<span v-if="g.winnerId === g.user2Id" style="margin-right: 0.75em; visibility: hidden;"><i class="ti ti-x"></i></span>
 								<MkAvatar :class="$style.gamePreviewPlayersAvatar" :user="g.user1"/>
 								<span style="margin: 0 1em;">vs</span>
 								<MkAvatar :class="$style.gamePreviewPlayersAvatar" :user="g.user2"/>
 								<span v-if="g.winnerId === g.user1Id" style="margin-left: 0.75em; visibility: hidden;"><i class="ti ti-x"></i></span>
-								<span v-if="g.winnerId === g.user2Id" style="margin-left: 0.75em; color: var(--accent); font-weight: bold;"><i class="ti ti-trophy"></i></span>
+								<span v-if="g.winnerId === g.user2Id" style="margin-left: 0.75em; color: var(--MI_THEME-accent); font-weight: bold;"><i class="ti ti-trophy"></i></span>
 							</div>
 							<div :class="$style.gamePreviewFooter">
 								<span v-if="g.isStarted && !g.isEnded" :class="$style.gamePreviewStatusActive">{{ i18n.ts._reversi.playing }}</span>
@@ -117,7 +117,7 @@ import { $i } from '@/account.js';
 import MkPagination from '@/components/MkPagination.vue';
 import { useRouter } from '@/router/supplier.js';
 import * as os from '@/os.js';
-import { useInterval } from '@/scripts/use-interval.js';
+import { useInterval } from '@@/js/use-interval.js';
 import { pleaseLogin } from '@/scripts/please-login.js';
 import * as sound from '@/scripts/sound.js';
 
@@ -285,7 +285,7 @@ definePageMetadata(() => ({
 .gamePreviews {
 	display: grid;
 	grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
-	grid-gap: var(--margin);
+	grid-gap: var(--MI-margin);
 }
 
 .gamePreview {
@@ -295,11 +295,11 @@ definePageMetadata(() => ({
 }
 
 .gamePreviewActive {
-	box-shadow: inset 0 0 8px 0px var(--accent);
+	box-shadow: inset 0 0 8px 0px var(--MI_THEME-accent);
 }
 
 .gamePreviewWaiting {
-	box-shadow: inset 0 0 8px 0px var(--warn);
+	box-shadow: inset 0 0 8px 0px var(--MI_THEME-warn);
 }
 
 .gamePreviewPlayers {
@@ -324,19 +324,19 @@ definePageMetadata(() => ({
 .gamePreviewFooter {
 	display: flex;
 	align-items: baseline;
-	border-top: solid 0.5px var(--divider);
+	border-top: solid 0.5px var(--MI_THEME-divider);
 	padding: 6px 10px;
 	font-size: 0.9em;
 }
 
 .gamePreviewStatusActive {
-	color: var(--accent);
+	color: var(--MI_THEME-accent);
 	font-weight: bold;
 	animation: blink 2s infinite;
 }
 
 .gamePreviewStatusWaiting {
-	color: var(--warn);
+	color: var(--MI_THEME-warn);
 	font-weight: bold;
 	animation: blink 2s infinite;
 }
diff --git a/packages/frontend/src/pages/role.vue b/packages/frontend/src/pages/role.vue
index ce80579cf9..46e510b49b 100644
--- a/packages/frontend/src/pages/role.vue
+++ b/packages/frontend/src/pages/role.vue
@@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 <template>
 <MkStickyContainer>
 	<template #header><MkPageHeader v-model:tab="tab" :tabs="headerTabs"/></template>
-	<MKSpacer v-if="!(typeof error === 'undefined')" :contentMax="1200">
+	<MkSpacer v-if="error != null" :contentMax="1200">
 		<div :class="$style.root">
 			<img :class="$style.img" :src="serverErrorImageUrl" class="_ghost"/>
 			<p :class="$style.text">
@@ -14,7 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 				{{ error }}
 			</p>
 		</div>
-	</MKSpacer>
+	</MkSpacer>
 	<MkSpacer v-else-if="tab === 'users'" :contentMax="1200">
 		<div class="_gaps_s">
 			<div v-if="role">{{ role.description }}</div>
@@ -26,7 +26,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 		</div>
 	</MkSpacer>
 	<MkSpacer v-else-if="tab === 'timeline'" :contentMax="700">
-		<MkTimeline v-if="visible" ref="timeline" src="role" :role="props.role"/>
+		<MkTimeline v-if="visible" ref="timeline" src="role" :role="props.roleId"/>
 		<div v-else-if="!visible" class="_fullinfo">
 			<img :src="infoImageUrl" class="_ghost"/>
 			<div>{{ i18n.ts.nothing }}</div>
@@ -43,27 +43,28 @@ import MkUserList from '@/components/MkUserList.vue';
 import { definePageMetadata } from '@/scripts/page-metadata.js';
 import { i18n } from '@/i18n.js';
 import MkTimeline from '@/components/MkTimeline.vue';
-import { instanceName } from '@/config.js';
+import { instanceName } from '@@/js/config.js';
 import { serverErrorImageUrl, infoImageUrl } from '@/instance.js';
 
 const props = withDefaults(defineProps<{
-	role: string;
+	roleId: string;
 	initialTab?: string;
 }>(), {
 	initialTab: 'users',
 });
 
+// eslint-disable-next-line vue/no-setup-props-reactivity-loss
 const tab = ref(props.initialTab);
-const role = ref<Misskey.entities.Role>();
-const error = ref();
+const role = ref<Misskey.entities.Role | null>(null);
+const error = ref<string | null>(null);
 const visible = ref(false);
 
-watch(() => props.role, () => {
+watch(() => props.roleId, () => {
 	misskeyApi('roles/show', {
-		roleId: props.role,
+		roleId: props.roleId,
 	}).then(res => {
 		role.value = res;
-		document.title = `${role.value.name} | ${instanceName}`;
+		error.value = null;
 		visible.value = res.isExplorable && res.isPublic;
 	}).catch((err) => {
 		if (err.code === 'NO_SUCH_ROLE') {
@@ -71,7 +72,6 @@ watch(() => props.role, () => {
 		} else {
 			error.value = i18n.ts.somethingHappened;
 		}
-		document.title = `${error.value} | ${instanceName}`;
 	});
 }, { immediate: true });
 
@@ -79,7 +79,7 @@ const users = computed(() => ({
 	endpoint: 'roles/users' as const,
 	limit: 30,
 	params: {
-		roleId: props.role,
+		roleId: props.roleId,
 	},
 }));
 
@@ -94,7 +94,7 @@ const headerTabs = computed(() => [{
 }]);
 
 definePageMetadata(() => ({
-	title: role.value ? role.value.name : i18n.ts.role,
+	title: role.value ? role.value.name : (error.value ?? i18n.ts.role),
 	icon: 'ti ti-badge',
 }));
 </script>
diff --git a/packages/frontend/src/pages/scratchpad.vue b/packages/frontend/src/pages/scratchpad.vue
index 9aaa8ff9c6..88171f7d70 100644
--- a/packages/frontend/src/pages/scratchpad.vue
+++ b/packages/frontend/src/pages/scratchpad.vue
@@ -30,6 +30,24 @@ SPDX-License-Identifier: AGPL-3.0-only
 				</div>
 			</MkContainer>
 
+			<MkContainer :foldable="true" :expanded="false">
+				<template #header>{{ i18n.ts.uiInspector }}</template>
+				<div :class="$style.uiInspector">
+					<div v-for="c in components" :key="c.value.id" :class="{ [$style.uiInspectorUnShown]: !showns.has(c.value.id) }">
+						<div :class="$style.uiInspectorType">{{ c.value.type }}</div>
+						<div :class="$style.uiInspectorId">{{ c.value.id }}</div>
+						<button :class="$style.uiInspectorPropsToggle" @click="() => uiInspectorOpenedComponents.set(c, !uiInspectorOpenedComponents.get(c))">
+							<i v-if="uiInspectorOpenedComponents.get(c)" class="ti ti-chevron-up icon"></i>
+							<i v-else class="ti ti-chevron-down icon"></i>
+						</button>
+						<div v-if="uiInspectorOpenedComponents.get(c)">
+							<MkTextarea :modelValue="stringifyUiProps(c.value)" code readonly></MkTextarea>
+						</div>
+					</div>
+					<div :class="$style.uiInspectorDescription">{{ i18n.ts.uiInspectorDescription }}</div>
+				</div>
+			</MkContainer>
+
 			<div class="">
 				{{ i18n.ts.scratchpadDescription }}
 			</div>
@@ -43,6 +61,7 @@ import { onDeactivated, onUnmounted, Ref, ref, watch, computed } from 'vue';
 import { Interpreter, Parser, utils } from '@syuilo/aiscript';
 import MkContainer from '@/components/MkContainer.vue';
 import MkButton from '@/components/MkButton.vue';
+import MkTextarea from '@/components/MkTextarea.vue';
 import MkCodeEditor from '@/components/MkCodeEditor.vue';
 import { aiScriptReadline, createAiScriptEnv } from '@/scripts/aiscript/api.js';
 import * as os from '@/os.js';
@@ -57,10 +76,15 @@ import { claimAchievement } from '@/scripts/achievements.js';
 const parser = new Parser();
 let aiscript: Interpreter;
 const code = ref('');
-const logs = ref<any[]>([]);
+const logs = ref<{
+	id: number;
+	text: string;
+	print: boolean;
+}[]>([]);
 const root = ref<AsUiRoot>();
 const components = ref<Ref<AsUiComponent>[]>([]);
 const uiKey = ref(0);
+const uiInspectorOpenedComponents = ref(new Map<string, boolean>);
 
 const saved = miLocalStorage.getItem('scratchpad');
 if (saved) {
@@ -71,6 +95,14 @@ watch(code, () => {
 	miLocalStorage.setItem('scratchpad', code.value);
 });
 
+function stringifyUiProps(uiProps) {
+	return JSON.stringify(
+		{ ...uiProps, type: undefined, id: undefined },
+		(k, v) => typeof v === 'function' ? '<function>' : v,
+		2
+	);
+}
+
 async function run() {
 	if (aiscript) aiscript.abort();
 	root.value = undefined;
@@ -152,6 +184,20 @@ const headerActions = computed(() => []);
 
 const headerTabs = computed(() => []);
 
+const showns = computed(() => {
+	const result = new Set<string>();
+	(function addChildrenToResult(c: AsUiComponent) {
+		result.add(c.id);
+		if (c.children) {
+			const childComponents = components.value.filter(v => c.children.includes(v.value.id));
+			for (const child of childComponents) {
+				addChildrenToResult(child.value);
+			}
+		}
+	})(root.value);
+	return result;
+});
+
 definePageMetadata(() => ({
 	title: i18n.ts.scratchpad,
 	icon: 'ti ti-terminal-2',
@@ -162,7 +208,7 @@ definePageMetadata(() => ({
 .root {
 	display: flex;
 	flex-direction: column;
-	gap: var(--margin);
+	gap: var(--MI-margin);
 }
 
 .editor {
@@ -192,4 +238,39 @@ definePageMetadata(() => ({
 		}
 	}
 }
+
+.uiInspector {
+	display: grid;
+	gap: 8px;
+	padding: 16px;
+}
+
+.uiInspectorUnShown {
+	color: var(--MI_THEME-fgTransparent);
+}
+
+.uiInspectorType {
+	display: inline-block;
+	border: hidden;
+	border-radius: 10px;
+	background-color: var(--MI_THEME-panelHighlight);
+	padding: 2px 8px;
+	font-size: 12px;
+}
+
+.uiInspectorId {
+	display: inline-block;
+	padding-left: 8px;
+}
+
+.uiInspectorDescription {
+	display: block;
+	font-size: 12px;
+	padding-top: 16px;
+}
+
+.uiInspectorPropsToggle {
+	background: none;
+	border: none;
+}
 </style>
diff --git a/packages/frontend/src/pages/search.note.vue b/packages/frontend/src/pages/search.note.vue
index d68bbaeeca..105c947d25 100644
--- a/packages/frontend/src/pages/search.note.vue
+++ b/packages/frontend/src/pages/search.note.vue
@@ -6,29 +6,38 @@ SPDX-License-Identifier: AGPL-3.0-only
 <template>
 <div class="_gaps">
 	<div class="_gaps">
-		<MkInput v-model="searchQuery" :large="true" :autofocus="true" type="search" @enter="search">
+		<MkInput v-model="searchQuery" :large="true" :autofocus="true" type="search" @enter.prevent="search">
 			<template #prefix><i class="ti ti-search"></i></template>
 		</MkInput>
-		<MkFolder>
-			<template #label>{{ i18n.ts.options }}</template>
+		<MkFoldableSection :expanded="true">
+			<template #header>{{ i18n.ts.options }}</template>
 
 			<div class="_gaps_m">
-				<MkSwitch v-model="isLocalOnly">{{ i18n.ts.localOnly }}</MkSwitch>
+				<MkRadios v-model="hostSelect">
+					<template #label>{{ i18n.ts.host }}</template>
+					<option value="all" default>{{ i18n.ts.all }}</option>
+					<option value="local">{{ i18n.ts.local }}</option>
+					<option v-if="noteSearchableScope === 'global'" value="specified">{{ i18n.ts.specifyHost }}</option>
+				</MkRadios>
+				<MkInput v-if="noteSearchableScope === 'global'" v-model="hostInput" :disabled="hostSelect !== 'specified'" :large="true" type="search">
+					<template #prefix><i class="ti ti-server"></i></template>
+				</MkInput>
 
 				<MkFolder :defaultOpen="true">
 					<template #label>{{ i18n.ts.specifyUser }}</template>
-					<template v-if="user" #suffix>@{{ user.username }}</template>
+					<template v-if="user" #suffix>@{{ user.username }}{{ user.host ? `@${user.host}` : "" }}</template>
 
-					<div style="text-align: center;" class="_gaps">
-						<div v-if="user">@{{ user.username }}</div>
-						<div>
-							<MkButton v-if="user == null" primary rounded inline @click="selectUser">{{ i18n.ts.selectUser }}</MkButton>
-							<MkButton v-else danger rounded inline @click="user = null">{{ i18n.ts.remove }}</MkButton>
+					<div class="_gaps">
+						<div :class="$style.userItem">
+							<MkUserCardMini v-if="user" :class="$style.userCard" :user="user" :withChart="false"/>
+							<MkButton v-if="user == null && $i != null" transparent :class="$style.addMeButton" @click="selectSelf"><div :class="$style.addUserButtonInner"><span><i class="ti ti-plus"></i><i class="ti ti-user"></i></span><span>{{ i18n.ts.selectSelf }}</span></div></MkButton>
+							<MkButton v-if="user == null" transparent :class="$style.addUserButton" @click="selectUser"><div :class="$style.addUserButtonInner"><i class="ti ti-plus"></i><span>{{ i18n.ts.selectUser }}</span></div></MkButton>
+							<button class="_button" :class="$style.remove" :disabled="user == null" @click="removeUser"><i class="ti ti-x"></i></button>
 						</div>
 					</div>
 				</MkFolder>
 			</div>
-		</MkFolder>
+		</MkFoldableSection>
 		<div>
 			<MkButton large primary gradate rounded style="margin: 0 auto;" @click="search">{{ i18n.ts.search }}</MkButton>
 		</div>
@@ -42,31 +51,90 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { ref } from 'vue';
+import { computed, ref, toRef, watch } from 'vue';
+import type { UserDetailed } from 'misskey-js/entities.js';
+import type { Paging } from '@/components/MkPagination.vue';
 import MkNotes from '@/components/MkNotes.vue';
 import MkInput from '@/components/MkInput.vue';
 import MkButton from '@/components/MkButton.vue';
-import MkSwitch from '@/components/MkSwitch.vue';
 import { i18n } from '@/i18n.js';
 import * as os from '@/os.js';
 import { misskeyApi } from '@/scripts/misskey-api.js';
 import MkFoldableSection from '@/components/MkFoldableSection.vue';
 import MkFolder from '@/components/MkFolder.vue';
 import { useRouter } from '@/router/supplier.js';
+import MkUserCardMini from '@/components/MkUserCardMini.vue';
+import MkRadios from '@/components/MkRadios.vue';
+import { $i } from '@/account.js';
+import { instance } from '@/instance.js';
+
+const props = withDefaults(defineProps<{
+	query?: string;
+	userId?: string;
+	username?: string;
+	host?: string | null;
+}>(), {
+	query: '',
+	userId: undefined,
+	username: undefined,
+	host: '',
+});
 
 const router = useRouter();
-
 const key = ref(0);
-const searchQuery = ref('');
-const searchOrigin = ref('combined');
-const notePagination = ref();
-const user = ref<any>(null);
-const isLocalOnly = ref(false);
+const searchQuery = ref(toRef(props, 'query').value);
+const notePagination = ref<Paging>();
+const user = ref<UserDetailed | null>(null);
+const hostInput = ref(toRef(props, 'host').value);
 
-function selectUser() {
-	os.selectUser({ includeSelf: true }).then(_user => {
+const noteSearchableScope = instance.noteSearchableScope ?? 'local';
+
+const hostSelect = ref<'all' | 'local' | 'specified'>('all');
+
+const setHostSelectWithInput = (after:string|undefined|null, before:string|undefined|null) => {
+	if (before === after) return;
+	if (after === '') hostSelect.value = 'all';
+	else hostSelect.value = 'specified';
+};
+
+setHostSelectWithInput(hostInput.value, undefined);
+
+watch(hostInput, setHostSelectWithInput);
+
+const searchHost = computed(() => {
+	if (hostSelect.value === 'local') return '.';
+	if (hostSelect.value === 'specified') return hostInput.value;
+	return null;
+});
+
+if (props.userId != null) {
+	misskeyApi('users/show', { userId: props.userId }).then(_user => {
 		user.value = _user;
 	});
+} else if (props.username != null) {
+	misskeyApi('users/show', {
+		username: props.username,
+		...(props.host != null && props.host !== '') ? { host: props.host } : {},
+	}).then(_user => {
+		user.value = _user;
+	});
+}
+
+function selectUser() {
+	os.selectUser({ includeSelf: true, localOnly: instance.noteSearchableScope === 'local' }).then(_user => {
+		user.value = _user;
+		hostInput.value = _user.host ?? '';
+	});
+}
+
+function selectSelf() {
+	user.value = $i as UserDetailed | null;
+	hostInput.value = null;
+}
+
+function removeUser() {
+	user.value = null;
+	hostInput.value = '';
 }
 
 async function search() {
@@ -74,22 +142,54 @@ async function search() {
 
 	if (query == null || query === '') return;
 
-	if (query.startsWith('https://')) {
-		const promise = misskeyApi('ap/show', {
-			uri: query,
+	//#region AP lookup
+	if (query.startsWith('https://') && !query.includes(' ')) {
+		const confirm = await os.confirm({
+			type: 'info',
+			text: i18n.ts.lookupConfirm,
 		});
+		if (!confirm.canceled) {
+			const promise = misskeyApi('ap/show', {
+				uri: query,
+			});
 
-		os.promiseDialog(promise, null, null, i18n.ts.fetchingAsApObject);
+			os.promiseDialog(promise, null, null, i18n.ts.fetchingAsApObject);
 
-		const res = await promise;
+			const res = await promise;
 
-		if (res.type === 'User') {
-			router.push(`/@${res.object.username}@${res.object.host}`);
-		} else if (res.type === 'Note') {
-			router.push(`/notes/${res.object.id}`);
+			if (res.type === 'User') {
+				router.push(`/@${res.object.username}@${res.object.host}`);
+			} else if (res.type === 'Note') {
+				router.push(`/notes/${res.object.id}`);
+			}
+
+			return;
+		}
+	}
+	//#endregion
+
+	if (query.length > 1 && !query.includes(' ')) {
+		if (query.startsWith('@')) {
+			const confirm = await os.confirm({
+				type: 'info',
+				text: i18n.ts.lookupConfirm,
+			});
+			if (!confirm.canceled) {
+				router.push(`/${query}`);
+				return;
+			}
 		}
 
-		return;
+		if (query.startsWith('#')) {
+			const confirm = await os.confirm({
+				type: 'info',
+				text: i18n.ts.openTagPageConfirm,
+			});
+			if (!confirm.canceled) {
+				router.push(`/tags/${encodeURIComponent(query.substring(1))}`);
+				return;
+			}
+		}
 	}
 
 	notePagination.value = {
@@ -98,11 +198,49 @@ async function search() {
 		params: {
 			query: searchQuery.value,
 			userId: user.value ? user.value.id : null,
+			...(searchHost.value ? { host: searchHost.value } : {}),
 		},
 	};
 
-	if (isLocalOnly.value) notePagination.value.params.host = '.';
-
 	key.value++;
 }
 </script>
+<style lang="scss" module>
+.userItem {
+	display: flex;
+	justify-content: center;
+}
+.addMeButton {
+  border: 2px dashed var(--MI_THEME-fgTransparent);
+	padding: 12px;
+	margin-right: 16px;
+}
+.addUserButton {
+  border: 2px dashed var(--MI_THEME-fgTransparent);
+	padding: 12px;
+	flex-grow: 1;
+}
+.addUserButtonInner {
+	display: flex;
+	flex-direction: column;
+	align-items: center;
+	justify-content: space-between;
+	min-height: 38px;
+}
+.userCard {
+	flex-grow: 1;
+}
+.remove {
+	width: 32px;
+	height: 32px;
+	align-self: center;
+
+	& > i:before {
+		color: #ff2a2a;
+	}
+
+	&:disabled {
+		opacity: 0;
+	}
+}
+</style>
diff --git a/packages/frontend/src/pages/search.stories.impl.ts b/packages/frontend/src/pages/search.stories.impl.ts
new file mode 100644
index 0000000000..0110a7ab8e
--- /dev/null
+++ b/packages/frontend/src/pages/search.stories.impl.ts
@@ -0,0 +1,88 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { StoryObj } from '@storybook/vue3';
+import { HttpResponse, http } from 'msw';
+import search_ from './search.vue';
+import { userDetailed } from '@/../.storybook/fakes.js';
+import { commonHandlers } from '@/../.storybook/mocks.js';
+
+const localUser = userDetailed('someuserid', 'miskist', null, 'Local Misskey User');
+
+export const Default = {
+	render(args) {
+		return {
+			components: {
+				search_,
+			},
+			setup() {
+				return {
+					args,
+				};
+			},
+			computed: {
+				props() {
+					return {
+						...this.args,
+					};
+				},
+			},
+			template: '<search_ v-bind="props" />',
+		};
+	},
+	args: {
+		ignoreNotesSearchAvailable: true,
+	},
+	parameters: {
+		layout: 'fullscreen',
+		msw: {
+			handlers: [
+				...commonHandlers,
+				http.post('/api/users/show', () => {
+					return HttpResponse.json(userDetailed());
+				}),
+				http.post('/api/users/search', () => {
+					return HttpResponse.json([userDetailed(), localUser]);
+				}),
+			],
+		},
+	},
+} satisfies StoryObj<typeof search_>;
+
+export const NoteSearchDisabled = {
+	...Default,
+	args: {},
+} satisfies StoryObj<typeof search_>;
+
+export const WithUsernameLocal = {
+	...Default,
+
+	args: {
+		...Default.args,
+		username: localUser.username,
+		host: localUser.host,
+	},
+	parameters: {
+		layout: 'fullscreen',
+		msw: {
+			handlers: [
+				...commonHandlers,
+				http.post('/api/users/show', () => {
+					return HttpResponse.json(localUser);
+				}),
+				http.post('/api/users/search', () => {
+					return HttpResponse.json([userDetailed(), localUser]);
+				}),
+			],
+		},
+	},
+} satisfies StoryObj<typeof search_>;
+
+export const WithUserType = {
+	...Default,
+	args: {
+		type: 'user',
+	},
+} satisfies StoryObj<typeof search_>;
diff --git a/packages/frontend/src/pages/search.user.vue b/packages/frontend/src/pages/search.user.vue
index b9c2704bc7..724fbfdfbd 100644
--- a/packages/frontend/src/pages/search.user.vue
+++ b/packages/frontend/src/pages/search.user.vue
@@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 <template>
 <div class="_gaps">
 	<div class="_gaps">
-		<MkInput v-model="searchQuery" :large="true" :autofocus="true" type="search" @enter="search">
+		<MkInput v-model="searchQuery" :large="true" :autofocus="true" type="search" @enter.prevent="search">
 			<template #prefix><i class="ti ti-search"></i></template>
 		</MkInput>
 		<MkRadios v-model="searchOrigin" @update:modelValue="search()">
@@ -25,7 +25,9 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { ref } from 'vue';
+import { ref, toRef } from 'vue';
+import type { Endpoints } from 'misskey-js';
+import type { Paging } from '@/components/MkPagination.vue';
 import MkUserList from '@/components/MkUserList.vue';
 import MkInput from '@/components/MkInput.vue';
 import MkRadios from '@/components/MkRadios.vue';
@@ -36,34 +38,74 @@ import MkFoldableSection from '@/components/MkFoldableSection.vue';
 import { misskeyApi } from '@/scripts/misskey-api.js';
 import { useRouter } from '@/router/supplier.js';
 
+const props = withDefaults(defineProps<{
+  query?: string,
+  origin?: Endpoints['users/search']['req']['origin'],
+}>(), {
+	query: '',
+	origin: 'combined',
+});
+
 const router = useRouter();
 
 const key = ref('');
-const searchQuery = ref('');
-const searchOrigin = ref('combined');
-const userPagination = ref();
+const searchQuery = ref(toRef(props, 'query').value);
+const searchOrigin = ref(toRef(props, 'origin').value);
+const userPagination = ref<Paging>();
 
 async function search() {
 	const query = searchQuery.value.toString().trim();
 
 	if (query == null || query === '') return;
 
-	if (query.startsWith('https://')) {
-		const promise = misskeyApi('ap/show', {
-			uri: query,
+	//#region AP lookup
+	if (query.startsWith('https://') && !query.includes(' ')) {
+		const confirm = await os.confirm({
+			type: 'info',
+			text: i18n.ts.lookupConfirm,
 		});
+		if (!confirm.canceled) {
+			const promise = misskeyApi('ap/show', {
+				uri: query,
+			});
 
-		os.promiseDialog(promise, null, null, i18n.ts.fetchingAsApObject);
+			os.promiseDialog(promise, null, null, i18n.ts.fetchingAsApObject);
 
-		const res = await promise;
+			const res = await promise;
 
-		if (res.type === 'User') {
-			router.push(`/@${res.object.username}@${res.object.host}`);
-		} else if (res.type === 'Note') {
-			router.push(`/notes/${res.object.id}`);
+			if (res.type === 'User') {
+				router.push(`/@${res.object.username}@${res.object.host}`);
+			} else if (res.type === 'Note') {
+				router.push(`/notes/${res.object.id}`);
+			}
+
+			return;
+		}
+	}
+	//#endregion
+
+	if (query.length > 1 && !query.includes(' ')) {
+		if (query.startsWith('@')) {
+			const confirm = await os.confirm({
+				type: 'info',
+				text: i18n.ts.lookupConfirm,
+			});
+			if (!confirm.canceled) {
+				router.push(`/${query}`);
+				return;
+			}
 		}
 
-		return;
+		if (query.startsWith('#')) {
+			const confirm = await os.confirm({
+				type: 'info',
+				text: i18n.ts.openTagPageConfirm,
+			});
+			if (!confirm.canceled) {
+				router.push(`/user-tags/${encodeURIComponent(query.substring(1))}`);
+				return;
+			}
+		}
 	}
 
 	userPagination.value = {
diff --git a/packages/frontend/src/pages/search.vue b/packages/frontend/src/pages/search.vue
index a3dcda77be..38d7548fa8 100644
--- a/packages/frontend/src/pages/search.vue
+++ b/packages/frontend/src/pages/search.vue
@@ -9,8 +9,8 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 	<MkHorizontalSwipe v-model:tab="tab" :tabs="headerTabs">
 		<MkSpacer v-if="tab === 'note'" key="note" :contentMax="800">
-			<div v-if="notesSearchAvailable">
-				<XNote/>
+			<div v-if="notesSearchAvailable || ignoreNotesSearchAvailable">
+				<XNote v-bind="props"/>
 			</div>
 			<div v-else>
 				<MkInfo warn>{{ i18n.ts.notesSearchNotAvailable }}</MkInfo>
@@ -18,27 +18,43 @@ SPDX-License-Identifier: AGPL-3.0-only
 		</MkSpacer>
 
 		<MkSpacer v-else-if="tab === 'user'" key="user" :contentMax="800">
-			<XUser/>
+			<XUser v-bind="props"/>
 		</MkSpacer>
 	</MkHorizontalSwipe>
 </MkStickyContainer>
 </template>
 
 <script lang="ts" setup>
-import { computed, defineAsyncComponent, ref } from 'vue';
+import { computed, defineAsyncComponent, ref, toRef } from 'vue';
 import { i18n } from '@/i18n.js';
 import { definePageMetadata } from '@/scripts/page-metadata.js';
-import { $i } from '@/account.js';
-import { instance } from '@/instance.js';
+import { notesSearchAvailable } from '@/scripts/check-permissions.js';
 import MkInfo from '@/components/MkInfo.vue';
 import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue';
 
+const props = withDefaults(defineProps<{
+	query?: string,
+	userId?: string,
+	username?: string,
+	host?: string | null,
+	type?: 'note' | 'user',
+	origin?: 'combined' | 'local' | 'remote',
+	// For storybook only
+	ignoreNotesSearchAvailable?: boolean,
+}>(), {
+	query: '',
+	userId: undefined,
+	username: undefined,
+	host: undefined,
+	type: 'note',
+	origin: 'combined',
+	ignoreNotesSearchAvailable: false,
+});
+
 const XNote = defineAsyncComponent(() => import('./search.note.vue'));
 const XUser = defineAsyncComponent(() => import('./search.user.vue'));
 
-const tab = ref('note');
-
-const notesSearchAvailable = (($i == null && instance.policies.canSearchNotes) || ($i != null && $i.policies.canSearchNotes));
+const tab = ref(toRef(props, 'type').value);
 
 const headerActions = computed(() => []);
 
diff --git a/packages/frontend/src/pages/settings/2fa.qrdialog.vue b/packages/frontend/src/pages/settings/2fa.qrdialog.vue
index 2244047b31..18c82ffdf6 100644
--- a/packages/frontend/src/pages/settings/2fa.qrdialog.vue
+++ b/packages/frontend/src/pages/settings/2fa.qrdialog.vue
@@ -138,12 +138,13 @@ const token = ref<string | number | null>(null);
 const backupCodes = ref<string[]>();
 
 function cancel() {
-	dialog.value.close();
+	dialog.value?.close();
 }
 
 async function tokenDone() {
+	if (token.value == null) return;
 	const res = await os.apiWithDialog('i/2fa/done', {
-		token: token.value.toString(),
+		token: typeof token.value === 'string' ? token.value : token.value.toString(),
 	});
 
 	backupCodes.value = res.backupCodes;
@@ -166,7 +167,7 @@ function downloadBackupCodes() {
 }
 
 function allDone() {
-	dialog.value.close();
+	dialog.value?.close();
 }
 </script>
 
diff --git a/packages/frontend/src/pages/settings/2fa.vue b/packages/frontend/src/pages/settings/2fa.vue
index b7d648c1a4..776f59dda3 100644
--- a/packages/frontend/src/pages/settings/2fa.vue
+++ b/packages/frontend/src/pages/settings/2fa.vue
@@ -19,7 +19,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 			<template #icon><i class="ti ti-shield-lock"></i></template>
 			<template #label>{{ i18n.ts.totp }}</template>
 			<template #caption>{{ i18n.ts.totpDescription }}</template>
-			<template #suffix><i v-if="$i.twoFactorEnabled" class="ti ti-check" style="color: var(--success)"></i></template>
+			<template #suffix><i v-if="$i.twoFactorEnabled" class="ti ti-check" style="color: var(--MI_THEME-success)"></i></template>
 
 			<div v-if="$i.twoFactorEnabled" class="_gaps_s">
 				<div v-text="i18n.ts._2fa.alreadyRegistered"/>
@@ -84,7 +84,7 @@ import FormSection from '@/components/form/section.vue';
 import MkFolder from '@/components/MkFolder.vue';
 import MkLink from '@/components/MkLink.vue';
 import * as os from '@/os.js';
-import { signinRequired, updateAccount } from '@/account.js';
+import { signinRequired, updateAccountPartial } from '@/account.js';
 import { i18n } from '@/i18n.js';
 
 const $i = signinRequired();
@@ -108,9 +108,11 @@ async function registerTOTP(): Promise<void> {
 		token: auth.result.token,
 	});
 
-	os.popup(defineAsyncComponent(() => import('./2fa.qrdialog.vue')), {
+	const { dispose } = os.popup(defineAsyncComponent(() => import('./2fa.qrdialog.vue')), {
 		twoFactorData,
-	}, {}, 'closed');
+	}, {
+		closed: () => dispose(),
+	});
 }
 
 async function unregisterTOTP(): Promise<void> {
@@ -121,7 +123,7 @@ async function unregisterTOTP(): Promise<void> {
 		password: auth.result.password,
 		token: auth.result.token,
 	}).then(res => {
-		updateAccount({
+		updateAccountPartial({
 			twoFactorEnabled: false,
 		});
 	}).catch(error => {
diff --git a/packages/frontend/src/pages/settings/accounts.vue b/packages/frontend/src/pages/settings/accounts.vue
index 1182346de9..97e960675f 100644
--- a/packages/frontend/src/pages/settings/accounts.vue
+++ b/packages/frontend/src/pages/settings/accounts.vue
@@ -19,13 +19,13 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { defineAsyncComponent, ref, computed } from 'vue';
+import { ref, computed } from 'vue';
 import type * as Misskey from 'misskey-js';
 import FormSuspense from '@/components/form/suspense.vue';
 import MkButton from '@/components/MkButton.vue';
 import * as os from '@/os.js';
 import { misskeyApi } from '@/scripts/misskey-api.js';
-import { getAccounts, addAccount as addAccounts, removeAccount as _removeAccount, login, $i } from '@/account.js';
+import { getAccounts, removeAccount as _removeAccount, login, $i, getAccountWithSigninDialog, getAccountWithSignupDialog } from '@/account.js';
 import { i18n } from '@/i18n.js';
 import { definePageMetadata } from '@/scripts/page-metadata.js';
 import MkUserCardMini from '@/components/MkUserCardMini.vue';
@@ -45,7 +45,7 @@ const init = async () => {
 	});
 };
 
-function menu(account, ev) {
+function menu(account: Misskey.entities.UserDetailed, ev: MouseEvent) {
 	os.popupMenu([{
 		text: i18n.ts.switch,
 		icon: 'ti ti-switch-horizontal',
@@ -58,7 +58,7 @@ function menu(account, ev) {
 	}], ev.currentTarget ?? ev.target);
 }
 
-function addAccount(ev) {
+function addAccount(ev: MouseEvent) {
 	os.popupMenu([{
 		text: i18n.ts.existingAccount,
 		action: () => { addExistingAccount(); },
@@ -68,33 +68,31 @@ function addAccount(ev) {
 	}], ev.currentTarget ?? ev.target);
 }
 
-async function removeAccount(account) {
+async function removeAccount(account: Misskey.entities.UserDetailed) {
 	await _removeAccount(account.id);
 	accounts.value = accounts.value.filter(x => x.id !== account.id);
 }
 
 function addExistingAccount() {
-	os.popup(defineAsyncComponent(() => import('@/components/MkSigninDialog.vue')), {}, {
-		done: async res => {
-			await addAccounts(res.id, res.i);
+	getAccountWithSigninDialog().then((res) => {
+		if (res != null) {
 			os.success();
 			init();
-		},
-	}, 'closed');
+		}
+	});
 }
 
 function createAccount() {
-	os.popup(defineAsyncComponent(() => import('@/components/MkSignupDialog.vue')), {}, {
-		done: async res => {
-			await addAccounts(res.id, res.i);
-			switchAccountWithToken(res.i);
-		},
-	}, 'closed');
+	getAccountWithSignupDialog().then((res) => {
+		if (res != null) {
+			switchAccountWithToken(res.token);
+		}
+	});
 }
 
-async function switchAccount(account: any) {
-	const fetchedAccounts: any[] = await getAccounts();
-	const token = fetchedAccounts.find(x => x.id === account.id).token;
+async function switchAccount(account: Misskey.entities.UserDetailed) {
+	const fetchedAccounts = await getAccounts();
+	const token = fetchedAccounts.find(x => x.id === account.id)!.token;
 	switchAccountWithToken(token);
 }
 
diff --git a/packages/frontend/src/pages/settings/api.vue b/packages/frontend/src/pages/settings/api.vue
index d9596b4e45..b35d406a98 100644
--- a/packages/frontend/src/pages/settings/api.vue
+++ b/packages/frontend/src/pages/settings/api.vue
@@ -23,7 +23,7 @@ import { definePageMetadata } from '@/scripts/page-metadata.js';
 const isDesktop = ref(window.innerWidth >= 1100);
 
 function generateToken() {
-	os.popup(defineAsyncComponent(() => import('@/components/MkTokenGenerateWindow.vue')), {}, {
+	const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkTokenGenerateWindow.vue')), {}, {
 		done: async result => {
 			const { name, permissions } = result;
 			const { token } = await misskeyApi('miauth/gen-token', {
@@ -38,7 +38,8 @@ function generateToken() {
 				text: token,
 			});
 		},
-	}, 'closed');
+		closed: () => dispose(),
+	});
 }
 
 const headerActions = computed(() => []);
diff --git a/packages/frontend/src/pages/settings/apps.vue b/packages/frontend/src/pages/settings/apps.vue
index 0e0c1f4c0c..6515503505 100644
--- a/packages/frontend/src/pages/settings/apps.vue
+++ b/packages/frontend/src/pages/settings/apps.vue
@@ -14,30 +14,39 @@ SPDX-License-Identifier: AGPL-3.0-only
 		</template>
 		<template #default="{items}">
 			<div class="_gaps">
-				<div v-for="token in items" :key="token.id" class="_panel" :class="$style.app">
-					<img v-if="token.iconUrl" :class="$style.appIcon" :src="token.iconUrl" alt=""/>
-					<div :class="$style.appBody">
-						<div :class="$style.appName">{{ token.name }}</div>
-						<div>{{ token.description }}</div>
-						<MkKeyValue oneline>
-							<template #key>{{ i18n.ts.installedDate }}</template>
-							<template #value><MkTime :time="token.createdAt"/></template>
-						</MkKeyValue>
-						<MkKeyValue oneline>
-							<template #key>{{ i18n.ts.lastUsedDate }}</template>
-							<template #value><MkTime :time="token.lastUsedAt"/></template>
-						</MkKeyValue>
-						<details>
-							<summary>{{ i18n.ts.details }}</summary>
+				<MkFolder v-for="token in items" :key="token.id" :defaultOpen="true">
+					<template #icon>
+						<img v-if="token.iconUrl" :class="$style.appIcon" :src="token.iconUrl" alt=""/>
+						<i v-else class="ti ti-plug"/>
+					</template>
+					<template #label>{{ token.name }}</template>
+					<template #caption>{{ token.description }}</template>
+					<template #suffix><MkTime :time="token.lastUsedAt"/></template>
+					<template #footer>
+						<MkButton danger @click="revoke(token)"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton>
+					</template>
+
+					<div class="_gaps_s">
+						<div v-if="token.description">{{ token.description }}</div>
+						<div>
+							<MkKeyValue oneline>
+								<template #key>{{ i18n.ts.installedDate }}</template>
+								<template #value><MkTime :time="token.createdAt" :mode="'detail'"/></template>
+							</MkKeyValue>
+							<MkKeyValue oneline>
+								<template #key>{{ i18n.ts.lastUsedDate }}</template>
+								<template #value><MkTime :time="token.lastUsedAt" :mode="'detail'"/></template>
+							</MkKeyValue>
+						</div>
+						<MkFolder>
+							<template #label>{{ i18n.ts.permission }}</template>
+							<template #suffix>{{ Object.keys(token.permission).length === 0 ? i18n.ts.none : Object.keys(token.permission).length }}</template>
 							<ul>
 								<li v-for="p in token.permission" :key="p">{{ i18n.ts._permissions[p] }}</li>
 							</ul>
-						</details>
-						<div>
-							<MkButton inline danger @click="revoke(token)"><i class="ti ti-trash"></i></MkButton>
-						</div>
+						</MkFolder>
 					</div>
-				</div>
+				</MkFolder>
 			</div>
 		</template>
 	</FormPagination>
@@ -46,12 +55,14 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <script lang="ts" setup>
 import { ref, computed } from 'vue';
+import * as Misskey from 'misskey-js';
 import FormPagination from '@/components/MkPagination.vue';
 import { misskeyApi } from '@/scripts/misskey-api.js';
 import { i18n } from '@/i18n.js';
 import { definePageMetadata } from '@/scripts/page-metadata.js';
 import MkKeyValue from '@/components/MkKeyValue.vue';
 import MkButton from '@/components/MkButton.vue';
+import MkFolder from '@/components/MkFolder.vue';
 import { infoImageUrl } from '@/instance.js';
 
 const list = ref<InstanceType<typeof FormPagination>>();
@@ -67,7 +78,7 @@ const pagination = {
 
 function revoke(token) {
 	misskeyApi('i/revoke-token', { tokenId: token.id }).then(() => {
-		list.value.reload();
+		list.value?.reload();
 	});
 }
 
@@ -82,26 +93,9 @@ definePageMetadata(() => ({
 </script>
 
 <style lang="scss" module>
-.app {
-	display: flex;
-	padding: 16px;
-}
-
 .appIcon {
-	display: block;
-	flex-shrink: 0;
-	margin: 0 12px 0 0;
-	width: 50px;
-	height: 50px;
-	border-radius: 8px;
-}
-
-.appBody {
-	width: calc(100% - 62px);
-	position: relative;
-}
-
-.appName {
-	font-weight: bold;
+	width: 20px;
+	height: 20px;
+	border-radius: 4px;
 }
 </style>
diff --git a/packages/frontend/src/pages/settings/avatar-decoration.decoration.vue b/packages/frontend/src/pages/settings/avatar-decoration.decoration.vue
index f37b53aebb..3c9914b4e2 100644
--- a/packages/frontend/src/pages/settings/avatar-decoration.decoration.vue
+++ b/packages/frontend/src/pages/settings/avatar-decoration.decoration.vue
@@ -10,12 +10,12 @@ SPDX-License-Identifier: AGPL-3.0-only
 >
 	<div :class="$style.name"><MkCondensedLine :minScale="0.5">{{ decoration.name }}</MkCondensedLine></div>
 	<MkAvatar style="width: 60px; height: 60px;" :user="$i" :decorations="[{ url: decoration.url, angle, flipH, offsetX, offsetY }]" forceShowDecoration/>
-	<i v-if="decoration.roleIdsThatCanBeUsedThisDecoration.length > 0 && !$i.roles.some(r => decoration.roleIdsThatCanBeUsedThisDecoration.includes(r.id))" :class="$style.lock" class="ti ti-lock"></i>
+	<i v-if="locked" :class="$style.lock" class="ti ti-lock"></i>
 </div>
 </template>
 
 <script lang="ts" setup>
-import { } from 'vue';
+import { computed } from 'vue';
 import { signinRequired } from '@/account.js';
 
 const $i = signinRequired();
@@ -37,13 +37,15 @@ const props = defineProps<{
 const emit = defineEmits<{
 	(ev: 'click'): void;
 }>();
+
+const locked = computed(() => props.decoration.roleIdsThatCanBeUsedThisDecoration.length > 0 && !$i.roles.some(r => props.decoration.roleIdsThatCanBeUsedThisDecoration.includes(r.id)));
 </script>
 
 <style lang="scss" module>
 .root {
 	cursor: pointer;
 	padding: 16px 16px 28px 16px;
-	border: solid 2px var(--divider);
+	border: solid 2px var(--MI_THEME-divider);
 	border-radius: 8px;
 	text-align: center;
 	font-size: 90%;
@@ -52,8 +54,8 @@ const emit = defineEmits<{
 }
 
 .active {
-	background-color: var(--accentedBg);
-	border-color: var(--accent);
+	background-color: var(--MI_THEME-accentedBg);
+	border-color: var(--MI_THEME-accent);
 }
 
 .name {
@@ -67,5 +69,6 @@ const emit = defineEmits<{
 	position: absolute;
 	bottom: 12px;
 	right: 12px;
+	color: var(--MI_THEME-warn);
 }
 </style>
diff --git a/packages/frontend/src/pages/settings/avatar-decoration.dialog.vue b/packages/frontend/src/pages/settings/avatar-decoration.dialog.vue
index ce1d4e48d8..40542ad5b2 100644
--- a/packages/frontend/src/pages/settings/avatar-decoration.dialog.vue
+++ b/packages/frontend/src/pages/settings/avatar-decoration.dialog.vue
@@ -38,7 +38,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 		<div :class="$style.footer" class="_buttonsCenter">
 			<MkButton v-if="usingIndex != null" primary rounded @click="update"><i class="ti ti-check"></i> {{ i18n.ts.update }}</MkButton>
 			<MkButton v-if="usingIndex != null" rounded @click="detach"><i class="ti ti-x"></i> {{ i18n.ts.detach }}</MkButton>
-			<MkButton v-else :disabled="exceeded" primary rounded @click="attach"><i class="ti ti-check"></i> {{ i18n.ts.attach }}</MkButton>
+			<MkButton v-else :disabled="exceeded || locked" primary rounded @click="attach"><i class="ti ti-check"></i> {{ i18n.ts.attach }}</MkButton>
 		</div>
 	</div>
 </MkModalWindow>
@@ -61,6 +61,7 @@ const props = defineProps<{
 		id: string;
 		url: string;
 		name: string;
+		roleIdsThatCanBeUsedThisDecoration: string[];
 	};
 }>();
 
@@ -83,6 +84,7 @@ const emit = defineEmits<{
 
 const dialog = shallowRef<InstanceType<typeof MkModalWindow>>();
 const exceeded = computed(() => ($i.policies.avatarDecorationLimit - $i.avatarDecorations.length) <= 0);
+const locked = computed(() => props.decoration.roleIdsThatCanBeUsedThisDecoration.length > 0 && !$i.roles.some(r => props.decoration.roleIdsThatCanBeUsedThisDecoration.includes(r.id)));
 const angle = ref((props.usingIndex != null ? $i.avatarDecorations[props.usingIndex].angle : null) ?? 0);
 const flipH = ref((props.usingIndex != null ? $i.avatarDecorations[props.usingIndex].flipH : null) ?? false);
 const offsetX = ref((props.usingIndex != null ? $i.avatarDecorations[props.usingIndex].offsetX : null) ?? 0);
@@ -96,6 +98,7 @@ const decorationsForPreview = computed(() => {
 		flipH: flipH.value,
 		offsetX: offsetX.value,
 		offsetY: offsetY.value,
+		blink: true,
 	};
 	const decorations = [...$i.avatarDecorations];
 	if (props.usingIndex != null) {
@@ -107,7 +110,7 @@ const decorationsForPreview = computed(() => {
 });
 
 function cancel() {
-	dialog.value.close();
+	dialog.value?.close();
 }
 
 async function update() {
@@ -117,7 +120,7 @@ async function update() {
 		offsetX: offsetX.value,
 		offsetY: offsetY.value,
 	});
-	dialog.value.close();
+	dialog.value?.close();
 }
 
 async function attach() {
@@ -127,12 +130,12 @@ async function attach() {
 		offsetX: offsetX.value,
 		offsetY: offsetY.value,
 	});
-	dialog.value.close();
+	dialog.value?.close();
 }
 
 async function detach() {
 	emit('detach');
-	dialog.value.close();
+	dialog.value?.close();
 }
 </script>
 
@@ -149,8 +152,8 @@ async function detach() {
 	bottom: 0;
 	left: 0;
 	padding: 12px;
-	border-top: solid 0.5px var(--divider);
-	-webkit-backdrop-filter: var(--blur, blur(15px));
-	backdrop-filter: var(--blur, blur(15px));
+	border-top: solid 0.5px var(--MI_THEME-divider);
+	-webkit-backdrop-filter: var(--MI-blur, blur(15px));
+	backdrop-filter: var(--MI-blur, blur(15px));
 }
 </style>
diff --git a/packages/frontend/src/pages/settings/avatar-decoration.vue b/packages/frontend/src/pages/settings/avatar-decoration.vue
index 3cc911c014..9fca306f9f 100644
--- a/packages/frontend/src/pages/settings/avatar-decoration.vue
+++ b/packages/frontend/src/pages/settings/avatar-decoration.vue
@@ -67,7 +67,7 @@ misskeyApi('get-avatar-decorations').then(_avatarDecorations => {
 });
 
 function openDecoration(avatarDecoration, index?: number) {
-	os.popup(defineAsyncComponent(() => import('./avatar-decoration.dialog.vue')), {
+	const { dispose } = os.popup(defineAsyncComponent(() => import('./avatar-decoration.dialog.vue')), {
 		decoration: avatarDecoration,
 		usingIndex: index,
 	}, {
@@ -108,7 +108,8 @@ function openDecoration(avatarDecoration, index?: number) {
 			});
 			$i.avatarDecorations = update;
 		},
-	}, 'closed');
+		closed: () => dispose(),
+	});
 }
 
 function detachAllDecorations() {
@@ -144,7 +145,7 @@ definePageMetadata(() => ({
 
 .current {
 	padding: 16px;
-	border-radius: var(--radius);
+	border-radius: var(--MI-radius);
 }
 
 .decorations {
diff --git a/packages/frontend/src/pages/settings/drive-cleaner.vue b/packages/frontend/src/pages/settings/drive-cleaner.vue
index b20774c4ec..6a13984dd7 100644
--- a/packages/frontend/src/pages/settings/drive-cleaner.vue
+++ b/packages/frontend/src/pages/settings/drive-cleaner.vue
@@ -48,7 +48,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script setup lang="ts">
-import { computed, ref, watch } from 'vue';
+import { computed, ref, watch, type StyleValue } from 'vue';
 import tinycolor from 'tinycolor2';
 import * as os from '@/os.js';
 import { misskeyApi } from '@/scripts/misskey-api.js';
@@ -102,10 +102,10 @@ function fetchDriveInfo(): void {
 	});
 }
 
-function genUsageBar(fsize: number): object {
+function genUsageBar(fsize: number): StyleValue {
 	return {
 		width: `${fsize / usage.value * 100}%`,
-		background: tinycolor({ h: 180 - (fsize / usage.value * 180), s: 0.7, l: 0.5 }),
+		background: tinycolor({ h: 180 - (fsize / usage.value * 180), s: 0.7, l: 0.5 }).toHslString(),
 	};
 }
 
@@ -132,7 +132,7 @@ definePageMetadata(() => ({
 	align-items: center;
 
 	&:hover {
-		color: var(--accent);
+		color: var(--MI_THEME-accent);
 	}
 }
 
diff --git a/packages/frontend/src/pages/settings/drive.vue b/packages/frontend/src/pages/settings/drive.vue
index 81a8d474d2..0e66b93f1c 100644
--- a/packages/frontend/src/pages/settings/drive.vue
+++ b/packages/frontend/src/pages/settings/drive.vue
@@ -95,7 +95,7 @@ const meterStyle = computed(() => {
 			h: 180 - (usage.value / capacity.value * 180),
 			s: 0.7,
 			l: 0.5,
-		}),
+		}).toHslString(),
 	};
 });
 
diff --git a/packages/frontend/src/pages/settings/email.vue b/packages/frontend/src/pages/settings/email.vue
index f226647569..d452f249b6 100644
--- a/packages/frontend/src/pages/settings/email.vue
+++ b/packages/frontend/src/pages/settings/email.vue
@@ -10,7 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 		<MkInput v-model="emailAddress" type="email" manualSave>
 			<template #prefix><i class="ti ti-mail"></i></template>
 			<template v-if="$i.email && !$i.emailVerified" #caption>{{ i18n.ts.verificationEmailSent }}</template>
-			<template v-else-if="emailAddress === $i.email && $i.emailVerified" #caption><i class="ti ti-check" style="color: var(--success);"></i> {{ i18n.ts.emailVerified }}</template>
+			<template v-else-if="emailAddress === $i.email && $i.emailVerified" #caption><i class="ti ti-check" style="color: var(--MI_THEME-success);"></i> {{ i18n.ts.emailVerified }}</template>
 		</MkInput>
 	</FormSection>
 
diff --git a/packages/frontend/src/pages/settings/emoji-picker.vue b/packages/frontend/src/pages/settings/emoji-picker.vue
index dc3e3ee503..fd3581d114 100644
--- a/packages/frontend/src/pages/settings/emoji-picker.vue
+++ b/packages/frontend/src/pages/settings/emoji-picker.vue
@@ -113,10 +113,13 @@ SPDX-License-Identifier: AGPL-3.0-only
 				<option :value="4">{{ i18n.ts.large }}+</option>
 			</MkRadios>
 
-			<MkSwitch v-model="emojiPickerUseDrawerForMobile">
-				{{ i18n.ts.useDrawerReactionPickerForMobile }}
+			<MkSelect v-model="emojiPickerStyle">
+				<template #label>{{ i18n.ts.style }}</template>
 				<template #caption>{{ i18n.ts.needReloadToApply }}</template>
-			</MkSwitch>
+				<option value="auto">{{ i18n.ts.auto }}</option>
+				<option value="popup">{{ i18n.ts.popup }}</option>
+				<option value="drawer">{{ i18n.ts.drawer }}</option>
+			</MkSelect>
 		</div>
 	</FormSection>
 </div>
@@ -128,7 +131,7 @@ import Sortable from 'vuedraggable';
 import MkRadios from '@/components/MkRadios.vue';
 import MkButton from '@/components/MkButton.vue';
 import FormSection from '@/components/form/section.vue';
-import MkSwitch from '@/components/MkSwitch.vue';
+import MkSelect from '@/components/MkSelect.vue';
 import * as os from '@/os.js';
 import { defaultStore } from '@/store.js';
 import { i18n } from '@/i18n.js';
@@ -146,7 +149,7 @@ const pinnedEmojis: Ref<string[]> = ref(deepClone(defaultStore.state.pinnedEmoji
 const emojiPickerScale = computed(defaultStore.makeGetterSetter('emojiPickerScale'));
 const emojiPickerWidth = computed(defaultStore.makeGetterSetter('emojiPickerWidth'));
 const emojiPickerHeight = computed(defaultStore.makeGetterSetter('emojiPickerHeight'));
-const emojiPickerUseDrawerForMobile = computed(defaultStore.makeGetterSetter('emojiPickerUseDrawerForMobile'));
+const emojiPickerStyle = computed(defaultStore.makeGetterSetter('emojiPickerStyle'));
 
 const removeReaction = (reaction: string, ev: MouseEvent) => remove(pinnedEmojisForReaction, reaction, ev);
 const chooseReaction = (ev: MouseEvent) => pickEmoji(pinnedEmojisForReaction, ev);
@@ -245,9 +248,9 @@ definePageMetadata(() => ({
 
 <style lang="scss" module>
 .tab {
-	margin: calc(var(--margin) / 2) 0;
-	padding: calc(var(--margin) / 2) 0;
-	background: var(--bg);
+	margin: calc(var(--MI-margin) / 2) 0;
+	padding: calc(var(--MI-margin) / 2) 0;
+	background: var(--MI_THEME-bg);
 }
 
 .emojis {
@@ -269,6 +272,6 @@ definePageMetadata(() => ({
 .editorCaption {
 	font-size: 0.85em;
 	padding: 8px 0 0 0;
-	color: var(--fgTransparentWeak);
+	color: var(--MI_THEME-fgTransparentWeak);
 }
 </style>
diff --git a/packages/frontend/src/pages/settings/general.vue b/packages/frontend/src/pages/settings/general.vue
index cfc63f2a08..1bfdfd0e76 100644
--- a/packages/frontend/src/pages/settings/general.vue
+++ b/packages/frontend/src/pages/settings/general.vue
@@ -17,13 +17,6 @@ SPDX-License-Identifier: AGPL-3.0-only
 		</template>
 	</MkSelect>
 
-	<MkRadios v-model="hemisphere">
-		<template #label>{{ i18n.ts.hemisphere }}</template>
-		<option value="N">{{ i18n.ts._hemisphere.N }}</option>
-		<option value="S">{{ i18n.ts._hemisphere.S }}</option>
-		<template #caption>{{ i18n.ts._hemisphere.caption }}</template>
-	</MkRadios>
-
 	<MkRadios v-model="overridedDeviceKind">
 		<template #label>{{ i18n.ts.overridedDeviceKind }}</template>
 		<option :value="null">{{ i18n.ts.auto }}</option>
@@ -132,11 +125,18 @@ SPDX-License-Identifier: AGPL-3.0-only
 				<MkSwitch v-model="squareAvatars">{{ i18n.ts.squareAvatars }}</MkSwitch>
 				<MkSwitch v-model="showAvatarDecorations">{{ i18n.ts.showAvatarDecorations }}</MkSwitch>
 				<MkSwitch v-model="useSystemFont">{{ i18n.ts.useSystemFont }}</MkSwitch>
-				<MkSwitch v-model="disableDrawer">{{ i18n.ts.disableDrawer }}</MkSwitch>
 				<MkSwitch v-model="forceShowAds">{{ i18n.ts.forceShowAds }}</MkSwitch>
 				<MkSwitch v-model="enableSeasonalScreenEffect">{{ i18n.ts.seasonalScreenEffect }}</MkSwitch>
 				<MkSwitch v-model="useNativeUIForVideoAudioPlayer">{{ i18n.ts.useNativeUIForVideoAudioPlayer }}</MkSwitch>
 			</div>
+
+			<MkSelect v-model="menuStyle">
+				<template #label>{{ i18n.ts.menuStyle }}</template>
+				<option value="auto">{{ i18n.ts.auto }}</option>
+				<option value="popup">{{ i18n.ts.popup }}</option>
+				<option value="drawer">{{ i18n.ts.drawer }}</option>
+			</MkSelect>
+
 			<div>
 				<MkRadios v-model="emojiStyle">
 					<template #label>{{ i18n.ts.emojiStyle }}</template>
@@ -169,6 +169,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 				<MkSwitch v-model="disableStreamingTimeline">{{ i18n.ts.disableStreamingTimeline }}</MkSwitch>
 				<MkSwitch v-model="enableHorizontalSwipe">{{ i18n.ts.enableHorizontalSwipe }}</MkSwitch>
 				<MkSwitch v-model="alwaysConfirmFollow">{{ i18n.ts.alwaysConfirmFollow }}</MkSwitch>
+				<MkSwitch v-model="confirmWhenRevealingSensitiveMedia">{{ i18n.ts.confirmWhenRevealingSensitiveMedia }}</MkSwitch>
 			</div>
 			<MkSelect v-model="serverDisconnectedBehavior">
 				<template #label>{{ i18n.ts.whenServerDisconnected }}</template>
@@ -176,6 +177,12 @@ SPDX-License-Identifier: AGPL-3.0-only
 				<option value="dialog">{{ i18n.ts._serverDisconnectedBehavior.dialog }}</option>
 				<option value="quiet">{{ i18n.ts._serverDisconnectedBehavior.quiet }}</option>
 			</MkSelect>
+			<MkSelect v-model="contextMenu">
+				<template #label>{{ i18n.ts._contextMenu.title }}</template>
+				<option value="app">{{ i18n.ts._contextMenu.app }}</option>
+				<option value="appWithShift">{{ i18n.ts._contextMenu.appWithShift }}</option>
+				<option value="native">{{ i18n.ts._contextMenu.native }}</option>
+			</MkSelect>
 			<MkRange v-model="numberOfPageCache" :min="1" :max="10" :step="1" easing>
 				<template #label>{{ i18n.ts.numberOfPageCache }}</template>
 				<template #caption>{{ i18n.ts.numberOfPageCacheDescription }}</template>
@@ -218,6 +225,12 @@ SPDX-License-Identifier: AGPL-3.0-only
 		<template #label>{{ i18n.ts.other }}</template>
 
 		<div class="_gaps">
+			<MkRadios v-model="hemisphere">
+				<template #label>{{ i18n.ts.hemisphere }}</template>
+				<option value="N">{{ i18n.ts._hemisphere.N }}</option>
+				<option value="S">{{ i18n.ts._hemisphere.S }}</option>
+				<template #caption>{{ i18n.ts._hemisphere.caption }}</template>
+			</MkRadios>
 			<MkFolder>
 				<template #label>{{ i18n.ts.additionalEmojiDictionary }}</template>
 				<div class="_buttons">
@@ -237,6 +250,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 <script lang="ts" setup>
 import { computed, ref, watch } from 'vue';
 import * as Misskey from 'misskey-js';
+import { langs } from '@@/js/config.js';
 import MkSwitch from '@/components/MkSwitch.vue';
 import MkSelect from '@/components/MkSelect.vue';
 import MkRadios from '@/components/MkRadios.vue';
@@ -247,11 +261,10 @@ import FormSection from '@/components/form/section.vue';
 import FormLink from '@/components/form/link.vue';
 import MkLink from '@/components/MkLink.vue';
 import MkInfo from '@/components/MkInfo.vue';
-import { langs } from '@/config.js';
 import { defaultStore } from '@/store.js';
 import * as os from '@/os.js';
 import { misskeyApi } from '@/scripts/misskey-api.js';
-import { unisonReload } from '@/scripts/unison-reload.js';
+import { reloadAsk } from '@/scripts/reload-ask.js';
 import { i18n } from '@/i18n.js';
 import { definePageMetadata } from '@/scripts/page-metadata.js';
 import { miLocalStorage } from '@/local-storage.js';
@@ -263,16 +276,6 @@ const fontSize = ref(miLocalStorage.getItem('fontSize'));
 const useSystemFont = ref(miLocalStorage.getItem('useSystemFont') != null);
 const dataSaver = ref(defaultStore.state.dataSaver);
 
-async function reloadAsk() {
-	const { canceled } = await os.confirm({
-		type: 'info',
-		text: i18n.ts.reloadToApplySetting,
-	});
-	if (canceled) return;
-
-	unisonReload();
-}
-
 const hemisphere = computed(defaultStore.makeGetterSetter('hemisphere'));
 const overridedDeviceKind = computed(defaultStore.makeGetterSetter('overridedDeviceKind'));
 const serverDisconnectedBehavior = computed(defaultStore.makeGetterSetter('serverDisconnectedBehavior'));
@@ -290,7 +293,7 @@ const advancedMfm = computed(defaultStore.makeGetterSetter('advancedMfm'));
 const showReactionsCount = computed(defaultStore.makeGetterSetter('showReactionsCount'));
 const enableQuickAddMfmFunction = computed(defaultStore.makeGetterSetter('enableQuickAddMfmFunction'));
 const emojiStyle = computed(defaultStore.makeGetterSetter('emojiStyle'));
-const disableDrawer = computed(defaultStore.makeGetterSetter('disableDrawer'));
+const menuStyle = computed(defaultStore.makeGetterSetter('menuStyle'));
 const disableShowingAnimatedImages = computed(defaultStore.makeGetterSetter('disableShowingAnimatedImages'));
 const forceShowAds = computed(defaultStore.makeGetterSetter('forceShowAds'));
 const loadRawImages = computed(defaultStore.makeGetterSetter('loadRawImages'));
@@ -315,6 +318,8 @@ const enableSeasonalScreenEffect = computed(defaultStore.makeGetterSetter('enabl
 const enableHorizontalSwipe = computed(defaultStore.makeGetterSetter('enableHorizontalSwipe'));
 const useNativeUIForVideoAudioPlayer = computed(defaultStore.makeGetterSetter('useNativeUIForVideoAudioPlayer'));
 const alwaysConfirmFollow = computed(defaultStore.makeGetterSetter('alwaysConfirmFollow'));
+const confirmWhenRevealingSensitiveMedia = computed(defaultStore.makeGetterSetter('confirmWhenRevealingSensitiveMedia'));
+const contextMenu = computed(defaultStore.makeGetterSetter('contextMenu'));
 
 watch(lang, () => {
 	miLocalStorage.setItem('lang', lang.value as string);
@@ -357,8 +362,10 @@ watch([
 	disableStreamingTimeline,
 	enableSeasonalScreenEffect,
 	alwaysConfirmFollow,
+	confirmWhenRevealingSensitiveMedia,
+	contextMenu,
 ], async () => {
-	await reloadAsk();
+	await reloadAsk({ reason: i18n.ts.reloadToApplySetting, unison: true });
 });
 
 const emojiIndexLangs = ['en-US', 'ja-JP', 'ja-JP_hira'] as const;
diff --git a/packages/frontend/src/pages/settings/import-export.vue b/packages/frontend/src/pages/settings/import-export.vue
index 9bb3957a84..5acbc50756 100644
--- a/packages/frontend/src/pages/settings/import-export.vue
+++ b/packages/frontend/src/pages/settings/import-export.vue
@@ -45,7 +45,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 					<MkButton primary :class="$style.button" inline @click="exportFollowing()"><i class="ti ti-download"></i> {{ i18n.ts.export }}</MkButton>
 				</div>
 			</MkFolder>
-			<MkFolder v-if="$i && !$i.movedTo">
+			<MkFolder v-if="$i && !$i.movedTo && $i.policies.canImportFollowing">
 				<template #label>{{ i18n.ts.import }}</template>
 				<template #icon><i class="ti ti-upload"></i></template>
 				<MkSwitch v-model="withReplies">
@@ -63,7 +63,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 				<template #icon><i class="ti ti-download"></i></template>
 				<MkButton primary :class="$style.button" inline @click="exportUserLists()"><i class="ti ti-download"></i> {{ i18n.ts.export }}</MkButton>
 			</MkFolder>
-			<MkFolder v-if="$i && !$i.movedTo">
+			<MkFolder v-if="$i && !$i.movedTo && $i.policies.canImportUserLists">
 				<template #label>{{ i18n.ts.import }}</template>
 				<template #icon><i class="ti ti-upload"></i></template>
 				<MkButton primary :class="$style.button" inline @click="importUserLists($event)"><i class="ti ti-upload"></i> {{ i18n.ts.import }}</MkButton>
@@ -78,7 +78,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 				<template #icon><i class="ti ti-download"></i></template>
 				<MkButton primary :class="$style.button" inline @click="exportMuting()"><i class="ti ti-download"></i> {{ i18n.ts.export }}</MkButton>
 			</MkFolder>
-			<MkFolder v-if="$i && !$i.movedTo">
+			<MkFolder v-if="$i && !$i.movedTo && $i.policies.canImportMuting">
 				<template #label>{{ i18n.ts.import }}</template>
 				<template #icon><i class="ti ti-upload"></i></template>
 				<MkButton primary :class="$style.button" inline @click="importMuting($event)"><i class="ti ti-upload"></i> {{ i18n.ts.import }}</MkButton>
@@ -93,7 +93,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 				<template #icon><i class="ti ti-download"></i></template>
 				<MkButton primary :class="$style.button" inline @click="exportBlocking()"><i class="ti ti-download"></i> {{ i18n.ts.export }}</MkButton>
 			</MkFolder>
-			<MkFolder v-if="$i && !$i.movedTo">
+			<MkFolder v-if="$i && !$i.movedTo && $i.policies.canImportBlocking">
 				<template #label>{{ i18n.ts.import }}</template>
 				<template #icon><i class="ti ti-upload"></i></template>
 				<MkButton primary :class="$style.button" inline @click="importBlocking($event)"><i class="ti ti-upload"></i> {{ i18n.ts.import }}</MkButton>
@@ -108,7 +108,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 				<template #icon><i class="ti ti-download"></i></template>
 				<MkButton primary :class="$style.button" inline @click="exportAntennas()"><i class="ti ti-download"></i> {{ i18n.ts.export }}</MkButton>
 			</MkFolder>
-			<MkFolder v-if="$i && !$i.movedTo">
+			<MkFolder v-if="$i && !$i.movedTo && $i.policies.canImportAntennas">
 				<template #label>{{ i18n.ts.import }}</template>
 				<template #icon><i class="ti ti-upload"></i></template>
 				<MkButton primary :class="$style.button" inline @click="importAntennas($event)"><i class="ti ti-upload"></i> {{ i18n.ts.import }}</MkButton>
diff --git a/packages/frontend/src/pages/settings/index.vue b/packages/frontend/src/pages/settings/index.vue
index 5fc1fd1bca..96a95f1635 100644
--- a/packages/frontend/src/pages/settings/index.vue
+++ b/packages/frontend/src/pages/settings/index.vue
@@ -17,7 +17,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 				</div>
 				<div v-if="!(narrow && currentPage?.route.name == null)" class="main">
 					<div class="bkzroven" style="container-type: inline-size;">
-						<RouterView/>
+						<RouterView nested/>
 					</div>
 				</div>
 			</div>
@@ -198,9 +198,6 @@ const menuDef = computed(() => [{
 	}],
 }]);
 
-watch(narrow, () => {
-});
-
 onMounted(() => {
 	ro.observe(el.value);
 
diff --git a/packages/frontend/src/pages/settings/mute-block.vue b/packages/frontend/src/pages/settings/mute-block.vue
index f4ee7dffbf..4d413d53ab 100644
--- a/packages/frontend/src/pages/settings/mute-block.vue
+++ b/packages/frontend/src/pages/settings/mute-block.vue
@@ -244,7 +244,7 @@ definePageMetadata(() => ({
 .userItemSub {
 	padding: 6px 12px;
 	font-size: 85%;
-	color: var(--fgTransparentWeak);
+	color: var(--MI_THEME-fgTransparentWeak);
 }
 
 .userItemMainBody {
diff --git a/packages/frontend/src/pages/settings/navbar.vue b/packages/frontend/src/pages/settings/navbar.vue
index 7f8460e316..b189db0f8f 100644
--- a/packages/frontend/src/pages/settings/navbar.vue
+++ b/packages/frontend/src/pages/settings/navbar.vue
@@ -54,7 +54,7 @@ import MkContainer from '@/components/MkContainer.vue';
 import * as os from '@/os.js';
 import { navbarItemDef } from '@/navbar.js';
 import { defaultStore } from '@/store.js';
-import { unisonReload } from '@/scripts/unison-reload.js';
+import { reloadAsk } from '@/scripts/reload-ask.js';
 import { i18n } from '@/i18n.js';
 import { definePageMetadata } from '@/scripts/page-metadata.js';
 
@@ -67,16 +67,6 @@ const items = ref(defaultStore.state.menu.map(x => ({
 
 const menuDisplay = computed(defaultStore.makeGetterSetter('menuDisplay'));
 
-async function reloadAsk() {
-	const { canceled } = await os.confirm({
-		type: 'info',
-		text: i18n.ts.reloadToApplySetting,
-	});
-	if (canceled) return;
-
-	unisonReload();
-}
-
 async function addItem() {
 	const menu = Object.keys(navbarItemDef).filter(k => !defaultStore.state.menu.includes(k));
 	const { canceled, result: item } = await os.select({
@@ -100,7 +90,7 @@ function removeItem(index: number) {
 
 async function save() {
 	defaultStore.set('menu', items.value.map(x => x.type));
-	await reloadAsk();
+	await reloadAsk({ reason: i18n.ts.reloadToApplySetting, unison: true });
 }
 
 function reset() {
@@ -111,7 +101,7 @@ function reset() {
 }
 
 watch(menuDisplay, async () => {
-	await reloadAsk();
+	await reloadAsk({ reason: i18n.ts.reloadToApplySetting, unison: true });
 });
 
 const headerActions = computed(() => []);
@@ -132,7 +122,7 @@ definePageMetadata(() => ({
 	text-overflow: ellipsis;
 	overflow: hidden;
 	white-space: nowrap;
-	color: var(--navFg);
+	color: var(--MI_THEME-navFg);
 }
 
 .itemIcon {
diff --git a/packages/frontend/src/pages/settings/notifications.notification-config.vue b/packages/frontend/src/pages/settings/notifications.notification-config.vue
index a36f036303..0ea415f673 100644
--- a/packages/frontend/src/pages/settings/notifications.notification-config.vue
+++ b/packages/frontend/src/pages/settings/notifications.notification-config.vue
@@ -6,13 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 <template>
 <div class="_gaps_m">
 	<MkSelect v-model="type">
-		<option value="all">{{ i18n.ts.all }}</option>
-		<option value="following">{{ i18n.ts.following }}</option>
-		<option value="follower">{{ i18n.ts.followers }}</option>
-		<option value="mutualFollow">{{ i18n.ts.mutualFollow }}</option>
-		<option value="followingOrFollower">{{ i18n.ts.followingOrFollower }}</option>
-		<option value="list">{{ i18n.ts.userList }}</option>
-		<option value="never">{{ i18n.ts.none }}</option>
+		<option v-for="type in props.configurableTypes ?? notificationConfigTypes" :key="type" :value="type">{{ notificationConfigTypesI18nMap[type] }}</option>
 	</MkSelect>
 
 	<MkSelect v-if="type === 'list'" v-model="userListId">
@@ -21,31 +15,61 @@ SPDX-License-Identifier: AGPL-3.0-only
 	</MkSelect>
 
 	<div class="_buttons">
-		<MkButton inline primary @click="save"><i class="ti ti-check"></i> {{ i18n.ts.save }}</MkButton>
+		<MkButton inline primary :disabled="type === 'list' && userListId === null" @click="save"><i class="ti ti-check"></i> {{ i18n.ts.save }}</MkButton>
 	</div>
 </div>
 </template>
 
+<script lang="ts">
+const notificationConfigTypes = [
+	'all',
+	'following',
+	'follower',
+	'mutualFollow',
+	'followingOrFollower',
+	'list',
+	'never'
+] as const;
+
+export type NotificationConfig = {
+	type: Exclude<typeof notificationConfigTypes[number], 'list'>;
+} | {
+	type: 'list';
+	userListId: string;
+};
+</script>
+
 <script lang="ts" setup>
-import { ref } from 'vue';
 import * as Misskey from 'misskey-js';
+import { ref } from 'vue';
 import MkSelect from '@/components/MkSelect.vue';
 import MkButton from '@/components/MkButton.vue';
 import { i18n } from '@/i18n.js';
 
 const props = defineProps<{
-	value: any;
+	value: NotificationConfig;
 	userLists: Misskey.entities.UserList[];
+	configurableTypes?: NotificationConfig['type'][]; // If not specified, all types are configurable
 }>();
 
 const emit = defineEmits<{
-	(ev: 'update', result: any): void;
+	(ev: 'update', result: NotificationConfig): void;
 }>();
 
+const notificationConfigTypesI18nMap: Record<typeof notificationConfigTypes[number], string> = {
+	all: i18n.ts.all,
+	following: i18n.ts.following,
+	follower: i18n.ts.followers,
+	mutualFollow: i18n.ts.mutualFollow,
+	followingOrFollower: i18n.ts.followingOrFollower,
+	list: i18n.ts.userList,
+	never: i18n.ts.none,
+};
+
 const type = ref(props.value.type);
-const userListId = ref(props.value.userListId);
+const userListId = ref(props.value.type === 'list' ? props.value.userListId : null);
 
 function save() {
-	emit('update', { type: type.value, userListId: userListId.value });
+	emit('update', type.value === 'list' ? { type: type.value, userListId: userListId.value! } : { type: type.value });
 }
 </script>
diff --git a/packages/frontend/src/pages/settings/notifications.vue b/packages/frontend/src/pages/settings/notifications.vue
index 70db6a5109..8ffe0d6a7a 100644
--- a/packages/frontend/src/pages/settings/notifications.vue
+++ b/packages/frontend/src/pages/settings/notifications.vue
@@ -22,7 +22,12 @@ SPDX-License-Identifier: AGPL-3.0-only
 					}}
 				</template>
 
-				<XNotificationConfig :userLists="userLists" :value="$i.notificationRecieveConfig[type] ?? { type: 'all' }" @update="(res) => updateReceiveConfig(type, res)"/>
+				<XNotificationConfig
+					:userLists="userLists"
+					:value="$i.notificationRecieveConfig[type] ?? { type: 'all' }"
+					:configurableTypes="onlyOnOrOffNotificationTypes.includes(type) ? ['all', 'never'] : undefined"
+					@update="(res) => updateReceiveConfig(type, res)"
+				/>
 			</MkFolder>
 		</div>
 	</FormSection>
@@ -58,7 +63,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <script lang="ts" setup>
 import { shallowRef, computed } from 'vue';
-import XNotificationConfig from './notifications.notification-config.vue';
+import XNotificationConfig, { type NotificationConfig } from './notifications.notification-config.vue';
 import FormLink from '@/components/form/link.vue';
 import FormSection from '@/components/form/section.vue';
 import MkFolder from '@/components/MkFolder.vue';
@@ -69,11 +74,13 @@ import { misskeyApi } from '@/scripts/misskey-api.js';
 import { i18n } from '@/i18n.js';
 import { definePageMetadata } from '@/scripts/page-metadata.js';
 import MkPushNotificationAllowButton from '@/components/MkPushNotificationAllowButton.vue';
-import { notificationTypes } from '@/const.js';
+import { notificationTypes } from '@@/js/const.js';
 
 const $i = signinRequired();
 
-const nonConfigurableNotificationTypes = ['note', 'roleAssigned', 'followRequestAccepted', 'achievementEarned'];
+const nonConfigurableNotificationTypes = ['note', 'roleAssigned', 'followRequestAccepted', 'test', 'exportCompleted'] satisfies (typeof notificationTypes[number])[] as string[];
+
+const onlyOnOrOffNotificationTypes = ['app', 'achievementEarned', 'login'] satisfies (typeof notificationTypes[number])[] as string[];
 
 const allowButton = shallowRef<InstanceType<typeof MkPushNotificationAllowButton>>();
 const pushRegistrationInServer = computed(() => allowButton.value?.pushRegistrationInServer);
@@ -88,7 +95,7 @@ async function readAllNotifications() {
 	await os.apiWithDialog('notifications/mark-all-as-read');
 }
 
-async function updateReceiveConfig(type, value) {
+async function updateReceiveConfig(type: typeof notificationTypes[number], value: NotificationConfig) {
 	await os.apiWithDialog('i/update', {
 		notificationRecieveConfig: {
 			...$i.notificationRecieveConfig,
diff --git a/packages/frontend/src/pages/settings/other.vue b/packages/frontend/src/pages/settings/other.vue
index a1cb2ea1c4..4a52e59d02 100644
--- a/packages/frontend/src/pages/settings/other.vue
+++ b/packages/frontend/src/pages/settings/other.vue
@@ -51,8 +51,11 @@ SPDX-License-Identifier: AGPL-3.0-only
 				<template #label>{{ i18n.ts.experimentalFeatures }}</template>
 
 				<div class="_gaps_m">
-					<MkSwitch v-model="enableCondensedLineForAcct">
-						<template #label>Enable condensed line for acct</template>
+					<MkSwitch v-model="enableCondensedLine">
+						<template #label>Enable condensed line</template>
+					</MkSwitch>
+					<MkSwitch v-model="skipNoteRender">
+						<template #label>Enable note render skipping</template>
 					</MkSwitch>
 				</div>
 			</MkFolder>
@@ -98,16 +101,21 @@ import { defaultStore } from '@/store.js';
 import { signout, signinRequired } from '@/account.js';
 import { i18n } from '@/i18n.js';
 import { definePageMetadata } from '@/scripts/page-metadata.js';
-import { unisonReload } from '@/scripts/unison-reload.js';
+import { reloadAsk } from '@/scripts/reload-ask.js';
 import FormSection from '@/components/form/section.vue';
 
 const $i = signinRequired();
 
 const reportError = computed(defaultStore.makeGetterSetter('reportError'));
-const enableCondensedLineForAcct = computed(defaultStore.makeGetterSetter('enableCondensedLineForAcct'));
+const enableCondensedLine = computed(defaultStore.makeGetterSetter('enableCondensedLine'));
+const skipNoteRender = computed(defaultStore.makeGetterSetter('skipNoteRender'));
 const devMode = computed(defaultStore.makeGetterSetter('devMode'));
 const defaultWithReplies = computed(defaultStore.makeGetterSetter('defaultWithReplies'));
 
+watch(skipNoteRender, async () => {
+	await reloadAsk({ reason: i18n.ts.reloadToApplySetting, unison: true });
+});
+
 async function deleteAccount() {
 	{
 		const { canceled } = await os.confirm({
@@ -132,16 +140,6 @@ async function deleteAccount() {
 	await signout();
 }
 
-async function reloadAsk() {
-	const { canceled } = await os.confirm({
-		type: 'info',
-		text: i18n.ts.reloadToApplySetting,
-	});
-	if (canceled) return;
-
-	unisonReload();
-}
-
 async function updateRepliesAll(withReplies: boolean) {
 	const { canceled } = await os.confirm({
 		type: 'warning',
@@ -152,12 +150,6 @@ async function updateRepliesAll(withReplies: boolean) {
 	misskeyApi('following/update-all', { withReplies });
 }
 
-watch([
-	enableCondensedLineForAcct,
-], async () => {
-	await reloadAsk();
-});
-
 const headerActions = computed(() => []);
 
 const headerTabs = computed(() => []);
diff --git a/packages/frontend/src/pages/settings/plugin.vue b/packages/frontend/src/pages/settings/plugin.vue
index 9804454e66..3c3dcfe41e 100644
--- a/packages/frontend/src/pages/settings/plugin.vue
+++ b/packages/frontend/src/pages/settings/plugin.vue
@@ -82,7 +82,7 @@ import MkCode from '@/components/MkCode.vue';
 import MkFolder from '@/components/MkFolder.vue';
 import MkKeyValue from '@/components/MkKeyValue.vue';
 import * as os from '@/os.js';
-import copyToClipboard from '@/scripts/copy-to-clipboard.js';
+import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
 import { ColdDeviceStorage } from '@/store.js';
 import { unisonReload } from '@/scripts/unison-reload.js';
 import { i18n } from '@/i18n.js';
diff --git a/packages/frontend/src/pages/settings/preferences-backups.vue b/packages/frontend/src/pages/settings/preferences-backups.vue
index b6f1043154..7388e014ed 100644
--- a/packages/frontend/src/pages/settings/preferences-backups.vue
+++ b/packages/frontend/src/pages/settings/preferences-backups.vue
@@ -6,12 +6,12 @@ SPDX-License-Identifier: AGPL-3.0-only
 <template>
 <div class="_gaps_m">
 	<div :class="$style.buttons">
-		<MkButton inline primary @click="saveNew">{{ ts._preferencesBackups.saveNew }}</MkButton>
-		<MkButton inline @click="loadFile">{{ ts._preferencesBackups.loadFile }}</MkButton>
+		<MkButton inline primary @click="saveNew">{{ i18n.ts._preferencesBackups.saveNew }}</MkButton>
+		<MkButton inline @click="loadFile">{{ i18n.ts._preferencesBackups.loadFile }}</MkButton>
 	</div>
 
 	<FormSection>
-		<template #label>{{ ts._preferencesBackups.list }}</template>
+		<template #label>{{ i18n.ts._preferencesBackups.list }}</template>
 		<template v-if="profiles && Object.keys(profiles).length > 0">
 			<div class="_gaps_s">
 				<div
@@ -23,13 +23,13 @@ SPDX-License-Identifier: AGPL-3.0-only
 					@contextmenu.prevent.stop="$event => menu($event, id)"
 				>
 					<div :class="$style.profileName">{{ profile.name }}</div>
-					<div :class="$style.profileTime">{{ t('_preferencesBackups.createdAt', { date: (new Date(profile.createdAt)).toLocaleDateString(), time: (new Date(profile.createdAt)).toLocaleTimeString() }) }}</div>
-					<div v-if="profile.updatedAt" :class="$style.profileTime">{{ t('_preferencesBackups.updatedAt', { date: (new Date(profile.updatedAt)).toLocaleDateString(), time: (new Date(profile.updatedAt)).toLocaleTimeString() }) }}</div>
+					<div :class="$style.profileTime">{{ i18n.tsx._preferencesBackups.createdAt({ date: (new Date(profile.createdAt)).toLocaleDateString(), time: (new Date(profile.createdAt)).toLocaleTimeString() }) }}</div>
+					<div v-if="profile.updatedAt" :class="$style.profileTime">{{ i18n.tsx._preferencesBackups.updatedAt({ date: (new Date(profile.updatedAt)).toLocaleDateString(), time: (new Date(profile.updatedAt)).toLocaleTimeString() }) }}</div>
 				</div>
 			</div>
 		</template>
 		<div v-else-if="profiles">
-			<MkInfo>{{ ts._preferencesBackups.noBackups }}</MkInfo>
+			<MkInfo>{{ i18n.ts._preferencesBackups.noBackups }}</MkInfo>
 		</div>
 		<MkLoading v-else/>
 	</FormSection>
@@ -39,6 +39,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 <script lang="ts" setup>
 import { onMounted, onUnmounted, ref } from 'vue';
 import { v4 as uuid } from 'uuid';
+import { version, host } from '@@/js/config.js';
 import FormSection from '@/components/form/section.vue';
 import MkButton from '@/components/MkButton.vue';
 import MkInfo from '@/components/MkInfo.vue';
@@ -49,10 +50,8 @@ import { unisonReload } from '@/scripts/unison-reload.js';
 import { useStream } from '@/stream.js';
 import { $i } from '@/account.js';
 import { i18n } from '@/i18n.js';
-import { version, host } from '@/config.js';
 import { definePageMetadata } from '@/scripts/page-metadata.js';
 import { miLocalStorage } from '@/local-storage.js';
-const { t, ts } = i18n;
 
 const defaultStoreSaveKeys: (keyof typeof defaultStore['state'])[] = [
 	'collapseRenotes',
@@ -76,7 +75,7 @@ const defaultStoreSaveKeys: (keyof typeof defaultStore['state'])[] = [
 	'dataSaver',
 	'disableShowingAnimatedImages',
 	'emojiStyle',
-	'disableDrawer',
+	'menuStyle',
 	'useBlurEffectForModal',
 	'useBlurEffect',
 	'showFixedPostForm',
@@ -88,7 +87,7 @@ const defaultStoreSaveKeys: (keyof typeof defaultStore['state'])[] = [
 	'emojiPickerScale',
 	'emojiPickerWidth',
 	'emojiPickerHeight',
-	'emojiPickerUseDrawerForMobile',
+	'emojiPickerStyle',
 	'defaultSideView',
 	'menuDisplay',
 	'reportError',
@@ -104,7 +103,6 @@ const defaultStoreSaveKeys: (keyof typeof defaultStore['state'])[] = [
 	'mediaListWithOneImageAppearance',
 	'notificationPosition',
 	'notificationStackAxis',
-	'enableCondensedLineForAcct',
 	'keepScreenOn',
 	'defaultWithReplies',
 	'disableStreamingTimeline',
@@ -113,8 +111,6 @@ const defaultStoreSaveKeys: (keyof typeof defaultStore['state'])[] = [
 	'sound_note',
 	'sound_noteMy',
 	'sound_notification',
-	'sound_antenna',
-	'sound_channel',
 ];
 const coldDeviceStorageSaveKeys: (keyof typeof ColdDeviceStorage.default)[] = [
 	'lightTheme',
@@ -203,15 +199,15 @@ async function saveNew(): Promise<void> {
 	if (!profiles.value) return;
 
 	const { canceled, result: name } = await os.inputText({
-		title: ts._preferencesBackups.inputName,
+		title: i18n.ts._preferencesBackups.inputName,
 		default: '',
 	});
 	if (canceled) return;
 
 	if (Object.values(profiles.value).some(x => x.name === name)) {
 		return os.alert({
-			title: ts._preferencesBackups.cannotSave,
-			text: t('_preferencesBackups.nameAlreadyExists', { name }),
+			title: i18n.ts._preferencesBackups.cannotSave,
+			text: i18n.tsx._preferencesBackups.nameAlreadyExists({ name }),
 		});
 	}
 
@@ -240,8 +236,8 @@ function loadFile(): void {
 		if (file.type !== 'application/json') {
 			return os.alert({
 				type: 'error',
-				title: ts._preferencesBackups.cannotLoad,
-				text: ts._preferencesBackups.invalidFile,
+				title: i18n.ts._preferencesBackups.cannotLoad,
+				text: i18n.ts._preferencesBackups.invalidFile,
 			});
 		}
 
@@ -252,7 +248,7 @@ function loadFile(): void {
 		} catch (err) {
 			return os.alert({
 				type: 'error',
-				title: ts._preferencesBackups.cannotLoad,
+				title: i18n.ts._preferencesBackups.cannotLoad,
 				text: (err as any)?.message ?? '',
 			});
 		}
@@ -278,8 +274,8 @@ async function applyProfile(id: string): Promise<void> {
 
 	const { canceled: cancel1 } = await os.confirm({
 		type: 'warning',
-		title: ts._preferencesBackups.apply,
-		text: t('_preferencesBackups.applyConfirm', { name: profile.name }),
+		title: i18n.ts._preferencesBackups.apply,
+		text: i18n.tsx._preferencesBackups.applyConfirm({ name: profile.name }),
 	});
 	if (cancel1) return;
 
@@ -324,7 +320,7 @@ async function applyProfile(id: string): Promise<void> {
 
 	const { canceled: cancel2 } = await os.confirm({
 		type: 'info',
-		text: ts.reloadToApplySetting,
+		text: i18n.ts.reloadToApplySetting,
 	});
 	if (cancel2) return;
 
@@ -336,8 +332,8 @@ async function deleteProfile(id: string): Promise<void> {
 
 	const { canceled } = await os.confirm({
 		type: 'info',
-		title: ts.delete,
-		text: t('deleteAreYouSure', { x: profiles.value[id].name }),
+		title: i18n.ts.delete,
+		text: i18n.tsx.deleteAreYouSure({ x: profiles.value[id].name }),
 	});
 	if (canceled) return;
 
@@ -352,8 +348,8 @@ async function save(id: string): Promise<void> {
 
 	const { canceled } = await os.confirm({
 		type: 'info',
-		title: ts._preferencesBackups.save,
-		text: t('_preferencesBackups.saveConfirm', { name }),
+		title: i18n.ts._preferencesBackups.save,
+		text: i18n.tsx._preferencesBackups.saveConfirm({ name }),
 	});
 	if (canceled) return;
 
@@ -372,15 +368,15 @@ async function rename(id: string): Promise<void> {
 	if (!profiles.value) return;
 
 	const { canceled: cancel1, result: name } = await os.inputText({
-		title: ts._preferencesBackups.inputName,
+		title: i18n.ts._preferencesBackups.inputName,
 		default: '',
 	});
 	if (cancel1 || profiles.value[id].name === name) return;
 
 	if (Object.values(profiles.value).some(x => x.name === name)) {
 		return os.alert({
-			title: ts._preferencesBackups.cannotSave,
-			text: t('_preferencesBackups.nameAlreadyExists', { name }),
+			title: i18n.ts._preferencesBackups.cannotSave,
+			text: i18n.tsx._preferencesBackups.nameAlreadyExists({ name }),
 		});
 	}
 
@@ -388,8 +384,8 @@ async function rename(id: string): Promise<void> {
 
 	const { canceled: cancel2 } = await os.confirm({
 		type: 'info',
-		title: ts.rename,
-		text: t('_preferencesBackups.renameConfirm', { old: registry.name, new: name }),
+		title: i18n.ts.rename,
+		text: i18n.tsx._preferencesBackups.renameConfirm({ old: registry.name, new: name }),
 	});
 	if (cancel2) return;
 
@@ -401,25 +397,25 @@ function menu(ev: MouseEvent, profileId: string) {
 	if (!profiles.value) return;
 
 	return os.popupMenu([{
-		text: ts._preferencesBackups.apply,
+		text: i18n.ts._preferencesBackups.apply,
 		icon: 'ti ti-check',
 		action: () => applyProfile(profileId),
 	}, {
 		type: 'a',
-		text: ts.download,
+		text: i18n.ts.download,
 		icon: 'ti ti-download',
 		href: URL.createObjectURL(new Blob([JSON.stringify(profiles.value[profileId], null, 2)], { type: 'application/json' })),
 		download: `${profiles.value[profileId].name}.json`,
 	}, { type: 'divider' }, {
-		text: ts.rename,
+		text: i18n.ts.rename,
 		icon: 'ti ti-forms',
 		action: () => rename(profileId),
 	}, {
-		text: ts._preferencesBackups.save,
+		text: i18n.ts._preferencesBackups.save,
 		icon: 'ti ti-device-floppy',
 		action: () => save(profileId),
 	}, { type: 'divider' }, {
-		text: ts.delete,
+		text: i18n.ts.delete,
 		icon: 'ti ti-trash',
 		action: () => deleteProfile(profileId),
 		danger: true,
@@ -441,7 +437,7 @@ onUnmounted(() => {
 });
 
 definePageMetadata(() => ({
-	title: ts.preferencesBackups,
+	title: i18n.ts.preferencesBackups,
 	icon: 'ti ti-device-floppy',
 }));
 </script>
@@ -449,7 +445,7 @@ definePageMetadata(() => ({
 <style lang="scss" module>
 .buttons {
 	display: flex;
-	gap: var(--margin);
+	gap: var(--MI-margin);
 	flex-wrap: wrap;
 }
 
diff --git a/packages/frontend/src/pages/settings/privacy.vue b/packages/frontend/src/pages/settings/privacy.vue
index d418be624e..da3d36b31a 100644
--- a/packages/frontend/src/pages/settings/privacy.vue
+++ b/packages/frontend/src/pages/settings/privacy.vue
@@ -36,7 +36,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 		<template #caption>{{ i18n.ts.noCrawleDescription }}</template>
 	</MkSwitch>
 	<MkSwitch v-model="preventAiLearning" @update:modelValue="save()">
-		{{ i18n.ts.preventAiLearning }}<span class="_beta">{{ i18n.ts.beta }}</span>
+		{{ i18n.ts.preventAiLearning }}
 		<template #caption>{{ i18n.ts.preventAiLearningDescription }}</template>
 	</MkSwitch>
 	<MkSwitch v-model="isExplorable" @update:modelValue="save()">
@@ -44,6 +44,93 @@ SPDX-License-Identifier: AGPL-3.0-only
 		<template #caption>{{ i18n.ts.makeExplorableDescription }}</template>
 	</MkSwitch>
 
+	<FormSection>
+		<template #label>{{ i18n.ts.lockdown }}<span class="_beta">{{ i18n.ts.beta }}</span></template>
+
+		<div class="_gaps_m">
+			<MkSwitch v-model="requireSigninToViewContents" @update:modelValue="save()">
+				{{ i18n.ts._accountSettings.requireSigninToViewContents }}
+				<template #caption>
+					<div>{{ i18n.ts._accountSettings.requireSigninToViewContentsDescription1 }}</div>
+					<div><i class="ti ti-alert-triangle" style="color: var(--MI_THEME-warn);"></i> {{ i18n.ts._accountSettings.requireSigninToViewContentsDescription2 }}</div>
+					<div><i class="ti ti-alert-triangle" style="color: var(--MI_THEME-warn);"></i> {{ i18n.ts._accountSettings.requireSigninToViewContentsDescription3 }}</div>
+				</template>
+			</MkSwitch>
+
+			<FormSlot>
+				<template #label>{{ i18n.ts._accountSettings.makeNotesFollowersOnlyBefore }}</template>
+
+				<div class="_gaps_s">
+					<MkSelect :modelValue="makeNotesFollowersOnlyBefore_type" @update:modelValue="makeNotesFollowersOnlyBefore = $event === 'relative' ? -604800 : $event === 'absolute' ? Math.floor(Date.now() / 1000) : null">
+						<option :value="null">{{ i18n.ts.none }}</option>
+						<option value="relative">{{ i18n.ts._accountSettings.notesHavePassedSpecifiedPeriod }}</option>
+						<option value="absolute">{{ i18n.ts._accountSettings.notesOlderThanSpecifiedDateAndTime }}</option>
+					</MkSelect>
+
+					<MkSelect v-if="makeNotesFollowersOnlyBefore_type === 'relative'" v-model="makeNotesFollowersOnlyBefore">
+						<option :value="-3600">{{ i18n.ts.oneHour }}</option>
+						<option :value="-86400">{{ i18n.ts.oneDay }}</option>
+						<option :value="-259200">{{ i18n.ts.threeDays }}</option>
+						<option :value="-604800">{{ i18n.ts.oneWeek }}</option>
+						<option :value="-2592000">{{ i18n.ts.oneMonth }}</option>
+						<option :value="-7776000">{{ i18n.ts.threeMonths }}</option>
+						<option :value="-31104000">{{ i18n.ts.oneYear }}</option>
+					</MkSelect>
+
+					<MkInput
+						v-if="makeNotesFollowersOnlyBefore_type === 'absolute'"
+						:modelValue="formatDateTimeString(new Date(makeNotesFollowersOnlyBefore * 1000), 'yyyy-MM-dd')"
+						type="date"
+						:manualSave="true"
+						@update:modelValue="makeNotesFollowersOnlyBefore = Math.floor(new Date($event).getTime() / 1000)"
+					>
+					</MkInput>
+				</div>
+
+				<template #caption>
+					<div>{{ i18n.ts._accountSettings.makeNotesFollowersOnlyBeforeDescription }}</div>
+					<div><i class="ti ti-alert-triangle" style="color: var(--MI_THEME-warn);"></i> {{ i18n.ts._accountSettings.mayNotEffectForFederatedNotes }}</div>
+				</template>
+			</FormSlot>
+
+			<FormSlot>
+				<template #label>{{ i18n.ts._accountSettings.makeNotesHiddenBefore }}</template>
+
+				<div class="_gaps_s">
+					<MkSelect :modelValue="makeNotesHiddenBefore_type" @update:modelValue="makeNotesHiddenBefore = $event === 'relative' ? -604800 : $event === 'absolute' ? Math.floor(Date.now() / 1000) : null">
+						<option :value="null">{{ i18n.ts.none }}</option>
+						<option value="relative">{{ i18n.ts._accountSettings.notesHavePassedSpecifiedPeriod }}</option>
+						<option value="absolute">{{ i18n.ts._accountSettings.notesOlderThanSpecifiedDateAndTime }}</option>
+					</MkSelect>
+
+					<MkSelect v-if="makeNotesHiddenBefore_type === 'relative'" v-model="makeNotesHiddenBefore">
+						<option :value="-3600">{{ i18n.ts.oneHour }}</option>
+						<option :value="-86400">{{ i18n.ts.oneDay }}</option>
+						<option :value="-259200">{{ i18n.ts.threeDays }}</option>
+						<option :value="-604800">{{ i18n.ts.oneWeek }}</option>
+						<option :value="-2592000">{{ i18n.ts.oneMonth }}</option>
+						<option :value="-7776000">{{ i18n.ts.threeMonths }}</option>
+						<option :value="-31104000">{{ i18n.ts.oneYear }}</option>
+					</MkSelect>
+
+					<MkInput
+						v-if="makeNotesHiddenBefore_type === 'absolute'"
+						:modelValue="formatDateTimeString(new Date(makeNotesHiddenBefore * 1000), 'yyyy-MM-dd')"
+						type="date"
+						:manualSave="true"
+						@update:modelValue="makeNotesHiddenBefore = Math.floor(new Date($event).getTime() / 1000)"
+					>
+					</MkInput>
+				</div>
+
+				<template #caption>
+					<div>{{ i18n.ts._accountSettings.makeNotesHiddenBeforeDescription }}</div>
+					<div><i class="ti ti-alert-triangle" style="color: var(--MI_THEME-warn);"></i> {{ i18n.ts._accountSettings.mayNotEffectForFederatedNotes }}</div>
+				</template>
+			</FormSlot>
+		</div>
+	</FormSection>
+
 	<FormSection>
 		<div class="_gaps_m">
 			<MkSwitch v-model="rememberNoteVisibility" @update:modelValue="save()">{{ i18n.ts.rememberNoteVisibility }}</MkSwitch>
@@ -72,7 +159,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { ref, computed } from 'vue';
+import { ref, computed, watch } from 'vue';
 import MkSwitch from '@/components/MkSwitch.vue';
 import MkSelect from '@/components/MkSelect.vue';
 import FormSection from '@/components/form/section.vue';
@@ -82,6 +169,9 @@ import { defaultStore } from '@/store.js';
 import { i18n } from '@/i18n.js';
 import { signinRequired } from '@/account.js';
 import { definePageMetadata } from '@/scripts/page-metadata.js';
+import FormSlot from '@/components/form/slot.vue';
+import { formatDateTimeString } from '@/scripts/format-time-string.js';
+import MkInput from '@/components/MkInput.vue';
 
 const $i = signinRequired();
 
@@ -90,6 +180,9 @@ const autoAcceptFollowed = ref($i.autoAcceptFollowed);
 const noCrawle = ref($i.noCrawle);
 const preventAiLearning = ref($i.preventAiLearning);
 const isExplorable = ref($i.isExplorable);
+const requireSigninToViewContents = ref($i.requireSigninToViewContents ?? false);
+const makeNotesFollowersOnlyBefore = ref($i.makeNotesFollowersOnlyBefore ?? null);
+const makeNotesHiddenBefore = ref($i.makeNotesHiddenBefore ?? null);
 const hideOnlineStatus = ref($i.hideOnlineStatus);
 const publicReactions = ref($i.publicReactions);
 const followingVisibility = ref($i.followingVisibility);
@@ -100,6 +193,30 @@ const defaultNoteLocalOnly = computed(defaultStore.makeGetterSetter('defaultNote
 const rememberNoteVisibility = computed(defaultStore.makeGetterSetter('rememberNoteVisibility'));
 const keepCw = computed(defaultStore.makeGetterSetter('keepCw'));
 
+const makeNotesFollowersOnlyBefore_type = computed(() => {
+	if (makeNotesFollowersOnlyBefore.value == null) {
+		return null;
+	} else if (makeNotesFollowersOnlyBefore.value >= 0) {
+		return 'absolute';
+	} else {
+		return 'relative';
+	}
+});
+
+const makeNotesHiddenBefore_type = computed(() => {
+	if (makeNotesHiddenBefore.value == null) {
+		return null;
+	} else if (makeNotesHiddenBefore.value >= 0) {
+		return 'absolute';
+	} else {
+		return 'relative';
+	}
+});
+
+watch([makeNotesFollowersOnlyBefore, makeNotesHiddenBefore], () => {
+	save();
+});
+
 function save() {
 	misskeyApi('i/update', {
 		isLocked: !!isLocked.value,
@@ -107,6 +224,9 @@ function save() {
 		noCrawle: !!noCrawle.value,
 		preventAiLearning: !!preventAiLearning.value,
 		isExplorable: !!isExplorable.value,
+		requireSigninToViewContents: !!requireSigninToViewContents.value,
+		makeNotesFollowersOnlyBefore: makeNotesFollowersOnlyBefore.value,
+		makeNotesHiddenBefore: makeNotesHiddenBefore.value,
 		hideOnlineStatus: !!hideOnlineStatus.value,
 		publicReactions: !!publicReactions.value,
 		followingVisibility: followingVisibility.value,
diff --git a/packages/frontend/src/pages/settings/profile.vue b/packages/frontend/src/pages/settings/profile.vue
index 60bf9b4d3d..561894d2b7 100644
--- a/packages/frontend/src/pages/settings/profile.vue
+++ b/packages/frontend/src/pages/settings/profile.vue
@@ -46,14 +46,17 @@ SPDX-License-Identifier: AGPL-3.0-only
 		<MkFolder>
 			<template #icon><i class="ti ti-list"></i></template>
 			<template #label>{{ i18n.ts._profile.metadataEdit }}</template>
-
-			<div :class="$style.metadataRoot">
-				<div :class="$style.metadataMargin">
-					<MkButton :disabled="fields.length >= 16" inline style="margin-right: 8px;" @click="addField"><i class="ti ti-plus"></i> {{ i18n.ts.add }}</MkButton>
-					<MkButton v-if="!fieldEditMode" :disabled="fields.length <= 1" inline danger style="margin-right: 8px;" @click="fieldEditMode = !fieldEditMode"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton>
-					<MkButton v-else inline style="margin-right: 8px;" @click="fieldEditMode = !fieldEditMode"><i class="ti ti-arrows-sort"></i> {{ i18n.ts.rearrange }}</MkButton>
-					<MkButton inline primary @click="saveFields"><i class="ti ti-check"></i> {{ i18n.ts.save }}</MkButton>
+			<template #footer>
+				<div class="_buttons">
+					<MkButton primary @click="saveFields"><i class="ti ti-check"></i> {{ i18n.ts.save }}</MkButton>
+					<MkButton :disabled="fields.length >= 16" @click="addField"><i class="ti ti-plus"></i> {{ i18n.ts.add }}</MkButton>
+					<MkButton v-if="!fieldEditMode" :disabled="fields.length <= 1" danger @click="fieldEditMode = !fieldEditMode"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton>
+					<MkButton v-else @click="fieldEditMode = !fieldEditMode"><i class="ti ti-arrows-sort"></i> {{ i18n.ts.rearrange }}</MkButton>
 				</div>
+			</template>
+
+			<div :class="$style.metadataRoot" class="_gaps_s">
+				<MkInfo>{{ i18n.ts._profile.verifiedLinkDescription }}</MkInfo>
 
 				<Sortable
 					v-model="fields"
@@ -65,37 +68,32 @@ SPDX-License-Identifier: AGPL-3.0-only
 					@end="e => e.item.classList.remove('active')"
 				>
 					<template #item="{element, index}">
-						<div :class="$style.fieldDragItem">
+						<div v-panel :class="$style.fieldDragItem">
 							<button v-if="!fieldEditMode" class="_button" :class="$style.dragItemHandle" tabindex="-1"><i class="ti ti-menu"></i></button>
 							<button v-if="fieldEditMode" :disabled="fields.length <= 1" class="_button" :class="$style.dragItemRemove" @click="deleteField(index)"><i class="ti ti-x"></i></button>
 							<div :class="$style.dragItemForm">
 								<FormSplit :minWidth="200">
-									<MkInput v-model="element.name" small>
-										<template #label>{{ i18n.ts._profile.metadataLabel }}</template>
+									<MkInput v-model="element.name" small :placeholder="i18n.ts._profile.metadataLabel">
 									</MkInput>
-									<MkInput v-model="element.value" small>
-										<template #label>{{ i18n.ts._profile.metadataContent }}</template>
+									<MkInput v-model="element.value" small :placeholder="i18n.ts._profile.metadataContent">
 									</MkInput>
 								</FormSplit>
 							</div>
 						</div>
 					</template>
 				</Sortable>
-
-				<MkInfo>{{ i18n.ts._profile.verifiedLinkDescription }}</MkInfo>
 			</div>
 		</MkFolder>
 		<template #caption>{{ i18n.ts._profile.metadataDescription }}</template>
 	</FormSlot>
 
-	<MkFolder>
-		<template #label>{{ i18n.ts.advancedSettings }}</template>
-
-		<div class="_gaps_m">
-			<MkSwitch v-model="profile.isCat">{{ i18n.ts.flagAsCat }}<template #caption>{{ i18n.ts.flagAsCatDescription }}</template></MkSwitch>
-			<MkSwitch v-model="profile.isBot">{{ i18n.ts.flagAsBot }}<template #caption>{{ i18n.ts.flagAsBotDescription }}</template></MkSwitch>
-		</div>
-	</MkFolder>
+	<MkInput v-model="profile.followedMessage" :max="200" manualSave :mfmPreview="false">
+		<template #label>{{ i18n.ts._profile.followedMessage }}<span class="_beta">{{ i18n.ts.beta }}</span></template>
+		<template #caption>
+			<div>{{ i18n.ts._profile.followedMessageDescription }}</div>
+			<div>{{ i18n.ts._profile.followedMessageDescriptionForLockedAccount }}</div>
+		</template>
+	</MkInput>
 
 	<MkSelect v-model="reactionAcceptance">
 		<template #label>{{ i18n.ts.reactionAcceptance }}</template>
@@ -105,6 +103,15 @@ SPDX-License-Identifier: AGPL-3.0-only
 		<option value="nonSensitiveOnlyForLocalLikeOnlyForRemote">{{ i18n.ts.nonSensitiveOnlyForLocalLikeOnlyForRemote }}</option>
 		<option value="likeOnly">{{ i18n.ts.likeOnly }}</option>
 	</MkSelect>
+
+	<MkFolder>
+		<template #label>{{ i18n.ts.advancedSettings }}</template>
+
+		<div class="_gaps_m">
+			<MkSwitch v-model="profile.isCat">{{ i18n.ts.flagAsCat }}<template #caption>{{ i18n.ts.flagAsCatDescription }}</template></MkSwitch>
+			<MkSwitch v-model="profile.isBot">{{ i18n.ts.flagAsBot }}<template #caption>{{ i18n.ts.flagAsBotDescription }}</template></MkSwitch>
+		</div>
+	</MkFolder>
 </div>
 </template>
 
@@ -135,12 +142,17 @@ const Sortable = defineAsyncComponent(() => import('vuedraggable').then(x => x.d
 
 const reactionAcceptance = computed(defaultStore.makeGetterSetter('reactionAcceptance'));
 
+function assertVaildLang(lang: string | null): lang is keyof typeof langmap {
+	return lang != null && lang in langmap;
+}
+
 const profile = reactive({
 	name: $i.name,
 	description: $i.description,
+	followedMessage: $i.followedMessage,
 	location: $i.location,
 	birthday: $i.birthday,
-	lang: $i.lang,
+	lang: assertVaildLang($i.lang) ? $i.lang : null,
 	isBot: $i.isBot ?? false,
 	isCat: $i.isCat ?? false,
 });
@@ -185,6 +197,8 @@ function save() {
 		// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
 		description: profile.description || null,
 		// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
+		followedMessage: profile.followedMessage || null,
+		// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
 		location: profile.location || null,
 		// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
 		birthday: profile.birthday || null,
@@ -192,6 +206,11 @@ function save() {
 		lang: profile.lang || null,
 		isBot: !!profile.isBot,
 		isCat: !!profile.isCat,
+	}, undefined, {
+		'0b3f9f6a-2f4d-4b1f-9fb4-49d3a2fd7191': {
+			title: i18n.ts.yourNameContainsProhibitedWords,
+			text: i18n.ts.yourNameContainsProhibitedWordsDescription,
+		},
 	});
 	globalEvents.emit('requestClearPageCache');
 	claimAchievement('profileFilled');
@@ -272,7 +291,7 @@ definePageMetadata(() => ({
 	height: 130px;
 	background-size: cover;
 	background-position: center;
-	border-bottom: solid 1px var(--divider);
+	border-bottom: solid 1px var(--MI_THEME-divider);
 	overflow: clip;
 }
 
@@ -299,19 +318,11 @@ definePageMetadata(() => ({
 	container-type: inline-size;
 }
 
-.metadataMargin {
-	margin-bottom: 1.5em;
-}
-
 .fieldDragItem {
 	display: flex;
-	padding-bottom: .75em;
+	padding: 10px;
 	align-items: flex-end;
-	border-bottom: solid 0.5px var(--divider);
-
-	&:last-child {
-		border-bottom: 0;
-	}
+	border-radius: 6px;
 
 	/* (drag button) 32px + (drag button margin) 8px + (input width) 200px * 2 + (input gap) 12px = 452px */
 	@container (max-width: 452px) {
@@ -342,6 +353,7 @@ definePageMetadata(() => ({
 	&:hover, &:focus {
 		opacity: .7;
 	}
+
 	&:active {
 		cursor: pointer;
 	}
diff --git a/packages/frontend/src/pages/settings/security.vue b/packages/frontend/src/pages/settings/security.vue
index de0f63630e..8f9d4f858b 100644
--- a/packages/frontend/src/pages/settings/security.vue
+++ b/packages/frontend/src/pages/settings/security.vue
@@ -124,7 +124,7 @@ definePageMetadata(() => ({
 	}
 
 	&:not(:last-child) {
-		border-bottom: solid 0.5px var(--divider);
+		border-bottom: solid 0.5px var(--MI_THEME-divider);
 	}
 
 	> header {
@@ -136,11 +136,11 @@ definePageMetadata(() => ({
 			margin-right: 0.75em;
 
 			&.succ {
-				color: var(--success);
+				color: var(--MI_THEME-success);
 			}
 
 			&.fail {
-				color: var(--error);
+				color: var(--MI_THEME-error);
 			}
 		}
 
diff --git a/packages/frontend/src/pages/settings/sounds.sound.vue b/packages/frontend/src/pages/settings/sounds.sound.vue
index 113abd708b..56f65e2309 100644
--- a/packages/frontend/src/pages/settings/sounds.sound.vue
+++ b/packages/frontend/src/pages/settings/sounds.sound.vue
@@ -9,7 +9,13 @@ SPDX-License-Identifier: AGPL-3.0-only
 		<template #label>{{ i18n.ts.sound }}</template>
 		<option v-for="x in soundsTypes" :key="x ?? 'null'" :value="x">{{ getSoundTypeName(x) }}</option>
 	</MkSelect>
-	<div v-if="type === '_driveFile_'" :class="$style.fileSelectorRoot">
+	<div v-if="type === '_driveFile_' && driveFileError === true" :class="$style.fileSelectorRoot">
+		<MkButton :class="$style.fileSelectorButton" inline rounded primary @click="selectSound">{{ i18n.ts.selectFile }}</MkButton>
+		<div :class="$style.fileErrorRoot">
+			<MkCondensedLine>{{ i18n.ts._soundSettings.driveFileError }}</MkCondensedLine>
+		</div>
+	</div>
+	<div v-else-if="type === '_driveFile_'" :class="$style.fileSelectorRoot">
 		<MkButton :class="$style.fileSelectorButton" inline rounded primary @click="selectSound">{{ i18n.ts.selectFile }}</MkButton>
 		<div :class="['_nowrap', !fileUrl && $style.fileNotSelected]">{{ friendlyFileName }}</div>
 	</div>
@@ -19,13 +25,13 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 	<div class="_buttons">
 		<MkButton inline @click="listen"><i class="ti ti-player-play"></i> {{ i18n.ts.listen }}</MkButton>
-		<MkButton inline primary @click="save"><i class="ti ti-check"></i> {{ i18n.ts.save }}</MkButton>
+		<MkButton inline primary :disabled="!hasChanged || driveFileError" @click="save"><i class="ti ti-check"></i> {{ i18n.ts.save }}</MkButton>
 	</div>
 </div>
 </template>
 
 <script lang="ts" setup>
-import { ref, computed } from 'vue';
+import { ref, computed, watch } from 'vue';
 import type { SoundType } from '@/scripts/sound.js';
 import MkSelect from '@/components/MkSelect.vue';
 import MkButton from '@/components/MkButton.vue';
@@ -51,13 +57,18 @@ const type = ref<SoundType>(props.type);
 const fileId = ref(props.fileId);
 const fileUrl = ref(props.fileUrl);
 const fileName = ref<string>('');
+const driveFileError = ref(false);
+const hasChanged = ref(false);
 const volume = ref(props.volume);
 
 if (type.value === '_driveFile_' && fileId.value) {
-	const apiRes = await misskeyApi('drive/files/show', {
+	await misskeyApi('drive/files/show', {
 		fileId: fileId.value,
+	}).then((res) => {
+		fileName.value = res.name;
+	}).catch((res) => {
+		driveFileError.value = true;
 	});
-	fileName.value = apiRes.name;
 }
 
 function getSoundTypeName(f: SoundType): string {
@@ -107,9 +118,21 @@ function selectSound(ev) {
 		fileUrl.value = file.url;
 		fileName.value = file.name;
 		fileId.value = file.id;
+		driveFileError.value = false;
+		hasChanged.value = true;
 	});
 }
 
+watch([type, volume], ([typeTo, volumeTo], [typeFrom, volumeFrom]) => {
+	if (typeFrom !== typeTo && typeTo !== '_driveFile_') {
+		fileUrl.value = undefined;
+		fileName.value = '';
+		fileId.value = undefined;
+		driveFileError.value = false;
+	}
+	hasChanged.value = true;
+});
+
 function listen() {
 	if (type.value === '_driveFile_' && (!fileUrl.value || !fileId.value)) {
 		os.alert({
@@ -131,6 +154,10 @@ function listen() {
 }
 
 function save() {
+	if (hasChanged.value === false || driveFileError.value === true) {
+		return;
+	}
+
 	if (type.value === '_driveFile_' && !fileUrl.value) {
 		os.alert({
 			type: 'warning',
@@ -163,12 +190,19 @@ function save() {
 	gap: 8px;
 }
 
+.fileErrorRoot {
+	flex-grow: 1;
+	min-width: 0;
+	font-weight: 700;
+	color: var(--MI_THEME-error);
+}
+
 .fileSelectorButton {
 	flex-shrink: 0;
 }
 
 .fileNotSelected {
 	font-weight: 700;
-	color: var(--infoWarnFg);
+	color: var(--MI_THEME-infoWarnFg);
 }
 </style>
diff --git a/packages/frontend/src/pages/settings/sounds.vue b/packages/frontend/src/pages/settings/sounds.vue
index 090f0cf14c..9fcf564e55 100644
--- a/packages/frontend/src/pages/settings/sounds.vue
+++ b/packages/frontend/src/pages/settings/sounds.vue
@@ -21,8 +21,14 @@ SPDX-License-Identifier: AGPL-3.0-only
 			<MkFolder v-for="type in operationTypes" :key="type">
 				<template #label>{{ i18n.ts._sfx[type] }}</template>
 				<template #suffix>{{ getSoundTypeName(sounds[type].type) }}</template>
-
-				<XSound :type="sounds[type].type" :volume="sounds[type].volume" :fileId="sounds[type].fileId" :fileUrl="sounds[type].fileUrl" @update="(res) => updated(type, res)"/>
+				<Suspense>
+					<template #default>
+						<XSound :type="sounds[type].type" :volume="sounds[type].volume" :fileId="sounds[type].fileId" :fileUrl="sounds[type].fileUrl" @update="(res) => updated(type, res)"/>
+					</template>
+					<template #fallback>
+						<MkLoading/>
+					</template>
+				</Suspense>
 			</MkFolder>
 		</div>
 	</FormSection>
@@ -54,8 +60,6 @@ const sounds = ref<Record<OperationType, Ref<SoundStore>>>({
 	note: defaultStore.reactiveState.sound_note,
 	noteMy: defaultStore.reactiveState.sound_noteMy,
 	notification: defaultStore.reactiveState.sound_notification,
-	antenna: defaultStore.reactiveState.sound_antenna,
-	channel: defaultStore.reactiveState.sound_channel,
 	reaction: defaultStore.reactiveState.sound_reaction,
 });
 
diff --git a/packages/frontend/src/pages/settings/statusbar.statusbar.vue b/packages/frontend/src/pages/settings/statusbar.statusbar.vue
index 92e389a288..67943524ef 100644
--- a/packages/frontend/src/pages/settings/statusbar.statusbar.vue
+++ b/packages/frontend/src/pages/settings/statusbar.statusbar.vue
@@ -36,7 +36,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 		<MkSwitch v-model="statusbar.props.shuffle">
 			<template #label>{{ i18n.ts.shuffle }}</template>
 		</MkSwitch>
-		<MkInput v-model="statusbar.props.refreshIntervalSec" manualSave type="number">
+		<MkInput v-model="statusbar.props.refreshIntervalSec" manualSave type="number" min="1">
 			<template #label>{{ i18n.ts.refreshInterval }}</template>
 		</MkInput>
 		<MkRange v-model="statusbar.props.marqueeDuration" :min="5" :max="150" :step="1">
@@ -48,7 +48,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 		</MkSwitch>
 	</template>
 	<template v-else-if="statusbar.type === 'federation'">
-		<MkInput v-model="statusbar.props.refreshIntervalSec" manualSave type="number">
+		<MkInput v-model="statusbar.props.refreshIntervalSec" manualSave type="number" min="1">
 			<template #label>{{ i18n.ts.refreshInterval }}</template>
 		</MkInput>
 		<MkRange v-model="statusbar.props.marqueeDuration" :min="5" :max="150" :step="1">
diff --git a/packages/frontend/src/pages/settings/theme.manage.vue b/packages/frontend/src/pages/settings/theme.manage.vue
index 8a94d7388b..579ca6b20b 100644
--- a/packages/frontend/src/pages/settings/theme.manage.vue
+++ b/packages/frontend/src/pages/settings/theme.manage.vue
@@ -38,7 +38,7 @@ import MkSelect from '@/components/MkSelect.vue';
 import MkInput from '@/components/MkInput.vue';
 import MkButton from '@/components/MkButton.vue';
 import { Theme, getBuiltinThemesRef } from '@/scripts/theme.js';
-import copyToClipboard from '@/scripts/copy-to-clipboard.js';
+import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
 import * as os from '@/os.js';
 import { getThemes, removeTheme } from '@/theme-store.js';
 import { i18n } from '@/i18n.js';
diff --git a/packages/frontend/src/pages/settings/theme.vue b/packages/frontend/src/pages/settings/theme.vue
index 0a4bd4b826..f1ec231588 100644
--- a/packages/frontend/src/pages/settings/theme.vue
+++ b/packages/frontend/src/pages/settings/theme.vue
@@ -88,19 +88,9 @@ import { uniqueBy } from '@/scripts/array.js';
 import { fetchThemes, getThemes } from '@/theme-store.js';
 import { definePageMetadata } from '@/scripts/page-metadata.js';
 import { miLocalStorage } from '@/local-storage.js';
-import { unisonReload } from '@/scripts/unison-reload.js';
+import { reloadAsk } from '@/scripts/reload-ask.js';
 import * as os from '@/os.js';
 
-async function reloadAsk() {
-	const { canceled } = await os.confirm({
-		type: 'info',
-		text: i18n.ts.reloadToApplySetting,
-	});
-	if (canceled) return;
-
-	unisonReload();
-}
-
 const installedThemes = ref(getThemes());
 const builtinThemes = getBuiltinThemesRef();
 
@@ -148,13 +138,13 @@ watch(syncDeviceDarkMode, () => {
 	}
 });
 
-watch(wallpaper, () => {
+watch(wallpaper, async () => {
 	if (wallpaper.value == null) {
 		miLocalStorage.removeItem('wallpaper');
 	} else {
 		miLocalStorage.setItem('wallpaper', wallpaper.value);
 	}
-	reloadAsk();
+	await reloadAsk({ reason: i18n.ts.reloadToApplySetting, unison: true });
 });
 
 onActivated(() => {
@@ -213,12 +203,18 @@ definePageMetadata(() => ({
 			}
 		}
 
+		.dn:focus-visible ~ .toggle {
+			outline: 2px solid var(--MI_THEME-focus);
+			outline-offset: 2px;
+		}
+
 		.toggle {
 			cursor: pointer;
 			display: inline-block;
 			position: relative;
 			width: 90px;
 			height: 50px;
+			margin: 4px; // focus用のアウトライン
 			background-color: #83D8FF;
 			border-radius: 90px - 6;
 			transition: background-color 200ms cubic-bezier(0.445, 0.05, 0.55, 0.95) !important;
@@ -231,12 +227,12 @@ definePageMetadata(() => ({
 
 			> .before {
 				left: -70px;
-				color: var(--accent);
+				color: var(--MI_THEME-accent);
 			}
 
 			> .after {
 				right: -68px;
-				color: var(--fg);
+				color: var(--MI_THEME-fg);
 			}
 		}
 
@@ -354,11 +350,11 @@ definePageMetadata(() => ({
 				background-color: #749DD6;
 
 				> .before {
-					color: var(--fg);
+					color: var(--MI_THEME-fg);
 				}
 
 				> .after {
-					color: var(--accent);
+					color: var(--MI_THEME-accent);
 				}
 
 				.toggle__handler {
@@ -409,14 +405,14 @@ definePageMetadata(() => ({
 
 	> .sync {
 		padding: 14px 16px;
-		border-top: solid 0.5px var(--divider);
+		border-top: solid 0.5px var(--MI_THEME-divider);
 	}
 }
 
 .rsljpzjq {
 	> .selects {
 		display: flex;
-		gap: 1.5em var(--margin);
+		gap: 1.5em var(--MI-margin);
 		flex-wrap: wrap;
 
 		> .select {
diff --git a/packages/frontend/src/pages/settings/webhook.edit.vue b/packages/frontend/src/pages/settings/webhook.edit.vue
index e9fb1e471e..40d23e36c5 100644
--- a/packages/frontend/src/pages/settings/webhook.edit.vue
+++ b/packages/frontend/src/pages/settings/webhook.edit.vue
@@ -19,16 +19,43 @@ SPDX-License-Identifier: AGPL-3.0-only
 	</MkInput>
 
 	<FormSection>
-		<template #label>{{ i18n.ts._webhookSettings.events }}</template>
+		<template #label>{{ i18n.ts._webhookSettings.trigger }}</template>
 
-		<div class="_gaps_s">
-			<MkSwitch v-model="event_follow">{{ i18n.ts._webhookSettings._events.follow }}</MkSwitch>
-			<MkSwitch v-model="event_followed">{{ i18n.ts._webhookSettings._events.followed }}</MkSwitch>
-			<MkSwitch v-model="event_note">{{ i18n.ts._webhookSettings._events.note }}</MkSwitch>
-			<MkSwitch v-model="event_reply">{{ i18n.ts._webhookSettings._events.reply }}</MkSwitch>
-			<MkSwitch v-model="event_renote">{{ i18n.ts._webhookSettings._events.renote }}</MkSwitch>
-			<MkSwitch v-model="event_reaction">{{ i18n.ts._webhookSettings._events.reaction }}</MkSwitch>
-			<MkSwitch v-model="event_mention">{{ i18n.ts._webhookSettings._events.mention }}</MkSwitch>
+		<div class="_gaps">
+			<div class="_gaps_s">
+				<div :class="$style.switchBox">
+					<MkSwitch v-model="event_follow">{{ i18n.ts._webhookSettings._events.follow }}</MkSwitch>
+					<MkButton transparent :class="$style.testButton" :disabled="!(active && event_follow)" @click="test('follow')"><i class="ti ti-send"></i></MkButton>
+				</div>
+				<div :class="$style.switchBox">
+					<MkSwitch v-model="event_followed">{{ i18n.ts._webhookSettings._events.followed }}</MkSwitch>
+					<MkButton transparent :class="$style.testButton" :disabled="!(active && event_followed)" @click="test('followed')"><i class="ti ti-send"></i></MkButton>
+				</div>
+				<div :class="$style.switchBox">
+					<MkSwitch v-model="event_note">{{ i18n.ts._webhookSettings._events.note }}</MkSwitch>
+					<MkButton transparent :class="$style.testButton" :disabled="!(active && event_note)" @click="test('note')"><i class="ti ti-send"></i></MkButton>
+				</div>
+				<div :class="$style.switchBox">
+					<MkSwitch v-model="event_reply">{{ i18n.ts._webhookSettings._events.reply }}</MkSwitch>
+					<MkButton transparent :class="$style.testButton" :disabled="!(active && event_reply)" @click="test('reply')"><i class="ti ti-send"></i></MkButton>
+				</div>
+				<div :class="$style.switchBox">
+					<MkSwitch v-model="event_renote">{{ i18n.ts._webhookSettings._events.renote }}</MkSwitch>
+					<MkButton transparent :class="$style.testButton" :disabled="!(active && event_renote)" @click="test('renote')"><i class="ti ti-send"></i></MkButton>
+				</div>
+				<div :class="$style.switchBox">
+					<MkSwitch v-model="event_reaction">{{ i18n.ts._webhookSettings._events.reaction }}</MkSwitch>
+					<MkButton transparent :class="$style.testButton" :disabled="!(active && event_reaction)" @click="test('reaction')"><i class="ti ti-send"></i></MkButton>
+				</div>
+				<div :class="$style.switchBox">
+					<MkSwitch v-model="event_mention">{{ i18n.ts._webhookSettings._events.mention }}</MkSwitch>
+					<MkButton transparent :class="$style.testButton" :disabled="!(active && event_mention)" @click="test('mention')"><i class="ti ti-send"></i></MkButton>
+				</div>
+			</div>
+
+			<div :class="$style.description">
+				{{ i18n.ts._webhookSettings.testRemarks }}
+			</div>
 		</div>
 	</FormSection>
 
@@ -43,6 +70,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <script lang="ts" setup>
 import { ref, computed } from 'vue';
+import * as Misskey from 'misskey-js';
 import MkInput from '@/components/MkInput.vue';
 import FormSection from '@/components/form/section.vue';
 import MkSwitch from '@/components/MkSwitch.vue';
@@ -76,8 +104,8 @@ const event_renote = ref(webhook.on.includes('renote'));
 const event_reaction = ref(webhook.on.includes('reaction'));
 const event_mention = ref(webhook.on.includes('mention'));
 
-async function save(): Promise<void> {
-	const events = [];
+function save() {
+	const events: Misskey.entities.UserWebhook['on'] = [];
 	if (event_follow.value) events.push('follow');
 	if (event_followed.value) events.push('followed');
 	if (event_note.value) events.push('note');
@@ -110,8 +138,21 @@ async function del(): Promise<void> {
 	router.push('/settings/webhook');
 }
 
+async function test(type: Misskey.entities.UserWebhook['on'][number]): Promise<void> {
+	await os.apiWithDialog('i/webhooks/test', {
+		webhookId: props.webhookId,
+		type,
+		override: {
+			secret: secret.value,
+			url: url.value,
+		},
+	});
+}
+
+// eslint-disable-next-line @typescript-eslint/no-unused-vars
 const headerActions = computed(() => []);
 
+// eslint-disable-next-line @typescript-eslint/no-unused-vars
 const headerTabs = computed(() => []);
 
 definePageMetadata(() => ({
@@ -119,3 +160,30 @@ definePageMetadata(() => ({
 	icon: 'ti ti-webhook',
 }));
 </script>
+
+<style module lang="scss">
+.switchBox {
+	display: flex;
+	align-items: center;
+	justify-content: start;
+
+	.testButton {
+		$buttonSize: 28px;
+		padding: 0;
+		width: $buttonSize;
+		min-width: $buttonSize;
+		max-width: $buttonSize;
+		height: $buttonSize;
+		margin-left: auto;
+		line-height: inherit;
+		font-size: 90%;
+		border-radius: 9999px;
+	}
+}
+
+.description {
+	font-size: 0.85em;
+	padding: 8px 0 0 0;
+	color: var(--MI_THEME-fgTransparentWeak);
+}
+</style>
diff --git a/packages/frontend/src/pages/settings/webhook.new.vue b/packages/frontend/src/pages/settings/webhook.new.vue
index 5bf85e48f4..d62357caaf 100644
--- a/packages/frontend/src/pages/settings/webhook.new.vue
+++ b/packages/frontend/src/pages/settings/webhook.new.vue
@@ -19,7 +19,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 	</MkInput>
 
 	<FormSection>
-		<template #label>{{ i18n.ts._webhookSettings.events }}</template>
+		<template #label>{{ i18n.ts._webhookSettings.trigger }}</template>
 
 		<div class="_gaps_s">
 			<MkSwitch v-model="event_follow">{{ i18n.ts._webhookSettings._events.follow }}</MkSwitch>
diff --git a/packages/frontend/src/pages/settings/webhook.vue b/packages/frontend/src/pages/settings/webhook.vue
index 0d11b00c97..af8b7ca945 100644
--- a/packages/frontend/src/pages/settings/webhook.vue
+++ b/packages/frontend/src/pages/settings/webhook.vue
@@ -17,8 +17,8 @@ SPDX-License-Identifier: AGPL-3.0-only
 						<template #icon>
 							<i v-if="webhook.active === false" class="ti ti-player-pause"></i>
 							<i v-else-if="webhook.latestStatus === null" class="ti ti-circle"></i>
-							<i v-else-if="[200, 201, 204].includes(webhook.latestStatus)" class="ti ti-check" :style="{ color: 'var(--success)' }"></i>
-							<i v-else class="ti ti-alert-triangle" :style="{ color: 'var(--error)' }"></i>
+							<i v-else-if="[200, 201, 204].includes(webhook.latestStatus)" class="ti ti-check" :style="{ color: 'var(--MI_THEME-success)' }"></i>
+							<i v-else class="ti ti-alert-triangle" :style="{ color: 'var(--MI_THEME-error)' }"></i>
 						</template>
 						{{ webhook.name || webhook.url }}
 						<template #suffix>
diff --git a/packages/frontend/src/pages/signup-complete.vue b/packages/frontend/src/pages/signup-complete.vue
index 8c2f7042cd..14fb96d4f1 100644
--- a/packages/frontend/src/pages/signup-complete.vue
+++ b/packages/frontend/src/pages/signup-complete.vue
@@ -71,7 +71,7 @@ place-content: center;
 .form {
 	position: relative;
 	z-index: 10;
-	border-radius: var(--radius);
+	border-radius: var(--MI-radius);
 	box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1);
 	overflow: clip;
 	max-width: 500px;
@@ -81,7 +81,7 @@ place-content: center;
 	padding: 16px;
 	text-align: center;
 	font-size: 26px;
-	background-color: var(--accentedBg);
-	color: var(--accent);
+	background-color: var(--MI_THEME-accentedBg);
+	color: var(--MI_THEME-accent);
 }
 </style>
diff --git a/packages/frontend/src/pages/tag.vue b/packages/frontend/src/pages/tag.vue
index 9b77392872..b669e25179 100644
--- a/packages/frontend/src/pages/tag.vue
+++ b/packages/frontend/src/pages/tag.vue
@@ -28,6 +28,7 @@ import { i18n } from '@/i18n.js';
 import { $i } from '@/account.js';
 import { defaultStore } from '@/store.js';
 import * as os from '@/os.js';
+import { genEmbedCode } from '@/scripts/get-embed-code.js';
 
 const props = defineProps<{
 	tag: string;
@@ -51,7 +52,19 @@ async function post() {
 	notes.value?.pagingComponent?.reload();
 }
 
-const headerActions = computed(() => []);
+const headerActions = computed(() => [{
+	icon: 'ti ti-dots',
+	label: i18n.ts.more,
+	handler: (ev: MouseEvent) => {
+		os.popupMenu([{
+			text: i18n.ts.genEmbedCode,
+			icon: 'ti ti-code',
+			action: () => {
+				genEmbedCode('tags', props.tag);
+			},
+		}], ev.currentTarget ?? ev.target);
+	}
+}]);
 
 const headerTabs = computed(() => []);
 
@@ -63,10 +76,10 @@ definePageMetadata(() => ({
 
 <style lang="scss" module>
 .footer {
-	-webkit-backdrop-filter: var(--blur, blur(15px));
-	backdrop-filter: var(--blur, blur(15px));
-	background: var(--acrylicBg);
-	border-top: solid 0.5px var(--divider);
+	-webkit-backdrop-filter: var(--MI-blur, blur(15px));
+	backdrop-filter: var(--MI-blur, blur(15px));
+	background: var(--MI_THEME-acrylicBg);
+	border-top: solid 0.5px var(--MI_THEME-divider);
 	display: flex;
 }
 
diff --git a/packages/frontend/src/pages/theme-editor.vue b/packages/frontend/src/pages/theme-editor.vue
index 50c3beeabc..b2e084f53f 100644
--- a/packages/frontend/src/pages/theme-editor.vue
+++ b/packages/frontend/src/pages/theme-editor.vue
@@ -79,6 +79,8 @@ import tinycolor from 'tinycolor2';
 import { v4 as uuid } from 'uuid';
 import JSON5 from 'json5';
 
+import lightTheme from '@@/themes/_light.json5';
+import darkTheme from '@@/themes/_dark.json5';
 import MkButton from '@/components/MkButton.vue';
 import MkCodeEditor from '@/components/MkCodeEditor.vue';
 import MkTextarea from '@/components/MkTextarea.vue';
@@ -86,9 +88,7 @@ import MkFolder from '@/components/MkFolder.vue';
 
 import { $i } from '@/account.js';
 import { Theme, applyTheme } from '@/scripts/theme.js';
-import lightTheme from '@/themes/_light.json5';
-import darkTheme from '@/themes/_dark.json5';
-import { host } from '@/config.js';
+import { host } from '@@/js/config.js';
 import * as os from '@/os.js';
 import { ColdDeviceStorage, defaultStore } from '@/store.js';
 import { addTheme } from '@/theme-store.js';
@@ -268,7 +268,7 @@ definePageMetadata(() => ({
 				}
 
 				&.active {
-					box-shadow: 0 0 0 2px var(--divider) inset;
+					box-shadow: 0 0 0 2px var(--MI_THEME-divider) inset;
 				}
 
 				&.rounded {
diff --git a/packages/frontend/src/pages/timeline.vue b/packages/frontend/src/pages/timeline.vue
index 98744c6318..044a1908ab 100644
--- a/packages/frontend/src/pages/timeline.vue
+++ b/packages/frontend/src/pages/timeline.vue
@@ -8,20 +8,21 @@ SPDX-License-Identifier: AGPL-3.0-only
 	<template #header><MkPageHeader v-model:tab="src" :actions="headerActions" :tabs="$i ? headerTabs : headerTabsWhenNotLogin" :displayMyAvatar="true"/></template>
 	<MkSpacer :contentMax="800">
 		<MkHorizontalSwipe v-model:tab="src" :tabs="$i ? headerTabs : headerTabsWhenNotLogin">
-			<div :key="src" ref="rootEl" v-hotkey.global="keymap">
-				<MkInfo v-if="['home', 'local', 'social', 'global'].includes(src) && !defaultStore.reactiveState.timelineTutorials.value[src]" style="margin-bottom: var(--margin);" closable @close="closeTutorial()">
+			<div :key="src" ref="rootEl">
+				<MkInfo v-if="isBasicTimeline(src) && !defaultStore.reactiveState.timelineTutorials.value[src]" style="margin-bottom: var(--MI-margin);" closable @close="closeTutorial()">
 					{{ i18n.ts._timelineDescription[src] }}
 				</MkInfo>
-				<MkPostForm v-if="defaultStore.reactiveState.showFixedPostForm.value" :class="$style.postForm" class="post-form _panel" fixed style="margin-bottom: var(--margin);"/>
+				<MkPostForm v-if="defaultStore.reactiveState.showFixedPostForm.value" :class="$style.postForm" class="post-form _panel" fixed style="margin-bottom: var(--MI-margin);"/>
 				<div v-if="queue > 0" :class="$style.new"><button class="_buttonPrimary" :class="$style.newButton" @click="top()">{{ i18n.ts.newNoteRecived }}</button></div>
 				<div :class="$style.tl">
 					<MkTimeline
 						ref="tlComponent"
-						:key="src + withRenotes + withReplies + onlyFiles"
+						:key="src + withRenotes + withReplies + onlyFiles + withSensitive"
 						:src="src.split(':')[0]"
 						:list="src.split(':')[1]"
 						:withRenotes="withRenotes"
 						:withReplies="withReplies"
+						:withSensitive="withSensitive"
 						:onlyFiles="onlyFiles"
 						:sound="true"
 						@queue="queueUpdated"
@@ -34,40 +35,37 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { computed, watch, provide, shallowRef, ref } from 'vue';
+import { computed, watch, provide, shallowRef, ref, onMounted, onActivated } from 'vue';
 import type { Tab } from '@/components/global/MkPageHeader.tabs.vue';
 import MkTimeline from '@/components/MkTimeline.vue';
 import MkInfo from '@/components/MkInfo.vue';
 import MkPostForm from '@/components/MkPostForm.vue';
 import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue';
-import { scroll } from '@/scripts/scroll.js';
+import { scroll } from '@@/js/scroll.js';
 import * as os from '@/os.js';
 import { misskeyApi } from '@/scripts/misskey-api.js';
 import { defaultStore } from '@/store.js';
 import { i18n } from '@/i18n.js';
-import { instance } from '@/instance.js';
 import { $i } from '@/account.js';
 import { definePageMetadata } from '@/scripts/page-metadata.js';
 import { antennasCache, userListsCache, favoritedChannelsCache } from '@/cache.js';
 import { deviceKind } from '@/scripts/device-kind.js';
 import { deepMerge } from '@/scripts/merge.js';
-import { MenuItem } from '@/types/menu.js';
+import type { MenuItem } from '@/types/menu.js';
 import { miLocalStorage } from '@/local-storage.js';
+import { availableBasicTimelines, hasWithReplies, isAvailableBasicTimeline, isBasicTimeline, basicTimelineIconClass } from '@/timelines.js';
+import type { BasicTimelineType } from '@/timelines.js';
 
 provide('shouldOmitHeaderTitle', true);
 
-const isLocalTimelineAvailable = ($i == null && instance.policies.ltlAvailable) || ($i != null && $i.policies.ltlAvailable);
-const isGlobalTimelineAvailable = ($i == null && instance.policies.gtlAvailable) || ($i != null && $i.policies.gtlAvailable);
-const keymap = {
-	't': focus,
-};
-
 const tlComponent = shallowRef<InstanceType<typeof MkTimeline>>();
 const rootEl = shallowRef<HTMLElement>();
 
+type TimelinePageSrc = BasicTimelineType | `list:${string}`;
+
 const queue = ref(0);
-const srcWhenNotSignin = ref<'local' | 'global'>(isLocalTimelineAvailable ? 'local' : 'global');
-const src = computed<'home' | 'local' | 'social' | 'global' | `list:${string}`>({
+const srcWhenNotSignin = ref<'local' | 'global'>(isAvailableBasicTimeline('local') ? 'local' : 'global');
+const src = computed<TimelinePageSrc>({
 	get: () => ($i ? defaultStore.reactiveState.tl.value.src : srcWhenNotSignin.value),
 	set: (x) => saveSrc(x),
 });
@@ -77,7 +75,11 @@ const withRenotes = computed<boolean>({
 });
 
 // computed内での無限ループを防ぐためのフラグ
-const localSocialTLFilterSwitchStore = ref<'withReplies' | 'onlyFiles' | false>('withReplies');
+const localSocialTLFilterSwitchStore = ref<'withReplies' | 'onlyFiles' | false>(
+	defaultStore.reactiveState.tl.value.filter.withReplies ? 'withReplies' :
+	defaultStore.reactiveState.tl.value.filter.onlyFiles ? 'onlyFiles' :
+	false,
+);
 
 const withReplies = computed<boolean>({
 	get: () => {
@@ -120,11 +122,6 @@ watch(src, () => {
 	queue.value = 0;
 });
 
-watch(withSensitive, () => {
-	// これだけはクライアント側で完結する処理なので手動でリロード
-	tlComponent.value?.reloadTimeline();
-});
-
 function queueUpdated(q: number): void {
 	queue.value = q;
 }
@@ -188,7 +185,7 @@ async function chooseChannel(ev: MouseEvent): Promise<void> {
 		}),
 		(channels.length === 0 ? undefined : { type: 'divider' }),
 		{
-			type: 'link' as const,
+			type: 'link',
 			icon: 'ti ti-plus',
 			text: i18n.ts.createNew,
 			to: '/channels',
@@ -197,7 +194,7 @@ async function chooseChannel(ev: MouseEvent): Promise<void> {
 	os.popupMenu(items, ev.currentTarget ?? ev.target);
 }
 
-function saveSrc(newSrc: 'home' | 'local' | 'social' | 'global' | `list:${string}`): void {
+function saveSrc(newSrc: TimelinePageSrc): void {
 	const out = deepMerge({ src: newSrc }, defaultStore.state.tl);
 
 	if (newSrc.startsWith('userList:')) {
@@ -232,28 +229,49 @@ function focus(): void {
 }
 
 function closeTutorial(): void {
-	if (!['home', 'local', 'social', 'global'].includes(src.value)) return;
+	if (!isBasicTimeline(src.value)) return;
 	const before = defaultStore.state.timelineTutorials;
 	before[src.value] = true;
 	defaultStore.set('timelineTutorials', before);
 }
 
+function switchTlIfNeeded() {
+	if (isBasicTimeline(src.value) && !isAvailableBasicTimeline(src.value)) {
+		src.value = availableBasicTimelines()[0];
+	}
+}
+
+onMounted(() => {
+	switchTlIfNeeded();
+});
+onActivated(() => {
+	switchTlIfNeeded();
+});
+
 const headerActions = computed(() => {
 	const tmp = [
 		{
 			icon: 'ti ti-dots',
 			text: i18n.ts.options,
 			handler: (ev) => {
-				os.popupMenu([{
+				const menuItems: MenuItem[] = [];
+
+				menuItems.push({
 					type: 'switch',
 					text: i18n.ts.showRenotes,
 					ref: withRenotes,
-				}, src.value === 'local' || src.value === 'social' ? {
-					type: 'switch',
-					text: i18n.ts.showRepliesToOthersInTimeline,
-					ref: withReplies,
-					disabled: onlyFiles,
-				} : undefined, {
+				});
+
+				if (isBasicTimeline(src.value) && hasWithReplies(src.value)) {
+					menuItems.push({
+						type: 'switch',
+						text: i18n.ts.showRepliesToOthersInTimeline,
+						ref: withReplies,
+						disabled: onlyFiles,
+					});
+				}
+
+				menuItems.push({
 					type: 'switch',
 					text: i18n.ts.withSensitive,
 					ref: withSensitive,
@@ -261,8 +279,10 @@ const headerActions = computed(() => {
 					type: 'switch',
 					text: i18n.ts.fileAttachedOnly,
 					ref: onlyFiles,
-					disabled: src.value === 'local' || src.value === 'social' ? withReplies : false,
-				}], ev.currentTarget ?? ev.target);
+					disabled: isBasicTimeline(src.value) && hasWithReplies(src.value) ? withReplies : false,
+				});
+
+				os.popupMenu(menuItems, ev.currentTarget ?? ev.target);
 			},
 		},
 	];
@@ -283,27 +303,12 @@ const headerTabs = computed(() => [...(defaultStore.reactiveState.pinnedUserList
 	title: l.name,
 	icon: 'ti ti-star',
 	iconOnly: true,
-}))), {
-	key: 'home',
-	title: i18n.ts._timelines.home,
-	icon: 'ti ti-home',
+}))), ...availableBasicTimelines().map(tl => ({
+	key: tl,
+	title: i18n.ts._timelines[tl],
+	icon: basicTimelineIconClass(tl),
 	iconOnly: true,
-}, ...(isLocalTimelineAvailable ? [{
-	key: 'local',
-	title: i18n.ts._timelines.local,
-	icon: 'ti ti-planet',
-	iconOnly: true,
-}, {
-	key: 'social',
-	title: i18n.ts._timelines.social,
-	icon: 'ti ti-universe',
-	iconOnly: true,
-}] : []), ...(isGlobalTimelineAvailable ? [{
-	key: 'global',
-	title: i18n.ts._timelines.global,
-	icon: 'ti ti-whirl',
-	iconOnly: true,
-}] : []), {
+})), {
 	icon: 'ti ti-list',
 	title: i18n.ts.lists,
 	iconOnly: true,
@@ -320,54 +325,46 @@ const headerTabs = computed(() => [...(defaultStore.reactiveState.pinnedUserList
 	onClick: chooseChannel,
 }] as Tab[]);
 
-const headerTabsWhenNotLogin = computed(() => [
-	...(isLocalTimelineAvailable ? [{
-		key: 'local',
-		title: i18n.ts._timelines.local,
-		icon: 'ti ti-planet',
-		iconOnly: true,
-	}] : []),
-	...(isGlobalTimelineAvailable ? [{
-		key: 'global',
-		title: i18n.ts._timelines.global,
-		icon: 'ti ti-whirl',
-		iconOnly: true,
-	}] : []),
-] as Tab[]);
+const headerTabsWhenNotLogin = computed(() => [...availableBasicTimelines().map(tl => ({
+	key: tl,
+	title: i18n.ts._timelines[tl],
+	icon: basicTimelineIconClass(tl),
+	iconOnly: true,
+}))] as Tab[]);
 
 definePageMetadata(() => ({
 	title: i18n.ts.timeline,
-	icon: src.value === 'local' ? 'ti ti-planet' : src.value === 'social' ? 'ti ti-universe' : src.value === 'global' ? 'ti ti-whirl' : 'ti ti-home',
+	icon: isBasicTimeline(src.value) ? basicTimelineIconClass(src.value) : 'ti ti-home',
 }));
 </script>
 
 <style lang="scss" module>
 .new {
 	position: sticky;
-	top: calc(var(--stickyTop, 0px) + 16px);
+	top: calc(var(--MI-stickyTop, 0px) + 16px);
 	z-index: 1000;
 	width: 100%;
 	margin: calc(-0.675em - 8px) 0;
 
 	&:first-child {
-		margin-top: calc(-0.675em - 8px - var(--margin));
+		margin-top: calc(-0.675em - 8px - var(--MI-margin));
 	}
 }
 
 .newButton {
 	display: block;
-	margin: var(--margin) auto 0 auto;
+	margin: var(--MI-margin) auto 0 auto;
 	padding: 8px 16px;
 	border-radius: 32px;
 }
 
 .postForm {
-	border-radius: var(--radius);
+	border-radius: var(--MI-radius);
 }
 
 .tl {
-	background: var(--bg);
-	border-radius: var(--radius);
+	background: var(--MI_THEME-bg);
+	border-radius: var(--MI-radius);
 	overflow: clip;
 }
 </style>
diff --git a/packages/frontend/src/pages/user-list-timeline.vue b/packages/frontend/src/pages/user-list-timeline.vue
index de6737f37d..3efeb46c0a 100644
--- a/packages/frontend/src/pages/user-list-timeline.vue
+++ b/packages/frontend/src/pages/user-list-timeline.vue
@@ -27,7 +27,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { computed, watch, ref, shallowRef } from 'vue';
 import * as Misskey from 'misskey-js';
 import MkTimeline from '@/components/MkTimeline.vue';
-import { scroll } from '@/scripts/scroll.js';
+import { scroll } from '@@/js/scroll.js';
 import { misskeyApi } from '@/scripts/misskey-api.js';
 import { definePageMetadata } from '@/scripts/page-metadata.js';
 import { i18n } from '@/i18n.js';
@@ -79,26 +79,26 @@ definePageMetadata(() => ({
 <style lang="scss" module>
 .new {
 	position: sticky;
-	top: calc(var(--stickyTop, 0px) + 16px);
+	top: calc(var(--MI-stickyTop, 0px) + 16px);
 	z-index: 1000;
 	width: 100%;
 	margin: calc(-0.675em - 8px) 0;
 
 	&:first-child {
-		margin-top: calc(-0.675em - 8px - var(--margin));
+		margin-top: calc(-0.675em - 8px - var(--MI-margin));
 	}
 }
 
 .newButton {
 	display: block;
-	margin: var(--margin) auto 0 auto;
+	margin: var(--MI-margin) auto 0 auto;
 	padding: 8px 16px;
 	border-radius: 32px;
 }
 
 .tl {
-	background: var(--bg);
-	border-radius: var(--radius);
+	background: var(--MI_THEME-bg);
+	border-radius: var(--MI-radius);
 	overflow: clip;
 }
 </style>
diff --git a/packages/frontend/src/pages/user/clips.vue b/packages/frontend/src/pages/user/clips.vue
index ac01cff8cd..38ce78e8d5 100644
--- a/packages/frontend/src/pages/user/clips.vue
+++ b/packages/frontend/src/pages/user/clips.vue
@@ -43,6 +43,6 @@ const pagination = {
 .description {
 	margin-top: 8px;
 	padding-top: 8px;
-	border-top: solid 0.5px var(--divider);
+	border-top: solid 0.5px var(--MI_THEME-divider);
 }
 </style>
diff --git a/packages/frontend/src/pages/user/follow-list.vue b/packages/frontend/src/pages/user/follow-list.vue
index e60dccec17..868767e8f4 100644
--- a/packages/frontend/src/pages/user/follow-list.vue
+++ b/packages/frontend/src/pages/user/follow-list.vue
@@ -45,6 +45,6 @@ const followersPagination = {
 .users {
 	display: grid;
 	grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
-	grid-gap: var(--margin);
+	grid-gap: var(--MI-margin);
 }
 </style>
diff --git a/packages/frontend/src/pages/user/gallery.vue b/packages/frontend/src/pages/user/gallery.vue
index 9ba81322ba..0bc5628528 100644
--- a/packages/frontend/src/pages/user/gallery.vue
+++ b/packages/frontend/src/pages/user/gallery.vue
@@ -38,6 +38,6 @@ const pagination = {
 	display: grid;
 	grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
 	grid-gap: 12px;
-	margin: var(--margin);
+	margin: var(--MI-margin);
 }
 </style>
diff --git a/packages/frontend/src/pages/user/home.vue b/packages/frontend/src/pages/user/home.vue
index 4e3e383e33..2794db2821 100644
--- a/packages/frontend/src/pages/user/home.vue
+++ b/packages/frontend/src/pages/user/home.vue
@@ -23,7 +23,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 							<MkUserName class="name" :user="user" :nowrap="true"/>
 							<div class="bottom">
 								<span class="username"><MkAcct :user="user" :detail="true"/></span>
-								<span v-if="user.isAdmin" :title="i18n.ts.isAdmin" style="color: var(--badge);"><i class="ti ti-shield"></i></span>
+								<span v-if="user.isAdmin" :title="i18n.ts.isAdmin" style="color: var(--MI_THEME-badge);"><i class="ti ti-shield"></i></span>
 								<span v-if="user.isLocked" :title="i18n.ts.isLocked"><i class="ti ti-lock"></i></span>
 								<span v-if="user.isBot" :title="i18n.ts.isBot"><i class="ti ti-robot"></i></span>
 								<button v-if="$i && !isEditingMemo && !memoDraft" class="_button add-note-button" @click="showMemoTextarea">
@@ -32,9 +32,9 @@ SPDX-License-Identifier: AGPL-3.0-only
 							</div>
 						</div>
 						<span v-if="$i && $i.id != user.id && user.isFollowed" class="followed">{{ i18n.ts.followsYou }}</span>
-						<div v-if="$i" class="actions">
+						<div class="actions">
 							<button class="menu _button" @click="menu"><i class="ti ti-dots"></i></button>
-							<MkFollowButton v-if="$i.id != user.id" v-model:user="user" :inline="true" :transparent="false" :full="true" class="koudoku"/>
+							<MkFollowButton v-if="$i?.id != user.id" v-model:user="user" :inline="true" :transparent="false" :full="true" class="koudoku"/>
 						</div>
 					</div>
 					<MkAvatar class="avatar" :user="user" indicator/>
@@ -42,11 +42,17 @@ SPDX-License-Identifier: AGPL-3.0-only
 						<MkUserName :user="user" :nowrap="false" class="name"/>
 						<div class="bottom">
 							<span class="username"><MkAcct :user="user" :detail="true"/></span>
-							<span v-if="user.isAdmin" :title="i18n.ts.isAdmin" style="color: var(--badge);"><i class="ti ti-shield"></i></span>
+							<span v-if="user.isAdmin" :title="i18n.ts.isAdmin" style="color: var(--MI_THEME-badge);"><i class="ti ti-shield"></i></span>
 							<span v-if="user.isLocked" :title="i18n.ts.isLocked"><i class="ti ti-lock"></i></span>
 							<span v-if="user.isBot" :title="i18n.ts.isBot"><i class="ti ti-robot"></i></span>
 						</div>
 					</div>
+					<div v-if="user.followedMessage != null" class="followedMessage">
+						<MkFukidashi class="fukidashi" :tail="narrow ? 'none' : 'left'" negativeMargin shadow>
+							<div class="messageHeader">{{ i18n.ts.messageToFollower }}</div>
+							<div><MkSparkle><Mfm :plain="true" :text="user.followedMessage" :author="user"/></MkSparkle></div>
+						</MkFukidashi>
+					</div>
 					<div v-if="user.roles.length > 0" class="roles">
 						<span v-for="role in user.roles" :key="role.id" v-tooltip="role.description" class="role" :style="{ '--color': role.color }">
 							<MkA v-adaptive-bg :to="`/roles/${role.id}`">
@@ -58,6 +64,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 					<div v-if="iAmModerator" class="moderationNote">
 						<MkTextarea v-if="editModerationNote || (moderationNote != null && moderationNote !== '')" v-model="moderationNote" manualSave>
 							<template #label>{{ i18n.ts.moderationNote }}</template>
+							<template #caption>{{ i18n.ts.moderationNoteDescription }}</template>
 						</MkTextarea>
 						<div v-else>
 							<MkButton small @click="editModerationNote = true">{{ i18n.ts.addModerationNote }}</MkButton>
@@ -97,7 +104,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 					<div v-if="user.fields.length > 0" class="fields">
 						<dl v-for="(field, i) in user.fields" :key="i" class="field">
 							<dt class="name">
-								<Mfm :text="field.name" :plain="true" :colored="false"/>
+								<Mfm :text="field.name" :author="user" :plain="true" :colored="false"/>
 							</dt>
 							<dd class="value">
 								<Mfm :text="field.value" :author="user" :colored="false"/>
@@ -153,26 +160,30 @@ SPDX-License-Identifier: AGPL-3.0-only
 <script lang="ts" setup>
 import { defineAsyncComponent, computed, onMounted, onUnmounted, nextTick, watch, ref } from 'vue';
 import * as Misskey from 'misskey-js';
+import { getScrollPosition } from '@@/js/scroll.js';
 import MkNote from '@/components/MkNote.vue';
 import MkFollowButton from '@/components/MkFollowButton.vue';
 import MkAccountMoved from '@/components/MkAccountMoved.vue';
+import MkFukidashi from '@/components/MkFukidashi.vue';
 import MkRemoteCaution from '@/components/MkRemoteCaution.vue';
 import MkTextarea from '@/components/MkTextarea.vue';
 import MkOmit from '@/components/MkOmit.vue';
 import MkInfo from '@/components/MkInfo.vue';
 import MkButton from '@/components/MkButton.vue';
-import { getScrollPosition } from '@/scripts/scroll.js';
 import { getUserMenu } from '@/scripts/get-user-menu.js';
 import number from '@/filters/number.js';
 import { userPage } from '@/filters/user.js';
 import * as os from '@/os.js';
 import { i18n } from '@/i18n.js';
+import { defaultStore } from '@/store.js';
 import { $i, iAmModerator } from '@/account.js';
 import { dateString } from '@/filters/date.js';
 import { confetti } from '@/scripts/confetti.js';
 import { misskeyApi } from '@/scripts/misskey-api.js';
 import { isFollowingVisibleForMe, isFollowersVisibleForMe } from '@/scripts/isFfVisibleForMe.js';
 import { useRouter } from '@/router/supplier.js';
+import { getStaticImageUrl } from '@/scripts/media-proxy.js';
+import MkSparkle from '@/components/MkSparkle.vue';
 
 function calcAge(birthdate: string): number {
 	const date = new Date(birthdate);
@@ -220,8 +231,14 @@ watch(moderationNote, async () => {
 
 const style = computed(() => {
 	if (props.user.bannerUrl == null) return {};
-	return {
-		backgroundImage: `url(${ props.user.bannerUrl })`,
+	if (defaultStore.state.disableShowingAnimatedImages) {
+		return {
+			backgroundImage: `url(${ getStaticImageUrl(props.user.bannerUrl) })`,
+		};
+	} else {
+		return {
+			backgroundImage: `url(${ props.user.bannerUrl })`,
+		};
 	};
 });
 
@@ -240,7 +257,7 @@ function parallaxLoop() {
 }
 
 function parallax() {
-	const banner = bannerEl.value as any;
+	const banner = bannerEl.value;
 	if (banner == null) return;
 
 	const top = getScrollPosition(rootEl.value);
@@ -360,8 +377,8 @@ onUnmounted(() => {
 						position: absolute;
 						top: 12px;
 						right: 12px;
-						-webkit-backdrop-filter: var(--blur, blur(8px));
-						backdrop-filter: var(--blur, blur(8px));
+						-webkit-backdrop-filter: var(--MI-blur, blur(8px));
+						backdrop-filter: var(--MI-blur, blur(8px));
 						background: rgba(0, 0, 0, 0.2);
 						padding: 8px;
 						border-radius: 24px;
@@ -392,11 +409,12 @@ onUnmounted(() => {
 
 						> .name {
 							display: block;
-							margin: 0;
+							margin: -10px;
+							padding: 10px;
 							line-height: 32px;
 							font-weight: bold;
 							font-size: 1.8em;
-							text-shadow: 0 0 8px #000;
+							filter: drop-shadow(0 0 4px #000);
 						}
 
 						> .bottom {
@@ -414,8 +432,8 @@ onUnmounted(() => {
 							> .add-note-button {
 								background: rgba(0, 0, 0, 0.2);
 								color: #fff;
-								-webkit-backdrop-filter: var(--blur, blur(8px));
-								backdrop-filter: var(--blur, blur(8px));
+								-webkit-backdrop-filter: var(--MI-blur, blur(8px));
+								backdrop-filter: var(--MI-blur, blur(8px));
 								border-radius: 24px;
 								padding: 4px 8px;
 								font-size: 80%;
@@ -429,7 +447,7 @@ onUnmounted(() => {
 					text-align: center;
 					padding: 50px 8px 16px 8px;
 					font-weight: bold;
-					border-bottom: solid 0.5px var(--divider);
+					border-bottom: solid 0.5px var(--MI_THEME-divider);
 
 					> .bottom {
 						> * {
@@ -451,6 +469,22 @@ onUnmounted(() => {
 					box-shadow: 1px 1px 3px rgba(#000, 0.2);
 				}
 
+				> .followedMessage {
+					padding: 24px 24px 0 154px;
+
+					> .fukidashi {
+						display: block;
+						--fukidashi-bg: color-mix(in srgb, var(--MI_THEME-accent), var(--MI_THEME-panel) 85%);
+						--fukidashi-radius: 16px;
+						font-size: 0.9em;
+
+						.messageHeader {
+							opacity: 0.7;
+							font-size: 0.85em;
+						}
+					}
+				}
+
 				> .roles {
 					padding: 24px 24px 0 154px;
 					font-size: 0.95em;
@@ -459,7 +493,7 @@ onUnmounted(() => {
 					gap: 8px;
 
 					> .role {
-						border: solid 1px var(--color, var(--divider));
+						border: solid 1px var(--color, var(--MI_THEME-divider));
 						border-radius: 999px;
 						margin-right: 4px;
 						padding: 3px 8px;
@@ -473,15 +507,15 @@ onUnmounted(() => {
 				> .memo {
 					margin: 12px 24px 0 154px;
 					background: transparent;
-					color: var(--fg);
-					border: 1px solid var(--divider);
+					color: var(--MI_THEME-fg);
+					border: 1px solid var(--MI_THEME-divider);
 					border-radius: 8px;
 					padding: 8px;
 					line-height: 0;
 
 					> .heading {
 						text-align: left;
-						color: var(--fgTransparent);
+						color: var(--MI_THEME-fgTransparent);
 						line-height: 1.5;
 						font-size: 85%;
 					}
@@ -496,7 +530,7 @@ onUnmounted(() => {
 						height: auto;
 						min-height: 0;
 						line-height: 1.5;
-						color: var(--fg);
+						color: var(--MI_THEME-fg);
 						overflow: hidden;
 						background: transparent;
 						font-family: inherit;
@@ -516,7 +550,7 @@ onUnmounted(() => {
 				> .fields {
 					padding: 24px;
 					font-size: 0.9em;
-					border-top: solid 0.5px var(--divider);
+					border-top: solid 0.5px var(--MI_THEME-divider);
 
 					> .field {
 						display: flex;
@@ -553,14 +587,14 @@ onUnmounted(() => {
 				> .status {
 					display: flex;
 					padding: 24px;
-					border-top: solid 0.5px var(--divider);
+					border-top: solid 0.5px var(--MI_THEME-divider);
 
 					> a {
 						flex: 1;
 						text-align: center;
 
 						&.active {
-							color: var(--accent);
+							color: var(--MI_THEME-accent);
 						}
 
 						&:hover {
@@ -582,7 +616,7 @@ onUnmounted(() => {
 
 		> .contents {
 			> .content {
-				margin-bottom: var(--margin);
+				margin-bottom: var(--MI-margin);
 			}
 		}
 	}
@@ -599,7 +633,7 @@ onUnmounted(() => {
 		> .sub {
 			max-width: 350px;
 			min-width: 350px;
-			margin-left: var(--margin);
+			margin-left: var(--MI-margin);
 		}
 	}
 }
@@ -633,6 +667,10 @@ onUnmounted(() => {
 					margin: auto;
 				}
 
+				> .followedMessage {
+					padding: 16px 16px 0 16px;
+				}
+
 				> .roles {
 					padding: 16px 16px 0 16px;
 					justify-content: center;
@@ -672,13 +710,13 @@ onUnmounted(() => {
 
 <style lang="scss" module>
 .tl {
-	background: var(--bg);
-	border-radius: var(--radius);
+	background: var(--MI_THEME-bg);
+	border-radius: var(--MI-radius);
 	overflow: clip;
 }
 
 .verifiedLink {
 	margin-left: 4px;
-	color: var(--success);
+	color: var(--MI_THEME-success);
 }
 </style>
diff --git a/packages/frontend/src/pages/user/index.timeline.vue b/packages/frontend/src/pages/user/index.timeline.vue
index 8dbf90f344..49d015a530 100644
--- a/packages/frontend/src/pages/user/index.timeline.vue
+++ b/packages/frontend/src/pages/user/index.timeline.vue
@@ -51,13 +51,13 @@ const pagination = computed(() => tab.value === 'featured' ? {
 
 <style lang="scss" module>
 .tab {
-	padding: calc(var(--margin) / 2) 0;
-	background: var(--bg);
+	padding: calc(var(--MI-margin) / 2) 0;
+	background: var(--MI_THEME-bg);
 }
 
 .tl {
-	background: var(--bg);
-	border-radius: var(--radius);
+	background: var(--MI_THEME-bg);
+	border-radius: var(--MI-radius);
 	overflow: clip;
 }
 </style>
diff --git a/packages/frontend/src/pages/user/lists.vue b/packages/frontend/src/pages/user/lists.vue
index 5e9b95eb74..00de3e9132 100644
--- a/packages/frontend/src/pages/user/lists.vue
+++ b/packages/frontend/src/pages/user/lists.vue
@@ -44,12 +44,12 @@ const pagination = {
 .list {
 	display: block;
 	padding: 16px;
-	border: solid 1px var(--divider);
+	border: solid 1px var(--MI_THEME-divider);
 	border-radius: 6px;
 	margin-bottom: 8px;
 
 	&:hover {
-		border: solid 1px var(--accent);
+		border: solid 1px var(--MI_THEME-accent);
 		text-decoration: none;
 	}
 }
diff --git a/packages/frontend/src/pages/user/raw.vue b/packages/frontend/src/pages/user/raw.vue
index dd57048409..e6e66bd6af 100644
--- a/packages/frontend/src/pages/user/raw.vue
+++ b/packages/frontend/src/pages/user/raw.vue
@@ -113,18 +113,18 @@ const suspended = computed(() => props.user.isSuspended ?? false);
 	}
 
 	> .suspended {
-		color: var(--error);
-		border-color: var(--error);
+		color: var(--MI_THEME-error);
+		border-color: var(--MI_THEME-error);
 	}
 
 	> .silenced {
-		color: var(--warn);
-		border-color: var(--warn);
+		color: var(--MI_THEME-warn);
+		border-color: var(--MI_THEME-warn);
 	}
 
 	> .moderator {
-		color: var(--success);
-		border-color: var(--success);
+		color: var(--MI_THEME-success);
+		border-color: var(--MI_THEME-success);
 	}
 }
 </style>
diff --git a/packages/frontend/src/pages/user/reactions.vue b/packages/frontend/src/pages/user/reactions.vue
index 3671decc18..7168778e12 100644
--- a/packages/frontend/src/pages/user/reactions.vue
+++ b/packages/frontend/src/pages/user/reactions.vue
@@ -44,7 +44,7 @@ const pagination = {
 	align-items: center;
 	padding: 8px 16px;
 	margin-bottom: 8px;
-	border-bottom: solid 2px var(--divider);
+	border-bottom: solid 2px var(--MI_THEME-divider);
 }
 
 .avatar {
diff --git a/packages/frontend/src/pages/welcome.entrance.a.vue b/packages/frontend/src/pages/welcome.entrance.a.vue
index d6ba397f1b..f0e4a852c9 100644
--- a/packages/frontend/src/pages/welcome.entrance.a.vue
+++ b/packages/frontend/src/pages/welcome.entrance.a.vue
@@ -98,7 +98,7 @@ misskeyApiGet('federation/instances', {
 		left: 0;
 		width: 100vw;
 		height: 100vh;
-		background: var(--accent);
+		background: var(--MI_THEME-accent);
 		clip-path: polygon(0% 0%, 45% 0%, 20% 100%, 0% 100%);
 	}
 	> .shape2 {
@@ -107,7 +107,7 @@ misskeyApiGet('federation/instances', {
 		left: 0;
 		width: 100vw;
 		height: 100vh;
-		background: var(--accent);
+		background: var(--MI_THEME-accent);
 		clip-path: polygon(0% 0%, 25% 0%, 35% 100%, 0% 100%);
 		opacity: 0.5;
 	}
@@ -164,9 +164,9 @@ misskeyApiGet('federation/instances', {
 		left: 0;
 		right: 0;
 		margin: auto;
-		background: var(--acrylicPanel);
-		-webkit-backdrop-filter: var(--blur, blur(15px));
-		backdrop-filter: var(--blur, blur(15px));
+		background: var(--MI_THEME-acrylicPanel);
+		-webkit-backdrop-filter: var(--MI-blur, blur(15px));
+		backdrop-filter: var(--MI-blur, blur(15px));
 		border-radius: 999px;
 		overflow: clip;
 		width: 800px;
@@ -186,7 +186,7 @@ misskeyApiGet('federation/instances', {
 	vertical-align: bottom;
 	padding: 6px 12px 6px 6px;
 	margin: 0 10px 0 0;
-	background: var(--panel);
+	background: var(--MI_THEME-panel);
 	border-radius: 999px;
 
 	> :global(.icon) {
diff --git a/packages/frontend/src/pages/welcome.setup.vue b/packages/frontend/src/pages/welcome.setup.vue
index 5c31259499..33cc139a45 100644
--- a/packages/frontend/src/pages/welcome.setup.vue
+++ b/packages/frontend/src/pages/welcome.setup.vue
@@ -14,6 +14,10 @@ SPDX-License-Identifier: AGPL-3.0-only
 			</div>
 			<div class="_gaps_m" style="padding: 32px;">
 				<div>{{ i18n.ts.intro }}</div>
+				<MkInput v-model="setupPassword" type="password" data-cy-admin-initial-password>
+					<template #label>{{ i18n.ts.initialPasswordForSetup }} <div v-tooltip:dialog="i18n.ts.initialPasswordForSetupDescription" class="_button _help"><i class="ti ti-help-circle"></i></div></template>
+					<template #prefix><i class="ti ti-lock"></i></template>
+				</MkInput>
 				<MkInput v-model="username" pattern="^[a-zA-Z0-9_]{1,20}$" :spellcheck="false" required data-cy-admin-username>
 					<template #label>{{ i18n.ts.username }}</template>
 					<template #prefix>@</template>
@@ -36,9 +40,9 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <script lang="ts" setup>
 import { ref } from 'vue';
+import { host, version } from '@@/js/config.js';
 import MkButton from '@/components/MkButton.vue';
 import MkInput from '@/components/MkInput.vue';
-import { host, version } from '@/config.js';
 import * as os from '@/os.js';
 import { misskeyApi } from '@/scripts/misskey-api.js';
 import { login } from '@/account.js';
@@ -47,6 +51,7 @@ import MkAnimBg from '@/components/MkAnimBg.vue';
 
 const username = ref('');
 const password = ref('');
+const setupPassword = ref('');
 const submitting = ref(false);
 
 function submit() {
@@ -56,14 +61,27 @@ function submit() {
 	misskeyApi('admin/accounts/create', {
 		username: username.value,
 		password: password.value,
+		setupPassword: setupPassword.value === '' ? null : setupPassword.value,
 	}).then(res => {
 		return login(res.token);
-	}).catch(() => {
+	}).catch((err) => {
 		submitting.value = false;
 
+		let title = i18n.ts.somethingHappened;
+		let text = err.message + '\n' + err.id;
+
+		if (err.code === 'ACCESS_DENIED') {
+			title = i18n.ts.permissionDeniedError;
+			text = i18n.ts.operationForbidden;
+		} else if (err.code === 'INCORRECT_INITIAL_PASSWORD') {
+			title = i18n.ts.permissionDeniedError;
+			text = i18n.ts.incorrectPassword;
+		}
+
 		os.alert({
 			type: 'error',
-			text: i18n.ts.somethingHappened,
+			title,
+			text,
 		});
 	});
 }
@@ -74,14 +92,14 @@ function submit() {
 	min-height: 100svh;
 	padding: 32px 32px 64px 32px;
 	box-sizing: border-box;
-display: grid;
-place-content: center;
+	display: grid;
+	place-content: center;
 }
 
 .form {
 	position: relative;
 	z-index: 10;
-	border-radius: var(--radius);
+	border-radius: var(--MI-radius);
 	box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1);
 	overflow: clip;
 	max-width: 500px;
@@ -92,8 +110,8 @@ place-content: center;
 	font-size: 1.5em;
 	text-align: center;
 	padding: 32px;
-	background: var(--accentedBg);
-	color: var(--accent);
+	background: var(--MI_THEME-accentedBg);
+	color: var(--MI_THEME-accent);
 	font-weight: bold;
 }
 
diff --git a/packages/frontend/src/pages/welcome.timeline.note.vue b/packages/frontend/src/pages/welcome.timeline.note.vue
new file mode 100644
index 0000000000..8fb84fd58f
--- /dev/null
+++ b/packages/frontend/src/pages/welcome.timeline.note.vue
@@ -0,0 +1,109 @@
+<!--
+SPDX-FileCopyrightText: syuilo and misskey-project
+SPDX-License-Identifier: AGPL-3.0-only
+-->
+
+<template>
+<div :key="note.id" :class="$style.note">
+	<div class="_panel _gaps_s" :class="$style.content">
+		<div v-if="note.cw != null" :class="$style.richcontent">
+			<div><Mfm :text="note.cw" :author="note.user"/></div>
+			<MkCwButton v-model="showContent" :text="note.text" :renote="note.renote" :files="note.files" :poll="note.poll" style="margin: 4px 0;"/>
+			<div v-if="showContent">
+				<MkA v-if="note.replyId" class="reply" :to="`/notes/${note.replyId}`"><i class="ti ti-arrow-back-up"></i></MkA>
+				<Mfm v-if="note.text" :text="note.text" :author="note.user"/>
+				<MkA v-if="note.renoteId" class="rp" :to="`/notes/${note.renoteId}`">RN: ...</MkA>
+			</div>
+		</div>
+		<div v-else ref="noteTextEl" :class="[$style.text, { [$style.collapsed]: shouldCollapse }]">
+			<MkA v-if="note.replyId" class="reply" :to="`/notes/${note.replyId}`"><i class="ti ti-arrow-back-up"></i></MkA>
+			<Mfm v-if="note.text" :text="note.text" :author="note.user"/>
+			<MkA v-if="note.renoteId" class="rp" :to="`/notes/${note.renoteId}`">RN: ...</MkA>
+		</div>
+		<div v-if="note.files && note.files.length > 0" :class="$style.richcontent">
+			<MkMediaList :mediaList="note.files.slice(0, 4)"/>
+		</div>
+		<div v-if="note.poll">
+			<MkPoll :noteId="note.id" :poll="note.poll" :readOnly="true"/>
+		</div>
+		<div v-if="note.reactionCount > 0" :class="$style.reactions">
+			<MkReactionsViewer :note="note" :maxNumber="16"/>
+		</div>
+	</div>
+</div>
+</template>
+
+<script lang="ts" setup>
+import { ref, shallowRef, onUpdated, onMounted } from 'vue';
+import * as Misskey from 'misskey-js';
+import MkReactionsViewer from '@/components/MkReactionsViewer.vue';
+import MkMediaList from '@/components/MkMediaList.vue';
+import MkPoll from '@/components/MkPoll.vue';
+import MkCwButton from '@/components/MkCwButton.vue';
+
+defineProps<{
+	note: Misskey.entities.Note;
+}>();
+
+const noteTextEl = shallowRef<HTMLDivElement>();
+const shouldCollapse = ref(false);
+const showContent = ref(false);
+
+function calcCollapse() {
+	if (noteTextEl.value) {
+		const height = noteTextEl.value.scrollHeight;
+		if (height > 200) {
+			shouldCollapse.value = true;
+		}
+	}
+}
+
+onMounted(() => {
+	calcCollapse();
+});
+
+onUpdated(() => {
+	calcCollapse();
+});
+</script>
+
+<style lang="scss" module>
+.note {
+	margin-left: auto;
+}
+
+.text {
+	position: relative;
+	max-height: 200px;
+	overflow: hidden;
+
+	&.collapsed::after {
+		content: '';
+		position: absolute;
+		bottom: 0;
+		left: 0;
+		width: 100%;
+		height: 64px;
+		background: linear-gradient(0deg, var(--MI_THEME-panel), color(from var(--MI_THEME-panel) srgb r g b / 0));
+	}
+}
+
+.content {
+	padding: 16px;
+	margin: 0 0 0 auto;
+	max-width: max-content;
+	border-radius: 16px;
+}
+
+.reactions {
+	box-sizing: border-box;
+	margin: 8px -16px -8px;
+	padding: 8px 16px 0;
+	width: calc(100% + 32px);
+	border-top: 1px solid var(--MI_THEME-divider);
+}
+
+.richcontent {
+	min-width: 250px;
+}
+</style>
diff --git a/packages/frontend/src/pages/welcome.timeline.vue b/packages/frontend/src/pages/welcome.timeline.vue
index 139b2e0a07..9be3a80a9e 100644
--- a/packages/frontend/src/pages/welcome.timeline.vue
+++ b/packages/frontend/src/pages/welcome.timeline.vue
@@ -4,24 +4,17 @@ SPDX-License-Identifier: AGPL-3.0-only
 -->
 
 <template>
-<div :class="$style.root">
-	<div ref="scrollEl" :class="[$style.scrollbox, { [$style.scroll]: isScrolling }]">
-		<div v-for="note in notes" :key="note.id" :class="$style.note">
-			<div class="_panel" :class="$style.content">
-				<div>
-					<MkA v-if="note.replyId" class="reply" :to="`/notes/${note.replyId}`"><i class="ti ti-arrow-back-up"></i></MkA>
-					<Mfm v-if="note.text" :text="note.text" :author="note.user"/>
-					<MkA v-if="note.renoteId" class="rp" :to="`/notes/${note.renoteId}`">RN: ...</MkA>
-				</div>
-				<div v-if="note.files.length > 0" :class="$style.richcontent">
-					<MkMediaList :mediaList="note.files"/>
-				</div>
-				<div v-if="note.poll">
-					<MkPoll :noteId="note.id" :poll="note.poll" :readOnly="true"/>
-				</div>
-			</div>
-			<MkReactionsViewer ref="reactionsViewer" :note="note"/>
-		</div>
+<div :class="$style.root" class="_gaps">
+	<div
+		ref="notesMainContainerEl"
+		class="_gaps"
+		:class="[$style.scrollBoxMain, { [$style.scrollIntro]: (scrollState === 'intro'), [$style.scrollLoop]: (scrollState === 'loop') }]"
+		@animationend="changeScrollState"
+	>
+		<XNote v-for="note in notes" :key="`${note.id}_1`" :class="$style.note" :note="note"/>
+	</div>
+	<div v-if="isScrolling" class="_gaps" :class="[$style.scrollBoxSub, { [$style.scrollIntro]: (scrollState === 'intro'), [$style.scrollLoop]: (scrollState === 'loop') }]">
+		<XNote v-for="note in notes" :key="`${note.id}_2`" :class="$style.note" :note="note"/>
 	</div>
 </div>
 </template>
@@ -29,43 +22,54 @@ SPDX-License-Identifier: AGPL-3.0-only
 <script lang="ts" setup>
 import * as Misskey from 'misskey-js';
 import { onUpdated, ref, shallowRef } from 'vue';
-import MkReactionsViewer from '@/components/MkReactionsViewer.vue';
-import MkMediaList from '@/components/MkMediaList.vue';
-import MkPoll from '@/components/MkPoll.vue';
+import XNote from '@/pages/welcome.timeline.note.vue';
 import { misskeyApiGet } from '@/scripts/misskey-api.js';
-import { getScrollContainer } from '@/scripts/scroll.js';
+import { getScrollContainer } from '@@/js/scroll.js';
 
 const notes = ref<Misskey.entities.Note[]>([]);
 const isScrolling = ref(false);
-const scrollEl = shallowRef<HTMLElement>();
+const scrollState = ref<null | 'intro' | 'loop'>(null);
+const notesMainContainerEl = shallowRef<HTMLElement>();
 
 misskeyApiGet('notes/featured').then(_notes => {
 	notes.value = _notes;
 });
 
+function changeScrollState() {
+	if (scrollState.value !== 'loop') {
+		scrollState.value = 'loop';
+	}
+}
+
 onUpdated(() => {
-	if (!scrollEl.value) return;
-	const container = getScrollContainer(scrollEl.value);
+	if (!notesMainContainerEl.value) return;
+	const container = getScrollContainer(notesMainContainerEl.value);
 	const containerHeight = container ? container.clientHeight : window.innerHeight;
-	if (scrollEl.value.offsetHeight > containerHeight) {
+	if (notesMainContainerEl.value.offsetHeight > containerHeight) {
+		if (scrollState.value === null) {
+			scrollState.value = 'intro';
+		}
 		isScrolling.value = true;
 	}
 });
 </script>
 
 <style lang="scss" module>
-@keyframes scroll {
+@keyframes scrollIntro {
 	0% {
 		transform: translate3d(0, 0, 0);
 	}
-	5% {
-		transform: translate3d(0, 0, 0);
+	100% {
+		transform: translate3d(0, calc(calc(-100% - 128px) - var(--MI-margin)), 0);
 	}
-	75% {
-		transform: translate3d(0, calc(-100% + 90vh), 0);
+}
+
+@keyframes scrollConstant {
+	0% {
+		transform: translate3d(0, -128px, 0);
 	}
-	90% {
-		transform: translate3d(0, calc(-100% + 90vh), 0);
+	100% {
+		transform: translate3d(0, calc(calc(-100% - 128px) - var(--MI-margin)), 0);
 	}
 }
 
@@ -73,24 +77,26 @@ onUpdated(() => {
 	text-align: right;
 }
 
-.scrollbox {
-	&.scroll {
-		animation: scroll 45s linear infinite;
+.scrollBoxMain {
+	&.scrollIntro {
+		animation: scrollIntro 30s linear forwards;
+	}
+	&.scrollLoop {
+		animation: scrollConstant 30s linear infinite;
 	}
 }
 
-.note {
-	margin: 16px 0 16px auto;
+.scrollBoxSub {
+	&.scrollIntro {
+		animation: scrollIntro 30s linear forwards;
+	}
+	&.scrollLoop {
+		animation: scrollConstant 30s linear infinite;
+	}
 }
 
-.content {
-	padding: 16px;
-	margin: 0 0 0 auto;
-	max-width: max-content;
-	border-radius: 16px;
-}
-
-.richcontent {
-	min-width: 250px;
+.root:has(.note:hover) .scrollBoxMain,
+.root:has(.note:hover) .scrollBoxSub {
+	animation-play-state: paused;
 }
 </style>
diff --git a/packages/frontend/src/pages/welcome.vue b/packages/frontend/src/pages/welcome.vue
index 915fe35025..38d257506c 100644
--- a/packages/frontend/src/pages/welcome.vue
+++ b/packages/frontend/src/pages/welcome.vue
@@ -15,7 +15,7 @@ import { computed, ref } from 'vue';
 import * as Misskey from 'misskey-js';
 import XSetup from './welcome.setup.vue';
 import XEntrance from './welcome.entrance.a.vue';
-import { instanceName } from '@/config.js';
+import { instanceName } from '@@/js/config.js';
 import { definePageMetadata } from '@/scripts/page-metadata.js';
 import { fetchInstance } from '@/instance.js';
 
diff --git a/packages/frontend/src/router/definition.ts b/packages/frontend/src/router/definition.ts
index c12ae0fa57..e98e0b59b1 100644
--- a/packages/frontend/src/router/definition.ts
+++ b/packages/frontend/src/router/definition.ts
@@ -3,15 +3,14 @@
  * SPDX-License-Identifier: AGPL-3.0-only
  */
 
-import { App, AsyncComponentLoader, defineAsyncComponent, provide } from 'vue';
-import type { RouteDef } from '@/nirax.js';
-import { IRouter, Router } from '@/nirax.js';
+import { AsyncComponentLoader, defineAsyncComponent } from 'vue';
+import type { IRouter, RouteDef } from '@/nirax.js';
+import { Router } from '@/nirax.js';
 import { $i, iAmModerator } from '@/account.js';
 import MkLoading from '@/pages/_loading_.vue';
 import MkError from '@/pages/_error_.vue';
-import { setMainRouter } from '@/router/main.js';
 
-const page = (loader: AsyncComponentLoader<any>) => defineAsyncComponent({
+export const page = (loader: AsyncComponentLoader) => defineAsyncComponent({
 	loader: loader,
 	loadingComponent: MkLoading,
 	errorComponent: MkError,
@@ -218,7 +217,7 @@ const routes: RouteDef[] = [{
 	component: page(() => import('@/pages/theme-editor.vue')),
 	loginRequired: true,
 }, {
-	path: '/roles/:role',
+	path: '/roles/:roleId',
 	component: page(() => import('@/pages/role.vue')),
 }, {
 	path: '/user-tags/:tag',
@@ -232,13 +231,26 @@ const routes: RouteDef[] = [{
 	component: page(() => import('@/pages/search.vue')),
 	query: {
 		q: 'query',
+		userId: 'userId',
+		username: 'username',
+		host: 'host',
 		channel: 'channel',
 		type: 'type',
 		origin: 'origin',
 	},
 }, {
+	// Legacy Compatibility
 	path: '/authorize-follow',
-	component: page(() => import('@/pages/follow.vue')),
+	redirect: '/lookup',
+	loginRequired: true,
+}, {
+	// Mastodon Compatibility
+	path: '/authorize_interaction',
+	redirect: '/lookup',
+	loginRequired: true,
+}, {
+	path: '/lookup',
+	component: page(() => import('@/pages/lookup.vue')),
 	loginRequired: true,
 }, {
 	path: '/share',
@@ -251,6 +263,9 @@ const routes: RouteDef[] = [{
 }, {
 	path: '/scratchpad',
 	component: page(() => import('@/pages/scratchpad.vue')),
+}, {
+	path: '/preview',
+	component: page(() => import('@/pages/preview.vue')),
 }, {
 	path: '/auth/:token',
 	component: page(() => import('@/pages/auth.vue')),
@@ -447,22 +462,14 @@ const routes: RouteDef[] = [{
 		path: '/relays',
 		name: 'relays',
 		component: page(() => import('@/pages/admin/relays.vue')),
-	}, {
-		path: '/instance-block',
-		name: 'instance-block',
-		component: page(() => import('@/pages/admin/instance-block.vue')),
-	}, {
-		path: '/proxy-account',
-		name: 'proxy-account',
-		component: page(() => import('@/pages/admin/proxy-account.vue')),
 	}, {
 		path: '/external-services',
 		name: 'external-services',
 		component: page(() => import('@/pages/admin/external-services.vue')),
 	}, {
-		path: '/other-settings',
-		name: 'other-settings',
-		component: page(() => import('@/pages/admin/other-settings.vue')),
+		path: '/performance',
+		name: 'performance',
+		component: page(() => import('@/pages/admin/performance.vue')),
 	}, {
 		path: '/server-rules',
 		name: 'server-rules',
@@ -471,6 +478,14 @@ const routes: RouteDef[] = [{
 		path: '/invites',
 		name: 'invites',
 		component: page(() => import('@/pages/admin/invites.vue')),
+	}, {
+		path: '/abuse-report-notification-recipient',
+		name: 'abuse-report-notification-recipient',
+		component: page(() => import('@/pages/admin/abuse-report/notification-recipient.vue')),
+	}, {
+		path: '/system-webhook',
+		name: 'system-webhook',
+		component: page(() => import('@/pages/admin/system-webhook.vue')),
 	}, {
 		path: '/',
 		component: page(() => import('@/pages/_empty_.vue')),
@@ -573,32 +588,6 @@ const routes: RouteDef[] = [{
 	component: page(() => import('@/pages/not-found.vue')),
 }];
 
-function createRouterImpl(path: string): IRouter {
+export function createMainRouter(path: string): IRouter {
 	return new Router(routes, path, !!$i, page(() => import('@/pages/not-found.vue')));
 }
-
-/**
- * {@link Router}による画面遷移を可能とするために{@link mainRouter}をセットアップする。
- * また、{@link Router}のインスタンスを作成するためのファクトリも{@link provide}経由で公開する(`routerFactory`というキーで取得可能)
- */
-export function setupRouter(app: App) {
-	app.provide('routerFactory', createRouterImpl);
-
-	const mainRouter = createRouterImpl(location.pathname + location.search + location.hash);
-
-	window.addEventListener('popstate', (event) => {
-		mainRouter.replace(location.pathname + location.search + location.hash, event.state?.key);
-	});
-
-	mainRouter.addListener('push', ctx => {
-		window.history.pushState({ key: ctx.key }, '', ctx.path);
-	});
-
-	mainRouter.addListener('replace', ctx => {
-		window.history.replaceState({ key: ctx.key }, '', ctx.path);
-	});
-
-	mainRouter.init();
-
-	setMainRouter(mainRouter);
-}
diff --git a/packages/frontend/src/router/main.ts b/packages/frontend/src/router/main.ts
index 7a3fde131e..3c25e80d12 100644
--- a/packages/frontend/src/router/main.ts
+++ b/packages/frontend/src/router/main.ts
@@ -3,9 +3,36 @@
  * SPDX-License-Identifier: AGPL-3.0-only
  */
 
-import { ShallowRef } from 'vue';
 import { EventEmitter } from 'eventemitter3';
-import { IRouter, Resolved, RouteDef, RouterEvent } from '@/nirax.js';
+import { IRouter, Resolved, RouteDef, RouterEvent, RouterFlag } from '@/nirax.js';
+
+import type { App, ShallowRef } from 'vue';
+
+/**
+ * {@link Router}による画面遷移を可能とするために{@link mainRouter}をセットアップする。
+ * また、{@link Router}のインスタンスを作成するためのファクトリも{@link provide}経由で公開する(`routerFactory`というキーで取得可能)
+ */
+export function setupRouter(app: App, routerFactory: ((path: string) => IRouter)): void {
+	app.provide('routerFactory', routerFactory);
+
+	const mainRouter = routerFactory(location.pathname + location.search + location.hash);
+
+	window.addEventListener('popstate', (event) => {
+		mainRouter.replace(location.pathname + location.search + location.hash, event.state?.key);
+	});
+
+	mainRouter.addListener('push', ctx => {
+		window.history.pushState({ key: ctx.key }, '', ctx.path);
+	});
+
+	mainRouter.addListener('replace', ctx => {
+		window.history.replaceState({ key: ctx.key }, '', ctx.path);
+	});
+
+	mainRouter.init();
+
+	setMainRouter(mainRouter);
+}
 
 function getMainRouter(): IRouter {
 	const router = mainRouterHolder;
@@ -52,7 +79,7 @@ class MainRouterProxy implements IRouter {
 		return this.supplier().currentRoute;
 	}
 
-	get navHook(): ((path: string, flag?: any) => boolean) | null {
+	get navHook(): ((path: string, flag?: RouterFlag) => boolean) | null {
 		return this.supplier().navHook;
 	}
 
@@ -64,11 +91,11 @@ class MainRouterProxy implements IRouter {
 		return this.supplier().getCurrentKey();
 	}
 
-	getCurrentPath(): any {
+	getCurrentPath(): string {
 		return this.supplier().getCurrentPath();
 	}
 
-	push(path: string, flag?: any): void {
+	push(path: string, flag?: RouterFlag): void {
 		this.supplier().push(path, flag);
 	}
 
diff --git a/packages/frontend/src/scripts/aiscript/api.ts b/packages/frontend/src/scripts/aiscript/api.ts
index 98a0c61752..46aed49330 100644
--- a/packages/frontend/src/scripts/aiscript/api.ts
+++ b/packages/frontend/src/scripts/aiscript/api.ts
@@ -4,13 +4,13 @@
  */
 
 import { utils, values } from '@syuilo/aiscript';
+import * as Misskey from 'misskey-js';
 import * as os from '@/os.js';
 import { misskeyApi } from '@/scripts/misskey-api.js';
 import { $i } from '@/account.js';
 import { miLocalStorage } from '@/local-storage.js';
 import { customEmojis } from '@/custom-emojis.js';
-import { url, lang } from '@/config.js';
-import { nyaize } from '@/scripts/nyaize.js';
+import { url, lang } from '@@/js/config.js';
 
 export function aiScriptReadline(q: string): Promise<string> {
 	return new Promise(ok => {
@@ -87,7 +87,7 @@ export function createAiScriptEnv(opts) {
 		}),
 		'Mk:nyaize': values.FN_NATIVE(([text]) => {
 			utils.assertString(text);
-			return values.STR(nyaize(text.value));
+			return values.STR(Misskey.nyaize(text.value));
 		}),
 	};
 }
diff --git a/packages/frontend/src/scripts/aiscript/ui.ts b/packages/frontend/src/scripts/aiscript/ui.ts
index fa3fcac2e7..2b386bebb8 100644
--- a/packages/frontend/src/scripts/aiscript/ui.ts
+++ b/packages/frontend/src/scripts/aiscript/ui.ts
@@ -27,6 +27,8 @@ export type AsUiContainer = AsUiComponentBase & {
 	font?: 'serif' | 'sans-serif' | 'monospace';
 	borderWidth?: number;
 	borderColor?: string;
+	borderStyle?: 'hidden' | 'dotted' | 'dashed' | 'solid' | 'double' | 'groove' | 'ridge' | 'inset' | 'outset';
+	borderRadius?: number;
 	padding?: number;
 	rounded?: boolean;
 	hidden?: boolean;
@@ -173,6 +175,10 @@ function getContainerOptions(def: values.Value | undefined): Omit<AsUiContainer,
 	if (borderWidth) utils.assertNumber(borderWidth);
 	const borderColor = def.value.get('borderColor');
 	if (borderColor) utils.assertString(borderColor);
+	const borderStyle = def.value.get('borderStyle');
+	if (borderStyle) utils.assertString(borderStyle);
+	const borderRadius = def.value.get('borderRadius');
+	if (borderRadius) utils.assertNumber(borderRadius);
 	const padding = def.value.get('padding');
 	if (padding) utils.assertNumber(padding);
 	const rounded = def.value.get('rounded');
@@ -191,6 +197,8 @@ function getContainerOptions(def: values.Value | undefined): Omit<AsUiContainer,
 		font: font?.value,
 		borderWidth: borderWidth?.value,
 		borderColor: borderColor?.value,
+		borderStyle: borderStyle?.value,
+		borderRadius: borderRadius?.value,
 		padding: padding?.value,
 		rounded: rounded?.value,
 		hidden: hidden?.value,
diff --git a/packages/frontend/src/scripts/array.ts b/packages/frontend/src/scripts/array.ts
index b3d76e149f..f2feb29dfc 100644
--- a/packages/frontend/src/scripts/array.ts
+++ b/packages/frontend/src/scripts/array.ts
@@ -77,44 +77,6 @@ export function maximum(xs: number[]): number {
 	return Math.max(...xs);
 }
 
-/**
- * Splits an array based on the equivalence relation.
- * The concatenation of the result is equal to the argument.
- */
-export function groupBy<T>(f: EndoRelation<T>, xs: T[]): T[][] {
-	const groups = [] as T[][];
-	for (const x of xs) {
-		const lastGroup = groups.at(-1);
-		if (lastGroup !== undefined && f(lastGroup[0], x)) {
-			lastGroup.push(x);
-		} else {
-			groups.push([x]);
-		}
-	}
-	return groups;
-}
-
-/**
- * Splits an array based on the equivalence relation induced by the function.
- * The concatenation of the result is equal to the argument.
- */
-export function groupOn<T, S>(f: (x: T) => S, xs: T[]): T[][] {
-	return groupBy((a, b) => f(a) === f(b), xs);
-}
-
-export function groupByX<T>(collections: T[], keySelector: (x: T) => string) {
-	return collections.reduce((obj: Record<string, T[]>, item: T) => {
-		const key = keySelector(item);
-		if (typeof obj[key] === 'undefined') {
-			obj[key] = [];
-		}
-
-		obj[key].push(item);
-
-		return obj;
-	}, {});
-}
-
 /**
  * Compare two arrays by lexicographical order
  */
diff --git a/packages/frontend/src/scripts/check-permissions.ts b/packages/frontend/src/scripts/check-permissions.ts
new file mode 100644
index 0000000000..ed86529d5b
--- /dev/null
+++ b/packages/frontend/src/scripts/check-permissions.ts
@@ -0,0 +1,19 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { instance } from '@/instance.js';
+import { $i } from '@/account.js';
+
+export const notesSearchAvailable = (
+	// FIXME: instance.policies would be null in Vitest
+	// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
+	($i == null && instance.policies != null && instance.policies.canSearchNotes) ||
+	($i != null && $i.policies.canSearchNotes) ||
+	false
+) as boolean;
+
+export const canSearchNonLocalNotes = (
+	instance.noteSearchableScope === 'global'
+);
diff --git a/packages/frontend/src/scripts/check-reaction-permissions.ts b/packages/frontend/src/scripts/check-reaction-permissions.ts
index 8fc857f84f..c3c3f419a9 100644
--- a/packages/frontend/src/scripts/check-reaction-permissions.ts
+++ b/packages/frontend/src/scripts/check-reaction-permissions.ts
@@ -4,7 +4,7 @@
  */
 
 import * as Misskey from 'misskey-js';
-import { UnicodeEmojiDef } from './emojilist.js';
+import { UnicodeEmojiDef } from '@@/js/emojilist.js';
 
 export function checkReactionPermissions(me: Misskey.entities.MeDetailed, note: Misskey.entities.Note, emoji: Misskey.entities.EmojiSimple | UnicodeEmojiDef | string): boolean {
 	if (typeof emoji === 'string') return true; // UnicodeEmojiDefにも無い絵文字であれば文字列で来る。Unicode絵文字であることには変わりないので常にリアクション可能とする;
diff --git a/packages/frontend/src/scripts/check-word-mute.ts b/packages/frontend/src/scripts/check-word-mute.ts
index 67e896b4b9..0a37a08bf0 100644
--- a/packages/frontend/src/scripts/check-word-mute.ts
+++ b/packages/frontend/src/scripts/check-word-mute.ts
@@ -2,8 +2,9 @@
  * SPDX-FileCopyrightText: syuilo and misskey-project
  * SPDX-License-Identifier: AGPL-3.0-only
  */
+import * as Misskey from 'misskey-js';
 
-export function checkWordMute(note: Record<string, any>, me: Record<string, any> | null | undefined, mutedWords: Array<string | string[]>): boolean {
+export function checkWordMute(note: Misskey.entities.Note, me: Misskey.entities.UserLite | null | undefined, mutedWords: Array<string | string[]>): boolean {
 	// 自分自身
 	if (me && (note.userId === me.id)) return false;
 
diff --git a/packages/frontend/src/scripts/code-highlighter.ts b/packages/frontend/src/scripts/code-highlighter.ts
index e94027d302..6710d9826e 100644
--- a/packages/frontend/src/scripts/code-highlighter.ts
+++ b/packages/frontend/src/scripts/code-highlighter.ts
@@ -3,17 +3,17 @@
  * SPDX-License-Identifier: AGPL-3.0-only
  */
 
-import { getHighlighterCore, loadWasm } from 'shiki/core';
+import { createHighlighterCore, loadWasm } from 'shiki/core';
 import darkPlus from 'shiki/themes/dark-plus.mjs';
 import { bundledThemesInfo } from 'shiki/themes';
 import { bundledLanguagesInfo } from 'shiki/langs';
+import lightTheme from '@@/themes/_light.json5';
+import darkTheme from '@@/themes/_dark.json5';
 import { unique } from './array.js';
 import { deepClone } from './clone.js';
 import { deepMerge } from './merge.js';
 import type { HighlighterCore, LanguageRegistration, ThemeRegistration, ThemeRegistrationRaw } from 'shiki/core';
 import { ColdDeviceStorage } from '@/store.js';
-import lightTheme from '@/themes/_light.json5';
-import darkTheme from '@/themes/_dark.json5';
 
 let _highlighter: HighlighterCore | null = null;
 
@@ -69,7 +69,7 @@ async function initHighlighter() {
 	]);
 
 	const jsLangInfo = bundledLanguagesInfo.find(t => t.id === 'javascript');
-	const highlighter = await getHighlighterCore({
+	const highlighter = await createHighlighterCore({
 		themes,
 		langs: [
 			...(jsLangInfo ? [async () => await jsLangInfo.import()] : []),
diff --git a/packages/frontend/src/scripts/copy-to-clipboard.ts b/packages/frontend/src/scripts/copy-to-clipboard.ts
index 216c0464b3..7e0bb25606 100644
--- a/packages/frontend/src/scripts/copy-to-clipboard.ts
+++ b/packages/frontend/src/scripts/copy-to-clipboard.ts
@@ -6,33 +6,6 @@
 /**
  * Clipboardに値をコピー(TODO: 文字列以外も対応)
  */
-export default val => {
-	// 空div 生成
-	const tmp = document.createElement('div');
-	// 選択用のタグ生成
-	const pre = document.createElement('pre');
-
-	// 親要素のCSSで user-select: none だとコピーできないので書き換える
-	pre.style.webkitUserSelect = 'auto';
-	pre.style.userSelect = 'auto';
-
-	tmp.appendChild(pre).textContent = val;
-
-	// 要素を画面外へ
-	const s = tmp.style;
-	s.position = 'fixed';
-	s.right = '200%';
-
-	// body に追加
-	document.body.appendChild(tmp);
-	// 要素を選択
-	document.getSelection().selectAllChildren(tmp);
-
-	// クリップボードにコピー
-	const result = document.execCommand('copy');
-
-	// 要素削除
-	document.body.removeChild(tmp);
-
-	return result;
+export function copyToClipboard(input: string | null) {
+	if (input) navigator.clipboard.writeText(input);
 };
diff --git a/packages/frontend/src/scripts/focus-trap.ts b/packages/frontend/src/scripts/focus-trap.ts
new file mode 100644
index 0000000000..fb7caea830
--- /dev/null
+++ b/packages/frontend/src/scripts/focus-trap.ts
@@ -0,0 +1,134 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+import { getHTMLElementOrNull } from '@/scripts/get-dom-node-or-null.js';
+
+const focusTrapElements = new Set<HTMLElement>();
+const ignoreElements = [
+	'script',
+	'style',
+];
+
+function containsFocusTrappedElements(el: HTMLElement): boolean {
+	return Array.from(focusTrapElements).some((focusTrapElement) => {
+		return el.contains(focusTrapElement);
+	});
+}
+
+function getZIndex(el: HTMLElement): number {
+	const zIndex = parseInt(window.getComputedStyle(el).zIndex || '0', 10);
+	if (isNaN(zIndex)) {
+		return 0;
+	}
+	return zIndex;
+}
+
+function getHighestZIndexElement(): { el: HTMLElement; zIndex: number; } | null {
+	let highestZIndexElement: HTMLElement | null = null;
+	let highestZIndex = -Infinity;
+
+	focusTrapElements.forEach((el) => {
+		const zIndex = getZIndex(el);
+		if (zIndex > highestZIndex) {
+			highestZIndex = zIndex;
+			highestZIndexElement = el;
+		}
+	});
+
+	return highestZIndexElement == null ? null : {
+		el: highestZIndexElement,
+		zIndex: highestZIndex,
+	};
+}
+
+function releaseFocusTrap(el: HTMLElement): void {
+	focusTrapElements.delete(el);
+	if (el.inert === true) {
+		el.inert = false;
+	}
+
+	const highestZIndexElement = getHighestZIndexElement();
+
+	if (el.parentElement != null && el !== document.body) {
+		el.parentElement.childNodes.forEach((siblingNode) => {
+			const siblingEl = getHTMLElementOrNull(siblingNode);
+			if (!siblingEl) return;
+			if (
+				siblingEl !== el &&
+				(
+					highestZIndexElement == null ||
+					siblingEl === highestZIndexElement.el ||
+					siblingEl.contains(highestZIndexElement.el)
+				)
+			) {
+				siblingEl.inert = false;
+			} else if (
+				highestZIndexElement != null &&
+				siblingEl !== highestZIndexElement.el &&
+				!siblingEl.contains(highestZIndexElement.el) &&
+				!ignoreElements.includes(siblingEl.tagName.toLowerCase())
+			) {
+				siblingEl.inert = true;
+			} else {
+				siblingEl.inert = false;
+			}
+		});
+		releaseFocusTrap(el.parentElement);
+	}
+}
+
+export function focusTrap(el: HTMLElement, hasInteractionWithOtherFocusTrappedEls: boolean, parent: true): void;
+export function focusTrap(el: HTMLElement, hasInteractionWithOtherFocusTrappedEls?: boolean, parent?: false): { release: () => void; };
+export function focusTrap(el: HTMLElement, hasInteractionWithOtherFocusTrappedEls = false, parent = false): { release: () => void; } | void {
+	const highestZIndexElement = getHighestZIndexElement();
+
+	const highestZIndex = highestZIndexElement == null ? -Infinity : highestZIndexElement.zIndex;
+	const zIndex = getZIndex(el);
+
+	// If the element has a lower z-index than the highest z-index element, focus trap the highest z-index element instead
+	// Focus trapping for this element will be done in the release function
+	if (!parent && zIndex < highestZIndex) {
+		focusTrapElements.add(el);
+		if (highestZIndexElement) {
+			focusTrap(highestZIndexElement.el, hasInteractionWithOtherFocusTrappedEls);
+		}
+		return {
+			release: () => {
+				releaseFocusTrap(el);
+			},
+		};
+	}
+
+	if (el.inert === true) {
+		el.inert = false;
+	}
+
+	if (el.parentElement != null && el !== document.body) {
+		el.parentElement.childNodes.forEach((siblingNode) => {
+			const siblingEl = getHTMLElementOrNull(siblingNode);
+			if (!siblingEl) return;
+			if (
+				siblingEl !== el &&
+				(
+					hasInteractionWithOtherFocusTrappedEls === false ||
+					(!focusTrapElements.has(siblingEl) && !containsFocusTrappedElements(siblingEl))
+				) &&
+				!ignoreElements.includes(siblingEl.tagName.toLowerCase())
+			) {
+				siblingEl.inert = true;
+			}
+		});
+		focusTrap(el.parentElement, hasInteractionWithOtherFocusTrappedEls, true);
+	}
+
+	if (!parent) {
+		focusTrapElements.add(el);
+
+		return {
+			release: () => {
+				releaseFocusTrap(el);
+			},
+		};
+	}
+}
diff --git a/packages/frontend/src/scripts/focus.ts b/packages/frontend/src/scripts/focus.ts
index ea6ee61c88..81278b17ea 100644
--- a/packages/frontend/src/scripts/focus.ts
+++ b/packages/frontend/src/scripts/focus.ts
@@ -3,30 +3,78 @@
  * SPDX-License-Identifier: AGPL-3.0-only
  */
 
-export function focusPrev(el: Element | null, self = false, scroll = true) {
-	if (el == null) return;
-	if (!self) el = el.previousElementSibling;
-	if (el) {
-		if (el.hasAttribute('tabindex')) {
-			(el as HTMLElement).focus({
-				preventScroll: !scroll,
-			});
-		} else {
-			focusPrev(el.previousElementSibling, true);
-		}
-	}
-}
+import { getScrollPosition, getScrollContainer, getStickyBottom, getStickyTop } from '@@/js/scroll.js';
+import { getElementOrNull, getNodeOrNull } from '@/scripts/get-dom-node-or-null.js';
 
-export function focusNext(el: Element | null, self = false, scroll = true) {
-	if (el == null) return;
-	if (!self) el = el.nextElementSibling;
-	if (el) {
-		if (el.hasAttribute('tabindex')) {
-			(el as HTMLElement).focus({
-				preventScroll: !scroll,
-			});
-		} else {
-			focusPrev(el.nextElementSibling, true);
-		}
+type MaybeHTMLElement = EventTarget | Node | Element | HTMLElement;
+
+export const isFocusable = (input: MaybeHTMLElement | null | undefined): input is HTMLElement => {
+	if (input == null || !(input instanceof HTMLElement)) return false;
+
+	if (input.tabIndex < 0) return false;
+	if ('disabled' in input && input.disabled === true) return false;
+	if ('readonly' in input && input.readonly === true) return false;
+
+	if (!input.ownerDocument.contains(input)) return false;
+
+	const style = window.getComputedStyle(input);
+	if (style.display === 'none') return false;
+	if (style.visibility === 'hidden') return false;
+	if (style.opacity === '0') return false;
+	if (style.pointerEvents === 'none') return false;
+
+	return true;
+};
+
+export const focusPrev = (input: MaybeHTMLElement | null | undefined, self = false, scroll = true) => {
+	const element = self ? input : getElementOrNull(input)?.previousElementSibling;
+	if (element == null) return;
+	if (isFocusable(element)) {
+		focusOrScroll(element, scroll);
+	} else {
+		focusPrev(element, false, scroll);
 	}
-}
+};
+
+export const focusNext = (input: MaybeHTMLElement | null | undefined, self = false, scroll = true) => {
+	const element = self ? input : getElementOrNull(input)?.nextElementSibling;
+	if (element == null) return;
+	if (isFocusable(element)) {
+		focusOrScroll(element, scroll);
+	} else {
+		focusNext(element, false, scroll);
+	}
+};
+
+export const focusParent = (input: MaybeHTMLElement | null | undefined, self = false, scroll = true) => {
+	const element = self ? input : getNodeOrNull(input)?.parentElement;
+	if (element == null) return;
+	if (isFocusable(element)) {
+		focusOrScroll(element, scroll);
+	} else {
+		focusParent(element, false, scroll);
+	}
+};
+
+const focusOrScroll = (element: HTMLElement, scroll: boolean) => {
+	if (scroll) {
+		const scrollContainer = getScrollContainer(element) ?? document.documentElement;
+		const scrollContainerTop = getScrollPosition(scrollContainer);
+		const stickyTop = getStickyTop(element, scrollContainer);
+		const stickyBottom = getStickyBottom(element, scrollContainer);
+		const top = element.getBoundingClientRect().top;
+		const bottom = element.getBoundingClientRect().bottom;
+
+		let scrollTo = scrollContainerTop;
+		if (top < stickyTop) {
+			scrollTo += top - stickyTop;
+		} else if (bottom > window.innerHeight - stickyBottom) {
+			scrollTo += bottom - window.innerHeight + stickyBottom;
+		}
+		scrollContainer.scrollTo({ top: scrollTo, behavior: 'instant' });
+	}
+
+	if (document.activeElement !== element) {
+		element.focus({ preventScroll: true });
+	}
+};
diff --git a/packages/frontend/src/scripts/form.ts b/packages/frontend/src/scripts/form.ts
index 242a504c3b..1032e97ac9 100644
--- a/packages/frontend/src/scripts/form.ts
+++ b/packages/frontend/src/scripts/form.ts
@@ -15,7 +15,7 @@ type Hidden = boolean | ((v: any) => boolean);
 export type FormItem = {
 	label?: string;
 	type: 'string';
-	default: string | null;
+	default?: string | null;
 	description?: string;
 	required?: boolean;
 	hidden?: Hidden;
@@ -24,7 +24,7 @@ export type FormItem = {
 } | {
 	label?: string;
 	type: 'number';
-	default: number | null;
+	default?: number | null;
 	description?: string;
 	required?: boolean;
 	hidden?: Hidden;
@@ -32,20 +32,20 @@ export type FormItem = {
 } | {
 	label?: string;
 	type: 'boolean';
-	default: boolean | null;
+	default?: boolean | null;
 	description?: string;
 	hidden?: Hidden;
 } | {
 	label?: string;
 	type: 'enum';
-	default: string | null;
+	default?: string | null;
 	required?: boolean;
 	hidden?: Hidden;
 	enum: EnumItem[];
 } | {
 	label?: string;
 	type: 'radio';
-	default: unknown | null;
+	default?: unknown | null;
 	required?: boolean;
 	hidden?: Hidden;
 	options: {
@@ -55,7 +55,7 @@ export type FormItem = {
 } | {
 	label?: string;
 	type: 'range';
-	default: number | null;
+	default?: number | null;
 	description?: string;
 	required?: boolean;
 	step?: number;
@@ -66,12 +66,12 @@ export type FormItem = {
 } | {
 	label?: string;
 	type: 'object';
-	default: Record<string, unknown> | null;
+	default?: Record<string, unknown> | null;
 	hidden: Hidden;
 } | {
 	label?: string;
 	type: 'array';
-	default: unknown[] | null;
+	default?: unknown[] | null;
 	hidden: Hidden;
 } | {
 	type: 'button';
diff --git a/packages/frontend/src/scripts/gen-search-query.ts b/packages/frontend/src/scripts/gen-search-query.ts
index 60884d08d3..a85ee01e26 100644
--- a/packages/frontend/src/scripts/gen-search-query.ts
+++ b/packages/frontend/src/scripts/gen-search-query.ts
@@ -4,7 +4,7 @@
  */
 
 import * as Misskey from 'misskey-js';
-import { host as localHost } from '@/config.js';
+import { host as localHost } from '@@/js/config.js';
 
 export async function genSearchQuery(v: any, q: string) {
 	let host: string;
diff --git a/packages/frontend/src/scripts/get-appear-note.ts b/packages/frontend/src/scripts/get-appear-note.ts
new file mode 100644
index 0000000000..40ce80eac9
--- /dev/null
+++ b/packages/frontend/src/scripts/get-appear-note.ts
@@ -0,0 +1,10 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import * as Misskey from 'misskey-js';
+
+export function getAppearNote(note: Misskey.entities.Note) {
+	return Misskey.note.isPureRenote(note) ? note.renote : note;
+}
diff --git a/packages/frontend/src/scripts/get-bg-color.ts b/packages/frontend/src/scripts/get-bg-color.ts
new file mode 100644
index 0000000000..ccf60b454f
--- /dev/null
+++ b/packages/frontend/src/scripts/get-bg-color.ts
@@ -0,0 +1,18 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import tinycolor from 'tinycolor2';
+
+export const getBgColor = (elem?: Element | null | undefined): string | null => {
+	if (elem == null) return null;
+
+	const { backgroundColor: bg } = window.getComputedStyle(elem);
+
+	if (bg && tinycolor(bg).getAlpha() !== 0) {
+		return bg;
+	}
+
+	return getBgColor(elem.parentElement);
+};
diff --git a/packages/frontend/src/scripts/get-dom-node-or-null.ts b/packages/frontend/src/scripts/get-dom-node-or-null.ts
new file mode 100644
index 0000000000..fbf54675fd
--- /dev/null
+++ b/packages/frontend/src/scripts/get-dom-node-or-null.ts
@@ -0,0 +1,19 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+export const getNodeOrNull = (input: unknown): Node | null => {
+	if (input instanceof Node) return input;
+	return null;
+};
+
+export const getElementOrNull = (input: unknown): Element | null => {
+	if (input instanceof Element) return input;
+	return null;
+};
+
+export const getHTMLElementOrNull = (input: unknown): HTMLElement | null => {
+	if (input instanceof HTMLElement) return input;
+	return null;
+};
diff --git a/packages/frontend/src/scripts/get-drive-file-menu.ts b/packages/frontend/src/scripts/get-drive-file-menu.ts
index 7aca5f83b2..c8ab9238d3 100644
--- a/packages/frontend/src/scripts/get-drive-file-menu.ts
+++ b/packages/frontend/src/scripts/get-drive-file-menu.ts
@@ -6,10 +6,10 @@
 import * as Misskey from 'misskey-js';
 import { defineAsyncComponent } from 'vue';
 import { i18n } from '@/i18n.js';
-import copyToClipboard from '@/scripts/copy-to-clipboard.js';
+import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
 import * as os from '@/os.js';
 import { misskeyApi } from '@/scripts/misskey-api.js';
-import { MenuItem } from '@/types/menu.js';
+import type { MenuItem } from '@/types/menu.js';
 import { defaultStore } from '@/store.js';
 
 function rename(file: Misskey.entities.DriveFile) {
@@ -27,7 +27,7 @@ function rename(file: Misskey.entities.DriveFile) {
 }
 
 function describe(file: Misskey.entities.DriveFile) {
-	os.popup(defineAsyncComponent(() => import('@/components/MkFileCaptionEditWindow.vue')), {
+	const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkFileCaptionEditWindow.vue')), {
 		default: file.comment ?? '',
 		file: file,
 	}, {
@@ -37,7 +37,17 @@ function describe(file: Misskey.entities.DriveFile) {
 				comment: caption.length === 0 ? null : caption,
 			});
 		},
-	}, 'closed');
+		closed: () => dispose(),
+	});
+}
+
+function move(file: Misskey.entities.DriveFile) {
+	os.selectDriveFolder(false).then(folder => {
+		misskeyApi('drive/files/update', {
+			fileId: file.id,
+			folderId: folder[0] ? folder[0].id : null,
+		});
+	});
 }
 
 function toggleSensitive(file: Misskey.entities.DriveFile) {
@@ -77,8 +87,10 @@ async function deleteFile(file: Misskey.entities.DriveFile) {
 
 export function getDriveFileMenu(file: Misskey.entities.DriveFile, folder?: Misskey.entities.DriveFolder | null): MenuItem[] {
 	const isImage = file.type.startsWith('image/');
-	let menu;
-	menu = [{
+
+	const menuItems: MenuItem[] = [];
+
+	menuItems.push({
 		type: 'link',
 		to: `/my/drive/file/${file.id}`,
 		text: i18n.ts._fileViewer.title,
@@ -87,6 +99,10 @@ export function getDriveFileMenu(file: Misskey.entities.DriveFile, folder?: Miss
 		text: i18n.ts.rename,
 		icon: 'ti ti-forms',
 		action: () => rename(file),
+	}, {
+		text: i18n.ts.move,
+		icon: 'ti ti-folder-symlink',
+		action: () => move(file),
 	}, {
 		text: file.isSensitive ? i18n.ts.unmarkAsSensitive : i18n.ts.markAsSensitive,
 		icon: file.isSensitive ? 'ti ti-eye' : 'ti ti-eye-exclamation',
@@ -95,14 +111,20 @@ export function getDriveFileMenu(file: Misskey.entities.DriveFile, folder?: Miss
 		text: i18n.ts.describeFile,
 		icon: 'ti ti-text-caption',
 		action: () => describe(file),
-	}, ...isImage ? [{
-		text: i18n.ts.cropImage,
-		icon: 'ti ti-crop',
-		action: () => os.cropImage(file, {
-			aspectRatio: NaN,
-			uploadFolder: folder ? folder.id : folder,
-		}),
-	}] : [], { type: 'divider' }, {
+	});
+
+	if (isImage) {
+		menuItems.push({
+			text: i18n.ts.cropImage,
+			icon: 'ti ti-crop',
+			action: () => os.cropImage(file, {
+				aspectRatio: NaN,
+				uploadFolder: folder ? folder.id : folder,
+			}),
+		});
+	}
+
+	menuItems.push({ type: 'divider' }, {
 		text: i18n.ts.createNoteFromTheFile,
 		icon: 'ti ti-pencil',
 		action: () => os.post({
@@ -124,17 +146,17 @@ export function getDriveFileMenu(file: Misskey.entities.DriveFile, folder?: Miss
 		icon: 'ti ti-trash',
 		danger: true,
 		action: () => deleteFile(file),
-	}];
+	});
 
 	if (defaultStore.state.devMode) {
-		menu = menu.concat([{ type: 'divider' }, {
+		menuItems.push({ type: 'divider' }, {
 			icon: 'ti ti-id',
 			text: i18n.ts.copyFileId,
 			action: () => {
 				copyToClipboard(file.id);
 			},
-		}]);
+		});
 	}
 
-	return menu;
+	return menuItems;
 }
diff --git a/packages/frontend/src/scripts/get-embed-code.ts b/packages/frontend/src/scripts/get-embed-code.ts
new file mode 100644
index 0000000000..158ab9c7f8
--- /dev/null
+++ b/packages/frontend/src/scripts/get-embed-code.ts
@@ -0,0 +1,87 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+import { defineAsyncComponent } from 'vue';
+import { v4 as uuid } from 'uuid';
+import type { EmbedParams, EmbeddableEntity } from '@@/js/embed-page.js';
+import { url } from '@@/js/config.js';
+import * as os from '@/os.js';
+import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
+import { defaultEmbedParams, embedRouteWithScrollbar } from '@@/js/embed-page.js';
+
+const MOBILE_THRESHOLD = 500;
+
+/**
+ * パラメータを正規化する(埋め込みコード作成用)
+ * @param params パラメータ
+ * @returns 正規化されたパラメータ
+ */
+export function normalizeEmbedParams(params: EmbedParams): Record<string, string> {
+	// paramsのvalueをすべてstringに変換。undefinedやnullはプロパティごと消す
+	const normalizedParams: Record<string, string> = {};
+	for (const key in params) {
+		// デフォルトの値と同じならparamsに含めない
+		if (params[key] == null || params[key] === defaultEmbedParams[key]) {
+			continue;
+		}
+		switch (typeof params[key]) {
+			case 'number':
+				normalizedParams[key] = params[key].toString();
+				break;
+			case 'boolean':
+				normalizedParams[key] = params[key] ? 'true' : 'false';
+				break;
+			default:
+				normalizedParams[key] = params[key];
+				break;
+		}
+	}
+	return normalizedParams;
+}
+
+/**
+ * 埋め込みコードを生成(iframe IDの発番もやる)
+ */
+export function getEmbedCode(path: string, params?: EmbedParams): string {
+	const iframeId = 'v1_' + uuid(); // 将来embed.jsのバージョンが上がったとき用にv1_を付けておく
+
+	let paramString = '';
+	if (params) {
+		const searchParams = new URLSearchParams(normalizeEmbedParams(params));
+		paramString = searchParams.toString() === '' ? '' : '?' + searchParams.toString();
+	}
+
+	const iframeCode = [
+		`<iframe src="${url + path + paramString}" data-misskey-embed-id="${iframeId}" loading="lazy" referrerpolicy="strict-origin-when-cross-origin" style="border: none; width: 100%; max-width: 500px; height: 300px; color-scheme: light dark;"></iframe>`,
+		`<script defer src="${url}/embed.js"></script>`,
+	];
+	return iframeCode.join('\n');
+}
+
+/**
+ * 埋め込みコードを生成してコピーする(カスタマイズ機能つき)
+ *
+ * カスタマイズ機能がいらない場合(事前にパラメータを指定する場合)は getEmbedCode を直接使ってください
+ */
+export function genEmbedCode(entity: EmbeddableEntity, id: string, params?: EmbedParams) {
+	const _params = { ...params };
+
+	if (embedRouteWithScrollbar.includes(entity) && _params.maxHeight == null) {
+		_params.maxHeight = 700;
+	}
+
+	// PCじゃない場合はコードカスタマイズ画面を出さずにそのままコピー
+	if (window.innerWidth < MOBILE_THRESHOLD) {
+		copyToClipboard(getEmbedCode(`/embed/${entity}/${id}`, _params));
+		os.success();
+	} else {
+		const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkEmbedCodeGenDialog.vue')), {
+			entity,
+			id,
+			params: _params,
+		}, {
+			closed: () => dispose(),
+		});
+	}
+}
diff --git a/packages/frontend/src/scripts/get-note-menu.ts b/packages/frontend/src/scripts/get-note-menu.ts
index 71ad299f50..c1846b0589 100644
--- a/packages/frontend/src/scripts/get-note-menu.ts
+++ b/packages/frontend/src/scripts/get-note-menu.ts
@@ -11,15 +11,17 @@ import { i18n } from '@/i18n.js';
 import { instance } from '@/instance.js';
 import * as os from '@/os.js';
 import { misskeyApi } from '@/scripts/misskey-api.js';
-import copyToClipboard from '@/scripts/copy-to-clipboard.js';
-import { url } from '@/config.js';
+import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
+import { url } from '@@/js/config.js';
 import { defaultStore, noteActions } from '@/store.js';
 import { miLocalStorage } from '@/local-storage.js';
 import { getUserMenu } from '@/scripts/get-user-menu.js';
 import { clipsCache, favoritedChannelsCache } from '@/cache.js';
-import { MenuItem } from '@/types/menu.js';
+import type { MenuItem } from '@/types/menu.js';
 import MkRippleEffect from '@/components/MkRippleEffect.vue';
 import { isSupportShare } from '@/scripts/navigator.js';
+import { getAppearNote } from '@/scripts/get-appear-note.js';
+import { genEmbedCode } from '@/scripts/get-embed-code.js';
 
 export async function getNoteClipMenu(props: {
 	note: Misskey.entities.Note;
@@ -34,14 +36,7 @@ export async function getNoteClipMenu(props: {
 		}
 	}
 
-	const isRenote = (
-		props.note.renote != null &&
-		props.note.text == null &&
-		props.note.fileIds.length === 0 &&
-		props.note.poll == null
-	);
-
-	const appearNote = isRenote ? props.note.renote as Misskey.entities.Note : props.note;
+	const appearNote = getAppearNote(props.note);
 
 	const clips = await clipsCache.fetch();
 	const menu: MenuItem[] = [...clips.map(clip => ({
@@ -72,6 +67,11 @@ export async function getNoteClipMenu(props: {
 							});
 							if (props.currentClip?.id === clip.id) props.isDeleted.value = true;
 						}
+					} else if (err.id === 'f0dba960-ff73-4615-8df4-d6ac5d9dc118') {
+						os.alert({
+							type: 'error',
+							text: i18n.ts.clipNoteLimitExceeded,
+						});
 					} else {
 						os.alert({
 							type: 'error',
@@ -99,11 +99,13 @@ export async function getNoteClipMenu(props: {
 			const { canceled, result } = await os.form(i18n.ts.createNewClip, {
 				name: {
 					type: 'string',
+					default: null,
 					label: i18n.ts.name,
 				},
 				description: {
 					type: 'string',
 					required: false,
+					default: null,
 					multiline: true,
 					label: i18n.ts.description,
 				},
@@ -136,10 +138,12 @@ export function getAbuseNoteMenu(note: Misskey.entities.Note, text: string): Men
 			let noteInfo = '';
 			if (note.url ?? note.uri != null) noteInfo = `Note: ${note.url ?? note.uri}\n`;
 			noteInfo += `Local Note: ${localUrl}\n`;
-			os.popup(defineAsyncComponent(() => import('@/components/MkAbuseReportWindow.vue')), {
+			const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkAbuseReportWindow.vue')), {
 				user: note.user,
 				initialComment: `${noteInfo}-----\n`,
-			}, {}, 'closed');
+			}, {
+				closed: () => dispose(),
+			});
 		},
 	};
 }
@@ -155,6 +159,19 @@ export function getCopyNoteLinkMenu(note: Misskey.entities.Note, text: string):
 	};
 }
 
+function getNoteEmbedCodeMenu(note: Misskey.entities.Note, text: string): MenuItem | undefined {
+	if (note.url != null || note.uri != null) return undefined;
+	if (['specified', 'followers'].includes(note.visibility)) return undefined;
+
+	return {
+		icon: 'ti ti-code',
+		text,
+		action: (): void => {
+			genEmbedCode('notes', note.id);
+		},
+	};
+}
+
 export function getNoteMenu(props: {
 	note: Misskey.entities.Note;
 	translation: Ref<Misskey.entities.NotesTranslateResponse | null>;
@@ -162,14 +179,7 @@ export function getNoteMenu(props: {
 	isDeleted: Ref<boolean>;
 	currentClip?: Misskey.entities.Clip;
 }) {
-	const isRenote = (
-		props.note.renote != null &&
-		props.note.text == null &&
-		props.note.fileIds.length === 0 &&
-		props.note.poll == null
-	);
-
-	const appearNote = isRenote ? props.note.renote as Misskey.entities.Note : props.note;
+	const appearNote = getAppearNote(props.note);
 
 	const cleanups = [] as (() => void)[];
 
@@ -235,17 +245,15 @@ export function getNoteMenu(props: {
 	function togglePin(pin: boolean): void {
 		os.apiWithDialog(pin ? 'i/pin' : 'i/unpin', {
 			noteId: appearNote.id,
-		}, undefined, null, res => {
-			if (res.id === '72dab508-c64d-498f-8740-a8eec1ba385a') {
-				os.alert({
-					type: 'error',
-					text: i18n.ts.pinLimitExceeded,
-				});
-			}
+		}, undefined, {
+			'72dab508-c64d-498f-8740-a8eec1ba385a': {
+				text: i18n.ts.pinLimitExceeded,
+			},
 		});
 	}
 
 	async function unclip(): Promise<void> {
+		if (!props.currentClip) return;
 		os.apiWithDialog('clips/remove-note', { clipId: props.currentClip.id, noteId: appearNote.id });
 		props.isDeleted.value = true;
 	}
@@ -255,7 +263,7 @@ export function getNoteMenu(props: {
 			title: i18n.ts.numberOfDays,
 		});
 
-		if (canceled) return;
+		if (canceled || days == null) return;
 
 		os.apiWithDialog('admin/promo/create', {
 			noteId: appearNote.id,
@@ -265,8 +273,8 @@ export function getNoteMenu(props: {
 
 	function share(): void {
 		navigator.share({
-			title: i18n.tsx.noteOf({ user: appearNote.user.name }),
-			text: appearNote.text,
+			title: i18n.tsx.noteOf({ user: appearNote.user.name ?? appearNote.user.username }),
+			text: appearNote.text ?? '',
 			url: `${url}/notes/${appearNote.id}`,
 		});
 	}
@@ -286,161 +294,23 @@ export function getNoteMenu(props: {
 		props.translation.value = res;
 	}
 
-	let menu: MenuItem[];
+	const menuItems: MenuItem[] = [];
+
 	if ($i) {
 		const statePromise = misskeyApi('notes/state', {
 			noteId: appearNote.id,
 		});
 
-		menu = [
-			...(
-				props.currentClip?.userId === $i.id ? [{
-					icon: 'ti ti-backspace',
-					text: i18n.ts.unclip,
-					danger: true,
-					action: unclip,
-				}, { type: 'divider' }] : []
-			), {
-				icon: 'ti ti-info-circle',
-				text: i18n.ts.details,
-				action: openDetail,
-			}, {
-				icon: 'ti ti-copy',
-				text: i18n.ts.copyContent,
-				action: copyContent,
-			}, getCopyNoteLinkMenu(appearNote, i18n.ts.copyLink)
-			, (appearNote.url || appearNote.uri) ? {
-				icon: 'ti ti-external-link',
-				text: i18n.ts.showOnRemote,
-				action: () => {
-					window.open(appearNote.url ?? appearNote.uri, '_blank', 'noopener');
-				},
-			} : undefined,
-			...(isSupportShare() ? [{
-				icon: 'ti ti-share',
-				text: i18n.ts.share,
-				action: share,
-			}] : []),
-			$i && $i.policies.canUseTranslator && instance.translatorAvailable ? {
-				icon: 'ti ti-language-hiragana',
-				text: i18n.ts.translate,
-				action: translate,
-			} : undefined,
-			{ type: 'divider' },
-			statePromise.then(state => state.isFavorited ? {
-				icon: 'ti ti-star-off',
-				text: i18n.ts.unfavorite,
-				action: () => toggleFavorite(false),
-			} : {
-				icon: 'ti ti-star',
-				text: i18n.ts.favorite,
-				action: () => toggleFavorite(true),
-			}),
-			{
-				type: 'parent' as const,
-				icon: 'ti ti-paperclip',
-				text: i18n.ts.clip,
-				children: () => getNoteClipMenu(props),
-			},
-			statePromise.then(state => state.isMutedThread ? {
-				icon: 'ti ti-message-off',
-				text: i18n.ts.unmuteThread,
-				action: () => toggleThreadMute(false),
-			} : {
-				icon: 'ti ti-message-off',
-				text: i18n.ts.muteThread,
-				action: () => toggleThreadMute(true),
-			}),
-			appearNote.userId === $i.id ? ($i.pinnedNoteIds ?? []).includes(appearNote.id) ? {
-				icon: 'ti ti-pinned-off',
-				text: i18n.ts.unpin,
-				action: () => togglePin(false),
-			} : {
-				icon: 'ti ti-pin',
-				text: i18n.ts.pin,
-				action: () => togglePin(true),
-			} : undefined,
-			{
-				type: 'parent' as const,
-				icon: 'ti ti-user',
-				text: i18n.ts.user,
-				children: async () => {
-					const user = appearNote.userId === $i?.id ? $i : await misskeyApi('users/show', { userId: appearNote.userId });
-					const { menu, cleanup } = getUserMenu(user);
-					cleanups.push(cleanup);
-					return menu;
-				},
-			},
-			/*
-		...($i.isModerator || $i.isAdmin ? [
-			{ type: 'divider' },
-			{
-				icon: 'ti ti-speakerphone',
-				text: i18n.ts.promote,
-				action: promote
-			}]
-			: []
-		),*/
-			...(appearNote.userId !== $i.id ? [
-				{ type: 'divider' },
-				appearNote.userId !== $i.id ? getAbuseNoteMenu(appearNote, i18n.ts.reportAbuse) : undefined,
-			]
-			: []
-			),
-			...(appearNote.channel && (appearNote.channel.userId === $i.id || $i.isModerator || $i.isAdmin) ? [
-				{ type: 'divider' },
-				{
-					type: 'parent' as const,
-					icon: 'ti ti-device-tv',
-					text: i18n.ts.channel,
-					children: async () => {
-						const channelChildMenu = [] as MenuItem[];
+		if (props.currentClip?.userId === $i.id) {
+			menuItems.push({
+				icon: 'ti ti-backspace',
+				text: i18n.ts.unclip,
+				danger: true,
+				action: unclip,
+			}, { type: 'divider' });
+		}
 
-						const channel = await misskeyApi('channels/show', { channelId: appearNote.channel!.id });
-
-						if (channel.pinnedNoteIds.includes(appearNote.id)) {
-							channelChildMenu.push({
-								icon: 'ti ti-pinned-off',
-								text: i18n.ts.unpin,
-								action: () => os.apiWithDialog('channels/update', {
-									channelId: appearNote.channel!.id,
-									pinnedNoteIds: channel.pinnedNoteIds.filter(id => id !== appearNote.id),
-								}),
-							});
-						} else {
-							channelChildMenu.push({
-								icon: 'ti ti-pin',
-								text: i18n.ts.pin,
-								action: () => os.apiWithDialog('channels/update', {
-									channelId: appearNote.channel!.id,
-									pinnedNoteIds: [...channel.pinnedNoteIds, appearNote.id],
-								}),
-							});
-						}
-						return channelChildMenu;
-					},
-				},
-			]
-			: []
-			),
-			...(appearNote.userId === $i.id || $i.isModerator || $i.isAdmin ? [
-				{ type: 'divider' },
-				appearNote.userId === $i.id ? {
-					icon: 'ti ti-edit',
-					text: i18n.ts.deleteAndEdit,
-					action: delEdit,
-				} : undefined,
-				{
-					icon: 'ti ti-trash',
-					text: i18n.ts.delete,
-					danger: true,
-					action: del,
-				}]
-			: []
-			)]
-			.filter(x => x !== undefined);
-	} else {
-		menu = [{
+		menuItems.push({
 			icon: 'ti ti-info-circle',
 			text: i18n.ts.details,
 			action: openDetail,
@@ -448,35 +318,194 @@ export function getNoteMenu(props: {
 			icon: 'ti ti-copy',
 			text: i18n.ts.copyContent,
 			action: copyContent,
-		}, getCopyNoteLinkMenu(appearNote, i18n.ts.copyLink)
-		, (appearNote.url || appearNote.uri) ? {
-			icon: 'ti ti-external-link',
-			text: i18n.ts.showOnRemote,
-			action: () => {
-				window.open(appearNote.url ?? appearNote.uri, '_blank', 'noopener');
+		}, getCopyNoteLinkMenu(appearNote, i18n.ts.copyLink));
+
+		if (appearNote.url || appearNote.uri) {
+			menuItems.push({
+				icon: 'ti ti-external-link',
+				text: i18n.ts.showOnRemote,
+				action: () => {
+					window.open(appearNote.url ?? appearNote.uri, '_blank', 'noopener');
+				},
+			});
+		} else {
+			menuItems.push(getNoteEmbedCodeMenu(appearNote, i18n.ts.genEmbedCode));
+		}
+
+		if (isSupportShare()) {
+			menuItems.push({
+				icon: 'ti ti-share',
+				text: i18n.ts.share,
+				action: share,
+			});
+		}
+
+		if ($i.policies.canUseTranslator && instance.translatorAvailable) {
+			menuItems.push({
+				icon: 'ti ti-language-hiragana',
+				text: i18n.ts.translate,
+				action: translate,
+			});
+		}
+
+		menuItems.push({ type: 'divider' });
+
+		menuItems.push(statePromise.then(state => state.isFavorited ? {
+			icon: 'ti ti-star-off',
+			text: i18n.ts.unfavorite,
+			action: () => toggleFavorite(false),
+		} : {
+			icon: 'ti ti-star',
+			text: i18n.ts.favorite,
+			action: () => toggleFavorite(true),
+		}));
+
+		menuItems.push({
+			type: 'parent',
+			icon: 'ti ti-paperclip',
+			text: i18n.ts.clip,
+			children: () => getNoteClipMenu(props),
+		});
+
+		menuItems.push(statePromise.then(state => state.isMutedThread ? {
+			icon: 'ti ti-message-off',
+			text: i18n.ts.unmuteThread,
+			action: () => toggleThreadMute(false),
+		} : {
+			icon: 'ti ti-message-off',
+			text: i18n.ts.muteThread,
+			action: () => toggleThreadMute(true),
+		}));
+
+		if (appearNote.userId === $i.id) {
+			if (($i.pinnedNoteIds ?? []).includes(appearNote.id)) {
+				menuItems.push({
+					icon: 'ti ti-pinned-off',
+					text: i18n.ts.unpin,
+					action: () => togglePin(false),
+				});
+			} else {
+				menuItems.push({
+					icon: 'ti ti-pin',
+					text: i18n.ts.pin,
+					action: () => togglePin(true),
+				});
+			}
+		}
+
+		menuItems.push({
+			type: 'parent',
+			icon: 'ti ti-user',
+			text: i18n.ts.user,
+			children: async () => {
+				const user = appearNote.userId === $i?.id ? $i : await misskeyApi('users/show', { userId: appearNote.userId });
+				const { menu, cleanup } = getUserMenu(user);
+				cleanups.push(cleanup);
+				return menu;
 			},
-		} : undefined]
-			.filter(x => x !== undefined);
+		});
+
+		if (appearNote.userId !== $i.id) {
+			menuItems.push({ type: 'divider' });
+			menuItems.push(getAbuseNoteMenu(appearNote, i18n.ts.reportAbuse));
+		}
+
+		if (appearNote.channel && (appearNote.channel.userId === $i.id || $i.isModerator || $i.isAdmin)) {
+			menuItems.push({ type: 'divider' });
+			menuItems.push({
+				type: 'parent',
+				icon: 'ti ti-device-tv',
+				text: i18n.ts.channel,
+				children: async () => {
+					const channelChildMenu = [] as MenuItem[];
+
+					const channel = await misskeyApi('channels/show', { channelId: appearNote.channel!.id });
+
+					if (channel.pinnedNoteIds.includes(appearNote.id)) {
+						channelChildMenu.push({
+							icon: 'ti ti-pinned-off',
+							text: i18n.ts.unpin,
+							action: () => os.apiWithDialog('channels/update', {
+								channelId: appearNote.channel!.id,
+								pinnedNoteIds: channel.pinnedNoteIds.filter(id => id !== appearNote.id),
+							}),
+						});
+					} else {
+						channelChildMenu.push({
+							icon: 'ti ti-pin',
+							text: i18n.ts.pin,
+							action: () => os.apiWithDialog('channels/update', {
+								channelId: appearNote.channel!.id,
+								pinnedNoteIds: [...channel.pinnedNoteIds, appearNote.id],
+							}),
+						});
+					}
+					return channelChildMenu;
+				},
+			});
+		}
+
+		if (appearNote.userId === $i.id || $i.isModerator || $i.isAdmin) {
+			menuItems.push({ type: 'divider' });
+			if (appearNote.userId === $i.id) {
+				menuItems.push({
+					icon: 'ti ti-edit',
+					text: i18n.ts.deleteAndEdit,
+					action: delEdit,
+				});
+			}
+			menuItems.push({
+				icon: 'ti ti-trash',
+				text: i18n.ts.delete,
+				danger: true,
+				action: del,
+			});
+		}
+	} else {
+		menuItems.push({
+			icon: 'ti ti-info-circle',
+			text: i18n.ts.details,
+			action: openDetail,
+		}, {
+			icon: 'ti ti-copy',
+			text: i18n.ts.copyContent,
+			action: copyContent,
+		}, getCopyNoteLinkMenu(appearNote, i18n.ts.copyLink));
+
+		if (appearNote.url || appearNote.uri) {
+			menuItems.push({
+				icon: 'ti ti-external-link',
+				text: i18n.ts.showOnRemote,
+				action: () => {
+					window.open(appearNote.url ?? appearNote.uri, '_blank', 'noopener');
+				},
+			});
+		} else {
+			menuItems.push(getNoteEmbedCodeMenu(appearNote, i18n.ts.genEmbedCode));
+		}
 	}
 
 	if (noteActions.length > 0) {
-		menu = menu.concat([{ type: 'divider' }, ...noteActions.map(action => ({
+		menuItems.push({ type: 'divider' });
+
+		menuItems.push(...noteActions.map(action => ({
 			icon: 'ti ti-plug',
 			text: action.title,
 			action: () => {
 				action.handler(appearNote);
 			},
-		}))]);
+		})));
 	}
 
 	if (defaultStore.state.devMode) {
-		menu = menu.concat([{ type: 'divider' }, {
+		menuItems.push({ type: 'divider' }, {
 			icon: 'ti ti-id',
 			text: i18n.ts.copyNoteId,
 			action: () => {
 				copyToClipboard(appearNote.id);
+				os.success();
 			},
-		}]);
+		});
 	}
 
 	const cleanup = () => {
@@ -487,7 +516,7 @@ export function getNoteMenu(props: {
 	};
 
 	return {
-		menu,
+		menu: menuItems,
 		cleanup,
 	};
 }
@@ -507,14 +536,7 @@ export function getRenoteMenu(props: {
 	renoteButton: ShallowRef<HTMLElement | undefined>;
 	mock?: boolean;
 }) {
-	const isRenote = (
-		props.note.renote != null &&
-		props.note.text == null &&
-		props.note.fileIds.length === 0 &&
-		props.note.poll == null
-	);
-
-	const appearNote = isRenote ? props.note.renote as Misskey.entities.Note : props.note;
+	const appearNote = getAppearNote(props.note);
 
 	const channelRenoteItems: MenuItem[] = [];
 	const normalRenoteItems: MenuItem[] = [];
@@ -530,7 +552,9 @@ export function getRenoteMenu(props: {
 					const rect = el.getBoundingClientRect();
 					const x = rect.left + (el.offsetWidth / 2);
 					const y = rect.top + (el.offsetHeight / 2);
-					os.popup(MkRippleEffect, { x, y }, {}, 'end');
+					const { dispose } = os.popup(MkRippleEffect, { x, y }, {
+						end: () => dispose(),
+					});
 				}
 
 				if (!props.mock) {
@@ -566,7 +590,9 @@ export function getRenoteMenu(props: {
 					const rect = el.getBoundingClientRect();
 					const x = rect.left + (el.offsetWidth / 2);
 					const y = rect.top + (el.offsetHeight / 2);
-					os.popup(MkRippleEffect, { x, y }, {}, 'end');
+					const { dispose } = os.popup(MkRippleEffect, { x, y }, {
+						end: () => dispose(),
+					});
 				}
 
 				const configuredVisibility = defaultStore.state.rememberNoteVisibility ? defaultStore.state.visibility : defaultStore.state.defaultNoteVisibility;
@@ -615,7 +641,9 @@ export function getRenoteMenu(props: {
 							const rect = el.getBoundingClientRect();
 							const x = rect.left + (el.offsetWidth / 2);
 							const y = rect.top + (el.offsetHeight / 2);
-							os.popup(MkRippleEffect, { x, y }, {}, 'end');
+							const { dispose } = os.popup(MkRippleEffect, { x, y }, {
+								end: () => dispose(),
+							});
 						}
 
 						if (!props.mock) {
diff --git a/packages/frontend/src/scripts/get-user-menu.ts b/packages/frontend/src/scripts/get-user-menu.ts
index 3e031d232f..d15279d633 100644
--- a/packages/frontend/src/scripts/get-user-menu.ts
+++ b/packages/frontend/src/scripts/get-user-menu.ts
@@ -7,15 +7,18 @@ import { toUnicode } from 'punycode';
 import { defineAsyncComponent, ref, watch } from 'vue';
 import * as Misskey from 'misskey-js';
 import { i18n } from '@/i18n.js';
-import copyToClipboard from '@/scripts/copy-to-clipboard.js';
-import { host, url } from '@/config.js';
+import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
+import { host, url } from '@@/js/config.js';
 import * as os from '@/os.js';
 import { misskeyApi } from '@/scripts/misskey-api.js';
 import { defaultStore, userActions } from '@/store.js';
 import { $i, iAmModerator } from '@/account.js';
+import { notesSearchAvailable, canSearchNonLocalNotes } from '@/scripts/check-permissions.js';
 import { IRouter } from '@/nirax.js';
 import { antennasCache, rolesCache, userListsCache } from '@/cache.js';
 import { mainRouter } from '@/router/main.js';
+import { genEmbedCode } from '@/scripts/get-embed-code.js';
+import type { MenuItem } from '@/types/menu.js';
 
 export function getUserMenu(user: Misskey.entities.UserDetailed, router: IRouter = mainRouter) {
 	const meId = $i ? $i.id : null;
@@ -81,15 +84,6 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: IRouter
 		});
 	}
 
-	async function toggleWithReplies() {
-		os.apiWithDialog('following/update', {
-			userId: user.id,
-			withReplies: !user.withReplies,
-		}).then(() => {
-			user.withReplies = !user.withReplies;
-		});
-	}
-
 	async function toggleNotify() {
 		os.apiWithDialog('following/update', {
 			userId: user.id,
@@ -100,9 +94,11 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: IRouter
 	}
 
 	function reportAbuse() {
-		os.popup(defineAsyncComponent(() => import('@/components/MkAbuseReportWindow.vue')), {
+		const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkAbuseReportWindow.vue')), {
 			user: user,
-		}, {}, 'closed');
+		}, {
+			closed: () => dispose(),
+		});
 	}
 
 	async function getConfirmed(text: string): Promise<boolean> {
@@ -152,116 +148,154 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: IRouter
 		});
 	}
 
-	let menu = [{
+	const menuItems: MenuItem[] = [];
+
+	menuItems.push({
 		icon: 'ti ti-at',
 		text: i18n.ts.copyUsername,
 		action: () => {
 			copyToClipboard(`@${user.username}@${user.host ?? host}`);
 		},
-	}, ...(iAmModerator ? [{
-		icon: 'ti ti-user-exclamation',
-		text: i18n.ts.moderation,
-		action: () => {
-			router.push(`/admin/user/${user.id}`);
-		},
-	}] : []), {
+	});
+
+	if (notesSearchAvailable && (user.host == null || canSearchNonLocalNotes)) {
+		menuItems.push({
+			icon: 'ti ti-search',
+			text: i18n.ts.searchThisUsersNotes,
+			action: () => {
+				router.push(`/search?username=${encodeURIComponent(user.username)}${user.host != null ? '&host=' + encodeURIComponent(user.host) : ''}`);
+			},
+		});
+	}
+
+	if (iAmModerator) {
+		menuItems.push({
+			icon: 'ti ti-user-exclamation',
+			text: i18n.ts.moderation,
+			action: () => {
+				router.push(`/admin/user/${user.id}`);
+			},
+		});
+	}
+
+	menuItems.push({
 		icon: 'ti ti-rss',
 		text: i18n.ts.copyRSS,
 		action: () => {
 			copyToClipboard(`${user.host ?? host}/@${user.username}.atom`);
 		},
-	}, ...(user.host != null && user.url != null ? [{
-		icon: 'ti ti-external-link',
-		text: i18n.ts.showOnRemote,
-		action: () => {
-			if (user.url == null) return;
-			window.open(user.url, '_blank', 'noopener');
-		},
-	}] : []), {
+	});
+
+	if (user.host != null && user.url != null) {
+		menuItems.push({
+			icon: 'ti ti-external-link',
+			text: i18n.ts.showOnRemote,
+			action: () => {
+				if (user.url == null) return;
+				window.open(user.url, '_blank', 'noopener');
+			},
+		});
+	} else {
+		menuItems.push({
+			icon: 'ti ti-code',
+			text: i18n.ts.genEmbedCode,
+			type: 'parent',
+			children: [{
+				text: i18n.ts.noteOfThisUser,
+				action: () => {
+					genEmbedCode('user-timeline', user.id);
+				},
+			}], // TODO: ユーザーカードの埋め込みなど
+		});
+	}
+
+	menuItems.push({
 		icon: 'ti ti-share',
 		text: i18n.ts.copyProfileUrl,
 		action: () => {
 			const canonical = user.host === null ? `@${user.username}` : `@${user.username}@${toUnicode(user.host)}`;
 			copyToClipboard(`${url}/${canonical}`);
 		},
-	}, {
-		icon: 'ti ti-mail',
-		text: i18n.ts.sendMessage,
-		action: () => {
-			const canonical = user.host === null ? `@${user.username}` : `@${user.username}@${user.host}`;
-			os.post({ specified: user, initialText: `${canonical} ` });
-		},
-	}, { type: 'divider' }, {
-		icon: 'ti ti-pencil',
-		text: i18n.ts.editMemo,
-		action: () => {
-			editMemo();
-		},
-	}, {
-		type: 'parent',
-		icon: 'ti ti-list',
-		text: i18n.ts.addToList,
-		children: async () => {
-			const lists = await userListsCache.fetch();
-			return lists.map(list => {
-				const isListed = ref(list.userIds.includes(user.id));
-				cleanups.push(watch(isListed, () => {
-					if (isListed.value) {
-						os.apiWithDialog('users/lists/push', {
-							listId: list.id,
-							userId: user.id,
-						}).then(() => {
-							list.userIds.push(user.id);
-						});
-					} else {
-						os.apiWithDialog('users/lists/pull', {
-							listId: list.id,
-							userId: user.id,
-						}).then(() => {
-							list.userIds.splice(list.userIds.indexOf(user.id), 1);
-						});
-					}
-				}));
+	});
 
-				return {
-					type: 'switch',
-					text: list.name,
-					ref: isListed,
-				};
-			});
-		},
-	}, {
-		type: 'parent',
-		icon: 'ti ti-antenna',
-		text: i18n.ts.addToAntenna,
-		children: async () => {
-			const antennas = await antennasCache.fetch();
-			const canonical = user.host === null ? `@${user.username}` : `@${user.username}@${toUnicode(user.host)}`;
-			return antennas.filter((a) => a.src === 'users').map(antenna => ({
-				text: antenna.name,
-				action: async () => {
-					await os.apiWithDialog('antennas/update', {
-						antennaId: antenna.id,
-						name: antenna.name,
-						keywords: antenna.keywords,
-						excludeKeywords: antenna.excludeKeywords,
-						src: antenna.src,
-						userListId: antenna.userListId,
-						users: [...antenna.users, canonical],
-						caseSensitive: antenna.caseSensitive,
-						withReplies: antenna.withReplies,
-						withFile: antenna.withFile,
-						notify: antenna.notify,
-					});
-					antennasCache.delete();
-				},
-			}));
-		},
-	}] as any;
+	if ($i) {
+		menuItems.push({
+			icon: 'ti ti-mail',
+			text: i18n.ts.sendMessage,
+			action: () => {
+				const canonical = user.host === null ? `@${user.username}` : `@${user.username}@${user.host}`;
+				os.post({ specified: user, initialText: `${canonical} ` });
+			},
+		}, { type: 'divider' }, {
+			icon: 'ti ti-pencil',
+			text: i18n.ts.editMemo,
+			action: editMemo,
+		}, {
+			type: 'parent',
+			icon: 'ti ti-list',
+			text: i18n.ts.addToList,
+			children: async () => {
+				const lists = await userListsCache.fetch();
+				return lists.map(list => {
+					const isListed = ref(list.userIds?.includes(user.id) ?? false);
+					cleanups.push(watch(isListed, () => {
+						if (isListed.value) {
+							os.apiWithDialog('users/lists/push', {
+								listId: list.id,
+								userId: user.id,
+							}).then(() => {
+								list.userIds?.push(user.id);
+							});
+						} else {
+							os.apiWithDialog('users/lists/pull', {
+								listId: list.id,
+								userId: user.id,
+							}).then(() => {
+								list.userIds?.splice(list.userIds?.indexOf(user.id), 1);
+							});
+						}
+					}));
+
+					return {
+						type: 'switch',
+						text: list.name,
+						ref: isListed,
+					};
+				});
+			},
+		}, {
+			type: 'parent',
+			icon: 'ti ti-antenna',
+			text: i18n.ts.addToAntenna,
+			children: async () => {
+				const antennas = await antennasCache.fetch();
+				const canonical = user.host === null ? `@${user.username}` : `@${user.username}@${toUnicode(user.host)}`;
+				return antennas.filter((a) => a.src === 'users').map(antenna => ({
+					text: antenna.name,
+					action: async () => {
+						await os.apiWithDialog('antennas/update', {
+							antennaId: antenna.id,
+							name: antenna.name,
+							keywords: antenna.keywords,
+							excludeKeywords: antenna.excludeKeywords,
+							src: antenna.src,
+							userListId: antenna.userListId,
+							users: [...antenna.users, canonical],
+							caseSensitive: antenna.caseSensitive,
+							withReplies: antenna.withReplies,
+							withFile: antenna.withFile,
+							notify: antenna.notify,
+						});
+						antennasCache.delete();
+					},
+				}));
+			},
+		});
+	}
 
 	if ($i && meId !== user.id) {
 		if (iAmModerator) {
-			menu = menu.concat([{
+			menuItems.push({
 				type: 'parent',
 				icon: 'ti ti-badges',
 				text: i18n.ts.roles,
@@ -299,23 +333,35 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: IRouter
 						},
 					}));
 				},
-			}]);
+			});
 		}
 
 		// フォローしたとしても user.isFollowing はリアルタイム更新されないので不便なため
 		//if (user.isFollowing) {
-		menu = menu.concat([{
-			icon: user.withReplies ? 'ti ti-messages-off' : 'ti ti-messages',
-			text: user.withReplies ? i18n.ts.hideRepliesToOthersInTimeline : i18n.ts.showRepliesToOthersInTimeline,
-			action: toggleWithReplies,
+		const withRepliesRef = ref(user.withReplies ?? false);
+
+		menuItems.push({
+			type: 'switch',
+			icon: 'ti ti-messages',
+			text: i18n.ts.showRepliesToOthersInTimeline,
+			ref: withRepliesRef,
 		}, {
 			icon: user.notify === 'none' ? 'ti ti-bell' : 'ti ti-bell-off',
 			text: user.notify === 'none' ? i18n.ts.notifyNotes : i18n.ts.unnotifyNotes,
 			action: toggleNotify,
-		}]);
+		});
+
+		watch(withRepliesRef, (withReplies) => {
+			misskeyApi('following/update', {
+				userId: user.id,
+				withReplies,
+			}).then(() => {
+				user.withReplies = withReplies;
+			});
+		});
 		//}
 
-		menu = menu.concat([{ type: 'divider' }, {
+		menuItems.push({ type: 'divider' }, {
 			icon: user.isMuted ? 'ti ti-eye' : 'ti ti-eye-off',
 			text: user.isMuted ? i18n.ts.unmute : i18n.ts.mute,
 			action: toggleMute,
@@ -327,70 +373,68 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: IRouter
 			icon: 'ti ti-ban',
 			text: user.isBlocking ? i18n.ts.unblock : i18n.ts.block,
 			action: toggleBlock,
-		}]);
+		});
 
 		if (user.isFollowed) {
-			menu = menu.concat([{
+			menuItems.push({
 				icon: 'ti ti-link-off',
 				text: i18n.ts.breakFollow,
 				action: invalidateFollow,
-			}]);
+			});
 		}
 
-		menu = menu.concat([{ type: 'divider' }, {
+		menuItems.push({ type: 'divider' }, {
 			icon: 'ti ti-exclamation-circle',
 			text: i18n.ts.reportAbuse,
 			action: reportAbuse,
-		}]);
+		});
 	}
 
 	if (user.host !== null) {
-		menu = menu.concat([{ type: 'divider' }, {
+		menuItems.push({ type: 'divider' }, {
 			icon: 'ti ti-refresh',
 			text: i18n.ts.updateRemoteUser,
 			action: userInfoUpdate,
-		}]);
+		});
 	}
 
 	if (defaultStore.state.devMode) {
-		menu = menu.concat([{ type: 'divider' }, {
+		menuItems.push({ type: 'divider' }, {
 			icon: 'ti ti-id',
 			text: i18n.ts.copyUserId,
 			action: () => {
 				copyToClipboard(user.id);
 			},
-		}]);
+		});
 	}
 
 	if ($i && meId === user.id) {
-		menu = menu.concat([{ type: 'divider' }, {
+		menuItems.push({ type: 'divider' }, {
 			icon: 'ti ti-pencil',
 			text: i18n.ts.editProfile,
 			action: () => {
 				router.push('/settings/profile');
 			},
-		}]);
+		});
 	}
 
 	if (userActions.length > 0) {
-		menu = menu.concat([{ type: 'divider' }, ...userActions.map(action => ({
+		menuItems.push({ type: 'divider' }, ...userActions.map(action => ({
 			icon: 'ti ti-plug',
 			text: action.title,
 			action: () => {
 				action.handler(user);
 			},
-		}))]);
+		})));
 	}
 
-	const cleanup = () => {
-		if (_DEV_) console.log('user menu cleanup', cleanups);
-		for (const cl of cleanups) {
-			cl();
-		}
-	};
-
 	return {
-		menu,
-		cleanup,
+		menu: menuItems,
+		cleanup: () => {
+			if (_DEV_) console.log('user menu cleanup', cleanups);
+			for (const cl of cleanups) {
+				cl();
+			}
+		},
 	};
 }
diff --git a/packages/frontend/src/scripts/hotkey.ts b/packages/frontend/src/scripts/hotkey.ts
index 0600bff893..04fb235694 100644
--- a/packages/frontend/src/scripts/hotkey.ts
+++ b/packages/frontend/src/scripts/hotkey.ts
@@ -2,94 +2,171 @@
  * SPDX-FileCopyrightText: syuilo and misskey-project
  * SPDX-License-Identifier: AGPL-3.0-only
  */
+import { getHTMLElementOrNull } from "@/scripts/get-dom-node-or-null.js";
 
-import keyCode from './keycode.js';
+//#region types
+export type Keymap = Record<string, CallbackFunction | CallbackObject>;
 
-type Callback = (ev: KeyboardEvent) => void;
+type CallbackFunction = (ev: KeyboardEvent) => unknown;
 
-type Keymap = Record<string, Callback>;
+type CallbackObject = {
+	callback: CallbackFunction;
+	allowRepeat?: boolean;
+};
 
 type Pattern = {
 	which: string[];
-	ctrl?: boolean;
-	shift?: boolean;
-	alt?: boolean;
+	ctrl: boolean;
+	alt: boolean;
+	shift: boolean;
 };
 
 type Action = {
 	patterns: Pattern[];
-	callback: Callback;
-	allowRepeat: boolean;
+	callback: CallbackFunction;
+	options: Required<Omit<CallbackObject, 'callback'>>;
+};
+//#endregion
+
+//#region consts
+const KEY_ALIASES = {
+	'esc': 'Escape',
+	'enter': 'Enter',
+	'space': ' ',
+	'up': 'ArrowUp',
+	'down': 'ArrowDown',
+	'left': 'ArrowLeft',
+	'right': 'ArrowRight',
+	'plus': ['+', ';'],
 };
 
-const parseKeymap = (keymap: Keymap) => Object.entries(keymap).map(([patterns, callback]): Action => {
-	const result = {
-		patterns: [],
-		callback,
-		allowRepeat: true,
-	} as Action;
+const MODIFIER_KEYS = ['ctrl', 'alt', 'shift'];
 
-	if (patterns.match(/^\(.*\)$/) !== null) {
-		result.allowRepeat = false;
-		patterns = patterns.slice(1, -1);
-	}
+const IGNORE_ELEMENTS = ['input', 'textarea'];
+//#endregion
 
-	result.patterns = patterns.split('|').map(part => {
-		const pattern = {
-			which: [],
-			ctrl: false,
-			alt: false,
-			shift: false,
-		} as Pattern;
-
-		const keys = part.trim().split('+').map(x => x.trim().toLowerCase());
-		for (const key of keys) {
-			switch (key) {
-				case 'ctrl': pattern.ctrl = true; break;
-				case 'alt': pattern.alt = true; break;
-				case 'shift': pattern.shift = true; break;
-				default: pattern.which = keyCode(key).map(k => k.toLowerCase());
-			}
-		}
-
-		return pattern;
-	});
-
-	return result;
-});
-
-const ignoreElements = ['input', 'textarea'];
-
-function match(ev: KeyboardEvent, patterns: Action['patterns']): boolean {
-	const key = ev.key.toLowerCase();
-	return patterns.some(pattern => pattern.which.includes(key) &&
-		pattern.ctrl === ev.ctrlKey &&
-		pattern.shift === ev.shiftKey &&
-		pattern.alt === ev.altKey &&
-		!ev.metaKey,
-	);
-}
+//#region store
+let latestHotkey: Pattern & { callback: CallbackFunction } | null = null;
+//#endregion
 
+//#region impl
 export const makeHotkey = (keymap: Keymap) => {
 	const actions = parseKeymap(keymap);
-
 	return (ev: KeyboardEvent) => {
-		if (document.activeElement) {
-			if (ignoreElements.some(el => document.activeElement!.matches(el))) return;
-			if (document.activeElement.attributes['contenteditable']) return;
+		if ('pswp' in window && window.pswp != null) return;
+		if (document.activeElement != null) {
+			if (IGNORE_ELEMENTS.includes(document.activeElement.tagName.toLowerCase())) return;
+			if (getHTMLElementOrNull(document.activeElement)?.isContentEditable) return;
 		}
-
 		for (const action of actions) {
-			const matched = match(ev, action.patterns);
-
-			if (matched) {
-				if (!action.allowRepeat && ev.repeat) return;
-
+			if (matchPatterns(ev, action)) {
 				ev.preventDefault();
 				ev.stopPropagation();
 				action.callback(ev);
-				break;
+				storePattern(ev, action.callback);
 			}
 		}
 	};
 };
+
+const parseKeymap = (keymap: Keymap) => {
+	return Object.entries(keymap).map(([rawPatterns, rawCallback]) => {
+		const patterns = parsePatterns(rawPatterns);
+		const callback = parseCallback(rawCallback);
+		const options = parseOptions(rawCallback);
+		return { patterns, callback, options } as const satisfies Action;
+	});
+};
+
+const parsePatterns = (rawPatterns: keyof Keymap) => {
+	return rawPatterns.split('|').map(part => {
+		const keys = part.split('+').map(trimLower);
+		const which = parseKeyCode(keys.findLast(x => !MODIFIER_KEYS.includes(x)));
+		const ctrl = keys.includes('ctrl');
+		const alt = keys.includes('alt');
+		const shift = keys.includes('shift');
+		return { which, ctrl, alt, shift } as const satisfies Pattern;
+	});
+};
+
+const parseCallback = (rawCallback: Keymap[keyof Keymap]) => {
+	if (typeof rawCallback === 'object') {
+		return rawCallback.callback;
+	}
+	return rawCallback;
+};
+
+const parseOptions = (rawCallback: Keymap[keyof Keymap]) => {
+	const defaultOptions = {
+		allowRepeat: false,
+	} as const satisfies Action['options'];
+	if (typeof rawCallback === 'object') {
+		const { callback, ...rawOptions } = rawCallback;
+		const options = { ...defaultOptions, ...rawOptions };
+		return { ...options } as const satisfies Action['options'];
+	}
+	return { ...defaultOptions } as const satisfies Action['options'];
+};
+
+const matchPatterns = (ev: KeyboardEvent, action: Action) => {
+	const { patterns, options, callback } = action;
+	if (ev.repeat && !options.allowRepeat) return false;
+	const key = ev.key.toLowerCase();
+	return patterns.some(({ which, ctrl, shift, alt }) => {
+		if (
+			options.allowRepeat === false &&
+			latestHotkey != null &&
+			latestHotkey.which.includes(key) &&
+			latestHotkey.ctrl === ctrl &&
+			latestHotkey.alt === alt &&
+			latestHotkey.shift === shift &&
+			latestHotkey.callback === callback
+		) {
+			return false;
+		}
+		if (!which.includes(key)) return false;
+		if (ctrl !== (ev.ctrlKey || ev.metaKey)) return false;
+		if (alt !== ev.altKey) return false;
+		if (shift !== ev.shiftKey) return false;
+		return true;
+	});
+};
+
+let lastHotKeyStoreTimer: number | null = null;
+
+const storePattern = (ev: KeyboardEvent, callback: CallbackFunction) => {
+	if (lastHotKeyStoreTimer != null) {
+		clearTimeout(lastHotKeyStoreTimer);
+	}
+
+	latestHotkey = {
+		which: [ev.key.toLowerCase()],
+		ctrl: ev.ctrlKey || ev.metaKey,
+		alt: ev.altKey,
+		shift: ev.shiftKey,
+		callback,
+	};
+
+	lastHotKeyStoreTimer = window.setTimeout(() => {
+		latestHotkey = null;
+	}, 500);
+};
+
+const parseKeyCode = (input?: string | null) => {
+	if (input == null) return [];
+	const raw = getValueByKey(KEY_ALIASES, input);
+	if (raw == null) return [input];
+	if (typeof raw === 'string') return [trimLower(raw)];
+	return raw.map(trimLower);
+};
+
+const getValueByKey = <
+	T extends Record<keyof any, unknown>,
+	K extends keyof T | keyof any,
+	R extends K extends keyof T ? T[K] : T[keyof T] | undefined,
+>(obj: T, key: K) => {
+	return obj[key] as R;
+};
+
+const trimLower = (str: string) => str.trim().toLowerCase();
+//#endregion
diff --git a/packages/frontend/src/scripts/idb-proxy.ts b/packages/frontend/src/scripts/idb-proxy.ts
index 6b511f2a5f..20f51660c7 100644
--- a/packages/frontend/src/scripts/idb-proxy.ts
+++ b/packages/frontend/src/scripts/idb-proxy.ts
@@ -10,10 +10,11 @@ import {
 	set as iset,
 	del as idel,
 } from 'idb-keyval';
+import { miLocalStorage } from '@/local-storage.js';
 
-const fallbackName = (key: string) => `idbfallback::${key}`;
+const PREFIX = 'idbfallback::';
 
-let idbAvailable = typeof window !== 'undefined' ? !!(window.indexedDB && window.indexedDB.open) : true;
+let idbAvailable = typeof window !== 'undefined' ? !!(window.indexedDB && typeof window.indexedDB.open === 'function') : true;
 
 // iframe.contentWindow.indexedDB.deleteDatabase() がchromeのバグで使用できないため、indexedDBを無効化している。
 // バグが治って再度有効化するのであれば、cypressのコマンド内のコメントアウトを外すこと
@@ -38,15 +39,15 @@ if (idbAvailable) {
 
 export async function get(key: string) {
 	if (idbAvailable) return iget(key);
-	return JSON.parse(window.localStorage.getItem(fallbackName(key)));
+	return miLocalStorage.getItemAsJson(`${PREFIX}${key}`);
 }
 
 export async function set(key: string, val: any) {
 	if (idbAvailable) return iset(key, val);
-	return window.localStorage.setItem(fallbackName(key), JSON.stringify(val));
+	return miLocalStorage.setItemAsJson(`${PREFIX}${key}`, val);
 }
 
 export async function del(key: string) {
 	if (idbAvailable) return idel(key);
-	return window.localStorage.removeItem(fallbackName(key));
+	return miLocalStorage.removeItem(`${PREFIX}${key}`);
 }
diff --git a/packages/frontend/src/scripts/init-chart.ts b/packages/frontend/src/scripts/init-chart.ts
index 2465a14703..41e1636aa7 100644
--- a/packages/frontend/src/scripts/init-chart.ts
+++ b/packages/frontend/src/scripts/init-chart.ts
@@ -50,7 +50,7 @@ export function initChart() {
 	);
 
 	// フォントカラー
-	Chart.defaults.color = getComputedStyle(document.documentElement).getPropertyValue('--fg');
+	Chart.defaults.color = getComputedStyle(document.documentElement).getPropertyValue('--MI_THEME-fg');
 
 	Chart.defaults.borderColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)';
 
diff --git a/packages/frontend/src/scripts/initialize-sw.ts b/packages/frontend/src/scripts/initialize-sw.ts
index 1517e4e1e8..867ebf19ed 100644
--- a/packages/frontend/src/scripts/initialize-sw.ts
+++ b/packages/frontend/src/scripts/initialize-sw.ts
@@ -3,7 +3,7 @@
  * SPDX-License-Identifier: AGPL-3.0-only
  */
 
-import { lang } from '@/config.js';
+import { lang } from '@@/js/config.js';
 
 export async function initializeSw() {
 	if (!('serviceWorker' in navigator)) return;
diff --git a/packages/frontend/src/scripts/install-plugin.ts b/packages/frontend/src/scripts/install-plugin.ts
index d0a8675b19..37f473b6de 100644
--- a/packages/frontend/src/scripts/install-plugin.ts
+++ b/packages/frontend/src/scripts/install-plugin.ts
@@ -103,7 +103,7 @@ export async function installPlugin(code: string, meta?: AiScriptPluginMeta) {
 	}
 
 	const token = realMeta.permissions == null || realMeta.permissions.length === 0 ? null : await new Promise((res, rej) => {
-		os.popup(defineAsyncComponent(() => import('@/components/MkTokenGenerateWindow.vue')), {
+		const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkTokenGenerateWindow.vue')), {
 			title: i18n.ts.tokenRequested,
 			information: i18n.ts.pluginTokenRequestedDescription,
 			initialName: realMeta.name,
@@ -118,7 +118,8 @@ export async function installPlugin(code: string, meta?: AiScriptPluginMeta) {
 				});
 				res(token);
 			},
-		}, 'closed');
+			closed: () => dispose(),
+		});
 	});
 
 	savePlugin({
diff --git a/packages/frontend/src/scripts/intl-const.ts b/packages/frontend/src/scripts/intl-const.ts
index aaa4f0a86e..385f59ec39 100644
--- a/packages/frontend/src/scripts/intl-const.ts
+++ b/packages/frontend/src/scripts/intl-const.ts
@@ -3,7 +3,7 @@
  * SPDX-License-Identifier: AGPL-3.0-only
  */
 
-import { lang } from '@/config.js';
+import { lang } from '@@/js/config.js';
 
 export const versatileLang = (lang ?? 'ja-JP').replace('ja-KS', 'ja-JP');
 
diff --git a/packages/frontend/src/scripts/isFfVisibleForMe.ts b/packages/frontend/src/scripts/isFfVisibleForMe.ts
index 406404c462..e28e5725bc 100644
--- a/packages/frontend/src/scripts/isFfVisibleForMe.ts
+++ b/packages/frontend/src/scripts/isFfVisibleForMe.ts
@@ -7,7 +7,7 @@ import * as Misskey from 'misskey-js';
 import { $i } from '@/account.js';
 
 export function isFollowingVisibleForMe(user: Misskey.entities.UserDetailed): boolean {
-	if ($i && $i.id === user.id) return true;
+	if ($i && ($i.id === user.id || $i.isAdmin || $i.isModerator)) return true;
 
 	if (user.followingVisibility === 'private') return false;
 	if (user.followingVisibility === 'followers' && !user.isFollowing) return false;
@@ -15,7 +15,7 @@ export function isFollowingVisibleForMe(user: Misskey.entities.UserDetailed): bo
 	return true;
 }
 export function isFollowersVisibleForMe(user: Misskey.entities.UserDetailed): boolean {
-	if ($i && $i.id === user.id) return true;
+	if ($i && ($i.id === user.id || $i.isAdmin || $i.isModerator)) return true;
 
 	if (user.followersVisibility === 'private') return false;
 	if (user.followersVisibility === 'followers' && !user.isFollowing) return false;
diff --git a/packages/frontend/src/scripts/keycode.ts b/packages/frontend/src/scripts/keycode.ts
deleted file mode 100644
index 7ffceafada..0000000000
--- a/packages/frontend/src/scripts/keycode.ts
+++ /dev/null
@@ -1,24 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and misskey-project
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-export default (input: string): string[] => {
-	if (Object.keys(aliases).some(a => a.toLowerCase() === input.toLowerCase())) {
-		const codes = aliases[input];
-		return Array.isArray(codes) ? codes : [codes];
-	} else {
-		return [input];
-	}
-};
-
-export const aliases = {
-	'esc': 'Escape',
-	'enter': ['Enter', 'NumpadEnter'],
-	'space': [' ', 'Spacebar'],
-	'up': 'ArrowUp',
-	'down': 'ArrowDown',
-	'left': 'ArrowLeft',
-	'right': 'ArrowRight',
-	'plus': ['NumpadAdd', 'Semicolon'],
-};
diff --git a/packages/frontend/src/scripts/lookup.ts b/packages/frontend/src/scripts/lookup.ts
index 7f020b15cc..a261ec0669 100644
--- a/packages/frontend/src/scripts/lookup.ts
+++ b/packages/frontend/src/scripts/lookup.ts
@@ -16,7 +16,7 @@ export async function lookup(router?: Router) {
 		title: i18n.ts.lookup,
 	});
 	const query = temp ? temp.trim() : '';
-	if (canceled) return;
+	if (canceled || query.length <= 1) return;
 
 	if (query.startsWith('@') && !query.includes(' ')) {
 		_router.push(`/${query}`);
diff --git a/packages/frontend/src/scripts/media-proxy.ts b/packages/frontend/src/scripts/media-proxy.ts
index 099a22163a..78eba35ead 100644
--- a/packages/frontend/src/scripts/media-proxy.ts
+++ b/packages/frontend/src/scripts/media-proxy.ts
@@ -3,51 +3,32 @@
  * SPDX-License-Identifier: AGPL-3.0-only
  */
 
-import { query } from '@/scripts/url.js';
-import { url } from '@/config.js';
+import { MediaProxy } from '@@/js/media-proxy.js';
+import { url } from '@@/js/config.js';
 import { instance } from '@/instance.js';
 
-export function getProxiedImageUrl(imageUrl: string, type?: 'preview' | 'emoji' | 'avatar', mustOrigin = false, noFallback = false): string {
-	const localProxy = `${url}/proxy`;
+let _mediaProxy: MediaProxy | null = null;
 
-	if (imageUrl.startsWith(instance.mediaProxy + '/') || imageUrl.startsWith('/proxy/') || imageUrl.startsWith(localProxy + '/')) {
-		// もう既にproxyっぽそうだったらurlを取り出す
-		imageUrl = (new URL(imageUrl)).searchParams.get('url') ?? imageUrl;
+export function getProxiedImageUrl(...args: Parameters<MediaProxy['getProxiedImageUrl']>): string {
+	if (_mediaProxy == null) {
+		_mediaProxy = new MediaProxy(instance, url);
 	}
 
-	return `${mustOrigin ? localProxy : instance.mediaProxy}/${
-		type === 'preview' ? 'preview.webp'
-		: 'image.webp'
-	}?${query({
-		url: imageUrl,
-		...(!noFallback ? { 'fallback': '1' } : {}),
-		...(type ? { [type]: '1' } : {}),
-		...(mustOrigin ? { origin: '1' } : {}),
-	})}`;
+	return _mediaProxy.getProxiedImageUrl(...args);
 }
 
-export function getProxiedImageUrlNullable(imageUrl: string | null | undefined, type?: 'preview'): string | null {
-	if (imageUrl == null) return null;
-	return getProxiedImageUrl(imageUrl, type);
-}
-
-export function getStaticImageUrl(baseUrl: string): string {
-	const u = baseUrl.startsWith('http') ? new URL(baseUrl) : new URL(baseUrl, url);
-
-	if (u.href.startsWith(`${url}/emoji/`)) {
-		// もう既にemojiっぽそうだったらsearchParams付けるだけ
-		u.searchParams.set('static', '1');
-		return u.href;
+export function getProxiedImageUrlNullable(...args: Parameters<MediaProxy['getProxiedImageUrlNullable']>): string | null {
+	if (_mediaProxy == null) {
+		_mediaProxy = new MediaProxy(instance, url);
 	}
 
-	if (u.href.startsWith(instance.mediaProxy + '/')) {
-		// もう既にproxyっぽそうだったらsearchParams付けるだけ
-		u.searchParams.set('static', '1');
-		return u.href;
+	return _mediaProxy.getProxiedImageUrlNullable(...args);
+}
+
+export function getStaticImageUrl(...args: Parameters<MediaProxy['getStaticImageUrl']>): string {
+	if (_mediaProxy == null) {
+		_mediaProxy = new MediaProxy(instance, url);
 	}
 
-	return `${instance.mediaProxy}/static.webp?${query({
-		url: u.href,
-		static: '1',
-	})}`;
+	return _mediaProxy.getStaticImageUrl(...args);
 }
diff --git a/packages/frontend/src/scripts/merge.ts b/packages/frontend/src/scripts/merge.ts
index 4e39a0fa06..9794a300da 100644
--- a/packages/frontend/src/scripts/merge.ts
+++ b/packages/frontend/src/scripts/merge.ts
@@ -6,7 +6,7 @@
 import { deepClone } from './clone.js';
 import type { Cloneable } from './clone.js';
 
-type DeepPartial<T> = {
+export type DeepPartial<T> = {
 	[P in keyof T]?: T[P] extends Record<string | number | symbol, unknown> ? DeepPartial<T[P]> : T[P];
 };
 
diff --git a/packages/frontend/src/scripts/mfm-function-picker.ts b/packages/frontend/src/scripts/mfm-function-picker.ts
index 8867a8c50f..bf59fe98a0 100644
--- a/packages/frontend/src/scripts/mfm-function-picker.ts
+++ b/packages/frontend/src/scripts/mfm-function-picker.ts
@@ -6,30 +6,25 @@
 import { Ref, nextTick } from 'vue';
 import * as os from '@/os.js';
 import { i18n } from '@/i18n.js';
-import { MFM_TAGS } from '@/const.js';
+import { MFM_TAGS } from '@@/js/const.js';
+import type { MenuItem } from '@/types/menu.js';
 
 /**
  * MFMの装飾のリストを表示する
  */
-export function mfmFunctionPicker(src: any, textArea: HTMLInputElement | HTMLTextAreaElement, textRef: Ref<string>) {
-	return new Promise((res, rej) => {
-		os.popupMenu([{
-			text: i18n.ts.addMfmFunction,
-			type: 'label',
-		}, ...getFunctionList(textArea, textRef)], src);
-	});
+export function mfmFunctionPicker(src: HTMLElement | EventTarget | null, textArea: HTMLInputElement | HTMLTextAreaElement, textRef: Ref<string>) {
+	os.popupMenu([{
+		text: i18n.ts.addMfmFunction,
+		type: 'label',
+	}, ...getFunctionList(textArea, textRef)], src);
 }
 
-function getFunctionList(textArea: HTMLInputElement | HTMLTextAreaElement, textRef: Ref<string>) : object[] {
-	const ret: object[] = [];
-	MFM_TAGS.forEach(tag => {
-		ret.push({
-			text: tag,
-			icon: 'ti ti-icons',
-			action: () => add(textArea, textRef, tag),
-		});
-	});
-	return ret;
+function getFunctionList(textArea: HTMLInputElement | HTMLTextAreaElement, textRef: Ref<string>): MenuItem[] {
+	return MFM_TAGS.map(tag => ({
+		text: tag,
+		icon: 'ti ti-icons',
+		action: () => add(textArea, textRef, tag),
+	}));
 }
 
 function add(textArea: HTMLInputElement | HTMLTextAreaElement, textRef: Ref<string>, type: string) {
diff --git a/packages/frontend/src/scripts/misskey-api.ts b/packages/frontend/src/scripts/misskey-api.ts
index 49fb6f9e59..e7a92e2d5c 100644
--- a/packages/frontend/src/scripts/misskey-api.ts
+++ b/packages/frontend/src/scripts/misskey-api.ts
@@ -5,7 +5,7 @@
 
 import * as Misskey from 'misskey-js';
 import { ref } from 'vue';
-import { apiUrl } from '@/config.js';
+import { apiUrl } from '@@/js/config.js';
 import { $i } from '@/account.js';
 export const pendingApiRequestsCount = ref(0);
 
@@ -17,7 +17,7 @@ export function misskeyApi<
 	_ResT = ResT extends void ? Misskey.api.SwitchCaseResponseType<E, P> : ResT,
 >(
 	endpoint: E,
-	data: P = {} as any,
+	data: P & { i?: string | null; } = {} as any,
 	token?: string | null | undefined,
 	signal?: AbortSignal,
 ): Promise<_ResT> {
@@ -30,8 +30,8 @@ export function misskeyApi<
 
 	const promise = new Promise<_ResT>((resolve, reject) => {
 		// Append a credential
-		if ($i) (data as any).i = $i.token;
-		if (token !== undefined) (data as any).i = token;
+		if ($i) data.i = $i.token;
+		if (token !== undefined) data.i = token;
 
 		// Send request
 		window.fetch(`${apiUrl}/${endpoint}`, {
diff --git a/packages/frontend/src/scripts/player-url-transform.ts b/packages/frontend/src/scripts/player-url-transform.ts
new file mode 100644
index 0000000000..39c6df6500
--- /dev/null
+++ b/packages/frontend/src/scripts/player-url-transform.ts
@@ -0,0 +1,26 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+import { hostname } from '@@/js/config.js';
+
+export function transformPlayerUrl(url: string): string {
+	const urlObj = new URL(url);
+	if (!['https:', 'http:'].includes(urlObj.protocol)) throw new Error('Invalid protocol');
+
+	const urlParams = new URLSearchParams(urlObj.search);
+
+	if (urlObj.hostname === 'player.twitch.tv') {
+		// TwitchはCSPの制約あり
+		// https://dev.twitch.tv/docs/embed/video-and-clips/
+		urlParams.set('parent', hostname);
+		urlParams.set('allowfullscreen', '');
+		urlParams.set('autoplay', 'true');
+	} else {
+		urlParams.set('autoplay', '1');
+		urlParams.set('auto_play', '1');
+	}
+	urlObj.search = urlParams.toString();
+
+	return urlObj.toString();
+}
diff --git a/packages/frontend/src/scripts/please-login.ts b/packages/frontend/src/scripts/please-login.ts
index 9e51272791..43dcf11936 100644
--- a/packages/frontend/src/scripts/please-login.ts
+++ b/packages/frontend/src/scripts/please-login.ts
@@ -8,19 +8,61 @@ import { $i } from '@/account.js';
 import { i18n } from '@/i18n.js';
 import { popup } from '@/os.js';
 
-export function pleaseLogin(path?: string) {
+export type OpenOnRemoteOptions = {
+	/**
+	 * 外部のMisskey Webで特定のパスを開く
+	 */
+	type: 'web';
+
+	/**
+	 * 内部パス(例: `/settings`)
+	 */
+	path: string;
+} | {
+	/**
+	 * 外部のMisskey Webで照会する
+	 */
+	type: 'lookup';
+
+	/**
+	 * 照会したいエンティティのURL
+	 *
+	 * (例: `https://misskey.example.com/notes/abcdexxxxyz`)
+	 */
+	url: string;
+} | {
+	/**
+	 * 外部のMisskeyでノートする
+	 */
+	type: 'share';
+
+	/**
+	 * `/share` ページに渡すクエリストリング
+	 *
+	 * @see https://go.misskey-hub.net/spec/share/
+	 */
+	params: Record<string, string>;
+};
+
+export function pleaseLogin(opts: {
+	path?: string;
+	message?: string;
+	openOnRemote?: OpenOnRemoteOptions;
+} = {}) {
 	if ($i) return;
 
-	popup(defineAsyncComponent(() => import('@/components/MkSigninDialog.vue')), {
+	const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkSigninDialog.vue')), {
 		autoSet: true,
-		message: i18n.ts.signinRequired,
+		message: opts.message ?? (opts.openOnRemote ? i18n.ts.signinOrContinueOnRemote : i18n.ts.signinRequired),
+		openOnRemote: opts.openOnRemote,
 	}, {
 		cancelled: () => {
-			if (path) {
-				window.location.href = path;
+			if (opts.path) {
+				window.location.href = opts.path;
 			}
 		},
-	}, 'closed');
+		closed: () => dispose(),
+	});
 
 	throw new Error('signin required');
 }
diff --git a/packages/frontend/src/scripts/popout.ts b/packages/frontend/src/scripts/popout.ts
index 1caa2dfc21..5b141222e8 100644
--- a/packages/frontend/src/scripts/popout.ts
+++ b/packages/frontend/src/scripts/popout.ts
@@ -3,8 +3,8 @@
  * SPDX-License-Identifier: AGPL-3.0-only
  */
 
-import { appendQuery } from './url.js';
-import * as config from '@/config.js';
+import { appendQuery } from '@@/js/url.js';
+import * as config from '@@/js/config.js';
 
 export function popout(path: string, w?: HTMLElement) {
 	let url = path.startsWith('http://') || path.startsWith('https://') ? path : config.url + path;
diff --git a/packages/frontend/src/scripts/post-message.ts b/packages/frontend/src/scripts/post-message.ts
index 31a9ac1ad9..11b6f52ddd 100644
--- a/packages/frontend/src/scripts/post-message.ts
+++ b/packages/frontend/src/scripts/post-message.ts
@@ -18,7 +18,7 @@ export type MiPostMessageEvent = {
  * 親フレームにイベントを送信
  */
 export function postMessageToParentWindow(type: PostMessageEventType, payload?: any): void {
-	window.postMessage({
+	window.parent.postMessage({
 		type,
 		payload,
 	}, '*');
diff --git a/packages/frontend/src/scripts/reload-ask.ts b/packages/frontend/src/scripts/reload-ask.ts
new file mode 100644
index 0000000000..733d91b85a
--- /dev/null
+++ b/packages/frontend/src/scripts/reload-ask.ts
@@ -0,0 +1,40 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { i18n } from '@/i18n.js';
+import * as os from '@/os.js';
+import { unisonReload } from '@/scripts/unison-reload.js';
+
+let isReloadConfirming = false;
+
+export async function reloadAsk(opts: {
+	unison?: boolean;
+	reason?: string;
+}) {
+	if (isReloadConfirming) {
+		return;
+	}
+
+	isReloadConfirming = true;
+
+	const { canceled } = await os.confirm(opts.reason == null ? {
+		type: 'info',
+		text: i18n.ts.reloadConfirm,
+	} : {
+		type: 'info',
+		title: i18n.ts.reloadConfirm,
+		text: opts.reason,
+	}).finally(() => {
+		isReloadConfirming = false;
+	});
+
+	if (canceled) return;
+
+	if (opts.unison) {
+		unisonReload();
+	} else {
+		location.reload();
+	}
+}
diff --git a/packages/frontend/src/scripts/safe-parse.ts b/packages/frontend/src/scripts/safe-parse.ts
deleted file mode 100644
index 6bfcef6c36..0000000000
--- a/packages/frontend/src/scripts/safe-parse.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and misskey-project
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-export function safeParseFloat(str: unknown): number | null {
-	if (typeof str !== 'string' || str === '') return null;
-	const num = parseFloat(str);
-	if (isNaN(num)) return null;
-	return num;
-}
diff --git a/packages/frontend/src/scripts/safe-uri-decode.ts b/packages/frontend/src/scripts/safe-uri-decode.ts
deleted file mode 100644
index 0edf4e9eba..0000000000
--- a/packages/frontend/src/scripts/safe-uri-decode.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and misskey-project
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-export function safeURIDecode(str: string): string {
-	try {
-		return decodeURIComponent(str);
-	} catch {
-		return str;
-	}
-}
diff --git a/packages/frontend/src/scripts/select-file.ts b/packages/frontend/src/scripts/select-file.ts
index 9aa38178b2..b037aa8acc 100644
--- a/packages/frontend/src/scripts/select-file.ts
+++ b/packages/frontend/src/scripts/select-file.ts
@@ -80,7 +80,7 @@ export function chooseFileFromUrl(): Promise<Misskey.entities.DriveFile> {
 	});
 }
 
-function select(src: any, label: string | null, multiple: boolean): Promise<Misskey.entities.DriveFile[]> {
+function select(src: HTMLElement | EventTarget | null, label: string | null, multiple: boolean): Promise<Misskey.entities.DriveFile[]> {
 	return new Promise((res, rej) => {
 		const keepOriginal = ref(defaultStore.state.keepOriginalUploading);
 
@@ -107,10 +107,10 @@ function select(src: any, label: string | null, multiple: boolean): Promise<Miss
 	});
 }
 
-export function selectFile(src: any, label: string | null = null): Promise<Misskey.entities.DriveFile> {
+export function selectFile(src: HTMLElement | EventTarget | null, label: string | null = null): Promise<Misskey.entities.DriveFile> {
 	return select(src, label, false).then(files => files[0]);
 }
 
-export function selectFiles(src: any, label: string | null = null): Promise<Misskey.entities.DriveFile[]> {
+export function selectFiles(src: HTMLElement | EventTarget | null, label: string | null = null): Promise<Misskey.entities.DriveFile[]> {
 	return select(src, label, true);
 }
diff --git a/packages/frontend/src/scripts/shuffle.ts b/packages/frontend/src/scripts/shuffle.ts
index fed16bc71c..1f6ef1928c 100644
--- a/packages/frontend/src/scripts/shuffle.ts
+++ b/packages/frontend/src/scripts/shuffle.ts
@@ -6,8 +6,9 @@
 /**
  * 配列をシャッフル (破壊的)
  */
-export function shuffle<T extends any[]>(array: T): T {
-	let currentIndex = array.length, randomIndex;
+export function shuffle<T extends unknown[]>(array: T): T {
+	let currentIndex = array.length;
+	let randomIndex: number;
 
 	// While there remain elements to shuffle.
 	while (currentIndex !== 0) {
diff --git a/packages/frontend/src/scripts/sound.ts b/packages/frontend/src/scripts/sound.ts
index fcd59510df..05f82fce7d 100644
--- a/packages/frontend/src/scripts/sound.ts
+++ b/packages/frontend/src/scripts/sound.ts
@@ -74,8 +74,6 @@ export const soundsTypes = [
 export const operationTypes = [
 	'noteMy',
 	'note',
-	'antenna',
-	'channel',
 	'notification',
 	'reaction',
 ] as const;
@@ -126,14 +124,16 @@ export async function loadAudio(url: string, options?: { useCache?: boolean; })
  */
 export function playMisskeySfx(operationType: OperationType) {
 	const sound = defaultStore.state[`sound_${operationType}`];
-	if (sound.type == null || !canPlay || ('userActivation' in navigator && !navigator.userActivation.hasBeenActive)) return;
-
-	canPlay = false;
-	playMisskeySfxFile(sound).finally(() => {
-		// ごく短時間に音が重複しないように
-		setTimeout(() => {
-			canPlay = true;
-		}, 25);
+	playMisskeySfxFile(sound).then((succeed) => {
+		if (!succeed && sound.type === '_driveFile_') {
+			// ドライブファイルが存在しない場合はデフォルトのサウンドを再生する
+			const soundName = defaultStore.def[`sound_${operationType}`].default.type as Exclude<SoundType, '_driveFile_'>;
+			if (_DEV_) console.log(`Failed to play sound: ${sound.fileUrl}, so play default sound: ${soundName}`);
+			playMisskeySfxFileInternal({
+				type: soundName,
+				volume: sound.volume,
+			});
+		}
 	});
 }
 
@@ -141,19 +141,39 @@ export function playMisskeySfx(operationType: OperationType) {
  * サウンド設定形式で指定された音声を再生する
  * @param soundStore サウンド設定
  */
-export async function playMisskeySfxFile(soundStore: SoundStore) {
+export async function playMisskeySfxFile(soundStore: SoundStore): Promise<boolean> {
+	// 連続して再生しない
+	if (!canPlay) return false;
+	// ユーザーアクティベーションが必要な場合はそれがない場合は再生しない
+	if ('userActivation' in navigator && !navigator.userActivation.hasBeenActive) return false;
+	// サウンドがない場合は再生しない
+	if (soundStore.type === null || soundStore.type === '_driveFile_' && !soundStore.fileUrl) return false;
+
+	canPlay = false;
+	return await playMisskeySfxFileInternal(soundStore).finally(() => {
+		// ごく短時間に音が重複しないように
+		setTimeout(() => {
+			canPlay = true;
+		}, 25);
+	});
+}
+
+async function playMisskeySfxFileInternal(soundStore: SoundStore): Promise<boolean> {
 	if (soundStore.type === null || (soundStore.type === '_driveFile_' && !soundStore.fileUrl)) {
-		return;
+		return false;
 	}
 	const masterVolume = defaultStore.state.sound_masterVolume;
 	if (isMute() || masterVolume === 0 || soundStore.volume === 0) {
-		return;
+		return true; // ミュート時は成功として扱う
 	}
 	const url = soundStore.type === '_driveFile_' ? soundStore.fileUrl : `/client-assets/sounds/${soundStore.type}.mp3`;
-	const buffer = await loadAudio(url);
-	if (!buffer) return;
+	const buffer = await loadAudio(url).catch(() => {
+		return undefined;
+	});
+	if (!buffer) return false;
 	const volume = soundStore.volume * masterVolume;
 	createSourceNode(buffer, { volume }).soundSource.start();
+	return true;
 }
 
 export async function playUrl(url: string, opts: {
diff --git a/packages/frontend/src/scripts/stream-mock.ts b/packages/frontend/src/scripts/stream-mock.ts
new file mode 100644
index 0000000000..cb0e607fcb
--- /dev/null
+++ b/packages/frontend/src/scripts/stream-mock.ts
@@ -0,0 +1,81 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { EventEmitter } from 'eventemitter3';
+import * as Misskey from 'misskey-js';
+import type { Channels, StreamEvents, IStream, IChannelConnection } from 'misskey-js';
+
+type AnyOf<T extends Record<any, any>> = T[keyof T];
+type OmitFirst<T extends any[]> = T extends [any, ...infer R] ? R : never;
+
+/**
+ * Websocket無効化時に使うStreamのモック(なにもしない)
+ */
+export class StreamMock extends EventEmitter<StreamEvents> implements IStream {
+	public readonly state = 'initializing';
+
+	constructor(...args: ConstructorParameters<typeof Misskey.Stream>) {
+		super();
+		// do nothing
+	}
+
+	public useChannel<C extends keyof Channels>(channel: C, params?: Channels[C]['params'], name?: string): ChannelConnectionMock<Channels[C]> {
+		return new ChannelConnectionMock(this, channel, name);
+	}
+
+	public removeSharedConnection(connection: any): void {
+		// do nothing
+	}
+
+	public removeSharedConnectionPool(pool: any): void {
+		// do nothing
+	}
+
+	public disconnectToChannel(): void {
+		// do nothing
+	}
+
+	public send(typeOrPayload: string): void
+	public send(typeOrPayload: string, payload: any): void
+	public send(typeOrPayload: Record<string, any> | any[]): void
+	public send(typeOrPayload: string | Record<string, any> | any[], payload?: any): void {
+		// do nothing
+	}
+
+	public ping(): void {
+		// do nothing
+	}
+
+	public heartbeat(): void {
+		// do nothing
+	}
+
+	public close(): void {
+		// do nothing
+	}
+}
+
+class ChannelConnectionMock<Channel extends AnyOf<Channels> = any> extends EventEmitter<Channel['events']> implements IChannelConnection<Channel> {
+	public id = '';
+	public name?: string; // for debug
+	public inCount = 0; // for debug
+	public outCount = 0; // for debug
+	public channel: string;
+
+	constructor(stream: IStream, ...args: OmitFirst<ConstructorParameters<typeof Misskey.ChannelConnection<Channel>>>) {
+		super();
+
+		this.channel = args[0];
+		this.name = args[1];
+	}
+
+	public send<T extends keyof Channel['receives']>(type: T, body: Channel['receives'][T]): void {
+		// do nothing
+	}
+
+	public dispose(): void {
+		// do nothing
+	}
+}
diff --git a/packages/frontend/src/scripts/theme.ts b/packages/frontend/src/scripts/theme.ts
index c7f8b3d596..1a3909c132 100644
--- a/packages/frontend/src/scripts/theme.ts
+++ b/packages/frontend/src/scripts/theme.ts
@@ -5,11 +5,11 @@
 
 import { ref } from 'vue';
 import tinycolor from 'tinycolor2';
+import lightTheme from '@@/themes/_light.json5';
+import darkTheme from '@@/themes/_dark.json5';
 import { deepClone } from './clone.js';
 import type { BundledTheme } from 'shiki/themes';
 import { globalEvents } from '@/events.js';
-import lightTheme from '@/themes/_light.json5';
-import darkTheme from '@/themes/_dark.json5';
 import { miLocalStorage } from '@/local-storage.js';
 
 export type Theme = {
@@ -52,7 +52,7 @@ export const getBuiltinThemes = () => Promise.all(
 		'd-cherry',
 		'd-ice',
 		'd-u0',
-	].map(name => import(`@/themes/${name}.json5`).then(({ default: _default }): Theme => _default)),
+	].map(name => import(`@@/themes/${name}.json5`).then(({ default: _default }): Theme => _default)),
 );
 
 export const getBuiltinThemesRef = () => {
@@ -74,6 +74,8 @@ export function applyTheme(theme: Theme, persist = true) {
 
 	const colorScheme = theme.base === 'dark' ? 'dark' : 'light';
 
+	document.documentElement.dataset.colorScheme = colorScheme;
+
 	// Deep copy
 	const _theme = deepClone(theme);
 
@@ -92,7 +94,7 @@ export function applyTheme(theme: Theme, persist = true) {
 	}
 
 	for (const [k, v] of Object.entries(props)) {
-		document.documentElement.style.setProperty(`--${k}`, v.toString());
+		document.documentElement.style.setProperty(`--MI_THEME-${k}`, v.toString());
 	}
 
 	document.documentElement.style.setProperty('color-scheme', colorScheme);
diff --git a/packages/frontend/src/scripts/upload.ts b/packages/frontend/src/scripts/upload.ts
index 3e947183c9..713573a377 100644
--- a/packages/frontend/src/scripts/upload.ts
+++ b/packages/frontend/src/scripts/upload.ts
@@ -9,10 +9,11 @@ import { v4 as uuid } from 'uuid';
 import { readAndCompressImage } from '@misskey-dev/browser-image-resizer';
 import { getCompressionConfig } from './upload/compress-config.js';
 import { defaultStore } from '@/store.js';
-import { apiUrl } from '@/config.js';
+import { apiUrl } from '@@/js/config.js';
 import { $i } from '@/account.js';
 import { alert } from '@/os.js';
 import { i18n } from '@/i18n.js';
+import { instance } from '@/instance.js';
 
 type Uploading = {
 	id: string;
@@ -31,13 +32,22 @@ const mimeTypeMap = {
 
 export function uploadFile(
 	file: File,
-	folder?: any,
+	folder?: string | Misskey.entities.DriveFolder,
 	name?: string,
 	keepOriginal: boolean = defaultStore.state.keepOriginalUploading,
 ): Promise<Misskey.entities.DriveFile> {
 	if ($i == null) throw new Error('Not logged in');
 
-	if (folder && typeof folder === 'object') folder = folder.id;
+	const _folder = typeof folder === 'string' ? folder : folder?.id;
+
+	if (file.size > instance.maxFileSize) {
+		alert({
+			type: 'error',
+			title: i18n.ts.failedToUpload,
+			text: i18n.ts.cannotUploadBecauseExceedsFileSizeLimit,
+		});
+		return Promise.reject();
+	}
 
 	return new Promise((resolve, reject) => {
 		const id = uuid();
@@ -79,11 +89,11 @@ export function uploadFile(
 			}
 
 			const formData = new FormData();
-			formData.append('i', $i.token);
+			formData.append('i', $i!.token);
 			formData.append('force', 'true');
 			formData.append('file', resizedImage ?? file);
 			formData.append('name', ctx.name);
-			if (folder) formData.append('folderId', folder);
+			if (_folder) formData.append('folderId', _folder);
 
 			const xhr = new XMLHttpRequest();
 			xhr.open('POST', apiUrl + '/drive/files/create', true);
diff --git a/packages/frontend/src/scripts/use-chart-tooltip.ts b/packages/frontend/src/scripts/use-chart-tooltip.ts
index bed221a622..bba64fc6ee 100644
--- a/packages/frontend/src/scripts/use-chart-tooltip.ts
+++ b/packages/frontend/src/scripts/use-chart-tooltip.ts
@@ -17,20 +17,16 @@ export function useChartTooltip(opts: { position: 'top' | 'middle' } = { positio
 		borderColor: string;
 		text: string;
 	}[] | null>(null);
-	let disposeTooltipComponent;
-
-	os.popup(MkChartTooltip, {
+	const { dispose: disposeTooltipComponent } = os.popup(MkChartTooltip, {
 		showing: tooltipShowing,
 		x: tooltipX,
 		y: tooltipY,
 		title: tooltipTitle,
 		series: tooltipSeries,
-	}, {}).then(({ dispose }) => {
-		disposeTooltipComponent = dispose;
-	});
+	}, {});
 
 	onUnmounted(() => {
-		if (disposeTooltipComponent) disposeTooltipComponent();
+		disposeTooltipComponent();
 	});
 
 	onDeactivated(() => {
diff --git a/packages/frontend/src/scripts/use-form.ts b/packages/frontend/src/scripts/use-form.ts
new file mode 100644
index 0000000000..0d505fe466
--- /dev/null
+++ b/packages/frontend/src/scripts/use-form.ts
@@ -0,0 +1,55 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { computed, Reactive, reactive, watch } from 'vue';
+
+function copy<T>(v: T): T {
+	return JSON.parse(JSON.stringify(v));
+}
+
+function unwrapReactive<T>(v: Reactive<T>): T {
+	return JSON.parse(JSON.stringify(v));
+}
+
+export function useForm<T extends Record<string, any>>(initialState: T, save: (newState: T) => Promise<void>) {
+	const currentState = reactive<T>(copy(initialState));
+	const previousState = reactive<T>(copy(initialState));
+
+	const modifiedStates = reactive<Record<keyof T, boolean>>({} as any);
+	for (const key in currentState) {
+		modifiedStates[key] = false;
+	}
+	const modified = computed(() => Object.values(modifiedStates).some(v => v));
+	const modifiedCount = computed(() => Object.values(modifiedStates).filter(v => v).length);
+
+	watch([currentState, previousState], () => {
+		for (const key in modifiedStates) {
+			modifiedStates[key] = currentState[key] !== previousState[key];
+		}
+	}, { deep: true });
+
+	async function _save() {
+		await save(unwrapReactive(currentState));
+		for (const key in currentState) {
+			previousState[key] = copy(currentState[key]);
+		}
+	}
+
+	function discard() {
+		for (const key in currentState) {
+			currentState[key] = copy(previousState[key]);
+		}
+	}
+
+	return {
+		state: currentState,
+		savedState: previousState,
+		modifiedStates,
+		modified,
+		modifiedCount,
+		save: _save,
+		discard,
+	};
+}
diff --git a/packages/frontend/src/store.ts b/packages/frontend/src/store.ts
index e8eb5a1ed7..911a463636 100644
--- a/packages/frontend/src/store.ts
+++ b/packages/frontend/src/store.ts
@@ -5,10 +5,13 @@
 
 import { markRaw, ref } from 'vue';
 import * as Misskey from 'misskey-js';
+import { hemisphere } from '@@/js/intl-const.js';
+import lightTheme from '@@/themes/l-light.json5';
+import darkTheme from '@@/themes/d-green-lime.json5';
 import { miLocalStorage } from './local-storage.js';
 import type { SoundType } from '@/scripts/sound.js';
 import { Storage } from '@/pizzax.js';
-import { hemisphere } from '@/scripts/intl-const.js';
+import type { Ast } from '@syuilo/aiscript';
 
 interface PostFormAction {
 	title: string,
@@ -76,6 +79,10 @@ export const defaultStore = markRaw(new Storage('base', {
 			global: false,
 		},
 	},
+	abusesTutorial: {
+		where: 'account',
+		default: false,
+	},
 	keepCw: {
 		where: 'account',
 		default: true,
@@ -220,7 +227,7 @@ export const defaultStore = markRaw(new Storage('base', {
 	},
 	animatedMfm: {
 		where: 'device',
-		default: false,
+		default: !window.matchMedia('(prefers-reduced-motion)').matches,
 	},
 	advancedMfm: {
 		where: 'device',
@@ -250,9 +257,9 @@ export const defaultStore = markRaw(new Storage('base', {
 		where: 'device',
 		default: 'twemoji', // twemoji / fluentEmoji / native
 	},
-	disableDrawer: {
+	menuStyle: {
 		where: 'device',
-		default: false,
+		default: 'auto' as 'auto' | 'popup' | 'drawer',
 	},
 	useBlurEffectForModal: {
 		where: 'device',
@@ -302,9 +309,9 @@ export const defaultStore = markRaw(new Storage('base', {
 		where: 'device',
 		default: 2,
 	},
-	emojiPickerUseDrawerForMobile: {
+	emojiPickerStyle: {
 		where: 'device',
-		default: true,
+		default: 'auto' as 'auto' | 'popup' | 'drawer',
 	},
 	recentlyUsedEmojis: {
 		where: 'device',
@@ -390,9 +397,9 @@ export const defaultStore = markRaw(new Storage('base', {
 		where: 'device',
 		default: 'horizontal' as 'vertical' | 'horizontal',
 	},
-	enableCondensedLineForAcct: {
+	enableCondensedLine: {
 		where: 'device',
-		default: false,
+		default: true,
 	},
 	additionalUnicodeEmojiIndexes: {
 		where: 'device',
@@ -454,6 +461,18 @@ export const defaultStore = markRaw(new Storage('base', {
 		where: 'device',
 		default: true,
 	},
+	confirmWhenRevealingSensitiveMedia: {
+		where: 'device',
+		default: false,
+	},
+	contextMenu: {
+		where: 'device',
+		default: 'app' as 'app' | 'appWithShift' | 'native',
+	},
+	skipNoteRender: {
+		where: 'device',
+		default: true,
+	},
 
 	sound_masterVolume: {
 		where: 'device',
@@ -479,14 +498,6 @@ export const defaultStore = markRaw(new Storage('base', {
 		where: 'device',
 		default: { type: 'syuilo/n-ea', volume: 1 } as SoundStore,
 	},
-	sound_antenna: {
-		where: 'device',
-		default: { type: 'syuilo/triple', volume: 1 } as SoundStore,
-	},
-	sound_channel: {
-		where: 'device',
-		default: { type: 'syuilo/square-pico', volume: 1 } as SoundStore,
-	},
 	sound_reaction: {
 		where: 'device',
 		default: { type: 'syuilo/bubble2', volume: 1 } as SoundStore,
@@ -506,7 +517,7 @@ export type Plugin = {
 	token: string;
 	src: string | null;
 	version: string;
-	ast: any[];
+	ast: Ast.Node[];
 	author?: string;
 	description?: string;
 	permissions?: string[];
@@ -520,8 +531,6 @@ interface Watcher {
 /**
  * 常にメモリにロードしておく必要がないような設定情報を保管するストレージ(非リアクティブ)
  */
-import lightTheme from '@/themes/l-light.json5';
-import darkTheme from '@/themes/d-green-lime.json5';
 
 export class ColdDeviceStorage {
 	public static default = {
@@ -546,19 +555,19 @@ export class ColdDeviceStorage {
 	}
 
 	public static getAll(): Partial<typeof this.default> {
-		return (Object.keys(this.default) as (keyof typeof this.default)[]).reduce((acc, key) => {
+		return (Object.keys(this.default) as (keyof typeof this.default)[]).reduce<Partial<typeof this.default>>((acc, key) => {
 			const value = localStorage.getItem(PREFIX + key);
 			if (value != null) {
 				acc[key] = JSON.parse(value);
 			}
 			return acc;
-		}, {} as any);
+		}, {});
 	}
 
 	public static set<T extends keyof typeof ColdDeviceStorage.default>(key: T, value: typeof ColdDeviceStorage.default[T]): void {
 		// 呼び出し側のバグ等で undefined が来ることがある
 		// undefined を文字列として miLocalStorage に入れると参照する際の JSON.parse でコケて不具合の元になるため無視
-		// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
+
 		if (value === undefined) {
 			console.error(`attempt to store undefined value for key '${key}'`);
 			return;
@@ -597,7 +606,7 @@ export class ColdDeviceStorage {
 			get: () => {
 				return valueRef.value;
 			},
-			set: (value: unknown) => {
+			set: (value: typeof ColdDeviceStorage.default[K]) => {
 				const val = value;
 				ColdDeviceStorage.set(key, val);
 			},
diff --git a/packages/frontend/src/stream.ts b/packages/frontend/src/stream.ts
index 0d5bd78b09..e63dac951c 100644
--- a/packages/frontend/src/stream.ts
+++ b/packages/frontend/src/stream.ts
@@ -6,18 +6,21 @@
 import * as Misskey from 'misskey-js';
 import { markRaw } from 'vue';
 import { $i } from '@/account.js';
-import { wsOrigin } from '@/config.js';
+import { wsOrigin } from '@@/js/config.js';
+// TODO: No WebsocketモードでStreamMockが使えそう
+//import { StreamMock } from '@/scripts/stream-mock.js';
 
 // heart beat interval in ms
 const HEART_BEAT_INTERVAL = 1000 * 60;
 
-let stream: Misskey.Stream | null = null;
-let timeoutHeartBeat: ReturnType<typeof setTimeout> | null = null;
+let stream: Misskey.IStream | null = null;
+let timeoutHeartBeat: number | null = null;
 let lastHeartbeatCall = 0;
 
-export function useStream(): Misskey.Stream {
+export function useStream(): Misskey.IStream {
 	if (stream) return stream;
 
+	// TODO: No Websocketモードもここで判定
 	stream = markRaw(new Misskey.Stream(wsOrigin, $i ? {
 		token: $i.token,
 	} : null));
diff --git a/packages/frontend/src/style.scss b/packages/frontend/src/style.scss
index 250a2616a7..4204c5af59 100644
--- a/packages/frontend/src/style.scss
+++ b/packages/frontend/src/style.scss
@@ -7,39 +7,30 @@
  */
 
 :root {
-	--radius: 12px;
-	--marginFull: 16px;
-	--marginHalf: 10px;
+	--MI-radius: 12px;
+	--MI-marginFull: 16px;
+	--MI-marginHalf: 10px;
 
-	--margin: var(--marginFull);
+	--MI-margin: var(--MI-marginFull);
 
 	// switch dynamically
-	--minBottomSpacingMobile: calc(72px + max(12px, env(safe-area-inset-bottom, 0px)));
-	--minBottomSpacing: var(--minBottomSpacingMobile);
+	--MI-minBottomSpacingMobile: calc(72px + max(12px, env(safe-area-inset-bottom, 0px)));
+	--MI-minBottomSpacing: var(--MI-minBottomSpacingMobile);
 
 	@media (max-width: 500px) {
-		--margin: var(--marginHalf);
+		--MI-margin: var(--MI-marginHalf);
 	}
-
-	//--ad: rgb(255 169 0 / 10%);
-	--eventFollow: #36aed2;
-	--eventRenote: #36d298;
-	--eventReply: #007aff;
-	--eventReactionHeart: #dd2e44;
-	--eventReaction: #e99a0b;
-	--eventAchievement: #cb9a11;
-	--eventOther: #88a6b7;
 }
 
 ::selection {
-	color: var(--fgOnAccent);
-	background-color: var(--accent);
+	color: var(--MI_THEME-fgOnAccent);
+	background-color: var(--MI_THEME-accent);
 }
 
 html {
-	background-color: var(--bg);
-	color: var(--fg);
-	accent-color: var(--accent);
+	background-color: var(--MI_THEME-bg);
+	color: var(--MI_THEME-fg);
+	accent-color: var(--MI_THEME-accent);
 	overflow: auto;
 	overflow-wrap: break-word;
 	font-family: 'Hiragino Maru Gothic Pro', "BIZ UDGothic", Roboto, HelveticaNeue, Arial, sans-serif;
@@ -50,7 +41,7 @@ html {
 	-webkit-text-size-adjust: 100%;
 
 	&, * {
-		scrollbar-color: var(--scrollbarHandle) transparent;
+		scrollbar-color: var(--MI_THEME-scrollbarHandle) transparent;
 		scrollbar-width: thin;
 
 		&::-webkit-scrollbar {
@@ -63,14 +54,14 @@ html {
 		}
 
 		&::-webkit-scrollbar-thumb {
-			background: var(--scrollbarHandle);
+			background: var(--MI_THEME-scrollbarHandle);
 
 			&:hover {
-				background: var(--scrollbarHandleHover);
+				background: var(--MI_THEME-scrollbarHandleHover);
 			}
 
 			&:active {
-				background: var(--accent);
+				background: var(--MI_THEME-accent);
 			}
 		}
 	}
@@ -113,6 +104,10 @@ a {
 	-webkit-tap-highlight-color: transparent;
 	-webkit-touch-callout: none;
 
+	&:focus-visible {
+		outline-offset: 2px;
+	}
+
 	&:hover {
 		text-decoration: underline;
 	}
@@ -128,27 +123,36 @@ textarea, input {
 }
 
 optgroup, option {
-	background: var(--panel);
-	color: var(--fg);
+	background: var(--MI_THEME-panel);
+	color: var(--MI_THEME-fg);
 }
 
 hr {
-	margin: var(--margin) 0 var(--margin) 0;
+	margin: var(--MI-margin) 0 var(--MI-margin) 0;
 	border: none;
 	height: 1px;
-	background: var(--divider);
+	background: var(--MI_THEME-divider);
 }
 
 rt {
 	white-space: initial;
 }
 
+:focus-visible {
+	outline: var(--MI_THEME-focus) solid 2px;
+	outline-offset: -2px;
+
+	&:hover {
+		text-decoration: none;
+	}
+}
+
 .ti {
 	width: 1.28em;
 	vertical-align: -12%;
 	line-height: 1em;
 
-	&:before {
+	&::before {
 		font-size: 128%;
 	}
 }
@@ -168,9 +172,9 @@ rt {
 
 ._indicateCounter {
 	display: inline-flex;
-	color: var(--fgOnAccent);
+	color: var(--MI_THEME-fgOnAccent);
 	font-weight: 700;
-	background: var(--indicator);
+	background: var(--MI_THEME-indicator);
 	height: 1.5em;
 	min-width: 1.5em;
 	align-items: center;
@@ -203,13 +207,13 @@ rt {
 	left: 0;
 	width: 100%;
 	height: 100%;
-	background: var(--modalBg);
-	-webkit-backdrop-filter: var(--modalBgFilter);
-	backdrop-filter: var(--modalBgFilter);
+	background: var(--MI_THEME-modalBg);
+	-webkit-backdrop-filter: var(--MI-modalBgFilter);
+	backdrop-filter: var(--MI-modalBgFilter);
 }
 
 ._shadow {
-	box-shadow: 0px 4px 32px var(--shadow) !important;
+	box-shadow: 0px 4px 32px var(--MI_THEME-shadow) !important;
 }
 
 ._button {
@@ -230,10 +234,6 @@ rt {
 	line-height: inherit;
 	max-width: 100%;
 
-	&:focus-visible {
-		outline: none;
-	}
-
 	&:disabled {
 		opacity: 0.5;
 		cursor: default;
@@ -242,40 +242,44 @@ rt {
 
 ._buttonPrimary {
 	@extend ._button;
-	color: var(--fgOnAccent);
-	background: var(--accent);
+	color: var(--MI_THEME-fgOnAccent);
+	background: var(--MI_THEME-accent);
 
 	&:not(:disabled):hover {
-		background: var(--X8);
+		background: hsl(from var(--MI_THEME-accent) h s calc(l + 5));
 	}
 
 	&:not(:disabled):active {
-		background: var(--X9);
+		background: hsl(from var(--MI_THEME-accent) h s calc(l - 5));
 	}
 }
 
 ._buttonGradate {
 	@extend ._buttonPrimary;
-	color: var(--fgOnAccent);
-	background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB));
+	color: var(--MI_THEME-fgOnAccent);
+	background: linear-gradient(90deg, var(--MI_THEME-buttonGradateA), var(--MI_THEME-buttonGradateB));
 
 	&:not(:disabled):hover {
-		background: linear-gradient(90deg, var(--X8), var(--X8));
+		background: linear-gradient(90deg, hsl(from var(--MI_THEME-accent) h s calc(l + 5)), hsl(from var(--MI_THEME-accent) h s calc(l + 5)));
 	}
 
 	&:not(:disabled):active {
-		background: linear-gradient(90deg, var(--X8), var(--X8));
+		background: linear-gradient(90deg, hsl(from var(--MI_THEME-accent) h s calc(l + 5)), hsl(from var(--MI_THEME-accent) h s calc(l + 5)));
 	}
 }
 
 ._help {
-	color: var(--accent);
-	cursor: help
+	color: var(--MI_THEME-accent);
+	cursor: help;
 }
 
 ._textButton {
 	@extend ._button;
-	color: var(--accent);
+	color: var(--MI_THEME-accent);
+
+	&:focus-visible {
+		outline-offset: 2px;
+	}
 
 	&:not(:disabled):hover {
 		text-decoration: underline;
@@ -283,13 +287,13 @@ rt {
 }
 
 ._panel {
-	background: var(--panel);
-	border-radius: var(--radius);
+	background: var(--MI_THEME-panel);
+	border-radius: var(--MI-radius);
 	overflow: clip;
 }
 
 ._margin {
-	margin: var(--margin) 0;
+	margin: var(--MI-margin) 0;
 }
 
 ._gaps_m {
@@ -307,7 +311,7 @@ rt {
 ._gaps {
 	display: flex;
 	flex-direction: column;
-	gap: var(--margin);
+	gap: var(--MI-margin);
 }
 
 ._buttons {
@@ -329,24 +333,24 @@ rt {
 	padding: 10px;
 	box-sizing: border-box;
 	text-align: center;
-	border: solid 0.5px var(--divider);
-	border-radius: var(--radius);
+	border: solid 0.5px var(--MI_THEME-divider);
+	border-radius: var(--MI-radius);
 
 	&:active {
-		border-color: var(--accent);
+		border-color: var(--MI_THEME-accent);
 	}
 }
 
 ._popup {
-	background: var(--popup);
-	border-radius: var(--radius);
+	background: var(--MI_THEME-popup);
+	border-radius: var(--MI-radius);
 	contain: content;
 }
 
 ._acrylic {
-	background: var(--acrylicPanel);
-	-webkit-backdrop-filter: var(--blur, blur(15px));
-	backdrop-filter: var(--blur, blur(15px));
+	background: var(--MI_THEME-acrylicPanel);
+	-webkit-backdrop-filter: var(--MI-blur, blur(15px));
+	backdrop-filter: var(--MI-blur, blur(15px));
 }
 
 ._formLinksGrid {
@@ -359,8 +363,18 @@ rt {
 	margin-left: 0.7em;
 	font-size: 65%;
 	padding: 2px 3px;
-	color: var(--accent);
-	border: solid 1px var(--accent);
+	color: var(--MI_THEME-accent);
+	border: solid 1px var(--MI_THEME-accent);
+	border-radius: 4px;
+	vertical-align: top;
+}
+
+._modified {
+	margin-left: 0.7em;
+	font-size: 65%;
+	padding: 2px 3px;
+	color: var(--MI_THEME-warn);
+	border: solid 1px var(--MI_THEME-warn);
 	border-radius: 4px;
 	vertical-align: top;
 }
@@ -406,7 +420,7 @@ rt {
 }
 
 ._link {
-	color: var(--link);
+	color: var(--MI_THEME-link);
 }
 
 ._caption {
@@ -430,14 +444,14 @@ rt {
 	box-shadow: 0 6px 16px #0007, 0 0 1px 1px #693410, inset 0 0 2px 1px #ce8a5c;
 	border-radius: 10px;
 
-	--bg: #F1E8DC;
-	--fg: #693410;
+	--MI_THEME-bg: #F1E8DC;
+	--MI_THEME-fg: #693410;
 }
 
-html[data-color-mode=dark] ._woodenFrame {
-	--bg: #1d0c02;
-	--fg: #F1E8DC;
-	--panel: #192320;
+html[data-color-scheme=dark] ._woodenFrame {
+	--MI_THEME-bg: #1d0c02;
+	--MI_THEME-fg: #F1E8DC;
+	--MI_THEME-panel: #192320;
 }
 
 ._woodenFrameH {
@@ -448,10 +462,10 @@ html[data-color-mode=dark] ._woodenFrame {
 ._woodenFrameInner {
 	padding: 8px;
 	margin-top: 8px;
-	background: var(--bg);
+	background: var(--MI_THEME-bg);
 	box-shadow: 0 0 2px 1px #ce8a5c, inset 0 0 1px 1px #693410;
 	border-radius: 6px;
-	color: var(--fg);
+	color: var(--MI_THEME-fg);
 
 	&:first-child {
 		margin-top: 0;
@@ -466,7 +480,11 @@ html[data-color-mode=dark] ._woodenFrame {
 	transform: scale(0.9);
 }
 
-@keyframes global-blink {
+._blink {
+	animation: blink 1s infinite;
+}
+
+@keyframes blink {
 	0% { opacity: 1; transform: scale(1); }
 	30% { opacity: 1; transform: scale(1); }
 	90% { opacity: 0; transform: scale(0.5); }
diff --git a/packages/frontend/src/timelines.ts b/packages/frontend/src/timelines.ts
new file mode 100644
index 0000000000..94eda3545e
--- /dev/null
+++ b/packages/frontend/src/timelines.ts
@@ -0,0 +1,56 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { $i } from '@/account.js';
+import { instance } from '@/instance.js';
+
+export const basicTimelineTypes = [
+	'home',
+	'local',
+	'social',
+	'global',
+] as const;
+
+export type BasicTimelineType = typeof basicTimelineTypes[number];
+
+export function isBasicTimeline(timeline: string): timeline is BasicTimelineType {
+	return basicTimelineTypes.includes(timeline as BasicTimelineType);
+}
+
+export function basicTimelineIconClass(timeline: BasicTimelineType): string {
+	switch (timeline) {
+		case 'home':
+			return 'ti ti-home';
+		case 'local':
+			return 'ti ti-planet';
+		case 'social':
+			return 'ti ti-universe';
+		case 'global':
+			return 'ti ti-whirl';
+	}
+}
+
+export function isAvailableBasicTimeline(timeline: BasicTimelineType | undefined | null): boolean {
+	switch (timeline) {
+		case 'home':
+			return $i != null;
+		case 'local':
+			return ($i == null && instance.policies.ltlAvailable) || ($i != null && $i.policies.ltlAvailable);
+		case 'social':
+			return $i != null && $i.policies.ltlAvailable;
+		case 'global':
+			return ($i == null && instance.policies.gtlAvailable) || ($i != null && $i.policies.gtlAvailable);
+		default:
+			return false;
+	}
+}
+
+export function availableBasicTimelines(): BasicTimelineType[] {
+	return basicTimelineTypes.filter(isAvailableBasicTimeline);
+}
+
+export function hasWithReplies(timeline: BasicTimelineType | undefined | null): boolean {
+	return timeline === 'local' || timeline === 'social';
+}
diff --git a/packages/frontend/src/types/post-form.ts b/packages/frontend/src/types/post-form.ts
new file mode 100644
index 0000000000..5bb04a95a0
--- /dev/null
+++ b/packages/frontend/src/types/post-form.ts
@@ -0,0 +1,22 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import * as Misskey from 'misskey-js';
+
+export interface PostFormProps {
+	reply?: Misskey.entities.Note;
+	renote?: Misskey.entities.Note;
+	channel?: Misskey.entities.Channel; // TODO
+	mention?: Misskey.entities.User;
+	specified?: Misskey.entities.UserDetailed;
+	initialText?: string;
+	initialCw?: string;
+	initialVisibility?: (typeof Misskey.noteVisibilities)[number];
+	initialFiles?: Misskey.entities.DriveFile[];
+	initialLocalOnly?: boolean;
+	initialVisibleUsers?: Misskey.entities.UserDetailed[];
+	initialNote?: Misskey.entities.Note;
+	instant?: boolean;
+};
diff --git a/packages/frontend/src/ui/_common_/announcements.vue b/packages/frontend/src/ui/_common_/announcements.vue
index 374bc20b54..d153dc8726 100644
--- a/packages/frontend/src/ui/_common_/announcements.vue
+++ b/packages/frontend/src/ui/_common_/announcements.vue
@@ -13,9 +13,9 @@ SPDX-License-Identifier: AGPL-3.0-only
 	>
 		<span :class="$style.icon">
 			<i v-if="announcement.icon === 'info'" class="ti ti-info-circle"></i>
-			<i v-else-if="announcement.icon === 'warning'" class="ti ti-alert-triangle" style="color: var(--warn);"></i>
-			<i v-else-if="announcement.icon === 'error'" class="ti ti-circle-x" style="color: var(--error);"></i>
-			<i v-else-if="announcement.icon === 'success'" class="ti ti-check" style="color: var(--success);"></i>
+			<i v-else-if="announcement.icon === 'warning'" class="ti ti-alert-triangle" style="color: var(--MI_THEME-warn);"></i>
+			<i v-else-if="announcement.icon === 'error'" class="ti ti-circle-x" style="color: var(--MI_THEME-error);"></i>
+			<i v-else-if="announcement.icon === 'success'" class="ti ti-check" style="color: var(--MI_THEME-success);"></i>
 		</span>
 		<span :class="$style.title">{{ announcement.title }}</span>
 		<span :class="$style.body">{{ announcement.text }}</span>
@@ -30,7 +30,7 @@ import { $i } from '@/account.js';
 <style lang="scss" module>
 .root {
 	font-size: 15px;
-	background: var(--panel);
+	background: var(--MI_THEME-panel);
 }
 
 .item {
@@ -44,8 +44,8 @@ import { $i } from '@/account.js';
 	height: var(--height);
 	overflow: clip;
 	contain: strict;
-	background: var(--accent);
-	color: var(--fgOnAccent);
+	background: var(--MI_THEME-accent);
+	color: var(--MI_THEME-fgOnAccent);
 
 	@container (max-width: 1000px) {
 		display: block;
diff --git a/packages/frontend/src/ui/_common_/common.ts b/packages/frontend/src/ui/_common_/common.ts
index 839fa5faf8..f908803f01 100644
--- a/packages/frontend/src/ui/_common_/common.ts
+++ b/packages/frontend/src/ui/_common_/common.ts
@@ -7,7 +7,7 @@ import { defineAsyncComponent } from 'vue';
 import type { MenuItem } from '@/types/menu.js';
 import * as os from '@/os.js';
 import { instance } from '@/instance.js';
-import { host } from '@/config.js';
+import { host } from '@@/js/config.js';
 import { i18n } from '@/i18n.js';
 import { $i } from '@/account.js';
 
@@ -41,7 +41,9 @@ function toolsMenuItems(): MenuItem[] {
 }
 
 export function openInstanceMenu(ev: MouseEvent) {
-	os.popupMenu([{
+	const menuItems: MenuItem[] = [];
+
+	menuItems.push({
 		text: instance.name ?? host,
 		type: 'label',
 	}, {
@@ -69,12 +71,18 @@ export function openInstanceMenu(ev: MouseEvent) {
 		text: i18n.ts.ads,
 		icon: 'ti ti-ad',
 		to: '/ads',
-	}, ($i && ($i.isAdmin || $i.policies.canInvite) && instance.disableRegistration) ? {
-		type: 'link',
-		to: '/invite',
-		text: i18n.ts.invite,
-		icon: 'ti ti-user-plus',
-	} : undefined, {
+	});
+
+	if ($i && ($i.isAdmin || $i.policies.canInvite) && instance.disableRegistration) {
+		menuItems.push({
+			type: 'link',
+			to: '/invite',
+			text: i18n.ts.invite,
+			icon: 'ti ti-user-plus',
+		});
+	}
+
+	menuItems.push({
 		type: 'parent',
 		text: i18n.ts.tools,
 		icon: 'ti ti-tool',
@@ -84,41 +92,69 @@ export function openInstanceMenu(ev: MouseEvent) {
 		text: i18n.ts.inquiry,
 		icon: 'ti ti-help-circle',
 		to: '/contact',
-	}, (instance.impressumUrl) ? {
-		text: i18n.ts.impressum,
-		icon: 'ti ti-file-invoice',
-		action: () => {
-			window.open(instance.impressumUrl, '_blank', 'noopener');
-		},
-	} : undefined, (instance.tosUrl) ? {
-		text: i18n.ts.termsOfService,
-		icon: 'ti ti-notebook',
-		action: () => {
-			window.open(instance.tosUrl, '_blank', 'noopener');
-		},
-	} : undefined, (instance.privacyPolicyUrl) ? {
-		text: i18n.ts.privacyPolicy,
-		icon: 'ti ti-shield-lock',
-		action: () => {
-			window.open(instance.privacyPolicyUrl, '_blank', 'noopener');
-		},
-	} : undefined, (!instance.impressumUrl && !instance.tosUrl && !instance.privacyPolicyUrl) ? undefined : { type: 'divider' }, {
+	});
+
+	if (instance.impressumUrl) {
+		menuItems.push({
+			type: 'a',
+			text: i18n.ts.impressum,
+			icon: 'ti ti-file-invoice',
+			href: instance.impressumUrl,
+			target: '_blank',
+		});
+	}
+
+	if (instance.tosUrl) {
+		menuItems.push({
+			type: 'a',
+			text: i18n.ts.termsOfService,
+			icon: 'ti ti-notebook',
+			href: instance.tosUrl,
+			target: '_blank',
+		});
+	}
+
+	if (instance.privacyPolicyUrl) {
+		menuItems.push({
+			type: 'a',
+			text: i18n.ts.privacyPolicy,
+			icon: 'ti ti-shield-lock',
+			href: instance.privacyPolicyUrl,
+			target: '_blank',
+		});
+	}
+
+	if (!instance.impressumUrl && !instance.tosUrl && !instance.privacyPolicyUrl) {
+		menuItems.push({ type: 'divider' });
+	}
+
+	menuItems.push({
+		type: 'a',
 		text: i18n.ts.document,
 		icon: 'ti ti-bulb',
-		action: () => {
-			window.open('https://misskey-hub.net/docs/for-users/', '_blank', 'noopener');
-		},
-	}, ($i) ? {
-		text: i18n.ts._initialTutorial.launchTutorial,
-		icon: 'ti ti-presentation',
-		action: () => {
-			os.popup(defineAsyncComponent(() => import('@/components/MkTutorialDialog.vue')), {}, {}, 'closed');
-		},
-	} : undefined, {
+		href: 'https://misskey-hub.net/docs/for-users/',
+		target: '_blank',
+	});
+
+	if ($i) {
+		menuItems.push({
+			text: i18n.ts._initialTutorial.launchTutorial,
+			icon: 'ti ti-presentation',
+			action: () => {
+				const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkTutorialDialog.vue')), {}, {
+					closed: () => dispose(),
+				});
+			},
+		});
+	}
+
+	menuItems.push({
 		type: 'link',
 		text: i18n.ts.aboutMisskey,
 		to: '/about-misskey',
-	}], ev.currentTarget ?? ev.target, {
+	});
+
+	os.popupMenu(menuItems, ev.currentTarget ?? ev.target, {
 		align: 'left',
 	});
 }
diff --git a/packages/frontend/src/ui/_common_/common.vue b/packages/frontend/src/ui/_common_/common.vue
index 822b552837..d145b9b6c6 100644
--- a/packages/frontend/src/ui/_common_/common.vue
+++ b/packages/frontend/src/ui/_common_/common.vue
@@ -39,9 +39,9 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <div v-if="pendingApiRequestsCount > 0" id="wait"></div>
 
-<div v-if="dev" id="devTicker"><span>DEV BUILD</span></div>
+<div v-if="dev" id="devTicker"><span style="animation: dev-ticker-blink 2s infinite;">DEV BUILD</span></div>
 
-<div v-if="$i && $i.isBot" id="botWarn"><span>{{ i18n.ts.loggedInAsBot }}</span></div>
+<div v-if="$i && $i.isBot" id="botWarn"><span style="animation: dev-ticker-blink 2s infinite;">{{ i18n.ts.loggedInAsBot }}</span></div>
 </template>
 
 <script lang="ts" setup>
@@ -116,27 +116,27 @@ if ($i) {
 .notifications {
 	position: fixed;
 	z-index: 3900000;
-	padding: 0 var(--margin);
+	padding: 0 var(--MI-margin);
 	pointer-events: none;
 	display: flex;
 
 	&.notificationsPosition_leftTop {
-		top: var(--margin);
+		top: var(--MI-margin);
 		left: 0;
 	}
 
 	&.notificationsPosition_rightTop {
-		top: var(--margin);
+		top: var(--MI-margin);
 		right: 0;
 	}
 
 	&.notificationsPosition_leftBottom {
-		bottom: calc(var(--minBottomSpacing) + var(--margin));
+		bottom: calc(var(--MI-minBottomSpacing) + var(--MI-margin));
 		left: 0;
 	}
 
 	&.notificationsPosition_rightBottom {
-		bottom: calc(var(--minBottomSpacing) + var(--margin));
+		bottom: calc(var(--MI-minBottomSpacing) + var(--MI-margin));
 		right: 0;
 	}
 
@@ -227,15 +227,15 @@ if ($i) {
 	right: 15px;
 	pointer-events: none;
 
-	&:before {
+	&::before {
 		content: "";
 		display: block;
 		width: 18px;
 		height: 18px;
 		box-sizing: border-box;
 		border: solid 2px transparent;
-		border-top-color: var(--accent);
-		border-left-color: var(--accent);
+		border-top-color: var(--MI_THEME-accent);
+		border-left-color: var(--MI_THEME-accent);
 		border-radius: 50%;
 		animation: progress-spinner 400ms linear infinite;
 	}
@@ -258,10 +258,6 @@ if ($i) {
 	font-size: 14px;
 	pointer-events: none;
 	user-select: none;
-
-	> span {
-		animation: dev-ticker-blink 2s infinite;
-	}
 }
 
 #devTicker {
@@ -275,9 +271,5 @@ if ($i) {
 	font-size: 14px;
 	pointer-events: none;
 	user-select: none;
-
-	> span {
-		animation: dev-ticker-blink 2s infinite;
-	}
 }
 </style>
diff --git a/packages/frontend/src/ui/_common_/navbar-for-mobile.vue b/packages/frontend/src/ui/_common_/navbar-for-mobile.vue
index 5d0e065f09..44253e93bd 100644
--- a/packages/frontend/src/ui/_common_/navbar-for-mobile.vue
+++ b/packages/frontend/src/ui/_common_/navbar-for-mobile.vue
@@ -19,7 +19,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 			<div v-if="item === '-'" :class="$style.divider"></div>
 			<component :is="navbarItemDef[item].to ? 'MkA' : 'button'" v-else-if="navbarItemDef[item] && (navbarItemDef[item].show !== false)" class="_button" :class="[$style.item, { [$style.active]: navbarItemDef[item].active }]" :activeClass="$style.active" :to="navbarItemDef[item].to" v-on="navbarItemDef[item].action ? { click: navbarItemDef[item].action } : {}">
 				<i class="ti-fw" :class="[$style.itemIcon, navbarItemDef[item].icon]"></i><span :class="$style.itemText">{{ navbarItemDef[item].title }}</span>
-				<span v-if="navbarItemDef[item].indicated" :class="$style.itemIndicator">
+				<span v-if="navbarItemDef[item].indicated" :class="$style.itemIndicator" class="_blink">
 					<span v-if="navbarItemDef[item].indicateValue" class="_indicateCounter" :class="$style.itemIndicateValueIcon">{{ navbarItemDef[item].indicateValue }}</span>
 					<i v-else class="_indicatorCircle"></i>
 				</span>
@@ -31,7 +31,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 		</MkA>
 		<button :class="$style.item" class="_button" @click="more">
 			<i :class="$style.itemIcon" class="ti ti-grid-dots ti-fw"></i><span :class="$style.itemText">{{ i18n.ts.more }}</span>
-			<span v-if="otherMenuItemIndicated" :class="$style.itemIndicator"><i class="_indicatorCircle"></i></span>
+			<span v-if="otherMenuItemIndicated" :class="$style.itemIndicator" class="_blink"><i class="_indicatorCircle"></i></span>
 		</button>
 		<MkA :class="$style.item" :activeClass="$style.active" to="/settings">
 			<i :class="$style.itemIcon" class="ti ti-settings ti-fw"></i><span :class="$style.itemText">{{ i18n.ts.settings }}</span>
@@ -74,13 +74,16 @@ function openAccountMenu(ev: MouseEvent) {
 }
 
 function more() {
-	os.popup(defineAsyncComponent(() => import('@/components/MkLaunchPad.vue')), {}, {
-	}, 'closed');
+	const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkLaunchPad.vue')), {}, {
+		closed: () => dispose(),
+	});
 }
 </script>
 
 <style lang="scss" module>
 .root {
+	--nav-bg-transparent: color(from var(--MI_THEME-navBg) srgb r g b / 0.5);
+
 	display: flex;
 	flex-direction: column;
 }
@@ -90,9 +93,9 @@ function more() {
 	top: 0;
 	z-index: 1;
 	padding: 20px 0;
-	background: var(--X14);
-	-webkit-backdrop-filter: var(--blur, blur(8px));
-	backdrop-filter: var(--blur, blur(8px));
+	background: var(--nav-bg-transparent);
+	-webkit-backdrop-filter: var(--MI-blur, blur(8px));
+	backdrop-filter: var(--MI-blur, blur(8px));
 }
 
 .banner {
@@ -124,9 +127,9 @@ function more() {
 	position: sticky;
 	bottom: 0;
 	padding: 20px 0;
-	background: var(--X14);
-	-webkit-backdrop-filter: var(--blur, blur(8px));
-	backdrop-filter: var(--blur, blur(8px));
+	background: var(--nav-bg-transparent);
+	-webkit-backdrop-filter: var(--MI-blur, blur(8px));
+	backdrop-filter: var(--MI-blur, blur(8px));
 }
 
 .post {
@@ -134,11 +137,11 @@ function more() {
 	display: block;
 	width: 100%;
 	height: 40px;
-	color: var(--fgOnAccent);
+	color: var(--MI_THEME-fgOnAccent);
 	font-weight: bold;
 	text-align: left;
 
-	&:before {
+	&::before {
 		content: "";
 		display: block;
 		width: calc(100% - 38px);
@@ -150,12 +153,12 @@ function more() {
 		right: 0;
 		bottom: 0;
 		border-radius: 999px;
-		background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB));
+		background: linear-gradient(90deg, var(--MI_THEME-buttonGradateA), var(--MI_THEME-buttonGradateB));
 	}
 
 	&:hover, &.active {
-		&:before {
-			background: var(--accentLighten);
+		&::before {
+			background: var(--MI_THEME-accentLighten);
 		}
 	}
 }
@@ -199,7 +202,7 @@ function more() {
 
 .divider {
 	margin: 16px 16px;
-	border-top: solid 0.5px var(--divider);
+	border-top: solid 0.5px var(--MI_THEME-divider);
 }
 
 .item {
@@ -213,19 +216,19 @@ function more() {
 	width: 100%;
 	text-align: left;
 	box-sizing: border-box;
-	color: var(--navFg);
+	color: var(--MI_THEME-navFg);
 
 	&:hover {
 		text-decoration: none;
-		color: var(--navHoverFg);
+		color: var(--MI_THEME-navHoverFg);
 	}
 
 	&.active {
-		color: var(--navActive);
+		color: var(--MI_THEME-navActive);
 	}
 
 	&:hover, &.active {
-		&:before {
+		&::before {
 			content: "";
 			display: block;
 			width: calc(100% - 24px);
@@ -237,7 +240,7 @@ function more() {
 			right: 0;
 			bottom: 0;
 			border-radius: 999px;
-			background: var(--accentedBg);
+			background: var(--MI_THEME-accentedBg);
 		}
 	}
 }
@@ -252,9 +255,8 @@ function more() {
 	position: absolute;
 	top: 0;
 	left: 20px;
-	color: var(--navIndicator);
+	color: var(--MI_THEME-navIndicator);
 	font-size: 8px;
-	animation: global-blink 1s infinite;
 
 	&:has(.itemIndicateValueIcon) {
 		animation: none;
diff --git a/packages/frontend/src/ui/_common_/navbar.vue b/packages/frontend/src/ui/_common_/navbar.vue
index fa1f0eb8c7..8ae11efa2c 100644
--- a/packages/frontend/src/ui/_common_/navbar.vue
+++ b/packages/frontend/src/ui/_common_/navbar.vue
@@ -29,7 +29,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 					v-on="navbarItemDef[item].action ? { click: navbarItemDef[item].action } : {}"
 				>
 					<i class="ti-fw" :class="[$style.itemIcon, navbarItemDef[item].icon]"></i><span :class="$style.itemText">{{ navbarItemDef[item].title }}</span>
-					<span v-if="navbarItemDef[item].indicated" :class="$style.itemIndicator">
+					<span v-if="navbarItemDef[item].indicated" :class="$style.itemIndicator" class="_blink">
 						<span v-if="navbarItemDef[item].indicateValue" class="_indicateCounter" :class="$style.itemIndicateValueIcon">{{ navbarItemDef[item].indicateValue }}</span>
 						<i v-else class="_indicatorCircle"></i>
 					</span>
@@ -41,7 +41,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 			</MkA>
 			<button class="_button" :class="$style.item" @click="more">
 				<i :class="$style.itemIcon" class="ti ti-grid-dots ti-fw"></i><span :class="$style.itemText">{{ i18n.ts.more }}</span>
-				<span v-if="otherMenuItemIndicated" :class="$style.itemIndicator"><i class="_indicatorCircle"></i></span>
+				<span v-if="otherMenuItemIndicated" :class="$style.itemIndicator" class="_blink"><i class="_indicatorCircle"></i></span>
 			</button>
 			<MkA v-tooltip.noDelay.right="i18n.ts.settings" :class="$style.item" :activeClass="$style.active" to="/settings">
 				<i :class="$style.itemIcon" class="ti ti-settings ti-fw"></i><span :class="$style.itemText">{{ i18n.ts.settings }}</span>
@@ -99,10 +99,11 @@ function openAccountMenu(ev: MouseEvent) {
 }
 
 function more(ev: MouseEvent) {
-	os.popup(defineAsyncComponent(() => import('@/components/MkLaunchPad.vue')), {
+	const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkLaunchPad.vue')), {
 		src: ev.currentTarget ?? ev.target,
 	}, {
-	}, 'closed');
+		closed: () => dispose(),
+	});
 }
 </script>
 
@@ -110,6 +111,7 @@ function more(ev: MouseEvent) {
 .root {
 	--nav-width: 250px;
 	--nav-icon-only-width: 80px;
+	--nav-bg-transparent: color(from var(--MI_THEME-navBg) srgb r g b / 0.5);
 
 	flex: 0 0 var(--nav-width);
 	width: var(--nav-width);
@@ -127,7 +129,7 @@ function more(ev: MouseEvent) {
 	overflow: auto;
 	overflow-x: clip;
 	overscroll-behavior: contain;
-	background: var(--navBg);
+	background: var(--MI_THEME-navBg);
 	contain: strict;
 	display: flex;
 	flex-direction: column;
@@ -143,9 +145,9 @@ function more(ev: MouseEvent) {
 		top: 0;
 		z-index: 1;
 		padding: 20px 0;
-		background: var(--X14);
-		-webkit-backdrop-filter: var(--blur, blur(8px));
-		backdrop-filter: var(--blur, blur(8px));
+		background: var(--nav-bg-transparent);
+		-webkit-backdrop-filter: var(--MI-blur, blur(8px));
+		backdrop-filter: var(--MI-blur, blur(8px));
 	}
 
 	.banner {
@@ -165,6 +167,15 @@ function more(ev: MouseEvent) {
 		display: block;
 		text-align: center;
 		width: 100%;
+
+		&:focus-visible {
+			outline: none;
+
+			> .instanceIcon {
+				outline: 2px solid var(--MI_THEME-focus);
+				outline-offset: 2px;
+			}
+		}
 	}
 
 	.instanceIcon {
@@ -177,9 +188,9 @@ function more(ev: MouseEvent) {
 		position: sticky;
 		bottom: 0;
 		padding-top: 20px;
-		background: var(--X14);
-		-webkit-backdrop-filter: var(--blur, blur(8px));
-		backdrop-filter: var(--blur, blur(8px));
+		background: var(--nav-bg-transparent);
+		-webkit-backdrop-filter: var(--MI-blur, blur(8px));
+		backdrop-filter: var(--MI-blur, blur(8px));
 	}
 
 	.post {
@@ -187,11 +198,11 @@ function more(ev: MouseEvent) {
 		display: block;
 		width: 100%;
 		height: 40px;
-		color: var(--fgOnAccent);
+		color: var(--MI_THEME-fgOnAccent);
 		font-weight: bold;
 		text-align: left;
 
-		&:before {
+		&::before {
 			content: "";
 			display: block;
 			width: calc(100% - 38px);
@@ -203,12 +214,21 @@ function more(ev: MouseEvent) {
 			right: 0;
 			bottom: 0;
 			border-radius: 999px;
-			background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB));
+			background: linear-gradient(90deg, var(--MI_THEME-buttonGradateA), var(--MI_THEME-buttonGradateB));
+		}
+
+		&:focus-visible {
+			outline: none;
+
+			&::before {
+				outline: 2px solid var(--MI_THEME-fgOnAccent);
+				outline-offset: -4px;
+			}
 		}
 
 		&:hover, &.active {
-			&:before {
-				background: var(--accentLighten);
+			&::before {
+				background: var(--MI_THEME-accentLighten);
 			}
 		}
 	}
@@ -233,6 +253,14 @@ function more(ev: MouseEvent) {
 		text-align: left;
 		box-sizing: border-box;
 		overflow: clip;
+
+		&:focus-visible {
+			outline: none;
+
+			> .avatar {
+				box-shadow: 0 0 0 4px var(--MI_THEME-focus);
+			}
+		}
 	}
 
 	.avatar {
@@ -256,7 +284,7 @@ function more(ev: MouseEvent) {
 
 	.divider {
 		margin: 16px 16px;
-		border-top: solid 0.5px var(--divider);
+		border-top: solid 0.5px var(--MI_THEME-divider);
 	}
 
 	.item {
@@ -270,21 +298,30 @@ function more(ev: MouseEvent) {
 		width: 100%;
 		text-align: left;
 		box-sizing: border-box;
-		color: var(--navFg);
+		color: var(--MI_THEME-navFg);
 
 		&:hover {
 			text-decoration: none;
-			color: var(--navHoverFg);
+			color: var(--MI_THEME-navHoverFg);
 		}
 
 		&.active {
-			color: var(--navActive);
+			color: var(--MI_THEME-navActive);
 		}
 
-		&:hover, &.active {
-			color: var(--accent);
+		&:focus-visible {
+			outline: none;
 
-			&:before {
+			&::before {
+				outline: 2px solid var(--MI_THEME-focus);
+				outline-offset: -2px;
+			}
+		}
+
+		&:hover, &.active, &:focus {
+			color: var(--MI_THEME-accent);
+
+			&::before {
 				content: "";
 				display: block;
 				width: calc(100% - 34px);
@@ -296,7 +333,7 @@ function more(ev: MouseEvent) {
 				right: 0;
 				bottom: 0;
 				border-radius: 999px;
-				background: var(--accentedBg);
+				background: var(--MI_THEME-accentedBg);
 			}
 		}
 	}
@@ -311,9 +348,8 @@ function more(ev: MouseEvent) {
 		position: absolute;
 		top: 0;
 		left: 20px;
-		color: var(--navIndicator);
+		color: var(--MI_THEME-navIndicator);
 		font-size: 8px;
-		animation: global-blink 1s infinite;
 
 		&:has(.itemIndicateValueIcon) {
 			animation: none;
@@ -342,15 +378,24 @@ function more(ev: MouseEvent) {
 		top: 0;
 		z-index: 1;
 		padding: 20px 0;
-		background: var(--X14);
-		-webkit-backdrop-filter: var(--blur, blur(8px));
-		backdrop-filter: var(--blur, blur(8px));
+		background: var(--nav-bg-transparent);
+		-webkit-backdrop-filter: var(--MI-blur, blur(8px));
+		backdrop-filter: var(--MI-blur, blur(8px));
 	}
 
 	.instance {
 		display: block;
 		text-align: center;
 		width: 100%;
+
+		&:focus-visible {
+			outline: none;
+
+			> .instanceIcon {
+				outline: 2px solid var(--MI_THEME-focus);
+				outline-offset: 2px;
+			}
+		}
 	}
 
 	.instanceIcon {
@@ -363,9 +408,9 @@ function more(ev: MouseEvent) {
 		position: sticky;
 		bottom: 0;
 		padding-top: 20px;
-		background: var(--X14);
-		-webkit-backdrop-filter: var(--blur, blur(8px));
-		backdrop-filter: var(--blur, blur(8px));
+		background: var(--nav-bg-transparent);
+		-webkit-backdrop-filter: var(--MI-blur, blur(8px));
+		backdrop-filter: var(--MI-blur, blur(8px));
 	}
 
 	.post {
@@ -375,7 +420,7 @@ function more(ev: MouseEvent) {
 		height: 52px;
 		text-align: center;
 
-		&:before {
+		&::before {
 			content: "";
 			display: block;
 			position: absolute;
@@ -387,19 +432,28 @@ function more(ev: MouseEvent) {
 			width: 52px;
 			aspect-ratio: 1/1;
 			border-radius: 100%;
-			background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB));
+			background: linear-gradient(90deg, var(--MI_THEME-buttonGradateA), var(--MI_THEME-buttonGradateB));
+		}
+
+		&:focus-visible {
+			outline: none;
+
+			&::before {
+				outline: 2px solid var(--MI_THEME-fgOnAccent);
+				outline-offset: -4px;
+			}
 		}
 
 		&:hover, &.active {
-			&:before {
-				background: var(--accentLighten);
+			&::before {
+				background: var(--MI_THEME-accentLighten);
 			}
 		}
 	}
 
 	.postIcon {
 		position: relative;
-		color: var(--fgOnAccent);
+		color: var(--MI_THEME-fgOnAccent);
 	}
 
 	.postText {
@@ -412,6 +466,14 @@ function more(ev: MouseEvent) {
 		padding: 20px 0;
 		width: 100%;
 		overflow: clip;
+
+		&:focus-visible {
+			outline: none;
+
+			> .avatar {
+				box-shadow: 0 0 0 4px var(--MI_THEME-focus);
+			}
+		}
 	}
 
 	.avatar {
@@ -431,7 +493,7 @@ function more(ev: MouseEvent) {
 	.divider {
 		margin: 8px auto;
 		width: calc(100% - 32px);
-		border-top: solid 0.5px var(--divider);
+		border-top: solid 0.5px var(--MI_THEME-divider);
 	}
 
 	.item {
@@ -441,11 +503,20 @@ function more(ev: MouseEvent) {
 		width: 100%;
 		text-align: center;
 
-		&:hover, &.active {
-			text-decoration: none;
-			color: var(--accent);
+		&:focus-visible {
+			outline: none;
 
-			&:before {
+			&::before {
+				outline: 2px solid var(--MI_THEME-focus);
+				outline-offset: -2px;
+			}
+		}
+
+		&:hover, &.active, &:focus {
+			text-decoration: none;
+			color: var(--MI_THEME-accent);
+
+			&::before {
 				content: "";
 				display: block;
 				height: 100%;
@@ -457,7 +528,7 @@ function more(ev: MouseEvent) {
 				right: 0;
 				bottom: 0;
 				border-radius: 999px;
-				background: var(--accentedBg);
+				background: var(--MI_THEME-accentedBg);
 			}
 
 			> .icon,
@@ -481,9 +552,8 @@ function more(ev: MouseEvent) {
 		position: absolute;
 		top: 6px;
 		left: 24px;
-		color: var(--navIndicator);
+		color: var(--MI_THEME-navIndicator);
 		font-size: 8px;
-		animation: global-blink 1s infinite;
 
 		&:has(.itemIndicateValueIcon) {
 			animation: none;
diff --git a/packages/frontend/src/ui/_common_/statusbar-federation.vue b/packages/frontend/src/ui/_common_/statusbar-federation.vue
index 8dad666623..e234bb3a33 100644
--- a/packages/frontend/src/ui/_common_/statusbar-federation.vue
+++ b/packages/frontend/src/ui/_common_/statusbar-federation.vue
@@ -35,7 +35,7 @@ import { ref } from 'vue';
 import * as Misskey from 'misskey-js';
 import MarqueeText from '@/components/MkMarquee.vue';
 import { misskeyApi } from '@/scripts/misskey-api.js';
-import { useInterval } from '@/scripts/use-interval.js';
+import { useInterval } from '@@/js/use-interval.js';
 import { getProxiedImageUrlNullable } from '@/scripts/media-proxy.js';
 
 const props = defineProps<{
diff --git a/packages/frontend/src/ui/_common_/statusbar-rss.vue b/packages/frontend/src/ui/_common_/statusbar-rss.vue
index 6e1d06eec1..da8fa8bb21 100644
--- a/packages/frontend/src/ui/_common_/statusbar-rss.vue
+++ b/packages/frontend/src/ui/_common_/statusbar-rss.vue
@@ -30,7 +30,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { ref } from 'vue';
 import * as Misskey from 'misskey-js';
 import MarqueeText from '@/components/MkMarquee.vue';
-import { useInterval } from '@/scripts/use-interval.js';
+import { useInterval } from '@@/js/use-interval.js';
 import { shuffle } from '@/scripts/shuffle.js';
 
 const props = defineProps<{
@@ -48,7 +48,7 @@ const fetching = ref(true);
 const key = ref(0);
 
 const tick = () => {
-	window.fetch(`/api/fetch-rss?url=${props.url}`, {}).then(res => {
+	window.fetch(`/api/fetch-rss?url=${encodeURIComponent(props.url)}`, {}).then(res => {
 		res.json().then((feed: Misskey.entities.FetchRssResponse) => {
 			if (props.shuffle) {
 				shuffle(feed.items);
diff --git a/packages/frontend/src/ui/_common_/statusbar-user-list.vue b/packages/frontend/src/ui/_common_/statusbar-user-list.vue
index 67f8b109c4..078b595dca 100644
--- a/packages/frontend/src/ui/_common_/statusbar-user-list.vue
+++ b/packages/frontend/src/ui/_common_/statusbar-user-list.vue
@@ -35,7 +35,7 @@ import { ref, watch } from 'vue';
 import * as Misskey from 'misskey-js';
 import MarqueeText from '@/components/MkMarquee.vue';
 import { misskeyApi } from '@/scripts/misskey-api.js';
-import { useInterval } from '@/scripts/use-interval.js';
+import { useInterval } from '@@/js/use-interval.js';
 import { getNoteSummary } from '@/scripts/get-note-summary.js';
 import { notePage } from '@/filters/note.js';
 
diff --git a/packages/frontend/src/ui/_common_/statusbars.vue b/packages/frontend/src/ui/_common_/statusbars.vue
index 872c69810c..5f9a938017 100644
--- a/packages/frontend/src/ui/_common_/statusbars.vue
+++ b/packages/frontend/src/ui/_common_/statusbars.vue
@@ -32,7 +32,7 @@ const XUserList = defineAsyncComponent(() => import('./statusbar-user-list.vue')
 <style lang="scss" module>
 .root {
 	font-size: 15px;
-	background: var(--panel);
+	background: var(--MI_THEME-panel);
 }
 
 .item {
@@ -40,6 +40,14 @@ const XUserList = defineAsyncComponent(() => import('./statusbar-user-list.vue')
 	--nameMargin: 10px;
 	font-size: 0.85em;
 
+	display: flex;
+	vertical-align: bottom;
+	width: 100%;
+	line-height: var(--height);
+	height: var(--height);
+	overflow: clip;
+	contain: strict;
+
 	&.verySmall {
 		--nameMargin: 7px;
 		--height: 16px;
@@ -64,14 +72,6 @@ const XUserList = defineAsyncComponent(() => import('./statusbar-user-list.vue')
 		font-size: 0.9em;
 	}
 
-	display: flex;
-	vertical-align: bottom;
-	width: 100%;
-	line-height: var(--height);
-	height: var(--height);
-	overflow: clip;
-	contain: strict;
-
 	&.black {
 		background: #000;
 		color: #fff;
@@ -81,7 +81,7 @@ const XUserList = defineAsyncComponent(() => import('./statusbar-user-list.vue')
 .name {
 	padding: 0 var(--nameMargin);
 	font-weight: bold;
-	color: var(--accent);
+	color: var(--MI_THEME-accent);
 
 	&:empty {
 		display: none;
diff --git a/packages/frontend/src/ui/_common_/stream-indicator.vue b/packages/frontend/src/ui/_common_/stream-indicator.vue
index ad93b7e61c..cc62a28b14 100644
--- a/packages/frontend/src/ui/_common_/stream-indicator.vue
+++ b/packages/frontend/src/ui/_common_/stream-indicator.vue
@@ -48,8 +48,8 @@ onUnmounted(() => {
 .root {
 	position: fixed;
 	z-index: v-bind(zIndex);
-	bottom: calc(var(--minBottomSpacing) + var(--margin));
-	right: var(--margin);
+	bottom: calc(var(--MI-minBottomSpacing) + var(--MI-margin));
+	right: var(--MI-margin);
 	margin: 0;
 	padding: 12px;
 	font-size: 0.9em;
diff --git a/packages/frontend/src/ui/_common_/upload.vue b/packages/frontend/src/ui/_common_/upload.vue
index 6db7f9cae7..c7d1387eae 100644
--- a/packages/frontend/src/ui/_common_/upload.vue
+++ b/packages/frontend/src/ui/_common_/upload.vue
@@ -125,10 +125,10 @@ const zIndex = os.claimZIndex('high');
 	height: 8px;
 }
 .mk-uploader > ol > li > progress::-webkit-progress-value {
-  background: var(--accent);
+  background: var(--MI_THEME-accent);
 }
 .mk-uploader > ol > li > progress::-webkit-progress-bar {
-  //background: var(--accentAlpha01);
+  //background: var(--MI_THEME-accentAlpha01);
 	background: transparent;
 }
 </style>
diff --git a/packages/frontend/src/ui/classic.header.vue b/packages/frontend/src/ui/classic.header.vue
index ee5176b558..f4633314ae 100644
--- a/packages/frontend/src/ui/classic.header.vue
+++ b/packages/frontend/src/ui/classic.header.vue
@@ -17,7 +17,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 				<div v-if="item === '-'" class="divider"></div>
 				<component :is="navbarItemDef[item].to ? 'MkA' : 'button'" v-else-if="navbarItemDef[item] && (navbarItemDef[item].show !== false)" v-click-anime v-tooltip="navbarItemDef[item].title" class="item _button" :class="item" activeClass="active" :to="navbarItemDef[item].to" v-on="navbarItemDef[item].action ? { click: navbarItemDef[item].action } : {}">
 					<i class="ti-fw" :class="navbarItemDef[item].icon"></i>
-					<span v-if="navbarItemDef[item].indicated" class="indicator"><i class="_indicatorCircle"></i></span>
+					<span v-if="navbarItemDef[item].indicated" class="indicator _blink"><i class="_indicatorCircle"></i></span>
 				</component>
 			</template>
 			<div class="divider"></div>
@@ -26,7 +26,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 			</MkA>
 			<button v-click-anime class="item _button" @click="more">
 				<i class="ti ti-dots ti-fw"></i>
-				<span v-if="otherNavItemIndicated" class="indicator"><i class="_indicatorCircle"></i></span>
+				<span v-if="otherNavItemIndicated" class="indicator _blink"><i class="_indicatorCircle"></i></span>
 			</button>
 		</div>
 		<div class="right">
@@ -71,11 +71,12 @@ const otherNavItemIndicated = computed<boolean>(() => {
 });
 
 function more(ev: MouseEvent) {
-	os.popup(defineAsyncComponent(() => import('@/components/MkLaunchPad.vue')), {
+	const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkLaunchPad.vue')), {
 		src: ev.currentTarget ?? ev.target,
 		anchor: { x: 'center', y: 'bottom' },
 	}, {
-	}, 'closed');
+		closed: () => dispose(),
+	});
 }
 
 function openAccountMenu(ev: MouseEvent) {
@@ -103,7 +104,7 @@ onMounted(() => {
 	z-index: 1000;
 	width: 100%;
 	height: $height;
-	background-color: var(--bg);
+	background-color: var(--MI_THEME-bg);
 
 	> .body {
 		max-width: 1380px;
@@ -139,18 +140,17 @@ onMounted(() => {
 					position: absolute;
 					top: 0;
 					left: 0;
-					color: var(--navIndicator);
+					color: var(--MI_THEME-navIndicator);
 					font-size: 8px;
-					animation: global-blink 1s infinite;
 				}
 
 				&:hover {
 					text-decoration: none;
-					color: var(--navHoverFg);
+					color: var(--MI_THEME-navHoverFg);
 				}
 
 				&.active {
-					color: var(--navActive);
+					color: var(--MI_THEME-navActive);
 				}
 			}
 
@@ -158,7 +158,7 @@ onMounted(() => {
 				display: inline-block;
 				height: 16px;
 				margin: 0 10px;
-				border-right: solid 0.5px var(--divider);
+				border-right: solid 0.5px var(--MI_THEME-divider);
 			}
 
 			> .instance {
diff --git a/packages/frontend/src/ui/classic.sidebar.vue b/packages/frontend/src/ui/classic.sidebar.vue
index 19672ef87f..5acef0bef8 100644
--- a/packages/frontend/src/ui/classic.sidebar.vue
+++ b/packages/frontend/src/ui/classic.sidebar.vue
@@ -21,7 +21,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 		<div v-if="item === '-'" class="divider"></div>
 		<component :is="navbarItemDef[item].to ? 'MkA' : 'button'" v-else-if="navbarItemDef[item] && (navbarItemDef[item].show !== false)" v-click-anime class="item _button" :class="item" activeClass="active" :to="navbarItemDef[item].to" v-on="navbarItemDef[item].action ? { click: navbarItemDef[item].action } : {}">
 			<i class="ti-fw" :class="navbarItemDef[item].icon"></i><span class="text">{{ navbarItemDef[item].title }}</span>
-			<span v-if="navbarItemDef[item].indicated" class="indicator">
+			<span v-if="navbarItemDef[item].indicated" class="indicator _blink">
 				<span v-if="navbarItemDef[item].indicateValue" class="_indicateCounter itemIndicateValueIcon">{{ navbarItemDef[item].indicateValue }}</span>
 				<i v-else class="_indicatorCircle"></i>
 			</span>
@@ -33,7 +33,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 	</MkA>
 	<button v-click-anime class="item _button" @click="more">
 		<i class="ti ti-dots ti-fw"></i><span class="text">{{ i18n.ts.more }}</span>
-		<span v-if="otherNavItemIndicated" class="indicator"><i class="_indicatorCircle"></i></span>
+		<span v-if="otherNavItemIndicated" class="indicator _blink"><i class="_indicatorCircle"></i></span>
 	</button>
 	<MkA v-click-anime class="item" activeClass="active" to="/settings" :behavior="settingsWindowed ? 'window' : null">
 		<i class="ti ti-settings ti-fw"></i><span class="text">{{ i18n.ts.settings }}</span>
@@ -51,7 +51,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 <script lang="ts" setup>
 import { defineAsyncComponent, computed, watch, ref, shallowRef } from 'vue';
 import { openInstanceMenu } from './_common_/common.js';
-// import { host } from '@/config.js';
+// import { host } from '@@/js/config.js';
 import * as os from '@/os.js';
 import { navbarItemDef } from '@/navbar.js';
 import { openAccountMenu as openAccountMenu_, $i } from '@/account.js';
@@ -86,9 +86,11 @@ function calcViewState() {
 }
 
 function more(ev: MouseEvent) {
-	os.popup(defineAsyncComponent(() => import('@/components/MkLaunchPad.vue')), {
+	const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkLaunchPad.vue')), {
 		src: ev.currentTarget ?? ev.target,
-	}, {}, 'closed');
+	}, {
+		closed: () => dispose(),
+	});
 }
 
 function openAccountMenu(ev: MouseEvent) {
@@ -155,7 +157,7 @@ watch(defaultStore.reactiveState.menuDisplay, () => {
 
 	> .divider {
 		margin: 10px 0;
-		border-top: solid 0.5px var(--divider);
+		border-top: solid 0.5px var(--MI_THEME-divider);
 	}
 
 	> .post {
@@ -163,7 +165,7 @@ watch(defaultStore.reactiveState.menuDisplay, () => {
 		top: 0;
 		z-index: 1;
 		padding: 16px 0;
-		background: var(--bg);
+		background: var(--MI_THEME-bg);
 
 		> .button {
 			min-width: 0;
@@ -218,9 +220,8 @@ watch(defaultStore.reactiveState.menuDisplay, () => {
 			position: absolute;
 			top: 0;
 			left: 0;
-			color: var(--navIndicator);
+			color: var(--MI_THEME-navIndicator);
 			font-size: 8px;
-			animation: global-blink 1s infinite;
 
 			&:has(.itemIndicateValueIcon) {
 				animation: none;
@@ -231,11 +232,11 @@ watch(defaultStore.reactiveState.menuDisplay, () => {
 
 		&:hover {
 			text-decoration: none;
-			color: var(--navHoverFg);
+			color: var(--MI_THEME-navHoverFg);
 		}
 
 		&.active {
-			color: var(--navActive);
+			color: var(--MI_THEME-navActive);
 		}
 	}
 }
diff --git a/packages/frontend/src/ui/classic.vue b/packages/frontend/src/ui/classic.vue
index b833e9f6be..5ea9bf7068 100644
--- a/packages/frontend/src/ui/classic.vue
+++ b/packages/frontend/src/ui/classic.vue
@@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 			<XSidebar/>
 		</div>
 		<div v-else-if="!pageMetadata?.needWideArea" ref="widgetsLeft" class="widgets left">
-			<XWidgets place="left" :marginTop="'var(--margin)'" @mounted="attachSticky(widgetsLeft)"/>
+			<XWidgets place="left" :marginTop="'var(--MI-margin)'" @mounted="attachSticky(widgetsLeft)"/>
 		</div>
 
 		<main class="main" @contextmenu.stop="onContextmenu">
@@ -22,7 +22,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 		</main>
 
 		<div v-if="isDesktop && !pageMetadata?.needWideArea" ref="widgetsRight" class="widgets right">
-			<XWidgets :place="showMenuOnTop ? 'right' : null" :marginTop="showMenuOnTop ? '0' : 'var(--margin)'" @mounted="attachSticky(widgetsRight)"/>
+			<XWidgets :place="showMenuOnTop ? 'right' : null" :marginTop="showMenuOnTop ? '0' : 'var(--MI-margin)'" @mounted="attachSticky(widgetsRight)"/>
 		</div>
 	</div>
 
@@ -49,7 +49,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { defineAsyncComponent, onMounted, provide, ref, computed, shallowRef } from 'vue';
 import XSidebar from './classic.sidebar.vue';
 import XCommon from './_common_/common.vue';
-import { instanceName } from '@/config.js';
+import { instanceName } from '@@/js/config.js';
 import { StickySidebar } from '@/scripts/sticky-sidebar.js';
 import * as os from '@/os.js';
 import { PageMetadata, provideMetadataReceiver, provideReactiveMetadata } from '@/scripts/page-metadata.js';
@@ -57,6 +57,8 @@ import { defaultStore } from '@/store.js';
 import { i18n } from '@/i18n.js';
 import { miLocalStorage } from '@/local-storage.js';
 import { mainRouter } from '@/router/main.js';
+import { isLink } from '@@/js/is-link.js';
+
 const XHeaderMenu = defineAsyncComponent(() => import('./classic.header.vue'));
 const XWidgets = defineAsyncComponent(() => import('./universal.widgets.vue'));
 
@@ -104,12 +106,6 @@ function top() {
 }
 
 function onContextmenu(ev: MouseEvent) {
-	const isLink = (el: HTMLElement) => {
-		if (el.tagName === 'A') return true;
-		if (el.parentElement) {
-			return isLink(el.parentElement);
-		}
-	};
 	if (isLink(ev.target)) return;
 	if (['INPUT', 'TEXTAREA', 'IMG', 'VIDEO', 'CANVAS'].includes(ev.target.tagName) || ev.target.attributes['contenteditable']) return;
 	if (window.getSelection().toString() !== '') return;
@@ -220,8 +216,8 @@ onMounted(() => {
 	box-sizing: border-box;
 
 	&.wallpaper {
-		background: var(--wallpaperOverlay);
-		//backdrop-filter: var(--blur, blur(4px));
+		background: var(--MI_THEME-wallpaperOverlay);
+		//backdrop-filter: var(--MI-blur, blur(4px));
 	}
 
 	> .columns {
@@ -253,17 +249,17 @@ onMounted(() => {
 			min-width: 0;
 			width: 750px;
 			margin: 0 16px 0 0;
-			border-left: solid 1px var(--divider);
-			border-right: solid 1px var(--divider);
+			border-left: solid 1px var(--MI_THEME-divider);
+			border-right: solid 1px var(--MI_THEME-divider);
 			border-radius: 0;
 			overflow: clip;
-			--margin: 12px;
+			--MI-margin: 12px;
 		}
 
 		> .widgets {
-			//--panelBorder: none;
+			//--MI_THEME-panelBorder: none;
 			width: 300px;
-			padding-bottom: calc(var(--margin) + env(safe-area-inset-bottom, 0px));
+			padding-bottom: calc(var(--MI-margin) + env(safe-area-inset-bottom, 0px));
 
 			@media (max-width: $widgets-hide-threshold) {
 				display: none;
@@ -281,13 +277,13 @@ onMounted(() => {
 		&.withGlobalHeader {
 			> .main {
 				margin-top: 0;
-				border: solid 1px var(--divider);
-				border-radius: var(--radius);
-				--stickyTop: var(--globalHeaderHeight);
+				border: solid 1px var(--MI_THEME-divider);
+				border-radius: var(--MI-radius);
+				--MI-stickyTop: var(--globalHeaderHeight);
 			}
 
 			> .widgets {
-				--stickyTop: var(--globalHeaderHeight);
+				--MI-stickyTop: var(--globalHeaderHeight);
 				margin-top: 0;
 			}
 		}
@@ -296,7 +292,7 @@ onMounted(() => {
 			margin: 0;
 
 			> .sidebar {
-				border-right: solid 0.5px var(--divider);
+				border-right: solid 0.5px var(--MI_THEME-divider);
 			}
 
 			> .main {
@@ -318,10 +314,10 @@ onMounted(() => {
 		right: 0;
 		z-index: 1001;
 		height: 100dvh;
-		padding: var(--margin) var(--margin) calc(var(--margin) + env(safe-area-inset-bottom, 0px));
+		padding: var(--MI-margin) var(--MI-margin) calc(var(--MI-margin) + env(safe-area-inset-bottom, 0px));
 		box-sizing: border-box;
 		overflow: auto;
-		background: var(--bg);
+		background: var(--MI_THEME-bg);
 	}
 
 	> .ivnzpscs {
diff --git a/packages/frontend/src/ui/deck.vue b/packages/frontend/src/ui/deck.vue
index bdb62dca15..a1a76a7e7d 100644
--- a/packages/frontend/src/ui/deck.vue
+++ b/packages/frontend/src/ui/deck.vue
@@ -24,7 +24,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 					:ref="id"
 					:key="id"
 					:class="$style.column"
-					:column="columns.find(c => c.id === id)"
+					:column="columns.find(c => c.id === id)!"
 					:isStacked="ids.length > 1"
 					@headerWheel="onWheel"
 				/>
@@ -50,11 +50,11 @@ SPDX-License-Identifier: AGPL-3.0-only
 	</div>
 
 	<div v-if="isMobile" :class="$style.nav">
-		<button :class="$style.navButton" class="_button" @click="drawerMenuShowing = true"><i :class="$style.navButtonIcon" class="ti ti-menu-2"></i><span v-if="menuIndicated" :class="$style.navButtonIndicator"><i class="_indicatorCircle"></i></span></button>
+		<button :class="$style.navButton" class="_button" @click="drawerMenuShowing = true"><i :class="$style.navButtonIcon" class="ti ti-menu-2"></i><span v-if="menuIndicated" :class="$style.navButtonIndicator" class="_blink"><i class="_indicatorCircle"></i></span></button>
 		<button :class="$style.navButton" class="_button" @click="mainRouter.push('/')"><i :class="$style.navButtonIcon" class="ti ti-home"></i></button>
 		<button :class="$style.navButton" class="_button" @click="mainRouter.push('/my/notifications')">
 			<i :class="$style.navButtonIcon" class="ti ti-bell"></i>
-			<span v-if="$i?.hasUnreadNotification" :class="$style.navButtonIndicator">
+			<span v-if="$i?.hasUnreadNotification" :class="$style.navButtonIndicator" class="_blink">
 				<span class="_indicateCounter" :class="$style.itemIndicateValueIcon">{{ $i.unreadNotificationsCount > 99 ? '99+' : $i.unreadNotificationsCount }}</span>
 			</span>
 		</button>
@@ -95,7 +95,9 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { computed, defineAsyncComponent, ref, watch, shallowRef } from 'vue';
 import { v4 as uuid } from 'uuid';
 import XCommon from './_common_/common.vue';
-import { deckStore, addColumn as addColumnToStore, loadDeck, getProfiles, deleteProfile as deleteProfile_ } from './deck/deck-store.js';
+import { deckStore, columnTypes, addColumn as addColumnToStore, loadDeck, getProfiles, deleteProfile as deleteProfile_ } from './deck/deck-store.js';
+import type { ColumnType } from './deck/deck-store.js';
+import type { MenuItem } from '@/types/menu.js';
 import XSidebar from '@/ui/_common_/navbar.vue';
 import XDrawerMenu from '@/ui/_common_/navbar-for-mobile.vue';
 import MkButton from '@/components/MkButton.vue';
@@ -117,7 +119,6 @@ import XMentionsColumn from '@/ui/deck/mentions-column.vue';
 import XDirectColumn from '@/ui/deck/direct-column.vue';
 import XRoleTimelineColumn from '@/ui/deck/role-timeline-column.vue';
 import { mainRouter } from '@/router/main.js';
-import { MenuItem } from '@/types/menu.js';
 const XStatusBars = defineAsyncComponent(() => import('@/ui/_common_/statusbars.vue'));
 const XAnnouncements = defineAsyncComponent(() => import('@/ui/_common_/announcements.vue'));
 
@@ -152,10 +153,12 @@ window.addEventListener('resize', () => {
 const snapScroll = deviceKind === 'smartphone' || deviceKind === 'tablet';
 const drawerMenuShowing = ref(false);
 
+/*
 const route = 'TODO';
 watch(route, () => {
 	drawerMenuShowing.value = false;
 });
+*/
 
 const columns = deckStore.reactiveState.columns;
 const layout = deckStore.reactiveState.layout;
@@ -174,32 +177,20 @@ function showSettings() {
 const columnsEl = shallowRef<HTMLElement>();
 
 const addColumn = async (ev) => {
-	const columns = [
-		'main',
-		'widgets',
-		'notifications',
-		'tl',
-		'antenna',
-		'list',
-		'channel',
-		'mentions',
-		'direct',
-		'roleTimeline',
-	];
-
 	const { canceled, result: column } = await os.select({
 		title: i18n.ts._deck.addColumn,
-		items: columns.map(column => ({
+		items: columnTypes.map(column => ({
 			value: column, text: i18n.ts._deck._columns[column],
 		})),
 	});
-	if (canceled) return;
+	if (canceled || column == null) return;
 
 	addColumnToStore({
 		type: column,
 		id: uuid(),
 		name: i18n.ts._deck._columns[column],
 		width: 330,
+		soundSetting: { type: null, volume: 1 },
 	});
 };
 
@@ -211,7 +202,7 @@ const onContextmenu = (ev) => {
 };
 
 function onWheel(ev: WheelEvent) {
-	if (ev.deltaX === 0) {
+	if (ev.deltaX === 0 && columnsEl.value != null) {
 		columnsEl.value.scrollLeft += ev.deltaY;
 	}
 }
@@ -242,7 +233,7 @@ function changeProfile(ev: MouseEvent) {
 					title: i18n.ts._deck.profile,
 					minLength: 1,
 				});
-				if (canceled) return;
+				if (canceled || name == null) return;
 
 				deckStore.set('profile', name);
 				unisonReload();
@@ -314,7 +305,7 @@ body {
 .root {
 	$nav-hide-threshold: 650px; // TODO: どこかに集約したい
 
-	--margin: var(--marginHalf);
+	--MI-margin: var(--MI-marginHalf);
 
 	--columnGap: 6px;
 
@@ -341,7 +332,7 @@ body {
 	overflow-x: auto;
 	overflow-y: clip;
 	overscroll-behavior: contain;
-	background: var(--deckBg);
+	background: var(--MI_THEME-deckBg);
 
 	&.center {
 		> .section:first-of-type {
@@ -423,7 +414,7 @@ body {
 	contain: strict;
 	overflow: auto;
 	overscroll-behavior: contain;
-	background: var(--navBg);
+	background: var(--MI_THEME-navBg);
 }
 
 .nav {
@@ -437,10 +428,10 @@ body {
 	grid-gap: 8px;
 	width: 100%;
 	box-sizing: border-box;
-	-webkit-backdrop-filter: var(--blur, blur(32px));
-	backdrop-filter: var(--blur, blur(32px));
-	background-color: var(--header);
-	border-top: solid 0.5px var(--divider);
+	-webkit-backdrop-filter: var(--MI-blur, blur(32px));
+	backdrop-filter: var(--MI-blur, blur(32px));
+	background-color: var(--MI_THEME-header);
+	border-top: solid 0.5px var(--MI_THEME-divider);
 }
 
 .navButton {
@@ -451,29 +442,29 @@ body {
 	max-width: 60px;
 	margin: auto;
 	border-radius: 100%;
-	background: var(--panel);
-	color: var(--fg);
+	background: var(--MI_THEME-panel);
+	color: var(--MI_THEME-fg);
 
 	&:hover {
-		background: var(--panelHighlight);
+		background: var(--MI_THEME-panelHighlight);
 	}
 
 	&:active {
-		background: var(--X2);
+		background: hsl(from var(--MI_THEME-panel) h s calc(l - 2));
 	}
 }
 
 .postButton {
 	composes: navButton;
-	background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB));
-	color: var(--fgOnAccent);
+	background: linear-gradient(90deg, var(--MI_THEME-buttonGradateA), var(--MI_THEME-buttonGradateB));
+	color: var(--MI_THEME-fgOnAccent);
 
 	&:hover {
-		background: linear-gradient(90deg, var(--X8), var(--X8));
+		background: linear-gradient(90deg, hsl(from var(--MI_THEME-accent) h s calc(l + 5)), hsl(from var(--MI_THEME-accent) h s calc(l + 5)));
 	}
 
 	&:active {
-		background: linear-gradient(90deg, var(--X8), var(--X8));
+		background: linear-gradient(90deg, hsl(from var(--MI_THEME-accent) h s calc(l + 5)), hsl(from var(--MI_THEME-accent) h s calc(l + 5)));
 	}
 }
 
@@ -486,9 +477,8 @@ body {
 	position: absolute;
 	top: 0;
 	left: 0;
-	color: var(--indicator);
+	color: var(--MI_THEME-indicator);
 	font-size: 16px;
-	animation: global-blink 1s infinite;
 
 	&:has(.itemIndicateValueIcon) {
 		animation: none;
diff --git a/packages/frontend/src/ui/deck/antenna-column.vue b/packages/frontend/src/ui/deck/antenna-column.vue
index c3dc1e4fce..a41639e71c 100644
--- a/packages/frontend/src/ui/deck/antenna-column.vue
+++ b/packages/frontend/src/ui/deck/antenna-column.vue
@@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 -->
 
 <template>
-<XColumn :menu="menu" :column="column" :isStacked="isStacked" :refresher="() => timeline.reloadTimeline()">
+<XColumn :menu="menu" :column="column" :isStacked="isStacked" :refresher="async () => { await timeline?.reloadTimeline() }">
 	<template #header>
 		<i class="ti ti-antenna"></i><span style="margin-left: 8px;">{{ column.name }}</span>
 	</template>
@@ -14,14 +14,16 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { onMounted, ref, shallowRef, watch } from 'vue';
+import { onMounted, ref, shallowRef, watch, defineAsyncComponent } from 'vue';
+import type { entities as MisskeyEntities } from 'misskey-js';
 import XColumn from './column.vue';
 import { updateColumn, Column } from './deck-store.js';
 import MkTimeline from '@/components/MkTimeline.vue';
 import * as os from '@/os.js';
 import { misskeyApi } from '@/scripts/misskey-api.js';
 import { i18n } from '@/i18n.js';
-import { MenuItem } from '@/types/menu.js';
+import type { MenuItem } from '@/types/menu.js';
+import { antennasCache } from '@/cache.js';
 import { SoundStore } from '@/store.js';
 import { soundSettingsButton } from '@/ui/deck/tl-note-notification.js';
 import * as sound from '@/scripts/sound.js';
@@ -46,14 +48,36 @@ watch(soundSetting, v => {
 
 async function setAntenna() {
 	const antennas = await misskeyApi('antennas/list');
-	const { canceled, result: antenna } = await os.select({
+	const { canceled, result: antenna } = await os.select<MisskeyEntities.Antenna | '_CREATE_'>({
 		title: i18n.ts.selectAntenna,
-		items: antennas.map(x => ({
-			value: x, text: x.name,
-		})),
+		items: [
+			{ value: '_CREATE_', text: i18n.ts.createNew },
+			(antennas.length > 0 ? {
+				sectionTitle: i18n.ts.createdAntennas,
+				items: antennas.map(x => ({
+					value: x, text: x.name,
+				})),
+			} : undefined),
+		],
 		default: props.column.antennaId,
 	});
-	if (canceled) return;
+	if (canceled || antenna == null) return;
+
+	if (antenna === '_CREATE_') {
+		const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkAntennaEditorDialog.vue')), {}, {
+			created: (newAntenna: MisskeyEntities.Antenna) => {
+				antennasCache.delete();
+				updateColumn(props.column.id, {
+					antennaId: newAntenna.id,
+				});
+			},
+			closed: () => {
+				dispose();
+			},
+		});
+		return;
+	}
+
 	updateColumn(props.column.id, {
 		antennaId: antenna.id,
 	});
diff --git a/packages/frontend/src/ui/deck/channel-column.vue b/packages/frontend/src/ui/deck/channel-column.vue
index 7c5b13eaf1..5479b53d90 100644
--- a/packages/frontend/src/ui/deck/channel-column.vue
+++ b/packages/frontend/src/ui/deck/channel-column.vue
@@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 -->
 
 <template>
-<XColumn :menu="menu" :column="column" :isStacked="isStacked" :refresher="() => timeline.reloadTimeline()">
+<XColumn :menu="menu" :column="column" :isStacked="isStacked" :refresher="async () => { await timeline?.reloadTimeline() }">
 	<template #header>
 		<i class="ti ti-device-tv"></i><span style="margin-left: 8px;">{{ column.name }}</span>
 	</template>
@@ -29,7 +29,7 @@ import * as os from '@/os.js';
 import { favoritedChannelsCache } from '@/cache.js';
 import { misskeyApi } from '@/scripts/misskey-api.js';
 import { i18n } from '@/i18n.js';
-import { MenuItem } from '@/types/menu.js';
+import type { MenuItem } from '@/types/menu.js';
 import { SoundStore } from '@/store.js';
 import { soundSettingsButton } from '@/ui/deck/tl-note-notification.js';
 import * as sound from '@/scripts/sound.js';
@@ -68,6 +68,7 @@ async function setChannel() {
 }
 
 async function post() {
+	if (props.column.channelId == null) return;
 	if (!channel.value || channel.value.id !== props.column.channelId) {
 		channel.value = await misskeyApi('channels/show', {
 			channelId: props.column.channelId,
diff --git a/packages/frontend/src/ui/deck/column.vue b/packages/frontend/src/ui/deck/column.vue
index 07845bacbb..da0bf24a56 100644
--- a/packages/frontend/src/ui/deck/column.vue
+++ b/packages/frontend/src/ui/deck/column.vue
@@ -21,7 +21,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 	>
 		<svg viewBox="0 0 256 128" :class="$style.tabShape">
 			<g transform="matrix(6.2431,0,0,6.2431,-677.417,-29.3839)">
-				<path d="M149.512,4.707L108.507,4.707C116.252,4.719 118.758,14.958 118.758,14.958C118.758,14.958 121.381,25.283 129.009,25.209L149.512,25.209L149.512,4.707Z" style="fill:var(--deckBg);"/>
+				<path d="M149.512,4.707L108.507,4.707C116.252,4.719 118.758,14.958 118.758,14.958C118.758,14.958 121.381,25.283 129.009,25.209L149.512,25.209L149.512,4.707Z" style="fill:var(--MI_THEME-deckBg);"/>
 			</g>
 		</svg>
 		<div :class="$style.color"></div>
@@ -46,7 +46,7 @@ import { onBeforeUnmount, onMounted, provide, watch, shallowRef, ref, computed }
 import { updateColumn, swapLeftColumn, swapRightColumn, swapUpColumn, swapDownColumn, stackLeftColumn, popRightColumn, removeColumn, swapColumn, Column } from './deck-store.js';
 import * as os from '@/os.js';
 import { i18n } from '@/i18n.js';
-import { MenuItem } from '@/types/menu.js';
+import type { MenuItem } from '@/types/menu.js';
 
 provide('shouldHeaderThin', true);
 provide('shouldOmitHeaderTitle', true);
@@ -104,7 +104,27 @@ function toggleActive() {
 }
 
 function getMenu() {
-	let items: MenuItem[] = [{
+	const menuItems: MenuItem[] = [];
+
+	if (props.menu) {
+		menuItems.push(...props.menu, {
+			type: 'divider',
+		});
+	}
+
+	if (props.refresher) {
+		menuItems.push({
+			icon: 'ti ti-refresh',
+			text: i18n.ts.reload,
+			action: () => {
+				if (props.refresher) {
+					props.refresher();
+				}
+			},
+		});
+	}
+
+	menuItems.push({
 		icon: 'ti ti-settings',
 		text: i18n.ts._deck.configureColumn,
 		action: async () => {
@@ -129,74 +149,73 @@ function getMenu() {
 			if (canceled) return;
 			updateColumn(props.column.id, result);
 		},
+	});
+
+	const moveToMenuItems: MenuItem[] = [];
+
+	moveToMenuItems.push({
+		icon: 'ti ti-arrow-left',
+		text: i18n.ts._deck.swapLeft,
+		action: () => {
+			swapLeftColumn(props.column.id);
+		},
 	}, {
-		type: 'parent',
-		text: i18n.ts.move + '...',
-		icon: 'ti ti-arrows-move',
-		children: [{
-			icon: 'ti ti-arrow-left',
-			text: i18n.ts._deck.swapLeft,
-			action: () => {
-				swapLeftColumn(props.column.id);
-			},
-		}, {
-			icon: 'ti ti-arrow-right',
-			text: i18n.ts._deck.swapRight,
-			action: () => {
-				swapRightColumn(props.column.id);
-			},
-		}, props.isStacked ? {
+		icon: 'ti ti-arrow-right',
+		text: i18n.ts._deck.swapRight,
+		action: () => {
+			swapRightColumn(props.column.id);
+		},
+	});
+
+	if (props.isStacked) {
+		moveToMenuItems.push({
 			icon: 'ti ti-arrow-up',
 			text: i18n.ts._deck.swapUp,
 			action: () => {
 				swapUpColumn(props.column.id);
 			},
-		} : undefined, props.isStacked ? {
+		}, {
 			icon: 'ti ti-arrow-down',
 			text: i18n.ts._deck.swapDown,
 			action: () => {
 				swapDownColumn(props.column.id);
 			},
-		} : undefined],
+		});
+	}
+
+	menuItems.push({
+		type: 'parent',
+		text: i18n.ts.move + '...',
+		icon: 'ti ti-arrows-move',
+		children: moveToMenuItems,
 	}, {
 		icon: 'ti ti-stack-2',
 		text: i18n.ts._deck.stackLeft,
 		action: () => {
 			stackLeftColumn(props.column.id);
 		},
-	}, props.isStacked ? {
-		icon: 'ti ti-window-maximize',
-		text: i18n.ts._deck.popRight,
-		action: () => {
-			popRightColumn(props.column.id);
-		},
-	} : undefined, { type: 'divider' }, {
+	});
+
+	if (props.isStacked) {
+		menuItems.push({
+			icon: 'ti ti-window-maximize',
+			text: i18n.ts._deck.popRight,
+			action: () => {
+				popRightColumn(props.column.id);
+			},
+		});
+	}
+
+	menuItems.push({ type: 'divider' }, {
 		icon: 'ti ti-trash',
 		text: i18n.ts.remove,
 		danger: true,
 		action: () => {
 			removeColumn(props.column.id);
 		},
-	}];
+	});
 
-	if (props.menu) {
-		items.unshift({ type: 'divider' });
-		items = props.menu.concat(items);
-	}
-
-	if (props.refresher) {
-		items = [{
-			icon: 'ti ti-refresh',
-			text: i18n.ts.reload,
-			action: () => {
-				if (props.refresher) {
-					props.refresher();
-				}
-			},
-		}, ...items];
-	}
-
-	return items;
+	return menuItems;
 }
 
 function showSettingsMenu(ev: MouseEvent) {
@@ -271,7 +290,7 @@ function onDrop(ev) {
 	border-radius: 10px;
 
 	&.draghover {
-		&:after {
+		&::after {
 			content: "";
 			display: block;
 			position: absolute;
@@ -280,12 +299,12 @@ function onDrop(ev) {
 			left: 0;
 			width: 100%;
 			height: 100%;
-			background: var(--focus);
+			background: var(--MI_THEME-focus);
 		}
 	}
 
 	&.dragging {
-		&:after {
+		&::after {
 			content: "";
 			display: block;
 			position: absolute;
@@ -294,7 +313,7 @@ function onDrop(ev) {
 			left: 0;
 			width: 100%;
 			height: 100%;
-			background: var(--focus);
+			background: var(--MI_THEME-focus);
 			opacity: 0.5;
 		}
 	}
@@ -312,37 +331,37 @@ function onDrop(ev) {
 	}
 
 	&.naked {
-		background: var(--acrylicBg) !important;
-		-webkit-backdrop-filter: var(--blur, blur(10px));
-		backdrop-filter: var(--blur, blur(10px));
+		background: var(--MI_THEME-acrylicBg) !important;
+		-webkit-backdrop-filter: var(--MI-blur, blur(10px));
+		backdrop-filter: var(--MI-blur, blur(10px));
 
 		> .header {
 			background: transparent;
 			box-shadow: none;
-			color: var(--fg);
+			color: var(--MI_THEME-fg);
 		}
 
 		> .body {
 			background: transparent !important;
+			scrollbar-color: var(--MI_THEME-scrollbarHandle) transparent;
 
 			&::-webkit-scrollbar-track {
 				background: transparent;
 			}
-			scrollbar-color: var(--scrollbarHandle) transparent;
 		}
 	}
 
 	&.paged {
-		background: var(--bg) !important;
+		background: var(--MI_THEME-bg) !important;
 
 		> .body {
-			background: var(--bg) !important;
+			background: var(--MI_THEME-bg) !important;
 			overflow-y: scroll !important;
+			scrollbar-color: var(--MI_THEME-scrollbarHandle) transparent;
 
 			&::-webkit-scrollbar-track {
 				background: inherit;
 			}
-			scrollbar-color: var(--scrollbarHandle) transparent;
 		}
 	}
 }
@@ -355,9 +374,9 @@ function onDrop(ev) {
 	height: var(--deckColumnHeaderHeight);
 	padding: 0 16px 0 30px;
 	font-size: 0.9em;
-	color: var(--panelHeaderFg);
-	background: var(--panelHeaderBg);
-	box-shadow: 0 1px 0 0 var(--panelHeaderDivider);
+	color: var(--MI_THEME-panelHeaderFg);
+	background: var(--MI_THEME-panelHeaderBg);
+	box-shadow: 0 1px 0 0 var(--MI_THEME-panelHeaderDivider);
 	cursor: pointer;
 	user-select: none;
 }
@@ -368,7 +387,7 @@ function onDrop(ev) {
 	left: 12px;
 	width: 3px;
 	height: calc(100% - 24px);
-	background: var(--accent);
+	background: var(--MI_THEME-accent);
 	border-radius: 999px;
 }
 
@@ -422,11 +441,11 @@ function onDrop(ev) {
 	overscroll-behavior-y: contain;
 	box-sizing: border-box;
 	container-type: size;
-	background-color: var(--bg);
+	background-color: var(--MI_THEME-bg);
+	scrollbar-color: var(--MI_THEME-scrollbarHandle) var(--MI_THEME-panel);
 
 	&::-webkit-scrollbar-track {
-		background: var(--panel);
+		background: var(--MI_THEME-panel);
 	}
-	scrollbar-color: var(--scrollbarHandle) var(--panel);
 }
 </style>
diff --git a/packages/frontend/src/ui/deck/deck-store.ts b/packages/frontend/src/ui/deck/deck-store.ts
index bb3c04cd5c..3186982349 100644
--- a/packages/frontend/src/ui/deck/deck-store.ts
+++ b/packages/frontend/src/ui/deck/deck-store.ts
@@ -6,6 +6,7 @@
 import { throttle } from 'throttle-debounce';
 import { markRaw } from 'vue';
 import { notificationTypes } from 'misskey-js';
+import type { BasicTimelineType } from '@/timelines.js';
 import { Storage } from '@/pizzax.js';
 import { misskeyApi } from '@/scripts/misskey-api.js';
 import { deepClone } from '@/scripts/clone.js';
@@ -17,9 +18,24 @@ type ColumnWidget = {
 	data: Record<string, any>;
 };
 
+export const columnTypes = [
+	'main',
+	'widgets',
+	'notifications',
+	'tl',
+	'antenna',
+	'list',
+	'channel',
+	'mentions',
+	'direct',
+	'roleTimeline',
+] as const;
+
+export type ColumnType = typeof columnTypes[number];
+
 export type Column = {
 	id: string;
-	type: 'main' | 'widgets' | 'notifications' | 'tl' | 'antenna' | 'channel' | 'list' | 'mentions' | 'direct';
+	type: ColumnType;
 	name: string | null;
 	width: number;
 	widgets?: ColumnWidget[];
@@ -30,9 +46,10 @@ export type Column = {
 	channelId?: string;
 	roleId?: string;
 	excludeTypes?: typeof notificationTypes[number][];
-	tl?: 'home' | 'local' | 'social' | 'global';
+	tl?: BasicTimelineType;
 	withRenotes?: boolean;
 	withReplies?: boolean;
+	withSensitive?: boolean;
 	onlyFiles?: boolean;
 	soundSetting: SoundStore;
 };
@@ -265,7 +282,7 @@ export function removeColumnWidget(id: Column['id'], widget: ColumnWidget) {
 	const columns = deepClone(deckStore.state.columns);
 	const columnIndex = deckStore.state.columns.findIndex(c => c.id === id);
 	const column = deepClone(deckStore.state.columns[columnIndex]);
-	if (column == null) return;
+	if (column == null || column.widgets == null) return;
 	column.widgets = column.widgets.filter(w => w.id !== widget.id);
 	columns[columnIndex] = column;
 	deckStore.set('columns', columns);
@@ -287,7 +304,7 @@ export function updateColumnWidget(id: Column['id'], widgetId: string, widgetDat
 	const columns = deepClone(deckStore.state.columns);
 	const columnIndex = deckStore.state.columns.findIndex(c => c.id === id);
 	const column = deepClone(deckStore.state.columns[columnIndex]);
-	if (column == null) return;
+	if (column == null || column.widgets == null) return;
 	column.widgets = column.widgets.map(w => w.id === widgetId ? {
 		...w,
 		data: widgetData,
diff --git a/packages/frontend/src/ui/deck/direct-column.vue b/packages/frontend/src/ui/deck/direct-column.vue
index e011de0e3b..d12a18f760 100644
--- a/packages/frontend/src/ui/deck/direct-column.vue
+++ b/packages/frontend/src/ui/deck/direct-column.vue
@@ -34,7 +34,7 @@ const tlComponent = ref<InstanceType<typeof MkNotes>>();
 
 function reloadTimeline() {
 	return new Promise<void>((res) => {
-		tlComponent.value.pagingComponent?.reload().then(() => {
+		tlComponent.value?.pagingComponent?.reload().then(() => {
 			res();
 		});
 	});
diff --git a/packages/frontend/src/ui/deck/list-column.vue b/packages/frontend/src/ui/deck/list-column.vue
index 5369112494..8bb8fe7225 100644
--- a/packages/frontend/src/ui/deck/list-column.vue
+++ b/packages/frontend/src/ui/deck/list-column.vue
@@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 -->
 
 <template>
-<XColumn :menu="menu" :column="column" :isStacked="isStacked" :refresher="() => timeline.reloadTimeline()">
+<XColumn :menu="menu" :column="column" :isStacked="isStacked" :refresher="async () => { await timeline?.reloadTimeline() }">
 	<template #header>
 		<i class="ti ti-list"></i><span style="margin-left: 8px;">{{ column.name }}</span>
 	</template>
@@ -15,14 +15,16 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <script lang="ts" setup>
 import { watch, shallowRef, ref } from 'vue';
+import type { entities as MisskeyEntities } from 'misskey-js';
 import XColumn from './column.vue';
 import { updateColumn, Column } from './deck-store.js';
 import MkTimeline from '@/components/MkTimeline.vue';
 import * as os from '@/os.js';
 import { misskeyApi } from '@/scripts/misskey-api.js';
 import { i18n } from '@/i18n.js';
-import { MenuItem } from '@/types/menu.js';
+import type { MenuItem } from '@/types/menu.js';
 import { SoundStore } from '@/store.js';
+import { userListsCache } from '@/cache.js';
 import { soundSettingsButton } from '@/ui/deck/tl-note-notification.js';
 import * as sound from '@/scripts/sound.js';
 
@@ -51,17 +53,38 @@ watch(soundSetting, v => {
 
 async function setList() {
 	const lists = await misskeyApi('users/lists/list');
-	const { canceled, result: list } = await os.select({
+	const { canceled, result: list } = await os.select<MisskeyEntities.UserList | '_CREATE_'>({
 		title: i18n.ts.selectList,
-		items: lists.map(x => ({
-			value: x, text: x.name,
-		})),
+		items: [
+			{ value: '_CREATE_', text: i18n.ts.createNew },
+			(lists.length > 0 ? {
+				sectionTitle: i18n.ts.createdLists,
+				items: lists.map(x => ({
+					value: x, text: x.name,
+				})),
+			} : undefined),
+		],
 		default: props.column.listId,
 	});
-	if (canceled) return;
-	updateColumn(props.column.id, {
-		listId: list.id,
-	});
+	if (canceled || list == null) return;
+
+	if (list === '_CREATE_') {
+		const { canceled, result: name } = await os.inputText({
+			title: i18n.ts.enterListName,
+		});
+		if (canceled || name == null || name === '') return;
+
+		const res = await os.apiWithDialog('users/lists/create', { name: name });
+		userListsCache.delete();
+
+		updateColumn(props.column.id, {
+			listId: res.id,
+		});
+	} else {
+		updateColumn(props.column.id, {
+			listId: list.id,
+		});
+	}
 }
 
 function editList() {
diff --git a/packages/frontend/src/ui/deck/main-column.vue b/packages/frontend/src/ui/deck/main-column.vue
index 79c9671917..f8c712c371 100644
--- a/packages/frontend/src/ui/deck/main-column.vue
+++ b/packages/frontend/src/ui/deck/main-column.vue
@@ -26,7 +26,8 @@ import * as os from '@/os.js';
 import { i18n } from '@/i18n.js';
 import { PageMetadata, provideMetadataReceiver, provideReactiveMetadata } from '@/scripts/page-metadata.js';
 import { useScrollPositionManager } from '@/nirax.js';
-import { getScrollContainer } from '@/scripts/scroll.js';
+import { getScrollContainer } from '@@/js/scroll.js';
+import { isLink } from '@@/js/is-link.js';
 import { mainRouter } from '@/router/main.js';
 
 defineProps<{
@@ -52,12 +53,6 @@ function back() {
 function onContextmenu(ev: MouseEvent) {
 	if (!ev.target) return;
 
-	const isLink = (el: HTMLElement) => {
-		if (el.tagName === 'A') return true;
-		if (el.parentElement) {
-			return isLink(el.parentElement);
-		}
-	};
 	if (isLink(ev.target as HTMLElement)) return;
 	if (['INPUT', 'TEXTAREA', 'IMG', 'VIDEO', 'CANVAS'].includes((ev.target as HTMLElement).tagName) || (ev.target as HTMLElement).attributes['contenteditable']) return;
 	if (window.getSelection()?.toString() !== '') return;
diff --git a/packages/frontend/src/ui/deck/mentions-column.vue b/packages/frontend/src/ui/deck/mentions-column.vue
index 81926dd7cb..7b25a55ec3 100644
--- a/packages/frontend/src/ui/deck/mentions-column.vue
+++ b/packages/frontend/src/ui/deck/mentions-column.vue
@@ -26,7 +26,7 @@ const tlComponent = ref<InstanceType<typeof MkNotes>>();
 
 function reloadTimeline() {
 	return new Promise<void>((res) => {
-		tlComponent.value.pagingComponent?.reload().then(() => {
+		tlComponent.value?.pagingComponent?.reload().then(() => {
 			res();
 		});
 	});
diff --git a/packages/frontend/src/ui/deck/notifications-column.vue b/packages/frontend/src/ui/deck/notifications-column.vue
index 451cc58791..19ccfc1f7c 100644
--- a/packages/frontend/src/ui/deck/notifications-column.vue
+++ b/packages/frontend/src/ui/deck/notifications-column.vue
@@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 -->
 
 <template>
-<XColumn :column="column" :isStacked="isStacked" :menu="menu" :refresher="() => notificationsComponent.reload()">
+<XColumn :column="column" :isStacked="isStacked" :menu="menu" :refresher="async () => { await notificationsComponent?.reload() }">
 	<template #header><i class="ti ti-bell" style="margin-right: 8px;"></i>{{ column.name }}</template>
 
 	<XNotifications ref="notificationsComponent" :excludeTypes="props.column.excludeTypes"/>
@@ -27,7 +27,7 @@ const props = defineProps<{
 const notificationsComponent = shallowRef<InstanceType<typeof XNotifications>>();
 
 function func() {
-	os.popup(defineAsyncComponent(() => import('@/components/MkNotificationSelectWindow.vue')), {
+	const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkNotificationSelectWindow.vue')), {
 		excludeTypes: props.column.excludeTypes,
 	}, {
 		done: async (res) => {
@@ -36,7 +36,8 @@ function func() {
 				excludeTypes: excludeTypes,
 			});
 		},
-	}, 'closed');
+		closed: () => dispose(),
+	});
 }
 
 const menu = [{
diff --git a/packages/frontend/src/ui/deck/role-timeline-column.vue b/packages/frontend/src/ui/deck/role-timeline-column.vue
index 32ab7527b4..beb4237978 100644
--- a/packages/frontend/src/ui/deck/role-timeline-column.vue
+++ b/packages/frontend/src/ui/deck/role-timeline-column.vue
@@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 -->
 
 <template>
-<XColumn :menu="menu" :column="column" :isStacked="isStacked" :refresher="() => timeline.reloadTimeline()">
+<XColumn :menu="menu" :column="column" :isStacked="isStacked" :refresher="async () => { await timeline?.reloadTimeline() }">
 	<template #header>
 		<i class="ti ti-badge"></i><span style="margin-left: 8px;">{{ column.name }}</span>
 	</template>
@@ -21,7 +21,7 @@ import MkTimeline from '@/components/MkTimeline.vue';
 import * as os from '@/os.js';
 import { misskeyApi } from '@/scripts/misskey-api.js';
 import { i18n } from '@/i18n.js';
-import { MenuItem } from '@/types/menu.js';
+import type { MenuItem } from '@/types/menu.js';
 import { SoundStore } from '@/store.js';
 import { soundSettingsButton } from '@/ui/deck/tl-note-notification.js';
 import * as sound from '@/scripts/sound.js';
@@ -53,7 +53,7 @@ async function setRole() {
 		})),
 		default: props.column.roleId,
 	});
-	if (canceled) return;
+	if (canceled || role == null) return;
 	updateColumn(props.column.id, {
 		roleId: role.id,
 	});
diff --git a/packages/frontend/src/ui/deck/tl-column.vue b/packages/frontend/src/ui/deck/tl-column.vue
index a967335edf..74c4fb504b 100644
--- a/packages/frontend/src/ui/deck/tl-column.vue
+++ b/packages/frontend/src/ui/deck/tl-column.vue
@@ -4,16 +4,13 @@ SPDX-License-Identifier: AGPL-3.0-only
 -->
 
 <template>
-<XColumn :menu="menu" :column="column" :isStacked="isStacked" :refresher="() => timeline.reloadTimeline()">
+<XColumn :menu="menu" :column="column" :isStacked="isStacked" :refresher="async () => { await timeline?.reloadTimeline() }">
 	<template #header>
-		<i v-if="column.tl === 'home'" class="ti ti-home"></i>
-		<i v-else-if="column.tl === 'local'" class="ti ti-planet"></i>
-		<i v-else-if="column.tl === 'social'" class="ti ti-universe"></i>
-		<i v-else-if="column.tl === 'global'" class="ti ti-whirl"></i>
+		<i v-if="column.tl != null" :class="basicTimelineIconClass(column.tl)"/>
 		<span style="margin-left: 8px;">{{ column.name }}</span>
 	</template>
 
-	<div v-if="(((column.tl === 'local' || column.tl === 'social') && !isLocalTimelineAvailable) || (column.tl === 'global' && !isGlobalTimelineAvailable))" :class="$style.disabled">
+	<div v-if="!isAvailableBasicTimeline(column.tl)" :class="$style.disabled">
 		<p :class="$style.disabledTitle">
 			<i class="ti ti-circle-minus"></i>
 			{{ i18n.ts._disabledTimeline.title }}
@@ -27,6 +24,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 		:src="column.tl"
 		:withRenotes="withRenotes"
 		:withReplies="withReplies"
+		:withSensitive="withSensitive"
 		:onlyFiles="onlyFiles"
 		@note="onNote"
 	/>
@@ -34,15 +32,15 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { onMounted, watch, ref, shallowRef } from 'vue';
+import { onMounted, watch, ref, shallowRef, computed } from 'vue';
 import XColumn from './column.vue';
 import { removeColumn, updateColumn, Column } from './deck-store.js';
+import type { MenuItem } from '@/types/menu.js';
 import MkTimeline from '@/components/MkTimeline.vue';
 import * as os from '@/os.js';
-import { $i } from '@/account.js';
 import { i18n } from '@/i18n.js';
+import { hasWithReplies, isAvailableBasicTimeline, basicTimelineIconClass } from '@/timelines.js';
 import { instance } from '@/instance.js';
-import { MenuItem } from '@/types/menu.js';
 import { SoundStore } from '@/store.js';
 import { soundSettingsButton } from '@/ui/deck/tl-note-notification.js';
 import * as sound from '@/scripts/sound.js';
@@ -52,14 +50,12 @@ const props = defineProps<{
 	isStacked: boolean;
 }>();
 
-const disabled = ref(false);
 const timeline = shallowRef<InstanceType<typeof MkTimeline>>();
 
-const isLocalTimelineAvailable = (($i == null && instance.policies.ltlAvailable) || ($i != null && $i.policies.ltlAvailable));
-const isGlobalTimelineAvailable = (($i == null && instance.policies.gtlAvailable) || ($i != null && $i.policies.gtlAvailable));
 const soundSetting = ref<SoundStore>(props.column.soundSetting ?? { type: null, volume: 1 });
 const withRenotes = ref(props.column.withRenotes ?? true);
 const withReplies = ref(props.column.withReplies ?? false);
+const withSensitive = ref(props.column.withSensitive ?? true);
 const onlyFiles = ref(props.column.onlyFiles ?? false);
 
 watch(withRenotes, v => {
@@ -74,6 +70,12 @@ watch(withReplies, v => {
 	});
 });
 
+watch(withSensitive, v => {
+	updateColumn(props.column.id, {
+		withSensitive: v,
+	});
+});
+
 watch(onlyFiles, v => {
 	updateColumn(props.column.id, {
 		onlyFiles: v,
@@ -87,10 +89,6 @@ watch(soundSetting, v => {
 onMounted(() => {
 	if (props.column.tl == null) {
 		setType();
-	} else if ($i) {
-		disabled.value = (
-			(!((instance.policies.ltlAvailable) || ($i.policies.ltlAvailable)) && ['local', 'social'].includes(props.column.tl)) ||
-			(!((instance.policies.gtlAvailable) || ($i.policies.gtlAvailable)) && ['global'].includes(props.column.tl)));
 	}
 });
 
@@ -113,8 +111,9 @@ async function setType() {
 		}
 		return;
 	}
+	if (src == null) return;
 	updateColumn(props.column.id, {
-		tl: src,
+		tl: src ?? undefined,
 	});
 }
 
@@ -122,29 +121,45 @@ function onNote() {
 	sound.playMisskeySfxFile(soundSetting.value);
 }
 
-const menu: MenuItem[] = [{
-	icon: 'ti ti-pencil',
-	text: i18n.ts.timeline,
-	action: setType,
-}, {
-	icon: 'ti ti-bell',
-	text: i18n.ts._deck.newNoteNotificationSettings,
-	action: () => soundSettingsButton(soundSetting),
-}, {
-	type: 'switch',
-	text: i18n.ts.showRenotes,
-	ref: withRenotes,
-}, props.column.tl === 'local' || props.column.tl === 'social' ? {
-	type: 'switch',
-	text: i18n.ts.showRepliesToOthersInTimeline,
-	ref: withReplies,
-	disabled: onlyFiles,
-} : undefined, {
-	type: 'switch',
-	text: i18n.ts.fileAttachedOnly,
-	ref: onlyFiles,
-	disabled: props.column.tl === 'local' || props.column.tl === 'social' ? withReplies : false,
-}];
+const menu = computed<MenuItem[]>(() => {
+	const menuItems: MenuItem[] = [];
+
+	menuItems.push({
+		icon: 'ti ti-pencil',
+		text: i18n.ts.timeline,
+		action: setType,
+	}, {
+		icon: 'ti ti-bell',
+		text: i18n.ts._deck.newNoteNotificationSettings,
+		action: () => soundSettingsButton(soundSetting),
+	}, {
+		type: 'switch',
+		text: i18n.ts.showRenotes,
+		ref: withRenotes,
+	});
+
+	if (hasWithReplies(props.column.tl)) {
+		menuItems.push({
+			type: 'switch',
+			text: i18n.ts.showRepliesToOthersInTimeline,
+			ref: withReplies,
+			disabled: onlyFiles,
+		});
+	}
+
+	menuItems.push({
+		type: 'switch',
+		text: i18n.ts.fileAttachedOnly,
+		ref: onlyFiles,
+		disabled: hasWithReplies(props.column.tl) ? withReplies : false,
+	}, {
+		type: 'switch',
+		text: i18n.ts.withSensitive,
+		ref: withSensitive,
+	});
+
+	return menuItems;
+});
 </script>
 
 <style lang="scss" module>
diff --git a/packages/frontend/src/ui/deck/widgets-column.vue b/packages/frontend/src/ui/deck/widgets-column.vue
index 9995996771..a0e62c8264 100644
--- a/packages/frontend/src/ui/deck/widgets-column.vue
+++ b/packages/frontend/src/ui/deck/widgets-column.vue
@@ -57,10 +57,10 @@ const menu = [{
 
 <style lang="scss" module>
 .root {
-	--margin: 8px;
-	--panelBorder: none;
+	--MI-margin: 8px;
+	--MI_THEME-panelBorder: none;
 
-	padding: 0 var(--margin);
+	padding: 0 var(--MI-margin);
 }
 
 .intro {
diff --git a/packages/frontend/src/ui/minimum.vue b/packages/frontend/src/ui/minimum.vue
index db5eb19c20..9e41c48c5b 100644
--- a/packages/frontend/src/ui/minimum.vue
+++ b/packages/frontend/src/ui/minimum.vue
@@ -17,7 +17,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { computed, provide, ref } from 'vue';
 import XCommon from './_common_/common.vue';
 import { PageMetadata, provideMetadataReceiver, provideReactiveMetadata } from '@/scripts/page-metadata.js';
-import { instanceName } from '@/config.js';
+import { instanceName } from '@@/js/config.js';
 import { mainRouter } from '@/router/main.js';
 
 const isRoot = computed(() => mainRouter.currentRoute.value.name === 'index');
diff --git a/packages/frontend/src/ui/universal.vue b/packages/frontend/src/ui/universal.vue
index 3cb6f598a6..d739c2e1cd 100644
--- a/packages/frontend/src/ui/universal.vue
+++ b/packages/frontend/src/ui/universal.vue
@@ -25,11 +25,11 @@ SPDX-License-Identifier: AGPL-3.0-only
 	<button v-if="(!isDesktop || pageMetadata?.needWideArea) && !isMobile" :class="$style.widgetButton" class="_button" @click="widgetsShowing = true"><i class="ti ti-apps"></i></button>
 
 	<div v-if="isMobile" ref="navFooter" :class="$style.nav">
-		<button :class="$style.navButton" class="_button" @click="drawerMenuShowing = true"><i :class="$style.navButtonIcon" class="ti ti-menu-2"></i><span v-if="menuIndicated" :class="$style.navButtonIndicator"><i class="_indicatorCircle"></i></span></button>
+		<button :class="$style.navButton" class="_button" @click="drawerMenuShowing = true"><i :class="$style.navButtonIcon" class="ti ti-menu-2"></i><span v-if="menuIndicated" :class="$style.navButtonIndicator" class="_blink"><i class="_indicatorCircle"></i></span></button>
 		<button :class="$style.navButton" class="_button" @click="isRoot ? top() : mainRouter.push('/')"><i :class="$style.navButtonIcon" class="ti ti-home"></i></button>
 		<button :class="$style.navButton" class="_button" @click="mainRouter.push('/my/notifications')">
 			<i :class="$style.navButtonIcon" class="ti ti-bell"></i>
-			<span v-if="$i?.hasUnreadNotification" :class="$style.navButtonIndicator">
+			<span v-if="$i?.hasUnreadNotification" :class="$style.navButtonIndicator" class="_blink">
 				<span class="_indicateCounter" :class="$style.itemIndicateValueIcon">{{ $i.unreadNotificationsCount > 99 ? '99+' : $i.unreadNotificationsCount }}</span>
 			</span>
 		</button>
@@ -96,9 +96,11 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <script lang="ts" setup>
 import { defineAsyncComponent, provide, onMounted, computed, ref, watch, shallowRef, Ref } from 'vue';
+import { instanceName } from '@@/js/config.js';
+import { CURRENT_STICKY_BOTTOM } from '@@/js/const.js';
+import { isLink } from '@@/js/is-link.js';
 import XCommon from './_common_/common.vue';
 import type MkStickyContainer from '@/components/global/MkStickyContainer.vue';
-import { instanceName } from '@/config.js';
 import XDrawerMenu from '@/ui/_common_/navbar-for-mobile.vue';
 import * as os from '@/os.js';
 import { defaultStore } from '@/store.js';
@@ -108,7 +110,6 @@ import { $i } from '@/account.js';
 import { PageMetadata, provideMetadataReceiver, provideReactiveMetadata } from '@/scripts/page-metadata.js';
 import { deviceKind } from '@/scripts/device-kind.js';
 import { miLocalStorage } from '@/local-storage.js';
-import { CURRENT_STICKY_BOTTOM } from '@/const.js';
 import { useScrollPositionManager } from '@/nirax.js';
 import { mainRouter } from '@/router/main.js';
 
@@ -195,12 +196,6 @@ onMounted(() => {
 });
 
 const onContextmenu = (ev) => {
-	const isLink = (el: HTMLElement) => {
-		if (el.tagName === 'A') return true;
-		if (el.parentElement) {
-			return isLink(el.parentElement);
-		}
-	};
 	if (isLink(ev.target)) return;
 	if (['INPUT', 'TEXTAREA', 'IMG', 'VIDEO', 'CANVAS'].includes(ev.target.tagName) || ev.target.attributes['contenteditable']) return;
 	if (window.getSelection()?.toString() !== '') return;
@@ -230,12 +225,12 @@ provide<Ref<number>>(CURRENT_STICKY_BOTTOM, navFooterHeight);
 watch(navFooter, () => {
 	if (navFooter.value) {
 		navFooterHeight.value = navFooter.value.offsetHeight;
-		document.body.style.setProperty('--stickyBottom', `${navFooterHeight.value}px`);
-		document.body.style.setProperty('--minBottomSpacing', 'var(--minBottomSpacingMobile)');
+		document.body.style.setProperty('--MI-stickyBottom', `${navFooterHeight.value}px`);
+		document.body.style.setProperty('--MI-minBottomSpacing', 'var(--MI-minBottomSpacingMobile)');
 	} else {
 		navFooterHeight.value = 0;
-		document.body.style.setProperty('--stickyBottom', '0px');
-		document.body.style.setProperty('--minBottomSpacing', '0px');
+		document.body.style.setProperty('--MI-stickyBottom', '0px');
+		document.body.style.setProperty('--MI-minBottomSpacing', '0px');
 	}
 }, {
 	immediate: true,
@@ -323,7 +318,7 @@ $widgets-hide-threshold: 1090px;
 }
 
 .sidebar {
-	border-right: solid 0.5px var(--divider);
+	border-right: solid 0.5px var(--MI_THEME-divider);
 }
 
 .contents {
@@ -333,7 +328,7 @@ $widgets-hide-threshold: 1090px;
 	overflow: auto;
 	overflow-y: scroll;
 	overscroll-behavior: contain;
-	background: var(--bg);
+	background: var(--MI_THEME-bg);
 }
 
 .widgets {
@@ -341,9 +336,9 @@ $widgets-hide-threshold: 1090px;
 	height: 100%;
 	box-sizing: border-box;
 	overflow: auto;
-	padding: var(--margin) var(--margin) calc(var(--margin) + env(safe-area-inset-bottom, 0px));
-	border-left: solid 0.5px var(--divider);
-	background: var(--bg);
+	padding: var(--MI-margin) var(--MI-margin) calc(var(--MI-margin) + env(safe-area-inset-bottom, 0px));
+	border-left: solid 0.5px var(--MI_THEME-divider);
+	background: var(--MI_THEME-bg);
 
 	@media (max-width: $widgets-hide-threshold) {
 		display: none;
@@ -361,7 +356,7 @@ $widgets-hide-threshold: 1090px;
 	border-radius: 100%;
 	box-shadow: 0 3px 5px -1px rgba(0, 0, 0, 0.2), 0 6px 10px 0 rgba(0, 0, 0, 0.14), 0 1px 18px 0 rgba(0, 0, 0, 0.12);
 	font-size: 22px;
-	background: var(--panel);
+	background: var(--MI_THEME-panel);
 }
 
 .widgetsDrawerBg {
@@ -375,11 +370,11 @@ $widgets-hide-threshold: 1090px;
 	z-index: 1001;
 	width: 310px;
 	height: 100dvh;
-	padding: var(--margin) var(--margin) calc(var(--margin) + env(safe-area-inset-bottom, 0px)) !important;
+	padding: var(--MI-margin) var(--MI-margin) calc(var(--MI-margin) + env(safe-area-inset-bottom, 0px)) !important;
 	box-sizing: border-box;
 	overflow: auto;
 	overscroll-behavior: contain;
-	background: var(--bg);
+	background: var(--MI_THEME-bg);
 }
 
 .widgetsCloseButton {
@@ -405,10 +400,10 @@ $widgets-hide-threshold: 1090px;
 	grid-gap: 8px;
 	width: 100%;
 	box-sizing: border-box;
-	-webkit-backdrop-filter: var(--blur, blur(24px));
-	backdrop-filter: var(--blur, blur(24px));
-	background-color: var(--header);
-	border-top: solid 0.5px var(--divider);
+	-webkit-backdrop-filter: var(--MI-blur, blur(24px));
+	backdrop-filter: var(--MI-blur, blur(24px));
+	background-color: var(--MI_THEME-header);
+	border-top: solid 0.5px var(--MI_THEME-divider);
 }
 
 .navButton {
@@ -419,29 +414,29 @@ $widgets-hide-threshold: 1090px;
 	max-width: 60px;
 	margin: auto;
 	border-radius: 100%;
-	background: var(--panel);
-	color: var(--fg);
+	background: var(--MI_THEME-panel);
+	color: var(--MI_THEME-fg);
 
 	&:hover {
-		background: var(--panelHighlight);
+		background: var(--MI_THEME-panelHighlight);
 	}
 
 	&:active {
-		background: var(--X2);
+		background: hsl(from var(--MI_THEME-panel) h s calc(l - 2));
 	}
 }
 
 .postButton {
 	composes: navButton;
-	background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB));
-	color: var(--fgOnAccent);
+	background: linear-gradient(90deg, var(--MI_THEME-buttonGradateA), var(--MI_THEME-buttonGradateB));
+	color: var(--MI_THEME-fgOnAccent);
 
 	&:hover {
-		background: linear-gradient(90deg, var(--X8), var(--X8));
+		background: linear-gradient(90deg, hsl(from var(--MI_THEME-accent) h s calc(l + 5)), hsl(from var(--MI_THEME-accent) h s calc(l + 5)));
 	}
 
 	&:active {
-		background: linear-gradient(90deg, var(--X8), var(--X8));
+		background: linear-gradient(90deg, hsl(from var(--MI_THEME-accent) h s calc(l + 5)), hsl(from var(--MI_THEME-accent) h s calc(l + 5)));
 	}
 }
 
@@ -454,9 +449,8 @@ $widgets-hide-threshold: 1090px;
 	position: absolute;
 	top: 0;
 	left: 0;
-	color: var(--indicator);
+	color: var(--MI_THEME-indicator);
 	font-size: 16px;
-	animation: global-blink 1s infinite;
 
 	&:has(.itemIndicateValueIcon) {
 		animation: none;
@@ -479,7 +473,7 @@ $widgets-hide-threshold: 1090px;
 	contain: strict;
 	overflow: auto;
 	overscroll-behavior: contain;
-	background: var(--navBg);
+	background: var(--MI_THEME-navBg);
 }
 
 .statusbars {
@@ -489,6 +483,6 @@ $widgets-hide-threshold: 1090px;
 }
 
 .spacer {
-	height: calc(var(--minBottomSpacing));
+	height: calc(var(--MI-minBottomSpacing));
 }
 </style>
diff --git a/packages/frontend/src/ui/visitor.vue b/packages/frontend/src/ui/visitor.vue
index 80623083cf..7d8677e3be 100644
--- a/packages/frontend/src/ui/visitor.vue
+++ b/packages/frontend/src/ui/visitor.vue
@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <template>
 <div class="mk-app">
-	<a v-if="isRoot" href="https://github.com/misskey-dev/misskey" target="_blank" class="github-corner" aria-label="View source on GitHub"><svg width="80" height="80" viewBox="0 0 250 250" style="fill:var(--panel); color:var(--fg); position: fixed; z-index: 10; top: 0; border: 0; right: 0;" aria-hidden="true"><path d="M0,0 L115,115 L130,115 L142,142 L250,250 L250,0 Z"></path><path d="M128.3,109.0 C113.8,99.7 119.0,89.6 119.0,89.6 C122.0,82.7 120.5,78.6 120.5,78.6 C119.2,72.0 123.4,76.3 123.4,76.3 C127.3,80.9 125.5,87.3 125.5,87.3 C122.9,97.6 130.6,101.9 134.4,103.2" fill="currentColor" style="transform-origin: 130px 106px;" class="octo-arm"></path><path d="M115.0,115.0 C114.9,115.1 118.7,116.5 119.8,115.4 L133.7,101.6 C136.9,99.2 139.9,98.4 142.2,98.6 C133.8,88.0 127.5,74.4 143.8,58.0 C148.5,53.4 154.0,51.2 159.7,51.0 C160.3,49.4 163.2,43.6 171.4,40.1 C171.4,40.1 176.1,42.5 178.8,56.2 C183.1,58.6 187.2,61.8 190.9,65.4 C194.5,69.0 197.7,73.2 200.1,77.6 C213.8,80.2 216.3,84.9 216.3,84.9 C212.7,93.1 206.9,96.0 205.4,96.6 C205.1,102.4 203.0,107.8 198.3,112.5 C181.9,128.9 168.3,122.5 157.7,114.1 C157.9,116.9 156.7,120.9 152.7,124.9 L141.0,136.5 C139.8,137.7 141.6,141.9 141.8,141.8 Z" fill="currentColor" class="octo-body"></path></svg></a>
+	<a v-if="isRoot" href="https://github.com/misskey-dev/misskey" target="_blank" class="github-corner" aria-label="View source on GitHub"><svg width="80" height="80" viewBox="0 0 250 250" style="fill:var(--MI_THEME-panel); color:var(--MI_THEME-fg); position: fixed; z-index: 10; top: 0; border: 0; right: 0;" aria-hidden="true"><path d="M0,0 L115,115 L130,115 L142,142 L250,250 L250,0 Z"></path><path d="M128.3,109.0 C113.8,99.7 119.0,89.6 119.0,89.6 C122.0,82.7 120.5,78.6 120.5,78.6 C119.2,72.0 123.4,76.3 123.4,76.3 C127.3,80.9 125.5,87.3 125.5,87.3 C122.9,97.6 130.6,101.9 134.4,103.2" fill="currentColor" style="transform-origin: 130px 106px;" class="octo-arm"></path><path d="M115.0,115.0 C114.9,115.1 118.7,116.5 119.8,115.4 L133.7,101.6 C136.9,99.2 139.9,98.4 142.2,98.6 C133.8,88.0 127.5,74.4 143.8,58.0 C148.5,53.4 154.0,51.2 159.7,51.0 C160.3,49.4 163.2,43.6 171.4,40.1 C171.4,40.1 176.1,42.5 178.8,56.2 C183.1,58.6 187.2,61.8 190.9,65.4 C194.5,69.0 197.7,73.2 200.1,77.6 C213.8,80.2 216.3,84.9 216.3,84.9 C212.7,93.1 206.9,96.0 205.4,96.6 C205.1,102.4 203.0,107.8 198.3,112.5 C181.9,128.9 168.3,122.5 157.7,114.1 C157.9,116.9 156.7,120.9 152.7,124.9 L141.0,136.5 C139.8,137.7 141.6,141.9 141.8,141.8 Z" fill="currentColor" class="octo-body"></path></svg></a>
 
 	<div v-if="!narrow && !isRoot" class="side">
 		<div class="banner" :style="{ backgroundImage: instance.backgroundImageUrl ? `url(${ instance.backgroundImageUrl })` : 'none' }"></div>
@@ -71,7 +71,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 <script lang="ts" setup>
 import { onMounted, provide, ref, computed } from 'vue';
 import XCommon from './_common_/common.vue';
-import { instanceName } from '@/config.js';
+import { instanceName } from '@@/js/config.js';
 import * as os from '@/os.js';
 import { instance } from '@/instance.js';
 import XSigninDialog from '@/components/MkSigninDialog.vue';
@@ -126,15 +126,19 @@ const keymap = computed(() => {
 });
 
 function signin() {
-	os.popup(XSigninDialog, {
+	const { dispose } = os.popup(XSigninDialog, {
 		autoSet: true,
-	}, {}, 'closed');
+	}, {
+		closed: () => dispose(),
+	});
 }
 
 function signup() {
-	os.popup(XSignupDialog, {
+	const { dispose } = os.popup(XSignupDialog, {
 		autoSet: true,
-	}, {}, 'closed');
+	}, {
+		closed: () => dispose(),
+	});
 }
 
 onMounted(() => {
@@ -187,7 +191,7 @@ defineExpose({
 		left: 0;
 		width: 500px;
 		height: 100vh;
-		background: var(--accent);
+		background: var(--MI_THEME-accent);
 
 		> .banner {
 			position: absolute;
@@ -215,7 +219,7 @@ defineExpose({
 		min-width: 0;
 
 		> .header {
-			background: var(--panel);
+			background: var(--MI_THEME-panel);
 
 			> .wide {
 				line-height: 50px;
@@ -250,7 +254,7 @@ defineExpose({
 		left: 0;
 		width: 240px;
 		height: 100vh;
-		background: var(--panel);
+		background: var(--MI_THEME-panel);
 
 		> .link {
 			display: block;
@@ -264,7 +268,7 @@ defineExpose({
 		> .divider {
 			margin: 8px auto;
 			width: calc(100% - 32px);
-			border-top: solid 0.5px var(--divider);
+			border-top: solid 0.5px var(--MI_THEME-divider);
 		}
 
 		> .action {
@@ -279,7 +283,7 @@ defineExpose({
 				border-radius: 999px;
 
 				&._button {
-					background: var(--panel);
+					background: var(--MI_THEME-panel);
 				}
 
 				&:first-child {
diff --git a/packages/frontend/src/ui/zen.vue b/packages/frontend/src/ui/zen.vue
index bb8cffaf52..1f73b5fcaf 100644
--- a/packages/frontend/src/ui/zen.vue
+++ b/packages/frontend/src/ui/zen.vue
@@ -25,7 +25,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { computed, provide, ref } from 'vue';
 import XCommon from './_common_/common.vue';
 import { PageMetadata, provideMetadataReceiver, provideReactiveMetadata } from '@/scripts/page-metadata.js';
-import { instanceName, ui } from '@/config.js';
+import { instanceName, ui } from '@@/js/config.js';
 import { i18n } from '@/i18n.js';
 import { mainRouter } from '@/router/main.js';
 
@@ -63,12 +63,12 @@ document.documentElement.style.overflowY = 'scroll';
 }
 
 .rootWithBottom {
-	min-height: calc(100dvh - (60px + (var(--margin) * 2) + env(safe-area-inset-bottom, 0px)));
+	min-height: calc(100dvh - (60px + (var(--MI-margin) * 2) + env(safe-area-inset-bottom, 0px)));
 	box-sizing: border-box;
 }
 
 .bottom {
-	height: calc(60px + (var(--margin) * 2) + env(safe-area-inset-bottom, 0px));
+	height: calc(60px + (var(--MI-margin) * 2) + env(safe-area-inset-bottom, 0px));
 	width: 100%;
 	margin-top: auto;
 }
@@ -81,9 +81,9 @@ document.documentElement.style.overflowY = 'scroll';
 	max-width: 60px;
 	margin: auto;
 	border-radius: 100%;
-	background: var(--panel);
-	color: var(--fg);
-	right: var(--margin);
-	bottom: calc(var(--margin) + env(safe-area-inset-bottom, 0px));
+	background: var(--MI_THEME-panel);
+	color: var(--MI_THEME-fg);
+	right: var(--MI-margin);
+	bottom: calc(var(--MI-margin) + env(safe-area-inset-bottom, 0px));
 }
 </style>
diff --git a/packages/frontend/src/widgets/WidgetAiscript.vue b/packages/frontend/src/widgets/WidgetAiscript.vue
index a74483e85e..cf7e9c5a3e 100644
--- a/packages/frontend/src/widgets/WidgetAiscript.vue
+++ b/packages/frontend/src/widgets/WidgetAiscript.vue
@@ -126,10 +126,10 @@ defineExpose<WidgetComponentExpose>({
 		max-width: 100%;
 		min-width: 100%;
 		padding: 16px;
-		color: var(--fg);
+		color: var(--MI_THEME-fg);
 		background: transparent;
 		border: none;
-		border-bottom: solid 0.5px var(--divider);
+		border-bottom: solid 0.5px var(--MI_THEME-divider);
 		border-radius: 0;
 		box-sizing: border-box;
 		font: inherit;
@@ -154,7 +154,7 @@ defineExpose<WidgetComponentExpose>({
 	}
 
 	> .logs {
-		border-top: solid 0.5px var(--divider);
+		border-top: solid 0.5px var(--MI_THEME-divider);
 		text-align: left;
 		padding: 16px;
 
diff --git a/packages/frontend/src/widgets/WidgetBirthdayFollowings.vue b/packages/frontend/src/widgets/WidgetBirthdayFollowings.vue
index 49fd103d37..c2bda85ac7 100644
--- a/packages/frontend/src/widgets/WidgetBirthdayFollowings.vue
+++ b/packages/frontend/src/widgets/WidgetBirthdayFollowings.vue
@@ -25,11 +25,11 @@ SPDX-License-Identifier: AGPL-3.0-only
 <script lang="ts" setup>
 import { ref } from 'vue';
 import * as Misskey from 'misskey-js';
+import { useInterval } from '@@/js/use-interval.js';
 import { useWidgetPropsManager, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
 import { GetFormResultType } from '@/scripts/form.js';
 import MkContainer from '@/components/MkContainer.vue';
 import { misskeyApi } from '@/scripts/misskey-api.js';
-import { useInterval } from '@/scripts/use-interval.js';
 import { i18n } from '@/i18n.js';
 import { infoImageUrl } from '@/instance.js';
 import { $i } from '@/account.js';
@@ -115,7 +115,7 @@ defineExpose<WidgetComponentExpose>({
 <style lang="scss" module>
 .bdayFRoot {
 	overflow: hidden;
-	min-height: calc(calc(calc(50px * 3) - 8px) + calc(var(--margin) * 2));
+	min-height: calc(calc(calc(50px * 3) - 8px) + calc(var(--MI-margin) * 2));
 }
 .bdayFGrid {
 	display: grid;
@@ -123,7 +123,7 @@ defineExpose<WidgetComponentExpose>({
 	grid-template-rows: repeat(3, 42px);
 	place-content: center;
 	gap: 8px;
-	margin: var(--margin) auto;
+	margin: var(--MI-margin) auto;
 }
 
 .bdayFFallback {
@@ -139,6 +139,6 @@ defineExpose<WidgetComponentExpose>({
 	width: auto;
 	max-width: 90%;
 	margin-bottom: 8px;
-	border-radius: var(--radius);
+	border-radius: var(--MI-radius);
 }
 </style>
diff --git a/packages/frontend/src/widgets/WidgetCalendar.vue b/packages/frontend/src/widgets/WidgetCalendar.vue
index c688e8a0b1..2443e40789 100644
--- a/packages/frontend/src/widgets/WidgetCalendar.vue
+++ b/packages/frontend/src/widgets/WidgetCalendar.vue
@@ -42,7 +42,7 @@ import { ref } from 'vue';
 import { useWidgetPropsManager, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
 import { GetFormResultType } from '@/scripts/form.js';
 import { i18n } from '@/i18n.js';
-import { useInterval } from '@/scripts/use-interval.js';
+import { useInterval } from '@@/js/use-interval.js';
 
 const name = 'calendar';
 
@@ -121,7 +121,7 @@ defineExpose<WidgetComponentExpose>({
 .root {
 	padding: 16px 0;
 
-	&:after {
+	&::after {
 		content: "";
 		display: block;
 		clear: both;
@@ -207,7 +207,7 @@ defineExpose<WidgetComponentExpose>({
 .meter {
 	width: 100%;
 	overflow: hidden;
-	background: var(--X11);
+	background: var(--MI_THEME-X11);
 	border-radius: 8px;
 }
 
diff --git a/packages/frontend/src/widgets/WidgetFederation.vue b/packages/frontend/src/widgets/WidgetFederation.vue
index ed907de9b8..4a10a612e2 100644
--- a/packages/frontend/src/widgets/WidgetFederation.vue
+++ b/packages/frontend/src/widgets/WidgetFederation.vue
@@ -32,7 +32,7 @@ import { GetFormResultType } from '@/scripts/form.js';
 import MkContainer from '@/components/MkContainer.vue';
 import MkMiniChart from '@/components/MkMiniChart.vue';
 import { misskeyApi, misskeyApiGet } from '@/scripts/misskey-api.js';
-import { useInterval } from '@/scripts/use-interval.js';
+import { useInterval } from '@@/js/use-interval.js';
 import { i18n } from '@/i18n.js';
 import { getProxiedImageUrlNullable } from '@/scripts/media-proxy.js';
 import { defaultStore } from '@/store.js';
@@ -105,7 +105,7 @@ defineExpose<WidgetComponentExpose>({
 			display: flex;
 			align-items: center;
 			padding: 14px 16px;
-			border-bottom: solid 0.5px var(--divider);
+			border-bottom: solid 0.5px var(--MI_THEME-divider);
 
 			> img {
 				display: block;
@@ -120,7 +120,7 @@ defineExpose<WidgetComponentExpose>({
 				flex: 1;
 				overflow: hidden;
 				font-size: 0.9em;
-				color: var(--fg);
+				color: var(--MI_THEME-fg);
 				padding-right: 8px;
 
 				> .a {
diff --git a/packages/frontend/src/widgets/WidgetInstanceCloud.vue b/packages/frontend/src/widgets/WidgetInstanceCloud.vue
index 76ccdb3971..d090372b9a 100644
--- a/packages/frontend/src/widgets/WidgetInstanceCloud.vue
+++ b/packages/frontend/src/widgets/WidgetInstanceCloud.vue
@@ -26,7 +26,7 @@ import MkContainer from '@/components/MkContainer.vue';
 import MkTagCloud from '@/components/MkTagCloud.vue';
 import * as os from '@/os.js';
 import { misskeyApi } from '@/scripts/misskey-api.js';
-import { useInterval } from '@/scripts/use-interval.js';
+import { useInterval } from '@@/js/use-interval.js';
 import { getProxiedImageUrlNullable } from '@/scripts/media-proxy.js';
 
 const name = 'instanceCloud';
diff --git a/packages/frontend/src/widgets/WidgetInstanceInfo.vue b/packages/frontend/src/widgets/WidgetInstanceInfo.vue
index 25d824c8ae..ec12aa265c 100644
--- a/packages/frontend/src/widgets/WidgetInstanceInfo.vue
+++ b/packages/frontend/src/widgets/WidgetInstanceInfo.vue
@@ -22,7 +22,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 <script lang="ts" setup>
 import { useWidgetPropsManager, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
 import { GetFormResultType } from '@/scripts/form.js';
-import { host } from '@/config.js';
+import { host } from '@@/js/config.js';
 import { instance } from '@/instance.js';
 
 const name = 'instanceInfo';
@@ -81,16 +81,19 @@ defineExpose<WidgetComponentExpose>({
 .body {
 	text-overflow: ellipsis;
 	overflow: clip;
+	margin-left: -10px;
+	padding: 10px;
 }
 
 .name {
 	color: #fff;
-	filter: drop-shadow(0 0 4px #000);
+	filter: drop-shadow(0 0 4px #000) drop-shadow(0 0 0.1px rgba(0, 0, 0, 0.5));
 	font-weight: bold;
 }
 
 .host {
 	color: #fff;
-	filter: drop-shadow(0 0 4px #000);
+	filter: drop-shadow(0 0 4px #000) drop-shadow(0 0 0.1px rgba(0, 0, 0, 0.5));
+
 }
 </style>
diff --git a/packages/frontend/src/widgets/WidgetJobQueue.vue b/packages/frontend/src/widgets/WidgetJobQueue.vue
index edf6622a13..0ee6b863dc 100644
--- a/packages/frontend/src/widgets/WidgetJobQueue.vue
+++ b/packages/frontend/src/widgets/WidgetJobQueue.vue
@@ -173,14 +173,14 @@ defineExpose<WidgetComponentExpose>({
 		padding: 16px;
 
 		&:not(:first-child) {
-			border-top: solid 0.5px var(--divider);
+			border-top: solid 0.5px var(--MI_THEME-divider);
 		}
 
 		> .label {
 			display: flex;
 
 			> .icon {
-				color: var(--warn);
+				color: var(--MI_THEME-warn);
 				margin-left: auto;
 				animation: warnBlink 1s infinite;
 			}
@@ -198,11 +198,11 @@ defineExpose<WidgetComponentExpose>({
 
 				> div:last-child {
 					&.inc {
-						color: var(--warn);
+						color: var(--MI_THEME-warn);
 					}
 
 					&.dec {
-						color: var(--success);
+						color: var(--MI_THEME-success);
 					}
 				}
 			}
diff --git a/packages/frontend/src/widgets/WidgetMemo.vue b/packages/frontend/src/widgets/WidgetMemo.vue
index 7ee83157c6..7b179ce703 100644
--- a/packages/frontend/src/widgets/WidgetMemo.vue
+++ b/packages/frontend/src/widgets/WidgetMemo.vue
@@ -84,10 +84,10 @@ defineExpose<WidgetComponentExpose>({
 	max-width: 100%;
 	min-width: 100%;
 	padding: 16px;
-	color: var(--fg);
+	color: var(--MI_THEME-fg);
 	background: transparent;
 	border: none;
-	border-bottom: solid 0.5px var(--divider);
+	border-bottom: solid 0.5px var(--MI_THEME-divider);
 	border-radius: 0;
 	box-sizing: border-box;
 	font: inherit;
diff --git a/packages/frontend/src/widgets/WidgetNotifications.vue b/packages/frontend/src/widgets/WidgetNotifications.vue
index 4b3265dab7..773c078b49 100644
--- a/packages/frontend/src/widgets/WidgetNotifications.vue
+++ b/packages/frontend/src/widgets/WidgetNotifications.vue
@@ -54,7 +54,7 @@ const { widgetProps, configure, save } = useWidgetPropsManager(name,
 );
 
 const configureNotification = () => {
-	os.popup(defineAsyncComponent(() => import('@/components/MkNotificationSelectWindow.vue')), {
+	const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkNotificationSelectWindow.vue')), {
 		excludeTypes: widgetProps.excludeTypes,
 	}, {
 		done: async (res) => {
@@ -62,7 +62,8 @@ const configureNotification = () => {
 			widgetProps.excludeTypes = excludeTypes;
 			save();
 		},
-	}, 'closed');
+		closed: () => dispose(),
+	});
 };
 
 defineExpose<WidgetComponentExpose>({
diff --git a/packages/frontend/src/widgets/WidgetOnlineUsers.vue b/packages/frontend/src/widgets/WidgetOnlineUsers.vue
index 5c89a06c62..d8c4e259c8 100644
--- a/packages/frontend/src/widgets/WidgetOnlineUsers.vue
+++ b/packages/frontend/src/widgets/WidgetOnlineUsers.vue
@@ -18,7 +18,7 @@ import { ref } from 'vue';
 import { useWidgetPropsManager, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
 import { GetFormResultType } from '@/scripts/form.js';
 import { misskeyApi, misskeyApiGet } from '@/scripts/misskey-api.js';
-import { useInterval } from '@/scripts/use-interval.js';
+import { useInterval } from '@@/js/use-interval.js';
 import { i18n } from '@/i18n.js';
 import number from '@/filters/number.js';
 
@@ -72,6 +72,6 @@ defineExpose<WidgetComponentExpose>({
 }
 
 .text {
-	color: var(--fgTransparentWeak);
+	color: var(--MI_THEME-fgTransparentWeak);
 }
 </style>
diff --git a/packages/frontend/src/widgets/WidgetPhotos.vue b/packages/frontend/src/widgets/WidgetPhotos.vue
index 34be8c5e57..40e2d8fbc7 100644
--- a/packages/frontend/src/widgets/WidgetPhotos.vue
+++ b/packages/frontend/src/widgets/WidgetPhotos.vue
@@ -68,10 +68,10 @@ const onDriveFileCreated = (file) => {
 	}
 };
 
-const thumbnail = (image: any): string => {
+const thumbnail = (image: Misskey.entities.DriveFile): string => {
 	return defaultStore.state.disableShowingAnimatedImages
 		? getStaticImageUrl(image.url)
-		: image.thumbnailUrl;
+		: image.thumbnailUrl ?? image.url;
 };
 
 misskeyApi('drive/stream', {
diff --git a/packages/frontend/src/widgets/WidgetProfile.vue b/packages/frontend/src/widgets/WidgetProfile.vue
index a5578d4de6..ae39098305 100644
--- a/packages/frontend/src/widgets/WidgetProfile.vue
+++ b/packages/frontend/src/widgets/WidgetProfile.vue
@@ -82,16 +82,19 @@ defineExpose<WidgetComponentExpose>({
 .body {
 	text-overflow: ellipsis;
 	overflow: clip;
+	margin-left: -10px;
+	padding: 10px;
 }
 
 .name {
 	color: #fff;
-	filter: drop-shadow(0 0 4px #000);
+	filter: drop-shadow(0 0 4px #000) drop-shadow(0 0 0.1px rgba(0, 0, 0, 0.5));
 	font-weight: bold;
 }
 
 .username {
 	color: #fff;
-	filter: drop-shadow(0 0 4px #000);
+	filter: drop-shadow(0 0 4px #000) drop-shadow(0 0 0.1px rgba(0, 0, 0, 0.5));
+	font-weight: normal;
 }
 </style>
diff --git a/packages/frontend/src/widgets/WidgetRss.vue b/packages/frontend/src/widgets/WidgetRss.vue
index e5758662cc..92dc6d148e 100644
--- a/packages/frontend/src/widgets/WidgetRss.vue
+++ b/packages/frontend/src/widgets/WidgetRss.vue
@@ -28,9 +28,9 @@ import * as Misskey from 'misskey-js';
 import { useWidgetPropsManager, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
 import { GetFormResultType } from '@/scripts/form.js';
 import MkContainer from '@/components/MkContainer.vue';
-import { url as base } from '@/config.js';
+import { url as base } from '@@/js/config.js';
 import { i18n } from '@/i18n.js';
-import { useInterval } from '@/scripts/use-interval.js';
+import { useInterval } from '@@/js/use-interval.js';
 import { infoImageUrl } from '@/instance.js';
 
 const name = 'rss';
@@ -70,7 +70,7 @@ const items = computed(() => rawItems.value.slice(0, widgetProps.maxEntries));
 const fetching = ref(true);
 const fetchEndpoint = computed(() => {
 	const url = new URL('/api/fetch-rss', base);
-	url.searchParams.set('url', widgetProps.url);
+	url.searchParams.set('url', encodeURIComponent(widgetProps.url));
 	return url;
 });
 const intervalClear = ref<(() => void) | undefined>();
@@ -113,7 +113,7 @@ defineExpose<WidgetComponentExpose>({
 .item {
 	display: block;
 	padding: 8px 16px;
-	color: var(--fg);
+	color: var(--MI_THEME-fg);
 	white-space: nowrap;
 	text-overflow: ellipsis;
 	overflow: hidden;
diff --git a/packages/frontend/src/widgets/WidgetRssTicker.vue b/packages/frontend/src/widgets/WidgetRssTicker.vue
index 16306ef5ba..6957878572 100644
--- a/packages/frontend/src/widgets/WidgetRssTicker.vue
+++ b/packages/frontend/src/widgets/WidgetRssTicker.vue
@@ -34,8 +34,8 @@ import MarqueeText from '@/components/MkMarquee.vue';
 import { GetFormResultType } from '@/scripts/form.js';
 import MkContainer from '@/components/MkContainer.vue';
 import { shuffle } from '@/scripts/shuffle.js';
-import { url as base } from '@/config.js';
-import { useInterval } from '@/scripts/use-interval.js';
+import { url as base } from '@@/js/config.js';
+import { useInterval } from '@@/js/use-interval.js';
 
 const name = 'rssTicker';
 
@@ -99,7 +99,7 @@ const items = computed(() => {
 const fetching = ref(true);
 const fetchEndpoint = computed(() => {
 	const url = new URL('/api/fetch-rss', base);
-	url.searchParams.set('url', widgetProps.url);
+	url.searchParams.set('url', encodeURIComponent(widgetProps.url));
 	return url;
 });
 const intervalClear = ref<(() => void) | undefined>();
@@ -171,7 +171,7 @@ defineExpose<WidgetComponentExpose>({
 	display: inline-flex;
 	align-items: center;
 	vertical-align: bottom;
-	color: var(--fg);
+	color: var(--MI_THEME-fg);
 }
 
 .divider {
@@ -179,6 +179,6 @@ defineExpose<WidgetComponentExpose>({
 	width: 0.5px;
 	height: 16px;
 	margin: 0 1em;
-	background: var(--divider);
+	background: var(--MI_THEME-divider);
 }
 </style>
diff --git a/packages/frontend/src/widgets/WidgetSlideshow.vue b/packages/frontend/src/widgets/WidgetSlideshow.vue
index b8efd3bda9..3fea1d7053 100644
--- a/packages/frontend/src/widgets/WidgetSlideshow.vue
+++ b/packages/frontend/src/widgets/WidgetSlideshow.vue
@@ -23,7 +23,7 @@ import { useWidgetPropsManager, WidgetComponentEmits, WidgetComponentExpose, Wid
 import { GetFormResultType } from '@/scripts/form.js';
 import * as os from '@/os.js';
 import { misskeyApi } from '@/scripts/misskey-api.js';
-import { useInterval } from '@/scripts/use-interval.js';
+import { useInterval } from '@@/js/use-interval.js';
 import { i18n } from '@/i18n.js';
 
 const name = 'slideshow';
diff --git a/packages/frontend/src/widgets/WidgetTimeline.vue b/packages/frontend/src/widgets/WidgetTimeline.vue
index 150e838582..a4685fd1fc 100644
--- a/packages/frontend/src/widgets/WidgetTimeline.vue
+++ b/packages/frontend/src/widgets/WidgetTimeline.vue
@@ -6,10 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 <template>
 <MkContainer :showHeader="widgetProps.showHeader" :style="`height: ${widgetProps.height}px;`" :scrollable="true" data-cy-mkw-timeline class="mkw-timeline">
 	<template #icon>
-		<i v-if="widgetProps.src === 'home'" class="ti ti-home"></i>
-		<i v-else-if="widgetProps.src === 'local'" class="ti ti-planet"></i>
-		<i v-else-if="widgetProps.src === 'social'" class="ti ti-universe"></i>
-		<i v-else-if="widgetProps.src === 'global'" class="ti ti-whirl"></i>
+		<i v-if="isBasicTimeline(widgetProps.src)" :class="basicTimelineIconClass(widgetProps.src)"></i>
 		<i v-else-if="widgetProps.src === 'list'" class="ti ti-list"></i>
 		<i v-else-if="widgetProps.src === 'antenna'" class="ti ti-antenna"></i>
 	</template>
@@ -20,7 +17,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 		</button>
 	</template>
 
-	<div v-if="(((widgetProps.src === 'local' || widgetProps.src === 'social') && !isLocalTimelineAvailable) || (widgetProps.src === 'global' && !isGlobalTimelineAvailable))" :class="$style.disabled">
+	<div v-if="isBasicTimeline(widgetProps.src) && !isAvailableBasicTimeline(widgetProps.src)" :class="$style.disabled">
 		<p :class="$style.disabledTitle">
 			<i class="ti ti-minus"></i>
 			{{ i18n.ts._disabledTimeline.title }}
@@ -42,12 +39,10 @@ import { misskeyApi } from '@/scripts/misskey-api.js';
 import MkContainer from '@/components/MkContainer.vue';
 import MkTimeline from '@/components/MkTimeline.vue';
 import { i18n } from '@/i18n.js';
-import { $i } from '@/account.js';
-import { instance } from '@/instance.js';
+import { availableBasicTimelines, isAvailableBasicTimeline, isBasicTimeline, basicTimelineIconClass } from '@/timelines.js';
+import type { MenuItem } from '@/types/menu.js';
 
 const name = 'timeline';
-const isLocalTimelineAvailable = (($i == null && instance.policies.ltlAvailable) || ($i != null && $i.policies.ltlAvailable));
-const isGlobalTimelineAvailable = (($i == null && instance.policies.gtlAvailable) || ($i != null && $i.policies.gtlAvailable));
 
 const widgetPropsDef = {
 	showHeader: {
@@ -115,23 +110,26 @@ const choose = async (ev) => {
 			setSrc('list');
 		},
 	}));
-	os.popupMenu([{
-		text: i18n.ts._timelines.home,
-		icon: 'ti ti-home',
-		action: () => { setSrc('home'); },
-	}, {
-		text: i18n.ts._timelines.local,
-		icon: 'ti ti-planet',
-		action: () => { setSrc('local'); },
-	}, {
-		text: i18n.ts._timelines.social,
-		icon: 'ti ti-universe',
-		action: () => { setSrc('social'); },
-	}, {
-		text: i18n.ts._timelines.global,
-		icon: 'ti ti-whirl',
-		action: () => { setSrc('global'); },
-	}, antennaItems.length > 0 ? { type: 'divider' } : undefined, ...antennaItems, listItems.length > 0 ? { type: 'divider' } : undefined, ...listItems], ev.currentTarget ?? ev.target).then(() => {
+
+	const menuItems: MenuItem[] = [];
+
+	menuItems.push(...availableBasicTimelines().map(tl => ({
+		text: i18n.ts._timelines[tl],
+		icon: basicTimelineIconClass(tl),
+		action: () => { setSrc(tl); },
+	})));
+
+	if (antennaItems.length > 0) {
+		menuItems.push({ type: 'divider' });
+		menuItems.push(...antennaItems);
+	}
+
+	if (listItems.length > 0) {
+		menuItems.push({ type: 'divider' });
+		menuItems.push(...listItems);
+	}
+
+	os.popupMenu(menuItems, ev.currentTarget ?? ev.target).then(() => {
 		menuOpened.value = false;
 	});
 };
diff --git a/packages/frontend/src/widgets/WidgetTrends.vue b/packages/frontend/src/widgets/WidgetTrends.vue
index 4299181a27..47a4efc106 100644
--- a/packages/frontend/src/widgets/WidgetTrends.vue
+++ b/packages/frontend/src/widgets/WidgetTrends.vue
@@ -31,7 +31,7 @@ import { GetFormResultType } from '@/scripts/form.js';
 import MkContainer from '@/components/MkContainer.vue';
 import MkMiniChart from '@/components/MkMiniChart.vue';
 import { misskeyApiGet } from '@/scripts/misskey-api.js';
-import { useInterval } from '@/scripts/use-interval.js';
+import { useInterval } from '@@/js/use-interval.js';
 import { i18n } from '@/i18n.js';
 import { defaultStore } from '@/store.js';
 
@@ -91,13 +91,13 @@ defineExpose<WidgetComponentExpose>({
 			display: flex;
 			align-items: center;
 			padding: 14px 16px;
-			border-bottom: solid 0.5px var(--divider);
+			border-bottom: solid 0.5px var(--MI_THEME-divider);
 
 			> .tag {
 				flex: 1;
 				overflow: hidden;
 				font-size: 0.9em;
-				color: var(--fg);
+				color: var(--MI_THEME-fg);
 
 				> .a {
 					display: block;
diff --git a/packages/frontend/src/widgets/WidgetUserList.vue b/packages/frontend/src/widgets/WidgetUserList.vue
index d9f4dc49ea..72391d622e 100644
--- a/packages/frontend/src/widgets/WidgetUserList.vue
+++ b/packages/frontend/src/widgets/WidgetUserList.vue
@@ -31,7 +31,7 @@ import { GetFormResultType } from '@/scripts/form.js';
 import MkContainer from '@/components/MkContainer.vue';
 import * as os from '@/os.js';
 import { misskeyApi } from '@/scripts/misskey-api.js';
-import { useInterval } from '@/scripts/use-interval.js';
+import { useInterval } from '@@/js/use-interval.js';
 import { i18n } from '@/i18n.js';
 import MkButton from '@/components/MkButton.vue';
 
diff --git a/packages/frontend/src/widgets/server-metric/cpu-mem.vue b/packages/frontend/src/widgets/server-metric/cpu-mem.vue
index 27d3234207..469075e2c4 100644
--- a/packages/frontend/src/widgets/server-metric/cpu-mem.vue
+++ b/packages/frontend/src/widgets/server-metric/cpu-mem.vue
@@ -138,7 +138,7 @@ function onStats(connStats: Misskey.entities.ServerStats) {
 }
 
 function onStatsLog(statsLog: Misskey.entities.ServerStatsLog) {
-	for (const revStats of statsLog.reverse()) {
+	for (const revStats of statsLog.toReversed()) {
 		onStats(revStats);
 	}
 }
diff --git a/packages/frontend/src/widgets/server-metric/net.vue b/packages/frontend/src/widgets/server-metric/net.vue
index d46aaa5f69..d78494b8d2 100644
--- a/packages/frontend/src/widgets/server-metric/net.vue
+++ b/packages/frontend/src/widgets/server-metric/net.vue
@@ -111,7 +111,7 @@ function onStats(connStats: Misskey.entities.ServerStats) {
 }
 
 function onStatsLog(statsLog: Misskey.entities.ServerStatsLog) {
-	for (const revStats of statsLog.reverse()) {
+	for (const revStats of statsLog.toReversed()) {
 		onStats(revStats);
 	}
 }
diff --git a/packages/frontend/test/emoji.test.ts b/packages/frontend/test/emoji.test.ts
index 9a2989b373..cf686efd0d 100644
--- a/packages/frontend/test/emoji.test.ts
+++ b/packages/frontend/test/emoji.test.ts
@@ -6,7 +6,7 @@
 import { describe, test, assert, afterEach } from 'vitest';
 import { render, cleanup, type RenderResult } from '@testing-library/vue';
 import { defaultStoreState } from './init.js';
-import { getEmojiName } from '@/scripts/emojilist.js';
+import { getEmojiName } from '@@/js/emojilist.js';
 import { components } from '@/components/index.js';
 import { directives } from '@/directives/index.js';
 import MkEmoji from '@/components/global/MkEmoji.vue';
diff --git a/packages/frontend/test/i18n.test.ts b/packages/frontend/test/i18n.test.ts
new file mode 100644
index 0000000000..9d6cf855f3
--- /dev/null
+++ b/packages/frontend/test/i18n.test.ts
@@ -0,0 +1,54 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { describe, expect, it } from 'vitest';
+import { I18n } from '../../frontend-shared/js/i18n.js'; // @@で参照できなかったので
+import { ParameterizedString } from '../../../locales/index.js';
+
+// TODO: このテストはfrontend-sharedに移動する
+
+describe('i18n', () => {
+	it('t', () => {
+		const i18n = new I18n({
+			foo: 'foo',
+			bar: {
+				baz: 'baz',
+				qux: 'qux {0}' as unknown as ParameterizedString<'0'>,
+				quux: 'quux {0} {1}' as unknown as ParameterizedString<'0' | '1'>,
+			},
+		});
+
+		expect(i18n.t('foo')).toBe('foo');
+		expect(i18n.t('bar.baz')).toBe('baz');
+		expect(i18n.tsx.bar.qux({ 0: 'hoge' })).toBe('qux hoge');
+		expect(i18n.tsx.bar.quux({ 0: 'hoge', 1: 'fuga' })).toBe('quux hoge fuga');
+	});
+	it('ts', () => {
+		const i18n = new I18n({
+			foo: 'foo',
+			bar: {
+				baz: 'baz',
+				qux: 'qux {0}' as unknown as ParameterizedString<'0'>,
+				quux: 'quux {0} {1}' as unknown as ParameterizedString<'0' | '1'>,
+			},
+		});
+
+		expect(i18n.ts.foo).toBe('foo');
+		expect(i18n.ts.bar.baz).toBe('baz');
+	});
+	it('tsx', () => {
+		const i18n = new I18n({
+			foo: 'foo',
+			bar: {
+				baz: 'baz',
+				qux: 'qux {0}' as unknown as ParameterizedString<'0'>,
+				quux: 'quux {0} {1}' as unknown as ParameterizedString<'0' | '1'>,
+			},
+		});
+
+		expect(i18n.tsx.bar.qux({ 0: 'hoge' })).toBe('qux hoge');
+		expect(i18n.tsx.bar.quux({ 0: 'hoge', 1: 'fuga' })).toBe('quux hoge fuga');
+	});
+});
diff --git a/packages/frontend/test/scroll.test.ts b/packages/frontend/test/scroll.test.ts
index a0b56b7221..32a5a1c558 100644
--- a/packages/frontend/test/scroll.test.ts
+++ b/packages/frontend/test/scroll.test.ts
@@ -5,7 +5,7 @@
 
 import { describe, test, assert, afterEach } from 'vitest';
 import { Window } from 'happy-dom';
-import { onScrollBottom, onScrollTop } from '@/scripts/scroll.js';
+import { onScrollBottom, onScrollTop } from '@@/js/scroll.js';
 
 describe('Scroll', () => {
 	describe('onScrollTop', () => {
diff --git a/packages/frontend/tsconfig.json b/packages/frontend/tsconfig.json
index 819629a9cf..b88773b598 100644
--- a/packages/frontend/tsconfig.json
+++ b/packages/frontend/tsconfig.json
@@ -23,7 +23,8 @@
 		"useDefineForClassFields": true,
 		"baseUrl": ".",
 		"paths": {
-			"@/*": ["./src/*"]
+			"@/*": ["./src/*"],
+			"@@/*": ["../frontend-shared/*"]
 		},
 		"typeRoots": [
 			"./@types",
@@ -37,13 +38,13 @@
 		],
 		"lib": [
 			"esnext",
-			"dom"
+			"dom",
+			"dom.iterable"
 		],
 		"jsx": "preserve"
 	},
 	"compileOnSave": false,
 	"include": [
-		".eslintrc.js",
 		"./**/*.ts",
 		"./**/*.vue"
 	],
diff --git a/packages/frontend/vite.config.local-dev.ts b/packages/frontend/vite.config.local-dev.ts
index f9dff13b15..922fb45995 100644
--- a/packages/frontend/vite.config.local-dev.ts
+++ b/packages/frontend/vite.config.local-dev.ts
@@ -1,6 +1,8 @@
 import dns from 'dns';
 import { readFile } from 'node:fs/promises';
+import type { IncomingMessage } from 'node:http';
 import { defineConfig } from 'vite';
+import type { UserConfig } from 'vite';
 import * as yaml from 'js-yaml';
 import locales from '../../locales/index.js';
 import { getConfig } from './vite.config.js';
@@ -13,8 +15,17 @@ const { port } = yaml.load(await readFile('../../.config/default.yml', 'utf-8'))
 
 const httpUrl = `http://localhost:${port}/`;
 const websocketUrl = `ws://localhost:${port}/`;
+const embedUrl = `http://localhost:5174/`;
 
-const devConfig = {
+// activitypubリクエストはProxyを通し、それ以外はViteの開発サーバーを返す
+function varyHandler(req: IncomingMessage) {
+	if (req.headers.accept?.includes('application/activity+json')) {
+		return null;
+	}
+	return '/index.html';
+}
+
+const devConfig: UserConfig = {
 	// 基本の設定は vite.config.js から引き継ぐ
 	...defaultConfig,
 	root: 'src',
@@ -40,6 +51,12 @@ const devConfig = {
 				ws: true,
 			},
 			'/favicon.ico': httpUrl,
+			'/robots.txt': httpUrl,
+			'/embed.js': httpUrl,
+			'/embed': {
+				target: embedUrl,
+				ws: true,
+			},
 			'/identicon': {
 				target: httpUrl,
 				rewrite(path) {
@@ -52,17 +69,14 @@ const devConfig = {
 			'/bios': httpUrl,
 			'/cli': httpUrl,
 			'/inbox': httpUrl,
+			'/emoji/': httpUrl,
 			'/notes': {
 				target: httpUrl,
-				headers: {
-					'Accept': 'application/activity+json',
-				},
+				bypass: varyHandler,
 			},
 			'/users': {
 				target: httpUrl,
-				headers: {
-					'Accept': 'application/activity+json',
-				},
+				bypass: varyHandler,
 			},
 			'/.well-known': {
 				target: httpUrl,
diff --git a/packages/frontend/vite.config.ts b/packages/frontend/vite.config.ts
index 82eb2af464..504562a91e 100644
--- a/packages/frontend/vite.config.ts
+++ b/packages/frontend/vite.config.ts
@@ -5,7 +5,7 @@ import { type UserConfig, defineConfig } from 'vite';
 
 import locales from '../../locales/index.js';
 import meta from '../../package.json';
-import packageInfo from './package.json' assert { type: 'json' };
+import packageInfo from './package.json' with { type: 'json' };
 import pluginUnwindCssModuleClassName from './lib/rollup-plugin-unwind-css-module-class-name.js';
 import pluginJson5 from './vite.json5.js';
 
@@ -65,6 +65,9 @@ export function getConfig(): UserConfig {
 
 		server: {
 			port: 5173,
+			headers: { // なんか効かない
+				'X-Frame-Options': 'DENY',
+			},
 		},
 
 		plugins: [
@@ -87,6 +90,7 @@ export function getConfig(): UserConfig {
 			extensions,
 			alias: {
 				'@/': __dirname + '/src/',
+				'@@/': __dirname + '/../frontend-shared/',
 				'/client-assets/': __dirname + '/assets/',
 				'/static-assets/': __dirname + '/../backend/assets/',
 				'/fluent-emojis/': __dirname + '/../../fluent-emojis/dist/',
@@ -105,6 +109,11 @@ export function getConfig(): UserConfig {
 					}
 				},
 			},
+			preprocessorOptions: {
+				scss: {
+					api: 'modern-compiler',
+				},
+			},
 		},
 
 		define: {
@@ -151,7 +160,7 @@ export function getConfig(): UserConfig {
 				},
 			},
 			cssCodeSplit: true,
-			outDir: __dirname + '/../../built/_vite_',
+			outDir: __dirname + '/../../built/_frontend_vite_',
 			assetsDir: '.',
 			emptyOutDir: false,
 			sourcemap: process.env.NODE_ENV === 'development',
diff --git a/packages/misskey-bubble-game/.eslintignore b/packages/misskey-bubble-game/.eslintignore
deleted file mode 100644
index 52ea8b3362..0000000000
--- a/packages/misskey-bubble-game/.eslintignore
+++ /dev/null
@@ -1,8 +0,0 @@
-node_modules
-/built
-/coverage
-/.eslintrc.js
-/jest.config.ts
-/test
-/test-d
-build.js
diff --git a/packages/misskey-bubble-game/.eslintrc.cjs b/packages/misskey-bubble-game/.eslintrc.cjs
deleted file mode 100644
index e2e31e9e33..0000000000
--- a/packages/misskey-bubble-game/.eslintrc.cjs
+++ /dev/null
@@ -1,9 +0,0 @@
-module.exports = {
-	parserOptions: {
-		tsconfigRootDir: __dirname,
-		project: ['./tsconfig.json'],
-	},
-	extends: [
-		'../shared/.eslintrc.js',
-	],
-};
diff --git a/packages/misskey-bubble-game/build.js b/packages/misskey-bubble-game/build.js
index 0b79f4b915..a80b71646f 100644
--- a/packages/misskey-bubble-game/build.js
+++ b/packages/misskey-bubble-game/build.js
@@ -1,32 +1,32 @@
-import * as esbuild from "esbuild";
-import { build } from "esbuild";
-import { globSync } from "glob";
-import { execa } from "execa";
-import fs from "node:fs";
-import { fileURLToPath } from "node:url";
-import { dirname } from "node:path";
+import fs from 'node:fs';
+import { fileURLToPath } from 'node:url';
+import { dirname } from 'node:path';
+import * as esbuild from 'esbuild';
+import { build } from 'esbuild';
+import { globSync } from 'glob';
+import { execa } from 'execa';
 
 const _filename = fileURLToPath(import.meta.url);
 const _dirname = dirname(_filename);
 const _package = JSON.parse(fs.readFileSync(_dirname + '/package.json', 'utf-8'));
 
-const entryPoints = globSync("./src/**/**.{ts,tsx}");
+const entryPoints = globSync('./src/**/**.{ts,tsx}');
 
 /** @type {import('esbuild').BuildOptions} */
 const options = {
 	entryPoints,
 	minify: process.env.NODE_ENV === 'production',
-	outdir: "./built",
-	target: "es2022",
-	platform: "browser",
-	format: "esm",
+	outdir: './built',
+	target: 'es2022',
+	platform: 'browser',
+	format: 'esm',
 	sourcemap: 'linked',
 };
 
 // built配下をすべて削除する
 fs.rmSync('./built', { recursive: true, force: true });
 
-if (process.argv.map(arg => arg.toLowerCase()).includes("--watch")) {
+if (process.argv.map(arg => arg.toLowerCase()).includes('--watch')) {
 	await watchSrc();
 } else {
 	await buildSrc();
@@ -36,7 +36,7 @@ async function buildSrc() {
 	console.log(`[${_package.name}] start building...`);
 
 	await build(options)
-		.then(it => {
+		.then(() => {
 			console.log(`[${_package.name}] build succeeded.`);
 		})
 		.catch((err) => {
@@ -65,7 +65,7 @@ function buildDts() {
 		{
 			stdout: process.stdout,
 			stderr: process.stderr,
-		}
+		},
 	);
 }
 
@@ -86,7 +86,7 @@ async function watchSrc() {
 		},
 	}];
 
-	console.log(`[${_package.name}] start watching...`)
+	console.log(`[${_package.name}] start watching...`);
 
 	const context = await esbuild.context({ ...options, plugins });
 	await context.watch();
@@ -95,7 +95,6 @@ async function watchSrc() {
 		process.on('SIGHUP', resolve);
 		process.on('SIGINT', resolve);
 		process.on('SIGTERM', resolve);
-		process.on('SIGKILL', resolve);
 		process.on('uncaughtException', reject);
 		process.on('exit', resolve);
 	}).finally(async () => {
diff --git a/packages/misskey-bubble-game/eslint.config.js b/packages/misskey-bubble-game/eslint.config.js
new file mode 100644
index 0000000000..bce383b1a6
--- /dev/null
+++ b/packages/misskey-bubble-game/eslint.config.js
@@ -0,0 +1,28 @@
+import tsParser from '@typescript-eslint/parser';
+import sharedConfig from '../shared/eslint.config.js';
+
+// eslint-disable-next-line import/no-default-export
+export default [
+	...sharedConfig,
+	{
+		ignores: [
+			'**/node_modules',
+			'built',
+			'coverage',
+			'jest.config.ts',
+			'test',
+			'test-d',
+		],
+	},
+	{
+		files: ['**/*.ts', '**/*.tsx'],
+		languageOptions: {
+			parserOptions: {
+				parser: tsParser,
+				project: ['./tsconfig.json'],
+				sourceType: 'module',
+				tsconfigRootDir: import.meta.dirname,
+			},
+		},
+	},
+];
diff --git a/packages/misskey-bubble-game/package.json b/packages/misskey-bubble-game/package.json
index a3aad147a9..528eb00b74 100644
--- a/packages/misskey-bubble-game/package.json
+++ b/packages/misskey-bubble-game/package.json
@@ -17,18 +17,16 @@
 	"scripts": {
 		"build": "node ./build.js",
 		"watch": "nodemon -w package.json -e json --exec \"node ./build.js --watch\"",
-		"eslint": "eslint . --ext .js,.jsx,.ts,.tsx",
+		"eslint": "eslint './**/*.{js,jsx,ts,tsx}'",
 		"typecheck": "tsc --noEmit",
 		"lint": "pnpm typecheck && pnpm eslint"
 	},
 	"devDependencies": {
-		"@misskey-dev/eslint-plugin": "1.0.0",
 		"@types/matter-js": "0.19.6",
 		"@types/seedrandom": "3.0.8",
 		"@types/node": "20.11.5",
 		"@typescript-eslint/eslint-plugin": "7.1.0",
 		"@typescript-eslint/parser": "7.1.0",
-		"eslint": "8.57.0",
 		"nodemon": "3.0.2",
 		"execa": "8.0.1",
 		"typescript": "5.3.3",
diff --git a/packages/misskey-bubble-game/src/game.ts b/packages/misskey-bubble-game/src/game.ts
index 3bce4b1dcf..7f230e39cb 100644
--- a/packages/misskey-bubble-game/src/game.ts
+++ b/packages/misskey-bubble-game/src/game.ts
@@ -199,13 +199,12 @@ export class DropAndFusionGame extends EventEmitter<{
 		};
 		if (mono.shape === 'circle') {
 			return Matter.Bodies.circle(x, y, mono.sizeX / 2, options);
-		// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
 		} else if (mono.shape === 'rectangle') {
 			return Matter.Bodies.rectangle(x, y, mono.sizeX, mono.sizeY, options);
-		} else if (mono.shape === 'custom') {
-			return Matter.Bodies.fromVertices(x, y, mono.vertices!.map(i => i.map(j => ({
-				x: (j.x / mono.verticesSize!) * mono.sizeX,
-				y: (j.y / mono.verticesSize!) * mono.sizeY,
+		} else if (mono.shape === 'custom' && mono.vertices != null && mono.verticesSize != null) { //eslint-disable-line @typescript-eslint/no-unnecessary-condition
+			return Matter.Bodies.fromVertices(x, y, mono.vertices.map(i => i.map(j => ({
+				x: (j.x / mono.verticesSize!) * mono.sizeX, //eslint-disable-line @typescript-eslint/no-non-null-assertion
+				y: (j.y / mono.verticesSize!) * mono.sizeY, //eslint-disable-line @typescript-eslint/no-non-null-assertion
 			}))), options);
 		} else {
 			throw new Error('unrecognized shape');
@@ -227,7 +226,12 @@ export class DropAndFusionGame extends EventEmitter<{
 		this.gameOverReadyBodyIds = this.gameOverReadyBodyIds.filter(x => x !== bodyA.id && x !== bodyB.id);
 		Matter.Composite.remove(this.engine.world, [bodyA, bodyB]);
 
-		const currentMono = this.monoDefinitions.find(y => y.id === bodyA.label)!;
+		const currentMono = this.monoDefinitions.find(y => y.id === bodyA.label);
+
+		if (currentMono == null) {
+			throw new Error('Current Mono Not Found');
+		}
+
 		const nextMono = this.monoDefinitions.find(x => x.level === currentMono.level + 1) ?? null;
 
 		if (nextMono) {
@@ -362,14 +366,18 @@ export class DropAndFusionGame extends EventEmitter<{
 	}
 
 	public getActiveMonos() {
-		return this.engine.world.bodies.map(x => this.monoDefinitions.find((mono) => mono.id === x.label)!).filter(x => x !== undefined);
+		return this.engine.world.bodies
+			.map(x => this.monoDefinitions.find((mono) => mono.id === x.label))
+			.filter(x => x !== undefined);
 	}
 
 	public drop(_x: number) {
 		if (this.isGameOver) return;
 		if (this.frame - this.latestDroppedAt < this.DROP_COOLTIME) return;
 
-		const head = this.stock.shift()!;
+		const head = this.stock.shift();
+		if (!head) return;
+
 		this.stock.push({
 			id: this.rng().toString(),
 			mono: this.monoDefinitions.filter(x => x.dropCandidate)[Math.floor(this.rng() * this.monoDefinitions.filter(x => x.dropCandidate).length)],
@@ -411,13 +419,15 @@ export class DropAndFusionGame extends EventEmitter<{
 		});
 
 		if (this.holding) {
-			const head = this.stock.shift()!;
+			const head = this.stock.shift();
+			if (!head) return;
 			this.stock.unshift(this.holding);
 			this.holding = head;
 			this.emit('changeHolding', this.holding);
 			this.emit('changeStock', this.stock);
 		} else {
-			const head = this.stock.shift()!;
+			const head = this.stock.shift();
+			if (!head) return;
 			this.holding = head;
 			this.stock.push({
 				id: this.rng().toString(),
diff --git a/packages/misskey-js/.eslintignore b/packages/misskey-js/.eslintignore
deleted file mode 100644
index 52ea8b3362..0000000000
--- a/packages/misskey-js/.eslintignore
+++ /dev/null
@@ -1,8 +0,0 @@
-node_modules
-/built
-/coverage
-/.eslintrc.js
-/jest.config.ts
-/test
-/test-d
-build.js
diff --git a/packages/misskey-js/.eslintrc.cjs b/packages/misskey-js/.eslintrc.cjs
deleted file mode 100644
index e2e31e9e33..0000000000
--- a/packages/misskey-js/.eslintrc.cjs
+++ /dev/null
@@ -1,9 +0,0 @@
-module.exports = {
-	parserOptions: {
-		tsconfigRootDir: __dirname,
-		project: ['./tsconfig.json'],
-	},
-	extends: [
-		'../shared/.eslintrc.js',
-	],
-};
diff --git a/packages/misskey-js/README.md b/packages/misskey-js/README.md
index 63d4b36c56..4753e2434b 100644
--- a/packages/misskey-js/README.md
+++ b/packages/misskey-js/README.md
@@ -154,5 +154,5 @@ stream.on('_disconnected_', () => {
 ---
 
 <div align="center">
-	<a href="https://github.com/misskey-dev/misskey/blob/develop/CONTRIBUTING.md"><img src="https://raw.githubusercontent.com/misskey-dev/assets/main/i-want-you.png" width="300"></a>
+	<a href="https://github.com/misskey-dev/misskey/blob/develop/CONTRIBUTING.md"><img src="https://assets.misskey-hub.net/public/i-want-you.png" width="300"></a>
 </div>
diff --git a/packages/misskey-js/build.js b/packages/misskey-js/build.js
index 0b79f4b915..a80b71646f 100644
--- a/packages/misskey-js/build.js
+++ b/packages/misskey-js/build.js
@@ -1,32 +1,32 @@
-import * as esbuild from "esbuild";
-import { build } from "esbuild";
-import { globSync } from "glob";
-import { execa } from "execa";
-import fs from "node:fs";
-import { fileURLToPath } from "node:url";
-import { dirname } from "node:path";
+import fs from 'node:fs';
+import { fileURLToPath } from 'node:url';
+import { dirname } from 'node:path';
+import * as esbuild from 'esbuild';
+import { build } from 'esbuild';
+import { globSync } from 'glob';
+import { execa } from 'execa';
 
 const _filename = fileURLToPath(import.meta.url);
 const _dirname = dirname(_filename);
 const _package = JSON.parse(fs.readFileSync(_dirname + '/package.json', 'utf-8'));
 
-const entryPoints = globSync("./src/**/**.{ts,tsx}");
+const entryPoints = globSync('./src/**/**.{ts,tsx}');
 
 /** @type {import('esbuild').BuildOptions} */
 const options = {
 	entryPoints,
 	minify: process.env.NODE_ENV === 'production',
-	outdir: "./built",
-	target: "es2022",
-	platform: "browser",
-	format: "esm",
+	outdir: './built',
+	target: 'es2022',
+	platform: 'browser',
+	format: 'esm',
 	sourcemap: 'linked',
 };
 
 // built配下をすべて削除する
 fs.rmSync('./built', { recursive: true, force: true });
 
-if (process.argv.map(arg => arg.toLowerCase()).includes("--watch")) {
+if (process.argv.map(arg => arg.toLowerCase()).includes('--watch')) {
 	await watchSrc();
 } else {
 	await buildSrc();
@@ -36,7 +36,7 @@ async function buildSrc() {
 	console.log(`[${_package.name}] start building...`);
 
 	await build(options)
-		.then(it => {
+		.then(() => {
 			console.log(`[${_package.name}] build succeeded.`);
 		})
 		.catch((err) => {
@@ -65,7 +65,7 @@ function buildDts() {
 		{
 			stdout: process.stdout,
 			stderr: process.stderr,
-		}
+		},
 	);
 }
 
@@ -86,7 +86,7 @@ async function watchSrc() {
 		},
 	}];
 
-	console.log(`[${_package.name}] start watching...`)
+	console.log(`[${_package.name}] start watching...`);
 
 	const context = await esbuild.context({ ...options, plugins });
 	await context.watch();
@@ -95,7 +95,6 @@ async function watchSrc() {
 		process.on('SIGHUP', resolve);
 		process.on('SIGINT', resolve);
 		process.on('SIGTERM', resolve);
-		process.on('SIGKILL', resolve);
 		process.on('uncaughtException', reject);
 		process.on('exit', resolve);
 	}).finally(async () => {
diff --git a/packages/misskey-js/eslint.config.js b/packages/misskey-js/eslint.config.js
new file mode 100644
index 0000000000..d8173f30e9
--- /dev/null
+++ b/packages/misskey-js/eslint.config.js
@@ -0,0 +1,29 @@
+import tsParser from '@typescript-eslint/parser';
+import sharedConfig from '../shared/eslint.config.js';
+
+// eslint-disable-next-line import/no-default-export
+export default [
+	...sharedConfig,
+	{
+		ignores: [
+			'**/node_modules',
+			'built',
+			'coverage',
+			'jest.config.ts',
+			'test',
+			'test-d',
+			'generator',
+		],
+	},
+	{
+		files: ['**/*.ts', '**/*.tsx'],
+		languageOptions: {
+			parserOptions: {
+				parser: tsParser,
+				project: ['./tsconfig.json'],
+				sourceType: 'module',
+				tsconfigRootDir: import.meta.dirname,
+			},
+		},
+	},
+];
diff --git a/packages/misskey-js/etc/misskey-js.api.md b/packages/misskey-js/etc/misskey-js.api.md
index 6ff711cabb..061b533b72 100644
--- a/packages/misskey-js/etc/misskey-js.api.md
+++ b/packages/misskey-js/etc/misskey-js.api.md
@@ -4,7 +4,15 @@
 
 ```ts
 
+import type { AuthenticationResponseJSON } from '@simplewebauthn/types';
 import { EventEmitter } from 'eventemitter3';
+import type { PublicKeyCredentialRequestOptionsJSON } from '@simplewebauthn/types';
+import _ReconnectingWebsocket from 'reconnecting-websocket';
+
+// Warning: (ae-forgotten-export) The symbol "components" needs to be exported by the entry point index.d.ts
+//
+// @public (undocumented)
+type AbuseReportNotificationRecipient = components['schemas']['AbuseReportNotificationRecipient'];
 
 // @public (undocumented)
 export type Acct = {
@@ -21,13 +29,38 @@ declare namespace acct {
 }
 export { acct }
 
-// Warning: (ae-forgotten-export) The symbol "components" needs to be exported by the entry point index.d.ts
-//
 // @public (undocumented)
 type Ad = components['schemas']['Ad'];
 
 // Warning: (ae-forgotten-export) The symbol "operations" needs to be exported by the entry point index.d.ts
 //
+// @public (undocumented)
+type AdminAbuseReportNotificationRecipientCreateRequest = operations['admin___abuse-report___notification-recipient___create']['requestBody']['content']['application/json'];
+
+// @public (undocumented)
+type AdminAbuseReportNotificationRecipientCreateResponse = operations['admin___abuse-report___notification-recipient___create']['responses']['200']['content']['application/json'];
+
+// @public (undocumented)
+type AdminAbuseReportNotificationRecipientDeleteRequest = operations['admin___abuse-report___notification-recipient___delete']['requestBody']['content']['application/json'];
+
+// @public (undocumented)
+type AdminAbuseReportNotificationRecipientListRequest = operations['admin___abuse-report___notification-recipient___list']['requestBody']['content']['application/json'];
+
+// @public (undocumented)
+type AdminAbuseReportNotificationRecipientListResponse = operations['admin___abuse-report___notification-recipient___list']['responses']['200']['content']['application/json'];
+
+// @public (undocumented)
+type AdminAbuseReportNotificationRecipientShowRequest = operations['admin___abuse-report___notification-recipient___show']['requestBody']['content']['application/json'];
+
+// @public (undocumented)
+type AdminAbuseReportNotificationRecipientShowResponse = operations['admin___abuse-report___notification-recipient___show']['responses']['200']['content']['application/json'];
+
+// @public (undocumented)
+type AdminAbuseReportNotificationRecipientUpdateRequest = operations['admin___abuse-report___notification-recipient___update']['requestBody']['content']['application/json'];
+
+// @public (undocumented)
+type AdminAbuseReportNotificationRecipientUpdateResponse = operations['admin___abuse-report___notification-recipient___update']['responses']['200']['content']['application/json'];
+
 // @public (undocumented)
 type AdminAbuseUserReportsRequest = operations['admin___abuse-user-reports']['requestBody']['content']['application/json'];
 
@@ -88,6 +121,9 @@ type AdminAnnouncementsUpdateRequest = operations['admin___announcements___updat
 // @public (undocumented)
 type AdminAvatarDecorationsCreateRequest = operations['admin___avatar-decorations___create']['requestBody']['content']['application/json'];
 
+// @public (undocumented)
+type AdminAvatarDecorationsCreateResponse = operations['admin___avatar-decorations___create']['responses']['200']['content']['application/json'];
+
 // @public (undocumented)
 type AdminAvatarDecorationsDeleteRequest = operations['admin___avatar-decorations___delete']['requestBody']['content']['application/json'];
 
@@ -181,6 +217,9 @@ type AdminFederationRemoveAllFollowingRequest = operations['admin___federation__
 // @public (undocumented)
 type AdminFederationUpdateInstanceRequest = operations['admin___federation___update-instance']['requestBody']['content']['application/json'];
 
+// @public (undocumented)
+type AdminForwardAbuseUserReportRequest = operations['admin___forward-abuse-user-report']['requestBody']['content']['application/json'];
+
 // @public (undocumented)
 type AdminGetIndexStatsResponse = operations['admin___get-index-stats']['responses']['200']['content']['application/json'];
 
@@ -307,6 +346,36 @@ type AdminShowUsersResponse = operations['admin___show-users']['responses']['200
 // @public (undocumented)
 type AdminSuspendUserRequest = operations['admin___suspend-user']['requestBody']['content']['application/json'];
 
+// @public (undocumented)
+type AdminSystemWebhookCreateRequest = operations['admin___system-webhook___create']['requestBody']['content']['application/json'];
+
+// @public (undocumented)
+type AdminSystemWebhookCreateResponse = operations['admin___system-webhook___create']['responses']['200']['content']['application/json'];
+
+// @public (undocumented)
+type AdminSystemWebhookDeleteRequest = operations['admin___system-webhook___delete']['requestBody']['content']['application/json'];
+
+// @public (undocumented)
+type AdminSystemWebhookListRequest = operations['admin___system-webhook___list']['requestBody']['content']['application/json'];
+
+// @public (undocumented)
+type AdminSystemWebhookListResponse = operations['admin___system-webhook___list']['responses']['200']['content']['application/json'];
+
+// @public (undocumented)
+type AdminSystemWebhookShowRequest = operations['admin___system-webhook___show']['requestBody']['content']['application/json'];
+
+// @public (undocumented)
+type AdminSystemWebhookShowResponse = operations['admin___system-webhook___show']['responses']['200']['content']['application/json'];
+
+// @public (undocumented)
+type AdminSystemWebhookTestRequest = operations['admin___system-webhook___test']['requestBody']['content']['application/json'];
+
+// @public (undocumented)
+type AdminSystemWebhookUpdateRequest = operations['admin___system-webhook___update']['requestBody']['content']['application/json'];
+
+// @public (undocumented)
+type AdminSystemWebhookUpdateResponse = operations['admin___system-webhook___update']['responses']['200']['content']['application/json'];
+
 // @public (undocumented)
 type AdminUnsetUserAvatarRequest = operations['admin___unset-user-avatar']['requestBody']['content']['application/json'];
 
@@ -316,6 +385,9 @@ type AdminUnsetUserBannerRequest = operations['admin___unset-user-banner']['requ
 // @public (undocumented)
 type AdminUnsuspendUserRequest = operations['admin___unsuspend-user']['requestBody']['content']['application/json'];
 
+// @public (undocumented)
+type AdminUpdateAbuseUserReportRequest = operations['admin___update-abuse-user-report']['requestBody']['content']['application/json'];
+
 // @public (undocumented)
 type AdminUpdateMetaRequest = operations['admin___update-meta']['requestBody']['content']['application/json'];
 
@@ -494,7 +566,7 @@ type Channel = components['schemas']['Channel'];
 // Warning: (ae-forgotten-export) The symbol "AnyOf" needs to be exported by the entry point index.d.ts
 //
 // @public (undocumented)
-export abstract class ChannelConnection<Channel extends AnyOf<Channels> = any> extends EventEmitter<Channel['events']> {
+export abstract class ChannelConnection<Channel extends AnyOf<Channels> = AnyOf<Channels>> extends EventEmitter<Channel['events']> implements IChannelConnection<Channel> {
     constructor(stream: Stream, channel: string, name?: string);
     // (undocumented)
     channel: string;
@@ -611,7 +683,7 @@ export type Channels = {
     };
     hashtag: {
         params: {
-            q?: string;
+            q: string[][];
         };
         events: {
             note: (payload: Note) => void;
@@ -714,12 +786,12 @@ export type Channels = {
                 user1: boolean;
                 user2: boolean;
             }) => void;
-            updateSettings: (payload: {
+            updateSettings: <K extends ReversiUpdateKey>(payload: {
                 userId: User['id'];
-                key: string;
-                value: any;
+                key: K;
+                value: ReversiGameDetailed[K];
             }) => void;
-            log: (payload: Record<string, any>) => void;
+            log: (payload: Record<string, unknown>) => void;
         };
         receives: {
             putStone: {
@@ -728,10 +800,7 @@ export type Channels = {
             };
             ready: boolean;
             cancel: null | Record<string, never>;
-            updateSettings: {
-                key: string;
-                value: any;
-            };
+            updateSettings: ReversiUpdateSettings<ReversiUpdateKey>;
             claimTimeIsUp: null | Record<string, never>;
         };
     };
@@ -1099,9 +1168,31 @@ export type Endpoints = Overwrite<Endpoints_2, {
         req: SignupPendingRequest;
         res: SignupPendingResponse;
     };
-    'signin': {
-        req: SigninRequest;
-        res: SigninResponse;
+    'signin-flow': {
+        req: SigninFlowRequest;
+        res: SigninFlowResponse;
+    };
+    'signin-with-passkey': {
+        req: SigninWithPasskeyRequest;
+        res: {
+            $switch: {
+                $cases: [
+                [
+                    {
+                    context: string;
+                },
+                SigninWithPasskeyResponse
+                ]
+                ];
+                $default: SigninWithPasskeyInitResponse;
+            };
+        };
+    };
+    'admin/roles/create': {
+        req: Overwrite<AdminRolesCreateRequest, {
+            policies: PartialRolePolicyOverride;
+        }>;
+        res: AdminRolesCreateResponse;
     };
 }>;
 
@@ -1112,6 +1203,7 @@ declare namespace entities {
     export {
         ID,
         DateString,
+        PureRenote,
         PageEvent,
         ModerationLog,
         ServerStats,
@@ -1126,13 +1218,26 @@ declare namespace entities {
         SignupResponse,
         SignupPendingRequest,
         SignupPendingResponse,
-        SigninRequest,
-        SigninResponse,
+        SigninFlowRequest,
+        SigninFlowResponse,
+        SigninWithPasskeyRequest,
+        SigninWithPasskeyInitResponse,
+        SigninWithPasskeyResponse,
+        PartialRolePolicyOverride,
         EmptyRequest,
         EmptyResponse,
         AdminMetaResponse,
         AdminAbuseUserReportsRequest,
         AdminAbuseUserReportsResponse,
+        AdminAbuseReportNotificationRecipientListRequest,
+        AdminAbuseReportNotificationRecipientListResponse,
+        AdminAbuseReportNotificationRecipientShowRequest,
+        AdminAbuseReportNotificationRecipientShowResponse,
+        AdminAbuseReportNotificationRecipientCreateRequest,
+        AdminAbuseReportNotificationRecipientCreateResponse,
+        AdminAbuseReportNotificationRecipientUpdateRequest,
+        AdminAbuseReportNotificationRecipientUpdateResponse,
+        AdminAbuseReportNotificationRecipientDeleteRequest,
         AdminAccountsCreateRequest,
         AdminAccountsCreateResponse,
         AdminAccountsDeleteRequest,
@@ -1151,6 +1256,7 @@ declare namespace entities {
         AdminAnnouncementsListResponse,
         AdminAnnouncementsUpdateRequest,
         AdminAvatarDecorationsCreateRequest,
+        AdminAvatarDecorationsCreateResponse,
         AdminAvatarDecorationsDeleteRequest,
         AdminAvatarDecorationsListRequest,
         AdminAvatarDecorationsListResponse,
@@ -1203,6 +1309,8 @@ declare namespace entities {
         AdminResetPasswordRequest,
         AdminResetPasswordResponse,
         AdminResolveAbuseUserReportRequest,
+        AdminForwardAbuseUserReportRequest,
+        AdminUpdateAbuseUserReportRequest,
         AdminSendEmailRequest,
         AdminServerInfoResponse,
         AdminShowModerationLogsRequest,
@@ -1228,6 +1336,16 @@ declare namespace entities {
         AdminRolesUpdateDefaultPoliciesRequest,
         AdminRolesUsersRequest,
         AdminRolesUsersResponse,
+        AdminSystemWebhookCreateRequest,
+        AdminSystemWebhookCreateResponse,
+        AdminSystemWebhookDeleteRequest,
+        AdminSystemWebhookListRequest,
+        AdminSystemWebhookListResponse,
+        AdminSystemWebhookShowRequest,
+        AdminSystemWebhookShowResponse,
+        AdminSystemWebhookUpdateRequest,
+        AdminSystemWebhookUpdateResponse,
+        AdminSystemWebhookTestRequest,
         AnnouncementsRequest,
         AnnouncementsResponse,
         AnnouncementsShowRequest,
@@ -1487,6 +1605,7 @@ declare namespace entities {
         IWebhooksShowResponse,
         IWebhooksUpdateRequest,
         IWebhooksDeleteRequest,
+        IWebhooksTestRequest,
         InviteCreateResponse,
         InviteDeleteRequest,
         InviteListRequest,
@@ -1574,6 +1693,7 @@ declare namespace entities {
         FlashCreateRequest,
         FlashCreateResponse,
         FlashDeleteRequest,
+        FlashFeaturedRequest,
         FlashFeaturedResponse,
         FlashLikeRequest,
         FlashShowRequest,
@@ -1733,7 +1853,9 @@ declare namespace entities {
         ReversiGameDetailed,
         MetaLite,
         MetaDetailedOnly,
-        MetaDetailed
+        MetaDetailed,
+        SystemWebhook,
+        AbuseReportNotificationRecipient
     }
 }
 export { entities }
@@ -1792,7 +1914,7 @@ type FetchExternalResourcesResponse = operations['fetch-external-resources']['re
 // @public (undocumented)
 type FetchLike = (input: string, init?: {
     method?: string;
-    body?: string;
+    body?: Blob | FormData | string;
     credentials?: RequestCredentials;
     cache?: RequestCache;
     headers: {
@@ -1821,6 +1943,9 @@ type FlashCreateResponse = operations['flash___create']['responses']['200']['con
 // @public (undocumented)
 type FlashDeleteRequest = operations['flash___delete']['requestBody']['content']['application/json'];
 
+// @public (undocumented)
+type FlashFeaturedRequest = operations['flash___featured']['requestBody']['content']['application/json'];
+
 // @public (undocumented)
 type FlashFeaturedResponse = operations['flash___featured']['responses']['200']['content']['application/json'];
 
@@ -2037,6 +2162,24 @@ type IAuthorizedAppsResponse = operations['i___authorized-apps']['responses']['2
 // @public (undocumented)
 type IChangePasswordRequest = operations['i___change-password']['requestBody']['content']['application/json'];
 
+// @public (undocumented)
+export interface IChannelConnection<Channel extends AnyOf<Channels> = AnyOf<Channels>> extends EventEmitter<Channel['events']> {
+    // (undocumented)
+    channel: string;
+    // (undocumented)
+    dispose(): void;
+    // (undocumented)
+    id: string;
+    // (undocumented)
+    inCount: number;
+    // (undocumented)
+    name?: string;
+    // (undocumented)
+    outCount: number;
+    // (undocumented)
+    send<T extends keyof Channel['receives']>(type: T, body: Channel['receives'][T]): void;
+}
+
 // @public (undocumented)
 type IClaimAchievementRequest = operations['i___claim-achievement']['requestBody']['content']['application/json'];
 
@@ -2196,6 +2339,43 @@ type ISigninHistoryRequest = operations['i___signin-history']['requestBody']['co
 // @public (undocumented)
 type ISigninHistoryResponse = operations['i___signin-history']['responses']['200']['content']['application/json'];
 
+// @public (undocumented)
+function isPureRenote(note: Note): note is PureRenote;
+
+// @public (undocumented)
+export interface IStream extends EventEmitter<StreamEvents> {
+    // (undocumented)
+    close(): void;
+    // Warning: (ae-forgotten-export) The symbol "NonSharedConnection" needs to be exported by the entry point index.d.ts
+    //
+    // (undocumented)
+    disconnectToChannel(connection: NonSharedConnection): void;
+    // (undocumented)
+    heartbeat(): void;
+    // (undocumented)
+    ping(): void;
+    // Warning: (ae-forgotten-export) The symbol "SharedConnection" needs to be exported by the entry point index.d.ts
+    //
+    // (undocumented)
+    removeSharedConnection(connection: SharedConnection): void;
+    // Warning: (ae-forgotten-export) The symbol "Pool" needs to be exported by the entry point index.d.ts
+    //
+    // (undocumented)
+    removeSharedConnectionPool(pool: Pool): void;
+    // (undocumented)
+    send(typeOrPayload: string): void;
+    // (undocumented)
+    send(typeOrPayload: string, payload: unknown): void;
+    // (undocumented)
+    send(typeOrPayload: Record<string, unknown> | unknown[]): void;
+    // (undocumented)
+    send(typeOrPayload: string | Record<string, unknown> | unknown[], payload?: unknown): void;
+    // (undocumented)
+    state: 'initializing' | 'reconnecting' | 'connected';
+    // (undocumented)
+    useChannel<C extends keyof Channels>(channel: C, params?: Channels[C]['params'], name?: string): IChannelConnection<Channels[C]>;
+}
+
 // @public (undocumented)
 type IUnpinRequest = operations['i___unpin']['requestBody']['content']['application/json'];
 
@@ -2232,6 +2412,9 @@ type IWebhooksShowRequest = operations['i___webhooks___show']['requestBody']['co
 // @public (undocumented)
 type IWebhooksShowResponse = operations['i___webhooks___show']['responses']['200']['content']['application/json'];
 
+// @public (undocumented)
+type IWebhooksTestRequest = operations['i___webhooks___test']['requestBody']['content']['application/json'];
+
 // @public (undocumented)
 type IWebhooksUpdateRequest = operations['i___webhooks___update']['requestBody']['content']['application/json'];
 
@@ -2376,16 +2559,52 @@ type ModerationLog = {
 } | {
     type: 'resolveAbuseReport';
     info: ModerationLogPayloads['resolveAbuseReport'];
+} | {
+    type: 'forwardAbuseReport';
+    info: ModerationLogPayloads['forwardAbuseReport'];
+} | {
+    type: 'updateAbuseReportNote';
+    info: ModerationLogPayloads['updateAbuseReportNote'];
 } | {
     type: 'unsetUserAvatar';
     info: ModerationLogPayloads['unsetUserAvatar'];
 } | {
     type: 'unsetUserBanner';
     info: ModerationLogPayloads['unsetUserBanner'];
+} | {
+    type: 'createSystemWebhook';
+    info: ModerationLogPayloads['createSystemWebhook'];
+} | {
+    type: 'updateSystemWebhook';
+    info: ModerationLogPayloads['updateSystemWebhook'];
+} | {
+    type: 'deleteSystemWebhook';
+    info: ModerationLogPayloads['deleteSystemWebhook'];
+} | {
+    type: 'createAbuseReportNotificationRecipient';
+    info: ModerationLogPayloads['createAbuseReportNotificationRecipient'];
+} | {
+    type: 'updateAbuseReportNotificationRecipient';
+    info: ModerationLogPayloads['updateAbuseReportNotificationRecipient'];
+} | {
+    type: 'deleteAbuseReportNotificationRecipient';
+    info: ModerationLogPayloads['deleteAbuseReportNotificationRecipient'];
+} | {
+    type: 'deleteAccount';
+    info: ModerationLogPayloads['deleteAccount'];
+} | {
+    type: 'deletePage';
+    info: ModerationLogPayloads['deletePage'];
+} | {
+    type: 'deleteFlash';
+    info: ModerationLogPayloads['deleteFlash'];
+} | {
+    type: 'deleteGalleryPost';
+    info: ModerationLogPayloads['deleteGalleryPost'];
 });
 
 // @public (undocumented)
-export const moderationLogTypes: readonly ["updateServerSettings", "suspend", "unsuspend", "updateUserNote", "addCustomEmoji", "updateCustomEmoji", "deleteCustomEmoji", "assignRole", "unassignRole", "createRole", "updateRole", "deleteRole", "clearQueue", "promoteQueue", "deleteDriveFile", "deleteNote", "createGlobalAnnouncement", "createUserAnnouncement", "updateGlobalAnnouncement", "updateUserAnnouncement", "deleteGlobalAnnouncement", "deleteUserAnnouncement", "resetPassword", "suspendRemoteInstance", "unsuspendRemoteInstance", "updateRemoteInstanceNote", "markSensitiveDriveFile", "unmarkSensitiveDriveFile", "resolveAbuseReport", "createInvitation", "createAd", "updateAd", "deleteAd", "createAvatarDecoration", "updateAvatarDecoration", "deleteAvatarDecoration", "unsetUserAvatar", "unsetUserBanner"];
+export const moderationLogTypes: readonly ["updateServerSettings", "suspend", "unsuspend", "updateUserNote", "addCustomEmoji", "updateCustomEmoji", "deleteCustomEmoji", "assignRole", "unassignRole", "createRole", "updateRole", "deleteRole", "clearQueue", "promoteQueue", "deleteDriveFile", "deleteNote", "createGlobalAnnouncement", "createUserAnnouncement", "updateGlobalAnnouncement", "updateUserAnnouncement", "deleteGlobalAnnouncement", "deleteUserAnnouncement", "resetPassword", "suspendRemoteInstance", "unsuspendRemoteInstance", "updateRemoteInstanceNote", "markSensitiveDriveFile", "unmarkSensitiveDriveFile", "resolveAbuseReport", "forwardAbuseReport", "updateAbuseReportNote", "createInvitation", "createAd", "updateAd", "deleteAd", "createAvatarDecoration", "updateAvatarDecoration", "deleteAvatarDecoration", "unsetUserAvatar", "unsetUserBanner", "createSystemWebhook", "updateSystemWebhook", "deleteSystemWebhook", "createAbuseReportNotificationRecipient", "updateAbuseReportNotificationRecipient", "deleteAbuseReportNotificationRecipient", "deleteAccount", "deletePage", "deleteFlash", "deleteGalleryPost"];
 
 // @public (undocumented)
 type MuteCreateRequest = operations['mute___create']['requestBody']['content']['application/json'];
@@ -2414,6 +2633,13 @@ type MyAppsResponse = operations['my___apps']['responses']['200']['content']['ap
 // @public (undocumented)
 type Note = components['schemas']['Note'];
 
+declare namespace note {
+    export {
+        isPureRenote
+    }
+}
+export { note }
+
 // @public (undocumented)
 type NoteFavorite = components['schemas']['NoteFavorite'];
 
@@ -2585,6 +2811,9 @@ type NotificationsCreateRequest = operations['notifications___create']['requestB
 // @public (undocumented)
 export const notificationTypes: readonly ["note", "follow", "mention", "reply", "renote", "quote", "reaction", "pollVote", "pollEnded", "receiveFollowRequest", "followRequestAccepted", "groupInvited", "app", "roleAssigned", "achievementEarned"];
 
+// @public (undocumented)
+export function nyaize(text: string): string;
+
 // @public (undocumented)
 type Page = components['schemas']['Page'];
 
@@ -2631,7 +2860,16 @@ type PagesUnlikeRequest = operations['pages___unlike']['requestBody']['content']
 type PagesUpdateRequest = operations['pages___update']['requestBody']['content']['application/json'];
 
 // @public (undocumented)
-function parse(acct: string): Acct;
+function parse(_acct: string): Acct;
+
+// Warning: (ae-forgotten-export) The symbol "Values" needs to be exported by the entry point index.d.ts
+//
+// @public (undocumented)
+type PartialRolePolicyOverride = Partial<{
+    [k in keyof RolePolicies]: Omit<Values<Role['policies']>, 'value'> & {
+        value: RolePolicies[k];
+    };
+}>;
 
 // @public (undocumented)
 export const permissions: readonly ["read:account", "write:account", "read:blocks", "write:blocks", "read:drive", "write:drive", "read:favorites", "write:favorites", "read:following", "write:following", "read:messaging", "write:messaging", "read:mutes", "write:mutes", "write:notes", "read:notifications", "write:notifications", "read:reactions", "write:reactions", "write:votes", "read:pages", "write:pages", "write:page-likes", "read:page-likes", "read:user-groups", "write:user-groups", "read:channels", "write:channels", "read:gallery", "write:gallery", "read:gallery-likes", "write:gallery-likes", "read:flash", "write:flash", "read:flash-likes", "write:flash-likes", "read:admin:abuse-user-reports", "write:admin:delete-account", "write:admin:delete-all-files-of-a-user", "read:admin:index-stats", "read:admin:table-stats", "read:admin:user-ips", "read:admin:meta", "write:admin:reset-password", "write:admin:resolve-abuse-user-report", "write:admin:send-email", "read:admin:server-info", "read:admin:show-moderation-log", "read:admin:show-user", "write:admin:suspend-user", "write:admin:unset-user-avatar", "write:admin:unset-user-banner", "write:admin:unsuspend-user", "write:admin:meta", "write:admin:user-note", "write:admin:roles", "read:admin:roles", "write:admin:relays", "read:admin:relays", "write:admin:invite-codes", "read:admin:invite-codes", "write:admin:announcements", "read:admin:announcements", "write:admin:avatar-decorations", "read:admin:avatar-decorations", "write:admin:federation", "write:admin:account", "read:admin:account", "write:admin:emoji", "read:admin:emoji", "write:admin:queue", "read:admin:queue", "write:admin:promo", "write:admin:drive", "read:admin:drive", "write:admin:ad", "read:admin:ad", "write:invite-codes", "read:invite-codes", "write:clip-favorite", "read:clip-favorite", "read:federation", "write:report-abuse"];
@@ -2645,6 +2883,15 @@ type PinnedUsersResponse = operations['pinned-users']['responses']['200']['conte
 // @public (undocumented)
 type PromoReadRequest = operations['promo___read']['requestBody']['content']['application/json'];
 
+// Warning: (ae-forgotten-export) The symbol "AllNullRecord" needs to be exported by the entry point index.d.ts
+// Warning: (ae-forgotten-export) The symbol "NonNullableRecord" needs to be exported by the entry point index.d.ts
+//
+// @public (undocumented)
+type PureRenote = Omit<Note, 'renote' | 'renoteId' | 'reply' | 'replyId' | 'text' | 'cw' | 'files' | 'fileIds' | 'poll'> & AllNullRecord<Pick<Note, 'reply' | 'replyId' | 'text' | 'cw' | 'poll'>> & {
+    files: [];
+    fileIds: [];
+} & NonNullableRecord<Pick<Note, 'renote' | 'renoteId'>>;
+
 // @public (undocumented)
 type QueueCount = components['schemas']['QueueCount'];
 
@@ -2724,6 +2971,9 @@ type ReversiShowGameResponse = operations['reversi___show-game']['responses']['2
 // @public (undocumented)
 type ReversiSurrenderRequest = operations['reversi___surrender']['requestBody']['content']['application/json'];
 
+// @public (undocumented)
+export const reversiUpdateKeys: ["map", "bw", "isLlotheo", "canPutEverywhere", "loopedBoard", "timeLimitForEachTurn"];
+
 // @public (undocumented)
 type ReversiVerifyRequest = operations['reversi___verify']['requestBody']['content']['application/json'];
 
@@ -2811,16 +3061,48 @@ type ServerStatsLog = ServerStats[];
 type Signin = components['schemas']['Signin'];
 
 // @public (undocumented)
-type SigninRequest = {
+type SigninFlowRequest = {
     username: string;
-    password: string;
+    password?: string;
     token?: string;
+    credential?: AuthenticationResponseJSON;
+    'hcaptcha-response'?: string | null;
+    'g-recaptcha-response'?: string | null;
+    'turnstile-response'?: string | null;
+    'm-captcha-response'?: string | null;
 };
 
 // @public (undocumented)
-type SigninResponse = {
+type SigninFlowResponse = {
+    finished: true;
     id: User['id'];
     i: string;
+} | {
+    finished: false;
+    next: 'captcha' | 'password' | 'totp';
+} | {
+    finished: false;
+    next: 'passkey';
+    authRequest: PublicKeyCredentialRequestOptionsJSON;
+};
+
+// @public (undocumented)
+type SigninWithPasskeyInitResponse = {
+    option: PublicKeyCredentialRequestOptionsJSON;
+    context: string;
+};
+
+// @public (undocumented)
+type SigninWithPasskeyRequest = {
+    credential?: AuthenticationResponseJSON;
+    context?: string;
+};
+
+// @public (undocumented)
+type SigninWithPasskeyResponse = {
+    signinResponse: SigninFlowResponse & {
+        finished: true;
+    };
 };
 
 // @public (undocumented)
@@ -2844,6 +3126,7 @@ type SignupRequest = {
     'hcaptcha-response'?: string | null;
     'g-recaptcha-response'?: string | null;
     'turnstile-response'?: string | null;
+    'm-captcha-response'?: string | null;
 };
 
 // @public (undocumented)
@@ -2854,45 +3137,45 @@ type SignupResponse = MeDetailed & {
 // @public (undocumented)
 type StatsResponse = operations['stats']['responses']['200']['content']['application/json'];
 
-// Warning: (ae-forgotten-export) The symbol "StreamEvents" needs to be exported by the entry point index.d.ts
-//
 // @public (undocumented)
-export class Stream extends EventEmitter<StreamEvents> {
+export class Stream extends EventEmitter<StreamEvents> implements IStream {
     constructor(origin: string, user: {
         token: string;
     } | null, options?: {
-        WebSocket?: any;
+        WebSocket?: _ReconnectingWebsocket.Options['WebSocket'];
     });
     // (undocumented)
     close(): void;
-    // Warning: (ae-forgotten-export) The symbol "NonSharedConnection" needs to be exported by the entry point index.d.ts
-    //
     // (undocumented)
     disconnectToChannel(connection: NonSharedConnection): void;
     // (undocumented)
     heartbeat(): void;
     // (undocumented)
     ping(): void;
-    // Warning: (ae-forgotten-export) The symbol "SharedConnection" needs to be exported by the entry point index.d.ts
-    //
     // (undocumented)
     removeSharedConnection(connection: SharedConnection): void;
-    // Warning: (ae-forgotten-export) The symbol "Pool" needs to be exported by the entry point index.d.ts
-    //
     // (undocumented)
     removeSharedConnectionPool(pool: Pool): void;
     // (undocumented)
     send(typeOrPayload: string): void;
     // (undocumented)
-    send(typeOrPayload: string, payload: any): void;
+    send(typeOrPayload: string, payload: unknown): void;
     // (undocumented)
-    send(typeOrPayload: Record<string, any> | any[]): void;
+    send(typeOrPayload: Record<string, unknown> | unknown[]): void;
     // (undocumented)
     state: 'initializing' | 'reconnecting' | 'connected';
     // (undocumented)
     useChannel<C extends keyof Channels>(channel: C, params?: Channels[C]['params'], name?: string): ChannelConnection<Channels[C]>;
 }
 
+// Warning: (ae-forgotten-export) The symbol "BroadcastEvents" needs to be exported by the entry point index.d.ts
+//
+// @public (undocumented)
+export type StreamEvents = {
+    _connected_: void;
+    _disconnected_: void;
+} & BroadcastEvents;
+
 // Warning: (ae-forgotten-export) The symbol "SwitchCase" needs to be exported by the entry point index.d.ts
 // Warning: (ae-forgotten-export) The symbol "IsCaseMatched" needs to be exported by the entry point index.d.ts
 // Warning: (ae-forgotten-export) The symbol "GetCaseResult" needs to be exported by the entry point index.d.ts
@@ -2921,6 +3204,9 @@ type SwUpdateRegistrationRequest = operations['sw___update-registration']['reque
 // @public (undocumented)
 type SwUpdateRegistrationResponse = operations['sw___update-registration']['responses']['200']['content']['application/json'];
 
+// @public (undocumented)
+type SystemWebhook = components['schemas']['SystemWebhook'];
+
 // @public (undocumented)
 type TestRequest = operations['test']['requestBody']['content']['application/json'];
 
@@ -3118,7 +3404,9 @@ type UsersUpdateMemoRequest = operations['users___update-memo']['requestBody']['
 
 // Warnings were encountered during analysis:
 //
-// src/entities.ts:25:2 - (ae-forgotten-export) The symbol "ModerationLogPayloads" needs to be exported by the entry point index.d.ts
+// src/entities.ts:50:2 - (ae-forgotten-export) The symbol "ModerationLogPayloads" needs to be exported by the entry point index.d.ts
+// src/streaming.types.ts:220:4 - (ae-forgotten-export) The symbol "ReversiUpdateKey" needs to be exported by the entry point index.d.ts
+// src/streaming.types.ts:230:4 - (ae-forgotten-export) The symbol "ReversiUpdateSettings" needs to be exported by the entry point index.d.ts
 
 // (No @packageDocumentation comment for this package)
 
diff --git a/packages/misskey-js/generator/.eslintrc.cjs b/packages/misskey-js/generator/.eslintrc.cjs
deleted file mode 100644
index 6a8b31da9c..0000000000
--- a/packages/misskey-js/generator/.eslintrc.cjs
+++ /dev/null
@@ -1,9 +0,0 @@
-module.exports = {
-	parserOptions: {
-		tsconfigRootDir: __dirname,
-		project: ['./tsconfig.json'],
-	},
-	extends: [
-		'../../shared/.eslintrc.js',
-	],
-};
diff --git a/packages/misskey-js/generator/eslint.config.js b/packages/misskey-js/generator/eslint.config.js
new file mode 100644
index 0000000000..4bf78c3b91
--- /dev/null
+++ b/packages/misskey-js/generator/eslint.config.js
@@ -0,0 +1,17 @@
+import tsParser from '@typescript-eslint/parser';
+import sharedConfig from '../../shared/eslint.config.js';
+
+export default [
+	...sharedConfig,
+	{
+		files: ['**/*.ts', '**/*.tsx'],
+		languageOptions: {
+			parserOptions: {
+				parser: tsParser,
+				project: ['./tsconfig.json'],
+				sourceType: 'module',
+				tsconfigRootDir: import.meta.dirname,
+			},
+		},
+	},
+];
diff --git a/packages/misskey-js/generator/package.json b/packages/misskey-js/generator/package.json
index a1c0f41cb2..212c92fba5 100644
--- a/packages/misskey-js/generator/package.json
+++ b/packages/misskey-js/generator/package.json
@@ -4,20 +4,18 @@
 	"description": "Misskey TypeGenerator",
 	"type": "module",
 	"scripts": {
-		"generate": "tsx src/generator.ts && eslint ./built/**/* --ext .ts --fix"
+		"generate": "tsx src/generator.ts && eslint ./built/**/*.ts --fix"
 	},
 	"devDependencies": {
-		"@misskey-dev/eslint-plugin": "^1.0.0",
-		"@readme/openapi-parser": "2.5.0",
+		"@readme/openapi-parser": "2.6.0",
 		"@types/node": "20.9.1",
-		"@typescript-eslint/eslint-plugin": "6.11.0",
-		"@typescript-eslint/parser": "6.11.0",
-		"eslint": "8.53.0",
+		"@typescript-eslint/eslint-plugin": "7.17.0",
+		"@typescript-eslint/parser": "7.17.0",
 		"openapi-types": "12.1.3",
 		"openapi-typescript": "6.7.3",
-		"ts-case-convert": "2.0.2",
+		"ts-case-convert": "2.0.7",
 		"tsx": "4.4.0",
-		"typescript": "5.3.3"
+		"typescript": "5.6.2"
 	},
 	"files": [
 		"built"
diff --git a/packages/misskey-js/generator/src/generator.ts b/packages/misskey-js/generator/src/generator.ts
index 78178d7c7e..88f2ae9ee9 100644
--- a/packages/misskey-js/generator/src/generator.ts
+++ b/packages/misskey-js/generator/src/generator.ts
@@ -20,7 +20,14 @@ async function generateBaseTypes(
 	}
 	lines.push('');
 
-	const generatedTypes = await openapiTS(openApiJsonPath, { exportType: true });
+	const generatedTypes = await openapiTS(openApiJsonPath, {
+		exportType: true,
+		transform(schemaObject) {
+			if ('format' in schemaObject && schemaObject.format === 'binary') {
+				return schemaObject.nullable ? 'Blob | null' : 'Blob';
+			}
+		},
+	});
 	lines.push(generatedTypes);
 	lines.push('');
 
@@ -56,6 +63,8 @@ async function generateEndpoints(
 	endpointOutputPath: string,
 ) {
 	const endpoints: Endpoint[] = [];
+	const endpointReqMediaTypes: EndpointReqMediaType[] = [];
+	const endpointReqMediaTypesSet = new Set<string>();
 
 	// misskey-jsはPOST固定で送っているので、こちらも決め打ちする。別メソッドに対応することがあればこちらも直す必要あり
 	const paths = openApiDocs.paths ?? {};
@@ -78,12 +87,19 @@ async function generateEndpoints(
 			const supportMediaTypes = Object.keys(reqContent);
 			if (supportMediaTypes.length > 0) {
 				// いまのところ複数のメディアタイプをとるエンドポイントは無いので決め打ちする
-				endpoint.request = new OperationTypeAlias(
+				const req = new OperationTypeAlias(
 					operationId,
 					path,
 					supportMediaTypes[0],
 					OperationsAliasType.REQUEST,
 				);
+				endpoint.request = req;
+
+				const reqType = new EndpointReqMediaType(path, req);
+				if (reqType.getMediaType() !== 'application/json') {
+					endpointReqMediaTypesSet.add(reqType.getMediaType());
+					endpointReqMediaTypes.push(reqType);
+				}
 			}
 		}
 
@@ -137,6 +153,22 @@ async function generateEndpoints(
 	endpointOutputLine.push('}');
 	endpointOutputLine.push('');
 
+	function generateEndpointReqMediaTypesType() {
+		return `{ [K in keyof Endpoints]?: ${[...endpointReqMediaTypesSet].map((t) => `'${t}'`).join(' | ')}; }`;
+	}
+
+	endpointOutputLine.push(`/**
+ * NOTE: The content-type for all endpoints not listed here is application/json.
+ */`);
+	endpointOutputLine.push('export const endpointReqTypes = {');
+
+	endpointOutputLine.push(
+		...endpointReqMediaTypes.map(it => '\t' + it.toLine()),
+	);
+
+	endpointOutputLine.push(`} as const satisfies ${generateEndpointReqMediaTypesType()};`);
+	endpointOutputLine.push('');
+
 	await writeFile(endpointOutputPath, endpointOutputLine.join('\n'));
 }
 
@@ -314,6 +346,26 @@ class Endpoint {
 	}
 }
 
+class EndpointReqMediaType {
+	public readonly path: string;
+	public readonly mediaType: string;
+
+	constructor(path: string, request: OperationTypeAlias, mediaType?: undefined);
+	constructor(path: string, request: undefined, mediaType: string);
+	constructor(path: string, request: OperationTypeAlias | undefined, mediaType?: string) {
+		this.path = path;
+		this.mediaType = mediaType ?? request?.mediaType ?? 'application/json';
+	}
+
+	getMediaType(): string {
+		return this.mediaType;
+	}
+
+	toLine(): string {
+		return `'${this.path}': '${this.mediaType}',`;
+	}
+}
+
 async function main() {
 	const generatePath = './built/autogen';
 	await mkdir(generatePath, { recursive: true });
diff --git a/packages/misskey-js/package.json b/packages/misskey-js/package.json
index 4ff1a57309..32d6c8b0cb 100644
--- a/packages/misskey-js/package.json
+++ b/packages/misskey-js/package.json
@@ -1,7 +1,7 @@
 {
 	"type": "module",
 	"name": "misskey-js",
-	"version": "2024.5.0",
+	"version": "2024.10.2-alpha.2",
 	"description": "Misskey SDK for JavaScript",
 	"license": "MIT",
 	"main": "./built/index.js",
@@ -22,7 +22,7 @@
 		"tsd": "tsd",
 		"api": "pnpm api-extractor run --local --verbose",
 		"api-prod": "pnpm api-extractor run --verbose",
-		"eslint": "eslint . --ext .js,.jsx,.ts,.tsx",
+		"eslint": "eslint './**/*.{js,jsx,ts,tsx}'",
 		"typecheck": "tsc --noEmit",
 		"lint": "pnpm typecheck && pnpm eslint",
 		"jest": "jest --coverage --detectOpenHandles",
@@ -35,30 +35,29 @@
 		"directory": "packages/misskey-js"
 	},
 	"devDependencies": {
-		"@microsoft/api-extractor": "7.43.1",
-		"@misskey-dev/eslint-plugin": "1.0.0",
+		"@microsoft/api-extractor": "7.47.9",
 		"@swc/jest": "0.2.36",
-		"@types/jest": "29.5.12",
-		"@types/node": "20.12.7",
-		"@typescript-eslint/eslint-plugin": "7.7.1",
-		"@typescript-eslint/parser": "7.7.1",
-		"eslint": "8.57.0",
+		"@types/jest": "29.5.13",
+		"@types/node": "20.14.12",
+		"@typescript-eslint/eslint-plugin": "7.17.0",
+		"@typescript-eslint/parser": "7.17.0",
 		"jest": "29.7.0",
 		"jest-fetch-mock": "3.0.3",
 		"jest-websocket-mock": "2.5.0",
 		"mock-socket": "9.3.1",
 		"ncp": "2.0.0",
-		"nodemon": "3.1.0",
-		"execa": "8.0.1",
-		"tsd": "0.30.7",
-		"typescript": "5.4.5",
-		"esbuild": "0.19.11",
-		"glob": "10.3.12"
+		"nodemon": "3.1.7",
+		"execa": "9.4.0",
+		"tsd": "0.31.2",
+		"typescript": "5.6.2",
+		"esbuild": "0.23.1",
+		"glob": "11.0.0"
 	},
 	"files": [
 		"built"
 	],
 	"dependencies": {
+		"@simplewebauthn/types": "10.0.0",
 		"eventemitter3": "5.0.1",
 		"reconnecting-websocket": "4.4.0"
 	}
diff --git a/packages/misskey-js/src/acct.ts b/packages/misskey-js/src/acct.ts
index b25bc564ea..aa8658cdbd 100644
--- a/packages/misskey-js/src/acct.ts
+++ b/packages/misskey-js/src/acct.ts
@@ -3,7 +3,8 @@ export type Acct = {
 	host: string | null;
 };
 
-export function parse(acct: string): Acct {
+export function parse(_acct: string): Acct {
+	let acct = _acct;
 	if (acct.startsWith('@')) acct = acct.substring(1);
 	const split = acct.split('@', 2);
 	return { username: split[0], host: split[1] || null };
diff --git a/packages/misskey-js/src/api.ts b/packages/misskey-js/src/api.ts
index 959a634a74..ed1282957f 100644
--- a/packages/misskey-js/src/api.ts
+++ b/packages/misskey-js/src/api.ts
@@ -1,7 +1,7 @@
 import './autogen/apiClientJSDoc.js';
 
-import { SwitchCaseResponseType } from './api.types.js';
-import type { Endpoints } from './api.types.js';
+import { endpointReqTypes } from './autogen/endpoint.js';
+import type { SwitchCaseResponseType, Endpoints } from './api.types.js';
 
 export type {
 	SwitchCaseResponseType,
@@ -14,6 +14,7 @@ export type APIError = {
 	code: string;
 	message: string;
 	kind: 'client' | 'server';
+	// eslint-disable-next-line @typescript-eslint/no-explicit-any
 	info: Record<string, any>;
 };
 
@@ -23,12 +24,13 @@ export function isAPIError(reason: Record<PropertyKey, unknown>): reason is APIE
 
 export type FetchLike = (input: string, init?: {
 	method?: string;
-	body?: string;
+	body?: Blob | FormData | string;
 	credentials?: RequestCredentials;
 	cache?: RequestCache;
 	headers: { [key in string]: string }
 }) => Promise<{
 	status: number;
+	// eslint-disable-next-line @typescript-eslint/no-explicit-any
 	json(): Promise<any>;
 }>;
 
@@ -49,20 +51,63 @@ export class APIClient {
 		this.fetch = opts.fetch ?? ((...args) => fetch(...args));
 	}
 
+	// eslint-disable-next-line @typescript-eslint/no-explicit-any
+	private assertIsRecord<T>(obj: T): obj is T & Record<string, any> {
+		return obj !== null && typeof obj === 'object' && !Array.isArray(obj);
+	}
+
+	private assertSpecialEpReqType(ep: keyof Endpoints): ep is keyof typeof endpointReqTypes {
+		return ep in endpointReqTypes;
+	}
+
 	public request<E extends keyof Endpoints, P extends Endpoints[E]['req']>(
 		endpoint: E,
 		params: P = {} as P,
 		credential?: string | null,
 	): Promise<SwitchCaseResponseType<E, P>> {
 		return new Promise((resolve, reject) => {
-			this.fetch(`${this.origin}/api/${endpoint}`, {
-				method: 'POST',
-				body: JSON.stringify({
+			let mediaType = 'application/json';
+			// (autogenがバグったときのため、念の為nullチェックも行う)
+			// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
+			if (this.assertSpecialEpReqType(endpoint) && endpointReqTypes[endpoint] != null) {
+				mediaType = endpointReqTypes[endpoint];
+			}
+
+			let payload: FormData | string = '{}';
+
+			if (mediaType === 'application/json') {
+				payload = JSON.stringify({
 					...params,
 					i: credential !== undefined ? credential : this.credential,
-				}),
+				});
+			} else if (mediaType === 'multipart/form-data') {
+				payload = new FormData();
+				const i = credential !== undefined ? credential : this.credential;
+				if (i != null) {
+					payload.append('i', i);
+				}
+				if (this.assertIsRecord(params)) {
+					for (const key in params) {
+						const value = params[key];
+
+						if (value == null) continue;
+
+						if (value instanceof File || value instanceof Blob) {
+							payload.append(key, value);
+						} else if (typeof value === 'object') {
+							payload.append(key, JSON.stringify(value));
+						} else {
+							payload.append(key, value);
+						}
+					}
+				}
+			}
+
+			this.fetch(`${this.origin}/api/${endpoint}`, {
+				method: 'POST',
+				body: payload,
 				headers: {
-					'Content-Type': 'application/json',
+					'Content-Type': mediaType,
 				},
 				credentials: 'omit',
 				cache: 'no-cache',
diff --git a/packages/misskey-js/src/api.types.ts b/packages/misskey-js/src/api.types.ts
index af0bade5b3..838949f8e1 100644
--- a/packages/misskey-js/src/api.types.ts
+++ b/packages/misskey-js/src/api.types.ts
@@ -1,9 +1,13 @@
 import { Endpoints as Gen } from './autogen/endpoint.js';
 import { UserDetailed } from './autogen/models.js';
-import { UsersShowRequest } from './autogen/entities.js';
+import { AdminRolesCreateRequest, AdminRolesCreateResponse, UsersShowRequest } from './autogen/entities.js';
 import {
-	SigninRequest,
-	SigninResponse,
+	PartialRolePolicyOverride,
+	SigninFlowRequest,
+	SigninFlowResponse,
+	SigninWithPasskeyInitResponse,
+	SigninWithPasskeyRequest,
+	SigninWithPasskeyResponse,
 	SignupPendingRequest,
 	SignupPendingResponse,
 	SignupRequest,
@@ -27,11 +31,13 @@ type StrictExtract<Union, Cond> = Cond extends Union ? Union : never;
 
 type IsCaseMatched<E extends keyof Endpoints, P extends Endpoints[E]['req'], C extends number> =
 	Endpoints[E]['res'] extends SwitchCase
+		// eslint-disable-next-line @typescript-eslint/no-explicit-any
 		? IsNeverType<StrictExtract<Endpoints[E]['res']['$switch']['$cases'][C], [P, any]>> extends false ? true : false
 		: false
 
 type GetCaseResult<E extends keyof Endpoints, P extends Endpoints[E]['req'], C extends number> =
 	Endpoints[E]['res'] extends SwitchCase
+		// eslint-disable-next-line @typescript-eslint/no-explicit-any
 		? StrictExtract<Endpoints[E]['res']['$switch']['$cases'][C], [P, any]>[1]
 		: never
 
@@ -75,9 +81,29 @@ export type Endpoints = Overwrite<
 			res: SignupPendingResponse;
 		},
 		// api.jsonには載せないものなのでここで定義
-		'signin': {
-			req: SigninRequest;
-			res: SigninResponse;
+		'signin-flow': {
+			req: SigninFlowRequest;
+			res: SigninFlowResponse;
 		},
+		'signin-with-passkey': {
+			req: SigninWithPasskeyRequest;
+			res: {
+				$switch: {
+					$cases: [
+						[
+							{
+								context: string;
+							},
+							SigninWithPasskeyResponse,
+						],
+					];
+					$default: SigninWithPasskeyInitResponse;
+				},
+			},
+		},
+		'admin/roles/create': {
+			req: Overwrite<AdminRolesCreateRequest, { policies: PartialRolePolicyOverride }>;
+			res: AdminRolesCreateResponse;
+		}
 	}
 >
diff --git a/packages/misskey-js/src/autogen/apiClientJSDoc.ts b/packages/misskey-js/src/autogen/apiClientJSDoc.ts
index 181f7274b7..e2c7cbba52 100644
--- a/packages/misskey-js/src/autogen/apiClientJSDoc.ts
+++ b/packages/misskey-js/src/autogen/apiClientJSDoc.ts
@@ -25,6 +25,66 @@ declare module '../api.js' {
       credential?: string | null,
     ): Promise<SwitchCaseResponseType<E, P>>;
 
+    /**
+     * No description provided.
+     * 
+     * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
+     * **Credential required**: *Yes* / **Permission**: *read:admin:abuse-report:notification-recipient*
+     */
+    request<E extends 'admin/abuse-report/notification-recipient/list', P extends Endpoints[E]['req']>(
+      endpoint: E,
+      params: P,
+      credential?: string | null,
+    ): Promise<SwitchCaseResponseType<E, P>>;
+
+    /**
+     * No description provided.
+     * 
+     * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
+     * **Credential required**: *Yes* / **Permission**: *read:admin:abuse-report:notification-recipient*
+     */
+    request<E extends 'admin/abuse-report/notification-recipient/show', P extends Endpoints[E]['req']>(
+      endpoint: E,
+      params: P,
+      credential?: string | null,
+    ): Promise<SwitchCaseResponseType<E, P>>;
+
+    /**
+     * No description provided.
+     * 
+     * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
+     * **Credential required**: *Yes* / **Permission**: *write:admin:abuse-report:notification-recipient*
+     */
+    request<E extends 'admin/abuse-report/notification-recipient/create', P extends Endpoints[E]['req']>(
+      endpoint: E,
+      params: P,
+      credential?: string | null,
+    ): Promise<SwitchCaseResponseType<E, P>>;
+
+    /**
+     * No description provided.
+     * 
+     * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
+     * **Credential required**: *Yes* / **Permission**: *write:admin:abuse-report:notification-recipient*
+     */
+    request<E extends 'admin/abuse-report/notification-recipient/update', P extends Endpoints[E]['req']>(
+      endpoint: E,
+      params: P,
+      credential?: string | null,
+    ): Promise<SwitchCaseResponseType<E, P>>;
+
+    /**
+     * No description provided.
+     * 
+     * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
+     * **Credential required**: *Yes* / **Permission**: *write:admin:abuse-report:notification-recipient*
+     */
+    request<E extends 'admin/abuse-report/notification-recipient/delete', P extends Endpoints[E]['req']>(
+      endpoint: E,
+      params: P,
+      credential?: string | null,
+    ): Promise<SwitchCaseResponseType<E, P>>;
+
     /**
      * No description provided.
      * 
@@ -631,6 +691,28 @@ declare module '../api.js' {
       credential?: string | null,
     ): Promise<SwitchCaseResponseType<E, P>>;
 
+    /**
+     * No description provided.
+     * 
+     * **Credential required**: *Yes* / **Permission**: *write:admin:resolve-abuse-user-report*
+     */
+    request<E extends 'admin/forward-abuse-user-report', P extends Endpoints[E]['req']>(
+      endpoint: E,
+      params: P,
+      credential?: string | null,
+    ): Promise<SwitchCaseResponseType<E, P>>;
+
+    /**
+     * No description provided.
+     * 
+     * **Credential required**: *Yes* / **Permission**: *write:admin:resolve-abuse-user-report*
+     */
+    request<E extends 'admin/update-abuse-user-report', P extends Endpoints[E]['req']>(
+      endpoint: E,
+      params: P,
+      credential?: string | null,
+    ): Promise<SwitchCaseResponseType<E, P>>;
+
     /**
      * No description provided.
      * 
@@ -840,6 +922,78 @@ declare module '../api.js' {
       credential?: string | null,
     ): Promise<SwitchCaseResponseType<E, P>>;
 
+    /**
+     * No description provided.
+     * 
+     * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
+     * **Credential required**: *Yes* / **Permission**: *write:admin:system-webhook*
+     */
+    request<E extends 'admin/system-webhook/create', P extends Endpoints[E]['req']>(
+      endpoint: E,
+      params: P,
+      credential?: string | null,
+    ): Promise<SwitchCaseResponseType<E, P>>;
+
+    /**
+     * No description provided.
+     * 
+     * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
+     * **Credential required**: *Yes* / **Permission**: *write:admin:system-webhook*
+     */
+    request<E extends 'admin/system-webhook/delete', P extends Endpoints[E]['req']>(
+      endpoint: E,
+      params: P,
+      credential?: string | null,
+    ): Promise<SwitchCaseResponseType<E, P>>;
+
+    /**
+     * No description provided.
+     * 
+     * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
+     * **Credential required**: *Yes* / **Permission**: *write:admin:system-webhook*
+     */
+    request<E extends 'admin/system-webhook/list', P extends Endpoints[E]['req']>(
+      endpoint: E,
+      params: P,
+      credential?: string | null,
+    ): Promise<SwitchCaseResponseType<E, P>>;
+
+    /**
+     * No description provided.
+     * 
+     * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
+     * **Credential required**: *Yes* / **Permission**: *write:admin:system-webhook*
+     */
+    request<E extends 'admin/system-webhook/show', P extends Endpoints[E]['req']>(
+      endpoint: E,
+      params: P,
+      credential?: string | null,
+    ): Promise<SwitchCaseResponseType<E, P>>;
+
+    /**
+     * No description provided.
+     * 
+     * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
+     * **Credential required**: *Yes* / **Permission**: *write:admin:system-webhook*
+     */
+    request<E extends 'admin/system-webhook/update', P extends Endpoints[E]['req']>(
+      endpoint: E,
+      params: P,
+      credential?: string | null,
+    ): Promise<SwitchCaseResponseType<E, P>>;
+
+    /**
+     * No description provided.
+     * 
+     * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
+     * **Credential required**: *Yes* / **Permission**: *read:admin:system-webhook*
+     */
+    request<E extends 'admin/system-webhook/test', P extends Endpoints[E]['req']>(
+      endpoint: E,
+      params: P,
+      credential?: string | null,
+    ): Promise<SwitchCaseResponseType<E, P>>;
+
     /**
      * No description provided.
      * 
@@ -2699,6 +2853,18 @@ declare module '../api.js' {
       credential?: string | null,
     ): Promise<SwitchCaseResponseType<E, P>>;
 
+    /**
+     * No description provided.
+     * 
+     * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
+     * **Credential required**: *Yes* / **Permission**: *read:account*
+     */
+    request<E extends 'i/webhooks/test', P extends Endpoints[E]['req']>(
+      endpoint: E,
+      params: P,
+      credential?: string | null,
+    ): Promise<SwitchCaseResponseType<E, P>>;
+
     /**
      * No description provided.
      * 
diff --git a/packages/misskey-js/src/autogen/endpoint.ts b/packages/misskey-js/src/autogen/endpoint.ts
index ab3baf1670..5e6bc0a99c 100644
--- a/packages/misskey-js/src/autogen/endpoint.ts
+++ b/packages/misskey-js/src/autogen/endpoint.ts
@@ -4,6 +4,15 @@ import type {
 	AdminMetaResponse,
 	AdminAbuseUserReportsRequest,
 	AdminAbuseUserReportsResponse,
+	AdminAbuseReportNotificationRecipientListRequest,
+	AdminAbuseReportNotificationRecipientListResponse,
+	AdminAbuseReportNotificationRecipientShowRequest,
+	AdminAbuseReportNotificationRecipientShowResponse,
+	AdminAbuseReportNotificationRecipientCreateRequest,
+	AdminAbuseReportNotificationRecipientCreateResponse,
+	AdminAbuseReportNotificationRecipientUpdateRequest,
+	AdminAbuseReportNotificationRecipientUpdateResponse,
+	AdminAbuseReportNotificationRecipientDeleteRequest,
 	AdminAccountsCreateRequest,
 	AdminAccountsCreateResponse,
 	AdminAccountsDeleteRequest,
@@ -22,6 +31,7 @@ import type {
 	AdminAnnouncementsListResponse,
 	AdminAnnouncementsUpdateRequest,
 	AdminAvatarDecorationsCreateRequest,
+	AdminAvatarDecorationsCreateResponse,
 	AdminAvatarDecorationsDeleteRequest,
 	AdminAvatarDecorationsListRequest,
 	AdminAvatarDecorationsListResponse,
@@ -74,6 +84,8 @@ import type {
 	AdminResetPasswordRequest,
 	AdminResetPasswordResponse,
 	AdminResolveAbuseUserReportRequest,
+	AdminForwardAbuseUserReportRequest,
+	AdminUpdateAbuseUserReportRequest,
 	AdminSendEmailRequest,
 	AdminServerInfoResponse,
 	AdminShowModerationLogsRequest,
@@ -99,6 +111,16 @@ import type {
 	AdminRolesUpdateDefaultPoliciesRequest,
 	AdminRolesUsersRequest,
 	AdminRolesUsersResponse,
+	AdminSystemWebhookCreateRequest,
+	AdminSystemWebhookCreateResponse,
+	AdminSystemWebhookDeleteRequest,
+	AdminSystemWebhookListRequest,
+	AdminSystemWebhookListResponse,
+	AdminSystemWebhookShowRequest,
+	AdminSystemWebhookShowResponse,
+	AdminSystemWebhookUpdateRequest,
+	AdminSystemWebhookUpdateResponse,
+	AdminSystemWebhookTestRequest,
 	AnnouncementsRequest,
 	AnnouncementsResponse,
 	AnnouncementsShowRequest,
@@ -358,6 +380,7 @@ import type {
 	IWebhooksShowResponse,
 	IWebhooksUpdateRequest,
 	IWebhooksDeleteRequest,
+	IWebhooksTestRequest,
 	InviteCreateResponse,
 	InviteDeleteRequest,
 	InviteListRequest,
@@ -445,6 +468,7 @@ import type {
 	FlashCreateRequest,
 	FlashCreateResponse,
 	FlashDeleteRequest,
+	FlashFeaturedRequest,
 	FlashFeaturedResponse,
 	FlashLikeRequest,
 	FlashShowRequest,
@@ -558,6 +582,11 @@ import type {
 export type Endpoints = {
 	'admin/meta': { req: EmptyRequest; res: AdminMetaResponse };
 	'admin/abuse-user-reports': { req: AdminAbuseUserReportsRequest; res: AdminAbuseUserReportsResponse };
+	'admin/abuse-report/notification-recipient/list': { req: AdminAbuseReportNotificationRecipientListRequest; res: AdminAbuseReportNotificationRecipientListResponse };
+	'admin/abuse-report/notification-recipient/show': { req: AdminAbuseReportNotificationRecipientShowRequest; res: AdminAbuseReportNotificationRecipientShowResponse };
+	'admin/abuse-report/notification-recipient/create': { req: AdminAbuseReportNotificationRecipientCreateRequest; res: AdminAbuseReportNotificationRecipientCreateResponse };
+	'admin/abuse-report/notification-recipient/update': { req: AdminAbuseReportNotificationRecipientUpdateRequest; res: AdminAbuseReportNotificationRecipientUpdateResponse };
+	'admin/abuse-report/notification-recipient/delete': { req: AdminAbuseReportNotificationRecipientDeleteRequest; res: EmptyResponse };
 	'admin/accounts/create': { req: AdminAccountsCreateRequest; res: AdminAccountsCreateResponse };
 	'admin/accounts/delete': { req: AdminAccountsDeleteRequest; res: EmptyResponse };
 	'admin/accounts/find-by-email': { req: AdminAccountsFindByEmailRequest; res: AdminAccountsFindByEmailResponse };
@@ -569,7 +598,7 @@ export type Endpoints = {
 	'admin/announcements/delete': { req: AdminAnnouncementsDeleteRequest; res: EmptyResponse };
 	'admin/announcements/list': { req: AdminAnnouncementsListRequest; res: AdminAnnouncementsListResponse };
 	'admin/announcements/update': { req: AdminAnnouncementsUpdateRequest; res: EmptyResponse };
-	'admin/avatar-decorations/create': { req: AdminAvatarDecorationsCreateRequest; res: EmptyResponse };
+	'admin/avatar-decorations/create': { req: AdminAvatarDecorationsCreateRequest; res: AdminAvatarDecorationsCreateResponse };
 	'admin/avatar-decorations/delete': { req: AdminAvatarDecorationsDeleteRequest; res: EmptyResponse };
 	'admin/avatar-decorations/list': { req: AdminAvatarDecorationsListRequest; res: AdminAvatarDecorationsListResponse };
 	'admin/avatar-decorations/update': { req: AdminAvatarDecorationsUpdateRequest; res: EmptyResponse };
@@ -613,6 +642,8 @@ export type Endpoints = {
 	'admin/relays/remove': { req: AdminRelaysRemoveRequest; res: EmptyResponse };
 	'admin/reset-password': { req: AdminResetPasswordRequest; res: AdminResetPasswordResponse };
 	'admin/resolve-abuse-user-report': { req: AdminResolveAbuseUserReportRequest; res: EmptyResponse };
+	'admin/forward-abuse-user-report': { req: AdminForwardAbuseUserReportRequest; res: EmptyResponse };
+	'admin/update-abuse-user-report': { req: AdminUpdateAbuseUserReportRequest; res: EmptyResponse };
 	'admin/send-email': { req: AdminSendEmailRequest; res: EmptyResponse };
 	'admin/server-info': { req: EmptyRequest; res: AdminServerInfoResponse };
 	'admin/show-moderation-logs': { req: AdminShowModerationLogsRequest; res: AdminShowModerationLogsResponse };
@@ -632,6 +663,12 @@ export type Endpoints = {
 	'admin/roles/unassign': { req: AdminRolesUnassignRequest; res: EmptyResponse };
 	'admin/roles/update-default-policies': { req: AdminRolesUpdateDefaultPoliciesRequest; res: EmptyResponse };
 	'admin/roles/users': { req: AdminRolesUsersRequest; res: AdminRolesUsersResponse };
+	'admin/system-webhook/create': { req: AdminSystemWebhookCreateRequest; res: AdminSystemWebhookCreateResponse };
+	'admin/system-webhook/delete': { req: AdminSystemWebhookDeleteRequest; res: EmptyResponse };
+	'admin/system-webhook/list': { req: AdminSystemWebhookListRequest; res: AdminSystemWebhookListResponse };
+	'admin/system-webhook/show': { req: AdminSystemWebhookShowRequest; res: AdminSystemWebhookShowResponse };
+	'admin/system-webhook/update': { req: AdminSystemWebhookUpdateRequest; res: AdminSystemWebhookUpdateResponse };
+	'admin/system-webhook/test': { req: AdminSystemWebhookTestRequest; res: EmptyResponse };
 	'announcements': { req: AnnouncementsRequest; res: AnnouncementsResponse };
 	'announcements/show': { req: AnnouncementsShowRequest; res: AnnouncementsShowResponse };
 	'antennas/create': { req: AntennasCreateRequest; res: AntennasCreateResponse };
@@ -798,6 +835,7 @@ export type Endpoints = {
 	'i/webhooks/show': { req: IWebhooksShowRequest; res: IWebhooksShowResponse };
 	'i/webhooks/update': { req: IWebhooksUpdateRequest; res: EmptyResponse };
 	'i/webhooks/delete': { req: IWebhooksDeleteRequest; res: EmptyResponse };
+	'i/webhooks/test': { req: IWebhooksTestRequest; res: EmptyResponse };
 	'invite/create': { req: EmptyRequest; res: InviteCreateResponse };
 	'invite/delete': { req: InviteDeleteRequest; res: EmptyResponse };
 	'invite/list': { req: InviteListRequest; res: InviteListResponse };
@@ -857,7 +895,7 @@ export type Endpoints = {
 	'pages/update': { req: PagesUpdateRequest; res: EmptyResponse };
 	'flash/create': { req: FlashCreateRequest; res: FlashCreateResponse };
 	'flash/delete': { req: FlashDeleteRequest; res: EmptyResponse };
-	'flash/featured': { req: EmptyRequest; res: FlashFeaturedResponse };
+	'flash/featured': { req: FlashFeaturedRequest; res: FlashFeaturedResponse };
 	'flash/like': { req: FlashLikeRequest; res: EmptyResponse };
 	'flash/show': { req: FlashShowRequest; res: FlashShowResponse };
 	'flash/unlike': { req: FlashUnlikeRequest; res: EmptyResponse };
@@ -926,3 +964,10 @@ export type Endpoints = {
 	'reversi/surrender': { req: ReversiSurrenderRequest; res: EmptyResponse };
 	'reversi/verify': { req: ReversiVerifyRequest; res: ReversiVerifyResponse };
 }
+
+/**
+ * NOTE: The content-type for all endpoints not listed here is application/json.
+ */
+export const endpointReqTypes = {
+	'drive/files/create': 'multipart/form-data',
+} as const satisfies { [K in keyof Endpoints]?: 'multipart/form-data'; };
diff --git a/packages/misskey-js/src/autogen/entities.ts b/packages/misskey-js/src/autogen/entities.ts
index 02ca932d8a..f3ddf64481 100644
--- a/packages/misskey-js/src/autogen/entities.ts
+++ b/packages/misskey-js/src/autogen/entities.ts
@@ -7,6 +7,15 @@ export type EmptyResponse = Record<string, unknown> | undefined;
 export type AdminMetaResponse = operations['admin___meta']['responses']['200']['content']['application/json'];
 export type AdminAbuseUserReportsRequest = operations['admin___abuse-user-reports']['requestBody']['content']['application/json'];
 export type AdminAbuseUserReportsResponse = operations['admin___abuse-user-reports']['responses']['200']['content']['application/json'];
+export type AdminAbuseReportNotificationRecipientListRequest = operations['admin___abuse-report___notification-recipient___list']['requestBody']['content']['application/json'];
+export type AdminAbuseReportNotificationRecipientListResponse = operations['admin___abuse-report___notification-recipient___list']['responses']['200']['content']['application/json'];
+export type AdminAbuseReportNotificationRecipientShowRequest = operations['admin___abuse-report___notification-recipient___show']['requestBody']['content']['application/json'];
+export type AdminAbuseReportNotificationRecipientShowResponse = operations['admin___abuse-report___notification-recipient___show']['responses']['200']['content']['application/json'];
+export type AdminAbuseReportNotificationRecipientCreateRequest = operations['admin___abuse-report___notification-recipient___create']['requestBody']['content']['application/json'];
+export type AdminAbuseReportNotificationRecipientCreateResponse = operations['admin___abuse-report___notification-recipient___create']['responses']['200']['content']['application/json'];
+export type AdminAbuseReportNotificationRecipientUpdateRequest = operations['admin___abuse-report___notification-recipient___update']['requestBody']['content']['application/json'];
+export type AdminAbuseReportNotificationRecipientUpdateResponse = operations['admin___abuse-report___notification-recipient___update']['responses']['200']['content']['application/json'];
+export type AdminAbuseReportNotificationRecipientDeleteRequest = operations['admin___abuse-report___notification-recipient___delete']['requestBody']['content']['application/json'];
 export type AdminAccountsCreateRequest = operations['admin___accounts___create']['requestBody']['content']['application/json'];
 export type AdminAccountsCreateResponse = operations['admin___accounts___create']['responses']['200']['content']['application/json'];
 export type AdminAccountsDeleteRequest = operations['admin___accounts___delete']['requestBody']['content']['application/json'];
@@ -25,6 +34,7 @@ export type AdminAnnouncementsListRequest = operations['admin___announcements___
 export type AdminAnnouncementsListResponse = operations['admin___announcements___list']['responses']['200']['content']['application/json'];
 export type AdminAnnouncementsUpdateRequest = operations['admin___announcements___update']['requestBody']['content']['application/json'];
 export type AdminAvatarDecorationsCreateRequest = operations['admin___avatar-decorations___create']['requestBody']['content']['application/json'];
+export type AdminAvatarDecorationsCreateResponse = operations['admin___avatar-decorations___create']['responses']['200']['content']['application/json'];
 export type AdminAvatarDecorationsDeleteRequest = operations['admin___avatar-decorations___delete']['requestBody']['content']['application/json'];
 export type AdminAvatarDecorationsListRequest = operations['admin___avatar-decorations___list']['requestBody']['content']['application/json'];
 export type AdminAvatarDecorationsListResponse = operations['admin___avatar-decorations___list']['responses']['200']['content']['application/json'];
@@ -77,6 +87,8 @@ export type AdminRelaysRemoveRequest = operations['admin___relays___remove']['re
 export type AdminResetPasswordRequest = operations['admin___reset-password']['requestBody']['content']['application/json'];
 export type AdminResetPasswordResponse = operations['admin___reset-password']['responses']['200']['content']['application/json'];
 export type AdminResolveAbuseUserReportRequest = operations['admin___resolve-abuse-user-report']['requestBody']['content']['application/json'];
+export type AdminForwardAbuseUserReportRequest = operations['admin___forward-abuse-user-report']['requestBody']['content']['application/json'];
+export type AdminUpdateAbuseUserReportRequest = operations['admin___update-abuse-user-report']['requestBody']['content']['application/json'];
 export type AdminSendEmailRequest = operations['admin___send-email']['requestBody']['content']['application/json'];
 export type AdminServerInfoResponse = operations['admin___server-info']['responses']['200']['content']['application/json'];
 export type AdminShowModerationLogsRequest = operations['admin___show-moderation-logs']['requestBody']['content']['application/json'];
@@ -102,6 +114,16 @@ export type AdminRolesUnassignRequest = operations['admin___roles___unassign']['
 export type AdminRolesUpdateDefaultPoliciesRequest = operations['admin___roles___update-default-policies']['requestBody']['content']['application/json'];
 export type AdminRolesUsersRequest = operations['admin___roles___users']['requestBody']['content']['application/json'];
 export type AdminRolesUsersResponse = operations['admin___roles___users']['responses']['200']['content']['application/json'];
+export type AdminSystemWebhookCreateRequest = operations['admin___system-webhook___create']['requestBody']['content']['application/json'];
+export type AdminSystemWebhookCreateResponse = operations['admin___system-webhook___create']['responses']['200']['content']['application/json'];
+export type AdminSystemWebhookDeleteRequest = operations['admin___system-webhook___delete']['requestBody']['content']['application/json'];
+export type AdminSystemWebhookListRequest = operations['admin___system-webhook___list']['requestBody']['content']['application/json'];
+export type AdminSystemWebhookListResponse = operations['admin___system-webhook___list']['responses']['200']['content']['application/json'];
+export type AdminSystemWebhookShowRequest = operations['admin___system-webhook___show']['requestBody']['content']['application/json'];
+export type AdminSystemWebhookShowResponse = operations['admin___system-webhook___show']['responses']['200']['content']['application/json'];
+export type AdminSystemWebhookUpdateRequest = operations['admin___system-webhook___update']['requestBody']['content']['application/json'];
+export type AdminSystemWebhookUpdateResponse = operations['admin___system-webhook___update']['responses']['200']['content']['application/json'];
+export type AdminSystemWebhookTestRequest = operations['admin___system-webhook___test']['requestBody']['content']['application/json'];
 export type AnnouncementsRequest = operations['announcements']['requestBody']['content']['application/json'];
 export type AnnouncementsResponse = operations['announcements']['responses']['200']['content']['application/json'];
 export type AnnouncementsShowRequest = operations['announcements___show']['requestBody']['content']['application/json'];
@@ -361,6 +383,7 @@ export type IWebhooksShowRequest = operations['i___webhooks___show']['requestBod
 export type IWebhooksShowResponse = operations['i___webhooks___show']['responses']['200']['content']['application/json'];
 export type IWebhooksUpdateRequest = operations['i___webhooks___update']['requestBody']['content']['application/json'];
 export type IWebhooksDeleteRequest = operations['i___webhooks___delete']['requestBody']['content']['application/json'];
+export type IWebhooksTestRequest = operations['i___webhooks___test']['requestBody']['content']['application/json'];
 export type InviteCreateResponse = operations['invite___create']['responses']['200']['content']['application/json'];
 export type InviteDeleteRequest = operations['invite___delete']['requestBody']['content']['application/json'];
 export type InviteListRequest = operations['invite___list']['requestBody']['content']['application/json'];
@@ -448,6 +471,7 @@ export type PagesUpdateRequest = operations['pages___update']['requestBody']['co
 export type FlashCreateRequest = operations['flash___create']['requestBody']['content']['application/json'];
 export type FlashCreateResponse = operations['flash___create']['responses']['200']['content']['application/json'];
 export type FlashDeleteRequest = operations['flash___delete']['requestBody']['content']['application/json'];
+export type FlashFeaturedRequest = operations['flash___featured']['requestBody']['content']['application/json'];
 export type FlashFeaturedResponse = operations['flash___featured']['responses']['200']['content']['application/json'];
 export type FlashLikeRequest = operations['flash___like']['requestBody']['content']['application/json'];
 export type FlashShowRequest = operations['flash___show']['requestBody']['content']['application/json'];
diff --git a/packages/misskey-js/src/autogen/models.ts b/packages/misskey-js/src/autogen/models.ts
index a6e5fbe689..04574849d4 100644
--- a/packages/misskey-js/src/autogen/models.ts
+++ b/packages/misskey-js/src/autogen/models.ts
@@ -51,3 +51,5 @@ export type ReversiGameDetailed = components['schemas']['ReversiGameDetailed'];
 export type MetaLite = components['schemas']['MetaLite'];
 export type MetaDetailedOnly = components['schemas']['MetaDetailedOnly'];
 export type MetaDetailed = components['schemas']['MetaDetailed'];
+export type SystemWebhook = components['schemas']['SystemWebhook'];
+export type AbuseReportNotificationRecipient = components['schemas']['AbuseReportNotificationRecipient'];
diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts
index 2c80676f3e..a5333d4f93 100644
--- a/packages/misskey-js/src/autogen/types.ts
+++ b/packages/misskey-js/src/autogen/types.ts
@@ -30,6 +30,56 @@ export type paths = {
      */
     post: operations['admin___abuse-user-reports'];
   };
+  '/admin/abuse-report/notification-recipient/list': {
+    /**
+     * admin/abuse-report/notification-recipient/list
+     * @description No description provided.
+     *
+     * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
+     * **Credential required**: *Yes* / **Permission**: *read:admin:abuse-report:notification-recipient*
+     */
+    post: operations['admin___abuse-report___notification-recipient___list'];
+  };
+  '/admin/abuse-report/notification-recipient/show': {
+    /**
+     * admin/abuse-report/notification-recipient/show
+     * @description No description provided.
+     *
+     * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
+     * **Credential required**: *Yes* / **Permission**: *read:admin:abuse-report:notification-recipient*
+     */
+    post: operations['admin___abuse-report___notification-recipient___show'];
+  };
+  '/admin/abuse-report/notification-recipient/create': {
+    /**
+     * admin/abuse-report/notification-recipient/create
+     * @description No description provided.
+     *
+     * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
+     * **Credential required**: *Yes* / **Permission**: *write:admin:abuse-report:notification-recipient*
+     */
+    post: operations['admin___abuse-report___notification-recipient___create'];
+  };
+  '/admin/abuse-report/notification-recipient/update': {
+    /**
+     * admin/abuse-report/notification-recipient/update
+     * @description No description provided.
+     *
+     * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
+     * **Credential required**: *Yes* / **Permission**: *write:admin:abuse-report:notification-recipient*
+     */
+    post: operations['admin___abuse-report___notification-recipient___update'];
+  };
+  '/admin/abuse-report/notification-recipient/delete': {
+    /**
+     * admin/abuse-report/notification-recipient/delete
+     * @description No description provided.
+     *
+     * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
+     * **Credential required**: *Yes* / **Permission**: *write:admin:abuse-report:notification-recipient*
+     */
+    post: operations['admin___abuse-report___notification-recipient___delete'];
+  };
   '/admin/accounts/create': {
     /**
      * admin/accounts/create
@@ -526,6 +576,24 @@ export type paths = {
      */
     post: operations['admin___resolve-abuse-user-report'];
   };
+  '/admin/forward-abuse-user-report': {
+    /**
+     * admin/forward-abuse-user-report
+     * @description No description provided.
+     *
+     * **Credential required**: *Yes* / **Permission**: *write:admin:resolve-abuse-user-report*
+     */
+    post: operations['admin___forward-abuse-user-report'];
+  };
+  '/admin/update-abuse-user-report': {
+    /**
+     * admin/update-abuse-user-report
+     * @description No description provided.
+     *
+     * **Credential required**: *Yes* / **Permission**: *write:admin:resolve-abuse-user-report*
+     */
+    post: operations['admin___update-abuse-user-report'];
+  };
   '/admin/send-email': {
     /**
      * admin/send-email
@@ -697,6 +765,66 @@ export type paths = {
      */
     post: operations['admin___roles___users'];
   };
+  '/admin/system-webhook/create': {
+    /**
+     * admin/system-webhook/create
+     * @description No description provided.
+     *
+     * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
+     * **Credential required**: *Yes* / **Permission**: *write:admin:system-webhook*
+     */
+    post: operations['admin___system-webhook___create'];
+  };
+  '/admin/system-webhook/delete': {
+    /**
+     * admin/system-webhook/delete
+     * @description No description provided.
+     *
+     * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
+     * **Credential required**: *Yes* / **Permission**: *write:admin:system-webhook*
+     */
+    post: operations['admin___system-webhook___delete'];
+  };
+  '/admin/system-webhook/list': {
+    /**
+     * admin/system-webhook/list
+     * @description No description provided.
+     *
+     * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
+     * **Credential required**: *Yes* / **Permission**: *write:admin:system-webhook*
+     */
+    post: operations['admin___system-webhook___list'];
+  };
+  '/admin/system-webhook/show': {
+    /**
+     * admin/system-webhook/show
+     * @description No description provided.
+     *
+     * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
+     * **Credential required**: *Yes* / **Permission**: *write:admin:system-webhook*
+     */
+    post: operations['admin___system-webhook___show'];
+  };
+  '/admin/system-webhook/update': {
+    /**
+     * admin/system-webhook/update
+     * @description No description provided.
+     *
+     * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
+     * **Credential required**: *Yes* / **Permission**: *write:admin:system-webhook*
+     */
+    post: operations['admin___system-webhook___update'];
+  };
+  '/admin/system-webhook/test': {
+    /**
+     * admin/system-webhook/test
+     * @description No description provided.
+     *
+     * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
+     * **Credential required**: *Yes* / **Permission**: *read:admin:system-webhook*
+     */
+    post: operations['admin___system-webhook___test'];
+  };
   '/announcements': {
     /**
      * announcements
@@ -2336,6 +2464,16 @@ export type paths = {
      */
     post: operations['i___webhooks___delete'];
   };
+  '/i/webhooks/test': {
+    /**
+     * i/webhooks/test
+     * @description No description provided.
+     *
+     * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
+     * **Credential required**: *Yes* / **Permission**: *read:account*
+     */
+    post: operations['i___webhooks___test'];
+  };
   '/invite/create': {
     /**
      * invite/create
@@ -3598,6 +3736,9 @@ export type components = {
         }[];
       isBot?: boolean;
       isCat?: boolean;
+      requireSigninToViewContents?: boolean;
+      makeNotesFollowersOnlyBefore?: number | null;
+      makeNotesHiddenBefore?: number | null;
       instance?: {
         name: string | null;
         softwareName: string | null;
@@ -3662,15 +3803,13 @@ export type components = {
       followingVisibility: 'public' | 'followers' | 'private';
       /** @enum {string} */
       followersVisibility: 'public' | 'followers' | 'private';
-      /** @default false */
-      twoFactorEnabled: boolean;
-      /** @default false */
-      usePasswordLessLogin: boolean;
-      /** @default false */
-      securityKeys: boolean;
       roles: components['schemas']['RoleLite'][];
+      followedMessage?: string | null;
       memo: string | null;
       moderationNote?: string;
+      twoFactorEnabled?: boolean;
+      usePasswordLessLogin?: boolean;
+      securityKeys?: boolean;
       isFollowing?: boolean;
       isFollowed?: boolean;
       hasPendingFollowRequestFromYou?: boolean;
@@ -3688,6 +3827,7 @@ export type components = {
       avatarId: string | null;
       /** Format: id */
       bannerId: string | null;
+      followedMessage: string | null;
       isModerator: boolean | null;
       isAdmin: boolean | null;
       injectFeaturedNote: boolean;
@@ -3850,6 +3990,12 @@ export type components = {
         }[];
       loggedInDays: number;
       policies: components['schemas']['RolePolicies'];
+      /** @default false */
+      twoFactorEnabled: boolean;
+      /** @default false */
+      usePasswordLessLogin: boolean;
+      /** @default false */
+      securityKeys: boolean;
       email?: string | null;
       emailVerified?: boolean | null;
       securityKeysList?: {
@@ -3989,7 +4135,8 @@ export type components = {
         userId: string | null;
       }) | null;
       localOnly?: boolean;
-      reactionAcceptance: string | null;
+      /** @enum {string|null} */
+      reactionAcceptance: 'likeOnly' | 'likeOnlyForRemote' | 'nonSensitiveOnly' | 'nonSensitiveOnlyForLocalLikeOnlyForRemote' | null;
       reactionEmojis: {
         [key: string]: string;
       };
@@ -4126,7 +4273,7 @@ export type components = {
       user: components['schemas']['UserLite'];
       /** Format: id */
       userId: string;
-    } | {
+    } | ({
       /** Format: id */
       id: string;
       /** Format: date-time */
@@ -4136,7 +4283,8 @@ export type components = {
       user: components['schemas']['UserLite'];
       /** Format: id */
       userId: string;
-    } | {
+      message: string | null;
+    }) | {
       /** Format: id */
       id: string;
       /** Format: date-time */
@@ -4144,15 +4292,34 @@ export type components = {
       /** @enum {string} */
       type: 'roleAssigned';
       role: components['schemas']['Role'];
-    } | {
+    } | ({
       /** Format: id */
       id: string;
       /** Format: date-time */
       createdAt: string;
       /** @enum {string} */
       type: 'achievementEarned';
-      achievement: string;
-    } | {
+      /** @enum {string} */
+      achievement: 'notes1' | 'notes10' | 'notes100' | 'notes500' | 'notes1000' | 'notes5000' | 'notes10000' | 'notes20000' | 'notes30000' | 'notes40000' | 'notes50000' | 'notes60000' | 'notes70000' | 'notes80000' | 'notes90000' | 'notes100000' | 'login3' | 'login7' | 'login15' | 'login30' | 'login60' | 'login100' | 'login200' | 'login300' | 'login400' | 'login500' | 'login600' | 'login700' | 'login800' | 'login900' | 'login1000' | 'passedSinceAccountCreated1' | 'passedSinceAccountCreated2' | 'passedSinceAccountCreated3' | 'loggedInOnBirthday' | 'loggedInOnNewYearsDay' | 'noteClipped1' | 'noteFavorited1' | 'myNoteFavorited1' | 'profileFilled' | 'markedAsCat' | 'following1' | 'following10' | 'following50' | 'following100' | 'following300' | 'followers1' | 'followers10' | 'followers50' | 'followers100' | 'followers300' | 'followers500' | 'followers1000' | 'collectAchievements30' | 'viewAchievements3min' | 'iLoveMisskey' | 'foundTreasure' | 'client30min' | 'client60min' | 'noteDeletedWithin1min' | 'postedAtLateNight' | 'postedAt0min0sec' | 'selfQuote' | 'htl20npm' | 'viewInstanceChart' | 'outputHelloWorldOnScratchpad' | 'open3windows' | 'driveFolderCircularReference' | 'reactWithoutRead' | 'clickedClickHere' | 'justPlainLucky' | 'setNameToSyuilo' | 'cookieClicked' | 'brainDiver' | 'smashTestNotificationButton' | 'tutorialCompleted' | 'bubbleGameExplodingHead' | 'bubbleGameDoubleExplodingHead';
+    }) | ({
+      /** Format: id */
+      id: string;
+      /** Format: date-time */
+      createdAt: string;
+      /** @enum {string} */
+      type: 'exportCompleted';
+      /** @enum {string} */
+      exportedEntity: 'antenna' | 'blocking' | 'clip' | 'customEmoji' | 'favorite' | 'following' | 'muting' | 'note' | 'userList';
+      /** Format: id */
+      fileId: string;
+    }) | {
+      /** Format: id */
+      id: string;
+      /** Format: date-time */
+      createdAt: string;
+      /** @enum {string} */
+      type: 'login';
+    } | ({
       /** Format: id */
       id: string;
       /** Format: date-time */
@@ -4160,9 +4327,9 @@ export type components = {
       /** @enum {string} */
       type: 'app';
       body: string;
-      header: string;
-      icon: string;
-    } | {
+      header: string | null;
+      icon: string | null;
+    }) | {
       /** Format: id */
       id: string;
       /** Format: date-time */
@@ -4199,7 +4366,7 @@ export type components = {
       id: string;
       /** Format: date-time */
       createdAt: string;
-      /** @example lenna.jpg */
+      /** @example 192.jpg */
       name: string;
       /** @example image/jpeg */
       type: string;
@@ -4498,6 +4665,7 @@ export type components = {
       maintainerName: string | null;
       maintainerEmail: string | null;
       isSilenced: boolean;
+      isMediaSilenced: boolean;
       /** Format: url */
       iconUrl: string | null;
       /** Format: url */
@@ -4570,6 +4738,8 @@ export type components = {
       title: string;
       summary: string;
       script: string;
+      /** @enum {string} */
+      visibility: 'private' | 'public';
       likedCount: number | null;
       isLiked?: boolean;
     };
@@ -4686,6 +4856,7 @@ export type components = {
       canHideAds: boolean;
       driveCapacityMb: number;
       alwaysMarkNsfw: boolean;
+      canUpdateBioMedia: boolean;
       pinLimit: number;
       antennaLimit: number;
       wordMuteLimit: number;
@@ -4696,6 +4867,11 @@ export type components = {
       userEachUserListsLimit: number;
       rateLimitFactor: number;
       avatarDecorationLimit: number;
+      canImportAntennas: boolean;
+      canImportBlocking: boolean;
+      canImportFollowing: boolean;
+      canImportMuting: boolean;
+      canImportUserLists: boolean;
     };
     ReversiGameLite: {
       /** Format: id */
@@ -4799,6 +4975,7 @@ export type components = {
       recaptchaSiteKey: string | null;
       enableTurnstile: boolean;
       turnstileSiteKey: string | null;
+      enableTestcaptcha: boolean;
       swPublickey: string | null;
       /** @default /assets/ai.png */
       mascotImageUrl: string;
@@ -4837,6 +5014,12 @@ export type components = {
       serverRules: string[];
       themeColor: string | null;
       policies: components['schemas']['RolePolicies'];
+      /**
+       * @default local
+       * @enum {string}
+       */
+      noteSearchableScope: 'local' | 'global';
+      maxFileSize: number;
     };
     MetaDetailedOnly: {
       features?: {
@@ -4859,6 +5042,32 @@ export type components = {
       cacheRemoteSensitiveFiles: boolean;
     };
     MetaDetailed: components['schemas']['MetaLite'] & components['schemas']['MetaDetailedOnly'];
+    SystemWebhook: {
+      id: string;
+      isActive: boolean;
+      /** Format: date-time */
+      updatedAt: string;
+      /** Format: date-time */
+      latestSentAt: string | null;
+      latestStatus: number | null;
+      name: string;
+      on: ('abuseReport' | 'abuseReportResolved' | 'userCreated' | 'inactiveModeratorsWarning' | 'inactiveModeratorsInvitationOnlyChanged')[];
+      url: string;
+      secret: string;
+    };
+    AbuseReportNotificationRecipient: {
+      id: string;
+      isActive: boolean;
+      /** Format: date-time */
+      updatedAt: string;
+      name: string;
+      /** @enum {string} */
+      method: 'email' | 'webhook';
+      userId?: string;
+      user?: components['schemas']['UserLite'];
+      systemWebhookId?: string;
+      systemWebhook?: components['schemas']['SystemWebhook'];
+    };
   };
   responses: never;
   parameters: never;
@@ -4897,6 +5106,7 @@ export type operations = {
             recaptchaSiteKey: string | null;
             enableTurnstile: boolean;
             turnstileSiteKey: string | null;
+            enableTestcaptcha: boolean;
             swPublickey: string | null;
             /** @default /assets/ai.png */
             mascotImageUrl: string | null;
@@ -4911,11 +5121,13 @@ export type operations = {
             enableServiceWorker: boolean;
             translatorAvailable: boolean;
             silencedHosts?: string[];
+            mediaSilencedHosts: string[];
             pinnedUsers: string[];
             hiddenTags: string[];
             blockedHosts: string[];
             sensitiveWords: string[];
             prohibitedWords: string[];
+            prohibitedWordsForNameOfUser: string[];
             bannedEmailDomains?: string[];
             preservedUsernames: string[];
             hcaptchaSecretKey: string | null;
@@ -4956,6 +5168,7 @@ export type operations = {
             truemailAuthKey: string | null;
             enableChartsForRemoteUser: boolean;
             enableChartsForFederatedInstances: boolean;
+            enableStatsForFederatedInstances: boolean;
             enableServerMachineStats: boolean;
             enableIdenticonGeneration: boolean;
             manifestJsonOverride: string;
@@ -4966,6 +5179,7 @@ export type operations = {
             perRemoteUserUserTimelineCacheMax: number;
             perUserHomeTimelineCacheMax: number;
             perUserListTimelineCacheMax: number;
+            enableReactionsBuffering: boolean;
             notesPerOneAd: number;
             backgroundImageUrl: string | null;
             deeplAuthKey: string | null;
@@ -4998,6 +5212,8 @@ export type operations = {
             urlPreviewRequireContentLength: boolean;
             urlPreviewUserAgent: string | null;
             urlPreviewSummaryProxyUrl: string | null;
+            federation: string;
+            federationHosts: string[];
           };
         };
       };
@@ -5061,8 +5277,6 @@ export type operations = {
            * @enum {string}
            */
           targetUserOrigin?: 'combined' | 'local' | 'remote';
-          /** @default false */
-          forwarded?: boolean;
         };
       };
     };
@@ -5089,7 +5303,11 @@ export type operations = {
               assigneeId: string | null;
               reporter: components['schemas']['UserDetailedNotMe'];
               targetUser: components['schemas']['UserDetailedNotMe'];
-              assignee?: components['schemas']['UserDetailedNotMe'] | null;
+              assignee: components['schemas']['UserDetailedNotMe'] | null;
+              forwarded: boolean;
+              /** @enum {string|null} */
+              resolvedAs: 'accept' | 'reject' | null;
+              moderationNote: string;
             })[];
         };
       };
@@ -5125,6 +5343,292 @@ export type operations = {
       };
     };
   };
+  /**
+   * admin/abuse-report/notification-recipient/list
+   * @description No description provided.
+   *
+   * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
+   * **Credential required**: *Yes* / **Permission**: *read:admin:abuse-report:notification-recipient*
+   */
+  'admin___abuse-report___notification-recipient___list': {
+    requestBody: {
+      content: {
+        'application/json': {
+          method?: ('email' | 'webhook')[];
+        };
+      };
+    };
+    responses: {
+      /** @description OK (with results) */
+      200: {
+        content: {
+          'application/json': components['schemas']['AbuseReportNotificationRecipient'][];
+        };
+      };
+      /** @description Client error */
+      400: {
+        content: {
+          'application/json': components['schemas']['Error'];
+        };
+      };
+      /** @description Authentication error */
+      401: {
+        content: {
+          'application/json': components['schemas']['Error'];
+        };
+      };
+      /** @description Forbidden error */
+      403: {
+        content: {
+          'application/json': components['schemas']['Error'];
+        };
+      };
+      /** @description I'm Ai */
+      418: {
+        content: {
+          'application/json': components['schemas']['Error'];
+        };
+      };
+      /** @description Internal server error */
+      500: {
+        content: {
+          'application/json': components['schemas']['Error'];
+        };
+      };
+    };
+  };
+  /**
+   * admin/abuse-report/notification-recipient/show
+   * @description No description provided.
+   *
+   * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
+   * **Credential required**: *Yes* / **Permission**: *read:admin:abuse-report:notification-recipient*
+   */
+  'admin___abuse-report___notification-recipient___show': {
+    requestBody: {
+      content: {
+        'application/json': {
+          /** Format: misskey:id */
+          id: string;
+        };
+      };
+    };
+    responses: {
+      /** @description OK (with results) */
+      200: {
+        content: {
+          'application/json': components['schemas']['AbuseReportNotificationRecipient'];
+        };
+      };
+      /** @description Client error */
+      400: {
+        content: {
+          'application/json': components['schemas']['Error'];
+        };
+      };
+      /** @description Authentication error */
+      401: {
+        content: {
+          'application/json': components['schemas']['Error'];
+        };
+      };
+      /** @description Forbidden error */
+      403: {
+        content: {
+          'application/json': components['schemas']['Error'];
+        };
+      };
+      /** @description I'm Ai */
+      418: {
+        content: {
+          'application/json': components['schemas']['Error'];
+        };
+      };
+      /** @description Internal server error */
+      500: {
+        content: {
+          'application/json': components['schemas']['Error'];
+        };
+      };
+    };
+  };
+  /**
+   * admin/abuse-report/notification-recipient/create
+   * @description No description provided.
+   *
+   * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
+   * **Credential required**: *Yes* / **Permission**: *write:admin:abuse-report:notification-recipient*
+   */
+  'admin___abuse-report___notification-recipient___create': {
+    requestBody: {
+      content: {
+        'application/json': {
+          isActive: boolean;
+          name: string;
+          /** @enum {string} */
+          method: 'email' | 'webhook';
+          /** Format: misskey:id */
+          userId?: string;
+          /** Format: misskey:id */
+          systemWebhookId?: string;
+        };
+      };
+    };
+    responses: {
+      /** @description OK (with results) */
+      200: {
+        content: {
+          'application/json': components['schemas']['AbuseReportNotificationRecipient'];
+        };
+      };
+      /** @description Client error */
+      400: {
+        content: {
+          'application/json': components['schemas']['Error'];
+        };
+      };
+      /** @description Authentication error */
+      401: {
+        content: {
+          'application/json': components['schemas']['Error'];
+        };
+      };
+      /** @description Forbidden error */
+      403: {
+        content: {
+          'application/json': components['schemas']['Error'];
+        };
+      };
+      /** @description I'm Ai */
+      418: {
+        content: {
+          'application/json': components['schemas']['Error'];
+        };
+      };
+      /** @description Internal server error */
+      500: {
+        content: {
+          'application/json': components['schemas']['Error'];
+        };
+      };
+    };
+  };
+  /**
+   * admin/abuse-report/notification-recipient/update
+   * @description No description provided.
+   *
+   * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
+   * **Credential required**: *Yes* / **Permission**: *write:admin:abuse-report:notification-recipient*
+   */
+  'admin___abuse-report___notification-recipient___update': {
+    requestBody: {
+      content: {
+        'application/json': {
+          /** Format: misskey:id */
+          id: string;
+          isActive: boolean;
+          name: string;
+          /** @enum {string} */
+          method: 'email' | 'webhook';
+          /** Format: misskey:id */
+          userId?: string;
+          /** Format: misskey:id */
+          systemWebhookId?: string;
+        };
+      };
+    };
+    responses: {
+      /** @description OK (with results) */
+      200: {
+        content: {
+          'application/json': components['schemas']['AbuseReportNotificationRecipient'];
+        };
+      };
+      /** @description Client error */
+      400: {
+        content: {
+          'application/json': components['schemas']['Error'];
+        };
+      };
+      /** @description Authentication error */
+      401: {
+        content: {
+          'application/json': components['schemas']['Error'];
+        };
+      };
+      /** @description Forbidden error */
+      403: {
+        content: {
+          'application/json': components['schemas']['Error'];
+        };
+      };
+      /** @description I'm Ai */
+      418: {
+        content: {
+          'application/json': components['schemas']['Error'];
+        };
+      };
+      /** @description Internal server error */
+      500: {
+        content: {
+          'application/json': components['schemas']['Error'];
+        };
+      };
+    };
+  };
+  /**
+   * admin/abuse-report/notification-recipient/delete
+   * @description No description provided.
+   *
+   * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
+   * **Credential required**: *Yes* / **Permission**: *write:admin:abuse-report:notification-recipient*
+   */
+  'admin___abuse-report___notification-recipient___delete': {
+    requestBody: {
+      content: {
+        'application/json': {
+          /** Format: misskey:id */
+          id: string;
+        };
+      };
+    };
+    responses: {
+      /** @description OK (without any results) */
+      204: {
+        content: never;
+      };
+      /** @description Client error */
+      400: {
+        content: {
+          'application/json': components['schemas']['Error'];
+        };
+      };
+      /** @description Authentication error */
+      401: {
+        content: {
+          'application/json': components['schemas']['Error'];
+        };
+      };
+      /** @description Forbidden error */
+      403: {
+        content: {
+          'application/json': components['schemas']['Error'];
+        };
+      };
+      /** @description I'm Ai */
+      418: {
+        content: {
+          'application/json': components['schemas']['Error'];
+        };
+      };
+      /** @description Internal server error */
+      500: {
+        content: {
+          'application/json': components['schemas']['Error'];
+        };
+      };
+    };
+  };
   /**
    * admin/accounts/create
    * @description No description provided.
@@ -5137,6 +5641,7 @@ export type operations = {
         'application/json': {
           username: string;
           password: string;
+          setupPassword?: string | null;
         };
       };
     };
@@ -5469,15 +5974,15 @@ export type operations = {
         'application/json': {
           /** Format: misskey:id */
           id: string;
-          memo: string;
-          url: string;
-          imageUrl: string;
-          place: string;
-          priority: string;
-          ratio: number;
-          expiresAt: number;
-          startsAt: number;
-          dayOfWeek: number;
+          memo?: string;
+          url?: string;
+          imageUrl?: string;
+          place?: string;
+          priority?: string;
+          ratio?: number;
+          expiresAt?: number;
+          startsAt?: number;
+          dayOfWeek?: number;
         };
       };
     };
@@ -5677,6 +6182,11 @@ export type operations = {
           untilId?: string;
           /** Format: misskey:id */
           userId?: string | null;
+          /**
+           * @default active
+           * @enum {string}
+           */
+          status?: 'all' | 'active' | 'archived';
         };
       };
     };
@@ -5814,9 +6324,22 @@ export type operations = {
       };
     };
     responses: {
-      /** @description OK (without any results) */
-      204: {
-        content: never;
+      /** @description OK (with results) */
+      200: {
+        content: {
+          'application/json': {
+            /** Format: id */
+            id: string;
+            /** Format: date-time */
+            createdAt: string;
+            /** Format: date-time */
+            updatedAt: string | null;
+            name: string;
+            description: string;
+            url: string;
+            roleIdsThatCanBeUsedThisDecoration: string[];
+          };
+        };
       };
       /** @description Client error */
       400: {
@@ -6387,7 +6910,7 @@ export type operations = {
              * @example 15eca7fba0480996e2245f5185bf39f2
              */
             md5: string;
-            /** @example lenna.jpg */
+            /** @example 192.jpg */
             name: string;
             /** @example image/jpeg */
             type: string;
@@ -7788,7 +8311,7 @@ export type operations = {
       /** @description OK (with results) */
       200: {
         content: {
-          'application/json': ((string | number)[])[];
+          'application/json': [string, number][];
         };
       };
       /** @description Client error */
@@ -7834,7 +8357,7 @@ export type operations = {
       /** @description OK (with results) */
       200: {
         content: {
-          'application/json': ((string | number)[])[];
+          'application/json': [string, number][];
         };
       };
       /** @description Client error */
@@ -8210,8 +8733,113 @@ export type operations = {
         'application/json': {
           /** Format: misskey:id */
           reportId: string;
-          /** @default false */
-          forward?: boolean;
+          /** @enum {string|null} */
+          resolvedAs?: 'accept' | 'reject' | null;
+        };
+      };
+    };
+    responses: {
+      /** @description OK (without any results) */
+      204: {
+        content: never;
+      };
+      /** @description Client error */
+      400: {
+        content: {
+          'application/json': components['schemas']['Error'];
+        };
+      };
+      /** @description Authentication error */
+      401: {
+        content: {
+          'application/json': components['schemas']['Error'];
+        };
+      };
+      /** @description Forbidden error */
+      403: {
+        content: {
+          'application/json': components['schemas']['Error'];
+        };
+      };
+      /** @description I'm Ai */
+      418: {
+        content: {
+          'application/json': components['schemas']['Error'];
+        };
+      };
+      /** @description Internal server error */
+      500: {
+        content: {
+          'application/json': components['schemas']['Error'];
+        };
+      };
+    };
+  };
+  /**
+   * admin/forward-abuse-user-report
+   * @description No description provided.
+   *
+   * **Credential required**: *Yes* / **Permission**: *write:admin:resolve-abuse-user-report*
+   */
+  'admin___forward-abuse-user-report': {
+    requestBody: {
+      content: {
+        'application/json': {
+          /** Format: misskey:id */
+          reportId: string;
+        };
+      };
+    };
+    responses: {
+      /** @description OK (without any results) */
+      204: {
+        content: never;
+      };
+      /** @description Client error */
+      400: {
+        content: {
+          'application/json': components['schemas']['Error'];
+        };
+      };
+      /** @description Authentication error */
+      401: {
+        content: {
+          'application/json': components['schemas']['Error'];
+        };
+      };
+      /** @description Forbidden error */
+      403: {
+        content: {
+          'application/json': components['schemas']['Error'];
+        };
+      };
+      /** @description I'm Ai */
+      418: {
+        content: {
+          'application/json': components['schemas']['Error'];
+        };
+      };
+      /** @description Internal server error */
+      500: {
+        content: {
+          'application/json': components['schemas']['Error'];
+        };
+      };
+    };
+  };
+  /**
+   * admin/update-abuse-user-report
+   * @description No description provided.
+   *
+   * **Credential required**: *Yes* / **Permission**: *write:admin:resolve-abuse-user-report*
+   */
+  'admin___update-abuse-user-report': {
+    requestBody: {
+      content: {
+        'application/json': {
+          /** Format: misskey:id */
+          reportId: string;
+          moderationNote?: string;
         };
       };
     };
@@ -8468,6 +9096,7 @@ export type operations = {
           'application/json': {
             email: string | null;
             emailVerified: boolean;
+            followedMessage: string | null;
             autoAcceptFollowed: boolean;
             noCrawle: boolean;
             preventAiLearning: boolean;
@@ -8850,6 +9479,7 @@ export type operations = {
           blockedHosts?: string[] | null;
           sensitiveWords?: string[] | null;
           prohibitedWords?: string[] | null;
+          prohibitedWordsForNameOfUser?: string[] | null;
           themeColor?: string | null;
           mascotImageUrl?: string | null;
           bannerUrl?: string | null;
@@ -8882,6 +9512,7 @@ export type operations = {
           enableTurnstile?: boolean;
           turnstileSiteKey?: string | null;
           turnstileSecretKey?: string | null;
+          enableTestcaptcha?: boolean;
           /** @enum {string} */
           sensitiveMediaDetection?: 'none' | 'all' | 'local' | 'remote';
           /** @enum {string} */
@@ -8933,6 +9564,7 @@ export type operations = {
           truemailAuthKey?: string | null;
           enableChartsForRemoteUser?: boolean;
           enableChartsForFederatedInstances?: boolean;
+          enableStatsForFederatedInstances?: boolean;
           enableServerMachineStats?: boolean;
           enableIdenticonGeneration?: boolean;
           serverRules?: string[];
@@ -8945,8 +9577,10 @@ export type operations = {
           perRemoteUserUserTimelineCacheMax?: number;
           perUserHomeTimelineCacheMax?: number;
           perUserListTimelineCacheMax?: number;
+          enableReactionsBuffering?: boolean;
           notesPerOneAd?: number;
           silencedHosts?: string[] | null;
+          mediaSilencedHosts?: string[] | null;
           /** @description [Deprecated] Use "urlPreviewSummaryProxyUrl" instead. */
           summalyProxy?: string | null;
           urlPreviewEnabled?: boolean;
@@ -8955,6 +9589,9 @@ export type operations = {
           urlPreviewRequireContentLength?: boolean;
           urlPreviewUserAgent?: string | null;
           urlPreviewSummaryProxyUrl?: string | null;
+          /** @enum {string} */
+          federation?: 'all' | 'none' | 'specified';
+          federationHosts?: string[];
         };
       };
     };
@@ -9332,21 +9969,21 @@ export type operations = {
         'application/json': {
           /** Format: misskey:id */
           roleId: string;
-          name: string;
-          description: string;
-          color: string | null;
-          iconUrl: string | null;
+          name?: string;
+          description?: string;
+          color?: string | null;
+          iconUrl?: string | null;
           /** @enum {string} */
-          target: 'manual' | 'conditional';
-          condFormula: Record<string, never>;
-          isPublic: boolean;
-          isModerator: boolean;
-          isAdministrator: boolean;
+          target?: 'manual' | 'conditional';
+          condFormula?: Record<string, never>;
+          isPublic?: boolean;
+          isModerator?: boolean;
+          isAdministrator?: boolean;
           isExplorable?: boolean;
-          asBadge: boolean;
-          canEditMembersByModerator: boolean;
-          displayOrder: number;
-          policies: Record<string, never>;
+          asBadge?: boolean;
+          canEditMembersByModerator?: boolean;
+          displayOrder?: number;
+          policies?: Record<string, never>;
         };
       };
     };
@@ -9615,6 +10252,352 @@ export type operations = {
       };
     };
   };
+  /**
+   * admin/system-webhook/create
+   * @description No description provided.
+   *
+   * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
+   * **Credential required**: *Yes* / **Permission**: *write:admin:system-webhook*
+   */
+  'admin___system-webhook___create': {
+    requestBody: {
+      content: {
+        'application/json': {
+          isActive: boolean;
+          name: string;
+          on: ('abuseReport' | 'abuseReportResolved' | 'userCreated' | 'inactiveModeratorsWarning' | 'inactiveModeratorsInvitationOnlyChanged')[];
+          url: string;
+          secret: string;
+        };
+      };
+    };
+    responses: {
+      /** @description OK (with results) */
+      200: {
+        content: {
+          'application/json': components['schemas']['SystemWebhook'];
+        };
+      };
+      /** @description Client error */
+      400: {
+        content: {
+          'application/json': components['schemas']['Error'];
+        };
+      };
+      /** @description Authentication error */
+      401: {
+        content: {
+          'application/json': components['schemas']['Error'];
+        };
+      };
+      /** @description Forbidden error */
+      403: {
+        content: {
+          'application/json': components['schemas']['Error'];
+        };
+      };
+      /** @description I'm Ai */
+      418: {
+        content: {
+          'application/json': components['schemas']['Error'];
+        };
+      };
+      /** @description Internal server error */
+      500: {
+        content: {
+          'application/json': components['schemas']['Error'];
+        };
+      };
+    };
+  };
+  /**
+   * admin/system-webhook/delete
+   * @description No description provided.
+   *
+   * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
+   * **Credential required**: *Yes* / **Permission**: *write:admin:system-webhook*
+   */
+  'admin___system-webhook___delete': {
+    requestBody: {
+      content: {
+        'application/json': {
+          /** Format: misskey:id */
+          id: string;
+        };
+      };
+    };
+    responses: {
+      /** @description OK (without any results) */
+      204: {
+        content: never;
+      };
+      /** @description Client error */
+      400: {
+        content: {
+          'application/json': components['schemas']['Error'];
+        };
+      };
+      /** @description Authentication error */
+      401: {
+        content: {
+          'application/json': components['schemas']['Error'];
+        };
+      };
+      /** @description Forbidden error */
+      403: {
+        content: {
+          'application/json': components['schemas']['Error'];
+        };
+      };
+      /** @description I'm Ai */
+      418: {
+        content: {
+          'application/json': components['schemas']['Error'];
+        };
+      };
+      /** @description Internal server error */
+      500: {
+        content: {
+          'application/json': components['schemas']['Error'];
+        };
+      };
+    };
+  };
+  /**
+   * admin/system-webhook/list
+   * @description No description provided.
+   *
+   * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
+   * **Credential required**: *Yes* / **Permission**: *write:admin:system-webhook*
+   */
+  'admin___system-webhook___list': {
+    requestBody: {
+      content: {
+        'application/json': {
+          isActive?: boolean;
+          on?: ('abuseReport' | 'abuseReportResolved' | 'userCreated' | 'inactiveModeratorsWarning' | 'inactiveModeratorsInvitationOnlyChanged')[];
+        };
+      };
+    };
+    responses: {
+      /** @description OK (with results) */
+      200: {
+        content: {
+          'application/json': components['schemas']['SystemWebhook'][];
+        };
+      };
+      /** @description Client error */
+      400: {
+        content: {
+          'application/json': components['schemas']['Error'];
+        };
+      };
+      /** @description Authentication error */
+      401: {
+        content: {
+          'application/json': components['schemas']['Error'];
+        };
+      };
+      /** @description Forbidden error */
+      403: {
+        content: {
+          'application/json': components['schemas']['Error'];
+        };
+      };
+      /** @description I'm Ai */
+      418: {
+        content: {
+          'application/json': components['schemas']['Error'];
+        };
+      };
+      /** @description Internal server error */
+      500: {
+        content: {
+          'application/json': components['schemas']['Error'];
+        };
+      };
+    };
+  };
+  /**
+   * admin/system-webhook/show
+   * @description No description provided.
+   *
+   * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
+   * **Credential required**: *Yes* / **Permission**: *write:admin:system-webhook*
+   */
+  'admin___system-webhook___show': {
+    requestBody: {
+      content: {
+        'application/json': {
+          /** Format: misskey:id */
+          id: string;
+        };
+      };
+    };
+    responses: {
+      /** @description OK (with results) */
+      200: {
+        content: {
+          'application/json': components['schemas']['SystemWebhook'];
+        };
+      };
+      /** @description Client error */
+      400: {
+        content: {
+          'application/json': components['schemas']['Error'];
+        };
+      };
+      /** @description Authentication error */
+      401: {
+        content: {
+          'application/json': components['schemas']['Error'];
+        };
+      };
+      /** @description Forbidden error */
+      403: {
+        content: {
+          'application/json': components['schemas']['Error'];
+        };
+      };
+      /** @description I'm Ai */
+      418: {
+        content: {
+          'application/json': components['schemas']['Error'];
+        };
+      };
+      /** @description Internal server error */
+      500: {
+        content: {
+          'application/json': components['schemas']['Error'];
+        };
+      };
+    };
+  };
+  /**
+   * admin/system-webhook/update
+   * @description No description provided.
+   *
+   * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
+   * **Credential required**: *Yes* / **Permission**: *write:admin:system-webhook*
+   */
+  'admin___system-webhook___update': {
+    requestBody: {
+      content: {
+        'application/json': {
+          /** Format: misskey:id */
+          id: string;
+          isActive: boolean;
+          name: string;
+          on: ('abuseReport' | 'abuseReportResolved' | 'userCreated' | 'inactiveModeratorsWarning' | 'inactiveModeratorsInvitationOnlyChanged')[];
+          url: string;
+          secret: string;
+        };
+      };
+    };
+    responses: {
+      /** @description OK (with results) */
+      200: {
+        content: {
+          'application/json': components['schemas']['SystemWebhook'];
+        };
+      };
+      /** @description Client error */
+      400: {
+        content: {
+          'application/json': components['schemas']['Error'];
+        };
+      };
+      /** @description Authentication error */
+      401: {
+        content: {
+          'application/json': components['schemas']['Error'];
+        };
+      };
+      /** @description Forbidden error */
+      403: {
+        content: {
+          'application/json': components['schemas']['Error'];
+        };
+      };
+      /** @description I'm Ai */
+      418: {
+        content: {
+          'application/json': components['schemas']['Error'];
+        };
+      };
+      /** @description Internal server error */
+      500: {
+        content: {
+          'application/json': components['schemas']['Error'];
+        };
+      };
+    };
+  };
+  /**
+   * admin/system-webhook/test
+   * @description No description provided.
+   *
+   * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
+   * **Credential required**: *Yes* / **Permission**: *read:admin:system-webhook*
+   */
+  'admin___system-webhook___test': {
+    requestBody: {
+      content: {
+        'application/json': {
+          /** Format: misskey:id */
+          webhookId: string;
+          /** @enum {string} */
+          type: 'abuseReport' | 'abuseReportResolved' | 'userCreated' | 'inactiveModeratorsWarning' | 'inactiveModeratorsInvitationOnlyChanged';
+          override?: {
+            url?: string;
+            secret?: string;
+          };
+        };
+      };
+    };
+    responses: {
+      /** @description OK (without any results) */
+      204: {
+        content: never;
+      };
+      /** @description Client error */
+      400: {
+        content: {
+          'application/json': components['schemas']['Error'];
+        };
+      };
+      /** @description Authentication error */
+      401: {
+        content: {
+          'application/json': components['schemas']['Error'];
+        };
+      };
+      /** @description Forbidden error */
+      403: {
+        content: {
+          'application/json': components['schemas']['Error'];
+        };
+      };
+      /** @description I'm Ai */
+      418: {
+        content: {
+          'application/json': components['schemas']['Error'];
+        };
+      };
+      /** @description To many requests */
+      429: {
+        content: {
+          'application/json': components['schemas']['Error'];
+        };
+      };
+      /** @description Internal server error */
+      500: {
+        content: {
+          'application/json': components['schemas']['Error'];
+        };
+      };
+    };
+  };
   /**
    * announcements
    * @description No description provided.
@@ -12707,7 +13690,7 @@ export type operations = {
         'application/json': {
           /** Format: misskey:id */
           clipId: string;
-          name: string;
+          name?: string;
           isPublic?: boolean;
           description?: string | null;
         };
@@ -13157,7 +14140,7 @@ export type operations = {
            * Format: binary
            * @description The file contents.
            */
-          file: string;
+          file: Blob;
         };
       };
     };
@@ -15554,9 +16537,9 @@ export type operations = {
         'application/json': {
           /** Format: misskey:id */
           postId: string;
-          title: string;
+          title?: string;
           description?: string | null;
-          fileIds: string[];
+          fileIds?: string[];
           /** @default false */
           isSensitive?: boolean;
         };
@@ -17726,8 +18709,8 @@ export type operations = {
           untilId?: string;
           /** @default true */
           markAsRead?: boolean;
-          includeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'receiveFollowRequest' | 'followRequestAccepted' | 'roleAssigned' | 'achievementEarned' | 'app' | 'test' | 'pollVote' | 'groupInvited')[];
-          excludeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'receiveFollowRequest' | 'followRequestAccepted' | 'roleAssigned' | 'achievementEarned' | 'app' | 'test' | 'pollVote' | 'groupInvited')[];
+          includeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'receiveFollowRequest' | 'followRequestAccepted' | 'roleAssigned' | 'achievementEarned' | 'exportCompleted' | 'login' | 'app' | 'test' | 'pollVote' | 'groupInvited')[];
+          excludeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'receiveFollowRequest' | 'followRequestAccepted' | 'roleAssigned' | 'achievementEarned' | 'exportCompleted' | 'login' | 'app' | 'test' | 'pollVote' | 'groupInvited')[];
         };
       };
     };
@@ -17794,8 +18777,8 @@ export type operations = {
           untilId?: string;
           /** @default true */
           markAsRead?: boolean;
-          includeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'receiveFollowRequest' | 'followRequestAccepted' | 'roleAssigned' | 'achievementEarned' | 'app' | 'test' | 'reaction:grouped' | 'renote:grouped' | 'pollVote' | 'groupInvited')[];
-          excludeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'receiveFollowRequest' | 'followRequestAccepted' | 'roleAssigned' | 'achievementEarned' | 'app' | 'test' | 'reaction:grouped' | 'renote:grouped' | 'pollVote' | 'groupInvited')[];
+          includeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'receiveFollowRequest' | 'followRequestAccepted' | 'roleAssigned' | 'achievementEarned' | 'exportCompleted' | 'login' | 'app' | 'test' | 'reaction:grouped' | 'renote:grouped' | 'pollVote' | 'groupInvited')[];
+          excludeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'receiveFollowRequest' | 'followRequestAccepted' | 'roleAssigned' | 'achievementEarned' | 'exportCompleted' | 'login' | 'app' | 'test' | 'reaction:grouped' | 'renote:grouped' | 'pollVote' | 'groupInvited')[];
         };
       };
     };
@@ -18848,6 +19831,7 @@ export type operations = {
         'application/json': {
           name?: string | null;
           description?: string | null;
+          followedMessage?: string | null;
           location?: string | null;
           birthday?: string | null;
           /** @enum {string|null} */
@@ -18876,6 +19860,9 @@ export type operations = {
           autoAcceptFollowed?: boolean;
           noCrawle?: boolean;
           preventAiLearning?: boolean;
+          requireSigninToViewContents?: boolean;
+          makeNotesFollowersOnlyBefore?: number | null;
+          makeNotesHiddenBefore?: number | null;
           isBot?: boolean;
           isCat?: boolean;
           injectFeaturedNote?: boolean;
@@ -19337,12 +20324,11 @@ export type operations = {
         'application/json': {
           /** Format: misskey:id */
           webhookId: string;
-          name: string;
-          url: string;
-          /** @default */
-          secret?: string;
-          on: ('mention' | 'unfollow' | 'follow' | 'followed' | 'note' | 'reply' | 'renote' | 'reaction')[];
-          active: boolean;
+          name?: string;
+          url?: string;
+          secret?: string | null;
+          on?: ('mention' | 'unfollow' | 'follow' | 'followed' | 'note' | 'reply' | 'renote' | 'reaction')[];
+          active?: boolean;
         };
       };
     };
@@ -19435,6 +20421,71 @@ export type operations = {
       };
     };
   };
+  /**
+   * i/webhooks/test
+   * @description No description provided.
+   *
+   * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
+   * **Credential required**: *Yes* / **Permission**: *read:account*
+   */
+  i___webhooks___test: {
+    requestBody: {
+      content: {
+        'application/json': {
+          /** Format: misskey:id */
+          webhookId: string;
+          /** @enum {string} */
+          type: 'mention' | 'unfollow' | 'follow' | 'followed' | 'note' | 'reply' | 'renote' | 'reaction';
+          override?: {
+            url?: string;
+            secret?: string;
+          };
+        };
+      };
+    };
+    responses: {
+      /** @description OK (without any results) */
+      204: {
+        content: never;
+      };
+      /** @description Client error */
+      400: {
+        content: {
+          'application/json': components['schemas']['Error'];
+        };
+      };
+      /** @description Authentication error */
+      401: {
+        content: {
+          'application/json': components['schemas']['Error'];
+        };
+      };
+      /** @description Forbidden error */
+      403: {
+        content: {
+          'application/json': components['schemas']['Error'];
+        };
+      };
+      /** @description I'm Ai */
+      418: {
+        content: {
+          'application/json': components['schemas']['Error'];
+        };
+      };
+      /** @description To many requests */
+      429: {
+        content: {
+          'application/json': components['schemas']['Error'];
+        };
+      };
+      /** @description Internal server error */
+      500: {
+        content: {
+          'application/json': components['schemas']['Error'];
+        };
+      };
+    };
+  };
   /**
    * invite/create
    * @description No description provided.
@@ -22711,16 +23762,16 @@ export type operations = {
         'application/json': {
           /** Format: misskey:id */
           pageId: string;
-          title: string;
-          name: string;
+          title?: string;
+          name?: string;
           summary?: string | null;
-          content: {
+          content?: {
               [key: string]: unknown;
             }[];
-          variables: {
+          variables?: {
               [key: string]: unknown;
             }[];
-          script: string;
+          script?: string;
           /** Format: misskey:id */
           eyeCatchingImageId?: string | null;
           /** @enum {string} */
@@ -22899,6 +23950,16 @@ export type operations = {
    * **Credential required**: *No*
    */
   flash___featured: {
+    requestBody: {
+      content: {
+        'application/json': {
+          /** @default 0 */
+          offset?: number;
+          /** @default 10 */
+          limit?: number;
+        };
+      };
+    };
     responses: {
       /** @description OK (with results) */
       200: {
diff --git a/packages/misskey-js/src/consts.ts b/packages/misskey-js/src/consts.ts
index fd6ef4d68d..c5911a70eb 100644
--- a/packages/misskey-js/src/consts.ts
+++ b/packages/misskey-js/src/consts.ts
@@ -1,3 +1,21 @@
+import type { operations } from './autogen/types.js';
+import type {
+	AbuseReportNotificationRecipient,
+	Ad,
+	Announcement,
+	EmojiDetailed,
+	Flash,
+	GalleryPost,
+	InviteCode,
+	MetaDetailed,
+	Note,
+	Page,
+	Role,
+	ReversiGameDetailed,
+	SystemWebhook,
+	UserLite,
+} from './autogen/models.js';
+
 export const notificationTypes = ['note', 'follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'app', 'roleAssigned', 'achievementEarned'] as const;
 
 export const noteVisibilities = ['public', 'home', 'followers', 'specified'] as const;
@@ -124,6 +142,8 @@ export const moderationLogTypes = [
 	'markSensitiveDriveFile',
 	'unmarkSensitiveDriveFile',
 	'resolveAbuseReport',
+	'forwardAbuseReport',
+	'updateAbuseReportNote',
 	'createInvitation',
 	'createAd',
 	'updateAd',
@@ -133,12 +153,42 @@ export const moderationLogTypes = [
 	'deleteAvatarDecoration',
 	'unsetUserAvatar',
 	'unsetUserBanner',
+	'createSystemWebhook',
+	'updateSystemWebhook',
+	'deleteSystemWebhook',
+	'createAbuseReportNotificationRecipient',
+	'updateAbuseReportNotificationRecipient',
+	'deleteAbuseReportNotificationRecipient',
+	'deleteAccount',
+	'deletePage',
+	'deleteFlash',
+	'deleteGalleryPost',
 ] as const;
 
+// See: packages/backend/src/core/ReversiService.ts@L410
+export const reversiUpdateKeys = [
+	'map',
+	'bw',
+	'isLlotheo',
+	'canPutEverywhere',
+	'loopedBoard',
+	'timeLimitForEachTurn',
+] as const satisfies (keyof ReversiGameDetailed)[];
+
+export type ReversiUpdateKey = typeof reversiUpdateKeys[number];
+
+type AvatarDecoration = UserLite['avatarDecorations'][number];
+
+type ReceivedAbuseReport = {
+	reportId: AbuseReportNotificationRecipient['id'];
+	report: operations['admin___abuse-user-reports']['responses'][200]['content']['application/json'];
+	forwarded: boolean;
+};
+
 export type ModerationLogPayloads = {
 	updateServerSettings: {
-		before: any | null;
-		after: any | null;
+		before: MetaDetailed | null;
+		after: MetaDetailed | null;
 	};
 	suspend: {
 		userId: string;
@@ -159,16 +209,16 @@ export type ModerationLogPayloads = {
 	};
 	addCustomEmoji: {
 		emojiId: string;
-		emoji: any;
+		emoji: EmojiDetailed;
 	};
 	updateCustomEmoji: {
 		emojiId: string;
-		before: any;
-		after: any;
+		before: EmojiDetailed;
+		after: EmojiDetailed;
 	};
 	deleteCustomEmoji: {
 		emojiId: string;
-		emoji: any;
+		emoji: EmojiDetailed;
 	};
 	assignRole: {
 		userId: string;
@@ -187,16 +237,16 @@ export type ModerationLogPayloads = {
 	};
 	createRole: {
 		roleId: string;
-		role: any;
+		role: Role;
 	};
 	updateRole: {
 		roleId: string;
-		before: any;
-		after: any;
+		before: Role;
+		after: Role;
 	};
 	deleteRole: {
 		roleId: string;
-		role: any;
+		role: Role;
 	};
 	clearQueue: Record<string, never>;
 	promoteQueue: Record<string, never>;
@@ -211,39 +261,39 @@ export type ModerationLogPayloads = {
 		noteUserId: string;
 		noteUserUsername: string;
 		noteUserHost: string | null;
-		note: any;
+		note: Note;
 	};
 	createGlobalAnnouncement: {
 		announcementId: string;
-		announcement: any;
+		announcement: Announcement;
 	};
 	createUserAnnouncement: {
 		announcementId: string;
-		announcement: any;
+		announcement: Announcement;
 		userId: string;
 		userUsername: string;
 		userHost: string | null;
 	};
 	updateGlobalAnnouncement: {
 		announcementId: string;
-		before: any;
-		after: any;
+		before: Announcement;
+		after: Announcement;
 	};
 	updateUserAnnouncement: {
 		announcementId: string;
-		before: any;
-		after: any;
+		before: Announcement;
+		after: Announcement;
 		userId: string;
 		userUsername: string;
 		userHost: string | null;
 	};
 	deleteGlobalAnnouncement: {
 		announcementId: string;
-		announcement: any;
+		announcement: Announcement;
 	};
 	deleteUserAnnouncement: {
 		announcementId: string;
-		announcement: any;
+		announcement: Announcement;
 		userId: string;
 		userUsername: string;
 		userHost: string | null;
@@ -281,37 +331,48 @@ export type ModerationLogPayloads = {
 	};
 	resolveAbuseReport: {
 		reportId: string;
-		report: any;
-		forwarded: boolean;
+		report: ReceivedAbuseReport;
+		forwarded?: boolean;
+		resolvedAs?: string | null;
+	};
+	forwardAbuseReport: {
+		reportId: string;
+		report: ReceivedAbuseReport;
+	};
+	updateAbuseReportNote: {
+		reportId: string;
+		report: ReceivedAbuseReport;
+		before: string;
+		after: string;
 	};
 	createInvitation: {
-		invitations: any[];
+		invitations: InviteCode[];
 	};
 	createAd: {
 		adId: string;
-		ad: any;
+		ad: Ad;
 	};
 	updateAd: {
 		adId: string;
-		before: any;
-		after: any;
+		before: Ad;
+		after: Ad;
 	};
 	deleteAd: {
 		adId: string;
-		ad: any;
+		ad: Ad;
 	};
 	createAvatarDecoration: {
 		avatarDecorationId: string;
-		avatarDecoration: any;
+		avatarDecoration: AvatarDecoration;
 	};
 	updateAvatarDecoration: {
 		avatarDecorationId: string;
-		before: any;
-		after: any;
+		before: AvatarDecoration;
+		after: AvatarDecoration;
 	};
 	deleteAvatarDecoration: {
 		avatarDecorationId: string;
-		avatarDecoration: any;
+		avatarDecoration: AvatarDecoration;
 	};
 	unsetUserAvatar: {
 		userId: string;
@@ -325,4 +386,53 @@ export type ModerationLogPayloads = {
 		userHost: string | null;
 		fileId: string;
 	};
+	createSystemWebhook: {
+		systemWebhookId: string;
+		webhook: SystemWebhook;
+	};
+	updateSystemWebhook: {
+		systemWebhookId: string;
+		before: SystemWebhook;
+		after: SystemWebhook;
+	};
+	deleteSystemWebhook: {
+		systemWebhookId: string;
+		webhook: SystemWebhook;
+	};
+	createAbuseReportNotificationRecipient: {
+		recipientId: string;
+		recipient: AbuseReportNotificationRecipient;
+	};
+	updateAbuseReportNotificationRecipient: {
+		recipientId: string;
+		before: AbuseReportNotificationRecipient;
+		after: AbuseReportNotificationRecipient;
+	};
+	deleteAbuseReportNotificationRecipient: {
+		recipientId: string;
+		recipient: AbuseReportNotificationRecipient;
+	};
+	deleteAccount: {
+		userId: string;
+		userUsername: string;
+		userHost: string | null;
+	};
+	deletePage: {
+		pageId: string;
+		pageUserId: string;
+		pageUserUsername: string;
+		page: Page;
+	};
+	deleteFlash: {
+		flashId: string;
+		flashUserId: string;
+		flashUserUsername: string;
+		flash: Flash;
+	};
+	deleteGalleryPost: {
+		postId: string;
+		postUserId: string;
+		postUserUsername: string;
+		post: GalleryPost;
+	};
 };
diff --git a/packages/misskey-js/src/entities.ts b/packages/misskey-js/src/entities.ts
index 35503d6d6f..dd88791ed0 100644
--- a/packages/misskey-js/src/entities.ts
+++ b/packages/misskey-js/src/entities.ts
@@ -1,5 +1,16 @@
 import { ModerationLogPayloads } from './consts.js';
-import { Announcement, EmojiDetailed, MeDetailed, Page, User, UserDetailedNotMe } from './autogen/models.js';
+import {
+	Announcement,
+	EmojiDetailed,
+	MeDetailed,
+	Note,
+	Page,
+	Role,
+	RolePolicies,
+	User,
+	UserDetailedNotMe,
+} from './autogen/models.js';
+import type { AuthenticationResponseJSON, PublicKeyCredentialRequestOptionsJSON } from '@simplewebauthn/types';
 
 export * from './autogen/entities.js';
 export * from './autogen/models.js';
@@ -7,9 +18,23 @@ export * from './autogen/models.js';
 export type ID = string;
 export type DateString = string;
 
+type NonNullableRecord<T> = {
+	[P in keyof T]-?: NonNullable<T[P]>;
+};
+type AllNullRecord<T> = {
+	[P in keyof T]: null;
+};
+
+export type PureRenote =
+	Omit<Note, 'renote' | 'renoteId' | 'reply' | 'replyId' | 'text' | 'cw' | 'files' | 'fileIds' | 'poll'>
+	& AllNullRecord<Pick<Note, 'reply' | 'replyId' | 'text' | 'cw' | 'poll'>>
+	& { files: []; fileIds: []; }
+	& NonNullableRecord<Pick<Note, 'renote' | 'renoteId'>>;
+
 export type PageEvent = {
 	pageId: Page['id'];
 	event: string;
+	// eslint-disable-next-line @typescript-eslint/no-explicit-any
 	var: any;
 	userId: User['id'];
 	user: User;
@@ -128,12 +153,48 @@ export type ModerationLog = {
 } | {
 	type: 'resolveAbuseReport';
 	info: ModerationLogPayloads['resolveAbuseReport'];
+} | {
+	type: 'forwardAbuseReport';
+	info: ModerationLogPayloads['forwardAbuseReport'];
+} | {
+	type: 'updateAbuseReportNote';
+	info: ModerationLogPayloads['updateAbuseReportNote'];
 } | {
 	type: 'unsetUserAvatar';
 	info: ModerationLogPayloads['unsetUserAvatar'];
 } | {
 	type: 'unsetUserBanner';
 	info: ModerationLogPayloads['unsetUserBanner'];
+} | {
+	type: 'createSystemWebhook';
+	info: ModerationLogPayloads['createSystemWebhook'];
+} | {
+	type: 'updateSystemWebhook';
+	info: ModerationLogPayloads['updateSystemWebhook'];
+} | {
+	type: 'deleteSystemWebhook';
+	info: ModerationLogPayloads['deleteSystemWebhook'];
+} | {
+	type: 'createAbuseReportNotificationRecipient';
+	info: ModerationLogPayloads['createAbuseReportNotificationRecipient'];
+} | {
+	type: 'updateAbuseReportNotificationRecipient';
+	info: ModerationLogPayloads['updateAbuseReportNotificationRecipient'];
+} | {
+	type: 'deleteAbuseReportNotificationRecipient';
+	info: ModerationLogPayloads['deleteAbuseReportNotificationRecipient'];
+} | {
+	type: 'deleteAccount';
+	info: ModerationLogPayloads['deleteAccount'];
+} | {
+	type: 'deletePage';
+	info: ModerationLogPayloads['deletePage'];
+} | {
+	type: 'deleteFlash';
+	info: ModerationLogPayloads['deleteFlash'];
+} | {
+	type: 'deleteGalleryPost';
+	info: ModerationLogPayloads['deleteGalleryPost'];
 });
 
 export type ServerStats = {
@@ -196,6 +257,7 @@ export type SignupRequest = {
 	'hcaptcha-response'?: string | null;
 	'g-recaptcha-response'?: string | null;
 	'turnstile-response'?: string | null;
+	'm-captcha-response'?: string | null;
 }
 
 export type SignupResponse = MeDetailed & {
@@ -211,13 +273,44 @@ export type SignupPendingResponse = {
 	i: string,
 };
 
-export type SigninRequest = {
+export type SigninFlowRequest = {
 	username: string;
-	password: string;
+	password?: string;
 	token?: string;
+	credential?: AuthenticationResponseJSON;
+	'hcaptcha-response'?: string | null;
+	'g-recaptcha-response'?: string | null;
+	'turnstile-response'?: string | null;
+	'm-captcha-response'?: string | null;
 };
 
-export type SigninResponse = {
-	id: User['id'],
-	i: string,
+export type SigninFlowResponse = {
+	finished: true;
+	id: User['id'];
+	i: string;
+} | {
+	finished: false;
+	next: 'captcha' | 'password' | 'totp';
+} | {
+	finished: false;
+	next: 'passkey';
+	authRequest: PublicKeyCredentialRequestOptionsJSON;
 };
+
+export type SigninWithPasskeyRequest = {
+	credential?: AuthenticationResponseJSON;
+	context?: string;
+};
+
+export type SigninWithPasskeyInitResponse = {
+	option: PublicKeyCredentialRequestOptionsJSON;
+	context: string;
+};
+
+export type SigninWithPasskeyResponse = {
+	signinResponse: SigninFlowResponse & { finished: true };
+};
+
+type Values<T extends Record<PropertyKey, unknown>> = T[keyof T];
+
+export type PartialRolePolicyOverride = Partial<{[k in keyof RolePolicies]: Omit<Values<Role['policies']>, 'value'> & { value: RolePolicies[k] }}>;
diff --git a/packages/misskey-js/src/index.ts b/packages/misskey-js/src/index.ts
index 28007a8ade..e4c9364aa1 100644
--- a/packages/misskey-js/src/index.ts
+++ b/packages/misskey-js/src/index.ts
@@ -1,15 +1,6 @@
-import { type Endpoints } from './api.types.js';
 import Stream, { Connection } from './streaming.js';
-import { type Channels } from './streaming.types.js';
-import { type Acct } from './acct.js';
 import * as consts from './consts.js';
 
-export type {
-	Endpoints,
-	Channels,
-	Acct,
-};
-
 export {
 	Stream,
 	Connection as ChannelConnection,
@@ -22,6 +13,7 @@ export const mutedNoteReasons = consts.mutedNoteReasons;
 export const followingVisibilities = consts.followingVisibilities;
 export const followersVisibilities = consts.followersVisibilities;
 export const moderationLogTypes = consts.moderationLogTypes;
+export const reversiUpdateKeys = consts.reversiUpdateKeys;
 
 // api extractor not supported yet
 //export * as api from './api.js';
@@ -29,4 +21,22 @@ export const moderationLogTypes = consts.moderationLogTypes;
 import * as api from './api.js';
 import * as entities from './entities.js';
 import * as acct from './acct.js';
-export { api, entities, acct };
+import * as note from './note.js';
+import { nyaize } from './nyaize.js';
+export { api, entities, acct, note, nyaize };
+
+//#region standalone types
+import type { Endpoints } from './api.types.js';
+import type { StreamEvents, IStream, IChannelConnection } from './streaming.js';
+import type { Channels } from './streaming.types.js';
+import type { Acct } from './acct.js';
+
+export type {
+	Endpoints,
+	Channels,
+	Acct,
+	StreamEvents,
+	IStream,
+	IChannelConnection,
+};
+//#endregion
diff --git a/packages/misskey-js/src/note.ts b/packages/misskey-js/src/note.ts
new file mode 100644
index 0000000000..5c8298c7e4
--- /dev/null
+++ b/packages/misskey-js/src/note.ts
@@ -0,0 +1,12 @@
+import type { Note, PureRenote } from './entities.js';
+
+export function isPureRenote(note: Note): note is PureRenote {
+	return (
+		note.renote != null &&
+		note.reply == null &&
+		note.text == null &&
+		note.cw == null &&
+		(note.fileIds == null || note.fileIds.length === 0) &&
+		note.poll == null
+	);
+}
diff --git a/packages/frontend/src/scripts/nyaize.ts b/packages/misskey-js/src/nyaize.ts
similarity index 82%
rename from packages/frontend/src/scripts/nyaize.ts
rename to packages/misskey-js/src/nyaize.ts
index abc8ada461..729fea8fc3 100644
--- a/packages/frontend/src/scripts/nyaize.ts
+++ b/packages/misskey-js/src/nyaize.ts
@@ -19,9 +19,9 @@ export function nyaize(text: string): string {
 		.replace(enRegex2, x => x === 'ING' ? 'YAN' : 'yan')
 		.replace(enRegex3, x => x === 'ONE' ? 'NYAN' : 'nyan')
 		// ko-KR
-		.replace(koRegex1, match => String.fromCharCode(
-			match.charCodeAt(0)! + '냐'.charCodeAt(0) - '나'.charCodeAt(0),
-		))
+		.replace(koRegex1, match => !isNaN(match.charCodeAt(0)) ? String.fromCharCode(
+			match.charCodeAt(0) + '냐'.charCodeAt(0) - '나'.charCodeAt(0),
+		) : match)
 		.replace(koRegex2, '다냥')
 		.replace(koRegex3, '냥');
 }
diff --git a/packages/misskey-js/src/streaming.ts b/packages/misskey-js/src/streaming.ts
index 0f26857782..6e34ec1508 100644
--- a/packages/misskey-js/src/streaming.ts
+++ b/packages/misskey-js/src/streaming.ts
@@ -15,17 +15,34 @@ export function urlQuery(obj: Record<string, string | number | boolean | undefin
 		.join('&');
 }
 
-type AnyOf<T extends Record<any, any>> = T[keyof T];
+type AnyOf<T extends Record<PropertyKey, unknown>> = T[keyof T];
 
-type StreamEvents = {
+export type StreamEvents = {
 	_connected_: void;
 	_disconnected_: void;
 } & BroadcastEvents;
 
+export interface IStream extends EventEmitter<StreamEvents> {
+	state: 'initializing' | 'reconnecting' | 'connected';
+
+	useChannel<C extends keyof Channels>(channel: C, params?: Channels[C]['params'], name?: string): IChannelConnection<Channels[C]>;
+	removeSharedConnection(connection: SharedConnection): void;
+	removeSharedConnectionPool(pool: Pool): void;
+	disconnectToChannel(connection: NonSharedConnection): void;
+	send(typeOrPayload: string): void;
+	send(typeOrPayload: string, payload: unknown): void;
+	send(typeOrPayload: Record<string, unknown> | unknown[]): void;
+	send(typeOrPayload: string | Record<string, unknown> | unknown[], payload?: unknown): void;
+	ping(): void;
+	heartbeat(): void;
+	close(): void;
+}
+
 /**
  * Misskey stream connection
  */
-export default class Stream extends EventEmitter<StreamEvents> {
+// eslint-disable-next-line import/no-default-export
+export default class Stream extends EventEmitter<StreamEvents> implements IStream {
 	private stream: _ReconnectingWebsocket.default;
 	public state: 'initializing' | 'reconnecting' | 'connected' = 'initializing';
 	private sharedConnectionPools: Pool[] = [];
@@ -34,7 +51,7 @@ export default class Stream extends EventEmitter<StreamEvents> {
 	private idCounter = 0;
 
 	constructor(origin: string, user: { token: string; } | null, options?: {
-		WebSocket?: any;
+		WebSocket?: _ReconnectingWebsocket.Options['WebSocket'];
 	}) {
 		super();
 
@@ -51,6 +68,7 @@ export default class Stream extends EventEmitter<StreamEvents> {
 		this.send = this.send.bind(this);
 		this.close = this.close.bind(this);
 
+		// eslint-disable-next-line no-param-reassign
 		options = options ?? { };
 
 		const query = urlQuery({
@@ -91,8 +109,8 @@ export default class Stream extends EventEmitter<StreamEvents> {
 			this.sharedConnectionPools.push(pool);
 		}
 
-		const connection = new SharedConnection(this, channel, pool, name);
-		this.sharedConnections.push(connection);
+		const connection = new SharedConnection<Channels[C]>(this, channel, pool, name);
+		this.sharedConnections.push(connection as unknown as SharedConnection);
 		return connection;
 	}
 
@@ -106,7 +124,7 @@ export default class Stream extends EventEmitter<StreamEvents> {
 
 	private connectToChannel<C extends keyof Channels>(channel: C, params: Channels[C]['params']): NonSharedConnection<Channels[C]> {
 		const connection = new NonSharedConnection(this, channel, this.genId(), params);
-		this.nonSharedConnections.push(connection);
+		this.nonSharedConnections.push(connection as unknown as NonSharedConnection);
 		return connection;
 	}
 
@@ -174,9 +192,9 @@ export default class Stream extends EventEmitter<StreamEvents> {
 	 * ! ストリーム上のやり取りはすべてJSONで行われます !
 	 */
 	public send(typeOrPayload: string): void
-	public send(typeOrPayload: string, payload: any): void
-	public send(typeOrPayload: Record<string, any> | any[]): void
-	public send(typeOrPayload: string | Record<string, any> | any[], payload?: any): void {
+	public send(typeOrPayload: string, payload: unknown): void
+	public send(typeOrPayload: Record<string, unknown> | unknown[]): void
+	public send(typeOrPayload: string | Record<string, unknown> | unknown[], payload?: unknown): void {
 		if (typeof typeOrPayload === 'string') {
 			this.stream.send(JSON.stringify({
 				type: typeOrPayload,
@@ -211,7 +229,7 @@ class Pool {
 	public id: string;
 	protected stream: Stream;
 	public users = 0;
-	private disposeTimerId: any;
+	private disposeTimerId: ReturnType<typeof setTimeout> | null = null;
 	private isConnected = false;
 
 	constructor(stream: Stream, channel: string, id: string) {
@@ -275,7 +293,18 @@ class Pool {
 	}
 }
 
-export abstract class Connection<Channel extends AnyOf<Channels> = any> extends EventEmitter<Channel['events']> {
+export interface IChannelConnection<Channel extends AnyOf<Channels> = AnyOf<Channels>> extends EventEmitter<Channel['events']> {
+	id: string;
+	name?: string;
+	inCount: number;
+	outCount: number;
+	channel: string;
+
+	send<T extends keyof Channel['receives']>(type: T, body: Channel['receives'][T]): void;
+	dispose(): void;
+}
+
+export abstract class Connection<Channel extends AnyOf<Channels> = AnyOf<Channels>> extends EventEmitter<Channel['events']> implements IChannelConnection<Channel> {
 	public channel: string;
 	protected stream: Stream;
 	public abstract id: string;
@@ -291,7 +320,9 @@ export abstract class Connection<Channel extends AnyOf<Channels> = any> extends
 
 		this.stream = stream;
 		this.channel = channel;
-		this.name = name;
+		if (name !== undefined) {
+			this.name = name;
+		}
 	}
 
 	public send<T extends keyof Channel['receives']>(type: T, body: Channel['receives'][T]): void {
@@ -307,7 +338,7 @@ export abstract class Connection<Channel extends AnyOf<Channels> = any> extends
 	public abstract dispose(): void;
 }
 
-class SharedConnection<Channel extends AnyOf<Channels> = any> extends Connection<Channel> {
+class SharedConnection<Channel extends AnyOf<Channels> = AnyOf<Channels>> extends Connection<Channel> {
 	private pool: Pool;
 
 	public get id(): string {
@@ -326,11 +357,11 @@ class SharedConnection<Channel extends AnyOf<Channels> = any> extends Connection
 	public dispose(): void {
 		this.pool.dec();
 		this.removeAllListeners();
-		this.stream.removeSharedConnection(this);
+		this.stream.removeSharedConnection(this as unknown as SharedConnection);
 	}
 }
 
-class NonSharedConnection<Channel extends AnyOf<Channels> = any> extends Connection<Channel> {
+class NonSharedConnection<Channel extends AnyOf<Channels> = AnyOf<Channels>> extends Connection<Channel> {
 	public id: string;
 	protected params: Channel['params'];
 
@@ -357,6 +388,6 @@ class NonSharedConnection<Channel extends AnyOf<Channels> = any> extends Connect
 	public dispose(): void {
 		this.removeAllListeners();
 		this.stream.send('disconnect', { id: this.id });
-		this.stream.disconnectToChannel(this);
+		this.stream.disconnectToChannel(this as unknown as NonSharedConnection);
 	}
 }
diff --git a/packages/misskey-js/src/streaming.types.ts b/packages/misskey-js/src/streaming.types.ts
index 9a86e03d69..26a50f9fa4 100644
--- a/packages/misskey-js/src/streaming.types.ts
+++ b/packages/misskey-js/src/streaming.types.ts
@@ -21,6 +21,14 @@ import {
 	ServerStatsLog,
 	ReversiGameDetailed,
 } from './entities.js';
+import {
+	ReversiUpdateKey,
+} from './consts.js';
+
+type ReversiUpdateSettings<K extends ReversiUpdateKey> = {
+	key: K;
+	value: ReversiGameDetailed[K];
+};
 
 export type Channels = {
 	main: {
@@ -51,6 +59,7 @@ export type Channels = {
 			registryUpdated: (payload: {
 				scope?: string[];
 				key: string;
+				// eslint-disable-next-line @typescript-eslint/no-explicit-any
 				value: any | null;
 			}) => void;
 			driveFileCreated: (payload: DriveFile) => void;
@@ -115,7 +124,7 @@ export type Channels = {
 	};
 	hashtag: {
 		params: {
-			q?: string;
+			q: string[][];
 		};
 		events: {
 			note: (payload: Note) => void;
@@ -208,8 +217,8 @@ export type Channels = {
 			ended: (payload: { winnerId: User['id'] | null; game: ReversiGameDetailed; }) => void;
 			canceled: (payload: { userId: User['id']; }) => void;
 			changeReadyStates: (payload: { user1: boolean; user2: boolean; }) => void;
-			updateSettings: (payload: { userId: User['id']; key: string; value: any; }) => void;
-			log: (payload: Record<string, any>) => void;
+			updateSettings: <K extends ReversiUpdateKey>(payload: { userId: User['id']; key: K; value: ReversiGameDetailed[K]; }) => void;
+			log: (payload: Record<string, unknown>) => void;
 		};
 		receives: {
 			putStone: {
@@ -218,16 +227,13 @@ export type Channels = {
 			};
 			ready: boolean;
 			cancel: null | Record<string, never>;
-			updateSettings: {
-				key: string;
-				value: any;
-			};
+			updateSettings: ReversiUpdateSettings<ReversiUpdateKey>;
 			claimTimeIsUp: null | Record<string, never>;
 		}
 	}
 };
 
-export type NoteUpdatedEvent = {
+export type NoteUpdatedEvent = { id: Note['id'] } & ({
 	type: 'reacted';
 	body: {
 		reaction: string;
@@ -251,7 +257,7 @@ export type NoteUpdatedEvent = {
 		choice: number;
 		userId: User['id'];
 	};
-};
+});
 
 export type BroadcastEvents = {
 	noteUpdated: (payload: NoteUpdatedEvent) => void;
diff --git a/packages/misskey-js/test-d/api.ts b/packages/misskey-js/test-d/api.ts
index 4b72ff4e9d..ca6d8dcb88 100644
--- a/packages/misskey-js/test-d/api.ts
+++ b/packages/misskey-js/test-d/api.ts
@@ -11,7 +11,7 @@ describe('API', () => {
 		expectType<Misskey.entities.MetaResponse>(res);
 	});
 
-	test('conditional respose type (meta)', async () => {
+	test('conditional response type (meta)', async () => {
 		const cli = new Misskey.api.APIClient({
 			origin: 'https://misskey.test',
 			credential: 'TOKEN'
@@ -30,7 +30,7 @@ describe('API', () => {
 		expectType<Misskey.entities.MetaResponse>(res4);
 	});
 
-	test('conditional respose type (users/show)', async () => {
+	test('conditional response type (users/show)', async () => {
 		const cli = new Misskey.api.APIClient({
 			origin: 'https://misskey.test',
 			credential: 'TOKEN'
diff --git a/packages/misskey-js/test/api.ts b/packages/misskey-js/test/api.ts
index fa31d23faa..1a7574de25 100644
--- a/packages/misskey-js/test/api.ts
+++ b/packages/misskey-js/test/api.ts
@@ -5,13 +5,19 @@ enableFetchMocks();
 
 function getFetchCall(call: any[]) {
 	const { body, method } = call[1];
-	if (body != null && typeof body != 'string') {
+	const contentType = call[1].headers['Content-Type'];
+	if (
+		body == null ||
+		(contentType === 'application/json' && typeof body !== 'string') ||
+		(contentType === 'multipart/form-data' && !(body instanceof FormData))
+	) {
 		throw new Error('invalid body');
 	}
 	return {
 		url: call[0],
 		method: method,
-		body: JSON.parse(body as any)
+		contentType: contentType,
+		body: body instanceof FormData ? Object.fromEntries(body.entries()) : JSON.parse(body),
 	};
 }
 
@@ -45,6 +51,7 @@ describe('API', () => {
 		expect(getFetchCall(fetchMock.mock.calls[0])).toEqual({
 			url: 'https://misskey.test/api/i',
 			method: 'POST',
+			contentType: 'application/json',
 			body: { i: 'TOKEN' }
 		});
 	});
@@ -78,10 +85,52 @@ describe('API', () => {
 		expect(getFetchCall(fetchMock.mock.calls[0])).toEqual({
 			url: 'https://misskey.test/api/notes/show',
 			method: 'POST',
+			contentType: 'application/json',
 			body: { i: 'TOKEN', noteId: 'aaaaa' }
 		});
 	});
 
+	test('multipart/form-data', async () => {
+		fetchMock.resetMocks();
+		fetchMock.mockResponse(async (req) => {
+			if (req.method == 'POST' && req.url == 'https://misskey.test/api/drive/files/create') {
+				if (req.headers.get('Content-Type')?.includes('multipart/form-data')) {
+					return JSON.stringify({ id: 'foo' });
+				} else {
+					return { status: 400 };
+				}
+			} else {
+				return { status: 404 };
+			}
+		});
+
+		const cli = new APIClient({
+			origin: 'https://misskey.test',
+			credential: 'TOKEN',
+		});
+
+		const testFile = new File([], 'foo.txt');
+
+		const res = await cli.request('drive/files/create', {
+			file: testFile,
+			name: null, // nullのパラメータは消える
+		});
+
+		expect(res).toEqual({
+			id: 'foo'
+		});
+
+		expect(getFetchCall(fetchMock.mock.calls[0])).toEqual({
+			url: 'https://misskey.test/api/drive/files/create',
+			method: 'POST',
+			contentType: 'multipart/form-data',
+			body: {
+				i: 'TOKEN',
+				file: testFile,
+			}
+		});
+	});
+
 	test('204 No Content で null が返る', async () => {
 		fetchMock.resetMocks();
 		fetchMock.mockResponse(async (req) => {
@@ -104,6 +153,7 @@ describe('API', () => {
 		expect(getFetchCall(fetchMock.mock.calls[0])).toEqual({
 			url: 'https://misskey.test/api/reset-password',
 			method: 'POST',
+			contentType: 'application/json',
 			body: { i: 'TOKEN', token: 'aaa', password: 'aaa' }
 		});
 	});
@@ -209,4 +259,42 @@ describe('API', () => {
 			expect(isAPIError(e)).toEqual(false);
 		}
 	});
+
+	test('admin/roles/create の型が合う', async() => {
+		fetchMock.resetMocks();
+		fetchMock.mockResponse(async () => {
+			return {
+				// 本来返すべき値は`Role`型だが、テストなのでお茶を濁す
+				status: 200,
+				body: '{}'
+			};
+		});
+
+		const cli = new APIClient({
+			origin: 'https://misskey.test',
+			credential: 'TOKEN',
+		});
+		await cli.request('admin/roles/create', {
+			name: 'aaa',
+			asBadge: false,
+			canEditMembersByModerator: false,
+			color: '#123456',
+			condFormula: {},
+			description: '',
+			displayOrder: 0,
+			iconUrl: '',
+			isAdministrator: false,
+			isExplorable: false,
+			isModerator: false,
+			isPublic: false,
+			policies: {
+				ltlAvailable: {
+					value: true,
+					priority: 0,
+					useDefault: false,
+				},
+			},
+			target: 'manual',
+		});
+	})
 });
diff --git a/packages/misskey-js/tsconfig.json b/packages/misskey-js/tsconfig.json
index 6e34e332e0..f7bbc47304 100644
--- a/packages/misskey-js/tsconfig.json
+++ b/packages/misskey-js/tsconfig.json
@@ -15,6 +15,7 @@
 		"experimentalDecorators": true,
 		"noImplicitReturns": true,
 		"esModuleInterop": true,
+		"exactOptionalPropertyTypes": true,
 		"typeRoots": [
 			"./node_modules/@types"
 		],
diff --git a/packages/misskey-reversi/.eslintignore b/packages/misskey-reversi/.eslintignore
deleted file mode 100644
index 52ea8b3362..0000000000
--- a/packages/misskey-reversi/.eslintignore
+++ /dev/null
@@ -1,8 +0,0 @@
-node_modules
-/built
-/coverage
-/.eslintrc.js
-/jest.config.ts
-/test
-/test-d
-build.js
diff --git a/packages/misskey-reversi/.eslintrc.cjs b/packages/misskey-reversi/.eslintrc.cjs
deleted file mode 100644
index db37a01098..0000000000
--- a/packages/misskey-reversi/.eslintrc.cjs
+++ /dev/null
@@ -1,10 +0,0 @@
-module.exports = {
-	root: true,
-	parserOptions: {
-		tsconfigRootDir: __dirname,
-		project: ['./tsconfig.json'],
-	},
-	extends: [
-		'../shared/.eslintrc.js',
-	],
-};
diff --git a/packages/misskey-reversi/build.js b/packages/misskey-reversi/build.js
index 0b79f4b915..a80b71646f 100644
--- a/packages/misskey-reversi/build.js
+++ b/packages/misskey-reversi/build.js
@@ -1,32 +1,32 @@
-import * as esbuild from "esbuild";
-import { build } from "esbuild";
-import { globSync } from "glob";
-import { execa } from "execa";
-import fs from "node:fs";
-import { fileURLToPath } from "node:url";
-import { dirname } from "node:path";
+import fs from 'node:fs';
+import { fileURLToPath } from 'node:url';
+import { dirname } from 'node:path';
+import * as esbuild from 'esbuild';
+import { build } from 'esbuild';
+import { globSync } from 'glob';
+import { execa } from 'execa';
 
 const _filename = fileURLToPath(import.meta.url);
 const _dirname = dirname(_filename);
 const _package = JSON.parse(fs.readFileSync(_dirname + '/package.json', 'utf-8'));
 
-const entryPoints = globSync("./src/**/**.{ts,tsx}");
+const entryPoints = globSync('./src/**/**.{ts,tsx}');
 
 /** @type {import('esbuild').BuildOptions} */
 const options = {
 	entryPoints,
 	minify: process.env.NODE_ENV === 'production',
-	outdir: "./built",
-	target: "es2022",
-	platform: "browser",
-	format: "esm",
+	outdir: './built',
+	target: 'es2022',
+	platform: 'browser',
+	format: 'esm',
 	sourcemap: 'linked',
 };
 
 // built配下をすべて削除する
 fs.rmSync('./built', { recursive: true, force: true });
 
-if (process.argv.map(arg => arg.toLowerCase()).includes("--watch")) {
+if (process.argv.map(arg => arg.toLowerCase()).includes('--watch')) {
 	await watchSrc();
 } else {
 	await buildSrc();
@@ -36,7 +36,7 @@ async function buildSrc() {
 	console.log(`[${_package.name}] start building...`);
 
 	await build(options)
-		.then(it => {
+		.then(() => {
 			console.log(`[${_package.name}] build succeeded.`);
 		})
 		.catch((err) => {
@@ -65,7 +65,7 @@ function buildDts() {
 		{
 			stdout: process.stdout,
 			stderr: process.stderr,
-		}
+		},
 	);
 }
 
@@ -86,7 +86,7 @@ async function watchSrc() {
 		},
 	}];
 
-	console.log(`[${_package.name}] start watching...`)
+	console.log(`[${_package.name}] start watching...`);
 
 	const context = await esbuild.context({ ...options, plugins });
 	await context.watch();
@@ -95,7 +95,6 @@ async function watchSrc() {
 		process.on('SIGHUP', resolve);
 		process.on('SIGINT', resolve);
 		process.on('SIGTERM', resolve);
-		process.on('SIGKILL', resolve);
 		process.on('uncaughtException', reject);
 		process.on('exit', resolve);
 	}).finally(async () => {
diff --git a/packages/misskey-reversi/eslint.config.js b/packages/misskey-reversi/eslint.config.js
new file mode 100644
index 0000000000..8453d9b8e7
--- /dev/null
+++ b/packages/misskey-reversi/eslint.config.js
@@ -0,0 +1,24 @@
+import tsParser from '@typescript-eslint/parser';
+import sharedConfig from '../shared/eslint.config.js';
+
+// eslint-disable-next-line import/no-default-export
+export default [
+	...sharedConfig,
+	{
+		ignores: [
+			'**/node_modules',
+			'built',
+		],
+	},
+	{
+		files: ['**/*.ts', '**/*.tsx'],
+		languageOptions: {
+			parserOptions: {
+				parser: tsParser,
+				project: ['./tsconfig.json'],
+				sourceType: 'module',
+				tsconfigRootDir: import.meta.dirname,
+			},
+		},
+	},
+];
diff --git a/packages/misskey-reversi/package.json b/packages/misskey-reversi/package.json
index 45a6120861..c6db6e6221 100644
--- a/packages/misskey-reversi/package.json
+++ b/packages/misskey-reversi/package.json
@@ -17,16 +17,14 @@
 	"scripts": {
 		"build": "node ./build.js",
 		"watch": "nodemon -w package.json -e json --exec \"node ./build.js --watch\"",
-		"eslint": "eslint . --ext .js,.jsx,.ts,.tsx",
+		"eslint": "eslint './**/*.{js,jsx,ts,tsx}'",
 		"typecheck": "tsc --noEmit",
 		"lint": "pnpm typecheck && pnpm eslint"
 	},
 	"devDependencies": {
-		"@misskey-dev/eslint-plugin": "1.0.0",
 		"@types/node": "20.11.5",
 		"@typescript-eslint/eslint-plugin": "7.1.0",
 		"@typescript-eslint/parser": "7.1.0",
-		"eslint": "8.57.0",
 		"execa": "8.0.1",
 		"nodemon": "3.0.2",
 		"typescript": "5.3.3",
diff --git a/packages/misskey-reversi/src/game.ts b/packages/misskey-reversi/src/game.ts
index 4afca9898c..35cc44feb4 100644
--- a/packages/misskey-reversi/src/game.ts
+++ b/packages/misskey-reversi/src/game.ts
@@ -53,9 +53,13 @@ export class Game {
 
 		//#region Options
 		this.opts = opts;
+
+		/* eslint-disable @typescript-eslint/no-unnecessary-condition */
 		if (this.opts.isLlotheo == null) this.opts.isLlotheo = false;
 		if (this.opts.canPutEverywhere == null) this.opts.canPutEverywhere = false;
 		if (this.opts.loopedBoard == null) this.opts.loopedBoard = false;
+		/* eslint-enable */
+
 		//#endregion
 
 		//#region Parse map data
@@ -123,12 +127,13 @@ export class Game {
 		// ターン計算
 		this.turn =
 			this.canPutSomewhere(!this.prevColor) ? !this.prevColor :
-			this.canPutSomewhere(this.prevColor!) ? this.prevColor :
+			this.canPutSomewhere(this.prevColor!) ? this.prevColor : //eslint-disable-line @typescript-eslint/no-non-null-assertion
 			null;
 	}
 
 	public undo() {
-		const undo = this.logs.pop()!;
+		const undo = this.logs.pop();
+		if (undo == null) return;
 		this.prevColor = undo.color;
 		this.prevPos = undo.pos;
 		this.board[undo.pos] = null;
@@ -183,7 +188,7 @@ export class Game {
 
 			const found: number[] = []; // 挟めるかもしれない相手の石を入れておく配列
 			let [x, y] = this.posToXy(initPos);
-			while (true) {
+			while (true) { // eslint-disable-line @typescript-eslint/no-unnecessary-condition
 				[x, y] = nextPos(x, y);
 
 				// 座標が指し示す位置がボード外に出たとき
diff --git a/packages/shared/.eslintrc.js b/packages/shared/.eslintrc.js
deleted file mode 100644
index 58247877ae..0000000000
--- a/packages/shared/.eslintrc.js
+++ /dev/null
@@ -1,7 +0,0 @@
-module.exports = {
-	root: true,
-	ignorePatterns: ['**/.eslintrc.cjs'],
-	extends: [
-		'plugin:@misskey-dev/recommended',
-	],
-};
diff --git a/packages/shared/eslint.config.js b/packages/shared/eslint.config.js
new file mode 100644
index 0000000000..0368d008c0
--- /dev/null
+++ b/packages/shared/eslint.config.js
@@ -0,0 +1,35 @@
+import globals from 'globals';
+import pluginMisskey from '@misskey-dev/eslint-plugin';
+
+export default [
+	...pluginMisskey.configs['recommended'],
+	{
+		files: ['**/*.cjs'],
+		languageOptions: {
+			sourceType: 'commonjs',
+			parserOptions: {
+				sourceType: 'commonjs',
+			},
+		},
+	},
+	{
+		files: ['**/*.js', '**/*.jsx'],
+		languageOptions: {
+			parserOptions: {
+				sourceType: 'module',
+			},
+		},
+	},
+	{
+		files: ['build.js'],
+		languageOptions: {
+			globals: globals.node,
+		},
+	},
+	{
+		files: ['**/*.js', '**/*.cjs'],
+		rules: {
+			'@typescript-eslint/no-var-requires': 'off',
+		},
+	},
+];
diff --git a/packages/shared/package.json b/packages/shared/package.json
new file mode 100644
index 0000000000..bedb411a91
--- /dev/null
+++ b/packages/shared/package.json
@@ -0,0 +1,3 @@
+{
+	"type": "module"
+}
diff --git a/packages/sw/.eslintrc.cjs b/packages/sw/.eslintrc.cjs
deleted file mode 100644
index b1fd6b5edc..0000000000
--- a/packages/sw/.eslintrc.cjs
+++ /dev/null
@@ -1,20 +0,0 @@
-module.exports = {
-	root: true,
-	env: {
-		node: false,
-	},
-	parserOptions: {
-		parser: '@typescript-eslint/parser',
-		tsconfigRootDir: __dirname,
-		project: ['./tsconfig.json'],
-	},
-	extends: ['../shared/.eslintrc.js'],
-	globals: {
-		require: false,
-		_DEV_: false,
-		_LANGS_: false,
-		_VERSION_: false,
-		_ENV_: false,
-		_PERF_PREFIX_: false,
-	},
-};
diff --git a/packages/sw/build.js b/packages/sw/build.js
index eb9a944f47..a9c2e428c0 100644
--- a/packages/sw/build.js
+++ b/packages/sw/build.js
@@ -8,10 +8,10 @@
 import { fileURLToPath } from 'node:url';
 import * as esbuild from 'esbuild';
 import locales from '../../locales/index.js';
-import meta from '../../package.json' assert { type: "json" };
+import meta from '../../package.json' with { type: 'json' };
 const watch = process.argv[2]?.includes('watch');
 
-const __dirname = fileURLToPath(new URL('.', import.meta.url))
+const __dirname = fileURLToPath(new URL('.', import.meta.url));
 
 console.log('Starting SW building...');
 
diff --git a/packages/sw/eslint.config.js b/packages/sw/eslint.config.js
new file mode 100644
index 0000000000..c62a2eadc6
--- /dev/null
+++ b/packages/sw/eslint.config.js
@@ -0,0 +1,32 @@
+import globals from 'globals';
+import tsParser from '@typescript-eslint/parser';
+import sharedConfig from '../shared/eslint.config.js';
+
+export default [
+	...sharedConfig,
+	{
+		ignores: ['build.js'],
+		languageOptions: {
+			globals: {
+				...Object.fromEntries(Object.entries(globals.node).map(([key]) => [key, 'off'])),
+				require: false,
+				_DEV_: false,
+				_LANGS_: false,
+				_VERSION_: false,
+				_ENV_: false,
+				_PERF_PREFIX_: false,
+			},
+		},
+	},
+	{
+		files: ['**/*.ts', '**/*.tsx'],
+		languageOptions: {
+			parserOptions: {
+				parser: tsParser,
+				project: ['./tsconfig.json'],
+				sourceType: 'module',
+				tsconfigRootDir: import.meta.dirname,
+			},
+		},
+	},
+];
diff --git a/packages/sw/package.json b/packages/sw/package.json
index cb59a70238..09e604ff4c 100644
--- a/packages/sw/package.json
+++ b/packages/sw/package.json
@@ -9,18 +9,16 @@
 		"lint": "pnpm typecheck && pnpm eslint"
 	},
 	"dependencies": {
-		"esbuild": "0.20.2",
+		"esbuild": "0.23.1",
 		"idb-keyval": "6.2.1",
 		"misskey-js": "workspace:*"
 	},
 	"devDependencies": {
-		"@misskey-dev/eslint-plugin": "1.0.0",
-		"@typescript-eslint/parser": "7.7.1",
+		"@typescript-eslint/parser": "7.17.0",
 		"@typescript/lib-webworker": "npm:@types/serviceworker@0.0.67",
-		"eslint": "8.57.0",
-		"eslint-plugin-import": "2.29.1",
-		"nodemon": "3.1.0",
-		"typescript": "5.4.5"
+		"eslint-plugin-import": "2.30.0",
+		"nodemon": "3.1.7",
+		"typescript": "5.6.2"
 	},
 	"type": "module"
 }
diff --git a/packages/sw/src/scripts/create-notification.ts b/packages/sw/src/scripts/create-notification.ts
index 7d28d8a694..364328d4b0 100644
--- a/packages/sw/src/scripts/create-notification.ts
+++ b/packages/sw/src/scripts/create-notification.ts
@@ -41,11 +41,10 @@ export async function createNotification<K extends keyof PushNotificationDataMap
 
 async function composeNotification(data: PushNotificationDataMap[keyof PushNotificationDataMap]): Promise<[string, NotificationOptions] | null> {
 	const i18n = await (swLang.i18n ?? swLang.fetchLocale());
-	const { t } = i18n;
 	switch (data.type) {
 		/*
 		case 'driveFileCreated': // TODO (Server Side)
-			return [t('_notification.fileUploaded'), {
+			return [i18n.ts._notification.fileUploaded, {
 				body: body.name,
 				icon: body.url,
 				data
@@ -58,52 +57,52 @@ async function composeNotification(data: PushNotificationDataMap[keyof PushNotif
 					const account = await getAccountFromId(data.userId);
 					if (!account) return null;
 					const userDetail = await cli.request('users/show', { userId: data.body.userId }, account.token);
-					return [t('_notification.youWereFollowed'), {
+					return [i18n.ts._notification.youWereFollowed, {
 						body: getUserName(data.body.user),
-						icon: data.body.user.avatarUrl,
+						icon: data.body.user.avatarUrl ?? undefined,
 						badge: iconUrl('user-plus'),
 						data,
 						actions: userDetail.isFollowing ? [] : [
 							{
 								action: 'follow',
-								title: t('_notification._actions.followBack'),
+								title: i18n.ts._notification._actions.followBack,
 							},
 						],
 					}];
 				}
 
 				case 'mention':
-					return [t('_notification.youGotMention', { name: getUserName(data.body.user) }), {
+					return [i18n.tsx._notification.youGotMention({ name: getUserName(data.body.user) }), {
 						body: data.body.note.text ?? '',
-						icon: data.body.user.avatarUrl,
+						icon: data.body.user.avatarUrl ?? undefined,
 						badge: iconUrl('at'),
 						data,
 						actions: [
 							{
 								action: 'reply',
-								title: t('_notification._actions.reply'),
+								title: i18n.ts._notification._actions.reply,
 							},
 						],
 					}];
 
 				case 'reply':
-					return [t('_notification.youGotReply', { name: getUserName(data.body.user) }), {
+					return [i18n.tsx._notification.youGotReply({ name: getUserName(data.body.user) }), {
 						body: data.body.note.text ?? '',
-						icon: data.body.user.avatarUrl,
+						icon: data.body.user.avatarUrl ?? undefined,
 						badge: iconUrl('arrow-back-up'),
 						data,
 						actions: [
 							{
 								action: 'reply',
-								title: t('_notification._actions.reply'),
+								title: i18n.ts._notification._actions.reply,
 							},
 						],
 					}];
 
 				case 'renote':
-					return [t('_notification.youRenoted', { name: getUserName(data.body.user) }), {
+					return [i18n.tsx._notification.youRenoted({ name: getUserName(data.body.user) }), {
 						body: data.body.note.text ?? '',
-						icon: data.body.user.avatarUrl,
+						icon: data.body.user.avatarUrl ?? undefined,
 						badge: iconUrl('repeat'),
 						data,
 						actions: [
@@ -115,29 +114,29 @@ async function composeNotification(data: PushNotificationDataMap[keyof PushNotif
 					}];
 
 				case 'quote':
-					return [t('_notification.youGotQuote', { name: getUserName(data.body.user) }), {
+					return [i18n.tsx._notification.youGotQuote({ name: getUserName(data.body.user) }), {
 						body: data.body.note.text ?? '',
-						icon: data.body.user.avatarUrl,
+						icon: data.body.user.avatarUrl ?? undefined,
 						badge: iconUrl('quote'),
 						data,
 						actions: [
 							{
 								action: 'reply',
-								title: t('_notification._actions.reply'),
+								title: i18n.ts._notification._actions.reply,
 							},
 							...((data.body.note.visibility === 'public' || data.body.note.visibility === 'home') ? [
 								{
 									action: 'renote',
-									title: t('_notification._actions.renote'),
+									title: i18n.ts._notification._actions.renote,
 								},
 							] : []),
 						],
 					}];
 
 				case 'note':
-					return [t('_notification.newNote') + ': ' + getUserName(data.body.user), {
+					return [i18n.ts._notification.newNote + ': ' + getUserName(data.body.user), {
 						body: data.body.note.text ?? '',
-						icon: data.body.user.avatarUrl,
+						icon: data.body.user.avatarUrl ?? undefined,
 						data,
 					}];
 
@@ -164,7 +163,7 @@ async function composeNotification(data: PushNotificationDataMap[keyof PushNotif
 					const tag = `reaction:${data.body.note.id}`;
 					return [`${reaction} ${getUserName(data.body.user)}`, {
 						body: data.body.note.text ?? '',
-						icon: data.body.user.avatarUrl,
+						icon: data.body.user.avatarUrl ?? undefined,
 						tag,
 						badge,
 						data,
@@ -178,41 +177,66 @@ async function composeNotification(data: PushNotificationDataMap[keyof PushNotif
 				}
 
 				case 'receiveFollowRequest':
-					return [t('_notification.youReceivedFollowRequest'), {
+					return [i18n.ts._notification.youReceivedFollowRequest, {
 						body: getUserName(data.body.user),
-						icon: data.body.user.avatarUrl,
+						icon: data.body.user.avatarUrl ?? undefined,
 						badge: iconUrl('user-plus'),
 						data,
 						actions: [
 							{
 								action: 'accept',
-								title: t('accept'),
+								title: i18n.ts.accept,
 							},
 							{
 								action: 'reject',
-								title: t('reject'),
+								title: i18n.ts.reject,
 							},
 						],
 					}];
 
 				case 'followRequestAccepted':
-					return [t('_notification.yourFollowRequestAccepted'), {
+					return [i18n.ts._notification.yourFollowRequestAccepted, {
 						body: getUserName(data.body.user),
-						icon: data.body.user.avatarUrl,
+						icon: data.body.user.avatarUrl ?? undefined,
 						badge: iconUrl('circle-check'),
 						data,
 					}];
 
 				case 'achievementEarned':
-					return [t('_notification.achievementEarned'), {
-						body: t(`_achievements._types._${data.body.achievement}.title`),
+					return [i18n.ts._notification.achievementEarned, {
+						body: i18n.ts._achievements._types[`_${data.body.achievement}`].title,
 						badge: iconUrl('medal'),
 						data,
 						tag: `achievement:${data.body.achievement}`,
 					}];
 
+				case 'login':
+					return [i18n.ts._notification.login, {
+						badge: iconUrl('login-2'),
+						data,
+					}];
+
+				case 'exportCompleted': {
+					const entityName = {
+						antenna: i18n.ts.antennas,
+						blocking: i18n.ts.blockedUsers,
+						clip: i18n.ts.clips,
+						customEmoji: i18n.ts.customEmojis,
+						favorite: i18n.ts.favorites,
+						following: i18n.ts.following,
+						muting: i18n.ts.mutedUsers,
+						note: i18n.ts.notes,
+						userList: i18n.ts.lists,
+					} as const satisfies Record<typeof data.body.exportedEntity, string>;
+
+					return [i18n.tsx._notification.exportOfXCompleted({ x: entityName[data.body.exportedEntity] }), {
+						badge: iconUrl('circle-check'),
+						data,
+					}];
+				}
+
 				case 'pollEnded':
-					return [t('_notification.pollEnded'), {
+					return [i18n.ts._notification.pollEnded, {
 						body: data.body.note.text ?? '',
 						badge: iconUrl('chart-arrows'),
 						data,
@@ -226,8 +250,8 @@ async function composeNotification(data: PushNotificationDataMap[keyof PushNotif
 					}];
 
 				case 'test':
-					return [t('_notification.testNotification'), {
-						body: t('_notification.notificationWillBeDisplayedLikeThis'),
+					return [i18n.ts._notification.testNotification, {
+						body: i18n.ts._notification.notificationWillBeDisplayedLikeThis,
 						badge: iconUrl('bell'),
 						data,
 					}];
@@ -236,9 +260,9 @@ async function composeNotification(data: PushNotificationDataMap[keyof PushNotif
 					return null;
 			}
 		case 'unreadAntennaNote':
-			return [t('_notification.unreadAntennaNote', { name: data.body.antenna.name }), {
+			return [i18n.tsx._notification.unreadAntennaNote({ name: data.body.antenna.name }), {
 				body: `${getUserName(data.body.note.user)}: ${data.body.note.text ?? ''}`,
-				icon: data.body.note.user.avatarUrl,
+				icon: data.body.note.user.avatarUrl ?? undefined,
 				badge: iconUrl('antenna'),
 				tag: `antenna:${data.body.antenna.id}`,
 				data,
@@ -252,7 +276,6 @@ async function composeNotification(data: PushNotificationDataMap[keyof PushNotif
 export async function createEmptyNotification(): Promise<void> {
 	return new Promise<void>(async res => {
 		const i18n = await (swLang.i18n ?? swLang.fetchLocale());
-		const { t } = i18n;
 
 		await globalThis.registration.showNotification(
 			(new URL(origin)).host,
@@ -264,11 +287,11 @@ export async function createEmptyNotification(): Promise<void> {
 				actions: [
 					{
 						action: 'markAllAsRead',
-						title: t('markAllAsRead'),
+						title: i18n.ts.markAllAsRead,
 					},
 					{
 						action: 'settings',
-						title: t('notificationSettings'),
+						title: i18n.ts.notificationSettings,
 					},
 				],
 				data: {},
diff --git a/packages/sw/src/scripts/get-account-from-id.ts b/packages/sw/src/scripts/get-account-from-id.ts
index 19bfe052ee..157dbd005e 100644
--- a/packages/sw/src/scripts/get-account-from-id.ts
+++ b/packages/sw/src/scripts/get-account-from-id.ts
@@ -4,9 +4,10 @@
  */
 
 import { get } from 'idb-keyval';
+import * as Misskey from 'misskey-js';
 
-export async function getAccountFromId(id: string): Promise<{ token: string; id: string } | void> {
-	const accounts = await get<{ token: string; id: string }[]>('accounts');
+export async function getAccountFromId(id: string): Promise<Pick<Misskey.entities.SignupResponse, 'id' | 'token'> | undefined> {
+	const accounts = await get<Pick<Misskey.entities.SignupResponse, 'id' | 'token'>[]>('accounts');
 	if (!accounts) {
 		console.log('Accounts are not recorded');
 		return;
diff --git a/packages/sw/src/scripts/i18n.ts b/packages/sw/src/scripts/i18n.ts
deleted file mode 100644
index 77b955dbe8..0000000000
--- a/packages/sw/src/scripts/i18n.ts
+++ /dev/null
@@ -1,37 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and misskey-project
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-export type Locale = { [key: string]: string | Locale };
-
-export class I18n<T extends Locale = Locale> {
-	public ts: T;
-
-	constructor(locale: T) {
-		this.ts = locale;
-
-		//#region BIND
-		this.t = this.t.bind(this);
-		//#endregion
-	}
-
-	// string にしているのは、ドット区切りでのパス指定を許可するため
-	// なるべくこのメソッド使うよりもlocale直接参照の方がvueのキャッシュ効いてパフォーマンスが良いかも
-	public t(key: string, args?: Record<string, string>): string {
-		try {
-			let str = key.split('.').reduce<Locale | Locale[keyof Locale]>((o, i) => o[i], this.ts);
-			if (typeof str !== 'string') throw new Error();
-
-			if (args) {
-				for (const [k, v] of Object.entries(args)) {
-					str = str.replace(`{${k}}`, v);
-				}
-			}
-			return str;
-		} catch (err) {
-			console.warn(`missing localization '${key}'`);
-			return key;
-		}
-	}
-}
diff --git a/packages/sw/src/scripts/lang.ts b/packages/sw/src/scripts/lang.ts
index 6fccedd746..3000160e41 100644
--- a/packages/sw/src/scripts/lang.ts
+++ b/packages/sw/src/scripts/lang.ts
@@ -7,7 +7,8 @@
  * Language manager for SW
  */
 import { get, set } from 'idb-keyval';
-import { I18n, type Locale } from '@/scripts/i18n.js';
+import { I18n } from '@@/js/i18n.js';
+import type { Locale } from '../../../../locales/index.js';
 
 class SwLang {
 	public cacheName = `mk-cache-${_VERSION_}`;
@@ -23,7 +24,7 @@ class SwLang {
 		return this.fetchLocale();
 	}
 
-	public i18n: Promise<I18n> | null = null;
+	public i18n: Promise<I18n<Locale>> | null = null;
 
 	public fetchLocale(): Promise<I18n<Locale>> {
 		return (this.i18n = this._fetch());
diff --git a/packages/sw/src/scripts/operations.ts b/packages/sw/src/scripts/operations.ts
index 24eea06231..8862c6faa5 100644
--- a/packages/sw/src/scripts/operations.ts
+++ b/packages/sw/src/scripts/operations.ts
@@ -14,15 +14,22 @@ import { getUrlWithLoginId } from '@/scripts/login-id.js';
 
 export const cli = new Misskey.api.APIClient({ origin, fetch: (...args): Promise<Response> => fetch(...args) });
 
-export async function api<E extends keyof Misskey.Endpoints, O extends Misskey.Endpoints[E]['req']>(endpoint: E, userId?: string, options?: O): Promise<void | ReturnType<typeof cli.request<E, O>>> {
-	let account: { token: string; id: string } | void = undefined;
+export async function api<
+	E extends keyof Misskey.Endpoints,
+	P extends Misskey.Endpoints[E]['req']
+>(endpoint: E, userId?: string, params?: P): Promise<Misskey.api.SwitchCaseResponseType<E, P> | undefined> {
+	let account: Pick<Misskey.entities.SignupResponse, 'id' | 'token'> | undefined;
 
 	if (userId) {
 		account = await getAccountFromId(userId);
 		if (!account) return;
 	}
 
-	return cli.request(endpoint, options, account?.token);
+	return (cli.request as <E extends keyof Misskey.Endpoints, P extends Misskey.Endpoints[E]['req']>(
+		endpoint: E,
+		params: P,
+		credential?: string | null,
+	) => Promise<Misskey.api.SwitchCaseResponseType<E, P>>)(endpoint, params, account?.token);
 }
 
 // mark-all-as-read送出を1秒間隔に制限する
@@ -33,7 +40,7 @@ export function sendMarkAllAsRead(userId: string): Promise<null | undefined | vo
 	return new Promise(resolve => {
 		setTimeout(() => {
 			readBlockingStatus.set(userId, false);
-			api('notifications/mark-all-as-read', userId).then(resolve, resolve);
+			(api('notifications/mark-all-as-read', userId) as Promise<void>).then(resolve, resolve);
 		}, 1000);
 	});
 }
diff --git a/packages/sw/src/sw.ts b/packages/sw/src/sw.ts
index cc79d88713..bf980b83a4 100644
--- a/packages/sw/src/sw.ts
+++ b/packages/sw/src/sw.ts
@@ -6,7 +6,8 @@
 import { get } from 'idb-keyval';
 import * as Misskey from 'misskey-js';
 import type { PushNotificationDataMap } from '@/types.js';
-import type { I18n, Locale } from '@/scripts/i18n.js';
+import type { I18n } from '@@/js/i18n.js';
+import type { Locale } from '../../../locales/index.js';
 import { createEmptyNotification, createNotification } from '@/scripts/create-notification.js';
 import { swLang } from '@/scripts/lang.js';
 import * as swos from '@/scripts/operations.js';
@@ -30,8 +31,8 @@ globalThis.addEventListener('activate', ev => {
 async function offlineContentHTML() {
 	const i18n = await (swLang.i18n ?? swLang.fetchLocale()) as Partial<I18n<Locale>>;
 	const messages = {
-		title: i18n.ts?._offlineScreen?.title ?? 'Offline - Could not connect to server',
-		header: i18n.ts?._offlineScreen?.header ?? 'Could not connect to server',
+		title: i18n.ts?._offlineScreen.title ?? 'Offline - Could not connect to server',
+		header: i18n.ts?._offlineScreen.header ?? 'Could not connect to server',
 		reload: i18n.ts?.reload ?? 'Reload',
 	};
 
@@ -159,8 +160,8 @@ globalThis.addEventListener('notificationclick', (ev: ServiceWorkerGlobalScopeEv
 					case 'markAllAsRead':
 						await globalThis.registration.getNotifications()
 							.then(notifications => notifications.forEach(n => n.tag !== 'read_notification' && n.close()));
-						await get('accounts').then(accounts => {
-							return Promise.all(accounts.map(async account => {
+						await get<Pick<Misskey.entities.SignupResponse, 'id' | 'token'>[]>('accounts').then(accounts => {
+							return Promise.all((accounts ?? []).map(async account => {
 								await swos.sendMarkAllAsRead(account.id);
 							}));
 						});
diff --git a/packages/sw/src/types.ts b/packages/sw/src/types.ts
index fac3e707d8..4f82779808 100644
--- a/packages/sw/src/types.ts
+++ b/packages/sw/src/types.ts
@@ -50,4 +50,5 @@ export type BadgeNames =
 	| 'quote'
 	| 'repeat'
 	| 'user-plus'
-	| 'users';
+	| 'users'
+	| 'login-2';
diff --git a/packages/sw/tsconfig.json b/packages/sw/tsconfig.json
index f3f3543013..2712475a37 100644
--- a/packages/sw/tsconfig.json
+++ b/packages/sw/tsconfig.json
@@ -21,7 +21,8 @@
 		"isolatedModules": true,
 		"baseUrl": ".",
 		"paths": {
-			"@/*": ["./src/*"]
+			"@/*": ["./src/*"],
+			"@@/*": ["../frontend-shared/*"]
 		},
 		"typeRoots": [
 			"./node_modules/@types",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 6bf1cf158c..1312e8c886 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -14,10 +14,10 @@ importers:
     dependencies:
       cssnano:
         specifier: 6.1.2
-        version: 6.1.2(postcss@8.4.38)
+        version: 6.1.2(postcss@8.4.47)
       esbuild:
-        specifier: 0.20.2
-        version: 0.20.2
+        specifier: 0.23.1
+        version: 0.23.1
       execa:
         specifier: 8.0.1
         version: 8.0.1
@@ -25,100 +25,106 @@ importers:
         specifier: 3.3.2
         version: 3.3.2
       glob:
-        specifier: 10.3.12
-        version: 10.3.12
+        specifier: 11.0.0
+        version: 11.0.0
       ignore-walk:
-        specifier: 6.0.4
-        version: 6.0.4
+        specifier: 6.0.5
+        version: 6.0.5
       js-yaml:
         specifier: 4.1.0
         version: 4.1.0
       postcss:
-        specifier: 8.4.38
-        version: 8.4.38
+        specifier: 8.4.47
+        version: 8.4.47
       tar:
         specifier: 6.2.1
         version: 6.2.1
       terser:
-        specifier: 5.30.3
-        version: 5.30.3
+        specifier: 5.33.0
+        version: 5.33.0
       typescript:
-        specifier: 5.4.5
-        version: 5.4.5
+        specifier: 5.6.2
+        version: 5.6.2
     optionalDependencies:
       '@tensorflow/tfjs-core':
         specifier: 4.4.0
         version: 4.4.0(encoding@0.1.13)
     devDependencies:
+      '@misskey-dev/eslint-plugin':
+        specifier: 2.0.3
+        version: 2.0.3(@eslint/compat@1.1.1)(@typescript-eslint/eslint-plugin@7.17.0(@typescript-eslint/parser@7.17.0(eslint@9.8.0)(typescript@5.6.2))(eslint@9.8.0)(typescript@5.6.2))(@typescript-eslint/parser@7.17.0(eslint@9.8.0)(typescript@5.6.2))(eslint-plugin-import@2.31.0(@typescript-eslint/parser@7.17.0(eslint@9.8.0)(typescript@5.6.2))(eslint@9.8.0))(eslint@9.8.0)(globals@15.9.0)
       '@types/node':
-        specifier: 20.12.7
-        version: 20.12.7
+        specifier: 20.14.12
+        version: 20.14.12
       '@typescript-eslint/eslint-plugin':
-        specifier: 7.7.1
-        version: 7.7.1(@typescript-eslint/parser@7.7.1(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0)(typescript@5.4.5)
+        specifier: 7.17.0
+        version: 7.17.0(@typescript-eslint/parser@7.17.0(eslint@9.8.0)(typescript@5.6.2))(eslint@9.8.0)(typescript@5.6.2)
       '@typescript-eslint/parser':
-        specifier: 7.7.1
-        version: 7.7.1(eslint@8.57.0)(typescript@5.4.5)
+        specifier: 7.17.0
+        version: 7.17.0(eslint@9.8.0)(typescript@5.6.2)
       cross-env:
         specifier: 7.0.3
         version: 7.0.3
       cypress:
-        specifier: 13.7.3
-        version: 13.7.3
+        specifier: 13.14.2
+        version: 13.14.2
       eslint:
-        specifier: 8.57.0
-        version: 8.57.0
+        specifier: 9.8.0
+        version: 9.8.0
+      globals:
+        specifier: 15.9.0
+        version: 15.9.0
       ncp:
         specifier: 2.0.0
         version: 2.0.0
       start-server-and-test:
-        specifier: 2.0.3
-        version: 2.0.3
+        specifier: 2.0.8
+        version: 2.0.8
 
   packages/backend:
     dependencies:
       '@aws-sdk/client-s3':
-        specifier: 3.412.0
-        version: 3.412.0
+        specifier: 3.620.0
+        version: 3.620.0
       '@aws-sdk/lib-storage':
-        specifier: 3.412.0
-        version: 3.412.0(@aws-sdk/client-s3@3.412.0)
+        specifier: 3.620.0
+        version: 3.620.0(@aws-sdk/client-s3@3.620.0)
       '@bull-board/api':
-        specifier: 5.17.0
-        version: 5.17.0(@bull-board/ui@5.17.0)
+        specifier: 6.0.0
+        version: 6.0.0(@bull-board/ui@6.0.0)
       '@bull-board/fastify':
-        specifier: 5.17.0
-        version: 5.17.0
+        specifier: 6.0.0
+        version: 6.0.0
       '@bull-board/ui':
-        specifier: 5.17.0
-        version: 5.17.0
+        specifier: 6.0.0
+        version: 6.0.0
       '@discordapp/twemoji':
-        specifier: 15.0.3
-        version: 15.0.3
+        specifier: 15.1.0
+        version: 15.1.0
       '@fastify/accepts':
-        specifier: 4.3.0
-        version: 4.3.0
+        specifier: 5.0.1
+        version: 5.0.1
       '@fastify/cookie':
-        specifier: 9.3.1
-        version: 9.3.1
+        specifier: 10.0.1
+        version: 10.0.1
       '@fastify/cors':
+        specifier: 10.0.1
+        version: 10.0.1
+      '@fastify/express':
+        specifier: 4.0.1
+        version: 4.0.1
+      '@fastify/http-proxy':
+        specifier: 10.0.0
+        version: 10.0.0(bufferutil@4.0.7)(utf-8-validate@6.0.3)
+      '@fastify/multipart':
         specifier: 9.0.1
         version: 9.0.1
-      '@fastify/express':
-        specifier: 3.0.0
-        version: 3.0.0
-      '@fastify/http-proxy':
-        specifier: 9.5.0
-        version: 9.5.0(bufferutil@4.0.7)(utf-8-validate@6.0.3)
-      '@fastify/multipart':
-        specifier: 8.2.0
-        version: 8.2.0
       '@fastify/static':
-        specifier: 7.0.3
-        version: 7.0.3
+        specifier: 8.0.1
+        version: 8.0.1
       '@fastify/view':
-        specifier: 9.1.0
-        version: 9.1.0
+        specifier: 10.0.1
+        version: 10.0.1
       '@misskey-dev/sharp-read-bmp':
         specifier: 1.2.0
         version: 1.2.0
@@ -126,29 +132,29 @@ importers:
         specifier: 5.1.0
         version: 5.1.0
       '@napi-rs/canvas':
-        specifier: ^0.1.52
-        version: 0.1.52
+        specifier: 0.1.56
+        version: 0.1.56
       '@nestjs/common':
-        specifier: 10.3.8
-        version: 10.3.8(reflect-metadata@0.2.2)(rxjs@7.8.1)
+        specifier: 10.4.4
+        version: 10.4.4(reflect-metadata@0.2.2)(rxjs@7.8.1)
       '@nestjs/core':
-        specifier: 10.3.8
-        version: 10.3.8(@nestjs/common@10.3.8(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.3.8)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.1)
+        specifier: 10.4.4
+        version: 10.4.4(@nestjs/common@10.4.4(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.4)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.1)
       '@nestjs/testing':
-        specifier: 10.3.8
-        version: 10.3.8(@nestjs/common@10.3.8(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.3.8(@nestjs/common@10.3.8(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.3.8)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.3.8(@nestjs/common@10.3.8(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.3.8))
+        specifier: 10.4.4
+        version: 10.4.4(@nestjs/common@10.4.4(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.4(@nestjs/common@10.4.4(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.4)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.4(@nestjs/common@10.4.4(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.4))
       '@peertube/http-signature':
         specifier: 1.7.0
         version: 1.7.0
       '@sentry/node':
-        specifier: ^8.5.0
-        version: 8.5.0
+        specifier: 8.20.0
+        version: 8.20.0
       '@sentry/profiling-node':
-        specifier: ^8.5.0
-        version: 8.5.0
+        specifier: 8.20.0
+        version: 8.20.0
       '@simplewebauthn/server':
-        specifier: 10.0.0
-        version: 10.0.0(encoding@0.1.13)
+        specifier: 10.0.1
+        version: 10.0.1(encoding@0.1.13)
       '@sinonjs/fake-timers':
         specifier: 11.2.2
         version: 11.2.2
@@ -157,10 +163,10 @@ importers:
         version: 2.5.0
       '@swc/cli':
         specifier: 0.3.12
-        version: 0.3.12(@swc/core@1.4.17)(chokidar@3.5.3)
+        version: 0.3.12(@swc/core@1.6.6)(chokidar@3.5.3)
       '@swc/core':
-        specifier: 1.4.17
-        version: 1.4.17
+        specifier: 1.6.6
+        version: 1.6.6
       '@twemoji/parser':
         specifier: 15.1.1
         version: 15.1.1
@@ -168,8 +174,8 @@ importers:
         specifier: 1.3.8
         version: 1.3.8
       ajv:
-        specifier: 8.13.0
-        version: 8.13.0
+        specifier: 8.17.1
+        version: 8.17.1
       archiver:
         specifier: 7.0.1
         version: 7.0.1
@@ -183,11 +189,11 @@ importers:
         specifier: 2.0.5
         version: 2.0.5
       body-parser:
-        specifier: 1.20.2
-        version: 1.20.2
+        specifier: 1.20.3
+        version: 1.20.3
       bullmq:
-        specifier: 5.7.8
-        version: 5.7.8
+        specifier: 5.15.0
+        version: 5.15.0
       cacheable-lookup:
         specifier: 7.0.0
         version: 7.0.0
@@ -219,29 +225,29 @@ importers:
         specifier: 0.1.21
         version: 0.1.21
       fastify:
-        specifier: 4.26.2
-        version: 4.26.2
+        specifier: 5.0.0
+        version: 5.0.0
       fastify-raw-body:
-        specifier: 4.3.0
-        version: 4.3.0
+        specifier: 5.0.0
+        version: 5.0.0
       feed:
         specifier: 4.2.2
         version: 4.2.2
       file-type:
-        specifier: 19.0.0
-        version: 19.0.0
+        specifier: 19.5.0
+        version: 19.5.0
       fluent-ffmpeg:
-        specifier: 2.1.2
-        version: 2.1.2
+        specifier: 2.1.3
+        version: 2.1.3
       form-data:
         specifier: 4.0.0
         version: 4.0.0
       got:
-        specifier: 14.2.1
-        version: 14.2.1
+        specifier: 14.4.2
+        version: 14.4.2
       happy-dom:
-        specifier: 10.0.3
-        version: 10.0.3
+        specifier: 15.7.4
+        version: 15.7.4
       hpagent:
         specifier: 1.2.0
         version: 1.2.0
@@ -255,32 +261,35 @@ importers:
         specifier: 5.4.1
         version: 5.4.1
       ip-cidr:
-        specifier: 3.1.0
-        version: 3.1.0
+        specifier: 4.0.2
+        version: 4.0.2
       ipaddr.js:
         specifier: 2.2.0
         version: 2.2.0
       is-svg:
-        specifier: 5.0.0
-        version: 5.0.0
+        specifier: 5.1.0
+        version: 5.1.0
       js-yaml:
         specifier: 4.1.0
         version: 4.1.0
       jsdom:
-        specifier: 24.0.0
-        version: 24.0.0(bufferutil@4.0.7)(utf-8-validate@6.0.3)
+        specifier: 24.1.1
+        version: 24.1.1(bufferutil@4.0.7)(utf-8-validate@6.0.3)
       json5:
         specifier: 2.2.3
         version: 2.2.3
       jsonld:
         specifier: 8.3.2
-        version: 8.3.2(web-streams-polyfill@3.2.1)
+        version: 8.3.2(web-streams-polyfill@4.0.0)
       jsrsasign:
         specifier: 11.1.0
         version: 11.1.0
+      juice:
+        specifier: 11.0.0
+        version: 11.0.0
       meilisearch:
-        specifier: 0.38.0
-        version: 0.38.0(encoding@0.1.13)
+        specifier: 0.42.0
+        version: 0.42.0(encoding@0.1.13)
       mfm-js:
         specifier: 0.24.0
         version: 0.24.0
@@ -309,8 +318,8 @@ importers:
         specifier: 3.3.2
         version: 3.3.2
       nodemailer:
-        specifier: 6.9.13
-        version: 6.9.13
+        specifier: 6.9.15
+        version: 6.9.15
       nsfwjs:
         specifier: 2.4.2
         version: 2.4.2(@tensorflow/tfjs@4.4.0(encoding@0.1.13)(seedrandom@3.0.5))
@@ -327,14 +336,14 @@ importers:
         specifier: 0.0.14
         version: 0.0.14
       otpauth:
-        specifier: 9.2.3
-        version: 9.2.3
+        specifier: 9.3.4
+        version: 9.3.4
       parse5:
         specifier: 7.1.2
         version: 7.1.2
       pg:
-        specifier: 8.11.5
-        version: 8.11.5
+        specifier: 8.13.0
+        version: 8.13.0
       pkce-challenge:
         specifier: 4.1.0
         version: 4.1.0
@@ -345,14 +354,14 @@ importers:
         specifier: 2.7.0
         version: 2.7.0
       pug:
-        specifier: 3.0.2
-        version: 3.0.2
+        specifier: 3.0.3
+        version: 3.0.3
       punycode:
         specifier: 2.3.1
         version: 2.3.1
       qrcode:
-        specifier: 1.5.3
-        version: 1.5.3
+        specifier: 1.5.4
+        version: 1.5.4
       random-seed:
         specifier: 0.3.0
         version: 0.3.0
@@ -360,8 +369,8 @@ importers:
         specifier: 3.4.1
         version: 3.4.1
       re2:
-        specifier: 1.20.10
-        version: 1.20.10
+        specifier: 1.21.4
+        version: 1.21.4
       redis-lock:
         specifier: 0.1.4
         version: 0.1.4
@@ -378,14 +387,14 @@ importers:
         specifier: 7.8.1
         version: 7.8.1
       sanitize-html:
-        specifier: 2.13.0
-        version: 2.13.0
+        specifier: 2.13.1
+        version: 2.13.1
       secure-json-parse:
         specifier: 2.7.0
         version: 2.7.0
       sharp:
-        specifier: 0.33.3
-        version: 0.33.3
+        specifier: 0.33.5
+        version: 0.33.5
       slacc:
         specifier: 0.0.10
         version: 0.0.10
@@ -396,8 +405,8 @@ importers:
         specifier: 2.1.0
         version: 2.1.0
       systeminformation:
-        specifier: 5.22.7
-        version: 5.22.7
+        specifier: 5.23.5
+        version: 5.23.5
       tinycolor2:
         specifier: 1.6.0
         version: 1.6.0
@@ -405,17 +414,17 @@ importers:
         specifier: 0.2.3
         version: 0.2.3
       tsc-alias:
-        specifier: 1.8.8
-        version: 1.8.8
+        specifier: 1.8.10
+        version: 1.8.10
       tsconfig-paths:
         specifier: 4.2.0
         version: 4.2.0
       typeorm:
         specifier: 0.3.20
-        version: 0.3.20(ioredis@5.4.1)(pg@8.11.5)
+        version: 0.3.20(ioredis@5.4.1)(pg@8.13.0)
       typescript:
-        specifier: 5.4.5
-        version: 5.4.5
+        specifier: 5.6.2
+        version: 5.6.2
       ulid:
         specifier: 2.3.0
         version: 2.3.0
@@ -426,8 +435,8 @@ importers:
         specifier: 3.6.7
         version: 3.6.7
       ws:
-        specifier: 8.17.0
-        version: 8.17.0(bufferutil@4.0.7)(utf-8-validate@6.0.3)
+        specifier: 8.18.0
+        version: 8.18.0(bufferutil@4.0.7)(utf-8-validate@6.0.3)
       xev:
         specifier: 3.0.2
         version: 3.0.2
@@ -523,18 +532,15 @@ importers:
       '@jest/globals':
         specifier: 29.7.0
         version: 29.7.0
-      '@misskey-dev/eslint-plugin':
-        specifier: 1.0.0
-        version: 1.0.0(@typescript-eslint/eslint-plugin@7.7.1(@typescript-eslint/parser@7.7.1(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0)(typescript@5.4.5))(@typescript-eslint/parser@7.7.1(eslint@8.57.0)(typescript@5.4.5))(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.7.1(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0))(eslint@8.57.0)
       '@nestjs/platform-express':
-        specifier: 10.3.8
-        version: 10.3.8(@nestjs/common@10.3.8(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.3.8)
+        specifier: 10.4.4
+        version: 10.4.4(@nestjs/common@10.4.4(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.4)
       '@simplewebauthn/types':
         specifier: 10.0.0
         version: 10.0.0
       '@swc/jest':
         specifier: 0.2.36
-        version: 0.2.36(@swc/core@1.4.17)
+        version: 0.2.36(@swc/core@1.6.6)
       '@types/accepts':
         specifier: 1.3.7
         version: 1.3.7
@@ -548,32 +554,32 @@ importers:
         specifier: 1.19.5
         version: 1.19.5
       '@types/color-convert':
-        specifier: 2.0.3
-        version: 2.0.3
+        specifier: 2.0.4
+        version: 2.0.4
       '@types/content-disposition':
         specifier: 0.5.8
         version: 0.5.8
       '@types/fluent-ffmpeg':
-        specifier: 2.1.24
-        version: 2.1.24
+        specifier: 2.1.26
+        version: 2.1.26
       '@types/htmlescape':
-        specifier: ^1.1.3
+        specifier: 1.1.3
         version: 1.1.3
       '@types/http-link-header':
-        specifier: 1.0.5
-        version: 1.0.5
+        specifier: 1.0.7
+        version: 1.0.7
       '@types/jest':
-        specifier: 29.5.12
-        version: 29.5.12
+        specifier: 29.5.13
+        version: 29.5.13
       '@types/js-yaml':
         specifier: 4.0.9
         version: 4.0.9
       '@types/jsdom':
-        specifier: 21.1.6
-        version: 21.1.6
+        specifier: 21.1.7
+        version: 21.1.7
       '@types/jsonld':
-        specifier: 1.5.13
-        version: 1.5.13
+        specifier: 1.5.15
+        version: 1.5.15
       '@types/jsrsasign':
         specifier: 10.5.14
         version: 10.5.14
@@ -584,17 +590,14 @@ importers:
         specifier: 0.7.34
         version: 0.7.34
       '@types/node':
-        specifier: 20.12.7
-        version: 20.12.7
-      '@types/node-fetch':
-        specifier: 3.0.3
-        version: 3.0.3
+        specifier: 20.14.12
+        version: 20.14.12
       '@types/nodemailer':
-        specifier: 6.4.15
-        version: 6.4.15
+        specifier: 6.4.16
+        version: 6.4.16
       '@types/oauth':
-        specifier: 0.9.4
-        version: 0.9.4
+        specifier: 0.9.5
+        version: 0.9.5
       '@types/oauth2orize':
         specifier: 1.11.5
         version: 1.11.5
@@ -602,8 +605,8 @@ importers:
         specifier: 0.1.2
         version: 0.1.2
       '@types/pg':
-        specifier: 8.11.5
-        version: 8.11.5
+        specifier: 8.11.10
+        version: 8.11.10
       '@types/pug':
         specifier: 2.0.10
         version: 2.0.10
@@ -623,8 +626,8 @@ importers:
         specifier: 1.0.7
         version: 1.0.7
       '@types/sanitize-html':
-        specifier: 2.11.0
-        version: 2.11.0
+        specifier: 2.13.0
+        version: 2.13.0
       '@types/semver':
         specifier: 7.5.8
         version: 7.5.8
@@ -647,53 +650,50 @@ importers:
         specifier: 3.6.3
         version: 3.6.3
       '@types/ws':
-        specifier: 8.5.10
-        version: 8.5.10
+        specifier: 8.5.12
+        version: 8.5.12
       '@typescript-eslint/eslint-plugin':
-        specifier: 7.7.1
-        version: 7.7.1(@typescript-eslint/parser@7.7.1(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0)(typescript@5.4.5)
+        specifier: 7.17.0
+        version: 7.17.0(@typescript-eslint/parser@7.17.0(eslint@9.11.0)(typescript@5.6.2))(eslint@9.11.0)(typescript@5.6.2)
       '@typescript-eslint/parser':
-        specifier: 7.7.1
-        version: 7.7.1(eslint@8.57.0)(typescript@5.4.5)
+        specifier: 7.17.0
+        version: 7.17.0(eslint@9.11.0)(typescript@5.6.2)
       aws-sdk-client-mock:
-        specifier: 3.0.1
-        version: 3.0.1
+        specifier: 4.0.1
+        version: 4.0.1
       cross-env:
         specifier: 7.0.3
         version: 7.0.3
-      eslint:
-        specifier: 8.57.0
-        version: 8.57.0
       eslint-plugin-import:
-        specifier: 2.29.1
-        version: 2.29.1(@typescript-eslint/parser@7.7.1(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0)
+        specifier: 2.30.0
+        version: 2.30.0(@typescript-eslint/parser@7.17.0(eslint@9.11.0)(typescript@5.6.2))(eslint@9.11.0)
       execa:
-        specifier: 8.0.1
-        version: 8.0.1
+        specifier: 9.4.0
+        version: 9.4.0
       fkill:
-        specifier: ^9.0.0
+        specifier: 9.0.0
         version: 9.0.0
       jest:
         specifier: 29.7.0
-        version: 29.7.0(@types/node@20.12.7)
+        version: 29.7.0(@types/node@20.14.12)
       jest-mock:
         specifier: 29.7.0
         version: 29.7.0
       nodemon:
-        specifier: 3.1.0
-        version: 3.1.0
+        specifier: 3.1.7
+        version: 3.1.7
       pid-port:
         specifier: 1.0.0
         version: 1.0.0
       simple-oauth2:
-        specifier: 5.0.0
-        version: 5.0.0
+        specifier: 5.1.0
+        version: 5.1.0
 
   packages/frontend:
     dependencies:
       '@discordapp/twemoji':
-        specifier: 15.0.3
-        version: 15.0.3
+        specifier: 15.1.0
+        version: 15.1.0
       '@github/webauthn-json':
         specifier: 2.1.1
         version: 2.1.1
@@ -705,16 +705,16 @@ importers:
         version: 2024.1.0
       '@rollup/plugin-json':
         specifier: 6.1.0
-        version: 6.1.0(rollup@4.17.2)
+        version: 6.1.0(rollup@4.22.5)
       '@rollup/plugin-replace':
-        specifier: 5.0.5
-        version: 5.0.5(rollup@4.17.2)
+        specifier: 5.0.7
+        version: 5.0.7(rollup@4.22.5)
       '@rollup/pluginutils':
-        specifier: 5.1.0
-        version: 5.1.0(rollup@4.17.2)
+        specifier: 5.1.2
+        version: 5.1.2(rollup@4.22.5)
       '@syuilo/aiscript':
-        specifier: 0.18.0
-        version: 0.18.0
+        specifier: 0.19.0
+        version: 0.19.0
       '@tabler/icons-webfont':
         specifier: 3.3.0
         version: 3.3.0
@@ -722,17 +722,17 @@ importers:
         specifier: 15.1.1
         version: 15.1.1
       '@vitejs/plugin-vue':
-        specifier: 5.0.4
-        version: 5.0.4(vite@5.2.11(@types/node@20.12.7)(sass@1.76.0)(terser@5.30.3))(vue@3.4.26(typescript@5.4.5))
+        specifier: 5.1.4
+        version: 5.1.4(vite@5.4.8(@types/node@20.14.12)(sass@1.79.3)(terser@5.33.0))(vue@3.5.11(typescript@5.6.2))
       '@vue/compiler-sfc':
-        specifier: 3.4.26
-        version: 3.4.26
+        specifier: 3.5.11
+        version: 3.5.11
       aiscript-vscode:
-        specifier: github:aiscript-dev/aiscript-vscode#v0.1.9
-        version: https://codeload.github.com/aiscript-dev/aiscript-vscode/tar.gz/34bf4e1530efcf1efa855bd04e2dab39735e1b02
+        specifier: github:aiscript-dev/aiscript-vscode#v0.1.11
+        version: https://codeload.github.com/aiscript-dev/aiscript-vscode/tar.gz/e1e1b27f2f72cd28a473e004b6da0d8fc0bd40d9
       astring:
-        specifier: 1.8.6
-        version: 1.8.6
+        specifier: 1.9.0
+        version: 1.9.0
       broadcast-channel:
         specifier: 7.0.0
         version: 7.0.0
@@ -743,41 +743,41 @@ importers:
         specifier: 1.9.3
         version: 1.9.3
       chart.js:
-        specifier: 4.4.2
-        version: 4.4.2
+        specifier: 4.4.4
+        version: 4.4.4
       chartjs-adapter-date-fns:
         specifier: 3.0.0
-        version: 3.0.0(chart.js@4.4.2)(date-fns@2.30.0)
+        version: 3.0.0(chart.js@4.4.4)(date-fns@2.30.0)
       chartjs-chart-matrix:
         specifier: 2.0.1
-        version: 2.0.1(chart.js@4.4.2)
+        version: 2.0.1(chart.js@4.4.4)
       chartjs-plugin-gradient:
         specifier: 0.6.1
-        version: 0.6.1(chart.js@4.4.2)
+        version: 0.6.1(chart.js@4.4.4)
       chartjs-plugin-zoom:
         specifier: 2.0.1
-        version: 2.0.1(chart.js@4.4.2)
+        version: 2.0.1(chart.js@4.4.4)
       chromatic:
-        specifier: 11.3.0
-        version: 11.3.0
+        specifier: 11.11.0
+        version: 11.11.0
       compare-versions:
-        specifier: 6.1.0
-        version: 6.1.0
+        specifier: 6.1.1
+        version: 6.1.1
       cropperjs:
-        specifier: 2.0.0-beta.5
-        version: 2.0.0-beta.5
+        specifier: 2.0.0-rc.2
+        version: 2.0.0-rc.2
       date-fns:
         specifier: 2.30.0
         version: 2.30.0
-      escape-regexp:
-        specifier: 0.0.1
-        version: 0.0.1
       estree-walker:
         specifier: 3.0.3
         version: 3.0.3
       eventemitter3:
         specifier: 5.0.1
         version: 5.0.1
+      frontend-shared:
+        specifier: workspace:*
+        version: link:../frontend-shared
       idb-keyval:
         specifier: 6.2.1
         version: 6.2.1
@@ -806,23 +806,23 @@ importers:
         specifier: workspace:*
         version: link:../misskey-reversi
       photoswipe:
-        specifier: 5.4.3
-        version: 5.4.3
+        specifier: 5.4.4
+        version: 5.4.4
       punycode:
         specifier: 2.3.1
         version: 2.3.1
       rollup:
-        specifier: 4.17.2
-        version: 4.17.2
+        specifier: 4.22.5
+        version: 4.22.5
       sanitize-html:
-        specifier: 2.13.0
-        version: 2.13.0
+        specifier: 2.13.1
+        version: 2.13.1
       sass:
-        specifier: 1.76.0
-        version: 1.76.0
+        specifier: 1.79.3
+        version: 1.79.3
       shiki:
-        specifier: 1.4.0
-        version: 1.4.0
+        specifier: 1.21.0
+        version: 1.21.0
       strict-event-emitter-types:
         specifier: 2.0.0
         version: 2.0.0
@@ -830,123 +830,123 @@ importers:
         specifier: 3.1.0
         version: 3.1.0
       three:
-        specifier: 0.164.1
-        version: 0.164.1
+        specifier: 0.169.0
+        version: 0.169.0
       throttle-debounce:
-        specifier: 5.0.0
-        version: 5.0.0
+        specifier: 5.0.2
+        version: 5.0.2
       tinycolor2:
         specifier: 1.6.0
         version: 1.6.0
       tsc-alias:
-        specifier: 1.8.8
-        version: 1.8.8
+        specifier: 1.8.10
+        version: 1.8.10
       tsconfig-paths:
         specifier: 4.2.0
         version: 4.2.0
       typescript:
-        specifier: 5.4.5
-        version: 5.4.5
+        specifier: 5.6.2
+        version: 5.6.2
       uuid:
-        specifier: 9.0.1
-        version: 9.0.1
+        specifier: 10.0.0
+        version: 10.0.0
       v-code-diff:
-        specifier: 1.11.0
-        version: 1.11.0(vue@3.4.26(typescript@5.4.5))
+        specifier: 1.13.1
+        version: 1.13.1(vue@3.5.11(typescript@5.6.2))
       vite:
-        specifier: 5.2.11
-        version: 5.2.11(@types/node@20.12.7)(sass@1.76.0)(terser@5.30.3)
+        specifier: 5.4.8
+        version: 5.4.8(@types/node@20.14.12)(sass@1.79.3)(terser@5.33.0)
       vue:
-        specifier: 3.4.26
-        version: 3.4.26(typescript@5.4.5)
+        specifier: 3.5.11
+        version: 3.5.11(typescript@5.6.2)
       vuedraggable:
         specifier: next
-        version: 4.1.0(vue@3.4.26(typescript@5.4.5))
+        version: 4.1.0(vue@3.5.11(typescript@5.6.2))
     devDependencies:
-      '@misskey-dev/eslint-plugin':
-        specifier: 1.0.0
-        version: 1.0.0(@typescript-eslint/eslint-plugin@7.7.1(@typescript-eslint/parser@7.7.1(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0)(typescript@5.4.5))(@typescript-eslint/parser@7.7.1(eslint@8.57.0)(typescript@5.4.5))(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.7.1(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0))(eslint@8.57.0)
       '@misskey-dev/summaly':
         specifier: 5.1.0
         version: 5.1.0
       '@storybook/addon-actions':
-        specifier: 8.0.9
-        version: 8.0.9
+        specifier: 8.3.4
+        version: 8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))
       '@storybook/addon-essentials':
-        specifier: 8.0.9
-        version: 8.0.9(@types/react@18.0.28)(encoding@0.1.13)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+        specifier: 8.3.4
+        version: 8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))
       '@storybook/addon-interactions':
-        specifier: 8.0.9
-        version: 8.0.9(@jest/globals@29.7.0)(@types/jest@29.5.12)(jest@29.7.0(@types/node@20.12.7))(vitest@0.34.6(happy-dom@10.0.3)(jsdom@24.0.0(bufferutil@4.0.7)(utf-8-validate@6.0.3))(sass@1.76.0)(terser@5.30.3))
+        specifier: 8.3.4
+        version: 8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))
       '@storybook/addon-links':
-        specifier: 8.0.9
-        version: 8.0.9(react@18.3.1)
+        specifier: 8.3.4
+        version: 8.3.4(react@18.3.1)(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))
       '@storybook/addon-mdx-gfm':
-        specifier: 8.0.9
-        version: 8.0.9
+        specifier: 8.3.4
+        version: 8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))
       '@storybook/addon-storysource':
-        specifier: 8.0.9
-        version: 8.0.9
+        specifier: 8.3.4
+        version: 8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))
       '@storybook/blocks':
-        specifier: 8.0.9
-        version: 8.0.9(@types/react@18.0.28)(encoding@0.1.13)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+        specifier: 8.3.4
+        version: 8.3.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))
       '@storybook/components':
-        specifier: 8.0.9
-        version: 8.0.9(@types/react@18.0.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+        specifier: 8.3.4
+        version: 8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))
       '@storybook/core-events':
-        specifier: 8.0.9
-        version: 8.0.9
+        specifier: 8.3.4
+        version: 8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))
       '@storybook/manager-api':
-        specifier: 8.0.9
-        version: 8.0.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+        specifier: 8.3.4
+        version: 8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))
       '@storybook/preview-api':
-        specifier: 8.0.9
-        version: 8.0.9
+        specifier: 8.3.4
+        version: 8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))
       '@storybook/react':
-        specifier: 8.0.9
-        version: 8.0.9(encoding@0.1.13)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.4.5)
+        specifier: 8.3.4
+        version: 8.3.4(@storybook/test@8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4)))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))(typescript@5.6.2)
       '@storybook/react-vite':
-        specifier: 8.0.9
-        version: 8.0.9(encoding@0.1.13)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@4.17.2)(typescript@5.4.5)(vite@5.2.11(@types/node@20.12.7)(sass@1.76.0)(terser@5.30.3))
+        specifier: 8.3.4
+        version: 8.3.4(@storybook/test@8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4)))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@4.22.5)(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))(typescript@5.6.2)(vite@5.4.8(@types/node@20.14.12)(sass@1.79.3)(terser@5.33.0))
       '@storybook/test':
-        specifier: 8.0.9
-        version: 8.0.9(@jest/globals@29.7.0)(@types/jest@29.5.12)(jest@29.7.0(@types/node@20.12.7))(vitest@0.34.6(happy-dom@10.0.3)(jsdom@24.0.0(bufferutil@4.0.7)(utf-8-validate@6.0.3))(sass@1.76.0)(terser@5.30.3))
+        specifier: 8.3.4
+        version: 8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))
       '@storybook/theming':
-        specifier: 8.0.9
-        version: 8.0.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+        specifier: 8.3.4
+        version: 8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))
       '@storybook/types':
-        specifier: 8.0.9
-        version: 8.0.9
+        specifier: 8.3.4
+        version: 8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))
       '@storybook/vue3':
-        specifier: 8.0.9
-        version: 8.0.9(encoding@0.1.13)(vue@3.4.26(typescript@5.4.5))
+        specifier: 8.3.4
+        version: 8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))(vue@3.5.11(typescript@5.6.2))
       '@storybook/vue3-vite':
-        specifier: 8.0.9
-        version: 8.0.9(bufferutil@4.0.7)(encoding@0.1.13)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(utf-8-validate@6.0.3)(vite@5.2.11(@types/node@20.12.7)(sass@1.76.0)(terser@5.30.3))(vue@3.4.26(typescript@5.4.5))
+        specifier: 8.3.4
+        version: 8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))(vite@5.4.8(@types/node@20.14.12)(sass@1.79.3)(terser@5.33.0))(vue@3.5.11(typescript@5.6.2))
       '@testing-library/vue':
-        specifier: 8.0.3
-        version: 8.0.3(@vue/compiler-sfc@3.4.26)(@vue/server-renderer@3.4.26(vue@3.4.26(typescript@5.4.5)))(vue@3.4.26(typescript@5.4.5))
-      '@types/escape-regexp':
-        specifier: 0.0.3
-        version: 0.0.3
+        specifier: 8.1.0
+        version: 8.1.0(@vue/compiler-sfc@3.5.11)(@vue/server-renderer@3.5.11(vue@3.5.11(typescript@5.6.2)))(vue@3.5.11(typescript@5.6.2))
+      '@types/canvas-confetti':
+        specifier: ^1.6.4
+        version: 1.6.4
       '@types/estree':
-        specifier: 1.0.5
-        version: 1.0.5
+        specifier: 1.0.6
+        version: 1.0.6
       '@types/matter-js':
-        specifier: 0.19.6
-        version: 0.19.6
+        specifier: 0.19.7
+        version: 0.19.7
       '@types/micromatch':
-        specifier: 4.0.7
-        version: 4.0.7
+        specifier: 4.0.9
+        version: 4.0.9
       '@types/node':
-        specifier: 20.12.7
-        version: 20.12.7
+        specifier: 20.14.12
+        version: 20.14.12
       '@types/punycode':
         specifier: 2.1.4
         version: 2.1.4
       '@types/sanitize-html':
-        specifier: 2.11.0
-        version: 2.11.0
+        specifier: 2.13.0
+        version: 2.13.0
+      '@types/seedrandom':
+        specifier: 3.0.8
+        version: 3.0.8
       '@types/throttle-debounce':
         specifier: 5.0.2
         version: 5.0.2
@@ -954,41 +954,38 @@ importers:
         specifier: 1.4.6
         version: 1.4.6
       '@types/uuid':
-        specifier: 9.0.8
-        version: 9.0.8
+        specifier: 10.0.0
+        version: 10.0.0
       '@types/ws':
-        specifier: 8.5.10
-        version: 8.5.10
+        specifier: 8.5.12
+        version: 8.5.12
       '@typescript-eslint/eslint-plugin':
-        specifier: 7.7.1
-        version: 7.7.1(@typescript-eslint/parser@7.7.1(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0)(typescript@5.4.5)
+        specifier: 7.17.0
+        version: 7.17.0(@typescript-eslint/parser@7.17.0(eslint@9.11.0)(typescript@5.6.2))(eslint@9.11.0)(typescript@5.6.2)
       '@typescript-eslint/parser':
-        specifier: 7.7.1
-        version: 7.7.1(eslint@8.57.0)(typescript@5.4.5)
+        specifier: 7.17.0
+        version: 7.17.0(eslint@9.11.0)(typescript@5.6.2)
       '@vitest/coverage-v8':
-        specifier: 0.34.6
-        version: 0.34.6(vitest@0.34.6(happy-dom@10.0.3)(jsdom@24.0.0(bufferutil@4.0.7)(utf-8-validate@6.0.3))(sass@1.76.0)(terser@5.30.3))
+        specifier: 1.6.0
+        version: 1.6.0(vitest@1.6.0(@types/node@20.14.12)(happy-dom@10.0.3)(jsdom@24.1.1(bufferutil@4.0.8)(utf-8-validate@6.0.4))(sass@1.79.3)(terser@5.33.0))
       '@vue/runtime-core':
-        specifier: 3.4.26
-        version: 3.4.26
+        specifier: 3.5.11
+        version: 3.5.11
       acorn:
-        specifier: 8.11.3
-        version: 8.11.3
+        specifier: 8.12.1
+        version: 8.12.1
       cross-env:
         specifier: 7.0.3
         version: 7.0.3
       cypress:
-        specifier: 13.8.1
-        version: 13.8.1
-      eslint:
-        specifier: 8.57.0
-        version: 8.57.0
+        specifier: 13.15.0
+        version: 13.15.0
       eslint-plugin-import:
-        specifier: 2.29.1
-        version: 2.29.1(@typescript-eslint/parser@7.7.1(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0)
+        specifier: 2.31.0
+        version: 2.31.0(@typescript-eslint/parser@7.17.0(eslint@9.11.0)(typescript@5.6.2))(eslint@9.11.0)
       eslint-plugin-vue:
-        specifier: 9.25.0
-        version: 9.25.0(eslint@8.57.0)
+        specifier: 9.28.0
+        version: 9.28.0(eslint@9.11.0)
       fast-glob:
         specifier: 3.3.2
         version: 3.3.2
@@ -999,53 +996,256 @@ importers:
         specifier: 0.12.2
         version: 0.12.2
       micromatch:
-        specifier: 4.0.5
-        version: 4.0.5
+        specifier: 4.0.8
+        version: 4.0.8
       msw:
-        specifier: 2.2.14
-        version: 2.2.14(typescript@5.4.5)
+        specifier: 2.4.9
+        version: 2.4.9(typescript@5.6.2)
       msw-storybook-addon:
-        specifier: 2.0.1
-        version: 2.0.1(msw@2.2.14(typescript@5.4.5))
+        specifier: 2.0.3
+        version: 2.0.3(msw@2.4.9(typescript@5.6.2))
       nodemon:
-        specifier: 3.1.0
-        version: 3.1.0
+        specifier: 3.1.7
+        version: 3.1.7
       prettier:
-        specifier: 3.2.5
-        version: 3.2.5
+        specifier: 3.3.3
+        version: 3.3.3
       react:
         specifier: 18.3.1
         version: 18.3.1
       react-dom:
         specifier: 18.3.1
         version: 18.3.1(react@18.3.1)
+      seedrandom:
+        specifier: 3.0.5
+        version: 3.0.5
       start-server-and-test:
-        specifier: 2.0.3
-        version: 2.0.3
+        specifier: 2.0.8
+        version: 2.0.8
       storybook:
-        specifier: 8.0.9
-        version: 8.0.9(@babel/preset-env@7.23.5(@babel/core@7.24.0))(bufferutil@4.0.7)(encoding@0.1.13)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(utf-8-validate@6.0.3)
+        specifier: 8.3.4
+        version: 8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4)
       storybook-addon-misskey-theme:
         specifier: github:misskey-dev/storybook-addon-misskey-theme
-        version: https://codeload.github.com/misskey-dev/storybook-addon-misskey-theme/tar.gz/cf583db098365b2ccc81a82f63ca9c93bc32b640(@storybook/blocks@8.0.9(@types/react@18.0.28)(encoding@0.1.13)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@storybook/components@8.0.9(@types/react@18.0.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@storybook/core-events@8.0.9)(@storybook/manager-api@8.0.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@storybook/preview-api@8.0.9)(@storybook/theming@8.0.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@storybook/types@8.0.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+        version: https://codeload.github.com/misskey-dev/storybook-addon-misskey-theme/tar.gz/cf583db098365b2ccc81a82f63ca9c93bc32b640(@storybook/blocks@8.3.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4)))(@storybook/components@8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4)))(@storybook/core-events@8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4)))(@storybook/manager-api@8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4)))(@storybook/preview-api@8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4)))(@storybook/theming@8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4)))(@storybook/types@8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4)))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
       vite-plugin-turbosnap:
         specifier: 1.0.3
         version: 1.0.3
       vitest:
-        specifier: 0.34.6
-        version: 0.34.6(happy-dom@10.0.3)(jsdom@24.0.0(bufferutil@4.0.7)(utf-8-validate@6.0.3))(sass@1.76.0)(terser@5.30.3)
+        specifier: 1.6.0
+        version: 1.6.0(@types/node@20.14.12)(happy-dom@10.0.3)(jsdom@24.1.1(bufferutil@4.0.8)(utf-8-validate@6.0.4))(sass@1.79.3)(terser@5.33.0)
       vitest-fetch-mock:
         specifier: 0.2.2
-        version: 0.2.2(encoding@0.1.13)(vitest@0.34.6(happy-dom@10.0.3)(jsdom@24.0.0(bufferutil@4.0.7)(utf-8-validate@6.0.3))(sass@1.76.0)(terser@5.30.3))
+        version: 0.2.2(encoding@0.1.13)(vitest@1.6.0(@types/node@20.14.12)(happy-dom@10.0.3)(jsdom@24.1.1(bufferutil@4.0.8)(utf-8-validate@6.0.4))(sass@1.79.3)(terser@5.33.0))
       vue-component-type-helpers:
-        specifier: 2.0.16
-        version: 2.0.16
+        specifier: 2.1.6
+        version: 2.1.6
       vue-eslint-parser:
-        specifier: 9.4.2
-        version: 9.4.2(eslint@8.57.0)
+        specifier: 9.4.3
+        version: 9.4.3(eslint@9.11.0)
       vue-tsc:
-        specifier: 2.0.16
-        version: 2.0.16(typescript@5.4.5)
+        specifier: 2.1.6
+        version: 2.1.6(typescript@5.6.2)
+
+  packages/frontend-embed:
+    dependencies:
+      '@discordapp/twemoji':
+        specifier: 15.1.0
+        version: 15.1.0
+      '@rollup/plugin-json':
+        specifier: 6.1.0
+        version: 6.1.0(rollup@4.22.5)
+      '@rollup/plugin-replace':
+        specifier: 5.0.7
+        version: 5.0.7(rollup@4.22.5)
+      '@rollup/pluginutils':
+        specifier: 5.1.2
+        version: 5.1.2(rollup@4.22.5)
+      '@tabler/icons-webfont':
+        specifier: 3.3.0
+        version: 3.3.0
+      '@twemoji/parser':
+        specifier: 15.1.1
+        version: 15.1.1
+      '@vitejs/plugin-vue':
+        specifier: 5.1.4
+        version: 5.1.4(vite@5.4.8(@types/node@20.14.12)(sass@1.79.4)(terser@5.33.0))(vue@3.5.11(typescript@5.6.2))
+      '@vue/compiler-sfc':
+        specifier: 3.5.11
+        version: 3.5.11
+      astring:
+        specifier: 1.9.0
+        version: 1.9.0
+      buraha:
+        specifier: 0.0.1
+        version: 0.0.1
+      estree-walker:
+        specifier: 3.0.3
+        version: 3.0.3
+      frontend-shared:
+        specifier: workspace:*
+        version: link:../frontend-shared
+      json5:
+        specifier: 2.2.3
+        version: 2.2.3
+      mfm-js:
+        specifier: 0.24.0
+        version: 0.24.0
+      misskey-js:
+        specifier: workspace:*
+        version: link:../misskey-js
+      punycode:
+        specifier: 2.3.1
+        version: 2.3.1
+      rollup:
+        specifier: 4.22.5
+        version: 4.22.5
+      sass:
+        specifier: 1.79.4
+        version: 1.79.4
+      shiki:
+        specifier: 1.21.0
+        version: 1.21.0
+      tinycolor2:
+        specifier: 1.6.0
+        version: 1.6.0
+      tsc-alias:
+        specifier: 1.8.10
+        version: 1.8.10
+      tsconfig-paths:
+        specifier: 4.2.0
+        version: 4.2.0
+      typescript:
+        specifier: 5.6.2
+        version: 5.6.2
+      uuid:
+        specifier: 10.0.0
+        version: 10.0.0
+      vite:
+        specifier: 5.4.8
+        version: 5.4.8(@types/node@20.14.12)(sass@1.79.4)(terser@5.33.0)
+      vue:
+        specifier: 3.5.11
+        version: 3.5.11(typescript@5.6.2)
+    devDependencies:
+      '@misskey-dev/summaly':
+        specifier: 5.1.0
+        version: 5.1.0
+      '@testing-library/vue':
+        specifier: 8.1.0
+        version: 8.1.0(@vue/compiler-sfc@3.5.11)(@vue/server-renderer@3.5.11(vue@3.5.11(typescript@5.6.2)))(vue@3.5.11(typescript@5.6.2))
+      '@types/estree':
+        specifier: 1.0.6
+        version: 1.0.6
+      '@types/micromatch':
+        specifier: 4.0.9
+        version: 4.0.9
+      '@types/node':
+        specifier: 20.14.12
+        version: 20.14.12
+      '@types/punycode':
+        specifier: 2.1.4
+        version: 2.1.4
+      '@types/tinycolor2':
+        specifier: 1.4.6
+        version: 1.4.6
+      '@types/uuid':
+        specifier: 10.0.0
+        version: 10.0.0
+      '@types/ws':
+        specifier: 8.5.12
+        version: 8.5.12
+      '@typescript-eslint/eslint-plugin':
+        specifier: 7.17.0
+        version: 7.17.0(@typescript-eslint/parser@7.17.0(eslint@9.11.0)(typescript@5.6.2))(eslint@9.11.0)(typescript@5.6.2)
+      '@typescript-eslint/parser':
+        specifier: 7.17.0
+        version: 7.17.0(eslint@9.11.0)(typescript@5.6.2)
+      '@vitest/coverage-v8':
+        specifier: 1.6.0
+        version: 1.6.0(vitest@1.6.0(@types/node@20.14.12)(happy-dom@10.0.3)(jsdom@24.1.1(bufferutil@4.0.8)(utf-8-validate@6.0.4))(sass@1.79.4)(terser@5.33.0))
+      '@vue/runtime-core':
+        specifier: 3.5.11
+        version: 3.5.11
+      acorn:
+        specifier: 8.12.1
+        version: 8.12.1
+      cross-env:
+        specifier: 7.0.3
+        version: 7.0.3
+      eslint-plugin-import:
+        specifier: 2.31.0
+        version: 2.31.0(@typescript-eslint/parser@7.17.0(eslint@9.11.0)(typescript@5.6.2))(eslint@9.11.0)
+      eslint-plugin-vue:
+        specifier: 9.28.0
+        version: 9.28.0(eslint@9.11.0)
+      fast-glob:
+        specifier: 3.3.2
+        version: 3.3.2
+      happy-dom:
+        specifier: 10.0.3
+        version: 10.0.3
+      intersection-observer:
+        specifier: 0.12.2
+        version: 0.12.2
+      micromatch:
+        specifier: 4.0.8
+        version: 4.0.8
+      msw:
+        specifier: 2.3.4
+        version: 2.3.4(typescript@5.6.2)
+      nodemon:
+        specifier: 3.1.7
+        version: 3.1.7
+      prettier:
+        specifier: 3.3.3
+        version: 3.3.3
+      start-server-and-test:
+        specifier: 2.0.8
+        version: 2.0.8
+      vite-plugin-turbosnap:
+        specifier: 1.0.3
+        version: 1.0.3
+      vue-component-type-helpers:
+        specifier: 2.1.6
+        version: 2.1.6
+      vue-eslint-parser:
+        specifier: 9.4.3
+        version: 9.4.3(eslint@9.11.0)
+      vue-tsc:
+        specifier: 2.1.6
+        version: 2.1.6(typescript@5.6.2)
+
+  packages/frontend-shared:
+    dependencies:
+      misskey-js:
+        specifier: workspace:*
+        version: link:../misskey-js
+      vue:
+        specifier: 3.4.37
+        version: 3.4.37(typescript@5.5.4)
+    devDependencies:
+      '@types/node':
+        specifier: 20.14.12
+        version: 20.14.12
+      '@typescript-eslint/eslint-plugin':
+        specifier: 7.17.0
+        version: 7.17.0(@typescript-eslint/parser@7.17.0(eslint@9.11.0)(typescript@5.5.4))(eslint@9.11.0)(typescript@5.5.4)
+      '@typescript-eslint/parser':
+        specifier: 7.17.0
+        version: 7.17.0(eslint@9.11.0)(typescript@5.5.4)
+      esbuild:
+        specifier: 0.23.0
+        version: 0.23.0
+      eslint-plugin-vue:
+        specifier: 9.27.0
+        version: 9.27.0(eslint@9.11.0)
+      typescript:
+        specifier: 5.5.4
+        version: 5.5.4
+      vue-eslint-parser:
+        specifier: 9.4.3
+        version: 9.4.3(eslint@9.11.0)
 
   packages/misskey-bubble-game:
     dependencies:
@@ -1059,9 +1259,6 @@ importers:
         specifier: 3.0.5
         version: 3.0.5
     devDependencies:
-      '@misskey-dev/eslint-plugin':
-        specifier: 1.0.0
-        version: 1.0.0(@typescript-eslint/eslint-plugin@7.1.0(@typescript-eslint/parser@7.1.0(eslint@8.57.0)(typescript@5.3.3))(eslint@8.57.0)(typescript@5.3.3))(@typescript-eslint/parser@7.1.0(eslint@8.57.0)(typescript@5.3.3))(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.1.0(eslint@8.57.0)(typescript@5.3.3))(eslint@8.57.0))(eslint@8.57.0)
       '@types/matter-js':
         specifier: 0.19.6
         version: 0.19.6
@@ -1073,16 +1270,13 @@ importers:
         version: 3.0.8
       '@typescript-eslint/eslint-plugin':
         specifier: 7.1.0
-        version: 7.1.0(@typescript-eslint/parser@7.1.0(eslint@8.57.0)(typescript@5.3.3))(eslint@8.57.0)(typescript@5.3.3)
+        version: 7.1.0(@typescript-eslint/parser@7.1.0(eslint@9.11.0)(typescript@5.3.3))(eslint@9.11.0)(typescript@5.3.3)
       '@typescript-eslint/parser':
         specifier: 7.1.0
-        version: 7.1.0(eslint@8.57.0)(typescript@5.3.3)
+        version: 7.1.0(eslint@9.11.0)(typescript@5.3.3)
       esbuild:
         specifier: 0.19.11
         version: 0.19.11
-      eslint:
-        specifier: 8.57.0
-        version: 8.57.0
       execa:
         specifier: 8.0.1
         version: 8.0.1
@@ -1098,6 +1292,9 @@ importers:
 
   packages/misskey-js:
     dependencies:
+      '@simplewebauthn/types':
+        specifier: 10.0.0
+        version: 10.0.0
       eventemitter3:
         specifier: 5.0.1
         version: 5.0.1
@@ -1106,41 +1303,35 @@ importers:
         version: 4.4.0
     devDependencies:
       '@microsoft/api-extractor':
-        specifier: 7.43.1
-        version: 7.43.1(@types/node@20.12.7)
-      '@misskey-dev/eslint-plugin':
-        specifier: 1.0.0
-        version: 1.0.0(@typescript-eslint/eslint-plugin@7.7.1(@typescript-eslint/parser@7.7.1(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0)(typescript@5.4.5))(@typescript-eslint/parser@7.7.1(eslint@8.57.0)(typescript@5.4.5))(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.7.1(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0))(eslint@8.57.0)
+        specifier: 7.47.9
+        version: 7.47.9(@types/node@20.14.12)
       '@swc/jest':
         specifier: 0.2.36
-        version: 0.2.36(@swc/core@1.4.17)
+        version: 0.2.36(@swc/core@1.6.13)
       '@types/jest':
-        specifier: 29.5.12
-        version: 29.5.12
+        specifier: 29.5.13
+        version: 29.5.13
       '@types/node':
-        specifier: 20.12.7
-        version: 20.12.7
+        specifier: 20.14.12
+        version: 20.14.12
       '@typescript-eslint/eslint-plugin':
-        specifier: 7.7.1
-        version: 7.7.1(@typescript-eslint/parser@7.7.1(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0)(typescript@5.4.5)
+        specifier: 7.17.0
+        version: 7.17.0(@typescript-eslint/parser@7.17.0(eslint@9.11.0)(typescript@5.6.2))(eslint@9.11.0)(typescript@5.6.2)
       '@typescript-eslint/parser':
-        specifier: 7.7.1
-        version: 7.7.1(eslint@8.57.0)(typescript@5.4.5)
+        specifier: 7.17.0
+        version: 7.17.0(eslint@9.11.0)(typescript@5.6.2)
       esbuild:
-        specifier: 0.19.11
-        version: 0.19.11
-      eslint:
-        specifier: 8.57.0
-        version: 8.57.0
+        specifier: 0.23.1
+        version: 0.23.1
       execa:
-        specifier: 8.0.1
-        version: 8.0.1
+        specifier: 9.4.0
+        version: 9.4.0
       glob:
-        specifier: 10.3.12
-        version: 10.3.12
+        specifier: 11.0.0
+        version: 11.0.0
       jest:
         specifier: 29.7.0
-        version: 29.7.0(@types/node@20.12.7)
+        version: 29.7.0(@types/node@20.14.12)
       jest-fetch-mock:
         specifier: 3.0.3
         version: 3.0.3(encoding@0.1.13)
@@ -1154,35 +1345,29 @@ importers:
         specifier: 2.0.0
         version: 2.0.0
       nodemon:
-        specifier: 3.1.0
-        version: 3.1.0
+        specifier: 3.1.7
+        version: 3.1.7
       tsd:
-        specifier: 0.30.7
-        version: 0.30.7
+        specifier: 0.31.2
+        version: 0.31.2
       typescript:
-        specifier: 5.4.5
-        version: 5.4.5
+        specifier: 5.6.2
+        version: 5.6.2
 
   packages/misskey-js/generator:
     devDependencies:
-      '@misskey-dev/eslint-plugin':
-        specifier: ^1.0.0
-        version: 1.0.0(@typescript-eslint/eslint-plugin@6.11.0(@typescript-eslint/parser@6.11.0(eslint@8.53.0)(typescript@5.3.3))(eslint@8.53.0)(typescript@5.3.3))(@typescript-eslint/parser@6.11.0(eslint@8.53.0)(typescript@5.3.3))(eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.11.0(eslint@8.53.0)(typescript@5.3.3))(eslint@8.53.0))(eslint@8.53.0)
       '@readme/openapi-parser':
-        specifier: 2.5.0
-        version: 2.5.0(openapi-types@12.1.3)
+        specifier: 2.6.0
+        version: 2.6.0(openapi-types@12.1.3)
       '@types/node':
         specifier: 20.9.1
         version: 20.9.1
       '@typescript-eslint/eslint-plugin':
-        specifier: 6.11.0
-        version: 6.11.0(@typescript-eslint/parser@6.11.0(eslint@8.53.0)(typescript@5.3.3))(eslint@8.53.0)(typescript@5.3.3)
+        specifier: 7.17.0
+        version: 7.17.0(@typescript-eslint/parser@7.17.0(eslint@9.11.0)(typescript@5.6.2))(eslint@9.11.0)(typescript@5.6.2)
       '@typescript-eslint/parser':
-        specifier: 6.11.0
-        version: 6.11.0(eslint@8.53.0)(typescript@5.3.3)
-      eslint:
-        specifier: 8.53.0
-        version: 8.53.0
+        specifier: 7.17.0
+        version: 7.17.0(eslint@9.11.0)(typescript@5.6.2)
       openapi-types:
         specifier: 12.1.3
         version: 12.1.3
@@ -1190,14 +1375,14 @@ importers:
         specifier: 6.7.3
         version: 6.7.3
       ts-case-convert:
-        specifier: 2.0.2
-        version: 2.0.2
+        specifier: 2.0.7
+        version: 2.0.7
       tsx:
         specifier: 4.4.0
         version: 4.4.0
       typescript:
-        specifier: 5.3.3
-        version: 5.3.3
+        specifier: 5.6.2
+        version: 5.6.2
 
   packages/misskey-reversi:
     dependencies:
@@ -1205,24 +1390,18 @@ importers:
         specifier: 1.2.2
         version: 1.2.2
     devDependencies:
-      '@misskey-dev/eslint-plugin':
-        specifier: 1.0.0
-        version: 1.0.0(@typescript-eslint/eslint-plugin@7.1.0(@typescript-eslint/parser@7.1.0(eslint@8.57.0)(typescript@5.3.3))(eslint@8.57.0)(typescript@5.3.3))(@typescript-eslint/parser@7.1.0(eslint@8.57.0)(typescript@5.3.3))(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.1.0(eslint@8.57.0)(typescript@5.3.3))(eslint@8.57.0))(eslint@8.57.0)
       '@types/node':
         specifier: 20.11.5
         version: 20.11.5
       '@typescript-eslint/eslint-plugin':
         specifier: 7.1.0
-        version: 7.1.0(@typescript-eslint/parser@7.1.0(eslint@8.57.0)(typescript@5.3.3))(eslint@8.57.0)(typescript@5.3.3)
+        version: 7.1.0(@typescript-eslint/parser@7.1.0(eslint@9.11.0)(typescript@5.3.3))(eslint@9.11.0)(typescript@5.3.3)
       '@typescript-eslint/parser':
         specifier: 7.1.0
-        version: 7.1.0(eslint@8.57.0)(typescript@5.3.3)
+        version: 7.1.0(eslint@9.11.0)(typescript@5.3.3)
       esbuild:
         specifier: 0.19.11
         version: 0.19.11
-      eslint:
-        specifier: 8.57.0
-        version: 8.57.0
       execa:
         specifier: 8.0.1
         version: 8.0.1
@@ -1239,8 +1418,8 @@ importers:
   packages/sw:
     dependencies:
       esbuild:
-        specifier: 0.20.2
-        version: 0.20.2
+        specifier: 0.23.1
+        version: 0.23.1
       idb-keyval:
         specifier: 6.2.1
         version: 6.2.1
@@ -1248,36 +1427,26 @@ importers:
         specifier: workspace:*
         version: link:../misskey-js
     devDependencies:
-      '@misskey-dev/eslint-plugin':
-        specifier: 1.0.0
-        version: 1.0.0(@typescript-eslint/eslint-plugin@7.7.1(@typescript-eslint/parser@7.7.1(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0)(typescript@5.4.5))(@typescript-eslint/parser@7.7.1(eslint@8.57.0)(typescript@5.4.5))(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.7.1(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0))(eslint@8.57.0)
       '@typescript-eslint/parser':
-        specifier: 7.7.1
-        version: 7.7.1(eslint@8.57.0)(typescript@5.4.5)
+        specifier: 7.17.0
+        version: 7.17.0(eslint@9.11.0)(typescript@5.6.2)
       '@typescript/lib-webworker':
         specifier: npm:@types/serviceworker@0.0.67
         version: '@types/serviceworker@0.0.67'
-      eslint:
-        specifier: 8.57.0
-        version: 8.57.0
       eslint-plugin-import:
-        specifier: 2.29.1
-        version: 2.29.1(@typescript-eslint/parser@7.7.1(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0)
+        specifier: 2.30.0
+        version: 2.30.0(@typescript-eslint/parser@7.17.0(eslint@9.11.0)(typescript@5.6.2))(eslint@9.11.0)
       nodemon:
-        specifier: 3.1.0
-        version: 3.1.0
+        specifier: 3.1.7
+        version: 3.1.7
       typescript:
-        specifier: 5.4.5
-        version: 5.4.5
+        specifier: 5.6.2
+        version: 5.6.2
 
 packages:
 
-  '@aashutoshrathi/word-wrap@1.2.6':
-    resolution: {integrity: sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==}
-    engines: {node: '>=0.10.0'}
-
-  '@adobe/css-tools@4.3.3':
-    resolution: {integrity: sha512-rE0Pygv0sEZ4vBWHlAgJLGDU7Pm8xoO6p3wsEceb7GYAjScrOHpEo8KK/eVkAcnSM+slAEtXjA2JpdjLp4fJQQ==}
+  '@adobe/css-tools@4.4.0':
+    resolution: {integrity: sha512-Ff9+ksdQQB3rMncgqDK78uLznstjyfIf2Arnh22pW8kBpLs6rpKDwgnZT46hin5Hl1WzazzK64DOrhSwYpS7bQ==}
 
   '@aiscript-dev/aiscript-languageserver@https://github.com/aiscript-dev/aiscript-languageserver/releases/download/0.1.6/aiscript-dev-aiscript-languageserver-0.1.6.tgz':
     resolution: {tarball: https://github.com/aiscript-dev/aiscript-languageserver/releases/download/0.1.6/aiscript-dev-aiscript-languageserver-0.1.6.tgz}
@@ -1288,360 +1457,332 @@ packages:
     resolution: {integrity: sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==}
     engines: {node: '>=6.0.0'}
 
-  '@apidevtools/openapi-schemas@2.1.0':
-    resolution: {integrity: sha512-Zc1AlqrJlX3SlpupFGpiLi2EbteyP7fXmUOGup6/DnkRgjP9bgMM/ag+n91rsv0U1Gpz0H3VILA/o3bW7Ua6BQ==}
-    engines: {node: '>=10'}
-
   '@apidevtools/swagger-methods@3.0.2':
     resolution: {integrity: sha512-QAkD5kK2b1WfjDS/UQn/qQkbwF31uqRjPTrsCs5ZG9BQGAkjwvqGFjjPqAuzac/IYzpPtRzjCP1WrTuAIjMrXg==}
 
-  '@aw-web-design/x-default-browser@1.4.126':
-    resolution: {integrity: sha512-Xk1sIhyNC/esHGGVjL/niHLowM0csl/kFO5uawBy4IrWwy0o1G8LGt3jP6nmWGz+USxeeqbihAmp/oVZju6wug==}
-    hasBin: true
+  '@aws-crypto/crc32@5.2.0':
+    resolution: {integrity: sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==}
+    engines: {node: '>=16.0.0'}
 
-  '@aws-crypto/crc32@3.0.0':
-    resolution: {integrity: sha512-IzSgsrxUcsrejQbPVilIKy16kAT52EwB6zSaI+M3xxIhKh5+aldEyvI+z6erM7TCLB2BJsFrtHjp6/4/sr+3dA==}
+  '@aws-crypto/crc32c@5.2.0':
+    resolution: {integrity: sha512-+iWb8qaHLYKrNvGRbiYRHSdKRWhto5XlZUEBwDjYNf+ly5SVYG6zEoYIdxvf5R3zyeP16w4PLBn3rH1xc74Rag==}
 
-  '@aws-crypto/crc32c@3.0.0':
-    resolution: {integrity: sha512-ENNPPManmnVJ4BTXlOjAgD7URidbAznURqD0KvfREyc4o20DPYdEldU1f5cQ7Jbj0CJJSPaMIk/9ZshdB3210w==}
+  '@aws-crypto/sha1-browser@5.2.0':
+    resolution: {integrity: sha512-OH6lveCFfcDjX4dbAvCFSYUjJZjDr/3XJ3xHtjn3Oj5b9RjojQo8npoLeA/bNwkOkrSQ0wgrHzXk4tDRxGKJeg==}
 
-  '@aws-crypto/ie11-detection@3.0.0':
-    resolution: {integrity: sha512-341lBBkiY1DfDNKai/wXM3aujNBkXR7tq1URPQDL9wi3AUbI80NR74uF1TXHMm7po1AcnFk8iu2S2IeU/+/A+Q==}
+  '@aws-crypto/sha256-browser@5.2.0':
+    resolution: {integrity: sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==}
 
-  '@aws-crypto/sha1-browser@3.0.0':
-    resolution: {integrity: sha512-NJth5c997GLHs6nOYTzFKTbYdMNA6/1XlKVgnZoaZcQ7z7UJlOgj2JdbHE8tiYLS3fzXNCguct77SPGat2raSw==}
+  '@aws-crypto/sha256-js@5.2.0':
+    resolution: {integrity: sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==}
+    engines: {node: '>=16.0.0'}
 
-  '@aws-crypto/sha256-browser@3.0.0':
-    resolution: {integrity: sha512-8VLmW2B+gjFbU5uMeqtQM6Nj0/F1bro80xQXCW6CQBWgosFWXTx77aeOF5CAIAmbOK64SdMBJdNr6J41yP5mvQ==}
+  '@aws-crypto/supports-web-crypto@5.2.0':
+    resolution: {integrity: sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==}
 
-  '@aws-crypto/sha256-js@3.0.0':
-    resolution: {integrity: sha512-PnNN7os0+yd1XvXAy23CFOmTbMaDxgxXtTKHybrJ39Y8kGzBATgBFibWJKH6BhytLI/Zyszs87xCOBNyBig6vQ==}
+  '@aws-crypto/util@5.2.0':
+    resolution: {integrity: sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==}
 
-  '@aws-crypto/supports-web-crypto@3.0.0':
-    resolution: {integrity: sha512-06hBdMwUAb2WFTuGG73LSC0wfPu93xWwo5vL2et9eymgmu3Id5vFAHBbajVWiGhPO37qcsdCap/FqXvJGJWPIg==}
+  '@aws-sdk/client-s3@3.620.0':
+    resolution: {integrity: sha512-kf3Lqvuq/ciUn4myQjd1a9nhVg95+FEWkIq7pdkgxFoKow8HKj3nuiwI7zYBRTfk0RKXRkJca3GE+3RXpeZSiA==}
+    engines: {node: '>=16.0.0'}
 
-  '@aws-crypto/util@3.0.0':
-    resolution: {integrity: sha512-2OJlpeJpCR48CC8r+uKVChzs9Iungj9wkZrl8Z041DWEWvyIHILYKCPNzJghKsivj+S3mLo6BVc7mBNzdxA46w==}
-
-  '@aws-sdk/client-s3@3.412.0':
-    resolution: {integrity: sha512-sNrlx9sSBmFUCqMgTznwk9Fee3PJat0nZ3RIDR5Crhsld/eexxrqb6TYKsxzFfBfXTL/oPh+/S5driRV2xsB8A==}
-    engines: {node: '>=14.0.0'}
-
-  '@aws-sdk/client-sso@3.410.0':
-    resolution: {integrity: sha512-MC9GrgwtlOuSL2WS3DRM3dQ/5y+49KSMMJRH6JiEcU5vE0dX/OtEcX+VfEwpi73x5pSfIjm7xnzjzOFx+sQBIg==}
-    engines: {node: '>=14.0.0'}
-
-  '@aws-sdk/client-sts@3.410.0':
-    resolution: {integrity: sha512-e6VMrBJtnTxxUXwDmkADGIvyppmDMFf4+cGGA68tVCUm1cFNlCI6M/67bVSIPN/WVKAAfhEL5O2vVXCM7aatYg==}
-    engines: {node: '>=14.0.0'}
-
-  '@aws-sdk/credential-provider-env@3.410.0':
-    resolution: {integrity: sha512-c7TB9LbN0PkFOsXI0lcRJnqPNOmc4VBvrHf8jP/BkTDg4YUoKQKOFd4d0SqzODmlZiAyoMQVZTR4ISZo95Zj4Q==}
-    engines: {node: '>=14.0.0'}
-
-  '@aws-sdk/credential-provider-ini@3.410.0':
-    resolution: {integrity: sha512-D8rcr5bRCFD0f42MPQ7K6TWZq5d3pfqrKINL1/bpfkK5BJbvq1BGYmR88UC6CLpTRtZ1LHY2HgYG0fp/2zjjww==}
-    engines: {node: '>=14.0.0'}
-
-  '@aws-sdk/credential-provider-node@3.410.0':
-    resolution: {integrity: sha512-0wmVm33T/j1FS7MZ/j+WsPlgSc0YnCXnpbWSov1Mn6R86SHI2b2JhdIPRRE4XbGfyW2QGNUl2CwoZVaqhXeF5g==}
-    engines: {node: '>=14.0.0'}
-
-  '@aws-sdk/credential-provider-process@3.410.0':
-    resolution: {integrity: sha512-BMju1hlDCDNkkSZpKF5SQ8G0WCLRj6/Jvw9QmudLHJuVwYJXEW1r2AsVMg98OZ3hB9G+MAvHruHZIbMiNmUMXQ==}
-    engines: {node: '>=14.0.0'}
-
-  '@aws-sdk/credential-provider-sso@3.410.0':
-    resolution: {integrity: sha512-zEaoY/sY+KYTlQUkp9dvveAHf175b8RIt0DsQkDrRPtrg/RBHR00r5rFvz9+nrwsR8546RaBU7h/zzTaQGhmcA==}
-    engines: {node: '>=14.0.0'}
-
-  '@aws-sdk/credential-provider-web-identity@3.410.0':
-    resolution: {integrity: sha512-cE0l8LmEHdWbDkdPNgrfdYSgp4/cIVXrjUKI1QCATA729CrHZ/OQjB/maOBOrMHO9YTiggko887NkslVvwVB7w==}
-    engines: {node: '>=14.0.0'}
-
-  '@aws-sdk/lib-storage@3.412.0':
-    resolution: {integrity: sha512-uAdVtNuip06rJOs28zVrYXLNeHfKraxvJRTzTA+DW1dXkzh70GTKqDKHWH9IJkW/xMTE6wGSM+fDs8jsMOn/yA==}
-    engines: {node: '>=14.0.0'}
+  '@aws-sdk/client-sso-oidc@3.620.0':
+    resolution: {integrity: sha512-CWL8aJa6rrNaQXNsLhblGZzbFBrRz4BXAsFBbyqAZEmryr9q/IC7z/ww3nq8CD2UsW+bn89U/XcoP5r1KWUHuQ==}
+    engines: {node: '>=16.0.0'}
     peerDependencies:
-      '@aws-sdk/client-s3': ^3.0.0
+      '@aws-sdk/client-sts': ^3.620.0
 
-  '@aws-sdk/middleware-bucket-endpoint@3.410.0':
-    resolution: {integrity: sha512-pUGrpFgCKf9fDHu01JJhhw+MUImheS0HFlZwNG37OMubkxUAbCdmYGewGxfTCUvWyZJtx9bVjrSu6gG7w+RARg==}
-    engines: {node: '>=14.0.0'}
+  '@aws-sdk/client-sso@3.620.0':
+    resolution: {integrity: sha512-J1CvF7u39XwtCK9rPlkW2AA631EPqkb4PjOOj9aZ9LjQmkJ0DkL+9tEqU2XIWcjDd2Z3hS3LBuS8uN7upIkEnQ==}
+    engines: {node: '>=16.0.0'}
 
-  '@aws-sdk/middleware-expect-continue@3.410.0':
-    resolution: {integrity: sha512-e5YqGCNmW99GZjEPPujJ02RlEZql19U40oORysBhVF7mKz8BBvF3s8l37tvu37oxebDEkh1u/2cm2+ggOXxLjQ==}
-    engines: {node: '>=14.0.0'}
+  '@aws-sdk/client-sts@3.620.0':
+    resolution: {integrity: sha512-pG4SqDHZV/ZbpoVoVtpxo6ZZoqVDbVItC3QUO73UJ3Gemxznd/Ck7kAsyb6/dJkV/Aqm3gt2O5UL7vzQLNHSjw==}
+    engines: {node: '>=16.0.0'}
 
-  '@aws-sdk/middleware-flexible-checksums@3.410.0':
-    resolution: {integrity: sha512-IK7KlvEKtrQVBfmAp/MmGd0wbWLuN2GZwwfAmsU0qFb0f5vOVUbKDsu6tudtDKCBG9uXyTEsx3/QGvoK2zDy+g==}
-    engines: {node: '>=14.0.0'}
+  '@aws-sdk/core@3.620.0':
+    resolution: {integrity: sha512-5D9tMahxIDDFLULS9/ULa0HuIu7CZSshfj6wmDSmigXzkWyUvHoVIrme2z6eM3Icat/MO3d4WEy3445Vk385gQ==}
+    engines: {node: '>=16.0.0'}
 
-  '@aws-sdk/middleware-host-header@3.410.0':
-    resolution: {integrity: sha512-ED/OVcyITln5rrxnajZP+V0PN1nug+gSDHJDqdDo/oLy7eiDr/ZWn3nlWW7WcMplQ1/Jnb+hK0UetBp/25XooA==}
-    engines: {node: '>=14.0.0'}
+  '@aws-sdk/credential-provider-env@3.609.0':
+    resolution: {integrity: sha512-v69ZCWcec2iuV9vLVJMa6fAb5xwkzN4jYIT8yjo2c4Ia/j976Q+TPf35Pnz5My48Xr94EFcaBazrWedF+kwfuQ==}
+    engines: {node: '>=16.0.0'}
 
-  '@aws-sdk/middleware-location-constraint@3.410.0':
-    resolution: {integrity: sha512-jAftSpOpw/5AdpOJ/cGiXCb+Vv22KXR5QZmxmllUDsnlm18672tpRaI2plmu/1d98CVvqhY61eSklFMrIf2c4w==}
-    engines: {node: '>=14.0.0'}
+  '@aws-sdk/credential-provider-http@3.620.0':
+    resolution: {integrity: sha512-BI2BdrSKDmB/2ouB/NJR0PT0x/+5fmoF6XOE78hFBb4F5w/yynGgcJY936dF+oREfpME6ehjB2b0okGg78Scpw==}
+    engines: {node: '>=16.0.0'}
 
-  '@aws-sdk/middleware-logger@3.410.0':
-    resolution: {integrity: sha512-YtmKYCVtBfScq3/UFJk+aSZOktKJBNZL9DaSc2aPcy/goCVsYDOkGwtHk0jIkC1JRSNCkVTqL7ya60sSr8zaQQ==}
-    engines: {node: '>=14.0.0'}
+  '@aws-sdk/credential-provider-ini@3.620.0':
+    resolution: {integrity: sha512-P9fYi6dzZIl8ITC7qAPf5DX9omI3LfA91g3KH+0OUmS3ctP7tN+gNo3HmqlzoqnwPe0pXn1FumYAe1qFl6Yjjg==}
+    engines: {node: '>=16.0.0'}
+    peerDependencies:
+      '@aws-sdk/client-sts': ^3.620.0
 
-  '@aws-sdk/middleware-recursion-detection@3.410.0':
-    resolution: {integrity: sha512-KWaes5FLzRqj28vaIEE4Bimpga2E596WdPF2HaH6zsVMJddoRDsc3ZX9ZhLOGrXzIO1RqBd0QxbLrM0S/B2aOQ==}
-    engines: {node: '>=14.0.0'}
+  '@aws-sdk/credential-provider-node@3.620.0':
+    resolution: {integrity: sha512-or8ahy4ysURcWgKX00367DMDTTyMynDEl+FQh4wce66fMyePhFVuoPcRgXzWsi8KYmL95sPCfJFNqBMyFNcgvQ==}
+    engines: {node: '>=16.0.0'}
 
-  '@aws-sdk/middleware-sdk-s3@3.410.0':
-    resolution: {integrity: sha512-K2sG2V1ZkezYMCIy3uMt0MwtflcfIwLptwm0iFLaYitiINZQ1tcslk9ggAjyTHg0rslDSI4/zjkhy8VHFOV7HA==}
-    engines: {node: '>=14.0.0'}
+  '@aws-sdk/credential-provider-process@3.614.0':
+    resolution: {integrity: sha512-Q0SI0sTRwi8iNODLs5+bbv8vgz8Qy2QdxbCHnPk/6Cx6LMf7i3dqmWquFbspqFRd8QiqxStrblwxrUYZi09tkA==}
+    engines: {node: '>=16.0.0'}
 
-  '@aws-sdk/middleware-sdk-sts@3.410.0':
-    resolution: {integrity: sha512-YfBpctDocRR4CcROoDueJA7D+aMLBV8nTFfmVNdLLLgyuLZ/AUR11VQSu1lf9gQZKl8IpKE/BLf2fRE/qV1ZuA==}
-    engines: {node: '>=14.0.0'}
+  '@aws-sdk/credential-provider-sso@3.620.0':
+    resolution: {integrity: sha512-xtIj2hmq3jcKwvGmqhoYapbWeQfFyoQgKBtwD6nx0M6oS5lbFH4rzHhj0gBwatZDjMa35cWtcYVUJCv2/9mWvA==}
+    engines: {node: '>=16.0.0'}
 
-  '@aws-sdk/middleware-signing@3.410.0':
-    resolution: {integrity: sha512-KBAZ/eoAJUSJv5us2HsKwK2OszG2s9FEyKpEhgnHLcbbKzW873zHBH5GcOGEQu4AWArTy2ndzJu3FF+9/J9hJQ==}
-    engines: {node: '>=14.0.0'}
+  '@aws-sdk/credential-provider-web-identity@3.609.0':
+    resolution: {integrity: sha512-U+PG8NhlYYF45zbr1km3ROtBMYqyyj/oK8NRp++UHHeuavgrP+4wJ4wQnlEaKvJBjevfo3+dlIBcaeQ7NYejWg==}
+    engines: {node: '>=16.0.0'}
+    peerDependencies:
+      '@aws-sdk/client-sts': ^3.609.0
 
-  '@aws-sdk/middleware-ssec@3.410.0':
-    resolution: {integrity: sha512-DNsjVTXoxIh+PuW9o45CFaMiconbuZRm19MC3NA1yNCaCj3ZxD5OdXAutq6UjQdrx8UG4EjUlCJEEvBKmboITw==}
-    engines: {node: '>=14.0.0'}
+  '@aws-sdk/lib-storage@3.620.0':
+    resolution: {integrity: sha512-xUeKH8RrPQqE6J49Yun0qCdu5SGaN5LgyyjWutLeElqR0G3jhmPVrRzFXsHDXbr9S0QVE4V8DjeC17bkEdG4dw==}
+    engines: {node: '>=16.0.0'}
+    peerDependencies:
+      '@aws-sdk/client-s3': ^3.620.0
 
-  '@aws-sdk/middleware-user-agent@3.410.0':
-    resolution: {integrity: sha512-ZayDtLfvCZUohSxQc/49BfoU/y6bDHLfLdyyUJbJ54Sv8zQcrmdyKvCBFUZwE6tHQgAmv9/ZT18xECMl+xiONA==}
-    engines: {node: '>=14.0.0'}
+  '@aws-sdk/middleware-bucket-endpoint@3.620.0':
+    resolution: {integrity: sha512-eGLL0W6L3HDb3OACyetZYOWpHJ+gLo0TehQKeQyy2G8vTYXqNTeqYhuI6up9HVjBzU9eQiULVQETmgQs7TFaRg==}
+    engines: {node: '>=16.0.0'}
 
-  '@aws-sdk/signature-v4-multi-region@3.412.0':
-    resolution: {integrity: sha512-ijxOeYpNDuk2T940S9HYcZ1C+wTP9vqp1Cw37zw9whVY2mKV3Vr7i+44D4FQ5HhWULgdwhjD7IctbNxPIPzUZQ==}
-    engines: {node: '>=14.0.0'}
+  '@aws-sdk/middleware-expect-continue@3.620.0':
+    resolution: {integrity: sha512-QXeRFMLfyQ31nAHLbiTLtk0oHzG9QLMaof5jIfqcUwnOkO8YnQdeqzakrg1Alpy/VQ7aqzIi8qypkBe2KXZz0A==}
+    engines: {node: '>=16.0.0'}
 
-  '@aws-sdk/token-providers@3.410.0':
-    resolution: {integrity: sha512-d5Nc0xydkH/X0LA1HDyhGY5sEv4LuADFk+QpDtT8ogLilcre+b1jpdY8Sih/gd1KoGS1H+d1tz2hSGwUHAbUbw==}
-    engines: {node: '>=14.0.0'}
+  '@aws-sdk/middleware-flexible-checksums@3.620.0':
+    resolution: {integrity: sha512-ftz+NW7qka2sVuwnnO1IzBku5ccP+s5qZGeRTPgrKB7OzRW85gthvIo1vQR2w+OwHFk7WJbbhhWwbCbktnP4UA==}
+    engines: {node: '>=16.0.0'}
 
-  '@aws-sdk/types@3.410.0':
-    resolution: {integrity: sha512-D7iaUCszv/v04NDaZUmCmekamy6VD/lKozm/3gS9+dkfU6cC2CsNoUfPV8BlV6dPdw0oWgF91am3I1stdvfVrQ==}
-    engines: {node: '>=14.0.0'}
+  '@aws-sdk/middleware-host-header@3.620.0':
+    resolution: {integrity: sha512-VMtPEZwqYrII/oUkffYsNWY9PZ9xpNJpMgmyU0rlDQ25O1c0Hk3fJmZRe6pEkAJ0omD7kLrqGl1DUjQVxpd/Rg==}
+    engines: {node: '>=16.0.0'}
 
-  '@aws-sdk/types@3.413.0':
-    resolution: {integrity: sha512-j1xib0f/TazIFc5ySIKOlT1ujntRbaoG4LJFeEezz4ji03/wSJMI8Vi4KjzpBp8J1tTu0oRDnsxRIGixsUBeYQ==}
-    engines: {node: '>=14.0.0'}
+  '@aws-sdk/middleware-location-constraint@3.609.0':
+    resolution: {integrity: sha512-xzsdoTkszGVqGVPjUmgoP7TORiByLueMHieI1fhQL888WPdqctwAx3ES6d/bA9Q/i8jnc6hs+Fjhy8UvBTkE9A==}
+    engines: {node: '>=16.0.0'}
 
-  '@aws-sdk/util-arn-parser@3.310.0':
-    resolution: {integrity: sha512-jL8509owp/xB9+Or0pvn3Fe+b94qfklc2yPowZZIFAkFcCSIdkIglz18cPDWnYAcy9JGewpMS1COXKIUhZkJsA==}
-    engines: {node: '>=14.0.0'}
+  '@aws-sdk/middleware-logger@3.609.0':
+    resolution: {integrity: sha512-S62U2dy4jMDhDFDK5gZ4VxFdWzCtLzwbYyFZx2uvPYTECkepLUfzLic2BHg2Qvtu4QjX+oGE3P/7fwaGIsGNuQ==}
+    engines: {node: '>=16.0.0'}
 
-  '@aws-sdk/util-endpoints@3.410.0':
-    resolution: {integrity: sha512-iNiqJyC7N3+8zFwnXUqcWSxrZecVZLToo1iTQQdeYL2af1IcOtRgb7n8jpAI/hmXhBSx2+3RI+Y7pxyFo1vu+w==}
-    engines: {node: '>=14.0.0'}
+  '@aws-sdk/middleware-recursion-detection@3.620.0':
+    resolution: {integrity: sha512-nh91S7aGK3e/o1ck64sA/CyoFw+gAYj2BDOnoNa6ouyCrVJED96ZXWbhye/fz9SgmNUZR2g7GdVpiLpMKZoI5w==}
+    engines: {node: '>=16.0.0'}
+
+  '@aws-sdk/middleware-sdk-s3@3.620.0':
+    resolution: {integrity: sha512-AAZ6NLVOx/bP97PYj/afCMeySzxOHocgJG3ZXh6f8MnJcGpZgx8NyRm0vtiYUTFrS2JtU4xV05Dl3j4afV3s4A==}
+    engines: {node: '>=16.0.0'}
+
+  '@aws-sdk/middleware-signing@3.620.0':
+    resolution: {integrity: sha512-gxI7rubiaanUXaLfJ4NybERa9MGPNg2Ycl/OqANsozrBnR3Pw8vqy3EuVImQOyn2pJ2IFvl8ZPoSMHf4pX56FQ==}
+    engines: {node: '>=16.0.0'}
+
+  '@aws-sdk/middleware-ssec@3.609.0':
+    resolution: {integrity: sha512-GZSD1s7+JswWOTamVap79QiDaIV7byJFssBW68GYjyRS5EBjNfwA/8s+6uE6g39R3ojyTbYOmvcANoZEhSULXg==}
+    engines: {node: '>=16.0.0'}
+
+  '@aws-sdk/middleware-user-agent@3.620.0':
+    resolution: {integrity: sha512-bvS6etn+KsuL32ubY5D3xNof1qkenpbJXf/ugGXbg0n98DvDFQ/F+SMLxHgbnER5dsKYchNnhmtI6/FC3HFu/A==}
+    engines: {node: '>=16.0.0'}
+
+  '@aws-sdk/region-config-resolver@3.614.0':
+    resolution: {integrity: sha512-vDCeMXvic/LU0KFIUjpC3RiSTIkkvESsEfbVHiHH0YINfl8HnEqR5rj+L8+phsCeVg2+LmYwYxd5NRz4PHxt5g==}
+    engines: {node: '>=16.0.0'}
+
+  '@aws-sdk/signature-v4-multi-region@3.620.0':
+    resolution: {integrity: sha512-yu1pTCqIbkSdaOvmyfW9vV9jWe3pDApkQPZLg4VEN5dXDWRtgQ/amv88myyCEoG14irUN1tsbvytcKzGyEXnhA==}
+    engines: {node: '>=16.0.0'}
+
+  '@aws-sdk/token-providers@3.614.0':
+    resolution: {integrity: sha512-okItqyY6L9IHdxqs+Z116y5/nda7rHxLvROxtAJdLavWTYDydxrZstImNgGWTeVdmc0xX2gJCI77UYUTQWnhRw==}
+    engines: {node: '>=16.0.0'}
+    peerDependencies:
+      '@aws-sdk/client-sso-oidc': ^3.614.0
+
+  '@aws-sdk/types@3.609.0':
+    resolution: {integrity: sha512-+Tqnh9w0h2LcrUsdXyT1F8mNhXz+tVYBtP19LpeEGntmvHwa2XzvLUCWpoIAIVsHp5+HdB2X9Sn0KAtmbFXc2Q==}
+    engines: {node: '>=16.0.0'}
+
+  '@aws-sdk/util-arn-parser@3.568.0':
+    resolution: {integrity: sha512-XUKJWWo+KOB7fbnPP0+g/o5Ulku/X53t7i/h+sPHr5xxYTJJ9CYnbToo95mzxe7xWvkLrsNtJ8L+MnNn9INs2w==}
+    engines: {node: '>=16.0.0'}
+
+  '@aws-sdk/util-endpoints@3.614.0':
+    resolution: {integrity: sha512-wK2cdrXHH4oz4IomV/yrGkftU9A+ITB6nFL+rxxyO78is2ifHJpFdV4aqk4LSkXYPi6CXWNru/Dqc7yiKXgJPw==}
+    engines: {node: '>=16.0.0'}
 
   '@aws-sdk/util-locate-window@3.208.0':
     resolution: {integrity: sha512-iua1A2+P7JJEDHVgvXrRJSvsnzG7stYSGQnBVphIUlemwl6nN5D+QrgbjECtrbxRz8asYFHSzhdhECqN+tFiBg==}
     engines: {node: '>=14.0.0'}
 
-  '@aws-sdk/util-user-agent-browser@3.410.0':
-    resolution: {integrity: sha512-i1G/XGpXGMRT2zEiAhi1xucJsfCWk8nNYjk/LbC0sA+7B9Huri96YAzVib12wkHPsJQvZxZC6CpQDIHWm4lXMA==}
+  '@aws-sdk/util-user-agent-browser@3.609.0':
+    resolution: {integrity: sha512-fojPU+mNahzQ0YHYBsx0ZIhmMA96H+ZIZ665ObU9tl+SGdbLneVZVikGve+NmHTQwHzwkFsZYYnVKAkreJLAtA==}
 
-  '@aws-sdk/util-user-agent-node@3.410.0':
-    resolution: {integrity: sha512-bK70t1jHRl8HrJXd4hEIwc5PBZ7U0w+81AKFnanIVKZwZedd6nLibUXDTK14z/Jp2GFcBqd4zkt2YLGkRt/U4A==}
-    engines: {node: '>=14.0.0'}
+  '@aws-sdk/util-user-agent-node@3.614.0':
+    resolution: {integrity: sha512-15ElZT88peoHnq5TEoEtZwoXTXRxNrk60TZNdpl/TUBJ5oNJ9Dqb5Z4ryb8ofN6nm9aFf59GVAerFDz8iUoHBA==}
+    engines: {node: '>=16.0.0'}
     peerDependencies:
       aws-crt: '>=1.0.0'
     peerDependenciesMeta:
       aws-crt:
         optional: true
 
-  '@aws-sdk/util-utf8-browser@3.259.0':
-    resolution: {integrity: sha512-UvFa/vR+e19XookZF8RzFZBrw2EUkQWxiBW0yYQAhvk3C+QVGl0H3ouca8LDBlBfQKXwmW3huo/59H8rwb1wJw==}
-
-  '@aws-sdk/xml-builder@3.310.0':
-    resolution: {integrity: sha512-TqELu4mOuSIKQCqj63fGVs86Yh+vBx5nHRpWKNUNhB2nPTpfbziTs5c1X358be3peVWA4wPxW7Nt53KIg1tnNw==}
-    engines: {node: '>=14.0.0'}
+  '@aws-sdk/xml-builder@3.609.0':
+    resolution: {integrity: sha512-l9XxNcA4HX98rwCC2/KoiWcmEiRfZe4G+mYwDbCFT87JIMj6GBhLDkAzr/W8KAaA2IDr8Vc6J8fZPgVulxxfMA==}
+    engines: {node: '>=16.0.0'}
 
   '@babel/code-frame@7.23.5':
     resolution: {integrity: sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==}
     engines: {node: '>=6.9.0'}
 
+  '@babel/code-frame@7.24.7':
+    resolution: {integrity: sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==}
+    engines: {node: '>=6.9.0'}
+
   '@babel/compat-data@7.23.5':
     resolution: {integrity: sha512-uU27kfDRlhfKl+w1U6vp16IuvSLtjAxdArVXPa9BvLkrr7CYIsxH5adpHObeAGY/41+syctUWOZ140a2Rvkgjw==}
     engines: {node: '>=6.9.0'}
 
+  '@babel/compat-data@7.24.7':
+    resolution: {integrity: sha512-qJzAIcv03PyaWqxRgO4mSU3lihncDT296vnyuE2O8uA4w3UHWI4S3hgeZd1L8W1Bft40w9JxJ2b412iDUFFRhw==}
+    engines: {node: '>=6.9.0'}
+
   '@babel/core@7.23.5':
     resolution: {integrity: sha512-Cwc2XjUrG4ilcfOw4wBAK+enbdgwAcAJCfGUItPBKR7Mjw4aEfAFYrLxeRp4jWgtNIKn3n2AlBOfwwafl+42/g==}
     engines: {node: '>=6.9.0'}
 
-  '@babel/core@7.24.0':
-    resolution: {integrity: sha512-fQfkg0Gjkza3nf0c7/w6Xf34BW4YvzNfACRLmmb7XRLa6XHdR+K9AlJlxneFfWYf6uhOzuzZVTjF/8KfndZANw==}
+  '@babel/core@7.24.7':
+    resolution: {integrity: sha512-nykK+LEK86ahTkX/3TgauT0ikKoNCfKHEaZYTUVupJdTLzGNvrblu4u6fa7DhZONAltdf8e662t/abY8idrd/g==}
     engines: {node: '>=6.9.0'}
 
   '@babel/generator@7.23.5':
     resolution: {integrity: sha512-BPssCHrBD+0YrxviOa3QzpqwhNIXKEtOa2jQrm4FlmkC2apYgRnQcmPWiGZDlGxiNtltnUFolMe8497Esry+jA==}
     engines: {node: '>=6.9.0'}
 
-  '@babel/generator@7.23.6':
-    resolution: {integrity: sha512-qrSfCYxYQB5owCmGLbl8XRpX1ytXlpueOb0N0UmQwA073KZxejgQTzAmJezxvpwQD9uGtK2shHdi55QT+MbjIw==}
-    engines: {node: '>=6.9.0'}
-
-  '@babel/helper-annotate-as-pure@7.22.5':
-    resolution: {integrity: sha512-LvBTxu8bQSQkcyKOU+a1btnNFQ1dMAd0R6PyW3arXes06F6QLWLIrd681bxRPIXlrMGR3XYnW9JyML7dP3qgxg==}
-    engines: {node: '>=6.9.0'}
-
-  '@babel/helper-builder-binary-assignment-operator-visitor@7.22.15':
-    resolution: {integrity: sha512-QkBXwGgaoC2GtGZRoma6kv7Szfv06khvhFav67ZExau2RaXzy8MpHSMO2PNoP2XtmQphJQRHFfg77Bq731Yizw==}
+  '@babel/generator@7.24.7':
+    resolution: {integrity: sha512-oipXieGC3i45Y1A41t4tAqpnEZWgB/lC6Ehh6+rOviR5XWpTtMmLN+fGjz9vOiNRt0p6RtO6DtD0pdU3vpqdSA==}
     engines: {node: '>=6.9.0'}
 
   '@babel/helper-compilation-targets@7.22.15':
     resolution: {integrity: sha512-y6EEzULok0Qvz8yyLkCvVX+02ic+By2UdOhylwUOvOn9dvYc9mKICJuuU1n1XBI02YWsNsnrY1kc6DVbjcXbtw==}
     engines: {node: '>=6.9.0'}
 
-  '@babel/helper-compilation-targets@7.23.6':
-    resolution: {integrity: sha512-9JB548GZoQVmzrFgp8o7KxdgkTGm6xs9DW0o/Pim72UDjzr5ObUQ6ZzYPqA+g9OTS2bBQoctLJrky0RDCAWRgQ==}
+  '@babel/helper-compilation-targets@7.24.7':
+    resolution: {integrity: sha512-ctSdRHBi20qWOfy27RUb4Fhp07KSJ3sXcuSvTrXrc4aG8NSYDo1ici3Vhg9bg69y5bj0Mr1lh0aeEgTvc12rMg==}
     engines: {node: '>=6.9.0'}
 
-  '@babel/helper-create-class-features-plugin@7.23.5':
-    resolution: {integrity: sha512-QELlRWxSpgdwdJzSJn4WAhKC+hvw/AtHbbrIoncKHkhKKR/luAlKkgBDcri1EzWAo8f8VvYVryEHN4tax/V67A==}
-    engines: {node: '>=6.9.0'}
-    peerDependencies:
-      '@babel/core': ^7.0.0
-
-  '@babel/helper-create-regexp-features-plugin@7.22.15':
-    resolution: {integrity: sha512-29FkPLFjn4TPEa3RE7GpW+qbE8tlsu3jntNYNfcGsc49LphF1PQIiD+vMZ1z1xVOKt+93khA9tc2JBs3kBjA7w==}
-    engines: {node: '>=6.9.0'}
-    peerDependencies:
-      '@babel/core': ^7.0.0
-
-  '@babel/helper-define-polyfill-provider@0.4.3':
-    resolution: {integrity: sha512-WBrLmuPP47n7PNwsZ57pqam6G/RGo1vw/87b0Blc53tZNGZ4x7YvZ6HgQe2vo1W/FR20OgjeZuGXzudPiXHFug==}
-    peerDependencies:
-      '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0
-
   '@babel/helper-environment-visitor@7.22.20':
     resolution: {integrity: sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==}
     engines: {node: '>=6.9.0'}
 
+  '@babel/helper-environment-visitor@7.24.7':
+    resolution: {integrity: sha512-DoiN84+4Gnd0ncbBOM9AZENV4a5ZiL39HYMyZJGZ/AZEykHYdJw0wW3kdcsh9/Kn+BRXHLkkklZ51ecPKmI1CQ==}
+    engines: {node: '>=6.9.0'}
+
   '@babel/helper-function-name@7.23.0':
     resolution: {integrity: sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==}
     engines: {node: '>=6.9.0'}
 
+  '@babel/helper-function-name@7.24.7':
+    resolution: {integrity: sha512-FyoJTsj/PEUWu1/TYRiXTIHc8lbw+TDYkZuoE43opPS5TrI7MyONBE1oNvfguEXAD9yhQRrVBnXdXzSLQl9XnA==}
+    engines: {node: '>=6.9.0'}
+
   '@babel/helper-hoist-variables@7.22.5':
     resolution: {integrity: sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==}
     engines: {node: '>=6.9.0'}
 
-  '@babel/helper-member-expression-to-functions@7.23.0':
-    resolution: {integrity: sha512-6gfrPwh7OuT6gZyJZvd6WbTfrqAo7vm4xCzAXOusKqq/vWdKXphTpj5klHKNmRUU6/QRGlBsyU9mAIPaWHlqJA==}
+  '@babel/helper-hoist-variables@7.24.7':
+    resolution: {integrity: sha512-MJJwhkoGy5c4ehfoRyrJ/owKeMl19U54h27YYftT0o2teQ3FJ3nQUf/I3LlJsX4l3qlw7WRXUmiyajvHXoTubQ==}
     engines: {node: '>=6.9.0'}
 
   '@babel/helper-module-imports@7.22.15':
     resolution: {integrity: sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w==}
     engines: {node: '>=6.9.0'}
 
+  '@babel/helper-module-imports@7.24.7':
+    resolution: {integrity: sha512-8AyH3C+74cgCVVXow/myrynrAGv+nTVg5vKu2nZph9x7RcRwzmh0VFallJuFTZ9mx6u4eSdXZfcOzSqTUm0HCA==}
+    engines: {node: '>=6.9.0'}
+
   '@babel/helper-module-transforms@7.23.3':
     resolution: {integrity: sha512-7bBs4ED9OmswdfDzpz4MpWgSrV7FXlc3zIagvLFjS5H+Mk7Snr21vQ6QwrsoCGMfNC4e4LQPdoULEt4ykz0SRQ==}
     engines: {node: '>=6.9.0'}
     peerDependencies:
       '@babel/core': ^7.0.0
 
-  '@babel/helper-optimise-call-expression@7.22.5':
-    resolution: {integrity: sha512-HBwaojN0xFRx4yIvpwGqxiV2tUfl7401jlok564NgB9EHS1y6QT17FmKWm4ztqjeVdXLuC4fSvHc5ePpQjoTbw==}
+  '@babel/helper-module-transforms@7.24.7':
+    resolution: {integrity: sha512-1fuJEwIrp+97rM4RWdO+qrRsZlAeL1lQJoPqtCYWv0NL115XM93hIH4CSRln2w52SqvmY5hqdtauB6QFCDiZNQ==}
     engines: {node: '>=6.9.0'}
+    peerDependencies:
+      '@babel/core': ^7.0.0
 
   '@babel/helper-plugin-utils@7.22.5':
     resolution: {integrity: sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg==}
     engines: {node: '>=6.9.0'}
 
-  '@babel/helper-remap-async-to-generator@7.22.20':
-    resolution: {integrity: sha512-pBGyV4uBqOns+0UvhsTO8qgl8hO89PmiDYv+/COyp1aeMcmfrfruz+/nCMFiYyFF/Knn0yfrC85ZzNFjembFTw==}
-    engines: {node: '>=6.9.0'}
-    peerDependencies:
-      '@babel/core': ^7.0.0
-
-  '@babel/helper-replace-supers@7.22.20':
-    resolution: {integrity: sha512-qsW0In3dbwQUbK8kejJ4R7IHVGwHJlV6lpG6UA7a9hSa2YEiAib+N1T2kr6PEeUT+Fl7najmSOS6SmAwCHK6Tw==}
-    engines: {node: '>=6.9.0'}
-    peerDependencies:
-      '@babel/core': ^7.0.0
-
   '@babel/helper-simple-access@7.22.5':
     resolution: {integrity: sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==}
     engines: {node: '>=6.9.0'}
 
-  '@babel/helper-skip-transparent-expression-wrappers@7.22.5':
-    resolution: {integrity: sha512-tK14r66JZKiC43p8Ki33yLBVJKlQDFoA8GYN67lWCDCqoL6EMMSuM9b+Iff2jHaM/RRFYl7K+iiru7hbRqNx8Q==}
+  '@babel/helper-simple-access@7.24.7':
+    resolution: {integrity: sha512-zBAIvbCMh5Ts+b86r/CjU+4XGYIs+R1j951gxI3KmmxBMhCg4oQMsv6ZXQ64XOm/cvzfU1FmoCyt6+owc5QMYg==}
     engines: {node: '>=6.9.0'}
 
   '@babel/helper-split-export-declaration@7.22.6':
     resolution: {integrity: sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==}
     engines: {node: '>=6.9.0'}
 
-  '@babel/helper-string-parser@7.23.4':
-    resolution: {integrity: sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==}
+  '@babel/helper-split-export-declaration@7.24.7':
+    resolution: {integrity: sha512-oy5V7pD+UvfkEATUKvIjvIAH/xCzfsFVw7ygW2SI6NClZzquT+mwdTfgfdbUiceh6iQO0CHtCPsyze/MZ2YbAA==}
     engines: {node: '>=6.9.0'}
 
-  '@babel/helper-validator-identifier@7.22.20':
-    resolution: {integrity: sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==}
+  '@babel/helper-string-parser@7.24.7':
+    resolution: {integrity: sha512-7MbVt6xrwFQbunH2DNQsAP5sTGxfqQtErvBIvIMi6EQnbgUOuVYanvREcmFrOPhoXBrTtjhhP+lW+o5UfK+tDg==}
+    engines: {node: '>=6.9.0'}
+
+  '@babel/helper-string-parser@7.24.8':
+    resolution: {integrity: sha512-pO9KhhRcuUyGnJWwyEgnRJTSIZHiT+vMD0kPeD+so0l7mxkMT19g3pjY9GTnHySck/hDzq+dtW/4VgnMkippsQ==}
+    engines: {node: '>=6.9.0'}
+
+  '@babel/helper-validator-identifier@7.24.7':
+    resolution: {integrity: sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==}
     engines: {node: '>=6.9.0'}
 
   '@babel/helper-validator-option@7.23.5':
     resolution: {integrity: sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw==}
     engines: {node: '>=6.9.0'}
 
-  '@babel/helper-wrap-function@7.22.20':
-    resolution: {integrity: sha512-pms/UwkOpnQe/PDAEdV/d7dVCoBbB+R4FvYoHGZz+4VPcg7RtYy2KP7S2lbuWM6FCSgob5wshfGESbC/hzNXZw==}
+  '@babel/helper-validator-option@7.24.7':
+    resolution: {integrity: sha512-yy1/KvjhV/ZCL+SM7hBrvnZJ3ZuT9OuZgIJAGpPEToANvc3iM6iDvBnRjtElWibHU6n8/LPR/EjX9EtIEYO3pw==}
     engines: {node: '>=6.9.0'}
 
   '@babel/helpers@7.23.5':
     resolution: {integrity: sha512-oO7us8FzTEsG3U6ag9MfdF1iA/7Z6dz+MtFhifZk8C8o453rGJFFWUP1t+ULM9TUIAzC9uxXEiXjOiVMyd7QPg==}
     engines: {node: '>=6.9.0'}
 
-  '@babel/helpers@7.24.0':
-    resolution: {integrity: sha512-ulDZdc0Aj5uLc5nETsa7EPx2L7rM0YJM8r7ck7U73AXi7qOV44IHHRAYZHY6iU1rr3C5N4NtTmMRUJP6kwCWeA==}
+  '@babel/helpers@7.24.7':
+    resolution: {integrity: sha512-NlmJJtvcw72yRJRcnCmGvSi+3jDEg8qFu3z0AFoymmzLx5ERVWyzd9kVXr7Th9/8yIJi2Zc6av4Tqz3wFs8QWg==}
     engines: {node: '>=6.9.0'}
 
   '@babel/highlight@7.23.4':
     resolution: {integrity: sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==}
     engines: {node: '>=6.9.0'}
 
-  '@babel/parser@7.23.9':
-    resolution: {integrity: sha512-9tcKgqKbs3xGJ+NtKF2ndOBBLVwPjl1SHxPQkd36r3Dlirw3xWUeGaTbqr7uGZcTaxkVNwc+03SVP7aCdWrTlA==}
+  '@babel/highlight@7.24.7':
+    resolution: {integrity: sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==}
+    engines: {node: '>=6.9.0'}
+
+  '@babel/parser@7.24.7':
+    resolution: {integrity: sha512-9uUYRm6OqQrCqQdG1iCBwBPZgN8ciDBro2nIOFaiRz1/BCxaI7CNvQbDHvsArAC7Tw9Hda/B3U+6ui9u4HWXPw==}
     engines: {node: '>=6.0.0'}
     hasBin: true
 
-  '@babel/parser@7.24.0':
-    resolution: {integrity: sha512-QuP/FxEAzMSjXygs8v4N9dvdXzEHN4W1oF3PxuWAtPo08UdM17u89RDMgjLn/mlc56iM0HlLmVkO/wgR+rDgHg==}
+  '@babel/parser@7.25.6':
+    resolution: {integrity: sha512-trGdfBdbD0l1ZPmcJ83eNxB9rbEax4ALFTF7fN386TMYbeCQbyme5cOEXQhbGXKebwGaB/J52w1mrklMcbgy6Q==}
     engines: {node: '>=6.0.0'}
     hasBin: true
 
-  '@babel/parser@7.24.5':
-    resolution: {integrity: sha512-EOv5IK8arwh3LI47dz1b0tKUb/1uhHAnHJOrjgtQMIpu1uXd9mlFrJg9IUgGUgZ41Ch0K8REPTYpO7B76b4vJg==}
-    engines: {node: '>=6.0.0'}
-    hasBin: true
-
-  '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.23.3':
-    resolution: {integrity: sha512-iRkKcCqb7iGnq9+3G6rZ+Ciz5VywC4XNRHe57lKM+jOeYAoR0lVqdeeDRfh0tQcTfw/+vBhHn926FmQhLtlFLQ==}
-    engines: {node: '>=6.9.0'}
-    peerDependencies:
-      '@babel/core': ^7.0.0
-
-  '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.23.3':
-    resolution: {integrity: sha512-WwlxbfMNdVEpQjZmK5mhm7oSwD3dS6eU+Iwsi4Knl9wAletWem7kaRsGOG+8UEbRyqxY4SS5zvtfXwX+jMxUwQ==}
-    engines: {node: '>=6.9.0'}
-    peerDependencies:
-      '@babel/core': ^7.13.0
-
-  '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.23.3':
-    resolution: {integrity: sha512-XaJak1qcityzrX0/IU5nKHb34VaibwP3saKqG6a/tppelgllOH13LUann4ZCIBcVOeE6H18K4Vx9QKkVww3z/w==}
-    engines: {node: '>=6.9.0'}
-    peerDependencies:
-      '@babel/core': ^7.0.0
-
-  '@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2':
-    resolution: {integrity: sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==}
-    engines: {node: '>=6.9.0'}
-    peerDependencies:
-      '@babel/core': ^7.0.0-0
-
   '@babel/plugin-syntax-async-generators@7.8.4':
     resolution: {integrity: sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==}
     peerDependencies:
@@ -1657,40 +1798,6 @@ packages:
     peerDependencies:
       '@babel/core': ^7.0.0-0
 
-  '@babel/plugin-syntax-class-static-block@7.14.5':
-    resolution: {integrity: sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==}
-    engines: {node: '>=6.9.0'}
-    peerDependencies:
-      '@babel/core': ^7.0.0-0
-
-  '@babel/plugin-syntax-dynamic-import@7.8.3':
-    resolution: {integrity: sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==}
-    peerDependencies:
-      '@babel/core': ^7.0.0-0
-
-  '@babel/plugin-syntax-export-namespace-from@7.8.3':
-    resolution: {integrity: sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==}
-    peerDependencies:
-      '@babel/core': ^7.0.0-0
-
-  '@babel/plugin-syntax-flow@7.23.3':
-    resolution: {integrity: sha512-YZiAIpkJAwQXBJLIQbRFayR5c+gJ35Vcz3bg954k7cd73zqjvhacJuL9RbrzPz8qPmZdgqP6EUKwy0PCNhaaPA==}
-    engines: {node: '>=6.9.0'}
-    peerDependencies:
-      '@babel/core': ^7.0.0-0
-
-  '@babel/plugin-syntax-import-assertions@7.23.3':
-    resolution: {integrity: sha512-lPgDSU+SJLK3xmFDTV2ZRQAiM7UuUjGidwBywFavObCiZc1BeAAcMtHJKUya92hPHO+at63JJPLygilZard8jw==}
-    engines: {node: '>=6.9.0'}
-    peerDependencies:
-      '@babel/core': ^7.0.0-0
-
-  '@babel/plugin-syntax-import-attributes@7.23.3':
-    resolution: {integrity: sha512-pawnE0P9g10xgoP7yKr6CK63K2FMsTE+FZidZO/1PwRdzmAPVs+HS1mAURUsgaoxammTJvULUdIkEK0gOcU2tA==}
-    engines: {node: '>=6.9.0'}
-    peerDependencies:
-      '@babel/core': ^7.0.0-0
-
   '@babel/plugin-syntax-import-meta@7.10.4':
     resolution: {integrity: sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==}
     peerDependencies:
@@ -1737,12 +1844,6 @@ packages:
     peerDependencies:
       '@babel/core': ^7.0.0-0
 
-  '@babel/plugin-syntax-private-property-in-object@7.14.5':
-    resolution: {integrity: sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==}
-    engines: {node: '>=6.9.0'}
-    peerDependencies:
-      '@babel/core': ^7.0.0-0
-
   '@babel/plugin-syntax-top-level-await@7.14.5':
     resolution: {integrity: sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==}
     engines: {node: '>=6.9.0'}
@@ -1755,344 +1856,6 @@ packages:
     peerDependencies:
       '@babel/core': ^7.0.0-0
 
-  '@babel/plugin-syntax-unicode-sets-regex@7.18.6':
-    resolution: {integrity: sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==}
-    engines: {node: '>=6.9.0'}
-    peerDependencies:
-      '@babel/core': ^7.0.0
-
-  '@babel/plugin-transform-arrow-functions@7.23.3':
-    resolution: {integrity: sha512-NzQcQrzaQPkaEwoTm4Mhyl8jI1huEL/WWIEvudjTCMJ9aBZNpsJbMASx7EQECtQQPS/DcnFpo0FIh3LvEO9cxQ==}
-    engines: {node: '>=6.9.0'}
-    peerDependencies:
-      '@babel/core': ^7.0.0-0
-
-  '@babel/plugin-transform-async-generator-functions@7.23.4':
-    resolution: {integrity: sha512-efdkfPhHYTtn0G6n2ddrESE91fgXxjlqLsnUtPWnJs4a4mZIbUaK7ffqKIIUKXSHwcDvaCVX6GXkaJJFqtX7jw==}
-    engines: {node: '>=6.9.0'}
-    peerDependencies:
-      '@babel/core': ^7.0.0-0
-
-  '@babel/plugin-transform-async-to-generator@7.23.3':
-    resolution: {integrity: sha512-A7LFsKi4U4fomjqXJlZg/u0ft/n8/7n7lpffUP/ZULx/DtV9SGlNKZolHH6PE8Xl1ngCc0M11OaeZptXVkfKSw==}
-    engines: {node: '>=6.9.0'}
-    peerDependencies:
-      '@babel/core': ^7.0.0-0
-
-  '@babel/plugin-transform-block-scoped-functions@7.23.3':
-    resolution: {integrity: sha512-vI+0sIaPIO6CNuM9Kk5VmXcMVRiOpDh7w2zZt9GXzmE/9KD70CUEVhvPR/etAeNK/FAEkhxQtXOzVF3EuRL41A==}
-    engines: {node: '>=6.9.0'}
-    peerDependencies:
-      '@babel/core': ^7.0.0-0
-
-  '@babel/plugin-transform-block-scoping@7.23.4':
-    resolution: {integrity: sha512-0QqbP6B6HOh7/8iNR4CQU2Th/bbRtBp4KS9vcaZd1fZ0wSh5Fyssg0UCIHwxh+ka+pNDREbVLQnHCMHKZfPwfw==}
-    engines: {node: '>=6.9.0'}
-    peerDependencies:
-      '@babel/core': ^7.0.0-0
-
-  '@babel/plugin-transform-class-properties@7.23.3':
-    resolution: {integrity: sha512-uM+AN8yCIjDPccsKGlw271xjJtGii+xQIF/uMPS8H15L12jZTsLfF4o5vNO7d/oUguOyfdikHGc/yi9ge4SGIg==}
-    engines: {node: '>=6.9.0'}
-    peerDependencies:
-      '@babel/core': ^7.0.0-0
-
-  '@babel/plugin-transform-class-static-block@7.23.4':
-    resolution: {integrity: sha512-nsWu/1M+ggti1SOALj3hfx5FXzAY06fwPJsUZD4/A5e1bWi46VUIWtD+kOX6/IdhXGsXBWllLFDSnqSCdUNydQ==}
-    engines: {node: '>=6.9.0'}
-    peerDependencies:
-      '@babel/core': ^7.12.0
-
-  '@babel/plugin-transform-classes@7.23.5':
-    resolution: {integrity: sha512-jvOTR4nicqYC9yzOHIhXG5emiFEOpappSJAl73SDSEDcybD+Puuze8Tnpb9p9qEyYup24tq891gkaygIFvWDqg==}
-    engines: {node: '>=6.9.0'}
-    peerDependencies:
-      '@babel/core': ^7.0.0-0
-
-  '@babel/plugin-transform-computed-properties@7.23.3':
-    resolution: {integrity: sha512-dTj83UVTLw/+nbiHqQSFdwO9CbTtwq1DsDqm3CUEtDrZNET5rT5E6bIdTlOftDTDLMYxvxHNEYO4B9SLl8SLZw==}
-    engines: {node: '>=6.9.0'}
-    peerDependencies:
-      '@babel/core': ^7.0.0-0
-
-  '@babel/plugin-transform-destructuring@7.23.3':
-    resolution: {integrity: sha512-n225npDqjDIr967cMScVKHXJs7rout1q+tt50inyBCPkyZ8KxeI6d+GIbSBTT/w/9WdlWDOej3V9HE5Lgk57gw==}
-    engines: {node: '>=6.9.0'}
-    peerDependencies:
-      '@babel/core': ^7.0.0-0
-
-  '@babel/plugin-transform-dotall-regex@7.23.3':
-    resolution: {integrity: sha512-vgnFYDHAKzFaTVp+mneDsIEbnJ2Np/9ng9iviHw3P/KVcgONxpNULEW/51Z/BaFojG2GI2GwwXck5uV1+1NOYQ==}
-    engines: {node: '>=6.9.0'}
-    peerDependencies:
-      '@babel/core': ^7.0.0-0
-
-  '@babel/plugin-transform-duplicate-keys@7.23.3':
-    resolution: {integrity: sha512-RrqQ+BQmU3Oyav3J+7/myfvRCq7Tbz+kKLLshUmMwNlDHExbGL7ARhajvoBJEvc+fCguPPu887N+3RRXBVKZUA==}
-    engines: {node: '>=6.9.0'}
-    peerDependencies:
-      '@babel/core': ^7.0.0-0
-
-  '@babel/plugin-transform-dynamic-import@7.23.4':
-    resolution: {integrity: sha512-V6jIbLhdJK86MaLh4Jpghi8ho5fGzt3imHOBu/x0jlBaPYqDoWz4RDXjmMOfnh+JWNaQleEAByZLV0QzBT4YQQ==}
-    engines: {node: '>=6.9.0'}
-    peerDependencies:
-      '@babel/core': ^7.0.0-0
-
-  '@babel/plugin-transform-exponentiation-operator@7.23.3':
-    resolution: {integrity: sha512-5fhCsl1odX96u7ILKHBj4/Y8vipoqwsJMh4csSA8qFfxrZDEA4Ssku2DyNvMJSmZNOEBT750LfFPbtrnTP90BQ==}
-    engines: {node: '>=6.9.0'}
-    peerDependencies:
-      '@babel/core': ^7.0.0-0
-
-  '@babel/plugin-transform-export-namespace-from@7.23.4':
-    resolution: {integrity: sha512-GzuSBcKkx62dGzZI1WVgTWvkkz84FZO5TC5T8dl/Tht/rAla6Dg/Mz9Yhypg+ezVACf/rgDuQt3kbWEv7LdUDQ==}
-    engines: {node: '>=6.9.0'}
-    peerDependencies:
-      '@babel/core': ^7.0.0-0
-
-  '@babel/plugin-transform-flow-strip-types@7.23.3':
-    resolution: {integrity: sha512-26/pQTf9nQSNVJCrLB1IkHUKyPxR+lMrH2QDPG89+Znu9rAMbtrybdbWeE9bb7gzjmE5iXHEY+e0HUwM6Co93Q==}
-    engines: {node: '>=6.9.0'}
-    peerDependencies:
-      '@babel/core': ^7.0.0-0
-
-  '@babel/plugin-transform-for-of@7.23.3':
-    resolution: {integrity: sha512-X8jSm8X1CMwxmK878qsUGJRmbysKNbdpTv/O1/v0LuY/ZkZrng5WYiekYSdg9m09OTmDDUWeEDsTE+17WYbAZw==}
-    engines: {node: '>=6.9.0'}
-    peerDependencies:
-      '@babel/core': ^7.0.0-0
-
-  '@babel/plugin-transform-function-name@7.23.3':
-    resolution: {integrity: sha512-I1QXp1LxIvt8yLaib49dRW5Okt7Q4oaxao6tFVKS/anCdEOMtYwWVKoiOA1p34GOWIZjUK0E+zCp7+l1pfQyiw==}
-    engines: {node: '>=6.9.0'}
-    peerDependencies:
-      '@babel/core': ^7.0.0-0
-
-  '@babel/plugin-transform-json-strings@7.23.4':
-    resolution: {integrity: sha512-81nTOqM1dMwZ/aRXQ59zVubN9wHGqk6UtqRK+/q+ciXmRy8fSolhGVvG09HHRGo4l6fr/c4ZhXUQH0uFW7PZbg==}
-    engines: {node: '>=6.9.0'}
-    peerDependencies:
-      '@babel/core': ^7.0.0-0
-
-  '@babel/plugin-transform-literals@7.23.3':
-    resolution: {integrity: sha512-wZ0PIXRxnwZvl9AYpqNUxpZ5BiTGrYt7kueGQ+N5FiQ7RCOD4cm8iShd6S6ggfVIWaJf2EMk8eRzAh52RfP4rQ==}
-    engines: {node: '>=6.9.0'}
-    peerDependencies:
-      '@babel/core': ^7.0.0-0
-
-  '@babel/plugin-transform-logical-assignment-operators@7.23.4':
-    resolution: {integrity: sha512-Mc/ALf1rmZTP4JKKEhUwiORU+vcfarFVLfcFiolKUo6sewoxSEgl36ak5t+4WamRsNr6nzjZXQjM35WsU+9vbg==}
-    engines: {node: '>=6.9.0'}
-    peerDependencies:
-      '@babel/core': ^7.0.0-0
-
-  '@babel/plugin-transform-member-expression-literals@7.23.3':
-    resolution: {integrity: sha512-sC3LdDBDi5x96LA+Ytekz2ZPk8i/Ck+DEuDbRAll5rknJ5XRTSaPKEYwomLcs1AA8wg9b3KjIQRsnApj+q51Ag==}
-    engines: {node: '>=6.9.0'}
-    peerDependencies:
-      '@babel/core': ^7.0.0-0
-
-  '@babel/plugin-transform-modules-amd@7.23.3':
-    resolution: {integrity: sha512-vJYQGxeKM4t8hYCKVBlZX/gtIY2I7mRGFNcm85sgXGMTBcoV3QdVtdpbcWEbzbfUIUZKwvgFT82mRvaQIebZzw==}
-    engines: {node: '>=6.9.0'}
-    peerDependencies:
-      '@babel/core': ^7.0.0-0
-
-  '@babel/plugin-transform-modules-commonjs@7.23.3':
-    resolution: {integrity: sha512-aVS0F65LKsdNOtcz6FRCpE4OgsP2OFnW46qNxNIX9h3wuzaNcSQsJysuMwqSibC98HPrf2vCgtxKNwS0DAlgcA==}
-    engines: {node: '>=6.9.0'}
-    peerDependencies:
-      '@babel/core': ^7.0.0-0
-
-  '@babel/plugin-transform-modules-systemjs@7.23.3':
-    resolution: {integrity: sha512-ZxyKGTkF9xT9YJuKQRo19ewf3pXpopuYQd8cDXqNzc3mUNbOME0RKMoZxviQk74hwzfQsEe66dE92MaZbdHKNQ==}
-    engines: {node: '>=6.9.0'}
-    peerDependencies:
-      '@babel/core': ^7.0.0-0
-
-  '@babel/plugin-transform-modules-umd@7.23.3':
-    resolution: {integrity: sha512-zHsy9iXX2nIsCBFPud3jKn1IRPWg3Ing1qOZgeKV39m1ZgIdpJqvlWVeiHBZC6ITRG0MfskhYe9cLgntfSFPIg==}
-    engines: {node: '>=6.9.0'}
-    peerDependencies:
-      '@babel/core': ^7.0.0-0
-
-  '@babel/plugin-transform-named-capturing-groups-regex@7.22.5':
-    resolution: {integrity: sha512-YgLLKmS3aUBhHaxp5hi1WJTgOUb/NCuDHzGT9z9WTt3YG+CPRhJs6nprbStx6DnWM4dh6gt7SU3sZodbZ08adQ==}
-    engines: {node: '>=6.9.0'}
-    peerDependencies:
-      '@babel/core': ^7.0.0
-
-  '@babel/plugin-transform-new-target@7.23.3':
-    resolution: {integrity: sha512-YJ3xKqtJMAT5/TIZnpAR3I+K+WaDowYbN3xyxI8zxx/Gsypwf9B9h0VB+1Nh6ACAAPRS5NSRje0uVv5i79HYGQ==}
-    engines: {node: '>=6.9.0'}
-    peerDependencies:
-      '@babel/core': ^7.0.0-0
-
-  '@babel/plugin-transform-nullish-coalescing-operator@7.23.4':
-    resolution: {integrity: sha512-jHE9EVVqHKAQx+VePv5LLGHjmHSJR76vawFPTdlxR/LVJPfOEGxREQwQfjuZEOPTwG92X3LINSh3M40Rv4zpVA==}
-    engines: {node: '>=6.9.0'}
-    peerDependencies:
-      '@babel/core': ^7.0.0-0
-
-  '@babel/plugin-transform-numeric-separator@7.23.4':
-    resolution: {integrity: sha512-mps6auzgwjRrwKEZA05cOwuDc9FAzoyFS4ZsG/8F43bTLf/TgkJg7QXOrPO1JO599iA3qgK9MXdMGOEC8O1h6Q==}
-    engines: {node: '>=6.9.0'}
-    peerDependencies:
-      '@babel/core': ^7.0.0-0
-
-  '@babel/plugin-transform-object-rest-spread@7.23.4':
-    resolution: {integrity: sha512-9x9K1YyeQVw0iOXJlIzwm8ltobIIv7j2iLyP2jIhEbqPRQ7ScNgwQufU2I0Gq11VjyG4gI4yMXt2VFags+1N3g==}
-    engines: {node: '>=6.9.0'}
-    peerDependencies:
-      '@babel/core': ^7.0.0-0
-
-  '@babel/plugin-transform-object-super@7.23.3':
-    resolution: {integrity: sha512-BwQ8q0x2JG+3lxCVFohg+KbQM7plfpBwThdW9A6TMtWwLsbDA01Ek2Zb/AgDN39BiZsExm4qrXxjk+P1/fzGrA==}
-    engines: {node: '>=6.9.0'}
-    peerDependencies:
-      '@babel/core': ^7.0.0-0
-
-  '@babel/plugin-transform-optional-catch-binding@7.23.4':
-    resolution: {integrity: sha512-XIq8t0rJPHf6Wvmbn9nFxU6ao4c7WhghTR5WyV8SrJfUFzyxhCm4nhC+iAp3HFhbAKLfYpgzhJ6t4XCtVwqO5A==}
-    engines: {node: '>=6.9.0'}
-    peerDependencies:
-      '@babel/core': ^7.0.0-0
-
-  '@babel/plugin-transform-optional-chaining@7.23.4':
-    resolution: {integrity: sha512-ZU8y5zWOfjM5vZ+asjgAPwDaBjJzgufjES89Rs4Lpq63O300R/kOz30WCLo6BxxX6QVEilwSlpClnG5cZaikTA==}
-    engines: {node: '>=6.9.0'}
-    peerDependencies:
-      '@babel/core': ^7.0.0-0
-
-  '@babel/plugin-transform-parameters@7.23.3':
-    resolution: {integrity: sha512-09lMt6UsUb3/34BbECKVbVwrT9bO6lILWln237z7sLaWnMsTi7Yc9fhX5DLpkJzAGfaReXI22wP41SZmnAA3Vw==}
-    engines: {node: '>=6.9.0'}
-    peerDependencies:
-      '@babel/core': ^7.0.0-0
-
-  '@babel/plugin-transform-private-methods@7.23.3':
-    resolution: {integrity: sha512-UzqRcRtWsDMTLrRWFvUBDwmw06tCQH9Rl1uAjfh6ijMSmGYQ+fpdB+cnqRC8EMh5tuuxSv0/TejGL+7vyj+50g==}
-    engines: {node: '>=6.9.0'}
-    peerDependencies:
-      '@babel/core': ^7.0.0-0
-
-  '@babel/plugin-transform-private-property-in-object@7.23.4':
-    resolution: {integrity: sha512-9G3K1YqTq3F4Vt88Djx1UZ79PDyj+yKRnUy7cZGSMe+a7jkwD259uKKuUzQlPkGam7R+8RJwh5z4xO27fA1o2A==}
-    engines: {node: '>=6.9.0'}
-    peerDependencies:
-      '@babel/core': ^7.0.0-0
-
-  '@babel/plugin-transform-property-literals@7.23.3':
-    resolution: {integrity: sha512-jR3Jn3y7cZp4oEWPFAlRsSWjxKe4PZILGBSd4nis1TsC5qeSpb+nrtihJuDhNI7QHiVbUaiXa0X2RZY3/TI6Nw==}
-    engines: {node: '>=6.9.0'}
-    peerDependencies:
-      '@babel/core': ^7.0.0-0
-
-  '@babel/plugin-transform-regenerator@7.23.3':
-    resolution: {integrity: sha512-KP+75h0KghBMcVpuKisx3XTu9Ncut8Q8TuvGO4IhY+9D5DFEckQefOuIsB/gQ2tG71lCke4NMrtIPS8pOj18BQ==}
-    engines: {node: '>=6.9.0'}
-    peerDependencies:
-      '@babel/core': ^7.0.0-0
-
-  '@babel/plugin-transform-reserved-words@7.23.3':
-    resolution: {integrity: sha512-QnNTazY54YqgGxwIexMZva9gqbPa15t/x9VS+0fsEFWplwVpXYZivtgl43Z1vMpc1bdPP2PP8siFeVcnFvA3Cg==}
-    engines: {node: '>=6.9.0'}
-    peerDependencies:
-      '@babel/core': ^7.0.0-0
-
-  '@babel/plugin-transform-shorthand-properties@7.23.3':
-    resolution: {integrity: sha512-ED2fgqZLmexWiN+YNFX26fx4gh5qHDhn1O2gvEhreLW2iI63Sqm4llRLCXALKrCnbN4Jy0VcMQZl/SAzqug/jg==}
-    engines: {node: '>=6.9.0'}
-    peerDependencies:
-      '@babel/core': ^7.0.0-0
-
-  '@babel/plugin-transform-spread@7.23.3':
-    resolution: {integrity: sha512-VvfVYlrlBVu+77xVTOAoxQ6mZbnIq5FM0aGBSFEcIh03qHf+zNqA4DC/3XMUozTg7bZV3e3mZQ0i13VB6v5yUg==}
-    engines: {node: '>=6.9.0'}
-    peerDependencies:
-      '@babel/core': ^7.0.0-0
-
-  '@babel/plugin-transform-sticky-regex@7.23.3':
-    resolution: {integrity: sha512-HZOyN9g+rtvnOU3Yh7kSxXrKbzgrm5X4GncPY1QOquu7epga5MxKHVpYu2hvQnry/H+JjckSYRb93iNfsioAGg==}
-    engines: {node: '>=6.9.0'}
-    peerDependencies:
-      '@babel/core': ^7.0.0-0
-
-  '@babel/plugin-transform-template-literals@7.23.3':
-    resolution: {integrity: sha512-Flok06AYNp7GV2oJPZZcP9vZdszev6vPBkHLwxwSpaIqx75wn6mUd3UFWsSsA0l8nXAKkyCmL/sR02m8RYGeHg==}
-    engines: {node: '>=6.9.0'}
-    peerDependencies:
-      '@babel/core': ^7.0.0-0
-
-  '@babel/plugin-transform-typeof-symbol@7.23.3':
-    resolution: {integrity: sha512-4t15ViVnaFdrPC74be1gXBSMzXk3B4Us9lP7uLRQHTFpV5Dvt33pn+2MyyNxmN3VTTm3oTrZVMUmuw3oBnQ2oQ==}
-    engines: {node: '>=6.9.0'}
-    peerDependencies:
-      '@babel/core': ^7.0.0-0
-
-  '@babel/plugin-transform-typescript@7.23.5':
-    resolution: {integrity: sha512-2fMkXEJkrmwgu2Bsv1Saxgj30IXZdJ+84lQcKKI7sm719oXs0BBw2ZENKdJdR1PjWndgLCEBNXJOri0fk7RYQA==}
-    engines: {node: '>=6.9.0'}
-    peerDependencies:
-      '@babel/core': ^7.0.0-0
-
-  '@babel/plugin-transform-unicode-escapes@7.23.3':
-    resolution: {integrity: sha512-OMCUx/bU6ChE3r4+ZdylEqAjaQgHAgipgW8nsCfu5pGqDcFytVd91AwRvUJSBZDz0exPGgnjoqhgRYLRjFZc9Q==}
-    engines: {node: '>=6.9.0'}
-    peerDependencies:
-      '@babel/core': ^7.0.0-0
-
-  '@babel/plugin-transform-unicode-property-regex@7.23.3':
-    resolution: {integrity: sha512-KcLIm+pDZkWZQAFJ9pdfmh89EwVfmNovFBcXko8szpBeF8z68kWIPeKlmSOkT9BXJxs2C0uk+5LxoxIv62MROA==}
-    engines: {node: '>=6.9.0'}
-    peerDependencies:
-      '@babel/core': ^7.0.0-0
-
-  '@babel/plugin-transform-unicode-regex@7.23.3':
-    resolution: {integrity: sha512-wMHpNA4x2cIA32b/ci3AfwNgheiva2W0WUKWTK7vBHBhDKfPsc5cFGNWm69WBqpwd86u1qwZ9PWevKqm1A3yAw==}
-    engines: {node: '>=6.9.0'}
-    peerDependencies:
-      '@babel/core': ^7.0.0-0
-
-  '@babel/plugin-transform-unicode-sets-regex@7.23.3':
-    resolution: {integrity: sha512-W7lliA/v9bNR83Qc3q1ip9CQMZ09CcHDbHfbLRDNuAhn1Mvkr1ZNF7hPmztMQvtTGVLJ9m8IZqWsTkXOml8dbw==}
-    engines: {node: '>=6.9.0'}
-    peerDependencies:
-      '@babel/core': ^7.0.0
-
-  '@babel/preset-env@7.23.5':
-    resolution: {integrity: sha512-0d/uxVD6tFGWXGDSfyMD1p2otoaKmu6+GD+NfAx0tMaH+dxORnp7T9TaVQ6mKyya7iBtCIVxHjWT7MuzzM9z+A==}
-    engines: {node: '>=6.9.0'}
-    peerDependencies:
-      '@babel/core': ^7.0.0-0
-
-  '@babel/preset-flow@7.23.3':
-    resolution: {integrity: sha512-7yn6hl8RIv+KNk6iIrGZ+D06VhVY35wLVf23Cz/mMu1zOr7u4MMP4j0nZ9tLf8+4ZFpnib8cFYgB/oYg9hfswA==}
-    engines: {node: '>=6.9.0'}
-    peerDependencies:
-      '@babel/core': ^7.0.0-0
-
-  '@babel/preset-modules@0.1.6-no-external-plugins':
-    resolution: {integrity: sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==}
-    peerDependencies:
-      '@babel/core': ^7.0.0-0 || ^8.0.0-0 <8.0.0
-
-  '@babel/preset-typescript@7.23.3':
-    resolution: {integrity: sha512-17oIGVlqz6CchO9RFYn5U6ZpWRZIngayYCtrPRSgANSwC2V1Jb+iP74nVxzzXJte8b8BYxrL1yY96xfhTBrNNQ==}
-    engines: {node: '>=6.9.0'}
-    peerDependencies:
-      '@babel/core': ^7.0.0-0
-
-  '@babel/register@7.22.15':
-    resolution: {integrity: sha512-V3Q3EqoQdn65RCgTLwauZaTfd1ShhwPmbBv+1dkZV/HpCGMKVyn6oFcRlI7RaKqiDQjX2Qd3AuoEguBgdjIKlg==}
-    engines: {node: '>=6.9.0'}
-    peerDependencies:
-      '@babel/core': ^7.0.0-0
-
-  '@babel/regjsgen@0.8.0':
-    resolution: {integrity: sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA==}
-
   '@babel/runtime@7.23.4':
     resolution: {integrity: sha512-2Yv65nlWnWlSpe3fXEyX5i7fx5kIKo4Qbcj+hMO0odwaneFjfXw5fdum+4yL20O0QiaHpia0cYQ9xpNMqrBwHg==}
     engines: {node: '>=6.9.0'}
@@ -2105,20 +1868,24 @@ packages:
     resolution: {integrity: sha512-Bkf2q8lMB0AFpX0NFEqSbx1OkTHf0f+0j82mkw+ZpzBnkk7e9Ql0891vlfgi+kHwOk8tQjiQHpqh4LaSa0fKEA==}
     engines: {node: '>=6.9.0'}
 
+  '@babel/template@7.24.7':
+    resolution: {integrity: sha512-jYqfPrU9JTF0PmPy1tLYHW4Mp4KlgxJD9l2nP9fD6yT/ICi554DmrWBAEYpIelzjHf1msDP3PxJIRt/nFNfBig==}
+    engines: {node: '>=6.9.0'}
+
   '@babel/traverse@7.23.5':
     resolution: {integrity: sha512-czx7Xy5a6sapWWRx61m1Ke1Ra4vczu1mCTtJam5zRTBOonfdJ+S/B6HYmGYu3fJtr8GGET3si6IhgWVBhJ/m8w==}
     engines: {node: '>=6.9.0'}
 
-  '@babel/traverse@7.24.0':
-    resolution: {integrity: sha512-HfuJlI8qq3dEDmNU5ChzzpZRWq+oxCZQyMzIMEqLho+AQnhMnKQUzH6ydo3RBl/YjPCuk68Y6s0Gx0AeyULiWw==}
+  '@babel/traverse@7.24.7':
+    resolution: {integrity: sha512-yb65Ed5S/QAcewNPh0nZczy9JdYXkkAbIsEo+P7BE7yO3txAY30Y/oPa3QkQ5It3xVG2kpKMg9MsdxZaO31uKA==}
     engines: {node: '>=6.9.0'}
 
-  '@babel/types@7.23.5':
-    resolution: {integrity: sha512-ON5kSOJwVO6xXVRTvOI0eOnWe7VdUcIpsovGo9U/Br4Ie4UVFQTboO2cYnDhAGU6Fp+UxSiT+pMft0SMHfuq6w==}
+  '@babel/types@7.24.7':
+    resolution: {integrity: sha512-XEFXSlxiG5td2EJRe8vOmRbaXVgfcBlszKujvVmWIK/UpywWljQCfzAv3RQCGujWQ1RD4YYWEAqDXfuJiy8f5Q==}
     engines: {node: '>=6.9.0'}
 
-  '@babel/types@7.24.0':
-    resolution: {integrity: sha512-+j7a5c253RfKh8iABBhywc8NSfP5LURe7Uh4qpsh6jc+aLJguvmIUBdjSdEMQv2bENrCR5MfRdjGo7vzS/ob7w==}
+  '@babel/types@7.25.6':
+    resolution: {integrity: sha512-/l42B1qxpG6RdfYf343Uw1vmDjeNhneUXtzhojE7pDgfpEypmRhI6j1kr17XCVv4Cgl9HdAiQY2x0GwKm7rWCw==}
     engines: {node: '>=6.9.0'}
 
   '@base2/pretty-print-object@1.0.1':
@@ -2127,16 +1894,16 @@ packages:
   '@bcoe/v8-coverage@0.2.3':
     resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==}
 
-  '@bull-board/api@5.17.0':
-    resolution: {integrity: sha512-qU+AiZIaYa//rkt1x7jDowtYa8u7/dLsDfEWgenZMkgvUszZ1kxJszdCtGapsDTVyPmnXgTRxpOWcR6sAYwSNQ==}
+  '@bull-board/api@6.0.0':
+    resolution: {integrity: sha512-O0IsIwAOU47bPTJnqRO7RtKFQToMvwRebbuPi6M+SG1gXyiqixLg9pycnfXgSeroaT9E7QQ2PsCPW1HO8VORvw==}
     peerDependencies:
-      '@bull-board/ui': 5.17.0
+      '@bull-board/ui': 6.0.0
 
-  '@bull-board/fastify@5.17.0':
-    resolution: {integrity: sha512-73YrPc7ERTWSOQRgBP6a7BPscWfcHd8U+Zq0auMdL/KkjPhG9GxapbfnovGZDDahJL/p/4YQb6ULu03zdtOrEA==}
+  '@bull-board/fastify@6.0.0':
+    resolution: {integrity: sha512-VrKa5BdxYmXh5fJvlSPSm71b+QA9VVXHyGk6xmI/qAefUQbwd2cWJo+ppqaWSaweXa9ymJc+V4l/un0K4oomVA==}
 
-  '@bull-board/ui@5.17.0':
-    resolution: {integrity: sha512-Vj+yWPjrjx3Iqh2N/ZBDhK2d2yJD44dfvIxm+SnXQb4ne312j117TpViInceysxGtbbAOlAW6hq6JvsDoRl7KQ==}
+  '@bull-board/ui@6.0.0':
+    resolution: {integrity: sha512-wAFTlBTJbq5DSWxCzTV+FOyZDVwrXP+G1CQ2BpLG9o9+dpwYxUESx/VxNEDHnyPcy13gm29kB4fSRY+nkelkcQ==}
 
   '@bundled-es-modules/cookie@2.0.0':
     resolution: {integrity: sha512-Or6YHg/kamKHpxULAdSqhGqnWFneIXu1NKvvfBBzKGwpVsYuFIQ5aBPHDnnoR3ghW1nvSkALd+EF9iMtY7Vjxw==}
@@ -2144,6 +1911,9 @@ packages:
   '@bundled-es-modules/statuses@1.0.1':
     resolution: {integrity: sha512-yn7BklA5acgcBr+7w064fGV+SGIFySjCKpqjcWgBAIfrAkY+4GQTJJHQMeT3V/sgz23VTEVV8TtOmkvJAhFVfg==}
 
+  '@bundled-es-modules/tough-cookie@0.1.6':
+    resolution: {integrity: sha512-dvMHbL464C0zI+Yqxbz6kZ5TOEp7GLW+pry/RWndAR8MJQAXZ2rPmIs8tziTZjeIyhSNZgZbCePtfSbdWqStJw==}
+
   '@canvas/image-data@1.0.0':
     resolution: {integrity: sha512-BxOqI5LgsIQP1odU5KMwV9yoijleOPzHL18/YvNqF9KFSGF2K/DLlYAbDQsWqd/1nbaFuSkYD/191dpMtNh4vw==}
 
@@ -2151,41 +1921,41 @@ packages:
     resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==}
     engines: {node: '>=0.1.90'}
 
-  '@cropper/element-canvas@2.0.0-beta.5':
-    resolution: {integrity: sha512-TS+NTVQAyLKBFLIjzFcaFK6V5GaNCNSp8FBjApTD/AosV/dPRlNCsgmdJ/BugwJTJUSowVnLrPmulI35z4npAg==}
+  '@cropper/element-canvas@2.0.0-rc.2':
+    resolution: {integrity: sha512-0aqbJ3ycQM6/yn4T03vw8K/OeTB8C6+Z/jimuavy4UM2CENH9ucSLM4hAG0yYCgghIyv9Zd0unaBmtgW+I5+SQ==}
 
-  '@cropper/element-crosshair@2.0.0-beta.5':
-    resolution: {integrity: sha512-en3EjiqS/O/uVPVLUanx2ZxvE2n3J5VxGABvBTwQimX4c3kNixq8TUVlsaLdcG7jbehxFpT3S19+tiuZudHqxg==}
+  '@cropper/element-crosshair@2.0.0-rc.2':
+    resolution: {integrity: sha512-yopINLvaZhL3E2GNienju1zeQ1Cifkn5f/0R7ZabXcAgUI0s2sLzNqL8+2XV2J3DzEzYEIYc+49KmMle04nVWQ==}
 
-  '@cropper/element-grid@2.0.0-beta.5':
-    resolution: {integrity: sha512-uKQExNTOMOGo5d6Tv1NJDbjJHRR/0NgqeROUSt2J8g9ymPP+/MoFdCCf+Nj/KM5pk7/fBEV3HhzUnO8jh1hZfQ==}
+  '@cropper/element-grid@2.0.0-rc.2':
+    resolution: {integrity: sha512-PzAfEya6CmIc/o/lcA/NZ1rohszz42wjq2z3E2zq2jMfNDxY/EIoFnGI6+hJrxCAaoKD8UlKOEHQdRQbtnjcMg==}
 
-  '@cropper/element-handle@2.0.0-beta.5':
-    resolution: {integrity: sha512-SlaV5/qbEBQLHnuaGD8J0EqSp797m/MMB8V10EUZpv6cznSRxg/SXOj6ROE0ePzo5+0i96Dw+8ZukLilDgc1Xw==}
+  '@cropper/element-handle@2.0.0-rc.2':
+    resolution: {integrity: sha512-wOWX4xpryxKcrhnJC2mHebqQQ622UN2oyQoDZcaMzvlwt7nnX3bInF+SFrIj9/aCxtCUYY0oD2gaJkfd6aNJ0g==}
 
-  '@cropper/element-image@2.0.0-beta.5':
-    resolution: {integrity: sha512-369ztVaoRS2DN8SaiHZ/bRCz0Snw9ss7PZrX1OQK86fwVhCoeRqCHj48ayfLMdchx+J3RbM5f2g8ePf7ejwOKw==}
+  '@cropper/element-image@2.0.0-rc.2':
+    resolution: {integrity: sha512-RTKnuJrqn1K8FscS11auit2W57AG04mxRNOxBldYs3lKTkwZjzJdQFkZ/Nxu+cwVXT+c6IeEiayNKvu4B7CAQg==}
 
-  '@cropper/element-selection@2.0.0-beta.5':
-    resolution: {integrity: sha512-l8DvOBAZYytTarpkfhCglhxD+zDQ2acVwIzGwp5r9xR+ERleJHxr2rHYVhowRHT/JZRd94DJBlye91c1uO/XGg==}
+  '@cropper/element-selection@2.0.0-rc.2':
+    resolution: {integrity: sha512-UIgIHKHz4qNKlm5YRnC/Pu9+VrInm5TSOzkmU8kPt2swUk0WHNRv3ZcOjCQZ2ccTQnAH3FVM3FYDZ8HjRwLcBg==}
 
-  '@cropper/element-shade@2.0.0-beta.5':
-    resolution: {integrity: sha512-LGSVLAD1lasFrS+Pd7JnQSJRCMSNnc40UCcjLhscDuRcRHK/ViMglnwCfFxeGnS26kugbDLF5IbYDCLCbykUog==}
+  '@cropper/element-shade@2.0.0-rc.2':
+    resolution: {integrity: sha512-vHAGFxlqgflGZWkRYNWNHUY0zsV72YZGmCgtUu4sMrnWLZL/jMGhxmm8zZCe/aB94F829XcQ6uf3BoiApB+7Ng==}
 
-  '@cropper/element-viewer@2.0.0-beta.5':
-    resolution: {integrity: sha512-i4cc+L+j8Gq1L8g1BQWfQ842QxH5T9v2EkIeGuW66SVSBglafxu8gxmSOyRD3hDAMHM3wbJ+XVmFwBHZzlYCvQ==}
+  '@cropper/element-viewer@2.0.0-rc.2':
+    resolution: {integrity: sha512-2z9mIA7ic3enNS4xvq9Gq6hnRZ1tPr0h+lCrOHP55NL4he63lE9oTVJfDx19rL95wUS4VxL2ANvr2BVLNiBM7A==}
 
-  '@cropper/element@2.0.0-beta.5':
-    resolution: {integrity: sha512-+pHX/iYw+Y/HxgpcjvSPBc3+hvJaycznbZdWifnChmDkpLStd6Xu9gO2ful9sSL0uGSjQxUYV4xPyikYJOnfug==}
+  '@cropper/element@2.0.0-rc.2':
+    resolution: {integrity: sha512-4G6lTJblndwzpsb43YKeHiKcocOkDIWystGzbHNbqRysE0U0lYHuRyvV7FW6a9S63wtMFSYuwFxcdUdUcmkF8w==}
 
-  '@cropper/elements@2.0.0-beta.5':
-    resolution: {integrity: sha512-KWa5/dmJpLcKDJpNlbEQzO9Shz+f4aB0I3y97CqqTf8JSGS6CEKOd9uLywd1eow1r4O0Hwo65ktXPwAEhMWDZg==}
+  '@cropper/elements@2.0.0-rc.2':
+    resolution: {integrity: sha512-NG5kdqpv7/tGvUfNjJiIHr2Ip431v5t/P5cIXTcYAgt8PRyFJmjx3fatC7NLnP/FUlv+bbzd8PMRI4LY4Gaw3Q==}
 
-  '@cropper/utils@2.0.0-beta.5':
-    resolution: {integrity: sha512-xE7Klel/4WSjhLUeldfROwbWqV/1A3YKrQLqTrs5/X0ath7B05Fmvhr3TNFvN51v2KSx46Ug6xDJzmbg772m1g==}
+  '@cropper/utils@2.0.0-rc.2':
+    resolution: {integrity: sha512-EEivNsyV6BtL496m4Q/IeAC6FGlyKjKIT1qMtwaxtkR+2ZlKnf9O7AdcGpClemIBA+TbwWAzp0UyIvYFtKUZ1Q==}
 
-  '@cypress/request@3.0.0':
-    resolution: {integrity: sha512-GKFCqwZwMYmL3IBoNeR2MM1SnxRIGERsQOTWeQKoYBt2JLqcqiy7JXqO894FLrpjZYqGxW92MNwRH2BN56obdQ==}
+  '@cypress/request@3.0.5':
+    resolution: {integrity: sha512-v+XHd9XmWbufxF1/bTaVm2yhbxY+TB4YtWRqF2zaXBlDNMkls34KiATz0AVDLavL3iB6bQk9/7n3oY1EoLSWGA==}
     engines: {node: '>= 6'}
 
   '@cypress/xvfb@1.2.4':
@@ -2195,20 +1965,11 @@ packages:
     resolution: {integrity: sha512-Ahk1N+s7urkgj7WvvUND5f8GiWEPfUw0D41hdElaqLgu8wZScI8gdI0q+qWw5N1d35x7GCRH2uk9mi+Uzo9M3g==}
     engines: {node: '>=14.0'}
 
-  '@discordapp/twemoji@15.0.3':
-    resolution: {integrity: sha512-5t0LLrNaSqViG0cSaomWwfR0+3fWqok+xLq40M8hJHxNX7s8gIoyNZYybQJo+s5/rGMjgdldpt8Ox8MapGvBUA==}
+  '@discordapp/twemoji@15.1.0':
+    resolution: {integrity: sha512-QdpV4ifTONAXvDjRrMohausZeGrQ1ac/Ox6togUh6Xl3XKJ/KAaMMuAEi0qsb0wDwoVTSZBll5Y6+N3hB2ktBw==}
 
-  '@discoveryjs/json-ext@0.5.7':
-    resolution: {integrity: sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==}
-    engines: {node: '>=10.0.0'}
-
-  '@emnapi/runtime@1.1.1':
-    resolution: {integrity: sha512-3bfqkzuR1KLx57nZfjr2NLnFOobvyS0aTszaEGCGqmYMVDRaGvgIZbjGSV/MHSSmLgQ/b9JFHQ5xm5WRZYd+XQ==}
-
-  '@emotion/use-insertion-effect-with-fallbacks@1.0.1':
-    resolution: {integrity: sha512-jT/qyKZ9rzLErtrjGgdkMBn2OP8wl0G3sQlBb3YPryvKHsjvINUhVaPFfP+fpBcOkmrVOVEEHQFJ7nbj2TH2gw==}
-    peerDependencies:
-      react: '>=16.8.0'
+  '@emnapi/runtime@1.2.0':
+    resolution: {integrity: sha512-bV21/9LQmcQeCPEg3BDFtvwL6cwiTMksYNWQQ4KOxCZikEGalWtenoZ0wCiukJINlGCIi2KXx01g4FoH/LxpzQ==}
 
   '@esbuild/aix-ppc64@0.19.11':
     resolution: {integrity: sha512-FnzU0LyE3ySQk7UntJO4+qIiQgI7KoODnZg5xzXIrFJlKd2P2gwHsHY4927xj9y5PJmJSzULiUCWmv7iWnNa7g==}
@@ -2216,12 +1977,24 @@ packages:
     cpu: [ppc64]
     os: [aix]
 
-  '@esbuild/aix-ppc64@0.20.2':
-    resolution: {integrity: sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==}
+  '@esbuild/aix-ppc64@0.21.5':
+    resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==}
     engines: {node: '>=12'}
     cpu: [ppc64]
     os: [aix]
 
+  '@esbuild/aix-ppc64@0.23.0':
+    resolution: {integrity: sha512-3sG8Zwa5fMcA9bgqB8AfWPQ+HFke6uD3h1s3RIwUNK8EG7a4buxvuFTs3j1IMs2NXAk9F30C/FF4vxRgQCcmoQ==}
+    engines: {node: '>=18'}
+    cpu: [ppc64]
+    os: [aix]
+
+  '@esbuild/aix-ppc64@0.23.1':
+    resolution: {integrity: sha512-6VhYk1diRqrhBAqpJEdjASR/+WVRtfjpqKuNw11cLiaWpAT/Uu+nokB+UJnevzy/P9C/ty6AOe0dwueMrGh/iQ==}
+    engines: {node: '>=18'}
+    cpu: [ppc64]
+    os: [aix]
+
   '@esbuild/android-arm64@0.18.20':
     resolution: {integrity: sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==}
     engines: {node: '>=12'}
@@ -2234,12 +2007,24 @@ packages:
     cpu: [arm64]
     os: [android]
 
-  '@esbuild/android-arm64@0.20.2':
-    resolution: {integrity: sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==}
+  '@esbuild/android-arm64@0.21.5':
+    resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==}
     engines: {node: '>=12'}
     cpu: [arm64]
     os: [android]
 
+  '@esbuild/android-arm64@0.23.0':
+    resolution: {integrity: sha512-EuHFUYkAVfU4qBdyivULuu03FhJO4IJN9PGuABGrFy4vUuzk91P2d+npxHcFdpUnfYKy0PuV+n6bKIpHOB3prQ==}
+    engines: {node: '>=18'}
+    cpu: [arm64]
+    os: [android]
+
+  '@esbuild/android-arm64@0.23.1':
+    resolution: {integrity: sha512-xw50ipykXcLstLeWH7WRdQuysJqejuAGPd30vd1i5zSyKK3WE+ijzHmLKxdiCMtH1pHz78rOg0BKSYOSB/2Khw==}
+    engines: {node: '>=18'}
+    cpu: [arm64]
+    os: [android]
+
   '@esbuild/android-arm@0.18.20':
     resolution: {integrity: sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==}
     engines: {node: '>=12'}
@@ -2252,12 +2037,24 @@ packages:
     cpu: [arm]
     os: [android]
 
-  '@esbuild/android-arm@0.20.2':
-    resolution: {integrity: sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==}
+  '@esbuild/android-arm@0.21.5':
+    resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==}
     engines: {node: '>=12'}
     cpu: [arm]
     os: [android]
 
+  '@esbuild/android-arm@0.23.0':
+    resolution: {integrity: sha512-+KuOHTKKyIKgEEqKbGTK8W7mPp+hKinbMBeEnNzjJGyFcWsfrXjSTNluJHCY1RqhxFurdD8uNXQDei7qDlR6+g==}
+    engines: {node: '>=18'}
+    cpu: [arm]
+    os: [android]
+
+  '@esbuild/android-arm@0.23.1':
+    resolution: {integrity: sha512-uz6/tEy2IFm9RYOyvKl88zdzZfwEfKZmnX9Cj1BHjeSGNuGLuMD1kR8y5bteYmwqKm1tj8m4cb/aKEorr6fHWQ==}
+    engines: {node: '>=18'}
+    cpu: [arm]
+    os: [android]
+
   '@esbuild/android-x64@0.18.20':
     resolution: {integrity: sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==}
     engines: {node: '>=12'}
@@ -2270,12 +2067,24 @@ packages:
     cpu: [x64]
     os: [android]
 
-  '@esbuild/android-x64@0.20.2':
-    resolution: {integrity: sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==}
+  '@esbuild/android-x64@0.21.5':
+    resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==}
     engines: {node: '>=12'}
     cpu: [x64]
     os: [android]
 
+  '@esbuild/android-x64@0.23.0':
+    resolution: {integrity: sha512-WRrmKidLoKDl56LsbBMhzTTBxrsVwTKdNbKDalbEZr0tcsBgCLbEtoNthOW6PX942YiYq8HzEnb4yWQMLQuipQ==}
+    engines: {node: '>=18'}
+    cpu: [x64]
+    os: [android]
+
+  '@esbuild/android-x64@0.23.1':
+    resolution: {integrity: sha512-nlN9B69St9BwUoB+jkyU090bru8L0NA3yFvAd7k8dNsVH8bi9a8cUAUSEcEEgTp2z3dbEDGJGfP6VUnkQnlReg==}
+    engines: {node: '>=18'}
+    cpu: [x64]
+    os: [android]
+
   '@esbuild/darwin-arm64@0.18.20':
     resolution: {integrity: sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==}
     engines: {node: '>=12'}
@@ -2288,12 +2097,24 @@ packages:
     cpu: [arm64]
     os: [darwin]
 
-  '@esbuild/darwin-arm64@0.20.2':
-    resolution: {integrity: sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==}
+  '@esbuild/darwin-arm64@0.21.5':
+    resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==}
     engines: {node: '>=12'}
     cpu: [arm64]
     os: [darwin]
 
+  '@esbuild/darwin-arm64@0.23.0':
+    resolution: {integrity: sha512-YLntie/IdS31H54Ogdn+v50NuoWF5BDkEUFpiOChVa9UnKpftgwzZRrI4J132ETIi+D8n6xh9IviFV3eXdxfow==}
+    engines: {node: '>=18'}
+    cpu: [arm64]
+    os: [darwin]
+
+  '@esbuild/darwin-arm64@0.23.1':
+    resolution: {integrity: sha512-YsS2e3Wtgnw7Wq53XXBLcV6JhRsEq8hkfg91ESVadIrzr9wO6jJDMZnCQbHm1Guc5t/CdDiFSSfWP58FNuvT3Q==}
+    engines: {node: '>=18'}
+    cpu: [arm64]
+    os: [darwin]
+
   '@esbuild/darwin-x64@0.18.20':
     resolution: {integrity: sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==}
     engines: {node: '>=12'}
@@ -2306,12 +2127,24 @@ packages:
     cpu: [x64]
     os: [darwin]
 
-  '@esbuild/darwin-x64@0.20.2':
-    resolution: {integrity: sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==}
+  '@esbuild/darwin-x64@0.21.5':
+    resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==}
     engines: {node: '>=12'}
     cpu: [x64]
     os: [darwin]
 
+  '@esbuild/darwin-x64@0.23.0':
+    resolution: {integrity: sha512-IMQ6eme4AfznElesHUPDZ+teuGwoRmVuuixu7sv92ZkdQcPbsNHzutd+rAfaBKo8YK3IrBEi9SLLKWJdEvJniQ==}
+    engines: {node: '>=18'}
+    cpu: [x64]
+    os: [darwin]
+
+  '@esbuild/darwin-x64@0.23.1':
+    resolution: {integrity: sha512-aClqdgTDVPSEGgoCS8QDG37Gu8yc9lTHNAQlsztQ6ENetKEO//b8y31MMu2ZaPbn4kVsIABzVLXYLhCGekGDqw==}
+    engines: {node: '>=18'}
+    cpu: [x64]
+    os: [darwin]
+
   '@esbuild/freebsd-arm64@0.18.20':
     resolution: {integrity: sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==}
     engines: {node: '>=12'}
@@ -2324,12 +2157,24 @@ packages:
     cpu: [arm64]
     os: [freebsd]
 
-  '@esbuild/freebsd-arm64@0.20.2':
-    resolution: {integrity: sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==}
+  '@esbuild/freebsd-arm64@0.21.5':
+    resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==}
     engines: {node: '>=12'}
     cpu: [arm64]
     os: [freebsd]
 
+  '@esbuild/freebsd-arm64@0.23.0':
+    resolution: {integrity: sha512-0muYWCng5vqaxobq6LB3YNtevDFSAZGlgtLoAc81PjUfiFz36n4KMpwhtAd4he8ToSI3TGyuhyx5xmiWNYZFyw==}
+    engines: {node: '>=18'}
+    cpu: [arm64]
+    os: [freebsd]
+
+  '@esbuild/freebsd-arm64@0.23.1':
+    resolution: {integrity: sha512-h1k6yS8/pN/NHlMl5+v4XPfikhJulk4G+tKGFIOwURBSFzE8bixw1ebjluLOjfwtLqY0kewfjLSrO6tN2MgIhA==}
+    engines: {node: '>=18'}
+    cpu: [arm64]
+    os: [freebsd]
+
   '@esbuild/freebsd-x64@0.18.20':
     resolution: {integrity: sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==}
     engines: {node: '>=12'}
@@ -2342,12 +2187,24 @@ packages:
     cpu: [x64]
     os: [freebsd]
 
-  '@esbuild/freebsd-x64@0.20.2':
-    resolution: {integrity: sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==}
+  '@esbuild/freebsd-x64@0.21.5':
+    resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==}
     engines: {node: '>=12'}
     cpu: [x64]
     os: [freebsd]
 
+  '@esbuild/freebsd-x64@0.23.0':
+    resolution: {integrity: sha512-XKDVu8IsD0/q3foBzsXGt/KjD/yTKBCIwOHE1XwiXmrRwrX6Hbnd5Eqn/WvDekddK21tfszBSrE/WMaZh+1buQ==}
+    engines: {node: '>=18'}
+    cpu: [x64]
+    os: [freebsd]
+
+  '@esbuild/freebsd-x64@0.23.1':
+    resolution: {integrity: sha512-lK1eJeyk1ZX8UklqFd/3A60UuZ/6UVfGT2LuGo3Wp4/z7eRTRYY+0xOu2kpClP+vMTi9wKOfXi2vjUpO1Ro76g==}
+    engines: {node: '>=18'}
+    cpu: [x64]
+    os: [freebsd]
+
   '@esbuild/linux-arm64@0.18.20':
     resolution: {integrity: sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==}
     engines: {node: '>=12'}
@@ -2360,12 +2217,24 @@ packages:
     cpu: [arm64]
     os: [linux]
 
-  '@esbuild/linux-arm64@0.20.2':
-    resolution: {integrity: sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==}
+  '@esbuild/linux-arm64@0.21.5':
+    resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==}
     engines: {node: '>=12'}
     cpu: [arm64]
     os: [linux]
 
+  '@esbuild/linux-arm64@0.23.0':
+    resolution: {integrity: sha512-j1t5iG8jE7BhonbsEg5d9qOYcVZv/Rv6tghaXM/Ug9xahM0nX/H2gfu6X6z11QRTMT6+aywOMA8TDkhPo8aCGw==}
+    engines: {node: '>=18'}
+    cpu: [arm64]
+    os: [linux]
+
+  '@esbuild/linux-arm64@0.23.1':
+    resolution: {integrity: sha512-/93bf2yxencYDnItMYV/v116zff6UyTjo4EtEQjUBeGiVpMmffDNUyD9UN2zV+V3LRV3/on4xdZ26NKzn6754g==}
+    engines: {node: '>=18'}
+    cpu: [arm64]
+    os: [linux]
+
   '@esbuild/linux-arm@0.18.20':
     resolution: {integrity: sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==}
     engines: {node: '>=12'}
@@ -2378,12 +2247,24 @@ packages:
     cpu: [arm]
     os: [linux]
 
-  '@esbuild/linux-arm@0.20.2':
-    resolution: {integrity: sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==}
+  '@esbuild/linux-arm@0.21.5':
+    resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==}
     engines: {node: '>=12'}
     cpu: [arm]
     os: [linux]
 
+  '@esbuild/linux-arm@0.23.0':
+    resolution: {integrity: sha512-SEELSTEtOFu5LPykzA395Mc+54RMg1EUgXP+iw2SJ72+ooMwVsgfuwXo5Fn0wXNgWZsTVHwY2cg4Vi/bOD88qw==}
+    engines: {node: '>=18'}
+    cpu: [arm]
+    os: [linux]
+
+  '@esbuild/linux-arm@0.23.1':
+    resolution: {integrity: sha512-CXXkzgn+dXAPs3WBwE+Kvnrf4WECwBdfjfeYHpMeVxWE0EceB6vhWGShs6wi0IYEqMSIzdOF1XjQ/Mkm5d7ZdQ==}
+    engines: {node: '>=18'}
+    cpu: [arm]
+    os: [linux]
+
   '@esbuild/linux-ia32@0.18.20':
     resolution: {integrity: sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==}
     engines: {node: '>=12'}
@@ -2396,12 +2277,24 @@ packages:
     cpu: [ia32]
     os: [linux]
 
-  '@esbuild/linux-ia32@0.20.2':
-    resolution: {integrity: sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==}
+  '@esbuild/linux-ia32@0.21.5':
+    resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==}
     engines: {node: '>=12'}
     cpu: [ia32]
     os: [linux]
 
+  '@esbuild/linux-ia32@0.23.0':
+    resolution: {integrity: sha512-P7O5Tkh2NbgIm2R6x1zGJJsnacDzTFcRWZyTTMgFdVit6E98LTxO+v8LCCLWRvPrjdzXHx9FEOA8oAZPyApWUA==}
+    engines: {node: '>=18'}
+    cpu: [ia32]
+    os: [linux]
+
+  '@esbuild/linux-ia32@0.23.1':
+    resolution: {integrity: sha512-VTN4EuOHwXEkXzX5nTvVY4s7E/Krz7COC8xkftbbKRYAl96vPiUssGkeMELQMOnLOJ8k3BY1+ZY52tttZnHcXQ==}
+    engines: {node: '>=18'}
+    cpu: [ia32]
+    os: [linux]
+
   '@esbuild/linux-loong64@0.18.20':
     resolution: {integrity: sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==}
     engines: {node: '>=12'}
@@ -2414,12 +2307,24 @@ packages:
     cpu: [loong64]
     os: [linux]
 
-  '@esbuild/linux-loong64@0.20.2':
-    resolution: {integrity: sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==}
+  '@esbuild/linux-loong64@0.21.5':
+    resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==}
     engines: {node: '>=12'}
     cpu: [loong64]
     os: [linux]
 
+  '@esbuild/linux-loong64@0.23.0':
+    resolution: {integrity: sha512-InQwepswq6urikQiIC/kkx412fqUZudBO4SYKu0N+tGhXRWUqAx+Q+341tFV6QdBifpjYgUndV1hhMq3WeJi7A==}
+    engines: {node: '>=18'}
+    cpu: [loong64]
+    os: [linux]
+
+  '@esbuild/linux-loong64@0.23.1':
+    resolution: {integrity: sha512-Vx09LzEoBa5zDnieH8LSMRToj7ir/Jeq0Gu6qJ/1GcBq9GkfoEAoXvLiW1U9J1qE/Y/Oyaq33w5p2ZWrNNHNEw==}
+    engines: {node: '>=18'}
+    cpu: [loong64]
+    os: [linux]
+
   '@esbuild/linux-mips64el@0.18.20':
     resolution: {integrity: sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==}
     engines: {node: '>=12'}
@@ -2432,12 +2337,24 @@ packages:
     cpu: [mips64el]
     os: [linux]
 
-  '@esbuild/linux-mips64el@0.20.2':
-    resolution: {integrity: sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==}
+  '@esbuild/linux-mips64el@0.21.5':
+    resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==}
     engines: {node: '>=12'}
     cpu: [mips64el]
     os: [linux]
 
+  '@esbuild/linux-mips64el@0.23.0':
+    resolution: {integrity: sha512-J9rflLtqdYrxHv2FqXE2i1ELgNjT+JFURt/uDMoPQLcjWQA5wDKgQA4t/dTqGa88ZVECKaD0TctwsUfHbVoi4w==}
+    engines: {node: '>=18'}
+    cpu: [mips64el]
+    os: [linux]
+
+  '@esbuild/linux-mips64el@0.23.1':
+    resolution: {integrity: sha512-nrFzzMQ7W4WRLNUOU5dlWAqa6yVeI0P78WKGUo7lg2HShq/yx+UYkeNSE0SSfSure0SqgnsxPvmAUu/vu0E+3Q==}
+    engines: {node: '>=18'}
+    cpu: [mips64el]
+    os: [linux]
+
   '@esbuild/linux-ppc64@0.18.20':
     resolution: {integrity: sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==}
     engines: {node: '>=12'}
@@ -2450,12 +2367,24 @@ packages:
     cpu: [ppc64]
     os: [linux]
 
-  '@esbuild/linux-ppc64@0.20.2':
-    resolution: {integrity: sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==}
+  '@esbuild/linux-ppc64@0.21.5':
+    resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==}
     engines: {node: '>=12'}
     cpu: [ppc64]
     os: [linux]
 
+  '@esbuild/linux-ppc64@0.23.0':
+    resolution: {integrity: sha512-cShCXtEOVc5GxU0fM+dsFD10qZ5UpcQ8AM22bYj0u/yaAykWnqXJDpd77ublcX6vdDsWLuweeuSNZk4yUxZwtw==}
+    engines: {node: '>=18'}
+    cpu: [ppc64]
+    os: [linux]
+
+  '@esbuild/linux-ppc64@0.23.1':
+    resolution: {integrity: sha512-dKN8fgVqd0vUIjxuJI6P/9SSSe/mB9rvA98CSH2sJnlZ/OCZWO1DJvxj8jvKTfYUdGfcq2dDxoKaC6bHuTlgcw==}
+    engines: {node: '>=18'}
+    cpu: [ppc64]
+    os: [linux]
+
   '@esbuild/linux-riscv64@0.18.20':
     resolution: {integrity: sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==}
     engines: {node: '>=12'}
@@ -2468,12 +2397,24 @@ packages:
     cpu: [riscv64]
     os: [linux]
 
-  '@esbuild/linux-riscv64@0.20.2':
-    resolution: {integrity: sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==}
+  '@esbuild/linux-riscv64@0.21.5':
+    resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==}
     engines: {node: '>=12'}
     cpu: [riscv64]
     os: [linux]
 
+  '@esbuild/linux-riscv64@0.23.0':
+    resolution: {integrity: sha512-HEtaN7Y5UB4tZPeQmgz/UhzoEyYftbMXrBCUjINGjh3uil+rB/QzzpMshz3cNUxqXN7Vr93zzVtpIDL99t9aRw==}
+    engines: {node: '>=18'}
+    cpu: [riscv64]
+    os: [linux]
+
+  '@esbuild/linux-riscv64@0.23.1':
+    resolution: {integrity: sha512-5AV4Pzp80fhHL83JM6LoA6pTQVWgB1HovMBsLQ9OZWLDqVY8MVobBXNSmAJi//Csh6tcY7e7Lny2Hg1tElMjIA==}
+    engines: {node: '>=18'}
+    cpu: [riscv64]
+    os: [linux]
+
   '@esbuild/linux-s390x@0.18.20':
     resolution: {integrity: sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==}
     engines: {node: '>=12'}
@@ -2486,12 +2427,24 @@ packages:
     cpu: [s390x]
     os: [linux]
 
-  '@esbuild/linux-s390x@0.20.2':
-    resolution: {integrity: sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==}
+  '@esbuild/linux-s390x@0.21.5':
+    resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==}
     engines: {node: '>=12'}
     cpu: [s390x]
     os: [linux]
 
+  '@esbuild/linux-s390x@0.23.0':
+    resolution: {integrity: sha512-WDi3+NVAuyjg/Wxi+o5KPqRbZY0QhI9TjrEEm+8dmpY9Xir8+HE/HNx2JoLckhKbFopW0RdO2D72w8trZOV+Wg==}
+    engines: {node: '>=18'}
+    cpu: [s390x]
+    os: [linux]
+
+  '@esbuild/linux-s390x@0.23.1':
+    resolution: {integrity: sha512-9ygs73tuFCe6f6m/Tb+9LtYxWR4c9yg7zjt2cYkjDbDpV/xVn+68cQxMXCjUpYwEkze2RcU/rMnfIXNRFmSoDw==}
+    engines: {node: '>=18'}
+    cpu: [s390x]
+    os: [linux]
+
   '@esbuild/linux-x64@0.18.20':
     resolution: {integrity: sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==}
     engines: {node: '>=12'}
@@ -2504,12 +2457,24 @@ packages:
     cpu: [x64]
     os: [linux]
 
-  '@esbuild/linux-x64@0.20.2':
-    resolution: {integrity: sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==}
+  '@esbuild/linux-x64@0.21.5':
+    resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==}
     engines: {node: '>=12'}
     cpu: [x64]
     os: [linux]
 
+  '@esbuild/linux-x64@0.23.0':
+    resolution: {integrity: sha512-a3pMQhUEJkITgAw6e0bWA+F+vFtCciMjW/LPtoj99MhVt+Mfb6bbL9hu2wmTZgNd994qTAEw+U/r6k3qHWWaOQ==}
+    engines: {node: '>=18'}
+    cpu: [x64]
+    os: [linux]
+
+  '@esbuild/linux-x64@0.23.1':
+    resolution: {integrity: sha512-EV6+ovTsEXCPAp58g2dD68LxoP/wK5pRvgy0J/HxPGB009omFPv3Yet0HiaqvrIrgPTBuC6wCH1LTOY91EO5hQ==}
+    engines: {node: '>=18'}
+    cpu: [x64]
+    os: [linux]
+
   '@esbuild/netbsd-x64@0.18.20':
     resolution: {integrity: sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==}
     engines: {node: '>=12'}
@@ -2522,12 +2487,36 @@ packages:
     cpu: [x64]
     os: [netbsd]
 
-  '@esbuild/netbsd-x64@0.20.2':
-    resolution: {integrity: sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==}
+  '@esbuild/netbsd-x64@0.21.5':
+    resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==}
     engines: {node: '>=12'}
     cpu: [x64]
     os: [netbsd]
 
+  '@esbuild/netbsd-x64@0.23.0':
+    resolution: {integrity: sha512-cRK+YDem7lFTs2Q5nEv/HHc4LnrfBCbH5+JHu6wm2eP+d8OZNoSMYgPZJq78vqQ9g+9+nMuIsAO7skzphRXHyw==}
+    engines: {node: '>=18'}
+    cpu: [x64]
+    os: [netbsd]
+
+  '@esbuild/netbsd-x64@0.23.1':
+    resolution: {integrity: sha512-aevEkCNu7KlPRpYLjwmdcuNz6bDFiE7Z8XC4CPqExjTvrHugh28QzUXVOZtiYghciKUacNktqxdpymplil1beA==}
+    engines: {node: '>=18'}
+    cpu: [x64]
+    os: [netbsd]
+
+  '@esbuild/openbsd-arm64@0.23.0':
+    resolution: {integrity: sha512-suXjq53gERueVWu0OKxzWqk7NxiUWSUlrxoZK7usiF50C6ipColGR5qie2496iKGYNLhDZkPxBI3erbnYkU0rQ==}
+    engines: {node: '>=18'}
+    cpu: [arm64]
+    os: [openbsd]
+
+  '@esbuild/openbsd-arm64@0.23.1':
+    resolution: {integrity: sha512-3x37szhLexNA4bXhLrCC/LImN/YtWis6WXr1VESlfVtVeoFJBRINPJ3f0a/6LV8zpikqoUg4hyXw0sFBt5Cr+Q==}
+    engines: {node: '>=18'}
+    cpu: [arm64]
+    os: [openbsd]
+
   '@esbuild/openbsd-x64@0.18.20':
     resolution: {integrity: sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==}
     engines: {node: '>=12'}
@@ -2540,12 +2529,24 @@ packages:
     cpu: [x64]
     os: [openbsd]
 
-  '@esbuild/openbsd-x64@0.20.2':
-    resolution: {integrity: sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==}
+  '@esbuild/openbsd-x64@0.21.5':
+    resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==}
     engines: {node: '>=12'}
     cpu: [x64]
     os: [openbsd]
 
+  '@esbuild/openbsd-x64@0.23.0':
+    resolution: {integrity: sha512-6p3nHpby0DM/v15IFKMjAaayFhqnXV52aEmv1whZHX56pdkK+MEaLoQWj+H42ssFarP1PcomVhbsR4pkz09qBg==}
+    engines: {node: '>=18'}
+    cpu: [x64]
+    os: [openbsd]
+
+  '@esbuild/openbsd-x64@0.23.1':
+    resolution: {integrity: sha512-aY2gMmKmPhxfU+0EdnN+XNtGbjfQgwZj43k8G3fyrDM/UdZww6xrWxmDkuz2eCZchqVeABjV5BpildOrUbBTqA==}
+    engines: {node: '>=18'}
+    cpu: [x64]
+    os: [openbsd]
+
   '@esbuild/sunos-x64@0.18.20':
     resolution: {integrity: sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==}
     engines: {node: '>=12'}
@@ -2558,12 +2559,24 @@ packages:
     cpu: [x64]
     os: [sunos]
 
-  '@esbuild/sunos-x64@0.20.2':
-    resolution: {integrity: sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==}
+  '@esbuild/sunos-x64@0.21.5':
+    resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==}
     engines: {node: '>=12'}
     cpu: [x64]
     os: [sunos]
 
+  '@esbuild/sunos-x64@0.23.0':
+    resolution: {integrity: sha512-BFelBGfrBwk6LVrmFzCq1u1dZbG4zy/Kp93w2+y83Q5UGYF1d8sCzeLI9NXjKyujjBBniQa8R8PzLFAUrSM9OA==}
+    engines: {node: '>=18'}
+    cpu: [x64]
+    os: [sunos]
+
+  '@esbuild/sunos-x64@0.23.1':
+    resolution: {integrity: sha512-RBRT2gqEl0IKQABT4XTj78tpk9v7ehp+mazn2HbUeZl1YMdaGAQqhapjGTCe7uw7y0frDi4gS0uHzhvpFuI1sA==}
+    engines: {node: '>=18'}
+    cpu: [x64]
+    os: [sunos]
+
   '@esbuild/win32-arm64@0.18.20':
     resolution: {integrity: sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==}
     engines: {node: '>=12'}
@@ -2576,12 +2589,24 @@ packages:
     cpu: [arm64]
     os: [win32]
 
-  '@esbuild/win32-arm64@0.20.2':
-    resolution: {integrity: sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==}
+  '@esbuild/win32-arm64@0.21.5':
+    resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==}
     engines: {node: '>=12'}
     cpu: [arm64]
     os: [win32]
 
+  '@esbuild/win32-arm64@0.23.0':
+    resolution: {integrity: sha512-lY6AC8p4Cnb7xYHuIxQ6iYPe6MfO2CC43XXKo9nBXDb35krYt7KGhQnOkRGar5psxYkircpCqfbNDB4uJbS2jQ==}
+    engines: {node: '>=18'}
+    cpu: [arm64]
+    os: [win32]
+
+  '@esbuild/win32-arm64@0.23.1':
+    resolution: {integrity: sha512-4O+gPR5rEBe2FpKOVyiJ7wNDPA8nGzDuJ6gN4okSA1gEOYZ67N8JPk58tkWtdtPeLz7lBnY6I5L3jdsr3S+A6A==}
+    engines: {node: '>=18'}
+    cpu: [arm64]
+    os: [win32]
+
   '@esbuild/win32-ia32@0.18.20':
     resolution: {integrity: sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==}
     engines: {node: '>=12'}
@@ -2594,12 +2619,24 @@ packages:
     cpu: [ia32]
     os: [win32]
 
-  '@esbuild/win32-ia32@0.20.2':
-    resolution: {integrity: sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==}
+  '@esbuild/win32-ia32@0.21.5':
+    resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==}
     engines: {node: '>=12'}
     cpu: [ia32]
     os: [win32]
 
+  '@esbuild/win32-ia32@0.23.0':
+    resolution: {integrity: sha512-7L1bHlOTcO4ByvI7OXVI5pNN6HSu6pUQq9yodga8izeuB1KcT2UkHaH6118QJwopExPn0rMHIseCTx1CRo/uNA==}
+    engines: {node: '>=18'}
+    cpu: [ia32]
+    os: [win32]
+
+  '@esbuild/win32-ia32@0.23.1':
+    resolution: {integrity: sha512-BcaL0Vn6QwCwre3Y717nVHZbAa4UBEigzFm6VdsVdT/MbZ38xoj1X9HPkZhbmaBGUD1W8vxAfffbDe8bA6AKnQ==}
+    engines: {node: '>=18'}
+    cpu: [ia32]
+    os: [win32]
+
   '@esbuild/win32-x64@0.18.20':
     resolution: {integrity: sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==}
     engines: {node: '>=12'}
@@ -2612,96 +2649,124 @@ packages:
     cpu: [x64]
     os: [win32]
 
-  '@esbuild/win32-x64@0.20.2':
-    resolution: {integrity: sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==}
+  '@esbuild/win32-x64@0.21.5':
+    resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==}
     engines: {node: '>=12'}
     cpu: [x64]
     os: [win32]
 
+  '@esbuild/win32-x64@0.23.0':
+    resolution: {integrity: sha512-Arm+WgUFLUATuoxCJcahGuk6Yj9Pzxd6l11Zb/2aAuv5kWWvvfhLFo2fni4uSK5vzlUdCGZ/BdV5tH8klj8p8g==}
+    engines: {node: '>=18'}
+    cpu: [x64]
+    os: [win32]
+
+  '@esbuild/win32-x64@0.23.1':
+    resolution: {integrity: sha512-BHpFFeslkWrXWyUPnbKm+xYYVYruCinGcftSBaa8zoF9hZO4BcSCFUvHVTtzpIY6YzUnYtuEhZ+C9iEXjxnasg==}
+    engines: {node: '>=18'}
+    cpu: [x64]
+    os: [win32]
+
   '@eslint-community/eslint-utils@4.4.0':
     resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==}
     engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
     peerDependencies:
       eslint: ^6.0.0 || ^7.0.0 || >=8.0.0
 
-  '@eslint-community/regexpp@4.10.0':
-    resolution: {integrity: sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==}
+  '@eslint-community/regexpp@4.11.0':
+    resolution: {integrity: sha512-G/M/tIiMrTAxEWRfLfQJMmGNX28IxBg4PBz8XqQhqUHLFI6TL2htpIB1iQCj144V5ee/JaKyT9/WZ0MGZWfA7A==}
     engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0}
 
   '@eslint-community/regexpp@4.6.2':
     resolution: {integrity: sha512-pPTNuaAG3QMH+buKyBIGJs3g/S5y0caxw0ygM3YyE6yJFySwiGGSzA+mM3KJ8QQvzeLh3blwgSonkFjgQdxzMw==}
     engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0}
 
-  '@eslint/eslintrc@2.1.4':
-    resolution: {integrity: sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==}
-    engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
+  '@eslint/compat@1.1.1':
+    resolution: {integrity: sha512-lpHyRyplhGPL5mGEh6M9O5nnKk0Gz4bFI+Zu6tKlPpDUN7XshWvH9C/px4UVm87IAANE0W81CEsNGbS1KlzXpA==}
+    engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
 
-  '@eslint/js@8.53.0':
-    resolution: {integrity: sha512-Kn7K8dx/5U6+cT1yEhpX1w4PCSg0M+XyRILPgvwcEBjerFWCwQj5sbr3/VmxqV0JGHCBCzyd6LxypEuehypY1w==}
-    engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
+  '@eslint/config-array@0.17.1':
+    resolution: {integrity: sha512-BlYOpej8AQ8Ev9xVqroV7a02JK3SkBAaN9GfMMH9W6Ch8FlQlkjGw4Ir7+FgYwfirivAf4t+GtzuAxqfukmISA==}
+    engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
 
-  '@eslint/js@8.57.0':
-    resolution: {integrity: sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==}
-    engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
+  '@eslint/config-array@0.18.0':
+    resolution: {integrity: sha512-fTxvnS1sRMu3+JjXwJG0j/i4RT9u4qJ+lqS/yCGap4lH4zZGzQ7tu+xZqQmcMZq5OBZDL4QRxQzRjkWcGt8IVw==}
+    engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
 
-  '@fal-works/esbuild-plugin-global-externals@2.1.2':
-    resolution: {integrity: sha512-cEee/Z+I12mZcFJshKcCqC8tuX5hG3s+d+9nZ3LabqKF1vKdF41B92pJVCBggjAGORAeOzyyDDKrZwIkLffeOQ==}
+  '@eslint/eslintrc@3.1.0':
+    resolution: {integrity: sha512-4Bfj15dVJdoy3RfZmmo86RK1Fwzn6SstsvK9JS+BaVKqC6QQQQyXekNaC+g+LKNgkQ+2VhGAzm6hO40AhMR3zQ==}
+    engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
 
-  '@fastify/accept-negotiator@1.0.0':
-    resolution: {integrity: sha512-4R/N2KfYeld7A5LGkai+iUFMahXcxxYbDp+XS2B1yuL3cdmZLJ9TlCnNzT3q5xFTqsYm0GPpinLUwfSwjcVjyA==}
-    engines: {node: '>=14'}
+  '@eslint/js@9.11.0':
+    resolution: {integrity: sha512-LPkkenkDqyzTFauZLLAPhIb48fj6drrfMvRGSL9tS3AcZBSVTllemLSNyCvHNNL2t797S/6DJNSIwRwXgMO/eQ==}
+    engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
 
-  '@fastify/accepts@4.3.0':
-    resolution: {integrity: sha512-QK4FoqXdwwPmaPOLL6NrxsyaXVvdviYVoS6ltHyOLdFlUyREIaMykHQIp+x0aJz9hB3B3n/Ht6QRdvBeGkptGQ==}
+  '@eslint/js@9.8.0':
+    resolution: {integrity: sha512-MfluB7EUfxXtv3i/++oh89uzAr4PDI4nn201hsp+qaXqsjAWzinlZEHEfPgAX4doIlKvPG/i0A9dpKxOLII8yA==}
+    engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
 
-  '@fastify/ajv-compiler@3.5.0':
-    resolution: {integrity: sha512-ebbEtlI7dxXF5ziNdr05mOY8NnDiPB1XvAlLHctRt/Rc+C3LCOVW5imUVX+mhvUhnNzmPBHewUkOFgGlCxgdAA==}
+  '@eslint/object-schema@2.1.4':
+    resolution: {integrity: sha512-BsWiH1yFGjXXS2yvrf5LyuoSIIbPrGUWob917o+BTKuZ7qJdxX8aJLRxs1fS9n6r7vESrq1OUqb68dANcFXuQQ==}
+    engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+  '@eslint/plugin-kit@0.2.0':
+    resolution: {integrity: sha512-vH9PiIMMwvhCx31Af3HiGzsVNULDbyVkHXwlemn/B0TFj/00ho3y55efXrUZTfQipxoHC5u4xq6zblww1zm1Ig==}
+    engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+  '@fastify/accept-negotiator@2.0.0':
+    resolution: {integrity: sha512-/Sce/kBzuTxIq5tJh85nVNOq9wKD8s+viIgX0fFMDBdw95gnpf53qmF1oBgJym3cPFliWUuSloVg/1w/rH0FcQ==}
+
+  '@fastify/accepts@5.0.1':
+    resolution: {integrity: sha512-8ji2MGTbceSnAXKYx/U9iWt6Fmf0zJovh0meO5rpwYS/vy0Z3QIR2J/hKmbcTpYfMu5NUliNpsAtMavmzBQhmA==}
+
+  '@fastify/ajv-compiler@4.0.0':
+    resolution: {integrity: sha512-dt0jyLAlay14LpIn4Fg1SY7V5NJ9KH0YFDpYVQY5cgIVBvdI8908AMx5zQ0bBYPGT6Wh+bM3f2caMmOXLP3QsQ==}
 
   '@fastify/busboy@2.1.0':
     resolution: {integrity: sha512-+KpH+QxZU7O4675t3mnkQKcZZg56u+K/Ct2K+N2AZYNVK8kyeo/bI18tI8aPm3tvNNRyTWfj6s5tnGNlcbQRsA==}
     engines: {node: '>=14'}
 
-  '@fastify/cookie@9.3.1':
-    resolution: {integrity: sha512-h1NAEhB266+ZbZ0e9qUE6NnNR07i7DnNXWG9VbbZ8uC6O/hxHpl+Zoe5sw1yfdZ2U6XhToUGDnzQtWJdCaPwfg==}
+  '@fastify/busboy@3.0.0':
+    resolution: {integrity: sha512-83rnH2nCvclWaPQQKvkJ2pdOjG4TZyEVuFDnlOF6KP08lDaaceVyw/W63mDuafQT+MKHCvXIPpE5uYWeM0rT4w==}
 
-  '@fastify/cors@9.0.1':
-    resolution: {integrity: sha512-YY9Ho3ovI+QHIL2hW+9X4XqQjXLjJqsU+sMV/xFsxZkE8p3GNnYVFpoOxF7SsP5ZL76gwvbo3V9L+FIekBGU4Q==}
+  '@fastify/cookie@10.0.1':
+    resolution: {integrity: sha512-NV/wbCUv4ETJ5KM1KMu0fLx0nSCm9idIxwg66NZnNbfPQH3rdbx6k0qRs5uy0y+MhBgvDudYRA30KlK659chyw==}
 
-  '@fastify/deepmerge@1.3.0':
-    resolution: {integrity: sha512-J8TOSBq3SoZbDhM9+R/u77hP93gz/rajSA+K2kGyijPpORPWUXHUpTaleoj+92As0S9uPRP7Oi8IqMf0u+ro6A==}
+  '@fastify/cors@10.0.1':
+    resolution: {integrity: sha512-O8JIf6448uQbOgzSkCqhClw6gFTAqrdfeA6R3fc/3gwTJGUp7gl8/3tbNB+6INuu4RmgVOq99BmvdGbtu5pgOA==}
 
-  '@fastify/error@3.4.0':
-    resolution: {integrity: sha512-e/mafFwbK3MNqxUcFBLgHhgxsF8UT1m8aj0dAlqEa2nJEgPsRtpHTZ3ObgrgkZ2M1eJHPTwgyUl/tXkvabsZdQ==}
+  '@fastify/deepmerge@2.0.0':
+    resolution: {integrity: sha512-fsaybTGDyQ5KpPsplQqb9yKdCf2x/pbNpMNk8Tvp3rRz7lVcupKysH4b2ELMN2P4Hak1+UqTYdTj/u4FNV2p0g==}
 
-  '@fastify/express@3.0.0':
-    resolution: {integrity: sha512-Ug6aulXCUiHgMyrHVYQqnQbGdsAV0aTad6nZxbOr6w3QjKn1mdQS3Kyzvc+I0xMjZ9yIyMUWHSooHgZ0l7nOng==}
+  '@fastify/error@4.0.0':
+    resolution: {integrity: sha512-OO/SA8As24JtT1usTUTKgGH7uLvhfwZPwlptRi2Dp5P4KKmJI3gvsZ8MIHnNwDs4sLf/aai5LzTyl66xr7qMxA==}
 
-  '@fastify/fast-json-stringify-compiler@4.3.0':
-    resolution: {integrity: sha512-aZAXGYo6m22Fk1zZzEUKBvut/CIIQe/BapEORnxiD5Qr0kPHqqI69NtEMCme74h+at72sPhbkb4ZrLd1W3KRLA==}
+  '@fastify/express@4.0.1':
+    resolution: {integrity: sha512-mEQ6pawaENeZ3swqVtkxdLi8NQC5eKBkclE+7ma1qQMuB+yI6WxDyEp55pdbqPIqBQTN/cGgHv84qxVS7NKC2Q==}
 
-  '@fastify/http-proxy@9.5.0':
-    resolution: {integrity: sha512-1iqIdV10d5k9YtfHq9ylX5zt1NiM50fG+rIX40qt00R694sqWso3ukyTFZVk33SDoSiBW8roB7n11RUVUoN+Ag==}
+  '@fastify/fast-json-stringify-compiler@5.0.0':
+    resolution: {integrity: sha512-tywfuZfXsyxLC5kEqrMubbFa9vpAxNtuPE7j9w5si1r+6p5b981pDfZ5Y8HBqmjDQl+PABT7cV5jZgXI2j+I5g==}
 
-  '@fastify/multipart@8.2.0':
-    resolution: {integrity: sha512-OZ8nsyyoS2TV7Yeu3ZdrdDGsKUTAbfjrKC9jSxGgT2qdgek+BxpWX31ZubTrWMNZyU5xwk4ox6AvTjAbYWjrWg==}
+  '@fastify/http-proxy@10.0.0':
+    resolution: {integrity: sha512-n5/EPspNKtzpCUavuDflYtvtB+aEkablb2sZM83gDKbxM9GF+93maJYQrGozJ2HNRqpt7wfzsDeUuGVFFkYzMQ==}
 
-  '@fastify/reply-from@9.0.1':
-    resolution: {integrity: sha512-q9vFNUiXZTY1x8omDPe59os2MYq+3y7KgO/kZoXpZlnud+45Nd8Ot/svEvrUATzjkizIggfS4K8LR9zXDyZZKg==}
+  '@fastify/merge-json-schemas@0.1.1':
+    resolution: {integrity: sha512-fERDVz7topgNjtXsJTTW1JKLy0rhuLRcquYqNR9rF7OcVpCa2OVW49ZPDIhaRRCaUuvVxI+N416xUoF76HNSXA==}
 
-  '@fastify/send@2.0.1':
-    resolution: {integrity: sha512-8jdouu0o5d0FMq1+zCKeKXc1tmOQ5tTGYdQP3MpyF9+WWrZT1KCBdh6hvoEYxOm3oJG/akdE9BpehLiJgYRvGw==}
+  '@fastify/multipart@9.0.1':
+    resolution: {integrity: sha512-vt2gOCw/O4EwpN4KlLVJxth4iQlDf7T5ggw2Db2C+UbO2WJBG7y0jEBvu/HT6JIW/lBYaqrrUy9MmTpCKgXEpw==}
 
-  '@fastify/static@6.12.0':
-    resolution: {integrity: sha512-KK1B84E6QD/FcQWxDI2aiUCwHxMJBI1KeCUzm1BwYpPY1b742+jeKruGHP2uOluuM6OkBPI8CIANrXcCRtC2oQ==}
+  '@fastify/reply-from@11.0.0':
+    resolution: {integrity: sha512-dv3o8hyy4sxhg1RN9l6ueM+PMMaIPKLjtL2T99H5M7h1Xt8d1RX3r+xC+sL5AqJqLvReX4N+7mTq9QDeB8i6Lg==}
 
-  '@fastify/static@7.0.3':
-    resolution: {integrity: sha512-2tmTdF+uFCykasutaO6k4/wOt7eXyi7m3dGuCPo5micXzv0qt6ttb/nWnDYL/BlXjYGfp1JI4a1gyluTIylvQA==}
+  '@fastify/send@3.1.1':
+    resolution: {integrity: sha512-LdiV2mle/2tH8vh6GwGl0ubfUAgvY+9yF9oGI1iiwVyNUVOQamvw5n+OFu6iCNNoyuCY80FFURBn4TZCbTe8LA==}
 
-  '@fastify/view@8.2.0':
-    resolution: {integrity: sha512-hBSiBofCnJNlPHEMZWpO1SL84eqOaqujJ1hR3jntFyZZCkweH5jMs12DKYyGesjVll7SJFRRxPUBB8kmUmneRQ==}
+  '@fastify/static@8.0.1':
+    resolution: {integrity: sha512-7idyhbcgf14v4bjWzUeHEFvnVxvNJ1n5cyGPgFtwTZjnjUQ1wgC7a2FQai7OGKqCKywDEjzbPhAZRW+uEK1LMg==}
 
-  '@fastify/view@9.1.0':
-    resolution: {integrity: sha512-jRTGDljs/uB2p8bf6c1x4stGjP7H84VQkhbtDgCx55Mxf9Fplud5UZIHubvL4BTTX8jNYEzP1FpNAOBi7vibxg==}
+  '@fastify/view@10.0.1':
+    resolution: {integrity: sha512-rXtBN0oVDmoRZAS7lelrCIahf+qFtlMOOas8VPdA7JvrJ9ChcF7e36pIUPU0Vbs3KmHxESUb7XatavUZEe/k5Q==}
 
   '@github/webauthn-json@2.1.1':
     resolution: {integrity: sha512-XrftRn4z75SnaJOmZQbt7Mk+IIjqVHw+glDGOxuHwXkZBZh/MBoRS7MHjSZMDaLhT4RjN2VqiEU7EOYleuJWSQ==}
@@ -2713,11 +2778,8 @@ packages:
   '@hapi/bourne@3.0.0':
     resolution: {integrity: sha512-Waj1cwPXJDucOib4a3bAISsKJVb15MKi9IvmTI/7ssVEm6sywXGjVJDhl6/umt1pK1ZS7PacXU3A1PmFKHEZ2w==}
 
-  '@hapi/hoek@10.0.1':
-    resolution: {integrity: sha512-CvlW7jmOhWzuqOqiJQ3rQVLMcREh0eel4IBnxDx2FAcK8g7qoJRQK4L1CPBASoCY6y8e6zuCy3f2g+HWdkzcMw==}
-
-  '@hapi/hoek@11.0.2':
-    resolution: {integrity: sha512-aKmlCO57XFZ26wso4rJsW4oTUnrgTFw2jh3io7CAtO9w4UltBNwRXvXIVzzyfkaaLRo3nluP/19msA8vDUUuKw==}
+  '@hapi/hoek@11.0.4':
+    resolution: {integrity: sha512-PnsP5d4q7289pS2T2EgGz147BFJ2Jpb4yrEdkpz2IhgEUzos1S7HTl7ezWh1yfYzYlj89KzLdCRkqsP6SIryeQ==}
 
   '@hapi/hoek@9.3.0':
     resolution: {integrity: sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==}
@@ -2731,14 +2793,6 @@ packages:
   '@hexagon/base64@1.1.27':
     resolution: {integrity: sha512-PdUmzpvcUM3Rh39kvz9RdbPVYhMjBjdV7Suw7ZduP7urRLsZR8l5tzgSWKm7TExwBYDFwTnYrZbnE0rQ3N5NLQ==}
 
-  '@humanwhocodes/config-array@0.11.13':
-    resolution: {integrity: sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ==}
-    engines: {node: '>=10.10.0'}
-
-  '@humanwhocodes/config-array@0.11.14':
-    resolution: {integrity: sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==}
-    engines: {node: '>=10.10.0'}
-
   '@humanwhocodes/module-importer@1.0.1':
     resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==}
     engines: {node: '>=12.22'}
@@ -2747,122 +2801,112 @@ packages:
     resolution: {integrity: sha512-RE815I4arJFtt+FVeU1Tgp9/Xvecacji8w/V6XtXsWWH/wz/eNkNbhb+ny/+PlVZjV0rxQpRSQKNKE3lcktHEA==}
     engines: {node: '>=10.10.0'}
 
-  '@humanwhocodes/object-schema@2.0.1':
-    resolution: {integrity: sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==}
+  '@humanwhocodes/retry@0.3.0':
+    resolution: {integrity: sha512-d2CGZR2o7fS6sWB7DG/3a95bGKQyHMACZ5aW8qGkkqQpUoZV6C0X7Pc7l4ZNMZkfNBf4VWNe9E1jRsf0G146Ew==}
+    engines: {node: '>=18.18'}
 
-  '@humanwhocodes/object-schema@2.0.2':
-    resolution: {integrity: sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==}
-
-  '@img/sharp-darwin-arm64@0.33.3':
-    resolution: {integrity: sha512-FaNiGX1MrOuJ3hxuNzWgsT/mg5OHG/Izh59WW2mk1UwYHUwtfbhk5QNKYZgxf0pLOhx9ctGiGa2OykD71vOnSw==}
-    engines: {glibc: '>=2.26', node: ^18.17.0 || ^20.3.0 || >=21.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'}
+  '@img/sharp-darwin-arm64@0.33.5':
+    resolution: {integrity: sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==}
+    engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
     cpu: [arm64]
     os: [darwin]
 
-  '@img/sharp-darwin-x64@0.33.3':
-    resolution: {integrity: sha512-2QeSl7QDK9ru//YBT4sQkoq7L0EAJZA3rtV+v9p8xTKl4U1bUqTIaCnoC7Ctx2kCjQgwFXDasOtPTCT8eCTXvw==}
-    engines: {glibc: '>=2.26', node: ^18.17.0 || ^20.3.0 || >=21.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'}
+  '@img/sharp-darwin-x64@0.33.5':
+    resolution: {integrity: sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==}
+    engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
     cpu: [x64]
     os: [darwin]
 
-  '@img/sharp-libvips-darwin-arm64@1.0.2':
-    resolution: {integrity: sha512-tcK/41Rq8IKlSaKRCCAuuY3lDJjQnYIW1UXU1kxcEKrfL8WR7N6+rzNoOxoQRJWTAECuKwgAHnPvqXGN8XfkHA==}
-    engines: {macos: '>=11', npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'}
+  '@img/sharp-libvips-darwin-arm64@1.0.4':
+    resolution: {integrity: sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==}
     cpu: [arm64]
     os: [darwin]
 
-  '@img/sharp-libvips-darwin-x64@1.0.2':
-    resolution: {integrity: sha512-Ofw+7oaWa0HiiMiKWqqaZbaYV3/UGL2wAPeLuJTx+9cXpCRdvQhCLG0IH8YGwM0yGWGLpsF4Su9vM1o6aer+Fw==}
-    engines: {macos: '>=10.13', npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'}
+  '@img/sharp-libvips-darwin-x64@1.0.4':
+    resolution: {integrity: sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==}
     cpu: [x64]
     os: [darwin]
 
-  '@img/sharp-libvips-linux-arm64@1.0.2':
-    resolution: {integrity: sha512-x7kCt3N00ofFmmkkdshwj3vGPCnmiDh7Gwnd4nUwZln2YjqPxV1NlTyZOvoDWdKQVDL911487HOueBvrpflagw==}
-    engines: {glibc: '>=2.26', npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'}
+  '@img/sharp-libvips-linux-arm64@1.0.4':
+    resolution: {integrity: sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==}
     cpu: [arm64]
     os: [linux]
 
-  '@img/sharp-libvips-linux-arm@1.0.2':
-    resolution: {integrity: sha512-iLWCvrKgeFoglQxdEwzu1eQV04o8YeYGFXtfWU26Zr2wWT3q3MTzC+QTCO3ZQfWd3doKHT4Pm2kRmLbupT+sZw==}
-    engines: {glibc: '>=2.28', npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'}
+  '@img/sharp-libvips-linux-arm@1.0.5':
+    resolution: {integrity: sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==}
     cpu: [arm]
     os: [linux]
 
-  '@img/sharp-libvips-linux-s390x@1.0.2':
-    resolution: {integrity: sha512-cmhQ1J4qVhfmS6szYW7RT+gLJq9dH2i4maq+qyXayUSn9/3iY2ZeWpbAgSpSVbV2E1JUL2Gg7pwnYQ1h8rQIog==}
-    engines: {glibc: '>=2.28', npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'}
+  '@img/sharp-libvips-linux-s390x@1.0.4':
+    resolution: {integrity: sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==}
     cpu: [s390x]
     os: [linux]
 
-  '@img/sharp-libvips-linux-x64@1.0.2':
-    resolution: {integrity: sha512-E441q4Qdb+7yuyiADVi5J+44x8ctlrqn8XgkDTwr4qPJzWkaHwD489iZ4nGDgcuya4iMN3ULV6NwbhRZJ9Z7SQ==}
-    engines: {glibc: '>=2.26', npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'}
+  '@img/sharp-libvips-linux-x64@1.0.4':
+    resolution: {integrity: sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==}
     cpu: [x64]
     os: [linux]
 
-  '@img/sharp-libvips-linuxmusl-arm64@1.0.2':
-    resolution: {integrity: sha512-3CAkndNpYUrlDqkCM5qhksfE+qSIREVpyoeHIU6jd48SJZViAmznoQQLAv4hVXF7xyUB9zf+G++e2v1ABjCbEQ==}
-    engines: {musl: '>=1.2.2', npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'}
+  '@img/sharp-libvips-linuxmusl-arm64@1.0.4':
+    resolution: {integrity: sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==}
     cpu: [arm64]
     os: [linux]
 
-  '@img/sharp-libvips-linuxmusl-x64@1.0.2':
-    resolution: {integrity: sha512-VI94Q6khIHqHWNOh6LLdm9s2Ry4zdjWJwH56WoiJU7NTeDwyApdZZ8c+SADC8OH98KWNQXnE01UdJ9CSfZvwZw==}
-    engines: {musl: '>=1.2.2', npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'}
+  '@img/sharp-libvips-linuxmusl-x64@1.0.4':
+    resolution: {integrity: sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==}
     cpu: [x64]
     os: [linux]
 
-  '@img/sharp-linux-arm64@0.33.3':
-    resolution: {integrity: sha512-Zf+sF1jHZJKA6Gor9hoYG2ljr4wo9cY4twaxgFDvlG0Xz9V7sinsPp8pFd1XtlhTzYo0IhDbl3rK7P6MzHpnYA==}
-    engines: {glibc: '>=2.26', node: ^18.17.0 || ^20.3.0 || >=21.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'}
+  '@img/sharp-linux-arm64@0.33.5':
+    resolution: {integrity: sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==}
+    engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
     cpu: [arm64]
     os: [linux]
 
-  '@img/sharp-linux-arm@0.33.3':
-    resolution: {integrity: sha512-Q7Ee3fFSC9P7vUSqVEF0zccJsZ8GiiCJYGWDdhEjdlOeS9/jdkyJ6sUSPj+bL8VuOYFSbofrW0t/86ceVhx32w==}
-    engines: {glibc: '>=2.28', node: ^18.17.0 || ^20.3.0 || >=21.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'}
+  '@img/sharp-linux-arm@0.33.5':
+    resolution: {integrity: sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==}
+    engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
     cpu: [arm]
     os: [linux]
 
-  '@img/sharp-linux-s390x@0.33.3':
-    resolution: {integrity: sha512-vFk441DKRFepjhTEH20oBlFrHcLjPfI8B0pMIxGm3+yilKyYeHEVvrZhYFdqIseSclIqbQ3SnZMwEMWonY5XFA==}
-    engines: {glibc: '>=2.28', node: ^18.17.0 || ^20.3.0 || >=21.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'}
+  '@img/sharp-linux-s390x@0.33.5':
+    resolution: {integrity: sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==}
+    engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
     cpu: [s390x]
     os: [linux]
 
-  '@img/sharp-linux-x64@0.33.3':
-    resolution: {integrity: sha512-Q4I++herIJxJi+qmbySd072oDPRkCg/SClLEIDh5IL9h1zjhqjv82H0Seupd+q2m0yOfD+/fJnjSoDFtKiHu2g==}
-    engines: {glibc: '>=2.26', node: ^18.17.0 || ^20.3.0 || >=21.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'}
+  '@img/sharp-linux-x64@0.33.5':
+    resolution: {integrity: sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==}
+    engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
     cpu: [x64]
     os: [linux]
 
-  '@img/sharp-linuxmusl-arm64@0.33.3':
-    resolution: {integrity: sha512-qnDccehRDXadhM9PM5hLvcPRYqyFCBN31kq+ErBSZtZlsAc1U4Z85xf/RXv1qolkdu+ibw64fUDaRdktxTNP9A==}
-    engines: {musl: '>=1.2.2', node: ^18.17.0 || ^20.3.0 || >=21.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'}
+  '@img/sharp-linuxmusl-arm64@0.33.5':
+    resolution: {integrity: sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==}
+    engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
     cpu: [arm64]
     os: [linux]
 
-  '@img/sharp-linuxmusl-x64@0.33.3':
-    resolution: {integrity: sha512-Jhchim8kHWIU/GZ+9poHMWRcefeaxFIs9EBqf9KtcC14Ojk6qua7ghKiPs0sbeLbLj/2IGBtDcxHyjCdYWkk2w==}
-    engines: {musl: '>=1.2.2', node: ^18.17.0 || ^20.3.0 || >=21.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'}
+  '@img/sharp-linuxmusl-x64@0.33.5':
+    resolution: {integrity: sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==}
+    engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
     cpu: [x64]
     os: [linux]
 
-  '@img/sharp-wasm32@0.33.3':
-    resolution: {integrity: sha512-68zivsdJ0koE96stdUfM+gmyaK/NcoSZK5dV5CAjES0FUXS9lchYt8LAB5rTbM7nlWtxaU/2GON0HVN6/ZYJAQ==}
-    engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'}
+  '@img/sharp-wasm32@0.33.5':
+    resolution: {integrity: sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==}
+    engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
     cpu: [wasm32]
 
-  '@img/sharp-win32-ia32@0.33.3':
-    resolution: {integrity: sha512-CyimAduT2whQD8ER4Ux7exKrtfoaUiVr7HG0zZvO0XTFn2idUWljjxv58GxNTkFb8/J9Ub9AqITGkJD6ZginxQ==}
-    engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'}
+  '@img/sharp-win32-ia32@0.33.5':
+    resolution: {integrity: sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==}
+    engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
     cpu: [ia32]
     os: [win32]
 
-  '@img/sharp-win32-x64@0.33.3':
-    resolution: {integrity: sha512-viT4fUIDKnli3IfOephGnolMzhz5VaTvDRkYqtZxOMIoMQ4MrAziO7pT1nVnOt2FAm7qW5aa+CCc13aEY6Le0g==}
-    engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'}
+  '@img/sharp-win32-x64@0.33.5':
+    resolution: {integrity: sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==}
+    engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
     cpu: [x64]
     os: [win32]
 
@@ -2882,18 +2926,6 @@ packages:
     resolution: {integrity: sha512-Pe3PFccjPVJV1vtlfVvm9OnlbxqdnP5QcscFEFEnK5quChf1ufZtM0r8mR5ToWHMxZOh0s8o/qp9ANGRTo/DAw==}
     engines: {node: '>=18'}
 
-  '@intlify/core-base@9.13.1':
-    resolution: {integrity: sha512-+bcQRkJO9pcX8d0gel9ZNfrzU22sZFSA0WVhfXrf5jdJOS24a+Bp8pozuS9sBI9Hk/tGz83pgKfmqcn/Ci7/8w==}
-    engines: {node: '>= 16'}
-
-  '@intlify/message-compiler@9.13.1':
-    resolution: {integrity: sha512-SKsVa4ajYGBVm7sHMXd5qX70O2XXjm55zdZB3VeMFCvQyvLew/dLvq3MqnaIsTMF1VkkOb9Ttr6tHcMlyPDL9w==}
-    engines: {node: '>= 16'}
-
-  '@intlify/shared@9.13.1':
-    resolution: {integrity: sha512-u3b6BKGhE6j/JeRU6C/RL2FgyJfy6LakbtfeVF8fJXURpZZTzfh3e05J0bu0XPw447Q6/WUp3C4ajv4TMS4YsQ==}
-    engines: {node: '>= 16'}
-
   '@ioredis/commands@1.2.0':
     resolution: {integrity: sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==}
 
@@ -2992,6 +3024,10 @@ packages:
     resolution: {integrity: sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==}
     engines: {node: '>=6.0.0'}
 
+  '@jridgewell/gen-mapping@0.3.5':
+    resolution: {integrity: sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==}
+    engines: {node: '>=6.0.0'}
+
   '@jridgewell/resolve-uri@3.1.0':
     resolution: {integrity: sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==}
     engines: {node: '>=6.0.0'}
@@ -3000,8 +3036,12 @@ packages:
     resolution: {integrity: sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==}
     engines: {node: '>=6.0.0'}
 
-  '@jridgewell/source-map@0.3.5':
-    resolution: {integrity: sha512-UTYAUj/wviwdsMfzoSJspJxbkH5o1snzwX0//0ENX1u/55kkZZkcTZP6u9bwKGkv+dkk9at4m1Cpt0uY80kcpQ==}
+  '@jridgewell/set-array@1.2.1':
+    resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==}
+    engines: {node: '>=6.0.0'}
+
+  '@jridgewell/source-map@0.3.6':
+    resolution: {integrity: sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==}
 
   '@jridgewell/sourcemap-codec@1.4.14':
     resolution: {integrity: sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==}
@@ -3009,9 +3049,15 @@ packages:
   '@jridgewell/sourcemap-codec@1.4.15':
     resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==}
 
+  '@jridgewell/sourcemap-codec@1.5.0':
+    resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==}
+
   '@jridgewell/trace-mapping@0.3.18':
     resolution: {integrity: sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA==}
 
+  '@jridgewell/trace-mapping@0.3.25':
+    resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==}
+
   '@jsdevtools/ono@7.1.3':
     resolution: {integrity: sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==}
 
@@ -3025,8 +3071,8 @@ packages:
     resolution: {integrity: sha512-uSvJdwQU5nK+Vdf6zxcWAY2A8r7uqe+gePwLWzJ+fsQehq18pc0I2hJKwypZ2aLM90+Er9u1xn4iLJPZ+xlL4g==}
     engines: {node: '>=8'}
 
-  '@lukeed/ms@2.0.1':
-    resolution: {integrity: sha512-Xs/4RZltsAL7pkvaNStUQt7netTkyxrS0K+RILcVr3TRMS/ToOg4I6uNfhB9SlGsnWBym4U+EaXq0f0cEMNkHA==}
+  '@lukeed/ms@2.0.2':
+    resolution: {integrity: sha512-9I2Zn6+NJLfaGoz9jN3lpwDgAYvfGeNYdbAIjJOqzs4Tpc+VU3Jqq4IofSUBKajiDS8k9fZIg18/z13mpk1bsA==}
     engines: {node: '>=8'}
 
   '@mapbox/node-pre-gyp@1.0.9':
@@ -3045,29 +3091,31 @@ packages:
       '@types/react': '>=16'
       react: '>=16'
 
-  '@microsoft/api-extractor-model@7.28.14':
-    resolution: {integrity: sha512-Bery/c8A8SsKPSvA82cTTuy/+OcxZbLRmKhPkk91/AJOQzxZsShcrmHFAGeiEqSIrv1nPZ3tKq9kfMLdCHmsqg==}
+  '@microsoft/api-extractor-model@7.29.8':
+    resolution: {integrity: sha512-t3Z/xcO6TRbMcnKGVMs4uMzv/gd5j0NhMiJIGjD4cJMeFJ1Hf8wnLSx37vxlRlL0GWlGJhnFgxvnaL6JlS+73g==}
 
-  '@microsoft/api-extractor@7.43.1':
-    resolution: {integrity: sha512-ohg40SsvFFgzHFAtYq5wKJc8ZDyY46bphjtnSvhSSlXpPTG7GHwyyXkn48UZiUCBwr2WC7TRC1Jfwz7nreuiyQ==}
+  '@microsoft/api-extractor@7.47.9':
+    resolution: {integrity: sha512-TTq30M1rikVsO5wZVToQT/dGyJY7UXJmjiRtkHPLb74Prx3Etw8+bX7Bv7iLuby6ysb7fuu1NFWqma+csym8Jw==}
     hasBin: true
 
-  '@microsoft/tsdoc-config@0.16.2':
-    resolution: {integrity: sha512-OGiIzzoBLgWWR0UdRJX98oYO+XKGf7tiK4Zk6tQ/E4IJqGCe7dvkTvgDZV5cFJUzLGDOjeAXrnZoA6QkVySuxw==}
+  '@microsoft/tsdoc-config@0.17.0':
+    resolution: {integrity: sha512-v/EYRXnCAIHxOHW+Plb6OWuUoMotxTN0GLatnpOb1xq0KuTNw/WI3pamJx/UbsoJP5k9MCw1QxvvhPcF9pH3Zg==}
 
-  '@microsoft/tsdoc@0.14.2':
-    resolution: {integrity: sha512-9b8mPpKrfeGRuhFH5iO1iwCLeIIsV6+H1sRfxbkoGXIyQE2BTsPd9zqSqQJ+pv5sJ/hT5M1zvOFL02MnEezFug==}
+  '@microsoft/tsdoc@0.15.0':
+    resolution: {integrity: sha512-HZpPoABogPvjeJOdzCOSJsXeL/SMCBgBZMVC3X3d7YYp2gf31MfxhUoYUNwf1ERPJOnQc0wkFn9trqI6ZEdZuA==}
 
   '@misskey-dev/browser-image-resizer@2024.1.0':
     resolution: {integrity: sha512-4EnO0zLW5NDtng3Gaz5MuT761uiuoOuplwX18wBqgj8w56LTU5BjLn/vbHwDIIe0j2gwqDYhMb7bDjmr1/Fomg==}
 
-  '@misskey-dev/eslint-plugin@1.0.0':
-    resolution: {integrity: sha512-dh6UbcrNDVg5DD8k8Qh4ab30OPpuEYIlJCqaBV/lkIV8wNN/AfCJ2V7iTP8V8KjryM4t+sf5IqzQLQnT0mWI4A==}
+  '@misskey-dev/eslint-plugin@2.0.3':
+    resolution: {integrity: sha512-Gd7chezh53Gq4BZ+Tw/uRi4t5DocXR+vTFuTSRpwrZjbyTk6tWMHLV0EtaGY/6GT+Q6WH3UMJYExsFyHFK+JPw==}
     peerDependencies:
-      '@typescript-eslint/eslint-plugin': '>= 6'
-      '@typescript-eslint/parser': '>= 6'
-      eslint: '>= 3'
+      '@eslint/compat': '>= 1'
+      '@typescript-eslint/eslint-plugin': '>= 7'
+      '@typescript-eslint/parser': '>= 7'
+      eslint: '>= 8'
       eslint-plugin-import: '>= 2'
+      globals: '>= 15'
 
   '@misskey-dev/sharp-read-bmp@1.2.0':
     resolution: {integrity: sha512-er4pRakXzHYfEgOFAFfQagqDouG+wLm+kwNq1I30oSdIHDa0wM3KjFpfIGQ25Fks4GcmOl1s7Zh6xoQu5dNjTw==}
@@ -3109,77 +3157,74 @@ packages:
     cpu: [x64]
     os: [win32]
 
-  '@mswjs/cookies@1.1.0':
-    resolution: {integrity: sha512-0ZcCVQxifZmhwNBoQIrystCb+2sWBY2Zw8lpfJBPCHGCA/HWqehITeCRVIv4VMy8MPlaHo2w2pTHFV2pFfqKPw==}
+  '@mswjs/interceptors@0.29.1':
+    resolution: {integrity: sha512-3rDakgJZ77+RiQUuSK69t1F0m8BQKA8Vh5DCS5V0DWvNY67zob2JhhQrhCO0AKLGINTRSFd1tBaHcJTkhefoSw==}
     engines: {node: '>=18'}
 
-  '@mswjs/interceptors@0.26.15':
-    resolution: {integrity: sha512-HM47Lu1YFmnYHKMBynFfjCp0U/yRskHj/8QEJW0CBEPOlw8Gkmjfll+S9b8M7V5CNDw2/ciRxjjnWeaCiblSIQ==}
+  '@mswjs/interceptors@0.35.8':
+    resolution: {integrity: sha512-PFfqpHplKa7KMdoQdj5td03uG05VK2Ng1dG0sP4pT9h0dGSX2v9txYt/AnrzPb/vAmfyBBC0NQV7VaBEX+efgQ==}
     engines: {node: '>=18'}
 
-  '@napi-rs/canvas-android-arm64@0.1.52':
-    resolution: {integrity: sha512-x/K471KbASPVh5mfBUxokza66J0FNIlOgMNANWAf5C8HiATb487KecEhSkUQvvTS3WLYC9uSqIPHFgwF+tir3w==}
+  '@napi-rs/canvas-android-arm64@0.1.56':
+    resolution: {integrity: sha512-xBGqW2RZMAupkzar9t3gpbok9r524f3Wlk4PG2qnQdxbsiEND06OB8VxVtTcql6R02uJpXJGnyIhN02Te+GMVQ==}
     engines: {node: '>= 10'}
     cpu: [arm64]
     os: [android]
 
-  '@napi-rs/canvas-darwin-arm64@0.1.52':
-    resolution: {integrity: sha512-4OgVRD7TW02q5Q7lWLLjT+pYJ9ZHkQUTBOuXbPQ5wB0Wnh3RIq/aMY6thoXDZDzdR5vV3a5TUtbZUJ0aqLq3NA==}
+  '@napi-rs/canvas-darwin-arm64@0.1.56':
+    resolution: {integrity: sha512-Pvuz6Ib9YZTB5MlGL9WSu9a2asUC0DZ1zBHozDiBXr/6Zurs9l/ZH5NxFYTM829BpkdkO8kuI8b8Rz7ek30zzQ==}
     engines: {node: '>= 10'}
     cpu: [arm64]
     os: [darwin]
 
-  '@napi-rs/canvas-darwin-x64@0.1.52':
-    resolution: {integrity: sha512-3fgeGJ3j2X6Mtmn0QYf3iA+A6y1ePnsayakc2emEokzf03ErrPczONw3vjnTQo53JLPMzEnfPGAffdktU/ssPA==}
+  '@napi-rs/canvas-darwin-x64@0.1.56':
+    resolution: {integrity: sha512-O393jWt7G6rg0X1ralbsbBeskSG0iwlkD7mEHhMLJxqRqe+eQn0/xnwhs9l6dUNFC+5dM8LOvfFca4o9Vs2Vww==}
     engines: {node: '>= 10'}
     cpu: [x64]
     os: [darwin]
 
-  '@napi-rs/canvas-linux-arm-gnueabihf@0.1.52':
-    resolution: {integrity: sha512-aaDEEK5XwHUrPt0q4SR8l7Va0vtn50KmSs+itxP+o7RNk3Nuch8fINHOXyhMyhwNYgv1tfiJVyHsJhD0E6lXGA==}
+  '@napi-rs/canvas-linux-arm-gnueabihf@0.1.56':
+    resolution: {integrity: sha512-30NFb5lrF3YEwAO5XuATxpWDSXaBAgaFVswPJ+hYcAUyE3IkPPIFRY4ijQEh4frcSBvrzFGGYdNSoC18oLLWaQ==}
     engines: {node: '>= 10'}
     cpu: [arm]
     os: [linux]
 
-  '@napi-rs/canvas-linux-arm64-gnu@0.1.52':
-    resolution: {integrity: sha512-tzuwM7Amt5mkrp4csQjYWkFzwFdiCm7RNdJ5usX8syzKSXmozqWzLHjzo/2ozdSQNUy6wyzRrxkG4Rh6g0OpOA==}
+  '@napi-rs/canvas-linux-arm64-gnu@0.1.56':
+    resolution: {integrity: sha512-ODbWH9TLvba+39UxFwPn2Hm1ImALmWOZ0pEv5do/pz0439326Oz49hlfGot4KmkSBeKK81knWxRj9EXMSPwXPg==}
     engines: {node: '>= 10'}
     cpu: [arm64]
     os: [linux]
 
-  '@napi-rs/canvas-linux-arm64-musl@0.1.52':
-    resolution: {integrity: sha512-HQCtJlDT0dFp3uUZVzZOZ1VLMO7lbLRc548MjMxPpojit2ZdGopFzJ8jDSr4iszHrTO1SM1AxPaCM3pRvCAtjw==}
+  '@napi-rs/canvas-linux-arm64-musl@0.1.56':
+    resolution: {integrity: sha512-zqE4nz8CWiJJ0q5By7q9CDPicNkc0oyErgavK3ZV279zJL7Aapd3cIqayT6ynECArg7GgBl2WYSvr5AaRFmYgg==}
     engines: {node: '>= 10'}
     cpu: [arm64]
     os: [linux]
 
-  '@napi-rs/canvas-linux-x64-gnu@0.1.52':
-    resolution: {integrity: sha512-z5sBEw0PVWPH/MIQL8hOR8C3YYVlu8lqtRUcYajigMfXAhbMiNqDWTjuIWGMz3nIydDjZmn8KTxw/D4a0HFPqQ==}
+  '@napi-rs/canvas-linux-x64-gnu@0.1.56':
+    resolution: {integrity: sha512-JTnGAtJBQMhfSpN8/rbMnf5oxuO/juUNa0n4LA0LlW0JS9UBpmsS2BwFNCakFqOeAPaqIM6sFFsK3M4hve+Esw==}
     engines: {node: '>= 10'}
     cpu: [x64]
     os: [linux]
 
-  '@napi-rs/canvas-linux-x64-musl@0.1.52':
-    resolution: {integrity: sha512-G1+JdWFhHLyHhULJS51xTEhB7EL0ZiAUQwQaRi4/w75OOYDQ91O+o4miaxDHiV0hZuxBhHtZU6ftV2Zl3RMguw==}
+  '@napi-rs/canvas-linux-x64-musl@0.1.56':
+    resolution: {integrity: sha512-mpws7DhVDIj8ZKa/qcnUVLAm0fxD9RK5ojfNNSI9TOzn2E0f+GUXx8sGsCxDpMVMtN+mtyrMwRqH3F3rTUMWXw==}
     engines: {node: '>= 10'}
     cpu: [x64]
     os: [linux]
 
-  '@napi-rs/canvas-win32-x64-msvc@0.1.52':
-    resolution: {integrity: sha512-hMI626VsCC/wv29qHF78N7TSG+auatOp08DHln0Zdif5y1NJ14NU/rNUhzlTW8Zc6ssw+AMDJ3KKYYWYYg1aoA==}
+  '@napi-rs/canvas-win32-x64-msvc@0.1.56':
+    resolution: {integrity: sha512-VKAAkgXF+lbFvRFawPOtkfV/P7ogAgWTu5FMCIiBn0Gc3vnkKFG2cLo/IHIJ7FuriToKEidkJGT88iAh7W7GDA==}
     engines: {node: '>= 10'}
     cpu: [x64]
     os: [win32]
 
-  '@napi-rs/canvas@0.1.52':
-    resolution: {integrity: sha512-xeW9EghZLDPZuqWJ4l1+eG3ld0i9J7SpV2zlgi34MPt/FE9K2XWGCfnLr0gHGOBkcI3YOVhI13I0HqRAkMPdVw==}
+  '@napi-rs/canvas@0.1.56':
+    resolution: {integrity: sha512-SujSchzG6lLc/wT+Mwxam/w30Kk2sFTiU6bLFcidecKSmlhenAhGMQhZh2iGFfKoh2+8iit0jrt99n6TqReICQ==}
     engines: {node: '>= 10'}
 
-  '@ndelangen/get-tarball@3.0.7':
-    resolution: {integrity: sha512-NqGfTZIZpRFef1GoVaShSSRwDC3vde3ThtTeqFdcYd6ipKqnfEVhjK2hUeHjCQUcptyZr2TONqcloFXM+5QBrQ==}
-
-  '@nestjs/common@10.3.8':
-    resolution: {integrity: sha512-P+vPEIvqx2e+fonsYVlFXKvoChyJ8Tq+lfpqdVFqblovHbFr3kZ/nYX0cPs+XuW6bnRT8tz0SSR9XBGU43kJhw==}
+  '@nestjs/common@10.4.4':
+    resolution: {integrity: sha512-0j2/zqRw9nvHV1GKTktER8B/hIC/Z8CYFjN/ZqUuvwayCH+jZZBhCR2oRyuvLTXdnlSmtCAg2xvQ0ULqQvzqhA==}
     peerDependencies:
       class-transformer: '*'
       class-validator: '*'
@@ -3191,8 +3236,8 @@ packages:
       class-validator:
         optional: true
 
-  '@nestjs/core@10.3.8':
-    resolution: {integrity: sha512-AxF4tpYLDNn5Wfb3C4bNaaHJ4pREH5FJrSisR2A5zkYpQFORFs0Tc36lOFPMwBTy8Iv2wUwWLUVc5ftBnxEv4w==}
+  '@nestjs/core@10.4.4':
+    resolution: {integrity: sha512-y9tjmAzU6LTh1cC/lWrRsCcOd80khSR0qAHAqwY2svbW+AhsR/XCzgpZrAAKJrm/dDfjLCZKyxJSayeirGcW5Q==}
     peerDependencies:
       '@nestjs/common': ^10.0.0
       '@nestjs/microservices': ^10.0.0
@@ -3208,14 +3253,14 @@ packages:
       '@nestjs/websockets':
         optional: true
 
-  '@nestjs/platform-express@10.3.8':
-    resolution: {integrity: sha512-sifLoxgEJvAgbim1UuW6wyScMfkS9SVQRH+lN33N/9ZvZSjO6NSDLOe+wxqsnZkia+QrjFC0qy0ITRAsggfqbg==}
+  '@nestjs/platform-express@10.4.4':
+    resolution: {integrity: sha512-y52q1MxhbHaT3vAgWd08RgiYon0lJgtTa8U6g6gV0KI0IygwZhDQFJVxnrRDUdxQGIP5CKHmfQu3sk9gTNFoEA==}
     peerDependencies:
       '@nestjs/common': ^10.0.0
       '@nestjs/core': ^10.0.0
 
-  '@nestjs/testing@10.3.8':
-    resolution: {integrity: sha512-hpX9das2TdFTKQ4/2ojhjI6YgXtCfXRKui3A4Qaj54VVzc5+mtK502Jj18Vzji98o9MVS6skmYu+S/UvW3U6Fw==}
+  '@nestjs/testing@10.4.4':
+    resolution: {integrity: sha512-qRGFj51A5RM7JqA8pcyEwSLA3Y0dle/PAZ8oxP0suimoCusRY3Tk7wYqutZdCNj1ATb678SDaUZDHk2pwSv9/g==}
     peerDependencies:
       '@nestjs/common': ^10.0.0
       '@nestjs/core': ^10.0.0
@@ -3227,6 +3272,10 @@ packages:
       '@nestjs/platform-express':
         optional: true
 
+  '@noble/hashes@1.5.0':
+    resolution: {integrity: sha512-1j6kQFb7QRru7eKN3ZDvRcP13rugwdxZqCjbiAVZfIJwgj2A65UmT4TgARXGlXgnRkORLTDTrO19ZErt7+QXgA==}
+    engines: {node: ^14.21.3 || >=16}
+
   '@nodelib/fs.scandir@2.1.5':
     resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
     engines: {node: '>= 8'}
@@ -3270,19 +3319,19 @@ packages:
   '@open-draft/until@2.1.0':
     resolution: {integrity: sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==}
 
-  '@opentelemetry/api-logs@0.51.1':
-    resolution: {integrity: sha512-E3skn949Pk1z2XtXu/lxf6QAZpawuTM/IUEXcAzpiUkTd73Hmvw26FiN3cJuTmkpM5hZzHwkomVdtrh/n/zzwA==}
+  '@opentelemetry/api-logs@0.52.1':
+    resolution: {integrity: sha512-qnSqB2DQ9TPP96dl8cDubDvrUyWc0/sK81xHTK8eSUspzDM3bsewX903qclQFvVhgStjRWdC5bLb3kQqMkfV5A==}
     engines: {node: '>=14'}
 
-  '@opentelemetry/api@1.8.0':
-    resolution: {integrity: sha512-I/s6F7yKUDdtMsoBWXJe8Qz40Tui5vsuKCWJEWVL+5q9sSWRzzx6v2KeNsOBEwd94j0eWkpWCH4yB6rZg9Mf0w==}
+  '@opentelemetry/api@1.9.0':
+    resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==}
     engines: {node: '>=8.0.0'}
 
-  '@opentelemetry/context-async-hooks@1.24.1':
-    resolution: {integrity: sha512-R5r6DO4kgEOVBxFXhXjwospLQkv+sYxwCfjvoZBe7Zm6KKXAV9kDSJhi/D1BweowdZmO+sdbENLs374gER8hpQ==}
+  '@opentelemetry/context-async-hooks@1.25.1':
+    resolution: {integrity: sha512-UW/ge9zjvAEmRWVapOP0qyCvPulWU6cQxGxDbWEFfGOj1VBBZAuOqTo3X6yWmDTD3Xe15ysCZChHncr2xFMIfQ==}
     engines: {node: '>=14'}
     peerDependencies:
-      '@opentelemetry/api': '>=1.0.0 <1.9.0'
+      '@opentelemetry/api': '>=1.0.0 <1.10.0'
 
   '@opentelemetry/core@1.24.1':
     resolution: {integrity: sha512-wMSGfsdmibI88K9wB498zXY04yThPexo8jvwNNlm542HZB7XrrMRBbAyKJqG8qDRJwIBdBrPMi4V9ZPW/sqrcg==}
@@ -3290,98 +3339,110 @@ packages:
     peerDependencies:
       '@opentelemetry/api': '>=1.0.0 <1.9.0'
 
-  '@opentelemetry/instrumentation-connect@0.36.0':
-    resolution: {integrity: sha512-k9++bmJZ9zDEs3u3DnKTn2l7QTiNFg3gPx7G9rW0TPnP+xZoBSBTrEcGYBaqflQlrFG23Q58+X1sM2ayWPv5Fg==}
+  '@opentelemetry/core@1.25.1':
+    resolution: {integrity: sha512-GeT/l6rBYWVQ4XArluLVB6WWQ8flHbdb6r2FCHC3smtdOAbrJBIv35tpV/yp9bmYUJf+xmZpu9DRTIeJVhFbEQ==}
+    engines: {node: '>=14'}
+    peerDependencies:
+      '@opentelemetry/api': '>=1.0.0 <1.10.0'
+
+  '@opentelemetry/instrumentation-connect@0.38.0':
+    resolution: {integrity: sha512-2/nRnx3pjYEmdPIaBwtgtSviTKHWnDZN3R+TkRUnhIVrvBKVcq+I5B2rtd6mr6Fe9cHlZ9Ojcuh7pkNh/xdWWg==}
     engines: {node: '>=14'}
     peerDependencies:
       '@opentelemetry/api': ^1.3.0
 
-  '@opentelemetry/instrumentation-express@0.39.0':
-    resolution: {integrity: sha512-AG8U7z7D0JcBu/7dDcwb47UMEzj9/FMiJV2iQZqrsZnxR3FjB9J9oIH2iszJYci2eUdp2WbdvtpD9RV/zmME5A==}
+  '@opentelemetry/instrumentation-express@0.41.0':
+    resolution: {integrity: sha512-/B7fbMdaf3SYe5f1P973tkqd6s7XZirjpfkoJ63E7nltU30qmlgm9tY5XwZOzAFI0rHS9tbrFI2HFPAvQUFe/A==}
     engines: {node: '>=14'}
     peerDependencies:
       '@opentelemetry/api': ^1.3.0
 
-  '@opentelemetry/instrumentation-fastify@0.36.1':
-    resolution: {integrity: sha512-3Nfm43PI0I+3EX+1YbSy6xbDu276R1Dh1tqAk68yd4yirnIh52Kd5B+nJ8CgHA7o3UKakpBjj6vSzi5vNCzJIA==}
+  '@opentelemetry/instrumentation-fastify@0.38.0':
+    resolution: {integrity: sha512-HBVLpTSYpkQZ87/Df3N0gAw7VzYZV3n28THIBrJWfuqw3Or7UqdhnjeuMIPQ04BKk3aZc0cWn2naSQObbh5vXw==}
     engines: {node: '>=14'}
     peerDependencies:
       '@opentelemetry/api': ^1.3.0
 
-  '@opentelemetry/instrumentation-graphql@0.40.0':
-    resolution: {integrity: sha512-LVRdEHWACWOczv2imD+mhUrLMxsEjPPi32vIZJT57zygR5aUiA4em8X3aiGOCycgbMWkIu8xOSGSxdx3JmzN+w==}
+  '@opentelemetry/instrumentation-graphql@0.42.0':
+    resolution: {integrity: sha512-N8SOwoKL9KQSX7z3gOaw5UaTeVQcfDO1c21csVHnmnmGUoqsXbArK2B8VuwPWcv6/BC/i3io+xTo7QGRZ/z28Q==}
     engines: {node: '>=14'}
     peerDependencies:
       '@opentelemetry/api': ^1.3.0
 
-  '@opentelemetry/instrumentation-hapi@0.38.0':
-    resolution: {integrity: sha512-ZcOqEuwuutTDYIjhDIStix22ECblG/i9pHje23QGs4Q4YS4RMaZ5hKCoQJxW88Z4K7T53rQkdISmoXFKDV8xMg==}
+  '@opentelemetry/instrumentation-hapi@0.40.0':
+    resolution: {integrity: sha512-8U/w7Ifumtd2bSN1OLaSwAAFhb9FyqWUki3lMMB0ds+1+HdSxYBe9aspEJEgvxAqOkrQnVniAPTEGf1pGM7SOw==}
     engines: {node: '>=14'}
     peerDependencies:
       '@opentelemetry/api': ^1.3.0
 
-  '@opentelemetry/instrumentation-http@0.51.1':
-    resolution: {integrity: sha512-6b3nZnFFEz/3xZ6w8bVxctPUWIPWiXuPQ725530JgxnN1cvYFd8CJ75PrHZNjynmzSSnqBkN3ef4R9N+RpMh8Q==}
+  '@opentelemetry/instrumentation-http@0.52.1':
+    resolution: {integrity: sha512-dG/aevWhaP+7OLv4BQQSEKMJv8GyeOp3Wxl31NHqE8xo9/fYMfEljiZphUHIfyg4gnZ9swMyWjfOQs5GUQe54Q==}
     engines: {node: '>=14'}
     peerDependencies:
       '@opentelemetry/api': ^1.3.0
 
-  '@opentelemetry/instrumentation-ioredis@0.40.0':
-    resolution: {integrity: sha512-Jv/fH7KhpWe4KBirsiqeUJIYrsdR2iu2l4nWhfOlRvaZ+zYIiLEzTQR6QhBbyRoAbU4OuYJzjWusOmmpGBnwng==}
+  '@opentelemetry/instrumentation-ioredis@0.42.0':
+    resolution: {integrity: sha512-P11H168EKvBB9TUSasNDOGJCSkpT44XgoM6d3gRIWAa9ghLpYhl0uRkS8//MqPzcJVHr3h3RmfXIpiYLjyIZTw==}
     engines: {node: '>=14'}
     peerDependencies:
       '@opentelemetry/api': ^1.3.0
 
-  '@opentelemetry/instrumentation-koa@0.40.0':
-    resolution: {integrity: sha512-dJc3H/bKMcgUYcQpLF+1IbmUKus0e5Fnn/+ru/3voIRHwMADT3rFSUcGLWSczkg68BCgz0vFWGDTvPtcWIFr7A==}
+  '@opentelemetry/instrumentation-koa@0.42.0':
+    resolution: {integrity: sha512-H1BEmnMhho8o8HuNRq5zEI4+SIHDIglNB7BPKohZyWG4fWNuR7yM4GTlR01Syq21vODAS7z5omblScJD/eZdKw==}
     engines: {node: '>=14'}
     peerDependencies:
       '@opentelemetry/api': ^1.3.0
 
-  '@opentelemetry/instrumentation-mongodb@0.43.0':
-    resolution: {integrity: sha512-bMKej7Y76QVUD3l55Q9YqizXybHUzF3pujsBFjqbZrRn2WYqtsDtTUlbCK7fvXNPwFInqZ2KhnTqd0gwo8MzaQ==}
+  '@opentelemetry/instrumentation-mongodb@0.46.0':
+    resolution: {integrity: sha512-VF/MicZ5UOBiXrqBslzwxhN7TVqzu1/LN/QDpkskqM0Zm0aZ4CVRbUygL8d7lrjLn15x5kGIe8VsSphMfPJzlA==}
     engines: {node: '>=14'}
     peerDependencies:
       '@opentelemetry/api': ^1.3.0
 
-  '@opentelemetry/instrumentation-mongoose@0.38.1':
-    resolution: {integrity: sha512-zaeiasdnRjXe6VhYCBMdkmAVh1S5MmXC/0spet+yqoaViGnYst/DOxPvhwg3yT4Yag5crZNWsVXnA538UjP6Ow==}
+  '@opentelemetry/instrumentation-mongoose@0.40.0':
+    resolution: {integrity: sha512-niRi5ZUnkgzRhIGMOozTyoZIvJKNJyhijQI4nF4iFSb+FUx2v5fngfR+8XLmdQAO7xmsD8E5vEGdDVYVtKbZew==}
     engines: {node: '>=14'}
     peerDependencies:
       '@opentelemetry/api': ^1.3.0
 
-  '@opentelemetry/instrumentation-mysql2@0.38.1':
-    resolution: {integrity: sha512-qkpHMgWSDTYVB1vlZ9sspf7l2wdS5DDq/rbIepDwX5BA0N0068JTQqh0CgAh34tdFqSCnWXIhcyOXC2TtRb0sg==}
+  '@opentelemetry/instrumentation-mysql2@0.40.0':
+    resolution: {integrity: sha512-0xfS1xcqUmY7WE1uWjlmI67Xg3QsSUlNT+AcXHeA4BDUPwZtWqF4ezIwLgpVZfHOnkAEheqGfNSWd1PIu3Wnfg==}
     engines: {node: '>=14'}
     peerDependencies:
       '@opentelemetry/api': ^1.3.0
 
-  '@opentelemetry/instrumentation-mysql@0.38.1':
-    resolution: {integrity: sha512-+iBAawUaTfX/HAlvySwozx0C2B6LBfNPXX1W8Z2On1Uva33AGkw2UjL9XgIg1Pj4eLZ9R4EoJ/aFz+Xj4E/7Fw==}
+  '@opentelemetry/instrumentation-mysql@0.40.0':
+    resolution: {integrity: sha512-d7ja8yizsOCNMYIJt5PH/fKZXjb/mS48zLROO4BzZTtDfhNCl2UM/9VIomP2qkGIFVouSJrGr/T00EzY7bPtKA==}
     engines: {node: '>=14'}
     peerDependencies:
       '@opentelemetry/api': ^1.3.0
 
-  '@opentelemetry/instrumentation-nestjs-core@0.37.1':
-    resolution: {integrity: sha512-ebYQjHZEmGHWEALwwDGhSQVLBaurFnuLIkZD5igPXrt7ohfF4lc5/4al1LO+vKc0NHk8SJWStuRueT86ISA8Vg==}
+  '@opentelemetry/instrumentation-nestjs-core@0.39.0':
+    resolution: {integrity: sha512-mewVhEXdikyvIZoMIUry8eb8l3HUjuQjSjVbmLVTt4NQi35tkpnHQrG9bTRBrl3403LoWZ2njMPJyg4l6HfKvA==}
     engines: {node: '>=14'}
     peerDependencies:
       '@opentelemetry/api': ^1.3.0
 
-  '@opentelemetry/instrumentation-pg@0.41.0':
-    resolution: {integrity: sha512-BSlhpivzBD77meQNZY9fS4aKgydA8AJBzv2dqvxXFy/Hq64b7HURgw/ztbmwFeYwdF5raZZUifiiNSMLpOJoSA==}
+  '@opentelemetry/instrumentation-pg@0.43.0':
+    resolution: {integrity: sha512-og23KLyoxdnAeFs1UWqzSonuCkePUzCX30keSYigIzJe/6WSYA8rnEI5lobcxPEzg+GcU06J7jzokuEHbjVJNw==}
     engines: {node: '>=14'}
     peerDependencies:
       '@opentelemetry/api': ^1.3.0
 
-  '@opentelemetry/instrumentation@0.43.0':
-    resolution: {integrity: sha512-S1uHE+sxaepgp+t8lvIDuRgyjJWisAb733198kwQTUc9ZtYQ2V2gmyCtR1x21ePGVLoMiX/NWY7WA290hwkjJQ==}
+  '@opentelemetry/instrumentation-redis-4@0.41.0':
+    resolution: {integrity: sha512-H7IfGTqW2reLXqput4yzAe8YpDC0fmVNal95GHMLOrS89W+qWUKIqxolSh63hJyfmwPSFwXASzj7wpSk8Az+Dg==}
     engines: {node: '>=14'}
     peerDependencies:
       '@opentelemetry/api': ^1.3.0
 
-  '@opentelemetry/instrumentation@0.51.1':
-    resolution: {integrity: sha512-JIrvhpgqY6437QIqToyozrUG1h5UhwHkaGK/WAX+fkrpyPtc+RO5FkRtUd9BH0MibabHHvqsnBGKfKVijbmp8w==}
+  '@opentelemetry/instrumentation@0.46.0':
+    resolution: {integrity: sha512-a9TijXZZbk0vI5TGLZl+0kxyFfrXHhX6Svtz7Pp2/VBlCSKrazuULEyoJQrOknJyFWNMEmbbJgOciHCCpQcisw==}
+    engines: {node: '>=14'}
+    peerDependencies:
+      '@opentelemetry/api': ^1.3.0
+
+  '@opentelemetry/instrumentation@0.52.1':
+    resolution: {integrity: sha512-uXJbYU/5/MBHjMp1FqrILLRuiJCs3Ofk0MeRDk8g1S1gD47U8X3JnSwcMO1rtRo1x1a7zKaQHaoYu49p/4eSKw==}
     engines: {node: '>=14'}
     peerDependencies:
       '@opentelemetry/api': ^1.3.0
@@ -3396,22 +3457,32 @@ packages:
     peerDependencies:
       '@opentelemetry/api': '>=1.0.0 <1.9.0'
 
+  '@opentelemetry/resources@1.25.1':
+    resolution: {integrity: sha512-pkZT+iFYIZsVn6+GzM0kSX+u3MSLCY9md+lIJOoKl/P+gJFfxJte/60Usdp8Ce4rOs8GduUpSPNe1ddGyDT1sQ==}
+    engines: {node: '>=14'}
+    peerDependencies:
+      '@opentelemetry/api': '>=1.0.0 <1.10.0'
+
   '@opentelemetry/sdk-metrics@1.24.1':
     resolution: {integrity: sha512-FrAqCbbGao9iKI+Mgh+OsC9+U2YMoXnlDHe06yH7dvavCKzE3S892dGtX54+WhSFVxHR/TMRVJiK/CV93GR0TQ==}
     engines: {node: '>=14'}
     peerDependencies:
       '@opentelemetry/api': '>=1.3.0 <1.9.0'
 
-  '@opentelemetry/sdk-trace-base@1.24.1':
-    resolution: {integrity: sha512-zz+N423IcySgjihl2NfjBf0qw1RWe11XIAWVrTNOSSI6dtSPJiVom2zipFB2AEEtJWpv0Iz6DY6+TjnyTV5pWg==}
+  '@opentelemetry/sdk-trace-base@1.25.1':
+    resolution: {integrity: sha512-C8k4hnEbc5FamuZQ92nTOp8X/diCY56XUTnMiv9UTuJitCzaNNHAVsdm5+HLCdI8SLQsLWIrG38tddMxLVoftw==}
     engines: {node: '>=14'}
     peerDependencies:
-      '@opentelemetry/api': '>=1.0.0 <1.9.0'
+      '@opentelemetry/api': '>=1.0.0 <1.10.0'
 
   '@opentelemetry/semantic-conventions@1.24.1':
     resolution: {integrity: sha512-VkliWlS4/+GHLLW7J/rVBA00uXus1SWvwFvcUDxDwmFxYfg/2VI6ekwdXS28cjI8Qz2ky2BzG8OUHo+WeYIWqw==}
     engines: {node: '>=14'}
 
+  '@opentelemetry/semantic-conventions@1.25.1':
+    resolution: {integrity: sha512-ZDjMJJQRlyk8A1KZFCc+bCbsyrn1wTwdNt56F7twdfUfnHUZUq77/WfONCj8p72NZOyP7pNTdUWSTYC3GTbuuQ==}
+    engines: {node: '>=14'}
+
   '@opentelemetry/sql-common@0.40.1':
     resolution: {integrity: sha512-nSDlnHSqzC3pXn/wZEZVLuAuJ1MYMXPBwtv2qAbCa3847SaHItdE7SzUq/Jtb0KZmh1zfAbNi3AAMjztTT4Ugg==}
     engines: {node: '>=14'}
@@ -3441,26 +3512,8 @@ packages:
     resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
     engines: {node: '>=14'}
 
-  '@prisma/instrumentation@5.14.0':
-    resolution: {integrity: sha512-DeybWvIZzu/mUsOYP9MVd6AyBj+MP7xIMrcuIn25MX8FiQX39QBnET5KhszTAip/ToctUuDwSJ46QkIoyo3RFA==}
-
-  '@radix-ui/react-compose-refs@1.0.1':
-    resolution: {integrity: sha512-fDSBgd44FKHa1FRMU59qBMPFcl2PZE+2nmqunj+BWFyYYjnhIDWL2ItDs3rrbJDQOtzt5nIebLCQc4QRfz6LJw==}
-    peerDependencies:
-      '@types/react': '*'
-      react: ^16.8 || ^17.0 || ^18.0
-    peerDependenciesMeta:
-      '@types/react':
-        optional: true
-
-  '@radix-ui/react-slot@1.0.2':
-    resolution: {integrity: sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==}
-    peerDependencies:
-      '@types/react': '*'
-      react: ^16.8 || ^17.0 || ^18.0
-    peerDependenciesMeta:
-      '@types/react':
-        optional: true
+  '@prisma/instrumentation@5.17.0':
+    resolution: {integrity: sha512-c1Sle4ji8aasMcYfBBHFM56We4ljfenVtRmS8aY06BllS7SoU6SmJBwG7vil+GHiR0Yrh+t9iBwt4AY0Jr4KNQ==}
 
   '@readme/better-ajv-errors@1.6.0':
     resolution: {integrity: sha512-9gO9rld84Jgu13kcbKRU+WHseNhaVt76wYMeRDGsUGYxwJtI3RmEJ9LY9dZCYQGI8eUZLuxb5qDja0nqklpFjQ==}
@@ -3471,12 +3524,16 @@ packages:
   '@readme/json-schema-ref-parser@1.2.0':
     resolution: {integrity: sha512-Bt3QVovFSua4QmHa65EHUmh2xS0XJ3rgTEUPH998f4OW4VVJke3BuS16f+kM0ZLOGdvIrzrPRqwihuv5BAjtrA==}
 
-  '@readme/openapi-parser@2.5.0':
-    resolution: {integrity: sha512-IbymbOqRuUzoIgxfAAR7XJt2FWl6n2yqN09fF5adacGm7W03siA3bj1Emql0X9D2T+RpBYz3x9zDsMhuoMP62A==}
-    engines: {node: '>=14'}
+  '@readme/openapi-parser@2.6.0':
+    resolution: {integrity: sha512-pyFJXezWj9WI1O+gdp95CoxfY+i+Uq3kKk4zXIFuRAZi9YnHpHOpjumWWr67wkmRTw19Hskh9spyY0Iyikf3fA==}
+    engines: {node: '>=18'}
     peerDependencies:
       openapi-types: '>=7'
 
+  '@readme/openapi-schemas@3.1.0':
+    resolution: {integrity: sha512-9FC/6ho8uFa8fV50+FPy/ngWN53jaUu4GRXlAjcxIRrzhltJnpKkBG2Tp0IDraFJeWrOpk84RJ9EMEEYzaI1Bw==}
+    engines: {node: '>=18'}
+
   '@rollup/plugin-json@6.1.0':
     resolution: {integrity: sha512-EGI2te5ENk1coGeADSIwZ7G2Q8CJS2sF120T7jLw4xFw9n7wIOXHo+kIYRAoVpJAN+kmqZSoO3Fp4JtoNF4ReA==}
     engines: {node: '>=14.0.0'}
@@ -3486,8 +3543,8 @@ packages:
       rollup:
         optional: true
 
-  '@rollup/plugin-replace@5.0.5':
-    resolution: {integrity: sha512-rYO4fOi8lMaTg/z5Jb+hKnrHHVn8j2lwkqwyS4kTRhKyWOLf2wST2sWXr4WzWiTcoHTp2sTjqUbqIj2E39slKQ==}
+  '@rollup/plugin-replace@5.0.7':
+    resolution: {integrity: sha512-PqxSfuorkHz/SPpyngLyg5GCEkOcee9M1bkxiVDr41Pd61mqP1PLOoDPbpl44SB2mQGKwV/In74gqQmGITOhEQ==}
     engines: {node: '>=14.0.0'}
     peerDependencies:
       rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0
@@ -3495,8 +3552,8 @@ packages:
       rollup:
         optional: true
 
-  '@rollup/pluginutils@5.1.0':
-    resolution: {integrity: sha512-XTIWOPPcpvyKI6L1NHo0lFlCyznUEyPmPY1mc3KpPVDYulHSTvyeLNVW00QTLIAFNhR3kYnJTQHeGqU4M3n09g==}
+  '@rollup/pluginutils@5.1.2':
+    resolution: {integrity: sha512-/FIdS3PyZ39bjZlwqFnWqCOVnW7o963LtKMwQOD0NhQqw22gSr2YY1afu3FxRip4ZCZNsD5jq6Aaz6QV3D/Njw==}
     engines: {node: '>=14.0.0'}
     peerDependencies:
       rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0
@@ -3504,153 +3561,174 @@ packages:
       rollup:
         optional: true
 
-  '@rollup/rollup-android-arm-eabi@4.17.2':
-    resolution: {integrity: sha512-NM0jFxY8bB8QLkoKxIQeObCaDlJKewVlIEkuyYKm5An1tdVZ966w2+MPQ2l8LBZLjR+SgyV+nRkTIunzOYBMLQ==}
+  '@rollup/rollup-android-arm-eabi@4.22.5':
+    resolution: {integrity: sha512-SU5cvamg0Eyu/F+kLeMXS7GoahL+OoizlclVFX3l5Ql6yNlywJJ0OuqTzUx0v+aHhPHEB/56CT06GQrRrGNYww==}
     cpu: [arm]
     os: [android]
 
-  '@rollup/rollup-android-arm64@4.17.2':
-    resolution: {integrity: sha512-yeX/Usk7daNIVwkq2uGoq2BYJKZY1JfyLTaHO/jaiSwi/lsf8fTFoQW/n6IdAsx5tx+iotu2zCJwz8MxI6D/Bw==}
+  '@rollup/rollup-android-arm64@4.22.5':
+    resolution: {integrity: sha512-S4pit5BP6E5R5C8S6tgU/drvgjtYW76FBuG6+ibG3tMvlD1h9LHVF9KmlmaUBQ8Obou7hEyS+0w+IR/VtxwNMQ==}
     cpu: [arm64]
     os: [android]
 
-  '@rollup/rollup-darwin-arm64@4.17.2':
-    resolution: {integrity: sha512-kcMLpE6uCwls023+kknm71ug7MZOrtXo+y5p/tsg6jltpDtgQY1Eq5sGfHcQfb+lfuKwhBmEURDga9N0ol4YPw==}
+  '@rollup/rollup-darwin-arm64@4.22.5':
+    resolution: {integrity: sha512-250ZGg4ipTL0TGvLlfACkIxS9+KLtIbn7BCZjsZj88zSg2Lvu3Xdw6dhAhfe/FjjXPVNCtcSp+WZjVsD3a/Zlw==}
     cpu: [arm64]
     os: [darwin]
 
-  '@rollup/rollup-darwin-x64@4.17.2':
-    resolution: {integrity: sha512-AtKwD0VEx0zWkL0ZjixEkp5tbNLzX+FCqGG1SvOu993HnSz4qDI6S4kGzubrEJAljpVkhRSlg5bzpV//E6ysTQ==}
+  '@rollup/rollup-darwin-x64@4.22.5':
+    resolution: {integrity: sha512-D8brJEFg5D+QxFcW6jYANu+Rr9SlKtTenmsX5hOSzNYVrK5oLAEMTUgKWYJP+wdKyCdeSwnapLsn+OVRFycuQg==}
     cpu: [x64]
     os: [darwin]
 
-  '@rollup/rollup-linux-arm-gnueabihf@4.17.2':
-    resolution: {integrity: sha512-3reX2fUHqN7sffBNqmEyMQVj/CKhIHZd4y631duy0hZqI8Qoqf6lTtmAKvJFYa6bhU95B1D0WgzHkmTg33In0A==}
+  '@rollup/rollup-linux-arm-gnueabihf@4.22.5':
+    resolution: {integrity: sha512-PNqXYmdNFyWNg0ma5LdY8wP+eQfdvyaBAojAXgO7/gs0Q/6TQJVXAXe8gwW9URjbS0YAammur0fynYGiWsKlXw==}
     cpu: [arm]
     os: [linux]
 
-  '@rollup/rollup-linux-arm-musleabihf@4.17.2':
-    resolution: {integrity: sha512-uSqpsp91mheRgw96xtyAGP9FW5ChctTFEoXP0r5FAzj/3ZRv3Uxjtc7taRQSaQM/q85KEKjKsZuiZM3GyUivRg==}
+  '@rollup/rollup-linux-arm-musleabihf@4.22.5':
+    resolution: {integrity: sha512-kSSCZOKz3HqlrEuwKd9TYv7vxPYD77vHSUvM2y0YaTGnFc8AdI5TTQRrM1yIp3tXCKrSL9A7JLoILjtad5t8pQ==}
     cpu: [arm]
     os: [linux]
 
-  '@rollup/rollup-linux-arm64-gnu@4.17.2':
-    resolution: {integrity: sha512-EMMPHkiCRtE8Wdk3Qhtciq6BndLtstqZIroHiiGzB3C5LDJmIZcSzVtLRbwuXuUft1Cnv+9fxuDtDxz3k3EW2A==}
+  '@rollup/rollup-linux-arm64-gnu@4.22.5':
+    resolution: {integrity: sha512-oTXQeJHRbOnwRnRffb6bmqmUugz0glXaPyspp4gbQOPVApdpRrY/j7KP3lr7M8kTfQTyrBUzFjj5EuHAhqH4/w==}
     cpu: [arm64]
     os: [linux]
 
-  '@rollup/rollup-linux-arm64-musl@4.17.2':
-    resolution: {integrity: sha512-NMPylUUZ1i0z/xJUIx6VUhISZDRT+uTWpBcjdv0/zkp7b/bQDF+NfnfdzuTiB1G6HTodgoFa93hp0O1xl+/UbA==}
+  '@rollup/rollup-linux-arm64-musl@4.22.5':
+    resolution: {integrity: sha512-qnOTIIs6tIGFKCHdhYitgC2XQ2X25InIbZFor5wh+mALH84qnFHvc+vmWUpyX97B0hNvwNUL4B+MB8vJvH65Fw==}
     cpu: [arm64]
     os: [linux]
 
-  '@rollup/rollup-linux-powerpc64le-gnu@4.17.2':
-    resolution: {integrity: sha512-T19My13y8uYXPw/L/k0JYaX1fJKFT/PWdXiHr8mTbXWxjVF1t+8Xl31DgBBvEKclw+1b00Chg0hxE2O7bTG7GQ==}
+  '@rollup/rollup-linux-powerpc64le-gnu@4.22.5':
+    resolution: {integrity: sha512-TMYu+DUdNlgBXING13rHSfUc3Ky5nLPbWs4bFnT+R6Vu3OvXkTkixvvBKk8uO4MT5Ab6lC3U7x8S8El2q5o56w==}
     cpu: [ppc64]
     os: [linux]
 
-  '@rollup/rollup-linux-riscv64-gnu@4.17.2':
-    resolution: {integrity: sha512-BOaNfthf3X3fOWAB+IJ9kxTgPmMqPPH5f5k2DcCsRrBIbWnaJCgX2ll77dV1TdSy9SaXTR5iDXRL8n7AnoP5cg==}
+  '@rollup/rollup-linux-riscv64-gnu@4.22.5':
+    resolution: {integrity: sha512-PTQq1Kz22ZRvuhr3uURH+U/Q/a0pbxJoICGSprNLAoBEkyD3Sh9qP5I0Asn0y0wejXQBbsVMRZRxlbGFD9OK4A==}
     cpu: [riscv64]
     os: [linux]
 
-  '@rollup/rollup-linux-s390x-gnu@4.17.2':
-    resolution: {integrity: sha512-W0UP/x7bnn3xN2eYMql2T/+wpASLE5SjObXILTMPUBDB/Fg/FxC+gX4nvCfPBCbNhz51C+HcqQp2qQ4u25ok6g==}
+  '@rollup/rollup-linux-s390x-gnu@4.22.5':
+    resolution: {integrity: sha512-bR5nCojtpuMss6TDEmf/jnBnzlo+6n1UhgwqUvRoe4VIotC7FG1IKkyJbwsT7JDsF2jxR+NTnuOwiGv0hLyDoQ==}
     cpu: [s390x]
     os: [linux]
 
-  '@rollup/rollup-linux-x64-gnu@4.17.2':
-    resolution: {integrity: sha512-Hy7pLwByUOuyaFC6mAr7m+oMC+V7qyifzs/nW2OJfC8H4hbCzOX07Ov0VFk/zP3kBsELWNFi7rJtgbKYsav9QQ==}
+  '@rollup/rollup-linux-x64-gnu@4.22.5':
+    resolution: {integrity: sha512-N0jPPhHjGShcB9/XXZQWuWBKZQnC1F36Ce3sDqWpujsGjDz/CQtOL9LgTrJ+rJC8MJeesMWrMWVLKKNR/tMOCA==}
     cpu: [x64]
     os: [linux]
 
-  '@rollup/rollup-linux-x64-musl@4.17.2':
-    resolution: {integrity: sha512-h1+yTWeYbRdAyJ/jMiVw0l6fOOm/0D1vNLui9iPuqgRGnXA0u21gAqOyB5iHjlM9MMfNOm9RHCQ7zLIzT0x11Q==}
+  '@rollup/rollup-linux-x64-musl@4.22.5':
+    resolution: {integrity: sha512-uBa2e28ohzNNwjr6Uxm4XyaA1M/8aTgfF2T7UIlElLaeXkgpmIJ2EitVNQxjO9xLLLy60YqAgKn/AqSpCUkE9g==}
     cpu: [x64]
     os: [linux]
 
-  '@rollup/rollup-win32-arm64-msvc@4.17.2':
-    resolution: {integrity: sha512-tmdtXMfKAjy5+IQsVtDiCfqbynAQE/TQRpWdVataHmhMb9DCoJxp9vLcCBjEQWMiUYxO1QprH/HbY9ragCEFLA==}
+  '@rollup/rollup-win32-arm64-msvc@4.22.5':
+    resolution: {integrity: sha512-RXT8S1HP8AFN/Kr3tg4fuYrNxZ/pZf1HemC5Tsddc6HzgGnJm0+Lh5rAHJkDuW3StI0ynNXukidROMXYl6ew8w==}
     cpu: [arm64]
     os: [win32]
 
-  '@rollup/rollup-win32-ia32-msvc@4.17.2':
-    resolution: {integrity: sha512-7II/QCSTAHuE5vdZaQEwJq2ZACkBpQDOmQsE6D6XUbnBHW8IAhm4eTufL6msLJorzrHDFv3CF8oCA/hSIRuZeQ==}
+  '@rollup/rollup-win32-ia32-msvc@4.22.5':
+    resolution: {integrity: sha512-ElTYOh50InL8kzyUD6XsnPit7jYCKrphmddKAe1/Ytt74apOxDq5YEcbsiKs0fR3vff3jEneMM+3I7jbqaMyBg==}
     cpu: [ia32]
     os: [win32]
 
-  '@rollup/rollup-win32-x64-msvc@4.17.2':
-    resolution: {integrity: sha512-TGGO7v7qOq4CYmSBVEYpI1Y5xDuCEnbVC5Vth8mOsW0gDSzxNrVERPc790IGHsrT2dQSimgMr9Ub3Y1Jci5/8w==}
+  '@rollup/rollup-win32-x64-msvc@4.22.5':
+    resolution: {integrity: sha512-+lvL/4mQxSV8MukpkKyyvfwhH266COcWlXE/1qxwN08ajovta3459zrjLghYMgDerlzNwLAcFpvU+WWE5y6nAQ==}
     cpu: [x64]
     os: [win32]
 
-  '@rushstack/node-core-library@4.1.0':
-    resolution: {integrity: sha512-qz4JFBZJCf1YN5cAXa1dP6Mki/HrsQxc/oYGAGx29dF2cwF2YMxHoly0FBhMw3IEnxo5fMj0boVfoHVBkpkx/w==}
+  '@rtsao/scc@1.1.0':
+    resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==}
+
+  '@rushstack/node-core-library@5.9.0':
+    resolution: {integrity: sha512-MMsshEWkTbXqxqFxD4gcIUWQOCeBChlGczdZbHfqmNZQFLHB3yWxDFSMHFUdu2/OB9NUk7Awn5qRL+rws4HQNg==}
     peerDependencies:
       '@types/node': '*'
     peerDependenciesMeta:
       '@types/node':
         optional: true
 
-  '@rushstack/rig-package@0.5.2':
-    resolution: {integrity: sha512-mUDecIJeH3yYGZs2a48k+pbhM6JYwWlgjs2Ca5f2n1G2/kgdgP9D/07oglEGf6mRyXEnazhEENeYTSNDRCwdqA==}
+  '@rushstack/rig-package@0.5.3':
+    resolution: {integrity: sha512-olzSSjYrvCNxUFZowevC3uz8gvKr3WTpHQ7BkpjtRpA3wK+T0ybep/SRUMfr195gBzJm5gaXw0ZMgjIyHqJUow==}
 
-  '@rushstack/terminal@0.10.1':
-    resolution: {integrity: sha512-C6Vi/m/84IYJTkfzmXr1+W8Wi3MmBjVF/q3za91Gb3VYjKbpALHVxY6FgH625AnDe5Z0Kh4MHKWA3Z7bqgAezA==}
+  '@rushstack/terminal@0.14.2':
+    resolution: {integrity: sha512-2fC1wqu1VCExKC0/L+0noVcFQEXEnoBOtCIex1TOjBzEDWcw8KzJjjj7aTP6mLxepG0XIyn9OufeFb6SFsa+sg==}
     peerDependencies:
       '@types/node': '*'
     peerDependenciesMeta:
       '@types/node':
         optional: true
 
-  '@rushstack/ts-command-line@4.19.2':
-    resolution: {integrity: sha512-cqmXXmBEBlzo9WtyUrHtF9e6kl0LvBY7aTSVX4jfnBfXWZQWnPq9JTFPlQZ+L/ZwjZ4HrNwQsOVvhe9oOucZkw==}
+  '@rushstack/ts-command-line@4.22.8':
+    resolution: {integrity: sha512-XbFjOoV7qZHJnSuFUHv0pKaFA4ixyCuki+xMjsMfDwfvQjs5MYG0IK5COal3tRnG7KCDe2l/G+9LrzYE/RJhgg==}
 
-  '@sentry/core@8.5.0':
-    resolution: {integrity: sha512-SO3ddBzGdha+Oflp+IKwBxj+7ds1q69OAT3VsypTd+WUFQdI9DIhR92Bjf+QQZCIzUNOi79VWOh3aOi3f6hMnw==}
+  '@sec-ant/readable-stream@0.4.1':
+    resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==}
+
+  '@sentry/core@8.20.0':
+    resolution: {integrity: sha512-R81snuw+67VT4aCxr6ShST/s0Y6FlwN2YczhDwaGyzumn5rlvA6A4JtQDeExduNoDDyv4T3LrmW8wlYZn3CJJw==}
     engines: {node: '>=14.18'}
 
-  '@sentry/node@8.5.0':
-    resolution: {integrity: sha512-t9cHAx/wLJYtdVf2XlzKlRJGvwdAp1wjzG0tC4E1Znx74OuUS1cFNo5WrGuOi0/YcWSxiJaxBvtUcsWK86fIgw==}
+  '@sentry/node@8.20.0':
+    resolution: {integrity: sha512-i4ywT2m0Gw65U3uwI4NwiNcyqp9YF6/RsusfH1pg4YkiL/RYp7FS0MPVgMggfvoue9S3KjCgRVlzTLwFATyPXQ==}
     engines: {node: '>=14.18'}
 
-  '@sentry/opentelemetry@8.5.0':
-    resolution: {integrity: sha512-AbxFUNjuTKQ9ugZrssmGtPxWkBr4USNoP7GjaaGCNwNzvIVYCa+i8dv7BROJiW2lsxNAremULEbh+nbVmhGxDA==}
+  '@sentry/opentelemetry@8.20.0':
+    resolution: {integrity: sha512-NFcLK6+t9wUc4HlGKeuDn6W4KjZxZfZmWlrK2/tgC5KzG1cnVeOnWUrJzGHTa+YDDdIijpjiFUcpXGPkX3rmIg==}
     engines: {node: '>=14.18'}
     peerDependencies:
-      '@opentelemetry/api': ^1.8.0
-      '@opentelemetry/core': ^1.24.1
-      '@opentelemetry/instrumentation': ^0.51.1
-      '@opentelemetry/sdk-trace-base': ^1.23.0
-      '@opentelemetry/semantic-conventions': ^1.23.0
+      '@opentelemetry/api': ^1.9.0
+      '@opentelemetry/core': ^1.25.1
+      '@opentelemetry/instrumentation': ^0.52.1
+      '@opentelemetry/sdk-trace-base': ^1.25.1
+      '@opentelemetry/semantic-conventions': ^1.25.1
 
-  '@sentry/profiling-node@8.5.0':
-    resolution: {integrity: sha512-nEXJqVNfZWYi4PakQXBZCJeH59UlnBv+zaYftDNUUXttCmzRXpL1ujNm5mJrJHlWjV7tgIFw02HW3nh2yyKOkw==}
+  '@sentry/profiling-node@8.20.0':
+    resolution: {integrity: sha512-vQaMYjPM7o0qvmj4atxXZywIDhnxMwTlc6x24eKqT8zN0OFBuIc1nYIacT7pEmd7R6e/mXdiG04GH1Vg0bHfOQ==}
     engines: {node: '>=14.18'}
     hasBin: true
 
-  '@sentry/types@8.5.0':
-    resolution: {integrity: sha512-eDgkSmKI4+XL0QZm4H3j/n1RgnrbnjXZmjj+LsfccRZQwbPu9bWlc8q7Y7Ty1gOsoUpX+TecNLp2a8CRID4KHA==}
+  '@sentry/types@8.20.0':
+    resolution: {integrity: sha512-6IP278KojOpiAA7vrd1hjhUyn26cl0n0nGsShzic5ztCVs92sTeVRnh7MTB9irDVtAbOEyt/YH6go3h+Jia1pA==}
     engines: {node: '>=14.18'}
 
-  '@sentry/utils@8.5.0':
-    resolution: {integrity: sha512-fdrCzo8SAYiw9JBhkJPqYqJkDXZ/wICzN7+zcXIuzKNhE1hdoFjeKcPnpUI3bKZCG6e3hT1PTYQXhVw7GIZV9w==}
+  '@sentry/utils@8.20.0':
+    resolution: {integrity: sha512-+1I5H8dojURiEUGPliDwheQk8dhjp8uV1sMccR/W/zjFrt4wZyPs+Ttp/V7gzm9LDJoNek9tmELert/jQqWTgg==}
     engines: {node: '>=14.18'}
 
-  '@shikijs/core@1.4.0':
-    resolution: {integrity: sha512-CxpKLntAi64h3j+TwWqVIQObPTED0FyXLHTTh3MKXtqiQNn2JGcMQQ362LftDbc9kYbDtrksNMNoVmVXzKFYUQ==}
+  '@shikijs/core@1.21.0':
+    resolution: {integrity: sha512-zAPMJdiGuqXpZQ+pWNezQAk5xhzRXBNiECFPcJLtUdsFM3f//G95Z15EHTnHchYycU8kIIysqGgxp8OVSj1SPQ==}
+
+  '@shikijs/engine-javascript@1.21.0':
+    resolution: {integrity: sha512-jxQHNtVP17edFW4/0vICqAVLDAxmyV31MQJL4U/Kg+heQALeKYVOWo0sMmEZ18FqBt+9UCdyqGKYE7bLRtk9mg==}
+
+  '@shikijs/engine-oniguruma@1.21.0':
+    resolution: {integrity: sha512-AIZ76XocENCrtYzVU7S4GY/HL+tgHGbVU+qhiDyNw1qgCA5OSi4B4+HY4BtAoJSMGuD/L5hfTzoRVbzEm2WTvg==}
+
+  '@shikijs/types@1.21.0':
+    resolution: {integrity: sha512-tzndANDhi5DUndBtpojEq/42+dpUF2wS7wdCDQaFtIXm3Rd1QkrcVgSSRLOvEwexekihOXfbYJINW37g96tJRw==}
+
+  '@shikijs/vscode-textmate@9.2.2':
+    resolution: {integrity: sha512-TMp15K+GGYrWlZM8+Lnj9EaHEFmOen0WJBrfa17hF7taDOYthuPPV0GWzfd/9iMij0akS/8Yw2ikquH7uVi/fg==}
 
   '@sideway/address@4.1.4':
     resolution: {integrity: sha512-7vwq+rOHVWjyXxVlR76Agnvhy8I9rpzjosTESvmhNeXOXdZZB15Fl+TI9x1SiHZH5Jv2wTGduSxFDIaq0m3DUw==}
 
+  '@sideway/address@4.1.5':
+    resolution: {integrity: sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q==}
+
   '@sideway/formula@3.0.1':
     resolution: {integrity: sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==}
 
   '@sideway/pinpoint@2.0.0':
     resolution: {integrity: sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==}
 
-  '@simplewebauthn/server@10.0.0':
-    resolution: {integrity: sha512-w5eIoiF7ltg1sgggjY5Tx654j+DBuyEx2B3869jjmPp0xl2Z4BUP4kJ3yJ6DnZIv+ZYYntT3E6nZXNjPOQbrtw==}
+  '@simplewebauthn/server@10.0.1':
+    resolution: {integrity: sha512-djNWcRn+H+6zvihBFJSpG3fzb0NQS9c/Mw5dYOtZ9H+oDw8qn9Htqxt4cpqRvSOAfwqP7rOvE9rwqVaoGGc3hg==}
     engines: {node: '>=20.0.0'}
 
   '@simplewebauthn/types@10.0.0':
@@ -3667,9 +3745,13 @@ packages:
     resolution: {integrity: sha512-CX6t4SYQ37lzxicAqsBtxA3OseeoVrh9cSJ5PFYam0GksYlupRfy1A+Q4aYD3zvcfECLc0zO2u+ZnR2UYKvCrw==}
     engines: {node: '>=14.16'}
 
-  '@sindresorhus/is@6.1.0':
-    resolution: {integrity: sha512-BuvU07zq3tQ/2SIgBsEuxKYDyDjC0n7Zir52bpHy2xnBbW81+po43aLFPLbeV3HRAheFbGud1qgcqSYfhtHMAg==}
-    engines: {node: '>=16'}
+  '@sindresorhus/is@7.0.0':
+    resolution: {integrity: sha512-WDTlVTyvFivSOuyvMeedzg2hdoBLZ3f1uNVuEida2Rl9BrfjrIRjWA/VZIrMRLvSwJYCAlCRA3usDt1THytxWQ==}
+    engines: {node: '>=18'}
+
+  '@sindresorhus/merge-streams@4.0.0':
+    resolution: {integrity: sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==}
+    engines: {node: '>=18'}
 
   '@sinonjs/commons@2.0.0':
     resolution: {integrity: sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==}
@@ -3689,277 +3771,327 @@ packages:
   '@sinonjs/text-encoding@0.7.2':
     resolution: {integrity: sha512-sXXKG+uL9IrKqViTtao2Ws6dy0znu9sOaP1di/jKGW1M6VssO8vlpXCQcpZ+jisQ1tTFAC5Jo/EOzFbggBagFQ==}
 
-  '@smithy/abort-controller@2.0.14':
-    resolution: {integrity: sha512-zXtteuYLWbSXnzI3O6xq3FYvigYZFW8mdytGibfarLL2lxHto9L3ILtGVnVGmFZa7SDh62l39EnU5hesLN87Fw==}
-    engines: {node: '>=14.0.0'}
-
   '@smithy/abort-controller@2.2.0':
     resolution: {integrity: sha512-wRlta7GuLWpTqtFfGo+nZyOO1vEvewdNR1R4rTxpC8XU6vG/NDyrFBhwLZsqg1NUoR1noVaXJPC/7ZK47QCySw==}
     engines: {node: '>=14.0.0'}
 
-  '@smithy/chunked-blob-reader-native@2.0.0':
-    resolution: {integrity: sha512-HM8V2Rp1y8+1343tkZUKZllFhEQPNmpNdgFAncbTsxkZ18/gqjk23XXv3qGyXWp412f3o43ZZ1UZHVcHrpRnCQ==}
+  '@smithy/abort-controller@3.1.1':
+    resolution: {integrity: sha512-MBJBiidoe+0cTFhyxT8g+9g7CeVccLM0IOKKUMCNQ1CNMJ/eIfoo0RTfVrXOONEI1UCN1W+zkiHSbzUNE9dZtQ==}
+    engines: {node: '>=16.0.0'}
 
-  '@smithy/chunked-blob-reader@2.0.0':
-    resolution: {integrity: sha512-k+J4GHJsMSAIQPChGBrjEmGS+WbPonCXesoqP9fynIqjn7rdOThdH8FAeCmokP9mxTYKQAKoHCLPzNlm6gh7Wg==}
+  '@smithy/chunked-blob-reader-native@3.0.0':
+    resolution: {integrity: sha512-VDkpCYW+peSuM4zJip5WDfqvg2Mo/e8yxOv3VF1m11y7B8KKMKVFtmZWDe36Fvk8rGuWrPZHHXZ7rR7uM5yWyg==}
 
-  '@smithy/config-resolver@2.0.9':
-    resolution: {integrity: sha512-QBkGPLUqyPmis9Erz8v4q5lo/ErnF7+GD5WZHa6JZiXopUPfaaM+B21n8gzS5xCkIXZmnwzNQhObP9xQPu8oqQ==}
-    engines: {node: '>=14.0.0'}
+  '@smithy/chunked-blob-reader@3.0.0':
+    resolution: {integrity: sha512-sbnURCwjF0gSToGlsBiAmd1lRCmSn72nu9axfJu5lIx6RUEgHu6GwTMbqCdhQSi0Pumcm5vFxsi9XWXb2mTaoA==}
 
-  '@smithy/credential-provider-imds@2.0.11':
-    resolution: {integrity: sha512-uJJs8dnM5iXkn8a2GaKvlKMhcOJ+oJPYqY9gY3CM/EieCVObIDjxUtR/g8lU/k/A+OauA78GzScAfulmFjPOYA==}
-    engines: {node: '>=14.0.0'}
+  '@smithy/config-resolver@3.0.5':
+    resolution: {integrity: sha512-SkW5LxfkSI1bUC74OtfBbdz+grQXYiPYolyu8VfpLIjEoN/sHVBlLeGXMQ1vX4ejkgfv6sxVbQJ32yF2cl1veA==}
+    engines: {node: '>=16.0.0'}
 
-  '@smithy/eventstream-codec@2.0.8':
-    resolution: {integrity: sha512-onO4to8ujCKn4m5XagReT9Nc6FlNG5vveuvjp1H7AtaG7njdet1LOl6/jmUOkskF2C/w+9jNw3r9Ak+ghOvN0A==}
+  '@smithy/core@2.3.1':
+    resolution: {integrity: sha512-BC7VMXx/1BCmRPCVzzn4HGWAtsrb7/0758EtwOGFJQrlSwJBEjCcDLNZLFoL/68JexYa2s+KmgL/UfmXdG6v1w==}
+    engines: {node: '>=16.0.0'}
 
-  '@smithy/eventstream-serde-browser@2.0.8':
-    resolution: {integrity: sha512-/RGlkKUnC0sd+xKBKH/2APSBRmVMZTeLOKZMhrZmrO+ONoU+DwyMr/RLJ6WnmBKN+2ebjffM4pcIJTKLNNDD8g==}
-    engines: {node: '>=14.0.0'}
+  '@smithy/credential-provider-imds@3.2.0':
+    resolution: {integrity: sha512-0SCIzgd8LYZ9EJxUjLXBmEKSZR/P/w6l7Rz/pab9culE/RWuqelAKGJvn5qUOl8BgX8Yj5HWM50A5hiB/RzsgA==}
+    engines: {node: '>=16.0.0'}
 
-  '@smithy/eventstream-serde-config-resolver@2.0.8':
-    resolution: {integrity: sha512-EyAEj258eMUv9zcMvBbqrInh2eHRYuiwQAjXDMxZFCyP+JePzQB6O++3wFwjQeRKMFFgZipNgnEXfReII4+NAw==}
-    engines: {node: '>=14.0.0'}
+  '@smithy/eventstream-codec@3.1.2':
+    resolution: {integrity: sha512-0mBcu49JWt4MXhrhRAlxASNy0IjDRFU+aWNDRal9OtUJvJNiwDuyKMUONSOjLjSCeGwZaE0wOErdqULer8r7yw==}
 
-  '@smithy/eventstream-serde-node@2.0.8':
-    resolution: {integrity: sha512-FMBatSUSKwh6aguKVJokXfJaV8nqsuCkCZHb9MP9zah0ZF+ohbTLeeed7DQGeTVBueVIVWEzIsShPxtxBv7MMQ==}
-    engines: {node: '>=14.0.0'}
+  '@smithy/eventstream-serde-browser@3.0.5':
+    resolution: {integrity: sha512-dEyiUYL/ekDfk+2Ra4GxV+xNnFoCmk1nuIXg+fMChFTrM2uI/1r9AdiTYzPqgb72yIv/NtAj6C3dG//1wwgakQ==}
+    engines: {node: '>=16.0.0'}
 
-  '@smithy/eventstream-serde-universal@2.0.8':
-    resolution: {integrity: sha512-6InMXH8BUKoEDa6CAuxR4Gn8Gf2vBfVtjA9A6zDKZClYHT+ANUJS+2EtOBc5wECJJGk4KLn5ajQyrt9MBv5lcw==}
-    engines: {node: '>=14.0.0'}
+  '@smithy/eventstream-serde-config-resolver@3.0.3':
+    resolution: {integrity: sha512-NVTYjOuYpGfrN/VbRQgn31x73KDLfCXCsFdad8DiIc3IcdxL+dYA9zEQPyOP7Fy2QL8CPy2WE4WCUD+ZsLNfaQ==}
+    engines: {node: '>=16.0.0'}
 
-  '@smithy/fetch-http-handler@2.1.4':
-    resolution: {integrity: sha512-SL24M9W5ERByoXaVicRx+bj9GJVujDnPn+QO7GY7adhY0mPGa6DSF58pVKsgIh4r5Tx/k3SWCPlH4BxxSxA/fQ==}
+  '@smithy/eventstream-serde-node@3.0.4':
+    resolution: {integrity: sha512-mjlG0OzGAYuUpdUpflfb9zyLrBGgmQmrobNT8b42ZTsGv/J03+t24uhhtVEKG/b2jFtPIHF74Bq+VUtbzEKOKg==}
+    engines: {node: '>=16.0.0'}
 
-  '@smithy/hash-blob-browser@2.0.8':
-    resolution: {integrity: sha512-IgvRlBMfg/qLg321a59T1yTdEEbaizLrEVsU3DHj65DAO4lFRMF5f+l7vuV+je6m1G9wSD5GQXLturX8qlGb4g==}
+  '@smithy/eventstream-serde-universal@3.0.4':
+    resolution: {integrity: sha512-Od9dv8zh3PgOD7Vj4T3HSuox16n0VG8jJIM2gvKASL6aCtcS8CfHZDWe1Ik3ZXW6xBouU+45Q5wgoliWDZiJ0A==}
+    engines: {node: '>=16.0.0'}
 
-  '@smithy/hash-node@2.0.8':
-    resolution: {integrity: sha512-yZL/nmxZzjZV5/QX5JWSgXlt0HxuMTwFO89CS++jOMMPiCMZngf6VYmtNdccs8IIIAMmfQeTzwu07XgUE/Zd3Q==}
-    engines: {node: '>=14.0.0'}
+  '@smithy/fetch-http-handler@3.2.4':
+    resolution: {integrity: sha512-kBprh5Gs5h7ug4nBWZi1FZthdqSM+T7zMmsZxx0IBvWUn7dK3diz2SHn7Bs4dQGFDk8plDv375gzenDoNwrXjg==}
 
-  '@smithy/hash-stream-node@2.0.8':
-    resolution: {integrity: sha512-82zC6I9ZJycbEZH8TVyXyBx9c2ZIPQDgBvM0x5AFPUl/i1AxwKKX+lwYRnzgkF//cYhIIoJaCfJ9mjSMPRGvCQ==}
-    engines: {node: '>=14.0.0'}
+  '@smithy/hash-blob-browser@3.1.2':
+    resolution: {integrity: sha512-hAbfqN2UbISltakCC2TP0kx4LqXBttEv2MqSPE98gVuDFMf05lU+TpC41QtqGP3Ff5A3GwZMPfKnEy0VmEUpmg==}
 
-  '@smithy/invalid-dependency@2.0.8':
-    resolution: {integrity: sha512-88VOS7W3KzUz/bNRc+Sl/F/CDIasFspEE4G39YZRHIh9YmsXF7GUyVaAKURfMNulTie62ayk6BHC9O0nOBAVgQ==}
+  '@smithy/hash-node@3.0.3':
+    resolution: {integrity: sha512-2ctBXpPMG+B3BtWSGNnKELJ7SH9e4TNefJS0cd2eSkOOROeBnnVBnAy9LtJ8tY4vUEoe55N4CNPxzbWvR39iBw==}
+    engines: {node: '>=16.0.0'}
+
+  '@smithy/hash-stream-node@3.1.2':
+    resolution: {integrity: sha512-PBgDMeEdDzi6JxKwbfBtwQG9eT9cVwsf0dZzLXoJF4sHKHs5HEo/3lJWpn6jibfJwT34I1EBXpBnZE8AxAft6g==}
+    engines: {node: '>=16.0.0'}
+
+  '@smithy/invalid-dependency@3.0.3':
+    resolution: {integrity: sha512-ID1eL/zpDULmHJbflb864k72/SNOZCADRc9i7Exq3RUNJw6raWUSlFEQ+3PX3EYs++bTxZB2dE9mEHTQLv61tw==}
 
   '@smithy/is-array-buffer@2.0.0':
     resolution: {integrity: sha512-z3PjFjMyZNI98JFRJi/U0nGoLWMSJlDjAW4QUX2WNZLas5C0CmVV6LJ01JI0k90l7FvpmixjWxPFmENSClQ7ug==}
     engines: {node: '>=14.0.0'}
 
-  '@smithy/md5-js@2.0.8':
-    resolution: {integrity: sha512-1VVECXEiuJvjXv+mudiaUFKYwgDLOWz5MTTy8RzbrPiU3GiOb3/o5/urdkYpqmgoMfxdvxxOw/Adjv2dV2q2Yg==}
+  '@smithy/is-array-buffer@3.0.0':
+    resolution: {integrity: sha512-+Fsu6Q6C4RSJiy81Y8eApjEB5gVtM+oFKTffg+jSuwtvomJJrhUJBu2zS8wjXSgH/g1MKEWrzyChTBe6clb5FQ==}
+    engines: {node: '>=16.0.0'}
 
-  '@smithy/middleware-content-length@2.0.10':
-    resolution: {integrity: sha512-EGSbysyA4jH0p3xI6G0jdXoj9Iz9GUnAta6aEaHtXm3wVWtenRf80y2TeVvNkVSr5jwKOdSCjKIRI2l1A/oZLA==}
-    engines: {node: '>=14.0.0'}
+  '@smithy/md5-js@3.0.3':
+    resolution: {integrity: sha512-O/SAkGVwpWmelpj/8yDtsaVe6sINHLB1q8YE/+ZQbDxIw3SRLbTZuRaI10K12sVoENdnHqzPp5i3/H+BcZ3m3Q==}
 
-  '@smithy/middleware-endpoint@2.0.8':
-    resolution: {integrity: sha512-yOpogfG2d2V0cbJdAJ6GLAWkNOc9pVsL5hZUfXcxJu408N3CUCsXzIAFF6+70ZKSE+lCfG3GFErcSXv/UfUbjw==}
-    engines: {node: '>=14.0.0'}
+  '@smithy/middleware-content-length@3.0.5':
+    resolution: {integrity: sha512-ILEzC2eyxx6ncej3zZSwMpB5RJ0zuqH7eMptxC4KN3f+v9bqT8ohssKbhNR78k/2tWW+KS5Spw+tbPF4Ejyqvw==}
+    engines: {node: '>=16.0.0'}
 
-  '@smithy/middleware-retry@2.0.11':
-    resolution: {integrity: sha512-pknfokumZ+wvBERSuKAI2vVr+aK3ZgPiWRg6+0ZG4kKJogBRpPmDGWw+Jht0izS9ZaEbIobNzueIb4wD33JJVg==}
-    engines: {node: '>=14.0.0'}
+  '@smithy/middleware-endpoint@3.1.0':
+    resolution: {integrity: sha512-5y5aiKCEwg9TDPB4yFE7H6tYvGFf1OJHNczeY10/EFF8Ir8jZbNntQJxMWNfeQjC1mxPsaQ6mR9cvQbf+0YeMw==}
+    engines: {node: '>=16.0.0'}
 
-  '@smithy/middleware-serde@2.0.8':
-    resolution: {integrity: sha512-Is0sm+LiNlgsc0QpstDzifugzL9ehno1wXp109GgBgpnKTK3j+KphiparBDI4hWTtH9/7OUsxuspNqai2yyhcg==}
-    engines: {node: '>=14.0.0'}
+  '@smithy/middleware-retry@3.0.13':
+    resolution: {integrity: sha512-zvCLfaRYCaUmjbF2yxShGZdolSHft7NNCTA28HVN9hKcEbOH+g5irr1X9s+in8EpambclGnevZY4A3lYpvDCFw==}
+    engines: {node: '>=16.0.0'}
 
-  '@smithy/middleware-stack@2.0.1':
-    resolution: {integrity: sha512-UexsfY6/oQZRjTQL56s9AKtMcR60tBNibSgNYX1I2WXaUaXg97W9JCkFyth85TzBWKDBTyhLfenrukS/kyu54A==}
-    engines: {node: '>=14.0.0'}
+  '@smithy/middleware-serde@3.0.3':
+    resolution: {integrity: sha512-puUbyJQBcg9eSErFXjKNiGILJGtiqmuuNKEYNYfUD57fUl4i9+mfmThtQhvFXU0hCVG0iEJhvQUipUf+/SsFdA==}
+    engines: {node: '>=16.0.0'}
 
-  '@smithy/node-config-provider@2.0.11':
-    resolution: {integrity: sha512-CaR1dciSSGKttjhcefpytYjsfI/Yd5mqL8am4wfmyFCDxSiPsvnEWHl8UjM/RbcAjX0klt+CeIKPSHEc0wGvJA==}
-    engines: {node: '>=14.0.0'}
+  '@smithy/middleware-stack@3.0.3':
+    resolution: {integrity: sha512-r4klY9nFudB0r9UdSMaGSyjyQK5adUyPnQN/ZM6M75phTxOdnc/AhpvGD1fQUvgmqjQEBGCwpnPbDm8pH5PapA==}
+    engines: {node: '>=16.0.0'}
+
+  '@smithy/node-config-provider@3.1.4':
+    resolution: {integrity: sha512-YvnElQy8HR4vDcAjoy7Xkx9YT8xZP4cBXcbJSgm/kxmiQu08DwUwj8rkGnyoJTpfl/3xYHH+d8zE+eHqoDCSdQ==}
+    engines: {node: '>=16.0.0'}
 
   '@smithy/node-http-handler@2.5.0':
     resolution: {integrity: sha512-mVGyPBzkkGQsPoxQUbxlEfRjrj6FPyA3u3u2VXGr9hT8wilsoQdZdvKpMBFMB8Crfhv5dNkKHIW0Yyuc7eABqA==}
     engines: {node: '>=14.0.0'}
 
-  '@smithy/property-provider@2.0.9':
-    resolution: {integrity: sha512-25pPZ8f8DeRwYI5wbPRZaoMoR+3vrw8DwbA0TjP+GsdiB2KxScndr4HQehiJ5+WJ0giOTWhLz0bd+7Djv1qpUQ==}
-    engines: {node: '>=14.0.0'}
+  '@smithy/node-http-handler@3.1.4':
+    resolution: {integrity: sha512-+UmxgixgOr/yLsUxcEKGH0fMNVteJFGkmRltYFHnBMlogyFdpzn2CwqWmxOrfJELhV34v0WSlaqG1UtE1uXlJg==}
+    engines: {node: '>=16.0.0'}
 
-  '@smithy/protocol-http@3.0.10':
-    resolution: {integrity: sha512-6+tjNk7rXW7YTeGo9qwxXj/2BFpJTe37kTj3EnZCoX/nH+NP/WLA7O83fz8XhkGqsaAhLUPo/bB12vvd47nsmg==}
-    engines: {node: '>=14.0.0'}
+  '@smithy/property-provider@3.1.3':
+    resolution: {integrity: sha512-zahyOVR9Q4PEoguJ/NrFP4O7SMAfYO1HLhB18M+q+Z4KFd4V2obiMnlVoUFzFLSPeVt1POyNWneHHrZaTMoc/g==}
+    engines: {node: '>=16.0.0'}
 
   '@smithy/protocol-http@3.3.0':
     resolution: {integrity: sha512-Xy5XK1AFWW2nlY/biWZXu6/krgbaf2dg0q492D8M5qthsnU2H+UgFeZLbM76FnH7s6RO/xhQRkj+T6KBO3JzgQ==}
     engines: {node: '>=14.0.0'}
 
-  '@smithy/querystring-builder@2.0.14':
-    resolution: {integrity: sha512-lQ4pm9vTv9nIhl5jt6uVMPludr6syE2FyJmHsIJJuOD7QPIJnrf9HhUGf1iHh9KJ4CUv21tpOU3X6s0rB6uJ0g==}
-    engines: {node: '>=14.0.0'}
+  '@smithy/protocol-http@4.1.0':
+    resolution: {integrity: sha512-dPVoHYQ2wcHooGXg3LQisa1hH0e4y0pAddPMeeUPipI1tEOqL6A4N0/G7abeq+K8wrwSgjk4C0wnD1XZpJm5aA==}
+    engines: {node: '>=16.0.0'}
 
   '@smithy/querystring-builder@2.2.0':
     resolution: {integrity: sha512-L1kSeviUWL+emq3CUVSgdogoM/D9QMFaqxL/dd0X7PCNWmPXqt+ExtrBjqT0V7HLN03Vs9SuiLrG3zy3JGnE5A==}
     engines: {node: '>=14.0.0'}
 
-  '@smithy/querystring-parser@2.0.8':
-    resolution: {integrity: sha512-ArbanNuR7O/MmTd90ZqhDqGOPPDYmxx3huHxD+R3cuCnazcK/1tGQA+SnnR5307T7ZRb5WTpB6qBggERuibVSA==}
-    engines: {node: '>=14.0.0'}
+  '@smithy/querystring-builder@3.0.3':
+    resolution: {integrity: sha512-vyWckeUeesFKzCDaRwWLUA1Xym9McaA6XpFfAK5qI9DKJ4M33ooQGqvM4J+LalH4u/Dq9nFiC8U6Qn1qi0+9zw==}
+    engines: {node: '>=16.0.0'}
 
-  '@smithy/service-error-classification@2.0.1':
-    resolution: {integrity: sha512-QHa9+t+v4s0cMuDCcbjIJN67mNZ42/+fc3jKe8P6ZMPXZl5ksKk6a8vhZ/m494GZng5eFTc3OePv+NF9cG83yg==}
-    engines: {node: '>=14.0.0'}
+  '@smithy/querystring-parser@3.0.3':
+    resolution: {integrity: sha512-zahM1lQv2YjmznnfQsWbYojFe55l0SLG/988brlLv1i8z3dubloLF+75ATRsqPBboUXsW6I9CPGE5rQgLfY0vQ==}
+    engines: {node: '>=16.0.0'}
 
-  '@smithy/shared-ini-file-loader@2.0.10':
-    resolution: {integrity: sha512-jWASteSezRKohJ7GdA7pHDvmr7Q7tw3b5mu3xLHIkZy/ICftJ+O7aqNaF8wklhI7UNFoQ7flFRM3Rd0KA+1BbQ==}
-    engines: {node: '>=14.0.0'}
+  '@smithy/service-error-classification@3.0.3':
+    resolution: {integrity: sha512-Jn39sSl8cim/VlkLsUhRFq/dKDnRUFlfRkvhOJaUbLBXUsLRLNf9WaxDv/z9BjuQ3A6k/qE8af1lsqcwm7+DaQ==}
+    engines: {node: '>=16.0.0'}
 
-  '@smithy/signature-v4@2.0.5':
-    resolution: {integrity: sha512-ABIzXmUDXK4n2c9cXjQLELgH2RdtABpYKT+U131e2I6RbCypFZmxIHmIBufJzU2kdMCQ3+thBGDWorAITFW04A==}
-    engines: {node: '>=14.0.0'}
+  '@smithy/shared-ini-file-loader@3.1.4':
+    resolution: {integrity: sha512-qMxS4hBGB8FY2GQqshcRUy1K6k8aBWP5vwm8qKkCT3A9K2dawUwOIJfqh9Yste/Bl0J2lzosVyrXDj68kLcHXQ==}
+    engines: {node: '>=16.0.0'}
 
-  '@smithy/smithy-client@2.1.5':
-    resolution: {integrity: sha512-7S865uKzsxApM8W8Q6zkij7tcUFgaG8PuADMFdMt1yL/ku3d0+s6Zwrg3N7iXCPM08Gu/mf0BIfTXIu/9i450Q==}
-    engines: {node: '>=14.0.0'}
+  '@smithy/signature-v4@4.1.0':
+    resolution: {integrity: sha512-aRryp2XNZeRcOtuJoxjydO6QTaVhxx/vjaR+gx7ZjaFgrgPRyZ3HCTbfwqYj6ZWEBHkCSUfcaymKPURaByukag==}
+    engines: {node: '>=16.0.0'}
+
+  '@smithy/smithy-client@3.1.11':
+    resolution: {integrity: sha512-l0BpyYkciNyMaS+PnFFz4aO5sBcXvGLoJd7mX9xrMBIm2nIQBVvYgp2ZpPDMzwjKCavsXu06iuCm0F6ZJZc6yQ==}
+    engines: {node: '>=16.0.0'}
 
   '@smithy/types@2.12.0':
     resolution: {integrity: sha512-QwYgloJ0sVNBeBuBs65cIkTbfzV/Q6ZNPCJ99EICFEdJYG50nGIY/uYXp+TbsdJReIuPr0a0kXmCvren3MbRRw==}
     engines: {node: '>=14.0.0'}
 
-  '@smithy/types@2.6.0':
-    resolution: {integrity: sha512-PgqxJq2IcdMF9iAasxcqZqqoOXBHufEfmbEUdN1pmJrJltT42b0Sc8UiYSWWzKkciIp9/mZDpzYi4qYG1qqg6g==}
-    engines: {node: '>=14.0.0'}
+  '@smithy/types@3.3.0':
+    resolution: {integrity: sha512-IxvBBCTFDHbVoK7zIxqA1ZOdc4QfM5HM7rGleCuHi7L1wnKv5Pn69xXJQ9hgxH60ZVygH9/JG0jRgtUncE3QUA==}
+    engines: {node: '>=16.0.0'}
 
-  '@smithy/url-parser@2.0.8':
-    resolution: {integrity: sha512-wQw7j004ScCrBRJ+oNPXlLE9mtofxyadSZ9D8ov/rHkyurS7z1HTNuyaGRj6OvKsEk0SVQsuY0C9+EfM75XTkw==}
+  '@smithy/url-parser@3.0.3':
+    resolution: {integrity: sha512-pw3VtZtX2rg+s6HMs6/+u9+hu6oY6U7IohGhVNnjbgKy86wcIsSZwgHrFR+t67Uyxvp4Xz3p3kGXXIpTNisq8A==}
 
-  '@smithy/util-base64@2.0.0':
-    resolution: {integrity: sha512-Zb1E4xx+m5Lud8bbeYi5FkcMJMnn+1WUnJF3qD7rAdXpaL7UjkFQLdmW5fHadoKbdHpwH9vSR8EyTJFHJs++tA==}
-    engines: {node: '>=14.0.0'}
+  '@smithy/util-base64@3.0.0':
+    resolution: {integrity: sha512-Kxvoh5Qtt0CDsfajiZOCpJxgtPHXOKwmM+Zy4waD43UoEMA+qPxxa98aE/7ZhdnBFZFXMOiBR5xbcaMhLtznQQ==}
+    engines: {node: '>=16.0.0'}
 
-  '@smithy/util-body-length-browser@2.0.0':
-    resolution: {integrity: sha512-JdDuS4ircJt+FDnaQj88TzZY3+njZ6O+D3uakS32f2VNnDo3vyEuNdBOh/oFd8Df1zSZOuH1HEChk2AOYDezZg==}
+  '@smithy/util-body-length-browser@3.0.0':
+    resolution: {integrity: sha512-cbjJs2A1mLYmqmyVl80uoLTJhAcfzMOyPgjwAYusWKMdLeNtzmMz9YxNl3/jRLoxSS3wkqkf0jwNdtXWtyEBaQ==}
 
-  '@smithy/util-body-length-node@2.1.0':
-    resolution: {integrity: sha512-/li0/kj/y3fQ3vyzn36NTLGmUwAICb7Jbe/CsWCktW363gh1MOcpEcSO3mJ344Gv2dqz8YJCLQpb6hju/0qOWw==}
-    engines: {node: '>=14.0.0'}
+  '@smithy/util-body-length-node@3.0.0':
+    resolution: {integrity: sha512-Tj7pZ4bUloNUP6PzwhN7K386tmSmEET9QtQg0TgdNOnxhZvCssHji+oZTUIuzxECRfG8rdm2PMw2WCFs6eIYkA==}
+    engines: {node: '>=16.0.0'}
 
   '@smithy/util-buffer-from@2.0.0':
     resolution: {integrity: sha512-/YNnLoHsR+4W4Vf2wL5lGv0ksg8Bmk3GEGxn2vEQt52AQaPSCuaO5PM5VM7lP1K9qHRKHwrPGktqVoAHKWHxzw==}
     engines: {node: '>=14.0.0'}
 
-  '@smithy/util-config-provider@2.0.0':
-    resolution: {integrity: sha512-xCQ6UapcIWKxXHEU4Mcs2s7LcFQRiU3XEluM2WcCjjBtQkUN71Tb+ydGmJFPxMUrW/GWMgQEEGipLym4XG0jZg==}
-    engines: {node: '>=14.0.0'}
+  '@smithy/util-buffer-from@3.0.0':
+    resolution: {integrity: sha512-aEOHCgq5RWFbP+UDPvPot26EJHjOC+bRgse5A8V3FSShqd5E5UN4qc7zkwsvJPPAVsf73QwYcHN1/gt/rtLwQA==}
+    engines: {node: '>=16.0.0'}
 
-  '@smithy/util-defaults-mode-browser@2.0.9':
-    resolution: {integrity: sha512-JONLJVQWT8165XoSV36ERn3SVlZLJJ4D6IeGsCSePv65Uxa93pzSLE0UMSR9Jwm4zix7rst9AS8W5QIypZWP8Q==}
+  '@smithy/util-config-provider@3.0.0':
+    resolution: {integrity: sha512-pbjk4s0fwq3Di/ANL+rCvJMKM5bzAQdE5S/6RL5NXgMExFAi6UgQMPOm5yPaIWPpr+EOXKXRonJ3FoxKf4mCJQ==}
+    engines: {node: '>=16.0.0'}
+
+  '@smithy/util-defaults-mode-browser@3.0.13':
+    resolution: {integrity: sha512-ZIRSUsnnMRStOP6OKtW+gCSiVFkwnfQF2xtf32QKAbHR6ACjhbAybDvry+3L5qQYdh3H6+7yD/AiUE45n8mTTw==}
     engines: {node: '>= 10.0.0'}
 
-  '@smithy/util-defaults-mode-node@2.0.11':
-    resolution: {integrity: sha512-tmqjNsfj+bgZN6jXBe6efZnukzILA7BUytHkzqikuRLNtR+0VVchQHvawD0w6vManh76rO81ydhioe7i4oBzuA==}
+  '@smithy/util-defaults-mode-node@3.0.13':
+    resolution: {integrity: sha512-voUa8TFJGfD+U12tlNNLCDlXibt9vRdNzRX45Onk/WxZe7TS+hTOZouEZRa7oARGicdgeXvt1A0W45qLGYdy+g==}
     engines: {node: '>= 10.0.0'}
 
-  '@smithy/util-hex-encoding@2.0.0':
-    resolution: {integrity: sha512-c5xY+NUnFqG6d7HFh1IFfrm3mGl29lC+vF+geHv4ToiuJCBmIfzx6IeHLg+OgRdPFKDXIw6pvi+p3CsscaMcMA==}
-    engines: {node: '>=14.0.0'}
+  '@smithy/util-endpoints@2.0.5':
+    resolution: {integrity: sha512-ReQP0BWihIE68OAblC/WQmDD40Gx+QY1Ez8mTdFMXpmjfxSyz2fVQu3A4zXRfQU9sZXtewk3GmhfOHswvX+eNg==}
+    engines: {node: '>=16.0.0'}
 
-  '@smithy/util-middleware@2.0.1':
-    resolution: {integrity: sha512-LnsBMi0Mg3gfz/TpNGLv2Jjcz2ra1OX5HR/4IaCepIYmtPQzqMWDdhX/XTW1LS8OZ0xbQuyQPcHkQ+2XkhWOVQ==}
-    engines: {node: '>=14.0.0'}
+  '@smithy/util-hex-encoding@3.0.0':
+    resolution: {integrity: sha512-eFndh1WEK5YMUYvy3lPlVmYY/fZcQE1D8oSf41Id2vCeIkKJXPcYDCZD+4+xViI6b1XSd7tE+s5AmXzz5ilabQ==}
+    engines: {node: '>=16.0.0'}
 
-  '@smithy/util-retry@2.0.1':
-    resolution: {integrity: sha512-naj4X0IafJ9yJnVJ58QgSMkCNLjyQOnyrnKh/T0f+0UOUxJiT8vuFn/hS7B/pNqbo2STY7PyJ4J4f+5YqxwNtA==}
-    engines: {node: '>= 14.0.0'}
+  '@smithy/util-middleware@3.0.3':
+    resolution: {integrity: sha512-l+StyYYK/eO3DlVPbU+4Bi06Jjal+PFLSMmlWM1BEwyLxZ3aKkf1ROnoIakfaA7mC6uw3ny7JBkau4Yc+5zfWw==}
+    engines: {node: '>=16.0.0'}
 
-  '@smithy/util-stream@2.0.11':
-    resolution: {integrity: sha512-2MeWfqSpZKdmEJ+tH8CJQSgzLWhH5cmdE24X7JB0hiamXrOmswWGGuPvyj/9sQCTclo57pNxLR2p7KrP8Ahiyg==}
-    engines: {node: '>=14.0.0'}
+  '@smithy/util-retry@3.0.3':
+    resolution: {integrity: sha512-AFw+hjpbtVApzpNDhbjNG5NA3kyoMs7vx0gsgmlJF4s+yz1Zlepde7J58zpIRIsdjc+emhpAITxA88qLkPF26w==}
+    engines: {node: '>=16.0.0'}
 
-  '@smithy/util-uri-escape@2.0.0':
-    resolution: {integrity: sha512-ebkxsqinSdEooQduuk9CbKcI+wheijxEb3utGXkCoYQkJnwTnLbH1JXGimJtUkQwNQbsbuYwG2+aFVyZf5TLaw==}
-    engines: {node: '>=14.0.0'}
+  '@smithy/util-stream@3.1.3':
+    resolution: {integrity: sha512-FIv/bRhIlAxC0U7xM1BCnF2aDRPq0UaelqBHkM2lsCp26mcBbgI0tCVTv+jGdsQLUmAMybua/bjDsSu8RQHbmw==}
+    engines: {node: '>=16.0.0'}
 
   '@smithy/util-uri-escape@2.2.0':
     resolution: {integrity: sha512-jtmJMyt1xMD/d8OtbVJ2gFZOSKc+ueYJZPW20ULW1GOp/q/YIM0wNh+u8ZFao9UaIGz4WoPW8hC64qlWLIfoDA==}
     engines: {node: '>=14.0.0'}
 
+  '@smithy/util-uri-escape@3.0.0':
+    resolution: {integrity: sha512-LqR7qYLgZTD7nWLBecUi4aqolw8Mhza9ArpNEQ881MJJIU2sE5iHCK6TdyqqzcDLy0OPe10IY4T8ctVdtynubg==}
+    engines: {node: '>=16.0.0'}
+
   '@smithy/util-utf8@2.0.0':
     resolution: {integrity: sha512-rctU1VkziY84n5OXe3bPNpKR001ZCME2JCaBBFgtiM2hfKbHFudc/BkMuPab8hRbLd0j3vbnBTTZ1igBf0wgiQ==}
     engines: {node: '>=14.0.0'}
 
-  '@smithy/util-waiter@2.0.8':
-    resolution: {integrity: sha512-t9yaoofNhdEhNlyDeV5al/JJEFJ62HIQBGktgCUE63MvKn6imnbkh1qISsYMyMYVLwhWCpZ3Xa3R1LA+SnWcng==}
-    engines: {node: '>=14.0.0'}
+  '@smithy/util-utf8@3.0.0':
+    resolution: {integrity: sha512-rUeT12bxFnplYDe815GXbq/oixEGHfRFFtcTF3YdDi/JaENIM6aSYYLJydG83UNzLXeRI5K8abYd/8Sp/QM0kA==}
+    engines: {node: '>=16.0.0'}
+
+  '@smithy/util-waiter@3.1.2':
+    resolution: {integrity: sha512-4pP0EV3iTsexDx+8PPGAKCQpd/6hsQBaQhqWzU4hqKPHN5epPsxKbvUTIiYIHTxaKt6/kEaqPBpu/ufvfbrRzw==}
+    engines: {node: '>=16.0.0'}
 
   '@sqltools/formatter@1.2.5':
     resolution: {integrity: sha512-Uy0+khmZqUrUGm5dmMqVlnvufZRSK0FbYzVgp0UMstm+F5+W2/jnEEQyc9vo1ZR/E5ZI/B1WjjoTqBqwJL6Krw==}
 
-  '@storybook/addon-actions@8.0.9':
-    resolution: {integrity: sha512-+I3VTvlKdj8puHeS2tyaOVv9syDiNLneVZbTfqN+UDOK2i42NwvZr8PVwjTzMlEj9eePJdCZgiipz55xwts5bw==}
-
-  '@storybook/addon-backgrounds@8.0.9':
-    resolution: {integrity: sha512-pCDecACrVyxPaJKEWS0sHsRb8xw+IPCSxDM1TkjaAQ6zZ468A/dcUnqW+LVK8bSXgQwWzn23wqnqPFSy5yptuQ==}
-
-  '@storybook/addon-controls@8.0.9':
-    resolution: {integrity: sha512-wWdmd62UP/sfPm8M7aJjEA+kEXTUIR/QsYi9PoYBhBZcXiikZ4kNan7oD7GfsnzGGKHrBVfwQhO+TqaENGYytA==}
-
-  '@storybook/addon-docs@8.0.9':
-    resolution: {integrity: sha512-x7hX7UuzJtClu6XwU3SfpyFhuckVcgqgD6BU6Ihxl0zs+i4xp6iKVXYSnHFMRM1sgoeT8TjPxab35Ke8w8BVRw==}
-
-  '@storybook/addon-essentials@8.0.9':
-    resolution: {integrity: sha512-mwAgdfrOsTuTDcagvM7veBh+iayZIWmKOazzkhrIWbhYcrXOsweigD2UOVeHgAiAzJK49znr4FXTCKcE1hOWcw==}
-
-  '@storybook/addon-highlight@8.0.9':
-    resolution: {integrity: sha512-vaRHGDbx7dpNpQECAHk5wczlZO3ntstprGlqnZt0o7ylz6xB5+pTQwTuIFty0hwKv+3TPcskzzifATUyEOEmyg==}
-
-  '@storybook/addon-interactions@8.0.9':
-    resolution: {integrity: sha512-AMIdNcyM6DDAWvMitBJMqp1iPZND8AXB4QT4VZHGMKG2ngHNKktriEKpTfcRkfKPGTJs9T+71dWfm6/R4tticw==}
-
-  '@storybook/addon-links@8.0.9':
-    resolution: {integrity: sha512-FVt+AdW3JFSqbJzkKiqKsMRWqHXqEvCBqFs7lNfk3OW0w0jfv1iREtrxE0dVdJoUFQC9V/2Im/EpJ7UB3C2bNQ==}
+  '@storybook/addon-actions@8.3.4':
+    resolution: {integrity: sha512-1y0yD3upKcyzNwwA6loAGW2cRDqExwl4oAT7GJQA4tmabI+fNwmANSgU/ezLvvSUf4Qo0eJHg2Zcn8y+Apq2eA==}
     peerDependencies:
-      react: ^16.8.0 || ^17.0.0 || ^18.0.0
+      storybook: ^8.3.4
+
+  '@storybook/addon-backgrounds@8.3.4':
+    resolution: {integrity: sha512-o3nl7cN3x8erJNxLEv8YptanEQAnbqnaseOAsvSC6/nnSAcRYBSs3BvekKvo4CcpS2mxn7F5NJTBFYnCXzy8EA==}
+    peerDependencies:
+      storybook: ^8.3.4
+
+  '@storybook/addon-controls@8.3.4':
+    resolution: {integrity: sha512-qQcaK6dczsb6wXkzGZKOjUYNA7FfKBewRv6NvoVKYY6LfhllGOkmUAtYpdtQG8adsZWTSoZaAOJS2vP2uM67lw==}
+    peerDependencies:
+      storybook: ^8.3.4
+
+  '@storybook/addon-docs@8.3.4':
+    resolution: {integrity: sha512-TWauhqF/gJgfwPuWeM6KM3LwC+ErCOM+K2z16w3vgao9s67sij8lnrdAoQ0hjA+kw2/KAdCakFS6FyciG81qog==}
+    peerDependencies:
+      storybook: ^8.3.4
+
+  '@storybook/addon-essentials@8.3.4':
+    resolution: {integrity: sha512-C3+3hpmSn/8zdx5sXEP0eE6zMzxgRosHVZYfe9nBcMiEDp6UKVUyHVetWxEULOEgN46ysjcpllZ0bUkRYxi2IQ==}
+    peerDependencies:
+      storybook: ^8.3.4
+
+  '@storybook/addon-highlight@8.3.4':
+    resolution: {integrity: sha512-rxZTeuZyZ7RnU+xmRhS01COFLbGnVEmlUNxBw8ArsrTEZKW5PbKpIxNLTj9F0zdH8H0MfryJGP+Aadcm0oHWlw==}
+    peerDependencies:
+      storybook: ^8.3.4
+
+  '@storybook/addon-interactions@8.3.4':
+    resolution: {integrity: sha512-ORxqe35wUmF7EDHo45mdDHiju3Ryk2pZ1vO9PyvW6ZItNlHt/IxAr7T/TysGejZ/eTBg6tMZR3ExGky3lTg/CQ==}
+    peerDependencies:
+      storybook: ^8.3.4
+
+  '@storybook/addon-links@8.3.4':
+    resolution: {integrity: sha512-R1DjARmxRIKJDGIG6uxmQ1yFNyoQbb+QIPUFjgWCak8+AdLJbC7W+Esvo9F5hQfh6czyy0piiM3qj5hpQJVh3A==}
+    peerDependencies:
+      react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta
+      storybook: ^8.3.4
     peerDependenciesMeta:
       react:
         optional: true
 
-  '@storybook/addon-mdx-gfm@8.0.9':
-    resolution: {integrity: sha512-AoEx+OGKANtVZgKyWKrQhGpMpDuc2S7PnOlNLUiDYzmj8ABAGPmEJmqeb/VHVgqLQSjhOW1fMsQ4fYsecvMxTQ==}
-
-  '@storybook/addon-measure@8.0.9':
-    resolution: {integrity: sha512-91svOOGEXmGG4USglwXLE3wtlUVgtbKJVxTKX7xRI+AC5JEEaKByVzP17/X8Qn/8HilUL7AfSQ0kCoqtPSJ5cA==}
-
-  '@storybook/addon-outline@8.0.9':
-    resolution: {integrity: sha512-fQ+jm356TgUnz81IxsC99/aOesbLw3N5OQRJpo/A6kqbLMzlq3ybVzuXYCKC3f0ArgQRNh4NoMeJBMRFMtaWRw==}
-
-  '@storybook/addon-storysource@8.0.9':
-    resolution: {integrity: sha512-5m3K2Rs4fQtKtqwrq4CDS1jK2wzWOlnxhE2ArX5XTWytb1am65CEPxfYTEQkvZH9oPGwX3cXytPCziynqysFMQ==}
-
-  '@storybook/addon-toolbars@8.0.9':
-    resolution: {integrity: sha512-nNSBnnBOhQ+EJwkrIkK4ZBYPcozNmEH770CZ/6NK85SUJ6WEBZapE6ru33jIUokFGEvlOlNCeai0GUc++cQP8w==}
-
-  '@storybook/addon-viewport@8.0.9':
-    resolution: {integrity: sha512-Ao4+D56cO7biaw+iTlMU1FBec1idX0cmdosDeCFZin06MSawcPkeBlRBeruaSQYdLes8TBMdZPFgfuqI5yIk6g==}
-
-  '@storybook/blocks@8.0.9':
-    resolution: {integrity: sha512-F2zSrfSwzTFN7qW3zB80tG+EXtmfmCDC6Ird0F7tolszb6tOqJcAcBOwQbE2O0wI63sLu21qxzXgaKBMkiWvJg==}
+  '@storybook/addon-mdx-gfm@8.3.4':
+    resolution: {integrity: sha512-O0sMP7VFo1fKsdViY+W6OMNYEXvB5FzEEsqgsydMcsJ0qOKR1li2l3cLCMLXdUKVZ+2uRbEhnm2RnB9RWF5O7g==}
     peerDependencies:
-      react: ^16.8.0 || ^17.0.0 || ^18.0.0
-      react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
+      storybook: ^8.3.4
+
+  '@storybook/addon-measure@8.3.4':
+    resolution: {integrity: sha512-IJ6WKEbqmG+r7sukFjo+bVmPB2Zry04sylGx/OGyOh7zIhhqAqpwOwMHP0uQrc3tLNnUM6qB/o83UyYX79ql+A==}
+    peerDependencies:
+      storybook: ^8.3.4
+
+  '@storybook/addon-outline@8.3.4':
+    resolution: {integrity: sha512-kRRJTTLKM8gMfeh/e83djN5XLlc0hFtr9zKWxuZxaXt9Hmr+9tH/PRFtVK/S4SgqnBDoXk49Wgv6raiwj5/e3A==}
+    peerDependencies:
+      storybook: ^8.3.4
+
+  '@storybook/addon-storysource@8.3.4':
+    resolution: {integrity: sha512-uHTUiK7dzWRZAKpPafBH3U5PWAP7+J97lg66HDKAHpmmQdy7v3HfXaYNX1FoI+PeC5piUxFETXM0z+BNvJCknA==}
+    peerDependencies:
+      storybook: ^8.3.4
+
+  '@storybook/addon-toolbars@8.3.4':
+    resolution: {integrity: sha512-Km1YciVIxqluDbd1xmHjANNFyMonEOtnA6e4MrnBnC9XkPXSigeFlj0JvxyI/zjBsLBoFRmQiwq55W6l3hQ9sA==}
+    peerDependencies:
+      storybook: ^8.3.4
+
+  '@storybook/addon-viewport@8.3.4':
+    resolution: {integrity: sha512-fU4LdXSSqIOLbCEh2leq/tZUYlFliXZBWr/+igQHdUoU7HY8RIImXqVUaR9wlCaTb48WezAWT60vJtwNijyIiQ==}
+    peerDependencies:
+      storybook: ^8.3.4
+
+  '@storybook/blocks@8.3.4':
+    resolution: {integrity: sha512-1g4aCrd5CcN+pVhF2ATu9ZRVvAIgBMb2yF9KkCuTpdvqKDuDNK3sGb0CxjS7jp3LOvyjJr9laTOQsz8v8MQc5A==}
+    peerDependencies:
+      react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta
+      react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta
+      storybook: ^8.3.4
     peerDependenciesMeta:
       react:
         optional: true
       react-dom:
         optional: true
 
-  '@storybook/builder-manager@8.0.9':
-    resolution: {integrity: sha512-/PxDwZIfMc/PSRZcasb6SIdGr3azIlenzx7dBF7Imt8i4jLHiAf1t00GvghlfJsvsrn4DNp95rbRbXTDyTj7tQ==}
-
-  '@storybook/builder-vite@8.0.9':
-    resolution: {integrity: sha512-7hEQFZIIz7VvxdySDpPE96iMvZxQvRZcRdhaNGeE+8Y2pyc3DgYE4WY3sjr+LUoB0a6TYLpAIKqbXwtLz0R+PQ==}
+  '@storybook/builder-vite@8.3.4':
+    resolution: {integrity: sha512-Sa6SZ7LeHpkrnuvua8P8MR8e8a+MPKbyMmr9TqCCy8Ud/t4AM4kHY3JpJGtrgeK9l43fBnBwfdZYoRl5J6oWeA==}
     peerDependencies:
       '@preact/preset-vite': '*'
+      storybook: ^8.3.4
       typescript: '>= 4.3.x'
       vite: ^4.0.0 || ^5.0.0
       vite-plugin-glimmerx: '*'
@@ -3971,138 +4103,115 @@ packages:
       vite-plugin-glimmerx:
         optional: true
 
-  '@storybook/channels@8.0.9':
-    resolution: {integrity: sha512-7Lcfyy5CsLWWGhMPO9WG4jZ/Alzp0AjepFhEreYHRPtQrfttp6qMAjE/g1aHgun0qHCYWxwqIG4NLR/hqDNrXQ==}
-
-  '@storybook/cli@8.0.9':
-    resolution: {integrity: sha512-lilYTKn8F5YOePijqfRYFa5v2mHVIJxPCIgTn+OXAmAFbcizZ6P8P6niU4J/NXulgx68Ln1M7hYhFtTP25hVTw==}
-    hasBin: true
-
-  '@storybook/client-logger@8.0.9':
-    resolution: {integrity: sha512-LzV/RHkbf07sRc1Jc0ff36RlapKf9Ul7/+9VMvVbI3hshH1CpmrZK4t/tsIdpX/EVOdJ1Gg5cES06PnleOAIPA==}
-
-  '@storybook/codemod@8.0.9':
-    resolution: {integrity: sha512-VBeGpSZSQpL6iyLLqceJSNGhdCqcNwv+xC/aWdDFOkmuE1YfbmNNwpa9QYv4ZFJ2QjUsm4iTWG60qK+9NXeSKA==}
-
-  '@storybook/components@8.0.9':
-    resolution: {integrity: sha512-JcwBGADzIJs0PSzqykrrD2KHzNG9wtexUOKuidt+FSv9szpUhe3qBAXIHpdfBRl7mOJ9TRZ5rt+mukEnfncdzA==}
+  '@storybook/components@8.3.4':
+    resolution: {integrity: sha512-iQzLJd87uGbFBbYNqlrN/ABrnx3dUrL0tjPCarzglzshZoPCNOsllJeJx5TJwB9kCxSZ8zB9TTOgr7NXl+oyVA==}
     peerDependencies:
-      react: ^16.8.0 || ^17.0.0 || ^18.0.0
-      react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
+      storybook: ^8.3.4
 
-  '@storybook/core-common@8.0.9':
-    resolution: {integrity: sha512-Jmue+sfHFb4GTYBzyWYw1MygoJiQSfISIrKmNIzAmZ+oR9EOr+jpu/i/bH+uetZ2Hqg1AGhj1VB7OtJp9HQyWw==}
+  '@storybook/core-events@8.3.4':
+    resolution: {integrity: sha512-3/5oJN2UnlmUILXCh7SXMTa2MYZOvrjeZCm3wFomoQASU2FFzS5AxBYYnwNdtrZmn4w32uw4T7qvA0+96Utwsg==}
+    peerDependencies:
+      storybook: ^8.3.4
 
-  '@storybook/core-events@8.0.9':
-    resolution: {integrity: sha512-DxSUx7wG9Qe3OFUBnv3OrYq48J8UWNo2DUR5/JecJCtp3n++L4fAEW3J0IF5FfxpQDMQSp1yTNjZ2PaWCMd2ag==}
+  '@storybook/core@8.3.4':
+    resolution: {integrity: sha512-4PZB91JJpuKfcjeOR2LXj3ABaPLLSd2P/SfYOKNCygrDstsQa/yay3/yN5Z9yi1cIG84KRr6/sUW+0x8HsGLPg==}
 
-  '@storybook/core-server@8.0.9':
-    resolution: {integrity: sha512-BIe1T5YUBl0GYxEjRoTQsvXD2pyuzL8rPTUD41zlzSQM0R8U6Iant9SzRms4u0+rKUm2mGxxKuODlUo5ewqaGA==}
+  '@storybook/csf-plugin@8.3.4':
+    resolution: {integrity: sha512-ZMFWYxeTN4GxCn8dyIH4roECyLDy29yv/QKM+pHM3AC5Ny2HWI35SohWao4fGBAFxPQFbR5hPN8xa6ofHPSSTg==}
+    peerDependencies:
+      storybook: ^8.3.4
 
-  '@storybook/csf-plugin@8.0.9':
-    resolution: {integrity: sha512-pXaNCNi++kxKsqSWwvx215fPx8cNqvepLVxQ7B69qXLHj80DHn0Q3DFBO3sLXNiQMJ2JK4OYcTxMfuOiyzszKw==}
-
-  '@storybook/csf-tools@8.0.9':
-    resolution: {integrity: sha512-PiNMhL97giLytTdQwuhsZ92buVk4gy9H/8DtrDhUc45/1OmF95gogm6T2Yap729SIFwgpOcuq/U3aVo6d6swVQ==}
-
-  '@storybook/csf@0.1.6':
-    resolution: {integrity: sha512-JjWnBptVhBYJ14yq+cHs66BXjykRUWQ5TlD1RhPxMOtavynYyV/Q+QR98/N+XB+mcPtFMm5I2DvNkpj0/Dk8Mw==}
-
-  '@storybook/docs-mdx@3.0.0':
-    resolution: {integrity: sha512-NmiGXl2HU33zpwTv1XORe9XG9H+dRUC1Jl11u92L4xr062pZtrShLmD4VKIsOQujxhhOrbxpwhNOt+6TdhyIdQ==}
-
-  '@storybook/docs-tools@8.0.9':
-    resolution: {integrity: sha512-OzogAeOmeHea/MxSPKRBWtOQVNSpoq+OOpimO9YRA5h5GBRJ2TUOGT44Gny6QT4ll5AvQA8fIiq9KezKcLekAg==}
+  '@storybook/csf@0.1.11':
+    resolution: {integrity: sha512-dHYFQH3mA+EtnCkHXzicbLgsvzYjcDJ1JWsogbItZogkPHgSJM/Wr71uMkcvw8v9mmCyP4NpXJuu6bPoVsOnzg==}
 
   '@storybook/global@5.0.0':
     resolution: {integrity: sha512-FcOqPAXACP0I3oJ/ws6/rrPT9WGhu915Cg8D02a9YxLo0DE9zI+a9A5gRGvmQ09fiWPukqI8ZAEoQEdWUKMQdQ==}
 
-  '@storybook/icons@1.2.5':
-    resolution: {integrity: sha512-m3jnuE+zmkZy6K+cdUDzAoUuCJyl0fWCAXPCji7VZCH1TzFohyvnPqhc9JMkQpanej2TOW3wWXaplPzHghcBSg==}
+  '@storybook/icons@1.2.12':
+    resolution: {integrity: sha512-UxgyK5W3/UV4VrI3dl6ajGfHM4aOqMAkFLWe2KibeQudLf6NJpDrDMSHwZj+3iKC4jFU7dkKbbtH2h/al4sW3Q==}
     engines: {node: '>=14.0.0'}
     peerDependencies:
       react: ^16.8.0 || ^17.0.0 || ^18.0.0
       react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
 
-  '@storybook/instrumenter@8.0.9':
-    resolution: {integrity: sha512-Gw74dgpTU/2p7FG0s7DuVdqCbJ2MEcSuRJjDo7HcXRYcvWp7I6Ly+C0v7N5VaoS+kbBVerAhLKIHZgG/LZf1og==}
-
-  '@storybook/manager-api@8.0.9':
-    resolution: {integrity: sha512-99b3yKArDSvfabXL7QE3nA95e4DdW/5H/ZCcr6/E2qCQJayZ6G1v/WWamKXbiaTpkndulFmcb/+ZmnDXcweIIQ==}
-
-  '@storybook/manager@8.0.9':
-    resolution: {integrity: sha512-+NnRo+5JQFGNqveKrLtC0b+Z08Tae4m44iq292bPeZMpr9OkFsIkU0PBPsHTHPkrqC/zZXRNsCsTEgvu3p2OIA==}
-
-  '@storybook/node-logger@8.0.9':
-    resolution: {integrity: sha512-5ajMdZFrYrjGLJOVDq7dlEQNFsgeLHymt4dCK9MulL/ciXykmXUZXE3Bye0wFy+I2qqDVvrvR8uzCvSFvm5MAQ==}
-
-  '@storybook/preview-api@8.0.9':
-    resolution: {integrity: sha512-zHfX34bkAMzzmE7vbDzaqFwSW6ExiBD0HiO1L/IsHF55f0f7xV7IH8uJyFRrDTvAoW3ReSxZDMvvPpeydFPKGA==}
-
-  '@storybook/preview@8.0.9':
-    resolution: {integrity: sha512-tFsR8xc8AYBZZrZw8enklFbSQt7ZAV+rv20BoxwDhd3q7fjXyK7O4moGPqUwBZ7rukTG13nPoISxr+VXAk/HYA==}
-
-  '@storybook/react-dom-shim@8.0.9':
-    resolution: {integrity: sha512-8011KlRuG3obr5pZZ7bcEyYYNWF3tR596YadoMd267NPoHKvwAbKL1L/DNgb6kiYjZDUf9QfaKSCWW31k0kcRQ==}
+  '@storybook/instrumenter@8.3.4':
+    resolution: {integrity: sha512-jVhfNOPekOyJmta0BTkQl9Z6rgRbFHlc0eV4z1oSrzaawSlc9TFzAeDCtCP57vg3FuBX8ydDYAvyZ7s4xPpLyg==}
     peerDependencies:
-      react: ^16.8.0 || ^17.0.0 || ^18.0.0
-      react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
+      storybook: ^8.3.4
 
-  '@storybook/react-vite@8.0.9':
-    resolution: {integrity: sha512-FT5KeulUH6grfzOJOxJCxpv9+81UVDrT9UPcgiFhQT9rKtsgmltezThwbHknByZNw3WWnf+ieidMLEis9hd73A==}
+  '@storybook/manager-api@8.3.4':
+    resolution: {integrity: sha512-tBx7MBfPUrKSlD666zmVjtIvoNArwCciZiW/UJ8IWmomrTJRfFBnVvPVM2gp1lkDIzRHYmz5x9BHbYaEDNcZWQ==}
+    peerDependencies:
+      storybook: ^8.3.4
+
+  '@storybook/preview-api@8.3.4':
+    resolution: {integrity: sha512-/YKQ3QDVSHmtFXXCShf5w0XMlg8wkfTpdYxdGv1CKFV8DU24f3N7KWulAgeWWCWQwBzZClDa9kzxmroKlQqx3A==}
+    peerDependencies:
+      storybook: ^8.3.4
+
+  '@storybook/react-dom-shim@8.3.4':
+    resolution: {integrity: sha512-L4llDvjaAzqPx6h4ddZMh36wPr75PrI2S8bXy+flLqAeVRYnRt4WNKGuxqH0t0U6MwId9+vlCZ13JBfFuY7eQQ==}
+    peerDependencies:
+      react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta
+      react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta
+      storybook: ^8.3.4
+
+  '@storybook/react-vite@8.3.4':
+    resolution: {integrity: sha512-0Xm8eTH+jQ7SV4moLkPN4G6U2IDrqXPXUqsZdXaccepIMcD4G75foQFm2LOrFJuY+IMySPspKeTqf8OLskPppw==}
     engines: {node: '>=18.0.0'}
     peerDependencies:
-      react: ^16.8.0 || ^17.0.0 || ^18.0.0
-      react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
+      react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta
+      react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta
+      storybook: ^8.3.4
       vite: ^4.0.0 || ^5.0.0
 
-  '@storybook/react@8.0.9':
-    resolution: {integrity: sha512-NeQ6suZG3HKikwe3Tx9cAIaRx7uP8FKCmlVvIiBg4LTTI5orCt94PPakvuZukZcbkqvcCnEBkebAzwUpn8PiJw==}
+  '@storybook/react@8.3.4':
+    resolution: {integrity: sha512-PA7iQL4/9X2/iLrv+AUPNtlhTHJWhDao9gQIT1Hef39FtFk+TU9lZGbv+g29R1H9V3cHP5162nG2aTu395kmbA==}
     engines: {node: '>=18.0.0'}
     peerDependencies:
-      react: ^16.8.0 || ^17.0.0 || ^18.0.0
-      react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
+      '@storybook/test': 8.3.4
+      react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta
+      react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta
+      storybook: ^8.3.4
       typescript: '>= 4.2.x'
     peerDependenciesMeta:
+      '@storybook/test':
+        optional: true
       typescript:
         optional: true
 
-  '@storybook/router@8.0.9':
-    resolution: {integrity: sha512-aAOWxbM9J4mt+cp4o88T2PB29mgBBTOzU37/pUsTHYnKnR9XI4npXEXdN8Gv+ryqM0kj0AbBpz/llFlnR2MNNA==}
-
-  '@storybook/source-loader@8.0.9':
-    resolution: {integrity: sha512-FDnpxIGE5nIYT15pvYe6rz95TSBrdLcDll7lOHNyZisWt19MI3wZU3YkVsFNRBuFrebo+FjVU3wHyoV81ur1Qw==}
-
-  '@storybook/telemetry@8.0.9':
-    resolution: {integrity: sha512-AGGfcup06t+wxhBIkHd0iybieOh9PDVZQJ9oPct5JGB39+ni9wvs0WOD+MYlHbsjp8id7+aGkh6mYuYOvfck+Q==}
-
-  '@storybook/test@8.0.9':
-    resolution: {integrity: sha512-bRd5tBJnPzR6UKbDXONWnFWtdkNOY99HMLDUWe5fTRo50GwkrpFBVqPflhdkruEeof0kAbBUbnoN2CIYgtnAFw==}
-
-  '@storybook/theming@8.0.9':
-    resolution: {integrity: sha512-jgfDuYoiNMMirQiASN3Eg0hGDXsEtpdAcMxyShqYGwu9elxgD9yUnYC2nSckYsM74a3ZQ3JaViZ9ZFSe2FHmeQ==}
+  '@storybook/source-loader@8.3.4':
+    resolution: {integrity: sha512-wH//LuWfa2iOmjykSqsub8M8e0EdhEUZoHUFhwBeizfYQQHaMaSEBhhAQCaWWKmdGB9lnCe1cioQ32c2IWtBIw==}
     peerDependencies:
-      react: ^16.8.0 || ^17.0.0 || ^18.0.0
-      react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
-    peerDependenciesMeta:
-      react:
-        optional: true
-      react-dom:
-        optional: true
+      storybook: ^8.3.4
 
-  '@storybook/types@8.0.9':
-    resolution: {integrity: sha512-ew0EXzk9k4B557P1qIWYrnvUcgaE0WWA5qQS0AU8l+fRTp5nvl9O3SP/zNIB0SN1qDFO7dXr3idTNTyIikTcEQ==}
+  '@storybook/test@8.3.4':
+    resolution: {integrity: sha512-HRiUenitln8QPHu6DEWUg9s9cEoiGN79lMykzXzw9shaUvdEIhWCsh82YKtmB3GJPj6qcc6dZL/Aio8srxyGAg==}
+    peerDependencies:
+      storybook: ^8.3.4
 
-  '@storybook/vue3-vite@8.0.9':
-    resolution: {integrity: sha512-IkzYsEyCo5HIvLWbJeGrBu/VIN4u+LvdIAz7vcFqVVXBtTUhy+9/8caLx8fdnM0FWgKcBRQs8HnjBB2V0lOFcg==}
+  '@storybook/theming@8.3.4':
+    resolution: {integrity: sha512-D4XVsQgTtpHEHLhwkx59aGy1GBwOedVr/mNns7hFrH8FjEpxrrWCuZQASq1ZpCl8LXlh7uvmT5sM2rOdQbGuGg==}
+    peerDependencies:
+      storybook: ^8.3.4
+
+  '@storybook/types@8.3.4':
+    resolution: {integrity: sha512-kIyb0g8C6EizI0Mv+l6L6yjCJe9/vW3UvgsZL5BXqs8THTAfs3/+A9Q9jDEMovSIVI3EgesO79+OCEazDUHmOA==}
+    peerDependencies:
+      storybook: ^8.3.4
+
+  '@storybook/vue3-vite@8.3.4':
+    resolution: {integrity: sha512-0H1tLbRd8i6L3EW8QC9bDlgPIUM5i6b7onvyyQhyIxODWRfigHi6UP9sjHfrljdvnlOtYlZT2A5QbpkugzwLjg==}
     engines: {node: '>=18.0.0'}
     peerDependencies:
+      storybook: ^8.3.4
       vite: ^4.0.0 || ^5.0.0
 
-  '@storybook/vue3@8.0.9':
-    resolution: {integrity: sha512-EqVdS62YbOCAE0wJrQKW0sHpM90be8N8Mvmj+HzB0QYhJNtFqP9ehwbcTfwEKtaVGudisHgGBOzNoSKDlxFaag==}
+  '@storybook/vue3@8.3.4':
+    resolution: {integrity: sha512-NNQXwidr+QjLndORWtPjXv/obsNNfJhP5Xj6vUZslrDpdIyTL3NEM+ktLK2EMw/a3zUbJMnMkyMgoWvioCNHxQ==}
     engines: {node: '>=18.0.0'}
     peerDependencies:
+      storybook: ^8.3.4
       vue: ^3.0.0
 
   '@swc/cli@0.3.12':
@@ -4128,8 +4237,14 @@ packages:
     cpu: [arm64]
     os: [darwin]
 
-  '@swc/core-darwin-arm64@1.4.17':
-    resolution: {integrity: sha512-HVl+W4LezoqHBAYg2JCqR+s9ife9yPfgWSj37iIawLWzOmuuJ7jVdIB7Ee2B75bEisSEKyxRlTl6Y1Oq3owBgw==}
+  '@swc/core-darwin-arm64@1.6.13':
+    resolution: {integrity: sha512-SOF4buAis72K22BGJ3N8y88mLNfxLNprTuJUpzikyMGrvkuBFNcxYtMhmomO0XHsgLDzOJ+hWzcgjRNzjMsUcQ==}
+    engines: {node: '>=10'}
+    cpu: [arm64]
+    os: [darwin]
+
+  '@swc/core-darwin-arm64@1.6.6':
+    resolution: {integrity: sha512-5DA8NUGECcbcK1YLKJwNDKqdtTYDVnkfDU1WvQSXq/rU+bjYCLtn5gCe8/yzL7ISXA6rwqPU1RDejhbNt4ARLQ==}
     engines: {node: '>=10'}
     cpu: [arm64]
     os: [darwin]
@@ -4140,8 +4255,14 @@ packages:
     cpu: [x64]
     os: [darwin]
 
-  '@swc/core-darwin-x64@1.4.17':
-    resolution: {integrity: sha512-WYRO9Fdzq4S/he8zjW5I95G1zcvyd9yyD3Tgi4/ic84P5XDlSMpBDpBLbr/dCPjmSg7aUXxNQqKqGkl6dQxYlA==}
+  '@swc/core-darwin-x64@1.6.13':
+    resolution: {integrity: sha512-AW8akFSC+tmPE6YQQvK9S2A1B8pjnXEINg+gGgw0KRUUXunvu1/OEOeC5L2Co1wAwhD7bhnaefi06Qi9AiwOag==}
+    engines: {node: '>=10'}
+    cpu: [x64]
+    os: [darwin]
+
+  '@swc/core-darwin-x64@1.6.6':
+    resolution: {integrity: sha512-2nbh/RHpweNRsJiYDFk1KcX7UtaKgzzTNUjwtvK5cp0wWrpbXmPvdlWOx3yzwoiSASDFx78242JHHXCIOlEdsw==}
     engines: {node: '>=10'}
     cpu: [x64]
     os: [darwin]
@@ -4158,8 +4279,14 @@ packages:
     cpu: [arm]
     os: [linux]
 
-  '@swc/core-linux-arm-gnueabihf@1.4.17':
-    resolution: {integrity: sha512-cgbvpWOvtMH0XFjvwppUCR+Y+nf6QPaGu6AQ5hqCP+5Lv2zO5PG0RfasC4zBIjF53xgwEaaWmGP5/361P30X8Q==}
+  '@swc/core-linux-arm-gnueabihf@1.6.13':
+    resolution: {integrity: sha512-f4gxxvDXVUm2HLYXRd311mSrmbpQF2MZ4Ja6XCQz1hWAxXdhRl1gpnZ+LH/xIfGSwQChrtLLVrkxdYUCVuIjFg==}
+    engines: {node: '>=10'}
+    cpu: [arm]
+    os: [linux]
+
+  '@swc/core-linux-arm-gnueabihf@1.6.6':
+    resolution: {integrity: sha512-YgytuyUfR7b0z0SRHKV+ylr83HmgnROgeT7xryEkth6JGpAEHooCspQ4RrWTU8+WKJ7aXiZlGXPgybQ4TiS+TA==}
     engines: {node: '>=10'}
     cpu: [arm]
     os: [linux]
@@ -4170,8 +4297,14 @@ packages:
     cpu: [arm64]
     os: [linux]
 
-  '@swc/core-linux-arm64-gnu@1.4.17':
-    resolution: {integrity: sha512-l7zHgaIY24cF9dyQ/FOWbmZDsEj2a9gRFbmgx2u19e3FzOPuOnaopFj0fRYXXKCmtdx+anD750iBIYnTR+pq/Q==}
+  '@swc/core-linux-arm64-gnu@1.6.13':
+    resolution: {integrity: sha512-Nf/eoW2CbG8s+9JoLtjl9FByBXyQ5cjdBsA4efO7Zw4p+YSuXDgc8HRPC+E2+ns0praDpKNZtLvDtmF2lL+2Gg==}
+    engines: {node: '>=10'}
+    cpu: [arm64]
+    os: [linux]
+
+  '@swc/core-linux-arm64-gnu@1.6.6':
+    resolution: {integrity: sha512-yGwx9fddzEE0iURqRVwKBQ4IwRHE6hNhl15WliHpi/PcYhzmYkUIpcbRXjr0dssubXAVPVnx6+jZVDSbutvnfg==}
     engines: {node: '>=10'}
     cpu: [arm64]
     os: [linux]
@@ -4182,8 +4315,14 @@ packages:
     cpu: [arm64]
     os: [linux]
 
-  '@swc/core-linux-arm64-musl@1.4.17':
-    resolution: {integrity: sha512-qhH4gr9gAlVk8MBtzXbzTP3BJyqbAfUOATGkyUtohh85fPXQYuzVlbExix3FZXTwFHNidGHY8C+ocscI7uDaYw==}
+  '@swc/core-linux-arm64-musl@1.6.13':
+    resolution: {integrity: sha512-2OysYSYtdw79prJYuKIiux/Gj0iaGEbpS2QZWCIY4X9sGoETJ5iMg+lY+YCrIxdkkNYd7OhIbXdYFyGs/w5LDg==}
+    engines: {node: '>=10'}
+    cpu: [arm64]
+    os: [linux]
+
+  '@swc/core-linux-arm64-musl@1.6.6':
+    resolution: {integrity: sha512-a6fMbqzSAsS5KCxFJyg1mD5kwN3ZFO8qQLyJ75R/htZP/eCt05jrhmOI7h2n+1HjiG332jLnZ9S8lkVE5O8Nqw==}
     engines: {node: '>=10'}
     cpu: [arm64]
     os: [linux]
@@ -4194,8 +4333,14 @@ packages:
     cpu: [x64]
     os: [linux]
 
-  '@swc/core-linux-x64-gnu@1.4.17':
-    resolution: {integrity: sha512-vRDFATL1oN5oZMImkwbgSHEkp8xG1ofEASBypze01W1Tqto8t+yo6gsp69wzCZBlxldsvPpvFZW55Jq0Rn+UnA==}
+  '@swc/core-linux-x64-gnu@1.6.13':
+    resolution: {integrity: sha512-PkR4CZYJNk5hcd2+tMWBpnisnmYsUzazI1O5X7VkIGFcGePTqJ/bWlfUIVVExWxvAI33PQFzLbzmN5scyIUyGQ==}
+    engines: {node: '>=10'}
+    cpu: [x64]
+    os: [linux]
+
+  '@swc/core-linux-x64-gnu@1.6.6':
+    resolution: {integrity: sha512-hRGsUKNzzZle28YF0dYIpN0bt9PceR9LaVBq7x8+l9TAaDLFbgksSxcnU/ubTtsy+WsYSYGn+A83w3xWC0O8CQ==}
     engines: {node: '>=10'}
     cpu: [x64]
     os: [linux]
@@ -4206,8 +4351,14 @@ packages:
     cpu: [x64]
     os: [linux]
 
-  '@swc/core-linux-x64-musl@1.4.17':
-    resolution: {integrity: sha512-zQNPXAXn3nmPqv54JVEN8k2JMEcMTQ6veVuU0p5O+A7KscJq+AGle/7ZQXzpXSfUCXlLMX4wvd+rwfGhh3J4cw==}
+  '@swc/core-linux-x64-musl@1.6.13':
+    resolution: {integrity: sha512-OdsY7wryTxCKwGQcwW9jwWg3cxaHBkTTHi91+5nm7hFPpmZMz1HivJrWAMwVE7iXFw+M4l6ugB/wCvpYrUAAjA==}
+    engines: {node: '>=10'}
+    cpu: [x64]
+    os: [linux]
+
+  '@swc/core-linux-x64-musl@1.6.6':
+    resolution: {integrity: sha512-NokIUtFxJDVv3LzGeEtYMTV3j2dnGKLac59luTeq36DQLZdJQawQIdTbzzWl2jE7lxxTZme+dhsVOH9LxE3ceg==}
     engines: {node: '>=10'}
     cpu: [x64]
     os: [linux]
@@ -4218,8 +4369,14 @@ packages:
     cpu: [arm64]
     os: [win32]
 
-  '@swc/core-win32-arm64-msvc@1.4.17':
-    resolution: {integrity: sha512-z86n7EhOwyzxwm+DLE5NoLkxCTme2lq7QZlDjbQyfCxOt6isWz8rkW5QowTX8w9Rdmk34ncrjSLvnHOeLY17+w==}
+  '@swc/core-win32-arm64-msvc@1.6.13':
+    resolution: {integrity: sha512-ap6uNmYjwk9M/+bFEuWRNl3hq4VqgQ/Lk+ID/F5WGqczNr0L7vEf+pOsRAn0F6EV+o/nyb3ePt8rLhE/wjHpPg==}
+    engines: {node: '>=10'}
+    cpu: [arm64]
+    os: [win32]
+
+  '@swc/core-win32-arm64-msvc@1.6.6':
+    resolution: {integrity: sha512-lzYdI4qb4k1dFG26yv+9Jaq/bUMAhgs/2JsrLncGjLof86+uj74wKYCQnbzKAsq2hDtS5DqnHnl+//J+miZfGA==}
     engines: {node: '>=10'}
     cpu: [arm64]
     os: [win32]
@@ -4230,8 +4387,14 @@ packages:
     cpu: [ia32]
     os: [win32]
 
-  '@swc/core-win32-ia32-msvc@1.4.17':
-    resolution: {integrity: sha512-JBwuSTJIgiJJX6wtr4wmXbfvOswHFj223AumUrK544QV69k60FJ9q2adPW9Csk+a8wm1hLxq4HKa2K334UHJ/g==}
+  '@swc/core-win32-ia32-msvc@1.6.13':
+    resolution: {integrity: sha512-IJ8KH4yIUHTnS/U1jwQmtbfQals7zWPG0a9hbEfIr4zI0yKzjd83lmtS09lm2Q24QBWOCFGEEbuZxR4tIlvfzA==}
+    engines: {node: '>=10'}
+    cpu: [ia32]
+    os: [win32]
+
+  '@swc/core-win32-ia32-msvc@1.6.6':
+    resolution: {integrity: sha512-bvl7FMaXIJQ76WZU0ER4+RyfKIMGb6S2MgRkBhJOOp0i7VFx4WLOnrmMzaeoPJaJSkityVKAftfNh7NBzTIydQ==}
     engines: {node: '>=10'}
     cpu: [ia32]
     os: [win32]
@@ -4242,17 +4405,32 @@ packages:
     cpu: [x64]
     os: [win32]
 
-  '@swc/core-win32-x64-msvc@1.4.17':
-    resolution: {integrity: sha512-jFkOnGQamtVDBm3MF5Kq1lgW8vx4Rm1UvJWRUfg+0gx7Uc3Jp3QMFeMNw/rDNQYRDYPG3yunCC+2463ycd5+dg==}
+  '@swc/core-win32-x64-msvc@1.6.13':
+    resolution: {integrity: sha512-f6/sx6LMuEnbuxtiSL/EkR0Y6qUHFw1XVrh6rwzKXptTipUdOY+nXpKoh+1UsBm/r7H0/5DtOdrn3q5ZHbFZjQ==}
     engines: {node: '>=10'}
     cpu: [x64]
     os: [win32]
 
-  '@swc/core@1.4.17':
-    resolution: {integrity: sha512-tq+mdWvodMBNBBZbwFIMTVGYHe9N7zvEaycVVjfvAx20k1XozHbHhRv+9pEVFJjwRxLdXmtvFZd3QZHRAOpoNQ==}
+  '@swc/core-win32-x64-msvc@1.6.6':
+    resolution: {integrity: sha512-WAP0JoCTfgeYKgOeYJoJV4ZS0sQUmU3OwvXa2dYYtMLF7zsNqOiW4niU7QlThBHgUv/qNZm2p6ITEgh3w1cltw==}
+    engines: {node: '>=10'}
+    cpu: [x64]
+    os: [win32]
+
+  '@swc/core@1.6.13':
+    resolution: {integrity: sha512-eailUYex6fkfaQTev4Oa3mwn0/e3mQU4H8y1WPuImYQESOQDtVrowwUGDSc19evpBbHpKtwM+hw8nLlhIsF+Tw==}
     engines: {node: '>=10'}
     peerDependencies:
-      '@swc/helpers': ^0.5.0
+      '@swc/helpers': '*'
+    peerDependenciesMeta:
+      '@swc/helpers':
+        optional: true
+
+  '@swc/core@1.6.6':
+    resolution: {integrity: sha512-sHfmIUPUXNrQTwFMVCY5V5Ena2GTOeaWjS2GFUpjLhAgVfP90OP67DWow7+cYrfFtqBdILHuWnjkTcd0+uPKlg==}
+    engines: {node: '>=10'}
+    peerDependencies:
+      '@swc/helpers': '*'
     peerDependenciesMeta:
       '@swc/helpers':
         optional: true
@@ -4266,14 +4444,14 @@ packages:
     peerDependencies:
       '@swc/core': '*'
 
-  '@swc/types@0.1.5':
-    resolution: {integrity: sha512-myfUej5naTBWnqOCc/MdVOLVjXUXtIA+NpDrDBKJtLLg2shUjBu3cZmB/85RyitKc55+lUUyl7oRfLOvkr2hsw==}
+  '@swc/types@0.1.9':
+    resolution: {integrity: sha512-qKnCno++jzcJ4lM4NTfYifm1EFSCeIfKiAHAfkENZAV5Kl9PjJIyd2yeeVv6c/2CckuLyv2NmRC5pv6pm2WQBg==}
 
   '@swc/wasm@1.2.130':
     resolution: {integrity: sha512-rNcJsBxS70+pv8YUWwf5fRlWX6JoY/HJc25HD/F8m6Kv7XhJdqPPMhyX6TKkUBPAG7TWlZYoxa+rHAjPy4Cj3Q==}
 
-  '@syuilo/aiscript@0.18.0':
-    resolution: {integrity: sha512-/iY9Vv4LLjtW/KUzId1QwXC4BlpIEPCMcoT7dyRhYdyxtwhS3Hx4b/4j1HYP+n3Pq9XKyW5zvkY72/+DNu4g6Q==}
+  '@syuilo/aiscript@0.19.0':
+    resolution: {integrity: sha512-ZWG4s1m6RrFjE7NeIMaxFz769YO1jW5ReTrOROrEO4IHheOrjxxJ/Ffe2TUNqX9/XxDloMwfWplKhfSzx8LGMA==}
 
   '@szmarczak/http-timer@4.0.6':
     resolution: {integrity: sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==}
@@ -4329,34 +4507,17 @@ packages:
     resolution: {integrity: sha512-EmCsnzdvawyk4b+4JKaLLuicHcJQRZtL1zSy9AWJLiiHTbDDseYgLxfaCEfLk8v2bUe7SBXwl3n3B7OjgvH11Q==}
     hasBin: true
 
-  '@testing-library/dom@9.3.3':
-    resolution: {integrity: sha512-fB0R+fa3AUqbLHWyxXa2kGVtf1Fe1ZZFr0Zp6AIbIAzXb2mKbEXl+PCQNUOaq5lbTab5tfctfXRNsWXxa2f7Aw==}
-    engines: {node: '>=14'}
+  '@testing-library/dom@10.4.0':
+    resolution: {integrity: sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==}
+    engines: {node: '>=18'}
 
   '@testing-library/dom@9.3.4':
     resolution: {integrity: sha512-FlS4ZWlp97iiNWig0Muq8p+3rVDjRiYE+YKGbAqXOu9nwJFFOdL00kFpz42M+4huzYi86vAK1sOOfyOG45muIQ==}
     engines: {node: '>=14'}
 
-  '@testing-library/jest-dom@6.4.2':
-    resolution: {integrity: sha512-CzqH0AFymEMG48CpzXFriYYkOjk6ZGPCLMhW9e9jg3KMCn5OfJecF8GtGW7yGfR/IgCe3SX8BSwjdzI6BBbZLw==}
+  '@testing-library/jest-dom@6.5.0':
+    resolution: {integrity: sha512-xGGHpBXYSHUUr6XsKBfs85TWlYKpTc37cSBBVrXcib2MkHLboWlkClhWF37JKlDb9KEq3dHs+f2xR7XJEWGBxA==}
     engines: {node: '>=14', npm: '>=6', yarn: '>=1'}
-    peerDependencies:
-      '@jest/globals': '>= 28'
-      '@types/bun': latest
-      '@types/jest': '>= 28'
-      jest: '>= 28'
-      vitest: '>= 0.32'
-    peerDependenciesMeta:
-      '@jest/globals':
-        optional: true
-      '@types/bun':
-        optional: true
-      '@types/jest':
-        optional: true
-      jest:
-        optional: true
-      vitest:
-        optional: true
 
   '@testing-library/user-event@14.5.2':
     resolution: {integrity: sha512-YAh82Wh4TIrxYLmfGcixwD18oIjyC1pFQC2Y01F2lzV2HTMiYrI0nze0FD0ocB//CKS/7jIUgae+adPqxK5yCQ==}
@@ -4364,8 +4525,8 @@ packages:
     peerDependencies:
       '@testing-library/dom': '>=7.21.4'
 
-  '@testing-library/vue@8.0.3':
-    resolution: {integrity: sha512-wSsbNlZ69ZFQgVlHMtc/ZC/g9BHO7MhyDrd4nHyfEubtMr3kToN/w4/BsSBknGIF8w9UmPbsgbIuq/CbdBHzCA==}
+  '@testing-library/vue@8.1.0':
+    resolution: {integrity: sha512-ls4RiHO1ta4mxqqajWRh8158uFObVrrtAPoxk7cIp4HrnQUj/ScKzqz53HxYpG3X6Zb7H2v+0eTGLSoy8HQ2nA==}
     engines: {node: '>=14'}
     peerDependencies:
       '@vue/compiler-sfc': '>= 3'
@@ -4381,13 +4542,16 @@ packages:
     resolution: {integrity: sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==}
     engines: {node: '>=10.13.0'}
 
-  '@tsd/typescript@5.3.3':
-    resolution: {integrity: sha512-CQlfzol0ldaU+ftWuG52vH29uRoKboLinLy84wS8TQOu+m+tWoaUfk4svL4ij2V8M5284KymJBlHUusKj6k34w==}
+  '@tsd/typescript@5.4.5':
+    resolution: {integrity: sha512-saiCxzHRhUrRxQV2JhH580aQUZiKQUXI38FcAcikcfOomAil4G4lxT0RfrrKywoAYP/rqAdYXYmNRLppcd+hQQ==}
     engines: {node: '>=14.17'}
 
   '@twemoji/parser@15.0.0':
     resolution: {integrity: sha512-lh9515BNsvKSNvyUqbj5yFu83iIDQ77SwVcsN/SnEGawczhsKU6qWuogewN1GweTi5Imo5ToQ9s+nNTf97IXvg==}
 
+  '@twemoji/parser@15.1.0':
+    resolution: {integrity: sha512-3HTiSxPvkWUJ4kZeCvwyKlIwkpTUfBOk6igpBBRQni58ceQMv5YK4smkc8vX/eqOlMMNER/9qobv+Q6Q8LVrqA==}
+
   '@twemoji/parser@15.1.1':
     resolution: {integrity: sha512-CChRzIu6ngkCJOmURBlYEdX5DZSu+bBTtqR60XjBkFrmvplKW7OQsea+i8XwF4bLVlUXBO7ZmHhRPDzfQyLwwg==}
 
@@ -4427,14 +4591,11 @@ packages:
   '@types/cacheable-request@6.0.3':
     resolution: {integrity: sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==}
 
-  '@types/chai-subset@1.3.5':
-    resolution: {integrity: sha512-c2mPnw+xHtXDoHmdtcCXGwyLMiauiAyxWMzhGpqHC4nqI/Y5G2XhTampslK2rb59kpcuHon03UH8W6iYUzw88A==}
+  '@types/canvas-confetti@1.6.4':
+    resolution: {integrity: sha512-fNyZ/Fdw/Y92X0vv7B+BD6ysHL4xVU5dJcgzgxLdGbn8O3PezZNIJpml44lKM0nsGur+o/6+NZbZeNTt00U1uA==}
 
-  '@types/chai@4.3.11':
-    resolution: {integrity: sha512-qQR1dr2rGIHYlJulmr8Ioq3De0Le9E4MJ5AiaeAETJJpndT1uUNHsGFK3L/UIu+rbkQSdj8J/w2bCsBZc/Y5fQ==}
-
-  '@types/color-convert@2.0.3':
-    resolution: {integrity: sha512-2Q6wzrNiuEvYxVQqhh7sXM2mhIhvZR/Paq4FdsQkOMgWsCIkKvSGj8Le1/XalulrmgOzPMqNa0ix+ePY4hTrfg==}
+  '@types/color-convert@2.0.4':
+    resolution: {integrity: sha512-Ub1MmDdyZ7mX//g25uBAoH/mWGd9swVbt8BseymnaE18SU4po/PjmCrHxqIIRjBo3hV/vh1KGr0eMxUhp+t+dQ==}
 
   '@types/color-name@1.1.1':
     resolution: {integrity: sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==}
@@ -4451,36 +4612,15 @@ packages:
   '@types/cookie@0.6.0':
     resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==}
 
-  '@types/cookies@0.9.0':
-    resolution: {integrity: sha512-40Zk8qR147RABiQ7NQnBzWzDcjKzNrntB5BAmeGCb2p/MIyOE+4BVvc17wumsUqUw00bJYqoXFHYygQnEFh4/Q==}
-
-  '@types/cross-spawn@6.0.2':
-    resolution: {integrity: sha512-KuwNhp3eza+Rhu8IFI5HUXRP0LIhqH5cAjubUvGXXthh4YYBuP2ntwEX+Cz8GJoZUHlKo247wPWOfA9LYEq4cw==}
-
   '@types/debug@4.1.12':
     resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==}
 
-  '@types/detect-port@1.3.2':
-    resolution: {integrity: sha512-xxgAGA2SAU4111QefXPSp5eGbDm/hW6zhvYl9IeEPZEry9F4d66QAHm5qpUXjb6IsevZV/7emAEx5MhP6O192g==}
-
   '@types/disposable-email-domains@1.0.2':
     resolution: {integrity: sha512-SDKwyYTjk3y5aZBxxc38yRecpJPjsqn57STz1bNxYYlv4k11bBe7QB8w4llXDTmQXKT1mFvgGmJv+8Zdu3YmJw==}
 
-  '@types/doctrine@0.0.3':
-    resolution: {integrity: sha512-w5jZ0ee+HaPOaX25X2/2oGR/7rgAQSYII7X7pp0m9KgBfMP7uKfMfTvcpl5Dj+eDBbpxKGiqE+flqDr6XTd2RA==}
-
   '@types/doctrine@0.0.9':
     resolution: {integrity: sha512-eOIHzCUSH7SMfonMG1LsC2f8vxBFtho6NGBznK41R84YzPuvSBzrhEps33IsQiOW9+VL6NQ9DbjQJznk/S4uRA==}
 
-  '@types/ejs@3.1.2':
-    resolution: {integrity: sha512-ZmiaE3wglXVWBM9fyVC17aGPkLo/UgaOjEiI2FXQfyczrCefORPxIe+2dVmnmk3zkVIbizjrlQzmPGhSYGXG5g==}
-
-  '@types/emscripten@1.39.7':
-    resolution: {integrity: sha512-tLqYV94vuqDrXh515F/FOGtBcRMTPGvVV1LzLbtYDcQmmhtpf/gLYf+hikBbQk8MzOHNz37wpFfJbYAuSn8HqA==}
-
-  '@types/escape-regexp@0.0.3':
-    resolution: {integrity: sha512-FQMYUxaf1dVeWLUzJFSvfdDugfOpDyM13p67QfyMdagxSkBa689opkr/q9uR/VWyrWrl0jAyQaSPKxX9MpAXFw==}
-
   '@types/escodegen@0.0.6':
     resolution: {integrity: sha512-AjwI4MvWx3HAOaZqYsjKWyEObT9lcVV0Y0V8nXo6cXzN8ZiMxVhf6F3d/UNvXVGKrEzL/Dluc5p+y9GkzlTWig==}
 
@@ -4490,8 +4630,8 @@ packages:
   '@types/estree@0.0.51':
     resolution: {integrity: sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ==}
 
-  '@types/estree@1.0.5':
-    resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==}
+  '@types/estree@1.0.6':
+    resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==}
 
   '@types/express-serve-static-core@4.17.33':
     resolution: {integrity: sha512-TPBqmR/HRYI3eC2E5hmiivIzv+bidAfXofM+sbonAGvyDhySGw9/PQZFt2BLOrjUUR++4eJVpx6KnLQK1Fk9tA==}
@@ -4499,11 +4639,14 @@ packages:
   '@types/express@4.17.17':
     resolution: {integrity: sha512-Q4FmmuLGBG58btUnfS1c1r/NQdlp3DMfGDGig8WhfpA2YRUtEkxAjkZb0yvplJGYdF1fsQ81iMDcH24sSCNC/Q==}
 
+  '@types/express@4.17.21':
+    resolution: {integrity: sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==}
+
   '@types/find-cache-dir@3.2.1':
     resolution: {integrity: sha512-frsJrz2t/CeGifcu/6uRo4b+SzAwT4NYCVPu1GN8IB9XTzrpPkGuV0tmh9mN+/L0PklAlsC3u5Fxt0ju00LXIw==}
 
-  '@types/fluent-ffmpeg@2.1.24':
-    resolution: {integrity: sha512-g5oQO8Jgi2kFS3tTub7wLvfLztr1s8tdXmRd8PiL/hLMLzTIAyMR2sANkTggM/rdEDAg3d63nYRRVepwBiCw5A==}
+  '@types/fluent-ffmpeg@2.1.26':
+    resolution: {integrity: sha512-0JVF3wdQG+pN0ImwWD0bNgJiKF2OHg/7CDBHw5UIbRTvlnkgGHK6V5doE54ltvhud4o31/dEiHm23CAlxFiUQg==}
 
   '@types/glob@7.2.0':
     resolution: {integrity: sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==}
@@ -4517,17 +4660,11 @@ packages:
   '@types/htmlescape@1.1.3':
     resolution: {integrity: sha512-tuC81YJXGUe0q8WRtBNW+uyx79rkkzWK651ALIXXYq5/u/IxjX4iHneGF2uUqzsNp+F+9J2mFZOv9jiLTtIq0w==}
 
-  '@types/http-assert@1.5.5':
-    resolution: {integrity: sha512-4+tE/lwdAahgZT1g30Jkdm9PzFRde0xwxBNUyRsCitRvCQB90iuA2uJYdUnhnANRcqGXaWOGY4FEoxeElNAK2g==}
-
   '@types/http-cache-semantics@4.0.4':
     resolution: {integrity: sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==}
 
-  '@types/http-errors@2.0.4':
-    resolution: {integrity: sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==}
-
-  '@types/http-link-header@1.0.5':
-    resolution: {integrity: sha512-AxhIKR8UbyoqCTNp9rRepkktHuUOw3DjfOfDCaO9kwI8AYzjhxyrvZq4+mRw/2daD3hYDknrtSeV6SsPwmc71w==}
+  '@types/http-link-header@1.0.7':
+    resolution: {integrity: sha512-snm5oLckop0K3cTDAiBnZDy6ncx9DJ3mCRDvs42C884MbVYPP74Tiq2hFsSDRTyjK6RyDYDIulPiW23ge+g5Lw==}
 
   '@types/istanbul-lib-coverage@2.0.4':
     resolution: {integrity: sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g==}
@@ -4538,14 +4675,14 @@ packages:
   '@types/istanbul-reports@3.0.1':
     resolution: {integrity: sha512-c3mAZEuK0lvBp8tmuL74XRKn1+y2dcwOUpH7x4WrF6gk1GIgiluDRgMYQtw2OFcBvAJWlt6ASU3tSqxp0Uu0Aw==}
 
-  '@types/jest@29.5.12':
-    resolution: {integrity: sha512-eDC8bTvT/QhYdxJAulQikueigY5AsdBRH2yDKW3yveW7svY3+DzN84/2NUgkw10RTiJbWqZrTtoGVdYlvFJdLw==}
+  '@types/jest@29.5.13':
+    resolution: {integrity: sha512-wd+MVEZCHt23V0/L642O5APvspWply/rGY5BcW4SUETo2UzPU3Z26qr8jC2qxpimI2jjx9h7+2cj2FwIr01bXg==}
 
   '@types/js-yaml@4.0.9':
     resolution: {integrity: sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==}
 
-  '@types/jsdom@21.1.6':
-    resolution: {integrity: sha512-/7kkMsC+/kMs7gAYmmBR9P0vGTnOoLhQhyhQJSlXGI5bzTHp6xdo0TtKWQAsz6pmSAeVqKSbqeyP6hytqr9FDw==}
+  '@types/jsdom@21.1.7':
+    resolution: {integrity: sha512-yOriVnggzrnQ3a9OKOCxaVuSug3w3/SbOj5i7VwXWZEyUNl3bLF9V3MfxGbZKuwqJOQyRfqXyROBB1CoZLFWzA==}
 
   '@types/json-schema@7.0.12':
     resolution: {integrity: sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA==}
@@ -4556,27 +4693,15 @@ packages:
   '@types/json5@0.0.29':
     resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==}
 
-  '@types/jsonld@1.5.13':
-    resolution: {integrity: sha512-n7fUU6W4kSYK8VQlf/LsE9kddBHPKhODoVOjsZswmve+2qLwBy6naWxs/EiuSZN9NU0N06Ra01FR+j87C62T0A==}
+  '@types/jsonld@1.5.15':
+    resolution: {integrity: sha512-PlAFPZjL+AuGYmwlqwKEL0IMP8M8RexH0NIPGfCVWSQ041H2rR/8OlyZSD7KsCVoN8vCfWdtWDBxX8yBVP+xow==}
 
   '@types/jsrsasign@10.5.14':
     resolution: {integrity: sha512-lppSlfK6etu+cuKs40K4rg8As79PH6hzIB+v55zSqImbSH3SE6Fm8MBHCiI91cWlAP3Z4igtJK1VL3fSN09blQ==}
 
-  '@types/keygrip@1.0.6':
-    resolution: {integrity: sha512-lZuNAY9xeJt7Bx4t4dx0rYCDqGPW8RXhQZK1td7d4H6E9zYbLoOtjBvfwdTKpsyxQI/2jv+armjX/RW+ZNpXOQ==}
-
   '@types/keyv@3.1.4':
     resolution: {integrity: sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==}
 
-  '@types/koa-compose@3.2.8':
-    resolution: {integrity: sha512-4Olc63RY+MKvxMwVknCUDhRQX1pFQoBZ/lXcRLP69PQkEpze/0cr8LNqJQe5NFb/b19DWi2a5bTi2VAlQzhJuA==}
-
-  '@types/koa@2.14.0':
-    resolution: {integrity: sha512-DTDUyznHGNHAl+wd1n0z1jxNajduyTh8R53xoewuerdBzGo6Ogj6F2299BFtrexJw4NtgjsI5SMPCmV9gZwGXA==}
-
-  '@types/koa__router@12.0.3':
-    resolution: {integrity: sha512-5YUJVv6NwM1z7m6FuYpKfNLTZ932Z6EF6xy2BbtpJSyn13DKNQEkXVffFVSnJHxvwwWh2SAeumpjAYUELqgjyw==}
-
   '@types/lodash@4.14.191':
     resolution: {integrity: sha512-BdZ5BCCvho3EIXw6wUCXHe7rS53AIDPLE+JzwgT+OsJk53oBfbSmZZ7CX4VaRoN78N+TJpFi9QPlfIVNmJYWxQ==}
 
@@ -4586,14 +4711,17 @@ packages:
   '@types/matter-js@0.19.6':
     resolution: {integrity: sha512-ffk6tqJM5scla+ThXmnox+mdfCo3qYk6yMjQsNcrbo6eQ5DqorVdtnaL+1agCoYzxUjmHeiNB7poBMAmhuLY7w==}
 
+  '@types/matter-js@0.19.7':
+    resolution: {integrity: sha512-dlh50YEh1lQS4fiCDGBnK75ocHQIq/1E371Qk6hASJImICIivdZQC2GkOqnfBm0Hac2xLk5+yrqRFDAEfj/yLA==}
+
   '@types/mdast@4.0.3':
     resolution: {integrity: sha512-LsjtqsyF+d2/yFOYaN22dHZI1Cpwkrj+g06G8+qtUKlhovPW89YhqSnfKtMbkgmEtYpH2gydRNULd6y8mciAFg==}
 
   '@types/mdx@2.0.3':
     resolution: {integrity: sha512-IgHxcT3RC8LzFLhKwP3gbMPeaK7BM9eBH46OdapPA7yvuIUJ8H6zHZV53J8hGZcTSnt95jANt+rTBNUUc22ACQ==}
 
-  '@types/micromatch@4.0.7':
-    resolution: {integrity: sha512-C/FMQ8HJAZhTsDpl4wDKZdMeeW5USjgzOczUwTGbRc1ZopPgOhIEnxY2ZgUrsuyy4DwK1JVOJZKFakv3TbCKiA==}
+  '@types/micromatch@4.0.9':
+    resolution: {integrity: sha512-7V+8ncr22h4UoYRLnLXSpTxjQrNUXtWHGeMPRJt1nULXI57G9bIcpyrHlmrQ7QK24EyyuXvYcSSWAM8GA9nqCg==}
 
   '@types/mime-types@2.1.4':
     resolution: {integrity: sha512-lfU4b34HOri+kAY5UheuFMWPDOI+OPceBSHZKp69gEyTL/mmJ4cnU6Y/rlme3UL3GyOn6Y42hyIEw0/q8sWx5w==}
@@ -4616,27 +4744,23 @@ packages:
   '@types/mysql@2.15.22':
     resolution: {integrity: sha512-wK1pzsJVVAjYCSZWQoWHziQZbNggXFDUEIGf54g4ZM/ERuP86uGdWeKZWMYlqTPMZfHJJvLPyogXGvCOg87yLQ==}
 
-  '@types/node-fetch@2.6.4':
-    resolution: {integrity: sha512-1ZX9fcN4Rvkvgv4E6PAY5WXUFWFcRWxZa3EW83UjycOB9ljJCedb2CupIP4RZMEwF/M3eTcCihbBRgwtGbg5Rg==}
-
-  '@types/node-fetch@3.0.3':
-    resolution: {integrity: sha512-HhggYPH5N+AQe/OmN6fmhKmRRt2XuNJow+R3pQwJxOOF9GuwM7O2mheyGeIrs5MOIeNjDEdgdoyHBOrFeJBR3g==}
-    deprecated: This is a stub types definition. node-fetch provides its own type definitions, so you do not need this installed.
-
-  '@types/node@18.17.15':
-    resolution: {integrity: sha512-2yrWpBk32tvV/JAd3HNHWuZn/VDN1P+72hWirHnvsvTGSqbANi+kSeuQR9yAHnbvaBvHDsoTdXV0Fe+iRtHLKA==}
+  '@types/node-fetch@2.6.11':
+    resolution: {integrity: sha512-24xFj9R5+rfQJLRyM56qh+wnVSYhyXC2tkoBndtY0U+vubqNsYXGjufB2nn8Q6gt0LrARwL6UBtMCSVCwl4B1g==}
 
   '@types/node@20.11.5':
     resolution: {integrity: sha512-g557vgQjUUfN76MZAN/dt1z3dzcUsimuysco0KeluHgrPdJXkP/XdAURgyO2W9fZWHRtRBiVKzKn8vyOAwlG+w==}
 
-  '@types/node@20.12.7':
-    resolution: {integrity: sha512-wq0cICSkRLVaf3UGLMGItu/PtdY7oaXaI/RVU+xliKVOtRna3PRY57ZDfztpDL0n11vfymMUnXv8QwYCO7L1wg==}
+  '@types/node@20.14.12':
+    resolution: {integrity: sha512-r7wNXakLeSsGT0H1AU863vS2wa5wBOK4bWMjZz2wj+8nBx+m5PeIn0k8AloSLpRuiwdRQZwarZqHE4FNArPuJQ==}
 
   '@types/node@20.9.1':
     resolution: {integrity: sha512-HhmzZh5LSJNS5O8jQKpJ/3ZcrrlG6L70hpGqMIAoM9YVD0YBRNWYsfwcXq8VnSjlNpCpgLzMXdiPo+dxcvSmiA==}
 
-  '@types/nodemailer@6.4.15':
-    resolution: {integrity: sha512-0EBJxawVNjPkng1zm2vopRctuWVCxk34JcIlRuXSf54habUWdz1FB7wHDqOqvDa8Mtpt0Q3LTXQkAs2LNyK5jQ==}
+  '@types/node@22.5.5':
+    resolution: {integrity: sha512-Xjs4y5UPO/CLdzpgR6GirZJx36yScjh73+2NlLlkFRSoQN8B0DpfXPdZGnvVmLRLOsqDpOfTNv7D9trgGhmOIA==}
+
+  '@types/nodemailer@6.4.16':
+    resolution: {integrity: sha512-uz6hN6Pp0upXMcilM61CoKyjT7sskBoOWpptkjjJp8jIMlTdc3xG01U7proKkXzruMS4hS0zqtHNkNPFB20rKQ==}
 
   '@types/normalize-package-data@2.4.1':
     resolution: {integrity: sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==}
@@ -4647,8 +4771,8 @@ packages:
   '@types/oauth2orize@1.11.5':
     resolution: {integrity: sha512-C6hrRoh9hCnqis39OpeUZSwgw+TIzcV0CsxwJMGfQjTx4I1r+CLmuEPzoDJr5NRTfc7OMwHNLkQwrGFLKrJjMQ==}
 
-  '@types/oauth@0.9.4':
-    resolution: {integrity: sha512-qk9orhti499fq5XxKCCEbd0OzdPZuancneyse3KtR+vgMiHRbh+mn8M4G6t64ob/Fg+GZGpa565MF/2dKWY32A==}
+  '@types/oauth@0.9.5':
+    resolution: {integrity: sha512-+oQ3C2Zx6ambINOcdIARF5Z3Tu3x//HipE889/fqo3sgpQZbe9c6ExdQFtN6qlhpR7p83lTZfPJt0tCAW29dog==}
 
   '@types/offscreencanvas@2019.3.0':
     resolution: {integrity: sha512-esIJx9bQg+QYF0ra8GnvfianIY8qWB0GBx54PK5Eps6m+xTj86KLavHv6qDhzKcu5UUOgNfJ2pWaIIV7TRUd9Q==}
@@ -4659,15 +4783,12 @@ packages:
   '@types/pg-pool@2.0.4':
     resolution: {integrity: sha512-qZAvkv1K3QbmHHFYSNRYPkRjOWRLBYrL4B9c+wG0GSVGBw0NtJwPcgx/DSddeDJvRGMHCEQ4VMEVfuJ/0gZ3XQ==}
 
-  '@types/pg@8.11.5':
-    resolution: {integrity: sha512-2xMjVviMxneZHDHX5p5S6tsRRs7TpDHeeK7kTTMe/kAC/mRRNjWHjZg0rkiY+e17jXSZV3zJYDxXV8Cy72/Vuw==}
+  '@types/pg@8.11.10':
+    resolution: {integrity: sha512-LczQUW4dbOQzsH2RQ5qoeJ6qJPdrcM/DcMLoqWQkMLMsq83J5lAX3LXjdkWdpscFy67JSOWDnh7Ny/sPFykmkg==}
 
   '@types/pg@8.6.1':
     resolution: {integrity: sha512-1Kc4oAGzAl7uqUStZCDvaLFqZrW9qWSjXOmBfdgyBP5La7Us6Mg4GBvRlSoaZMhQF/zSj1C8CtKMBkoiT8eL8w==}
 
-  '@types/pretty-hrtime@1.0.1':
-    resolution: {integrity: sha512-VjID5MJb1eGKthz2qUerWT8+R4b9N+CHvGCzg9fn4kWZgaF9AhdYikQio3R7wV8YY1NsQKPaCwKz1Yff+aHNUQ==}
-
   '@types/prop-types@15.7.5':
     resolution: {integrity: sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==}
 
@@ -4707,14 +4828,14 @@ packages:
   '@types/responselike@1.0.0':
     resolution: {integrity: sha512-85Y2BjiufFzaMIlvJDvTTB8Fxl2xfLo4HgmHzVBz08w4wDePCTjYw66PdrolO0kzli3yam/YCgRufyo1DdQVTA==}
 
-  '@types/sanitize-html@2.11.0':
-    resolution: {integrity: sha512-7oxPGNQHXLHE48r/r/qjn7q0hlrs3kL7oZnGj0Wf/h9tj/6ibFyRkNbsDxaBBZ4XUZ0Dx5LGCyDJ04ytSofacQ==}
+  '@types/sanitize-html@2.13.0':
+    resolution: {integrity: sha512-X31WxbvW9TjIhZZNyNBZ/p5ax4ti7qsNDBDEnH4zAgmEh35YnFD1UiS6z9Cd34kKm0LslFW0KPmTQzu/oGtsqQ==}
 
   '@types/scheduler@0.16.2':
     resolution: {integrity: sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==}
 
-  '@types/seedrandom@2.4.30':
-    resolution: {integrity: sha512-AnxLHewubLVzoF/A4qdxBGHCKifw8cY32iro3DQX9TPcetE95zBeVt3jnsvtvAUf1vwzMfwzp4t/L2yqPlnjkQ==}
+  '@types/seedrandom@2.4.34':
+    resolution: {integrity: sha512-ytDiArvrn/3Xk6/vtylys5tlY6eo7Ane0hvcx++TKo6RxQXuVfW0AF/oeWqAj9dN29SyhtawuXstgmPlwNcv/A==}
 
   '@types/seedrandom@3.0.8':
     resolution: {integrity: sha512-TY1eezMU2zH2ozQoAFAQFOPpvP15g+ZgSfTZt31AUUH/Rxtnz3H+A/Sv1Snw2/amp//omibc+AEkTaA8KUeOLQ==}
@@ -4764,9 +4885,15 @@ packages:
   '@types/tough-cookie@4.0.2':
     resolution: {integrity: sha512-Q5vtl1W5ue16D+nIaW8JWebSSraJVlK+EthKn7e7UcD4KWsaSJ8BqGPXNaPghgtcn/fhvrN17Tv8ksUsQpiplw==}
 
+  '@types/tough-cookie@4.0.5':
+    resolution: {integrity: sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==}
+
   '@types/unist@3.0.2':
     resolution: {integrity: sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ==}
 
+  '@types/uuid@10.0.0':
+    resolution: {integrity: sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==}
+
   '@types/uuid@9.0.8':
     resolution: {integrity: sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==}
 
@@ -4782,8 +4909,8 @@ packages:
   '@types/wrap-ansi@3.0.0':
     resolution: {integrity: sha512-ltIpx+kM7g/MLRZfkbL7EsCEjfzCcScLpkg37eXEtx5kmrAKBkTJwd1GIAjDSL8wTpM6Hzn5YO4pSb91BEwu1g==}
 
-  '@types/ws@8.5.10':
-    resolution: {integrity: sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A==}
+  '@types/ws@8.5.12':
+    resolution: {integrity: sha512-3tPRkv1EtkDpzlgyKyI8pGsGZAGPEaXeu0DOj5DI25Ja91bdAYddYHbADRYVrZMRbfW+1l5YwXVDKohDJNQxkQ==}
 
   '@types/yargs-parser@21.0.0':
     resolution: {integrity: sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==}
@@ -4794,17 +4921,6 @@ packages:
   '@types/yauzl@2.10.0':
     resolution: {integrity: sha512-Cn6WYCm0tXv8p6k+A8PvbDG763EDpBoTzHdA+Q/MF6H3sapGjCm9NzoaJncJS9tUKSuCoDs9XHxYYsQDgxR6kw==}
 
-  '@typescript-eslint/eslint-plugin@6.11.0':
-    resolution: {integrity: sha512-uXnpZDc4VRjY4iuypDBKzW1rz9T5YBBK0snMn8MaTSNd2kMlj50LnLBABELjJiOL5YHk7ZD8hbSpI9ubzqYI0w==}
-    engines: {node: ^16.0.0 || >=18.0.0}
-    peerDependencies:
-      '@typescript-eslint/parser': ^6.0.0 || ^6.0.0-alpha
-      eslint: ^7.0.0 || ^8.0.0
-      typescript: '*'
-    peerDependenciesMeta:
-      typescript:
-        optional: true
-
   '@typescript-eslint/eslint-plugin@7.1.0':
     resolution: {integrity: sha512-j6vT/kCulhG5wBmGtstKeiVr1rdXE4nk+DT1k6trYkwlrvW9eOF5ZbgKnd/YR6PcM4uTEXa0h6Fcvf6X7Dxl0w==}
     engines: {node: ^16.0.0 || >=18.0.0}
@@ -4816,8 +4932,8 @@ packages:
       typescript:
         optional: true
 
-  '@typescript-eslint/eslint-plugin@7.7.1':
-    resolution: {integrity: sha512-KwfdWXJBOviaBVhxO3p5TJiLpNuh2iyXyjmWN0f1nU87pwyvfS0EmjC6ukQVYVFJd/K1+0NWGPDXiyEyQorn0Q==}
+  '@typescript-eslint/eslint-plugin@7.17.0':
+    resolution: {integrity: sha512-pyiDhEuLM3PuANxH7uNYan1AaFs5XE0zw1hq69JBvGvE7gSuEoQl1ydtEe/XQeoC3GQxLXyOVa5kNOATgM638A==}
     engines: {node: ^18.18.0 || >=20.0.0}
     peerDependencies:
       '@typescript-eslint/parser': ^7.0.0
@@ -4827,16 +4943,6 @@ packages:
       typescript:
         optional: true
 
-  '@typescript-eslint/parser@6.11.0':
-    resolution: {integrity: sha512-+whEdjk+d5do5nxfxx73oanLL9ghKO3EwM9kBCkUtWMRwWuPaFv9ScuqlYfQ6pAD6ZiJhky7TZ2ZYhrMsfMxVQ==}
-    engines: {node: ^16.0.0 || >=18.0.0}
-    peerDependencies:
-      eslint: ^7.0.0 || ^8.0.0
-      typescript: '*'
-    peerDependenciesMeta:
-      typescript:
-        optional: true
-
   '@typescript-eslint/parser@7.1.0':
     resolution: {integrity: sha512-V1EknKUubZ1gWFjiOZhDSNToOjs63/9O0puCgGS8aDOgpZY326fzFu15QAUjwaXzRZjf/qdsdBrckYdv9YxB8w==}
     engines: {node: ^16.0.0 || >=18.0.0}
@@ -4847,8 +4953,8 @@ packages:
       typescript:
         optional: true
 
-  '@typescript-eslint/parser@7.7.1':
-    resolution: {integrity: sha512-vmPzBOOtz48F6JAGVS/kZYk4EkXao6iGrD838sp1w3NQQC0W8ry/q641KU4PrG7AKNAf56NOcR8GOpH8l9FPCw==}
+  '@typescript-eslint/parser@7.17.0':
+    resolution: {integrity: sha512-puiYfGeg5Ydop8eusb/Hy1k7QmOU6X3nvsqCgzrB2K4qMavK//21+PzNE8qeECgNOIoertJPUC1SpegHDI515A==}
     engines: {node: ^18.18.0 || >=20.0.0}
     peerDependencies:
       eslint: ^8.56.0
@@ -4857,28 +4963,14 @@ packages:
       typescript:
         optional: true
 
-  '@typescript-eslint/scope-manager@6.11.0':
-    resolution: {integrity: sha512-0A8KoVvIURG4uhxAdjSaxy8RdRE//HztaZdG8KiHLP8WOXSk0vlF7Pvogv+vlJA5Rnjj/wDcFENvDaHb+gKd1A==}
-    engines: {node: ^16.0.0 || >=18.0.0}
-
   '@typescript-eslint/scope-manager@7.1.0':
     resolution: {integrity: sha512-6TmN4OJiohHfoOdGZ3huuLhpiUgOGTpgXNUPJgeZOZR3DnIpdSgtt83RS35OYNNXxM4TScVlpVKC9jyQSETR1A==}
     engines: {node: ^16.0.0 || >=18.0.0}
 
-  '@typescript-eslint/scope-manager@7.7.1':
-    resolution: {integrity: sha512-PytBif2SF+9SpEUKynYn5g1RHFddJUcyynGpztX3l/ik7KmZEv19WCMhUBkHXPU9es/VWGD3/zg3wg90+Dh2rA==}
+  '@typescript-eslint/scope-manager@7.17.0':
+    resolution: {integrity: sha512-0P2jTTqyxWp9HiKLu/Vemr2Rg1Xb5B7uHItdVZ6iAenXmPo4SZ86yOPCJwMqpCyaMiEHTNqizHfsbmCFT1x9SA==}
     engines: {node: ^18.18.0 || >=20.0.0}
 
-  '@typescript-eslint/type-utils@6.11.0':
-    resolution: {integrity: sha512-nA4IOXwZtqBjIoYrJcYxLRO+F9ri+leVGoJcMW1uqr4r1Hq7vW5cyWrA43lFbpRvQ9XgNrnfLpIkO3i1emDBIA==}
-    engines: {node: ^16.0.0 || >=18.0.0}
-    peerDependencies:
-      eslint: ^7.0.0 || ^8.0.0
-      typescript: '*'
-    peerDependenciesMeta:
-      typescript:
-        optional: true
-
   '@typescript-eslint/type-utils@7.1.0':
     resolution: {integrity: sha512-UZIhv8G+5b5skkcuhgvxYWHjk7FW7/JP5lPASMEUoliAPwIH/rxoUSQPia2cuOj9AmDZmwUl1usKm85t5VUMew==}
     engines: {node: ^16.0.0 || >=18.0.0}
@@ -4889,8 +4981,8 @@ packages:
       typescript:
         optional: true
 
-  '@typescript-eslint/type-utils@7.7.1':
-    resolution: {integrity: sha512-ZksJLW3WF7o75zaBPScdW1Gbkwhd/lyeXGf1kQCxJaOeITscoSl0MjynVvCzuV5boUz/3fOI06Lz8La55mu29Q==}
+  '@typescript-eslint/type-utils@7.17.0':
+    resolution: {integrity: sha512-XD3aaBt+orgkM/7Cei0XNEm1vwUxQ958AOLALzPlbPqb8C1G8PZK85tND7Jpe69Wualri81PLU+Zc48GVKIMMA==}
     engines: {node: ^18.18.0 || >=20.0.0}
     peerDependencies:
       eslint: ^8.56.0
@@ -4899,27 +4991,14 @@ packages:
       typescript:
         optional: true
 
-  '@typescript-eslint/types@6.11.0':
-    resolution: {integrity: sha512-ZbEzuD4DwEJxwPqhv3QULlRj8KYTAnNsXxmfuUXFCxZmO6CF2gM/y+ugBSAQhrqaJL3M+oe4owdWunaHM6beqA==}
-    engines: {node: ^16.0.0 || >=18.0.0}
-
   '@typescript-eslint/types@7.1.0':
     resolution: {integrity: sha512-qTWjWieJ1tRJkxgZYXx6WUYtWlBc48YRxgY2JN1aGeVpkhmnopq+SUC8UEVGNXIvWH7XyuTjwALfG6bFEgCkQA==}
     engines: {node: ^16.0.0 || >=18.0.0}
 
-  '@typescript-eslint/types@7.7.1':
-    resolution: {integrity: sha512-AmPmnGW1ZLTpWa+/2omPrPfR7BcbUU4oha5VIbSbS1a1Tv966bklvLNXxp3mrbc+P2j4MNOTfDffNsk4o0c6/w==}
+  '@typescript-eslint/types@7.17.0':
+    resolution: {integrity: sha512-a29Ir0EbyKTKHnZWbNsrc/gqfIBqYPwj3F2M+jWE/9bqfEHg0AMtXzkbUkOG6QgEScxh2+Pz9OXe11jHDnHR7A==}
     engines: {node: ^18.18.0 || >=20.0.0}
 
-  '@typescript-eslint/typescript-estree@6.11.0':
-    resolution: {integrity: sha512-Aezzv1o2tWJwvZhedzvD5Yv7+Lpu1by/U1LZ5gLc4tCx8jUmuSCMioPFRjliN/6SJIvY6HpTtJIWubKuYYYesQ==}
-    engines: {node: ^16.0.0 || >=18.0.0}
-    peerDependencies:
-      typescript: '*'
-    peerDependenciesMeta:
-      typescript:
-        optional: true
-
   '@typescript-eslint/typescript-estree@7.1.0':
     resolution: {integrity: sha512-k7MyrbD6E463CBbSpcOnwa8oXRdHzH1WiVzOipK3L5KSML92ZKgUBrTlehdi7PEIMT8k0bQixHUGXggPAlKnOQ==}
     engines: {node: ^16.0.0 || >=18.0.0}
@@ -4929,8 +5008,8 @@ packages:
       typescript:
         optional: true
 
-  '@typescript-eslint/typescript-estree@7.7.1':
-    resolution: {integrity: sha512-CXe0JHCXru8Fa36dteXqmH2YxngKJjkQLjxzoj6LYwzZ7qZvgsLSc+eqItCrqIop8Vl2UKoAi0StVWu97FQZIQ==}
+  '@typescript-eslint/typescript-estree@7.17.0':
+    resolution: {integrity: sha512-72I3TGq93t2GoSBWI093wmKo0n6/b7O4j9o8U+f65TVD0FS6bI2180X5eGEr8MA8PhKMvYe9myZJquUT2JkCZw==}
     engines: {node: ^18.18.0 || >=20.0.0}
     peerDependencies:
       typescript: '*'
@@ -4938,116 +5017,124 @@ packages:
       typescript:
         optional: true
 
-  '@typescript-eslint/utils@6.11.0':
-    resolution: {integrity: sha512-p23ibf68fxoZy605dc0dQAEoUsoiNoP3MD9WQGiHLDuTSOuqoTsa4oAy+h3KDkTcxbbfOtUjb9h3Ta0gT4ug2g==}
-    engines: {node: ^16.0.0 || >=18.0.0}
-    peerDependencies:
-      eslint: ^7.0.0 || ^8.0.0
-
   '@typescript-eslint/utils@7.1.0':
     resolution: {integrity: sha512-WUFba6PZC5OCGEmbweGpnNJytJiLG7ZvDBJJoUcX4qZYf1mGZ97mO2Mps6O2efxJcJdRNpqweCistDbZMwIVHw==}
     engines: {node: ^16.0.0 || >=18.0.0}
     peerDependencies:
       eslint: ^8.56.0
 
-  '@typescript-eslint/utils@7.7.1':
-    resolution: {integrity: sha512-QUvBxPEaBXf41ZBbaidKICgVL8Hin0p6prQDu6bbetWo39BKbWJxRsErOzMNT1rXvTll+J7ChrbmMCXM9rsvOQ==}
+  '@typescript-eslint/utils@7.17.0':
+    resolution: {integrity: sha512-r+JFlm5NdB+JXc7aWWZ3fKSm1gn0pkswEwIYsrGPdsT2GjsRATAKXiNtp3vgAAO1xZhX8alIOEQnNMl3kbTgJw==}
     engines: {node: ^18.18.0 || >=20.0.0}
     peerDependencies:
       eslint: ^8.56.0
 
-  '@typescript-eslint/visitor-keys@6.11.0':
-    resolution: {integrity: sha512-+SUN/W7WjBr05uRxPggJPSzyB8zUpaYo2hByKasWbqr3PM8AXfZt8UHdNpBS1v9SA62qnSSMF3380SwDqqprgQ==}
-    engines: {node: ^16.0.0 || >=18.0.0}
-
   '@typescript-eslint/visitor-keys@7.1.0':
     resolution: {integrity: sha512-FhUqNWluiGNzlvnDZiXad4mZRhtghdoKW6e98GoEOYSu5cND+E39rG5KwJMUzeENwm1ztYBRqof8wMLP+wNPIA==}
     engines: {node: ^16.0.0 || >=18.0.0}
 
-  '@typescript-eslint/visitor-keys@7.7.1':
-    resolution: {integrity: sha512-gBL3Eq25uADw1LQ9kVpf3hRM+DWzs0uZknHYK3hq4jcTPqVCClHGDnB6UUUV2SFeBeA4KWHWbbLqmbGcZ4FYbw==}
+  '@typescript-eslint/visitor-keys@7.17.0':
+    resolution: {integrity: sha512-RVGC9UhPOCsfCdI9pU++K4nD7to+jTcMIbXTSOcrLqUEW6gF2pU1UUbYJKc9cvcRSK1UDeMJ7pdMxf4bhMpV/A==}
     engines: {node: ^18.18.0 || >=20.0.0}
 
   '@ungap/structured-clone@1.2.0':
     resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==}
 
-  '@vitejs/plugin-vue@5.0.4':
-    resolution: {integrity: sha512-WS3hevEszI6CEVEx28F8RjTX97k3KsrcY6kvTg7+Whm5y3oYvcqzVeGCU3hxSAn4uY2CLCkeokkGKpoctccilQ==}
+  '@vitejs/plugin-vue@5.1.4':
+    resolution: {integrity: sha512-N2XSI2n3sQqp5w7Y/AN/L2XDjBIRGqXko+eDp42sydYSBeJuSm5a1sLf8zakmo8u7tA8NmBgoDLA1HeOESjp9A==}
     engines: {node: ^18.0.0 || >=20.0.0}
     peerDependencies:
       vite: ^5.0.0
       vue: ^3.2.25
 
-  '@vitest/coverage-v8@0.34.6':
-    resolution: {integrity: sha512-fivy/OK2d/EsJFoEoxHFEnNGTg+MmdZBAVK9Ka4qhXR2K3J0DS08vcGVwzDtXSuUMabLv4KtPcpSKkcMXFDViw==}
+  '@vitest/coverage-v8@1.6.0':
+    resolution: {integrity: sha512-KvapcbMY/8GYIG0rlwwOKCVNRc0OL20rrhFkg/CHNzncV03TE2XWvO5w9uZYoxNiMEBacAJt3unSOiZ7svePew==}
     peerDependencies:
-      vitest: '>=0.32.0 <1'
+      vitest: 1.6.0
 
-  '@vitest/expect@0.34.6':
-    resolution: {integrity: sha512-QUzKpUQRc1qC7qdGo7rMK3AkETI7w18gTCUrsNnyjjJKYiuUB9+TQK3QnR1unhCnWRC0AbKv2omLGQDF/mIjOw==}
+  '@vitest/expect@1.6.0':
+    resolution: {integrity: sha512-ixEvFVQjycy/oNgHjqsL6AZCDduC+tflRluaHIzKIsdbzkLn2U/iBnVeJwB6HsIjQBdfMR8Z0tRxKUsvFJEeWQ==}
 
-  '@vitest/expect@1.3.1':
-    resolution: {integrity: sha512-xofQFwIzfdmLLlHa6ag0dPV8YsnKOCP1KdAeVVh34vSjN2dcUiXYCD9htu/9eM7t8Xln4v03U9HLxLpPlsXdZw==}
+  '@vitest/expect@2.0.5':
+    resolution: {integrity: sha512-yHZtwuP7JZivj65Gxoi8upUN2OzHTi3zVfjwdpu2WrvCZPLwsJ2Ey5ILIPccoW23dd/zQBlJ4/dhi7DWNyXCpA==}
 
-  '@vitest/runner@0.34.6':
-    resolution: {integrity: sha512-1CUQgtJSLF47NnhN+F9X2ycxUP0kLHQ/JWvNHbeBfwW8CzEGgeskzNnHDyv1ieKTltuR6sdIHV+nmR6kPxQqzQ==}
+  '@vitest/pretty-format@2.0.5':
+    resolution: {integrity: sha512-h8k+1oWHfwTkyTkb9egzwNMfJAEx4veaPSnMeKbVSjp4euqGSbQlm5+6VHwTr7u4FJslVVsUG5nopCaAYdOmSQ==}
 
-  '@vitest/snapshot@0.34.6':
-    resolution: {integrity: sha512-B3OZqYn6k4VaN011D+ve+AA4whM4QkcwcrwaKwAbyyvS/NB1hCWjFIBQxAQQSQir9/RtyAAGuq+4RJmbn2dH4w==}
+  '@vitest/pretty-format@2.1.1':
+    resolution: {integrity: sha512-SjxPFOtuINDUW8/UkElJYQSFtnWX7tMksSGW0vfjxMneFqxVr8YJ979QpMbDW7g+BIiq88RAGDjf7en6rvLPPQ==}
 
-  '@vitest/spy@0.34.6':
-    resolution: {integrity: sha512-xaCvneSaeBw/cz8ySmF7ZwGvL0lBjfvqc1LpQ/vcdHEvpLn3Ff1vAvjw+CoGn0802l++5L/pxb7whwcWAw+DUQ==}
+  '@vitest/runner@1.6.0':
+    resolution: {integrity: sha512-P4xgwPjwesuBiHisAVz/LSSZtDjOTPYZVmNAnpHHSR6ONrf8eCJOFRvUwdHn30F5M1fxhqtl7QZQUk2dprIXAg==}
 
-  '@vitest/spy@1.3.1':
-    resolution: {integrity: sha512-xAcW+S099ylC9VLU7eZfdT9myV67Nor9w9zhf0mGCYJSO+zM2839tOeROTdikOi/8Qeusffvxb/MyBSOja1Uig==}
+  '@vitest/snapshot@1.6.0':
+    resolution: {integrity: sha512-+Hx43f8Chus+DCmygqqfetcAZrDJwvTj0ymqjQq4CvmpKFSTVteEOBzCusu1x2tt4OJcvBflyHUE0DZSLgEMtQ==}
 
   '@vitest/spy@1.6.0':
     resolution: {integrity: sha512-leUTap6B/cqi/bQkXUu6bQV5TZPx7pmMBKBQiI0rJA8c3pB56ZsaTbREnF7CJfmvAS4V2cXIBAh/3rVwrrCYgw==}
 
-  '@vitest/utils@0.34.6':
-    resolution: {integrity: sha512-IG5aDD8S6zlvloDsnzHw0Ut5xczlF+kv2BOTo+iXfPr54Yhi5qbVOgGB1hZaVq4iJ4C/MZ2J0y15IlsV/ZcI0A==}
-
-  '@vitest/utils@1.3.1':
-    resolution: {integrity: sha512-d3Waie/299qqRyHTm2DjADeTaNdNSVsnwHPWrs20JMpjh6eiVq7ggggweO8rc4arhf6rRkWuHKwvxGvejUXZZQ==}
+  '@vitest/spy@2.0.5':
+    resolution: {integrity: sha512-c/jdthAhvJdpfVuaexSrnawxZz6pywlTPe84LUB2m/4t3rl2fTo9NFGBG4oWgaD+FTgDDV8hJ/nibT7IfH3JfA==}
 
   '@vitest/utils@1.6.0':
     resolution: {integrity: sha512-21cPiuGMoMZwiOHa2i4LXkMkMkCGzA+MVFV70jRwHo95dL4x/ts5GZhML1QWuy7yfp3WzK3lRvZi3JnXTYqrBw==}
 
+  '@vitest/utils@2.0.5':
+    resolution: {integrity: sha512-d8HKbqIcya+GR67mkZbrzhS5kKhtp8dQLcmRZLGTscGVg7yImT82cIrhtn2L8+VujWcy6KZweApgNmPsTAO/UQ==}
+
+  '@vitest/utils@2.1.1':
+    resolution: {integrity: sha512-Y6Q9TsI+qJ2CC0ZKj6VBb+T8UPz593N113nnUykqwANqhgf3QkZeHFlusgKLTqrnVHbj/XDKZcDHol+dxVT+rQ==}
+
   '@volar/language-core@2.2.0':
     resolution: {integrity: sha512-a8WG9+4OdeNDW4ywABZIM6S6UN7em8uIlM/BZ2pWQUYrVmX+m8sj/X+QadvO+Li/t/LjAqbWJQtVgxdpEWLALQ==}
 
+  '@volar/language-core@2.4.5':
+    resolution: {integrity: sha512-F4tA0DCO5Q1F5mScHmca0umsi2ufKULAnMOVBfMsZdT4myhVl4WdKRwCaKcfOkIEuyrAVvtq1ESBdZ+rSyLVww==}
+
   '@volar/source-map@2.2.0':
     resolution: {integrity: sha512-HQlPRlHOVqCCHK8wI76ZldHkEwKsjp7E6idUc36Ekni+KJDNrqgSqPvyHQixybXPHNU7CI9Uxd9/IkxO7LuNBw==}
 
+  '@volar/source-map@2.4.5':
+    resolution: {integrity: sha512-varwD7RaKE2J/Z+Zu6j3mNNJbNT394qIxXwdvz/4ao/vxOfyClZpSDtLKkwWmecinkOVos5+PWkWraelfMLfpw==}
+
   '@volar/typescript@2.2.0':
     resolution: {integrity: sha512-wC6l4zLiiCLxF+FGaHCbWlQYf4vMsnRxYhcI6WgvaNppOD6r1g+Ef1RKRJUApALWU46Yy/JDU/TbdV6w/X6Liw==}
 
-  '@vue/compiler-core@3.4.21':
-    resolution: {integrity: sha512-MjXawxZf2SbZszLPYxaFCjxfibYrzr3eYbKxwpLR9EQN+oaziSu3qKVbwBERj1IFIB8OLUewxB5m/BFzi613og==}
+  '@volar/typescript@2.4.5':
+    resolution: {integrity: sha512-mcT1mHvLljAEtHviVcBuOyAwwMKz1ibXTi5uYtP/pf4XxoAzpdkQ+Br2IC0NPCvLCbjPZmbf3I0udndkfB1CDg==}
 
-  '@vue/compiler-core@3.4.25':
-    resolution: {integrity: sha512-Y2pLLopaElgWnMNolgG8w3C5nNUVev80L7hdQ5iIKPtMJvhVpG0zhnBG/g3UajJmZdvW0fktyZTotEHD1Srhbg==}
+  '@vue/compiler-core@3.4.37':
+    resolution: {integrity: sha512-ZDDT/KiLKuCRXyzWecNzC5vTcubGz4LECAtfGPENpo0nrmqJHwuWtRLxk/Sb9RAKtR9iFflFycbkjkY+W/PZUQ==}
 
-  '@vue/compiler-core@3.4.26':
-    resolution: {integrity: sha512-N9Vil6Hvw7NaiyFUFBPXrAyETIGlQ8KcFMkyk6hW1Cl6NvoqvP+Y8p1Eqvx+UdqsnrnI9+HMUEJegzia3mhXmQ==}
+  '@vue/compiler-core@3.5.10':
+    resolution: {integrity: sha512-iXWlk+Cg/ag7gLvY0SfVucU8Kh2CjysYZjhhP70w9qI4MvSox4frrP+vDGvtQuzIcgD8+sxM6lZvCtdxGunTAA==}
 
-  '@vue/compiler-dom@3.4.21':
-    resolution: {integrity: sha512-IZC6FKowtT1sl0CR5DpXSiEB5ayw75oT2bma1BEhV7RRR1+cfwLrxc2Z8Zq/RGFzJ8w5r9QtCOvTjQgdn0IKmA==}
+  '@vue/compiler-core@3.5.11':
+    resolution: {integrity: sha512-PwAdxs7/9Hc3ieBO12tXzmTD+Ln4qhT/56S+8DvrrZ4kLDn4Z/AMUr8tXJD0axiJBS0RKIoNaR0yMuQB9v9Udg==}
 
-  '@vue/compiler-dom@3.4.25':
-    resolution: {integrity: sha512-Ugz5DusW57+HjllAugLci19NsDK+VyjGvmbB2TXaTcSlQxwL++2PETHx/+Qv6qFwNLzSt7HKepPe4DcTE3pBWg==}
+  '@vue/compiler-dom@3.4.37':
+    resolution: {integrity: sha512-rIiSmL3YrntvgYV84rekAtU/xfogMUJIclUMeIKEtVBFngOL3IeZHhsH3UaFEgB5iFGpj6IW+8YuM/2Up+vVag==}
 
-  '@vue/compiler-dom@3.4.26':
-    resolution: {integrity: sha512-4CWbR5vR9fMg23YqFOhr6t6WB1Fjt62d6xdFPyj8pxrYub7d+OgZaObMsoxaF9yBUHPMiPFK303v61PwAuGvZA==}
+  '@vue/compiler-dom@3.5.10':
+    resolution: {integrity: sha512-DyxHC6qPcktwYGKOIy3XqnHRrrXyWR2u91AjP+nLkADko380srsC2DC3s7Y1Rk6YfOlxOlvEQKa9XXmLI+W4ZA==}
 
-  '@vue/compiler-sfc@3.4.26':
-    resolution: {integrity: sha512-It1dp+FAOCgluYSVYlDn5DtZBxk1NCiJJfu2mlQqa/b+k8GL6NG/3/zRbJnHdhV2VhxFghaDq5L4K+1dakW6cw==}
+  '@vue/compiler-dom@3.5.11':
+    resolution: {integrity: sha512-pyGf8zdbDDRkBrEzf8p7BQlMKNNF5Fk/Cf/fQ6PiUz9at4OaUfyXW0dGJTo2Vl1f5U9jSLCNf0EZJEogLXoeew==}
 
-  '@vue/compiler-ssr@3.4.26':
-    resolution: {integrity: sha512-FNwLfk7LlEPRY/g+nw2VqiDKcnDTVdCfBREekF8X74cPLiWHUX6oldktf/Vx28yh4STNy7t+/yuLoMBBF7YDiQ==}
+  '@vue/compiler-sfc@3.4.37':
+    resolution: {integrity: sha512-vCfetdas40Wk9aK/WWf8XcVESffsbNkBQwS5t13Y/PcfqKfIwJX2gF+82th6dOpnpbptNMlMjAny80li7TaCIg==}
 
-  '@vue/devtools-api@6.6.1':
-    resolution: {integrity: sha512-LgPscpE3Vs0x96PzSSB4IGVSZXZBZHpfxs+ZA1d+VEPwHdOXowy/Y2CsvCAIFrf+ssVU1pD1jidj505EpUnfbA==}
+  '@vue/compiler-sfc@3.5.11':
+    resolution: {integrity: sha512-gsbBtT4N9ANXXepprle+X9YLg2htQk1sqH/qGJ/EApl+dgpUBdTv3yP7YlR535uHZY3n6XaR0/bKo0BgwwDniw==}
+
+  '@vue/compiler-ssr@3.4.37':
+    resolution: {integrity: sha512-TyAgYBWrHlFrt4qpdACh8e9Ms6C/AZQ6A6xLJaWrCL8GCX5DxMzxyeFAEMfU/VFr4tylHm+a2NpfJpcd7+20XA==}
+
+  '@vue/compiler-ssr@3.5.11':
+    resolution: {integrity: sha512-P4+GPjOuC2aFTk1Z4WANvEhyOykcvEd5bIj2KVNGKGfM745LaXGr++5njpdBTzVz5pZifdlR1kpYSJJpIlSePA==}
+
+  '@vue/compiler-vue2@2.7.16':
+    resolution: {integrity: sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A==}
 
   '@vue/language-core@2.0.16':
     resolution: {integrity: sha512-Bc2sexRH99pznOph8mLw2BlRZ9edm7tW51kcBXgx8adAoOcZUWJj3UNSsdQ6H9Y8meGz7BoazVrVo/jUukIsPw==}
@@ -5057,28 +5144,50 @@ packages:
       typescript:
         optional: true
 
-  '@vue/reactivity@3.4.26':
-    resolution: {integrity: sha512-E/ynEAu/pw0yotJeLdvZEsp5Olmxt+9/WqzvKff0gE67tw73gmbx6tRkiagE/eH0UCubzSlGRebCbidB1CpqZQ==}
-
-  '@vue/runtime-core@3.4.26':
-    resolution: {integrity: sha512-AFJDLpZvhT4ujUgZSIL9pdNcO23qVFh7zWCsNdGQBw8ecLNxOOnPcK9wTTIYCmBJnuPHpukOwo62a2PPivihqw==}
-
-  '@vue/runtime-dom@3.4.26':
-    resolution: {integrity: sha512-UftYA2hUXR2UOZD/Fc3IndZuCOOJgFxJsWOxDkhfVcwLbsfh2CdXE2tG4jWxBZuDAs9J9PzRTUFt1PgydEtItw==}
-
-  '@vue/server-renderer@3.4.26':
-    resolution: {integrity: sha512-xoGAqSjYDPGAeRWxeoYwqJFD/gw7mpgzOvSxEmjWaFO2rE6qpbD1PC172YRpvKhrihkyHJkNDADFXTfCyVGhKw==}
+  '@vue/language-core@2.1.6':
+    resolution: {integrity: sha512-MW569cSky9R/ooKMh6xa2g1D0AtRKbL56k83dzus/bx//RDJk24RHWkMzbAlXjMdDNyxAaagKPRquBIxkxlCkg==}
     peerDependencies:
-      vue: 3.4.26
+      typescript: '*'
+    peerDependenciesMeta:
+      typescript:
+        optional: true
 
-  '@vue/shared@3.4.21':
-    resolution: {integrity: sha512-PuJe7vDIi6VYSinuEbUIQgMIRZGgM8e4R+G+/dQTk0X1NEdvgvvgv7m+rfmDH1gZzyA1OjjoWskvHlfRNfQf3g==}
+  '@vue/reactivity@3.4.37':
+    resolution: {integrity: sha512-UmdKXGx0BZ5kkxPqQr3PK3tElz6adTey4307NzZ3whZu19i5VavYal7u2FfOmAzlcDVgE8+X0HZ2LxLb/jgbYw==}
 
-  '@vue/shared@3.4.25':
-    resolution: {integrity: sha512-k0yappJ77g2+KNrIaF0FFnzwLvUBLUYr8VOwz+/6vLsmItFp51AcxLL7Ey3iPd7BIRyWPOcqUjMnm7OkahXllA==}
+  '@vue/reactivity@3.5.11':
+    resolution: {integrity: sha512-Nqo5VZEn8MJWlCce8XoyVqHZbd5P2NH+yuAaFzuNSR96I+y1cnuUiq7xfSG+kyvLSiWmaHTKP1r3OZY4mMD50w==}
 
-  '@vue/shared@3.4.26':
-    resolution: {integrity: sha512-Fg4zwR0GNnjzodMt3KRy2AWGMKQXByl56+4HjN87soxLNU9P5xcJkstAlIeEF3cU6UYOzmJl1tV0dVPGIljCnQ==}
+  '@vue/runtime-core@3.4.37':
+    resolution: {integrity: sha512-MNjrVoLV/sirHZoD7QAilU1Ifs7m/KJv4/84QVbE6nyAZGQNVOa1HGxaOzp9YqCG+GpLt1hNDC4RbH+KtanV7w==}
+
+  '@vue/runtime-core@3.5.11':
+    resolution: {integrity: sha512-7PsxFGqwfDhfhh0OcDWBG1DaIQIVOLgkwA5q6MtkPiDFjp5gohVnJEahSktwSFLq7R5PtxDKy6WKURVN1UDbzA==}
+
+  '@vue/runtime-dom@3.4.37':
+    resolution: {integrity: sha512-Mg2EwgGZqtwKrqdL/FKMF2NEaOHuH+Ks9TQn3DHKyX//hQTYOun+7Tqp1eo0P4Ds+SjltZshOSRq6VsU0baaNg==}
+
+  '@vue/runtime-dom@3.5.11':
+    resolution: {integrity: sha512-GNghjecT6IrGf0UhuYmpgaOlN7kxzQBhxWEn08c/SQDxv1yy4IXI1bn81JgEpQ4IXjRxWtPyI8x0/7TF5rPfYQ==}
+
+  '@vue/server-renderer@3.4.37':
+    resolution: {integrity: sha512-jZ5FAHDR2KBq2FsRUJW6GKDOAG9lUTX8aBEGq4Vf6B/35I9fPce66BornuwmqmKgfiSlecwuOb6oeoamYMohkg==}
+    peerDependencies:
+      vue: 3.4.37
+
+  '@vue/server-renderer@3.5.11':
+    resolution: {integrity: sha512-cVOwYBxR7Wb1B1FoxYvtjJD8X/9E5nlH4VSkJy2uMA1MzYNdzAAB//l8nrmN9py/4aP+3NjWukf9PZ3TeWULaA==}
+    peerDependencies:
+      vue: 3.5.11
+
+  '@vue/shared@3.4.37':
+    resolution: {integrity: sha512-nIh8P2fc3DflG8+5Uw8PT/1i17ccFn0xxN/5oE9RfV5SVnd7G0XEFRwakrnNFE/jlS95fpGXDVG5zDETS26nmg==}
+
+  '@vue/shared@3.5.10':
+    resolution: {integrity: sha512-VkkBhU97Ki+XJ0xvl4C9YJsIZ2uIlQ7HqPpZOS3m9VCvmROPaChZU6DexdMJqvz9tbgG+4EtFVrSuailUq5KGQ==}
+
+  '@vue/shared@3.5.11':
+    resolution: {integrity: sha512-W8GgysJVnFo81FthhzurdRAWP/byq3q2qIw70e0JWblzVhjgOMiC2GyovXrZTFQJnFVryYaKGP3Tc9vYzYm6PQ==}
 
   '@vue/test-utils@2.4.1':
     resolution: {integrity: sha512-VO8nragneNzUZUah6kOjiFmD/gwRjUauG9DROh6oaOeFwX1cZRUNHhdeogE8635cISigXFTtGLUQWx5KCb0xeg==}
@@ -5092,20 +5201,6 @@ packages:
   '@webgpu/types@0.1.30':
     resolution: {integrity: sha512-9AXJSmL3MzY8ZL//JjudA//q+2kBRGhLBFpkdGksWIuxrMy81nFrCzj2Am+mbh8WoU6rXmv7cY5E3rdlyru2Qg==}
 
-  '@yarnpkg/esbuild-plugin-pnp@3.0.0-rc.15':
-    resolution: {integrity: sha512-kYzDJO5CA9sy+on/s2aIW0411AklfCi8Ck/4QDivOqsMKpStZA2SsR+X27VTggGwpStWaLrjJcDcdDMowtG8MA==}
-    engines: {node: '>=14.15.0'}
-    peerDependencies:
-      esbuild: '>=0.10.0'
-
-  '@yarnpkg/fslib@2.10.3':
-    resolution: {integrity: sha512-41H+Ga78xT9sHvWLlFOZLIhtU6mTGZ20pZ29EiZa97vnxdohJD2AF42rCoAoWfqUz486xY6fhjMH+DYEM9r14A==}
-    engines: {node: '>=12 <14 || 14.2 - 14.9 || >14.10.0'}
-
-  '@yarnpkg/libzip@2.3.0':
-    resolution: {integrity: sha512-6xm38yGVIa6mKm/DUCF2zFFJhERh/QWp1ufm4cNUvxsONBmfPg8uZ9pZBdOmF6qFGr/HlT6ABBkCSx/dlEtvWg==}
-    engines: {node: '>=12 <14 || 14.2 - 14.9 || >14.10.0'}
-
   abbrev@1.1.1:
     resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==}
 
@@ -5152,15 +5247,11 @@ packages:
     engines: {node: '>=0.4.0'}
     hasBin: true
 
-  acorn@8.11.3:
-    resolution: {integrity: sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==}
+  acorn@8.12.1:
+    resolution: {integrity: sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==}
     engines: {node: '>=0.4.0'}
     hasBin: true
 
-  address@1.2.2:
-    resolution: {integrity: sha512-4B/qKCfeE/ODUaAUpSwfzazo5x29WD4r3vXiWsB7I2mSDAihwEqKO+g8GELZUQSSAo5e1XTYh3ZVfLyxBc12nA==}
-    engines: {node: '>= 10.0.0'}
-
   adm-zip@0.5.10:
     resolution: {integrity: sha512-x0HvcHqVJNTPk/Bw8JbLWlWoo6Wwnsug0fnYYro1HBrjxZ3G7/AZk7Ahv8JwDe1uIcz8eBqvu86FuF1POiG7vQ==}
     engines: {node: '>=6.0'}
@@ -5185,9 +5276,9 @@ packages:
     resolution: {integrity: sha512-gOsf2YwSlleG6IjRYG2A7k0HmBMEo6qVNk9Bp/EaLgAJT5ngH6PXbqa4ItvnEwCm/velL5jAnQgsHsWnjhGmvw==}
     engines: {node: '>=18'}
 
-  aiscript-vscode@https://codeload.github.com/aiscript-dev/aiscript-vscode/tar.gz/34bf4e1530efcf1efa855bd04e2dab39735e1b02:
-    resolution: {tarball: https://codeload.github.com/aiscript-dev/aiscript-vscode/tar.gz/34bf4e1530efcf1efa855bd04e2dab39735e1b02}
-    version: 0.1.9
+  aiscript-vscode@https://codeload.github.com/aiscript-dev/aiscript-vscode/tar.gz/e1e1b27f2f72cd28a473e004b6da0d8fc0bd40d9:
+    resolution: {tarball: https://codeload.github.com/aiscript-dev/aiscript-vscode/tar.gz/e1e1b27f2f72cd28a473e004b6da0d8fc0bd40d9}
+    version: 0.1.11
     engines: {vscode: ^1.83.0}
 
   ajv-draft-04@1.0.0:
@@ -5198,8 +5289,8 @@ packages:
       ajv:
         optional: true
 
-  ajv-formats@2.1.1:
-    resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==}
+  ajv-formats@3.0.1:
+    resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==}
     peerDependencies:
       ajv: ^8.0.0
     peerDependenciesMeta:
@@ -5209,9 +5300,15 @@ packages:
   ajv@6.12.6:
     resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==}
 
+  ajv@8.12.0:
+    resolution: {integrity: sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==}
+
   ajv@8.13.0:
     resolution: {integrity: sha512-PRA911Blj99jR5RMeTunVbNXMF6Lp4vZXnk5GQjcnUWUTsrXtekg/pnmFFI2u/I36Y/2bITGS30GZCXei6uNkA==}
 
+  ajv@8.17.1:
+    resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==}
+
   ansi-colors@4.1.3:
     resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==}
     engines: {node: '>=6'}
@@ -5251,9 +5348,6 @@ packages:
     resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==}
     engines: {node: '>= 8'}
 
-  app-root-dir@1.0.2:
-    resolution: {integrity: sha512-jlpIfsOoNoafl92Sz//64uQHGSyMrD2vYG5d8o2a4qGvyNCvXur7bzIsWtAC/6flI2RYAp3kv8rsfBtaLm7w0g==}
-
   app-root-path@3.1.0:
     resolution: {integrity: sha512-biN3PwB2gUtjaYy/isrU3aNWI5w+fAfvHkSvCKeQGxhmYpwKFUxudR3Yya+KqVRHBmEDYh+/lTozYCFbmzX4nA==}
     engines: {node: '>= 6.0.0'}
@@ -5275,12 +5369,10 @@ packages:
     resolution: {integrity: sha512-ZcbTaIqJOfCc03QwD468Unz/5Ir8ATtvAHsK+FdXbDIbGfihqh9mrvdcYunQzqn4HrvWWaFyaxJhGZagaJJpPQ==}
     engines: {node: '>= 14'}
 
-  archy@1.0.0:
-    resolution: {integrity: sha512-Xg+9RwCg/0p32teKdGMPTPnVXKD0w3DfHnFTficozsAgsvq2XenPJq/MYpzzQ/v8zrOyJn6Ds39VA4JIDwFfqw==}
-
   are-we-there-yet@2.0.0:
     resolution: {integrity: sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==}
     engines: {node: '>=10'}
+    deprecated: This package is no longer supported.
 
   arg@5.0.2:
     resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==}
@@ -5294,22 +5386,29 @@ packages:
   aria-query@5.1.3:
     resolution: {integrity: sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ==}
 
+  aria-query@5.3.0:
+    resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==}
+
   array-buffer-byte-length@1.0.0:
     resolution: {integrity: sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==}
 
+  array-buffer-byte-length@1.0.1:
+    resolution: {integrity: sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg==}
+    engines: {node: '>= 0.4'}
+
   array-flatten@1.1.1:
     resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==}
 
-  array-includes@3.1.7:
-    resolution: {integrity: sha512-dlcsNBIiWhPkHdOEEKnehA+RNUWDc4UqFtnIXU4uuYDPtA4LDkr7qip2p0VvFAEXNDr0yWZ9PJyIRiGjRLQzwQ==}
+  array-includes@3.1.8:
+    resolution: {integrity: sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==}
     engines: {node: '>= 0.4'}
 
   array-union@2.1.0:
     resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==}
     engines: {node: '>=8'}
 
-  array.prototype.findlastindex@1.2.3:
-    resolution: {integrity: sha512-LzLoiOMAxvy+Gd3BAq3B7VeIgPdo+Q8hthvKtXybMvRV0jrXfJM/t8mw7nNlpEcVlVUnCnM2KSX4XU5HmpodOA==}
+  array.prototype.findlastindex@1.2.5:
+    resolution: {integrity: sha512-zfETvRFA8o7EiNn++N5f/kaCw221hrpGsDmcpndVupkPzEc1Wuf3VgC0qby1BbHs7f5DVYjgtEU2LLh5bqeGfQ==}
     engines: {node: '>= 0.4'}
 
   array.prototype.flat@1.3.2:
@@ -5324,6 +5423,10 @@ packages:
     resolution: {integrity: sha512-09x0ZWFEjj4WD8PDbykUwo3t9arLn8NIzmmYEJFpYekOAQjpkGSyrQhNoRTcwwcFRu+ycWF78QZ63oWTqSjBcw==}
     engines: {node: '>= 0.4'}
 
+  arraybuffer.prototype.slice@1.0.3:
+    resolution: {integrity: sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A==}
+    engines: {node: '>= 0.4'}
+
   arrify@1.0.1:
     resolution: {integrity: sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==}
     engines: {node: '>=0.10.0'}
@@ -5348,12 +5451,13 @@ packages:
     resolution: {integrity: sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==}
     engines: {node: '>=0.8'}
 
-  assert@2.1.0:
-    resolution: {integrity: sha512-eLHpSK/Y4nhMJ07gDaAzoX/XAKS8PSaojml3M0DM4JpV1LAi5JOJ/p6H/XWrl8L+DzVEvVCW1z3vWAaB9oTsQw==}
-
   assertion-error@1.1.0:
     resolution: {integrity: sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==}
 
+  assertion-error@2.0.1:
+    resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==}
+    engines: {node: '>=12'}
+
   ast-types@0.16.1:
     resolution: {integrity: sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg==}
     engines: {node: '>=4'}
@@ -5362,13 +5466,16 @@ packages:
     resolution: {integrity: sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==}
     engines: {node: '>=8'}
 
-  astring@1.8.6:
-    resolution: {integrity: sha512-ISvCdHdlTDlH5IpxQJIex7BWBywFWgjJSVdwst+/iQCoEYnyOaQ95+X1JGshuBjGp6nxKUy1jMgE3zPqN7fQdg==}
+  astring@1.9.0:
+    resolution: {integrity: sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg==}
     hasBin: true
 
   async-mutex@0.5.0:
     resolution: {integrity: sha512-1A94B18jkJ3DYq284ohPxoXbfTA5HsQ7/Mf4DEhcyLx3Bz27Rh59iScbB6EPiP+B+joue6YCxcMXSbFC1tZKwA==}
 
+  async@0.2.10:
+    resolution: {integrity: sha512-eAkdoKxU6/LkKDBzLpT+t6Ff5EtfSF4wx1WfJiPEEV7WNLnDaRXk0oVysiEPm262roaachGexwUv94WhSgN5TQ==}
+
   async@3.2.4:
     resolution: {integrity: sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==}
 
@@ -5387,11 +5494,15 @@ packages:
     resolution: {integrity: sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==}
     engines: {node: '>= 0.4'}
 
-  avvio@8.3.0:
-    resolution: {integrity: sha512-VBVH0jubFr9LdFASy/vNtm5giTrnbVquWBhT0fyizuNK2rQ7e7ONU2plZQWUNqtE1EmxFEb+kbSkFRkstiaS9Q==}
+  available-typed-arrays@1.0.7:
+    resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==}
+    engines: {node: '>= 0.4'}
 
-  aws-sdk-client-mock@3.0.1:
-    resolution: {integrity: sha512-9VAzJLl8mz99KP9HjOm/93d8vznRRUTpJooPBOunRdUAnVYopCe9xmMuu7eVemu8fQ+w6rP7o5bBK1kAFkB2KQ==}
+  avvio@9.0.0:
+    resolution: {integrity: sha512-UbYrOXgE/I+knFG+3kJr9AgC7uNo8DG+FGGODpH9Bj1O1kL/QDjBXnTem9leD3VdQKtaHjV3O85DQ7hHh4IIHw==}
+
+  aws-sdk-client-mock@4.0.1:
+    resolution: {integrity: sha512-yD2mmgy73Xce097G5hIpr1k7j50qzvJ49/+6osGZiCyk4m6cwhb+2x7kKFY1gEMwTzaS8+m8fXv9SB29SkRYyQ==}
 
   aws-sign2@0.7.0:
     resolution: {integrity: sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==}
@@ -5402,17 +5513,12 @@ packages:
   axios@0.24.0:
     resolution: {integrity: sha512-Q6cWsys88HoPgAaFAVUb0WpPk0O8iTeisR9IMqy9G8AbO4NlpVknrnQS03zzF9PGAWgO3cgletO3VjV/P7VztA==}
 
-  axios@1.6.2:
-    resolution: {integrity: sha512-7i24Ri4pmDRfJTR7LDBhsOTtcm+9kjX5WiY1X3wIisx6G9So3pfMkEiU7emUBe46oceVImccTEM3k6C5dbVW8A==}
+  axios@1.7.7:
+    resolution: {integrity: sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==}
 
   b4a@1.6.4:
     resolution: {integrity: sha512-fpWrvyVHEKyeEvbKZTVOeZF3VSKKWtJxFIxX/jaVPf+cLbGUSitjb49pHLqPV2BUNNZ0LcoeEGfE/YCpyDYHIw==}
 
-  babel-core@7.0.0-bridge.0:
-    resolution: {integrity: sha512-poPX9mZH/5CSanm50Q+1toVci6pv5KSRv/5TWCwtzQS5XEwn40BcCrgIeMFWP9CKKIniKXNxoIOnOq4VVlGXhg==}
-    peerDependencies:
-      '@babel/core': ^7.0.0-0
-
   babel-jest@29.7.0:
     resolution: {integrity: sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==}
     engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
@@ -5427,21 +5533,6 @@ packages:
     resolution: {integrity: sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==}
     engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
 
-  babel-plugin-polyfill-corejs2@0.4.6:
-    resolution: {integrity: sha512-jhHiWVZIlnPbEUKSSNb9YoWcQGdlTLq7z1GHL4AjFxaoOUMuuEVJ+Y4pAaQUGOGk93YsVCKPbqbfw3m0SM6H8Q==}
-    peerDependencies:
-      '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0
-
-  babel-plugin-polyfill-corejs3@0.8.6:
-    resolution: {integrity: sha512-leDIc4l4tUgU7str5BWLS2h8q2N4Nf6lGZP6UrNDxdtfF2g69eJ5L0H7S8A5Ln/arfFAfHor5InAdZuIOwZdgQ==}
-    peerDependencies:
-      '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0
-
-  babel-plugin-polyfill-regenerator@0.5.3:
-    resolution: {integrity: sha512-8sHeDOmXC8csczMrYEOf0UTNa4yE2SxV5JGeT/LP1n0OYVDUUFPxG9vdk2AlDlIit4t+Kf0xCtpgXPBwnn/9pw==}
-    peerDependencies:
-      '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0
-
   babel-preset-current-node-syntax@1.0.1:
     resolution: {integrity: sha512-M7LQ0bxarkxQoN+vz5aJPsLBn77n8QgTFmo8WK0/44auK2xlCXrYcUxHFxgU7qW5Yzw/CjmLRK2uJzaCd7LvqQ==}
     peerDependencies:
@@ -5476,10 +5567,6 @@ packages:
     resolution: {integrity: sha512-aVNobHnJqLiUelTaHat9DZ1qM2w0C0Eym4LPI/3JxOnSokGVdsl1T1kN7TFvsEAD8G47A6VKQ0TVHqbBnYMJlQ==}
     engines: {node: '>=12.0.0'}
 
-  big-integer@1.6.51:
-    resolution: {integrity: sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg==}
-    engines: {node: '>=0.6'}
-
   bin-check@4.1.0:
     resolution: {integrity: sha512-b6weQyEUKsDGFlACWSIOfveEnImkJyK/FGW6FAG42loyoquvjdtOIqO6yBFzHyqyVVhNgNkQxxx09SFLK28YnA==}
     engines: {node: '>=4'}
@@ -5496,9 +5583,6 @@ packages:
     resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==}
     engines: {node: '>=8'}
 
-  bl@4.1.0:
-    resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==}
-
   blob-util@2.0.2:
     resolution: {integrity: sha512-T7JQa+zsXXEa6/8ZhHcQEW1UFfVM49Ts65uBkFL6fz2QmrElqmbajIDJvuA0tEhRe5eIjpV9ZF+0RfZR9voJFQ==}
 
@@ -5511,12 +5595,8 @@ packages:
   bn.js@4.12.0:
     resolution: {integrity: sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==}
 
-  body-parser@1.20.1:
-    resolution: {integrity: sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==}
-    engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16}
-
-  body-parser@1.20.2:
-    resolution: {integrity: sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==}
+  body-parser@1.20.3:
+    resolution: {integrity: sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==}
     engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16}
 
   boolbase@1.0.0:
@@ -5525,10 +5605,6 @@ packages:
   bowser@2.11.0:
     resolution: {integrity: sha512-AlcaJBi/pqqJBIQ8U9Mcpc9i8Aqxn88Skv5d+xBX006BY5u8N3mGLHa5Lgppa7L/HfwgwLgZ6NYs+Ag6uUmJRA==}
 
-  bplist-parser@0.2.0:
-    resolution: {integrity: sha512-z0M+byMThzQmD9NILRniCUXYsYpjwnlO8N5uCFaCqIOpqRsJCrQL9NK3JsD67CN5a08nF5oIL2bD6loTdHOuKw==}
-    engines: {node: '>= 5.10.0'}
-
   brace-expansion@1.1.11:
     resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==}
 
@@ -5539,15 +5615,16 @@ packages:
     resolution: {integrity: sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==}
     engines: {node: '>=8'}
 
+  braces@3.0.3:
+    resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
+    engines: {node: '>=8'}
+
   broadcast-channel@7.0.0:
     resolution: {integrity: sha512-a2tW0Ia1pajcPBOGUF2jXlDnvE9d5/dg6BG9h60OmRUcZVr/veUrU8vEQFwwQIhwG3KVzYwSk3v2nRRGFgQDXQ==}
 
   browser-assert@1.2.1:
     resolution: {integrity: sha512-nfulgvOR6S4gt9UKCeGJOuSGBPGiFT6oQ/2UBnvTY/5aQ1PnksW72fhZkM30DzoRRv2WpwZf1vHHEr3mtuXIWQ==}
 
-  browserify-zlib@0.1.4:
-    resolution: {integrity: sha512-19OEpq7vWgsH6WkvkBJQDFvJS1uPcbFOQ4v9CU839dO+ZZXUZO6XpE6hNCqvlIIj+4fZvRiJ6DsAQ382GwiyTQ==}
-
   browserslist@4.22.2:
     resolution: {integrity: sha512-0UgcrvQmBDvZHFGdYUehrCNIazki7/lUP3kkoi/r3YB2amZbFM9J43ZRkJTXBUZK4gmx56+Sqk9+Vs9mwZx9+A==}
     engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
@@ -5587,8 +5664,12 @@ packages:
     resolution: {integrity: sha512-kukuqc39WOHtdxtw4UScxF/WVnMFVSQVKhtx3AjZJzhd0RGZZldcrfSEbVsWWe6KNH253574cq5F+wpv0G9pJw==}
     engines: {node: '>=6.14.2'}
 
-  bullmq@5.7.8:
-    resolution: {integrity: sha512-F/Haeu6AVHkFrfeaU/kLOjhfrH6x3CaKAZlQQ+76fa8l3kfI9oaUHeFMW+1mYVz0NtYPF7PNTWFq4ylAHYcCgA==}
+  bufferutil@4.0.8:
+    resolution: {integrity: sha512-4T53u4PdgsXqKaIctwF8ifXlRTTmEPJ8iEPWFdGZvcf7sbwYo6FKFEX9eNNAnzFZ7EzJAQ3CJeOtCRA4rDp7Pw==}
+    engines: {node: '>=6.14.2'}
+
+  bullmq@5.15.0:
+    resolution: {integrity: sha512-h53shVjx8s6wxYGtUfzAfENpSP7N5T0D4PMTvbZncozLjb8yUKhopfpa7PmcpQfq7SSO9dm/OZ9XQuGOCSGNug==}
 
   buraha@0.0.1:
     resolution: {integrity: sha512-G563A0mTbzknm2jDaNxfZuNKIdeArs8T+XQN6t+KbmgnOoevXSXhKDkyf8Md/36Jrx99ikwbCag37VGe3myExQ==}
@@ -5597,10 +5678,6 @@ packages:
     resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==}
     engines: {node: '>=10.16.0'}
 
-  bytes@3.0.0:
-    resolution: {integrity: sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==}
-    engines: {node: '>= 0.8'}
-
   bytes@3.1.2:
     resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==}
     engines: {node: '>= 0.8'}
@@ -5625,6 +5702,10 @@ packages:
     resolution: {integrity: sha512-zkDT5WAF4hSSoUgyfg5tFIxz8XQK+25W/TLVojJTMKBaxevLBBtLxgqguAuVQB8PVW79FVjHcU+GJ9tVbDZ9mQ==}
     engines: {node: '>=14.16'}
 
+  cacheable-request@12.0.1:
+    resolution: {integrity: sha512-Yo9wGIQUaAfIbk+qY0X4cDQgCosecfBe3V9NSyeY4qPC2SAkbCS4Xj79VP8WOzitpJUZKc/wsRCYF5ariDIwkg==}
+    engines: {node: '>=18'}
+
   cacheable-request@7.0.2:
     resolution: {integrity: sha512-pouW8/FmiPQbuGpkXQ9BAPv/Mo5xDGANgSNXzTzJ8DrKGuXOssM4wIQRjfanNRh3Yu5cfYPvcorqbhg2KIJtew==}
     engines: {node: '>=8'}
@@ -5636,6 +5717,10 @@ packages:
   call-bind@1.0.2:
     resolution: {integrity: sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==}
 
+  call-bind@1.0.7:
+    resolution: {integrity: sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==}
+    engines: {node: '>= 0.4'}
+
   call-me-maybe@1.0.2:
     resolution: {integrity: sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==}
 
@@ -5684,6 +5769,10 @@ packages:
     resolution: {integrity: sha512-0UXG04VuVbruMUYbJ6JctvH0YnC/4q3/AkT18q4NaITo91CUm0liMS9VqzT9vZhVQ/1eqPanMWjBM+Juhfb/9g==}
     engines: {node: '>=4'}
 
+  chai@5.1.1:
+    resolution: {integrity: sha512-pT1ZgP8rPNqUgieVaEY+ryQr6Q4HXNg8Ei9UnLUrjN4IA7dvQC5JB+/kxVcPNDHyBcc/26CXPkbNzq3qwrOEKA==}
+    engines: {node: '>=12'}
+
   chalk-template@1.1.0:
     resolution: {integrity: sha512-T2VJbcDuZQ0Tb2EWwSotMPJjgpy1/tGee1BTpUNsGZ/qgNjV2t7Mvu+d4600U564nbLesN1x2dPL+xii174Ekg==}
     engines: {node: '>=14.16'}
@@ -5708,14 +5797,20 @@ packages:
     resolution: {integrity: sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==}
     engines: {node: '>=10'}
 
+  character-entities-html4@2.1.0:
+    resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==}
+
+  character-entities-legacy@3.0.0:
+    resolution: {integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==}
+
   character-entities@2.0.2:
     resolution: {integrity: sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==}
 
   character-parser@2.2.0:
     resolution: {integrity: sha512-+UqJQjFEFaTAs3bNsF2j2kEN1baG/zghZbdqoYEDxGZtJo9LBzl1A+m0D4n3qKx8N2FNv8/Xp6yV9mQmBuptaw==}
 
-  chart.js@4.4.2:
-    resolution: {integrity: sha512-6GD7iKwFpP5kbSD4MeRRRlTnQvxfQREy36uEtm1hzHzcOqwWx0YEHuspuoNlslu+nciLIB7fjjsHkUv/FzFcOg==}
+  chart.js@4.4.4:
+    resolution: {integrity: sha512-emICKGBABnxhMjUjlYRR12PmOXhJ2eJjEHL2/dZlWjxRAZT1D8xplLFq5M0tMQK8ja+wBS/tuVEJB5C6r7VxJA==}
     engines: {pnpm: '>=8'}
 
   chartjs-adapter-date-fns@3.0.0:
@@ -5742,6 +5837,10 @@ packages:
   check-error@1.0.3:
     resolution: {integrity: sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==}
 
+  check-error@2.1.1:
+    resolution: {integrity: sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==}
+    engines: {node: '>= 16'}
+
   check-more-types@2.24.0:
     resolution: {integrity: sha512-Pj779qHxV2tuapviy1bSZNEL1maXr13bPYpsvSDB68HlYcYuhlDrmGd63i0JHMCLKzc7rUSNIrpdJlhVlNwrxA==}
     engines: {node: '>= 0.8.0'}
@@ -5749,6 +5848,10 @@ packages:
   cheerio-select@2.1.0:
     resolution: {integrity: sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==}
 
+  cheerio@1.0.0:
+    resolution: {integrity: sha512-quS9HgjQpdaXOvsZz82Oz7uxtXiy6UIsIQcpBj7HRw2M63Skasm9qlDocAM7jNuaxdhpPU7c4kJN+gA5MCu4ww==}
+    engines: {node: '>=18.17'}
+
   cheerio@1.0.0-rc.12:
     resolution: {integrity: sha512-VqR8m68vM46BNnuZ5NtnGBKIE/DfN0cRIzg9n40EIq9NOv90ayxLBXA8fXC5gquFRGJSTRqBq25Jt2ECLR431Q==}
     engines: {node: '>= 6'}
@@ -5764,8 +5867,8 @@ packages:
     resolution: {integrity: sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==}
     engines: {node: '>=10'}
 
-  chromatic@11.3.0:
-    resolution: {integrity: sha512-q1ZtJDJrjLGnz60ivpC16gmd7KFzcaA4eTb7gcytCqbaKqlHhCFr1xQmcUDsm14CK7JsqdkFU6S+JQdOd2ZNJg==}
+  chromatic@11.11.0:
+    resolution: {integrity: sha512-mwmYsNMsZlRLtlfFUEtac5zhoVRhc+O/lsuMdOpwkiDQiKX6WdSNIhic+dkLenfuzao2r18s50nphcOgFoatBg==}
     hasBin: true
     peerDependencies:
       '@chromatic-com/cypress': ^0.*.* || ^1.0.0
@@ -5800,10 +5903,6 @@ packages:
     engines: {node: '>=8.0.0', npm: '>=5.0.0'}
     hasBin: true
 
-  cli-spinners@2.7.0:
-    resolution: {integrity: sha512-qu3pN8Y3qHNgE2AFweciB1IfMnmZ/fsNTEE+NOFjmGB2F/7rLhnhzppvpCnN4FovtP26k8lHyy9ptEbNwWFLzw==}
-    engines: {node: '>=6'}
-
   cli-spinners@2.9.2:
     resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==}
     engines: {node: '>=6'}
@@ -5830,17 +5929,9 @@ packages:
     resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==}
     engines: {node: '>=12'}
 
-  clone-deep@4.0.1:
-    resolution: {integrity: sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==}
-    engines: {node: '>=6'}
-
   clone-response@1.0.3:
     resolution: {integrity: sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==}
 
-  clone@1.0.4:
-    resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==}
-    engines: {node: '>=0.8'}
-
   cluster-key-slot@1.1.2:
     resolution: {integrity: sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==}
     engines: {node: '>=0.10.0'}
@@ -5890,10 +5981,17 @@ packages:
     resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
     engines: {node: '>= 0.8'}
 
+  comma-separated-tokens@2.0.3:
+    resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==}
+
   commander@10.0.1:
     resolution: {integrity: sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==}
     engines: {node: '>=14'}
 
+  commander@12.1.0:
+    resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==}
+    engines: {node: '>=18'}
+
   commander@2.20.3:
     resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==}
 
@@ -5920,21 +6018,13 @@ packages:
   commondir@1.0.1:
     resolution: {integrity: sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==}
 
-  compare-versions@6.1.0:
-    resolution: {integrity: sha512-LNZQXhqUvqUTotpZ00qLSaify3b4VFD588aRr8MKFw4CMUr98ytzCW5wDH5qx/DEY5kCDXcbcRuCqL0szEf2tg==}
+  compare-versions@6.1.1:
+    resolution: {integrity: sha512-4hm4VPpIecmlg59CHXnRDnqGplJFrbLG4aFEl5vl6cK1u76ws3LLvX7ikFnTDl5vo39sjWD6AaDPYodJp/NNHg==}
 
   compress-commons@6.0.2:
     resolution: {integrity: sha512-6FqVXeETqWPoGcfzrXb37E50NP0LXT8kAMu5ooZayhWWdgEY4lBEEcbQNXtkuKQsGduxiIcI4gOTsxTmuq/bSg==}
     engines: {node: '>= 14'}
 
-  compressible@2.0.18:
-    resolution: {integrity: sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==}
-    engines: {node: '>= 0.6'}
-
-  compression@1.7.4:
-    resolution: {integrity: sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==}
-    engines: {node: '>= 0.8.0'}
-
   computeds@0.0.1:
     resolution: {integrity: sha512-7CEBgcMjVmitjYo5q8JTJVra6X5mQ20uTThdK+0kR7UEaDrAWEQcRiBtWJzga4eRpP6afNwwLsX2SET2JhVB1Q==}
 
@@ -5983,9 +6073,6 @@ packages:
     resolution: {integrity: sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==}
     engines: {node: '>= 0.6'}
 
-  core-js-compat@3.33.3:
-    resolution: {integrity: sha512-cNzGqFsh3Ot+529GIXacjTJ7kegdt5fPXxCBVS1G0iaZpuo/tBz399ymceLJveQhFFZ8qThHiP3fzuoQjKN2ow==}
-
   core-js@3.29.1:
     resolution: {integrity: sha512-+jwgnhg6cQxKYIIjGtAHq2nwUOolo9eoFZ4sHfUH09BLXBgxnH4gA0zEd+t+BO2cNB8idaBtZFcFTRjQJRJmAw==}
 
@@ -6017,8 +6104,8 @@ packages:
     resolution: {integrity: sha512-jbokKWGcyU4gl6jAfX97E1gDpY12DJ1cLJZmoDzaAln/shZ+S3KBFBuA2Q6WeUN4gJf/8klnV1EfvhA2lK5IRQ==}
     engines: {node: '>=12.0.0'}
 
-  cropperjs@2.0.0-beta.5:
-    resolution: {integrity: sha512-8RIynsyHV7KyCxbjV4fCQubGiM6sHMgYvRPKkzuUQSTYHK6shoUNvdvbBekwAwS8QRLsxEBcJ5lvl0W3dvkDQA==}
+  cropperjs@2.0.0-rc.2:
+    resolution: {integrity: sha512-BTuz+UeZphGOEnBCuQiNT4rk1uFfKJaKmTgoH9XU7Q8IMkLdodW7YPWINmXJXwWMt1nXiKze5qKADVbz9xtVFg==}
 
   cross-env@7.0.3:
     resolution: {integrity: sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==}
@@ -6038,10 +6125,6 @@ packages:
     resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==}
     engines: {node: '>= 8'}
 
-  crypto-random-string@2.0.0:
-    resolution: {integrity: sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==}
-    engines: {node: '>=8'}
-
   css-declaration-sorter@7.2.0:
     resolution: {integrity: sha512-h70rUM+3PNFuaBDTLe8wF/cdWu+dOZmb7pJt8Z2sedYbAcQVQV/tEchueg3GWxwqS0cxtbxmaHEdkNACqcvsow==}
     engines: {node: ^14 || ^16 || >=18}
@@ -6103,13 +6186,13 @@ packages:
   cwise-compiler@1.1.3:
     resolution: {integrity: sha512-WXlK/m+Di8DMMcCjcWr4i+XzcQra9eCdXIJrgh4TUgh0pIS/yJduLxS9JgefsHJ/YVLdgPtXm9r62W92MvanEQ==}
 
-  cypress@13.7.3:
-    resolution: {integrity: sha512-uoecY6FTCAuIEqLUYkTrxamDBjMHTYak/1O7jtgwboHiTnS1NaMOoR08KcTrbRZFCBvYOiS4tEkQRmsV+xcrag==}
+  cypress@13.14.2:
+    resolution: {integrity: sha512-lsiQrN17vHMB2fnvxIrKLAjOr9bPwsNbPZNrWf99s4u+DVmCY6U+w7O3GGG9FvP4EUVYaDu+guWeNLiUzBrqvA==}
     engines: {node: ^16.0.0 || ^18.0.0 || >=20.0.0}
     hasBin: true
 
-  cypress@13.8.1:
-    resolution: {integrity: sha512-Uk6ovhRbTg6FmXjeZW/TkbRM07KPtvM5gah1BIMp4Y2s+i/NMxgaLw0+PbYTOdw1+egE0FP3mWRiGcRkjjmhzA==}
+  cypress@13.15.0:
+    resolution: {integrity: sha512-53aO7PwOfi604qzOkCSzNlWquCynLlKE/rmmpSPcziRH6LNfaDUAklQT6WJIsD8ywxlIy+uVZsnTMCCQVd2kTw==}
     engines: {node: ^16.0.0 || ^18.0.0 || >=20.0.0}
     hasBin: true
 
@@ -6128,6 +6211,18 @@ packages:
     resolution: {integrity: sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==}
     engines: {node: '>=18'}
 
+  data-view-buffer@1.0.1:
+    resolution: {integrity: sha512-0lht7OugA5x3iJLOWFhWK/5ehONdprk0ISXqVFn/NFrDu+cuc8iADFrGQz5BnRK7LLU3JmkbXSxaqX+/mXYtUA==}
+    engines: {node: '>= 0.4'}
+
+  data-view-byte-length@1.0.1:
+    resolution: {integrity: sha512-4J7wRJD3ABAzr8wP+OcIcqq2dlUKp4DVflx++hs5h5ZKydWMI6/D/fAot+yh6g2tHh8fLFTvNOaVN357NvSrOQ==}
+    engines: {node: '>= 0.4'}
+
+  data-view-byte-offset@1.0.0:
+    resolution: {integrity: sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA==}
+    engines: {node: '>= 0.4'}
+
   date-fns@2.30.0:
     resolution: {integrity: sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==}
     engines: {node: '>=0.11'}
@@ -6163,6 +6258,24 @@ packages:
       supports-color:
         optional: true
 
+  debug@4.3.5:
+    resolution: {integrity: sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==}
+    engines: {node: '>=6.0'}
+    peerDependencies:
+      supports-color: '*'
+    peerDependenciesMeta:
+      supports-color:
+        optional: true
+
+  debug@4.3.7:
+    resolution: {integrity: sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==}
+    engines: {node: '>=6.0'}
+    peerDependencies:
+      supports-color: '*'
+    peerDependenciesMeta:
+      supports-color:
+        optional: true
+
   decamelize-keys@1.1.1:
     resolution: {integrity: sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg==}
     engines: {node: '>=0.10.0'}
@@ -6204,6 +6317,10 @@ packages:
     resolution: {integrity: sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==}
     engines: {node: '>=6'}
 
+  deep-eql@5.0.2:
+    resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==}
+    engines: {node: '>=6'}
+
   deep-equal@2.2.0:
     resolution: {integrity: sha512-RdpzE0Hv4lhowpIUKKMJfeH6C1pXdtT1/it80ubgWqwI3qpuxUBpC1S4hnHg+zjnuOoDkzUtUCEEkG+XG5l3Mw==}
 
@@ -6214,17 +6331,14 @@ packages:
     resolution: {integrity: sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==}
     engines: {node: '>=0.10.0'}
 
-  default-browser-id@3.0.0:
-    resolution: {integrity: sha512-OZ1y3y0SqSICtE8DE4S8YOE9UZOJ8wO16fKWVP5J1Qz42kV9jcnMVFrEE/noXb/ss3Q4pZIH79kxofzyNNtUNA==}
-    engines: {node: '>=12'}
-
-  defaults@1.0.4:
-    resolution: {integrity: sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==}
-
   defer-to-connect@2.0.1:
     resolution: {integrity: sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==}
     engines: {node: '>=10'}
 
+  define-data-property@1.1.4:
+    resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==}
+    engines: {node: '>= 0.4'}
+
   define-lazy-prop@2.0.0:
     resolution: {integrity: sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==}
     engines: {node: '>=8'}
@@ -6233,12 +6347,9 @@ packages:
     resolution: {integrity: sha512-xvqAVKGfT1+UAvPwKTVw/njhdQ8ZhXK4lI0bCIuCMrp2up9nPnaDftrLtmpTazqd1o+UY4zgzU+avtMbDP+ldA==}
     engines: {node: '>= 0.4'}
 
-  defu@6.1.4:
-    resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==}
-
-  del@6.1.1:
-    resolution: {integrity: sha512-ua8BhapfP0JUJKC/zV9yHHDW/rDoDxP4Zhn3AkA6/xT6gY7jYXJiaeyBZznYVujhZZET+UgcbZiQ7sN3WqcImg==}
-    engines: {node: '>=10'}
+  define-properties@1.2.1:
+    resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==}
+    engines: {node: '>= 0.4'}
 
   delayed-stream@1.0.0:
     resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
@@ -6263,14 +6374,6 @@ packages:
     resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==}
     engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16}
 
-  detect-indent@6.1.0:
-    resolution: {integrity: sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==}
-    engines: {node: '>=8'}
-
-  detect-libc@2.0.2:
-    resolution: {integrity: sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw==}
-    engines: {node: '>=8'}
-
   detect-libc@2.0.3:
     resolution: {integrity: sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==}
     engines: {node: '>=8'}
@@ -6279,14 +6382,6 @@ packages:
     resolution: {integrity: sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==}
     engines: {node: '>=8'}
 
-  detect-package-manager@2.0.1:
-    resolution: {integrity: sha512-j/lJHyoLlWi6G1LDdLgvUtz60Zo5GEj+sVYtTVXnYLDPuzgC3llMxonXym9zIwhhUII8vjdw0LXxavpLqTbl1A==}
-    engines: {node: '>=12'}
-
-  detect-port@1.5.1:
-    resolution: {integrity: sha512-aBzdj76lueB6uUst5iAs7+0H/oOjqI5D16XUWxlWMIMROhcM0rfsNVk93zTngq1dDNpoXRr++Sus7ETAExppAQ==}
-    hasBin: true
-
   devlop@1.1.0:
     resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==}
 
@@ -6301,6 +6396,10 @@ packages:
     resolution: {integrity: sha512-D+mk+qE8VC/PAUrlAU34N+VfXev0ghe5ywmpqrawphmVZc1bEfn56uo9qpyGp1p4xpzOHkSW4ztBd6L7Xx4ACw==}
     engines: {node: '>=0.3.1'}
 
+  diff@5.2.0:
+    resolution: {integrity: sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==}
+    engines: {node: '>=0.3.1'}
+
   dijkstrajs@1.0.2:
     resolution: {integrity: sha512-QV6PMaHTCNmKSeP6QoXhVTw9snc9VD8MulTT0Bd99Pacp4SS1cjcrYPgBPmibqKVtMJJfqC6XvOXgPMEEPH/fg==}
 
@@ -6328,22 +6427,35 @@ packages:
   dom-accessibility-api@0.6.3:
     resolution: {integrity: sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==}
 
+  dom-serializer@1.4.1:
+    resolution: {integrity: sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==}
+
   dom-serializer@2.0.0:
     resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==}
 
   domelementtype@2.3.0:
     resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==}
 
+  domhandler@3.3.0:
+    resolution: {integrity: sha512-J1C5rIANUbuYK+FuFL98650rihynUOEzRLxW+90bKZRWB6A1X1Tf82GxR1qAWLyfNPRvjqfip3Q5tdYlmAa9lA==}
+    engines: {node: '>= 4'}
+
+  domhandler@4.3.1:
+    resolution: {integrity: sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==}
+    engines: {node: '>= 4'}
+
   domhandler@5.0.3:
     resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==}
     engines: {node: '>= 4'}
 
+  domutils@2.8.0:
+    resolution: {integrity: sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==}
+
   domutils@3.0.1:
     resolution: {integrity: sha512-z08c1l761iKhDFtfXO04C7kTdPBLi41zwOZl00WS8b5eiaebNpY00HKbztwBq+e3vyqWNwWF3mP9YLUeqIrF+Q==}
 
-  dotenv-expand@10.0.0:
-    resolution: {integrity: sha512-GopVGCpVS1UKH75VKHGuQFqS1Gusej0z4FyQkPdwjil2gNIv+LNsqBlboOzpJFZKVT95GkCyWJbBSdFEFUWI2A==}
-    engines: {node: '>=12'}
+  domutils@3.1.0:
+    resolution: {integrity: sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==}
 
   dotenv@16.0.3:
     resolution: {integrity: sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ==}
@@ -6352,9 +6464,6 @@ packages:
   duplexer@0.1.2:
     resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==}
 
-  duplexify@3.7.1:
-    resolution: {integrity: sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==}
-
   eastasianwidth@0.2.0:
     resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
 
@@ -6372,8 +6481,8 @@ packages:
   ee-first@1.1.1:
     resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==}
 
-  ejs@3.1.9:
-    resolution: {integrity: sha512-rC+QVNMJWv+MtPgkt0y+0rVEIdbtxVADApW9JXrUVlzHetgcyczP/E7DJmWJ4fJCZF2cPcBk0laWO9ZHMG3DmQ==}
+  ejs@3.1.10:
+    resolution: {integrity: sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==}
     engines: {node: '>=0.10.0'}
     hasBin: true
 
@@ -6393,13 +6502,17 @@ packages:
   emoji-regex@9.2.2:
     resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==}
 
-  encode-utf8@1.0.3:
-    resolution: {integrity: sha512-ucAnuBEhUK4boH2HjVYG5Q2mQyPorvv0u/ocS+zhdw0S8AlHYY+GOFhP1Gio5z4icpP2ivFSvhtFjQi8+T9ppw==}
-
   encodeurl@1.0.2:
     resolution: {integrity: sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==}
     engines: {node: '>= 0.8'}
 
+  encodeurl@2.0.0:
+    resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==}
+    engines: {node: '>= 0.8'}
+
+  encoding-sniffer@0.2.0:
+    resolution: {integrity: sha512-ju7Wq1kg04I3HtiYIOrUrdfdDvkyO9s5XM8QAj/bN61Yo/Vb4vgJxy5vi4Yxk01gWHbrofpPtpxM8bKger9jhg==}
+
   encoding@0.1.13:
     resolution: {integrity: sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==}
 
@@ -6417,15 +6530,14 @@ packages:
     resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==}
     engines: {node: '>=0.12'}
 
+  entities@5.0.0:
+    resolution: {integrity: sha512-BeJFvFRJddxobhvEdm5GqHzRV/X+ACeuw0/BuuxsCh1EUZcAIz8+kYmBp/LrQuloy6K1f3a0M7+IhmZ7QnkISA==}
+    engines: {node: '>=0.12'}
+
   env-paths@2.2.1:
     resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==}
     engines: {node: '>=6'}
 
-  envinfo@7.8.1:
-    resolution: {integrity: sha512-/o+BXHmB7ocbHEAs6F2EnG0ogybVVUdkRunTT2glZU9XAaGmhqskrvKwqXuDfNjEO0LZKWdejEEpnq8aM0tOaw==}
-    engines: {node: '>=4'}
-    hasBin: true
-
   err-code@2.0.3:
     resolution: {integrity: sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==}
 
@@ -6436,19 +6548,42 @@ packages:
     resolution: {integrity: sha512-ioRRcXMO6OFyRpyzV3kE1IIBd4WG5/kltnzdxSCqoP8CMGs/Li+M1uF5o7lOkZVFjDs+NLesthnF66Pg/0q0Lw==}
     engines: {node: '>= 0.4'}
 
+  es-abstract@1.23.3:
+    resolution: {integrity: sha512-e+HfNH61Bj1X9/jLc5v1owaLYuHdeHHSQlkhCBiTK8rBvKaULl/beGMxwrMXjpYrv4pz22BlY570vVePA2ho4A==}
+    engines: {node: '>= 0.4'}
+
+  es-define-property@1.0.0:
+    resolution: {integrity: sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==}
+    engines: {node: '>= 0.4'}
+
+  es-errors@1.3.0:
+    resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==}
+    engines: {node: '>= 0.4'}
+
   es-get-iterator@1.1.3:
     resolution: {integrity: sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==}
 
-  es-module-lexer@0.9.3:
-    resolution: {integrity: sha512-1HQ2M2sPtxwnvOvT1ZClHyQDiggdNjURWpY2we6aMKCQiUVxTmVs2UYPLIrD84sS+kMdUwfBSylbJPwNnBrnHQ==}
+  es-module-lexer@1.5.4:
+    resolution: {integrity: sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw==}
+
+  es-object-atoms@1.0.0:
+    resolution: {integrity: sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==}
+    engines: {node: '>= 0.4'}
 
   es-set-tostringtag@2.0.1:
     resolution: {integrity: sha512-g3OMbtlwY3QewlqAiMLI47KywjWZoEytKr8pf6iTC8uJq5bIAH52Z9pnQ8pVL6whrCto53JZDuUIsifGeLorTg==}
     engines: {node: '>= 0.4'}
 
+  es-set-tostringtag@2.0.3:
+    resolution: {integrity: sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ==}
+    engines: {node: '>= 0.4'}
+
   es-shim-unscopables@1.0.0:
     resolution: {integrity: sha512-Jm6GPcCdC30eMLbZ2x8z2WuRwAws3zTBBKuusffYVUrNj/GVSUAZ+xKMaUpfNDR5IbyNA5LJbaecoUVbmUcB1w==}
 
+  es-shim-unscopables@1.0.2:
+    resolution: {integrity: sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw==}
+
   es-to-primitive@1.2.1:
     resolution: {integrity: sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==}
     engines: {node: '>= 0.4'}
@@ -6459,9 +6594,6 @@ packages:
   es6-promisify@5.0.0:
     resolution: {integrity: sha512-C+d6UdsYDk0lMebHNR4S2NybQMMngAOnOwYBQjTOiv0MkoJMP0Myw2mgpDLBcpfCmRLxyFqYhS/CfOENq4SJhQ==}
 
-  esbuild-plugin-alias@0.2.1:
-    resolution: {integrity: sha512-jyfL/pwPqaFXyKnj8lP8iLk6Z0m099uXR45aSN8Av1XD4vhvQutxxPzgA2bTcAwQpa1zCXDcWOlhFgyP3GKqhQ==}
-
   esbuild-register@3.5.0:
     resolution: {integrity: sha512-+4G/XmakeBAsvJuDugJvtyF1x+XJT4FMocynNpxrvEBViirpfUn2PgNpCHedfWhF4WokNsO/OvMKrmJOIJsI5A==}
     peerDependencies:
@@ -6477,15 +6609,29 @@ packages:
     engines: {node: '>=12'}
     hasBin: true
 
-  esbuild@0.20.2:
-    resolution: {integrity: sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==}
+  esbuild@0.21.5:
+    resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==}
     engines: {node: '>=12'}
     hasBin: true
 
+  esbuild@0.23.0:
+    resolution: {integrity: sha512-1lvV17H2bMYda/WaFb2jLPeHU3zml2k4/yagNMG8Q/YtfMjCwEUZa2eXXMgZTVSL5q1n4H7sQ0X6CdJDqqeCFA==}
+    engines: {node: '>=18'}
+    hasBin: true
+
+  esbuild@0.23.1:
+    resolution: {integrity: sha512-VVNz/9Sa0bs5SELtn3f7qhJCDPCF5oMEl5cO9/SSinpE9hbPVvxbd572HH5AKiP7WD8INO53GgfDDhRjkylHEg==}
+    engines: {node: '>=18'}
+    hasBin: true
+
   escalade@3.1.1:
     resolution: {integrity: sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==}
     engines: {node: '>=6'}
 
+  escape-goat@3.0.0:
+    resolution: {integrity: sha512-w3PwNZJwRxlp47QGzhuEBldEqVHHhh8/tIPcl6ecf2Bou99cdAt0knihBV0Ecc7CGxYduXVBDheH1K2oADRlvw==}
+    engines: {node: '>=10'}
+
   escape-html@1.0.3:
     resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==}
 
@@ -6520,8 +6666,8 @@ packages:
   eslint-import-resolver-node@0.3.9:
     resolution: {integrity: sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==}
 
-  eslint-module-utils@2.8.0:
-    resolution: {integrity: sha512-aWajIYfsqCKRDgUfjEXNN/JlrzauMuSEy5sbd7WXbtW3EH6A6MpwEh42c7qD+MqQo9QMJ6fWLAeIJynx0g6OAw==}
+  eslint-module-utils@2.11.0:
+    resolution: {integrity: sha512-gbBE5Hitek/oG6MUVj6sFuzEjA/ClzNflVrLovHi/JgLdC7fiN5gLAY1WIPW1a0V5I999MnsrvVrCOGmmVqDBQ==}
     engines: {node: '>=4'}
     peerDependencies:
       '@typescript-eslint/parser': '*'
@@ -6541,8 +6687,29 @@ packages:
       eslint-import-resolver-webpack:
         optional: true
 
-  eslint-plugin-import@2.29.1:
-    resolution: {integrity: sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw==}
+  eslint-module-utils@2.12.0:
+    resolution: {integrity: sha512-wALZ0HFoytlyh/1+4wuZ9FJCD/leWHQzzrxJ8+rebyReSLk7LApMyd3WJaLVoN+D5+WIdJyDK1c6JnE65V4Zyg==}
+    engines: {node: '>=4'}
+    peerDependencies:
+      '@typescript-eslint/parser': '*'
+      eslint: '*'
+      eslint-import-resolver-node: '*'
+      eslint-import-resolver-typescript: '*'
+      eslint-import-resolver-webpack: '*'
+    peerDependenciesMeta:
+      '@typescript-eslint/parser':
+        optional: true
+      eslint:
+        optional: true
+      eslint-import-resolver-node:
+        optional: true
+      eslint-import-resolver-typescript:
+        optional: true
+      eslint-import-resolver-webpack:
+        optional: true
+
+  eslint-plugin-import@2.30.0:
+    resolution: {integrity: sha512-/mHNE9jINJfiD2EKkg1BKyPyUk4zdnT54YgbOgfjSakWT5oyX/qQLVNTkehyfpcMxZXMy1zyonZ2v7hZTX43Yw==}
     engines: {node: '>=4'}
     peerDependencies:
       '@typescript-eslint/parser': '*'
@@ -6551,8 +6718,24 @@ packages:
       '@typescript-eslint/parser':
         optional: true
 
-  eslint-plugin-vue@9.25.0:
-    resolution: {integrity: sha512-tDWlx14bVe6Bs+Nnh3IGrD+hb11kf2nukfm6jLsmJIhmiRQ1SUaksvwY9U5MvPB0pcrg0QK0xapQkfITs3RKOA==}
+  eslint-plugin-import@2.31.0:
+    resolution: {integrity: sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A==}
+    engines: {node: '>=4'}
+    peerDependencies:
+      '@typescript-eslint/parser': '*'
+      eslint: ^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9
+    peerDependenciesMeta:
+      '@typescript-eslint/parser':
+        optional: true
+
+  eslint-plugin-vue@9.27.0:
+    resolution: {integrity: sha512-5Dw3yxEyuBSXTzT5/Ge1X5kIkRTQ3nvBn/VwPwInNiZBSJOO/timWMUaflONnFBzU6NhB68lxnCda7ULV5N7LA==}
+    engines: {node: ^14.17.0 || >=16.0.0}
+    peerDependencies:
+      eslint: ^6.2.0 || ^7.0.0 || ^8.0.0 || ^9.0.0
+
+  eslint-plugin-vue@9.28.0:
+    resolution: {integrity: sha512-ShrihdjIhOTxs+MfWun6oJWuk+g/LAhN+CiuOl/jjkG3l0F2AuK5NMTaWqyvBgkFtpYmyks6P4603mLmhNJW8g==}
     engines: {node: ^14.17.0 || >=16.0.0}
     peerDependencies:
       eslint: ^6.2.0 || ^7.0.0 || ^8.0.0 || ^9.0.0
@@ -6564,19 +6747,36 @@ packages:
     resolution: {integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==}
     engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
 
+  eslint-scope@8.0.2:
+    resolution: {integrity: sha512-6E4xmrTw5wtxnLA5wYL3WDfhZ/1bUBGOXV0zQvVRDOtrR8D0p6W7fs3JweNYhwRYeGvd/1CKX2se0/2s7Q/nJA==}
+    engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
   eslint-visitor-keys@3.4.3:
     resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==}
     engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
 
-  eslint@8.53.0:
-    resolution: {integrity: sha512-N4VuiPjXDUa4xVeV/GC/RV3hQW9Nw+Y463lkWaKKXKYMvmRiRDAtfpuPFLN+E1/6ZhyR8J2ig+eVREnYgUsiag==}
-    engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
+  eslint-visitor-keys@4.0.0:
+    resolution: {integrity: sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==}
+    engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+  eslint@9.11.0:
+    resolution: {integrity: sha512-yVS6XODx+tMFMDFcG4+Hlh+qG7RM6cCJXtQhCKLSsr3XkLvWggHjCqjfh0XsPPnt1c56oaT6PMgW9XWQQjdHXA==}
+    engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+    hasBin: true
+    peerDependencies:
+      jiti: '*'
+    peerDependenciesMeta:
+      jiti:
+        optional: true
+
+  eslint@9.8.0:
+    resolution: {integrity: sha512-K8qnZ/QJzT2dLKdZJVX6W4XOwBzutMYmt0lqUS+JdXgd+HTYFlonFgkJ8s44d/zMPPCnOOk0kMWCApCPhiOy9A==}
+    engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
     hasBin: true
 
-  eslint@8.57.0:
-    resolution: {integrity: sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==}
-    engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
-    hasBin: true
+  espree@10.1.0:
+    resolution: {integrity: sha512-M1M6CpiE6ffoigIOWYO9UDP8TMUw9kqb21tf+08IgDYjCsOvCuDt4jQcZmoYxx+w7zlKw9/N0KXfto+I8/FrXA==}
+    engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
 
   espree@9.6.1:
     resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==}
@@ -6587,8 +6787,8 @@ packages:
     engines: {node: '>=4'}
     hasBin: true
 
-  esquery@1.4.2:
-    resolution: {integrity: sha512-JVSoLdTlTDkmjFmab7H/9SL9qGSyjElT3myyKp7krqjVFQCDLmj1QFaCLRFBszBKI0XVZaiiXvuPIX3ZwHe1Ng==}
+  esquery@1.6.0:
+    resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==}
     engines: {node: '>=0.10'}
 
   esrecurse@4.3.0:
@@ -6653,6 +6853,10 @@ packages:
     resolution: {integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==}
     engines: {node: '>=16.17'}
 
+  execa@9.4.0:
+    resolution: {integrity: sha512-yKHlle2YGxZE842MERVIplWwNH5VYmqqcPFgtnlU//K8gxuFFXu0pwd/CrfXTumFpeEiufsP7+opT/bPJa1yVw==}
+    engines: {node: ^18.19.0 || >=20.5.0}
+
   executable@4.1.1:
     resolution: {integrity: sha512-8iA79xD3uAch729dUG8xaaBBFGaEa0wdD2VkYLFHwlqosEj/jT66AzcreRDSgV7ehnNLBW2WR5jIXwGKjVdTLg==}
     engines: {node: '>=4'}
@@ -6668,12 +6872,8 @@ packages:
   exponential-backoff@3.1.1:
     resolution: {integrity: sha512-dX7e/LHVJ6W3DE1MHWi9S1EYzDESENfLrYohG2G++ovZrYOkm4Knwa0mc1cn84xJOR4KEU0WSchhLbd0UklbHw==}
 
-  express@4.18.2:
-    resolution: {integrity: sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==}
-    engines: {node: '>= 0.10.0'}
-
-  express@4.19.2:
-    resolution: {integrity: sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==}
+  express@4.21.0:
+    resolution: {integrity: sha512-VqcNGcj/Id5ZT1LZ/cfihi3ttTn+NJmkli2eZADigjq29qTlWi/hAQ43t/VLPq8+UX06FCEx3ByOYet6ZFblng==}
     engines: {node: '>= 0.10.0'}
 
   ext-list@2.2.2:
@@ -6696,8 +6896,8 @@ packages:
     resolution: {integrity: sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==}
     engines: {'0': node >=0.6.0}
 
-  fast-content-type-parse@1.1.0:
-    resolution: {integrity: sha512-fBHHqSTFLVnR61C+gltJuE5GkVQMV0S2nqUO8TJ+5Z3qAKG8vAx4FKai1s5jq/inV1+sREynIWSuQ6HgoSXpDQ==}
+  fast-content-type-parse@2.0.0:
+    resolution: {integrity: sha512-fCqg/6Sps8tqk8p+kqyKqYfOF0VjPNYrqpLiqNl0RBKmD80B080AJWVV6EkSkscjToNExcXg1+Mfzftrx6+iSA==}
 
   fast-decode-uri-component@1.0.1:
     resolution: {integrity: sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==}
@@ -6715,8 +6915,8 @@ packages:
   fast-json-stable-stringify@2.1.0:
     resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==}
 
-  fast-json-stringify@5.8.0:
-    resolution: {integrity: sha512-VVwK8CFMSALIvt14U8AvrSzQAwN/0vaVRiFFUVlpnXSnDGrSkOAO5MtzyN8oQNjLd5AqTW5OZRgyjoNuAuR3jQ==}
+  fast-json-stringify@6.0.0:
+    resolution: {integrity: sha512-FGMKZwniMTgZh7zQp9b6XnBVxUmKVahQLQeRQHqwYmPDqDhcEKZ3BaQsxelFFI5PY7nN71OEeiL47/zUWcYe1A==}
 
   fast-levenshtein@2.0.6:
     resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==}
@@ -6731,25 +6931,32 @@ packages:
   fast-safe-stringify@2.1.1:
     resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==}
 
-  fast-uri@2.2.0:
-    resolution: {integrity: sha512-cIusKBIt/R/oI6z/1nyfe2FvGKVTohVRfvkOhvx0nCEW+xf5NoCXjAHcWp93uOUBchzYcsvPlrapAdX1uW+YGg==}
+  fast-uri@2.4.0:
+    resolution: {integrity: sha512-ypuAmmMKInk5q7XcepxlnUWDLWv4GFtaJqAzWKqn62IpQ3pejtr5dTVbt3vwqVaMKmkNR55sTT+CqUKIaT21BA==}
+
+  fast-uri@3.0.1:
+    resolution: {integrity: sha512-MWipKbbYiYI0UC7cl8m/i/IWTqfC8YXsqjzybjddLsFjStroQzsHXkc73JutMvBiXmOvapk+axIl79ig5t55Bw==}
 
   fast-xml-parser@4.2.5:
     resolution: {integrity: sha512-B9/wizE4WngqQftFPmdaMYlXoJlJOYxGQOanC77fq9k8+Z0v5dDSVh+3glErdIROP//s/jgb7ZuxKfB8nVyo0g==}
     hasBin: true
 
-  fastify-plugin@4.5.0:
-    resolution: {integrity: sha512-79ak0JxddO0utAXAQ5ccKhvs6vX2MGyHHMMsmZkBANrq3hXc1CHzvNPHOcvTsVMEPl5I+NT+RO4YKMGehOfSIg==}
+  fast-xml-parser@4.5.0:
+    resolution: {integrity: sha512-/PlTQCI96+fZMAOLMZK4CWG1ItCbfZ/0jx7UIJFChPNrx7tcEgerUgWbeieCM9MfHInUDyK8DWYZ+YrywDJuTg==}
+    hasBin: true
 
-  fastify-raw-body@4.3.0:
-    resolution: {integrity: sha512-F4o8ZIMVx4YoxGfwrZys6wyjl40gF3Yv6AWWRy62ozFAyZBSS831/uyyCAqKYw3tR73g180ryG98yih6To1PUQ==}
+  fastify-plugin@4.5.1:
+    resolution: {integrity: sha512-stRHYGeuqpEZTL1Ef0Ovr2ltazUT9g844X5z/zEBFLG8RYlpDiOCIG+ATvYEp+/zmc7sN29mcIMp8gvYplYPIQ==}
+
+  fastify-plugin@5.0.0:
+    resolution: {integrity: sha512-0725fmH/yYi8ugsjszLci+lLnGBK6cG+WSxM7edY2OXJEU7gr2JiGBoieL2h9mhTych1vFsEfXsAsGGDJ/Rd5w==}
+
+  fastify-raw-body@5.0.0:
+    resolution: {integrity: sha512-2qfoaQ3BQDhZ1gtbkKZd6n0kKxJISJGM6u/skD9ljdWItAscjXrtZ1lnjr7PavmXX9j4EyCPmBDiIsLn07d5vA==}
     engines: {node: '>= 10'}
 
-  fastify@4.26.2:
-    resolution: {integrity: sha512-90pjTuPGrfVKtdpLeLzND5nyC4woXZN5VadiNQCicj/iJU4viNHKhsAnb7jmv1vu2IzkLXyBiCzdWuzeXgQ5Ug==}
-
-  fastq@1.15.0:
-    resolution: {integrity: sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==}
+  fastify@5.0.0:
+    resolution: {integrity: sha512-Qe4dU+zGOzg7vXjw4EvcuyIbNnMwTmcuOhlOrOJsgwzvjEZmsM/IeHulgJk+r46STjdJS/ZJbxO8N70ODXDMEQ==}
 
   fastq@1.17.1:
     resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==}
@@ -6768,26 +6975,24 @@ packages:
     resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==}
     engines: {node: ^12.20 || >= 14.13}
 
-  fetch-retry@5.0.4:
-    resolution: {integrity: sha512-LXcdgpdcVedccGg0AZqg+S8lX/FCdwXD92WNZ5k5qsb0irRhSFsBOpcJt7oevyqT2/C2nEE0zSFNdBEpj3YOSw==}
-
   figures@3.2.0:
     resolution: {integrity: sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==}
     engines: {node: '>=8'}
 
-  file-entry-cache@6.0.1:
-    resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==}
-    engines: {node: ^10.12.0 || >=12.0.0}
+  figures@6.1.0:
+    resolution: {integrity: sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==}
+    engines: {node: '>=18'}
 
-  file-system-cache@2.3.0:
-    resolution: {integrity: sha512-l4DMNdsIPsVnKrgEXbJwDJsA5mB8rGwHYERMgqQx/xAUtChPJMre1bXBzDEqqVbWv9AIbFezXMxeEkZDSrXUOQ==}
+  file-entry-cache@8.0.0:
+    resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==}
+    engines: {node: '>=16.0.0'}
 
   file-type@17.1.6:
     resolution: {integrity: sha512-hlDw5Ev+9e883s0pwUsuuYNu4tD7GgpUnOvykjv1Gya0ZIjuKumthDRua90VUn6/nlRKAjcxLUnHNTIUWwWIiw==}
     engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
 
-  file-type@19.0.0:
-    resolution: {integrity: sha512-s7cxa7/leUWLiXO78DVVfBVse+milos9FitauDLG1pI7lNaJ2+5lzPnr2N24ym+84HVwJL6hVuGfgVE+ALvU8Q==}
+  file-type@19.5.0:
+    resolution: {integrity: sha512-dMuq6WWnP6BpQY0zYJNpTtQWgeCImSMG0BTIzUBXvxbwc1HWP/E7AE4UWU9XSCOPGJuOHda0HpDnwM2FW+d90A==}
     engines: {node: '>=18'}
 
   filelist@1.0.4:
@@ -6805,29 +7010,25 @@ packages:
     resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==}
     engines: {node: '>=8'}
 
-  finalhandler@1.2.0:
-    resolution: {integrity: sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==}
-    engines: {node: '>= 0.8'}
+  fill-range@7.1.1:
+    resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==}
+    engines: {node: '>=8'}
 
-  find-cache-dir@2.1.0:
-    resolution: {integrity: sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ==}
-    engines: {node: '>=6'}
+  finalhandler@1.3.1:
+    resolution: {integrity: sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==}
+    engines: {node: '>= 0.8'}
 
   find-cache-dir@3.3.2:
     resolution: {integrity: sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==}
     engines: {node: '>=8'}
 
-  find-my-way@8.2.0:
-    resolution: {integrity: sha512-HdWXgFYc6b1BJcOBDBwjqWuHJj1WYiqrxSh25qtU4DabpMFdj/gSunNBQb83t+8Zt67D7CXEzJWTkxaShMTMOA==}
+  find-my-way@9.0.1:
+    resolution: {integrity: sha512-/5NN/R0pFWuff16TMajeKt2JyiW+/OE8nOO8vo1DwZTxLaIURb7lcBYPIgRPh61yCNh9l8voeKwcrkUzmB00vw==}
     engines: {node: '>=14'}
 
   find-package-json@1.2.0:
     resolution: {integrity: sha512-+SOGcLGYDJHtyqHd87ysBhmaeQ95oWspDKnMXBrnQ9Eq4OkLNqejgoaD8xVWu6GPa0B6roa6KinCMEMcVeqONw==}
 
-  find-up@3.0.0:
-    resolution: {integrity: sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==}
-    engines: {node: '>=6'}
-
   find-up@4.1.0:
     resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==}
     engines: {node: '>=8'}
@@ -6844,20 +7045,16 @@ packages:
     resolution: {integrity: sha512-MdYSsbdCaIRjzo5edthZtWmEZVMfr1qrtYZUHIdO3swCE+CoZA8S5l0s4jDsYlTa9ZiXv0pTgpzE7s4N8NeUOA==}
     engines: {node: '>=18'}
 
-  flat-cache@3.0.4:
-    resolution: {integrity: sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==}
-    engines: {node: ^10.12.0 || >=12.0.0}
+  flat-cache@4.0.1:
+    resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==}
+    engines: {node: '>=16'}
 
-  flatted@3.2.7:
-    resolution: {integrity: sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==}
+  flatted@3.3.1:
+    resolution: {integrity: sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==}
 
-  flow-parser@0.202.0:
-    resolution: {integrity: sha512-ZiXxSIXK3zPmY3zrzCofFonM2T+/3Jz5QZKJyPVtUERQEJUnYkXBQ+0H3FzyqiyJs+VXqb/UNU6/K6sziVYdxw==}
-    engines: {node: '>=0.4.0'}
-
-  fluent-ffmpeg@2.1.2:
-    resolution: {integrity: sha512-IZTB4kq5GK0DPp7sGQ0q/BWurGHffRtQQwVkiqDgeO6wYJLLV5ZhgNOQ65loZxxuPMKZKZcICCUnaGtlxBiR0Q==}
-    engines: {node: '>=0.8.0'}
+  fluent-ffmpeg@2.1.3:
+    resolution: {integrity: sha512-Be3narBNt2s6bsaqP6Jzq91heDgOEaDCJAXcE3qcma/EJBSy5FB4cvO31XBInuAuKBx8Kptf8dkhjK0IOru39Q==}
+    engines: {node: '>=18'}
 
   follow-redirects@1.15.2:
     resolution: {integrity: sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==}
@@ -6868,6 +7065,15 @@ packages:
       debug:
         optional: true
 
+  follow-redirects@1.15.9:
+    resolution: {integrity: sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==}
+    engines: {node: '>=4.0'}
+    peerDependencies:
+      debug: '*'
+    peerDependenciesMeta:
+      debug:
+        optional: true
+
   for-each@0.3.3:
     resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==}
 
@@ -6890,10 +7096,6 @@ packages:
     resolution: {integrity: sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==}
     engines: {node: '>= 0.12'}
 
-  form-data@3.0.1:
-    resolution: {integrity: sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==}
-    engines: {node: '>= 6'}
-
   form-data@4.0.0:
     resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==}
     engines: {node: '>= 6'}
@@ -6913,9 +7115,6 @@ packages:
   from@0.1.7:
     resolution: {integrity: sha512-twe20eF1OxVxp/ML/kq2p1uc6KvFK/+vs8WjEbeKmV2He22MKm7YF2ANIt+EOqhJ5L3K/SuuPhk0hWQDjOM23g==}
 
-  fs-constants@1.0.0:
-    resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==}
-
   fs-extra@11.1.1:
     resolution: {integrity: sha512-MGIE4HOvQCeUCzmlHs0vXpih4ysz4wg9qiSAu6cd42lVwPbTM1TjV7RusoyQqMmk/95gdQZX72u+YW+c3eEpFQ==}
     engines: {node: '>=14.14'}
@@ -6939,8 +7138,8 @@ packages:
     resolution: {integrity: sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==}
     engines: {node: '>= 8'}
 
-  fs-minipass@3.0.2:
-    resolution: {integrity: sha512-2GAfyfoaCDRrM6jaOS3UsBts8yJ55VioXdWcOL7dK9zdAuKT71+WBA4ifnNYqVjYv+4SsPxjK0JT4yIIn4cA/g==}
+  fs-minipass@3.0.3:
+    resolution: {integrity: sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw==}
     engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
 
   fs.realpath@1.0.0:
@@ -6958,12 +7157,17 @@ packages:
     resolution: {integrity: sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA==}
     engines: {node: '>= 0.4'}
 
+  function.prototype.name@1.1.6:
+    resolution: {integrity: sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==}
+    engines: {node: '>= 0.4'}
+
   functions-have-names@1.2.3:
     resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==}
 
   gauge@3.0.2:
     resolution: {integrity: sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==}
     engines: {node: '>=10'}
+    deprecated: This package is no longer supported.
 
   gensync@1.0.0-beta.2:
     resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==}
@@ -6979,9 +7183,9 @@ packages:
   get-intrinsic@1.2.1:
     resolution: {integrity: sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==}
 
-  get-npm-tarball-url@2.0.3:
-    resolution: {integrity: sha512-R/PW6RqyaBQNWYaSyfrh54/qtcnOp22FHCCiRhSSZj0FP3KQWCsxxt0DzIdVTbwTqe9CtQfvl/FPD4UIPt4pqw==}
-    engines: {node: '>=12.17'}
+  get-intrinsic@1.2.4:
+    resolution: {integrity: sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==}
+    engines: {node: '>= 0.4'}
 
   get-package-type@0.1.0:
     resolution: {integrity: sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==}
@@ -7006,10 +7210,18 @@ packages:
     resolution: {integrity: sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==}
     engines: {node: '>=16'}
 
+  get-stream@9.0.1:
+    resolution: {integrity: sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==}
+    engines: {node: '>=18'}
+
   get-symbol-description@1.0.0:
     resolution: {integrity: sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==}
     engines: {node: '>= 0.4'}
 
+  get-symbol-description@1.0.2:
+    resolution: {integrity: sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg==}
+    engines: {node: '>= 0.4'}
+
   get-tsconfig@4.7.2:
     resolution: {integrity: sha512-wuMsz4leaj5hbGgg4IvDU0bqJagpftG5l5cXIAvo8uZrqn0NJqwtfupTN00VnkQJPcIRrxYrm1Ue24btpCha2A==}
 
@@ -7023,10 +7235,6 @@ packages:
     resolution: {integrity: sha512-++rNGpDBgWQ9eXj9JfTBLHMUEd7lDOdzIvFyHQM9yL8ffxkcg4G6jWmsgu/r59Uq6nHc3wcVwtgy3geLnIWunQ==}
     engines: {node: '>= 0.8.0'}
 
-  giget@1.1.2:
-    resolution: {integrity: sha512-HsLoS07HiQ5oqvObOI+Qb2tyZH4Gj5nYGfF9qQcZNrPw+uEFhdXtgJr01aO2pWadGHucajYDLxxbtQkm97ON2A==}
-    hasBin: true
-
   github-slugger@2.0.0:
     resolution: {integrity: sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==}
 
@@ -7044,25 +7252,24 @@ packages:
     peerDependencies:
       glob: ^7.1.6
 
-  glob-to-regexp@0.4.1:
-    resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==}
-
   glob@10.3.10:
     resolution: {integrity: sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==}
     engines: {node: '>=16 || 14 >=14.17'}
     hasBin: true
 
-  glob@10.3.12:
-    resolution: {integrity: sha512-TCNv8vJ+xz4QiqTpfOJA7HvYv+tNIRHKfUWw/q+v2jdgN4ebz+KY9tGx5J4rHP0o84mNP+ApH66HRX8us3Khqg==}
-    engines: {node: '>=16 || 14 >=14.17'}
+  glob@11.0.0:
+    resolution: {integrity: sha512-9UiX/Bl6J2yaBbxKoEBRm4Cipxgok8kQYcOPEhScPwebu2I0HoQOuYdIO6S3hLuWoZgpDpwQZMzTFxgpkyT76g==}
+    engines: {node: 20 || >=22}
     hasBin: true
 
   glob@7.2.3:
     resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==}
+    deprecated: Glob versions prior to v9 are no longer supported
 
   glob@8.1.0:
     resolution: {integrity: sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==}
     engines: {node: '>=12'}
+    deprecated: Glob versions prior to v9 are no longer supported
 
   global-dirs@3.0.1:
     resolution: {integrity: sha512-NBcGGFbBA9s1VzD41QXDG+3++t9Mn5t1FpLdhESY6oKY4gYTFpX4wO3sqGUa0Srjtbfj3szX0RnemmrVRUdULA==}
@@ -7072,14 +7279,18 @@ packages:
     resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==}
     engines: {node: '>=4'}
 
-  globals@13.19.0:
-    resolution: {integrity: sha512-dkQ957uSRWHw7CFXLUtUHQI3g3aWApYhfNR2O6jn/907riyTYKVBmxYVROkBcY614FSSeSJh7Xm7SrUWCxvJMQ==}
-    engines: {node: '>=8'}
-
   globals@13.24.0:
     resolution: {integrity: sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==}
     engines: {node: '>=8'}
 
+  globals@14.0.0:
+    resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==}
+    engines: {node: '>=18'}
+
+  globals@15.9.0:
+    resolution: {integrity: sha512-SmSKyLLKFbSr6rptvP8izbyxJL4ILwqO9Jg23UA0sDlGlu58V59D1//I3vlc0KJphVdUR7vMjHIplYnzBxorQA==}
+    engines: {node: '>=18'}
+
   globalthis@1.0.3:
     resolution: {integrity: sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==}
     engines: {node: '>= 0.4'}
@@ -7102,8 +7313,8 @@ packages:
     resolution: {integrity: sha512-mThBblvlAF1d4O5oqyvN+ZxLAYwIJK7bpMxgYqPD9okW0C3qm5FFn7k811QrcuEBwaogR3ngOFoCfs6mRv7teQ==}
     engines: {node: '>=14.16'}
 
-  got@14.2.1:
-    resolution: {integrity: sha512-KOaPMremmsvx6l9BLC04LYE6ZFW4x7e4HkTe3LwBmtuYYQwpeS4XKqzhubTIkaQ1Nr+eXxeori0zuwupXMovBQ==}
+  got@14.4.2:
+    resolution: {integrity: sha512-+Te/qEZ6hr7i+f0FNgXx/6WQteSM/QqueGvxeYQQFm0GDfoxLVJ/oiwUKYMTeioColWUTdewZ06hmrBjw6F7tw==}
     engines: {node: '>=20'}
 
   graceful-fs@4.2.11:
@@ -7119,22 +7330,17 @@ packages:
     resolution: {integrity: sha512-59LZHPdGZVh695Ud9lRzPBVTtlX9ZCV150Er2W43ro37wVof0ctenSaskPPjN7lVTIN8mSZt8PHUNKZuNQUuxw==}
     engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0}
 
-  gunzip-maybe@1.4.2:
-    resolution: {integrity: sha512-4haO1M4mLO91PW57BMsDFf75UmwoRX0GkdD+Faw+Lr+r/OZrOCS0pIBwOL1xCKQqnQzbNFGgK2V2CpBUPeFNTw==}
-    hasBin: true
-
   hammerjs@2.0.8:
     resolution: {integrity: sha512-tSQXBXS/MWQOn/RKckawJ61vvsDpCom87JgxiYdGwHdOa0ht0vzUWDlfioofFCRU0L+6NGDt6XzbgoJvZkMeRQ==}
     engines: {node: '>=0.8.0'}
 
-  handlebars@4.7.7:
-    resolution: {integrity: sha512-aAcXm5OAfE/8IXkcZvCepKU3VzW1/39Fb5ZuqMtgI/hT8X2YgoMvBY5dLhq/cpOvw7Lk1nK/UF71aLG/ZnVYRA==}
-    engines: {node: '>=0.4.7'}
-    hasBin: true
-
   happy-dom@10.0.3:
     resolution: {integrity: sha512-WkCP+Z5fX6U5PY+yHP3ElV5D9PoxRAHRWPFq3pG9rg/6Hjf5ak7dozAgSCywsTRUq2qfa8vV8OQvUy5pRXy8EQ==}
 
+  happy-dom@15.7.4:
+    resolution: {integrity: sha512-r1vadDYGMtsHAAsqhDuk4IpPvr6N8MGKy5ntBo7tSdim+pWDxus2PNqOcOt8LuDZ4t3KJHE+gCuzupcx/GKnyQ==}
+    engines: {node: '>=18.0.0'}
+
   har-schema@2.0.0:
     resolution: {integrity: sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q==}
     engines: {node: '>=4'}
@@ -7162,10 +7368,17 @@ packages:
   has-property-descriptors@1.0.0:
     resolution: {integrity: sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==}
 
+  has-property-descriptors@1.0.2:
+    resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==}
+
   has-proto@1.0.1:
     resolution: {integrity: sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==}
     engines: {node: '>= 0.4'}
 
+  has-proto@1.0.3:
+    resolution: {integrity: sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==}
+    engines: {node: '>= 0.4'}
+
   has-symbols@1.0.3:
     resolution: {integrity: sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==}
     engines: {node: '>= 0.4'}
@@ -7174,6 +7387,10 @@ packages:
     resolution: {integrity: sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==}
     engines: {node: '>= 0.4'}
 
+  has-tostringtag@1.0.2:
+    resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==}
+    engines: {node: '>= 0.4'}
+
   has-unicode@2.0.1:
     resolution: {integrity: sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==}
 
@@ -7184,22 +7401,29 @@ packages:
   hash-sum@2.0.0:
     resolution: {integrity: sha512-WdZTbAByD+pHfl/g9QSsBIIwy8IT+EsPiKDs0KNX+zSHhdDLFKdZu0BQHljvO+0QI/BasbMSUa8wYNCZTvhslg==}
 
-  hashlru@2.3.0:
-    resolution: {integrity: sha512-0cMsjjIC8I+D3M44pOQdsy0OHXGLVz6Z0beRuufhKa0KfaD2wGwAev6jILzXsd3/vpnNQJmWyZtIILqM1N+n5A==}
-
   hasown@2.0.0:
     resolution: {integrity: sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==}
     engines: {node: '>= 0.4'}
 
+  hasown@2.0.2:
+    resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
+    engines: {node: '>= 0.4'}
+
   hast-util-heading-rank@3.0.0:
     resolution: {integrity: sha512-EJKb8oMUXVHcWZTDepnr+WNbfnXKFNf9duMesmr4S8SXTJBJ9M4Yok08pu9vxdJwdlGRhVumk9mEhkEvKGifwA==}
 
   hast-util-is-element@3.0.0:
     resolution: {integrity: sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g==}
 
+  hast-util-to-html@9.0.3:
+    resolution: {integrity: sha512-M17uBDzMJ9RPCqLMO92gNNUDuBSq10a25SDBI08iCCxmorf4Yy6sYHK57n9WAbRAAaU+DuR4W6GN9K4DFZesYg==}
+
   hast-util-to-string@3.0.0:
     resolution: {integrity: sha512-OGkAxX1Ua3cbcW6EJ5pT/tslVb90uViVkcJ4ZZIMW/R33DX/AkcJcRrPebPwJkHYwlDHXz4aIwvAAaAdtrACFA==}
 
+  hast-util-whitespace@3.0.0:
+    resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==}
+
   he@1.2.0:
     resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==}
     hasBin: true
@@ -7210,8 +7434,8 @@ packages:
   highlight.js@10.7.3:
     resolution: {integrity: sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==}
 
-  highlight.js@11.9.0:
-    resolution: {integrity: sha512-fJ7cW7fQGCYAkgv4CPfwFHrfd/cLS4Hau96JuJ+ZTOWhjnhoeN1ub1tFmALm/+lW5z4WCAuAV9bm05AP0mS6Gw==}
+  highlight.js@11.10.0:
+    resolution: {integrity: sha512-SYVnVFswQER+zu1laSya563s+F8VDGt7o35d4utbamowvUNLLMovFqwCLSocpZTz3MgaSRA1IbqRWZv97dtErQ==}
     engines: {node: '>=12.0.0'}
 
   hosted-git-info@2.8.9:
@@ -7239,13 +7463,22 @@ packages:
     resolution: {integrity: sha512-vy7ClnArOZwCnqZgvv+ddgHgJiAFXe3Ge9ML5/mBctVJoUoYPCdxVucOywjDARn6CVoh3dRSFdPHy2sX80L0Wg==}
     engines: {node: '>=8'}
 
+  html-void-elements@3.0.0:
+    resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==}
+
   htmlescape@1.1.1:
     resolution: {integrity: sha512-eVcrzgbR4tim7c7soKQKtxa/kQM4TzjnlU83rcZ9bHU6t31ehfV7SktN6McWgwPWg+JYMA/O3qpGxBvFq1z2Jg==}
     engines: {node: '>=0.10'}
 
+  htmlparser2@5.0.1:
+    resolution: {integrity: sha512-vKZZra6CSe9qsJzh0BjBGXo8dvzNsq/oGvsjfRdOrrryfeD9UOBEEQdeoqCRmKZchF5h2zOBMQ6YuQ0uRUmdbQ==}
+
   htmlparser2@8.0.1:
     resolution: {integrity: sha512-4lVbmc1diZC7GUJQtRQ5yBAeUCL1exyMwmForWkRLnwyzWBFxN633SALPMGYaWZvKe9j1pRZJpauvmxENSp/EA==}
 
+  htmlparser2@9.1.0:
+    resolution: {integrity: sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ==}
+
   http-cache-semantics@4.1.1:
     resolution: {integrity: sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==}
 
@@ -7257,16 +7490,16 @@ packages:
     resolution: {integrity: sha512-3cZ0SRL8fb9MUlU3mKM61FcQvPfXx2dBrZW3Vbg5CXa8jFlK8OaEpePenLe1oEXQduhz8b0QjsqfS59QP4AJDQ==}
     engines: {node: '>=6.0.0'}
 
-  http-proxy-agent@7.0.0:
-    resolution: {integrity: sha512-+ZT+iBxVUQ1asugqnD6oWoRiS25AkjNfG085dKJGtGxkdwLQrMKU5wJr2bOOFAXzKcTuqq+7fZlTMgG3SRfIYQ==}
+  http-proxy-agent@7.0.2:
+    resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==}
     engines: {node: '>= 14'}
 
   http-signature@1.2.0:
     resolution: {integrity: sha512-CAbnr6Rz4CYQkLYUtSNXxQPUH2gK8f3iWexVlsnMeD+GjlsQ0Xsy1cOX+mN3dtxYomRy21CiOzU8Uhw6OwncEQ==}
     engines: {node: '>=0.8', npm: '>=1.3.7'}
 
-  http-signature@1.3.6:
-    resolution: {integrity: sha512-3adrsD6zqo4GsTqtO7FyrejHNv+NgiIfAfv68+jVlFmSr9OGy7zrxONceFRLKvnnZA5jbxQBX1u9PpB6Wi32Gw==}
+  http-signature@1.4.0:
+    resolution: {integrity: sha512-G5akfn7eKbpDN+8nPS/cb57YeA1jLTVxjpCj7tmm3QKPdyDy7T+qSC40e9ptydSWvkwjSXw1VbkpyEm39ukeAg==}
     engines: {node: '>=0.10'}
 
   http2-wrapper@1.0.3:
@@ -7293,6 +7526,10 @@ packages:
     resolution: {integrity: sha512-NmLNjm6ucYwtcUmL7JQC1ZQ57LmHP4lT15FQ8D61nak1rO6DH+fz5qNK2Ap5UN4ZapYICE3/0KodcLYSPsPbaA==}
     engines: {node: '>= 14'}
 
+  https-proxy-agent@7.0.5:
+    resolution: {integrity: sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==}
+    engines: {node: '>= 14'}
+
   human-signals@1.1.1:
     resolution: {integrity: sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==}
     engines: {node: '>=8.12.0'}
@@ -7309,6 +7546,10 @@ packages:
     resolution: {integrity: sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==}
     engines: {node: '>=16.17.0'}
 
+  human-signals@8.0.0:
+    resolution: {integrity: sha512-/1/GPCpDUCCYwlERiYjxoczfP0zfvZMU/OWgQPMya9AbAE24vseigFdhAMObpc8Q4lc/kjutPfUddDYyAmejnA==}
+    engines: {node: '>=18.18.0'}
+
   iconv-lite@0.4.24:
     resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==}
     engines: {node: '>=0.10.0'}
@@ -7326,8 +7567,8 @@ packages:
   ignore-by-default@1.0.1:
     resolution: {integrity: sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==}
 
-  ignore-walk@6.0.4:
-    resolution: {integrity: sha512-t7sv42WkwFkyKbivUCglsQW5YWMskWtbEf4MNKX5u/CCWHKSPzN4FtBQGsQZgCLbxOzpVlcbWVK5KB3auIOjSw==}
+  ignore-walk@6.0.5:
+    resolution: {integrity: sha512-VuuG0wCnjhnylG1ABXT3dAuIpTNDs/G8jlpmwXY03fXoXy/8ZK8/T+hMzt8L4WnrLCJgdybqgPagnF/f97cg3A==}
     engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
 
   ignore@5.2.4:
@@ -7345,11 +7586,11 @@ packages:
     resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==}
     engines: {node: '>=6'}
 
-  import-in-the-middle@1.4.2:
-    resolution: {integrity: sha512-9WOz1Yh/cvO/p69sxRmhyQwrIGGSp7EIdcb+fFNVi7CzQGQB8U1/1XrKVSbEd/GNOAeM0peJtmi7+qphe7NvAw==}
+  import-in-the-middle@1.10.0:
+    resolution: {integrity: sha512-Z1jumVdF2GwnnYfM0a/y2ts7mZbwFMgt5rRuVmLgobgahC6iKgN5MBuXjzfTIOUpq5LSU10vJIPpVKe0X89fIw==}
 
-  import-in-the-middle@1.7.4:
-    resolution: {integrity: sha512-Lk+qzWmiQuRPPulGQeK5qq0v32k2bHnWrRPFgqyvhw7Kkov5L6MOLOIU3pcWeujc9W4q54Cp3Q2WV16eQkc7Bg==}
+  import-in-the-middle@1.7.1:
+    resolution: {integrity: sha512-1LrZPDtW+atAxH42S6288qyDFNQ2YCty+2mxEPRtfazH6Z5QwkaBSTS2ods7hnVJioF6rkRfNoA6A/MstpFXLg==}
 
   import-lazy@4.0.0:
     resolution: {integrity: sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw==}
@@ -7374,6 +7615,7 @@ packages:
 
   inflight@1.0.6:
     resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==}
+    deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.
 
   inherits@2.0.4:
     resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
@@ -7396,6 +7638,10 @@ packages:
     resolution: {integrity: sha512-Y+R5hJrzs52QCG2laLn4udYVnxsfny9CpOhNhUvk/SSSVyF6T27FzRbF0sroPidSu3X8oEAkOn2K804mjpt6UQ==}
     engines: {node: '>= 0.4'}
 
+  internal-slot@1.0.7:
+    resolution: {integrity: sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==}
+    engines: {node: '>= 0.4'}
+
   intersection-observer@0.12.2:
     resolution: {integrity: sha512-7m1vEcPCxXYI8HqnL8CKI6siDyD+eIWSwgB3DZA+ZTogxk9I4CDnj4wilt9x/+/QbHI4YG5YZNmC6458/e9Ktg==}
 
@@ -7406,13 +7652,13 @@ packages:
   iota-array@1.0.0:
     resolution: {integrity: sha512-pZ2xT+LOHckCatGQ3DcG/a+QuEqvoxqkiL7tvE8nn3uuu+f6i1TtpB5/FtWFbxUuVr5PZCx8KskuGatbJDXOWA==}
 
-  ip-address@7.1.0:
-    resolution: {integrity: sha512-V9pWC/VJf2lsXqP7IWJ+pe3P1/HCYGBMZrrnT62niLGjAfCbeiwXMUxaeHvnVlz19O27pvXP4azs+Pj/A0x+SQ==}
-    engines: {node: '>= 10'}
+  ip-address@9.0.5:
+    resolution: {integrity: sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==}
+    engines: {node: '>= 12'}
 
-  ip-cidr@3.1.0:
-    resolution: {integrity: sha512-HUCn4snshEX1P8cja/IyU3qk8FVDW8T5zZcegDFbu4w7NojmAhk5NcOgj3M8+0fmumo1afJTPDtJlzsxLdOjtg==}
-    engines: {node: '>=10.0.0'}
+  ip-cidr@4.0.2:
+    resolution: {integrity: sha512-KifhLKBjdS/hB3TD4UUOalVp1BpzPFvRpgJvXcP0Ya98tuSQTUQ71iI7EW7CKddkBJTYB3GfTWl5eJwpLOXj2A==}
+    engines: {node: '>=16.14.0'}
 
   ip-regex@4.3.0:
     resolution: {integrity: sha512-B9ZWJxHHOHUhUjCPrMpLD4xEq35bUTClHM1S6CBU5ixQnkZmwipwgc96vAd7AAGM9TGHvJR+Uss+/Ak6UphK+Q==}
@@ -7444,6 +7690,10 @@ packages:
   is-array-buffer@3.0.2:
     resolution: {integrity: sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==}
 
+  is-array-buffer@3.0.4:
+    resolution: {integrity: sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw==}
+    engines: {node: '>= 0.4'}
+
   is-arrayish@0.2.1:
     resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==}
 
@@ -7475,13 +7725,18 @@ packages:
   is-core-module@2.13.1:
     resolution: {integrity: sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==}
 
+  is-core-module@2.15.1:
+    resolution: {integrity: sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==}
+    engines: {node: '>= 0.4'}
+
+  is-data-view@1.0.1:
+    resolution: {integrity: sha512-AHkaJrsUVW6wq6JS8y3JnM/GJF/9cf+k20+iDzlSaJrinEo5+7vRiteOSwBhHRiAyQATN1AmY4hwzxJKPmYf+w==}
+    engines: {node: '>= 0.4'}
+
   is-date-object@1.0.5:
     resolution: {integrity: sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==}
     engines: {node: '>= 0.4'}
 
-  is-deflate@1.0.0:
-    resolution: {integrity: sha512-YDoFpuZWu1VRXlsnlYMzKyVRITXj7Ej/V9gXQ2/pAe7X1J7M/RNOqaIYi6qUn+B7nGyB9pDXrv02dsB58d2ZAQ==}
-
   is-docker@2.2.1:
     resolution: {integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==}
     engines: {node: '>=8'}
@@ -7513,18 +7768,10 @@ packages:
     resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
     engines: {node: '>=0.10.0'}
 
-  is-gzip@1.0.0:
-    resolution: {integrity: sha512-rcfALRIb1YewtnksfRIHGcIY93QnK8BIQ/2c9yDYcG/Y6+vRoJuTWBmmSEbyLLYtXm7q35pHOHbZFQBaLrhlWQ==}
-    engines: {node: '>=0.10.0'}
-
   is-installed-globally@0.4.0:
     resolution: {integrity: sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ==}
     engines: {node: '>=10'}
 
-  is-interactive@1.0.0:
-    resolution: {integrity: sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==}
-    engines: {node: '>=8'}
-
   is-ip@3.1.0:
     resolution: {integrity: sha512-35vd5necO7IitFPjd/YBeqwWnyDWbuLH9ZXQdMfDA8TEo7pv5X8yfrvVO3xbJbLUlERCMvf6X0hTUamQxCYJ9Q==}
     engines: {node: '>=8'}
@@ -7535,14 +7782,14 @@ packages:
   is-map@2.0.2:
     resolution: {integrity: sha512-cOZFQQozTha1f4MxLFzlgKYPTyj26picdZTx82hbc/Xf4K/tZOOXSCkMvU4pKioRXGDLJRn0GM7Upe7kR721yg==}
 
-  is-nan@1.3.2:
-    resolution: {integrity: sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w==}
-    engines: {node: '>= 0.4'}
-
   is-negative-zero@2.0.2:
     resolution: {integrity: sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==}
     engines: {node: '>= 0.4'}
 
+  is-negative-zero@2.0.3:
+    resolution: {integrity: sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==}
+    engines: {node: '>= 0.4'}
+
   is-node-process@1.2.0:
     resolution: {integrity: sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==}
 
@@ -7554,10 +7801,6 @@ packages:
     resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
     engines: {node: '>=0.12.0'}
 
-  is-path-cwd@2.2.0:
-    resolution: {integrity: sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ==}
-    engines: {node: '>=6'}
-
   is-path-inside@3.0.3:
     resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==}
     engines: {node: '>=8'}
@@ -7570,10 +7813,6 @@ packages:
     resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==}
     engines: {node: '>=12'}
 
-  is-plain-object@2.0.4:
-    resolution: {integrity: sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==}
-    engines: {node: '>=0.10.0'}
-
   is-plain-object@5.0.0:
     resolution: {integrity: sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==}
     engines: {node: '>=0.10.0'}
@@ -7594,6 +7833,10 @@ packages:
   is-shared-array-buffer@1.0.2:
     resolution: {integrity: sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==}
 
+  is-shared-array-buffer@1.0.3:
+    resolution: {integrity: sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg==}
+    engines: {node: '>= 0.4'}
+
   is-stream@1.1.0:
     resolution: {integrity: sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==}
     engines: {node: '>=0.10.0'}
@@ -7606,12 +7849,16 @@ packages:
     resolution: {integrity: sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==}
     engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
 
+  is-stream@4.0.1:
+    resolution: {integrity: sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==}
+    engines: {node: '>=18'}
+
   is-string@1.0.7:
     resolution: {integrity: sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==}
     engines: {node: '>= 0.4'}
 
-  is-svg@5.0.0:
-    resolution: {integrity: sha512-sRl7J0oX9yUNamSdc8cwgzh9KBLnQXNzGmW0RVHwg/jEYjGNYHC6UvnYD8+hAeut9WwxRvhG9biK7g/wDGxcMw==}
+  is-svg@5.1.0:
+    resolution: {integrity: sha512-uVg5yifaTxHoefNf5Jcx+i9RZe2OBYd/UStp1umx+EERa4xGRa3LLGXjoEph43qUORC0qkafUgrXZ6zzK89yGA==}
     engines: {node: '>=14.16'}
 
   is-symbol@1.0.4:
@@ -7622,6 +7869,10 @@ packages:
     resolution: {integrity: sha512-PJqgEHiWZvMpaFZ3uTc8kHPM4+4ADTlDniuQL7cU/UDA0Ql7F70yGfHph3cLNe+c9toaigv+DFzTJKhc2CtO6A==}
     engines: {node: '>= 0.4'}
 
+  is-typed-array@1.1.13:
+    resolution: {integrity: sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==}
+    engines: {node: '>= 0.4'}
+
   is-typedarray@1.0.0:
     resolution: {integrity: sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==}
 
@@ -7629,6 +7880,10 @@ packages:
     resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==}
     engines: {node: '>=10'}
 
+  is-unicode-supported@2.0.0:
+    resolution: {integrity: sha512-FRdAyx5lusK1iHG0TWpVtk9+1i+GjrzRffhDg4ovQ7mcidMQ6mj+MhKPmvh7Xwyv5gIS06ns49CA7Sqg7lC22Q==}
+    engines: {node: '>=18'}
+
   is-weakmap@2.0.1:
     resolution: {integrity: sha512-NSBR4kH5oVj1Uwvv970ruUkCV7O1mzgVFO4/rev2cLRda9Tm9HrL70ZPut4rOHgY0FNrUu9BCbXA2sdQ+x0chA==}
 
@@ -7658,10 +7913,6 @@ packages:
     resolution: {integrity: sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==}
     engines: {node: '>=16'}
 
-  isobject@3.0.1:
-    resolution: {integrity: sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==}
-    engines: {node: '>=0.10.0'}
-
   isstream@0.1.2:
     resolution: {integrity: sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==}
 
@@ -7685,6 +7936,10 @@ packages:
     resolution: {integrity: sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==}
     engines: {node: '>=10'}
 
+  istanbul-lib-source-maps@5.0.4:
+    resolution: {integrity: sha512-wHOoEsNJTVltaJp8eVkm8w+GVkVNHT2YDYo53YdzQEL2gWm1hBX5cGFR9hQJtuGLebidVX7et3+dmDZrmclduw==}
+    engines: {node: '>=10'}
+
   istanbul-reports@3.1.6:
     resolution: {integrity: sha512-TLgnMkKg3iTDsQ9PbPTdpfAK2DzjF9mqUG7RMgcQl8oFjad8ob4laGxv5XV5U9MAfx8D6tSJiUyuAwzLicaxlg==}
     engines: {node: '>=8'}
@@ -7697,6 +7952,10 @@ packages:
     resolution: {integrity: sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==}
     engines: {node: '>=14'}
 
+  jackspeak@4.0.1:
+    resolution: {integrity: sha512-cub8rahkh0Q/bw1+GxP7aeSe29hHHn2V4m29nnDlvCdlgU+3UGxkZp7Z53jLUdpX3jdTO0nJZUDl3xvbWc2Xog==}
+    engines: {node: 20 || >=22}
+
   jake@10.8.5:
     resolution: {integrity: sha512-sVpxYeuAhWt0OTWITwT98oyV0GsXyMlXCF+3L1SuafBVUIr/uILGRB+NqwkzhgXKvoJpDIpQvqkUALgdmQsQxw==}
     engines: {node: '>=10'}
@@ -7843,6 +8102,9 @@ packages:
   joi@17.11.0:
     resolution: {integrity: sha512-NgB+lZLNoqISVy1rZocE9PZI36bL/77ie924Ri43yEvi9GUUMPeyVIr8KdFTMUlby1p0PBYMk9spIxEUQYqrJQ==}
 
+  joi@17.13.3:
+    resolution: {integrity: sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA==}
+
   jpeg-js@0.3.7:
     resolution: {integrity: sha512-9IXdWudL61npZjvLuVe/ktHiA41iE8qFyLB+4VDTblEsWBzeg8WQTlktdUK4CdncUqtUgUg0bbOmTE2bKBKaBQ==}
 
@@ -7857,6 +8119,9 @@ packages:
   js-tokens@4.0.0:
     resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
 
+  js-tokens@9.0.0:
+    resolution: {integrity: sha512-WriZw1luRMlmV3LGJaR6QOJjWwgLUTf89OwT2lUOyjX2dJGBwgmIkbcz+7WFZjrZM635JOIR517++e/67CP9dQ==}
+
   js-yaml@3.14.1:
     resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==}
     hasBin: true
@@ -7875,17 +8140,12 @@ packages:
     resolution: {integrity: sha512-lJH6tJ77V8Nzd5QWRkFYCLc13a3vADkh3r/Fi8HupZGWk2OVVDfnZP8V/VgQgZ+lzW0kG2UGb5hFgt3V3ndotQ==}
     engines: {node: '>=0.1.90'}
 
-  jscodeshift@0.15.1:
-    resolution: {integrity: sha512-hIJfxUy8Rt4HkJn/zZPU9ChKfKZM1342waJ1QC2e2YsPcWhM+3BJ4dcfQCzArTrk1jJeNLB341H+qOcEHRxJZg==}
-    hasBin: true
-    peerDependencies:
-      '@babel/preset-env': ^7.1.6
-    peerDependenciesMeta:
-      '@babel/preset-env':
-        optional: true
+  jsdoc-type-pratt-parser@4.1.0:
+    resolution: {integrity: sha512-Hicd6JK5Njt2QB6XYFS7ok9e37O8AYk3jTcppG4YVQnYjOemymvTcmc7OWsmq/Qqj5TdRFO5/x/tIPmBeRtGHg==}
+    engines: {node: '>=12.0.0'}
 
-  jsdom@24.0.0:
-    resolution: {integrity: sha512-UDS2NayCvmXSXVP6mpTj+73JnNQadZlr9N68189xib2tx5Mls7swlTNao26IoHv46BZJFvXygyRtyXd1feAk1A==}
+  jsdom@24.1.1:
+    resolution: {integrity: sha512-5O1wWV99Jhq4DV7rCLIoZ/UIhyQeDR7wHVyZAHAshbrvZsLs+Xzz7gtwnlJTJDjleiTKh54F4dXrX70vJQTyJQ==}
     engines: {node: '>=18'}
     peerDependencies:
       canvas: ^2.11.2
@@ -7893,10 +8153,6 @@ packages:
       canvas:
         optional: true
 
-  jsesc@0.5.0:
-    resolution: {integrity: sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==}
-    hasBin: true
-
   jsesc@2.5.2:
     resolution: {integrity: sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==}
     engines: {node: '>=4'}
@@ -7908,6 +8164,9 @@ packages:
   json-parse-even-better-errors@2.3.1:
     resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==}
 
+  json-schema-ref-resolver@1.0.1:
+    resolution: {integrity: sha512-EJAj1pgHc1hxF6vo2Z3s69fMjO1INq6eGHXZ8Z6wCQeldCuwxGK9Sxf4/cScGn3FZubCVUehfWtcDM/PLteCQw==}
+
   json-schema-traverse@0.4.1:
     resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==}
 
@@ -7967,12 +8226,14 @@ packages:
   jsrsasign@11.1.0:
     resolution: {integrity: sha512-Ov74K9GihaK9/9WncTe1mPmvrO7Py665TUfUKvraXBpu+xcTWitrtuOwcjf4KMU9maPaYn0OuaWy0HOzy/GBXg==}
 
-  jssha@3.3.1:
-    resolution: {integrity: sha512-VCMZj12FCFMQYcFLPRm/0lOBbLi8uM2BhXPTqw3U4YAfs4AZfiApOoBLoN8cQE60Z50m1MYMTQVCfgF/KaCVhQ==}
-
   jstransformer@1.0.0:
     resolution: {integrity: sha512-C9YK3Rf8q6VAPDCCU9fnqo3mAfOH6vUGnMcP4AQAYIEpWtfGLpwOTmZ+igtdK5y+VvI2n3CyYSzy4Qh34eq24A==}
 
+  juice@11.0.0:
+    resolution: {integrity: sha512-sGF8hPz9/Wg+YXbaNDqc1Iuoaw+J/P9lBHNQKXAGc9pPNjCd4fyPai0Zxj7MRtdjMr0lcgk5PjEIkP2b8R9F3w==}
+    engines: {node: '>=18.17'}
+    hasBin: true
+
   just-extend@4.2.1:
     resolution: {integrity: sha512-g3UB796vUFIY90VIv/WX3L2c8CS2MdWUww3CNrYmqza1Fg0DURc2K/O4YrnklBdQarSJ/y8JnJYDGc+1iumQjg==}
 
@@ -8011,10 +8272,6 @@ packages:
     resolution: {integrity: sha512-cc8oEVoctTvsFZ/Oje/kGnHbpWHYBe8IAJe4C0QNc3t8uM/0Y8+erSz/7Y1ALuXTEZTMvxXwO6YbX1ey3ujiZw==}
     engines: {node: '> 0.8'}
 
-  lazy-universal-dotenv@4.0.0:
-    resolution: {integrity: sha512-aXpZJRnTkpK6gQ/z4nk+ZBLd/Qdp118cvPruLSIQzQNRhKwEcdXCOzXuF55VDqIiuAaY3UGZ10DJtvZzDcvsxg==}
-    engines: {node: '>=14.0.0'}
-
   lazystream@1.0.1:
     resolution: {integrity: sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==}
     engines: {node: '>= 0.6.3'}
@@ -8027,8 +8284,8 @@ packages:
     resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==}
     engines: {node: '>= 0.8.0'}
 
-  light-my-request@5.11.0:
-    resolution: {integrity: sha512-qkFCeloXCOMpmEdZ/MV91P8AT4fjwFXWaAFz3lUeStM8RcoM1ks4J/F8r1b3r6y/H4u3ACEJ1T+Gv5bopj7oDA==}
+  light-my-request@6.0.0:
+    resolution: {integrity: sha512-kFkFXrmKCL0EEeOmJybMH5amWFd+AFvlvMlvFTRxCUwbhfapZqDmeLMPoWihntnYY6JpoQDE9k+vOzObF1fDqg==}
 
   lilconfig@3.1.1:
     resolution: {integrity: sha512-O18pf7nyvHTckunPWCV1XUNXU1piu01y2b7ATJ0ppkUkk8ocqVWBrYjJBCwHDjD/ZWcfyrA0P4gKhzWGi5EINQ==}
@@ -8046,14 +8303,10 @@ packages:
       enquirer:
         optional: true
 
-  local-pkg@0.4.3:
-    resolution: {integrity: sha512-SFppqq5p42fe2qcZQqqEOiVRXl+WCP1MdT6k7BDEW1j++sp5fIY+/fdRQitvKgB5BrBcmrs5m/L0v2FrU5MY1g==}
+  local-pkg@0.5.0:
+    resolution: {integrity: sha512-ok6z3qlYyCDS4ZEU27HaU6x/xZa9Whf8jD4ptH5UZTQYZVYeb9bnZ3ojVhiJNLiXK1Hfc0GNbLXcmZ5plLDDBg==}
     engines: {node: '>=14'}
 
-  locate-path@3.0.0:
-    resolution: {integrity: sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==}
-    engines: {node: '>=6'}
-
   locate-path@5.0.0:
     resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==}
     engines: {node: '>=8'}
@@ -8062,9 +8315,6 @@ packages:
     resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==}
     engines: {node: '>=10'}
 
-  lodash.debounce@4.0.8:
-    resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==}
-
   lodash.defaults@4.2.0:
     resolution: {integrity: sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==}
 
@@ -8074,9 +8324,6 @@ packages:
   lodash.isarguments@3.1.0:
     resolution: {integrity: sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==}
 
-  lodash.isequal@4.5.0:
-    resolution: {integrity: sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==}
-
   lodash.memoize@4.1.2:
     resolution: {integrity: sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==}
 
@@ -8113,6 +8360,9 @@ packages:
   loupe@2.3.7:
     resolution: {integrity: sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==}
 
+  loupe@3.1.1:
+    resolution: {integrity: sha512-edNu/8D5MKVfGVFRhFf8aAxiTM6Wumfz5XsaatSxlD3w4R1d/WEKUTydCdPGbl9K7QG/Ca3GnDV2sIKIpXRQcw==}
+
   lowercase-keys@2.0.0:
     resolution: {integrity: sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==}
     engines: {node: '>=8'}
@@ -8129,6 +8379,10 @@ packages:
     resolution: {integrity: sha512-9hp3Vp2/hFQUiIwKo8XCeFVnrg8Pk3TYNPIR7tJADKi5YfcF7vEaK7avFHTlSy3kOKYaJQaalfEo6YuXdceBOQ==}
     engines: {node: 14 || >=16.14}
 
+  lru-cache@11.0.0:
+    resolution: {integrity: sha512-Qv32eSV1RSCfhY3fpPE2GNZ8jgM9X7rdAfemLWqTUxwiyIC4jJ6Sy0fZ8H+oLWevO6i4/bizg7c8d8i6bxrzbA==}
+    engines: {node: 20 || >=22}
+
   lru-cache@4.1.5:
     resolution: {integrity: sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==}
 
@@ -8158,17 +8412,15 @@ packages:
   magic-string@0.30.10:
     resolution: {integrity: sha512-iIRwTIf0QKV3UAnYK4PU8uiEc4SRh5jX0mwpIwETPpHdhVM4f53RSwS/vXvN1JhGX+Cs7B8qIq3d6AH49O5fAQ==}
 
-  magic-string@0.30.7:
-    resolution: {integrity: sha512-8vBuFF/I/+OSLRmdf2wwFCJCz+nSn0m6DPvGH1fS/KiQoSaR+sETbov0eIk9KhEKy8CYqIkIAnbohxT/4H0kuA==}
-    engines: {node: '>=12'}
+  magic-string@0.30.11:
+    resolution: {integrity: sha512-+Wri9p0QHMy+545hKww7YAu5NyzF8iomPL/RQazugQ9+Ez4Ic3mERMd8ZTX5rfK944j+560ZJi8iAwgak1Ac7A==}
+
+  magicast@0.3.4:
+    resolution: {integrity: sha512-TyDF/Pn36bBji9rWKHlZe+PZb6Mx5V8IHCSxk7X4aljM4e/vyDvZZYwHewdVaqiA0nb3ghfHU/6AUpDxWoER2Q==}
 
   mailcheck@1.1.1:
     resolution: {integrity: sha512-3WjL8+ZDouZwKlyJBMp/4LeziLFXgleOdsYu87piGcMLqhBzCsy2QFdbtAwv757TFC/rtqd738fgJw1tFQCSgA==}
 
-  make-dir@2.1.0:
-    resolution: {integrity: sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==}
-    engines: {node: '>=6'}
-
   make-dir@3.1.0:
     resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==}
     engines: {node: '>=8'}
@@ -8201,8 +8453,8 @@ packages:
   markdown-table@3.0.3:
     resolution: {integrity: sha512-Z1NL3Tb1M9wH4XESsCDEksWoKTdlUafKc4pt0GRwjUyXaCFZ+dc3g2erqB6zm3szA2IUSi7VnPI+o/9jnxh9hw==}
 
-  markdown-to-jsx@7.3.2:
-    resolution: {integrity: sha512-B+28F5ucp83aQm+OxNrPkS8z0tMKaeHiy0lHJs3LqCyDQFtWuenaIrkaVTgAm1pf1AU85LXltva86hlaT17i8Q==}
+  markdown-to-jsx@7.4.7:
+    resolution: {integrity: sha512-0+ls1IQZdU6cwM1yu0ZjjiVWYtkbExSyUIFU2ZeDIFuZM1W42Mh4OlJ4nb4apX4H8smxDHRdFaoIVJGwfv5hkg==}
     engines: {node: '>= 10'}
     peerDependencies:
       react: '>= 0.14.0'
@@ -8237,6 +8489,9 @@ packages:
   mdast-util-phrasing@4.1.0:
     resolution: {integrity: sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==}
 
+  mdast-util-to-hast@13.2.0:
+    resolution: {integrity: sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA==}
+
   mdast-util-to-markdown@2.1.0:
     resolution: {integrity: sha512-SR2VnIEdVNCJbP6y7kVTJgPLifdr8WEU440fQec7qHoHOUz/oJ2jmNRqdDQ3rbiStOXb2mCDGTuwsK5OPUgYlQ==}
 
@@ -8253,18 +8508,21 @@ packages:
     resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==}
     engines: {node: '>= 0.6'}
 
-  meilisearch@0.38.0:
-    resolution: {integrity: sha512-bHaq8nYxSKw9/Qslq1Zes5g9tHgFkxy/I9o8942wv2PqlNOT0CzptIkh/x98N52GikoSZOXSQkgt6oMjtf5uZw==}
+  meilisearch@0.42.0:
+    resolution: {integrity: sha512-pXaOPx/uhVGYVpejNuOcXifQVJlRVSxtvpgrGKb7ygmYo4qSNXkQXPxq1p0Tv+4/RsPJug3W04pcNnYXiqungA==}
 
   memoizerific@1.11.3:
     resolution: {integrity: sha512-/EuHYwAPdLtXwAwSZkh/Gutery6pD2KYd44oQLhAvQp/50mpyduZh8Q7PYHXTCJ+wuXxt7oij2LXyIJOOYFPog==}
 
+  mensch@0.3.4:
+    resolution: {integrity: sha512-IAeFvcOnV9V0Yk+bFhYR07O3yNina9ANIN5MoXBKYJ/RLYPurd2d0yw14MDhpr9/momp0WofT1bPUh3hkzdi/g==}
+
   meow@9.0.0:
     resolution: {integrity: sha512-+obSblOQmRhcyBt62furQqRAQpNyWXo8BuQ5bN7dG8wmwQ+vwHKp/rCFD4CrTP8CsDQD1sjoZ94K417XEUk8IQ==}
     engines: {node: '>=10'}
 
-  merge-descriptors@1.0.1:
-    resolution: {integrity: sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==}
+  merge-descriptors@1.0.3:
+    resolution: {integrity: sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==}
 
   merge-stream@2.0.0:
     resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==}
@@ -8368,8 +8626,8 @@ packages:
   micromark@4.0.0:
     resolution: {integrity: sha512-o/sd0nMof8kYff+TqcDx3VSrgBTcZpSvYcAHIfHhv5VAuNmisCxjhx6YmxS8PFEpb9z5WKWKPdzf0jM23ro3RQ==}
 
-  micromatch@4.0.5:
-    resolution: {integrity: sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==}
+  micromatch@4.0.8:
+    resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==}
     engines: {node: '>=8.6'}
 
   mime-db@1.52.0:
@@ -8385,6 +8643,11 @@ packages:
     engines: {node: '>=4'}
     hasBin: true
 
+  mime@2.6.0:
+    resolution: {integrity: sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==}
+    engines: {node: '>=4.0.0'}
+    hasBin: true
+
   mime@3.0.0:
     resolution: {integrity: sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==}
     engines: {node: '>=10.0.0'}
@@ -8417,6 +8680,10 @@ packages:
   minimalistic-assert@1.0.1:
     resolution: {integrity: sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==}
 
+  minimatch@10.0.1:
+    resolution: {integrity: sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==}
+    engines: {node: 20 || >=22}
+
   minimatch@3.0.8:
     resolution: {integrity: sha512-6FsRAQsxQ61mw+qP1ZzbL9Bc78x2p5OqNgNpnoAFLTrX8n5Kxph0CsnhmKKNXTWjXqU5L0pGPR7hYk+XWZr60Q==}
 
@@ -8481,6 +8748,10 @@ packages:
     resolution: {integrity: sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==}
     engines: {node: '>=16 || 14 >=14.17'}
 
+  minipass@7.1.2:
+    resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==}
+    engines: {node: '>=16 || 14 >=14.17'}
+
   minizlib@1.3.3:
     resolution: {integrity: sha512-6ZYMOEnmVsdCeTJVE0W9ZD+pVnE8h9Hma/iOwwRDsdQoePpoX56/8B6z3P9VNwppJuBKNRuFDRNRqRWexT9G9Q==}
 
@@ -8488,9 +8759,6 @@ packages:
     resolution: {integrity: sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==}
     engines: {node: '>= 8'}
 
-  mkdirp-classic@0.5.3:
-    resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==}
-
   mkdirp@0.5.6:
     resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==}
     hasBin: true
@@ -8508,8 +8776,8 @@ packages:
   mlly@1.5.0:
     resolution: {integrity: sha512-NPVQvAY1xr1QoVeG0cy8yUYC7FQcOx6evl/RjT1wL5FvzPnzOysoqB/jmx/DhssT2dYa8nxECLAaFI/+gVLhDQ==}
 
-  mnemonist@0.39.6:
-    resolution: {integrity: sha512-A/0v5Z59y63US00cRSLiloEIw3t5G+MiKz4BhX21FI+YBJXBOGW0ohFxTxO08dsOYlzxo87T7vGfZKYp2bcAWA==}
+  mnemonist@0.39.8:
+    resolution: {integrity: sha512-vyWo2K3fjrUw8YeeZ1zF0fy6Mu59RHokURlld8ymdUPjMlD9EC9ov1/YPqTgqRvUN9nTr3Gqfz29LYAmu0PHPQ==}
 
   mock-socket@9.3.1:
     resolution: {integrity: sha512-qxBgB7Qa2sEQgHFjj0dSigq7fX4k6Saisd5Nelwp2q8mlbAFh5dHV9JTTlF8viYJLSSWgMCZFUom8PJcMNBoJw==}
@@ -8518,10 +8786,6 @@ packages:
   module-details-from-path@1.0.3:
     resolution: {integrity: sha512-ySViT69/76t8VhE1xXHK6Ch4NcDd26gx0MzKXLO+F7NOtnqH68d9zF94nT8ZWSxXh8ELOERsnJO/sWt1xZYw5A==}
 
-  mri@1.2.0:
-    resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==}
-    engines: {node: '>=4'}
-
   ms@2.0.0:
     resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==}
 
@@ -8542,13 +8806,13 @@ packages:
   msgpackr@1.10.1:
     resolution: {integrity: sha512-r5VRLv9qouXuLiIBrLpl2d5ZvPt8svdQTl5/vMvE4nzDMyEX4sgW5yWhuBBj5UmgwOTWj8CIdSXn5sAfsHAWIQ==}
 
-  msw-storybook-addon@2.0.1:
-    resolution: {integrity: sha512-pZ3JDQ9HkGQ3XDMIHvMcDSI4Vbp/LHmwHwiZu+pHLzimtI1vhAo1swjFEDAEJuBcozljYvREEC4sS7rQHPNtWg==}
+  msw-storybook-addon@2.0.3:
+    resolution: {integrity: sha512-CzHmGO32JeOPnyUnRWnB0PFTXCY1HKfHiEB/6fYoUYiFm2NYosLjzs9aBd3XJUryYEN0avJqMNh7nCRDxE5JjQ==}
     peerDependencies:
       msw: ^2.0.0
 
-  msw@2.2.14:
-    resolution: {integrity: sha512-64i8rNCa1xzDK8ZYsTrVMli05D687jty8+Th+PU5VTbJ2/4P7fkQFVyDQ6ZFT5FrNR8z2BHhbY47fKNvfHrumA==}
+  msw@2.3.4:
+    resolution: {integrity: sha512-sHMlwrajgmZSA2l1o7qRSe+azm/I+x9lvVVcOxAzi4vCtH8uVPJk1K5BQYDkzGl+tt0RvM9huEXXdeGrgcc79g==}
     engines: {node: '>=18'}
     hasBin: true
     peerDependencies:
@@ -8557,6 +8821,16 @@ packages:
       typescript:
         optional: true
 
+  msw@2.4.9:
+    resolution: {integrity: sha512-1m8xccT6ipN4PTqLinPwmzhxQREuxaEJYdx4nIbggxP8aM7r1e71vE7RtOUSQoAm1LydjGfZKy7370XD/tsuYg==}
+    engines: {node: '>=18'}
+    hasBin: true
+    peerDependencies:
+      typescript: '>= 4.8.x'
+    peerDependenciesMeta:
+      typescript:
+        optional: true
+
   muggle-string@0.4.1:
     resolution: {integrity: sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==}
 
@@ -8578,8 +8852,8 @@ packages:
   mz@2.7.0:
     resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==}
 
-  nan@2.18.0:
-    resolution: {integrity: sha512-W7tfG7vMOGtD30sHoZSSc/JVYiyDPEyQVso/Zz+/uQd0B0L46gtC+pHha5FFMRpil6fm/AoEcRWyOVi4+E/f8w==}
+  nan@2.20.0:
+    resolution: {integrity: sha512-bk3gXBZDGILuuo/6sKtr0DQmSThYHLtNCdSdXk9YkxD/jK6X2vmCyyXBBxyqZ4XcnzTyYEAThfX3DCEnLf6igw==}
 
   nanoid@3.3.7:
     resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==}
@@ -8619,9 +8893,6 @@ packages:
     resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==}
     engines: {node: '>= 0.6'}
 
-  neo-async@2.6.2:
-    resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==}
-
   nested-property@4.0.0:
     resolution: {integrity: sha512-yFehXNWRs4cM0+dz7QxCd06hTbWbSkV0ISsqBfkntU6TOY4Qm3Q88fRRLOddkGh2Qq6dZvnKVAahfhjcUvLnyA==}
 
@@ -8650,19 +8921,12 @@ packages:
     resolution: {integrity: sha512-Jx5lPaaLdIaOsj2mVLWMWulXF6GQVdyLvNSxmiYCvZ8Ma2hfKX0POoR2kgKOqz+oFsRreq0yYZjQ2wjE9VNzCA==}
     engines: {node: '>=v0.6.5'}
 
-  node-dir@0.1.17:
-    resolution: {integrity: sha512-tmPX422rYgofd4epzrNoOXiE8XFZYOcCq1vD7MAXCDO+O+zndlA2ztdKKMa+EeuBG5tHETpr4ml4RGgpqDCCAg==}
-    engines: {node: '>= 0.10.5'}
-
   node-domexception@1.0.0:
     resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==}
     engines: {node: '>=10.5.0'}
 
-  node-fetch-native@1.0.2:
-    resolution: {integrity: sha512-KIkvH1jl6b3O7es/0ShyCgWLcfXxlBrLBbP3rOr23WArC66IMcU4DeZEeYEOwnopYhawLTn7/y+YtmASe8DFVQ==}
-
-  node-fetch@2.6.11:
-    resolution: {integrity: sha512-4I6pdBY1EthSqDmJkiNk3JIT8cswwR9nfeW/cPdUagJYEQG7R95WRH74wpz7ma8Gh/9dI9FP+OU+0E4FvtA55w==}
+  node-fetch@2.6.13:
+    resolution: {integrity: sha512-StxNAxh15zr77QvvkmveSQ8uCQ4+v5FkvNTj0OESmiHu+VRi/gXArXtkWMElOsOUNLtUEvI4yS+rdtOHZTwlQA==}
     engines: {node: 4.x || >=6.0.0}
     peerDependencies:
       encoding: ^0.1.0
@@ -8691,8 +8955,8 @@ packages:
     resolution: {integrity: sha512-NTZVKn9IylLwUzaKjkas1e4u2DLNcV4rdYagA4PWdPwW87Bi7z+BznyKSRwS/761tV/lzCGXplWsiaMjLqP2zQ==}
     hasBin: true
 
-  node-gyp@10.0.1:
-    resolution: {integrity: sha512-gg3/bHehQfZivQVfqIyy8wTdSymF9yTyP4CJifK73imyNMU8AIGQE2pUa7dNWfmMeG9cDVF2eehiRMv0LC1iAg==}
+  node-gyp@10.2.0:
+    resolution: {integrity: sha512-sp3FonBAaFe4aYTcFdZUn2NYkbP7xroPGYvQmP4Nl5PxamznItBnNCgjrVTKrEfQynInMsJvZrdmqUnysCJ8rw==}
     engines: {node: ^16.14.0 || >=18.0.0}
     hasBin: true
 
@@ -8702,8 +8966,8 @@ packages:
   node-releases@2.0.14:
     resolution: {integrity: sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==}
 
-  nodemailer@6.9.13:
-    resolution: {integrity: sha512-7o38Yogx6krdoBf3jCAqnIN4oSQFx+fMa0I7dK1D+me9kBxx12D+/33wSb+fhOCtIxvYJ+4x4IMEhmhCKfAiOA==}
+  nodemailer@6.9.15:
+    resolution: {integrity: sha512-AHf04ySLC6CIfuRtRiEYtGEXgRfa6INgWGluDhnxTZhHSKvrBu7lc1VVchQ0d8nPc4cFaZoPq8vkyNoZr0TpGQ==}
     engines: {node: '>=6.0.0'}
 
   nodemon@3.0.2:
@@ -8711,8 +8975,8 @@ packages:
     engines: {node: '>=10'}
     hasBin: true
 
-  nodemon@3.1.0:
-    resolution: {integrity: sha512-xqlktYlDMCepBJd43ZQhjWwMw2obW/JRvkrLxq5RCNcuDDX1DbcPT+qT1IlIIdf+DhnWs90JpTMe+Y5KxOchvA==}
+  nodemon@3.1.7:
+    resolution: {integrity: sha512-hLj7fuMow6f0lbB0cD14Lz2xNjwsyruH251Pk4t/yIitCFJbmY1myuLlHm/q06aST4jg6EgAh74PIBBrRqpVAQ==}
     engines: {node: '>=10'}
     hasBin: true
 
@@ -8754,8 +9018,8 @@ packages:
     resolution: {integrity: sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==}
     engines: {node: '>=10'}
 
-  normalize-url@8.0.0:
-    resolution: {integrity: sha512-uVFpKhj5MheNBJRTiMZ9pE/7hD1QTeEvugSJW/OmLzAp78PB5O6adfMNTvmfKhXBkvCzC+rqifWcVYpGFwTjnw==}
+  normalize-url@8.0.1:
+    resolution: {integrity: sha512-IO9QvjUMWxPQQhs60oOu10CRkWCiZzSUkzbXGGV9pviYl1fXYcvkzQ5jV9z8Y6un8ARoVRl4EtC6v6jNqbaJ/w==}
     engines: {node: '>=14.16'}
 
   npm-run-path@2.0.2:
@@ -8770,8 +9034,17 @@ packages:
     resolution: {integrity: sha512-sJOdmRGrY2sjNTRMbSvluQqg+8X7ZK61yvzBEIDhz4f8z1TZFYABsqjjCBd/0PUNE9M6QDgHJXQkGUEm7Q+l9Q==}
     engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
 
+  npm-run-path@5.3.0:
+    resolution: {integrity: sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==}
+    engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
+
+  npm-run-path@6.0.0:
+    resolution: {integrity: sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==}
+    engines: {node: '>=18'}
+
   npmlog@5.0.1:
     resolution: {integrity: sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==}
+    deprecated: This package is no longer supported.
 
   nsfwjs@2.4.2:
     resolution: {integrity: sha512-i4Pp2yt59qPQgeZFyg3wXFBX52uSeu/hkDoqdZfe+sILRxNBUu0VDogj7Lmqak0GlrXviS/wLiVeIx40IDUu7A==}
@@ -8781,8 +9054,8 @@ packages:
   nth-check@2.1.1:
     resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==}
 
-  nwsapi@2.2.9:
-    resolution: {integrity: sha512-2f3F0SEEer8bBu0dsNCFF50N0cTThV1nWFYcEYFZttdW0lDAoybv9cQoK7X7/68Z89S7FoRrVjP1LPX4XRf9vg==}
+  nwsapi@2.2.12:
+    resolution: {integrity: sha512-qXDmcVlZV4XRtKFzddidpfVP4oMSGhga+xdMc25mv8kaLUHtgzCDhUxkrN8exkGdTlLNaXj7CV3GtON7zuGZ+w==}
 
   oauth-sign@0.9.0:
     resolution: {integrity: sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==}
@@ -8804,6 +9077,10 @@ packages:
   object-inspect@1.12.3:
     resolution: {integrity: sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==}
 
+  object-inspect@1.13.2:
+    resolution: {integrity: sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==}
+    engines: {node: '>= 0.4'}
+
   object-is@1.1.5:
     resolution: {integrity: sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw==}
     engines: {node: '>= 0.4'}
@@ -8816,15 +9093,20 @@ packages:
     resolution: {integrity: sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==}
     engines: {node: '>= 0.4'}
 
-  object.fromentries@2.0.7:
-    resolution: {integrity: sha512-UPbPHML6sL8PI/mOqPwsH4G6iyXcCGzLin8KvEPenOZN5lpCNBZZQ+V62vdjB1mQHrmqGQt5/OJzemUA+KJmEA==}
+  object.assign@4.1.5:
+    resolution: {integrity: sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==}
     engines: {node: '>= 0.4'}
 
-  object.groupby@1.0.1:
-    resolution: {integrity: sha512-HqaQtqLnp/8Bn4GL16cj+CUYbnpe1bh0TtEaWvybszDG4tgxCJuRpV8VGuvNaI1fAnI4lUJzDG55MXcOH4JZcQ==}
+  object.fromentries@2.0.8:
+    resolution: {integrity: sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==}
+    engines: {node: '>= 0.4'}
 
-  object.values@1.1.7:
-    resolution: {integrity: sha512-aU6xnDFYT3x17e/f0IiiwlGPTy2jzMySGfUB4fq6z7CV8l85CWHDk5ErhyhpfDHhrOMwGFhSQkhMGHaIotA6Ng==}
+  object.groupby@1.0.3:
+    resolution: {integrity: sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==}
+    engines: {node: '>= 0.4'}
+
+  object.values@1.2.0:
+    resolution: {integrity: sha512-yBYjY9QX2hnRmZHAjG/f13MzmBzxzYgQhFrke06TTyKY5zSTEqkOeukBzIdVA3j3ulu8Qa3MbVFShV7T2RmGtQ==}
     engines: {node: '>= 0.4'}
 
   obliterator@2.0.4:
@@ -8847,10 +9129,6 @@ packages:
     resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==}
     engines: {node: '>= 0.8'}
 
-  on-headers@1.0.2:
-    resolution: {integrity: sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==}
-    engines: {node: '>= 0.8'}
-
   once@1.4.0:
     resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
 
@@ -8862,6 +9140,9 @@ packages:
     resolution: {integrity: sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==}
     engines: {node: '>=12'}
 
+  oniguruma-to-js@0.4.3:
+    resolution: {integrity: sha512-X0jWUcAlxORhOqqBREgPMgnshB7ZGYszBNspP+tS9hPD3l13CdaXcHbgImoHUHlrvGx/7AvFEkTRhAGYh+jzjQ==}
+
   open@8.4.2:
     resolution: {integrity: sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==}
     engines: {node: '>=12'}
@@ -8873,18 +9154,16 @@ packages:
     resolution: {integrity: sha512-es3mGcDXV6TKPo6n3aohzHm0qxhLyR39MhF6mkD1FwFGjhxnqMqfSIgM0eCpInZvqatve4CxmXcMZw3jnnsaXw==}
     hasBin: true
 
-  opentelemetry-instrumentation-fetch-node@1.2.0:
-    resolution: {integrity: sha512-aiSt/4ubOTyb1N5C2ZbGrBvaJOXIZhZvpRPYuUVxQJe27wJZqf/o65iPrqgLcgfeOLaQ8cS2Q+762jrYvniTrA==}
+  opentelemetry-instrumentation-fetch-node@1.2.3:
+    resolution: {integrity: sha512-Qb11T7KvoCevMaSeuamcLsAD+pZnavkhDnlVL0kRozfhl42dKG5Q3anUklAFKJZjY3twLR+BnRa6DlwwkIE/+A==}
     engines: {node: '>18.0.0'}
+    peerDependencies:
+      '@opentelemetry/api': ^1.6.0
 
-  optionator@0.9.3:
-    resolution: {integrity: sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==}
+  optionator@0.9.4:
+    resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
     engines: {node: '>= 0.8.0'}
 
-  ora@5.4.1:
-    resolution: {integrity: sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==}
-    engines: {node: '>=10'}
-
   os-filter-obj@2.0.0:
     resolution: {integrity: sha512-uksVLsqG3pVdzzPvmAHpBK0wKxYItuzZr7SziusRPoz67tGV8rL1szZ6IdeUrbqLjGDwApBtN29eEE3IqGHOjg==}
     engines: {node: '>=4'}
@@ -8895,12 +9174,15 @@ packages:
   ospath@1.2.2:
     resolution: {integrity: sha512-o6E5qJV5zkAbIDNhGSIlyOhScKXgQrSRMilfph0clDfM0nEnBOlKlH4sWDmG95BW/CvwNz0vmm7dJVtU2KlMiA==}
 
-  otpauth@9.2.3:
-    resolution: {integrity: sha512-oAG55Ch4MBL5Jdg+RXfKiRCZ2lCwa/UIQKsmSfYbGGLSI4dErY1HPZv0JGPPESIYGyDO3s9iJqM4HU/1IppMoQ==}
+  otpauth@9.3.4:
+    resolution: {integrity: sha512-qXv+lpsCUO9ewitLYfeDKbLYt7UUCivnU/fwGK2OqhgrCBsRkTUNKWsgKAhkXG3aistOY+jEeuL90JEBu6W3mQ==}
 
   outvariant@1.4.2:
     resolution: {integrity: sha512-Ou3dJ6bA/UJ5GVHxah4LnqDwZRwAmWxrG3wtrHrbGnP4RnLCtA64A4F+ae7Y8ww660JaddSoArUR5HjipWSHAQ==}
 
+  outvariant@1.4.3:
+    resolution: {integrity: sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==}
+
   p-cancelable@2.1.1:
     resolution: {integrity: sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==}
     engines: {node: '>=8'}
@@ -8925,13 +9207,9 @@ packages:
     resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==}
     engines: {node: '>=10'}
 
-  p-limit@4.0.0:
-    resolution: {integrity: sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==}
-    engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
-
-  p-locate@3.0.0:
-    resolution: {integrity: sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==}
-    engines: {node: '>=6'}
+  p-limit@5.0.0:
+    resolution: {integrity: sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ==}
+    engines: {node: '>=18'}
 
   p-locate@4.1.0:
     resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==}
@@ -8957,8 +9235,8 @@ packages:
     resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==}
     engines: {node: '>=6'}
 
-  pako@0.2.9:
-    resolution: {integrity: sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==}
+  package-json-from-dist@1.0.0:
+    resolution: {integrity: sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==}
 
   parent-module@1.0.1:
     resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
@@ -8971,6 +9249,10 @@ packages:
     resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==}
     engines: {node: '>=8'}
 
+  parse-ms@4.0.0:
+    resolution: {integrity: sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==}
+    engines: {node: '>=18'}
+
   parse-srcset@1.0.2:
     resolution: {integrity: sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q==}
 
@@ -8980,6 +9262,9 @@ packages:
   parse5-htmlparser2-tree-adapter@7.0.0:
     resolution: {integrity: sha512-B77tOZrqqfUfnVcOrUvfdLbz4pu4RopLD/4vmu3HUPswwTA8OH0EMW9BlWR2B0RCoiZRAHEUu7IxeP1Pd1UU+g==}
 
+  parse5-parser-stream@7.1.2:
+    resolution: {integrity: sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==}
+
   parse5@5.1.1:
     resolution: {integrity: sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug==}
 
@@ -8996,10 +9281,6 @@ packages:
   path-browserify@1.0.1:
     resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==}
 
-  path-exists@3.0.0:
-    resolution: {integrity: sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==}
-    engines: {node: '>=4'}
-
   path-exists@4.0.0:
     resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==}
     engines: {node: '>=8'}
@@ -9027,22 +9308,25 @@ packages:
     resolution: {integrity: sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==}
     engines: {node: '>=16 || 14 >=14.17'}
 
-  path-scurry@1.10.2:
-    resolution: {integrity: sha512-7xTavNy5RQXnsjANvVvMkEjvloOinkAjv/Z6Ildz9v2RinZ4SBKTWFOVRbaF8p0vpHnyjV/UwNDdKuUv6M5qcA==}
-    engines: {node: '>=16 || 14 >=14.17'}
+  path-scurry@2.0.0:
+    resolution: {integrity: sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==}
+    engines: {node: 20 || >=22}
 
-  path-to-regexp@0.1.7:
-    resolution: {integrity: sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==}
+  path-to-regexp@0.1.10:
+    resolution: {integrity: sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==}
 
   path-to-regexp@1.8.0:
     resolution: {integrity: sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==}
 
-  path-to-regexp@3.2.0:
-    resolution: {integrity: sha512-jczvQbCUS7XmS7o+y1aEO9OBVFeZBQ1MDSEqmO7xSoPgOPoowY/SxLpZ6Vh97/8qHZOteiCKb7gkG9gA2ZUxJA==}
+  path-to-regexp@3.3.0:
+    resolution: {integrity: sha512-qyCH421YQPS2WFDxDjftfc1ZR5WKQzVzqsp4n9M2kQhVOo/ByahFoUNJfl58kOcEGfQ//7weFTDhm+ss8Ecxgw==}
 
   path-to-regexp@6.2.1:
     resolution: {integrity: sha512-JLyh7xT1kizaEvcaXOQwOc2/Yhw6KZOvPf1S8401UyLk86CU79LN3vl7ztXGm/pZ+YjoyAJ4rxmHwbkBXJX+yw==}
 
+  path-to-regexp@6.3.0:
+    resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==}
+
   path-type@4.0.0:
     resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==}
     engines: {node: '>=8'}
@@ -9053,15 +9337,20 @@ packages:
   pathval@1.1.1:
     resolution: {integrity: sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==}
 
+  pathval@2.0.0:
+    resolution: {integrity: sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==}
+    engines: {node: '>= 14.16'}
+
   pause-stream@0.0.11:
     resolution: {integrity: sha512-e3FBlXLmN/D1S+zHzanP4E/4Z60oFAa3O051qt1pxa7DEJWKAyil6upYVXCWadEnuoqa4Pkc9oUx9zsxYeRv8A==}
 
-  peek-readable@5.0.0:
-    resolution: {integrity: sha512-YtCKvLUOvwtMGmrniQPdO7MwPjgkFBtFIrmfSbYmYuq3tKDV/mcfAhBth1+C3ru7uXIZasc/pHnb+YDYNkkj4A==}
+  peek-readable@5.1.3:
+    resolution: {integrity: sha512-kCsc9HwH5RgVA3H3VqkWFyGQwsxUxLdiSX1d5nqAm7hnMFjNFX1VhBLmJoUY0hZNc8gmDNgBkLjfhiWPsziXWA==}
     engines: {node: '>=14.16'}
 
-  peek-stream@1.1.3:
-    resolution: {integrity: sha512-FhJ+YbOSBb9/rIl2ZeE/QHEsWn7PqNYt8ARAY3kIgNGOk13g9FGyIY6JIl/xB/3TFRVoTv5as0l11weORrTekA==}
+  peek-readable@5.2.0:
+    resolution: {integrity: sha512-U94a+eXHzct7vAd19GH3UQ2dH4Satbng0MyYTMaQatL0pvYYL5CTPR25HBhKtecl+4bfu1/i3vC6k0hydO5Vcw==}
+    engines: {node: '>=14.16'}
 
   pend@1.2.0:
     resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==}
@@ -9072,8 +9361,8 @@ packages:
   pg-cloudflare@1.1.1:
     resolution: {integrity: sha512-xWPagP/4B6BgFO+EKz3JONXv3YDgvkbVrGw2mTo3D6tVDQRh1e7cqVGvyR3BE+eQgAvx1XhW/iEASj4/jCWl3Q==}
 
-  pg-connection-string@2.6.4:
-    resolution: {integrity: sha512-v+Z7W/0EO707aNMaAEfiGnGL9sxxumwLl2fJvCQtMn9Fxsg+lPpPkdcyBSv/KFgpGdYkMfn+EI1Or2EHjpgLCA==}
+  pg-connection-string@2.7.0:
+    resolution: {integrity: sha512-PI2W9mv53rXJQEOb8xNR8lH7Hr+EKa6oJa38zsK0S/ky2er16ios1wLKhZyxzD7jUReiWokc9WK5nxSnC7W1TA==}
 
   pg-int8@1.0.1:
     resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==}
@@ -9083,17 +9372,17 @@ packages:
     resolution: {integrity: sha512-BM/Thnrw5jm2kKLE5uJkXqqExRUY/toLHda65XgFTBTFYZyopbKjBe29Ii3RbkvlsMoFwD+tHeGaCjjv0gHlyw==}
     engines: {node: '>=4'}
 
-  pg-pool@3.6.2:
-    resolution: {integrity: sha512-Htjbg8BlwXqSBQ9V8Vjtc+vzf/6fVUuak/3/XXKA9oxZprwW3IMDQTGHP+KDmVL7rtd+R1QjbnCFPuTHm3G4hg==}
+  pg-pool@3.7.0:
+    resolution: {integrity: sha512-ZOBQForurqh4zZWjrgSwwAtzJ7QiRX0ovFkZr2klsen3Nm0aoh33Ls0fzfv3imeH/nw/O27cjdz5kzYJfeGp/g==}
     peerDependencies:
       pg: '>=8.0'
 
-  pg-protocol@1.6.0:
-    resolution: {integrity: sha512-M+PDm637OY5WM307051+bsDia5Xej6d9IR4GwJse1qA1DIhiKlksvrneZOYQq42OM+spubpcNYEo2FcKQrDk+Q==}
-
   pg-protocol@1.6.1:
     resolution: {integrity: sha512-jPIlvgoD63hrEuihvIg+tJhoGjUsLPn6poJY9N5CnlPd91c2T18T/9zBtLxZSb1EhYxBRoZJtzScCaWlYLtktg==}
 
+  pg-protocol@1.7.0:
+    resolution: {integrity: sha512-hTK/mE36i8fDDhgDFjy6xNOG+LCorxLG3WO17tku+ij6sVHXh1jQUJ8hYAnRhNla4QVD2H8er/FOjc/+EgC6yQ==}
+
   pg-types@2.2.0:
     resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==}
     engines: {node: '>=4'}
@@ -9102,8 +9391,8 @@ packages:
     resolution: {integrity: sha512-hRCSDuLII9/LE3smys1hRHcu5QGcLs9ggT7I/TCs0IE+2Eesxi9+9RWAAwZ0yaGjxoWICF/YHLOEjydGujoJ+g==}
     engines: {node: '>=10'}
 
-  pg@8.11.5:
-    resolution: {integrity: sha512-jqgNHSKL5cbDjFlHyYsCXmQDrfIX/3RsNwYqpd4N0Kt8niLuNoRNH+aazv6cOd43gPh9Y4DjQCtb+X0MH0Hvnw==}
+  pg@8.13.0:
+    resolution: {integrity: sha512-34wkUTh3SxTClfoHB3pQ7bIMvw9dpFU1audQQeZG837fmHfHpr14n/AELVDoOYVDW2h5RDWU78tFjkD+erSBsw==}
     engines: {node: '>= 8.0.0'}
     peerDependencies:
       pg-native: '>=3.0.1'
@@ -9114,13 +9403,19 @@ packages:
   pgpass@1.0.5:
     resolution: {integrity: sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==}
 
-  photoswipe@5.4.3:
-    resolution: {integrity: sha512-9UC6oJBK4oXFZ5HcdlcvGkfEHsVrmE4csUdCQhEjHYb3PvPLO3PG7UhnPuOgjxwmhq5s17Un5NUdum01LgBDng==}
+  photoswipe@5.4.4:
+    resolution: {integrity: sha512-WNFHoKrkZNnvFFhbHL93WDkW3ifwVOXSW3w1UuZZelSmgXpIGiZSNlZJq37rR8YejqME2rHs9EhH9ZvlvFH2NA==}
     engines: {node: '>= 0.12.0'}
 
   picocolors@1.0.0:
     resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==}
 
+  picocolors@1.0.1:
+    resolution: {integrity: sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==}
+
+  picocolors@1.1.0:
+    resolution: {integrity: sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==}
+
   picomatch@2.3.1:
     resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==}
     engines: {node: '>=8.6'}
@@ -9133,18 +9428,14 @@ packages:
     resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==}
     engines: {node: '>=0.10.0'}
 
-  pify@4.0.1:
-    resolution: {integrity: sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==}
-    engines: {node: '>=6'}
+  pino-abstract-transport@1.2.0:
+    resolution: {integrity: sha512-Guhh8EZfPCfH+PMXAb6rKOjGQEoy0xlAIn+irODG5kgfYV+BQ0rGYYWTIel3P5mmyXqkYkPmdIkywsn6QKUR1Q==}
 
-  pino-abstract-transport@1.1.0:
-    resolution: {integrity: sha512-lsleG3/2a/JIWUtf9Q5gUNErBqwIu1tUKTT3dUzaf5DySw9ra1wcqKjJjLX1VTY64Wk1eEOYsVGSaGfCK85ekA==}
+  pino-std-serializers@7.0.0:
+    resolution: {integrity: sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA==}
 
-  pino-std-serializers@6.1.0:
-    resolution: {integrity: sha512-KO0m2f1HkrPe9S0ldjx7za9BJjeHqBku5Ch8JyxETxT8dEFGz1PwgrHaOQupVYitpzbFSYm7nnljxD8dik2c+g==}
-
-  pino@8.17.0:
-    resolution: {integrity: sha512-ey+Mku+PVPhvxglLXMg1l1zQMwSHuNrKC3MD40EDZbkckJmmuY7DYZLIOwwjZ8ix/Nvhe9dZt5H99cgkot9bAw==}
+  pino@9.2.0:
+    resolution: {integrity: sha512-g3/hpwfujK5a4oVbaefoJxezLzsDgLcNJeITvC6yrfwYeT9la+edCK42j5QpEQSQCZgTKapXvnQIdgZwvRaZug==}
     hasBin: true
 
   pirates@4.0.5:
@@ -9158,18 +9449,10 @@ packages:
     resolution: {integrity: sha512-ZBmhE1C9LcPoH9XZSdwiPtbPHZROwAnMy+kIFQVrnMCxY4Cudlz3gBOpzilgc0jOgRaiT3sIWfpMomW2ar2orQ==}
     engines: {node: '>=16.20.0'}
 
-  pkg-dir@3.0.0:
-    resolution: {integrity: sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==}
-    engines: {node: '>=6'}
-
   pkg-dir@4.2.0:
     resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==}
     engines: {node: '>=8'}
 
-  pkg-dir@5.0.0:
-    resolution: {integrity: sha512-NPE8TDbzl/3YQYY7CSS228s3g2ollTFnc+Qi3tqmqJp9Vg2ovUpixcJEo2HJScN2Ez+kEaal6y70c0ehqJBJeA==}
-    engines: {node: '>=10'}
-
   pkg-types@1.0.3:
     resolution: {integrity: sha512-nN7pYi0AQqJnoLPC9eHFQ8AcyaixBUOwvqc5TDnIKCMEE6I0y8P7OKA7fPexsXGCGxQDl/cmrLAp26LhcwxZ4A==}
 
@@ -9196,6 +9479,10 @@ packages:
     resolution: {integrity: sha512-Sz2Lkdxz6F2Pgnpi9U5Ng/WdWAUZxmHrNPoVlm3aAemxoy2Qy7LGjQg4uf8qKelDAUW94F4np3iH2YPf2qefcQ==}
     engines: {node: '>=10'}
 
+  possible-typed-array-names@1.0.0:
+    resolution: {integrity: sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==}
+    engines: {node: '>= 0.4'}
+
   postcss-calc@9.0.1:
     resolution: {integrity: sha512-TipgjGyzP5QzEhsOZUaIkeO5mKeMFpebWzRogWG/ysonUlnHcq5aJe0jOjpfzUU8PeSaBQnrE8ehR0QA5vs8PQ==}
     engines: {node: ^14 || ^16 || >=18.0}
@@ -9346,10 +9633,6 @@ packages:
     peerDependencies:
       postcss: ^8.4.31
 
-  postcss-selector-parser@6.0.15:
-    resolution: {integrity: sha512-rEYkQOMUCEMhsKbK66tbEU9QVIxbhN18YiniAwA7XQYTVBqrBy+P2p5JcdqsHgKM2zWylp8d7J6eszocfds5Sw==}
-    engines: {node: '>=4'}
-
   postcss-selector-parser@6.0.16:
     resolution: {integrity: sha512-A0RVJrX+IUkVZbW3ClroRWurercFhieevHB38sr2+l9eUClMqome3LmEmnhlNy+5Mr2EYN6B2Kaw9wYdd+VHiw==}
     engines: {node: '>=4'}
@@ -9369,8 +9652,8 @@ packages:
   postcss-value-parser@4.2.0:
     resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==}
 
-  postcss@8.4.38:
-    resolution: {integrity: sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==}
+  postcss@8.4.47:
+    resolution: {integrity: sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==}
     engines: {node: ^10 || ^12 || >=14}
 
   postgres-array@2.0.0:
@@ -9412,8 +9695,8 @@ packages:
     resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
     engines: {node: '>= 0.8.0'}
 
-  prettier@3.2.5:
-    resolution: {integrity: sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==}
+  prettier@3.3.3:
+    resolution: {integrity: sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==}
     engines: {node: '>=14'}
     hasBin: true
 
@@ -9429,9 +9712,9 @@ packages:
     resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==}
     engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
 
-  pretty-hrtime@1.0.3:
-    resolution: {integrity: sha512-66hKPCr+72mlfiSjlEB1+45IjXSqvVAIy6mocupoww4tBFE9R9IhwwUGoI4G++Tc9Aq+2rxOt0RFU6gPcrte0A==}
-    engines: {node: '>= 0.8'}
+  pretty-ms@9.0.0:
+    resolution: {integrity: sha512-E9e9HJ9R9NasGOgPaPE8VMeiPKAyWR5jcFpNnwIejslIhWqdqOrb2wShBsncMPUb+BcCd2OPYfh7p2W6oemTng==}
+    engines: {node: '>=18'}
 
   private-ip@2.3.3:
     resolution: {integrity: sha512-5zyFfekIVUOTVbL92hc8LJOtE/gyGHeREHkJ2yTyByP8Q2YZVoBqLg3EfYLeF0oVvGqtaEX2t2Qovja0/gStXw==}
@@ -9439,8 +9722,8 @@ packages:
   probe-image-size@7.2.3:
     resolution: {integrity: sha512-HubhG4Rb2UH8YtV4ba0Vp5bQ7L78RTONYu/ujmCu5nBI8wGv24s4E9xSKBi0N1MowRpxk76pFCpJtW0KPzOK0w==}
 
-  proc-log@3.0.0:
-    resolution: {integrity: sha512-++Vn7NS4Xf9NacaU9Xq3URUuqZETPsf8L4j5/ckhaRYsfPeRyzGw+iDjFhV/Jr3uNmTvvddEJFWh5R1gRgUH8A==}
+  proc-log@4.2.0:
+    resolution: {integrity: sha512-g8+OnU/L2v+wyiVK+D5fA34J7EH8jZ8DDlvwhRCMxmMj7UCBvxiO1mGeN+36JXIKF4zevU4kRBd8lVgG9vLelA==}
     engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
 
   process-exists@5.0.0:
@@ -9450,12 +9733,12 @@ packages:
   process-nextick-args@2.0.1:
     resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==}
 
-  process-warning@2.2.0:
-    resolution: {integrity: sha512-/1WZ8+VQjR6avWOgHeEPd7SDQmFQ1B5mC1eRXsCm5TarlNmx/wCsa5GEaxGm05BORRtyG/Ex/3xq3TuRvq57qg==}
-
   process-warning@3.0.0:
     resolution: {integrity: sha512-mqn0kFRl0EoqhnL0GQ0veqFHyIN1yig9RHh/InzORTUiZHFRAur+aMtRkELNwGs9aNwKS6tg/An4NYBPGwvtzQ==}
 
+  process-warning@4.0.0:
+    resolution: {integrity: sha512-/MyYDxttz7DfGMMHiysAsFE4qF+pQYAA8ziO/3NcRVrQ5fSk+Mns4QZA/oRPFzvcqNoVJXQNWNAsdwBXLUkQKw==}
+
   process@0.11.10:
     resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==}
     engines: {node: '>= 0.6.0'}
@@ -9484,6 +9767,9 @@ packages:
   prop-types@15.8.1:
     resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==}
 
+  property-information@6.5.0:
+    resolution: {integrity: sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==}
+
   proto-list@1.2.4:
     resolution: {integrity: sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==}
 
@@ -9518,12 +9804,15 @@ packages:
   pug-attrs@3.0.0:
     resolution: {integrity: sha512-azINV9dUtzPMFQktvTXciNAfAuVh/L/JCl0vtPCwvOA21uZrC08K/UnmrL+SXGEVc1FwzjW62+xw5S/uaLj6cA==}
 
-  pug-code-gen@3.0.2:
-    resolution: {integrity: sha512-nJMhW16MbiGRiyR4miDTQMRWDgKplnHyeLvioEJYbk1RsPI3FuA3saEP8uwnTb2nTJEKBU90NFVWJBk4OU5qyg==}
+  pug-code-gen@3.0.3:
+    resolution: {integrity: sha512-cYQg0JW0w32Ux+XTeZnBEeuWrAY7/HNE6TWnhiHGnnRYlCgyAUPoyh9KzCMa9WhcJlJ1AtQqpEYHc+vbCzA+Aw==}
 
   pug-error@2.0.0:
     resolution: {integrity: sha512-sjiUsi9M4RAGHktC1drQfCr5C5eriu24Lfbt4s+7SykztEOwVZtbFk1RRq0tzLxcMxMYTBR+zMQaG07J/btayQ==}
 
+  pug-error@2.1.0:
+    resolution: {integrity: sha512-lv7sU9e5Jk8IeUheHata6/UThZ7RK2jnaaNztxfPYUY+VxZyk/ePVaNZ/vwmH8WqGvDz3LrNYt/+gA55NDg6Pg==}
+
   pug-filters@4.0.0:
     resolution: {integrity: sha512-yeNFtq5Yxmfz0f9z2rMXGw/8/4i1cCFecw/Q7+D0V2DdtII5UvqE12VaZ2AY7ri6o5RNXiweGH79OCq+2RQU4A==}
 
@@ -9548,18 +9837,12 @@ packages:
   pug-walk@2.0.0:
     resolution: {integrity: sha512-yYELe9Q5q9IQhuvqsZNwA5hfPkMJ8u92bQLIMcsMxf/VADjNtEYptU+inlufAFYcWdHlwNfZOEnOOQrZrcyJCQ==}
 
-  pug@3.0.2:
-    resolution: {integrity: sha512-bp0I/hiK1D1vChHh6EfDxtndHji55XP/ZJKwsRqrz6lRia6ZC2OZbdAymlxdVFwd1L70ebrVJw4/eZ79skrIaw==}
-
-  pump@2.0.1:
-    resolution: {integrity: sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==}
+  pug@3.0.3:
+    resolution: {integrity: sha512-uBi6kmc9f3SZ3PXxqcHiUZLmIXgfgWooKWXcwSGwQd2Zi5Rb0bT14+8CJjJgI8AB+nndLaNgHGrcc6bPIB665g==}
 
   pump@3.0.0:
     resolution: {integrity: sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==}
 
-  pumpify@1.5.1:
-    resolution: {integrity: sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ==}
-
   punycode@2.3.1:
     resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
     engines: {node: '>=6'}
@@ -9574,21 +9857,13 @@ packages:
     resolution: {integrity: sha512-pMpnA0qRdFp32b1sJl1wOJNxZLQ2cbQx+k6tjNtZ8CpvVhNqEPRgivZ2WOUev2YMajecdH7ctUPDvEe87nariQ==}
     engines: {node: '>=6.0.0'}
 
-  qrcode@1.5.3:
-    resolution: {integrity: sha512-puyri6ApkEHYiVl4CFzo1tDkAZ+ATcnbJrJ6RiBM1Fhctdn/ix9MTE3hRph33omisEbC/2fcfemsseiKgBPKZg==}
+  qrcode@1.5.4:
+    resolution: {integrity: sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==}
     engines: {node: '>=10.13.0'}
     hasBin: true
 
-  qs@6.10.4:
-    resolution: {integrity: sha512-OQiU+C+Ds5qiH91qh/mg0w+8nwQuLjM4F4M/PbmhDOoYehPh+Fb0bDjtR1sOvy7YKxvj28Y/M0PhP5uVX0kB+g==}
-    engines: {node: '>=0.6'}
-
-  qs@6.11.0:
-    resolution: {integrity: sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==}
-    engines: {node: '>=0.6'}
-
-  qs@6.11.1:
-    resolution: {integrity: sha512-0wsrzgTz/kAVIeuxSjnpGC56rzYtr6JT/2BwEvMaPhFIoYa1aGO8LbzuU1R0uUYQkLpWBTOj0l/CLAJB64J6nQ==}
+  qs@6.13.0:
+    resolution: {integrity: sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==}
     engines: {node: '>=0.6'}
 
   qs@6.5.3:
@@ -9618,9 +9893,6 @@ packages:
     resolution: {integrity: sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==}
     engines: {node: '>=10'}
 
-  ramda@0.29.0:
-    resolution: {integrity: sha512-BBea6L67bYLtdbOqfp8f58fPMqEwx0doL+pAi8TZyp2YWz8R9G8z9x75CZI8W+ftqhFHCpEX2cRnUUXK130iKA==}
-
   random-seed@0.3.0:
     resolution: {integrity: sha512-y13xtn3kcTlLub3HKWXxJNeC2qK4mB59evwZ5EkeRlolx+Bp2ztF7LbcZmyCnOqlHQrLnfuNbi1sVmm9lPDlDA==}
     engines: {node: '>= 0.6.0'}
@@ -9632,20 +9904,20 @@ packages:
   ratelimiter@3.4.1:
     resolution: {integrity: sha512-5FJbRW/Jkkdk29ksedAfWFkQkhbUrMx3QJGwMKAypeIiQf4yrLW+gtPKZiaWt4zPrtw1uGufOjGO7UGM6VllsQ==}
 
-  raw-body@2.5.1:
-    resolution: {integrity: sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==}
-    engines: {node: '>= 0.8'}
-
   raw-body@2.5.2:
     resolution: {integrity: sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==}
     engines: {node: '>= 0.8'}
 
+  raw-body@3.0.0:
+    resolution: {integrity: sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==}
+    engines: {node: '>= 0.8'}
+
   rdf-canonize@3.4.0:
     resolution: {integrity: sha512-fUeWjrkOO0t1rg7B2fdyDTvngj+9RlUyL92vOdiB7c0FPguWVsniIMjEtHH+meLBO9rzkUlUzBVXgWrjI8P9LA==}
     engines: {node: '>=12'}
 
-  re2@1.20.10:
-    resolution: {integrity: sha512-/5JjSPXobSDaKFL6rD5Gb4qD4CVBITQb7NAxfQ/NA7o0HER3SJAPV3lPO2kvzw0/PN1pVJNVATEUk4y9j7oIIA==}
+  re2@1.21.4:
+    resolution: {integrity: sha512-MVIfXWJmsP28mRsSt8HeL750ifb8H5+oF2UDIxGaiJCr8fkMqhLZ7kcX9ADRk2dC8qeGKedB7UVYRfBVpEiLfA==}
 
   react-colorful@5.6.1:
     resolution: {integrity: sha512-1exovf0uGTGyq5mXQT0zgQ80uvj2PCwvF8zY1RN9/vbJVSjSo3fsB/4L3ObbF7u70NduSiK4xu4Y6q1MHoUGEw==}
@@ -9726,10 +9998,6 @@ packages:
     resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==}
     engines: {node: '>= 12.13.0'}
 
-  recast@0.23.4:
-    resolution: {integrity: sha512-qtEDqIZGVcSZCHniWwZWbRy79Dc6Wp3kT/UmDA2RJKBPg7+7k51aQBZirHmUGn5uvHf2rg8DkjizrN26k61ATw==}
-    engines: {node: '>= 4'}
-
   recast@0.23.6:
     resolution: {integrity: sha512-9FHoNjX1yjuesMwuthAmPKabxYQdOgihFYmT5ebXfYGBcnqXZf3WOVz+5foEZ8Y83P4ZY6yQD5GMmtV+pgCCAQ==}
     engines: {node: '>= 4'}
@@ -9759,33 +10027,22 @@ packages:
   reflect-metadata@0.2.2:
     resolution: {integrity: sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==}
 
-  regenerate-unicode-properties@10.1.0:
-    resolution: {integrity: sha512-d1VudCLoIGitcU/hEg2QqvyGZQmdC0Lf8BqdOMXGFSvJP4bNV1+XqbPQeHHLD51Jh4QJJ225dlIFvY4Ly6MXmQ==}
-    engines: {node: '>=4'}
-
-  regenerate@1.4.2:
-    resolution: {integrity: sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==}
-
   regenerator-runtime@0.13.11:
     resolution: {integrity: sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==}
 
   regenerator-runtime@0.14.0:
     resolution: {integrity: sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==}
 
-  regenerator-transform@0.15.2:
-    resolution: {integrity: sha512-hfMp2BoF0qOk3uc5V20ALGDS2ddjQaLrdl7xrGXvAIow7qeWRM2VA2HuCHkUKk9slq3VwEwLNK3DFBqDfPGYtg==}
+  regex@4.3.3:
+    resolution: {integrity: sha512-r/AadFO7owAq1QJVeZ/nq9jNS1vyZt+6t1p/E59B56Rn2GCya+gr1KSyOzNL/er+r+B7phv5jG2xU2Nz1YkmJg==}
 
   regexp.prototype.flags@1.5.0:
     resolution: {integrity: sha512-0SutC3pNudRKgquxGoRGIz946MZVHqbNfPjBdxeOhBrdgDKlRoXmYLQN9xRbrR09ZXWeGAdPuif7egofn6v5LA==}
     engines: {node: '>= 0.4'}
 
-  regexpu-core@5.3.2:
-    resolution: {integrity: sha512-RAM5FlZz+Lhmo7db9L298p2vHP5ZywrVXmVXpmAD9GuL5MPH6t9ROw1iA/wfHkQ76Qe7AaPF0nGuim96/IrQMQ==}
-    engines: {node: '>=4'}
-
-  regjsparser@0.9.1:
-    resolution: {integrity: sha512-dQUtn90WanSNl+7mQKcXAgZxvUe7Z0SqXlgzv0za4LwiUhyzBC58yQO3liFoUgu8GiJVInAhJjkj1N0EtQ5nkQ==}
-    hasBin: true
+  regexp.prototype.flags@1.5.2:
+    resolution: {integrity: sha512-NcDiDkTLuPR+++OCKB0nWafEmhg/Da8aUPLPMQbK+bxKKCm1/S5he+AqYa4PlMCVBalb4/yxIRub6qkEx5yJbw==}
+    engines: {node: '>= 0.4'}
 
   rehype-external-links@3.0.0:
     resolution: {integrity: sha512-yp+e5N9V3C6bwBeAC4n796kc86M4gJCdlVhiMTxIrJG5UHDMh+PJANf9heqORJbt1nrCbDwIlAZKjANIaVBbvw==}
@@ -9853,9 +10110,6 @@ packages:
     resolution: {integrity: sha512-6K/gDlqgQscOlg9fSRpWstA8sYe8rbELsSTNpx+3kTrsVCzvSl0zIvRErM7fdl9ERWDsKnrLnwB+Ne89918XOg==}
     engines: {node: '>=10'}
 
-  resolve@1.19.0:
-    resolution: {integrity: sha512-rArEXAgsBG4UgRGcynxWIWKFvh/XZCcS8UJdHhwy91zwAvCZIbcs+vAbflgBnNjYMs/i/i+/Ux6IZhML1yPvxg==}
-
   resolve@1.22.8:
     resolution: {integrity: sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==}
     hasBin: true
@@ -9871,8 +10125,8 @@ packages:
     resolution: {integrity: sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==}
     engines: {node: '>=8'}
 
-  ret@0.4.3:
-    resolution: {integrity: sha512-0f4Memo5QP7WQyUEAYUO3esD/XjOc3Zjjg5CPsAq1p8sIu0XPeMbHJemKA0BO7tV0X7+A0FoEpbmHXWxPyD3wQ==}
+  ret@0.5.0:
+    resolution: {integrity: sha512-I1XxrZSQ+oErkRR4jYbAyEEu2I0avBvvMM5JN+6EBprOGRCs63ENqZ3vjavq8fBw2+62G5LF5XelKwuJpcvcxw==}
     engines: {node: '>=10'}
 
   retry@0.12.0:
@@ -9886,26 +10140,30 @@ packages:
   rfdc@1.3.0:
     resolution: {integrity: sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==}
 
-  rimraf@2.6.3:
-    resolution: {integrity: sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==}
-    hasBin: true
+  rfdc@1.4.1:
+    resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==}
 
   rimraf@2.7.1:
     resolution: {integrity: sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==}
+    deprecated: Rimraf versions prior to v4 are no longer supported
     hasBin: true
 
   rimraf@3.0.2:
     resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==}
+    deprecated: Rimraf versions prior to v4 are no longer supported
     hasBin: true
 
-  rollup@4.17.2:
-    resolution: {integrity: sha512-/9ClTJPByC0U4zNLowV1tMBe8yMEAxewtR3cUNX5BoEpGH3dQEWpJLr6CLp0fPdYRF/fzVOgvDb1zXuakwF5kQ==}
+  rollup@4.22.5:
+    resolution: {integrity: sha512-WoinX7GeQOFMGznEcWA1WrTQCd/tpEbMkc3nuMs9BT0CPjMdSjPMTVClwWd4pgSQwJdP65SK9mTCNvItlr5o7w==}
     engines: {node: '>=18.0.0', npm: '>=8.0.0'}
     hasBin: true
 
   rrweb-cssom@0.6.0:
     resolution: {integrity: sha512-APM0Gt1KoXBz0iIkkdB/kfvGOwC4UuJFeG/c+yV7wSc7q96cG/kJ0HiYCnzivD9SB53cLV1MlHFNfOuPaadYSw==}
 
+  rrweb-cssom@0.7.1:
+    resolution: {integrity: sha512-TrEMa7JGdVm0UThDJSx7ddw5nVm3UJS9o9CCIZ72B1vSyEZoziDqBYP3XIoi/12lKrJR8rE3jeFHMok2F/Mnsg==}
+
   rss-parser@3.13.0:
     resolution: {integrity: sha512-7jWUBV5yGN3rqMMj7CZufl/291QAhvrrGpDNE4k/02ZchL0npisiYYqULF71jCEKoIiHvK/Q2e6IkDwPziT7+w==}
 
@@ -9919,6 +10177,10 @@ packages:
     resolution: {integrity: sha512-9dVEFruWIsnie89yym+xWTAYASdpw3CJV7Li/6zBewGf9z2i1j31rP6jnY0pHEO4QZh6N0K11bFjWmdR8UGdPQ==}
     engines: {node: '>=0.4'}
 
+  safe-array-concat@1.1.2:
+    resolution: {integrity: sha512-vj6RsCsWBCf19jIeHEfkRMw8DPiBb+DMXklQ/1SGDHOMlHdPUkZXFQ2YdplS23zESTijAcurb1aSgJA3AgMu1Q==}
+    engines: {node: '>=0.4'}
+
   safe-buffer@5.1.2:
     resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==}
 
@@ -9928,8 +10190,12 @@ packages:
   safe-regex-test@1.0.0:
     resolution: {integrity: sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==}
 
-  safe-regex2@3.1.0:
-    resolution: {integrity: sha512-RAAZAGbap2kBfbVhvmnTFv73NWLMvDGOITFYTZBAaY8eR+Ir4ef7Up/e7amo+y1+AH+3PtLkrt9mvcTsG9LXug==}
+  safe-regex-test@1.0.3:
+    resolution: {integrity: sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw==}
+    engines: {node: '>= 0.4'}
+
+  safe-regex2@4.0.0:
+    resolution: {integrity: sha512-Hvjfv25jPDVr3U+4LDzBuZPPOymELG3PYcSk5hcevooo1yxxamQL/bHs/GrEPGmMoMEwRrHVGiCA1pXi97B8Ew==}
 
   safe-stable-stringify@2.4.2:
     resolution: {integrity: sha512-gMxvPJYhP0O9n2pvcfYfIuYgbledAOJFcqRThtPRmjscaipiwcwPPKLytpVzMkG2HAN87Qmo2d4PtGiri1dSLA==}
@@ -9938,11 +10204,16 @@ packages:
   safer-buffer@2.1.2:
     resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
 
-  sanitize-html@2.13.0:
-    resolution: {integrity: sha512-Xff91Z+4Mz5QiNSLdLWwjgBDm5b1RU6xBT0+12rapjiaR7SwfRdjw8f+6Rir2MXKLrDicRFHdb51hGOAxmsUIA==}
+  sanitize-html@2.13.1:
+    resolution: {integrity: sha512-ZXtKq89oue4RP7abL9wp/9URJcqQNABB5GGJ2acW1sdO8JTVl92f4ygD7Yc9Ze09VAZhnt2zegeU0tbNsdcLYg==}
 
-  sass@1.76.0:
-    resolution: {integrity: sha512-nc3LeqvF2FNW5xGF1zxZifdW3ffIz5aBb7I7tSvOoNu7z1RQ6pFt9MBuiPtjgaI62YWrM/txjWlOCFiGtf2xpw==}
+  sass@1.79.3:
+    resolution: {integrity: sha512-m7dZxh0W9EZ3cw50Me5GOuYm/tVAJAn91SUnohLRo9cXBixGUOdvmryN+dXpwR831bhoY3Zv7rEFt85PUwTmzA==}
+    engines: {node: '>=14.0.0'}
+    hasBin: true
+
+  sass@1.79.4:
+    resolution: {integrity: sha512-K0QDSNPXgyqO4GZq2HO5Q70TLxTH6cIT59RdoCHMivrC8rqzaTw5ab9prjz9KUN1El4FLXrBXJhik61JR4HcGg==}
     engines: {node: '>=14.0.0'}
     hasBin: true
 
@@ -9959,6 +10230,9 @@ packages:
   secure-json-parse@2.7.0:
     resolution: {integrity: sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==}
 
+  secure-json-parse@3.0.0:
+    resolution: {integrity: sha512-YO+gVWyp97H+nCG/qdC8X819iKx5g+BpnO9nYT4uFq4uyI0rSxwtx5qD9rGfScg7FGLYu/YBf8uOtwQKv+gq8g==}
+
   seedrandom@3.0.5:
     resolution: {integrity: sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg==}
 
@@ -9988,12 +10262,17 @@ packages:
     engines: {node: '>=10'}
     hasBin: true
 
-  send@0.18.0:
-    resolution: {integrity: sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==}
+  semver@7.6.3:
+    resolution: {integrity: sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==}
+    engines: {node: '>=10'}
+    hasBin: true
+
+  send@0.19.0:
+    resolution: {integrity: sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==}
     engines: {node: '>= 0.8.0'}
 
-  serve-static@1.15.0:
-    resolution: {integrity: sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==}
+  serve-static@1.16.2:
+    resolution: {integrity: sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==}
     engines: {node: '>= 0.8.0'}
 
   set-blocking@2.0.0:
@@ -10002,6 +10281,14 @@ packages:
   set-cookie-parser@2.6.0:
     resolution: {integrity: sha512-RVnVQxTXuerk653XfuliOxBP81Sf0+qfQE73LIYKcyMYHG94AuH0kgrQpRDuTZnSmjpysHmzxJXKNfa6PjFhyQ==}
 
+  set-function-length@1.2.2:
+    resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==}
+    engines: {node: '>= 0.4'}
+
+  set-function-name@2.0.2:
+    resolution: {integrity: sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==}
+    engines: {node: '>= 0.4'}
+
   setimmediate@1.0.5:
     resolution: {integrity: sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==}
 
@@ -10012,13 +10299,9 @@ packages:
     resolution: {integrity: sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==}
     hasBin: true
 
-  shallow-clone@3.0.1:
-    resolution: {integrity: sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==}
-    engines: {node: '>=8'}
-
-  sharp@0.33.3:
-    resolution: {integrity: sha512-vHUeXJU1UvlO/BNwTpT0x/r53WkLUVxrmb5JTgW92fdFCFk0ispLMAeu/jPO2vjkXM1fYUi3K7/qcLF47pwM1A==}
-    engines: {libvips: '>=8.15.2', node: ^18.17.0 || ^20.3.0 || >=21.0.0}
+  sharp@0.33.5:
+    resolution: {integrity: sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==}
+    engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
 
   shebang-command@1.2.0:
     resolution: {integrity: sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==}
@@ -10036,8 +10319,8 @@ packages:
     resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==}
     engines: {node: '>=8'}
 
-  shiki@1.4.0:
-    resolution: {integrity: sha512-5WIn0OL8PWm7JhnTwRWXniy6eEDY234mRrERVlFa646V2ErQqwIFd2UML7e0Pq9eqSKLoMa3Ke+xbsF+DAuy+Q==}
+  shiki@1.21.0:
+    resolution: {integrity: sha512-apCH5BoWTrmHDPGgg3RF8+HAAbEL/CdbYr8rMw7eIrdhCkZHdVGat5mMNlRtd1erNG01VPMIKHNQ0Pj2HMAiog==}
 
   shimmer@1.2.1:
     resolution: {integrity: sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw==}
@@ -10045,6 +10328,10 @@ packages:
   side-channel@1.0.4:
     resolution: {integrity: sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==}
 
+  side-channel@1.0.6:
+    resolution: {integrity: sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==}
+    engines: {node: '>= 0.4'}
+
   siginfo@2.0.0:
     resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==}
 
@@ -10055,8 +10342,8 @@ packages:
     resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==}
     engines: {node: '>=14'}
 
-  simple-oauth2@5.0.0:
-    resolution: {integrity: sha512-8291lo/z5ZdpmiOFzOs1kF3cxn22bMj5FFH+DNUppLJrpoIlM1QnFiE7KpshHu3J3i21TVcx4yW+gXYjdCKDLQ==}
+  simple-oauth2@5.1.0:
+    resolution: {integrity: sha512-gWDa38Ccm4MwlG5U7AlcJxPv3lvr80dU7ARJWrGdgvOKyzSj1gr3GBPN1rABTedAYvC/LsGYoFuFxwDBPtGEbw==}
 
   simple-swizzle@0.2.2:
     resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==}
@@ -10164,6 +10451,9 @@ packages:
     resolution: {integrity: sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==}
     engines: {node: '>=10'}
 
+  slick@1.12.2:
+    resolution: {integrity: sha512-4qdtOGcBjral6YIBCWJ0ljFSKNLz9KkhbWtuGvUyRowl1kxfuE1x/Z/aJcaiilpb3do9bl5K7/1h9XC5wWpY/A==}
+
   smart-buffer@4.2.0:
     resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==}
     engines: {node: '>= 6.0.0', npm: '>= 3.0.0'}
@@ -10176,8 +10466,8 @@ packages:
     resolution: {integrity: sha512-7maUZy1N7uo6+WVEX6psASxtNlKaNVMlGQKkG/63nEDdLOWNbiUMoLK7X4uYoLhQstau72mLgfEWcXcwsaHbYQ==}
     engines: {node: '>= 10.13.0', npm: '>= 3.0.0'}
 
-  sonic-boom@3.7.0:
-    resolution: {integrity: sha512-IudtNvSqA/ObjN97tfgNmOKyDOs4dNcg4cUUsHDebqsgb8wGBBwb31LIgShNO8fye0dFI52X1+tFoKKI6Rq1Gg==}
+  sonic-boom@4.0.1:
+    resolution: {integrity: sha512-hTSD/6JMLyT4r9zeof6UtuBDpjJ9sO08/nmS5djaA9eozT9oOlNdpXSnzcgj4FTqpk3nkLrs61l4gip9r1HCrQ==}
 
   sort-keys-length@1.0.1:
     resolution: {integrity: sha512-GRbEOUqCxemTAk/b32F2xa8wDTs+Z1QHOkbhJDQTvv/6G3ZkbJ+frYWsTcc7cBB3Fu4wy4XlLCuNtJuMn7Gsvw==}
@@ -10190,14 +10480,14 @@ packages:
   sortablejs@1.14.0:
     resolution: {integrity: sha512-pBXvQCs5/33fdN1/39pPL0NZF20LeRbLQ5jtnheIPN9JQAaufGjKdWduZn4U7wCtVuzKhmRkI0DFYHYRbB2H1w==}
 
-  source-map-js@1.0.2:
-    resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==}
-    engines: {node: '>=0.10.0'}
-
   source-map-js@1.2.0:
     resolution: {integrity: sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==}
     engines: {node: '>=0.10.0'}
 
+  source-map-js@1.2.1:
+    resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
+    engines: {node: '>=0.10.0'}
+
   source-map-support@0.5.13:
     resolution: {integrity: sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==}
 
@@ -10237,14 +10527,19 @@ packages:
   sprintf-js@1.0.3:
     resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==}
 
-  sprintf-js@1.1.2:
-    resolution: {integrity: sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug==}
+  sprintf-js@1.1.3:
+    resolution: {integrity: sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==}
 
   sshpk@1.17.0:
     resolution: {integrity: sha512-/9HIEs1ZXGhSPE8X6Ccm7Nam1z8KcoCqPdI7ecm1N33EzAetWahvQWVqLZtaZQ+IDKX4IyA2o0gBzqIMkAagHQ==}
     engines: {node: '>=0.10.0'}
     hasBin: true
 
+  sshpk@1.18.0:
+    resolution: {integrity: sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==}
+    engines: {node: '>=0.10.0'}
+    hasBin: true
+
   ssri@10.0.4:
     resolution: {integrity: sha512-12+IR2CB2C28MMAw0Ncqwj5QbTcs0nGIhgJzYWzDkb21vWmfNI83KS4f3Ci6GI98WreIfG7o9UXp3C0qbpA8nQ==}
     engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
@@ -10259,8 +10554,8 @@ packages:
   standard-as-callback@2.1.0:
     resolution: {integrity: sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==}
 
-  start-server-and-test@2.0.3:
-    resolution: {integrity: sha512-QsVObjfjFZKJE6CS6bSKNwWZCKBG6975/jKRPPGFfFh+yOQglSeGXiNWjzgQNXdphcBI9nXbyso9tPfX4YAUhg==}
+  start-server-and-test@2.0.8:
+    resolution: {integrity: sha512-v2fV6NV2F7tL1ocwfI4Wpait+IKjRbT5l3ZZ+ZikXdMLmxYsS8ynGAsCQAUVXkVyGyS+UibsRnvgHkMvJIvCsw==}
     engines: {node: '>=16'}
     hasBin: true
 
@@ -10275,9 +10570,6 @@ packages:
     resolution: {integrity: sha512-iCGQj+0l0HOdZ2AEeBADlsRC+vsnDsZsbdSiH1yNSjcfKM7fdpCMfqAL/dwF5BLiw/XhRft/Wax6zQbhq2BcjQ==}
     engines: {node: '>= 0.4'}
 
-  store2@2.14.2:
-    resolution: {integrity: sha512-siT1RiqlfQnGqgT/YzXVUNsom9S0H1OX+dpdGN1xkyYATo4I6sep5NmsRD/40s3IIOvlCq6akxkqG82urIZW1w==}
-
   storybook-addon-misskey-theme@https://codeload.github.com/misskey-dev/storybook-addon-misskey-theme/tar.gz/cf583db098365b2ccc81a82f63ca9c93bc32b640:
     resolution: {tarball: https://codeload.github.com/misskey-dev/storybook-addon-misskey-theme/tar.gz/cf583db098365b2ccc81a82f63ca9c93bc32b640}
     version: 0.0.0
@@ -10297,8 +10589,8 @@ packages:
       react-dom:
         optional: true
 
-  storybook@8.0.9:
-    resolution: {integrity: sha512-/Mvij0Br5bUwJpCvqAUZMEDIWmdRxEyllvVj8Ukw5lIWJePxfpSsz4px5jg9+R6B9tO8sQSqjg4HJvQ/pZk8Tg==}
+  storybook@8.3.4:
+    resolution: {integrity: sha512-nzvuK5TsEgJwcWGLGgafabBOxKn37lfJVv7ZoUVPgJIjk2mNRyJDFwYRJzUZaD37eiR/c/lQ6MoaeqlGwiXoxw==}
     hasBin: true
 
   stream-browserify@3.0.0:
@@ -10310,13 +10602,6 @@ packages:
   stream-parser@0.3.1:
     resolution: {integrity: sha512-bJ/HgKq41nlKvlhccD5kaCr/P+Hu0wPNKPJOH7en+YrJu/9EgqUF+88w5Jb6KNcjOFMhfX4B2asfeAtIGuHObQ==}
 
-  stream-shift@1.0.1:
-    resolution: {integrity: sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==}
-
-  stream-wormhole@1.1.0:
-    resolution: {integrity: sha512-gHFfL3px0Kctd6Po0M8TzEvt3De/xu6cnRrjlfYNhwbhLPLwigI2t1nc6jrzNuaYg5C4YF78PPFuQPzRiqn9ew==}
-    engines: {node: '>=4.0.0'}
-
   streamsearch@1.1.0:
     resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==}
     engines: {node: '>=10.0.0'}
@@ -10350,12 +10635,23 @@ packages:
     resolution: {integrity: sha512-p6TmeT1T3411M8Cgg9wBTMRtY2q9+PNy9EV1i2lIXUN/btt763oIfxwN3RR8VU6wHX8j/1CFy0L+YuThm6bgOg==}
     engines: {node: '>= 0.4'}
 
+  string.prototype.trim@1.2.9:
+    resolution: {integrity: sha512-klHuCNxiMZ8MlsOihJhJEBJAiMVqU3Z2nEXWfWnIqjN0gEFS9J9+IxKozWWtQGcgoa1WUZzLjKPTr4ZHNFTFxw==}
+    engines: {node: '>= 0.4'}
+
   string.prototype.trimend@1.0.6:
     resolution: {integrity: sha512-JySq+4mrPf9EsDBEDYMOb/lM7XQLulwg5R/m1r0PXEFqrV0qHvl58sdTilSXtKOflCsK2E8jxf+GKC0T07RWwQ==}
 
+  string.prototype.trimend@1.0.8:
+    resolution: {integrity: sha512-p73uL5VCHCO2BZZ6krwwQE3kCzM7NKmis8S//xEC6fQonchbum4eP6kR4DLEjQFO3Wnj3Fuo8NM0kOSjVdHjZQ==}
+
   string.prototype.trimstart@1.0.6:
     resolution: {integrity: sha512-omqjMDaY92pbn5HOX7f9IccLA+U1tA9GvtU4JrodiXFfYB7jPzzHpRzpglLAjtUV6bB557zwClJezTqnAiYnQA==}
 
+  string.prototype.trimstart@1.0.8:
+    resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==}
+    engines: {node: '>= 0.4'}
+
   string_decoder@0.10.31:
     resolution: {integrity: sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==}
 
@@ -10365,6 +10661,9 @@ packages:
   string_decoder@1.3.0:
     resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==}
 
+  stringify-entities@4.0.4:
+    resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==}
+
   stringz@2.1.0:
     resolution: {integrity: sha512-KlywLT+MZ+v0IRepfMxRtnSvDCMc3nR1qqCs3m/qIbSOWkNZYT8XHQA31rS3TnKp0c5xjZu3M4GY/2aRKSi/6A==}
 
@@ -10396,6 +10695,10 @@ packages:
     resolution: {integrity: sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==}
     engines: {node: '>=12'}
 
+  strip-final-newline@4.0.0:
+    resolution: {integrity: sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==}
+    engines: {node: '>=18'}
+
   strip-indent@3.0.0:
     resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==}
     engines: {node: '>=8'}
@@ -10408,8 +10711,8 @@ packages:
     resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==}
     engines: {node: '>=8'}
 
-  strip-literal@1.3.0:
-    resolution: {integrity: sha512-PugKzOsyXpArk0yWmUwqOZecSO0GH0bPoctLcqNDH9J04pVW3lflYE0ujElBGTloevcxF5MofAOZ7C5l2b+wLg==}
+  strip-literal@2.1.0:
+    resolution: {integrity: sha512-Op+UycaUt/8FbN/Z2TWPBLge3jWrP3xj10f3fnYxf052bKuS3EKs1ZQcVGjnEMdsNVAM+plXRdmjrZ/KgG3Skw==}
 
   strip-outer@2.0.0:
     resolution: {integrity: sha512-A21Xsm1XzUkK0qK1ZrytDUvqsQWict2Cykhvi0fBQntGG5JSprESasEyV1EZ/4CiR5WB5KjzLTrP/bO37B0wPg==}
@@ -10422,6 +10725,10 @@ packages:
     resolution: {integrity: sha512-pQ+V+nYQdC5H3Q7qBZAz/MO6lwGhoC2gOAjuouGf/VO0m7vQRh8QNMl2Uf6SwAtzZ9bOw3UIeBukEGNJl5dtXQ==}
     engines: {node: '>=14.16'}
 
+  strtok3@8.1.0:
+    resolution: {integrity: sha512-ExzDvHYPj6F6QkSNe/JxSlBxTh3OrI6wrAIz53ulxo1c4hBJ1bT9C/JrAthEKHWG9riVH3Xzg7B03Oxty6S2Lw==}
+    engines: {node: '>=16'}
+
   stylehacks@6.1.1:
     resolution: {integrity: sha512-gSTTEQ670cJNoaeIp9KX6lZmm8LJ3jPB5yJmX8Zq/wQxOsAFXV3qjWzHas3YYk1qesuVIyYWWUpZ0vSE/dTSGg==}
     engines: {node: ^14 || ^16 || >=18.0}
@@ -10460,19 +10767,12 @@ packages:
   symbol-tree@3.2.4:
     resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==}
 
-  systeminformation@5.22.7:
-    resolution: {integrity: sha512-AWxlP05KeHbpGdgvZkcudJpsmChc2Y5Eo/GvxG/iUA/Aws5LZKHAMSeAo+V+nD+nxWZaxrwpWcnx4SH3oxNL3A==}
+  systeminformation@5.23.5:
+    resolution: {integrity: sha512-PEpJwhRYxZgBCAlWZhWIgfMTjXLqfcaZ1pJsJn9snWNfBW/Z1YQg1mbIUSWrEV3ErAHF7l/OoVLQeaZDlPzkpA==}
     engines: {node: '>=8.0.0'}
     os: [darwin, linux, win32, freebsd, openbsd, netbsd, sunos, android]
     hasBin: true
 
-  tar-fs@2.1.1:
-    resolution: {integrity: sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==}
-
-  tar-stream@2.2.0:
-    resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==}
-    engines: {node: '>=6'}
-
   tar-stream@3.1.6:
     resolution: {integrity: sha512-B/UyjYwPpMBv+PaFSWAmtYjwdrlEaZQEhMIBFNC5oEG8lpiW8XjcSdmEaClj28ArfKScKHs2nshz3k2le6crsg==}
 
@@ -10491,20 +10791,8 @@ packages:
   telejson@7.2.0:
     resolution: {integrity: sha512-1QTEcJkJEhc8OnStBx/ILRu5J2p0GjvWsBx56bmZRqnrkdBMUe+nX92jxV+p3dB4CP6PZCdJMQJwCggkNBMzkQ==}
 
-  temp-dir@2.0.0:
-    resolution: {integrity: sha512-aoBAniQmmwtcKp/7BzsH8Cxzv8OL736p7v1ihGb5e9DJ9kTwGWHrQrVB5+lfVDzfGrdRzXch+ig7LHaY1JTOrg==}
-    engines: {node: '>=8'}
-
-  temp@0.8.4:
-    resolution: {integrity: sha512-s0ZZzd0BzYv5tLSptZooSjK8oj6C+c19p7Vqta9+6NPOf7r+fxq0cJe6/oN4LTC79sy5NY8ucOJNgwsKCSbfqg==}
-    engines: {node: '>=6.0.0'}
-
-  tempy@1.0.1:
-    resolution: {integrity: sha512-biM9brNqxSc04Ee71hzFbryD11nX7VPhQQY32AdDmjFvodsRFz/3ufeoTZ6uYkRFfGo188tENcASNs3vTdsM0w==}
-    engines: {node: '>=10'}
-
-  terser@5.30.3:
-    resolution: {integrity: sha512-STdUgOUx8rLbMGO9IOwHLpCqolkDITFFQSMYYwKE1N2lY6MVSaeoi10z/EhWxRc6ybqoVmKSkhKYH/XUpl7vSA==}
+  terser@5.33.0:
+    resolution: {integrity: sha512-JuPVaB7s1gdFKPKTelwUyRq5Sid2A3Gko2S0PncwdBq7kN9Ti9HPWDQ06MPsEDGsZeVESjKEnyGy68quBk1w6g==}
     engines: {node: '>=10'}
     hasBin: true
 
@@ -10525,52 +10813,50 @@ packages:
   thenify@3.3.1:
     resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==}
 
-  thread-stream@2.3.0:
-    resolution: {integrity: sha512-kaDqm1DET9pp3NXwR8382WHbnpXnRkN9xGN9dQt3B2+dmXiW8X1SOwmFOxAErEQ47ObhZ96J6yhZNXuyCOL7KA==}
+  thread-stream@3.1.0:
+    resolution: {integrity: sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==}
 
-  three@0.164.1:
-    resolution: {integrity: sha512-iC/hUBbl1vzFny7f5GtqzVXYjMJKaTPxiCxXfrvVdBi1Sf+jhd1CAkitiFwC7mIBFCo3MrDLJG97yisoaWig0w==}
+  three@0.169.0:
+    resolution: {integrity: sha512-Ed906MA3dR4TS5riErd4QBsRGPcx+HBDX2O5yYE5GqJeFQTPU+M56Va/f/Oph9X7uZo3W3o4l2ZhBZ6f6qUv0w==}
 
-  throttle-debounce@5.0.0:
-    resolution: {integrity: sha512-2iQTSgkkc1Zyk0MeVrt/3BvuOXYPl/R8Z0U2xxo9rjwNciaHDG3R+Lm6dh4EeUci49DanvBnuqI6jshoQQRGEg==}
+  throttle-debounce@5.0.2:
+    resolution: {integrity: sha512-B71/4oyj61iNH0KeCamLuE2rmKuTO5byTOSVwECM5FA7TiAiAW+UqTKZ9ERueC4qvgSttUhdmq1mXC3kJqGX7A==}
     engines: {node: '>=12.22'}
 
   throttleit@1.0.0:
     resolution: {integrity: sha512-rkTVqu6IjfQ/6+uNuuc3sZek4CEYxTJom3IktzgdSxcZqdARuebbA/f4QmAxMQIxqq9ZLEUkSYqvuk1I6VKq4g==}
 
-  through2@2.0.5:
-    resolution: {integrity: sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==}
-
   through@2.3.4:
     resolution: {integrity: sha512-DwbmSAcABsMazNkLOJJSLRC3gfh4cPxUxJCn9npmvbcI6undhgoJ2ShvEOgZrW8BH62Gyr9jKboGbfFcmY5VsQ==}
 
   through@2.3.8:
     resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==}
 
-  tiny-invariant@1.3.1:
-    resolution: {integrity: sha512-AD5ih2NlSssTCwsMznbvwMZpJ1cbhkGd2uueNxzv2jDlEeZdU04JQfRnggJQ8DrcVBGjAsCKwFBbDlVNtEMlzw==}
-
   tiny-invariant@1.3.3:
     resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==}
 
-  tiny-lru@10.0.1:
-    resolution: {integrity: sha512-Vst+6kEsWvb17Zpz14sRJV/f8bUWKhqm6Dc+v08iShmIJ/WxqWytHzCTd6m88pS33rE2zpX34TRmOpAJPloNCA==}
-    engines: {node: '>=6'}
-
   tinybench@2.6.0:
     resolution: {integrity: sha512-N8hW3PG/3aOoZAN5V/NSAEDz0ZixDSSt5b/a05iqtpgfLWMSVuCo7w0k2vVvEjdrIoeGqZzweX2WlyioNIHchA==}
 
   tinycolor2@1.6.0:
     resolution: {integrity: sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==}
 
-  tinypool@0.7.0:
-    resolution: {integrity: sha512-zSYNUlYSMhJ6Zdou4cJwo/p7w5nmAH17GRfU/ui3ctvjXFErXXkruT4MWW6poDeXgCaIBlGLrfU6TbTXxyGMww==}
+  tinypool@0.8.4:
+    resolution: {integrity: sha512-i11VH5gS6IFeLY3gMBQ00/MmLncVP7JLXOw1vlgkytLmJK7QnEr7NXf0LBdxfmNPAeyetukOk0bOYrJrFGjYJQ==}
+    engines: {node: '>=14.0.0'}
+
+  tinyrainbow@1.2.0:
+    resolution: {integrity: sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==}
     engines: {node: '>=14.0.0'}
 
   tinyspy@2.2.0:
     resolution: {integrity: sha512-d2eda04AN/cPOR89F7Xv5bK/jrQEhmcLFe6HFldoeO9AJtps+fqEnh486vnT/8y4bw38pSyxDcTCAq+Ks2aJTg==}
     engines: {node: '>=14.0.0'}
 
+  tinyspy@3.0.2:
+    resolution: {integrity: sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==}
+    engines: {node: '>=14.0.0'}
+
   tmp@0.2.3:
     resolution: {integrity: sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==}
     engines: {node: '>=14.14'}
@@ -10589,17 +10875,10 @@ packages:
     resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
     engines: {node: '>=8.0'}
 
-  toad-cache@3.3.0:
-    resolution: {integrity: sha512-3oDzcogWGHZdkwrHyvJVpPjA7oNzY6ENOV3PsWJY9XYPZ6INo94Yd47s5may1U+nleBPwDhrRiTPMIvKaa3MQg==}
-    engines: {node: '>=12'}
-
   toad-cache@3.7.0:
     resolution: {integrity: sha512-/m8M+2BJUpoJdgAHoG+baCwBT+tf2VraSfkBgl0Y00qIWt41DJ8R5B8nsEw0I58YwF5IZH6z24/2TobDKnqSWw==}
     engines: {node: '>=12'}
 
-  tocbot@4.21.1:
-    resolution: {integrity: sha512-IfajhBTeg0HlMXu1f+VMbPef05QpDTsZ9X2Yn1+8npdaXsXg/+wrm9Ze1WG5OS1UDC3qJ5EQN/XOZ3gfXjPFCw==}
-
   toidentifier@1.0.1:
     resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==}
     engines: {node: '>=0.6'}
@@ -10611,6 +10890,10 @@ packages:
     resolution: {integrity: sha512-Y2fmSnZjQdDb9W4w4r1tswlMHylzWIeOKpx0aZH9BgGtACHhrk3OkT52AzwcuqTRBZtvvnTjDBh8eynMulu8Vg==}
     engines: {node: '>=14.16'}
 
+  token-types@6.0.0:
+    resolution: {integrity: sha512-lbDrTLVsHhOMljPscd0yitpozq7Ga2M5Cvez5AjGg8GASBjtt6iERCAJ93yommPmz62fb45oFIXHEZ3u9bfJEA==}
+    engines: {node: '>=14.16'}
+
   touch@3.1.0:
     resolution: {integrity: sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA==}
     hasBin: true
@@ -10619,8 +10902,8 @@ packages:
     resolution: {integrity: sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==}
     engines: {node: '>=0.8'}
 
-  tough-cookie@4.1.3:
-    resolution: {integrity: sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==}
+  tough-cookie@4.1.4:
+    resolution: {integrity: sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==}
     engines: {node: '>=6'}
 
   tr46@0.0.3:
@@ -10633,6 +10916,9 @@ packages:
   trace-redirect@1.0.6:
     resolution: {integrity: sha512-UUfa1DjjU5flcjMdaFIiIEGDTyu2y/IiMjOX4uGXa7meKBS4vD4f2Uy/tken9Qkd4Jsm4sRsfZcIIPqrRVF3Mg==}
 
+  trim-lines@3.0.1:
+    resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==}
+
   trim-newlines@3.0.1:
     resolution: {integrity: sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==}
     engines: {node: '>=8'}
@@ -10656,9 +10942,8 @@ packages:
     peerDependencies:
       typescript: '>=4.2.0'
 
-  ts-case-convert@2.0.2:
-    resolution: {integrity: sha512-vdKfx1VAdpvEBOBv5OpVu5ZFqRg9HdTI4sYt6qqMeICBeNyXvitrarCnFWNDAki51IKwCyx+ZssY46Q9jH5otA==}
-    bundledDependencies: []
+  ts-case-convert@2.0.7:
+    resolution: {integrity: sha512-Kqj8wrkuduWsKUOUNRczrkdHCDt4ZNNd6HKjVw42EnMIGHQUABS4pqfy0acETVLwUTppc1fzo/yi11+uMTaqzw==}
 
   ts-dedent@2.2.0:
     resolution: {integrity: sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==}
@@ -10667,8 +10952,8 @@ packages:
   ts-map@1.0.3:
     resolution: {integrity: sha512-vDWbsl26LIcPGmDpoVzjEP6+hvHZkBkLW7JpvwbCv/5IYPJlsbzCVXY3wsCeAxAUeTclNOUZxnLdGh3VBD/J6w==}
 
-  tsc-alias@1.8.8:
-    resolution: {integrity: sha512-OYUOd2wl0H858NvABWr/BoSKNERw3N9GTi3rHPK8Iv4O1UyUXIrTTOAZNHsjlVpXFOhpJBVARI1s+rzwLivN3Q==}
+  tsc-alias@1.8.10:
+    resolution: {integrity: sha512-Ibv4KAWfFkFdKJxnWfVtdOmB0Zi1RJVxcbPGiCDsFpCQSsmpWyuzHG3rQyI5YkobWwxFPEyQfu1hdo4qLG2zPw==}
     hasBin: true
 
   tsconfig-paths@3.15.0:
@@ -10678,17 +10963,20 @@ packages:
     resolution: {integrity: sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==}
     engines: {node: '>=6'}
 
-  tsd@0.30.7:
-    resolution: {integrity: sha512-oTiJ28D6B/KXoU3ww/Eji+xqHJojiuPVMwA12g4KYX1O72N93Nb6P3P3h2OAhhf92Xl8NIhb/xFmBZd5zw/xUw==}
+  tsd@0.31.2:
+    resolution: {integrity: sha512-VplBAQwvYrHzVihtzXiUVXu5bGcr7uH1juQZ1lmKgkuGNGT+FechUCqmx9/zk7wibcqR2xaNEwCkDyKh+VVZnQ==}
     engines: {node: '>=14.16'}
     hasBin: true
 
-  tslib@1.14.1:
-    resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==}
-
   tslib@2.6.2:
     resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==}
 
+  tslib@2.6.3:
+    resolution: {integrity: sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==}
+
+  tslib@2.7.0:
+    resolution: {integrity: sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==}
+
   tsx@4.4.0:
     resolution: {integrity: sha512-4fwcEjRUxW20ciSaMB8zkpGwCPxuRGnadDuj/pBk5S9uT29zvWz15PK36GrKJo45mSJomDxVejZ73c6lr3811Q==}
     engines: {node: '>=18.0.0'}
@@ -10708,10 +10996,6 @@ packages:
     resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==}
     engines: {node: '>=4'}
 
-  type-fest@0.16.0:
-    resolution: {integrity: sha512-eaBzG6MxNzEn9kiwvtre90cXaNLkmadMWa1zQMs3XORCXNbsH/OewwbxC5ia9dCxIxnTAsSxXJaa/p5y8DlvJg==}
-    engines: {node: '>=10'}
-
   type-fest@0.18.1:
     resolution: {integrity: sha512-OIAYXk8+ISY+qTOwkHtKqzAuxchoMiD9Udx+FSGQDuiRR+PJKJHc2NJAXlbhkGwTt/4/nKZxELY1w3ReWOL8mw==}
     engines: {node: '>=10'}
@@ -10736,8 +11020,8 @@ packages:
     resolution: {integrity: sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==}
     engines: {node: '>=12.20'}
 
-  type-fest@4.9.0:
-    resolution: {integrity: sha512-KS/6lh/ynPGiHD/LnAobrEFq3Ad4pBzOlJ1wAnJx9N4EYoqFhMfLIBjUT2UEx4wg5ZE+cC1ob6DCSpppVo+rtg==}
+  type-fest@4.20.1:
+    resolution: {integrity: sha512-R6wDsVsoS9xYOpy8vgeBlqpdOyzJ12HNfQhC/aAKWM3YoCV9TtunJzh/QpkMgeDhkoynDcw5f1y+qF9yc/HHyg==}
     engines: {node: '>=16'}
 
   type-is@1.6.18:
@@ -10748,17 +11032,33 @@ packages:
     resolution: {integrity: sha512-Y8KTSIglk9OZEr8zywiIHG/kmQ7KWyjseXs1CbSo8vC42w7hg2HgYTxSWwP0+is7bWDc1H+Fo026CpHFwm8tkw==}
     engines: {node: '>= 0.4'}
 
+  typed-array-buffer@1.0.2:
+    resolution: {integrity: sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ==}
+    engines: {node: '>= 0.4'}
+
   typed-array-byte-length@1.0.0:
     resolution: {integrity: sha512-Or/+kvLxNpeQ9DtSydonMxCx+9ZXOswtwJn17SNLvhptaXYDJvkFFP5zbfU/uLmvnBJlI4yrnXRxpdWH/M5tNA==}
     engines: {node: '>= 0.4'}
 
+  typed-array-byte-length@1.0.1:
+    resolution: {integrity: sha512-3iMJ9q0ao7WE9tWcaYKIptkNBuOIcZCCT0d4MRvuuH88fEoEH62IuQe0OtraD3ebQEoTRk8XCBoknUNc1Y67pw==}
+    engines: {node: '>= 0.4'}
+
   typed-array-byte-offset@1.0.0:
     resolution: {integrity: sha512-RD97prjEt9EL8YgAgpOkf3O4IF9lhJFr9g0htQkm0rchFp/Vx7LW5Q8fSXXub7BXAODyUQohRMyOc3faCPd0hg==}
     engines: {node: '>= 0.4'}
 
+  typed-array-byte-offset@1.0.2:
+    resolution: {integrity: sha512-Ous0vodHa56FviZucS2E63zkgtgrACj7omjwd/8lTEMEPFFyjfixMZ1ZXenpgCFBBt4EC1J2XsyVS2gkG0eTFA==}
+    engines: {node: '>= 0.4'}
+
   typed-array-length@1.0.4:
     resolution: {integrity: sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng==}
 
+  typed-array-length@1.0.6:
+    resolution: {integrity: sha512-/OxDN6OtAk5KBpGb28T+HZc2M+ADtvRxXrKKbUwtsLgdoxgX13hyy7ek6bFRl5+aBs2yZzB0c4CnQfAtVypW/g==}
+    engines: {node: '>= 0.4'}
+
   typedarray@0.0.6:
     resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==}
 
@@ -10830,19 +11130,19 @@ packages:
     engines: {node: '>=14.17'}
     hasBin: true
 
-  typescript@5.4.5:
-    resolution: {integrity: sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==}
+  typescript@5.5.4:
+    resolution: {integrity: sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==}
+    engines: {node: '>=14.17'}
+    hasBin: true
+
+  typescript@5.6.2:
+    resolution: {integrity: sha512-NW8ByodCSNCwZeghjN3o+JX5OFH0Ojg6sadjEKY4huZ52TqbJTJnDo5+Tw98lSy63NZvi4n+ez5m2u5d4PkZyw==}
     engines: {node: '>=14.17'}
     hasBin: true
 
   ufo@1.3.2:
     resolution: {integrity: sha512-o+ORpgGwaYQXgqGDwd+hkS4PuZ3QnmqMMxRuajK/a38L6fTpcE5GPIfrf+L/KemFzfUpeUQc1rRS1iDBozvnFA==}
 
-  uglify-js@3.17.4:
-    resolution: {integrity: sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g==}
-    engines: {node: '>=0.8.0'}
-    hasBin: true
-
   uid2@0.0.4:
     resolution: {integrity: sha512-IevTus0SbGwQzYh3+fRsAMTVVPOoIVufzacXcHPmdlle1jUpq7BRL+mw3dgeLanvGZdwwbWhRV6XrcFNdBmjWA==}
 
@@ -10850,6 +11150,10 @@ packages:
     resolution: {integrity: sha512-u3xV3X7uzvi5b1MncmZo3i2Aw222Zk1keqLA1YkHldREkAhAqi65wuPfe7lHx8H/Wzy+8CE7S7uS3jekIM5s8g==}
     engines: {node: '>=8'}
 
+  uint8array-extras@1.4.0:
+    resolution: {integrity: sha512-ZPtzy0hu4cZjv3z5NW9gfKnNLjoz4y6uv4HlelAjDK7sY/xOkKZv9xK/WQpcsBB3jEybChz9DPC2U/+cusjJVQ==}
+    engines: {node: '>=18'}
+
   ulid@2.3.0:
     resolution: {integrity: sha512-keqHubrlpvT6G2wH0OEfSW4mquYRcbe/J8NMmveoQOjUqmo+hXtO+ORCpWhdbZ7k72UtY61BL7haGxW6enBnjw==}
     hasBin: true
@@ -10863,25 +11167,20 @@ packages:
   undici-types@5.26.5:
     resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==}
 
+  undici-types@6.19.8:
+    resolution: {integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==}
+
   undici@5.28.2:
     resolution: {integrity: sha512-wh1pHJHnUeQV5Xa8/kyQhO7WFa8M34l026L5P/+2TYiakvGy5Rdc8jWZVyG7ieht/0WgJLEd3kcU5gKx+6GC8w==}
     engines: {node: '>=14.0'}
 
-  unicode-canonical-property-names-ecmascript@2.0.0:
-    resolution: {integrity: sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ==}
-    engines: {node: '>=4'}
+  undici@6.19.8:
+    resolution: {integrity: sha512-U8uCCl2x9TK3WANvmBavymRzxbfFYG+tAu+fgx3zxQy3qdagQqBLwJVrdyO1TBfUXvfKveMKJZhpvUYoOjM+4g==}
+    engines: {node: '>=18.17'}
 
-  unicode-match-property-ecmascript@2.0.0:
-    resolution: {integrity: sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==}
-    engines: {node: '>=4'}
-
-  unicode-match-property-value-ecmascript@2.1.0:
-    resolution: {integrity: sha512-qxkjQt6qjg/mYscYMC0XKRn3Rh0wFPlfxB0xkt9CfyTvpX1Ra0+rAmdX2QyAobptSEvuy4RtpPRui6XkV+8wjA==}
-    engines: {node: '>=4'}
-
-  unicode-property-aliases-ecmascript@2.1.0:
-    resolution: {integrity: sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==}
-    engines: {node: '>=4'}
+  unicorn-magic@0.3.0:
+    resolution: {integrity: sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==}
+    engines: {node: '>=18'}
 
   unified@11.0.4:
     resolution: {integrity: sha512-apMPnyLjAX+ty4OrNap7yumyVAMlKx5IWU2wlzzUdYJO9A8f1p9m/gywF/GM2ZDFcjQPrx59Mc90KwmxsoklxQ==}
@@ -10897,13 +11196,12 @@ packages:
     resolution: {integrity: sha512-WrcA6AyEfqDX5bWige/4NQfPZMtASNVxdmWR76WESYQVAACSgWcR6e9i0mofqqBxYFtL4oAxPIptY73/0YE1DQ==}
     engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
 
-  unique-string@2.0.0:
-    resolution: {integrity: sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==}
-    engines: {node: '>=8'}
-
   unist-util-is@6.0.0:
     resolution: {integrity: sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==}
 
+  unist-util-position@5.0.0:
+    resolution: {integrity: sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==}
+
   unist-util-stringify-position@4.0.0:
     resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==}
 
@@ -10955,6 +11253,10 @@ packages:
     resolution: {integrity: sha512-uIuGf9TWQ/y+0Lp+KGZCMuJWc3N9BHA+l/UmHd/oUHwJJDeysyTRxNQVkbzsIWfGFbRe3OcgML/i0mvVRPOyDA==}
     engines: {node: '>=6.14.2'}
 
+  utf-8-validate@6.0.4:
+    resolution: {integrity: sha512-xu9GQDeFp+eZ6LnCywXN/zBancWvOpUMzgjLPSjy4BRHSmTelvn2E0DG0o1sTiw5hkCKBHo8rwSKncfRfv2EEQ==}
+    engines: {node: '>=6.14.2'}
+
   util-deprecate@1.0.2:
     resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
 
@@ -10965,6 +11267,10 @@ packages:
     resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==}
     engines: {node: '>= 0.4.0'}
 
+  uuid@10.0.0:
+    resolution: {integrity: sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==}
+    hasBin: true
+
   uuid@3.4.0:
     resolution: {integrity: sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==}
     deprecated: Please upgrade  to version 7 or higher.  Older versions may use Math.random() in certain circumstances, which is known to be problematic.  See https://v8.dev/blog/math-random for details.
@@ -10978,8 +11284,8 @@ packages:
     resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==}
     hasBin: true
 
-  v-code-diff@1.11.0:
-    resolution: {integrity: sha512-lBlO+FXw3I3qFKbnlorXZ4sb5cFnrdxlc6lj3Y1CWrbn2LC7PoVbGlwH0W+nvAVX1rdJhhc15rKIQdHyMkXe/w==}
+  v-code-diff@1.13.1:
+    resolution: {integrity: sha512-9LTV1dZhC1oYTntyB94vfumGgsfIX5u0fEDSI2Txx4vCE5sI5LkgeLJRRy2SsTVZmDcV+R73sBr0GpPn0TJxMw==}
     peerDependencies:
       '@vue/composition-api': ^1.4.9
       vue: ^2.6.0 || >=3.0.0
@@ -10991,13 +11297,13 @@ packages:
     resolution: {integrity: sha512-/EH/sDgxU2eGxajKdwLCDmQ4FWq+kpi3uCmBGpw1xJtnAxEjlD8j8PEiGWpCIMIs3ciNAgH0d3TTJiUkYzyZjA==}
     engines: {node: '>=10.12.0'}
 
+  valid-data-url@3.0.1:
+    resolution: {integrity: sha512-jOWVmzVceKlVVdwjNSenT4PbGghU0SBIizAev8ofZVgivk/TVHXSbNL8LP6M3spZvkR9/QolkyJavGSX5Cs0UA==}
+    engines: {node: '>=10'}
+
   validate-npm-package-license@3.0.4:
     resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==}
 
-  validator@13.9.0:
-    resolution: {integrity: sha512-B+dGG8U3fdtM0/aNK4/X8CXq/EcxU2WPrPEkJGslb47qyHsxmbggTWK0yEA4qnYVNF+nxNlN88o14hIcPmSIEA==}
-    engines: {node: '>= 0.10'}
-
   vary@1.1.2:
     resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==}
     engines: {node: '>= 0.8'}
@@ -11012,16 +11318,16 @@ packages:
   vfile@6.0.1:
     resolution: {integrity: sha512-1bYqc7pt6NIADBJ98UiG0Bn/CHIVOoZ/IyEkqIruLg0mE1BKzkOXY2D6CSqQIcKqgadppE5lrxgWXJmXd7zZJw==}
 
-  vite-node@0.34.6:
-    resolution: {integrity: sha512-nlBMJ9x6n7/Amaz6F3zJ97EBwR2FkzhBRxF5e+jE6LA3yi6Wtc2lyTij1OnDMIr34v5g/tVQtsVAzhT0jc5ygA==}
-    engines: {node: '>=v14.18.0'}
+  vite-node@1.6.0:
+    resolution: {integrity: sha512-de6HJgzC+TFzOu0NTC4RAIsyf/DY/ibWDYQUcuEA84EMHhcefTUGkjFHKKEJhQN4A+6I0u++kr3l36ZF2d7XRw==}
+    engines: {node: ^18.0.0 || >=20.0.0}
     hasBin: true
 
   vite-plugin-turbosnap@1.0.3:
     resolution: {integrity: sha512-p4D8CFVhZS412SyQX125qxyzOgIFouwOcvjZWk6bQbNPR1wtaEzFT6jZxAjf1dejlGqa6fqHcuCvQea6EWUkUA==}
 
-  vite@5.2.11:
-    resolution: {integrity: sha512-HndV31LWW05i1BLPMUCE1B9E9GFbOu1MbenhS58FuK6owSO5qHm7GiCotrNY1YE5rMeQSFBGmT5ZaLEjFizgiQ==}
+  vite@5.4.8:
+    resolution: {integrity: sha512-FqrItQ4DT1NC4zCUqMB4c4AZORMKIa0m8/URVCZ77OZ/QSNeJ54bU1vrFADbDsuwfIPcgknRkmqakQcgnL4GiQ==}
     engines: {node: ^18.0.0 || >=20.0.0}
     hasBin: true
     peerDependencies:
@@ -11029,6 +11335,7 @@ packages:
       less: '*'
       lightningcss: ^1.21.0
       sass: '*'
+      sass-embedded: '*'
       stylus: '*'
       sugarss: '*'
       terser: ^5.4.0
@@ -11041,6 +11348,8 @@ packages:
         optional: true
       sass:
         optional: true
+      sass-embedded:
+        optional: true
       stylus:
         optional: true
       sugarss:
@@ -11054,22 +11363,22 @@ packages:
     peerDependencies:
       vitest: '>=0.16.0'
 
-  vitest@0.34.6:
-    resolution: {integrity: sha512-+5CALsOvbNKnS+ZHMXtuUC7nL8/7F1F2DnHGjSsszX8zCjWSSviphCb/NuS9Nzf4Q03KyyDRBAXhF/8lffME4Q==}
-    engines: {node: '>=v14.18.0'}
+  vitest@1.6.0:
+    resolution: {integrity: sha512-H5r/dN06swuFnzNFhq/dnz37bPXnq8xB2xB5JOVk8K09rUtoeNN+LHWkoQ0A/i3hvbUKKcCei9KpbxqHMLhLLA==}
+    engines: {node: ^18.0.0 || >=20.0.0}
     hasBin: true
     peerDependencies:
       '@edge-runtime/vm': '*'
-      '@vitest/browser': '*'
-      '@vitest/ui': '*'
+      '@types/node': ^18.0.0 || >=20.0.0
+      '@vitest/browser': 1.6.0
+      '@vitest/ui': 1.6.0
       happy-dom: '*'
       jsdom: '*'
-      playwright: '*'
-      safaridriver: '*'
-      webdriverio: '*'
     peerDependenciesMeta:
       '@edge-runtime/vm':
         optional: true
+      '@types/node':
+        optional: true
       '@vitest/browser':
         optional: true
       '@vitest/ui':
@@ -11078,12 +11387,6 @@ packages:
         optional: true
       jsdom:
         optional: true
-      playwright:
-        optional: true
-      safaridriver:
-        optional: true
-      webdriverio:
-        optional: true
 
   void-elements@3.1.0:
     resolution: {integrity: sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==}
@@ -11110,6 +11413,9 @@ packages:
     resolution: {integrity: sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g==}
     hasBin: true
 
+  vscode-uri@3.0.8:
+    resolution: {integrity: sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==}
+
   vue-component-meta@2.0.16:
     resolution: {integrity: sha512-IyIMClUMYcKxAL34GqdPbR4V45MUeHXqQiZlHxeYMV5Qcqp4M+CEmtGpF//XBSS138heDkYkceHAtJQjLUB1Lw==}
     peerDependencies:
@@ -11124,8 +11430,8 @@ packages:
   vue-component-type-helpers@2.0.16:
     resolution: {integrity: sha512-qisL/iAfdO++7w+SsfYQJVPj6QKvxp4i1MMxvsNO41z/8zu3KuAw9LkhKUfP/kcOWGDxESp+pQObWppXusejCA==}
 
-  vue-component-type-helpers@2.0.19:
-    resolution: {integrity: sha512-cN3f1aTxxKo4lzNeQAkVopswuImUrb5Iurll9Gaw5cqpnbTAxtEMM1mgi6ou4X79OCyqYv1U1mzBHJkzmiK82w==}
+  vue-component-type-helpers@2.1.6:
+    resolution: {integrity: sha512-ng11B8B/ZADUMMOsRbqv0arc442q7lifSubD0v8oDXIFoMg/mXwAPUunrroIDkY+mcD0dHKccdaznSVp8EoX3w==}
 
   vue-demi@0.14.7:
     resolution: {integrity: sha512-EOG8KXDQNwkJILkx/gPcoL/7vH+hORoBaKgGe+6W7VFMvCYJfmF2dGbvgDroVnI8LU7/kTu8mbjRZGBU1z9NTA==}
@@ -11143,18 +11449,12 @@ packages:
     peerDependencies:
       vue: '>=2'
 
-  vue-eslint-parser@9.4.2:
-    resolution: {integrity: sha512-Ry9oiGmCAK91HrKMtCrKFWmSFWvYkpGglCeFAIqDdr9zdXmMMpJOmUJS7WWsW7fX81h6mwHmUZCQQ1E0PkSwYQ==}
+  vue-eslint-parser@9.4.3:
+    resolution: {integrity: sha512-2rYRLWlIpaiN8xbPiDyXZXRgLGOtWxERV7ND5fFAv5qo1D2N9Fu9MNajBNc6o13lZ+24DAWCkQCvj4klgmcITg==}
     engines: {node: ^14.17.0 || >=16.0.0}
     peerDependencies:
       eslint: '>=6.0.0'
 
-  vue-i18n@9.13.1:
-    resolution: {integrity: sha512-mh0GIxx0wPtPlcB1q4k277y0iKgo25xmDPWioVVYanjPufDBpvu5ySTjP5wOrSvlYQ2m1xI+CFhGdauv/61uQg==}
-    engines: {node: '>= 16'}
-    peerDependencies:
-      vue: ^3.0.0
-
   vue-inbrowser-compiler-independent-utils@4.71.1:
     resolution: {integrity: sha512-K3wt3iVmNGaFEOUR4JIThQRWfqokxLfnPslD41FDZB2ajXp789+wCqJyGYlIFsvEQ2P61PInw6/ph5iiqg51gg==}
     peerDependencies:
@@ -11163,14 +11463,22 @@ packages:
   vue-template-compiler@2.7.14:
     resolution: {integrity: sha512-zyA5Y3ArvVG0NacJDkkzJuPQDF8RFeRlzV2vLeSnhSpieO6LK2OVbdLPi5MPPs09Ii+gMO8nY4S3iKQxBxDmWQ==}
 
-  vue-tsc@2.0.16:
-    resolution: {integrity: sha512-/gHAWJa216PeEhfxtAToIbxdWgw01wuQzo48ZUqMYVEyNqDp+OYV9xMO5HaPS2P3Ls0+EsjguMZLY4cGobX4Ew==}
+  vue-tsc@2.1.6:
+    resolution: {integrity: sha512-f98dyZp5FOukcYmbFpuSCJ4Z0vHSOSmxGttZJCsFeX0M4w/Rsq0s4uKXjcSRsZqsRgQa6z7SfuO+y0HVICE57Q==}
     hasBin: true
     peerDependencies:
-      typescript: '*'
+      typescript: '>=5.0.0'
 
-  vue@3.4.26:
-    resolution: {integrity: sha512-bUIq/p+VB+0xrJubaemrfhk1/FiW9iX+pDV+62I/XJ6EkspAO9/DXEjbDFoe8pIfOZBqfk45i9BMc41ptP/uRg==}
+  vue@3.4.37:
+    resolution: {integrity: sha512-3vXvNfkKTBsSJ7JP+LyR7GBuwQuckbWvuwAid3xbqK9ppsKt/DUvfqgZ48fgOLEfpy1IacL5f8QhUVl77RaI7A==}
+    peerDependencies:
+      typescript: '*'
+    peerDependenciesMeta:
+      typescript:
+        optional: true
+
+  vue@3.5.11:
+    resolution: {integrity: sha512-/8Wurrd9J3lb72FTQS7gRMNQD4nztTtKPmuDuPuhqXmmpD6+skVjAeahNpVzsuky6Sy9gy7wn8UadqPtt9SQIg==}
     peerDependencies:
       typescript: '*'
     peerDependenciesMeta:
@@ -11186,30 +11494,31 @@ packages:
     resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==}
     engines: {node: '>=18'}
 
-  wait-on@7.2.0:
-    resolution: {integrity: sha512-wCQcHkRazgjG5XoAq9jbTMLpNIjoSlZslrJ2+N9MxDsGEv1HnFoVjOCexL0ESva7Y9cu350j+DWADdk54s4AFQ==}
+  wait-on@8.0.1:
+    resolution: {integrity: sha512-1wWQOyR2LVVtaqrcIL2+OM+x7bkpmzVROa0Nf6FryXkS+er5Sa1kzFGjzZRqLnHa3n1rACFLeTwUqE1ETL9Mig==}
     engines: {node: '>=12.0.0'}
     hasBin: true
 
   walker@1.0.8:
     resolution: {integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==}
 
-  watchpack@2.4.0:
-    resolution: {integrity: sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==}
-    engines: {node: '>=10.13.0'}
-
-  wcwidth@1.0.1:
-    resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==}
-
   web-push@3.6.7:
     resolution: {integrity: sha512-OpiIUe8cuGjrj3mMBFWY+e4MMIkW3SVT+7vEIjvD9kejGUypv8GPDf84JdPWskK8zMRIJ6xYGm+Kxr8YkPyA0A==}
     engines: {node: '>= 16'}
     hasBin: true
 
+  web-resource-inliner@7.0.0:
+    resolution: {integrity: sha512-NlfnGF8MY9ZUwFjyq3vOUBx7KwF8bmE+ywR781SB0nWB6MoMxN4BA8gtgP1KGTZo/O/AyWJz7HZpR704eaj4mg==}
+    engines: {node: '>=10.0.0'}
+
   web-streams-polyfill@3.2.1:
     resolution: {integrity: sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q==}
     engines: {node: '>= 8'}
 
+  web-streams-polyfill@4.0.0:
+    resolution: {integrity: sha512-0zJXHRAYEjM2tUfZ2DiSOHAa2aw1tisnnhU3ufD57R8iefL+DcdJyRBRyJpG+NUimDgbTI/lH+gAE1PAvV3Cgw==}
+    engines: {node: '>= 8'}
+
   webidl-conversions@3.0.1:
     resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
 
@@ -11260,6 +11569,10 @@ packages:
     resolution: {integrity: sha512-qe9UWWpkeG5yzZ0tNYxDmd7vo58HDBc39mZ0xWWpolAGADdFOzkfamWLDxkOWcvHQKVmdTyQdLD4NOfjLWTKew==}
     engines: {node: '>= 0.4'}
 
+  which-typed-array@1.1.15:
+    resolution: {integrity: sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==}
+    engines: {node: '>= 0.4'}
+
   which@1.3.1:
     resolution: {integrity: sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==}
     hasBin: true
@@ -11286,8 +11599,9 @@ packages:
     resolution: {integrity: sha512-RNGKj82nUPg3g5ygxkQl0R937xLyho1J24ItRCBTr/m1YnZkzJy1hUiHUJrc/VlsDQzsCnInEGSg3bci0Lmd4w==}
     engines: {node: '>= 10.0.0'}
 
-  wordwrap@1.0.0:
-    resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==}
+  word-wrap@1.2.5:
+    resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==}
+    engines: {node: '>=0.10.0'}
 
   wrap-ansi@6.2.0:
     resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==}
@@ -11304,15 +11618,12 @@ packages:
   wrappy@1.0.2:
     resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
 
-  write-file-atomic@2.4.3:
-    resolution: {integrity: sha512-GaETH5wwsX+GcnzhPgKcKjJ6M2Cq3/iZp1WyY/X1CSqrW+jVNM9Y7D8EC2sM4ZG/V8wZlSniJnCKWPmBYAucRQ==}
-
   write-file-atomic@4.0.2:
     resolution: {integrity: sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==}
     engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0}
 
-  ws@8.17.0:
-    resolution: {integrity: sha512-uJq6108EgZMAl20KagGkzCKfMEjxmKvZHG7Tlq0Z6nOky7YF7aq4mOx6xK8TJ/i1LeK4Qus7INktacctDgY8Ow==}
+  ws@8.18.0:
+    resolution: {integrity: sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==}
     engines: {node: '>=10.0.0'}
     peerDependencies:
       bufferutil: ^4.0.1
@@ -11404,10 +11715,9 @@ packages:
     resolution: {integrity: sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==}
     engines: {node: '>=12.20'}
 
-  z-schema@5.0.5:
-    resolution: {integrity: sha512-D7eujBWkLa3p2sIpJA0d1pr7es+a7m0vFAnZLlCEKq/Ij2k0MLi9Br2UPxoxdYystm5K1yeBGzub0FlYUEWj2Q==}
-    engines: {node: '>=8.0.0'}
-    hasBin: true
+  yoctocolors@2.0.2:
+    resolution: {integrity: sha512-Ct97huExsu7cWeEjmrXlofevF8CvzUglJ4iGUet5B8xn1oumtAZBpHU4GzYuoE6PVqcZ5hghtBrSlhwHuR1Jmw==}
+    engines: {node: '>=18'}
 
   zip-stream@6.0.1:
     resolution: {integrity: sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA==}
@@ -11418,9 +11728,7 @@ packages:
 
 snapshots:
 
-  '@aashutoshrathi/word-wrap@1.2.6': {}
-
-  '@adobe/css-tools@4.3.3': {}
+  '@adobe/css-tools@4.4.0': {}
 
   '@aiscript-dev/aiscript-languageserver@https://github.com/aiscript-dev/aiscript-languageserver/releases/download/0.1.6/aiscript-dev-aiscript-languageserver-0.1.6.tgz':
     dependencies:
@@ -11432,478 +11740,532 @@ snapshots:
 
   '@ampproject/remapping@2.2.1':
     dependencies:
-      '@jridgewell/gen-mapping': 0.3.2
-      '@jridgewell/trace-mapping': 0.3.18
-
-  '@apidevtools/openapi-schemas@2.1.0': {}
+      '@jridgewell/gen-mapping': 0.3.5
+      '@jridgewell/trace-mapping': 0.3.25
 
   '@apidevtools/swagger-methods@3.0.2': {}
 
-  '@aw-web-design/x-default-browser@1.4.126':
+  '@aws-crypto/crc32@5.2.0':
     dependencies:
-      default-browser-id: 3.0.0
+      '@aws-crypto/util': 5.2.0
+      '@aws-sdk/types': 3.609.0
+      tslib: 2.7.0
 
-  '@aws-crypto/crc32@3.0.0':
+  '@aws-crypto/crc32c@5.2.0':
     dependencies:
-      '@aws-crypto/util': 3.0.0
-      '@aws-sdk/types': 3.413.0
-      tslib: 1.14.1
+      '@aws-crypto/util': 5.2.0
+      '@aws-sdk/types': 3.609.0
+      tslib: 2.7.0
 
-  '@aws-crypto/crc32c@3.0.0':
+  '@aws-crypto/sha1-browser@5.2.0':
     dependencies:
-      '@aws-crypto/util': 3.0.0
-      '@aws-sdk/types': 3.413.0
-      tslib: 1.14.1
-
-  '@aws-crypto/ie11-detection@3.0.0':
-    dependencies:
-      tslib: 1.14.1
-
-  '@aws-crypto/sha1-browser@3.0.0':
-    dependencies:
-      '@aws-crypto/ie11-detection': 3.0.0
-      '@aws-crypto/supports-web-crypto': 3.0.0
-      '@aws-crypto/util': 3.0.0
-      '@aws-sdk/types': 3.413.0
+      '@aws-crypto/supports-web-crypto': 5.2.0
+      '@aws-crypto/util': 5.2.0
+      '@aws-sdk/types': 3.609.0
       '@aws-sdk/util-locate-window': 3.208.0
-      '@aws-sdk/util-utf8-browser': 3.259.0
-      tslib: 1.14.1
+      '@smithy/util-utf8': 2.0.0
+      tslib: 2.7.0
 
-  '@aws-crypto/sha256-browser@3.0.0':
+  '@aws-crypto/sha256-browser@5.2.0':
     dependencies:
-      '@aws-crypto/ie11-detection': 3.0.0
-      '@aws-crypto/sha256-js': 3.0.0
-      '@aws-crypto/supports-web-crypto': 3.0.0
-      '@aws-crypto/util': 3.0.0
-      '@aws-sdk/types': 3.413.0
+      '@aws-crypto/sha256-js': 5.2.0
+      '@aws-crypto/supports-web-crypto': 5.2.0
+      '@aws-crypto/util': 5.2.0
+      '@aws-sdk/types': 3.609.0
       '@aws-sdk/util-locate-window': 3.208.0
-      '@aws-sdk/util-utf8-browser': 3.259.0
-      tslib: 1.14.1
-
-  '@aws-crypto/sha256-js@3.0.0':
-    dependencies:
-      '@aws-crypto/util': 3.0.0
-      '@aws-sdk/types': 3.413.0
-      tslib: 1.14.1
-
-  '@aws-crypto/supports-web-crypto@3.0.0':
-    dependencies:
-      tslib: 1.14.1
-
-  '@aws-crypto/util@3.0.0':
-    dependencies:
-      '@aws-sdk/types': 3.413.0
-      '@aws-sdk/util-utf8-browser': 3.259.0
-      tslib: 1.14.1
-
-  '@aws-sdk/client-s3@3.412.0':
-    dependencies:
-      '@aws-crypto/sha1-browser': 3.0.0
-      '@aws-crypto/sha256-browser': 3.0.0
-      '@aws-crypto/sha256-js': 3.0.0
-      '@aws-sdk/client-sts': 3.410.0
-      '@aws-sdk/credential-provider-node': 3.410.0
-      '@aws-sdk/middleware-bucket-endpoint': 3.410.0
-      '@aws-sdk/middleware-expect-continue': 3.410.0
-      '@aws-sdk/middleware-flexible-checksums': 3.410.0
-      '@aws-sdk/middleware-host-header': 3.410.0
-      '@aws-sdk/middleware-location-constraint': 3.410.0
-      '@aws-sdk/middleware-logger': 3.410.0
-      '@aws-sdk/middleware-recursion-detection': 3.410.0
-      '@aws-sdk/middleware-sdk-s3': 3.410.0
-      '@aws-sdk/middleware-signing': 3.410.0
-      '@aws-sdk/middleware-ssec': 3.410.0
-      '@aws-sdk/middleware-user-agent': 3.410.0
-      '@aws-sdk/signature-v4-multi-region': 3.412.0
-      '@aws-sdk/types': 3.410.0
-      '@aws-sdk/util-endpoints': 3.410.0
-      '@aws-sdk/util-user-agent-browser': 3.410.0
-      '@aws-sdk/util-user-agent-node': 3.410.0
-      '@aws-sdk/xml-builder': 3.310.0
-      '@smithy/config-resolver': 2.0.9
-      '@smithy/eventstream-serde-browser': 2.0.8
-      '@smithy/eventstream-serde-config-resolver': 2.0.8
-      '@smithy/eventstream-serde-node': 2.0.8
-      '@smithy/fetch-http-handler': 2.1.4
-      '@smithy/hash-blob-browser': 2.0.8
-      '@smithy/hash-node': 2.0.8
-      '@smithy/hash-stream-node': 2.0.8
-      '@smithy/invalid-dependency': 2.0.8
-      '@smithy/md5-js': 2.0.8
-      '@smithy/middleware-content-length': 2.0.10
-      '@smithy/middleware-endpoint': 2.0.8
-      '@smithy/middleware-retry': 2.0.11
-      '@smithy/middleware-serde': 2.0.8
-      '@smithy/middleware-stack': 2.0.1
-      '@smithy/node-config-provider': 2.0.11
-      '@smithy/node-http-handler': 2.5.0
-      '@smithy/protocol-http': 3.0.10
-      '@smithy/smithy-client': 2.1.5
-      '@smithy/types': 2.6.0
-      '@smithy/url-parser': 2.0.8
-      '@smithy/util-base64': 2.0.0
-      '@smithy/util-body-length-browser': 2.0.0
-      '@smithy/util-body-length-node': 2.1.0
-      '@smithy/util-defaults-mode-browser': 2.0.9
-      '@smithy/util-defaults-mode-node': 2.0.11
-      '@smithy/util-retry': 2.0.1
-      '@smithy/util-stream': 2.0.11
       '@smithy/util-utf8': 2.0.0
-      '@smithy/util-waiter': 2.0.8
+      tslib: 2.7.0
+
+  '@aws-crypto/sha256-js@5.2.0':
+    dependencies:
+      '@aws-crypto/util': 5.2.0
+      '@aws-sdk/types': 3.609.0
+      tslib: 2.7.0
+
+  '@aws-crypto/supports-web-crypto@5.2.0':
+    dependencies:
+      tslib: 2.7.0
+
+  '@aws-crypto/util@5.2.0':
+    dependencies:
+      '@aws-sdk/types': 3.609.0
+      '@smithy/util-utf8': 2.0.0
+      tslib: 2.7.0
+
+  '@aws-sdk/client-s3@3.620.0':
+    dependencies:
+      '@aws-crypto/sha1-browser': 5.2.0
+      '@aws-crypto/sha256-browser': 5.2.0
+      '@aws-crypto/sha256-js': 5.2.0
+      '@aws-sdk/client-sso-oidc': 3.620.0(@aws-sdk/client-sts@3.620.0)
+      '@aws-sdk/client-sts': 3.620.0
+      '@aws-sdk/core': 3.620.0
+      '@aws-sdk/credential-provider-node': 3.620.0(@aws-sdk/client-sso-oidc@3.620.0(@aws-sdk/client-sts@3.620.0))(@aws-sdk/client-sts@3.620.0)
+      '@aws-sdk/middleware-bucket-endpoint': 3.620.0
+      '@aws-sdk/middleware-expect-continue': 3.620.0
+      '@aws-sdk/middleware-flexible-checksums': 3.620.0
+      '@aws-sdk/middleware-host-header': 3.620.0
+      '@aws-sdk/middleware-location-constraint': 3.609.0
+      '@aws-sdk/middleware-logger': 3.609.0
+      '@aws-sdk/middleware-recursion-detection': 3.620.0
+      '@aws-sdk/middleware-sdk-s3': 3.620.0
+      '@aws-sdk/middleware-signing': 3.620.0
+      '@aws-sdk/middleware-ssec': 3.609.0
+      '@aws-sdk/middleware-user-agent': 3.620.0
+      '@aws-sdk/region-config-resolver': 3.614.0
+      '@aws-sdk/signature-v4-multi-region': 3.620.0
+      '@aws-sdk/types': 3.609.0
+      '@aws-sdk/util-endpoints': 3.614.0
+      '@aws-sdk/util-user-agent-browser': 3.609.0
+      '@aws-sdk/util-user-agent-node': 3.614.0
+      '@aws-sdk/xml-builder': 3.609.0
+      '@smithy/config-resolver': 3.0.5
+      '@smithy/core': 2.3.1
+      '@smithy/eventstream-serde-browser': 3.0.5
+      '@smithy/eventstream-serde-config-resolver': 3.0.3
+      '@smithy/eventstream-serde-node': 3.0.4
+      '@smithy/fetch-http-handler': 3.2.4
+      '@smithy/hash-blob-browser': 3.1.2
+      '@smithy/hash-node': 3.0.3
+      '@smithy/hash-stream-node': 3.1.2
+      '@smithy/invalid-dependency': 3.0.3
+      '@smithy/md5-js': 3.0.3
+      '@smithy/middleware-content-length': 3.0.5
+      '@smithy/middleware-endpoint': 3.1.0
+      '@smithy/middleware-retry': 3.0.13
+      '@smithy/middleware-serde': 3.0.3
+      '@smithy/middleware-stack': 3.0.3
+      '@smithy/node-config-provider': 3.1.4
+      '@smithy/node-http-handler': 3.1.4
+      '@smithy/protocol-http': 4.1.0
+      '@smithy/smithy-client': 3.1.11
+      '@smithy/types': 3.3.0
+      '@smithy/url-parser': 3.0.3
+      '@smithy/util-base64': 3.0.0
+      '@smithy/util-body-length-browser': 3.0.0
+      '@smithy/util-body-length-node': 3.0.0
+      '@smithy/util-defaults-mode-browser': 3.0.13
+      '@smithy/util-defaults-mode-node': 3.0.13
+      '@smithy/util-endpoints': 2.0.5
+      '@smithy/util-retry': 3.0.3
+      '@smithy/util-stream': 3.1.3
+      '@smithy/util-utf8': 3.0.0
+      '@smithy/util-waiter': 3.1.2
+      tslib: 2.6.3
+    transitivePeerDependencies:
+      - aws-crt
+
+  '@aws-sdk/client-sso-oidc@3.620.0(@aws-sdk/client-sts@3.620.0)':
+    dependencies:
+      '@aws-crypto/sha256-browser': 5.2.0
+      '@aws-crypto/sha256-js': 5.2.0
+      '@aws-sdk/client-sts': 3.620.0
+      '@aws-sdk/core': 3.620.0
+      '@aws-sdk/credential-provider-node': 3.620.0(@aws-sdk/client-sso-oidc@3.620.0(@aws-sdk/client-sts@3.620.0))(@aws-sdk/client-sts@3.620.0)
+      '@aws-sdk/middleware-host-header': 3.620.0
+      '@aws-sdk/middleware-logger': 3.609.0
+      '@aws-sdk/middleware-recursion-detection': 3.620.0
+      '@aws-sdk/middleware-user-agent': 3.620.0
+      '@aws-sdk/region-config-resolver': 3.614.0
+      '@aws-sdk/types': 3.609.0
+      '@aws-sdk/util-endpoints': 3.614.0
+      '@aws-sdk/util-user-agent-browser': 3.609.0
+      '@aws-sdk/util-user-agent-node': 3.614.0
+      '@smithy/config-resolver': 3.0.5
+      '@smithy/core': 2.3.1
+      '@smithy/fetch-http-handler': 3.2.4
+      '@smithy/hash-node': 3.0.3
+      '@smithy/invalid-dependency': 3.0.3
+      '@smithy/middleware-content-length': 3.0.5
+      '@smithy/middleware-endpoint': 3.1.0
+      '@smithy/middleware-retry': 3.0.13
+      '@smithy/middleware-serde': 3.0.3
+      '@smithy/middleware-stack': 3.0.3
+      '@smithy/node-config-provider': 3.1.4
+      '@smithy/node-http-handler': 3.1.4
+      '@smithy/protocol-http': 4.1.0
+      '@smithy/smithy-client': 3.1.11
+      '@smithy/types': 3.3.0
+      '@smithy/url-parser': 3.0.3
+      '@smithy/util-base64': 3.0.0
+      '@smithy/util-body-length-browser': 3.0.0
+      '@smithy/util-body-length-node': 3.0.0
+      '@smithy/util-defaults-mode-browser': 3.0.13
+      '@smithy/util-defaults-mode-node': 3.0.13
+      '@smithy/util-endpoints': 2.0.5
+      '@smithy/util-middleware': 3.0.3
+      '@smithy/util-retry': 3.0.3
+      '@smithy/util-utf8': 3.0.0
+      tslib: 2.7.0
+    transitivePeerDependencies:
+      - aws-crt
+
+  '@aws-sdk/client-sso@3.620.0':
+    dependencies:
+      '@aws-crypto/sha256-browser': 5.2.0
+      '@aws-crypto/sha256-js': 5.2.0
+      '@aws-sdk/core': 3.620.0
+      '@aws-sdk/middleware-host-header': 3.620.0
+      '@aws-sdk/middleware-logger': 3.609.0
+      '@aws-sdk/middleware-recursion-detection': 3.620.0
+      '@aws-sdk/middleware-user-agent': 3.620.0
+      '@aws-sdk/region-config-resolver': 3.614.0
+      '@aws-sdk/types': 3.609.0
+      '@aws-sdk/util-endpoints': 3.614.0
+      '@aws-sdk/util-user-agent-browser': 3.609.0
+      '@aws-sdk/util-user-agent-node': 3.614.0
+      '@smithy/config-resolver': 3.0.5
+      '@smithy/core': 2.3.1
+      '@smithy/fetch-http-handler': 3.2.4
+      '@smithy/hash-node': 3.0.3
+      '@smithy/invalid-dependency': 3.0.3
+      '@smithy/middleware-content-length': 3.0.5
+      '@smithy/middleware-endpoint': 3.1.0
+      '@smithy/middleware-retry': 3.0.13
+      '@smithy/middleware-serde': 3.0.3
+      '@smithy/middleware-stack': 3.0.3
+      '@smithy/node-config-provider': 3.1.4
+      '@smithy/node-http-handler': 3.1.4
+      '@smithy/protocol-http': 4.1.0
+      '@smithy/smithy-client': 3.1.11
+      '@smithy/types': 3.3.0
+      '@smithy/url-parser': 3.0.3
+      '@smithy/util-base64': 3.0.0
+      '@smithy/util-body-length-browser': 3.0.0
+      '@smithy/util-body-length-node': 3.0.0
+      '@smithy/util-defaults-mode-browser': 3.0.13
+      '@smithy/util-defaults-mode-node': 3.0.13
+      '@smithy/util-endpoints': 2.0.5
+      '@smithy/util-middleware': 3.0.3
+      '@smithy/util-retry': 3.0.3
+      '@smithy/util-utf8': 3.0.0
+      tslib: 2.7.0
+    transitivePeerDependencies:
+      - aws-crt
+
+  '@aws-sdk/client-sts@3.620.0':
+    dependencies:
+      '@aws-crypto/sha256-browser': 5.2.0
+      '@aws-crypto/sha256-js': 5.2.0
+      '@aws-sdk/client-sso-oidc': 3.620.0(@aws-sdk/client-sts@3.620.0)
+      '@aws-sdk/core': 3.620.0
+      '@aws-sdk/credential-provider-node': 3.620.0(@aws-sdk/client-sso-oidc@3.620.0(@aws-sdk/client-sts@3.620.0))(@aws-sdk/client-sts@3.620.0)
+      '@aws-sdk/middleware-host-header': 3.620.0
+      '@aws-sdk/middleware-logger': 3.609.0
+      '@aws-sdk/middleware-recursion-detection': 3.620.0
+      '@aws-sdk/middleware-user-agent': 3.620.0
+      '@aws-sdk/region-config-resolver': 3.614.0
+      '@aws-sdk/types': 3.609.0
+      '@aws-sdk/util-endpoints': 3.614.0
+      '@aws-sdk/util-user-agent-browser': 3.609.0
+      '@aws-sdk/util-user-agent-node': 3.614.0
+      '@smithy/config-resolver': 3.0.5
+      '@smithy/core': 2.3.1
+      '@smithy/fetch-http-handler': 3.2.4
+      '@smithy/hash-node': 3.0.3
+      '@smithy/invalid-dependency': 3.0.3
+      '@smithy/middleware-content-length': 3.0.5
+      '@smithy/middleware-endpoint': 3.1.0
+      '@smithy/middleware-retry': 3.0.13
+      '@smithy/middleware-serde': 3.0.3
+      '@smithy/middleware-stack': 3.0.3
+      '@smithy/node-config-provider': 3.1.4
+      '@smithy/node-http-handler': 3.1.4
+      '@smithy/protocol-http': 4.1.0
+      '@smithy/smithy-client': 3.1.11
+      '@smithy/types': 3.3.0
+      '@smithy/url-parser': 3.0.3
+      '@smithy/util-base64': 3.0.0
+      '@smithy/util-body-length-browser': 3.0.0
+      '@smithy/util-body-length-node': 3.0.0
+      '@smithy/util-defaults-mode-browser': 3.0.13
+      '@smithy/util-defaults-mode-node': 3.0.13
+      '@smithy/util-endpoints': 2.0.5
+      '@smithy/util-middleware': 3.0.3
+      '@smithy/util-retry': 3.0.3
+      '@smithy/util-utf8': 3.0.0
+      tslib: 2.7.0
+    transitivePeerDependencies:
+      - aws-crt
+
+  '@aws-sdk/core@3.620.0':
+    dependencies:
+      '@smithy/core': 2.3.1
+      '@smithy/protocol-http': 4.1.0
+      '@smithy/signature-v4': 4.1.0
+      '@smithy/smithy-client': 3.1.11
+      '@smithy/types': 3.3.0
       fast-xml-parser: 4.2.5
-      tslib: 2.6.2
+      tslib: 2.7.0
+
+  '@aws-sdk/credential-provider-env@3.609.0':
+    dependencies:
+      '@aws-sdk/types': 3.609.0
+      '@smithy/property-provider': 3.1.3
+      '@smithy/types': 3.3.0
+      tslib: 2.7.0
+
+  '@aws-sdk/credential-provider-http@3.620.0':
+    dependencies:
+      '@aws-sdk/types': 3.609.0
+      '@smithy/fetch-http-handler': 3.2.4
+      '@smithy/node-http-handler': 3.1.4
+      '@smithy/property-provider': 3.1.3
+      '@smithy/protocol-http': 4.1.0
+      '@smithy/smithy-client': 3.1.11
+      '@smithy/types': 3.3.0
+      '@smithy/util-stream': 3.1.3
+      tslib: 2.7.0
+
+  '@aws-sdk/credential-provider-ini@3.620.0(@aws-sdk/client-sso-oidc@3.620.0(@aws-sdk/client-sts@3.620.0))(@aws-sdk/client-sts@3.620.0)':
+    dependencies:
+      '@aws-sdk/client-sts': 3.620.0
+      '@aws-sdk/credential-provider-env': 3.609.0
+      '@aws-sdk/credential-provider-http': 3.620.0
+      '@aws-sdk/credential-provider-process': 3.614.0
+      '@aws-sdk/credential-provider-sso': 3.620.0(@aws-sdk/client-sso-oidc@3.620.0(@aws-sdk/client-sts@3.620.0))
+      '@aws-sdk/credential-provider-web-identity': 3.609.0(@aws-sdk/client-sts@3.620.0)
+      '@aws-sdk/types': 3.609.0
+      '@smithy/credential-provider-imds': 3.2.0
+      '@smithy/property-provider': 3.1.3
+      '@smithy/shared-ini-file-loader': 3.1.4
+      '@smithy/types': 3.3.0
+      tslib: 2.7.0
     transitivePeerDependencies:
+      - '@aws-sdk/client-sso-oidc'
       - aws-crt
 
-  '@aws-sdk/client-sso@3.410.0':
+  '@aws-sdk/credential-provider-node@3.620.0(@aws-sdk/client-sso-oidc@3.620.0(@aws-sdk/client-sts@3.620.0))(@aws-sdk/client-sts@3.620.0)':
     dependencies:
-      '@aws-crypto/sha256-browser': 3.0.0
-      '@aws-crypto/sha256-js': 3.0.0
-      '@aws-sdk/middleware-host-header': 3.410.0
-      '@aws-sdk/middleware-logger': 3.410.0
-      '@aws-sdk/middleware-recursion-detection': 3.410.0
-      '@aws-sdk/middleware-user-agent': 3.410.0
-      '@aws-sdk/types': 3.410.0
-      '@aws-sdk/util-endpoints': 3.410.0
-      '@aws-sdk/util-user-agent-browser': 3.410.0
-      '@aws-sdk/util-user-agent-node': 3.410.0
-      '@smithy/config-resolver': 2.0.9
-      '@smithy/fetch-http-handler': 2.1.4
-      '@smithy/hash-node': 2.0.8
-      '@smithy/invalid-dependency': 2.0.8
-      '@smithy/middleware-content-length': 2.0.10
-      '@smithy/middleware-endpoint': 2.0.8
-      '@smithy/middleware-retry': 2.0.11
-      '@smithy/middleware-serde': 2.0.8
-      '@smithy/middleware-stack': 2.0.1
-      '@smithy/node-config-provider': 2.0.11
-      '@smithy/node-http-handler': 2.5.0
-      '@smithy/protocol-http': 3.0.10
-      '@smithy/smithy-client': 2.1.5
-      '@smithy/types': 2.6.0
-      '@smithy/url-parser': 2.0.8
-      '@smithy/util-base64': 2.0.0
-      '@smithy/util-body-length-browser': 2.0.0
-      '@smithy/util-body-length-node': 2.1.0
-      '@smithy/util-defaults-mode-browser': 2.0.9
-      '@smithy/util-defaults-mode-node': 2.0.11
-      '@smithy/util-retry': 2.0.1
-      '@smithy/util-utf8': 2.0.0
-      tslib: 2.6.2
+      '@aws-sdk/credential-provider-env': 3.609.0
+      '@aws-sdk/credential-provider-http': 3.620.0
+      '@aws-sdk/credential-provider-ini': 3.620.0(@aws-sdk/client-sso-oidc@3.620.0(@aws-sdk/client-sts@3.620.0))(@aws-sdk/client-sts@3.620.0)
+      '@aws-sdk/credential-provider-process': 3.614.0
+      '@aws-sdk/credential-provider-sso': 3.620.0(@aws-sdk/client-sso-oidc@3.620.0(@aws-sdk/client-sts@3.620.0))
+      '@aws-sdk/credential-provider-web-identity': 3.609.0(@aws-sdk/client-sts@3.620.0)
+      '@aws-sdk/types': 3.609.0
+      '@smithy/credential-provider-imds': 3.2.0
+      '@smithy/property-provider': 3.1.3
+      '@smithy/shared-ini-file-loader': 3.1.4
+      '@smithy/types': 3.3.0
+      tslib: 2.7.0
     transitivePeerDependencies:
+      - '@aws-sdk/client-sso-oidc'
+      - '@aws-sdk/client-sts'
       - aws-crt
 
-  '@aws-sdk/client-sts@3.410.0':
+  '@aws-sdk/credential-provider-process@3.614.0':
     dependencies:
-      '@aws-crypto/sha256-browser': 3.0.0
-      '@aws-crypto/sha256-js': 3.0.0
-      '@aws-sdk/credential-provider-node': 3.410.0
-      '@aws-sdk/middleware-host-header': 3.410.0
-      '@aws-sdk/middleware-logger': 3.410.0
-      '@aws-sdk/middleware-recursion-detection': 3.410.0
-      '@aws-sdk/middleware-sdk-sts': 3.410.0
-      '@aws-sdk/middleware-signing': 3.410.0
-      '@aws-sdk/middleware-user-agent': 3.410.0
-      '@aws-sdk/types': 3.410.0
-      '@aws-sdk/util-endpoints': 3.410.0
-      '@aws-sdk/util-user-agent-browser': 3.410.0
-      '@aws-sdk/util-user-agent-node': 3.410.0
-      '@smithy/config-resolver': 2.0.9
-      '@smithy/fetch-http-handler': 2.1.4
-      '@smithy/hash-node': 2.0.8
-      '@smithy/invalid-dependency': 2.0.8
-      '@smithy/middleware-content-length': 2.0.10
-      '@smithy/middleware-endpoint': 2.0.8
-      '@smithy/middleware-retry': 2.0.11
-      '@smithy/middleware-serde': 2.0.8
-      '@smithy/middleware-stack': 2.0.1
-      '@smithy/node-config-provider': 2.0.11
-      '@smithy/node-http-handler': 2.5.0
-      '@smithy/protocol-http': 3.0.10
-      '@smithy/smithy-client': 2.1.5
-      '@smithy/types': 2.6.0
-      '@smithy/url-parser': 2.0.8
-      '@smithy/util-base64': 2.0.0
-      '@smithy/util-body-length-browser': 2.0.0
-      '@smithy/util-body-length-node': 2.1.0
-      '@smithy/util-defaults-mode-browser': 2.0.9
-      '@smithy/util-defaults-mode-node': 2.0.11
-      '@smithy/util-retry': 2.0.1
-      '@smithy/util-utf8': 2.0.0
-      fast-xml-parser: 4.2.5
-      tslib: 2.6.2
+      '@aws-sdk/types': 3.609.0
+      '@smithy/property-provider': 3.1.3
+      '@smithy/shared-ini-file-loader': 3.1.4
+      '@smithy/types': 3.3.0
+      tslib: 2.7.0
+
+  '@aws-sdk/credential-provider-sso@3.620.0(@aws-sdk/client-sso-oidc@3.620.0(@aws-sdk/client-sts@3.620.0))':
+    dependencies:
+      '@aws-sdk/client-sso': 3.620.0
+      '@aws-sdk/token-providers': 3.614.0(@aws-sdk/client-sso-oidc@3.620.0(@aws-sdk/client-sts@3.620.0))
+      '@aws-sdk/types': 3.609.0
+      '@smithy/property-provider': 3.1.3
+      '@smithy/shared-ini-file-loader': 3.1.4
+      '@smithy/types': 3.3.0
+      tslib: 2.7.0
     transitivePeerDependencies:
+      - '@aws-sdk/client-sso-oidc'
       - aws-crt
 
-  '@aws-sdk/credential-provider-env@3.410.0':
+  '@aws-sdk/credential-provider-web-identity@3.609.0(@aws-sdk/client-sts@3.620.0)':
     dependencies:
-      '@aws-sdk/types': 3.410.0
-      '@smithy/property-provider': 2.0.9
-      '@smithy/types': 2.6.0
-      tslib: 2.6.2
+      '@aws-sdk/client-sts': 3.620.0
+      '@aws-sdk/types': 3.609.0
+      '@smithy/property-provider': 3.1.3
+      '@smithy/types': 3.3.0
+      tslib: 2.7.0
 
-  '@aws-sdk/credential-provider-ini@3.410.0':
+  '@aws-sdk/lib-storage@3.620.0(@aws-sdk/client-s3@3.620.0)':
     dependencies:
-      '@aws-sdk/credential-provider-env': 3.410.0
-      '@aws-sdk/credential-provider-process': 3.410.0
-      '@aws-sdk/credential-provider-sso': 3.410.0
-      '@aws-sdk/credential-provider-web-identity': 3.410.0
-      '@aws-sdk/types': 3.410.0
-      '@smithy/credential-provider-imds': 2.0.11
-      '@smithy/property-provider': 2.0.9
-      '@smithy/shared-ini-file-loader': 2.0.10
-      '@smithy/types': 2.6.0
-      tslib: 2.6.2
-    transitivePeerDependencies:
-      - aws-crt
-
-  '@aws-sdk/credential-provider-node@3.410.0':
-    dependencies:
-      '@aws-sdk/credential-provider-env': 3.410.0
-      '@aws-sdk/credential-provider-ini': 3.410.0
-      '@aws-sdk/credential-provider-process': 3.410.0
-      '@aws-sdk/credential-provider-sso': 3.410.0
-      '@aws-sdk/credential-provider-web-identity': 3.410.0
-      '@aws-sdk/types': 3.410.0
-      '@smithy/credential-provider-imds': 2.0.11
-      '@smithy/property-provider': 2.0.9
-      '@smithy/shared-ini-file-loader': 2.0.10
-      '@smithy/types': 2.6.0
-      tslib: 2.6.2
-    transitivePeerDependencies:
-      - aws-crt
-
-  '@aws-sdk/credential-provider-process@3.410.0':
-    dependencies:
-      '@aws-sdk/types': 3.410.0
-      '@smithy/property-provider': 2.0.9
-      '@smithy/shared-ini-file-loader': 2.0.10
-      '@smithy/types': 2.6.0
-      tslib: 2.6.2
-
-  '@aws-sdk/credential-provider-sso@3.410.0':
-    dependencies:
-      '@aws-sdk/client-sso': 3.410.0
-      '@aws-sdk/token-providers': 3.410.0
-      '@aws-sdk/types': 3.410.0
-      '@smithy/property-provider': 2.0.9
-      '@smithy/shared-ini-file-loader': 2.0.10
-      '@smithy/types': 2.6.0
-      tslib: 2.6.2
-    transitivePeerDependencies:
-      - aws-crt
-
-  '@aws-sdk/credential-provider-web-identity@3.410.0':
-    dependencies:
-      '@aws-sdk/types': 3.410.0
-      '@smithy/property-provider': 2.0.9
-      '@smithy/types': 2.6.0
-      tslib: 2.6.2
-
-  '@aws-sdk/lib-storage@3.412.0(@aws-sdk/client-s3@3.412.0)':
-    dependencies:
-      '@aws-sdk/client-s3': 3.412.0
-      '@smithy/abort-controller': 2.0.14
-      '@smithy/middleware-endpoint': 2.0.8
-      '@smithy/smithy-client': 2.1.5
+      '@aws-sdk/client-s3': 3.620.0
+      '@smithy/abort-controller': 3.1.1
+      '@smithy/middleware-endpoint': 3.1.0
+      '@smithy/smithy-client': 3.1.11
       buffer: 5.6.0
       events: 3.3.0
       stream-browserify: 3.0.0
-      tslib: 2.6.2
+      tslib: 2.6.3
 
-  '@aws-sdk/middleware-bucket-endpoint@3.410.0':
+  '@aws-sdk/middleware-bucket-endpoint@3.620.0':
     dependencies:
-      '@aws-sdk/types': 3.410.0
-      '@aws-sdk/util-arn-parser': 3.310.0
-      '@smithy/node-config-provider': 2.0.11
-      '@smithy/protocol-http': 3.0.10
-      '@smithy/types': 2.6.0
-      '@smithy/util-config-provider': 2.0.0
-      tslib: 2.6.2
+      '@aws-sdk/types': 3.609.0
+      '@aws-sdk/util-arn-parser': 3.568.0
+      '@smithy/node-config-provider': 3.1.4
+      '@smithy/protocol-http': 4.1.0
+      '@smithy/types': 3.3.0
+      '@smithy/util-config-provider': 3.0.0
+      tslib: 2.7.0
 
-  '@aws-sdk/middleware-expect-continue@3.410.0':
+  '@aws-sdk/middleware-expect-continue@3.620.0':
     dependencies:
-      '@aws-sdk/types': 3.410.0
-      '@smithy/protocol-http': 3.0.10
-      '@smithy/types': 2.6.0
-      tslib: 2.6.2
+      '@aws-sdk/types': 3.609.0
+      '@smithy/protocol-http': 4.1.0
+      '@smithy/types': 3.3.0
+      tslib: 2.7.0
 
-  '@aws-sdk/middleware-flexible-checksums@3.410.0':
+  '@aws-sdk/middleware-flexible-checksums@3.620.0':
     dependencies:
-      '@aws-crypto/crc32': 3.0.0
-      '@aws-crypto/crc32c': 3.0.0
-      '@aws-sdk/types': 3.410.0
-      '@smithy/is-array-buffer': 2.0.0
-      '@smithy/protocol-http': 3.0.10
-      '@smithy/types': 2.6.0
-      '@smithy/util-utf8': 2.0.0
-      tslib: 2.6.2
+      '@aws-crypto/crc32': 5.2.0
+      '@aws-crypto/crc32c': 5.2.0
+      '@aws-sdk/types': 3.609.0
+      '@smithy/is-array-buffer': 3.0.0
+      '@smithy/protocol-http': 4.1.0
+      '@smithy/types': 3.3.0
+      '@smithy/util-utf8': 3.0.0
+      tslib: 2.7.0
 
-  '@aws-sdk/middleware-host-header@3.410.0':
+  '@aws-sdk/middleware-host-header@3.620.0':
     dependencies:
-      '@aws-sdk/types': 3.410.0
-      '@smithy/protocol-http': 3.0.10
-      '@smithy/types': 2.6.0
-      tslib: 2.6.2
+      '@aws-sdk/types': 3.609.0
+      '@smithy/protocol-http': 4.1.0
+      '@smithy/types': 3.3.0
+      tslib: 2.7.0
 
-  '@aws-sdk/middleware-location-constraint@3.410.0':
+  '@aws-sdk/middleware-location-constraint@3.609.0':
     dependencies:
-      '@aws-sdk/types': 3.410.0
-      '@smithy/types': 2.6.0
-      tslib: 2.6.2
+      '@aws-sdk/types': 3.609.0
+      '@smithy/types': 3.3.0
+      tslib: 2.7.0
 
-  '@aws-sdk/middleware-logger@3.410.0':
+  '@aws-sdk/middleware-logger@3.609.0':
     dependencies:
-      '@aws-sdk/types': 3.410.0
-      '@smithy/types': 2.6.0
-      tslib: 2.6.2
+      '@aws-sdk/types': 3.609.0
+      '@smithy/types': 3.3.0
+      tslib: 2.7.0
 
-  '@aws-sdk/middleware-recursion-detection@3.410.0':
+  '@aws-sdk/middleware-recursion-detection@3.620.0':
     dependencies:
-      '@aws-sdk/types': 3.410.0
-      '@smithy/protocol-http': 3.0.10
-      '@smithy/types': 2.6.0
-      tslib: 2.6.2
+      '@aws-sdk/types': 3.609.0
+      '@smithy/protocol-http': 4.1.0
+      '@smithy/types': 3.3.0
+      tslib: 2.7.0
 
-  '@aws-sdk/middleware-sdk-s3@3.410.0':
+  '@aws-sdk/middleware-sdk-s3@3.620.0':
     dependencies:
-      '@aws-sdk/types': 3.410.0
-      '@aws-sdk/util-arn-parser': 3.310.0
-      '@smithy/protocol-http': 3.0.10
-      '@smithy/types': 2.6.0
-      tslib: 2.6.2
+      '@aws-sdk/types': 3.609.0
+      '@aws-sdk/util-arn-parser': 3.568.0
+      '@smithy/node-config-provider': 3.1.4
+      '@smithy/protocol-http': 4.1.0
+      '@smithy/signature-v4': 4.1.0
+      '@smithy/smithy-client': 3.1.11
+      '@smithy/types': 3.3.0
+      '@smithy/util-config-provider': 3.0.0
+      '@smithy/util-stream': 3.1.3
+      '@smithy/util-utf8': 3.0.0
+      tslib: 2.7.0
 
-  '@aws-sdk/middleware-sdk-sts@3.410.0':
+  '@aws-sdk/middleware-signing@3.620.0':
     dependencies:
-      '@aws-sdk/middleware-signing': 3.410.0
-      '@aws-sdk/types': 3.410.0
-      '@smithy/types': 2.6.0
-      tslib: 2.6.2
+      '@aws-sdk/types': 3.609.0
+      '@smithy/property-provider': 3.1.3
+      '@smithy/protocol-http': 4.1.0
+      '@smithy/signature-v4': 4.1.0
+      '@smithy/types': 3.3.0
+      '@smithy/util-middleware': 3.0.3
+      tslib: 2.7.0
 
-  '@aws-sdk/middleware-signing@3.410.0':
+  '@aws-sdk/middleware-ssec@3.609.0':
     dependencies:
-      '@aws-sdk/types': 3.410.0
-      '@smithy/property-provider': 2.0.9
-      '@smithy/protocol-http': 3.0.10
-      '@smithy/signature-v4': 2.0.5
-      '@smithy/types': 2.6.0
-      '@smithy/util-middleware': 2.0.1
-      tslib: 2.6.2
+      '@aws-sdk/types': 3.609.0
+      '@smithy/types': 3.3.0
+      tslib: 2.7.0
 
-  '@aws-sdk/middleware-ssec@3.410.0':
+  '@aws-sdk/middleware-user-agent@3.620.0':
     dependencies:
-      '@aws-sdk/types': 3.410.0
-      '@smithy/types': 2.6.0
-      tslib: 2.6.2
+      '@aws-sdk/types': 3.609.0
+      '@aws-sdk/util-endpoints': 3.614.0
+      '@smithy/protocol-http': 4.1.0
+      '@smithy/types': 3.3.0
+      tslib: 2.7.0
 
-  '@aws-sdk/middleware-user-agent@3.410.0':
+  '@aws-sdk/region-config-resolver@3.614.0':
     dependencies:
-      '@aws-sdk/types': 3.410.0
-      '@aws-sdk/util-endpoints': 3.410.0
-      '@smithy/protocol-http': 3.0.10
-      '@smithy/types': 2.6.0
-      tslib: 2.6.2
+      '@aws-sdk/types': 3.609.0
+      '@smithy/node-config-provider': 3.1.4
+      '@smithy/types': 3.3.0
+      '@smithy/util-config-provider': 3.0.0
+      '@smithy/util-middleware': 3.0.3
+      tslib: 2.7.0
 
-  '@aws-sdk/signature-v4-multi-region@3.412.0':
+  '@aws-sdk/signature-v4-multi-region@3.620.0':
     dependencies:
-      '@aws-sdk/types': 3.410.0
-      '@smithy/protocol-http': 3.0.10
-      '@smithy/signature-v4': 2.0.5
-      '@smithy/types': 2.6.0
-      tslib: 2.6.2
+      '@aws-sdk/middleware-sdk-s3': 3.620.0
+      '@aws-sdk/types': 3.609.0
+      '@smithy/protocol-http': 4.1.0
+      '@smithy/signature-v4': 4.1.0
+      '@smithy/types': 3.3.0
+      tslib: 2.7.0
 
-  '@aws-sdk/token-providers@3.410.0':
+  '@aws-sdk/token-providers@3.614.0(@aws-sdk/client-sso-oidc@3.620.0(@aws-sdk/client-sts@3.620.0))':
     dependencies:
-      '@aws-crypto/sha256-browser': 3.0.0
-      '@aws-crypto/sha256-js': 3.0.0
-      '@aws-sdk/middleware-host-header': 3.410.0
-      '@aws-sdk/middleware-logger': 3.410.0
-      '@aws-sdk/middleware-recursion-detection': 3.410.0
-      '@aws-sdk/middleware-user-agent': 3.410.0
-      '@aws-sdk/types': 3.410.0
-      '@aws-sdk/util-endpoints': 3.410.0
-      '@aws-sdk/util-user-agent-browser': 3.410.0
-      '@aws-sdk/util-user-agent-node': 3.410.0
-      '@smithy/config-resolver': 2.0.9
-      '@smithy/fetch-http-handler': 2.1.4
-      '@smithy/hash-node': 2.0.8
-      '@smithy/invalid-dependency': 2.0.8
-      '@smithy/middleware-content-length': 2.0.10
-      '@smithy/middleware-endpoint': 2.0.8
-      '@smithy/middleware-retry': 2.0.11
-      '@smithy/middleware-serde': 2.0.8
-      '@smithy/middleware-stack': 2.0.1
-      '@smithy/node-config-provider': 2.0.11
-      '@smithy/node-http-handler': 2.5.0
-      '@smithy/property-provider': 2.0.9
-      '@smithy/protocol-http': 3.0.10
-      '@smithy/shared-ini-file-loader': 2.0.10
-      '@smithy/smithy-client': 2.1.5
-      '@smithy/types': 2.6.0
-      '@smithy/url-parser': 2.0.8
-      '@smithy/util-base64': 2.0.0
-      '@smithy/util-body-length-browser': 2.0.0
-      '@smithy/util-body-length-node': 2.1.0
-      '@smithy/util-defaults-mode-browser': 2.0.9
-      '@smithy/util-defaults-mode-node': 2.0.11
-      '@smithy/util-retry': 2.0.1
-      '@smithy/util-utf8': 2.0.0
-      tslib: 2.6.2
-    transitivePeerDependencies:
-      - aws-crt
+      '@aws-sdk/client-sso-oidc': 3.620.0(@aws-sdk/client-sts@3.620.0)
+      '@aws-sdk/types': 3.609.0
+      '@smithy/property-provider': 3.1.3
+      '@smithy/shared-ini-file-loader': 3.1.4
+      '@smithy/types': 3.3.0
+      tslib: 2.7.0
 
-  '@aws-sdk/types@3.410.0':
+  '@aws-sdk/types@3.609.0':
     dependencies:
-      '@smithy/types': 2.6.0
-      tslib: 2.6.2
+      '@smithy/types': 3.3.0
+      tslib: 2.7.0
 
-  '@aws-sdk/types@3.413.0':
+  '@aws-sdk/util-arn-parser@3.568.0':
     dependencies:
-      '@smithy/types': 2.6.0
-      tslib: 2.6.2
+      tslib: 2.7.0
 
-  '@aws-sdk/util-arn-parser@3.310.0':
+  '@aws-sdk/util-endpoints@3.614.0':
     dependencies:
-      tslib: 2.6.2
-
-  '@aws-sdk/util-endpoints@3.410.0':
-    dependencies:
-      '@aws-sdk/types': 3.410.0
-      tslib: 2.6.2
+      '@aws-sdk/types': 3.609.0
+      '@smithy/types': 3.3.0
+      '@smithy/util-endpoints': 2.0.5
+      tslib: 2.7.0
 
   '@aws-sdk/util-locate-window@3.208.0':
     dependencies:
-      tslib: 2.6.2
+      tslib: 2.7.0
 
-  '@aws-sdk/util-user-agent-browser@3.410.0':
+  '@aws-sdk/util-user-agent-browser@3.609.0':
     dependencies:
-      '@aws-sdk/types': 3.410.0
-      '@smithy/types': 2.6.0
+      '@aws-sdk/types': 3.609.0
+      '@smithy/types': 3.3.0
       bowser: 2.11.0
-      tslib: 2.6.2
+      tslib: 2.7.0
 
-  '@aws-sdk/util-user-agent-node@3.410.0':
+  '@aws-sdk/util-user-agent-node@3.614.0':
     dependencies:
-      '@aws-sdk/types': 3.410.0
-      '@smithy/node-config-provider': 2.0.11
-      '@smithy/types': 2.6.0
-      tslib: 2.6.2
+      '@aws-sdk/types': 3.609.0
+      '@smithy/node-config-provider': 3.1.4
+      '@smithy/types': 3.3.0
+      tslib: 2.7.0
 
-  '@aws-sdk/util-utf8-browser@3.259.0':
+  '@aws-sdk/xml-builder@3.609.0':
     dependencies:
-      tslib: 2.6.2
-
-  '@aws-sdk/xml-builder@3.310.0':
-    dependencies:
-      tslib: 2.6.2
+      '@smithy/types': 3.3.0
+      tslib: 2.7.0
 
   '@babel/code-frame@7.23.5':
     dependencies:
       '@babel/highlight': 7.23.4
       chalk: 2.4.2
 
+  '@babel/code-frame@7.24.7':
+    dependencies:
+      '@babel/highlight': 7.24.7
+      picocolors: 1.0.1
+
   '@babel/compat-data@7.23.5': {}
 
+  '@babel/compat-data@7.24.7': {}
+
   '@babel/core@7.23.5':
     dependencies:
       '@ampproject/remapping': 2.2.1
@@ -11912,32 +12274,32 @@ snapshots:
       '@babel/helper-compilation-targets': 7.22.15
       '@babel/helper-module-transforms': 7.23.3(@babel/core@7.23.5)
       '@babel/helpers': 7.23.5
-      '@babel/parser': 7.23.9
+      '@babel/parser': 7.24.7
       '@babel/template': 7.22.15
       '@babel/traverse': 7.23.5
-      '@babel/types': 7.23.5
+      '@babel/types': 7.24.7
       convert-source-map: 2.0.0
-      debug: 4.3.4(supports-color@8.1.1)
+      debug: 4.3.7(supports-color@8.1.1)
       gensync: 1.0.0-beta.2
       json5: 2.2.3
       semver: 6.3.1
     transitivePeerDependencies:
       - supports-color
 
-  '@babel/core@7.24.0':
+  '@babel/core@7.24.7':
     dependencies:
       '@ampproject/remapping': 2.2.1
-      '@babel/code-frame': 7.23.5
-      '@babel/generator': 7.23.6
-      '@babel/helper-compilation-targets': 7.23.6
-      '@babel/helper-module-transforms': 7.23.3(@babel/core@7.24.0)
-      '@babel/helpers': 7.24.0
-      '@babel/parser': 7.24.0
-      '@babel/template': 7.24.0
-      '@babel/traverse': 7.24.0
-      '@babel/types': 7.24.0
+      '@babel/code-frame': 7.24.7
+      '@babel/generator': 7.24.7
+      '@babel/helper-compilation-targets': 7.24.7
+      '@babel/helper-module-transforms': 7.24.7(@babel/core@7.24.7)
+      '@babel/helpers': 7.24.7
+      '@babel/parser': 7.24.7
+      '@babel/template': 7.24.7
+      '@babel/traverse': 7.24.7
+      '@babel/types': 7.24.7
       convert-source-map: 2.0.0
-      debug: 4.3.4(supports-color@8.1.1)
+      debug: 4.3.7(supports-color@8.1.1)
       gensync: 1.0.0-beta.2
       json5: 2.2.3
       semver: 6.3.1
@@ -11946,26 +12308,18 @@ snapshots:
 
   '@babel/generator@7.23.5':
     dependencies:
-      '@babel/types': 7.23.5
+      '@babel/types': 7.24.7
       '@jridgewell/gen-mapping': 0.3.2
       '@jridgewell/trace-mapping': 0.3.18
       jsesc: 2.5.2
 
-  '@babel/generator@7.23.6':
+  '@babel/generator@7.24.7':
     dependencies:
-      '@babel/types': 7.24.0
-      '@jridgewell/gen-mapping': 0.3.2
-      '@jridgewell/trace-mapping': 0.3.18
+      '@babel/types': 7.24.7
+      '@jridgewell/gen-mapping': 0.3.5
+      '@jridgewell/trace-mapping': 0.3.25
       jsesc: 2.5.2
 
-  '@babel/helper-annotate-as-pure@7.22.5':
-    dependencies:
-      '@babel/types': 7.24.0
-
-  '@babel/helper-builder-binary-assignment-operator-visitor@7.22.15':
-    dependencies:
-      '@babel/types': 7.24.0
-
   '@babel/helper-compilation-targets@7.22.15':
     dependencies:
       '@babel/compat-data': 7.23.5
@@ -11974,63 +12328,48 @@ snapshots:
       lru-cache: 5.1.1
       semver: 6.3.1
 
-  '@babel/helper-compilation-targets@7.23.6':
+  '@babel/helper-compilation-targets@7.24.7':
     dependencies:
-      '@babel/compat-data': 7.23.5
-      '@babel/helper-validator-option': 7.23.5
+      '@babel/compat-data': 7.24.7
+      '@babel/helper-validator-option': 7.24.7
       browserslist: 4.23.0
       lru-cache: 5.1.1
       semver: 6.3.1
 
-  '@babel/helper-create-class-features-plugin@7.23.5(@babel/core@7.24.0)':
-    dependencies:
-      '@babel/core': 7.24.0
-      '@babel/helper-annotate-as-pure': 7.22.5
-      '@babel/helper-environment-visitor': 7.22.20
-      '@babel/helper-function-name': 7.23.0
-      '@babel/helper-member-expression-to-functions': 7.23.0
-      '@babel/helper-optimise-call-expression': 7.22.5
-      '@babel/helper-replace-supers': 7.22.20(@babel/core@7.24.0)
-      '@babel/helper-skip-transparent-expression-wrappers': 7.22.5
-      '@babel/helper-split-export-declaration': 7.22.6
-      semver: 6.3.1
-
-  '@babel/helper-create-regexp-features-plugin@7.22.15(@babel/core@7.24.0)':
-    dependencies:
-      '@babel/core': 7.24.0
-      '@babel/helper-annotate-as-pure': 7.22.5
-      regexpu-core: 5.3.2
-      semver: 6.3.1
-
-  '@babel/helper-define-polyfill-provider@0.4.3(@babel/core@7.24.0)':
-    dependencies:
-      '@babel/core': 7.24.0
-      '@babel/helper-compilation-targets': 7.23.6
-      '@babel/helper-plugin-utils': 7.22.5
-      debug: 4.3.4(supports-color@8.1.1)
-      lodash.debounce: 4.0.8
-      resolve: 1.22.8
-    transitivePeerDependencies:
-      - supports-color
-
   '@babel/helper-environment-visitor@7.22.20': {}
 
+  '@babel/helper-environment-visitor@7.24.7':
+    dependencies:
+      '@babel/types': 7.24.7
+
   '@babel/helper-function-name@7.23.0':
     dependencies:
-      '@babel/template': 7.24.0
-      '@babel/types': 7.24.0
+      '@babel/template': 7.24.7
+      '@babel/types': 7.24.7
+
+  '@babel/helper-function-name@7.24.7':
+    dependencies:
+      '@babel/template': 7.24.7
+      '@babel/types': 7.24.7
 
   '@babel/helper-hoist-variables@7.22.5':
     dependencies:
-      '@babel/types': 7.24.0
+      '@babel/types': 7.24.7
 
-  '@babel/helper-member-expression-to-functions@7.23.0':
+  '@babel/helper-hoist-variables@7.24.7':
     dependencies:
-      '@babel/types': 7.24.0
+      '@babel/types': 7.24.7
 
   '@babel/helper-module-imports@7.22.15':
     dependencies:
-      '@babel/types': 7.23.5
+      '@babel/types': 7.24.7
+
+  '@babel/helper-module-imports@7.24.7':
+    dependencies:
+      '@babel/traverse': 7.24.7
+      '@babel/types': 7.24.7
+    transitivePeerDependencies:
+      - supports-color
 
   '@babel/helper-module-transforms@7.23.3(@babel/core@7.23.5)':
     dependencies:
@@ -12039,127 +12378,89 @@ snapshots:
       '@babel/helper-module-imports': 7.22.15
       '@babel/helper-simple-access': 7.22.5
       '@babel/helper-split-export-declaration': 7.22.6
-      '@babel/helper-validator-identifier': 7.22.20
+      '@babel/helper-validator-identifier': 7.24.7
 
-  '@babel/helper-module-transforms@7.23.3(@babel/core@7.24.0)':
+  '@babel/helper-module-transforms@7.24.7(@babel/core@7.24.7)':
     dependencies:
-      '@babel/core': 7.24.0
-      '@babel/helper-environment-visitor': 7.22.20
-      '@babel/helper-module-imports': 7.22.15
-      '@babel/helper-simple-access': 7.22.5
-      '@babel/helper-split-export-declaration': 7.22.6
-      '@babel/helper-validator-identifier': 7.22.20
-
-  '@babel/helper-optimise-call-expression@7.22.5':
-    dependencies:
-      '@babel/types': 7.24.0
+      '@babel/core': 7.24.7
+      '@babel/helper-environment-visitor': 7.24.7
+      '@babel/helper-module-imports': 7.24.7
+      '@babel/helper-simple-access': 7.24.7
+      '@babel/helper-split-export-declaration': 7.24.7
+      '@babel/helper-validator-identifier': 7.24.7
+    transitivePeerDependencies:
+      - supports-color
 
   '@babel/helper-plugin-utils@7.22.5': {}
 
-  '@babel/helper-remap-async-to-generator@7.22.20(@babel/core@7.24.0)':
-    dependencies:
-      '@babel/core': 7.24.0
-      '@babel/helper-annotate-as-pure': 7.22.5
-      '@babel/helper-environment-visitor': 7.22.20
-      '@babel/helper-wrap-function': 7.22.20
-
-  '@babel/helper-replace-supers@7.22.20(@babel/core@7.24.0)':
-    dependencies:
-      '@babel/core': 7.24.0
-      '@babel/helper-environment-visitor': 7.22.20
-      '@babel/helper-member-expression-to-functions': 7.23.0
-      '@babel/helper-optimise-call-expression': 7.22.5
-
   '@babel/helper-simple-access@7.22.5':
     dependencies:
-      '@babel/types': 7.23.5
+      '@babel/types': 7.24.7
 
-  '@babel/helper-skip-transparent-expression-wrappers@7.22.5':
+  '@babel/helper-simple-access@7.24.7':
     dependencies:
-      '@babel/types': 7.24.0
+      '@babel/traverse': 7.24.7
+      '@babel/types': 7.24.7
+    transitivePeerDependencies:
+      - supports-color
 
   '@babel/helper-split-export-declaration@7.22.6':
     dependencies:
-      '@babel/types': 7.23.5
+      '@babel/types': 7.24.7
 
-  '@babel/helper-string-parser@7.23.4': {}
+  '@babel/helper-split-export-declaration@7.24.7':
+    dependencies:
+      '@babel/types': 7.24.7
 
-  '@babel/helper-validator-identifier@7.22.20': {}
+  '@babel/helper-string-parser@7.24.7': {}
+
+  '@babel/helper-string-parser@7.24.8': {}
+
+  '@babel/helper-validator-identifier@7.24.7': {}
 
   '@babel/helper-validator-option@7.23.5': {}
 
-  '@babel/helper-wrap-function@7.22.20':
-    dependencies:
-      '@babel/helper-function-name': 7.23.0
-      '@babel/template': 7.24.0
-      '@babel/types': 7.24.0
+  '@babel/helper-validator-option@7.24.7': {}
 
   '@babel/helpers@7.23.5':
     dependencies:
       '@babel/template': 7.22.15
       '@babel/traverse': 7.23.5
-      '@babel/types': 7.23.5
+      '@babel/types': 7.24.7
     transitivePeerDependencies:
       - supports-color
 
-  '@babel/helpers@7.24.0':
+  '@babel/helpers@7.24.7':
     dependencies:
-      '@babel/template': 7.24.0
-      '@babel/traverse': 7.24.0
-      '@babel/types': 7.24.0
-    transitivePeerDependencies:
-      - supports-color
+      '@babel/template': 7.24.7
+      '@babel/types': 7.24.7
 
   '@babel/highlight@7.23.4':
     dependencies:
-      '@babel/helper-validator-identifier': 7.22.20
+      '@babel/helper-validator-identifier': 7.24.7
       chalk: 2.4.2
       js-tokens: 4.0.0
 
-  '@babel/parser@7.23.9':
+  '@babel/highlight@7.24.7':
     dependencies:
-      '@babel/types': 7.23.5
+      '@babel/helper-validator-identifier': 7.24.7
+      chalk: 2.4.2
+      js-tokens: 4.0.0
+      picocolors: 1.0.1
 
-  '@babel/parser@7.24.0':
+  '@babel/parser@7.24.7':
     dependencies:
-      '@babel/types': 7.24.0
+      '@babel/types': 7.24.7
 
-  '@babel/parser@7.24.5':
+  '@babel/parser@7.25.6':
     dependencies:
-      '@babel/types': 7.24.0
-
-  '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.23.3(@babel/core@7.24.0)':
-    dependencies:
-      '@babel/core': 7.24.0
-      '@babel/helper-plugin-utils': 7.22.5
-
-  '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.23.3(@babel/core@7.24.0)':
-    dependencies:
-      '@babel/core': 7.24.0
-      '@babel/helper-plugin-utils': 7.22.5
-      '@babel/helper-skip-transparent-expression-wrappers': 7.22.5
-      '@babel/plugin-transform-optional-chaining': 7.23.4(@babel/core@7.24.0)
-
-  '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.23.3(@babel/core@7.24.0)':
-    dependencies:
-      '@babel/core': 7.24.0
-      '@babel/helper-environment-visitor': 7.22.20
-      '@babel/helper-plugin-utils': 7.22.5
-
-  '@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2(@babel/core@7.24.0)':
-    dependencies:
-      '@babel/core': 7.24.0
+      '@babel/types': 7.25.6
 
   '@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.23.5)':
     dependencies:
       '@babel/core': 7.23.5
       '@babel/helper-plugin-utils': 7.22.5
 
-  '@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.24.0)':
-    dependencies:
-      '@babel/core': 7.24.0
-      '@babel/helper-plugin-utils': 7.22.5
-
   '@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.23.5)':
     dependencies:
       '@babel/core': 7.23.5
@@ -12170,666 +12471,145 @@ snapshots:
       '@babel/core': 7.23.5
       '@babel/helper-plugin-utils': 7.22.5
 
-  '@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.24.0)':
-    dependencies:
-      '@babel/core': 7.24.0
-      '@babel/helper-plugin-utils': 7.22.5
-
-  '@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.24.0)':
-    dependencies:
-      '@babel/core': 7.24.0
-      '@babel/helper-plugin-utils': 7.22.5
-
-  '@babel/plugin-syntax-dynamic-import@7.8.3(@babel/core@7.24.0)':
-    dependencies:
-      '@babel/core': 7.24.0
-      '@babel/helper-plugin-utils': 7.22.5
-
-  '@babel/plugin-syntax-export-namespace-from@7.8.3(@babel/core@7.24.0)':
-    dependencies:
-      '@babel/core': 7.24.0
-      '@babel/helper-plugin-utils': 7.22.5
-
-  '@babel/plugin-syntax-flow@7.23.3(@babel/core@7.24.0)':
-    dependencies:
-      '@babel/core': 7.24.0
-      '@babel/helper-plugin-utils': 7.22.5
-
-  '@babel/plugin-syntax-import-assertions@7.23.3(@babel/core@7.24.0)':
-    dependencies:
-      '@babel/core': 7.24.0
-      '@babel/helper-plugin-utils': 7.22.5
-
-  '@babel/plugin-syntax-import-attributes@7.23.3(@babel/core@7.24.0)':
-    dependencies:
-      '@babel/core': 7.24.0
-      '@babel/helper-plugin-utils': 7.22.5
-
   '@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.23.5)':
     dependencies:
       '@babel/core': 7.23.5
       '@babel/helper-plugin-utils': 7.22.5
 
-  '@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.24.0)':
-    dependencies:
-      '@babel/core': 7.24.0
-      '@babel/helper-plugin-utils': 7.22.5
-
   '@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.23.5)':
     dependencies:
       '@babel/core': 7.23.5
       '@babel/helper-plugin-utils': 7.22.5
 
-  '@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.24.0)':
-    dependencies:
-      '@babel/core': 7.24.0
-      '@babel/helper-plugin-utils': 7.22.5
-
   '@babel/plugin-syntax-jsx@7.23.3(@babel/core@7.23.5)':
     dependencies:
       '@babel/core': 7.23.5
       '@babel/helper-plugin-utils': 7.22.5
 
-  '@babel/plugin-syntax-jsx@7.23.3(@babel/core@7.24.0)':
-    dependencies:
-      '@babel/core': 7.24.0
-      '@babel/helper-plugin-utils': 7.22.5
-
   '@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.23.5)':
     dependencies:
       '@babel/core': 7.23.5
       '@babel/helper-plugin-utils': 7.22.5
 
-  '@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.24.0)':
-    dependencies:
-      '@babel/core': 7.24.0
-      '@babel/helper-plugin-utils': 7.22.5
-
   '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.23.5)':
     dependencies:
       '@babel/core': 7.23.5
       '@babel/helper-plugin-utils': 7.22.5
 
-  '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.24.0)':
-    dependencies:
-      '@babel/core': 7.24.0
-      '@babel/helper-plugin-utils': 7.22.5
-
   '@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.23.5)':
     dependencies:
       '@babel/core': 7.23.5
       '@babel/helper-plugin-utils': 7.22.5
 
-  '@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.24.0)':
-    dependencies:
-      '@babel/core': 7.24.0
-      '@babel/helper-plugin-utils': 7.22.5
-
   '@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.23.5)':
     dependencies:
       '@babel/core': 7.23.5
       '@babel/helper-plugin-utils': 7.22.5
 
-  '@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.24.0)':
-    dependencies:
-      '@babel/core': 7.24.0
-      '@babel/helper-plugin-utils': 7.22.5
-
   '@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.23.5)':
     dependencies:
       '@babel/core': 7.23.5
       '@babel/helper-plugin-utils': 7.22.5
 
-  '@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.24.0)':
-    dependencies:
-      '@babel/core': 7.24.0
-      '@babel/helper-plugin-utils': 7.22.5
-
   '@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.23.5)':
     dependencies:
       '@babel/core': 7.23.5
       '@babel/helper-plugin-utils': 7.22.5
 
-  '@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.24.0)':
-    dependencies:
-      '@babel/core': 7.24.0
-      '@babel/helper-plugin-utils': 7.22.5
-
-  '@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.24.0)':
-    dependencies:
-      '@babel/core': 7.24.0
-      '@babel/helper-plugin-utils': 7.22.5
-
   '@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.23.5)':
     dependencies:
       '@babel/core': 7.23.5
       '@babel/helper-plugin-utils': 7.22.5
 
-  '@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.24.0)':
-    dependencies:
-      '@babel/core': 7.24.0
-      '@babel/helper-plugin-utils': 7.22.5
-
   '@babel/plugin-syntax-typescript@7.23.3(@babel/core@7.23.5)':
     dependencies:
       '@babel/core': 7.23.5
       '@babel/helper-plugin-utils': 7.22.5
 
-  '@babel/plugin-syntax-typescript@7.23.3(@babel/core@7.24.0)':
-    dependencies:
-      '@babel/core': 7.24.0
-      '@babel/helper-plugin-utils': 7.22.5
-
-  '@babel/plugin-syntax-unicode-sets-regex@7.18.6(@babel/core@7.24.0)':
-    dependencies:
-      '@babel/core': 7.24.0
-      '@babel/helper-create-regexp-features-plugin': 7.22.15(@babel/core@7.24.0)
-      '@babel/helper-plugin-utils': 7.22.5
-
-  '@babel/plugin-transform-arrow-functions@7.23.3(@babel/core@7.24.0)':
-    dependencies:
-      '@babel/core': 7.24.0
-      '@babel/helper-plugin-utils': 7.22.5
-
-  '@babel/plugin-transform-async-generator-functions@7.23.4(@babel/core@7.24.0)':
-    dependencies:
-      '@babel/core': 7.24.0
-      '@babel/helper-environment-visitor': 7.22.20
-      '@babel/helper-plugin-utils': 7.22.5
-      '@babel/helper-remap-async-to-generator': 7.22.20(@babel/core@7.24.0)
-      '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.24.0)
-
-  '@babel/plugin-transform-async-to-generator@7.23.3(@babel/core@7.24.0)':
-    dependencies:
-      '@babel/core': 7.24.0
-      '@babel/helper-module-imports': 7.22.15
-      '@babel/helper-plugin-utils': 7.22.5
-      '@babel/helper-remap-async-to-generator': 7.22.20(@babel/core@7.24.0)
-
-  '@babel/plugin-transform-block-scoped-functions@7.23.3(@babel/core@7.24.0)':
-    dependencies:
-      '@babel/core': 7.24.0
-      '@babel/helper-plugin-utils': 7.22.5
-
-  '@babel/plugin-transform-block-scoping@7.23.4(@babel/core@7.24.0)':
-    dependencies:
-      '@babel/core': 7.24.0
-      '@babel/helper-plugin-utils': 7.22.5
-
-  '@babel/plugin-transform-class-properties@7.23.3(@babel/core@7.24.0)':
-    dependencies:
-      '@babel/core': 7.24.0
-      '@babel/helper-create-class-features-plugin': 7.23.5(@babel/core@7.24.0)
-      '@babel/helper-plugin-utils': 7.22.5
-
-  '@babel/plugin-transform-class-static-block@7.23.4(@babel/core@7.24.0)':
-    dependencies:
-      '@babel/core': 7.24.0
-      '@babel/helper-create-class-features-plugin': 7.23.5(@babel/core@7.24.0)
-      '@babel/helper-plugin-utils': 7.22.5
-      '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.24.0)
-
-  '@babel/plugin-transform-classes@7.23.5(@babel/core@7.24.0)':
-    dependencies:
-      '@babel/core': 7.24.0
-      '@babel/helper-annotate-as-pure': 7.22.5
-      '@babel/helper-compilation-targets': 7.23.6
-      '@babel/helper-environment-visitor': 7.22.20
-      '@babel/helper-function-name': 7.23.0
-      '@babel/helper-optimise-call-expression': 7.22.5
-      '@babel/helper-plugin-utils': 7.22.5
-      '@babel/helper-replace-supers': 7.22.20(@babel/core@7.24.0)
-      '@babel/helper-split-export-declaration': 7.22.6
-      globals: 11.12.0
-
-  '@babel/plugin-transform-computed-properties@7.23.3(@babel/core@7.24.0)':
-    dependencies:
-      '@babel/core': 7.24.0
-      '@babel/helper-plugin-utils': 7.22.5
-      '@babel/template': 7.24.0
-
-  '@babel/plugin-transform-destructuring@7.23.3(@babel/core@7.24.0)':
-    dependencies:
-      '@babel/core': 7.24.0
-      '@babel/helper-plugin-utils': 7.22.5
-
-  '@babel/plugin-transform-dotall-regex@7.23.3(@babel/core@7.24.0)':
-    dependencies:
-      '@babel/core': 7.24.0
-      '@babel/helper-create-regexp-features-plugin': 7.22.15(@babel/core@7.24.0)
-      '@babel/helper-plugin-utils': 7.22.5
-
-  '@babel/plugin-transform-duplicate-keys@7.23.3(@babel/core@7.24.0)':
-    dependencies:
-      '@babel/core': 7.24.0
-      '@babel/helper-plugin-utils': 7.22.5
-
-  '@babel/plugin-transform-dynamic-import@7.23.4(@babel/core@7.24.0)':
-    dependencies:
-      '@babel/core': 7.24.0
-      '@babel/helper-plugin-utils': 7.22.5
-      '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.24.0)
-
-  '@babel/plugin-transform-exponentiation-operator@7.23.3(@babel/core@7.24.0)':
-    dependencies:
-      '@babel/core': 7.24.0
-      '@babel/helper-builder-binary-assignment-operator-visitor': 7.22.15
-      '@babel/helper-plugin-utils': 7.22.5
-
-  '@babel/plugin-transform-export-namespace-from@7.23.4(@babel/core@7.24.0)':
-    dependencies:
-      '@babel/core': 7.24.0
-      '@babel/helper-plugin-utils': 7.22.5
-      '@babel/plugin-syntax-export-namespace-from': 7.8.3(@babel/core@7.24.0)
-
-  '@babel/plugin-transform-flow-strip-types@7.23.3(@babel/core@7.24.0)':
-    dependencies:
-      '@babel/core': 7.24.0
-      '@babel/helper-plugin-utils': 7.22.5
-      '@babel/plugin-syntax-flow': 7.23.3(@babel/core@7.24.0)
-
-  '@babel/plugin-transform-for-of@7.23.3(@babel/core@7.24.0)':
-    dependencies:
-      '@babel/core': 7.24.0
-      '@babel/helper-plugin-utils': 7.22.5
-
-  '@babel/plugin-transform-function-name@7.23.3(@babel/core@7.24.0)':
-    dependencies:
-      '@babel/core': 7.24.0
-      '@babel/helper-compilation-targets': 7.23.6
-      '@babel/helper-function-name': 7.23.0
-      '@babel/helper-plugin-utils': 7.22.5
-
-  '@babel/plugin-transform-json-strings@7.23.4(@babel/core@7.24.0)':
-    dependencies:
-      '@babel/core': 7.24.0
-      '@babel/helper-plugin-utils': 7.22.5
-      '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.24.0)
-
-  '@babel/plugin-transform-literals@7.23.3(@babel/core@7.24.0)':
-    dependencies:
-      '@babel/core': 7.24.0
-      '@babel/helper-plugin-utils': 7.22.5
-
-  '@babel/plugin-transform-logical-assignment-operators@7.23.4(@babel/core@7.24.0)':
-    dependencies:
-      '@babel/core': 7.24.0
-      '@babel/helper-plugin-utils': 7.22.5
-      '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.24.0)
-
-  '@babel/plugin-transform-member-expression-literals@7.23.3(@babel/core@7.24.0)':
-    dependencies:
-      '@babel/core': 7.24.0
-      '@babel/helper-plugin-utils': 7.22.5
-
-  '@babel/plugin-transform-modules-amd@7.23.3(@babel/core@7.24.0)':
-    dependencies:
-      '@babel/core': 7.24.0
-      '@babel/helper-module-transforms': 7.23.3(@babel/core@7.24.0)
-      '@babel/helper-plugin-utils': 7.22.5
-
-  '@babel/plugin-transform-modules-commonjs@7.23.3(@babel/core@7.24.0)':
-    dependencies:
-      '@babel/core': 7.24.0
-      '@babel/helper-module-transforms': 7.23.3(@babel/core@7.24.0)
-      '@babel/helper-plugin-utils': 7.22.5
-      '@babel/helper-simple-access': 7.22.5
-
-  '@babel/plugin-transform-modules-systemjs@7.23.3(@babel/core@7.24.0)':
-    dependencies:
-      '@babel/core': 7.24.0
-      '@babel/helper-hoist-variables': 7.22.5
-      '@babel/helper-module-transforms': 7.23.3(@babel/core@7.24.0)
-      '@babel/helper-plugin-utils': 7.22.5
-      '@babel/helper-validator-identifier': 7.22.20
-
-  '@babel/plugin-transform-modules-umd@7.23.3(@babel/core@7.24.0)':
-    dependencies:
-      '@babel/core': 7.24.0
-      '@babel/helper-module-transforms': 7.23.3(@babel/core@7.24.0)
-      '@babel/helper-plugin-utils': 7.22.5
-
-  '@babel/plugin-transform-named-capturing-groups-regex@7.22.5(@babel/core@7.24.0)':
-    dependencies:
-      '@babel/core': 7.24.0
-      '@babel/helper-create-regexp-features-plugin': 7.22.15(@babel/core@7.24.0)
-      '@babel/helper-plugin-utils': 7.22.5
-
-  '@babel/plugin-transform-new-target@7.23.3(@babel/core@7.24.0)':
-    dependencies:
-      '@babel/core': 7.24.0
-      '@babel/helper-plugin-utils': 7.22.5
-
-  '@babel/plugin-transform-nullish-coalescing-operator@7.23.4(@babel/core@7.24.0)':
-    dependencies:
-      '@babel/core': 7.24.0
-      '@babel/helper-plugin-utils': 7.22.5
-      '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.24.0)
-
-  '@babel/plugin-transform-numeric-separator@7.23.4(@babel/core@7.24.0)':
-    dependencies:
-      '@babel/core': 7.24.0
-      '@babel/helper-plugin-utils': 7.22.5
-      '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.24.0)
-
-  '@babel/plugin-transform-object-rest-spread@7.23.4(@babel/core@7.24.0)':
-    dependencies:
-      '@babel/compat-data': 7.23.5
-      '@babel/core': 7.24.0
-      '@babel/helper-compilation-targets': 7.23.6
-      '@babel/helper-plugin-utils': 7.22.5
-      '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.24.0)
-      '@babel/plugin-transform-parameters': 7.23.3(@babel/core@7.24.0)
-
-  '@babel/plugin-transform-object-super@7.23.3(@babel/core@7.24.0)':
-    dependencies:
-      '@babel/core': 7.24.0
-      '@babel/helper-plugin-utils': 7.22.5
-      '@babel/helper-replace-supers': 7.22.20(@babel/core@7.24.0)
-
-  '@babel/plugin-transform-optional-catch-binding@7.23.4(@babel/core@7.24.0)':
-    dependencies:
-      '@babel/core': 7.24.0
-      '@babel/helper-plugin-utils': 7.22.5
-      '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.24.0)
-
-  '@babel/plugin-transform-optional-chaining@7.23.4(@babel/core@7.24.0)':
-    dependencies:
-      '@babel/core': 7.24.0
-      '@babel/helper-plugin-utils': 7.22.5
-      '@babel/helper-skip-transparent-expression-wrappers': 7.22.5
-      '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.24.0)
-
-  '@babel/plugin-transform-parameters@7.23.3(@babel/core@7.24.0)':
-    dependencies:
-      '@babel/core': 7.24.0
-      '@babel/helper-plugin-utils': 7.22.5
-
-  '@babel/plugin-transform-private-methods@7.23.3(@babel/core@7.24.0)':
-    dependencies:
-      '@babel/core': 7.24.0
-      '@babel/helper-create-class-features-plugin': 7.23.5(@babel/core@7.24.0)
-      '@babel/helper-plugin-utils': 7.22.5
-
-  '@babel/plugin-transform-private-property-in-object@7.23.4(@babel/core@7.24.0)':
-    dependencies:
-      '@babel/core': 7.24.0
-      '@babel/helper-annotate-as-pure': 7.22.5
-      '@babel/helper-create-class-features-plugin': 7.23.5(@babel/core@7.24.0)
-      '@babel/helper-plugin-utils': 7.22.5
-      '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.24.0)
-
-  '@babel/plugin-transform-property-literals@7.23.3(@babel/core@7.24.0)':
-    dependencies:
-      '@babel/core': 7.24.0
-      '@babel/helper-plugin-utils': 7.22.5
-
-  '@babel/plugin-transform-regenerator@7.23.3(@babel/core@7.24.0)':
-    dependencies:
-      '@babel/core': 7.24.0
-      '@babel/helper-plugin-utils': 7.22.5
-      regenerator-transform: 0.15.2
-
-  '@babel/plugin-transform-reserved-words@7.23.3(@babel/core@7.24.0)':
-    dependencies:
-      '@babel/core': 7.24.0
-      '@babel/helper-plugin-utils': 7.22.5
-
-  '@babel/plugin-transform-shorthand-properties@7.23.3(@babel/core@7.24.0)':
-    dependencies:
-      '@babel/core': 7.24.0
-      '@babel/helper-plugin-utils': 7.22.5
-
-  '@babel/plugin-transform-spread@7.23.3(@babel/core@7.24.0)':
-    dependencies:
-      '@babel/core': 7.24.0
-      '@babel/helper-plugin-utils': 7.22.5
-      '@babel/helper-skip-transparent-expression-wrappers': 7.22.5
-
-  '@babel/plugin-transform-sticky-regex@7.23.3(@babel/core@7.24.0)':
-    dependencies:
-      '@babel/core': 7.24.0
-      '@babel/helper-plugin-utils': 7.22.5
-
-  '@babel/plugin-transform-template-literals@7.23.3(@babel/core@7.24.0)':
-    dependencies:
-      '@babel/core': 7.24.0
-      '@babel/helper-plugin-utils': 7.22.5
-
-  '@babel/plugin-transform-typeof-symbol@7.23.3(@babel/core@7.24.0)':
-    dependencies:
-      '@babel/core': 7.24.0
-      '@babel/helper-plugin-utils': 7.22.5
-
-  '@babel/plugin-transform-typescript@7.23.5(@babel/core@7.24.0)':
-    dependencies:
-      '@babel/core': 7.24.0
-      '@babel/helper-annotate-as-pure': 7.22.5
-      '@babel/helper-create-class-features-plugin': 7.23.5(@babel/core@7.24.0)
-      '@babel/helper-plugin-utils': 7.22.5
-      '@babel/plugin-syntax-typescript': 7.23.3(@babel/core@7.24.0)
-
-  '@babel/plugin-transform-unicode-escapes@7.23.3(@babel/core@7.24.0)':
-    dependencies:
-      '@babel/core': 7.24.0
-      '@babel/helper-plugin-utils': 7.22.5
-
-  '@babel/plugin-transform-unicode-property-regex@7.23.3(@babel/core@7.24.0)':
-    dependencies:
-      '@babel/core': 7.24.0
-      '@babel/helper-create-regexp-features-plugin': 7.22.15(@babel/core@7.24.0)
-      '@babel/helper-plugin-utils': 7.22.5
-
-  '@babel/plugin-transform-unicode-regex@7.23.3(@babel/core@7.24.0)':
-    dependencies:
-      '@babel/core': 7.24.0
-      '@babel/helper-create-regexp-features-plugin': 7.22.15(@babel/core@7.24.0)
-      '@babel/helper-plugin-utils': 7.22.5
-
-  '@babel/plugin-transform-unicode-sets-regex@7.23.3(@babel/core@7.24.0)':
-    dependencies:
-      '@babel/core': 7.24.0
-      '@babel/helper-create-regexp-features-plugin': 7.22.15(@babel/core@7.24.0)
-      '@babel/helper-plugin-utils': 7.22.5
-
-  '@babel/preset-env@7.23.5(@babel/core@7.24.0)':
-    dependencies:
-      '@babel/compat-data': 7.23.5
-      '@babel/core': 7.24.0
-      '@babel/helper-compilation-targets': 7.23.6
-      '@babel/helper-plugin-utils': 7.22.5
-      '@babel/helper-validator-option': 7.23.5
-      '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression': 7.23.3(@babel/core@7.24.0)
-      '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining': 7.23.3(@babel/core@7.24.0)
-      '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly': 7.23.3(@babel/core@7.24.0)
-      '@babel/plugin-proposal-private-property-in-object': 7.21.0-placeholder-for-preset-env.2(@babel/core@7.24.0)
-      '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.24.0)
-      '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.24.0)
-      '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.24.0)
-      '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.24.0)
-      '@babel/plugin-syntax-export-namespace-from': 7.8.3(@babel/core@7.24.0)
-      '@babel/plugin-syntax-import-assertions': 7.23.3(@babel/core@7.24.0)
-      '@babel/plugin-syntax-import-attributes': 7.23.3(@babel/core@7.24.0)
-      '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.24.0)
-      '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.24.0)
-      '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.24.0)
-      '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.24.0)
-      '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.24.0)
-      '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.24.0)
-      '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.24.0)
-      '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.24.0)
-      '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.24.0)
-      '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.24.0)
-      '@babel/plugin-syntax-unicode-sets-regex': 7.18.6(@babel/core@7.24.0)
-      '@babel/plugin-transform-arrow-functions': 7.23.3(@babel/core@7.24.0)
-      '@babel/plugin-transform-async-generator-functions': 7.23.4(@babel/core@7.24.0)
-      '@babel/plugin-transform-async-to-generator': 7.23.3(@babel/core@7.24.0)
-      '@babel/plugin-transform-block-scoped-functions': 7.23.3(@babel/core@7.24.0)
-      '@babel/plugin-transform-block-scoping': 7.23.4(@babel/core@7.24.0)
-      '@babel/plugin-transform-class-properties': 7.23.3(@babel/core@7.24.0)
-      '@babel/plugin-transform-class-static-block': 7.23.4(@babel/core@7.24.0)
-      '@babel/plugin-transform-classes': 7.23.5(@babel/core@7.24.0)
-      '@babel/plugin-transform-computed-properties': 7.23.3(@babel/core@7.24.0)
-      '@babel/plugin-transform-destructuring': 7.23.3(@babel/core@7.24.0)
-      '@babel/plugin-transform-dotall-regex': 7.23.3(@babel/core@7.24.0)
-      '@babel/plugin-transform-duplicate-keys': 7.23.3(@babel/core@7.24.0)
-      '@babel/plugin-transform-dynamic-import': 7.23.4(@babel/core@7.24.0)
-      '@babel/plugin-transform-exponentiation-operator': 7.23.3(@babel/core@7.24.0)
-      '@babel/plugin-transform-export-namespace-from': 7.23.4(@babel/core@7.24.0)
-      '@babel/plugin-transform-for-of': 7.23.3(@babel/core@7.24.0)
-      '@babel/plugin-transform-function-name': 7.23.3(@babel/core@7.24.0)
-      '@babel/plugin-transform-json-strings': 7.23.4(@babel/core@7.24.0)
-      '@babel/plugin-transform-literals': 7.23.3(@babel/core@7.24.0)
-      '@babel/plugin-transform-logical-assignment-operators': 7.23.4(@babel/core@7.24.0)
-      '@babel/plugin-transform-member-expression-literals': 7.23.3(@babel/core@7.24.0)
-      '@babel/plugin-transform-modules-amd': 7.23.3(@babel/core@7.24.0)
-      '@babel/plugin-transform-modules-commonjs': 7.23.3(@babel/core@7.24.0)
-      '@babel/plugin-transform-modules-systemjs': 7.23.3(@babel/core@7.24.0)
-      '@babel/plugin-transform-modules-umd': 7.23.3(@babel/core@7.24.0)
-      '@babel/plugin-transform-named-capturing-groups-regex': 7.22.5(@babel/core@7.24.0)
-      '@babel/plugin-transform-new-target': 7.23.3(@babel/core@7.24.0)
-      '@babel/plugin-transform-nullish-coalescing-operator': 7.23.4(@babel/core@7.24.0)
-      '@babel/plugin-transform-numeric-separator': 7.23.4(@babel/core@7.24.0)
-      '@babel/plugin-transform-object-rest-spread': 7.23.4(@babel/core@7.24.0)
-      '@babel/plugin-transform-object-super': 7.23.3(@babel/core@7.24.0)
-      '@babel/plugin-transform-optional-catch-binding': 7.23.4(@babel/core@7.24.0)
-      '@babel/plugin-transform-optional-chaining': 7.23.4(@babel/core@7.24.0)
-      '@babel/plugin-transform-parameters': 7.23.3(@babel/core@7.24.0)
-      '@babel/plugin-transform-private-methods': 7.23.3(@babel/core@7.24.0)
-      '@babel/plugin-transform-private-property-in-object': 7.23.4(@babel/core@7.24.0)
-      '@babel/plugin-transform-property-literals': 7.23.3(@babel/core@7.24.0)
-      '@babel/plugin-transform-regenerator': 7.23.3(@babel/core@7.24.0)
-      '@babel/plugin-transform-reserved-words': 7.23.3(@babel/core@7.24.0)
-      '@babel/plugin-transform-shorthand-properties': 7.23.3(@babel/core@7.24.0)
-      '@babel/plugin-transform-spread': 7.23.3(@babel/core@7.24.0)
-      '@babel/plugin-transform-sticky-regex': 7.23.3(@babel/core@7.24.0)
-      '@babel/plugin-transform-template-literals': 7.23.3(@babel/core@7.24.0)
-      '@babel/plugin-transform-typeof-symbol': 7.23.3(@babel/core@7.24.0)
-      '@babel/plugin-transform-unicode-escapes': 7.23.3(@babel/core@7.24.0)
-      '@babel/plugin-transform-unicode-property-regex': 7.23.3(@babel/core@7.24.0)
-      '@babel/plugin-transform-unicode-regex': 7.23.3(@babel/core@7.24.0)
-      '@babel/plugin-transform-unicode-sets-regex': 7.23.3(@babel/core@7.24.0)
-      '@babel/preset-modules': 0.1.6-no-external-plugins(@babel/core@7.24.0)
-      babel-plugin-polyfill-corejs2: 0.4.6(@babel/core@7.24.0)
-      babel-plugin-polyfill-corejs3: 0.8.6(@babel/core@7.24.0)
-      babel-plugin-polyfill-regenerator: 0.5.3(@babel/core@7.24.0)
-      core-js-compat: 3.33.3
-      semver: 6.3.1
-    transitivePeerDependencies:
-      - supports-color
-
-  '@babel/preset-flow@7.23.3(@babel/core@7.24.0)':
-    dependencies:
-      '@babel/core': 7.24.0
-      '@babel/helper-plugin-utils': 7.22.5
-      '@babel/helper-validator-option': 7.23.5
-      '@babel/plugin-transform-flow-strip-types': 7.23.3(@babel/core@7.24.0)
-
-  '@babel/preset-modules@0.1.6-no-external-plugins(@babel/core@7.24.0)':
-    dependencies:
-      '@babel/core': 7.24.0
-      '@babel/helper-plugin-utils': 7.22.5
-      '@babel/types': 7.24.0
-      esutils: 2.0.3
-
-  '@babel/preset-typescript@7.23.3(@babel/core@7.24.0)':
-    dependencies:
-      '@babel/core': 7.24.0
-      '@babel/helper-plugin-utils': 7.22.5
-      '@babel/helper-validator-option': 7.23.5
-      '@babel/plugin-syntax-jsx': 7.23.3(@babel/core@7.24.0)
-      '@babel/plugin-transform-modules-commonjs': 7.23.3(@babel/core@7.24.0)
-      '@babel/plugin-transform-typescript': 7.23.5(@babel/core@7.24.0)
-
-  '@babel/register@7.22.15(@babel/core@7.24.0)':
-    dependencies:
-      '@babel/core': 7.24.0
-      clone-deep: 4.0.1
-      find-cache-dir: 2.1.0
-      make-dir: 2.1.0
-      pirates: 4.0.5
-      source-map-support: 0.5.21
-
-  '@babel/regjsgen@0.8.0': {}
-
   '@babel/runtime@7.23.4':
     dependencies:
       regenerator-runtime: 0.14.0
 
   '@babel/template@7.22.15':
     dependencies:
-      '@babel/code-frame': 7.23.5
-      '@babel/parser': 7.23.9
-      '@babel/types': 7.23.5
+      '@babel/code-frame': 7.24.7
+      '@babel/parser': 7.25.6
+      '@babel/types': 7.24.7
 
   '@babel/template@7.24.0':
     dependencies:
-      '@babel/code-frame': 7.23.5
-      '@babel/parser': 7.24.0
-      '@babel/types': 7.24.0
+      '@babel/code-frame': 7.24.7
+      '@babel/parser': 7.24.7
+      '@babel/types': 7.24.7
+
+  '@babel/template@7.24.7':
+    dependencies:
+      '@babel/code-frame': 7.24.7
+      '@babel/parser': 7.25.6
+      '@babel/types': 7.24.7
 
   '@babel/traverse@7.23.5':
     dependencies:
-      '@babel/code-frame': 7.23.5
+      '@babel/code-frame': 7.24.7
       '@babel/generator': 7.23.5
       '@babel/helper-environment-visitor': 7.22.20
       '@babel/helper-function-name': 7.23.0
       '@babel/helper-hoist-variables': 7.22.5
       '@babel/helper-split-export-declaration': 7.22.6
-      '@babel/parser': 7.23.9
-      '@babel/types': 7.23.5
-      debug: 4.3.4(supports-color@8.1.1)
+      '@babel/parser': 7.25.6
+      '@babel/types': 7.24.7
+      debug: 4.3.7(supports-color@8.1.1)
       globals: 11.12.0
     transitivePeerDependencies:
       - supports-color
 
-  '@babel/traverse@7.24.0':
+  '@babel/traverse@7.24.7':
     dependencies:
-      '@babel/code-frame': 7.23.5
-      '@babel/generator': 7.23.6
-      '@babel/helper-environment-visitor': 7.22.20
-      '@babel/helper-function-name': 7.23.0
-      '@babel/helper-hoist-variables': 7.22.5
-      '@babel/helper-split-export-declaration': 7.22.6
-      '@babel/parser': 7.24.0
-      '@babel/types': 7.24.0
-      debug: 4.3.4(supports-color@8.1.1)
+      '@babel/code-frame': 7.24.7
+      '@babel/generator': 7.24.7
+      '@babel/helper-environment-visitor': 7.24.7
+      '@babel/helper-function-name': 7.24.7
+      '@babel/helper-hoist-variables': 7.24.7
+      '@babel/helper-split-export-declaration': 7.24.7
+      '@babel/parser': 7.25.6
+      '@babel/types': 7.24.7
+      debug: 4.3.7(supports-color@8.1.1)
       globals: 11.12.0
     transitivePeerDependencies:
       - supports-color
 
-  '@babel/types@7.23.5':
+  '@babel/types@7.24.7':
     dependencies:
-      '@babel/helper-string-parser': 7.23.4
-      '@babel/helper-validator-identifier': 7.22.20
+      '@babel/helper-string-parser': 7.24.7
+      '@babel/helper-validator-identifier': 7.24.7
       to-fast-properties: 2.0.0
 
-  '@babel/types@7.24.0':
+  '@babel/types@7.25.6':
     dependencies:
-      '@babel/helper-string-parser': 7.23.4
-      '@babel/helper-validator-identifier': 7.22.20
+      '@babel/helper-string-parser': 7.24.8
+      '@babel/helper-validator-identifier': 7.24.7
       to-fast-properties: 2.0.0
 
   '@base2/pretty-print-object@1.0.1': {}
 
   '@bcoe/v8-coverage@0.2.3': {}
 
-  '@bull-board/api@5.17.0(@bull-board/ui@5.17.0)':
+  '@bull-board/api@6.0.0(@bull-board/ui@6.0.0)':
     dependencies:
-      '@bull-board/ui': 5.17.0
+      '@bull-board/ui': 6.0.0
       redis-info: 3.1.0
 
-  '@bull-board/fastify@5.17.0':
+  '@bull-board/fastify@6.0.0':
     dependencies:
-      '@bull-board/api': 5.17.0(@bull-board/ui@5.17.0)
-      '@bull-board/ui': 5.17.0
-      '@fastify/static': 6.12.0
-      '@fastify/view': 8.2.0
-      ejs: 3.1.9
+      '@bull-board/api': 6.0.0(@bull-board/ui@6.0.0)
+      '@bull-board/ui': 6.0.0
+      '@fastify/static': 8.0.1
+      '@fastify/view': 10.0.1
+      ejs: 3.1.10
 
-  '@bull-board/ui@5.17.0':
+  '@bull-board/ui@6.0.0':
     dependencies:
-      '@bull-board/api': 5.17.0(@bull-board/ui@5.17.0)
+      '@bull-board/api': 6.0.0(@bull-board/ui@6.0.0)
 
   '@bundled-es-modules/cookie@2.0.0':
     dependencies:
@@ -12839,78 +12619,83 @@ snapshots:
     dependencies:
       statuses: 2.0.1
 
+  '@bundled-es-modules/tough-cookie@0.1.6':
+    dependencies:
+      '@types/tough-cookie': 4.0.5
+      tough-cookie: 4.1.4
+
   '@canvas/image-data@1.0.0': {}
 
   '@colors/colors@1.5.0':
     optional: true
 
-  '@cropper/element-canvas@2.0.0-beta.5':
+  '@cropper/element-canvas@2.0.0-rc.2':
     dependencies:
-      '@cropper/element': 2.0.0-beta.5
-      '@cropper/utils': 2.0.0-beta.5
+      '@cropper/element': 2.0.0-rc.2
+      '@cropper/utils': 2.0.0-rc.2
 
-  '@cropper/element-crosshair@2.0.0-beta.5':
+  '@cropper/element-crosshair@2.0.0-rc.2':
     dependencies:
-      '@cropper/element': 2.0.0-beta.5
-      '@cropper/utils': 2.0.0-beta.5
+      '@cropper/element': 2.0.0-rc.2
+      '@cropper/utils': 2.0.0-rc.2
 
-  '@cropper/element-grid@2.0.0-beta.5':
+  '@cropper/element-grid@2.0.0-rc.2':
     dependencies:
-      '@cropper/element': 2.0.0-beta.5
-      '@cropper/utils': 2.0.0-beta.5
+      '@cropper/element': 2.0.0-rc.2
+      '@cropper/utils': 2.0.0-rc.2
 
-  '@cropper/element-handle@2.0.0-beta.5':
+  '@cropper/element-handle@2.0.0-rc.2':
     dependencies:
-      '@cropper/element': 2.0.0-beta.5
-      '@cropper/utils': 2.0.0-beta.5
+      '@cropper/element': 2.0.0-rc.2
+      '@cropper/utils': 2.0.0-rc.2
 
-  '@cropper/element-image@2.0.0-beta.5':
+  '@cropper/element-image@2.0.0-rc.2':
     dependencies:
-      '@cropper/element': 2.0.0-beta.5
-      '@cropper/element-canvas': 2.0.0-beta.5
-      '@cropper/utils': 2.0.0-beta.5
+      '@cropper/element': 2.0.0-rc.2
+      '@cropper/element-canvas': 2.0.0-rc.2
+      '@cropper/utils': 2.0.0-rc.2
 
-  '@cropper/element-selection@2.0.0-beta.5':
+  '@cropper/element-selection@2.0.0-rc.2':
     dependencies:
-      '@cropper/element': 2.0.0-beta.5
-      '@cropper/element-canvas': 2.0.0-beta.5
-      '@cropper/element-image': 2.0.0-beta.5
-      '@cropper/utils': 2.0.0-beta.5
+      '@cropper/element': 2.0.0-rc.2
+      '@cropper/element-canvas': 2.0.0-rc.2
+      '@cropper/element-image': 2.0.0-rc.2
+      '@cropper/utils': 2.0.0-rc.2
 
-  '@cropper/element-shade@2.0.0-beta.5':
+  '@cropper/element-shade@2.0.0-rc.2':
     dependencies:
-      '@cropper/element': 2.0.0-beta.5
-      '@cropper/element-canvas': 2.0.0-beta.5
-      '@cropper/element-selection': 2.0.0-beta.5
-      '@cropper/utils': 2.0.0-beta.5
+      '@cropper/element': 2.0.0-rc.2
+      '@cropper/element-canvas': 2.0.0-rc.2
+      '@cropper/element-selection': 2.0.0-rc.2
+      '@cropper/utils': 2.0.0-rc.2
 
-  '@cropper/element-viewer@2.0.0-beta.5':
+  '@cropper/element-viewer@2.0.0-rc.2':
     dependencies:
-      '@cropper/element': 2.0.0-beta.5
-      '@cropper/element-canvas': 2.0.0-beta.5
-      '@cropper/element-image': 2.0.0-beta.5
-      '@cropper/element-selection': 2.0.0-beta.5
-      '@cropper/utils': 2.0.0-beta.5
+      '@cropper/element': 2.0.0-rc.2
+      '@cropper/element-canvas': 2.0.0-rc.2
+      '@cropper/element-image': 2.0.0-rc.2
+      '@cropper/element-selection': 2.0.0-rc.2
+      '@cropper/utils': 2.0.0-rc.2
 
-  '@cropper/element@2.0.0-beta.5':
+  '@cropper/element@2.0.0-rc.2':
     dependencies:
-      '@cropper/utils': 2.0.0-beta.5
+      '@cropper/utils': 2.0.0-rc.2
 
-  '@cropper/elements@2.0.0-beta.5':
+  '@cropper/elements@2.0.0-rc.2':
     dependencies:
-      '@cropper/element': 2.0.0-beta.5
-      '@cropper/element-canvas': 2.0.0-beta.5
-      '@cropper/element-crosshair': 2.0.0-beta.5
-      '@cropper/element-grid': 2.0.0-beta.5
-      '@cropper/element-handle': 2.0.0-beta.5
-      '@cropper/element-image': 2.0.0-beta.5
-      '@cropper/element-selection': 2.0.0-beta.5
-      '@cropper/element-shade': 2.0.0-beta.5
-      '@cropper/element-viewer': 2.0.0-beta.5
+      '@cropper/element': 2.0.0-rc.2
+      '@cropper/element-canvas': 2.0.0-rc.2
+      '@cropper/element-crosshair': 2.0.0-rc.2
+      '@cropper/element-grid': 2.0.0-rc.2
+      '@cropper/element-handle': 2.0.0-rc.2
+      '@cropper/element-image': 2.0.0-rc.2
+      '@cropper/element-selection': 2.0.0-rc.2
+      '@cropper/element-shade': 2.0.0-rc.2
+      '@cropper/element-viewer': 2.0.0-rc.2
 
-  '@cropper/utils@2.0.0-beta.5': {}
+  '@cropper/utils@2.0.0-rc.2': {}
 
-  '@cypress/request@3.0.0':
+  '@cypress/request@3.0.5':
     dependencies:
       aws-sign2: 0.7.0
       aws4: 1.12.0
@@ -12918,16 +12703,16 @@ snapshots:
       combined-stream: 1.0.8
       extend: 3.0.2
       forever-agent: 0.6.1
-      form-data: 2.3.3
-      http-signature: 1.3.6
+      form-data: 4.0.0
+      http-signature: 1.4.0
       is-typedarray: 1.0.0
       isstream: 0.1.2
       json-stringify-safe: 5.0.1
       mime-types: 2.1.35
       performance-now: 2.1.0
-      qs: 6.10.4
+      qs: 6.13.0
       safe-buffer: 5.2.1
-      tough-cookie: 4.1.3
+      tough-cookie: 4.1.4
       tunnel-agent: 0.6.0
       uuid: 8.3.2
 
@@ -12938,36 +12723,36 @@ snapshots:
     transitivePeerDependencies:
       - supports-color
 
-  '@digitalbazaar/http-client@3.4.1(web-streams-polyfill@3.2.1)':
+  '@digitalbazaar/http-client@3.4.1(web-streams-polyfill@4.0.0)':
     dependencies:
       ky: 0.33.3
-      ky-universal: 0.11.0(ky@0.33.3)(web-streams-polyfill@3.2.1)
+      ky-universal: 0.11.0(ky@0.33.3)(web-streams-polyfill@4.0.0)
       undici: 5.28.2
     transitivePeerDependencies:
       - web-streams-polyfill
 
-  '@discordapp/twemoji@15.0.3':
+  '@discordapp/twemoji@15.1.0':
     dependencies:
-      '@twemoji/parser': 15.0.0
+      '@twemoji/parser': 15.1.0
       fs-extra: 8.1.0
       jsonfile: 5.0.0
       universalify: 0.1.2
 
-  '@discoveryjs/json-ext@0.5.7': {}
-
-  '@emnapi/runtime@1.1.1':
+  '@emnapi/runtime@1.2.0':
     dependencies:
-      tslib: 2.6.2
+      tslib: 2.7.0
     optional: true
 
-  '@emotion/use-insertion-effect-with-fallbacks@1.0.1(react@18.3.1)':
-    dependencies:
-      react: 18.3.1
-
   '@esbuild/aix-ppc64@0.19.11':
     optional: true
 
-  '@esbuild/aix-ppc64@0.20.2':
+  '@esbuild/aix-ppc64@0.21.5':
+    optional: true
+
+  '@esbuild/aix-ppc64@0.23.0':
+    optional: true
+
+  '@esbuild/aix-ppc64@0.23.1':
     optional: true
 
   '@esbuild/android-arm64@0.18.20':
@@ -12976,7 +12761,13 @@ snapshots:
   '@esbuild/android-arm64@0.19.11':
     optional: true
 
-  '@esbuild/android-arm64@0.20.2':
+  '@esbuild/android-arm64@0.21.5':
+    optional: true
+
+  '@esbuild/android-arm64@0.23.0':
+    optional: true
+
+  '@esbuild/android-arm64@0.23.1':
     optional: true
 
   '@esbuild/android-arm@0.18.20':
@@ -12985,7 +12776,13 @@ snapshots:
   '@esbuild/android-arm@0.19.11':
     optional: true
 
-  '@esbuild/android-arm@0.20.2':
+  '@esbuild/android-arm@0.21.5':
+    optional: true
+
+  '@esbuild/android-arm@0.23.0':
+    optional: true
+
+  '@esbuild/android-arm@0.23.1':
     optional: true
 
   '@esbuild/android-x64@0.18.20':
@@ -12994,7 +12791,13 @@ snapshots:
   '@esbuild/android-x64@0.19.11':
     optional: true
 
-  '@esbuild/android-x64@0.20.2':
+  '@esbuild/android-x64@0.21.5':
+    optional: true
+
+  '@esbuild/android-x64@0.23.0':
+    optional: true
+
+  '@esbuild/android-x64@0.23.1':
     optional: true
 
   '@esbuild/darwin-arm64@0.18.20':
@@ -13003,7 +12806,13 @@ snapshots:
   '@esbuild/darwin-arm64@0.19.11':
     optional: true
 
-  '@esbuild/darwin-arm64@0.20.2':
+  '@esbuild/darwin-arm64@0.21.5':
+    optional: true
+
+  '@esbuild/darwin-arm64@0.23.0':
+    optional: true
+
+  '@esbuild/darwin-arm64@0.23.1':
     optional: true
 
   '@esbuild/darwin-x64@0.18.20':
@@ -13012,7 +12821,13 @@ snapshots:
   '@esbuild/darwin-x64@0.19.11':
     optional: true
 
-  '@esbuild/darwin-x64@0.20.2':
+  '@esbuild/darwin-x64@0.21.5':
+    optional: true
+
+  '@esbuild/darwin-x64@0.23.0':
+    optional: true
+
+  '@esbuild/darwin-x64@0.23.1':
     optional: true
 
   '@esbuild/freebsd-arm64@0.18.20':
@@ -13021,7 +12836,13 @@ snapshots:
   '@esbuild/freebsd-arm64@0.19.11':
     optional: true
 
-  '@esbuild/freebsd-arm64@0.20.2':
+  '@esbuild/freebsd-arm64@0.21.5':
+    optional: true
+
+  '@esbuild/freebsd-arm64@0.23.0':
+    optional: true
+
+  '@esbuild/freebsd-arm64@0.23.1':
     optional: true
 
   '@esbuild/freebsd-x64@0.18.20':
@@ -13030,7 +12851,13 @@ snapshots:
   '@esbuild/freebsd-x64@0.19.11':
     optional: true
 
-  '@esbuild/freebsd-x64@0.20.2':
+  '@esbuild/freebsd-x64@0.21.5':
+    optional: true
+
+  '@esbuild/freebsd-x64@0.23.0':
+    optional: true
+
+  '@esbuild/freebsd-x64@0.23.1':
     optional: true
 
   '@esbuild/linux-arm64@0.18.20':
@@ -13039,7 +12866,13 @@ snapshots:
   '@esbuild/linux-arm64@0.19.11':
     optional: true
 
-  '@esbuild/linux-arm64@0.20.2':
+  '@esbuild/linux-arm64@0.21.5':
+    optional: true
+
+  '@esbuild/linux-arm64@0.23.0':
+    optional: true
+
+  '@esbuild/linux-arm64@0.23.1':
     optional: true
 
   '@esbuild/linux-arm@0.18.20':
@@ -13048,7 +12881,13 @@ snapshots:
   '@esbuild/linux-arm@0.19.11':
     optional: true
 
-  '@esbuild/linux-arm@0.20.2':
+  '@esbuild/linux-arm@0.21.5':
+    optional: true
+
+  '@esbuild/linux-arm@0.23.0':
+    optional: true
+
+  '@esbuild/linux-arm@0.23.1':
     optional: true
 
   '@esbuild/linux-ia32@0.18.20':
@@ -13057,7 +12896,13 @@ snapshots:
   '@esbuild/linux-ia32@0.19.11':
     optional: true
 
-  '@esbuild/linux-ia32@0.20.2':
+  '@esbuild/linux-ia32@0.21.5':
+    optional: true
+
+  '@esbuild/linux-ia32@0.23.0':
+    optional: true
+
+  '@esbuild/linux-ia32@0.23.1':
     optional: true
 
   '@esbuild/linux-loong64@0.18.20':
@@ -13066,7 +12911,13 @@ snapshots:
   '@esbuild/linux-loong64@0.19.11':
     optional: true
 
-  '@esbuild/linux-loong64@0.20.2':
+  '@esbuild/linux-loong64@0.21.5':
+    optional: true
+
+  '@esbuild/linux-loong64@0.23.0':
+    optional: true
+
+  '@esbuild/linux-loong64@0.23.1':
     optional: true
 
   '@esbuild/linux-mips64el@0.18.20':
@@ -13075,7 +12926,13 @@ snapshots:
   '@esbuild/linux-mips64el@0.19.11':
     optional: true
 
-  '@esbuild/linux-mips64el@0.20.2':
+  '@esbuild/linux-mips64el@0.21.5':
+    optional: true
+
+  '@esbuild/linux-mips64el@0.23.0':
+    optional: true
+
+  '@esbuild/linux-mips64el@0.23.1':
     optional: true
 
   '@esbuild/linux-ppc64@0.18.20':
@@ -13084,7 +12941,13 @@ snapshots:
   '@esbuild/linux-ppc64@0.19.11':
     optional: true
 
-  '@esbuild/linux-ppc64@0.20.2':
+  '@esbuild/linux-ppc64@0.21.5':
+    optional: true
+
+  '@esbuild/linux-ppc64@0.23.0':
+    optional: true
+
+  '@esbuild/linux-ppc64@0.23.1':
     optional: true
 
   '@esbuild/linux-riscv64@0.18.20':
@@ -13093,7 +12956,13 @@ snapshots:
   '@esbuild/linux-riscv64@0.19.11':
     optional: true
 
-  '@esbuild/linux-riscv64@0.20.2':
+  '@esbuild/linux-riscv64@0.21.5':
+    optional: true
+
+  '@esbuild/linux-riscv64@0.23.0':
+    optional: true
+
+  '@esbuild/linux-riscv64@0.23.1':
     optional: true
 
   '@esbuild/linux-s390x@0.18.20':
@@ -13102,7 +12971,13 @@ snapshots:
   '@esbuild/linux-s390x@0.19.11':
     optional: true
 
-  '@esbuild/linux-s390x@0.20.2':
+  '@esbuild/linux-s390x@0.21.5':
+    optional: true
+
+  '@esbuild/linux-s390x@0.23.0':
+    optional: true
+
+  '@esbuild/linux-s390x@0.23.1':
     optional: true
 
   '@esbuild/linux-x64@0.18.20':
@@ -13111,7 +12986,13 @@ snapshots:
   '@esbuild/linux-x64@0.19.11':
     optional: true
 
-  '@esbuild/linux-x64@0.20.2':
+  '@esbuild/linux-x64@0.21.5':
+    optional: true
+
+  '@esbuild/linux-x64@0.23.0':
+    optional: true
+
+  '@esbuild/linux-x64@0.23.1':
     optional: true
 
   '@esbuild/netbsd-x64@0.18.20':
@@ -13120,7 +13001,19 @@ snapshots:
   '@esbuild/netbsd-x64@0.19.11':
     optional: true
 
-  '@esbuild/netbsd-x64@0.20.2':
+  '@esbuild/netbsd-x64@0.21.5':
+    optional: true
+
+  '@esbuild/netbsd-x64@0.23.0':
+    optional: true
+
+  '@esbuild/netbsd-x64@0.23.1':
+    optional: true
+
+  '@esbuild/openbsd-arm64@0.23.0':
+    optional: true
+
+  '@esbuild/openbsd-arm64@0.23.1':
     optional: true
 
   '@esbuild/openbsd-x64@0.18.20':
@@ -13129,7 +13022,13 @@ snapshots:
   '@esbuild/openbsd-x64@0.19.11':
     optional: true
 
-  '@esbuild/openbsd-x64@0.20.2':
+  '@esbuild/openbsd-x64@0.21.5':
+    optional: true
+
+  '@esbuild/openbsd-x64@0.23.0':
+    optional: true
+
+  '@esbuild/openbsd-x64@0.23.1':
     optional: true
 
   '@esbuild/sunos-x64@0.18.20':
@@ -13138,7 +13037,13 @@ snapshots:
   '@esbuild/sunos-x64@0.19.11':
     optional: true
 
-  '@esbuild/sunos-x64@0.20.2':
+  '@esbuild/sunos-x64@0.21.5':
+    optional: true
+
+  '@esbuild/sunos-x64@0.23.0':
+    optional: true
+
+  '@esbuild/sunos-x64@0.23.1':
     optional: true
 
   '@esbuild/win32-arm64@0.18.20':
@@ -13147,7 +13052,13 @@ snapshots:
   '@esbuild/win32-arm64@0.19.11':
     optional: true
 
-  '@esbuild/win32-arm64@0.20.2':
+  '@esbuild/win32-arm64@0.21.5':
+    optional: true
+
+  '@esbuild/win32-arm64@0.23.0':
+    optional: true
+
+  '@esbuild/win32-arm64@0.23.1':
     optional: true
 
   '@esbuild/win32-ia32@0.18.20':
@@ -13156,7 +13067,13 @@ snapshots:
   '@esbuild/win32-ia32@0.19.11':
     optional: true
 
-  '@esbuild/win32-ia32@0.20.2':
+  '@esbuild/win32-ia32@0.21.5':
+    optional: true
+
+  '@esbuild/win32-ia32@0.23.0':
+    optional: true
+
+  '@esbuild/win32-ia32@0.23.1':
     optional: true
 
   '@esbuild/win32-x64@0.18.20':
@@ -13165,30 +13082,54 @@ snapshots:
   '@esbuild/win32-x64@0.19.11':
     optional: true
 
-  '@esbuild/win32-x64@0.20.2':
+  '@esbuild/win32-x64@0.21.5':
     optional: true
 
-  '@eslint-community/eslint-utils@4.4.0(eslint@8.53.0)':
+  '@esbuild/win32-x64@0.23.0':
+    optional: true
+
+  '@esbuild/win32-x64@0.23.1':
+    optional: true
+
+  '@eslint-community/eslint-utils@4.4.0(eslint@9.11.0)':
     dependencies:
-      eslint: 8.53.0
+      eslint: 9.11.0
       eslint-visitor-keys: 3.4.3
 
-  '@eslint-community/eslint-utils@4.4.0(eslint@8.57.0)':
+  '@eslint-community/eslint-utils@4.4.0(eslint@9.8.0)':
     dependencies:
-      eslint: 8.57.0
+      eslint: 9.8.0
       eslint-visitor-keys: 3.4.3
 
-  '@eslint-community/regexpp@4.10.0': {}
+  '@eslint-community/regexpp@4.11.0': {}
 
   '@eslint-community/regexpp@4.6.2': {}
 
-  '@eslint/eslintrc@2.1.4':
+  '@eslint/compat@1.1.1': {}
+
+  '@eslint/config-array@0.17.1':
+    dependencies:
+      '@eslint/object-schema': 2.1.4
+      debug: 4.3.7(supports-color@8.1.1)
+      minimatch: 3.1.2
+    transitivePeerDependencies:
+      - supports-color
+
+  '@eslint/config-array@0.18.0':
+    dependencies:
+      '@eslint/object-schema': 2.1.4
+      debug: 4.3.7(supports-color@8.1.1)
+      minimatch: 3.1.2
+    transitivePeerDependencies:
+      - supports-color
+
+  '@eslint/eslintrc@3.1.0':
     dependencies:
       ajv: 6.12.6
-      debug: 4.3.4(supports-color@8.1.1)
-      espree: 9.6.1
-      globals: 13.19.0
-      ignore: 5.2.4
+      debug: 4.3.7(supports-color@8.1.1)
+      espree: 10.1.0
+      globals: 14.0.0
+      ignore: 5.3.1
       import-fresh: 3.3.0
       js-yaml: 4.1.0
       minimatch: 3.1.2
@@ -13196,128 +13137,121 @@ snapshots:
     transitivePeerDependencies:
       - supports-color
 
-  '@eslint/js@8.53.0': {}
+  '@eslint/js@9.11.0': {}
 
-  '@eslint/js@8.57.0': {}
+  '@eslint/js@9.8.0': {}
 
-  '@fal-works/esbuild-plugin-global-externals@2.1.2': {}
+  '@eslint/object-schema@2.1.4': {}
 
-  '@fastify/accept-negotiator@1.0.0': {}
+  '@eslint/plugin-kit@0.2.0':
+    dependencies:
+      levn: 0.4.1
 
-  '@fastify/accepts@4.3.0':
+  '@fastify/accept-negotiator@2.0.0': {}
+
+  '@fastify/accepts@5.0.1':
     dependencies:
       accepts: 1.3.8
-      fastify-plugin: 4.5.0
+      fastify-plugin: 5.0.0
 
-  '@fastify/ajv-compiler@3.5.0':
+  '@fastify/ajv-compiler@4.0.0':
     dependencies:
-      ajv: 8.13.0
-      ajv-formats: 2.1.1(ajv@8.13.0)
-      fast-uri: 2.2.0
+      ajv: 8.17.1
+      ajv-formats: 3.0.1(ajv@8.17.1)
+      fast-uri: 3.0.1
 
   '@fastify/busboy@2.1.0': {}
 
-  '@fastify/cookie@9.3.1':
+  '@fastify/busboy@3.0.0': {}
+
+  '@fastify/cookie@10.0.1':
     dependencies:
       cookie-signature: 1.2.1
-      fastify-plugin: 4.5.0
+      fastify-plugin: 5.0.0
 
-  '@fastify/cors@9.0.1':
+  '@fastify/cors@10.0.1':
     dependencies:
-      fastify-plugin: 4.5.0
-      mnemonist: 0.39.6
+      fastify-plugin: 5.0.0
+      mnemonist: 0.39.8
 
-  '@fastify/deepmerge@1.3.0': {}
+  '@fastify/deepmerge@2.0.0': {}
 
-  '@fastify/error@3.4.0': {}
+  '@fastify/error@4.0.0': {}
 
-  '@fastify/express@3.0.0':
+  '@fastify/express@4.0.1':
     dependencies:
-      express: 4.19.2
-      fastify-plugin: 4.5.0
+      express: 4.21.0
+      fastify-plugin: 5.0.0
     transitivePeerDependencies:
       - supports-color
 
-  '@fastify/fast-json-stringify-compiler@4.3.0':
+  '@fastify/fast-json-stringify-compiler@5.0.0':
     dependencies:
-      fast-json-stringify: 5.8.0
+      fast-json-stringify: 6.0.0
 
-  '@fastify/http-proxy@9.5.0(bufferutil@4.0.7)(utf-8-validate@6.0.3)':
+  '@fastify/http-proxy@10.0.0(bufferutil@4.0.7)(utf-8-validate@6.0.3)':
     dependencies:
-      '@fastify/reply-from': 9.0.1
+      '@fastify/reply-from': 11.0.0
       fast-querystring: 1.1.2
-      fastify-plugin: 4.5.0
-      ws: 8.17.0(bufferutil@4.0.7)(utf-8-validate@6.0.3)
+      fastify-plugin: 5.0.0
+      ws: 8.18.0(bufferutil@4.0.7)(utf-8-validate@6.0.3)
     transitivePeerDependencies:
       - bufferutil
       - utf-8-validate
 
-  '@fastify/multipart@8.2.0':
+  '@fastify/merge-json-schemas@0.1.1':
     dependencies:
-      '@fastify/busboy': 2.1.0
-      '@fastify/deepmerge': 1.3.0
-      '@fastify/error': 3.4.0
-      fastify-plugin: 4.5.0
-      secure-json-parse: 2.7.0
-      stream-wormhole: 1.1.0
+      fast-deep-equal: 3.1.3
 
-  '@fastify/reply-from@9.0.1':
+  '@fastify/multipart@9.0.1':
     dependencies:
-      '@fastify/error': 3.4.0
+      '@fastify/busboy': 3.0.0
+      '@fastify/deepmerge': 2.0.0
+      '@fastify/error': 4.0.0
+      fastify-plugin: 5.0.0
+      secure-json-parse: 3.0.0
+
+  '@fastify/reply-from@11.0.0':
+    dependencies:
+      '@fastify/error': 4.0.0
       end-of-stream: 1.4.4
+      fast-content-type-parse: 2.0.0
       fast-querystring: 1.1.2
-      fastify-plugin: 4.5.0
-      pump: 3.0.0
-      tiny-lru: 10.0.1
-      undici: 5.28.2
+      fastify-plugin: 4.5.1
+      toad-cache: 3.7.0
+      undici: 6.19.8
 
-  '@fastify/send@2.0.1':
+  '@fastify/send@3.1.1':
     dependencies:
-      '@lukeed/ms': 2.0.1
+      '@lukeed/ms': 2.0.2
       escape-html: 1.0.3
       fast-decode-uri-component: 1.0.1
       http-errors: 2.0.0
       mime: 3.0.0
 
-  '@fastify/static@6.12.0':
+  '@fastify/static@8.0.1':
     dependencies:
-      '@fastify/accept-negotiator': 1.0.0
-      '@fastify/send': 2.0.1
+      '@fastify/accept-negotiator': 2.0.0
+      '@fastify/send': 3.1.1
       content-disposition: 0.5.4
-      fastify-plugin: 4.5.0
-      glob: 8.1.0
-      p-limit: 3.1.0
-
-  '@fastify/static@7.0.3':
-    dependencies:
-      '@fastify/accept-negotiator': 1.0.0
-      '@fastify/send': 2.0.1
-      content-disposition: 0.5.4
-      fastify-plugin: 4.5.0
+      fastify-plugin: 5.0.0
       fastq: 1.17.1
-      glob: 10.3.12
+      glob: 11.0.0
 
-  '@fastify/view@8.2.0':
+  '@fastify/view@10.0.1':
     dependencies:
-      fastify-plugin: 4.5.0
-      hashlru: 2.3.0
-
-  '@fastify/view@9.1.0':
-    dependencies:
-      fastify-plugin: 4.5.0
+      fastify-plugin: 5.0.0
       toad-cache: 3.7.0
 
   '@github/webauthn-json@2.1.1': {}
 
   '@hapi/boom@10.0.1':
     dependencies:
-      '@hapi/hoek': 11.0.2
+      '@hapi/hoek': 11.0.4
 
   '@hapi/bourne@3.0.0': {}
 
-  '@hapi/hoek@10.0.1': {}
-
-  '@hapi/hoek@11.0.2': {}
+  '@hapi/hoek@11.0.4': {}
 
   '@hapi/hoek@9.3.0': {}
 
@@ -13329,107 +13263,89 @@ snapshots:
     dependencies:
       '@hapi/boom': 10.0.1
       '@hapi/bourne': 3.0.0
-      '@hapi/hoek': 11.0.2
+      '@hapi/hoek': 11.0.4
 
   '@hexagon/base64@1.1.27': {}
 
-  '@humanwhocodes/config-array@0.11.13':
-    dependencies:
-      '@humanwhocodes/object-schema': 2.0.1
-      debug: 4.3.4(supports-color@8.1.1)
-      minimatch: 3.1.2
-    transitivePeerDependencies:
-      - supports-color
-
-  '@humanwhocodes/config-array@0.11.14':
-    dependencies:
-      '@humanwhocodes/object-schema': 2.0.2
-      debug: 4.3.4(supports-color@8.1.1)
-      minimatch: 3.1.2
-    transitivePeerDependencies:
-      - supports-color
-
   '@humanwhocodes/module-importer@1.0.1': {}
 
   '@humanwhocodes/momoa@2.0.4': {}
 
-  '@humanwhocodes/object-schema@2.0.1': {}
+  '@humanwhocodes/retry@0.3.0': {}
 
-  '@humanwhocodes/object-schema@2.0.2': {}
-
-  '@img/sharp-darwin-arm64@0.33.3':
+  '@img/sharp-darwin-arm64@0.33.5':
     optionalDependencies:
-      '@img/sharp-libvips-darwin-arm64': 1.0.2
+      '@img/sharp-libvips-darwin-arm64': 1.0.4
     optional: true
 
-  '@img/sharp-darwin-x64@0.33.3':
+  '@img/sharp-darwin-x64@0.33.5':
     optionalDependencies:
-      '@img/sharp-libvips-darwin-x64': 1.0.2
+      '@img/sharp-libvips-darwin-x64': 1.0.4
     optional: true
 
-  '@img/sharp-libvips-darwin-arm64@1.0.2':
+  '@img/sharp-libvips-darwin-arm64@1.0.4':
     optional: true
 
-  '@img/sharp-libvips-darwin-x64@1.0.2':
+  '@img/sharp-libvips-darwin-x64@1.0.4':
     optional: true
 
-  '@img/sharp-libvips-linux-arm64@1.0.2':
+  '@img/sharp-libvips-linux-arm64@1.0.4':
     optional: true
 
-  '@img/sharp-libvips-linux-arm@1.0.2':
+  '@img/sharp-libvips-linux-arm@1.0.5':
     optional: true
 
-  '@img/sharp-libvips-linux-s390x@1.0.2':
+  '@img/sharp-libvips-linux-s390x@1.0.4':
     optional: true
 
-  '@img/sharp-libvips-linux-x64@1.0.2':
+  '@img/sharp-libvips-linux-x64@1.0.4':
     optional: true
 
-  '@img/sharp-libvips-linuxmusl-arm64@1.0.2':
+  '@img/sharp-libvips-linuxmusl-arm64@1.0.4':
     optional: true
 
-  '@img/sharp-libvips-linuxmusl-x64@1.0.2':
+  '@img/sharp-libvips-linuxmusl-x64@1.0.4':
     optional: true
 
-  '@img/sharp-linux-arm64@0.33.3':
+  '@img/sharp-linux-arm64@0.33.5':
     optionalDependencies:
-      '@img/sharp-libvips-linux-arm64': 1.0.2
+      '@img/sharp-libvips-linux-arm64': 1.0.4
     optional: true
 
-  '@img/sharp-linux-arm@0.33.3':
+  '@img/sharp-linux-arm@0.33.5':
     optionalDependencies:
-      '@img/sharp-libvips-linux-arm': 1.0.2
+      '@img/sharp-libvips-linux-arm': 1.0.5
     optional: true
 
-  '@img/sharp-linux-s390x@0.33.3':
+  '@img/sharp-linux-s390x@0.33.5':
     optionalDependencies:
-      '@img/sharp-libvips-linux-s390x': 1.0.2
+      '@img/sharp-libvips-linux-s390x': 1.0.4
     optional: true
 
-  '@img/sharp-linux-x64@0.33.3':
+  '@img/sharp-linux-x64@0.33.5':
     optionalDependencies:
-      '@img/sharp-libvips-linux-x64': 1.0.2
+      '@img/sharp-libvips-linux-x64': 1.0.4
     optional: true
 
-  '@img/sharp-linuxmusl-arm64@0.33.3':
+  '@img/sharp-linuxmusl-arm64@0.33.5':
     optionalDependencies:
-      '@img/sharp-libvips-linuxmusl-arm64': 1.0.2
+      '@img/sharp-libvips-linuxmusl-arm64': 1.0.4
     optional: true
 
-  '@img/sharp-linuxmusl-x64@0.33.3':
+  '@img/sharp-linuxmusl-x64@0.33.5':
     optionalDependencies:
-      '@img/sharp-libvips-linuxmusl-x64': 1.0.2
+      '@img/sharp-libvips-linuxmusl-x64': 1.0.4
     optional: true
 
-  '@img/sharp-wasm32@0.33.3':
+  '@img/sharp-wasm32@0.33.5':
     dependencies:
-      '@emnapi/runtime': 1.1.1
+      '@emnapi/runtime': 1.2.0
     optional: true
 
-  '@img/sharp-win32-ia32@0.33.3':
+  '@img/sharp-win32-ia32@0.33.5':
     optional: true
 
-  '@img/sharp-win32-x64@0.33.3':
+  '@img/sharp-win32-x64@0.33.5':
     optional: true
 
   '@inquirer/confirm@3.1.6':
@@ -13442,7 +13358,7 @@ snapshots:
       '@inquirer/figures': 1.0.1
       '@inquirer/type': 1.3.1
       '@types/mute-stream': 0.0.4
-      '@types/node': 20.12.7
+      '@types/node': 20.14.12
       '@types/wrap-ansi': 3.0.0
       ansi-escapes: 4.3.2
       chalk: 4.1.2
@@ -13457,18 +13373,6 @@ snapshots:
 
   '@inquirer/type@1.3.1': {}
 
-  '@intlify/core-base@9.13.1':
-    dependencies:
-      '@intlify/message-compiler': 9.13.1
-      '@intlify/shared': 9.13.1
-
-  '@intlify/message-compiler@9.13.1':
-    dependencies:
-      '@intlify/shared': 9.13.1
-      source-map-js: 1.0.2
-
-  '@intlify/shared@9.13.1': {}
-
   '@ioredis/commands@1.2.0': {}
 
   '@isaacs/cliui@8.0.2':
@@ -13493,7 +13397,7 @@ snapshots:
   '@jest/console@29.7.0':
     dependencies:
       '@jest/types': 29.6.3
-      '@types/node': 20.12.7
+      '@types/node': 20.14.12
       chalk: 4.1.2
       jest-message-util: 29.7.0
       jest-util: 29.7.0
@@ -13506,14 +13410,14 @@ snapshots:
       '@jest/test-result': 29.7.0
       '@jest/transform': 29.7.0
       '@jest/types': 29.6.3
-      '@types/node': 20.12.7
+      '@types/node': 20.14.12
       ansi-escapes: 4.3.2
       chalk: 4.1.2
       ci-info: 3.7.1
       exit: 0.1.2
       graceful-fs: 4.2.11
       jest-changed-files: 29.7.0
-      jest-config: 29.7.0(@types/node@20.12.7)
+      jest-config: 29.7.0(@types/node@20.14.12)
       jest-haste-map: 29.7.0
       jest-message-util: 29.7.0
       jest-regex-util: 29.6.3
@@ -13525,7 +13429,7 @@ snapshots:
       jest-util: 29.7.0
       jest-validate: 29.7.0
       jest-watcher: 29.7.0
-      micromatch: 4.0.5
+      micromatch: 4.0.8
       pretty-format: 29.7.0
       slash: 3.0.0
       strip-ansi: 6.0.1
@@ -13542,7 +13446,7 @@ snapshots:
     dependencies:
       '@jest/fake-timers': 29.7.0
       '@jest/types': 29.6.3
-      '@types/node': 20.12.7
+      '@types/node': 20.14.12
       jest-mock: 29.7.0
 
   '@jest/expect-utils@29.7.0':
@@ -13560,7 +13464,7 @@ snapshots:
     dependencies:
       '@jest/types': 29.6.3
       '@sinonjs/fake-timers': 10.3.0
-      '@types/node': 20.12.7
+      '@types/node': 20.14.12
       jest-message-util: 29.7.0
       jest-mock: 29.7.0
       jest-util: 29.7.0
@@ -13581,8 +13485,8 @@ snapshots:
       '@jest/test-result': 29.7.0
       '@jest/transform': 29.7.0
       '@jest/types': 29.6.3
-      '@jridgewell/trace-mapping': 0.3.18
-      '@types/node': 20.12.7
+      '@jridgewell/trace-mapping': 0.3.25
+      '@types/node': 20.14.12
       chalk: 4.1.2
       collect-v8-coverage: 1.0.1
       exit: 0.1.2
@@ -13609,7 +13513,7 @@ snapshots:
 
   '@jest/source-map@29.6.3':
     dependencies:
-      '@jridgewell/trace-mapping': 0.3.18
+      '@jridgewell/trace-mapping': 0.3.25
       callsites: 3.1.0
       graceful-fs: 4.2.11
 
@@ -13629,9 +13533,9 @@ snapshots:
 
   '@jest/transform@29.7.0':
     dependencies:
-      '@babel/core': 7.23.5
+      '@babel/core': 7.24.7
       '@jest/types': 29.6.3
-      '@jridgewell/trace-mapping': 0.3.18
+      '@jridgewell/trace-mapping': 0.3.25
       babel-plugin-istanbul: 6.1.1
       chalk: 4.1.2
       convert-source-map: 2.0.0
@@ -13640,7 +13544,7 @@ snapshots:
       jest-haste-map: 29.7.0
       jest-regex-util: 29.6.3
       jest-util: 29.7.0
-      micromatch: 4.0.5
+      micromatch: 4.0.8
       pirates: 4.0.5
       slash: 3.0.0
       write-file-atomic: 4.0.2
@@ -13652,19 +13556,19 @@ snapshots:
       '@jest/schemas': 29.6.3
       '@types/istanbul-lib-coverage': 2.0.4
       '@types/istanbul-reports': 3.0.1
-      '@types/node': 20.12.7
+      '@types/node': 20.14.12
       '@types/yargs': 17.0.19
       chalk: 4.1.2
 
-  '@joshwooding/vite-plugin-react-docgen-typescript@0.3.0(typescript@5.4.5)(vite@5.2.11(@types/node@20.12.7)(sass@1.76.0)(terser@5.30.3))':
+  '@joshwooding/vite-plugin-react-docgen-typescript@0.3.0(typescript@5.6.2)(vite@5.4.8(@types/node@20.14.12)(sass@1.79.3)(terser@5.33.0))':
     dependencies:
       glob: 7.2.3
       glob-promise: 4.2.2(glob@7.2.3)
       magic-string: 0.27.0
-      react-docgen-typescript: 2.2.2(typescript@5.4.5)
-      vite: 5.2.11(@types/node@20.12.7)(sass@1.76.0)(terser@5.30.3)
+      react-docgen-typescript: 2.2.2(typescript@5.6.2)
+      vite: 5.4.8(@types/node@20.14.12)(sass@1.79.3)(terser@5.33.0)
     optionalDependencies:
-      typescript: 5.4.5
+      typescript: 5.6.2
 
   '@jridgewell/gen-mapping@0.3.2':
     dependencies:
@@ -13672,24 +13576,39 @@ snapshots:
       '@jridgewell/sourcemap-codec': 1.4.15
       '@jridgewell/trace-mapping': 0.3.18
 
+  '@jridgewell/gen-mapping@0.3.5':
+    dependencies:
+      '@jridgewell/set-array': 1.2.1
+      '@jridgewell/sourcemap-codec': 1.4.15
+      '@jridgewell/trace-mapping': 0.3.25
+
   '@jridgewell/resolve-uri@3.1.0': {}
 
   '@jridgewell/set-array@1.1.2': {}
 
-  '@jridgewell/source-map@0.3.5':
+  '@jridgewell/set-array@1.2.1': {}
+
+  '@jridgewell/source-map@0.3.6':
     dependencies:
-      '@jridgewell/gen-mapping': 0.3.2
-      '@jridgewell/trace-mapping': 0.3.18
+      '@jridgewell/gen-mapping': 0.3.5
+      '@jridgewell/trace-mapping': 0.3.25
 
   '@jridgewell/sourcemap-codec@1.4.14': {}
 
   '@jridgewell/sourcemap-codec@1.4.15': {}
 
+  '@jridgewell/sourcemap-codec@1.5.0': {}
+
   '@jridgewell/trace-mapping@0.3.18':
     dependencies:
       '@jridgewell/resolve-uri': 3.1.0
       '@jridgewell/sourcemap-codec': 1.4.14
 
+  '@jridgewell/trace-mapping@0.3.25':
+    dependencies:
+      '@jridgewell/resolve-uri': 3.1.0
+      '@jridgewell/sourcemap-codec': 1.4.15
+
   '@jsdevtools/ono@7.1.3': {}
 
   '@kurkle/color@0.3.2': {}
@@ -13698,18 +13617,18 @@ snapshots:
 
   '@lukeed/csprng@1.0.1': {}
 
-  '@lukeed/ms@2.0.1': {}
+  '@lukeed/ms@2.0.2': {}
 
   '@mapbox/node-pre-gyp@1.0.9(encoding@0.1.13)':
     dependencies:
-      detect-libc: 2.0.2
+      detect-libc: 2.0.3
       https-proxy-agent: 5.0.1
       make-dir: 3.1.0
       node-fetch: 2.7.0(encoding@0.1.13)
       nopt: 5.0.0
       npmlog: 5.0.1
       rimraf: 3.0.2
-      semver: 7.5.4
+      semver: 7.6.0
       tar: 6.2.1
     transitivePeerDependencies:
       - encoding
@@ -13728,23 +13647,23 @@ snapshots:
       '@types/react': 18.0.28
       react: 18.3.1
 
-  '@microsoft/api-extractor-model@7.28.14(@types/node@20.12.7)':
+  '@microsoft/api-extractor-model@7.29.8(@types/node@20.14.12)':
     dependencies:
-      '@microsoft/tsdoc': 0.14.2
-      '@microsoft/tsdoc-config': 0.16.2
-      '@rushstack/node-core-library': 4.1.0(@types/node@20.12.7)
+      '@microsoft/tsdoc': 0.15.0
+      '@microsoft/tsdoc-config': 0.17.0
+      '@rushstack/node-core-library': 5.9.0(@types/node@20.14.12)
     transitivePeerDependencies:
       - '@types/node'
 
-  '@microsoft/api-extractor@7.43.1(@types/node@20.12.7)':
+  '@microsoft/api-extractor@7.47.9(@types/node@20.14.12)':
     dependencies:
-      '@microsoft/api-extractor-model': 7.28.14(@types/node@20.12.7)
-      '@microsoft/tsdoc': 0.14.2
-      '@microsoft/tsdoc-config': 0.16.2
-      '@rushstack/node-core-library': 4.1.0(@types/node@20.12.7)
-      '@rushstack/rig-package': 0.5.2
-      '@rushstack/terminal': 0.10.1(@types/node@20.12.7)
-      '@rushstack/ts-command-line': 4.19.2(@types/node@20.12.7)
+      '@microsoft/api-extractor-model': 7.29.8(@types/node@20.14.12)
+      '@microsoft/tsdoc': 0.15.0
+      '@microsoft/tsdoc-config': 0.17.0
+      '@rushstack/node-core-library': 5.9.0(@types/node@20.14.12)
+      '@rushstack/rig-package': 0.5.3
+      '@rushstack/terminal': 0.14.2(@types/node@20.14.12)
+      '@rushstack/ts-command-line': 4.22.8(@types/node@20.14.12)
       lodash: 4.17.21
       minimatch: 3.0.8
       resolve: 1.22.8
@@ -13754,43 +13673,31 @@ snapshots:
     transitivePeerDependencies:
       - '@types/node'
 
-  '@microsoft/tsdoc-config@0.16.2':
+  '@microsoft/tsdoc-config@0.17.0':
     dependencies:
-      '@microsoft/tsdoc': 0.14.2
-      ajv: 6.12.6
+      '@microsoft/tsdoc': 0.15.0
+      ajv: 8.12.0
       jju: 1.4.0
-      resolve: 1.19.0
+      resolve: 1.22.8
 
-  '@microsoft/tsdoc@0.14.2': {}
+  '@microsoft/tsdoc@0.15.0': {}
 
   '@misskey-dev/browser-image-resizer@2024.1.0': {}
 
-  '@misskey-dev/eslint-plugin@1.0.0(@typescript-eslint/eslint-plugin@6.11.0(@typescript-eslint/parser@6.11.0(eslint@8.53.0)(typescript@5.3.3))(eslint@8.53.0)(typescript@5.3.3))(@typescript-eslint/parser@6.11.0(eslint@8.53.0)(typescript@5.3.3))(eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.11.0(eslint@8.53.0)(typescript@5.3.3))(eslint@8.53.0))(eslint@8.53.0)':
+  '@misskey-dev/eslint-plugin@2.0.3(@eslint/compat@1.1.1)(@typescript-eslint/eslint-plugin@7.17.0(@typescript-eslint/parser@7.17.0(eslint@9.8.0)(typescript@5.6.2))(eslint@9.8.0)(typescript@5.6.2))(@typescript-eslint/parser@7.17.0(eslint@9.8.0)(typescript@5.6.2))(eslint-plugin-import@2.31.0(@typescript-eslint/parser@7.17.0(eslint@9.8.0)(typescript@5.6.2))(eslint@9.8.0))(eslint@9.8.0)(globals@15.9.0)':
     dependencies:
-      '@typescript-eslint/eslint-plugin': 6.11.0(@typescript-eslint/parser@6.11.0(eslint@8.53.0)(typescript@5.3.3))(eslint@8.53.0)(typescript@5.3.3)
-      '@typescript-eslint/parser': 6.11.0(eslint@8.53.0)(typescript@5.3.3)
-      eslint: 8.53.0
-      eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.11.0(eslint@8.53.0)(typescript@5.3.3))(eslint@8.53.0)
-
-  '@misskey-dev/eslint-plugin@1.0.0(@typescript-eslint/eslint-plugin@7.1.0(@typescript-eslint/parser@7.1.0(eslint@8.57.0)(typescript@5.3.3))(eslint@8.57.0)(typescript@5.3.3))(@typescript-eslint/parser@7.1.0(eslint@8.57.0)(typescript@5.3.3))(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.1.0(eslint@8.57.0)(typescript@5.3.3))(eslint@8.57.0))(eslint@8.57.0)':
-    dependencies:
-      '@typescript-eslint/eslint-plugin': 7.1.0(@typescript-eslint/parser@7.1.0(eslint@8.57.0)(typescript@5.3.3))(eslint@8.57.0)(typescript@5.3.3)
-      '@typescript-eslint/parser': 7.1.0(eslint@8.57.0)(typescript@5.3.3)
-      eslint: 8.57.0
-      eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.1.0(eslint@8.57.0)(typescript@5.3.3))(eslint@8.57.0)
-
-  '@misskey-dev/eslint-plugin@1.0.0(@typescript-eslint/eslint-plugin@7.7.1(@typescript-eslint/parser@7.7.1(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0)(typescript@5.4.5))(@typescript-eslint/parser@7.7.1(eslint@8.57.0)(typescript@5.4.5))(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.7.1(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0))(eslint@8.57.0)':
-    dependencies:
-      '@typescript-eslint/eslint-plugin': 7.7.1(@typescript-eslint/parser@7.7.1(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0)(typescript@5.4.5)
-      '@typescript-eslint/parser': 7.7.1(eslint@8.57.0)(typescript@5.4.5)
-      eslint: 8.57.0
-      eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.7.1(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0)
+      '@eslint/compat': 1.1.1
+      '@typescript-eslint/eslint-plugin': 7.17.0(@typescript-eslint/parser@7.17.0(eslint@9.8.0)(typescript@5.6.2))(eslint@9.8.0)(typescript@5.6.2)
+      '@typescript-eslint/parser': 7.17.0(eslint@9.8.0)(typescript@5.6.2)
+      eslint: 9.8.0
+      eslint-plugin-import: 2.31.0(@typescript-eslint/parser@7.17.0(eslint@9.8.0)(typescript@5.6.2))(eslint@9.8.0)
+      globals: 15.9.0
 
   '@misskey-dev/sharp-read-bmp@1.2.0':
     dependencies:
       decode-bmp: 0.2.1
       decode-ico: 0.4.1
-      sharp: 0.33.3
+      sharp: 0.33.5
 
   '@misskey-dev/summaly@5.1.0':
     dependencies:
@@ -13832,9 +13739,7 @@ snapshots:
   '@msgpackr-extract/msgpackr-extract-win32-x64@3.0.2':
     optional: true
 
-  '@mswjs/cookies@1.1.0': {}
-
-  '@mswjs/interceptors@0.26.15':
+  '@mswjs/interceptors@0.29.1':
     dependencies:
       '@open-draft/deferred-promise': 2.2.0
       '@open-draft/logger': 0.3.0
@@ -13843,94 +13748,99 @@ snapshots:
       outvariant: 1.4.2
       strict-event-emitter: 0.5.1
 
-  '@napi-rs/canvas-android-arm64@0.1.52':
-    optional: true
-
-  '@napi-rs/canvas-darwin-arm64@0.1.52':
-    optional: true
-
-  '@napi-rs/canvas-darwin-x64@0.1.52':
-    optional: true
-
-  '@napi-rs/canvas-linux-arm-gnueabihf@0.1.52':
-    optional: true
-
-  '@napi-rs/canvas-linux-arm64-gnu@0.1.52':
-    optional: true
-
-  '@napi-rs/canvas-linux-arm64-musl@0.1.52':
-    optional: true
-
-  '@napi-rs/canvas-linux-x64-gnu@0.1.52':
-    optional: true
-
-  '@napi-rs/canvas-linux-x64-musl@0.1.52':
-    optional: true
-
-  '@napi-rs/canvas-win32-x64-msvc@0.1.52':
-    optional: true
-
-  '@napi-rs/canvas@0.1.52':
-    optionalDependencies:
-      '@napi-rs/canvas-android-arm64': 0.1.52
-      '@napi-rs/canvas-darwin-arm64': 0.1.52
-      '@napi-rs/canvas-darwin-x64': 0.1.52
-      '@napi-rs/canvas-linux-arm-gnueabihf': 0.1.52
-      '@napi-rs/canvas-linux-arm64-gnu': 0.1.52
-      '@napi-rs/canvas-linux-arm64-musl': 0.1.52
-      '@napi-rs/canvas-linux-x64-gnu': 0.1.52
-      '@napi-rs/canvas-linux-x64-musl': 0.1.52
-      '@napi-rs/canvas-win32-x64-msvc': 0.1.52
-
-  '@ndelangen/get-tarball@3.0.7':
+  '@mswjs/interceptors@0.35.8':
     dependencies:
-      gunzip-maybe: 1.4.2
-      pump: 3.0.0
-      tar-fs: 2.1.1
+      '@open-draft/deferred-promise': 2.2.0
+      '@open-draft/logger': 0.3.0
+      '@open-draft/until': 2.1.0
+      is-node-process: 1.2.0
+      outvariant: 1.4.3
+      strict-event-emitter: 0.5.1
 
-  '@nestjs/common@10.3.8(reflect-metadata@0.2.2)(rxjs@7.8.1)':
+  '@napi-rs/canvas-android-arm64@0.1.56':
+    optional: true
+
+  '@napi-rs/canvas-darwin-arm64@0.1.56':
+    optional: true
+
+  '@napi-rs/canvas-darwin-x64@0.1.56':
+    optional: true
+
+  '@napi-rs/canvas-linux-arm-gnueabihf@0.1.56':
+    optional: true
+
+  '@napi-rs/canvas-linux-arm64-gnu@0.1.56':
+    optional: true
+
+  '@napi-rs/canvas-linux-arm64-musl@0.1.56':
+    optional: true
+
+  '@napi-rs/canvas-linux-x64-gnu@0.1.56':
+    optional: true
+
+  '@napi-rs/canvas-linux-x64-musl@0.1.56':
+    optional: true
+
+  '@napi-rs/canvas-win32-x64-msvc@0.1.56':
+    optional: true
+
+  '@napi-rs/canvas@0.1.56':
+    optionalDependencies:
+      '@napi-rs/canvas-android-arm64': 0.1.56
+      '@napi-rs/canvas-darwin-arm64': 0.1.56
+      '@napi-rs/canvas-darwin-x64': 0.1.56
+      '@napi-rs/canvas-linux-arm-gnueabihf': 0.1.56
+      '@napi-rs/canvas-linux-arm64-gnu': 0.1.56
+      '@napi-rs/canvas-linux-arm64-musl': 0.1.56
+      '@napi-rs/canvas-linux-x64-gnu': 0.1.56
+      '@napi-rs/canvas-linux-x64-musl': 0.1.56
+      '@napi-rs/canvas-win32-x64-msvc': 0.1.56
+
+  '@nestjs/common@10.4.4(reflect-metadata@0.2.2)(rxjs@7.8.1)':
     dependencies:
       iterare: 1.2.1
       reflect-metadata: 0.2.2
       rxjs: 7.8.1
-      tslib: 2.6.2
+      tslib: 2.7.0
       uid: 2.0.2
 
-  '@nestjs/core@10.3.8(@nestjs/common@10.3.8(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.3.8)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.1)':
+  '@nestjs/core@10.4.4(@nestjs/common@10.4.4(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.4)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.1)':
     dependencies:
-      '@nestjs/common': 10.3.8(reflect-metadata@0.2.2)(rxjs@7.8.1)
+      '@nestjs/common': 10.4.4(reflect-metadata@0.2.2)(rxjs@7.8.1)
       '@nuxtjs/opencollective': 0.3.2(encoding@0.1.13)
       fast-safe-stringify: 2.1.1
       iterare: 1.2.1
-      path-to-regexp: 3.2.0
+      path-to-regexp: 3.3.0
       reflect-metadata: 0.2.2
       rxjs: 7.8.1
-      tslib: 2.6.2
+      tslib: 2.7.0
       uid: 2.0.2
     optionalDependencies:
-      '@nestjs/platform-express': 10.3.8(@nestjs/common@10.3.8(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.3.8)
+      '@nestjs/platform-express': 10.4.4(@nestjs/common@10.4.4(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.4)
     transitivePeerDependencies:
       - encoding
 
-  '@nestjs/platform-express@10.3.8(@nestjs/common@10.3.8(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.3.8)':
+  '@nestjs/platform-express@10.4.4(@nestjs/common@10.4.4(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.4)':
     dependencies:
-      '@nestjs/common': 10.3.8(reflect-metadata@0.2.2)(rxjs@7.8.1)
-      '@nestjs/core': 10.3.8(@nestjs/common@10.3.8(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.3.8)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.1)
-      body-parser: 1.20.2
+      '@nestjs/common': 10.4.4(reflect-metadata@0.2.2)(rxjs@7.8.1)
+      '@nestjs/core': 10.4.4(@nestjs/common@10.4.4(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.4)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.1)
+      body-parser: 1.20.3
       cors: 2.8.5
-      express: 4.19.2
+      express: 4.21.0
       multer: 1.4.4-lts.1
-      tslib: 2.6.2
+      tslib: 2.7.0
     transitivePeerDependencies:
       - supports-color
 
-  '@nestjs/testing@10.3.8(@nestjs/common@10.3.8(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.3.8(@nestjs/common@10.3.8(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.3.8)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.3.8(@nestjs/common@10.3.8(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.3.8))':
+  '@nestjs/testing@10.4.4(@nestjs/common@10.4.4(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.4(@nestjs/common@10.4.4(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.4)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.4(@nestjs/common@10.4.4(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.4))':
     dependencies:
-      '@nestjs/common': 10.3.8(reflect-metadata@0.2.2)(rxjs@7.8.1)
-      '@nestjs/core': 10.3.8(@nestjs/common@10.3.8(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.3.8)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.1)
-      tslib: 2.6.2
+      '@nestjs/common': 10.4.4(reflect-metadata@0.2.2)(rxjs@7.8.1)
+      '@nestjs/core': 10.4.4(@nestjs/common@10.4.4(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.4)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.1)
+      tslib: 2.7.0
     optionalDependencies:
-      '@nestjs/platform-express': 10.3.8(@nestjs/common@10.3.8(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.3.8)
+      '@nestjs/platform-express': 10.4.4(@nestjs/common@10.4.4(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.4)
+
+  '@noble/hashes@1.5.0': {}
 
   '@nodelib/fs.scandir@2.1.5':
     dependencies:
@@ -13942,21 +13852,21 @@ snapshots:
   '@nodelib/fs.walk@1.2.8':
     dependencies:
       '@nodelib/fs.scandir': 2.1.5
-      fastq: 1.15.0
+      fastq: 1.17.1
 
   '@npmcli/agent@2.2.0':
     dependencies:
       agent-base: 7.1.0
-      http-proxy-agent: 7.0.0
-      https-proxy-agent: 7.0.2
-      lru-cache: 10.0.2
+      http-proxy-agent: 7.0.2
+      https-proxy-agent: 7.0.5
+      lru-cache: 10.2.2
       socks-proxy-agent: 8.0.2
     transitivePeerDependencies:
       - supports-color
 
   '@npmcli/fs@3.1.0':
     dependencies:
-      semver: 7.6.0
+      semver: 7.6.3
 
   '@nsfw-filter/gif-frames@1.0.2':
     dependencies:
@@ -13991,228 +13901,248 @@ snapshots:
 
   '@open-draft/until@2.1.0': {}
 
-  '@opentelemetry/api-logs@0.51.1':
+  '@opentelemetry/api-logs@0.52.1':
     dependencies:
-      '@opentelemetry/api': 1.8.0
+      '@opentelemetry/api': 1.9.0
 
-  '@opentelemetry/api@1.8.0': {}
+  '@opentelemetry/api@1.9.0': {}
 
-  '@opentelemetry/context-async-hooks@1.24.1(@opentelemetry/api@1.8.0)':
+  '@opentelemetry/context-async-hooks@1.25.1(@opentelemetry/api@1.9.0)':
     dependencies:
-      '@opentelemetry/api': 1.8.0
+      '@opentelemetry/api': 1.9.0
 
-  '@opentelemetry/core@1.24.1(@opentelemetry/api@1.8.0)':
+  '@opentelemetry/core@1.24.1(@opentelemetry/api@1.9.0)':
     dependencies:
-      '@opentelemetry/api': 1.8.0
+      '@opentelemetry/api': 1.9.0
       '@opentelemetry/semantic-conventions': 1.24.1
 
-  '@opentelemetry/instrumentation-connect@0.36.0(@opentelemetry/api@1.8.0)':
+  '@opentelemetry/core@1.25.1(@opentelemetry/api@1.9.0)':
     dependencies:
-      '@opentelemetry/api': 1.8.0
-      '@opentelemetry/core': 1.24.1(@opentelemetry/api@1.8.0)
-      '@opentelemetry/instrumentation': 0.51.1(@opentelemetry/api@1.8.0)
-      '@opentelemetry/semantic-conventions': 1.24.1
+      '@opentelemetry/api': 1.9.0
+      '@opentelemetry/semantic-conventions': 1.25.1
+
+  '@opentelemetry/instrumentation-connect@0.38.0(@opentelemetry/api@1.9.0)':
+    dependencies:
+      '@opentelemetry/api': 1.9.0
+      '@opentelemetry/core': 1.25.1(@opentelemetry/api@1.9.0)
+      '@opentelemetry/instrumentation': 0.52.1(@opentelemetry/api@1.9.0)
+      '@opentelemetry/semantic-conventions': 1.25.1
       '@types/connect': 3.4.36
     transitivePeerDependencies:
       - supports-color
 
-  '@opentelemetry/instrumentation-express@0.39.0(@opentelemetry/api@1.8.0)':
+  '@opentelemetry/instrumentation-express@0.41.0(@opentelemetry/api@1.9.0)':
     dependencies:
-      '@opentelemetry/api': 1.8.0
-      '@opentelemetry/core': 1.24.1(@opentelemetry/api@1.8.0)
-      '@opentelemetry/instrumentation': 0.51.1(@opentelemetry/api@1.8.0)
-      '@opentelemetry/semantic-conventions': 1.24.1
+      '@opentelemetry/api': 1.9.0
+      '@opentelemetry/core': 1.25.1(@opentelemetry/api@1.9.0)
+      '@opentelemetry/instrumentation': 0.52.1(@opentelemetry/api@1.9.0)
+      '@opentelemetry/semantic-conventions': 1.25.1
     transitivePeerDependencies:
       - supports-color
 
-  '@opentelemetry/instrumentation-fastify@0.36.1(@opentelemetry/api@1.8.0)':
+  '@opentelemetry/instrumentation-fastify@0.38.0(@opentelemetry/api@1.9.0)':
     dependencies:
-      '@opentelemetry/api': 1.8.0
-      '@opentelemetry/core': 1.24.1(@opentelemetry/api@1.8.0)
-      '@opentelemetry/instrumentation': 0.51.1(@opentelemetry/api@1.8.0)
-      '@opentelemetry/semantic-conventions': 1.24.1
+      '@opentelemetry/api': 1.9.0
+      '@opentelemetry/core': 1.25.1(@opentelemetry/api@1.9.0)
+      '@opentelemetry/instrumentation': 0.52.1(@opentelemetry/api@1.9.0)
+      '@opentelemetry/semantic-conventions': 1.25.1
     transitivePeerDependencies:
       - supports-color
 
-  '@opentelemetry/instrumentation-graphql@0.40.0(@opentelemetry/api@1.8.0)':
+  '@opentelemetry/instrumentation-graphql@0.42.0(@opentelemetry/api@1.9.0)':
     dependencies:
-      '@opentelemetry/api': 1.8.0
-      '@opentelemetry/instrumentation': 0.51.1(@opentelemetry/api@1.8.0)
+      '@opentelemetry/api': 1.9.0
+      '@opentelemetry/instrumentation': 0.52.1(@opentelemetry/api@1.9.0)
     transitivePeerDependencies:
       - supports-color
 
-  '@opentelemetry/instrumentation-hapi@0.38.0(@opentelemetry/api@1.8.0)':
+  '@opentelemetry/instrumentation-hapi@0.40.0(@opentelemetry/api@1.9.0)':
     dependencies:
-      '@opentelemetry/api': 1.8.0
-      '@opentelemetry/core': 1.24.1(@opentelemetry/api@1.8.0)
-      '@opentelemetry/instrumentation': 0.51.1(@opentelemetry/api@1.8.0)
-      '@opentelemetry/semantic-conventions': 1.24.1
+      '@opentelemetry/api': 1.9.0
+      '@opentelemetry/core': 1.25.1(@opentelemetry/api@1.9.0)
+      '@opentelemetry/instrumentation': 0.52.1(@opentelemetry/api@1.9.0)
+      '@opentelemetry/semantic-conventions': 1.25.1
     transitivePeerDependencies:
       - supports-color
 
-  '@opentelemetry/instrumentation-http@0.51.1(@opentelemetry/api@1.8.0)':
+  '@opentelemetry/instrumentation-http@0.52.1(@opentelemetry/api@1.9.0)':
     dependencies:
-      '@opentelemetry/api': 1.8.0
-      '@opentelemetry/core': 1.24.1(@opentelemetry/api@1.8.0)
-      '@opentelemetry/instrumentation': 0.51.1(@opentelemetry/api@1.8.0)
-      '@opentelemetry/semantic-conventions': 1.24.1
-      semver: 7.6.0
+      '@opentelemetry/api': 1.9.0
+      '@opentelemetry/core': 1.25.1(@opentelemetry/api@1.9.0)
+      '@opentelemetry/instrumentation': 0.52.1(@opentelemetry/api@1.9.0)
+      '@opentelemetry/semantic-conventions': 1.25.1
+      semver: 7.6.3
     transitivePeerDependencies:
       - supports-color
 
-  '@opentelemetry/instrumentation-ioredis@0.40.0(@opentelemetry/api@1.8.0)':
+  '@opentelemetry/instrumentation-ioredis@0.42.0(@opentelemetry/api@1.9.0)':
     dependencies:
-      '@opentelemetry/api': 1.8.0
-      '@opentelemetry/instrumentation': 0.51.1(@opentelemetry/api@1.8.0)
+      '@opentelemetry/api': 1.9.0
+      '@opentelemetry/instrumentation': 0.52.1(@opentelemetry/api@1.9.0)
       '@opentelemetry/redis-common': 0.36.2
-      '@opentelemetry/semantic-conventions': 1.24.1
+      '@opentelemetry/semantic-conventions': 1.25.1
     transitivePeerDependencies:
       - supports-color
 
-  '@opentelemetry/instrumentation-koa@0.40.0(@opentelemetry/api@1.8.0)':
+  '@opentelemetry/instrumentation-koa@0.42.0(@opentelemetry/api@1.9.0)':
     dependencies:
-      '@opentelemetry/api': 1.8.0
-      '@opentelemetry/core': 1.24.1(@opentelemetry/api@1.8.0)
-      '@opentelemetry/instrumentation': 0.51.1(@opentelemetry/api@1.8.0)
-      '@opentelemetry/semantic-conventions': 1.24.1
-      '@types/koa': 2.14.0
-      '@types/koa__router': 12.0.3
+      '@opentelemetry/api': 1.9.0
+      '@opentelemetry/core': 1.25.1(@opentelemetry/api@1.9.0)
+      '@opentelemetry/instrumentation': 0.52.1(@opentelemetry/api@1.9.0)
+      '@opentelemetry/semantic-conventions': 1.25.1
     transitivePeerDependencies:
       - supports-color
 
-  '@opentelemetry/instrumentation-mongodb@0.43.0(@opentelemetry/api@1.8.0)':
+  '@opentelemetry/instrumentation-mongodb@0.46.0(@opentelemetry/api@1.9.0)':
     dependencies:
-      '@opentelemetry/api': 1.8.0
-      '@opentelemetry/instrumentation': 0.51.1(@opentelemetry/api@1.8.0)
-      '@opentelemetry/sdk-metrics': 1.24.1(@opentelemetry/api@1.8.0)
-      '@opentelemetry/semantic-conventions': 1.24.1
+      '@opentelemetry/api': 1.9.0
+      '@opentelemetry/instrumentation': 0.52.1(@opentelemetry/api@1.9.0)
+      '@opentelemetry/sdk-metrics': 1.24.1(@opentelemetry/api@1.9.0)
+      '@opentelemetry/semantic-conventions': 1.25.1
     transitivePeerDependencies:
       - supports-color
 
-  '@opentelemetry/instrumentation-mongoose@0.38.1(@opentelemetry/api@1.8.0)':
+  '@opentelemetry/instrumentation-mongoose@0.40.0(@opentelemetry/api@1.9.0)':
     dependencies:
-      '@opentelemetry/api': 1.8.0
-      '@opentelemetry/core': 1.24.1(@opentelemetry/api@1.8.0)
-      '@opentelemetry/instrumentation': 0.51.1(@opentelemetry/api@1.8.0)
-      '@opentelemetry/semantic-conventions': 1.24.1
+      '@opentelemetry/api': 1.9.0
+      '@opentelemetry/core': 1.25.1(@opentelemetry/api@1.9.0)
+      '@opentelemetry/instrumentation': 0.52.1(@opentelemetry/api@1.9.0)
+      '@opentelemetry/semantic-conventions': 1.25.1
     transitivePeerDependencies:
       - supports-color
 
-  '@opentelemetry/instrumentation-mysql2@0.38.1(@opentelemetry/api@1.8.0)':
+  '@opentelemetry/instrumentation-mysql2@0.40.0(@opentelemetry/api@1.9.0)':
     dependencies:
-      '@opentelemetry/api': 1.8.0
-      '@opentelemetry/instrumentation': 0.51.1(@opentelemetry/api@1.8.0)
-      '@opentelemetry/semantic-conventions': 1.24.1
-      '@opentelemetry/sql-common': 0.40.1(@opentelemetry/api@1.8.0)
+      '@opentelemetry/api': 1.9.0
+      '@opentelemetry/instrumentation': 0.52.1(@opentelemetry/api@1.9.0)
+      '@opentelemetry/semantic-conventions': 1.25.1
+      '@opentelemetry/sql-common': 0.40.1(@opentelemetry/api@1.9.0)
     transitivePeerDependencies:
       - supports-color
 
-  '@opentelemetry/instrumentation-mysql@0.38.1(@opentelemetry/api@1.8.0)':
+  '@opentelemetry/instrumentation-mysql@0.40.0(@opentelemetry/api@1.9.0)':
     dependencies:
-      '@opentelemetry/api': 1.8.0
-      '@opentelemetry/instrumentation': 0.51.1(@opentelemetry/api@1.8.0)
-      '@opentelemetry/semantic-conventions': 1.24.1
+      '@opentelemetry/api': 1.9.0
+      '@opentelemetry/instrumentation': 0.52.1(@opentelemetry/api@1.9.0)
+      '@opentelemetry/semantic-conventions': 1.25.1
       '@types/mysql': 2.15.22
     transitivePeerDependencies:
       - supports-color
 
-  '@opentelemetry/instrumentation-nestjs-core@0.37.1(@opentelemetry/api@1.8.0)':
+  '@opentelemetry/instrumentation-nestjs-core@0.39.0(@opentelemetry/api@1.9.0)':
     dependencies:
-      '@opentelemetry/api': 1.8.0
-      '@opentelemetry/instrumentation': 0.51.1(@opentelemetry/api@1.8.0)
-      '@opentelemetry/semantic-conventions': 1.24.1
+      '@opentelemetry/api': 1.9.0
+      '@opentelemetry/instrumentation': 0.52.1(@opentelemetry/api@1.9.0)
+      '@opentelemetry/semantic-conventions': 1.25.1
     transitivePeerDependencies:
       - supports-color
 
-  '@opentelemetry/instrumentation-pg@0.41.0(@opentelemetry/api@1.8.0)':
+  '@opentelemetry/instrumentation-pg@0.43.0(@opentelemetry/api@1.9.0)':
     dependencies:
-      '@opentelemetry/api': 1.8.0
-      '@opentelemetry/instrumentation': 0.51.1(@opentelemetry/api@1.8.0)
-      '@opentelemetry/semantic-conventions': 1.24.1
-      '@opentelemetry/sql-common': 0.40.1(@opentelemetry/api@1.8.0)
+      '@opentelemetry/api': 1.9.0
+      '@opentelemetry/instrumentation': 0.52.1(@opentelemetry/api@1.9.0)
+      '@opentelemetry/semantic-conventions': 1.25.1
+      '@opentelemetry/sql-common': 0.40.1(@opentelemetry/api@1.9.0)
       '@types/pg': 8.6.1
       '@types/pg-pool': 2.0.4
     transitivePeerDependencies:
       - supports-color
 
-  '@opentelemetry/instrumentation@0.43.0(@opentelemetry/api@1.8.0)':
+  '@opentelemetry/instrumentation-redis-4@0.41.0(@opentelemetry/api@1.9.0)':
     dependencies:
-      '@opentelemetry/api': 1.8.0
+      '@opentelemetry/api': 1.9.0
+      '@opentelemetry/instrumentation': 0.52.1(@opentelemetry/api@1.9.0)
+      '@opentelemetry/redis-common': 0.36.2
+      '@opentelemetry/semantic-conventions': 1.25.1
+    transitivePeerDependencies:
+      - supports-color
+
+  '@opentelemetry/instrumentation@0.46.0(@opentelemetry/api@1.9.0)':
+    dependencies:
+      '@opentelemetry/api': 1.9.0
       '@types/shimmer': 1.0.5
-      import-in-the-middle: 1.4.2
+      import-in-the-middle: 1.7.1
       require-in-the-middle: 7.3.0
-      semver: 7.6.0
+      semver: 7.6.3
       shimmer: 1.2.1
     transitivePeerDependencies:
       - supports-color
     optional: true
 
-  '@opentelemetry/instrumentation@0.51.1(@opentelemetry/api@1.8.0)':
+  '@opentelemetry/instrumentation@0.52.1(@opentelemetry/api@1.9.0)':
     dependencies:
-      '@opentelemetry/api': 1.8.0
-      '@opentelemetry/api-logs': 0.51.1
+      '@opentelemetry/api': 1.9.0
+      '@opentelemetry/api-logs': 0.52.1
       '@types/shimmer': 1.0.5
-      import-in-the-middle: 1.7.4
+      import-in-the-middle: 1.10.0
       require-in-the-middle: 7.3.0
-      semver: 7.6.0
+      semver: 7.6.3
       shimmer: 1.2.1
     transitivePeerDependencies:
       - supports-color
 
   '@opentelemetry/redis-common@0.36.2': {}
 
-  '@opentelemetry/resources@1.24.1(@opentelemetry/api@1.8.0)':
+  '@opentelemetry/resources@1.24.1(@opentelemetry/api@1.9.0)':
     dependencies:
-      '@opentelemetry/api': 1.8.0
-      '@opentelemetry/core': 1.24.1(@opentelemetry/api@1.8.0)
+      '@opentelemetry/api': 1.9.0
+      '@opentelemetry/core': 1.24.1(@opentelemetry/api@1.9.0)
       '@opentelemetry/semantic-conventions': 1.24.1
 
-  '@opentelemetry/sdk-metrics@1.24.1(@opentelemetry/api@1.8.0)':
+  '@opentelemetry/resources@1.25.1(@opentelemetry/api@1.9.0)':
     dependencies:
-      '@opentelemetry/api': 1.8.0
-      '@opentelemetry/core': 1.24.1(@opentelemetry/api@1.8.0)
-      '@opentelemetry/resources': 1.24.1(@opentelemetry/api@1.8.0)
+      '@opentelemetry/api': 1.9.0
+      '@opentelemetry/core': 1.25.1(@opentelemetry/api@1.9.0)
+      '@opentelemetry/semantic-conventions': 1.25.1
+
+  '@opentelemetry/sdk-metrics@1.24.1(@opentelemetry/api@1.9.0)':
+    dependencies:
+      '@opentelemetry/api': 1.9.0
+      '@opentelemetry/core': 1.24.1(@opentelemetry/api@1.9.0)
+      '@opentelemetry/resources': 1.24.1(@opentelemetry/api@1.9.0)
       lodash.merge: 4.6.2
 
-  '@opentelemetry/sdk-trace-base@1.24.1(@opentelemetry/api@1.8.0)':
+  '@opentelemetry/sdk-trace-base@1.25.1(@opentelemetry/api@1.9.0)':
     dependencies:
-      '@opentelemetry/api': 1.8.0
-      '@opentelemetry/core': 1.24.1(@opentelemetry/api@1.8.0)
-      '@opentelemetry/resources': 1.24.1(@opentelemetry/api@1.8.0)
-      '@opentelemetry/semantic-conventions': 1.24.1
+      '@opentelemetry/api': 1.9.0
+      '@opentelemetry/core': 1.25.1(@opentelemetry/api@1.9.0)
+      '@opentelemetry/resources': 1.25.1(@opentelemetry/api@1.9.0)
+      '@opentelemetry/semantic-conventions': 1.25.1
 
   '@opentelemetry/semantic-conventions@1.24.1': {}
 
-  '@opentelemetry/sql-common@0.40.1(@opentelemetry/api@1.8.0)':
+  '@opentelemetry/semantic-conventions@1.25.1': {}
+
+  '@opentelemetry/sql-common@0.40.1(@opentelemetry/api@1.9.0)':
     dependencies:
-      '@opentelemetry/api': 1.8.0
-      '@opentelemetry/core': 1.24.1(@opentelemetry/api@1.8.0)
+      '@opentelemetry/api': 1.9.0
+      '@opentelemetry/core': 1.25.1(@opentelemetry/api@1.9.0)
 
   '@peculiar/asn1-android@2.3.10':
     dependencies:
       '@peculiar/asn1-schema': 2.3.8
       asn1js: 3.0.5
-      tslib: 2.6.2
+      tslib: 2.7.0
 
   '@peculiar/asn1-ecc@2.3.8':
     dependencies:
       '@peculiar/asn1-schema': 2.3.8
       '@peculiar/asn1-x509': 2.3.8
       asn1js: 3.0.5
-      tslib: 2.6.2
+      tslib: 2.7.0
 
   '@peculiar/asn1-rsa@2.3.8':
     dependencies:
       '@peculiar/asn1-schema': 2.3.8
       '@peculiar/asn1-x509': 2.3.8
       asn1js: 3.0.5
-      tslib: 2.6.2
+      tslib: 2.7.0
 
   '@peculiar/asn1-schema@2.3.8':
     dependencies:
       asn1js: 3.0.5
       pvtsutils: 1.3.5
-      tslib: 2.6.2
+      tslib: 2.7.0
 
   '@peculiar/asn1-x509@2.3.8':
     dependencies:
@@ -14220,7 +14150,7 @@ snapshots:
       asn1js: 3.0.5
       ipaddr.js: 2.2.0
       pvtsutils: 1.3.5
-      tslib: 2.6.2
+      tslib: 2.7.0
 
   '@peertube/http-signature@1.7.0':
     dependencies:
@@ -14231,35 +14161,20 @@ snapshots:
   '@pkgjs/parseargs@0.11.0':
     optional: true
 
-  '@prisma/instrumentation@5.14.0':
+  '@prisma/instrumentation@5.17.0':
     dependencies:
-      '@opentelemetry/api': 1.8.0
-      '@opentelemetry/instrumentation': 0.51.1(@opentelemetry/api@1.8.0)
-      '@opentelemetry/sdk-trace-base': 1.24.1(@opentelemetry/api@1.8.0)
+      '@opentelemetry/api': 1.9.0
+      '@opentelemetry/instrumentation': 0.52.1(@opentelemetry/api@1.9.0)
+      '@opentelemetry/sdk-trace-base': 1.25.1(@opentelemetry/api@1.9.0)
     transitivePeerDependencies:
       - supports-color
 
-  '@radix-ui/react-compose-refs@1.0.1(@types/react@18.0.28)(react@18.3.1)':
+  '@readme/better-ajv-errors@1.6.0(ajv@8.17.1)':
     dependencies:
-      '@babel/runtime': 7.23.4
-      react: 18.3.1
-    optionalDependencies:
-      '@types/react': 18.0.28
-
-  '@radix-ui/react-slot@1.0.2(@types/react@18.0.28)(react@18.3.1)':
-    dependencies:
-      '@babel/runtime': 7.23.4
-      '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.0.28)(react@18.3.1)
-      react: 18.3.1
-    optionalDependencies:
-      '@types/react': 18.0.28
-
-  '@readme/better-ajv-errors@1.6.0(ajv@8.13.0)':
-    dependencies:
-      '@babel/code-frame': 7.23.5
+      '@babel/code-frame': 7.24.7
       '@babel/runtime': 7.23.4
       '@humanwhocodes/momoa': 2.0.4
-      ajv: 8.13.0
+      ajv: 8.17.1
       chalk: 4.1.2
       json-to-ast: 2.1.0
       jsonpointer: 5.0.1
@@ -14268,200 +14183,239 @@ snapshots:
   '@readme/json-schema-ref-parser@1.2.0':
     dependencies:
       '@jsdevtools/ono': 7.1.3
-      '@types/json-schema': 7.0.12
+      '@types/json-schema': 7.0.15
       call-me-maybe: 1.0.2
       js-yaml: 4.1.0
 
-  '@readme/openapi-parser@2.5.0(openapi-types@12.1.3)':
+  '@readme/openapi-parser@2.6.0(openapi-types@12.1.3)':
     dependencies:
-      '@apidevtools/openapi-schemas': 2.1.0
       '@apidevtools/swagger-methods': 3.0.2
       '@jsdevtools/ono': 7.1.3
-      '@readme/better-ajv-errors': 1.6.0(ajv@8.13.0)
+      '@readme/better-ajv-errors': 1.6.0(ajv@8.17.1)
       '@readme/json-schema-ref-parser': 1.2.0
-      ajv: 8.13.0
-      ajv-draft-04: 1.0.0(ajv@8.13.0)
+      '@readme/openapi-schemas': 3.1.0
+      ajv: 8.17.1
+      ajv-draft-04: 1.0.0(ajv@8.17.1)
       call-me-maybe: 1.0.2
       openapi-types: 12.1.3
 
-  '@rollup/plugin-json@6.1.0(rollup@4.17.2)':
-    dependencies:
-      '@rollup/pluginutils': 5.1.0(rollup@4.17.2)
-    optionalDependencies:
-      rollup: 4.17.2
+  '@readme/openapi-schemas@3.1.0': {}
 
-  '@rollup/plugin-replace@5.0.5(rollup@4.17.2)':
+  '@rollup/plugin-json@6.1.0(rollup@4.22.5)':
     dependencies:
-      '@rollup/pluginutils': 5.1.0(rollup@4.17.2)
-      magic-string: 0.30.7
+      '@rollup/pluginutils': 5.1.2(rollup@4.22.5)
     optionalDependencies:
-      rollup: 4.17.2
+      rollup: 4.22.5
 
-  '@rollup/pluginutils@5.1.0(rollup@4.17.2)':
+  '@rollup/plugin-replace@5.0.7(rollup@4.22.5)':
     dependencies:
-      '@types/estree': 1.0.5
+      '@rollup/pluginutils': 5.1.2(rollup@4.22.5)
+      magic-string: 0.30.10
+    optionalDependencies:
+      rollup: 4.22.5
+
+  '@rollup/pluginutils@5.1.2(rollup@4.22.5)':
+    dependencies:
+      '@types/estree': 1.0.6
       estree-walker: 2.0.2
       picomatch: 2.3.1
     optionalDependencies:
-      rollup: 4.17.2
+      rollup: 4.22.5
 
-  '@rollup/rollup-android-arm-eabi@4.17.2':
+  '@rollup/rollup-android-arm-eabi@4.22.5':
     optional: true
 
-  '@rollup/rollup-android-arm64@4.17.2':
+  '@rollup/rollup-android-arm64@4.22.5':
     optional: true
 
-  '@rollup/rollup-darwin-arm64@4.17.2':
+  '@rollup/rollup-darwin-arm64@4.22.5':
     optional: true
 
-  '@rollup/rollup-darwin-x64@4.17.2':
+  '@rollup/rollup-darwin-x64@4.22.5':
     optional: true
 
-  '@rollup/rollup-linux-arm-gnueabihf@4.17.2':
+  '@rollup/rollup-linux-arm-gnueabihf@4.22.5':
     optional: true
 
-  '@rollup/rollup-linux-arm-musleabihf@4.17.2':
+  '@rollup/rollup-linux-arm-musleabihf@4.22.5':
     optional: true
 
-  '@rollup/rollup-linux-arm64-gnu@4.17.2':
+  '@rollup/rollup-linux-arm64-gnu@4.22.5':
     optional: true
 
-  '@rollup/rollup-linux-arm64-musl@4.17.2':
+  '@rollup/rollup-linux-arm64-musl@4.22.5':
     optional: true
 
-  '@rollup/rollup-linux-powerpc64le-gnu@4.17.2':
+  '@rollup/rollup-linux-powerpc64le-gnu@4.22.5':
     optional: true
 
-  '@rollup/rollup-linux-riscv64-gnu@4.17.2':
+  '@rollup/rollup-linux-riscv64-gnu@4.22.5':
     optional: true
 
-  '@rollup/rollup-linux-s390x-gnu@4.17.2':
+  '@rollup/rollup-linux-s390x-gnu@4.22.5':
     optional: true
 
-  '@rollup/rollup-linux-x64-gnu@4.17.2':
+  '@rollup/rollup-linux-x64-gnu@4.22.5':
     optional: true
 
-  '@rollup/rollup-linux-x64-musl@4.17.2':
+  '@rollup/rollup-linux-x64-musl@4.22.5':
     optional: true
 
-  '@rollup/rollup-win32-arm64-msvc@4.17.2':
+  '@rollup/rollup-win32-arm64-msvc@4.22.5':
     optional: true
 
-  '@rollup/rollup-win32-ia32-msvc@4.17.2':
+  '@rollup/rollup-win32-ia32-msvc@4.22.5':
     optional: true
 
-  '@rollup/rollup-win32-x64-msvc@4.17.2':
+  '@rollup/rollup-win32-x64-msvc@4.22.5':
     optional: true
 
-  '@rushstack/node-core-library@4.1.0(@types/node@20.12.7)':
+  '@rtsao/scc@1.1.0': {}
+
+  '@rushstack/node-core-library@5.9.0(@types/node@20.14.12)':
     dependencies:
+      ajv: 8.13.0
+      ajv-draft-04: 1.0.0(ajv@8.13.0)
+      ajv-formats: 3.0.1(ajv@8.13.0)
       fs-extra: 7.0.1
       import-lazy: 4.0.0
       jju: 1.4.0
       resolve: 1.22.8
       semver: 7.5.4
-      z-schema: 5.0.5
     optionalDependencies:
-      '@types/node': 20.12.7
+      '@types/node': 20.14.12
 
-  '@rushstack/rig-package@0.5.2':
+  '@rushstack/rig-package@0.5.3':
     dependencies:
       resolve: 1.22.8
       strip-json-comments: 3.1.1
 
-  '@rushstack/terminal@0.10.1(@types/node@20.12.7)':
+  '@rushstack/terminal@0.14.2(@types/node@20.14.12)':
     dependencies:
-      '@rushstack/node-core-library': 4.1.0(@types/node@20.12.7)
+      '@rushstack/node-core-library': 5.9.0(@types/node@20.14.12)
       supports-color: 8.1.1
     optionalDependencies:
-      '@types/node': 20.12.7
+      '@types/node': 20.14.12
 
-  '@rushstack/ts-command-line@4.19.2(@types/node@20.12.7)':
+  '@rushstack/ts-command-line@4.22.8(@types/node@20.14.12)':
     dependencies:
-      '@rushstack/terminal': 0.10.1(@types/node@20.12.7)
+      '@rushstack/terminal': 0.14.2(@types/node@20.14.12)
       '@types/argparse': 1.0.38
       argparse: 1.0.10
       string-argv: 0.3.1
     transitivePeerDependencies:
       - '@types/node'
 
-  '@sentry/core@8.5.0':
-    dependencies:
-      '@sentry/types': 8.5.0
-      '@sentry/utils': 8.5.0
+  '@sec-ant/readable-stream@0.4.1': {}
 
-  '@sentry/node@8.5.0':
+  '@sentry/core@8.20.0':
     dependencies:
-      '@opentelemetry/api': 1.8.0
-      '@opentelemetry/context-async-hooks': 1.24.1(@opentelemetry/api@1.8.0)
-      '@opentelemetry/core': 1.24.1(@opentelemetry/api@1.8.0)
-      '@opentelemetry/instrumentation': 0.51.1(@opentelemetry/api@1.8.0)
-      '@opentelemetry/instrumentation-connect': 0.36.0(@opentelemetry/api@1.8.0)
-      '@opentelemetry/instrumentation-express': 0.39.0(@opentelemetry/api@1.8.0)
-      '@opentelemetry/instrumentation-fastify': 0.36.1(@opentelemetry/api@1.8.0)
-      '@opentelemetry/instrumentation-graphql': 0.40.0(@opentelemetry/api@1.8.0)
-      '@opentelemetry/instrumentation-hapi': 0.38.0(@opentelemetry/api@1.8.0)
-      '@opentelemetry/instrumentation-http': 0.51.1(@opentelemetry/api@1.8.0)
-      '@opentelemetry/instrumentation-ioredis': 0.40.0(@opentelemetry/api@1.8.0)
-      '@opentelemetry/instrumentation-koa': 0.40.0(@opentelemetry/api@1.8.0)
-      '@opentelemetry/instrumentation-mongodb': 0.43.0(@opentelemetry/api@1.8.0)
-      '@opentelemetry/instrumentation-mongoose': 0.38.1(@opentelemetry/api@1.8.0)
-      '@opentelemetry/instrumentation-mysql': 0.38.1(@opentelemetry/api@1.8.0)
-      '@opentelemetry/instrumentation-mysql2': 0.38.1(@opentelemetry/api@1.8.0)
-      '@opentelemetry/instrumentation-nestjs-core': 0.37.1(@opentelemetry/api@1.8.0)
-      '@opentelemetry/instrumentation-pg': 0.41.0(@opentelemetry/api@1.8.0)
-      '@opentelemetry/resources': 1.24.1(@opentelemetry/api@1.8.0)
-      '@opentelemetry/sdk-trace-base': 1.24.1(@opentelemetry/api@1.8.0)
-      '@opentelemetry/semantic-conventions': 1.24.1
-      '@prisma/instrumentation': 5.14.0
-      '@sentry/core': 8.5.0
-      '@sentry/opentelemetry': 8.5.0(@opentelemetry/api@1.8.0)(@opentelemetry/core@1.24.1(@opentelemetry/api@1.8.0))(@opentelemetry/instrumentation@0.51.1(@opentelemetry/api@1.8.0))(@opentelemetry/sdk-trace-base@1.24.1(@opentelemetry/api@1.8.0))(@opentelemetry/semantic-conventions@1.24.1)
-      '@sentry/types': 8.5.0
-      '@sentry/utils': 8.5.0
+      '@sentry/types': 8.20.0
+      '@sentry/utils': 8.20.0
+
+  '@sentry/node@8.20.0':
+    dependencies:
+      '@opentelemetry/api': 1.9.0
+      '@opentelemetry/context-async-hooks': 1.25.1(@opentelemetry/api@1.9.0)
+      '@opentelemetry/core': 1.25.1(@opentelemetry/api@1.9.0)
+      '@opentelemetry/instrumentation': 0.52.1(@opentelemetry/api@1.9.0)
+      '@opentelemetry/instrumentation-connect': 0.38.0(@opentelemetry/api@1.9.0)
+      '@opentelemetry/instrumentation-express': 0.41.0(@opentelemetry/api@1.9.0)
+      '@opentelemetry/instrumentation-fastify': 0.38.0(@opentelemetry/api@1.9.0)
+      '@opentelemetry/instrumentation-graphql': 0.42.0(@opentelemetry/api@1.9.0)
+      '@opentelemetry/instrumentation-hapi': 0.40.0(@opentelemetry/api@1.9.0)
+      '@opentelemetry/instrumentation-http': 0.52.1(@opentelemetry/api@1.9.0)
+      '@opentelemetry/instrumentation-ioredis': 0.42.0(@opentelemetry/api@1.9.0)
+      '@opentelemetry/instrumentation-koa': 0.42.0(@opentelemetry/api@1.9.0)
+      '@opentelemetry/instrumentation-mongodb': 0.46.0(@opentelemetry/api@1.9.0)
+      '@opentelemetry/instrumentation-mongoose': 0.40.0(@opentelemetry/api@1.9.0)
+      '@opentelemetry/instrumentation-mysql': 0.40.0(@opentelemetry/api@1.9.0)
+      '@opentelemetry/instrumentation-mysql2': 0.40.0(@opentelemetry/api@1.9.0)
+      '@opentelemetry/instrumentation-nestjs-core': 0.39.0(@opentelemetry/api@1.9.0)
+      '@opentelemetry/instrumentation-pg': 0.43.0(@opentelemetry/api@1.9.0)
+      '@opentelemetry/instrumentation-redis-4': 0.41.0(@opentelemetry/api@1.9.0)
+      '@opentelemetry/resources': 1.25.1(@opentelemetry/api@1.9.0)
+      '@opentelemetry/sdk-trace-base': 1.25.1(@opentelemetry/api@1.9.0)
+      '@opentelemetry/semantic-conventions': 1.25.1
+      '@prisma/instrumentation': 5.17.0
+      '@sentry/core': 8.20.0
+      '@sentry/opentelemetry': 8.20.0(@opentelemetry/api@1.9.0)(@opentelemetry/core@1.25.1(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.52.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.25.1(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.25.1)
+      '@sentry/types': 8.20.0
+      '@sentry/utils': 8.20.0
+      import-in-the-middle: 1.10.0
     optionalDependencies:
-      opentelemetry-instrumentation-fetch-node: 1.2.0
+      opentelemetry-instrumentation-fetch-node: 1.2.3(@opentelemetry/api@1.9.0)
     transitivePeerDependencies:
       - supports-color
 
-  '@sentry/opentelemetry@8.5.0(@opentelemetry/api@1.8.0)(@opentelemetry/core@1.24.1(@opentelemetry/api@1.8.0))(@opentelemetry/instrumentation@0.51.1(@opentelemetry/api@1.8.0))(@opentelemetry/sdk-trace-base@1.24.1(@opentelemetry/api@1.8.0))(@opentelemetry/semantic-conventions@1.24.1)':
+  '@sentry/opentelemetry@8.20.0(@opentelemetry/api@1.9.0)(@opentelemetry/core@1.25.1(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.52.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.25.1(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.25.1)':
     dependencies:
-      '@opentelemetry/api': 1.8.0
-      '@opentelemetry/core': 1.24.1(@opentelemetry/api@1.8.0)
-      '@opentelemetry/instrumentation': 0.51.1(@opentelemetry/api@1.8.0)
-      '@opentelemetry/sdk-trace-base': 1.24.1(@opentelemetry/api@1.8.0)
-      '@opentelemetry/semantic-conventions': 1.24.1
-      '@sentry/core': 8.5.0
-      '@sentry/types': 8.5.0
-      '@sentry/utils': 8.5.0
+      '@opentelemetry/api': 1.9.0
+      '@opentelemetry/core': 1.25.1(@opentelemetry/api@1.9.0)
+      '@opentelemetry/instrumentation': 0.52.1(@opentelemetry/api@1.9.0)
+      '@opentelemetry/sdk-trace-base': 1.25.1(@opentelemetry/api@1.9.0)
+      '@opentelemetry/semantic-conventions': 1.25.1
+      '@sentry/core': 8.20.0
+      '@sentry/types': 8.20.0
+      '@sentry/utils': 8.20.0
 
-  '@sentry/profiling-node@8.5.0':
+  '@sentry/profiling-node@8.20.0':
     dependencies:
-      '@sentry/core': 8.5.0
-      '@sentry/node': 8.5.0
-      '@sentry/types': 8.5.0
-      '@sentry/utils': 8.5.0
+      '@sentry/core': 8.20.0
+      '@sentry/node': 8.20.0
+      '@sentry/types': 8.20.0
+      '@sentry/utils': 8.20.0
       detect-libc: 2.0.3
       node-abi: 3.62.0
     transitivePeerDependencies:
       - supports-color
 
-  '@sentry/types@8.5.0': {}
+  '@sentry/types@8.20.0': {}
 
-  '@sentry/utils@8.5.0':
+  '@sentry/utils@8.20.0':
     dependencies:
-      '@sentry/types': 8.5.0
+      '@sentry/types': 8.20.0
 
-  '@shikijs/core@1.4.0': {}
+  '@shikijs/core@1.21.0':
+    dependencies:
+      '@shikijs/engine-javascript': 1.21.0
+      '@shikijs/engine-oniguruma': 1.21.0
+      '@shikijs/types': 1.21.0
+      '@shikijs/vscode-textmate': 9.2.2
+      '@types/hast': 3.0.4
+      hast-util-to-html: 9.0.3
+
+  '@shikijs/engine-javascript@1.21.0':
+    dependencies:
+      '@shikijs/types': 1.21.0
+      '@shikijs/vscode-textmate': 9.2.2
+      oniguruma-to-js: 0.4.3
+
+  '@shikijs/engine-oniguruma@1.21.0':
+    dependencies:
+      '@shikijs/types': 1.21.0
+      '@shikijs/vscode-textmate': 9.2.2
+
+  '@shikijs/types@1.21.0':
+    dependencies:
+      '@shikijs/vscode-textmate': 9.2.2
+      '@types/hast': 3.0.4
+
+  '@shikijs/vscode-textmate@9.2.2': {}
 
   '@sideway/address@4.1.4':
     dependencies:
       '@hapi/hoek': 9.3.0
 
+  '@sideway/address@4.1.5':
+    dependencies:
+      '@hapi/hoek': 9.3.0
+
   '@sideway/formula@3.0.1': {}
 
   '@sideway/pinpoint@2.0.0': {}
 
-  '@simplewebauthn/server@10.0.0(encoding@0.1.13)':
+  '@simplewebauthn/server@10.0.1(encoding@0.1.13)':
     dependencies:
       '@hexagon/base64': 1.1.27
       '@levischuck/tiny-cbor': 0.2.2
@@ -14483,7 +14437,9 @@ snapshots:
 
   '@sindresorhus/is@5.3.0': {}
 
-  '@sindresorhus/is@6.1.0': {}
+  '@sindresorhus/is@7.0.0': {}
+
+  '@sindresorhus/merge-streams@4.0.0': {}
 
   '@sinonjs/commons@2.0.0':
     dependencies:
@@ -14509,155 +14465,173 @@ snapshots:
 
   '@sinonjs/text-encoding@0.7.2': {}
 
-  '@smithy/abort-controller@2.0.14':
-    dependencies:
-      '@smithy/types': 2.6.0
-      tslib: 2.6.2
-
   '@smithy/abort-controller@2.2.0':
     dependencies:
       '@smithy/types': 2.12.0
-      tslib: 2.6.2
+      tslib: 2.7.0
 
-  '@smithy/chunked-blob-reader-native@2.0.0':
+  '@smithy/abort-controller@3.1.1':
     dependencies:
-      '@smithy/util-base64': 2.0.0
-      tslib: 2.6.2
+      '@smithy/types': 3.3.0
+      tslib: 2.7.0
 
-  '@smithy/chunked-blob-reader@2.0.0':
+  '@smithy/chunked-blob-reader-native@3.0.0':
     dependencies:
-      tslib: 2.6.2
+      '@smithy/util-base64': 3.0.0
+      tslib: 2.7.0
 
-  '@smithy/config-resolver@2.0.9':
+  '@smithy/chunked-blob-reader@3.0.0':
     dependencies:
-      '@smithy/node-config-provider': 2.0.11
-      '@smithy/types': 2.6.0
-      '@smithy/util-config-provider': 2.0.0
-      '@smithy/util-middleware': 2.0.1
-      tslib: 2.6.2
+      tslib: 2.7.0
 
-  '@smithy/credential-provider-imds@2.0.11':
+  '@smithy/config-resolver@3.0.5':
     dependencies:
-      '@smithy/node-config-provider': 2.0.11
-      '@smithy/property-provider': 2.0.9
-      '@smithy/types': 2.6.0
-      '@smithy/url-parser': 2.0.8
-      tslib: 2.6.2
+      '@smithy/node-config-provider': 3.1.4
+      '@smithy/types': 3.3.0
+      '@smithy/util-config-provider': 3.0.0
+      '@smithy/util-middleware': 3.0.3
+      tslib: 2.7.0
 
-  '@smithy/eventstream-codec@2.0.8':
+  '@smithy/core@2.3.1':
     dependencies:
-      '@aws-crypto/crc32': 3.0.0
-      '@smithy/types': 2.6.0
-      '@smithy/util-hex-encoding': 2.0.0
-      tslib: 2.6.2
+      '@smithy/middleware-endpoint': 3.1.0
+      '@smithy/middleware-retry': 3.0.13
+      '@smithy/middleware-serde': 3.0.3
+      '@smithy/protocol-http': 4.1.0
+      '@smithy/smithy-client': 3.1.11
+      '@smithy/types': 3.3.0
+      '@smithy/util-middleware': 3.0.3
+      tslib: 2.7.0
 
-  '@smithy/eventstream-serde-browser@2.0.8':
+  '@smithy/credential-provider-imds@3.2.0':
     dependencies:
-      '@smithy/eventstream-serde-universal': 2.0.8
-      '@smithy/types': 2.6.0
-      tslib: 2.6.2
+      '@smithy/node-config-provider': 3.1.4
+      '@smithy/property-provider': 3.1.3
+      '@smithy/types': 3.3.0
+      '@smithy/url-parser': 3.0.3
+      tslib: 2.7.0
 
-  '@smithy/eventstream-serde-config-resolver@2.0.8':
+  '@smithy/eventstream-codec@3.1.2':
     dependencies:
-      '@smithy/types': 2.6.0
-      tslib: 2.6.2
+      '@aws-crypto/crc32': 5.2.0
+      '@smithy/types': 3.3.0
+      '@smithy/util-hex-encoding': 3.0.0
+      tslib: 2.7.0
 
-  '@smithy/eventstream-serde-node@2.0.8':
+  '@smithy/eventstream-serde-browser@3.0.5':
     dependencies:
-      '@smithy/eventstream-serde-universal': 2.0.8
-      '@smithy/types': 2.6.0
-      tslib: 2.6.2
+      '@smithy/eventstream-serde-universal': 3.0.4
+      '@smithy/types': 3.3.0
+      tslib: 2.7.0
 
-  '@smithy/eventstream-serde-universal@2.0.8':
+  '@smithy/eventstream-serde-config-resolver@3.0.3':
     dependencies:
-      '@smithy/eventstream-codec': 2.0.8
-      '@smithy/types': 2.6.0
-      tslib: 2.6.2
+      '@smithy/types': 3.3.0
+      tslib: 2.7.0
 
-  '@smithy/fetch-http-handler@2.1.4':
+  '@smithy/eventstream-serde-node@3.0.4':
     dependencies:
-      '@smithy/protocol-http': 3.0.10
-      '@smithy/querystring-builder': 2.0.14
-      '@smithy/types': 2.6.0
-      '@smithy/util-base64': 2.0.0
-      tslib: 2.6.2
+      '@smithy/eventstream-serde-universal': 3.0.4
+      '@smithy/types': 3.3.0
+      tslib: 2.7.0
 
-  '@smithy/hash-blob-browser@2.0.8':
+  '@smithy/eventstream-serde-universal@3.0.4':
     dependencies:
-      '@smithy/chunked-blob-reader': 2.0.0
-      '@smithy/chunked-blob-reader-native': 2.0.0
-      '@smithy/types': 2.6.0
-      tslib: 2.6.2
+      '@smithy/eventstream-codec': 3.1.2
+      '@smithy/types': 3.3.0
+      tslib: 2.7.0
 
-  '@smithy/hash-node@2.0.8':
+  '@smithy/fetch-http-handler@3.2.4':
     dependencies:
-      '@smithy/types': 2.6.0
-      '@smithy/util-buffer-from': 2.0.0
-      '@smithy/util-utf8': 2.0.0
-      tslib: 2.6.2
+      '@smithy/protocol-http': 4.1.0
+      '@smithy/querystring-builder': 3.0.3
+      '@smithy/types': 3.3.0
+      '@smithy/util-base64': 3.0.0
+      tslib: 2.7.0
 
-  '@smithy/hash-stream-node@2.0.8':
+  '@smithy/hash-blob-browser@3.1.2':
     dependencies:
-      '@smithy/types': 2.6.0
-      '@smithy/util-utf8': 2.0.0
-      tslib: 2.6.2
+      '@smithy/chunked-blob-reader': 3.0.0
+      '@smithy/chunked-blob-reader-native': 3.0.0
+      '@smithy/types': 3.3.0
+      tslib: 2.7.0
 
-  '@smithy/invalid-dependency@2.0.8':
+  '@smithy/hash-node@3.0.3':
     dependencies:
-      '@smithy/types': 2.6.0
-      tslib: 2.6.2
+      '@smithy/types': 3.3.0
+      '@smithy/util-buffer-from': 3.0.0
+      '@smithy/util-utf8': 3.0.0
+      tslib: 2.7.0
+
+  '@smithy/hash-stream-node@3.1.2':
+    dependencies:
+      '@smithy/types': 3.3.0
+      '@smithy/util-utf8': 3.0.0
+      tslib: 2.7.0
+
+  '@smithy/invalid-dependency@3.0.3':
+    dependencies:
+      '@smithy/types': 3.3.0
+      tslib: 2.7.0
 
   '@smithy/is-array-buffer@2.0.0':
     dependencies:
-      tslib: 2.6.2
+      tslib: 2.7.0
 
-  '@smithy/md5-js@2.0.8':
+  '@smithy/is-array-buffer@3.0.0':
     dependencies:
-      '@smithy/types': 2.6.0
-      '@smithy/util-utf8': 2.0.0
-      tslib: 2.6.2
+      tslib: 2.7.0
 
-  '@smithy/middleware-content-length@2.0.10':
+  '@smithy/md5-js@3.0.3':
     dependencies:
-      '@smithy/protocol-http': 3.0.10
-      '@smithy/types': 2.6.0
-      tslib: 2.6.2
+      '@smithy/types': 3.3.0
+      '@smithy/util-utf8': 3.0.0
+      tslib: 2.7.0
 
-  '@smithy/middleware-endpoint@2.0.8':
+  '@smithy/middleware-content-length@3.0.5':
     dependencies:
-      '@smithy/middleware-serde': 2.0.8
-      '@smithy/types': 2.6.0
-      '@smithy/url-parser': 2.0.8
-      '@smithy/util-middleware': 2.0.1
-      tslib: 2.6.2
+      '@smithy/protocol-http': 4.1.0
+      '@smithy/types': 3.3.0
+      tslib: 2.7.0
 
-  '@smithy/middleware-retry@2.0.11':
+  '@smithy/middleware-endpoint@3.1.0':
     dependencies:
-      '@smithy/node-config-provider': 2.0.11
-      '@smithy/protocol-http': 3.0.10
-      '@smithy/service-error-classification': 2.0.1
-      '@smithy/types': 2.6.0
-      '@smithy/util-middleware': 2.0.1
-      '@smithy/util-retry': 2.0.1
-      tslib: 2.6.2
-      uuid: 8.3.2
+      '@smithy/middleware-serde': 3.0.3
+      '@smithy/node-config-provider': 3.1.4
+      '@smithy/shared-ini-file-loader': 3.1.4
+      '@smithy/types': 3.3.0
+      '@smithy/url-parser': 3.0.3
+      '@smithy/util-middleware': 3.0.3
+      tslib: 2.7.0
 
-  '@smithy/middleware-serde@2.0.8':
+  '@smithy/middleware-retry@3.0.13':
     dependencies:
-      '@smithy/types': 2.6.0
-      tslib: 2.6.2
+      '@smithy/node-config-provider': 3.1.4
+      '@smithy/protocol-http': 4.1.0
+      '@smithy/service-error-classification': 3.0.3
+      '@smithy/smithy-client': 3.1.11
+      '@smithy/types': 3.3.0
+      '@smithy/util-middleware': 3.0.3
+      '@smithy/util-retry': 3.0.3
+      tslib: 2.7.0
+      uuid: 9.0.1
 
-  '@smithy/middleware-stack@2.0.1':
+  '@smithy/middleware-serde@3.0.3':
     dependencies:
-      '@smithy/types': 2.6.0
-      tslib: 2.6.2
+      '@smithy/types': 3.3.0
+      tslib: 2.7.0
 
-  '@smithy/node-config-provider@2.0.11':
+  '@smithy/middleware-stack@3.0.3':
     dependencies:
-      '@smithy/property-provider': 2.0.9
-      '@smithy/shared-ini-file-loader': 2.0.10
-      '@smithy/types': 2.6.0
-      tslib: 2.6.2
+      '@smithy/types': 3.3.0
+      tslib: 2.7.0
+
+  '@smithy/node-config-provider@3.1.4':
+    dependencies:
+      '@smithy/property-provider': 3.1.3
+      '@smithy/shared-ini-file-loader': 3.1.4
+      '@smithy/types': 3.3.0
+      tslib: 2.7.0
 
   '@smithy/node-http-handler@2.5.0':
     dependencies:
@@ -14667,831 +14641,526 @@ snapshots:
       '@smithy/types': 2.12.0
       tslib: 2.6.2
 
-  '@smithy/property-provider@2.0.9':
+  '@smithy/node-http-handler@3.1.4':
     dependencies:
-      '@smithy/types': 2.6.0
-      tslib: 2.6.2
+      '@smithy/abort-controller': 3.1.1
+      '@smithy/protocol-http': 4.1.0
+      '@smithy/querystring-builder': 3.0.3
+      '@smithy/types': 3.3.0
+      tslib: 2.7.0
 
-  '@smithy/protocol-http@3.0.10':
+  '@smithy/property-provider@3.1.3':
     dependencies:
-      '@smithy/types': 2.6.0
-      tslib: 2.6.2
+      '@smithy/types': 3.3.0
+      tslib: 2.7.0
 
   '@smithy/protocol-http@3.3.0':
     dependencies:
       '@smithy/types': 2.12.0
-      tslib: 2.6.2
+      tslib: 2.7.0
 
-  '@smithy/querystring-builder@2.0.14':
+  '@smithy/protocol-http@4.1.0':
     dependencies:
-      '@smithy/types': 2.6.0
-      '@smithy/util-uri-escape': 2.0.0
-      tslib: 2.6.2
+      '@smithy/types': 3.3.0
+      tslib: 2.7.0
 
   '@smithy/querystring-builder@2.2.0':
     dependencies:
       '@smithy/types': 2.12.0
       '@smithy/util-uri-escape': 2.2.0
-      tslib: 2.6.2
+      tslib: 2.7.0
 
-  '@smithy/querystring-parser@2.0.8':
+  '@smithy/querystring-builder@3.0.3':
     dependencies:
-      '@smithy/types': 2.6.0
-      tslib: 2.6.2
+      '@smithy/types': 3.3.0
+      '@smithy/util-uri-escape': 3.0.0
+      tslib: 2.7.0
 
-  '@smithy/service-error-classification@2.0.1':
+  '@smithy/querystring-parser@3.0.3':
     dependencies:
-      '@smithy/types': 2.6.0
+      '@smithy/types': 3.3.0
+      tslib: 2.7.0
 
-  '@smithy/shared-ini-file-loader@2.0.10':
+  '@smithy/service-error-classification@3.0.3':
     dependencies:
-      '@smithy/types': 2.6.0
-      tslib: 2.6.2
+      '@smithy/types': 3.3.0
 
-  '@smithy/signature-v4@2.0.5':
+  '@smithy/shared-ini-file-loader@3.1.4':
     dependencies:
-      '@smithy/eventstream-codec': 2.0.8
-      '@smithy/is-array-buffer': 2.0.0
-      '@smithy/types': 2.6.0
-      '@smithy/util-hex-encoding': 2.0.0
-      '@smithy/util-middleware': 2.0.1
-      '@smithy/util-uri-escape': 2.0.0
-      '@smithy/util-utf8': 2.0.0
-      tslib: 2.6.2
+      '@smithy/types': 3.3.0
+      tslib: 2.7.0
 
-  '@smithy/smithy-client@2.1.5':
+  '@smithy/signature-v4@4.1.0':
     dependencies:
-      '@smithy/middleware-stack': 2.0.1
-      '@smithy/types': 2.6.0
-      '@smithy/util-stream': 2.0.11
-      tslib: 2.6.2
+      '@smithy/is-array-buffer': 3.0.0
+      '@smithy/protocol-http': 4.1.0
+      '@smithy/types': 3.3.0
+      '@smithy/util-hex-encoding': 3.0.0
+      '@smithy/util-middleware': 3.0.3
+      '@smithy/util-uri-escape': 3.0.0
+      '@smithy/util-utf8': 3.0.0
+      tslib: 2.7.0
+
+  '@smithy/smithy-client@3.1.11':
+    dependencies:
+      '@smithy/middleware-endpoint': 3.1.0
+      '@smithy/middleware-stack': 3.0.3
+      '@smithy/protocol-http': 4.1.0
+      '@smithy/types': 3.3.0
+      '@smithy/util-stream': 3.1.3
+      tslib: 2.7.0
 
   '@smithy/types@2.12.0':
     dependencies:
-      tslib: 2.6.2
+      tslib: 2.7.0
 
-  '@smithy/types@2.6.0':
+  '@smithy/types@3.3.0':
     dependencies:
-      tslib: 2.6.2
+      tslib: 2.7.0
 
-  '@smithy/url-parser@2.0.8':
+  '@smithy/url-parser@3.0.3':
     dependencies:
-      '@smithy/querystring-parser': 2.0.8
-      '@smithy/types': 2.6.0
-      tslib: 2.6.2
+      '@smithy/querystring-parser': 3.0.3
+      '@smithy/types': 3.3.0
+      tslib: 2.7.0
 
-  '@smithy/util-base64@2.0.0':
+  '@smithy/util-base64@3.0.0':
     dependencies:
-      '@smithy/util-buffer-from': 2.0.0
-      tslib: 2.6.2
+      '@smithy/util-buffer-from': 3.0.0
+      '@smithy/util-utf8': 3.0.0
+      tslib: 2.7.0
 
-  '@smithy/util-body-length-browser@2.0.0':
+  '@smithy/util-body-length-browser@3.0.0':
     dependencies:
-      tslib: 2.6.2
+      tslib: 2.7.0
 
-  '@smithy/util-body-length-node@2.1.0':
+  '@smithy/util-body-length-node@3.0.0':
     dependencies:
-      tslib: 2.6.2
+      tslib: 2.7.0
 
   '@smithy/util-buffer-from@2.0.0':
     dependencies:
       '@smithy/is-array-buffer': 2.0.0
-      tslib: 2.6.2
+      tslib: 2.7.0
 
-  '@smithy/util-config-provider@2.0.0':
+  '@smithy/util-buffer-from@3.0.0':
     dependencies:
-      tslib: 2.6.2
+      '@smithy/is-array-buffer': 3.0.0
+      tslib: 2.7.0
 
-  '@smithy/util-defaults-mode-browser@2.0.9':
+  '@smithy/util-config-provider@3.0.0':
     dependencies:
-      '@smithy/property-provider': 2.0.9
-      '@smithy/smithy-client': 2.1.5
-      '@smithy/types': 2.6.0
+      tslib: 2.7.0
+
+  '@smithy/util-defaults-mode-browser@3.0.13':
+    dependencies:
+      '@smithy/property-provider': 3.1.3
+      '@smithy/smithy-client': 3.1.11
+      '@smithy/types': 3.3.0
       bowser: 2.11.0
-      tslib: 2.6.2
+      tslib: 2.7.0
 
-  '@smithy/util-defaults-mode-node@2.0.11':
+  '@smithy/util-defaults-mode-node@3.0.13':
     dependencies:
-      '@smithy/config-resolver': 2.0.9
-      '@smithy/credential-provider-imds': 2.0.11
-      '@smithy/node-config-provider': 2.0.11
-      '@smithy/property-provider': 2.0.9
-      '@smithy/smithy-client': 2.1.5
-      '@smithy/types': 2.6.0
-      tslib: 2.6.2
+      '@smithy/config-resolver': 3.0.5
+      '@smithy/credential-provider-imds': 3.2.0
+      '@smithy/node-config-provider': 3.1.4
+      '@smithy/property-provider': 3.1.3
+      '@smithy/smithy-client': 3.1.11
+      '@smithy/types': 3.3.0
+      tslib: 2.7.0
 
-  '@smithy/util-hex-encoding@2.0.0':
+  '@smithy/util-endpoints@2.0.5':
     dependencies:
-      tslib: 2.6.2
+      '@smithy/node-config-provider': 3.1.4
+      '@smithy/types': 3.3.0
+      tslib: 2.7.0
 
-  '@smithy/util-middleware@2.0.1':
+  '@smithy/util-hex-encoding@3.0.0':
     dependencies:
-      '@smithy/types': 2.6.0
-      tslib: 2.6.2
+      tslib: 2.7.0
 
-  '@smithy/util-retry@2.0.1':
+  '@smithy/util-middleware@3.0.3':
     dependencies:
-      '@smithy/service-error-classification': 2.0.1
-      '@smithy/types': 2.6.0
-      tslib: 2.6.2
+      '@smithy/types': 3.3.0
+      tslib: 2.7.0
 
-  '@smithy/util-stream@2.0.11':
+  '@smithy/util-retry@3.0.3':
     dependencies:
-      '@smithy/fetch-http-handler': 2.1.4
-      '@smithy/node-http-handler': 2.5.0
-      '@smithy/types': 2.6.0
-      '@smithy/util-base64': 2.0.0
-      '@smithy/util-buffer-from': 2.0.0
-      '@smithy/util-hex-encoding': 2.0.0
-      '@smithy/util-utf8': 2.0.0
-      tslib: 2.6.2
+      '@smithy/service-error-classification': 3.0.3
+      '@smithy/types': 3.3.0
+      tslib: 2.7.0
 
-  '@smithy/util-uri-escape@2.0.0':
+  '@smithy/util-stream@3.1.3':
     dependencies:
-      tslib: 2.6.2
+      '@smithy/fetch-http-handler': 3.2.4
+      '@smithy/node-http-handler': 3.1.4
+      '@smithy/types': 3.3.0
+      '@smithy/util-base64': 3.0.0
+      '@smithy/util-buffer-from': 3.0.0
+      '@smithy/util-hex-encoding': 3.0.0
+      '@smithy/util-utf8': 3.0.0
+      tslib: 2.7.0
 
   '@smithy/util-uri-escape@2.2.0':
     dependencies:
-      tslib: 2.6.2
+      tslib: 2.7.0
+
+  '@smithy/util-uri-escape@3.0.0':
+    dependencies:
+      tslib: 2.7.0
 
   '@smithy/util-utf8@2.0.0':
     dependencies:
       '@smithy/util-buffer-from': 2.0.0
-      tslib: 2.6.2
+      tslib: 2.7.0
 
-  '@smithy/util-waiter@2.0.8':
+  '@smithy/util-utf8@3.0.0':
     dependencies:
-      '@smithy/abort-controller': 2.0.14
-      '@smithy/types': 2.6.0
-      tslib: 2.6.2
+      '@smithy/util-buffer-from': 3.0.0
+      tslib: 2.7.0
+
+  '@smithy/util-waiter@3.1.2':
+    dependencies:
+      '@smithy/abort-controller': 3.1.1
+      '@smithy/types': 3.3.0
+      tslib: 2.7.0
 
   '@sqltools/formatter@1.2.5': {}
 
-  '@storybook/addon-actions@8.0.9':
+  '@storybook/addon-actions@8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))':
     dependencies:
-      '@storybook/core-events': 8.0.9
       '@storybook/global': 5.0.0
       '@types/uuid': 9.0.8
       dequal: 2.0.3
       polished: 4.2.2
+      storybook: 8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4)
       uuid: 9.0.1
 
-  '@storybook/addon-backgrounds@8.0.9':
+  '@storybook/addon-backgrounds@8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))':
     dependencies:
       '@storybook/global': 5.0.0
       memoizerific: 1.11.3
+      storybook: 8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4)
       ts-dedent: 2.2.0
 
-  '@storybook/addon-controls@8.0.9(@types/react@18.0.28)(encoding@0.1.13)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
+  '@storybook/addon-controls@8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))':
     dependencies:
-      '@storybook/blocks': 8.0.9(@types/react@18.0.28)(encoding@0.1.13)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
-      lodash: 4.17.21
-      ts-dedent: 2.2.0
-    transitivePeerDependencies:
-      - '@types/react'
-      - encoding
-      - react
-      - react-dom
-      - supports-color
-
-  '@storybook/addon-docs@8.0.9(encoding@0.1.13)':
-    dependencies:
-      '@babel/core': 7.24.0
-      '@mdx-js/react': 3.0.1(@types/react@18.0.28)(react@18.3.1)
-      '@storybook/blocks': 8.0.9(@types/react@18.0.28)(encoding@0.1.13)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
-      '@storybook/client-logger': 8.0.9
-      '@storybook/components': 8.0.9(@types/react@18.0.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
-      '@storybook/csf-plugin': 8.0.9
-      '@storybook/csf-tools': 8.0.9
       '@storybook/global': 5.0.0
-      '@storybook/node-logger': 8.0.9
-      '@storybook/preview-api': 8.0.9
-      '@storybook/react-dom-shim': 8.0.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
-      '@storybook/theming': 8.0.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
-      '@storybook/types': 8.0.9
+      dequal: 2.0.3
+      lodash: 4.17.21
+      storybook: 8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4)
+      ts-dedent: 2.2.0
+
+  '@storybook/addon-docs@8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))':
+    dependencies:
+      '@mdx-js/react': 3.0.1(@types/react@18.0.28)(react@18.3.1)
+      '@storybook/blocks': 8.3.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))
+      '@storybook/csf-plugin': 8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))
+      '@storybook/global': 5.0.0
+      '@storybook/react-dom-shim': 8.3.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))
       '@types/react': 18.0.28
       fs-extra: 11.1.1
       react: 18.3.1
       react-dom: 18.3.1(react@18.3.1)
       rehype-external-links: 3.0.0
       rehype-slug: 6.0.0
+      storybook: 8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4)
       ts-dedent: 2.2.0
-    transitivePeerDependencies:
-      - encoding
-      - supports-color
 
-  '@storybook/addon-essentials@8.0.9(@types/react@18.0.28)(encoding@0.1.13)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
+  '@storybook/addon-essentials@8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))':
     dependencies:
-      '@storybook/addon-actions': 8.0.9
-      '@storybook/addon-backgrounds': 8.0.9
-      '@storybook/addon-controls': 8.0.9(@types/react@18.0.28)(encoding@0.1.13)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
-      '@storybook/addon-docs': 8.0.9(encoding@0.1.13)
-      '@storybook/addon-highlight': 8.0.9
-      '@storybook/addon-measure': 8.0.9
-      '@storybook/addon-outline': 8.0.9
-      '@storybook/addon-toolbars': 8.0.9
-      '@storybook/addon-viewport': 8.0.9
-      '@storybook/core-common': 8.0.9(encoding@0.1.13)
-      '@storybook/manager-api': 8.0.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
-      '@storybook/node-logger': 8.0.9
-      '@storybook/preview-api': 8.0.9
+      '@storybook/addon-actions': 8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))
+      '@storybook/addon-backgrounds': 8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))
+      '@storybook/addon-controls': 8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))
+      '@storybook/addon-docs': 8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))
+      '@storybook/addon-highlight': 8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))
+      '@storybook/addon-measure': 8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))
+      '@storybook/addon-outline': 8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))
+      '@storybook/addon-toolbars': 8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))
+      '@storybook/addon-viewport': 8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))
+      storybook: 8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4)
       ts-dedent: 2.2.0
-    transitivePeerDependencies:
-      - '@types/react'
-      - encoding
-      - react
-      - react-dom
-      - supports-color
 
-  '@storybook/addon-highlight@8.0.9':
+  '@storybook/addon-highlight@8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))':
     dependencies:
       '@storybook/global': 5.0.0
+      storybook: 8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4)
 
-  '@storybook/addon-interactions@8.0.9(@jest/globals@29.7.0)(@types/jest@29.5.12)(jest@29.7.0(@types/node@20.12.7))(vitest@0.34.6(happy-dom@10.0.3)(jsdom@24.0.0(bufferutil@4.0.7)(utf-8-validate@6.0.3))(sass@1.76.0)(terser@5.30.3))':
+  '@storybook/addon-interactions@8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))':
     dependencies:
       '@storybook/global': 5.0.0
-      '@storybook/instrumenter': 8.0.9
-      '@storybook/test': 8.0.9(@jest/globals@29.7.0)(@types/jest@29.5.12)(jest@29.7.0(@types/node@20.12.7))(vitest@0.34.6(happy-dom@10.0.3)(jsdom@24.0.0(bufferutil@4.0.7)(utf-8-validate@6.0.3))(sass@1.76.0)(terser@5.30.3))
-      '@storybook/types': 8.0.9
+      '@storybook/instrumenter': 8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))
+      '@storybook/test': 8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))
       polished: 4.2.2
+      storybook: 8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4)
       ts-dedent: 2.2.0
-    transitivePeerDependencies:
-      - '@jest/globals'
-      - '@types/bun'
-      - '@types/jest'
-      - jest
-      - vitest
 
-  '@storybook/addon-links@8.0.9(react@18.3.1)':
+  '@storybook/addon-links@8.3.4(react@18.3.1)(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))':
     dependencies:
-      '@storybook/csf': 0.1.6
+      '@storybook/csf': 0.1.11
       '@storybook/global': 5.0.0
+      storybook: 8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4)
       ts-dedent: 2.2.0
     optionalDependencies:
       react: 18.3.1
 
-  '@storybook/addon-mdx-gfm@8.0.9':
+  '@storybook/addon-mdx-gfm@8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))':
     dependencies:
-      '@storybook/node-logger': 8.0.9
       remark-gfm: 4.0.0
+      storybook: 8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4)
       ts-dedent: 2.2.0
     transitivePeerDependencies:
       - supports-color
 
-  '@storybook/addon-measure@8.0.9':
+  '@storybook/addon-measure@8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))':
     dependencies:
       '@storybook/global': 5.0.0
-      tiny-invariant: 1.3.1
+      storybook: 8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4)
+      tiny-invariant: 1.3.3
 
-  '@storybook/addon-outline@8.0.9':
+  '@storybook/addon-outline@8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))':
     dependencies:
       '@storybook/global': 5.0.0
+      storybook: 8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4)
       ts-dedent: 2.2.0
 
-  '@storybook/addon-storysource@8.0.9':
+  '@storybook/addon-storysource@8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))':
     dependencies:
-      '@storybook/source-loader': 8.0.9
+      '@storybook/source-loader': 8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))
       estraverse: 5.3.0
-      tiny-invariant: 1.3.1
+      storybook: 8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4)
+      tiny-invariant: 1.3.3
 
-  '@storybook/addon-toolbars@8.0.9': {}
+  '@storybook/addon-toolbars@8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))':
+    dependencies:
+      storybook: 8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4)
 
-  '@storybook/addon-viewport@8.0.9':
+  '@storybook/addon-viewport@8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))':
     dependencies:
       memoizerific: 1.11.3
+      storybook: 8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4)
 
-  '@storybook/blocks@8.0.9(@types/react@18.0.28)(encoding@0.1.13)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
+  '@storybook/blocks@8.3.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))':
     dependencies:
-      '@storybook/channels': 8.0.9
-      '@storybook/client-logger': 8.0.9
-      '@storybook/components': 8.0.9(@types/react@18.0.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
-      '@storybook/core-events': 8.0.9
-      '@storybook/csf': 0.1.6
-      '@storybook/docs-tools': 8.0.9(encoding@0.1.13)
+      '@storybook/csf': 0.1.11
       '@storybook/global': 5.0.0
-      '@storybook/icons': 1.2.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
-      '@storybook/manager-api': 8.0.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
-      '@storybook/preview-api': 8.0.9
-      '@storybook/theming': 8.0.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
-      '@storybook/types': 8.0.9
+      '@storybook/icons': 1.2.12(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
       '@types/lodash': 4.14.191
       color-convert: 2.0.1
       dequal: 2.0.3
       lodash: 4.17.21
-      markdown-to-jsx: 7.3.2(react@18.3.1)
+      markdown-to-jsx: 7.4.7(react@18.3.1)
       memoizerific: 1.11.3
       polished: 4.2.2
       react-colorful: 5.6.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+      storybook: 8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4)
       telejson: 7.2.0
-      tocbot: 4.21.1
       ts-dedent: 2.2.0
       util-deprecate: 1.0.2
     optionalDependencies:
       react: 18.3.1
       react-dom: 18.3.1(react@18.3.1)
-    transitivePeerDependencies:
-      - '@types/react'
-      - encoding
-      - supports-color
 
-  '@storybook/builder-manager@8.0.9(encoding@0.1.13)':
+  '@storybook/builder-vite@8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))(typescript@5.6.2)(vite@5.4.8(@types/node@20.14.12)(sass@1.79.3)(terser@5.33.0))':
     dependencies:
-      '@fal-works/esbuild-plugin-global-externals': 2.1.2
-      '@storybook/core-common': 8.0.9(encoding@0.1.13)
-      '@storybook/manager': 8.0.9
-      '@storybook/node-logger': 8.0.9
-      '@types/ejs': 3.1.2
-      '@yarnpkg/esbuild-plugin-pnp': 3.0.0-rc.15(esbuild@0.20.2)
-      browser-assert: 1.2.1
-      ejs: 3.1.9
-      esbuild: 0.20.2
-      esbuild-plugin-alias: 0.2.1
-      express: 4.19.2
-      fs-extra: 11.1.1
-      process: 0.11.10
-      util: 0.12.5
-    transitivePeerDependencies:
-      - encoding
-      - supports-color
-
-  '@storybook/builder-vite@8.0.9(encoding@0.1.13)(typescript@5.4.5)(vite@5.2.11(@types/node@20.12.7)(sass@1.76.0)(terser@5.30.3))':
-    dependencies:
-      '@storybook/channels': 8.0.9
-      '@storybook/client-logger': 8.0.9
-      '@storybook/core-common': 8.0.9(encoding@0.1.13)
-      '@storybook/core-events': 8.0.9
-      '@storybook/csf-plugin': 8.0.9
-      '@storybook/node-logger': 8.0.9
-      '@storybook/preview': 8.0.9
-      '@storybook/preview-api': 8.0.9
-      '@storybook/types': 8.0.9
+      '@storybook/csf-plugin': 8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))
       '@types/find-cache-dir': 3.2.1
       browser-assert: 1.2.1
-      es-module-lexer: 0.9.3
-      express: 4.18.2
+      es-module-lexer: 1.5.4
+      express: 4.21.0
       find-cache-dir: 3.3.2
       fs-extra: 11.1.1
-      magic-string: 0.30.7
+      magic-string: 0.30.11
+      storybook: 8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4)
       ts-dedent: 2.2.0
-      vite: 5.2.11(@types/node@20.12.7)(sass@1.76.0)(terser@5.30.3)
+      vite: 5.4.8(@types/node@20.14.12)(sass@1.79.3)(terser@5.33.0)
     optionalDependencies:
-      typescript: 5.4.5
-    transitivePeerDependencies:
-      - encoding
-      - supports-color
-
-  '@storybook/channels@8.0.9':
-    dependencies:
-      '@storybook/client-logger': 8.0.9
-      '@storybook/core-events': 8.0.9
-      '@storybook/global': 5.0.0
-      telejson: 7.2.0
-      tiny-invariant: 1.3.1
-
-  '@storybook/cli@8.0.9(@babel/preset-env@7.23.5(@babel/core@7.24.0))(bufferutil@4.0.7)(encoding@0.1.13)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(utf-8-validate@6.0.3)':
-    dependencies:
-      '@babel/core': 7.24.0
-      '@babel/types': 7.24.0
-      '@ndelangen/get-tarball': 3.0.7
-      '@storybook/codemod': 8.0.9
-      '@storybook/core-common': 8.0.9(encoding@0.1.13)
-      '@storybook/core-events': 8.0.9
-      '@storybook/core-server': 8.0.9(bufferutil@4.0.7)(encoding@0.1.13)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(utf-8-validate@6.0.3)
-      '@storybook/csf-tools': 8.0.9
-      '@storybook/node-logger': 8.0.9
-      '@storybook/telemetry': 8.0.9(encoding@0.1.13)
-      '@storybook/types': 8.0.9
-      '@types/semver': 7.5.8
-      '@yarnpkg/fslib': 2.10.3
-      '@yarnpkg/libzip': 2.3.0
-      chalk: 4.1.2
-      commander: 6.2.1
-      cross-spawn: 7.0.3
-      detect-indent: 6.1.0
-      envinfo: 7.8.1
-      execa: 5.1.1
-      find-up: 5.0.0
-      fs-extra: 11.1.1
-      get-npm-tarball-url: 2.0.3
-      giget: 1.1.2
-      globby: 11.1.0
-      jscodeshift: 0.15.1(@babel/preset-env@7.23.5(@babel/core@7.24.0))
-      leven: 3.1.0
-      ora: 5.4.1
-      prettier: 3.2.5
-      prompts: 2.4.2
-      read-pkg-up: 7.0.1
-      semver: 7.6.0
-      strip-json-comments: 3.1.1
-      tempy: 1.0.1
-      tiny-invariant: 1.3.1
-      ts-dedent: 2.2.0
-    transitivePeerDependencies:
-      - '@babel/preset-env'
-      - bufferutil
-      - encoding
-      - react
-      - react-dom
-      - supports-color
-      - utf-8-validate
-
-  '@storybook/client-logger@8.0.9':
-    dependencies:
-      '@storybook/global': 5.0.0
-
-  '@storybook/codemod@8.0.9':
-    dependencies:
-      '@babel/core': 7.24.0
-      '@babel/preset-env': 7.23.5(@babel/core@7.24.0)
-      '@babel/types': 7.24.0
-      '@storybook/csf': 0.1.6
-      '@storybook/csf-tools': 8.0.9
-      '@storybook/node-logger': 8.0.9
-      '@storybook/types': 8.0.9
-      '@types/cross-spawn': 6.0.2
-      cross-spawn: 7.0.3
-      globby: 11.1.0
-      jscodeshift: 0.15.1(@babel/preset-env@7.23.5(@babel/core@7.24.0))
-      lodash: 4.17.21
-      prettier: 3.2.5
-      recast: 0.23.6
-      tiny-invariant: 1.3.1
+      typescript: 5.6.2
     transitivePeerDependencies:
       - supports-color
 
-  '@storybook/components@8.0.9(@types/react@18.0.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
+  '@storybook/components@8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))':
     dependencies:
-      '@radix-ui/react-slot': 1.0.2(@types/react@18.0.28)(react@18.3.1)
-      '@storybook/client-logger': 8.0.9
-      '@storybook/csf': 0.1.6
-      '@storybook/global': 5.0.0
-      '@storybook/icons': 1.2.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
-      '@storybook/theming': 8.0.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
-      '@storybook/types': 8.0.9
-      memoizerific: 1.11.3
-      react: 18.3.1
-      react-dom: 18.3.1(react@18.3.1)
-      util-deprecate: 1.0.2
-    transitivePeerDependencies:
-      - '@types/react'
+      storybook: 8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4)
 
-  '@storybook/core-common@8.0.9(encoding@0.1.13)':
+  '@storybook/core-events@8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))':
     dependencies:
-      '@storybook/core-events': 8.0.9
-      '@storybook/csf-tools': 8.0.9
-      '@storybook/node-logger': 8.0.9
-      '@storybook/types': 8.0.9
-      '@yarnpkg/fslib': 2.10.3
-      '@yarnpkg/libzip': 2.3.0
-      chalk: 4.1.2
-      cross-spawn: 7.0.3
-      esbuild: 0.20.2
-      esbuild-register: 3.5.0(esbuild@0.20.2)
-      execa: 5.1.1
-      file-system-cache: 2.3.0
-      find-cache-dir: 3.3.2
-      find-up: 5.0.0
-      fs-extra: 11.1.1
-      glob: 10.3.12
-      handlebars: 4.7.7
-      lazy-universal-dotenv: 4.0.0
-      node-fetch: 2.7.0(encoding@0.1.13)
-      picomatch: 2.3.1
-      pkg-dir: 5.0.0
-      pretty-hrtime: 1.0.3
-      resolve-from: 5.0.0
-      semver: 7.6.0
-      tempy: 1.0.1
-      tiny-invariant: 1.3.1
-      ts-dedent: 2.2.0
-      util: 0.12.5
-    transitivePeerDependencies:
-      - encoding
-      - supports-color
+      storybook: 8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4)
 
-  '@storybook/core-events@8.0.9':
+  '@storybook/core@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4)':
     dependencies:
-      ts-dedent: 2.2.0
-
-  '@storybook/core-server@8.0.9(bufferutil@4.0.7)(encoding@0.1.13)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(utf-8-validate@6.0.3)':
-    dependencies:
-      '@aw-web-design/x-default-browser': 1.4.126
-      '@babel/core': 7.24.0
-      '@discoveryjs/json-ext': 0.5.7
-      '@storybook/builder-manager': 8.0.9(encoding@0.1.13)
-      '@storybook/channels': 8.0.9
-      '@storybook/core-common': 8.0.9(encoding@0.1.13)
-      '@storybook/core-events': 8.0.9
-      '@storybook/csf': 0.1.6
-      '@storybook/csf-tools': 8.0.9
-      '@storybook/docs-mdx': 3.0.0
-      '@storybook/global': 5.0.0
-      '@storybook/manager': 8.0.9
-      '@storybook/manager-api': 8.0.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
-      '@storybook/node-logger': 8.0.9
-      '@storybook/preview-api': 8.0.9
-      '@storybook/telemetry': 8.0.9(encoding@0.1.13)
-      '@storybook/types': 8.0.9
-      '@types/detect-port': 1.3.2
-      '@types/node': 18.17.15
-      '@types/pretty-hrtime': 1.0.1
-      '@types/semver': 7.5.8
+      '@storybook/csf': 0.1.11
+      '@types/express': 4.17.21
       better-opn: 3.0.2
-      chalk: 4.1.2
-      cli-table3: 0.6.3
-      compression: 1.7.4
-      detect-port: 1.5.1
-      express: 4.18.2
-      fs-extra: 11.1.1
-      globby: 11.1.0
-      ip: 2.0.1
-      lodash: 4.17.21
-      open: 8.4.2
-      pretty-hrtime: 1.0.3
-      prompts: 2.4.2
-      read-pkg-up: 7.0.1
-      semver: 7.6.0
-      telejson: 7.2.0
-      tiny-invariant: 1.3.1
-      ts-dedent: 2.2.0
+      browser-assert: 1.2.1
+      esbuild: 0.23.1
+      esbuild-register: 3.5.0(esbuild@0.23.1)
+      express: 4.21.0
+      jsdoc-type-pratt-parser: 4.1.0
+      process: 0.11.10
+      recast: 0.23.6
+      semver: 7.6.3
       util: 0.12.5
-      util-deprecate: 1.0.2
-      watchpack: 2.4.0
-      ws: 8.17.0(bufferutil@4.0.7)(utf-8-validate@6.0.3)
+      ws: 8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4)
     transitivePeerDependencies:
       - bufferutil
-      - encoding
-      - react
-      - react-dom
       - supports-color
       - utf-8-validate
 
-  '@storybook/csf-plugin@8.0.9':
+  '@storybook/csf-plugin@8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))':
     dependencies:
-      '@storybook/csf-tools': 8.0.9
+      storybook: 8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4)
       unplugin: 1.4.0
-    transitivePeerDependencies:
-      - supports-color
 
-  '@storybook/csf-tools@8.0.9':
-    dependencies:
-      '@babel/generator': 7.23.6
-      '@babel/parser': 7.24.0
-      '@babel/traverse': 7.24.0
-      '@babel/types': 7.24.0
-      '@storybook/csf': 0.1.6
-      '@storybook/types': 8.0.9
-      fs-extra: 11.1.1
-      recast: 0.23.6
-      ts-dedent: 2.2.0
-    transitivePeerDependencies:
-      - supports-color
-
-  '@storybook/csf@0.1.6':
+  '@storybook/csf@0.1.11':
     dependencies:
       type-fest: 2.19.0
 
-  '@storybook/docs-mdx@3.0.0': {}
-
-  '@storybook/docs-tools@8.0.9(encoding@0.1.13)':
-    dependencies:
-      '@storybook/core-common': 8.0.9(encoding@0.1.13)
-      '@storybook/core-events': 8.0.9
-      '@storybook/preview-api': 8.0.9
-      '@storybook/types': 8.0.9
-      '@types/doctrine': 0.0.3
-      assert: 2.1.0
-      doctrine: 3.0.0
-      lodash: 4.17.21
-    transitivePeerDependencies:
-      - encoding
-      - supports-color
-
   '@storybook/global@5.0.0': {}
 
-  '@storybook/icons@1.2.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
+  '@storybook/icons@1.2.12(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
     dependencies:
       react: 18.3.1
       react-dom: 18.3.1(react@18.3.1)
 
-  '@storybook/instrumenter@8.0.9':
+  '@storybook/instrumenter@8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))':
     dependencies:
-      '@storybook/channels': 8.0.9
-      '@storybook/client-logger': 8.0.9
-      '@storybook/core-events': 8.0.9
       '@storybook/global': 5.0.0
-      '@storybook/preview-api': 8.0.9
-      '@vitest/utils': 1.6.0
+      '@vitest/utils': 2.1.1
+      storybook: 8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4)
       util: 0.12.5
 
-  '@storybook/manager-api@8.0.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
+  '@storybook/manager-api@8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))':
     dependencies:
-      '@storybook/channels': 8.0.9
-      '@storybook/client-logger': 8.0.9
-      '@storybook/core-events': 8.0.9
-      '@storybook/csf': 0.1.6
-      '@storybook/global': 5.0.0
-      '@storybook/icons': 1.2.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
-      '@storybook/router': 8.0.9
-      '@storybook/theming': 8.0.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
-      '@storybook/types': 8.0.9
-      dequal: 2.0.3
-      lodash: 4.17.21
-      memoizerific: 1.11.3
-      store2: 2.14.2
-      telejson: 7.2.0
-      ts-dedent: 2.2.0
-    transitivePeerDependencies:
-      - react
-      - react-dom
+      storybook: 8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4)
 
-  '@storybook/manager@8.0.9': {}
-
-  '@storybook/node-logger@8.0.9': {}
-
-  '@storybook/preview-api@8.0.9':
+  '@storybook/preview-api@8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))':
     dependencies:
-      '@storybook/channels': 8.0.9
-      '@storybook/client-logger': 8.0.9
-      '@storybook/core-events': 8.0.9
-      '@storybook/csf': 0.1.6
-      '@storybook/global': 5.0.0
-      '@storybook/types': 8.0.9
-      '@types/qs': 6.9.7
-      dequal: 2.0.3
-      lodash: 4.17.21
-      memoizerific: 1.11.3
-      qs: 6.11.1
-      tiny-invariant: 1.3.1
-      ts-dedent: 2.2.0
-      util-deprecate: 1.0.2
+      storybook: 8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4)
 
-  '@storybook/preview@8.0.9': {}
-
-  '@storybook/react-dom-shim@8.0.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
+  '@storybook/react-dom-shim@8.3.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))':
     dependencies:
       react: 18.3.1
       react-dom: 18.3.1(react@18.3.1)
+      storybook: 8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4)
 
-  '@storybook/react-vite@8.0.9(encoding@0.1.13)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@4.17.2)(typescript@5.4.5)(vite@5.2.11(@types/node@20.12.7)(sass@1.76.0)(terser@5.30.3))':
+  '@storybook/react-vite@8.3.4(@storybook/test@8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4)))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@4.22.5)(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))(typescript@5.6.2)(vite@5.4.8(@types/node@20.14.12)(sass@1.79.3)(terser@5.33.0))':
     dependencies:
-      '@joshwooding/vite-plugin-react-docgen-typescript': 0.3.0(typescript@5.4.5)(vite@5.2.11(@types/node@20.12.7)(sass@1.76.0)(terser@5.30.3))
-      '@rollup/pluginutils': 5.1.0(rollup@4.17.2)
-      '@storybook/builder-vite': 8.0.9(encoding@0.1.13)(typescript@5.4.5)(vite@5.2.11(@types/node@20.12.7)(sass@1.76.0)(terser@5.30.3))
-      '@storybook/node-logger': 8.0.9
-      '@storybook/react': 8.0.9(encoding@0.1.13)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.4.5)
+      '@joshwooding/vite-plugin-react-docgen-typescript': 0.3.0(typescript@5.6.2)(vite@5.4.8(@types/node@20.14.12)(sass@1.79.3)(terser@5.33.0))
+      '@rollup/pluginutils': 5.1.2(rollup@4.22.5)
+      '@storybook/builder-vite': 8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))(typescript@5.6.2)(vite@5.4.8(@types/node@20.14.12)(sass@1.79.3)(terser@5.33.0))
+      '@storybook/react': 8.3.4(@storybook/test@8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4)))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))(typescript@5.6.2)
       find-up: 5.0.0
-      magic-string: 0.30.7
+      magic-string: 0.30.11
       react: 18.3.1
       react-docgen: 7.0.1
       react-dom: 18.3.1(react@18.3.1)
       resolve: 1.22.8
+      storybook: 8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4)
       tsconfig-paths: 4.2.0
-      vite: 5.2.11(@types/node@20.12.7)(sass@1.76.0)(terser@5.30.3)
+      vite: 5.4.8(@types/node@20.14.12)(sass@1.79.3)(terser@5.33.0)
     transitivePeerDependencies:
       - '@preact/preset-vite'
-      - encoding
+      - '@storybook/test'
       - rollup
       - supports-color
       - typescript
       - vite-plugin-glimmerx
 
-  '@storybook/react@8.0.9(encoding@0.1.13)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.4.5)':
+  '@storybook/react@8.3.4(@storybook/test@8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4)))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))(typescript@5.6.2)':
     dependencies:
-      '@storybook/client-logger': 8.0.9
-      '@storybook/docs-tools': 8.0.9(encoding@0.1.13)
+      '@storybook/components': 8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))
       '@storybook/global': 5.0.0
-      '@storybook/preview-api': 8.0.9
-      '@storybook/react-dom-shim': 8.0.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
-      '@storybook/types': 8.0.9
+      '@storybook/manager-api': 8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))
+      '@storybook/preview-api': 8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))
+      '@storybook/react-dom-shim': 8.3.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))
+      '@storybook/theming': 8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))
       '@types/escodegen': 0.0.6
       '@types/estree': 0.0.51
-      '@types/node': 18.17.15
+      '@types/node': 22.5.5
       acorn: 7.4.1
       acorn-jsx: 5.3.2(acorn@7.4.1)
       acorn-walk: 7.2.0
       escodegen: 2.1.0
       html-tags: 3.2.0
-      lodash: 4.17.21
       prop-types: 15.8.1
       react: 18.3.1
       react-dom: 18.3.1(react@18.3.1)
       react-element-to-jsx-string: 15.0.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
-      semver: 7.6.0
+      semver: 7.6.3
+      storybook: 8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4)
       ts-dedent: 2.2.0
       type-fest: 2.19.0
       util-deprecate: 1.0.2
     optionalDependencies:
-      typescript: 5.4.5
-    transitivePeerDependencies:
-      - encoding
-      - supports-color
+      '@storybook/test': 8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))
+      typescript: 5.6.2
 
-  '@storybook/router@8.0.9':
+  '@storybook/source-loader@8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))':
     dependencies:
-      '@storybook/client-logger': 8.0.9
-      memoizerific: 1.11.3
-      qs: 6.11.1
-
-  '@storybook/source-loader@8.0.9':
-    dependencies:
-      '@storybook/csf': 0.1.6
-      '@storybook/types': 8.0.9
+      '@storybook/csf': 0.1.11
       estraverse: 5.3.0
       lodash: 4.17.21
-      prettier: 3.2.5
+      prettier: 3.3.3
+      storybook: 8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4)
 
-  '@storybook/telemetry@8.0.9(encoding@0.1.13)':
+  '@storybook/test@8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))':
     dependencies:
-      '@storybook/client-logger': 8.0.9
-      '@storybook/core-common': 8.0.9(encoding@0.1.13)
-      '@storybook/csf-tools': 8.0.9
-      chalk: 4.1.2
-      detect-package-manager: 2.0.1
-      fetch-retry: 5.0.4
-      fs-extra: 11.1.1
-      read-pkg-up: 7.0.1
-    transitivePeerDependencies:
-      - encoding
-      - supports-color
-
-  '@storybook/test@8.0.9(@jest/globals@29.7.0)(@types/jest@29.5.12)(jest@29.7.0(@types/node@20.12.7))(vitest@0.34.6(happy-dom@10.0.3)(jsdom@24.0.0(bufferutil@4.0.7)(utf-8-validate@6.0.3))(sass@1.76.0)(terser@5.30.3))':
-    dependencies:
-      '@storybook/client-logger': 8.0.9
-      '@storybook/core-events': 8.0.9
-      '@storybook/instrumenter': 8.0.9
-      '@storybook/preview-api': 8.0.9
-      '@testing-library/dom': 9.3.4
-      '@testing-library/jest-dom': 6.4.2(@jest/globals@29.7.0)(@types/jest@29.5.12)(jest@29.7.0(@types/node@20.12.7))(vitest@0.34.6(happy-dom@10.0.3)(jsdom@24.0.0(bufferutil@4.0.7)(utf-8-validate@6.0.3))(sass@1.76.0)(terser@5.30.3))
-      '@testing-library/user-event': 14.5.2(@testing-library/dom@9.3.4)
-      '@vitest/expect': 1.3.1
-      '@vitest/spy': 1.6.0
-      util: 0.12.5
-    transitivePeerDependencies:
-      - '@jest/globals'
-      - '@types/bun'
-      - '@types/jest'
-      - jest
-      - vitest
-
-  '@storybook/theming@8.0.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
-    dependencies:
-      '@emotion/use-insertion-effect-with-fallbacks': 1.0.1(react@18.3.1)
-      '@storybook/client-logger': 8.0.9
+      '@storybook/csf': 0.1.11
       '@storybook/global': 5.0.0
-      memoizerific: 1.11.3
-    optionalDependencies:
-      react: 18.3.1
-      react-dom: 18.3.1(react@18.3.1)
+      '@storybook/instrumenter': 8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))
+      '@testing-library/dom': 10.4.0
+      '@testing-library/jest-dom': 6.5.0
+      '@testing-library/user-event': 14.5.2(@testing-library/dom@10.4.0)
+      '@vitest/expect': 2.0.5
+      '@vitest/spy': 2.0.5
+      storybook: 8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4)
+      util: 0.12.5
 
-  '@storybook/types@8.0.9':
+  '@storybook/theming@8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))':
     dependencies:
-      '@storybook/channels': 8.0.9
-      '@types/express': 4.17.17
-      file-system-cache: 2.3.0
+      storybook: 8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4)
 
-  '@storybook/vue3-vite@8.0.9(bufferutil@4.0.7)(encoding@0.1.13)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(utf-8-validate@6.0.3)(vite@5.2.11(@types/node@20.12.7)(sass@1.76.0)(terser@5.30.3))(vue@3.4.26(typescript@5.4.5))':
+  '@storybook/types@8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))':
     dependencies:
-      '@storybook/builder-vite': 8.0.9(encoding@0.1.13)(typescript@5.4.5)(vite@5.2.11(@types/node@20.12.7)(sass@1.76.0)(terser@5.30.3))
-      '@storybook/core-server': 8.0.9(bufferutil@4.0.7)(encoding@0.1.13)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(utf-8-validate@6.0.3)
-      '@storybook/vue3': 8.0.9(encoding@0.1.13)(vue@3.4.26(typescript@5.4.5))
+      storybook: 8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4)
+
+  '@storybook/vue3-vite@8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))(vite@5.4.8(@types/node@20.14.12)(sass@1.79.3)(terser@5.33.0))(vue@3.5.11(typescript@5.6.2))':
+    dependencies:
+      '@storybook/builder-vite': 8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))(typescript@5.6.2)(vite@5.4.8(@types/node@20.14.12)(sass@1.79.3)(terser@5.33.0))
+      '@storybook/vue3': 8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))(vue@3.5.11(typescript@5.6.2))
       find-package-json: 1.2.0
-      magic-string: 0.30.7
-      typescript: 5.4.5
-      vite: 5.2.11(@types/node@20.12.7)(sass@1.76.0)(terser@5.30.3)
-      vue-component-meta: 2.0.16(typescript@5.4.5)
-      vue-docgen-api: 4.75.1(vue@3.4.26(typescript@5.4.5))
+      magic-string: 0.30.11
+      storybook: 8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4)
+      typescript: 5.6.2
+      vite: 5.4.8(@types/node@20.14.12)(sass@1.79.3)(terser@5.33.0)
+      vue-component-meta: 2.0.16(typescript@5.6.2)
+      vue-docgen-api: 4.75.1(vue@3.5.11(typescript@5.6.2))
     transitivePeerDependencies:
       - '@preact/preset-vite'
-      - bufferutil
-      - encoding
-      - react
-      - react-dom
       - supports-color
-      - utf-8-validate
       - vite-plugin-glimmerx
       - vue
 
-  '@storybook/vue3@8.0.9(encoding@0.1.13)(vue@3.4.26(typescript@5.4.5))':
+  '@storybook/vue3@8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))(vue@3.5.11(typescript@5.6.2))':
     dependencies:
-      '@storybook/docs-tools': 8.0.9(encoding@0.1.13)
+      '@storybook/components': 8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))
       '@storybook/global': 5.0.0
-      '@storybook/preview-api': 8.0.9
-      '@storybook/types': 8.0.9
-      '@vue/compiler-core': 3.4.21
-      lodash: 4.17.21
+      '@storybook/manager-api': 8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))
+      '@storybook/preview-api': 8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))
+      '@storybook/theming': 8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))
+      '@vue/compiler-core': 3.5.10
+      storybook: 8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4)
       ts-dedent: 2.2.0
       type-fest: 2.19.0
-      vue: 3.4.26(typescript@5.4.5)
-      vue-component-type-helpers: 2.0.19
-    transitivePeerDependencies:
-      - encoding
-      - supports-color
+      vue: 3.5.11(typescript@5.6.2)
+      vue-component-type-helpers: 2.1.6
 
-  '@swc/cli@0.3.12(@swc/core@1.4.17)(chokidar@3.5.3)':
+  '@swc/cli@0.3.12(@swc/core@1.6.6)(chokidar@3.5.3)':
     dependencies:
       '@mole-inc/bin-wrapper': 8.0.1
-      '@swc/core': 1.4.17
+      '@swc/core': 1.6.6
       '@swc/counter': 0.1.3
       commander: 8.3.0
       fast-glob: 3.3.2
@@ -15511,13 +15180,19 @@ snapshots:
   '@swc/core-darwin-arm64@1.3.56':
     optional: true
 
-  '@swc/core-darwin-arm64@1.4.17':
+  '@swc/core-darwin-arm64@1.6.13':
+    optional: true
+
+  '@swc/core-darwin-arm64@1.6.6':
     optional: true
 
   '@swc/core-darwin-x64@1.3.56':
     optional: true
 
-  '@swc/core-darwin-x64@1.4.17':
+  '@swc/core-darwin-x64@1.6.13':
+    optional: true
+
+  '@swc/core-darwin-x64@1.6.6':
     optional: true
 
   '@swc/core-freebsd-x64@1.3.11':
@@ -15528,82 +15203,131 @@ snapshots:
   '@swc/core-linux-arm-gnueabihf@1.3.56':
     optional: true
 
-  '@swc/core-linux-arm-gnueabihf@1.4.17':
+  '@swc/core-linux-arm-gnueabihf@1.6.13':
+    optional: true
+
+  '@swc/core-linux-arm-gnueabihf@1.6.6':
     optional: true
 
   '@swc/core-linux-arm64-gnu@1.3.56':
     optional: true
 
-  '@swc/core-linux-arm64-gnu@1.4.17':
+  '@swc/core-linux-arm64-gnu@1.6.13':
+    optional: true
+
+  '@swc/core-linux-arm64-gnu@1.6.6':
     optional: true
 
   '@swc/core-linux-arm64-musl@1.3.56':
     optional: true
 
-  '@swc/core-linux-arm64-musl@1.4.17':
+  '@swc/core-linux-arm64-musl@1.6.13':
+    optional: true
+
+  '@swc/core-linux-arm64-musl@1.6.6':
     optional: true
 
   '@swc/core-linux-x64-gnu@1.3.56':
     optional: true
 
-  '@swc/core-linux-x64-gnu@1.4.17':
+  '@swc/core-linux-x64-gnu@1.6.13':
+    optional: true
+
+  '@swc/core-linux-x64-gnu@1.6.6':
     optional: true
 
   '@swc/core-linux-x64-musl@1.3.56':
     optional: true
 
-  '@swc/core-linux-x64-musl@1.4.17':
+  '@swc/core-linux-x64-musl@1.6.13':
+    optional: true
+
+  '@swc/core-linux-x64-musl@1.6.6':
     optional: true
 
   '@swc/core-win32-arm64-msvc@1.3.56':
     optional: true
 
-  '@swc/core-win32-arm64-msvc@1.4.17':
+  '@swc/core-win32-arm64-msvc@1.6.13':
+    optional: true
+
+  '@swc/core-win32-arm64-msvc@1.6.6':
     optional: true
 
   '@swc/core-win32-ia32-msvc@1.3.56':
     optional: true
 
-  '@swc/core-win32-ia32-msvc@1.4.17':
+  '@swc/core-win32-ia32-msvc@1.6.13':
+    optional: true
+
+  '@swc/core-win32-ia32-msvc@1.6.6':
     optional: true
 
   '@swc/core-win32-x64-msvc@1.3.56':
     optional: true
 
-  '@swc/core-win32-x64-msvc@1.4.17':
+  '@swc/core-win32-x64-msvc@1.6.13':
     optional: true
 
-  '@swc/core@1.4.17':
+  '@swc/core-win32-x64-msvc@1.6.6':
+    optional: true
+
+  '@swc/core@1.6.13':
     dependencies:
       '@swc/counter': 0.1.3
-      '@swc/types': 0.1.5
+      '@swc/types': 0.1.9
     optionalDependencies:
-      '@swc/core-darwin-arm64': 1.4.17
-      '@swc/core-darwin-x64': 1.4.17
-      '@swc/core-linux-arm-gnueabihf': 1.4.17
-      '@swc/core-linux-arm64-gnu': 1.4.17
-      '@swc/core-linux-arm64-musl': 1.4.17
-      '@swc/core-linux-x64-gnu': 1.4.17
-      '@swc/core-linux-x64-musl': 1.4.17
-      '@swc/core-win32-arm64-msvc': 1.4.17
-      '@swc/core-win32-ia32-msvc': 1.4.17
-      '@swc/core-win32-x64-msvc': 1.4.17
+      '@swc/core-darwin-arm64': 1.6.13
+      '@swc/core-darwin-x64': 1.6.13
+      '@swc/core-linux-arm-gnueabihf': 1.6.13
+      '@swc/core-linux-arm64-gnu': 1.6.13
+      '@swc/core-linux-arm64-musl': 1.6.13
+      '@swc/core-linux-x64-gnu': 1.6.13
+      '@swc/core-linux-x64-musl': 1.6.13
+      '@swc/core-win32-arm64-msvc': 1.6.13
+      '@swc/core-win32-ia32-msvc': 1.6.13
+      '@swc/core-win32-x64-msvc': 1.6.13
+
+  '@swc/core@1.6.6':
+    dependencies:
+      '@swc/counter': 0.1.3
+      '@swc/types': 0.1.9
+    optionalDependencies:
+      '@swc/core-darwin-arm64': 1.6.6
+      '@swc/core-darwin-x64': 1.6.6
+      '@swc/core-linux-arm-gnueabihf': 1.6.6
+      '@swc/core-linux-arm64-gnu': 1.6.6
+      '@swc/core-linux-arm64-musl': 1.6.6
+      '@swc/core-linux-x64-gnu': 1.6.6
+      '@swc/core-linux-x64-musl': 1.6.6
+      '@swc/core-win32-arm64-msvc': 1.6.6
+      '@swc/core-win32-ia32-msvc': 1.6.6
+      '@swc/core-win32-x64-msvc': 1.6.6
 
   '@swc/counter@0.1.3': {}
 
-  '@swc/jest@0.2.36(@swc/core@1.4.17)':
+  '@swc/jest@0.2.36(@swc/core@1.6.13)':
     dependencies:
       '@jest/create-cache-key-function': 29.7.0
-      '@swc/core': 1.4.17
+      '@swc/core': 1.6.13
       '@swc/counter': 0.1.3
       jsonc-parser: 3.2.0
 
-  '@swc/types@0.1.5': {}
+  '@swc/jest@0.2.36(@swc/core@1.6.6)':
+    dependencies:
+      '@jest/create-cache-key-function': 29.7.0
+      '@swc/core': 1.6.6
+      '@swc/counter': 0.1.3
+      jsonc-parser: 3.2.0
+
+  '@swc/types@0.1.9':
+    dependencies:
+      '@swc/counter': 0.1.3
 
   '@swc/wasm@1.2.130':
     optional: true
 
-  '@syuilo/aiscript@0.18.0':
+  '@syuilo/aiscript@0.19.0':
     dependencies:
       seedrandom: 3.0.5
       stringz: 2.1.0
@@ -15626,7 +15350,7 @@ snapshots:
   '@tensorflow/tfjs-backend-cpu@4.4.0(@tensorflow/tfjs-core@4.4.0(encoding@0.1.13))':
     dependencies:
       '@tensorflow/tfjs-core': 4.4.0(encoding@0.1.13)
-      '@types/seedrandom': 2.4.30
+      '@types/seedrandom': 2.4.34
       seedrandom: 3.0.5
 
   '@tensorflow/tfjs-backend-webgl@4.4.0(@tensorflow/tfjs-core@4.4.0(encoding@0.1.13))':
@@ -15634,7 +15358,7 @@ snapshots:
       '@tensorflow/tfjs-backend-cpu': 4.4.0(@tensorflow/tfjs-core@4.4.0(encoding@0.1.13))
       '@tensorflow/tfjs-core': 4.4.0(encoding@0.1.13)
       '@types/offscreencanvas': 2019.3.0
-      '@types/seedrandom': 2.4.30
+      '@types/seedrandom': 2.4.34
       '@types/webgl-ext': 0.0.30
       seedrandom: 3.0.5
 
@@ -15646,11 +15370,11 @@ snapshots:
     dependencies:
       '@types/long': 4.0.2
       '@types/offscreencanvas': 2019.7.0
-      '@types/seedrandom': 2.4.30
+      '@types/seedrandom': 2.4.34
       '@types/webgl-ext': 0.0.30
       '@webgpu/types': 0.1.30
       long: 4.0.0
-      node-fetch: 2.6.11(encoding@0.1.13)
+      node-fetch: 2.6.13(encoding@0.1.13)
       seedrandom: 3.0.5
     transitivePeerDependencies:
       - encoding
@@ -15658,8 +15382,8 @@ snapshots:
   '@tensorflow/tfjs-data@4.4.0(@tensorflow/tfjs-core@4.4.0(encoding@0.1.13))(encoding@0.1.13)(seedrandom@3.0.5)':
     dependencies:
       '@tensorflow/tfjs-core': 4.4.0(encoding@0.1.13)
-      '@types/node-fetch': 2.6.4
-      node-fetch: 2.6.11(encoding@0.1.13)
+      '@types/node-fetch': 2.6.11
+      node-fetch: 2.6.13(encoding@0.1.13)
       seedrandom: 3.0.5
       string_decoder: 1.3.0
     transitivePeerDependencies:
@@ -15702,12 +15426,12 @@ snapshots:
       - encoding
       - seedrandom
 
-  '@testing-library/dom@9.3.3':
+  '@testing-library/dom@10.4.0':
     dependencies:
-      '@babel/code-frame': 7.23.5
+      '@babel/code-frame': 7.24.7
       '@babel/runtime': 7.23.4
       '@types/aria-query': 5.0.1
-      aria-query: 5.1.3
+      aria-query: 5.3.0
       chalk: 4.1.2
       dom-accessibility-api: 0.5.16
       lz-string: 1.5.0
@@ -15715,7 +15439,7 @@ snapshots:
 
   '@testing-library/dom@9.3.4':
     dependencies:
-      '@babel/code-frame': 7.23.5
+      '@babel/code-frame': 7.24.7
       '@babel/runtime': 7.23.4
       '@types/aria-query': 5.0.1
       aria-query: 5.1.3
@@ -15724,34 +15448,28 @@ snapshots:
       lz-string: 1.5.0
       pretty-format: 27.5.1
 
-  '@testing-library/jest-dom@6.4.2(@jest/globals@29.7.0)(@types/jest@29.5.12)(jest@29.7.0(@types/node@20.12.7))(vitest@0.34.6(happy-dom@10.0.3)(jsdom@24.0.0(bufferutil@4.0.7)(utf-8-validate@6.0.3))(sass@1.76.0)(terser@5.30.3))':
+  '@testing-library/jest-dom@6.5.0':
     dependencies:
-      '@adobe/css-tools': 4.3.3
-      '@babel/runtime': 7.23.4
-      aria-query: 5.1.3
+      '@adobe/css-tools': 4.4.0
+      aria-query: 5.3.0
       chalk: 3.0.0
       css.escape: 1.5.1
       dom-accessibility-api: 0.6.3
       lodash: 4.17.21
       redent: 3.0.0
-    optionalDependencies:
-      '@jest/globals': 29.7.0
-      '@types/jest': 29.5.12
-      jest: 29.7.0(@types/node@20.12.7)
-      vitest: 0.34.6(happy-dom@10.0.3)(jsdom@24.0.0(bufferutil@4.0.7)(utf-8-validate@6.0.3))(sass@1.76.0)(terser@5.30.3)
 
-  '@testing-library/user-event@14.5.2(@testing-library/dom@9.3.4)':
+  '@testing-library/user-event@14.5.2(@testing-library/dom@10.4.0)':
     dependencies:
-      '@testing-library/dom': 9.3.4
+      '@testing-library/dom': 10.4.0
 
-  '@testing-library/vue@8.0.3(@vue/compiler-sfc@3.4.26)(@vue/server-renderer@3.4.26(vue@3.4.26(typescript@5.4.5)))(vue@3.4.26(typescript@5.4.5))':
+  '@testing-library/vue@8.1.0(@vue/compiler-sfc@3.5.11)(@vue/server-renderer@3.5.11(vue@3.5.11(typescript@5.6.2)))(vue@3.5.11(typescript@5.6.2))':
     dependencies:
       '@babel/runtime': 7.23.4
-      '@testing-library/dom': 9.3.3
-      '@vue/test-utils': 2.4.1(@vue/server-renderer@3.4.26(vue@3.4.26(typescript@5.4.5)))(vue@3.4.26(typescript@5.4.5))
-      vue: 3.4.26(typescript@5.4.5)
+      '@testing-library/dom': 9.3.4
+      '@vue/test-utils': 2.4.1(@vue/server-renderer@3.5.11(vue@3.5.11(typescript@5.6.2)))(vue@3.5.11(typescript@5.6.2))
+      vue: 3.5.11(typescript@5.6.2)
     optionalDependencies:
-      '@vue/compiler-sfc': 3.4.26
+      '@vue/compiler-sfc': 3.5.11
     transitivePeerDependencies:
       - '@vue/server-renderer'
 
@@ -15759,15 +15477,17 @@ snapshots:
 
   '@trysound/sax@0.2.0': {}
 
-  '@tsd/typescript@5.3.3': {}
+  '@tsd/typescript@5.4.5': {}
 
   '@twemoji/parser@15.0.0': {}
 
+  '@twemoji/parser@15.1.0': {}
+
   '@twemoji/parser@15.1.1': {}
 
   '@types/accepts@1.3.7':
     dependencies:
-      '@types/node': 20.12.7
+      '@types/node': 20.14.12
 
   '@types/archiver@6.0.2':
     dependencies:
@@ -15779,31 +15499,31 @@ snapshots:
 
   '@types/babel__core@7.20.0':
     dependencies:
-      '@babel/parser': 7.24.0
-      '@babel/types': 7.24.0
+      '@babel/parser': 7.24.7
+      '@babel/types': 7.24.7
       '@types/babel__generator': 7.6.4
       '@types/babel__template': 7.4.1
       '@types/babel__traverse': 7.20.0
 
   '@types/babel__generator@7.6.4':
     dependencies:
-      '@babel/types': 7.24.0
+      '@babel/types': 7.24.7
 
   '@types/babel__template@7.4.1':
     dependencies:
-      '@babel/parser': 7.24.0
-      '@babel/types': 7.24.0
+      '@babel/parser': 7.24.7
+      '@babel/types': 7.24.7
 
   '@types/babel__traverse@7.20.0':
     dependencies:
-      '@babel/types': 7.24.0
+      '@babel/types': 7.24.7
 
   '@types/bcryptjs@2.4.6': {}
 
   '@types/body-parser@1.19.5':
     dependencies:
       '@types/connect': 3.4.35
-      '@types/node': 20.12.7
+      '@types/node': 20.14.12
 
   '@types/braces@3.0.1': {}
 
@@ -15811,16 +15531,12 @@ snapshots:
     dependencies:
       '@types/http-cache-semantics': 4.0.4
       '@types/keyv': 3.1.4
-      '@types/node': 20.12.7
+      '@types/node': 20.14.12
       '@types/responselike': 1.0.0
 
-  '@types/chai-subset@1.3.5':
-    dependencies:
-      '@types/chai': 4.3.11
+  '@types/canvas-confetti@1.6.4': {}
 
-  '@types/chai@4.3.11': {}
-
-  '@types/color-convert@2.0.3':
+  '@types/color-convert@2.0.4':
     dependencies:
       '@types/color-name': 1.1.1
 
@@ -15828,59 +15544,38 @@ snapshots:
 
   '@types/connect@3.4.35':
     dependencies:
-      '@types/node': 20.12.7
+      '@types/node': 20.14.12
 
   '@types/connect@3.4.36':
     dependencies:
-      '@types/node': 20.12.7
+      '@types/node': 20.14.12
 
   '@types/content-disposition@0.5.8': {}
 
   '@types/cookie@0.6.0': {}
 
-  '@types/cookies@0.9.0':
-    dependencies:
-      '@types/connect': 3.4.35
-      '@types/express': 4.17.17
-      '@types/keygrip': 1.0.6
-      '@types/node': 20.12.7
-
-  '@types/cross-spawn@6.0.2':
-    dependencies:
-      '@types/node': 20.12.7
-
   '@types/debug@4.1.12':
     dependencies:
       '@types/ms': 0.7.34
 
-  '@types/detect-port@1.3.2': {}
-
   '@types/disposable-email-domains@1.0.2': {}
 
-  '@types/doctrine@0.0.3': {}
-
   '@types/doctrine@0.0.9': {}
 
-  '@types/ejs@3.1.2': {}
-
-  '@types/emscripten@1.39.7': {}
-
-  '@types/escape-regexp@0.0.3': {}
-
   '@types/escodegen@0.0.6': {}
 
   '@types/eslint@7.29.0':
     dependencies:
-      '@types/estree': 1.0.5
-      '@types/json-schema': 7.0.12
+      '@types/estree': 1.0.6
+      '@types/json-schema': 7.0.15
 
   '@types/estree@0.0.51': {}
 
-  '@types/estree@1.0.5': {}
+  '@types/estree@1.0.6': {}
 
   '@types/express-serve-static-core@4.17.33':
     dependencies:
-      '@types/node': 20.12.7
+      '@types/node': 20.14.12
       '@types/qs': 6.9.7
       '@types/range-parser': 1.2.4
 
@@ -15891,20 +15586,27 @@ snapshots:
       '@types/qs': 6.9.7
       '@types/serve-static': 1.15.1
 
+  '@types/express@4.17.21':
+    dependencies:
+      '@types/body-parser': 1.19.5
+      '@types/express-serve-static-core': 4.17.33
+      '@types/qs': 6.9.7
+      '@types/serve-static': 1.15.1
+
   '@types/find-cache-dir@3.2.1': {}
 
-  '@types/fluent-ffmpeg@2.1.24':
+  '@types/fluent-ffmpeg@2.1.26':
     dependencies:
-      '@types/node': 20.12.7
+      '@types/node': 20.14.12
 
   '@types/glob@7.2.0':
     dependencies:
       '@types/minimatch': 5.1.2
-      '@types/node': 20.12.7
+      '@types/node': 20.14.12
 
   '@types/graceful-fs@4.1.6':
     dependencies:
-      '@types/node': 20.12.7
+      '@types/node': 20.14.12
 
   '@types/hast@3.0.4':
     dependencies:
@@ -15912,15 +15614,11 @@ snapshots:
 
   '@types/htmlescape@1.1.3': {}
 
-  '@types/http-assert@1.5.5': {}
-
   '@types/http-cache-semantics@4.0.4': {}
 
-  '@types/http-errors@2.0.4': {}
-
-  '@types/http-link-header@1.0.5':
+  '@types/http-link-header@1.0.7':
     dependencies:
-      '@types/node': 20.12.7
+      '@types/node': 20.14.12
 
   '@types/istanbul-lib-coverage@2.0.4': {}
 
@@ -15932,16 +15630,16 @@ snapshots:
     dependencies:
       '@types/istanbul-lib-report': 3.0.0
 
-  '@types/jest@29.5.12':
+  '@types/jest@29.5.13':
     dependencies:
       expect: 29.7.0
       pretty-format: 29.7.0
 
   '@types/js-yaml@4.0.9': {}
 
-  '@types/jsdom@21.1.6':
+  '@types/jsdom@21.1.7':
     dependencies:
-      '@types/node': 20.12.7
+      '@types/node': 20.14.12
       '@types/tough-cookie': 4.0.2
       parse5: 7.1.2
 
@@ -15951,34 +15649,13 @@ snapshots:
 
   '@types/json5@0.0.29': {}
 
-  '@types/jsonld@1.5.13': {}
+  '@types/jsonld@1.5.15': {}
 
   '@types/jsrsasign@10.5.14': {}
 
-  '@types/keygrip@1.0.6': {}
-
   '@types/keyv@3.1.4':
     dependencies:
-      '@types/node': 20.12.7
-
-  '@types/koa-compose@3.2.8':
-    dependencies:
-      '@types/koa': 2.14.0
-
-  '@types/koa@2.14.0':
-    dependencies:
-      '@types/accepts': 1.3.7
-      '@types/content-disposition': 0.5.8
-      '@types/cookies': 0.9.0
-      '@types/http-assert': 1.5.5
-      '@types/http-errors': 2.0.4
-      '@types/keygrip': 1.0.6
-      '@types/koa-compose': 3.2.8
-      '@types/node': 20.12.7
-
-  '@types/koa__router@12.0.3':
-    dependencies:
-      '@types/koa': 2.14.0
+      '@types/node': 20.14.12
 
   '@types/lodash@4.14.191': {}
 
@@ -15986,13 +15663,15 @@ snapshots:
 
   '@types/matter-js@0.19.6': {}
 
+  '@types/matter-js@0.19.7': {}
+
   '@types/mdast@4.0.3':
     dependencies:
       '@types/unist': 3.0.2
 
   '@types/mdx@2.0.3': {}
 
-  '@types/micromatch@4.0.7':
+  '@types/micromatch@4.0.9':
     dependencies:
       '@types/braces': 3.0.1
 
@@ -16008,28 +15687,22 @@ snapshots:
 
   '@types/mute-stream@0.0.4':
     dependencies:
-      '@types/node': 20.12.7
+      '@types/node': 20.14.12
 
   '@types/mysql@2.15.22':
     dependencies:
-      '@types/node': 20.12.7
+      '@types/node': 20.14.12
 
-  '@types/node-fetch@2.6.4':
+  '@types/node-fetch@2.6.11':
     dependencies:
-      '@types/node': 20.12.7
-      form-data: 3.0.1
-
-  '@types/node-fetch@3.0.3':
-    dependencies:
-      node-fetch: 3.3.2
-
-  '@types/node@18.17.15': {}
+      '@types/node': 20.14.12
+      form-data: 4.0.0
 
   '@types/node@20.11.5':
     dependencies:
       undici-types: 5.26.5
 
-  '@types/node@20.12.7':
+  '@types/node@20.14.12':
     dependencies:
       undici-types: 5.26.5
 
@@ -16037,9 +15710,13 @@ snapshots:
     dependencies:
       undici-types: 5.26.5
 
-  '@types/nodemailer@6.4.15':
+  '@types/node@22.5.5':
     dependencies:
-      '@types/node': 20.12.7
+      undici-types: 6.19.8
+
+  '@types/nodemailer@6.4.16':
+    dependencies:
+      '@types/node': 20.14.12
 
   '@types/normalize-package-data@2.4.1': {}
 
@@ -16050,11 +15727,11 @@ snapshots:
   '@types/oauth2orize@1.11.5':
     dependencies:
       '@types/express': 4.17.17
-      '@types/node': 20.12.7
+      '@types/node': 20.14.12
 
-  '@types/oauth@0.9.4':
+  '@types/oauth@0.9.5':
     dependencies:
-      '@types/node': 20.12.7
+      '@types/node': 20.14.12
 
   '@types/offscreencanvas@2019.3.0': {}
 
@@ -16062,22 +15739,20 @@ snapshots:
 
   '@types/pg-pool@2.0.4':
     dependencies:
-      '@types/pg': 8.11.5
+      '@types/pg': 8.11.10
 
-  '@types/pg@8.11.5':
+  '@types/pg@8.11.10':
     dependencies:
-      '@types/node': 20.12.7
-      pg-protocol: 1.6.0
+      '@types/node': 20.14.12
+      pg-protocol: 1.6.1
       pg-types: 4.0.1
 
   '@types/pg@8.6.1':
     dependencies:
-      '@types/node': 20.12.7
+      '@types/node': 20.14.12
       pg-protocol: 1.6.1
       pg-types: 2.2.0
 
-  '@types/pretty-hrtime@1.0.1': {}
-
   '@types/prop-types@15.7.5': {}
 
   '@types/pug@2.0.10': {}
@@ -16086,7 +15761,7 @@ snapshots:
 
   '@types/qrcode@1.5.5':
     dependencies:
-      '@types/node': 20.12.7
+      '@types/node': 20.14.12
 
   '@types/qs@6.9.7': {}
 
@@ -16104,7 +15779,7 @@ snapshots:
 
   '@types/readdir-glob@1.1.1':
     dependencies:
-      '@types/node': 20.12.7
+      '@types/node': 20.14.12
 
   '@types/rename@1.0.7': {}
 
@@ -16112,15 +15787,15 @@ snapshots:
 
   '@types/responselike@1.0.0':
     dependencies:
-      '@types/node': 20.12.7
+      '@types/node': 20.14.12
 
-  '@types/sanitize-html@2.11.0':
+  '@types/sanitize-html@2.13.0':
     dependencies:
       htmlparser2: 8.0.1
 
   '@types/scheduler@0.16.2': {}
 
-  '@types/seedrandom@2.4.30': {}
+  '@types/seedrandom@2.4.34': {}
 
   '@types/seedrandom@3.0.8': {}
 
@@ -16129,7 +15804,7 @@ snapshots:
   '@types/serve-static@1.15.1':
     dependencies:
       '@types/mime': 3.0.1
-      '@types/node': 20.12.7
+      '@types/node': 20.14.12
 
   '@types/serviceworker@0.0.67': {}
 
@@ -16159,25 +15834,29 @@ snapshots:
 
   '@types/tough-cookie@4.0.2': {}
 
+  '@types/tough-cookie@4.0.5': {}
+
   '@types/unist@3.0.2': {}
 
+  '@types/uuid@10.0.0': {}
+
   '@types/uuid@9.0.8': {}
 
   '@types/vary@1.1.3':
     dependencies:
-      '@types/node': 20.12.7
+      '@types/node': 20.14.12
 
   '@types/web-push@3.6.3':
     dependencies:
-      '@types/node': 20.12.7
+      '@types/node': 20.14.12
 
   '@types/webgl-ext@0.0.30': {}
 
   '@types/wrap-ansi@3.0.0': {}
 
-  '@types/ws@8.5.10':
+  '@types/ws@8.5.12':
     dependencies:
-      '@types/node': 20.12.7
+      '@types/node': 20.14.12
 
   '@types/yargs-parser@21.0.0': {}
 
@@ -16187,39 +15866,19 @@ snapshots:
 
   '@types/yauzl@2.10.0':
     dependencies:
-      '@types/node': 20.12.7
+      '@types/node': 20.14.12
     optional: true
 
-  '@typescript-eslint/eslint-plugin@6.11.0(@typescript-eslint/parser@6.11.0(eslint@8.53.0)(typescript@5.3.3))(eslint@8.53.0)(typescript@5.3.3)':
+  '@typescript-eslint/eslint-plugin@7.1.0(@typescript-eslint/parser@7.1.0(eslint@9.11.0)(typescript@5.3.3))(eslint@9.11.0)(typescript@5.3.3)':
     dependencies:
       '@eslint-community/regexpp': 4.6.2
-      '@typescript-eslint/parser': 6.11.0(eslint@8.53.0)(typescript@5.3.3)
-      '@typescript-eslint/scope-manager': 6.11.0
-      '@typescript-eslint/type-utils': 6.11.0(eslint@8.53.0)(typescript@5.3.3)
-      '@typescript-eslint/utils': 6.11.0(eslint@8.53.0)(typescript@5.3.3)
-      '@typescript-eslint/visitor-keys': 6.11.0
-      debug: 4.3.4(supports-color@8.1.1)
-      eslint: 8.53.0
-      graphemer: 1.4.0
-      ignore: 5.2.4
-      natural-compare: 1.4.0
-      semver: 7.5.4
-      ts-api-utils: 1.0.1(typescript@5.3.3)
-    optionalDependencies:
-      typescript: 5.3.3
-    transitivePeerDependencies:
-      - supports-color
-
-  '@typescript-eslint/eslint-plugin@7.1.0(@typescript-eslint/parser@7.1.0(eslint@8.57.0)(typescript@5.3.3))(eslint@8.57.0)(typescript@5.3.3)':
-    dependencies:
-      '@eslint-community/regexpp': 4.6.2
-      '@typescript-eslint/parser': 7.1.0(eslint@8.57.0)(typescript@5.3.3)
+      '@typescript-eslint/parser': 7.1.0(eslint@9.11.0)(typescript@5.3.3)
       '@typescript-eslint/scope-manager': 7.1.0
-      '@typescript-eslint/type-utils': 7.1.0(eslint@8.57.0)(typescript@5.3.3)
-      '@typescript-eslint/utils': 7.1.0(eslint@8.57.0)(typescript@5.3.3)
+      '@typescript-eslint/type-utils': 7.1.0(eslint@9.11.0)(typescript@5.3.3)
+      '@typescript-eslint/utils': 7.1.0(eslint@9.11.0)(typescript@5.3.3)
       '@typescript-eslint/visitor-keys': 7.1.0
-      debug: 4.3.4(supports-color@8.1.1)
-      eslint: 8.57.0
+      debug: 4.3.4(supports-color@5.5.0)
+      eslint: 9.11.0
       graphemer: 1.4.0
       ignore: 5.2.4
       natural-compare: 1.4.0
@@ -16230,141 +15889,179 @@ snapshots:
     transitivePeerDependencies:
       - supports-color
 
-  '@typescript-eslint/eslint-plugin@7.7.1(@typescript-eslint/parser@7.7.1(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0)(typescript@5.4.5)':
+  '@typescript-eslint/eslint-plugin@7.17.0(@typescript-eslint/parser@7.17.0(eslint@9.11.0)(typescript@5.5.4))(eslint@9.11.0)(typescript@5.5.4)':
     dependencies:
-      '@eslint-community/regexpp': 4.10.0
-      '@typescript-eslint/parser': 7.7.1(eslint@8.57.0)(typescript@5.4.5)
-      '@typescript-eslint/scope-manager': 7.7.1
-      '@typescript-eslint/type-utils': 7.7.1(eslint@8.57.0)(typescript@5.4.5)
-      '@typescript-eslint/utils': 7.7.1(eslint@8.57.0)(typescript@5.4.5)
-      '@typescript-eslint/visitor-keys': 7.7.1
-      debug: 4.3.4(supports-color@8.1.1)
-      eslint: 8.57.0
+      '@eslint-community/regexpp': 4.11.0
+      '@typescript-eslint/parser': 7.17.0(eslint@9.11.0)(typescript@5.5.4)
+      '@typescript-eslint/scope-manager': 7.17.0
+      '@typescript-eslint/type-utils': 7.17.0(eslint@9.11.0)(typescript@5.5.4)
+      '@typescript-eslint/utils': 7.17.0(eslint@9.11.0)(typescript@5.5.4)
+      '@typescript-eslint/visitor-keys': 7.17.0
+      eslint: 9.11.0
       graphemer: 1.4.0
       ignore: 5.3.1
       natural-compare: 1.4.0
-      semver: 7.6.0
-      ts-api-utils: 1.3.0(typescript@5.4.5)
+      ts-api-utils: 1.3.0(typescript@5.5.4)
     optionalDependencies:
-      typescript: 5.4.5
+      typescript: 5.5.4
     transitivePeerDependencies:
       - supports-color
 
-  '@typescript-eslint/parser@6.11.0(eslint@8.53.0)(typescript@5.3.3)':
+  '@typescript-eslint/eslint-plugin@7.17.0(@typescript-eslint/parser@7.17.0(eslint@9.11.0)(typescript@5.6.2))(eslint@9.11.0)(typescript@5.6.2)':
     dependencies:
-      '@typescript-eslint/scope-manager': 6.11.0
-      '@typescript-eslint/types': 6.11.0
-      '@typescript-eslint/typescript-estree': 6.11.0(typescript@5.3.3)
-      '@typescript-eslint/visitor-keys': 6.11.0
-      debug: 4.3.4(supports-color@8.1.1)
-      eslint: 8.53.0
+      '@eslint-community/regexpp': 4.11.0
+      '@typescript-eslint/parser': 7.17.0(eslint@9.11.0)(typescript@5.6.2)
+      '@typescript-eslint/scope-manager': 7.17.0
+      '@typescript-eslint/type-utils': 7.17.0(eslint@9.11.0)(typescript@5.6.2)
+      '@typescript-eslint/utils': 7.17.0(eslint@9.11.0)(typescript@5.6.2)
+      '@typescript-eslint/visitor-keys': 7.17.0
+      eslint: 9.11.0
+      graphemer: 1.4.0
+      ignore: 5.3.1
+      natural-compare: 1.4.0
+      ts-api-utils: 1.3.0(typescript@5.6.2)
     optionalDependencies:
-      typescript: 5.3.3
+      typescript: 5.6.2
     transitivePeerDependencies:
       - supports-color
 
-  '@typescript-eslint/parser@7.1.0(eslint@8.57.0)(typescript@5.3.3)':
+  '@typescript-eslint/eslint-plugin@7.17.0(@typescript-eslint/parser@7.17.0(eslint@9.8.0)(typescript@5.6.2))(eslint@9.8.0)(typescript@5.6.2)':
+    dependencies:
+      '@eslint-community/regexpp': 4.11.0
+      '@typescript-eslint/parser': 7.17.0(eslint@9.8.0)(typescript@5.6.2)
+      '@typescript-eslint/scope-manager': 7.17.0
+      '@typescript-eslint/type-utils': 7.17.0(eslint@9.8.0)(typescript@5.6.2)
+      '@typescript-eslint/utils': 7.17.0(eslint@9.8.0)(typescript@5.6.2)
+      '@typescript-eslint/visitor-keys': 7.17.0
+      eslint: 9.8.0
+      graphemer: 1.4.0
+      ignore: 5.3.1
+      natural-compare: 1.4.0
+      ts-api-utils: 1.3.0(typescript@5.6.2)
+    optionalDependencies:
+      typescript: 5.6.2
+    transitivePeerDependencies:
+      - supports-color
+
+  '@typescript-eslint/parser@7.1.0(eslint@9.11.0)(typescript@5.3.3)':
     dependencies:
       '@typescript-eslint/scope-manager': 7.1.0
       '@typescript-eslint/types': 7.1.0
       '@typescript-eslint/typescript-estree': 7.1.0(typescript@5.3.3)
       '@typescript-eslint/visitor-keys': 7.1.0
-      debug: 4.3.4(supports-color@8.1.1)
-      eslint: 8.57.0
+      debug: 4.3.4(supports-color@5.5.0)
+      eslint: 9.11.0
     optionalDependencies:
       typescript: 5.3.3
     transitivePeerDependencies:
       - supports-color
 
-  '@typescript-eslint/parser@7.7.1(eslint@8.57.0)(typescript@5.4.5)':
+  '@typescript-eslint/parser@7.17.0(eslint@9.11.0)(typescript@5.5.4)':
     dependencies:
-      '@typescript-eslint/scope-manager': 7.7.1
-      '@typescript-eslint/types': 7.7.1
-      '@typescript-eslint/typescript-estree': 7.7.1(typescript@5.4.5)
-      '@typescript-eslint/visitor-keys': 7.7.1
-      debug: 4.3.4(supports-color@8.1.1)
-      eslint: 8.57.0
+      '@typescript-eslint/scope-manager': 7.17.0
+      '@typescript-eslint/types': 7.17.0
+      '@typescript-eslint/typescript-estree': 7.17.0(typescript@5.5.4)
+      '@typescript-eslint/visitor-keys': 7.17.0
+      debug: 4.3.5(supports-color@5.5.0)
+      eslint: 9.11.0
     optionalDependencies:
-      typescript: 5.4.5
+      typescript: 5.5.4
     transitivePeerDependencies:
       - supports-color
 
-  '@typescript-eslint/scope-manager@6.11.0':
+  '@typescript-eslint/parser@7.17.0(eslint@9.11.0)(typescript@5.6.2)':
     dependencies:
-      '@typescript-eslint/types': 6.11.0
-      '@typescript-eslint/visitor-keys': 6.11.0
+      '@typescript-eslint/scope-manager': 7.17.0
+      '@typescript-eslint/types': 7.17.0
+      '@typescript-eslint/typescript-estree': 7.17.0(typescript@5.6.2)
+      '@typescript-eslint/visitor-keys': 7.17.0
+      debug: 4.3.5(supports-color@5.5.0)
+      eslint: 9.11.0
+    optionalDependencies:
+      typescript: 5.6.2
+    transitivePeerDependencies:
+      - supports-color
+
+  '@typescript-eslint/parser@7.17.0(eslint@9.8.0)(typescript@5.6.2)':
+    dependencies:
+      '@typescript-eslint/scope-manager': 7.17.0
+      '@typescript-eslint/types': 7.17.0
+      '@typescript-eslint/typescript-estree': 7.17.0(typescript@5.6.2)
+      '@typescript-eslint/visitor-keys': 7.17.0
+      debug: 4.3.5(supports-color@5.5.0)
+      eslint: 9.8.0
+    optionalDependencies:
+      typescript: 5.6.2
+    transitivePeerDependencies:
+      - supports-color
 
   '@typescript-eslint/scope-manager@7.1.0':
     dependencies:
       '@typescript-eslint/types': 7.1.0
       '@typescript-eslint/visitor-keys': 7.1.0
 
-  '@typescript-eslint/scope-manager@7.7.1':
+  '@typescript-eslint/scope-manager@7.17.0':
     dependencies:
-      '@typescript-eslint/types': 7.7.1
-      '@typescript-eslint/visitor-keys': 7.7.1
+      '@typescript-eslint/types': 7.17.0
+      '@typescript-eslint/visitor-keys': 7.17.0
 
-  '@typescript-eslint/type-utils@6.11.0(eslint@8.53.0)(typescript@5.3.3)':
-    dependencies:
-      '@typescript-eslint/typescript-estree': 6.11.0(typescript@5.3.3)
-      '@typescript-eslint/utils': 6.11.0(eslint@8.53.0)(typescript@5.3.3)
-      debug: 4.3.4(supports-color@8.1.1)
-      eslint: 8.53.0
-      ts-api-utils: 1.0.1(typescript@5.3.3)
-    optionalDependencies:
-      typescript: 5.3.3
-    transitivePeerDependencies:
-      - supports-color
-
-  '@typescript-eslint/type-utils@7.1.0(eslint@8.57.0)(typescript@5.3.3)':
+  '@typescript-eslint/type-utils@7.1.0(eslint@9.11.0)(typescript@5.3.3)':
     dependencies:
       '@typescript-eslint/typescript-estree': 7.1.0(typescript@5.3.3)
-      '@typescript-eslint/utils': 7.1.0(eslint@8.57.0)(typescript@5.3.3)
-      debug: 4.3.4(supports-color@8.1.1)
-      eslint: 8.57.0
+      '@typescript-eslint/utils': 7.1.0(eslint@9.11.0)(typescript@5.3.3)
+      debug: 4.3.5(supports-color@5.5.0)
+      eslint: 9.11.0
       ts-api-utils: 1.0.1(typescript@5.3.3)
     optionalDependencies:
       typescript: 5.3.3
     transitivePeerDependencies:
       - supports-color
 
-  '@typescript-eslint/type-utils@7.7.1(eslint@8.57.0)(typescript@5.4.5)':
+  '@typescript-eslint/type-utils@7.17.0(eslint@9.11.0)(typescript@5.5.4)':
     dependencies:
-      '@typescript-eslint/typescript-estree': 7.7.1(typescript@5.4.5)
-      '@typescript-eslint/utils': 7.7.1(eslint@8.57.0)(typescript@5.4.5)
-      debug: 4.3.4(supports-color@8.1.1)
-      eslint: 8.57.0
-      ts-api-utils: 1.3.0(typescript@5.4.5)
+      '@typescript-eslint/typescript-estree': 7.17.0(typescript@5.5.4)
+      '@typescript-eslint/utils': 7.17.0(eslint@9.11.0)(typescript@5.5.4)
+      debug: 4.3.5(supports-color@5.5.0)
+      eslint: 9.11.0
+      ts-api-utils: 1.3.0(typescript@5.5.4)
     optionalDependencies:
-      typescript: 5.4.5
+      typescript: 5.5.4
     transitivePeerDependencies:
       - supports-color
 
-  '@typescript-eslint/types@6.11.0': {}
+  '@typescript-eslint/type-utils@7.17.0(eslint@9.11.0)(typescript@5.6.2)':
+    dependencies:
+      '@typescript-eslint/typescript-estree': 7.17.0(typescript@5.6.2)
+      '@typescript-eslint/utils': 7.17.0(eslint@9.11.0)(typescript@5.6.2)
+      debug: 4.3.5(supports-color@5.5.0)
+      eslint: 9.11.0
+      ts-api-utils: 1.3.0(typescript@5.6.2)
+    optionalDependencies:
+      typescript: 5.6.2
+    transitivePeerDependencies:
+      - supports-color
+
+  '@typescript-eslint/type-utils@7.17.0(eslint@9.8.0)(typescript@5.6.2)':
+    dependencies:
+      '@typescript-eslint/typescript-estree': 7.17.0(typescript@5.6.2)
+      '@typescript-eslint/utils': 7.17.0(eslint@9.8.0)(typescript@5.6.2)
+      debug: 4.3.5(supports-color@5.5.0)
+      eslint: 9.8.0
+      ts-api-utils: 1.3.0(typescript@5.6.2)
+    optionalDependencies:
+      typescript: 5.6.2
+    transitivePeerDependencies:
+      - supports-color
 
   '@typescript-eslint/types@7.1.0': {}
 
-  '@typescript-eslint/types@7.7.1': {}
-
-  '@typescript-eslint/typescript-estree@6.11.0(typescript@5.3.3)':
-    dependencies:
-      '@typescript-eslint/types': 6.11.0
-      '@typescript-eslint/visitor-keys': 6.11.0
-      debug: 4.3.4(supports-color@8.1.1)
-      globby: 11.1.0
-      is-glob: 4.0.3
-      semver: 7.5.4
-      ts-api-utils: 1.0.1(typescript@5.3.3)
-    optionalDependencies:
-      typescript: 5.3.3
-    transitivePeerDependencies:
-      - supports-color
+  '@typescript-eslint/types@7.17.0': {}
 
   '@typescript-eslint/typescript-estree@7.1.0(typescript@5.3.3)':
     dependencies:
       '@typescript-eslint/types': 7.1.0
       '@typescript-eslint/visitor-keys': 7.1.0
-      debug: 4.3.4(supports-color@8.1.1)
+      debug: 4.3.5(supports-color@5.5.0)
       globby: 11.1.0
       is-glob: 4.0.3
       minimatch: 9.0.3
@@ -16375,150 +16072,183 @@ snapshots:
     transitivePeerDependencies:
       - supports-color
 
-  '@typescript-eslint/typescript-estree@7.7.1(typescript@5.4.5)':
+  '@typescript-eslint/typescript-estree@7.17.0(typescript@5.5.4)':
     dependencies:
-      '@typescript-eslint/types': 7.7.1
-      '@typescript-eslint/visitor-keys': 7.7.1
-      debug: 4.3.4(supports-color@8.1.1)
+      '@typescript-eslint/types': 7.17.0
+      '@typescript-eslint/visitor-keys': 7.17.0
+      debug: 4.3.5(supports-color@5.5.0)
       globby: 11.1.0
       is-glob: 4.0.3
       minimatch: 9.0.4
       semver: 7.6.0
-      ts-api-utils: 1.3.0(typescript@5.4.5)
+      ts-api-utils: 1.3.0(typescript@5.5.4)
     optionalDependencies:
-      typescript: 5.4.5
+      typescript: 5.5.4
     transitivePeerDependencies:
       - supports-color
 
-  '@typescript-eslint/utils@6.11.0(eslint@8.53.0)(typescript@5.3.3)':
+  '@typescript-eslint/typescript-estree@7.17.0(typescript@5.6.2)':
     dependencies:
-      '@eslint-community/eslint-utils': 4.4.0(eslint@8.53.0)
-      '@types/json-schema': 7.0.12
-      '@types/semver': 7.5.8
-      '@typescript-eslint/scope-manager': 6.11.0
-      '@typescript-eslint/types': 6.11.0
-      '@typescript-eslint/typescript-estree': 6.11.0(typescript@5.3.3)
-      eslint: 8.53.0
-      semver: 7.5.4
+      '@typescript-eslint/types': 7.17.0
+      '@typescript-eslint/visitor-keys': 7.17.0
+      debug: 4.3.5(supports-color@5.5.0)
+      globby: 11.1.0
+      is-glob: 4.0.3
+      minimatch: 9.0.4
+      semver: 7.6.0
+      ts-api-utils: 1.3.0(typescript@5.6.2)
+    optionalDependencies:
+      typescript: 5.6.2
     transitivePeerDependencies:
       - supports-color
-      - typescript
 
-  '@typescript-eslint/utils@7.1.0(eslint@8.57.0)(typescript@5.3.3)':
+  '@typescript-eslint/utils@7.1.0(eslint@9.11.0)(typescript@5.3.3)':
     dependencies:
-      '@eslint-community/eslint-utils': 4.4.0(eslint@8.57.0)
+      '@eslint-community/eslint-utils': 4.4.0(eslint@9.11.0)
       '@types/json-schema': 7.0.12
       '@types/semver': 7.5.8
       '@typescript-eslint/scope-manager': 7.1.0
       '@typescript-eslint/types': 7.1.0
       '@typescript-eslint/typescript-estree': 7.1.0(typescript@5.3.3)
-      eslint: 8.57.0
+      eslint: 9.11.0
       semver: 7.6.0
     transitivePeerDependencies:
       - supports-color
       - typescript
 
-  '@typescript-eslint/utils@7.7.1(eslint@8.57.0)(typescript@5.4.5)':
+  '@typescript-eslint/utils@7.17.0(eslint@9.11.0)(typescript@5.5.4)':
     dependencies:
-      '@eslint-community/eslint-utils': 4.4.0(eslint@8.57.0)
-      '@types/json-schema': 7.0.15
-      '@types/semver': 7.5.8
-      '@typescript-eslint/scope-manager': 7.7.1
-      '@typescript-eslint/types': 7.7.1
-      '@typescript-eslint/typescript-estree': 7.7.1(typescript@5.4.5)
-      eslint: 8.57.0
-      semver: 7.6.0
+      '@eslint-community/eslint-utils': 4.4.0(eslint@9.11.0)
+      '@typescript-eslint/scope-manager': 7.17.0
+      '@typescript-eslint/types': 7.17.0
+      '@typescript-eslint/typescript-estree': 7.17.0(typescript@5.5.4)
+      eslint: 9.11.0
     transitivePeerDependencies:
       - supports-color
       - typescript
 
-  '@typescript-eslint/visitor-keys@6.11.0':
+  '@typescript-eslint/utils@7.17.0(eslint@9.11.0)(typescript@5.6.2)':
     dependencies:
-      '@typescript-eslint/types': 6.11.0
-      eslint-visitor-keys: 3.4.3
+      '@eslint-community/eslint-utils': 4.4.0(eslint@9.11.0)
+      '@typescript-eslint/scope-manager': 7.17.0
+      '@typescript-eslint/types': 7.17.0
+      '@typescript-eslint/typescript-estree': 7.17.0(typescript@5.6.2)
+      eslint: 9.11.0
+    transitivePeerDependencies:
+      - supports-color
+      - typescript
+
+  '@typescript-eslint/utils@7.17.0(eslint@9.8.0)(typescript@5.6.2)':
+    dependencies:
+      '@eslint-community/eslint-utils': 4.4.0(eslint@9.8.0)
+      '@typescript-eslint/scope-manager': 7.17.0
+      '@typescript-eslint/types': 7.17.0
+      '@typescript-eslint/typescript-estree': 7.17.0(typescript@5.6.2)
+      eslint: 9.8.0
+    transitivePeerDependencies:
+      - supports-color
+      - typescript
 
   '@typescript-eslint/visitor-keys@7.1.0':
     dependencies:
       '@typescript-eslint/types': 7.1.0
       eslint-visitor-keys: 3.4.3
 
-  '@typescript-eslint/visitor-keys@7.7.1':
+  '@typescript-eslint/visitor-keys@7.17.0':
     dependencies:
-      '@typescript-eslint/types': 7.7.1
+      '@typescript-eslint/types': 7.17.0
       eslint-visitor-keys: 3.4.3
 
   '@ungap/structured-clone@1.2.0': {}
 
-  '@vitejs/plugin-vue@5.0.4(vite@5.2.11(@types/node@20.12.7)(sass@1.76.0)(terser@5.30.3))(vue@3.4.26(typescript@5.4.5))':
+  '@vitejs/plugin-vue@5.1.4(vite@5.4.8(@types/node@20.14.12)(sass@1.79.3)(terser@5.33.0))(vue@3.5.11(typescript@5.6.2))':
     dependencies:
-      vite: 5.2.11(@types/node@20.12.7)(sass@1.76.0)(terser@5.30.3)
-      vue: 3.4.26(typescript@5.4.5)
+      vite: 5.4.8(@types/node@20.14.12)(sass@1.79.3)(terser@5.33.0)
+      vue: 3.5.11(typescript@5.6.2)
 
-  '@vitest/coverage-v8@0.34.6(vitest@0.34.6(happy-dom@10.0.3)(jsdom@24.0.0(bufferutil@4.0.7)(utf-8-validate@6.0.3))(sass@1.76.0)(terser@5.30.3))':
+  '@vitejs/plugin-vue@5.1.4(vite@5.4.8(@types/node@20.14.12)(sass@1.79.4)(terser@5.33.0))(vue@3.5.11(typescript@5.6.2))':
+    dependencies:
+      vite: 5.4.8(@types/node@20.14.12)(sass@1.79.4)(terser@5.33.0)
+      vue: 3.5.11(typescript@5.6.2)
+
+  '@vitest/coverage-v8@1.6.0(vitest@1.6.0(@types/node@20.14.12)(happy-dom@10.0.3)(jsdom@24.1.1(bufferutil@4.0.8)(utf-8-validate@6.0.4))(sass@1.79.3)(terser@5.33.0))':
     dependencies:
       '@ampproject/remapping': 2.2.1
       '@bcoe/v8-coverage': 0.2.3
+      debug: 4.3.5(supports-color@5.5.0)
       istanbul-lib-coverage: 3.2.2
       istanbul-lib-report: 3.0.1
-      istanbul-lib-source-maps: 4.0.1
+      istanbul-lib-source-maps: 5.0.4
       istanbul-reports: 3.1.6
-      magic-string: 0.30.7
-      picocolors: 1.0.0
+      magic-string: 0.30.10
+      magicast: 0.3.4
+      picocolors: 1.0.1
       std-env: 3.7.0
+      strip-literal: 2.1.0
       test-exclude: 6.0.0
-      v8-to-istanbul: 9.2.0
-      vitest: 0.34.6(happy-dom@10.0.3)(jsdom@24.0.0(bufferutil@4.0.7)(utf-8-validate@6.0.3))(sass@1.76.0)(terser@5.30.3)
+      vitest: 1.6.0(@types/node@20.14.12)(happy-dom@10.0.3)(jsdom@24.1.1(bufferutil@4.0.8)(utf-8-validate@6.0.4))(sass@1.79.3)(terser@5.33.0)
     transitivePeerDependencies:
       - supports-color
 
-  '@vitest/expect@0.34.6':
+  '@vitest/coverage-v8@1.6.0(vitest@1.6.0(@types/node@20.14.12)(happy-dom@10.0.3)(jsdom@24.1.1(bufferutil@4.0.8)(utf-8-validate@6.0.4))(sass@1.79.4)(terser@5.33.0))':
     dependencies:
-      '@vitest/spy': 0.34.6
-      '@vitest/utils': 0.34.6
+      '@ampproject/remapping': 2.2.1
+      '@bcoe/v8-coverage': 0.2.3
+      debug: 4.3.5(supports-color@5.5.0)
+      istanbul-lib-coverage: 3.2.2
+      istanbul-lib-report: 3.0.1
+      istanbul-lib-source-maps: 5.0.4
+      istanbul-reports: 3.1.6
+      magic-string: 0.30.10
+      magicast: 0.3.4
+      picocolors: 1.0.1
+      std-env: 3.7.0
+      strip-literal: 2.1.0
+      test-exclude: 6.0.0
+      vitest: 1.6.0(@types/node@20.14.12)(happy-dom@10.0.3)(jsdom@24.1.1(bufferutil@4.0.8)(utf-8-validate@6.0.4))(sass@1.79.4)(terser@5.33.0)
+    transitivePeerDependencies:
+      - supports-color
+
+  '@vitest/expect@1.6.0':
+    dependencies:
+      '@vitest/spy': 1.6.0
+      '@vitest/utils': 1.6.0
       chai: 4.3.10
 
-  '@vitest/expect@1.3.1':
+  '@vitest/expect@2.0.5':
     dependencies:
-      '@vitest/spy': 1.3.1
-      '@vitest/utils': 1.3.1
-      chai: 4.3.10
+      '@vitest/spy': 2.0.5
+      '@vitest/utils': 2.0.5
+      chai: 5.1.1
+      tinyrainbow: 1.2.0
 
-  '@vitest/runner@0.34.6':
+  '@vitest/pretty-format@2.0.5':
     dependencies:
-      '@vitest/utils': 0.34.6
-      p-limit: 4.0.0
+      tinyrainbow: 1.2.0
+
+  '@vitest/pretty-format@2.1.1':
+    dependencies:
+      tinyrainbow: 1.2.0
+
+  '@vitest/runner@1.6.0':
+    dependencies:
+      '@vitest/utils': 1.6.0
+      p-limit: 5.0.0
       pathe: 1.1.2
 
-  '@vitest/snapshot@0.34.6':
+  '@vitest/snapshot@1.6.0':
     dependencies:
-      magic-string: 0.30.7
+      magic-string: 0.30.11
       pathe: 1.1.2
       pretty-format: 29.7.0
 
-  '@vitest/spy@0.34.6':
-    dependencies:
-      tinyspy: 2.2.0
-
-  '@vitest/spy@1.3.1':
-    dependencies:
-      tinyspy: 2.2.0
-
   '@vitest/spy@1.6.0':
     dependencies:
       tinyspy: 2.2.0
 
-  '@vitest/utils@0.34.6':
+  '@vitest/spy@2.0.5':
     dependencies:
-      diff-sequences: 29.6.3
-      loupe: 2.3.7
-      pretty-format: 29.7.0
-
-  '@vitest/utils@1.3.1':
-    dependencies:
-      diff-sequences: 29.6.3
-      estree-walker: 3.0.3
-      loupe: 2.3.7
-      pretty-format: 29.7.0
+      tinyspy: 3.0.2
 
   '@vitest/utils@1.6.0':
     dependencies:
@@ -16527,141 +16257,207 @@ snapshots:
       loupe: 2.3.7
       pretty-format: 29.7.0
 
+  '@vitest/utils@2.0.5':
+    dependencies:
+      '@vitest/pretty-format': 2.0.5
+      estree-walker: 3.0.3
+      loupe: 3.1.1
+      tinyrainbow: 1.2.0
+
+  '@vitest/utils@2.1.1':
+    dependencies:
+      '@vitest/pretty-format': 2.1.1
+      loupe: 3.1.1
+      tinyrainbow: 1.2.0
+
   '@volar/language-core@2.2.0':
     dependencies:
       '@volar/source-map': 2.2.0
 
+  '@volar/language-core@2.4.5':
+    dependencies:
+      '@volar/source-map': 2.4.5
+
   '@volar/source-map@2.2.0':
     dependencies:
       muggle-string: 0.4.1
 
+  '@volar/source-map@2.4.5': {}
+
   '@volar/typescript@2.2.0':
     dependencies:
       '@volar/language-core': 2.2.0
       path-browserify: 1.0.1
 
-  '@vue/compiler-core@3.4.21':
+  '@volar/typescript@2.4.5':
     dependencies:
-      '@babel/parser': 7.24.0
-      '@vue/shared': 3.4.21
+      '@volar/language-core': 2.4.5
+      path-browserify: 1.0.1
+      vscode-uri: 3.0.8
+
+  '@vue/compiler-core@3.4.37':
+    dependencies:
+      '@babel/parser': 7.25.6
+      '@vue/shared': 3.4.37
+      entities: 5.0.0
+      estree-walker: 2.0.2
+      source-map-js: 1.2.1
+
+  '@vue/compiler-core@3.5.10':
+    dependencies:
+      '@babel/parser': 7.25.6
+      '@vue/shared': 3.5.10
       entities: 4.5.0
       estree-walker: 2.0.2
-      source-map-js: 1.0.2
+      source-map-js: 1.2.1
 
-  '@vue/compiler-core@3.4.25':
+  '@vue/compiler-core@3.5.11':
     dependencies:
-      '@babel/parser': 7.24.5
-      '@vue/shared': 3.4.25
+      '@babel/parser': 7.25.6
+      '@vue/shared': 3.5.11
       entities: 4.5.0
       estree-walker: 2.0.2
-      source-map-js: 1.2.0
+      source-map-js: 1.2.1
 
-  '@vue/compiler-core@3.4.26':
+  '@vue/compiler-dom@3.4.37':
     dependencies:
-      '@babel/parser': 7.24.5
-      '@vue/shared': 3.4.26
-      entities: 4.5.0
+      '@vue/compiler-core': 3.4.37
+      '@vue/shared': 3.4.37
+
+  '@vue/compiler-dom@3.5.10':
+    dependencies:
+      '@vue/compiler-core': 3.5.10
+      '@vue/shared': 3.5.10
+
+  '@vue/compiler-dom@3.5.11':
+    dependencies:
+      '@vue/compiler-core': 3.5.11
+      '@vue/shared': 3.5.11
+
+  '@vue/compiler-sfc@3.4.37':
+    dependencies:
+      '@babel/parser': 7.24.7
+      '@vue/compiler-core': 3.4.37
+      '@vue/compiler-dom': 3.4.37
+      '@vue/compiler-ssr': 3.4.37
+      '@vue/shared': 3.4.37
       estree-walker: 2.0.2
+      magic-string: 0.30.11
+      postcss: 8.4.47
       source-map-js: 1.2.0
 
-  '@vue/compiler-dom@3.4.21':
+  '@vue/compiler-sfc@3.5.11':
     dependencies:
-      '@vue/compiler-core': 3.4.21
-      '@vue/shared': 3.4.21
-
-  '@vue/compiler-dom@3.4.25':
-    dependencies:
-      '@vue/compiler-core': 3.4.25
-      '@vue/shared': 3.4.25
-
-  '@vue/compiler-dom@3.4.26':
-    dependencies:
-      '@vue/compiler-core': 3.4.26
-      '@vue/shared': 3.4.26
-
-  '@vue/compiler-sfc@3.4.26':
-    dependencies:
-      '@babel/parser': 7.24.5
-      '@vue/compiler-core': 3.4.26
-      '@vue/compiler-dom': 3.4.26
-      '@vue/compiler-ssr': 3.4.26
-      '@vue/shared': 3.4.26
+      '@babel/parser': 7.25.6
+      '@vue/compiler-core': 3.5.11
+      '@vue/compiler-dom': 3.5.11
+      '@vue/compiler-ssr': 3.5.11
+      '@vue/shared': 3.5.11
       estree-walker: 2.0.2
-      magic-string: 0.30.10
-      postcss: 8.4.38
-      source-map-js: 1.2.0
+      magic-string: 0.30.11
+      postcss: 8.4.47
+      source-map-js: 1.2.1
 
-  '@vue/compiler-ssr@3.4.26':
+  '@vue/compiler-ssr@3.4.37':
     dependencies:
-      '@vue/compiler-dom': 3.4.26
-      '@vue/shared': 3.4.26
+      '@vue/compiler-dom': 3.4.37
+      '@vue/shared': 3.4.37
 
-  '@vue/devtools-api@6.6.1': {}
+  '@vue/compiler-ssr@3.5.11':
+    dependencies:
+      '@vue/compiler-dom': 3.5.11
+      '@vue/shared': 3.5.11
 
-  '@vue/language-core@2.0.16(typescript@5.4.5)':
+  '@vue/compiler-vue2@2.7.16':
+    dependencies:
+      de-indent: 1.0.2
+      he: 1.2.0
+
+  '@vue/language-core@2.0.16(typescript@5.6.2)':
     dependencies:
       '@volar/language-core': 2.2.0
-      '@vue/compiler-dom': 3.4.25
-      '@vue/shared': 3.4.25
+      '@vue/compiler-dom': 3.5.11
+      '@vue/shared': 3.5.11
       computeds: 0.0.1
       minimatch: 9.0.4
       path-browserify: 1.0.1
       vue-template-compiler: 2.7.14
     optionalDependencies:
-      typescript: 5.4.5
+      typescript: 5.6.2
 
-  '@vue/reactivity@3.4.26':
+  '@vue/language-core@2.1.6(typescript@5.6.2)':
     dependencies:
-      '@vue/shared': 3.4.26
+      '@volar/language-core': 2.4.5
+      '@vue/compiler-dom': 3.4.37
+      '@vue/compiler-vue2': 2.7.16
+      '@vue/shared': 3.5.11
+      computeds: 0.0.1
+      minimatch: 9.0.4
+      muggle-string: 0.4.1
+      path-browserify: 1.0.1
+    optionalDependencies:
+      typescript: 5.6.2
 
-  '@vue/runtime-core@3.4.26':
+  '@vue/reactivity@3.4.37':
     dependencies:
-      '@vue/reactivity': 3.4.26
-      '@vue/shared': 3.4.26
+      '@vue/shared': 3.4.37
 
-  '@vue/runtime-dom@3.4.26':
+  '@vue/reactivity@3.5.11':
     dependencies:
-      '@vue/runtime-core': 3.4.26
-      '@vue/shared': 3.4.26
+      '@vue/shared': 3.5.11
+
+  '@vue/runtime-core@3.4.37':
+    dependencies:
+      '@vue/reactivity': 3.4.37
+      '@vue/shared': 3.4.37
+
+  '@vue/runtime-core@3.5.11':
+    dependencies:
+      '@vue/reactivity': 3.5.11
+      '@vue/shared': 3.5.11
+
+  '@vue/runtime-dom@3.4.37':
+    dependencies:
+      '@vue/reactivity': 3.4.37
+      '@vue/runtime-core': 3.4.37
+      '@vue/shared': 3.4.37
       csstype: 3.1.3
 
-  '@vue/server-renderer@3.4.26(vue@3.4.26(typescript@5.4.5))':
+  '@vue/runtime-dom@3.5.11':
     dependencies:
-      '@vue/compiler-ssr': 3.4.26
-      '@vue/shared': 3.4.26
-      vue: 3.4.26(typescript@5.4.5)
+      '@vue/reactivity': 3.5.11
+      '@vue/runtime-core': 3.5.11
+      '@vue/shared': 3.5.11
+      csstype: 3.1.3
 
-  '@vue/shared@3.4.21': {}
+  '@vue/server-renderer@3.4.37(vue@3.4.37(typescript@5.5.4))':
+    dependencies:
+      '@vue/compiler-ssr': 3.4.37
+      '@vue/shared': 3.4.37
+      vue: 3.4.37(typescript@5.5.4)
 
-  '@vue/shared@3.4.25': {}
+  '@vue/server-renderer@3.5.11(vue@3.5.11(typescript@5.6.2))':
+    dependencies:
+      '@vue/compiler-ssr': 3.5.11
+      '@vue/shared': 3.5.11
+      vue: 3.5.11(typescript@5.6.2)
 
-  '@vue/shared@3.4.26': {}
+  '@vue/shared@3.4.37': {}
 
-  '@vue/test-utils@2.4.1(@vue/server-renderer@3.4.26(vue@3.4.26(typescript@5.4.5)))(vue@3.4.26(typescript@5.4.5))':
+  '@vue/shared@3.5.10': {}
+
+  '@vue/shared@3.5.11': {}
+
+  '@vue/test-utils@2.4.1(@vue/server-renderer@3.5.11(vue@3.5.11(typescript@5.6.2)))(vue@3.5.11(typescript@5.6.2))':
     dependencies:
       js-beautify: 1.14.9
-      vue: 3.4.26(typescript@5.4.5)
+      vue: 3.5.11(typescript@5.6.2)
       vue-component-type-helpers: 1.8.4
     optionalDependencies:
-      '@vue/server-renderer': 3.4.26(vue@3.4.26(typescript@5.4.5))
+      '@vue/server-renderer': 3.5.11(vue@3.5.11(typescript@5.6.2))
 
   '@webgpu/types@0.1.30': {}
 
-  '@yarnpkg/esbuild-plugin-pnp@3.0.0-rc.15(esbuild@0.20.2)':
-    dependencies:
-      esbuild: 0.20.2
-      tslib: 2.6.2
-
-  '@yarnpkg/fslib@2.10.3':
-    dependencies:
-      '@yarnpkg/libzip': 2.3.0
-      tslib: 1.14.1
-
-  '@yarnpkg/libzip@2.3.0':
-    dependencies:
-      '@types/emscripten': 1.39.7
-      tslib: 1.14.1
-
   abbrev@1.1.1: {}
 
   abbrev@2.0.0: {}
@@ -16677,22 +16473,22 @@ snapshots:
       mime-types: 2.1.35
       negotiator: 0.6.3
 
-  acorn-import-assertions@1.9.0(acorn@8.11.3):
+  acorn-import-assertions@1.9.0(acorn@8.12.1):
     dependencies:
-      acorn: 8.11.3
+      acorn: 8.12.1
     optional: true
 
-  acorn-import-attributes@1.9.5(acorn@8.11.3):
+  acorn-import-attributes@1.9.5(acorn@8.12.1):
     dependencies:
-      acorn: 8.11.3
+      acorn: 8.12.1
 
   acorn-jsx@5.3.2(acorn@7.4.1):
     dependencies:
       acorn: 7.4.1
 
-  acorn-jsx@5.3.2(acorn@8.11.3):
+  acorn-jsx@5.3.2(acorn@8.12.1):
     dependencies:
-      acorn: 8.11.3
+      acorn: 8.12.1
 
   acorn-walk@7.2.0: {}
 
@@ -16700,9 +16496,7 @@ snapshots:
 
   acorn@7.4.1: {}
 
-  acorn@8.11.3: {}
-
-  address@1.2.2: {}
+  acorn@8.12.1: {}
 
   adm-zip@0.5.10:
     optional: true
@@ -16714,13 +16508,14 @@ snapshots:
 
   agent-base@6.0.2:
     dependencies:
-      debug: 4.3.4(supports-color@8.1.1)
+      debug: 4.3.7(supports-color@8.1.1)
     transitivePeerDependencies:
       - supports-color
+    optional: true
 
   agent-base@7.1.0:
     dependencies:
-      debug: 4.3.4(supports-color@8.1.1)
+      debug: 4.3.5(supports-color@5.5.0)
     transitivePeerDependencies:
       - supports-color
 
@@ -16734,7 +16529,7 @@ snapshots:
       clean-stack: 5.2.0
       indent-string: 5.0.0
 
-  aiscript-vscode@https://codeload.github.com/aiscript-dev/aiscript-vscode/tar.gz/34bf4e1530efcf1efa855bd04e2dab39735e1b02:
+  aiscript-vscode@https://codeload.github.com/aiscript-dev/aiscript-vscode/tar.gz/e1e1b27f2f72cd28a473e004b6da0d8fc0bd40d9:
     dependencies:
       '@aiscript-dev/aiscript-languageserver': https://github.com/aiscript-dev/aiscript-languageserver/releases/download/0.1.6/aiscript-dev-aiscript-languageserver-0.1.6.tgz
       vscode-languageclient: 9.0.1
@@ -16743,10 +16538,18 @@ snapshots:
     optionalDependencies:
       ajv: 8.13.0
 
-  ajv-formats@2.1.1(ajv@8.13.0):
+  ajv-draft-04@1.0.0(ajv@8.17.1):
+    optionalDependencies:
+      ajv: 8.17.1
+
+  ajv-formats@3.0.1(ajv@8.13.0):
     optionalDependencies:
       ajv: 8.13.0
 
+  ajv-formats@3.0.1(ajv@8.17.1):
+    optionalDependencies:
+      ajv: 8.17.1
+
   ajv@6.12.6:
     dependencies:
       fast-deep-equal: 3.1.3
@@ -16754,6 +16557,13 @@ snapshots:
       json-schema-traverse: 0.4.1
       uri-js: 4.4.1
 
+  ajv@8.12.0:
+    dependencies:
+      fast-deep-equal: 3.1.3
+      json-schema-traverse: 1.0.0
+      require-from-string: 2.0.2
+      uri-js: 4.4.1
+
   ajv@8.13.0:
     dependencies:
       fast-deep-equal: 3.1.3
@@ -16761,6 +16571,13 @@ snapshots:
       require-from-string: 2.0.2
       uri-js: 4.4.1
 
+  ajv@8.17.1:
+    dependencies:
+      fast-deep-equal: 3.1.3
+      fast-uri: 3.0.1
+      json-schema-traverse: 1.0.0
+      require-from-string: 2.0.2
+
   ansi-colors@4.1.3: {}
 
   ansi-escapes@4.3.2:
@@ -16790,8 +16607,6 @@ snapshots:
       normalize-path: 3.0.0
       picomatch: 2.3.1
 
-  app-root-dir@1.0.2: {}
-
   app-root-path@3.1.0: {}
 
   append-field@1.0.0: {}
@@ -16803,7 +16618,7 @@ snapshots:
 
   archiver-utils@5.0.2:
     dependencies:
-      glob: 10.3.12
+      glob: 10.3.10
       graceful-fs: 4.2.11
       is-stream: 2.0.1
       lazystream: 1.0.1
@@ -16821,8 +16636,6 @@ snapshots:
       tar-stream: 3.1.6
       zip-stream: 6.0.1
 
-  archy@1.0.0: {}
-
   are-we-there-yet@2.0.0:
     dependencies:
       delegates: 1.0.0
@@ -16841,41 +16654,52 @@ snapshots:
     dependencies:
       deep-equal: 2.2.0
 
+  aria-query@5.3.0:
+    dependencies:
+      dequal: 2.0.3
+
   array-buffer-byte-length@1.0.0:
     dependencies:
-      call-bind: 1.0.2
+      call-bind: 1.0.7
       is-array-buffer: 3.0.2
 
+  array-buffer-byte-length@1.0.1:
+    dependencies:
+      call-bind: 1.0.7
+      is-array-buffer: 3.0.4
+
   array-flatten@1.1.1: {}
 
-  array-includes@3.1.7:
+  array-includes@3.1.8:
     dependencies:
-      call-bind: 1.0.2
-      define-properties: 1.2.0
-      es-abstract: 1.22.1
-      get-intrinsic: 1.2.1
+      call-bind: 1.0.7
+      define-properties: 1.2.1
+      es-abstract: 1.23.3
+      es-object-atoms: 1.0.0
+      get-intrinsic: 1.2.4
       is-string: 1.0.7
 
   array-union@2.1.0: {}
 
-  array.prototype.findlastindex@1.2.3:
+  array.prototype.findlastindex@1.2.5:
     dependencies:
-      call-bind: 1.0.2
-      define-properties: 1.2.0
-      es-abstract: 1.22.1
-      es-shim-unscopables: 1.0.0
-      get-intrinsic: 1.2.1
+      call-bind: 1.0.7
+      define-properties: 1.2.1
+      es-abstract: 1.23.3
+      es-errors: 1.3.0
+      es-object-atoms: 1.0.0
+      es-shim-unscopables: 1.0.2
 
   array.prototype.flat@1.3.2:
     dependencies:
-      call-bind: 1.0.2
+      call-bind: 1.0.7
       define-properties: 1.2.0
       es-abstract: 1.22.1
       es-shim-unscopables: 1.0.0
 
   array.prototype.flatmap@1.3.2:
     dependencies:
-      call-bind: 1.0.2
+      call-bind: 1.0.7
       define-properties: 1.2.0
       es-abstract: 1.22.1
       es-shim-unscopables: 1.0.0
@@ -16883,12 +16707,23 @@ snapshots:
   arraybuffer.prototype.slice@1.0.1:
     dependencies:
       array-buffer-byte-length: 1.0.0
-      call-bind: 1.0.2
+      call-bind: 1.0.7
       define-properties: 1.2.0
-      get-intrinsic: 1.2.1
+      get-intrinsic: 1.2.4
       is-array-buffer: 3.0.2
       is-shared-array-buffer: 1.0.2
 
+  arraybuffer.prototype.slice@1.0.3:
+    dependencies:
+      array-buffer-byte-length: 1.0.1
+      call-bind: 1.0.7
+      define-properties: 1.2.1
+      es-abstract: 1.23.3
+      es-errors: 1.3.0
+      get-intrinsic: 1.2.4
+      is-array-buffer: 3.0.4
+      is-shared-array-buffer: 1.0.3
+
   arrify@1.0.1: {}
 
   asap@2.0.6: {}
@@ -16908,34 +16743,30 @@ snapshots:
     dependencies:
       pvtsutils: 1.3.5
       pvutils: 1.1.3
-      tslib: 2.6.2
+      tslib: 2.7.0
 
   assert-never@1.2.1: {}
 
   assert-plus@1.0.0: {}
 
-  assert@2.1.0:
-    dependencies:
-      call-bind: 1.0.2
-      is-nan: 1.3.2
-      object-is: 1.1.5
-      object.assign: 4.1.4
-      util: 0.12.5
-
   assertion-error@1.1.0: {}
 
+  assertion-error@2.0.1: {}
+
   ast-types@0.16.1:
     dependencies:
-      tslib: 2.6.2
+      tslib: 2.7.0
 
   astral-regex@2.0.0: {}
 
-  astring@1.8.6: {}
+  astring@1.9.0: {}
 
   async-mutex@0.5.0:
     dependencies:
       tslib: 2.6.2
 
+  async@0.2.10: {}
+
   async@3.2.4: {}
 
   asynckit@0.4.0: {}
@@ -16946,16 +16777,16 @@ snapshots:
 
   available-typed-arrays@1.0.5: {}
 
-  avvio@8.3.0:
+  available-typed-arrays@1.0.7:
     dependencies:
-      '@fastify/error': 3.4.0
-      archy: 1.0.0
-      debug: 4.3.4(supports-color@8.1.1)
-      fastq: 1.17.1
-    transitivePeerDependencies:
-      - supports-color
+      possible-typed-array-names: 1.0.0
 
-  aws-sdk-client-mock@3.0.1:
+  avvio@9.0.0:
+    dependencies:
+      '@fastify/error': 4.0.0
+      fastq: 1.17.1
+
+  aws-sdk-client-mock@4.0.1:
     dependencies:
       '@types/sinon': 10.0.13
       sinon: 16.1.3
@@ -16967,13 +16798,13 @@ snapshots:
 
   axios@0.24.0:
     dependencies:
-      follow-redirects: 1.15.2(debug@4.3.4)
+      follow-redirects: 1.15.2
     transitivePeerDependencies:
       - debug
 
-  axios@1.6.2(debug@4.3.4):
+  axios@1.7.7(debug@4.3.7):
     dependencies:
-      follow-redirects: 1.15.2(debug@4.3.4)
+      follow-redirects: 1.15.9(debug@4.3.7)
       form-data: 4.0.0
       proxy-from-env: 1.1.0
     transitivePeerDependencies:
@@ -16981,10 +16812,6 @@ snapshots:
 
   b4a@1.6.4: {}
 
-  babel-core@7.0.0-bridge.0(@babel/core@7.24.0):
-    dependencies:
-      '@babel/core': 7.24.0
-
   babel-jest@29.7.0(@babel/core@7.23.5):
     dependencies:
       '@babel/core': 7.23.5
@@ -17011,34 +16838,10 @@ snapshots:
   babel-plugin-jest-hoist@29.6.3:
     dependencies:
       '@babel/template': 7.24.0
-      '@babel/types': 7.24.0
+      '@babel/types': 7.24.7
       '@types/babel__core': 7.20.0
       '@types/babel__traverse': 7.20.0
 
-  babel-plugin-polyfill-corejs2@0.4.6(@babel/core@7.24.0):
-    dependencies:
-      '@babel/compat-data': 7.23.5
-      '@babel/core': 7.24.0
-      '@babel/helper-define-polyfill-provider': 0.4.3(@babel/core@7.24.0)
-      semver: 6.3.1
-    transitivePeerDependencies:
-      - supports-color
-
-  babel-plugin-polyfill-corejs3@0.8.6(@babel/core@7.24.0):
-    dependencies:
-      '@babel/core': 7.24.0
-      '@babel/helper-define-polyfill-provider': 0.4.3(@babel/core@7.24.0)
-      core-js-compat: 3.33.3
-    transitivePeerDependencies:
-      - supports-color
-
-  babel-plugin-polyfill-regenerator@0.5.3(@babel/core@7.24.0):
-    dependencies:
-      '@babel/core': 7.24.0
-      '@babel/helper-define-polyfill-provider': 0.4.3(@babel/core@7.24.0)
-    transitivePeerDependencies:
-      - supports-color
-
   babel-preset-current-node-syntax@1.0.1(@babel/core@7.23.5):
     dependencies:
       '@babel/core': 7.23.5
@@ -17063,7 +16866,7 @@ snapshots:
 
   babel-walk@3.0.0-canary-5:
     dependencies:
-      '@babel/types': 7.23.5
+      '@babel/types': 7.24.7
 
   bail@2.0.2: {}
 
@@ -17081,8 +16884,6 @@ snapshots:
     dependencies:
       open: 8.4.2
 
-  big-integer@1.6.51: {}
-
   bin-check@4.1.0:
     dependencies:
       execa: 0.7.0
@@ -17101,12 +16902,6 @@ snapshots:
 
   binary-extensions@2.2.0: {}
 
-  bl@4.1.0:
-    dependencies:
-      buffer: 5.7.1
-      inherits: 2.0.4
-      readable-stream: 3.6.0
-
   blob-util@2.0.2: {}
 
   bluebird@3.7.2: {}
@@ -17115,7 +16910,7 @@ snapshots:
 
   bn.js@4.12.0: {}
 
-  body-parser@1.20.1:
+  body-parser@1.20.3:
     dependencies:
       bytes: 3.1.2
       content-type: 1.0.5
@@ -17125,24 +16920,7 @@ snapshots:
       http-errors: 2.0.0
       iconv-lite: 0.4.24
       on-finished: 2.4.1
-      qs: 6.11.0
-      raw-body: 2.5.1
-      type-is: 1.6.18
-      unpipe: 1.0.0
-    transitivePeerDependencies:
-      - supports-color
-
-  body-parser@1.20.2:
-    dependencies:
-      bytes: 3.1.2
-      content-type: 1.0.5
-      debug: 2.6.9
-      depd: 2.0.0
-      destroy: 1.2.0
-      http-errors: 2.0.0
-      iconv-lite: 0.4.24
-      on-finished: 2.4.1
-      qs: 6.11.0
+      qs: 6.13.0
       raw-body: 2.5.2
       type-is: 1.6.18
       unpipe: 1.0.0
@@ -17153,10 +16931,6 @@ snapshots:
 
   bowser@2.11.0: {}
 
-  bplist-parser@0.2.0:
-    dependencies:
-      big-integer: 1.6.51
-
   brace-expansion@1.1.11:
     dependencies:
       balanced-match: 1.0.2
@@ -17170,6 +16944,10 @@ snapshots:
     dependencies:
       fill-range: 7.0.1
 
+  braces@3.0.3:
+    dependencies:
+      fill-range: 7.1.1
+
   broadcast-channel@7.0.0:
     dependencies:
       '@babel/runtime': 7.23.4
@@ -17179,10 +16957,6 @@ snapshots:
 
   browser-assert@1.2.1: {}
 
-  browserify-zlib@0.1.4:
-    dependencies:
-      pako: 0.2.9
-
   browserslist@4.22.2:
     dependencies:
       caniuse-lite: 1.0.30001566
@@ -17229,14 +17003,19 @@ snapshots:
       node-gyp-build: 4.6.0
     optional: true
 
-  bullmq@5.7.8:
+  bufferutil@4.0.8:
+    dependencies:
+      node-gyp-build: 4.6.0
+    optional: true
+
+  bullmq@5.15.0:
     dependencies:
       cron-parser: 4.8.1
       ioredis: 5.4.1
       msgpackr: 1.10.1
       node-abort-controller: 3.1.1
-      semver: 7.6.0
-      tslib: 2.6.2
+      semver: 7.6.3
+      tslib: 2.7.0
       uuid: 9.0.1
     transitivePeerDependencies:
       - supports-color
@@ -17247,8 +17026,6 @@ snapshots:
     dependencies:
       streamsearch: 1.1.0
 
-  bytes@3.0.0: {}
-
   bytes@3.1.2: {}
 
   cac@6.7.14: {}
@@ -17256,10 +17033,10 @@ snapshots:
   cacache@18.0.0:
     dependencies:
       '@npmcli/fs': 3.1.0
-      fs-minipass: 3.0.2
-      glob: 10.3.12
-      lru-cache: 10.0.2
-      minipass: 7.0.4
+      fs-minipass: 3.0.3
+      glob: 10.3.10
+      lru-cache: 10.2.2
+      minipass: 7.1.2
       minipass-collect: 1.0.2
       minipass-flush: 1.0.5
       minipass-pipeline: 1.2.4
@@ -17279,7 +17056,17 @@ snapshots:
       http-cache-semantics: 4.1.1
       keyv: 4.5.4
       mimic-response: 4.0.0
-      normalize-url: 8.0.0
+      normalize-url: 8.0.1
+      responselike: 3.0.0
+
+  cacheable-request@12.0.1:
+    dependencies:
+      '@types/http-cache-semantics': 4.0.4
+      get-stream: 9.0.1
+      http-cache-semantics: 4.1.1
+      keyv: 4.5.4
+      mimic-response: 4.0.0
+      normalize-url: 8.0.1
       responselike: 3.0.0
 
   cacheable-request@7.0.2:
@@ -17299,6 +17086,14 @@ snapshots:
       function-bind: 1.1.2
       get-intrinsic: 1.2.1
 
+  call-bind@1.0.7:
+    dependencies:
+      es-define-property: 1.0.0
+      es-errors: 1.3.0
+      function-bind: 1.1.2
+      get-intrinsic: 1.2.4
+      set-function-length: 1.2.2
+
   call-me-maybe@1.0.2: {}
 
   callsites@3.1.0: {}
@@ -17346,6 +17141,14 @@ snapshots:
       pathval: 1.1.1
       type-detect: 4.0.8
 
+  chai@5.1.1:
+    dependencies:
+      assertion-error: 2.0.1
+      check-error: 2.1.1
+      deep-eql: 5.0.2
+      loupe: 3.1.1
+      pathval: 2.0.0
+
   chalk-template@1.1.0:
     dependencies:
       chalk: 5.3.0
@@ -17370,38 +17173,44 @@ snapshots:
 
   char-regex@1.0.2: {}
 
+  character-entities-html4@2.1.0: {}
+
+  character-entities-legacy@3.0.0: {}
+
   character-entities@2.0.2: {}
 
   character-parser@2.2.0:
     dependencies:
       is-regex: 1.1.4
 
-  chart.js@4.4.2:
+  chart.js@4.4.4:
     dependencies:
       '@kurkle/color': 0.3.2
 
-  chartjs-adapter-date-fns@3.0.0(chart.js@4.4.2)(date-fns@2.30.0):
+  chartjs-adapter-date-fns@3.0.0(chart.js@4.4.4)(date-fns@2.30.0):
     dependencies:
-      chart.js: 4.4.2
+      chart.js: 4.4.4
       date-fns: 2.30.0
 
-  chartjs-chart-matrix@2.0.1(chart.js@4.4.2):
+  chartjs-chart-matrix@2.0.1(chart.js@4.4.4):
     dependencies:
-      chart.js: 4.4.2
+      chart.js: 4.4.4
 
-  chartjs-plugin-gradient@0.6.1(chart.js@4.4.2):
+  chartjs-plugin-gradient@0.6.1(chart.js@4.4.4):
     dependencies:
-      chart.js: 4.4.2
+      chart.js: 4.4.4
 
-  chartjs-plugin-zoom@2.0.1(chart.js@4.4.2):
+  chartjs-plugin-zoom@2.0.1(chart.js@4.4.4):
     dependencies:
-      chart.js: 4.4.2
+      chart.js: 4.4.4
       hammerjs: 2.0.8
 
   check-error@1.0.3:
     dependencies:
       get-func-name: 2.0.2
 
+  check-error@2.1.1: {}
+
   check-more-types@2.24.0: {}
 
   cheerio-select@2.1.0:
@@ -17413,6 +17222,20 @@ snapshots:
       domhandler: 5.0.3
       domutils: 3.0.1
 
+  cheerio@1.0.0:
+    dependencies:
+      cheerio-select: 2.1.0
+      dom-serializer: 2.0.0
+      domhandler: 5.0.3
+      domutils: 3.1.0
+      encoding-sniffer: 0.2.0
+      htmlparser2: 9.1.0
+      parse5: 7.1.2
+      parse5-htmlparser2-tree-adapter: 7.0.0
+      parse5-parser-stream: 7.1.2
+      undici: 6.19.8
+      whatwg-mimetype: 4.0.0
+
   cheerio@1.0.0-rc.12:
     dependencies:
       cheerio-select: 2.1.0
@@ -17435,11 +17258,12 @@ snapshots:
     optionalDependencies:
       fsevents: 2.3.3
 
-  chownr@1.1.4: {}
+  chownr@1.1.4:
+    optional: true
 
   chownr@2.0.0: {}
 
-  chromatic@11.3.0: {}
+  chromatic@11.11.0: {}
 
   ci-info@3.7.1: {}
 
@@ -17464,8 +17288,6 @@ snapshots:
       parse5-htmlparser2-tree-adapter: 6.0.1
       yargs: 16.2.0
 
-  cli-spinners@2.7.0: {}
-
   cli-spinners@2.9.2: {}
 
   cli-table3@0.6.3:
@@ -17499,18 +17321,10 @@ snapshots:
       strip-ansi: 6.0.1
       wrap-ansi: 7.0.0
 
-  clone-deep@4.0.1:
-    dependencies:
-      is-plain-object: 2.0.4
-      kind-of: 6.0.3
-      shallow-clone: 3.0.1
-
   clone-response@1.0.3:
     dependencies:
       mimic-response: 1.0.1
 
-  clone@1.0.4: {}
-
   cluster-key-slot@1.1.2: {}
 
   co@4.6.0: {}
@@ -17552,8 +17366,12 @@ snapshots:
     dependencies:
       delayed-stream: 1.0.0
 
+  comma-separated-tokens@2.0.3: {}
+
   commander@10.0.1: {}
 
+  commander@12.1.0: {}
+
   commander@2.20.3: {}
 
   commander@6.2.1: {}
@@ -17568,7 +17386,7 @@ snapshots:
 
   commondir@1.0.1: {}
 
-  compare-versions@6.1.0: {}
+  compare-versions@6.1.1: {}
 
   compress-commons@6.0.2:
     dependencies:
@@ -17578,22 +17396,6 @@ snapshots:
       normalize-path: 3.0.0
       readable-stream: 4.3.0
 
-  compressible@2.0.18:
-    dependencies:
-      mime-db: 1.52.0
-
-  compression@1.7.4:
-    dependencies:
-      accepts: 1.3.8
-      bytes: 3.0.0
-      compressible: 2.0.18
-      debug: 2.6.9
-      on-headers: 1.0.2
-      safe-buffer: 5.1.2
-      vary: 1.1.2
-    transitivePeerDependencies:
-      - supports-color
-
   computeds@0.0.1: {}
 
   concat-map@0.0.1: {}
@@ -17617,8 +17419,8 @@ snapshots:
 
   constantinople@4.0.1:
     dependencies:
-      '@babel/parser': 7.23.9
-      '@babel/types': 7.23.5
+      '@babel/parser': 7.24.7
+      '@babel/types': 7.24.7
 
   content-disposition@0.5.4:
     dependencies:
@@ -17636,10 +17438,6 @@ snapshots:
 
   cookie@0.6.0: {}
 
-  core-js-compat@3.33.3:
-    dependencies:
-      browserslist: 4.23.0
-
   core-js@3.29.1: {}
 
   core-util-is@1.0.2: {}
@@ -17658,13 +17456,13 @@ snapshots:
       crc-32: 1.2.2
       readable-stream: 4.3.0
 
-  create-jest@29.7.0(@types/node@20.12.7):
+  create-jest@29.7.0(@types/node@20.14.12):
     dependencies:
       '@jest/types': 29.6.3
       chalk: 4.1.2
       exit: 0.1.2
       graceful-fs: 4.2.11
-      jest-config: 29.7.0(@types/node@20.12.7)
+      jest-config: 29.7.0(@types/node@20.14.12)
       jest-util: 29.7.0
       prompts: 2.4.2
     transitivePeerDependencies:
@@ -17677,10 +17475,10 @@ snapshots:
     dependencies:
       luxon: 3.3.0
 
-  cropperjs@2.0.0-beta.5:
+  cropperjs@2.0.0-rc.2:
     dependencies:
-      '@cropper/elements': 2.0.0-beta.5
-      '@cropper/utils': 2.0.0-beta.5
+      '@cropper/elements': 2.0.0-rc.2
+      '@cropper/utils': 2.0.0-rc.2
 
   cross-env@7.0.3:
     dependencies:
@@ -17710,11 +17508,9 @@ snapshots:
       shebang-command: 2.0.0
       which: 2.0.2
 
-  crypto-random-string@2.0.0: {}
-
-  css-declaration-sorter@7.2.0(postcss@8.4.38):
+  css-declaration-sorter@7.2.0(postcss@8.4.47):
     dependencies:
-      postcss: 8.4.38
+      postcss: 8.4.47
 
   css-select@5.1.0:
     dependencies:
@@ -17727,12 +17523,12 @@ snapshots:
   css-tree@2.2.1:
     dependencies:
       mdn-data: 2.0.28
-      source-map-js: 1.0.2
+      source-map-js: 1.2.1
 
   css-tree@2.3.1:
     dependencies:
       mdn-data: 2.0.30
-      source-map-js: 1.0.2
+      source-map-js: 1.2.1
 
   css-what@6.1.0: {}
 
@@ -17740,49 +17536,49 @@ snapshots:
 
   cssesc@3.0.0: {}
 
-  cssnano-preset-default@6.1.2(postcss@8.4.38):
+  cssnano-preset-default@6.1.2(postcss@8.4.47):
     dependencies:
       browserslist: 4.23.0
-      css-declaration-sorter: 7.2.0(postcss@8.4.38)
-      cssnano-utils: 4.0.2(postcss@8.4.38)
-      postcss: 8.4.38
-      postcss-calc: 9.0.1(postcss@8.4.38)
-      postcss-colormin: 6.1.0(postcss@8.4.38)
-      postcss-convert-values: 6.1.0(postcss@8.4.38)
-      postcss-discard-comments: 6.0.2(postcss@8.4.38)
-      postcss-discard-duplicates: 6.0.3(postcss@8.4.38)
-      postcss-discard-empty: 6.0.3(postcss@8.4.38)
-      postcss-discard-overridden: 6.0.2(postcss@8.4.38)
-      postcss-merge-longhand: 6.0.5(postcss@8.4.38)
-      postcss-merge-rules: 6.1.1(postcss@8.4.38)
-      postcss-minify-font-values: 6.1.0(postcss@8.4.38)
-      postcss-minify-gradients: 6.0.3(postcss@8.4.38)
-      postcss-minify-params: 6.1.0(postcss@8.4.38)
-      postcss-minify-selectors: 6.0.4(postcss@8.4.38)
-      postcss-normalize-charset: 6.0.2(postcss@8.4.38)
-      postcss-normalize-display-values: 6.0.2(postcss@8.4.38)
-      postcss-normalize-positions: 6.0.2(postcss@8.4.38)
-      postcss-normalize-repeat-style: 6.0.2(postcss@8.4.38)
-      postcss-normalize-string: 6.0.2(postcss@8.4.38)
-      postcss-normalize-timing-functions: 6.0.2(postcss@8.4.38)
-      postcss-normalize-unicode: 6.1.0(postcss@8.4.38)
-      postcss-normalize-url: 6.0.2(postcss@8.4.38)
-      postcss-normalize-whitespace: 6.0.2(postcss@8.4.38)
-      postcss-ordered-values: 6.0.2(postcss@8.4.38)
-      postcss-reduce-initial: 6.1.0(postcss@8.4.38)
-      postcss-reduce-transforms: 6.0.2(postcss@8.4.38)
-      postcss-svgo: 6.0.3(postcss@8.4.38)
-      postcss-unique-selectors: 6.0.4(postcss@8.4.38)
+      css-declaration-sorter: 7.2.0(postcss@8.4.47)
+      cssnano-utils: 4.0.2(postcss@8.4.47)
+      postcss: 8.4.47
+      postcss-calc: 9.0.1(postcss@8.4.47)
+      postcss-colormin: 6.1.0(postcss@8.4.47)
+      postcss-convert-values: 6.1.0(postcss@8.4.47)
+      postcss-discard-comments: 6.0.2(postcss@8.4.47)
+      postcss-discard-duplicates: 6.0.3(postcss@8.4.47)
+      postcss-discard-empty: 6.0.3(postcss@8.4.47)
+      postcss-discard-overridden: 6.0.2(postcss@8.4.47)
+      postcss-merge-longhand: 6.0.5(postcss@8.4.47)
+      postcss-merge-rules: 6.1.1(postcss@8.4.47)
+      postcss-minify-font-values: 6.1.0(postcss@8.4.47)
+      postcss-minify-gradients: 6.0.3(postcss@8.4.47)
+      postcss-minify-params: 6.1.0(postcss@8.4.47)
+      postcss-minify-selectors: 6.0.4(postcss@8.4.47)
+      postcss-normalize-charset: 6.0.2(postcss@8.4.47)
+      postcss-normalize-display-values: 6.0.2(postcss@8.4.47)
+      postcss-normalize-positions: 6.0.2(postcss@8.4.47)
+      postcss-normalize-repeat-style: 6.0.2(postcss@8.4.47)
+      postcss-normalize-string: 6.0.2(postcss@8.4.47)
+      postcss-normalize-timing-functions: 6.0.2(postcss@8.4.47)
+      postcss-normalize-unicode: 6.1.0(postcss@8.4.47)
+      postcss-normalize-url: 6.0.2(postcss@8.4.47)
+      postcss-normalize-whitespace: 6.0.2(postcss@8.4.47)
+      postcss-ordered-values: 6.0.2(postcss@8.4.47)
+      postcss-reduce-initial: 6.1.0(postcss@8.4.47)
+      postcss-reduce-transforms: 6.0.2(postcss@8.4.47)
+      postcss-svgo: 6.0.3(postcss@8.4.47)
+      postcss-unique-selectors: 6.0.4(postcss@8.4.47)
 
-  cssnano-utils@4.0.2(postcss@8.4.38):
+  cssnano-utils@4.0.2(postcss@8.4.47):
     dependencies:
-      postcss: 8.4.38
+      postcss: 8.4.47
 
-  cssnano@6.1.2(postcss@8.4.38):
+  cssnano@6.1.2(postcss@8.4.47):
     dependencies:
-      cssnano-preset-default: 6.1.2(postcss@8.4.38)
+      cssnano-preset-default: 6.1.2(postcss@8.4.47)
       lilconfig: 3.1.1
-      postcss: 8.4.38
+      postcss: 8.4.47
 
   csso@5.0.5:
     dependencies:
@@ -17798,9 +17594,9 @@ snapshots:
     dependencies:
       uniq: 1.0.1
 
-  cypress@13.7.3:
+  cypress@13.14.2:
     dependencies:
-      '@cypress/request': 3.0.0
+      '@cypress/request': 3.0.5
       '@cypress/xvfb': 1.2.4(supports-color@8.1.1)
       '@types/sinonjs__fake-timers': 8.1.1
       '@types/sizzle': 2.3.3
@@ -17816,7 +17612,7 @@ snapshots:
       commander: 6.2.1
       common-tags: 1.8.2
       dayjs: 1.11.10
-      debug: 4.3.4(supports-color@8.1.1)
+      debug: 4.3.5(supports-color@8.1.1)
       enquirer: 2.3.6
       eventemitter2: 6.4.7
       execa: 4.1.0
@@ -17843,9 +17639,9 @@ snapshots:
       untildify: 4.0.0
       yauzl: 2.10.0
 
-  cypress@13.8.1:
+  cypress@13.15.0:
     dependencies:
-      '@cypress/request': 3.0.0
+      '@cypress/request': 3.0.5
       '@cypress/xvfb': 1.2.4(supports-color@8.1.1)
       '@types/sinonjs__fake-timers': 8.1.1
       '@types/sizzle': 2.3.3
@@ -17861,7 +17657,7 @@ snapshots:
       commander: 6.2.1
       common-tags: 1.8.2
       dayjs: 1.11.10
-      debug: 4.3.4(supports-color@8.1.1)
+      debug: 4.3.7(supports-color@8.1.1)
       enquirer: 2.3.6
       eventemitter2: 6.4.7
       execa: 4.1.0
@@ -17882,7 +17678,7 @@ snapshots:
       process: 0.11.10
       proxy-from-env: 1.0.0
       request-progress: 3.0.0
-      semver: 7.6.0
+      semver: 7.6.3
       supports-color: 8.1.1
       tmp: 0.2.3
       untildify: 4.0.0
@@ -17901,6 +17697,24 @@ snapshots:
       whatwg-mimetype: 4.0.0
       whatwg-url: 14.0.0
 
+  data-view-buffer@1.0.1:
+    dependencies:
+      call-bind: 1.0.7
+      es-errors: 1.3.0
+      is-data-view: 1.0.1
+
+  data-view-byte-length@1.0.1:
+    dependencies:
+      call-bind: 1.0.7
+      es-errors: 1.3.0
+      is-data-view: 1.0.1
+
+  data-view-byte-offset@1.0.0:
+    dependencies:
+      call-bind: 1.0.7
+      es-errors: 1.3.0
+      is-data-view: 1.0.1
+
   date-fns@2.30.0:
     dependencies:
       '@babel/runtime': 7.23.4
@@ -17925,9 +17739,21 @@ snapshots:
     optionalDependencies:
       supports-color: 5.5.0
 
-  debug@4.3.4(supports-color@8.1.1):
+  debug@4.3.5(supports-color@5.5.0):
     dependencies:
       ms: 2.1.2
+    optionalDependencies:
+      supports-color: 5.5.0
+
+  debug@4.3.5(supports-color@8.1.1):
+    dependencies:
+      ms: 2.1.2
+    optionalDependencies:
+      supports-color: 8.1.1
+
+  debug@4.3.7(supports-color@8.1.1):
+    dependencies:
+      ms: 2.1.3
     optionalDependencies:
       supports-color: 8.1.1
 
@@ -17974,6 +17800,8 @@ snapshots:
     dependencies:
       type-detect: 4.0.8
 
+  deep-eql@5.0.2: {}
+
   deep-equal@2.2.0:
     dependencies:
       call-bind: 1.0.2
@@ -17998,17 +17826,14 @@ snapshots:
 
   deepmerge@4.2.2: {}
 
-  default-browser-id@3.0.0:
-    dependencies:
-      bplist-parser: 0.2.0
-      untildify: 4.0.0
-
-  defaults@1.0.4:
-    dependencies:
-      clone: 1.0.4
-
   defer-to-connect@2.0.1: {}
 
+  define-data-property@1.1.4:
+    dependencies:
+      es-define-property: 1.0.0
+      es-errors: 1.3.0
+      gopd: 1.0.1
+
   define-lazy-prop@2.0.0: {}
 
   define-properties@1.2.0:
@@ -18016,18 +17841,11 @@ snapshots:
       has-property-descriptors: 1.0.0
       object-keys: 1.1.1
 
-  defu@6.1.4: {}
-
-  del@6.1.1:
+  define-properties@1.2.1:
     dependencies:
-      globby: 11.1.0
-      graceful-fs: 4.2.11
-      is-glob: 4.0.3
-      is-path-cwd: 2.2.0
-      is-path-inside: 3.0.3
-      p-map: 4.0.0
-      rimraf: 3.0.2
-      slash: 3.0.0
+      define-data-property: 1.1.4
+      has-property-descriptors: 1.0.2
+      object-keys: 1.1.1
 
   delayed-stream@1.0.0: {}
 
@@ -18042,26 +17860,10 @@ snapshots:
 
   destroy@1.2.0: {}
 
-  detect-indent@6.1.0: {}
-
-  detect-libc@2.0.2:
-    optional: true
-
   detect-libc@2.0.3: {}
 
   detect-newline@3.1.0: {}
 
-  detect-package-manager@2.0.1:
-    dependencies:
-      execa: 5.1.1
-
-  detect-port@1.5.1:
-    dependencies:
-      address: 1.2.2
-      debug: 4.3.4(supports-color@8.1.1)
-    transitivePeerDependencies:
-      - supports-color
-
   devlop@1.1.0:
     dependencies:
       dequal: 2.0.3
@@ -18072,6 +17874,8 @@ snapshots:
 
   diff@5.1.0: {}
 
+  diff@5.2.0: {}
+
   dijkstrajs@1.0.2: {}
 
   dir-glob@3.0.1:
@@ -18094,6 +17898,12 @@ snapshots:
 
   dom-accessibility-api@0.6.3: {}
 
+  dom-serializer@1.4.1:
+    dependencies:
+      domelementtype: 2.3.0
+      domhandler: 4.3.1
+      entities: 2.2.0
+
   dom-serializer@2.0.0:
     dependencies:
       domelementtype: 2.3.0
@@ -18102,29 +17912,40 @@ snapshots:
 
   domelementtype@2.3.0: {}
 
+  domhandler@3.3.0:
+    dependencies:
+      domelementtype: 2.3.0
+
+  domhandler@4.3.1:
+    dependencies:
+      domelementtype: 2.3.0
+
   domhandler@5.0.3:
     dependencies:
       domelementtype: 2.3.0
 
+  domutils@2.8.0:
+    dependencies:
+      dom-serializer: 1.4.1
+      domelementtype: 2.3.0
+      domhandler: 4.3.1
+
   domutils@3.0.1:
     dependencies:
       dom-serializer: 2.0.0
       domelementtype: 2.3.0
       domhandler: 5.0.3
 
-  dotenv-expand@10.0.0: {}
+  domutils@3.1.0:
+    dependencies:
+      dom-serializer: 2.0.0
+      domelementtype: 2.3.0
+      domhandler: 5.0.3
 
   dotenv@16.0.3: {}
 
   duplexer@0.1.2: {}
 
-  duplexify@3.7.1:
-    dependencies:
-      end-of-stream: 1.4.4
-      inherits: 2.0.4
-      readable-stream: 2.3.7
-      stream-shift: 1.0.1
-
   eastasianwidth@0.2.0: {}
 
   ecc-jsbn@0.1.2:
@@ -18141,11 +17962,11 @@ snapshots:
       '@one-ini/wasm': 0.1.1
       commander: 10.0.1
       minimatch: 9.0.1
-      semver: 7.6.0
+      semver: 7.6.3
 
   ee-first@1.1.1: {}
 
-  ejs@3.1.9:
+  ejs@3.1.10:
     dependencies:
       jake: 10.8.5
 
@@ -18159,10 +17980,15 @@ snapshots:
 
   emoji-regex@9.2.2: {}
 
-  encode-utf8@1.0.3: {}
-
   encodeurl@1.0.2: {}
 
+  encodeurl@2.0.0: {}
+
+  encoding-sniffer@0.2.0:
+    dependencies:
+      iconv-lite: 0.6.3
+      whatwg-encoding: 3.1.1
+
   encoding@0.1.13:
     dependencies:
       iconv-lite: 0.6.3
@@ -18180,9 +18006,9 @@ snapshots:
 
   entities@4.5.0: {}
 
-  env-paths@2.2.1: {}
+  entities@5.0.0: {}
 
-  envinfo@7.8.1: {}
+  env-paths@2.2.1: {}
 
   err-code@2.0.3: {}
 
@@ -18195,16 +18021,16 @@ snapshots:
       array-buffer-byte-length: 1.0.0
       arraybuffer.prototype.slice: 1.0.1
       available-typed-arrays: 1.0.5
-      call-bind: 1.0.2
+      call-bind: 1.0.7
       es-set-tostringtag: 2.0.1
       es-to-primitive: 1.2.1
       function.prototype.name: 1.1.5
-      get-intrinsic: 1.2.1
+      get-intrinsic: 1.2.4
       get-symbol-description: 1.0.0
       globalthis: 1.0.3
       gopd: 1.0.1
       has: 1.0.3
-      has-property-descriptors: 1.0.0
+      has-property-descriptors: 1.0.2
       has-proto: 1.0.1
       has-symbols: 1.0.3
       internal-slot: 1.0.5
@@ -18216,7 +18042,7 @@ snapshots:
       is-string: 1.0.7
       is-typed-array: 1.1.10
       is-weakref: 1.0.2
-      object-inspect: 1.12.3
+      object-inspect: 1.13.2
       object-keys: 1.1.1
       object.assign: 4.1.4
       regexp.prototype.flags: 1.5.0
@@ -18232,6 +18058,61 @@ snapshots:
       unbox-primitive: 1.0.2
       which-typed-array: 1.1.11
 
+  es-abstract@1.23.3:
+    dependencies:
+      array-buffer-byte-length: 1.0.1
+      arraybuffer.prototype.slice: 1.0.3
+      available-typed-arrays: 1.0.7
+      call-bind: 1.0.7
+      data-view-buffer: 1.0.1
+      data-view-byte-length: 1.0.1
+      data-view-byte-offset: 1.0.0
+      es-define-property: 1.0.0
+      es-errors: 1.3.0
+      es-object-atoms: 1.0.0
+      es-set-tostringtag: 2.0.3
+      es-to-primitive: 1.2.1
+      function.prototype.name: 1.1.6
+      get-intrinsic: 1.2.4
+      get-symbol-description: 1.0.2
+      globalthis: 1.0.3
+      gopd: 1.0.1
+      has-property-descriptors: 1.0.2
+      has-proto: 1.0.3
+      has-symbols: 1.0.3
+      hasown: 2.0.2
+      internal-slot: 1.0.7
+      is-array-buffer: 3.0.4
+      is-callable: 1.2.7
+      is-data-view: 1.0.1
+      is-negative-zero: 2.0.3
+      is-regex: 1.1.4
+      is-shared-array-buffer: 1.0.3
+      is-string: 1.0.7
+      is-typed-array: 1.1.13
+      is-weakref: 1.0.2
+      object-inspect: 1.13.2
+      object-keys: 1.1.1
+      object.assign: 4.1.5
+      regexp.prototype.flags: 1.5.2
+      safe-array-concat: 1.1.2
+      safe-regex-test: 1.0.3
+      string.prototype.trim: 1.2.9
+      string.prototype.trimend: 1.0.8
+      string.prototype.trimstart: 1.0.8
+      typed-array-buffer: 1.0.2
+      typed-array-byte-length: 1.0.1
+      typed-array-byte-offset: 1.0.2
+      typed-array-length: 1.0.6
+      unbox-primitive: 1.0.2
+      which-typed-array: 1.1.15
+
+  es-define-property@1.0.0:
+    dependencies:
+      get-intrinsic: 1.2.4
+
+  es-errors@1.3.0: {}
+
   es-get-iterator@1.1.3:
     dependencies:
       call-bind: 1.0.2
@@ -18244,18 +18125,32 @@ snapshots:
       isarray: 2.0.5
       stop-iteration-iterator: 1.0.0
 
-  es-module-lexer@0.9.3: {}
+  es-module-lexer@1.5.4: {}
+
+  es-object-atoms@1.0.0:
+    dependencies:
+      es-errors: 1.3.0
 
   es-set-tostringtag@2.0.1:
     dependencies:
-      get-intrinsic: 1.2.1
+      get-intrinsic: 1.2.4
       has: 1.0.3
       has-tostringtag: 1.0.0
 
+  es-set-tostringtag@2.0.3:
+    dependencies:
+      get-intrinsic: 1.2.4
+      has-tostringtag: 1.0.2
+      hasown: 2.0.2
+
   es-shim-unscopables@1.0.0:
     dependencies:
       has: 1.0.3
 
+  es-shim-unscopables@1.0.2:
+    dependencies:
+      hasown: 2.0.2
+
   es-to-primitive@1.2.1:
     dependencies:
       is-callable: 1.2.7
@@ -18270,12 +18165,10 @@ snapshots:
       es6-promise: 4.2.8
     optional: true
 
-  esbuild-plugin-alias@0.2.1: {}
-
-  esbuild-register@3.5.0(esbuild@0.20.2):
+  esbuild-register@3.5.0(esbuild@0.23.1):
     dependencies:
-      debug: 4.3.4(supports-color@8.1.1)
-      esbuild: 0.20.2
+      debug: 4.3.7(supports-color@8.1.1)
+      esbuild: 0.23.1
     transitivePeerDependencies:
       - supports-color
 
@@ -18330,34 +18223,90 @@ snapshots:
       '@esbuild/win32-ia32': 0.19.11
       '@esbuild/win32-x64': 0.19.11
 
-  esbuild@0.20.2:
+  esbuild@0.21.5:
     optionalDependencies:
-      '@esbuild/aix-ppc64': 0.20.2
-      '@esbuild/android-arm': 0.20.2
-      '@esbuild/android-arm64': 0.20.2
-      '@esbuild/android-x64': 0.20.2
-      '@esbuild/darwin-arm64': 0.20.2
-      '@esbuild/darwin-x64': 0.20.2
-      '@esbuild/freebsd-arm64': 0.20.2
-      '@esbuild/freebsd-x64': 0.20.2
-      '@esbuild/linux-arm': 0.20.2
-      '@esbuild/linux-arm64': 0.20.2
-      '@esbuild/linux-ia32': 0.20.2
-      '@esbuild/linux-loong64': 0.20.2
-      '@esbuild/linux-mips64el': 0.20.2
-      '@esbuild/linux-ppc64': 0.20.2
-      '@esbuild/linux-riscv64': 0.20.2
-      '@esbuild/linux-s390x': 0.20.2
-      '@esbuild/linux-x64': 0.20.2
-      '@esbuild/netbsd-x64': 0.20.2
-      '@esbuild/openbsd-x64': 0.20.2
-      '@esbuild/sunos-x64': 0.20.2
-      '@esbuild/win32-arm64': 0.20.2
-      '@esbuild/win32-ia32': 0.20.2
-      '@esbuild/win32-x64': 0.20.2
+      '@esbuild/aix-ppc64': 0.21.5
+      '@esbuild/android-arm': 0.21.5
+      '@esbuild/android-arm64': 0.21.5
+      '@esbuild/android-x64': 0.21.5
+      '@esbuild/darwin-arm64': 0.21.5
+      '@esbuild/darwin-x64': 0.21.5
+      '@esbuild/freebsd-arm64': 0.21.5
+      '@esbuild/freebsd-x64': 0.21.5
+      '@esbuild/linux-arm': 0.21.5
+      '@esbuild/linux-arm64': 0.21.5
+      '@esbuild/linux-ia32': 0.21.5
+      '@esbuild/linux-loong64': 0.21.5
+      '@esbuild/linux-mips64el': 0.21.5
+      '@esbuild/linux-ppc64': 0.21.5
+      '@esbuild/linux-riscv64': 0.21.5
+      '@esbuild/linux-s390x': 0.21.5
+      '@esbuild/linux-x64': 0.21.5
+      '@esbuild/netbsd-x64': 0.21.5
+      '@esbuild/openbsd-x64': 0.21.5
+      '@esbuild/sunos-x64': 0.21.5
+      '@esbuild/win32-arm64': 0.21.5
+      '@esbuild/win32-ia32': 0.21.5
+      '@esbuild/win32-x64': 0.21.5
+
+  esbuild@0.23.0:
+    optionalDependencies:
+      '@esbuild/aix-ppc64': 0.23.0
+      '@esbuild/android-arm': 0.23.0
+      '@esbuild/android-arm64': 0.23.0
+      '@esbuild/android-x64': 0.23.0
+      '@esbuild/darwin-arm64': 0.23.0
+      '@esbuild/darwin-x64': 0.23.0
+      '@esbuild/freebsd-arm64': 0.23.0
+      '@esbuild/freebsd-x64': 0.23.0
+      '@esbuild/linux-arm': 0.23.0
+      '@esbuild/linux-arm64': 0.23.0
+      '@esbuild/linux-ia32': 0.23.0
+      '@esbuild/linux-loong64': 0.23.0
+      '@esbuild/linux-mips64el': 0.23.0
+      '@esbuild/linux-ppc64': 0.23.0
+      '@esbuild/linux-riscv64': 0.23.0
+      '@esbuild/linux-s390x': 0.23.0
+      '@esbuild/linux-x64': 0.23.0
+      '@esbuild/netbsd-x64': 0.23.0
+      '@esbuild/openbsd-arm64': 0.23.0
+      '@esbuild/openbsd-x64': 0.23.0
+      '@esbuild/sunos-x64': 0.23.0
+      '@esbuild/win32-arm64': 0.23.0
+      '@esbuild/win32-ia32': 0.23.0
+      '@esbuild/win32-x64': 0.23.0
+
+  esbuild@0.23.1:
+    optionalDependencies:
+      '@esbuild/aix-ppc64': 0.23.1
+      '@esbuild/android-arm': 0.23.1
+      '@esbuild/android-arm64': 0.23.1
+      '@esbuild/android-x64': 0.23.1
+      '@esbuild/darwin-arm64': 0.23.1
+      '@esbuild/darwin-x64': 0.23.1
+      '@esbuild/freebsd-arm64': 0.23.1
+      '@esbuild/freebsd-x64': 0.23.1
+      '@esbuild/linux-arm': 0.23.1
+      '@esbuild/linux-arm64': 0.23.1
+      '@esbuild/linux-ia32': 0.23.1
+      '@esbuild/linux-loong64': 0.23.1
+      '@esbuild/linux-mips64el': 0.23.1
+      '@esbuild/linux-ppc64': 0.23.1
+      '@esbuild/linux-riscv64': 0.23.1
+      '@esbuild/linux-s390x': 0.23.1
+      '@esbuild/linux-x64': 0.23.1
+      '@esbuild/netbsd-x64': 0.23.1
+      '@esbuild/openbsd-arm64': 0.23.1
+      '@esbuild/openbsd-x64': 0.23.1
+      '@esbuild/sunos-x64': 0.23.1
+      '@esbuild/win32-arm64': 0.23.1
+      '@esbuild/win32-ia32': 0.23.1
+      '@esbuild/win32-x64': 0.23.1
 
   escalade@3.1.1: {}
 
+  escape-goat@3.0.0: {}
+
   escape-html@1.0.3: {}
 
   escape-regexp@0.0.1: {}
@@ -18392,132 +18341,151 @@ snapshots:
   eslint-import-resolver-node@0.3.9:
     dependencies:
       debug: 3.2.7(supports-color@8.1.1)
-      is-core-module: 2.13.1
+      is-core-module: 2.15.1
       resolve: 1.22.8
     transitivePeerDependencies:
       - supports-color
 
-  eslint-module-utils@2.8.0(@typescript-eslint/parser@6.11.0(eslint@8.53.0)(typescript@5.3.3))(eslint-import-resolver-node@0.3.9)(eslint@8.53.0):
+  eslint-module-utils@2.11.0(@typescript-eslint/parser@7.17.0(eslint@9.11.0)(typescript@5.6.2))(eslint-import-resolver-node@0.3.9)(eslint@9.11.0):
     dependencies:
       debug: 3.2.7(supports-color@8.1.1)
     optionalDependencies:
-      '@typescript-eslint/parser': 6.11.0(eslint@8.53.0)(typescript@5.3.3)
-      eslint: 8.53.0
+      '@typescript-eslint/parser': 7.17.0(eslint@9.11.0)(typescript@5.6.2)
+      eslint: 9.11.0
       eslint-import-resolver-node: 0.3.9
     transitivePeerDependencies:
       - supports-color
 
-  eslint-module-utils@2.8.0(@typescript-eslint/parser@7.1.0(eslint@8.57.0)(typescript@5.3.3))(eslint-import-resolver-node@0.3.9)(eslint@8.57.0):
+  eslint-module-utils@2.12.0(@typescript-eslint/parser@7.17.0(eslint@9.11.0)(typescript@5.6.2))(eslint-import-resolver-node@0.3.9)(eslint@9.11.0):
     dependencies:
       debug: 3.2.7(supports-color@8.1.1)
     optionalDependencies:
-      '@typescript-eslint/parser': 7.1.0(eslint@8.57.0)(typescript@5.3.3)
-      eslint: 8.57.0
+      '@typescript-eslint/parser': 7.17.0(eslint@9.11.0)(typescript@5.6.2)
+      eslint: 9.11.0
       eslint-import-resolver-node: 0.3.9
     transitivePeerDependencies:
       - supports-color
 
-  eslint-module-utils@2.8.0(@typescript-eslint/parser@7.7.1(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint@8.57.0):
+  eslint-module-utils@2.12.0(@typescript-eslint/parser@7.17.0(eslint@9.8.0)(typescript@5.6.2))(eslint-import-resolver-node@0.3.9)(eslint@9.8.0):
     dependencies:
       debug: 3.2.7(supports-color@8.1.1)
     optionalDependencies:
-      '@typescript-eslint/parser': 7.7.1(eslint@8.57.0)(typescript@5.4.5)
-      eslint: 8.57.0
+      '@typescript-eslint/parser': 7.17.0(eslint@9.8.0)(typescript@5.6.2)
+      eslint: 9.8.0
       eslint-import-resolver-node: 0.3.9
     transitivePeerDependencies:
       - supports-color
 
-  eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.11.0(eslint@8.53.0)(typescript@5.3.3))(eslint@8.53.0):
+  eslint-plugin-import@2.30.0(@typescript-eslint/parser@7.17.0(eslint@9.11.0)(typescript@5.6.2))(eslint@9.11.0):
     dependencies:
-      array-includes: 3.1.7
-      array.prototype.findlastindex: 1.2.3
+      '@rtsao/scc': 1.1.0
+      array-includes: 3.1.8
+      array.prototype.findlastindex: 1.2.5
       array.prototype.flat: 1.3.2
       array.prototype.flatmap: 1.3.2
       debug: 3.2.7(supports-color@8.1.1)
       doctrine: 2.1.0
-      eslint: 8.53.0
+      eslint: 9.11.0
       eslint-import-resolver-node: 0.3.9
-      eslint-module-utils: 2.8.0(@typescript-eslint/parser@6.11.0(eslint@8.53.0)(typescript@5.3.3))(eslint-import-resolver-node@0.3.9)(eslint@8.53.0)
-      hasown: 2.0.0
-      is-core-module: 2.13.1
+      eslint-module-utils: 2.11.0(@typescript-eslint/parser@7.17.0(eslint@9.11.0)(typescript@5.6.2))(eslint-import-resolver-node@0.3.9)(eslint@9.11.0)
+      hasown: 2.0.2
+      is-core-module: 2.15.1
       is-glob: 4.0.3
       minimatch: 3.1.2
-      object.fromentries: 2.0.7
-      object.groupby: 1.0.1
-      object.values: 1.1.7
+      object.fromentries: 2.0.8
+      object.groupby: 1.0.3
+      object.values: 1.2.0
       semver: 6.3.1
       tsconfig-paths: 3.15.0
     optionalDependencies:
-      '@typescript-eslint/parser': 6.11.0(eslint@8.53.0)(typescript@5.3.3)
+      '@typescript-eslint/parser': 7.17.0(eslint@9.11.0)(typescript@5.6.2)
     transitivePeerDependencies:
       - eslint-import-resolver-typescript
       - eslint-import-resolver-webpack
       - supports-color
 
-  eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.1.0(eslint@8.57.0)(typescript@5.3.3))(eslint@8.57.0):
+  eslint-plugin-import@2.31.0(@typescript-eslint/parser@7.17.0(eslint@9.11.0)(typescript@5.6.2))(eslint@9.11.0):
     dependencies:
-      array-includes: 3.1.7
-      array.prototype.findlastindex: 1.2.3
+      '@rtsao/scc': 1.1.0
+      array-includes: 3.1.8
+      array.prototype.findlastindex: 1.2.5
       array.prototype.flat: 1.3.2
       array.prototype.flatmap: 1.3.2
       debug: 3.2.7(supports-color@8.1.1)
       doctrine: 2.1.0
-      eslint: 8.57.0
+      eslint: 9.11.0
       eslint-import-resolver-node: 0.3.9
-      eslint-module-utils: 2.8.0(@typescript-eslint/parser@7.1.0(eslint@8.57.0)(typescript@5.3.3))(eslint-import-resolver-node@0.3.9)(eslint@8.57.0)
-      hasown: 2.0.0
-      is-core-module: 2.13.1
+      eslint-module-utils: 2.12.0(@typescript-eslint/parser@7.17.0(eslint@9.11.0)(typescript@5.6.2))(eslint-import-resolver-node@0.3.9)(eslint@9.11.0)
+      hasown: 2.0.2
+      is-core-module: 2.15.1
       is-glob: 4.0.3
       minimatch: 3.1.2
-      object.fromentries: 2.0.7
-      object.groupby: 1.0.1
-      object.values: 1.1.7
+      object.fromentries: 2.0.8
+      object.groupby: 1.0.3
+      object.values: 1.2.0
       semver: 6.3.1
+      string.prototype.trimend: 1.0.8
       tsconfig-paths: 3.15.0
     optionalDependencies:
-      '@typescript-eslint/parser': 7.1.0(eslint@8.57.0)(typescript@5.3.3)
+      '@typescript-eslint/parser': 7.17.0(eslint@9.11.0)(typescript@5.6.2)
     transitivePeerDependencies:
       - eslint-import-resolver-typescript
       - eslint-import-resolver-webpack
       - supports-color
 
-  eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.7.1(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0):
+  eslint-plugin-import@2.31.0(@typescript-eslint/parser@7.17.0(eslint@9.8.0)(typescript@5.6.2))(eslint@9.8.0):
     dependencies:
-      array-includes: 3.1.7
-      array.prototype.findlastindex: 1.2.3
+      '@rtsao/scc': 1.1.0
+      array-includes: 3.1.8
+      array.prototype.findlastindex: 1.2.5
       array.prototype.flat: 1.3.2
       array.prototype.flatmap: 1.3.2
       debug: 3.2.7(supports-color@8.1.1)
       doctrine: 2.1.0
-      eslint: 8.57.0
+      eslint: 9.8.0
       eslint-import-resolver-node: 0.3.9
-      eslint-module-utils: 2.8.0(@typescript-eslint/parser@7.7.1(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint@8.57.0)
-      hasown: 2.0.0
-      is-core-module: 2.13.1
+      eslint-module-utils: 2.12.0(@typescript-eslint/parser@7.17.0(eslint@9.8.0)(typescript@5.6.2))(eslint-import-resolver-node@0.3.9)(eslint@9.8.0)
+      hasown: 2.0.2
+      is-core-module: 2.15.1
       is-glob: 4.0.3
       minimatch: 3.1.2
-      object.fromentries: 2.0.7
-      object.groupby: 1.0.1
-      object.values: 1.1.7
+      object.fromentries: 2.0.8
+      object.groupby: 1.0.3
+      object.values: 1.2.0
       semver: 6.3.1
+      string.prototype.trimend: 1.0.8
       tsconfig-paths: 3.15.0
     optionalDependencies:
-      '@typescript-eslint/parser': 7.7.1(eslint@8.57.0)(typescript@5.4.5)
+      '@typescript-eslint/parser': 7.17.0(eslint@9.8.0)(typescript@5.6.2)
     transitivePeerDependencies:
       - eslint-import-resolver-typescript
       - eslint-import-resolver-webpack
       - supports-color
 
-  eslint-plugin-vue@9.25.0(eslint@8.57.0):
+  eslint-plugin-vue@9.27.0(eslint@9.11.0):
     dependencies:
-      '@eslint-community/eslint-utils': 4.4.0(eslint@8.57.0)
-      eslint: 8.57.0
+      '@eslint-community/eslint-utils': 4.4.0(eslint@9.11.0)
+      eslint: 9.11.0
       globals: 13.24.0
       natural-compare: 1.4.0
       nth-check: 2.1.1
-      postcss-selector-parser: 6.0.15
+      postcss-selector-parser: 6.0.16
       semver: 7.6.0
-      vue-eslint-parser: 9.4.2(eslint@8.57.0)
+      vue-eslint-parser: 9.4.3(eslint@9.11.0)
+      xml-name-validator: 4.0.0
+    transitivePeerDependencies:
+      - supports-color
+
+  eslint-plugin-vue@9.28.0(eslint@9.11.0):
+    dependencies:
+      '@eslint-community/eslint-utils': 4.4.0(eslint@9.11.0)
+      eslint: 9.11.0
+      globals: 13.24.0
+      natural-compare: 1.4.0
+      nth-check: 2.1.1
+      postcss-selector-parser: 6.0.16
+      semver: 7.6.3
+      vue-eslint-parser: 9.4.3(eslint@9.11.0)
       xml-name-validator: 4.0.0
     transitivePeerDependencies:
       - supports-color
@@ -18529,103 +18497,108 @@ snapshots:
       esrecurse: 4.3.0
       estraverse: 5.3.0
 
+  eslint-scope@8.0.2:
+    dependencies:
+      esrecurse: 4.3.0
+      estraverse: 5.3.0
+
   eslint-visitor-keys@3.4.3: {}
 
-  eslint@8.53.0:
+  eslint-visitor-keys@4.0.0: {}
+
+  eslint@9.11.0:
     dependencies:
-      '@eslint-community/eslint-utils': 4.4.0(eslint@8.53.0)
-      '@eslint-community/regexpp': 4.6.2
-      '@eslint/eslintrc': 2.1.4
-      '@eslint/js': 8.53.0
-      '@humanwhocodes/config-array': 0.11.13
+      '@eslint-community/eslint-utils': 4.4.0(eslint@9.11.0)
+      '@eslint-community/regexpp': 4.11.0
+      '@eslint/config-array': 0.18.0
+      '@eslint/eslintrc': 3.1.0
+      '@eslint/js': 9.11.0
+      '@eslint/plugin-kit': 0.2.0
       '@humanwhocodes/module-importer': 1.0.1
+      '@humanwhocodes/retry': 0.3.0
       '@nodelib/fs.walk': 1.2.8
-      '@ungap/structured-clone': 1.2.0
       ajv: 6.12.6
       chalk: 4.1.2
       cross-spawn: 7.0.3
-      debug: 4.3.4(supports-color@8.1.1)
-      doctrine: 3.0.0
+      debug: 4.3.7(supports-color@8.1.1)
       escape-string-regexp: 4.0.0
-      eslint-scope: 7.2.2
-      eslint-visitor-keys: 3.4.3
-      espree: 9.6.1
-      esquery: 1.4.2
+      eslint-scope: 8.0.2
+      eslint-visitor-keys: 4.0.0
+      espree: 10.1.0
+      esquery: 1.6.0
       esutils: 2.0.3
       fast-deep-equal: 3.1.3
-      file-entry-cache: 6.0.1
+      file-entry-cache: 8.0.0
       find-up: 5.0.0
       glob-parent: 6.0.2
-      globals: 13.19.0
-      graphemer: 1.4.0
-      ignore: 5.2.4
+      ignore: 5.3.1
       imurmurhash: 0.1.4
       is-glob: 4.0.3
       is-path-inside: 3.0.3
-      js-yaml: 4.1.0
       json-stable-stringify-without-jsonify: 1.0.1
-      levn: 0.4.1
       lodash.merge: 4.6.2
       minimatch: 3.1.2
       natural-compare: 1.4.0
-      optionator: 0.9.3
+      optionator: 0.9.4
       strip-ansi: 6.0.1
       text-table: 0.2.0
     transitivePeerDependencies:
       - supports-color
 
-  eslint@8.57.0:
+  eslint@9.8.0:
     dependencies:
-      '@eslint-community/eslint-utils': 4.4.0(eslint@8.57.0)
-      '@eslint-community/regexpp': 4.6.2
-      '@eslint/eslintrc': 2.1.4
-      '@eslint/js': 8.57.0
-      '@humanwhocodes/config-array': 0.11.14
+      '@eslint-community/eslint-utils': 4.4.0(eslint@9.8.0)
+      '@eslint-community/regexpp': 4.11.0
+      '@eslint/config-array': 0.17.1
+      '@eslint/eslintrc': 3.1.0
+      '@eslint/js': 9.8.0
       '@humanwhocodes/module-importer': 1.0.1
+      '@humanwhocodes/retry': 0.3.0
       '@nodelib/fs.walk': 1.2.8
-      '@ungap/structured-clone': 1.2.0
       ajv: 6.12.6
       chalk: 4.1.2
       cross-spawn: 7.0.3
-      debug: 4.3.4(supports-color@8.1.1)
-      doctrine: 3.0.0
+      debug: 4.3.7(supports-color@8.1.1)
       escape-string-regexp: 4.0.0
-      eslint-scope: 7.2.2
-      eslint-visitor-keys: 3.4.3
-      espree: 9.6.1
-      esquery: 1.4.2
+      eslint-scope: 8.0.2
+      eslint-visitor-keys: 4.0.0
+      espree: 10.1.0
+      esquery: 1.6.0
       esutils: 2.0.3
       fast-deep-equal: 3.1.3
-      file-entry-cache: 6.0.1
+      file-entry-cache: 8.0.0
       find-up: 5.0.0
       glob-parent: 6.0.2
-      globals: 13.19.0
-      graphemer: 1.4.0
-      ignore: 5.2.4
+      ignore: 5.3.1
       imurmurhash: 0.1.4
       is-glob: 4.0.3
       is-path-inside: 3.0.3
-      js-yaml: 4.1.0
       json-stable-stringify-without-jsonify: 1.0.1
       levn: 0.4.1
       lodash.merge: 4.6.2
       minimatch: 3.1.2
       natural-compare: 1.4.0
-      optionator: 0.9.3
+      optionator: 0.9.4
       strip-ansi: 6.0.1
       text-table: 0.2.0
     transitivePeerDependencies:
       - supports-color
 
+  espree@10.1.0:
+    dependencies:
+      acorn: 8.12.1
+      acorn-jsx: 5.3.2(acorn@8.12.1)
+      eslint-visitor-keys: 4.0.0
+
   espree@9.6.1:
     dependencies:
-      acorn: 8.11.3
-      acorn-jsx: 5.3.2(acorn@8.11.3)
+      acorn: 8.12.1
+      acorn-jsx: 5.3.2(acorn@8.12.1)
       eslint-visitor-keys: 3.4.3
 
   esprima@4.0.1: {}
 
-  esquery@1.4.2:
+  esquery@1.6.0:
     dependencies:
       estraverse: 5.3.0
 
@@ -18639,7 +18612,7 @@ snapshots:
 
   estree-walker@3.0.3:
     dependencies:
-      '@types/estree': 1.0.5
+      '@types/estree': 1.0.6
 
   esutils@2.0.3: {}
 
@@ -18706,7 +18679,7 @@ snapshots:
       human-signals: 3.0.1
       is-stream: 3.0.0
       merge-stream: 2.0.0
-      npm-run-path: 5.1.0
+      npm-run-path: 5.3.0
       onetime: 6.0.0
       signal-exit: 3.0.7
       strip-final-newline: 3.0.0
@@ -18723,6 +18696,21 @@ snapshots:
       signal-exit: 4.1.0
       strip-final-newline: 3.0.0
 
+  execa@9.4.0:
+    dependencies:
+      '@sindresorhus/merge-streams': 4.0.0
+      cross-spawn: 7.0.3
+      figures: 6.1.0
+      get-stream: 9.0.1
+      human-signals: 8.0.0
+      is-plain-obj: 4.1.0
+      is-stream: 4.0.1
+      npm-run-path: 6.0.0
+      pretty-ms: 9.0.0
+      signal-exit: 4.1.0
+      strip-final-newline: 4.0.0
+      yoctocolors: 2.0.2
+
   executable@4.1.1:
     dependencies:
       pify: 2.3.0
@@ -18739,70 +18727,34 @@ snapshots:
 
   exponential-backoff@3.1.1: {}
 
-  express@4.18.2:
+  express@4.21.0:
     dependencies:
       accepts: 1.3.8
       array-flatten: 1.1.1
-      body-parser: 1.20.1
-      content-disposition: 0.5.4
-      content-type: 1.0.5
-      cookie: 0.5.0
-      cookie-signature: 1.0.6
-      debug: 2.6.9
-      depd: 2.0.0
-      encodeurl: 1.0.2
-      escape-html: 1.0.3
-      etag: 1.8.1
-      finalhandler: 1.2.0
-      fresh: 0.5.2
-      http-errors: 2.0.0
-      merge-descriptors: 1.0.1
-      methods: 1.1.2
-      on-finished: 2.4.1
-      parseurl: 1.3.3
-      path-to-regexp: 0.1.7
-      proxy-addr: 2.0.7
-      qs: 6.11.0
-      range-parser: 1.2.1
-      safe-buffer: 5.2.1
-      send: 0.18.0
-      serve-static: 1.15.0
-      setprototypeof: 1.2.0
-      statuses: 2.0.1
-      type-is: 1.6.18
-      utils-merge: 1.0.1
-      vary: 1.1.2
-    transitivePeerDependencies:
-      - supports-color
-
-  express@4.19.2:
-    dependencies:
-      accepts: 1.3.8
-      array-flatten: 1.1.1
-      body-parser: 1.20.2
+      body-parser: 1.20.3
       content-disposition: 0.5.4
       content-type: 1.0.5
       cookie: 0.6.0
       cookie-signature: 1.0.6
       debug: 2.6.9
       depd: 2.0.0
-      encodeurl: 1.0.2
+      encodeurl: 2.0.0
       escape-html: 1.0.3
       etag: 1.8.1
-      finalhandler: 1.2.0
+      finalhandler: 1.3.1
       fresh: 0.5.2
       http-errors: 2.0.0
-      merge-descriptors: 1.0.1
+      merge-descriptors: 1.0.3
       methods: 1.1.2
       on-finished: 2.4.1
       parseurl: 1.3.3
-      path-to-regexp: 0.1.7
+      path-to-regexp: 0.1.10
       proxy-addr: 2.0.7
-      qs: 6.11.0
+      qs: 6.13.0
       range-parser: 1.2.1
       safe-buffer: 5.2.1
-      send: 0.18.0
-      serve-static: 1.15.0
+      send: 0.19.0
+      serve-static: 1.16.2
       setprototypeof: 1.2.0
       statuses: 2.0.1
       type-is: 1.6.18
@@ -18824,7 +18776,7 @@ snapshots:
 
   extract-zip@2.0.1(supports-color@8.1.1):
     dependencies:
-      debug: 4.3.4(supports-color@8.1.1)
+      debug: 4.3.5(supports-color@8.1.1)
       get-stream: 5.2.0
       yauzl: 2.10.0
     optionalDependencies:
@@ -18834,7 +18786,7 @@ snapshots:
 
   extsprintf@1.3.0: {}
 
-  fast-content-type-parse@1.1.0: {}
+  fast-content-type-parse@2.0.0: {}
 
   fast-decode-uri-component@1.0.1: {}
 
@@ -18848,18 +18800,19 @@ snapshots:
       '@nodelib/fs.walk': 1.2.8
       glob-parent: 5.1.2
       merge2: 1.4.1
-      micromatch: 4.0.5
+      micromatch: 4.0.8
 
   fast-json-stable-stringify@2.1.0: {}
 
-  fast-json-stringify@5.8.0:
+  fast-json-stringify@6.0.0:
     dependencies:
-      '@fastify/deepmerge': 1.3.0
-      ajv: 8.13.0
-      ajv-formats: 2.1.1(ajv@8.13.0)
+      '@fastify/merge-json-schemas': 0.1.1
+      ajv: 8.17.1
+      ajv-formats: 3.0.1(ajv@8.17.1)
       fast-deep-equal: 3.1.3
-      fast-uri: 2.2.0
-      rfdc: 1.3.0
+      fast-uri: 2.4.0
+      json-schema-ref-resolver: 1.0.1
+      rfdc: 1.4.1
 
   fast-levenshtein@2.0.6: {}
 
@@ -18871,44 +18824,45 @@ snapshots:
 
   fast-safe-stringify@2.1.1: {}
 
-  fast-uri@2.2.0: {}
+  fast-uri@2.4.0: {}
+
+  fast-uri@3.0.1: {}
 
   fast-xml-parser@4.2.5:
     dependencies:
       strnum: 1.0.5
 
-  fastify-plugin@4.5.0: {}
-
-  fastify-raw-body@4.3.0:
+  fast-xml-parser@4.5.0:
     dependencies:
-      fastify-plugin: 4.5.0
-      raw-body: 2.5.2
+      strnum: 1.0.5
+
+  fastify-plugin@4.5.1: {}
+
+  fastify-plugin@5.0.0: {}
+
+  fastify-raw-body@5.0.0:
+    dependencies:
+      fastify-plugin: 5.0.0
+      raw-body: 3.0.0
       secure-json-parse: 2.7.0
 
-  fastify@4.26.2:
+  fastify@5.0.0:
     dependencies:
-      '@fastify/ajv-compiler': 3.5.0
-      '@fastify/error': 3.4.0
-      '@fastify/fast-json-stringify-compiler': 4.3.0
+      '@fastify/ajv-compiler': 4.0.0
+      '@fastify/error': 4.0.0
+      '@fastify/fast-json-stringify-compiler': 5.0.0
       abstract-logging: 2.0.1
-      avvio: 8.3.0
-      fast-content-type-parse: 1.1.0
-      fast-json-stringify: 5.8.0
-      find-my-way: 8.2.0
-      light-my-request: 5.11.0
-      pino: 8.17.0
-      process-warning: 3.0.0
+      avvio: 9.0.0
+      fast-json-stringify: 6.0.0
+      find-my-way: 9.0.1
+      light-my-request: 6.0.0
+      pino: 9.2.0
+      process-warning: 4.0.0
       proxy-addr: 2.0.7
-      rfdc: 1.3.0
+      rfdc: 1.4.1
       secure-json-parse: 2.7.0
       semver: 7.6.0
-      toad-cache: 3.3.0
-    transitivePeerDependencies:
-      - supports-color
-
-  fastq@1.15.0:
-    dependencies:
-      reusify: 1.0.4
+      toad-cache: 3.7.0
 
   fastq@1.17.1:
     dependencies:
@@ -18931,20 +18885,17 @@ snapshots:
       node-domexception: 1.0.0
       web-streams-polyfill: 3.2.1
 
-  fetch-retry@5.0.4: {}
-
   figures@3.2.0:
     dependencies:
       escape-string-regexp: 1.0.5
 
-  file-entry-cache@6.0.1:
+  figures@6.1.0:
     dependencies:
-      flat-cache: 3.0.4
+      is-unicode-supported: 2.0.0
 
-  file-system-cache@2.3.0:
+  file-entry-cache@8.0.0:
     dependencies:
-      fs-extra: 11.1.1
-      ramda: 0.29.0
+      flat-cache: 4.0.1
 
   file-type@17.1.6:
     dependencies:
@@ -18952,11 +18903,12 @@ snapshots:
       strtok3: 7.0.0
       token-types: 5.0.1
 
-  file-type@19.0.0:
+  file-type@19.5.0:
     dependencies:
-      readable-web-to-node-stream: 3.0.2
-      strtok3: 7.0.0
-      token-types: 5.0.1
+      get-stream: 9.0.1
+      strtok3: 8.1.0
+      token-types: 6.0.0
+      uint8array-extras: 1.4.0
 
   filelist@1.0.4:
     dependencies:
@@ -18974,10 +18926,14 @@ snapshots:
     dependencies:
       to-regex-range: 5.0.1
 
-  finalhandler@1.2.0:
+  fill-range@7.1.1:
+    dependencies:
+      to-regex-range: 5.0.1
+
+  finalhandler@1.3.1:
     dependencies:
       debug: 2.6.9
-      encodeurl: 1.0.2
+      encodeurl: 2.0.0
       escape-html: 1.0.3
       on-finished: 2.4.1
       parseurl: 1.3.3
@@ -18986,30 +18942,20 @@ snapshots:
     transitivePeerDependencies:
       - supports-color
 
-  find-cache-dir@2.1.0:
-    dependencies:
-      commondir: 1.0.1
-      make-dir: 2.1.0
-      pkg-dir: 3.0.0
-
   find-cache-dir@3.3.2:
     dependencies:
       commondir: 1.0.1
       make-dir: 3.1.0
       pkg-dir: 4.2.0
 
-  find-my-way@8.2.0:
+  find-my-way@9.0.1:
     dependencies:
       fast-deep-equal: 3.1.3
       fast-querystring: 1.1.2
-      safe-regex2: 3.1.0
+      safe-regex2: 4.0.0
 
   find-package-json@1.2.0: {}
 
-  find-up@3.0.0:
-    dependencies:
-      locate-path: 3.0.0
-
   find-up@4.1.0:
     dependencies:
       locate-path: 5.0.0
@@ -19033,23 +18979,23 @@ snapshots:
       ps-list: 8.1.1
       taskkill: 5.0.0
 
-  flat-cache@3.0.4:
+  flat-cache@4.0.1:
     dependencies:
-      flatted: 3.2.7
-      rimraf: 3.0.2
+      flatted: 3.3.1
+      keyv: 4.5.4
 
-  flatted@3.2.7: {}
+  flatted@3.3.1: {}
 
-  flow-parser@0.202.0: {}
-
-  fluent-ffmpeg@2.1.2:
+  fluent-ffmpeg@2.1.3:
     dependencies:
-      async: 3.2.4
+      async: 0.2.10
       which: 1.3.1
 
-  follow-redirects@1.15.2(debug@4.3.4):
+  follow-redirects@1.15.2: {}
+
+  follow-redirects@1.15.9(debug@4.3.7):
     optionalDependencies:
-      debug: 4.3.4(supports-color@8.1.1)
+      debug: 4.3.7(supports-color@8.1.1)
 
   for-each@0.3.3:
     dependencies:
@@ -19072,12 +19018,6 @@ snapshots:
       combined-stream: 1.0.8
       mime-types: 2.1.35
 
-  form-data@3.0.1:
-    dependencies:
-      asynckit: 0.4.0
-      combined-stream: 1.0.8
-      mime-types: 2.1.35
-
   form-data@4.0.0:
     dependencies:
       asynckit: 0.4.0
@@ -19094,8 +19034,6 @@ snapshots:
 
   from@0.1.7: {}
 
-  fs-constants@1.0.0: {}
-
   fs-extra@11.1.1:
     dependencies:
       graceful-fs: 4.2.11
@@ -19130,9 +19068,9 @@ snapshots:
     dependencies:
       minipass: 3.3.6
 
-  fs-minipass@3.0.2:
+  fs-minipass@3.0.3:
     dependencies:
-      minipass: 5.0.0
+      minipass: 7.1.2
 
   fs.realpath@1.0.0: {}
 
@@ -19143,11 +19081,18 @@ snapshots:
 
   function.prototype.name@1.1.5:
     dependencies:
-      call-bind: 1.0.2
+      call-bind: 1.0.7
       define-properties: 1.2.0
       es-abstract: 1.22.1
       functions-have-names: 1.2.3
 
+  function.prototype.name@1.1.6:
+    dependencies:
+      call-bind: 1.0.7
+      define-properties: 1.2.1
+      es-abstract: 1.23.3
+      functions-have-names: 1.2.3
+
   functions-have-names@1.2.3: {}
 
   gauge@3.0.2:
@@ -19176,7 +19121,13 @@ snapshots:
       has-proto: 1.0.1
       has-symbols: 1.0.3
 
-  get-npm-tarball-url@2.0.3: {}
+  get-intrinsic@1.2.4:
+    dependencies:
+      es-errors: 1.3.0
+      function-bind: 1.1.2
+      has-proto: 1.0.1
+      has-symbols: 1.0.3
+      hasown: 2.0.0
 
   get-package-type@0.1.0: {}
 
@@ -19204,10 +19155,21 @@ snapshots:
 
   get-stream@8.0.1: {}
 
+  get-stream@9.0.1:
+    dependencies:
+      '@sec-ant/readable-stream': 0.4.1
+      is-stream: 4.0.1
+
   get-symbol-description@1.0.0:
     dependencies:
-      call-bind: 1.0.2
-      get-intrinsic: 1.2.1
+      call-bind: 1.0.7
+      get-intrinsic: 1.2.4
+
+  get-symbol-description@1.0.2:
+    dependencies:
+      call-bind: 1.0.7
+      es-errors: 1.3.0
+      get-intrinsic: 1.2.4
 
   get-tsconfig@4.7.2:
     dependencies:
@@ -19225,18 +19187,6 @@ snapshots:
     dependencies:
       readable-stream: 1.1.14
 
-  giget@1.1.2:
-    dependencies:
-      colorette: 2.0.19
-      defu: 6.1.4
-      https-proxy-agent: 5.0.1
-      mri: 1.2.0
-      node-fetch-native: 1.0.2
-      pathe: 1.1.2
-      tar: 6.2.1
-    transitivePeerDependencies:
-      - supports-color
-
   github-slugger@2.0.0: {}
 
   glob-parent@5.1.2:
@@ -19252,8 +19202,6 @@ snapshots:
       '@types/glob': 7.2.0
       glob: 7.2.3
 
-  glob-to-regexp@0.4.1: {}
-
   glob@10.3.10:
     dependencies:
       foreground-child: 3.1.1
@@ -19262,13 +19210,14 @@ snapshots:
       minipass: 7.0.4
       path-scurry: 1.10.1
 
-  glob@10.3.12:
+  glob@11.0.0:
     dependencies:
       foreground-child: 3.1.1
-      jackspeak: 2.3.6
-      minimatch: 9.0.3
-      minipass: 7.0.4
-      path-scurry: 1.10.2
+      jackspeak: 4.0.1
+      minimatch: 10.0.1
+      minipass: 7.1.2
+      package-json-from-dist: 1.0.0
+      path-scurry: 2.0.0
 
   glob@7.2.3:
     dependencies:
@@ -19293,14 +19242,14 @@ snapshots:
 
   globals@11.12.0: {}
 
-  globals@13.19.0:
-    dependencies:
-      type-fest: 0.20.2
-
   globals@13.24.0:
     dependencies:
       type-fest: 0.20.2
 
+  globals@14.0.0: {}
+
+  globals@15.9.0: {}
+
   globalthis@1.0.3:
     dependencies:
       define-properties: 1.2.0
@@ -19319,7 +19268,7 @@ snapshots:
 
   gopd@1.0.1:
     dependencies:
-      get-intrinsic: 1.2.1
+      get-intrinsic: 1.2.4
 
   got@11.8.5:
     dependencies:
@@ -19349,19 +19298,19 @@ snapshots:
       p-cancelable: 3.0.0
       responselike: 3.0.0
 
-  got@14.2.1:
+  got@14.4.2:
     dependencies:
-      '@sindresorhus/is': 6.1.0
+      '@sindresorhus/is': 7.0.0
       '@szmarczak/http-timer': 5.0.1
       cacheable-lookup: 7.0.0
-      cacheable-request: 10.2.14
+      cacheable-request: 12.0.1
       decompress-response: 6.0.0
       form-data-encoder: 4.0.2
-      get-stream: 8.0.1
       http2-wrapper: 2.2.1
       lowercase-keys: 3.0.0
       p-cancelable: 4.0.1
       responselike: 3.0.0
+      type-fest: 4.20.1
 
   graceful-fs@4.2.11: {}
 
@@ -19371,26 +19320,8 @@ snapshots:
 
   graphql@16.8.1: {}
 
-  gunzip-maybe@1.4.2:
-    dependencies:
-      browserify-zlib: 0.1.4
-      is-deflate: 1.0.0
-      is-gzip: 1.0.0
-      peek-stream: 1.1.3
-      pumpify: 1.5.1
-      through2: 2.0.5
-
   hammerjs@2.0.8: {}
 
-  handlebars@4.7.7:
-    dependencies:
-      minimist: 1.2.8
-      neo-async: 2.6.2
-      source-map: 0.6.1
-      wordwrap: 1.0.0
-    optionalDependencies:
-      uglify-js: 3.17.4
-
   happy-dom@10.0.3:
     dependencies:
       css.escape: 1.5.1
@@ -19400,6 +19331,12 @@ snapshots:
       whatwg-encoding: 2.0.0
       whatwg-mimetype: 3.0.0
 
+  happy-dom@15.7.4:
+    dependencies:
+      entities: 4.5.0
+      webidl-conversions: 7.0.0
+      whatwg-mimetype: 3.0.0
+
   har-schema@2.0.0: {}
 
   har-validator@5.1.5:
@@ -19419,14 +19356,24 @@ snapshots:
     dependencies:
       get-intrinsic: 1.2.1
 
+  has-property-descriptors@1.0.2:
+    dependencies:
+      es-define-property: 1.0.0
+
   has-proto@1.0.1: {}
 
+  has-proto@1.0.3: {}
+
   has-symbols@1.0.3: {}
 
   has-tostringtag@1.0.0:
     dependencies:
       has-symbols: 1.0.3
 
+  has-tostringtag@1.0.2:
+    dependencies:
+      has-symbols: 1.0.3
+
   has-unicode@2.0.1:
     optional: true
 
@@ -19436,12 +19383,14 @@ snapshots:
 
   hash-sum@2.0.0: {}
 
-  hashlru@2.3.0: {}
-
   hasown@2.0.0:
     dependencies:
       function-bind: 1.1.2
 
+  hasown@2.0.2:
+    dependencies:
+      function-bind: 1.1.2
+
   hast-util-heading-rank@3.0.0:
     dependencies:
       '@types/hast': 3.0.4
@@ -19450,17 +19399,35 @@ snapshots:
     dependencies:
       '@types/hast': 3.0.4
 
+  hast-util-to-html@9.0.3:
+    dependencies:
+      '@types/hast': 3.0.4
+      '@types/unist': 3.0.2
+      ccount: 2.0.1
+      comma-separated-tokens: 2.0.3
+      hast-util-whitespace: 3.0.0
+      html-void-elements: 3.0.0
+      mdast-util-to-hast: 13.2.0
+      property-information: 6.5.0
+      space-separated-tokens: 2.0.2
+      stringify-entities: 4.0.4
+      zwitch: 2.0.4
+
   hast-util-to-string@3.0.0:
     dependencies:
       '@types/hast': 3.0.4
 
+  hast-util-whitespace@3.0.0:
+    dependencies:
+      '@types/hast': 3.0.4
+
   he@1.2.0: {}
 
   headers-polyfill@4.0.2: {}
 
   highlight.js@10.7.3: {}
 
-  highlight.js@11.9.0: {}
+  highlight.js@11.10.0: {}
 
   hosted-git-info@2.8.9: {}
 
@@ -19480,8 +19447,17 @@ snapshots:
 
   html-tags@3.2.0: {}
 
+  html-void-elements@3.0.0: {}
+
   htmlescape@1.1.1: {}
 
+  htmlparser2@5.0.1:
+    dependencies:
+      domelementtype: 2.3.0
+      domhandler: 3.3.0
+      domutils: 2.8.0
+      entities: 2.2.0
+
   htmlparser2@8.0.1:
     dependencies:
       domelementtype: 2.3.0
@@ -19489,6 +19465,13 @@ snapshots:
       domutils: 3.0.1
       entities: 4.5.0
 
+  htmlparser2@9.1.0:
+    dependencies:
+      domelementtype: 2.3.0
+      domhandler: 5.0.3
+      domutils: 3.1.0
+      entities: 4.5.0
+
   http-cache-semantics@4.1.1: {}
 
   http-errors@2.0.0:
@@ -19501,10 +19484,10 @@ snapshots:
 
   http-link-header@1.1.3: {}
 
-  http-proxy-agent@7.0.0:
+  http-proxy-agent@7.0.2:
     dependencies:
       agent-base: 7.1.0
-      debug: 4.3.4(supports-color@8.1.1)
+      debug: 4.3.5(supports-color@5.5.0)
     transitivePeerDependencies:
       - supports-color
 
@@ -19514,11 +19497,11 @@ snapshots:
       jsprim: 1.4.2
       sshpk: 1.17.0
 
-  http-signature@1.3.6:
+  http-signature@1.4.0:
     dependencies:
       assert-plus: 1.0.0
       jsprim: 2.0.2
-      sshpk: 1.17.0
+      sshpk: 1.18.0
 
   http2-wrapper@1.0.3:
     dependencies:
@@ -19543,14 +19526,22 @@ snapshots:
   https-proxy-agent@5.0.1:
     dependencies:
       agent-base: 6.0.2
-      debug: 4.3.4(supports-color@8.1.1)
+      debug: 4.3.5(supports-color@5.5.0)
     transitivePeerDependencies:
       - supports-color
+    optional: true
 
   https-proxy-agent@7.0.2:
     dependencies:
       agent-base: 7.1.0
-      debug: 4.3.4(supports-color@8.1.1)
+      debug: 4.3.5(supports-color@5.5.0)
+    transitivePeerDependencies:
+      - supports-color
+
+  https-proxy-agent@7.0.5:
+    dependencies:
+      agent-base: 7.1.0
+      debug: 4.3.5(supports-color@5.5.0)
     transitivePeerDependencies:
       - supports-color
 
@@ -19562,6 +19553,8 @@ snapshots:
 
   human-signals@5.0.0: {}
 
+  human-signals@8.0.0: {}
+
   iconv-lite@0.4.24:
     dependencies:
       safer-buffer: 2.1.2
@@ -19576,9 +19569,9 @@ snapshots:
 
   ignore-by-default@1.0.1: {}
 
-  ignore-walk@6.0.4:
+  ignore-walk@6.0.5:
     dependencies:
-      minimatch: 9.0.3
+      minimatch: 9.0.4
 
   ignore@5.2.4: {}
 
@@ -19591,21 +19584,21 @@ snapshots:
       parent-module: 1.0.1
       resolve-from: 4.0.0
 
-  import-in-the-middle@1.4.2:
+  import-in-the-middle@1.10.0:
     dependencies:
-      acorn: 8.11.3
-      acorn-import-assertions: 1.9.0(acorn@8.11.3)
+      acorn: 8.12.1
+      acorn-import-attributes: 1.9.5(acorn@8.12.1)
+      cjs-module-lexer: 1.2.2
+      module-details-from-path: 1.0.3
+
+  import-in-the-middle@1.7.1:
+    dependencies:
+      acorn: 8.12.1
+      acorn-import-assertions: 1.9.0(acorn@8.12.1)
       cjs-module-lexer: 1.2.2
       module-details-from-path: 1.0.3
     optional: true
 
-  import-in-the-middle@1.7.4:
-    dependencies:
-      acorn: 8.11.3
-      acorn-import-attributes: 1.9.5(acorn@8.11.3)
-      cjs-module-lexer: 1.2.2
-      module-details-from-path: 1.0.3
-
   import-lazy@4.0.0: {}
 
   import-local@3.1.0:
@@ -19640,13 +19633,19 @@ snapshots:
       has: 1.0.3
       side-channel: 1.0.4
 
+  internal-slot@1.0.7:
+    dependencies:
+      es-errors: 1.3.0
+      hasown: 2.0.2
+      side-channel: 1.0.6
+
   intersection-observer@0.12.2: {}
 
   ioredis@5.4.1:
     dependencies:
       '@ioredis/commands': 1.2.0
       cluster-key-slot: 1.1.2
-      debug: 4.3.4(supports-color@8.1.1)
+      debug: 4.3.4(supports-color@5.5.0)
       denque: 2.1.0
       lodash.defaults: 4.2.0
       lodash.isarguments: 3.1.0
@@ -19658,15 +19657,14 @@ snapshots:
 
   iota-array@1.0.0: {}
 
-  ip-address@7.1.0:
+  ip-address@9.0.5:
     dependencies:
       jsbn: 1.1.0
-      sprintf-js: 1.1.2
+      sprintf-js: 1.1.3
 
-  ip-cidr@3.1.0:
+  ip-cidr@4.0.2:
     dependencies:
-      ip-address: 7.1.0
-      jsbn: 1.1.0
+      ip-address: 9.0.5
 
   ip-regex@4.3.0: {}
 
@@ -19691,6 +19689,11 @@ snapshots:
       get-intrinsic: 1.2.1
       is-typed-array: 1.1.10
 
+  is-array-buffer@3.0.4:
+    dependencies:
+      call-bind: 1.0.7
+      get-intrinsic: 1.2.4
+
   is-arrayish@0.2.1: {}
 
   is-arrayish@0.3.2: {}
@@ -19705,7 +19708,7 @@ snapshots:
 
   is-boolean-object@1.1.2:
     dependencies:
-      call-bind: 1.0.2
+      call-bind: 1.0.7
       has-tostringtag: 1.0.0
 
   is-buffer@1.1.6: {}
@@ -19720,12 +19723,18 @@ snapshots:
     dependencies:
       hasown: 2.0.0
 
+  is-core-module@2.15.1:
+    dependencies:
+      hasown: 2.0.2
+
+  is-data-view@1.0.1:
+    dependencies:
+      is-typed-array: 1.1.13
+
   is-date-object@1.0.5:
     dependencies:
       has-tostringtag: 1.0.0
 
-  is-deflate@1.0.0: {}
-
   is-docker@2.2.1: {}
 
   is-expression@4.0.0:
@@ -19743,21 +19752,17 @@ snapshots:
 
   is-generator-function@1.0.10:
     dependencies:
-      has-tostringtag: 1.0.0
+      has-tostringtag: 1.0.2
 
   is-glob@4.0.3:
     dependencies:
       is-extglob: 2.1.1
 
-  is-gzip@1.0.0: {}
-
   is-installed-globally@0.4.0:
     dependencies:
       global-dirs: 3.0.1
       is-path-inside: 3.0.3
 
-  is-interactive@1.0.0: {}
-
   is-ip@3.1.0:
     dependencies:
       ip-regex: 4.3.0
@@ -19766,13 +19771,10 @@ snapshots:
 
   is-map@2.0.2: {}
 
-  is-nan@1.3.2:
-    dependencies:
-      call-bind: 1.0.2
-      define-properties: 1.2.0
-
   is-negative-zero@2.0.2: {}
 
+  is-negative-zero@2.0.3: {}
+
   is-node-process@1.2.0: {}
 
   is-number-object@1.0.7:
@@ -19781,18 +19783,12 @@ snapshots:
 
   is-number@7.0.0: {}
 
-  is-path-cwd@2.2.0: {}
-
   is-path-inside@3.0.3: {}
 
   is-plain-obj@1.1.0: {}
 
   is-plain-obj@4.1.0: {}
 
-  is-plain-object@2.0.4:
-    dependencies:
-      isobject: 3.0.1
-
   is-plain-object@5.0.0: {}
 
   is-potential-custom-element-name@1.0.1: {}
@@ -19810,19 +19806,25 @@ snapshots:
     dependencies:
       call-bind: 1.0.2
 
+  is-shared-array-buffer@1.0.3:
+    dependencies:
+      call-bind: 1.0.7
+
   is-stream@1.1.0: {}
 
   is-stream@2.0.1: {}
 
   is-stream@3.0.0: {}
 
+  is-stream@4.0.1: {}
+
   is-string@1.0.7:
     dependencies:
       has-tostringtag: 1.0.0
 
-  is-svg@5.0.0:
+  is-svg@5.1.0:
     dependencies:
-      fast-xml-parser: 4.2.5
+      fast-xml-parser: 4.5.0
 
   is-symbol@1.0.4:
     dependencies:
@@ -19836,19 +19838,25 @@ snapshots:
       gopd: 1.0.1
       has-tostringtag: 1.0.0
 
+  is-typed-array@1.1.13:
+    dependencies:
+      which-typed-array: 1.1.15
+
   is-typedarray@1.0.0: {}
 
   is-unicode-supported@0.1.0: {}
 
+  is-unicode-supported@2.0.0: {}
+
   is-weakmap@2.0.1: {}
 
   is-weakref@1.0.2:
     dependencies:
-      call-bind: 1.0.2
+      call-bind: 1.0.7
 
   is-weakset@2.0.2:
     dependencies:
-      call-bind: 1.0.2
+      call-bind: 1.0.7
       get-intrinsic: 1.2.1
 
   is-wsl@2.2.0:
@@ -19865,16 +19873,14 @@ snapshots:
 
   isexe@3.1.1: {}
 
-  isobject@3.0.1: {}
-
   isstream@0.1.2: {}
 
   istanbul-lib-coverage@3.2.2: {}
 
   istanbul-lib-instrument@5.2.1:
     dependencies:
-      '@babel/core': 7.24.0
-      '@babel/parser': 7.24.0
+      '@babel/core': 7.24.7
+      '@babel/parser': 7.24.7
       '@istanbuljs/schema': 0.1.3
       istanbul-lib-coverage: 3.2.2
       semver: 6.3.1
@@ -19883,11 +19889,11 @@ snapshots:
 
   istanbul-lib-instrument@6.0.0:
     dependencies:
-      '@babel/core': 7.24.0
-      '@babel/parser': 7.24.0
+      '@babel/core': 7.24.7
+      '@babel/parser': 7.24.7
       '@istanbuljs/schema': 0.1.3
       istanbul-lib-coverage: 3.2.2
-      semver: 7.6.0
+      semver: 7.6.3
     transitivePeerDependencies:
       - supports-color
 
@@ -19899,12 +19905,20 @@ snapshots:
 
   istanbul-lib-source-maps@4.0.1:
     dependencies:
-      debug: 4.3.4(supports-color@8.1.1)
+      debug: 4.3.7(supports-color@8.1.1)
       istanbul-lib-coverage: 3.2.2
       source-map: 0.6.1
     transitivePeerDependencies:
       - supports-color
 
+  istanbul-lib-source-maps@5.0.4:
+    dependencies:
+      '@jridgewell/trace-mapping': 0.3.25
+      debug: 4.3.5(supports-color@5.5.0)
+      istanbul-lib-coverage: 3.2.2
+    transitivePeerDependencies:
+      - supports-color
+
   istanbul-reports@3.1.6:
     dependencies:
       html-escaper: 2.0.2
@@ -19918,6 +19932,12 @@ snapshots:
     optionalDependencies:
       '@pkgjs/parseargs': 0.11.0
 
+  jackspeak@4.0.1:
+    dependencies:
+      '@isaacs/cliui': 8.0.2
+    optionalDependencies:
+      '@pkgjs/parseargs': 0.11.0
+
   jake@10.8.5:
     dependencies:
       async: 3.2.4
@@ -19937,7 +19957,7 @@ snapshots:
       '@jest/expect': 29.7.0
       '@jest/test-result': 29.7.0
       '@jest/types': 29.6.3
-      '@types/node': 20.12.7
+      '@types/node': 20.14.12
       chalk: 4.1.2
       co: 4.6.0
       dedent: 1.3.0
@@ -19957,16 +19977,16 @@ snapshots:
       - babel-plugin-macros
       - supports-color
 
-  jest-cli@29.7.0(@types/node@20.12.7):
+  jest-cli@29.7.0(@types/node@20.14.12):
     dependencies:
       '@jest/core': 29.7.0
       '@jest/test-result': 29.7.0
       '@jest/types': 29.6.3
       chalk: 4.1.2
-      create-jest: 29.7.0(@types/node@20.12.7)
+      create-jest: 29.7.0(@types/node@20.14.12)
       exit: 0.1.2
       import-local: 3.1.0
-      jest-config: 29.7.0(@types/node@20.12.7)
+      jest-config: 29.7.0(@types/node@20.14.12)
       jest-util: 29.7.0
       jest-validate: 29.7.0
       yargs: 17.7.2
@@ -19976,7 +19996,7 @@ snapshots:
       - supports-color
       - ts-node
 
-  jest-config@29.7.0(@types/node@20.12.7):
+  jest-config@29.7.0(@types/node@20.14.12):
     dependencies:
       '@babel/core': 7.23.5
       '@jest/test-sequencer': 29.7.0
@@ -19995,13 +20015,13 @@ snapshots:
       jest-runner: 29.7.0
       jest-util: 29.7.0
       jest-validate: 29.7.0
-      micromatch: 4.0.5
+      micromatch: 4.0.8
       parse-json: 5.2.0
       pretty-format: 29.7.0
       slash: 3.0.0
       strip-json-comments: 3.1.1
     optionalDependencies:
-      '@types/node': 20.12.7
+      '@types/node': 20.14.12
     transitivePeerDependencies:
       - babel-plugin-macros
       - supports-color
@@ -20030,7 +20050,7 @@ snapshots:
       '@jest/environment': 29.7.0
       '@jest/fake-timers': 29.7.0
       '@jest/types': 29.6.3
-      '@types/node': 20.12.7
+      '@types/node': 20.14.12
       jest-mock: 29.7.0
       jest-util: 29.7.0
 
@@ -20047,14 +20067,14 @@ snapshots:
     dependencies:
       '@jest/types': 29.6.3
       '@types/graceful-fs': 4.1.6
-      '@types/node': 20.12.7
+      '@types/node': 20.14.12
       anymatch: 3.1.3
       fb-watchman: 2.0.2
       graceful-fs: 4.2.11
       jest-regex-util: 29.6.3
       jest-util: 29.7.0
       jest-worker: 29.7.0
-      micromatch: 4.0.5
+      micromatch: 4.0.8
       walker: 1.0.8
     optionalDependencies:
       fsevents: 2.3.3
@@ -20078,7 +20098,7 @@ snapshots:
       '@types/stack-utils': 2.0.1
       chalk: 4.1.2
       graceful-fs: 4.2.11
-      micromatch: 4.0.5
+      micromatch: 4.0.8
       pretty-format: 29.7.0
       slash: 3.0.0
       stack-utils: 2.0.6
@@ -20086,7 +20106,7 @@ snapshots:
   jest-mock@29.7.0:
     dependencies:
       '@jest/types': 29.6.3
-      '@types/node': 20.12.7
+      '@types/node': 20.14.12
       jest-util: 29.7.0
 
   jest-pnp-resolver@1.2.3(jest-resolve@29.7.0):
@@ -20121,7 +20141,7 @@ snapshots:
       '@jest/test-result': 29.7.0
       '@jest/transform': 29.7.0
       '@jest/types': 29.6.3
-      '@types/node': 20.12.7
+      '@types/node': 20.14.12
       chalk: 4.1.2
       emittery: 0.13.1
       graceful-fs: 4.2.11
@@ -20149,7 +20169,7 @@ snapshots:
       '@jest/test-result': 29.7.0
       '@jest/transform': 29.7.0
       '@jest/types': 29.6.3
-      '@types/node': 20.12.7
+      '@types/node': 20.14.12
       chalk: 4.1.2
       cjs-module-lexer: 1.2.2
       collect-v8-coverage: 1.0.1
@@ -20173,7 +20193,7 @@ snapshots:
       '@babel/generator': 7.23.5
       '@babel/plugin-syntax-jsx': 7.23.3(@babel/core@7.23.5)
       '@babel/plugin-syntax-typescript': 7.23.3(@babel/core@7.23.5)
-      '@babel/types': 7.23.5
+      '@babel/types': 7.24.7
       '@jest/expect-utils': 29.7.0
       '@jest/transform': 29.7.0
       '@jest/types': 29.6.3
@@ -20188,14 +20208,14 @@ snapshots:
       jest-util: 29.7.0
       natural-compare: 1.4.0
       pretty-format: 29.7.0
-      semver: 7.5.4
+      semver: 7.6.0
     transitivePeerDependencies:
       - supports-color
 
   jest-util@29.7.0:
     dependencies:
       '@jest/types': 29.6.3
-      '@types/node': 20.12.7
+      '@types/node': 20.14.12
       chalk: 4.1.2
       ci-info: 3.7.1
       graceful-fs: 4.2.11
@@ -20214,7 +20234,7 @@ snapshots:
     dependencies:
       '@jest/test-result': 29.7.0
       '@jest/types': 29.6.3
-      '@types/node': 20.12.7
+      '@types/node': 20.14.12
       ansi-escapes: 4.3.2
       chalk: 4.1.2
       emittery: 0.13.1
@@ -20228,17 +20248,17 @@ snapshots:
 
   jest-worker@29.7.0:
     dependencies:
-      '@types/node': 20.12.7
+      '@types/node': 20.14.12
       jest-util: 29.7.0
       merge-stream: 2.0.0
       supports-color: 8.1.1
 
-  jest@29.7.0(@types/node@20.12.7):
+  jest@29.7.0(@types/node@20.14.12):
     dependencies:
       '@jest/core': 29.7.0
       '@jest/types': 29.6.3
       import-local: 3.1.0
-      jest-cli: 29.7.0(@types/node@20.12.7)
+      jest-cli: 29.7.0(@types/node@20.14.12)
     transitivePeerDependencies:
       - '@types/node'
       - babel-plugin-macros
@@ -20255,6 +20275,14 @@ snapshots:
       '@sideway/formula': 3.0.1
       '@sideway/pinpoint': 2.0.0
 
+  joi@17.13.3:
+    dependencies:
+      '@hapi/hoek': 9.3.0
+      '@hapi/topo': 5.1.0
+      '@sideway/address': 4.1.5
+      '@sideway/formula': 3.0.1
+      '@sideway/pinpoint': 2.0.0
+
   jpeg-js@0.3.7: {}
 
   js-beautify@1.14.9:
@@ -20268,6 +20296,8 @@ snapshots:
 
   js-tokens@4.0.0: {}
 
+  js-tokens@9.0.0: {}
+
   js-yaml@3.14.1:
     dependencies:
       argparse: 1.0.10
@@ -20283,62 +20313,64 @@ snapshots:
 
   jschardet@3.0.0: {}
 
-  jscodeshift@0.15.1(@babel/preset-env@7.23.5(@babel/core@7.24.0)):
-    dependencies:
-      '@babel/core': 7.24.0
-      '@babel/parser': 7.24.0
-      '@babel/plugin-transform-class-properties': 7.23.3(@babel/core@7.24.0)
-      '@babel/plugin-transform-modules-commonjs': 7.23.3(@babel/core@7.24.0)
-      '@babel/plugin-transform-nullish-coalescing-operator': 7.23.4(@babel/core@7.24.0)
-      '@babel/plugin-transform-optional-chaining': 7.23.4(@babel/core@7.24.0)
-      '@babel/plugin-transform-private-methods': 7.23.3(@babel/core@7.24.0)
-      '@babel/preset-flow': 7.23.3(@babel/core@7.24.0)
-      '@babel/preset-typescript': 7.23.3(@babel/core@7.24.0)
-      '@babel/register': 7.22.15(@babel/core@7.24.0)
-      babel-core: 7.0.0-bridge.0(@babel/core@7.24.0)
-      chalk: 4.1.2
-      flow-parser: 0.202.0
-      graceful-fs: 4.2.11
-      micromatch: 4.0.5
-      neo-async: 2.6.2
-      node-dir: 0.1.17
-      recast: 0.23.4
-      temp: 0.8.4
-      write-file-atomic: 2.4.3
-    optionalDependencies:
-      '@babel/preset-env': 7.23.5(@babel/core@7.24.0)
-    transitivePeerDependencies:
-      - supports-color
+  jsdoc-type-pratt-parser@4.1.0: {}
 
-  jsdom@24.0.0(bufferutil@4.0.7)(utf-8-validate@6.0.3):
+  jsdom@24.1.1(bufferutil@4.0.7)(utf-8-validate@6.0.3):
     dependencies:
       cssstyle: 4.0.1
       data-urls: 5.0.0
       decimal.js: 10.4.3
       form-data: 4.0.0
       html-encoding-sniffer: 4.0.0
-      http-proxy-agent: 7.0.0
-      https-proxy-agent: 7.0.2
+      http-proxy-agent: 7.0.2
+      https-proxy-agent: 7.0.5
       is-potential-custom-element-name: 1.0.1
-      nwsapi: 2.2.9
+      nwsapi: 2.2.12
       parse5: 7.1.2
-      rrweb-cssom: 0.6.0
+      rrweb-cssom: 0.7.1
       saxes: 6.0.0
       symbol-tree: 3.2.4
-      tough-cookie: 4.1.3
+      tough-cookie: 4.1.4
       w3c-xmlserializer: 5.0.0
       webidl-conversions: 7.0.0
       whatwg-encoding: 3.1.1
       whatwg-mimetype: 4.0.0
       whatwg-url: 14.0.0
-      ws: 8.17.0(bufferutil@4.0.7)(utf-8-validate@6.0.3)
+      ws: 8.18.0(bufferutil@4.0.7)(utf-8-validate@6.0.3)
       xml-name-validator: 5.0.0
     transitivePeerDependencies:
       - bufferutil
       - supports-color
       - utf-8-validate
 
-  jsesc@0.5.0: {}
+  jsdom@24.1.1(bufferutil@4.0.8)(utf-8-validate@6.0.4):
+    dependencies:
+      cssstyle: 4.0.1
+      data-urls: 5.0.0
+      decimal.js: 10.4.3
+      form-data: 4.0.0
+      html-encoding-sniffer: 4.0.0
+      http-proxy-agent: 7.0.2
+      https-proxy-agent: 7.0.5
+      is-potential-custom-element-name: 1.0.1
+      nwsapi: 2.2.12
+      parse5: 7.1.2
+      rrweb-cssom: 0.7.1
+      saxes: 6.0.0
+      symbol-tree: 3.2.4
+      tough-cookie: 4.1.4
+      w3c-xmlserializer: 5.0.0
+      webidl-conversions: 7.0.0
+      whatwg-encoding: 3.1.1
+      whatwg-mimetype: 4.0.0
+      whatwg-url: 14.0.0
+      ws: 8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4)
+      xml-name-validator: 5.0.0
+    transitivePeerDependencies:
+      - bufferutil
+      - supports-color
+      - utf-8-validate
+    optional: true
 
   jsesc@2.5.2: {}
 
@@ -20346,6 +20378,10 @@ snapshots:
 
   json-parse-even-better-errors@2.3.1: {}
 
+  json-schema-ref-resolver@1.0.1:
+    dependencies:
+      fast-deep-equal: 3.1.3
+
   json-schema-traverse@0.4.1: {}
 
   json-schema-traverse@1.0.0: {}
@@ -20385,9 +20421,9 @@ snapshots:
     optionalDependencies:
       graceful-fs: 4.2.11
 
-  jsonld@8.3.2(web-streams-polyfill@3.2.1):
+  jsonld@8.3.2(web-streams-polyfill@4.0.0):
     dependencies:
-      '@digitalbazaar/http-client': 3.4.1(web-streams-polyfill@3.2.1)
+      '@digitalbazaar/http-client': 3.4.1(web-streams-polyfill@4.0.0)
       canonicalize: 1.0.8
       lru-cache: 6.0.0
       rdf-canonize: 3.4.0
@@ -20412,13 +20448,19 @@ snapshots:
 
   jsrsasign@11.1.0: {}
 
-  jssha@3.3.1: {}
-
   jstransformer@1.0.0:
     dependencies:
       is-promise: 2.2.2
       promise: 7.3.1
 
+  juice@11.0.0:
+    dependencies:
+      cheerio: 1.0.0
+      commander: 12.1.0
+      mensch: 0.3.4
+      slick: 1.12.2
+      web-resource-inliner: 7.0.0
+
   just-extend@4.2.1: {}
 
   jwa@2.0.0:
@@ -20440,24 +20482,18 @@ snapshots:
 
   kleur@3.0.3: {}
 
-  ky-universal@0.11.0(ky@0.33.3)(web-streams-polyfill@3.2.1):
+  ky-universal@0.11.0(ky@0.33.3)(web-streams-polyfill@4.0.0):
     dependencies:
       abort-controller: 3.0.0
       ky: 0.33.3
       node-fetch: 3.3.2
     optionalDependencies:
-      web-streams-polyfill: 3.2.1
+      web-streams-polyfill: 4.0.0
 
   ky@0.33.3: {}
 
   lazy-ass@1.6.0: {}
 
-  lazy-universal-dotenv@4.0.0:
-    dependencies:
-      app-root-dir: 1.0.2
-      dotenv: 16.0.3
-      dotenv-expand: 10.0.0
-
   lazystream@1.0.1:
     dependencies:
       readable-stream: 2.3.7
@@ -20469,10 +20505,10 @@ snapshots:
       prelude-ls: 1.2.1
       type-check: 0.4.0
 
-  light-my-request@5.11.0:
+  light-my-request@6.0.0:
     dependencies:
-      cookie: 0.5.0
-      process-warning: 2.2.0
+      cookie: 0.6.0
+      process-warning: 4.0.0
       set-cookie-parser: 2.6.0
 
   lilconfig@3.1.1: {}
@@ -20492,12 +20528,10 @@ snapshots:
     optionalDependencies:
       enquirer: 2.3.6
 
-  local-pkg@0.4.3: {}
-
-  locate-path@3.0.0:
+  local-pkg@0.5.0:
     dependencies:
-      p-locate: 3.0.0
-      path-exists: 3.0.0
+      mlly: 1.5.0
+      pkg-types: 1.0.3
 
   locate-path@5.0.0:
     dependencies:
@@ -20507,16 +20541,12 @@ snapshots:
     dependencies:
       p-locate: 5.0.0
 
-  lodash.debounce@4.0.8: {}
-
   lodash.defaults@4.2.0: {}
 
   lodash.get@4.4.2: {}
 
   lodash.isarguments@3.1.0: {}
 
-  lodash.isequal@4.5.0: {}
-
   lodash.memoize@4.1.2: {}
 
   lodash.merge@4.6.2: {}
@@ -20551,16 +20581,22 @@ snapshots:
     dependencies:
       get-func-name: 2.0.2
 
+  loupe@3.1.1:
+    dependencies:
+      get-func-name: 2.0.2
+
   lowercase-keys@2.0.0: {}
 
   lowercase-keys@3.0.0: {}
 
   lru-cache@10.0.2:
     dependencies:
-      semver: 7.6.0
+      semver: 7.6.3
 
   lru-cache@10.2.2: {}
 
+  lru-cache@11.0.0: {}
+
   lru-cache@4.1.5:
     dependencies:
       pseudomap: 1.0.2
@@ -20582,30 +20618,31 @@ snapshots:
 
   magic-string@0.27.0:
     dependencies:
-      '@jridgewell/sourcemap-codec': 1.4.15
+      '@jridgewell/sourcemap-codec': 1.5.0
 
   magic-string@0.30.10:
     dependencies:
       '@jridgewell/sourcemap-codec': 1.4.15
 
-  magic-string@0.30.7:
+  magic-string@0.30.11:
     dependencies:
-      '@jridgewell/sourcemap-codec': 1.4.15
+      '@jridgewell/sourcemap-codec': 1.5.0
+
+  magicast@0.3.4:
+    dependencies:
+      '@babel/parser': 7.24.7
+      '@babel/types': 7.24.7
+      source-map-js: 1.2.0
 
   mailcheck@1.1.1: {}
 
-  make-dir@2.1.0:
-    dependencies:
-      pify: 4.0.1
-      semver: 5.7.1
-
   make-dir@3.1.0:
     dependencies:
       semver: 6.3.1
 
   make-dir@4.0.0:
     dependencies:
-      semver: 7.6.0
+      semver: 7.6.3
 
   make-fetch-happen@13.0.0:
     dependencies:
@@ -20613,7 +20650,7 @@ snapshots:
       cacache: 18.0.0
       http-cache-semantics: 4.1.1
       is-lambda: 1.0.1
-      minipass: 7.0.4
+      minipass: 7.1.2
       minipass-fetch: 3.0.3
       minipass-flush: 1.0.5
       minipass-pipeline: 1.2.4
@@ -20637,7 +20674,7 @@ snapshots:
 
   markdown-table@3.0.3: {}
 
-  markdown-to-jsx@7.3.2(react@18.3.1):
+  markdown-to-jsx@7.4.7(react@18.3.1):
     dependencies:
       react: 18.3.1
 
@@ -20729,6 +20766,18 @@ snapshots:
       '@types/mdast': 4.0.3
       unist-util-is: 6.0.0
 
+  mdast-util-to-hast@13.2.0:
+    dependencies:
+      '@types/hast': 3.0.4
+      '@types/mdast': 4.0.3
+      '@ungap/structured-clone': 1.2.0
+      devlop: 1.1.0
+      micromark-util-sanitize-uri: 2.0.0
+      trim-lines: 3.0.1
+      unist-util-position: 5.0.0
+      unist-util-visit: 5.0.0
+      vfile: 6.0.1
+
   mdast-util-to-markdown@2.1.0:
     dependencies:
       '@types/mdast': 4.0.3
@@ -20750,7 +20799,7 @@ snapshots:
 
   media-typer@0.3.0: {}
 
-  meilisearch@0.38.0(encoding@0.1.13):
+  meilisearch@0.42.0(encoding@0.1.13):
     dependencies:
       cross-fetch: 3.1.6(encoding@0.1.13)
     transitivePeerDependencies:
@@ -20760,6 +20809,8 @@ snapshots:
     dependencies:
       map-or-similar: 1.5.0
 
+  mensch@0.3.4: {}
+
   meow@9.0.0:
     dependencies:
       '@types/minimist': 1.2.2
@@ -20775,7 +20826,7 @@ snapshots:
       type-fest: 0.18.1
       yargs-parser: 20.2.9
 
-  merge-descriptors@1.0.1: {}
+  merge-descriptors@1.0.3: {}
 
   merge-stream@2.0.0: {}
 
@@ -20963,7 +21014,7 @@ snapshots:
   micromark@4.0.0:
     dependencies:
       '@types/debug': 4.1.12
-      debug: 4.3.4(supports-color@8.1.1)
+      debug: 4.3.7(supports-color@8.1.1)
       decode-named-character-reference: 1.0.2
       devlop: 1.1.0
       micromark-core-commonmark: 2.0.0
@@ -20982,9 +21033,9 @@ snapshots:
     transitivePeerDependencies:
       - supports-color
 
-  micromatch@4.0.5:
+  micromatch@4.0.8:
     dependencies:
-      braces: 3.0.2
+      braces: 3.0.3
       picomatch: 2.3.1
 
   mime-db@1.52.0: {}
@@ -20995,6 +21046,8 @@ snapshots:
 
   mime@1.6.0: {}
 
+  mime@2.6.0: {}
+
   mime@3.0.0: {}
 
   mimic-fn@2.1.0: {}
@@ -21011,6 +21064,10 @@ snapshots:
 
   minimalistic-assert@1.0.1: {}
 
+  minimatch@10.0.1:
+    dependencies:
+      brace-expansion: 2.0.1
+
   minimatch@3.0.8:
     dependencies:
       brace-expansion: 1.1.11
@@ -21081,6 +21138,8 @@ snapshots:
 
   minipass@7.0.4: {}
 
+  minipass@7.1.2: {}
+
   minizlib@1.3.3:
     dependencies:
       minipass: 2.9.0
@@ -21091,8 +21150,6 @@ snapshots:
       minipass: 3.3.6
       yallist: 4.0.0
 
-  mkdirp-classic@0.5.3: {}
-
   mkdirp@0.5.6:
     dependencies:
       minimist: 1.2.8
@@ -21103,12 +21160,12 @@ snapshots:
 
   mlly@1.5.0:
     dependencies:
-      acorn: 8.11.3
+      acorn: 8.12.1
       pathe: 1.1.2
       pkg-types: 1.0.3
       ufo: 1.3.2
 
-  mnemonist@0.39.6:
+  mnemonist@0.39.8:
     dependencies:
       obliterator: 2.0.4
 
@@ -21116,8 +21173,6 @@ snapshots:
 
   module-details-from-path@1.0.3: {}
 
-  mri@1.2.0: {}
-
   ms@2.0.0: {}
 
   ms@2.1.2: {}
@@ -21142,18 +21197,18 @@ snapshots:
     optionalDependencies:
       msgpackr-extract: 3.0.2
 
-  msw-storybook-addon@2.0.1(msw@2.2.14(typescript@5.4.5)):
+  msw-storybook-addon@2.0.3(msw@2.4.9(typescript@5.6.2)):
     dependencies:
       is-node-process: 1.2.0
-      msw: 2.2.14(typescript@5.4.5)
+      msw: 2.4.9(typescript@5.6.2)
 
-  msw@2.2.14(typescript@5.4.5):
+  msw@2.3.4(typescript@5.6.2):
     dependencies:
       '@bundled-es-modules/cookie': 2.0.0
       '@bundled-es-modules/statuses': 1.0.1
+      '@bundled-es-modules/tough-cookie': 0.1.6
       '@inquirer/confirm': 3.1.6
-      '@mswjs/cookies': 1.1.0
-      '@mswjs/interceptors': 0.26.15
+      '@mswjs/interceptors': 0.29.1
       '@open-draft/until': 2.1.0
       '@types/cookie': 0.6.0
       '@types/statuses': 2.0.4
@@ -21164,10 +21219,32 @@ snapshots:
       outvariant: 1.4.2
       path-to-regexp: 6.2.1
       strict-event-emitter: 0.5.1
-      type-fest: 4.9.0
+      type-fest: 4.20.1
       yargs: 17.7.2
     optionalDependencies:
-      typescript: 5.4.5
+      typescript: 5.6.2
+
+  msw@2.4.9(typescript@5.6.2):
+    dependencies:
+      '@bundled-es-modules/cookie': 2.0.0
+      '@bundled-es-modules/statuses': 1.0.1
+      '@bundled-es-modules/tough-cookie': 0.1.6
+      '@inquirer/confirm': 3.1.6
+      '@mswjs/interceptors': 0.35.8
+      '@open-draft/until': 2.1.0
+      '@types/cookie': 0.6.0
+      '@types/statuses': 2.0.4
+      chalk: 4.1.2
+      graphql: 16.8.1
+      headers-polyfill: 4.0.2
+      is-node-process: 1.2.0
+      outvariant: 1.4.2
+      path-to-regexp: 6.3.0
+      strict-event-emitter: 0.5.1
+      type-fest: 4.20.1
+      yargs: 17.7.2
+    optionalDependencies:
+      typescript: 5.6.2
 
   muggle-string@0.4.1: {}
 
@@ -21193,7 +21270,7 @@ snapshots:
       object-assign: 4.1.1
       thenify-all: 1.6.0
 
-  nan@2.18.0: {}
+  nan@2.20.0: {}
 
   nanoid@3.3.7: {}
 
@@ -21232,8 +21309,6 @@ snapshots:
 
   negotiator@0.6.3: {}
 
-  neo-async@2.6.2: {}
-
   nested-property@4.0.0: {}
 
   netmask@2.0.2: {}
@@ -21263,15 +21338,9 @@ snapshots:
 
   node-bitmap@0.0.1: {}
 
-  node-dir@0.1.17:
-    dependencies:
-      minimatch: 3.1.2
-
   node-domexception@1.0.0: {}
 
-  node-fetch-native@1.0.2: {}
-
-  node-fetch@2.6.11(encoding@0.1.13):
+  node-fetch@2.6.13(encoding@0.1.13):
     dependencies:
       whatwg-url: 5.0.0
     optionalDependencies:
@@ -21295,15 +21364,15 @@ snapshots:
   node-gyp-build@4.6.0:
     optional: true
 
-  node-gyp@10.0.1:
+  node-gyp@10.2.0:
     dependencies:
       env-paths: 2.2.1
       exponential-backoff: 3.1.1
-      glob: 10.3.12
+      glob: 10.3.10
       graceful-fs: 4.2.11
       make-fetch-happen: 13.0.0
       nopt: 7.2.0
-      proc-log: 3.0.0
+      proc-log: 4.2.0
       semver: 7.6.0
       tar: 6.2.1
       which: 4.0.0
@@ -21314,7 +21383,7 @@ snapshots:
 
   node-releases@2.0.14: {}
 
-  nodemailer@6.9.13: {}
+  nodemailer@6.9.15: {}
 
   nodemon@3.0.2:
     dependencies:
@@ -21329,14 +21398,14 @@ snapshots:
       touch: 3.1.0
       undefsafe: 2.0.5
 
-  nodemon@3.1.0:
+  nodemon@3.1.7:
     dependencies:
       chokidar: 3.5.3
-      debug: 4.3.4(supports-color@5.5.0)
+      debug: 4.3.5(supports-color@5.5.0)
       ignore-by-default: 1.0.1
       minimatch: 3.1.2
       pstree.remy: 1.1.8
-      semver: 7.5.4
+      semver: 7.6.0
       simple-update-notifier: 2.0.0
       supports-color: 5.5.0
       touch: 3.1.0
@@ -21372,14 +21441,14 @@ snapshots:
     dependencies:
       hosted-git-info: 4.1.0
       is-core-module: 2.13.1
-      semver: 7.6.0
+      semver: 7.6.3
       validate-npm-package-license: 3.0.4
 
   normalize-path@3.0.0: {}
 
   normalize-url@6.1.0: {}
 
-  normalize-url@8.0.0: {}
+  normalize-url@8.0.1: {}
 
   npm-run-path@2.0.2:
     dependencies:
@@ -21393,6 +21462,15 @@ snapshots:
     dependencies:
       path-key: 4.0.0
 
+  npm-run-path@5.3.0:
+    dependencies:
+      path-key: 4.0.0
+
+  npm-run-path@6.0.0:
+    dependencies:
+      path-key: 4.0.0
+      unicorn-magic: 0.3.0
+
   npmlog@5.0.1:
     dependencies:
       are-we-there-yet: 2.0.0
@@ -21410,7 +21488,7 @@ snapshots:
     dependencies:
       boolbase: 1.0.0
 
-  nwsapi@2.2.9: {}
+  nwsapi@2.2.12: {}
 
   oauth-sign@0.9.0: {}
 
@@ -21430,6 +21508,8 @@ snapshots:
 
   object-inspect@1.12.3: {}
 
+  object-inspect@1.13.2: {}
+
   object-is@1.1.5:
     dependencies:
       call-bind: 1.0.2
@@ -21444,24 +21524,31 @@ snapshots:
       has-symbols: 1.0.3
       object-keys: 1.1.1
 
-  object.fromentries@2.0.7:
+  object.assign@4.1.5:
     dependencies:
-      call-bind: 1.0.2
-      define-properties: 1.2.0
-      es-abstract: 1.22.1
+      call-bind: 1.0.7
+      define-properties: 1.2.1
+      has-symbols: 1.0.3
+      object-keys: 1.1.1
 
-  object.groupby@1.0.1:
+  object.fromentries@2.0.8:
     dependencies:
-      call-bind: 1.0.2
-      define-properties: 1.2.0
-      es-abstract: 1.22.1
-      get-intrinsic: 1.2.1
+      call-bind: 1.0.7
+      define-properties: 1.2.1
+      es-abstract: 1.23.3
+      es-object-atoms: 1.0.0
 
-  object.values@1.1.7:
+  object.groupby@1.0.3:
     dependencies:
-      call-bind: 1.0.2
-      define-properties: 1.2.0
-      es-abstract: 1.22.1
+      call-bind: 1.0.7
+      define-properties: 1.2.1
+      es-abstract: 1.23.3
+
+  object.values@1.2.0:
+    dependencies:
+      call-bind: 1.0.7
+      define-properties: 1.2.1
+      es-object-atoms: 1.0.0
 
   obliterator@2.0.4: {}
 
@@ -21477,8 +21564,6 @@ snapshots:
     dependencies:
       ee-first: 1.1.1
 
-  on-headers@1.0.2: {}
-
   once@1.4.0:
     dependencies:
       wrappy: 1.0.2
@@ -21491,6 +21576,10 @@ snapshots:
     dependencies:
       mimic-fn: 4.0.0
 
+  oniguruma-to-js@0.4.3:
+    dependencies:
+      regex: 4.3.3
+
   open@8.4.2:
     dependencies:
       define-lazy-prop: 2.0.0
@@ -21508,35 +21597,23 @@ snapshots:
       undici: 5.28.2
       yargs-parser: 21.1.1
 
-  opentelemetry-instrumentation-fetch-node@1.2.0:
+  opentelemetry-instrumentation-fetch-node@1.2.3(@opentelemetry/api@1.9.0):
     dependencies:
-      '@opentelemetry/api': 1.8.0
-      '@opentelemetry/instrumentation': 0.43.0(@opentelemetry/api@1.8.0)
-      '@opentelemetry/semantic-conventions': 1.24.1
+      '@opentelemetry/api': 1.9.0
+      '@opentelemetry/instrumentation': 0.46.0(@opentelemetry/api@1.9.0)
+      '@opentelemetry/semantic-conventions': 1.25.1
     transitivePeerDependencies:
       - supports-color
     optional: true
 
-  optionator@0.9.3:
+  optionator@0.9.4:
     dependencies:
-      '@aashutoshrathi/word-wrap': 1.2.6
       deep-is: 0.1.4
       fast-levenshtein: 2.0.6
       levn: 0.4.1
       prelude-ls: 1.2.1
       type-check: 0.4.0
-
-  ora@5.4.1:
-    dependencies:
-      bl: 4.1.0
-      chalk: 4.1.2
-      cli-cursor: 3.1.0
-      cli-spinners: 2.7.0
-      is-interactive: 1.0.0
-      is-unicode-supported: 0.1.0
-      log-symbols: 4.1.0
-      strip-ansi: 6.0.1
-      wcwidth: 1.0.1
+      word-wrap: 1.2.5
 
   os-filter-obj@2.0.0:
     dependencies:
@@ -21546,12 +21623,14 @@ snapshots:
 
   ospath@1.2.2: {}
 
-  otpauth@9.2.3:
+  otpauth@9.3.4:
     dependencies:
-      jssha: 3.3.1
+      '@noble/hashes': 1.5.0
 
   outvariant@1.4.2: {}
 
+  outvariant@1.4.3: {}
+
   p-cancelable@2.1.1: {}
 
   p-cancelable@3.0.0: {}
@@ -21568,14 +21647,10 @@ snapshots:
     dependencies:
       yocto-queue: 0.1.0
 
-  p-limit@4.0.0:
+  p-limit@5.0.0:
     dependencies:
       yocto-queue: 1.0.0
 
-  p-locate@3.0.0:
-    dependencies:
-      p-limit: 2.3.0
-
   p-locate@4.1.0:
     dependencies:
       p-limit: 2.3.0
@@ -21599,7 +21674,7 @@ snapshots:
 
   p-try@2.2.0: {}
 
-  pako@0.2.9: {}
+  package-json-from-dist@1.0.0: {}
 
   parent-module@1.0.1:
     dependencies:
@@ -21611,11 +21686,13 @@ snapshots:
 
   parse-json@5.2.0:
     dependencies:
-      '@babel/code-frame': 7.23.5
+      '@babel/code-frame': 7.24.7
       error-ex: 1.3.2
       json-parse-even-better-errors: 2.3.1
       lines-and-columns: 1.2.4
 
+  parse-ms@4.0.0: {}
+
   parse-srcset@1.0.2: {}
 
   parse5-htmlparser2-tree-adapter@6.0.1:
@@ -21627,6 +21704,10 @@ snapshots:
       domhandler: 5.0.3
       parse5: 7.1.2
 
+  parse5-parser-stream@7.1.2:
+    dependencies:
+      parse5: 7.1.2
+
   parse5@5.1.1: {}
 
   parse5@6.0.1: {}
@@ -21639,8 +21720,6 @@ snapshots:
 
   path-browserify@1.0.1: {}
 
-  path-exists@3.0.0: {}
-
   path-exists@4.0.0: {}
 
   path-is-absolute@1.0.1: {}
@@ -21658,38 +21737,38 @@ snapshots:
       lru-cache: 10.0.2
       minipass: 7.0.4
 
-  path-scurry@1.10.2:
+  path-scurry@2.0.0:
     dependencies:
-      lru-cache: 10.2.2
-      minipass: 7.0.4
+      lru-cache: 11.0.0
+      minipass: 7.1.2
 
-  path-to-regexp@0.1.7: {}
+  path-to-regexp@0.1.10: {}
 
   path-to-regexp@1.8.0:
     dependencies:
       isarray: 0.0.1
 
-  path-to-regexp@3.2.0: {}
+  path-to-regexp@3.3.0: {}
 
   path-to-regexp@6.2.1: {}
 
+  path-to-regexp@6.3.0: {}
+
   path-type@4.0.0: {}
 
   pathe@1.1.2: {}
 
   pathval@1.1.1: {}
 
+  pathval@2.0.0: {}
+
   pause-stream@0.0.11:
     dependencies:
       through: 2.3.8
 
-  peek-readable@5.0.0: {}
+  peek-readable@5.1.3: {}
 
-  peek-stream@1.1.3:
-    dependencies:
-      buffer-from: 1.1.2
-      duplexify: 3.7.1
-      through2: 2.0.5
+  peek-readable@5.2.0: {}
 
   pend@1.2.0: {}
 
@@ -21698,20 +21777,20 @@ snapshots:
   pg-cloudflare@1.1.1:
     optional: true
 
-  pg-connection-string@2.6.4: {}
+  pg-connection-string@2.7.0: {}
 
   pg-int8@1.0.1: {}
 
   pg-numeric@1.0.2: {}
 
-  pg-pool@3.6.2(pg@8.11.5):
+  pg-pool@3.7.0(pg@8.13.0):
     dependencies:
-      pg: 8.11.5
-
-  pg-protocol@1.6.0: {}
+      pg: 8.13.0
 
   pg-protocol@1.6.1: {}
 
+  pg-protocol@1.7.0: {}
+
   pg-types@2.2.0:
     dependencies:
       pg-int8: 1.0.1
@@ -21730,11 +21809,11 @@ snapshots:
       postgres-interval: 3.0.0
       postgres-range: 1.1.3
 
-  pg@8.11.5:
+  pg@8.13.0:
     dependencies:
-      pg-connection-string: 2.6.4
-      pg-pool: 3.6.2(pg@8.11.5)
-      pg-protocol: 1.6.1
+      pg-connection-string: 2.7.0
+      pg-pool: 3.7.0(pg@8.13.0)
+      pg-protocol: 1.7.0
       pg-types: 2.2.0
       pgpass: 1.0.5
     optionalDependencies:
@@ -21744,10 +21823,14 @@ snapshots:
     dependencies:
       split2: 4.1.0
 
-  photoswipe@5.4.3: {}
+  photoswipe@5.4.4: {}
 
   picocolors@1.0.0: {}
 
+  picocolors@1.0.1: {}
+
+  picocolors@1.1.0: {}
+
   picomatch@2.3.1: {}
 
   pid-port@1.0.0:
@@ -21756,28 +21839,26 @@ snapshots:
 
   pify@2.3.0: {}
 
-  pify@4.0.1: {}
-
-  pino-abstract-transport@1.1.0:
+  pino-abstract-transport@1.2.0:
     dependencies:
       readable-stream: 4.3.0
       split2: 4.1.0
 
-  pino-std-serializers@6.1.0: {}
+  pino-std-serializers@7.0.0: {}
 
-  pino@8.17.0:
+  pino@9.2.0:
     dependencies:
       atomic-sleep: 1.0.0
       fast-redact: 3.1.2
       on-exit-leak-free: 2.1.0
-      pino-abstract-transport: 1.1.0
-      pino-std-serializers: 6.1.0
-      process-warning: 2.2.0
+      pino-abstract-transport: 1.2.0
+      pino-std-serializers: 7.0.0
+      process-warning: 3.0.0
       quick-format-unescaped: 4.0.4
       real-require: 0.2.0
       safe-stable-stringify: 2.4.2
-      sonic-boom: 3.7.0
-      thread-stream: 2.3.0
+      sonic-boom: 4.0.1
+      thread-stream: 3.1.0
 
   pirates@4.0.5: {}
 
@@ -21787,18 +21868,10 @@ snapshots:
 
   pkce-challenge@4.1.0: {}
 
-  pkg-dir@3.0.0:
-    dependencies:
-      find-up: 3.0.0
-
   pkg-dir@4.2.0:
     dependencies:
       find-up: 4.1.0
 
-  pkg-dir@5.0.0:
-    dependencies:
-      find-up: 5.0.0
-
   pkg-types@1.0.3:
     dependencies:
       jsonc-parser: 3.2.0
@@ -21823,170 +21896,167 @@ snapshots:
     dependencies:
       '@babel/runtime': 7.23.4
 
-  postcss-calc@9.0.1(postcss@8.4.38):
+  possible-typed-array-names@1.0.0: {}
+
+  postcss-calc@9.0.1(postcss@8.4.47):
     dependencies:
-      postcss: 8.4.38
-      postcss-selector-parser: 6.0.15
+      postcss: 8.4.47
+      postcss-selector-parser: 6.0.16
       postcss-value-parser: 4.2.0
 
-  postcss-colormin@6.1.0(postcss@8.4.38):
+  postcss-colormin@6.1.0(postcss@8.4.47):
     dependencies:
       browserslist: 4.23.0
       caniuse-api: 3.0.0
       colord: 2.9.3
-      postcss: 8.4.38
+      postcss: 8.4.47
       postcss-value-parser: 4.2.0
 
-  postcss-convert-values@6.1.0(postcss@8.4.38):
+  postcss-convert-values@6.1.0(postcss@8.4.47):
     dependencies:
       browserslist: 4.23.0
-      postcss: 8.4.38
+      postcss: 8.4.47
       postcss-value-parser: 4.2.0
 
-  postcss-discard-comments@6.0.2(postcss@8.4.38):
+  postcss-discard-comments@6.0.2(postcss@8.4.47):
     dependencies:
-      postcss: 8.4.38
+      postcss: 8.4.47
 
-  postcss-discard-duplicates@6.0.3(postcss@8.4.38):
+  postcss-discard-duplicates@6.0.3(postcss@8.4.47):
     dependencies:
-      postcss: 8.4.38
+      postcss: 8.4.47
 
-  postcss-discard-empty@6.0.3(postcss@8.4.38):
+  postcss-discard-empty@6.0.3(postcss@8.4.47):
     dependencies:
-      postcss: 8.4.38
+      postcss: 8.4.47
 
-  postcss-discard-overridden@6.0.2(postcss@8.4.38):
+  postcss-discard-overridden@6.0.2(postcss@8.4.47):
     dependencies:
-      postcss: 8.4.38
+      postcss: 8.4.47
 
-  postcss-merge-longhand@6.0.5(postcss@8.4.38):
+  postcss-merge-longhand@6.0.5(postcss@8.4.47):
     dependencies:
-      postcss: 8.4.38
+      postcss: 8.4.47
       postcss-value-parser: 4.2.0
-      stylehacks: 6.1.1(postcss@8.4.38)
+      stylehacks: 6.1.1(postcss@8.4.47)
 
-  postcss-merge-rules@6.1.1(postcss@8.4.38):
+  postcss-merge-rules@6.1.1(postcss@8.4.47):
     dependencies:
       browserslist: 4.23.0
       caniuse-api: 3.0.0
-      cssnano-utils: 4.0.2(postcss@8.4.38)
-      postcss: 8.4.38
+      cssnano-utils: 4.0.2(postcss@8.4.47)
+      postcss: 8.4.47
       postcss-selector-parser: 6.0.16
 
-  postcss-minify-font-values@6.1.0(postcss@8.4.38):
+  postcss-minify-font-values@6.1.0(postcss@8.4.47):
     dependencies:
-      postcss: 8.4.38
+      postcss: 8.4.47
       postcss-value-parser: 4.2.0
 
-  postcss-minify-gradients@6.0.3(postcss@8.4.38):
+  postcss-minify-gradients@6.0.3(postcss@8.4.47):
     dependencies:
       colord: 2.9.3
-      cssnano-utils: 4.0.2(postcss@8.4.38)
-      postcss: 8.4.38
+      cssnano-utils: 4.0.2(postcss@8.4.47)
+      postcss: 8.4.47
       postcss-value-parser: 4.2.0
 
-  postcss-minify-params@6.1.0(postcss@8.4.38):
+  postcss-minify-params@6.1.0(postcss@8.4.47):
     dependencies:
       browserslist: 4.23.0
-      cssnano-utils: 4.0.2(postcss@8.4.38)
-      postcss: 8.4.38
+      cssnano-utils: 4.0.2(postcss@8.4.47)
+      postcss: 8.4.47
       postcss-value-parser: 4.2.0
 
-  postcss-minify-selectors@6.0.4(postcss@8.4.38):
+  postcss-minify-selectors@6.0.4(postcss@8.4.47):
     dependencies:
-      postcss: 8.4.38
+      postcss: 8.4.47
       postcss-selector-parser: 6.0.16
 
-  postcss-normalize-charset@6.0.2(postcss@8.4.38):
+  postcss-normalize-charset@6.0.2(postcss@8.4.47):
     dependencies:
-      postcss: 8.4.38
+      postcss: 8.4.47
 
-  postcss-normalize-display-values@6.0.2(postcss@8.4.38):
+  postcss-normalize-display-values@6.0.2(postcss@8.4.47):
     dependencies:
-      postcss: 8.4.38
+      postcss: 8.4.47
       postcss-value-parser: 4.2.0
 
-  postcss-normalize-positions@6.0.2(postcss@8.4.38):
+  postcss-normalize-positions@6.0.2(postcss@8.4.47):
     dependencies:
-      postcss: 8.4.38
+      postcss: 8.4.47
       postcss-value-parser: 4.2.0
 
-  postcss-normalize-repeat-style@6.0.2(postcss@8.4.38):
+  postcss-normalize-repeat-style@6.0.2(postcss@8.4.47):
     dependencies:
-      postcss: 8.4.38
+      postcss: 8.4.47
       postcss-value-parser: 4.2.0
 
-  postcss-normalize-string@6.0.2(postcss@8.4.38):
+  postcss-normalize-string@6.0.2(postcss@8.4.47):
     dependencies:
-      postcss: 8.4.38
+      postcss: 8.4.47
       postcss-value-parser: 4.2.0
 
-  postcss-normalize-timing-functions@6.0.2(postcss@8.4.38):
+  postcss-normalize-timing-functions@6.0.2(postcss@8.4.47):
     dependencies:
-      postcss: 8.4.38
+      postcss: 8.4.47
       postcss-value-parser: 4.2.0
 
-  postcss-normalize-unicode@6.1.0(postcss@8.4.38):
+  postcss-normalize-unicode@6.1.0(postcss@8.4.47):
     dependencies:
       browserslist: 4.23.0
-      postcss: 8.4.38
+      postcss: 8.4.47
       postcss-value-parser: 4.2.0
 
-  postcss-normalize-url@6.0.2(postcss@8.4.38):
+  postcss-normalize-url@6.0.2(postcss@8.4.47):
     dependencies:
-      postcss: 8.4.38
+      postcss: 8.4.47
       postcss-value-parser: 4.2.0
 
-  postcss-normalize-whitespace@6.0.2(postcss@8.4.38):
+  postcss-normalize-whitespace@6.0.2(postcss@8.4.47):
     dependencies:
-      postcss: 8.4.38
+      postcss: 8.4.47
       postcss-value-parser: 4.2.0
 
-  postcss-ordered-values@6.0.2(postcss@8.4.38):
+  postcss-ordered-values@6.0.2(postcss@8.4.47):
     dependencies:
-      cssnano-utils: 4.0.2(postcss@8.4.38)
-      postcss: 8.4.38
+      cssnano-utils: 4.0.2(postcss@8.4.47)
+      postcss: 8.4.47
       postcss-value-parser: 4.2.0
 
-  postcss-reduce-initial@6.1.0(postcss@8.4.38):
+  postcss-reduce-initial@6.1.0(postcss@8.4.47):
     dependencies:
       browserslist: 4.23.0
       caniuse-api: 3.0.0
-      postcss: 8.4.38
+      postcss: 8.4.47
 
-  postcss-reduce-transforms@6.0.2(postcss@8.4.38):
+  postcss-reduce-transforms@6.0.2(postcss@8.4.47):
     dependencies:
-      postcss: 8.4.38
+      postcss: 8.4.47
       postcss-value-parser: 4.2.0
 
-  postcss-selector-parser@6.0.15:
-    dependencies:
-      cssesc: 3.0.0
-      util-deprecate: 1.0.2
-
   postcss-selector-parser@6.0.16:
     dependencies:
       cssesc: 3.0.0
       util-deprecate: 1.0.2
 
-  postcss-svgo@6.0.3(postcss@8.4.38):
+  postcss-svgo@6.0.3(postcss@8.4.47):
     dependencies:
-      postcss: 8.4.38
+      postcss: 8.4.47
       postcss-value-parser: 4.2.0
       svgo: 3.2.0
 
-  postcss-unique-selectors@6.0.4(postcss@8.4.38):
+  postcss-unique-selectors@6.0.4(postcss@8.4.47):
     dependencies:
-      postcss: 8.4.38
+      postcss: 8.4.47
       postcss-selector-parser: 6.0.16
 
   postcss-value-parser@4.2.0: {}
 
-  postcss@8.4.38:
+  postcss@8.4.47:
     dependencies:
       nanoid: 3.3.7
-      picocolors: 1.0.0
-      source-map-js: 1.2.0
+      picocolors: 1.1.0
+      source-map-js: 1.2.1
 
   postgres-array@2.0.0: {}
 
@@ -22012,7 +22082,7 @@ snapshots:
 
   prelude-ls@1.2.1: {}
 
-  prettier@3.2.5: {}
+  prettier@3.3.3: {}
 
   pretty-bytes@5.6.0: {}
 
@@ -22028,7 +22098,9 @@ snapshots:
       ansi-styles: 5.2.0
       react-is: 18.2.0
 
-  pretty-hrtime@1.0.3: {}
+  pretty-ms@9.0.0:
+    dependencies:
+      parse-ms: 4.0.0
 
   private-ip@2.3.3:
     dependencies:
@@ -22045,7 +22117,7 @@ snapshots:
     transitivePeerDependencies:
       - supports-color
 
-  proc-log@3.0.0: {}
+  proc-log@4.2.0: {}
 
   process-exists@5.0.0:
     dependencies:
@@ -22053,10 +22125,10 @@ snapshots:
 
   process-nextick-args@2.0.1: {}
 
-  process-warning@2.2.0: {}
-
   process-warning@3.0.0: {}
 
+  process-warning@4.0.0: {}
+
   process@0.11.10: {}
 
   progress@2.0.3:
@@ -22086,6 +22158,8 @@ snapshots:
       object-assign: 4.1.1
       react-is: 16.13.1
 
+  property-information@6.5.0: {}
+
   proto-list@1.2.4: {}
 
   proxy-addr@2.0.7:
@@ -22115,19 +22189,21 @@ snapshots:
       js-stringify: 1.0.2
       pug-runtime: 3.0.1
 
-  pug-code-gen@3.0.2:
+  pug-code-gen@3.0.3:
     dependencies:
       constantinople: 4.0.1
       doctypes: 1.1.0
       js-stringify: 1.0.2
       pug-attrs: 3.0.0
-      pug-error: 2.0.0
+      pug-error: 2.1.0
       pug-runtime: 3.0.1
       void-elements: 3.1.0
       with: 7.0.2
 
   pug-error@2.0.0: {}
 
+  pug-error@2.1.0: {}
+
   pug-filters@4.0.0:
     dependencies:
       constantinople: 4.0.1
@@ -22165,9 +22241,9 @@ snapshots:
 
   pug-walk@2.0.0: {}
 
-  pug@3.0.2:
+  pug@3.0.3:
     dependencies:
-      pug-code-gen: 3.0.2
+      pug-code-gen: 3.0.3
       pug-filters: 4.0.0
       pug-lexer: 5.0.1
       pug-linker: 4.0.0
@@ -22176,50 +22252,30 @@ snapshots:
       pug-runtime: 3.0.1
       pug-strip-comments: 2.0.0
 
-  pump@2.0.1:
-    dependencies:
-      end-of-stream: 1.4.4
-      once: 1.4.0
-
   pump@3.0.0:
     dependencies:
       end-of-stream: 1.4.4
       once: 1.4.0
 
-  pumpify@1.5.1:
-    dependencies:
-      duplexify: 3.7.1
-      inherits: 2.0.4
-      pump: 2.0.1
-
   punycode@2.3.1: {}
 
   pure-rand@6.0.0: {}
 
   pvtsutils@1.3.5:
     dependencies:
-      tslib: 2.6.2
+      tslib: 2.7.0
 
   pvutils@1.1.3: {}
 
-  qrcode@1.5.3:
+  qrcode@1.5.4:
     dependencies:
       dijkstrajs: 1.0.2
-      encode-utf8: 1.0.3
       pngjs: 5.0.0
       yargs: 15.4.1
 
-  qs@6.10.4:
+  qs@6.13.0:
     dependencies:
-      side-channel: 1.0.4
-
-  qs@6.11.0:
-    dependencies:
-      side-channel: 1.0.4
-
-  qs@6.11.1:
-    dependencies:
-      side-channel: 1.0.4
+      side-channel: 1.0.6
 
   qs@6.5.3: {}
 
@@ -22237,8 +22293,6 @@ snapshots:
 
   quick-lru@5.1.1: {}
 
-  ramda@0.29.0: {}
-
   random-seed@0.3.0:
     dependencies:
       json-stringify-safe: 5.0.1
@@ -22247,13 +22301,6 @@ snapshots:
 
   ratelimiter@3.4.1: {}
 
-  raw-body@2.5.1:
-    dependencies:
-      bytes: 3.1.2
-      http-errors: 2.0.0
-      iconv-lite: 0.4.24
-      unpipe: 1.0.0
-
   raw-body@2.5.2:
     dependencies:
       bytes: 3.1.2
@@ -22261,15 +22308,22 @@ snapshots:
       iconv-lite: 0.4.24
       unpipe: 1.0.0
 
+  raw-body@3.0.0:
+    dependencies:
+      bytes: 3.1.2
+      http-errors: 2.0.0
+      iconv-lite: 0.6.3
+      unpipe: 1.0.0
+
   rdf-canonize@3.4.0:
     dependencies:
       setimmediate: 1.0.5
 
-  re2@1.20.10:
+  re2@1.21.4:
     dependencies:
       install-artifact-from-github: 1.3.5
-      nan: 2.18.0
-      node-gyp: 10.0.1
+      nan: 2.20.0
+      node-gyp: 10.2.0
     transitivePeerDependencies:
       - supports-color
 
@@ -22278,15 +22332,15 @@ snapshots:
       react: 18.3.1
       react-dom: 18.3.1(react@18.3.1)
 
-  react-docgen-typescript@2.2.2(typescript@5.4.5):
+  react-docgen-typescript@2.2.2(typescript@5.6.2):
     dependencies:
-      typescript: 5.4.5
+      typescript: 5.6.2
 
   react-docgen@7.0.1:
     dependencies:
-      '@babel/core': 7.24.0
-      '@babel/traverse': 7.24.0
-      '@babel/types': 7.24.0
+      '@babel/core': 7.24.7
+      '@babel/traverse': 7.24.7
+      '@babel/types': 7.25.6
       '@types/babel__core': 7.20.0
       '@types/babel__traverse': 7.20.0
       '@types/doctrine': 0.0.9
@@ -22380,21 +22434,13 @@ snapshots:
 
   real-require@0.2.0: {}
 
-  recast@0.23.4:
-    dependencies:
-      assert: 2.1.0
-      ast-types: 0.16.1
-      esprima: 4.0.1
-      source-map: 0.6.1
-      tslib: 2.6.2
-
   recast@0.23.6:
     dependencies:
       ast-types: 0.16.1
       esprima: 4.0.1
       source-map: 0.6.1
       tiny-invariant: 1.3.3
-      tslib: 2.6.2
+      tslib: 2.7.0
 
   reconnecting-websocket@4.4.0: {}
 
@@ -22417,19 +22463,11 @@ snapshots:
 
   reflect-metadata@0.2.2: {}
 
-  regenerate-unicode-properties@10.1.0:
-    dependencies:
-      regenerate: 1.4.2
-
-  regenerate@1.4.2: {}
-
   regenerator-runtime@0.13.11: {}
 
   regenerator-runtime@0.14.0: {}
 
-  regenerator-transform@0.15.2:
-    dependencies:
-      '@babel/runtime': 7.23.4
+  regex@4.3.3: {}
 
   regexp.prototype.flags@1.5.0:
     dependencies:
@@ -22437,18 +22475,12 @@ snapshots:
       define-properties: 1.2.0
       functions-have-names: 1.2.3
 
-  regexpu-core@5.3.2:
+  regexp.prototype.flags@1.5.2:
     dependencies:
-      '@babel/regjsgen': 0.8.0
-      regenerate: 1.4.2
-      regenerate-unicode-properties: 10.1.0
-      regjsparser: 0.9.1
-      unicode-match-property-ecmascript: 2.0.0
-      unicode-match-property-value-ecmascript: 2.1.0
-
-  regjsparser@0.9.1:
-    dependencies:
-      jsesc: 0.5.0
+      call-bind: 1.0.7
+      define-properties: 1.2.1
+      es-errors: 1.3.0
+      set-function-name: 2.0.2
 
   rehype-external-links@3.0.0:
     dependencies:
@@ -22532,7 +22564,7 @@ snapshots:
 
   require-in-the-middle@7.3.0:
     dependencies:
-      debug: 4.3.4(supports-color@8.1.1)
+      debug: 4.3.7(supports-color@8.1.1)
       module-details-from-path: 1.0.3
       resolve: 1.22.8
     transitivePeerDependencies:
@@ -22556,11 +22588,6 @@ snapshots:
 
   resolve.exports@2.0.0: {}
 
-  resolve@1.19.0:
-    dependencies:
-      is-core-module: 2.13.1
-      path-parse: 1.0.7
-
   resolve@1.22.8:
     dependencies:
       is-core-module: 2.13.1
@@ -22580,7 +22607,7 @@ snapshots:
       onetime: 5.1.2
       signal-exit: 3.0.7
 
-  ret@0.4.3: {}
+  ret@0.5.0: {}
 
   retry@0.12.0: {}
 
@@ -22588,9 +22615,7 @@ snapshots:
 
   rfdc@1.3.0: {}
 
-  rimraf@2.6.3:
-    dependencies:
-      glob: 7.2.3
+  rfdc@1.4.1: {}
 
   rimraf@2.7.1:
     dependencies:
@@ -22600,31 +22625,34 @@ snapshots:
   rimraf@3.0.2:
     dependencies:
       glob: 7.2.3
+    optional: true
 
-  rollup@4.17.2:
+  rollup@4.22.5:
     dependencies:
-      '@types/estree': 1.0.5
+      '@types/estree': 1.0.6
     optionalDependencies:
-      '@rollup/rollup-android-arm-eabi': 4.17.2
-      '@rollup/rollup-android-arm64': 4.17.2
-      '@rollup/rollup-darwin-arm64': 4.17.2
-      '@rollup/rollup-darwin-x64': 4.17.2
-      '@rollup/rollup-linux-arm-gnueabihf': 4.17.2
-      '@rollup/rollup-linux-arm-musleabihf': 4.17.2
-      '@rollup/rollup-linux-arm64-gnu': 4.17.2
-      '@rollup/rollup-linux-arm64-musl': 4.17.2
-      '@rollup/rollup-linux-powerpc64le-gnu': 4.17.2
-      '@rollup/rollup-linux-riscv64-gnu': 4.17.2
-      '@rollup/rollup-linux-s390x-gnu': 4.17.2
-      '@rollup/rollup-linux-x64-gnu': 4.17.2
-      '@rollup/rollup-linux-x64-musl': 4.17.2
-      '@rollup/rollup-win32-arm64-msvc': 4.17.2
-      '@rollup/rollup-win32-ia32-msvc': 4.17.2
-      '@rollup/rollup-win32-x64-msvc': 4.17.2
+      '@rollup/rollup-android-arm-eabi': 4.22.5
+      '@rollup/rollup-android-arm64': 4.22.5
+      '@rollup/rollup-darwin-arm64': 4.22.5
+      '@rollup/rollup-darwin-x64': 4.22.5
+      '@rollup/rollup-linux-arm-gnueabihf': 4.22.5
+      '@rollup/rollup-linux-arm-musleabihf': 4.22.5
+      '@rollup/rollup-linux-arm64-gnu': 4.22.5
+      '@rollup/rollup-linux-arm64-musl': 4.22.5
+      '@rollup/rollup-linux-powerpc64le-gnu': 4.22.5
+      '@rollup/rollup-linux-riscv64-gnu': 4.22.5
+      '@rollup/rollup-linux-s390x-gnu': 4.22.5
+      '@rollup/rollup-linux-x64-gnu': 4.22.5
+      '@rollup/rollup-linux-x64-musl': 4.22.5
+      '@rollup/rollup-win32-arm64-msvc': 4.22.5
+      '@rollup/rollup-win32-ia32-msvc': 4.22.5
+      '@rollup/rollup-win32-x64-msvc': 4.22.5
       fsevents: 2.3.3
 
   rrweb-cssom@0.6.0: {}
 
+  rrweb-cssom@0.7.1: {}
+
   rss-parser@3.13.0:
     dependencies:
       entities: 2.2.0
@@ -22640,8 +22668,15 @@ snapshots:
 
   safe-array-concat@1.0.0:
     dependencies:
-      call-bind: 1.0.2
-      get-intrinsic: 1.2.1
+      call-bind: 1.0.7
+      get-intrinsic: 1.2.4
+      has-symbols: 1.0.3
+      isarray: 2.0.5
+
+  safe-array-concat@1.1.2:
+    dependencies:
+      call-bind: 1.0.7
+      get-intrinsic: 1.2.4
       has-symbols: 1.0.3
       isarray: 2.0.5
 
@@ -22651,33 +22686,45 @@ snapshots:
 
   safe-regex-test@1.0.0:
     dependencies:
-      call-bind: 1.0.2
-      get-intrinsic: 1.2.1
+      call-bind: 1.0.7
+      get-intrinsic: 1.2.4
       is-regex: 1.1.4
 
-  safe-regex2@3.1.0:
+  safe-regex-test@1.0.3:
     dependencies:
-      ret: 0.4.3
+      call-bind: 1.0.7
+      es-errors: 1.3.0
+      is-regex: 1.1.4
+
+  safe-regex2@4.0.0:
+    dependencies:
+      ret: 0.5.0
 
   safe-stable-stringify@2.4.2: {}
 
   safer-buffer@2.1.2: {}
 
-  sanitize-html@2.13.0:
+  sanitize-html@2.13.1:
     dependencies:
       deepmerge: 4.2.2
       escape-string-regexp: 4.0.0
       htmlparser2: 8.0.1
       is-plain-object: 5.0.0
       parse-srcset: 1.0.2
-      postcss: 8.4.38
+      postcss: 8.4.47
 
-  sass@1.76.0:
+  sass@1.79.3:
     dependencies:
       chokidar: 3.5.3
       immutable: 4.2.2
       source-map-js: 1.2.0
 
+  sass@1.79.4:
+    dependencies:
+      chokidar: 3.5.3
+      immutable: 4.2.2
+      source-map-js: 1.2.1
+
   sax@1.2.4: {}
 
   saxes@6.0.0:
@@ -22690,6 +22737,8 @@ snapshots:
 
   secure-json-parse@2.7.0: {}
 
+  secure-json-parse@3.0.0: {}
+
   seedrandom@3.0.5: {}
 
   semver-regex@4.0.5: {}
@@ -22710,7 +22759,9 @@ snapshots:
     dependencies:
       lru-cache: 6.0.0
 
-  send@0.18.0:
+  semver@7.6.3: {}
+
+  send@0.19.0:
     dependencies:
       debug: 2.6.9
       depd: 2.0.0
@@ -22728,12 +22779,12 @@ snapshots:
     transitivePeerDependencies:
       - supports-color
 
-  serve-static@1.15.0:
+  serve-static@1.16.2:
     dependencies:
-      encodeurl: 1.0.2
+      encodeurl: 2.0.0
       escape-html: 1.0.3
       parseurl: 1.3.3
-      send: 0.18.0
+      send: 0.19.0
     transitivePeerDependencies:
       - supports-color
 
@@ -22741,6 +22792,22 @@ snapshots:
 
   set-cookie-parser@2.6.0: {}
 
+  set-function-length@1.2.2:
+    dependencies:
+      define-data-property: 1.1.4
+      es-errors: 1.3.0
+      function-bind: 1.1.2
+      get-intrinsic: 1.2.4
+      gopd: 1.0.1
+      has-property-descriptors: 1.0.2
+
+  set-function-name@2.0.2:
+    dependencies:
+      define-data-property: 1.1.4
+      es-errors: 1.3.0
+      functions-have-names: 1.2.3
+      has-property-descriptors: 1.0.2
+
   setimmediate@1.0.5: {}
 
   setprototypeof@1.2.0: {}
@@ -22750,35 +22817,31 @@ snapshots:
       inherits: 2.0.4
       safe-buffer: 5.2.1
 
-  shallow-clone@3.0.1:
-    dependencies:
-      kind-of: 6.0.3
-
-  sharp@0.33.3:
+  sharp@0.33.5:
     dependencies:
       color: 4.2.3
       detect-libc: 2.0.3
-      semver: 7.6.0
+      semver: 7.6.3
     optionalDependencies:
-      '@img/sharp-darwin-arm64': 0.33.3
-      '@img/sharp-darwin-x64': 0.33.3
-      '@img/sharp-libvips-darwin-arm64': 1.0.2
-      '@img/sharp-libvips-darwin-x64': 1.0.2
-      '@img/sharp-libvips-linux-arm': 1.0.2
-      '@img/sharp-libvips-linux-arm64': 1.0.2
-      '@img/sharp-libvips-linux-s390x': 1.0.2
-      '@img/sharp-libvips-linux-x64': 1.0.2
-      '@img/sharp-libvips-linuxmusl-arm64': 1.0.2
-      '@img/sharp-libvips-linuxmusl-x64': 1.0.2
-      '@img/sharp-linux-arm': 0.33.3
-      '@img/sharp-linux-arm64': 0.33.3
-      '@img/sharp-linux-s390x': 0.33.3
-      '@img/sharp-linux-x64': 0.33.3
-      '@img/sharp-linuxmusl-arm64': 0.33.3
-      '@img/sharp-linuxmusl-x64': 0.33.3
-      '@img/sharp-wasm32': 0.33.3
-      '@img/sharp-win32-ia32': 0.33.3
-      '@img/sharp-win32-x64': 0.33.3
+      '@img/sharp-darwin-arm64': 0.33.5
+      '@img/sharp-darwin-x64': 0.33.5
+      '@img/sharp-libvips-darwin-arm64': 1.0.4
+      '@img/sharp-libvips-darwin-x64': 1.0.4
+      '@img/sharp-libvips-linux-arm': 1.0.5
+      '@img/sharp-libvips-linux-arm64': 1.0.4
+      '@img/sharp-libvips-linux-s390x': 1.0.4
+      '@img/sharp-libvips-linux-x64': 1.0.4
+      '@img/sharp-libvips-linuxmusl-arm64': 1.0.4
+      '@img/sharp-libvips-linuxmusl-x64': 1.0.4
+      '@img/sharp-linux-arm': 0.33.5
+      '@img/sharp-linux-arm64': 0.33.5
+      '@img/sharp-linux-s390x': 0.33.5
+      '@img/sharp-linux-x64': 0.33.5
+      '@img/sharp-linuxmusl-arm64': 0.33.5
+      '@img/sharp-linuxmusl-x64': 0.33.5
+      '@img/sharp-wasm32': 0.33.5
+      '@img/sharp-win32-ia32': 0.33.5
+      '@img/sharp-win32-x64': 0.33.5
 
   shebang-command@1.2.0:
     dependencies:
@@ -22792,9 +22855,14 @@ snapshots:
 
   shebang-regex@3.0.0: {}
 
-  shiki@1.4.0:
+  shiki@1.21.0:
     dependencies:
-      '@shikijs/core': 1.4.0
+      '@shikijs/core': 1.21.0
+      '@shikijs/engine-javascript': 1.21.0
+      '@shikijs/engine-oniguruma': 1.21.0
+      '@shikijs/types': 1.21.0
+      '@shikijs/vscode-textmate': 9.2.2
+      '@types/hast': 3.0.4
 
   shimmer@1.2.1: {}
 
@@ -22804,17 +22872,24 @@ snapshots:
       get-intrinsic: 1.2.1
       object-inspect: 1.12.3
 
+  side-channel@1.0.6:
+    dependencies:
+      call-bind: 1.0.7
+      es-errors: 1.3.0
+      get-intrinsic: 1.2.4
+      object-inspect: 1.13.2
+
   siginfo@2.0.0: {}
 
   signal-exit@3.0.7: {}
 
   signal-exit@4.1.0: {}
 
-  simple-oauth2@5.0.0:
+  simple-oauth2@5.1.0:
     dependencies:
-      '@hapi/hoek': 10.0.1
+      '@hapi/hoek': 11.0.4
       '@hapi/wreck': 18.0.1
-      debug: 4.3.4(supports-color@8.1.1)
+      debug: 4.3.5(supports-color@5.5.0)
       joi: 17.11.0
     transitivePeerDependencies:
       - supports-color
@@ -22825,7 +22900,7 @@ snapshots:
 
   simple-update-notifier@2.0.0:
     dependencies:
-      semver: 7.5.4
+      semver: 7.6.0
 
   sinon@16.1.3:
     dependencies:
@@ -22907,12 +22982,14 @@ snapshots:
       astral-regex: 2.0.0
       is-fullwidth-code-point: 3.0.0
 
+  slick@1.12.2: {}
+
   smart-buffer@4.2.0: {}
 
   socks-proxy-agent@8.0.2:
     dependencies:
       agent-base: 7.1.0
-      debug: 4.3.4(supports-color@8.1.1)
+      debug: 4.3.7(supports-color@8.1.1)
       socks: 2.7.1
     transitivePeerDependencies:
       - supports-color
@@ -22922,7 +22999,7 @@ snapshots:
       ip: 2.0.1
       smart-buffer: 4.2.0
 
-  sonic-boom@3.7.0:
+  sonic-boom@4.0.1:
     dependencies:
       atomic-sleep: 1.0.0
 
@@ -22936,10 +23013,10 @@ snapshots:
 
   sortablejs@1.14.0: {}
 
-  source-map-js@1.0.2: {}
-
   source-map-js@1.2.0: {}
 
+  source-map-js@1.2.1: {}
+
   source-map-support@0.5.13:
     dependencies:
       buffer-from: 1.1.2
@@ -22978,7 +23055,7 @@ snapshots:
 
   sprintf-js@1.0.3: {}
 
-  sprintf-js@1.1.2: {}
+  sprintf-js@1.1.3: {}
 
   sshpk@1.17.0:
     dependencies:
@@ -22992,6 +23069,18 @@ snapshots:
       safer-buffer: 2.1.2
       tweetnacl: 0.14.5
 
+  sshpk@1.18.0:
+    dependencies:
+      asn1: 0.2.6
+      assert-plus: 1.0.0
+      bcrypt-pbkdf: 1.0.2
+      dashdash: 1.14.1
+      ecc-jsbn: 0.1.2
+      getpass: 0.1.7
+      jsbn: 0.1.1
+      safer-buffer: 2.1.2
+      tweetnacl: 0.14.5
+
   ssri@10.0.4:
     dependencies:
       minipass: 5.0.0
@@ -23004,16 +23093,16 @@ snapshots:
 
   standard-as-callback@2.1.0: {}
 
-  start-server-and-test@2.0.3:
+  start-server-and-test@2.0.8:
     dependencies:
       arg: 5.0.2
       bluebird: 3.7.2
       check-more-types: 2.24.0
-      debug: 4.3.4(supports-color@8.1.1)
+      debug: 4.3.7(supports-color@8.1.1)
       execa: 5.1.1
       lazy-ass: 1.6.0
       ps-tree: 1.2.0
-      wait-on: 7.2.0(debug@4.3.4)
+      wait-on: 8.0.1(debug@4.3.7)
     transitivePeerDependencies:
       - supports-color
 
@@ -23025,30 +23114,24 @@ snapshots:
     dependencies:
       internal-slot: 1.0.5
 
-  store2@2.14.2: {}
-
-  storybook-addon-misskey-theme@https://codeload.github.com/misskey-dev/storybook-addon-misskey-theme/tar.gz/cf583db098365b2ccc81a82f63ca9c93bc32b640(@storybook/blocks@8.0.9(@types/react@18.0.28)(encoding@0.1.13)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@storybook/components@8.0.9(@types/react@18.0.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@storybook/core-events@8.0.9)(@storybook/manager-api@8.0.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@storybook/preview-api@8.0.9)(@storybook/theming@8.0.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@storybook/types@8.0.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
+  storybook-addon-misskey-theme@https://codeload.github.com/misskey-dev/storybook-addon-misskey-theme/tar.gz/cf583db098365b2ccc81a82f63ca9c93bc32b640(@storybook/blocks@8.3.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4)))(@storybook/components@8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4)))(@storybook/core-events@8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4)))(@storybook/manager-api@8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4)))(@storybook/preview-api@8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4)))(@storybook/theming@8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4)))(@storybook/types@8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4)))(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
     dependencies:
-      '@storybook/blocks': 8.0.9(@types/react@18.0.28)(encoding@0.1.13)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
-      '@storybook/components': 8.0.9(@types/react@18.0.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
-      '@storybook/core-events': 8.0.9
-      '@storybook/manager-api': 8.0.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
-      '@storybook/preview-api': 8.0.9
-      '@storybook/theming': 8.0.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
-      '@storybook/types': 8.0.9
+      '@storybook/blocks': 8.3.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))
+      '@storybook/components': 8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))
+      '@storybook/core-events': 8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))
+      '@storybook/manager-api': 8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))
+      '@storybook/preview-api': 8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))
+      '@storybook/theming': 8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))
+      '@storybook/types': 8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))
     optionalDependencies:
       react: 18.3.1
       react-dom: 18.3.1(react@18.3.1)
 
-  storybook@8.0.9(@babel/preset-env@7.23.5(@babel/core@7.24.0))(bufferutil@4.0.7)(encoding@0.1.13)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(utf-8-validate@6.0.3):
+  storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4):
     dependencies:
-      '@storybook/cli': 8.0.9(@babel/preset-env@7.23.5(@babel/core@7.24.0))(bufferutil@4.0.7)(encoding@0.1.13)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(utf-8-validate@6.0.3)
+      '@storybook/core': 8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4)
     transitivePeerDependencies:
-      - '@babel/preset-env'
       - bufferutil
-      - encoding
-      - react
-      - react-dom
       - supports-color
       - utf-8-validate
 
@@ -23067,10 +23150,6 @@ snapshots:
     transitivePeerDependencies:
       - supports-color
 
-  stream-shift@1.0.1: {}
-
-  stream-wormhole@1.1.0: {}
-
   streamsearch@1.1.0: {}
 
   streamx@2.15.0:
@@ -23103,22 +23182,41 @@ snapshots:
 
   string.prototype.trim@1.2.7:
     dependencies:
-      call-bind: 1.0.2
+      call-bind: 1.0.7
       define-properties: 1.2.0
       es-abstract: 1.22.1
 
+  string.prototype.trim@1.2.9:
+    dependencies:
+      call-bind: 1.0.7
+      define-properties: 1.2.1
+      es-abstract: 1.23.3
+      es-object-atoms: 1.0.0
+
   string.prototype.trimend@1.0.6:
     dependencies:
-      call-bind: 1.0.2
+      call-bind: 1.0.7
       define-properties: 1.2.0
       es-abstract: 1.22.1
 
+  string.prototype.trimend@1.0.8:
+    dependencies:
+      call-bind: 1.0.7
+      define-properties: 1.2.1
+      es-object-atoms: 1.0.0
+
   string.prototype.trimstart@1.0.6:
     dependencies:
-      call-bind: 1.0.2
+      call-bind: 1.0.7
       define-properties: 1.2.0
       es-abstract: 1.22.1
 
+  string.prototype.trimstart@1.0.8:
+    dependencies:
+      call-bind: 1.0.7
+      define-properties: 1.2.1
+      es-object-atoms: 1.0.0
+
   string_decoder@0.10.31: {}
 
   string_decoder@1.1.1:
@@ -23129,6 +23227,11 @@ snapshots:
     dependencies:
       safe-buffer: 5.2.1
 
+  stringify-entities@4.0.4:
+    dependencies:
+      character-entities-html4: 2.1.0
+      character-entities-legacy: 3.0.0
+
   stringz@2.1.0:
     dependencies:
       char-regex: 1.0.2
@@ -23151,6 +23254,8 @@ snapshots:
 
   strip-final-newline@3.0.0: {}
 
+  strip-final-newline@4.0.0: {}
+
   strip-indent@3.0.0:
     dependencies:
       min-indent: 1.0.1
@@ -23161,9 +23266,9 @@ snapshots:
 
   strip-json-comments@3.1.1: {}
 
-  strip-literal@1.3.0:
+  strip-literal@2.1.0:
     dependencies:
-      acorn: 8.11.3
+      js-tokens: 9.0.0
 
   strip-outer@2.0.0: {}
 
@@ -23172,12 +23277,17 @@ snapshots:
   strtok3@7.0.0:
     dependencies:
       '@tokenizer/token': 0.3.0
-      peek-readable: 5.0.0
+      peek-readable: 5.1.3
 
-  stylehacks@6.1.1(postcss@8.4.38):
+  strtok3@8.1.0:
+    dependencies:
+      '@tokenizer/token': 0.3.0
+      peek-readable: 5.2.0
+
+  stylehacks@6.1.1(postcss@8.4.47):
     dependencies:
       browserslist: 4.23.0
-      postcss: 8.4.38
+      postcss: 8.4.47
       postcss-selector-parser: 6.0.16
 
   supports-color@5.5.0:
@@ -23209,26 +23319,11 @@ snapshots:
       css-tree: 2.3.1
       css-what: 6.1.0
       csso: 5.0.5
-      picocolors: 1.0.0
+      picocolors: 1.0.1
 
   symbol-tree@3.2.4: {}
 
-  systeminformation@5.22.7: {}
-
-  tar-fs@2.1.1:
-    dependencies:
-      chownr: 1.1.4
-      mkdirp-classic: 0.5.3
-      pump: 3.0.0
-      tar-stream: 2.2.0
-
-  tar-stream@2.2.0:
-    dependencies:
-      bl: 4.1.0
-      end-of-stream: 1.4.4
-      fs-constants: 1.0.0
-      inherits: 2.0.4
-      readable-stream: 3.6.0
+  systeminformation@5.23.5: {}
 
   tar-stream@3.1.6:
     dependencies:
@@ -23264,24 +23359,10 @@ snapshots:
     dependencies:
       memoizerific: 1.11.3
 
-  temp-dir@2.0.0: {}
-
-  temp@0.8.4:
+  terser@5.33.0:
     dependencies:
-      rimraf: 2.6.3
-
-  tempy@1.0.1:
-    dependencies:
-      del: 6.1.1
-      is-stream: 2.0.1
-      temp-dir: 2.0.0
-      type-fest: 0.16.0
-      unique-string: 2.0.0
-
-  terser@5.30.3:
-    dependencies:
-      '@jridgewell/source-map': 0.3.5
-      acorn: 8.11.3
+      '@jridgewell/source-map': 0.3.6
+      acorn: 8.12.1
       commander: 2.20.3
       source-map-support: 0.5.21
 
@@ -23303,39 +23384,34 @@ snapshots:
     dependencies:
       any-promise: 1.3.0
 
-  thread-stream@2.3.0:
+  thread-stream@3.1.0:
     dependencies:
       real-require: 0.2.0
 
-  three@0.164.1: {}
+  three@0.169.0: {}
 
-  throttle-debounce@5.0.0: {}
+  throttle-debounce@5.0.2: {}
 
   throttleit@1.0.0: {}
 
-  through2@2.0.5:
-    dependencies:
-      readable-stream: 2.3.7
-      xtend: 4.0.2
-
   through@2.3.4: {}
 
   through@2.3.8: {}
 
-  tiny-invariant@1.3.1: {}
-
   tiny-invariant@1.3.3: {}
 
-  tiny-lru@10.0.1: {}
-
   tinybench@2.6.0: {}
 
   tinycolor2@1.6.0: {}
 
-  tinypool@0.7.0: {}
+  tinypool@0.8.4: {}
+
+  tinyrainbow@1.2.0: {}
 
   tinyspy@2.2.0: {}
 
+  tinyspy@3.0.2: {}
+
   tmp@0.2.3: {}
 
   tmpl@1.0.5: {}
@@ -23348,12 +23424,8 @@ snapshots:
     dependencies:
       is-number: 7.0.0
 
-  toad-cache@3.3.0: {}
-
   toad-cache@3.7.0: {}
 
-  tocbot@4.21.1: {}
-
   toidentifier@1.0.1: {}
 
   token-stream@1.0.0: {}
@@ -23363,6 +23435,11 @@ snapshots:
       '@tokenizer/token': 0.3.0
       ieee754: 1.2.1
 
+  token-types@6.0.0:
+    dependencies:
+      '@tokenizer/token': 0.3.0
+      ieee754: 1.2.1
+
   touch@3.1.0:
     dependencies:
       nopt: 1.0.10
@@ -23372,7 +23449,7 @@ snapshots:
       psl: 1.9.0
       punycode: 2.3.1
 
-  tough-cookie@4.1.3:
+  tough-cookie@4.1.4:
     dependencies:
       psl: 1.9.0
       punycode: 2.3.1
@@ -23387,6 +23464,8 @@ snapshots:
 
   trace-redirect@1.0.6: {}
 
+  trim-lines@3.0.1: {}
+
   trim-newlines@3.0.1: {}
 
   trim-repeated@2.0.0:
@@ -23399,17 +23478,21 @@ snapshots:
     dependencies:
       typescript: 5.3.3
 
-  ts-api-utils@1.3.0(typescript@5.4.5):
+  ts-api-utils@1.3.0(typescript@5.5.4):
     dependencies:
-      typescript: 5.4.5
+      typescript: 5.5.4
 
-  ts-case-convert@2.0.2: {}
+  ts-api-utils@1.3.0(typescript@5.6.2):
+    dependencies:
+      typescript: 5.6.2
+
+  ts-case-convert@2.0.7: {}
 
   ts-dedent@2.2.0: {}
 
   ts-map@1.0.3: {}
 
-  tsc-alias@1.8.8:
+  tsc-alias@1.8.10:
     dependencies:
       chokidar: 3.5.3
       commander: 9.5.0
@@ -23431,9 +23514,9 @@ snapshots:
       minimist: 1.2.8
       strip-bom: 3.0.0
 
-  tsd@0.30.7:
+  tsd@0.31.2:
     dependencies:
-      '@tsd/typescript': 5.3.3
+      '@tsd/typescript': 5.4.5
       eslint-formatter-pretty: 4.1.0
       globby: 11.1.0
       jest-diff: 29.7.0
@@ -23441,10 +23524,12 @@ snapshots:
       path-exists: 4.0.0
       read-pkg-up: 7.0.1
 
-  tslib@1.14.1: {}
-
   tslib@2.6.2: {}
 
+  tslib@2.6.3: {}
+
+  tslib@2.7.0: {}
+
   tsx@4.4.0:
     dependencies:
       esbuild: 0.18.20
@@ -23464,8 +23549,6 @@ snapshots:
 
   type-detect@4.0.8: {}
 
-  type-fest@0.16.0: {}
-
   type-fest@0.18.1: {}
 
   type-fest@0.20.2: {}
@@ -23478,7 +23561,7 @@ snapshots:
 
   type-fest@2.19.0: {}
 
-  type-fest@4.9.0: {}
+  type-fest@4.20.1: {}
 
   type-is@1.6.18:
     dependencies:
@@ -23487,34 +23570,66 @@ snapshots:
 
   typed-array-buffer@1.0.0:
     dependencies:
-      call-bind: 1.0.2
-      get-intrinsic: 1.2.1
+      call-bind: 1.0.7
+      get-intrinsic: 1.2.4
       is-typed-array: 1.1.10
 
+  typed-array-buffer@1.0.2:
+    dependencies:
+      call-bind: 1.0.7
+      es-errors: 1.3.0
+      is-typed-array: 1.1.13
+
   typed-array-byte-length@1.0.0:
     dependencies:
-      call-bind: 1.0.2
+      call-bind: 1.0.7
       for-each: 0.3.3
       has-proto: 1.0.1
       is-typed-array: 1.1.10
 
+  typed-array-byte-length@1.0.1:
+    dependencies:
+      call-bind: 1.0.7
+      for-each: 0.3.3
+      gopd: 1.0.1
+      has-proto: 1.0.3
+      is-typed-array: 1.1.13
+
   typed-array-byte-offset@1.0.0:
     dependencies:
       available-typed-arrays: 1.0.5
-      call-bind: 1.0.2
+      call-bind: 1.0.7
       for-each: 0.3.3
       has-proto: 1.0.1
       is-typed-array: 1.1.10
 
+  typed-array-byte-offset@1.0.2:
+    dependencies:
+      available-typed-arrays: 1.0.7
+      call-bind: 1.0.7
+      for-each: 0.3.3
+      gopd: 1.0.1
+      has-proto: 1.0.3
+      is-typed-array: 1.1.13
+
   typed-array-length@1.0.4:
     dependencies:
-      call-bind: 1.0.2
+      call-bind: 1.0.7
       for-each: 0.3.3
       is-typed-array: 1.1.10
 
+  typed-array-length@1.0.6:
+    dependencies:
+      call-bind: 1.0.7
+      for-each: 0.3.3
+      gopd: 1.0.1
+      has-proto: 1.0.3
+      is-typed-array: 1.1.13
+      possible-typed-array-names: 1.0.0
+
   typedarray@0.0.6: {}
 
-  typeorm@0.3.20(ioredis@5.4.1)(pg@8.11.5):
+  typeorm@0.3.20(ioredis@5.4.1)(pg@8.13.0):
     dependencies:
       '@sqltools/formatter': 1.2.5
       app-root-path: 3.1.0
@@ -23522,7 +23637,7 @@ snapshots:
       chalk: 4.1.2
       cli-highlight: 2.1.11
       dayjs: 1.11.10
-      debug: 4.3.4(supports-color@8.1.1)
+      debug: 4.3.4(supports-color@5.5.0)
       dotenv: 16.0.3
       glob: 10.3.10
       mkdirp: 2.1.6
@@ -23533,7 +23648,7 @@ snapshots:
       yargs: 17.7.2
     optionalDependencies:
       ioredis: 5.4.1
-      pg: 8.11.5
+      pg: 8.13.0
     transitivePeerDependencies:
       - supports-color
 
@@ -23541,24 +23656,25 @@ snapshots:
 
   typescript@5.4.2: {}
 
-  typescript@5.4.5: {}
+  typescript@5.5.4: {}
+
+  typescript@5.6.2: {}
 
   ufo@1.3.2: {}
 
-  uglify-js@3.17.4:
-    optional: true
-
   uid2@0.0.4: {}
 
   uid@2.0.2:
     dependencies:
       '@lukeed/csprng': 1.0.1
 
+  uint8array-extras@1.4.0: {}
+
   ulid@2.3.0: {}
 
   unbox-primitive@1.0.2:
     dependencies:
-      call-bind: 1.0.2
+      call-bind: 1.0.7
       has-bigints: 1.0.2
       has-symbols: 1.0.3
       which-boxed-primitive: 1.0.2
@@ -23567,20 +23683,15 @@ snapshots:
 
   undici-types@5.26.5: {}
 
+  undici-types@6.19.8: {}
+
   undici@5.28.2:
     dependencies:
       '@fastify/busboy': 2.1.0
 
-  unicode-canonical-property-names-ecmascript@2.0.0: {}
+  undici@6.19.8: {}
 
-  unicode-match-property-ecmascript@2.0.0:
-    dependencies:
-      unicode-canonical-property-names-ecmascript: 2.0.0
-      unicode-property-aliases-ecmascript: 2.1.0
-
-  unicode-match-property-value-ecmascript@2.1.0: {}
-
-  unicode-property-aliases-ecmascript@2.1.0: {}
+  unicorn-magic@0.3.0: {}
 
   unified@11.0.4:
     dependencies:
@@ -23602,14 +23713,14 @@ snapshots:
     dependencies:
       imurmurhash: 0.1.4
 
-  unique-string@2.0.0:
-    dependencies:
-      crypto-random-string: 2.0.0
-
   unist-util-is@6.0.0:
     dependencies:
       '@types/unist': 3.0.2
 
+  unist-util-position@5.0.0:
+    dependencies:
+      '@types/unist': 3.0.2
+
   unist-util-stringify-position@4.0.0:
     dependencies:
       '@types/unist': 3.0.2
@@ -23637,7 +23748,7 @@ snapshots:
 
   unplugin@1.4.0:
     dependencies:
-      acorn: 8.11.3
+      acorn: 8.12.1
       chokidar: 3.5.3
       webpack-sources: 3.2.3
       webpack-virtual-modules: 0.5.0
@@ -23648,13 +23759,13 @@ snapshots:
     dependencies:
       browserslist: 4.22.2
       escalade: 3.1.1
-      picocolors: 1.0.0
+      picocolors: 1.0.1
 
   update-browserslist-db@1.0.13(browserslist@4.23.0):
     dependencies:
       browserslist: 4.23.0
       escalade: 3.1.1
-      picocolors: 1.0.0
+      picocolors: 1.0.1
 
   uri-js@4.4.1:
     dependencies:
@@ -23670,6 +23781,11 @@ snapshots:
       node-gyp-build: 4.6.0
     optional: true
 
+  utf-8-validate@6.0.4:
+    dependencies:
+      node-gyp-build: 4.6.0
+    optional: true
+
   util-deprecate@1.0.2: {}
 
   util@0.12.5:
@@ -23677,39 +23793,40 @@ snapshots:
       inherits: 2.0.4
       is-arguments: 1.1.1
       is-generator-function: 1.0.10
-      is-typed-array: 1.1.10
-      which-typed-array: 1.1.11
+      is-typed-array: 1.1.13
+      which-typed-array: 1.1.15
 
   utils-merge@1.0.1: {}
 
+  uuid@10.0.0: {}
+
   uuid@3.4.0: {}
 
   uuid@8.3.2: {}
 
   uuid@9.0.1: {}
 
-  v-code-diff@1.11.0(vue@3.4.26(typescript@5.4.5)):
+  v-code-diff@1.13.1(vue@3.5.11(typescript@5.6.2)):
     dependencies:
-      diff: 5.1.0
+      diff: 5.2.0
       diff-match-patch: 1.0.5
-      highlight.js: 11.9.0
-      vue: 3.4.26(typescript@5.4.5)
-      vue-demi: 0.14.7(vue@3.4.26(typescript@5.4.5))
-      vue-i18n: 9.13.1(vue@3.4.26(typescript@5.4.5))
+      highlight.js: 11.10.0
+      vue: 3.5.11(typescript@5.6.2)
+      vue-demi: 0.14.7(vue@3.5.11(typescript@5.6.2))
 
   v8-to-istanbul@9.2.0:
     dependencies:
-      '@jridgewell/trace-mapping': 0.3.18
+      '@jridgewell/trace-mapping': 0.3.25
       '@types/istanbul-lib-coverage': 2.0.4
       convert-source-map: 2.0.0
 
+  valid-data-url@3.0.1: {}
+
   validate-npm-package-license@3.0.4:
     dependencies:
       spdx-correct: 3.1.1
       spdx-expression-parse: 3.0.1
 
-  validator@13.9.0: {}
-
   vary@1.1.2: {}
 
   verror@1.10.0:
@@ -23729,19 +23846,37 @@ snapshots:
       unist-util-stringify-position: 4.0.0
       vfile-message: 4.0.2
 
-  vite-node@0.34.6(@types/node@20.12.7)(sass@1.76.0)(terser@5.30.3):
+  vite-node@1.6.0(@types/node@20.14.12)(sass@1.79.3)(terser@5.33.0):
     dependencies:
       cac: 6.7.14
-      debug: 4.3.4(supports-color@8.1.1)
-      mlly: 1.5.0
+      debug: 4.3.5(supports-color@5.5.0)
       pathe: 1.1.2
-      picocolors: 1.0.0
-      vite: 5.2.11(@types/node@20.12.7)(sass@1.76.0)(terser@5.30.3)
+      picocolors: 1.0.1
+      vite: 5.4.8(@types/node@20.14.12)(sass@1.79.3)(terser@5.33.0)
     transitivePeerDependencies:
       - '@types/node'
       - less
       - lightningcss
       - sass
+      - sass-embedded
+      - stylus
+      - sugarss
+      - supports-color
+      - terser
+
+  vite-node@1.6.0(@types/node@20.14.12)(sass@1.79.4)(terser@5.33.0):
+    dependencies:
+      cac: 6.7.14
+      debug: 4.3.5(supports-color@5.5.0)
+      pathe: 1.1.2
+      picocolors: 1.0.1
+      vite: 5.4.8(@types/node@20.14.12)(sass@1.79.4)(terser@5.33.0)
+    transitivePeerDependencies:
+      - '@types/node'
+      - less
+      - lightningcss
+      - sass
+      - sass-embedded
       - stylus
       - sugarss
       - supports-color
@@ -23749,57 +23884,102 @@ snapshots:
 
   vite-plugin-turbosnap@1.0.3: {}
 
-  vite@5.2.11(@types/node@20.12.7)(sass@1.76.0)(terser@5.30.3):
+  vite@5.4.8(@types/node@20.14.12)(sass@1.79.3)(terser@5.33.0):
     dependencies:
-      esbuild: 0.20.2
-      postcss: 8.4.38
-      rollup: 4.17.2
+      esbuild: 0.21.5
+      postcss: 8.4.47
+      rollup: 4.22.5
     optionalDependencies:
-      '@types/node': 20.12.7
+      '@types/node': 20.14.12
       fsevents: 2.3.3
-      sass: 1.76.0
-      terser: 5.30.3
+      sass: 1.79.3
+      terser: 5.33.0
 
-  vitest-fetch-mock@0.2.2(encoding@0.1.13)(vitest@0.34.6(happy-dom@10.0.3)(jsdom@24.0.0(bufferutil@4.0.7)(utf-8-validate@6.0.3))(sass@1.76.0)(terser@5.30.3)):
+  vite@5.4.8(@types/node@20.14.12)(sass@1.79.4)(terser@5.33.0):
+    dependencies:
+      esbuild: 0.21.5
+      postcss: 8.4.47
+      rollup: 4.22.5
+    optionalDependencies:
+      '@types/node': 20.14.12
+      fsevents: 2.3.3
+      sass: 1.79.4
+      terser: 5.33.0
+
+  vitest-fetch-mock@0.2.2(encoding@0.1.13)(vitest@1.6.0(@types/node@20.14.12)(happy-dom@10.0.3)(jsdom@24.1.1(bufferutil@4.0.8)(utf-8-validate@6.0.4))(sass@1.79.3)(terser@5.33.0)):
     dependencies:
       cross-fetch: 3.1.6(encoding@0.1.13)
-      vitest: 0.34.6(happy-dom@10.0.3)(jsdom@24.0.0(bufferutil@4.0.7)(utf-8-validate@6.0.3))(sass@1.76.0)(terser@5.30.3)
+      vitest: 1.6.0(@types/node@20.14.12)(happy-dom@10.0.3)(jsdom@24.1.1(bufferutil@4.0.8)(utf-8-validate@6.0.4))(sass@1.79.3)(terser@5.33.0)
     transitivePeerDependencies:
       - encoding
 
-  vitest@0.34.6(happy-dom@10.0.3)(jsdom@24.0.0(bufferutil@4.0.7)(utf-8-validate@6.0.3))(sass@1.76.0)(terser@5.30.3):
+  vitest@1.6.0(@types/node@20.14.12)(happy-dom@10.0.3)(jsdom@24.1.1(bufferutil@4.0.8)(utf-8-validate@6.0.4))(sass@1.79.3)(terser@5.33.0):
     dependencies:
-      '@types/chai': 4.3.11
-      '@types/chai-subset': 1.3.5
-      '@types/node': 20.12.7
-      '@vitest/expect': 0.34.6
-      '@vitest/runner': 0.34.6
-      '@vitest/snapshot': 0.34.6
-      '@vitest/spy': 0.34.6
-      '@vitest/utils': 0.34.6
-      acorn: 8.11.3
+      '@vitest/expect': 1.6.0
+      '@vitest/runner': 1.6.0
+      '@vitest/snapshot': 1.6.0
+      '@vitest/spy': 1.6.0
+      '@vitest/utils': 1.6.0
       acorn-walk: 8.3.2
-      cac: 6.7.14
       chai: 4.3.10
-      debug: 4.3.4(supports-color@8.1.1)
-      local-pkg: 0.4.3
-      magic-string: 0.30.7
+      debug: 4.3.4(supports-color@5.5.0)
+      execa: 8.0.1
+      local-pkg: 0.5.0
+      magic-string: 0.30.10
       pathe: 1.1.2
       picocolors: 1.0.0
       std-env: 3.7.0
-      strip-literal: 1.3.0
+      strip-literal: 2.1.0
       tinybench: 2.6.0
-      tinypool: 0.7.0
-      vite: 5.2.11(@types/node@20.12.7)(sass@1.76.0)(terser@5.30.3)
-      vite-node: 0.34.6(@types/node@20.12.7)(sass@1.76.0)(terser@5.30.3)
+      tinypool: 0.8.4
+      vite: 5.4.8(@types/node@20.14.12)(sass@1.79.3)(terser@5.33.0)
+      vite-node: 1.6.0(@types/node@20.14.12)(sass@1.79.3)(terser@5.33.0)
       why-is-node-running: 2.2.2
     optionalDependencies:
+      '@types/node': 20.14.12
       happy-dom: 10.0.3
-      jsdom: 24.0.0(bufferutil@4.0.7)(utf-8-validate@6.0.3)
+      jsdom: 24.1.1(bufferutil@4.0.8)(utf-8-validate@6.0.4)
     transitivePeerDependencies:
       - less
       - lightningcss
       - sass
+      - sass-embedded
+      - stylus
+      - sugarss
+      - supports-color
+      - terser
+
+  vitest@1.6.0(@types/node@20.14.12)(happy-dom@10.0.3)(jsdom@24.1.1(bufferutil@4.0.8)(utf-8-validate@6.0.4))(sass@1.79.4)(terser@5.33.0):
+    dependencies:
+      '@vitest/expect': 1.6.0
+      '@vitest/runner': 1.6.0
+      '@vitest/snapshot': 1.6.0
+      '@vitest/spy': 1.6.0
+      '@vitest/utils': 1.6.0
+      acorn-walk: 8.3.2
+      chai: 4.3.10
+      debug: 4.3.4(supports-color@5.5.0)
+      execa: 8.0.1
+      local-pkg: 0.5.0
+      magic-string: 0.30.10
+      pathe: 1.1.2
+      picocolors: 1.0.0
+      std-env: 3.7.0
+      strip-literal: 2.1.0
+      tinybench: 2.6.0
+      tinypool: 0.8.4
+      vite: 5.4.8(@types/node@20.14.12)(sass@1.79.4)(terser@5.33.0)
+      vite-node: 1.6.0(@types/node@20.14.12)(sass@1.79.4)(terser@5.33.0)
+      why-is-node-running: 2.2.2
+    optionalDependencies:
+      '@types/node': 20.14.12
+      happy-dom: 10.0.3
+      jsdom: 24.1.1(bufferutil@4.0.8)(utf-8-validate@6.0.4)
+    transitivePeerDependencies:
+      - less
+      - lightningcss
+      - sass
+      - sass-embedded
       - stylus
       - sugarss
       - supports-color
@@ -23812,7 +23992,7 @@ snapshots:
   vscode-languageclient@9.0.1:
     dependencies:
       minimatch: 5.1.2
-      semver: 7.6.0
+      semver: 7.6.3
       vscode-languageserver-protocol: 3.17.5
 
   vscode-languageserver-protocol@3.17.5:
@@ -23828,99 +24008,104 @@ snapshots:
     dependencies:
       vscode-languageserver-protocol: 3.17.5
 
-  vue-component-meta@2.0.16(typescript@5.4.5):
+  vscode-uri@3.0.8: {}
+
+  vue-component-meta@2.0.16(typescript@5.6.2):
     dependencies:
       '@volar/typescript': 2.2.0
-      '@vue/language-core': 2.0.16(typescript@5.4.5)
+      '@vue/language-core': 2.0.16(typescript@5.6.2)
       path-browserify: 1.0.1
       vue-component-type-helpers: 2.0.16
     optionalDependencies:
-      typescript: 5.4.5
+      typescript: 5.6.2
 
   vue-component-type-helpers@1.8.4: {}
 
   vue-component-type-helpers@2.0.16: {}
 
-  vue-component-type-helpers@2.0.19: {}
+  vue-component-type-helpers@2.1.6: {}
 
-  vue-demi@0.14.7(vue@3.4.26(typescript@5.4.5)):
+  vue-demi@0.14.7(vue@3.5.11(typescript@5.6.2)):
     dependencies:
-      vue: 3.4.26(typescript@5.4.5)
+      vue: 3.5.11(typescript@5.6.2)
 
-  vue-docgen-api@4.75.1(vue@3.4.26(typescript@5.4.5)):
+  vue-docgen-api@4.75.1(vue@3.5.11(typescript@5.6.2)):
     dependencies:
-      '@babel/parser': 7.24.0
-      '@babel/types': 7.24.0
-      '@vue/compiler-dom': 3.4.21
-      '@vue/compiler-sfc': 3.4.26
+      '@babel/parser': 7.25.6
+      '@babel/types': 7.25.6
+      '@vue/compiler-dom': 3.5.10
+      '@vue/compiler-sfc': 3.5.11
       ast-types: 0.16.1
       hash-sum: 2.0.0
       lru-cache: 8.0.4
-      pug: 3.0.2
-      recast: 0.23.4
+      pug: 3.0.3
+      recast: 0.23.6
       ts-map: 1.0.3
-      vue: 3.4.26(typescript@5.4.5)
-      vue-inbrowser-compiler-independent-utils: 4.71.1(vue@3.4.26(typescript@5.4.5))
+      vue: 3.5.11(typescript@5.6.2)
+      vue-inbrowser-compiler-independent-utils: 4.71.1(vue@3.5.11(typescript@5.6.2))
 
-  vue-eslint-parser@9.4.2(eslint@8.57.0):
+  vue-eslint-parser@9.4.3(eslint@9.11.0):
     dependencies:
-      debug: 4.3.4(supports-color@8.1.1)
-      eslint: 8.57.0
+      debug: 4.3.5(supports-color@5.5.0)
+      eslint: 9.11.0
       eslint-scope: 7.2.2
       eslint-visitor-keys: 3.4.3
       espree: 9.6.1
-      esquery: 1.4.2
+      esquery: 1.6.0
       lodash: 4.17.21
-      semver: 7.5.4
+      semver: 7.6.0
     transitivePeerDependencies:
       - supports-color
 
-  vue-i18n@9.13.1(vue@3.4.26(typescript@5.4.5)):
+  vue-inbrowser-compiler-independent-utils@4.71.1(vue@3.5.11(typescript@5.6.2)):
     dependencies:
-      '@intlify/core-base': 9.13.1
-      '@intlify/shared': 9.13.1
-      '@vue/devtools-api': 6.6.1
-      vue: 3.4.26(typescript@5.4.5)
-
-  vue-inbrowser-compiler-independent-utils@4.71.1(vue@3.4.26(typescript@5.4.5)):
-    dependencies:
-      vue: 3.4.26(typescript@5.4.5)
+      vue: 3.5.11(typescript@5.6.2)
 
   vue-template-compiler@2.7.14:
     dependencies:
       de-indent: 1.0.2
       he: 1.2.0
 
-  vue-tsc@2.0.16(typescript@5.4.5):
+  vue-tsc@2.1.6(typescript@5.6.2):
     dependencies:
-      '@volar/typescript': 2.2.0
-      '@vue/language-core': 2.0.16(typescript@5.4.5)
+      '@volar/typescript': 2.4.5
+      '@vue/language-core': 2.1.6(typescript@5.6.2)
       semver: 7.6.0
-      typescript: 5.4.5
+      typescript: 5.6.2
 
-  vue@3.4.26(typescript@5.4.5):
+  vue@3.4.37(typescript@5.5.4):
     dependencies:
-      '@vue/compiler-dom': 3.4.26
-      '@vue/compiler-sfc': 3.4.26
-      '@vue/runtime-dom': 3.4.26
-      '@vue/server-renderer': 3.4.26(vue@3.4.26(typescript@5.4.5))
-      '@vue/shared': 3.4.26
+      '@vue/compiler-dom': 3.4.37
+      '@vue/compiler-sfc': 3.4.37
+      '@vue/runtime-dom': 3.4.37
+      '@vue/server-renderer': 3.4.37(vue@3.4.37(typescript@5.5.4))
+      '@vue/shared': 3.4.37
     optionalDependencies:
-      typescript: 5.4.5
+      typescript: 5.5.4
 
-  vuedraggable@4.1.0(vue@3.4.26(typescript@5.4.5)):
+  vue@3.5.11(typescript@5.6.2):
+    dependencies:
+      '@vue/compiler-dom': 3.5.11
+      '@vue/compiler-sfc': 3.5.11
+      '@vue/runtime-dom': 3.5.11
+      '@vue/server-renderer': 3.5.11(vue@3.5.11(typescript@5.6.2))
+      '@vue/shared': 3.5.11
+    optionalDependencies:
+      typescript: 5.6.2
+
+  vuedraggable@4.1.0(vue@3.5.11(typescript@5.6.2)):
     dependencies:
       sortablejs: 1.14.0
-      vue: 3.4.26(typescript@5.4.5)
+      vue: 3.5.11(typescript@5.6.2)
 
   w3c-xmlserializer@5.0.0:
     dependencies:
       xml-name-validator: 5.0.0
 
-  wait-on@7.2.0(debug@4.3.4):
+  wait-on@8.0.1(debug@4.3.7):
     dependencies:
-      axios: 1.6.2(debug@4.3.4)
-      joi: 17.11.0
+      axios: 1.7.7(debug@4.3.7)
+      joi: 17.13.3
       lodash: 4.17.21
       minimist: 1.2.8
       rxjs: 7.8.1
@@ -23931,15 +24116,6 @@ snapshots:
     dependencies:
       makeerror: 1.0.12
 
-  watchpack@2.4.0:
-    dependencies:
-      glob-to-regexp: 0.4.1
-      graceful-fs: 4.2.11
-
-  wcwidth@1.0.1:
-    dependencies:
-      defaults: 1.0.4
-
   web-push@3.6.7:
     dependencies:
       asn1.js: 5.4.1
@@ -23950,8 +24126,19 @@ snapshots:
     transitivePeerDependencies:
       - supports-color
 
+  web-resource-inliner@7.0.0:
+    dependencies:
+      ansi-colors: 4.1.3
+      escape-goat: 3.0.0
+      htmlparser2: 5.0.1
+      mime: 2.6.0
+      valid-data-url: 3.0.1
+
   web-streams-polyfill@3.2.1: {}
 
+  web-streams-polyfill@4.0.0:
+    optional: true
+
   webidl-conversions@3.0.1: {}
 
   webidl-conversions@7.0.0: {}
@@ -24007,6 +24194,14 @@ snapshots:
       gopd: 1.0.1
       has-tostringtag: 1.0.0
 
+  which-typed-array@1.1.15:
+    dependencies:
+      available-typed-arrays: 1.0.7
+      call-bind: 1.0.7
+      for-each: 0.3.3
+      gopd: 1.0.1
+      has-tostringtag: 1.0.2
+
   which@1.3.1:
     dependencies:
       isexe: 2.0.0
@@ -24031,12 +24226,12 @@ snapshots:
 
   with@7.0.2:
     dependencies:
-      '@babel/parser': 7.23.9
-      '@babel/types': 7.23.5
+      '@babel/parser': 7.24.7
+      '@babel/types': 7.24.7
       assert-never: 1.2.1
       babel-walk: 3.0.0-canary-5
 
-  wordwrap@1.0.0: {}
+  word-wrap@1.2.5: {}
 
   wrap-ansi@6.2.0:
     dependencies:
@@ -24058,22 +24253,21 @@ snapshots:
 
   wrappy@1.0.2: {}
 
-  write-file-atomic@2.4.3:
-    dependencies:
-      graceful-fs: 4.2.11
-      imurmurhash: 0.1.4
-      signal-exit: 3.0.7
-
   write-file-atomic@4.0.2:
     dependencies:
       imurmurhash: 0.1.4
       signal-exit: 3.0.7
 
-  ws@8.17.0(bufferutil@4.0.7)(utf-8-validate@6.0.3):
+  ws@8.18.0(bufferutil@4.0.7)(utf-8-validate@6.0.3):
     optionalDependencies:
       bufferutil: 4.0.7
       utf-8-validate: 6.0.3
 
+  ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4):
+    optionalDependencies:
+      bufferutil: 4.0.8
+      utf-8-validate: 6.0.4
+
   xev@3.0.2: {}
 
   xml-js@1.6.11:
@@ -24157,13 +24351,7 @@ snapshots:
 
   yocto-queue@1.0.0: {}
 
-  z-schema@5.0.5:
-    dependencies:
-      lodash.get: 4.4.2
-      lodash.isequal: 4.5.0
-      validator: 13.9.0
-    optionalDependencies:
-      commander: 9.5.0
+  yoctocolors@2.0.2: {}
 
   zip-stream@6.0.1:
     dependencies:
diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml
index 193669e7a4..d222614eda 100644
--- a/pnpm-workspace.yaml
+++ b/pnpm-workspace.yaml
@@ -1,6 +1,8 @@
 packages:
  - 'packages/backend'
+ - 'packages/frontend-shared'
  - 'packages/frontend'
+ - 'packages/frontend-embed'
  - 'packages/sw'
  - 'packages/misskey-js'
  - 'packages/misskey-js/generator'
diff --git a/scripts/build-assets.mjs b/scripts/build-assets.mjs
index b5aa5eb4ab..421d4a6d1b 100644
--- a/scripts/build-assets.mjs
+++ b/scripts/build-assets.mjs
@@ -13,7 +13,7 @@ import * as terser from 'terser';
 
 import { build as buildLocales } from '../locales/index.js';
 import generateDTS from '../locales/generateDTS.js';
-import meta from '../package.json' assert { type: "json" };
+import meta from '../package.json' with { type: "json" };
 import buildTarball from './tarball.mjs';
 
 const configDir = fileURLToPath(new URL('../.config', import.meta.url));
@@ -58,6 +58,7 @@ async function buildBackendScript() {
 
   for (const file of [
     './packages/backend/src/server/web/boot.js',
+    './packages/backend/src/server/web/boot.embed.js',
     './packages/backend/src/server/web/bios.js',
     './packages/backend/src/server/web/cli.js'
   ]) {
@@ -73,6 +74,7 @@ async function buildBackendStyle() {
 
   for (const file of [
     './packages/backend/src/server/web/style.css',
+    './packages/backend/src/server/web/style.embed.css',
     './packages/backend/src/server/web/bios.css',
     './packages/backend/src/server/web/cli.css',
     './packages/backend/src/server/web/error.css'
diff --git a/scripts/changelog-checker/.eslintrc.cjs b/scripts/changelog-checker/.eslintrc.cjs
deleted file mode 100644
index 6acf8b3e6e..0000000000
--- a/scripts/changelog-checker/.eslintrc.cjs
+++ /dev/null
@@ -1,9 +0,0 @@
-module.exports = {
-	parserOptions: {
-		tsconfigRootDir: __dirname,
-		project: ['./tsconfig.json'],
-	},
-	extends: [
-		'../../packages/shared/.eslintrc.js',
-	],
-};
diff --git a/scripts/changelog-checker/eslint.config.js b/scripts/changelog-checker/eslint.config.js
new file mode 100644
index 0000000000..813e96981a
--- /dev/null
+++ b/scripts/changelog-checker/eslint.config.js
@@ -0,0 +1,17 @@
+import tsParser from '@typescript-eslint/parser';
+import sharedConfig from '../../packages/shared/eslint.config.js';
+
+export default [
+	...sharedConfig,
+	{
+		files: ['src/**/*.ts', 'src/**/*.tsx'],
+		languageOptions: {
+			parserOptions: {
+				parser: tsParser,
+				project: ['./tsconfig.json'],
+				sourceType: 'module',
+				tsconfigRootDir: import.meta.dirname,
+			},
+		},
+	},
+];
diff --git a/scripts/changelog-checker/package-lock.json b/scripts/changelog-checker/package-lock.json
index 6ad3273e60..b7ec909abe 100644
--- a/scripts/changelog-checker/package-lock.json
+++ b/scripts/changelog-checker/package-lock.json
@@ -16,7 +16,7 @@
         "remark-parse": "11.0.0",
         "typescript": "5.3.3",
         "unified": "11.0.4",
-        "vite": "5.0.12",
+        "vite": "5.4.6",
         "vite-node": "1.1.3",
         "vitest": "1.1.3"
       }
@@ -85,9 +85,9 @@
       "dev": true
     },
     "node_modules/@esbuild/aix-ppc64": {
-      "version": "0.19.11",
-      "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.19.11.tgz",
-      "integrity": "sha512-FnzU0LyE3ySQk7UntJO4+qIiQgI7KoODnZg5xzXIrFJlKd2P2gwHsHY4927xj9y5PJmJSzULiUCWmv7iWnNa7g==",
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
+      "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==",
       "cpu": [
         "ppc64"
       ],
@@ -101,9 +101,9 @@
       }
     },
     "node_modules/@esbuild/android-arm": {
-      "version": "0.19.11",
-      "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.11.tgz",
-      "integrity": "sha512-5OVapq0ClabvKvQ58Bws8+wkLCV+Rxg7tUVbo9xu034Nm536QTII4YzhaFriQ7rMrorfnFKUsArD2lqKbFY4vw==",
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz",
+      "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==",
       "cpu": [
         "arm"
       ],
@@ -117,9 +117,9 @@
       }
     },
     "node_modules/@esbuild/android-arm64": {
-      "version": "0.19.11",
-      "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.11.tgz",
-      "integrity": "sha512-aiu7K/5JnLj//KOnOfEZ0D90obUkRzDMyqd/wNAUQ34m4YUPVhRZpnqKV9uqDGxT7cToSDnIHsGooyIczu9T+Q==",
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz",
+      "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==",
       "cpu": [
         "arm64"
       ],
@@ -133,9 +133,9 @@
       }
     },
     "node_modules/@esbuild/android-x64": {
-      "version": "0.19.11",
-      "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.11.tgz",
-      "integrity": "sha512-eccxjlfGw43WYoY9QgB82SgGgDbibcqyDTlk3l3C0jOVHKxrjdc9CTwDUQd0vkvYg5um0OH+GpxYvp39r+IPOg==",
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz",
+      "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==",
       "cpu": [
         "x64"
       ],
@@ -149,9 +149,9 @@
       }
     },
     "node_modules/@esbuild/darwin-arm64": {
-      "version": "0.19.11",
-      "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.11.tgz",
-      "integrity": "sha512-ETp87DRWuSt9KdDVkqSoKoLFHYTrkyz2+65fj9nfXsaV3bMhTCjtQfw3y+um88vGRKRiF7erPrh/ZuIdLUIVxQ==",
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz",
+      "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==",
       "cpu": [
         "arm64"
       ],
@@ -165,9 +165,9 @@
       }
     },
     "node_modules/@esbuild/darwin-x64": {
-      "version": "0.19.11",
-      "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.11.tgz",
-      "integrity": "sha512-fkFUiS6IUK9WYUO/+22omwetaSNl5/A8giXvQlcinLIjVkxwTLSktbF5f/kJMftM2MJp9+fXqZ5ezS7+SALp4g==",
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz",
+      "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==",
       "cpu": [
         "x64"
       ],
@@ -181,9 +181,9 @@
       }
     },
     "node_modules/@esbuild/freebsd-arm64": {
-      "version": "0.19.11",
-      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.11.tgz",
-      "integrity": "sha512-lhoSp5K6bxKRNdXUtHoNc5HhbXVCS8V0iZmDvyWvYq9S5WSfTIHU2UGjcGt7UeS6iEYp9eeymIl5mJBn0yiuxA==",
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz",
+      "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==",
       "cpu": [
         "arm64"
       ],
@@ -197,9 +197,9 @@
       }
     },
     "node_modules/@esbuild/freebsd-x64": {
-      "version": "0.19.11",
-      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.11.tgz",
-      "integrity": "sha512-JkUqn44AffGXitVI6/AbQdoYAq0TEullFdqcMY/PCUZ36xJ9ZJRtQabzMA+Vi7r78+25ZIBosLTOKnUXBSi1Kw==",
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz",
+      "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==",
       "cpu": [
         "x64"
       ],
@@ -213,9 +213,9 @@
       }
     },
     "node_modules/@esbuild/linux-arm": {
-      "version": "0.19.11",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.11.tgz",
-      "integrity": "sha512-3CRkr9+vCV2XJbjwgzjPtO8T0SZUmRZla+UL1jw+XqHZPkPgZiyWvbDvl9rqAN8Zl7qJF0O/9ycMtjU67HN9/Q==",
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz",
+      "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==",
       "cpu": [
         "arm"
       ],
@@ -229,9 +229,9 @@
       }
     },
     "node_modules/@esbuild/linux-arm64": {
-      "version": "0.19.11",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.11.tgz",
-      "integrity": "sha512-LneLg3ypEeveBSMuoa0kwMpCGmpu8XQUh+mL8XXwoYZ6Be2qBnVtcDI5azSvh7vioMDhoJFZzp9GWp9IWpYoUg==",
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz",
+      "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==",
       "cpu": [
         "arm64"
       ],
@@ -245,9 +245,9 @@
       }
     },
     "node_modules/@esbuild/linux-ia32": {
-      "version": "0.19.11",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.11.tgz",
-      "integrity": "sha512-caHy++CsD8Bgq2V5CodbJjFPEiDPq8JJmBdeyZ8GWVQMjRD0sU548nNdwPNvKjVpamYYVL40AORekgfIubwHoA==",
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz",
+      "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==",
       "cpu": [
         "ia32"
       ],
@@ -261,9 +261,9 @@
       }
     },
     "node_modules/@esbuild/linux-loong64": {
-      "version": "0.19.11",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.11.tgz",
-      "integrity": "sha512-ppZSSLVpPrwHccvC6nQVZaSHlFsvCQyjnvirnVjbKSHuE5N24Yl8F3UwYUUR1UEPaFObGD2tSvVKbvR+uT1Nrg==",
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz",
+      "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==",
       "cpu": [
         "loong64"
       ],
@@ -277,9 +277,9 @@
       }
     },
     "node_modules/@esbuild/linux-mips64el": {
-      "version": "0.19.11",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.11.tgz",
-      "integrity": "sha512-B5x9j0OgjG+v1dF2DkH34lr+7Gmv0kzX6/V0afF41FkPMMqaQ77pH7CrhWeR22aEeHKaeZVtZ6yFwlxOKPVFyg==",
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz",
+      "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==",
       "cpu": [
         "mips64el"
       ],
@@ -293,9 +293,9 @@
       }
     },
     "node_modules/@esbuild/linux-ppc64": {
-      "version": "0.19.11",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.11.tgz",
-      "integrity": "sha512-MHrZYLeCG8vXblMetWyttkdVRjQlQUb/oMgBNurVEnhj4YWOr4G5lmBfZjHYQHHN0g6yDmCAQRR8MUHldvvRDA==",
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz",
+      "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==",
       "cpu": [
         "ppc64"
       ],
@@ -309,9 +309,9 @@
       }
     },
     "node_modules/@esbuild/linux-riscv64": {
-      "version": "0.19.11",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.11.tgz",
-      "integrity": "sha512-f3DY++t94uVg141dozDu4CCUkYW+09rWtaWfnb3bqe4w5NqmZd6nPVBm+qbz7WaHZCoqXqHz5p6CM6qv3qnSSQ==",
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz",
+      "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==",
       "cpu": [
         "riscv64"
       ],
@@ -325,9 +325,9 @@
       }
     },
     "node_modules/@esbuild/linux-s390x": {
-      "version": "0.19.11",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.11.tgz",
-      "integrity": "sha512-A5xdUoyWJHMMlcSMcPGVLzYzpcY8QP1RtYzX5/bS4dvjBGVxdhuiYyFwp7z74ocV7WDc0n1harxmpq2ePOjI0Q==",
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz",
+      "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==",
       "cpu": [
         "s390x"
       ],
@@ -341,9 +341,9 @@
       }
     },
     "node_modules/@esbuild/linux-x64": {
-      "version": "0.19.11",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.11.tgz",
-      "integrity": "sha512-grbyMlVCvJSfxFQUndw5mCtWs5LO1gUlwP4CDi4iJBbVpZcqLVT29FxgGuBJGSzyOxotFG4LoO5X+M1350zmPA==",
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz",
+      "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==",
       "cpu": [
         "x64"
       ],
@@ -357,9 +357,9 @@
       }
     },
     "node_modules/@esbuild/netbsd-x64": {
-      "version": "0.19.11",
-      "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.11.tgz",
-      "integrity": "sha512-13jvrQZJc3P230OhU8xgwUnDeuC/9egsjTkXN49b3GcS5BKvJqZn86aGM8W9pd14Kd+u7HuFBMVtrNGhh6fHEQ==",
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz",
+      "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==",
       "cpu": [
         "x64"
       ],
@@ -373,9 +373,9 @@
       }
     },
     "node_modules/@esbuild/openbsd-x64": {
-      "version": "0.19.11",
-      "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.11.tgz",
-      "integrity": "sha512-ysyOGZuTp6SNKPE11INDUeFVVQFrhcNDVUgSQVDzqsqX38DjhPEPATpid04LCoUr2WXhQTEZ8ct/EgJCUDpyNw==",
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz",
+      "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==",
       "cpu": [
         "x64"
       ],
@@ -389,9 +389,9 @@
       }
     },
     "node_modules/@esbuild/sunos-x64": {
-      "version": "0.19.11",
-      "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.11.tgz",
-      "integrity": "sha512-Hf+Sad9nVwvtxy4DXCZQqLpgmRTQqyFyhT3bZ4F2XlJCjxGmRFF0Shwn9rzhOYRB61w9VMXUkxlBy56dk9JJiQ==",
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz",
+      "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==",
       "cpu": [
         "x64"
       ],
@@ -405,9 +405,9 @@
       }
     },
     "node_modules/@esbuild/win32-arm64": {
-      "version": "0.19.11",
-      "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.11.tgz",
-      "integrity": "sha512-0P58Sbi0LctOMOQbpEOvOL44Ne0sqbS0XWHMvvrg6NE5jQ1xguCSSw9jQeUk2lfrXYsKDdOe6K+oZiwKPilYPQ==",
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz",
+      "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==",
       "cpu": [
         "arm64"
       ],
@@ -421,9 +421,9 @@
       }
     },
     "node_modules/@esbuild/win32-ia32": {
-      "version": "0.19.11",
-      "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.11.tgz",
-      "integrity": "sha512-6YOrWS+sDJDmshdBIQU+Uoyh7pQKrdykdefC1avn76ss5c+RN6gut3LZA4E2cH5xUEp5/cA0+YxRaVtRAb0xBg==",
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz",
+      "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==",
       "cpu": [
         "ia32"
       ],
@@ -437,9 +437,9 @@
       }
     },
     "node_modules/@esbuild/win32-x64": {
-      "version": "0.19.11",
-      "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.11.tgz",
-      "integrity": "sha512-vfkhltrjCAb603XaFhqhAF4LGDi2M4OrCRrFusyQ+iTLQ/o60QQXxc9cZC/FFpihBI9N1Grn6SMKVJ4KP7Fuiw==",
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz",
+      "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==",
       "cpu": [
         "x64"
       ],
@@ -522,9 +522,9 @@
       }
     },
     "node_modules/@rollup/rollup-android-arm-eabi": {
-      "version": "4.9.4",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.9.4.tgz",
-      "integrity": "sha512-ub/SN3yWqIv5CWiAZPHVS1DloyZsJbtXmX4HxUTIpS0BHm9pW5iYBo2mIZi+hE3AeiTzHz33blwSnhdUo+9NpA==",
+      "version": "4.21.3",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.21.3.tgz",
+      "integrity": "sha512-MmKSfaB9GX+zXl6E8z4koOr/xU63AMVleLEa64v7R0QF/ZloMs5vcD1sHgM64GXXS1csaJutG+ddtzcueI/BLg==",
       "cpu": [
         "arm"
       ],
@@ -535,9 +535,9 @@
       ]
     },
     "node_modules/@rollup/rollup-android-arm64": {
-      "version": "4.9.4",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.9.4.tgz",
-      "integrity": "sha512-ehcBrOR5XTl0W0t2WxfTyHCR/3Cq2jfb+I4W+Ch8Y9b5G+vbAecVv0Fx/J1QKktOrgUYsIKxWAKgIpvw56IFNA==",
+      "version": "4.21.3",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.21.3.tgz",
+      "integrity": "sha512-zrt8ecH07PE3sB4jPOggweBjJMzI1JG5xI2DIsUbkA+7K+Gkjys6eV7i9pOenNSDJH3eOr/jLb/PzqtmdwDq5g==",
       "cpu": [
         "arm64"
       ],
@@ -548,9 +548,9 @@
       ]
     },
     "node_modules/@rollup/rollup-darwin-arm64": {
-      "version": "4.9.4",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.9.4.tgz",
-      "integrity": "sha512-1fzh1lWExwSTWy8vJPnNbNM02WZDS8AW3McEOb7wW+nPChLKf3WG2aG7fhaUmfX5FKw9zhsF5+MBwArGyNM7NA==",
+      "version": "4.21.3",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.21.3.tgz",
+      "integrity": "sha512-P0UxIOrKNBFTQaXTxOH4RxuEBVCgEA5UTNV6Yz7z9QHnUJ7eLX9reOd/NYMO3+XZO2cco19mXTxDMXxit4R/eQ==",
       "cpu": [
         "arm64"
       ],
@@ -561,9 +561,9 @@
       ]
     },
     "node_modules/@rollup/rollup-darwin-x64": {
-      "version": "4.9.4",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.9.4.tgz",
-      "integrity": "sha512-Gc6cukkF38RcYQ6uPdiXi70JB0f29CwcQ7+r4QpfNpQFVHXRd0DfWFidoGxjSx1DwOETM97JPz1RXL5ISSB0pA==",
+      "version": "4.21.3",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.21.3.tgz",
+      "integrity": "sha512-L1M0vKGO5ASKntqtsFEjTq/fD91vAqnzeaF6sfNAy55aD+Hi2pBI5DKwCO+UNDQHWsDViJLqshxOahXyLSh3EA==",
       "cpu": [
         "x64"
       ],
@@ -574,9 +574,22 @@
       ]
     },
     "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
-      "version": "4.9.4",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.9.4.tgz",
-      "integrity": "sha512-g21RTeFzoTl8GxosHbnQZ0/JkuFIB13C3T7Y0HtKzOXmoHhewLbVTFBQZu+z5m9STH6FZ7L/oPgU4Nm5ErN2fw==",
+      "version": "4.21.3",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.21.3.tgz",
+      "integrity": "sha512-btVgIsCjuYFKUjopPoWiDqmoUXQDiW2A4C3Mtmp5vACm7/GnyuprqIDPNczeyR5W8rTXEbkmrJux7cJmD99D2g==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-arm-musleabihf": {
+      "version": "4.21.3",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.21.3.tgz",
+      "integrity": "sha512-zmjbSphplZlau6ZTkxd3+NMtE4UKVy7U4aVFMmHcgO5CUbw17ZP6QCgyxhzGaU/wFFdTfiojjbLG3/0p9HhAqA==",
       "cpu": [
         "arm"
       ],
@@ -587,9 +600,9 @@
       ]
     },
     "node_modules/@rollup/rollup-linux-arm64-gnu": {
-      "version": "4.9.4",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.9.4.tgz",
-      "integrity": "sha512-TVYVWD/SYwWzGGnbfTkrNpdE4HON46orgMNHCivlXmlsSGQOx/OHHYiQcMIOx38/GWgwr/po2LBn7wypkWw/Mg==",
+      "version": "4.21.3",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.21.3.tgz",
+      "integrity": "sha512-nSZfcZtAnQPRZmUkUQwZq2OjQciR6tEoJaZVFvLHsj0MF6QhNMg0fQ6mUOsiCUpTqxTx0/O6gX0V/nYc7LrgPw==",
       "cpu": [
         "arm64"
       ],
@@ -600,9 +613,9 @@
       ]
     },
     "node_modules/@rollup/rollup-linux-arm64-musl": {
-      "version": "4.9.4",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.9.4.tgz",
-      "integrity": "sha512-XcKvuendwizYYhFxpvQ3xVpzje2HHImzg33wL9zvxtj77HvPStbSGI9czrdbfrf8DGMcNNReH9pVZv8qejAQ5A==",
+      "version": "4.21.3",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.21.3.tgz",
+      "integrity": "sha512-MnvSPGO8KJXIMGlQDYfvYS3IosFN2rKsvxRpPO2l2cum+Z3exiExLwVU+GExL96pn8IP+GdH8Tz70EpBhO0sIQ==",
       "cpu": [
         "arm64"
       ],
@@ -612,10 +625,23 @@
         "linux"
       ]
     },
+    "node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
+      "version": "4.21.3",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.21.3.tgz",
+      "integrity": "sha512-+W+p/9QNDr2vE2AXU0qIy0qQE75E8RTwTwgqS2G5CRQ11vzq0tbnfBd6brWhS9bCRjAjepJe2fvvkvS3dno+iw==",
+      "cpu": [
+        "ppc64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
     "node_modules/@rollup/rollup-linux-riscv64-gnu": {
-      "version": "4.9.4",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.9.4.tgz",
-      "integrity": "sha512-LFHS/8Q+I9YA0yVETyjonMJ3UA+DczeBd/MqNEzsGSTdNvSJa1OJZcSH8GiXLvcizgp9AlHs2walqRcqzjOi3A==",
+      "version": "4.21.3",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.21.3.tgz",
+      "integrity": "sha512-yXH6K6KfqGXaxHrtr+Uoy+JpNlUlI46BKVyonGiaD74ravdnF9BUNC+vV+SIuB96hUMGShhKV693rF9QDfO6nQ==",
       "cpu": [
         "riscv64"
       ],
@@ -625,10 +651,23 @@
         "linux"
       ]
     },
+    "node_modules/@rollup/rollup-linux-s390x-gnu": {
+      "version": "4.21.3",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.21.3.tgz",
+      "integrity": "sha512-R8cwY9wcnApN/KDYWTH4gV/ypvy9yZUHlbJvfaiXSB48JO3KpwSpjOGqO4jnGkLDSk1hgjYkTbTt6Q7uvPf8eg==",
+      "cpu": [
+        "s390x"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
     "node_modules/@rollup/rollup-linux-x64-gnu": {
-      "version": "4.9.4",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.9.4.tgz",
-      "integrity": "sha512-dIYgo+j1+yfy81i0YVU5KnQrIJZE8ERomx17ReU4GREjGtDW4X+nvkBak2xAUpyqLs4eleDSj3RrV72fQos7zw==",
+      "version": "4.21.3",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.21.3.tgz",
+      "integrity": "sha512-kZPbX/NOPh0vhS5sI+dR8L1bU2cSO9FgxwM8r7wHzGydzfSjLRCFAT87GR5U9scj2rhzN3JPYVC7NoBbl4FZ0g==",
       "cpu": [
         "x64"
       ],
@@ -639,9 +678,9 @@
       ]
     },
     "node_modules/@rollup/rollup-linux-x64-musl": {
-      "version": "4.9.4",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.9.4.tgz",
-      "integrity": "sha512-RoaYxjdHQ5TPjaPrLsfKqR3pakMr3JGqZ+jZM0zP2IkDtsGa4CqYaWSfQmZVgFUCgLrTnzX+cnHS3nfl+kB6ZQ==",
+      "version": "4.21.3",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.21.3.tgz",
+      "integrity": "sha512-S0Yq+xA1VEH66uiMNhijsWAafffydd2X5b77eLHfRmfLsRSpbiAWiRHV6DEpz6aOToPsgid7TI9rGd6zB1rhbg==",
       "cpu": [
         "x64"
       ],
@@ -652,9 +691,9 @@
       ]
     },
     "node_modules/@rollup/rollup-win32-arm64-msvc": {
-      "version": "4.9.4",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.9.4.tgz",
-      "integrity": "sha512-T8Q3XHV+Jjf5e49B4EAaLKV74BbX7/qYBRQ8Wop/+TyyU0k+vSjiLVSHNWdVd1goMjZcbhDmYZUYW5RFqkBNHQ==",
+      "version": "4.21.3",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.21.3.tgz",
+      "integrity": "sha512-9isNzeL34yquCPyerog+IMCNxKR8XYmGd0tHSV+OVx0TmE0aJOo9uw4fZfUuk2qxobP5sug6vNdZR6u7Mw7Q+Q==",
       "cpu": [
         "arm64"
       ],
@@ -665,9 +704,9 @@
       ]
     },
     "node_modules/@rollup/rollup-win32-ia32-msvc": {
-      "version": "4.9.4",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.9.4.tgz",
-      "integrity": "sha512-z+JQ7JirDUHAsMecVydnBPWLwJjbppU+7LZjffGf+Jvrxq+dVjIE7By163Sc9DKc3ADSU50qPVw0KonBS+a+HQ==",
+      "version": "4.21.3",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.21.3.tgz",
+      "integrity": "sha512-nMIdKnfZfzn1Vsk+RuOvl43ONTZXoAPUUxgcU0tXooqg4YrAqzfKzVenqqk2g5efWh46/D28cKFrOzDSW28gTA==",
       "cpu": [
         "ia32"
       ],
@@ -678,9 +717,9 @@
       ]
     },
     "node_modules/@rollup/rollup-win32-x64-msvc": {
-      "version": "4.9.4",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.9.4.tgz",
-      "integrity": "sha512-LfdGXCV9rdEify1oxlN9eamvDSjv9md9ZVMAbNHA87xqIfFCxImxan9qZ8+Un54iK2nnqPlbnSi4R54ONtbWBw==",
+      "version": "4.21.3",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.21.3.tgz",
+      "integrity": "sha512-fOvu7PCQjAj4eWDEuD8Xz5gpzFqXzGlxHZozHP4b9Jxv9APtdxL6STqztDzMLuRXEc4UpXGGhx029Xgm91QBeA==",
       "cpu": [
         "x64"
       ],
@@ -1060,9 +1099,9 @@
       }
     },
     "node_modules/esbuild": {
-      "version": "0.19.11",
-      "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.11.tgz",
-      "integrity": "sha512-HJ96Hev2hX/6i5cDVwcqiJBBtuo9+FeIJOtZ9W1kA5M6AMJRHUZlpYZ1/SbEwtO0ioNAW8rUooVpC/WehY2SfA==",
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
+      "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==",
       "dev": true,
       "hasInstallScript": true,
       "bin": {
@@ -1072,29 +1111,29 @@
         "node": ">=12"
       },
       "optionalDependencies": {
-        "@esbuild/aix-ppc64": "0.19.11",
-        "@esbuild/android-arm": "0.19.11",
-        "@esbuild/android-arm64": "0.19.11",
-        "@esbuild/android-x64": "0.19.11",
-        "@esbuild/darwin-arm64": "0.19.11",
-        "@esbuild/darwin-x64": "0.19.11",
-        "@esbuild/freebsd-arm64": "0.19.11",
-        "@esbuild/freebsd-x64": "0.19.11",
-        "@esbuild/linux-arm": "0.19.11",
-        "@esbuild/linux-arm64": "0.19.11",
-        "@esbuild/linux-ia32": "0.19.11",
-        "@esbuild/linux-loong64": "0.19.11",
-        "@esbuild/linux-mips64el": "0.19.11",
-        "@esbuild/linux-ppc64": "0.19.11",
-        "@esbuild/linux-riscv64": "0.19.11",
-        "@esbuild/linux-s390x": "0.19.11",
-        "@esbuild/linux-x64": "0.19.11",
-        "@esbuild/netbsd-x64": "0.19.11",
-        "@esbuild/openbsd-x64": "0.19.11",
-        "@esbuild/sunos-x64": "0.19.11",
-        "@esbuild/win32-arm64": "0.19.11",
-        "@esbuild/win32-ia32": "0.19.11",
-        "@esbuild/win32-x64": "0.19.11"
+        "@esbuild/aix-ppc64": "0.21.5",
+        "@esbuild/android-arm": "0.21.5",
+        "@esbuild/android-arm64": "0.21.5",
+        "@esbuild/android-x64": "0.21.5",
+        "@esbuild/darwin-arm64": "0.21.5",
+        "@esbuild/darwin-x64": "0.21.5",
+        "@esbuild/freebsd-arm64": "0.21.5",
+        "@esbuild/freebsd-x64": "0.21.5",
+        "@esbuild/linux-arm": "0.21.5",
+        "@esbuild/linux-arm64": "0.21.5",
+        "@esbuild/linux-ia32": "0.21.5",
+        "@esbuild/linux-loong64": "0.21.5",
+        "@esbuild/linux-mips64el": "0.21.5",
+        "@esbuild/linux-ppc64": "0.21.5",
+        "@esbuild/linux-riscv64": "0.21.5",
+        "@esbuild/linux-s390x": "0.21.5",
+        "@esbuild/linux-x64": "0.21.5",
+        "@esbuild/netbsd-x64": "0.21.5",
+        "@esbuild/openbsd-x64": "0.21.5",
+        "@esbuild/sunos-x64": "0.21.5",
+        "@esbuild/win32-arm64": "0.21.5",
+        "@esbuild/win32-ia32": "0.21.5",
+        "@esbuild/win32-x64": "0.21.5"
       }
     },
     "node_modules/estree-walker": {
@@ -2086,9 +2125,9 @@
       }
     },
     "node_modules/picocolors": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
-      "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==",
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz",
+      "integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==",
       "dev": true
     },
     "node_modules/pkg-types": {
@@ -2103,9 +2142,9 @@
       }
     },
     "node_modules/postcss": {
-      "version": "8.4.33",
-      "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.33.tgz",
-      "integrity": "sha512-Kkpbhhdjw2qQs2O2DGX+8m5OVqEcbB9HRBvuYM9pgrjEFUg30A9LmXNlTAUj4S9kgtGyrMbTzVjH7E+s5Re2yg==",
+      "version": "8.4.47",
+      "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz",
+      "integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==",
       "dev": true,
       "funding": [
         {
@@ -2123,8 +2162,8 @@
       ],
       "dependencies": {
         "nanoid": "^3.3.7",
-        "picocolors": "^1.0.0",
-        "source-map-js": "^1.0.2"
+        "picocolors": "^1.1.0",
+        "source-map-js": "^1.2.1"
       },
       "engines": {
         "node": "^10 || ^12 || >=14"
@@ -2198,9 +2237,9 @@
       }
     },
     "node_modules/rollup": {
-      "version": "4.9.4",
-      "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.9.4.tgz",
-      "integrity": "sha512-2ztU7pY/lrQyXSCnnoU4ICjT/tCG9cdH3/G25ERqE3Lst6vl2BCM5hL2Nw+sslAvAf+ccKsAq1SkKQALyqhR7g==",
+      "version": "4.21.3",
+      "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.21.3.tgz",
+      "integrity": "sha512-7sqRtBNnEbcBtMeRVc6VRsJMmpI+JU1z9VTvW8D4gXIYQFz0aLcsE6rRkyghZkLfEgUZgVvOG7A5CVz/VW5GIA==",
       "dev": true,
       "dependencies": {
         "@types/estree": "1.0.5"
@@ -2213,19 +2252,22 @@
         "npm": ">=8.0.0"
       },
       "optionalDependencies": {
-        "@rollup/rollup-android-arm-eabi": "4.9.4",
-        "@rollup/rollup-android-arm64": "4.9.4",
-        "@rollup/rollup-darwin-arm64": "4.9.4",
-        "@rollup/rollup-darwin-x64": "4.9.4",
-        "@rollup/rollup-linux-arm-gnueabihf": "4.9.4",
-        "@rollup/rollup-linux-arm64-gnu": "4.9.4",
-        "@rollup/rollup-linux-arm64-musl": "4.9.4",
-        "@rollup/rollup-linux-riscv64-gnu": "4.9.4",
-        "@rollup/rollup-linux-x64-gnu": "4.9.4",
-        "@rollup/rollup-linux-x64-musl": "4.9.4",
-        "@rollup/rollup-win32-arm64-msvc": "4.9.4",
-        "@rollup/rollup-win32-ia32-msvc": "4.9.4",
-        "@rollup/rollup-win32-x64-msvc": "4.9.4",
+        "@rollup/rollup-android-arm-eabi": "4.21.3",
+        "@rollup/rollup-android-arm64": "4.21.3",
+        "@rollup/rollup-darwin-arm64": "4.21.3",
+        "@rollup/rollup-darwin-x64": "4.21.3",
+        "@rollup/rollup-linux-arm-gnueabihf": "4.21.3",
+        "@rollup/rollup-linux-arm-musleabihf": "4.21.3",
+        "@rollup/rollup-linux-arm64-gnu": "4.21.3",
+        "@rollup/rollup-linux-arm64-musl": "4.21.3",
+        "@rollup/rollup-linux-powerpc64le-gnu": "4.21.3",
+        "@rollup/rollup-linux-riscv64-gnu": "4.21.3",
+        "@rollup/rollup-linux-s390x-gnu": "4.21.3",
+        "@rollup/rollup-linux-x64-gnu": "4.21.3",
+        "@rollup/rollup-linux-x64-musl": "4.21.3",
+        "@rollup/rollup-win32-arm64-msvc": "4.21.3",
+        "@rollup/rollup-win32-ia32-msvc": "4.21.3",
+        "@rollup/rollup-win32-x64-msvc": "4.21.3",
         "fsevents": "~2.3.2"
       }
     },
@@ -2293,9 +2335,9 @@
       }
     },
     "node_modules/source-map-js": {
-      "version": "1.0.2",
-      "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz",
-      "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==",
+      "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,
       "engines": {
         "node": ">=0.10.0"
@@ -2558,14 +2600,14 @@
       }
     },
     "node_modules/vite": {
-      "version": "5.0.12",
-      "resolved": "https://registry.npmjs.org/vite/-/vite-5.0.12.tgz",
-      "integrity": "sha512-4hsnEkG3q0N4Tzf1+t6NdN9dg/L3BM+q8SWgbSPnJvrgH2kgdyzfVJwbR1ic69/4uMJJ/3dqDZZE5/WwqW8U1w==",
+      "version": "5.4.6",
+      "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.6.tgz",
+      "integrity": "sha512-IeL5f8OO5nylsgzd9tq4qD2QqI0k2CQLGrWD0rCN0EQJZpBK5vJAx0I+GDkMOXxQX/OfFHMuLIx6ddAxGX/k+Q==",
       "dev": true,
       "dependencies": {
-        "esbuild": "^0.19.3",
-        "postcss": "^8.4.32",
-        "rollup": "^4.2.0"
+        "esbuild": "^0.21.3",
+        "postcss": "^8.4.43",
+        "rollup": "^4.20.0"
       },
       "bin": {
         "vite": "bin/vite.js"
@@ -2584,6 +2626,7 @@
         "less": "*",
         "lightningcss": "^1.21.0",
         "sass": "*",
+        "sass-embedded": "*",
         "stylus": "*",
         "sugarss": "*",
         "terser": "^5.4.0"
@@ -2601,6 +2644,9 @@
         "sass": {
           "optional": true
         },
+        "sass-embedded": {
+          "optional": true
+        },
         "stylus": {
           "optional": true
         },
diff --git a/scripts/changelog-checker/package.json b/scripts/changelog-checker/package.json
index 8b3c9843b7..dccb47d037 100644
--- a/scripts/changelog-checker/package.json
+++ b/scripts/changelog-checker/package.json
@@ -17,7 +17,7 @@
     "remark-parse": "11.0.0",
     "typescript": "5.3.3",
     "unified": "11.0.4",
-    "vite": "5.0.12",
+    "vite": "5.4.6",
     "vite-node": "1.1.3",
     "vitest": "1.1.3"
   }
diff --git a/scripts/clean-all.js b/scripts/clean-all.js
index e9512e2d5a..dc391ecfd8 100644
--- a/scripts/clean-all.js
+++ b/scripts/clean-all.js
@@ -10,9 +10,15 @@ const fs = require('fs');
 	fs.rmSync(__dirname + '/../packages/backend/built', { recursive: true, force: true });
 	fs.rmSync(__dirname + '/../packages/backend/node_modules', { recursive: true, force: true });
 
+	fs.rmSync(__dirname + '/../packages/frontend-shared/built', { recursive: true, force: true });
+	fs.rmSync(__dirname + '/../packages/frontend-shared/node_modules', { recursive: true, force: true });
+
 	fs.rmSync(__dirname + '/../packages/frontend/built', { recursive: true, force: true });
 	fs.rmSync(__dirname + '/../packages/frontend/node_modules', { recursive: true, force: true });
 
+	fs.rmSync(__dirname + '/../packages/frontend-embed/built', { recursive: true, force: true });
+	fs.rmSync(__dirname + '/../packages/frontend-embed/node_modules', { recursive: true, force: true });
+
 	fs.rmSync(__dirname + '/../packages/sw/built', { recursive: true, force: true });
 	fs.rmSync(__dirname + '/../packages/sw/node_modules', { recursive: true, force: true });
 
diff --git a/scripts/clean.js b/scripts/clean.js
index af66c24a8f..86c19281ea 100644
--- a/scripts/clean.js
+++ b/scripts/clean.js
@@ -7,7 +7,9 @@ const fs = require('fs');
 
 (async () => {
 	fs.rmSync(__dirname + '/../packages/backend/built', { recursive: true, force: true });
+	fs.rmSync(__dirname + '/../packages/frontend-shared/built', { recursive: true, force: true });
 	fs.rmSync(__dirname + '/../packages/frontend/built', { recursive: true, force: true });
+	fs.rmSync(__dirname + '/../packages/frontend-embed/built', { recursive: true, force: true });
 	fs.rmSync(__dirname + '/../packages/sw/built', { recursive: true, force: true });
 	fs.rmSync(__dirname + '/../packages/misskey-js/built', { recursive: true, force: true });
 	fs.rmSync(__dirname + '/../packages/misskey-reversi/built', { recursive: true, force: true });
diff --git a/scripts/dev.mjs b/scripts/dev.mjs
index bbb2547758..a4c82d46e1 100644
--- a/scripts/dev.mjs
+++ b/scripts/dev.mjs
@@ -65,12 +65,24 @@ execa('pnpm', ['--filter', 'backend', 'dev'], {
 	stderr: process.stderr,
 });
 
+execa('pnpm', ['--filter', 'frontend-shared', 'watch'], {
+	cwd: _dirname + '/../',
+	stdout: process.stdout,
+	stderr: process.stderr,
+});
+
 execa('pnpm', ['--filter', 'frontend', process.env.MK_DEV_PREFER === 'backend' ? 'watch' : 'dev'], {
 	cwd: _dirname + '/../',
 	stdout: process.stdout,
 	stderr: process.stderr,
 });
 
+execa('pnpm', ['--filter', 'frontend-embed', process.env.MK_DEV_PREFER === 'backend' ? 'watch' : 'dev'], {
+	cwd: _dirname + '/../',
+	stdout: process.stdout,
+	stderr: process.stderr,
+});
+
 execa('pnpm', ['--filter', 'sw', 'watch'], {
 	cwd: _dirname + '/../',
 	stdout: process.stdout,
diff --git a/scripts/tarball.mjs b/scripts/tarball.mjs
index b1862ad289..e9d8900aca 100644
--- a/scripts/tarball.mjs
+++ b/scripts/tarball.mjs
@@ -10,7 +10,7 @@ import { fileURLToPath } from 'node:url';
 import glob from 'fast-glob';
 import walk from 'ignore-walk';
 import Pack from 'tar/lib/pack.js';
-import meta from '../package.json' assert { type: "json" };
+import meta from '../package.json' with { type: "json" };
 
 const cwd = fileURLToPath(new URL('..', import.meta.url));
 const ignore = [