diff --git a/.config/ci.yml b/.config/ci.yml
index c381d21d92..44092d3662 100644
--- a/.config/ci.yml
+++ b/.config/ci.yml
@@ -106,7 +106,7 @@ redis:
 #   ┌───────────────────────────┐
 #───┘ MeiliSearch configuration └─────────────────────────────
 
-# You can set scope to local (default value) or global 
+# You can set scope to local (default value) or global
 # (include notes from remote).
 
 #meilisearch:
@@ -198,13 +198,18 @@ proxyRemoteFiles: true
 #   https://example.com/thumbnail.webp?thumbnail=1&url=https%3A%2F%2Fstorage.example.com%2Fpath%2Fto%2Fvideo.mp4
 #videoThumbnailGenerator: https://example.com
 
-# Sign to ActivityPub GET request (default: true)
+# Sign outgoing ActivityPub GET request (default: true)
 signToActivityPubGet: true
+# Sign outgoing ActivityPub Activities (default: true)
+# Linked Data signatures are cryptographic signatures attached to each activity to provide proof of authenticity.
+# When using authorized fetch, this is often undesired as any signed activity can be forwarded to a blocked instance by relays and other instances.
+# This setting allows admins to disable LD signatures for increased privacy, at the expense of fewer relayed activities and additional inbound fetch (GET) requests.
+attachLdSignatureForRelays: true
 # check that inbound ActivityPub GET requests are signed ("authorized fetch")
 checkActivityPubGetSignature: false
 
 # For security reasons, uploading attachments from the intranet is prohibited,
-# but exceptions can be made from the following settings. Default value is "undefined". 
+# but exceptions can be made from the following settings. Default value is "undefined".
 # Read changelog to learn more (Improvements of 12.90.0 (2021/09/04)).
 #allowedPrivateNetworks: [
 #  '127.0.0.1/32'
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 c22bd83c2e..de95f1b21a 100644
--- a/.config/docker_example.yml
+++ b/.config/docker_example.yml
@@ -63,6 +63,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
@@ -95,9 +96,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
 
@@ -270,8 +273,13 @@ proxyRemoteFiles: true
 #   https://example.com/thumbnail.webp?thumbnail=1&url=https%3A%2F%2Fstorage.example.com%2Fpath%2Fto%2Fvideo.mp4
 #videoThumbnailGenerator: https://example.com
 
-# Sign to ActivityPub GET request (default: true)
+# Sign outgoing ActivityPub GET request (default: true)
 signToActivityPubGet: true
+# Sign outgoing ActivityPub Activities (default: true)
+# Linked Data signatures are cryptographic signatures attached to each activity to provide proof of authenticity.
+# When using authorized fetch, this is often undesired as any signed activity can be forwarded to a blocked instance by relays and other instances.
+# This setting allows admins to disable LD signatures for increased privacy, at the expense of fewer relayed activities and additional inbound fetch (GET) requests.
+attachLdSignatureForRelays: true
 # check that inbound ActivityPub GET requests are signed ("authorized fetch")
 checkActivityPubGetSignature: false
 
diff --git a/.config/example.yml b/.config/example.yml
index ae55b983bb..21e85b7b89 100644
--- a/.config/example.yml
+++ b/.config/example.yml
@@ -285,8 +285,13 @@ proxyRemoteFiles: true
 #   https://example.com/thumbnail.webp?thumbnail=1&url=https%3A%2F%2Fstorage.example.com%2Fpath%2Fto%2Fvideo.mp4
 #videoThumbnailGenerator: https://example.com
 
-# Sign to ActivityPub GET request (default: true)
+# Sign outgoing ActivityPub GET request (default: true)
 signToActivityPubGet: true
+# Sign outgoing ActivityPub Activities (default: true)
+# Linked Data signatures are cryptographic signatures attached to each activity to provide proof of authenticity.
+# When using authorized fetch, this is often undesired as any signed activity can be forwarded to a blocked instance by relays and other instances.
+# This setting allows admins to disable LD signatures for increased privacy, at the expense of fewer relayed activities and additional inbound fetch (GET) requests.
+attachLdSignatureForRelays: true
 # check that inbound ActivityPub GET requests are signed ("authorized fetch")
 checkActivityPubGetSignature: false
 
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/init.sh b/.devcontainer/init.sh
index 729e1a9d2d..55fb1e6fa6 100755
--- a/.devcontainer/init.sh
+++ b/.devcontainer/init.sh
@@ -2,7 +2,8 @@
 
 set -xe
 
-sudo chown -R node /workspace
+sudo chown node node_modules
+git config --global --add safe.directory /workspace
 git submodule update --init
 corepack install
 corepack enable
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/.gitignore b/.gitignore
index 2b6a5c1ebf..a8887eab92 100644
--- a/.gitignore
+++ b/.gitignore
@@ -36,7 +36,9 @@ coverage
 !/.config/docker_example.yml
 !/.config/docker_example.env
 docker-compose.yml
-!/.devcontainer/docker-compose.yml
+compose.yml
+.devcontainer/compose.yml
+!/.devcontainer/compose.yml
 
 # misskey
 /build
@@ -59,6 +61,7 @@ ormconfig.json
 temp
 /packages/frontend/src/**/*.stories.ts
 tsdoc-metadata.json
+misskey-assets
 
 # Sharkey
 /packages/megalodon/lib
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 84d29851b2..2e773eddf9 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -20,7 +20,7 @@ testCommit:
     - pnpm install --frozen-lockfile
     - pnpm run build
     - pnpm run migrate
-    - pnpm run --filter='!megalodon' test
+    - pnpm run --filter='!megalodon' --workspace-concurrency=1 test
     - pnpm run --filter=backend lint
     - pnpm run --filter=frontend eslint
   cache:
diff --git a/.gitmodules b/.gitmodules
index a3ca76cc96..1a68b48180 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 e2528594c3..0986b6eae3 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,14 +1,167 @@
-## Unreleased
+## 2024.8.0
 
 ### General
-- Fix: 配信停止したインスタンス一覧が見れなくなる問題を修正
+- Enhance: モデレーターはすべてのユーザーのフォロー・フォロワーの一覧を見られるように
+- Enhance: アカウントの削除のモデレーションログを残すように
+- Enhance: 不適切なページ、ギャラリー、Playを管理者権限で削除できるように
+- Fix: リモートユーザのフォロー・フォロワーの一覧が非公開設定の場合も表示できてしまう問題を修正
 
 ### Client
--
+- Enhance: 「自分のPlay」ページにおいてPlayが非公開かどうかが一目でわかるように
+- Enhance: 不適切なページ、ギャラリー、Playを通報できるように
+- Fix: Play編集時に公開範囲が「パブリック」にリセットされる問題を修正
+- Fix: ページ遷移に失敗することがある問題を修正
+- Fix: iOSでユーザー名などがリンクとして誤検知される現象を抑制
+- Fix: mCaptchaを使用していてもbotプロテクションに関する警告が消えないのを修正
+- Fix: ユーザーのモデレーションページにおいてユーザー名にドットが入っているとシステムアカウントとして表示されてしまう問題を修正
+- Fix: 特定の条件下でノートの削除ボタンが出ないのを修正
 
 ### Server
-- チャート生成時にinstance.suspentionStateに置き換えられたinstance.isSuspendedが参照されてしまう問題を修正
+- 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: フォローしていないユーザによるフォロワー限定投稿に対するリプライがソーシャルタイムラインで表示されることがある問題を修正
+- Fix: ActivityPubのエンティティタイプ判定で不明なタイプを受け取った場合でも処理を継続するように
+  - キュー処理のつまりが改善される可能性があります
+
+### 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 2acacd6dfa..a50b550b3a 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -11,10 +11,12 @@ 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 [Discord](https://discord.gg/6VgKmEqHNk).
 
-> **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, *please open an issue*,
 don't just start writing code. We may suggest different approaches, or
 show that the "bug" is actually intended behaviour (and offer
@@ -25,7 +27,20 @@ Misskey. Each of these examples have actually happened!
 On the other hand, it's very likely that we'll tell you "go
 ahead!". We try our best to incorporate improvements from our users!
 
-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
 - **`stable`** branch is tracking the latest release and used for production purposes.
@@ -35,14 +50,14 @@ 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 🤗
@@ -52,8 +67,8 @@ 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
@@ -68,7 +83,7 @@ Be willing to comment on the good points and not just the things you want fixed
 
 ## Release
 ### Release Instructions
-1. Commit version changes in the `develop` branch ([package.json](https://activitypub.software/TransFem-org/Sharkey/-/blob/develop/package.json))
+1. Commit version changes in the `develop` branch ([package.json](package.json))
 2. Create a release PR.
 	- Into `stable` from `develop` branch.
 	- The title must be in the format `Release: x.y.z`.
@@ -79,7 +94,7 @@ Be willing to comment on the good points and not just the things you want fixed
 	- The target branch must be `stable`
 	- The tag name must be the version
 
-> **Note**
+> [!NOTE]
 > Why this instruction is necessary:
 > - To perform final QA checks
 > - To distribute responsibility
@@ -96,13 +111,52 @@ If your language is not listed in Crowdin, please open an issue.
 
 ![Crowdin](https://d322cqt584bo4o.cloudfront.net/misskey/localized.svg)
 
-## Development
-During development, it is useful to use the
+## Icon Font (Shark Font)
+Sharkey has its own Icon Font called Shark Font which can be found at https://activitypub.software/TransFem-org/shark-font
+Build Instructions can all be found over there in the `README`.
 
+If you have an Icon Suggestion or want to add an Icon please open an issue/merge request over at that repo.
+
+When Updating the Font make sure to copy **all generated files** from the `dest` folder into `packages/backend/assets/fonts/sharkey-icons`
+For the CSS simply copy the file content and replace the old content in `style.css` and for the WOFF, TTF and SVG simply replace them.
+
+## Development
+### 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).
@@ -126,26 +180,6 @@ 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).
 
@@ -156,7 +190,7 @@ cp .github/misskey/test.yml .config/
 ```
 Prepare DB/Redis for testing.
 ```
-docker compose -f packages/backend/test/docker-compose.yml up
+docker compose -f packages/backend/test/compose.yml up
 ```
 Alternatively, prepare an empty (data can be erased) DB and edit `.config/test.yml`.
 
@@ -195,7 +229,7 @@ niraxは、Misskeyで使用しているオリジナルのフロントエンド
 ### ルート定義
 ルート定義は、以下の形式のオブジェクトの配列です。
 
-``` ts
+```ts
 {
 	name?: string;
 	path: string;
@@ -208,7 +242,7 @@ niraxは、Misskeyで使用しているオリジナルのフロントエンド
 }
 ```
 
-> **Warning**
+> [!WARNING]
 > 現状、ルートは定義された順に評価されます。
 > たとえば、`/foo/:id`ルート定義の次に`/foo/bar`ルート定義がされていた場合、後者がマッチすることはありません。
 
@@ -270,7 +304,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.
@@ -381,7 +415,7 @@ describe('test', () => {
 		})
 			.useMocker(...
 			.compile();
-	
+
 		fooService = app.get<FooService>(FooService);
 		barService = app.get<BarService>(BarService) as jest.Mocked<BarService>;
 
@@ -502,13 +536,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: {
@@ -555,30 +589,50 @@ seems to do a decent job)
 
 *after that commit*, do all the extra work, on the same branch:
 
-* copy all changes:
-  * from `NoteCreateService.create` to `NoteCreateService.import` (and
-    vice versa if `git` got confused!)
-  * from `NoteCreateService` to `NoteEditService`
-  * from `ApNoteService.createNote` to `ApNoteService.updateNote`
-  * from `endoints/notes/create.ts` to `endoints/notes/edit.ts`
-  * from `MkNote*` to `SkNote*` (if sensible)
+* copy all changes (commit after each step):
+  * in `packages/backend/src/core/NoteCreateService.ts`, from `create` to
+    `import` (and vice versa if `git` got confused!)
+  * in
+    `packages/backend/src/core/activitypub/models/ApNoteService.ts`,
+    from `createNote` to `updateNote`
+  * from `packages/backend/src/core/NoteCreateService.ts` to
+    `packages/backend/src/core/NoteEditService.vue`
+  * in `packages/backend/src/core/activitypub/models/ApNoteService.ts`,
+    from `createNote` to `updateNote`
+  * from `packages/backend/src/server/api/endpoints/notes/create.ts`
+    to `packages/backend/src/server/api/endpoints/notes/edit.ts`
+  * from `packages/frontend/src/components/MkNote*.vue` to
+    `packages/frontend/src/components/SkNote*.vue` (if sensible)
   * from the global timeline to the bubble timeline
     (`packages/backend/src/server/api/stream/channels/global-timeline.ts`,
     `packages/backend/src/server/api/stream/channels/bubble-timeline.ts`,
+    `packages/frontend/src/timelines.ts`,
     `packages/frontend/src/components/MkTimeline.vue`,
     `packages/frontend/src/pages/timeline.vue`,
     `packages/frontend/src/ui/deck/tl-column.vue`,
     `packages/frontend/src/widgets/WidgetTimeline.vue`)
+* check the changes against our `develop` (`git diff develop`) and
+  against Misskey (`git diff misskey/develop`)
+* re-generate `misskey-js` (`pnpm build-misskey-js-with-types`) and commit
+* build the frontend: `rm -rf built/; NODE_ENV=development pnpm --filter=frontend
+  build` (the `development` tells it to keep some of the original
+  filenames in the built files)
 * make sure there aren't any new `ti-*` classes (Tabler Icons), and
-  replace them with appropriate `ph-*` ones (Phosphor Icons).
-  `git grep '["'\'']ti[ -](?!fw)'` should show you what to change.
+  replace them with appropriate `ph-*` ones (Phosphor Icons):
+  `grep -rP '["'\'']ti[ -](?!fw)' -- built/` should show you what to change.
   NOTE: `ti-fw` is a special class that's defined by Misskey, leave it
   alone
-* re-generate `misskey-js`: `pnpm build-misskey-js-with-types`
-* run tests `pnpm test` and fix as much as you can
-  * right now `megalodon` doesn't pass its tests, you probably need to
-    run `pnpm --filter=backend test` (requires a test database, [see
-    above](#testing)) and `pnpm --filter=frontend test`
+
+  after every change, re-build the frontend and check again, until
+  there are no more `ti-*` classes in the built files
+
+  commit!
+* double-check the new migration, that they won't conflict with our db
+  changes: `git diff develop -- packages/backend/migration/`
+* `pnpm clean; pnpm build`
+* run tests `pnpm --filter='!megalodon' test` (requires a test
+  database, [see above](#testing)) and fix as much as you can
+  * right now `megalodon` doesn't pass its tests, so we skip them
 * run lint `pnpm --filter=backend lint` + `pnpm --filter=frontend
   eslint` and fix as much as you can
 
diff --git a/Dockerfile b/Dockerfile
index b937c69cdb..288e97481c 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,6 +1,6 @@
 # syntax = docker/dockerfile:1.4
 
-ARG NODE_VERSION=20.12.2-alpine3.19
+ARG NODE_VERSION=20.16.0-alpine3.20
 
 FROM node:${NODE_VERSION} as build
 
@@ -45,6 +45,10 @@ RUN apk add ffmpeg tini jemalloc \
 USER sharkey
 WORKDIR /sharkey
 
+# add package.json to add pnpm
+COPY --chown=sharkey:sharkey ./package.json ./package.json
+RUN corepack install
+
 COPY --chown=sharkey:sharkey --from=build /sharkey/node_modules ./node_modules
 COPY --chown=sharkey:sharkey --from=build /sharkey/packages/backend/node_modules ./packages/backend/node_modules
 COPY --chown=sharkey:sharkey --from=build /sharkey/packages/misskey-js/node_modules ./packages/misskey-js/node_modules
@@ -61,7 +65,6 @@ COPY --chown=sharkey:sharkey --from=build /sharkey/fluent-emojis ./fluent-emojis
 COPY --chown=sharkey:sharkey --from=build /sharkey/tossface-emojis/dist ./tossface-emojis/dist
 COPY --chown=sharkey:sharkey --from=build /sharkey/sharkey-assets ./packages/frontend/assets
 
-COPY --chown=sharkey:sharkey package.json ./package.json
 COPY --chown=sharkey:sharkey pnpm-workspace.yaml ./pnpm-workspace.yaml
 COPY --chown=sharkey:sharkey packages/backend/package.json ./packages/backend/package.json
 COPY --chown=sharkey:sharkey packages/backend/scripts/check_connect.js ./packages/backend/scripts/check_connect.js
diff --git a/README.md b/README.md
index 6840503243..f9198c06c0 100644
--- a/README.md
+++ b/README.md
@@ -7,9 +7,6 @@
 
 ---
 
-<a href="https://fedidb.org/software/sharkey">
-		<img src="https://custom-icon-badges.herokuapp.com/badge/find_an-instance-acea31?logoColor=acea31&style=for-the-badge&logo=sharkey&labelColor=363B40" alt="find an instance"/></a>
-
 <a href="https://docs.joinsharkey.org/docs/install/fresh/">
 		<img src="https://custom-icon-badges.herokuapp.com/badge/create_an-instance-FBD53C?logoColor=FBD53C&style=for-the-badge&logo=server&labelColor=363B40" alt="create an instance"/></a>
 
@@ -19,8 +16,8 @@
 <a href="https://discord.gg/6VgKmEqHNk">
 		<img src="https://custom-icon-badges.herokuapp.com/badge/join_the-community-5865F2?logoColor=5865F2&style=for-the-badge&logo=discord&labelColor=363B40" alt="join the community"/></a>
 
-<a href="https://ko-fi.com/transfem">
-		<img src="https://custom-icon-badges.herokuapp.com/badge/donate-F96854?logoColor=F96854&style=for-the-badge&logo=kofi&labelColor=363B40" alt="donate"/></a>
+<a href="https://opencollective.com/sharkey">
+		<img src="https://custom-icon-badges.herokuapp.com/badge/donate-81ACF4?logoColor=81ACF4&style=for-the-badge&logo=opencollective&labelColor=363B40" alt="donate"/></a>
 
 ---
 
diff --git a/chart/files/default.yml b/chart/files/default.yml
index 2e1381ec57..aab7ed6ce1 100644
--- a/chart/files/default.yml
+++ b/chart/files/default.yml
@@ -208,8 +208,13 @@ id: "aidx"
 # Media Proxy
 #mediaProxy: https://example.com/proxy
 
-# Sign to ActivityPub GET request (default: true)
+# Sign outgoing ActivityPub GET request (default: true)
 signToActivityPubGet: true
+# Sign outgoing ActivityPub Activities (default: true)
+# Linked Data signatures are cryptographic signatures attached to each activity to provide proof of authenticity.
+# When using authorized fetch, this is often undesired as any signed activity can be forwarded to a blocked instance by relays and other instances.
+# This setting allows admins to disable LD signatures for increased privacy, at the expense of fewer relayed activities and additional inbound fetch (GET) requests.
+attachLdSignatureForRelays: true
 # check that inbound ActivityPub GET requests are signed ("authorized fetch")
 checkActivityPubGetSignature: false
 
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 95%
rename from docker-compose_example.yml
rename to compose_example.yml
index 647f6f0c77..15df128eff 100644
--- a/docker-compose_example.yml
+++ b/compose_example.yml
@@ -1,5 +1,3 @@
-version: "3"
-
 services:
   web:
 #   image: registry.activitypub.software/transfem-org/sharkey:latest
@@ -19,6 +17,8 @@ services:
       - "3000:3000"
     networks:
       - shonk
+    # env_file:
+    #   - .config/docker.env
     volumes:
       - ./files:/sharkey/files
       - ./.config:/sharkey/.config:ro
@@ -53,8 +53,7 @@ services:
 #    restart: always
 #    image: mcaptcha/mcaptcha:latest
 #    networks:
-#      internal_network:
-#      external_network:
+#      shonks:
 #        aliases:
 #          - localhost
 #    ports:
@@ -73,7 +72,7 @@ services:
 #  mcaptcha_redis:
 #    image: mcaptcha/cache:latest
 #    networks:
-#      - internal_network
+#      - shonks
 #    healthcheck:
 #      test: "redis-cli ping"
 #      interval: 5s
diff --git a/locales/ar-SA.yml b/locales/ar-SA.yml
index 955d672c1d..b6bfbfa682 100644
--- a/locales/ar-SA.yml
+++ b/locales/ar-SA.yml
@@ -1262,8 +1262,6 @@ _sfx:
   note: "الملاحظات"
   noteMy: "ملاحظتي"
   notification: "الإشعارات"
-  antenna: "الهوائيات"
-  channel: "إشعارات القنات"
 _ago:
   future: "المستقبَل"
   justNow: "اللحظة"
@@ -1566,6 +1564,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..6fb51ea5d8 100644
--- a/locales/bn-BD.yml
+++ b/locales/bn-BD.yml
@@ -1033,8 +1033,6 @@ _sfx:
   note: "নোটগুলি"
   noteMy: "নোট (আপনার)"
   notification: "বিজ্ঞপ্তি"
-  antenna: "অ্যান্টেনাগুলি"
-  channel: "চ্যানেলের বিজ্ঞপ্তি"
 _ago:
   future: "ভবিষ্যৎ"
   justNow: "এইমাত্র"
@@ -1346,6 +1344,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 bda8579e27..0b2acf8f05 100644
--- a/locales/ca-ES.yml
+++ b/locales/ca-ES.yml
@@ -540,7 +540,7 @@ objectStorageRegion: "Regió "
 objectStorageRegionDesc: "Especifica una regió com 'xx-east-1'. Si el teu servei no diferència regions has de posar 'us-east-1'. Deixa'l buit si fas servir variables d'entorn o un arxiu de configuració d'AWS."
 objectStorageUseSSL: "Fes servir SSL"
 objectStorageUseSSLDesc: "Desactiva'l si no tens pensat fer servir HTTPS per les connexions de l'API"
-objectStorageUseProxy: "Connectar-se  mitjançant un Proxy"
+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."
@@ -1739,7 +1739,7 @@ _email:
   _follow:
     title: "t'ha seguit"
   _receiveFollowRequest:
-    title: "Has rebut una sol·licitud  de seguiment"
+    title: "Has rebut una sol·licitud de seguiment"
 _plugin:
   install: "Instal·lar un afegit "
   installWarn: "Si us plau, no instal·lis afegits que no siguin de confiança."
@@ -1893,8 +1893,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"
@@ -2225,6 +2223,10 @@ _deck:
 _webhookSettings:
   name: "Nom"
   active: "Activat"
+_abuseReport:
+  _notificationRecipient:
+    _recipientType:
+      mail: "Correu electrònic"
 _moderationLogTypes:
   suspend: "Suspèn"
   resetPassword: "Restableix la contrasenya"
diff --git a/locales/cs-CZ.yml b/locales/cs-CZ.yml
index bcab28db2c..0983e05ad9 100644
--- a/locales/cs-CZ.yml
+++ b/locales/cs-CZ.yml
@@ -1645,8 +1645,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ď"
@@ -2012,7 +2010,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 +2019,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 47b97a8e82..8c7d372c3f 100644
--- a/locales/de-DE.yml
+++ b/locales/de-DE.yml
@@ -1803,8 +1803,6 @@ _sfx:
   note: "Notizen"
   noteMy: "Meine Notizen"
   notification: "Benachrichtigungen"
-  antenna: "Antennen"
-  channel: "Kanalbenachrichtigung"
 _ago:
   future: "Zukunft"
   justNow: "Gerade eben"
@@ -2196,7 +2194,6 @@ _webhookSettings:
   createWebhook: "Webhook erstellen"
   name: "Name"
   secret: "Secret"
-  events: "Webhook-Ereignisse"
   active: "Aktiviert"
   _events:
     follow: "Wenn du jemandem folgst"
@@ -2206,6 +2203,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..5eca348e18 100644
--- a/locales/el-GR.yml
+++ b/locales/el-GR.yml
@@ -301,8 +301,6 @@ _theme:
 _sfx:
   note: "Σημειώματα"
   notification: "Ειδοποιήσεις"
-  antenna: "Αντένες"
-  channel: "Ειδοποιήσεις καναλιών"
 _ago:
   future: "Μελλοντικό"
   justNow: "Μόλις τώρα"
diff --git a/locales/en-US.yml b/locales/en-US.yml
index fe7bc3c451..58712657e8 100644
--- a/locales/en-US.yml
+++ b/locales/en-US.yml
@@ -62,6 +62,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"
@@ -129,8 +130,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"
@@ -162,6 +163,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"
@@ -173,7 +175,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."
@@ -190,6 +192,10 @@ addAccount: "Add account"
 reloadAccountsList: "Reload account list"
 loginFailed: "Failed to sign in"
 showOnRemote: "View on remote instance"
+continueOnRemote: "Continue on remote instance"
+chooseServerOnMisskeyHub: "Choose a instance from Misskey Hub"
+specifyServerHost: "Specify a server host directly"
+inputHostName: "Enter the domain"
 general: "General"
 wallpaper: "Wallpaper"
 setWallpaper: "Set wallpaper"
@@ -200,6 +206,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"
@@ -215,6 +222,7 @@ perDay: "Per Day"
 stopActivityDelivery: "Stop sending activities"
 blockThisInstance: "Block this instance"
 silenceThisInstance: "Silence this instance"
+mediaSilenceThisInstance: "Silence media from this instance"
 operations: "Operations"
 software: "Software"
 version: "Version"
@@ -235,7 +243,9 @@ 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 instances that you want to silence, separated by a new line. All accounts belonging to the listed instances 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 instances."
+mediaSilencedInstances: "Media-silenced instances"
+mediaSilencedInstancesDescription: "List the host names of the instances that you want to media-silence, separated by a new line. All accounts belonging to the listed instances will be treated as sensitive, and can't use custom emojis. This will not affect the blocked instances."
 muteAndBlock: "Mutes and Blocks"
 mutedUsers: "Muted users"
 blockedUsers: "Blocked users"
@@ -321,6 +331,7 @@ lightThemes: "Light themes"
 darkThemes: "Dark themes"
 syncDeviceDarkMode: "Sync Dark Mode with your device settings"
 drive: "Drive"
+driveSearchbarPlaceholder: "Search drive"
 fileName: "Filename"
 selectFile: "Select a file"
 selectFiles: "Select files"
@@ -399,7 +410,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"
@@ -490,6 +501,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 go to your instance to perform this action or sign up / log in to the instance you are trying to interact with."
 invitations: "Invites"
 invitationCode: "Invitation code"
 checking: "Checking..."
@@ -901,7 +913,7 @@ whatIsNew: "Show changes"
 translate: "Translate"
 translatedFrom: "Translated from {x}"
 accountDeletionInProgress: "Account deletion is currently in progress"
-usernameInfo: "A name that identifies your account from others on this server.  You can use the alphabet (a~z, A~Z), digits (0~9) or underscores (_). Usernames cannot be changed later."
+usernameInfo: "A name that identifies your account from others on this server. You can use the alphabet (a~z, A~Z), digits (0~9) or underscores (_). Usernames cannot be changed later."
 aiChanMode: "Ai Mode"
 devMode: "Developer mode"
 keepCw: "Keep content warnings"
@@ -1153,6 +1165,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."
@@ -1163,6 +1177,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: "Are you sure that you want to look this up?"
+openTagPageConfirm: "Are you sure you want to open this hashtags page?"
+specifyHost: "Specify a host"
 failedToPreviewUrl: "Could not preview"
 update: "Update"
 rolesThatCanBeUsedThisEmojiAsReaction: "Roles that can use this emoji as reaction"
@@ -1302,10 +1319,15 @@ keepOriginalFilenameDescription: "If you turn off this setting, files names will
 noDescription: "No description"
 alwaysConfirmFollow: "Always confirm when following"
 inquiry: "Contact"
+tryAgain: "Please try again later"
+confirmWhenRevealingSensitiveMedia: "Confirm when revealing sensitive media"
+sensitiveMediaRevealConfirm: "This media might be sensitive. Are you sure you want to reveal it?"
+createdLists: "Created lists"
+createdAntennas: "Created antennas"
 _delivery:
   status: "Delivery status"
-  stop: "Suspended"
-  resume: "Delivery resume"
+  stop: "Suspend delivery"
+  resume: "Resume delivery"
   _type:
     none: "Publishing"
     manuallySuspended: "Manually suspended"
@@ -1404,7 +1426,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."
@@ -1756,6 +1778,7 @@ _role:
     canManageAvatarDecorations: "Manage avatar decorations"
     driveCapacity: "Drive capacity"
     alwaysMarkNsfw: "Always mark files as NSFW"
+    canUpdateBioMedia: "Allow users to edit their avatar or banner"
     pinMax: "Maximum number of pinned notes"
     antennaMax: "Maximum number of antennas"
     wordMuteMax: "Maximum number of characters allowed in word mutes"
@@ -2005,8 +2028,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."
@@ -2015,6 +2036,7 @@ _soundSettings:
   driveFileTypeWarnDescription: "Select an audio file"
   driveFileDurationWarn: "The audio is too long."
   driveFileDurationWarnDescription: "Long audio may disrupt using Sharkey. Still continue?"
+  driveFileError: "The audio couldn't be loaded. Please make sure you selected an audio file."
 _ago:
   future: "Future"
   justNow: "Just now"
@@ -2132,7 +2154,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"
@@ -2320,6 +2342,7 @@ _timelines:
   local: "Local"
   social: "Social"
   global: "Global"
+  bubble: "Bubble"
 _play:
   new: "Create Play"
   edit: "Edit Play"
@@ -2370,6 +2393,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"
@@ -2431,6 +2455,7 @@ _notification:
     roleAssigned: "Role given"
     achievementEarned: "Achievement unlocked"
     app: "Notifications from linked apps"
+    edited: "Edits"
   _actions:
     followBack: "followed you back"
     reply: "Reply"
@@ -2439,7 +2464,7 @@ _deck:
   alwaysShowMainColumn: "Always show main column"
   columnAlign: "Align columns"
   addColumn: "Add column"
-  newNoteNotificationSettings: "New note notification"
+  newNoteNotificationSettings: "Notification setting for new notes"
   configureColumn: "Column settings"
   swapLeft: "Swap with the left column"
   swapRight: "Swap with the right column"
@@ -2478,9 +2503,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"
@@ -2490,6 +2516,26 @@ _webhookSettings:
     renote: "When boosted"
     reaction: "When receiving a reaction"
     mention: "When being mentioned"
+  _systemEvents:
+    abuseReport: "When received a new abuse report"
+    abuseReportResolved: "When resolved abuse reports"
+    userCreated: "When user is created"
+  deleteConfirm: "Are you sure you want to delete the Webhook?"
+_abuseReport:
+  _notificationRecipient:
+    createRecipient: "Add a recipient for abuse reports"
+    modifyRecipient: "Edit a recipient for abuse reports"
+    recipientType: "Notification type"
+    _recipientType:
+      mail: "Email"
+      webhook: "Webhook"
+      _captions:
+        mail: "Send an email to the moderators when an abuse report is received."
+        webhook: "Send a notification to the SystemWebhook when an abuse report is received or resolved."
+    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"
@@ -2528,6 +2574,17 @@ _moderationLogTypes:
   deleteAvatarDecoration: "Avatar decoration deleted"
   unsetUserAvatar: "Unset this user's avatar"
   unsetUserBanner: "Unset this user's banner"
+  createSystemWebhook: "Create SystemWebhook"
+  updateSystemWebhook: "Update SystemWebhook"
+  deleteSystemWebhook: "Delete SystemWebhook"
+  createAbuseReportNotificationRecipient: "Create a recipient for abuse reports"
+  updateAbuseReportNotificationRecipient: "Update recipients for abuse reports"
+  deleteAbuseReportNotificationRecipient: "Delete a recipient for abuse reports"
+  deleteAccount: "Delete the account"
+  deletePage: "Delete the page"
+  deleteFlash: "Delete Play"
+  deleteGalleryPost: "Delete the gallery post"
+
 _mfm:
   uncommonFeature: "This is not a widespread feature, it may not display properly on most other fedi software, including other Misskey forks"
   intro: "MFM is a markup language used on Misskey, Sharkey, Firefish, Akkoma, and more that can be used in many places. Here you can view a list of all available MFM syntax."
@@ -2608,6 +2665,7 @@ _mfm:
   backgroundDescription: "Change the background color of text."
   plain: "Plain"
   plainDescription: "Deactivates the effects of all MFM contained within this MFM effect."
+
 _fileViewer:
   title: "File details"
   type: "File type"
@@ -2677,7 +2735,7 @@ _dataSaver:
     description: "Prevents images/videos from being loaded automatically. Hidden images/videos will be loaded when tapped."
   _avatar:
     title: "Avatar image"
-    description: "Stop avatar image animation. Animated images can be larger in file size than normal  images, potentially leading to further reductions in data traffic."
+    description: "Stop avatar image animation. Animated images can be larger in file size than normal images, potentially leading to further reductions in data traffic."
   _urlPreview:
     title: "URL preview thumbnails"
     description: "URL preview thumbnail images will no longer be loaded."
@@ -2753,3 +2811,8 @@ _mediaControls:
   pip: "Picture in Picture"
   playbackRate: "Playback Speed"
   loop: "Loop playback"
+_contextMenu:
+  title: "Context menu"
+  app: "Application"
+  appWithShift: "Application with shift key"
+  native: "Native"
diff --git a/locales/es-ES.yml b/locales/es-ES.yml
index 209c2dec2d..f5d1d11036 100644
--- a/locales/es-ES.yml
+++ b/locales/es-ES.yml
@@ -60,6 +60,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"
@@ -302,7 +303,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"
@@ -867,7 +868,7 @@ whatIsNew: "Mostrar cambios"
 translate: "Traducir"
 translatedFrom: "Traducido de {x}"
 accountDeletionInProgress: "La eliminación de la cuenta está en curso"
-usernameInfo: "Un nombre que identifique su cuenta de otras en este servidor.  Puede utilizar el alfabeto (a~z, A~Z), dígitos (0~9) o guiones bajos (_). Los nombres de usuario no se pueden cambiar posteriormente."
+usernameInfo: "Un nombre que identifique su cuenta de otras en este servidor. Puede utilizar el alfabeto (a~z, A~Z), dígitos (0~9) o guiones bajos (_). Los nombres de usuario no se pueden cambiar posteriormente."
 aiChanMode: "Modo Ai"
 devMode: "Modo de desarrollador"
 keepCw: "Mantener la advertencia de contenido"
@@ -1920,8 +1921,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"
@@ -2356,7 +2355,7 @@ _deck:
   newProfile: "Nuevo perfil"
   deleteProfile: "Eliminar perfil"
   introduction: "¡Crea la interfaz perfecta para tí organizando las columnas libremente!"
-  introduction2: "Presiona en la  + de la derecha de la pantalla para añadir nuevas columnas donde quieras."
+  introduction2: "Presiona en la + de la derecha de la pantalla para añadir nuevas columnas donde quieras."
   widgetsIntroduction: "Por favor selecciona \"Editar Widgets\" en el menú columna y agrega un widget."
   useSimpleUiForNonRootPages: "Mostrar páginas no pertenecientes a la raíz con la interfaz simple"
   usedAsMinWidthWhenFlexible: "Se usará el ancho mínimo cuando la opción \"Autoajustar ancho\" esté habilitada"
@@ -2385,7 +2384,6 @@ _webhookSettings:
   createWebhook: "Crear Webhook"
   name: "Nombre"
   secret: "Secreto"
-  events: "Eventos de webhook"
   active: "Activado"
   _events:
     follow: "Cuando se sigue a alguien"
@@ -2395,6 +2393,10 @@ _webhookSettings:
     renote: "Cuando reciba un \"re-note\""
     reaction: "Cuando se recibe una reacción"
     mention: "Cuando hay una mención"
+_abuseReport:
+  _notificationRecipient:
+    _recipientType:
+      mail: "Correo"
 _moderationLogTypes:
   createRole: "Rol creado"
   deleteRole: "Rol eliminado"
diff --git a/locales/fr-FR.yml b/locales/fr-FR.yml
index 22a9e2449f..f615349b89 100644
--- a/locales/fr-FR.yml
+++ b/locales/fr-FR.yml
@@ -1094,6 +1094,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 +1226,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é"
@@ -1245,7 +1250,7 @@ _announcement:
   end: "Archiver l'annonce"
   tooManyActiveAnnouncementDescription: "Un grand nombre d'annonces actives peut baisser l'expérience utilisateur. Considérez d'archiver les annonces obsolètes."
   readConfirmTitle: "Marquer comme lu ?"
-  readConfirmText: "Cela marquera le contenu de  « {title} » comme lu."
+  readConfirmText: "Cela marquera le contenu de « {title} » comme lu."
   shouldNotBeUsedToPresentPermanentInfo: "Puisque cela pourrait nuire considérablement à l'expérience utilisateur pour les nouveaux utilisateurs, il est recommandé d'utiliser les annonces pour afficher des informations temporaires plutôt que des informations persistantes."
   dialogAnnouncementUxWarn: "Avoir deux ou plus annonces de style dialogue en même temps pourrait nuire considérablement à l'expérience utilisateur. Veuillez les utiliser avec caution."
   silence: "Ne pas me notifier"
@@ -1712,8 +1717,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"
@@ -2073,6 +2076,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/id-ID.yml b/locales/id-ID.yml
index 01fa02e678..2f225c1199 100644
--- a/locales/id-ID.yml
+++ b/locales/id-ID.yml
@@ -180,6 +180,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"
@@ -316,6 +320,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"
@@ -579,7 +584,7 @@ sort: "Urutkan"
 ascendingOrder: "Urutkan naik"
 descendingOrder: "Urutkan menurun"
 scratchpad: "Scratchpad"
-scratchpadDescription: "Scratchpad menyediakan lingkungan eksperimen untuk AiScript. Kamu  bisa menulis, mengeksuksi, serta mengecek hasil yang berinteraksi dengan Misskey."
+scratchpadDescription: "Scratchpad menyediakan lingkungan eksperimen untuk AiScript. Kamu bisa menulis, mengeksuksi, serta mengecek hasil yang berinteraksi dengan Misskey."
 output: "Keluaran"
 script: "Script"
 disablePagesScript: "Nonaktifkan script pada halaman"
@@ -1239,6 +1244,7 @@ keepOriginalFilenameDescription: "Apabila pengaturan ini dimatikan, nama berkas
 noDescription: "Tidak ada deskripsi"
 alwaysConfirmFollow: "Selalu konfirmasi ketika mengikuti"
 inquiry: "Hubungi kami"
+tryAgain: "Silahkan coba lagi."
 _delivery:
   status: "Status pengiriman"
   stop: "Ditangguhkan"
@@ -1739,7 +1745,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:
@@ -1932,8 +1938,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"
@@ -2396,9 +2400,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 +2412,11 @@ _webhookSettings:
     renote: "Ketika direnote"
     reaction: "Ketika menerima reaksi"
     mention: "Ketika sedang disebut"
+  deleteConfirm: "Apakah kamu yakin ingin menghapus Webhook?"
+_abuseReport:
+  _notificationRecipient:
+    _recipientType:
+      mail: "Surel"
 _moderationLogTypes:
   createRole: "Peran telah dibuat"
   deleteRole: "Peran telah dihapus"
diff --git a/locales/index.d.ts b/locales/index.d.ts
index 7314151708..55a95f8fd9 100644
--- a/locales/index.d.ts
+++ b/locales/index.d.ts
@@ -264,6 +264,10 @@ export interface Locale extends ILocale {
      * ユーザーを検索
      */
     "searchUser": string;
+    /**
+     * ユーザーのノートを検索
+     */
+    "searchThisUsersNotes": string;
     /**
      * 返信
      */
@@ -664,6 +668,10 @@ export interface Locale extends ILocale {
      * アンテナを編集
      */
     "editAntenna": string;
+    /**
+     * アンテナを作成
+     */
+    "createAntenna": string;
     /**
      * ウィジェットを選択
      */
@@ -776,6 +784,22 @@ export interface Locale extends ILocale {
      * リモートで表示
      */
     "showOnRemote": string;
+    /**
+     * リモートで続行
+     */
+    "continueOnRemote": string;
+    /**
+     * Misskey Hubからサーバーを選択
+     */
+    "chooseServerOnMisskeyHub": string;
+    /**
+     * サーバーのドメインを直接指定
+     */
+    "specifyServerHost": string;
+    /**
+     * ドメインを入力してください
+     */
+    "inputHostName": string;
     /**
      * 全般
      */
@@ -816,6 +840,10 @@ export interface Locale extends ILocale {
      * ホスト
      */
     "host": string;
+    /**
+     * 自分を選択
+     */
+    "selectSelf": string;
     /**
      * ユーザーを選択
      */
@@ -869,13 +897,17 @@ export interface Locale extends ILocale {
      */
     "stopActivityDelivery": string;
     /**
-     * このインスタンスをブロック
+     * このサーバーをブロック
      */
     "blockThisInstance": string;
     /**
-     * インスタンスをサイレンス
+     * サーバーをサイレンス
      */
     "silenceThisInstance": string;
+    /**
+     * サーバーをメディアサイレンス
+     */
+    "mediaSilenceThisInstance": string;
     /**
      * 操作
      */
@@ -960,6 +992,14 @@ export interface Locale extends ILocale {
      * サイレンスしたいサーバーのホストを改行で区切って設定します。サイレンスされたサーバーに所属するアカウントはすべて「サイレンス」として扱われ、フォローがすべてリクエストになります。ブロックしたインスタンスには影響しません。
      */
     "silencedInstancesDescription": string;
+    /**
+     * メディアサイレンスしたサーバー
+     */
+    "mediaSilencedInstances": string;
+    /**
+     * メディアサイレンスしたいサーバーのホストを改行で区切って設定します。メディアサイレンスされたサーバーに所属するアカウントによるファイルはすべてセンシティブとして扱われ、カスタム絵文字が使用できないようになります。ブロックしたインスタンスには影響しません。
+     */
+    "mediaSilencedInstancesDescription": string;
     /**
      * ミュートとブロック
      */
@@ -1300,6 +1340,10 @@ export interface Locale extends ILocale {
      * ドライブ
      */
     "drive": string;
+    /**
+     * 検索ドライブ
+     */
+    "driveSearchbarPlaceholder": string;
     /**
      * ファイル名
      */
@@ -1973,9 +2017,13 @@ export interface Locale extends ILocale {
      */
     "onlyOneFileCanBeAttached": string;
     /**
-     * 続行する前に、サインアップまたはサインインが必要です
+     * 続行する前に、登録またはログインが必要です
      */
     "signinRequired": string;
+    /**
+     * 続行するには、お使いのサーバーに移動するか、このサーバーに登録・ログインする必要があります
+     */
+    "signinOrContinueOnRemote": string;
     /**
      * 招待
      */
@@ -2140,6 +2188,10 @@ export interface Locale extends ILocale {
      * 画像を新しいタブで開く
      */
     "openImageInNewTab": string;
+    /**
+     * 代替テキストを入れ忘れたときに警告する
+     */
+    "warnForMissingAltText": string;
     /**
      * ダッシュボード
      */
@@ -2269,11 +2321,11 @@ export interface Locale extends ILocale {
      */
     "s3ForcePathStyleDesc": string;
     /**
-     * DeepLX-JS を使用する (認証キーなし)
+     * DeepLX-JS を使用する (認証キー不要)
      */
     "deeplFreeMode": string;
     /**
-     * ヘルプが必要ですか? DeepLX-JSのセットアップ方法については、ドキュメントを参照してください。
+     * DeepLX-JSの設定方法については、ドキュメントを参照してください。
      */
     "deeplFreeModeDescription": string;
     /**
@@ -2337,7 +2389,7 @@ export interface Locale extends ILocale {
      */
     "notUseSound": string;
     /**
-     * Misskeyがアクティブな時のみサウンドを出力する
+     * Sharkeyがアクティブな時のみサウンドを出力する
      */
     "useSoundOnlyWhenActive": string;
     /**
@@ -2797,19 +2849,19 @@ export interface Locale extends ILocale {
      */
     "enableFaviconNotificationDot": string;
     /**
-     * 通知ドットがインスタンスで機能するかどうかを確認します。
+     * タブアイコン強調機能の動作確認
      */
     "verifyNotificationDotWorkingButton": string;
     /**
-     * 残念ながら、このインスタンスは現時点では通知ドット機能をサポートしていません。
+     * このサーバーは現時点ではタブアイコン強調機能をサポートしていません。
      */
     "notificationDotNotWorking": string;
     /**
-     * 通知ドットは、このインスタンスで正しく機能しています。
+     * タブアイコン強調機能は、このサーバーで正しく機能しています。
      */
     "notificationDotWorking": string;
     /**
-     * 通知ドットが機能しない場合は、管理者にドキュメントを確認するように依頼してください {link}
+     * タブアイコン強調機能が機能しない場合は、管理者にドキュメントを確認するように依頼してください {link}
      */
     "notificationDotNotWorkingAdvice": ParameterizedString<"link">;
     /**
@@ -2869,7 +2921,7 @@ export interface Locale extends ILocale {
      */
     "reportAbuseOf": ParameterizedString<"name">;
     /**
-     * 通報理由の詳細を記入してください。対象のノートがある場合はそのURLも記入してください。
+     * 通報理由の詳細を記入してください。対象のノートやページなどがある場合はそのURLも記入してください。
      */
     "fillAbuseReportDescription": string;
     /**
@@ -3073,15 +3125,15 @@ export interface Locale extends ILocale {
      */
     "searchEngine": string;
     /**
-     * 他
+     * カスタム
      */
     "searchEngineOther": string;
     /**
-     * カスタム URI は、"https://www.google.com/search?q=\{query}" や "https://www.google.com/search?q=%s" のような形式で入力する必要があります。
+     * カスタム検索エンジンのURIは、"https://www.google.com/search?q=\{query}" や "https://www.google.com/search?q=%s" のような形式で入力する必要があります。
      */
     "searchEngineCustomURIDescription": string;
     /**
-     * カスタム URI
+     * カスタム検索エンジン URI
      */
     "searchEngineCusomURI": string;
     /**
@@ -3677,7 +3729,7 @@ export interface Locale extends ILocale {
      */
     "emailRequiredForSignup": string;
     /**
-     * 新規ユーザーの承認が必要
+     * アカウント登録を承認制にする
      */
     "approvalRequiredForSignup": string;
     /**
@@ -3886,7 +3938,7 @@ export interface Locale extends ILocale {
      */
     "thereIsUnresolvedAbuseReportWarning": string;
     /**
-     * 承認待ちのユーザーがいる。
+     * 承認待ちのユーザーがいます。
      */
     "pendingUserApprovals": string;
     /**
@@ -3958,7 +4010,7 @@ export interface Locale extends ILocale {
      */
     "numberOfReplies": string;
     /**
-     * この数値を大きくすると、より多くの返信が表示されます。この値を大きくしすぎると、返信が窮屈になり、読めなくなることがあります。
+     * この数値を大きくすると、より多くの返信が表示されます。この値を大きくしすぎると、UIが窮屈になって読みにくくなることがあります。
      */
     "numberOfRepliesDescription": string;
     /**
@@ -3966,15 +4018,15 @@ export interface Locale extends ILocale {
      */
     "boostSettings": string;
     /**
-     * 可視性セレクタを表示
+     * 公開範囲セレクターを表示
      */
     "showVisibilitySelectorOnBoost": string;
     /**
-     * 無効の場合、以下で定義されるデフォルトの可視性が使用され、セレクタは表示されません。
+     * 無効の場合、以下で設定したデフォルトの公開範囲が使用され、セレクターは表示されません。
      */
     "showVisibilitySelectorOnBoostDescription": string;
     /**
-     * デフォルトのブースト可視性の設定
+     * デフォルトのブースト公開範囲
      */
     "visibilityOnBoost": string;
     /**
@@ -4302,7 +4354,7 @@ export interface Locale extends ILocale {
      */
     "thisPostIsMissingAltTextIgnore": string;
     /**
-     * この投稿に添付されたファイルの 1 つに代替テキストがありません。すべての添付ファイルに代替テキストが含まれていることを確認してください。
+     * 代替テキストがないファイルが添付されています。すべての添付ファイルに代替テキストを含むようにしてください。
      */
     "thisPostIsMissingAltText": string;
     /**
@@ -4314,7 +4366,7 @@ export interface Locale extends ILocale {
      */
     "collapseRenotesDescription": string;
     /**
-     * 返信されたノート省略
+     * 返信元のノートを折りたたむ
      */
     "collapseNotesRepliedTo": string;
     /**
@@ -4322,7 +4374,7 @@ export interface Locale extends ILocale {
      */
     "collapseFiles": string;
     /**
-     * 返信に会話を読み込む
+     * 会話スレッドを自動で読み込む
      */
     "autoloadConversation": string;
     /**
@@ -4366,7 +4418,7 @@ export interface Locale extends ILocale {
      */
     "invitationRequiredToRegister": string;
     /**
-     * このインスタンスは、登録理由を指定したユーザーのみを受け入れています。
+     * 現在このサーバーは承認制です。参加したい理由を記入し、承認された方のみ登録できます。
      */
     "approvalRequiredToRegister": string;
     /**
@@ -4538,7 +4590,7 @@ export interface Locale extends ILocale {
      */
     "forceShowAds": string;
     /**
-     * 猫友達 :3
+     * にゃんこフレンド :3
      */
     "oneko": string;
     /**
@@ -4625,6 +4677,14 @@ export interface Locale extends ILocale {
      * アーカイブ
      */
     "archive": string;
+    /**
+     * アーカイブ済み
+     */
+    "archived": string;
+    /**
+     * アーカイブ解除
+     */
+    "unarchive": string;
     /**
      * {name}をアーカイブしますか?
      */
@@ -4665,6 +4725,18 @@ export interface Locale extends ILocale {
      * ユーザー指定
      */
     "specifyUser": string;
+    /**
+     * 照会しますか?
+     */
+    "lookupConfirm": string;
+    /**
+     * ハッシュタグのページを開きますか?
+     */
+    "openTagPageConfirm": string;
+    /**
+     * ホスト指定
+     */
+    "specifyHost": string;
     /**
      * プレビューできません
      */
@@ -4970,7 +5042,7 @@ export interface Locale extends ILocale {
      */
     "repositoryUrl": string;
     /**
-     * ソースコードが公開されているリポジトリがある場合、そのURLを記入します。Misskeyを現状のまま(ソースコードにいかなる変更も加えずに)使用している場合は https://github.com/misskey-dev/misskey と記入します。
+     * ソースコードが公開されているリポジトリがある場合、そのURLを記入します。Sharkeyを現状のまま(ソースコードにいかなる変更も加えずに)使用している場合は https://activitypub.software/TransFem-org/Sharkey/ と記入します。
      */
     "repositoryUrlDescription": string;
     /**
@@ -5221,6 +5293,26 @@ export interface Locale extends ILocale {
      * お問い合わせ
      */
     "inquiry": string;
+    /**
+     * もう一度お試しください。
+     */
+    "tryAgain": string;
+    /**
+     * センシティブなメディアを表示するとき確認する
+     */
+    "confirmWhenRevealingSensitiveMedia": string;
+    /**
+     * センシティブなメディアです。表示しますか?
+     */
+    "sensitiveMediaRevealConfirm": string;
+    /**
+     * 作成したリスト
+     */
+    "createdLists": string;
+    /**
+     * 作成したアンテナ
+     */
+    "createdAntennas": string;
     "_delivery": {
         /**
          * 配信状態
@@ -6839,6 +6931,10 @@ export interface Locale extends ILocale {
              * ファイルにNSFWを常に付与
              */
             "alwaysMarkNsfw": string;
+            /**
+             * アイコンとバナーの更新を許可
+             */
+            "canUpdateBioMedia": string;
             /**
              * ノートのピン留めの最大数
              */
@@ -7784,14 +7880,6 @@ export interface Locale extends ILocale {
          * 通知
          */
         "notification": string;
-        /**
-         * アンテナ受信
-         */
-        "antenna": string;
-        /**
-         * チャンネル通知
-         */
-        "channel": string;
         /**
          * リアクション選択時
          */
@@ -7819,9 +7907,13 @@ export interface Locale extends ILocale {
          */
         "driveFileDurationWarn": string;
         /**
-         * 長い音声を使用するとMisskeyの使用に支障をきたす可能性があります。それでも続行しますか?
+         * 長い音声を使用するとSharkeyの使用に支障をきたす可能性があります。それでも続行しますか?
          */
         "driveFileDurationWarnDescription": string;
+        /**
+         * 音声が読み込めませんでした。設定を変更してください
+         */
+        "driveFileError": string;
     };
     "_ago": {
         /**
@@ -9002,6 +9094,10 @@ export interface Locale extends ILocale {
          * グローバル
          */
         "global": string;
+        /**
+         * バッッブル
+         */
+        "bubble": string;
     };
     "_play": {
         /**
@@ -9198,6 +9294,10 @@ export interface Locale extends ILocale {
          * ブロックを追加
          */
         "chooseBlock": string;
+        /**
+         * セクションタイトルを入力
+         */
+        "enterSectionTitle": string;
         /**
          * 種類を選択
          */
@@ -9431,6 +9531,10 @@ export interface Locale extends ILocale {
              * 連携アプリからの通知
              */
             "app": string;
+            /**
+             * 編集済み
+             */
+            "edited": string;
         };
         "_actions": {
             /**
@@ -9442,7 +9546,7 @@ export interface Locale extends ILocale {
              */
             "reply": string;
             /**
-             * Boost
+             * ブースト
              */
             "renote": string;
         };
@@ -9606,6 +9710,10 @@ export interface Locale extends ILocale {
          * Webhookを作成
          */
         "createWebhook": string;
+        /**
+         * Webhookを編集
+         */
+        "modifyWebhook": string;
         /**
          * 名前
          */
@@ -9615,9 +9723,9 @@ export interface Locale extends ILocale {
          */
         "secret": string;
         /**
-         * Webhookを実行するタイミング
+         * トリガー
          */
-        "events": string;
+        "trigger": string;
         /**
          * 有効
          */
@@ -9652,6 +9760,76 @@ export interface Locale extends ILocale {
              */
             "mention": string;
         };
+        "_systemEvents": {
+            /**
+             * ユーザーから通報があったとき
+             */
+            "abuseReport": string;
+            /**
+             * ユーザーからの通報を処理したとき
+             */
+            "abuseReportResolved": string;
+            /**
+             * ユーザーが作成されたとき
+             */
+            "userCreated": string;
+        };
+        /**
+         * Webhookを削除しますか?
+         */
+        "deleteConfirm": 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": {
         /**
@@ -9802,6 +9980,364 @@ 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;
+    };
+    "_mfm": {
+        /**
+         * この機能は一般的に普及していないため、他のMisskeyフォークを含めた多くのFediverseソフトウェアで表示できないことがあります。
+         */
+        "uncommonFeature": string;
+        /**
+         * MFM はMisskey, Sharkey, Firefish, Akkomaなど、多くの場所で使用できるマークアップ言語です。ここでは、利用できるMFM構文の一覧をご覧いただけます。
+         */
+        "intro": string;
+        /**
+         * SharkeyでFediverseの世界が広がります
+         */
+        "dummy": string;
+        /**
+         * メンション
+         */
+        "mention": string;
+        /**
+         * アットマーク + ユーザー名で、特定のユーザーを示すことができます。
+         */
+        "mentionDescription": string;
+        /**
+         * ハッシュタグ
+         */
+        "hashtag": string;
+        /**
+         * ナンバーサイン + タグで、ハッシュタグを示すことができます。
+         */
+        "hashtagDescription": string;
+        /**
+         * URL
+         */
+        "url": string;
+        /**
+         * URLを示すことができます。
+         */
+        "urlDescription": string;
+        /**
+         * リンク
+         */
+        "link": string;
+        /**
+         * 文章の特定の範囲を、URLに紐づけることができます。
+         */
+        "linkDescription": string;
+        /**
+         * 太字
+         */
+        "bold": string;
+        /**
+         * 文字を太く表示して強調することができます。
+         */
+        "boldDescription": string;
+        /**
+         * 小文字
+         */
+        "small": string;
+        /**
+         * 内容を小さく・薄く表示させることができます。
+         */
+        "smallDescription": string;
+        /**
+         * 中央寄せ
+         */
+        "center": string;
+        /**
+         * 内容を中央寄せで表示させることができます。
+         */
+        "centerDescription": string;
+        /**
+         * コード(インライン)
+         */
+        "inlineCode": string;
+        /**
+         * プログラムなどのコードをインラインでシンタックスハイライトします。
+         */
+        "inlineCodeDescription": string;
+        /**
+         * コード(ブロック)
+         */
+        "blockCode": string;
+        /**
+         * 複数行のプログラムなどのコードをブロックでシンタックスハイライトします。
+         */
+        "blockCodeDescription": string;
+        /**
+         * 数式(インライン)
+         */
+        "inlineMath": string;
+        /**
+         * 数式 (KaTeX形式)をインラインで表示します。
+         */
+        "inlineMathDescription": string;
+        /**
+         * 数式(ブロック)
+         */
+        "blockMath": string;
+        /**
+         * 数式 (KaTeX形式)をブロックで表示します。
+         */
+        "blockMathDescription": string;
+        /**
+         * 引用
+         */
+        "quote": string;
+        /**
+         * 内容が引用であることを示すことができます。
+         */
+        "quoteDescription": string;
+        /**
+         * カスタム絵文字
+         */
+        "emoji": string;
+        /**
+         * コロンでカスタム絵文字名を囲むと、カスタム絵文字を表示させることができます。
+         */
+        "emojiDescription": string;
+        /**
+         * 検索
+         */
+        "search": string;
+        /**
+         * 検索ボックスを表示できます。
+         */
+        "searchDescription": string;
+        /**
+         * 反転
+         */
+        "flip": string;
+        /**
+         * 内容を上下または左右に反転させます。
+         */
+        "flipDescription": string;
+        /**
+         * アニメーション(びよんびよん)
+         */
+        "jelly": string;
+        /**
+         * ゼリーが揺れるような感じのアニメーションをさせます。
+         */
+        "jellyDescription": string;
+        /**
+         * アニメーション(じゃーん)
+         */
+        "tada": string;
+        /**
+         * 「じゃーん!」と強調するような感じのアニメーションをさせます。
+         */
+        "tadaDescription": string;
+        /**
+         * アニメーション(ジャンプ)
+         */
+        "jump": string;
+        /**
+         * 跳ねるアニメーションをさせます。
+         */
+        "jumpDescription": string;
+        /**
+         * アニメーション(バウンド)
+         */
+        "bounce": string;
+        /**
+         * 跳ねて着地するようなアニメーションをさせます。
+         */
+        "bounceDescription": string;
+        /**
+         * アニメーション(ぶるぶる)
+         */
+        "shake": string;
+        /**
+         * 震えるアニメーションをさせます。
+         */
+        "shakeDescription": string;
+        /**
+         * アニメーション(ガタガタ)
+         */
+        "twitch": string;
+        /**
+         * より激しく震えるアニメーションをさせます。
+         */
+        "twitchDescription": string;
+        /**
+         * アニメーション(回転)
+         */
+        "spin": string;
+        /**
+         * 内容を回転させます。
+         */
+        "spinDescription": string;
+        /**
+         * 大
+         */
+        "x2": string;
+        /**
+         * 内容を大きく表示させます。
+         */
+        "x2Description": string;
+        /**
+         * 特大
+         */
+        "x3": string;
+        /**
+         * 内容をより大きく表示させます。
+         */
+        "x3Description": string;
+        /**
+         * 超特大
+         */
+        "x4": string;
+        /**
+         * 内容をさらに大きく表示させます。
+         */
+        "x4Description": string;
+        /**
+         * ぼかし
+         */
+        "blur": string;
+        /**
+         * 内容をぼかすことができます。ポインターを上に乗せるとはっきり見えるようになります。
+         */
+        "blurDescription": string;
+        /**
+         * フォント
+         */
+        "font": string;
+        /**
+         * 内容のフォントを指定することができます。
+         */
+        "fontDescription": string;
+        /**
+         * レインボー
+         */
+        "rainbow": string;
+        /**
+         * 内容を虹色で表示させます。
+         */
+        "rainbowDescription": string;
+        /**
+         * キラキラ
+         */
+        "sparkle": string;
+        /**
+         * キラキラと星型のパーティクルを表示させます。
+         */
+        "sparkleDescription": string;
+        /**
+         * 角度変更
+         */
+        "rotate": string;
+        /**
+         * 指定した角度で回転させます。
+         */
+        "rotateDescription": string;
+        /**
+         * 位置変更
+         */
+        "position": string;
+        /**
+         * 位置をずらすことができます。
+         */
+        "positionDescription": string;
+        /**
+         * 切り取り
+         */
+        "crop": string;
+        /**
+         * 内容を切り抜きます。
+         */
+        "cropDescription": string;
+        /**
+         * マウス追従
+         */
+        "followMouse": string;
+        /**
+         * 内容がマウスに追従します。スマホの場合はタップした場所に追従します。
+         */
+        "followMouseDescription": string;
+        /**
+         * 拡大
+         */
+        "scale": string;
+        /**
+         * 内容を引き伸ばして表示します。
+         */
+        "scaleDescription": string;
+        /**
+         * 文字色
+         */
+        "foreground": string;
+        /**
+         * 文字色を変更します。
+         */
+        "foregroundDescription": string;
+        /**
+         * フェード
+         */
+        "fade": string;
+        /**
+         * 内容をフェードイン・フェードアウトさせます。
+         */
+        "fadeDescription": string;
+        /**
+         * 背景色
+         */
+        "background": string;
+        /**
+         * 背景色を変更します。
+         */
+        "backgroundDescription": string;
+        /**
+         * Plain
+         */
+        "plain": string;
+        /**
+         * 内側の構文を全て無効にします。
+         */
+        "plainDescription": string;
     };
     "_fileViewer": {
         /**
@@ -9980,7 +10516,7 @@ export interface Locale extends ILocale {
         "stop": string;
         "_alert": {
             /**
-             * MFMアニメーションには、点滅するライトや高速で動くテキスト/絵文字を含まれる場合があります。
+             * MFMアニメーションには、高速で点滅したり動いたりするテキスト・絵文字を含む場合があります。
              */
             "text": string;
             /**
@@ -9999,18 +10535,18 @@ export interface Locale extends ILocale {
          */
         "warn": string;
         /**
-         * データの保存が完了すると、このアカウントに登録されているEメールアドレスにメールが送信されます。
+         * データの保存が完了すると、このアカウントに登録されているメールアドレスにメールが送信されます。
          */
         "text": string;
         /**
-         * リクエスト
+         * データリクエスト実行
          */
         "button": string;
     };
     "_dataSaver": {
         "_media": {
             /**
-             * メディアの読み込み
+             * メディアの読み込みを無効化
              */
             "title": string;
             /**
@@ -10020,7 +10556,7 @@ export interface Locale extends ILocale {
         };
         "_avatar": {
             /**
-             * アイコン画像
+             * アイコン画像のアニメーションを無効化
              */
             "title": string;
             /**
@@ -10030,7 +10566,7 @@ export interface Locale extends ILocale {
         };
         "_urlPreview": {
             /**
-             * URLプレビューのサムネイル
+             * URLプレビューのサムネイルを非表示
              */
             "title": string;
             /**
@@ -10040,7 +10576,7 @@ export interface Locale extends ILocale {
         };
         "_code": {
             /**
-             * コードハイライト
+             * コードハイライトを非表示
              */
             "title": string;
             /**
@@ -10315,6 +10851,24 @@ export interface Locale extends ILocale {
          */
         "loop": string;
     };
+    "_contextMenu": {
+        /**
+         * コンテキストメニュー
+         */
+        "title": string;
+        /**
+         * アプリケーション
+         */
+        "app": string;
+        /**
+         * Shiftキーでアプリケーション
+         */
+        "appWithShift": string;
+        /**
+         * ブラウザのUI
+         */
+        "native": string;
+    };
 }
 declare const locales: {
     [lang: string]: Locale;
diff --git a/locales/index.js b/locales/index.js
index c7a693fb77..48ff85f1a5 100644
--- a/locales/index.js
+++ b/locales/index.js
@@ -56,7 +56,11 @@ const primaries = {
 const clean = (text) => text.replace(new RegExp(String.fromCodePoint(0x08), 'g'), '').replaceAll(new RegExp(/\\+\{/,'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 2b04c5abfb..734b4bd0b1 100644
--- a/locales/it-IT.yml
+++ b/locales/it-IT.yml
@@ -109,11 +109,14 @@ 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"
@@ -178,6 +181,10 @@ 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"
@@ -314,6 +321,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"
@@ -469,10 +477,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"
@@ -696,7 +706,7 @@ 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"
+abuseMarkAsResolved: "Risolvi segnalazione"
 openInNewTab: "Apri in una nuova scheda"
 openInSideView: "Apri in vista laterale"
 defaultNavigationBehaviour: "Navigazione preimpostata"
@@ -835,6 +845,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"
@@ -1025,6 +1036,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"
@@ -1237,10 +1249,20 @@ 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?"
 _delivery:
-  stop: "Sospensione"
+  status: "Stato della distribuzione di attività"
+  stop: "Sospendi la distribuzione di attività"
+  resume: "Riprendi la distribuzione di attività"
   _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"
@@ -1366,6 +1388,8 @@ _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."
+  inquiryUrl: "URL di contatto"
+  inquiryUrlDescription: "Specificare l'URL al modulo di contatto, oppure le informazioni con i dati di contatto dell'amministrazione."
 _accountMigration:
   moveFrom: "Migra un altro profilo dentro a questo"
   moveFromSub: "Crea un alias verso un altro profilo remoto"
@@ -1682,6 +1706,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"
@@ -1700,6 +1725,11 @@ _role:
     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"
@@ -1753,7 +1783,7 @@ _ad:
 _forgotPassword:
   enterEmail: "Inserisci l'indirizzo di posta elettronica che hai registrato nel tuo profilo. Il collegamento necessario per ripristinare la password verrà inviato a questo indirizzo."
   ifNoEmail: "Se il tuo indirizzo email non risulta registrato, contatta l'amministrazione dell'istanza."
-  contactAdmin: "Poiché questa istanza non permette di impostare l'indirizzo mail, contatta l'amministrazione per  ripristinare la password.\n"
+  contactAdmin: "Poiché questa istanza non permette di impostare l'indirizzo mail, contatta l'amministrazione per ripristinare la password.\n"
 _gallery:
   my: "Le mie pubblicazioni"
   liked: "Pubblicazioni che mi piacciono"
@@ -1921,8 +1951,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"
@@ -2347,6 +2375,7 @@ _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"
@@ -2376,7 +2405,7 @@ _deck:
     roleTimeline: "Timeline Ruolo"
 _dialog:
   charactersExceeded: "Hai superato il limite di {max} caratteri! ({current})"
-  charactersBelow: "Sei al di sotto del minimo di {min} caratteri!  ({current})"
+  charactersBelow: "Sei al di sotto del minimo di {min} caratteri! ({current})"
 _disabledTimeline:
   title: "Timeline disabilitata"
   description: "Il ruolo in cui sei non ti permette di leggere questa timeline"
@@ -2385,9 +2414,9 @@ _drivecleaner:
   orderByCreatedAtAsc: "Dal più vecchio al più recente"
 _webhookSettings:
   createWebhook: "Creazione Webhook"
+  modifyWebhook: "Modifica Webhook"
   name: "Nome"
   secret: "Segreto"
-  events: "Quando eseguire il Webhook"
   active: "Attivo"
   _events:
     follow: "Quando segui un profilo"
@@ -2397,6 +2426,25 @@ _webhookSettings:
     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"
+  deleteConfirm: "Vuoi davvero eliminare il Webhook?"
+_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"
@@ -2434,6 +2482,12 @@ _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"
 _fileViewer:
   title: "Dettagli del file"
   type: "Tipo di file"
@@ -2559,6 +2613,8 @@ _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"
diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index 8485037111..da208147a1 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -62,6 +62,7 @@ copyFileId: "ファイルIDをコピー"
 copyFolderId: "フォルダーIDをコピー"
 copyProfileUrl: "プロフィールURLをコピー"
 searchUser: "ユーザーを検索"
+searchThisUsersNotes: "ユーザーのノートを検索"
 reply: "返信"
 loadMore: "もっと見る"
 showMore: "もっと見る"
@@ -162,6 +163,7 @@ editList: "リストを編集"
 selectChannel: "チャンネルを選択"
 selectAntenna: "アンテナを選択"
 editAntenna: "アンテナを編集"
+createAntenna: "アンテナを作成"
 selectWidget: "ウィジェットを選択"
 editWidgets: "ウィジェットを編集"
 editWidgetsExit: "編集を終了"
@@ -190,6 +192,10 @@ addAccount: "アカウントを追加"
 reloadAccountsList: "アカウントリストの情報を更新"
 loginFailed: "ログインに失敗しました"
 showOnRemote: "リモートで表示"
+continueOnRemote: "リモートで続行"
+chooseServerOnMisskeyHub: "Misskey Hubからサーバーを選択"
+specifyServerHost: "サーバーのドメインを直接指定"
+inputHostName: "ドメインを入力してください"
 general: "全般"
 wallpaper: "壁紙"
 setWallpaper: "壁紙を設定"
@@ -200,6 +206,7 @@ followConfirm: "{name}をフォローしますか?"
 proxyAccount: "プロキシアカウント"
 proxyAccountDescription: "プロキシアカウントは、特定の条件下でユーザーのリモートフォローを代行するアカウントです。例えば、ユーザーがリモートユーザーをリストに入れたとき、リストに入れられたユーザーを誰もフォローしていないとアクティビティがサーバーに配達されないため、代わりにプロキシアカウントがフォローするようにします。"
 host: "ホスト"
+selectSelf: "自分を選択"
 selectUser: "ユーザーを選択"
 recipient: "宛先"
 annotation: "注釈"
@@ -213,8 +220,9 @@ charts: "チャート"
 perHour: "1時間ごと"
 perDay: "1日ごと"
 stopActivityDelivery: "アクティビティの配送を停止"
-blockThisInstance: "このインスタンスをブロック"
-silenceThisInstance: "インスタンスをサイレンス"
+blockThisInstance: "このサーバーをブロック"
+silenceThisInstance: "サーバーをサイレンス"
+mediaSilenceThisInstance: "サーバーをメディアサイレンス"
 operations: "操作"
 software: "ソフトウェア"
 version: "バージョン"
@@ -236,6 +244,8 @@ blockedInstances: "ブロックしたサーバー"
 blockedInstancesDescription: "ブロックしたいサーバーのホストを改行で区切って設定します。ブロックされたサーバーは、このインスタンスとやり取りできなくなります。"
 silencedInstances: "サイレンスしたサーバー"
 silencedInstancesDescription: "サイレンスしたいサーバーのホストを改行で区切って設定します。サイレンスされたサーバーに所属するアカウントはすべて「サイレンス」として扱われ、フォローがすべてリクエストになります。ブロックしたインスタンスには影響しません。"
+mediaSilencedInstances: "メディアサイレンスしたサーバー"
+mediaSilencedInstancesDescription: "メディアサイレンスしたいサーバーのホストを改行で区切って設定します。メディアサイレンスされたサーバーに所属するアカウントによるファイルはすべてセンシティブとして扱われ、カスタム絵文字が使用できないようになります。ブロックしたインスタンスには影響しません。"
 muteAndBlock: "ミュートとブロック"
 mutedUsers: "ミュートしたユーザー"
 blockedUsers: "ブロックしたユーザー"
@@ -321,6 +331,7 @@ lightThemes: "明るいテーマ"
 darkThemes: "暗いテーマ"
 syncDeviceDarkMode: "デバイスのダークモードと同期する"
 drive: "ドライブ"
+driveSearchbarPlaceholder: "検索ドライブ"
 fileName: "ファイル名"
 selectFile: "ファイルを選択"
 selectFiles: "ファイルを選択"
@@ -489,7 +500,8 @@ attachAsFileQuestion: "クリップボードのテキストが長いです。テ
 noMessagesYet: "まだチャットはありません"
 newMessageExists: "新しいメッセージがあります"
 onlyOneFileCanBeAttached: "メッセージに添付できるファイルはひとつです"
-signinRequired: "続行する前に、サインアップまたはサインインが必要です"
+signinRequired: "続行する前に、登録またはログインが必要です"
+signinOrContinueOnRemote: "続行するには、お使いのサーバーに移動するか、このサーバーに登録・ログインする必要があります"
 invitations: "招待"
 invitationCode: "招待コード"
 checking: "確認しています"
@@ -531,6 +543,7 @@ mediaListWithOneImageAppearance: "画像が1枚のみのメディアリストの
 limitTo: "{x}を上限に"
 noFollowRequests: "フォロー申請はありません"
 openImageInNewTab: "画像を新しいタブで開く"
+warnForMissingAltText: "代替テキストを入れ忘れたときに警告する"
 dashboard: "ダッシュボード"
 local: "ローカル"
 remote: "リモート"
@@ -563,8 +576,8 @@ objectStorageUseProxy: "Proxyを利用する"
 objectStorageUseProxyDesc: "API接続にproxyを利用しない場合はオフにしてください"
 objectStorageSetPublicRead: "アップロード時に'public-read'を設定する"
 s3ForcePathStyleDesc: "s3ForcePathStyleを有効にすると、バケット名をURLのホスト名ではなくパスの一部として指定することを強制します。セルフホストされたMinioなどの使用時に有効にする必要がある場合があります。"
-deeplFreeMode: "DeepLX-JS を使用する (認証キーなし)"
-deeplFreeModeDescription: "ヘルプが必要ですか? DeepLX-JSのセットアップ方法については、ドキュメントを参照してください。"
+deeplFreeMode: "DeepLX-JS を使用する (認証キー不要)"
+deeplFreeModeDescription: "DeepLX-JSの設定方法については、ドキュメントを参照してください。"
 serverLogs: "サーバーログ"
 deleteAll: "全て削除"
 showFixedPostForm: "タイムライン上部に投稿フォームを表示する"
@@ -580,7 +593,7 @@ popout: "ポップアウト"
 volume: "音量"
 masterVolume: "マスター音量"
 notUseSound: "サウンドを出力しない"
-useSoundOnlyWhenActive: "Misskeyがアクティブな時のみサウンドを出力する"
+useSoundOnlyWhenActive: "Sharkeyがアクティブな時のみサウンドを出力する"
 details: "詳細"
 chooseEmoji: "絵文字を選択"
 unableToProcess: "操作を完了できません"
@@ -695,10 +708,10 @@ create: "作成"
 notificationSetting: "通知設定"
 notificationSettingDesc: "表示する通知の種別を選択してください。"
 enableFaviconNotificationDot: "未読の通知があるときにタブのアイコンを目立たせる"
-verifyNotificationDotWorkingButton: "通知ドットがインスタンスで機能するかどうかを確認します。"
-notificationDotNotWorking: "残念ながら、このインスタンスは現時点では通知ドット機能をサポートしていません。"
-notificationDotWorking: "通知ドットは、このインスタンスで正しく機能しています。"
-notificationDotNotWorkingAdvice: "通知ドットが機能しない場合は、管理者にドキュメントを確認するように依頼してください {link}"
+verifyNotificationDotWorkingButton: "タブアイコン強調機能の動作確認"
+notificationDotNotWorking: "このサーバーは現時点ではタブアイコン強調機能をサポートしていません。"
+notificationDotWorking: "タブアイコン強調機能は、このサーバーで正しく機能しています。"
+notificationDotNotWorkingAdvice: "タブアイコン強調機能が機能しない場合は、管理者にドキュメントを確認するように依頼してください {link}"
 useGlobalSetting: "グローバル設定を使う"
 useGlobalSettingDesc: "オンにすると、アカウントの通知設定が使用されます。オフにすると、個別に設定できるようになります。"
 other: "その他"
@@ -713,7 +726,7 @@ abuseReports: "通報"
 reportAbuse: "通報"
 reportAbuseRenote: "ブーストを通報"
 reportAbuseOf: "{name}を通報する"
-fillAbuseReportDescription: "通報理由の詳細を記入してください。対象のノートがある場合はそのURLも記入してください。"
+fillAbuseReportDescription: "通報理由の詳細を記入してください。対象のノートやページなどがある場合はそのURLも記入してください。"
 abuseReported: "内容が送信されました。ご報告ありがとうございました。"
 reporter: "通報者"
 reporteeOrigin: "通報先"
@@ -764,9 +777,9 @@ lockedAccountInfo: "フォローを承認制にしても、ノートの公開範
 alwaysMarkSensitive: "デフォルトでメディアをセンシティブ設定にする"
 loadRawImages: "添付画像のサムネイルをオリジナル画質にする"
 searchEngine: "検索MFMの検索エンジン"
-searchEngineOther: "他"
-searchEngineCustomURIDescription: "カスタム URI は、\"https://www.google.com/search?q=\\{query}\" や \"https://www.google.com/search?q=%s\" のような形式で入力する必要があります。"
-searchEngineCusomURI: "カスタム URI"
+searchEngineOther: "カスタム"
+searchEngineCustomURIDescription: "カスタム検索エンジンのURIは、\"https://www.google.com/search?q=\\{query}\" や \"https://www.google.com/search?q=%s\" のような形式で入力する必要があります。"
+searchEngineCusomURI: "カスタム検索エンジン URI"
 disableShowingAnimatedImages: "アニメーション画像を再生しない"
 highlightSensitiveMedia: "メディアがセンシティブであることを分かりやすく表示"
 verificationEmailSent: "確認のメールを送信しました。メールに記載されたリンクにアクセスして、設定を完了してください。"
@@ -915,7 +928,7 @@ itsOff: "オフになっています"
 on: "オン"
 off: "オフ"
 emailRequiredForSignup: "アカウント登録にメールアドレスを必須にする"
-approvalRequiredForSignup: "新規ユーザーの承認が必要"
+approvalRequiredForSignup: "アカウント登録を承認制にする"
 unread: "未読"
 filter: "フィルタ"
 controlPanel: "コントロールパネル"
@@ -967,7 +980,7 @@ recentNHours: "直近{n}時間"
 recentNDays: "直近{n}日"
 noEmailServerWarning: "メールサーバーの設定がされていません。"
 thereIsUnresolvedAbuseReportWarning: "未対応の通報があります。"
-pendingUserApprovals: "承認待ちのユーザーがいる。"
+pendingUserApprovals: "承認待ちのユーザーがいます。"
 recommended: "推奨"
 check: "チェック"
 driveCapOverrideLabel: "このユーザーのドライブ容量上限を変更"
@@ -985,11 +998,11 @@ document: "ドキュメント"
 numberOfPageCache: "ページキャッシュ数"
 numberOfPageCacheDescription: "多くすると利便性が向上しますが、負荷とメモリ使用量が増えます。"
 numberOfReplies: "スレッド内の返信数"
-numberOfRepliesDescription: "この数値を大きくすると、より多くの返信が表示されます。この値を大きくしすぎると、返信が窮屈になり、読めなくなることがあります。"
+numberOfRepliesDescription: "この数値を大きくすると、より多くの返信が表示されます。この値を大きくしすぎると、UIが窮屈になって読みにくくなることがあります。"
 boostSettings: "ブースト設定"
-showVisibilitySelectorOnBoost: "可視性セレクタを表示"
-showVisibilitySelectorOnBoostDescription: "無効の場合、以下で定義されるデフォルトの可視性が使用され、セレクタは表示されません。"
-visibilityOnBoost: "デフォルトのブースト可視性の設定"
+showVisibilitySelectorOnBoost: "公開範囲セレクターを表示"
+showVisibilitySelectorOnBoostDescription: "無効の場合、以下で設定したデフォルトの公開範囲が使用され、セレクターは表示されません。"
+visibilityOnBoost: "デフォルトのブースト公開範囲"
 logoutConfirm: "ログアウトしますか?"
 lastActiveDate: "最終利用日時"
 statusbar: "ステータスバー"
@@ -1071,12 +1084,12 @@ thisPostMayBeAnnoyingCancel: "やめる"
 thisPostMayBeAnnoyingIgnore: "このまま投稿"
 thisPostIsMissingAltTextCancel: "やめる"
 thisPostIsMissingAltTextIgnore: "このまま投稿"
-thisPostIsMissingAltText: "この投稿に添付されたファイルの 1 つに代替テキストがありません。すべての添付ファイルに代替テキストが含まれていることを確認してください。"
+thisPostIsMissingAltText: "代替テキストがないファイルが添付されています。すべての添付ファイルに代替テキストを含むようにしてください。"
 collapseRenotes: "ブーストのスマート省略"
 collapseRenotesDescription: "リアクションやブーストをしたことがあるノートをたたんで表示します。"
-collapseNotesRepliedTo: "返信されたノート省略"
+collapseNotesRepliedTo: "返信元のノートを折りたたむ"
 collapseFiles: "ファイルを折りたたむ"
-autoloadConversation: "返信に会話を読み込む"
+autoloadConversation: "会話スレッドを自動で読み込む"
 internalServerError: "サーバー内部エラー"
 internalServerErrorDescription: "サーバー内部で予期しないエラーが発生しました。"
 copyErrorInfo: "エラー情報をコピー"
@@ -1087,7 +1100,7 @@ disableFederationConfirm: "連合なしにしますか?"
 disableFederationConfirmWarn: "連合なしにしても投稿は非公開になりません。ほとんどの場合、連合なしにする必要はありません。"
 disableFederationOk: "連合なしにする"
 invitationRequiredToRegister: "現在このサーバーは招待制です。招待コードをお持ちの方のみ登録できます。"
-approvalRequiredToRegister: "このインスタンスは、登録理由を指定したユーザーのみを受け入れています。"
+approvalRequiredToRegister: "現在このサーバーは承認制です。参加したい理由を記入し、承認された方のみ登録できます。"
 emailNotSupported: "このサーバーではメール配信はサポートされていません"
 postToTheChannel: "チャンネルに投稿"
 cannotBeChangedLater: "後から変更できません。"
@@ -1130,7 +1143,7 @@ accountMoved: "このユーザーは新しいアカウントに移行しまし
 accountMovedShort: "このアカウントは移行されています"
 operationForbidden: "この操作はできません"
 forceShowAds: "常に広告を表示する"
-oneko: "猫友達 :3"
+oneko: "にゃんこフレンド :3"
 addMemo: "メモを追加"
 editMemo: "メモを編集"
 reactionsList: "リアクション一覧"
@@ -1152,6 +1165,8 @@ preservedUsernames: "予約ユーザー名"
 preservedUsernamesDescription: "予約するユーザー名を改行で列挙します。ここで指定されたユーザー名はアカウント作成時に使えなくなりますが、管理者によるアカウント作成時はこの制限を受けません。また、既に存在するアカウントも影響を受けません。"
 createNoteFromTheFile: "このファイルからノートを作成"
 archive: "アーカイブ"
+archived: "アーカイブ済み"
+unarchive: "アーカイブ解除"
 channelArchiveConfirmTitle: "{name}をアーカイブしますか?"
 channelArchiveConfirmDescription: "アーカイブすると、チャンネル一覧や検索結果に表示されなくなり、新たな書き込みもできなくなります。"
 thisChannelArchived: "このチャンネルはアーカイブされています。"
@@ -1162,6 +1177,9 @@ preventAiLearning: "生成AIによる学習を拒否"
 preventAiLearningDescription: "外部の文章生成AIや画像生成AIに対して、投稿したノートや画像などのコンテンツを学習の対象にしないように要求します。これはnoaiフラグをHTMLレスポンスに含めることによって実現されますが、この要求に従うかはそのAI次第であるため、学習を完全に防止するものではありません。"
 options: "オプション"
 specifyUser: "ユーザー指定"
+lookupConfirm: "照会しますか?"
+openTagPageConfirm: "ハッシュタグのページを開きますか?"
+specifyHost: "ホスト指定"
 failedToPreviewUrl: "プレビューできません"
 update: "更新"
 rolesThatCanBeUsedThisEmojiAsReaction: "リアクションとして使えるロール"
@@ -1238,7 +1256,7 @@ externalServices: "外部サービス"
 sourceCode: "ソースコード"
 sourceCodeIsNotYetProvided: "ソースコードはまだ提供されていません。この問題の修正について管理者に問い合わせてください。"
 repositoryUrl: "リポジトリURL"
-repositoryUrlDescription: "ソースコードが公開されているリポジトリがある場合、そのURLを記入します。Misskeyを現状のまま(ソースコードにいかなる変更も加えずに)使用している場合は https://github.com/misskey-dev/misskey と記入します。"
+repositoryUrlDescription: "ソースコードが公開されているリポジトリがある場合、そのURLを記入します。Sharkeyを現状のまま(ソースコードにいかなる変更も加えずに)使用している場合は https://activitypub.software/TransFem-org/Sharkey/ と記入します。"
 repositoryUrlOrTarballRequired: "リポジトリを公開していない場合、代わりにtarballを提供する必要があります。詳細は.config/example.ymlを参照してください。"
 feedback: "フィードバック"
 feedbackUrl: "フィードバックURL"
@@ -1301,6 +1319,11 @@ keepOriginalFilenameDescription: "この設定をオフにすると、アップ
 noDescription: "説明文はありません"
 alwaysConfirmFollow: "フォローの際常に確認する"
 inquiry: "お問い合わせ"
+tryAgain: "もう一度お試しください。"
+confirmWhenRevealingSensitiveMedia: "センシティブなメディアを表示するとき確認する"
+sensitiveMediaRevealConfirm: "センシティブなメディアです。表示しますか?"
+createdLists: "作成したリスト"
+createdAntennas: "作成したアンテナ"
 
 _delivery:
   status: "配信状態"
@@ -1766,6 +1789,7 @@ _role:
     canManageAvatarDecorations: "アバターデコレーションの管理"
     driveCapacity: "ドライブ容量"
     alwaysMarkNsfw: "ファイルにNSFWを常に付与"
+    canUpdateBioMedia: "アイコンとバナーの更新を許可"
     pinMax: "ノートのピン留めの最大数"
     antennaMax: "アンテナの作成可能数"
     wordMuteMax: "ワードミュートの最大文字数"
@@ -2038,8 +2062,6 @@ _sfx:
   note: "ノート"
   noteMy: "ノート(自分)"
   notification: "通知"
-  antenna: "アンテナ受信"
-  channel: "チャンネル通知"
   reaction: "リアクション選択時"
 
 _soundSettings:
@@ -2048,7 +2070,8 @@ _soundSettings:
   driveFileTypeWarn: "このファイルは対応していません"
   driveFileTypeWarnDescription: "音声ファイルを選択してください"
   driveFileDurationWarn: "音声が長すぎます"
-  driveFileDurationWarnDescription: "長い音声を使用するとMisskeyの使用に支障をきたす可能性があります。それでも続行しますか?"
+  driveFileDurationWarnDescription: "長い音声を使用するとSharkeyの使用に支障をきたす可能性があります。それでも続行しますか?"
+  driveFileError: "音声が読み込めませんでした。設定を変更してください"
 
 _ago:
   future: "未来"
@@ -2372,6 +2395,7 @@ _timelines:
   local: "ローカル"
   social: "ソーシャル"
   global: "グローバル"
+  bubble: "バッッブル"
 
 _play:
   new: "Playの作成"
@@ -2424,6 +2448,7 @@ _pages:
   eyeCatchingImageSet: "アイキャッチ画像を設定"
   eyeCatchingImageRemove: "アイキャッチ画像を削除"
   chooseBlock: "ブロックを追加"
+  enterSectionTitle: "セクションタイトルを入力"
   selectType: "種類を選択"
   contentBlocks: "コンテンツ"
   inputBlocks: "入力"
@@ -2489,11 +2514,12 @@ _notification:
     roleAssigned: "ロールが付与された"
     achievementEarned: "実績の獲得"
     app: "連携アプリからの通知"
+    edited: "編集済み"
 
   _actions:
     followBack: "フォローバック"
     reply: "返信"
-    renote: "Boost"
+    renote: "ブースト"
 
 _deck:
   alwaysShowMainColumn: "常にメインカラムを表示"
@@ -2543,9 +2569,10 @@ _drivecleaner:
 
 _webhookSettings:
   createWebhook: "Webhookを作成"
+  modifyWebhook: "Webhookを編集"
   name: "名前"
   secret: "シークレット"
-  events: "Webhookを実行するタイミング"
+  trigger: "トリガー"
   active: "有効"
   _events:
     follow: "フォローしたとき"
@@ -2555,6 +2582,27 @@ _webhookSettings:
     renote: "Boostされたとき"
     reaction: "リアクションがあったとき"
     mention: "メンションされたとき"
+  _systemEvents:
+    abuseReport: "ユーザーから通報があったとき"
+    abuseReportResolved: "ユーザーからの通報を処理したとき"
+    userCreated: "ユーザーが作成されたとき"
+  deleteConfirm: "Webhookを削除しますか?"
+
+_abuseReport:
+  _notificationRecipient:
+    createRecipient: "通報の通知先を追加"
+    modifyRecipient: "通報の通知先を編集"
+    recipientType: "通知先の種類"
+    _recipientType:
+      mail: "メール"
+      webhook: "Webhook"
+      _captions:
+        mail: "モデレーター権限を持つユーザーのメールアドレスに通知を送ります(通報を受けた時のみ)"
+        webhook: "指定したSystemWebhookに通知を送ります(通報を受けた時と通報を解決した時にそれぞれ発信)"
+    keywords: "キーワード"
+    notifiedUser: "通知先ユーザー"
+    notifiedWebhook: "使用するWebhook"
+    deleteConfirm: "通知先を削除しますか?"
 
 _moderationLogTypes:
   createRole: "ロールを作成"
@@ -2594,6 +2642,97 @@ _moderationLogTypes:
   deleteAvatarDecoration: "アイコンデコレーションを削除"
   unsetUserAvatar: "ユーザーのアイコンを解除"
   unsetUserBanner: "ユーザーのバナーを解除"
+  createSystemWebhook: "SystemWebhookを作成"
+  updateSystemWebhook: "SystemWebhookを更新"
+  deleteSystemWebhook: "SystemWebhookを削除"
+  createAbuseReportNotificationRecipient: "通報の通知先を作成"
+  updateAbuseReportNotificationRecipient: "通報の通知先を更新"
+  deleteAbuseReportNotificationRecipient: "通報の通知先を削除"
+  deleteAccount: "アカウントを削除"
+  deletePage: "ページを削除"
+  deleteFlash: "Playを削除"
+  deleteGalleryPost: "ギャラリーの投稿を削除"
+
+_mfm:
+  uncommonFeature: "この機能は一般的に普及していないため、他のMisskeyフォークを含めた多くのFediverseソフトウェアで表示できないことがあります。"
+  intro: "MFM はMisskey, Sharkey, Firefish, Akkomaなど、多くの場所で使用できるマークアップ言語です。ここでは、利用できるMFM構文の一覧をご覧いただけます。"
+  dummy: "SharkeyでFediverseの世界が広がります"
+  mention: "メンション"
+  mentionDescription: "アットマーク + ユーザー名で、特定のユーザーを示すことができます。"
+  hashtag: "ハッシュタグ"
+  hashtagDescription: "ナンバーサイン + タグで、ハッシュタグを示すことができます。"
+  url: "URL"
+  urlDescription: "URLを示すことができます。"
+  link: "リンク"
+  linkDescription: "文章の特定の範囲を、URLに紐づけることができます。"
+  bold: "太字"
+  boldDescription: "文字を太く表示して強調することができます。"
+  small: "小文字"
+  smallDescription: "内容を小さく・薄く表示させることができます。"
+  center: "中央寄せ"
+  centerDescription: "内容を中央寄せで表示させることができます。"
+  inlineCode: "コード(インライン)"
+  inlineCodeDescription: "プログラムなどのコードをインラインでシンタックスハイライトします。"
+  blockCode: "コード(ブロック)"
+  blockCodeDescription: "複数行のプログラムなどのコードをブロックでシンタックスハイライトします。"
+  inlineMath: "数式(インライン)"
+  inlineMathDescription: "数式 (KaTeX形式)をインラインで表示します。"
+  blockMath: "数式(ブロック)"
+  blockMathDescription: "数式 (KaTeX形式)をブロックで表示します。"
+  quote: "引用"
+  quoteDescription: "内容が引用であることを示すことができます。"
+  emoji: "カスタム絵文字"
+  emojiDescription: "コロンでカスタム絵文字名を囲むと、カスタム絵文字を表示させることができます。"
+  search: "検索"
+  searchDescription: "検索ボックスを表示できます。"
+  flip: "反転"
+  flipDescription: "内容を上下または左右に反転させます。"
+  jelly: "アニメーション(びよんびよん)"
+  jellyDescription: "ゼリーが揺れるような感じのアニメーションをさせます。"
+  tada: "アニメーション(じゃーん)"
+  tadaDescription: "「じゃーん!」と強調するような感じのアニメーションをさせます。"
+  jump: "アニメーション(ジャンプ)"
+  jumpDescription: "跳ねるアニメーションをさせます。"
+  bounce: "アニメーション(バウンド)"
+  bounceDescription: "跳ねて着地するようなアニメーションをさせます。"
+  shake: "アニメーション(ぶるぶる)"
+  shakeDescription: "震えるアニメーションをさせます。"
+  twitch: "アニメーション(ガタガタ)"
+  twitchDescription: "より激しく震えるアニメーションをさせます。"
+  spin: "アニメーション(回転)"
+  spinDescription: "内容を回転させます。"
+  x2: "大"
+  x2Description: "内容を大きく表示させます。"
+  x3: "特大"
+  x3Description: "内容をより大きく表示させます。"
+  x4: "超特大"
+  x4Description: "内容をさらに大きく表示させます。"
+  blur: "ぼかし"
+  blurDescription: "内容をぼかすことができます。ポインターを上に乗せるとはっきり見えるようになります。"
+  font: "フォント"
+  fontDescription: "内容のフォントを指定することができます。"
+  rainbow: "レインボー"
+  rainbowDescription: "内容を虹色で表示させます。"
+  sparkle: "キラキラ"
+  sparkleDescription: "キラキラと星型のパーティクルを表示させます。"
+  rotate: "角度変更"
+  rotateDescription: "指定した角度で回転させます。"
+  position: "位置変更"
+  positionDescription: "位置をずらすことができます。"
+  crop: "切り取り"
+  cropDescription: "内容を切り抜きます。"
+  followMouse: "マウス追従"
+  followMouseDescription: "内容がマウスに追従します。スマホの場合はタップした場所に追従します。"
+  scale: "拡大"
+  scaleDescription: "内容を引き伸ばして表示します。"
+  foreground: "文字色"
+  foregroundDescription: "文字色を変更します。"
+  fade: 'フェード'
+  fadeDescription: '内容をフェードイン・フェードアウトさせます。'
+  background: "背景色"
+  backgroundDescription: "背景色を変更します。"
+  plain: "Plain"
+  plainDescription: "内側の構文を全て無効にします。"
 
 _fileViewer:
   title: "ファイルの詳細"
@@ -2650,27 +2789,27 @@ _animatedMFM:
   play: "MFMアニメーションを再生"
   stop: "MFMアニメーション停止"
   _alert:
-    text: "MFMアニメーションには、点滅するライトや高速で動くテキスト/絵文字を含まれる場合があります。"
+    text: "MFMアニメーションには、高速で点滅したり動いたりするテキスト・絵文字を含む場合があります。"
     confirm: "再生する"
 
 _dataRequest:
   title: "データリクエスト"
   warn: "データリクエストは3日ごとに可能です。"
-  text: "データの保存が完了すると、このアカウントに登録されているEメールアドレスにメールが送信されます。"
-  button: "リクエスト"
+  text: "データの保存が完了すると、このアカウントに登録されているメールアドレスにメールが送信されます。"
+  button: "データリクエスト実行"
 
 _dataSaver:
   _media:
-    title: "メディアの読み込み"
+    title: "メディアの読み込みを無効化"
     description: "画像・動画が自動で読み込まれるのを防止します。隠れている画像・動画はタップすると読み込まれます。"
   _avatar:
-    title: "アイコン画像"
+    title: "アイコン画像のアニメーションを無効化"
     description: "アイコン画像のアニメーションが停止します。アニメーション画像は通常の画像よりファイルサイズが大きいことがあるので、データ通信量をさらに削減できます。"
   _urlPreview:
-    title: "URLプレビューのサムネイル"
+    title: "URLプレビューのサムネイルを非表示"
     description: "URLプレビューのサムネイル画像が読み込まれなくなります。"
   _code:
-    title: "コードハイライト"
+    title: "コードハイライトを非表示"
     description: "MFMなどでコードハイライト記法が使われている場合、タップするまで読み込まれなくなります。コードハイライトではハイライトする言語ごとにその定義ファイルを読み込む必要がありますが、それらが自動で読み込まれなくなるため、通信量の削減が見込めます。"
 
 _hemisphere:
@@ -2746,3 +2885,9 @@ _mediaControls:
   pip: "ピクチャインピクチャ"
   playbackRate: "再生速度"
   loop: "ループ再生"
+
+_contextMenu:
+  title: "コンテキストメニュー"
+  app: "アプリケーション"
+  appWithShift: "Shiftキーでアプリケーション"
+  native: "ブラウザのUI"
diff --git a/locales/ja-KS.yml b/locales/ja-KS.yml
index 641425aa17..448355eb4e 100644
--- a/locales/ja-KS.yml
+++ b/locales/ja-KS.yml
@@ -60,6 +60,7 @@ copyFileId: "ファイルIDをコピー"
 copyFolderId: "フォルダーIDをコピー"
 copyProfileUrl: "プロフィールURLをコピー"
 searchUser: "ユーザーを探す"
+searchThisUsersNotes: "ユーザーのノートを検索"
 reply: "返事"
 loadMore: "まだまだあるで!"
 showMore: "まだまだあるで!"
@@ -105,14 +106,17 @@ followRequests: "フォロー申請"
 unfollow: "フォローやめる"
 followRequestPending: "フォロー許してくれるん待っとる"
 enterEmoji: "絵文字を入れてや"
-renote: "ブースト"
-unrenote: "ブーストやめる"
-renoted: "ブーストしたで。"
-cantRenote: "この投稿はブーストできへんっぽい。"
-cantReRenote: "ブースト自体はブーストできへんで。"
+renote: "リノート"
+unrenote: "リノートやめる"
+renoted: "リノートしたで。"
+renotedToX: "{name}にリノートしたで"
+cantRenote: "この投稿はリノートできへんっぽい。"
+cantReRenote: "リノート自体はリノートできへんで。"
 quote: "引用"
 inChannelRenote: "チャンネルの中でブースト"
 inChannelQuote: "チャンネル内引用"
+renoteToChannel: "チャンネルにリノート"
+renoteToOtherChannel: "他のチャンネルにリノート"
 pinnedNote: "ピン留めされとるノート"
 pinned: "ピン留めしとく"
 you: "あんた"
@@ -151,6 +155,7 @@ editList: "リストいじる"
 selectChannel: "チャンネルを選ぶ"
 selectAntenna: "アンテナを選ぶ"
 editAntenna: "アンテナいじる"
+createAntenna: "アンテナを作成"
 selectWidget: "ウィジェットを選ぶ"
 editWidgets: "ウィジェットをいじる"
 editWidgetsExit: "いじるのをやめる"
@@ -179,6 +184,10 @@ addAccount: "アカウントを追加"
 reloadAccountsList: "アカウントリストの情報を更新"
 loginFailed: "ログインに失敗してもうた…"
 showOnRemote: "リモートで見る"
+continueOnRemote: "リモートで続行"
+chooseServerOnMisskeyHub: "Misskey Hubからサーバーを選択"
+specifyServerHost: "サーバーのドメインを直接指定"
+inputHostName: "ドメインを入力せえや"
 general: "全般"
 wallpaper: "壁紙"
 setWallpaper: "壁紙を設定"
@@ -189,6 +198,7 @@ followConfirm: "{name}をフォローしてええか?"
 proxyAccount: "プロキシアカウント"
 proxyAccountDescription: "プロキシアカウントは、代わりにフォローしてくれるアカウントや。例えば、551に豚まんが無いときやったり、ユーザーがリモートユーザーをアカウントに入れたとき、リストに入れられたユーザーが誰からもフォローされてないと寂しいやん。寂しいし、アクティビティも配達されへんから、プロキシアカウントがフォローしてくれるで。ええやつやん…"
 host: "ホスト"
+selectSelf: "自分を選択"
 selectUser: "ユーザーを選ぶ"
 recipient: "宛先"
 annotation: "注釈"
@@ -204,6 +214,7 @@ perDay: "1日ごと"
 stopActivityDelivery: "アクティビティの配送をやめる"
 blockThisInstance: "このサーバーをブロックすんで"
 silenceThisInstance: "サーバーサイレンスすんで?"
+mediaSilenceThisInstance: "サーバーをメディアサイレンス"
 operations: "操作"
 software: "ソフトウェア"
 version: "バージョン"
@@ -225,6 +236,8 @@ blockedInstances: "ブロックしたサーバー"
 blockedInstancesDescription: "ブロックしたいサーバーのホストを改行で区切って設定してな。ブロックされてもうたサーバーとはもう金輪際やり取りできひんくなるで。"
 silencedInstances: "サーバーサイレンスされてんねん"
 silencedInstancesDescription: "サイレンスしたいサーバーのホストを改行で区切って設定すんで。サイレンスされたサーバーに所属するアカウントはすべて「サイレンス」として扱われ、フォローがすべてリクエストになり、フォロワーでないローカルアカウントにはメンションできなくなんねん。ブロックしたインスタンスには影響せーへんで。"
+mediaSilencedInstances: "メディアサイレンスしたサーバー"
+mediaSilencedInstancesDescription: "メディアサイレンスしたいサーバーのホストを改行で区切って設定するで。メディアサイレンスされたサーバーに所属するアカウントによるファイルはすべてセンシティブとして扱われてな、カスタム絵文字が使えへんようになるで。ブロックしたインスタンスには影響せえへんで。"
 muteAndBlock: "ミュートとブロック"
 mutedUsers: "ミュートしとるユーザー"
 blockedUsers: "ブロックしとるユーザー"
@@ -315,6 +328,7 @@ selectFile: "ファイル選んでや"
 selectFiles: "ファイル選んでや"
 selectFolder: "フォルダ選んでや"
 selectFolders: "フォルダ選んでや"
+fileNotSelected: "ファイルが選択されてへんで"
 renameFile: "ファイル名をいらう"
 folderName: "フォルダー名"
 createFolder: "フォルダー作る"
@@ -470,10 +484,12 @@ retype: "もっかい入力"
 noteOf: "{user}はんのノート"
 quoteAttached: "引用付いとるで"
 quoteQuestion: "引用として添付してもええか?"
+attachAsFileQuestion: "クリップボードのテキストが長すぎるからテキストファイルとして添付してもええか?"
 noMessagesYet: "まだチャットはあらへんで"
 newMessageExists: "新しいメッセージがきたで"
 onlyOneFileCanBeAttached: "ごめんな、メッセージに添付できるファイルはひとつだけなんよ。"
 signinRequired: "ログインしてくれへん?"
+signinOrContinueOnRemote: "続行するには、お使いのサーバーに移動するか、このサーバーに登録・ログインする必要があるで"
 invitations: "来てや"
 invitationCode: "招待コード"
 checking: "確認しとるで"
@@ -834,6 +850,7 @@ administration: "管理"
 accounts: "アカウント"
 switch: "切り替え"
 noMaintainerInformationWarning: "管理者情報が設定されてへんで"
+noInquiryUrlWarning: "問い合わせ先URLが設定されてへんで。"
 noBotProtectionWarning: "Botプロテクションが設定されてへんで。"
 configure: "設定する"
 postToGallery: "ギャラリーへ投稿"
@@ -1023,6 +1040,7 @@ thisPostMayBeAnnoyingHome: "ホームに投稿"
 thisPostMayBeAnnoyingCancel: "やめとく"
 thisPostMayBeAnnoyingIgnore: "このまま投稿"
 collapseRenotes: "見たことあるブーストは飛ばして表示するで"
+collapseRenotesDescription: "リアクションやブーストをしたことがあるノートをたたんで表示するで。"
 internalServerError: "サーバー内部エラー"
 internalServerErrorDescription: "サーバーでなんか変なこと起こっとるわ。"
 copyErrorInfo: "エラー情報をコピるで"
@@ -1096,6 +1114,8 @@ preservedUsernames: "予約ユーザー名"
 preservedUsernamesDescription: "予約しとくユーザー名を行ごとに挙げるで。ここで指定されたユーザー名はアカウント作るときに使えへんくなるけど、管理者は例外や。あと、もうあるアカウントも例外やな。"
 createNoteFromTheFile: "このファイル使うてノート作るで"
 archive: "アーカイブ"
+archived: "アーカイブ済み"
+unarchive: "アーカイブ解除"
 channelArchiveConfirmTitle: "{name}をアーカイブしてええか?"
 channelArchiveConfirmDescription: "アーカイブしたら、チャンネル一覧とか検索結果からなくなるし、新しく書き込みもできへんなるで。"
 thisChannelArchived: "このチャンネル、アーカイブされとるで。"
@@ -1106,6 +1126,9 @@ preventAiLearning: "生成AIの学習に使わんといて"
 preventAiLearningDescription: "他の文章生成AIとか画像生成AIに、投稿したノートとか画像なんかを勝手に使わんように頼むで。具体的にはnoaiフラグをHTMLレスポンスに含めるんやけど、これ聞いてくれるんはAIの気分次第やから、使われる可能性もちょっとはあるな。"
 options: "オプション"
 specifyUser: "ユーザー指定"
+lookupConfirm: "照会するけどええか?"
+openTagPageConfirm: "ハッシュタグのページを開くんか?"
+specifyHost: "ホスト指定"
 failedToPreviewUrl: "プレビューできへん"
 update: "更新"
 rolesThatCanBeUsedThisEmojiAsReaction: "ツッコミとして使えるロール"
@@ -1237,10 +1260,20 @@ keepOriginalFilenameDescription: "この設定をオフにすると、アップ
 noDescription: "説明文はあらへんで"
 alwaysConfirmFollow: "フォローの際常に確認する"
 inquiry: "問い合わせ"
+tryAgain: "もう一度試しいや。"
+confirmWhenRevealingSensitiveMedia: "センシティブなメディアを表示するとき確認する"
+sensitiveMediaRevealConfirm: "センシティブなメディアやで。表示するんか?"
+createdLists: "作成したリスト"
+createdAntennas: "作成したアンテナ"
 _delivery:
+  status: "配信状態"
   stop: "配信せぇへん"
+  resume: "配信再開"
   _type:
     none: "配信しとる"
+    manuallySuspended: "手動停止中"
+    goneSuspended: "サーバー削除のため停止中"
+    autoSuspendedForNotResponding: "サーバー応答せえへんから停止中"
 _bubbleGame:
   howToPlay: "遊び方"
   hold: "ホールド"
@@ -1366,6 +1399,8 @@ _serverSettings:
   fanoutTimelineDescription: "入れると、おのおのタイムラインを取得するときにめちゃめちゃ動きが良うなって、データベースが軽くなるわ。でも、Redisのメモリ使う量が増えるから注意な。サーバーのメモリが足りんときとか、動きが変なときは切れるで。"
   fanoutTimelineDbFallback: "データベースにフォールバックする"
   fanoutTimelineDbFallbackDescription: "有効にしたら、タイムラインがキャッシュん中に入ってないときにDBにもっかい問い合わせるフォールバック処理ってのをやっとくで。切ったらフォールバック処理をやらんからサーバーはもっと軽くなんねんけど、タイムラインの取得範囲がちょっと減るで。"
+  inquiryUrl: "問い合わせ先URL"
+  inquiryUrlDescription: "サーバー運営者へのお問い合わせフォームのURLや、運営者の連絡先等が記載されたWebページのURLを指定するで。"
 _accountMigration:
   moveFrom: "別のアカウントからこのアカウントに引っ越す"
   moveFromSub: "別のアカウントへエイリアスを作る"
@@ -1682,6 +1717,7 @@ _role:
     canManageAvatarDecorations: "アバターを飾るモンの管理"
     driveCapacity: "ドライブ容量"
     alwaysMarkNsfw: "勝手にファイルにNSFWをくっつける"
+    canUpdateBioMedia: "アイコンとバナーの更新を許可"
     pinMax: "ノートピン留めできる数"
     antennaMax: "アンテナ作れる数"
     wordMuteMax: "ワードミュートの最大文字数"
@@ -1925,8 +1961,6 @@ _sfx:
   note: "ノート"
   noteMy: "ノート(自分)"
   notification: "通知"
-  antenna: "アンテナ受信"
-  channel: "チャンネル通知"
   reaction: "ツッコミ選んどるとき"
 _soundSettings:
   driveFile: "ドライブん中の音使う"
@@ -1935,6 +1969,7 @@ _soundSettings:
   driveFileTypeWarnDescription: "音声ファイルを選びや"
   driveFileDurationWarn: "音が長すぎるわ"
   driveFileDurationWarnDescription: "長い音使うたらSharkey使うのに良うないかもしれへんで。それでもええか?"
+  driveFileError: "音声が読み込めへんかったで。設定を変更せえや"
 _ago:
   future: "未来"
   justNow: "ついさっき"
@@ -2351,6 +2386,7 @@ _deck:
   alwaysShowMainColumn: "いつもメインカラムを表示"
   columnAlign: "カラムの寄せ"
   addColumn: "カラムを追加"
+  newNoteNotificationSettings: "新着ノート通知の設定"
   configureColumn: "カラムの設定"
   swapLeft: "左に移動"
   swapRight: "右に移動"
@@ -2389,9 +2425,10 @@ _drivecleaner:
   orderByCreatedAtAsc: "追加日の古い順"
 _webhookSettings:
   createWebhook: "Webhookをつくる"
+  modifyWebhook: "Webhookを編集"
   name: "名前"
   secret: "シークレット"
-  events: "Webhookを投げるタイミング"
+  trigger: "トリガー"
   active: "有効"
   _events:
     follow: "フォローしたとき~!"
@@ -2401,6 +2438,26 @@ _webhookSettings:
     renote: "ブーストされるとき~!"
     reaction: "ツッコまれたとき~!"
     mention: "メンションがあるとき~!"
+  _systemEvents:
+    abuseReport: "ユーザーから通報があったとき"
+    abuseReportResolved: "ユーザーからの通報を処理したとき"
+    userCreated: "ユーザーが作成されたとき"
+  deleteConfirm: "ほんまにWebhookをほかしてもええんか?"
+_abuseReport:
+  _notificationRecipient:
+    createRecipient: "通報の通知先を追加"
+    modifyRecipient: "通報の通知先を編集"
+    recipientType: "通知先の種類"
+    _recipientType:
+      mail: "メール"
+      webhook: "Webhook"
+      _captions:
+        mail: "モデレーター権限を持つユーザーのメアドに通知を送るで(通報を受けた時のみ)"
+        webhook: "指定したSystemWebhookに通知を送るで(通報を受けた時と通報を解決した時にそれぞれ発信)"
+    keywords: "キーワード"
+    notifiedUser: "通知先ユーザー"
+    notifiedWebhook: "使用するWebhook"
+    deleteConfirm: "通知先を削除してもええか?"
 _moderationLogTypes:
   createRole: "ロールを追加すんで"
   deleteRole: "ロールほかす"
@@ -2438,6 +2495,8 @@ _moderationLogTypes:
   deleteAvatarDecoration: "アイコンデコレーションを削除"
   unsetUserAvatar: "この子のアイコン元に戻す"
   unsetUserBanner: "この子のバナー元に戻す"
+  createSystemWebhook: "SystemWebhookを作成"
+  updateSystemWebhook: "SystemWebhookを更新"
 _fileViewer:
   title: "ファイルの詳しい情報"
   type: "ファイルの種類"
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/ko-GS.yml b/locales/ko-GS.yml
index 9466aff01f..9323ed2a26 100644
--- a/locales/ko-GS.yml
+++ b/locales/ko-GS.yml
@@ -805,6 +805,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 69e1216ce4..64bae5a9d7 100644
--- a/locales/ko-KR.yml
+++ b/locales/ko-KR.yml
@@ -52,14 +52,14 @@ deleteAndEditConfirm: "이 노트를 삭제한 뒤 다시 편집하시겠습니
 addToList: "리스트에 추가"
 addToAntenna: "안테나에 추가"
 sendMessage: "메시지 보내기"
-copyRSS: "RSS 복사"
-copyUsername: "사용자 이름 복사"
-copyUserId: "사용자 ID 복사"
+copyRSS: "RSS 주소 복사"
+copyUsername: "유저명 복사"
+copyUserId: "유저 ID 복사"
 copyNoteId: "노트 ID 복사"
 copyFileId: "파일 ID 복사"
 copyFolderId: "폴더 ID 복사"
 copyProfileUrl: "프로필 URL 복사"
-searchUser: "사용자 검색"
+searchUser: "유저 검색"
 reply: "답글"
 loadMore: "더 보기"
 showMore: "더 보기"
@@ -108,22 +108,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 +139,7 @@ unmarkAsSensitive: "열람주의 해제"
 enterFileName: "파일명을 입력"
 mute: "뮤트"
 unmute: "뮤트 해제"
-renoteMute: "리노트 뮤트하기"
+renoteMute: "리노트 뮤트"
 renoteUnmute: "리노트 뮤트 해제"
 block: "차단"
 unblock: "차단 해제"
@@ -174,12 +177,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 +194,7 @@ followConfirm: "{name}님을 팔로우 하시겠습니까?"
 proxyAccount: "프록시 계정"
 proxyAccountDescription: "프록시 계정은 특정 조건 하에서 유저의 리모트 팔로우를 대행하는 계정입니다. 예를 들면, 유저가 리모트 유저를 리스트에 넣었을 때, 리스트에 들어간 유저를 아무도 팔로우한 적이 없다면 액티비티가 서버로 배달되지 않기 때문에, 대신 프록시 계정이 해당 유저를 팔로우하도록 합니다."
 host: "호스트"
-selectUser: "사용자 선택"
+selectUser: "유저 선택"
 recipient: "수신인"
 annotation: "내용에 대한 주석"
 federation: "연합"
@@ -230,7 +237,7 @@ noUsers: "아무도 없습니다"
 editProfile: "프로필 수정"
 noteDeleteConfirm: "이 노트를 삭제하시겠습니까?"
 pinLimitExceeded: "더 이상 고정할 수 없습니다."
-intro: "Misskey의 설치를 완료했습니다! 관리자 계정을 만들어 주세요."
+intro: "Misskey의 설치가 완료되었습니다! 관리자 계정을 생성해주세요."
 done: "완료"
 processing: "처리중"
 preview: "미리보기"
@@ -247,7 +254,7 @@ publishing: "배포 중"
 notResponding: "응답 없음"
 instanceFollowing: "서버의 팔로잉"
 instanceFollowers: "서버의 팔로워"
-instanceUsers: "서버의 유저"
+instanceUsers: "서버의 사용자"
 changePassword: "비밀번호 변경"
 security: "보안"
 retypedNotMatch: "입력이 일치하지 않습니다."
@@ -263,12 +270,12 @@ lookup: "찾아보기"
 announcements: "공지사항"
 imageUrl: "이미지 URL"
 remove: "삭제"
-removed: "삭제하였습니다"
+removed: "삭제했습니다"
 removeAreYouSure: "\"{x}\" 을(를) 삭제하시겠습니까?"
 deleteAreYouSure: "\"{x}\" 을(를) 삭제하시겠습니까?"
 resetAreYouSure: "초기화 하시겠습니까?"
 areYouSure: "계속 진행하시겠습니까?"
-saved: "저장하였습니다"
+saved: "저장했습니다"
 messaging: "대화"
 upload: "업로드"
 keepOriginalUploading: "원본 이미지를 유지"
@@ -296,7 +303,7 @@ activity: "활동"
 images: "이미지"
 image: "이미지"
 birthday: "생일"
-yearsOld: "만 {age} 세"
+yearsOld: "{age}세"
 registeredDate: "등록일"
 location: "장소"
 theme: "테마"
@@ -313,6 +320,7 @@ selectFile: "파일 선택"
 selectFiles: "파일 선택"
 selectFolder: "폴더 선택"
 selectFolders: "폴더 선택"
+fileNotSelected: "파일을 선택하지 않았습니다"
 renameFile: "파일 이름 변경"
 folderName: "폴더 이름"
 createFolder: "폴더 만들기"
@@ -370,7 +378,7 @@ inMb: "메가바이트 단위"
 bannerUrl: "배너 이미지 URL"
 backgroundImageUrl: "배경 이미지 URL"
 basicInfo: "기본 정보"
-pinnedUsers: "고정된 유저"
+pinnedUsers: "고정한 사용자"
 pinnedUsersDescription: "\"발견하기\" 페이지 등에 고정하고 싶은 유저를 한 줄에 한 명씩 적습니다."
 pinnedPages: "고정한 페이지"
 pinnedPagesDescription: "서버의 대문에 고정하고 싶은 페이지의 경로를 한 줄에 하나씩 적습니다."
@@ -437,13 +445,13 @@ moderationNote: "조정 기록"
 addModerationNote: "조정 기록 추가하기"
 moderationLogs: "모더레이션 로그"
 nUsersMentioned: "{n}명이 언급함"
-securityKeyAndPasskey: "보안 키 또는 패스 키"
+securityKeyAndPasskey: "보안 키 또는 패스키"
 securityKey: "보안 키"
 lastUsed: "마지막 사용"
 lastUsedAt: "마지막 사용: {t}"
 unregister: "등록 해제"
 passwordLessLogin: "비밀번호 없이 로그인"
-passwordLessLoginDescription: "비밀번호를 사용하지 않고 보안 키 또는 패스 키 등으로만 로그인합니다."
+passwordLessLoginDescription: "비밀번호 없이 보안 키 또는 패스키만 사용해서 로그인합니다."
 resetPassword: "비밀번호 재설정"
 newPasswordIs: "새로운 비밀번호는 \"{password}\" 입니다"
 reduceUiAnimation: "UI의 애니메이션을 줄이기"
@@ -468,10 +476,12 @@ retype: "다시 입력"
 noteOf: "{user}의 노트"
 quoteAttached: "인용함"
 quoteQuestion: "인용해서 작성하시겠습니까?"
+attachAsFileQuestion: "붙여넣으려는 글이 너무 깁니다. 텍스트 파일로 첨부하시겠습니까?"
 noMessagesYet: "아직 대화가 없습니다"
 newMessageExists: "새 메시지가 있습니다"
 onlyOneFileCanBeAttached: "메시지에 첨부할 수 있는 파일은 하나까지입니다"
 signinRequired: "진행하기 전에 로그인을 해 주세요"
+signinOrContinueOnRemote: "계속하려면 사용하는 서버로 이동하거나 이 서버에 로그인해야 합니다."
 invitations: "초대"
 invitationCode: "초대 코드"
 checking: "확인하는 중입니다"
@@ -486,7 +496,7 @@ strongPassword: "강한 비밀번호"
 passwordMatched: "일치합니다"
 passwordNotMatched: "일치하지 않습니다"
 signinWith: "{x}로 로그인"
-signinFailed: "로그인할 수 없습니다. 사용자명과 비밀번호를 확인하여 주십시오."
+signinFailed: "로그인할 수 없습니다. 사용자 이름과 비밀번호를 확인해 주십시오."
 or: "혹은"
 language: "언어"
 uiLanguage: "UI 표시 언어"
@@ -494,7 +504,7 @@ aboutX: "{x}에 대하여"
 emojiStyle: "이모지 스타일"
 native: "기본"
 disableDrawer: "드로어 메뉴를 사용하지 않기"
-showNoteActionsOnlyHover: "노트 액션 버튼을 마우스를 올렸을 때에만 표시"
+showNoteActionsOnlyHover: "마우스가 올라간 때에만 노트 동작 버튼을 표시하기"
 showReactionsCount: "노트의 반응 수를 표시하기"
 noHistory: "기록이 없습니다"
 signinHistory: "로그인 기록"
@@ -559,7 +569,7 @@ popout: "새 창으로 열기"
 volume: "음량"
 masterVolume: "마스터 볼륨"
 notUseSound: "음소거 하기"
-useSoundOnlyWhenActive: "Misskey가 활성화 되어져 있을 때만 소리 출력하기"
+useSoundOnlyWhenActive: "Misskey를 활성화한 때에만 소리를 출력하기"
 details: "자세히"
 chooseEmoji: "이모지 선택"
 unableToProcess: "작업을 완료할 수 없습니다"
@@ -588,7 +598,7 @@ deleteAllFiles: "모든 파일 삭제"
 deleteAllFilesConfirm: "모든 파일을 삭제하시겠습니까?"
 removeAllFollowing: "모든 팔로잉 해제"
 removeAllFollowingDescription: "{host} 서버의 모든 팔로잉을 해제합니다. 해당 서버가 더 이상 존재하지 않는 경우 등에 실행해 주세요."
-userSuspended: "이 계정은 정지된 상태입니다."
+userSuspended: "이 사용자는 정지되었습니다."
 userSilenced: "이 계정은 사일런스된 상태입니다."
 yourAccountSuspendedTitle: "계정이 정지되었습니다"
 yourAccountSuspendedDescription: "이 계정은 서버의 이용 약관을 위반하거나, 기타 다른 이유로 인해 정지되었습니다. 자세한 사항은 관리자에게 문의해 주십시오. 계정을 새로 생성하지 마십시오."
@@ -752,7 +762,7 @@ experimentalFeatures: "실험실"
 experimental: "실험실"
 thisIsExperimentalFeature: "이 기능은 실험적인 기능입니다. 사양이 변경되거나 정상적으로 동작하지 않을 가능성이 있습니다."
 developer: "개발자"
-makeExplorable: "\"발견하기\"에 내 계정 보이기"
+makeExplorable: "계정을 쉽게 발견하도록 하기"
 makeExplorableDescription: "비활성화하면 \"발견하기\"에 나의 계정을 표시하지 않습니다."
 showGapBetweenNotesInTimeline: "타임라인의 노트 사이를 띄워서 표시"
 duplicate: "복제"
@@ -798,7 +808,7 @@ emailNotification: "메일 알림"
 publish: "게시"
 inChannelSearch: "채널에서 검색"
 useReactionPickerForContextMenu: "우클릭하여 리액션 선택기 열기"
-typingUsers: "{users} 님이 입력하고 있어요.."
+typingUsers: "{users}님이 입력 중"
 jumpToSpecifiedDate: "특정 날짜로 이동"
 showingPastTimeline: "과거의 타임라인을 표시하고 있어요"
 clear: "지우기"
@@ -832,6 +842,7 @@ administration: "관리"
 accounts: "계정"
 switch: "전환"
 noMaintainerInformationWarning: "관리자 정보가 설정되어 있지 않습니다."
+noInquiryUrlWarning: "문의처 주소를 설정하지 않았습니다."
 noBotProtectionWarning: "Bot 방어가 설정되어 있지 않습니다."
 configure: "설정하기"
 postToGallery: "갤러리에 업로드"
@@ -1021,6 +1032,7 @@ thisPostMayBeAnnoyingHome: "홈에 게시"
 thisPostMayBeAnnoyingCancel: "그만두기"
 thisPostMayBeAnnoyingIgnore: "이대로 게시"
 collapseRenotes: "이미 본 리노트를 간략화하기"
+collapseRenotesDescription: "반응이나 리노트를 한 노트를 접어서 표시합니다."
 internalServerError: "내부 서버 오류"
 internalServerErrorDescription: "내부 서버에서 예기치 않은 오류가 발생했습니다."
 copyErrorInfo: "오류 정보 복사"
@@ -1090,7 +1102,7 @@ serverRules: "서버 규칙"
 pleaseConfirmBelowBeforeSignup: "이 서버에 가입하기 전에 아래 사항을 확인하여 주십시오."
 pleaseAgreeAllToContinue: "계속하시려면 모든 항목에 동의하십시오."
 continue: "계속"
-preservedUsernames: "예약된 사용자명"
+preservedUsernames: "예약한 사용자 이름"
 preservedUsernamesDescription: "예약할 사용자명을 한 줄에 하나씩 입력합니다. 여기에서 지정한 사용자명으로는 계정을 생성할 수 없게 됩니다. 단, 관리자 권한으로 계정을 생성할 때에는 해당되지 않으며, 이미 존재하는 계정도 영향을 받지 않습니다."
 createNoteFromTheFile: "이 파일로 노트를 작성"
 archive: "아카이브"
@@ -1230,10 +1242,22 @@ useTotp: "일회용 비밀번호 사용"
 useBackupCode: "백업 코드 사용"
 launchApp: "앱 실행"
 useNativeUIForVideoAudioPlayer: "브라우저 UI에서 미디어 재생"
+keepOriginalFilename: "원본 파일 이름을 유지"
+keepOriginalFilenameDescription: "이 설정을 끄면 업로드를 할 때 파일 이름이 자동으로 무작위 문자열로 바뀝니다."
+noDescription: "설명문이 없습니다"
+alwaysConfirmFollow: "팔로우일 때 항상 확인하기"
+inquiry: "문의하기"
+tryAgain: "다시 시도해 주세요."
+confirmWhenRevealingSensitiveMedia: "민감한 미디어를 열 때 두 번 확인"
 _delivery:
+  status: "전송 상태"
   stop: "정지됨"
+  resume: "전송 다시 시작"
   _type:
     none: "배포 중"
+    manuallySuspended: "수동 정지 중"
+    goneSuspended: "서버 삭제를 이유로 정지 중"
+    autoSuspendedForNotResponding: "서버 응답 없음을 이유로 정지 중"
 _bubbleGame:
   howToPlay: "설명"
   hold: "홀드"
@@ -1359,6 +1383,8 @@ _serverSettings:
   fanoutTimelineDescription: "활성화하면 각종 타임라인을 가져올 때의 성능을 대폭 향상하며, 데이터베이스의 부하를 줄일 수 있습니다. 단, Redis의 메모리 사용량이 증가합니다. 서버의 메모리 용량이 작거나, 서비스가 불안정해지는 경우 비활성화할 수 있습니다."
   fanoutTimelineDbFallback: "데이터베이스를 예비로 사용하기"
   fanoutTimelineDbFallbackDescription: "활성화하면 타임라인의 캐시되어 있지 않은 부분에 대해 DB에 질의하여 정보를 가져옵니다. 비활성화하면 이를 실행하지 않음으로써 서버의 부하를 줄일 수 있지만, 타임라인에서 가져올 수 있는 게시물 범위가 한정됩니다."
+  inquiryUrl: "문의처 URL"
+  inquiryUrlDescription: "서버 운영자에게 보내는 문의 양식의 URL이나 운영자의 연락처 등이 적힌 웹 페이지의 URL을 설정합니다."
 _accountMigration:
   moveFrom: "다른 계정에서 이 계정으로 이사"
   moveFromSub: "다른 계정에 대한 별칭을 생성"
@@ -1675,10 +1701,11 @@ _role:
     canManageAvatarDecorations: "아바타 꾸미기 관리"
     driveCapacity: "드라이브 용량"
     alwaysMarkNsfw: "파일을 항상 NSFW로 지정"
+    canUpdateBioMedia: "아바타 및 배너 이미지 변경 허용"
     pinMax: "고정할 수 있는 노트 수"
     antennaMax: "만들 수 있는 안테나 수"
     wordMuteMax: "단어 뮤트할 수 있는 문자 수"
-    webhookMax: "만들 수 있는 웹후크 수"
+    webhookMax: "만들 수 있는 Webhook 수"
     clipMax: "만들 수 있는 클립 수"
     noteEachClipsMax: "클립에 넣을 수 있는 노트 수"
     userListMax: "만들 수 있는 사용자 리스트 수"
@@ -1693,6 +1720,11 @@ _role:
     roleAssignedTo: "수동 역할에 이미 할당됨"
     isLocal: "로컬 사용자"
     isRemote: "리모트 사용자"
+    isCat: "고양이 사용자"
+    isBot: "봇 사용자"
+    isSuspended: "정지된 사용자"
+    isLocked: "잠금 계정 사용자"
+    isExplorable: "‘계정을 쉽게 발견하도록 하기’를 활성화한 사용자"
     createdLessThan: "가입한 지 다음 일수 이내인 유저"
     createdMoreThan: "가입한 지 다음 일수 이상인 유저"
     followersLessThanOrEq: "팔로워 수가 다음 이하인 유저"
@@ -1913,8 +1945,6 @@ _sfx:
   note: "새 노트"
   noteMy: "내 노트"
   notification: "알림"
-  antenna: "안테나 수신"
-  channel: "채널 알림"
   reaction: "리액션 선택"
 _soundSettings:
   driveFile: "드라이브에 있는 오디오를 사용"
@@ -1975,6 +2005,7 @@ _2fa:
   backupCodesDescription: "인증 앱을 사용할 수 없게 된 경우 아래 백업 코드를 사용하여 계정에 액세스 할 수 있습니다.이 코드들은 반드시 안전한 장소에 보관하십시오.각 코드는 한 번만 사용할 수 있습니다."
   backupCodeUsedWarning: "백업 코드가 사용되었습니다.인증 앱을 사용할 수 없게 된 경우, 조속히 인증 앱을 다시 설정해 주십시오."
   backupCodesExhaustedWarning: "백업 코드가 모두 사용되었습니다.인증 앱을 사용할 수 없는 경우 더 이상 계정에 액세스하는 것이 불가능합니다.인증 앱을 다시 등록해 주세요."
+  moreDetailedGuideHere: "여기에 자세한 설명이 있습니다"
 _permissions:
   "read:account": "계정의 정보를 봅니다"
   "write:account": "계정의 정보를 변경합니다"
@@ -2163,7 +2194,7 @@ _postForm:
     c: "무엇을 생각하고 있나요?"
     d: "말하고 싶은 게 있나요?"
     e: "여기에 적어 주세요"
-    f: "글 쓰기를 기다려요…"
+    f: "작성해주시길 기다리고 있어요..."
 _profile:
   name: "이름"
   username: "사용자 이름"
@@ -2338,6 +2369,7 @@ _deck:
   alwaysShowMainColumn: "메인 칼럼 항상 표시"
   columnAlign: "칼럼 정렬"
   addColumn: "칼럼 추가"
+  newNoteNotificationSettings: "새 노트 알림 설정"
   configureColumn: "칼럼 설정"
   swapLeft: "왼쪽으로 이동"
   swapRight: "오른쪽으로 이동"
@@ -2376,9 +2408,9 @@ _drivecleaner:
   orderByCreatedAtAsc: "등록일이 오래된 순"
 _webhookSettings:
   createWebhook: "Webhook 생성"
+  modifyWebhook: "Webhook 수정"
   name: "이름"
   secret: "시크릿"
-  events: "Webhook을 실행할 타이밍"
   active: "활성화"
   _events:
     follow: "누군가를 팔로우했을 때"
@@ -2388,6 +2420,26 @@ _webhookSettings:
     renote: "누군가 내 글을 리노트했을 때"
     reaction: "누군가 내 노트에 리액션했을 때"
     mention: "누군가 나를 멘션했을 때"
+  _systemEvents:
+    abuseReport: "유저로부터 신고를 받았을 때"
+    abuseReportResolved: "받은 신고를 처리했을 때"
+    userCreated: "유저가 생성되었을 때"
+  deleteConfirm: "Webhook을 삭제할까요?"
+_abuseReport:
+  _notificationRecipient:
+    createRecipient: "신고 수신자 추가"
+    modifyRecipient: "신고 수신자 편집"
+    recipientType: "알림 수신 유형"
+    _recipientType:
+      mail: "이메일"
+      webhook: "Webhook"
+      _captions:
+        mail: "모더레이터 권한을 가진 사용자의 이메일 주소에 알림을 보냅니다 (신고를 받은 때에만)"
+        webhook: "지정한 SystemWebhook에 알림을 보냅니다 (신고를 받은 때와 해결했을 때에 송신)"
+    keywords: "키워드"
+    notifiedUser: "신고 알림을 보낼 유저"
+    notifiedWebhook: "사용할 Webhook"
+    deleteConfirm: "수신자를 삭제하시겠습니까?"
 _moderationLogTypes:
   createRole: "역할 생성"
   deleteRole: "역할 삭제"
@@ -2403,7 +2455,7 @@ _moderationLogTypes:
   updateUserNote: "조정 기록 갱신"
   deleteDriveFile: "파일 삭제"
   deleteNote: "노트 삭제"
-  createGlobalAnnouncement: "모든 공지사항 만들기"
+  createGlobalAnnouncement: "전역 공지사항 생성"
   createUserAnnouncement: "사용자 공지사항 만들기"
   updateGlobalAnnouncement: "모든 공지사항 수정"
   updateUserAnnouncement: "사용자 공지사항 수정"
@@ -2425,6 +2477,12 @@ _moderationLogTypes:
   deleteAvatarDecoration: "아바타 장식 삭제"
   unsetUserAvatar: "유저 아바타 제거"
   unsetUserBanner: "유저 배너 제거"
+  createSystemWebhook: "SystemWebhook을 생성"
+  updateSystemWebhook: "SystemWebhook을 수정"
+  deleteSystemWebhook: "SystemWebhook을 삭제"
+  createAbuseReportNotificationRecipient: "신고 알림 수신자 생성"
+  updateAbuseReportNotificationRecipient: "신고 알림 수신자 편집"
+  deleteAbuseReportNotificationRecipient: "신고 알림 수신자 삭제"
 _fileViewer:
   title: "파일 상세"
   type: "파일 유형"
diff --git a/locales/lo-LA.yml b/locales/lo-LA.yml
index 087bac3745..1bead5635d 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,28 @@ _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"
   _actions:
-    reply: "ຕອບ​ໄປ​ທີ"
+    reply: "ຕອບ​ກັບ"
     renote: "Renote"
 _deck:
   _columns:
@@ -465,8 +465,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 2154e248af..686d532c4c 100644
--- a/locales/nl-NL.yml
+++ b/locales/nl-NL.yml
@@ -251,7 +251,7 @@ upload: "Uploaden"
 keepOriginalUploading: "Origineel beeld behouden."
 keepOriginalUploadingDescription: "Bewaar de originele versie bij het uploaden van afbeeldingen. Indien uitgeschakeld, wordt bij het uploaden een alternatieve versie voor webpublicatie genereert."
 fromDrive: "Van schijf"
-fromUrl: "Van  URL"
+fromUrl: "Van URL"
 uploadFromUrl: "Uploaden vanaf een URL"
 uploadFromUrlDescription: "URL van het bestand dat je wil uploaden"
 uploadFromUrlRequested: "Uploadverzoek"
diff --git a/locales/no-NO.yml b/locales/no-NO.yml
index 2b4c9b7776..cd00ecf9ab 100644
--- a/locales/no-NO.yml
+++ b/locales/no-NO.yml
@@ -721,5 +721,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 4acd6af991..b20eabf7f2 100644
--- a/locales/pl-PL.yml
+++ b/locales/pl-PL.yml
@@ -865,7 +865,7 @@ whatIsNew: "Pokaż zmiany"
 translate: "Przetłumacz"
 translatedFrom: "Przetłumaczone z {x}"
 accountDeletionInProgress: "Trwa usuwanie konta"
-usernameInfo: "Nazwa, która identyfikuje Twoje konto spośród innych na tym serwerze.  Możesz użyć alfabetu (a~z, A~Z), cyfr (0~9) lub podkreślników (_). Nazwy użytkownika nie mogą być później zmieniane."
+usernameInfo: "Nazwa, która identyfikuje Twoje konto spośród innych na tym serwerze. Możesz użyć alfabetu (a~z, A~Z), cyfr (0~9) lub podkreślników (_). Nazwy użytkownika nie mogą być później zmieniane."
 aiChanMode: "Tryb Ai"
 devMode: "Tryb programisty"
 keepCw: "Zostaw ostrzeżenia o zawartości"
@@ -1221,8 +1221,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ą"
@@ -1546,7 +1544,6 @@ _webhookSettings:
   createWebhook: "Stwórz Webhook"
   name: "Nazwa"
   secret: "Sekret"
-  events: "Uruchomienie Webhooka"
   active: "Właczono"
   _events:
     follow: "Po zaobserwowaniu użytkownika"
@@ -1556,6 +1553,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 0bfd1f778b..71ba7c4371 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"
@@ -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"
@@ -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..."
@@ -476,6 +511,7 @@ 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 +560,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 +574,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 +596,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 +650,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 +672,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 +694,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."
@@ -708,6 +754,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 +768,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 +843,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 +908,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 +975,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 +1010,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 +1020,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 +1038,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 proibídas"
+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"
-replies: "Respostas"
-renotes: "Repostagens"
+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: "Post 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 +1676,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 +1707,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 +1729,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 +1753,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 +1785,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 +1804,224 @@ _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"
+    listItemHoverBg: "Plano de fundo do item de uma lista (Selecionado)"
+    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 +2055,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 +2161,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 +2275,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 +2349,34 @@ _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"
     app: "Notificações de aplicativos conectados"
   _actions:
     followBack: "te seguiu de volta"
@@ -1472,13 +2386,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 +2414,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 88424cbbfb..95c1e16508 100644
--- a/locales/ro-RO.yml
+++ b/locales/ro-RO.yml
@@ -728,6 +728,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..88f59155d6 100644
--- a/locales/ru-RU.yml
+++ b/locales/ru-RU.yml
@@ -1612,8 +1612,6 @@ _sfx:
   note: "Заметки"
   noteMy: "Собственные заметки"
   notification: "Уведомления"
-  antenna: "Антенна"
-  channel: "Канал"
 _ago:
   future: "Из будущего"
   justNow: "Только что"
@@ -1983,6 +1981,10 @@ _webhookSettings:
   createWebhook: "Создать вебхук"
   name: "Название"
   active: "Вкл."
+_abuseReport:
+  _notificationRecipient:
+    _recipientType:
+      mail: "Электронная почта"
 _moderationLogTypes:
   suspend: "Заморозить"
   addCustomEmoji: "Добавлено эмодзи"
diff --git a/locales/sk-SK.yml b/locales/sk-SK.yml
index 6ae90395ab..409d682af7 100644
--- a/locales/sk-SK.yml
+++ b/locales/sk-SK.yml
@@ -1124,8 +1124,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"
@@ -1447,6 +1445,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 11b2b36499..c98782450f 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:
@@ -577,6 +576,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 01510cc03c..508ca38949 100644
--- a/locales/th-TH.yml
+++ b/locales/th-TH.yml
@@ -2,10 +2,10 @@
 _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: "รหัสผ่าน"
 forgotPassword: "ลืมรหัสผ่าน"
@@ -18,7 +18,7 @@ enterUsername: "กรอกชื่อผู้ใช้"
 renotedBy: "รีโน้ตโดย {user}"
 noNotes: "ไม่มีโน้ต"
 noNotifications: "ไม่มีการแจ้งเตือน"
-instance: "อินสแตนซ์"
+instance: "เซิร์ฟเวอร์"
 settings: "การตั้งค่า"
 notificationSettings: "ตั้งค่าการแจ้งเตือน"
 basicSettings: "การตั้งค่าพื้นฐาน"
@@ -48,7 +48,7 @@ copyLink: "คัดลอกลิงก์"
 copyLinkRenote: "คัดลอกลิงก์รีโน้ต"
 delete: "ลบ"
 deleteAndEdit: "ลบและแก้ไข"
-deleteAndEditConfirm: "คุณต้องการลบโน้ตนี้และแก้ไขใหม่ใช่ไหม? รีแอคชั่น รีโน้ต และการตอบกลับต่อโน้ตนี้ทั้งหมดจะถูกลบออกด้วย"
+deleteAndEditConfirm: "ต้องการลบโน้ตนี้และแก้ไขใหม่ใช่ไหม? รีแอคชั่น รีโน้ต และการตอบกลับต่อโน้ตนี้ทั้งหมดจะถูกลบออกด้วย"
 addToList: "เพิ่มลงรายชื่อ"
 addToAntenna: "เพิ่มไปยังเสาอากาศ"
 sendMessage: "ส่งข้อความ"
@@ -60,6 +60,7 @@ copyFileId: "คัดลอกไฟล์ ID"
 copyFolderId: "คัดลอกโฟลเดอร์ ID"
 copyProfileUrl: "คัดลอกโปรไฟล์ URL"
 searchUser: "ค้นหาผู้ใช้"
+searchThisUsersNotes: "ค้นหาโน้ตของผู้ใช้"
 reply: "ตอบกลับ"
 loadMore: "แสดงเพิ่มเติม"
 showMore: "แสดงเพิ่มเติม"
@@ -68,7 +69,7 @@ youGotNewFollower: "ได้ติดตามคุณ"
 receiveFollowRequest: "มีคำขอติดตามส่งมาหา"
 followRequestAccepted: "การติดตามได้รับการอนุมัติแล้ว"
 mention: "กล่าวถึง"
-mentions: "พูดถึง"
+mentions: "กล่าวถึงคุณ"
 directNotes: "โพสต์แบบไดเร็กต์"
 importAndExport: "นำเข้า / ส่งออก"
 import: "นำเข้า"
@@ -92,7 +93,7 @@ error: "ผิดพลาด!"
 somethingHappened: "อุ๊ย ! มีอะไรบางอย่างผิดพลาด"
 retry: "ลองใหม่อีกครั้ง"
 pageLoadError: "เกิดข้อผิดพลาดในการโหลดหน้านี้"
-pageLoadErrorDescription: "โดยปกติแล้วมักจะเกิดจากข้อผิดพลาดของเครือข่ายหรือแคชของเบราว์เซอร์ ลองล้างแคชแล้วลองใหม่อีกครั้งหลังจากรอสักครู่ "
+pageLoadErrorDescription: "ปัญหานี้มักเกิดจากแคชของเครือข่ายหรือเบราว์เซอร์ ควรล้างแคช, รอสักครู่ แล้วลองใหม่อีกครั้ง"
 serverIsDead: "เซิร์ฟเวอร์นี้ไม่มีการตอบสนอง โปรดกรุณารอสักครู่แล้วลองใหม่อีกครั้ง"
 youShouldUpgradeClient: "หากต้องการดูหน้านี้ กรุณาโหลดหน้าใหม่เพื่ออัปเดตไคลเอ็นต์ของคุณ"
 enterListName: "ป้อนนามเรียกของรายชื่อชุดนี้"
@@ -108,11 +109,14 @@ enterEmoji: "พิมพ์เอโมจิ"
 renote: "รีโน้ต"
 unrenote: "เลิกรีโน้ต"
 renoted: "รีโน้ตแล้ว"
+renotedToX: "รีโน้ตให้ {name} แล้ว"
 cantRenote: "โพสต์นี้ไม่สามารถรีโน้ตใหม่ได้"
 cantReRenote: "รีโน้ตไม่สามารถรีโน้ตซ้ำได้"
 quote: "อ้างอิง"
 inChannelRenote: "รีโน้ตในช่องเท่านั้น"
 inChannelQuote: "อ้างอิงในช่องเท่านั้น"
+renoteToChannel: "รีโน้ตไปที่ช่อง"
+renoteToOtherChannel: "รีโน้ตไปยังช่องอื่น"
 pinnedNote: "โน้ตที่ปักหมุดไว้"
 pinned: "ปักหมุด"
 you: "คุณ"
@@ -151,6 +155,7 @@ editList: "แก้ไขรายชื่อ"
 selectChannel: "เลือกช่อง"
 selectAntenna: "เลือกเสาอากาศ"
 editAntenna: "แก้ไขเสาอากาศ"
+createAntenna: "สร้างเสาอากาศ"
 selectWidget: "เลือกวิดเจ็ต"
 editWidgets: "แก้ไขวิดเจ็ต"
 editWidgetsExit: "เรียบร้อย"
@@ -163,20 +168,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 +194,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 +223,19 @@ jobQueue: "คิวงาน"
 cpuAndMemory: "ซีพียู และ หน่วยความจำ"
 network: "เครือข่าย"
 disk: "ดิสก์"
-instanceInfo: "ข้อมูลอินสแตนซ์"
+instanceInfo: "ข้อมูลเซิร์ฟเวอร์"
 statistics: "สถิติการใช้งาน"
 clearQueue: "ล้างคิว"
 clearQueueConfirmTitle: "ต้องการล้างคิวใช่ไหม?"
 clearQueueConfirmText: "โพสต์ที่ยังค้างในคิวจะไม่ถูกจัดส่งอีกต่อไป โดยปกติแล้วการดำเนินการนี้ไม่จำเป็น"
 clearCachedFiles: "ล้างแคช"
 clearCachedFilesConfirm: "ต้องการลบไฟล์ระยะไกลที่แคชไว้ทั้งหมดใช่ไหม?"
-blockedInstances: "อินสแตนซ์ที่ถูกบล็อก"
-blockedInstancesDescription: "ระบุชื่อโฮสต์ของอินสแตนซ์ที่คุณต้องการบล็อก อินสแตนซ์ที่อยู่ในรายการนั้นจะไม่สามารถพูดคุยกับอินสแตนซ์นี้ได้อีกต่อไป"
-silencedInstances: "ปิดปากอินสแตนซ์นี้แล้ว"
-silencedInstancesDescription: "ตั้งค่ารายชื่อโฮสต์ของอินสแตนซ์ที่คุณต้องการปิดปาก บัญชีทั้งหมดของอินสแตนซ์ที่อยู่ในรายชื่อนั้นๆ จะถือว่าถูกปิดปากเช่นกัน ทำได้เฉพาะคำขอติดตามเท่านั้น และไม่สามารถกล่าวถึงบัญชีในพื้นที่ได้หากไม่ได้ติดตาม | สิ่งนี้ไม่มีผลต่ออินสแตนซ์ที่ถูกบล็อก"
+blockedInstances: "เซิร์ฟเวอร์ที่ถูกบล็อก"
+blockedInstancesDescription: "ระบุโฮสต์ของเซิร์ฟเวอร์ที่ต้องการบล็อก คั่นด้วยการขึ้นบรรทัดใหม่ เซิร์ฟเวอร์ที่ถูกบล็อกจะไม่สามารถติดต่อกับอินสแตนซ์นี้ได้"
+silencedInstances: "ปิดปากเซิร์ฟเวอร์นี้แล้ว"
+silencedInstancesDescription: "ระบุโฮสต์ของเซิร์ฟเวอร์ที่ต้องการปิดปาก คั่นด้วยการขึ้นบรรทัดใหม่, บัญชีทั้งหมดของเซิร์ฟเวอร์ดังกล่าวจะถือว่าถูกปิดปากเช่นกัน ทำได้เฉพาะคำขอติดตามเท่านั้น และไม่สามารถกล่าวถึงบัญชีในเซิร์ฟเวอร์นี้ได้หากไม่ได้ถูกติดตามกลับ | สิ่งนี้ไม่มีผลต่ออินสแตนซ์ที่ถูกบล็อก"
+mediaSilencedInstances: "เซิร์ฟเวอร์ที่ถูกปิดปากสื่อ"
+mediaSilencedInstancesDescription: "ระบุโฮสต์ของเซิร์ฟเวอร์ที่ต้องการปิดปากสื่อ คั่นด้วยการขึ้นบรรทัดใหม่, ไฟล์ที่ถูกส่งจากบัญชีของเซิร์ฟเวอร์ดังกล่าวจะถือว่าถูกปิดปาก แล้วจะถูกติดเครื่องหมายว่ามีเนื้อหาละเอียดอ่อน และเอโมจิแบบกำหนดเองก็จะใช้ไม่ได้ด้วย | สิ่งนี้ไม่มีผลต่ออินสแตนซ์ที่ถูกบล็อก"
 muteAndBlock: "ปิดเสียงและบล็อก"
 mutedUsers: "ผู้ใช้ที่ถูกปิดเสียง"
 blockedUsers: "ผู้ใช้ที่ถูกบล็อก"
@@ -240,14 +253,14 @@ noCustomEmojis: "ไม่มีเอโมจิ"
 noJobs: "ไม่มีงาน"
 federating: "สหพันธ์"
 blocked: "ถูกบล็อก"
-suspended: "ถูกระงับ"
+suspended: "ระงับการส่ง"
 all: "ทั้งหมด"
-subscribing: "สมัครแล้ว"
+subscribing: "กำลังสมัครสมาชิก"
 publishing: "กำลังเผยแพร่"
 notResponding: "ไม่มีการตอบสนอง"
-instanceFollowing: "กำลังติดตามบนอินสแตนซ์"
-instanceFollowers: "ผู้ติดตามของอินสแตนซ์"
-instanceUsers: "ผู้ใช้งานของอินสแตนซ์นี้"
+instanceFollowing: "กำลังติดตามบนเซิร์ฟเวอร์"
+instanceFollowers: "ผู้ติดตามของเซิร์ฟเวอร์"
+instanceUsers: "ผู้ใช้ของเซิร์ฟเวอร์นี้"
 changePassword: "เปลี่ยนรหัสผ่าน"
 security: "ความปลอดภัย"
 retypedNotMatch: "ทั้งสองป้อนข้อมูลไม่สอดคล้องกัน"
@@ -284,20 +297,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 +326,7 @@ selectFile: "เลือกไฟล์"
 selectFiles: "เลือกไฟล์"
 selectFolder: "เลือกโฟลเดอร์"
 selectFolders: "เลือกโฟลเดอร์"
+fileNotSelected: "ยังไม่ได้เลือกไฟล์"
 renameFile: "เปลี่ยนชื่อไฟล์"
 folderName: "ชื่อโฟลเดอร์"
 createFolder: "สร้างโฟลเดอร์"
@@ -336,15 +350,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 +369,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 +387,7 @@ basicInfo: "ข้อมูลเบื้องต้น"
 pinnedUsers: "ผู้ใช้ที่ถูกปักหมุด"
 pinnedUsersDescription: "ป้อนชื่อผู้ใช้ที่คุณต้องการปักหมุดในหน้า “ค้นพบ” ฯลฯ คั่นด้วยการขึ้นบรรทัดใหม่"
 pinnedPages: "หน้าเพจที่ปักหมุด"
-pinnedPagesDescription: "ป้อนเส้นทางของหน้าเพจที่คุณต้องการปักหมุดไว้ที่หน้าแรกของอินสแตนซ์นี้ คั่นด้วยขึ้นบรรทัดใหม่"
+pinnedPagesDescription: "ป้อนเส้นทางของหน้าเพจที่คุณต้องการปักหมุดไว้ที่หน้าแรกของเซิร์ฟเวอร์นี้ คั่นด้วยการขึ้นบรรทัดใหม่"
 pinnedClipId: "ID ของคลิปที่จะปักหมุด"
 pinnedNotes: "โน้ตที่ปักหมุดไว้"
 hcaptcha: "hCaptcha"
@@ -389,11 +403,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 +415,7 @@ antennaSource: "แหล่งเสาอากาศ"
 antennaKeywords: "คีย์เวิร์ดที่ควรฟัง"
 antennaExcludeKeywords: "คีย์เวิร์ดที่จะยกเว้น"
 antennaExcludeBots: "ยกเว้นบัญชีบอต"
-antennaKeywordsDescription: "คั่นด้วยช่องว่างสำหรับเงื่อนไข AND หรือด้วยการขึ้นบรรทัดใหม่สำหรับเงื่อนไข OR"
+antennaKeywordsDescription: "คั่นด้วยเว้นวรรคสำหรับเงื่อนไข AND, หรือขึ้นบรรทัดใหม่สำหรับเงื่อนไข OR"
 notifyAntenna: "แจ้งเตือนเกี่ยวกับโน้ตใหม่"
 withFileAntenna: "เฉพาะโน้ตที่มีไฟล์"
 enableServiceworker: "เปิดใช้งานการแจ้งเตือนแบบพุชไปยังเบราว์เซอร์ของคุณ"
@@ -418,7 +432,7 @@ unsilenceConfirm: "ต้องการเลิกปิดปากผู้
 popularUsers: "ผู้ใช้ที่เป็นที่นิยม"
 recentlyUpdatedUsers: "ผู้ใช้ที่เพิ่งใช้งานล่าสุด"
 recentlyRegisteredUsers: "ผู้ใช้ที่เข้าร่วมใหม่"
-recentlyDiscoveredUsers: "ผู้ใช้ที่เพิ่งค้นพบใหม่"
+recentlyDiscoveredUsers: "ผู้ใช้ที่เพิ่งค้นพบล่าสุด"
 exploreUsersCount: "มีผู้ใช้ {count} ราย"
 exploreFediverse: "สำรวจสหพันธ์"
 popularTags: "แท็กยอดนิยม"
@@ -435,7 +449,7 @@ moderator: "ผู้ควบคุม"
 moderation: "การกลั่นกรอง"
 moderationNote: "โน้ตการกลั่นกรอง"
 addModerationNote: "เพิ่มโน้ตการกลั่นกรอง"
-moderationLogs: "ปูมการแก้ไข"
+moderationLogs: "ปูมการควบคุมดูแล"
 nUsersMentioned: "กล่าวถึงโดยผู้ใช้ {n} ราย"
 securityKeyAndPasskey: "ความปลอดภัยและรหัสผ่าน"
 securityKey: "กุญแจความปลอดภัย"
@@ -468,44 +482,46 @@ 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: "แสดงการดำเนินการเฉพาะโน้ตเมื่อโฮเวอร์"
+disableDrawer: "ไม่แสดงเมนูในรูปแบบลิ้นชัก"
+showNoteActionsOnlyHover: "แสดงการดำเนินการโน้ตเมื่อโฮเวอร์(วางเมาส์เหนือ)เท่านั้น"
 showReactionsCount: "แสดงจำนวนรีแอกชั่นในโน้ต"
 noHistory: "ไม่มีประวัติ"
 signinHistory: "ประวัติการเข้าสู่ระบบ"
 enableAdvancedMfm: "เปิดใช้งาน MFM ขั้นสูง"
-enableAnimatedMfm: "เปิดการใช้งาน MFM ด้วยแอนิเมชั่น"
+enableAnimatedMfm: "เปิดการใช้งาน MFM แบบเคลื่อนไหว"
 doing: "กำลังประมวลผล......"
 category: "หมวดหมู่"
 tags: "นามแฝง"
 docSource: "ที่มาของเอกสารนี้"
 createAccount: "สร้างบัญชี"
-existingAccount: "บัญชีที่มีอยู่"
+existingAccount: "บัญชีที่มีอยู่แล้ว"
 regenerate: "สร้างอีกครั้ง"
 fontSize: "ขนาดตัวอักษร"
 mediaListWithOneImageAppearance: "ความสูงของรายการสื่อที่มีเพียงรูปเดียว"
@@ -513,11 +529,11 @@ limitTo: "จำกัดไว้ที่ {x}"
 noFollowRequests: "คุณไม่มีคำขอติดตามที่รอดำเนินการ"
 openImageInNewTab: "เปิดรูปภาพในแท็บใหม่"
 dashboard: "หน้ากระดานหลัก"
-local: "ในพื้นที่"
+local: "ท้องถิ่น"
 remote: "ระยะไกล"
 total: "รวมทั้งหมด"
-weekOverWeekChanges: "เปลี่ยนแปลงไปเมื่อสัปดาห์ที่แล้ว"
-dayOverDayChanges: "เปลี่ยนแปลงไปเมื่อวานนี้"
+weekOverWeekChanges: "เทียบกับสัปดาห์ก่อน"
+dayOverDayChanges: "เทียบกับเมื่อวาน"
 appearance: "ภาพลักษณ์"
 clientSettings: "การตั้งค่าไคลเอนต์"
 accountSettings: "ตั้งค่าบัญชี"
@@ -531,19 +547,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 +591,7 @@ sort: "เรียงลำดับ"
 ascendingOrder: "เรียงลำดับขึ้น"
 descendingOrder: "เรียงลำดับลง"
 scratchpad: "Scratchpad"
-scratchpadDescription: "Scratchpad เป็นการจัดเตรียมสภาพแวดล้อมสำหรับการทดลอง AiScript แต่คุณสามารถเขียน ดำเนินการ และตรวจสอบผลลัพธ์ของการโต้ตอบกับ Misskey มันได้ด้วยนะ"
+scratchpadDescription: "Scratchpad ให้สภาพแวดล้อมสำหรับการทดลอง AiScript คุณสามารถเขียนโค้ด/สั่งดำเนินการ/ตรวจสอบผลลัพธ์ ของการโต้ตอบกับ Misskey ได้"
 output: "เอาท์พุต"
 script: "สคริปต์"
 disablePagesScript: "ปิดการใช้งาน AiScript บนเพจ"
@@ -587,15 +603,15 @@ unsetUserBannerConfirm: "ต้องการเลิกตั้งแบน
 deleteAllFiles: "ลบไฟล์ทั้งหมด"
 deleteAllFilesConfirm: "ต้องการลบไฟล์ทั้งหมดใช่ไหม?"
 removeAllFollowing: "เลิกติดตามผู้ใช้ที่ติดตามทั้งหมด"
-removeAllFollowingDescription: "เลิกติดตามทั้งหมดจาก {host} โปรดเรียกใช้สิ่งนี้เมื่ออินสแตนซ์ดังกล่าวได้สูญหายตายจากไปแล้ว"
+removeAllFollowingDescription: "จะเลิกติดตามทั้งหมดจาก {host} โปรดดำเนินการสิ่งนี้เมื่อเซิร์ฟเวอร์ดังกล่าวได้สูญหายตายจากไปแล้ว"
 userSuspended: "ผู้ใช้รายนี้ถูกระงับการใช้งาน"
 userSilenced: "ผู้ใช้รายนี้ถูกปิดปากอยู่"
 yourAccountSuspendedTitle: "บัญชีนี้นั้นถูกระงับ"
 yourAccountSuspendedDescription: "บัญชีนี้ถูกระงับ เนื่องจากละเมิดข้อกำหนดในการให้บริการของเซิร์ฟเวอร์หรืออาจจะละเมิดหลักเกณฑ์ชุมชน หรือ อาจจะโดนร้องเรียนเรื่องการละเมิดลิขสิทธิ์และอื่นๆอย่างต่อเนื่องซ้ำๆ หากคุณคิดว่าไม่ได้ทำผิดจริงๆหรือตัดสินผิดพลาด ได้โปรดกรุณาติดต่อผู้ดูแลระบบหากคุณต้องการทราบเหตุผลโดยละเอียดเพิ่มเติม และขอความกรุณาอย่าสร้างบัญชีใหม่"
 tokenRevoked: "โทเค็นไม่ถูกต้อง"
-tokenRevokedDescription: "โทเค็นนี้หมดอายุแล้วนะค่ะกรุณาเข้าสู่ระบบอีกครั้งนะ"
+tokenRevokedDescription: "โทเค็นการเข้าสู่ระบบหมดอายุ กรุณาเข้าสู่ระบบใหม่อีกครั้ง"
 accountDeleted: "ลบบัญชีแล้ว"
-accountDeletedDescription: "บัญชีนี้ถูกลบไปแล้วนะ"
+accountDeletedDescription: "บัญชีนี้ถูกลบแล้ว"
 menu: "เมนู"
 divider: "ตัวแบ่ง"
 addItem: "เพิ่มรายการ"
@@ -615,14 +631,14 @@ enablePlayer: "เปิดเครื่องเล่นวิดีโอ"
 disablePlayer: "ปิดเครื่องเล่นวิดีโอ"
 expandTweet: "ขยายทวีต"
 themeEditor: "ตัวแก้ไขธีม"
-description: "รายละเอียด"
+description: "คำอธิบาย"
 describeFile: "เพิ่มแคปชั่น"
 enterFileDescription: "ใส่แคปชั่น"
 author: "ผู้เขียน"
 leaveConfirm: "มีการเปลี่ยนแปลงที่ยังไม่ได้บันทึก ต้องการละทิ้งมันใช่ไหม?"
 manage: "การจัดการ"
 plugins: "ปลั๊กอิน"
-preferencesBackups: "ตั้งค่าการสำรองข้อมูล"
+preferencesBackups: "สำรองการตั้งค่า"
 deck: "เด็ค"
 undeck: "ออกจากเด็ค"
 useBlurEffectForModal: "ใช้เอฟเฟกต์เบลอสำหรับโมดอล"
@@ -632,21 +648,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 +672,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 +706,17 @@ reportAbuseOf: "รายงาน {name}"
 fillAbuseReportDescription: "กรุณากรอกรายละเอียดเกี่ยวกับรายงานนี้ หากเป็นเรื่องเกี่ยวกับโน้ตโดยเฉพาะ ได้โปรดระบุ URL"
 abuseReported: "เราได้ส่งรายงานของคุณไปแล้ว ขอบคุณมากๆนะ"
 reporter: "ผู้รายงาน"
-reporteeOrigin: "รายงานต้นทาง"
+reporteeOrigin: "ปลายทางรายงาน"
 reporterOrigin: "แหล่งผู้รายงาน"
-forwardReport: "ส่งต่อรายงานไปยังอินสแตนซ์ระยะไกล"
-forwardReportIsAnonymous: "ข้อมูลของคุณจะไม่ปรากฏบนอินสแตนซ์ระยะไกลและปรากฏเป็นบัญชีระบบที่ไม่ระบุชื่อ"
+forwardReport: "ส่งต่อรายงานไปยังเซิร์ฟเวอร์ระยะไกล"
+forwardReportIsAnonymous: "ข้อมูลของคุณจะไม่ปรากฏบนเซิร์ฟเวอร์ระยะไกลและปรากฏเป็นบัญชีระบบที่ไม่ระบุชื่อ"
 send: "ส่ง"
 abuseMarkAsResolved: "ทำเครื่องหมายรายงานว่าแก้ไขแล้ว"
 openInNewTab: "เปิดในแท็บใหม่"
 openInSideView: "เปิดในมุมมองด้านข้าง"
 defaultNavigationBehaviour: "พฤติกรรมการนำทางที่เป็นค่าเริ่มต้น"
 editTheseSettingsMayBreakAccount: "การแก้ไขการตั้งค่าเหล่านี้อาจทำให้บัญชีของคุณเสียหายนะ"
-instanceTicker: "ข้อมูลอินสแตนซ์ของโน้ต"
+instanceTicker: "ข้อมูลเซิร์ฟเวอร์ของโน้ต"
 waitingFor: "กำลังรอ {x}"
 random: "สุ่มค่า"
 system: "ระบบ"
@@ -739,7 +755,7 @@ alwaysMarkSensitive: "ทำเครื่องหมายว่ามีเ
 loadRawImages: "โหลดภาพต้นฉบับแทนการแสดงภาพขนาดย่อ"
 disableShowingAnimatedImages: "ไม่ต้องเล่นภาพเคลื่อนไหว"
 highlightSensitiveMedia: "ไฮไลท์สื่อที่มีเนื้อหาละเอียดอ่อน"
-verificationEmailSent: "ส่งอีเมลยืนยันแล้วนะ ได้โปรดกรุณาไปที่ลิงก์ที่รวมไว้เพื่อทำการตรวจสอบให้เสร็จสิ้น"
+verificationEmailSent: "ได้ส่งอีเมลยืนยันแล้ว กรุณาเข้าลิงก์ที่ระบุในอีเมลเพื่อทำการตั้งค่าให้เสร็จสิ้น"
 notSet: "ไม่ได้ตั้งค่า"
 emailVerified: "อีเมลได้รับการยืนยันแล้ว"
 noteFavoritesCount: "จำนวนโน้ตที่ชื่นชอบ"
@@ -750,7 +766,7 @@ useSystemFont: "ใช้ฟอนต์เริ่มต้นของระ
 clips: "คลิป"
 experimentalFeatures: "ฟังก์ชั่นทดสอบ"
 experimental: "ทดลอง"
-thisIsExperimentalFeature: "นี่คือฟีเจอร์ทดลองนะค่ะ ฟังก์ชันการทำงานบางอย่างอาจเปลี่ยนแปลงได้ และอาจไม่ทำงานหรือไม่เสถียรตามที่ตั้งใจไว้นะ"
+thisIsExperimentalFeature: "นี่เป็นฟีเจอร์ทดลอง ซึ่งอาจมีการเปลี่ยนแปลงการทำงาน และอาจไม่ทำงานตามที่ตั้งใจไว้"
 developer: "สำหรับนักพัฒนา"
 makeExplorable: "ทำให้บัญชีมองเห็นใน “สำรวจ”"
 makeExplorableDescription: "ถ้าหากคุณปิดการทำงานนี้ บัญชีของคุณนั้นจะไม่แสดงในส่วน “สำรวจ”"
@@ -761,14 +777,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 +796,7 @@ value: "ค่า"
 createdAt: "สร้างเมื่อ"
 updatedAt: "อัปเดตล่าสุด"
 saveConfirm: "บันทึกเปลี่ยนแปลงมั้ย?"
-deleteConfirm: "ลบจริงๆเหรอ?"
+deleteConfirm: "ต้องการลบใช่ไหม?"
 invalidValue: "ค่านี้ไม่ถูกต้อง"
 registry: "ทะเบียน"
 closeAccount: "ปิด บัญชี"
@@ -793,7 +809,7 @@ capacity: "ความจุ"
 inUse: "ใช้แล้ว"
 editCode: "แก้ไขโค้ด"
 apply: "นำไปใช้"
-receiveAnnouncementFromInstance: "รับการแจ้งเตือนจากอินสแตนซ์นี้"
+receiveAnnouncementFromInstance: "รับการแจ้งเตือนจากเซิร์ฟเวอร์นี้"
 emailNotification: "การแจ้งเตือนทางอีเมล"
 publish: "เผยแพร่"
 inChannelSearch: "ค้นหาในช่อง"
@@ -821,7 +837,7 @@ active: "ใช้งานอยู่"
 offline: "ออฟไลน์"
 notRecommended: "ไม่แนะนำ"
 botProtection: "การป้องกัน Bot"
-instanceBlocking: "อินสแตนซ์ที่ถูกบล็อก"
+instanceBlocking: "เซิร์ฟเวอร์ที่ถูกบล็อก/ปิดปาก"
 selectAccount: "เลือกบัญชี"
 switchAccount: "สลับบัญชีผู้ใช้"
 enabled: "เปิดใช้งาน"
@@ -831,9 +847,10 @@ user: "ผู้ใช้"
 administration: "การจัดการ"
 accounts: "บัญชีผู้ใช้"
 switch: "สลับ"
-noMaintainerInformationWarning: "ข้อมูลผู้ดูแลไม่ได้รับการกำหนดค่านะ"
-noBotProtectionWarning: "ไม่ได้กำหนดค่าการป้องกันบอทนะ"
-configure: "กำหนดค่า"
+noMaintainerInformationWarning: "ยังไม่ได้ตั้งค่าข้อมูลของผู้ดูแลระบบ"
+noInquiryUrlWarning: "ยังไม่ได้ตั้งค่า URL สำหรับการติดต่อสอบถาม"
+noBotProtectionWarning: "ยังไม่ได้ตั้งค่าการป้องกันบอต"
+configure: "ตั้งค่า"
 postToGallery: "สร้างโพสต์แกลเลอรี่ใหม่"
 postToHashtag: "โพสต์ไปที่แฮชแท็กนี้"
 gallery: "แกลเลอรี่"
@@ -870,7 +887,7 @@ accountDeletionInProgress: "กำลังดำเนินการลบบ
 usernameInfo: "ชื่อที่ระบุบัญชีของคุณจากผู้อื่นในเซิร์ฟเวอร์นี้ คุณสามารถใช้ตัวอักษร (a~z, A~Z), ตัวเลข (0~9) หรือขีดล่าง (_) ชื่อผู้ใช้ไม่สามารถเปลี่ยนแปลงได้ในภายหลัง"
 aiChanMode: "โหมด Ai "
 devMode: "โหมดนักพัฒนา"
-keepCw: "เก็บคำเตือนเนื้อหา"
+keepCw: "คงการเตือนเนื้อหาไว้"
 pubSub: "บัญชี Pub/Sub"
 lastCommunication: "การสื่อสารครั้งสุดท้ายล่าสุด"
 resolved: "คลี่คลายแล้ว"
@@ -909,8 +926,8 @@ themeColor: "สีธีม"
 size: "ขนาด"
 numberOfColumn: "จำนวนคอลัมน์"
 searchByGoogle: "ค้นหา"
-instanceDefaultLightTheme: "ธีมสว่างตามค่าเริ่มต้นของอินสแตนซ์"
-instanceDefaultDarkTheme: "ธีมมืดตามค่าเริ่มต้นของอินสแตนซ์"
+instanceDefaultLightTheme: "ธีมสว่างตามค่าเริ่มต้นของเซิร์ฟเวอร์"
+instanceDefaultDarkTheme: "ธีมมืดตามค่าเริ่มต้นของเซิร์ฟเวอร์"
 instanceDefaultThemeDescription: "ป้อนรหัสธีมในรูปแบบออบเจ็กต์"
 mutePeriod: "ระยะเวลาปิดเสียง"
 period: "ระยะเวลา"
@@ -930,7 +947,7 @@ cropNo: "ใช้ตามที่เป็นอยู่"
 file: "ไฟล์"
 recentNHours: "ล่าสุด {n} ชั่วโมงที่แล้ว"
 recentNDays: "ล่าสุด {n} วันที่แล้ว"
-noEmailServerWarning: "ไม่ได้กำหนดค่าเซิร์ฟเวอร์อีเมลนี้"
+noEmailServerWarning: "ยังไม่ได้ตั้งค่าเซิร์ฟเวอร์ของอีเมล"
 thereIsUnresolvedAbuseReportWarning: "มีรายงานที่ยังไม่ได้แก้ไข"
 recommended: "แนะนำ"
 check: "ตรวจสอบ"
@@ -965,7 +982,7 @@ cannotUploadBecauseExceedsFileSizeLimit: "ไม่สามารถอัป
 beta: "เบต้า"
 enableAutoSensitive: "ทำเครื่องหมายว่ามีเนื้อหาที่ละเอียดอ่อนโดยอัตโนมัติ"
 enableAutoSensitiveDescription: "อนุญาตให้ตรวจหาและทำเครื่องหมายสื่อว่ามีเนื้อหาโดยละเอียดอ่อนโดยอัตโนมัติ ผ่าน Machine Learning หากเป็นไปได้ แม้ว่าคุณจะปิดคุณสมบัตินี้ ก็อาจถูกตั้งค่าโดยอัตโนมัติ ทั้งนี้ขึ้นอยู่กับเซิร์ฟเวอร์"
-activeEmailValidationDescription: "เปิดใช้งานการตรวจสอบที่อยู่อีเมลให้มีความเข้มงวดยิ่งขึ้น ซึ่งอาจจะรวมไปถึงการตรวจสอบที่อยู่อีเมล์ที่ใช้แล้วทิ้งและโดยให้พิจารณาว่าสามารถสื่อสารด้วยได้หรือไม่ เมื่อไม่เลือกระบบจะตรวจสอบเฉพาะรูปแบบของอีเมลเท่านั้น"
+activeEmailValidationDescription: "การตรวจสอบอีเมลของผู้ใช้จะเข้มงวดมากขึ้น โดยพิจารณาว่าเป็นอีเมลชั่วคราวหรือไม่ และสามารถติดต่อได้จริงหรือไม่ หากปิดการตรวจสอบนี้ จะตรวจสอบเพียงว่ารูปแบบอีเมลที่ถูกต้องหรือไม่เท่านั้น"
 navbar: "แถบนำทาง"
 shuffle: "สลับ"
 account: "บัญชีผู้ใช้"
@@ -974,7 +991,7 @@ pushNotification: "การแจ้งเตือนแบบพุช"
 subscribePushNotification: "เปิดการแจ้งเตือนแบบพุช"
 unsubscribePushNotification: "ปิดการแจ้งเตือนแบบพุช"
 pushNotificationAlreadySubscribed: "การแจ้งเตือนแบบพุชได้เปิดใช้งานแล้ว"
-pushNotificationNotSupported: "เบราว์เซอร์หรืออินสแตนซ์ของคุณนั้นไม่รองรับการแจ้งเตือนแบบพุช"
+pushNotificationNotSupported: "เบราว์เซอร์หรือเซิร์ฟเวอร์ไม่รองรับการแจ้งเตือนแบบพุช"
 sendPushNotificationReadMessage: "ลบการแจ้งเตือนแบบพุชเมื่ออ่านการแจ้งเตือนหรือข้อความที่เกี่ยวข้องแล้ว"
 sendPushNotificationReadMessageCaption: "อาจทำให้อุปกรณ์ของคุณใช้พลังงานมากขึ้น"
 windowMaximize: "ขยายใหญ่สุด"
@@ -1017,36 +1034,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 +1076,7 @@ retryAllQueuesNow: "ลองเรียกใช้คิวทั้งหม
 retryAllQueuesConfirmTitle: "ลองใหม่ทั้งหมดจริงๆหรอแน่ใจนะ?"
 retryAllQueuesConfirmText: "สิ่งนี้จะเพิ่มการโหลดเซิร์ฟเวอร์ชั่วคราวนะ"
 enableChartsForRemoteUser: "สร้างแผนภูมิข้อมูลผู้ใช้ระยะไกล"
-enableChartsForFederatedInstances: "สร้างแผนภูมิข้อมูลอินสแตนซ์ระยะไกล"
+enableChartsForFederatedInstances: "สร้างแผนภูมิของเซิร์ฟเวอร์ระยะไกล"
 showClipButtonInNoteFooter: "เพิ่ม “คลิป” ไปยังเมนูสั่งการของโน้ต"
 reactionsDisplaySize: "ขนาดของรีแอคชั่น"
 limitWidthOfReaction: "จำกัดความกว้างสูงสุดของรีแอคชั่นและแสดงให้เล็กลง"
@@ -1077,7 +1095,7 @@ addMemo: "เพิ่มเมโม"
 editMemo: "แก้ไขเมโม"
 reactionsList: "รายการรีแอคชั่น"
 renotesList: "รายการรีโน้ต"
-notificationDisplay: "การแจ้งเตือน"
+notificationDisplay: "การแสดงการแจ้งเตือน"
 leftTop: "บนซ้าย"
 rightTop: "บนขวา"
 leftBottom: "ล่างซ้าย"
@@ -1087,13 +1105,15 @@ vertical: "แนวตั้ง"
 horizontal: "แนวนอน"
 position: "ตำแหน่ง"
 serverRules: "กฎของเซิร์ฟเวอร์"
-pleaseConfirmBelowBeforeSignup: "โปรดยืนยันที่ด้านล่างก่อนสมัครใช้งาน"
+pleaseConfirmBelowBeforeSignup: "หากต้องการลงทะเบียนในเซิร์ฟเวอร์นี้ คุณต้องตรวจสอบและยอมรับสิ่งต่อไปนี้"
 pleaseAgreeAllToContinue: "คุณต้องยอมรับทุกช่องตรงด้านบนเพื่อดำเนินการต่อค่ะ"
 continue: "ดำเนินการต่อ"
 preservedUsernames: "ชื่อผู้ใช้ที่สงวนไว้"
 preservedUsernamesDescription: "ระบุชื่อผู้ใช้ที่จะสงวนชื่อไว้ คั่นด้วยการขึ้นบรรทัดใหม่ ชื่อผู้ใช้ที่ระบุที่นี่จะไม่สามารถใช้งานได้อีกต่อไปเมื่อสร้างบัญชีใหม่ ยกเว้นเมื่อผู้ดูแลระบบสร้างบัญชี นอกจากนี้ บัญชีที่มีอยู่แล้วจะไม่ได้รับผลกระทบ"
 createNoteFromTheFile: "เรียบเรียงโน้ตจากไฟล์นี้"
 archive: "เก็บถาวร"
+archived: "เก็บถาวรแล้ว"
+unarchive: "เลิกการเก็บถาวร"
 channelArchiveConfirmTitle: "ต้องการเก็บถาวรเจ้า {name} ใช่ไหม?"
 channelArchiveConfirmDescription: "เมื่อเก็บถาวรแล้ว จะไม่ปรากฏในรายการช่องหรือผลการค้นหาอีกต่อไป และจะไม่สามารถโพสต์ใหม่ได้อีกต่อไป"
 thisChannelArchived: "ช่องนี้ถูกเก็บถาวรแล้วนะ"
@@ -1104,6 +1124,9 @@ preventAiLearning: "ปฏิเสธการเรียนรู้ด้ว
 preventAiLearningDescription: "ส่งคำร้องขอไม่ให้ใช้ ข้อความในโน้ตที่โพสต์, หรือเนื้อหารูปภาพ ฯลฯ ในการเรียนรู้ของเครื่อง(machine learning) / Predictive AI / Generative AI โดยการเพิ่มแฟล็ก “noai” ลง HTML-Response ให้กับเนื้อหาที่เกี่ยวข้อง แต่ทั้งนี้ ไม่ได้ป้องกัน AI จากการเรียนรู้ได้อย่างสมบูรณ์ เนื่องจากมี AI บางตัวเท่านั้นที่จะเคารพคำขอดังกล่าว"
 options: "ตัวเลือกบทบาท"
 specifyUser: "ผู้ใช้เฉพาะ"
+lookupConfirm: "ต้องการเรียกดูข้อมูลใช่ไหม?"
+openTagPageConfirm: "ต้องการเปิดหน้าแฮชแท็กใช่ไหม?"
+specifyHost: "ระบุโฮสต์"
 failedToPreviewUrl: "ไม่สามารถดูตัวอย่างได้"
 update: "อัปเดต"
 rolesThatCanBeUsedThisEmojiAsReaction: "บทบาทที่สามารถใช้เอโมจินี้เป็นรีแอคชั่นได้"
@@ -1157,7 +1180,7 @@ notifyNotes: "แจ้งเตือนเกี่ยวกับโพสต
 unnotifyNotes: "หยุดการแจ้งเตือนเกี่ยวกับโน้ตใหม่"
 authentication: "การตรวจสอบสิทธิ์"
 authenticationRequiredToContinue: "กรุณายืนยันตัวตนทางอิเล็กทรอนิกส์เพื่อดำเนินการต่อ"
-dateAndTime: "เวลาประทับ"
+dateAndTime: "วันเวลา"
 showRenotes: "แสดงรีโน้ต"
 edited: "แก้ไขแล้ว"
 notificationRecieveConfig: "การตั้งค่าการแจ้งเตือน"
@@ -1168,11 +1191,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 +1250,7 @@ surrender: "ยอมแพ้"
 gameRetry: "เริ่มเกมใหม่"
 notUsePleaseLeaveBlank: "หากไม่ได้ใช้กรุณาเว้นว่างไว้"
 useTotp: "ใช้รหัสผ่านแบบใช้ครั้งเดียว (TOTP)"
-useBackupCode: "ใช้รหัสสำรอง"
+useBackupCode: "ใช้รหัสแบ๊กอัป"
 launchApp: "เริ่มแอป"
 useNativeUIForVideoAudioPlayer: "ใช้ UI ของเบราว์เซอร์เพื่อเล่นวิดีโอ/เสียง"
 keepOriginalFilename: "คงชื่อไฟล์เดิมไว้"
@@ -1235,13 +1258,23 @@ keepOriginalFilenameDescription: "หากปิดการตั้งค่
 noDescription: "ไม่มีข้อความอธิบาย"
 alwaysConfirmFollow: "แสดงข้อความยืนยันเมื่อกดติดตาม"
 inquiry: "ติดต่อเรา"
+tryAgain: "โปรดลองอีกครั้ง"
+confirmWhenRevealingSensitiveMedia: "ตรวจสอบก่อนแสดงสื่อที่มีเนื้อหาละเอียดอ่อน"
+sensitiveMediaRevealConfirm: "สื่อนี้มีเนื้อหาละเอียดอ่อน, ต้องการแสดงใช่ไหม?"
+createdLists: "รายชื่อที่ถูกสร้าง"
+createdAntennas: "เสาอากาศที่ถูกสร้าง"
 _delivery:
-  stop: "ถูกระงับ"
+  status: "สถานะการจัดส่ง"
+  stop: "ระงับการส่ง"
+  resume: "จัดส่งต่อ"
   _type:
     none: "กำลังเผยแพร่"
+    manuallySuspended: "หยุดชั่วคราวด้วยตนเอง"
+    goneSuspended: "เซิร์ฟเวอร์ถูกระงับเนื่องจากมีการลบเซิร์ฟเวอร์นี้"
+    autoSuspendedForNotResponding: "เซิร์ฟเวอร์ถูกระงับเนื่องจากไม่ตอบสนอง"
 _bubbleGame:
   howToPlay: "วิธีเล่น"
-  hold: "หยุดชั่วคราว"
+  hold: "ถือไว้"
   _score:
     score: "คะแนน"
     scoreYen: "จำนวนเงินที่ได้รับ"
@@ -1255,27 +1288,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 +1343,7 @@ _initialTutorial:
     description1: "Misskey มีหลายไทม์ไลน์ขึ้นอยู่กับวิธีการใช้งานของคุณ (บางไทม์ไลน์อาจไม่สามารถใช้ได้ขึ้นอยู่กับนโยบายของเซิร์ฟเวอร์)"
     home: "คุณสามารถดูโพสต์จากบัญชีที่คุณติดตามได้"
     local: "คุณสามารถดูโพสต์จากผู้ใช้ทั้งหมดบนเซิร์ฟเวอร์นี้"
-    social: "โพสต์จากทั้งไทม์ไลน์หน้าแรกและไทม์ไลน์ในพื้นที่ของคุณจะปรากฏขึ้น"
+    social: "จะแสดงโพสต์ทั้งจากไทม์ไลน์หลักและไทม์ไลน์ท้องถิ่น"
     global: "คุณสามารถดูโพสต์จากเซิร์ฟเวอร์ที่เชื่อมต่ออื่นๆ ทั้งหมดได้"
     description2: "คุณสามารถสลับระหว่างแต่ละไทม์ไลน์ได้ตลอดเวลาได้ที่บริเวณด้านบนของหน้าจอ"
     description3: "นอกจากนี้ยังมีรายการไทม์ไลน์ ไทม์ไลน์ของช่อง ฯลฯ โปรดดู {link} สำหรับรายละเอียดเพิ่มเติม"
@@ -1320,7 +1353,7 @@ _initialTutorial:
     _visibility:
       description: "คุณสามารถจำกัดผู้ที่สามารถดูโน้ตของคุณได้นะ"
       public: "โน้ตของคุณนั้นจะปรากฏแก่ผู้ใช้งานทุกคน"
-      home: "เผยแพร่บนไทม์ไลน์หน้าแรกเท่านั้น ผู้คนที่เข้าชมโปรไฟล์ของคุณ ผ่านผู้ติดตาม และผ่านการรีโน้ตสามารถเห็นได้"
+      home: "เผยแพร่บนไทม์ไลน์หลักเท่านั้น แต่ผู้ติดตาม ผู้ที่เข้ามาดูโปรไฟล์ และผู้ที่เห็นจากรีโน้ตยังสามารถดูโพสต์นี้ได้"
       followers: "มองเห็นได้เฉพาะผู้ติดตามเท่านั้น ไม่มีใครอื่นนอกจากตัวคุณเองที่สามารถรีโน้ตได้ และมีเพียงผู้ติดตามของคุณเท่านั้นที่สามารถดูได้"
       direct: "เปิดให้เห็นเฉพาะผู้ใช้ที่ระบุเท่านั้น และพวกเขาจะได้รับแจ้งเตือนด้วย คุณสามารถใช้มันแทนข้อความโดยตรง (dm)"
       doNotSendConfidencialOnDirect1: "โปรดใช้ความระมัดระวังในการส่งข้อมูลที่ละเอียดอ่อน"
@@ -1328,9 +1361,9 @@ _initialTutorial:
       localOnly: "การโพสต์ด้วย flag นี้จะไม่รวมโน้ตไปยังเซิร์ฟเวอร์อื่น ผู้ใช้บนเซิร์ฟเวอร์อื่นจะไม่สามารถดูโน้ตเหล่านี้ได้โดยตรง โดยไม่คำนึงถึงการตั้งค่าการแสดงผลข้างต้น"
     _cw:
       title: "คำเตือนเกี่ยวกับเนื้อหา"
-      description: "เนื้อหาที่เขียนด้วย “คำอธิบายประกอบ” จะแสดงแทนข้อความหลัก คลิก “ดูเพิ่มเติม” เพื่อแสดงข้อความเต็ม"
+      description: "เนื้อหาที่เขียนใน “คำอธิบายประกอบ” จะแสดงแทนเนื้อหาหลัก ต้องคลิก “ดูเพิ่มเติม” เพื่อให้เนื้อหาหลักแสดง"
       _exampleNote:
-        cw: "นี่อาจจะทำให้คุณหิวอย่างแน่นอน!"
+        cw: " ห้ามดู ระวังหิว"
         note: "เพิ่งไปกินโดนัทเคลือบช็อคโกแลตมา 🍩😋"
       useCases: "ใช้สิ่งนี้เพื่อระบุโน้ตที่ต้องตามแนวทางปฏิบัติของเซิร์ฟเวอร์ หรือเพื่อควบคุมการสปอยล์และข้อความที่ละเอียดอ่อนด้วยตนเอง"
   _howToMakeAttachmentsSensitive:
@@ -1346,17 +1379,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 +1397,29 @@ _serverSettings:
   fanoutTimelineDescription: "เพิ่มประสิทธิภาพการดึงข้อมูลไทม์ไลน์อย่างมาก และลดภาระในฐานข้อมูลเมื่อเปิดใช้งาน ในทางกลับกัน การใช้หน่วยความจำของ Redis จะเพิ่มขึ้น ลองปิดการใช้งานนี้ในกรณีที่หน่วยความจำเซิร์ฟเวอร์เหลือน้อยหรือเซิร์ฟเวอร์ไม่เสถียร"
   fanoutTimelineDbFallback: "ฟอลแบ๊กกลับฐานข้อมูล"
   fanoutTimelineDbFallbackDescription: "เมื่อเปิดใช้งาน หากไม่ได้แคชไทม์ไลน์ ไทม์ไลน์จะฟอลแบ๊กไปยังฐานข้อมูลสำหรับการ query เพิ่มเติม การปิดใช้งานจะช่วยลดภาระของเซิร์ฟเวอร์ด้วยการกำจัดกระบวนฟอลแบ๊ก แต่มันก็จะจำกัดช่วงเวลาไทม์ไลน์ที่สามารถดึงข้อมูลได้"
+  inquiryUrl: "URL สำหรับการติดต่อสอบถาม"
+  inquiryUrlDescription: "ระบุ URL ของหน้าเว็บที่มีแบบฟอร์มสำหรับติดต่อผู้ดูแลเซิร์ฟเวอร์ หรือข้อมูลการติดต่อของผู้ดูแลเซิร์ฟเวอร์"
 _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 shonk"
-      description: "โพสต์โน้ตแรกของคุณ"
+      description: "โพสต์โน้ตเป็นครั้งแรก"
       flavor: "ขอให้มีช่วงเวลาที่ดีกับ Misskey นะคะ!"
     _notes10:
       title: "โน้ตไม่กี่ชิ้น"
@@ -1444,15 +1479,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 +1519,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 +1574,7 @@ _achievements:
       description: "ได้รับความสำเร็จ 30 ครั้ง"
     _viewAchievements3min:
       title: "ชอบบรรลุความสําเร็จ"
-      description: "มองดูรายการความสำเร็จของคุณเป็นเวลาอย่างน้อย 3 นาที"
+      description: "มองดูรายการความสำเร็จเป็นเวลานานกว่า 3 นาที"
     _iLoveMisskey:
       title: "ฉันรัก Misskey"
       description: "โพสต์ “I ❤ #Misskey”"
@@ -1566,13 +1601,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 +1628,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 +1672,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 +1685,8 @@ _role:
   descriptionOfIsPublic: "บทบาทจะปรากฏบนโปรไฟล์ของผู้ใช้และเปิดเผยต่อสาธารณะ (ทุกคนสามารถเห็นได้ว่าผู้ใช้รายนี้มีบทบาทนี้)"
   options: "ตัวเลือกบทบาท"
   policies: "นโยบาย"
-  baseRole: "เทมเพลตบทบาท"
-  useBaseValue: "ใช้ตามเทมเพลตบทบาท"
+  baseRole: "แม่แบบบทบาท"
+  useBaseValue: "ใช้ตามแม่แบบบทบาท"
   chooseRoleToAssign: "เลือกบทบาทที่ต้องการกำหนด"
   iconUrl: "URL ไอคอน"
   asBadge: "แสดงเป็นตรา"
@@ -1668,11 +1703,11 @@ _role:
     middle: "ปานกลาง"
     high: "สูง"
   _options:
-    gtlAvailable: "การดูไทม์ไลน์ทั่วโลก"
-    ltlAvailable: "การดูไทม์ไลน์ในท้องถิ่น"
+    gtlAvailable: "สามารถดูไทม์ไลน์ทั่วโลกได้"
+    ltlAvailable: "สามารถดูไทม์ไลน์ท้องถิ่นได้"
     canPublicNote: "สามารถโพสต์แบบสาธารณะ"
     mentionMax: "จำนวนการกล่าวถึงสูงสุดต่อโน้ต"
-    canInvite: "สร้างรหัสเชิญอินสแตนซ์"
+    canInvite: "สร้างรหัสเชิญเข้าเซิร์ฟเวอร์"
     inviteLimit: "จำกัดการเชิญ"
     inviteLimitCycle: "คูลดาวน์ในการเชิญ"
     inviteExpirationTime: "วันหมดอายุของรหัสการเชิญ"
@@ -1680,6 +1715,7 @@ _role:
     canManageAvatarDecorations: "จัดการตกแต่งอวตาร"
     driveCapacity: "ความจุของไดรฟ์"
     alwaysMarkNsfw: "ทำเครื่องหมายไฟล์ว่าเป็น NSFW เสมอ"
+    canUpdateBioMedia: "อนุญาตให้ปรับปรุงไอคอนและแบนเนอร์"
     pinMax: "จํานวนสูงสุดของโน้ตที่ปักหมุดไว้"
     antennaMax: "จำนวนสูงสุดของเสาอากาศ"
     wordMuteMax: "จำนวนอักขระสูงสุดที่อนุญาตในการปิดเสียงคำ"
@@ -1696,7 +1732,7 @@ _role:
     avatarDecorationLimit: "จำนวนการตกแต่งไอคอนสูงสุดที่สามารถติดตั้งได้"
   _condition:
     roleAssignedTo: "มอบหมายให้มีบทบาทแบบทำมือ"
-    isLocal: "ผู้ใช้ในพื้นที่"
+    isLocal: "ผู้ใช้ท้องถิ่น"
     isRemote: "ผู้ใช้ระยะไกล"
     isCat: "ผู้ใช้ที่เป็นแมว"
     isBot: "ผู้ใช้ที่เป็นบอต"
@@ -1755,8 +1791,8 @@ _ad:
   adsTooClose: "เนื่องจากช่วงเวลาการแสดงโฆษณาสั้นมาก ประสบการณ์ผู้ใช้จึงอาจลดลงอย่างมาก"
 _forgotPassword:
   enterEmail: "ป้อนที่อยู่อีเมลที่คุณเคยใช้ในการลงทะเบียนไว้ ลิงก์ที่คุณสามารถรีเซ็ตรหัสผ่านได้นั้นจะถูกส่งไปนะ"
-  ifNoEmail: "ถ้าหากคุณไม่ได้ใช้อีเมลระหว่างการลงทะเบียน กรุณาติดต่อผู้ดูแลระบบอินสแตนซ์แทนนะ"
-  contactAdmin: "อินสแตนซ์นี้ไม่รองรับการใช้งานที่อยู่อีเมลนี้ กรุณาติดต่อผู้ดูแลระบบอินสแตนซ์เพื่อรีเซ็ตรหัสผ่านของคุณแทน"
+  ifNoEmail: "หากลงทะเบียนแบบไม่ใช้อีเมล โปรดติดต่อผู้ดูแลระบบ"
+  contactAdmin: "เนื่องจากเซิร์ฟเวอร์นี้ไม่รองรับการส่งอีเมล หากต้องการรีเซ็ตรหัสผ่าน กรุณาติดต่อผู้ดูแลระบบ"
 _gallery:
   my: "แกลลอรี่ของฉัน"
   liked: "โพสต์ที่ถูกใจ"
@@ -1774,23 +1810,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 +1877,13 @@ _menuDisplay:
   hide: "ซ่อน"
 _wordMute:
   muteWords: "ปิดเสียงคำ"
-  muteWordsDescription: "คั่นด้วยช่องว่างสำหรับเงื่อนไข AND หรือด้วยการขึ้นบรรทัดใหม่สำหรับเงื่อนไข OR นะ"
+  muteWordsDescription: "คั่นด้วยเว้นวรรคสำหรับเงื่อนไข AND, หรือขึ้นบรรทัดใหม่สำหรับเงื่อนไข OR"
   muteWordsDescription2: "ล้อมรอบคีย์เวิร์ดด้วยเครื่องหมายทับเพื่อใช้นิพจน์ทั่วไป"
 _instanceMute:
-  instanceMuteDescription: "การดำเนินการนี้จะปิดเสียง\"โน้ต/รีโน้ต\"จากอินสแตนซ์ที่อยู่ในรายการ รวมถึงบันทึกของผู้ใช้ที่ตอบกลับผู้ใช้จากอินสแตนซ์ที่ปิดเสียง"
+  instanceMuteDescription: "ปิดเสียง “โน้ต/รีโน้ต” ทั้งหมดจากเซิร์ฟเวอร์ที่ระบุไว้ รวมถึงโน้ตของผู้ใช้ที่ตอบกลับผู้ใช้จากเซิร์ฟเวอร์ที่ถูกปิดเสียง"
   instanceMuteDescription2: "คั่นด้วยการขึ้นบรรทัดใหม่"
-  title: "ซ่อนโน้ตจากอินสแตนซ์ที่มีอยู่ในรายชื่อ"
-  heading: "รายชื่ออินสแตนซ์ที่ถูกปิดเสียง"
+  title: "ซ่อนโน้ตจากเซิร์ฟเวอร์ที่มีระบุไว้"
+  heading: "เซิร์ฟเวอร์ที่ถูกปิดเสียง"
 _theme:
   explore: "สำรวจธีม"
   install: "ติดตั้งธีม"
@@ -1923,8 +1959,6 @@ _sfx:
   note: "โน้ต"
   noteMy: "โน้ตของตัวเอง"
   notification: "การเเจ้งเตือน"
-  antenna: "เสาอากาศ"
-  channel: "การแจ้งเตือนช่อง"
   reaction: "เมื่อเลือกรีแอคชั่น"
 _soundSettings:
   driveFile: "ใช้เสียงจากไดรฟ์"
@@ -1932,7 +1966,8 @@ _soundSettings:
   driveFileTypeWarn: "ไม่รองรับไฟล์นี้"
   driveFileTypeWarnDescription: "กรุณาเลือกไฟล์เสียง"
   driveFileDurationWarn: "เสียงยาวเกินไป"
-  driveFileDurationWarnDescription: "การใช้เสียงที่ยาวอาจรบกวนการใช้งาน Misskey, ต้องการดำเนินการต่อหรือไม่?"
+  driveFileDurationWarnDescription: "การใช้เสียงที่ยาว อาจรบกวนการใช้งาน Misskey, ต้องการดำเนินการต่อใช่ไหม?"
+  driveFileError: "ไม่สามารถโหลดไฟล์เสียงได้ กรุณาเปลี่ยนแปลงการตั้งค่า"
 _ago:
   future: "อนาคต"
   justNow: "เมื่อกี๊นี้"
@@ -1976,49 +2011,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 +2062,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 +2102,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 +2132,7 @@ _weekday:
   saturday: "วันเสาร์"
 _widgets:
   profile: "โปรไฟล์"
-  instanceInfo: "ข้อมูล อินสแตนซ์"
+  instanceInfo: "ข้อมูลเซิร์ฟเวอร์"
   memo: "โน้ตแปะ"
   notifications: "การเเจ้งเตือน"
   timeline: "ไทม์ไลน์"
@@ -2111,7 +2146,7 @@ _widgets:
   digitalClock: "นาฬิกาดิจิตอล"
   unixClock: "นาฬิกา UNIX"
   federation: "สหพันธ์"
-  instanceCloud: "อินสแตนซ์คลาวด์"
+  instanceCloud: "กลุ่มเมฆเซิร์ฟเวอร์"
   postForm: "แบบฟอร์มการโพสต์"
   slideshow: "แสดงภาพนิ่ง"
   button: "ปุ่ม"
@@ -2120,7 +2155,7 @@ _widgets:
   serverMetric: "ตัวชี้วัดเซิร์ฟเวอร์"
   aiscript: " คอนโซล AiScript"
   aiscriptApp: "แอป AiScript"
-  aichan: "ไอ"
+  aichan: "藍 (ไอ)"
   userList: "รายชื่อผู้ใช้"
   _userList:
     chooseList: "เลือกรายชื่อ"
@@ -2144,7 +2179,7 @@ _poll:
   deadlineTime: "เวลา"
   duration: "ระยะเวลา"
   votesCount: "{n} คะแนนเสียง"
-  totalVotes: "{n} คะแนนเสียงทั้งหมด"
+  totalVotes: "ทั้งหมด {n} คะแนนเสียง"
   vote: "โหวต"
   showResult: "ดูผลลัพธ์"
   voted: "โหวตแล้ว"
@@ -2156,14 +2191,14 @@ _poll:
 _visibility:
   public: "สาธารณะ"
   publicDescription: "โน้ตของคุณจะปรากฏแก่ผู้ใช้ทุกคน"
-  home: "หน้าแรก"
-  homeDescription: "โพสลงไทม์ไลน์ที่บ้านเท่านั้น"
+  home: "หน้าหลัก"
+  homeDescription: "โพสต์ลงไทม์ไลน์หลักเท่านั้น"
   followers: "ผู้ติดตาม"
   followersDescription: "เฉพาะผู้ติดตามเท่านั้นที่มองเห็นได้"
   specified: "ไดเร็ค"
   specifiedDescription: "ทำให้มองเห็นได้เฉพาะผู้ใช้ที่ระบุเท่านั้น"
-  disableFederation: "ไม่มีสหพันธ์"
-  disableFederationDescription: "อย่าส่งไปยังอินสแตนซ์อื่น"
+  disableFederation: "การปิดใช้งานสหพันธ์"
+  disableFederationDescription: "อย่าส่งข้อมูลไปยังเซิร์ฟเวอร์อื่น"
 _postForm:
   replyPlaceholder: "ตอบกลับโน้ตนี้..."
   quotePlaceholder: "อ้างโน้ตนี้..."
@@ -2199,37 +2234,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 +2280,7 @@ _play:
   featured: "เป็นที่นิยม"
   title: "หัวข้อ"
   script: "สคริปต์"
-  summary: "รายละเอียด"
+  summary: "คำอธิบาย"
   visibilityDescription: "หากตั้งค่าเป็นส่วนตัว มันจะไม่ปรากฏในโปรไฟล์อีกต่อไป แต่ผู้ที่ทราบ URL ของมันจะยังสามารถเข้าถึงได้"
 _pages:
   newPage: "สร้างหน้าเพจใหม่"
@@ -2333,7 +2368,7 @@ _notification:
     mention: "กล่าวถึง"
     reply: "ตอบกลับ"
     renote: "รีโน้ต"
-    quote: "อ้างคำพูด"
+    quote: "อ้างอิง"
     reaction: "รีแอคชั่น"
     pollEnded: "โพลสิ้นสุดแล้ว"
     receiveFollowRequest: "ได้รับคำร้องขอติดตาม"
@@ -2349,6 +2384,7 @@ _deck:
   alwaysShowMainColumn: "แสดงคอลัมน์หลักเสมอ"
   columnAlign: "จัดแนวคอลัมน์"
   addColumn: "เพิ่มคอลัมน์"
+  newNoteNotificationSettings: "ตั้งค่าการแจ้งเตือนเมื่อมีโน้ตใหม่"
   configureColumn: "ตั้งค่าคอลัมน์"
   swapLeft: "ขยับไปทางซ้าย"
   swapRight: "ขยับไปทางขวา"
@@ -2373,7 +2409,7 @@ _deck:
     antenna: "เสาอากาศ"
     list: "รายการ"
     channel: "ช่อง"
-    mentions: "พูดถึง"
+    mentions: "กล่าวถึงคุณ"
     direct: "ไดเร็กต์"
     roleTimeline: "บทบาทไทม์ไลน์"
 _dialog:
@@ -2387,9 +2423,10 @@ _drivecleaner:
   orderByCreatedAtAsc: "วันที่จากน้อยไปหามาก"
 _webhookSettings:
   createWebhook: "สร้าง Webhook"
+  modifyWebhook: "แก้ไข Webhook"
   name: "ชื่อ"
   secret: "ความลับ"
-  events: "อีเว้นท์ Webhook"
+  trigger: "ทริกเกอร์"
   active: "เปิดใช้งาน"
   _events:
     follow: "เมื่อกำลังติดตามผู้ใช้"
@@ -2399,6 +2436,26 @@ _webhookSettings:
     renote: "รีโน้ตแล้วเมื่อ"
     reaction: "เมื่อได้รับรีแอคชั่น"
     mention: "เมื่อกำลังถูกกล่าวถึง"
+  _systemEvents:
+    abuseReport: "เมื่อมีการรายงานจากผู้ใช้"
+    abuseReportResolved: "เมื่อมีการจัดการกับการรายงานจากผู้ใช้"
+    userCreated: "เมื่อผู้ใช้ถูกสร้างขึ้น"
+  deleteConfirm: "ต้องการลบ Webhook ใช่ไหม?"
+_abuseReport:
+  _notificationRecipient:
+    createRecipient: "เพิ่มปลายทางการแจ้งเตือนการรายงาน"
+    modifyRecipient: "แก้ไขปลายทางการแจ้งเตือนการรายงาน"
+    recipientType: "ประเภทของปลายทางการแจ้งเตือน\n"
+    _recipientType:
+      mail: "อีเมล"
+      webhook: "Webhook"
+      _captions:
+        mail: "ส่งการแจ้งเตือนไปยังที่อยู่อีเมลของผู้ควบคุม (เฉพาะเมื่อได้รับการรายงาน)"
+        webhook: "ส่งการแจ้งเตือนไปยัง SystemWebhook ที่กำหนด (จะส่งเมื่อได้รับการรายงานและเมื่อการรายงานได้รับการแก้ไข)"
+    keywords: "คีย์เวิร์ด"
+    notifiedUser: "ผู้ใช้ที่ได้รับการแจ้งเตือน"
+    notifiedWebhook: "Webhook ที่ใช้"
+    deleteConfirm: "ต้องการลบปลายทางการแจ้งเตือนใช่ไหม?"
 _moderationLogTypes:
   createRole: "สร้างบทบาทแล้ว"
   deleteRole: "ลบบทบาทแล้ว"
@@ -2421,9 +2478,9 @@ _moderationLogTypes:
   deleteGlobalAnnouncement: "ลบประกาศทั่วโลกออกแล้ว"
   deleteUserAnnouncement: "ลบประกาศผู้ใช้ออกแล้ว"
   resetPassword: "รีเซ็ตรหัสผ่าน"
-  suspendRemoteInstance: "ระงับอินสแตนซ์ระยะไกล"
-  unsuspendRemoteInstance: "เลิกระงับอินสแตนซ์ระยะไกล"
-  updateRemoteInstanceNote: "อัปเดตโน้ตการกลั่นกรองของอินสแตนซ์ระยะไกลแล้ว"
+  suspendRemoteInstance: "ระงับเซิร์ฟเวอร์ระยะไกล"
+  unsuspendRemoteInstance: "เลิกระงับเซิร์ฟเวอร์ระยะไกล"
+  updateRemoteInstanceNote: "อัปเดตโน้ตการกลั่นกรองสำหรับเซิร์ฟเวอร์ระยะไกลแล้ว"
   markSensitiveDriveFile: "ทำเครื่องหมายไฟล์ว่ามีเนื้อหาละเอียดอ่อน"
   unmarkSensitiveDriveFile: "ยกเลิกทำเครื่องหมายไฟล์ว่ามีเนื้อหาละเอียดอ่อน"
   resolveAbuseReport: "รายงานได้รับการแก้ไขแล้ว"
@@ -2436,6 +2493,12 @@ _moderationLogTypes:
   deleteAvatarDecoration: "ลบการตกแต่งไอคอนแล้ว"
   unsetUserAvatar: "ลบไอคอนผู้ใช้"
   unsetUserBanner: "ลบแบนเนอร์ผู้ใช้"
+  createSystemWebhook: "สร้าง SystemWebhook"
+  updateSystemWebhook: "อัปเดต SystemWebhook"
+  deleteSystemWebhook: "ลบ SystemWebhook"
+  createAbuseReportNotificationRecipient: "สร้างปลายทางการแจ้งเตือนการรายงาน"
+  updateAbuseReportNotificationRecipient: "อัปเดตปลายทางการแจ้งเตือนการรายงาน"
+  deleteAbuseReportNotificationRecipient: "ลบปลายทางการแจ้งเตือนการรายงาน"
 _fileViewer:
   title: "รายละเอียดไฟล์"
   type: "ประเภทไฟล์"
@@ -2448,10 +2511,10 @@ _externalResourceInstaller:
   title: "ติดตั้งจากไซต์ภายนอก"
   checkVendorBeforeInstall: "โปรดตรวจสอบให้แน่ใจว่าแหล่งแจกหน่ายมีความน่าเชื่อถือก่อนทำการติดตั้ง"
   _plugin:
-    title: "ต้องการติดตั้งปลั๊กอินนี้หรือไม่?"
+    title: "ต้องการติดตั้งปลั๊กอินนี้ใช่ไหม?"
     metaTitle: "ข้อมูลส่วนเสริม"
   _theme:
-    title: "ต้องการติดตั้งธีมนี้หรือไม่?"
+    title: "ต้องการติดตั้งธีมนี้ใช่ไหม?"
     metaTitle: "ข้อมูลธีม"
   _meta:
     base: "โทนสีพื้นฐาน"
@@ -2487,7 +2550,7 @@ _externalResourceInstaller:
       description: "เกิดปัญหาระหว่างการติดตั้งธีม กรุณาลองอีกครั้ง. รายละเอียดข้อผิดพลาดสามารถดูได้ในคอนโซล Javascript"
 _dataSaver:
   _media:
-    title: "โหลดมีเดีย"
+    title: "โหลดสื่อ"
     description: "กันไม่ให้ภาพและวิดีโอโหลดโดยอัตโนมัติ แตะรูปภาพ/วิดีโอที่ซ่อนอยู่เพื่อโหลด"
   _avatar:
     title: "รูปไอคอน"
@@ -2567,3 +2630,8 @@ _mediaControls:
   pip: "รูปภาพในรูปภาม"
   playbackRate: "ความเร็วในการเล่น"
   loop: "เล่นวนซ้ำ"
+_contextMenu:
+  title: "เมนูเนื้อหา"
+  app: "แอปพลิเคชัน"
+  appWithShift: "แอปฟลิเคชันด้วยปุ่มยกแคร่ (Shift)"
+  native: "UI ของเบราว์เซอร์"
diff --git a/locales/tr-TR.yml b/locales/tr-TR.yml
index cf6729a81d..266fb3161c 100644
--- a/locales/tr-TR.yml
+++ b/locales/tr-TR.yml
@@ -359,7 +359,7 @@ smtpUser: "Kullanıcı Adı"
 smtpPass: "Şifre"
 notificationSetting: "Bildirim ayarları"
 instanceTicker: "Notların sunucu bilgileri"
-noCrawleDescription: "Arama motorlarından profilinde, notlarında, sayfalarında  vb. dolaşılmamasını ve dizine eklememesini talep et."
+noCrawleDescription: "Arama motorlarından profilinde, notlarında, sayfalarında vb. dolaşılmamasını ve dizine eklememesini talep et."
 clearCache: "Ön belleği temizle"
 onlineUsersCount: "{n} kullanıcı çevrim içi"
 user: "Kullanıcı"
diff --git a/locales/uk-UA.yml b/locales/uk-UA.yml
index 661ecf19d7..36d741d30e 100644
--- a/locales/uk-UA.yml
+++ b/locales/uk-UA.yml
@@ -1318,8 +1318,6 @@ _sfx:
   note: "Нотатки"
   noteMy: "Мої нотатки"
   notification: "Сповіщення"
-  antenna: "Прийом антени"
-  channel: "Повідомлення каналу"
 _ago:
   future: "Майбутнє"
   justNow: "Щойно"
@@ -1622,6 +1620,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 306705e42e..2023771104 100644
--- a/locales/uz-UZ.yml
+++ b/locales/uz-UZ.yml
@@ -443,7 +443,7 @@ text: "Matn"
 enable: "Yoqish"
 next: "Keyingisi"
 retype: "Qayta kiriting"
-noteOf: "{user} tomonidan  joylandi\n"
+noteOf: "{user} tomonidan joylandi\n"
 quoteAttached: "Iqtibos"
 quoteQuestion: "Iqtibos sifatida qo'shilsinmi?"
 noMessagesYet: "Bu yerda xabarlar yo'q"
@@ -1089,6 +1089,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 cf6eafd69f..87b4403c27 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"
@@ -1285,7 +1288,7 @@ _achievements:
     _iLoveMisskey:
       title: "Tôi Yêu Misskey"
       description: "Đăng lời nói \"I ❤ #Misskey\""
-      flavor: "Xin chân thành cảm ơn bạn đã sử dụng Misskey!!  by Đội ngũ phát triển"
+      flavor: "Xin chân thành cảm ơn bạn đã sử dụng Misskey!! by Đội ngũ phát triển"
     _foundTreasure:
       title: "Tìm kiếm kho báu"
       description: "Tìm thấy được những kho báu cất giấu"
@@ -1326,7 +1329,7 @@ _achievements:
       description: "Bấm chỗ này"
     _justPlainLucky:
       title: "Chỉ là một cuộc máy mắn"
-      description: "Mỗi 10 giây thu nhận được với tỷ lệ  0.005%"
+      description: "Mỗi 10 giây thu nhận được với tỷ lệ 0.005%"
     _setNameToSyuilo:
       title: "Ngưỡng mộ với vị thần"
       description: "Đạt tên là syuilo"
@@ -1480,7 +1483,7 @@ _wordMute:
   muteWordsDescription: "Separate with spaces for an AND condition or with line breaks for an OR condition."
   muteWordsDescription2: "Bao quanh các từ khóa bằng dấu gạch chéo để sử dụng cụm từ thông dụng."
 _instanceMute:
-  instanceMuteDescription: "Thao tác này sẽ ẩn mọi tút/lượt đăng lại từ các máy chủ được liệt kê, bao gồm cả những tút  dạng trả lời từ máy chủ bị ẩn."
+  instanceMuteDescription: "Thao tác này sẽ ẩn mọi tút/lượt đăng lại từ các máy chủ được liệt kê, bao gồm cả những tút dạng trả lời từ máy chủ bị ẩn."
   instanceMuteDescription2: "Tách bằng cách xuống dòng"
   title: "Ẩn tút từ những máy chủ đã liệt kê."
   heading: "Danh sách những máy chủ bị ẩn"
@@ -1559,8 +1562,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"
@@ -1917,11 +1918,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 7d1148909f..1bc1df7930 100644
--- a/locales/zh-CN.yml
+++ b/locales/zh-CN.yml
@@ -60,6 +60,7 @@ copyFileId: "复制文件ID"
 copyFolderId: "复制文件夹ID"
 copyProfileUrl: "复制个人资料URL"
 searchUser: "搜索用户"
+searchThisUsersNotes: "搜索用户帖子"
 reply: "回复"
 loadMore: "查看更多"
 showMore: "查看更多"
@@ -154,6 +155,7 @@ editList: "编辑列表"
 selectChannel: "选择频道"
 selectAntenna: "选择天线"
 editAntenna: "编辑天线"
+createAntenna: "创建天线"
 selectWidget: "选择小工具"
 editWidgets: "编辑部件"
 editWidgetsExit: "完成编辑"
@@ -180,6 +182,10 @@ addAccount: "添加账户"
 reloadAccountsList: "更新账户列表"
 loginFailed: "登录失败"
 showOnRemote: "转到所在服务器显示"
+continueOnRemote: "转到所在服务器继续"
+chooseServerOnMisskeyHub: "从 Misskey Hub 选择服务器"
+specifyServerHost: "直接输入服务器域名"
+inputHostName: "请输入域名"
 general: "常规设置"
 wallpaper: "壁纸"
 setWallpaper: "设置壁纸"
@@ -190,6 +196,7 @@ followConfirm: "你确定要关注 {name} 吗?"
 proxyAccount: "代理账户"
 proxyAccountDescription: "代理账户是在某些情况下替代用户进行远程关注用的账户。 例如说,当用户将一位远程用户放入一个列表中时,如果本地服务器上没有任何人关注这位远程用户,则这位远程用户的账户活动将不会被送到本地服务器上。作为替代,此时将使用代理账户进行关注。"
 host: "主机名"
+selectSelf: "选择自己"
 selectUser: "选择用户"
 recipient: "收件人"
 annotation: "注解"
@@ -205,6 +212,7 @@ perDay: "每天"
 stopActivityDelivery: "停止发送活动"
 blockThisInstance: "阻止此服务器向本服务器推流"
 silenceThisInstance: "使服务器静音"
+mediaSilenceThisInstance: "隐藏此服务器的媒体文件"
 operations: "操作"
 software: "软件"
 version: "版本"
@@ -223,9 +231,11 @@ clearQueueConfirmText: "未送达的帖子将不会被投递。 通常无需执
 clearCachedFiles: "清除缓存"
 clearCachedFilesConfirm: "确定要清除所有缓存的远程文件?"
 blockedInstances: "被封锁的服务器"
-blockedInstancesDescription: "设定要封锁的服务器,以换行来进行分割。被封锁的服务器将无法与本服务器进行交换通讯。子域名也同样会被封锁。"
+blockedInstancesDescription: "设定要封锁的服务器,以换行分隔。被封锁的服务器将无法与本服务器进行交换通讯。子域名也同样会被封锁。"
 silencedInstances: "被静音的服务器"
-silencedInstancesDescription: "设置要静音的服务器,以换行符分隔。被静音的服务器内所有的账户将默认处于「静音」状态,仅能发送关注请求,并且在未关注状态下无法提及本地账户。被阻止的实例不受影响。"
+silencedInstancesDescription: "设置要静音的服务器,以换行分隔。被静音的服务器内所有的账户将默认处于「静音」状态,仅能发送关注请求,并且在未关注状态下无法提及本地账户。被阻止的实例不受影响。"
+mediaSilencedInstances: "已隐藏媒体文件的服务器"
+mediaSilencedInstancesDescription: "设置要隐藏媒体文件的服务器,以换行分隔。被设置为隐藏媒体文件服务器内所有账号的文件均按照「敏感内容」处理,且将无法使用自定义表情符号。被阻止的实例不受影响。"
 muteAndBlock: "静音/拉黑"
 mutedUsers: "已静音用户"
 blockedUsers: "已拉黑的用户"
@@ -351,7 +361,7 @@ instanceName: "服务器名称"
 instanceDescription: "服务器简介"
 maintainerName: "管理员名称"
 maintainerEmail: "管理员电子邮箱"
-tosUrl: "服务条款 URL"
+tosUrl: "服务条款地址"
 thisYear: "今年"
 thisMonth: "本月"
 today: "今天"
@@ -433,8 +443,8 @@ administrator: "管理员"
 token: "Token (令牌)"
 2fa: "双因素认证"
 setupOf2fa: "设置双因素认证"
-totp: "身份验证应用"
-totpDescription: "使用认证应用输入一次性密码。"
+totp: "验证器"
+totpDescription: "使用验证器输入一次性密码"
 moderator: "监察员"
 moderation: "管理"
 moderationNote: "管理笔记"
@@ -477,6 +487,7 @@ noMessagesYet: "现在没有新的聊天"
 newMessageExists: "新信息"
 onlyOneFileCanBeAttached: "只能添加一个附件"
 signinRequired: "请先登录"
+signinOrContinueOnRemote: "若要继续,需要转到您所使用的实例,或者在此服务器上注册或登录。"
 invitations: "邀请"
 invitationCode: "邀请码"
 checking: "正在确认"
@@ -837,6 +848,7 @@ administration: "管理"
 accounts: "账户"
 switch: "切换"
 noMaintainerInformationWarning: "管理人员信息未设置。"
+noInquiryUrlWarning: "尚未设置联络地址。"
 noBotProtectionWarning: "Bot 防御未设置。"
 configure: "设置"
 postToGallery: "发送到图库"
@@ -848,7 +860,7 @@ shareWithNote: "在帖子中分享"
 ads: "广告"
 expiration: "截止时间"
 startingperiod: "开始时间"
-memo: "便笺"
+memo: "备注"
 priority: "优先级"
 high: "高"
 middle: "中"
@@ -1100,6 +1112,8 @@ preservedUsernames: "保留的用户名"
 preservedUsernamesDescription: "列出需要保留的用户名,使用换行来作为分割。被指定的用户名在建立账户时无法使用,但由管理员所创建的账户不受该限制。此外,现有的账户也不会受到影响。"
 createNoteFromTheFile: "从文件创建帖子"
 archive: "归档"
+archived: "已归档"
+unarchive: "取消归档"
 channelArchiveConfirmTitle: "要将 {name} 归档吗?"
 channelArchiveConfirmDescription: "归档后,在频道列表与搜索结果中不会显示,也无法发布新的贴文。"
 thisChannelArchived: "该频道已被归档。"
@@ -1110,6 +1124,9 @@ preventAiLearning: "拒绝接受生成式 AI 的学习"
 preventAiLearningDescription: "要求文章生成 AI 或图像生成 AI 不能够以发布的帖子和图像等内容作为学习对象。这是通过在 HTML 响应中包含 noai 标志来实现的,这不能完全阻止 AI 学习你的发布内容,并不是所有 AI 都会遵守这类请求。"
 options: "选项"
 specifyUser: "用户指定"
+lookupConfirm: "确定查询?"
+openTagPageConfirm: "确定打开话题标签页面?"
+specifyHost: "指定主机名"
 failedToPreviewUrl: "无法预览"
 update: "更新"
 rolesThatCanBeUsedThisEmojiAsReaction: "可以使用表情作为回应的角色"
@@ -1180,7 +1197,7 @@ 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 +1258,11 @@ keepOriginalFilenameDescription: "若关闭此设置,上传文件时文件名
 noDescription: "没有描述"
 alwaysConfirmFollow: "总是确认关注"
 inquiry: "联系我们"
+tryAgain: "请再试一次"
+confirmWhenRevealingSensitiveMedia: "显示敏感内容前需要确认"
+sensitiveMediaRevealConfirm: "这是敏感内容。是否显示?"
+createdLists: "已创建的列表"
+createdAntennas: "已创建的天线"
 _delivery:
   status: "投递状态"
   stop: "停止投递"
@@ -1375,6 +1397,8 @@ _serverSettings:
   fanoutTimelineDescription: "当启用时,可显著提高获取各种时间线时的性能,并减轻数据库的负荷。但是相对的 Redis 的内存使用量将会增加。如果服务器的内存不是很大,又或者运行不稳定的话可以把它关掉。"
   fanoutTimelineDbFallback: "回退到数据库"
   fanoutTimelineDbFallbackDescription: "当启用时,若时间线未被缓存,则将额外查询数据库。禁用该功能可通过不执行回退处理进一步减少服务器负载,但会限制可检索的时间线范围。"
+  inquiryUrl: "联络地址"
+  inquiryUrlDescription: "用来指定诸如向服务运营商咨询的论坛地址,或记载了运营商联系方式之类的网页地址。"
 _accountMigration:
   moveFrom: "从别的账号迁移到此账户"
   moveFromSub: "为另一个账户建立别名"
@@ -1641,6 +1665,7 @@ _achievements:
     _bubbleGameDoubleExplodingHead:
       title: "两个🤯"
       description: "你合成出了2个游戏里最大的Emoji"
+      flavor: ""
 _role:
   new: "创建角色"
   edit: "编辑角色"
@@ -1670,8 +1695,8 @@ _role:
   descriptionOfIsExplorable: "打开后将公开角色时间线。如果角色不是公开的,就无法公开时间线。"
   displayOrder: "显示顺序"
   descriptionOfDisplayOrder: "数字越大,显示位置越靠前。"
-  canEditMembersByModerator: "允许监察者编辑成员"
-  descriptionOfCanEditMembersByModerator: "如果选中,监察者和管理员都能够为用户分配/取消分配角色。如果未选中,则只有管理员可以执行此操作。"
+  canEditMembersByModerator: "允许监察员编辑成员"
+  descriptionOfCanEditMembersByModerator: "如果选中,监察员和管理员都能够为用户分配/取消分配角色。如果未选中,则只有管理员可以执行此操作。"
   priority: "优先级"
   _priority:
     low: "低"
@@ -1690,6 +1715,7 @@ _role:
     canManageAvatarDecorations: "管理头像挂件"
     driveCapacity: "网盘容量"
     alwaysMarkNsfw: "总是将文件标记为 NSFW"
+    canUpdateBioMedia: "可以更新头像和横幅"
     pinMax: "帖子置顶数量限制"
     antennaMax: "可创建的最大天线数量"
     wordMuteMax: "屏蔽词的字数限制"
@@ -1933,8 +1959,6 @@ _sfx:
   note: "帖子"
   noteMy: "我的帖子"
   notification: "通知"
-  antenna: "天线接收"
-  channel: "频道通知"
   reaction: "选择回应时"
 _soundSettings:
   driveFile: "使用网盘内的音频"
@@ -1943,6 +1967,7 @@ _soundSettings:
   driveFileTypeWarnDescription: "请选择音频文件"
   driveFileDurationWarn: "音频过长"
   driveFileDurationWarnDescription: "使用长音频可能会影响 Misskey 的使用。即使这样也要继续吗?"
+  driveFileError: "无法读取声音。请更改设置。"
 _ago:
   future: "未来"
   justNow: "最近"
@@ -1969,7 +1994,7 @@ _time:
   day: "日"
 _2fa:
   alreadyRegistered: "此设备已被注册"
-  registerTOTP: "开始设置认证应用"
+  registerTOTP: "开始设置验证器"
   step1: "首先,在您的设备上安装验证应用,例如 {a} 或 {b}。"
   step2: "然后,扫描屏幕上显示的二维码。"
   step2Uri: "如果使用桌面应用程序的话,请输入下面的 URI"
@@ -1978,23 +2003,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": "查看账户信息"
@@ -2291,6 +2316,7 @@ _pages:
   eyeCatchingImageSet: "设置封面图片"
   eyeCatchingImageRemove: "删除封面图片"
   chooseBlock: "添加块"
+  enterSectionTitle: "输入会话标题"
   selectType: "选择类型"
   contentBlocks: "内容"
   inputBlocks: "输入"
@@ -2398,9 +2424,10 @@ _drivecleaner:
   orderByCreatedAtAsc: "按添加日期降序排列"
 _webhookSettings:
   createWebhook: "创建 Webhook"
+  modifyWebhook: "编辑 webhook"
   name: "名称"
   secret: "密钥"
-  events: "何时运行 Webhook"
+  trigger: "触发器"
   active: "已启用"
   _events:
     follow: "关注时"
@@ -2410,6 +2437,26 @@ _webhookSettings:
     renote: "被转发时"
     reaction: "被回应时"
     mention: "被提及时"
+  _systemEvents:
+    abuseReport: "当收到举报时"
+    abuseReportResolved: "当举报被处理时"
+    userCreated: "当用户被创建时"
+  deleteConfirm: "要删除 webhook 吗?"
+_abuseReport:
+  _notificationRecipient:
+    createRecipient: "新建举报通知"
+    modifyRecipient: "编辑举报通知"
+    recipientType: "通知类型"
+    _recipientType:
+      mail: "邮箱"
+      webhook: "Webhook"
+      _captions:
+        mail: "当收到新举报时,向持有监察员权限的用户发送通知邮件"
+        webhook: "当收到新举报及举报被处理时,使用指定的 SystemWebhook 发送通知"
+    keywords: "关键字"
+    notifiedUser: "通知的用户"
+    notifiedWebhook: "使用的 webhook"
+    deleteConfirm: "要删除通知吗?"
 _moderationLogTypes:
   createRole: "创建角色"
   deleteRole: "删除角色"
@@ -2447,6 +2494,16 @@ _moderationLogTypes:
   deleteAvatarDecoration: "删除头像挂件"
   unsetUserAvatar: "清除用户头像"
   unsetUserBanner: "清除用户横幅"
+  createSystemWebhook: "新建了 SystemWebhook"
+  updateSystemWebhook: "更新了 SystemWebhook"
+  deleteSystemWebhook: "删除了 SystemWebhook"
+  createAbuseReportNotificationRecipient: "新建了举报通知"
+  updateAbuseReportNotificationRecipient: "更新了举报通知"
+  deleteAbuseReportNotificationRecipient: "删除了举报通知"
+  deleteAccount: "删除了账户"
+  deletePage: "删除了页面"
+  deleteFlash: "删除了 Play"
+  deleteGalleryPost: "删除了图库稿件"
 _fileViewer:
   title: "文件信息"
   type: "文件类型"
@@ -2578,3 +2635,8 @@ _mediaControls:
   pip: "画中画"
   playbackRate: "播放速度"
   loop: "循环播放"
+_contextMenu:
+  title: "上下文菜单"
+  app: "应用"
+  appWithShift: "Shift 键应用"
+  native: "浏览器的用户界面"
diff --git a/locales/zh-TW.yml b/locales/zh-TW.yml
index 78116254ba..80ad2eaf02 100644
--- a/locales/zh-TW.yml
+++ b/locales/zh-TW.yml
@@ -60,6 +60,7 @@ copyFileId: "複製檔案 ID"
 copyFolderId: "複製資料夾ID"
 copyProfileUrl: "複製個人資料網址"
 searchUser: "搜尋使用者"
+searchThisUsersNotes: "搜尋這個使用者的貼文"
 reply: "回覆"
 loadMore: "載入更多"
 showMore: "載入更多"
@@ -154,6 +155,7 @@ editList: "編輯清單"
 selectChannel: "選擇頻道"
 selectAntenna: "選擇天線"
 editAntenna: "編輯天線"
+createAntenna: "建立天線"
 selectWidget: "選擇小工具"
 editWidgets: "編輯小工具"
 editWidgetsExit: "完成"
@@ -180,6 +182,10 @@ addAccount: "新增帳戶"
 reloadAccountsList: "更新帳戶清單的資訊"
 loginFailed: "登入失敗"
 showOnRemote: "轉到所在實例顯示"
+continueOnRemote: "在遠端伺服器繼續"
+chooseServerOnMisskeyHub: "從 Misskey Hub 選擇伺服器"
+specifyServerHost: "直接指定伺服器網域"
+inputHostName: "請輸入域名"
 general: "一般"
 wallpaper: "桌布"
 setWallpaper: "設定桌布"
@@ -190,6 +196,7 @@ followConfirm: "你真的要追隨{name}嗎?"
 proxyAccount: "代理帳戶"
 proxyAccountDescription: "代理帳戶是在特定條件下充當遠端追隨者的帳戶。例如,當使用者新增遠端使用者至其列表時,若沒有本地使用者追隨該遠端使用者,則其活動將不會傳送至伺服器,此時便會由代理帳戶代為追隨以解決問題。"
 host: "主機"
+selectSelf: "選擇自己"
 selectUser: "選取使用者"
 recipient: "收件人"
 annotation: "註解"
@@ -205,6 +212,7 @@ perDay: "每日"
 stopActivityDelivery: "停止發送活動"
 blockThisInstance: "封鎖此伺服器"
 silenceThisInstance: "禁言此伺服器"
+mediaSilenceThisInstance: "將這個伺服器的媒體設為禁言"
 operations: "操作"
 software: "軟體"
 version: "版本"
@@ -226,6 +234,8 @@ blockedInstances: "已封鎖的伺服器"
 blockedInstancesDescription: "請逐行輸入需要封鎖的伺服器。已封鎖的伺服器將無法與本伺服器進行通訊。"
 silencedInstances: "被禁言的伺服器"
 silencedInstancesDescription: "設定要禁言的伺服器主機名稱,以換行分隔。隸屬於禁言伺服器的所有帳戶都將被視為「禁言帳戶」,只能發出「追隨請求」,而且無法提及未追隨的本地帳戶。這不會影響已封鎖的實例。"
+mediaSilencedInstances: "媒體被禁言的伺服器"
+mediaSilencedInstancesDescription: "設定您想要對媒體設定禁言的伺服器,以換行符號區隔。來自被媒體禁言的伺服器所屬帳戶的所有檔案都會被視為敏感檔案,且自訂表情符號不能使用。被封鎖的伺服器不受影響。"
 muteAndBlock: "靜音和封鎖"
 mutedUsers: "被靜音的使用者"
 blockedUsers: "被封鎖的使用者"
@@ -243,10 +253,10 @@ noCustomEmojis: "沒有自訂的表情符號"
 noJobs: "沒有任務"
 federating: "聯邦運作中"
 blocked: "已封鎖"
-suspended: "已凍結"
+suspended: "停止發送"
 all: "全部"
 subscribing: "訂閱中"
-publishing: "直播中"
+publishing: "發送中"
 notResponding: "沒有回應"
 instanceFollowing: "追隨的伺服器"
 instanceFollowers: "伺服器的追隨者"
@@ -343,7 +353,7 @@ reload: "重新整理"
 doNothing: "無視"
 reloadConfirm: "確定要重新整理嗎?"
 watch: "關注"
-unwatch: "取消追隨"
+unwatch: "取消關注"
 accept: "接受"
 reject: "拒絕"
 normal: "正常"
@@ -477,6 +487,7 @@ noMessagesYet: "沒有訊息"
 newMessageExists: "有新的訊息"
 onlyOneFileCanBeAttached: "只能加入一個附件"
 signinRequired: "請先登入"
+signinOrContinueOnRemote: "若要繼續,需前往您所在的伺服器,或者註冊並登入此伺服器"
 invitations: "邀請"
 invitationCode: "邀請碼"
 checking: "確認中"
@@ -837,6 +848,7 @@ administration: "管理"
 accounts: "帳戶"
 switch: "切換"
 noMaintainerInformationWarning: "尚未設定管理員訊息。"
+noInquiryUrlWarning: "尚未設定聯絡表單網址。"
 noBotProtectionWarning: "尚未設定 Bot 防護。"
 configure: "設定"
 postToGallery: "發佈到相簿"
@@ -1100,6 +1112,8 @@ preservedUsernames: "保留的使用者名稱"
 preservedUsernamesDescription: "換行列舉要保留的使用者名稱。此處出現的名稱將在註冊時禁用,但由管理者建立帳戶則不受此限。此外,既有的帳戶也不受影響。"
 createNoteFromTheFile: "由此檔案建立貼文"
 archive: "封存"
+archived: "已封存"
+unarchive: "取消封存"
 channelArchiveConfirmTitle: "要封存{name}嗎?"
 channelArchiveConfirmDescription: "封存後,將不會在頻道列表與搜尋結果中顯示,也無法發佈新貼文。"
 thisChannelArchived: "這個頻道已被封存。"
@@ -1110,6 +1124,9 @@ preventAiLearning: "拒絕接受生成式AI的訓練"
 preventAiLearningDescription: "要求站外生成式 AI 不使用您發佈的內容訓練模型。此功能會使伺服器於 HTML 回應新增「noai」標籤,而因為要視乎 AI 會否遵守該標籤,所以此功能無法完全阻止所有 AI 使用您的內容。"
 options: "選項"
 specifyUser: "指定使用者"
+lookupConfirm: "要查詢嗎?"
+openTagPageConfirm: "要開啟標籤的頁面嗎?"
+specifyHost: "指定主機"
 failedToPreviewUrl: "無法預覽"
 update: "更新"
 rolesThatCanBeUsedThisEmojiAsReaction: "可以使用此表情符號為反應的角色"
@@ -1241,12 +1258,17 @@ keepOriginalFilenameDescription: "如果關閉此設置,上傳時檔案名稱
 noDescription: "沒有說明文字"
 alwaysConfirmFollow: "點擊追隨時總是顯示確認訊息"
 inquiry: "聯絡我們"
+tryAgain: "請再試一次。"
+confirmWhenRevealingSensitiveMedia: "要顯示敏感媒體時需確認"
+sensitiveMediaRevealConfirm: "這是敏感媒體。確定要顯示嗎?"
+createdLists: "已建立的清單"
+createdAntennas: "已建立的天線"
 _delivery:
   status: "傳送狀態"
-  stop: "停止傳送"
-  resume: "恢復傳送"
+  stop: "停止發送"
+  resume: "恢復發送"
   _type:
-    none: "直播中"
+    none: "發送中"
     manuallySuspended: "手動暫停中"
     goneSuspended: "因為伺服器刪除所以暫停中"
     autoSuspendedForNotResponding: "因為伺服器沒有回應所以暫停中"
@@ -1376,7 +1398,7 @@ _serverSettings:
   fanoutTimelineDbFallback: "資料庫的回退"
   fanoutTimelineDbFallbackDescription: "若啟用,在時間軸沒有快取的情況下將執行回退處理以額外查詢資料庫。若停用,可以透過不執行回退處理來進一步減少伺服器的負荷,但會限制可取得的時間軸範圍。"
   inquiryUrl: "聯絡表單網址"
-  inquiryUrlDescription: "指定伺服器運營者的聯絡表單網址或包含運營者聯絡資訊網頁的網址。"
+  inquiryUrlDescription: "指定伺服器運營者的聯絡表單網址,或包含運營者聯絡資訊網頁的網址。"
 _accountMigration:
   moveFrom: "從其他帳戶遷移到這個帳戶"
   moveFromSub: "為另一個帳戶建立別名"
@@ -1693,6 +1715,7 @@ _role:
     canManageAvatarDecorations: "管理頭像裝飾"
     driveCapacity: "雲端硬碟容量"
     alwaysMarkNsfw: "總是將檔案標記為NSFW"
+    canUpdateBioMedia: "允許更新大頭貼和橫幅"
     pinMax: "置頂貼文的最大數量"
     antennaMax: "可建立的天線數量"
     wordMuteMax: "靜音文字的最大字數"
@@ -1936,8 +1959,6 @@ _sfx:
   note: "貼文"
   noteMy: "我的貼文"
   notification: "通知"
-  antenna: "天線接收"
-  channel: "頻道通知"
   reaction: "選擇反應時"
 _soundSettings:
   driveFile: "使用雲端硬碟的音效檔案"
@@ -1946,6 +1967,7 @@ _soundSettings:
   driveFileTypeWarnDescription: "請選擇音效檔案"
   driveFileDurationWarn: "音效太長了"
   driveFileDurationWarnDescription: "使用長音效檔可能會影響 Misskey 的使用體驗。仍要使用此檔案嗎?"
+  driveFileError: "無法載入語音。請變更設定"
 _ago:
   future: "未來"
   justNow: "剛剛"
@@ -2217,7 +2239,7 @@ _charts:
   federation: "聯邦宇宙"
   apRequest: "請求"
   usersIncDec: "使用者增減"
-  usersTotal: "使用者總數"
+  usersTotal: "使用者合計"
   activeUsers: "活躍使用者"
   notesIncDec: "貼文増減"
   localNotesIncDec: "本地貼文増減"
@@ -2294,6 +2316,7 @@ _pages:
   eyeCatchingImageSet: "設定封面影像"
   eyeCatchingImageRemove: "刪除封面影像"
   chooseBlock: "新增方塊"
+  enterSectionTitle: "輸入區段的標題"
   selectType: "選擇類型"
   contentBlocks: "內容"
   inputBlocks: "輸入"
@@ -2401,9 +2424,10 @@ _drivecleaner:
   orderByCreatedAtAsc: "按新增日期降序排列"
 _webhookSettings:
   createWebhook: "建立 Webhook"
+  modifyWebhook: "編輯 Webhook"
   name: "名字"
   secret: "密鑰"
-  events: "何時運行 Webhook"
+  trigger: "觸發器"
   active: "已啟用"
   _events:
     follow: "當你追隨時"
@@ -2413,6 +2437,26 @@ _webhookSettings:
     renote: "當被轉發時"
     reaction: "當獲得反應時"
     mention: "當被提到時"
+  _systemEvents:
+    abuseReport: "當使用者檢舉時"
+    abuseReportResolved: "當處理了使用者的檢舉時"
+    userCreated: "使用者被新增時"
+  deleteConfirm: "請問是否要刪除 Webhook?"
+_abuseReport:
+  _notificationRecipient:
+    createRecipient: "新增接收檢舉的通知對象"
+    modifyRecipient: "編輯接收檢舉的通知對象"
+    recipientType: "通知對象的種類"
+    _recipientType:
+      mail: "電子郵件"
+      webhook: "Webhook"
+      _captions:
+        mail: "寄送到擁有監察員權限的使用者電子郵件地址(僅在收到檢舉時)"
+        webhook: "向指定的 SystemWebhook 發送通知(在收到檢舉和解決檢舉時發送)"
+    keywords: "關鍵字"
+    notifiedUser: "被通知的使用者"
+    notifiedWebhook: "使用的 Webhook"
+    deleteConfirm: "確定要刪除通知對象嗎?"
 _moderationLogTypes:
   createRole: "新增角色"
   deleteRole: "刪除角色 "
@@ -2450,6 +2494,16 @@ _moderationLogTypes:
   deleteAvatarDecoration: "刪除頭像裝飾"
   unsetUserAvatar: "移除使用者的大頭貼"
   unsetUserBanner: "移除使用者的橫幅圖像"
+  createSystemWebhook: "建立 SystemWebhook"
+  updateSystemWebhook: "更新 SystemWebhook"
+  deleteSystemWebhook: "刪除 SystemWebhook"
+  createAbuseReportNotificationRecipient: "建立接收檢舉的通知對象"
+  updateAbuseReportNotificationRecipient: "更新接收檢舉的通知對象"
+  deleteAbuseReportNotificationRecipient: "刪除接收檢舉的通知對象"
+  deleteAccount: "刪除帳戶"
+  deletePage: "刪除頁面"
+  deleteFlash: "刪除 Play"
+  deleteGalleryPost: "刪除相簿的貼文"
 _fileViewer:
   title: "檔案詳細資訊"
   type: "檔案類型 "
@@ -2581,3 +2635,8 @@ _mediaControls:
   pip: "畫中畫"
   playbackRate: "播放速度"
   loop: "循環播放"
+_contextMenu:
+  title: "內容功能表"
+  app: "應用程式"
+  appWithShift: "Shift 鍵應用程式"
+  native: "瀏覽器的使用者介面"
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 93e5d97998..1ffdcb091a 100644
--- a/package.json
+++ b/package.json
@@ -1,12 +1,12 @@
 {
 	"name": "sharkey",
-	"version": "2024.5.1",
+	"version": "2024.8.1",
 	"codename": "shonk",
 	"repository": {
 		"type": "git",
 		"url": "https://activitypub.software/TransFem-org/Sharkey.git"
 	},
-	"packageManager": "pnpm@9.0.6",
+	"packageManager": "pnpm@9.6.0",
 	"workspaces": [
 		"packages/frontend",
 		"packages/backend",
@@ -21,8 +21,8 @@
 		"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",
-		"start": "pnpm check:connect && cd packages/backend && node ./built/boot/entry.js",
+		"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 && MK_WARNED_ABOUT_CONFIG=true node ./built/boot/entry.js",
 		"start:test": "cd packages/backend && cross-env NODE_ENV=test node ./built/boot/entry.js",
 		"init": "pnpm migrate",
 		"migrate": "cd packages/backend && pnpm migrate",
@@ -51,23 +51,25 @@
 		"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.40",
 		"tar": "6.2.1",
-		"terser": "5.30.3",
-		"typescript": "5.4.5",
-		"esbuild": "0.20.2",
-		"glob": "10.3.12"
+		"terser": "5.31.3",
+		"typescript": "5.5.4",
+		"esbuild": "0.23.0",
+		"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.13.1",
+		"eslint": "9.8.0",
+		"globals": "15.8.0",
 		"ncp": "2.0.0",
-		"start-server-and-test": "2.0.3"
+		"start-server-and-test": "2.0.4"
 	}
 }
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/fonts/sharkey-icons/custom-sharkey-icons.svg b/packages/backend/assets/fonts/sharkey-icons/custom-sharkey-icons.svg
deleted file mode 100644
index 9d21137072..0000000000
--- a/packages/backend/assets/fonts/sharkey-icons/custom-sharkey-icons.svg
+++ /dev/null
@@ -1,11 +0,0 @@
-<?xml version="1.0" standalone="no"?>
-<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
-<svg xmlns="http://www.w3.org/2000/svg">
-<defs>
-<font id="custom-sharkey-icons" horiz-adv-x="512">
-<font-face font-family="custom-sharkey-icons" units-per-em="512" ascent="480" descent="-32"/>
-<missing-glyph horiz-adv-x="512" />
-
-<glyph glyph-name="shark" unicode="&#97;" d="M469 171l0-43-42 0c-30 0-60 9-86 21-53-27-117-27-170 0-26-12-56-21-86-21l-42 0 0 43 42 0c30 0 60 10 86 27 51-36 119-36 170 0 26-17 56-27 86-27z m-356 47c11 3 23 9 34 16l24 16c14 49 16 107-9 174 93-16 177-97 209-193 16-10 32-16 48-17-30 140-155 255-291 255-7 0-14-4-18-10-4-6-4-14-1-21 46-92 32-167 4-220m228-105c-51-36-119-36-170 0-26-17-56-28-86-28l-42 0 0-42 42 0c30 0 60 8 86 21 53-28 117-28 170 0 26-13 56-21 86-21l42 0 0 42-42 0c-30 0-60 11-86 28z"/>
-<glyph glyph-name="misskey" unicode="&#98;" d="M190 152c-22 0-41 13-50 29-5 7-14 9-15 0l0-43c0-17-6-32-18-45-12-12-27-18-45-18-17 0-31 6-44 18-12 13-18 28-18 45l0 236c0 13 4 26 11 36 8 11 18 19 30 24 7 2 14 3 21 3 20 0 36-7 49-22l63-75c2-1 6-10 16-10 10 0 15 9 16 10l64 75c13 15 29 22 48 22 7 0 14-1 22-3 12-5 21-12 29-24 8-10 12-23 12-36l0-236c0-17-6-32-19-45-12-12-27-18-44-18-17 0-32 6-45 18-12 13-18 28-18 45l0 43c-1 12-11 4-15 0-9-18-28-29-50-29z m268 176c-15 0-28 6-39 16-10 11-15 24-15 39 0 15 5 28 15 38 11 11 24 16 39 16 14 0 27-5 38-16 11-10 16-23 16-38 0-15-5-28-16-39-11-10-24-16-38-16z m0-10c15 0 28-6 38-17 11-10 16-23 16-39l0-133c0-15-5-28-16-39-10-10-23-15-38-15-15 0-28 5-38 15-11 11-16 24-16 39l0 133c0 16 5 29 16 39 10 11 23 17 38 17z"/>
-</font></defs></svg>
diff --git a/packages/backend/assets/fonts/sharkey-icons/custom-sharkey-icons.ttf b/packages/backend/assets/fonts/sharkey-icons/custom-sharkey-icons.ttf
deleted file mode 100644
index a2601e0f1b..0000000000
Binary files a/packages/backend/assets/fonts/sharkey-icons/custom-sharkey-icons.ttf and /dev/null differ
diff --git a/packages/backend/assets/fonts/sharkey-icons/custom-sharkey-icons.woff b/packages/backend/assets/fonts/sharkey-icons/custom-sharkey-icons.woff
deleted file mode 100644
index d9f471fa35..0000000000
Binary files a/packages/backend/assets/fonts/sharkey-icons/custom-sharkey-icons.woff and /dev/null differ
diff --git a/packages/backend/assets/fonts/sharkey-icons/shark-font.svg b/packages/backend/assets/fonts/sharkey-icons/shark-font.svg
new file mode 100644
index 0000000000..b67bd7f7d8
--- /dev/null
+++ b/packages/backend/assets/fonts/sharkey-icons/shark-font.svg
@@ -0,0 +1,30 @@
+<?xml version="1.0" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" >
+<svg xmlns="http://www.w3.org/2000/svg">
+<defs>
+  <font id="shark-font" horiz-adv-x="512">
+    <font-face font-family="shark-font"
+      units-per-em="512" ascent="512"
+      descent="0" />
+    <missing-glyph horiz-adv-x="0" />
+    <glyph glyph-name="foldermove"
+      unicode="&#xEA01;"
+      horiz-adv-x="512" d="M74 429C63 425 54 415 52 403C52 400 51 369 52 323V248L68 249H83V292L84 335H431V114H84V207H52V157C52 108 52 107 54 102C56 95 64 87 71 85L76 83H441L445 85Q455.5 91 460 100L462 105V344L459 349C456 356 451 361 444 364L439 367H264L235 396C211 420 205 425 201 427L196 430H137C91 430 76 430 74 429M203 383L219 367H84V399H187zM166 303C160 300 157 294 159 289C160 287 168 277 185 261L209 237H122C40 237 34 237 32 236S28 233 27 232C25 229 25 229 25 126C25 26 25 23 27 21C32 13 44 14 47 24C47 26 48 62 48 121V215H128C172 215 208 215 208 214C208 214 197 203 184 189C157 162 156 162 160 155C162 150 168 147 173 149C174 150 191 166 211 185C249 223 248 223 247 229C246 232 177 302 173 303C171 304 168 304 166 303" />
+    <glyph glyph-name="foldermove-1"
+      unicode="&#x66;&#x6F;&#x6C;&#x64;&#x65;&#x72;&#x6D;&#x6F;&#x76;&#x65;"
+      horiz-adv-x="512" d="M74 429C63 425 54 415 52 403C52 400 51 369 52 323V248L68 249H83V292L84 335H431V114H84V207H52V157C52 108 52 107 54 102C56 95 64 87 71 85L76 83H441L445 85Q455.5 91 460 100L462 105V344L459 349C456 356 451 361 444 364L439 367H264L235 396C211 420 205 425 201 427L196 430H137C91 430 76 430 74 429M203 383L219 367H84V399H187zM166 303C160 300 157 294 159 289C160 287 168 277 185 261L209 237H122C40 237 34 237 32 236S28 233 27 232C25 229 25 229 25 126C25 26 25 23 27 21C32 13 44 14 47 24C47 26 48 62 48 121V215H128C172 215 208 215 208 214C208 214 197 203 184 189C157 162 156 162 160 155C162 150 168 147 173 149C174 150 191 166 211 185C249 223 248 223 247 229C246 232 177 302 173 303C171 304 168 304 166 303" />
+    <glyph glyph-name="misskey"
+      unicode="&#xEA02;"
+      horiz-adv-x="512" d="M190 152C168 152 149 165 140 181C135 188 126 190 125 181V138Q125 112.5 107 93Q89 75 62 75C45 75 31 81 18 93Q0 112.5 0 138V374C0 387 4 400 11 410Q23 426.5 41 434Q51.5 437 62 437C82 437 98 430 111 415L174 340C176 339 180 330 190 330S205 339 206 340L270 415C283 430 299 437 318 437C325 437 332 436 340 434C352 429 361 422 369 410C377 400 381 387 381 374V138C381 121 375 106 362 93C350 81 335 75 318 75Q292.5 75 273 93Q255 112.5 255 138V181C254 193 244 185 240 181C231 163 212 152 190 152M458 328C443 328 430 334 419 344Q404 360.5 404 383C404 398 409 411 419 421C430 432 443 437 458 437C472 437 485 432 496 421C507 411 512 398 512 383S507 355 496 344C485 334 472 328 458 328M458 318C473 318 486 312 496 301C507 291 512 278 512 262V129C512 114 507 101 496 90C486 80 473 75 458 75S430 80 420 90C409 101 404 114 404 129V262C404 278 409 291 420 301C430 312 443 318 458 318" />
+    <glyph glyph-name="misskey-1"
+      unicode="&#x6D;&#x69;&#x73;&#x73;&#x6B;&#x65;&#x79;"
+      horiz-adv-x="512" d="M190 152C168 152 149 165 140 181C135 188 126 190 125 181V138Q125 112.5 107 93Q89 75 62 75C45 75 31 81 18 93Q0 112.5 0 138V374C0 387 4 400 11 410Q23 426.5 41 434Q51.5 437 62 437C82 437 98 430 111 415L174 340C176 339 180 330 190 330S205 339 206 340L270 415C283 430 299 437 318 437C325 437 332 436 340 434C352 429 361 422 369 410C377 400 381 387 381 374V138C381 121 375 106 362 93C350 81 335 75 318 75Q292.5 75 273 93Q255 112.5 255 138V181C254 193 244 185 240 181C231 163 212 152 190 152M458 328C443 328 430 334 419 344Q404 360.5 404 383C404 398 409 411 419 421C430 432 443 437 458 437C472 437 485 432 496 421C507 411 512 398 512 383S507 355 496 344C485 334 472 328 458 328M458 318C473 318 486 312 496 301C507 291 512 278 512 262V129C512 114 507 101 496 90C486 80 473 75 458 75S430 80 420 90C409 101 404 114 404 129V262C404 278 409 291 420 301C430 312 443 318 458 318" />
+    <glyph glyph-name="shark"
+      unicode="&#xEA03;"
+      horiz-adv-x="512" d="M469 171V128H427C397 128 367 137 341 149C288 122 224 122 171 149C145 137 115 128 85 128H43V171H85C115 171 145 181 171 198C222 162 290 162 341 198C367 181 397 171 427 171zM113 218C124 221 136 227 147 234L171 250C185 299 187 357 162 424C255 408 339 327 371 231C387 221 403 215 419 214C389 354 264 469 128 469C121 469 114 465 110 459S106 445 109 438C155 346 141 271 113 218M341 113C290 77 222 77 171 113C145 96 115 85 85 85H43V43H85C115 43 145 51 171 64C224 36 288 36 341 64C367 51 397 43 427 43H469V85H427C397 85 367 96 341 113" />
+    <glyph glyph-name="shark-1"
+      unicode="&#x73;&#x68;&#x61;&#x72;&#x6B;"
+      horiz-adv-x="512" d="M469 171V128H427C397 128 367 137 341 149C288 122 224 122 171 149C145 137 115 128 85 128H43V171H85C115 171 145 181 171 198C222 162 290 162 341 198C367 181 397 171 427 171zM113 218C124 221 136 227 147 234L171 250C185 299 187 357 162 424C255 408 339 327 371 231C387 221 403 215 419 214C389 354 264 469 128 469C121 469 114 465 110 459S106 445 109 438C155 346 141 271 113 218M341 113C290 77 222 77 171 113C145 96 115 85 85 85H43V43H85C115 43 145 51 171 64C224 36 288 36 341 64C367 51 397 43 427 43H469V85H427C397 85 367 96 341 113" />
+  </font>
+</defs>
+</svg>
diff --git a/packages/backend/assets/fonts/sharkey-icons/shark-font.ttf b/packages/backend/assets/fonts/sharkey-icons/shark-font.ttf
new file mode 100644
index 0000000000..0fdf04df08
Binary files /dev/null and b/packages/backend/assets/fonts/sharkey-icons/shark-font.ttf differ
diff --git a/packages/backend/assets/fonts/sharkey-icons/shark-font.woff b/packages/backend/assets/fonts/sharkey-icons/shark-font.woff
new file mode 100644
index 0000000000..993666bc3a
Binary files /dev/null and b/packages/backend/assets/fonts/sharkey-icons/shark-font.woff differ
diff --git a/packages/backend/assets/fonts/sharkey-icons/style.css b/packages/backend/assets/fonts/sharkey-icons/style.css
index 7fb0f94504..7168702e5a 100644
--- a/packages/backend/assets/fonts/sharkey-icons/style.css
+++ b/packages/backend/assets/fonts/sharkey-icons/style.css
@@ -1,31 +1,114 @@
-@charset "UTF-8";
-
 @font-face {
-  font-family: "custom-sharkey-icons";
-  src: url("./custom-sharkey-icons.woff") format("woff"),
-    url("./custom-sharkey-icons.ttf") format("truetype"),
-    url("./custom-sharkey-icons.svg#custom-sharkey-icons") format("svg");
-  font-weight: normal;
+  font-display: auto;
+  font-family: "shark-font";
   font-style: normal;
-  font-display: block;
+  font-weight: normal;
+  
+  src: url("./shark-font.woff?1722899913909") format("woff"), url("./shark-font.ttf?1722899913909") format("truetype"), url("./shark-font.svg?1722899913909#shark-font") format("svg");
 }
 
 .sk-icons {
-  font-family: "custom-sharkey-icons" !important;
-  font-style: normal;
+  display: inline-block;
+  font-family: "shark-font";
   font-weight: normal;
+  font-style: normal;
   font-variant: normal;
-  text-transform: none;
+  text-rendering: auto;
   line-height: 1;
-  speak: none;
-  -webkit-font-smoothing: antialiased;
   -moz-osx-font-smoothing: grayscale;
+  -webkit-font-smoothing: antialiased;
 }
 
-.sk-icons.sk-shark:before {
-  content: "\61";
+.sk-icons-lg {
+  font-size: 1.33333em;
+  line-height: 0.75em;
+  vertical-align: -0.0667em;
 }
 
-.sk-icons.sk-misskey:before {
-  content: "\62";
+.sk-icons-xs {
+  font-size: 0.75em;
 }
+
+.sk-icons-sm {
+  font-size: 0.875em;
+}
+
+.sk-icons-1x {
+  font-size: 1em;
+}
+
+.sk-icons-2x {
+  font-size: 2em;
+}
+
+.sk-icons-3x {
+  font-size: 3em;
+}
+
+.sk-icons-4x {
+  font-size: 4em;
+}
+
+.sk-icons-5x {
+  font-size: 5em;
+}
+
+.sk-icons-6x {
+  font-size: 6em;
+}
+
+.sk-icons-7x {
+  font-size: 7em;
+}
+
+.sk-icons-8x {
+  font-size: 8em;
+}
+
+.sk-icons-9x {
+  font-size: 9em;
+}
+
+.sk-icons-10x {
+  font-size: 10em;
+}
+
+.sk-icons-fw {
+  text-align: center;
+  width: 1.25em;
+}
+
+.sk-icons-border {
+  border: solid 0.08em #eee;
+  border-radius: 0.1em;
+  padding: 0.2em 0.25em 0.15em;
+}
+
+.sk-icons-pull-left {
+  float: left;
+}
+
+.sk-icons-pull-right {
+  float: right;
+}
+
+.sk-icons.sk-icons-pull-left {
+  margin-right: 0.3em;
+}
+
+.sk-icons.sk-icons-pull-right {
+  margin-left: 0.3em;
+}
+
+
+.sk-icons.sk-foldermove::before {
+  content: "\ea01";
+}
+
+.sk-icons.sk-misskey::before {
+  content: "\ea02";
+}
+
+.sk-icons.sk-shark::before {
+  content: "\ea03";
+}
\ No newline at end of file
diff --git a/packages/backend/assets/redoc.html b/packages/backend/assets/redoc.html
deleted file mode 100644
index 1b7ff9e0d2..0000000000
--- a/packages/backend/assets/redoc.html
+++ /dev/null
@@ -1,24 +0,0 @@
-<!DOCTYPE html>
-<html>
-	<head>
-		<title>Sharkey 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/eslint.config.js b/packages/backend/eslint.config.js
new file mode 100644
index 0000000000..4fd9f0cd51
--- /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'],
+				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/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/package.json b/packages/backend/package.json
index b05dc10c11..ff84beec67 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",
@@ -31,7 +31,7 @@
 		"test:e2e": "pnpm build && pnpm build:test && pnpm jest:e2e",
 		"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",
@@ -63,45 +63,45 @@
 		"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",
+		"@aws-sdk/client-s3": "3.620.0",
+		"@aws-sdk/lib-storage": "3.620.0",
+		"@bull-board/api": "5.21.1",
+		"@bull-board/fastify": "5.21.1",
+		"@bull-board/ui": "5.21.1",
 		"@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/multipart": "8.3.0",
+		"@fastify/static": "7.0.4",
 		"@fastify/view": "9.1.0",
 		"@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.53",
+		"@nestjs/common": "10.3.10",
+		"@nestjs/core": "10.3.10",
+		"@nestjs/testing": "10.3.10",
 		"@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",
 		"@transfem-org/sfm-js": "0.24.5",
 		"@twemoji/parser": "15.1.1",
 		"accepts": "1.3.8",
-		"ajv": "8.13.0",
+		"ajv": "8.17.1",
 		"archiver": "7.0.1",
 		"argon2": "^0.40.1",
 		"async-mutex": "0.5.0",
 		"bcryptjs": "2.4.3",
 		"blurhash": "2.0.5",
 		"body-parser": "1.20.2",
-		"bullmq": "5.7.8",
+		"bullmq": "5.10.4",
 		"cacheable-lookup": "7.0.0",
 		"cbor": "9.0.2",
 		"chalk": "5.3.0",
@@ -112,30 +112,31 @@
 		"content-disposition": "0.5.4",
 		"date-fns": "2.30.0",
 		"deep-email-validator": "0.1.21",
-		"fastify": "4.26.2",
+		"fast-xml-parser": "^4.4.0",
+		"fastify": "4.28.1",
 		"fastify-multer": "^2.0.3",
 		"fastify-raw-body": "4.3.0",
 		"feed": "4.2.2",
-		"file-type": "19.0.0",
-		"fluent-ffmpeg": "2.1.2",
+		"file-type": "19.3.0",
+		"fluent-ffmpeg": "2.1.3",
 		"form-data": "4.0.0",
 		"glob": "10.3.10",
-		"got": "14.2.1",
-		"happy-dom": "10.0.3",
+		"got": "14.4.2",
+		"happy-dom": "15.6.1",
 		"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.1",
 		"ipaddr.js": "2.2.0",
-		"is-svg": "5.0.0",
+		"is-svg": "5.0.1",
 		"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",
 		"megalodon": "workspace:*",
-		"meilisearch": "0.38.0",
+		"meilisearch": "0.41.0",
 		"microformats-parser": "2.0.2",
 		"mime-types": "2.1.35",
 		"misskey-js": "workspace:*",
@@ -144,23 +145,24 @@
 		"nanoid": "5.0.7",
 		"nested-property": "4.0.0",
 		"node-fetch": "3.3.2",
-		"nodemailer": "6.9.13",
+		"nodemailer": "6.9.14",
 		"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.1",
 		"parse5": "7.1.2",
-		"pg": "8.11.5",
+		"pg": "8.12.0",
 		"pkce-challenge": "4.1.0",
 		"probe-image-size": "7.2.3",
 		"promise-limit": "2.7.0",
-		"pug": "3.0.2",
+		"proxy-addr": "^2.0.7",
+		"pug": "3.0.3",
 		"punycode": "2.3.1",
 		"qrcode": "1.5.3",
 		"random-seed": "0.3.0",
 		"ratelimiter": "3.4.1",
-		"re2": "1.20.10",
+		"re2": "1.21.3",
 		"redis-lock": "0.1.4",
 		"reflect-metadata": "0.2.2",
 		"rename": "1.0.4",
@@ -168,28 +170,27 @@
 		"rxjs": "7.8.1",
 		"sanitize-html": "2.13.0",
 		"secure-json-parse": "2.7.0",
-		"sharp": "0.33.3",
+		"sharp": "0.33.4",
 		"slacc": "0.0.10",
 		"strict-event-emitter-types": "2.0.0",
 		"stringz": "2.1.0",
-		"systeminformation": "5.22.7",
+		"systeminformation": "5.22.11",
 		"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.5.4",
 		"ulid": "2.3.0",
 		"uuid": "^9.0.1",
 		"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.3.10",
 		"@simplewebauthn/types": "10.0.0",
 		"@swc/jest": "0.2.36",
 		"@types/accepts": "1.3.7",
@@ -199,22 +200,22 @@
 		"@types/color-convert": "2.0.3",
 		"@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/htmlescape": "1.1.3",
+		"@types/http-link-header": "1.0.7",
 		"@types/jest": "29.5.12",
 		"@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/node": "20.14.12",
 		"@types/nodemailer": "6.4.15",
-		"@types/oauth": "0.9.4",
+		"@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.6",
+		"@types/proxy-addr": "^2.0.3",
 		"@types/pug": "2.0.10",
 		"@types/punycode": "2.1.4",
 		"@types/qrcode": "1.5.5",
@@ -230,19 +231,18 @@
 		"@types/uuid": "^9.0.4",
 		"@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.11",
+		"@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",
+		"execa": "9.3.0",
+		"fkill": "9.0.0",
 		"jest": "29.7.0",
 		"jest-mock": "29.7.0",
-		"nodemon": "3.1.0",
+		"nodemon": "3.1.4",
 		"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..d4bf4baf43 100644
--- a/packages/backend/scripts/check_connect.js
+++ b/packages/backend/scripts/check_connect.js
@@ -5,11 +5,33 @@
 
 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;
-});
+// createPostgresDataSource handels primaries and replicas automatically.
+// usually, it only opens connections first use, so we force it using
+// .initialize()
+createPostgresDataSource(config)
+	.initialize()
+	.then(c => { c.destroy() })
+	.catch(e => { throw e });
+
+
+// Connect to all redis servers
+function connectToRedis(redisOptions) {
+	const redis = new Redis(redisOptions);
+	redis.on('connect', () => redis.disconnect());
+	redis.on('error', (e) => {
+		throw e;
+	});
+}
+
+// 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.
+(new Set([
+	config.redis,
+	config.redisForPubsub,
+	config.redisForJobQueue,
+	config.redisForTimelines,
+])).forEach(connectToRedis);
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/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 d1744b4b4b..56128a7ab9 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();
 
 // We wrap this in a main function, that gets called,
diff --git a/packages/backend/src/boot/master.ts b/packages/backend/src/boot/master.ts
index 303ba94207..3559816e96 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');
 
@@ -44,7 +44,7 @@ function greet() {
 		//#endregion
 
 		console.log(' Sharkey is an open-source decentralized microblogging platform.');
-		console.log(chalk.rgb(255, 136, 0)(' If you like Sharkey, please donate to support development. https://ko-fi.com/transfem'));
+		console.log(chalk.rgb(255, 136, 0)(' If you like Sharkey, please donate to support development. https://opencollective.com/sharkey'));
 
 		console.log('');
 		console.log(chalkTemplate`--- ${os.hostname()} {gray (PID: ${process.pid.toString()})} ---`);
@@ -112,6 +112,11 @@ export async function masterMain() {
 			await server();
 		}
 
+		if (config.clusterLimit === 0) {
+			bootLogger.error("Configuration error: we can't create workers, `config.clusterLimit` is 0 (if you don't want to use clustering, set the environment variable `MK_DISABLE_CLUSTERING` to a non-empty value instead)", null, true);
+			process.exit(1);
+		}
+
 		await spawnWorkers(config.clusterLimit);
 	}
 
@@ -180,7 +185,10 @@ async function connectDb(): Promise<void> {
 */
 
 async function spawnWorkers(limit = 1) {
-	const workers = Math.min(limit, os.cpus().length);
+	const cpuCount = os.cpus().length;
+	// in some weird environments, node can't count the CPUs; we trust the config in those cases
+	const workers = cpuCount === 0 ? limit : Math.min(limit, cpuCount);
+
 	bootLogger.info(`Starting ${workers} worker${workers === 1 ? '' : 's'}...`);
 	await Promise.all([...Array(workers)].map(spawnWorker));
 	bootLogger.succ('All workers started');
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 58c4d028aa..3f9967ad19 100644
--- a/packages/backend/src/config.ts
+++ b/packages/backend/src/config.ts
@@ -12,9 +12,10 @@ import * as Sentry from '@sentry/node';
 import type { RedisOptions } from 'ioredis';
 
 type RedisOptionsSource = Partial<RedisOptions> & {
-	host: string;
-	port: number;
+	host?: string;
+	port?: number;
 	family?: number;
+	path?: string,
 	pass: string;
 	db?: number;
 	prefix?: string;
@@ -24,7 +25,7 @@ type RedisOptionsSource = Partial<RedisOptions> & {
  * 設定ファイルの型
  */
 type Source = {
-	url: string;
+	url?: string;
 	port?: number;
 	socket?: string;
 	chmodSocket?: string;
@@ -32,9 +33,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 };
 	};
@@ -95,6 +96,7 @@ type Source = {
 	customMOTD?: string[];
 
 	signToActivityPubGet?: boolean;
+	attachLdSignatureForRelays?: boolean;
 	checkActivityPubGetSignature?: boolean;
 
 	perChannelMaxNoteCacheCount?: number;
@@ -161,6 +163,7 @@ export type Config = {
 	proxyRemoteFiles: boolean | undefined;
 	customMOTD: string[] | undefined;
 	signToActivityPubGet: boolean;
+	attachLdSignatureForRelays: boolean;
 	checkActivityPubGetSignature: boolean | undefined;
 
 	version: string;
@@ -221,8 +224,15 @@ export function loadConfig(): Config {
 		JSON.parse(fs.readFileSync(`${_dirname}/../../../built/_vite_/manifest.json`, 'utf-8'))
 		: { 'src/_boot_.ts': { file: 'src/_boot_.ts' } };
 
-	const config = globSync(path).sort()
-		.map(path => fs.readFileSync(path, 'utf-8'))
+	const configFiles = globSync(path).sort();
+
+	if (configFiles.length === 0
+			&& !process.env['MK_WARNED_ABOUT_CONFIG']) {
+		console.log('No config files loaded, check if this is intentional');
+		process.env['MK_WARNED_ABOUT_CONFIG'] = '1';
+	}
+
+	const config = configFiles.map(path => fs.readFileSync(path, 'utf-8'))
 		.map(contents => yaml.load(contents) as Source)
 		.reduce(
 			(acc: Source, cur: Source) => Object.assign(acc, cur),
@@ -231,13 +241,17 @@ export function loadConfig(): Config {
 
 	applyEnvOverrides(config);
 
-	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;
@@ -248,7 +262,7 @@ export function loadConfig(): Config {
 		version,
 		publishTarballInsteadOfProvideRepositoryUrl: !!config.publishTarballInsteadOfProvideRepositoryUrl,
 		url: url.origin,
-		port: config.port ?? parseInt(process.env.PORT ?? '', 10),
+		port: config.port ?? parseInt(process.env.PORT ?? '3000', 10),
 		socket: config.socket,
 		chmodSocket: config.chmodSocket,
 		disableHsts: config.disableHsts,
@@ -260,7 +274,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,
@@ -291,6 +305,7 @@ export function loadConfig(): Config {
 		proxyRemoteFiles: config.proxyRemoteFiles,
 		customMOTD: config.customMOTD,
 		signToActivityPubGet: config.signToActivityPubGet ?? true,
+		attachLdSignatureForRelays: config.attachLdSignatureForRelays ?? true,
 		checkActivityPubGetSignature: config.checkActivityPubGetSignature,
 		mediaProxy: externalMediaProxy ?? internalMediaProxy,
 		externalMediaProxyEnabled: externalMediaProxy !== null && externalMediaProxy !== internalMediaProxy,
@@ -436,8 +451,8 @@ function applyEnvOverrides(config: Source) {
 
 	// these are all the settings that can be overridden
 
-	_apply_top([['url', 'port', 'socket', 'chmodSocket', 'disableHsts']]);
-	_apply_top(['db', ['host', 'port', 'db', 'user', 'pass']]);
+	_apply_top([['url', 'port', 'socket', 'chmodSocket', 'disableHsts', 'id', 'dbReplications']]);
+	_apply_top(['db', ['host', 'port', 'db', 'user', 'pass', 'disableCache']]);
 	_apply_top(['dbSlaves', Array.from((config.dbSlaves ?? []).keys()), ['host', 'port', 'db', 'user', 'pass']]);
 	_apply_top([
 		['redis', 'redisForPubsub', 'redisForJobQueue', 'redisForTimelines'],
@@ -447,7 +462,8 @@ function applyEnvOverrides(config: Source) {
 	_apply_top([['sentryForFrontend', 'sentryForBackend'], 'options', ['dsn', 'profileSampleRate', 'serverName', 'includeLocalVariables', 'proxy', 'keepAlive', 'caCerts']]);
 	_apply_top(['sentryForBackend', 'enableNodeProfiling']);
 	_apply_top([['clusterLimit', 'deliverJobConcurrency', 'inboxJobConcurrency', 'relashionshipJobConcurrency', 'deliverJobPerSec', 'inboxJobPerSec', 'relashionshipJobPerSec', 'deliverJobMaxAttempts', 'inboxJobMaxAttempts']]);
-	_apply_top([['outgoingAddress', 'outgoingAddressFamily', 'proxy', 'proxySmtp', 'mediaProxy', 'videoThumbnailGenerator']]);
+	_apply_top([['outgoingAddress', 'outgoingAddressFamily', 'proxy', 'proxySmtp', 'mediaProxy', 'proxyRemoteFiles', 'videoThumbnailGenerator']]);
 	_apply_top([['maxFileSize', 'maxNoteLength', 'pidFile']]);
 	_apply_top(['import', ['downloadTimeout', 'maxFileSize']]);
+	_apply_top([['signToActivityPubGet', 'checkActivityPubGetSignature']]);
 }
diff --git a/packages/backend/src/core/AbuseReportNotificationService.ts b/packages/backend/src/core/AbuseReportNotificationService.ts
new file mode 100644
index 0000000000..7be5335885
--- /dev/null
+++ b/packages/backend/src/core/AbuseReportNotificationService.ts
@@ -0,0 +1,405 @@
+/*
+ * 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,
+	MiUser,
+} from '@/models/_.js';
+import { EmailService } from '@/core/EmailService.js';
+import { MetaService } from '@/core/MetaService.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 { IdService } from './IdService.js';
+
+@Injectable()
+export class AbuseReportNotificationService implements OnApplicationShutdown {
+	constructor(
+		@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 metaService: MetaService,
+		private moderationLogService: ModerationLogService,
+		private globalEventService: GlobalEventService,
+	) {
+		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(true, 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),
+		);
+
+		// 送信先の鮮度を保つため、毎回取得する
+		const meta = await this.metaService.fetch(true);
+		recipientEMailAddresses.push(
+			...(meta.email ? [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 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(
+				abuseReports.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,
+			})
+			.then();
+
+		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,
+			})
+			.then();
+
+		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,
+			})
+			.then();
+	}
+
+	/**
+	 * モデレータ権限を持たない(*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(true, 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..007c3f1bf9
--- /dev/null
+++ b/packages/backend/src/core/AbuseReportService.ts
@@ -0,0 +1,138 @@
+/*
+ * 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 operator 通報を処理したユーザ
+	 * @see AbuseReportNotificationService.notify
+	 */
+	@bindThis
+	public async resolve(
+		params: {
+			reportId: string;
+			forward: boolean;
+		}[],
+		operator: MiUser,
+	) {
+		const paramsMap = new Map(params.map(it => [it.reportId, it]));
+		const reports = await this.abuseUserReportsRepository.findBy({
+			id: In(params.map(it => it.reportId)),
+		});
+
+		const targetUserMap = new Map();
+		for (const report of reports) {
+			const shouldForward = paramsMap.get(report.id)!.forward;
+
+			if (shouldForward && report.targetUserHost != null) {
+				targetUserMap.set(report.id, await this.usersRepository.findOneByOrFail({ id: report.targetUserId }));
+			} else {
+				targetUserMap.set(report.id, null);
+			}
+		}
+
+		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: operator.id,
+				forwarded: ps.forward && report.targetUserHost !== null,
+			});
+
+			const targetUser = targetUserMap.get(report.id)!;
+			if (targetUser != null) {
+				const actor = await this.instanceActorService.getInstanceActor();
+
+				// eslint-disable-next-line
+				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(operator, 'resolveAbuseReport', {
+					reportId: report.id,
+					report: report,
+					forwarded: ps.forward && report.targetUserHost !== null,
+				});
+		}
+
+		return this.abuseUserReportsRepository.findBy({ id: In(reports.map(it => it.id)) })
+			.then(reports => this.abuseReportNotificationService.notifySystemWebhook(reports, 'abuseReportResolved'));
+	}
+}
diff --git a/packages/backend/src/core/AnnouncementService.ts b/packages/backend/src/core/AnnouncementService.ts
index 9b60df2cae..40a9db01c0 100644
--- a/packages/backend/src/core/AnnouncementService.ts
+++ b/packages/backend/src/core/AnnouncementService.ts
@@ -67,7 +67,7 @@ export class AnnouncementService {
 
 	@bindThis
 	public async create(values: Partial<MiAnnouncement>, moderator?: MiUser): Promise<{ raw: MiAnnouncement; packed: Packed<'Announcement'> }> {
-		const announcement = await this.announcementsRepository.insert({
+		const announcement = await this.announcementsRepository.insertOne({
 			id: this.idService.gen(),
 			updatedAt: null,
 			title: values.title,
@@ -79,7 +79,7 @@ export class AnnouncementService {
 			silence: values.silence,
 			needConfirmationToRead: values.needConfirmationToRead,
 			userId: values.userId,
-		}).then(x => this.announcementsRepository.findOneByOrFail(x.identifiers[0]));
+		});
 
 		const packed = await this.announcementEntityService.pack(announcement);
 
diff --git a/packages/backend/src/core/AvatarDecorationService.ts b/packages/backend/src/core/AvatarDecorationService.ts
index 21e31d79a4..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);
 	}
@@ -55,10 +55,10 @@ export class AvatarDecorationService implements OnApplicationShutdown {
 
 	@bindThis
 	public async create(options: Partial<MiAvatarDecoration>, moderator?: MiUser): Promise<MiAvatarDecoration> {
-		const created = await this.avatarDecorationsRepository.insert({
+		const created = await this.avatarDecorationsRepository.insertOne({
 			id: this.idService.gen(),
 			...options,
-		}).then(x => this.avatarDecorationsRepository.findOneByOrFail(x.identifiers[0]));
+		});
 
 		this.globalEventService.publishInternalEvent('avatarDecorationCreated', created);
 
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/ClipService.ts b/packages/backend/src/core/ClipService.ts
index bb8be26ce6..929a9db064 100644
--- a/packages/backend/src/core/ClipService.ts
+++ b/packages/backend/src/core/ClipService.ts
@@ -41,17 +41,17 @@ 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();
 		}
 
-		const clip = await this.clipsRepository.insert({
+		const clip = await this.clipsRepository.insertOne({
 			id: this.idService.gen(),
 			userId: me.id,
 			name: name,
 			isPublic: isPublic,
 			description: description,
-		}).then(x => this.clipsRepository.findOneByOrFail(x.identifiers[0]));
+		});
 
 		return clip;
 	}
@@ -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 9baec9a59f..7e51d3afa4 100644
--- a/packages/backend/src/core/CoreModule.ts
+++ b/packages/backend/src/core/CoreModule.ts
@@ -5,6 +5,14 @@
 
 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 { AccountMoveService } from './AccountMoveService.js';
 import { AccountUpdateService } from './AccountUpdateService.js';
 import { AnnouncementService } from './AnnouncementService.js';
@@ -53,10 +61,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 +153,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 $AnnouncementService: Provider = { provide: 'AnnouncementService', useExisting: AnnouncementService };
@@ -193,10 +204,13 @@ 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 $UtilityService: Provider = { provide: 'UtilityService', useExisting: UtilityService };
 const $FileInfoService: Provider = { provide: 'FileInfoService', useExisting: FileInfoService };
 const $SearchService: Provider = { provide: 'SearchService', useExisting: SearchService };
@@ -225,6 +239,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 +273,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 +301,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
 	],
 	providers: [
 		LoggerService,
+		AbuseReportService,
+		AbuseReportNotificationService,
 		AccountMoveService,
 		AccountUpdateService,
 		AnnouncementService,
@@ -334,10 +352,13 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
 		UserKeypairService,
 		UserListService,
 		UserMutingService,
+		UserRenoteMutingService,
+		UserSearchService,
 		UserSuspendService,
 		UserAuthService,
 		VideoProcessingService,
-		WebhookService,
+		UserWebhookService,
+		SystemWebhookService,
 		UtilityService,
 		FileInfoService,
 		SearchService,
@@ -366,6 +387,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
 
 		AbuseUserReportEntityService,
 		AnnouncementEntityService,
+		AbuseReportNotificationRecipientEntityService,
 		AntennaEntityService,
 		AppEntityService,
 		AuthSessionEntityService,
@@ -399,6 +421,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
 		RoleEntityService,
 		ReversiGameEntityService,
 		MetaEntityService,
+		SystemWebhookEntityService,
 
 		ApAudienceService,
 		ApDbResolverService,
@@ -422,6 +445,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
 
 		//#region 文字列ベースでのinjection用(循環参照対応のため)
 		$LoggerService,
+		$AbuseReportService,
+		$AbuseReportNotificationService,
 		$AccountMoveService,
 		$AccountUpdateService,
 		$AnnouncementService,
@@ -471,10 +496,13 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
 		$UserKeypairService,
 		$UserListService,
 		$UserMutingService,
+		$UserRenoteMutingService,
+		$UserSearchService,
 		$UserSuspendService,
 		$UserAuthService,
 		$VideoProcessingService,
-		$WebhookService,
+		$UserWebhookService,
+		$SystemWebhookService,
 		$UtilityService,
 		$FileInfoService,
 		$SearchService,
@@ -503,6 +531,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
 
 		$AbuseUserReportEntityService,
 		$AnnouncementEntityService,
+		$AbuseReportNotificationRecipientEntityService,
 		$AntennaEntityService,
 		$AppEntityService,
 		$AuthSessionEntityService,
@@ -536,6 +565,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
 		$RoleEntityService,
 		$ReversiGameEntityService,
 		$MetaEntityService,
+		$SystemWebhookEntityService,
 
 		$ApAudienceService,
 		$ApDbResolverService,
@@ -560,6 +590,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
 	exports: [
 		QueueModule,
 		LoggerService,
+		AbuseReportService,
+		AbuseReportNotificationService,
 		AccountMoveService,
 		AccountUpdateService,
 		AnnouncementService,
@@ -609,10 +641,13 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
 		UserKeypairService,
 		UserListService,
 		UserMutingService,
+		UserRenoteMutingService,
+		UserSearchService,
 		UserSuspendService,
 		UserAuthService,
 		VideoProcessingService,
-		WebhookService,
+		UserWebhookService,
+		SystemWebhookService,
 		UtilityService,
 		FileInfoService,
 		SearchService,
@@ -640,6 +675,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
 
 		AbuseUserReportEntityService,
 		AnnouncementEntityService,
+		AbuseReportNotificationRecipientEntityService,
 		AntennaEntityService,
 		AppEntityService,
 		AuthSessionEntityService,
@@ -673,6 +709,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
 		RoleEntityService,
 		ReversiGameEntityService,
 		MetaEntityService,
+		SystemWebhookEntityService,
 
 		ApAudienceService,
 		ApDbResolverService,
@@ -696,6 +733,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
 
 		//#region 文字列ベースでのinjection用(循環参照対応のため)
 		$LoggerService,
+		$AbuseReportService,
+		$AbuseReportNotificationService,
 		$AccountMoveService,
 		$AccountUpdateService,
 		$AnnouncementService,
@@ -745,10 +784,13 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
 		$UserKeypairService,
 		$UserListService,
 		$UserMutingService,
+		$UserRenoteMutingService,
+		$UserSearchService,
 		$UserSuspendService,
 		$UserAuthService,
 		$VideoProcessingService,
-		$WebhookService,
+		$UserWebhookService,
+		$SystemWebhookService,
 		$UtilityService,
 		$FileInfoService,
 		$SearchService,
@@ -776,6 +818,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
 
 		$AbuseUserReportEntityService,
 		$AnnouncementEntityService,
+		$AbuseReportNotificationRecipientEntityService,
 		$AntennaEntityService,
 		$AppEntityService,
 		$AuthSessionEntityService,
@@ -809,6 +852,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 bfbc2b172d..cd906a72af 100644
--- a/packages/backend/src/core/CustomEmojiService.ts
+++ b/packages/backend/src/core/CustomEmojiService.ts
@@ -26,7 +26,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(
@@ -49,7 +49,7 @@ export class CustomEmojiService implements OnApplicationShutdown {
 		private globalEventService: GlobalEventService,
 		private driveService: DriveService,
 	) {
-		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
@@ -77,7 +77,7 @@ export class CustomEmojiService implements OnApplicationShutdown {
 		localOnly: boolean;
 		roleIdsThatCanBeUsedThisEmojiAsReaction: MiRole['id'][];
 	}, moderator?: MiUser): Promise<MiEmoji> {
-		const emoji = await this.emojisRepository.insert({
+		const emoji = await this.emojisRepository.insertOne({
 			id: this.idService.gen(),
 			updatedAt: new Date(),
 			name: data.name,
@@ -91,7 +91,7 @@ export class CustomEmojiService implements OnApplicationShutdown {
 			isSensitive: data.isSensitive,
 			localOnly: data.localOnly,
 			roleIdsThatCanBeUsedThisEmojiAsReaction: data.roleIdsThatCanBeUsedThisEmojiAsReaction,
-		}).then(x => this.emojisRepository.findOneByOrFail(x.identifiers[0]));
+		});
 
 		if (data.host == null) {
 			this.localEmojisCache.refresh();
@@ -142,6 +142,13 @@ export class CustomEmojiService implements OnApplicationShutdown {
 
 		this.localEmojisCache.refresh();
 
+		if (data.driveFile != null) {
+			const file = await this.driveFilesRepository.findOneBy({ url: emoji.originalUrl, userHost: emoji.host ? emoji.host : IsNull() });
+			if (file && file.id !== data.driveFile.id) {
+				await this.driveService.deleteFile(file, false, moderator ? moderator : undefined);
+			}
+		}
+
 		const packed = await this.emojiEntityService.packDetailed(emoji.id);
 
 		if (emoji.name === data.name) {
@@ -350,14 +357,14 @@ export class CustomEmojiService implements OnApplicationShutdown {
 		if (name == null) return null;
 		if (host == null) return null;
 
-		const newHost = host === this.config.host ? null : host; 
+		const newHost = host === this.config.host ? null : host;
 
 		const queryOrNull = async () => (await this.emojisRepository.findOneBy({
 			name,
 			host: newHost ?? IsNull(),
 		})) ?? 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なので??はだめ)
@@ -384,7 +391,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) {
@@ -399,7 +406,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);
 		}
 	}
 
@@ -424,7 +431,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/DriveService.ts b/packages/backend/src/core/DriveService.ts
index af5451bfc8..46fa4243a7 100644
--- a/packages/backend/src/core/DriveService.ts
+++ b/packages/backend/src/core/DriveService.ts
@@ -43,6 +43,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 */
@@ -127,6 +128,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');
@@ -220,7 +222,7 @@ export class DriveService {
 			file.size = size;
 			file.storedInternal = false;
 
-			return await this.driveFilesRepository.insert(file).then(x => this.driveFilesRepository.findOneByOrFail(x.identifiers[0]));
+			return await this.driveFilesRepository.insertOne(file);
 		} else { // use internal storage
 			const accessKey = randomUUID();
 			const thumbnailAccessKey = 'thumbnail-' + randomUUID();
@@ -254,7 +256,7 @@ export class DriveService {
 			file.md5 = hash;
 			file.size = size;
 
-			return await this.driveFilesRepository.insert(file).then(x => this.driveFilesRepository.findOneByOrFail(x.identifiers[0]));
+			return await this.driveFilesRepository.insertOne(file);
 		}
 	}
 
@@ -567,6 +569,7 @@ export class DriveService {
 			sensitive ?? false
 			: false;
 
+		if (user && this.utilityService.isMediaSilencedHost(instance.mediaSilencedHosts, user.host)) file.isSensitive = true;
 		if (info.sensitive && profile!.autoSensitive) file.isSensitive = true;
 		if (userRoleNSFW) file.isSensitive = true;
 
@@ -594,7 +597,7 @@ export class DriveService {
 				file.type = info.type.mime;
 				file.storedInternal = false;
 
-				file = await this.driveFilesRepository.insert(file).then(x => this.driveFilesRepository.findOneByOrFail(x.identifiers[0]));
+				file = await this.driveFilesRepository.insertOne(file);
 			} catch (err) {
 			// duplicate key error (when already registered)
 				if (isDuplicateKeyValueError(err)) {
diff --git a/packages/backend/src/core/EmailService.ts b/packages/backend/src/core/EmailService.ts
index 08f8f80a6e..435dbbae28 100644
--- a/packages/backend/src/core/EmailService.ts
+++ b/packages/backend/src/core/EmailService.ts
@@ -16,6 +16,7 @@ import type { UserProfilesRepository } from '@/models/_.js';
 import { LoggerService } from '@/core/LoggerService.js';
 import { bindThis } from '@/decorators.js';
 import { HttpRequestService } from '@/core/HttpRequestService.js';
+import { QueueService } from '@/core/QueueService.js';
 
 @Injectable()
 export class EmailService {
@@ -32,6 +33,7 @@ export class EmailService {
 		private loggerService: LoggerService,
 		private utilityService: UtilityService,
 		private httpRequestService: HttpRequestService,
+		private queueService: QueueService,
 	) {
 		this.logger = this.loggerService.getLogger('email');
 	}
diff --git a/packages/backend/src/core/FanoutTimelineEndpointService.ts b/packages/backend/src/core/FanoutTimelineEndpointService.ts
index 5725c795ed..bd86a80cbd 100644
--- a/packages/backend/src/core/FanoutTimelineEndpointService.ts
+++ b/packages/backend/src/core/FanoutTimelineEndpointService.ts
@@ -56,9 +56,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([]);
 
@@ -68,12 +65,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 e73e1d3a9d..7aeeb78178 100644
--- a/packages/backend/src/core/FederatedInstanceService.ts
+++ b/packages/backend/src/core/FederatedInstanceService.ts
@@ -56,11 +56,11 @@ export class FederatedInstanceService implements OnApplicationShutdown {
 		const index = await this.instancesRepository.findOneBy({ host });
 
 		if (index == null) {
-			const i = await this.instancesRepository.insert({
+			const i = await this.instancesRepository.insertOne({
 				id: this.idService.gen(),
 				host,
 				firstRetrievedAt: new Date(),
-			}).then(x => this.instancesRepository.findOneByOrFail(x.identifiers[0]));
+			});
 
 			this.federatedInstanceCache.set(host, i);
 			return i;
diff --git a/packages/backend/src/core/FileInfoService.ts b/packages/backend/src/core/FileInfoService.ts
index 9a9eae3cae..cc66e9fe3a 100644
--- a/packages/backend/src/core/FileInfoService.ts
+++ b/packages/backend/src/core/FileInfoService.ts
@@ -12,7 +12,7 @@ import FFmpeg from 'fluent-ffmpeg';
 import isSvg from 'is-svg';
 import probeImageSize from 'probe-image-size';
 import { sharpBmp } from '@misskey-dev/sharp-read-bmp';
-import { encode } from 'blurhash';
+import * as blurhash from 'blurhash';
 import { LoggerService } from '@/core/LoggerService.js';
 import type Logger from '@/logger.js';
 import { bindThis } from '@/decorators.js';
@@ -283,7 +283,7 @@ export class FileInfoService {
 	}
 
 	/**
-	 * Calculate average color of image
+	 * Calculate blurhash string of image
 	 */
 	@bindThis
 	private getBlurhash(path: string, type: string): Promise<string> {
@@ -298,7 +298,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/GlobalEventService.ts b/packages/backend/src/core/GlobalEventService.ts
index 22871adb16..753011cded 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';
@@ -135,6 +136,7 @@ export interface NoteEventTypes {
 	};
 	replied: {
 		id: MiNote['id'];
+		userId: MiUser['id'];
 	};
 }
 type NoteStreamEventTypes = {
@@ -212,6 +214,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']; };
@@ -231,6 +237,9 @@ export interface InternalEventTypes {
 	webhookCreated: MiWebhook;
 	webhookDeleted: MiWebhook;
 	webhookUpdated: MiWebhook;
+	systemWebhookCreated: MiSystemWebhook;
+	systemWebhookDeleted: MiSystemWebhook;
+	systemWebhookUpdated: MiSystemWebhook;
 	antennaCreated: MiAntenna;
 	antennaDeleted: MiAntenna;
 	antennaUpdated: MiAntenna;
@@ -247,43 +256,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';
@@ -291,11 +302,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/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/MfmService.ts b/packages/backend/src/core/MfmService.ts
index 625df1feaa..1b6951ca19 100644
--- a/packages/backend/src/core/MfmService.ts
+++ b/packages/backend/src/core/MfmService.ts
@@ -6,17 +6,19 @@
 import { URL } from 'node:url';
 import { Inject, Injectable } from '@nestjs/common';
 import * as parse5 from 'parse5';
-import { Window, XMLSerializer } from 'happy-dom';
+import { Window, DocumentFragment, XMLSerializer } from 'happy-dom';
 import { DI } from '@/di-symbols.js';
 import type { Config } from '@/config.js';
 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 '@transfem-org/sfm-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,8 +141,7 @@ export class MfmService {
 				}
 
 				case 'h2':
-				case 'h3':
-				{
+				case 'h3': {
 					text += '**';
 					appendChildren(node.childNodes);
 					text += '**\n';
@@ -148,16 +149,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>';
@@ -165,8 +164,7 @@ export class MfmService {
 				}
 
 				case 's':
-				case 'del':
-				{
+				case 'del': {
 					text += '~~';
 					appendChildren(node.childNodes);
 					text += '~~';
@@ -174,8 +172,7 @@ export class MfmService {
 				}
 
 				case 'i':
-				case 'em':
-				{
+				case 'em': {
 					text += '<i>';
 					appendChildren(node.childNodes);
 					text += '</i>';
@@ -214,8 +211,7 @@ export class MfmService {
 				case 'p':
 				case 'h4':
 				case 'h5':
-				case 'h6':
-				{
+				case 'h6': {
 					text += '\n\n';
 					appendChildren(node.childNodes);
 					break;
@@ -228,8 +224,7 @@ export class MfmService {
 				case 'article':
 				case 'li':
 				case 'dt':
-				case 'dd':
-				{
+				case 'dd': {
 					text += '\n';
 					appendChildren(node.childNodes);
 					break;
@@ -483,6 +478,8 @@ export class MfmService {
 
 		const doc = window.document;
 
+		const body = doc.createElement('p');
+
 		async function appendChildren(children: mfm.MfmNode[], targetElement: any): Promise<void> {
 			if (children) {
 				for (const child of await Promise.all(children.map(async (x) => await (handlers as any)[x.type](x)))) targetElement.appendChild(child);
@@ -661,7 +658,7 @@ export class MfmService {
 			},
 		};
 
-		await appendChildren(nodes, doc.body);
+		await appendChildren(nodes, body);
 
 		if (quoteUri !== null) {
 			const a = doc.createElement('a');
@@ -675,9 +672,15 @@ export class MfmService {
 			quote.innerHTML += 'RE: ';
 			quote.appendChild(a);
 
-			doc.body.appendChild(quote);
+			body.appendChild(quote);
 		}
 
-		return inline ? doc.body.innerHTML : `<p>${doc.body.innerHTML}</p>`;
+		let result = new XMLSerializer().serializeToString(body);
+
+		if (inline) {
+			result = result.replace(/^<p>/, '').replace(/<\/p>$/, '');
+		}
+
+		return result;
 	}
 }
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 41efa76f3f..c252336f99 100644
--- a/packages/backend/src/core/NoteCreateService.ts
+++ b/packages/backend/src/core/NoteCreateService.ts
@@ -38,7 +38,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';
@@ -61,7 +61,6 @@ import { CacheService } from '@/core/CacheService.js';
 import { isReply } from '@/misc/is-reply.js';
 import { trackPromise } from '@/misc/promise-tracker.js';
 import { isUserRelated } from '@/misc/is-user-related.js';
-import { isNotNull } from '@/misc/is-not-null.js';
 import { IdentifiableError } from '@/misc/identifiable-error.js';
 
 type NotificationType = 'reply' | 'renote' | 'quote' | 'mention';
@@ -207,7 +206,7 @@ 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,
@@ -282,192 +281,6 @@ export class NoteCreateService implements OnApplicationShutdown {
 			data.visibility = 'home';
 		}
 
-		if (this.isRenote(data)) {
-			switch (data.renote.visibility) {
-				case 'public':
-					// public noteは無条件にrenote可能
-					break;
-				case 'home':
-					// home noteはhome以下にrenote可能
-					if (data.visibility === 'public') {
-						data.visibility = 'home';
-					}
-					break;
-				case 'followers':
-					// 他人のfollowers noteはreject
-					if (data.renote.userId !== user.id) {
-						throw new Error('Renote target is not public or home');
-					}
-
-					// Renote対象がfollowersならfollowersにする
-					data.visibility = 'followers';
-					break;
-				case 'specified':
-					// specified / direct noteはreject
-					throw new Error('Renote target is not public or home');
-			}
-		}
-
-		// Check blocking
-		if (this.isRenote(data) && !this.isQuote(data)) {
-			if (data.renote.userHost === null) {
-				if (data.renote.userId !== user.id) {
-					const blocked = await this.userBlockingService.checkBlocked(data.renote.userId, user.id);
-					if (blocked) {
-						throw new Error('blocked');
-					}
-				}
-			}
-		}
-
-		// 返信対象がpublicではないならhomeにする
-		if (data.reply && data.reply.visibility !== 'public' && data.visibility === 'public') {
-			data.visibility = 'home';
-		}
-
-		// ローカルのみをRenoteしたらローカルのみにする
-		if (data.renote && data.renote.localOnly && data.channel == null) {
-			data.localOnly = true;
-		}
-
-		// ローカルのみにリプライしたらローカルのみにする
-		if (data.reply && data.reply.localOnly && data.channel == null) {
-			data.localOnly = true;
-		}
-
-		if (data.text) {
-			if (data.text.length > DB_MAX_NOTE_TEXT_LENGTH) {
-				data.text = data.text.slice(0, DB_MAX_NOTE_TEXT_LENGTH);
-			}
-			data.text = data.text.trim();
-			if (data.text === '') {
-				data.text = null;
-			}
-		} else {
-			data.text = null;
-		}
-
-		let tags = data.apHashtags;
-		let emojis = data.apEmojis;
-		let mentionedUsers = data.apMentions;
-
-		// Parse MFM if needed
-		if (!tags || !emojis || !mentionedUsers) {
-			const tokens = (data.text ? mfm.parse(data.text)! : []);
-			const cwTokens = data.cw ? mfm.parse(data.cw)! : [];
-			const choiceTokens = data.poll && data.poll.choices
-				? concat(data.poll.choices.map(choice => mfm.parse(choice)!))
-				: [];
-
-			const combinedTokens = tokens.concat(cwTokens).concat(choiceTokens);
-
-			tags = data.apHashtags ?? extractHashtags(combinedTokens);
-
-			emojis = data.apEmojis ?? extractCustomEmojisFromMfm(combinedTokens);
-
-			mentionedUsers = data.apMentions ?? await this.extractMentionedUsers(user, combinedTokens);
-		}
-
-		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)) {
-			mentionedUsers.push(await this.usersRepository.findOneByOrFail({ id: data.reply!.userId }));
-		}
-
-		if (data.visibility === 'specified') {
-			if (data.visibleUsers == null) throw new Error('invalid param');
-
-			for (const u of data.visibleUsers) {
-				if (!mentionedUsers.some(x => x.id === u.id)) {
-					mentionedUsers.push(u);
-				}
-			}
-
-			if (data.reply && !data.visibleUsers.some(x => x.id === data.reply!.userId)) {
-				data.visibleUsers.push(await this.usersRepository.findOneByOrFail({ id: data.reply!.userId }));
-			}
-		}
-
-		if (user.host && !data.cw) {
-			await this.federatedInstanceService.fetch(user.host).then(async i => {
-				if (i.isNSFW) {
-					data.cw = 'Instance is marked as NSFW';
-				}
-			});
-		}
-
-		if (mentionedUsers.length > 0 && mentionedUsers.length > (await this.roleService.getUserPolicies(user.id)).mentionLimit) {
-			throw new IdentifiableError('9f466dab-c856-48cd-9e65-ff90ff750580', 'Note contains too many mentions');
-		}
-
-		const note = await this.insertNote(user, data, tags, emojis, mentionedUsers);
-
-		setImmediate('post created', { signal: this.#shutdownController.signal }).then(
-			() => this.postNoteCreated(note, user, data, silent, tags!, mentionedUsers!),
-			() => { /* aborted, ignore this */ },
-		);
-
-		return note;
-	}
-
-	@bindThis
-	public async import(user: {
-		id: MiUser['id'];
-		username: MiUser['username'];
-		host: MiUser['host'];
-		isBot: MiUser['isBot'];
-		noindex: MiUser['noindex'];
-	}, data: Option, silent = false): Promise<MiNote> {
-		// チャンネル外にリプライしたら対象のスコープに合わせる
-		// (クライアントサイドでやっても良い処理だと思うけどとりあえずサーバーサイドで)
-		if (data.reply && data.channel && data.reply.channelId !== data.channel.id) {
-			if (data.reply.channelId) {
-				data.channel = await this.channelsRepository.findOneBy({ id: data.reply.channelId });
-			} else {
-				data.channel = null;
-			}
-		}
-
-		// チャンネル内にリプライしたら対象のスコープに合わせる
-		// (クライアントサイドでやっても良い処理だと思うけどとりあえずサーバーサイドで)
-		if (data.reply && (data.channel == null) && data.reply.channelId) {
-			data.channel = await this.channelsRepository.findOneBy({ id: data.reply.channelId });
-		}
-
-		if (data.createdAt == null) data.createdAt = new Date();
-		if (data.visibility == null) data.visibility = 'public';
-		if (data.localOnly == null) data.localOnly = false;
-		if (data.channel != null) data.visibility = 'public';
-		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;
-			if (this.utilityService.isKeyWordIncluded(data.cw ?? data.text ?? '', sensitiveWords)) {
-				data.visibility = 'home';
-			} else if ((await this.roleService.getUserPolicies(user.id)).canPublicNote === false) {
-				data.visibility = 'home';
-			}
-		}
-
-		const hasProhibitedWords = await this.checkProhibitedWordsContain({
-			cw: data.cw,
-			text: data.text,
-			pollChoices: data.poll?.choices,
-		}, 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);
-
-		if (data.visibility === 'public' && inSilencedInstance && user.host !== null) {
-			data.visibility = 'home';
-		}
-
 		if (data.renote) {
 			switch (data.renote.visibility) {
 				case 'public':
@@ -554,6 +367,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(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)) {
@@ -574,6 +390,14 @@ export class NoteCreateService implements OnApplicationShutdown {
 			}
 		}
 
+		if (user.host && !data.cw) {
+			await this.federatedInstanceService.fetch(user.host).then(async i => {
+				if (i.isNSFW) {
+					data.cw = 'Instance is marked as NSFW';
+				}
+			});
+		}
+
 		if (mentionedUsers.length > 0 && mentionedUsers.length > (await this.roleService.getUserPolicies(user.id)).mentionLimit) {
 			throw new IdentifiableError('9f466dab-c856-48cd-9e65-ff90ff750580', 'Note contains too many mentions');
 		}
@@ -581,13 +405,24 @@ export class NoteCreateService implements OnApplicationShutdown {
 		const note = await this.insertNote(user, data, tags, emojis, mentionedUsers);
 
 		setImmediate('post created', { signal: this.#shutdownController.signal }).then(
-			() => this.postNoteImported(note, user, data, silent, tags!, mentionedUsers!),
+			() => this.postNoteCreated(note, user, data, silent, tags!, mentionedUsers!),
 			() => { /* aborted, ignore this */ },
 		);
 
 		return note;
 	}
 
+	@bindThis
+	public async import(user: {
+		id: MiUser['id'];
+		username: MiUser['username'];
+		host: MiUser['host'];
+		isBot: MiUser['isBot'];
+		noindex: MiUser['noindex'];
+	}, data: Option): Promise<MiNote> {
+		return this.create(user, data, true);
+	}
+
 	@bindThis
 	private async insertNote(user: { id: MiUser['id']; host: MiUser['host']; }, data: Option, tags: string[], emojis: string[], mentionedUsers: MinimumUser[]) {
 		const insert = new MiNote({
@@ -705,7 +540,7 @@ export class NoteCreateService implements OnApplicationShutdown {
 		const meta = await this.metaService.fetch();
 
 		this.notesChart.update(note, true);
-		if (meta.enableChartsForRemoteUser || (user.host == null)) {
+		if (note.visibility !== 'specified' && (meta.enableChartsForRemoteUser || (user.host == null))) {
 			this.perUserNotesChart.update(user, note, true);
 		}
 
@@ -817,7 +652,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,
 					});
 				}
@@ -831,6 +666,7 @@ export class NoteCreateService implements OnApplicationShutdown {
 			if (data.reply) {
 				this.globalEventService.publishNoteStream(data.reply.id, 'replied', {
 					id: note.id,
+					userId: user.id,
 				});
 				// 通知
 				if (data.reply.userHost === null) {
@@ -855,7 +691,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,
 							});
 						}
@@ -895,7 +731,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,
 						});
 					}
@@ -964,105 +800,6 @@ export class NoteCreateService implements OnApplicationShutdown {
 		if (!user.noindex) this.index(note);
 	}
 
-	@bindThis
-	private async postNoteImported(note: MiNote, user: {
-		id: MiUser['id'];
-		username: MiUser['username'];
-		host: MiUser['host'];
-		isBot: MiUser['isBot'];
-		noindex: MiUser['noindex'];
-	}, 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)) {
-			this.perUserNotesChart.update(user, note, true);
-		}
-
-		// Register host
-		if (this.userEntityService.isRemoteUser(user)) {
-			this.federatedInstanceService.fetch(user.host).then(async i => {
-				if (note.renote && note.text) {
-					this.instancesRepository.increment({ id: i.id }, 'notesCount', 1);
-				} else if (!note.renote) {
-					this.instancesRepository.increment({ id: i.id }, 'notesCount', 1);
-				}
-				if ((await this.metaService.fetch()).enableChartsForFederatedInstances) {
-					this.instanceChart.updateNote(i.host, note, true);
-				}
-			});
-		}
-
-		if (data.renote && data.text) {
-			// Increment notes count (user)
-			this.incNotesCountOfUser(user);
-		} else if (!data.renote) {
-			// Increment notes count (user)
-			this.incNotesCountOfUser(user);
-		}
-
-		this.pushToTl(note, user);
-
-		this.antennaService.addNoteToAntennas(note, user);
-
-		if (data.reply) {
-			this.saveReply(data.reply, note);
-		}
-
-		if (data.reply == null) {
-			// TODO: キャッシュ
-			this.followingsRepository.findBy({
-				followeeId: user.id,
-				notify: 'normal',
-			}).then(followings => {
-				for (const following of followings) {
-					// TODO: ワードミュート考慮
-					this.notificationService.createNotification(following.followerId, 'note', {
-						noteId: note.id,
-					}, user.id);
-				}
-			});
-		}
-
-		if (data.renote && data.text == null && data.renote.userId !== user.id && !user.isBot) {
-			this.incRenoteCount(data.renote);
-		}
-
-		if (data.poll && data.poll.expiresAt) {
-			const delay = data.poll.expiresAt.getTime() - Date.now();
-			this.queueService.endedPollNotificationQueue.add(note.id, {
-				noteId: note.id,
-			}, {
-				delay,
-				removeOnComplete: true,
-			});
-		}
-
-		// Pack the note
-		const noteObj = await this.noteEntityService.pack(note, null, { skipHide: true, withReactionAndUserPairCache: true });
-
-		if (data.channel) {
-			this.channelsRepository.increment({ id: data.channel.id }, 'notesCount', 1);
-			this.channelsRepository.update(data.channel.id, {
-				lastNotedAt: new Date(),
-			});
-
-			this.notesRepository.countBy({
-				userId: user.id,
-				channelId: data.channel.id,
-			}).then(count => {
-				// この処理が行われるのはノート作成後なので、ノートが一つしかなかったら最初の投稿だと判断できる
-				// TODO: とはいえノートを削除して何回も投稿すればその分だけインクリメントされる雑さもあるのでどうにかしたい
-				if (count === 1) {
-					this.channelsRepository.increment({ id: data.channel!.id }, 'usersCount', 1);
-				}
-			});
-		}
-
-		// Register to search database
-		if (!user.noindex) this.index(note);
-	}
-
 	@bindThis
 	private isRenote(note: Option): note is Option & { renote: MiNote } {
 		return note.renote != null;
@@ -1134,7 +871,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,
 				});
 			}
@@ -1185,7 +922,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) =>
@@ -1280,10 +1017,13 @@ export class NoteCreateService implements OnApplicationShutdown {
 				}
 			}
 
-			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, meta.perUserHomeTimelineCacheMax, r);
+					if (note.fileIds.length > 0) {
+						this.fanoutTimelineService.push(`homeTimelineWithFiles:${user.id}`, note.id, meta.perUserHomeTimelineCacheMax / 2, r);
+					}
 				}
 			}
 
diff --git a/packages/backend/src/core/NoteDeleteService.ts b/packages/backend/src/core/NoteDeleteService.ts
index af65750a01..7ce6d7c605 100644
--- a/packages/backend/src/core/NoteDeleteService.ts
+++ b/packages/backend/src/core/NoteDeleteService.ts
@@ -99,7 +99,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;
diff --git a/packages/backend/src/core/NoteEditService.ts b/packages/backend/src/core/NoteEditService.ts
index 0cb58d04a2..5ff0f26e2b 100644
--- a/packages/backend/src/core/NoteEditService.ts
+++ b/packages/backend/src/core/NoteEditService.ts
@@ -31,7 +31,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 { QueueService } from '@/core/QueueService.js';
 import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
 import { UserEntityService } from '@/core/entities/UserEntityService.js';
@@ -51,7 +51,6 @@ import { CacheService } from '@/core/CacheService.js';
 import { isReply } from '@/misc/is-reply.js';
 import { trackPromise } from '@/misc/promise-tracker.js';
 import { isUserRelated } from '@/misc/is-user-related.js';
-import { isNotNull } from '@/misc/is-not-null.js';
 import { IdentifiableError } from '@/misc/identifiable-error.js';
 
 type NotificationType = 'reply' | 'renote' | 'quote' | 'mention' | 'edited';
@@ -203,7 +202,7 @@ export class NoteEditService implements OnApplicationShutdown {
 		private notificationService: NotificationService,
 		private relayService: RelayService,
 		private federatedInstanceService: FederatedInstanceService,
-		private webhookService: WebhookService,
+		private webhookService: UserWebhookService,
 		private remoteUserResolveService: RemoteUserResolveService,
 		private apDeliverManagerService: ApDeliverManagerService,
 		private apRendererService: ApRendererService,
@@ -388,6 +387,9 @@ export class NoteEditService 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(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)) {
@@ -631,7 +633,7 @@ export class NoteEditService 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,
 					});
 				}
@@ -666,7 +668,7 @@ export class NoteEditService implements OnApplicationShutdown {
 
 						const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === data.reply!.userId && x.on.includes('edited'));
 						for (const webhook of webhooks) {
-							this.queueService.webhookDeliver(webhook, 'edited', {
+							this.queueService.userWebhookDeliver(webhook, 'edited', {
 								note: noteObj,
 							});
 						}
@@ -801,7 +803,7 @@ export class NoteEditService implements OnApplicationShutdown {
 
 			const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === u.id && x.on.includes('edited'));
 			for (const webhook of webhooks) {
-				this.queueService.webhookDeliver(webhook, 'edited', {
+				this.queueService.userWebhookDeliver(webhook, 'edited', {
 					note: detailPackedNote,
 				});
 			}
@@ -838,7 +840,7 @@ export class NoteEditService 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) as MiUser[];
+		))).filter(x => x !== null) as MiUser[];
 
 		// Drop duplicate users
 		mentionedUsers = mentionedUsers.filter((u, i, self) =>
@@ -933,10 +935,13 @@ export class NoteEditService implements OnApplicationShutdown {
 				}
 			}
 
-			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, meta.perUserHomeTimelineCacheMax, r);
+					if (note.fileIds.length > 0) {
+						this.fanoutTimelineService.push(`homeTimelineWithFiles:${user.id}`, note.id, meta.perUserHomeTimelineCacheMax / 2, r);
+					}
 				}
 			}
 
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 103813acf2..be5f10771a 100644
--- a/packages/backend/src/core/QueueService.ts
+++ b/packages/backend/src/core/QueueService.ts
@@ -8,16 +8,34 @@ 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 { MiNote } from '@/models/Note.js';
-import { ApRequestCreator } from '@/core/activitypub/ApRequestService.js';
 
 @Injectable()
 export class QueueService {
@@ -32,7 +50,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', {
 		}, {
@@ -490,9 +509,13 @@ export class QueueService {
 		});
 	}
 
+	/**
+	 * @see UserWebhookDeliverJobData
+	 * @see WebhookDeliverProcessorService
+	 */
 	@bindThis
-	public webhookDeliver(webhook: MiWebhook, type: typeof webhookEventTypes[number], content: unknown) {
-		const data = {
+	public userWebhookDeliver(webhook: MiWebhook, type: typeof webhookEventTypes[number], content: unknown) {
+		const data: UserWebhookDeliverJobData = {
 			type,
 			content,
 			webhookId: webhook.id,
@@ -503,7 +526,33 @@ export class QueueService {
 			eventId: randomUUID(),
 		};
 
-		return this.webhookDeliverQueue.add(webhook.id, data, {
+		return this.userWebhookDeliverQueue.add(webhook.id, data, {
+			attempts: 4,
+			backoff: {
+				type: 'custom',
+			},
+			removeOnComplete: true,
+			removeOnFail: true,
+		});
+	}
+
+	/**
+	 * @see SystemWebhookDeliverJobData
+	 * @see WebhookDeliverProcessorService
+	 */
+	@bindThis
+	public systemWebhookDeliver(webhook: MiSystemWebhook, type: SystemWebhookEventType, content: unknown) {
+		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: 4,
 			backoff: {
 				type: 'custom',
diff --git a/packages/backend/src/core/ReactionService.ts b/packages/backend/src/core/ReactionService.ts
index c0b59e635d..17ff168786 100644
--- a/packages/backend/src/core/ReactionService.ts
+++ b/packages/backend/src/core/ReactionService.ts
@@ -29,6 +29,7 @@ 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';
 
 const FALLBACK = '\u2764';
 const PER_NOTE_REACTION_USER_PAIR_CACHE_MAX = 16;
@@ -107,6 +108,8 @@ export class ReactionService {
 
 	@bindThis
 	public async create(user: { id: MiUser['id']; host: MiUser['host']; isBot: MiUser['isBot'] }, note: MiNote, _reaction?: string | null) {
+		const meta = await this.metaService.fetch();
+
 		// Check blocking
 		if (note.userId !== user.id) {
 			const blocked = await this.userBlockingService.checkBlocked(note.userId, user.id);
@@ -120,11 +123,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);
@@ -145,6 +153,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(meta.mediaSilencedHosts, reacterHost)) {
+							reaction = FALLBACK;
+						}
 					} else {
 						// リアクションとして使う権限がない
 						reaction = FALLBACK;
@@ -217,8 +230,6 @@ export class ReactionService {
 			}
 		}
 
-		const meta = await this.metaService.fetch();
-
 		if (meta.enableChartsForRemoteUser || (user.host == null)) {
 			this.perUserReactionsChart.update(user, note);
 		}
diff --git a/packages/backend/src/core/RelayService.ts b/packages/backend/src/core/RelayService.ts
index e9dc9b57af..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
@@ -53,11 +53,11 @@ export class RelayService {
 
 	@bindThis
 	public async addRelay(inbox: string): Promise<MiRelay> {
-		const relay = await this.relaysRepository.insert({
+		const relay = await this.relaysRepository.insertOne({
 			id: this.idService.gen(),
 			inbox,
 			status: 'requesting',
-		}).then(x => this.relaysRepository.findOneByOrFail(x.identifiers[0]));
+		});
 
 		const relayActor = await this.getRelayActor();
 		const follow = await this.apRendererService.renderFollowRelay(relay, relayActor);
diff --git a/packages/backend/src/core/ReversiService.ts b/packages/backend/src/core/ReversiService.ts
index 53a7234823..8c0a8f6cc7 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 {
@@ -281,7 +282,7 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
 
 	@bindThis
 	private async matched(parentId: MiUser['id'], childId: MiUser['id'], options: { noIrregularRules: boolean; }): Promise<MiReversiGame> {
-		const game = await this.reversiGamesRepository.insert({
+		const game = await this.reversiGamesRepository.insertOne({
 			id: this.idService.gen(),
 			user1Id: parentId,
 			user2Id: childId,
@@ -294,10 +295,7 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
 			bw: 'random',
 			isLlotheo: false,
 			noIrregularRules: options.noIrregularRules,
-		}).then(x => this.reversiGamesRepository.findOneOrFail({
-			where: { id: x.identifiers[0].id },
-			relations: ['user1', 'user2'],
-		}));
+		}, { relations: ['user1', 'user2'] });
 		this.cacheGame(game);
 
 		const packed = await this.reversiGameEntityService.packDetail(game);
@@ -402,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;
@@ -410,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 f5a753afc7..7984dc5627 100644
--- a/packages/backend/src/core/RoleService.ts
+++ b/packages/backend/src/core/RoleService.ts
@@ -48,6 +48,7 @@ export type RolePolicies = {
 	canHideAds: boolean;
 	driveCapacityMb: number;
 	alwaysMarkNsfw: boolean;
+	canUpdateBioMedia: boolean;
 	pinLimit: number;
 	antennaLimit: number;
 	wordMuteLimit: number;
@@ -78,6 +79,7 @@ export const DEFAULT_POLICIES: RolePolicies = {
 	canHideAds: false,
 	driveCapacityMb: 100,
 	alwaysMarkNsfw: false,
+	canUpdateBioMedia: true,
 	pinLimit: 5,
 	antennaLimit: 5,
 	wordMuteLimit: 200,
@@ -129,10 +131,8 @@ 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.rolesCache = new MemorySingleCache<MiRole[]>(1000 * 60 * 60); // 1h
+		this.roleAssignmentByUserIdCache = new MemoryKVCache<MiRoleAssignment[]>(1000 * 60 * 5); // 5m
 
 		this.redisForSub.on('message', this.onMessage);
 	}
@@ -381,6 +381,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)),
@@ -416,14 +417,32 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
 	}
 
 	@bindThis
-	public async getModeratorIds(includeAdmins = true): Promise<MiUser['id'][]> {
+	public async getModeratorIds(includeAdmins = true, excludeExpire = false): Promise<MiUser['id'][]> {
 		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)),
-		}) : [];
+		const moderatorRoles = includeAdmins
+			? roles.filter(r => r.isModerator || r.isAdministrator)
+			: roles.filter(r => r.isModerator);
+
 		// TODO: isRootなアカウントも含める
-		return assigns.map(a => a.userId);
+		const assigns = moderatorRoles.length > 0
+			? await this.roleAssignmentsRepository.findBy({ roleId: In(moderatorRoles.map(r => r.id)) })
+			: [];
+
+		const now = Date.now();
+		const result = [
+			// Setを経由して重複を除去(ユーザIDは重複する可能性があるので)
+			...new Set(
+				assigns
+					.filter(it =>
+						(excludeExpire)
+							? (it.expiresAt == null || it.expiresAt.getTime() > now)
+							: true,
+					)
+					.map(a => a.userId),
+			),
+		];
+
+		return result.sort((x, y) => x.localeCompare(y));
 	}
 
 	@bindThis
@@ -477,12 +496,12 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
 			}
 		}
 
-		const created = await this.roleAssignmentsRepository.insert({
+		const created = await this.roleAssignmentsRepository.insertOne({
 			id: this.idService.gen(now),
 			expiresAt: expiresAt,
 			roleId: roleId,
 			userId: userId,
-		}).then(x => this.roleAssignmentsRepository.findOneByOrFail(x.identifiers[0]));
+		});
 
 		this.rolesRepository.update(roleId, {
 			lastUsedAt: new Date(),
@@ -490,14 +509,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,
@@ -564,7 +584,7 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
 	@bindThis
 	public async create(values: Partial<MiRole>, moderator?: MiUser): Promise<MiRole> {
 		const date = new Date();
-		const created = await this.rolesRepository.insert({
+		const created = await this.rolesRepository.insertOne({
 			id: this.idService.gen(date.getTime()),
 			updatedAt: date,
 			lastUsedAt: date,
@@ -582,7 +602,7 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
 			canEditMembersByModerator: values.canEditMembersByModerator,
 			displayOrder: values.displayOrder,
 			policies: values.policies,
-		}).then(x => this.rolesRepository.findOneByOrFail(x.identifiers[0]));
+		});
 
 		this.globalEventService.publishInternalEvent('roleCreated', created);
 
diff --git a/packages/backend/src/core/SignupService.ts b/packages/backend/src/core/SignupService.ts
index e3d69e5e94..80907a8921 100644
--- a/packages/backend/src/core/SignupService.ts
+++ b/packages/backend/src/core/SignupService.ts
@@ -22,6 +22,7 @@ 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 {
@@ -36,6 +37,7 @@ export class SignupService {
 		private usedUsernamesRepository: UsedUsernamesRepository,
 
 		private utilityService: UtilityService,
+		private userService: UserService,
 		private userEntityService: UserEntityService,
 		private idService: IdService,
 		private metaService: MetaService,
@@ -155,7 +157,8 @@ export class SignupService {
 			}));
 		});
 
-		this.usersChart.update(account, true);
+		this.usersChart.update(account, true).then();
+		this.userService.notifySystemWebhook(account, 'userCreated').then();
 
 		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..bc6851f788
--- /dev/null
+++ b/packages/backend/src/core/SystemWebhookService.ts
@@ -0,0 +1,233 @@
+/*
+ * 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 async 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,
+			})
+			.then();
+
+		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,
+			})
+			.then();
+
+		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,
+			})
+			.then();
+	}
+
+	/**
+	 * SystemWebhook をWebhook配送キューに追加する
+	 * @see QueueService.systemWebhookDeliver
+	 */
+	@bindThis
+	public async enqueueSystemWebhook(webhook: MiSystemWebhook | MiSystemWebhook['id'], type: SystemWebhookEventType, content: unknown) {
+		const webhookEntity = typeof webhook === 'string'
+			? (await this.fetchActiveSystemWebhooks()).find(a => a.id === webhook)
+			: webhook;
+		if (!webhookEntity || !webhookEntity.isActive) {
+			this.logger.info(`Webhook is not active or not found : ${webhook}`);
+			return;
+		}
+
+		if (!webhookEntity.on.includes(type)) {
+			this.logger.info(`Webhook ${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 deeecdeb1f..6aab8fde70 100644
--- a/packages/backend/src/core/UserFollowingService.ts
+++ b/packages/backend/src/core/UserFollowingService.ts
@@ -16,7 +16,7 @@ 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';
@@ -82,7 +82,7 @@ export class UserFollowingService implements OnModuleInit {
 		private metaService: MetaService,
 		private notificationService: NotificationService,
 		private federatedInstanceService: FederatedInstanceService,
-		private webhookService: WebhookService,
+		private webhookService: UserWebhookService,
 		private apRendererService: ApRendererService,
 		private accountMoveService: AccountMoveService,
 		private fanoutTimelineService: FanoutTimelineService,
@@ -279,8 +279,10 @@ export class UserFollowingService implements OnModuleInit {
 			});
 
 			// 通知を作成
-			this.notificationService.createNotification(follower.id, 'followRequestAccepted', {
-			}, followee.id);
+			if (follower.host === null) {
+				this.notificationService.createNotification(follower.id, 'followRequestAccepted', {
+				}, followee.id);
+			}
 		}
 
 		if (alreadyFollowed) return;
@@ -331,7 +333,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 +347,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 +400,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,
 					});
 				}
@@ -517,7 +519,7 @@ export class UserFollowingService implements OnModuleInit {
 			followerId: follower.id,
 		});
 
-		const followRequest = await this.followRequestsRepository.insert({
+		const followRequest = await this.followRequestsRepository.insertOne({
 			id: this.idService.gen(),
 			followerId: follower.id,
 			followeeId: followee.id,
@@ -531,7 +533,7 @@ export class UserFollowingService implements OnModuleInit {
 			followeeHost: followee.host,
 			followeeInbox: this.userEntityService.isRemoteUser(followee) ? followee.inbox : undefined,
 			followeeSharedInbox: this.userEntityService.isRemoteUser(followee) ? followee.sharedInbox : undefined,
-		}).then(x => this.followRequestsRepository.findOneByOrFail(x.identifiers[0]));
+		});
 
 		// Publish receiveRequest event
 		if (this.userEntityService.isLocalUser(followee)) {
@@ -740,7 +742,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..e96bfeea95
--- /dev/null
+++ b/packages/backend/src/core/UserWebhookService.ts
@@ -0,0 +1,99 @@
+/*
+ * 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 { 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;
+	}
+
+	@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 21c4af3ca5..22871e44f8 100644
--- a/packages/backend/src/core/UtilityService.ts
+++ b/packages/backend/src/core/UtilityService.ts
@@ -42,6 +42,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()}`.endsWith(`.${x}`));
+	}
+
 	@bindThis
 	public concatNoteContentsForKeyWordCheck(content: {
 		cw?: string | null;
diff --git a/packages/backend/src/core/WebfingerService.ts b/packages/backend/src/core/WebfingerService.ts
index 374536a741..1517dd0074 100644
--- a/packages/backend/src/core/WebfingerService.ts
+++ b/packages/backend/src/core/WebfingerService.ts
@@ -5,7 +5,7 @@
 
 import { URL } from 'node:url';
 import { Injectable } from '@nestjs/common';
-import { query as urlQuery } from '@/misc/prelude/url.js';
+import { XMLParser } from 'fast-xml-parser';
 import { HttpRequestService } from '@/core/HttpRequestService.js';
 import { bindThis } from '@/decorators.js';
 
@@ -22,6 +22,10 @@ export type IWebFinger = {
 const urlRegex = /^https?:\/\//;
 const mRegex = /^([^@]+)@(.*)/;
 
+// we have the colons here, because URL.protocol does as well, so it's
+// more uniform in the places we use both
+const defaultProtocol = process.env.MISSKEY_WEBFINGER_USE_HTTP?.toLowerCase() === 'true' ? 'http:' : 'https:';
+
 @Injectable()
 export class WebfingerService {
 	constructor(
@@ -31,25 +35,76 @@ export class WebfingerService {
 
 	@bindThis
 	public async webfinger(query: string): Promise<IWebFinger> {
-		const url = this.genUrl(query);
+		const hostMetaUrl = this.queryToHostMetaUrl(query);
+		const template = await this.fetchWebFingerTemplateFromHostMeta(hostMetaUrl) ?? this.queryToWebFingerTemplate(query);
+		const url = this.genUrl(query, template);
 
 		return await this.httpRequestService.getJson<IWebFinger>(url, 'application/jrd+json, application/json');
 	}
 
 	@bindThis
-	private genUrl(query: string): string {
+	private genUrl(query: string, template: string): string {
+		if (template.indexOf('{uri}') < 0) throw new Error(`Invalid webFingerUrl: ${template}`);
+
+		if (query.match(urlRegex)) {
+			return template.replace('{uri}', encodeURIComponent(query));
+		}
+
+		const m = query.match(mRegex);
+		if (m) {
+			return template.replace('{uri}', encodeURIComponent(`acct:${query}`));
+		}
+
+		throw new Error(`Invalid query (${query})`);
+	}
+
+	@bindThis
+	private queryToWebFingerTemplate(query: string): string {
 		if (query.match(urlRegex)) {
 			const u = new URL(query);
-			return `${u.protocol}//${u.hostname}/.well-known/webfinger?` + urlQuery({ resource: query });
+			return `${u.protocol}//${u.hostname}/.well-known/webfinger?resource={uri}`;
 		}
 
 		const m = query.match(mRegex);
 		if (m) {
 			const hostname = m[2];
-			const useHttp = process.env.MISSKEY_WEBFINGER_USE_HTTP && process.env.MISSKEY_WEBFINGER_USE_HTTP.toLowerCase() === 'true';
-			return `http${useHttp ? '' : 's'}://${hostname}/.well-known/webfinger?${urlQuery({ resource: `acct:${query}` })}`;
+			return `${defaultProtocol}//${hostname}/.well-known/webfinger?resource={uri}`;
 		}
 
 		throw new Error(`Invalid query (${query})`);
 	}
+
+	@bindThis
+	private queryToHostMetaUrl(query: string): string {
+		if (query.match(urlRegex)) {
+			const u = new URL(query);
+			return `${u.protocol}//${u.hostname}/.well-known/host-meta`;
+		}
+
+		const m = query.match(mRegex);
+		if (m) {
+			const hostname = m[2];
+			return `${defaultProtocol}//${hostname}/.well-known/host-meta`;
+		}
+
+		throw new Error(`Invalid query (${query})`);
+	}
+
+	@bindThis
+	private async fetchWebFingerTemplateFromHostMeta(url: string): Promise<string | null> {
+		try {
+			const res = await this.httpRequestService.getHtml(url, 'application/xrd+xml');
+			const options = {
+				ignoreAttributes: false,
+				isArray: (_name: string, jpath: string) => jpath === 'XRD.Link',
+			};
+			const parser = new XMLParser(options);
+			const hostMeta = parser.parse(res);
+			const template = (hostMeta['XRD']['Link'] as Array<any>).filter(p => p['@_rel'] === 'lrdd')[0]['@_template'];
+			return template.indexOf('{uri}') < 0 ? null : template;
+		} catch (err) {
+			console.error(`error while request host-meta for ${url}: ${err}`);
+			return null;
+		}
+	}
 }
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/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 44680a2ed5..062af39732 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 cf66816566..6a28cbad15 100644
--- a/packages/backend/src/core/activitypub/ApInboxService.ts
+++ b/packages/backend/src/core/activitypub/ApInboxService.ts
@@ -27,8 +27,8 @@ import { QueueService } from '@/core/QueueService.js';
 import type { UsersRepository, NotesRepository, FollowingsRepository, AbuseUserReportsRepository, FollowRequestsRepository } 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';
@@ -57,9 +57,6 @@ export class ApInboxService {
 		@Inject(DI.followingsRepository)
 		private followingsRepository: FollowingsRepository,
 
-		@Inject(DI.abuseUserReportsRepository)
-		private abuseUserReportsRepository: AbuseUserReportsRepository,
-
 		@Inject(DI.followRequestsRepository)
 		private followRequestsRepository: FollowRequestsRepository,
 
@@ -68,6 +65,7 @@ export class ApInboxService {
 		private utilityService: UtilityService,
 		private idService: IdService,
 		private metaService: MetaService,
+		private abuseReportService: AbuseReportService,
 		private userFollowingService: UserFollowingService,
 		private apAudienceService: ApAudienceService,
 		private reactionService: ReactionService,
@@ -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 6d53ce5147..318710fa93 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 90784fdc1d..55d1054de9 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, InstancesRepository } 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 { MetaService } from '../MetaService.js';
 import { JsonLdService } from './JsonLdService.js';
@@ -338,7 +337,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;
@@ -793,6 +792,13 @@ export class ApRendererService {
 
 	@bindThis
 	public async attachLdSignature(activity: any, user: { id: MiUser['id']; host: null; }): Promise<IActivity> {
+		// Linked Data signatures are cryptographic signatures attached to each activity to provide proof of authenticity.
+		// When using authorized fetch, this is often undesired as any signed activity can be forwarded to a blocked instance by relays and other instances.
+		// This setting allows admins to disable LD signatures for increased privacy, at the expense of fewer relayed activities and additional inbound fetch (GET) requests.
+		if (!this.config.attachLdSignatureForRelays) {
+			return activity;
+		}
+
 		const keypair = await this.userKeypairService.getUserKeypair(user.id);
 
 		const jsonLd = this.jsonLdService.use();
@@ -855,7 +861,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 fd8d65445a..63871b38f9 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';
@@ -182,7 +183,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({
@@ -200,9 +202,52 @@ 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 ((contentType ?? '').split(';')[0].trimEnd().toLowerCase() === 'text/html' && _followAlternate === true) {
+			const html = await res.text();
+			const window = 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
+			}
+		}
+		//#endregion
+
+		validateContentTypeSetAsActivityPub(res);
+
 		const finalUrl = res.url; // redirects may have been involved
 		const activity = await res.json() as IObject;
 
diff --git a/packages/backend/src/core/activitypub/models/ApImageService.ts b/packages/backend/src/core/activitypub/models/ApImageService.ts
index 3d98c5b764..b281ac9728 100644
--- a/packages/backend/src/core/activitypub/models/ApImageService.ts
+++ b/packages/backend/src/core/activitypub/models/ApImageService.ts
@@ -82,7 +82,7 @@ export class ApImageService {
 			url: image.url,
 			user: actor,
 			uri: image.url,
-			sensitive: image.sensitive,
+			sensitive: !!(image.sensitive),
 			isLink: !shouldBeCached,
 			comment: truncate(image.name ?? undefined, DB_MAX_IMAGE_COMMENT_LENGTH),
 		});
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 4827baad84..7b7a7921fb 100644
--- a/packages/backend/src/core/activitypub/models/ApNoteService.ts
+++ b/packages/backend/src/core/activitypub/models/ApNoteService.ts
@@ -25,7 +25,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';
@@ -84,9 +83,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) {
@@ -258,7 +258,7 @@ export class ApNoteService {
 				}
 			};
 
-			const uris = unique([note._misskey_quote, note.quoteUrl, note.quoteUri].filter(isNotNull));
+			const uris = unique([note._misskey_quote, note.quoteUrl, note.quoteUri].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);
@@ -358,7 +358,7 @@ export class ApNoteService {
 				value,
 				object,
 			});
-			throw new Error('invalid note');
+			throw err;
 		}
 
 		const note = object as IPost;
@@ -471,19 +471,19 @@ export class ApNoteService {
 				| { status: 'ok'; res: MiNote }
 				| { status: 'permerror' | 'temperror' }
 			> => {
-				if (!/^https?:/.test(uri)) return { status: 'permerror' };
+				if (typeof uri !== 'string' || !/^https?:/.test(uri)) return { status: 'permerror' };
 				try {
 					const res = await this.resolveNote(uri, { resolver });
 					if (res == null) return { status: 'permerror' };
 					return { status: 'ok', res };
 				} catch (e) {
 					return {
-						status: (e instanceof StatusError && e.isClientError) ? 'permerror' : 'temperror',
+						status: (e instanceof StatusError && !e.isRetryable) ? 'permerror' : 'temperror',
 					};
 				}
 			};
 
-			const uris = unique([note._misskey_quote, note.quoteUrl, note.quoteUri].filter((x): x is string => typeof x === 'string'));
+			const uris = unique([note._misskey_quote, note.quoteUrl, note.quoteUri].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);
@@ -496,10 +496,10 @@ export class ApNoteService {
 
 		// vote
 		if (reply && reply.hasPoll) {
-			const replyPoll = await this.pollsRepository.findOneByOrFail({ noteId: reply.id });
+			const poll = await this.pollsRepository.findOneByOrFail({ noteId: reply.id });
 
 			const tryCreateVote = async (name: string, index: number): Promise<null> => {
-				if (replyPoll.expiresAt && Date.now() > new Date(replyPoll.expiresAt).getTime()) {
+				if (poll.expiresAt && Date.now() > new Date(poll.expiresAt).getTime()) {
 					this.logger.warn(`vote to expired poll from AP: actor=${actor.username}@${actor.host}, note=${note.id}, choice=${name}`);
 				} else if (index >= 0) {
 					this.logger.info(`vote from AP: actor=${actor.username}@${actor.host}, note=${note.id}, choice=${name}`);
@@ -512,7 +512,7 @@ export class ApNoteService {
 			};
 
 			if (note.name) {
-				return await tryCreateVote(note.name, replyPoll.choices.findIndex(x => x === note.name));
+				return await tryCreateVote(note.name, poll.choices.findIndex(x => x === note.name));
 			}
 		}
 
@@ -637,7 +637,7 @@ export class ApNoteService {
 
 			this.logger.info(`register emoji host=${host}, name=${name}`);
 
-			return await this.emojisRepository.insert({
+			return await this.emojisRepository.insertOne({
 				id: this.idService.gen(),
 				host,
 				name,
@@ -646,7 +646,7 @@ export class ApNoteService {
 				publicUrl: tag.icon.url,
 				updatedAt: new Date(),
 				aliases: [],
-			}).then(x => this.emojisRepository.findOneByOrFail(x.identifiers[0]));
+			});
 		}));
 	}
 }
diff --git a/packages/backend/src/core/activitypub/models/ApPersonService.ts b/packages/backend/src/core/activitypub/models/ApPersonService.ts
index 224b8e8c3f..10b1fc0bf4 100644
--- a/packages/backend/src/core/activitypub/models/ApPersonService.ts
+++ b/packages/backend/src/core/activitypub/models/ApPersonService.ts
@@ -34,11 +34,11 @@ 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 { RoleService } from '@/core/RoleService.js';
 import { MetaService } from '@/core/MetaService.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';
@@ -48,7 +48,7 @@ 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;
@@ -101,6 +101,8 @@ export class ApPersonService implements OnModuleInit {
 
 		@Inject(DI.followingsRepository)
 		private followingsRepository: FollowingsRepository,
+
+		private roleService: RoleService,
 	) {
 	}
 
@@ -246,6 +248,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
@@ -301,6 +308,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);
@@ -375,6 +397,8 @@ export class ApPersonService implements OnModuleInit {
 					description: _description,
 					url,
 					fields,
+					followingVisibility,
+					followersVisibility,
 					birthday: bday?.[0] ?? null,
 					location: person['vcard:Address'] ?? null,
 					userHost: host,
@@ -483,6 +507,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);
@@ -554,6 +595,8 @@ export class ApPersonService implements OnModuleInit {
 			url,
 			fields,
 			description: _description,
+			followingVisibility,
+			followersVisibility,
 			birthday: bday?.[0] ?? null,
 			location: person['vcard:Address'] ?? null,
 			listenbrainz: person.listenbrainz ?? null,
@@ -667,7 +710,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),
@@ -726,4 +769,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 8edd8a1aba..2f58825de1 100644
--- a/packages/backend/src/core/activitypub/type.ts
+++ b/packages/backend/src/core/activitypub/type.ts
@@ -60,11 +60,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 +100,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';
@@ -159,8 +166,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';
@@ -246,12 +255,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';
@@ -329,7 +342,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 28e441d72c..c2329a2f73 100644
--- a/packages/backend/src/core/chart/charts/federation.ts
+++ b/packages/backend/src/core/chart/charts/federation.ts
@@ -47,7 +47,7 @@ export default class FederationChart extends Chart<typeof schema> { // eslint-di
 
 		const suspendedInstancesQuery = this.instancesRepository.createQueryBuilder('instance')
 			.select('instance.host')
-			.where("instance.suspensionState != 'none'");
+			.where('instance.suspensionState != \'none\'');
 
 		const pubsubSubQuery = this.followingsRepository.createQueryBuilder('f')
 			.select('f.followerHost')
@@ -89,7 +89,7 @@ export default class FederationChart extends Chart<typeof schema> { // eslint-di
 				.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.suspensionState = 'none'")
+				.andWhere('instance.suspensionState = \'none\'')
 				.andWhere('instance.isNotResponding = false')
 				.getRawOne()
 				.then(x => parseInt(x.count, 10)),
@@ -97,7 +97,7 @@ export default class FederationChart extends Chart<typeof schema> { // eslint-di
 				.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.suspensionState = 'none'")
+				.andWhere('instance.suspensionState = \'none\'')
 				.andWhere('instance.isNotResponding = false')
 				.getRawOne()
 				.then(x => parseInt(x.count, 10)),
diff --git a/packages/backend/src/core/chart/core.ts b/packages/backend/src/core/chart/core.ts
index f10e30ef10..af5485a46e 100644
--- a/packages/backend/src/core/chart/core.ts
+++ b/packages/backend/src/core/chart/core.ts
@@ -14,7 +14,8 @@ import { EntitySchema, LessThan, Between } from 'typeorm';
 import { dateUTC, isTimeSame, isTimeBefore, subtractTime, addTime } from '@/misc/prelude/time.js';
 import type Logger from '@/logger.js';
 import { bindThis } from '@/decorators.js';
-import type { Repository, DataSource } from 'typeorm';
+import { MiRepository, miRepository } from '@/models/_.js';
+import type { DataSource, Repository } from 'typeorm';
 
 const COLUMN_PREFIX = '___' as const;
 const UNIQUE_TEMP_COLUMN_PREFIX = 'unique_temp___' as const;
@@ -145,10 +146,10 @@ export default abstract class Chart<T extends Schema> {
 		group: string | null;
 	}[] = [];
 	// ↓にしたいけどfindOneとかで型エラーになる
-	//private repositoryForHour: Repository<RawRecord<T>>;
-	//private repositoryForDay: Repository<RawRecord<T>>;
-	private repositoryForHour: Repository<{ id: number; group?: string | null; date: number; }>;
-	private repositoryForDay: Repository<{ id: number; group?: string | null; date: number; }>;
+	//private repositoryForHour: Repository<RawRecord<T>> & MiRepository<RawRecord<T>>;
+	//private repositoryForDay: Repository<RawRecord<T>> & MiRepository<RawRecord<T>>;
+	private repositoryForHour: Repository<{ id: number; group?: string | null; date: number; }> & MiRepository<{ id: number; group?: string | null; date: number; }>;
+	private repositoryForDay: Repository<{ id: number; group?: string | null; date: number; }> & MiRepository<{ id: number; group?: string | null; date: number; }>;
 
 	/**
 	 * 1日に一回程度実行されれば良いような計算処理を入れる(主にCASCADE削除などアプリケーション側で感知できない変動によるズレの修正用)
@@ -211,6 +212,10 @@ export default abstract class Chart<T extends Schema> {
 	} {
 		const createEntity = (span: 'hour' | 'day'): EntitySchema => new EntitySchema({
 			name:
+				span === 'hour' ? `ChartX${name}` :
+				span === 'day' ? `ChartDayX${name}` :
+				new Error('not happen') as never,
+			tableName:
 				span === 'hour' ? `__chart__${camelToSnake(name)}` :
 				span === 'day' ? `__chart_day__${camelToSnake(name)}` :
 				new Error('not happen') as never,
@@ -271,8 +276,8 @@ export default abstract class Chart<T extends Schema> {
 		this.logger = logger;
 
 		const { hour, day } = Chart.schemaToEntity(name, schema, grouped);
-		this.repositoryForHour = db.getRepository<{ id: number; group?: string | null; date: number; }>(hour);
-		this.repositoryForDay = db.getRepository<{ id: number; group?: string | null; date: number; }>(day);
+		this.repositoryForHour = db.getRepository<{ id: number; group?: string | null; date: number; }>(hour).extend(miRepository as MiRepository<{ id: number; group?: string | null; date: number; }>);
+		this.repositoryForDay = db.getRepository<{ id: number; group?: string | null; date: number; }>(day).extend(miRepository as MiRepository<{ id: number; group?: string | null; date: number; }>);
 	}
 
 	@bindThis
@@ -387,11 +392,11 @@ export default abstract class Chart<T extends Schema> {
 			}
 
 			// 新規ログ挿入
-			log = await repository.insert({
+			log = await repository.insertOne({
 				date: date,
 				...(group ? { group: group } : {}),
 				...columns,
-			}).then(x => repository.findOneByOrFail(x.identifiers[0])) as RawRecord<T>;
+			}) as RawRecord<T>;
 
 			this.logger.info(`${this.name + (group ? `:${group}` : '')}(${span}): New commit created`);
 
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..a13c244c19 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';
 
@@ -63,7 +62,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..4aa7104c1e 100644
--- a/packages/backend/src/core/entities/FlashEntityService.ts
+++ b/packages/backend/src/core/entities/FlashEntityService.ts
@@ -49,6 +49,7 @@ export class FlashEntityService {
 			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,
 		});
diff --git a/packages/backend/src/core/entities/InstanceEntityService.ts b/packages/backend/src/core/entities/InstanceEntityService.ts
index 002a93397d..7695e6dfa7 100644
--- a/packages/backend/src/core/entities/InstanceEntityService.ts
+++ b/packages/backend/src/core/entities/InstanceEntityService.ts
@@ -50,6 +50,7 @@ export class InstanceEntityService {
 			maintainerName: instance.maintainerName,
 			maintainerEmail: instance.maintainerEmail,
 			isSilenced: this.utilityService.isSilencedHost(meta.silencedHosts, instance.host),
+			isMediaSilenced: this.utilityService.isMediaSilencedHost(meta.mediaSilencedHosts, instance.host),
 			iconUrl: instance.iconUrl,
 			faviconUrl: instance.faviconUrl,
 			themeColor: instance.themeColor,
@@ -63,8 +64,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 34d46e50e5..3128b762f4 100644
--- a/packages/backend/src/core/entities/MetaEntityService.ts
+++ b/packages/backend/src/core/entities/MetaEntityService.ts
@@ -49,6 +49,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,
@@ -92,9 +108,8 @@ export class MetaEntityService {
 			backgroundImageUrl: instance.backgroundImageUrl,
 			logoImageUrl: instance.logoImageUrl,
 			maxNoteTextLength: this.config.maxNoteLength,
-			// クライアントの手間を減らすためあらかじめJSONに変換しておく
-			defaultLightTheme: instance.defaultLightTheme ? JSON.stringify(JSON5.parse(instance.defaultLightTheme)) : null,
-			defaultDarkTheme: instance.defaultDarkTheme ? JSON.stringify(JSON5.parse(instance.defaultDarkTheme)) : null,
+			defaultLightTheme,
+			defaultDarkTheme,
 			defaultLike: instance.defaultLike,
 			ads: ads.map(ad => ({
 				id: ad.id,
@@ -116,6 +131,7 @@ export class MetaEntityService {
 
 			mediaProxy: this.config.mediaProxy,
 			enableUrlPreview: instance.urlPreviewEnabled,
+			noteSearchableScope: (this.config.meilisearch == null || this.config.meilisearch.scope !== 'local') ? 'global' : 'local',
 		};
 
 		return packed;
@@ -156,4 +172,3 @@ export class MetaEntityService {
 		return packDetailed;
 	}
 }
-
diff --git a/packages/backend/src/core/entities/NoteEntityService.ts b/packages/backend/src/core/entities/NoteEntityService.ts
index ca755ea286..493723ac45 100644
--- a/packages/backend/src/core/entities/NoteEntityService.ts
+++ b/packages/backend/src/core/entities/NoteEntityService.ts
@@ -14,7 +14,6 @@ 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 { 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 type { OnModuleInit } from '@nestjs/common';
@@ -293,7 +292,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
@@ -465,12 +464,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])));
diff --git a/packages/backend/src/core/entities/NotificationEntityService.ts b/packages/backend/src/core/entities/NotificationEntityService.ts
index 18b9d148c4..e2de450756 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';
@@ -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;
@@ -181,7 +180,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,7 +222,7 @@ export class NotificationEntityService implements OnModuleInit {
 			);
 		});
 
-		return (await Promise.all(packPromises)).filter(isNotNull);
+		return (await Promise.all(packPromises)).filter(x => x != null);
 	}
 
 	@bindThis
@@ -305,7 +304,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 +312,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 9e234318b5..2b1c4d5c63 100644
--- a/packages/backend/src/core/entities/UserEntityService.ts
+++ b/packages/backend/src/core/entities/UserEntityService.ts
@@ -49,7 +49,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';
@@ -491,12 +490,12 @@ export class UserEntityService implements OnModuleInit {
 		const mastoapi = !isDetailed ? opts.userProfile ?? await this.userProfilesRepository.findOneByOrFail({ userId: user.id }) : null;
 
 		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;
 
@@ -548,11 +547,15 @@ export class UserEntityService implements OnModuleInit {
 			emojis: this.customEmojiService.populateEmojis(user.emojis, checkHost),
 			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,
@@ -560,7 +563,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,
 				updatedAt: user.updatedAt ? user.updatedAt.toISOString() : null,
 				lastFetchedAt: user.lastFetchedAt ? user.lastFetchedAt.toISOString() : null,
diff --git a/packages/backend/src/daemons/ServerStatsService.ts b/packages/backend/src/daemons/ServerStatsService.ts
index 2c70344c94..0be2149a0a 100644
--- a/packages/backend/src/daemons/ServerStatsService.ts
+++ b/packages/backend/src/daemons/ServerStatsService.ts
@@ -108,5 +108,6 @@ async function net() {
 
 // FS STAT
 async function fs() {
-	return await si.disksIO().catch(() => ({ rIO_sec: 0, wIO_sec: 0 }));
+	const io = await si.disksIO().catch(() => null);
+	return io ?? { rIO_sec: 0, wIO_sec: 0 };
 }
diff --git a/packages/backend/src/di-symbols.ts b/packages/backend/src/di-symbols.ts
index 564a8db70a..d4a21ab625 100644
--- a/packages/backend/src/di-symbols.ts
+++ b/packages/backend/src/di-symbols.ts
@@ -49,6 +49,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 +71,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/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-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..a721b8663c 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]>;
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 bd6c8ee8e3..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
  */
@@ -142,7 +104,3 @@ export function toArray<T>(x: T | T[] | undefined): T[] {
 export function toSingle<T>(x: T | T[] | undefined): T | undefined {
 	return Array.isArray(x) ? x[0] : x;
 }
-
-export function toSingleLast<T>(x: T | T[] | undefined): T | undefined {
-	return Array.isArray(x) ? x.at(-1) : x;
-}
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/DriveFile.ts b/packages/backend/src/models/DriveFile.ts
index efb639f075..dd810681c5 100644
--- a/packages/backend/src/models/DriveFile.ts
+++ b/packages/backend/src/models/DriveFile.ts
@@ -83,7 +83,7 @@ export class MiDriveFile {
 	public storedInternal: boolean;
 
 	@Column('varchar', {
-		length: 512,
+		length: 1024,
 		comment: 'The URL of the DriveFile.',
 	})
 	public url: string;
@@ -125,13 +125,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/Meta.ts b/packages/backend/src/models/Meta.ts
index fb021f0f6b..07c4e28b3a 100644
--- a/packages/backend/src/models/Meta.ts
+++ b/packages/backend/src/models/Meta.ts
@@ -86,6 +86,11 @@ export class MiMeta {
 	})
 	public silencedHosts: string[];
 
+	@Column('varchar', {
+		length: 1024, array: true, default: '{}',
+	})
+	public mediaSilencedHosts: string[];
+
 	@Column('varchar', {
 		length: 1024,
 		nullable: true,
diff --git a/packages/backend/src/models/RepositoryModule.ts b/packages/backend/src/models/RepositoryModule.ts
index 053edd6094..1eaeb86df6 100644
--- a/packages/backend/src/models/RepositoryModule.ts
+++ b/packages/backend/src/models/RepositoryModule.ts
@@ -3,399 +3,484 @@
  * 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 { 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, NoteEdit, MiBubbleGameRecord, MiReversiGame } 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,
+	NoteEdit
+} from './_.js';
 import type { DataSource } from 'typeorm';
-import type { Provider } from '@nestjs/common';
 
 const $usersRepository: Provider = {
 	provide: DI.usersRepository,
-	useFactory: (db: DataSource) => db.getRepository(MiUser),
+	useFactory: (db: DataSource) => db.getRepository(MiUser).extend(miRepository as MiRepository<MiUser>),
 	inject: [DI.db],
 };
 
 const $notesRepository: Provider = {
 	provide: DI.notesRepository,
-	useFactory: (db: DataSource) => db.getRepository(MiNote),
+	useFactory: (db: DataSource) => db.getRepository(MiNote).extend(miRepository as MiRepository<MiNote>),
 	inject: [DI.db],
 };
 
 const $announcementsRepository: Provider = {
 	provide: DI.announcementsRepository,
-	useFactory: (db: DataSource) => db.getRepository(MiAnnouncement),
+	useFactory: (db: DataSource) => db.getRepository(MiAnnouncement).extend(miRepository as MiRepository<MiAnnouncement>),
 	inject: [DI.db],
 };
 
 const $announcementReadsRepository: Provider = {
 	provide: DI.announcementReadsRepository,
-	useFactory: (db: DataSource) => db.getRepository(MiAnnouncementRead),
+	useFactory: (db: DataSource) => db.getRepository(MiAnnouncementRead).extend(miRepository as MiRepository<MiAnnouncementRead>),
 	inject: [DI.db],
 };
 
 const $appsRepository: Provider = {
 	provide: DI.appsRepository,
-	useFactory: (db: DataSource) => db.getRepository(MiApp),
+	useFactory: (db: DataSource) => db.getRepository(MiApp).extend(miRepository as MiRepository<MiApp>),
 	inject: [DI.db],
 };
 
 const $avatarDecorationsRepository: Provider = {
 	provide: DI.avatarDecorationsRepository,
-	useFactory: (db: DataSource) => db.getRepository(MiAvatarDecoration),
+	useFactory: (db: DataSource) => db.getRepository(MiAvatarDecoration).extend(miRepository as MiRepository<MiAvatarDecoration>),
 	inject: [DI.db],
 };
 
 const $noteFavoritesRepository: Provider = {
 	provide: DI.noteFavoritesRepository,
-	useFactory: (db: DataSource) => db.getRepository(MiNoteFavorite),
+	useFactory: (db: DataSource) => db.getRepository(MiNoteFavorite).extend(miRepository as MiRepository<MiNoteFavorite>),
 	inject: [DI.db],
 };
 
 const $noteThreadMutingsRepository: Provider = {
 	provide: DI.noteThreadMutingsRepository,
-	useFactory: (db: DataSource) => db.getRepository(MiNoteThreadMuting),
+	useFactory: (db: DataSource) => db.getRepository(MiNoteThreadMuting).extend(miRepository as MiRepository<MiNoteThreadMuting>),
 	inject: [DI.db],
 };
 
 const $noteReactionsRepository: Provider = {
 	provide: DI.noteReactionsRepository,
-	useFactory: (db: DataSource) => db.getRepository(MiNoteReaction),
+	useFactory: (db: DataSource) => db.getRepository(MiNoteReaction).extend(miRepository as MiRepository<MiNoteReaction>),
 	inject: [DI.db],
 };
 
 const $noteUnreadsRepository: Provider = {
 	provide: DI.noteUnreadsRepository,
-	useFactory: (db: DataSource) => db.getRepository(MiNoteUnread),
+	useFactory: (db: DataSource) => db.getRepository(MiNoteUnread).extend(miRepository as MiRepository<MiNoteUnread>),
 	inject: [DI.db],
 };
 
 const $pollsRepository: Provider = {
 	provide: DI.pollsRepository,
-	useFactory: (db: DataSource) => db.getRepository(MiPoll),
+	useFactory: (db: DataSource) => db.getRepository(MiPoll).extend(miRepository as MiRepository<MiPoll>),
 	inject: [DI.db],
 };
 
 const $pollVotesRepository: Provider = {
 	provide: DI.pollVotesRepository,
-	useFactory: (db: DataSource) => db.getRepository(MiPollVote),
+	useFactory: (db: DataSource) => db.getRepository(MiPollVote).extend(miRepository as MiRepository<MiPollVote>),
 	inject: [DI.db],
 };
 
 const $userProfilesRepository: Provider = {
 	provide: DI.userProfilesRepository,
-	useFactory: (db: DataSource) => db.getRepository(MiUserProfile),
+	useFactory: (db: DataSource) => db.getRepository(MiUserProfile).extend(miRepository as MiRepository<MiUserProfile>),
 	inject: [DI.db],
 };
 
 const $userKeypairsRepository: Provider = {
 	provide: DI.userKeypairsRepository,
-	useFactory: (db: DataSource) => db.getRepository(MiUserKeypair),
+	useFactory: (db: DataSource) => db.getRepository(MiUserKeypair).extend(miRepository as MiRepository<MiUserKeypair>),
 	inject: [DI.db],
 };
 
 const $userPendingsRepository: Provider = {
 	provide: DI.userPendingsRepository,
-	useFactory: (db: DataSource) => db.getRepository(MiUserPending),
+	useFactory: (db: DataSource) => db.getRepository(MiUserPending).extend(miRepository as MiRepository<MiUserPending>),
 	inject: [DI.db],
 };
 
 const $userSecurityKeysRepository: Provider = {
 	provide: DI.userSecurityKeysRepository,
-	useFactory: (db: DataSource) => db.getRepository(MiUserSecurityKey),
+	useFactory: (db: DataSource) => db.getRepository(MiUserSecurityKey).extend(miRepository as MiRepository<MiUserSecurityKey>),
 	inject: [DI.db],
 };
 
 const $userPublickeysRepository: Provider = {
 	provide: DI.userPublickeysRepository,
-	useFactory: (db: DataSource) => db.getRepository(MiUserPublickey),
+	useFactory: (db: DataSource) => db.getRepository(MiUserPublickey).extend(miRepository as MiRepository<MiUserPublickey>),
 	inject: [DI.db],
 };
 
 const $userListsRepository: Provider = {
 	provide: DI.userListsRepository,
-	useFactory: (db: DataSource) => db.getRepository(MiUserList),
+	useFactory: (db: DataSource) => db.getRepository(MiUserList).extend(miRepository as MiRepository<MiUserList>),
 	inject: [DI.db],
 };
 
 const $userListFavoritesRepository: Provider = {
 	provide: DI.userListFavoritesRepository,
-	useFactory: (db: DataSource) => db.getRepository(MiUserListFavorite),
+	useFactory: (db: DataSource) => db.getRepository(MiUserListFavorite).extend(miRepository as MiRepository<MiUserListFavorite>),
 	inject: [DI.db],
 };
 
 const $userListMembershipsRepository: Provider = {
 	provide: DI.userListMembershipsRepository,
-	useFactory: (db: DataSource) => db.getRepository(MiUserListMembership),
+	useFactory: (db: DataSource) => db.getRepository(MiUserListMembership).extend(miRepository as MiRepository<MiUserListMembership>),
 	inject: [DI.db],
 };
 
 const $userNotePiningsRepository: Provider = {
 	provide: DI.userNotePiningsRepository,
-	useFactory: (db: DataSource) => db.getRepository(MiUserNotePining),
+	useFactory: (db: DataSource) => db.getRepository(MiUserNotePining).extend(miRepository as MiRepository<MiUserNotePining>),
 	inject: [DI.db],
 };
 
 const $userIpsRepository: Provider = {
 	provide: DI.userIpsRepository,
-	useFactory: (db: DataSource) => db.getRepository(MiUserIp),
+	useFactory: (db: DataSource) => db.getRepository(MiUserIp).extend(miRepository as MiRepository<MiUserIp>),
 	inject: [DI.db],
 };
 
 const $usedUsernamesRepository: Provider = {
 	provide: DI.usedUsernamesRepository,
-	useFactory: (db: DataSource) => db.getRepository(MiUsedUsername),
+	useFactory: (db: DataSource) => db.getRepository(MiUsedUsername).extend(miRepository as MiRepository<MiUsedUsername>),
 	inject: [DI.db],
 };
 
 const $followingsRepository: Provider = {
 	provide: DI.followingsRepository,
-	useFactory: (db: DataSource) => db.getRepository(MiFollowing),
+	useFactory: (db: DataSource) => db.getRepository(MiFollowing).extend(miRepository as MiRepository<MiFollowing>),
 	inject: [DI.db],
 };
 
 const $followRequestsRepository: Provider = {
 	provide: DI.followRequestsRepository,
-	useFactory: (db: DataSource) => db.getRepository(MiFollowRequest),
+	useFactory: (db: DataSource) => db.getRepository(MiFollowRequest).extend(miRepository as MiRepository<MiFollowRequest>),
 	inject: [DI.db],
 };
 
 const $instancesRepository: Provider = {
 	provide: DI.instancesRepository,
-	useFactory: (db: DataSource) => db.getRepository(MiInstance),
+	useFactory: (db: DataSource) => db.getRepository(MiInstance).extend(miRepository as MiRepository<MiInstance>),
 	inject: [DI.db],
 };
 
 const $emojisRepository: Provider = {
 	provide: DI.emojisRepository,
-	useFactory: (db: DataSource) => db.getRepository(MiEmoji),
+	useFactory: (db: DataSource) => db.getRepository(MiEmoji).extend(miRepository as MiRepository<MiEmoji>),
 	inject: [DI.db],
 };
 
 const $driveFilesRepository: Provider = {
 	provide: DI.driveFilesRepository,
-	useFactory: (db: DataSource) => db.getRepository(MiDriveFile),
+	useFactory: (db: DataSource) => db.getRepository(MiDriveFile).extend(miRepository as MiRepository<MiDriveFile>),
 	inject: [DI.db],
 };
 
 const $driveFoldersRepository: Provider = {
 	provide: DI.driveFoldersRepository,
-	useFactory: (db: DataSource) => db.getRepository(MiDriveFolder),
+	useFactory: (db: DataSource) => db.getRepository(MiDriveFolder).extend(miRepository as MiRepository<MiDriveFolder>),
 	inject: [DI.db],
 };
 
 const $metasRepository: Provider = {
 	provide: DI.metasRepository,
-	useFactory: (db: DataSource) => db.getRepository(MiMeta),
+	useFactory: (db: DataSource) => db.getRepository(MiMeta).extend(miRepository as MiRepository<MiMeta>),
 	inject: [DI.db],
 };
 
 const $mutingsRepository: Provider = {
 	provide: DI.mutingsRepository,
-	useFactory: (db: DataSource) => db.getRepository(MiMuting),
+	useFactory: (db: DataSource) => db.getRepository(MiMuting).extend(miRepository as MiRepository<MiMuting>),
 	inject: [DI.db],
 };
 
 const $renoteMutingsRepository: Provider = {
 	provide: DI.renoteMutingsRepository,
-	useFactory: (db: DataSource) => db.getRepository(MiRenoteMuting),
+	useFactory: (db: DataSource) => db.getRepository(MiRenoteMuting).extend(miRepository as MiRepository<MiRenoteMuting>),
 	inject: [DI.db],
 };
 
 const $blockingsRepository: Provider = {
 	provide: DI.blockingsRepository,
-	useFactory: (db: DataSource) => db.getRepository(MiBlocking),
+	useFactory: (db: DataSource) => db.getRepository(MiBlocking).extend(miRepository as MiRepository<MiBlocking>),
 	inject: [DI.db],
 };
 
 const $swSubscriptionsRepository: Provider = {
 	provide: DI.swSubscriptionsRepository,
-	useFactory: (db: DataSource) => db.getRepository(MiSwSubscription),
+	useFactory: (db: DataSource) => db.getRepository(MiSwSubscription).extend(miRepository as MiRepository<MiSwSubscription>),
 	inject: [DI.db],
 };
 
 const $hashtagsRepository: Provider = {
 	provide: DI.hashtagsRepository,
-	useFactory: (db: DataSource) => db.getRepository(MiHashtag),
+	useFactory: (db: DataSource) => db.getRepository(MiHashtag).extend(miRepository as MiRepository<MiHashtag>),
 	inject: [DI.db],
 };
 
 const $abuseUserReportsRepository: Provider = {
 	provide: DI.abuseUserReportsRepository,
-	useFactory: (db: DataSource) => db.getRepository(MiAbuseUserReport),
+	useFactory: (db: DataSource) => db.getRepository(MiAbuseUserReport).extend(miRepository as MiRepository<MiAbuseUserReport>),
+	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),
+	useFactory: (db: DataSource) => db.getRepository(MiRegistrationTicket).extend(miRepository as MiRepository<MiRegistrationTicket>),
 	inject: [DI.db],
 };
 
 const $authSessionsRepository: Provider = {
 	provide: DI.authSessionsRepository,
-	useFactory: (db: DataSource) => db.getRepository(MiAuthSession),
+	useFactory: (db: DataSource) => db.getRepository(MiAuthSession).extend(miRepository as MiRepository<MiAuthSession>),
 	inject: [DI.db],
 };
 
 const $accessTokensRepository: Provider = {
 	provide: DI.accessTokensRepository,
-	useFactory: (db: DataSource) => db.getRepository(MiAccessToken),
+	useFactory: (db: DataSource) => db.getRepository(MiAccessToken).extend(miRepository as MiRepository<MiAccessToken>),
 	inject: [DI.db],
 };
 
 const $signinsRepository: Provider = {
 	provide: DI.signinsRepository,
-	useFactory: (db: DataSource) => db.getRepository(MiSignin),
+	useFactory: (db: DataSource) => db.getRepository(MiSignin).extend(miRepository as MiRepository<MiSignin>),
 	inject: [DI.db],
 };
 
 const $pagesRepository: Provider = {
 	provide: DI.pagesRepository,
-	useFactory: (db: DataSource) => db.getRepository(MiPage),
+	useFactory: (db: DataSource) => db.getRepository(MiPage).extend(miRepository as MiRepository<MiPage>),
 	inject: [DI.db],
 };
 
 const $pageLikesRepository: Provider = {
 	provide: DI.pageLikesRepository,
-	useFactory: (db: DataSource) => db.getRepository(MiPageLike),
+	useFactory: (db: DataSource) => db.getRepository(MiPageLike).extend(miRepository as MiRepository<MiPageLike>),
 	inject: [DI.db],
 };
 
 const $galleryPostsRepository: Provider = {
 	provide: DI.galleryPostsRepository,
-	useFactory: (db: DataSource) => db.getRepository(MiGalleryPost),
+	useFactory: (db: DataSource) => db.getRepository(MiGalleryPost).extend(miRepository as MiRepository<MiGalleryPost>),
 	inject: [DI.db],
 };
 
 const $galleryLikesRepository: Provider = {
 	provide: DI.galleryLikesRepository,
-	useFactory: (db: DataSource) => db.getRepository(MiGalleryLike),
+	useFactory: (db: DataSource) => db.getRepository(MiGalleryLike).extend(miRepository as MiRepository<MiGalleryLike>),
 	inject: [DI.db],
 };
 
 const $moderationLogsRepository: Provider = {
 	provide: DI.moderationLogsRepository,
-	useFactory: (db: DataSource) => db.getRepository(MiModerationLog),
+	useFactory: (db: DataSource) => db.getRepository(MiModerationLog).extend(miRepository as MiRepository<MiModerationLog>),
 	inject: [DI.db],
 };
 
 const $clipsRepository: Provider = {
 	provide: DI.clipsRepository,
-	useFactory: (db: DataSource) => db.getRepository(MiClip),
+	useFactory: (db: DataSource) => db.getRepository(MiClip).extend(miRepository as MiRepository<MiClip>),
 	inject: [DI.db],
 };
 
 const $clipNotesRepository: Provider = {
 	provide: DI.clipNotesRepository,
-	useFactory: (db: DataSource) => db.getRepository(MiClipNote),
+	useFactory: (db: DataSource) => db.getRepository(MiClipNote).extend(miRepository as MiRepository<MiClipNote>),
 	inject: [DI.db],
 };
 
 const $clipFavoritesRepository: Provider = {
 	provide: DI.clipFavoritesRepository,
-	useFactory: (db: DataSource) => db.getRepository(MiClipFavorite),
+	useFactory: (db: DataSource) => db.getRepository(MiClipFavorite).extend(miRepository as MiRepository<MiClipFavorite>),
 	inject: [DI.db],
 };
 
 const $antennasRepository: Provider = {
 	provide: DI.antennasRepository,
-	useFactory: (db: DataSource) => db.getRepository(MiAntenna),
+	useFactory: (db: DataSource) => db.getRepository(MiAntenna).extend(miRepository as MiRepository<MiAntenna>),
 	inject: [DI.db],
 };
 
 const $promoNotesRepository: Provider = {
 	provide: DI.promoNotesRepository,
-	useFactory: (db: DataSource) => db.getRepository(MiPromoNote),
+	useFactory: (db: DataSource) => db.getRepository(MiPromoNote).extend(miRepository as MiRepository<MiPromoNote>),
 	inject: [DI.db],
 };
 
 const $promoReadsRepository: Provider = {
 	provide: DI.promoReadsRepository,
-	useFactory: (db: DataSource) => db.getRepository(MiPromoRead),
+	useFactory: (db: DataSource) => db.getRepository(MiPromoRead).extend(miRepository as MiRepository<MiPromoRead>),
 	inject: [DI.db],
 };
 
 const $relaysRepository: Provider = {
 	provide: DI.relaysRepository,
-	useFactory: (db: DataSource) => db.getRepository(MiRelay),
+	useFactory: (db: DataSource) => db.getRepository(MiRelay).extend(miRepository as MiRepository<MiRelay>),
 	inject: [DI.db],
 };
 
 const $channelsRepository: Provider = {
 	provide: DI.channelsRepository,
-	useFactory: (db: DataSource) => db.getRepository(MiChannel),
+	useFactory: (db: DataSource) => db.getRepository(MiChannel).extend(miRepository as MiRepository<MiChannel>),
 	inject: [DI.db],
 };
 
 const $channelFollowingsRepository: Provider = {
 	provide: DI.channelFollowingsRepository,
-	useFactory: (db: DataSource) => db.getRepository(MiChannelFollowing),
+	useFactory: (db: DataSource) => db.getRepository(MiChannelFollowing).extend(miRepository as MiRepository<MiChannelFollowing>),
 	inject: [DI.db],
 };
 
 const $channelFavoritesRepository: Provider = {
 	provide: DI.channelFavoritesRepository,
-	useFactory: (db: DataSource) => db.getRepository(MiChannelFavorite),
+	useFactory: (db: DataSource) => db.getRepository(MiChannelFavorite).extend(miRepository as MiRepository<MiChannelFavorite>),
 	inject: [DI.db],
 };
 
 const $registryItemsRepository: Provider = {
 	provide: DI.registryItemsRepository,
-	useFactory: (db: DataSource) => db.getRepository(MiRegistryItem),
+	useFactory: (db: DataSource) => db.getRepository(MiRegistryItem).extend(miRepository as MiRepository<MiRegistryItem>),
 	inject: [DI.db],
 };
 
 const $webhooksRepository: Provider = {
 	provide: DI.webhooksRepository,
-	useFactory: (db: DataSource) => db.getRepository(MiWebhook),
+	useFactory: (db: DataSource) => db.getRepository(MiWebhook).extend(miRepository as MiRepository<MiWebhook>),
+	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),
+	useFactory: (db: DataSource) => db.getRepository(MiAd).extend(miRepository as MiRepository<MiAd>),
 	inject: [DI.db],
 };
 
 const $passwordResetRequestsRepository: Provider = {
 	provide: DI.passwordResetRequestsRepository,
-	useFactory: (db: DataSource) => db.getRepository(MiPasswordResetRequest),
+	useFactory: (db: DataSource) => db.getRepository(MiPasswordResetRequest).extend(miRepository as MiRepository<MiPasswordResetRequest>),
 	inject: [DI.db],
 };
 
 const $retentionAggregationsRepository: Provider = {
 	provide: DI.retentionAggregationsRepository,
-	useFactory: (db: DataSource) => db.getRepository(MiRetentionAggregation),
+	useFactory: (db: DataSource) => db.getRepository(MiRetentionAggregation).extend(miRepository as MiRepository<MiRetentionAggregation>),
 	inject: [DI.db],
 };
 
 const $flashsRepository: Provider = {
 	provide: DI.flashsRepository,
-	useFactory: (db: DataSource) => db.getRepository(MiFlash),
+	useFactory: (db: DataSource) => db.getRepository(MiFlash).extend(miRepository as MiRepository<MiFlash>),
 	inject: [DI.db],
 };
 
 const $flashLikesRepository: Provider = {
 	provide: DI.flashLikesRepository,
-	useFactory: (db: DataSource) => db.getRepository(MiFlashLike),
+	useFactory: (db: DataSource) => db.getRepository(MiFlashLike).extend(miRepository as MiRepository<MiFlashLike>),
 	inject: [DI.db],
 };
 
 const $rolesRepository: Provider = {
 	provide: DI.rolesRepository,
-	useFactory: (db: DataSource) => db.getRepository(MiRole),
+	useFactory: (db: DataSource) => db.getRepository(MiRole).extend(miRepository as MiRepository<MiRole>),
 	inject: [DI.db],
 };
 
 const $roleAssignmentsRepository: Provider = {
 	provide: DI.roleAssignmentsRepository,
-	useFactory: (db: DataSource) => db.getRepository(MiRoleAssignment),
+	useFactory: (db: DataSource) => db.getRepository(MiRoleAssignment).extend(miRepository as MiRepository<MiRoleAssignment>),
 	inject: [DI.db],
 };
 
 const $userMemosRepository: Provider = {
 	provide: DI.userMemosRepository,
-	useFactory: (db: DataSource) => db.getRepository(MiUserMemo),
+	useFactory: (db: DataSource) => db.getRepository(MiUserMemo).extend(miRepository as MiRepository<MiUserMemo>),
 	inject: [DI.db],
 };
 
@@ -407,19 +492,18 @@ const $noteEditRepository: Provider = {
 
 const $bubbleGameRecordsRepository: Provider = {
 	provide: DI.bubbleGameRecordsRepository,
-	useFactory: (db: DataSource) => db.getRepository(MiBubbleGameRecord),
+	useFactory: (db: DataSource) => db.getRepository(MiBubbleGameRecord).extend(miRepository as MiRepository<MiBubbleGameRecord>),
 	inject: [DI.db],
 };
 
 const $reversiGamesRepository: Provider = {
 	provide: DI.reversiGamesRepository,
-	useFactory: (db: DataSource) => db.getRepository(MiReversiGame),
+	useFactory: (db: DataSource) => db.getRepository(MiReversiGame).extend(miRepository as MiRepository<MiReversiGame>),
 	inject: [DI.db],
 };
 
 @Module({
-	imports: [
-	],
+	imports: [],
 	providers: [
 		$usersRepository,
 		$notesRepository,
@@ -457,6 +541,7 @@ const $reversiGamesRepository: Provider = {
 		$swSubscriptionsRepository,
 		$hashtagsRepository,
 		$abuseUserReportsRepository,
+		$abuseReportNotificationRecipientRepository,
 		$registrationTicketsRepository,
 		$authSessionsRepository,
 		$accessTokensRepository,
@@ -478,6 +563,7 @@ const $reversiGamesRepository: Provider = {
 		$channelFavoritesRepository,
 		$registryItemsRepository,
 		$webhooksRepository,
+		$systemWebhooksRepository,
 		$adsRepository,
 		$passwordResetRequestsRepository,
 		$retentionAggregationsRepository,
@@ -527,6 +613,7 @@ const $reversiGamesRepository: Provider = {
 		$swSubscriptionsRepository,
 		$hashtagsRepository,
 		$abuseUserReportsRepository,
+		$abuseReportNotificationRecipientRepository,
 		$registrationTicketsRepository,
 		$authSessionsRepository,
 		$accessTokensRepository,
@@ -548,6 +635,7 @@ const $reversiGamesRepository: Provider = {
 		$channelFavoritesRepository,
 		$registryItemsRepository,
 		$webhooksRepository,
+		$systemWebhooksRepository,
 		$adsRepository,
 		$passwordResetRequestsRepository,
 		$retentionAggregationsRepository,
@@ -561,4 +649,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..d6c27eae51
--- /dev/null
+++ b/packages/backend/src/models/SystemWebhook.ts
@@ -0,0 +1,100 @@
+/*
+ * 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',
+] 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/_.ts b/packages/backend/src/models/_.ts
index 744a1dd4e7..f7646dce2a 100644
--- a/packages/backend/src/models/_.ts
+++ b/packages/backend/src/models/_.ts
@@ -3,7 +3,15 @@
  * SPDX-License-Identifier: AGPL-3.0-only
  */
 
+import { FindOneOptions, InsertQueryBuilder, ObjectLiteral, Repository, SelectQueryBuilder, TypeORMError } from 'typeorm';
+import { DriverUtils } from 'typeorm/driver/DriverUtils.js';
+import { RelationCountLoader } from 'typeorm/query-builder/relation-count/RelationCountLoader.js';
+import { RelationIdLoader } from 'typeorm/query-builder/relation-id/RelationIdLoader.js';
+import { RawSqlResultsToEntityTransformer } from 'typeorm/query-builder/transformer/RawSqlResultsToEntityTransformer.js';
+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';
@@ -61,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';
@@ -71,11 +80,54 @@ import { MiUserListFavorite } from '@/models/UserListFavorite.js';
 import { NoteEdit } from '@/models/NoteEdit.js';
 import { MiBubbleGameRecord } from '@/models/BubbleGameRecord.js';
 import { MiReversiGame } from '@/models/ReversiGame.js';
+import type { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity.js';
 
-import type { Repository } from 'typeorm';
+export interface MiRepository<T extends ObjectLiteral> {
+	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() {
+		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);
+		// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+		const mainAlias = queryBuilder.expressionMap.mainAlias!;
+		const name = mainAlias.name;
+		mainAlias.name = 't';
+		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
+		builder.expressionMap.mainAlias!.tablePath = 'cte';
+		this.selectAliasColumnNames(queryBuilder, builder);
+		if (findOptions) {
+			builder.setFindOptions(findOptions);
+		}
+		const raw = await builder.execute();
+		mainAlias.name = name;
+		const relationId = await new RelationIdLoader(builder.connection, this.queryRunner, builder.expressionMap.relationIdAttributes).load(raw);
+		const relationCount = await new RelationCountLoader(builder.connection, this.queryRunner, builder.expressionMap.relationCountAttributes).load(raw);
+		const result = new RawSqlResultsToEntityTransformer(builder.expressionMap, builder.connection.driver, relationId, relationCount, this.queryRunner).transform(raw, mainAlias);
+		return result[0];
+	},
+	selectAliasColumnNames(queryBuilder, builder) {
+		let selectOrAddSelect = (selection: string, selectionAliasName?: string) => {
+			selectOrAddSelect = (selection, selectionAliasName) => builder.addSelect(selection, selectionAliasName);
+			return builder.select(selection, selectionAliasName);
+		};
+		for (const columnName of this.createTableColumnNames()) {
+			selectOrAddSelect(`${builder.alias}.${columnName}`, `${builder.alias}_${columnName}`);
+		}
+	},
+} satisfies MiRepository<ObjectLiteral>;
 
 export {
 	MiAbuseUserReport,
+	MiAbuseReportNotificationRecipient,
 	MiAccessToken,
 	MiAd,
 	MiAnnouncement,
@@ -133,6 +185,7 @@ export {
 	MiUserPublickey,
 	MiUserSecurityKey,
 	MiWebhook,
+	MiSystemWebhook,
 	MiChannel,
 	MiRetentionAggregation,
 	MiRole,
@@ -145,71 +198,73 @@ export {
 	MiReversiGame,
 };
 
-export type AbuseUserReportsRepository = Repository<MiAbuseUserReport>;
-export type AccessTokensRepository = Repository<MiAccessToken>;
-export type AdsRepository = Repository<MiAd>;
-export type AnnouncementsRepository = Repository<MiAnnouncement>;
-export type AnnouncementReadsRepository = Repository<MiAnnouncementRead>;
-export type AntennasRepository = Repository<MiAntenna>;
-export type AppsRepository = Repository<MiApp>;
-export type AvatarDecorationsRepository = Repository<MiAvatarDecoration>;
-export type AuthSessionsRepository = Repository<MiAuthSession>;
-export type BlockingsRepository = Repository<MiBlocking>;
-export type ChannelFollowingsRepository = Repository<MiChannelFollowing>;
-export type ChannelFavoritesRepository = Repository<MiChannelFavorite>;
-export type ClipsRepository = Repository<MiClip>;
-export type ClipNotesRepository = Repository<MiClipNote>;
-export type ClipFavoritesRepository = Repository<MiClipFavorite>;
-export type DriveFilesRepository = Repository<MiDriveFile>;
-export type DriveFoldersRepository = Repository<MiDriveFolder>;
-export type EmojisRepository = Repository<MiEmoji>;
-export type FollowingsRepository = Repository<MiFollowing>;
-export type FollowRequestsRepository = Repository<MiFollowRequest>;
-export type GalleryLikesRepository = Repository<MiGalleryLike>;
-export type GalleryPostsRepository = Repository<MiGalleryPost>;
-export type HashtagsRepository = Repository<MiHashtag>;
-export type InstancesRepository = Repository<MiInstance>;
-export type MetasRepository = Repository<MiMeta>;
-export type ModerationLogsRepository = Repository<MiModerationLog>;
-export type MutingsRepository = Repository<MiMuting>;
-export type RenoteMutingsRepository = Repository<MiRenoteMuting>;
-export type NotesRepository = Repository<MiNote>;
-export type NoteFavoritesRepository = Repository<MiNoteFavorite>;
-export type NoteReactionsRepository = Repository<MiNoteReaction>;
-export type NoteThreadMutingsRepository = Repository<MiNoteThreadMuting>;
-export type NoteUnreadsRepository = Repository<MiNoteUnread>;
-export type PagesRepository = Repository<MiPage>;
-export type PageLikesRepository = Repository<MiPageLike>;
-export type PasswordResetRequestsRepository = Repository<MiPasswordResetRequest>;
-export type PollsRepository = Repository<MiPoll>;
-export type PollVotesRepository = Repository<MiPollVote>;
-export type PromoNotesRepository = Repository<MiPromoNote>;
-export type PromoReadsRepository = Repository<MiPromoRead>;
-export type RegistrationTicketsRepository = Repository<MiRegistrationTicket>;
-export type RegistryItemsRepository = Repository<MiRegistryItem>;
-export type RelaysRepository = Repository<MiRelay>;
-export type SigninsRepository = Repository<MiSignin>;
-export type SwSubscriptionsRepository = Repository<MiSwSubscription>;
-export type UsedUsernamesRepository = Repository<MiUsedUsername>;
-export type UsersRepository = Repository<MiUser>;
-export type UserIpsRepository = Repository<MiUserIp>;
-export type UserKeypairsRepository = Repository<MiUserKeypair>;
-export type UserListsRepository = Repository<MiUserList>;
-export type UserListFavoritesRepository = Repository<MiUserListFavorite>;
-export type UserListMembershipsRepository = Repository<MiUserListMembership>;
-export type UserNotePiningsRepository = Repository<MiUserNotePining>;
-export type UserPendingsRepository = Repository<MiUserPending>;
-export type UserProfilesRepository = Repository<MiUserProfile>;
-export type UserPublickeysRepository = Repository<MiUserPublickey>;
-export type UserSecurityKeysRepository = Repository<MiUserSecurityKey>;
-export type WebhooksRepository = Repository<MiWebhook>;
-export type ChannelsRepository = Repository<MiChannel>;
-export type RetentionAggregationsRepository = Repository<MiRetentionAggregation>;
-export type RolesRepository = Repository<MiRole>;
-export type RoleAssignmentsRepository = Repository<MiRoleAssignment>;
-export type FlashsRepository = Repository<MiFlash>;
-export type FlashLikesRepository = Repository<MiFlashLike>;
-export type UserMemoRepository = Repository<MiUserMemo>;
-export type NoteEditRepository = Repository<NoteEdit>;
-export type BubbleGameRecordsRepository = Repository<MiBubbleGameRecord>;
-export type ReversiGamesRepository = Repository<MiReversiGame>;
+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>;
+export type AnnouncementReadsRepository = Repository<MiAnnouncementRead> & MiRepository<MiAnnouncementRead>;
+export type AntennasRepository = Repository<MiAntenna> & MiRepository<MiAntenna>;
+export type AppsRepository = Repository<MiApp> & MiRepository<MiApp>;
+export type AvatarDecorationsRepository = Repository<MiAvatarDecoration> & MiRepository<MiAvatarDecoration>;
+export type AuthSessionsRepository = Repository<MiAuthSession> & MiRepository<MiAuthSession>;
+export type BlockingsRepository = Repository<MiBlocking> & MiRepository<MiBlocking>;
+export type ChannelFollowingsRepository = Repository<MiChannelFollowing> & MiRepository<MiChannelFollowing>;
+export type ChannelFavoritesRepository = Repository<MiChannelFavorite> & MiRepository<MiChannelFavorite>;
+export type ClipsRepository = Repository<MiClip> & MiRepository<MiClip>;
+export type ClipNotesRepository = Repository<MiClipNote> & MiRepository<MiClipNote>;
+export type ClipFavoritesRepository = Repository<MiClipFavorite> & MiRepository<MiClipFavorite>;
+export type DriveFilesRepository = Repository<MiDriveFile> & MiRepository<MiDriveFile>;
+export type DriveFoldersRepository = Repository<MiDriveFolder> & MiRepository<MiDriveFolder>;
+export type EmojisRepository = Repository<MiEmoji> & MiRepository<MiEmoji>;
+export type FollowingsRepository = Repository<MiFollowing> & MiRepository<MiFollowing>;
+export type FollowRequestsRepository = Repository<MiFollowRequest> & MiRepository<MiFollowRequest>;
+export type GalleryLikesRepository = Repository<MiGalleryLike> & MiRepository<MiGalleryLike>;
+export type GalleryPostsRepository = Repository<MiGalleryPost> & MiRepository<MiGalleryPost>;
+export type HashtagsRepository = Repository<MiHashtag> & MiRepository<MiHashtag>;
+export type InstancesRepository = Repository<MiInstance> & MiRepository<MiInstance>;
+export type MetasRepository = Repository<MiMeta> & MiRepository<MiMeta>;
+export type ModerationLogsRepository = Repository<MiModerationLog> & MiRepository<MiModerationLog>;
+export type MutingsRepository = Repository<MiMuting> & MiRepository<MiMuting>;
+export type RenoteMutingsRepository = Repository<MiRenoteMuting> & MiRepository<MiRenoteMuting>;
+export type NotesRepository = Repository<MiNote> & MiRepository<MiNote>;
+export type NoteFavoritesRepository = Repository<MiNoteFavorite> & MiRepository<MiNoteFavorite>;
+export type NoteReactionsRepository = Repository<MiNoteReaction> & MiRepository<MiNoteReaction>;
+export type NoteThreadMutingsRepository = Repository<MiNoteThreadMuting> & MiRepository<MiNoteThreadMuting>;
+export type NoteUnreadsRepository = Repository<MiNoteUnread> & MiRepository<MiNoteUnread>;
+export type PagesRepository = Repository<MiPage> & MiRepository<MiPage>;
+export type PageLikesRepository = Repository<MiPageLike> & MiRepository<MiPageLike>;
+export type PasswordResetRequestsRepository = Repository<MiPasswordResetRequest> & MiRepository<MiPasswordResetRequest>;
+export type PollsRepository = Repository<MiPoll> & MiRepository<MiPoll>;
+export type PollVotesRepository = Repository<MiPollVote> & MiRepository<MiPollVote>;
+export type PromoNotesRepository = Repository<MiPromoNote> & MiRepository<MiPromoNote>;
+export type PromoReadsRepository = Repository<MiPromoRead> & MiRepository<MiPromoRead>;
+export type RegistrationTicketsRepository = Repository<MiRegistrationTicket> & MiRepository<MiRegistrationTicket>;
+export type RegistryItemsRepository = Repository<MiRegistryItem> & MiRepository<MiRegistryItem>;
+export type RelaysRepository = Repository<MiRelay> & MiRepository<MiRelay>;
+export type SigninsRepository = Repository<MiSignin> & MiRepository<MiSignin>;
+export type SwSubscriptionsRepository = Repository<MiSwSubscription> & MiRepository<MiSwSubscription>;
+export type UsedUsernamesRepository = Repository<MiUsedUsername> & MiRepository<MiUsedUsername>;
+export type UsersRepository = Repository<MiUser> & MiRepository<MiUser>;
+export type UserIpsRepository = Repository<MiUserIp> & MiRepository<MiUserIp>;
+export type UserKeypairsRepository = Repository<MiUserKeypair> & MiRepository<MiUserKeypair>;
+export type UserListsRepository = Repository<MiUserList> & MiRepository<MiUserList>;
+export type UserListFavoritesRepository = Repository<MiUserListFavorite> & MiRepository<MiUserListFavorite>;
+export type UserListMembershipsRepository = Repository<MiUserListMembership> & MiRepository<MiUserListMembership>;
+export type UserNotePiningsRepository = Repository<MiUserNotePining> & MiRepository<MiUserNotePining>;
+export type UserPendingsRepository = Repository<MiUserPending> & MiRepository<MiUserPending>;
+export type UserProfilesRepository = Repository<MiUserProfile> & MiRepository<MiUserProfile>;
+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>;
+export type RoleAssignmentsRepository = Repository<MiRoleAssignment> & MiRepository<MiRoleAssignment>;
+export type FlashsRepository = Repository<MiFlash> & MiRepository<MiFlash>;
+export type FlashLikesRepository = Repository<MiFlashLike> & MiRepository<MiFlashLike>;
+export type UserMemoRepository = Repository<MiUserMemo> & MiRepository<MiUserMemo>;
+export type BubbleGameRecordsRepository = Repository<MiBubbleGameRecord> & MiRepository<MiBubbleGameRecord>;
+export type ReversiGamesRepository = Repository<MiReversiGame> & MiRepository<MiReversiGame>;
+export type NoteEditRepository = Repository<NoteEdit> & MiRepository<NoteEdit>;
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 a602126dc8..062dba9bad 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 7edd877f80..1d620f16fd 100644
--- a/packages/backend/src/models/json-schema/meta.ts
+++ b/packages/backend/src/models/json-schema/meta.ts
@@ -263,6 +263,12 @@ export const packedMetaLiteSchema = {
 			optional: false, nullable: false,
 			ref: 'RolePolicies',
 		},
+		noteSearchableScope: {
+			type: 'string',
+			enum: ['local', 'global'],
+			optional: false, nullable: false,
+			default: 'local',
+		},
 	},
 } 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/role.ts b/packages/backend/src/models/json-schema/role.ts
index 08580d22de..504b9b122f 100644
--- a/packages/backend/src/models/json-schema/role.ts
+++ b/packages/backend/src/models/json-schema/role.ts
@@ -232,6 +232,10 @@ export const packedRolePoliciesSchema = {
 			type: 'boolean',
 			optional: false, nullable: false,
 		},
+		canUpdateBioMedia: {
+			type: 'boolean',
+			optional: false, nullable: false,
+		},
 		pinLimit: {
 			type: 'integer',
 			optional: false, nullable: false,
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/postgres.ts b/packages/backend/src/postgres.ts
index 4a1b42383f..047b7f8ae9 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';
@@ -84,9 +84,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
@@ -168,6 +170,7 @@ export const entities = [
 	MiHashtag,
 	MiSwSubscription,
 	MiAbuseUserReport,
+	MiAbuseReportNotificationRecipient,
 	MiRegistrationTicket,
 	MiSignin,
 	MiModerationLog,
@@ -186,6 +189,7 @@ export const entities = [
 	MiPasswordResetRequest,
 	MiUserPending,
 	MiWebhook,
+	MiSystemWebhook,
 	MiUserIp,
 	MiRetentionAggregation,
 	MiRole,
@@ -236,12 +240,8 @@ export function createPostgresDataSource(config: Config) {
 		cache: !config.db.disableCache && process.env.NODE_ENV !== 'test' ? { // dbをcloseしても何故かredisのコネクションが内部的に残り続けるようで、テストの際に支障が出るため無効にする(キャッシュも含めてテストしたいため本当は有効にしたいが...)
 			type: 'ioredis',
 			options: {
-				host: config.redis.host,
-				port: config.redis.port,
-				family: config.redis.family ?? 0,
-				password: config.redis.pass,
+				...config.redis,
 				keyPrefix: `${config.redis.prefix}:query:`,
-				db: config.redis.db ?? 0,
 			},
 		} : false,
 		logging: log,
diff --git a/packages/backend/src/queue/QueueProcessorModule.ts b/packages/backend/src/queue/QueueProcessorModule.ts
index d7316e19e3..7daca687a1 100644
--- a/packages/backend/src/queue/QueueProcessorModule.ts
+++ b/packages/backend/src/queue/QueueProcessorModule.ts
@@ -11,7 +11,8 @@ 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 { CleanChartsProcessorService } from './processors/CleanChartsProcessorService.js';
 import { CleanProcessorService } from './processors/CleanProcessorService.js';
@@ -75,7 +76,8 @@ import { RelationshipProcessorService } from './processors/RelationshipProcessor
 		DeleteFileProcessorService,
 		CleanRemoteFilesProcessorService,
 		RelationshipProcessorService,
-		WebhookDeliverProcessorService,
+		UserWebhookDeliverProcessorService,
+		SystemWebhookDeliverProcessorService,
 		EndedPollNotificationProcessorService,
 		DeliverProcessorService,
 		InboxProcessorService,
diff --git a/packages/backend/src/queue/QueueProcessorService.ts b/packages/backend/src/queue/QueueProcessorService.ts
index 76b6d7fb05..7a6169bf9c 100644
--- a/packages/backend/src/queue/QueueProcessorService.ts
+++ b/packages/backend/src/queue/QueueProcessorService.ts
@@ -5,11 +5,13 @@
 
 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 { 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';
@@ -77,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;
@@ -87,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,
@@ -139,207 +143,375 @@ export class QueueProcessorService implements OnApplicationShutdown {
 		}
 
 		//#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 '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.stack}) id=${job ? job.id : '-'}`, { job, e: renderError(err) });
+					if (config.sentryForBackend) {
+						Sentry.captureMessage(`Queue: System: ${job?.name ?? '?'}: ${err.message}`, {
+							level: 'error',
+							extra: { job, err },
+						});
+					}
+				})
+				.on('error', (err: Error) => logger.error(`error ${err.stack}`, { 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 'exportAccountData': return this.exportAccountDataProcessorService.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 'importNotes': return this.importNotesProcessorService.process(job);
-				case 'importTweetsToDb': return this.importNotesProcessorService.processTwitterDb(job);
-				case 'importIGToDb': return this.importNotesProcessorService.processIGDb(job);
-				case 'importFBToDb': return this.importNotesProcessorService.processFBDb(job);
-				case 'importMastoToDb': return this.importNotesProcessorService.processMastoToDb(job);
-				case 'importPleroToDb': return this.importNotesProcessorService.processPleroToDb(job);
-				case 'importKeyNotesToDb': return this.importNotesProcessorService.processKeyNotesToDb(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 'exportAccountData': return this.exportAccountDataProcessorService.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 'importNotes': return this.importNotesProcessorService.process(job);
+					case 'importTweetsToDb': return this.importNotesProcessorService.processTwitterDb(job);
+					case 'importIGToDb': return this.importNotesProcessorService.processIGDb(job);
+					case 'importFBToDb': return this.importNotesProcessorService.processFBDb(job);
+					case 'importMastoToDb': return this.importNotesProcessorService.processMastoToDb(job);
+					case 'importPleroToDb': return this.importNotesProcessorService.processPleroToDb(job);
+					case 'importKeyNotesToDb': return this.importNotesProcessorService.processKeyNotesToDb(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.stack}) id=${job ? job.id : '-'}`, { job, e: renderError(err) });
+					if (config.sentryForBackend) {
+						Sentry.captureMessage(`Queue: DB: ${job?.name ?? '?'}: ${err.message}`, {
+							level: 'error',
+							extra: { job, err },
+						});
+					}
+				})
+				.on('error', (err: Error) => logger.error(`error ${err.stack}`, { 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.stack}) ${getJobInfo(job)} to=${job ? job.data.to : '-'}`);
+					if (config.sentryForBackend) {
+						Sentry.captureMessage(`Queue: Deliver: ${err.message}`, {
+							level: 'error',
+							extra: { job, err },
+						});
+					}
+				})
+				.on('error', (err: Error) => logger.error(`error ${err.stack}`, { 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.stack}) ${getJobInfo(job)} activity=${job ? (job.data.activity ? job.data.activity.id : 'none') : '-'}`, { job, e: renderError(err) });
+					if (config.sentryForBackend) {
+						Sentry.captureMessage(`Queue: Inbox: ${err.message}`, {
+							level: 'error',
+							extra: { job, err },
+						});
+					}
+				})
+				.on('error', (err: Error) => logger.error(`error ${err.stack}`, { 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.stack}) ${getJobInfo(job)} to=${job ? job.data.to : '-'}`);
+					if (config.sentryForBackend) {
+						Sentry.captureMessage(`Queue: UserWebhookDeliver: ${err.message}`, {
+							level: 'error',
+							extra: { job, err },
+						});
+					}
+				})
+				.on('error', (err: Error) => logger.error(`error ${err.stack}`, { 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.stack}) ${getJobInfo(job)} to=${job ? job.data.to : '-'}`);
+					if (config.sentryForBackend) {
+						Sentry.captureMessage(`Queue: SystemWebhookDeliver: ${err.message}`, {
+							level: 'error',
+							extra: { job, err },
+						});
+					}
+				})
+				.on('error', (err: Error) => logger.error(`error ${err.stack}`, { 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.stack}) id=${job ? job.id : '-'}`, { job, e: renderError(err) });
+					if (config.sentryForBackend) {
+						Sentry.captureMessage(`Queue: Relationship: ${job?.name ?? '?'}: ${err.message}`, {
+							level: 'error',
+							extra: { job, err },
+						});
+					}
+				})
+				.on('error', (err: Error) => logger.error(`error ${err.stack}`, { 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.stack}) id=${job ? job.id : '-'}`, { job, e: renderError(err) });
+					if (config.sentryForBackend) {
+						Sentry.captureMessage(`Queue: ObjectStorage: ${job?.name ?? '?'}: ${err.message}`, {
+							level: 'error',
+							extra: { job, err },
+						});
+					}
+				})
+				.on('error', (err: Error) => logger.error(`error ${err.stack}`, { 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
 	}
 
@@ -350,7 +522,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(),
@@ -364,7 +537,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/DeliverProcessorService.ts b/packages/backend/src/queue/processors/DeliverProcessorService.ts
index d665945861..4076e9da90 100644
--- a/packages/backend/src/queue/processors/DeliverProcessorService.ts
+++ b/packages/backend/src/queue/processors/DeliverProcessorService.ts
@@ -45,7 +45,7 @@ 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
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/ImportAntennasProcessorService.ts b/packages/backend/src/queue/processors/ImportAntennasProcessorService.ts
index e5b7c5ac52..9c033b73e2 100644
--- a/packages/backend/src/queue/processors/ImportAntennasProcessorService.ts
+++ b/packages/backend/src/queue/processors/ImportAntennasProcessorService.ts
@@ -76,7 +76,7 @@ export class ImportAntennasProcessorService {
 					this.logger.warn('Validation Failed');
 					continue;
 				}
-				const result = await this.antennasRepository.insert({
+				const result = await this.antennasRepository.insertOne({
 					id: this.idService.gen(now.getTime()),
 					lastUsedAt: now,
 					userId: job.data.user.id,
@@ -91,7 +91,7 @@ export class ImportAntennasProcessorService {
 					excludeBots: antenna.excludeBots,
 					withReplies: antenna.withReplies,
 					withFile: antenna.withFile,
-				}).then(x => this.antennasRepository.findOneByOrFail(x.identifiers[0]));
+				});
 				this.logger.succ('Antenna created: ' + result.id);
 				this.globalEventService.publishInternalEvent('antennaCreated', result);
 			}
diff --git a/packages/backend/src/queue/processors/ImportUserListsProcessorService.ts b/packages/backend/src/queue/processors/ImportUserListsProcessorService.ts
index a5992c28c8..db9255b35d 100644
--- a/packages/backend/src/queue/processors/ImportUserListsProcessorService.ts
+++ b/packages/backend/src/queue/processors/ImportUserListsProcessorService.ts
@@ -79,11 +79,11 @@ export class ImportUserListsProcessorService {
 				});
 
 				if (list == null) {
-					list = await this.userListsRepository.insert({
+					list = await this.userListsRepository.insertOne({
 						id: this.idService.gen(),
 						userId: user.id,
 						name: listName,
-					}).then(x => this.userListsRepository.findOneByOrFail(x.identifiers[0]));
+					});
 				}
 
 				let target = this.utilityService.isSelfHost(host!) ? await this.usersRepository.findOneBy({
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 91718898b2..c0d246ebbc 100644
--- a/packages/backend/src/queue/types.ts
+++ b/packages/backend/src/queue/types.ts
@@ -131,7 +131,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 e0b187f3cf..65a8218174 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);
 	}
diff --git a/packages/backend/src/server/NodeinfoServerService.ts b/packages/backend/src/server/NodeinfoServerService.ts
index 716bb0944b..bc8d3c0411 100644
--- a/packages/backend/src/server/NodeinfoServerService.ts
+++ b/packages/backend/src/server/NodeinfoServerService.ts
@@ -135,7 +135,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/ServerService.ts b/packages/backend/src/server/ServerService.ts
index 9eddf434f7..30c133d9ec 100644
--- a/packages/backend/src/server/ServerService.ts
+++ b/packages/backend/src/server/ServerService.ts
@@ -70,7 +70,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
diff --git a/packages/backend/src/server/api/ApiCallService.ts b/packages/backend/src/server/api/ApiCallService.ts
index 271ef80554..47f64f6609 100644
--- a/packages/backend/src/server/api/ApiCallService.ts
+++ b/packages/backend/src/server/api/ApiCallService.ts
@@ -73,6 +73,16 @@ export class ApiCallService implements OnApplicationShutdown {
 				reply.header('WWW-Authenticate', `Bearer realm="Misskey", error="insufficient_scope", error_description="${err.message}"`);
 			}
 			statusCode = statusCode ?? 403;
+		} 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 (!statusCode) {
 			statusCode = 500;
 		}
@@ -93,7 +103,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 +118,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,
@@ -305,12 +318,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 +428,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/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 f44635fba0..4a08410ceb 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';
@@ -87,6 +92,11 @@ 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___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';
@@ -394,6 +404,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 };
@@ -473,6 +488,11 @@ 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 $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 };
@@ -784,6 +804,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,
@@ -863,6 +888,11 @@ 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,
 		$announcements,
 		$announcements_show,
 		$antennas_create,
@@ -1168,6 +1198,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,
@@ -1247,6 +1282,11 @@ 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,
 		$announcements,
 		$announcements_show,
 		$antennas_create,
diff --git a/packages/backend/src/server/api/RateLimiterService.ts b/packages/backend/src/server/api/RateLimiterService.ts
index 0439cdfe5e..e94160a657 100644
--- a/packages/backend/src/server/api/RateLimiterService.ts
+++ b/packages/backend/src/server/api/RateLimiterService.ts
@@ -32,11 +32,18 @@ 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();
+			}
+
+			// those lines with the "wrong" brace style / indentation are
+			// done that way so that the *other* lines stay identical to
+			// Misskey, simplifying merges
 
 			// Short-term limit
-			const min = (): void => {
+			// eslint-disable-next-line brace-style
+			const minP = () => { return new Promise<void>((ok, reject) => {
 				const minIntervalLimiter = new Limiter({
 					id: `${actor}:${limitation.key}:min`,
 					duration: limitation.minInterval! * factor,
@@ -46,25 +53,27 @@ 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 maxP().then(ok, reject);
 						} else {
-							ok();
+							return ok();
 						}
 					}
 				});
-			};
+			// eslint-disable-next-line brace-style
+			}); };
 
 			// Long term limit
-			const max = (): void => {
+			// eslint-disable-next-line brace-style
+			const maxP = () => { return new Promise<void>((ok, reject) => {
 				const limiter = new Limiter({
 					id: `${actor}:${limitation.key}`,
 					duration: limitation.duration! * factor,
@@ -74,18 +83,19 @@ 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();
 					}
 				});
-			};
+			// eslint-disable-next-line brace-style
+			}); };
 
 			const hasShortTermLimit = typeof limitation.minInterval === 'number';
 
@@ -94,12 +104,12 @@ export class RateLimiterService {
 				typeof limitation.max === 'number';
 
 			if (hasShortTermLimit) {
-				min();
+				return minP();
 			} else if (hasLongTermLimit) {
-				max();
+				return maxP();
 			} else {
-				ok();
+				return Promise.resolve();
 			}
-		});
+		}
 	}
 }
diff --git a/packages/backend/src/server/api/SigninService.ts b/packages/backend/src/server/api/SigninService.ts
index 714e56e8c3..70306c3113 100644
--- a/packages/backend/src/server/api/SigninService.ts
+++ b/packages/backend/src/server/api/SigninService.ts
@@ -29,13 +29,13 @@ export class SigninService {
 	public signin(request: FastifyRequest, reply: FastifyReply, user: MiLocalUser) {
 		setImmediate(async () => {
 			// Append signin history
-			const record = await this.signinsRepository.insert({
+			const record = await this.signinsRepository.insertOne({
 				id: this.idService.gen(),
 				userId: user.id,
 				ip: request.ip,
 				headers: request.headers as any,
 				success: true,
-			}).then(x => this.signinsRepository.findOneByOrFail(x.identifiers[0]));
+			});
 
 			// Publish signin event
 			this.globalEventService.publishMainStream(user.id, 'signin', await this.signinEntityService.pack(record));
diff --git a/packages/backend/src/server/api/SignupApiService.ts b/packages/backend/src/server/api/SignupApiService.ts
index 9c221314ac..f89c3954f8 100644
--- a/packages/backend/src/server/api/SignupApiService.ts
+++ b/packages/backend/src/server/api/SignupApiService.ts
@@ -196,14 +196,14 @@ export class SignupApiService {
 			//const salt = await bcrypt.genSalt(8);
 			const hash = await argon2.hash(password);
 
-			const pendingUser = await this.userPendingsRepository.insert({
+			const pendingUser = await this.userPendingsRepository.insertOne({
 				id: this.idService.gen(),
 				code,
 				email: emailAddress!,
 				username: username,
 				password: hash,
 				reason: reason,
-			}).then(x => this.userPendingsRepository.findOneByOrFail(x.identifiers[0]));
+			});
 
 			const link = `${this.config.url}/signup-complete/${code}`;
 
diff --git a/packages/backend/src/server/api/StreamingApiServerService.ts b/packages/backend/src/server/api/StreamingApiServerService.ts
index b8f448477b..9b8464f705 100644
--- a/packages/backend/src/server/api/StreamingApiServerService.ts
+++ b/packages/backend/src/server/api/StreamingApiServerService.ts
@@ -19,7 +19,15 @@ import { ChannelFollowingService } from '@/core/ChannelFollowingService.js';
 import { AuthenticateService, AuthenticationError } from './AuthenticateService.js';
 import MainStreamConnection from './stream/Connection.js';
 import { ChannelsService } from './stream/ChannelsService.js';
+import { RateLimiterService } from './RateLimiterService.js';
+import { RoleService } from '@/core/RoleService.js';
+import { getIpHash } from '@/misc/get-ip-hash.js';
+import proxyAddr from 'proxy-addr';
+import ms from 'ms';
 import type * as http from 'node:http';
+import type { IEndpointMeta } from './endpoints.js';
+import { LoggerService } from '@/core/LoggerService.js';
+import type Logger from '@/logger.js';
 
 @Injectable()
 export class StreamingApiServerService {
@@ -41,9 +49,35 @@ export class StreamingApiServerService {
 		private notificationService: NotificationService,
 		private usersService: UserService,
 		private channelFollowingService: ChannelFollowingService,
+		private rateLimiterService: RateLimiterService,
+		private roleService: RoleService,
+		private loggerService: LoggerService,
 	) {
 	}
 
+	@bindThis
+	private async rateLimitThis(
+		user: MiLocalUser | null | undefined,
+		requestIp: string | undefined,
+		limit: IEndpointMeta['limit'] & { key: NonNullable<string> },
+	) : Promise<boolean> {
+		let limitActor: string;
+		if (user) {
+			limitActor = user.id;
+		} else {
+			limitActor = getIpHash(requestIp || 'wtf');
+		}
+
+		const factor = user ? (await this.roleService.getUserPolicies(user.id)).rateLimitFactor : 1;
+
+		if (factor <= 0) return false;
+
+		// Rate limit
+		return await this.rateLimiterService.limit(limit, limitActor, factor)
+			.then(() => { return false; })
+			.catch(err => { return true; });
+	}
+
 	@bindThis
 	public attach(server: http.Server): void {
 		this.#wss = new WebSocket.WebSocketServer({
@@ -57,6 +91,21 @@ export class StreamingApiServerService {
 				return;
 			}
 
+			// ServerServices sets `trustProxy: true`, which inside
+			// fastify/request.js ends up calling `proxyAddr` in this way,
+			// so we do the same
+			const requestIp = proxyAddr(request, () => { return true; } );
+
+			if (await this.rateLimitThis(null, requestIp, {
+				key: 'wsconnect',
+				duration: ms('5min'),
+				max: 32,
+			})) {
+				socket.write('HTTP/1.1 429 Rate Limit Exceeded\r\n\r\n');
+				socket.destroy();
+				return;
+			}
+
 			const q = new URL(request.url, `http://${request.headers.host}`).searchParams;
 
 			let user: MiLocalUser | null = null;
@@ -94,13 +143,27 @@ export class StreamingApiServerService {
 				return;
 			}
 
+			const rateLimiter = () => {
+				// rather high limit, because when catching up at the top of a
+				// timeline, the frontend may render many many notes, each of
+				// which causes a message via `useNoteCapture` to ask for
+				// realtime updates of that note
+				return this.rateLimitThis(user, requestIp, {
+					key: 'wsmessage',
+					duration: ms('2sec'),
+					max: 4096,
+				});
+			};
+
 			const stream = new MainStreamConnection(
 				this.channelsService,
 				this.noteReadService,
 				this.notificationService,
 				this.cacheService,
 				this.channelFollowingService,
-				user, app,
+				this.loggerService,
+				user, app, requestIp,
+				rateLimiter,
 			);
 
 			await stream.init();
diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts
index 89f60933ef..e2fcd1a9d0 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';
@@ -87,6 +98,11 @@ 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___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';
@@ -392,6 +408,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],
@@ -471,6 +492,11 @@ 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],
 	['announcements', ep___announcements],
 	['announcements/show', ep___announcements_show],
 	['antennas/create', ep___antennas_create],
@@ -899,8 +925,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/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/create.ts b/packages/backend/src/server/api/endpoints/admin/ad/create.ts
index 1e7a9fb3ec..955154f4fb 100644
--- a/packages/backend/src/server/api/endpoints/admin/ad/create.ts
+++ b/packages/backend/src/server/api/endpoints/admin/ad/create.ts
@@ -50,7 +50,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 		private moderationLogService: ModerationLogService,
 	) {
 		super(meta, paramDef, async (ps, me) => {
-			const ad = await this.adsRepository.insert({
+			const ad = await this.adsRepository.insertOne({
 				id: this.idService.gen(),
 				expiresAt: new Date(ps.expiresAt),
 				startsAt: new Date(ps.startsAt),
@@ -61,7 +61,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 				ratio: ps.ratio,
 				place: ps.place,
 				memo: ps.memo,
-			}).then(r => this.adsRepository.findOneByOrFail({ id: r.identifiers[0].id }));
+			});
 
 			this.moderationLogService.log(me, 'createAd', {
 				adId: ad.id,
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/delete-all-files-of-a-user.ts b/packages/backend/src/server/api/endpoints/admin/delete-all-files-of-a-user.ts
index d8341b3ad7..747c9f48d0 100644
--- a/packages/backend/src/server/api/endpoints/admin/delete-all-files-of-a-user.ts
+++ b/packages/backend/src/server/api/endpoints/admin/delete-all-files-of-a-user.ts
@@ -39,7 +39,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 			});
 
 			for (const file of files) {
-				this.driveService.deleteFile(file);
+				this.driveService.deleteFile(file, false, me);
 			}
 		});
 	}
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/invite/create.ts b/packages/backend/src/server/api/endpoints/admin/invite/create.ts
index 0f551e1ba2..5ecae3161a 100644
--- a/packages/backend/src/server/api/endpoints/admin/invite/create.ts
+++ b/packages/backend/src/server/api/endpoints/admin/invite/create.ts
@@ -66,11 +66,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 			const ticketsPromises = [];
 
 			for (let i = 0; i < ps.count; i++) {
-				ticketsPromises.push(this.registrationTicketsRepository.insert({
+				ticketsPromises.push(this.registrationTicketsRepository.insertOne({
 					id: this.idService.gen(),
 					expiresAt: ps.expiresAt ? new Date(ps.expiresAt) : null,
 					code: generateInviteCode(),
-				}).then(x => this.registrationTicketsRepository.findOneByOrFail(x.identifiers[0])));
+				}));
 			}
 
 			const tickets = await Promise.all(ticketsPromises);
diff --git a/packages/backend/src/server/api/endpoints/admin/meta.ts b/packages/backend/src/server/api/endpoints/admin/meta.ts
index ca4d63b834..063bb6751b 100644
--- a/packages/backend/src/server/api/endpoints/admin/meta.ts
+++ b/packages/backend/src/server/api/endpoints/admin/meta.ts
@@ -132,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,
@@ -586,6 +596,7 @@ 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,
 				preservedUsernames: instance.preservedUsernames,
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..9b79100fcf 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,6 +16,16 @@ 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 = {
@@ -29,47 +37,20 @@ export const paramDef = {
 	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, forward: ps.forward }], 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/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/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-meta.ts b/packages/backend/src/server/api/endpoints/admin/update-meta.ts
index 015a1e1f7c..6bda1ae6ad 100644
--- a/packages/backend/src/server/api/endpoints/admin/update-meta.ts
+++ b/packages/backend/src/server/api/endpoints/admin/update-meta.ts
@@ -158,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.',
@@ -211,6 +218,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;
 			}
diff --git a/packages/backend/src/server/api/endpoints/antennas/create.ts b/packages/backend/src/server/api/endpoints/antennas/create.ts
index 6b7bacb054..577b9e1b1f 100644
--- a/packages/backend/src/server/api/endpoints/antennas/create.ts
+++ b/packages/backend/src/server/api/endpoints/antennas/create.ts
@@ -93,7 +93,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 			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);
 			}
 
@@ -112,7 +112,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 
 			const now = new Date();
 
-			const antenna = await this.antennasRepository.insert({
+			const antenna = await this.antennasRepository.insertOne({
 				id: this.idService.gen(now.getTime()),
 				lastUsedAt: now,
 				userId: me.id,
@@ -127,7 +127,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 				excludeBots: ps.excludeBots,
 				withReplies: ps.withReplies,
 				withFile: ps.withFile,
-			}).then(x => this.antennasRepository.findOneByOrFail(x.identifiers[0]));
+			});
 
 			this.globalEventService.publishInternalEvent('antennaCreated', antenna);
 
diff --git a/packages/backend/src/server/api/endpoints/app/create.ts b/packages/backend/src/server/api/endpoints/app/create.ts
index 492705d6f9..ba847fc4f0 100644
--- a/packages/backend/src/server/api/endpoints/app/create.ts
+++ b/packages/backend/src/server/api/endpoints/app/create.ts
@@ -54,7 +54,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 			const permission = unique(ps.permission.map(v => v.replace(/^(.+)(\/|-)(read|write)$/, '$3:$1')));
 
 			// Create account
-			const app = await this.appsRepository.insert({
+			const app = await this.appsRepository.insertOne({
 				id: this.idService.gen(),
 				userId: me ? me.id : null,
 				name: ps.name,
@@ -62,7 +62,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 				permission,
 				callbackUrl: ps.callbackUrl,
 				secret: secret,
-			}).then(x => this.appsRepository.findOneByOrFail(x.identifiers[0]));
+			});
 
 			return await this.appEntityService.pack(app, null, {
 				detail: true,
diff --git a/packages/backend/src/server/api/endpoints/auth/session/generate.ts b/packages/backend/src/server/api/endpoints/auth/session/generate.ts
index 26dd893138..f8ddfdb75c 100644
--- a/packages/backend/src/server/api/endpoints/auth/session/generate.ts
+++ b/packages/backend/src/server/api/endpoints/auth/session/generate.ts
@@ -78,11 +78,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 			const token = randomUUID();
 
 			// Create session token document
-			const doc = await this.authSessionsRepository.insert({
+			const doc = await this.authSessionsRepository.insertOne({
 				id: this.idService.gen(),
 				appId: app.id,
 				token: token,
-			}).then(x => this.authSessionsRepository.findOneByOrFail(x.identifiers[0]));
+			});
 
 			return {
 				token: doc.token,
diff --git a/packages/backend/src/server/api/endpoints/blocking/create.ts b/packages/backend/src/server/api/endpoints/blocking/create.ts
index 5066215749..fd6c164449 100644
--- a/packages/backend/src/server/api/endpoints/blocking/create.ts
+++ b/packages/backend/src/server/api/endpoints/blocking/create.ts
@@ -6,9 +6,10 @@
 import ms from 'ms';
 import { Inject, Injectable } from '@nestjs/common';
 import { Endpoint } from '@/server/api/endpoint-base.js';
-import type { UsersRepository, BlockingsRepository } from '@/models/_.js';
+import type { UsersRepository, BlockingsRepository, MutingsRepository } from '@/models/_.js';
 import { UserEntityService } from '@/core/entities/UserEntityService.js';
 import { UserBlockingService } from '@/core/UserBlockingService.js';
+import { UserMutingService } from '@/core/UserMutingService.js';
 import { DI } from '@/di-symbols.js';
 import { GetterService } from '@/server/api/GetterService.js';
 import { ApiError } from '../../error.js';
@@ -69,9 +70,13 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 		@Inject(DI.blockingsRepository)
 		private blockingsRepository: BlockingsRepository,
 
+		@Inject(DI.mutingsRepository)
+		private mutingsRepository: MutingsRepository,
+
 		private userEntityService: UserEntityService,
 		private getterService: GetterService,
 		private userBlockingService: UserBlockingService,
+		private userMutingService: UserMutingService,
 	) {
 		super(meta, paramDef, async (ps, me) => {
 			const blocker = await this.usersRepository.findOneByOrFail({ id: me.id });
@@ -99,7 +104,19 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 				throw new ApiError(meta.errors.alreadyBlocking);
 			}
 
-			await this.userBlockingService.block(blocker, blockee);
+			await Promise.all([
+				this.userBlockingService.block(blocker, blockee),
+				this.mutingsRepository.exists({
+					where: {
+						muterId: blocker.id,
+						muteeId: blockee.id,
+					},
+				}).then(exists => {
+					if (!exists) {
+						this.userMutingService.mute(blocker, blockee, null);
+					}
+				}),
+			]);
 
 			return await this.userEntityService.pack(blockee.id, blocker, {
 				schema: 'UserDetailedNotMe',
diff --git a/packages/backend/src/server/api/endpoints/blocking/delete.ts b/packages/backend/src/server/api/endpoints/blocking/delete.ts
index cebb307338..a4cd1b1cde 100644
--- a/packages/backend/src/server/api/endpoints/blocking/delete.ts
+++ b/packages/backend/src/server/api/endpoints/blocking/delete.ts
@@ -6,9 +6,10 @@
 import ms from 'ms';
 import { Inject, Injectable } from '@nestjs/common';
 import { Endpoint } from '@/server/api/endpoint-base.js';
-import type { UsersRepository, BlockingsRepository } from '@/models/_.js';
+import type { UsersRepository, BlockingsRepository, MutingsRepository } from '@/models/_.js';
 import { UserEntityService } from '@/core/entities/UserEntityService.js';
 import { UserBlockingService } from '@/core/UserBlockingService.js';
+import { UserMutingService } from '@/core/UserMutingService.js';
 import { DI } from '@/di-symbols.js';
 import { GetterService } from '@/server/api/GetterService.js';
 import { ApiError } from '../../error.js';
@@ -69,9 +70,13 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 		@Inject(DI.blockingsRepository)
 		private blockingsRepository: BlockingsRepository,
 
+		@Inject(DI.mutingsRepository)
+		private mutingsRepository: MutingsRepository,
+
 		private userEntityService: UserEntityService,
 		private getterService: GetterService,
 		private userBlockingService: UserBlockingService,
+		private userMutingService: UserMutingService,
 	) {
 		super(meta, paramDef, async (ps, me) => {
 			const blocker = await this.usersRepository.findOneByOrFail({ id: me.id });
@@ -100,7 +105,17 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 			}
 
 			// Delete blocking
-			await this.userBlockingService.unblock(blocker, blockee);
+			await Promise.all([
+				this.userBlockingService.unblock(blocker, blockee),
+				this.mutingsRepository.findOneBy({
+					muterId: blocker.id,
+					muteeId: blockee.id,
+				}).then(exists => {
+					if (exists) {
+						this.userMutingService.unmute([exists]);
+					}
+				}),
+			]);
 
 			return await this.userEntityService.pack(blockee.id, blocker, {
 				schema: 'UserDetailedNotMe',
diff --git a/packages/backend/src/server/api/endpoints/channels/create.ts b/packages/backend/src/server/api/endpoints/channels/create.ts
index 2866db5424..e3a6d2d670 100644
--- a/packages/backend/src/server/api/endpoints/channels/create.ts
+++ b/packages/backend/src/server/api/endpoints/channels/create.ts
@@ -80,7 +80,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 				}
 			}
 
-			const channel = await this.channelsRepository.insert({
+			const channel = await this.channelsRepository.insertOne({
 				id: this.idService.gen(),
 				userId: me.id,
 				name: ps.name,
@@ -89,7 +89,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 				isSensitive: ps.isSensitive ?? false,
 				...(ps.color !== undefined ? { color: ps.color } : {}),
 				allowRenoteToExternal: ps.allowRenoteToExternal ?? true,
-			} as MiChannel).then(x => this.channelsRepository.findOneByOrFail(x.identifiers[0]));
+			} as MiChannel);
 
 			return await this.channelEntityService.pack(channel, 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/files.ts b/packages/backend/src/server/api/endpoints/drive/files.ts
index 10c521332d..d615036ce8 100644
--- a/packages/backend/src/server/api/endpoints/drive/files.ts
+++ b/packages/backend/src/server/api/endpoints/drive/files.ts
@@ -9,6 +9,8 @@ import type { DriveFilesRepository } from '@/models/_.js';
 import { QueryService } from '@/core/QueryService.js';
 import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
 import { DI } from '@/di-symbols.js';
+import { sqlLikeEscape } from '@/misc/sql-like-escape.js';
+import { Brackets } from 'typeorm';
 
 export const meta = {
 	tags: ['drive'],
@@ -37,6 +39,7 @@ export const paramDef = {
 		folderId: { type: 'string', format: 'misskey:id', nullable: true, default: null },
 		type: { type: 'string', nullable: true, pattern: /^[a-zA-Z\/\-*]+$/.toString().slice(1, -1) },
 		sort: { type: 'string', nullable: true, enum: ['+createdAt', '-createdAt', '+name', '-name', '+size', '-size', null] },
+		searchQuery: { type: 'string', default: '' }
 	},
 	required: [],
 } as const;
@@ -60,6 +63,15 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 				query.andWhere('file.folderId IS NULL');
 			}
 
+			if (ps.searchQuery.length > 0) {
+				const args = { searchQuery: `%${sqlLikeEscape(ps.searchQuery)}%` };
+				query.andWhere(new Brackets((qb) => {
+					qb
+						.where('file.name ILIKE :searchQuery', args)
+						.orWhere('file.comment ILIKE :searchQuery', args);
+				}));
+			}
+
 			if (ps.type) {
 				if (ps.type.endsWith('/*')) {
 					query.andWhere('file.type like :type', { type: ps.type.replace('/*', '/') + '%' });
diff --git a/packages/backend/src/server/api/endpoints/drive/folders.ts b/packages/backend/src/server/api/endpoints/drive/folders.ts
index 8c4848f8e1..9bcd824882 100644
--- a/packages/backend/src/server/api/endpoints/drive/folders.ts
+++ b/packages/backend/src/server/api/endpoints/drive/folders.ts
@@ -9,6 +9,7 @@ import type { DriveFoldersRepository } from '@/models/_.js';
 import { QueryService } from '@/core/QueryService.js';
 import { DriveFolderEntityService } from '@/core/entities/DriveFolderEntityService.js';
 import { DI } from '@/di-symbols.js';
+import { sqlLikeEscape } from '@/misc/sql-like-escape.js';
 
 export const meta = {
 	tags: ['drive'],
@@ -35,6 +36,7 @@ export const paramDef = {
 		sinceId: { type: 'string', format: 'misskey:id' },
 		untilId: { type: 'string', format: 'misskey:id' },
 		folderId: { type: 'string', format: 'misskey:id', nullable: true, default: null },
+		searchQuery: { type: 'string', default: '' }
 	},
 	required: [],
 } as const;
@@ -58,6 +60,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 				query.andWhere('folder.parentId IS NULL');
 			}
 
+			if (ps.searchQuery.length > 0) {
+				query.andWhere('folder.name ILIKE :searchQuery', { searchQuery: `%${sqlLikeEscape(ps.searchQuery)}%` });
+			}
 			const folders = await query.limit(ps.limit).getMany();
 
 			return await Promise.all(folders.map(folder => this.driveFolderEntityService.pack(folder)));
diff --git a/packages/backend/src/server/api/endpoints/drive/folders/create.ts b/packages/backend/src/server/api/endpoints/drive/folders/create.ts
index c94070d9ff..08d9d9cdc3 100644
--- a/packages/backend/src/server/api/endpoints/drive/folders/create.ts
+++ b/packages/backend/src/server/api/endpoints/drive/folders/create.ts
@@ -75,12 +75,12 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 			}
 
 			// Create folder
-			const folder = await this.driveFoldersRepository.insert({
+			const folder = await this.driveFoldersRepository.insertOne({
 				id: this.idService.gen(),
 				name: ps.name,
 				parentId: parent !== null ? parent.id : null,
 				userId: me.id,
-			}).then(x => this.driveFoldersRepository.findOneByOrFail(x.identifiers[0]));
+			});
 
 			const folderObj = await this.driveFolderEntityService.pack(folder);
 
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 c3f2247b69..c1ce3f2238 100644
--- a/packages/backend/src/server/api/endpoints/federation/instances.ts
+++ b/packages/backend/src/server/api/endpoints/federation/instances.ts
@@ -203,7 +203,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/create.ts b/packages/backend/src/server/api/endpoints/flash/create.ts
index 361496e17e..64f13a577e 100644
--- a/packages/backend/src/server/api/endpoints/flash/create.ts
+++ b/packages/backend/src/server/api/endpoints/flash/create.ts
@@ -59,7 +59,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 		private idService: IdService,
 	) {
 		super(meta, paramDef, async (ps, me) => {
-			const flash = await this.flashsRepository.insert({
+			const flash = await this.flashsRepository.insertOne({
 				id: this.idService.gen(),
 				userId: me.id,
 				updatedAt: new Date(),
@@ -68,7 +68,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 				script: ps.script,
 				permissions: ps.permissions,
 				visibility: ps.visibility,
-			}).then(x => this.flashsRepository.findOneByOrFail(x.identifiers[0]));
+			});
 
 			return await this.flashEntityService.pack(flash);
 		});
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/gallery/posts/create.ts b/packages/backend/src/server/api/endpoints/gallery/posts/create.ts
index b07cdf1ed9..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,13 +69,13 @@ 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();
 			}
 
-			const post = await this.galleryPostsRepository.insert(new MiGalleryPost({
+			const post = await this.galleryPostsRepository.insertOne(new MiGalleryPost({
 				id: this.idService.gen(),
 				updatedAt: new Date(),
 				title: ps.title,
@@ -84,7 +83,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 				userId: me.id,
 				isSensitive: ps.isSensitive,
 				fileIds: files.map(file => file.id),
-			})).then(x => this.galleryPostsRepository.findOneByOrFail(x.identifiers[0]));
+			}));
 
 			return await this.galleryPostEntityService.pack(post, me);
 		});
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/2fa/key-done.ts b/packages/backend/src/server/api/endpoints/i/2fa/key-done.ts
index a4e61a0e8f..084d4af658 100644
--- a/packages/backend/src/server/api/endpoints/i/2fa/key-done.ts
+++ b/packages/backend/src/server/api/endpoints/i/2fa/key-done.ts
@@ -14,12 +14,19 @@ import type { UserProfilesRepository, UserSecurityKeysRepository } from '@/model
 import { WebAuthnService } from '@/core/WebAuthnService.js';
 import { ApiError } from '@/server/api/error.js';
 import { UserAuthService } from '@/core/UserAuthService.js';
+import ms from 'ms';
 
 export const meta = {
 	requireCredential: true,
 
 	secure: true,
 
+	limit: {
+		duration: ms('1hour'),
+		max: 10,
+		minInterval: ms('1sec'),
+	},
+
 	errors: {
 		incorrectPassword: {
 			message: 'Incorrect password.',
diff --git a/packages/backend/src/server/api/endpoints/i/2fa/register-key.ts b/packages/backend/src/server/api/endpoints/i/2fa/register-key.ts
index cc6e9ee42d..6ab50a57c9 100644
--- a/packages/backend/src/server/api/endpoints/i/2fa/register-key.ts
+++ b/packages/backend/src/server/api/endpoints/i/2fa/register-key.ts
@@ -12,12 +12,19 @@ import { DI } from '@/di-symbols.js';
 import { WebAuthnService } from '@/core/WebAuthnService.js';
 import { ApiError } from '@/server/api/error.js';
 import { UserAuthService } from '@/core/UserAuthService.js';
+import ms from 'ms';
 
 export const meta = {
 	requireCredential: true,
 
 	secure: true,
 
+	limit: {
+		duration: ms('1hour'),
+		max: 10,
+		minInterval: ms('1sec'),
+	},
+
 	errors: {
 		userNotFound: {
 			message: 'User not found.',
diff --git a/packages/backend/src/server/api/endpoints/i/2fa/register.ts b/packages/backend/src/server/api/endpoints/i/2fa/register.ts
index 7283159f87..888d0fc6ef 100644
--- a/packages/backend/src/server/api/endpoints/i/2fa/register.ts
+++ b/packages/backend/src/server/api/endpoints/i/2fa/register.ts
@@ -14,12 +14,19 @@ import { DI } from '@/di-symbols.js';
 import type { Config } from '@/config.js';
 import { ApiError } from '@/server/api/error.js';
 import { UserAuthService } from '@/core/UserAuthService.js';
+import ms from 'ms';
 
 export const meta = {
 	requireCredential: true,
 
 	secure: true,
 
+	limit: {
+		duration: ms('1hour'),
+		max: 10,
+		minInterval: ms('1sec'),
+	},
+
 	errors: {
 		incorrectPassword: {
 			message: 'Incorrect password.',
diff --git a/packages/backend/src/server/api/endpoints/i/2fa/remove-key.ts b/packages/backend/src/server/api/endpoints/i/2fa/remove-key.ts
index 098fd59303..614fd0c498 100644
--- a/packages/backend/src/server/api/endpoints/i/2fa/remove-key.ts
+++ b/packages/backend/src/server/api/endpoints/i/2fa/remove-key.ts
@@ -13,10 +13,17 @@ import { GlobalEventService } from '@/core/GlobalEventService.js';
 import { DI } from '@/di-symbols.js';
 import { ApiError } from '@/server/api/error.js';
 import { UserAuthService } from '@/core/UserAuthService.js';
+import ms from 'ms';
 
 export const meta = {
 	requireCredential: true,
 
+	limit: {
+		duration: ms('1hour'),
+		max: 10,
+		minInterval: ms('1sec'),
+	},
+
 	secure: true,
 
 	errors: {
diff --git a/packages/backend/src/server/api/endpoints/i/2fa/unregister.ts b/packages/backend/src/server/api/endpoints/i/2fa/unregister.ts
index 8da331505b..2773825373 100644
--- a/packages/backend/src/server/api/endpoints/i/2fa/unregister.ts
+++ b/packages/backend/src/server/api/endpoints/i/2fa/unregister.ts
@@ -13,12 +13,19 @@ import { GlobalEventService } from '@/core/GlobalEventService.js';
 import { DI } from '@/di-symbols.js';
 import { ApiError } from '@/server/api/error.js';
 import { UserAuthService } from '@/core/UserAuthService.js';
+import ms from 'ms';
 
 export const meta = {
 	requireCredential: true,
 
 	secure: true,
 
+	limit: {
+		duration: ms('1hour'),
+		max: 10,
+		minInterval: ms('1sec'),
+	},
+
 	errors: {
 		incorrectPassword: {
 			message: 'Incorrect password.',
diff --git a/packages/backend/src/server/api/endpoints/i/change-password.ts b/packages/backend/src/server/api/endpoints/i/change-password.ts
index 6aedde717c..f131c7e9d1 100644
--- a/packages/backend/src/server/api/endpoints/i/change-password.ts
+++ b/packages/backend/src/server/api/endpoints/i/change-password.ts
@@ -10,10 +10,17 @@ import { Endpoint } from '@/server/api/endpoint-base.js';
 import type { UserProfilesRepository } from '@/models/_.js';
 import { DI } from '@/di-symbols.js';
 import { UserAuthService } from '@/core/UserAuthService.js';
+import ms from 'ms';
 
 export const meta = {
 	requireCredential: true,
 
+	limit: {
+		duration: ms('1hour'),
+		max: 10,
+		minInterval: ms('1sec'),
+	},
+
 	secure: true,
 } as const;
 
diff --git a/packages/backend/src/server/api/endpoints/i/delete-account.ts b/packages/backend/src/server/api/endpoints/i/delete-account.ts
index af4d601ad6..565eaaafc0 100644
--- a/packages/backend/src/server/api/endpoints/i/delete-account.ts
+++ b/packages/backend/src/server/api/endpoints/i/delete-account.ts
@@ -11,10 +11,17 @@ import { Endpoint } from '@/server/api/endpoint-base.js';
 import { DeleteAccountService } from '@/core/DeleteAccountService.js';
 import { DI } from '@/di-symbols.js';
 import { UserAuthService } from '@/core/UserAuthService.js';
+import ms from 'ms';
 
 export const meta = {
 	requireCredential: true,
 
+	limit: {
+		duration: ms('1hour'),
+		max: 10,
+		minInterval: ms('1sec'),
+	},
+
 	secure: true,
 } as const;
 
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..bc46163e3d 100644
--- a/packages/backend/src/server/api/endpoints/i/import-antennas.ts
+++ b/packages/backend/src/server/api/endpoints/i/import-antennas.ts
@@ -78,7 +78,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/regenerate-token.ts b/packages/backend/src/server/api/endpoints/i/regenerate-token.ts
index e1cdfdc185..814ffb5488 100644
--- a/packages/backend/src/server/api/endpoints/i/regenerate-token.ts
+++ b/packages/backend/src/server/api/endpoints/i/regenerate-token.ts
@@ -11,10 +11,17 @@ import type { UsersRepository, UserProfilesRepository } from '@/models/_.js';
 import generateUserToken from '@/misc/generate-native-user-token.js';
 import { GlobalEventService } from '@/core/GlobalEventService.js';
 import { DI } from '@/di-symbols.js';
+import ms from 'ms';
 
 export const meta = {
 	requireCredential: true,
 
+	limit: {
+		duration: ms('1hour'),
+		max: 10,
+		minInterval: ms('1sec'),
+	},
+
 	secure: true,
 } as const;
 
diff --git a/packages/backend/src/server/api/endpoints/i/update.ts b/packages/backend/src/server/api/endpoints/i/update.ts
index aa2f85845f..6cc22e7994 100644
--- a/packages/backend/src/server/api/endpoints/i/update.ts
+++ b/packages/backend/src/server/api/endpoints/i/update.ts
@@ -25,7 +25,7 @@ import { UserFollowingService } from '@/core/UserFollowingService.js';
 import { AccountUpdateService } from '@/core/AccountUpdateService.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';
@@ -272,8 +272,16 @@ 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.lang !== undefined) profileUpdates.lang = ps.lang;
 			if (ps.location !== undefined) profileUpdates.location = ps.location;
@@ -306,14 +314,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;
 			}
@@ -333,12 +343,16 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 			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 (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);
@@ -354,6 +368,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);
@@ -384,14 +401,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,
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 535a3ea308..9eb7f5b3a0 100644
--- a/packages/backend/src/server/api/endpoints/i/webhooks/create.ts
+++ b/packages/backend/src/server/api/endpoints/i/webhooks/create.ts
@@ -85,18 +85,18 @@ 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);
 			}
 
-			const webhook = await this.webhooksRepository.insert({
+			const webhook = await this.webhooksRepository.insertOne({
 				id: this.idService.gen(),
 				userId: me.id,
 				name: ps.name,
 				url: ps.url,
 				secret: ps.secret,
 				on: ps.on,
-			}).then(x => this.webhooksRepository.findOneByOrFail(x.identifiers[0]));
+			});
 
 			this.globalEventService.publishInternalEvent('webhookCreated', webhook);
 
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/create.ts b/packages/backend/src/server/api/endpoints/invite/create.ts
index 0ff125ad9c..a70b587da7 100644
--- a/packages/backend/src/server/api/endpoints/invite/create.ts
+++ b/packages/backend/src/server/api/endpoints/invite/create.ts
@@ -66,13 +66,13 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 				}
 			}
 
-			const ticket = await this.registrationTicketsRepository.insert({
+			const ticket = await this.registrationTicketsRepository.insertOne({
 				id: this.idService.gen(),
 				createdBy: me,
 				createdById: me.id,
 				expiresAt: policies.inviteExpirationTime ? new Date(Date.now() + (policies.inviteExpirationTime * 1000 * 60)) : null,
 				code: generateInviteCode(),
-			}).then(x => this.registrationTicketsRepository.findOneByOrFail(x.identifiers[0]));
+			});
 
 			return await this.inviteCodeEntityService.pack(ticket, me);
 		});
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 ce5ddadb9e..fdc9a77956 100644
--- a/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts
+++ b/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts
@@ -141,9 +141,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,
@@ -155,6 +162,13 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 				alwaysIncludeMyNotes: true,
 				excludePureRenotes: !ps.withRenotes,
 				excludeBots: !ps.withBots,
+				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/polls/vote.ts b/packages/backend/src/server/api/endpoints/notes/polls/vote.ts
index a91c506afd..f33f49075b 100644
--- a/packages/backend/src/server/api/endpoints/notes/polls/vote.ts
+++ b/packages/backend/src/server/api/endpoints/notes/polls/vote.ts
@@ -144,12 +144,12 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 			}
 
 			// Create vote
-			const vote = await this.pollVotesRepository.insert({
+			const vote = await this.pollVotesRepository.insertOne({
 				id: this.idService.gen(createdAt.getTime()),
 				noteId: note.id,
 				userId: me.id,
 				choice: ps.choice,
-			}).then(x => this.pollVotesRepository.findOneByOrFail(x.identifiers[0]));
+			});
 
 			// Increment votes count
 			const index = ps.choice + 1; // In SQL, array index is 1 based
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/timeline.ts b/packages/backend/src/server/api/endpoints/notes/timeline.ts
index 1e5869663f..1a14703e6e 100644
--- a/packages/backend/src/server/api/endpoints/notes/timeline.ts
+++ b/packages/backend/src/server/api/endpoints/notes/timeline.ts
@@ -116,7 +116,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 				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;
 					}
 					if (!ps.withBots && note.user?.isBot) return false;
 
diff --git a/packages/backend/src/server/api/endpoints/pages/create.ts b/packages/backend/src/server/api/endpoints/pages/create.ts
index 3a02d359f8..fa03b0b457 100644
--- a/packages/backend/src/server/api/endpoints/pages/create.ts
+++ b/packages/backend/src/server/api/endpoints/pages/create.ts
@@ -102,7 +102,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 				}
 			});
 
-			const page = await this.pagesRepository.insert(new MiPage({
+			const page = await this.pagesRepository.insertOne(new MiPage({
 				id: this.idService.gen(),
 				updatedAt: new Date(),
 				title: ps.title,
@@ -117,7 +117,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 				alignCenter: ps.alignCenter,
 				hideTitleWhenPinned: ps.hideTitleWhenPinned,
 				font: ps.font,
-			})).then(x => this.pagesRepository.findOneByOrFail(x.identifiers[0]));
+			}));
 
 			return await this.pageEntityService.pack(page);
 		});
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..15832ef7f8 100644
--- a/packages/backend/src/server/api/endpoints/pinned-users.ts
+++ b/packages/backend/src/server/api/endpoints/pinned-users.ts
@@ -12,7 +12,6 @@ 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'],
@@ -53,7 +52,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 				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/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 e2db71c5c7..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,15 +100,15 @@ 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);
 			}
 
-			const userList = await this.userListsRepository.insert({
+			const userList = await this.userListsRepository.insertOne({
 				id: this.idService.gen(),
 				userId: me.id,
 				name: ps.name,
-			} as MiUserList).then(x => this.userListsRepository.findOneByOrFail(x.identifiers[0]));
+			} as MiUserList);
 
 			const users = (await this.userListMembershipsRepository.findBy({
 				userListId: ps.listId,
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 952580e639..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,15 +61,15 @@ 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);
 			}
 
-			const userList = await this.userListsRepository.insert({
+			const userList = await this.userListsRepository.insertOne({
 				id: this.idService.gen(),
 				userId: me.id,
 				name: ps.name,
-			} as MiUserList).then(x => this.userListsRepository.findOneByOrFail(x.identifiers[0]));
+			} as MiUserList);
 
 			return await this.userListEntityService.pack(userList);
 		});
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 0685858d77..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, UserProfilesRepository } 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,71 +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,
-
-		@Inject(DI.userProfilesRepository)
-		private userProfilesRepository: UserProfilesRepository,
-
-		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.insert({
-				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,
-			}).then(x => this.abuseUserReportsRepository.findOneByOrFail(x.identifiers[0]));
-
-			// 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 profile = await this.userProfilesRepository.findOneBy({ userId: moderator.id });
-
-					if (profile?.email) {
-						this.emailService.sendEmail(profile.email, 'New abuse report',
-							sanitizeHtml(ps.comment),
-							sanitizeHtml(ps.comment));
-					}
-				}
-
-				const meta = await this.metaService.fetch();
-				if (meta.maintainerEmail) {
-					this.emailService.sendEmail(meta.maintainerEmail, '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 436f9a44bb..fb5954fee0 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..f102cb42e1 100644
--- a/packages/backend/src/server/api/stream/Connection.ts
+++ b/packages/backend/src/server/api/stream/Connection.ts
@@ -14,9 +14,15 @@ 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';
+import { LoggerService } from '@/core/LoggerService.js';
+import type Logger from '@/logger.js';
+
+const MAX_CHANNELS_PER_CONNECTION = 32;
 
 /**
  * Main stream connection
@@ -25,10 +31,11 @@ import type Channel from './channel.js';
 export default class Connection {
 	public user?: MiUser;
 	public token?: MiAccessToken;
+	private rateLimiter?: () => Promise<boolean>;
 	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> = {};
@@ -38,6 +45,9 @@ export default class Connection {
 	public userIdsWhoMeMutingRenotes: Set<string> = new Set();
 	public userMutedInstances: Set<string> = new Set();
 	private fetchIntervalId: NodeJS.Timeout | null = null;
+	private activeRateLimitRequests: number = 0;
+	private closingConnection: boolean = false;
+	private logger: Logger;
 
 	constructor(
 		private channelsService: ChannelsService,
@@ -45,12 +55,18 @@ export default class Connection {
 		private notificationService: NotificationService,
 		private cacheService: CacheService,
 		private channelFollowingService: ChannelFollowingService,
+		loggerService: LoggerService,
 
 		user: MiUser | null | undefined,
 		token: MiAccessToken | null | undefined,
+		private ip: string,
+		rateLimiter: () => Promise<boolean>,
 	) {
 		if (user) this.user = user;
 		if (token) this.token = token;
+		if (rateLimiter) this.rateLimiter = rateLimiter;
+
+		this.logger = loggerService.getLogger('streaming', 'coral');
 	}
 
 	@bindThis
@@ -101,7 +117,30 @@ export default class Connection {
 	 */
 	@bindThis
 	private async onWsConnectionMessage(data: WebSocket.RawData) {
-		let obj: Record<string, any>;
+		let obj: JsonObject;
+
+		if (this.closingConnection) return;
+
+		if (this.rateLimiter) {
+			// this 4096 should match the `max` of the `rateLimiter`, see
+			// StreamingApiServerService
+			if (this.activeRateLimitRequests <= 4096) {
+				this.activeRateLimitRequests++;
+				const shouldRateLimit = await this.rateLimiter();
+				this.activeRateLimitRequests--;
+
+				if (shouldRateLimit) return;
+				if (this.closingConnection) return;
+			} else {
+				let connectionInfo = `IP ${this.ip}`;
+				if (this.user) connectionInfo += `, user ID ${this.user.id}`;
+
+				this.logger.warn(`Closing a connection (${connectionInfo}) due to an excessive influx of messages.`);
+				this.closingConnection = true;
+				this.wsConnection.close(1008, 'Please stop spamming the streaming API.');
+				return;
+			}
+		}
 
 		try {
 			obj = JSON.parse(data.toString());
@@ -151,7 +190,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 +203,7 @@ export default class Connection {
 	}
 
 	@bindThis
-	private onReadNotification(payload: any) {
+	private onReadNotification(payload: JsonValue | undefined) {
 		this.notificationService.readAllNotification(this.user!.id);
 	}
 
@@ -171,16 +211,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 +228,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);
 		}
@@ -201,6 +244,18 @@ export default class Connection {
 
 	@bindThis
 	private async onNoteStreamMessage(data: GlobalEvents['note']['payload']) {
+		// we must not send to the frontend information about notes from
+		// users who blocked the logged-in user, even when they're replies
+		// to notes the logged-in user can see
+		if (data.type === 'replied') {
+			const noteUserId = data.body.body.userId;
+			if (noteUserId !== null) {
+				if (this.userIdsWhoBlockingMe.has(noteUserId)) {
+					return;
+				}
+			}
+		}
+
 		this.sendMessageToWs('noteUpdated', {
 			id: data.body.id,
 			type: data.type,
@@ -212,17 +267,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 +292,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 +303,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 +354,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 49b0ae1d5b..ae9c7e3e99 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/bubble-timeline.ts b/packages/backend/src/server/api/stream/channels/bubble-timeline.ts
index 01be2d2089..647e9cab81 100644
--- a/packages/backend/src/server/api/stream/channels/bubble-timeline.ts
+++ b/packages/backend/src/server/api/stream/channels/bubble-timeline.ts
@@ -11,6 +11,7 @@ import { bindThis } from '@/decorators.js';
 import { RoleService } from '@/core/RoleService.js';
 import type { MiMeta } from '@/models/Meta.js';
 import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js';
+import type { JsonObject } from '@/misc/json-value.js';
 import Channel, { MiChannelService } from '../channel.js';
 
 class BubbleTimelineChannel extends Channel {
@@ -35,13 +36,13 @@ class BubbleTimelineChannel 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.btlAvailable) return;
 
-		this.withRenotes = params.withRenotes ?? true;
-		this.withFiles = params.withFiles ?? false;
-		this.withBots = params.withBots ?? true;
+		this.withRenotes = !!(params.withRenotes ?? true);
+		this.withFiles = !!(params.withFiles ?? false);
+		this.withBots = !!(params.withBots ?? true);
 		this.instance = await this.metaService.fetch();
 
 		// Subscribe events
diff --git a/packages/backend/src/server/api/stream/channels/channel.ts b/packages/backend/src/server/api/stream/channels/channel.ts
index 865e4fed19..226e161122 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 {
@@ -30,9 +31,10 @@ class ChannelChannel extends Channel {
 
 	@bindThis
 	public async init(params: any) {
-		this.channelId = params.channelId as string;
-		this.withFiles = params.withFiles ?? false;
-		this.withRenotes = params.withRenotes ?? true;
+		if (typeof params.channelId !== 'string') return;
+		this.channelId = params.channelId;
+		this.withFiles = !!(params.withFiles ?? false);
+		this.withRenotes = !!(params.withRenotes ?? true);
 
 		// 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 0a894147a2..6fe76747ee 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 {
@@ -33,13 +34,13 @@ 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.withBots = params.withBots ?? true;
+		this.withRenotes = !!(params.withRenotes ?? true);
+		this.withFiles = !!(params.withFiles ?? false);
+		this.withBots = !!(params.withBots ?? true);
 
 		// 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 84ff241469..359ab3e223 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;
@@ -74,7 +75,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 b83d3ec817..01645fe657 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 {
@@ -35,14 +36,14 @@ 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.withBots = params.withBots ?? true;
-		this.withFiles = params.withFiles ?? false;
+		this.withRenotes = !!(params.withRenotes ?? true);
+		this.withReplies = !!(params.withReplies ?? false);
+		this.withFiles = !!(params.withFiles ?? false);
+		this.withBots = !!(params.withBots ?? true);
 
 		// Subscribe events
 		this.subscriber.on('notesStream', this.onNote);
@@ -78,7 +79,7 @@ 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;
@@ -87,7 +88,15 @@ class HybridTimelineChannel extends Channel {
 
 		if (note.user.isSilenced && !this.following[note.userId] && note.userId !== this.user!.id) 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 48cc76c497..1f9d25b44d 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 {
@@ -34,14 +35,14 @@ 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.withBots = params.withBots ?? true;
-		this.withFiles = params.withFiles ?? false;
+		this.withRenotes = !!(params.withRenotes ?? true);
+		this.withReplies = !!(params.withReplies ?? false);
+		this.withFiles = !!(params.withFiles ?? false);
+		this.withBots = !!(params.withBots ?? true);
 
 		// 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 702e306feb..0af90a844b 100644
--- a/packages/backend/src/server/web/ClientServerService.ts
+++ b/packages/backend/src/server/web/ClientServerService.ts
@@ -25,7 +25,16 @@ 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,
+	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';
@@ -111,7 +120,8 @@ export class ClientServerService {
 		@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,
 	) {
 		//this.createServer = this.createServer.bind(this);
 	}
@@ -242,7 +252,8 @@ export class ClientServerService {
 				this.inboxQueue,
 				this.dbQueue,
 				this.objectStorageQueue,
-				this.webhookDeliverQueue,
+				this.userWebhookDeliverQueue,
+				this.systemWebhookDeliverQueue,
 			].map(q => new BullMQAdapter(q)),
 			serverAdapter,
 		});
diff --git a/packages/backend/src/server/web/FeedService.ts b/packages/backend/src/server/web/FeedService.ts
index dc7f6452c8..57a32ca934 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 '@transfem-org/sfm-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..ef804b5bfd 100644
--- a/packages/backend/src/server/web/UrlPreviewService.ts
+++ b/packages/backend/src/server/web/UrlPreviewService.ts
@@ -17,20 +17,33 @@ import { bindThis } from '@/decorators.js';
 import { ApiError } from '@/server/api/error.js';
 import { MiMeta } from '@/models/Meta.js';
 import type { FastifyRequest, FastifyReply } from 'fastify';
+import * as Redis from 'ioredis';
+import { RedisKVCache } from '@/misc/cache.js';
 
 @Injectable()
 export class UrlPreviewService {
 	private logger: Logger;
+	private previewCache: RedisKVCache<SummalyResult>;
 
 	constructor(
 		@Inject(DI.config)
 		private config: Config,
 
+		@Inject(DI.redis)
+		private redisClient: Redis.Redis,
+
 		private metaService: MetaService,
 		private httpRequestService: HttpRequestService,
 		private loggerService: LoggerService,
 	) {
 		this.logger = this.loggerService.getLogger('url-preview');
+		this.previewCache = new RedisKVCache<SummalyResult>(this.redisClient, 'summaly', {
+			lifetime: 1000 * 60 * 60 * 24, // 1d
+			memoryCacheLifetime: 1000 * 60 * 10, // 10m
+			fetcher: (key: string) => { throw new Error('the UrlPreview cache should never fetch'); },
+			toRedisConverter: (value) => JSON.stringify(value),
+			fromRedisConverter: (value) => JSON.parse(value),
+		});
 	}
 
 	@bindThis
@@ -75,9 +88,19 @@ export class UrlPreviewService {
 			};
 		}
 
+		const key = `${url}@${lang}`;
+		const cached = await this.previewCache.get(key);
+		if (cached !== undefined) {
+			this.logger.info(`Returning cache preview of ${key}`);
+			// Cache 7days
+			reply.header('Cache-Control', 'max-age=604800, immutable');
+
+			return cached;
+		}
+
 		this.logger.info(meta.urlPreviewSummaryProxyUrl
-			? `(Proxy) Getting preview of ${url}@${lang} ...`
-			: `Getting preview of ${url}@${lang} ...`);
+			? `(Proxy) Getting preview of ${key} ...`
+			: `Getting preview of ${key} ...`);
 
 		try {
 			const summary = meta.urlPreviewSummaryProxyUrl
@@ -97,6 +120,8 @@ export class UrlPreviewService {
 			summary.icon = this.wrap(summary.icon);
 			summary.thumbnail = this.wrap(summary.thumbnail);
 
+			this.previewCache.set(key, summary);
+
 			// Cache 7days
 			reply.header('Cache-Control', 'max-age=604800, immutable');
 
diff --git a/packages/backend/src/server/web/boot.js b/packages/backend/src/server/web/boot.js
index 0543cc2b65..38e37ce093 100644
--- a/packages/backend/src/server/web/boot.js
+++ b/packages/backend/src/server/web/boot.js
@@ -29,7 +29,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
@@ -181,7 +182,12 @@
 		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) {
@@ -340,6 +346,6 @@
 			#errorInfo {
 				width: 50%;
 			}
-		`)
+		}`)
 	}
 })();
diff --git a/packages/backend/src/server/web/views/base.pug b/packages/backend/src/server/web/views/base.pug
index 4007632291..36cec20c85 100644
--- a/packages/backend/src/server/web/views/base.pug
+++ b/packages/backend/src/server/web/views/base.pug
@@ -32,6 +32,7 @@ html
 		meta(property='og:site_name' content= instanceName || 'Sharkey')
 		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')
diff --git a/packages/backend/src/types.ts b/packages/backend/src/types.ts
index edcf2530bc..d83d414096 100644
--- a/packages/backend/src/types.ts
+++ b/packages/backend/src/types.ts
@@ -92,6 +92,16 @@ export const moderationLogTypes = [
 	'deleteAvatarDecoration',
 	'unsetUserAvatar',
 	'unsetUserBanner',
+	'createSystemWebhook',
+	'updateSystemWebhook',
+	'deleteSystemWebhook',
+	'createAbuseReportNotificationRecipient',
+	'updateAbuseReportNotificationRecipient',
+	'deleteAbuseReportNotificationRecipient',
+	'deleteAccount',
+	'deletePage',
+	'deleteFlash',
+	'deleteGalleryPost',
 ] as const;
 
 export type ModerationLogPayloads = {
@@ -289,6 +299,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-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/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..06548fa7da 100644
--- a/packages/backend/test/e2e/2fa.ts
+++ b/packages/backend/test/e2e/2fa.ts
@@ -206,7 +206,7 @@ describe('2要素認証', () => {
 			username,
 		}, alice);
 		assert.strictEqual(usersShowResponse.status, 200);
-		assert.strictEqual(usersShowResponse.body.twoFactorEnabled, true);
+		assert.strictEqual((usersShowResponse.body as unknown as { twoFactorEnabled: boolean }).twoFactorEnabled, true);
 
 		const signinResponse = await api('signin', {
 			...signinParam(),
@@ -248,7 +248,7 @@ 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);
@@ -257,22 +257,22 @@ describe('2要素認証', () => {
 			username,
 		});
 		assert.strictEqual(usersShowResponse.status, 200);
-		assert.strictEqual(usersShowResponse.body.securityKeys, true);
+		assert.strictEqual((usersShowResponse.body as unknown as { securityKeys: boolean }).securityKeys, true);
 
 		const signinResponse = await api('signin', {
 			...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.notEqual((signinResponse.body as unknown as { challenge: unknown | undefined }).challenge, undefined);
+		assert.notEqual((signinResponse.body as unknown as { allowCredentials: unknown | undefined }).allowCredentials, undefined);
+		assert.strictEqual((signinResponse.body as unknown as { allowCredentials: {id: string}[] }).allowCredentials[0].id, credentialId.toString('base64url'));
 
 		const signinResponse2 = await api('signin', signinWithSecurityKeyParam({
 			keyName,
 			credentialId,
 			requestOptions: signinResponse.body,
-		}));
+		} as any));
 		assert.strictEqual(signinResponse2.status, 200);
 		assert.notEqual(signinResponse2.body.i, undefined);
 
@@ -307,7 +307,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', {
@@ -319,7 +319,7 @@ describe('2要素認証', () => {
 			username,
 		});
 		assert.strictEqual(usersShowResponse.status, 200);
-		assert.strictEqual(usersShowResponse.body.usePasswordLessLogin, true);
+		assert.strictEqual((usersShowResponse.body as unknown as { usePasswordLessLogin: boolean }).usePasswordLessLogin, true);
 
 		const signinResponse = await api('signin', {
 			...signinParam(),
@@ -333,7 +333,7 @@ describe('2要素認証', () => {
 				keyName,
 				credentialId,
 				requestOptions: signinResponse.body,
-			}),
+			} as any),
 			password: '',
 		});
 		assert.strictEqual(signinResponse2.status, 200);
@@ -370,7 +370,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 +383,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,13 +420,14 @@ 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', {
 		}, alice);
 		assert.strictEqual(iResponse.status, 200);
+		assert.ok(iResponse.body.securityKeysList);
 		for (const key of iResponse.body.securityKeysList) {
 			const removeKeyResponse = await api('i/2fa/remove-key', {
 				token: otpToken(registerResponse.body.secret),
@@ -439,7 +441,7 @@ describe('2要素認証', () => {
 			username,
 		});
 		assert.strictEqual(usersShowResponse.status, 200);
-		assert.strictEqual(usersShowResponse.body.securityKeys, false);
+		assert.strictEqual((usersShowResponse.body as unknown as { securityKeys: boolean }).securityKeys, false);
 
 		const signinResponse = await api('signin', {
 			...signinParam(),
@@ -470,7 +472,7 @@ describe('2要素認証', () => {
 			username,
 		});
 		assert.strictEqual(usersShowResponse.status, 200);
-		assert.strictEqual(usersShowResponse.body.twoFactorEnabled, true);
+		assert.strictEqual((usersShowResponse.body as unknown as { twoFactorEnabled: boolean }).twoFactorEnabled, true);
 
 		const unregisterResponse = await api('i/2fa/unregister', {
 			token: otpToken(registerResponse.body.secret),
diff --git a/packages/backend/test/e2e/antennas.ts b/packages/backend/test/e2e/antennas.ts
index 101238b601..6ac14cd8dc 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,
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..5aaec7f6f9 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', () => {
@@ -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 35050130dc..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 { MiUser, UsersRepository } from '@/models/_.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;
@@ -42,7 +43,7 @@ describe('Account Move', () => {
 		dave = await signup({ username: 'dave' });
 		eve = await signup({ username: 'eve' });
 		frank = await signup({ username: 'frank' });
-		Users = connection.getRepository(MiUser);
+		Users = connection.getRepository(MiUser).extend(miRepository as MiRepository<MiUser>);
 	}, 1000 * 60 * 2);
 
 	afterAll(async () => {
@@ -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/reversi-game.ts b/packages/backend/test/e2e/reversi-game.ts
new file mode 100644
index 0000000000..788255beac
--- /dev/null
+++ b/packages/backend/test/e2e/reversi-game.ts
@@ -0,0 +1,33 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+process.env.NODE_ENV = 'test';
+
+import * as assert from 'assert';
+import { ReversiMatchResponse } from 'misskey-js/entities.js';
+import { api, signup } from '../utils.js';
+import type * as misskey from 'misskey-js';
+
+describe('ReversiGame', () => {
+	let alice: misskey.entities.SignupResponse;
+	let bob: misskey.entities.SignupResponse;
+
+	beforeAll(async () => {
+		alice = await signup({ username: 'alice' });
+		bob = await signup({ username: 'bob' });
+	}, 1000 * 60 * 2);
+
+	test('matches when alice invites bob and bob accepts', async () => {
+		const response1 = await api('reversi/match', { userId: bob.id }, alice);
+		assert.strictEqual(response1.status, 204);
+		assert.strictEqual(response1.body, null);
+		const response2 = await api('reversi/match', { userId: alice.id }, bob);
+		assert.strictEqual(response2.status, 200);
+		assert.notStrictEqual(response2.body, null);
+		const body = response2.body as ReversiMatchResponse;
+		assert.strictEqual(body.user1.id, alice.id);
+		assert.strictEqual(body.user2.id, bob.id);
+	});
+});
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..6ce6e47781
--- /dev/null
+++ b/packages/backend/test/e2e/synalio/abuse-report.ts
@@ -0,0 +1,360 @@
+/*
+ * 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,
+					forward: false,
+				}, 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,
+					forward: false,
+				}, 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,
+					forward: false,
+				}, 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,
+					forward: false,
+				}, 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,
+					forward: false,
+				}, 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,
+					forward: false,
+				}, 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 89d48158ea..eb56f7bebf 100644
--- a/packages/backend/test/e2e/users.ts
+++ b/packages/backend/test/e2e/users.ts
@@ -234,7 +234,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' });
@@ -684,7 +684,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/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..e971659070
--- /dev/null
+++ b/packages/backend/test/unit/AbuseReportNotificationService.ts
@@ -0,0 +1,343 @@
+/*
+ * 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 { 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 { randomString } from '../utils.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: 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/FileInfoService.ts b/packages/backend/test/unit/FileInfoService.ts
index bfe7143aa4..cf54ebd909 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 { LoggerService } from '@/core/LoggerService.js';
 import type { TestingModule } from '@nestjs/testing';
@@ -27,6 +27,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({
@@ -58,11 +67,7 @@ describe('FileInfoService', () => {
 
 	test('Empty file', async () => {
 		const path = `${resources}/emptyfile`;
-		const info = await fileInfoService.getFileInfo(path) as any;
-		delete info.warnings;
-		delete info.blurhash;
-		delete info.sensitive;
-		delete info.porn;
+		const info = strip(await fileInfoService.getFileInfo(path));
 		assert.deepStrictEqual(info, {
 			size: 0,
 			md5: 'd41d8cd98f00b204e9800998ecf8427e',
@@ -78,32 +83,24 @@ describe('FileInfoService', () => {
 
 	describe('IMAGE', () => {
 		test('Generic JPEG', async () => {
-			const path = `${resources}/Lenna.jpg`;
-			const info = await fileInfoService.getFileInfo(path) 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));
 			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) as any;
-			delete info.warnings;
-			delete info.blurhash;
-			delete info.sensitive;
-			delete info.porn;
+			const info = strip(await fileInfoService.getFileInfo(path));
 			assert.deepStrictEqual(info, {
 				size: 1868,
 				md5: '08189c607bea3b952704676bb3c979e0',
@@ -119,11 +116,7 @@ describe('FileInfoService', () => {
 
 		test('Generic AGIF', async () => {
 			const path = `${resources}/anime.gif`;
-			const info = await fileInfoService.getFileInfo(path) as any;
-			delete info.warnings;
-			delete info.blurhash;
-			delete info.sensitive;
-			delete info.porn;
+			const info = strip(await fileInfoService.getFileInfo(path));
 			assert.deepStrictEqual(info, {
 				size: 2248,
 				md5: '32c47a11555675d9267aee1a86571e7e',
@@ -139,11 +132,7 @@ describe('FileInfoService', () => {
 
 		test('PNG with alpha', async () => {
 			const path = `${resources}/with-alpha.png`;
-			const info = await fileInfoService.getFileInfo(path) as any;
-			delete info.warnings;
-			delete info.blurhash;
-			delete info.sensitive;
-			delete info.porn;
+			const info = strip(await fileInfoService.getFileInfo(path));
 			assert.deepStrictEqual(info, {
 				size: 3772,
 				md5: 'f73535c3e1e27508885b69b10cf6e991',
@@ -159,11 +148,7 @@ describe('FileInfoService', () => {
 
 		test('Generic SVG', async () => {
 			const path = `${resources}/image.svg`;
-			const info = await fileInfoService.getFileInfo(path) as any;
-			delete info.warnings;
-			delete info.blurhash;
-			delete info.sensitive;
-			delete info.porn;
+			const info = strip(await fileInfoService.getFileInfo(path));
 			assert.deepStrictEqual(info, {
 				size: 505,
 				md5: 'b6f52b4b021e7b92cdd04509c7267965',
@@ -180,11 +165,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) as any;
-			delete info.warnings;
-			delete info.blurhash;
-			delete info.sensitive;
-			delete info.porn;
+			const info = strip(await fileInfoService.getFileInfo(path));
 			assert.deepStrictEqual(info, {
 				size: 544,
 				md5: '4b7a346cde9ccbeb267e812567e33397',
@@ -200,11 +181,7 @@ describe('FileInfoService', () => {
 
 		test('Dimension limit', async () => {
 			const path = `${resources}/25000x25000.png`;
-			const info = await fileInfoService.getFileInfo(path) as any;
-			delete info.warnings;
-			delete info.blurhash;
-			delete info.sensitive;
-			delete info.porn;
+			const info = strip(await fileInfoService.getFileInfo(path));
 			assert.deepStrictEqual(info, {
 				size: 75933,
 				md5: '268c5dde99e17cf8fe09f1ab3f97df56',
@@ -220,11 +197,7 @@ describe('FileInfoService', () => {
 
 		test('Rotate JPEG', async () => {
 			const path = `${resources}/rotate.jpg`;
-			const info = await fileInfoService.getFileInfo(path) as any;
-			delete info.warnings;
-			delete info.blurhash;
-			delete info.sensitive;
-			delete info.porn;
+			const info = strip(await fileInfoService.getFileInfo(path));
 			assert.deepStrictEqual(info, {
 				size: 12624,
 				md5: '68d5b2d8d1d1acbbce99203e3ec3857e',
@@ -242,11 +215,7 @@ describe('FileInfoService', () => {
 	describe('AUDIO', () => {
 		test('MP3', async () => {
 			const path = `${resources}/kick_gaba7.mp3`;
-			const info = await fileInfoService.getFileInfo(path) as any;
-			delete info.warnings;
-			delete info.blurhash;
-			delete info.sensitive;
-			delete info.porn;
+			const info = strip(await fileInfoService.getFileInfo(path));
 			delete info.width;
 			delete info.height;
 			delete info.orientation;
@@ -262,11 +231,7 @@ describe('FileInfoService', () => {
 
 		test('WAV', async () => {
 			const path = `${resources}/kick_gaba7.wav`;
-			const info = await fileInfoService.getFileInfo(path) as any;
-			delete info.warnings;
-			delete info.blurhash;
-			delete info.sensitive;
-			delete info.porn;
+			const info = strip(await fileInfoService.getFileInfo(path));
 			delete info.width;
 			delete info.height;
 			delete info.orientation;
@@ -282,11 +247,7 @@ describe('FileInfoService', () => {
 
 		test('AAC', async () => {
 			const path = `${resources}/kick_gaba7.aac`;
-			const info = await fileInfoService.getFileInfo(path) as any;
-			delete info.warnings;
-			delete info.blurhash;
-			delete info.sensitive;
-			delete info.porn;
+			const info = strip(await fileInfoService.getFileInfo(path));
 			delete info.width;
 			delete info.height;
 			delete info.orientation;
@@ -302,11 +263,7 @@ describe('FileInfoService', () => {
 
 		test('FLAC', async () => {
 			const path = `${resources}/kick_gaba7.flac`;
-			const info = await fileInfoService.getFileInfo(path) as any;
-			delete info.warnings;
-			delete info.blurhash;
-			delete info.sensitive;
-			delete info.porn;
+			const info = strip(await fileInfoService.getFileInfo(path));
 			delete info.width;
 			delete info.height;
 			delete info.orientation;
@@ -322,11 +279,7 @@ describe('FileInfoService', () => {
 
 		test('MPEG-4 AUDIO (M4A)', async () => {
 			const path = `${resources}/kick_gaba7.m4a`;
-			const info = await fileInfoService.getFileInfo(path) as any;
-			delete info.warnings;
-			delete info.blurhash;
-			delete info.sensitive;
-			delete info.porn;
+			const info = strip(await fileInfoService.getFileInfo(path));
 			delete info.width;
 			delete info.height;
 			delete info.orientation;
@@ -342,11 +295,7 @@ describe('FileInfoService', () => {
 
 		test('WEBM AUDIO', async () => {
 			const path = `${resources}/kick_gaba7.webm`;
-			const info = await fileInfoService.getFileInfo(path) as any;
-			delete info.warnings;
-			delete info.blurhash;
-			delete info.sensitive;
-			delete info.porn;
+			const info = strip(await fileInfoService.getFileInfo(path));
 			delete info.width;
 			delete info.height;
 			delete info.orientation;
diff --git a/packages/backend/test/unit/RoleService.ts b/packages/backend/test/unit/RoleService.ts
index ec441735d7..b6cbe4c520 100644
--- a/packages/backend/test/unit/RoleService.ts
+++ b/packages/backend/test/unit/RoleService.ts
@@ -3,17 +3,23 @@
  * 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 { GlobalModule } from '@/GlobalModule.js';
 import { RoleService } from '@/core/RoleService.js';
-import type { MiRole, MiUser, RoleAssignmentsRepository, RolesRepository, UsersRepository } from '@/models/_.js';
+import {
+	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,7 +29,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 { UserEntityService } from '@/core/entities/UserEntityService.js';
 import type { TestingModule } from '@nestjs/testing';
 import type { MockFunctionMetadata } from 'jest-mock';
 
@@ -39,27 +45,27 @@ describe('RoleService', () => {
 	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 +77,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());
 	}
@@ -258,13 +278,103 @@ 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, excludeExpire = false', async () => {
+			const [adminUser1, adminUser2, modeUser1, modeUser2, normalUser1, normalUser2] = await Promise.all([
+				createUser(), createUser(), createUser(), createUser(), createUser(), createUser(),
+			]);
+
+			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(false, false);
+			expect(result).toEqual([modeUser1.id, modeUser2.id]);
+		});
+
+		test('includeAdmins = false, excludeExpire = true', async () => {
+			const [adminUser1, adminUser2, modeUser1, modeUser2, normalUser1, normalUser2] = await Promise.all([
+				createUser(), createUser(), createUser(), createUser(), createUser(), createUser(),
+			]);
+
+			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(false, true);
+			expect(result).toEqual([modeUser1.id]);
+		});
+
+		test('includeAdmins = true, excludeExpire = false', async () => {
+			const [adminUser1, adminUser2, modeUser1, modeUser2, normalUser1, normalUser2] = await Promise.all([
+				createUser(), createUser(), createUser(), createUser(), createUser(), createUser(),
+			]);
+
+			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(true, false);
+			expect(result).toEqual([adminUser1.id, adminUser2.id, modeUser1.id, modeUser2.id]);
+		});
+
+		test('includeAdmins = true, excludeExpire = true', async () => {
+			const [adminUser1, adminUser2, modeUser1, modeUser2, normalUser1, normalUser2] = await Promise.all([
+				createUser(), createUser(), createUser(), createUser(), createUser(), createUser(),
+			]);
+
+			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(true, true);
+			expect(result).toEqual([adminUser1.id, modeUser1.id]);
+		});
+	});
+
 	describe('conditional role', () => {
 		test('~かつ~', async () => {
 			const [user1, user2, user3, user4] = await Promise.all([
@@ -697,7 +807,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 +835,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/SystemWebhookService.ts b/packages/backend/test/unit/SystemWebhookService.ts
new file mode 100644
index 0000000000..790cd1490e
--- /dev/null
+++ b/packages/backend/test/unit/SystemWebhookService.ts
@@ -0,0 +1,516 @@
+/*
+ * 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 { 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';
+import { randomString } from '../utils.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' });
+
+				expect(queueService.systemWebhookDeliver).toHaveBeenCalled();
+			});
+
+			test('非アクティブなWebhookはキューに追加されない', async () => {
+				const webhook = await createWebhook({
+					isActive: false,
+					on: ['abuseReport'],
+				});
+				await service.enqueueSystemWebhook(webhook.id, 'abuseReport', { foo: 'bar' });
+
+				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' });
+				await service.enqueueSystemWebhook(webhook2.id, 'abuseReport', { foo: 'bar' });
+
+				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/activitypub.ts b/packages/backend/test/unit/activitypub.ts
index 2e80b64a9f..889a6ac53f 100644
--- a/packages/backend/test/unit/activitypub.ts
+++ b/packages/backend/test/unit/activitypub.ts
@@ -20,7 +20,8 @@ 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';
@@ -86,6 +87,7 @@ async function createRandomRemoteUser(
 }
 
 describe('ActivityPub', () => {
+	let userProfilesRepository: UserProfilesRepository;
 	let imageService: ApImageService;
 	let noteService: ApNoteService;
 	let personService: ApPersonService;
@@ -127,6 +129,8 @@ describe('ActivityPub', () => {
 		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 +209,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', {
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/.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 a2325e69c6..758827c196 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',
@@ -127,7 +275,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..490a441b70 100644
--- a/packages/frontend/.storybook/generate.tsx
+++ b/packages/frontend/.storybook/generate.tsx
@@ -397,13 +397,15 @@ 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/Mk[A-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/search.vue'),
 		glob('src/pages/user/home.vue'),
 	]);
 	const components = globs.flat();
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/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/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/lib/rollup-plugin-unwind-css-module-class-name.test.ts b/packages/frontend/lib/rollup-plugin-unwind-css-module-class-name.test.ts
index 948cc8b2c9..5d8cf05fff 100644
--- a/packages/frontend/lib/rollup-plugin-unwind-css-module-class-name.test.ts
+++ b/packages/frontend/lib/rollup-plugin-unwind-css-module-class-name.test.ts
@@ -63,7 +63,7 @@ import { M as MkContainer } from './MkContainer-!~{03M}~.js';
 import { b as defineComponent, a as ref, e as onMounted, z as resolveComponent, g as openBlock, h as createBlock, i as withCtx, K as createTextVNode, E as toDisplayString, u as unref, l as createBaseVNode, q as normalizeClass, B as createCommentVNode, k as createElementBlock, F as Fragment, C as renderList, A as createVNode } from './vue-!~{002}~.js';
 import './photoswipe-!~{003}~.js';
 
-const _hoisted_1 = /* @__PURE__ */ createBaseVNode("i", { class: "ph-image-square ph-bold ph-lg" }, null, -1);
+const _hoisted_1 = /* @__PURE__ */ createBaseVNode("i", { class: "ti ti-photo" }, null, -1);
 const _sfc_main = /* @__PURE__ */ defineComponent({
   __name: "index.photos",
   props: {
@@ -178,7 +178,7 @@ import {M as MkContainer} from './MkContainer-!~{03M}~.js';
 import {b as defineComponent, a as ref, e as onMounted, z as resolveComponent, g as openBlock, h as createBlock, i as withCtx, K as createTextVNode, E as toDisplayString, u as unref, l as createBaseVNode, q as normalizeClass, B as createCommentVNode, k as createElementBlock, F as Fragment, C as renderList, A as createVNode} from './vue-!~{002}~.js';
 import './photoswipe-!~{003}~.js';
 const _hoisted_1 = createBaseVNode("i", {
-  class: "ph-image-square ph-bold ph-lg"
+  class: "ti ti-photo"
 }, null, -1);
 const index_photos = defineComponent({
   __name: "index.photos",
@@ -345,7 +345,7 @@ const _sfc_main = defineComponent({
             class: $style["date-1"]
           }, [
             h("i", {
-              class: \`ph-caret-up ph-bold ph-lg \${$style["date-1-icon"]}\`
+              class: \`ti ti-chevron-up \${$style["date-1-icon"]}\`
             }),
             getDateText(item.createdAt)
           ]),
@@ -354,7 +354,7 @@ const _sfc_main = defineComponent({
           }, [
             getDateText(props.items[i + 1].createdAt),
             h("i", {
-              class: \`ph-caret-down ph-bold ph-lg \${$style["date-2-icon"]}\`
+              class: \`ti ti-chevron-down \${$style["date-2-icon"]}\`
             })
           ])
         ]));
@@ -511,11 +511,11 @@ const _sfc_main = defineComponent({
         }, [h("span", {
           class: $style["date-1"]
         }, [h("i", {
-          class: \`ph-caret-up ph-bold ph-lg \${$style["date-1-icon"]}\`
+          class: \`ti ti-chevron-up \${$style["date-1-icon"]}\`
         }), getDateText(item.createdAt)]), h("span", {
           class: $style["date-2"]
         }, [getDateText(props.items[i + 1].createdAt), h("i", {
-          class: \`ph-caret-down ph-bold ph-lg \${$style["date-2-icon"]}\`
+          class: \`ti ti-chevron-down \${$style["date-2-icon"]}\`
         })])]));
         return [el, separator];
       } else {
diff --git a/packages/frontend/package.json b/packages/frontend/package.json
index 648134b491..35cb900d3b 100644
--- a/packages/frontend/package.json
+++ b/packages/frontend/package.json
@@ -23,26 +23,26 @@
 		"@misskey-dev/browser-image-resizer": "2024.1.0",
 		"@phosphor-icons/web": "^2.0.3",
 		"@rollup/plugin-json": "6.1.0",
-		"@rollup/plugin-replace": "5.0.5",
+		"@rollup/plugin-replace": "5.0.7",
 		"@rollup/pluginutils": "5.1.0",
 		"@transfem-org/sfm-js": "0.24.5",
-		"@syuilo/aiscript": "0.18.0",
+		"@syuilo/aiscript": "0.19.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",
+		"@vitejs/plugin-vue": "5.1.0",
+		"@vue/compiler-sfc": "3.4.37",
+		"aiscript-vscode": "github:aiscript-dev/aiscript-vscode#v0.1.11",
 		"astring": "1.8.6",
 		"broadcast-channel": "7.0.0",
 		"buraha": "0.0.1",
 		"canvas-confetti": "1.9.3",
-		"chart.js": "4.4.2",
+		"chart.js": "4.4.3",
 		"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.5.6",
+		"compare-versions": "6.1.1",
+		"cropperjs": "2.0.0-rc.1",
 		"date-fns": "2.30.0",
 		"escape-regexp": "0.0.1",
 		"estree-walker": "3.0.3",
@@ -56,87 +56,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",
+		"rollup": "4.19.1",
 		"sanitize-html": "2.13.0",
-		"sass": "1.76.0",
-		"shiki": "1.4.0",
+		"sass": "1.77.8",
+		"shiki": "1.12.0",
 		"strict-event-emitter-types": "2.0.0",
 		"textarea-caret": "3.1.0",
-		"three": "0.164.1",
-		"throttle-debounce": "5.0.0",
+		"three": "0.167.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.5.4",
+		"uuid": "10.0.0",
+		"v-code-diff": "1.12.0",
+		"vite": "5.3.5",
+		"vue": "3.4.37",
 		"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",
+		"@storybook/addon-actions": "8.2.6",
+		"@storybook/addon-essentials": "8.2.6",
+		"@storybook/addon-interactions": "8.2.6",
+		"@storybook/addon-links": "8.2.6",
+		"@storybook/addon-mdx-gfm": "8.2.6",
+		"@storybook/addon-storysource": "8.2.6",
+		"@storybook/blocks": "8.2.6",
+		"@storybook/components": "8.2.6",
+		"@storybook/core-events": "8.2.6",
+		"@storybook/manager-api": "8.2.6",
+		"@storybook/preview-api": "8.2.6",
+		"@storybook/react": "8.2.6",
+		"@storybook/react-vite": "8.2.6",
+		"@storybook/test": "8.2.6",
+		"@storybook/theming": "8.2.6",
+		"@storybook/types": "8.2.6",
+		"@storybook/vue3": "8.2.6",
+		"@storybook/vue3-vite": "8.1.11",
+		"@testing-library/vue": "8.1.0",
 		"@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",
+		"@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/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.11",
+		"@typescript-eslint/eslint-plugin": "7.17.0",
+		"@typescript-eslint/parser": "7.17.0",
+		"@vitest/coverage-v8": "1.6.0",
+		"@vue/runtime-core": "3.4.37",
+		"acorn": "8.12.1",
 		"cross-env": "7.0.3",
-		"cypress": "13.8.1",
-		"eslint": "8.57.0",
+		"cypress": "13.13.1",
 		"eslint-plugin-import": "2.29.1",
-		"eslint-plugin-vue": "9.25.0",
+		"eslint-plugin-vue": "9.27.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.7",
+		"msw": "2.3.4",
+		"msw-storybook-addon": "2.0.3",
+		"nodemon": "3.1.4",
+		"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.4",
+		"storybook": "8.2.6",
 		"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.0.29",
+		"vue-eslint-parser": "9.4.3",
+		"vue-tsc": "2.0.29"
 	}
 }
diff --git a/packages/frontend/src/account.ts b/packages/frontend/src/account.ts
index 90cc2e51c9..4fdd51c33b 100644
--- a/packages/frontend/src/account.ts
+++ b/packages/frontend/src/account.ts
@@ -121,7 +121,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)) {
@@ -185,10 +185,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 => {
@@ -224,21 +226,23 @@ export async function openAccountMenu(opts: {
 	if (!$i) return;
 
 	function showSigninDialog() {
-		popup(defineAsyncComponent(() => import('@/components/MkSigninDialog.vue')), {}, {
+		const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkSigninDialog.vue')), {}, {
 			done: res => {
 				addAccount(res.id, res.i);
 				success();
 			},
-		}, 'closed');
+			closed: () => dispose(),
+		});
 	}
 
 	function createAccount() {
-		popup(defineAsyncComponent(() => import('@/components/MkSignupDialog.vue')), {}, {
+		const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkSignupDialog.vue')), {}, {
 			done: res => {
 				addAccount(res.id, res.i);
 				switchAccountWithToken(res.i);
 			},
-		}, 'closed');
+			closed: () => dispose(),
+		});
 	}
 
 	async function switchAccount(account: Misskey.entities.UserDetailed) {
@@ -293,7 +297,7 @@ export async function openAccountMenu(opts: {
 			avatar: $i,
 		}, { type: 'divider' as const }, ...(opts.includeCurrentAccount ? [createItem($i)] : []), ...accountItemPromises, {
 			type: 'parent' as const,
-			icon: 'ph-plus ph-bold ph-lg',
+			icon: 'ti ti-plus',
 			text: i18n.ts.addAccount,
 			children: [{
 				text: i18n.ts.existingAccount,
@@ -304,7 +308,7 @@ export async function openAccountMenu(opts: {
 			}],
 		}, {
 			type: 'link' as const,
-			icon: 'ph-users ph-bold ph-lg',
+			icon: 'ti ti-users',
 			text: i18n.ts.manageAccounts,
 			to: '/settings/accounts',
 		}, {
diff --git a/packages/frontend/src/boot/main-boot.ts b/packages/frontend/src/boot/main-boot.ts
index e7c2ef9449..c10930a038 100644
--- a/packages/frontend/src/boot/main-boot.ts
+++ b/packages/frontend/src/boot/main-boot.ts
@@ -5,6 +5,7 @@
 
 import { createApp, defineAsyncComponent, markRaw } from 'vue';
 import { common } from './common.js';
+import type * as Misskey from 'misskey-js';
 import { ui } from '@/config.js';
 import { i18n } from '@/i18n.js';
 import { alert, confirm, popup, post, toast } from '@/os.js';
@@ -13,7 +14,6 @@ import * as sound from '@/scripts/sound.js';
 import { $i, signout, updateAccount } 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';
@@ -22,6 +22,7 @@ import { deckStore } from '@/ui/deck/deck-store.js';
 import { emojiPicker } from '@/scripts/emoji-picker.js';
 import { mainRouter } from '@/router/main.js';
 import { setFavIconDot } from '@/scripts/favicon-dot.js';
+import { type Keymap, makeHotkey } from '@/scripts/hotkey.js';
 
 export async function mainBoot() {
 	const { isClientUpdated } = await common(() => createApp(
@@ -36,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();
@@ -66,14 +69,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;
@@ -102,29 +97,34 @@ export async function mainBoot() {
 	}
 
 	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({
@@ -230,29 +230,34 @@ export async function mainBoot() {
 			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,
-				}), true);
-			}
-		}
-		miLocalStorage.setItem('lastUsed', Date.now().toString());
+		// 邪魔
+		//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,
+		//		}), true);
+		//	}
+		//}
+		//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://activitypub.software/TransFem-org/Sharkey/') {
-			popup(defineAsyncComponent(() => import('@/components/MkSourceCodeAvailablePopup.vue')), {}, {}, 'closed');
+			const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkSourceCodeAvailablePopup.vue')), {}, {
+				closed: () => dispose(),
+			});
 		}
 
 		if ('Notification' in window) {
@@ -288,7 +293,7 @@ export async function mainBoot() {
 
 		main.on('unreadNotification', () => {
 			attemptShowNotificationDot();
-			
+
 			const unreadNotificationsCount = ($i?.unreadNotificationsCount ?? 0) + 1;
 			updateAccount({
 				hasUnreadNotification: true,
@@ -325,6 +330,9 @@ export async function mainBoot() {
 			updateAccount({ hasUnreadAnnouncement: false });
 		});
 
+		// 個人宛てお知らせが発行されたとき
+		main.on('announcementCreated', onAnnouncementCreated);
+
 		// トークンが再生成されたとき
 		// このままではMisskeyが利用できないので強制的にサインアウトさせる
 		main.on('myTokenRegenerated', () => {
@@ -333,7 +341,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/MkAbuseReportWindow.vue b/packages/frontend/src/components/MkAbuseReportWindow.vue
index f228df85a6..a634a748e9 100644
--- a/packages/frontend/src/components/MkAbuseReportWindow.vue
+++ b/packages/frontend/src/components/MkAbuseReportWindow.vue
@@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 <template>
 <MkWindow ref="uiWindow" :initialWidth="400" :initialHeight="500" :canResize="true" @closed="emit('closed')">
 	<template #header>
-		<i class="ph-warning-circle ph-bold ph-lg" style="margin-right: 0.5em;"></i>
+		<i class="ti ti-exclamation-circle" style="margin-right: 0.5em;"></i>
 		<I18n :src="i18n.ts.reportAbuseOf" tag="span">
 			<template #name>
 				<b><MkAcct :user="user"/></b>
@@ -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 83283a7073..6c0774b634 100644
--- a/packages/frontend/src/components/MkAccountMoved.vue
+++ b/packages/frontend/src/components/MkAccountMoved.vue
@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <template>
 <div v-if="user" :class="$style.root">
-	<i class="ph-airplane-takeoff ph-bold ph-lg" style="margin-right: 8px;"></i>
+	<i class="ti ti-plane-departure" style="margin-right: 8px;"></i>
 	{{ i18n.ts.accountMoved }}
 	<MkMention :class="$style.link" :username="user.username" :host="user.host ?? localHost"/>
 </div>
diff --git a/packages/frontend/src/components/MkAchievements.vue b/packages/frontend/src/components/MkAchievements.vue
index 8ec3ec0505..ab7bafc47a 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/MkAnnouncementDialog.vue b/packages/frontend/src/components/MkAnnouncementDialog.vue
index 032a815ee6..c81fea175c 100644
--- a/packages/frontend/src/components/MkAnnouncementDialog.vue
+++ b/packages/frontend/src/components/MkAnnouncementDialog.vue
@@ -8,10 +8,10 @@ SPDX-License-Identifier: AGPL-3.0-only
 	<div ref="rootEl" :class="$style.root">
 		<div :class="$style.header">
 			<span :class="$style.icon">
-				<i v-if="announcement.icon === 'info'" class="ph-info ph-bold ph-lg"></i>
-				<i v-else-if="announcement.icon === 'warning'" class="ph-warning-circle ph-bold ph-lg" style="color: var(--warn);"></i>
-				<i v-else-if="announcement.icon === 'error'" class="ph-seal-warning ph-bold ph-lg" style="color: var(--error);"></i>
-				<i v-else-if="announcement.icon === 'success'" class="ph-check-circle ph-bold ph-lg" style="color: var(--success);"></i>
+				<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>
 			</span>
 			<span :class="$style.title">{{ announcement.title }}</span>
 		</div>
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 64%
rename from packages/frontend/src/pages/my-antennas/editor.vue
rename to packages/frontend/src/components/MkAntennaEditor.vue
index 51dbb3b08f..cb7ee3d6ca 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="ph-floppy-disk ph-bold ph-lg"></i> {{ i18n.ts.save }}</MkButton>
-			<MkButton v-if="antenna.id != null" inline danger @click="deleteAntenna()"><i class="ph-trash ph-bold ph-lg"></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();
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/MkButton.vue b/packages/frontend/src/components/MkButton.vue
index c8d2797e16..f968fc5861 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<{
@@ -248,7 +250,6 @@ function onMousedown(evt: MouseEvent): void {
 	}
 
 	&: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..c5b6e0caed 100644
--- a/packages/frontend/src/components/MkCaptcha.vue
+++ b/packages/frontend/src/components/MkCaptcha.vue
@@ -104,7 +104,6 @@ async function requestRender() {
 		});
 	} 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),
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 07732d9205..6dace43fde 100644
--- a/packages/frontend/src/components/MkChannelFollowButton.vue
+++ b/packages/frontend/src/components/MkChannelFollowButton.vue
@@ -12,10 +12,10 @@ SPDX-License-Identifier: AGPL-3.0-only
 >
 	<template v-if="!wait">
 		<template v-if="isFollowing">
-			<span v-if="full" :class="$style.text">{{ i18n.ts.unfollow }}</span><i class="ph-minus ph-bold ph-lg"></i>
+			<span v-if="full" :class="$style.text">{{ i18n.ts.unfollow }}</span><i class="ti ti-minus"></i>
 		</template>
 		<template v-else>
-			<span v-if="full" :class="$style.text">{{ i18n.ts.follow }}</span><i class="ph-plus ph-bold ph-lg"></i>
+			<span v-if="full" :class="$style.text">{{ i18n.ts.follow }}</span><i class="ti ti-plus"></i>
 		</template>
 	</template>
 	<template v-else>
@@ -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() {
@@ -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: var(--radius-xl);
-		}
+		outline-offset: 2px;
 	}
 
 	&:hover {
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 1bac59d6df..a50416befc 100644
--- a/packages/frontend/src/components/MkChannelPreview.vue
+++ b/packages/frontend/src/components/MkChannelPreview.vue
@@ -5,14 +5,14 @@ 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="ph-television ph-bold ph-lg"></i> {{ channel.name }}</div>
+			<div class="name"><i class="ti ti-device-tv"></i> {{ channel.name }}</div>
 			<div v-if="channel.isSensitive" class="sensitiveIndicator">{{ i18n.ts.sensitive }}</div>
 			<div class="status">
 				<div>
-					<i class="ph-users ph-bold ph-lg"></i>
+					<i class="ti ti-users ti-fw"></i>
 					<I18n :src="i18n.ts._channel.usersCount" tag="span" style="margin-left: 4px;">
 						<template #n>
 							<b>{{ channel.usersCount }}</b>
@@ -20,7 +20,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 					</I18n>
 				</div>
 				<div>
-					<i class="ph-pencil-simple ph-bold ph-lg"></i>
+					<i class="ti ti-pencil ti-fw"></i>
 					<I18n :src="i18n.ts._channel.notesCount" tag="span" style="margin-left: 4px;">
 						<template #n>
 							<b>{{ channel.notesCount }}</b>
@@ -80,6 +80,7 @@ const bannerStyle = computed(() => {
 <style lang="scss" scoped>
 .eftoefju {
 	display: block;
+	position: relative;
 	overflow: hidden;
 	width: 100%;
 
@@ -87,6 +88,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(--focus);
+		}
+	}
+
 	> .banner {
 		position: relative;
 		width: 100%;
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..4b24562249 100644
--- a/packages/frontend/src/components/MkChart.vue
+++ b/packages/frontend/src/components/MkChart.vue
@@ -19,8 +19,9 @@ SPDX-License-Identifier: AGPL-3.0-only
   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 +35,63 @@ 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,
-	},
+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'
+
+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 +114,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: {
diff --git a/packages/backend/src/misc/prelude/math.ts b/packages/frontend/src/components/MkChartLegend.stories.impl.ts
similarity index 53%
rename from packages/backend/src/misc/prelude/math.ts
rename to packages/frontend/src/components/MkChartLegend.stories.impl.ts
index 38556def2d..06146e20e4 100644
--- a/packages/backend/src/misc/prelude/math.ts
+++ b/packages/frontend/src/components/MkChartLegend.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 MkChartLegend from './MkChartLegend.vue';
+void MkChartLegend;
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 892ad31b09..00506fb735 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="ph-cookie ph-bold ph-lg" 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>
@@ -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..dd550733cb 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,6 +45,14 @@ const remaining = computed(() => {
 .link {
 	display: block;
 
+	&:focus-visible {
+		outline: none;
+
+		.root {
+			box-shadow: inset 0 0 0 2px var(--focus);
+		}
+	}
+
 	&:hover {
 		text-decoration: none;
 		color: var(--accent);
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.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 e5589813e1..2475e3dc89 100644
--- a/packages/frontend/src/components/MkCode.vue
+++ b/packages/frontend/src/components/MkCode.vue
@@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 <template>
 <div :class="$style.codeBlockRoot">
 	<button :class="$style.codeBlockCopyButton" class="_button" @click="copy">
-		<i class="ph-copy ph-bold ph-lg"></i>
+		<i class="ti ti-copy"></i>
 	</button>
 	<Suspense>
 		<template #fallback>
@@ -16,7 +16,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 		<pre v-else-if="show" :class="$style.codeBlockFallbackRoot"><code :class="$style.codeBlockFallbackCode">{{ code }}</code></pre>
 		<button v-else :class="$style.codePlaceholderRoot" @click="show = true">
 			<div :class="$style.codePlaceholderContainer">
-				<div><i class="ph-code ph-bold ph-lg"></i> {{ i18n.ts.code }}</div>
+				<div><i class="ti ti-code"></i> {{ i18n.ts.code }}</div>
 				<div>{{ i18n.ts.clickToShow }}</div>
 			</div>
 		</button>
@@ -30,7 +30,7 @@ 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<{
 	code: string;
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 30e518f8f0..b233189ab0 100644
--- a/packages/frontend/src/components/MkCodeEditor.vue
+++ b/packages/frontend/src/components/MkCodeEditor.vue
@@ -27,7 +27,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 		</div>
 	</div>
 	<div :class="$style.caption"><slot name="caption"></slot></div>
-	<MkButton v-if="manualSave && changed" primary :class="$style.save" @click="updated"><i class="ph-floppy-disk ph-bold ph-lg"></i> {{ i18n.ts.save }}</MkButton>
+	<MkButton v-if="manualSave && changed" primary :class="$style.save" @click="updated"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton>
 </div>
 </template>
 
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/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/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 95188c335e..d1fe5dbdcf 100644
--- a/packages/frontend/src/components/MkContainer.vue
+++ b/packages/frontend/src/components/MkContainer.vue
@@ -13,8 +13,8 @@ SPDX-License-Identifier: AGPL-3.0-only
 		<div :class="$style.headerSub">
 			<slot name="func" :buttonStyleClass="$style.headerButton"></slot>
 			<button v-if="foldable" :class="$style.headerButton" class="_button" @click="() => showBody = !showBody">
-				<template v-if="showBody"><i class="ph-caret-up ph-bold ph-lg"></i></template>
-				<template v-else><i class="ph-caret-down ph-bold ph-lg"></i></template>
+				<template v-if="showBody"><i class="ti ti-chevron-up"></i></template>
+				<template v-else><i class="ti ti-chevron-down"></i></template>
 			</button>
 		</div>
 	</header>
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..8ea8fa6cf3 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>
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/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/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 475fbcb397..9976cd00c9 100644
--- a/packages/frontend/src/components/MkDateSeparatedList.vue
+++ b/packages/frontend/src/components/MkDateSeparatedList.vue
@@ -76,7 +76,7 @@ export default defineComponent({
 						class: $style['date-1'],
 					}, [
 						h('i', {
-							class: `ph-caret-up ph-bold ph-lg ${$style['date-1-icon']}`,
+							class: `ti ti-chevron-up ${$style['date-1-icon']}`,
 						}),
 						getDateText(item.createdAt),
 					]),
@@ -85,7 +85,7 @@ export default defineComponent({
 					}, [
 						getDateText(props.items[i + 1].createdAt),
 						h('i', {
-							class: `ph-caret-down ph-bold ph-lg ${$style['date-2-icon']}`,
+							class: `ti ti-chevron-down ${$style['date-2-icon']}`,
 						}),
 					]),
 				]));
@@ -130,7 +130,7 @@ export default defineComponent({
 			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,
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 b534ae4c56..825c1d0513 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>
@@ -18,17 +18,17 @@ SPDX-License-Identifier: AGPL-3.0-only
 				[$style.type_info]: type === 'info',
 			}]"
 		>
-			<i v-if="type === 'success'" :class="$style.iconInner" class="ph-check ph-bold ph-lg"></i>
-			<i v-else-if="type === 'error'" :class="$style.iconInner" class="ph-x-circle ph-bold ph-lg"></i>
-			<i v-else-if="type === 'warning'" :class="$style.iconInner" class="ph-warning ph-bold ph-lg"></i>
-			<i v-else-if="type === 'info'" :class="$style.iconInner" class="ph-info ph-bold ph-lg"></i>
-			<i v-else-if="type === 'question'" :class="$style.iconInner" class="ph-question ph-bold ph-lg"></i>
+			<i v-if="type === 'success'" :class="$style.iconInner" class="ti ti-check"></i>
+			<i v-else-if="type === 'error'" :class="$style.iconInner" class="ti ti-circle-x"></i>
+			<i v-else-if="type === 'warning'" :class="$style.iconInner" class="ti ti-alert-triangle"></i>
+			<i v-else-if="type === 'info'" :class="$style.iconInner" class="ti ti-info-circle"></i>
+			<i v-else-if="type === 'question'" :class="$style.iconInner" class="ti ti-help-circle"></i>
 			<MkLoading v-else-if="type === 'waiting'" :class="$style.iconInner" :em="true"/>
 		</div>
 		<header v-if="title" :class="$style.title"><Mfm :text="title"/></header>
 		<div v-if="text" :class="$style.text"><Mfm :text="text" :isBlock="true" /></div>
 		<MkInput v-if="input" v-model="inputValue" autofocus :type="input.type || 'text'" :placeholder="input.placeholder || undefined" :autocomplete="input.autocomplete" @keydown="onInputKeydown">
-			<template v-if="input.type === 'password'" #prefix><i class="ph-lock ph-bold ph-lg"></i></template>
+			<template v-if="input.type === 'password'" #prefix><i class="ti ti-lock"></i></template>
 			<template #caption>
 				<span v-if="okButtonDisabledReason === 'charactersExceeded'" v-text="i18n.tsx._dialog.charactersExceeded({ current: (inputValue as string)?.length ?? 0, max: input.maxLength ?? 'NaN' })"/>
 				<span v-else-if="okButtonDisabledReason === 'charactersBelow'" v-text="i18n.tsx._dialog.charactersBelow({ current: (inputValue as string)?.length ?? 0, min: input.minLength ?? 'NaN' })"/>
@@ -36,7 +36,12 @@ 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">
@@ -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;
 };
 
@@ -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>
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..e4e3af99e4
--- /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(--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 a2780ddfe9..930f0f54cc 100644
--- a/packages/frontend/src/components/MkDonation.vue
+++ b/packages/frontend/src/components/MkDonation.vue
@@ -23,7 +23,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 				</template>
 			</I18n>
 			<div style="margin-top: 0.2em;">
-				<MkLink target="_blank" url="https://ko-fi.com/transfem">{{ i18n.ts.learnMore }}</MkLink>
+				<MkLink target="_blank" url="https://opencollective.com/sharkey">{{ i18n.ts.learnMore }}</MkLink>
 			</div>
 		</div>
 		<div v-if="instance.donationUrl" :class="$style.text">
@@ -41,7 +41,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 			<MkButton @click="neverShow">{{ i18n.ts.neverShow }}</MkButton>
 		</div>
 	</div>
-	<button class="_button" :class="$style.close" @click="close"><i class="ph-x ph-bold ph-lg"></i></button>
+	<button class="_button" :class="$style.close" @click="close"><i class="ti ti-x"></i></button>
 </div>
 </template>
 
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 13a2a2126c..20ad2984d8 100644
--- a/packages/frontend/src/components/MkDrive.file.vue
+++ b/packages/frontend/src/components/MkDrive.file.vue
@@ -119,14 +119,14 @@ function onDragend() {
 		background: rgba(#000, 0.05);
 
 		> .label {
-			&:before,
-			&:after {
+			&::before,
+			&::after {
 				background: #0b65a5;
 			}
 
 			&.red {
-				&:before,
-				&:after {
+				&::before,
+				&::after {
 					background: #c12113;
 				}
 			}
@@ -137,14 +137,14 @@ function onDragend() {
 		background: rgba(#000, 0.1);
 
 		> .label {
-			&:before,
-			&:after {
+			&::before,
+			&::after {
 				background: #0b588c;
 			}
 
 			&.red {
-				&:before,
-				&:after {
+				&::before,
+				&::after {
 					background: #ce2212;
 				}
 			}
@@ -163,8 +163,8 @@ function onDragend() {
 		}
 
 		> .label {
-			&:before,
-			&:after {
+			&::before,
+			&::after {
 				display: none;
 			}
 		}
@@ -185,8 +185,8 @@ function onDragend() {
 	left: 0;
 	pointer-events: none;
 
-	&:before,
-	&:after {
+	&::before,
+	&::after {
 		content: "";
 		display: block;
 		position: absolute;
@@ -194,14 +194,14 @@ function onDragend() {
 		background: #0c7ac9;
 	}
 
-	&:before {
+	&::before {
 		top: 0;
 		left: 57px;
 		width: 28px;
 		height: 8px;
 	}
 
-	&:after {
+	&::after {
 		top: 57px;
 		left: 0;
 		width: 8px;
@@ -209,8 +209,8 @@ function onDragend() {
 	}
 
 	&.red {
-		&:before,
-		&:after {
+		&::before,
+		&::after {
 			background: #c12113;
 		}
 	}
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 945f45c012..3990d0b861 100644
--- a/packages/frontend/src/components/MkDrive.folder.vue
+++ b/packages/frontend/src/components/MkDrive.folder.vue
@@ -20,14 +20,16 @@ SPDX-License-Identifier: AGPL-3.0-only
 	@dragend="onDragend"
 >
 	<p :class="$style.name">
-		<template v-if="hover"><i :class="$style.icon" class="ph-folder ph-bold ph-lg ti-fw"></i></template>
-		<template v-if="!hover"><i :class="$style.icon" class="ph-folder ph-bold ph-lg ti-fw"></i></template>
+		<template v-if="hover"><i :class="$style.icon" class="ti ti-folder ti-fw"></i></template>
+		<template v-if="!hover"><i :class="$style.icon" class="ti ti-folder ti-fw"></i></template>
 		{{ folder.name }}
 	</p>
 	<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>
 
@@ -39,7 +41,7 @@ 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 { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
 import { MenuItem } from '@/types/menu.js';
 
 const props = withDefaults(defineProps<{
@@ -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,
@@ -255,26 +273,31 @@ function onContextmenu(ev: MouseEvent) {
 	let menu: MenuItem[];
 	menu = [{
 		text: i18n.ts.openInWindow,
-		icon: 'ph-app-window ph-bold ph-lg',
+		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: 'ph-textbox ph-bold ph-lg',
+		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: 'ph-trash ph-bold ph-lg',
+		icon: 'ti ti-trash',
 		danger: true,
 		action: deleteFolder,
 	}];
 	if (defaultStore.state.devMode) {
 		menu = menu.concat([{ type: 'divider' }, {
-			icon: 'ph-identification-card ph-bold ph-lg',
+			icon: 'ti ti-id',
 			text: i18n.ts.copyFolderId,
 			action: () => {
 				copyToClipboard(props.folder.id);
@@ -295,7 +318,7 @@ function onContextmenu(ev: MouseEvent) {
 	cursor: pointer;
 
 	&.draghover {
-		&:after {
+		&::after {
 			content: "";
 			pointer-events: none;
 			position: absolute;
@@ -309,17 +332,43 @@ function onContextmenu(ev: MouseEvent) {
 	}
 }
 
-.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(--divider);
+		border-radius: 4px;
+		box-sizing: border-box;
+
+		&.checked {
+			border-color: var(--accent);
+			background: var(--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(--accentedBg);
 	}
 }
 
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.navFolder.vue b/packages/frontend/src/components/MkDrive.navFolder.vue
index d78c215328..8df3c86ebf 100644
--- a/packages/frontend/src/components/MkDrive.navFolder.vue
+++ b/packages/frontend/src/components/MkDrive.navFolder.vue
@@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 	@dragleave="onDragleave"
 	@drop.stop="onDrop"
 >
-	<i v-if="folder == null" class="ph-cloud ph-bold ph-lg" style="margin-right: 4px;"></i>
+	<i v-if="folder == null" class="ti ti-cloud" style="margin-right: 4px;"></i>
 	<span>{{ folder == null ? i18n.ts.drive : folder.name }}</span>
 </div>
 </template>
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 2990ea6861..2d1c7c95f0 100644
--- a/packages/frontend/src/components/MkDrive.vue
+++ b/packages/frontend/src/components/MkDrive.vue
@@ -16,7 +16,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 				@removeFolder="removeFolder"
 			/>
 			<template v-for="f in hierarchyFolders">
-				<span :class="[$style.navPathItem, $style.navSeparator]"><i class="ph-caret-right ph-bold ph-lg"></i></span>
+				<span :class="[$style.navPathItem, $style.navSeparator]"><i class="ti ti-chevron-right"></i></span>
 				<XNavFolder
 					:folder="f"
 					:parentFolder="folder"
@@ -27,10 +27,17 @@ SPDX-License-Identifier: AGPL-3.0-only
 					@removeFolder="removeFolder"
 				/>
 			</template>
-			<span v-if="folder != null" :class="[$style.navPathItem, $style.navSeparator]"><i class="ph-caret-right ph-bold ph-lg"></i></span>
+			<span v-if="folder != null" :class="[$style.navPathItem, $style.navSeparator]"><i class="ti ti-chevron-right"></i></span>
 			<span v-if="folder != null" :class="[$style.navPathItem, $style.navCurrent]">{{ folder.name }}</span>
 		</div>
-		<button class="_button" :class="$style.navMenu" @click="showMenu"><i class="ph-dots-three ph-bold ph-lg"></i></button>
+		<div :class="$style.navMenu">
+			<!-- "Search drive via alt text or file names" -->
+			<MkInput v-model="searchQuery" :large="true" :autofocus="true" type="search" :placeholder="i18n.ts.driveSearchbarPlaceholder" @enter="fetch">
+				<template #prefix><i class="ph-magnifying-glass ph-bold ph-lg"></i></template>
+			</MkInput>
+
+			<button class="_button" :class="$style.navMenu" @click="showMenu"><i class="ti ti-dots"></i></button>
+		</div>
 	</nav>
 	<div
 		ref="main"
@@ -52,6 +59,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"
@@ -102,6 +110,7 @@ import type { MenuItem } from '@/types/menu.js';
 import XNavFolder from '@/components/MkDrive.navFolder.vue';
 import XFolder from '@/components/MkDrive.folder.vue';
 import XFile from '@/components/MkDrive.file.vue';
+import MkInput from '@/components/MkInput.vue';
 import * as os from '@/os.js';
 import { misskeyApi } from '@/scripts/misskey-api.js';
 import { useStream } from '@/stream.js';
@@ -110,6 +119,8 @@ import { i18n } from '@/i18n.js';
 import { uploadFile, uploads } from '@/scripts/upload.js';
 import { claimAchievement } from '@/scripts/achievements.js';
 
+const searchQuery = ref('');
+
 const props = withDefaults(defineProps<{
 	initialFolder?: Misskey.entities.DriveFolder;
 	type?: string;
@@ -428,6 +439,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();
@@ -540,6 +556,7 @@ async function fetch() {
 	const foldersPromise = misskeyApi('drive/folders', {
 		folderId: folder.value ? folder.value.id : null,
 		limit: foldersMax + 1,
+		searchQuery: searchQuery.value.toString().trim(),
 	}).then(fetchedFolders => {
 		if (fetchedFolders.length === foldersMax + 1) {
 			moreFolders.value = true;
@@ -552,6 +569,7 @@ async function fetch() {
 		folderId: folder.value ? folder.value.id : null,
 		type: props.type,
 		limit: filesMax + 1,
+		searchQuery: searchQuery.value.toString().trim(),
 	}).then(fetchedFiles => {
 		if (fetchedFiles.length === filesMax + 1) {
 			moreFiles.value = true;
@@ -578,6 +596,7 @@ function fetchMoreFolders() {
 		type: props.type,
 		untilId: folders.value.at(-1)?.id,
 		limit: max + 1,
+		searchQuery: searchQuery.value.toString().trim(),
 	}).then(folders => {
 		if (folders.length === max + 1) {
 			moreFolders.value = true;
@@ -601,6 +620,7 @@ function fetchMoreFiles() {
 		type: props.type,
 		untilId: files.value.at(-1)?.id,
 		limit: max + 1,
+		searchQuery: searchQuery.value.toString().trim(),
 	}).then(files => {
 		if (files.length === max + 1) {
 			moreFiles.value = true;
@@ -623,26 +643,26 @@ function getMenu() {
 		type: 'label',
 	}, {
 		text: i18n.ts.upload,
-		icon: 'ph-upload ph-bold ph-lg',
+		icon: 'ti ti-upload',
 		action: () => { selectLocalFile(); },
 	}, {
 		text: i18n.ts.fromUrl,
-		icon: 'ph-link ph-bold ph-lg',
+		icon: 'ti ti-link',
 		action: () => { urlUpload(); },
 	}, { type: 'divider' }, {
 		text: folder.value ? folder.value.name : i18n.ts.drive,
 		type: 'label',
 	}, folder.value ? {
 		text: i18n.ts.renameFolder,
-		icon: 'ph-textbox ph-bold ph-lg',
+		icon: 'ti ti-forms',
 		action: () => { if (folder.value) renameFolder(folder.value); },
 	} : undefined, folder.value ? {
 		text: i18n.ts.deleteFolder,
-		icon: 'ph-trash ph-bold ph-lg',
+		icon: 'ti ti-trash',
 		action: () => { deleteFolder(folder.value as Misskey.entities.DriveFolder); },
 	} : undefined, {
 		text: i18n.ts.createFolder,
-		icon: 'ph-folder ph-bold ph-lg-plus',
+		icon: 'ti ti-folder-plus',
 		action: () => { createFolder(); },
 	}];
 
@@ -747,8 +767,13 @@ onBeforeUnmount(() => {
 }
 
 .navMenu {
+	display: flex;
 	margin-left: auto;
-	padding: 0 12px;
+	align-items: center;
+}
+
+.navMenu > *:not(:last-child) {
+	padding-right: 12px;
 }
 
 .main {
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 2f1fef4ea6..9a6d272113 100644
--- a/packages/frontend/src/components/MkDriveFileThumbnail.vue
+++ b/packages/frontend/src/components/MkDriveFileThumbnail.vue
@@ -6,16 +6,16 @@ SPDX-License-Identifier: AGPL-3.0-only
 <template>
 <div ref="thumbnail" :class="$style.root">
 	<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="ph-image-square ph-bold ph-lg" :class="$style.icon"></i>
-	<i v-else-if="is === 'video'" class="ph-video ph-bold ph-lg" :class="$style.icon"></i>
-	<i v-else-if="is === 'audio' || is === 'midi'" class="ph-file-audio ph-bold ph-lg" :class="$style.icon"></i>
-	<i v-else-if="is === 'csv'" class="ph-file-text ph-bold ph-lg" :class="$style.icon"></i>
-	<i v-else-if="is === 'pdf'" class="ph-file-text ph-bold ph-lg" :class="$style.icon"></i>
-	<i v-else-if="is === 'textfile'" class="ph-file-text ph-bold ph-lg" :class="$style.icon"></i>
-	<i v-else-if="is === 'archive'" class="ph-file-zip ph-bold ph-lg" :class="$style.icon"></i>
-	<i v-else class="ph-file ph-bold ph-lg" :class="$style.icon"></i>
+	<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>
+	<i v-else-if="is === 'audio' || is === 'midi'" class="ti ti-file-music" :class="$style.icon"></i>
+	<i v-else-if="is === 'csv'" class="ti ti-file-text" :class="$style.icon"></i>
+	<i v-else-if="is === 'pdf'" class="ti ti-file-text" :class="$style.icon"></i>
+	<i v-else-if="is === 'textfile'" class="ti ti-file-text" :class="$style.icon"></i>
+	<i v-else-if="is === 'archive'" class="ti ti-file-zip" :class="$style.icon"></i>
+	<i v-else class="ti ti-file" :class="$style.icon"></i>
 
-	<i v-if="isThumbnailAvailable && is === 'video'" class="ph-video ph-bold ph-lg" :class="$style.iconSub"></i>
+	<i v-if="isThumbnailAvailable && is === 'video'" class="ti ti-video" :class="$style.iconSub"></i>
 </div>
 </template>
 
@@ -26,7 +26,7 @@ import ImgWithBlurhash from '@/components/MkImgWithBlurhash.vue';
 
 const props = defineProps<{
 	file: Misskey.entities.DriveFile;
-	fit: string;
+	fit: 'cover' | 'contain';
 }>();
 
 const is = computed(() => {
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/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 a5839586b6..008613c27e 100644
--- a/packages/frontend/src/components/MkEmojiPicker.section.vue
+++ b/packages/frontend/src/components/MkEmojiPicker.section.vue
@@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 <!-- フォルダの中にはカスタム絵文字だけ(Unicode絵文字もこっち) -->
 <section v-if="!hasChildSection" v-panel style="border-radius: var(--radius-sm); border-bottom: 0.5px solid var(--divider);">
 	<header class="_acrylic" @click="shown = !shown">
-		<i class="toggle ti-fw" :class="shown ? 'ph-caret-down ph-bold ph-lg' : 'ph-caret-up ph-bold ph-lg'"></i> <slot></slot> (<i class="ph-bold ph-lg"></i>:{{ emojis.length }})
+		<i class="toggle ti-fw" :class="shown ? 'ti ti-chevron-down' : 'ti ti-chevron-up'"></i> <slot></slot> (<i class="ph-smiley-sticker ph-bold ph-lg"></i>:{{ emojis.length }})
 	</header>
 	<div v-if="shown" class="body">
 		<button
@@ -28,7 +28,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 <!-- フォルダの中にはカスタム絵文字やフォルダがある -->
 <section v-else v-panel style="border-radius: var(--radius-sm); border-bottom: 0.5px solid var(--divider);">
 	<header class="_acrylic" @click="shown = !shown">
-		<i class="toggle ti-fw" :class="shown ? 'ph-caret-down ph-bold ph-lg' : 'ph-caret-up ph-bold ph-lg'"></i> <slot></slot> (<i class="ph-folder ph-bold ph-lg"></i>:{{ customEmojiTree?.length }} <i class="ph-smiley-sticker ph-bold ph-lg ti-fw"></i>:{{ emojis.length }})
+		<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="ph-smiley-sticker ph-bold ph-lg ti-fw"></i>:{{ emojis.length }})
 	</header>
 	<div v-if="shown" style="padding-left: 9px;">
 		<MkEmojiPickerSection
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 53fca97fc0..297d5f899e 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">
@@ -56,7 +68,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 			</section>
 
 			<section>
-				<header class="_acrylic"><i class="ph-clock ph-bold ph-lg ti-fw"></i> {{ i18n.ts.recentUsed }}</header>
+				<header class="_acrylic"><i class="ti ti-clock ti-fw"></i> {{ i18n.ts.recentUsed }}</header>
 				<div class="body">
 					<button
 						v-for="emoji in recentlyUsedEmojisDef"
@@ -94,10 +106,10 @@ SPDX-License-Identifier: AGPL-3.0-only
 		</div>
 	</div>
 	<div class="tabs">
-		<button class="_button tab" :class="{ active: tab === 'index' }" @click="tab = 'index'"><i class="ph-asterisk ph-bold ph-lg ti-fw"></i></button>
-		<button class="_button tab" :class="{ active: tab === 'custom' }" @click="tab = 'custom'"><i class="ph-smiley ph-bold ph-lg ti-fw"></i></button>
-		<button class="_button tab" :class="{ active: tab === 'unicode' }" @click="tab = 'unicode'"><i class="ph-leaf ph-bold ph-lg ti-fw"></i></button>
-		<button class="_button tab" :class="{ active: tab === 'tags' }" @click="tab = 'tags'"><i class="ph-hash ph-bold ph-lg ti-fw"></i></button>
+		<button class="_button tab" :class="{ active: tab === 'index' }" @click="tab = 'index'"><i class="ti ti-asterisk ti-fw"></i></button>
+		<button class="_button tab" :class="{ active: tab === 'custom' }" @click="tab = 'custom'"><i class="ti ti-mood-happy ti-fw"></i></button>
+		<button class="_button tab" :class="{ active: tab === 'unicode' }" @click="tab = 'unicode'"><i class="ti ti-leaf ti-fw"></i></button>
+		<button class="_button tab" :class="{ active: tab === 'tags' }" @click="tab = 'tags'"><i class="ti ti-hash ti-fw"></i></button>
 	</div>
 </div>
 </template>
@@ -139,6 +151,7 @@ const props = withDefaults(defineProps<{
 
 const emit = defineEmits<{
 	(ev: 'chosen', v: string): void;
+	(ev: 'esc'): void;
 }>();
 
 const searchEl = shallowRef<HTMLInputElement>();
@@ -402,7 +415,9 @@ function chosen(emoji: any, 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(),
+		});
 	}
 
 	const key = getKey(emoji);
@@ -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 {
@@ -700,11 +724,6 @@ defineExpose({
 					border-radius: var(--radius-xs);
 					font-size: 24px;
 
-					&:focus-visible {
-						outline: solid 2px var(--focus);
-						z-index: 1;
-					}
-
 					&:hover {
 						background: rgba(0, 0, 0, 0.05);
 					}
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 c6b3896989..0ec0679393 100644
--- a/packages/frontend/src/components/MkEmojiPickerDialog.vue
+++ b/packages/frontend/src/components/MkEmojiPickerDialog.vue
@@ -9,10 +9,12 @@ SPDX-License-Identifier: AGPL-3.0-only
 	v-slot="{ type, maxHeight }"
 	:zPriority="'middle'"
 	:preferType="defaultStore.state.emojiPickerUseDrawerForMobile === false ? 'popup' : 'auto'"
+	: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/MkFileCaptionEditWindow.vue b/packages/frontend/src/components/MkFileCaptionEditWindow.vue
index 39551e6b3c..8754c72b7b 100644
--- a/packages/frontend/src/components/MkFileCaptionEditWindow.vue
+++ b/packages/frontend/src/components/MkFileCaptionEditWindow.vue
@@ -17,7 +17,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 	<template #header>{{ i18n.ts.describeFile }}</template>
 	<MkSpacer :marginMin="20" :marginMax="28">
 		<MkDriveFileThumbnail :file="file" fit="contain" style="height: 193px; margin-bottom: 16px;"/>
-		<MkTextarea v-model="caption" autofocus :placeholder="i18n.ts.inputNewDescription">
+		<MkTextarea v-model="caption" autofocus :placeholder="i18n.ts.inputNewDescription" @keydown="onKeydown($event)">
 			<template #label>{{ i18n.ts.caption }}</template>
 		</MkTextarea>
 	</MkSpacer>
@@ -46,6 +46,15 @@ const dialog = shallowRef<InstanceType<typeof MkModalWindow>>();
 
 const caption = ref(props.default);
 
+function onKeydown(ev: KeyboardEvent) {
+	if (ev.key === 'Enter' && (ev.ctrlKey || ev.metaKey)) ok();
+
+	if (ev.key === 'Escape') {
+		emit('closed');
+		dialog.value?.close();
+	}
+}
+
 async function ok() {
 	emit('done', caption.value);
 	dialog.value?.close();
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..8a2a438624 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>
 
@@ -39,6 +39,10 @@ const props = defineProps<{
 		color: var(--accent);
 	}
 
+	&:focus-visible {
+		outline-offset: -2px;
+	}
+
 	> article {
 		padding: 16px;
 
@@ -87,6 +91,12 @@ const props = defineProps<{
 		}
 	}
 
+	&:global(.gray) {
+		--c: var(--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 51bcafd1c2..f10d58b38a 100644
--- a/packages/frontend/src/components/MkFoldableSection.vue
+++ b/packages/frontend/src/components/MkFoldableSection.vue
@@ -9,8 +9,8 @@ SPDX-License-Identifier: AGPL-3.0-only
 		<div :class="$style.title"><div><slot name="header"></slot></div></div>
 		<div :class="$style.divider"></div>
 		<button class="_button" :class="$style.button">
-			<template v-if="showBody"><i class="ph-caret-up ph-bold ph-lg"></i></template>
-			<template v-else><i class="ph-caret-down ph-bold ph-lg"></i></template>
+			<template v-if="showBody"><i class="ti ti-chevron-up"></i></template>
+			<template v-else><i class="ti ti-chevron-down"></i></template>
 		</button>
 	</header>
 	<Transition
diff --git a/packages/frontend/src/components/MkFolder.vue b/packages/frontend/src/components/MkFolder.vue
index 64d390f52b..229cd59056 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">
@@ -19,10 +19,10 @@ SPDX-License-Identifier: AGPL-3.0-only
 				</div>
 				<div :class="$style.headerRight">
 					<span :class="$style.headerRightText"><slot name="suffix"></slot></span>
-					<i v-if="opened" class="ph-caret-up ph-bold ph-lg icon"></i>
-					<i v-else class="ph-caret-down ph-bold ph-lg icon"></i>
+					<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">
@@ -147,6 +147,10 @@ onMounted(() => {
 		background: var(--buttonHoverBg);
 	}
 
+	&:focus-within {
+		outline-offset: 2px;
+	}
+
 	&.active {
 		color: var(--accent);
 		background: var(--buttonHoverBg);
@@ -190,6 +194,12 @@ onMounted(() => {
 	padding-right: 12px;
 }
 
+.headerTextMain,
+.headerTextSub {
+	width: fit-content;
+	max-width: 100%;
+}
+
 .headerTextSub {
 	color: var(--fgTransparentWeak);
 	font-size: .85em;
diff --git a/packages/frontend/src/components/MkFollowButton.vue b/packages/frontend/src/components/MkFollowButton.vue
index c0a625c69f..3fdf673eb3 100644
--- a/packages/frontend/src/components/MkFollowButton.vue
+++ b/packages/frontend/src/components/MkFollowButton.vue
@@ -12,20 +12,20 @@ SPDX-License-Identifier: AGPL-3.0-only
 >
 	<template v-if="!wait">
 		<template v-if="hasPendingFollowRequestFromYou && user.isLocked">
-			<span v-if="full" :class="$style.text">{{ i18n.ts.followRequestPending }}</span><i class="ph-hourglass ph-bold ph-lg"></i>
+			<span v-if="full" :class="$style.text">{{ i18n.ts.followRequestPending }}</span><i class="ti ti-hourglass-empty"></i>
 		</template>
 		<template v-else-if="hasPendingFollowRequestFromYou && !user.isLocked">
 			<!-- つまりリモートフォローの場合。 -->
 			<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="ph-minus ph-bold ph-lg"></i>
+			<span v-if="full" :class="$style.text">{{ i18n.ts.unfollow }}</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="ph-plus ph-bold ph-lg"></i>
+			<span v-if="full" :class="$style.text">{{ i18n.ts.followRequest }}</span><i class="ti ti-plus"></i>
 		</template>
 		<template v-else-if="!isFollowing && !user.isLocked">
-			<span v-if="full" :class="$style.text">{{ i18n.ts.follow }}</span><i class="ph-plus ph-bold ph-lg"></i>
+			<span v-if="full" :class="$style.text">{{ i18n.ts.follow }}</span><i class="ti ti-plus"></i>
 		</template>
 	</template>
 	<template v-else>
@@ -42,6 +42,8 @@ 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 { host } from '@/config.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(undefined, { 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) {
@@ -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: var(--radius-xl);
-		}
+		outline-offset: 2px;
 	}
 
 	&:hover {
diff --git a/packages/frontend/src/components/MkGalleryPostPreview.vue b/packages/frontend/src/components/MkGalleryPostPreview.vue
index 47cccd9b7c..2bb5b8762a 100644
--- a/packages/frontend/src/components/MkGalleryPostPreview.vue
+++ b/packages/frontend/src/components/MkGalleryPostPreview.vue
@@ -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 d1809d1073..54c7585f18 100644
--- a/packages/frontend/src/components/MkGoogle.vue
+++ b/packages/frontend/src/components/MkGoogle.vue
@@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 <template>
 <div :class="$style.root">
 	<input v-model="query" :class="$style.input" type="search" :placeholder="q">
-	<button :class="$style.button" @click="search"><i class="ph-magnifying-glass ph-bold ph-lg"></i> {{ i18n.ts.searchByGoogle }}</button>
+	<button :class="$style.button" @click="search"><i class="ti ti-search"></i> {{ i18n.ts.searchByGoogle }}</button>
 </div>
 </template>
 
diff --git a/packages/frontend/src/components/MkImgWithBlurhash.vue b/packages/frontend/src/components/MkImgWithBlurhash.vue
index 4e3fafe845..8d301f16bd 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>
@@ -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 9a5874b5c0..46c2db4b1f 100644
--- a/packages/frontend/src/components/MkInfo.vue
+++ b/packages/frontend/src/components/MkInfo.vue
@@ -5,10 +5,10 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <template>
 <div :class="[$style.root, { [$style.warn]: warn }]">
-	<i v-if="warn" class="ph-warning ph-bold ph-lg" :class="$style.i"></i>
-	<i v-else class="ph-info ph-bold ph-lg" :class="$style.i"></i>
+	<i v-if="warn" class="ti ti-alert-triangle" :class="$style.i"></i>
+	<i v-else class="ti ti-info-circle" :class="$style.i"></i>
 	<div><slot></slot></div>
-	<button v-if="closable" :class="$style.button" class="_button" @click="close()"><i class="ph-x ph-bold ph-lg"></i></button>
+	<button v-if="closable" :class="$style.button" class="_button" @click="close()"><i class="ti ti-x"></i></button>
 </div>
 </template>
 
diff --git a/packages/frontend/src/components/MkInput.vue b/packages/frontend/src/components/MkInput.vue
index 201b409a05..06389b4013 100644
--- a/packages/frontend/src/components/MkInput.vue
+++ b/packages/frontend/src/components/MkInput.vue
@@ -39,7 +39,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 	</div>
 	<div :class="$style.caption"><slot name="caption"></slot></div>
 
-	<MkButton v-if="manualSave && changed" primary :class="$style.save" @click="updated"><i class="ph-check ph-bold ph-lg"></i> {{ i18n.ts.save }}</MkButton>
+	<MkButton v-if="manualSave && changed" primary :class="$style.save" @click="updated"><i class="ti ti-check"></i> {{ i18n.ts.save }}</MkButton>
 </div>
 </template>
 
@@ -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);
 	}
 };
 
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 feb62415aa..10b390e7f9 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 {
diff --git a/packages/frontend/src/components/MkInviteCode.vue b/packages/frontend/src/components/MkInviteCode.vue
index b095df20e5..de51a98789 100644
--- a/packages/frontend/src/components/MkInviteCode.vue
+++ b/packages/frontend/src/components/MkInviteCode.vue
@@ -50,8 +50,8 @@ SPDX-License-Identifier: AGPL-3.0-only
 			</div>
 		</div>
 		<div :class="$style.buttons">
-			<MkButton v-if="!invite.used && !isExpired" primary rounded @click="copyInviteCode()"><i class="ph-copy ph-bold ph-lg"></i> {{ i18n.ts.copy }}</MkButton>
-			<MkButton v-if="!invite.used || moderator" danger rounded @click="deleteCode()"><i class="ph-trash ph-bold ph-lg"></i> {{ i18n.ts.delete }}</MkButton>
+			<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>
@@ -62,7 +62,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';
 
diff --git a/packages/frontend/src/components/MkKeyValue.vue b/packages/frontend/src/components/MkKeyValue.vue
index 2175c0e888..50c9e16e5e 100644
--- a/packages/frontend/src/components/MkKeyValue.vue
+++ b/packages/frontend/src/components/MkKeyValue.vue
@@ -10,14 +10,14 @@ SPDX-License-Identifier: AGPL-3.0-only
 	</div>
 	<div :class="$style.value">
 		<slot name="value"></slot>
-		<button v-if="copy" v-tooltip="i18n.ts.copy" class="_textButton" style="margin-left: 0.5em;" @click="copy_"><i class="ph-copy ph-bold ph-lg"></i></button>
+		<button v-if="copy" v-tooltip="i18n.ts.copy" class="_textButton" style="margin-left: 0.5em;" @click="copy_"><i class="ti ti-copy"></i></button>
 	</div>
 </div>
 </template>
 
 <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 e232b4d66f..e0880ec3e7 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">
diff --git a/packages/frontend/src/components/MkLink.vue b/packages/frontend/src/components/MkLink.vue
index 49cbacd1e8..07cf9e0c37 100644
--- a/packages/frontend/src/components/MkLink.vue
+++ b/packages/frontend/src/components/MkLink.vue
@@ -11,7 +11,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 	@click.stop
 >
 	<slot></slot>
-	<i v-if="target === '_blank'" class="ph-arrow-square-out ph-bold ph-lg" :class="$style.icon"></i>
+	<i v-if="target === '_blank'" class="ti ti-external-link" :class="$style.icon"></i>
 </component>
 </template>
 
@@ -38,11 +38,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 41425facc3..93affd930f 100644
--- a/packages/frontend/src/components/MkMediaAudio.vue
+++ b/packages/frontend/src/components/MkMediaAudio.vue
@@ -15,10 +15,10 @@ 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="ph-eye-slash ph-bold ph-lg"></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="ph-music-notes ph-bold ph-lg"></i> {{ defaultStore.state.dataSaver.media && audio.size ? bytes(audio.size) : i18n.ts.audio }}</b>
+			<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>
 			<span style="display: block;">{{ i18n.ts.clickToShow }}</span>
 		</div>
 	</button>
@@ -39,28 +39,42 @@ 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">
-				<i v-if="isPlaying" class="ph-pause ph-bold ph-lg"></i>
-				<i v-else class="ph-play ph-bold ph-lg"></i>
+			<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]">
 			<a class="_button" :class="$style.controlButton" :href="audio.url" :download="audio.name" target="_blank">
 				<i class="ph-download ph-bold ph-lg"></i>
 			</a>
-			<button class="_button" :class="$style.controlButton" @click="showMenu">
-				<i class="ph-gear ph-bold ph-lg"></i>
+			<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">
-				<i v-if="volume === 0" class="ph-speaker-x ph-bold ph-lg"></i>
-				<i v-else class="ph-speaker-high ph-bold ph-lg"></i>
+			<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>
 			<MkMediaRange
 				v-model="volume"
@@ -83,6 +97,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';
@@ -93,32 +108,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() {
@@ -129,9 +156,21 @@ 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);
 
@@ -143,13 +182,13 @@ function showMenu(ev: MouseEvent) {
 		{
 			type: 'switch',
 			text: i18n.ts._mediaControls.loop,
-			icon: 'ph ph-repeat',
+			icon: 'ti ti-repeat',
 			ref: loop,
 		},
 		{
 			type: 'radio',
 			text: i18n.ts._mediaControls.playbackRate,
-			icon: 'ph ph-gauge',
+			icon: 'ti ti-clock-play',
 			ref: speed,
 			options: {
 				'0.25x': 0.25,
@@ -166,7 +205,7 @@ function showMenu(ev: MouseEvent) {
 		},
 		{
 			text: i18n.ts.hide,
-			icon: 'ph-eye-closed ph-bold ph-lg',
+			icon: 'ti ti-eye-off',
 			action: () => {
 				hide.value = true;
 			},
@@ -176,7 +215,7 @@ function showMenu(ev: MouseEvent) {
 	if (iAmModerator) {
 		menu.push({
 			text: props.audio.isSensitive ? i18n.ts.unmarkAsSensitive : i18n.ts.markAsSensitive,
-			icon: props.audio.isSensitive ? 'ph-eye ph-bold ph-lg' : 'ph-eye-slash ph-bold ph-lg',
+			icon: props.audio.isSensitive ? 'ti ti-eye' : 'ti ti-eye-exclamation',
 			danger: true,
 			action: () => toggleSensitive(props.audio),
 		});
@@ -188,7 +227,7 @@ function showMenu(ev: MouseEvent) {
 		}, {
 			type: 'link' as const,
 			text: i18n.ts._fileViewer.title,
-			icon: 'ph ph-info',
+			icon: 'ti ti-info-circle',
 			to: `/my/drive/file/${props.audio.id}`,
 		});
 	}
@@ -361,7 +400,7 @@ onDeactivated(() => {
 	border-radius: var(--radius);
 	overflow: clip;
 
-	&:focus {
+	&:focus-visible {
 		outline: none;
 	}
 }
@@ -427,6 +466,10 @@ onDeactivated(() => {
 			color: var(--accent);
 			background-color: var(--accentedBg);
 		}
+
+		&:focus-visible {
+			outline: none;
+		}
 	}
 }
 
diff --git a/packages/frontend/src/components/MkMediaBanner.vue b/packages/frontend/src/components/MkMediaBanner.vue
index 605c1a4c80..ed8d43273f 100644
--- a/packages/frontend/src/components/MkMediaBanner.vue
+++ b/packages/frontend/src/components/MkMediaBanner.vue
@@ -5,8 +5,8 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <template>
 <div :class="$style.root">
-	<div v-if="media.isSensitive && hide" :class="$style.sensitive" @click="hide = false">
-		<span style="font-size: 1.6em;"><i class="ph-warning ph-bold ph-lg"></i></span>
+	<div v-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>
 	</div>
@@ -17,31 +17,37 @@ SPDX-License-Identifier: AGPL-3.0-only
 		:title="media.name"
 		:download="media.name"
 	>
-		<span style="font-size: 1.6em;"><i class="ph-download ph-bold ph-lg"></i></span>
+		<span style="font-size: 1.6em;"><i class="ti ti-download"></i></span>
 		<b>{{ media.name }}</b>
 	</a>
 </div>
 </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>
diff --git a/packages/frontend/src/components/MkMediaImage.vue b/packages/frontend/src/components/MkMediaImage.vue
index f1fb4f5b44..cac419d42b 100644
--- a/packages/frontend/src/components/MkMediaImage.vue
+++ b/packages/frontend/src/components/MkMediaImage.vue
@@ -32,8 +32,8 @@ SPDX-License-Identifier: AGPL-3.0-only
 	<template v-if="hide">
 		<div :class="$style.hiddenText">
 			<div :class="$style.hiddenTextWrapper">
-				<b v-if="image.isSensitive" style="display: block;"><i class="ph-eye-closed ph-bold ph-lg"></i> {{ i18n.ts.sensitive }}{{ defaultStore.state.dataSaver.media ? ` (${i18n.ts.image}${image.size ? ' ' + bytes(image.size) : ''})` : '' }}</b>
-				<b v-else style="display: block;"><i class="ph-image-square ph-bold ph-lg"></i> {{ defaultStore.state.dataSaver.media && image.size ? bytes(image.size) : i18n.ts.image }}</b>
+				<b v-if="image.isSensitive" style="display: block;"><i class="ti ti-eye-exclamation"></i> {{ i18n.ts.sensitive }}{{ defaultStore.state.dataSaver.media ? ` (${i18n.ts.image}${image.size ? ' ' + bytes(image.size) : ''})` : '' }}</b>
+				<b v-else style="display: block;"><i class="ti ti-photo"></i> {{ defaultStore.state.dataSaver.media && image.size ? bytes(image.size) : i18n.ts.image }}</b>
 				<span v-if="controls" style="display: block;">{{ i18n.ts.clickToShow }}</span>
 			</div>
 		</div>
@@ -42,11 +42,11 @@ 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="ph-eye-closed ph-bold ph-lg"></i></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.comment" :class="$style.indicator" title="Image lacks descriptive text"><i class="ph-pencil-simple ph-bold ph-lg-off"></i></div>
 		</div>
-		<button :class="$style.menu" class="_button" @click.stop="showMenu"><i class="ph-dots-three ph-bold ph-lg" style="vertical-align: middle;"></i></button>
-		<i class="ph-eye-slash ph-bold ph-lg" :class="$style.hide" @click.stop="hide = true"></i>
+		<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>
 	</template>
 </div>
 </template>
@@ -84,11 +84,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;
 	}
 }
@@ -104,13 +114,13 @@ watch(() => props.image, () => {
 function showMenu(ev: MouseEvent) {
 	os.popupMenu([{
 		text: i18n.ts.hide,
-		icon: 'ph-eye-slash ph-bold ph-lg',
+		icon: 'ti ti-eye-off',
 		action: () => {
 			hide.value = true;
 		},
 	}, ...(iAmModerator ? [{
 		text: i18n.ts.markAsSensitive,
-		icon: 'ph-eye-closed ph-bold ph-lg',
+		icon: 'ti ti-eye-exclamation',
 		danger: true,
 		action: () => {
 			os.apiWithDialog('drive/files/update', { fileId: props.image.id, isSensitive: true });
@@ -120,7 +130,7 @@ function showMenu(ev: MouseEvent) {
 	}, {
 		type: 'link' as const,
 		text: i18n.ts._fileViewer.title,
-		icon: 'ph ph-info',
+		icon: 'ti ti-info-circle',
 		to: `/my/drive/file/${props.image.id}`,
 	}] : [])], ev.currentTarget ?? ev.target);
 }
diff --git a/packages/frontend/src/components/MkMediaList.vue b/packages/frontend/src/components/MkMediaList.vue
index 3bf44aea8e..4bc2b9fba7 100644
--- a/packages/frontend/src/components/MkMediaList.vue
+++ b/packages/frontend/src/components/MkMediaList.vue
@@ -37,10 +37,11 @@ import 'photoswipe/style.css';
 import XBanner from '@/components/MkMediaBanner.vue';
 import XImage from '@/components/MkMediaImage.vue';
 import XVideo from '@/components/MkMediaVideo.vue';
-import XModPlayer from '@/components/MkModPlayer.vue';
+import XModPlayer from '@/components/SkModPlayer.vue';
 import * as os from '@/os.js';
 import { FILE_TYPE_BROWSERSAFE, FILE_EXT_TRACKER_MODULES, FILE_TYPE_TRACKER_MODULES } from '@/const.js';
 import { defaultStore } from '@/store.js';
+import { focusParent } from '@/scripts/focus.js';
 
 const props = defineProps<{
 	mediaList: Misskey.entities.DriveFile[];
@@ -51,7 +52,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) {
@@ -62,7 +65,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 = '';
@@ -139,18 +142,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);
@@ -162,50 +164,60 @@ onMounted(() => {
 		itemData.alt = file.comment ?? undefined;
 		itemData.comment = file.comment;
 		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', (a) => {
-					if (pwsp.currSlide?.data.comment) {
+				pswp.on('change', () => {
+					if (pswp.currSlide?.data.comment) {
 						textBox.style.display = '';
 					} else {
 						textBox.style.display = 'none';
 					}
 
-					textBox.textContent = pwsp.currSlide?.data.comment;
+					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 => {
@@ -214,6 +226,16 @@ const previewable = (file: Misskey.entities.DriveFile): boolean => {
 	if (isModule(file)) return true;
 	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>
@@ -317,7 +339,7 @@ const previewable = (file: Misskey.entities.DriveFile): boolean => {
 	backdrop-filter: var(--modalBgFilter);
 }
 
-.pwsp__alt-text-container {
+.pswp__alt-text-container {
 	display: flex;
 	flex-direction: row;
 	align-items: center;
@@ -331,7 +353,7 @@ const previewable = (file: Misskey.entities.DriveFile): boolean => {
 	max-width: 800px;
 }
 
-.pwsp__alt-text {
+.pswp__alt-text {
 	color: var(--fg);
 	margin: 0 auto;
 	text-align: center;
diff --git a/packages/frontend/src/components/MkMediaVideo.vue b/packages/frontend/src/components/MkMediaVideo.vue
index dbf76bc33d..1c3c9a312b 100644
--- a/packages/frontend/src/components/MkMediaVideo.vue
+++ b/packages/frontend/src/components/MkMediaVideo.vue
@@ -18,10 +18,10 @@ 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="ph-warning ph-bold ph-lg"></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="ph-film-strip ph-bold ph-lg"></i> {{ defaultStore.state.dataSaver.media && video.size ? bytes(video.size) : i18n.ts.video }}</b>
+			<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>
 			<span style="display: block;">{{ i18n.ts.clickToShow }}</span>
 		</div>
 	</button>
@@ -39,10 +39,10 @@ SPDX-License-Identifier: AGPL-3.0-only
 		>
 			<source :src="video.url">
 		</video>
-		<i class="ph-eye-closed ph-bold ph-lg" :class="$style.hide" @click="hide = true"></i>
+		<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="ph-warning ph-bold ph-lg"></i></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>
 	</div>
 
@@ -60,20 +60,20 @@ SPDX-License-Identifier: AGPL-3.0-only
 		>
 			<source :src="video.url">
 		</video>
-		<button v-if="isReady && !isPlaying" class="_button" :class="$style.videoOverlayPlayButton" @click="togglePlayPause"><i class="ph-play ph-bold ph-lg"></i></button>
+		<button v-if="isReady && !isPlaying" class="_button" :class="$style.videoOverlayPlayButton" @click="togglePlayPause"><i class="ti ti-player-play-filled"></i></button>
 		<div v-else-if="!isActuallyPlaying" :class="$style.videoLoading">
 			<MkLoading/>
 		</div>
-		<i class="ph-eye-closed ph-bold ph-lg" :class="$style.hide" @click="hide = true"></i>
+		<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="ph-warning ph-bold ph-lg"></i></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>
 		<div :class="$style.videoControls" @click.self="togglePlayPause">
 			<div :class="[$style.controlsChild, $style.controlsLeft]">
 				<button class="_button" :class="$style.controlButton" @click="togglePlayPause">
-					<i v-if="isPlaying" class="ph-pause ph-bold ph-lg"></i>
-					<i v-else class="ph-play ph-bold ph-lg"></i>
+					<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]">
@@ -81,18 +81,18 @@ SPDX-License-Identifier: AGPL-3.0-only
 					<i class="ph-download ph-bold ph-lg"></i>
 				</a>
 				<button class="_button" :class="$style.controlButton" @click="showMenu">
-					<i class="ph-gear ph-bold ph-lg"></i>
+					<i class="ti ti-settings"></i>
 				</button>
 				<button class="_button" :class="$style.controlButton" @click="toggleFullscreen">
-					<i v-if="isFullscreen" class="ph-arrows-in ph-bold ph-lg"></i>
-					<i v-else class="ph-arrows-out ph-bold ph-lg"></i>
+					<i v-if="isFullscreen" class="ti ti-arrows-minimize"></i>
+					<i v-else class="ti ti-arrows-maximize"></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">
-					<i v-if="volume === 0" class="ph-speaker-x ph-bold ph-lg"></i>
-					<i v-else class="ph-speaker-high ph-bold ph-lg"></i>
+					<i v-if="volume === 0" class="ti ti-volume-3"></i>
+					<i v-else class="ti ti-volume"></i>
 				</button>
 				<MkMediaRange
 					v-model="volume"
@@ -115,6 +115,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';
@@ -130,32 +131,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() {
@@ -163,9 +176,21 @@ 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);
 
@@ -177,13 +202,13 @@ function showMenu(ev: MouseEvent) {
 		{
 			type: 'switch',
 			text: i18n.ts._mediaControls.loop,
-			icon: 'ph ph-repeat',
+			icon: 'ti ti-repeat',
 			ref: loop,
 		},
 		{
 			type: 'radio',
 			text: i18n.ts._mediaControls.playbackRate,
-			icon: 'ph ph-gauge',
+			icon: 'ti ti-clock-play',
 			ref: speed,
 			options: {
 				'0.25x': 0.25,
@@ -197,7 +222,7 @@ function showMenu(ev: MouseEvent) {
 		},
 		...(document.pictureInPictureEnabled ? [{
 			text: i18n.ts._mediaControls.pip,
-			icon: 'ph ph-picture-in-picture',
+			icon: 'ti ti-picture-in-picture',
 			action: togglePictureInPicture,
 		}] : []),
 		{
@@ -205,7 +230,7 @@ function showMenu(ev: MouseEvent) {
 		},
 		{
 			text: i18n.ts.hide,
-			icon: 'ph-eye-closed ph-bold ph-lg',
+			icon: 'ti ti-eye-off',
 			action: () => {
 				hide.value = true;
 			},
@@ -215,7 +240,7 @@ function showMenu(ev: MouseEvent) {
 	if (iAmModerator) {
 		menu.push({
 			text: props.video.isSensitive ? i18n.ts.unmarkAsSensitive : i18n.ts.markAsSensitive,
-			icon: props.video.isSensitive ? 'ph-eye ph-bold ph-lg' : 'ph-eye-slash ph-bold ph-lg',
+			icon: props.video.isSensitive ? 'ti ti-eye' : 'ti ti-eye-exclamation',
 			danger: true,
 			action: () => toggleSensitive(props.video),
 		});
@@ -227,7 +252,7 @@ function showMenu(ev: MouseEvent) {
 		}, {
 			type: 'link' as const,
 			text: i18n.ts._fileViewer.title,
-			icon: 'ph ph-info',
+			icon: 'ti ti-info-circle',
 			to: `/my/drive/file/${props.video.id}`,
 		});
 	}
@@ -471,7 +496,7 @@ onDeactivated(() => {
 	position: relative;
 	overflow: clip;
 
-	&:focus {
+	&:focus-visible {
 		outline: none;
 	}
 }
@@ -578,6 +603,10 @@ onDeactivated(() => {
 	border-radius: 99rem;
 
 	font-size: 1.1rem;
+
+	&:focus-visible {
+		outline: none;
+	}
 }
 
 .videoLoading {
@@ -641,6 +670,10 @@ onDeactivated(() => {
 		&:hover {
 			background-color: var(--accent);
 		}
+
+		&:focus-visible {
+			outline: none;
+		}
 	}
 }
 
diff --git a/packages/frontend/src/components/MkMenu.child.vue b/packages/frontend/src/components/MkMenu.child.vue
index dfb6d34618..235790556c 100644
--- a/packages/frontend/src/components/MkMenu.child.vue
+++ b/packages/frontend/src/components/MkMenu.child.vue
@@ -10,7 +10,7 @@ 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';
 
@@ -19,7 +19,6 @@ const props = defineProps<{
 	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 6ced2fecc2..0537f4f988 100644
--- a/packages/frontend/src/components/MkMenu.vue
+++ b/packages/frontend/src/components/MkMenu.vue
@@ -4,23 +4,42 @@ SPDX-License-Identifier: AGPL-3.0-only
 -->
 
 <template>
-<div role="menu">
+<div role="menu" @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.root]: true,
+			[$style.center]: align === 'center',
+			[$style.asDrawer]: asDrawer,
+		}"
+		: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">
@@ -28,20 +47,49 @@ SPDX-License-Identifier: AGPL-3.0-only
 					<span v-if="item.indicate" :class="$style.indicator"><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>
 				</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>
 				</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,29 +97,61 @@ 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="ph-caret-right ph-bold ph-lg"></i></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="ph-caret-right ph-bold ph-lg ti-fw"></i></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">
@@ -80,24 +160,26 @@ SPDX-License-Identifier: AGPL-3.0-only
 				</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 +189,6 @@ const XChild = defineAsyncComponent(() => import('./MkMenu.child.vue'));
 
 const props = defineProps<{
 	items: MenuItem[];
-	viaKeyboard?: boolean;
 	asDrawer?: boolean;
 	align?: 'center' | string;
 	width?: number;
@@ -119,17 +200,28 @@ const emit = defineEmits<{
 	(ev: 'hide'): void;
 }>();
 
-const itemsEl = shallowRef<HTMLDivElement>();
+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 +259,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 +286,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 +296,7 @@ async function showRadioOptions(item: MenuRadio, ev: MouseEvent) {
 	}
 }
 
-async function showChildren(item: MenuParent, ev: MouseEvent) {
+async function showChildren(item: MenuParent, ev: Event) {
 	const children: MenuItem[] = await (async () => {
 		if (childrenCache.has(item)) {
 			return childrenCache.get(item)!;
@@ -227,7 +313,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 +332,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,25 +344,75 @@ 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>
 
@@ -293,6 +425,10 @@ onBeforeUnmount(() => {
 	overflow: auto;
 	overscroll-behavior: contain;
 
+	&:focus-visible {
+		outline: none;
+	}
+
 	&.center {
 		> .item {
 			text-align: center;
@@ -310,7 +446,7 @@ onBeforeUnmount(() => {
 			font-size: 1em;
 			padding: 12px 24px;
 
-			&:before {
+			&::before {
 				width: calc(100% - 24px);
 				border-radius: var(--radius);
 			}
@@ -340,8 +476,10 @@ onBeforeUnmount(() => {
 	text-align: left;
 	overflow: hidden;
 	text-overflow: ellipsis;
+	text-decoration: none !important;
+	color: var(--menuFg, var(--fg));
 
-	&:before {
+	&::before {
 		content: "";
 		display: block;
 		position: absolute;
@@ -355,56 +493,56 @@ onBeforeUnmount(() => {
 		border-radius: var(--radius-sm);
 	}
 
-	&:not(:disabled):hover {
-		color: var(--accent);
-		text-decoration: none;
+	&:focus-visible {
+		outline: none;
 
-		&:before {
-			background: var(--accentedBg);
+		&:not(:hover):not(:active)::before {
+			outline: var(--focus) solid 2px;
+			outline-offset: -2px;
 		}
 	}
 
+	&:not(:disabled) {
+		&:hover,
+		&:focus-visible:active,
+		&:focus-visible.active {
+			color: var(--menuHoverFg, var(--accent));
+
+			&::before {
+				background-color: var(--menuHoverBg, var(--accentedBg));
+			}
+		}
+
+		&:not(:focus-visible):active,
+		&:not(:focus-visible).active {
+			color: var(--menuActiveFg, var(--fgOnAccent));
+
+			&::before {
+				background-color: var(--menuActiveBg, var(--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(--accent);
+		--menuActiveBg: var(--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(--accent);
+		--menuActiveBg: var(--accentedBg);
 	}
 
 	&.label {
@@ -422,22 +560,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 +578,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 +589,6 @@ onBeforeUnmount(() => {
 	text-overflow: ellipsis;
 }
 
-.switchInput {
-	position: absolute;
-	width: 0;
-	height: 0;
-	opacity: 0;
-	margin: 0;
-}
-
 .icon {
 	margin-right: 8px;
 	line-height: 1;
@@ -515,12 +617,12 @@ onBeforeUnmount(() => {
 	border-top: solid 0.5px var(--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);
diff --git a/packages/frontend/src/components/MkModal.vue b/packages/frontend/src/components/MkModal.vue
index 9e69ab2207..f8032f9b43 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,6 +100,7 @@ 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);
@@ -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 fc634176c7..c3c7812036 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="ph-x ph-bold ph-lg"></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="ph-x ph-bold ph-lg"></i></button>
-			<button v-if="withOkButton" :class="$style.headerButton" class="_button" :disabled="okButtonDisabled" @click="$emit('ok')"><i class="ph-check ph-bold ph-lg"></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>
@@ -27,11 +27,13 @@ import MkModal from './MkModal.vue';
 
 const props = withDefaults(defineProps<{
 	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;
diff --git a/packages/frontend/src/components/MkNote.vue b/packages/frontend/src/components/MkNote.vue
index fe85307a52..e2f0a4e492 100644
--- a/packages/frontend/src/components/MkNote.vue
+++ b/packages/frontend/src/components/MkNote.vue
@@ -10,23 +10,23 @@ SPDX-License-Identifier: AGPL-3.0-only
 	ref="rootEl"
 	v-hotkey="keymap"
 	:class="[$style.root, { [$style.showActionsOnlyHover]: defaultStore.state.showNoteActionsOnlyHover }]"
-	:tabindex="!isDeleted ? '-1' : undefined"
+	:tabindex="isDeleted ? '-1' : '0'"
 >
 	<div v-if="appearNote.reply && inReplyToCollapsed" :class="$style.collapsedInReplyTo">
 		<MkAvatar :class="$style.collapsedInReplyToAvatar" :user="appearNote.reply.user" link preview/>
-		<MkA v-user-preview="note.user.id" :class="$style.name" :to="userPage(note.user)">
+		<MkA v-user-preview="appearNote.reply.userId" :class="$style.name" :to="userPage(appearNote.reply.user)">
 			<MkAcct :user="appearNote.reply.user"/>
 		</MkA>:
 		<Mfm :text="getNoteSummary(appearNote.reply)" :plain="true" :nowrap="true" :author="appearNote.reply.user" :nyaize="'respect'" :class="$style.collapsedInReplyToText" @click="inReplyToCollapsed = false"/>
 	</div>
 	<MkNoteSub v-if="appearNote.reply && !renoteCollapsed && !inReplyToCollapsed" :note="appearNote.reply" :class="$style.replyTo"/>
-	<div v-if="pinned" :class="$style.tip"><i class="ph-push-pin ph-bold ph-lg"></i> {{ i18n.ts.pinnedNote }}</div>
-	<!--<div v-if="appearNote._prId_" class="tip"><i class="ph-megaphone ph-bold ph-lg"></i> {{ i18n.ts.promotion }}<button class="_textButton hide" @click="readPromo()">{{ i18n.ts.hideThisNote }} <i class="ph-x ph-bold ph-lg"></i></button></div>-->
-	<!--<div v-if="appearNote._featuredId_" class="tip"><i class="ph-lightning ph-bold ph-lg"></i> {{ i18n.ts.featured }}</div>-->
+	<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>
 		<MkAvatar :class="$style.renoteAvatar" :user="note.user" link preview/>
-		<i class="ph-rocket-launch ph-bold ph-lg" style="margin-right: 4px;"></i>
+		<i class="ti ti-repeat" style="margin-right: 4px;"></i>
 		<I18n :src="i18n.ts.renotedBy" tag="span" :class="$style.renoteText">
 			<template #user>
 				<MkA v-user-preview="note.userId" :class="$style.renoteUserName" :to="userPage(note.user)">
@@ -35,17 +35,17 @@ SPDX-License-Identifier: AGPL-3.0-only
 			</template>
 		</I18n>
 		<div :class="$style.renoteInfo">
-			<button ref="renoteTime" :class="$style.renoteTime" class="_button" @click="showRenoteMenu()">
-				<i class="ph-dots-three ph-bold ph-lg" :class="$style.renoteMenu"></i>
+			<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>
 			<span v-if="note.visibility !== 'public'" style="margin-left: 0.5em;" :title="i18n.ts._visibility[note.visibility]">
-				<i v-if="note.visibility === 'home'" class="ph-house ph-bold ph-lg"></i>
-				<i v-else-if="note.visibility === 'followers'" class="ph-lock ph-bold ph-lg"></i>
-				<i v-else-if="note.visibility === 'specified'" ref="specified" class="ph-envelope ph-bold ph-lg"></i>
+				<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="ph-rocket ph-bold ph-lg"></i></span>
-			<span v-if="note.channel" style="margin-left: 0.5em;" :title="note.channel.name"><i class="ph-television ph-bold ph-lg"></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>
 			<span v-if="note.updatedAt" ref="menuVersionsButton" style="margin-left: 0.5em;" title="Edited" @mousedown="menuVersions()"><i class="ph-pencil-simple ph-bold ph-lg"></i></span>
 		</div>
 	</div>
@@ -92,7 +92,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 						<MkButton v-else-if="!defaultStore.state.animatedMfm && allowAnim && animated" :class="$style.playMFMButton" :small="true" @click="animatedMFM()" @click.stop><i class="ph-stop ph-bold ph-lg "></i> {{ i18n.ts._animatedMFM.stop }}</MkButton>
 					</div>
 					<div v-if="appearNote.files && appearNote.files.length > 0">
-						<MkMediaList :mediaList="appearNote.files" @click.stop/>
+						<MkMediaList ref="galleryEl" :mediaList="appearNote.files" @click.stop/>
 					</div>
 					<MkPoll v-if="appearNote.poll" :noteId="appearNote.id" :poll="appearNote.poll" :class="$style.poll" @click.stop/>
 					<div v-if="isEnabledUrlPreview">
@@ -106,7 +106,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 						<span :class="$style.showLessLabel">{{ i18n.ts.showLess }}</span>
 					</button>
 				</div>
-				<MkA v-if="appearNote.channel && !inChannel" :class="$style.channel" :to="`/channels/${appearNote.channel.id}`"><i class="ph-television ph-bold ph-lg"></i> {{ appearNote.channel.name }}</MkA>
+				<MkA v-if="appearNote.channel && !inChannel" :class="$style.channel" :to="`/channels/${appearNote.channel.id}`"><i class="ti ti-device-tv"></i> {{ appearNote.channel.name }}</MkA>
 				</bdi>
 			</div>
 			<MkReactionsViewer v-if="appearNote.reactionAcceptance !== 'likeOnly'" :note="appearNote" :maxNumber="16" @click.stop @mockUpdateMyReaction="emitUpdReaction">
@@ -116,7 +116,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 			</MkReactionsViewer>
 			<footer :class="$style.footer">
 				<button :class="$style.footerButton" class="_button" @click.stop @click="reply()">
-					<i class="ph-arrow-u-up-left ph-bold ph-lg"></i>
+					<i class="ti ti-arrow-back-up"></i>
 					<p v-if="appearNote.repliesCount > 0" :class="$style.footerButtonCount">{{ number(appearNote.repliesCount) }}</p>
 				</button>
 				<button
@@ -126,13 +126,13 @@ SPDX-License-Identifier: AGPL-3.0-only
 					class="_button"
 					:style="renoted ? 'color: var(--accent) !important;' : ''"
 					@click.stop
-					@mousedown="renoted ? undoRenote(appearNote) : boostVisibility()"
+					@mousedown.prevent="renoted ? undoRenote(appearNote) : boostVisibility()"
 				>
-					<i class="ph-rocket-launch ph-bold ph-lg"></i>
+					<i class="ti ti-repeat"></i>
 					<p v-if="appearNote.renoteCount > 0" :class="$style.footerButtonCount">{{ number(appearNote.renoteCount) }}</p>
 				</button>
 				<button v-else :class="$style.footerButton" class="_button" disabled>
-					<i class="ph-prohibit ph-bold ph-lg"></i>
+					<i class="ti ti-ban"></i>
 				</button>
 				<button
 					v-if="canRenote && !props.mock"
@@ -148,17 +148,17 @@ SPDX-License-Identifier: AGPL-3.0-only
 					<i class="ph-heart ph-bold ph-lg"></i>
 				</button>
 				<button ref="reactButton" :class="$style.footerButton" class="_button" @click="toggleReact()" @click.stop>
-					<i v-if="appearNote.reactionAcceptance === 'likeOnly' && appearNote.myReaction != null" class="ph-heart ph-bold ph-lg" style="color: var(--eventReactionHeart);"></i>
-					<i v-else-if="appearNote.myReaction != null" class="ph-minus ph-bold ph-lg" style="color: var(--accent);"></i>
-					<i v-else-if="appearNote.reactionAcceptance === 'likeOnly'" class="ph-heart ph-bold ph-lg"></i>
+					<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-else-if="appearNote.reactionAcceptance === 'likeOnly'" class="ti ti-heart"></i>
 					<i v-else class="ph-smiley ph-bold ph-lg"></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()">
-					<i class="ph-paperclip ph-bold ph-lg"></i>
+				<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()">
-					<i class="ph-dots-three ph-bold ph-lg"></i>
+				<button ref="menuButton" :class="$style.footerButton" class="_button" @mousedown.prevent="showMenu()">
+					<i class="ti ti-dots"></i>
 				</button>
 			</footer>
 		</div>
@@ -204,8 +204,7 @@ import MkUsersTooltip from '@/components/MkUsersTooltip.vue';
 import MkUrlPreview from '@/components/MkUrlPreview.vue';
 import MkInstanceTicker from '@/components/MkInstanceTicker.vue';
 import MkButton from '@/components/MkButton.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';
@@ -231,7 +230,11 @@ import { showMovedDialog } from '@/scripts/show-moved-dialog.js';
 import { shouldCollapsed } from '@/scripts/collapsed.js';
 import { useRouter } from '@/router/supplier.js';
 import { boostMenuItems, type Visibility } from '@/scripts/boost-quote.js';
+import { host } from '@/config.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;
@@ -283,14 +286,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>();
@@ -301,8 +297,8 @@ const reactButton = shallowRef<HTMLElement>();
 const quoteButton = shallowRef<HTMLElement>();
 const clipButton = shallowRef<HTMLElement>();
 const likeButton = 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(defaultStore.state.uncollapseCW);
 const parsed = computed(() => appearNote.value.text ? mfm.parse(appearNote.value.text) : null);
@@ -328,6 +324,11 @@ const defaultLike = computed(() => defaultStore.state.like ? defaultStore.state.
 const animated = computed(() => parsed.value ? checkAnimationFromMfm(parsed.value) : null);
 const allowAnim = ref(defaultStore.state.advancedMfm && defaultStore.state.animatedMfm ? true : false);
 
+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';
@@ -348,15 +349,53 @@ function checkMute(noteToCheck: Misskey.entities.Note, mutedWords: Array<string
 let renoting = false;
 
 const keymap = {
-	'r': () => reply(true),
-	'e|a|plus': () => react(true),
-	'(q)': () => { if (canRenote.value && !renoted.value && !renoting) renote(defaultStore.state.visibilityOnBoost); },
-	'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;
+		if (canRenote.value && !renoted.value && !renoting) renote(defaultStore.state.visibilityOnBoost);
+	},
+	'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', {
@@ -389,12 +428,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(),
+		});
 	});
 
 	useTooltip(quoteButton, async (showing) => {
@@ -408,12 +449,14 @@ if (!props.mock) {
 
 		if (users.length < 1) return;
 
-		os.popup(MkUsersTooltip, {
+		const { dispose } = os.popup(MkUsersTooltip, {
 			showing,
 			users,
 			count: appearNote.value.renoteCount,
 			targetElement: quoteButton.value,
-		}, {}, 'closed');
+		}, {
+			closed: () => dispose(),
+		});
 	});
 
 	if ($i) {
@@ -438,13 +481,15 @@ 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(),
+			});
 		});
 	}
 }
@@ -460,7 +505,7 @@ function boostVisibility() {
 }
 
 function renote(visibility: Visibility, localOnly: boolean = false) {
-	pleaseLogin();
+	pleaseLogin(undefined, pleaseLoginContext.value);
 	showMovedDialog();
 
 	renoting = true;
@@ -471,7 +516,9 @@ function renote(visibility: Visibility, localOnly: boolean = false) {
 			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) {
@@ -489,7 +536,9 @@ function renote(visibility: Visibility, localOnly: boolean = false) {
 			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) {
@@ -506,7 +555,7 @@ function renote(visibility: Visibility, localOnly: boolean = false) {
 }
 
 function quote() {
-	pleaseLogin();
+	pleaseLogin(undefined, pleaseLoginContext.value);
 	showMovedDialog();
 	if (props.mock) {
 		return;
@@ -529,7 +578,9 @@ function quote() {
 					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(),
+					});
 				}
 
 				os.toast(i18n.ts.quoted);
@@ -551,7 +602,9 @@ function quote() {
 					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(),
+					});
 				}
 
 				os.toast(i18n.ts.quoted);
@@ -560,22 +613,21 @@ function quote() {
 	}
 }
 
-function reply(viaKeyboard = false): void {
-	pleaseLogin();
+function reply(): void {
+	pleaseLogin(undefined, pleaseLoginContext.value);
 	if (props.mock) {
 		return;
 	}
 	os.post({
 		reply: appearNote.value,
 		channel: appearNote.value.channel,
-		animation: !viaKeyboard,
 	}).then(() => {
 		focus();
 	});
 }
 
 function like(): void {
-	pleaseLogin();
+	pleaseLogin(undefined, pleaseLoginContext.value);
 	showMovedDialog();
 	sound.playMisskeySfx('reaction');
 	if (props.mock) {
@@ -590,12 +642,14 @@ function like(): 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(),
+		});
 	}
 }
 
 function react(viaKeyboard = false): void {
-	pleaseLogin();
+	pleaseLogin(undefined, pleaseLoginContext.value);
 	showMovedDialog();
 	if (appearNote.value.reactionAcceptance === 'likeOnly') {
 		sound.playMisskeySfx('reaction');
@@ -613,7 +667,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();
@@ -667,7 +723,9 @@ function undoRenote(note) : 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(),
+		});
 	}
 }
 
@@ -706,15 +764,13 @@ 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 menuVersions(viaKeyboard = false): Promise<void> {
@@ -724,7 +780,7 @@ async function menuVersions(viaKeyboard = false): Promise<void> {
 	}).then(focus).finally(cleanup);
 }
 
-async function clip() {
+async function clip(): Promise<void> {
 	if (props.mock) {
 		return;
 	}
@@ -732,7 +788,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;
 	}
@@ -740,7 +796,7 @@ function showRenoteMenu(viaKeyboard = false): void {
 	function getUnrenote(): MenuItem {
 		return {
 			text: i18n.ts.unrenote,
-			icon: 'ph-trash ph-bold ph-lg',
+			icon: 'ti ti-trash',
 			danger: true,
 			action: () => {
 				misskeyApi('notes/delete', {
@@ -752,23 +808,19 @@ function showRenoteMenu(viaKeyboard = false): void {
 	}
 
 	if (isMyRenote) {
-		pleaseLogin();
+		pleaseLogin(undefined, 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);
 	}
 }
 
@@ -793,11 +845,11 @@ function blur() {
 }
 
 function focusBefore() {
-	focusPrev(rootEl.value ?? null);
+	focusPrev(rootEl.value);
 }
 
 function focusAfter() {
-	focusNext(rootEl.value ?? null);
+	focusNext(rootEl.value);
 }
 
 function readPromo() {
@@ -835,7 +887,7 @@ function emitUpdReaction(emoji: string, delta: number) {
 	&:focus-visible {
 		outline: none;
 
-		&:after {
+		&::after {
 			content: "";
 			pointer-events: none;
 			display: block;
@@ -848,7 +900,7 @@ function emitUpdReaction(emoji: string, delta: number) {
 			margin: auto;
 			width: calc(100% - 8px);
 			height: calc(100% - 8px);
-			border: dashed 1px var(--focus);
+			border: dashed 2px var(--focus);
 			border-radius: var(--radius);
 			box-sizing: border-box;
 		}
diff --git a/packages/frontend/src/components/MkNoteDetailed.vue b/packages/frontend/src/components/MkNoteDetailed.vue
index f0b1ca82a4..64559ef265 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">
@@ -20,7 +21,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 	<MkNoteSub v-if="appearNote.reply" :note="appearNote.reply" :class="$style.replyTo" :expandAllCws="props.expandAllCws"/>
 	<div v-if="isRenote" :class="$style.renote">
 		<MkAvatar :class="$style.renoteAvatar" :user="note.user" link preview/>
-		<i class="ph-rocket-launch ph-bold ph-lg" style="margin-right: 4px;"></i>
+		<i class="ti ti-repeat" style="margin-right: 4px;"></i>
 		<span :class="$style.renoteText">
 			<I18n :src="i18n.ts.renotedBy" tag="span">
 				<template #user>
@@ -31,16 +32,16 @@ SPDX-License-Identifier: AGPL-3.0-only
 			</I18n>
 		</span>
 		<div :class="$style.renoteInfo">
-			<button ref="renoteTime" class="_button" :class="$style.renoteTime" @click="showRenoteMenu()">
-				<i v-if="isMyRenote" class="ph-dots-three ph-bold ph-lg" style="margin-right: 4px;"></i>
+			<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>
 			<span v-if="note.visibility !== 'public'" style="margin-left: 0.5em;" :title="i18n.ts._visibility[note.visibility]">
-				<i v-if="note.visibility === 'home'" class="ph-house ph-bold ph-lg"></i>
-				<i v-else-if="note.visibility === 'followers'" class="ph-lock ph-bold ph-lg"></i>
-				<i v-else-if="note.visibility === 'specified'" ref="specified" class="ph-envelope ph-bold ph-lg"></i>
+				<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="ph-rocket ph-bold ph-lg"></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" @contextmenu.stop="onContextmenu">
@@ -54,12 +55,12 @@ SPDX-License-Identifier: AGPL-3.0-only
 					<span v-if="appearNote.user.isBot" :class="$style.isBot">bot</span>
 					<div :class="$style.noteHeaderInfo">
 						<span v-if="appearNote.visibility !== 'public'" style="margin-left: 0.5em;" :title="i18n.ts._visibility[appearNote.visibility]">
-							<i v-if="appearNote.visibility === 'home'" class="ph-house ph-bold ph-lg"></i>
-							<i v-else-if="appearNote.visibility === 'followers'" class="ph-lock ph-bold ph-lg"></i>
-							<i v-else-if="appearNote.visibility === 'specified'" ref="specified" class="ph-envelope ph-bold ph-lg"></i>
+							<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.updatedAt" ref="menuVersionsButton" style="margin-left: 0.5em;" title="Edited" @mousedown="menuVersions()"><i class="ph-pencil-simple ph-bold ph-lg"></i></span>
-						<span v-if="appearNote.localOnly" style="margin-left: 0.5em;" :title="i18n.ts._visibility['disableFederation']"><i class="ph-rocket ph-bold ph-lg"></i></span>
+						<span v-if="appearNote.localOnly" style="margin-left: 0.5em;" :title="i18n.ts._visibility['disableFederation']"><i class="ti ti-rocket-off"></i></span>
 					</div>
 				</div>
 				<div :class="$style.noteHeaderUsername"><MkAcct :user="appearNote.user"/></div>
@@ -97,7 +98,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 				<MkButton v-if="!allowAnim && animated" :class="$style.playMFMButton" :small="true" @click="animatedMFM()" @click.stop><i class="ph-play ph-bold ph-lg "></i> {{ i18n.ts._animatedMFM.play }}</MkButton>
 				<MkButton v-else-if="!defaultStore.state.animatedMfm && allowAnim && animated" :class="$style.playMFMButton" :small="true" @click="animatedMFM()" @click.stop><i class="ph-stop ph-bold ph-lg "></i> {{ i18n.ts._animatedMFM.stop }}</MkButton>
 				<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">
@@ -105,7 +106,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 				</div>
 				<div v-if="appearNote.renote" :class="$style.quote"><MkNoteSimple :note="appearNote.renote" :class="$style.quoteNote" :expandAllCws="props.expandAllCws"/></div>
 			</div>
-			<MkA v-if="appearNote.channel && !inChannel" :class="$style.channel" :to="`/channels/${appearNote.channel.id}`"><i class="ph-television ph-bold ph-lg"></i> {{ appearNote.channel.name }}</MkA>
+			<MkA v-if="appearNote.channel && !inChannel" :class="$style.channel" :to="`/channels/${appearNote.channel.id}`"><i class="ti ti-device-tv"></i> {{ appearNote.channel.name }}</MkA>
 		</div>
 		<footer :class="$style.footer">
 			<div :class="$style.noteFooterInfo">
@@ -118,7 +119,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 			</div>
 			<MkReactionsViewer v-if="appearNote.reactionAcceptance !== 'likeOnly'" ref="reactionsViewer" :note="appearNote"/>
 			<button class="_button" :class="$style.noteFooterButton" @click="reply()">
-				<i class="ph-arrow-u-up-left ph-bold ph-lg"></i>
+				<i class="ti ti-arrow-back-up"></i>
 				<p v-if="appearNote.repliesCount > 0" :class="$style.noteFooterButtonCount">{{ number(appearNote.repliesCount) }}</p>
 			</button>
 			<button
@@ -127,13 +128,13 @@ SPDX-License-Identifier: AGPL-3.0-only
 				class="_button"
 				:class="$style.noteFooterButton"
 				:style="renoted ? 'color: var(--accent) !important;' : ''"
-				@mousedown="renoted ? undoRenote() : boostVisibility()"
+				@mousedown.prevent="renoted ? undoRenote() : boostVisibility()"
 			>
-				<i class="ph-rocket-launch ph-bold ph-lg"></i>
+				<i class="ti ti-repeat"></i>
 				<p v-if="appearNote.renoteCount > 0" :class="$style.noteFooterButtonCount">{{ number(appearNote.renoteCount) }}</p>
 			</button>
 			<button v-else class="_button" :class="$style.noteFooterButton" disabled>
-				<i class="ph-prohibit ph-bold ph-lg"></i>
+				<i class="ti ti-ban"></i>
 			</button>
 			<button
 				v-if="canRenote"
@@ -148,23 +149,23 @@ SPDX-License-Identifier: AGPL-3.0-only
 				<i class="ph-heart ph-bold ph-lg"></i>
 			</button>
 			<button ref="reactButton" :class="$style.noteFooterButton" class="_button" @click="toggleReact()">
-				<i v-if="appearNote.reactionAcceptance === 'likeOnly' && appearNote.myReaction != null" class="ph-heart ph-bold ph-lg" style="color: var(--eventReactionHeart);"></i>
-				<i v-else-if="appearNote.myReaction != null" class="ph-minus ph-bold ph-lg" style="color: var(--accent);"></i>
-				<i v-else-if="appearNote.reactionAcceptance === 'likeOnly'" class="ph-heart ph-bold ph-lg"></i>
+				<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-else-if="appearNote.reactionAcceptance === 'likeOnly'" class="ti ti-heart"></i>
 				<i v-else class="ph-smiley ph-bold ph-lg"></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()">
-				<i class="ph-paperclip ph-bold ph-lg"></i>
+			<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()">
-				<i class="ph-dots-three ph-bold ph-lg"></i>
+			<button ref="menuButton" class="_button" :class="$style.noteFooterButton" @mousedown.prevent="showMenu()">
+				<i class="ti ti-dots"></i>
 			</button>
 		</footer>
 	</article>
 	<div :class="$style.tabs">
-		<button class="_button" :class="[$style.tab, { [$style.tabActive]: tab === 'replies' }]" @click="tab = 'replies'"><i class="ph-arrow-u-up-left ph-bold ph-lg"></i> {{ i18n.ts.replies }}</button>
-		<button class="_button" :class="[$style.tab, { [$style.tabActive]: tab === 'renotes' }]" @click="tab = 'renotes'"><i class="ph-rocket-launch ph-bold ph-lg"></i> {{ i18n.ts.renotes }}</button>
+		<button class="_button" :class="[$style.tab, { [$style.tabActive]: tab === 'replies' }]" @click="tab = 'replies'"><i class="ti ti-arrow-back-up"></i> {{ i18n.ts.replies }}</button>
+		<button class="_button" :class="[$style.tab, { [$style.tabActive]: tab === 'renotes' }]" @click="tab = 'renotes'"><i class="ti ti-repeat"></i> {{ i18n.ts.renotes }}</button>
 		<button class="_button" :class="[$style.tab, { [$style.tabActive]: tab === 'quotes' }]" @click="tab = 'quotes'"><i class="ph-quotes ph-bold ph-lg"></i> {{ i18n.ts._notification._types.quote }}</button>
 		<button class="_button" :class="[$style.tab, { [$style.tabActive]: tab === 'reactions' }]" @click="tab = 'reactions'"><i class="ph-smiley ph-bold ph-lg"></i> {{ i18n.ts.reactions }}</button>
 	</div>
@@ -236,7 +237,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';
@@ -249,6 +250,7 @@ import { reactionPicker } from '@/scripts/reaction-picker.js';
 import { extractUrlFromMfm } from '@/scripts/extract-url-from-mfm.js';
 import { $i } from '@/account.js';
 import { i18n } from '@/i18n.js';
+import { host } from '@/config.js';
 import { getNoteClipMenu, getNoteMenu } from '@/scripts/get-note-menu.js';
 import { getNoteVersionsMenu } from '@/scripts/get-note-versions-menu.js';
 import { useNoteCapture } from '@/scripts/use-note-capture.js';
@@ -264,6 +266,8 @@ import MkReactionIcon from '@/components/MkReactionIcon.vue';
 import MkButton from '@/components/MkButton.vue';
 import { boostMenuItems, type Visibility } from '@/scripts/boost-quote.js';
 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;
@@ -296,14 +300,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>();
@@ -314,7 +311,8 @@ const reactButton = shallowRef<HTMLElement>();
 const quoteButton = shallowRef<HTMLElement>();
 const clipButton = shallowRef<HTMLElement>();
 const likeButton = 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(defaultStore.state.uncollapseCW);
 const isDeleted = ref(false);
@@ -349,14 +347,31 @@ if ($i) {
 
 let renoting = false;
 
+const pleaseLoginContext = computed<OpenOnRemoteOptions>(() => ({
+	type: 'lookup',
+	url: `https://${host}/notes/${appearNote.value.id}`,
+}));
+
 const keymap = {
-	'r': () => reply(true),
-	'e|a|plus': () => react(true),
-	'(q)': () => { if (canRenote.value && !renoted.value && !renoting) renote(defaultStore.state.visibilityOnBoost); },
-	'esc': blur,
-	'm|o': () => showMenu(true),
-	's': () => showContent.value !== showContent.value,
-};
+	'r': () => reply(),
+	'e|a|plus': () => react(),
+	'q': () => { if (canRenote.value && !renoted.value && !renoting) renote(defaultStore.state.visibilityOnBoost); },
+	'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', {
@@ -416,12 +431,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(),
+	});
 });
 
 useTooltip(quoteButton, async (showing) => {
@@ -435,12 +452,14 @@ useTooltip(quoteButton, async (showing) => {
 
 	if (users.length < 1) return;
 
-	os.popup(MkUsersTooltip, {
+	const { dispose } = os.popup(MkUsersTooltip, {
 		showing,
 		users,
 		count: appearNote.value.renoteCount,
 		targetElement: quoteButton.value,
-	}, {}, 'closed');
+	}, {
+		closed: () => dispose(),
+	});
 });
 
 function boostVisibility() {
@@ -465,18 +484,20 @@ 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(visibility: Visibility, localOnly: boolean = false) {
-	pleaseLogin();
+	pleaseLogin(undefined, pleaseLoginContext.value);
 	showMovedDialog();
 
 	renoting = true;
@@ -487,7 +508,9 @@ function renote(visibility: Visibility, localOnly: boolean = false) {
 			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(),
+			});
 		}
 
 		misskeyApi('notes/create', {
@@ -503,7 +526,9 @@ function renote(visibility: Visibility, localOnly: boolean = false) {
 			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(),
+			});
 		}
 
 		misskeyApi('notes/create', {
@@ -518,7 +543,7 @@ function renote(visibility: Visibility, localOnly: boolean = false) {
 }
 
 function quote() {
-	pleaseLogin();
+	pleaseLogin(undefined, pleaseLoginContext.value);
 	showMovedDialog();
 
 	if (appearNote.value.channel) {
@@ -538,7 +563,9 @@ function quote() {
 					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(),
+					});
 				}
 
 				os.toast(i18n.ts.quoted);
@@ -560,7 +587,9 @@ function quote() {
 					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(),
+					});
 				}
 
 				os.toast(i18n.ts.quoted);
@@ -569,20 +598,19 @@ function quote() {
 	}
 }
 
-function reply(viaKeyboard = false): void {
-	pleaseLogin();
+function reply(): void {
+	pleaseLogin(undefined, 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(undefined, pleaseLoginContext.value);
 	showMovedDialog();
 	if (appearNote.value.reactionAcceptance === 'likeOnly') {
 		sound.playMisskeySfx('reaction');
@@ -591,12 +619,14 @@ function react(viaKeyboard = false): void {
 			noteId: appearNote.value.id,
 			override: defaultLike.value,
 		});
-		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();
@@ -617,7 +647,7 @@ function react(viaKeyboard = false): void {
 }
 
 function like(): void {
-	pleaseLogin();
+	pleaseLogin(undefined, pleaseLoginContext.value);
 	showMovedDialog();
 	sound.playMisskeySfx('reaction');
 	misskeyApi('notes/like', {
@@ -629,7 +659,9 @@ function like(): 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(),
+		});
 	}
 }
 
@@ -654,7 +686,9 @@ function undoRenote() : 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(),
+		});
 	}
 }
 
@@ -687,30 +721,26 @@ 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 menuVersions(viaKeyboard = false): Promise<void> {
+async function menuVersions(): Promise<void> {
 	const { menu, cleanup } = await getNoteVersionsMenu({ note: note.value, menuVersionsButton });
-	os.popupMenu(menu, menuVersionsButton.value, {
-		viaKeyboard,
-	}).then(focus).finally(cleanup);
+	os.popupMenu(menu, menuVersionsButton.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(undefined, pleaseLoginContext.value);
 	os.popupMenu([{
 		text: i18n.ts.unrenote,
-		icon: 'ph-trash ph-bold ph-lg',
+		icon: 'ti ti-trash',
 		danger: true,
 		action: () => {
 			misskeyApi('notes/delete', {
@@ -718,9 +748,7 @@ function showRenoteMenu(viaKeyboard = false): void {
 			});
 			isDeleted.value = true;
 		},
-	}], renoteTime.value, {
-		viaKeyboard: viaKeyboard,
-	});
+	}], renoteTime.value);
 }
 
 function focus() {
@@ -794,6 +822,28 @@ function animatedMFM() {
 	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(--focus);
+			border-radius: var(--radius);
+			box-sizing: border-box;
+		}
+	}
 }
 
 .footer {
diff --git a/packages/frontend/src/components/MkNoteHeader.vue b/packages/frontend/src/components/MkNoteHeader.vue
index e643590e86..fd5da0d687 100644
--- a/packages/frontend/src/components/MkNoteHeader.vue
+++ b/packages/frontend/src/components/MkNoteHeader.vue
@@ -24,13 +24,13 @@ SPDX-License-Identifier: AGPL-3.0-only
 			<MkTime :time="note.createdAt" colored/>
 		</MkA>
 		<span v-if="note.visibility !== 'public'" style="margin-left: 0.5em;" :title="i18n.ts._visibility[note.visibility]">
-			<i v-if="note.visibility === 'home'" class="ph-house ph-bold ph-lg"></i>
-			<i v-else-if="note.visibility === 'followers'" class="ph-lock ph-bold ph-lg"></i>
-			<i v-else-if="note.visibility === 'specified'" ref="specified" class="ph-envelope ph-bold ph-lg"></i>
+			<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.updatedAt" ref="menuVersionsButton" style="margin-left: 0.5em; cursor: pointer;" title="Edited" @mousedown="menuVersions()"><i class="ph-pencil-simple ph-bold ph-lg"></i></span>
-		<span v-if="note.localOnly" style="margin-left: 0.5em;" :title="i18n.ts._visibility['disableFederation']"><i class="ph-rocket ph-bold ph-lg"></i></span>
-		<span v-if="note.channel" style="margin-left: 0.5em;" :title="note.channel.name"><i class="ph-television ph-bold ph-lg"></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>
 </header>
 </template>
diff --git a/packages/frontend/src/components/MkNotePreview.vue b/packages/frontend/src/components/MkNotePreview.vue
index a8853a8a5f..7ccc2c0320 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/MkNoteSub.vue b/packages/frontend/src/components/MkNoteSub.vue
index 66d1e51a6c..9caed62ce2 100644
--- a/packages/frontend/src/components/MkNoteSub.vue
+++ b/packages/frontend/src/components/MkNoteSub.vue
@@ -68,7 +68,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 		<MkNoteSub v-for="reply in replies" :key="reply.id" :note="reply" :class="$style.reply" :detail="true" :depth="depth + 1" :expandAllCws="props.expandAllCws" :onDeleteCallback="removeReply"/>
 	</template>
 	<div v-else :class="$style.more">
-		<MkA class="_link" :to="notePage(note)">{{ i18n.ts.continueThread }} <i class="ph-caret-double-right ph-bold ph-lg"></i></MkA>
+		<MkA class="_link" :to="notePage(note)">{{ i18n.ts.continueThread }} <i class="ti ti-chevron-double-right"></i></MkA>
 	</div>
 </div>
 <div v-else :class="$style.muted" @click="muted = false">
@@ -207,7 +207,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();
@@ -238,7 +240,9 @@ function like(): 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(),
+		});
 	}
 }
 
@@ -263,7 +267,9 @@ function undoRenote() : 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(),
+		});
 	}
 }
 
@@ -291,7 +297,9 @@ function renote(visibility: Visibility, localOnly: boolean = false) {
 			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(),
+			});
 		}
 
 		misskeyApi('notes/create', {
@@ -307,7 +315,9 @@ function renote(visibility: Visibility, localOnly: boolean = false) {
 			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(),
+			});
 		}
 
 		misskeyApi('notes/create', {
@@ -342,7 +352,9 @@ function quote() {
 					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(),
+					});
 				}
 
 				os.toast(i18n.ts.quoted);
@@ -364,7 +376,9 @@ function quote() {
 					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(),
+					});
 				}
 
 				os.toast(i18n.ts.quoted);
diff --git a/packages/frontend/src/components/MkNotes.vue b/packages/frontend/src/components/MkNotes.vue
index 5240d64661..15173fbd99 100644
--- a/packages/frontend/src/components/MkNotes.vue
+++ b/packages/frontend/src/components/MkNotes.vue
@@ -15,7 +15,6 @@ SPDX-License-Identifier: AGPL-3.0-only
 	<template #default="{ items: notes }">
 		<div :class="[$style.root, { [$style.noGap]: noGap }]">
 			<MkDateSeparatedList
-				v-if="defaultStore.state.noteDesign === 'misskey'"
 				ref="notes"
 				v-slot="{ item: note }"
 				:items="notes"
@@ -27,34 +26,25 @@ SPDX-License-Identifier: AGPL-3.0-only
 			>
 				<MkNote :key="note._featuredId_ || note._prId_ || note.id" :class="$style.note" :note="note" :withHardMute="true"/>
 			</MkDateSeparatedList>
-			<MkDateSeparatedList
-				v-else-if="defaultStore.state.noteDesign === 'sharkey'"
-				ref="notes" 
-				v-slot="{ item: note }"
-				:items="notes"
-				:direction="pagination.reversed ? 'up' : 'down'"
-				:reversed="pagination.reversed"
-				:noGap="noGap"
-				:ad="true"
-				:class="$style.notes"
-			>
-				<SkNote :key="note._featuredId_ || note._prId_ || note.id" :class="$style.note" :note="note" :withHardMute="true"/>
-			</MkDateSeparatedList>
 		</div>
 	</template>
 </MkPagination>
 </template>
 
 <script lang="ts" setup>
-import { shallowRef, ref } from 'vue';
-import MkNote from '@/components/MkNote.vue';
-import SkNote from '@/components/SkNote.vue';
+import { defineAsyncComponent, shallowRef, ref } from 'vue';
 import MkDateSeparatedList from '@/components/MkDateSeparatedList.vue';
 import MkPagination, { Paging } from '@/components/MkPagination.vue';
 import { i18n } from '@/i18n.js';
 import { infoImageUrl } from '@/instance.js';
 import { defaultStore } from '@/store.js';
 
+const MkNote = defineAsyncComponent(() =>
+	(defaultStore.state.noteDesign === 'misskey') ? import('@/components/MkNote.vue') :
+	(defaultStore.state.noteDesign === 'sharkey') ? import('@/components/SkNote.vue') :
+	null
+);
+
 const props = defineProps<{
 	pagination: Paging;
 	noGap?: boolean;
diff --git a/packages/frontend/src/components/MkNotification.vue b/packages/frontend/src/components/MkNotification.vue
index f849e94e93..9948676198 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', 'edited'].includes(notification.type) && notification.note" :class="$style.icon" :user="notification.note.user" link preview/>
+		<MkAvatar v-if="['pollEnded', 'note', 'edited'].includes(notification.type) && 'note' in notification" :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/>
 		<div v-else-if="notification.type === 'reaction:grouped' && notification.note.reactionAcceptance === 'likeOnly'" :class="[$style.icon, $style.icon_reactionGroup]"><i class="ph-smiley ph-bold ph-lg" style="line-height: 1;"></i></div>
 		<div v-else-if="notification.type === 'reaction:grouped'" :class="[$style.icon, $style.icon_reactionGroup]"><i class="ph-smiley ph-bold ph-lg" style="line-height: 1;"></i></div>
-		<div v-else-if="notification.type === 'renote:grouped'" :class="[$style.icon, $style.icon_renoteGroup]"><i class="ph-rocket-launch ph-bold ph-lg" 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" :class="[$style.icon, $style.icon_app]" :src="notification.icon" alt=""/>
 		<div
 			:class="[$style.subIcon, {
 				[$style.t_follow]: notification.type === 'follow',
@@ -29,18 +29,18 @@ SPDX-License-Identifier: AGPL-3.0-only
 				[$style.t_pollEnded]: notification.type === 'edited',
 			}]"
 		> <!-- we re-use t_pollEnded for "edited" instead of making an identical style -->
-			<i v-if="notification.type === 'follow'" class="ph-plus ph-bold ph-lg"></i>
-			<i v-else-if="notification.type === 'receiveFollowRequest'" class="ph-clock ph-bold ph-lg"></i>
-			<i v-else-if="notification.type === 'followRequestAccepted'" class="ph-check ph-bold ph-lg"></i>
-			<i v-else-if="notification.type === 'renote'" class="ph-rocket-launch ph-bold ph-lg"></i>
-			<i v-else-if="notification.type === 'reply'" class="ph-arrow-u-up-left ph-bold ph-lg"></i>
-			<i v-else-if="notification.type === 'mention'" class="ph-at ph-bold ph-lg"></i>
-			<i v-else-if="notification.type === 'quote'" class="ph-quotes ph-bold ph-lg"></i>
-			<i v-else-if="notification.type === 'pollEnded'" class="ph-chart-bar-horizontal ph-bold ph-lg"></i>
-			<i v-else-if="notification.type === 'achievementEarned'" class="ph-trophy ph-bold ph-lg"></i>
+			<i v-if="notification.type === 'follow'" class="ti ti-plus"></i>
+			<i v-else-if="notification.type === 'receiveFollowRequest'" class="ti ti-clock"></i>
+			<i v-else-if="notification.type === 'followRequestAccepted'" class="ti ti-check"></i>
+			<i v-else-if="notification.type === 'renote'" class="ti ti-repeat"></i>
+			<i v-else-if="notification.type === 'reply'" class="ti ti-arrow-back-up"></i>
+			<i v-else-if="notification.type === 'mention'" class="ti ti-at"></i>
+			<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>
 			<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="ph-seal-check ph-bold ph-lg"></i>
+				<i v-else class="ti ti-badges"></i>
 			</template>
 			<i v-else-if="notification.type === 'edited'" class="ph-pencil ph-bold ph-lg"></i>
 			<!-- notification.reaction が null になることはまずないが、ここでoptional chaining使うと一部ブラウザで刺さるので念の為 -->
@@ -70,14 +70,14 @@ SPDX-License-Identifier: AGPL-3.0-only
 		</header>
 		<div>
 			<MkA v-if="notification.type === 'reaction' || notification.type === 'reaction:grouped'" :class="$style.text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)">
-				<i class="ph-quotes ph-bold ph-lg" :class="$style.quote"></i>
+				<i class="ti ti-quote" :class="$style.quote"></i>
 				<Mfm :text="getNoteSummary(notification.note)" :isBlock="true" :plain="true" :nowrap="true" :author="notification.note.user"/>
-				<i class="ph-quotes ph-bold ph-lg" :class="$style.quote"></i>
+				<i class="ti ti-quote" :class="$style.quote"></i>
 			</MkA>
 			<MkA v-else-if="notification.type === 'renote' || notification.type === 'renote:grouped'" :class="$style.text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note.renote)">
-				<i class="ph-quotes ph-bold ph-lg" :class="$style.quote"></i>
+				<i class="ti ti-quote" :class="$style.quote"></i>
 				<Mfm :text="getNoteSummary(notification.note.renote)" :isBlock="true" :plain="true" :nowrap="true" :author="notification.note.renote?.user"/>
-				<i class="ph-quotes ph-bold ph-lg" :class="$style.quote"></i>
+				<i class="ti ti-quote" :class="$style.quote"></i>
 			</MkA>
 			<MkA v-else-if="notification.type === 'reply'" :class="$style.text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)">
 				<Mfm :text="getNoteSummary(notification.note)" :isBlock="true" :plain="true" :nowrap="true" :author="notification.note.user"/>
@@ -92,9 +92,9 @@ SPDX-License-Identifier: AGPL-3.0-only
 				<Mfm :text="getNoteSummary(notification.note)" :isBlock="true" :plain="true" :nowrap="true" :author="notification.note.user"/>
 			</MkA>
 			<MkA v-else-if="notification.type === 'pollEnded'" :class="$style.text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)">
-				<i class="ph-quotes ph-bold ph-lg" :class="$style.quote"></i>
+				<i class="ti ti-quote" :class="$style.quote"></i>
 				<Mfm :text="getNoteSummary(notification.note)" :isBlock="true" :plain="true" :nowrap="true" :author="notification.note.user"/>
-				<i class="ph-quotes ph-bold ph-lg" :class="$style.quote"></i>
+				<i class="ti ti-quote" :class="$style.quote"></i>
 			</MkA>
 			<div v-else-if="notification.type === 'roleAssigned'" :class="$style.text">
 				{{ notification.role.name }}
@@ -109,8 +109,8 @@ SPDX-License-Identifier: AGPL-3.0-only
 			<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">
-					<MkButton :class="$style.followRequestCommandButton" rounded primary @click="acceptFollowRequest()"><i class="ph-check ph-bold ph-lg"/> {{ i18n.ts.accept }}</MkButton>
-					<MkButton :class="$style.followRequestCommandButton" rounded danger @click="rejectFollowRequest()"><i class="ph-x ph-bold ph-lg"/> {{ i18n.ts.reject }}</MkButton>
+					<MkButton :class="$style.followRequestCommandButton" rounded primary @click="acceptFollowRequest()"><i class="ti ti-check"/> {{ i18n.ts.accept }}</MkButton>
+					<MkButton :class="$style.followRequestCommandButton" rounded danger @click="rejectFollowRequest()"><i class="ti ti-x"/> {{ i18n.ts.reject }}</MkButton>
 				</div>
 			</template>
 			<span v-else-if="notification.type === 'test'" :class="$style.text">{{ i18n.ts._notification.notificationWillBeDisplayedLikeThis }}</span>
@@ -174,13 +174,13 @@ const props = withDefaults(defineProps<{
 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 });
 };
@@ -353,7 +353,7 @@ function getActualReactedUsersCount(notification: Misskey.entities.Notification)
 	margin-right: 4px;
 	position: relative;
 
-	&:before {
+	&::before {
 		position: absolute;
 		transform: rotate(180deg);
 	}
diff --git a/packages/frontend/src/components/MkNotifications.vue b/packages/frontend/src/components/MkNotifications.vue
index 68bf1bf3d8..2dd6c21ef6 100644
--- a/packages/frontend/src/components/MkNotifications.vue
+++ b/packages/frontend/src/components/MkNotifications.vue
@@ -14,26 +14,20 @@ SPDX-License-Identifier: AGPL-3.0-only
 		</template>
 
 		<template #default="{ items: notifications }">
-			<MkDateSeparatedList v-if="defaultStore.state.noteDesign === 'misskey'" v-slot="{ item: notification }" :class="$style.list" :items="notifications" :noGap="true">
+			<MkDateSeparatedList v-slot="{ item: notification }" :class="$style.list" :items="notifications" :noGap="true">
 				<MkNote v-if="['reply', 'quote', 'mention'].includes(notification.type)" :key="notification.id + ':note'" :note="notification.note" :withHardMute="true"/>
 				<XNotification v-else :key="notification.id" :notification="notification" :withTime="true" :full="true" class="_panel"/>
 			</MkDateSeparatedList>
-			<MkDateSeparatedList v-else-if="defaultStore.state.noteDesign === 'sharkey'" v-slot="{ item: notification }" :class="$style.list" :items="notifications" :noGap="true">
-				<SkNote v-if="['reply', 'quote', 'mention'].includes(notification.type)" :key="notification.id + ':note'" :note="notification.note" :withHardMute="true"/>
-				<XNotification v-else :key="notification.id" :notification="notification" :withTime="true" :full="true" class="_panel"/>
-			</MkDateSeparatedList>
 		</template>
 	</MkPagination>
 </MkPullToRefresh>
 </template>
 
 <script lang="ts" setup>
-import { onUnmounted, onDeactivated, onMounted, computed, shallowRef, onActivated } from 'vue';
+import { defineAsyncComponent, onUnmounted, onDeactivated, onMounted, computed, shallowRef, onActivated } from 'vue';
 import MkPagination from '@/components/MkPagination.vue';
 import XNotification from '@/components/MkNotification.vue';
 import MkDateSeparatedList from '@/components/MkDateSeparatedList.vue';
-import MkNote from '@/components/MkNote.vue';
-import SkNote from '@/components/SkNote.vue';
 import { useStream } from '@/stream.js';
 import { i18n } from '@/i18n.js';
 import { notificationTypes } from '@/const.js';
@@ -42,6 +36,12 @@ import { defaultStore } from '@/store.js';
 import MkPullToRefresh from '@/components/MkPullToRefresh.vue';
 import * as Misskey from 'misskey-js';
 
+const MkNote = defineAsyncComponent(() =>
+	(defaultStore.state.noteDesign === 'misskey') ? import('@/components/MkNote.vue') :
+	(defaultStore.state.noteDesign === 'sharkey') ? import('@/components/SkNote.vue') :
+	null
+);
+
 const props = defineProps<{
 	excludeTypes?: typeof notificationTypes[number][];
 }>();
diff --git a/packages/frontend/src/components/MkPagePreview.vue b/packages/frontend/src/components/MkPagePreview.vue
index f6dc00698c..8559d4b96e 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"
@@ -50,12 +50,29 @@ const props = defineProps<{
 <style lang="scss" scoped>
 .vhpxefrj {
 	display: block;
+	position: relative;
 
 	&:hover {
 		text-decoration: none;
 		color: var(--accent);
 	}
 
+	&:focus-within {
+		outline: none;
+
+		&::after {
+			content: "";
+			position: absolute;
+			top: 0;
+			left: 0;
+			width: 100%;
+			height: 100%;
+			border-radius: var(--radius);
+			pointer-events: none;
+			box-shadow: inset 0 0 0 2px var(--focus);
+		}
+	}
+
 	> .thumbnail {
 		& + article {
 			border-radius: 0 0 var(--radius) var(--radius);
diff --git a/packages/frontend/src/components/MkPageWindow.vue b/packages/frontend/src/components/MkPageWindow.vue
index d664dc9731..8f6109ca04 100644
--- a/packages/frontend/src/components/MkPageWindow.vue
+++ b/packages/frontend/src/components/MkPageWindow.vue
@@ -33,7 +33,7 @@ import { computed, onMounted, onUnmounted, provide, ref, shallowRef } from 'vue'
 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 { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
 import { url } from '@/config.js';
 import { useScrollPositionManager } from '@/nirax.js';
 import { i18n } from '@/i18n.js';
@@ -68,7 +68,7 @@ const buttonsLeft = computed(() => {
 
 	if (history.value.length > 1) {
 		buttons.push({
-			icon: 'ph-arrow-left ph-bold ph-lg',
+			icon: 'ti ti-arrow-left',
 			onClick: back,
 		});
 	}
@@ -77,11 +77,11 @@ const buttonsLeft = computed(() => {
 });
 const buttonsRight = computed(() => {
 	const buttons = [{
-		icon: 'ph-arrows-clockwise ph-bold ph-lg',
+		icon: 'ti ti-reload',
 		title: i18n.ts.reload,
 		onClick: reload,
 	}, {
-		icon: 'ph-eject ph-bold ph-lg',
+		icon: 'ti ti-player-eject',
 		title: i18n.ts.showInPage,
 		onClick: expand,
 	}];
@@ -113,22 +113,22 @@ provide('forceSpacerMin', true);
 provide('shouldBackButton', false);
 
 const contextmenu = computed(() => ([{
-	icon: 'ph-eject ph-bold ph-lg',
+	icon: 'ti ti-player-eject',
 	text: i18n.ts.showInPage,
 	action: expand,
 }, {
-	icon: 'ph-frame-corners ph-bold ph-lg',
+	icon: 'ti ti-window-maximize',
 	text: i18n.ts.popout,
 	action: popout,
 }, {
-	icon: 'ph-arrow-square-out ph-bold ph-lg',
+	icon: 'ti ti-external-link',
 	text: i18n.ts.openInNewTab,
 	action: () => {
 		window.open(url + windowRouter.getCurrentPath(), '_blank', 'noopener');
 		windowEl.value?.close();
 	},
 }, {
-	icon: 'ph-link ph-bold ph-lg',
+	icon: 'ti ti-link',
 	text: i18n.ts.copyLink,
 	action: () => {
 		copyToClipboard(url + windowRouter.getCurrentPath());
diff --git a/packages/frontend/src/components/MkPagination.vue b/packages/frontend/src/components/MkPagination.vue
index 9a324849e2..4f2a4d2703 100644
--- a/packages/frontend/src/components/MkPagination.vue
+++ b/packages/frontend/src/components/MkPagination.vue
@@ -43,7 +43,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts">
-import { computed, ComputedRef, isRef, nextTick, onActivated, onBeforeMount, onBeforeUnmount, onDeactivated, ref, shallowRef, watch } from 'vue';
+import { computed, ComputedRef, isRef, nextTick, onActivated, onBeforeMount, onBeforeUnmount, onDeactivated, ref, shallowRef, watch, type Ref } from 'vue';
 import * as Misskey from 'misskey-js';
 import * as os from '@/os.js';
 import { misskeyApi } from '@/scripts/misskey-api.js';
@@ -199,11 +199,17 @@ watch(error, (n, o) => {
 	emit('status', n);
 });
 
+function getActualValue<T>(input: T|Ref<T>|undefined, defaultValue: T) : T {
+		if (!input) return defaultValue;
+		if (isRef(input)) return input.value;
+		return input;
+}
+
 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 : {};
+	const params = getActualValue<Paging['params']>(props.pagination.params, {});
 	await misskeyApi<MisskeyEntity[]>(props.pagination.endpoint, {
 		...params,
 		limit: props.pagination.limit ?? 10,
@@ -239,8 +245,8 @@ const reload = (): Promise<void> => {
 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 : {};
-	const offsetMode = props.offsetMode ? isRef(props.offsetMode) ? props.offsetMode.value : props.offsetMode : false;
+	const params = getActualValue<Paging['params']>(props.pagination.params, {});
+	const offsetMode = getActualValue(props.pagination.offsetMode, false);
 	await misskeyApi<MisskeyEntity[]>(props.pagination.endpoint, {
 		...params,
 		limit: SECOND_FETCH_LIMIT,
@@ -304,8 +310,8 @@ const fetchMore = async (): Promise<void> => {
 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 : {};
-	const offsetMode = props.offsetMode ? isRef(props.offsetMode) ? props.offsetMode.value : props.offsetMode : false;
+	const params = getActualValue<Paging['params']>(props.pagination.params, {});
+	const offsetMode = getActualValue(props.pagination.offsetMode, false);
 	await misskeyApi<MisskeyEntity[]>(props.pagination.endpoint, {
 		...params,
 		limit: SECOND_FETCH_LIMIT,
diff --git a/packages/frontend/src/components/MkPasswordDialog.vue b/packages/frontend/src/components/MkPasswordDialog.vue
index 3a13326946..e749725fea 100644
--- a/packages/frontend/src/components/MkPasswordDialog.vue
+++ b/packages/frontend/src/components/MkPasswordDialog.vue
@@ -22,16 +22,16 @@ SPDX-License-Identifier: AGPL-3.0-only
 		<form @submit.prevent="done">
 			<div class="_gaps">
 				<MkInput ref="passwordInput" v-model="password" :placeholder="i18n.ts.password" type="password" autocomplete="current-password webauthn" required :withPasswordToggle="true">
-					<template #prefix><i class="ph-password ph-bold ph-lg"></i></template>
+					<template #prefix><i class="ti ti-password"></i></template>
 				</MkInput>
 
 				<MkInput v-if="$i.twoFactorEnabled" 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="ph-keyhole ph-bold ph-lg"></i><i v-else class="ph-numpad ph-bold ph-lg"></i></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 :disabled="(password ?? '') == '' || ($i.twoFactorEnabled && (token ?? '') == '')" type="submit" primary rounded style="margin: 0 auto;"><i class="ph-lock ph-bold ph-lg-open"></i> {{ i18n.ts.continue }}</MkButton>
+				<MkButton :disabled="(password ?? '') == '' || ($i.twoFactorEnabled && (token ?? '') == '')" type="submit" primary rounded style="margin: 0 auto;"><i class="ti ti-lock-open"></i> {{ i18n.ts.continue }}</MkButton>
 			</div>
 		</form>
 	</MkSpacer>
diff --git a/packages/frontend/src/components/MkPoll.vue b/packages/frontend/src/components/MkPoll.vue
index 8c0804de04..393ac4efba 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="ph-check ph-bold ph-lg" 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(--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>
@@ -36,7 +36,9 @@ 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 { host } from '@/config.js';
 import { useInterval } from '@/scripts/use-interval.js';
+import type { OpenOnRemoteOptions } from '@/scripts/please-login.js';
 
 const props = defineProps<{
 	noteId: string;
@@ -62,6 +64,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 = () => {
@@ -78,7 +85,7 @@ if (props.poll.expiresAt) {
 }
 
 const vote = async (id) => {
-	pleaseLogin();
+	pleaseLogin(undefined, pleaseLoginContext.value);
 
 	if (props.readOnly || closed.value || isVoted.value) return;
 	if (!props.poll.multiple) {
diff --git a/packages/frontend/src/components/MkPollEditor.vue b/packages/frontend/src/components/MkPollEditor.vue
index 98fbf25370..3726ddf822 100644
--- a/packages/frontend/src/components/MkPollEditor.vue
+++ b/packages/frontend/src/components/MkPollEditor.vue
@@ -6,14 +6,14 @@ SPDX-License-Identifier: AGPL-3.0-only
 <template>
 <div class="zmdxowus">
 	<p v-if="choices.length < 2" class="caution">
-		<i class="ph-warning ph-bold ph-lg"></i>{{ i18n.ts._poll.noOnlyOneChoice }}
+		<i class="ti ti-alert-triangle"></i>{{ i18n.ts._poll.noOnlyOneChoice }}
 	</p>
 	<ul>
 		<li v-for="(choice, i) in choices" :key="i">
 			<MkInput class="input" small :modelValue="choice" :placeholder="i18n.tsx._poll.choiceN({ n: i + 1 })" @update:modelValue="onInput(i, $event)">
 			</MkInput>
 			<button class="_button" @click="remove(i)">
-				<i class="ph-x ph-bold ph-lg"></i>
+				<i class="ti ti-x"></i>
 			</button>
 		</li>
 	</ul>
@@ -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 3748f0cc64..ff29b66193 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>
 
@@ -19,8 +19,8 @@ defineProps<{
 	items: MenuItem[];
 	align?: 'center' | string;
 	width?: number;
-	viaKeyboard?: boolean;
 	src?: any;
+	returnFocusTo?: HTMLElement | null;
 }>();
 
 const emit = defineEmits<{
diff --git a/packages/frontend/src/components/MkPostForm.vue b/packages/frontend/src/components/MkPostForm.vue
index cfaaeecc34..2bc607fbb6 100644
--- a/packages/frontend/src/components/MkPostForm.vue
+++ b/packages/frontend/src/components/MkPostForm.vue
@@ -13,7 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 >
 	<header :class="$style.header">
 		<div :class="$style.headerLeft">
-			<button v-if="!fixed" :class="$style.cancel" class="_button" @click="cancel"><i class="ph-x ph-bold ph-lg"></i></button>
+			<button v-if="!fixed" :class="$style.cancel" class="_button" @click="cancel"><i class="ti ti-x"></i></button>
 			<button v-click-anime v-tooltip="i18n.ts.switchAccount" :class="$style.account" class="_button" @click="openAccountMenu">
 				<MkAvatar :user="postAccount ?? $i" :class="$style.avatar"/>
 			</button>
@@ -21,24 +21,24 @@ SPDX-License-Identifier: AGPL-3.0-only
 		<div :class="$style.headerRight">
 			<template v-if="!(channel != null && fixed)">
 				<button v-if="channel == null" ref="visibilityButton" v-click-anime v-tooltip="i18n.ts.visibility" :class="['_button', $style.headerRightItem, $style.visibility]" @click="setVisibility">
-					<span v-if="visibility === 'public'"><i class="ph-globe-hemisphere-west ph-bold ph-lg"></i></span>
-					<span v-if="visibility === 'home'"><i class="ph-house ph-bold ph-lg"></i></span>
-					<span v-if="visibility === 'followers'"><i class="ph-lock ph-bold ph-lg"></i></span>
-					<span v-if="visibility === 'specified'"><i class="ph-envelope ph-bold ph-lg"></i></span>
+					<span v-if="visibility === 'public'"><i class="ti ti-world"></i></span>
+					<span v-if="visibility === 'home'"><i class="ti ti-home"></i></span>
+					<span v-if="visibility === 'followers'"><i class="ti ti-lock"></i></span>
+					<span v-if="visibility === 'specified'"><i class="ti ti-mail"></i></span>
 					<span :class="$style.headerRightButtonText">{{ i18n.ts._visibility[visibility] }}</span>
 				</button>
 				<button v-else class="_button" :class="[$style.headerRightItem, $style.visibility]" disabled>
-					<span><i class="ph-television ph-bold ph-lg"></i></span>
+					<span><i class="ti ti-device-tv"></i></span>
 					<span :class="$style.headerRightButtonText">{{ channel.name }}</span>
 				</button>
 			</template>
 			<button v-click-anime v-tooltip="i18n.ts._visibility.disableFederation" class="_button" :class="[$style.headerRightItem, { [$style.danger]: localOnly }]" :disabled="channel != null || visibility === 'specified'" @click="toggleLocalOnly">
-				<span v-if="!localOnly"><i class="ph-rocket-launch ph-bold ph-lg"></i></span>
-				<span v-else><i class="ph-rocket ph-bold ph-lg"></i></span>
+				<span v-if="!localOnly"><i class="ti ti-rocket"></i></span>
+				<span v-else><i class="ti ti-rocket-off"></i></span>
 			</button>
 			<button v-click-anime v-tooltip="i18n.ts.reactionAcceptance" class="_button" :class="[$style.headerRightItem, { [$style.danger]: reactionAcceptance === 'likeOnly' }]" @click="toggleReactionAcceptance">
-				<span v-if="reactionAcceptance === 'likeOnly'"><i class="ph-heart ph-bold ph-lg"></i></span>
-				<span v-else-if="reactionAcceptance === 'likeOnlyForRemote'"><i class="ph-heart ph-bold ph-lg"></i></span>
+				<span v-if="reactionAcceptance === 'likeOnly'"><i class="ti ti-heart"></i></span>
+				<span v-else-if="reactionAcceptance === 'likeOnlyForRemote'"><i class="ti ti-heart-plus"></i></span>
 				<span v-else><i class="ph-smiley ph-bold ph-lg"></i></span>
 			</button>
 			<button v-click-anime class="_button" :class="$style.submit" :disabled="!canPost" data-cy-open-post-form-submit @click="post">
@@ -46,22 +46,22 @@ SPDX-License-Identifier: AGPL-3.0-only
 					<template v-if="posted"></template>
 					<template v-else-if="posting"><MkEllipsis/></template>
 					<template v-else>{{ submitText }}</template>
-					<i style="margin-left: 6px;" :class="posted ? 'ph-check ph-bold ph-lg' : reply ? 'ph-arrow-u-up-left ph-bold ph-lg' : renote ? 'ph-quotes ph-bold ph-lg' : 'ph-paper-plane-tilt ph-bold ph-lg'"></i>
+					<i style="margin-left: 6px;" :class="posted ? 'ti ti-check' : reply ? 'ti ti-arrow-back-up' : renote ? 'ti ti-quote' : 'ti ti-send'"></i>
 				</div>
 			</button>
 		</div>
 	</header>
 	<MkNoteSimple v-if="reply" :class="$style.targetNote" :hideFiles="true" :note="reply"/>
 	<MkNoteSimple v-if="renote" :class="$style.targetNote" :hideFiles="true" :note="renote"/>
-	<div v-if="quoteId" :class="$style.withQuote"><i class="ph-quotes ph-bold ph-lg"></i> {{ i18n.ts.quoteAttached }}<button @click="quoteId = null"><i class="ph-x ph-bold ph-lg"></i></button></div>
+	<div v-if="quoteId" :class="$style.withQuote"><i class="ti ti-quote"></i> {{ i18n.ts.quoteAttached }}<button @click="quoteId = null"><i class="ti ti-x"></i></button></div>
 	<div v-if="visibility === 'specified'" :class="$style.toSpecified">
 		<span style="margin-right: 8px;">{{ i18n.ts.recipient }}</span>
 		<div :class="$style.visibleUsers">
 			<span v-for="u in visibleUsers" :key="u.id" :class="$style.visibleUser">
 				<MkAcct :user="u"/>
-				<button class="_button" style="padding: 4px 8px;" @click="removeVisibleUser(u)"><i class="ph-x ph-bold ph-lg"></i></button>
+				<button class="_button" style="padding: 4px 8px;" @click="removeVisibleUser(u)"><i class="ti ti-x"></i></button>
 			</span>
-			<button class="_buttonPrimary" style="padding: 4px; border-radius: var(--radius-sm);" @click="addVisibleUser"><i class="ph-plus ph-bold ph-lg ti-fw"></i></button>
+			<button class="_buttonPrimary" style="padding: 4px; border-radius: var(--radius-sm);" @click="addVisibleUser"><i class="ti ti-plus ti-fw"></i></button>
 		</div>
 	</div>
 	<MkInfo v-if="hasNotSpecifiedMentions" warn :class="$style.hasNotSpecifiedMentions">{{ i18n.ts.notSpecifiedMentionWarning }} - <button class="_textButton" @click="addMissingMention()">{{ i18n.ts.add }}</button></MkInfo>
@@ -79,19 +79,19 @@ SPDX-License-Identifier: AGPL-3.0-only
 	</div>
 	<footer :class="$style.footer">
 		<div :class="$style.footerLeft">
-			<button v-tooltip="i18n.ts.attachFile" class="_button" :class="$style.footerButton" @click="chooseFileFrom"><i class="ph-image-square ph-bold ph-lg-plus"></i></button>
-			<button v-tooltip="i18n.ts.poll" class="_button" :class="[$style.footerButton, { [$style.footerButtonActive]: poll }]" @click="togglePoll"><i class="ph-chart-bar-horizontal ph-bold ph-lg"></i></button>
-			<button v-tooltip="i18n.ts.useCw" class="_button" :class="[$style.footerButton, { [$style.footerButtonActive]: useCw }]" @click="useCw = !useCw"><i class="ph-eye-slash ph-bold ph-lg"></i></button>
-			<button v-tooltip="i18n.ts.mention" class="_button" :class="$style.footerButton" @click="insertMention"><i class="ph-at ph-bold ph-lg"></i></button>
-			<button v-tooltip="i18n.ts.hashtags" class="_button" :class="[$style.footerButton, { [$style.footerButtonActive]: withHashtags }]" @click="withHashtags = !withHashtags"><i class="ph-hash ph-bold ph-lg"></i></button>
-			<button v-if="postFormActions.length > 0" v-tooltip="i18n.ts.plugins" class="_button" :class="$style.footerButton" @click="showActions"><i class="ph-plug ph-bold ph-lg"></i></button>
-			<button v-tooltip="i18n.ts.emoji" :class="['_button', $style.footerButton]" @click="insertEmoji"><i class="ph-smiley ph-bold ph-lg"></i></button>
-			<button v-if="showAddMfmFunction" v-tooltip="i18n.ts.addMfmFunction" :class="['_button', $style.footerButton]" @click="insertMfmFunction"><i class="ph-palette ph-bold ph-lg"></i></button>
+			<button v-tooltip="i18n.ts.attachFile" class="_button" :class="$style.footerButton" @click="chooseFileFrom"><i class="ti ti-photo-plus"></i></button>
+			<button v-tooltip="i18n.ts.poll" class="_button" :class="[$style.footerButton, { [$style.footerButtonActive]: poll }]" @click="togglePoll"><i class="ti ti-chart-arrows"></i></button>
+			<button v-tooltip="i18n.ts.useCw" class="_button" :class="[$style.footerButton, { [$style.footerButtonActive]: useCw }]" @click="useCw = !useCw"><i class="ti ti-eye-off"></i></button>
+			<button v-tooltip="i18n.ts.mention" class="_button" :class="$style.footerButton" @click="insertMention"><i class="ti ti-at"></i></button>
+			<button v-tooltip="i18n.ts.hashtags" class="_button" :class="[$style.footerButton, { [$style.footerButtonActive]: withHashtags }]" @click="withHashtags = !withHashtags"><i class="ti ti-hash"></i></button>
+			<button v-if="postFormActions.length > 0" v-tooltip="i18n.ts.plugins" class="_button" :class="$style.footerButton" @click="showActions"><i class="ti ti-plug"></i></button>
+			<button v-tooltip="i18n.ts.emoji" :class="['_button', $style.footerButton]" @click="insertEmoji"><i class="ti ti-mood-happy"></i></button>
+			<button v-if="showAddMfmFunction" v-tooltip="i18n.ts.addMfmFunction" :class="['_button', $style.footerButton]" @click="insertMfmFunction"><i class="ti ti-palette"></i></button>
 		</div>
 		<div :class="$style.footerRight">
-			<button v-tooltip="i18n.ts.previewNoteText" class="_button" :class="[$style.footerButton, { [$style.previewButtonActive]: showPreview }]" @click="showPreview = !showPreview"><i class="ph-eye ph-bold ph-lg"></i></button>
+			<button v-tooltip="i18n.ts.previewNoteText" class="_button" :class="[$style.footerButton, { [$style.previewButtonActive]: showPreview }]" @click="showPreview = !showPreview"><i class="ti ti-eye"></i></button>
 			<button v-tooltip="'MFM Cheatsheet'" class="_button" :class="$style.footerButton" @click="MFMWindow"><i class="ph-notebook ph-bold ph-lg"></i></button>
-			<!--<button v-tooltip="i18n.ts.more" class="_button" :class="$style.footerButton" @click="showingOptions = !showingOptions"><i class="ph-dots-three ph-bold ph-lg"></i></button>-->
+			<!--<button v-tooltip="i18n.ts.more" class="_button" :class="$style.footerButton" @click="showingOptions = !showingOptions"><i class="ti ti-dots"></i></button>-->
 		</div>
 	</footer>
 	<datalist id="hashtags">
@@ -261,7 +261,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);
@@ -369,10 +369,17 @@ function watchForDraft() {
 	watch(files, () => saveDraft(), { deep: true });
 	watch(visibility, () => saveDraft());
 	watch(localOnly, () => saveDraft());
+	watch(quoteId, () => saveDraft());
+	watch(reactionAcceptance, () => saveDraft());
 }
 
 function MFMWindow() {
-	os.popup(defineAsyncComponent(() => import('@/components/MkMfmWindow.vue')), {}, {}, 'closed');
+	const { dispose } = os.popup(
+		defineAsyncComponent(() => import('@/components/SkMfmWindow.vue')),
+		{},
+		{
+			closed: () => dispose(),
+		});
 }
 
 function checkMissingMention() {
@@ -469,7 +476,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,
@@ -482,7 +489,8 @@ function setVisibility() {
 				defaultStore.set('visibility', visibility.value);
 			}
 		},
-	}, 'closed');
+		closed: () => dispose(),
+	});
 }
 
 async function toggleLocalOnly() {
@@ -575,6 +583,7 @@ function clear() {
 
 function onKeydown(ev: KeyboardEvent) {
 	if (ev.key === 'Enter' && (ev.ctrlKey || ev.metaKey) && canPost.value) post();
+
 	if (ev.key === 'Escape') emit('esc');
 }
 
@@ -630,8 +639,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`);
 		});
 	}
@@ -707,6 +716,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,
 		},
 	};
 
@@ -737,7 +748,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(),
+			});
 		}
 	}
 
@@ -773,11 +786,13 @@ async function post(ev?: MouseEvent) {
 			visibility.value = 'home';
 		}
 	}
-	
+
 	if (defaultStore.state.warnMissingAltText) {
 		const filesData = toRaw(files.value);
 
-		const isMissingAltText = filesData.some(file => !file.comment);
+		const isMissingAltText = filesData.filter(
+			file => file.type.startsWith('image/') || file.type.startsWith('video/') || file.type.startsWith('audio/')
+		).some(file => !file.comment);
 
 		if (isMissingAltText) {
 			const { canceled, result } = await os.actions({
@@ -793,7 +808,7 @@ async function post(ev?: MouseEvent) {
 			});
 
 			if (canceled) return;
-			if (result === 'cancel') return;	
+			if (result === 'cancel') return;
 		}
 	}
 
@@ -928,10 +943,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;
@@ -1024,6 +1052,8 @@ onMounted(() => {
 						users.forEach(u => pushVisibleUser(u));
 					});
 				}
+				quoteId.value = draft.data.quoteId;
+				reactionAcceptance.value = draft.data.reactionAcceptance;
 			}
 		}
 
@@ -1031,9 +1061,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),
@@ -1042,9 +1074,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());
@@ -1117,6 +1153,15 @@ defineExpose({
 	margin: 12px 12px 12px 6px;
 	vertical-align: bottom;
 
+	&:focus-visible {
+		outline: none;
+
+		.submitInner {
+			outline: 2px solid var(--fgOnAccent);
+			outline-offset: -4px;
+		}
+	}
+
 	&:disabled {
 		opacity: 0.7;
 	}
diff --git a/packages/frontend/src/components/MkPostFormAttaches.vue b/packages/frontend/src/components/MkPostFormAttaches.vue
index 956dad8021..a3fb7c691f 100644
--- a/packages/frontend/src/components/MkPostFormAttaches.vue
+++ b/packages/frontend/src/components/MkPostFormAttaches.vue
@@ -10,7 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 			<div :class="$style.file" @click="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="ph-eye-closed ph-bold ph-lg" style="margin: auto;"></i>
+					<i class="ti ti-eye-exclamation" style="margin: auto;"></i>
 				</div>
 			</div>
 		</template>
@@ -108,7 +108,7 @@ async function rename(file) {
 async function describe(file) {
 	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 +121,8 @@ async function describe(file) {
 				file.comment = comment;
 			});
 		},
-	}, 'closed');
+		closed: () => dispose(),
+	});
 }
 
 async function crop(file: Misskey.entities.DriveFile): Promise<void> {
@@ -137,29 +138,29 @@ function showFileMenu(file: Misskey.entities.DriveFile, ev: MouseEvent): void {
 	const isImage = file.type.startsWith('image/');
 	os.popupMenu([{
 		text: i18n.ts.renameFile,
-		icon: 'ph-textbox ph-bold ph-lg',
+		icon: 'ti ti-forms',
 		action: () => { rename(file); },
 	}, {
 		text: file.isSensitive ? i18n.ts.unmarkAsSensitive : i18n.ts.markAsSensitive,
-		icon: file.isSensitive ? 'ph-eye-closed ph-bold ph-lg' : 'ph-eye ph-bold ph-lg',
+		icon: file.isSensitive ? 'ti ti-eye-exclamation' : 'ti ti-eye',
 		action: () => { toggleSensitive(file); },
 	}, {
 		text: i18n.ts.describeFile,
-		icon: 'ph-text-indent ph-bold ph-lg',
+		icon: 'ti ti-text-caption',
 		action: () => { describe(file); },
 	}, ...isImage ? [{
 		text: i18n.ts.cropImage,
-		icon: 'ph-crop ph-bold ph-lg',
+		icon: 'ti ti-crop',
 		action: () : void => { crop(file); },
 	}] : [], {
 		type: 'divider',
 	}, {
 		text: i18n.ts.attachCancel,
-		icon: 'ph-x-circle ph-bold ph-lg',
+		icon: 'ti ti-circle-x',
 		action: () => { detachMedia(file.id); },
 	}, {
 		text: i18n.ts.deleteFile,
-		icon: 'ph-trash ph-bold ph-lg',
+		icon: 'ti ti-trash',
 		danger: true,
 		action: () => { detachAndDeleteMedia(file); },
 	}], ev.currentTarget ?? ev.target).then(() => menuShowing = false);
diff --git a/packages/frontend/src/components/MkPostFormDialog.vue b/packages/frontend/src/components/MkPostFormDialog.vue
index ad990e21db..947c0ee4d0 100644
--- a/packages/frontend/src/components/MkPostFormDialog.vue
+++ b/packages/frontend/src/components/MkPostFormDialog.vue
@@ -4,7 +4,7 @@ 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>
diff --git a/packages/frontend/src/components/MkPreview.vue b/packages/frontend/src/components/MkPreview.vue
new file mode 100644
index 0000000000..649dee2fdb
--- /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 '@/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 b1ec440e42..e0d0b561be 100644
--- a/packages/frontend/src/components/MkPullToRefresh.vue
+++ b/packages/frontend/src/components/MkPullToRefresh.vue
@@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 	<div v-if="isPullStart" :class="$style.frame" :style="`--frame-min-height: ${pullDistance / (PULL_BRAKE_BASE + (pullDistance / PULL_BRAKE_FACTOR))}px;`">
 		<div :class="$style.frameContent">
 			<MkLoading v-if="isRefreshing" :class="$style.loader" :em="true"/>
-			<i v-else class="ph-arrow-line-down ph-bold ph-lg" :class="[$style.icon, { [$style.refresh]: isPullEnd }]"></i>
+			<i v-else class="ti ti-arrow-bar-to-down" :class="[$style.icon, { [$style.refresh]: isPullEnd }]"></i>
 			<div :class="$style.text">
 				<template v-if="isPullEnd">{{ i18n.ts.releaseToRefresh }}</template>
 				<template v-else-if="isRefreshing">{{ i18n.ts.refreshing }}</template>
diff --git a/packages/frontend/src/components/MkRadio.vue b/packages/frontend/src/components/MkRadio.vue
index 0b4023f254..e02f76a58f 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
@@ -69,6 +70,11 @@ function toggle(): void {
 		border-color: var(--inputBorderHover) !important;
 	}
 
+	&:focus-within {
+		outline: none;
+		box-shadow: 0 0 0 2px var(--focus);
+	}
+
 	&.checked {
 		background-color: var(--accentedBg) !important;
 		border-color: var(--accentedBg) !important;
@@ -78,7 +84,7 @@ function toggle(): void {
 		> .button {
 			border-color: var(--accent);
 
-			&:after {
+			&::after {
 				background-color: var(--accent);
 				transform: scale(1);
 				opacity: 1;
@@ -104,7 +110,7 @@ function toggle(): void {
 	border-radius: var(--radius-full);
 	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..705c93f770 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)),
diff --git a/packages/frontend/src/components/MkRange.vue b/packages/frontend/src/components/MkRange.vue
index 46d76e2551..244fcdceae 100644
--- a/packages/frontend/src/components/MkRange.vue
+++ b/packages/frontend/src/components/MkRange.vue
@@ -101,17 +101,19 @@ const steps = computed(() => {
 	}
 });
 
-const onMousedown = (ev: MouseEvent | TouchEvent) => {
+function onMousedown(ev: MouseEvent | TouchEvent) {
 	ev.preventDefault();
 
 	const tooltipShowing = ref(true);
-	os.popup(defineAsyncComponent(() => import('@/components/MkTooltip.vue')), {
+	const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkTooltip.vue')), {
 		showing: tooltipShowing,
 		text: computed(() => {
 			return props.textConverter(finalValue.value);
 		}),
 		targetElement: thumbEl,
-	}, {}, 'closed');
+	}, {
+		closed: () => dispose(),
+	});
 
 	const style = document.createElement('style');
 	style.appendChild(document.createTextNode('* { cursor: grabbing !important; } body * { pointer-events: none !important; }'));
@@ -152,7 +154,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>
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/MkReactionsViewer.details.vue b/packages/frontend/src/components/MkReactionsViewer.details.vue
index 8b5e6efdf3..60118fadd2 100644
--- a/packages/frontend/src/components/MkReactionsViewer.details.vue
+++ b/packages/frontend/src/components/MkReactionsViewer.details.vue
@@ -81,6 +81,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 2464d21b6a..6506035f8f 100644
--- a/packages/frontend/src/components/MkReactionsViewer.reaction.vue
+++ b/packages/frontend/src/components/MkReactionsViewer.reaction.vue
@@ -112,12 +112,14 @@ async function menu(ev) {
 
 	os.popupMenu([{
 		text: i18n.ts.info,
-		icon: 'ph-info ph-bold ph-lg',
+		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>
diff --git a/packages/frontend/src/components/MkRemoteCaution.vue b/packages/frontend/src/components/MkRemoteCaution.vue
index 5106cdfd6a..2b59eab9d9 100644
--- a/packages/frontend/src/components/MkRemoteCaution.vue
+++ b/packages/frontend/src/components/MkRemoteCaution.vue
@@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 -->
 
 <template>
-<div :class="$style.root"><i class="ph-warning ph-bold ph-lg" style="margin-right: 8px;"></i>{{ i18n.ts.remoteUserCaution }}<a :class="$style.link" :href="href" rel="nofollow noopener" target="_blank">{{ i18n.ts.showOnRemote }}</a></div>
+<div :class="$style.root"><i class="ti ti-alert-triangle" style="margin-right: 8px;"></i>{{ i18n.ts.remoteUserCaution }}<a :class="$style.link" :href="href" rel="nofollow noopener" target="_blank">{{ i18n.ts.showOnRemote }}</a></div>
 </template>
 
 <script lang="ts" setup>
diff --git a/packages/frontend/src/components/MkRolePreview.vue b/packages/frontend/src/components/MkRolePreview.vue
index f0343d499b..ce17ae08e0 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(--success)"></i>
+		<i v-else class="ti ti-lock" :class="$style.icon" style="color: var(--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(--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.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="ph-crown ph-bold ph-lg" style="color: var(--accent);"></i>
-				<i v-else-if="role.isModerator" class="ph-shield ph-bold ph-lg" style="color: var(--accent);"></i>
-				<i v-else class="ph-user ph-bold ph-lg" 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 ecac99ae45..8254ac83cf 100644
--- a/packages/frontend/src/components/MkSelect.vue
+++ b/packages/frontend/src/components/MkSelect.vue
@@ -6,28 +6,37 @@ 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
 			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 ref="suffixEl" :class="$style.suffix"><i class="ph-caret-down ph-bold ph-lg" :class="[$style.chevron, { [$style.chevronOpening]: opening }]"></i></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="ph-floppy-disk ph-bold ph-lg"></i> {{ i18n.ts.save }}</MkButton>
+	<MkButton v-if="manualSave && changed" primary :class="$style.save" @click="updated"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton>
 </div>
 </template>
 
@@ -75,7 +84,7 @@ const height =
 	props.large ? 39 :
 	36;
 
-const focus = () => inputEl.value?.focus();
+const focus = () => container.value?.focus();
 const onInput = (ev) => {
 	changed.value = true;
 };
@@ -126,7 +135,9 @@ onMounted(() => {
 });
 
 function show() {
-	focused.value = true;
+	if (opening.value) return;
+	focus();
+
 	opening.value = true;
 
 	const menu: MenuItem[] = [];
@@ -173,8 +184,6 @@ function show() {
 		onClosing: () => {
 			opening.value = false;
 		},
-	}).then(() => {
-		focused.value = false;
 	});
 }
 </script>
@@ -225,6 +234,10 @@ function show() {
 		}
 	}
 
+	&:focus {
+		outline: none;
+	}
+
 	&:hover {
 		> .inputCore {
 			border-color: var(--inputBorderHover) !important;
diff --git a/packages/frontend/src/components/MkSignin.vue b/packages/frontend/src/components/MkSignin.vue
index 6f7994dccb..42fa2bf4a7 100644
--- a/packages/frontend/src/components/MkSignin.vue
+++ b/packages/frontend/src/components/MkSignin.vue
@@ -6,17 +6,30 @@ 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>
+		<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="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>
 		<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="ph-lock ph-bold ph-lg"></i></template>
+				<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>
@@ -28,17 +41,17 @@ SPDX-License-Identifier: AGPL-3.0-only
 					{{ i18n.ts.retry }}
 				</MkButton>
 			</div>
-			<div v-if="user && user.securityKeys" class="or-hr">
-				<p class="or-msg">{{ i18n.ts.or }}</p>
+			<div v-if="user && user.securityKeys" :class="$style.orHr">
+				<p :class="$style.orMsg">{{ 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="ph-lock ph-bold ph-lg"></i></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="ph-keyhole ph-bold ph-lg"></i><i v-else class="ph-numpad ph-bold ph-lg"></i></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>
@@ -53,6 +66,7 @@ import { defineAsyncComponent, ref } from 'vue';
 import { toUnicode } from 'punycode/';
 import * as Misskey from 'misskey-js';
 import { supported as webAuthnSupported, get as webAuthnRequest, parseRequestOptionsFromJSON } from '@github/webauthn-json/browser-ponyfill';
+import type { OpenOnRemoteOptions } from '@/scripts/please-login.js';
 import { showSuspendedDialog } from '@/scripts/show-suspended-dialog.js';
 import MkButton from '@/components/MkButton.vue';
 import MkInput from '@/components/MkInput.vue';
@@ -60,6 +74,7 @@ import MkInfo from '@/components/MkInfo.vue';
 import { host as configHost } from '@/config.js';
 import * as os from '@/os.js';
 import { misskeyApi } from '@/scripts/misskey-api.js';
+import { query, extractDomain } from '@/scripts/url.js';
 import { login } from '@/account.js';
 import { i18n } from '@/i18n.js';
 
@@ -72,28 +87,22 @@ const host = ref(toUnicode(configHost));
 const totpLogin = ref(false);
 const isBackupCode = ref(false);
 const queryingKey = ref(false);
-const credentialRequest = ref<CredentialRequestOptions | null>(null);
+let credentialRequest: CredentialRequestOptions | null = null;
 
 const emit = defineEmits<{
 	(ev: 'login', v: any): 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<{
+	withAvatar?: boolean;
+	autoSet?: boolean;
+	message?: string,
+	openOnRemote?: OpenOnRemoteOptions,
+}>(), {
+	withAvatar: true,
+	autoSet: false,
+	message: '',
+	openOnRemote: undefined,
 });
 
 function onUsernameChange(): void {
@@ -113,14 +122,14 @@ function onLogin(res: any): Promise<void> | void {
 }
 
 async function queryKey(): Promise<void> {
-	if (credentialRequest.value == null) return;
+	if (credentialRequest == null) return;
 	queryingKey.value = true;
-	await webAuthnRequest(credentialRequest.value)
+	await webAuthnRequest(credentialRequest)
 		.catch(() => {
 			queryingKey.value = false;
 			return Promise.reject(null);
 		}).then(credential => {
-			credentialRequest.value = null;
+			credentialRequest = null;
 			queryingKey.value = false;
 			signing.value = true;
 			return misskeyApi('signin', {
@@ -151,7 +160,7 @@ function onSubmit(): void {
 			}).then(res => {
 				totpLogin.value = true;
 				signing.value = false;
-				credentialRequest.value = parseRequestOptionsFromJSON({
+				credentialRequest = parseRequestOptionsFromJSON({
 					publicKey: res,
 				});
 			})
@@ -218,8 +227,65 @@ function loginFailed(err: any): void {
 }
 
 function resetPassword(): void {
-	os.popup(defineAsyncComponent(() => import('@/components/MkForgotPassword.vue')), {}, {
-	}, 'closed');
+	const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkForgotPassword.vue')), {}, {
+		closed: () => dispose(),
+	});
+}
+
+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);
 }
 </script>
 
@@ -233,4 +299,36 @@ function resetPassword(): void {
 	background-size: cover;
 	border-radius: var(--radius-full);
 }
+
+.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(--divider);
+}
+
+.orMsg {
+	position: absolute;
+	top: -.6em;
+	display: inline-block;
+	padding: 0 1em;
+	background: var(--panel);
+	font-size: 0.8em;
+	color: var(--fgOnPanel);
+	margin: 0;
+	left: 50%;
+	transform: translateX(-50%);
+}
 </style>
diff --git a/packages/frontend/src/components/MkSigninDialog.vue b/packages/frontend/src/components/MkSigninDialog.vue
index 33355bb99e..524c62b4d3 100644
--- a/packages/frontend/src/components/MkSigninDialog.vue
+++ b/packages/frontend/src/components/MkSigninDialog.vue
@@ -6,21 +6,22 @@ SPDX-License-Identifier: AGPL-3.0-only
 <template>
 <MkModalWindow
 	ref="dialog"
-	:width="370"
-	:height="400"
+	:width="400"
+	:height="430"
 	@close="onClose"
 	@closed="emit('closed')"
 >
 	<template #header>{{ i18n.ts.login }}</template>
 
 	<MkSpacer :marginMin="20" :marginMax="28">
-		<MkSignin :autoSet="autoSet" :message="message" @login="onLogin"/>
+		<MkSignin :autoSet="autoSet" :message="message" :openOnRemote="openOnRemote" @login="onLogin"/>
 	</MkSpacer>
 </MkModalWindow>
 </template>
 
 <script lang="ts" setup>
 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 { i18n } from '@/i18n.js';
@@ -28,9 +29,11 @@ import { i18n } from '@/i18n.js';
 withDefaults(defineProps<{
 	autoSet?: boolean;
 	message?: string,
+	openOnRemote?: OpenOnRemoteOptions,
 }>(), {
 	autoSet: false,
 	message: '',
+	openOnRemote: undefined,
 });
 
 const emit = defineEmits<{
diff --git a/packages/frontend/src/components/MkSignupDialog.form.vue b/packages/frontend/src/components/MkSignupDialog.form.vue
index 7d03381a49..e673b6d530 100644
--- a/packages/frontend/src/components/MkSignupDialog.form.vue
+++ b/packages/frontend/src/components/MkSignupDialog.form.vue
@@ -6,60 +6,60 @@ SPDX-License-Identifier: AGPL-3.0-only
 <template>
 <div>
 	<div :class="$style.banner">
-		<i class="ph-user-list ph-bold ph-lg"></i>
+		<i class="ti ti-user-edit"></i>
 	</div>
 	<MkSpacer :marginMin="20" :marginMax="32">
 		<form class="_gaps_m" autocomplete="new-password" @submit.prevent="onSubmit">
 			<MkInput v-if="instance.disableRegistration" v-model="invitationCode" type="text" :spellcheck="false" required>
 				<template #label>{{ i18n.ts.invitationCode }}</template>
-				<template #prefix><i class="ph-key ph-bold ph-lg"></i></template>
+				<template #prefix><i class="ti ti-key"></i></template>
 			</MkInput>
 			<MkInput v-model="username" type="text" pattern="^[a-zA-Z0-9_]{1,20}$" :spellcheck="false" autocomplete="username" required data-cy-signup-username @update:modelValue="onChangeUsername">
-				<template #label>{{ i18n.ts.username }} <div v-tooltip:dialog="i18n.ts.usernameInfo" class="_button _help"><i class="ph-question ph-bold ph-lg"></i></div></template>
+				<template #label>{{ i18n.ts.username }} <div v-tooltip:dialog="i18n.ts.usernameInfo" class="_button _help"><i class="ti ti-help-circle"></i></div></template>
 				<template #prefix>@</template>
 				<template #suffix>@{{ host }}</template>
 				<template #caption>
-					<div><i class="ph-warning ph-bold ph-lg ti-fw"></i> {{ i18n.ts.cannotBeChangedLater }}</div>
+					<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="ph-check ph-bold ph-lg ti-fw"></i> {{ i18n.ts.available }}</span>
-					<span v-else-if="usernameState === 'unavailable'" style="color: var(--error)"><i class="ph-warning ph-bold ph-lg ti-fw"></i> {{ i18n.ts.unavailable }}</span>
-					<span v-else-if="usernameState === 'error'" style="color: var(--error)"><i class="ph-warning ph-bold ph-lg ti-fw"></i> {{ i18n.ts.error }}</span>
-					<span v-else-if="usernameState === 'invalid-format'" style="color: var(--error)"><i class="ph-warning ph-bold ph-lg ti-fw"></i> {{ i18n.ts.usernameInvalidFormat }}</span>
-					<span v-else-if="usernameState === 'min-range'" style="color: var(--error)"><i class="ph-warning ph-bold ph-lg ti-fw"></i> {{ i18n.ts.tooShort }}</span>
-					<span v-else-if="usernameState === 'max-range'" style="color: var(--error)"><i class="ph-warning ph-bold ph-lg ti-fw"></i> {{ i18n.ts.tooLong }}</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>
 				</template>
 			</MkInput>
 			<MkInput v-if="instance.emailRequiredForSignup" v-model="email" :debounce="true" type="email" :spellcheck="false" required data-cy-signup-email @update:modelValue="onChangeEmail">
-				<template #label>{{ i18n.ts.emailAddress }} <div v-tooltip:dialog="i18n.ts._signup.emailAddressInfo" class="_button _help"><i class="ph-question ph-bold ph-lg"></i></div></template>
-				<template #prefix><i class="ph-envelope ph-bold ph-lg"></i></template>
+				<template #label>{{ i18n.ts.emailAddress }} <div v-tooltip:dialog="i18n.ts._signup.emailAddressInfo" class="_button _help"><i class="ti ti-help-circle"></i></div></template>
+				<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="ph-check ph-bold ph-lg ti-fw"></i> {{ i18n.ts.available }}</span>
-					<span v-else-if="emailState === 'unavailable:used'" style="color: var(--error)"><i class="ph-warning ph-bold ph-lg ti-fw"></i> {{ i18n.ts._emailUnavailable.used }}</span>
-					<span v-else-if="emailState === 'unavailable:format'" style="color: var(--error)"><i class="ph-warning ph-bold ph-lg ti-fw"></i> {{ i18n.ts._emailUnavailable.format }}</span>
-					<span v-else-if="emailState === 'unavailable:disposable'" style="color: var(--error)"><i class="ph-warning ph-bold ph-lg ti-fw"></i> {{ i18n.ts._emailUnavailable.disposable }}</span>
-					<span v-else-if="emailState === 'unavailable:banned'" style="color: var(--error)"><i class="ph-warning ph-bold ph-lg ti-fw"></i> {{ i18n.ts._emailUnavailable.banned }}</span>
-					<span v-else-if="emailState === 'unavailable:mx'" style="color: var(--error)"><i class="ph-warning ph-bold ph-lg ti-fw"></i> {{ i18n.ts._emailUnavailable.mx }}</span>
-					<span v-else-if="emailState === 'unavailable:smtp'" style="color: var(--error)"><i class="ph-warning ph-bold ph-lg ti-fw"></i> {{ i18n.ts._emailUnavailable.smtp }}</span>
-					<span v-else-if="emailState === 'unavailable'" style="color: var(--error)"><i class="ph-warning ph-bold ph-lg ti-fw"></i> {{ i18n.ts.unavailable }}</span>
-					<span v-else-if="emailState === 'error'" style="color: var(--error)"><i class="ph-warning ph-bold ph-lg ti-fw"></i> {{ i18n.ts.error }}</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>
 				</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="ph-lock ph-bold ph-lg"></i></template>
+				<template #prefix><i class="ti ti-lock"></i></template>
 				<template #caption>
-					<span v-if="passwordStrength == 'low'" style="color: var(--error)"><i class="ph-warning ph-bold ph-lg ti-fw"></i> {{ i18n.ts.weakPassword }}</span>
-					<span v-if="passwordStrength == 'medium'" style="color: var(--warn)"><i class="ph-check ph-bold ph-lg ti-fw"></i> {{ i18n.ts.normalPassword }}</span>
-					<span v-if="passwordStrength == 'high'" style="color: var(--success)"><i class="ph-check ph-bold ph-lg ti-fw"></i> {{ i18n.ts.strongPassword }}</span>
+					<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>
 				</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="ph-lock ph-bold ph-lg"></i></template>
+				<template #prefix><i class="ti ti-lock"></i></template>
 				<template #caption>
-					<span v-if="passwordRetypeState == 'match'" style="color: var(--success)"><i class="ph-check ph-bold ph-lg ti-fw"></i> {{ i18n.ts.passwordMatched }}</span>
-					<span v-if="passwordRetypeState == 'not-match'" style="color: var(--error)"><i class="ph-warning ph-bold ph-lg ti-fw"></i> {{ i18n.ts.passwordNotMatched }}</span>
+					<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>
 				</template>
 			</MkInput>
 			<MkInput v-if="instance.approvalRequiredForSignup" v-model="reason" type="text" :spellcheck="false" required data-cy-signup-reason>
diff --git a/packages/frontend/src/components/MkSignupDialog.rules.vue b/packages/frontend/src/components/MkSignupDialog.rules.vue
index c2435b308f..251c805401 100644
--- a/packages/frontend/src/components/MkSignupDialog.rules.vue
+++ b/packages/frontend/src/components/MkSignupDialog.rules.vue
@@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 <template>
 <div>
 	<div :class="$style.banner">
-		<i class="ph-check ph-bold ph-lglist"></i>
+		<i class="ti ti-checklist"></i>
 	</div>
 	<MkSpacer :marginMin="20" :marginMax="28">
 		<div class="_gaps_m">
@@ -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="ph-check ph-bold ph-lg" style="color: var(--success)"></i></template>
+				<template #suffix><i v-if="agreeServerRules" class="ti ti-check" style="color: var(--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="sanitizeHtml(item)"></div></li>
@@ -32,10 +32,10 @@ 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="ph-check ph-bold ph-lg" style="color: var(--success)"></i></template>
+				<template #suffix><i v-if="agreeTosAndPrivacyPolicy" class="ti ti-check" style="color: var(--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="ph-arrow-square-out ph-bold ph-lg"></i></a></div>
-					<div v-if="availablePrivacyPolicy"><a :href="instance.privacyPolicyUrl ?? undefined" class="_link" target="_blank">{{ i18n.ts.privacyPolicy }} <i class="ph-arrow-square-out ph-bold ph-lg"></i></a></div>
+					<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>
 				</div>
 
 				<MkSwitch :modelValue="agreeTosAndPrivacyPolicy" style="margin-top: 16px;" @update:modelValue="updateAgreeTosAndPrivacyPolicy">{{ i18n.ts.agree }}</MkSwitch>
@@ -43,9 +43,9 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 			<MkFolder :defaultOpen="true">
 				<template #label>{{ i18n.ts.basicNotesBeforeCreateAccount }}</template>
-				<template #suffix><i v-if="agreeNote" class="ph-check ph-bold ph-lg" style="color: var(--success)"></i></template>
+				<template #suffix><i v-if="agreeNote" class="ti ti-check" style="color: var(--success)"></i></template>
 
-				<a href="https://activitypub.software/TransFem-org/Sharkey/-/blob/stable/IMPORTANT_NOTES.md" class="_link" target="_blank">{{ i18n.ts.basicNotesBeforeCreateAccount }} <i class="ph-arrow-square-out ph-bold ph-lg"></i></a>
+				<a href="https://activitypub.software/TransFem-org/Sharkey/-/blob/stable/IMPORTANT_NOTES.md" class="_link" target="_blank">{{ i18n.ts.basicNotesBeforeCreateAccount }} <i class="ti ti-external-link"></i></a>
 
 				<MkSwitch :modelValue="agreeNote" style="margin-top: 16px;" data-cy-signup-rules-notes-agree @update:modelValue="updateAgreeNote">{{ i18n.ts.agree }}</MkSwitch>
 			</MkFolder>
@@ -54,7 +54,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 			<div class="_buttonsCenter">
 				<MkButton inline rounded @click="emit('cancel')">{{ i18n.ts.cancel }}</MkButton>
-				<MkButton inline primary rounded gradate :disabled="!agreed" data-cy-signup-rules-continue @click="emit('done')">{{ i18n.ts.continue }} <i class="ph-arrow-right ph-bold ph-lg"></i></MkButton>
+				<MkButton inline primary rounded gradate :disabled="!agreed" data-cy-signup-rules-continue @click="emit('done')">{{ i18n.ts.continue }} <i class="ti ti-arrow-right"></i></MkButton>
 			</div>
 		</div>
 	</MkSpacer>
diff --git a/packages/frontend/src/components/MkSourceCodeAvailablePopup.vue b/packages/frontend/src/components/MkSourceCodeAvailablePopup.vue
index 7b936b656c..cd884f0b19 100644
--- a/packages/frontend/src/components/MkSourceCodeAvailablePopup.vue
+++ b/packages/frontend/src/components/MkSourceCodeAvailablePopup.vue
@@ -35,7 +35,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 			<MkButton @click="close">{{ i18n.ts.gotIt }}</MkButton>
 		</div>
 	</div>
-	<button class="_button" :class="$style.close" @click="close"><i class="ph-x ph-bold ph-lg"></i></button>
+	<button class="_button" :class="$style.close" @click="close"><i class="ti ti-x"></i></button>
 </div>
 </template>
 
diff --git a/packages/frontend/src/components/MkSuperMenu.vue b/packages/frontend/src/components/MkSuperMenu.vue
index 2a7c72ccd9..041ae88109 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>
@@ -67,6 +67,10 @@ defineProps<{
 					background: var(--panelHighlight);
 				}
 
+				&:focus-visible {
+					outline-offset: -2px;
+				}
+
 				&.active {
 					color: var(--accent);
 					background: var(--accentedBg);
diff --git a/packages/frontend/src/components/MkSwitch.vue b/packages/frontend/src/components/MkSwitch.vue
index 5672c8e9f7..a0994d9cc9 100644
--- a/packages/frontend/src/components/MkSwitch.vue
+++ b/packages/frontend/src/components/MkSwitch.vue
@@ -10,16 +10,16 @@ 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">
 				<slot name="label"></slot><slot></slot>
 			</span>
-			<span v-if="helpText" v-tooltip:dialog="helpText" class="_button _help" :class="$style.help"><i class="ph-question ph-bold ph-lg"></i></span>
+			<span v-if="helpText" v-tooltip:dialog="helpText" class="_button _help" :class="$style.help"><i class="ti ti-help-circle"></i></span>
 		</span>
 		<p :class="$style.caption"><slot name="caption"></slot></p>
 	</span>
@@ -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>
 
@@ -72,7 +75,13 @@ const toggle = () => {
 	height: 0;
 	opacity: 0;
 	margin: 0;
+
+	&:focus-visible ~ .toggle {
+		outline: 2px solid var(--focus);
+		outline-offset: 2px;
+	}
 }
+
 .body {
 	margin-left: 12px;
 	margin-top: 2px;
diff --git a/packages/frontend/src/components/MkSystemWebhookEditor.impl.ts b/packages/frontend/src/components/MkSystemWebhookEditor.impl.ts
new file mode 100644
index 0000000000..69b8edd85a
--- /dev/null
+++ b/packages/frontend/src/components/MkSystemWebhookEditor.impl.ts
@@ -0,0 +1,46 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { defineAsyncComponent } from 'vue';
+import * as os from '@/os.js';
+
+export type SystemWebhookEventType = 'abuseReport' | 'abuseReportResolved';
+
+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..f5c7a3160b
--- /dev/null
+++ b/packages/frontend/src/components/MkSystemWebhookEditor.vue
@@ -0,0 +1,238 @@
+<!--
+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_s">
+						<MkSwitch v-model="events.abuseReport" :disabled="disabledEvents.abuseReport">
+							<template #label>{{ i18n.ts._webhookSettings._systemEvents.abuseReport }}</template>
+						</MkSwitch>
+						<MkSwitch v-model="events.abuseReportResolved" :disabled="disabledEvents.abuseReportResolved">
+							<template #label>{{ i18n.ts._webhookSettings._systemEvents.abuseReportResolved }}</template>
+						</MkSwitch>
+						<MkSwitch v-model="events.userCreated" :disabled="disabledEvents.userCreated">
+							<template #label>{{ i18n.ts._webhookSettings._systemEvents.userCreated }}</template>
+						</MkSwitch>
+					</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 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;
+}
+
+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,
+});
+const isActive = ref<boolean>(true);
+
+const disabledEvents = ref<EventType>({
+	abuseReport: false,
+	abuseReportResolved: false,
+	userCreated: 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--;
+	}
+}
+
+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(--divider);
+	background: var(--acrylicBg);
+	-webkit-backdrop-filter: var(--blur, blur(15px));
+	backdrop-filter: var(--blur, blur(15px));
+}
+</style>
diff --git a/packages/frontend/src/components/MkTextarea.vue b/packages/frontend/src/components/MkTextarea.vue
index 7b9fb3d8ad..72d6e12656 100644
--- a/packages/frontend/src/components/MkTextarea.vue
+++ b/packages/frontend/src/components/MkTextarea.vue
@@ -31,7 +31,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 		<Mfm :text="v" :isBlock="true" />
 	</div>
 
-	<MkButton v-if="manualSave && changed" primary :class="$style.save" @click="updated"><i class="ph-floppy-disk ph-bold ph-lg"></i> {{ i18n.ts.save }}</MkButton>
+	<MkButton v-if="manualSave && changed" primary :class="$style.save" @click="updated"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton>
 </div>
 </template>
 
diff --git a/packages/frontend/src/components/MkTimeline.vue b/packages/frontend/src/components/MkTimeline.vue
index 0f7eb3b86c..b69c19eb9e 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' | 'bubble' | 'global' | 'mentions' | 'directs' | 'list' | 'antenna' | 'channel' | 'role';
+	src: BasicTimelineType | 'mentions' | 'directs' | 'list' | 'antenna' | 'channel' | 'role';
 	list?: string;
 	antenna?: string;
 	channel?: string;
diff --git a/packages/frontend/src/components/MkTutorialDialog.Note.vue b/packages/frontend/src/components/MkTutorialDialog.Note.vue
index 725cfcdc33..cec7d69943 100644
--- a/packages/frontend/src/components/MkTutorialDialog.Note.vue
+++ b/packages/frontend/src/components/MkTutorialDialog.Note.vue
@@ -8,10 +8,10 @@ SPDX-License-Identifier: AGPL-3.0-only
 	<div style="text-align: center; padding: 0 16px;">{{ i18n.ts._initialTutorial._note.description }}</div>
 	<MkNote :class="$style.exampleNoteRoot" style="pointer-events: none;" :note="exampleNote" :mock="true"/>
 	<div class="_gaps_s">
-		<div><i class="ph-arrow-u-up-left ph-bold ph-lg"></i> <b>{{ i18n.ts.reply }}</b> … {{ i18n.ts._initialTutorial._note.reply }}</div>
-		<div><i class="ph-rocket-launch ph-bold ph-lg"></i> <b>{{ i18n.ts.renote }}</b> … {{ i18n.ts._initialTutorial._note.renote }}</div>
+		<div><i class="ti ti-arrow-back-up"></i> <b>{{ i18n.ts.reply }}</b> … {{ i18n.ts._initialTutorial._note.reply }}</div>
+		<div><i class="ti ti-repeat"></i> <b>{{ i18n.ts.renote }}</b> … {{ i18n.ts._initialTutorial._note.renote }}</div>
 		<div><i class="ph-smiley ph-bold ph-lg"></i> <b>{{ i18n.ts.reaction }}</b> … {{ i18n.ts._initialTutorial._note.reaction }}</div>
-		<div><i class="ph-dots-three ph-bold ph-lg"></i> <b>{{ i18n.ts.menu }}</b> … {{ i18n.ts._initialTutorial._note.menu }}</div>
+		<div><i class="ti ti-dots"></i> <b>{{ i18n.ts.menu }}</b> … {{ i18n.ts._initialTutorial._note.menu }}</div>
 	</div>
 </div>
 <div v-else-if="phase === 'howToReact'" class="_gaps">
@@ -23,7 +23,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 	</I18n>
 	<MkNote :class="$style.exampleNoteRoot" :note="exampleNote" :mock="true" @reaction="addReaction" @removeReaction="removeReaction"/>
 	<div v-if="onceReacted">
-		<b style="color: var(--accent);"><i class="ph-check ph-bold ph-lg"></i> {{ i18n.ts._initialTutorial.wellDone }}</b> {{ i18n.ts._initialTutorial._reaction.reactNotification }}<br>
+		<b style="color: var(--accent);"><i class="ti ti-check"></i> {{ i18n.ts._initialTutorial.wellDone }}</b> {{ i18n.ts._initialTutorial._reaction.reactNotification }}<br>
 		<I18n :src="i18n.ts._initialTutorial._reaction.reactDone">
 			<template #undo>
 				<i class="ph-minus ph-bold ph-lg"></i>
diff --git a/packages/frontend/src/components/MkTutorialDialog.PostNote.vue b/packages/frontend/src/components/MkTutorialDialog.PostNote.vue
index b0561d4bae..a9014d4202 100644
--- a/packages/frontend/src/components/MkTutorialDialog.PostNote.vue
+++ b/packages/frontend/src/components/MkTutorialDialog.PostNote.vue
@@ -11,16 +11,16 @@ SPDX-License-Identifier: AGPL-3.0-only
 		<template #label>{{ i18n.ts.visibility }}</template>
 		<div class="_gaps">
 			<div>{{ i18n.ts._initialTutorial._postNote._visibility.description }}</div>
-			<div><i class="ph-globe-hemisphere-west ph-bold ph-lg"></i> <b>{{ i18n.ts._visibility.public }}</b> … {{ i18n.ts._initialTutorial._postNote._visibility.public }}</div>
-			<div><i class="ph-house ph-bold ph-lg"></i> <b>{{ i18n.ts._visibility.home }}</b> … {{ i18n.ts._initialTutorial._postNote._visibility.home }}</div>
-			<div><i class="ph-lock ph-bold ph-lg"></i> <b>{{ i18n.ts._visibility.followers }}</b> … {{ i18n.ts._initialTutorial._postNote._visibility.followers }}</div>
+			<div><i class="ti ti-world"></i> <b>{{ i18n.ts._visibility.public }}</b> … {{ i18n.ts._initialTutorial._postNote._visibility.public }}</div>
+			<div><i class="ti ti-home"></i> <b>{{ i18n.ts._visibility.home }}</b> … {{ i18n.ts._initialTutorial._postNote._visibility.home }}</div>
+			<div><i class="ti ti-lock"></i> <b>{{ i18n.ts._visibility.followers }}</b> … {{ i18n.ts._initialTutorial._postNote._visibility.followers }}</div>
 			<div class="_gaps_s">
-				<div><i class="ph-envelope ph-bold ph-lg"></i> <b>{{ i18n.ts._visibility.specified }}</b> … {{ i18n.ts._initialTutorial._postNote._visibility.direct }}</div>
+				<div><i class="ti ti-mail"></i> <b>{{ i18n.ts._visibility.specified }}</b> … {{ i18n.ts._initialTutorial._postNote._visibility.direct }}</div>
 				<MkInfo :warn="true">
 					<b>{{ i18n.ts._initialTutorial._postNote._visibility.doNotSendConfidencialOnDirect1 }}</b> {{ i18n.ts._initialTutorial._postNote._visibility.doNotSendConfidencialOnDirect2 }}
 				</MkInfo>
 			</div>
-			<div><i class="ph-rocket ph-bold ph-lg"></i> <b>{{ i18n.ts._visibility.disableFederation }}</b> … {{ i18n.ts._initialTutorial._postNote._visibility.localOnly }}</div>
+			<div><i class="ti ti-rocket-off"></i> <b>{{ i18n.ts._visibility.disableFederation }}</b> … {{ i18n.ts._initialTutorial._postNote._visibility.localOnly }}</div>
 		</div>
 	</MkFormSection>
 	<MkFormSection>
@@ -105,7 +105,7 @@ const exampleCWNote = reactive<Misskey.entities.Note>({
 	font-weight: bold;
 	text-align: left;
 
-	&:before {
+	&::before {
 		content: "";
 		display: block;
 		width: calc(100% - 38px);
diff --git a/packages/frontend/src/components/MkTutorialDialog.Sensitive.vue b/packages/frontend/src/components/MkTutorialDialog.Sensitive.vue
index f155ad7bcb..322082f5a0 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="ph-check ph-bold ph-lg"></i> {{ i18n.ts._initialTutorial.wellDone }}</b> {{ i18n.ts._initialTutorial._howToMakeAttachmentsSensitive.sensitiveSucceeded }}</div>
+	<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>
 	<MkFolder>
 		<template #label>{{ i18n.ts.previewNoteText }}</template>
 		<MkNote :mock="true" :note="exampleNote" :class="$style.exampleRoot"></MkNote>
@@ -115,7 +115,7 @@ const exampleNote = reactive<Misskey.entities.Note>({
 	font-weight: bold;
 	text-align: left;
 
-	&:before {
+	&::before {
 		content: "";
 		display: block;
 		width: calc(100% - 38px);
diff --git a/packages/frontend/src/components/MkTutorialDialog.Timeline.vue b/packages/frontend/src/components/MkTutorialDialog.Timeline.vue
index f5670c7ebd..b900a30c85 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="ph-house ph-bold pg-lg"></i> <b>{{ i18n.ts._timelines.home }}</b> … {{ i18n.ts._initialTutorial._timeline.home }}</div>
-		<div><i class="ph-planet ph-bold pg-lg"></i> <b>{{ i18n.ts._timelines.local }}</b> … {{ i18n.ts._initialTutorial._timeline.local }}</div>
-		<div><i class="ph-rocket-launch ph-bold ph-lg"></i> <b>{{ i18n.ts._timelines.social }}</b> … {{ i18n.ts._initialTutorial._timeline.social }}</div>
-		<div><i class="ph-globe-hemisphere-west ph-bold ph-lg"></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,12 +21,12 @@ 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>
@@ -56,7 +55,7 @@ import { i18n } from '@/i18n.js';
 	font-weight: bold;
 	text-align: left;
 
-	&:before {
+	&::before {
 		content: "";
 		display: block;
 		width: calc(100% - 38px);
diff --git a/packages/frontend/src/components/MkTutorialDialog.vue b/packages/frontend/src/components/MkTutorialDialog.vue
index 6cd7019fed..9adc8d466c 100644
--- a/packages/frontend/src/components/MkTutorialDialog.vue
+++ b/packages/frontend/src/components/MkTutorialDialog.vue
@@ -11,11 +11,11 @@ SPDX-License-Identifier: AGPL-3.0-only
 	@close="close(true)"
 	@closed="emit('closed')"
 >
-	<template v-if="page === 1" #header><i class="ph-pencil-simple ph-bold pg-lg"></i> {{ i18n.ts._initialTutorial._note.title }}</template>
-	<template v-else-if="page === 2" #header><i class="ph-smiley ph-bold pg-lg"></i> {{ i18n.ts._initialTutorial._reaction.title }}</template>
-	<template v-else-if="page === 3" #header><i class="ph-house ph-bold pg-lg"></i> {{ i18n.ts._initialTutorial._timeline.title }}</template>
-	<template v-else-if="page === 4" #header><i class="ph-plus ph-bold pg-lg"></i> {{ i18n.ts._initialTutorial._postNote.title }}</template>
-	<template v-else-if="page === 5" #header><i class="ph-eye-slash ph-bold pg-lg"></i> {{ i18n.ts._initialTutorial._howToMakeAttachmentsSensitive.title }}</template>
+	<template v-if="page === 1" #header><i class="ti ti-pencil"></i> {{ i18n.ts._initialTutorial._note.title }}</template>
+	<template v-else-if="page === 2" #header><i class="ti ti-mood-smile"></i> {{ i18n.ts._initialTutorial._reaction.title }}</template>
+	<template v-else-if="page === 3" #header><i class="ti ti-home"></i> {{ i18n.ts._initialTutorial._timeline.title }}</template>
+	<template v-else-if="page === 4" #header><i class="ti ti-pencil-plus"></i> {{ i18n.ts._initialTutorial._postNote.title }}</template>
+	<template v-else-if="page === 5" #header><i class="ti ti-eye-exclamation"></i> {{ i18n.ts._initialTutorial._howToMakeAttachmentsSensitive.title }}</template>
 	<template v-else #header>{{ i18n.ts._initialTutorial.title }}</template>
 
 	<div style="overflow-x: clip;">
@@ -31,10 +31,10 @@ 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="ph-confetti ph-bold pg-lg" 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(--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="ph-arrow-right ph-bold pg-lg"></i></MkButton>
+							<MkButton primary rounded gradate style="margin: 16px auto 0 auto;" @click="page++">{{ i18n.ts._initialTutorial.launchTutorial }} <i class="ti ti-arrow-right"></i></MkButton>
 							<MkButton style="margin: 0 auto;" transparent rounded @click="close(true)">{{ i18n.ts.close }}</MkButton>
 						</div>
 					</MkSpacer>
@@ -48,8 +48,8 @@ SPDX-License-Identifier: AGPL-3.0-only
 						</MkSpacer>
 						<div :class="$style.pageFooter">
 							<div class="_buttonsCenter">
-								<MkButton v-if="initialPage !== 1" rounded @click="page--"><i class="ph-arrow-left ph-bold pg-lg"></i> {{ i18n.ts.goBack }}</MkButton>
-								<MkButton primary rounded gradate @click="page++">{{ i18n.ts.continue }} <i class="ph-arrow-right ph-bold pg-lg"></i></MkButton>
+								<MkButton v-if="initialPage !== 1" rounded @click="page--"><i class="ti ti-arrow-left"></i> {{ i18n.ts.goBack }}</MkButton>
+								<MkButton primary rounded gradate @click="page++">{{ i18n.ts.continue }} <i class="ti ti-arrow-right"></i></MkButton>
 							</div>
 						</div>
 					</div>
@@ -66,8 +66,8 @@ SPDX-License-Identifier: AGPL-3.0-only
 						</MkSpacer>
 						<div :class="$style.pageFooter">
 							<div class="_buttonsCenter">
-								<MkButton v-if="initialPage !== 2" rounded @click="page--"><i class="ph-arrow-left ph-bold pg-lg"></i> {{ i18n.ts.goBack }}</MkButton>
-								<MkButton primary rounded gradate :disabled="!isReactionTutorialPushed" @click="page++">{{ i18n.ts.continue }} <i class="ph-arrow-right ph-bold pg-lg"></i></MkButton>
+								<MkButton v-if="initialPage !== 2" rounded @click="page--"><i class="ti ti-arrow-left"></i> {{ i18n.ts.goBack }}</MkButton>
+								<MkButton primary rounded gradate :disabled="!isReactionTutorialPushed" @click="page++">{{ i18n.ts.continue }} <i class="ti ti-arrow-right"></i></MkButton>
 							</div>
 						</div>
 					</div>
@@ -81,8 +81,8 @@ SPDX-License-Identifier: AGPL-3.0-only
 						</MkSpacer>
 						<div :class="$style.pageFooter">
 							<div class="_buttonsCenter">
-								<MkButton v-if="initialPage !== 3" rounded @click="page--"><i class="ph-arrow-left ph-bold pg-lg"></i> {{ i18n.ts.goBack }}</MkButton>
-								<MkButton primary rounded gradate @click="page++">{{ i18n.ts.continue }} <i class="ph-arrow-right ph-bold pg-lg"></i></MkButton>
+								<MkButton v-if="initialPage !== 3" rounded @click="page--"><i class="ti ti-arrow-left"></i> {{ i18n.ts.goBack }}</MkButton>
+								<MkButton primary rounded gradate @click="page++">{{ i18n.ts.continue }} <i class="ti ti-arrow-right"></i></MkButton>
 							</div>
 						</div>
 					</div>
@@ -96,8 +96,8 @@ SPDX-License-Identifier: AGPL-3.0-only
 						</MkSpacer>
 						<div :class="$style.pageFooter">
 							<div class="_buttonsCenter">
-								<MkButton v-if="initialPage !== 3" rounded @click="page--"><i class="ph-arrow-left ph-bold pg-lg"></i> {{ i18n.ts.goBack }}</MkButton>
-								<MkButton primary rounded gradate @click="page++">{{ i18n.ts.continue }} <i class="ph-arrow-right ph-bold pg-lg"></i></MkButton>
+								<MkButton v-if="initialPage !== 3" rounded @click="page--"><i class="ti ti-arrow-left"></i> {{ i18n.ts.goBack }}</MkButton>
+								<MkButton primary rounded gradate @click="page++">{{ i18n.ts.continue }} <i class="ti ti-arrow-right"></i></MkButton>
 							</div>
 						</div>
 					</div>
@@ -114,8 +114,8 @@ SPDX-License-Identifier: AGPL-3.0-only
 						</MkSpacer>
 						<div :class="$style.pageFooter">
 							<div class="_buttonsCenter">
-								<MkButton v-if="initialPage !== 2" rounded @click="page--"><i class="ph-arrow-left ph-bold pg-lg"></i> {{ i18n.ts.goBack }}</MkButton>
-								<MkButton primary rounded gradate :disabled="!isSensitiveTutorialSucceeded" @click="page++">{{ i18n.ts.continue }} <i class="ph-arrow-right ph-bold pg-lg"></i></MkButton>
+								<MkButton v-if="initialPage !== 2" rounded @click="page--"><i class="ti ti-arrow-left"></i> {{ i18n.ts.goBack }}</MkButton>
+								<MkButton primary rounded gradate :disabled="!isSensitiveTutorialSucceeded" @click="page++">{{ i18n.ts.continue }} <i class="ti ti-arrow-right"></i></MkButton>
 							</div>
 						</div>
 					</div>
@@ -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="ph-check ph-bold pg-lg" 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(--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>
@@ -135,7 +135,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 							</I18n>
 							<div>{{ i18n.tsx._initialAccountSetting.haveFun({ name: instance.name ?? host }) }}</div>
 							<div class="_buttonsCenter" style="margin-top: 16px;">
-								<MkButton v-if="initialPage !== 4" rounded @click="page--"><i class="ph-arrow-left ph-bold pg-lg"></i> {{ i18n.ts.goBack }}</MkButton>
+								<MkButton v-if="initialPage !== 4" rounded @click="page--"><i class="ti ti-arrow-left"></i> {{ i18n.ts.goBack }}</MkButton>
 								<MkButton rounded primary gradate @click="close(false)">{{ i18n.ts.close }}</MkButton>
 							</div>
 						</div>
@@ -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) => {
diff --git a/packages/frontend/src/components/MkUrlPreview.vue b/packages/frontend/src/components/MkUrlPreview.vue
index 2e069fcdd2..a51b878580 100644
--- a/packages/frontend/src/components/MkUrlPreview.vue
+++ b/packages/frontend/src/components/MkUrlPreview.vue
@@ -15,14 +15,14 @@ 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>
 	</div>
 	<div :class="$style.action">
 		<MkButton :small="true" inline @click="playerEnabled = false">
-			<i class="ph-x ph-bold ph-lg"></i> {{ i18n.ts.disablePlayer }}
+			<i class="ti ti-x"></i> {{ i18n.ts.disablePlayer }}
 		</MkButton>
 	</div>
 </template>
@@ -39,7 +39,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 	</div>
 	<div :class="$style.action">
 		<MkButton :small="true" inline @click="tweetExpanded = false">
-			<i class="ph-x ph-bold ph-lg"></i> {{ i18n.ts.close }}
+			<i class="ti ti-x"></i> {{ i18n.ts.close }}
 		</MkButton>
 	</div>
 </template>
@@ -67,15 +67,15 @@ SPDX-License-Identifier: AGPL-3.0-only
 	<template v-if="showActions">
 		<div v-if="tweetId" :class="$style.action">
 			<MkButton :small="true" inline @click="tweetExpanded = true">
-				<i class="ph-twitter-logo ph-bold ph-lg"></i> {{ i18n.ts.expandTweet }}
+				<i class="ti ti-brand-x"></i> {{ i18n.ts.expandTweet }}
 			</MkButton>
 		</div>
 		<div v-if="!playerEnabled && player.url" :class="$style.action">
 			<MkButton :small="true" inline @click="playerEnabled = true">
-				<i class="ph-play ph-bold ph-lg"></i> {{ i18n.ts.enablePlayer }}
+				<i class="ti ti-player-play"></i> {{ i18n.ts.enablePlayer }}
 			</MkButton>
 			<MkButton v-if="!isMobile" :small="true" inline @click="openPlayer()">
-				<i class="ph-picture-in-picture ph-bold ph-lg"></i> {{ i18n.ts.openInWindow }}
+				<i class="ti ti-picture-in-picture"></i> {{ i18n.ts.openInWindow }}
 			</MkButton>
 		</div>
 	</template>
@@ -91,6 +91,7 @@ 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>>;
@@ -188,11 +189,13 @@ 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,
+	}, {
+		// TODO
 	});
-};
+}
 
 (window as any).addEventListener('message', adjustTweetHeight);
 
diff --git a/packages/frontend/src/components/MkUserAnnouncementEditDialog.vue b/packages/frontend/src/components/MkUserAnnouncementEditDialog.vue
index 13ab6fd763..3c5f563aa0 100644
--- a/packages/frontend/src/components/MkUserAnnouncementEditDialog.vue
+++ b/packages/frontend/src/components/MkUserAnnouncementEditDialog.vue
@@ -24,10 +24,10 @@ SPDX-License-Identifier: AGPL-3.0-only
 				</MkTextarea>
 				<MkRadios v-model="icon">
 					<template #label>{{ i18n.ts.icon }}</template>
-					<option value="info"><i class="ph-info ph-bold ph-lg"></i></option>
-					<option value="warning"><i class="ph-warning ph-bold ph-lg" style="color: var(--warn);"></i></option>
-					<option value="error"><i class="ph-x-circle ph-bold ph-lg" style="color: var(--error);"></i></option>
-					<option value="success"><i class="ph-check ph-bold ph-lg" style="color: var(--success);"></i></option>
+					<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="display">
 					<template #label>{{ i18n.ts.display }}</template>
@@ -39,11 +39,11 @@ SPDX-License-Identifier: AGPL-3.0-only
 					{{ i18n.ts._announcement.needConfirmationToRead }}
 					<template #caption>{{ i18n.ts._announcement.needConfirmationToReadDescription }}</template>
 				</MkSwitch>
-				<MkButton v-if="announcement" danger @click="del()"><i class="ph-trash ph-bold ph-lg"></i> {{ i18n.ts.delete }}</MkButton>
+				<MkButton v-if="announcement" 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="ph-check ph-bold ph-lg"></i> {{ props.announcement ? i18n.ts.update : i18n.ts.create }}</MkButton>
+			<MkButton primary rounded style="margin: 0 auto;" @click="done"><i class="ti ti-check"></i> {{ props.announcement ? i18n.ts.update : i18n.ts.create }}</MkButton>
 		</div>
 	</div>
 </MkModalWindow>
diff --git a/packages/frontend/src/components/MkUserInfo.vue b/packages/frontend/src/components/MkUserInfo.vue
index 5658188c41..e528f04dfc 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;
diff --git a/packages/frontend/src/components/MkUserPopup.vue b/packages/frontend/src/components/MkUserPopup.vue
index 2aee918114..c6f4699b3e 100644
--- a/packages/frontend/src/components/MkUserPopup.vue
+++ b/packages/frontend/src/components/MkUserPopup.vue
@@ -13,7 +13,7 @@ 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>
 				<span v-if="user.isLocked && $i && $i.id != user.id && !user.isFollowing" :title="i18n.ts.isLocked" :class="$style.locked"><i class="ph-lock ph-bold ph-lg"></i></span>
 			</div>
@@ -56,7 +56,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 					<div>{{ number(user.followersCount) }}</div>
 				</div>
 			</div>
-			<button class="_button" :class="$style.menu" @click="showMenu"><i class="ph-dots-three ph-bold ph-lg"></i></button>
+			<button class="_button" :class="$style.menu" @click="showMenu"><i class="ti ti-dots"></i></button>
 			<MkFollowButton v-if="$i && user.id != $i.id" v-model:user="user" :class="$style.follow" mini/>
 		</div>
 		<div v-else>
@@ -79,6 +79,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;
diff --git a/packages/frontend/src/components/MkUserSelectDialog.vue b/packages/frontend/src/components/MkUserSelectDialog.vue
index b76be051d8..7d210a4385 100644
--- a/packages/frontend/src/components/MkUserSelectDialog.vue
+++ b/packages/frontend/src/components/MkUserSelectDialog.vue
@@ -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';
@@ -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 === '') {
@@ -123,7 +123,7 @@ async function ok() {
 	});
 	emit('ok', user);
 
-	dialogEl.value.close();
+	dialogEl.value?.close();
 
 	// 最近使ったユーザー更新
 	let recents = defaultStore.state.recentlyUsedUsers;
@@ -134,7 +134,7 @@ async function ok() {
 
 function cancel() {
 	emit('cancel');
-	dialogEl.value.close();
+	dialogEl.value?.close();
 }
 
 onMounted(() => {
diff --git a/packages/frontend/src/components/MkUserSetupDialog.Privacy.vue b/packages/frontend/src/components/MkUserSetupDialog.Privacy.vue
index 6d2f0bbb99..bc998d6158 100644
--- a/packages/frontend/src/components/MkUserSetupDialog.Privacy.vue
+++ b/packages/frontend/src/components/MkUserSetupDialog.Privacy.vue
@@ -9,7 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 	<MkFolder>
 		<template #label>{{ i18n.ts.makeFollowManuallyApprove }}</template>
-		<template #icon><i class="ph-lock ph-bold ph-lg"></i></template>
+		<template #icon><i class="ti ti-lock"></i></template>
 		<template #suffix>{{ isLocked ? i18n.ts.on : i18n.ts.off }}</template>
 
 		<MkSwitch v-model="isLocked">{{ i18n.ts.makeFollowManuallyApprove }}<template #caption>{{ i18n.ts.lockedAccountInfo }}</template></MkSwitch>
@@ -17,7 +17,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 	<MkFolder>
 		<template #label>{{ i18n.ts.hideOnlineStatus }}</template>
-		<template #icon><i class="ph-eye-slash ph-bold ph-lg"></i></template>
+		<template #icon><i class="ti ti-eye-off"></i></template>
 		<template #suffix>{{ hideOnlineStatus ? i18n.ts.on : i18n.ts.off }}</template>
 
 		<MkSwitch v-model="hideOnlineStatus">{{ i18n.ts.hideOnlineStatus }}<template #caption>{{ i18n.ts.hideOnlineStatusDescription }}</template></MkSwitch>
@@ -25,7 +25,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 	<MkFolder>
 		<template #label>{{ i18n.ts.noCrawle }}</template>
-		<template #icon><i class="ph-planet ph-bold ph-lg"></i></template>
+		<template #icon><i class="ti ti-world-x"></i></template>
 		<template #suffix>{{ noCrawle ? i18n.ts.on : i18n.ts.off }}</template>
 
 		<MkSwitch v-model="noCrawle">{{ i18n.ts.noCrawle }}<template #caption>{{ i18n.ts.noCrawleDescription }}</template></MkSwitch>
diff --git a/packages/frontend/src/components/MkUserSetupDialog.User.vue b/packages/frontend/src/components/MkUserSetupDialog.User.vue
index efb1ed5593..c80349d034 100644
--- a/packages/frontend/src/components/MkUserSetupDialog.User.vue
+++ b/packages/frontend/src/components/MkUserSetupDialog.User.vue
@@ -18,8 +18,8 @@ SPDX-License-Identifier: AGPL-3.0-only
 		<span v-else style="opacity: 0.7;">{{ i18n.ts.noAccountDescription }}</span>
 	</div>
 	<div :class="$style.footer">
-		<MkButton v-if="!isFollowing" primary gradate rounded full @click="follow"><i class="ph-plus ph-bold ph-lg"></i> {{ i18n.ts.follow }}</MkButton>
-		<div v-else style="opacity: 0.7; text-align: center;">{{ i18n.ts.youFollowing }} <i class="ph-check ph-bold ph-lg"></i></div>
+		<MkButton v-if="!isFollowing" primary gradate rounded full @click="follow"><i class="ti ti-plus"></i> {{ i18n.ts.follow }}</MkButton>
+		<div v-else style="opacity: 0.7; text-align: center;">{{ i18n.ts.youFollowing }} <i class="ti ti-check"></i></div>
 	</div>
 </div>
 </template>
diff --git a/packages/frontend/src/components/MkUserSetupDialog.vue b/packages/frontend/src/components/MkUserSetupDialog.vue
index bd8949890c..514350c930 100644
--- a/packages/frontend/src/components/MkUserSetupDialog.vue
+++ b/packages/frontend/src/components/MkUserSetupDialog.vue
@@ -12,10 +12,10 @@ SPDX-License-Identifier: AGPL-3.0-only
 	@close="close(true)"
 	@closed="emit('closed')"
 >
-	<template v-if="page === 1" #header><i class="ph-user-list ph-bold ph-lg"></i> {{ i18n.ts._initialAccountSetting.profileSetting }}</template>
-	<template v-else-if="page === 2" #header><i class="ph-lock ph-bold ph-lg"></i> {{ i18n.ts._initialAccountSetting.privacySetting }}</template>
-	<template v-else-if="page === 3" #header><i class="ph-user-plus ph-bold ph-lg"></i> {{ i18n.ts.follow }}</template>
-	<template v-else-if="page === 4" #header><i class="ph-bell-ringing ph-bold ph-lg"></i> {{ i18n.ts.pushNotification }}</template>
+	<template v-if="page === 1" #header><i class="ti ti-user-edit"></i> {{ i18n.ts._initialAccountSetting.profileSetting }}</template>
+	<template v-else-if="page === 2" #header><i class="ti ti-lock"></i> {{ i18n.ts._initialAccountSetting.privacySetting }}</template>
+	<template v-else-if="page === 3" #header><i class="ti ti-user-plus"></i> {{ i18n.ts.follow }}</template>
+	<template v-else-if="page === 4" #header><i class="ti ti-bell-plus"></i> {{ i18n.ts.pushNotification }}</template>
 	<template v-else-if="page === 5" #header>{{ i18n.ts.done }}</template>
 	<template v-else #header>{{ i18n.ts.initialAccountSetting }}</template>
 
@@ -35,10 +35,10 @@ 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="ph-confetti ph-bold ph-lg" 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(--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="ph-arrow-right ph-bold ph-lg"></i></MkButton>
+							<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>
 							<MkButton style="margin: 0 auto;" transparent rounded @click="later(true)">{{ i18n.ts.later }}</MkButton>
 						</div>
 					</MkSpacer>
@@ -52,8 +52,8 @@ SPDX-License-Identifier: AGPL-3.0-only
 						</MkSpacer>
 						<div :class="$style.pageFooter">
 							<div class="_buttonsCenter">
-								<MkButton rounded data-cy-user-setup-back @click="page--"><i class="ph-arrow-left ph-bold ph-lg"></i> {{ i18n.ts.goBack }}</MkButton>
-								<MkButton primary rounded gradate data-cy-user-setup-continue @click="page++">{{ i18n.ts.continue }} <i class="ph-arrow-right ph-bold ph-lg"></i></MkButton>
+								<MkButton rounded data-cy-user-setup-back @click="page--"><i class="ti ti-arrow-left"></i> {{ i18n.ts.goBack }}</MkButton>
+								<MkButton primary rounded gradate data-cy-user-setup-continue @click="page++">{{ i18n.ts.continue }} <i class="ti ti-arrow-right"></i></MkButton>
 							</div>
 						</div>
 					</div>
@@ -67,8 +67,8 @@ SPDX-License-Identifier: AGPL-3.0-only
 						</MkSpacer>
 						<div :class="$style.pageFooter">
 							<div class="_buttonsCenter">
-								<MkButton rounded data-cy-user-setup-back @click="page--"><i class="ph-arrow-left ph-bold ph-lg"></i> {{ i18n.ts.goBack }}</MkButton>
-								<MkButton primary rounded gradate data-cy-user-setup-continue @click="page++">{{ i18n.ts.continue }} <i class="ph-arrow-right ph-bold ph-lg"></i></MkButton>
+								<MkButton rounded data-cy-user-setup-back @click="page--"><i class="ti ti-arrow-left"></i> {{ i18n.ts.goBack }}</MkButton>
+								<MkButton primary rounded gradate data-cy-user-setup-continue @click="page++">{{ i18n.ts.continue }} <i class="ti ti-arrow-right"></i></MkButton>
 							</div>
 						</div>
 					</div>
@@ -81,8 +81,8 @@ SPDX-License-Identifier: AGPL-3.0-only
 					</MkSpacer>
 					<div :class="$style.pageFooter">
 						<div class="_buttonsCenter">
-							<MkButton rounded data-cy-user-setup-back @click="page--"><i class="ph-arrow-left ph-bold ph-lg"></i> {{ i18n.ts.goBack }}</MkButton>
-							<MkButton primary rounded gradate style="" data-cy-user-setup-continue @click="page++">{{ i18n.ts.continue }} <i class="ph-arrow-right ph-bold ph-lg"></i></MkButton>
+							<MkButton rounded data-cy-user-setup-back @click="page--"><i class="ti ti-arrow-left"></i> {{ i18n.ts.goBack }}</MkButton>
+							<MkButton primary rounded gradate style="" data-cy-user-setup-continue @click="page++">{{ i18n.ts.continue }} <i class="ti ti-arrow-right"></i></MkButton>
 						</div>
 					</div>
 				</div>
@@ -91,13 +91,13 @@ 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="ph-bell-ringing ph-bold ph-lg" 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(--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;"/>
 							<div class="_buttonsCenter" style="margin-top: 16px;">
-								<MkButton rounded data-cy-user-setup-back @click="page--"><i class="ph-arrow-left ph-bold ph-lg"></i> {{ i18n.ts.goBack }}</MkButton>
-								<MkButton primary rounded gradate data-cy-user-setup-continue @click="page++">{{ i18n.ts.continue }} <i class="ph-arrow-right ph-bold ph-lg"></i></MkButton>
+								<MkButton rounded data-cy-user-setup-back @click="page--"><i class="ti ti-arrow-left"></i> {{ i18n.ts.goBack }}</MkButton>
+								<MkButton primary rounded gradate data-cy-user-setup-continue @click="page++">{{ i18n.ts.continue }} <i class="ti ti-arrow-right"></i></MkButton>
 							</div>
 						</div>
 					</MkSpacer>
@@ -108,14 +108,14 @@ 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="ph-check ph-bold ph-lg" 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(--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;">
-								<MkButton rounded primary gradate data-cy-user-setup-continue @click="launchTutorial()">{{ i18n.ts._initialAccountSetting.startTutorial }} <i class="ph-arrow-right ph-bold ph-lg"></i></MkButton>
+								<MkButton rounded primary gradate data-cy-user-setup-continue @click="launchTutorial()">{{ i18n.ts._initialAccountSetting.startTutorial }} <i class="ti ti-arrow-right"></i></MkButton>
 							</div>
 							<div class="_buttonsCenter">
-								<MkButton rounded data-cy-user-setup-back @click="page--"><i class="ph-arrow-left ph-bold ph-lg"></i> {{ i18n.ts.goBack }}</MkButton>
+								<MkButton rounded data-cy-user-setup-back @click="page--"><i class="ti ti-arrow-left"></i> {{ i18n.ts.goBack }}</MkButton>
 								<MkButton rounded primary data-cy-user-setup-continue @click="setupComplete()">{{ i18n.ts.close }}</MkButton>
 							</div>
 						</div>
@@ -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(),
+		});
 	});
 }
 
diff --git a/packages/frontend/src/components/MkVisibilityPicker.vue b/packages/frontend/src/components/MkVisibilityPicker.vue
index bd6edad663..3c3f9e94b6 100644
--- a/packages/frontend/src/components/MkVisibilityPicker.vue
+++ b/packages/frontend/src/components/MkVisibilityPicker.vue
@@ -4,34 +4,34 @@ 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 }}
 		</div>
 		<button key="public" :disabled="isSilenced || isReplyVisibilitySpecified" class="_button" :class="[$style.item, { [$style.active]: v === 'public' }]" data-index="1" @click="choose('public')">
-			<div :class="$style.icon"><i class="ph-globe-hemisphere-west ph-bold ph-lg"></i></div>
+			<div :class="$style.icon"><i class="ti ti-world"></i></div>
 			<div :class="$style.body">
 				<span :class="$style.itemTitle">{{ i18n.ts._visibility.public }}</span>
 				<span :class="$style.itemDescription">{{ i18n.ts._visibility.publicDescription }}</span>
 			</div>
 		</button>
 		<button key="home" :disabled="isReplyVisibilitySpecified" class="_button" :class="[$style.item, { [$style.active]: v === 'home' }]" data-index="2" @click="choose('home')">
-			<div :class="$style.icon"><i class="ph-house ph-bold ph-lg"></i></div>
+			<div :class="$style.icon"><i class="ti ti-home"></i></div>
 			<div :class="$style.body">
 				<span :class="$style.itemTitle">{{ i18n.ts._visibility.home }}</span>
 				<span :class="$style.itemDescription">{{ i18n.ts._visibility.homeDescription }}</span>
 			</div>
 		</button>
 		<button key="followers" :disabled="isReplyVisibilitySpecified" class="_button" :class="[$style.item, { [$style.active]: v === 'followers' }]" data-index="3" @click="choose('followers')">
-			<div :class="$style.icon"><i class="ph-lock ph-bold ph-lg"></i></div>
+			<div :class="$style.icon"><i class="ti ti-lock"></i></div>
 			<div :class="$style.body">
 				<span :class="$style.itemTitle">{{ i18n.ts._visibility.followers }}</span>
 				<span :class="$style.itemDescription">{{ i18n.ts._visibility.followersDescription }}</span>
 			</div>
 		</button>
 		<button key="specified" :disabled="localOnly" class="_button" :class="[$style.item, { [$style.active]: v === 'specified' }]" data-index="4" @click="choose('specified')">
-			<div :class="$style.icon"><i class="ph-envelope ph-bold ph-lg"></i></div>
+			<div :class="$style.icon"><i class="ti ti-mail"></i></div>
 			<div :class="$style.body">
 				<span :class="$style.itemTitle">{{ i18n.ts._visibility.specified }}</span>
 				<span :class="$style.itemDescription">{{ i18n.ts._visibility.specifiedDescription }}</span>
diff --git a/packages/frontend/src/components/MkVisitorDashboard.vue b/packages/frontend/src/components/MkVisitorDashboard.vue
index b902494025..6fb3304468 100644
--- a/packages/frontend/src/components/MkVisitorDashboard.vue
+++ b/packages/frontend/src/components/MkVisitorDashboard.vue
@@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 <div v-if="instance" :class="$style.root">
 	<div :class="[$style.main, $style.panel]">
 		<img :src="instance.iconUrl || '/apple-touch-icon.png'" alt="" :class="$style.mainIcon"/>
-		<button class="_button _acrylic" :class="$style.mainMenu" @click="showMenu"><i class="ph-dots-three ph-bold ph-lg"></i></button>
+		<button class="_button _acrylic" :class="$style.mainMenu" @click="showMenu"><i class="ti ti-dots"></i></button>
 		<div :class="$style.mainFg">
 			<h1 :class="$style.mainTitle">
 				<!-- 背景色によってはロゴが見えなくなるのでとりあえず無効に -->
@@ -26,7 +26,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://joinsharkey.org/#findaninstance">{{ i18n.ts.exploreOtherServers }}</MkButton>
 				<MkButton :class="$style.mainAction" full rounded data-cy-signin @click="signin()">{{ i18n.ts.login }}</MkButton>
 			</div>
 		</div>
@@ -69,7 +69,8 @@ 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);
 
@@ -78,24 +79,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://joinsharkey.org/#findaninstance', '_blank', 'noopener');
-}
 </script>
 
 <style lang="scss" module>
diff --git a/packages/frontend/src/components/MkWaitingDialog.vue b/packages/frontend/src/components/MkWaitingDialog.vue
index ad2105cc0b..60b75b6d30 100644
--- a/packages/frontend/src/components/MkWaitingDialog.vue
+++ b/packages/frontend/src/components/MkWaitingDialog.vue
@@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 <template>
 <MkModal ref="modal" :preferType="'dialog'" :zPriority="'high'" @click="success ? done() : () => {}" @closed="emit('closed')">
 	<div :class="[$style.root, { [$style.iconOnly]: (text == null) || success }]">
-		<i v-if="success" :class="[$style.icon, $style.success]" class="ph-check ph-bold ph-lg"></i>
+		<i v-if="success" :class="[$style.icon, $style.success]" class="ti ti-check"></i>
 		<MkLoading v-else :class="[$style.icon, $style.waiting]" :em="true"/>
 		<div v-if="text && !success" :class="$style.text">{{ text }}<MkEllipsis/></div>
 	</div>
diff --git a/packages/frontend/src/components/MkWidgets.vue b/packages/frontend/src/components/MkWidgets.vue
index 05a0f6e04e..06879f5917 100644
--- a/packages/frontend/src/components/MkWidgets.vue
+++ b/packages/frontend/src/components/MkWidgets.vue
@@ -11,7 +11,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 				<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="ph-plus ph-bold ph-lg"></i> {{ i18n.ts.add }}</MkButton>
+			<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>
 		</header>
 		<Sortable
@@ -25,8 +25,8 @@ SPDX-License-Identifier: AGPL-3.0-only
 		>
 			<template #item="{element}">
 				<div :class="[$style.widget, $style.customizeContainer]" data-cy-customize-container>
-					<button :class="$style.customizeContainerConfig" class="_button" @click.prevent.stop="configWidget(element.id)"><i class="ph-gear ph-bold ph-lg"></i></button>
-					<button :class="$style.customizeContainerRemove" data-cy-customize-container-remove class="_button" @click.prevent.stop="removeWidget(element)"><i class="ph-x ph-bold ph-lg"></i></button>
+					<button :class="$style.customizeContainerConfig" class="_button" @click.prevent.stop="configWidget(element.id)"><i class="ti ti-settings"></i></button>
+					<button :class="$style.customizeContainerRemove" data-cy-customize-container-remove class="_button" @click.prevent.stop="removeWidget(element)"><i class="ti ti-x"></i></button>
 					<div class="handle">
 						<component :is="`widget-${element.name}`" :ref="el => widgetRefs[element.id] = el" class="widget" :class="$style.customizeContainerHandleWidget" :widget="element" @updateProps="updateWidget(element.id, $event)"/>
 					</div>
@@ -120,7 +120,7 @@ function onContextmenu(widget: Widget, ev: MouseEvent) {
 		type: 'label',
 		text: i18n.ts._widgets[widget.name],
 	}, {
-		icon: 'ph-gear ph-bold ph-lg',
+		icon: 'ti ti-settings',
 		text: i18n.ts.settings,
 		action: () => {
 			configWidget(widget.id);
diff --git a/packages/frontend/src/components/MkWindow.vue b/packages/frontend/src/components/MkWindow.vue
index f13b53b005..303e49de00 100644
--- a/packages/frontend/src/components/MkWindow.vue
+++ b/packages/frontend/src/components/MkWindow.vue
@@ -27,11 +27,11 @@ SPDX-License-Identifier: AGPL-3.0-only
 					<template v-if="!minimized">
 						<button v-for="button in buttonsRight" v-tooltip="button.title" class="_button" :class="[$style.headerButton, { [$style.highlighted]: button.highlighted }]" @click="button.onClick"><i :class="button.icon"></i></button>
 					</template>
-					<button v-if="canResize && minimized" v-tooltip="i18n.ts.windowRestore" class="_button" :class="$style.headerButton" @click="unMinimize()"><i class="ph-frame-corners ph-bold ph-lg"></i></button>
-					<button v-else-if="canResize && !maximized" v-tooltip="i18n.ts.windowMinimize" class="_button" :class="$style.headerButton" @click="minimize()"><i class="ph-arrows-in-simple ph-bold ph-lg"></i></button>
-					<button v-if="canResize && maximized" v-tooltip="i18n.ts.windowRestore" class="_button" :class="$style.headerButton" @click="unMaximize()"><i class="ph-picture-in-picture ph-bold ph-lg"></i></button>
-					<button v-else-if="canResize && !maximized && !minimized" v-tooltip="i18n.ts.windowMaximize" class="_button" :class="$style.headerButton" @click="maximize()"><i class="ph-frame-corners ph-bold ph-lg"></i></button>
-					<button v-if="closeButton" v-tooltip="i18n.ts.close" class="_button" :class="$style.headerButton" @click="close()"><i class="ph-x ph-bold ph-lg"></i></button>
+					<button v-if="canResize && minimized" v-tooltip="i18n.ts.windowRestore" class="_button" :class="$style.headerButton" @click="unMinimize()"><i class="ti ti-maximize"></i></button>
+					<button v-else-if="canResize && !maximized" v-tooltip="i18n.ts.windowMinimize" class="_button" :class="$style.headerButton" @click="minimize()"><i class="ti ti-minimize"></i></button>
+					<button v-if="canResize && maximized" v-tooltip="i18n.ts.windowRestore" class="_button" :class="$style.headerButton" @click="unMaximize()"><i class="ti ti-picture-in-picture"></i></button>
+					<button v-else-if="canResize && !maximized && !minimized" v-tooltip="i18n.ts.windowMaximize" class="_button" :class="$style.headerButton" @click="maximize()"><i class="ti ti-rectangle"></i></button>
+					<button v-if="closeButton" v-tooltip="i18n.ts.close" class="_button" :class="$style.headerButton" @click="close()"><i class="ti ti-x"></i></button>
 				</span>
 			</div>
 			<div :class="$style.content">
diff --git a/packages/frontend/src/components/MkYouTubePlayer.vue b/packages/frontend/src/components/MkYouTubePlayer.vue
index 3ad2a95bc3..e3711b3463 100644
--- a/packages/frontend/src/components/MkYouTubePlayer.vue
+++ b/packages/frontend/src/components/MkYouTubePlayer.vue
@@ -6,14 +6,14 @@ SPDX-License-Identifier: AGPL-3.0-only
 <template>
 <MkWindow :initialWidth="640" :initialHeight="402" :canResize="true" :closeButton="true">
 	<template #header>
-		<i class="icon ph-youtube-logo ph-bold ph-lg" style="margin-right: 0.5em;"></i>
+		<i class="icon ti ti-brand-youtube" style="margin-right: 0.5em;"></i>
 		<span>{{ title ?? 'YouTube' }}</span>
 	</template>
 
 	<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>
@@ -27,6 +27,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { ref } from 'vue';
 import MkWindow from '@/components/MkWindow.vue';
 import { versatileLang } from '@/scripts/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/MkFormula.vue b/packages/frontend/src/components/SkFormula.vue
similarity index 100%
rename from packages/frontend/src/components/MkFormula.vue
rename to packages/frontend/src/components/SkFormula.vue
diff --git a/packages/frontend/src/components/MkMfmWindow.vue b/packages/frontend/src/components/SkMfmWindow.vue
similarity index 100%
rename from packages/frontend/src/components/MkMfmWindow.vue
rename to packages/frontend/src/components/SkMfmWindow.vue
diff --git a/packages/frontend/src/components/MkModPlayer.vue b/packages/frontend/src/components/SkModPlayer.vue
similarity index 100%
rename from packages/frontend/src/components/MkModPlayer.vue
rename to packages/frontend/src/components/SkModPlayer.vue
diff --git a/packages/frontend/src/components/SkNote.vue b/packages/frontend/src/components/SkNote.vue
index a193df4326..b02d902482 100644
--- a/packages/frontend/src/components/SkNote.vue
+++ b/packages/frontend/src/components/SkNote.vue
@@ -10,24 +10,24 @@ SPDX-License-Identifier: AGPL-3.0-only
 	ref="rootEl"
 	v-hotkey="keymap"
 	:class="[$style.root, { [$style.showActionsOnlyHover]: defaultStore.state.showNoteActionsOnlyHover }]"
-	:tabindex="!isDeleted ? '-1' : undefined"
+	:tabindex="isDeleted ? '-1' : '0'"
 >
 	<SkNoteSub v-if="appearNote.reply && !renoteCollapsed && !inReplyToCollapsed" :note="appearNote.reply" :class="$style.replyTo"/>
 	<div v-if="appearNote.reply && inReplyToCollapsed && !renoteCollapsed" :class="$style.collapsedInReplyTo">
 		<div :class="$style.collapsedInReplyToLine"></div>
 		<MkAvatar :class="$style.collapsedInReplyToAvatar" :user="appearNote.reply.user" link preview/>
-		<MkA v-user-preview="note.user.id" :class="$style.name" :to="userPage(note.user)">
+		<MkA v-user-preview="appearNote.reply.userId" :class="$style.name" :to="userPage(appearNote.reply.user)">
 			<MkAcct :user="appearNote.reply.user"/>
 		</MkA>:
 		<Mfm :text="getNoteSummary(appearNote.reply)" :plain="true" :nowrap="true" :author="appearNote.reply.user" :nyaize="'respect'" :class="$style.collapsedInReplyToText" @click="inReplyToCollapsed = false"/>
 	</div>
-	<div v-if="pinned" :class="$style.tip"><i class="ph-push-pin ph-bold ph-lg"></i> {{ i18n.ts.pinnedNote }}</div>
-	<!--<div v-if="appearNote._prId_" class="tip"><i class="ph-megaphone ph-bold ph-lg"></i> {{ i18n.ts.promotion }}<button class="_textButton hide" @click="readPromo()">{{ i18n.ts.hideThisNote }} <i class="ph-x ph-bold ph-lg"></i></button></div>-->
-	<!--<div v-if="appearNote._featuredId_" class="tip"><i class="ph-lightning ph-bold ph-lg"></i> {{ i18n.ts.featured }}</div>-->
+	<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>
 		<MkAvatar :class="$style.renoteAvatar" :user="note.user" link preview/>
-		<i class="ph-rocket-launch ph-bold ph-lg" style="margin-right: 4px;"></i>
+		<i class="ti ti-repeat" style="margin-right: 4px;"></i>
 		<I18n :src="i18n.ts.renotedBy" tag="span" :class="$style.renoteText">
 			<template #user>
 				<MkA v-user-preview="note.userId" :class="$style.renoteUserName" :to="userPage(note.user)">
@@ -36,17 +36,17 @@ SPDX-License-Identifier: AGPL-3.0-only
 			</template>
 		</I18n>
 		<div :class="$style.renoteInfo">
-			<button ref="renoteTime" :class="$style.renoteTime" class="_button" @click="showRenoteMenu()">
-				<i class="ph-dots-three ph-bold ph-lg" :class="$style.renoteMenu"></i>
+			<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>
 			<span v-if="note.visibility !== 'public'" style="margin-left: 0.5em;" :title="i18n.ts._visibility[note.visibility]">
-				<i v-if="note.visibility === 'home'" class="ph-house ph-bold ph-lg"></i>
-				<i v-else-if="note.visibility === 'followers'" class="ph-lock ph-bold ph-lg"></i>
-				<i v-else-if="note.visibility === 'specified'" ref="specified" class="ph-envelope ph-bold ph-lg"></i>
+				<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="ph-rocket ph-bold ph-lg"></i></span>
-			<span v-if="note.channel" style="margin-left: 0.5em;" :title="note.channel.name"><i class="ph-television ph-bold ph-lg"></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>
 			<span v-if="note.updatedAt" ref="menuVersionsButton" style="margin-left: 0.5em;" title="Edited" @mousedown="menuVersions()"><i class="ph-pencil-simple ph-bold ph-lg"></i></span>
 		</div>
 	</div>
@@ -94,7 +94,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 						<MkButton v-else-if="!defaultStore.state.animatedMfm && allowAnim && animated" :class="$style.playMFMButton" :small="true" @click="animatedMFM()" @click.stop><i class="ph-stop ph-bold ph-lg "></i> {{ i18n.ts._animatedMFM.stop }}</MkButton>
 					</div>
 					<div v-if="appearNote.files && appearNote.files.length > 0">
-						<MkMediaList :mediaList="appearNote.files" @click.stop/>
+						<MkMediaList ref="galleryEl" :mediaList="appearNote.files" @click.stop/>
 					</div>
 					<MkPoll v-if="appearNote.poll" :noteId="appearNote.id" :poll="appearNote.poll" :class="$style.poll" @click.stop/>
 					<div v-if="isEnabledUrlPreview">
@@ -108,7 +108,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 						<span :class="$style.showLessLabel">{{ i18n.ts.showLess }}</span>
 					</button>
 				</div>
-				<MkA v-if="appearNote.channel && !inChannel" :class="$style.channel" :to="`/channels/${appearNote.channel.id}`"><i class="ph-television ph-bold ph-lg"></i> {{ appearNote.channel.name }}</MkA>
+				<MkA v-if="appearNote.channel && !inChannel" :class="$style.channel" :to="`/channels/${appearNote.channel.id}`"><i class="ti ti-device-tv"></i> {{ appearNote.channel.name }}</MkA>
 			</div>
 			<MkReactionsViewer v-if="appearNote.reactionAcceptance !== 'likeOnly'" :note="appearNote" :maxNumber="16" @click.stop @mockUpdateMyReaction="emitUpdReaction">
 				<template #more>
@@ -117,7 +117,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 			</MkReactionsViewer>
 			<footer :class="$style.footer">
 				<button :class="$style.footerButton" class="_button" @click.stop @click="reply()">
-					<i class="ph-arrow-u-up-left ph-bold ph-lg"></i>
+					<i class="ti ti-arrow-back-up"></i>
 					<p v-if="appearNote.repliesCount > 0" :class="$style.footerButtonCount">{{ number(appearNote.repliesCount) }}</p>
 				</button>
 				<button
@@ -127,13 +127,13 @@ SPDX-License-Identifier: AGPL-3.0-only
 					class="_button"
 					:style="renoted ? 'color: var(--accent) !important;' : ''"
 					@click.stop
-					@mousedown="renoted ? undoRenote(appearNote) : boostVisibility()"
+					@mousedown.prevent="renoted ? undoRenote(appearNote) : boostVisibility()"
 				>
-					<i class="ph-rocket-launch ph-bold ph-lg"></i>
+					<i class="ti ti-repeat"></i>
 					<p v-if="appearNote.renoteCount > 0" :class="$style.footerButtonCount">{{ number(appearNote.renoteCount) }}</p>
 				</button>
 				<button v-else :class="$style.footerButton" class="_button" disabled>
-					<i class="ph-prohibit ph-bold ph-lg"></i>
+					<i class="ti ti-ban"></i>
 				</button>
 				<button
 					v-if="canRenote && !props.mock"
@@ -149,17 +149,17 @@ SPDX-License-Identifier: AGPL-3.0-only
 					<i class="ph-heart ph-bold ph-lg"></i>
 				</button>
 				<button ref="reactButton" :class="$style.footerButton" class="_button" @click="toggleReact()" @click.stop>
-					<i v-if="appearNote.reactionAcceptance === 'likeOnly' && appearNote.myReaction != null" class="ph-heart ph-bold ph-lg" style="color: var(--eventReactionHeart);"></i>
-					<i v-else-if="appearNote.myReaction != null" class="ph-minus ph-bold ph-lg" style="color: var(--accent);"></i>
-					<i v-else-if="appearNote.reactionAcceptance === 'likeOnly'" class="ph-heart ph-bold ph-lg"></i>
+					<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-else-if="appearNote.reactionAcceptance === 'likeOnly'" class="ti ti-heart"></i>
 					<i v-else class="ph-smiley ph-bold ph-lg"></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()">
-					<i class="ph-paperclip ph-bold ph-lg"></i>
+				<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()">
-					<i class="ph-dots-three ph-bold ph-lg"></i>
+				<button ref="menuButton" :class="$style.footerButton" class="_button" @mousedown.prevent="showMenu()">
+					<i class="ti ti-dots"></i>
 				</button>
 			</footer>
 		</div>
@@ -204,8 +204,7 @@ import MkPoll from '@/components/MkPoll.vue';
 import MkUsersTooltip from '@/components/MkUsersTooltip.vue';
 import MkUrlPreview from '@/components/MkUrlPreview.vue';
 import MkButton from '@/components/MkButton.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';
@@ -231,7 +230,11 @@ import { showMovedDialog } from '@/scripts/show-moved-dialog.js';
 import { shouldCollapsed } from '@/scripts/collapsed.js';
 import { useRouter } from '@/router/supplier.js';
 import { boostMenuItems, type Visibility } from '@/scripts/boost-quote.js';
+import { host } from '@/config.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;
@@ -283,14 +286,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>();
@@ -301,8 +297,8 @@ const reactButton = shallowRef<HTMLElement>();
 const quoteButton = shallowRef<HTMLElement>();
 const clipButton = shallowRef<HTMLElement>();
 const likeButton = 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(defaultStore.state.uncollapseCW);
 const parsed = computed(() => appearNote.value.text ? mfm.parse(appearNote.value.text) : null);
@@ -328,6 +324,11 @@ const defaultLike = computed(() => defaultStore.state.like ? defaultStore.state.
 const animated = computed(() => parsed.value ? checkAnimationFromMfm(parsed.value) : null);
 const allowAnim = ref(defaultStore.state.advancedMfm && defaultStore.state.animatedMfm ? true : false);
 
+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';
@@ -348,15 +349,53 @@ function checkMute(noteToCheck: Misskey.entities.Note, mutedWords: Array<string
 let renoting = false;
 
 const keymap = {
-	'r': () => reply(true),
-	'e|a|plus': () => react(true),
-	'(q)': () => { if (canRenote.value && !renoted.value && !renoting) renote(defaultStore.state.visibilityOnBoost); },
-	'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;
+		if (canRenote.value && !renoted.value && !renoting) renote(defaultStore.state.visibilityOnBoost);
+	},
+	'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', {
@@ -389,12 +428,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(),
+		});
 	});
 
 	useTooltip(quoteButton, async (showing) => {
@@ -408,12 +449,14 @@ if (!props.mock) {
 
 		if (users.length < 1) return;
 
-		os.popup(MkUsersTooltip, {
+		const { dispose } = os.popup(MkUsersTooltip, {
 			showing,
 			users,
 			count: appearNote.value.renoteCount,
 			targetElement: quoteButton.value,
-		}, {}, 'closed');
+		}, {
+			closed: () => dispose(),
+		});
 	});
 
 	if ($i) {
@@ -438,13 +481,15 @@ 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(),
+			});
 		});
 	}
 }
@@ -460,7 +505,7 @@ function boostVisibility() {
 }
 
 function renote(visibility: Visibility, localOnly: boolean = false) {
-	pleaseLogin();
+	pleaseLogin(undefined, pleaseLoginContext.value);
 	showMovedDialog();
 
 	renoting = true;
@@ -471,7 +516,9 @@ function renote(visibility: Visibility, localOnly: boolean = false) {
 			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) {
@@ -489,7 +536,9 @@ function renote(visibility: Visibility, localOnly: boolean = false) {
 			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) {
@@ -506,7 +555,7 @@ function renote(visibility: Visibility, localOnly: boolean = false) {
 }
 
 function quote() {
-	pleaseLogin();
+	pleaseLogin(undefined, pleaseLoginContext.value);
 	showMovedDialog();
 	if (props.mock) {
 		return;
@@ -529,7 +578,9 @@ function quote() {
 					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(),
+					});
 				}
 
 				os.toast(i18n.ts.quoted);
@@ -551,7 +602,9 @@ function quote() {
 					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(),
+					});
 				}
 
 				os.toast(i18n.ts.quoted);
@@ -560,22 +613,21 @@ function quote() {
 	}
 }
 
-function reply(viaKeyboard = false): void {
-	pleaseLogin();
+function reply(): void {
+	pleaseLogin(undefined, pleaseLoginContext.value);
 	if (props.mock) {
 		return;
 	}
 	os.post({
 		reply: appearNote.value,
 		channel: appearNote.value.channel,
-		animation: !viaKeyboard,
 	}).then(() => {
 		focus();
 	});
 }
 
 function like(): void {
-	pleaseLogin();
+	pleaseLogin(undefined, pleaseLoginContext.value);
 	showMovedDialog();
 	sound.playMisskeySfx('reaction');
 	if (props.mock) {
@@ -590,12 +642,14 @@ function like(): 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(),
+		});
 	}
 }
 
 function react(viaKeyboard = false): void {
-	pleaseLogin();
+	pleaseLogin(undefined, pleaseLoginContext.value);
 	showMovedDialog();
 	if (appearNote.value.reactionAcceptance === 'likeOnly') {
 		sound.playMisskeySfx('reaction');
@@ -613,7 +667,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();
@@ -667,7 +723,9 @@ function undoRenote(note) : 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(),
+		});
 	}
 }
 
@@ -706,15 +764,13 @@ 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 menuVersions(viaKeyboard = false): Promise<void> {
@@ -724,7 +780,7 @@ async function menuVersions(viaKeyboard = false): Promise<void> {
 	}).then(focus).finally(cleanup);
 }
 
-async function clip() {
+async function clip(): Promise<void> {
 	if (props.mock) {
 		return;
 	}
@@ -732,7 +788,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;
 	}
@@ -740,7 +796,7 @@ function showRenoteMenu(viaKeyboard = false): void {
 	function getUnrenote(): MenuItem {
 		return {
 			text: i18n.ts.unrenote,
-			icon: 'ph-trash ph-bold ph-lg',
+			icon: 'ti ti-trash',
 			danger: true,
 			action: () => {
 				misskeyApi('notes/delete', {
@@ -752,23 +808,19 @@ function showRenoteMenu(viaKeyboard = false): void {
 	}
 
 	if (isMyRenote) {
-		pleaseLogin();
+		pleaseLogin(undefined, 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);
 	}
 }
 
@@ -793,11 +845,11 @@ function blur() {
 }
 
 function focusBefore() {
-	focusPrev(rootEl.value ?? null);
+	focusPrev(rootEl.value);
 }
 
 function focusAfter() {
-	focusNext(rootEl.value ?? null);
+	focusNext(rootEl.value);
 }
 
 function readPromo() {
@@ -835,7 +887,7 @@ function emitUpdReaction(emoji: string, delta: number) {
 	&:focus-visible {
 		outline: none;
 
-		&:after {
+		&::after {
 			content: "";
 			pointer-events: none;
 			display: block;
@@ -848,7 +900,7 @@ function emitUpdReaction(emoji: string, delta: number) {
 			margin: auto;
 			width: calc(100% - 8px);
 			height: calc(100% - 8px);
-			border: solid 1px var(--focus);
+			border: solid 2px var(--focus);
 			border-radius: var(--radius);
 			box-sizing: border-box;
 		}
diff --git a/packages/frontend/src/components/SkNoteDetailed.vue b/packages/frontend/src/components/SkNoteDetailed.vue
index 7a23d0aa73..cca6c7a40c 100644
--- a/packages/frontend/src/components/SkNoteDetailed.vue
+++ b/packages/frontend/src/components/SkNoteDetailed.vue
@@ -10,13 +10,14 @@ 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 && !conversationLoaded" style="padding: 16px">
 		<MkButton style="margin: 0 auto;" primary rounded @click="loadConversation">{{ i18n.ts.loadConversation }}</MkButton>
 	</div>
 	<div v-if="isRenote" :class="$style.renote">
 		<MkAvatar :class="$style.renoteAvatar" :user="note.user" link preview/>
-		<i class="ph-rocket-launch ph-bold ph-lg" style="margin-right: 4px;"></i>
+		<i class="ti ti-repeat" style="margin-right: 4px;"></i>
 		<span :class="$style.renoteText">
 			<I18n :src="i18n.ts.renotedBy" tag="span">
 				<template #user>
@@ -27,16 +28,16 @@ SPDX-License-Identifier: AGPL-3.0-only
 			</I18n>
 		</span>
 		<div :class="$style.renoteInfo">
-			<button ref="renoteTime" class="_button" :class="$style.renoteTime" @click="showRenoteMenu()">
-				<i v-if="isMyRenote" class="ph-dots-three ph-bold ph-lg" style="margin-right: 4px;"></i>
+			<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>
 			<span v-if="note.visibility !== 'public'" style="margin-left: 0.5em;" :title="i18n.ts._visibility[note.visibility]">
-				<i v-if="note.visibility === 'home'" class="ph-house ph-bold ph-lg"></i>
-				<i v-else-if="note.visibility === 'followers'" class="ph-lock ph-bold ph-lg"></i>
-				<i v-else-if="note.visibility === 'specified'" ref="specified" class="ph-envelope ph-bold ph-lg"></i>
+				<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="ph-rocket ph-bold ph-lg"></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>
 	<template v-if="appearNote.reply && appearNote.reply.replyId">
@@ -64,12 +65,12 @@ SPDX-License-Identifier: AGPL-3.0-only
 				<div :class="$style.noteHeaderBody">
 					<div :class="$style.noteHeaderInfo">
 						<span v-if="appearNote.visibility !== 'public'" style="margin-left: 0.5em;" :title="i18n.ts._visibility[appearNote.visibility]">
-							<i v-if="appearNote.visibility === 'home'" class="ph-house ph-bold ph-lg"></i>
-							<i v-else-if="appearNote.visibility === 'followers'" class="ph-lock ph-bold ph-lg"></i>
-							<i v-else-if="appearNote.visibility === 'specified'" ref="specified" class="ph-envelope ph-bold ph-lg"></i>
+							<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.updatedAt" ref="menuVersionsButton" style="margin-left: 0.5em;" title="Edited" @mousedown="menuVersions()"><i class="ph-pencil-simple ph-bold ph-lg"></i></span>
-						<span v-if="appearNote.localOnly" style="margin-left: 0.5em;" :title="i18n.ts._visibility['disableFederation']"><i class="ph-rocket ph-bold ph-lg"></i></span>
+						<span v-if="appearNote.localOnly" style="margin-left: 0.5em;" :title="i18n.ts._visibility['disableFederation']"><i class="ti ti-rocket-off"></i></span>
 					</div>
 					<SkInstanceTicker v-if="showTicker" :instance="appearNote.user.instance"/>
 				</div>
@@ -105,7 +106,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 				<MkButton v-if="!allowAnim && animated" :class="$style.playMFMButton" :small="true" @click="animatedMFM()" @click.stop><i class="ph-play ph-bold ph-lg "></i> {{ i18n.ts._animatedMFM.play }}</MkButton>
 				<MkButton v-else-if="!defaultStore.state.animatedMfm && allowAnim && animated" :class="$style.playMFMButton" :small="true" @click="animatedMFM()" @click.stop><i class="ph-stop ph-bold ph-lg "></i> {{ i18n.ts._animatedMFM.stop }}</MkButton>
 				<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">
@@ -113,7 +114,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 				</div>
 				<div v-if="appearNote.renote" :class="$style.quote"><SkNoteSimple :note="appearNote.renote" :class="$style.quoteNote" :expandAllCws="props.expandAllCws"/></div>
 			</div>
-			<MkA v-if="appearNote.channel && !inChannel" :class="$style.channel" :to="`/channels/${appearNote.channel.id}`"><i class="ph-television ph-bold ph-lg"></i> {{ appearNote.channel.name }}</MkA>
+			<MkA v-if="appearNote.channel && !inChannel" :class="$style.channel" :to="`/channels/${appearNote.channel.id}`"><i class="ti ti-device-tv"></i> {{ appearNote.channel.name }}</MkA>
 		</div>
 		<div :class="$style.noteFooterInfo">
 			<div v-if="appearNote.updatedAt">
@@ -126,7 +127,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 		<MkReactionsViewer v-if="appearNote.reactionAcceptance !== 'likeOnly'" ref="reactionsViewer" :note="appearNote"/>
 		<footer :class="$style.footer">
 			<button class="_button" :class="$style.noteFooterButton" @click="reply()">
-				<i class="ph-arrow-u-up-left ph-bold ph-lg"></i>
+				<i class="ti ti-arrow-back-up"></i>
 				<p v-if="appearNote.repliesCount > 0" :class="$style.noteFooterButtonCount">{{ number(appearNote.repliesCount) }}</p>
 			</button>
 			<button
@@ -135,13 +136,13 @@ SPDX-License-Identifier: AGPL-3.0-only
 				class="_button"
 				:class="$style.noteFooterButton"
 				:style="renoted ? 'color: var(--accent) !important;' : ''"
-				@mousedown="renoted ? undoRenote() : boostVisibility()"
+				@mousedown.prevent="renoted ? undoRenote() : boostVisibility()"
 			>
-				<i class="ph-rocket-launch ph-bold ph-lg"></i>
+				<i class="ti ti-repeat"></i>
 				<p v-if="appearNote.renoteCount > 0" :class="$style.noteFooterButtonCount">{{ number(appearNote.renoteCount) }}</p>
 			</button>
 			<button v-else class="_button" :class="$style.noteFooterButton" disabled>
-				<i class="ph-prohibit ph-bold ph-lg"></i>
+				<i class="ti ti-ban"></i>
 			</button>
 			<button
 				v-if="canRenote"
@@ -156,23 +157,23 @@ SPDX-License-Identifier: AGPL-3.0-only
 				<i class="ph-heart ph-bold ph-lg"></i>
 			</button>
 			<button ref="reactButton" :class="$style.noteFooterButton" class="_button" @click="toggleReact()">
-				<i v-if="appearNote.reactionAcceptance === 'likeOnly' && appearNote.myReaction != null" class="ph-heart ph-bold ph-lg" style="color: var(--eventReactionHeart);"></i>
-				<i v-else-if="appearNote.myReaction != null" class="ph-minus ph-bold ph-lg" style="color: var(--accent);"></i>
-				<i v-else-if="appearNote.reactionAcceptance === 'likeOnly'" class="ph-heart ph-bold ph-lg"></i>
+				<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-else-if="appearNote.reactionAcceptance === 'likeOnly'" class="ti ti-heart"></i>
 				<i v-else class="ph-smiley ph-bold ph-lg"></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()">
-				<i class="ph-paperclip ph-bold ph-lg"></i>
+			<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()">
-				<i class="ph-dots-three ph-bold ph-lg"></i>
+			<button ref="menuButton" class="_button" :class="$style.noteFooterButton" @mousedown.prevent="showMenu()">
+				<i class="ti ti-dots"></i>
 			</button>
 		</footer>
 	</article>
 	<div :class="$style.tabs">
-		<button class="_button" :class="[$style.tab, { [$style.tabActive]: tab === 'replies' }]" @click="tab = 'replies'"><i class="ph-arrow-u-up-left ph-bold ph-lg"></i> {{ i18n.ts.replies }}</button>
-		<button class="_button" :class="[$style.tab, { [$style.tabActive]: tab === 'renotes' }]" @click="tab = 'renotes'"><i class="ph-rocket-launch ph-bold ph-lg"></i> {{ i18n.ts.renotes }}</button>
+		<button class="_button" :class="[$style.tab, { [$style.tabActive]: tab === 'replies' }]" @click="tab = 'replies'"><i class="ti ti-arrow-back-up"></i> {{ i18n.ts.replies }}</button>
+		<button class="_button" :class="[$style.tab, { [$style.tabActive]: tab === 'renotes' }]" @click="tab = 'renotes'"><i class="ti ti-repeat"></i> {{ i18n.ts.renotes }}</button>
 		<button class="_button" :class="[$style.tab, { [$style.tabActive]: tab === 'quotes' }]" @click="tab = 'quotes'"><i class="ph-quotes ph-bold ph-lg"></i> {{ i18n.ts._notification._types.quote }}</button>
 		<button class="_button" :class="[$style.tab, { [$style.tabActive]: tab === 'reactions' }]" @click="tab = 'reactions'"><i class="ph-smiley ph-bold ph-lg"></i> {{ i18n.ts.reactions }}</button>
 	</div>
@@ -244,7 +245,7 @@ import MkPoll from '@/components/MkPoll.vue';
 import MkUsersTooltip from '@/components/MkUsersTooltip.vue';
 import MkUrlPreview from '@/components/MkUrlPreview.vue';
 import SkInstanceTicker from '@/components/SkInstanceTicker.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 number from '@/filters/number.js';
@@ -257,6 +258,7 @@ import { reactionPicker } from '@/scripts/reaction-picker.js';
 import { extractUrlFromMfm } from '@/scripts/extract-url-from-mfm.js';
 import { $i } from '@/account.js';
 import { i18n } from '@/i18n.js';
+import { host } from '@/config.js';
 import { getNoteClipMenu, getNoteMenu } from '@/scripts/get-note-menu.js';
 import { getNoteVersionsMenu } from '@/scripts/get-note-versions-menu.js';
 import { useNoteCapture } from '@/scripts/use-note-capture.js';
@@ -272,6 +274,8 @@ import MkReactionIcon from '@/components/MkReactionIcon.vue';
 import MkButton from '@/components/MkButton.vue';
 import { boostMenuItems, type Visibility } from '@/scripts/boost-quote.js';
 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;
@@ -304,14 +308,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 noteEl = shallowRef<HTMLElement>();
@@ -323,7 +320,8 @@ const reactButton = shallowRef<HTMLElement>();
 const quoteButton = shallowRef<HTMLElement>();
 const clipButton = shallowRef<HTMLElement>();
 const likeButton = 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(defaultStore.state.uncollapseCW);
 const isDeleted = ref(false);
@@ -358,14 +356,31 @@ if ($i) {
 
 let renoting = false;
 
+const pleaseLoginContext = computed<OpenOnRemoteOptions>(() => ({
+	type: 'lookup',
+	url: `https://${host}/notes/${appearNote.value.id}`,
+}));
+
 const keymap = {
-	'r': () => reply(true),
-	'e|a|plus': () => react(true),
-	'(q)': () => { if (canRenote.value && !renoted.value && !renoting) renote(defaultStore.state.visibilityOnBoost); },
-	'esc': blur,
-	'm|o': () => showMenu(true),
-	's': () => showContent.value !== showContent.value,
-};
+	'r': () => reply(),
+	'e|a|plus': () => react(),
+	'q': () => { if (canRenote.value && !renoted.value && !renoting) renote(defaultStore.state.visibilityOnBoost); },
+	'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', {
@@ -425,12 +440,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(),
+	});
 });
 
 useTooltip(quoteButton, async (showing) => {
@@ -444,12 +461,14 @@ useTooltip(quoteButton, async (showing) => {
 
 	if (users.length < 1) return;
 
-	os.popup(MkUsersTooltip, {
+	const { dispose } = os.popup(MkUsersTooltip, {
 		showing,
 		users,
 		count: appearNote.value.renoteCount,
 		targetElement: quoteButton.value,
-	}, {}, 'closed');
+	}, {
+		closed: () => dispose(),
+	});
 });
 
 function boostVisibility() {
@@ -474,18 +493,20 @@ 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(visibility: Visibility, localOnly: boolean = false) {
-	pleaseLogin();
+	pleaseLogin(undefined, pleaseLoginContext.value);
 	showMovedDialog();
 
 	renoting = true;
@@ -496,7 +517,9 @@ function renote(visibility: Visibility, localOnly: boolean = false) {
 			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(),
+			});
 		}
 
 		misskeyApi('notes/create', {
@@ -512,7 +535,9 @@ function renote(visibility: Visibility, localOnly: boolean = false) {
 			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(),
+			});
 		}
 
 		misskeyApi('notes/create', {
@@ -527,7 +552,7 @@ function renote(visibility: Visibility, localOnly: boolean = false) {
 }
 
 function quote() {
-	pleaseLogin();
+	pleaseLogin(undefined, pleaseLoginContext.value);
 	showMovedDialog();
 
 	if (appearNote.value.channel) {
@@ -547,7 +572,9 @@ function quote() {
 					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(),
+					});
 				}
 
 				os.toast(i18n.ts.quoted);
@@ -569,7 +596,9 @@ function quote() {
 					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(),
+					});
 				}
 
 				os.toast(i18n.ts.quoted);
@@ -578,20 +607,19 @@ function quote() {
 	}
 }
 
-function reply(viaKeyboard = false): void {
-	pleaseLogin();
+function reply(): void {
+	pleaseLogin(undefined, 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(undefined, pleaseLoginContext.value);
 	showMovedDialog();
 	if (appearNote.value.reactionAcceptance === 'likeOnly') {
 		sound.playMisskeySfx('reaction');
@@ -600,12 +628,14 @@ function react(viaKeyboard = false): void {
 			noteId: appearNote.value.id,
 			override: defaultLike.value,
 		});
-		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();
@@ -626,7 +656,7 @@ function react(viaKeyboard = false): void {
 }
 
 function like(): void {
-	pleaseLogin();
+	pleaseLogin(undefined, pleaseLoginContext.value);
 	showMovedDialog();
 	sound.playMisskeySfx('reaction');
 	misskeyApi('notes/like', {
@@ -638,7 +668,9 @@ function like(): 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(),
+		});
 	}
 }
 
@@ -663,7 +695,9 @@ function undoRenote() : 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(),
+		});
 	}
 }
 
@@ -696,30 +730,26 @@ 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 menuVersions(viaKeyboard = false): Promise<void> {
+async function menuVersions(): Promise<void> {
 	const { menu, cleanup } = await getNoteVersionsMenu({ note: note.value, menuVersionsButton });
-	os.popupMenu(menu, menuVersionsButton.value, {
-		viaKeyboard,
-	}).then(focus).finally(cleanup);
+	os.popupMenu(menu, menuVersionsButton.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(undefined, pleaseLoginContext.value);
 	os.popupMenu([{
 		text: i18n.ts.unrenote,
-		icon: 'ph-trash ph-bold ph-lg',
+		icon: 'ti ti-trash',
 		danger: true,
 		action: () => {
 			misskeyApi('notes/delete', {
@@ -727,9 +757,7 @@ function showRenoteMenu(viaKeyboard = false): void {
 			});
 			isDeleted.value = true;
 		},
-	}], renoteTime.value, {
-		viaKeyboard: viaKeyboard,
-	});
+	}], renoteTime.value);
 }
 
 function focus() {
@@ -829,6 +857,28 @@ onUnmounted(() => {
 	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(--focus);
+			border-radius: var(--radius);
+			box-sizing: border-box;
+		}
+	}
 }
 
 .footer {
diff --git a/packages/frontend/src/components/SkNoteHeader.vue b/packages/frontend/src/components/SkNoteHeader.vue
index 2f177815ee..45218fafb6 100644
--- a/packages/frontend/src/components/SkNoteHeader.vue
+++ b/packages/frontend/src/components/SkNoteHeader.vue
@@ -61,13 +61,13 @@ SPDX-License-Identifier: AGPL-3.0-only
 			<MkTime :time="note.createdAt" colored/>
 		</MkA>
 		<span v-if="note.visibility !== 'public'" style="margin-left: 0.5em;" :title="i18n.ts._visibility[note.visibility]">
-			<i v-if="note.visibility === 'home'" class="ph-house ph-bold ph-lg"></i>
-			<i v-else-if="note.visibility === 'followers'" class="ph-lock ph-bold ph-lg"></i>
-			<i v-else-if="note.visibility === 'specified'" ref="specified" class="ph-envelope ph-bold ph-lg"></i>
+			<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.updatedAt" ref="menuVersionsButton" style="margin-left: 0.5em; cursor: pointer;" title="Edited" @mousedown="menuVersions()"><i class="ph-pencil-simple ph-bold ph-lg"></i></span>
-		<span v-if="note.localOnly" style="margin-left: 0.5em;" :title="i18n.ts._visibility['disableFederation']"><i class="ph-rocket ph-bold ph-lg"></i></span>
-		<span v-if="note.channel" style="margin-left: 0.5em;" :title="note.channel.name"><i class="ph-television ph-bold ph-lg"></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>
 </header>
 </template>
diff --git a/packages/frontend/src/components/SkNoteSub.vue b/packages/frontend/src/components/SkNoteSub.vue
index dc1d5b10b2..c2986b2524 100644
--- a/packages/frontend/src/components/SkNoteSub.vue
+++ b/packages/frontend/src/components/SkNoteSub.vue
@@ -76,7 +76,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 		<SkNoteSub v-for="reply in replies" :key="reply.id" :note="reply" :class="[$style.reply, { [$style.single]: replies.length === 1 }]" :detail="true" :depth="depth + 1" :expandAllCws="props.expandAllCws" :onDeleteCallback="removeReply" :isReply="props.isReply"/>
 	</template>
 	<div v-else :class="$style.more">
-		<MkA class="_link" :to="notePage(note)">{{ i18n.ts.continueThread }} <i class="ph-caret-double-right ph-bold ph-lg"></i></MkA>
+		<MkA class="_link" :to="notePage(note)">{{ i18n.ts.continueThread }} <i class="ti ti-chevron-double-right"></i></MkA>
 	</div>
 </div>
 <div v-else :class="$style.muted" @click="muted = false">
@@ -221,7 +221,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();
@@ -252,7 +254,9 @@ function like(): 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(),
+		});
 	}
 }
 
@@ -277,7 +281,9 @@ function undoRenote() : 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(),
+		});
 	}
 }
 
@@ -305,7 +311,9 @@ function renote(visibility: Visibility, localOnly: boolean = false) {
 			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(),
+			});
 		}
 
 		misskeyApi('notes/create', {
@@ -321,7 +329,9 @@ function renote(visibility: Visibility, localOnly: boolean = false) {
 			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(),
+			});
 		}
 
 		misskeyApi('notes/create', {
@@ -356,7 +366,9 @@ function quote() {
 					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(),
+					});
 				}
 
 				os.toast(i18n.ts.quoted);
@@ -378,7 +390,9 @@ function quote() {
 					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(),
+					});
 				}
 
 				os.toast(i18n.ts.quoted);
diff --git a/packages/frontend/src/components/form/link.vue b/packages/frontend/src/components/form/link.vue
index 8c8a343010..164606e1f9 100644
--- a/packages/frontend/src/components/form/link.vue
+++ b/packages/frontend/src/components/form/link.vue
@@ -10,7 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 		<span :class="$style.text"><slot></slot></span>
 		<span :class="$style.suffix">
 			<span :class="$style.suffixText"><slot name="suffix"></slot></span>
-			<i class="ph-arrow-square-out ph-bold ph-lg"></i>
+			<i class="ti ti-external-link"></i>
 		</span>
 	</a>
 	<a v-else-if="onClick" :class="[$style.main, { [$style.active]: active }]" class="_button" :behavior="behavior" @click="onClick">
@@ -26,7 +26,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 		<span :class="$style.text"><slot></slot></span>
 		<span :class="$style.suffix">
 			<span :class="$style.suffixText"><slot name="suffix"></slot></span>
-			<i class="ph-caret-right ph-bold ph-lg"></i>
+			<i class="ti ti-chevron-right"></i>
 		</span>
 	</MkA>
 </div>
diff --git a/packages/frontend/src/components/form/suspense.vue b/packages/frontend/src/components/form/suspense.vue
index 54566dc135..5226c61d68 100644
--- a/packages/frontend/src/components/form/suspense.vue
+++ b/packages/frontend/src/components/form/suspense.vue
@@ -12,8 +12,8 @@ SPDX-License-Identifier: AGPL-3.0-only
 </div>
 <div v-else>
 	<div :class="$style.error">
-		<div><i class="ph-warning ph-bold ph-lg"></i> {{ i18n.ts.somethingHappened }}</div>
-		<MkButton inline style="margin-top: 16px;" @click="retry"><i class="ph-arrow-clockwise ph-bold ph-lg"></i> {{ i18n.ts.retry }}</MkButton>
+		<div><i class="ti ti-alert-triangle"></i> {{ i18n.ts.somethingHappened }}</div>
+		<MkButton inline style="margin-top: 16px;" @click="retry"><i class="ti ti-reload"></i> {{ i18n.ts.retry }}</MkButton>
 	</div>
 </div>
 </template>
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 5b67c3f7bd..e0303dbb27 100644
--- a/packages/frontend/src/components/global/MkA.vue
+++ b/packages/frontend/src/components/global/MkA.vue
@@ -16,7 +16,7 @@ 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 { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
 import { url } from '@/config.js';
 import { i18n } from '@/i18n.js';
 import { useRouter } from '@/router/supplier.js';
@@ -55,25 +55,25 @@ function onContextmenu(ev) {
 		type: 'label',
 		text: props.to,
 	}, {
-		icon: 'ph-app-window ph-bold ph-lg',
+		icon: 'ti ti-app-window',
 		text: i18n.ts.openInWindow,
 		action: () => {
 			os.pageWindow(props.to);
 		},
 	}, {
-		icon: 'ph-eject ph-bold ph-lg',
+		icon: 'ti ti-player-eject',
 		text: i18n.ts.showInPage,
 		action: () => {
 			router.push(props.to, 'forcePage');
 		},
 	}, { type: 'divider' }, {
-		icon: 'ph-arrow-square-out ph-bold ph-lg',
+		icon: 'ti ti-external-link',
 		text: i18n.ts.openInNewTab,
 		action: () => {
 			window.open(props.to, '_blank', 'noopener');
 		},
 	}, {
-		icon: 'ph-link ph-bold ph-lg',
+		icon: 'ti ti-link',
 		text: i18n.ts.copyLink,
 		action: () => {
 			copyToClipboard(`${url}${props.to}`);
diff --git a/packages/frontend/src/components/global/MkAcct.vue b/packages/frontend/src/components/global/MkAcct.vue
index 8cb082585b..bbcb070803 100644
--- a/packages/frontend/src/components/global/MkAcct.vue
+++ b/packages/frontend/src/components/global/MkAcct.vue
@@ -21,7 +21,7 @@ import { host as hostRaw } from '@/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 c01211443d..bee1d9ca4c 100644
--- a/packages/frontend/src/components/global/MkAd.vue
+++ b/packages/frontend/src/components/global/MkAd.vue
@@ -26,7 +26,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 			}"
 		>
 			<img :src="chosen.imageUrl" :class="$style.img">
-			<button class="_button" :class="$style.i" @click.prevent.stop="toggleMenu"><i :class="$style.iIcon" class="ph-info ph-bold ph-lg"></i></button>
+			<button class="_button" :class="$style.i" @click.prevent.stop="toggleMenu"><i :class="$style.iIcon" class="ti ti-info-circle"></i></button>
 		</component>
 	</div>
 	<div v-else :class="$style.menu">
diff --git a/packages/frontend/src/components/global/MkCustomEmoji.vue b/packages/frontend/src/components/global/MkCustomEmoji.vue
index 8ba4e396b7..4cacc7b292 100644
--- a/packages/frontend/src/components/global/MkCustomEmoji.vue
+++ b/packages/frontend/src/components/global/MkCustomEmoji.vue
@@ -31,7 +31,7 @@ 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';
@@ -91,7 +91,7 @@ function onClick(ev: MouseEvent) {
 			text: `:${props.name}:`,
 		}, {
 			text: i18n.ts.copy,
-			icon: 'ph-copy ph-bold ph-lg',
+			icon: 'ti ti-copy',
 			action: () => {
 				copyToClipboard(`:${props.name}:`);
 				os.success();
@@ -105,14 +105,14 @@ function onClick(ev: MouseEvent) {
 			},
 		}] : []), {
 			text: i18n.ts.info,
-			icon: 'ph-info ph-bold ph-lg',
+			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);
diff --git a/packages/frontend/src/components/global/MkEmoji.vue b/packages/frontend/src/components/global/MkEmoji.vue
index b6b5aaba32..c485305376 100644
--- a/packages/frontend/src/components/global/MkEmoji.vue
+++ b/packages/frontend/src/components/global/MkEmoji.vue
@@ -14,7 +14,7 @@ import { char2fluentEmojiFilePath, char2twemojiFilePath, char2tossfaceFilePath }
 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';
 
@@ -45,7 +45,7 @@ function onClick(ev: MouseEvent) {
 			text: props.emoji,
 		}, {
 			text: i18n.ts.copy,
-			icon: 'ph-copy ph-bold ph-lg',
+			icon: 'ti ti-copy',
 			action: () => {
 				copyToClipboard(props.emoji);
 				os.success();
diff --git a/packages/frontend/src/components/global/MkError.vue b/packages/frontend/src/components/global/MkError.vue
index 2976cd7be8..64c828ba4a 100644
--- a/packages/frontend/src/components/global/MkError.vue
+++ b/packages/frontend/src/components/global/MkError.vue
@@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 <Transition :name="defaultStore.state.animation ? '_transition_zoom' : ''" appear>
 	<div :class="$style.root">
 		<img :class="$style.img" :src="serverErrorImageUrl" class="_ghost"/>
-		<p :class="$style.text"><i class="ph-warning ph-bold ph-lg"></i> {{ i18n.ts.somethingHappened }}</p>
+		<p :class="$style.text"><i class="ti ti-alert-triangle"></i> {{ i18n.ts.somethingHappened }}</p>
 		<MkButton :class="$style.button" @click="() => emit('retry')">{{ i18n.ts.retry }}</MkButton>
 	</div>
 </Transition>
diff --git a/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.ts b/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.ts
index 1bc33585f2..a3a2b9f319 100644
--- a/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.ts
+++ b/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.ts
@@ -68,7 +68,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 useAnim = defaultStore.state.advancedMfm && defaultStore.state.animatedMfm ? true : props.isAnim ? true : false;
@@ -80,7 +80,7 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEven
 
 	const isBlock = props.isBlock ?? false;
 
-	const MkFormula = defineAsyncComponent(() => import('@/components/MkFormula.vue'));
+	const SkFormula = defineAsyncComponent(() => import('@/components/SkFormula.vue'));
 
 	/**
 	 * Gen Vue Elements from MFM AST
@@ -360,7 +360,7 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEven
 							style: 'display: inline-block; font-size: 90%; border: solid 1px var(--divider); border-radius: var(--radius-ellipse); padding: 4px 10px 4px 6px;',
 						}, [
 							h('i', {
-								class: 'ph-clock ph-bold ph-lg',
+								class: 'ti ti-clock',
 								style: 'margin-right: 0.25em;',
 							}),
 							h(MkTime, {
@@ -499,14 +499,14 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEven
 			}
 
 			case 'mathInline': {
-				return [h('bdi', h(MkFormula, {
+				return [h('bdi', h(SkFormula, {
 					formula: token.props.formula,
 					block: false,
 				}))];
 			}
 
 			case 'mathBlock': {
-				return [h('bdi', { class: 'block' }, h(MkFormula, {
+				return [h('bdi', { class: 'block' }, h(SkFormula, {
 					formula: token.props.formula,
 					block: true,
 				}))];
diff --git a/packages/frontend/src/components/global/MkPageHeader.stories.impl.ts b/packages/frontend/src/components/global/MkPageHeader.stories.impl.ts
index 25f5051648..1d079edd2c 100644
--- a/packages/frontend/src/components/global/MkPageHeader.stories.impl.ts
+++ b/packages/frontend/src/components/global/MkPageHeader.stories.impl.ts
@@ -59,7 +59,7 @@ export const Icon = {
 		tabs: [
 			{
 				...OneTab.args.tabs[0],
-				icon: 'ph-house ph-bold ph-lg',
+				icon: 'ti ti-home',
 			},
 		],
 	},
@@ -86,17 +86,17 @@ export const SomeTabs = {
 			{
 				key: 'princess',
 				title: 'Princess',
-				icon: 'ph-crown ph-bold ph-lg',
+				icon: 'ti ti-crown',
 			},
 			{
 				key: 'fairy',
 				title: 'Fairy',
-				icon: 'ph-snowflake ph-bold ph-lg',
+				icon: 'ti ti-snowflake',
 			},
 			{
 				key: 'angel',
 				title: 'Angel',
-				icon: 'ph-feather ph-bold ph-lg',
+				icon: 'ti ti-feather',
 			},
 		],
 	},
diff --git a/packages/frontend/src/components/global/MkStickyContainer.vue b/packages/frontend/src/components/global/MkStickyContainer.vue
index 89993e1b8e..b12dc8cb31 100644
--- a/packages/frontend/src/components/global/MkStickyContainer.vue
+++ b/packages/frontend/src/components/global/MkStickyContainer.vue
@@ -8,7 +8,11 @@ SPDX-License-Identifier: AGPL-3.0-only
 	<div ref="headerEl">
 		<slot name="header"></slot>
 	</div>
-	<div ref="bodyEl" :data-sticky-container-header-height="headerHeight">
+	<div
+		ref="bodyEl"
+		:data-sticky-container-header-height="headerHeight"
+		:data-sticky-container-footer-height="footerHeight"
+	>
 		<slot></slot>
 	</div>
 	<div ref="footerEl">
diff --git a/packages/frontend/src/components/global/MkTime.vue b/packages/frontend/src/components/global/MkTime.vue
index 23fe99bd9c..027b226f3f 100644
--- a/packages/frontend/src/components/global/MkTime.vue
+++ b/packages/frontend/src/components/global/MkTime.vue
@@ -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*/);
 
diff --git a/packages/frontend/src/components/global/MkUrl.vue b/packages/frontend/src/components/global/MkUrl.vue
index 19888ae146..15595ba515 100644
--- a/packages/frontend/src/components/global/MkUrl.vue
+++ b/packages/frontend/src/components/global/MkUrl.vue
@@ -21,7 +21,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 	<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="ph-arrow-square-out ph-bold ph-lg"></i>
+	<i v-if="target === '_blank'" :class="$style.icon" class="ti ti-external-link"></i>
 </component>
 </template>
 
@@ -51,11 +51,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..19bd794a5d 100644
--- a/packages/frontend/src/components/global/RouterView.vue
+++ b/packages/frontend/src/components/global/RouterView.vue
@@ -53,14 +53,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/page/page.dynamic.vue b/packages/frontend/src/components/page/page.dynamic.vue
index 7f80f0c455..8c511a690d 100644
--- a/packages/frontend/src/components/page/page.dynamic.vue
+++ b/packages/frontend/src/components/page/page.dynamic.vue
@@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 <!-- 動的ページのブロックの代替。利用できないということを表示する -->
 <template>
 <div :class="$style.root">
-	<div :class="$style.heading"><i class="ph ph-dice-5 ph-bold ph-lg"></i> {{ i18n.ts._pages.blocks.dynamic }}</div>
+	<div :class="$style.heading"><i class="ti ti-dice-5"></i> {{ i18n.ts._pages.blocks.dynamic }}</div>
 	<I18n :src="i18n.ts._pages.blocks.dynamicDescription" tag="div" :class="$style.text">
 		<template #play>
 			<MkA to="/play" class="_link">Play</MkA>
diff --git a/packages/frontend/src/const.ts b/packages/frontend/src/const.ts
index 5109c34c02..c94c0d4408 100644
--- a/packages/frontend/src/const.ts
+++ b/packages/frontend/src/const.ts
@@ -119,6 +119,7 @@ export const notificationTypes = [
 	'roleAssigned',
 	'achievementEarned',
 	'app',
+	'edited'
 ] as const;
 export const obsoleteNotificationTypes = ['pollVote', 'groupInvited'] as const;
 
@@ -140,6 +141,7 @@ export const ROLE_POLICIES = [
 	'canHideAds',
 	'driveCapacityMb',
 	'alwaysMarkNsfw',
+	'canUpdateBioMedia',
 	'pinLimit',
 	'antennaLimit',
 	'wordMuteLimit',
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/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/user.ts b/packages/frontend/src/filters/user.ts
index b713d41789..a87766764d 100644
--- a/packages/frontend/src/filters/user.ts
+++ b/packages/frontend/src/filters/user.ts
@@ -6,7 +6,7 @@
 import * as Misskey from 'misskey-js';
 import { url } from '@/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..10d6adbcd0 100644
--- a/packages/frontend/src/i18n.ts
+++ b/packages/frontend/src/i18n.ts
@@ -11,6 +11,5 @@ import { I18n } from '@/scripts/i18n.js';
 export const i18n = markRaw(new I18n<Locale>(locale));
 
 export function updateI18n(newLocale: Locale) {
-	// @ts-expect-error -- private field
 	i18n.locale = newLocale;
 }
diff --git a/packages/frontend/src/navbar.ts b/packages/frontend/src/navbar.ts
index b71c15d19f..ccd7034968 100644
--- a/packages/frontend/src/navbar.ts
+++ b/packages/frontend/src/navbar.ts
@@ -18,7 +18,7 @@ import { unisonReload } from '@/scripts/unison-reload.js';
 export const navbarItemDef = reactive({
 	notifications: {
 		title: i18n.ts.notifications,
-		icon: 'ph-bell ph-bold ph-lg',
+		icon: 'ti ti-bell',
 		show: computed(() => $i != null),
 		indicated: computed(() => $i != null && $i.hasUnreadNotification),
 		indicateValue: computed(() => {
@@ -34,66 +34,66 @@ export const navbarItemDef = reactive({
 	},
 	drive: {
 		title: i18n.ts.drive,
-		icon: 'ph-cloud ph-bold ph-lg',
+		icon: 'ti ti-cloud',
 		show: computed(() => $i != null),
 		to: '/my/drive',
 	},
 	followRequests: {
 		title: i18n.ts.followRequests,
-		icon: 'ph-user-plus ph-bold ph-lg',
+		icon: 'ti ti-user-plus',
 		show: computed(() => $i != null && $i.isLocked),
 		indicated: computed(() => $i != null && $i.hasPendingReceivedFollowRequest),
 		to: '/my/follow-requests',
 	},
 	explore: {
 		title: i18n.ts.explore,
-		icon: 'ph-hash ph-bold ph-lg',
+		icon: 'ti ti-hash',
 		to: '/explore',
 	},
 	announcements: {
 		title: i18n.ts.announcements,
-		icon: 'ph-megaphone ph-bold ph-lg',
+		icon: 'ti ti-speakerphone',
 		indicated: computed(() => $i != null && $i.hasUnreadAnnouncement),
 		to: '/announcements',
 	},
 	search: {
 		title: i18n.ts.search,
-		icon: 'ph-magnifying-glass ph-bold ph-lg',
+		icon: 'ti ti-search',
 		to: '/search',
 	},
 	lookup: {
 		title: i18n.ts.lookup,
-		icon: 'ph-binoculars ph-bold ph-lg',
+		icon: 'ti ti-world-search',
 		action: (ev) => {
 			lookup();
 		},
 	},
 	lists: {
 		title: i18n.ts.lists,
-		icon: 'ph-list ph-bold ph-lg',
+		icon: 'ti ti-list',
 		show: computed(() => $i != null),
 		to: '/my/lists',
 	},
 	antennas: {
 		title: i18n.ts.antennas,
-		icon: 'ph-flying-saucer ph-bold ph-lg',
+		icon: 'ti ti-antenna',
 		show: computed(() => $i != null),
 		to: '/my/antennas',
 	},
 	favorites: {
 		title: i18n.ts.favorites,
-		icon: 'ph-star ph-bold ph-lg',
+		icon: 'ti ti-star',
 		show: computed(() => $i != null),
 		to: '/my/favorites',
 	},
 	pages: {
 		title: i18n.ts.pages,
-		icon: 'ph-newspaper ph-bold ph-lg',
+		icon: 'ti ti-news',
 		to: '/pages',
 	},
 	play: {
 		title: 'Play',
-		icon: 'ph-play ph-bold ph-lg',
+		icon: 'ti ti-player-play',
 		to: '/play',
 	},
 	gallery: {
@@ -103,29 +103,29 @@ export const navbarItemDef = reactive({
 	},
 	clips: {
 		title: i18n.ts.clip,
-		icon: 'ph-paperclip ph-bold ph-lg',
+		icon: 'ti ti-paperclip',
 		show: computed(() => $i != null),
 		to: '/my/clips',
 	},
 	channels: {
 		title: i18n.ts.channel,
-		icon: 'ph-television ph-bold ph-lg',
+		icon: 'ti ti-device-tv',
 		to: '/channels',
 	},
 	achievements: {
 		title: i18n.ts.achievements,
-		icon: 'ph-trophy ph-bold ph-lg',
+		icon: 'ti ti-medal',
 		show: computed(() => $i != null && instance.enableAchievements),
 		to: '/my/achievements',
 	},
 	games: {
 		title: 'Games',
-		icon: 'ph-game-controller ph-bold ph-lg',
+		icon: 'ti ti-device-gamepad',
 		to: '/games',
 	},
 	ui: {
 		title: i18n.ts.switchUi,
-		icon: 'ph-devices ph-bold ph-lg',
+		icon: 'ti ti-devices',
 		action: (ev) => {
 			os.popupMenu([{
 				text: i18n.ts.default,
@@ -153,27 +153,27 @@ export const navbarItemDef = reactive({
 	},
 	about: {
 		title: i18n.ts.about,
-		icon: 'ph-info ph-bold ph-lg',
+		icon: 'ti ti-info-circle',
 		action: (ev) => {
 			openInstanceMenu(ev);
 		},
 	},
 	reload: {
 		title: i18n.ts.reload,
-		icon: 'ph-arrows-clockwise ph-bold ph-lg',
+		icon: 'ti ti-refresh',
 		action: (ev) => {
 			location.reload();
 		},
 	},
 	profile: {
 		title: i18n.ts.profile,
-		icon: 'ph-user ph-bold ph-lg',
+		icon: 'ti ti-user',
 		show: computed(() => $i != null),
 		to: `/@${$i?.username}`,
 	},
 	cacheClear: {
 		title: i18n.ts.clearCache,
-		icon: 'ph-trash ph-bold ph-lg',
+		icon: 'ti ti-trash',
 		action: (ev) => {
 			clearCache();
 		},
diff --git a/packages/frontend/src/os.ts b/packages/frontend/src/os.ts
index 6adf2e590b..f6f4d62d50 100644
--- a/packages/frontend/src/os.ts
+++ b/packages/frontend/src/os.ts
@@ -5,12 +5,13 @@
 
 // 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 { 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';
@@ -22,8 +23,11 @@ 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';
 
 export const openingWindowsCount = ref(0);
 
@@ -123,11 +127,13 @@ 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;
 }
@@ -173,28 +179,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,
 	};
 
@@ -206,16 +208,20 @@ 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, renderMfm = false) {
-	popup(MkToast, {
+	const { dispose } = popup(MkToast, {
 		message,
 		renderMfm,
-	}, {}, 'closed');
+	}, {
+		closed: () => dispose(),
+	});
 }
 
 export function alert(props: {
@@ -224,11 +230,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(),
+		});
 	});
 }
 
@@ -240,14 +247,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(),
+		});
 	});
 }
 
@@ -269,7 +277,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,
@@ -283,7 +291,8 @@ export function actions<T extends {
 			done: result => {
 				resolve(result ? result : { canceled: true });
 			},
-		}, 'closed');
+			closed: () => dispose(),
+		});
 	});
 }
 
@@ -331,7 +340,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: {
@@ -346,7 +355,8 @@ export function inputText(props: {
 			done: result => {
 				resolve(result ? result : { canceled: true });
 			},
-		}, 'closed');
+			closed: () => dispose(),
+		});
 	});
 }
 
@@ -385,7 +395,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: {
@@ -398,7 +408,8 @@ export function inputNumber(props: {
 			done: result => {
 				resolve(result ? result : { canceled: true });
 			},
-		}, 'closed');
+			closed: () => dispose(),
+		});
 	});
 }
 
@@ -413,7 +424,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: {
@@ -425,7 +436,8 @@ export function inputDate(props: {
 			done: result => {
 				resolve(result ? { result: new Date(result.result), canceled: false } : { result: undefined, canceled: true });
 			},
-		}, 'closed');
+			closed: () => dispose(),
+		});
 	});
 }
 
@@ -435,23 +447,29 @@ 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: {
 	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;
 } | {
@@ -461,10 +479,10 @@ export function select<C = any>(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;
 } | {
@@ -474,28 +492,29 @@ export function select<C = any>(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(),
+		});
 	});
 }
 
@@ -505,53 +524,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,
 		}, {
@@ -560,13 +583,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,
 		}, {
@@ -575,20 +599,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(),
+		});
 	});
 }
 
@@ -597,7 +623,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,
@@ -605,73 +631,88 @@ 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();
+	pleaseLogin(undefined, (props.initialText || props.initialNote ? {
+		type: 'share',
+		params: {
+			text: props.initialText ?? props.initialNote.text,
+			visibility: props.initialVisibility ?? props.initialNote?.visibility,
+			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 6b1eff5bcb..dbc113a766 100644
--- a/packages/frontend/src/pages/_error_.vue
+++ b/packages/frontend/src/pages/_error_.vue
@@ -9,7 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 	<div v-show="loaded" :class="$style.root">
 		<img :src="serverErrorImageUrl" class="_ghost" :class="$style.img"/>
 		<div class="_gaps">
-			<div><b><i class="ph-warning ph-bold ph-lg"></i> {{ i18n.ts.pageLoadError }}</b></div>
+			<div><b><i class="ti ti-alert-triangle"></i> {{ i18n.ts.pageLoadError }}</b></div>
 			<div v-if="meta && (version === meta.version)">{{ i18n.ts.pageLoadErrorDescription }}</div>
 			<div v-else-if="serverIsDead">{{ i18n.ts.serverIsDead }}</div>
 			<template v-else>
@@ -69,7 +69,7 @@ const headerTabs = computed(() => []);
 
 definePageMetadata(() => ({
 	title: i18n.ts.error,
-	icon: 'ph-warning ph-bold ph-lg',
+	icon: 'ti ti-alert-triangle',
 }));
 </script>
 
diff --git a/packages/frontend/src/pages/about-sharkey.vue b/packages/frontend/src/pages/about-sharkey.vue
index f5a8fcac42..2d1a3111c2 100644
--- a/packages/frontend/src/pages/about-sharkey.vue
+++ b/packages/frontend/src/pages/about-sharkey.vue
@@ -33,11 +33,11 @@ SPDX-License-Identifier: AGPL-3.0-only
 							{{ i18n.tsx._aboutMisskey.thisIsModifiedVersion({ name: instance.name }) }}
 						</MkInfo>
 						<FormLink v-if="instance.repositoryUrl" :to="instance.repositoryUrl" external>
-							<template #icon><i class="ph-code ph-bold ph-lg"></i></template>
+							<template #icon><i class="ti ti-code"></i></template>
 							{{ i18n.ts._aboutMisskey.source }}
 						</FormLink>
 						<FormLink v-if="instance.providesTarball" :to="`/tarball/sharkey-${version}.tar.gz`" external>
-							<template #icon><i class="ph-download ph-bold ph-lg"></i></template>
+							<template #icon><i class="ti ti-download"></i></template>
 							{{ i18n.ts._aboutMisskey.source }}
 							<template #suffix>Tarball</template>
 						</FormLink>
@@ -53,17 +53,17 @@ SPDX-License-Identifier: AGPL-3.0-only
 							{{ i18n.ts._aboutMisskey.source }} ({{ i18n.ts._aboutMisskey.original_sharkey }})
 							<template #suffix>GitLab</template>
 						</FormLink>
-						<FormLink to="https://ko-fi.com/transfem" external>
+						<FormLink to="https://opencollective.com/sharkey" external>
 							<template #icon><i class="ph-piggy-bank ph-bold ph-lg"></i></template>
 							{{ i18n.ts._aboutMisskey.donate_sharkey }}
-							<template #suffix>Ko-Fi</template>
+							<template #suffix>OpenCollective</template>
 						</FormLink>
 					</div>
 				</FormSection>
 				<FormSection>
 					<div class="_gaps_s">
 						<FormLink to="https://github.com/misskey-dev/misskey" external>
-							<template #icon><i class="ph-code ph-bold ph-lg"></i></template>
+							<template #icon><i class="ti ti-code"></i></template>
 							{{ i18n.ts._aboutMisskey.source }} ({{ i18n.ts._aboutMisskey.original }})
 							<template #suffix>GitHub</template>
 						</FormLink>
@@ -77,18 +77,10 @@ SPDX-License-Identifier: AGPL-3.0-only
 				<FormSection>
 					<template #label>{{ i18n.ts._aboutMisskey.projectMembers }}</template>
 					<div :class="$style.contributors" style="margin-bottom: 8px;">
-						<a href="https://activitypub.software/Amelia" target="_blank" :class="$style.contributor">
-							<img src="https://activitypub.software/uploads/-/system/user/avatar/1/avatar.png?width=128" :class="$style.contributorAvatar">
-							<span :class="$style.contributorUsername">@Amelia</span>
-						</a>
 						<a href="https://activitypub.software/dakkar" target="_blank" :class="$style.contributor">
 							<img src="https://secure.gravatar.com/avatar/c71b315eed7c63ff94c42b1b3e8dbad1?s=192&d=identicon" :class="$style.contributorAvatar">
 							<span :class="$style.contributorUsername">@dakkar</span>
 						</a>
-						<a href="https://activitypub.software/esm" target="_blank" :class="$style.contributor">
-							<img src="https://secure.gravatar.com/avatar/00fd054610e2a9dcf97a2aa661b168d0?s=192&d=identicon" :class="$style.contributorAvatar">
-							<span :class="$style.contributorUsername">@esm</span>
-						</a>
 						<a href="https://activitypub.software/supakaity" target="_blank" :class="$style.contributor">
 							<img src="https://activitypub.software/uploads/-/system/user/avatar/65/avatar.png?width=40" :class="$style.contributorAvatar">
 							<span :class="$style.contributorUsername">@supakaity</span>
@@ -109,6 +101,14 @@ SPDX-License-Identifier: AGPL-3.0-only
 							<img src="https://activitypub.software/uploads/-/system/user/avatar/132/avatar.png?width=128" :class="$style.contributorAvatar">
 							<span :class="$style.contributorUsername">@tess</span>
 						</a>
+						<a href="https://activitypub.software/marie" target="_blank" :class="$style.contributor">
+							<img src="https://activitypub.software/uploads/-/system/user/avatar/2/avatar.png?width=128" :class="$style.contributorAvatar">
+							<span :class="$style.contributorUsername">@Marie</span>
+						</a>
+						<a href="https://activitypub.software/luna" target="_blank" :class="$style.contributor">
+							<img src="https://secure.gravatar.com/avatar/4faf37df86a3d93a6c19ed6abf8588eade4efb837410dbbc53021b4fd12eaae7?s=80&d=identicon" :class="$style.contributorAvatar">
+							<span :class="$style.contributorUsername">@Luna</span>
+						</a>
 					</div>
 					<template #description><MkLink url="https://activitypub.software/TransFem-org/Sharkey/-/graphs/develop">{{ i18n.ts._aboutMisskey.allContributors }}</MkLink></template>
 				</FormSection>
@@ -119,9 +119,9 @@ SPDX-License-Identifier: AGPL-3.0-only
 							<img src="https://antani.cyou/proxy/avatar.webp?url=https%3A%2F%2Fantani.cyou%2Ffiles%2Fa2944119-024c-4abd-86e5-64bf0d30b26f&avatar=1" :class="$style.contributorAvatar">
 							<span :class="$style.contributorUsername">@lucent</span>
 						</a>
-						<a href="https://karilaa.app/@karilaa" target="_blank" :class="$style.contributor">
-							<img src="https://karilaa.app/proxy/avatar.webp?url=https%3A%2F%2Fkarilaa.app%2Ffiles%2Fc366e6f9-96d8-4d3b-b996-30e0a7cb3c5a&avatar=1" :class="$style.contributorAvatar">
-							<span :class="$style.contributorUsername">@karilaa</span>
+						<a href="https://plasmatrap.com/@privateger" target="_blank" :class="$style.contributor">
+							<img src="https://mediaproxy.plasmatrap.com/?url=https%3A%2F%2Fplasmatrap.com%2Ffiles%2F2cf35a8f-6520-4d4c-9611-bf22ee983293&avatar=1" :class="$style.contributorAvatar">
+							<span :class="$style.contributorUsername">@privateger</span>
 						</a>
 						<a href="https://thetransagenda.gay/@phoenix_fairy" target="_blank" :class="$style.contributor">
 							<img src="https://thetransagenda.gay/proxy/avatar.webp?url=https%3A%2F%2Fs3.us-east-005.backblazeb2.com%2Ftranssharkey%2Fnull%2Fd93ac6dc-2020-4b5a-bce7-84b41e97a0ac.png&avatar=1" :class="$style.contributorAvatar">
diff --git a/packages/frontend/src/pages/about.emojis.vue b/packages/frontend/src/pages/about.emojis.vue
index f37e9dbf96..d7d526f3ba 100644
--- a/packages/frontend/src/pages/about.emojis.vue
+++ b/packages/frontend/src/pages/about.emojis.vue
@@ -9,7 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 	<div class="query">
 		<MkInput v-model="q" class="" :placeholder="i18n.ts.search" autocapitalize="off">
-			<template #prefix><i class="ph-magnifying-glass ph-bold ph-lg"></i></template>
+			<template #prefix><i class="ti ti-search"></i></template>
 		</MkInput>
 
 		<!-- たくさんあると邪魔
diff --git a/packages/frontend/src/pages/about.federation.vue b/packages/frontend/src/pages/about.federation.vue
index b8f2fcc883..71353c7dfa 100644
--- a/packages/frontend/src/pages/about.federation.vue
+++ b/packages/frontend/src/pages/about.federation.vue
@@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 <div class="_gaps">
 	<div>
 		<MkInput v-model="host" :debounce="true" class="">
-			<template #prefix><i class="ph-magnifying-glass ph-bold ph-lg"></i></template>
+			<template #prefix><i class="ti ti-search"></i></template>
 			<template #label>{{ i18n.ts.host }}</template>
 		</MkInput>
 		<FormSplit style="margin-top: var(--margin);">
@@ -74,9 +74,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..c378c0a0b8
--- /dev/null
+++ b/packages/frontend/src/pages/about.overview.vue
@@ -0,0 +1,210 @@
+<!--
+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="sanitizeHtml(instance.description)"></div></template>
+	</MkKeyValue>
+
+	<FormSection>
+		<div class="_gaps_m">
+			<MkKeyValue :copy="version">
+				<template #key>Sharkey</template>
+				<template #value>{{ version }}</template>
+			</MkKeyValue>
+			<div v-html="i18n.tsx.poweredByMisskeyDescription({ name: instance.name ?? host })">
+			</div>
+			<FormLink to="/about-sharkey">
+				<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/sharkey-${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>
+			<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">
+				<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="sanitizeHtml(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 sanitizeHtml from '@/scripts/sanitize-html.js';
+import { host, version } from '@/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: var(--radius);
+	overflow: clip;
+	background-color: var(--panel);
+	background-size: cover;
+	background-position: center center;
+}
+
+.bannerIcon {
+	display: block;
+	margin: 16px auto 0 auto;
+	height: 64px;
+	border-radius: var(--radius-sm);;
+}
+
+.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: var(--radius-ellipse);
+	}
+}
+
+.ruleText {
+	padding-top: 6px;
+}
+</style>
diff --git a/packages/frontend/src/pages/about.vue b/packages/frontend/src/pages/about.vue
index 23960d39d9..f35cbe8d5a 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="sanitizeHtml(instance.description)"></div></template>
-				</MkKeyValue>
-
-				<FormSection>
-					<div class="_gaps_m">
-						<MkKeyValue :copy="version">
-							<template #key>Sharkey</template>
-							<template #value>{{ version }}</template>
-						</MkKeyValue>
-						<div v-html="i18n.tsx.poweredByMisskeyDescription({ name: instance.name ?? host })">
-						</div>
-						<FormLink to="/about-sharkey">
-							<template #icon><i class="ph-info ph-bold ph-lg"></i></template>
-							{{ i18n.ts.aboutMisskey }}
-						</FormLink>
-						<FormLink v-if="instance.repositoryUrl || instance.providesTarball" :to="instance.repositoryUrl || `/tarball/sharkey-${version}.tar.gz`" external>
-							<template #icon><i class="ph-code ph-bold ph-lg"></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="ph-newspaper-clipping ph-bold ph-lg"></i></template>
-							{{ i18n.ts.impressum }}
-						</FormLink>
-						<div class="_gaps_s">
-							<MkFolder v-if="instance.serverRules.length > 0">
-								<template #label>
-									<i class="ph-list-checks ph-bold ph-lg"></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="sanitizeHtml(item)"></div></li>
-								</ol>
-							</MkFolder>
-							<FormLink v-if="instance.tosUrl" :to="instance.tosUrl" external>
-								<template #icon><i class="ph-notebook ph-bold ph-lg"></i></template>
-								{{ i18n.ts.termsOfService }}
-							</FormLink>
-							<FormLink v-if="instance.privacyPolicyUrl" :to="instance.privacyPolicyUrl" external>
-								<template #icon><i class="ph-shield ph-bold ph-lg"></i></template>
-								{{ i18n.ts.privacyPolicy }}
-							</FormLink>
-							<FormLink v-if="instance.feedbackUrl" :to="instance.feedbackUrl" external>
-								<template #icon><i class="ph-envelope ph-bold ph-lg"></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,27 +24,16 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import sanitizeHtml from '@/scripts/sanitize-html.js';
-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;
@@ -158,7 +41,6 @@ const props = withDefaults(defineProps<{
 	initialTab: 'overview',
 });
 
-const stats = ref<Misskey.entities.StatsResponse | null>(null);
 const tab = ref(props.initialTab);
 
 watch(tab, () => {
@@ -167,11 +49,6 @@ watch(tab, () => {
 	}
 });
 
-const initStats = () => misskeyApi('stats', {
-}).then((res) => {
-	stats.value = res;
-});
-
 const headerActions = computed(() => []);
 
 const headerTabs = computed(() => [{
@@ -184,76 +61,15 @@ const headerTabs = computed(() => [{
 }, {
 	key: 'federation',
 	title: i18n.ts.federation,
-	icon: 'ph-globe-hemisphere-west ph-bold ph-lg',
+	icon: 'ti ti-whirl',
 }, {
 	key: 'charts',
 	title: i18n.ts.charts,
-	icon: 'ph-chart-line ph-bold ph-lg',
+	icon: 'ti ti-chart-line',
 }]);
 
 definePageMetadata(() => ({
 	title: i18n.ts.instanceInfo,
-	icon: 'ph-info ph-bold ph-lg',
+	icon: 'ti ti-info-circle',
 }));
 </script>
-
-<style lang="scss" module>
-.banner {
-	text-align: center;
-	border-radius: var(--radius);
-	overflow: clip;
-	background-size: cover;
-	background-position: center center;
-}
-
-.bannerIcon {
-	display: block;
-	margin: 16px auto 0 auto;
-	height: 64px;
-	border-radius: var(--radius-sm);
-}
-
-.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: var(--radius-ellipse);
-	}
-}
-
-.ruleText {
-	padding-top: 6px;
-}
-</style>
diff --git a/packages/frontend/src/pages/achievements.vue b/packages/frontend/src/pages/achievements.vue
index 4e496c3c6c..77ab473ea2 100644
--- a/packages/frontend/src/pages/achievements.vue
+++ b/packages/frontend/src/pages/achievements.vue
@@ -50,7 +50,7 @@ onDeactivated(() => {
 
 definePageMetadata(() => ({
 	title: i18n.ts.achievements,
-	icon: 'ph-trophy ph-bold ph-lg',
+	icon: 'ti ti-medal',
 }));
 </script>
 
diff --git a/packages/frontend/src/pages/admin-file.vue b/packages/frontend/src/pages/admin-file.vue
index b8f7e2c163..d8311186ab 100644
--- a/packages/frontend/src/pages/admin-file.vue
+++ b/packages/frontend/src/pages/admin-file.vue
@@ -41,7 +41,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 			</div>
 
 			<div>
-				<MkButton danger @click="del"><i class="ph-trash ph-bold ph-lg"></i> {{ i18n.ts.delete }}</MkButton>
+				<MkButton danger @click="del"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton>
 			</div>
 		</div>
 		<div v-else-if="tab === 'ip' && info" class="_gaps_m">
@@ -120,7 +120,7 @@ async function toggleIsSensitive(v) {
 
 const headerActions = computed(() => [{
 	text: i18n.ts.openInNewTab,
-	icon: 'ph-arrow-square-out ph-bold ph-lg',
+	icon: 'ti ti-external-link',
 	handler: () => {
 		window.open(file.value.url, '_blank', 'noopener');
 	},
@@ -129,20 +129,20 @@ const headerActions = computed(() => [{
 const headerTabs = computed(() => [{
 	key: 'overview',
 	title: i18n.ts.overview,
-	icon: 'ph-info ph-bold ph-lg',
+	icon: 'ti ti-info-circle',
 }, iAmModerator ? {
 	key: 'ip',
 	title: 'IP',
-	icon: 'ph-password ph-bold ph-lg',
+	icon: 'ti ti-password',
 } : null, {
 	key: 'raw',
 	title: 'Raw data',
-	icon: 'ph-code ph-bold ph-lg',
+	icon: 'ti ti-code',
 }]);
 
 definePageMetadata(() => ({
 	title: file.value ? `${i18n.ts.file}: ${file.value.name}` : i18n.ts.file,
-	icon: 'ph-file ph-bold ph-lg',
+	icon: 'ti ti-file',
 }));
 </script>
 
diff --git a/packages/frontend/src/pages/admin-user.vue b/packages/frontend/src/pages/admin-user.vue
index 07df36bd11..187ec66b42 100644
--- a/packages/frontend/src/pages/admin-user.vue
+++ b/packages/frontend/src/pages/admin-user.vue
@@ -24,7 +24,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>
 
@@ -64,7 +64,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 						<div style="display: flex; flex-direction: column; gap: 1em;">
 							<MkKeyValue oneline>
 								<template #key>{{ i18n.ts.instanceInfo }}</template>
-								<template #value><MkA :to="`/instance-info/${user.host}`" class="_link">{{ user.host }} <i class="ph-caret-right ph-bold ph-lg"></i></MkA></template>
+								<template #value><MkA :to="`/instance-info/${user.host}`" class="_link">{{ user.host }} <i class="ti ti-chevron-right"></i></MkA></template>
 							</MkKeyValue>
 							<MkKeyValue oneline>
 								<template #key>{{ i18n.ts.updatedAt }}</template>
@@ -72,7 +72,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 							</MkKeyValue>
 						</div>
 
-						<MkButton @click="updateRemoteUser"><i class="ph-arrows-counter-clockwise ph-bold ph-lg"></i> {{ i18n.ts.updateRemoteUser }}</MkButton>
+						<MkButton @click="updateRemoteUser"><i class="ti ti-refresh"></i> {{ i18n.ts.updateRemoteUser }}</MkButton>
 					</div>
 				</FormSection>
 
@@ -83,7 +83,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 						<MkSwitch v-model="markedAsNSFW" @update:modelValue="toggleNSFW">{{ i18n.ts.markAsNSFW }}</MkSwitch>
 
 						<div>
-							<MkButton v-if="user.host == null" inline style="margin-right: 8px;" @click="resetPassword"><i class="ph-key ph-bold ph-lg"></i> {{ i18n.ts.resetPassword }}</MkButton>
+							<MkButton v-if="user.host == null" inline style="margin-right: 8px;" @click="resetPassword"><i class="ti ti-key"></i> {{ i18n.ts.resetPassword }}</MkButton>
 						</div>
 
 						<MkFolder>
@@ -97,7 +97,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 						</MkFolder>
 
 						<MkFolder>
-							<template #icon><i class="ph-password ph-bold ph-lg"></i></template>
+							<template #icon><i class="ti ti-password"></i></template>
 							<template #label>IP</template>
 							<MkInfo v-if="!iAmAdmin" warn>{{ i18n.ts.requireAdminForView }}</MkInfo>
 							<MkInfo v-else>The date is the IP address was first acknowledged.</MkInfo>
@@ -110,8 +110,8 @@ SPDX-License-Identifier: AGPL-3.0-only
 						</MkFolder>
 
 						<div>
-							<MkButton v-if="iAmModerator" inline danger style="margin-right: 8px;" @click="unsetUserAvatar"><i class="ph-user-circle ph-bold ph-lg"></i> {{ i18n.ts.unsetUserAvatar }}</MkButton>
-							<MkButton v-if="iAmModerator" inline danger style="margin-right: 8px;" @click="unsetUserBanner"><i class="ph-image ph-bold ph-lg"></i> {{ i18n.ts.unsetUserBanner }}</MkButton>
+							<MkButton v-if="iAmModerator" inline danger style="margin-right: 8px;" @click="unsetUserAvatar"><i class="ti ti-user-circle"></i> {{ i18n.ts.unsetUserAvatar }}</MkButton>
+							<MkButton v-if="iAmModerator" inline danger style="margin-right: 8px;" @click="unsetUserBanner"><i class="ti ti-photo"></i> {{ i18n.ts.unsetUserBanner }}</MkButton>
 							<MkButton v-if="iAmModerator" inline danger @click="deleteAllFiles"><i class="ph-cloud ph-bold ph-lg"></i> {{ i18n.ts.deleteAllFiles }}</MkButton>
 						</div>
 						<MkButton v-if="$i.isAdmin" inline danger @click="deleteAccount">{{ i18n.ts.deleteAccount }}</MkButton>
@@ -120,14 +120,14 @@ SPDX-License-Identifier: AGPL-3.0-only
 			</div>
 
 			<div v-else-if="tab === 'roles'" class="_gaps">
-				<MkButton v-if="user.host == null" primary rounded @click="assignRole"><i class="ph-plus ph-bold ph-lg"></i> {{ i18n.ts.assign }}</MkButton>
+				<MkButton v-if="user.host == null" primary rounded @click="assignRole"><i class="ti ti-plus"></i> {{ i18n.ts.assign }}</MkButton>
 
 				<div v-for="role in info.roles" :key="role.id">
 					<div :class="$style.roleItemMain">
 						<MkRolePreview :class="$style.role" :role="role" :forModeration="true"/>
-						<button class="_button" @click="toggleRoleItem(role)"><i class="ph-caret-down ph-bold ph-lg"></i></button>
-						<button v-if="role.target === 'manual'" class="_button" :class="$style.roleUnassign" @click="unassignRole(role, $event)"><i class="ph-x ph-bold ph-lg"></i></button>
-						<button v-else class="_button" :class="$style.roleUnassign" disabled><i class="ph-prohibit ph-bold ph-lg"></i></button>
+						<button class="_button" @click="toggleRoleItem(role)"><i class="ti ti-chevron-down"></i></button>
+						<button v-if="role.target === 'manual'" class="_button" :class="$style.roleUnassign" @click="unassignRole(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 v-if="expandedRoles.includes(role.id)" :class="$style.roleItemSub">
 						<div>Assigned: <MkTime :time="info.roleAssigns.find(a => a.roleId === role.id).createdAt" mode="detail"/></div>
@@ -138,17 +138,17 @@ SPDX-License-Identifier: AGPL-3.0-only
 			</div>
 
 			<div v-else-if="tab === 'announcements'" class="_gaps">
-				<MkButton primary rounded @click="createAnnouncement"><i class="ph-plus ph-bold ph-lg"></i> {{ i18n.ts.new }}</MkButton>
+				<MkButton primary rounded @click="createAnnouncement"><i class="ti ti-plus"></i> {{ i18n.ts.new }}</MkButton>
 
 				<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="ph-info ph-bold ph-lg"></i>
-									<i v-else-if="announcement.icon === 'warning'" class="ph-warning ph-bold ph-lg" style="color: var(--warn);"></i>
-									<i v-else-if="announcement.icon === 'error'" class="ph-x-circle ph-bold ph-lg" style="color: var(--error);"></i>
-									<i v-else-if="announcement.icon === 'success'" class="ph-check ph-bold ph-lg" style="color: var(--success);"></i>
+									<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>
 								</span>
 								<span>{{ announcement.title }}</span>
 								<span v-if="announcement.reads > 0" style="margin-left: auto; opacity: 0.7;">{{ i18n.ts.messageRead }}</span>
@@ -464,7 +464,7 @@ async function assignRole() {
 async function unassignRole(role, ev) {
 	os.popupMenu([{
 		text: i18n.ts.unassign,
-		icon: 'ph-x ph-bold ph-lg',
+		icon: 'ti ti-x',
 		danger: true,
 		action: async () => {
 			await os.apiWithDialog('admin/roles/unassign', { roleId: role.id, userId: user.value.id });
@@ -482,16 +482,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, () => {
@@ -513,32 +517,32 @@ const headerActions = computed(() => []);
 const headerTabs = computed(() => [{
 	key: 'overview',
 	title: i18n.ts.overview,
-	icon: 'ph-info ph-bold ph-lg',
+	icon: 'ti ti-info-circle',
 }, {
 	key: 'roles',
 	title: i18n.ts.roles,
-	icon: 'ph-seal-check ph-bold ph-lg',
+	icon: 'ti ti-badges',
 }, {
 	key: 'announcements',
 	title: i18n.ts.announcements,
-	icon: 'ph-megaphone ph-bold ph-lg',
+	icon: 'ti ti-speakerphone',
 }, {
 	key: 'drive',
 	title: i18n.ts.drive,
-	icon: 'ph-cloud ph-bold ph-lg',
+	icon: 'ti ti-cloud',
 }, {
 	key: 'chart',
 	title: i18n.ts.charts,
-	icon: 'ph-chart-line ph-bold ph-lg',
+	icon: 'ti ti-chart-line',
 }, {
 	key: 'raw',
 	title: 'Raw',
-	icon: 'ph-code ph-bold ph-lg',
+	icon: 'ti ti-code',
 }]);
 
 definePageMetadata(() => ({
 	title: user.value ? acct(user.value) : i18n.ts.userInfo,
-	icon: 'ph-warning-circle ph-bold ph-lg',
+	icon: 'ti ti-user-exclamation',
 }));
 </script>
 
diff --git a/packages/frontend/src/pages/admin/RolesEditorFormula.vue b/packages/frontend/src/pages/admin/RolesEditorFormula.vue
index de1f6646bb..f001a4ac20 100644
--- a/packages/frontend/src/pages/admin/RolesEditorFormula.vue
+++ b/packages/frontend/src/pages/admin/RolesEditorFormula.vue
@@ -28,10 +28,10 @@ SPDX-License-Identifier: AGPL-3.0-only
 			<option value="not">{{ i18n.ts._role._condition.not }}</option>
 		</MkSelect>
 		<button v-if="draggable" class="drag-handle _button" :class="$style.dragHandle">
-			<i class="ph-list ph-bold ph-lg-2"></i>
+			<i class="ti ti-menu-2"></i>
 		</button>
 		<button v-if="draggable" class="_button" :class="$style.remove" @click="removeSelf">
-			<i class="ph-x ph-bold ph-lg"></i>
+			<i class="ti ti-x"></i>
 		</button>
 	</div>
 
@@ -44,7 +44,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 				</div>
 			</template>
 		</Sortable>
-		<MkButton rounded style="margin: 0 auto;" @click="addValue"><i class="ph-plus ph-bold ph-lg"></i> {{ i18n.ts.add }}</MkButton>
+		<MkButton rounded style="margin: 0 auto;" @click="addValue"><i class="ti ti-plus"></i> {{ i18n.ts.add }}</MkButton>
 	</div>
 
 	<div v-else-if="type === 'not'" :class="$style.item">
diff --git a/packages/frontend/src/pages/admin/_header_.vue b/packages/frontend/src/pages/admin/_header_.vue
index 5c9c32c964..61dc4f8549 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>
@@ -56,6 +56,7 @@ const props = defineProps<{
 		text: string;
 		icon: string;
 		asFullButton?: boolean;
+		disabled?: boolean;
 		handler: (ev: MouseEvent) => void;
 	}[];
 	thin?: boolean;
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..827e22e8ae
--- /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(--divider);
+	background: var(--acrylicBg);
+	-webkit-backdrop-filter: var(--blur, blur(15px));
+	backdrop-filter: var(--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..0b86808faf
--- /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(--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(--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 6e1f5f9cec..c8a9ca7112 100644
--- a/packages/frontend/src/pages/admin/abuses.vue
+++ b/packages/frontend/src/pages/admin/abuses.vue
@@ -7,30 +7,33 @@ 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>
+
+			<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 +44,9 @@ SPDX-License-Identifier: AGPL-3.0-only
 			</div>
 			-->
 
-					<MkPagination v-slot="{items}" ref="reports" :pagination="pagination" :displayLimit="50" style="margin-top: var(--margin);">
-						<XAbuseReport v-for="report in items" :key="report.id" :report="report" @resolved="resolved"/>
-					</MkPagination>
-				</div>
-			</div>
+			<MkPagination v-slot="{items}" ref="reports" :pagination="pagination" :displayLimit="50" style="margin-top: var(--margin);">
+				<XAbuseReport v-for="report in items" :key="report.id" :report="report" @resolved="resolved"/>
+			</MkPagination>
 		</div>
 	</MkSpacer>
 </MkStickyContainer>
@@ -60,6 +61,7 @@ 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';
 
 const reports = shallowRef<InstanceType<typeof MkPagination>>();
 
@@ -80,7 +82,7 @@ const pagination = {
 };
 
 function resolved(reportId) {
-	reports.value.removeItem(reportId);
+	reports.value?.removeItem(reportId);
 }
 
 const headerActions = computed(() => []);
@@ -89,6 +91,29 @@ const headerTabs = computed(() => []);
 
 definePageMetadata(() => ({
 	title: i18n.ts.abuseReports,
-	icon: 'ph-warning-circle ph-bold ph-lg',
+	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 6ec5abd2f2..bd442ccc69 100644
--- a/packages/frontend/src/pages/admin/ads.vue
+++ b/packages/frontend/src/pages/admin/ads.vue
@@ -68,16 +68,16 @@ SPDX-License-Identifier: AGPL-3.0-only
 				<div class="buttons">
 					<MkButton class="button" inline primary style="margin-right: 12px;" @click="save(ad)">
 						<i
-							class="ph-floppy-disk ph-bold ph-lg"
+							class="ti ti-device-floppy"
 						></i> {{ i18n.ts.save }}
 					</MkButton>
 					<MkButton class="button" inline danger @click="remove(ad)">
-						<i class="ph-trash ph-bold ph-lg"></i> {{ i18n.ts.remove }}
+						<i class="ti ti-trash"></i> {{ i18n.ts.remove }}
 					</MkButton>
 				</div>
 			</div>
 			<MkButton class="button" @click="more()">
-				<i class="ph-arrow-clockwise ph-bold ph-lg"></i>{{ i18n.ts.more }}
+				<i class="ti ti-reload"></i>{{ i18n.ts.more }}
 			</MkButton>
 		</div>
 	</MkSpacer>
@@ -248,7 +248,7 @@ refresh();
 
 const headerActions = computed(() => [{
 	asFullButton: true,
-	icon: 'ph-plus ph-bold ph-lg',
+	icon: 'ti ti-plus',
 	text: i18n.ts.add,
 	handler: add,
 }]);
@@ -257,7 +257,7 @@ const headerTabs = computed(() => []);
 
 definePageMetadata(() => ({
 	title: i18n.ts.ads,
-	icon: 'ph-flag ph-bold ph-lg',
+	icon: 'ti ti-ad',
 }));
 </script>
 
diff --git a/packages/frontend/src/pages/admin/announcements.vue b/packages/frontend/src/pages/admin/announcements.vue
index a8832b99fd..b9e09c8d03 100644
--- a/packages/frontend/src/pages/admin/announcements.vue
+++ b/packages/frontend/src/pages/admin/announcements.vue
@@ -11,70 +11,83 @@ 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="ph-info ph-bold ph-lg"></i>
-					<i v-else-if="announcement.icon === 'warning'" class="ph-warning ph-bold ph-lg" style="color: var(--warn);"></i>
-					<i v-else-if="announcement.icon === 'error'" class="ph-x-circle ph-bold ph-lg" style="color: var(--error);"></i>
-					<i v-else-if="announcement.icon === 'success'" class="ph-check ph-bold ph-lg" 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="ph-info ph-bold ph-lg"></i></option>
-						<option value="warning"><i class="ph-warning ph-bold ph-lg" style="color: var(--warn);"></i></option>
-						<option value="error"><i class="ph-x-circle ph-bold ph-lg" style="color: var(--error);"></i></option>
-						<option value="success"><i class="ph-check ph-bold ph-lg" 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="ph-floppy-disk ph-bold ph-lg"></i> {{ i18n.ts.save }}</MkButton>
-						<MkButton v-if="announcement.id != null" class="button" inline @click="archive(announcement)"><i class="ph-check ph-bold ph-lg"></i> {{ i18n.ts._announcement.end }} ({{ i18n.ts.archive }})</MkButton>
-						<MkButton v-if="announcement.id != null" class="button" inline danger @click="del(announcement)"><i class="ph-trash ph-bold ph-lg"></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(--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>
+
+					<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 && announcement.isActive" 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 && !announcement.isActive" class="button" inline @click="unarchive(announcement)"><i class="ti ti-restore"></i> {{ i18n.ts.unarchive }}</MkButton>
+							<MkButton v-if="announcement.id != null" class="button" inline danger @click="del(announcement)"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton>
+						</div>
 					</div>
-				</div>
-			</MkFolder>
-			<MkButton class="button" @click="more()">
-				<i class="ph-arrow-clockwise ph-bold ph-lg"></i>{{ i18n.ts.more }}
-			</MkButton>
+				</MkFolder>
+				<MkLoading v-if="loadingMore"/>
+				<MkButton class="button" @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 +98,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 +149,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,30 +167,38 @@ 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: 'ph-plus ph-bold ph-lg',
+	icon: 'ti ti-plus',
 	text: i18n.ts.add,
 	handler: add,
+	disabled: announcementsStatus.value === 'archived',
 }]);
 
 const headerTabs = computed(() => []);
 
 definePageMetadata(() => ({
 	title: i18n.ts.announcements,
-	icon: 'ph-megaphone ph-bold ph-lg',
+	icon: 'ti ti-speakerphone',
 }));
 </script>
diff --git a/packages/frontend/src/pages/admin/bot-protection.vue b/packages/frontend/src/pages/admin/bot-protection.vue
index 052d2f0bc2..73c5e1919f 100644
--- a/packages/frontend/src/pages/admin/bot-protection.vue
+++ b/packages/frontend/src/pages/admin/bot-protection.vue
@@ -17,11 +17,11 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 			<template v-if="provider === 'hcaptcha'">
 				<MkInput v-model="hcaptchaSiteKey">
-					<template #prefix><i class="ph-key ph-bold ph-lg"></i></template>
+					<template #prefix><i class="ti ti-key"></i></template>
 					<template #label>{{ i18n.ts.hcaptchaSiteKey }}</template>
 				</MkInput>
 				<MkInput v-model="hcaptchaSecretKey">
-					<template #prefix><i class="ph-key ph-bold ph-lg"></i></template>
+					<template #prefix><i class="ti ti-key"></i></template>
 					<template #label>{{ i18n.ts.hcaptchaSecretKey }}</template>
 				</MkInput>
 				<FormSlot>
@@ -31,15 +31,15 @@ SPDX-License-Identifier: AGPL-3.0-only
 			</template>
 			<template v-else-if="provider === 'mcaptcha'">
 				<MkInput v-model="mcaptchaSiteKey">
-					<template #prefix><i class="ph-key ph-bold ph-lg"></i></template>
+					<template #prefix><i class="ti ti-key"></i></template>
 					<template #label>{{ i18n.ts.mcaptchaSiteKey }}</template>
 				</MkInput>
 				<MkInput v-model="mcaptchaSecretKey">
-					<template #prefix><i class="ph-key ph-bold ph-lg"></i></template>
+					<template #prefix><i class="ti ti-key"></i></template>
 					<template #label>{{ i18n.ts.mcaptchaSecretKey }}</template>
 				</MkInput>
 				<MkInput v-model="mcaptchaInstanceUrl">
-					<template #prefix><i class="ph-globe-simple ph-bold ph-lg"></i></template>
+					<template #prefix><i class="ti ti-link"></i></template>
 					<template #label>{{ i18n.ts.mcaptchaInstanceUrl }}</template>
 				</MkInput>
 				<FormSlot v-if="mcaptchaSiteKey && mcaptchaInstanceUrl">
@@ -49,11 +49,11 @@ SPDX-License-Identifier: AGPL-3.0-only
 			</template>
 			<template v-else-if="provider === 'recaptcha'">
 				<MkInput v-model="recaptchaSiteKey">
-					<template #prefix><i class="ph-key ph-bold ph-lg"></i></template>
+					<template #prefix><i class="ti ti-key"></i></template>
 					<template #label>{{ i18n.ts.recaptchaSiteKey }}</template>
 				</MkInput>
 				<MkInput v-model="recaptchaSecretKey">
-					<template #prefix><i class="ph-key ph-bold ph-lg"></i></template>
+					<template #prefix><i class="ti ti-key"></i></template>
 					<template #label>{{ i18n.ts.recaptchaSecretKey }}</template>
 				</MkInput>
 				<FormSlot v-if="recaptchaSiteKey">
@@ -63,11 +63,11 @@ SPDX-License-Identifier: AGPL-3.0-only
 			</template>
 			<template v-else-if="provider === 'turnstile'">
 				<MkInput v-model="turnstileSiteKey">
-					<template #prefix><i class="ph-key ph-bold ph-lg"></i></template>
+					<template #prefix><i class="ti ti-key"></i></template>
 					<template #label>{{ i18n.ts.turnstileSiteKey }}</template>
 				</MkInput>
 				<MkInput v-model="turnstileSecretKey">
-					<template #prefix><i class="ph-key ph-bold ph-lg"></i></template>
+					<template #prefix><i class="ti ti-key"></i></template>
 					<template #label>{{ i18n.ts.turnstileSecretKey }}</template>
 				</MkInput>
 				<FormSlot>
@@ -76,7 +76,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 				</FormSlot>
 			</template>
 
-			<MkButton primary @click="save"><i class="ph-floppy-disk ph-bold ph-lg"></i> {{ i18n.ts.save }}</MkButton>
+			<MkButton primary @click="save"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton>
 		</div>
 	</FormSuspense>
 </div>
diff --git a/packages/frontend/src/pages/admin/branding.vue b/packages/frontend/src/pages/admin/branding.vue
index 9310f52bfb..2e14aef0b9 100644
--- a/packages/frontend/src/pages/admin/branding.vue
+++ b/packages/frontend/src/pages/admin/branding.vue
@@ -11,12 +11,12 @@ SPDX-License-Identifier: AGPL-3.0-only
 			<FormSuspense :p="init">
 				<div class="_gaps_m">
 					<MkInput v-model="iconUrl" type="url">
-						<template #prefix><i class="ph-link ph-bold ph-lg"></i></template>
+						<template #prefix><i class="ti ti-link"></i></template>
 						<template #label>{{ i18n.ts._serverSettings.iconUrl }}</template>
 					</MkInput>
 
 					<MkInput v-model="app192IconUrl" type="url">
-						<template #prefix><i class="ph-link ph-bold ph-lg"></i></template>
+						<template #prefix><i class="ti ti-link"></i></template>
 						<template #label>{{ i18n.ts._serverSettings.iconUrl }} (App/192px)</template>
 						<template #caption>
 							<div>{{ i18n.tsx._serverSettings.appIconDescription({ host: instance.name ?? host }) }}</div>
@@ -27,7 +27,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 					</MkInput>
 
 					<MkInput v-model="app512IconUrl" type="url">
-						<template #prefix><i class="ph-link ph-bold ph-lg"></i></template>
+						<template #prefix><i class="ti ti-link"></i></template>
 						<template #label>{{ i18n.ts._serverSettings.iconUrl }} (App/512px)</template>
 						<template #caption>
 							<div>{{ i18n.tsx._serverSettings.appIconDescription({ host: instance.name ?? host }) }}</div>
@@ -38,12 +38,12 @@ SPDX-License-Identifier: AGPL-3.0-only
 					</MkInput>
 
 					<MkInput v-model="bannerUrl" type="url">
-						<template #prefix><i class="ph-link ph-bold ph-lg"></i></template>
+						<template #prefix><i class="ti ti-link"></i></template>
 						<template #label>{{ i18n.ts.bannerUrl }}</template>
 					</MkInput>
 
 					<MkInput v-model="backgroundImageUrl" type="url">
-						<template #prefix><i class="ph-link ph-bold ph-lg"></i></template>
+						<template #prefix><i class="ti ti-link"></i></template>
 						<template #label>{{ i18n.ts.backgroundImageUrl }}</template>
 					</MkInput>
 
@@ -55,17 +55,17 @@ SPDX-License-Identifier: AGPL-3.0-only
 					</FromSlot>
 
 					<MkInput v-model="notFoundImageUrl" type="url">
-						<template #prefix><i class="ph-link ph-bold ph-lg"></i></template>
+						<template #prefix><i class="ti ti-link"></i></template>
 						<template #label>{{ i18n.ts.notFoundDescription }}</template>
 					</MkInput>
 
 					<MkInput v-model="infoImageUrl" type="url">
-						<template #prefix><i class="ph-link ph-bold ph-lg"></i></template>
+						<template #prefix><i class="ti ti-link"></i></template>
 						<template #label>{{ i18n.ts.nothing }}</template>
 					</MkInput>
 
 					<MkInput v-model="serverErrorImageUrl" type="url">
-						<template #prefix><i class="ph-link ph-bold ph-lg"></i></template>
+						<template #prefix><i class="ti ti-link"></i></template>
 						<template #label>{{ i18n.ts.somethingHappened }}</template>
 					</MkInput>
 
@@ -84,12 +84,12 @@ SPDX-License-Identifier: AGPL-3.0-only
 					</MkTextarea>
 
 					<MkInput v-model="repositoryUrl" type="url">
-						<template #prefix><i class="ph-link ph-bold ph-lg"></i></template>
+						<template #prefix><i class="ti ti-link"></i></template>
 						<template #label>{{ i18n.ts.repositoryUrl }}</template>
 					</MkInput>
 
 					<MkInput v-model="feedbackUrl" type="url">
-						<template #prefix><i class="ph-link ph-bold ph-lg"></i></template>
+						<template #prefix><i class="ti ti-link"></i></template>
 						<template #label>{{ i18n.ts.feedbackUrl }}</template>
 					</MkInput>
 
@@ -102,7 +102,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 		<template #footer>
 			<div :class="$style.footer">
 				<MkSpacer :contentMax="700" :marginMin="16" :marginMax="16">
-					<MkButton primary rounded @click="save"><i class="ph-check ph-bold ph-lg"></i> {{ i18n.ts.save }}</MkButton>
+					<MkButton primary rounded @click="save"><i class="ti ti-check"></i> {{ i18n.ts.save }}</MkButton>
 				</MkSpacer>
 			</div>
 		</template>
@@ -200,7 +200,7 @@ const headerTabs = computed(() => []);
 
 definePageMetadata(() => ({
 	title: i18n.ts.branding,
-	icon: 'ph-paint-roller ph-bold ph-lg',
+	icon: 'ti ti-paint',
 }));
 </script>
 
diff --git a/packages/frontend/src/pages/admin/database.vue b/packages/frontend/src/pages/admin/database.vue
index a64e07b4c7..e092efd92c 100644
--- a/packages/frontend/src/pages/admin/database.vue
+++ b/packages/frontend/src/pages/admin/database.vue
@@ -35,6 +35,6 @@ const headerTabs = computed(() => []);
 
 definePageMetadata(() => ({
 	title: i18n.ts.database,
-	icon: 'ph-database ph-bold ph-lg',
+	icon: 'ti ti-database',
 }));
 </script>
diff --git a/packages/frontend/src/pages/admin/email-settings.vue b/packages/frontend/src/pages/admin/email-settings.vue
index 1cbfdab094..4a858887f3 100644
--- a/packages/frontend/src/pages/admin/email-settings.vue
+++ b/packages/frontend/src/pages/admin/email-settings.vue
@@ -54,8 +54,8 @@ SPDX-License-Identifier: AGPL-3.0-only
 		<div :class="$style.footer">
 			<MkSpacer :contentMax="700" :marginMin="16" :marginMax="16">
 				<div class="_buttons">
-					<MkButton primary rounded @click="save"><i class="ph-check ph-bold ph-lg"></i> {{ i18n.ts.save }}</MkButton>
-					<MkButton rounded @click="testEmail"><i class="ph-paper-plane-tilt ph-bold ph-lg"></i> {{ i18n.ts.testEmail }}</MkButton>
+					<MkButton primary rounded @click="save"><i class="ti ti-check"></i> {{ i18n.ts.save }}</MkButton>
+					<MkButton rounded @click="testEmail"><i class="ti ti-send"></i> {{ i18n.ts.testEmail }}</MkButton>
 				</div>
 			</MkSpacer>
 		</div>
@@ -132,7 +132,7 @@ const headerTabs = computed(() => []);
 
 definePageMetadata(() => ({
 	title: i18n.ts.emailServer,
-	icon: 'ph-envelope ph-bold ph-lg',
+	icon: 'ti ti-mail',
 }));
 </script>
 
diff --git a/packages/frontend/src/pages/admin/external-services.vue b/packages/frontend/src/pages/admin/external-services.vue
index 728cdc60b0..e4308e6030 100644
--- a/packages/frontend/src/pages/admin/external-services.vue
+++ b/packages/frontend/src/pages/admin/external-services.vue
@@ -13,7 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 				<div class="_gaps_m">
 					<MkInput v-model="deeplAuthKey">
-						<template #prefix><i class="ph-key ph-bold ph-lg"></i></template>
+						<template #prefix><i class="ti ti-key"></i></template>
 						<template #label>DeepL Auth Key</template>
 					</MkInput>
 					<MkSwitch v-model="deeplIsPro">
@@ -34,7 +34,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 	<template #footer>
 		<div :class="$style.footer">
 			<MkSpacer :contentMax="700" :marginMin="16" :marginMax="16">
-				<MkButton primary rounded @click="save"><i class="ph-check ph-bold ph-lg"></i> {{ i18n.ts.save }}</MkButton>
+				<MkButton primary rounded @click="save"><i class="ti ti-check"></i> {{ i18n.ts.save }}</MkButton>
 			</MkSpacer>
 		</div>
 	</template>
diff --git a/packages/frontend/src/pages/admin/federation.vue b/packages/frontend/src/pages/admin/federation.vue
index c1f958cece..1902c97724 100644
--- a/packages/frontend/src/pages/admin/federation.vue
+++ b/packages/frontend/src/pages/admin/federation.vue
@@ -11,7 +11,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 			<div class="_gaps">
 				<div>
 					<MkInput v-model="host" :debounce="true" class="">
-						<template #prefix><i class="ph-magnifying-glass ph-bold ph-lg"></i></template>
+						<template #prefix><i class="ti ti-search"></i></template>
 						<template #label>{{ i18n.ts.host }}</template>
 					</MkInput>
 					<FormSplit style="margin-top: var(--margin);">
@@ -81,9 +81,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 } :
@@ -117,7 +117,7 @@ const headerTabs = computed(() => []);
 
 definePageMetadata(() => ({
 	title: i18n.ts.federation,
-	icon: 'ph-globe-hemisphere-west ph-bold ph-lg',
+	icon: 'ti ti-whirl',
 }));
 </script>
 
diff --git a/packages/frontend/src/pages/admin/files.vue b/packages/frontend/src/pages/admin/files.vue
index 29b5bc5f88..5132b85c64 100644
--- a/packages/frontend/src/pages/admin/files.vue
+++ b/packages/frontend/src/pages/admin/files.vue
@@ -75,11 +75,11 @@ function clear() {
 
 const headerActions = computed(() => [{
 	text: i18n.ts.lookup,
-	icon: 'ph-magnifying-glass ph-bold ph-lg',
+	icon: 'ti ti-search',
 	handler: lookupFile,
 }, {
 	text: i18n.ts.clearCachedFiles,
-	icon: 'ph-trash ph-bold ph-lg',
+	icon: 'ti ti-trash',
 	handler: clear,
 }]);
 
@@ -87,6 +87,6 @@ const headerTabs = computed(() => []);
 
 definePageMetadata(() => ({
 	title: i18n.ts.files,
-	icon: 'ph-cloud ph-bold ph-lg',
+	icon: 'ti ti-cloud',
 }));
 </script>
diff --git a/packages/frontend/src/pages/admin/index.vue b/packages/frontend/src/pages/admin/index.vue
index fae86246ee..f547bedacb 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>
@@ -50,7 +50,7 @@ const router = useRouter();
 
 const indexInfo = {
 	title: i18n.ts.controlPanel,
-	icon: 'ph-gear ph-bold ph-lg',
+	icon: 'ti ti-settings',
 	hideHeader: true,
 };
 
@@ -62,10 +62,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.enableMcaptcha && !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 pendingUserApprovals = ref(false);
 const currentPage = computed(() => router.currentRef.value.child);
@@ -95,29 +95,29 @@ const menuDef = computed(() => [{
 	title: i18n.ts.quickAction,
 	items: [{
 		type: 'button',
-		icon: 'ph-magnifying-glass ph-bold ph-lg',
+		icon: 'ti ti-search',
 		text: i18n.ts.lookup,
 		action: adminLookup,
 	}, ...(instance.disableRegistration ? [{
 		type: 'button',
-		icon: 'ph-user-plus ph-bold ph-lg',
+		icon: 'ti ti-user-plus',
 		text: i18n.ts.createInviteCode,
 		action: invite,
 	}] : [])],
 }, {
 	title: i18n.ts.administration,
 	items: [{
-		icon: 'ph-gauge ph-bold ph-lg',
+		icon: 'ti ti-dashboard',
 		text: i18n.ts.dashboard,
 		to: '/admin/overview',
 		active: currentPage.value?.route.name === 'overview',
 	}, {
-		icon: 'ph-users ph-bold ph-lg',
+		icon: 'ti ti-users',
 		text: i18n.ts.users,
 		to: '/admin/users',
 		active: currentPage.value?.route.name === 'users',
 	}, {
-		icon: 'ph-user-plus ph-bold ph-lg',
+		icon: 'ti ti-user-plus',
 		text: i18n.ts.invite,
 		to: '/admin/invites',
 		active: currentPage.value?.route.name === 'invites',
@@ -127,7 +127,7 @@ const menuDef = computed(() => [{
 		to: '/admin/approvals',
 		active: currentPage.value?.route.name === 'approvals',
 	}, {
-		icon: 'ph-seal-check ph-bold ph-lg',
+		icon: 'ti ti-badges',
 		text: i18n.ts.roles,
 		to: '/admin/roles',
 		active: currentPage.value?.route.name === 'roles',
@@ -137,42 +137,42 @@ const menuDef = computed(() => [{
 		to: '/admin/emojis',
 		active: currentPage.value?.route.name === 'emojis',
 	}, {
-		icon: 'ph-sparkle ph-bold ph-lg',
+		icon: 'ti ti-sparkles',
 		text: i18n.ts.avatarDecorations,
 		to: '/admin/avatar-decorations',
 		active: currentPage.value?.route.name === 'avatarDecorations',
 	}, {
-		icon: 'ph-globe-hemisphere-west ph-bold ph-lg',
+		icon: 'ti ti-whirl',
 		text: i18n.ts.federation,
 		to: '/admin/federation',
 		active: currentPage.value?.route.name === 'federation',
 	}, {
-		icon: 'ph-clock ph-bold ph-lg-play',
+		icon: 'ti ti-clock-play',
 		text: i18n.ts.jobQueue,
 		to: '/admin/queue',
 		active: currentPage.value?.route.name === 'queue',
 	}, {
-		icon: 'ph-cloud ph-bold ph-lg',
+		icon: 'ti ti-cloud',
 		text: i18n.ts.files,
 		to: '/admin/files',
 		active: currentPage.value?.route.name === 'files',
 	}, {
-		icon: 'ph-megaphone ph-bold ph-lg',
+		icon: 'ti ti-speakerphone',
 		text: i18n.ts.announcements,
 		to: '/admin/announcements',
 		active: currentPage.value?.route.name === 'announcements',
 	}, {
-		icon: 'ph-flag ph-bold ph-lg',
+		icon: 'ti ti-ad',
 		text: i18n.ts.ads,
 		to: '/admin/ads',
 		active: currentPage.value?.route.name === 'ads',
 	}, {
-		icon: 'ph-warning-circle ph-bold ph-lg',
+		icon: 'ti ti-exclamation-circle',
 		text: i18n.ts.abuseReports,
 		to: '/admin/abuses',
 		active: currentPage.value?.route.name === 'abuses',
 	}, {
-		icon: 'ph-list ph-bold ph-lg-search',
+		icon: 'ti ti-list-search',
 		text: i18n.ts.moderationLogs,
 		to: '/admin/modlog',
 		active: currentPage.value?.route.name === 'modlog',
@@ -180,47 +180,47 @@ const menuDef = computed(() => [{
 }, {
 	title: i18n.ts.settings,
 	items: [{
-		icon: 'ph-gear ph-bold ph-lg',
+		icon: 'ti ti-settings',
 		text: i18n.ts.general,
 		to: '/admin/settings',
 		active: currentPage.value?.route.name === 'settings',
 	}, {
-		icon: 'ph-paint-roller ph-bold ph-lg',
+		icon: 'ti ti-paint',
 		text: i18n.ts.branding,
 		to: '/admin/branding',
 		active: currentPage.value?.route.name === 'branding',
 	}, {
-		icon: 'ph-shield ph-bold ph-lg',
+		icon: 'ti ti-shield',
 		text: i18n.ts.moderation,
 		to: '/admin/moderation',
 		active: currentPage.value?.route.name === 'moderation',
 	}, {
-		icon: 'ph-envelope ph-bold ph-lg',
+		icon: 'ti ti-mail',
 		text: i18n.ts.emailServer,
 		to: '/admin/email-settings',
 		active: currentPage.value?.route.name === 'email-settings',
 	}, {
-		icon: 'ph-cloud ph-bold ph-lg',
+		icon: 'ti ti-cloud',
 		text: i18n.ts.objectStorage,
 		to: '/admin/object-storage',
 		active: currentPage.value?.route.name === 'object-storage',
 	}, {
-		icon: 'ph-lock ph-bold ph-lg',
+		icon: 'ti ti-lock',
 		text: i18n.ts.security,
 		to: '/admin/security',
 		active: currentPage.value?.route.name === 'security',
 	}, {
-		icon: 'ph-planet ph-bold ph-lg',
+		icon: 'ti ti-planet',
 		text: i18n.ts.relays,
 		to: '/admin/relays',
 		active: currentPage.value?.route.name === 'relays',
 	}, {
-		icon: 'ph-prohibit ph-bold ph-lg',
+		icon: 'ti ti-ban',
 		text: i18n.ts.instanceBlocking,
 		to: '/admin/instance-block',
 		active: currentPage.value?.route.name === 'instance-block',
 	}, {
-		icon: 'ph-ghost ph-bold ph-lg',
+		icon: 'ti ti-ghost',
 		text: i18n.ts.proxyAccount,
 		to: '/admin/proxy-account',
 		active: currentPage.value?.route.name === 'proxy-account',
@@ -230,7 +230,12 @@ const menuDef = computed(() => [{
 		to: '/admin/external-services',
 		active: currentPage.value?.route.name === 'external-services',
 	}, {
-		icon: 'ph-faders ph-bold ph-lg',
+		icon: 'ti ti-webhook',
+		text: 'Webhook',
+		to: '/admin/system-webhook',
+		active: currentPage.value?.route.name === 'system-webhook',
+	}, {
+		icon: 'ti ti-adjustments',
 		text: i18n.ts.other,
 		to: '/admin/other-settings',
 		active: currentPage.value?.route.name === 'other-settings',
@@ -238,32 +243,29 @@ const menuDef = computed(() => [{
 }, {
 	title: i18n.ts.info,
 	items: [{
-		icon: 'ph-database ph-bold ph-lg',
+		icon: 'ti ti-database',
 		text: i18n.ts.database,
 		to: '/admin/database',
 		active: currentPage.value?.route.name === 'database',
 	}],
 }]);
 
-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');
 	}
 });
 
@@ -305,25 +307,25 @@ function invite() {
 function adminLookup(ev: MouseEvent) {
 	os.popupMenu([{
 		text: i18n.ts.user,
-		icon: 'ph-user ph-bold ph-lg',
+		icon: 'ti ti-user',
 		action: () => {
 			lookupUser();
 		},
 	}, {
 		text: `${i18n.ts.user} (${i18n.ts.email})`,
-		icon: 'ph-user ph-bold ph-lg',
+		icon: 'ti ti-user',
 		action: () => {
 			lookupUserByEmail();
 		},
 	}, {
 		text: i18n.ts.file,
-		icon: 'ph-cloud ph-bold ph-lg',
+		icon: 'ti ti-cloud',
 		action: () => {
 			lookupFile();
 		},
 	}, {
 		text: i18n.ts.lookup,
-		icon: 'ph-planet ph-bold ph-lg',
+		icon: 'ti ti-world-search',
 		action: () => {
 			lookup();
 		},
diff --git a/packages/frontend/src/pages/admin/instance-block.vue b/packages/frontend/src/pages/admin/instance-block.vue
index fcb67633f6..e090616b26 100644
--- a/packages/frontend/src/pages/admin/instance-block.vue
+++ b/packages/frontend/src/pages/admin/instance-block.vue
@@ -8,15 +8,23 @@ SPDX-License-Identifier: AGPL-3.0-only
 	<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="ph-floppy-disk ph-bold ph-lg"></i> {{ i18n.ts.save }}</MkButton>
+			<template v-if="tab === 'block'">
+				<MkTextarea v-model="blockedHosts">
+					<span>{{ i18n.ts.blockedInstances }}</span>
+					<template #caption>{{ i18n.ts.blockedInstancesDescription }}</template>
+				</MkTextarea>
+			</template>
+			<template v-else-if="tab === 'silence'">
+				<MkTextarea v-model="silencedHosts" class="_formBlock">
+					<span>{{ i18n.ts.silencedInstances }}</span>
+					<template #caption>{{ i18n.ts.silencedInstancesDescription }}</template>
+				</MkTextarea>
+				<MkTextarea v-model="mediaSilencedHosts" class="_formBlock">
+					<span>{{ i18n.ts.mediaSilencedInstances }}</span>
+					<template #caption>{{ i18n.ts.mediaSilencedInstancesDescription }}</template>
+				</MkTextarea>
+			</template>
+			<MkButton primary @click="save"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton>
 		</FormSuspense>
 	</MkSpacer>
 </MkStickyContainer>
@@ -36,18 +44,21 @@ import { definePageMetadata } from '@/scripts/page-metadata.js';
 
 const blockedHosts = ref<string>('');
 const silencedHosts = ref<string>('');
+const mediaSilencedHosts = 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');
+	mediaSilencedHosts.value = meta.mediaSilencedHosts.join('\n');
 }
 
 function save() {
 	os.apiWithDialog('admin/update-meta', {
 		blockedHosts: blockedHosts.value.split('\n') || [],
 		silencedHosts: silencedHosts.value.split('\n') || [],
+		mediaSilencedHosts: mediaSilencedHosts.value.split('\n') || [],
 
 	}).then(() => {
 		fetchInstance(true);
@@ -59,15 +70,15 @@ const headerActions = computed(() => []);
 const headerTabs = computed(() => [{
 	key: 'block',
 	title: i18n.ts.block,
-	icon: 'ph-prohibit ph-bold ph-lg',
+	icon: 'ti ti-ban',
 }, {
 	key: 'silence',
 	title: i18n.ts.silence,
-	icon: 'ph-eye-closed ph-bold ph-lg',
+	icon: 'ti ti-eye-off',
 }]);
 
 definePageMetadata(() => ({
 	title: i18n.ts.instanceBlocking,
-	icon: 'ph-prohibit ph-bold ph-lg',
+	icon: 'ti ti-ban',
 }));
 </script>
diff --git a/packages/frontend/src/pages/admin/invites.vue b/packages/frontend/src/pages/admin/invites.vue
index 4dc8ded7a2..c4f2c292e0 100644
--- a/packages/frontend/src/pages/admin/invites.vue
+++ b/packages/frontend/src/pages/admin/invites.vue
@@ -9,7 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 	<MkSpacer :contentMax="800">
 		<div class="_gaps_m">
 			<MkFolder :expanded="false">
-				<template #icon><i class="ph-plus ph-bold ph-lg"></i></template>
+				<template #icon><i class="ti ti-plus"></i></template>
 				<template #label>{{ i18n.ts.createInviteCode }}</template>
 
 				<div class="_gaps_m">
@@ -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>
@@ -115,7 +115,7 @@ const headerTabs = computed(() => []);
 
 definePageMetadata(() => ({
 	title: i18n.ts.invite,
-	icon: 'ph-user-plus ph-bold ph-lg',
+	icon: 'ti ti-user-plus',
 }));
 </script>
 
diff --git a/packages/frontend/src/pages/admin/moderation.vue b/packages/frontend/src/pages/admin/moderation.vue
index 46ffb6bbc6..6297b9a182 100644
--- a/packages/frontend/src/pages/admin/moderation.vue
+++ b/packages/frontend/src/pages/admin/moderation.vue
@@ -25,12 +25,12 @@ SPDX-License-Identifier: AGPL-3.0-only
 					<FormLink to="/admin/server-rules">{{ i18n.ts.serverRules }}</FormLink>
 
 					<MkInput v-model="tosUrl" type="url">
-						<template #prefix><i class="ph-link ph-bold ph-lg"></i></template>
+						<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="ph-link ph-bold ph-lg"></i></template>
+						<template #prefix><i class="ti ti-link"></i></template>
 						<template #label>{{ i18n.ts.privacyPolicyUrl }}</template>
 					</MkInput>
 
@@ -40,7 +40,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 					</MkTextarea>
 
 					<MkInput v-model="inquiryUrl" type="url">
-						<template #prefix><i class="ph-link ph-bold ph-lg"></i></template>
+						<template #prefix><i class="ti ti-link"></i></template>
 						<template #label>{{ i18n.ts._serverSettings.inquiryUrl }}</template>
 						<template #caption>{{ i18n.ts._serverSettings.inquiryUrlDescription }}</template>
 					</MkInput>
@@ -70,7 +70,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 		<template #footer>
 			<div :class="$style.footer">
 				<MkSpacer :contentMax="700" :marginMin="16" :marginMax="16">
-					<MkButton primary rounded @click="save"><i class="ph-check ph-bold ph-lg"></i> {{ i18n.ts.save }}</MkButton>
+					<MkButton primary rounded @click="save"><i class="ti ti-check"></i> {{ i18n.ts.save }}</MkButton>
 				</MkSpacer>
 			</div>
 		</template>
@@ -144,7 +144,7 @@ const headerTabs = computed(() => []);
 
 definePageMetadata(() => ({
 	title: i18n.ts.moderation,
-	icon: 'ph-shield ph-bold ph-lg',
+	icon: 'ti ti-shield',
 }));
 </script>
 
diff --git a/packages/frontend/src/pages/admin/modlog.ModLog.vue b/packages/frontend/src/pages/admin/modlog.ModLog.vue
index 03d5d6ece1..f6f276de53 100644
--- a/packages/frontend/src/pages/admin/modlog.ModLog.vue
+++ b/packages/frontend/src/pages/admin/modlog.ModLog.vue
@@ -8,9 +8,40 @@ 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', 'approve', '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',
+					'approve',
+					'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>
@@ -18,8 +49,8 @@ SPDX-License-Identifier: AGPL-3.0-only
 		<span v-else-if="log.type === 'approve'">: @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }}</span>
 		<span v-else-if="log.type === 'unsuspend'">: @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }}</span>
 		<span v-else-if="log.type === 'resetPassword'">: @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }}</span>
-		<span v-else-if="log.type === 'assignRole'">: @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }} <i class="ph-arrow-right ph-bold ph-lg"></i> {{ log.info.roleName }}</span>
-		<span v-else-if="log.type === 'unassignRole'">: @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }} <i class="ph-prohibit ph-bold ph-lg"></i> {{ log.info.roleName }}</span>
+		<span v-else-if="log.type === 'assignRole'">: @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }} <i class="ti ti-arrow-right"></i> {{ log.info.roleName }}</span>
+		<span v-else-if="log.type === 'unassignRole'">: @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }} <i class="ti ti-equal-not"></i> {{ log.info.roleName }}</span>
 		<span v-else-if="log.type === 'createRole'">: {{ log.info.role.name }}</span>
 		<span v-else-if="log.type === 'updateRole'">: {{ log.info.before.name }}</span>
 		<span v-else-if="log.type === 'deleteRole'">: {{ log.info.role.name }}</span>
@@ -41,6 +72,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"/>
@@ -115,11 +156,20 @@ 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>
 
 		<details>
 			<summary>raw</summary>
diff --git a/packages/frontend/src/pages/admin/modlog.vue b/packages/frontend/src/pages/admin/modlog.vue
index 850d36063e..4e0d0f941e 100644
--- a/packages/frontend/src/pages/admin/modlog.vue
+++ b/packages/frontend/src/pages/admin/modlog.vue
@@ -60,6 +60,6 @@ const headerTabs = computed(() => []);
 
 definePageMetadata(() => ({
 	title: i18n.ts.moderationLogs,
-	icon: 'ph-list ph-bold ph-lg-search',
+	icon: 'ti ti-list-search',
 }));
 </script>
diff --git a/packages/frontend/src/pages/admin/object-storage.vue b/packages/frontend/src/pages/admin/object-storage.vue
index 4f362e1814..5fddb715cd 100644
--- a/packages/frontend/src/pages/admin/object-storage.vue
+++ b/packages/frontend/src/pages/admin/object-storage.vue
@@ -40,12 +40,12 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 					<FormSplit :minWidth="280">
 						<MkInput v-model="objectStorageAccessKey">
-							<template #prefix><i class="ph-key ph-bold ph-lg"></i></template>
+							<template #prefix><i class="ti ti-key"></i></template>
 							<template #label>Access key</template>
 						</MkInput>
 
 						<MkInput v-model="objectStorageSecretKey" type="password">
-							<template #prefix><i class="ph-key ph-bold ph-lg"></i></template>
+							<template #prefix><i class="ti ti-key"></i></template>
 							<template #label>Secret key</template>
 						</MkInput>
 					</FormSplit>
@@ -75,7 +75,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 	<template #footer>
 		<div :class="$style.footer">
 			<MkSpacer :contentMax="700" :marginMin="16" :marginMax="16">
-				<MkButton primary rounded @click="save"><i class="ph-check ph-bold ph-lg"></i> {{ i18n.ts.save }}</MkButton>
+				<MkButton primary rounded @click="save"><i class="ti ti-check"></i> {{ i18n.ts.save }}</MkButton>
 			</MkSpacer>
 		</div>
 	</template>
@@ -151,7 +151,7 @@ const headerTabs = computed(() => []);
 
 definePageMetadata(() => ({
 	title: i18n.ts.objectStorage,
-	icon: 'ph-cloud ph-bold ph-lg',
+	icon: 'ti ti-cloud',
 }));
 </script>
 
diff --git a/packages/frontend/src/pages/admin/other-settings.vue b/packages/frontend/src/pages/admin/other-settings.vue
index 20e0d6e578..a92034f2d7 100644
--- a/packages/frontend/src/pages/admin/other-settings.vue
+++ b/packages/frontend/src/pages/admin/other-settings.vue
@@ -99,7 +99,7 @@ function save() {
 
 const headerActions = computed(() => [{
 	asFullButton: true,
-	icon: 'ph-check ph-bold ph-lg',
+	icon: 'ti ti-check',
 	text: i18n.ts.save,
 	handler: save,
 }]);
@@ -108,6 +108,6 @@ const headerTabs = computed(() => []);
 
 definePageMetadata(() => ({
 	title: i18n.ts.other,
-	icon: 'ph-faders ph-bold ph-lg',
+	icon: 'ti ti-adjustments',
 }));
 </script>
diff --git a/packages/frontend/src/pages/admin/overview.federation.vue b/packages/frontend/src/pages/admin/overview.federation.vue
index 3a3550c6c0..ea01d073ea 100644
--- a/packages/frontend/src/pages/admin/overview.federation.vue
+++ b/packages/frontend/src/pages/admin/overview.federation.vue
@@ -21,7 +21,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 		</div>
 		<div v-if="!fetching" class="items">
 			<div class="item _panel sub">
-				<div class="icon"><i class="ph-globe-hemisphere-west ph-bold ph-lg-download"></i></div>
+				<div class="icon"><i class="ti ti-world-download"></i></div>
 				<div class="body">
 					<div class="value">
 						{{ number(federationSubActive) }}
@@ -31,7 +31,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 				</div>
 			</div>
 			<div class="item _panel pub">
-				<div class="icon"><i class="ph-globe-hemisphere-west ph-bold ph-lg-upload"></i></div>
+				<div class="icon"><i class="ti ti-world-upload"></i></div>
 				<div class="body">
 					<div class="value">
 						{{ number(federationPubActive) }}
@@ -193,4 +193,3 @@ onMounted(async () => {
 	}
 }
 </style>
-
diff --git a/packages/frontend/src/pages/admin/overview.stats.vue b/packages/frontend/src/pages/admin/overview.stats.vue
index 27ae52c32c..37399a5724 100644
--- a/packages/frontend/src/pages/admin/overview.stats.vue
+++ b/packages/frontend/src/pages/admin/overview.stats.vue
@@ -9,7 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 		<MkLoading v-if="fetching"/>
 		<div v-else :class="$style.root">
 			<div class="item _panel users">
-				<div class="icon"><i class="ph-users ph-bold ph-lg"></i></div>
+				<div class="icon"><i class="ti ti-users"></i></div>
 				<div class="body">
 					<div class="value">
 						<MkNumber :value="stats.originalUsersCount" style="margin-right: 0.5em;"/>
@@ -19,7 +19,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 				</div>
 			</div>
 			<div class="item _panel notes">
-				<div class="icon"><i class="ph-pencil-simple ph-bold ph-lg"></i></div>
+				<div class="icon"><i class="ti ti-pencil"></i></div>
 				<div class="body">
 					<div class="value">
 						<MkNumber :value="stats.originalNotesCount" style="margin-right: 0.5em;"/>
@@ -29,7 +29,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 				</div>
 			</div>
 			<div class="item _panel instances">
-				<div class="icon"><i class="ph-planet ph-bold ph-lg"></i></div>
+				<div class="icon"><i class="ti ti-planet"></i></div>
 				<div class="body">
 					<div class="value">
 						<MkNumber :value="stats.instances" style="margin-right: 0.5em;"/>
@@ -47,7 +47,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 				</div>
 			</div>
 			<div class="item _panel online">
-				<div class="icon"><i class="ph-broadcast ph-bold ph-lg"></i></div>
+				<div class="icon"><i class="ti ti-access-point"></i></div>
 				<div class="body">
 					<div class="value">
 						<MkNumber :value="onlineUsersCount" style="margin-right: 0.5em;"/>
diff --git a/packages/frontend/src/pages/admin/overview.vue b/packages/frontend/src/pages/admin/overview.vue
index bf766600e5..1de4dc0dc8 100644
--- a/packages/frontend/src/pages/admin/overview.vue
+++ b/packages/frontend/src/pages/admin/overview.vue
@@ -186,7 +186,7 @@ const headerTabs = computed(() => []);
 
 definePageMetadata(() => ({
 	title: i18n.ts.dashboard,
-	icon: 'ph-gauge ph-bold ph-lg',
+	icon: 'ti ti-dashboard',
 }));
 </script>
 
diff --git a/packages/frontend/src/pages/admin/proxy-account.vue b/packages/frontend/src/pages/admin/proxy-account.vue
index 59fd5911d4..81db9f1da9 100644
--- a/packages/frontend/src/pages/admin/proxy-account.vue
+++ b/packages/frontend/src/pages/admin/proxy-account.vue
@@ -66,6 +66,6 @@ const headerTabs = computed(() => []);
 
 definePageMetadata(() => ({
 	title: i18n.ts.proxyAccount,
-	icon: 'ph-ghost ph-bold ph-lg',
+	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 f7b4b27a68..8d3fe35320 100644
--- a/packages/frontend/src/pages/admin/queue.chart.vue
+++ b/packages/frontend/src/pages/admin/queue.chart.vue
@@ -30,7 +30,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 		</div>
 	</div>
 	<MkFolder :defaultOpen="true" :max-height="250">
-		<template #icon><i class="ph-warning ph-bold ph-lg"></i></template>
+		<template #icon><i class="ti ti-alert-triangle"></i></template>
 		<template #label>Errored instances</template>
 		<template #suffix>({{ number(jobs.reduce((a, b) => a + b[1], 0)) }} jobs)</template>
 
diff --git a/packages/frontend/src/pages/admin/queue.vue b/packages/frontend/src/pages/admin/queue.vue
index ba6911a943..8d77d927d7 100644
--- a/packages/frontend/src/pages/admin/queue.vue
+++ b/packages/frontend/src/pages/admin/queue.vue
@@ -10,7 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 		<XQueue v-if="tab === 'deliver'" domain="deliver"/>
 		<XQueue v-else-if="tab === 'inbox'" domain="inbox"/>
 		<br>
-		<MkButton @click="promoteAllQueues"><i class="ph-arrow-clockwise ph-bold ph-lg"></i> {{ i18n.ts.retryAllQueuesNow }}</MkButton>
+		<MkButton @click="promoteAllQueues"><i class="ti ti-reload"></i> {{ i18n.ts.retryAllQueuesNow }}</MkButton>
 	</MkSpacer>
 </MkStickyContainer>
 </template>
@@ -53,7 +53,7 @@ function promoteAllQueues() {
 
 const headerActions = computed(() => [{
 	asFullButton: true,
-	icon: 'ph-arrow-square-out ph-bold ph-lg',
+	icon: 'ti ti-external-link',
 	text: i18n.ts.dashboard,
 	handler: () => {
 		window.open(config.url + '/queue', '_blank', 'noopener');
@@ -70,6 +70,6 @@ const headerTabs = computed(() => [{
 
 definePageMetadata(() => ({
 	title: i18n.ts.jobQueue,
-	icon: 'ph-clock ph-bold ph-lg-play',
+	icon: 'ti ti-clock-play',
 }));
 </script>
diff --git a/packages/frontend/src/pages/admin/relays.vue b/packages/frontend/src/pages/admin/relays.vue
index 6ff0d8bd22..04982eea1f 100644
--- a/packages/frontend/src/pages/admin/relays.vue
+++ b/packages/frontend/src/pages/admin/relays.vue
@@ -11,12 +11,12 @@ 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="ph-check ph-bold ph-lg" :class="$style.icon" style="color: var(--success);"></i>
-					<i v-else-if="relay.status === 'rejected'" class="ph-prohibit ph-bold ph-lg" :class="$style.icon" style="color: var(--error);"></i>
-					<i v-else class="ph-clock ph-bold ph-lg" :class="$style.icon"></i>
+					<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-else class="ti ti-clock" :class="$style.icon"></i>
 					<span>{{ i18n.ts._relayStatus[relay.status] }}</span>
 				</div>
-				<MkButton class="button" inline danger @click="remove(relay.inbox)"><i class="ph-trash ph-bold ph-lg"></i> {{ i18n.ts.remove }}</MkButton>
+				<MkButton class="button" inline danger @click="remove(relay.inbox)"><i class="ti ti-trash"></i> {{ i18n.ts.remove }}</MkButton>
 			</div>
 		</div>
 	</MkSpacer>
@@ -77,7 +77,7 @@ refresh();
 
 const headerActions = computed(() => [{
 	asFullButton: true,
-	icon: 'ph-plus ph-bold ph-lg',
+	icon: 'ti ti-plus',
 	text: i18n.ts.addRelay,
 	handler: addRelay,
 }]);
@@ -86,7 +86,7 @@ const headerTabs = computed(() => []);
 
 definePageMetadata(() => ({
 	title: i18n.ts.relays,
-	icon: 'ph-planet ph-bold ph-lg',
+	icon: 'ti ti-planet',
 }));
 </script>
 
diff --git a/packages/frontend/src/pages/admin/roles.edit.vue b/packages/frontend/src/pages/admin/roles.edit.vue
index e6023d2f2a..60f06d50ba 100644
--- a/packages/frontend/src/pages/admin/roles.edit.vue
+++ b/packages/frontend/src/pages/admin/roles.edit.vue
@@ -13,7 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 		<template #footer>
 			<div :class="$style.footer">
 				<MkSpacer :contentMax="600" :marginMin="16" :marginMax="16">
-					<MkButton primary rounded @click="save"><i class="ph-check ph-bold ph-lg"></i> {{ i18n.ts.save }}</MkButton>
+					<MkButton primary rounded @click="save"><i class="ti ti-check"></i> {{ i18n.ts.save }}</MkButton>
 				</MkSpacer>
 			</div>
 		</template>
@@ -89,7 +89,7 @@ const headerTabs = computed(() => []);
 
 definePageMetadata(() => ({
 	title: role.value ? `${i18n.ts._role.edit}: ${role.value.name}` : i18n.ts._role.new,
-	icon: 'ph-seal-check ph-bold ph-lg',
+	icon: 'ti ti-badge',
 }));
 </script>
 
diff --git a/packages/frontend/src/pages/admin/roles.editor.vue b/packages/frontend/src/pages/admin/roles.editor.vue
index 99a31d5157..8200244cd7 100644
--- a/packages/frontend/src/pages/admin/roles.editor.vue
+++ b/packages/frontend/src/pages/admin/roles.editor.vue
@@ -31,7 +31,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 	</MkInput>
 
 	<MkSelect v-model="rolePermission" :readonly="readonly">
-		<template #label><i class="ph-shield ph-bold ph-lg-lock"></i> {{ i18n.ts._role.permission }}</template>
+		<template #label><i class="ti ti-shield-lock"></i> {{ i18n.ts._role.permission }}</template>
 		<template #caption><div v-html="i18n.ts._role.descriptionOfPermission.replaceAll('\n', '<br>')"></div></template>
 		<option value="normal">{{ i18n.ts.normalUser }}</option>
 		<option value="moderator">{{ i18n.ts.moderator }}</option>
@@ -39,7 +39,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 	</MkSelect>
 
 	<MkSelect v-model="role.target" :readonly="readonly">
-		<template #label><i class="ph-users ph-bold ph-lg"></i> {{ i18n.ts._role.assignTarget }}</template>
+		<template #label><i class="ti ti-users"></i> {{ i18n.ts._role.assignTarget }}</template>
 		<template #caption><div v-html="i18n.ts._role.descriptionOfAssignTarget.replaceAll('\n', '<br>')"></div></template>
 		<option value="manual">{{ i18n.ts._role.manual }}</option>
 		<option value="conditional">{{ i18n.ts._role.conditional }}</option>
@@ -76,7 +76,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 		<template #label><i class="ph-scroll ph-bold ph-lg"></i> {{ i18n.ts._role.policies }}</template>
 		<div class="_gaps_s">
 			<MkInput v-model="q" type="search">
-				<template #prefix><i class="ph-magnifying-glass ph-bold ph-lg"></i></template>
+				<template #prefix><i class="ti ti-search"></i></template>
 			</MkInput>
 
 			<MkFolder v-if="matchQuery([i18n.ts._role._options.rateLimitFactor, 'rateLimitFactor'])">
@@ -418,6 +418,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>
@@ -665,9 +685,9 @@ const rolePermission = computed({
 const q = ref('');
 
 function getPriorityIcon(option) {
-	if (option.priority === 2) return 'ph-arrow-up ph-bold ph-lg';
-	if (option.priority === 1) return 'ph-arrow-up ph-bold ph-lg';
-	return 'ph-circle ph-bold ph-lg';
+	if (option.priority === 2) return 'ti ti-arrows-up';
+	if (option.priority === 1) return 'ti ti-arrow-narrow-up';
+	return 'ti ti-point';
 }
 
 function matchQuery(keywords: string[]): boolean {
diff --git a/packages/frontend/src/pages/admin/roles.role.vue b/packages/frontend/src/pages/admin/roles.role.vue
index 593183ea07..6bce60cfb9 100644
--- a/packages/frontend/src/pages/admin/roles.role.vue
+++ b/packages/frontend/src/pages/admin/roles.role.vue
@@ -10,20 +10,20 @@ SPDX-License-Identifier: AGPL-3.0-only
 		<MkSpacer :contentMax="700">
 			<div class="_gaps">
 				<div class="_buttons">
-					<MkButton primary rounded @click="edit"><i class="ph-pencil-simple ph-bold ph-lg"></i> {{ i18n.ts.edit }}</MkButton>
-					<MkButton danger rounded @click="del"><i class="ph-trash ph-bold ph-lg"></i> {{ i18n.ts.delete }}</MkButton>
+					<MkButton primary rounded @click="edit"><i class="ti ti-pencil"></i> {{ i18n.ts.edit }}</MkButton>
+					<MkButton danger rounded @click="del"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton>
 				</div>
 				<MkFolder>
-					<template #icon><i class="ph-info ph-bold ph-lg"></i></template>
+					<template #icon><i class="ti ti-info-circle"></i></template>
 					<template #label>{{ i18n.ts.info }}</template>
 					<XEditor :modelValue="role" readonly/>
 				</MkFolder>
 				<MkFolder v-if="role.target === 'manual'" defaultOpen>
-					<template #icon><i class="ph-users ph-bold ph-lg"></i></template>
+					<template #icon><i class="ti ti-users"></i></template>
 					<template #label>{{ i18n.ts.users }}</template>
 					<template #suffix>{{ role.usersCount }}</template>
 					<div class="_gaps">
-						<MkButton primary rounded @click="assign"><i class="ph-plus ph-bold ph-lg"></i> {{ i18n.ts.assign }}</MkButton>
+						<MkButton primary rounded @click="assign"><i class="ti ti-plus"></i> {{ i18n.ts.assign }}</MkButton>
 
 						<MkPagination :pagination="usersPagination" :displayLimit="50">
 							<template #empty>
@@ -40,8 +40,8 @@ SPDX-License-Identifier: AGPL-3.0-only
 											<MkA :class="$style.userItemMainBody" :to="`/admin/user/${item.user.id}`">
 												<MkUserCardMini :user="item.user"/>
 											</MkA>
-											<button class="_button" :class="$style.userToggle" @click="toggleItem(item)"><i :class="$style.chevron" class="ph-caret-down ph-bold ph-lg"></i></button>
-											<button class="_button" :class="$style.unassign" @click="unassign(item.user, $event)"><i class="ph-x ph-bold ph-lg"></i></button>
+											<button class="_button" :class="$style.userToggle" @click="toggleItem(item)"><i :class="$style.chevron" class="ti ti-chevron-down"></i></button>
+											<button class="_button" :class="$style.unassign" @click="unassign(item.user, $event)"><i class="ti ti-x"></i></button>
 										</div>
 										<div v-if="expandedItems.includes(item.id)" :class="$style.userItemSub">
 											<div>Assigned: <MkTime :time="item.createdAt" mode="detail"/></div>
@@ -149,7 +149,7 @@ async function assign() {
 async function unassign(user, ev) {
 	os.popupMenu([{
 		text: i18n.ts.unassign,
-		icon: 'ph-x ph-bold ph-lg',
+		icon: 'ti ti-x',
 		danger: true,
 		action: async () => {
 			await os.apiWithDialog('admin/roles/unassign', { roleId: role.id, userId: user.id });
@@ -172,7 +172,7 @@ const headerTabs = computed(() => []);
 
 definePageMetadata(() => ({
 	title: `${i18n.ts.role}: ${role.name}`,
-	icon: 'ph-seal-check ph-bold ph-lg',
+	icon: 'ti ti-badge',
 }));
 </script>
 
diff --git a/packages/frontend/src/pages/admin/roles.vue b/packages/frontend/src/pages/admin/roles.vue
index f104f9a00d..0a8bd0e898 100644
--- a/packages/frontend/src/pages/admin/roles.vue
+++ b/packages/frontend/src/pages/admin/roles.vue
@@ -13,7 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 					<template #label>{{ i18n.ts._role.baseRole }}</template>
 					<div class="_gaps_s">
 						<MkInput v-model="baseRoleQ" type="search">
-							<template #prefix><i class="ph-magnifying-glass ph-bold ph-lg"></i></template>
+							<template #prefix><i class="ti ti-search"></i></template>
 						</MkInput>
 
 						<MkFolder v-if="matchQuery([i18n.ts._role._options.rateLimitFactor, 'rateLimitFactor'])">
@@ -153,6 +153,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>
@@ -228,7 +236,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 						<MkButton primary rounded @click="updateBaseRole">{{ i18n.ts.save }}</MkButton>
 					</div>
 				</MkFolder>
-				<MkButton primary rounded @click="create"><i class="ph-plus ph-bold ph-lg"></i> {{ i18n.ts._role.new }}</MkButton>
+				<MkButton primary rounded @click="create"><i class="ti ti-plus"></i> {{ i18n.ts._role.new }}</MkButton>
 				<div class="_gaps_s">
 					<MkFoldableSection>
 						<template #header>{{ i18n.ts._role.manualRoles }}</template>
@@ -263,7 +271,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 { 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';
@@ -287,6 +295,7 @@ async function updateBaseRole() {
 	await os.apiWithDialog('admin/roles/update-default-policies', {
 		policies,
 	});
+	fetchInstance(true);
 }
 
 function create() {
@@ -299,7 +308,7 @@ const headerTabs = computed(() => []);
 
 definePageMetadata(() => ({
 	title: i18n.ts.roles,
-	icon: 'ph-seal-check ph-bold ph-lg',
+	icon: 'ti ti-badges',
 }));
 </script>
 
diff --git a/packages/frontend/src/pages/admin/security.vue b/packages/frontend/src/pages/admin/security.vue
index 887d3c1446..2e80622d7a 100644
--- a/packages/frontend/src/pages/admin/security.vue
+++ b/packages/frontend/src/pages/admin/security.vue
@@ -10,7 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 		<FormSuspense :p="init">
 			<div class="_gaps_m">
 				<MkFolder>
-					<template #icon><i class="ph-shield ph-bold ph-lg"></i></template>
+					<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>
@@ -35,21 +35,21 @@ SPDX-License-Identifier: AGPL-3.0-only
 							<template #label>Use Verifymail.io API</template>
 						</MkSwitch>
 						<MkInput v-model="verifymailAuthKey">
-							<template #prefix><i class="ph-key ph-bold ph-lg"></i></template>
+							<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="ph-key ph-bold ph-lg"></i></template>
+							<template #prefix><i class="ti ti-key"></i></template>
 							<template #label>TrueMail API Instance</template>
 						</MkInput>
 						<MkInput v-model="truemailAuthKey">
-							<template #prefix><i class="ph-key ph-bold ph-lg"></i></template>
+							<template #prefix><i class="ti ti-key"></i></template>
 							<template #label>TrueMail API Auth Key</template>
 						</MkInput>
-						<MkButton primary @click="save"><i class="ph-floppy-disk ph-bold ph-lg"></i> {{ i18n.ts.save }}</MkButton>
+						<MkButton primary @click="save"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton>
 					</div>
 				</MkFolder>
 
@@ -60,7 +60,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 						<MkTextarea v-model="bannedEmailDomains">
 							<template #label>Banned Email Domains List</template>
 						</MkTextarea>
-						<MkButton primary @click="save"><i class="ph-floppy-disk ph-bold ph-lg"></i> {{ i18n.ts.save }}</MkButton>
+						<MkButton primary @click="save"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton>
 					</div>
 				</MkFolder>
 
@@ -149,6 +149,6 @@ const headerTabs = computed(() => []);
 
 definePageMetadata(() => ({
 	title: i18n.ts.security,
-	icon: 'ph-lock ph-bold ph-lg',
+	icon: 'ti ti-lock',
 }));
 </script>
diff --git a/packages/frontend/src/pages/admin/server-rules.vue b/packages/frontend/src/pages/admin/server-rules.vue
index db2bb56eac..4788c16830 100644
--- a/packages/frontend/src/pages/admin/server-rules.vue
+++ b/packages/frontend/src/pages/admin/server-rules.vue
@@ -23,16 +23,16 @@ SPDX-License-Identifier: AGPL-3.0-only
 						<div :class="$style.item">
 							<div :class="$style.itemHeader">
 								<div :class="$style.itemNumber" v-text="String(index + 1)"/>
-								<span :class="$style.itemHandle"><i class="ph-list ph-bold ph-lg"/></span>
-								<button class="_button" :class="$style.itemRemove" @click="remove(index)"><i class="ph-x ph-bold ph-lg"></i></button>
+								<span :class="$style.itemHandle"><i class="ti ti-menu"/></span>
+								<button class="_button" :class="$style.itemRemove" @click="remove(index)"><i class="ti ti-x"></i></button>
 							</div>
 							<MkInput v-model="serverRules[index]"/>
 						</div>
 					</template>
 				</Sortable>
 				<div :class="$style.commands">
-					<MkButton rounded @click="serverRules.push('')"><i class="ph-plus ph-bold ph-lg"></i> {{ i18n.ts.add }}</MkButton>
-					<MkButton primary rounded @click="save"><i class="ph-check ph-bold ph-lg"></i> {{ i18n.ts.save }}</MkButton>
+					<MkButton rounded @click="serverRules.push('')"><i class="ti ti-plus"></i> {{ i18n.ts.add }}</MkButton>
+					<MkButton primary rounded @click="save"><i class="ti ti-check"></i> {{ i18n.ts.save }}</MkButton>
 				</div>
 			</div>
 		</MkSpacer>
@@ -69,7 +69,7 @@ const headerTabs = computed(() => []);
 
 definePageMetadata(() => ({
 	title: i18n.ts.serverRules,
-	icon: 'ph-check ph-bold ph-lg',
+	icon: 'ti ti-checkbox',
 }));
 </script>
 
diff --git a/packages/frontend/src/pages/admin/settings.vue b/packages/frontend/src/pages/admin/settings.vue
index ebbe252cb7..90ac259c53 100644
--- a/packages/frontend/src/pages/admin/settings.vue
+++ b/packages/frontend/src/pages/admin/settings.vue
@@ -29,14 +29,14 @@ SPDX-License-Identifier: AGPL-3.0-only
 						</MkInput>
 
 						<MkInput v-model="maintainerEmail" type="email">
-							<template #prefix><i class="ph-envelope ph-bold ph-lg"></i></template>
+							<template #prefix><i class="ti ti-mail"></i></template>
 							<template #label>{{ i18n.ts.maintainerEmail }}</template>
 						</MkInput>
 					</FormSplit>
 
 					<MkInput v-model="repositoryUrl" type="url">
 						<template #label>{{ i18n.ts.repositoryUrl }}</template>
-						<template #prefix><i class="ph-link ph-bold ph-lg"></i></template>
+						<template #prefix><i class="ti ti-link"></i></template>
 						<template #caption>{{ i18n.ts.repositoryUrlDescription }}</template>
 					</MkInput>
 
@@ -46,7 +46,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 					<MkInput v-model="impressumUrl" type="url">
 						<template #label>{{ i18n.ts.impressumUrl }}</template>
-						<template #prefix><i class="ph-link ph-bold ph-lg"></i></template>
+						<template #prefix><i class="ti ti-link"></i></template>
 						<template #caption>{{ i18n.ts.impressumDescription }}</template>
 					</MkInput>
 
@@ -89,12 +89,12 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 							<template v-if="enableServiceWorker">
 								<MkInput v-model="swPublicKey">
-									<template #prefix><i class="ph-key ph-bold ph-lg"></i></template>
+									<template #prefix><i class="ti ti-key"></i></template>
 									<template #label>Public key</template>
 								</MkInput>
 
 								<MkInput v-model="swPrivateKey">
-									<template #prefix><i class="ph-key ph-bold ph-lg"></i></template>
+									<template #prefix><i class="ti ti-key"></i></template>
 									<template #label>Private key</template>
 								</MkInput>
 							</template>
@@ -201,7 +201,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 		<template #footer>
 			<div :class="$style.footer">
 				<MkSpacer :contentMax="700" :marginMin="16" :marginMax="16">
-					<MkButton primary rounded @click="save"><i class="ph-check ph-bold ph-lg"></i> {{ i18n.ts.save }}</MkButton>
+					<MkButton primary rounded @click="save"><i class="ti ti-check"></i> {{ i18n.ts.save }}</MkButton>
 				</MkSpacer>
 			</div>
 		</template>
@@ -325,7 +325,7 @@ const headerTabs = computed(() => []);
 
 definePageMetadata(() => ({
 	title: i18n.ts.general,
-	icon: 'ph-gear ph-bold ph-lg',
+	icon: 'ti ti-settings',
 }));
 </script>
 
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..0c07122af3
--- /dev/null
+++ b/packages/frontend/src/pages/admin/system-webhook.item.vue
@@ -0,0 +1,117 @@
+<!--
+SPDX-FileCopyrightText: syuilo and misskey-project
+SPDX-License-Identifier: AGPL-3.0-only
+-->
+
+<template>
+<div :class="$style.main">
+	<span :class="$style.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(--success)' }"
+		/>
+		<i v-else class="ti ti-alert-triangle" :style="{ color: 'var(--error)' }"/>
+	</span>
+	<span :class="$style.text">{{ entity.name || entity.url }}</span>
+	<span :class="$style.suffix">
+		<MkTime v-if="entity.latestSentAt" :time="entity.latestSentAt" style="margin-right: 8px"/>
+		<button :class="$style.suffixButton" @click="onEditClick">
+			<i class="ti ti-settings"></i>
+		</button>
+		<button :class="$style.suffixButton" @click="onDeleteClick">
+			<i class="ti ti-trash"></i>
+		</button>
+	</span>
+</div>
+</template>
+
+<script lang="ts" setup>
+import { entities } from 'misskey-js';
+import { toRefs } from '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">
+.main {
+	display: flex;
+	align-items: center;
+	width: 100%;
+	box-sizing: border-box;
+	padding: 10px 14px;
+	background: var(--buttonBg);
+	border: none;
+	border-radius: 6px;
+	font-size: 0.9em;
+
+	&:hover {
+		text-decoration: none;
+		background: var(--buttonHoverBg);
+	}
+
+	&.active {
+		color: var(--accent);
+		background: var(--buttonHoverBg);
+	}
+}
+
+.icon {
+	margin-right: 0.75em;
+	flex-shrink: 0;
+	text-align: center;
+	color: var(--fgTransparentWeak);
+}
+
+.text {
+	flex-shrink: 1;
+	white-space: normal;
+	padding-right: 12px;
+	text-align: center;
+}
+
+.suffix {
+	display: flex;
+	flex-direction: row;
+	align-items: center;
+	justify-content: center;
+	gaps: 4px;
+	margin-left: auto;
+	margin-right: -8px;
+	opacity: 0.7;
+	white-space: nowrap;
+}
+
+.suffixButton {
+	background: transparent;
+	border: none;
+	border-radius: 9999px;
+	margin-top: -8px;
+	margin-bottom: -8px;
+	padding: 8px;
+
+	&:hover {
+		background: var(--buttonBg);
+	}
+}
+</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..7a40eec944
--- /dev/null
+++ b/packages/frontend/src/pages/admin/system-webhook.vue
@@ -0,0 +1,96 @@
+<!--
+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 :class="$style.linkButton" full @click="onCreateWebhookClicked">
+				{{ 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">
+.linkButton {
+	text-align: left;
+	padding: 10px 18px;
+}
+</style>
diff --git a/packages/frontend/src/pages/admin/users.vue b/packages/frontend/src/pages/admin/users.vue
index 80696c8cea..e99dcfa489 100644
--- a/packages/frontend/src/pages/admin/users.vue
+++ b/packages/frontend/src/pages/admin/users.vue
@@ -34,11 +34,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>
@@ -121,17 +121,17 @@ function show(user) {
 }
 
 const headerActions = computed(() => [{
-	icon: 'ph-magnifying-glass ph-bold ph-lg',
+	icon: 'ti ti-search',
 	text: i18n.ts.search,
 	handler: searchUser,
 }, {
 	asFullButton: true,
-	icon: 'ph-plus ph-bold ph-lg',
+	icon: 'ti ti-plus',
 	text: i18n.ts.addUser,
 	handler: addUser,
 }, {
 	asFullButton: true,
-	icon: 'ph-magnifying-glass ph-bold ph-lg',
+	icon: 'ti ti-search',
 	text: i18n.ts.lookup,
 	handler: lookupUser,
 }]);
@@ -140,7 +140,7 @@ const headerTabs = computed(() => []);
 
 definePageMetadata(() => ({
 	title: i18n.ts.users,
-	icon: 'ph-users ph-bold ph-lg',
+	icon: 'ti ti-users',
 }));
 </script>
 
diff --git a/packages/frontend/src/pages/ads.vue b/packages/frontend/src/pages/ads.vue
index c6373e8d60..b31807f9f5 100644
--- a/packages/frontend/src/pages/ads.vue
+++ b/packages/frontend/src/pages/ads.vue
@@ -22,7 +22,7 @@ import { instance } from '@/instance.js';
 
 definePageMetadata(() => ({
 	title: i18n.ts.ads,
-	icon: 'ph-flag ph-bold ph-lg',
+	icon: 'ti ti-ad',
 }));
 </script>
 
diff --git a/packages/frontend/src/pages/announcement.vue b/packages/frontend/src/pages/announcement.vue
index fc4ec19b4e..802a6bf399 100644
--- a/packages/frontend/src/pages/announcement.vue
+++ b/packages/frontend/src/pages/announcement.vue
@@ -15,14 +15,14 @@ SPDX-License-Identifier: AGPL-3.0-only
 			mode="out-in"
 		>
 			<div v-if="announcement" :key="announcement.id" class="_panel" :class="$style.announcement">
-				<div v-if="announcement.forYou" :class="$style.forYou"><i class="ph-push-pin ph-bold ph-lg"></i> {{ i18n.ts.forYou }}</div>
+				<div v-if="announcement.forYou" :class="$style.forYou"><i class="ti ti-pin"></i> {{ i18n.ts.forYou }}</div>
 				<div :class="$style.header">
 					<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="ph-info ph-bold ph-lg"></i>
-						<i v-else-if="announcement.icon === 'warning'" class="ph-warning-circle ph-bold ph-lg" style="color: var(--warn);"></i>
-						<i v-else-if="announcement.icon === 'error'" class="ph-seal-warning ph-bold ph-lg" style="color: var(--error);"></i>
-						<i v-else-if="announcement.icon === 'success'" class="ph-check-circle ph-bold ph-lg" style="color: var(--success);"></i>
+						<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>
 					</span>
 					<Mfm :text="announcement.title"/>
 				</div>
@@ -37,7 +37,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 					</div>
 				</div>
 				<div v-if="$i && !announcement.silence && !announcement.isRead" :class="$style.footer">
-					<MkButton primary @click="read(announcement)"><i class="ph-check ph-bold ph-lg"></i> {{ i18n.ts.gotIt }}</MkButton>
+					<MkButton primary @click="read(announcement)"><i class="ti ti-check"></i> {{ i18n.ts.gotIt }}</MkButton>
 				</div>
 			</div>
 			<MkError v-else-if="error" @retry="fetch()"/>
@@ -104,11 +104,20 @@ const headerTabs = computed(() => []);
 
 definePageMetadata(() => ({
 	title: announcement.value ? `${i18n.ts.announcements}: ${announcement.value.title}` : i18n.ts.announcements,
-	icon: 'ph-megaphone ph-bold ph-lg',
+	icon: 'ti ti-speakerphone',
 }));
 </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 44dd2e90d8..a9bcfee803 100644
--- a/packages/frontend/src/pages/announcements.vue
+++ b/packages/frontend/src/pages/announcements.vue
@@ -12,14 +12,14 @@ SPDX-License-Identifier: AGPL-3.0-only
 				<MkInfo v-if="$i && $i.hasUnreadAnnouncement && tab === 'current'" warn>{{ i18n.ts.youHaveUnreadAnnouncements }}</MkInfo>
 				<MkPagination ref="paginationEl" :key="tab" v-slot="{items}" :pagination="tab === 'current' ? paginationCurrent : paginationPast" class="_gaps">
 					<section v-for="announcement in items" :key="announcement.id" class="_panel" :class="$style.announcement">
-						<div v-if="announcement.forYou" :class="$style.forYou"><i class="ph-push-pin ph-bold ph-lg"></i> {{ i18n.ts.forYou }}</div>
+						<div v-if="announcement.forYou" :class="$style.forYou"><i class="ti ti-pin"></i> {{ i18n.ts.forYou }}</div>
 						<div :class="$style.header">
 							<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="ph-info ph-bold ph-lg"></i>
-								<i v-else-if="announcement.icon === 'warning'" class="ph-warning ph-bold ph-lg" style="color: var(--warn);"></i>
-								<i v-else-if="announcement.icon === 'error'" class="ph-x-circle ph-bold ph-lg" style="color: var(--error);"></i>
-								<i v-else-if="announcement.icon === 'success'" class="ph-check ph-bold ph-lg" style="color: var(--success);"></i>
+								<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>
 							</span>
 							<MkA :to="`/announcements/${announcement.id}`"><span>{{ announcement.title }}</span></MkA>
 						</div>
@@ -36,7 +36,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 							</MkA>
 						</div>
 						<div v-if="tab !== 'past' && $i && !announcement.silence && !announcement.isRead" :class="$style.footer">
-							<MkButton primary @click="read(announcement)"><i class="ph-check ph-bold ph-lg"></i> {{ i18n.ts.gotIt }}</MkButton>
+							<MkButton primary @click="read(announcement)"><i class="ti ti-check"></i> {{ i18n.ts.gotIt }}</MkButton>
 						</div>
 					</section>
 				</MkPagination>
@@ -104,16 +104,16 @@ const headerActions = computed(() => []);
 const headerTabs = computed(() => [{
 	key: 'current',
 	title: i18n.ts.currentAnnouncements,
-	icon: 'ph-fire ph-bold ph-lg',
+	icon: 'ti ti-flare',
 }, {
 	key: 'past',
 	title: i18n.ts.pastAnnouncements,
-	icon: 'ph-circle ph-bold ph-lg',
+	icon: 'ti ti-point',
 }]);
 
 definePageMetadata(() => ({
 	title: i18n.ts.announcements,
-	icon: 'ph-megaphone ph-bold ph-lg',
+	icon: 'ti ti-speakerphone',
 }));
 </script>
 
diff --git a/packages/frontend/src/pages/antenna-timeline.vue b/packages/frontend/src/pages/antenna-timeline.vue
index 3e8deff711..e947ec9ba5 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
@@ -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;
@@ -80,11 +77,11 @@ watch(() => props.antennaId, async () => {
 }, { immediate: true });
 
 const headerActions = computed(() => antenna.value ? [{
-	icon: 'ph-calendar ph-bold ph-lg',
+	icon: 'ti ti-calendar-time',
 	text: i18n.ts.jumpToSpecifiedDate,
 	handler: timetravel,
 }, {
-	icon: 'ph-gear ph-bold ph-lg',
+	icon: 'ti ti-settings',
 	text: i18n.ts.settings,
 	handler: settings,
 }] : []);
@@ -93,7 +90,7 @@ const headerTabs = computed(() => []);
 
 definePageMetadata(() => ({
 	title: antenna.value ? antenna.value.name : i18n.ts.antennas,
-	icon: 'ph-flying-saucer ph-bold ph-lg',
+	icon: 'ti ti-antenna',
 }));
 </script>
 
diff --git a/packages/frontend/src/pages/api-console.vue b/packages/frontend/src/pages/api-console.vue
index 4d0cb2897f..30f12a8fb3 100644
--- a/packages/frontend/src/pages/api-console.vue
+++ b/packages/frontend/src/pages/api-console.vue
@@ -20,7 +20,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 				</MkSwitch>
 				<MkButton primary :disabled="sending" @click="send">
 					<template v-if="sending"><MkEllipsis/></template>
-					<template v-else><i class="ph-paper-plane-tilt ph-bold ph-lg"></i> Send</template>
+					<template v-else><i class="ti ti-send"></i> Send</template>
 				</MkButton>
 			</div>
 			<div v-if="res">
@@ -89,6 +89,6 @@ const headerTabs = computed(() => []);
 
 definePageMetadata(() => ({
 	title: 'API console',
-	icon: 'ph-terminal-window ph-bold ph-lg-2',
+	icon: 'ti ti-terminal-2',
 }));
 </script>
diff --git a/packages/frontend/src/pages/auth.vue b/packages/frontend/src/pages/auth.vue
index cb735a26d8..b9f45e219c 100644
--- a/packages/frontend/src/pages/auth.vue
+++ b/packages/frontend/src/pages/auth.vue
@@ -120,7 +120,7 @@ const headerTabs = computed(() => []);
 
 definePageMetadata(() => ({
 	title: i18n.ts._auth.shareAccessTitle,
-	icon: 'ph-squares-four ph-bold ph-lg',
+	icon: 'ti ti-apps',
 }));
 </script>
 
diff --git a/packages/frontend/src/pages/avatar-decorations.vue b/packages/frontend/src/pages/avatar-decorations.vue
index 41c87d3625..ad9ec3c4ee 100644
--- a/packages/frontend/src/pages/avatar-decorations.vue
+++ b/packages/frontend/src/pages/avatar-decorations.vue
@@ -23,8 +23,8 @@ SPDX-License-Identifier: AGPL-3.0-only
 						<template #label>{{ i18n.ts.imageUrl }}</template>
 					</MkInput>
 					<div class="buttons _buttons">
-						<MkButton class="button" inline primary @click="save(avatarDecoration)"><i class="ph-floppy ph-bold ph-lg"></i> {{ i18n.ts.save }}</MkButton>
-						<MkButton v-if="avatarDecoration.id != null" class="button" inline danger @click="del(avatarDecoration)"><i class="ph-trash ph-bold ph-lg"></i> {{ i18n.ts.delete }}</MkButton>
+						<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>
 			</MkFolder>
@@ -87,7 +87,7 @@ load();
 
 const headerActions = computed(() => [{
 	asFullButton: true,
-	icon: 'ph-plus ph-bold ph-lg',
+	icon: 'ti ti-plus',
 	text: i18n.ts.add,
 	handler: add,
 }]);
@@ -96,6 +96,6 @@ const headerTabs = computed(() => []);
 
 definePageMetadata(() => ({
 	title: i18n.ts.avatarDecorations,
-	icon: 'ph-sparkle ph-bold ph-lg',
+	icon: 'ti ti-sparkles',
 }));
 </script>
diff --git a/packages/frontend/src/pages/channel-editor.vue b/packages/frontend/src/pages/channel-editor.vue
index ea4374dd3c..d3f4a65b89 100644
--- a/packages/frontend/src/pages/channel-editor.vue
+++ b/packages/frontend/src/pages/channel-editor.vue
@@ -29,10 +29,10 @@ SPDX-License-Identifier: AGPL-3.0-only
 			</MkSwitch>
 
 			<div>
-				<MkButton v-if="bannerId == null" @click="setBannerImage"><i class="ph-plus ph-bold ph-lg"></i> {{ i18n.ts._channel.setBanner }}</MkButton>
+				<MkButton v-if="bannerId == null" @click="setBannerImage"><i class="ti ti-plus"></i> {{ i18n.ts._channel.setBanner }}</MkButton>
 				<div v-else-if="bannerUrl">
 					<img :src="bannerUrl" style="width: 100%;"/>
-					<MkButton @click="removeBannerImage()"><i class="ph-trash ph-bold ph-lg"></i> {{ i18n.ts._channel.removeBanner }}</MkButton>
+					<MkButton @click="removeBannerImage()"><i class="ti ti-trash"></i> {{ i18n.ts._channel.removeBanner }}</MkButton>
 				</div>
 			</div>
 
@@ -40,7 +40,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 				<template #label>{{ i18n.ts.pinnedNotes }}</template>
 
 				<div class="_gaps">
-					<MkButton primary rounded @click="addPinnedNote()"><i class="ph-plus ph-bold ph-lg"></i></MkButton>
+					<MkButton primary rounded @click="addPinnedNote()"><i class="ti ti-plus"></i></MkButton>
 
 					<Sortable
 						v-model="pinnedNotes"
@@ -50,9 +50,9 @@ SPDX-License-Identifier: AGPL-3.0-only
 					>
 						<template #item="{element,index}">
 							<div :class="$style.pinnedNote">
-								<button class="_button" :class="$style.pinnedNoteHandle"><i class="ph-list ph-bold ph-lg"></i></button>
+								<button class="_button" :class="$style.pinnedNoteHandle"><i class="ti ti-menu"></i></button>
 								{{ element.id }}
-								<button class="_button" :class="$style.pinnedNoteRemove" @click="removePinnedNote(index)"><i class="ph-x ph-bold ph-lg"></i></button>
+								<button class="_button" :class="$style.pinnedNoteRemove" @click="removePinnedNote(index)"><i class="ti ti-x"></i></button>
 							</div>
 						</template>
 					</Sortable>
@@ -60,8 +60,8 @@ SPDX-License-Identifier: AGPL-3.0-only
 			</MkFolder>
 
 			<div class="_buttons">
-				<MkButton primary @click="save()"><i class="ph-floppy-disk ph-bold ph-lg"></i> {{ channelId ? i18n.ts.save : i18n.ts.create }}</MkButton>
-				<MkButton v-if="channelId" danger @click="archive()"><i class="ph-trash ph-bold ph-lg"></i> {{ i18n.ts.archive }}</MkButton>
+				<MkButton primary @click="save()"><i class="ti ti-device-floppy"></i> {{ channelId ? i18n.ts.save : i18n.ts.create }}</MkButton>
+				<MkButton v-if="channelId" danger @click="archive()"><i class="ti ti-trash"></i> {{ i18n.ts.archive }}</MkButton>
 			</div>
 		</div>
 	</MkSpacer>
@@ -204,7 +204,7 @@ const headerTabs = computed(() => []);
 
 definePageMetadata(() => ({
 	title: props.channelId ? i18n.ts._channel.edit : i18n.ts._channel.create,
-	icon: 'ph-television ph-bold ph-lg',
+	icon: 'ti ti-device-tv',
 }));
 </script>
 
diff --git a/packages/frontend/src/pages/channel.vue b/packages/frontend/src/pages/channel.vue
index 7f49c984a8..e922599642 100644
--- a/packages/frontend/src/pages/channel.vue
+++ b/packages/frontend/src/pages/channel.vue
@@ -11,12 +11,12 @@ SPDX-License-Identifier: AGPL-3.0-only
 			<div v-if="channel && tab === 'overview'" key="overview" class="_gaps">
 				<div class="_panel" :class="$style.bannerContainer">
 					<XChannelFollowButton :channel="channel" :full="true" :class="$style.subscribe"/>
-					<MkButton v-if="favorited" v-tooltip="i18n.ts.unfavorite" asLike class="button" rounded primary :class="$style.favorite" @click="unfavorite()"><i class="ph-star ph-bold ph-lg"></i></MkButton>
-					<MkButton v-else v-tooltip="i18n.ts.favorite" asLike class="button" rounded :class="$style.favorite" @click="favorite()"><i class="ph-star ph-bold ph-lg"></i></MkButton>
+					<MkButton v-if="favorited" v-tooltip="i18n.ts.unfavorite" asLike class="button" rounded primary :class="$style.favorite" @click="unfavorite()"><i class="ti ti-star"></i></MkButton>
+					<MkButton v-else v-tooltip="i18n.ts.favorite" asLike class="button" rounded :class="$style.favorite" @click="favorite()"><i class="ti ti-star"></i></MkButton>
 					<div :style="{ backgroundImage: channel.bannerUrl ? `url(${channel.bannerUrl})` : undefined }" :class="$style.banner">
 						<div :class="$style.bannerStatus">
-							<div><i class="ph-users ph-bold ph-lg"></i><I18n :src="i18n.ts._channel.usersCount" tag="span" style="margin-left: 4px;"><template #n><b>{{ channel.usersCount }}</b></template></I18n></div>
-							<div><i class="ph-pencil-simple ph-bold ph-lg"></i><I18n :src="i18n.ts._channel.notesCount" tag="span" style="margin-left: 4px;"><template #n><b>{{ channel.notesCount }}</b></template></I18n></div>
+							<div><i class="ti ti-users ti-fw"></i><I18n :src="i18n.ts._channel.usersCount" tag="span" style="margin-left: 4px;"><template #n><b>{{ channel.usersCount }}</b></template></I18n></div>
+							<div><i class="ti ti-pencil ti-fw"></i><I18n :src="i18n.ts._channel.notesCount" tag="span" style="margin-left: 4px;"><template #n><b>{{ channel.notesCount }}</b></template></I18n></div>
 						</div>
 						<div v-if="channel.isSensitive" :class="$style.sensitiveIndicator">{{ i18n.ts.sensitive }}</div>
 						<div :class="$style.bannerFade"></div>
@@ -27,7 +27,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 				</div>
 
 				<MkFoldableSection>
-					<template #header><i class="ph-push-pin ph-bold ph-lg" style="margin-right: 0.5em;"></i>{{ i18n.ts.pinnedNotes }}</template>
+					<template #header><i class="ti ti-pin ti-fw" style="margin-right: 0.5em;"></i>{{ i18n.ts.pinnedNotes }}</template>
 					<div v-if="channel.pinnedNotes && channel.pinnedNotes.length > 0" class="_gaps">
 						<MkNote v-for="note in channel.pinnedNotes" :key="note.id" class="_panel" :note="note"/>
 					</div>
@@ -48,7 +48,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 				<div class="_gaps">
 					<div>
 						<MkInput v-model="searchQuery" @enter="search()">
-							<template #prefix><i class="ph-magnifying-glass ph-bold ph-lg"></i></template>
+							<template #prefix><i class="ti ti-search"></i></template>
 						</MkInput>
 						<MkButton primary rounded style="margin-top: 8px;" @click="search()">{{ i18n.ts.search }}</MkButton>
 					</div>
@@ -61,7 +61,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 		<div :class="$style.footer">
 			<MkSpacer :contentMax="700" :marginMin="16" :marginMax="16">
 				<div class="_buttonsCenter">
-					<MkButton inline rounded primary gradate @click="openPostForm()"><i class="ph-pencil-simple ph-bold ph-lg"></i> {{ i18n.ts.postToTheChannel }}</MkButton>
+					<MkButton inline rounded primary gradate @click="openPostForm()"><i class="ti ti-pencil"></i> {{ i18n.ts.postToTheChannel }}</MkButton>
 				</div>
 			</MkSpacer>
 		</div>
@@ -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';
 import { deepMerge } from '@/scripts/merge.js';
@@ -229,7 +229,7 @@ const headerActions = computed(() => {
 		}];
 
 		headerItems.push({
-			icon: 'ph-share-network ph-bold ph-lg',
+			icon: 'ti ti-link',
 			text: i18n.ts.copyUrl,
 			handler: async (): Promise<void> => {
 				if (!channel.value) {
@@ -243,7 +243,7 @@ const headerActions = computed(() => {
 
 		if (isSupportShare()) {
 			headerItems.push({
-				icon: 'ph-share-network ph-bold ph-lg',
+				icon: 'ti ti-share',
 				text: i18n.ts.share,
 				handler: async (): Promise<void> => {
 					if (!channel.value) {
@@ -262,7 +262,7 @@ const headerActions = computed(() => {
 
 		if (($i && $i.id === channel.value.userId) || iAmModerator) {
 			headerItems.push({
-				icon: 'ph-gear ph-bold ph-lg',
+				icon: 'ti ti-settings',
 				text: i18n.ts.edit,
 				handler: edit,
 			});
@@ -277,24 +277,24 @@ const headerActions = computed(() => {
 const headerTabs = computed(() => [{
 	key: 'overview',
 	title: i18n.ts.overview,
-	icon: 'ph-info ph-bold ph-lg',
+	icon: 'ti ti-info-circle',
 }, {
 	key: 'timeline',
 	title: i18n.ts.timeline,
-	icon: 'ph-house ph-bold ph-lg',
+	icon: 'ti ti-home',
 }, {
 	key: 'featured',
 	title: i18n.ts.featured,
-	icon: 'ph-lightning ph-bold ph-lg',
+	icon: 'ti ti-bolt',
 }, {
 	key: 'search',
 	title: i18n.ts.search,
-	icon: 'ph-magnifying-glass ph-bold ph-lg',
+	icon: 'ti ti-search',
 }]);
 
 definePageMetadata(() => ({
 	title: channel.value ? channel.value.name : i18n.ts.channel,
-	icon: 'ph-television ph-bold ph-lg',
+	icon: 'ti ti-device-tv',
 }));
 </script>
 
diff --git a/packages/frontend/src/pages/channels.vue b/packages/frontend/src/pages/channels.vue
index 253b272d2a..bde1650754 100644
--- a/packages/frontend/src/pages/channels.vue
+++ b/packages/frontend/src/pages/channels.vue
@@ -11,7 +11,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 			<div v-if="tab === 'search'" key="search">
 				<div class="_gaps">
 					<MkInput v-model="searchQuery" :large="true" :autofocus="true" type="search" @enter="search">
-						<template #prefix><i class="ph-magnifying-glass ph-bold ph-lg"></i></template>
+						<template #prefix><i class="ti ti-search"></i></template>
 					</MkInput>
 					<MkRadios v-model="searchType" @update:modelValue="search()">
 						<option value="nameAndDescription">{{ i18n.ts._channel.nameAndDescription }}</option>
@@ -41,7 +41,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 				</MkPagination>
 			</div>
 			<div v-else-if="tab === 'owned'" key="owned">
-				<MkButton class="new" @click="create()"><i class="ph-plus ph-bold ph-lg"></i></MkButton>
+				<MkButton class="new" @click="create()"><i class="ti ti-plus"></i></MkButton>
 				<MkPagination v-slot="{items}" :pagination="ownedPagination">
 					<MkChannelPreview v-for="channel in items" :key="channel.id" class="_margin" :channel="channel"/>
 				</MkPagination>
@@ -125,7 +125,7 @@ function create() {
 }
 
 const headerActions = computed(() => [{
-	icon: 'ph-plus ph-bold ph-lg',
+	icon: 'ti ti-plus',
 	text: i18n.ts.create,
 	handler: create,
 }]);
@@ -133,27 +133,27 @@ const headerActions = computed(() => [{
 const headerTabs = computed(() => [{
 	key: 'search',
 	title: i18n.ts.search,
-	icon: 'ph-magnifying-glass ph-bold ph-lg',
+	icon: 'ti ti-search',
 }, {
 	key: 'featured',
 	title: i18n.ts._channel.featured,
-	icon: 'ph-shooting-star ph-bold ph-lg',
+	icon: 'ti ti-comet',
 }, {
 	key: 'favorites',
 	title: i18n.ts.favorites,
-	icon: 'ph-star ph-bold ph-lg',
+	icon: 'ti ti-star',
 }, {
 	key: 'following',
 	title: i18n.ts._channel.following,
-	icon: 'ph-eye ph-bold ph-lg',
+	icon: 'ti ti-eye',
 }, {
 	key: 'owned',
 	title: i18n.ts._channel.owned,
-	icon: 'ph-pencil-simple-line ph-bold ph-lg',
+	icon: 'ti ti-edit',
 }]);
 
 definePageMetadata(() => ({
 	title: i18n.ts.channel,
-	icon: 'ph-television ph-bold ph-lg',
+	icon: 'ti ti-device-tv',
 }));
 </script>
diff --git a/packages/frontend/src/pages/clicker.vue b/packages/frontend/src/pages/clicker.vue
index 679fb67d25..9e9b5e8688 100644
--- a/packages/frontend/src/pages/clicker.vue
+++ b/packages/frontend/src/pages/clicker.vue
@@ -18,7 +18,7 @@ import { definePageMetadata } from '@/scripts/page-metadata.js';
 
 definePageMetadata(() => ({
 	title: '🍪👈',
-	icon: 'ph-cookie ph-bold ph-lg',
+	icon: 'ti ti-cookie',
 }));
 </script>
 
diff --git a/packages/frontend/src/pages/clip.vue b/packages/frontend/src/pages/clip.vue
index 9c42d2d3b8..506d906683 100644
--- a/packages/frontend/src/pages/clip.vue
+++ b/packages/frontend/src/pages/clip.vue
@@ -15,8 +15,8 @@ SPDX-License-Identifier: AGPL-3.0-only
 					</div>
 					<div v-else>({{ i18n.ts.noDescription }})</div>
 					<div>
-						<MkButton v-if="favorited" v-tooltip="i18n.ts.unfavorite" asLike rounded primary @click="unfavorite()"><i class="ph-heart ph-bold pd-lg"></i><span v-if="clip.favoritedCount > 0" style="margin-left: 6px;">{{ clip.favoritedCount }}</span></MkButton>
-						<MkButton v-else v-tooltip="i18n.ts.favorite" asLike rounded @click="favorite()"><i class="ph-heart ph-bold pd-lg"></i><span v-if="clip.favoritedCount > 0" style="margin-left: 6px;">{{ clip.favoritedCount }}</span></MkButton>
+						<MkButton v-if="favorited" v-tooltip="i18n.ts.unfavorite" asLike rounded primary @click="unfavorite()"><i class="ti ti-heart"></i><span v-if="clip.favoritedCount > 0" style="margin-left: 6px;">{{ clip.favoritedCount }}</span></MkButton>
+						<MkButton v-else v-tooltip="i18n.ts.favorite" asLike rounded @click="favorite()"><i class="ti ti-heart"></i><span v-if="clip.favoritedCount > 0" style="margin-left: 6px;">{{ clip.favoritedCount }}</span></MkButton>
 					</div>
 				</div>
 				<div :class="$style.user">
@@ -43,7 +43,7 @@ import { url } from '@/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';
 
 const props = defineProps<{
 	clipId: string,
@@ -94,7 +94,7 @@ async function unfavorite() {
 }
 
 const headerActions = computed(() => clip.value && isOwned.value ? [{
-	icon: 'ph-pencil-simple ph-bold ph-lg',
+	icon: 'ti ti-pencil',
 	text: i18n.ts.edit,
 	handler: async (): Promise<void> => {
 		const { canceled, result } = await os.form(clip.value.name, {
@@ -127,14 +127,14 @@ const headerActions = computed(() => clip.value && isOwned.value ? [{
 		clipsCache.delete();
 	},
 }, ...(clip.value.isPublic ? [{
-	icon: 'ph-share-network ph-bold ph-lg',
+	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: 'ph-share-network ph-bold ph-lg',
+	icon: 'ti ti-share',
 	text: i18n.ts.share,
 	handler: async (): Promise<void> => {
 		navigator.share({
@@ -144,7 +144,7 @@ const headerActions = computed(() => clip.value && isOwned.value ? [{
 		});
 	},
 }] : []), {
-	icon: 'ph-trash ph-bold ph-lg',
+	icon: 'ti ti-trash',
 	text: i18n.ts.delete,
 	danger: true,
 	handler: async (): Promise<void> => {
@@ -164,7 +164,7 @@ const headerActions = computed(() => clip.value && isOwned.value ? [{
 
 definePageMetadata(() => ({
 	title: clip.value ? clip.value.name : i18n.ts.clip,
-	icon: 'ph-paperclip ph-bold ph-lg',
+	icon: 'ti ti-paperclip',
 }));
 </script>
 
diff --git a/packages/frontend/src/pages/contact.vue b/packages/frontend/src/pages/contact.vue
index b6cfebf229..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,13 +36,13 @@ 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';
 
 definePageMetadata(() => ({
 	title: i18n.ts.inquiry,
-	icon: 'ph-question ph-bold ph-lg',
+	icon: 'ti ti-help-circle',
 }));
 </script>
diff --git a/packages/frontend/src/pages/custom-emojis-manager.vue b/packages/frontend/src/pages/custom-emojis-manager.vue
index 9357735c82..8904096875 100644
--- a/packages/frontend/src/pages/custom-emojis-manager.vue
+++ b/packages/frontend/src/pages/custom-emojis-manager.vue
@@ -11,7 +11,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 			<div class="ogwlenmc">
 				<div v-if="tab === 'local'" class="local">
 					<MkInput v-model="query" :debounce="true" type="search" autocapitalize="off">
-						<template #prefix><i class="ph-magnifying-glass ph-bold ph-lg"></i></template>
+						<template #prefix><i class="ti ti-search"></i></template>
 						<template #label>{{ i18n.ts.search }}</template>
 					</MkInput>
 					<MkSwitch v-model="selectMode" style="margin: 8px 0;">
@@ -45,7 +45,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 				<div v-else-if="tab === 'remote'" class="remote">
 					<FormSplit>
 						<MkInput v-model="queryRemote" :debounce="true" type="search" autocapitalize="off">
-							<template #prefix><i class="ph-magnifying-glass ph-bold ph-lg"></i></template>
+							<template #prefix><i class="ti ti-search"></i></template>
 							<template #label>{{ i18n.ts.search }}</template>
 						</MkInput>
 						<MkInput v-model="host" :debounce="true">
@@ -132,18 +132,19 @@ 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);
 			}
 		},
-	}, '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 => {
@@ -156,7 +157,8 @@ const edit = (emoji) => {
 				emojisPaginationComponent.value.removeItem(emoji.id);
 			}
 		},
-	}, 'closed');
+		closed: () => dispose(),
+	});
 };
 
 const importEmoji = (emoji) => {
@@ -172,7 +174,7 @@ const remoteMenu = (emoji, ev: MouseEvent) => {
 	},
 	{
 		text: i18n.ts.import,
-		icon: 'ph-plus ph-bold ph-lg',
+		icon: 'ti ti-plus',
 		action: () => { importEmoji(emoji); },
 	},
 	{
@@ -188,7 +190,7 @@ const remoteMenu = (emoji, ev: MouseEvent) => {
 
 const menu = (ev: MouseEvent) => {
 	os.popupMenu([{
-		icon: 'ph-download ph-bold ph-lg',
+		icon: 'ti ti-download',
 		text: i18n.ts.export,
 		action: async () => {
 			misskeyApi('export-custom-emojis', {
@@ -206,7 +208,7 @@ const menu = (ev: MouseEvent) => {
 				});
 		},
 	}, {
-		icon: 'ph-upload ph-bold ph-lg',
+		icon: 'ti ti-upload',
 		text: i18n.ts.import,
 		action: async () => {
 			const file = await selectFile(ev.currentTarget ?? ev.target);
@@ -302,11 +304,11 @@ const delBulk = async () => {
 
 const headerActions = computed(() => [{
 	asFullButton: true,
-	icon: 'ph-plus ph-bold ph-lg',
+	icon: 'ti ti-plus',
 	text: i18n.ts.addEmoji,
 	handler: add,
 }, {
-	icon: 'ph-dots-three ph-bold ph-lg',
+	icon: 'ti ti-dots',
 	handler: menu,
 }]);
 
diff --git a/packages/frontend/src/pages/drive.file.info.vue b/packages/frontend/src/pages/drive.file.info.vue
index 872dd4d5cf..4ed2a67678 100644
--- a/packages/frontend/src/pages/drive.file.info.vue
+++ b/packages/frontend/src/pages/drive.file.info.vue
@@ -14,34 +14,40 @@ SPDX-License-Identifier: AGPL-3.0-only
 		<div :class="$style.fileQuickActionsRoot">
 			<button class="_button" :class="$style.fileNameEditBtn" @click="rename()">
 				<h2 class="_nowrap" :class="$style.fileName">{{ file.name }}</h2>
-				<i class="ph-pencil-simple ph-bold ph-lg" :class="$style.fileNameEditIcon"></i>
+				<i class="ti ti-pencil" :class="$style.fileNameEditIcon"></i>
 			</button>
 			<div :class="$style.fileQuickActionsOthers">
 				<button v-tooltip="i18n.ts.createNoteFromTheFile" class="_button" :class="$style.fileQuickActionsOthersButton" @click="postThis()">
-					<i class="ph-pencil-simple ph-bold ph-lg"></i>
+					<i class="ti ti-pencil"></i>
 				</button>
 				<button v-if="isImage" v-tooltip="i18n.ts.cropImage" class="_button" :class="$style.fileQuickActionsOthersButton" @click="crop()">
-					<i class="ph-crop ph-bold ph-lg"></i>
+					<i class="ti ti-crop"></i>
 				</button>
 				<button v-if="file.isSensitive" v-tooltip="i18n.ts.unmarkAsSensitive" class="_button" :class="$style.fileQuickActionsOthersButton" @click="toggleSensitive()">
-					<i class="ph-eye ph-bold ph-lg"></i>
+					<i class="ti ti-eye"></i>
 				</button>
 				<button v-else v-tooltip="i18n.ts.markAsSensitive" class="_button" :class="$style.fileQuickActionsOthersButton" @click="toggleSensitive()">
-					<i class="ph-eye-slash ph-bold ph-lg"></i>
+					<i class="ti ti-eye-exclamation"></i>
 				</button>
 				<a v-tooltip="i18n.ts.download" :href="file.url" :download="file.name" class="_button" :class="$style.fileQuickActionsOthersButton">
-					<i class="ph-download ph-bold ph-lg"></i>
+					<i class="ti ti-download"></i>
 				</a>
 				<button v-tooltip="i18n.ts.delete" class="_button" :class="[$style.fileQuickActionsOthersButton, $style.danger]" @click="deleteFile()">
-					<i class="ph-trash ph-bold ph-lg"></i>
+					<i class="ti ti-trash"></i>
 				</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>
 					<template #key>{{ i18n.ts.description }}</template>
-					<template #value>{{ file.comment ? file.comment : `(${i18n.ts.none})` }}<i class="ph-pencil-simple ph-bold ph-lg" :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() {
@@ -233,6 +265,7 @@ onMounted(async () => {
 			background-color: var(--accentedBg);
 			color: var(--accent);
 			text-decoration: none;
+			outline: none;
 		}
 
 		&.danger {
@@ -280,14 +313,14 @@ onMounted(async () => {
 	padding: .5rem 1rem;
 }
 
-.fileAltEditBtn {
+.kvEditBtn {
 	text-align: start;
 	display: block;
 	width: 100%;
 	padding: .5rem 1rem;
 	border-radius: var(--radius);
 
-	.fileAltEditIcon {
+	.kvEditIcon {
 		display: inline-block;
 		color: transparent;
 		visibility: hidden;
@@ -298,7 +331,7 @@ onMounted(async () => {
 		color: var(--accent);
 		background-color: var(--accentedBg);
 
-		.fileAltEditIcon {
+		.kvEditIcon {
 			color: var(--accent);
 			visibility: visible;
 		}
diff --git a/packages/frontend/src/pages/drive.file.vue b/packages/frontend/src/pages/drive.file.vue
index 7cb2976e76..5711ec8b3a 100644
--- a/packages/frontend/src/pages/drive.file.vue
+++ b/packages/frontend/src/pages/drive.file.vue
@@ -41,15 +41,15 @@ const headerActions = computed(() => []);
 const headerTabs = computed(() => [{
 	key: 'info',
 	title: i18n.ts.info,
-	icon: 'ph-info ph-bold ph-lg',
+	icon: 'ti ti-info-circle',
 }, {
 	key: 'notes',
 	title: i18n.ts._fileViewer.attachedNotes,
-	icon: 'ph-pencil-simple ph-bold ph-lg',
+	icon: 'ti ti-pencil',
 }]);
 
 definePageMetadata(() => ({
 	title: i18n.ts._fileViewer.title,
-	icon: 'ph-file-text ph-bold ph-lg',
+	icon: 'ti ti-file',
 }));
 </script>
diff --git a/packages/frontend/src/pages/drive.vue b/packages/frontend/src/pages/drive.vue
index 7403986061..25e140f67f 100644
--- a/packages/frontend/src/pages/drive.vue
+++ b/packages/frontend/src/pages/drive.vue
@@ -24,7 +24,7 @@ const headerTabs = computed(() => []);
 
 definePageMetadata(() => ({
 	title: folder.value ? folder.value.name : i18n.ts.drive,
-	icon: 'ph-cloud ph-bold ph-lg',
+	icon: 'ti ti-cloud',
 	hideHeader: true,
 }));
 </script>
diff --git a/packages/frontend/src/pages/drop-and-fusion.game.vue b/packages/frontend/src/pages/drop-and-fusion.game.vue
index f1fa580c36..0f0b7e1ea8 100644
--- a/packages/frontend/src/pages/drop-and-fusion.game.vue
+++ b/packages/frontend/src/pages/drop-and-fusion.game.vue
@@ -105,7 +105,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 						</I18n>
 					</div>
 				</div>
-				<div v-if="replaying" :class="$style.replayIndicator"><span :class="$style.replayIndicatorText"><i class="ph-play ph-bold ph-lg"></i> {{ i18n.ts.replaying }}</span></div>
+				<div v-if="replaying" :class="$style.replayIndicator"><span :class="$style.replayIndicatorText"><i class="ti ti-player-play"></i> {{ i18n.ts.replaying }}</span></div>
 			</div>
 
 			<div v-if="replaying" class="_woodenFrame">
@@ -116,9 +116,9 @@ SPDX-License-Identifier: AGPL-3.0-only
 				</div>
 				<div class="_woodenFrameInner">
 					<div class="_buttonsCenter">
-						<MkButton @click="endReplay"><i class="ph-stop ph-bold ph-lg"></i> {{ i18n.ts.endReplay }}</MkButton>
-						<MkButton :primary="replayPlaybackRate === 4" @click="replayPlaybackRate = replayPlaybackRate === 4 ? 1 : 4"><i class="ph-skip-forward ph-bold ph-lg"></i> x4</MkButton>
-						<MkButton :primary="replayPlaybackRate === 16" @click="replayPlaybackRate = replayPlaybackRate === 16 ? 1 : 16"><i class="ph-skip-forward ph-bold ph-lg"></i> x16</MkButton>
+						<MkButton @click="endReplay"><i class="ti ti-player-stop"></i> {{ i18n.ts.endReplay }}</MkButton>
+						<MkButton :primary="replayPlaybackRate === 4" @click="replayPlaybackRate = replayPlaybackRate === 4 ? 1 : 4"><i class="ti ti-player-track-next"></i> x4</MkButton>
+						<MkButton :primary="replayPlaybackRate === 16" @click="replayPlaybackRate = replayPlaybackRate === 16 ? 1 : 16"><i class="ti ti-player-track-next"></i> x16</MkButton>
 					</div>
 				</div>
 			</div>
@@ -149,7 +149,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 				</div>
 				<div class="_woodenFrame" style="margin-left: auto;">
 					<div class="_woodenFrameInner" style="text-align: center;">
-						<div @click="showConfig = !showConfig"><i class="ph-gear ph-bold ph-lg"></i></div>
+						<div @click="showConfig = !showConfig"><i class="ti ti-settings"></i></div>
 					</div>
 				</div>
 			</div>
@@ -173,7 +173,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 					<div>
 						<div v-for="(mono, i) in game.monoDefinitions.sort((a, b) => a.level - b.level)" :key="mono.id" style="display: inline-block;">
 							<img :src="getTextureImageUrl(mono)" style="width: 32px; vertical-align: bottom;"/>
-							<div v-if="i < game.monoDefinitions.length - 1" style="display: inline-block; margin-left: 4px; vertical-align: bottom;"><i class="ph-arrow-fat-right ph-bold ph-lg"></i></div>
+							<div v-if="i < game.monoDefinitions.length - 1" style="display: inline-block; margin-left: 4px; vertical-align: bottom;"><i class="ti ti-arrow-big-right"></i></div>
 						</div>
 					</div>
 				</div>
@@ -210,7 +210,7 @@ import { apiUrl } from '@/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)!;
@@ -1220,7 +1230,7 @@ onDeactivated(() => {
 
 definePageMetadata(() => ({
 	title: i18n.ts.bubbleGame,
-	icon: 'ph-orange-slice ph-bold ph-lg',
+	icon: 'ti ti-apple',
 }));
 </script>
 
diff --git a/packages/frontend/src/pages/drop-and-fusion.vue b/packages/frontend/src/pages/drop-and-fusion.vue
index 3ece281468..54352c9b0d 100644
--- a/packages/frontend/src/pages/drop-and-fusion.vue
+++ b/packages/frontend/src/pages/drop-and-fusion.vue
@@ -35,7 +35,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 					</div>
 					<div class="_woodenFrameInner">
 						<div class="_gaps" style="padding: 16px;">
-							<div style="font-size: 90%;"><i class="ph-music-notes ph-bold ph-lg"></i> {{ i18n.ts.soundWillBePlayed }}</div>
+							<div style="font-size: 90%;"><i class="ti ti-music"></i> {{ i18n.ts.soundWillBePlayed }}</div>
 							<MkSwitch v-model="mute">
 								<template #label>{{ i18n.ts.mute }}</template>
 							</MkSwitch>
@@ -123,7 +123,7 @@ function onGameEnd() {
 
 definePageMetadata(() => ({
 	title: i18n.ts.bubbleGame,
-	icon: 'ph-game-controller ph-bold ph-lg',
+	icon: 'ti ti-device-gamepad',
 }));
 </script>
 
diff --git a/packages/frontend/src/pages/emoji-edit-dialog.vue b/packages/frontend/src/pages/emoji-edit-dialog.vue
index d03d3d9128..c5f0dde878 100644
--- a/packages/frontend/src/pages/emoji-edit-dialog.vue
+++ b/packages/frontend/src/pages/emoji-edit-dialog.vue
@@ -15,8 +15,8 @@ SPDX-License-Identifier: AGPL-3.0-only
 	<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">
@@ -54,12 +54,12 @@ SPDX-License-Identifier: AGPL-3.0-only
 					<template #suffix>{{ rolesThatCanBeUsedThisEmojiAsReaction.length === 0 ? i18n.ts.all : rolesThatCanBeUsedThisEmojiAsReaction.length }}</template>
 
 					<div class="_gaps">
-						<MkButton rounded @click="addRole"><i class="ph-plus ph-bold ph-lg"></i> {{ i18n.ts.add }}</MkButton>
+						<MkButton rounded @click="addRole"><i class="ti ti-plus"></i> {{ i18n.ts.add }}</MkButton>
 
 						<div v-for="role in rolesThatCanBeUsedThisEmojiAsReaction" :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="ph-x ph-bold ph-lg"></i></button>
-							<button v-else class="_button" :class="$style.roleUnassign" disabled><i class="ph-prohibit ph-bold ph-lg"></i></button>
+							<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>
 
 						<MkInfo>{{ i18n.ts.rolesThatCanBeUsedThisEmojiAsReactionEmptyDescription }}</MkInfo>
@@ -68,11 +68,11 @@ SPDX-License-Identifier: AGPL-3.0-only
 				</MkFolder>
 				<MkSwitch v-model="isSensitive">isSensitive</MkSwitch>
 				<MkSwitch v-model="localOnly">{{ i18n.ts.localOnly }}</MkSwitch>
-				<MkButton v-if="emoji" danger @click="del()"><i class="ph-trash ph-bold ph-lg"></i> {{ i18n.ts.delete }}</MkButton>
+				<MkButton v-if="emoji" 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="ph-check ph-bold ph-lg"></i> {{ props.emoji ? i18n.ts.update : i18n.ts.create }}</MkButton>
+			<MkButton primary rounded style="margin: 0 auto;" @click="done"><i class="ti ti-check"></i> {{ props.emoji ? i18n.ts.update : i18n.ts.create }}</MkButton>
 		</div>
 	</div>
 </MkWindow>
@@ -239,10 +239,12 @@ async function del() {
 
 .footer {
 	position: sticky;
+	z-index: 10000;
 	bottom: 0;
 	left: 0;
 	padding: 12px;
 	border-top: solid 0.5px var(--divider);
+	background: var(--acrylicBg);
 	-webkit-backdrop-filter: var(--blur, blur(15px));
 	backdrop-filter: var(--blur, blur(15px));
 }
diff --git a/packages/frontend/src/pages/emojis.emoji.vue b/packages/frontend/src/pages/emojis.emoji.vue
index c9805af51b..03a3b8f1c0 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';
 
@@ -31,21 +31,21 @@ function menu(ev) {
 		text: ':' + props.emoji.name + ':',
 	}, {
 		text: i18n.ts.copy,
-		icon: 'ph-copy ph-bold ph-lg',
+		icon: 'ti ti-copy',
 		action: () => {
 			copyToClipboard(`:${props.emoji.name}:`);
 			os.success();
 		},
 	}, {
 		text: i18n.ts.info,
-		icon: 'ph-info ph-bold ph-lg',
+		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);
diff --git a/packages/frontend/src/pages/explore.users.vue b/packages/frontend/src/pages/explore.users.vue
index c9ab5443b6..805d826946 100644
--- a/packages/frontend/src/pages/explore.users.vue
+++ b/packages/frontend/src/pages/explore.users.vue
@@ -12,11 +12,11 @@ SPDX-License-Identifier: AGPL-3.0-only
 	<div v-if="origin === 'local'">
 		<template v-if="tag == null">
 			<MkFoldableSection class="_margin" persistKey="explore-pinned-users">
-				<template #header><i class="ph-bookmark ph-bold ph-lg ti-fw" style="margin-right: 0.5em;"></i>{{ i18n.ts.pinnedUsers }}</template>
+				<template #header><i class="ti ti-bookmark ti-fw" style="margin-right: 0.5em;"></i>{{ i18n.ts.pinnedUsers }}</template>
 				<MkUserList :pagination="pinnedUsers"/>
 			</MkFoldableSection>
 			<MkFoldableSection class="_margin" persistKey="explore-popular-users">
-				<template #header><i class="ph-chart-line ph-bold ph-lg ti-fw" style="margin-right: 0.5em;"></i>{{ i18n.ts.popularUsers }}</template>
+				<template #header><i class="ti ti-chart-line ti-fw" style="margin-right: 0.5em;"></i>{{ i18n.ts.popularUsers }}</template>
 				<MkUserList :pagination="popularUsers"/>
 			</MkFoldableSection>
 			<MkFoldableSection class="_margin" persistKey="explore-recently-updated-users">
@@ -24,14 +24,14 @@ SPDX-License-Identifier: AGPL-3.0-only
 				<MkUserList :pagination="recentlyUpdatedUsers"/>
 			</MkFoldableSection>
 			<MkFoldableSection class="_margin" persistKey="explore-recently-registered-users">
-				<template #header><i class="ph-plus ph-bold ph-lg ti-fw" style="margin-right: 0.5em;"></i>{{ i18n.ts.recentlyRegisteredUsers }}</template>
+				<template #header><i class="ti ti-plus ti-fw" style="margin-right: 0.5em;"></i>{{ i18n.ts.recentlyRegisteredUsers }}</template>
 				<MkUserList :pagination="recentlyRegisteredUsers"/>
 			</MkFoldableSection>
 		</template>
 	</div>
 	<div v-else>
 		<MkFoldableSection ref="tagsEl" :foldable="true" :expanded="false" class="_margin">
-			<template #header><i class="ph-hash ph-bold ph-lg ti-fw" style="margin-right: 0.5em;"></i>{{ i18n.ts.popularTags }}</template>
+			<template #header><i class="ti ti-hash ti-fw" style="margin-right: 0.5em;"></i>{{ i18n.ts.popularTags }}</template>
 
 			<div>
 				<MkA v-for="tag in tagsLocal" :key="'local:' + tag.tag" :to="`/user-tags/${tag.tag}`" style="margin-right: 16px; font-weight: bold;">{{ tag.tag }}</MkA>
@@ -40,13 +40,13 @@ SPDX-License-Identifier: AGPL-3.0-only
 		</MkFoldableSection>
 
 		<MkFoldableSection v-if="tag != null" :key="`${tag}`" class="_margin">
-			<template #header><i class="ph-hash ph-bold ph-lg ti-fw" style="margin-right: 0.5em;"></i>{{ tag }}</template>
+			<template #header><i class="ti ti-hash ti-fw" style="margin-right: 0.5em;"></i>{{ tag }}</template>
 			<MkUserList :pagination="tagUsers"/>
 		</MkFoldableSection>
 
 		<template v-if="tag == null">
 			<MkFoldableSection class="_margin">
-				<template #header><i class="ph-chart-line ph-bold ph-lg ti-fw" style="margin-right: 0.5em;"></i>{{ i18n.ts.popularUsers }}</template>
+				<template #header><i class="ti ti-chart-line ti-fw" style="margin-right: 0.5em;"></i>{{ i18n.ts.popularUsers }}</template>
 				<MkUserList :pagination="popularUsersF"/>
 			</MkFoldableSection>
 			<MkFoldableSection class="_margin">
@@ -54,7 +54,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 				<MkUserList :pagination="recentlyUpdatedUsersF"/>
 			</MkFoldableSection>
 			<MkFoldableSection class="_margin">
-				<template #header><i class="ph-rocket-launch ph-bold ph-lg ti-fw" style="margin-right: 0.5em;"></i>{{ i18n.ts.recentlyDiscoveredUsers }}</template>
+				<template #header><i class="ti ti-rocket ti-fw" style="margin-right: 0.5em;"></i>{{ i18n.ts.recentlyDiscoveredUsers }}</template>
 				<MkUserList :pagination="recentlyRegisteredUsersF"/>
 			</MkFoldableSection>
 		</template>
diff --git a/packages/frontend/src/pages/explore.vue b/packages/frontend/src/pages/explore.vue
index c599a290f7..b1a8183d9b 100644
--- a/packages/frontend/src/pages/explore.vue
+++ b/packages/frontend/src/pages/explore.vue
@@ -48,20 +48,20 @@ const headerActions = computed(() => []);
 
 const headerTabs = computed(() => [{
 	key: 'featured',
-	icon: 'ph-lightning ph-bold ph-lg',
+	icon: 'ti ti-bolt',
 	title: i18n.ts.featured,
 }, {
 	key: 'users',
-	icon: 'ph-users ph-bold ph-lg',
+	icon: 'ti ti-users',
 	title: i18n.ts.users,
 }, {
 	key: 'roles',
-	icon: 'ph-seal-check ph-bold ph-lg',
+	icon: 'ti ti-badges',
 	title: i18n.ts.roles,
 }]);
 
 definePageMetadata(() => ({
 	title: i18n.ts.explore,
-	icon: 'ph-hash ph-bold ph-lg',
+	icon: 'ti ti-hash',
 }));
 </script>
diff --git a/packages/frontend/src/pages/favorites.vue b/packages/frontend/src/pages/favorites.vue
index 5835f0c093..cb1feef016 100644
--- a/packages/frontend/src/pages/favorites.vue
+++ b/packages/frontend/src/pages/favorites.vue
@@ -16,14 +16,9 @@ SPDX-License-Identifier: AGPL-3.0-only
 			</template>
 
 			<template #default="{ items }">
-				<MkDateSeparatedList v-if="defaultStore.state.noteDesign === 'misskey'"
-					v-slot="{ item }" :items="items" :direction="'down'" :noGap="false" :ad="false">
+				<MkDateSeparatedList v-slot="{ item }" :items="items" :direction="'down'" :noGap="false" :ad="false">
 					<MkNote :key="item.id" :note="item.note" :class="$style.note"/>
 				</MkDateSeparatedList>
-				<MkDateSeparatedList v-if="defaultStore.state.noteDesign === 'sharkey'"
-					v-slot="{ item }" :items="items" :direction="'down'" :noGap="false" :ad="false">
-					<SkNote :key="item.id" :note="item.note" :class="$style.note"/>
-				</MkDateSeparatedList>
 			</template>
 		</MkPagination>
 	</MkSpacer>
@@ -32,14 +27,19 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <script lang="ts" setup>
 import MkPagination from '@/components/MkPagination.vue';
-import MkNote from '@/components/MkNote.vue';
-import SkNote from '@/components/SkNote.vue';
 import MkDateSeparatedList from '@/components/MkDateSeparatedList.vue';
+import { defineAsyncComponent } from 'vue';
 import { i18n } from '@/i18n.js';
 import { definePageMetadata } from '@/scripts/page-metadata.js';
 import { infoImageUrl } from '@/instance.js';
 import { defaultStore } from '@/store.js';
 
+const MkNote = defineAsyncComponent(() =>
+	(defaultStore.state.noteDesign === 'misskey') ? import('@/components/MkNote.vue') :
+	(defaultStore.state.noteDesign === 'sharkey') ? import('@/components/SkNote.vue') :
+	null
+);
+
 const pagination = {
 	endpoint: 'i/favorites' as const,
 	limit: 10,
@@ -47,7 +47,7 @@ const pagination = {
 
 definePageMetadata(() => ({
 	title: i18n.ts.favorites,
-	icon: 'ph-star ph-bold ph-lg',
+	icon: 'ti ti-star',
 }));
 </script>
 
diff --git a/packages/frontend/src/pages/flash/flash-edit.vue b/packages/frontend/src/pages/flash/flash-edit.vue
index 1a832dfdec..d282ed4810 100644
--- a/packages/frontend/src/pages/flash/flash-edit.vue
+++ b/packages/frontend/src/pages/flash/flash-edit.vue
@@ -14,7 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 			<MkTextarea v-model="summary" :mfmAutocomplete="true" :mfmPreview="true">
 				<template #label>{{ i18n.ts._play.summary }}</template>
 			</MkTextarea>
-			<MkButton primary @click="selectPreset">{{ i18n.ts.selectFromPresets }}<i class="ph-caret-down ph-bold ph-lg"></i></MkButton>
+			<MkButton primary @click="selectPreset">{{ i18n.ts.selectFromPresets }}<i class="ti ti-chevron-down"></i></MkButton>
 			<MkCodeEditor v-model="script" lang="is">
 				<template #label>{{ i18n.ts._play.script }}</template>
 			</MkCodeEditor>
@@ -25,9 +25,9 @@ SPDX-License-Identifier: AGPL-3.0-only
 				<option :key="'private'" :value="'private'">{{ i18n.ts.private }}</option>
 			</MkSelect>
 			<div class="_buttons">
-				<MkButton primary @click="save"><i class="ph-check ph-bold ph-lg"></i> {{ i18n.ts.save }}</MkButton>
-				<MkButton @click="show"><i class="ph-eye ph-bold ph-lg"></i> {{ i18n.ts.show }}</MkButton>
-				<MkButton v-if="flash" danger @click="del"><i class="ph-trash ph-bold ph-lg"></i> {{ i18n.ts.delete }}</MkButton>
+				<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>
@@ -37,6 +37,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 <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 +49,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 +67,7 @@ Ui:render([
 ])
 `;
 
-const PRESET_OMIKUJI = `/// @ 0.18.0
+const PRESET_OMIKUJI = `/// @ ${AISCRIPT_VERSION}
 // ユーザーごとに日替わりのおみくじのプリセット
 
 // 選択肢
@@ -109,7 +110,7 @@ Ui:render([
 ])
 `;
 
-const PRESET_SHUFFLE = `/// @ 0.18.0
+const PRESET_SHUFFLE = `/// @ ${AISCRIPT_VERSION}
 // 巻き戻し可能な文字シャッフルのプリセット
 
 let string = "ペペロンチーノ"
@@ -188,7 +189,7 @@ var cursor = 0
 do()
 `;
 
-const PRESET_QUIZ = `/// @ 0.18.0
+const PRESET_QUIZ = `/// @ ${AISCRIPT_VERSION}
 let title = '地理クイズ'
 
 let qas = [{
@@ -301,7 +302,7 @@ qaEls.push(Ui:C:container({
 Ui:render(qaEls)
 `;
 
-const PRESET_TIMELINE = `/// @ 0.18.0
+const PRESET_TIMELINE = `/// @ ${AISCRIPT_VERSION}
 // APIリクエストを行いローカルタイムラインを表示するプリセット
 
 @fetch() {
@@ -368,7 +369,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 +379,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) {
diff --git a/packages/frontend/src/pages/flash/flash-index.vue b/packages/frontend/src/pages/flash/flash-index.vue
index 7e56d3f51b..f63a799365 100644
--- a/packages/frontend/src/pages/flash/flash-index.vue
+++ b/packages/frontend/src/pages/flash/flash-index.vue
@@ -18,7 +18,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 			<div v-else-if="tab === 'my'" key="my">
 				<div class="_gaps">
-					<MkButton gradate rounded style="margin: 0 auto;" @click="create()"><i class="ph-plus ph-bold ph-lg"></i></MkButton>
+					<MkButton gradate rounded style="margin: 0 auto;" @click="create()"><i class="ti ti-plus"></i></MkButton>
 					<MkPagination v-slot="{items}" :pagination="myFlashsPagination">
 						<div class="_gaps_s">
 							<MkFlashPreview v-for="flash in items" :key="flash.id" :flash="flash"/>
@@ -71,7 +71,7 @@ function create() {
 }
 
 const headerActions = computed(() => [{
-	icon: 'ph-plus ph-bold ph-lg',
+	icon: 'ti ti-plus',
 	text: i18n.ts.create,
 	handler: create,
 }]);
@@ -79,19 +79,19 @@ const headerActions = computed(() => [{
 const headerTabs = computed(() => [{
 	key: 'featured',
 	title: i18n.ts._play.featured,
-	icon: 'ph-fire ph-bold ph-lg',
+	icon: 'ti ti-flare',
 }, {
 	key: 'my',
 	title: i18n.ts._play.my,
-	icon: 'ph-pencil-simple-line ph-bold ph-lg',
+	icon: 'ti ti-edit',
 }, {
 	key: 'liked',
 	title: i18n.ts._play.liked,
-	icon: 'ph-heart ph-bold ph-lg',
+	icon: 'ti ti-heart',
 }]);
 
 definePageMetadata(() => ({
 	title: 'Play',
-	icon: 'ph-play ph-bold ph-lg',
+	icon: 'ti ti-player-play',
 }));
 </script>
diff --git a/packages/frontend/src/pages/flash/flash.vue b/packages/frontend/src/pages/flash/flash.vue
index febfdbaaf7..1b277c936a 100644
--- a/packages/frontend/src/pages/flash/flash.vue
+++ b/packages/frontend/src/pages/flash/flash.vue
@@ -16,13 +16,14 @@ SPDX-License-Identifier: AGPL-3.0-only
 						</div>
 						<div class="actions _panel">
 							<div class="items">
-								<MkButton v-tooltip="i18n.ts.reload" class="button" rounded @click="reset"><i class="ph-arrows-clockwise ph-bold ph-lg"></i></MkButton>
+								<MkButton v-tooltip="i18n.ts.reload" class="button" rounded @click="reset"><i class="ti ti-reload"></i></MkButton>
 							</div>
 							<div class="items">
-								<MkButton v-if="flash.isLiked" v-tooltip="i18n.ts.unlike" asLike class="button" rounded primary @click="unlike()"><i class="ph-heart ph-bold ph-lg"></i><span v-if="flash?.likedCount && flash.likedCount > 0" style="margin-left: 6px;">{{ flash.likedCount }}</span></MkButton>
-								<MkButton v-else v-tooltip="i18n.ts.like" asLike class="button" rounded @click="like()"><i class="ph-heart ph-bold ph-lg"></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="ph-link ph-bold ph-lg ti-fw"></i></MkButton>
-								<MkButton v-tooltip="i18n.ts.share" class="button" rounded @click="share"><i class="ph-share-network ph-bold ph-lg ti-fw"></i></MkButton>
+								<MkButton v-if="flash.isLiked" v-tooltip="i18n.ts.unlike" asLike class="button" rounded primary @click="unlike()"><i class="ti ti-heart"></i><span v-if="flash?.likedCount && flash.likedCount > 0" style="margin-left: 6px;">{{ flash.likedCount }}</span></MkButton>
+								<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>
@@ -32,13 +33,13 @@ SPDX-License-Identifier: AGPL-3.0-only
 							<div class="summary"><Mfm :text="flash.summary" :isBlock="true"/></div>
 							<MkButton class="start" gradate rounded large @click="start">Play</MkButton>
 							<div class="info">
-								<span v-tooltip="i18n.ts.numberOfLikes"><i class="ph-heart ph-bold ph-lg"></i> {{ flash.likedCount }}</span>
+								<span v-tooltip="i18n.ts.numberOfLikes"><i class="ti ti-heart"></i> {{ flash.likedCount }}</span>
 							</div>
 						</div>
 					</div>
 				</Transition>
 				<MkFolder :defaultOpen="false" :max-height="280" class="_margin">
-					<template #icon><i class="ph-code ph-bold ph-lg"></i></template>
+					<template #icon><i class="ti ti-code"></i></template>
 					<template #label>{{ i18n.ts._play.viewSource }}</template>
 
 					<MkCode :code="flash.script" lang="is" class="_monospace"/>
@@ -46,8 +47,8 @@ SPDX-License-Identifier: AGPL-3.0-only
 				<div :class="$style.footer">
 					<Mfm :text="`By @${flash.user.username}`"/>
 					<div class="date">
-						<div v-if="flash.createdAt != flash.updatedAt"><i class="ph-clock ph-bold ph-lg"></i> {{ i18n.ts.updatedAt }}: <MkTime :time="flash.updatedAt" mode="detail"/></div>
-						<div><i class="ph-clock ph-bold ph-lg"></i> {{ i18n.ts.createdAt }}: <MkTime :time="flash.createdAt" mode="detail"/></div>
+						<div v-if="flash.createdAt != flash.updatedAt"><i class="ti ti-clock"></i> {{ i18n.ts.updatedAt }}: <MkTime :time="flash.updatedAt" mode="detail"/></div>
+						<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>
@@ -61,7 +62,7 @@ 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';
@@ -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 { MenuItem } from '@/types/menu';
+import { pleaseLogin } from '@/scripts/please-login.js';
 
 const props = defineProps<{
 	id: string;
@@ -109,11 +112,11 @@ function share(ev: MouseEvent) {
 		},
 		...(isSupportShare() ? [{
 			text: i18n.ts.share,
-			icon: 'ph-share-network ph-bold ph-lg ti-fw',
+			icon: 'ti ti-share',
 			action: shareWithNavigator,
 		}] : []),
 	], ev.currentTarget ?? ev.target);
-	}
+}
 
 function copyLink() {
 	if (!flash.value) return;
@@ -143,6 +146,7 @@ function shareWithNote() {
 
 function like() {
 	if (!flash.value) return;
+	pleaseLogin();
 
 	os.apiWithDialog('flash/like', {
 		flashId: flash.value.id,
@@ -154,6 +158,7 @@ function like() {
 
 async function unlike() {
 	if (!flash.value) return;
+	pleaseLogin();
 
 	const confirm = await os.confirm({
 		type: 'warning',
@@ -226,6 +231,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;
diff --git a/packages/frontend/src/pages/follow-requests.vue b/packages/frontend/src/pages/follow-requests.vue
index 4cdfe28916..d50887b2e9 100644
--- a/packages/frontend/src/pages/follow-requests.vue
+++ b/packages/frontend/src/pages/follow-requests.vue
@@ -24,8 +24,8 @@ SPDX-License-Identifier: AGPL-3.0-only
 								<p class="acct">@{{ acct(req.follower) }}</p>
 							</div>
 							<div class="commands">
-								<MkButton class="command" rounded primary @click="accept(req.follower)"><i class="ph-check ph-bold ph-lg"/> {{ i18n.ts.accept }}</MkButton>
-								<MkButton class="command" rounded danger @click="reject(req.follower)"><i class="ph-x ph-bold ph-lg"/> {{ i18n.ts.reject }}</MkButton>
+								<MkButton class="command" rounded primary @click="accept(req.follower)"><i class="ti ti-check"/> {{ i18n.ts.accept }}</MkButton>
+								<MkButton class="command" rounded danger @click="reject(req.follower)"><i class="ti ti-x"/> {{ i18n.ts.reject }}</MkButton>
 							</div>
 						</div>
 					</div>
@@ -71,7 +71,7 @@ const headerTabs = computed(() => []);
 
 definePageMetadata(() => ({
 	title: i18n.ts.followRequests,
-	icon: 'ph-user-plus ph-bold ph-lg',
+	icon: 'ti ti-user-plus',
 }));
 </script>
 
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 d2fe271b0f..a68a7e5c41 100644
--- a/packages/frontend/src/pages/gallery/edit.vue
+++ b/packages/frontend/src/pages/gallery/edit.vue
@@ -19,18 +19,18 @@ SPDX-License-Identifier: AGPL-3.0-only
 			<div class="_gaps_s">
 				<div v-for="file in files" :key="file.id" class="wqugxsfx" :style="{ backgroundImage: file ? `url(${ file.thumbnailUrl })` : null }">
 					<div class="name">{{ file.name }}</div>
-					<button v-tooltip="i18n.ts.remove" class="remove _button" @click="remove(file)"><i class="ph-x ph-bold ph-lg"></i></button>
+					<button v-tooltip="i18n.ts.remove" class="remove _button" @click="remove(file)"><i class="ti ti-x"></i></button>
 				</div>
-				<MkButton primary @click="selectFile"><i class="ph-plus ph-bold ph-lg"></i> {{ i18n.ts.attachFile }}</MkButton>
+				<MkButton primary @click="selectFile"><i class="ti ti-plus"></i> {{ i18n.ts.attachFile }}</MkButton>
 			</div>
 
 			<MkSwitch v-model="isSensitive">{{ i18n.ts.markAsSensitive }}</MkSwitch>
 
 			<div class="_buttons">
-				<MkButton v-if="postId" primary @click="save"><i class="ph-floppy-disk ph-bold ph-lg"></i> {{ i18n.ts.save }}</MkButton>
-				<MkButton v-else primary @click="save"><i class="ph-floppy-disk ph-bold ph-lg"></i> {{ i18n.ts.publish }}</MkButton>
+				<MkButton v-if="postId" primary @click="save"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton>
+				<MkButton v-else primary @click="save"><i class="ti ti-device-floppy"></i> {{ i18n.ts.publish }}</MkButton>
 
-				<MkButton v-if="postId" danger @click="del"><i class="ph-trash ph-bold ph-lg"></i> {{ i18n.ts.delete }}</MkButton>
+				<MkButton v-if="postId" danger @click="del"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton>
 			</div>
 		</FormSuspense>
 	</MkSpacer>
@@ -124,7 +124,7 @@ const headerTabs = computed(() => []);
 
 definePageMetadata(() => ({
 	title: props.postId ? i18n.ts.edit : i18n.ts.postToGallery,
-	icon: 'ph-pencil-simple ph-bold ph-lg',
+	icon: 'ti ti-pencil',
 }));
 </script>
 
diff --git a/packages/frontend/src/pages/gallery/index.vue b/packages/frontend/src/pages/gallery/index.vue
index 96979250bd..e0b137ed28 100644
--- a/packages/frontend/src/pages/gallery/index.vue
+++ b/packages/frontend/src/pages/gallery/index.vue
@@ -10,7 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 		<MkHorizontalSwipe v-model:tab="tab" :tabs="headerTabs">
 			<div v-if="tab === 'explore'" key="explore">
 				<MkFoldableSection class="_margin">
-					<template #header><i class="ph-clock ph-bold ph-lg"></i>{{ i18n.ts.recentPosts }}</template>
+					<template #header><i class="ti ti-clock"></i>{{ i18n.ts.recentPosts }}</template>
 					<MkPagination v-slot="{items}" :pagination="recentPostsPagination" :disableAutoLoad="true">
 						<div :class="$style.items">
 							<MkGalleryPostPreview v-for="post in items" :key="post.id" :post="post" class="post"/>
@@ -18,7 +18,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 					</MkPagination>
 				</MkFoldableSection>
 				<MkFoldableSection class="_margin">
-					<template #header><i class="ph-shooting-star ph-bold ph-lg"></i>{{ i18n.ts.popularPosts }}</template>
+					<template #header><i class="ti ti-comet"></i>{{ i18n.ts.popularPosts }}</template>
 					<MkPagination v-slot="{items}" :pagination="popularPostsPagination" :disableAutoLoad="true">
 						<div :class="$style.items">
 							<MkGalleryPostPreview v-for="post in items" :key="post.id" :post="post" class="post"/>
@@ -34,7 +34,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 				</MkPagination>
 			</div>
 			<div v-else-if="tab === 'my'" key="my">
-				<MkA to="/gallery/new" class="_link" style="margin: 16px;"><i class="ph-plus ph-bold ph-lg"></i> {{ i18n.ts.postToGallery }}</MkA>
+				<MkA to="/gallery/new" class="_link" style="margin: 16px;"><i class="ti ti-plus"></i> {{ i18n.ts.postToGallery }}</MkA>
 				<MkPagination v-slot="{items}" :pagination="myPostsPagination">
 					<div :class="$style.items">
 						<MkGalleryPostPreview v-for="post in items" :key="post.id" :post="post" class="post"/>
@@ -98,7 +98,7 @@ watch(() => props.tag, () => {
 });
 
 const headerActions = computed(() => [{
-	icon: 'ph-plus ph-bold ph-lg',
+	icon: 'ti ti-plus',
 	text: i18n.ts.create,
 	handler: () => {
 		router.push('/gallery/new');
@@ -112,11 +112,11 @@ const headerTabs = computed(() => [{
 }, {
 	key: 'liked',
 	title: i18n.ts._gallery.liked,
-	icon: 'ph-heart ph-bold ph-lg',
+	icon: 'ti ti-heart',
 }, {
 	key: 'my',
 	title: i18n.ts._gallery.my,
-	icon: 'ph-pencil-simple-line ph-bold ph-lg',
+	icon: 'ti ti-edit',
 }]);
 
 definePageMetadata(() => ({
diff --git a/packages/frontend/src/pages/gallery/post.vue b/packages/frontend/src/pages/gallery/post.vue
index 99bd29f802..913758ba7e 100644
--- a/packages/frontend/src/pages/gallery/post.vue
+++ b/packages/frontend/src/pages/gallery/post.vue
@@ -19,18 +19,19 @@ SPDX-License-Identifier: AGPL-3.0-only
 						<div class="title">{{ post.title }}</div>
 						<div class="description"><Mfm :text="post.description" :isBlock="true"/></div>
 						<div class="info">
-							<i class="ph-clock ph-bold ph-lg"></i> <MkTime :time="post.createdAt" mode="detail"/>
+							<i class="ti ti-clock"></i> <MkTime :time="post.createdAt" mode="detail"/>
 						</div>
 						<div class="actions">
 							<div class="like">
-								<MkButton v-if="post.isLiked" v-tooltip="i18n.ts._gallery.unlike" class="button" primary @click="unlike()"><i class="ph-heart-break ph-bold ph-lg"></i><span v-if="post.likedCount > 0" class="count">{{ post.likedCount }}</span></MkButton>
-								<MkButton v-else v-tooltip="i18n.ts._gallery.like" class="button" @click="like()"><i class="ph-heart ph-bold ph-lg"></i><span v-if="post.likedCount > 0" class="count">{{ post.likedCount }}</span></MkButton>
+								<MkButton v-if="post.isLiked" v-tooltip="i18n.ts._gallery.unlike" class="button" primary @click="unlike()"><i class="ti ti-heart-off"></i><span v-if="post.likedCount > 0" class="count">{{ post.likedCount }}</span></MkButton>
+								<MkButton v-else v-tooltip="i18n.ts._gallery.like" class="button" @click="like()"><i class="ti ti-heart"></i><span v-if="post.likedCount > 0" class="count">{{ post.likedCount }}</span></MkButton>
 							</div>
 							<div class="other">
-								<button v-if="$i && $i.id === post.user.id" v-tooltip="i18n.ts.edit" v-click-anime class="_button" @click="edit"><i class="ph-pencil-simple ph-bold ph-lg ti-fw"></i></button>
-								<button v-tooltip="i18n.ts.shareWithNote" v-click-anime class="_button" @click="shareWithNote"><i class="ph-repeat ph-bold ph-lg ti-fw"></i></button>
-								<button v-tooltip="i18n.ts.copyLink" v-click-anime class="_button" @click="copyLink"><i class="ph-share-network ph-bold ph-lg ti-fw"></i></button>
-								<button v-if="isSupportShare()" v-tooltip="i18n.ts.share" v-click-anime class="_button" @click="share"><i class="ph-share-network ph-bold ph-lg ti-fw"></i></button>
+								<button v-if="$i && $i.id === post.user.id" v-tooltip="i18n.ts.edit" v-click-anime class="_button" @click="edit"><i class="ti ti-pencil ti-fw"></i></button>
+								<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">
@@ -44,7 +45,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 					</div>
 					<MkAd :prefer="['horizontal', 'horizontal-big']"/>
 					<MkContainer :max-height="300" :foldable="true" class="other">
-						<template #icon><i class="ph-clock ph-bold ph-lg"></i></template>
+						<template #icon><i class="ti ti-clock"></i></template>
 						<template #header>{{ i18n.ts.recentPosts }}</template>
 						<MkPagination v-slot="{items}" :pagination="otherPostsPagination">
 							<div class="sdrarzaf">
@@ -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';
@@ -77,8 +78,9 @@ 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 { MenuItem } from '@/types/menu';
 
 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 menu: MenuItem[] = [
+		...($i && $i.id !== post.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 || !post.value) return;
+
+						os.apiWithDialog('gallery/posts/delete', { postId: post.value.id });
+					}),
+				},
+			] : []),
+		] : []),
+	];
+
+	os.popupMenu(menu, ev.currentTarget ?? ev.target);
+}
+
 watch(() => props.postId, fetchPost, { immediate: true });
 
-const headerActions = computed(() => [{
-	icon: 'ph-pencil-simple ph-bold ph-lg',
-	text: i18n.ts.edit,
-	handler: edit,
-}]);
+const headerActions = computed(() => []);
 
 const headerTabs = computed(() => []);
 
diff --git a/packages/frontend/src/pages/games.vue b/packages/frontend/src/pages/games.vue
index 2822fbde89..b52f4decaa 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>
@@ -29,6 +29,13 @@ import { definePageMetadata } from '@/scripts/page-metadata.js';
 
 definePageMetadata(() => ({
 	title: 'Misskey Games',
-	icon: 'ph-game-controller ph-bold ph-lg',
+	icon: 'ti ti-device-gamepad',
 }));
 </script>
+
+<style module>
+.link:focus-within {
+	outline: 2px solid var(--focus);
+	outline-offset: -2px;
+}
+</style>
diff --git a/packages/frontend/src/pages/install-extensions.vue b/packages/frontend/src/pages/install-extensions.vue
index 32f6fbd185..4bee437f65 100644
--- a/packages/frontend/src/pages/install-extensions.vue
+++ b/packages/frontend/src/pages/install-extensions.vue
@@ -10,9 +10,9 @@ SPDX-License-Identifier: AGPL-3.0-only
 		<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="ph-plug ph-bold ph-lg"></i>
-				<i v-else-if="data.type === 'theme'" class="ph-palette ph-bold ph-lg"></i>
-				<i v-else class="ph-download ph-bold ph-lg"></i>
+				<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>
@@ -51,7 +51,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 						<template #value>{{ i18n.ts[data.meta.base] }}</template>
 					</MkKeyValue>
 					<MkFolder>
-						<template #icon><i class="ph-code ph-bold ph-lg"></i></template>
+						<template #icon><i class="ti ti-code"></i></template>
 						<template #label>{{ i18n.ts._plugin.viewSource }}</template>
 
 						<MkCode :code="data.raw ?? ''"/>
@@ -69,18 +69,18 @@ SPDX-License-Identifier: AGPL-3.0-only
 						<template #key>{{ i18n.ts._externalResourceInstaller._vendorInfo.hashVerify }}</template>
 						<template #value>
 							<!--この画面が出ている時点でハッシュの検証には成功している-->
-							<i class="ph-check ph-bold ph-lg" style="color: var(--accent)"></i>
+							<i class="ti ti-check" style="color: var(--accent)"></i>
 						</template>
 					</MkKeyValue>
 				</div>
 			</FormSection>
 			<div class="_buttonsCenter">
-				<MkButton primary @click="install()"><i class="ph-check ph-bold ph-lg"></i> {{ i18n.ts.install }}</MkButton>
+				<MkButton primary @click="install()"><i class="ti ti-check"></i> {{ i18n.ts.install }}</MkButton>
 			</div>
 		</div>
 		<div v-else-if="uiPhase === 'error'" class="_gaps_m" :class="[$style.extInstallerRoot, $style.error]">
 			<div :class="$style.extInstallerIconWrapper">
-				<i class="ph-x-circle ph-bold ph-lg"></i>
+				<i class="ti ti-circle-x"></i>
 			</div>
 			<h2 :class="$style.extInstallerTitle">{{ errorKV?.title }}</h2>
 			<div :class="$style.extInstallerNormDesc">{{ errorKV?.description }}</div>
@@ -314,7 +314,7 @@ const headerTabs = computed(() => []);
 
 definePageMetadata(() => ({
 	title: i18n.ts._externalResourceInstaller.title,
-	icon: 'ph-download ph-bold ph-lg',
+	icon: 'ti ti-download',
 }));
 </script>
 
diff --git a/packages/frontend/src/pages/instance-info.vue b/packages/frontend/src/pages/instance-info.vue
index 0cc4c1a48a..4ff26197d8 100644
--- a/packages/frontend/src/pages/instance-info.vue
+++ b/packages/frontend/src/pages/instance-info.vue
@@ -48,7 +48,8 @@ SPDX-License-Identifier: AGPL-3.0-only
 						<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="isNSFW" :disabled="!instance" @update:modelValue="toggleNSFW">Mark as NSFW</MkSwitch>
-						<MkButton @click="refreshMetadata"><i class="ph-arrows-clockwise ph-bold ph-lg"></i> Refresh metadata</MkButton>
+						<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>
 						</MkTextarea>
@@ -169,6 +170,7 @@ const suspensionState = ref<'none' | 'manuallySuspended' | 'goneSuspended' | 'au
 const isBlocked = ref(false);
 const isSilenced = ref(false);
 const isNSFW = ref(false);
+const isMediaSilenced = ref(false);
 const faviconUrl = ref<string | null>(null);
 const moderationNote = ref('');
 
@@ -198,8 +200,9 @@ async function fetch(): Promise<void> {
 	isBlocked.value = instance.value?.isBlocked ?? false;
 	isSilenced.value = instance.value?.isSilenced ?? false;
 	isNSFW.value = instance.value?.isNSFW ?? 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> {
@@ -221,6 +224,16 @@ async function toggleSilenced(): Promise<void> {
 	});
 }
 
+async function toggleMediaSilenced(): Promise<void> {
+	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 (!instance.value) throw new Error('No instance?');
 	suspensionState.value = 'manuallySuspended';
@@ -261,7 +274,7 @@ fetch();
 
 const headerActions = computed(() => [{
 	text: `https://${props.host}`,
-	icon: 'ph-arrow-square-out ph-bold ph-lg',
+	icon: 'ti ti-external-link',
 	handler: () => {
 		window.open(`https://${props.host}`, '_blank', 'noopener');
 	},
@@ -270,24 +283,24 @@ const headerActions = computed(() => [{
 const headerTabs = computed(() => [{
 	key: 'overview',
 	title: i18n.ts.overview,
-	icon: 'ph-info ph-bold ph-lg',
+	icon: 'ti ti-info-circle',
 }, {
 	key: 'chart',
 	title: i18n.ts.charts,
-	icon: 'ph-chart-line ph-bold ph-lg',
+	icon: 'ti ti-chart-line',
 }, {
 	key: 'users',
 	title: i18n.ts.users,
-	icon: 'ph-users ph-bold ph-lg',
+	icon: 'ti ti-users',
 }, {
 	key: 'raw',
 	title: 'Raw',
-	icon: 'ph-code ph-bold ph-lg',
+	icon: 'ti ti-code',
 }]);
 
 definePageMetadata(() => ({
 	title: props.host,
-	icon: 'ph-hard-drives ph-bold ph-lg',
+	icon: 'ti ti-server',
 }));
 </script>
 
diff --git a/packages/frontend/src/pages/invite.vue b/packages/frontend/src/pages/invite.vue
index b8c006eb77..ef485a9446 100644
--- a/packages/frontend/src/pages/invite.vue
+++ b/packages/frontend/src/pages/invite.vue
@@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 		<div :class="$style.root">
 			<img :class="$style.img" :src="serverErrorImageUrl" class="_ghost"/>
 			<div :class="$style.text">
-				<i class="ph-warning ph-bold ph-lg"></i>
+				<i class="ti ti-alert-triangle"></i>
 				{{ i18n.ts.nothing }}
 			</div>
 		</div>
@@ -20,7 +20,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 	<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>
-			<MkButton inline primary rounded :disabled="currentInviteLimit !== null && currentInviteLimit <= 0" @click="create"><i class="ph-user-plus ph-bold ph-lg"></i> {{ i18n.ts.createInviteCode }}</MkButton>
+			<MkButton inline primary rounded :disabled="currentInviteLimit !== null && currentInviteLimit <= 0" @click="create"><i class="ti ti-user-plus"></i> {{ i18n.ts.createInviteCode }}</MkButton>
 			<div v-if="currentInviteLimit !== null">{{ i18n.tsx.createLimitRemaining({ limit: currentInviteLimit }) }}</div>
 
 			<MkPagination ref="pagingComponent" :pagination="pagination">
@@ -95,7 +95,7 @@ update();
 
 definePageMetadata(() => ({
 	title: i18n.ts.invite,
-	icon: 'ph-user-plus ph-bold ph-lg',
+	icon: 'ti ti-user-plus',
 }));
 </script>
 
diff --git a/packages/frontend/src/pages/list.vue b/packages/frontend/src/pages/list.vue
index 87070d9167..03ab804d05 100644
--- a/packages/frontend/src/pages/list.vue
+++ b/packages/frontend/src/pages/list.vue
@@ -10,7 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 		<div :class="$style.root">
 			<img :class="$style.img" :src="serverErrorImageUrl" class="_ghost"/>
 			<p :class="$style.text">
-				<i class="ph-warning ph-bold ph-lg"></i>
+				<i class="ti ti-alert-triangle"></i>
 				{{ i18n.ts.nothing }}
 			</p>
 		</div>
@@ -26,9 +26,9 @@ SPDX-License-Identifier: AGPL-3.0-only
 				</div>
 			</div>
 		</div>
-		<MkButton v-if="list.isLiked" v-tooltip="i18n.ts.unlike" inline :class="$style.button" asLike primary @click="unlike()"><i class="ph-heart-break ph-bold ph-lg"></i><span v-if="list.likedCount > 0" class="count">{{ list.likedCount }}</span></MkButton>
-		<MkButton v-if="!list.isLiked" v-tooltip="i18n.ts.like" inline :class="$style.button" asLike @click="like()"><i class="ph-heart ph-bold ph-lg"></i><span v-if="1 > 0" class="count">{{ list.likedCount }}</span></MkButton>
-		<MkButton inline @click="create()"><i class="ph-download ph-bold ph-lg" :class="$style.import"></i>{{ i18n.ts.import }}</MkButton>
+		<MkButton v-if="list.isLiked" v-tooltip="i18n.ts.unlike" inline :class="$style.button" asLike primary @click="unlike()"><i class="ti ti-heart-off"></i><span v-if="list.likedCount > 0" class="count">{{ list.likedCount }}</span></MkButton>
+		<MkButton v-if="!list.isLiked" v-tooltip="i18n.ts.like" inline :class="$style.button" asLike @click="like()"><i class="ti ti-heart"></i><span v-if="1 > 0" class="count">{{ list.likedCount }}</span></MkButton>
+		<MkButton inline @click="create()"><i class="ti ti-download" :class="$style.import"></i>{{ i18n.ts.import }}</MkButton>
 	</MkSpacer>
 </MkStickyContainer>
 </template>
@@ -103,7 +103,7 @@ const headerTabs = computed(() => []);
 
 definePageMetadata(() => ({
 	title: list.value ? list.value.name : i18n.ts.lists,
-	icon: 'ph-list ph-bold ph-lg',
+	icon: 'ti ti-list',
 }));
 </script>
 <style lang="scss" module>
diff --git a/packages/frontend/src/pages/lookup.vue b/packages/frontend/src/pages/lookup.vue
new file mode 100644
index 0000000000..3233953942
--- /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<any>;
+
+	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 4812bfe70f..7e70f91710 100644
--- a/packages/frontend/src/pages/miauth.vue
+++ b/packages/frontend/src/pages/miauth.vue
@@ -95,7 +95,7 @@ const headerTabs = computed(() => []);
 
 definePageMetadata(() => ({
 	title: 'MiAuth',
-	icon: 'ph-squares-four ph-bold ph-lg',
+	icon: 'ti ti-apps',
 }));
 </script>
 
diff --git a/packages/frontend/src/pages/my-antennas/create.vue b/packages/frontend/src/pages/my-antennas/create.vue
index ee2330a4f7..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,
-	icon: 'ph-flying-saucer ph-bold ph-lg',
+	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 a262e932f3..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,
-	icon: 'ph-flying-saucer ph-bold ph-lg',
+	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 a312672f74..9233695900 100644
--- a/packages/frontend/src/pages/my-antennas/index.vue
+++ b/packages/frontend/src/pages/my-antennas/index.vue
@@ -15,7 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 				</div>
 			</div>
 
-			<MkButton :link="true" to="/my/antennas/create" primary :class="$style.add"><i class="ph-plus ph-bold ph-lg"></i> {{ i18n.ts.add }}</MkButton>
+			<MkButton :link="true" to="/my/antennas/create" primary :class="$style.add"><i class="ti ti-plus"></i> {{ i18n.ts.add }}</MkButton>
 
 			<div v-if="antennas.length > 0" class="_gaps">
 				<MkA v-for="antenna in antennas" :key="antenna.id" :class="$style.antenna" :to="`/my/antennas/${antenna.id}`">
@@ -45,7 +45,7 @@ fetch();
 
 const headerActions = computed(() => [{
 	asFullButton: true,
-	icon: 'ph-arrows-counter-clockwise ph-bold ph-lg',
+	icon: 'ti ti-refresh',
 	text: i18n.ts.reload,
 	handler: () => {
 		antennasCache.delete();
@@ -57,7 +57,7 @@ const headerTabs = computed(() => []);
 
 definePageMetadata(() => ({
 	title: i18n.ts.manageAntennas,
-	icon: 'ph-flying-saucer ph-bold ph-lg',
+	icon: 'ti ti-antenna',
 }));
 
 onActivated(() => {
diff --git a/packages/frontend/src/pages/my-clips/index.vue b/packages/frontend/src/pages/my-clips/index.vue
index 1d5c134b27..ece998a7a5 100644
--- a/packages/frontend/src/pages/my-clips/index.vue
+++ b/packages/frontend/src/pages/my-clips/index.vue
@@ -9,10 +9,10 @@ SPDX-License-Identifier: AGPL-3.0-only
 	<MkSpacer :contentMax="700">
 		<MkHorizontalSwipe v-model:tab="tab" :tabs="headerTabs">
 			<div v-if="tab === 'my'" key="my" class="_gaps">
-				<MkButton primary rounded class="add" @click="create"><i class="ph-plus ph-bold ph-lg"></i> {{ i18n.ts.add }}</MkButton>
+				<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">
@@ -93,16 +93,16 @@ const headerActions = computed(() => []);
 const headerTabs = computed(() => [{
 	key: 'my',
 	title: i18n.ts.myClips,
-	icon: 'ph-paperclip ph-bold ph-lg',
+	icon: 'ti ti-paperclip',
 }, {
 	key: 'favorites',
 	title: i18n.ts.favorites,
-	icon: 'ph-heart ph-bold ph-lg',
+	icon: 'ti ti-heart',
 }]);
 
 definePageMetadata(() => ({
 	title: i18n.ts.clip,
-	icon: 'ph-paperclip ph-bold ph-lg',
+	icon: 'ti ti-paperclip',
 }));
 </script>
 
diff --git a/packages/frontend/src/pages/my-lists/index.vue b/packages/frontend/src/pages/my-lists/index.vue
index f2469be8de..cd68be4ac3 100644
--- a/packages/frontend/src/pages/my-lists/index.vue
+++ b/packages/frontend/src/pages/my-lists/index.vue
@@ -15,7 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 				</div>
 			</div>
 
-			<MkButton primary rounded style="margin: 0 auto;" @click="create"><i class="ph-plus ph-bold ph-lg"></i> {{ i18n.ts.createList }}</MkButton>
+			<MkButton primary rounded style="margin: 0 auto;" @click="create"><i class="ti ti-plus"></i> {{ i18n.ts.createList }}</MkButton>
 
 			<div v-if="items.length > 0" class="_gaps">
 				<MkA v-for="list in items" :key="list.id" class="_panel" :class="$style.list" :to="`/my/lists/${ list.id }`">
@@ -61,7 +61,7 @@ async function create() {
 
 const headerActions = computed(() => [{
 	asFullButton: true,
-	icon: 'ph-arrows-counter-clockwise ph-bold ph-lg',
+	icon: 'ti ti-refresh',
 	text: i18n.ts.reload,
 	handler: () => {
 		userListsCache.delete();
@@ -73,7 +73,7 @@ const headerTabs = computed(() => []);
 
 definePageMetadata(() => ({
 	title: i18n.ts.manageLists,
-	icon: 'ph-list ph-bold ph-lg',
+	icon: 'ti ti-list',
 }));
 
 onActivated(() => {
diff --git a/packages/frontend/src/pages/my-lists/list.vue b/packages/frontend/src/pages/my-lists/list.vue
index 1b7aa3f938..a2ceb222fe 100644
--- a/packages/frontend/src/pages/my-lists/list.vue
+++ b/packages/frontend/src/pages/my-lists/list.vue
@@ -38,8 +38,8 @@ SPDX-License-Identifier: AGPL-3.0-only
 										<MkA :class="$style.userItemBody" :to="`${userPage(item.user)}`">
 											<MkUserCardMini :user="item.user"/>
 										</MkA>
-										<button class="_button" :class="$style.menu" @click="showMembershipMenu(item, $event)"><i class="ph-dots-three ph-bold ph-lg"></i></button>
-										<button class="_button" :class="$style.remove" @click="removeUser(item, $event)"><i class="ph-x ph-bold ph-lg"></i></button>
+										<button class="_button" :class="$style.menu" @click="showMembershipMenu(item, $event)"><i class="ti ti-dots"></i></button>
+										<button class="_button" :class="$style.remove" @click="removeUser(item, $event)"><i class="ti ti-x"></i></button>
 									</div>
 								</div>
 							</div>
@@ -118,7 +118,7 @@ function addUser() {
 async function removeUser(item, ev) {
 	os.popupMenu([{
 		text: i18n.ts.remove,
-		icon: 'ph-x ph-bold ph-lg',
+		icon: 'ti ti-x',
 		danger: true,
 		action: async () => {
 			if (!list.value) return;
@@ -133,22 +133,25 @@ async function removeUser(item, ev) {
 }
 
 async function showMembershipMenu(item, ev) {
+	const withRepliesRef = ref(item.withReplies);
 	os.popupMenu([{
-		text: item.withReplies ? i18n.ts.hideRepliesToOthersInTimeline : i18n.ts.showRepliesToOthersInTimeline,
-		icon: item.withReplies ? 'ph-envelope-open ph-bold ph-lg' : 'ph-envelope ph-bold ph-lg',
-		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() {
@@ -188,7 +191,7 @@ const headerTabs = computed(() => []);
 
 definePageMetadata(() => ({
 	title: list.value ? list.value.name : i18n.ts.lists,
-	icon: 'ph-list ph-bold ph-lg',
+	icon: 'ti ti-list',
 }));
 </script>
 
diff --git a/packages/frontend/src/pages/not-found.vue b/packages/frontend/src/pages/not-found.vue
index 6f69f9285d..93a792c42f 100644
--- a/packages/frontend/src/pages/not-found.vue
+++ b/packages/frontend/src/pages/not-found.vue
@@ -33,6 +33,6 @@ const headerTabs = computed(() => []);
 
 definePageMetadata(() => ({
 	title: i18n.ts.notFound,
-	icon: 'ph-warning ph-bold ph-lg',
+	icon: 'ti ti-alert-triangle',
 }));
 </script>
diff --git a/packages/frontend/src/pages/note.vue b/packages/frontend/src/pages/note.vue
index 418b33754b..d0bbaeddd9 100644
--- a/packages/frontend/src/pages/note.vue
+++ b/packages/frontend/src/pages/note.vue
@@ -16,17 +16,13 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 					<div class="_margin">
 						<div v-if="!showNext" class="_buttons" :class="$style.loadNext">
-							<MkButton v-if="note.channelId" rounded :class="$style.loadButton" @click="showNext = 'channel'"><i class="ph-caret-up ph-bold ph-lg"></i> <i class="ph-television-simple ph-bold ph-lg"></i></MkButton>
-							<MkButton rounded :class="$style.loadButton" @click="showNext = 'user'"><i class="ph-caret-up ph-bold ph-lg"></i> <i class="ph-user ph-bold ph-lg"></i></MkButton>
+							<MkButton v-if="note.channelId" rounded :class="$style.loadButton" @click="showNext = 'channel'"><i class="ti ti-chevron-up"></i> <i class="ti ti-device-tv"></i></MkButton>
+							<MkButton rounded :class="$style.loadButton" @click="showNext = 'user'"><i class="ti ti-chevron-up"></i> <i class="ti ti-user"></i></MkButton>
 						</div>
-						<div v-if="defaultStore.state.noteDesign === 'misskey'" class="_margin _gaps_s">
+						<div class="_margin _gaps_s">
 							<MkRemoteCaution v-if="note.user.host != null" :href="note.url ?? note.uri"/>
 							<MkNoteDetailed :key="note.id" v-model:note="note" :initialTab="initialTab" :class="$style.note" :expandAllCws="expandAllCws"/>
 						</div>
-						<div v-else-if="defaultStore.state.noteDesign === 'sharkey'" class="_margin _gaps_s">
-							<MkRemoteCaution v-if="note.user.host != null" :href="note.url ?? note.uri"/>
-							<SkNoteDetailed :key="note.id" v-model:note="note" :initialTab="initialTab" :class="$style.note" :expandAllCws="expandAllCws"/>
-						</div>
 						<div v-if="clips && clips.length > 0" class="_margin">
 							<div style="font-weight: bold; padding: 12px;">{{ i18n.ts.clip }}</div>
 							<div class="_gaps">
@@ -34,8 +30,8 @@ SPDX-License-Identifier: AGPL-3.0-only
 							</div>
 						</div>
 						<div v-if="!showPrev" class="_buttons" :class="$style.loadPrev">
-							<MkButton v-if="note.channelId" rounded :class="$style.loadButton" @click="showPrev = 'channel'"><i class="ph-caret-down ph-bold ph-lg"></i> <i class="ph-television-simple ph-bold ph-lg"></i></MkButton>
-							<MkButton rounded :class="$style.loadButton" @click="showPrev = 'user'"><i class="ph-caret-down ph-bold ph-lg"></i> <i class="ph-user ph-bold ph-lg"></i></MkButton>
+							<MkButton v-if="note.channelId" rounded :class="$style.loadButton" @click="showPrev = 'channel'"><i class="ti ti-chevron-down"></i> <i class="ti ti-device-tv"></i></MkButton>
+							<MkButton rounded :class="$style.loadButton" @click="showPrev = 'user'"><i class="ti ti-chevron-down"></i> <i class="ti ti-user"></i></MkButton>
 						</div>
 					</div>
 
@@ -52,12 +48,10 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { computed, watch, ref } from 'vue';
+import { defineAsyncComponent, computed, watch, ref } from 'vue';
 import * as Misskey from 'misskey-js';
 import type { Paging } from '@/components/MkPagination.vue';
-import MkNoteDetailed from '@/components/MkNoteDetailed.vue';
 import MkNotes from '@/components/MkNotes.vue';
-import SkNoteDetailed from '@/components/SkNoteDetailed.vue';
 import MkRemoteCaution from '@/components/MkRemoteCaution.vue';
 import MkButton from '@/components/MkButton.vue';
 import { misskeyApi } from '@/scripts/misskey-api.js';
@@ -67,6 +61,12 @@ import { dateString } from '@/filters/date.js';
 import MkClipPreview from '@/components/MkClipPreview.vue';
 import { defaultStore } from '@/store.js';
 
+const MkNoteDetailed = defineAsyncComponent(() =>
+	(defaultStore.state.noteDesign === 'misskey') ? import('@/components/MkNoteDetailed.vue') :
+	(defaultStore.state.noteDesign === 'sharkey') ? import('@/components/SkNoteDetailed.vue') :
+	null
+);
+
 const props = defineProps<{
 	noteId: string;
 	initialTab?: string;
diff --git a/packages/frontend/src/pages/notifications.vue b/packages/frontend/src/pages/notifications.vue
index 9cbb6323a8..28f5838296 100644
--- a/packages/frontend/src/pages/notifications.vue
+++ b/packages/frontend/src/pages/notifications.vue
@@ -58,7 +58,7 @@ function setFilter(ev) {
 		},
 	}));
 	const items = includeTypes.value != null ? [{
-		icon: 'ph-x ph-bold ph-lg',
+		icon: 'ti ti-x',
 		text: i18n.ts.clear,
 		action: () => {
 			includeTypes.value = null;
@@ -69,12 +69,12 @@ function setFilter(ev) {
 
 const headerActions = computed(() => [tab.value === 'all' ? {
 	text: i18n.ts.filter,
-	icon: 'ph-funnel ph-bold ph-lg',
+	icon: 'ti ti-filter',
 	highlighted: includeTypes.value != null,
 	handler: setFilter,
 } : undefined, tab.value === 'all' ? {
 	text: i18n.ts.markAllAsRead,
-	icon: 'ph-check ph-bold ph-lg',
+	icon: 'ti ti-check',
 	handler: () => {
 		os.apiWithDialog('notifications/mark-all-as-read');
 	},
@@ -83,20 +83,20 @@ const headerActions = computed(() => [tab.value === 'all' ? {
 const headerTabs = computed(() => [{
 	key: 'all',
 	title: i18n.ts.all,
-	icon: 'ph-circle ph-bold ph-lg',
+	icon: 'ti ti-point',
 }, {
 	key: 'mentions',
 	title: i18n.ts.mentions,
-	icon: 'ph-at ph-bold ph-lg',
+	icon: 'ti ti-at',
 }, {
 	key: 'directNotes',
 	title: i18n.ts.directNotes,
-	icon: 'ph-envelope ph-bold ph-lg',
+	icon: 'ti ti-mail',
 }]);
 
 definePageMetadata(() => ({
 	title: i18n.ts.notifications,
-	icon: 'ph-bell ph-bold ph-lg',
+	icon: 'ti ti-bell',
 }));
 </script>
 
diff --git a/packages/frontend/src/pages/oauth.vue b/packages/frontend/src/pages/oauth.vue
index 80b6a237ea..733e34eb2c 100644
--- a/packages/frontend/src/pages/oauth.vue
+++ b/packages/frontend/src/pages/oauth.vue
@@ -53,7 +53,7 @@ function onLogin(res): void {
 
 definePageMetadata(() => ({
 	title: 'OAuth',
-	icon: 'ph-squares-four ph-bold ph-lg',
+	icon: 'ti ti-apps',
 }));
 </script>
 
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 2a55c083d1..1cfe7a6d2d 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
@@ -6,10 +6,10 @@ SPDX-License-Identifier: AGPL-3.0-only
 <template>
 <!-- eslint-disable vue/no-mutating-props -->
 <XContainer :draggable="true" @remove="() => $emit('remove')">
-	<template #header><i class="ph-image-square ph-bold ph-lg"></i> {{ i18n.ts._pages.blocks.image }}</template>
+	<template #header><i class="ti ti-photo"></i> {{ i18n.ts._pages.blocks.image }}</template>
 	<template #func>
 		<button @click="choose()">
-			<i class="ph-folder ph-bold ph-lg"></i>
+			<i class="ti ti-folder"></i>
 		</button>
 	</template>
 
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 0bbe256cbf..0a28386986 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
@@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 <template>
 <!-- eslint-disable vue/no-mutating-props -->
 <XContainer :draggable="true" @remove="() => $emit('remove')">
-	<template #header><i class="ph-note ph-bold ph-lg"></i> {{ i18n.ts._pages.blocks.note }}</template>
+	<template #header><i class="ti ti-note"></i> {{ i18n.ts._pages.blocks.note }}</template>
 
 	<section style="padding: 16px;" class="_gaps_s">
 		<MkInput v-model="id">
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 2e4402085c..0f8dc33143 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
@@ -6,16 +6,16 @@ SPDX-License-Identifier: AGPL-3.0-only
 <template>
 <!-- eslint-disable vue/no-mutating-props -->
 <XContainer :draggable="true" @remove="() => $emit('remove')">
-	<template #header><i class="ph-note ph-bold ph-lg"></i> {{ props.modelValue.title }}</template>
+	<template #header><i class="ti ti-note"></i> {{ props.modelValue.title }}</template>
 	<template #func>
 		<button class="_button" @click="rename()">
-			<i class="ph-pencil-simple ph-bold ph-lg"></i>
+			<i class="ti ti-pencil"></i>
 		</button>
 	</template>
 
 	<section class="ilrvjyvi">
 		<XBlocks v-model="children" class="children"/>
-		<MkButton rounded class="add" @click="add()"><i class="ph-plus ph-bold ph-lg"></i></MkButton>
+		<MkButton rounded class="add" @click="add()"><i class="ti ti-plus"></i></MkButton>
 	</section>
 </XContainer>
 </template>
@@ -29,6 +29,7 @@ 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'));
 
@@ -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 e83ad058b4..14c3e6845e 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
@@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 <template>
 <!-- eslint-disable vue/no-mutating-props -->
 <XContainer :draggable="true" @remove="() => $emit('remove')">
-	<template #header><i class="ph-text-align-left ph-bold ph-lg"></i> {{ i18n.ts._pages.blocks.text }}</template>
+	<template #header><i class="ti ti-align-left"></i> {{ i18n.ts._pages.blocks.text }}</template>
 
 	<section>
 		<textarea ref="inputEl" v-model="text" :class="$style.textarea"></textarea>
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 7ef66c1c0c..2531de0e62 100644
--- a/packages/frontend/src/pages/page-editor/page-editor.container.vue
+++ b/packages/frontend/src/pages/page-editor/page-editor.container.vue
@@ -10,14 +10,14 @@ SPDX-License-Identifier: AGPL-3.0-only
 		<div class="buttons">
 			<slot name="func"></slot>
 			<button v-if="removable" class="_button" @click="remove()">
-				<i class="ph-trash ph-bold ph-lg"></i>
+				<i class="ti ti-trash"></i>
 			</button>
 			<button v-if="draggable" class="drag-handle _button">
-				<i class="ph-list ph-bold ph-lg-2"></i>
+				<i class="ti ti-menu-2"></i>
 			</button>
 			<button class="_button" @click="toggleContent(!showBody)">
-				<template v-if="showBody"><i class="ph-caret-up ph-bold ph-lg"></i></template>
-				<template v-else><i class="ph-caret-down ph-bold ph-lg"></i></template>
+				<template v-if="showBody"><i class="ti ti-chevron-up"></i></template>
+				<template v-else><i class="ti ti-chevron-down"></i></template>
 			</button>
 		</div>
 	</header>
diff --git a/packages/frontend/src/pages/page-editor/page-editor.vue b/packages/frontend/src/pages/page-editor/page-editor.vue
index 92de9d57a5..6bc3c0908a 100644
--- a/packages/frontend/src/pages/page-editor/page-editor.vue
+++ b/packages/frontend/src/pages/page-editor/page-editor.vue
@@ -8,10 +8,10 @@ SPDX-License-Identifier: AGPL-3.0-only
 	<template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template>
 	<MkSpacer :contentMax="700">
 		<div class="jqqmcavi">
-			<MkButton v-if="pageId" class="button" inline link :to="`/@${ author.username }/pages/${ currentName }`"><i class="ph-arrow-square-out ph-bold ph-lg"></i> {{ i18n.ts._pages.viewPage }}</MkButton>
-			<MkButton v-if="!readonly" inline primary class="button" @click="save"><i class="ph-floppy-disk ph-bold ph-lg"></i> {{ i18n.ts.save }}</MkButton>
-			<MkButton v-if="pageId" inline class="button" @click="duplicate"><i class="ph-copy ph-bold ph-lg"></i> {{ i18n.ts.duplicate }}</MkButton>
-			<MkButton v-if="pageId && !readonly" inline class="button" danger @click="del"><i class="ph-trash ph-bold ph-lg"></i> {{ i18n.ts.delete }}</MkButton>
+			<MkButton v-if="pageId" class="button" inline link :to="`/@${ author.username }/pages/${ currentName }`"><i class="ti ti-external-link"></i> {{ i18n.ts._pages.viewPage }}</MkButton>
+			<MkButton v-if="!readonly" inline primary class="button" @click="save"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton>
+			<MkButton v-if="pageId" inline class="button" @click="duplicate"><i class="ti ti-copy"></i> {{ i18n.ts.duplicate }}</MkButton>
+			<MkButton v-if="pageId && !readonly" inline class="button" danger @click="del"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton>
 		</div>
 
 		<div v-if="tab === 'settings'">
@@ -40,10 +40,10 @@ SPDX-License-Identifier: AGPL-3.0-only
 				<MkSwitch v-model="hideTitleWhenPinned">{{ i18n.ts._pages.hideTitleWhenPinned }}</MkSwitch>
 
 				<div class="eyeCatch">
-					<MkButton v-if="eyeCatchingImageId == null && !readonly" @click="setEyeCatchingImage"><i class="ph-plus ph-bold ph-lg"></i> {{ i18n.ts._pages.eyeCatchingImageSet }}</MkButton>
+					<MkButton v-if="eyeCatchingImageId == null && !readonly" @click="setEyeCatchingImage"><i class="ti ti-plus"></i> {{ i18n.ts._pages.eyeCatchingImageSet }}</MkButton>
 					<div v-else-if="eyeCatchingImage">
 						<img :src="eyeCatchingImage.url" :alt="eyeCatchingImage.name" style="max-width: 100%;"/>
-						<MkButton v-if="!readonly" @click="removeEyeCatchingImage()"><i class="ph-trash ph-bold ph-lg"></i> {{ i18n.ts._pages.eyeCatchingImageRemove }}</MkButton>
+						<MkButton v-if="!readonly" @click="removeEyeCatchingImage()"><i class="ti ti-trash"></i> {{ i18n.ts._pages.eyeCatchingImageRemove }}</MkButton>
 					</div>
 				</div>
 			</div>
@@ -53,7 +53,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 			<div :class="$style.contents">
 				<XBlocks v-model="content" class="content"/>
 
-				<MkButton v-if="!readonly" rounded class="add" @click="add()"><i class="ph-plus ph-bold ph-lg"></i></MkButton>
+				<MkButton v-if="!readonly" rounded class="add" @click="add()"><i class="ti ti-plus"></i></MkButton>
 			</div>
 		</div>
 	</MkSpacer>
@@ -76,6 +76,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;
@@ -100,7 +101,6 @@ const alignCenter = ref(false);
 const hideTitleWhenPinned = ref(false);
 
 provide('readonly', readonly.value);
-provide('getPageBlockList', getPageBlockList);
 
 watch(eyeCatchingImageId, async () => {
 	if (eyeCatchingImageId.value == null) {
@@ -215,15 +215,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;
@@ -276,18 +267,18 @@ const headerActions = computed(() => []);
 const headerTabs = computed(() => [{
 	key: 'settings',
 	title: i18n.ts._pages.pageSetting,
-	icon: 'ph-gear ph-bold ph-lg',
+	icon: 'ti ti-settings',
 }, {
 	key: 'contents',
 	title: i18n.ts._pages.contents,
-	icon: 'ph-note ph-bold ph-lg',
+	icon: 'ti ti-note',
 }]);
 
 definePageMetadata(() => ({
 	title: props.initPageId ? i18n.ts._pages.editPage
 				: props.initPageName && props.initUser ? i18n.ts._pages.readPage
 				: i18n.ts._pages.newPage,
-	icon: 'ph-pencil-simple ph-bold ph-lg',
+	icon: 'ti ti-pencil',
 }));
 </script>
 
diff --git a/packages/frontend/src/pages/page.vue b/packages/frontend/src/pages/page.vue
index 99b8b4143f..fc8627a772 100644
--- a/packages/frontend/src/pages/page.vue
+++ b/packages/frontend/src/pages/page.vue
@@ -47,8 +47,8 @@ SPDX-License-Identifier: AGPL-3.0-only
 									<MkAvatar :user="page.user" :class="$style.avatar" indicator link preview/> <MkA :to="`/@${username}`"><MkUserName :user="page.user" :nowrap="false"/></MkA>
 								</div>
 								<div :class="$style.pageBannerTitleSubActions">
-									<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="ph-pencil-simple ph-bold ph-lg"></i></MkA>
-									<button v-tooltip="i18n.ts.share" class="_button" :class="$style.generalActionButton" @click="share"><i class="ph-share-network ph-bold ph-lg ti-fw"></i></button>
+									<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.share" class="_button" :class="$style.generalActionButton" @click="share"><i class="ti ti-share ti-fw"></i></button>
 								</div>
 							</div>
 						</div>
@@ -58,12 +58,14 @@ SPDX-License-Identifier: AGPL-3.0-only
 					</div>
 					<div :class="$style.pageActions">
 						<div>
-							<MkButton v-if="page.isLiked" v-tooltip="i18n.ts._pages.unlike" class="button" asLike primary @click="unlike()"><i class="ph-heart-break ph-bold ph-lg"></i><span v-if="page.likedCount > 0" class="count">{{ page.likedCount }}</span></MkButton>
-							<MkButton v-else v-tooltip="i18n.ts._pages.like" class="button" asLike @click="like()"><i class="ph-heart ph-bold ph-lg"></i><span v-if="page.likedCount > 0" class="count">{{ page.likedCount }}</span></MkButton>
+							<MkButton v-if="page.isLiked" v-tooltip="i18n.ts._pages.unlike" class="button" asLike primary @click="unlike()"><i class="ti ti-heart-off"></i><span v-if="page.likedCount > 0" class="count">{{ page.likedCount }}</span></MkButton>
+							<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">
-							<button v-tooltip="i18n.ts.copyLink" class="_button" :class="$style.generalActionButton" @click="copyLink"><i class="ph-link ph-bold ph-lg ti-fw"></i></button>
-							<button v-tooltip="i18n.ts.share" class="_button" :class="$style.generalActionButton" @click="share"><i class="ph-share-network ph-bold ph-lg ti-fw"></i></button>
+							<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">
@@ -75,21 +77,13 @@ SPDX-License-Identifier: AGPL-3.0-only
 						<MkFollowButton v-if="!$i || $i.id != page.user.id" :user="page.user!" :inline="true" :transparent="false" :full="true" :class="$style.follow"/>
 					</div>
 					<div :class="$style.pageDate">
-						<div><i class="ph ph-clock ph-bold ph-lg"></i> {{ i18n.ts.createdAt }}: <MkTime :time="page.createdAt" mode="detail"/></div>
-						<div v-if="page.createdAt != page.updatedAt"><i class="ph-pencil-simple ph-bold ph-lg"></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><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>
 				<MkAd :prefer="['horizontal', 'horizontal-big']"/>
 				<MkContainer :max-height="300" :foldable="true" class="other">
-					<template #icon><i class="ph-clock ph-bold ph-lg"></i></template>
+					<template #icon><i class="ti ti-clock"></i></template>
 					<template #header>{{ i18n.ts.recentPosts }}</template>
 					<MkPagination v-slot="{items}" :pagination="otherPostsPagination" :class="$style.relatedPagesRoot" class="_gaps">
 						<MkPagePreview v-for="page in items" :key="page.id" :page="page" :class="$style.relatedPagesItem"/>
@@ -104,7 +98,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 XPage from '@/components/page/page.vue';
 import MkButton from '@/components/MkButton.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 { MenuItem } from '@/types/menu';
+
+const router = useRouter();
 
 const props = defineProps<{
 	pageName: string;
@@ -170,12 +168,12 @@ function share(ev: MouseEvent) {
 	os.popupMenu([
 		{
 			text: i18n.ts.shareWithNote,
-			icon: 'ph-pencil-simple',
+			icon: 'ti ti-pencil',
 			action: shareWithNote,
 		},
 		...(isSupportShare() ? [{
 			text: i18n.ts.share,
-			icon: 'ph-share-network',
+			icon: 'ti ti-share',
 			action: shareWithNavigator,
 		}] : []),
 	], ev.currentTarget ?? ev.target);
@@ -242,6 +240,69 @@ 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 menu: MenuItem[] = [
+		...($i && $i.id === page.value.userId ? [
+			{
+				icon: 'ti ti-code',
+				text: i18n.ts._pages.viewSource,
+				action: () => router.push(`/@${props.username}/pages/${props.pageName}/view-source`),
+			},
+			...($i.pinnedPageId === page.value.id ? [{
+				icon: 'ti ti-pinned-off',
+				text: i18n.ts.unpin,
+				action: () => pin(false),
+			}] : [{
+				icon: 'ti ti-pin',
+				text: i18n.ts.pin,
+				action: () => pin(true),
+			}]),
+		] : []),
+		...($i && $i.id !== page.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 || !page.value) return;
+
+						os.apiWithDialog('pages/delete', { pageId: page.value.id });
+					}),
+				},
+			] : []),
+		] : []),
+	];
+
+	os.popupMenu(menu, ev.currentTarget ?? ev.target);
+}
+
 watch(() => path.value, fetchPage, { immediate: true });
 
 const headerActions = computed(() => []);
@@ -286,6 +347,7 @@ definePageMetadata(() => ({
 		background-color: var(--accentedBg);
 		color: var(--accent);
 		text-decoration: none;
+		outline: none;
 	}
 }
 
diff --git a/packages/frontend/src/pages/pages.vue b/packages/frontend/src/pages/pages.vue
index 7b4dd83068..4ef9d3b091 100644
--- a/packages/frontend/src/pages/pages.vue
+++ b/packages/frontend/src/pages/pages.vue
@@ -17,7 +17,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 			</div>
 
 			<div v-else-if="tab === 'my'" key="my" class="_gaps">
-				<MkButton class="new" @click="create()"><i class="ph-plus ph-bold ph-lg"></i></MkButton>
+				<MkButton class="new" @click="create()"><i class="ti ti-plus"></i></MkButton>
 				<MkPagination v-slot="{items}" :pagination="myPagesPagination">
 					<div class="_gaps">
 						<MkPagePreview v-for="page in items" :key="page.id" :page="page"/>
@@ -69,7 +69,7 @@ function create() {
 }
 
 const headerActions = computed(() => [{
-	icon: 'ph-plus ph-bold ph-lg',
+	icon: 'ti ti-plus',
 	text: i18n.ts.create,
 	handler: create,
 }]);
@@ -77,19 +77,19 @@ const headerActions = computed(() => [{
 const headerTabs = computed(() => [{
 	key: 'featured',
 	title: i18n.ts._pages.featured,
-	icon: 'ph-fire ph-bold ph-lg',
+	icon: 'ti ti-flare',
 }, {
 	key: 'my',
 	title: i18n.ts._pages.my,
-	icon: 'ph-pencil-simple-line ph-bold ph-lg',
+	icon: 'ti ti-edit',
 }, {
 	key: 'liked',
 	title: i18n.ts._pages.liked,
-	icon: 'ph-heart ph-bold ph-lg',
+	icon: 'ti ti-heart',
 }]);
 
 definePageMetadata(() => ({
 	title: i18n.ts.pages,
-	icon: 'ph-note ph-bold ph-lg',
+	icon: 'ti ti-note',
 }));
 </script>
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 350c4fea1d..bac1d2bb70 100644
--- a/packages/frontend/src/pages/registry.keys.vue
+++ b/packages/frontend/src/pages/registry.keys.vue
@@ -98,6 +98,6 @@ const headerTabs = computed(() => []);
 
 definePageMetadata(() => ({
 	title: i18n.ts.registry,
-	icon: 'ph-faders ph-bold ph-lg',
+	icon: 'ti ti-adjustments',
 }));
 </script>
diff --git a/packages/frontend/src/pages/registry.value.vue b/packages/frontend/src/pages/registry.value.vue
index 61bf5f4545..c40d13f664 100644
--- a/packages/frontend/src/pages/registry.value.vue
+++ b/packages/frontend/src/pages/registry.value.vue
@@ -30,14 +30,14 @@ SPDX-License-Identifier: AGPL-3.0-only
 					<template #label>{{ i18n.ts.value }} (JSON)</template>
 				</MkCodeEditor>
 
-				<MkButton primary @click="save"><i class="ph-floppy-disk ph-bold ph-lg"></i> {{ i18n.ts.save }}</MkButton>
+				<MkButton primary @click="save"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton>
 
 				<MkKeyValue>
 					<template #key>{{ i18n.ts.updatedAt }}</template>
 					<template #value><MkTime :time="value.updatedAt" mode="detail"/></template>
 				</MkKeyValue>
 
-				<MkButton danger @click="del"><i class="ph-trash ph-bold ph-lg"></i> {{ i18n.ts.delete }}</MkButton>
+				<MkButton danger @click="del"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton>
 			</template>
 		</div>
 	</MkSpacer>
@@ -125,6 +125,6 @@ const headerTabs = computed(() => []);
 
 definePageMetadata(() => ({
 	title: i18n.ts.registry,
-	icon: 'ph-faders ph-bold ph-lg',
+	icon: 'ti ti-adjustments',
 }));
 </script>
diff --git a/packages/frontend/src/pages/registry.vue b/packages/frontend/src/pages/registry.vue
index de0c898187..c641874b17 100644
--- a/packages/frontend/src/pages/registry.vue
+++ b/packages/frontend/src/pages/registry.vue
@@ -75,6 +75,6 @@ const headerTabs = computed(() => []);
 
 definePageMetadata(() => ({
 	title: i18n.ts.registry,
-	icon: 'ph-faders ph-bold ph-lg',
+	icon: 'ti ti-adjustments',
 }));
 </script>
diff --git a/packages/frontend/src/pages/reset-password.vue b/packages/frontend/src/pages/reset-password.vue
index 8b0b4baa67..6d24029535 100644
--- a/packages/frontend/src/pages/reset-password.vue
+++ b/packages/frontend/src/pages/reset-password.vue
@@ -9,7 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 	<MkSpacer v-if="token" :contentMax="700" :marginMin="16" :marginMax="32">
 		<div class="_gaps_m">
 			<MkInput v-model="password" type="password">
-				<template #prefix><i class="ph-lock ph-bold ph-lg"></i></template>
+				<template #prefix><i class="ti ti-lock"></i></template>
 				<template #label>{{ i18n.ts.newPassword }}</template>
 			</MkInput>
 
@@ -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('/');
 	}
 });
@@ -55,6 +57,6 @@ const headerTabs = computed(() => []);
 
 definePageMetadata(() => ({
 	title: i18n.ts.resetPassword,
-	icon: 'ph-lock ph-bold ph-lg',
+	icon: 'ti ti-lock',
 }));
 </script>
diff --git a/packages/frontend/src/pages/reversi/game.board.vue b/packages/frontend/src/pages/reversi/game.board.vue
index af0309b699..7d9cefa5c9 100644
--- a/packages/frontend/src/pages/reversi/game.board.vue
+++ b/packages/frontend/src/pages/reversi/game.board.vue
@@ -89,12 +89,12 @@ SPDX-License-Identifier: AGPL-3.0-only
 		<div v-if="game.isEnded" class="_panel _gaps_s" style="padding: 16px;">
 			<div>{{ logPos }} / {{ game.logs.length }}</div>
 			<div v-if="!autoplaying" class="_buttonsCenter">
-				<MkButton :disabled="logPos === 0" @click="logPos = 0"><i class="ph-caret-left ph-bold ph-lg"></i></MkButton>
-				<MkButton :disabled="logPos === 0" @click="logPos--"><i class="ph-caret-left ph-bold ph-lg"></i></MkButton>
-				<MkButton :disabled="logPos === game.logs.length" @click="logPos++"><i class="ph-caret-right ph-bold ph-lg"></i></MkButton>
-				<MkButton :disabled="logPos === game.logs.length" @click="logPos = game.logs.length"><i class="ph-caret-right ph-bold ph-lg"></i></MkButton>
+				<MkButton :disabled="logPos === 0" @click="logPos = 0"><i class="ti ti-chevrons-left"></i></MkButton>
+				<MkButton :disabled="logPos === 0" @click="logPos--"><i class="ti ti-chevron-left"></i></MkButton>
+				<MkButton :disabled="logPos === game.logs.length" @click="logPos++"><i class="ti ti-chevron-right"></i></MkButton>
+				<MkButton :disabled="logPos === game.logs.length" @click="logPos = game.logs.length"><i class="ti ti-chevrons-right"></i></MkButton>
 			</div>
-			<MkButton style="margin: auto;" :disabled="autoplaying" @click="autoplay()"><i class="ph-play ph-bold ph-lg"></i></MkButton>
+			<MkButton style="margin: auto;" :disabled="autoplaying" @click="autoplay()"><i class="ti ti-player-play"></i></MkButton>
 		</div>
 
 		<div class="_panel" style="padding: 16px;">
@@ -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({
diff --git a/packages/frontend/src/pages/reversi/game.setting.vue b/packages/frontend/src/pages/reversi/game.setting.vue
index 93b0972e9c..31c0003130 100644
--- a/packages/frontend/src/pages/reversi/game.setting.vue
+++ b/packages/frontend/src/pages/reversi/game.setting.vue
@@ -23,10 +23,10 @@ SPDX-License-Identifier: AGPL-3.0-only
 						</div>
 
 						<div style="padding: 16px;">
-							<div v-if="game.map == null"><i class="ph-dice-five ph-bold ph-lg"></i></div>
+							<div v-if="game.map == null"><i class="ti ti-dice"></i></div>
 							<div v-else :class="$style.board" :style="{ 'grid-template-rows': `repeat(${ game.map.length }, 1fr)`, 'grid-template-columns': `repeat(${ game.map[0].length }, 1fr)` }">
 								<div v-for="(x, i) in game.map.join('')" :class="[$style.boardCell, { [$style.boardCellNone]: x == ' ' }]" @click="onMapCellClick(i, x)">
-									<i v-if="x === 'b' || x === 'w'" style="pointer-events: none; user-select: none;" :class="x === 'b' ? 'ph-circle-half ph-bold ph-lg' : 'ph-circle ph-bold ph-lg'"></i>
+									<i v-if="x === 'b' || x === 'w'" style="pointer-events: none; user-select: none;" :class="x === 'b' ? 'ti ti-circle-filled' : 'ti ti-circle'"></i>
 								</div>
 							</div>
 						</div>
diff --git a/packages/frontend/src/pages/reversi/game.vue b/packages/frontend/src/pages/reversi/game.vue
index 21b7797240..97a793753d 100644
--- a/packages/frontend/src/pages/reversi/game.vue
+++ b/packages/frontend/src/pages/reversi/game.vue
@@ -20,6 +20,7 @@ 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 '@/config.js';
 import { i18n } from '@/i18n.js';
 import { useInterval } from '@/scripts/use-interval.js';
 
@@ -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',
 		});
 	}
@@ -115,6 +116,6 @@ onUnmounted(() => {
 
 definePageMetadata(() => ({
 	title: 'Reversi',
-	icon: 'ph-game-controller ph-bold ph-lg',
+	icon: 'ti ti-device-gamepad',
 }));
 </script>
diff --git a/packages/frontend/src/pages/reversi/index.vue b/packages/frontend/src/pages/reversi/index.vue
index c863b91834..51a03e4418 100644
--- a/packages/frontend/src/pages/reversi/index.vue
+++ b/packages/frontend/src/pages/reversi/index.vue
@@ -15,7 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 				<MkButton primary gradate rounded @click="matchAny">{{ i18n.ts._reversi.freeMatch }}</MkButton>
 				<MkButton primary gradate rounded @click="matchUser">{{ i18n.ts.invite }}</MkButton>
 			</div>
-			<div style="font-size: 90%; opacity: 0.7; text-align: center;"><i class="ph-music-notes ph-bold ph-lg"></i> {{ i18n.ts.soundWillBePlayed }}</div>
+			<div style="font-size: 90%; opacity: 0.7; text-align: center;"><i class="ti ti-music"></i> {{ i18n.ts.soundWillBePlayed }}</div>
 		</div>
 
 		<MkFolder v-if="invitations.length > 0" :defaultOpen="true">
@@ -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="ph-trophy ph-bold ph-lg"></i></span>
-								<span v-if="g.winnerId === g.user2Id" style="margin-right: 0.75em; visibility: hidden;"><i class="ph-x ph-bold ph-lg"></i></span>
+								<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.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="ph-x ph-bold ph-lg"></i></span>
-								<span v-if="g.winnerId === g.user2Id" style="margin-left: 0.75em; color: var(--accent); font-weight: bold;"><i class="ph-trophy ph-bold ph-lg"></i></span>
+								<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>
 							</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="ph-trophy ph-bold ph-lg"></i></span>
-								<span v-if="g.winnerId === g.user2Id" style="margin-right: 0.75em; visibility: hidden;"><i class="ph-x ph-bold ph-lg"></i></span>
+								<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.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="ph-x ph-bold ph-lg"></i></span>
-								<span v-if="g.winnerId === g.user2Id" style="margin-left: 0.75em; color: var(--accent); font-weight: bold;"><i class="ph-trophy ph-bold ph-lg"></i></span>
+								<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>
 							</div>
 							<div :class="$style.gamePreviewFooter">
 								<span v-if="g.isStarted && !g.isEnded" :class="$style.gamePreviewStatusActive">{{ i18n.ts._reversi.playing }}</span>
@@ -263,7 +263,7 @@ onUnmounted(() => {
 
 definePageMetadata(() => ({
 	title: 'Reversi',
-	icon: 'ph-game-controller ph-bold ph-lg',
+	icon: 'ti ti-device-gamepad',
 }));
 </script>
 
diff --git a/packages/frontend/src/pages/role.vue b/packages/frontend/src/pages/role.vue
index 8621b61eeb..45edbc5da2 100644
--- a/packages/frontend/src/pages/role.vue
+++ b/packages/frontend/src/pages/role.vue
@@ -10,7 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 		<div :class="$style.root">
 			<img :class="$style.img" :src="serverErrorImageUrl" class="_ghost"/>
 			<p :class="$style.text">
-				<i class="ph-warning ph-bold ph-lg"></i>
+				<i class="ti ti-alert-triangle"></i>
 				{{ error }}
 			</p>
 		</div>
@@ -85,17 +85,17 @@ const users = computed(() => ({
 
 const headerTabs = computed(() => [{
 	key: 'users',
-	icon: 'ph-users ph-bold ph-lg',
+	icon: 'ti ti-users',
 	title: i18n.ts.users,
 }, {
 	key: 'timeline',
-	icon: 'ph-pencil-simple ph-bold ph-lg',
+	icon: 'ti ti-pencil',
 	title: i18n.ts.timeline,
 }]);
 
 definePageMetadata(() => ({
 	title: role.value ? role.value.name : i18n.ts.role,
-	icon: 'ph-seal-check ph-bold ph-lg',
+	icon: 'ti ti-badge',
 }));
 </script>
 
@@ -118,4 +118,3 @@ definePageMetadata(() => ({
 	border-radius: var(--radius-md);
 }
 </style>
-
diff --git a/packages/frontend/src/pages/scratchpad.vue b/packages/frontend/src/pages/scratchpad.vue
index fb3657cdc9..9aaa8ff9c6 100644
--- a/packages/frontend/src/pages/scratchpad.vue
+++ b/packages/frontend/src/pages/scratchpad.vue
@@ -13,7 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 				<div :class="$style.editor" class="_panel">
 					<MkCodeEditor v-model="code" lang="aiscript"/>
 				</div>
-				<MkButton primary @click="run()"><i class="ph-play ph-bold ph-lg"></i></MkButton>
+				<MkButton primary @click="run()"><i class="ti ti-player-play"></i></MkButton>
 			</div>
 
 			<MkContainer v-if="root && components.length > 1" :key="uiKey" :foldable="true">
@@ -154,7 +154,7 @@ const headerTabs = computed(() => []);
 
 definePageMetadata(() => ({
 	title: i18n.ts.scratchpad,
-	icon: 'ph-terminal-window ph-bold ph-lg-2',
+	icon: 'ti ti-terminal-2',
 }));
 </script>
 
diff --git a/packages/frontend/src/pages/search.note.vue b/packages/frontend/src/pages/search.note.vue
index 525e71cf00..66c5c92480 100644
--- a/packages/frontend/src/pages/search.note.vue
+++ b/packages/frontend/src/pages/search.note.vue
@@ -6,14 +6,22 @@ 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">
-			<template #prefix><i class="ph-magnifying-glass ph-bold ph-lg"></i></template>
+		<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>
 				<MkSwitch v-model="order">Sort by newest to oldest</MkSwitch>
 				<MkSelect v-model="filetype" small>
 					<template #label>File Type</template>
@@ -25,18 +33,19 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 				<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>
@@ -50,7 +59,9 @@ 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';
@@ -62,22 +73,80 @@ 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);
 const order = ref(false);
 const filetype = ref(null);
 
-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() {
@@ -85,22 +154,54 @@ async function search() {
 
 	if (query == null || query === '') return;
 
-	if (query.startsWith('http://') || query.startsWith('https://')) {
-		const promise = misskeyApi('ap/show', {
-			uri: query,
+	//#region AP lookup
+	if (query.startsWith('http://') || 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 = {
@@ -109,13 +210,51 @@ async function search() {
 		params: {
 			query: searchQuery.value,
 			userId: user.value ? user.value.id : null,
+			...(searchHost.value ? { host: searchHost.value } : {}),
 			order: order.value ? 'desc' : 'asc',
 			filetype: filetype.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(--fgTransparent);
+	padding: 12px;
+	margin-right: 16px;
+}
+.addUserButton {
+  border: 2px dashed var(--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 8dda3b5b02..a355c0eeaa 100644
--- a/packages/frontend/src/pages/search.user.vue
+++ b/packages/frontend/src/pages/search.user.vue
@@ -6,8 +6,8 @@ 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">
-			<template #prefix><i class="ph-magnifying-glass ph-bold ph-lg"></i></template>
+		<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()">
 			<option value="combined">{{ i18n.ts.all }}</option>
@@ -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('http://') || query.startsWith('https://')) {
-		const promise = misskeyApi('ap/show', {
-			uri: query,
+	//#region AP lookup
+	if (query.startsWith('http://') || 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;
+			}
+		}
 	}
 
 	if (query.match(/^@[a-z0-9_.-]+@[a-z0-9_.-]+$/i)) {
diff --git a/packages/frontend/src/pages/search.vue b/packages/frontend/src/pages/search.vue
index fe56297c65..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,42 +18,58 @@ 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(() => []);
 
 const headerTabs = computed(() => [{
 	key: 'note',
 	title: i18n.ts.notes,
-	icon: 'ph-pencil-simple ph-bold ph-lg',
+	icon: 'ti ti-pencil',
 }, {
 	key: 'user',
 	title: i18n.ts.users,
-	icon: 'ph-users ph-bold ph-lg',
+	icon: 'ti ti-users',
 }]);
 
 definePageMetadata(() => ({
 	title: i18n.ts.search,
-	icon: 'ph-magnifying-glass ph-bold ph-lg',
+	icon: 'ti ti-search',
 }));
 </script>
diff --git a/packages/frontend/src/pages/settings/2fa.qrdialog.vue b/packages/frontend/src/pages/settings/2fa.qrdialog.vue
index 857e28e1a1..2244047b31 100644
--- a/packages/frontend/src/pages/settings/2fa.qrdialog.vue
+++ b/packages/frontend/src/pages/settings/2fa.qrdialog.vue
@@ -48,7 +48,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 						</div>
 						<div class="_buttonsCenter" style="margin-top: 16px;">
 							<MkButton rounded @click="cancel">{{ i18n.ts.cancel }}</MkButton>
-							<MkButton primary rounded gradate @click="page++">{{ i18n.ts.continue }} <i class="ph-arrow-right ph-bold ph-lg"></i></MkButton>
+							<MkButton primary rounded gradate @click="page++">{{ i18n.ts.continue }} <i class="ti ti-arrow-right"></i></MkButton>
 						</div>
 					</MkSpacer>
 				</div>
@@ -62,8 +62,8 @@ SPDX-License-Identifier: AGPL-3.0-only
 							<div>{{ i18n.ts._2fa.step3 }}</div>
 						</div>
 						<div class="_buttonsCenter" style="margin-top: 16px;">
-							<MkButton rounded @click="page--"><i class="ph-arrow-left ph-bold ph-lg"></i> {{ i18n.ts.goBack }}</MkButton>
-							<MkButton primary rounded gradate @click="tokenDone">{{ i18n.ts.continue }} <i class="ph-arrow-right ph-bold ph-lg"></i></MkButton>
+							<MkButton rounded @click="page--"><i class="ti ti-arrow-left"></i> {{ i18n.ts.goBack }}</MkButton>
+							<MkButton primary rounded gradate @click="tokenDone">{{ i18n.ts.continue }} <i class="ti ti-arrow-right"></i></MkButton>
 						</div>
 					</MkSpacer>
 				</div>
@@ -77,7 +77,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 							<div style="text-align: center; font-weight: bold;">{{ i18n.ts._2fa.checkBackupCodesBeforeCloseThisWizard }}</div>
 
 							<MkFolder :defaultOpen="true">
-								<template #icon><i class="ph-key ph-bold ph-lg"></i></template>
+								<template #icon><i class="ti ti-key"></i></template>
 								<template #label>{{ i18n.ts._2fa.backupCodes }}</template>
 
 								<div class="_gaps">
@@ -90,7 +90,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 										</MkKeyValue>
 									</div>
 
-									<MkButton primary rounded gradate @click="downloadBackupCodes"><i class="ph-download ph-bold ph-lg"></i> {{ i18n.ts.download }}</MkButton>
+									<MkButton primary rounded gradate @click="downloadBackupCodes"><i class="ti ti-download"></i> {{ i18n.ts.download }}</MkButton>
 								</div>
 							</MkFolder>
 						</div>
diff --git a/packages/frontend/src/pages/settings/2fa.vue b/packages/frontend/src/pages/settings/2fa.vue
index 0396a3b085..6a9a1e16e2 100644
--- a/packages/frontend/src/pages/settings/2fa.vue
+++ b/packages/frontend/src/pages/settings/2fa.vue
@@ -16,10 +16,10 @@ SPDX-License-Identifier: AGPL-3.0-only
 		</MkInfo>
 
 		<MkFolder :defaultOpen="true">
-			<template #icon><i class="ph-shield ph-bold ph-lg-lock"></i></template>
+			<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="ph-check ph-bold ph-lg" style="color: var(--success)"></i></template>
+			<template #suffix><i v-if="$i.twoFactorEnabled" class="ti ti-check" style="color: var(--success)"></i></template>
 
 			<div v-if="$i.twoFactorEnabled" class="_gaps_s">
 				<div v-text="i18n.ts._2fa.alreadyRegistered"/>
@@ -32,12 +32,12 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 			<div v-else-if="!$i.twoFactorEnabled" class="_gaps_s">
 				<MkButton primary gradate @click="registerTOTP">{{ i18n.ts._2fa.registerTOTP }}</MkButton>
-				<MkLink url="https://misskey-hub.net/docs/for-users/stepped-guides/how-to-enable-2fa/" target="_blank"><i class="ph-question ph-bold ph-lg"></i> {{ i18n.ts.learnMore }}</MkLink>
+				<MkLink url="https://misskey-hub.net/docs/for-users/stepped-guides/how-to-enable-2fa/" target="_blank"><i class="ti ti-help-circle"></i> {{ i18n.ts.learnMore }}</MkLink>
 			</div>
 		</MkFolder>
 
 		<MkFolder>
-			<template #icon><i class="ph-key ph-bold ph-lg"></i></template>
+			<template #icon><i class="ti ti-key"></i></template>
 			<template #label>{{ i18n.ts.securityKeyAndPasskey }}</template>
 			<div class="_gaps_s">
 				<MkInfo>
@@ -58,8 +58,8 @@ SPDX-License-Identifier: AGPL-3.0-only
 						<template #label>{{ key.name }}</template>
 						<template #suffix><I18n :src="i18n.ts.lastUsedAt"><template #t><MkTime :time="key.lastUsed"/></template></I18n></template>
 						<div class="_buttons">
-							<MkButton @click="renameKey(key)"><i class="ph-textbox ph-bold ph-lg"></i> {{ i18n.ts.rename }}</MkButton>
-							<MkButton danger @click="unregisterKey(key)"><i class="ph-trash ph-bold ph-lg"></i> {{ i18n.ts.unregister }}</MkButton>
+							<MkButton @click="renameKey(key)"><i class="ti ti-forms"></i> {{ i18n.ts.rename }}</MkButton>
+							<MkButton danger @click="unregisterKey(key)"><i class="ti ti-trash"></i> {{ i18n.ts.unregister }}</MkButton>
 						</div>
 					</MkFolder>
 				</template>
@@ -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> {
diff --git a/packages/frontend/src/pages/settings/accounts.vue b/packages/frontend/src/pages/settings/accounts.vue
index f5effbd68b..08c9261dcf 100644
--- a/packages/frontend/src/pages/settings/accounts.vue
+++ b/packages/frontend/src/pages/settings/accounts.vue
@@ -8,8 +8,8 @@ SPDX-License-Identifier: AGPL-3.0-only
 	<FormSuspense :p="init">
 		<div class="_gaps">
 			<div class="_buttons">
-				<MkButton primary @click="addAccount"><i class="ph-plus ph-bold ph-lg"></i> {{ i18n.ts.addAccount }}</MkButton>
-				<MkButton @click="init"><i class="ph-arrows-counter-clockwise ph-bold ph-lg"></i> {{ i18n.ts.reloadAccountsList }}</MkButton>
+				<MkButton primary @click="addAccount"><i class="ti ti-plus"></i> {{ i18n.ts.addAccount }}</MkButton>
+				<MkButton @click="init"><i class="ti ti-refresh"></i> {{ i18n.ts.reloadAccountsList }}</MkButton>
 			</div>
 
 			<MkUserCardMini v-for="user in accounts" :key="user.id" :user="user" :class="$style.user" @click.prevent="menu(user, $event)"/>
@@ -48,11 +48,11 @@ const init = async () => {
 function menu(account, ev) {
 	os.popupMenu([{
 		text: i18n.ts.switch,
-		icon: 'ph-arrows-left-right ph-bold ph-lg',
+		icon: 'ti ti-switch-horizontal',
 		action: () => switchAccount(account),
 	}, {
 		text: i18n.ts.logout,
-		icon: 'ph-trash ph-bold ph-lg',
+		icon: 'ti ti-trash',
 		danger: true,
 		action: () => removeAccount(account),
 	}], ev.currentTarget ?? ev.target);
@@ -74,22 +74,24 @@ async function removeAccount(account) {
 }
 
 function addExistingAccount() {
-	os.popup(defineAsyncComponent(() => import('@/components/MkSigninDialog.vue')), {}, {
+	const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkSigninDialog.vue')), {}, {
 		done: async res => {
 			await addAccounts(res.id, res.i);
 			os.success();
 			init();
 		},
-	}, 'closed');
+		closed: () => dispose(),
+	});
 }
 
 function createAccount() {
-	os.popup(defineAsyncComponent(() => import('@/components/MkSignupDialog.vue')), {}, {
+	const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkSignupDialog.vue')), {}, {
 		done: async res => {
 			await addAccounts(res.id, res.i);
 			switchAccountWithToken(res.i);
 		},
-	}, 'closed');
+		closed: () => dispose(),
+	});
 }
 
 async function switchAccount(account: any) {
@@ -108,7 +110,7 @@ const headerTabs = computed(() => []);
 
 definePageMetadata(() => ({
 	title: i18n.ts.accounts,
-	icon: 'ph-users ph-bold ph-lg',
+	icon: 'ti ti-users',
 }));
 </script>
 
diff --git a/packages/frontend/src/pages/settings/api.vue b/packages/frontend/src/pages/settings/api.vue
index f8f340d602..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(() => []);
@@ -47,6 +48,6 @@ const headerTabs = computed(() => []);
 
 definePageMetadata(() => ({
 	title: 'API',
-	icon: 'ph-webhooks-logo ph-bold ph-lg',
+	icon: 'ti ti-api',
 }));
 </script>
diff --git a/packages/frontend/src/pages/settings/apps.vue b/packages/frontend/src/pages/settings/apps.vue
index abdb5d1cdd..c58ea5d378 100644
--- a/packages/frontend/src/pages/settings/apps.vue
+++ b/packages/frontend/src/pages/settings/apps.vue
@@ -34,7 +34,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 							</ul>
 						</details>
 						<div>
-							<MkButton inline danger @click="revoke(token)"><i class="ph-trash ph-bold ph-lg"></i></MkButton>
+							<MkButton inline danger @click="revoke(token)"><i class="ti ti-trash"></i></MkButton>
 						</div>
 					</div>
 				</div>
@@ -77,7 +77,7 @@ const headerTabs = computed(() => []);
 
 definePageMetadata(() => ({
 	title: i18n.ts.installedApps,
-	icon: 'ph-plug ph-bold ph-lg',
+	icon: 'ti ti-plug',
 }));
 </script>
 
diff --git a/packages/frontend/src/pages/settings/avatar-decoration.decoration.vue b/packages/frontend/src/pages/settings/avatar-decoration.decoration.vue
index 1b731ff624..0767fa7864 100644
--- a/packages/frontend/src/pages/settings/avatar-decoration.decoration.vue
+++ b/packages/frontend/src/pages/settings/avatar-decoration.decoration.vue
@@ -10,7 +10,7 @@ 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="ph-lock ph-bold ph-lg"></i>
+	<i v-if="decoration.roleIdsThatCanBeUsedThisDecoration.length > 0 && !$i.roles.some(r => decoration.roleIdsThatCanBeUsedThisDecoration.includes(r.id))" :class="$style.lock" class="ti ti-lock"></i>
 </div>
 </template>
 
diff --git a/packages/frontend/src/pages/settings/avatar-decoration.dialog.vue b/packages/frontend/src/pages/settings/avatar-decoration.dialog.vue
index 327e0ef723..ce1d4e48d8 100644
--- a/packages/frontend/src/pages/settings/avatar-decoration.dialog.vue
+++ b/packages/frontend/src/pages/settings/avatar-decoration.dialog.vue
@@ -36,9 +36,9 @@ SPDX-License-Identifier: AGPL-3.0-only
 		</MkSpacer>
 
 		<div :class="$style.footer" class="_buttonsCenter">
-			<MkButton v-if="usingIndex != null" primary rounded @click="update"><i class="ph-check ph-bold ph-lg"></i> {{ i18n.ts.update }}</MkButton>
-			<MkButton v-if="usingIndex != null" rounded @click="detach"><i class="ph-x ph-bold ph-lg"></i> {{ i18n.ts.detach }}</MkButton>
-			<MkButton v-else :disabled="exceeded" primary rounded @click="attach"><i class="ph-check ph-bold ph-lg"></i> {{ i18n.ts.attach }}</MkButton>
+			<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>
 		</div>
 	</div>
 </MkModalWindow>
diff --git a/packages/frontend/src/pages/settings/avatar-decoration.vue b/packages/frontend/src/pages/settings/avatar-decoration.vue
index a60d7209cf..77229d3349 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() {
@@ -130,7 +131,7 @@ const headerTabs = computed(() => []);
 
 definePageMetadata(() => ({
 	title: i18n.ts.avatarDecorations,
-	icon: 'ph-sparkle ph-bold ph-lg',
+	icon: 'ti ti-sparkles',
 }));
 </script>
 
diff --git a/packages/frontend/src/pages/settings/custom-css.vue b/packages/frontend/src/pages/settings/custom-css.vue
index 59733e896f..cf05e75acc 100644
--- a/packages/frontend/src/pages/settings/custom-css.vue
+++ b/packages/frontend/src/pages/settings/custom-css.vue
@@ -47,6 +47,6 @@ const headerTabs = computed(() => []);
 
 definePageMetadata(() => ({
 	title: i18n.ts.customCss,
-	icon: 'ph-code ph-bold ph-lg',
+	icon: 'ti ti-code',
 }));
 </script>
diff --git a/packages/frontend/src/pages/settings/deck.vue b/packages/frontend/src/pages/settings/deck.vue
index 81ae9bc2f7..e574ec7dc0 100644
--- a/packages/frontend/src/pages/settings/deck.vue
+++ b/packages/frontend/src/pages/settings/deck.vue
@@ -38,6 +38,6 @@ const headerTabs = computed(() => []);
 
 definePageMetadata(() => ({
 	title: i18n.ts.deck,
-	icon: 'ph-text-columns ph-bold ph-lg',
+	icon: 'ti ti-columns',
 }));
 </script>
diff --git a/packages/frontend/src/pages/settings/drive-cleaner.vue b/packages/frontend/src/pages/settings/drive-cleaner.vue
index fef12fee06..3f7db1b779 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(),
 	};
 }
 
@@ -119,7 +119,7 @@ function onContextMenu(ev: MouseEvent, file): void {
 
 definePageMetadata(() => ({
 	title: i18n.ts.drivecleaner,
-	icon: 'ph-trash ph-bold ph-lg',
+	icon: 'ti ti-trash',
 }));
 </script>
 
diff --git a/packages/frontend/src/pages/settings/drive.vue b/packages/frontend/src/pages/settings/drive.vue
index 06de4163b6..fa09637844 100644
--- a/packages/frontend/src/pages/settings/drive.vue
+++ b/packages/frontend/src/pages/settings/drive.vue
@@ -35,7 +35,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 			<FormLink to="" @click="chooseUploadFolder()">
 				{{ i18n.ts.uploadFolder }}
 				<template #suffix>{{ uploadFolder ? uploadFolder.name : '-' }}</template>
-				<template #suffixIcon><i class="ph-folder ph-bold ph-lg"></i></template>
+				<template #suffixIcon><i class="ti ti-folder"></i></template>
 			</FormLink>
 			<FormLink to="/settings/drive/cleaner">
 				{{ i18n.ts.drivecleaner }}
@@ -90,7 +90,7 @@ const meterStyle = computed(() => {
 			h: 180 - (usage.value / capacity.value * 180),
 			s: 0.7,
 			l: 0.5,
-		}),
+		}).toHslString(),
 	};
 });
 
@@ -144,7 +144,7 @@ const headerTabs = computed(() => []);
 
 definePageMetadata(() => ({
 	title: i18n.ts.drive,
-	icon: 'ph-cloud ph-bold ph-lg',
+	icon: 'ti ti-cloud',
 }));
 </script>
 
diff --git a/packages/frontend/src/pages/settings/email.vue b/packages/frontend/src/pages/settings/email.vue
index 938abb0651..f226647569 100644
--- a/packages/frontend/src/pages/settings/email.vue
+++ b/packages/frontend/src/pages/settings/email.vue
@@ -8,9 +8,9 @@ SPDX-License-Identifier: AGPL-3.0-only
 	<FormSection first>
 		<template #label>{{ i18n.ts.emailAddress }}</template>
 		<MkInput v-model="emailAddress" type="email" manualSave>
-			<template #prefix><i class="ph-envelope ph-bold ph-lg"></i></template>
+			<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="ph-check ph-bold ph-lg" 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(--success);"></i> {{ i18n.ts.emailVerified }}</template>
 		</MkInput>
 	</FormSection>
 
@@ -115,6 +115,6 @@ const headerTabs = computed(() => []);
 
 definePageMetadata(() => ({
 	title: i18n.ts.email,
-	icon: 'ph-envelope ph-bold ph-lg',
+	icon: 'ti ti-mail',
 }));
 </script>
diff --git a/packages/frontend/src/pages/settings/emoji-picker.vue b/packages/frontend/src/pages/settings/emoji-picker.vue
index e9936ca5f2..1df723c850 100644
--- a/packages/frontend/src/pages/settings/emoji-picker.vue
+++ b/packages/frontend/src/pages/settings/emoji-picker.vue
@@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 <template>
 <div class="_gaps_m">
 	<MkFolder :defaultOpen="true">
-		<template #icon><i class="ph-pin ph-bold ph-lg"></i></template>
+		<template #icon><i class="ti ti-pin"></i></template>
 		<template #label>{{ i18n.ts.pinned }} ({{ i18n.ts.reaction }})</template>
 		<template #caption>{{ i18n.ts.pinnedEmojisForReactionSettingDescription }}</template>
 
@@ -29,7 +29,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 						</template>
 						<template #footer>
 							<button class="_button" :class="$style.emojisAdd" @click="chooseReaction">
-								<i class="ph-plus ph-bold ph-lg"></i>
+								<i class="ti ti-plus"></i>
 							</button>
 						</template>
 					</Sortable>
@@ -38,15 +38,15 @@ SPDX-License-Identifier: AGPL-3.0-only
 			</div>
 
 			<div class="_buttons">
-				<MkButton inline @click="previewReaction"><i class="ph-eye ph-bold ph-lg"></i> {{ i18n.ts.preview }}</MkButton>
-				<MkButton inline danger @click="setDefaultReaction"><i class="ph-arrow-counter-clockwise ph-bold ph-lg"></i> {{ i18n.ts.default }}</MkButton>
-				<MkButton inline danger @click="overwriteFromPinnedEmojis"><i class="ph-copy ph-bold ph-lg"></i> {{ i18n.ts.overwriteFromPinnedEmojis }}</MkButton>
+				<MkButton inline @click="previewReaction"><i class="ti ti-eye"></i> {{ i18n.ts.preview }}</MkButton>
+				<MkButton inline danger @click="setDefaultReaction"><i class="ti ti-reload"></i> {{ i18n.ts.default }}</MkButton>
+				<MkButton inline danger @click="overwriteFromPinnedEmojis"><i class="ti ti-copy"></i> {{ i18n.ts.overwriteFromPinnedEmojis }}</MkButton>
 			</div>
 		</div>
 	</MkFolder>
 
 	<MkFolder>
-		<template #icon><i class="ph-pin ph-bold ph-lg"></i></template>
+		<template #icon><i class="ti ti-pin"></i></template>
 		<template #label>{{ i18n.ts.pinned }} ({{ i18n.ts.general }})</template>
 		<template #caption>{{ i18n.ts.pinnedEmojisSettingDescription }}</template>
 
@@ -69,7 +69,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 						</template>
 						<template #footer>
 							<button class="_button" :class="$style.emojisAdd" @click="chooseEmoji">
-								<i class="ph-plus ph-bold ph-lg"></i>
+								<i class="ti ti-plus"></i>
 							</button>
 						</template>
 					</Sortable>
@@ -78,9 +78,9 @@ SPDX-License-Identifier: AGPL-3.0-only
 			</div>
 
 			<div class="_buttons">
-				<MkButton inline @click="previewEmoji"><i class="ph-eye ph-bold ph-lg"></i> {{ i18n.ts.preview }}</MkButton>
-				<MkButton inline danger @click="setDefaultEmoji"><i class="ph-arrow-counter-clockwise ph-bold ph-lg"></i> {{ i18n.ts.default }}</MkButton>
-				<MkButton inline danger @click="overwriteFromPinnedEmojisForReaction"><i class="ph-copy ph-bold ph-lg"></i> {{ i18n.ts.overwriteFromPinnedEmojisForReaction }}</MkButton>
+				<MkButton inline @click="previewEmoji"><i class="ti ti-eye"></i> {{ i18n.ts.preview }}</MkButton>
+				<MkButton inline danger @click="setDefaultEmoji"><i class="ti ti-reload"></i> {{ i18n.ts.default }}</MkButton>
+				<MkButton inline danger @click="overwriteFromPinnedEmojisForReaction"><i class="ti ti-copy"></i> {{ i18n.ts.overwriteFromPinnedEmojisForReaction }}</MkButton>
 			</div>
 		</div>
 	</MkFolder>
@@ -278,7 +278,7 @@ watch(pinnedEmojis, () => {
 
 definePageMetadata(() => ({
 	title: i18n.ts.emojiPicker,
-	icon: 'ph-smiley ph-bold ph-lg',
+	icon: 'ti ti-mood-happy',
 }));
 </script>
 
diff --git a/packages/frontend/src/pages/settings/general.vue b/packages/frontend/src/pages/settings/general.vue
index eef69f694a..75afe67be7 100644
--- a/packages/frontend/src/pages/settings/general.vue
+++ b/packages/frontend/src/pages/settings/general.vue
@@ -27,9 +27,9 @@ SPDX-License-Identifier: AGPL-3.0-only
 	<MkRadios v-model="overridedDeviceKind">
 		<template #label>{{ i18n.ts.overridedDeviceKind }}</template>
 		<option :value="null">{{ i18n.ts.auto }}</option>
-		<option value="smartphone"><i class="ph-device-mobile ph-bold ph-lg"/> {{ i18n.ts.smartphone }}</option>
-		<option value="tablet"><i class="ph-device-tablet ph-bold ph-lg"/> {{ i18n.ts.tablet }}</option>
-		<option value="desktop"><i class="ph-desktop ph-bold ph-lg"/> {{ i18n.ts.desktop }}</option>
+		<option value="smartphone"><i class="ti ti-device-mobile"/> {{ i18n.ts.smartphone }}</option>
+		<option value="tablet"><i class="ti ti-device-tablet"/> {{ i18n.ts.tablet }}</option>
+		<option value="desktop"><i class="ti ti-device-desktop"/> {{ i18n.ts.desktop }}</option>
 	</MkRadios>
 
 	<FormSection>
@@ -40,7 +40,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 				<template #label>{{ i18n.ts.pinnedList }}</template>
 				<!-- 複数ピン止め管理できるようにしたいけどめんどいので一旦ひとつのみ -->
 				<MkButton v-if="defaultStore.reactiveState.pinnedUserLists.value.length === 0" @click="setPinnedList()">{{ i18n.ts.add }}</MkButton>
-				<MkButton v-else danger @click="removePinnedList()"><i class="ph-trash ph-bold ph-lg"></i> {{ i18n.ts.remove }}</MkButton>
+				<MkButton v-else danger @click="removePinnedList()"><i class="ti ti-trash"></i> {{ i18n.ts.remove }}</MkButton>
 			</MkFolder>
 		</div>
 	</FormSection>
@@ -98,8 +98,8 @@ SPDX-License-Identifier: AGPL-3.0-only
 				</MkRadios>
 				<MkRadios v-model="noteDesign">
 					<template #label>Note Design</template>
-					<option value="sharkey"><i class="sk-icons sk-shark ph-bold" style="top: 2px;position: relative;"></i> Sharkey</option>
-					<option value="misskey"><i class="sk-icons sk-misskey ph-bold" style="top: 2px;position: relative;"></i> Misskey</option>
+					<option value="sharkey"><i class="sk-icons sk-shark sk-icons-lg" style="top: 2px;position: relative;"></i> Sharkey</option>
+					<option value="misskey"><i class="sk-icons sk-misskey sk-icons-lg" style="top: 2px;position: relative;"></i> Misskey</option>
 				</MkRadios>
 				<MkSwitch v-model="limitWidthOfReaction">{{ i18n.ts.limitWidthOfReaction }}</MkSwitch>
 			</div>
@@ -157,16 +157,16 @@ SPDX-License-Identifier: AGPL-3.0-only
 			<!-- <p class="caption">Testing Testing</p> -->
 			<MkRadios v-model="notificationPosition">
 				<template #label>{{ i18n.ts.position }}</template>
-				<option value="leftTop"><i class="ph-arrow-up-left ph-bold ph-lg"></i> {{ i18n.ts.leftTop }}</option>
-				<option value="rightTop"><i class="ph-arrow-up-right ph-bold ph-lg"></i> {{ i18n.ts.rightTop }}</option>
-				<option value="leftBottom"><i class="ph-arrow-down-left ph-bold ph-lg"></i> {{ i18n.ts.leftBottom }}</option>
-				<option value="rightBottom"><i class="ph-arrow-down-right ph-bold ph-lg"></i> {{ i18n.ts.rightBottom }}</option>
+				<option value="leftTop"><i class="ti ti-align-box-left-top"></i> {{ i18n.ts.leftTop }}</option>
+				<option value="rightTop"><i class="ti ti-align-box-right-top"></i> {{ i18n.ts.rightTop }}</option>
+				<option value="leftBottom"><i class="ti ti-align-box-left-bottom"></i> {{ i18n.ts.leftBottom }}</option>
+				<option value="rightBottom"><i class="ti ti-align-box-right-bottom"></i> {{ i18n.ts.rightBottom }}</option>
 			</MkRadios>
 
 			<MkRadios v-model="notificationStackAxis">
 				<template #label>{{ i18n.ts.stackAxis }}</template>
-				<option value="vertical"><i class="ph-split-vertical ph-bold ph-lg"></i> {{ i18n.ts.vertical }}</option>
-				<option value="horizontal"><i class="ph-split-horizontal ph-bold ph-lg"></i> {{ i18n.ts.horizontal }}</option>
+				<option value="vertical"><i class="ti ti-carousel-vertical"></i> {{ i18n.ts.vertical }}</option>
+				<option value="horizontal"><i class="ti ti-carousel-horizontal"></i> {{ i18n.ts.horizontal }}</option>
 			</MkRadios>
 
 			<MkButton @click="testNotification">{{ i18n.ts._notification.checkNotificationBehavior }}</MkButton>
@@ -213,8 +213,8 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 			<MkRadios v-model="cornerRadius">
 				<template #label>{{ i18n.ts.cornerRadius }}</template>
-				<option :value="null"><i class="sk-icons sk-shark ph-bold" style="top: 2px;position: relative;"></i> Sharkey</option>
-				<option value="misskey"><i class="sk-icons sk-misskey ph-bold" style="top: 2px;position: relative;"></i> Misskey</option>
+				<option :value="null"><i class="sk-icons sk-shark sk-icons-lg" style="top: 2px;position: relative;"></i> Sharkey</option>
+				<option value="misskey"><i class="sk-icons sk-misskey sk-icons-lg" style="top: 2px;position: relative;"></i> Misskey</option>
 			</MkRadios>
 		</div>
 	</FormSection>
@@ -234,6 +234,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>
@@ -241,6 +242,12 @@ SPDX-License-Identifier: AGPL-3.0-only
 				<option value="quiet">{{ i18n.ts._serverDisconnectedBehavior.quiet }}</option>
 				<option value="disabled">{{ i18n.ts._serverDisconnectedBehavior.disabled }}</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>
@@ -303,13 +310,13 @@ SPDX-License-Identifier: AGPL-3.0-only
 				<template #label>{{ i18n.ts.additionalEmojiDictionary }}</template>
 				<div class="_buttons">
 					<template v-for="lang in emojiIndexLangs" :key="lang">
-						<MkButton v-if="defaultStore.reactiveState.additionalUnicodeEmojiIndexes.value[lang]" danger @click="removeEmojiIndex(lang)"><i class="ph-trash ph-bold ph-lg"></i> {{ i18n.ts.remove }} ({{ getEmojiIndexLangName(lang) }})</MkButton>
-						<MkButton v-else @click="downloadEmojiIndex(lang)"><i class="ph-download ph-bold ph-lg"></i> {{ getEmojiIndexLangName(lang) }}{{ defaultStore.reactiveState.additionalUnicodeEmojiIndexes.value[lang] ? ` (${ i18n.ts.installed })` : '' }}</MkButton>
+						<MkButton v-if="defaultStore.reactiveState.additionalUnicodeEmojiIndexes.value[lang]" danger @click="removeEmojiIndex(lang)"><i class="ti ti-trash"></i> {{ i18n.ts.remove }} ({{ getEmojiIndexLangName(lang) }})</MkButton>
+						<MkButton v-else @click="downloadEmojiIndex(lang)"><i class="ti ti-download"></i> {{ getEmojiIndexLangName(lang) }}{{ defaultStore.reactiveState.additionalUnicodeEmojiIndexes.value[lang] ? ` (${ i18n.ts.installed })` : '' }}</MkButton>
 					</template>
 				</div>
 			</MkFolder>
 			<FormLink to="/settings/deck">{{ i18n.ts.deck }}</FormLink>
-			<FormLink to="/settings/custom-css"><template #icon><i class="ph-code ph-bold ph-lg"></i></template>{{ i18n.ts.customCss }}</FormLink>
+			<FormLink to="/settings/custom-css"><template #icon><i class="ti ti-code"></i></template>{{ i18n.ts.customCss }}</FormLink>
 		</div>
 	</FormSection>
 </div>
@@ -426,6 +433,8 @@ const visibilityOnBoost = computed(defaultStore.makeGetterSetter('visibilityOnBo
 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);
@@ -485,6 +494,8 @@ watch([
 	showVisibilitySelectorOnBoost,
 	visibilityOnBoost,
 	alwaysConfirmFollow,
+	confirmWhenRevealingSensitiveMedia,
+	contextMenu,
 ], async () => {
 	await reloadAsk();
 });
@@ -579,7 +590,7 @@ function testNotification(): void {
 
 async function testNotificationDot() {
 	const success = await worksOnInstance();
-	
+
 	if (success) {
 		os.toast(i18n.ts.notificationDotWorking);
 	} else {
@@ -615,7 +626,7 @@ const headerTabs = computed(() => []);
 
 definePageMetadata(() => ({
 	title: i18n.ts.general,
-	icon: 'ph-faders ph-bold ph-lg',
+	icon: 'ti ti-adjustments',
 }));
 
 const useCustomSearchEngine = computed(() => !Object.keys(searchEngineMap).includes(searchEngine.value));
diff --git a/packages/frontend/src/pages/settings/import-export.vue b/packages/frontend/src/pages/settings/import-export.vue
index 87bde70fc2..9c02b604c0 100644
--- a/packages/frontend/src/pages/settings/import-export.vue
+++ b/packages/frontend/src/pages/settings/import-export.vue
@@ -6,12 +6,12 @@ SPDX-License-Identifier: AGPL-3.0-only
 <template>
 <div class="_gaps_m">
 	<FormSection first>
-		<template #label><i class="ph-pencil-simple ph-bold ph-lg"></i> {{ i18n.ts._exportOrImport.allNotes }}</template>
+		<template #label><i class="ti ti-pencil"></i> {{ i18n.ts._exportOrImport.allNotes }}</template>
 		<div class="_gaps_s">
 			<MkFolder>
 				<template #label>{{ i18n.ts.export }}</template>
-				<template #icon><i class="ph-download ph-bold ph-lg"></i></template>
-				<MkButton primary :class="$style.button" inline @click="exportNotes()"><i class="ph-download ph-bold ph-lg"></i> {{ i18n.ts.export }}</MkButton>
+				<template #icon><i class="ti ti-download"></i></template>
+				<MkButton primary :class="$style.button" inline @click="exportNotes()"><i class="ti ti-download"></i> {{ i18n.ts.export }}</MkButton>
 			</MkFolder>
 			<MkFolder v-if="$i && $i.policies.canImportNotes">
 				<template #label>{{ i18n.ts.import }}</template>
@@ -29,27 +29,27 @@ SPDX-License-Identifier: AGPL-3.0-only
 		</div>
 	</FormSection>
 	<FormSection>
-		<template #label><i class="ph-star ph-bold ph-lg"></i> {{ i18n.ts._exportOrImport.favoritedNotes }}</template>
+		<template #label><i class="ti ti-star"></i> {{ i18n.ts._exportOrImport.favoritedNotes }}</template>
 		<MkFolder>
 			<template #label>{{ i18n.ts.export }}</template>
-			<template #icon><i class="ph-download ph-bold ph-lg"></i></template>
-			<MkButton primary :class="$style.button" inline @click="exportFavorites()"><i class="ph-download ph-bold ph-lg"></i> {{ i18n.ts.export }}</MkButton>
+			<template #icon><i class="ti ti-download"></i></template>
+			<MkButton primary :class="$style.button" inline @click="exportFavorites()"><i class="ti ti-download"></i> {{ i18n.ts.export }}</MkButton>
 		</MkFolder>
 	</FormSection>
 	<FormSection>
 		<template #label><i class="ph-paperclip ph-bold ph-lg"></i> {{ i18n.ts._exportOrImport.clips }}</template>
 		<MkFolder>
 			<template #label>{{ i18n.ts.export }}</template>
-			<template #icon><i class="ph-download ph-bold ph-lg"></i></template>
-			<MkButton primary :class="$style.button" inline @click="exportClips()"><i class="ph-download ph-bold ph-lg"></i> {{ i18n.ts.export }}</MkButton>
+			<template #icon><i class="ti ti-download"></i></template>
+			<MkButton primary :class="$style.button" inline @click="exportClips()"><i class="ti ti-download"></i> {{ i18n.ts.export }}</MkButton>
 		</MkFolder>
 	</FormSection>
 	<FormSection>
-		<template #label><i class="ph-users ph-bold ph-lg"></i> {{ i18n.ts._exportOrImport.followingList }}</template>
+		<template #label><i class="ti ti-users"></i> {{ i18n.ts._exportOrImport.followingList }}</template>
 		<div class="_gaps_s">
 			<MkFolder>
 				<template #label>{{ i18n.ts.export }}</template>
-				<template #icon><i class="ph-download ph-bold ph-lg"></i></template>
+				<template #icon><i class="ti ti-download"></i></template>
 				<div class="_gaps_s">
 					<MkSwitch v-model="excludeMutingUsers">
 						{{ i18n.ts._exportOrImport.excludeMutingUsers }}
@@ -57,76 +57,76 @@ SPDX-License-Identifier: AGPL-3.0-only
 					<MkSwitch v-model="excludeInactiveUsers">
 						{{ i18n.ts._exportOrImport.excludeInactiveUsers }}
 					</MkSwitch>
-					<MkButton primary :class="$style.button" inline @click="exportFollowing()"><i class="ph-download ph-bold ph-lg"></i> {{ i18n.ts.export }}</MkButton>
+					<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">
 				<template #label>{{ i18n.ts.import }}</template>
-				<template #icon><i class="ph-upload ph-bold ph-lg"></i></template>
+				<template #icon><i class="ti ti-upload"></i></template>
 				<MkSwitch v-model="withReplies">
 					{{ i18n.ts._exportOrImport.withReplies }}
 				</MkSwitch>
-				<MkButton primary :class="$style.button" inline @click="importFollowing($event)"><i class="ph-upload ph-bold ph-lg"></i> {{ i18n.ts.import }}</MkButton>
+				<MkButton primary :class="$style.button" inline @click="importFollowing($event)"><i class="ti ti-upload"></i> {{ i18n.ts.import }}</MkButton>
 			</MkFolder>
 		</div>
 	</FormSection>
 	<FormSection>
-		<template #label><i class="ph-users ph-bold ph-lg"></i> {{ i18n.ts._exportOrImport.userLists }}</template>
+		<template #label><i class="ti ti-users"></i> {{ i18n.ts._exportOrImport.userLists }}</template>
 		<div class="_gaps_s">
 			<MkFolder>
 				<template #label>{{ i18n.ts.export }}</template>
-				<template #icon><i class="ph-download ph-bold ph-lg"></i></template>
-				<MkButton primary :class="$style.button" inline @click="exportUserLists()"><i class="ph-download ph-bold ph-lg"></i> {{ i18n.ts.export }}</MkButton>
+				<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">
 				<template #label>{{ i18n.ts.import }}</template>
-				<template #icon><i class="ph-upload ph-bold ph-lg"></i></template>
-				<MkButton primary :class="$style.button" inline @click="importUserLists($event)"><i class="ph-upload ph-bold ph-lg"></i> {{ i18n.ts.import }}</MkButton>
+				<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>
 			</MkFolder>
 		</div>
 	</FormSection>
 	<FormSection>
-		<template #label><i class="ph-user-minus ph-bold ph-lg"></i> {{ i18n.ts._exportOrImport.muteList }}</template>
+		<template #label><i class="ti ti-user-off"></i> {{ i18n.ts._exportOrImport.muteList }}</template>
 		<div class="_gaps_s">
 			<MkFolder>
 				<template #label>{{ i18n.ts.export }}</template>
-				<template #icon><i class="ph-download ph-bold ph-lg"></i></template>
-				<MkButton primary :class="$style.button" inline @click="exportMuting()"><i class="ph-download ph-bold ph-lg"></i> {{ i18n.ts.export }}</MkButton>
+				<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">
 				<template #label>{{ i18n.ts.import }}</template>
-				<template #icon><i class="ph-upload ph-bold ph-lg"></i></template>
-				<MkButton primary :class="$style.button" inline @click="importMuting($event)"><i class="ph-upload ph-bold ph-lg"></i> {{ i18n.ts.import }}</MkButton>
+				<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>
 			</MkFolder>
 		</div>
 	</FormSection>
 	<FormSection>
-		<template #label><i class="ph-user-minus ph-bold ph-lg"></i> {{ i18n.ts._exportOrImport.blockingList }}</template>
+		<template #label><i class="ti ti-user-off"></i> {{ i18n.ts._exportOrImport.blockingList }}</template>
 		<div class="_gaps_s">
 			<MkFolder>
 				<template #label>{{ i18n.ts.export }}</template>
-				<template #icon><i class="ph-download ph-bold ph-lg"></i></template>
-				<MkButton primary :class="$style.button" inline @click="exportBlocking()"><i class="ph-download ph-bold ph-lg"></i> {{ i18n.ts.export }}</MkButton>
+				<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">
 				<template #label>{{ i18n.ts.import }}</template>
-				<template #icon><i class="ph-upload ph-bold ph-lg"></i></template>
-				<MkButton primary :class="$style.button" inline @click="importBlocking($event)"><i class="ph-upload ph-bold ph-lg"></i> {{ i18n.ts.import }}</MkButton>
+				<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>
 			</MkFolder>
 		</div>
 	</FormSection>
 	<FormSection>
-		<template #label><i class="ph-flying-saucer ph-bold ph-lg"></i> {{ i18n.ts.antennas }}</template>
+		<template #label><i class="ti ti-antenna"></i> {{ i18n.ts.antennas }}</template>
 		<div class="_gaps_s">
 			<MkFolder>
 				<template #label>{{ i18n.ts.export }}</template>
-				<template #icon><i class="ph-download ph-bold ph-lg"></i></template>
-				<MkButton primary :class="$style.button" inline @click="exportAntennas()"><i class="ph-download ph-bold ph-lg"></i> {{ i18n.ts.export }}</MkButton>
+				<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">
 				<template #label>{{ i18n.ts.import }}</template>
-				<template #icon><i class="ph-upload ph-bold ph-lg"></i></template>
-				<MkButton primary :class="$style.button" inline @click="importAntennas($event)"><i class="ph-upload ph-bold ph-lg"></i> {{ i18n.ts.import }}</MkButton>
+				<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>
 			</MkFolder>
 		</div>
 	</FormSection>
@@ -252,7 +252,7 @@ const headerTabs = computed(() => []);
 
 definePageMetadata(() => ({
 	title: i18n.ts.importAndExport,
-	icon: 'ph-package ph-bold ph-lg',
+	icon: 'ti ti-package',
 }));
 </script>
 
diff --git a/packages/frontend/src/pages/settings/index.vue b/packages/frontend/src/pages/settings/index.vue
index 35fb1a03f4..f6f2ffc553 100644
--- a/packages/frontend/src/pages/settings/index.vue
+++ b/packages/frontend/src/pages/settings/index.vue
@@ -40,7 +40,7 @@ import { useRouter } from '@/router/supplier.js';
 
 const indexInfo = {
 	title: i18n.ts.settings,
-	icon: 'ph-gear ph-bold ph-lg',
+	icon: 'ti ti-settings',
 	hideHeader: true,
 };
 const INFO = ref(indexInfo);
@@ -62,37 +62,37 @@ const ro = new ResizeObserver((entries, observer) => {
 const menuDef = computed(() => [{
 	title: i18n.ts.basicSettings,
 	items: [{
-		icon: 'ph-user ph-bold ph-lg',
+		icon: 'ti ti-user',
 		text: i18n.ts.profile,
 		to: '/settings/profile',
 		active: currentPage.value?.route.name === 'profile',
 	}, {
-		icon: 'ph-lock ph-bold ph-lg-open',
+		icon: 'ti ti-lock-open',
 		text: i18n.ts.privacy,
 		to: '/settings/privacy',
 		active: currentPage.value?.route.name === 'privacy',
 	}, {
-		icon: 'ph-smiley ph-bold ph-lg',
+		icon: 'ti ti-mood-happy',
 		text: i18n.ts.emojiPicker,
 		to: '/settings/emoji-picker',
 		active: currentPage.value?.route.name === 'emojiPicker',
 	}, {
-		icon: 'ph-cloud ph-bold ph-lg',
+		icon: 'ti ti-cloud',
 		text: i18n.ts.drive,
 		to: '/settings/drive',
 		active: currentPage.value?.route.name === 'drive',
 	}, {
-		icon: 'ph-bell ph-bold ph-lg',
+		icon: 'ti ti-bell',
 		text: i18n.ts.notifications,
 		to: '/settings/notifications',
 		active: currentPage.value?.route.name === 'notifications',
 	}, {
-		icon: 'ph-envelope ph-bold ph-lg',
+		icon: 'ti ti-mail',
 		text: i18n.ts.email,
 		to: '/settings/email',
 		active: currentPage.value?.route.name === 'email',
 	}, {
-		icon: 'ph-lock ph-bold ph-lg',
+		icon: 'ti ti-lock',
 		text: i18n.ts.security,
 		to: '/settings/security',
 		active: currentPage.value?.route.name === 'security',
@@ -100,32 +100,32 @@ const menuDef = computed(() => [{
 }, {
 	title: i18n.ts.clientSettings,
 	items: [{
-		icon: 'ph-faders ph-bold ph-lg',
+		icon: 'ti ti-adjustments',
 		text: i18n.ts.general,
 		to: '/settings/general',
 		active: currentPage.value?.route.name === 'general',
 	}, {
-		icon: 'ph-palette ph-bold ph-lg',
+		icon: 'ti ti-palette',
 		text: i18n.ts.theme,
 		to: '/settings/theme',
 		active: currentPage.value?.route.name === 'theme',
 	}, {
-		icon: 'ph-list ph-bold ph-lg-2',
+		icon: 'ti ti-menu-2',
 		text: i18n.ts.navbar,
 		to: '/settings/navbar',
 		active: currentPage.value?.route.name === 'navbar',
 	}, {
-		icon: 'ph-equals ph-bold ph-lg',
+		icon: 'ti ti-equal-double',
 		text: i18n.ts.statusbar,
 		to: '/settings/statusbar',
 		active: currentPage.value?.route.name === 'statusbar',
 	}, {
-		icon: 'ph-music-notes ph-bold ph-lg',
+		icon: 'ti ti-music',
 		text: i18n.ts.sounds,
 		to: '/settings/sounds',
 		active: currentPage.value?.route.name === 'sounds',
 	}, {
-		icon: 'ph-plug ph-bold ph-lg',
+		icon: 'ti ti-plug',
 		text: i18n.ts.plugins,
 		to: '/settings/plugin',
 		active: currentPage.value?.route.name === 'plugin',
@@ -133,57 +133,57 @@ const menuDef = computed(() => [{
 }, {
 	title: i18n.ts.otherSettings,
 	items: [{
-		icon: 'ph-seal-check ph-bold ph-lg',
+		icon: 'ti ti-badges',
 		text: i18n.ts.roles,
 		to: '/settings/roles',
 		active: currentPage.value?.route.name === 'roles',
 	}, {
-		icon: 'ph-prohibit ph-bold ph-lg',
+		icon: 'ti ti-ban',
 		text: i18n.ts.muteAndBlock,
 		to: '/settings/mute-block',
 		active: currentPage.value?.route.name === 'mute-block',
 	}, {
-		icon: 'ph-key ph-bold ph-lg',
+		icon: 'ti ti-api',
 		text: 'API',
 		to: '/settings/api',
 		active: currentPage.value?.route.name === 'api',
 	}, {
-		icon: 'ph-webhooks-logo ph-bold ph-lg',
+		icon: 'ti ti-webhook',
 		text: 'Webhook',
 		to: '/settings/webhook',
 		active: currentPage.value?.route.name === 'webhook',
 	}, {
-		icon: 'ph-package ph-bold ph-lg',
+		icon: 'ti ti-package',
 		text: i18n.ts.importAndExport,
 		to: '/settings/import-export',
 		active: currentPage.value?.route.name === 'import-export',
 	}, {
-		icon: 'ph-airplane ph-bold ph-lg',
+		icon: 'ti ti-plane',
 		text: `${i18n.ts.accountMigration}`,
 		to: '/settings/migration',
 		active: currentPage.value?.route.name === 'migration',
 	}, {
-		icon: 'ph-dots-three ph-bold ph-lg',
+		icon: 'ti ti-dots',
 		text: i18n.ts.other,
 		to: '/settings/other',
 		active: currentPage.value?.route.name === 'other',
 	}],
 }, {
 	items: [{
-		icon: 'ph-floppy-disk ph-bold ph-lg',
+		icon: 'ti ti-device-floppy',
 		text: i18n.ts.preferencesBackups,
 		to: '/settings/preferences-backups',
 		active: currentPage.value?.route.name === 'preferences-backups',
 	}, {
 		type: 'button',
-		icon: 'ph-trash ph-bold ph-lg',
+		icon: 'ti ti-trash',
 		text: i18n.ts.clearCache,
 		action: async () => {
 			await clearCache();
 		},
 	}, {
 		type: 'button',
-		icon: 'ph-power ph-bold ph-lg',
+		icon: 'ti ti-power',
 		text: i18n.ts.logout,
 		action: async () => {
 			const { canceled } = await os.confirm({
@@ -197,9 +197,6 @@ const menuDef = computed(() => [{
 	}],
 }]);
 
-watch(narrow, () => {
-});
-
 onMounted(() => {
 	ro.observe(el.value);
 
diff --git a/packages/frontend/src/pages/settings/migration.vue b/packages/frontend/src/pages/settings/migration.vue
index 12f29e2ff8..ddc23945dd 100644
--- a/packages/frontend/src/pages/settings/migration.vue
+++ b/packages/frontend/src/pages/settings/migration.vue
@@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 <template>
 <div class="_gaps_m">
 	<MkFolder :defaultOpen="true">
-		<template #icon><i class="ph-airplane-landing ph-bold ph-lg"></i></template>
+		<template #icon><i class="ti ti-plane-arrival"></i></template>
 		<template #label>{{ i18n.ts._accountMigration.moveFrom }}</template>
 		<template #caption>{{ i18n.ts._accountMigration.moveFromSub }}</template>
 
@@ -15,12 +15,12 @@ SPDX-License-Identifier: AGPL-3.0-only
 				{{ i18n.ts._accountMigration.moveFromDescription }}
 			</FormInfo>
 			<div>
-				<MkButton :disabled="accountAliases.length >= 10" inline style="margin-right: 8px;" @click="add"><i class="ph-plus ph-bold ph-lg"></i> {{ i18n.ts.add }}</MkButton>
-				<MkButton inline primary @click="save"><i class="ph-check ph-bold ph-lg"></i> {{ i18n.ts.save }}</MkButton>
+				<MkButton :disabled="accountAliases.length >= 10" inline style="margin-right: 8px;" @click="add"><i class="ti ti-plus"></i> {{ i18n.ts.add }}</MkButton>
+				<MkButton inline primary @click="save"><i class="ti ti-check"></i> {{ i18n.ts.save }}</MkButton>
 			</div>
 			<div class="_gaps">
 				<MkInput v-for="(_, i) in accountAliases" v-model="accountAliases[i]">
-					<template #prefix><i class="ph-airplane-landing ph-bold ph-lg"></i></template>
+					<template #prefix><i class="ti ti-plane-arrival"></i></template>
 					<template #label>{{ i18n.tsx._accountMigration.moveFromLabel({ n: i + 1 }) }}</template>
 				</MkInput>
 			</div>
@@ -28,7 +28,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 	</MkFolder>
 
 	<MkFolder :defaultOpen="!!$i.movedTo">
-		<template #icon><i class="ph-airplane-takeoff ph-bold ph-lg"></i></template>
+		<template #icon><i class="ti ti-plane-departure"></i></template>
 		<template #label>{{ i18n.ts._accountMigration.moveTo }}</template>
 
 		<div class="_gaps_m">
@@ -39,11 +39,11 @@ SPDX-License-Identifier: AGPL-3.0-only
 				<FormInfo warn>{{ i18n.ts._accountMigration.moveCannotBeUndone }}</FormInfo>
 
 				<MkInput v-model="moveToAccount">
-					<template #prefix><i class="ph-airplane-takeoff ph-bold ph-lg"></i></template>
+					<template #prefix><i class="ti ti-plane-departure"></i></template>
 					<template #label>{{ i18n.ts._accountMigration.moveToLabel }}</template>
 				</MkInput>
 				<MkButton inline danger :disabled="!moveToAccount" @click="move">
-					<i class="ph-check ph-bold ph-lg"></i> {{ i18n.ts._accountMigration.startMigration }}
+					<i class="ti ti-check"></i> {{ i18n.ts._accountMigration.startMigration }}
 				</MkButton>
 			</template>
 			<template v-else-if="$i">
@@ -123,7 +123,7 @@ init();
 
 definePageMetadata(() => ({
 	title: i18n.ts.accountMigration,
-	icon: 'ph-airplane ph-bold ph-lg',
+	icon: 'ti ti-plane',
 }));
 </script>
 
diff --git a/packages/frontend/src/pages/settings/mute-block.instance-mute.vue b/packages/frontend/src/pages/settings/mute-block.instance-mute.vue
index 3b3376a9a7..d1fde2fc1c 100644
--- a/packages/frontend/src/pages/settings/mute-block.instance-mute.vue
+++ b/packages/frontend/src/pages/settings/mute-block.instance-mute.vue
@@ -10,7 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 		<template #label>{{ i18n.ts._instanceMute.heading }}</template>
 		<template #caption>{{ i18n.ts._instanceMute.instanceMuteDescription }}<br>{{ i18n.ts._instanceMute.instanceMuteDescription2 }}</template>
 	</MkTextarea>
-	<MkButton primary :disabled="!changed" @click="save()"><i class="ph-floppy-disk ph-bold ph-lg"></i> {{ i18n.ts.save }}</MkButton>
+	<MkButton primary :disabled="!changed" @click="save()"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton>
 </div>
 </template>
 
diff --git a/packages/frontend/src/pages/settings/mute-block.vue b/packages/frontend/src/pages/settings/mute-block.vue
index 588184826d..a5c381200f 100644
--- a/packages/frontend/src/pages/settings/mute-block.vue
+++ b/packages/frontend/src/pages/settings/mute-block.vue
@@ -20,14 +20,14 @@ SPDX-License-Identifier: AGPL-3.0-only
 	</MkFolder>
 
 	<MkFolder>
-		<template #icon><i class="ph-globe-simple ph-bold ph-lg"></i></template>
+		<template #icon><i class="ti ti-planet-off"></i></template>
 		<template #label>{{ i18n.ts.instanceMute }}</template>
 
 		<XInstanceMute/>
 	</MkFolder>
 
 	<MkFolder>
-		<template #icon><i class="ph-repeat ph-bold ph-lg"></i></template>
+		<template #icon><i class="ti ti-repeat-off"></i></template>
 		<template #label>{{ i18n.ts.mutedUsers }} ({{ i18n.ts.renote }})</template>
 
 		<MkPagination :pagination="renoteMutingPagination">
@@ -45,8 +45,8 @@ SPDX-License-Identifier: AGPL-3.0-only
 							<MkA :class="$style.userItemMainBody" :to="userPage(item.mutee)">
 								<MkUserCardMini :user="item.mutee"/>
 							</MkA>
-							<button class="_button" :class="$style.userToggle" @click="toggleRenoteMuteItem(item)"><i :class="$style.chevron" class="ph-caret-down ph-bold ph-lg"></i></button>
-							<button class="_button" :class="$style.remove" @click="unrenoteMute(item.mutee, $event)"><i class="ph-x ph-bold ph-lg"></i></button>
+							<button class="_button" :class="$style.userToggle" @click="toggleRenoteMuteItem(item)"><i :class="$style.chevron" class="ti ti-chevron-down"></i></button>
+							<button class="_button" :class="$style.remove" @click="unrenoteMute(item.mutee, $event)"><i class="ti ti-x"></i></button>
 						</div>
 						<div v-if="expandedRenoteMuteItems.includes(item.id)" :class="$style.userItemSub">
 							<div>Muted at: <MkTime :time="item.createdAt" mode="detail"/></div>
@@ -58,7 +58,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 	</MkFolder>
 
 	<MkFolder>
-		<template #icon><i class="ph-eye-slash ph-bold ph-lg"></i></template>
+		<template #icon><i class="ti ti-eye-off"></i></template>
 		<template #label>{{ i18n.ts.mutedUsers }}</template>
 
 		<MkPagination :pagination="mutingPagination">
@@ -76,8 +76,8 @@ SPDX-License-Identifier: AGPL-3.0-only
 							<MkA :class="$style.userItemMainBody" :to="userPage(item.mutee)">
 								<MkUserCardMini :user="item.mutee"/>
 							</MkA>
-							<button class="_button" :class="$style.userToggle" @click="toggleMuteItem(item)"><i :class="$style.chevron" class="ph-caret-down ph-bold ph-lg"></i></button>
-							<button class="_button" :class="$style.remove" @click="unmute(item.mutee, $event)"><i class="ph-x ph-bold ph-lg"></i></button>
+							<button class="_button" :class="$style.userToggle" @click="toggleMuteItem(item)"><i :class="$style.chevron" class="ti ti-chevron-down"></i></button>
+							<button class="_button" :class="$style.remove" @click="unmute(item.mutee, $event)"><i class="ti ti-x"></i></button>
 						</div>
 						<div v-if="expandedMuteItems.includes(item.id)" :class="$style.userItemSub">
 							<div>Muted at: <MkTime :time="item.createdAt" mode="detail"/></div>
@@ -91,7 +91,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 	</MkFolder>
 
 	<MkFolder>
-		<template #icon><i class="ph-prohibit ph-bold ph-lg"></i></template>
+		<template #icon><i class="ti ti-ban"></i></template>
 		<template #label>{{ i18n.ts.blockedUsers }}</template>
 
 		<MkPagination :pagination="blockingPagination">
@@ -109,8 +109,8 @@ SPDX-License-Identifier: AGPL-3.0-only
 							<MkA :class="$style.userItemMainBody" :to="userPage(item.blockee)">
 								<MkUserCardMini :user="item.blockee"/>
 							</MkA>
-							<button class="_button" :class="$style.userToggle" @click="toggleBlockItem(item)"><i :class="$style.chevron" class="ph-caret-down ph-bold ph-lg"></i></button>
-							<button class="_button" :class="$style.remove" @click="unblock(item.blockee, $event)"><i class="ph-x ph-bold ph-lg"></i></button>
+							<button class="_button" :class="$style.userToggle" @click="toggleBlockItem(item)"><i :class="$style.chevron" class="ti ti-chevron-down"></i></button>
+							<button class="_button" :class="$style.remove" @click="unblock(item.blockee, $event)"><i class="ti ti-x"></i></button>
 						</div>
 						<div v-if="expandedBlockItems.includes(item.id)" :class="$style.userItemSub">
 							<div>Blocked at: <MkTime :time="item.createdAt" mode="detail"/></div>
@@ -163,7 +163,7 @@ const expandedBlockItems = ref([]);
 async function unrenoteMute(user, ev) {
 	os.popupMenu([{
 		text: i18n.ts.renoteUnmute,
-		icon: 'ph-x ph-bold ph-lg',
+		icon: 'ti ti-x',
 		action: async () => {
 			await os.apiWithDialog('renote-mute/delete', { userId: user.id });
 			//role.users = role.users.filter(u => u.id !== user.id);
@@ -174,7 +174,7 @@ async function unrenoteMute(user, ev) {
 async function unmute(user, ev) {
 	os.popupMenu([{
 		text: i18n.ts.unmute,
-		icon: 'ph-x ph-bold ph-lg',
+		icon: 'ti ti-x',
 		action: async () => {
 			await os.apiWithDialog('mute/delete', { userId: user.id });
 			//role.users = role.users.filter(u => u.id !== user.id);
@@ -185,7 +185,7 @@ async function unmute(user, ev) {
 async function unblock(user, ev) {
 	os.popupMenu([{
 		text: i18n.ts.unblock,
-		icon: 'ph-x ph-bold ph-lg',
+		icon: 'ti ti-x',
 		action: async () => {
 			await os.apiWithDialog('blocking/delete', { userId: user.id });
 			//role.users = role.users.filter(u => u.id !== user.id);
@@ -231,7 +231,7 @@ const headerTabs = computed(() => []);
 
 definePageMetadata(() => ({
 	title: i18n.ts.muteAndBlock,
-	icon: 'ph-prohibit ph-bold ph-lg',
+	icon: 'ti ti-ban',
 }));
 </script>
 
diff --git a/packages/frontend/src/pages/settings/mute-block.word-mute.vue b/packages/frontend/src/pages/settings/mute-block.word-mute.vue
index faf16ca368..f5837abe98 100644
--- a/packages/frontend/src/pages/settings/mute-block.word-mute.vue
+++ b/packages/frontend/src/pages/settings/mute-block.word-mute.vue
@@ -11,7 +11,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 			<template #caption>{{ i18n.ts._wordMute.muteWordsDescription }}<br>{{ i18n.ts._wordMute.muteWordsDescription2 }}</template>
 		</MkTextarea>
 	</div>
-	<MkButton primary inline :disabled="!changed" @click="save()"><i class="ph-floppy-disk ph-bold ph-lg"></i> {{ i18n.ts.save }}</MkButton>
+	<MkButton primary inline :disabled="!changed" @click="save()"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton>
 </div>
 </template>
 
diff --git a/packages/frontend/src/pages/settings/navbar.vue b/packages/frontend/src/pages/settings/navbar.vue
index ae5f081e1c..7f8460e316 100644
--- a/packages/frontend/src/pages/settings/navbar.vue
+++ b/packages/frontend/src/pages/settings/navbar.vue
@@ -21,18 +21,18 @@ SPDX-License-Identifier: AGPL-3.0-only
 						v-if="element.type === '-' || navbarItemDef[element.type]"
 						:class="$style.item"
 					>
-						<button class="_button" :class="$style.itemHandle"><i class="ph-list ph-bold ph-lg"></i></button>
+						<button class="_button" :class="$style.itemHandle"><i class="ti ti-menu"></i></button>
 						<i class="ti-fw" :class="[$style.itemIcon, navbarItemDef[element.type]?.icon]"></i><span :class="$style.itemText">{{ navbarItemDef[element.type]?.title ?? i18n.ts.divider }}</span>
-						<button class="_button" :class="$style.itemRemove" @click="removeItem(index)"><i class="ph-x ph-bold ph-lg"></i></button>
+						<button class="_button" :class="$style.itemRemove" @click="removeItem(index)"><i class="ti ti-x"></i></button>
 					</div>
 				</template>
 			</Sortable>
 		</MkContainer>
 	</FormSlot>
 	<div class="_buttons">
-		<MkButton @click="addItem"><i class="ph-plus ph-bold ph-lg"></i> {{ i18n.ts.addItem }}</MkButton>
-		<MkButton danger @click="reset"><i class="ph-arrow-clockwise ph-bold ph-lg"></i> {{ i18n.ts.default }}</MkButton>
-		<MkButton primary class="save" @click="save"><i class="ph-floppy-disk ph-bold ph-lg"></i> {{ i18n.ts.save }}</MkButton>
+		<MkButton @click="addItem"><i class="ti ti-plus"></i> {{ i18n.ts.addItem }}</MkButton>
+		<MkButton danger @click="reset"><i class="ti ti-reload"></i> {{ i18n.ts.default }}</MkButton>
+		<MkButton primary class="save" @click="save"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton>
 	</div>
 
 	<MkRadios v-model="menuDisplay">
@@ -120,7 +120,7 @@ const headerTabs = computed(() => []);
 
 definePageMetadata(() => ({
 	title: i18n.ts.navbar,
-	icon: 'ph-list ph-bold ph-lg',
+	icon: 'ti ti-list',
 }));
 </script>
 
diff --git a/packages/frontend/src/pages/settings/notifications.notification-config.vue b/packages/frontend/src/pages/settings/notifications.notification-config.vue
index 8d78ce7031..1a945aa3ad 100644
--- a/packages/frontend/src/pages/settings/notifications.notification-config.vue
+++ b/packages/frontend/src/pages/settings/notifications.notification-config.vue
@@ -21,7 +21,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 	</MkSelect>
 
 	<div class="_buttons">
-		<MkButton inline primary @click="save"><i class="ph-check ph-bold ph-lg"></i> {{ i18n.ts.save }}</MkButton>
+		<MkButton inline primary @click="save"><i class="ti ti-check"></i> {{ i18n.ts.save }}</MkButton>
 	</div>
 </div>
 </template>
diff --git a/packages/frontend/src/pages/settings/notifications.vue b/packages/frontend/src/pages/settings/notifications.vue
index 36fe7df03e..a861f6ee0d 100644
--- a/packages/frontend/src/pages/settings/notifications.vue
+++ b/packages/frontend/src/pages/settings/notifications.vue
@@ -133,6 +133,6 @@ const headerTabs = computed(() => []);
 
 definePageMetadata(() => ({
 	title: i18n.ts.notifications,
-	icon: 'ph-bell ph-bold ph-lg',
+	icon: 'ti ti-bell',
 }));
 </script>
diff --git a/packages/frontend/src/pages/settings/other.vue b/packages/frontend/src/pages/settings/other.vue
index 683e5f0e30..86bbae431d 100644
--- a/packages/frontend/src/pages/settings/other.vue
+++ b/packages/frontend/src/pages/settings/other.vue
@@ -18,7 +18,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 	<FormSection first>
 		<div class="_gaps_s">
 			<MkFolder>
-				<template #icon><i class="ph-info ph-bold ph-lg"></i></template>
+				<template #icon><i class="ti ti-info-circle"></i></template>
 				<template #label>{{ i18n.ts.accountInfo }}</template>
 
 				<div class="_gaps_m">
@@ -46,7 +46,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 			</MkFolder>
 
 			<MkFolder>
-				<template #icon><i class="ph-warning ph-bold ph-lg"></i></template>
+				<template #icon><i class="ti ti-alert-triangle"></i></template>
 				<template #label>{{ i18n.ts.closeAccount }}</template>
 
 				<div class="_gaps_m">
@@ -58,7 +58,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 			</MkFolder>
 
 			<MkFolder>
-				<template #icon><i class="ph-flask ph-bold ph-lg"></i></template>
+				<template #icon><i class="ti ti-flask"></i></template>
 				<template #label>{{ i18n.ts.experimentalFeatures }}</template>
 
 				<div class="_gaps_m">
@@ -69,7 +69,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 			</MkFolder>
 
 			<MkFolder>
-				<template #icon><i class="ph-code ph-bold ph-lg"></i></template>
+				<template #icon><i class="ti ti-code"></i></template>
 				<template #label>{{ i18n.ts.developer }}</template>
 
 				<div class="_gaps_m">
@@ -82,7 +82,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 	</FormSection>
 
 	<FormSection>
-		<FormLink to="/registry"><template #icon><i class="ph-faders ph-bold ph-lg"></i></template>{{ i18n.ts.registry }}</FormLink>
+		<FormLink to="/registry"><template #icon><i class="ti ti-adjustments"></i></template>{{ i18n.ts.registry }}</FormLink>
 	</FormSection>
 
 	<FormSection>
@@ -189,6 +189,6 @@ const headerTabs = computed(() => []);
 
 definePageMetadata(() => ({
 	title: i18n.ts.other,
-	icon: 'ph-dots-three ph-bold ph-lg',
+	icon: 'ti ti-dots',
 }));
 </script>
diff --git a/packages/frontend/src/pages/settings/plugin.install.vue b/packages/frontend/src/pages/settings/plugin.install.vue
index f3dd862bd1..3ab26e80d9 100644
--- a/packages/frontend/src/pages/settings/plugin.install.vue
+++ b/packages/frontend/src/pages/settings/plugin.install.vue
@@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 	</MkCodeEditor>
 
 	<div>
-		<MkButton :disabled="code == null" primary inline @click="install"><i class="ph-check ph-bold ph-lg"></i> {{ i18n.ts.install }}</MkButton>
+		<MkButton :disabled="code == null" primary inline @click="install"><i class="ti ti-check"></i> {{ i18n.ts.install }}</MkButton>
 	</div>
 </div>
 </template>
@@ -55,6 +55,6 @@ const headerTabs = computed(() => []);
 
 definePageMetadata(() => ({
 	title: i18n.ts._plugin.install,
-	icon: 'ph-download ph-bold ph-lg',
+	icon: 'ti ti-download',
 }));
 </script>
diff --git a/packages/frontend/src/pages/settings/plugin.vue b/packages/frontend/src/pages/settings/plugin.vue
index 349015dd28..3c3dcfe41e 100644
--- a/packages/frontend/src/pages/settings/plugin.vue
+++ b/packages/frontend/src/pages/settings/plugin.vue
@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <template>
 <div class="_gaps_m">
-	<FormLink to="/settings/plugin/install"><template #icon><i class="ph-download ph-bold ph-lg"></i></template>{{ i18n.ts._plugin.install }}</FormLink>
+	<FormLink to="/settings/plugin/install"><template #icon><i class="ti ti-download"></i></template>{{ i18n.ts._plugin.install }}</FormLink>
 
 	<FormSection>
 		<template #label>{{ i18n.ts.manage }}</template>
@@ -37,17 +37,17 @@ SPDX-License-Identifier: AGPL-3.0-only
 				</div>
 
 				<div class="_buttons">
-					<MkButton v-if="plugin.config" inline @click="config(plugin)"><i class="ph-gear ph-bold ph-lg"></i> {{ i18n.ts.settings }}</MkButton>
-					<MkButton inline danger @click="uninstall(plugin)"><i class="ph-trash ph-bold ph-lg"></i> {{ i18n.ts.uninstall }}</MkButton>
+					<MkButton v-if="plugin.config" inline @click="config(plugin)"><i class="ti ti-settings"></i> {{ i18n.ts.settings }}</MkButton>
+					<MkButton inline danger @click="uninstall(plugin)"><i class="ti ti-trash"></i> {{ i18n.ts.uninstall }}</MkButton>
 				</div>
 
 				<MkFolder>
-					<template #icon><i class="ph-terminal-window ph-bold ph-lg"></i></template>
+					<template #icon><i class="ti ti-terminal-2"></i></template>
 					<template #label>{{ i18n.ts._plugin.viewLog }}</template>
 
 					<div class="_gaps_s">
 						<div class="_buttons">
-							<MkButton inline @click="copy(pluginLogs.get(plugin.id)?.join('\n'))"><i class="ph-copy ph-bold ph-lg"></i> {{ i18n.ts.copy }}</MkButton>
+							<MkButton inline @click="copy(pluginLogs.get(plugin.id)?.join('\n'))"><i class="ti ti-copy"></i> {{ i18n.ts.copy }}</MkButton>
 						</div>
 
 						<MkCode :code="pluginLogs.get(plugin.id)?.join('\n') ?? ''"/>
@@ -55,12 +55,12 @@ SPDX-License-Identifier: AGPL-3.0-only
 				</MkFolder>
 
 				<MkFolder>
-					<template #icon><i class="ph-code ph-bold ph-lg"></i></template>
+					<template #icon><i class="ti ti-code"></i></template>
 					<template #label>{{ i18n.ts._plugin.viewSource }}</template>
 
 					<div class="_gaps_s">
 						<div class="_buttons">
-							<MkButton inline @click="copy(plugin.src)"><i class="ph-copy ph-bold ph-lg"></i> {{ i18n.ts.copy }}</MkButton>
+							<MkButton inline @click="copy(plugin.src)"><i class="ti ti-copy"></i> {{ i18n.ts.copy }}</MkButton>
 						</div>
 
 						<MkCode :code="plugin.src ?? ''" lang="is"/>
@@ -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';
@@ -141,6 +141,6 @@ const headerTabs = computed(() => []);
 
 definePageMetadata(() => ({
 	title: i18n.ts.plugins,
-	icon: 'ph-plug ph-bold ph-lg',
+	icon: 'ti ti-plug',
 }));
 </script>
diff --git a/packages/frontend/src/pages/settings/preferences-backups.vue b/packages/frontend/src/pages/settings/preferences-backups.vue
index b99e1e03e4..f9fd494ce9 100644
--- a/packages/frontend/src/pages/settings/preferences-backups.vue
+++ b/packages/frontend/src/pages/settings/preferences-backups.vue
@@ -118,8 +118,6 @@ const defaultStoreSaveKeys: (keyof typeof defaultStore['state'])[] = [
 	'sound_note',
 	'sound_noteMy',
 	'sound_notification',
-	'sound_antenna',
-	'sound_channel',
 ];
 const coldDeviceStorageSaveKeys: (keyof typeof ColdDeviceStorage.default)[] = [
 	'lightTheme',
@@ -425,25 +423,25 @@ function menu(ev: MouseEvent, profileId: string) {
 
 	return os.popupMenu([{
 		text: ts._preferencesBackups.apply,
-		icon: 'ph-check ph-bold ph-lg',
+		icon: 'ti ti-check',
 		action: () => applyProfile(profileId),
 	}, {
 		type: 'a',
 		text: ts.download,
-		icon: 'ph-download ph-bold ph-lg',
+		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,
-		icon: 'ph-textbox ph-bold ph-lg',
+		icon: 'ti ti-forms',
 		action: () => rename(profileId),
 	}, {
 		text: ts._preferencesBackups.save,
-		icon: 'ph-floppy-disk ph-bold ph-lg',
+		icon: 'ti ti-device-floppy',
 		action: () => save(profileId),
 	}, { type: 'divider' }, {
 		text: ts.delete,
-		icon: 'ph-trash ph-bold ph-lg',
+		icon: 'ti ti-trash',
 		action: () => deleteProfile(profileId),
 		danger: true,
 	}], (ev.currentTarget ?? ev.target ?? undefined) as unknown as HTMLElement | undefined);
@@ -465,7 +463,7 @@ onUnmounted(() => {
 
 definePageMetadata(() => ({
 	title: ts.preferencesBackups,
-	icon: 'ph-floppy-disk ph-bold ph-lg',
+	icon: 'ti ti-device-floppy',
 }));
 </script>
 
diff --git a/packages/frontend/src/pages/settings/privacy.vue b/packages/frontend/src/pages/settings/privacy.vue
index 86cf5ab241..b155d6e316 100644
--- a/packages/frontend/src/pages/settings/privacy.vue
+++ b/packages/frontend/src/pages/settings/privacy.vue
@@ -120,6 +120,6 @@ const headerTabs = computed(() => []);
 
 definePageMetadata(() => ({
 	title: i18n.ts.privacy,
-	icon: 'ph-lock ph-bold ph-lg-open',
+	icon: 'ti ti-lock-open',
 }));
 </script>
diff --git a/packages/frontend/src/pages/settings/profile.vue b/packages/frontend/src/pages/settings/profile.vue
index 408cf4ed67..6cc19db127 100644
--- a/packages/frontend/src/pages/settings/profile.vue
+++ b/packages/frontend/src/pages/settings/profile.vue
@@ -14,7 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 			<MkAvatar :class="$style.avatar" :user="$i" forceShowDecoration @click="changeAvatar"/>
 			<div class="_buttonsCenter">
 				<MkButton primary rounded @click="changeAvatar">{{ i18n.ts._profile.changeAvatar }}</MkButton>
-				<MkButton primary rounded link to="/settings/avatar-decoration">{{ i18n.ts.decorate }} <i class="ph-sparkle ph-bold ph-lg"></i></MkButton>
+				<MkButton primary rounded link to="/settings/avatar-decoration">{{ i18n.ts.decorate }} <i class="ti ti-sparkles"></i></MkButton>
 			</div>
 		</div>
 	</div>
@@ -30,12 +30,12 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 	<MkInput v-model="profile.location" manualSave>
 		<template #label>{{ i18n.ts.location }}</template>
-		<template #prefix><i class="ph-map-pin ph-bold ph-lg"></i></template>
+		<template #prefix><i class="ti ti-map-pin"></i></template>
 	</MkInput>
 
 	<MkInput v-model="profile.birthday" :max="setMaxBirthDate()" type="date" manualSave>
 		<template #label>{{ i18n.ts.birthday }}</template>
-		<template #prefix><i class="ph-cake ph-bold ph-lg"></i></template>
+		<template #prefix><i class="ti ti-cake"></i></template>
 	</MkInput>
 
 	<MkInput v-model="profile.listenbrainz" manualSave>
@@ -50,15 +50,15 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 	<FormSlot>
 		<MkFolder>
-			<template #icon><i class="ph-list ph-bold ph-lg"></i></template>
+			<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="ph-plus ph-bold ph-lg"></i> {{ i18n.ts.add }}</MkButton>
-					<MkButton v-if="!fieldEditMode" :disabled="fields.length <= 1" inline danger style="margin-right: 8px;" @click="fieldEditMode = !fieldEditMode"><i class="ph-trash ph-bold ph-lg"></i> {{ i18n.ts.delete }}</MkButton>
-					<MkButton v-else inline style="margin-right: 8px;" @click="fieldEditMode = !fieldEditMode"><i class="ph-arrows-down-up ph-bold ph-lg"></i> {{ i18n.ts.rearrange }}</MkButton>
-					<MkButton inline primary @click="saveFields"><i class="ph-check ph-bold ph-lg"></i> {{ i18n.ts.save }}</MkButton>
+					<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>
 				</div>
 
 				<Sortable
@@ -72,8 +72,8 @@ SPDX-License-Identifier: AGPL-3.0-only
 				>
 					<template #item="{element, index}">
 						<div :class="$style.fieldDragItem">
-							<button v-if="!fieldEditMode" class="_button" :class="$style.dragItemHandle" tabindex="-1"><i class="ph-list ph-bold ph-lg"></i></button>
-							<button v-if="fieldEditMode" :disabled="fields.length <= 1" class="_button" :class="$style.dragItemRemove" @click="deleteField(index)"><i class="ph-x ph-bold ph-lg"></i></button>
+							<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>
@@ -396,7 +396,7 @@ const headerTabs = computed(() => []);
 
 definePageMetadata(() => ({
 	title: i18n.ts.profile,
-	icon: 'ph-user ph-bold ph-lg',
+	icon: 'ti ti-user',
 }));
 </script>
 
@@ -481,6 +481,7 @@ definePageMetadata(() => ({
 	&:hover, &:focus {
 		opacity: .7;
 	}
+
 	&:active {
 		cursor: pointer;
 	}
diff --git a/packages/frontend/src/pages/settings/roles.vue b/packages/frontend/src/pages/settings/roles.vue
index 273cf013f0..5346a58a79 100644
--- a/packages/frontend/src/pages/settings/roles.vue
+++ b/packages/frontend/src/pages/settings/roles.vue
@@ -39,7 +39,7 @@ const headerTabs = computed(() => []);
 
 definePageMetadata(() => ({
 	title: i18n.ts.roles,
-	icon: 'ph-seal-check ph-bold ph-lg',
+	icon: 'ti ti-badges',
 }));
 </script>
 
diff --git a/packages/frontend/src/pages/settings/security.vue b/packages/frontend/src/pages/settings/security.vue
index 43e5104d71..de0f63630e 100644
--- a/packages/frontend/src/pages/settings/security.vue
+++ b/packages/frontend/src/pages/settings/security.vue
@@ -19,8 +19,8 @@ SPDX-License-Identifier: AGPL-3.0-only
 				<div>
 					<div v-for="item in items" :key="item.id" v-panel class="timnmucd">
 						<header>
-							<i v-if="item.success" class="ph-check ph-bold ph-lg icon succ"></i>
-							<i v-else class="ph-x-circle ph-bold ph-lg icon fail"></i>
+							<i v-if="item.success" class="ti ti-check icon succ"></i>
+							<i v-else class="ti ti-circle-x icon fail"></i>
 							<code class="ip _monospace">{{ item.ip }}</code>
 							<MkTime :time="item.createdAt" class="time"/>
 						</header>
@@ -32,7 +32,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 	<FormSection>
 		<FormSlot>
-			<MkButton danger @click="regenerateToken"><i class="ph-arrows-counter-clockwise ph-bold ph-lg"></i> {{ i18n.ts.regenerateLoginToken }}</MkButton>
+			<MkButton danger @click="regenerateToken"><i class="ti ti-refresh"></i> {{ i18n.ts.regenerateLoginToken }}</MkButton>
 			<template #caption>{{ i18n.ts.regenerateLoginTokenDescription }}</template>
 		</FormSlot>
 	</FormSection>
@@ -105,7 +105,7 @@ const headerTabs = computed(() => []);
 
 definePageMetadata(() => ({
 	title: i18n.ts.security,
-	icon: 'ph-lock ph-bold ph-lg',
+	icon: 'ti ti-lock',
 }));
 </script>
 
diff --git a/packages/frontend/src/pages/settings/sounds.sound.vue b/packages/frontend/src/pages/settings/sounds.sound.vue
index 307c5eaae4..81478fede5 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>
@@ -18,14 +24,14 @@ SPDX-License-Identifier: AGPL-3.0-only
 	</MkRange>
 
 	<div class="_buttons">
-		<MkButton inline @click="listen"><i class="ph-play ph-bold ph-lg"></i> {{ i18n.ts.listen }}</MkButton>
-		<MkButton inline primary @click="save"><i class="ph-check ph-bold ph-lg"></i> {{ i18n.ts.save }}</MkButton>
+		<MkButton inline @click="listen"><i class="ti ti-player-play"></i> {{ i18n.ts.listen }}</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,6 +190,13 @@ function save() {
 	gap: 8px;
 }
 
+.fileErrorRoot {
+	flex-grow: 1;
+	min-width: 0;
+	font-weight: 700;
+	color: var(--error);
+}
+
 .fileSelectorButton {
 	flex-shrink: 0;
 }
diff --git a/packages/frontend/src/pages/settings/sounds.vue b/packages/frontend/src/pages/settings/sounds.vue
index bf398ac303..9fcf564e55 100644
--- a/packages/frontend/src/pages/settings/sounds.vue
+++ b/packages/frontend/src/pages/settings/sounds.vue
@@ -21,13 +21,19 @@ 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>
 
-	<MkButton danger @click="reset()"><i class="ph-arrow-clockwise ph-bold ph-lg"></i> {{ i18n.ts.default }}</MkButton>
+	<MkButton danger @click="reset()"><i class="ti ti-reload"></i> {{ i18n.ts.default }}</MkButton>
 </div>
 </template>
 
@@ -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,
 });
 
@@ -96,6 +100,6 @@ const headerTabs = computed(() => []);
 
 definePageMetadata(() => ({
 	title: i18n.ts.sounds,
-	icon: 'ph-music-notes ph-bold ph-lg',
+	icon: 'ti ti-music',
 }));
 </script>
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/statusbar.vue b/packages/frontend/src/pages/settings/statusbar.vue
index fa924d13f0..1ae3de7994 100644
--- a/packages/frontend/src/pages/settings/statusbar.vue
+++ b/packages/frontend/src/pages/settings/statusbar.vue
@@ -52,6 +52,6 @@ const headerTabs = computed(() => []);
 
 definePageMetadata(() => ({
 	title: i18n.ts.statusbar,
-	icon: 'ph-list ph-bold ph-lg',
+	icon: 'ti ti-list',
 }));
 </script>
diff --git a/packages/frontend/src/pages/settings/theme.install.vue b/packages/frontend/src/pages/settings/theme.install.vue
index 01ae5286b7..4f05d3784c 100644
--- a/packages/frontend/src/pages/settings/theme.install.vue
+++ b/packages/frontend/src/pages/settings/theme.install.vue
@@ -10,8 +10,8 @@ SPDX-License-Identifier: AGPL-3.0-only
 	</MkCodeEditor>
 
 	<div class="_buttons">
-		<MkButton :disabled="installThemeCode == null" inline @click="() => previewTheme(installThemeCode)"><i class="ph-eye ph-bold ph-lg"></i> {{ i18n.ts.preview }}</MkButton>
-		<MkButton :disabled="installThemeCode == null" primary inline @click="() => install(installThemeCode)"><i class="ph-check ph-bold ph-lg"></i> {{ i18n.ts.install }}</MkButton>
+		<MkButton :disabled="installThemeCode == null" inline @click="() => previewTheme(installThemeCode)"><i class="ti ti-eye"></i> {{ i18n.ts.preview }}</MkButton>
+		<MkButton :disabled="installThemeCode == null" primary inline @click="() => install(installThemeCode)"><i class="ti ti-check"></i> {{ i18n.ts.install }}</MkButton>
 	</div>
 </div>
 </template>
@@ -61,6 +61,6 @@ const headerTabs = computed(() => []);
 
 definePageMetadata(() => ({
 	title: i18n.ts._theme.install,
-	icon: 'ph-download ph-bold ph-lg',
+	icon: 'ti ti-download',
 }));
 </script>
diff --git a/packages/frontend/src/pages/settings/theme.manage.vue b/packages/frontend/src/pages/settings/theme.manage.vue
index 43d76951c0..579ca6b20b 100644
--- a/packages/frontend/src/pages/settings/theme.manage.vue
+++ b/packages/frontend/src/pages/settings/theme.manage.vue
@@ -25,7 +25,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 			<template #label>{{ i18n.ts._theme.code }}</template>
 			<template #caption><button class="_textButton" @click="copyThemeCode()">{{ i18n.ts.copy }}</button></template>
 		</MkTextarea>
-		<MkButton v-if="!builtinThemes.some(t => t.id == selectedTheme.id)" danger @click="uninstall()"><i class="ph-trash ph-bold ph-lg"></i> {{ i18n.ts.uninstall }}</MkButton>
+		<MkButton v-if="!builtinThemes.some(t => t.id == selectedTheme.id)" danger @click="uninstall()"><i class="ti ti-trash"></i> {{ i18n.ts.uninstall }}</MkButton>
 	</template>
 </div>
 </template>
@@ -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';
@@ -78,6 +78,6 @@ const headerTabs = computed(() => []);
 
 definePageMetadata(() => ({
 	title: i18n.ts._theme.manage,
-	icon: 'ph-wrench ph-bold ph-lg',
+	icon: 'ti ti-tool',
 }));
 </script>
diff --git a/packages/frontend/src/pages/settings/theme.vue b/packages/frontend/src/pages/settings/theme.vue
index 9b493f5ffe..ad07a6b539 100644
--- a/packages/frontend/src/pages/settings/theme.vue
+++ b/packages/frontend/src/pages/settings/theme.vue
@@ -34,7 +34,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 	<div class="selects">
 		<MkSelect v-model="lightThemeId" large class="select">
 			<template #label>{{ i18n.ts.themeForLightMode }}</template>
-			<template #prefix><i class="ph-sun ph-bold ph-lg"></i></template>
+			<template #prefix><i class="ti ti-sun"></i></template>
 			<option v-if="instanceLightTheme" :key="'instance:' + instanceLightTheme.id" :value="instanceLightTheme.id">{{ instanceLightTheme.name }}</option>
 			<optgroup v-if="installedLightThemes.length > 0" :label="i18n.ts._theme.installedThemes">
 				<option v-for="x in installedLightThemes" :key="'installed:' + x.id" :value="x.id">{{ x.name }}</option>
@@ -45,7 +45,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 		</MkSelect>
 		<MkSelect v-model="darkThemeId" large class="select">
 			<template #label>{{ i18n.ts.themeForDarkMode }}</template>
-			<template #prefix><i class="ph-moon ph-bold ph-lg"></i></template>
+			<template #prefix><i class="ti ti-moon"></i></template>
 			<option v-if="instanceDarkTheme" :key="'instance:' + instanceDarkTheme.id" :value="instanceDarkTheme.id">{{ instanceDarkTheme.name }}</option>
 			<optgroup v-if="installedDarkThemes.length > 0" :label="i18n.ts._theme.installedThemes">
 				<option v-for="x in installedDarkThemes" :key="'installed:' + x.id" :value="x.id">{{ x.name }}</option>
@@ -58,10 +58,10 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 	<FormSection>
 		<div class="_formLinksGrid">
-			<FormLink to="/settings/theme/manage"><template #icon><i class="ph-wrench ph-bold ph-lg"></i></template>{{ i18n.ts._theme.manage }}<template #suffix>{{ themesCount }}</template></FormLink>
-			<FormLink to="https://assets.misskey.io/theme/list" external><template #icon><i class="ph-globe-hemisphere-west ph-bold ph-lg"></i></template>{{ i18n.ts._theme.explore }}</FormLink>
-			<FormLink to="/settings/theme/install"><template #icon><i class="ph-download ph-bold ph-lg"></i></template>{{ i18n.ts._theme.install }}</FormLink>
-			<FormLink to="/theme-editor"><template #icon><i class="ph-paint-roller ph-bold ph-lg"></i></template>{{ i18n.ts._theme.make }}</FormLink>
+			<FormLink to="/settings/theme/manage"><template #icon><i class="ti ti-tool"></i></template>{{ i18n.ts._theme.manage }}<template #suffix>{{ themesCount }}</template></FormLink>
+			<FormLink to="https://assets.misskey.io/theme/list" external><template #icon><i class="ti ti-world"></i></template>{{ i18n.ts._theme.explore }}</FormLink>
+			<FormLink to="/settings/theme/install"><template #icon><i class="ti ti-download"></i></template>{{ i18n.ts._theme.install }}</FormLink>
+			<FormLink to="/theme-editor"><template #icon><i class="ti ti-paint"></i></template>{{ i18n.ts._theme.make }}</FormLink>
 		</div>
 	</FormSection>
 
@@ -179,7 +179,7 @@ const headerTabs = computed(() => []);
 
 definePageMetadata(() => ({
 	title: i18n.ts.theme,
-	icon: 'ph-palette ph-bold ph-lg',
+	icon: 'ti ti-palette',
 }));
 </script>
 
@@ -213,12 +213,18 @@ definePageMetadata(() => ({
 			}
 		}
 
+		.dn:focus-visible ~ .toggle {
+			outline: 2px solid var(--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;
diff --git a/packages/frontend/src/pages/settings/webhook.edit.vue b/packages/frontend/src/pages/settings/webhook.edit.vue
index 99326c8671..058ef69c35 100644
--- a/packages/frontend/src/pages/settings/webhook.edit.vue
+++ b/packages/frontend/src/pages/settings/webhook.edit.vue
@@ -14,12 +14,12 @@ SPDX-License-Identifier: AGPL-3.0-only
 	</MkInput>
 
 	<MkInput v-model="secret">
-		<template #prefix><i class="ph-lock ph-bold ph-lg"></i></template>
+		<template #prefix><i class="ti ti-lock"></i></template>
 		<template #label>{{ i18n.ts._webhookSettings.secret }}</template>
 	</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>
@@ -35,8 +35,8 @@ SPDX-License-Identifier: AGPL-3.0-only
 	<MkSwitch v-model="active">{{ i18n.ts._webhookSettings.active }}</MkSwitch>
 
 	<div class="_buttons">
-		<MkButton primary inline @click="save"><i class="ph-check ph-bold ph-lg"></i> {{ i18n.ts.save }}</MkButton>
-		<MkButton danger inline @click="del"><i class="ph-trash ph-bold ph-lg"></i> {{ i18n.ts.delete }}</MkButton>
+		<MkButton primary inline @click="save"><i class="ti ti-check"></i> {{ i18n.ts.save }}</MkButton>
+		<MkButton danger inline @click="del"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton>
 	</div>
 </div>
 </template>
@@ -116,6 +116,6 @@ const headerTabs = computed(() => []);
 
 definePageMetadata(() => ({
 	title: 'Edit webhook',
-	icon: 'ph-webhooks-logo ph-bold ph-lg',
+	icon: 'ti ti-webhook',
 }));
 </script>
diff --git a/packages/frontend/src/pages/settings/webhook.new.vue b/packages/frontend/src/pages/settings/webhook.new.vue
index 299386338a..d62357caaf 100644
--- a/packages/frontend/src/pages/settings/webhook.new.vue
+++ b/packages/frontend/src/pages/settings/webhook.new.vue
@@ -14,12 +14,12 @@ SPDX-License-Identifier: AGPL-3.0-only
 	</MkInput>
 
 	<MkInput v-model="secret">
-		<template #prefix><i class="ph-lock ph-bold ph-lg"></i></template>
+		<template #prefix><i class="ti ti-lock"></i></template>
 		<template #label>{{ i18n.ts._webhookSettings.secret }}</template>
 	</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>
@@ -33,7 +33,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 	</FormSection>
 
 	<div class="_buttons">
-		<MkButton primary inline @click="create"><i class="ph-check ph-bold ph-lg"></i> {{ i18n.ts.create }}</MkButton>
+		<MkButton primary inline @click="create"><i class="ti ti-check"></i> {{ i18n.ts.create }}</MkButton>
 	</div>
 </div>
 </template>
@@ -84,6 +84,6 @@ const headerTabs = computed(() => []);
 
 definePageMetadata(() => ({
 	title: 'Create new webhook',
-	icon: 'ph-webhooks-logo ph-bold ph-lg',
+	icon: 'ti ti-webhook',
 }));
 </script>
diff --git a/packages/frontend/src/pages/settings/webhook.vue b/packages/frontend/src/pages/settings/webhook.vue
index 3717abb13e..0d11b00c97 100644
--- a/packages/frontend/src/pages/settings/webhook.vue
+++ b/packages/frontend/src/pages/settings/webhook.vue
@@ -15,10 +15,10 @@ SPDX-License-Identifier: AGPL-3.0-only
 				<div class="_gaps">
 					<FormLink v-for="webhook in items" :key="webhook.id" :to="`/settings/webhook/edit/${webhook.id}`">
 						<template #icon>
-							<i v-if="webhook.active === false" class="ph-pause ph-bold ph-lg"></i>
-							<i v-else-if="webhook.latestStatus === null" class="ph-circle ph-bold ph-lg"></i>
-							<i v-else-if="[200, 201, 204].includes(webhook.latestStatus)" class="ph-check ph-bold ph-lg" :style="{ color: 'var(--success)' }"></i>
-							<i v-else class="ph-warning ph-bold ph-lg" :style="{ color: 'var(--error)' }"></i>
+							<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>
 						</template>
 						{{ webhook.name || webhook.url }}
 						<template #suffix>
@@ -52,6 +52,6 @@ const headerTabs = computed(() => []);
 
 definePageMetadata(() => ({
 	title: 'Webhook',
-	icon: 'ph-webhooks-logo ph-bold ph-lg',
+	icon: 'ti ti-webhook',
 }));
 </script>
diff --git a/packages/frontend/src/pages/share.vue b/packages/frontend/src/pages/share.vue
index 8d974b17fb..37f6558d64 100644
--- a/packages/frontend/src/pages/share.vue
+++ b/packages/frontend/src/pages/share.vue
@@ -201,6 +201,6 @@ const headerTabs = computed(() => []);
 
 definePageMetadata(() => ({
 	title: i18n.ts.share,
-	icon: 'ph-share-network ph-bold ph-lg',
+	icon: 'ti ti-share',
 }));
 </script>
diff --git a/packages/frontend/src/pages/signup-complete.vue b/packages/frontend/src/pages/signup-complete.vue
index b08a304cfd..226f08bf8e 100644
--- a/packages/frontend/src/pages/signup-complete.vue
+++ b/packages/frontend/src/pages/signup-complete.vue
@@ -9,7 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 	<div :class="$style.formContainer">
 		<form :class="$style.form" class="_panel" @submit.prevent="submit()">
 			<div :class="$style.banner">
-				<i class="ph-check ph-bold ph-lg"></i>
+				<i class="ti ti-user-check"></i>
 			</div>
 			<div class="_gaps_m" style="padding: 32px;">
 				<div>{{ i18n.tsx.clickToFinishEmailVerification({ ok: i18n.ts.gotIt }) }}</div>
diff --git a/packages/frontend/src/pages/tag.vue b/packages/frontend/src/pages/tag.vue
index d9c94569a7..9b77392872 100644
--- a/packages/frontend/src/pages/tag.vue
+++ b/packages/frontend/src/pages/tag.vue
@@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 	<template v-if="$i" #footer>
 		<div :class="$style.footer">
 			<MkSpacer :contentMax="800" :marginMin="16" :marginMax="16">
-				<MkButton rounded primary :class="$style.button" @click="post()"><i class="ph-pencil-simple ph-bold ph-lg"></i>{{ i18n.ts.postToHashtag }}</MkButton>
+				<MkButton rounded primary :class="$style.button" @click="post()"><i class="ti ti-pencil"></i>{{ i18n.ts.postToHashtag }}</MkButton>
 			</MkSpacer>
 		</div>
 	</template>
@@ -57,7 +57,7 @@ const headerTabs = computed(() => []);
 
 definePageMetadata(() => ({
 	title: props.tag,
-	icon: 'ph-hash ph-bold ph-lg',
+	icon: 'ti ti-hash',
 }));
 </script>
 
diff --git a/packages/frontend/src/pages/theme-editor.vue b/packages/frontend/src/pages/theme-editor.vue
index d020320b44..1cdccdc244 100644
--- a/packages/frontend/src/pages/theme-editor.vue
+++ b/packages/frontend/src/pages/theme-editor.vue
@@ -47,7 +47,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 			</MkFolder>
 
 			<MkFolder :defaultOpen="false">
-				<template #icon><i class="ph-code ph-bold ph-lg"></i></template>
+				<template #icon><i class="ti ti-code"></i></template>
 				<template #label>{{ i18n.ts.editCode }}</template>
 
 				<div class="_gaps_m">
@@ -212,7 +212,7 @@ watch(theme, apply, { deep: true });
 
 const headerActions = computed(() => [{
 	asFullButton: true,
-	icon: 'ph-check ph-bold ph-lg',
+	icon: 'ti ti-check',
 	text: i18n.ts.saveAs,
 	handler: saveAs,
 }]);
@@ -221,7 +221,7 @@ const headerTabs = computed(() => []);
 
 definePageMetadata(() => ({
 	title: i18n.ts.themeEditor,
-	icon: 'ph-palette ph-bold ph-lg',
+	icon: 'ti ti-palette',
 }));
 </script>
 
diff --git a/packages/frontend/src/pages/timeline.vue b/packages/frontend/src/pages/timeline.vue
index 1e5a244dd4..20d8abccf6 100644
--- a/packages/frontend/src/pages/timeline.vue
+++ b/packages/frontend/src/pages/timeline.vue
@@ -8,8 +8,8 @@ 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(--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);"/>
@@ -35,7 +35,7 @@ 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';
@@ -46,7 +46,6 @@ 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';
@@ -54,22 +53,19 @@ import { deviceKind } from '@/scripts/device-kind.js';
 import { deepMerge } from '@/scripts/merge.js';
 import { 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 isBubbleTimelineAvailable = ($i == null && instance.policies.btlAvailable) || ($i != null && $i.policies.btlAvailable);
-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' | 'bubble' | `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),
 });
@@ -79,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: () => {
@@ -151,7 +151,7 @@ async function chooseList(ev: MouseEvent): Promise<void> {
 		(lists.length === 0 ? undefined : { type: 'divider' }),
 		{
 			type: 'link' as const,
-			icon: 'ph-plus ph-bold ph-lg',
+			icon: 'ti ti-plus',
 			text: i18n.ts.createNew,
 			to: '/my/lists',
 		},
@@ -171,7 +171,7 @@ async function chooseAntenna(ev: MouseEvent): Promise<void> {
 		(antennas.length === 0 ? undefined : { type: 'divider' }),
 		{
 			type: 'link' as const,
-			icon: 'ph-plus ph-bold ph-lg',
+			icon: 'ti ti-plus',
 			text: i18n.ts.createNew,
 			to: '/my/antennas',
 		},
@@ -196,7 +196,7 @@ async function chooseChannel(ev: MouseEvent): Promise<void> {
 		(channels.length === 0 ? undefined : { type: 'divider' }),
 		{
 			type: 'link' as const,
-			icon: 'ph-plus ph-bold ph-lg',
+			icon: 'ti ti-plus',
 			text: i18n.ts.createNew,
 			to: '/channels',
 		},
@@ -204,7 +204,7 @@ async function chooseChannel(ev: MouseEvent): Promise<void> {
 	os.popupMenu(items, ev.currentTarget ?? ev.target);
 }
 
-function saveSrc(newSrc: 'home' | 'local' | 'social' | 'global' | 'bubble' | `list:${string}`): void {
+function saveSrc(newSrc: TimelinePageSrc): void {
 	const out = deepMerge({ src: newSrc }, defaultStore.state.tl);
 
 	if (newSrc.startsWith('userList:')) {
@@ -239,23 +239,36 @@ 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: 'ph-dots-three ph-bold ph-lg',
+			icon: 'ti ti-dots',
 			text: i18n.ts.options,
 			handler: (ev) => {
 				os.popupMenu([{
 					type: 'switch',
 					text: i18n.ts.showRenotes,
 					ref: withRenotes,
-				}, src.value === 'local' || src.value === 'social' ? {
+				}, isBasicTimeline(src.value) && hasWithReplies(src.value) ? {
 					type: 'switch',
 					text: i18n.ts.showRepliesToOthersInTimeline,
 					ref: withReplies,
@@ -268,14 +281,14 @@ const headerActions = computed(() => {
 					type: 'switch',
 					text: i18n.ts.fileAttachedOnly,
 					ref: onlyFiles,
-					disabled: src.value === 'local' || src.value === 'social' ? withReplies : false,
+					disabled: isBasicTimeline(src.value) && hasWithReplies(src.value) ? withReplies : false,
 				}], ev.currentTarget ?? ev.target);
 			},
 		},
 	];
 	if (deviceKind === 'desktop') {
 		tmp.unshift({
-			icon: 'ph-arrows-counter-clockwise ph-bold ph-lg',
+			icon: 'ti ti-refresh',
 			text: i18n.ts.reload,
 			handler: (ev: Event) => {
 				tlComponent.value?.reloadTimeline();
@@ -288,68 +301,40 @@ const headerActions = computed(() => {
 const headerTabs = computed(() => [...(defaultStore.reactiveState.pinnedUserLists.value.map(l => ({
 	key: 'list:' + l.id,
 	title: l.name,
-	icon: 'ph-star ph-bold ph-lg',
+	icon: 'ti ti-star',
 	iconOnly: true,
-}))), {
-	key: 'home',
-	title: i18n.ts._timelines.home,
-	icon: 'ph-house ph-bold ph-lg',
+}))), ...availableBasicTimelines().map(tl => ({
+	key: tl,
+	title: i18n.ts._timelines[tl],
+	icon: basicTimelineIconClass(tl),
 	iconOnly: true,
-}, ...(isLocalTimelineAvailable ? [{
-	key: 'local',
-	title: i18n.ts._timelines.local,
-	icon: 'ph-planet ph-bold ph-lg',
-	iconOnly: true,
-}, {
-	key: 'social',
-	title: i18n.ts._timelines.social,
-	icon: 'ph-rocket-launch ph-bold ph-lg',
-	iconOnly: true,
-}] : []), ...(isBubbleTimelineAvailable ? [{
-	key: 'bubble',
-	title: 'Bubble',
-	icon: 'ph-drop ph-bold ph-lg',
-	iconOnly: true,
-}] : []), ...(isGlobalTimelineAvailable ? [{
-	key: 'global',
-	title: i18n.ts._timelines.global,
-	icon: 'ph-globe-hemisphere-west ph-bold ph-lg',
-	iconOnly: true,
-}] : []), {
-	icon: 'ph-list ph-bold ph-lg',
+})), {
+	icon: 'ti ti-list',
 	title: i18n.ts.lists,
 	iconOnly: true,
 	onClick: chooseList,
 }, {
-	icon: 'ph-flying-saucer ph-bold ph-lg',
+	icon: 'ti ti-antenna',
 	title: i18n.ts.antennas,
 	iconOnly: true,
 	onClick: chooseAntenna,
 }, {
-	icon: 'ph-television ph-bold ph-lg',
+	icon: 'ti ti-device-tv',
 	title: i18n.ts.channel,
 	iconOnly: true,
 	onClick: chooseChannel,
 }] as Tab[]);
 
-const headerTabsWhenNotLogin = computed(() => [
-	...(isLocalTimelineAvailable ? [{
-		key: 'local',
-		title: i18n.ts._timelines.local,
-		icon: 'ph-planet ph-bold ph-lg',
-		iconOnly: true,
-	}] : []),
-	...(isGlobalTimelineAvailable ? [{
-		key: 'global',
-		title: i18n.ts._timelines.global,
-		icon: 'ph-globe-hemisphere-west ph-bold ph-lg',
-		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' ? 'ph-planet ph-bold ph-lg' : src.value === 'social' ? 'ph-rocket-launch ph-bold ph-lg' : src.value === 'global' ? 'ph-globe-hemisphere-west ph-bold ph-lg' : src.value === 'bubble' ? 'ph-drop ph-bold ph-lg' : 'ph-house ph-bold ph-lg',
+	icon: isBasicTimeline(src.value) ? basicTimelineIconClass(src.value) : 'ti ti-home',
 }));
 </script>
 
diff --git a/packages/frontend/src/pages/user-list-timeline.vue b/packages/frontend/src/pages/user-list-timeline.vue
index c60e3c0f29..6566263c01 100644
--- a/packages/frontend/src/pages/user-list-timeline.vue
+++ b/packages/frontend/src/pages/user-list-timeline.vue
@@ -98,7 +98,7 @@ const headerActions = computed(() => list.value ? [{
 		}], ev.currentTarget ?? ev.target);
 	},
 }, {
-	icon: 'ph-gear ph-bold ph-lg',
+	icon: 'ti ti-settings',
 	text: i18n.ts.settings,
 	handler: settings,
 }] : []);
@@ -107,7 +107,7 @@ const headerTabs = computed(() => []);
 
 definePageMetadata(() => ({
 	title: list.value ? list.value.name : i18n.ts.lists,
-	icon: 'ph-list ph-bold ph-lg',
+	icon: 'ti ti-list',
 }));
 </script>
 
diff --git a/packages/frontend/src/pages/user-tag.vue b/packages/frontend/src/pages/user-tag.vue
index b6ebfb8abc..a77493fe47 100644
--- a/packages/frontend/src/pages/user-tag.vue
+++ b/packages/frontend/src/pages/user-tag.vue
@@ -36,7 +36,7 @@ const tagUsers = computed(() => ({
 
 definePageMetadata(() => ({
 	title: props.tag,
-	icon: 'ph-user-circle ph-bold ph-lg',
+	icon: 'ti ti-user-search',
 }));
 </script>
 
diff --git a/packages/frontend/src/pages/user/activity.vue b/packages/frontend/src/pages/user/activity.vue
index 271631e8d1..994bd52705 100644
--- a/packages/frontend/src/pages/user/activity.vue
+++ b/packages/frontend/src/pages/user/activity.vue
@@ -7,19 +7,19 @@ SPDX-License-Identifier: AGPL-3.0-only
 <MkSpacer :contentMax="700">
 	<div class="_gaps">
 		<MkFoldableSection class="item">
-			<template #header><i class="ph-pulse ph-bold ph-lg"></i> Heatmap</template>
+			<template #header><i class="ti ti-activity"></i> Heatmap</template>
 			<MkHeatmap :user="user" :src="'notes'"/>
 		</MkFoldableSection>
 		<MkFoldableSection class="item">
-			<template #header><i class="ph-pencil-simple ph-bold ph-lg"></i> Notes</template>
+			<template #header><i class="ti ti-pencil"></i> Notes</template>
 			<XNotes :user="user"/>
 		</MkFoldableSection>
 		<MkFoldableSection class="item">
-			<template #header><i class="ph-users ph-bold ph-lg"></i> Following</template>
+			<template #header><i class="ti ti-users"></i> Following</template>
 			<XFollowing :user="user"/>
 		</MkFoldableSection>
 		<MkFoldableSection class="item">
-			<template #header><i class="ph-eye ph-bold ph-lg"></i> PV</template>
+			<template #header><i class="ti ti-eye"></i> PV</template>
 			<XPv :user="user"/>
 		</MkFoldableSection>
 	</div>
diff --git a/packages/frontend/src/pages/user/followers.vue b/packages/frontend/src/pages/user/followers.vue
index e8addf88b7..70883242e5 100644
--- a/packages/frontend/src/pages/user/followers.vue
+++ b/packages/frontend/src/pages/user/followers.vue
@@ -54,7 +54,7 @@ const headerTabs = computed(() => []);
 
 definePageMetadata(() => ({
 	title: i18n.ts.user,
-	icon: 'ph-user ph-bold ph-lg',
+	icon: 'ti ti-user',
 	...user.value ? {
 		title: user.value.name ? `${user.value.name} (@${user.value.username})` : `@${user.value.username}`,
 		subtitle: i18n.ts.followers,
diff --git a/packages/frontend/src/pages/user/following.vue b/packages/frontend/src/pages/user/following.vue
index 8e4da40383..37b25f694f 100644
--- a/packages/frontend/src/pages/user/following.vue
+++ b/packages/frontend/src/pages/user/following.vue
@@ -54,7 +54,7 @@ const headerTabs = computed(() => []);
 
 definePageMetadata(() => ({
 	title: i18n.ts.user,
-	icon: 'ph-user ph-bold ph-lg',
+	icon: 'ti ti-user',
 	...user.value ? {
 		title: user.value.name ? `${user.value.name} (@${user.value.username})` : `@${user.value.username}`,
 		subtitle: i18n.ts.following,
diff --git a/packages/frontend/src/pages/user/home.vue b/packages/frontend/src/pages/user/home.vue
index 095645636d..b997fe1c3f 100644
--- a/packages/frontend/src/pages/user/home.vue
+++ b/packages/frontend/src/pages/user/home.vue
@@ -22,18 +22,18 @@ 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="ph-shield ph-bold ph-lg"></i></span>
-								<span v-if="user.isLocked" :title="i18n.ts.isLocked"><i class="ph-lock ph-bold ph-lg"></i></span>
-								<span v-if="user.isBot" :title="i18n.ts.isBot"><i class="ph-robot ph-bold ph-lg"></i></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.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">
-									<i class="ph-pencil-simple-line ph-bold ph-lg"/> {{ i18n.ts.addMemo }}
+									<i class="ti ti-edit"/> {{ i18n.ts.addMemo }}
 								</button>
 							</div>
 						</div>
 						<span v-if="$i && $i.id != user.id && user.isFollowed" class="followed">{{ i18n.ts.followsYou }}</span>
-						<div v-if="$i" class="actions">
-							<button class="menu _button" @click="menu"><i class="ph-dots-three ph-bold ph-lg"></i></button>
-							<MkFollowButton v-if="$i.id != user.id" v-model:user="user" :inline="true" :transparent="false" :full="true" class="koudoku"/>
+						<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"/>
 						</div>
 					</div>
 					<MkAvatar class="avatar" :user="user" indicator/>
@@ -41,9 +41,9 @@ 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="ph-shield ph-bold ph-lg"></i></span>
-							<span v-if="user.isLocked" :title="i18n.ts.isLocked"><i class="ph-lock ph-bold ph-lg"></i></span>
-							<span v-if="user.isBot" :title="i18n.ts.isBot"><i class="ph-robot ph-bold ph-lg"></i></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.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.roles.length > 0" class="roles">
@@ -79,26 +79,26 @@ SPDX-License-Identifier: AGPL-3.0-only
 					</div>
 					<div class="fields system">
 						<dl v-if="user.location" class="field">
-							<dt class="name"><i class="ph-map-pin ph-bold ph-lg ti-fw"></i> {{ i18n.ts.location }}</dt>
+							<dt class="name"><i class="ti ti-map-pin ti-fw"></i> {{ i18n.ts.location }}</dt>
 							<dd class="value">{{ user.location }}</dd>
 						</dl>
 						<dl v-if="user.birthday" class="field">
-							<dt class="name"><i class="ph-cake ph-bold ph-lg ti-fw"></i> {{ i18n.ts.birthday }}</dt>
+							<dt class="name"><i class="ti ti-cake ti-fw"></i> {{ i18n.ts.birthday }}</dt>
 							<dd class="value">{{ user.birthday.replace('-', '/').replace('-', '/') }} ({{ i18n.tsx.yearsOld({ age }) }})</dd>
 						</dl>
 						<dl class="field">
-							<dt class="name"><i class="ph-calendar ph-bold ph-lg ti-fw"></i> {{ i18n.ts.registeredDate }}</dt>
+							<dt class="name"><i class="ti ti-calendar ti-fw"></i> {{ i18n.ts.registeredDate }}</dt>
 							<dd class="value">{{ dateString(user.createdAt) }} (<MkTime :time="user.createdAt"/>)</dd>
 						</dl>
 					</div>
 					<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"/>
-								<i v-if="user.verifiedLinks.includes(field.value)" v-tooltip:dialog="i18n.ts.verifiedLink" class="ph-seal-check ph-bold ph-lg" :class="$style.verifiedLink"></i>
+								<i v-if="user.verifiedLinks.includes(field.value)" v-tooltip:dialog="i18n.ts.verifiedLink" class="ti ti-circle-check" :class="$style.verifiedLink"></i>
 							</dd>
 						</dl>
 					</div>
@@ -120,12 +120,9 @@ SPDX-License-Identifier: AGPL-3.0-only
 			</div>
 
 			<div class="contents _gaps">
-				<div v-if="user.pinnedNotes.length > 0 && defaultStore.state.noteDesign === 'misskey'" class="_gaps">
+				<div v-if="user.pinnedNotes.length > 0" class="_gaps">
 					<MkNote v-for="note in user.pinnedNotes" :key="note.id" class="note _panel" :note="note" :pinned="true"/>
 				</div>
-				<div v-else-if="user.pinnedNotes.length > 0 && defaultStore.state.noteDesign === 'sharkey'" class="_gaps">
-					<SkNote v-for="note in user.pinnedNotes" :key="note.id" class="note _panel" :note="note" :pinned="true"/>
-				</div>
 				<MkInfo v-else-if="$i && $i.id === user.id">{{ i18n.ts.userPagePinTip }}</MkInfo>
 				<template v-if="narrow">
 					<MkLazy>
@@ -171,9 +168,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { defineAsyncComponent, computed, onMounted, onUnmounted, nextTick, watch, ref } from 'vue';
 import * as Misskey from 'misskey-js';
 import MkTab from '@/components/MkTab.vue';
-import MkNote from '@/components/MkNote.vue';
 import MkNotes from '@/components/MkNotes.vue';
-import SkNote from '@/components/SkNote.vue';
 import MkFollowButton from '@/components/MkFollowButton.vue';
 import MkAccountMoved from '@/components/MkAccountMoved.vue';
 import MkRemoteCaution from '@/components/MkRemoteCaution.vue';
@@ -186,13 +181,20 @@ 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 { defaultStore } from '@/store.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';
+
+const MkNote = defineAsyncComponent(() =>
+	(defaultStore.state.noteDesign === 'misskey') ? import('@/components/MkNote.vue') :
+	(defaultStore.state.noteDesign === 'sharkey') ? import('@/components/SkNote.vue') :
+	null
+);
 
 function calcAge(birthdate: string): number {
 	const date = new Date(birthdate);
@@ -258,9 +260,15 @@ if (props.user.listenbrainz) {
 
 const background = computed(() => {
 	if (props.user.backgroundUrl == null) return {};
-	return {
-		'--backgroundImageStatic': `url('${props.user.backgroundUrl}')`
-	};
+	if (defaultStore.state.disableShowingAnimatedImages) {
+		return {
+			'--backgroundImageStatic': `url('${getStaticImageUrl(props.user.backgroundUrl)}')`
+		};
+	} else {
+		return {
+			'--backgroundImageStatic': `url('${props.user.backgroundUrl}')`
+		};
+	}
 });
 
 watch(moderationNote, async () => {
@@ -289,9 +297,15 @@ const AllPagination = {
 
 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 })`,
+		};
+	}
 });
 
 const age = computed(() => {
@@ -478,11 +492,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 {
diff --git a/packages/frontend/src/pages/user/index.activity.vue b/packages/frontend/src/pages/user/index.activity.vue
index 857cf996ff..45bc35067b 100644
--- a/packages/frontend/src/pages/user/index.activity.vue
+++ b/packages/frontend/src/pages/user/index.activity.vue
@@ -5,11 +5,11 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <template>
 <MkContainer>
-	<template #icon><i class="ph-chart-line ph-bold ph-lg"></i></template>
+	<template #icon><i class="ti ti-chart-line"></i></template>
 	<template #header>{{ i18n.ts.activity }}</template>
 	<template #func="{ buttonStyleClass }">
 		<button class="_button" :class="buttonStyleClass" @click="showMenu">
-			<i class="ph-dots-three ph-bold ph-lg"></i>
+			<i class="ti ti-dots"></i>
 		</button>
 	</template>
 
diff --git a/packages/frontend/src/pages/user/index.files.vue b/packages/frontend/src/pages/user/index.files.vue
index be58cec24a..1430ae1296 100644
--- a/packages/frontend/src/pages/user/index.files.vue
+++ b/packages/frontend/src/pages/user/index.files.vue
@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <template>
 <MkContainer :max-height="300" :foldable="true">
-	<template #icon><i class="ph-image-square ph-bold ph-lg"></i></template>
+	<template #icon><i class="ti ti-photo"></i></template>
 	<template #header>{{ i18n.ts.files }}</template>
 	<div :class="$style.root">
 		<MkLoading v-if="fetching"/>
@@ -16,7 +16,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 					<ImgWithBlurhash :class="$style.sensitiveImg" :hash="file.file.blurhash" :src="thumbnail(file.file)" :title="file.file.name" :forceBlurhash="true"/>
 					<div :class="$style.sensitive">
 						<div>
-							<div><i class="ph-eye-slash ph-bold ph-lg"></i> {{ i18n.ts.sensitive }}</div>
+							<div><i class="ti ti-eye-exclamation"></i> {{ i18n.ts.sensitive }}</div>
 							<div>{{ i18n.ts.clickToShow }}</div>
 						</div>
 					</div>
diff --git a/packages/frontend/src/pages/user/index.listenbrainz.vue b/packages/frontend/src/pages/user/index.listenbrainz.vue
index 965a029fac..18092d9d87 100644
--- a/packages/frontend/src/pages/user/index.listenbrainz.vue
+++ b/packages/frontend/src/pages/user/index.listenbrainz.vue
@@ -24,7 +24,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 				</a>
 				<a :href="listenbrainz.listenbrainzurl">
 					<div class="playicon">
-						<i class="ph-play ph-bold ph-lg-filled"></i>
+						<i class="ph-play ph-bold ph-lg"></i>
 					</div>
 				</a>
 			</div>
diff --git a/packages/frontend/src/pages/user/index.vue b/packages/frontend/src/pages/user/index.vue
index ebe4176196..87c8bb2866 100644
--- a/packages/frontend/src/pages/user/index.vue
+++ b/packages/frontend/src/pages/user/index.vue
@@ -83,39 +83,39 @@ const headerActions = computed(() => []);
 const headerTabs = computed(() => user.value ? [{
 	key: 'home',
 	title: i18n.ts.overview,
-	icon: 'ph-house ph-bold ph-lg',
+	icon: 'ti ti-home',
 }, {
 	key: 'notes',
 	title: i18n.ts.notes,
-	icon: 'ph-pencil-simple ph-bold ph-lg',
+	icon: 'ti ti-pencil',
 }, {
 	key: 'activity',
 	title: i18n.ts.activity,
-	icon: 'ph-chart-line ph-bold ph-lg',
+	icon: 'ti ti-chart-line',
 }, ...(user.value.host == null ? [{
 	key: 'achievements',
 	title: i18n.ts.achievements,
-	icon: 'ph-trophy ph-bold ph-lg',
+	icon: 'ti ti-medal',
 }] : []), ...($i && ($i.id === user.value.id || $i.isAdmin || $i.isModerator)) || user.value.publicReactions ? [{
 	key: 'reactions',
 	title: i18n.ts.reaction,
-	icon: 'ph-smiley ph-bold ph-lg',
+	icon: 'ti ti-mood-happy',
 }] : [], {
 	key: 'clips',
 	title: i18n.ts.clips,
-	icon: 'ph-paperclip ph-bold ph-lg',
+	icon: 'ti ti-paperclip',
 }, {
 	key: 'lists',
 	title: i18n.ts.lists,
-	icon: 'ph-list ph-bold ph-lg',
+	icon: 'ti ti-list',
 }, {
 	key: 'pages',
 	title: i18n.ts.pages,
-	icon: 'ph-newspaper ph-bold ph-lg',
+	icon: 'ti ti-news',
 }, {
 	key: 'flashs',
 	title: 'Play',
-	icon: 'ph-play ph-bold ph-lg',
+	icon: 'ti ti-player-play',
 }, {
 	key: 'gallery',
 	title: i18n.ts.gallery,
@@ -123,12 +123,12 @@ const headerTabs = computed(() => user.value ? [{
 }, {
 	key: 'raw',
 	title: 'Raw',
-	icon: 'ph-code ph-bold ph-lg',
+	icon: 'ti ti-code',
 }] : []);
 
 definePageMetadata(() => ({
 	title: i18n.ts.user,
-	icon: 'ph-user ph-bold ph-lg',
+	icon: 'ti ti-user',
 	...user.value ? {
 		title: user.value.name ? ` (@${user.value.username})` : `@${user.value.username}`,
 		subtitle: `@${getAcct(user.value)}`,
diff --git a/packages/frontend/src/pages/welcome.setup.vue b/packages/frontend/src/pages/welcome.setup.vue
index 7d5861d2ae..31911649ac 100644
--- a/packages/frontend/src/pages/welcome.setup.vue
+++ b/packages/frontend/src/pages/welcome.setup.vue
@@ -21,7 +21,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 				</MkInput>
 				<MkInput v-model="password" type="password" data-cy-admin-password>
 					<template #label>{{ i18n.ts.password }}</template>
-					<template #prefix><i class="ph-lock ph-bold ph-lg"></i></template>
+					<template #prefix><i class="ti ti-lock"></i></template>
 				</MkInput>
 				<div>
 					<MkButton gradate large rounded type="submit" :disabled="submitting" data-cy-admin-ok style="margin: 0 auto;">
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..252b1a2955
--- /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" :isBlock="true" :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" :isBlock="true" :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(--panel), var(--X15));
+	}
+}
+
+.content {
+	padding: 16px;
+	margin: 0 0 0 auto;
+	max-width: max-content;
+	border-radius: var(--radius-md);
+}
+
+.reactions {
+	box-sizing: border-box;
+	margin: 8px -16px -8px;
+	padding: 8px 16px 0;
+	width: calc(100% + 32px);
+	border-top: 1px solid var(--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 4768e31d1d..045f424cda 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="ph-arrow-u-up-left ph-bold ph-lg"></i></MkA>
-					<Mfm v-if="note.text" :text="note.text" :isBlock="true" :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';
 
 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.filter(n => n.cw == null);
 });
 
+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(--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(--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: var(--radius-md);
-}
-
-.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/plugin.ts b/packages/frontend/src/plugin.ts
index a0a624ace7..81233a5a5e 100644
--- a/packages/frontend/src/plugin.ts
+++ b/packages/frontend/src/plugin.ts
@@ -28,31 +28,10 @@ export async function install(plugin: Plugin): Promise<void> {
 		},
 		log: (): void => {
 		},
-		/* dakkar 2024-06-20
-
-			passing an `err` triggers an unwanted side-effect inside the
-			AiScript Interpreter:
-
-			- the plugin code throws an exception of any kind (in the
-			specific case that made us look, it was `note.text.split(...)`
-			on a note with no text)
-
-			- the Interpreter's `handleError` calls `this.abort()` before
-			calling our `err`
-
-			- from that point on, every evaluation of that Interpreter object
-			returns null
-
-			- which, at least inside a noteViewInterruptor, causes all notes
-      to be replaced with a null
-
-			I'm reporting this problem upstream, in the meantime we'll have
-			to do without error logs
-		*/
-		// err: (err): void => {
-		// 	pluginLogs.value.get(plugin.id).push(`${err}`);
-		// 	throw err; // install時のtry-catchに反応させる
-		// },
+		err: (err): void => {
+			pluginLogs.value.get(plugin.id).push(`${err}`);
+			throw err; // install時のtry-catchに反応させる
+		},
 	});
 
 	initPlugin({ plugin, aiscript });
diff --git a/packages/frontend/src/router/definition.ts b/packages/frontend/src/router/definition.ts
index f18dac2a44..14110d1f9b 100644
--- a/packages/frontend/src/router/definition.ts
+++ b/packages/frontend/src/router/definition.ts
@@ -232,13 +232,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 +264,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')),
@@ -475,6 +491,14 @@ const routes: RouteDef[] = [{
 		path: '/approvals',
 		name: 'approvals',
 		component: page(() => import('@/pages/admin/approvals.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')),
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/copy-to-clipboard.ts b/packages/frontend/src/scripts/copy-to-clipboard.ts
index 216c0464b3..08f5b52dae 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..eb2da5ad86 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 '@/scripts/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/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-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 a883404307..108648d640 100644
--- a/packages/frontend/src/scripts/get-drive-file-menu.ts
+++ b/packages/frontend/src/scripts/get-drive-file-menu.ts
@@ -6,7 +6,7 @@
 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';
@@ -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) {
@@ -82,53 +92,57 @@ export function getDriveFileMenu(file: Misskey.entities.DriveFile, folder?: Miss
 		type: 'link',
 		to: `/my/drive/file/${file.id}`,
 		text: i18n.ts._fileViewer.title,
-		icon: 'ph-file-text ph-bold ph-lg',
+		icon: 'ti ti-info-circle',
 	}, { type: 'divider' }, {
 		text: i18n.ts.rename,
-		icon: 'ph-textbox ph-bold ph-lg',
+		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 ? 'ph-eye ph-bold ph-lg' : 'ph-eye-closed ph-bold ph-lg',
+		icon: file.isSensitive ? 'ti ti-eye' : 'ti ti-eye-exclamation',
 		action: () => toggleSensitive(file),
 	}, {
 		text: i18n.ts.describeFile,
-		icon: 'ph-text-indent ph-bold ph-lg',
+		icon: 'ti ti-text-caption',
 		action: () => describe(file),
 	}, ...isImage ? [{
 		text: i18n.ts.cropImage,
-		icon: 'ph-crop ph-bold ph-lg',
+		icon: 'ti ti-crop',
 		action: () => os.cropImage(file, {
 			aspectRatio: NaN,
 			uploadFolder: folder ? folder.id : folder,
 		}),
 	}] : [], { type: 'divider' }, {
 		text: i18n.ts.createNoteFromTheFile,
-		icon: 'ph-pencil-simple ph-bold ph-lg',
+		icon: 'ti ti-pencil',
 		action: () => os.post({
 			initialFiles: [file],
 		}),
 	}, {
 		text: i18n.ts.copyUrl,
-		icon: 'ph-link ph-bold ph-lg',
+		icon: 'ti ti-link',
 		action: () => copyUrl(file),
 	}, {
 		type: 'a',
 		href: file.url,
 		target: '_blank',
 		text: i18n.ts.download,
-		icon: 'ph-download ph-bold ph-lg',
+		icon: 'ti ti-download',
 		download: file.name,
 	}, { type: 'divider' }, {
 		text: i18n.ts.delete,
-		icon: 'ph-trash ph-bold ph-lg',
+		icon: 'ti ti-trash',
 		danger: true,
 		action: () => deleteFile(file),
 	}];
 
 	if (defaultStore.state.devMode) {
 		menu = menu.concat([{ type: 'divider' }, {
-			icon: 'ph-identification-card ph-bold ph-lg',
+			icon: 'ti ti-id',
 			text: i18n.ts.copyFileId,
 			action: () => {
 				copyToClipboard(file.id);
diff --git a/packages/frontend/src/scripts/get-note-menu.ts b/packages/frontend/src/scripts/get-note-menu.ts
index 23c98c1881..a7ec4ce6d7 100644
--- a/packages/frontend/src/scripts/get-note-menu.ts
+++ b/packages/frontend/src/scripts/get-note-menu.ts
@@ -11,7 +11,7 @@ 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 { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
 import { url } from '@/config.js';
 import { defaultStore, noteActions } from '@/store.js';
 import { miLocalStorage } from '@/local-storage.js';
@@ -20,6 +20,7 @@ import { clipsCache, favoritedChannelsCache } from '@/cache.js';
 import { 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';
 
 export async function getNoteClipMenu(props: {
 	note: Misskey.entities.Note;
@@ -34,14 +35,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 => ({
@@ -93,7 +87,7 @@ export async function getNoteClipMenu(props: {
 			});
 		},
 	})), { type: 'divider' }, {
-		icon: 'ph-plus ph-bold ph-lg',
+		icon: 'ti ti-plus',
 		text: i18n.ts.createNew,
 		action: async () => {
 			const { canceled, result } = await os.form(i18n.ts.createNewClip, {
@@ -129,24 +123,26 @@ export async function getNoteClipMenu(props: {
 
 export function getAbuseNoteMenu(note: Misskey.entities.Note, text: string): MenuItem {
 	return {
-		icon: 'ph-warning-circle ph-bold ph-lg',
+		icon: 'ti ti-exclamation-circle',
 		text,
 		action: (): void => {
 			const localUrl = `${url}/notes/${note.id}`;
 			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(),
+			});
 		},
 	};
 }
 
 export function getCopyNoteLinkMenu(note: Misskey.entities.Note, text: string): MenuItem {
 	return {
-		icon: 'ph-link ph-bold ph-lg',
+		icon: 'ti ti-link',
 		text,
 		action: (): void => {
 			copyToClipboard(`${url}/notes/${note.id}`);
@@ -173,14 +169,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)[];
 
@@ -268,6 +257,7 @@ export function getNoteMenu(props: {
 	}
 
 	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;
 	}
@@ -287,8 +277,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}`,
 		});
 	}
@@ -317,17 +307,17 @@ export function getNoteMenu(props: {
 		menu = [
 			...(
 				props.currentClip?.userId === $i.id ? [{
-					icon: 'ph-backspace ph-bold ph-lg',
+					icon: 'ti ti-backspace',
 					text: i18n.ts.unclip,
 					danger: true,
 					action: unclip,
 				}, { type: 'divider' }] : []
 			), {
-				icon: 'ph-info ph-bold ph-lg',
+				icon: 'ti ti-info-circle',
 				text: i18n.ts.details,
 				action: openDetail,
 			}, {
-				icon: 'ph-copy ph-bold ph-lg',
+				icon: 'ti ti-copy',
 				text: i18n.ts.copyContent,
 				action: copyContent,
 			}, getCopyNoteLinkMenu(appearNote, i18n.ts.copyLink)
@@ -335,59 +325,59 @@ export function getNoteMenu(props: {
 				getCopyNoteOriginLinkMenu(appearNote, 'Copy link (Origin)')
 			: undefined,
 			(appearNote.url || appearNote.uri) ? {
-				icon: 'ph-arrow-square-out ph-bold ph-lg',
+				icon: 'ti ti-external-link',
 				text: i18n.ts.showOnRemote,
 				action: () => {
 					window.open(appearNote.url ?? appearNote.uri, '_blank', 'noopener');
 				},
 			} : undefined,
 			...(isSupportShare() ? [{
-				icon: 'ph-share-network ph-bold ph-lg',
+				icon: 'ti ti-share',
 				text: i18n.ts.share,
 				action: share,
 			}] : []),
 			$i && $i.policies.canUseTranslator && instance.translatorAvailable ? {
-				icon: 'ph-translate ph-bold ph-lg',
+				icon: 'ti ti-language-hiragana',
 				text: i18n.ts.translate,
 				action: translate,
 			} : undefined,
 			{ type: 'divider' },
 			statePromise.then(state => state.isFavorited ? {
-				icon: 'ph-star-half ph-bold ph-lg',
+				icon: 'ti ti-star-off',
 				text: i18n.ts.unfavorite,
 				action: () => toggleFavorite(false),
 			} : {
-				icon: 'ph-star ph-bold ph-lg',
+				icon: 'ti ti-star',
 				text: i18n.ts.favorite,
 				action: () => toggleFavorite(true),
 			}),
 			{
 				type: 'parent' as const,
-				icon: 'ph-paperclip ph-bold ph-lg',
+				icon: 'ti ti-paperclip',
 				text: i18n.ts.clip,
 				children: () => getNoteClipMenu(props),
 			},
 			statePromise.then(state => state.isMutedThread ? {
-				icon: 'ph-bell-slash ph-bold ph-lg',
+				icon: 'ti ti-message-off',
 				text: i18n.ts.unmuteThread,
 				action: () => toggleThreadMute(false),
 			} : {
-				icon: 'ph-bell-slash ph-bold ph-lg',
+				icon: 'ti ti-message-off',
 				text: i18n.ts.muteThread,
 				action: () => toggleThreadMute(true),
 			}),
 			appearNote.userId === $i.id ? ($i.pinnedNoteIds ?? []).includes(appearNote.id) ? {
-				icon: 'ph-push-pin-slash ph-bold ph-lg',
+				icon: 'ti ti-pinned-off',
 				text: i18n.ts.unpin,
 				action: () => togglePin(false),
 			} : {
-				icon: 'ph-push-pin ph-bold ph-lg',
+				icon: 'ti ti-pin',
 				text: i18n.ts.pin,
 				action: () => togglePin(true),
 			} : undefined,
 			{
 				type: 'parent' as const,
-				icon: 'ph-user ph-bold ph-lg',
+				icon: 'ti ti-user',
 				text: i18n.ts.user,
 				children: async () => {
 					const user = appearNote.userId === $i?.id ? $i : await misskeyApi('users/show', { userId: appearNote.userId });
@@ -400,7 +390,7 @@ export function getNoteMenu(props: {
 		...($i.isModerator || $i.isAdmin ? [
 			{ type: 'divider' },
 			{
-				icon: 'ph-megaphone ph-bold ph-lg',
+				icon: 'ti ti-speakerphone',
 				text: i18n.ts.promote,
 				action: promote
 			}]
@@ -416,7 +406,7 @@ export function getNoteMenu(props: {
 				{ type: 'divider' },
 				{
 					type: 'parent' as const,
-					icon: 'ph-television ph-bold ph-lg',
+					icon: 'ti ti-device-tv',
 					text: i18n.ts.channel,
 					children: async () => {
 						const channelChildMenu = [] as MenuItem[];
@@ -425,7 +415,7 @@ export function getNoteMenu(props: {
 
 						if (channel.pinnedNoteIds.includes(appearNote.id)) {
 							channelChildMenu.push({
-								icon: 'ph-push-pin-slash ph-bold ph-lg',
+								icon: 'ti ti-pinned-off',
 								text: i18n.ts.unpin,
 								action: () => os.apiWithDialog('channels/update', {
 									channelId: appearNote.channel!.id,
@@ -434,7 +424,7 @@ export function getNoteMenu(props: {
 							});
 						} else {
 							channelChildMenu.push({
-								icon: 'ph-push-pin ph-bold ph-lg',
+								icon: 'ti ti-pin',
 								text: i18n.ts.pin,
 								action: () => os.apiWithDialog('channels/update', {
 									channelId: appearNote.channel!.id,
@@ -456,13 +446,13 @@ export function getNoteMenu(props: {
 					action: edit,
 				} : undefined,
 				{
-					icon: 'ph-pencil-simple-line ph-bold ph-lg',
+					icon: 'ti ti-edit',
 					text: i18n.ts.deleteAndEdit,
 					danger: true,
 					action: delEdit,
 				},
 				{
-					icon: 'ph-trash ph-bold ph-lg',
+					icon: 'ti ti-trash',
 					text: i18n.ts.delete,
 					danger: true,
 					action: del,
@@ -472,11 +462,11 @@ export function getNoteMenu(props: {
 			.filter(x => x !== undefined);
 	} else {
 		menu = [{
-			icon: 'ph-info ph-bold ph-lg',
+			icon: 'ti ti-info-circle',
 			text: i18n.ts.details,
 			action: openDetail,
 		}, {
-			icon: 'ph-copy ph-bold ph-lg',
+			icon: 'ti ti-copy',
 			text: i18n.ts.copyContent,
 			action: copyContent,
 		}, getCopyNoteLinkMenu(appearNote, i18n.ts.copyLink)
@@ -484,7 +474,7 @@ export function getNoteMenu(props: {
 			getCopyNoteOriginLinkMenu(appearNote, 'Copy link (Origin)')
 		: undefined,
 		(appearNote.url || appearNote.uri) ? {
-			icon: 'ph-arrow-square-out ph-bold ph-lg',
+			icon: 'ti ti-external-link',
 			text: i18n.ts.showOnRemote,
 			action: () => {
 				window.open(appearNote.url ?? appearNote.uri, '_blank', 'noopener');
@@ -495,7 +485,7 @@ export function getNoteMenu(props: {
 
 	if (noteActions.length > 0) {
 		menu = menu.concat([{ type: "divider" }, ...noteActions.map(action => ({
-			icon: 'ph-plug ph-bold ph-lg',
+			icon: 'ti ti-plug',
 			text: action.title,
 			action: () => {
 				action.handler(appearNote);
@@ -505,7 +495,7 @@ export function getNoteMenu(props: {
 
 	if (defaultStore.state.devMode) {
 		menu = menu.concat([{ type: "divider" }, {
-			icon: 'ph-identification-card ph-bold ph-lg',
+			icon: 'ti ti-id',
 			text: i18n.ts.copyNoteId,
 			action: () => {
 				copyToClipboard(appearNote.id);
@@ -541,14 +531,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[] = [];
@@ -557,14 +540,16 @@ export function getRenoteMenu(props: {
 	if (appearNote.channel) {
 		channelRenoteItems.push(...[{
 			text: i18n.ts.inChannelRenote,
-			icon: 'ph ph-repeat',
+			icon: 'ti ti-repeat',
 			action: () => {
 				const el = props.renoteButton.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(),
+					});
 				}
 
 				if (!props.mock) {
@@ -578,7 +563,7 @@ export function getRenoteMenu(props: {
 			},
 		}, {
 			text: i18n.ts.inChannelQuote,
-			icon: 'ph ph-quotes',
+			icon: 'ti ti-quote',
 			action: () => {
 				if (!props.mock) {
 					os.post({
@@ -593,14 +578,16 @@ export function getRenoteMenu(props: {
 	if (!appearNote.channel || appearNote.channel.allowRenoteToExternal) {
 		normalRenoteItems.push(...[{
 			text: i18n.ts.renote,
-			icon: 'ph ph-repeat',
+			icon: 'ti ti-repeat',
 			action: () => {
 				const el = props.renoteButton.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(),
+					});
 				}
 
 				const configuredVisibility = defaultStore.state.rememberNoteVisibility ? defaultStore.state.visibility : defaultStore.state.defaultNoteVisibility;
@@ -624,7 +611,7 @@ export function getRenoteMenu(props: {
 			},
 		}, (props.mock) ? undefined : {
 			text: i18n.ts.quote,
-			icon: 'ph ph-quotes',
+			icon: 'ti ti-quote',
 			action: () => {
 				os.post({
 					renote: appearNote,
@@ -634,7 +621,7 @@ export function getRenoteMenu(props: {
 
 		normalExternalChannelRenoteItems.push({
 			type: 'parent',
-			icon: 'ph ph-repeat',
+			icon: 'ti ti-repeat',
 			text: appearNote.channel ? i18n.ts.renoteToOtherChannel : i18n.ts.renoteToChannel,
 			children: async () => {
 				const channels = await favoritedChannelsCache.fetch();
@@ -649,7 +636,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-note-versions-menu.ts b/packages/frontend/src/scripts/get-note-versions-menu.ts
index 9108191d73..345cec9018 100644
--- a/packages/frontend/src/scripts/get-note-versions-menu.ts
+++ b/packages/frontend/src/scripts/get-note-versions-menu.ts
@@ -27,12 +27,13 @@ export async function getNoteVersionsMenu(props: {
 	const cleanups = [] as (() => void)[];
 
 	function openVersion(info): void {
-		os.popup(defineAsyncComponent(() => import('@/components/SkOldNoteWindow.vue')), {
+		const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/SkOldNoteWindow.vue')), {
 			note: appearNote,
 			oldText: info.text,
 			updatedAt: info.oldDate ? info.oldDate : info.updatedAt,
 		}, {
-		}, 'closed');
+			closed: () => dispose(),
+		});
 	}
 
 	const menu: MenuItem[] = [];
diff --git a/packages/frontend/src/scripts/get-user-menu.ts b/packages/frontend/src/scripts/get-user-menu.ts
index 3329a34ff3..33f16a68aa 100644
--- a/packages/frontend/src/scripts/get-user-menu.ts
+++ b/packages/frontend/src/scripts/get-user-menu.ts
@@ -7,15 +7,17 @@ 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 { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
 import { host, url } from '@/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 { MenuItem } from '@/types/menu.js';
 
 export function getUserMenu(user: Misskey.entities.UserDetailed, router: IRouter = mainRouter) {
 	const meId = $i ? $i.id : null;
@@ -81,15 +83,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 +93,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,54 +147,61 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: IRouter
 		});
 	}
 
-	let menu = [{
-		icon: 'ph-at ph-bold ph-lg',
+	let menu: MenuItem[] = [{
+		icon: 'ti ti-at',
 		text: i18n.ts.copyUsername,
 		action: () => {
 			copyToClipboard(`@${user.username}@${user.host ?? host}`);
 		},
-	}, ...(iAmModerator ? [{
-		icon: 'ph-warning-circle ph-bold ph-lg',
+	}, ...( notesSearchAvailable && (user.host == null || canSearchNonLocalNotes) ? [{
+		icon: 'ti ti-search',
+		text: i18n.ts.searchThisUsersNotes,
+		action: () => {
+			router.push(`/search?username=${encodeURIComponent(user.username)}${user.host != null ? '&host=' + encodeURIComponent(user.host) : ''}`);
+		},
+	}] : [])
+	, ...(iAmModerator ? [{
+		icon: 'ti ti-user-exclamation',
 		text: i18n.ts.moderation,
 		action: () => {
 			router.push(`/admin/user/${user.id}`);
 		},
 	}] : []), {
-		icon: 'ph-rss ph-bold ph-lg',
+		icon: 'ti ti-rss',
 		text: i18n.ts.copyRSS,
 		action: () => {
 			copyToClipboard(`${user.host ?? host}/@${user.username}.atom`);
 		},
 	}, ...(user.host != null && user.url != null ? [{
-		icon: 'ph-share ph-bold ph-lg',
+		icon: 'ti ti-external-link',
 		text: i18n.ts.showOnRemote,
 		action: () => {
 			if (user.url == null) return;
 			window.open(user.url, '_blank', 'noopener');
 		},
 	}] : []), {
-		icon: 'ph-share-network ph-bold ph-lg',
+		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: 'ph-envelope ph-bold ph-lg',
+	}, ...($i ? [{
+		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: 'ph-pencil-simple ph-bold ph-lg',
+		icon: 'ti ti-pencil',
 		text: i18n.ts.editMemo,
 		action: () => {
 			editMemo();
 		},
 	}, {
 		type: 'parent',
-		icon: 'ph-list ph-bold ph-lg',
+		icon: 'ti ti-list',
 		text: i18n.ts.addToList,
 		children: async () => {
 			const lists = await userListsCache.fetch();
@@ -232,7 +234,7 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: IRouter
 		},
 	}, {
 		type: 'parent',
-		icon: 'ph-flying-saucer ph-bold ph-lg',
+		icon: 'ti ti-antenna',
 		text: i18n.ts.addToAntenna,
 		children: async () => {
 			const antennas = await antennasCache.fetch();
@@ -257,13 +259,13 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: IRouter
 				},
 			}));
 		},
-	}] as any;
+	}] : [])] as any;
 
 	if ($i && meId !== user.id) {
 		if (iAmModerator) {
 			menu = menu.concat([{
 				type: 'parent',
-				icon: 'ph-seal-check ph-bold ph-lg',
+				icon: 'ti ti-badges',
 				text: i18n.ts.roles,
 				children: async () => {
 					const roles = await rolesCache.fetch();
@@ -304,41 +306,51 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: IRouter
 
 		// フォローしたとしても user.isFollowing はリアルタイム更新されないので不便なため
 		//if (user.isFollowing) {
+		const withRepliesRef = ref(user.withReplies);
 		menu = menu.concat([{
-			icon: user.withReplies ? 'ph-envelope-open ph-bold ph-lg' : 'ph-envelope ph-bold ph-lg-off',
-			text: user.withReplies ? i18n.ts.hideRepliesToOthersInTimeline : i18n.ts.showRepliesToOthersInTimeline,
-			action: toggleWithReplies,
+			type: 'switch',
+			icon: 'ti ti-messages',
+			text: i18n.ts.showRepliesToOthersInTimeline,
+			ref: withRepliesRef,
 		}, {
-			icon: user.notify === 'none' ? 'ph-bell ph-bold ph-lg' : 'ph-bell ph-bold ph-lg-off',
+			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' }, {
-			icon: user.isMuted ? 'ph-eye ph-bold ph-lg' : 'ph-eye-slash ph-bold ph-lg',
+			icon: user.isMuted ? 'ti ti-eye' : 'ti ti-eye-off',
 			text: user.isMuted ? i18n.ts.unmute : i18n.ts.mute,
 			action: toggleMute,
 		}, {
-			icon: user.isRenoteMuted ? 'ph-repeat ph-bold ph-lg' : 'ph-repeat ph-bold ph-lg-off',
+			icon: user.isRenoteMuted ? 'ti ti-repeat' : 'ti ti-repeat-off',
 			text: user.isRenoteMuted ? i18n.ts.renoteUnmute : i18n.ts.renoteMute,
 			action: toggleRenoteMute,
 		}, {
-			icon: 'ph-prohibit ph-bold ph-lg',
+			icon: 'ti ti-ban',
 			text: user.isBlocking ? i18n.ts.unblock : i18n.ts.block,
 			action: toggleBlock,
 		}]);
 
 		if (user.isFollowed) {
 			menu = menu.concat([{
-				icon: 'ph-link ph-bold ph-lg-off',
+				icon: 'ti ti-link-off',
 				text: i18n.ts.breakFollow,
 				action: invalidateFollow,
 			}]);
 		}
 
 		menu = menu.concat([{ type: 'divider' }, {
-			icon: 'ph-warning-circle ph-bold ph-lg',
+			icon: 'ti ti-exclamation-circle',
 			text: i18n.ts.reportAbuse,
 			action: reportAbuse,
 		}]);
@@ -346,7 +358,7 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: IRouter
 
 	if (user.host !== null) {
 		menu = menu.concat([{ type: 'divider' }, {
-			icon: 'ph-arrows-counter-clockwise ph-bold ph-lg',
+			icon: 'ti ti-refresh',
 			text: i18n.ts.updateRemoteUser,
 			action: userInfoUpdate,
 		}]);
@@ -354,7 +366,7 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: IRouter
 
 	if (defaultStore.state.devMode) {
 		menu = menu.concat([{ type: 'divider' }, {
-			icon: 'ph-identification-card ph-bold ph-lg',
+			icon: 'ti ti-id',
 			text: i18n.ts.copyUserId,
 			action: () => {
 				copyToClipboard(user.id);
@@ -364,7 +376,7 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: IRouter
 
 	if ($i && meId === user.id) {
 		menu = menu.concat([{ type: 'divider' }, {
-			icon: 'ph-pencil-simple ph-bold ph-lg',
+			icon: 'ti ti-pencil',
 			text: i18n.ts.editProfile,
 			action: () => {
 				router.push('/settings/profile');
@@ -374,7 +386,7 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: IRouter
 
 	if (userActions.length > 0) {
 		menu = menu.concat([{ type: 'divider' }, ...userActions.map(action => ({
-			icon: 'ph-plug ph-bold ph-lg',
+			icon: 'ti ti-plug',
 			text: action.title,
 			action: () => {
 				action.handler(user);
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/install-plugin.ts b/packages/frontend/src/scripts/install-plugin.ts
index 15b0cedc79..72ff8bd5ff 100644
--- a/packages/frontend/src/scripts/install-plugin.ts
+++ b/packages/frontend/src/scripts/install-plugin.ts
@@ -107,7 +107,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,
@@ -122,7 +122,8 @@ export async function installPlugin(code: string, meta?: AiScriptPluginMeta) {
 				});
 				res(token);
 			},
-		}, 'closed');
+			closed: () => dispose(),
+		});
 	});
 
 	savePlugin({
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 db3a96b15c..e20b23f166 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/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 36de146c27..63acf9d3de 100644
--- a/packages/frontend/src/scripts/mfm-function-picker.ts
+++ b/packages/frontend/src/scripts/mfm-function-picker.ts
@@ -7,29 +7,24 @@ import { Ref, nextTick } from 'vue';
 import * as os from '@/os.js';
 import { i18n } from '@/i18n.js';
 import { MFM_TAGS } from '@/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: 'ph-brackets-curly ph-bold ph-lg',
-			action: () => add(textArea, textRef, tag),
-		});
-	});
-	return ret;
+function getFunctionList(textArea: HTMLInputElement | HTMLTextAreaElement, textRef: Ref<string>): MenuItem[] {
+	return MFM_TAGS.map(tag => ({
+		text: tag,
+		icon: 'ph-brackets-curly ph-bold ph-lg',
+		action: () => add(textArea, textRef, tag),
+	}));
 }
 
 function add(textArea: HTMLInputElement | HTMLTextAreaElement, textRef: Ref<string>, type: string) {
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..53b2a9e441
--- /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 '@/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..18f05bc7f4 100644
--- a/packages/frontend/src/scripts/please-login.ts
+++ b/packages/frontend/src/scripts/please-login.ts
@@ -8,19 +8,57 @@ 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(path?: 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: openOnRemote ? i18n.ts.signinOrContinueOnRemote : i18n.ts.signinRequired,
+		openOnRemote,
 	}, {
 		cancelled: () => {
 			if (path) {
 				window.location.href = path;
 			}
 		},
-	}, 'closed');
+		closed: () => dispose(),
+	});
 
 	throw new Error('signin required');
 }
diff --git a/packages/frontend/src/scripts/scroll.ts b/packages/frontend/src/scripts/scroll.ts
index 8edb6fca05..f0274034b5 100644
--- a/packages/frontend/src/scripts/scroll.ts
+++ b/packages/frontend/src/scripts/scroll.ts
@@ -23,6 +23,14 @@ 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;
diff --git a/packages/frontend/src/scripts/select-file.ts b/packages/frontend/src/scripts/select-file.ts
index fd7cfc697b..9aa38178b2 100644
--- a/packages/frontend/src/scripts/select-file.ts
+++ b/packages/frontend/src/scripts/select-file.ts
@@ -93,15 +93,15 @@ function select(src: any, label: string | null, multiple: boolean): Promise<Miss
 			ref: keepOriginal,
 		}, {
 			text: i18n.ts.upload,
-			icon: 'ph-upload ph-bold ph-lg',
+			icon: 'ti ti-upload',
 			action: () => chooseFileFromPc(multiple, keepOriginal.value).then(files => res(files)),
 		}, {
 			text: i18n.ts.fromDrive,
-			icon: 'ph-cloud ph-bold ph-lg',
+			icon: 'ti ti-cloud',
 			action: () => chooseFileFromDrive(multiple).then(files => res(files)),
 		}, {
 			text: i18n.ts.fromUrl,
-			icon: 'ph-link ph-bold ph-lg',
+			icon: 'ti ti-link',
 			action: () => chooseFileFromUrl().then(file => res([file])),
 		}], src);
 	});
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/url.ts b/packages/frontend/src/scripts/url.ts
index e3072b3b7d..5a8265af9e 100644
--- a/packages/frontend/src/scripts/url.ts
+++ b/packages/frontend/src/scripts/url.ts
@@ -21,3 +21,8 @@ export function query(obj: Record<string, any>): string {
 export function appendQuery(url: string, query: string): string {
 	return `${url}${/\?/.test(url) ? url.endsWith('?') ? '' : '&' : '?'}${query}`;
 }
+
+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-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/store.ts b/packages/frontend/src/store.ts
index 13f544e588..dda320dbac 100644
--- a/packages/frontend/src/store.ts
+++ b/packages/frontend/src/store.ts
@@ -520,6 +520,14 @@ 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',
+  },
 
 	sound_masterVolume: {
 		where: 'device',
@@ -545,14 +553,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,
diff --git a/packages/frontend/src/style.scss b/packages/frontend/src/style.scss
index 37d1a3c557..62ba7a08d5 100644
--- a/packages/frontend/src/style.scss
+++ b/packages/frontend/src/style.scss
@@ -143,6 +143,10 @@ a {
 	-webkit-tap-highlight-color: transparent;
 	-webkit-touch-callout: none;
 
+	&:focus-visible {
+		outline-offset: 2px;
+	}
+
 	&:hover {
 		text-decoration: underline;
 	}
@@ -173,12 +177,21 @@ rt {
 	white-space: initial;
 }
 
+:focus-visible {
+	outline: var(--focus) solid 2px;
+	outline-offset: -2px;
+
+	&:hover {
+		text-decoration: none;
+	}
+}
+
 .ph-bold {
 	width: 1.28em;
 	vertical-align: -12%;
 	line-height: 1em;
 
-	&:before {
+	&::before {
 		font-size: 128%;
 	}
 }
@@ -264,10 +277,6 @@ rt {
 		text-decoration: none;
 	}
 
-	&:focus-visible {
-		outline: none;
-	}
-
 	&:disabled {
 		opacity: 0.5;
 		cursor: default;
@@ -304,13 +313,17 @@ rt {
 
 ._help {
 	color: var(--accent);
-	cursor: help
+	cursor: help;
 }
 
 ._textButton {
 	@extend ._button;
 	color: var(--accent);
 
+	&:focus-visible {
+		outline-offset: 2px;
+	}
+
 	&:not(:disabled):hover {
 		text-decoration: underline;
 	}
diff --git a/packages/frontend/src/timelines.ts b/packages/frontend/src/timelines.ts
new file mode 100644
index 0000000000..5080ef4b96
--- /dev/null
+++ b/packages/frontend/src/timelines.ts
@@ -0,0 +1,61 @@
+/*
+ * 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',
+	'bubble',
+	'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 'bubble':
+			return 'ph-drop ph-bold ph-lg';
+		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 'bubble':
+			return ($i == null && instance.policies.btlAvailable) || ($i != null && $i.policies.btlAvailable);
+		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/ui/_common_/announcements.vue b/packages/frontend/src/ui/_common_/announcements.vue
index 37d89f682f..374bc20b54 100644
--- a/packages/frontend/src/ui/_common_/announcements.vue
+++ b/packages/frontend/src/ui/_common_/announcements.vue
@@ -12,10 +12,10 @@ SPDX-License-Identifier: AGPL-3.0-only
 		:to="`/announcements/${announcement.id}`"
 	>
 		<span :class="$style.icon">
-			<i v-if="announcement.icon === 'info'" class="ph-info ph-bold ph-lg"></i>
-			<i v-else-if="announcement.icon === 'warning'" class="ph-warning ph-bold ph-lg" style="color: var(--warn);"></i>
-			<i v-else-if="announcement.icon === 'error'" class="ph-x-circle ph-bold ph-lg" style="color: var(--error);"></i>
-			<i v-else-if="announcement.icon === 'success'" class="ph-check ph-bold ph-lg" style="color: var(--success);"></i>
+			<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>
 		</span>
 		<span :class="$style.title">{{ announcement.title }}</span>
 		<span :class="$style.body">{{ announcement.text }}</span>
diff --git a/packages/frontend/src/ui/_common_/common.ts b/packages/frontend/src/ui/_common_/common.ts
index f1c23860b6..17079b3ddc 100644
--- a/packages/frontend/src/ui/_common_/common.ts
+++ b/packages/frontend/src/ui/_common_/common.ts
@@ -16,17 +16,17 @@ function toolsMenuItems(): MenuItem[] {
 		type: 'link',
 		to: '/scratchpad',
 		text: i18n.ts.scratchpad,
-		icon: 'ph-terminal-window ph-bold ph-lg-2',
+		icon: 'ti ti-terminal-2',
 	}, {
 		type: 'link',
 		to: '/api-console',
 		text: 'API Console',
-		icon: 'ph-terminal-window ph-bold ph-lg-2',
+		icon: 'ti ti-terminal-2',
 	}, {
 		type: 'link',
 		to: '/clicker',
 		text: '🍪👈',
-		icon: 'ph-cookie ph-bold ph-lg',
+		icon: 'ti ti-cookie',
 	}, ($i && ($i.isAdmin || $i.policies.canManageCustomEmojis)) ? {
 		type: 'link',
 		to: '/custom-emojis-manager',
@@ -36,7 +36,7 @@ function toolsMenuItems(): MenuItem[] {
 		type: 'link',
 		to: '/avatar-decorations',
 		text: i18n.ts.manageAvatarDecorations,
-		icon: 'ph-sparkle ph-bold ph-lg',
+		icon: 'ti ti-sparkles',
 	} : undefined];
 }
 
@@ -47,7 +47,7 @@ export function openInstanceMenu(ev: MouseEvent) {
 	}, {
 		type: 'link',
 		text: i18n.ts.instanceInfo,
-		icon: 'ph-info ph-bold ph-lg',
+		icon: 'ti ti-info-circle',
 		to: '/about',
 	}, {
 		type: 'link',
@@ -57,73 +57,75 @@ export function openInstanceMenu(ev: MouseEvent) {
 	}, {
 		type: 'link',
 		text: i18n.ts.federation,
-		icon: 'ph-globe-hemisphere-west ph-bold ph-lg',
+		icon: 'ti ti-whirl',
 		to: '/about#federation',
 	}, {
 		type: 'link',
 		text: i18n.ts.charts,
-		icon: 'ph-chart-line ph-bold ph-lg',
+		icon: 'ti ti-chart-line',
 		to: '/about#charts',
 	}, { type: 'divider' }, {
 		type: 'link',
 		text: i18n.ts.ads,
-		icon: 'ph-flag ph-bold ph-lg',
+		icon: 'ti ti-ad',
 		to: '/ads',
 	}, ($i && ($i.isAdmin || $i.policies.canInvite) && instance.disableRegistration) ? {
 		type: 'link',
 		to: '/invite',
 		text: i18n.ts.invite,
-		icon: 'ph-user-plus ph-bold ph-lg',
+		icon: 'ti ti-user-plus',
 	} : undefined, {
 		type: 'parent',
 		text: i18n.ts.tools,
-		icon: 'ph-toolbox ph-bold ph-lg',
+		icon: 'ti ti-tool',
 		children: toolsMenuItems(),
 	}, { type: 'divider' }, {
 		type: 'link',
 		text: i18n.ts.inquiry,
-		icon: 'ph-question ph-bold ph-lg',
+		icon: 'ti ti-help-circle',
 		to: '/contact',
 	}, (instance.impressumUrl) ? {
+		type: 'a',
 		text: i18n.ts.impressum,
-		icon: 'ph-newspaper-clipping ph-bold ph-lg',
-		action: () => {
-			window.open(instance.impressumUrl, '_blank', 'noopener');
-		},
+		icon: 'ti ti-file-invoice',
+		href: instance.impressumUrl,
+		target: '_blank',
 	} : undefined, (instance.tosUrl) ? {
+		type: 'a',
 		text: i18n.ts.termsOfService,
-		icon: 'ph-notebook ph-bold ph-lg',
-		action: () => {
-			window.open(instance.tosUrl, '_blank', 'noopener');
-		},
+		icon: 'ti ti-notebook',
+		href: instance.tosUrl,
+		target: '_blank',
 	} : undefined, (instance.privacyPolicyUrl) ? {
+		type: 'a',
 		text: i18n.ts.privacyPolicy,
-		icon: 'ph-shield ph-bold ph-lg',
-		action: () => {
-			window.open(instance.privacyPolicyUrl, '_blank', 'noopener');
-		},
+		icon: 'ti ti-shield-lock',
+		href: instance.privacyPolicyUrl,
+		target: '_blank',
 	} : undefined, (instance.donationUrl) ? {
+		type: 'a',
 		text: i18n.ts.donation,
 		icon: 'ph-hand-coins ph-bold ph-lg',
-		action: () => {
-			window.open(instance.donationUrl, '_blank', 'noopener');
-		},
+		href: instance.donationUrl,
+		target: '_blank',
 	} : undefined, (!instance.impressumUrl && !instance.tosUrl && !instance.privacyPolicyUrl && !instance.donationUrl) ? undefined : { type: 'divider' }, {
+		type: 'a',
 		text: i18n.ts.document,
-		icon: 'ph-libghtbulb ph-bold ph-lg',
-		action: () => {
-			window.open('https://misskey-hub.net/docs/for-users/', '_blank', 'noopener');
-		},
+		icon: 'ti ti-bulb',
+		href: 'https://misskey-hub.net/docs/for-users/',
+		target: '_blank',
 	}, ($i) ? {
 		text: i18n.ts._initialTutorial.launchTutorial,
-		icon: 'ph-presentation ph-bold ph-lg',
+		icon: 'ti ti-presentation',
 		action: () => {
-			os.popup(defineAsyncComponent(() => import('@/components/MkTutorialDialog.vue')), {}, {}, 'closed');
+			const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkTutorialDialog.vue')), {}, {
+				closed: () => dispose(),
+			});
 		},
 	} : undefined, {
 		type: 'link',
 		text: i18n.ts.aboutMisskey,
-		icon: 'sk-icons sk-shark ph-bold',
+		icon: 'sk-icons sk-shark sk-icons-lg',
 		to: '/about-sharkey',
 	}], 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 65b27a0cc2..442b6479dd 100644
--- a/packages/frontend/src/ui/_common_/common.vue
+++ b/packages/frontend/src/ui/_common_/common.vue
@@ -232,7 +232,7 @@ if ($i) {
 	right: 15px;
 	pointer-events: none;
 
-	&:before {
+	&::before {
 		content: "";
 		display: block;
 		width: 18px;
diff --git a/packages/frontend/src/ui/_common_/navbar-for-mobile.vue b/packages/frontend/src/ui/_common_/navbar-for-mobile.vue
index 85340fa2b7..a3f9d6cf2c 100644
--- a/packages/frontend/src/ui/_common_/navbar-for-mobile.vue
+++ b/packages/frontend/src/ui/_common_/navbar-for-mobile.vue
@@ -13,7 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 	</div>
 	<div :class="$style.middle">
 		<MkA :class="$style.item" :activeClass="$style.active" to="/" exact>
-			<i :class="$style.itemIcon" class="ph-house ph-bold ph-lg ti-fw"></i><span :class="$style.itemText">{{ i18n.ts.timeline }}</span>
+			<i :class="$style.itemIcon" class="ti ti-home ti-fw"></i><span :class="$style.itemText">{{ i18n.ts.timeline }}</span>
 		</MkA>
 		<template v-for="item in menu">
 			<div v-if="item === '-'" :class="$style.divider"></div>
@@ -27,19 +27,19 @@ SPDX-License-Identifier: AGPL-3.0-only
 		</template>
 		<div :class="$style.divider"></div>
 		<MkA v-if="$i.isAdmin || $i.isModerator" :class="$style.item" :activeClass="$style.active" to="/admin">
-			<i :class="$style.itemIcon" class="ph-gauge ph-bold ph-lg ti-fw"></i><span :class="$style.itemText">{{ i18n.ts.controlPanel }}</span>
+			<i :class="$style.itemIcon" class="ti ti-dashboard ti-fw"></i><span :class="$style.itemText">{{ i18n.ts.controlPanel }}</span>
 		</MkA>
 		<button :class="$style.item" class="_button" @click="more">
-			<i :class="$style.itemIcon" class="ph-dots-nine ph-bold ph-lg ti-fw"></i><span :class="$style.itemText">{{ i18n.ts.more }}</span>
+			<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>
 		</button>
 		<MkA :class="$style.item" :activeClass="$style.active" to="/settings">
-			<i :class="$style.itemIcon" class="ph-gear ph-bold ph-lg ti-fw"></i><span :class="$style.itemText">{{ i18n.ts.settings }}</span>
+			<i :class="$style.itemIcon" class="ti ti-settings ti-fw"></i><span :class="$style.itemText">{{ i18n.ts.settings }}</span>
 		</MkA>
 	</div>
 	<div :class="$style.bottom">
 		<button class="_button" :class="$style.post" data-cy-open-post-form @click="os.post">
-			<i :class="$style.postIcon" class="ph-pencil-simple ph-bold ph-lg ti-fw"></i><span style="position: relative;">{{ i18n.ts.note }}</span>
+			<i :class="$style.postIcon" class="ti ti-pencil ti-fw"></i><span style="position: relative;">{{ i18n.ts.note }}</span>
 		</button>
 		<button class="_button" :class="$style.account" @click="openAccountMenu">
 			<MkAvatar :user="$i" :class="$style.avatar"/><MkAcct :class="$style.acct" class="_nowrap" :user="$i"/>
@@ -74,8 +74,9 @@ 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>
 
@@ -138,7 +139,7 @@ function more() {
 	font-weight: bold;
 	text-align: left;
 
-	&:before {
+	&::before {
 		content: "";
 		display: block;
 		width: calc(100% - 38px);
@@ -154,7 +155,7 @@ function more() {
 	}
 
 	&:hover, &.active {
-		&:before {
+		&::before {
 			background: var(--accentLighten);
 		}
 	}
@@ -225,7 +226,7 @@ function more() {
 	}
 
 	&:hover, &.active {
-		&:before {
+		&::before {
 			content: "";
 			display: block;
 			width: calc(100% - 24px);
diff --git a/packages/frontend/src/ui/_common_/navbar.vue b/packages/frontend/src/ui/_common_/navbar.vue
index 65763bcfa8..1f1fd37def 100644
--- a/packages/frontend/src/ui/_common_/navbar.vue
+++ b/packages/frontend/src/ui/_common_/navbar.vue
@@ -14,7 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 		</div>
 		<div :class="$style.middle">
 			<MkA v-tooltip.noDelay.right="i18n.ts.timeline" :class="$style.item" :activeClass="$style.active" to="/" exact>
-				<i :class="$style.itemIcon" class="ph-house ph-bold ph-lg ti-fw"></i><span :class="$style.itemText">{{ i18n.ts.timeline }}</span>
+				<i :class="$style.itemIcon" class="ti ti-home ti-fw"></i><span :class="$style.itemText">{{ i18n.ts.timeline }}</span>
 			</MkA>
 			<template v-for="item in menu">
 				<div v-if="item === '-'" :class="$style.divider"></div>
@@ -37,19 +37,19 @@ SPDX-License-Identifier: AGPL-3.0-only
 			</template>
 			<div :class="$style.divider"></div>
 			<MkA v-if="$i.isAdmin || $i.isModerator" v-tooltip.noDelay.right="i18n.ts.controlPanel" :class="$style.item" :activeClass="$style.active" to="/admin">
-				<i :class="$style.itemIcon" class="ph-gauge ph-bold ph-lg ti-fw"></i><span :class="$style.itemText">{{ i18n.ts.controlPanel }}</span>
+				<i :class="$style.itemIcon" class="ti ti-dashboard ti-fw"></i><span :class="$style.itemText">{{ i18n.ts.controlPanel }}</span>
 			</MkA>
 			<button class="_button" :class="$style.item" @click="more">
-				<i :class="$style.itemIcon" class="ph-dots-nine ph-bold ph-lg ti-fw"></i><span :class="$style.itemText">{{ i18n.ts.more }}</span>
+				<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>
 			</button>
 			<MkA v-tooltip.noDelay.right="i18n.ts.settings" :class="$style.item" :activeClass="$style.active" to="/settings">
-				<i :class="$style.itemIcon" class="ph-gear ph-bold ph-lg ti-fw"></i><span :class="$style.itemText">{{ i18n.ts.settings }}</span>
+				<i :class="$style.itemIcon" class="ti ti-settings ti-fw"></i><span :class="$style.itemText">{{ i18n.ts.settings }}</span>
 			</MkA>
 		</div>
 		<div :class="$style.bottom">
 			<button v-tooltip.noDelay.right="i18n.ts.note" class="_button" :class="[$style.post]" data-cy-open-post-form @click="os.post">
-				<i class="ph-pencil-simple ph-bold ph-lg ti-fw" :class="$style.postIcon"></i><span :class="$style.postText">{{ i18n.ts.note }}</span>
+				<i class="ti ti-pencil ti-fw" :class="$style.postIcon"></i><span :class="$style.postText">{{ i18n.ts.note }}</span>
 			</button>
 			<button v-tooltip.noDelay.right="`${i18n.ts.account}: @${$i.username}`" class="_button" :class="[$style.account]" @click="openAccountMenu">
 				<MkAvatar :user="$i" :class="$style.avatar"/><MkAcct class="_nowrap" :class="$style.acct" :user="$i"/>
@@ -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>
 
@@ -165,6 +166,15 @@ function more(ev: MouseEvent) {
 		display: block;
 		text-align: center;
 		width: 100%;
+
+		&:focus-visible {
+			outline: none;
+
+			> .instanceIcon {
+				outline: 2px solid var(--focus);
+				outline-offset: 2px;
+			}
+		}
 	}
 
 	.instanceIcon {
@@ -191,7 +201,7 @@ function more(ev: MouseEvent) {
 		font-weight: bold;
 		text-align: left;
 
-		&:before {
+		&::before {
 			content: "";
 			display: block;
 			width: calc(100% - 38px);
@@ -206,8 +216,17 @@ function more(ev: MouseEvent) {
 			background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB));
 		}
 
+		&:focus-visible {
+			outline: none;
+
+			&::before {
+				outline: 2px solid var(--fgOnAccent);
+				outline-offset: -4px;
+			}
+		}
+
 		&:hover, &.active {
-			&:before {
+			&::before {
 				background: var(--accentLighten);
 			}
 		}
@@ -233,6 +252,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(--focus);
+			}
+		}
 	}
 
 	.avatar {
@@ -281,10 +308,19 @@ function more(ev: MouseEvent) {
 			color: var(--navActive);
 		}
 
-		&:hover, &.active {
+		&:focus-visible {
+			outline: none;
+
+			&::before {
+				outline: 2px solid var(--focus);
+				outline-offset: -2px;
+			}
+		}
+
+		&:hover, &.active, &:focus {
 			color: var(--accent);
 
-			&:before {
+			&::before {
 				content: "";
 				display: block;
 				width: calc(100% - 34px);
@@ -351,6 +387,15 @@ function more(ev: MouseEvent) {
 		display: block;
 		text-align: center;
 		width: 100%;
+
+		&:focus-visible {
+			outline: none;
+
+			> .instanceIcon {
+				outline: 2px solid var(--focus);
+				outline-offset: 2px;
+			}
+		}
 	}
 
 	.instanceIcon {
@@ -375,7 +420,7 @@ function more(ev: MouseEvent) {
 		height: 52px;
 		text-align: center;
 
-		&:before {
+		&::before {
 			content: "";
 			display: block;
 			position: absolute;
@@ -390,8 +435,17 @@ function more(ev: MouseEvent) {
 			background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB));
 		}
 
+		&:focus-visible {
+			outline: none;
+
+			&::before {
+				outline: 2px solid var(--fgOnAccent);
+				outline-offset: -4px;
+			}
+		}
+
 		&:hover, &.active {
-			&:before {
+			&::before {
 				background: var(--accentLighten);
 			}
 		}
@@ -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(--focus);
+			}
+		}
 	}
 
 	.avatar {
@@ -441,11 +503,20 @@ function more(ev: MouseEvent) {
 		width: 100%;
 		text-align: center;
 
-		&:hover, &.active {
+		&:focus-visible {
+			outline: none;
+
+			&::before {
+				outline: 2px solid var(--focus);
+				outline-offset: -2px;
+			}
+		}
+
+		&:hover, &.active, &:focus {
 			text-decoration: none;
 			color: var(--accent);
 
-			&:before {
+			&::before {
 				content: "";
 				display: block;
 				height: 100%;
diff --git a/packages/frontend/src/ui/_common_/stream-indicator.vue b/packages/frontend/src/ui/_common_/stream-indicator.vue
index 968c3969bb..ad93b7e61c 100644
--- a/packages/frontend/src/ui/_common_/stream-indicator.vue
+++ b/packages/frontend/src/ui/_common_/stream-indicator.vue
@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <template>
 <div v-if="hasDisconnected && defaultStore.state.serverDisconnectedBehavior === 'quiet'" :class="$style.root" class="_panel _shadow" @click="resetDisconnected">
-	<div><i class="ph-warning ph-bold ph-lg"></i> {{ i18n.ts.disconnectedFromServer }}</div>
+	<div><i class="ti ti-alert-triangle"></i> {{ i18n.ts.disconnectedFromServer }}</div>
 	<div :class="$style.command" class="_buttons">
 		<MkButton small primary @click="reload">{{ i18n.ts.reload }}</MkButton>
 		<MkButton small>{{ i18n.ts.doNothing }}</MkButton>
diff --git a/packages/frontend/src/ui/classic.header.vue b/packages/frontend/src/ui/classic.header.vue
index 527670e103..c03afd6cd6 100644
--- a/packages/frontend/src/ui/classic.header.vue
+++ b/packages/frontend/src/ui/classic.header.vue
@@ -11,7 +11,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 				<img :src="instance.iconUrl ?? instance.faviconUrl ?? '/favicon.ico'" class="_ghost"/>
 			</button>
 			<MkA v-click-anime v-tooltip="i18n.ts.timeline" class="item index" activeClass="active" to="/" exact>
-				<i class="ph-house ph-bold ph-lg ti-fw"></i>
+				<i class="ti ti-home ti-fw"></i>
 			</MkA>
 			<template v-for="item in menu">
 				<div v-if="item === '-'" class="divider"></div>
@@ -22,23 +22,23 @@ SPDX-License-Identifier: AGPL-3.0-only
 			</template>
 			<div class="divider"></div>
 			<MkA v-if="$i.isAdmin || $i.isModerator" v-click-anime v-tooltip="i18n.ts.controlPanel" class="item" activeClass="active" to="/admin" :behavior="settingsWindowed ? 'window' : null">
-				<i class="ph-gauge ph-bold ph-lg ti-fw"></i>
+				<i class="ti ti-dashboard ti-fw"></i>
 			</MkA>
 			<button v-click-anime class="item _button" @click="more">
-				<i class="ph-dots-three ph-bold ph-lg ti-fw"></i>
+				<i class="ti ti-dots ti-fw"></i>
 				<span v-if="otherNavItemIndicated" class="indicator"><i class="_indicatorCircle"></i></span>
 			</button>
 		</div>
 		<div class="right">
 			<MkA v-click-anime v-tooltip="i18n.ts.settings" class="item" activeClass="active" to="/settings" :behavior="settingsWindowed ? 'window' : null">
-				<i class="ph-gear ph-bold ph-lg ti-fw"></i>
+				<i class="ti ti-settings ti-fw"></i>
 			</MkA>
 			<button v-click-anime class="item _button account" @click="openAccountMenu">
 				<MkAvatar :user="$i" class="avatar"/><MkAcct class="acct" :user="$i"/>
 			</button>
 			<div class="post" @click="os.post()">
 				<MkButton class="button" gradate full rounded>
-					<i class="ph-pencil-simple ph-bold ph-lg ti-fw"></i>
+					<i class="ti ti-pencil ti-fw"></i>
 				</MkButton>
 			</div>
 		</div>
@@ -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) {
diff --git a/packages/frontend/src/ui/classic.sidebar.vue b/packages/frontend/src/ui/classic.sidebar.vue
index 25b9095574..d49df8e8ac 100644
--- a/packages/frontend/src/ui/classic.sidebar.vue
+++ b/packages/frontend/src/ui/classic.sidebar.vue
@@ -10,12 +10,12 @@ SPDX-License-Identifier: AGPL-3.0-only
 	</button>
 	<div class="post" data-cy-open-post-form @click="os.post">
 		<MkButton class="button" gradate full rounded>
-			<i class="ph-pencil-simple ph-bold ph-lg ti-fw"></i><span v-if="!iconOnly" class="text">{{ i18n.ts.note }}</span>
+			<i class="ti ti-pencil ti-fw"></i><span v-if="!iconOnly" class="text">{{ i18n.ts.note }}</span>
 		</MkButton>
 	</div>
 	<div class="divider"></div>
 	<MkA v-click-anime class="item index" activeClass="active" to="/" exact>
-		<i class="ph-house ph-bold ph-lg ti-fw"></i><span class="text">{{ i18n.ts.timeline }}</span>
+		<i class="ti ti-home ti-fw"></i><span class="text">{{ i18n.ts.timeline }}</span>
 	</MkA>
 	<template v-for="item in menu">
 		<div v-if="item === '-'" class="divider"></div>
@@ -29,14 +29,14 @@ SPDX-License-Identifier: AGPL-3.0-only
 	</template>
 	<div class="divider"></div>
 	<MkA v-if="$i.isAdmin || $i.isModerator" v-click-anime class="item" activeClass="active" to="/admin" :behavior="settingsWindowed ? 'window' : null">
-		<i class="ph-gauge ph-bold ph-lg ti-fw"></i><span class="text">{{ i18n.ts.controlPanel }}</span>
+		<i class="ti ti-dashboard ti-fw"></i><span class="text">{{ i18n.ts.controlPanel }}</span>
 	</MkA>
 	<button v-click-anime class="item _button" @click="more">
-		<i class="ph-dots-three ph-bold ph-lg ti-fw"></i><span class="text">{{ i18n.ts.more }}</span>
+		<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>
 	</button>
 	<MkA v-click-anime class="item" activeClass="active" to="/settings" :behavior="settingsWindowed ? 'window' : null">
-		<i class="ph-gear ph-bold ph-lg ti-fw"></i><span class="text">{{ i18n.ts.settings }}</span>
+		<i class="ti ti-settings ti-fw"></i><span class="text">{{ i18n.ts.settings }}</span>
 	</MkA>
 	<div class="divider"></div>
 	<div class="about">
@@ -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) {
diff --git a/packages/frontend/src/ui/classic.vue b/packages/frontend/src/ui/classic.vue
index ea9ea56b90..ce5a22a61d 100644
--- a/packages/frontend/src/ui/classic.vue
+++ b/packages/frontend/src/ui/classic.vue
@@ -118,13 +118,13 @@ function onContextmenu(ev: MouseEvent) {
 		type: 'label',
 		text: path,
 	}, {
-		icon: fullView.value ? 'ph-arrows-in-simple ph-bold ph-lg' : 'ph-frame-corners ph-bold ph-lg',
+		icon: fullView.value ? 'ti ti-minimize' : 'ti ti-maximize',
 		text: fullView.value ? i18n.ts.quitFullView : i18n.ts.fullView,
 		action: () => {
 			fullView.value = !fullView.value;
 		},
 	}, {
-		icon: 'ph-frame-corners ph-bold ph-lg',
+		icon: 'ti ti-window-maximize',
 		text: i18n.ts.openInWindow,
 		action: () => {
 			os.pageWindow(path);
diff --git a/packages/frontend/src/ui/deck.vue b/packages/frontend/src/ui/deck.vue
index 68c7f0fcd2..44f1af5f8f 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"
 				/>
@@ -36,29 +36,29 @@ SPDX-License-Identifier: AGPL-3.0-only
 			</div>
 			<div :class="$style.sideMenu">
 				<div :class="$style.sideMenuTop">
-					<button v-tooltip.noDelay.left="`${i18n.ts._deck.profile}: ${deckStore.state.profile}`" :class="$style.sideMenuButton" class="_button" @click="changeProfile"><i class="ph-caret-down ph-bold ph-lg"></i></button>
-					<button v-tooltip.noDelay.left="i18n.ts._deck.deleteProfile" :class="$style.sideMenuButton" class="_button" @click="deleteProfile"><i class="ph-trash ph-bold ph-lg"></i></button>
+					<button v-tooltip.noDelay.left="`${i18n.ts._deck.profile}: ${deckStore.state.profile}`" :class="$style.sideMenuButton" class="_button" @click="changeProfile"><i class="ti ti-caret-down"></i></button>
+					<button v-tooltip.noDelay.left="i18n.ts._deck.deleteProfile" :class="$style.sideMenuButton" class="_button" @click="deleteProfile"><i class="ti ti-trash"></i></button>
 				</div>
 				<div :class="$style.sideMenuMiddle">
-					<button v-tooltip.noDelay.left="i18n.ts._deck.addColumn" :class="$style.sideMenuButton" class="_button" @click="addColumn"><i class="ph-plus ph-bold ph-lg"></i></button>
+					<button v-tooltip.noDelay.left="i18n.ts._deck.addColumn" :class="$style.sideMenuButton" class="_button" @click="addColumn"><i class="ti ti-plus"></i></button>
 				</div>
 				<div :class="$style.sideMenuBottom">
-					<button v-tooltip.noDelay.left="i18n.ts.settings" :class="$style.sideMenuButton" class="_button" @click="showSettings"><i class="ph-gear ph-bold ph-lg"></i></button>
+					<button v-tooltip.noDelay.left="i18n.ts.settings" :class="$style.sideMenuButton" class="_button" @click="showSettings"><i class="ti ti-settings"></i></button>
 				</div>
 			</div>
 		</div>
 	</div>
 
 	<div v-if="isMobile" :class="$style.nav">
-		<button :class="$style.navButton" class="_button" @click="drawerMenuShowing = true"><i :class="$style.navButtonIcon" class="ph-list ph-bold ph-lg-2"></i><span v-if="menuIndicated" :class="$style.navButtonIndicator"><i class="_indicatorCircle"></i></span></button>
-		<button :class="$style.navButton" class="_button" @click="mainRouter.push('/')"><i :class="$style.navButtonIcon" class="ph-house ph-bold ph-lg"></i></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"><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="ph-bell ph-bold ph-lg"></i>
+			<i :class="$style.navButtonIcon" class="ti ti-bell"></i>
 			<span v-if="$i?.hasUnreadNotification" :class="$style.navButtonIndicator">
 				<span class="_indicateCounter" :class="$style.itemIndicateValueIcon">{{ $i.unreadNotificationsCount > 99 ? '99+' : $i.unreadNotificationsCount }}</span>
 			</span>
 		</button>
-		<button :class="$style.postButton" class="_button" @click="os.post()"><i :class="$style.navButtonIcon" class="ph-pencil-simple ph-bold ph-lg"></i></button>
+		<button :class="$style.postButton" class="_button" @click="os.post()"><i :class="$style.navButtonIcon" class="ti ti-pencil"></i></button>
 	</div>
 
 	<Transition
@@ -95,7 +95,8 @@ 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 XSidebar from '@/ui/_common_/navbar.vue';
 import XDrawerMenu from '@/ui/_common_/navbar-for-mobile.vue';
 import MkButton from '@/components/MkButton.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;
 	}
 }
@@ -236,13 +227,13 @@ function changeProfile(ev: MouseEvent) {
 			},
 		}))), { type: 'divider' as const }, {
 			text: i18n.ts._deck.newProfile,
-			icon: 'ph-plus ph-bold ph-lg',
+			icon: 'ti ti-plus',
 			action: async () => {
 				const { canceled, result: name } = await os.inputText({
 					title: i18n.ts._deck.profile,
 					minLength: 1,
 				});
-				if (canceled) return;
+				if (canceled || name == null) return;
 
 				deckStore.set('profile', name);
 				unisonReload();
diff --git a/packages/frontend/src/ui/deck/antenna-column.vue b/packages/frontend/src/ui/deck/antenna-column.vue
index ca7d7102a3..987bd4db55 100644
--- a/packages/frontend/src/ui/deck/antenna-column.vue
+++ b/packages/frontend/src/ui/deck/antenna-column.vue
@@ -4,9 +4,9 @@ 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="ph-flying-saucer ph-bold ph-lg"></i><span style="margin-left: 8px;">{{ column.name }}</span>
+		<i class="ti ti-antenna"></i><span style="margin-left: 8px;">{{ column.name }}</span>
 	</template>
 
 	<MkTimeline v-if="column.antennaId" ref="timeline" src="antenna" :antenna="column.antennaId" @note="onNote"/>
@@ -14,7 +14,8 @@ 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';
@@ -22,6 +23,7 @@ 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 { 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,
 	});
@@ -69,17 +93,17 @@ function onNote() {
 
 const menu: MenuItem[] = [
 	{
-		icon: 'ph-pencil-simple ph-bold ph-lg',
+		icon: 'ti ti-pencil',
 		text: i18n.ts.selectAntenna,
 		action: setAntenna,
 	},
 	{
-		icon: 'ph-gear ph-bold ph-lg',
+		icon: 'ti ti-settings',
 		text: i18n.ts.editAntenna,
 		action: editAntenna,
 	},
 	{
-		icon: 'ph-bell-ringing ph-bold ph-lg',
+		icon: 'ti ti-bell',
 		text: i18n.ts._deck.newNoteNotificationSettings,
 		action: () => soundSettingsButton(soundSetting),
 	},
diff --git a/packages/frontend/src/ui/deck/channel-column.vue b/packages/frontend/src/ui/deck/channel-column.vue
index 10b3d5a1ec..518df7a6fc 100644
--- a/packages/frontend/src/ui/deck/channel-column.vue
+++ b/packages/frontend/src/ui/deck/channel-column.vue
@@ -4,14 +4,14 @@ 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="ph-television ph-bold ph-lg"></i><span style="margin-left: 8px;">{{ column.name }}</span>
+		<i class="ti ti-device-tv"></i><span style="margin-left: 8px;">{{ column.name }}</span>
 	</template>
 
 	<template v-if="column.channelId">
 		<div style="padding: 8px; text-align: center;">
-			<MkButton primary gradate rounded inline small @click="post"><i class="ph-pencil-simple ph-bold ph-lg"></i></MkButton>
+			<MkButton primary gradate rounded inline small @click="post"><i class="ti ti-pencil"></i></MkButton>
 		</div>
 		<MkTimeline ref="timeline" :key="column.channelId + column.withRenotes + column.onlyFiles" src="channel" :channel="column.channelId" :withRenotes="withRenotes" :onlyFiles="onlyFiles" @note="onNote"/>
 	</template>
@@ -83,6 +83,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,
@@ -99,7 +100,7 @@ function onNote() {
 }
 
 const menu: MenuItem[] = [{
-	icon: 'ph-pencil-simple ph-bold ph-lg',
+	icon: 'ti ti-pencil',
 	text: i18n.ts.selectChannel,
 	action: setChannel,
 }, {
@@ -111,7 +112,7 @@ const menu: MenuItem[] = [{
 	text: i18n.ts.fileAttachedOnly,
 	ref: onlyFiles,
 }, {
-	icon: 'ph-bell-ringing ph-bold ph-lg',
+	icon: 'ti ti-bell',
 	text: i18n.ts._deck.newNoteNotificationSettings,
 	action: () => soundSettingsButton(soundSetting),
 }];
diff --git a/packages/frontend/src/ui/deck/column.vue b/packages/frontend/src/ui/deck/column.vue
index f9efb9d88c..5fed70fc90 100644
--- a/packages/frontend/src/ui/deck/column.vue
+++ b/packages/frontend/src/ui/deck/column.vue
@@ -26,14 +26,14 @@ SPDX-License-Identifier: AGPL-3.0-only
 		</svg>
 		<div :class="$style.color"></div>
 		<button v-if="isStacked && !isMainColumn" :class="$style.toggleActive" class="_button" @click="toggleActive">
-			<template v-if="active"><i class="ph-caret-up ph-bold ph-lg"></i></template>
-			<template v-else><i class="ph-caret-down ph-bold ph-lg"></i></template>
+			<template v-if="active"><i class="ti ti-chevron-up"></i></template>
+			<template v-else><i class="ti ti-chevron-down"></i></template>
 		</button>
 		<span :class="$style.title"><slot name="header"></slot></span>
 		<svg viewBox="0 0 16 16" version="1.1" :class="$style.grabber">
 			<path fill="currentColor" d="M10 13a1 1 0 1 1 0-2 1 1 0 0 1 0 2Zm0-4a1 1 0 1 1 0-2 1 1 0 0 1 0 2Zm-4 4a1 1 0 1 1 0-2 1 1 0 0 1 0 2Zm5-9a1 1 0 1 1-2 0 1 1 0 0 1 2 0ZM7 8a1 1 0 1 1-2 0 1 1 0 0 1 2 0ZM6 5a1 1 0 1 1 0-2 1 1 0 0 1 0 2Z"></path>
 		</svg>
-		<button v-tooltip="i18n.ts.settings" :class="$style.menu" class="_button" @click.stop="showSettingsMenu"><i class="ph-dots-three ph-bold ph-lg"></i></button>
+		<button v-tooltip="i18n.ts.settings" :class="$style.menu" class="_button" @click.stop="showSettingsMenu"><i class="ti ti-dots"></i></button>
 	</header>
 	<div v-if="active" ref="body" :class="$style.body">
 		<slot></slot>
@@ -105,7 +105,7 @@ function toggleActive() {
 
 function getMenu() {
 	let items: MenuItem[] = [{
-		icon: 'ph-gear ph-bold ph-lg',
+		icon: 'ti ti-settings',
 		text: i18n.ts._deck.configureColumn,
 		action: async () => {
 			const { canceled, result } = await os.form(props.column.name, {
@@ -132,46 +132,46 @@ function getMenu() {
 	}, {
 		type: 'parent',
 		text: i18n.ts.move + '...',
-		icon: 'ph-arrows-out-cardinal ph-bold ph-lg',
+		icon: 'ti ti-arrows-move',
 		children: [{
-			icon: 'ph-arrow-left ph-bold ph-lg',
+			icon: 'ti ti-arrow-left',
 			text: i18n.ts._deck.swapLeft,
 			action: () => {
 				swapLeftColumn(props.column.id);
 			},
 		}, {
-			icon: 'ph-arrow-right ph-bold ph-lg',
+			icon: 'ti ti-arrow-right',
 			text: i18n.ts._deck.swapRight,
 			action: () => {
 				swapRightColumn(props.column.id);
 			},
 		}, props.isStacked ? {
-			icon: 'ph-arrow-up ph-bold ph-lg',
+			icon: 'ti ti-arrow-up',
 			text: i18n.ts._deck.swapUp,
 			action: () => {
 				swapUpColumn(props.column.id);
 			},
 		} : undefined, props.isStacked ? {
-			icon: 'ph-arrow-down ph-bold ph-lg',
+			icon: 'ti ti-arrow-down',
 			text: i18n.ts._deck.swapDown,
 			action: () => {
 				swapDownColumn(props.column.id);
 			},
 		} : undefined],
 	}, {
-		icon: 'ph-stack ph-bold ph-lg',
+		icon: 'ti ti-stack-2',
 		text: i18n.ts._deck.stackLeft,
 		action: () => {
 			stackLeftColumn(props.column.id);
 		},
 	}, props.isStacked ? {
-		icon: 'ph-frame-corners ph-bold ph-lg',
+		icon: 'ti ti-window-maximize',
 		text: i18n.ts._deck.popRight,
 		action: () => {
 			popRightColumn(props.column.id);
 		},
 	} : undefined, { type: 'divider' }, {
-		icon: 'ph-trash ph-bold ph-lg',
+		icon: 'ti ti-trash',
 		text: i18n.ts.remove,
 		danger: true,
 		action: () => {
@@ -186,7 +186,7 @@ function getMenu() {
 
 	if (props.refresher) {
 		items = [{
-			icon: 'ph-arrows-counter-clockwise ph-bold ph-lg',
+			icon: 'ti ti-refresh',
 			text: i18n.ts.reload,
 			action: () => {
 				if (props.refresher) {
@@ -271,7 +271,7 @@ function onDrop(ev) {
 	border-radius: var(--radius);
 
 	&.draghover {
-		&:after {
+		&::after {
 			content: "";
 			display: block;
 			position: absolute;
@@ -285,7 +285,7 @@ function onDrop(ev) {
 	}
 
 	&.dragging {
-		&:after {
+		&::after {
 			content: "";
 			display: block;
 			position: absolute;
diff --git a/packages/frontend/src/ui/deck/deck-store.ts b/packages/frontend/src/ui/deck/deck-store.ts
index 1a4f7c5e17..eb587554b9 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,7 +46,7 @@ export type Column = {
 	channelId?: string;
 	roleId?: string;
 	excludeTypes?: typeof notificationTypes[number][];
-	tl?: 'home' | 'local' | 'social' | 'global' | 'bubble';
+	tl?: BasicTimelineType;
 	withRenotes?: boolean;
 	withReplies?: boolean;
 	onlyFiles?: boolean;
@@ -265,7 +281,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 +303,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 09412ce386..d12a18f760 100644
--- a/packages/frontend/src/ui/deck/direct-column.vue
+++ b/packages/frontend/src/ui/deck/direct-column.vue
@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <template>
 <XColumn :column="column" :isStacked="isStacked" :refresher="() => reloadTimeline()">
-	<template #header><i class="ph-envelope ph-bold ph-lg" style="margin-right: 8px;"></i>{{ column.name }}</template>
+	<template #header><i class="ti ti-mail" style="margin-right: 8px;"></i>{{ column.name }}</template>
 
 	<MkNotes ref="tlComponent" :pagination="pagination"/>
 </XColumn>
@@ -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 eb8e657e98..a0e318f7eb 100644
--- a/packages/frontend/src/ui/deck/list-column.vue
+++ b/packages/frontend/src/ui/deck/list-column.vue
@@ -4,9 +4,9 @@ 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="ph-list ph-bold ph-lg"></i><span style="margin-left: 8px;">{{ column.name }}</span>
+		<i class="ti ti-list"></i><span style="margin-left: 8px;">{{ column.name }}</span>
 	</template>
 
 	<MkTimeline v-if="column.listId" ref="timeline" :key="column.listId + column.withRenotes + column.onlyFiles" src="list" :list="column.listId" :withRenotes="withRenotes" :onlyFiles="onlyFiles" @note="onNote"/>
@@ -15,6 +15,7 @@ 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';
@@ -23,6 +24,7 @@ import { misskeyApi } from '@/scripts/misskey-api.js';
 import { i18n } from '@/i18n.js';
 import { 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';
 
@@ -58,17 +60,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() {
@@ -81,12 +104,12 @@ function onNote() {
 
 const menu: MenuItem[] = [
 	{
-		icon: 'ph-pencil-simple ph-bold ph-lg',
+		icon: 'ti ti-pencil',
 		text: i18n.ts.selectList,
 		action: setList,
 	},
 	{
-		icon: 'ph-gear ph-bold ph-lg',
+		icon: 'ti ti-settings',
 		text: i18n.ts.editList,
 		action: editList,
 	},
@@ -101,7 +124,7 @@ const menu: MenuItem[] = [
 		ref: onlyFiles,
 	},
 	{
-		icon: 'ph-bell-ringing ph-bold ph-lg',
+		icon: 'ti ti-bell',
 		text: i18n.ts._deck.newNoteNotificationSettings,
 		action: () => soundSettingsButton(soundSetting),
 	},
diff --git a/packages/frontend/src/ui/deck/main-column.vue b/packages/frontend/src/ui/deck/main-column.vue
index 847dcf247a..79c9671917 100644
--- a/packages/frontend/src/ui/deck/main-column.vue
+++ b/packages/frontend/src/ui/deck/main-column.vue
@@ -66,7 +66,7 @@ function onContextmenu(ev: MouseEvent) {
 		type: 'label',
 		text: path,
 	}, {
-		icon: 'ph-frame-corners ph-bold ph-lg',
+		icon: 'ti ti-window-maximize',
 		text: i18n.ts.openInWindow,
 		action: () => {
 			os.pageWindow(path);
diff --git a/packages/frontend/src/ui/deck/mentions-column.vue b/packages/frontend/src/ui/deck/mentions-column.vue
index 70ec98119a..7b25a55ec3 100644
--- a/packages/frontend/src/ui/deck/mentions-column.vue
+++ b/packages/frontend/src/ui/deck/mentions-column.vue
@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <template>
 <XColumn :column="column" :isStacked="isStacked" :refresher="() => reloadTimeline()">
-	<template #header><i class="ph-at ph-bold ph-lg" style="margin-right: 8px;"></i>{{ column.name }}</template>
+	<template #header><i class="ti ti-at" style="margin-right: 8px;"></i>{{ column.name }}</template>
 
 	<MkNotes ref="tlComponent" :pagination="pagination"/>
 </XColumn>
@@ -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 837953b1e8..19ccfc1f7c 100644
--- a/packages/frontend/src/ui/deck/notifications-column.vue
+++ b/packages/frontend/src/ui/deck/notifications-column.vue
@@ -4,8 +4,8 @@ SPDX-License-Identifier: AGPL-3.0-only
 -->
 
 <template>
-<XColumn :column="column" :isStacked="isStacked" :menu="menu" :refresher="() => notificationsComponent.reload()">
-	<template #header><i class="ph-bell ph-bold ph-lg" style="margin-right: 8px;"></i>{{ column.name }}</template>
+<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"/>
 </XColumn>
@@ -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,11 +36,12 @@ function func() {
 				excludeTypes: excludeTypes,
 			});
 		},
-	}, 'closed');
+		closed: () => dispose(),
+	});
 }
 
 const menu = [{
-	icon: 'ph-pencil-simple ph-bold ph-lg',
+	icon: 'ti ti-pencil',
 	text: i18n.ts.notificationSetting,
 	action: func,
 }];
diff --git a/packages/frontend/src/ui/deck/role-timeline-column.vue b/packages/frontend/src/ui/deck/role-timeline-column.vue
index 09a6438afd..a375e9c574 100644
--- a/packages/frontend/src/ui/deck/role-timeline-column.vue
+++ b/packages/frontend/src/ui/deck/role-timeline-column.vue
@@ -4,9 +4,9 @@ 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="ph-seal-check ph-bold ph-lg"></i><span style="margin-left: 8px;">{{ column.name }}</span>
+		<i class="ti ti-badge"></i><span style="margin-left: 8px;">{{ column.name }}</span>
 	</template>
 
 	<MkTimeline v-if="column.roleId" ref="timeline" src="role" :role="column.roleId" @note="onNote"/>
@@ -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,
 	});
@@ -64,11 +64,11 @@ function onNote() {
 }
 
 const menu: MenuItem[] = [{
-	icon: 'ph-pencil-simple ph-bold ph-lg',
+	icon: 'ti ti-pencil',
 	text: i18n.ts.role,
 	action: setRole,
 }, {
-	icon: 'ph-bell-ringing ph-bold ph-lg',
+	icon: 'ti ti-bell',
 	text: i18n.ts._deck.newNoteNotificationSettings,
 	action: () => soundSettingsButton(soundSetting),
 }];
diff --git a/packages/frontend/src/ui/deck/tl-column.vue b/packages/frontend/src/ui/deck/tl-column.vue
index 0c0a435403..17afa12551 100644
--- a/packages/frontend/src/ui/deck/tl-column.vue
+++ b/packages/frontend/src/ui/deck/tl-column.vue
@@ -4,19 +4,15 @@ 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="ph-house ph-bold ph-lg"></i>
-		<i v-else-if="column.tl === 'local'" class="ph-planet ph-bold ph-lg"></i>
-		<i v-else-if="column.tl === 'social'" class="ph-rocket-launch ph-bold ph-lg"></i>
-		<i v-else-if="column.tl === 'bubble'" class="ph-thumb-up ph-bold ph-lg"></i>
-		<i v-else-if="column.tl === 'global'" class="ph-globe-hemisphere-west ph-bold ph-lg"></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 === 'bubble' && !isBubbleTimelineAvailable) || (column.tl === 'global' && !isGlobalTimelineAvailable))" :class="$style.disabled">
+	<div v-if="!isAvailableBasicTimeline(column.tl)" :class="$style.disabled">
 		<p :class="$style.disabledTitle">
-			<i class="ph-minus-circle ph-bold ph-lg"></i>
+			<i class="ti ti-circle-minus"></i>
 			{{ i18n.ts._disabledTimeline.title }}
 		</p>
 		<p :class="$style.disabledDescription">{{ i18n.ts._disabledTimeline.description }}</p>
@@ -35,15 +31,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';
@@ -53,12 +49,8 @@ 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 isBubbleTimelineAvailable = ($i == null && instance.policies.btlAvailable) || ($i != null && $i.policies.btlAvailable);
 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);
@@ -89,11 +81,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)) ||
-			(!((instance.policies.btlAvailable) || ($i.policies.btlAvailable)) && ['bubble'].includes(props.column.tl)));
 	}
 });
 
@@ -107,7 +94,7 @@ async function setType() {
 		}, {
 			value: 'social' as const, text: i18n.ts._timelines.social,
 		}, {
-			value: 'bubble' as const, text: 'Bubble',
+			value: 'bubble' as const, text: i18n.ts._timelines.bubble,
 		}, {
 			value: 'global' as const, text: i18n.ts._timelines.global,
 		}],
@@ -118,8 +105,9 @@ async function setType() {
 		}
 		return;
 	}
+	if (src == null) return;
 	updateColumn(props.column.id, {
-		tl: src,
+		tl: src ?? undefined,
 	});
 }
 
@@ -127,19 +115,19 @@ function onNote() {
 	sound.playMisskeySfxFile(soundSetting.value);
 }
 
-const menu: MenuItem[] = [{
-	icon: 'ph-pencil-simple ph-bold ph-lg',
+const menu = computed<MenuItem[]>(() => [{
+	icon: 'ti ti-pencil',
 	text: i18n.ts.timeline,
 	action: setType,
 }, {
-	icon: 'ph-bell-ringing ph-bold ph-lg',
+	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' ? {
+}, hasWithReplies(props.column.tl) ? {
 	type: 'switch',
 	text: i18n.ts.showRepliesToOthersInTimeline,
 	ref: withReplies,
@@ -148,8 +136,8 @@ const menu: MenuItem[] = [{
 	type: 'switch',
 	text: i18n.ts.fileAttachedOnly,
 	ref: onlyFiles,
-	disabled: props.column.tl === 'local' || props.column.tl === 'social' ? withReplies : false,
-}];
+	disabled: hasWithReplies(props.column.tl) ? withReplies : false,
+}]);
 </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 92d1a673f6..9995996771 100644
--- a/packages/frontend/src/ui/deck/widgets-column.vue
+++ b/packages/frontend/src/ui/deck/widgets-column.vue
@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <template>
 <XColumn :menu="menu" :naked="true" :column="column" :isStacked="isStacked">
-	<template #header><i class="ph-stack ph-bold ph-lg" style="margin-right: 8px;"></i>{{ column.name }}</template>
+	<template #header><i class="ti ti-apps" style="margin-right: 8px;"></i>{{ column.name }}</template>
 
 	<div :class="$style.root">
 		<div v-if="!(column.widgets && column.widgets.length > 0) && !edit" :class="$style.intro">{{ i18n.ts._deck.widgetsIntroduction }}</div>
@@ -49,7 +49,7 @@ function func() {
 }
 
 const menu = [{
-	icon: 'ph-pencil-simple ph-bold ph-lg',
+	icon: 'ti ti-pencil',
 	text: i18n.ts.editWidgets,
 	action: func,
 }];
diff --git a/packages/frontend/src/ui/universal.vue b/packages/frontend/src/ui/universal.vue
index 3a48c5eab9..d2b5d8cc42 100644
--- a/packages/frontend/src/ui/universal.vue
+++ b/packages/frontend/src/ui/universal.vue
@@ -22,19 +22,19 @@ SPDX-License-Identifier: AGPL-3.0-only
 		<XWidgets/>
 	</div>
 
-	<button v-if="(!isDesktop || pageMetadata?.needWideArea) && !isMobile" :class="$style.widgetButton" class="_button" @click="widgetsShowing = true"><i class="ph-stack ph-bold ph-lg"></i></button>
+	<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="ph-list ph-bold ph-lg-2"></i><span v-if="menuIndicated" :class="$style.navButtonIndicator"><i class="_indicatorCircle"></i></span></button>
-		<button :class="$style.navButton" class="_button" @click="isRoot ? top() : mainRouter.push('/')"><i :class="$style.navButtonIcon" class="ph-house ph-bold ph-lg"></i></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"><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="ph-bell ph-bold ph-lg"></i>
+			<i :class="$style.navButtonIcon" class="ti ti-bell"></i>
 			<span v-if="$i?.hasUnreadNotification" :class="$style.navButtonIndicator">
 				<span class="_indicateCounter" :class="$style.itemIndicateValueIcon">{{ $i.unreadNotificationsCount > 99 ? '99+' : $i.unreadNotificationsCount }}</span>
 			</span>
 		</button>
-		<button :class="$style.navButton" class="_button" @click="widgetsShowing = true"><i :class="$style.navButtonIcon" class="ph-stack ph-bold ph-lg"></i></button>
-		<button :class="$style.postButton" class="_button" @click="os.post()"><i :class="$style.navButtonIcon" class="ph-pencil-simple ph-bold ph-lg"></i></button>
+		<button :class="$style.navButton" class="_button" @click="widgetsShowing = true"><i :class="$style.navButtonIcon" class="ti ti-apps"></i></button>
+		<button :class="$style.postButton" class="_button" @click="os.post()"><i :class="$style.navButtonIcon" class="ti ti-pencil"></i></button>
 	</div>
 
 	<Transition
@@ -85,7 +85,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 		:leaveToClass="defaultStore.state.animation ? $style.transition_widgetsDrawer_leaveTo : ''"
 	>
 		<div v-if="widgetsShowing" :class="$style.widgetsDrawer">
-			<button class="_button" :class="$style.widgetsCloseButton" @click="widgetsShowing = false"><i class="ph-x ph-bold ph-lg"></i></button>
+			<button class="_button" :class="$style.widgetsCloseButton" @click="widgetsShowing = false"><i class="ti ti-x"></i></button>
 			<XWidgets/>
 		</div>
 	</Transition>
@@ -209,7 +209,7 @@ const onContextmenu = (ev) => {
 		type: 'label',
 		text: path,
 	}, {
-		icon: 'ph-frame-corners ph-bold ph-lg',
+		icon: 'ti ti-window-maximize',
 		text: i18n.ts.openInWindow,
 		action: () => {
 			os.pageWindow(path);
diff --git a/packages/frontend/src/ui/universal.widgets.vue b/packages/frontend/src/ui/universal.widgets.vue
index 7e41328403..fc0a4475d2 100644
--- a/packages/frontend/src/ui/universal.widgets.vue
+++ b/packages/frontend/src/ui/universal.widgets.vue
@@ -7,8 +7,8 @@ SPDX-License-Identifier: AGPL-3.0-only
 <div>
 	<XWidgets :edit="editMode" :widgets="widgets" @addWidget="addWidget" @removeWidget="removeWidget" @updateWidget="updateWidget" @updateWidgets="updateWidgets" @exit="editMode = false"/>
 
-	<button v-if="editMode" class="_textButton" style="font-size: 0.9em;" @click="editMode = false"><i class="ph-check ph-bold ph-lg"></i> {{ i18n.ts.editWidgetsExit }}</button>
-	<button v-else class="_textButton" data-cy-widget-edit :class="$style.edit" style="font-size: 0.9em;" @click="editMode = true"><i class="ph-pencil-simple ph-bold ph-lg"></i> {{ i18n.ts.editWidgets }}</button>
+	<button v-if="editMode" class="_textButton" style="font-size: 0.9em;" @click="editMode = false"><i class="ti ti-check"></i> {{ i18n.ts.editWidgetsExit }}</button>
+	<button v-else class="_textButton" data-cy-widget-edit :class="$style.edit" style="font-size: 0.9em;" @click="editMode = true"><i class="ti ti-pencil"></i> {{ i18n.ts.editWidgets }}</button>
 </div>
 </template>
 
diff --git a/packages/frontend/src/ui/visitor.vue b/packages/frontend/src/ui/visitor.vue
index 0c818ba21e..ccc8d8fd97 100644
--- a/packages/frontend/src/ui/visitor.vue
+++ b/packages/frontend/src/ui/visitor.vue
@@ -15,14 +15,14 @@ SPDX-License-Identifier: AGPL-3.0-only
 	<div class="main">
 		<div v-if="!isRoot" class="header">
 			<div v-if="narrow === false" class="wide">
-				<MkA to="/" class="link" activeClass="active"><i class="ph-house ph-bold ph-lg icon"></i> {{ i18n.ts.home }}</MkA>
+				<MkA to="/" class="link" activeClass="active"><i class="ti ti-home icon"></i> {{ i18n.ts.home }}</MkA>
 				<MkA v-if="isTimelineAvailable" to="/timeline" class="link" activeClass="active"><i class="ph-chat-text ph-bold ph-lg icon"></i> {{ i18n.ts.timeline }}</MkA>
-				<MkA to="/explore" class="link" activeClass="active"><i class="ph-hash ph-bold ph-lg icon"></i> {{ i18n.ts.explore }}</MkA>
-				<MkA to="/channels" class="link" activeClass="active"><i class="ph-television ph-bold ph-lg icon"></i> {{ i18n.ts.channel }}</MkA>
+				<MkA to="/explore" class="link" activeClass="active"><i class="ti ti-hash icon"></i> {{ i18n.ts.explore }}</MkA>
+				<MkA to="/channels" class="link" activeClass="active"><i class="ti ti-device-tv icon"></i> {{ i18n.ts.channel }}</MkA>
 			</div>
 			<div v-else-if="narrow === true" class="narrow">
 				<button class="menu _button" @click="showMenu = true">
-					<i class="ph-list ph-bold ph-lg-2 icon"></i>
+					<i class="ti ti-menu-2 icon"></i>
 				</button>
 			</div>
 		</div>
@@ -47,14 +47,14 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 	<Transition :name="'tray'">
 		<div v-if="showMenu" class="menu">
-			<MkA to="/" class="link" activeClass="active"><i class="ph-house ph-bold ph-lg icon"></i>{{ i18n.ts.home }}</MkA>
+			<MkA to="/" class="link" activeClass="active"><i class="ti ti-home icon"></i>{{ i18n.ts.home }}</MkA>
 			<MkA v-if="isTimelineAvailable" to="/timeline" class="link" activeClass="active"><i class="ph-chat-text ph-bold ph-lg icon"></i>{{ i18n.ts.timeline }}</MkA>
-			<MkA to="/explore" class="link" activeClass="active"><i class="ph-hash ph-bold ph-lg icon"></i>{{ i18n.ts.explore }}</MkA>
-			<MkA to="/announcements" class="link" activeClass="active"><i class="ph-megaphone ph-bold ph-lg icon"></i>{{ i18n.ts.announcements }}</MkA>
-			<MkA to="/channels" class="link" activeClass="active"><i class="ph-television ph-bold ph-lg icon"></i>{{ i18n.ts.channel }}</MkA>
+			<MkA to="/explore" class="link" activeClass="active"><i class="ti ti-hash icon"></i>{{ i18n.ts.explore }}</MkA>
+			<MkA to="/announcements" class="link" activeClass="active"><i class="ti ti-speakerphone icon"></i>{{ i18n.ts.announcements }}</MkA>
+			<MkA to="/channels" class="link" activeClass="active"><i class="ti ti-device-tv icon"></i>{{ i18n.ts.channel }}</MkA>
 			<div class="divider"></div>
-			<MkA to="/pages" class="link" activeClass="active"><i class="ph-newspaper ph-bold ph-lg icon"></i>{{ i18n.ts.pages }}</MkA>
-			<MkA to="/play" class="link" activeClass="active"><i class="ph-play ph-bold ph-lg icon"></i>Play</MkA>
+			<MkA to="/pages" class="link" activeClass="active"><i class="ti ti-news icon"></i>{{ i18n.ts.pages }}</MkA>
+			<MkA to="/play" class="link" activeClass="active"><i class="ti ti-player-play icon"></i>Play</MkA>
 			<MkA to="/gallery" class="link" activeClass="active"><i class="ph-images-square ph-bold ph-lgs icon"></i>{{ i18n.ts.gallery }}</MkA>
 			<div class="action">
 				<button class="_buttonPrimary" @click="signup()">{{ i18n.ts.signup }}</button>
@@ -124,15 +124,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(() => {
diff --git a/packages/frontend/src/ui/zen.vue b/packages/frontend/src/ui/zen.vue
index 77193a3475..47502b2af1 100644
--- a/packages/frontend/src/ui/zen.vue
+++ b/packages/frontend/src/ui/zen.vue
@@ -17,7 +17,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 	See https://github.com/misskey-dev/misskey/issues/10905
 -->
 <div v-if="showBottom" :class="$style.bottom">
-	<button v-tooltip="i18n.ts.goToMisskey" :class="['_button', '_shadow', $style.button]" @click="goToMisskey"><i class="ph-house ph-bold ph-lg"></i></button>
+	<button v-tooltip="i18n.ts.goToMisskey" :class="['_button', '_shadow', $style.button]" @click="goToMisskey"><i class="ti ti-home"></i></button>
 </div>
 </template>
 
diff --git a/packages/frontend/src/widgets/WidgetActivity.vue b/packages/frontend/src/widgets/WidgetActivity.vue
index 9b65ca5e4a..0aaf18ddd1 100644
--- a/packages/frontend/src/widgets/WidgetActivity.vue
+++ b/packages/frontend/src/widgets/WidgetActivity.vue
@@ -5,9 +5,9 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <template>
 <MkContainer :showHeader="widgetProps.showHeader" :naked="widgetProps.transparent" data-cy-mkw-activity class="mkw-activity">
-	<template #icon><i class="ph-chart-line ph-bold ph-lg"></i></template>
+	<template #icon><i class="ti ti-chart-line"></i></template>
 	<template #header>{{ i18n.ts._widgets.activity }}</template>
-	<template #func="{ buttonStyleClass }"><button class="_button" :class="buttonStyleClass" @click="toggleView()"><i class="ph-caret-up-down ph-bold ph-lg"></i></button></template>
+	<template #func="{ buttonStyleClass }"><button class="_button" :class="buttonStyleClass" @click="toggleView()"><i class="ti ti-selector"></i></button></template>
 
 	<div>
 		<MkLoading v-if="fetching"/>
diff --git a/packages/frontend/src/widgets/WidgetAiscript.vue b/packages/frontend/src/widgets/WidgetAiscript.vue
index 70fac9ae55..8e29254819 100644
--- a/packages/frontend/src/widgets/WidgetAiscript.vue
+++ b/packages/frontend/src/widgets/WidgetAiscript.vue
@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <template>
 <MkContainer :showHeader="widgetProps.showHeader" data-cy-mkw-aiscript class="mkw-aiscript">
-	<template #icon><i class="ph-terminal-window ph-bold ph-lg-2"></i></template>
+	<template #icon><i class="ti ti-terminal-2"></i></template>
 	<template #header>{{ i18n.ts._widgets.aiscript }}</template>
 
 	<div class="uylguesu _monospace">
diff --git a/packages/frontend/src/widgets/WidgetBirthdayFollowings.vue b/packages/frontend/src/widgets/WidgetBirthdayFollowings.vue
index e22697ca68..49fd103d37 100644
--- a/packages/frontend/src/widgets/WidgetBirthdayFollowings.vue
+++ b/packages/frontend/src/widgets/WidgetBirthdayFollowings.vue
@@ -5,9 +5,9 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <template>
 <MkContainer :showHeader="widgetProps.showHeader" class="mkw-bdayfollowings">
-	<template #icon><i class="ph-cake ph-bold ph-lg"></i></template>
+	<template #icon><i class="ti ti-cake"></i></template>
 	<template #header>{{ i18n.ts._widgets.birthdayFollowings }}</template>
-	<template #func="{ buttonStyleClass }"><button class="_button" :class="buttonStyleClass" @click="actualFetch()"><i class="ph-arrows-clockwise ph-bold ph-lg"></i></button></template>
+	<template #func="{ buttonStyleClass }"><button class="_button" :class="buttonStyleClass" @click="actualFetch()"><i class="ti ti-refresh"></i></button></template>
 
 	<div :class="$style.bdayFRoot">
 		<MkLoading v-if="fetching"/>
diff --git a/packages/frontend/src/widgets/WidgetCalendar.vue b/packages/frontend/src/widgets/WidgetCalendar.vue
index 06b71311c4..19843f3949 100644
--- a/packages/frontend/src/widgets/WidgetCalendar.vue
+++ b/packages/frontend/src/widgets/WidgetCalendar.vue
@@ -121,7 +121,7 @@ defineExpose<WidgetComponentExpose>({
 .root {
 	padding: 16px 0;
 
-	&:after {
+	&::after {
 		content: "";
 		display: block;
 		clear: both;
diff --git a/packages/frontend/src/widgets/WidgetClicker.vue b/packages/frontend/src/widgets/WidgetClicker.vue
index 9d231ae715..5c978fdf72 100644
--- a/packages/frontend/src/widgets/WidgetClicker.vue
+++ b/packages/frontend/src/widgets/WidgetClicker.vue
@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <template>
 <MkContainer :showHeader="widgetProps.showHeader" class="mkw-clicker">
-	<template #icon><i class="ph-cookie ph-bold ph-lg"></i></template>
+	<template #icon><i class="ti ti-cookie"></i></template>
 	<template #header>Clicker</template>
 	<MkClickerGame/>
 </MkContainer>
diff --git a/packages/frontend/src/widgets/WidgetFederation.vue b/packages/frontend/src/widgets/WidgetFederation.vue
index ae770f9816..2ea1253c2b 100644
--- a/packages/frontend/src/widgets/WidgetFederation.vue
+++ b/packages/frontend/src/widgets/WidgetFederation.vue
@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <template>
 <MkContainer :showHeader="widgetProps.showHeader" :foldable="foldable" :scrollable="scrollable" data-cy-mkw-federation class="mkw-federation">
-	<template #icon><i class="ph-globe-hemisphere-west ph-bold ph-lg"></i></template>
+	<template #icon><i class="ti ti-whirl"></i></template>
 	<template #header>{{ i18n.ts._widgets.federation }}</template>
 
 	<div class="wbrkwalb">
diff --git a/packages/frontend/src/widgets/WidgetInstanceInfo.vue b/packages/frontend/src/widgets/WidgetInstanceInfo.vue
index 962521b25c..ce8b0dd602 100644
--- a/packages/frontend/src/widgets/WidgetInstanceInfo.vue
+++ b/packages/frontend/src/widgets/WidgetInstanceInfo.vue
@@ -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 b3e364a6d7..edf6622a13 100644
--- a/packages/frontend/src/widgets/WidgetJobQueue.vue
+++ b/packages/frontend/src/widgets/WidgetJobQueue.vue
@@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 <template>
 <div data-cy-mkw-jobQueue class="mkw-jobQueue _monospace" :class="{ _panel: !widgetProps.transparent }">
 	<div class="inbox">
-		<div class="label">Inbox queue<i v-if="current.inbox.waiting > 0" class="ph-warning ph-bold ph-lg icon"></i></div>
+		<div class="label">Inbox queue<i v-if="current.inbox.waiting > 0" class="ti ti-alert-triangle icon"></i></div>
 		<div class="values">
 			<div>
 				<div>Process</div>
@@ -27,7 +27,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 		</div>
 	</div>
 	<div class="deliver">
-		<div class="label">Deliver queue<i v-if="current.deliver.waiting > 0" class="ph-warning ph-bold ph-lg icon"></i></div>
+		<div class="label">Deliver queue<i v-if="current.deliver.waiting > 0" class="ti ti-alert-triangle icon"></i></div>
 		<div class="values">
 			<div>
 				<div>Process</div>
diff --git a/packages/frontend/src/widgets/WidgetMemo.vue b/packages/frontend/src/widgets/WidgetMemo.vue
index d9efe54623..b69152b4fe 100644
--- a/packages/frontend/src/widgets/WidgetMemo.vue
+++ b/packages/frontend/src/widgets/WidgetMemo.vue
@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <template>
 <MkContainer :showHeader="widgetProps.showHeader" data-cy-mkw-memo class="mkw-memo">
-	<template #icon><i class="ph-note ph-bold ph-lg"></i></template>
+	<template #icon><i class="ti ti-note"></i></template>
 	<template #header>{{ i18n.ts._widgets.memo }}</template>
 
 	<div :class="$style.root">
diff --git a/packages/frontend/src/widgets/WidgetNotifications.vue b/packages/frontend/src/widgets/WidgetNotifications.vue
index d590e7768e..773c078b49 100644
--- a/packages/frontend/src/widgets/WidgetNotifications.vue
+++ b/packages/frontend/src/widgets/WidgetNotifications.vue
@@ -5,9 +5,9 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <template>
 <MkContainer :style="`height: ${widgetProps.height}px;`" :showHeader="widgetProps.showHeader" :scrollable="true" data-cy-mkw-notifications class="mkw-notifications">
-	<template #icon><i class="ph-bell ph-bold ph-lg"></i></template>
+	<template #icon><i class="ti ti-bell"></i></template>
 	<template #header>{{ i18n.ts.notifications }}</template>
-	<template #func="{ buttonStyleClass }"><button class="_button" :class="buttonStyleClass" @click="configureNotification()"><i class="ph-gear ph-bold ph-lg"></i></button></template>
+	<template #func="{ buttonStyleClass }"><button class="_button" :class="buttonStyleClass" @click="configureNotification()"><i class="ti ti-settings"></i></button></template>
 
 	<div>
 		<XNotifications :excludeTypes="widgetProps.excludeTypes"/>
@@ -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/WidgetPhotos.vue b/packages/frontend/src/widgets/WidgetPhotos.vue
index e578ebe2c5..86ece91de6 100644
--- a/packages/frontend/src/widgets/WidgetPhotos.vue
+++ b/packages/frontend/src/widgets/WidgetPhotos.vue
@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <template>
 <MkContainer :showHeader="widgetProps.showHeader" :naked="widgetProps.transparent" :class="$style.root" :data-transparent="widgetProps.transparent ? true : null" data-cy-mkw-photos class="mkw-photos">
-	<template #icon><i class="ph-camera ph-bold ph-lg"></i></template>
+	<template #icon><i class="ti ti-camera"></i></template>
 	<template #header>{{ i18n.ts._widgets.photos }}</template>
 
 	<div class="">
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 aa4bf155de..e5758662cc 100644
--- a/packages/frontend/src/widgets/WidgetRss.vue
+++ b/packages/frontend/src/widgets/WidgetRss.vue
@@ -5,9 +5,9 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <template>
 <MkContainer :showHeader="widgetProps.showHeader" data-cy-mkw-rss class="mkw-rss">
-	<template #icon><i class="ph-rss ph-bold ph-lg"></i></template>
+	<template #icon><i class="ti ti-rss"></i></template>
 	<template #header>RSS</template>
-	<template #func="{ buttonStyleClass }"><button class="_button" :class="buttonStyleClass" @click="configure"><i class="ph-gear ph-bold ph-lg"></i></button></template>
+	<template #func="{ buttonStyleClass }"><button class="_button" :class="buttonStyleClass" @click="configure"><i class="ti ti-settings"></i></button></template>
 
 	<div class="ekmkgxbj">
 		<MkLoading v-if="fetching"/>
diff --git a/packages/frontend/src/widgets/WidgetRssTicker.vue b/packages/frontend/src/widgets/WidgetRssTicker.vue
index f1dde14bf6..16306ef5ba 100644
--- a/packages/frontend/src/widgets/WidgetRssTicker.vue
+++ b/packages/frontend/src/widgets/WidgetRssTicker.vue
@@ -5,9 +5,9 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <template>
 <MkContainer :naked="widgetProps.transparent" :showHeader="widgetProps.showHeader" class="mkw-rss-ticker">
-	<template #icon><i class="ph-rss ph-bold ph-lg"></i></template>
+	<template #icon><i class="ti ti-rss"></i></template>
 	<template #header>RSS</template>
-	<template #func="{ buttonStyleClass }"><button class="_button" :class="buttonStyleClass" @click="configure"><i class="ph-gear ph-bold ph-lg"></i></button></template>
+	<template #func="{ buttonStyleClass }"><button class="_button" :class="buttonStyleClass" @click="configure"><i class="ti ti-settings"></i></button></template>
 
 	<div :class="$style.feed">
 		<div v-if="fetching" :class="$style.loading">
diff --git a/packages/frontend/src/widgets/WidgetSearch.vue b/packages/frontend/src/widgets/WidgetSearch.vue
index aeb1bc6ee0..294c97e293 100644
--- a/packages/frontend/src/widgets/WidgetSearch.vue
+++ b/packages/frontend/src/widgets/WidgetSearch.vue
@@ -143,11 +143,12 @@ async function search() {
 
 	key.value++;
 
-	os.popup(defineAsyncComponent(() => import('@/components/SkSearchResultWindow.vue')), {
+	const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/SkSearchResultWindow.vue')), {
 		noteKey: key.value,
 		notePagination: notePagination.value,
 	}, {
-	}, 'closed');
+		closed: () => dispose(),
+	});
 }
 
 defineExpose<WidgetComponentExpose>({
diff --git a/packages/frontend/src/widgets/WidgetTimeline.vue b/packages/frontend/src/widgets/WidgetTimeline.vue
index f6cf13290f..d02f9b8e22 100644
--- a/packages/frontend/src/widgets/WidgetTimeline.vue
+++ b/packages/frontend/src/widgets/WidgetTimeline.vue
@@ -6,24 +6,20 @@ 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="ph-house ph-bold ph-lg"></i>
-		<i v-else-if="widgetProps.src === 'local'" class="ph-planet ph-bold ph-lg"></i>
-		<i v-else-if="widgetProps.src === 'social'" class="ph-rocket-launch ph-bold ph-lg"></i>
-		<i v-else-if="widgetProps.src === 'bubble'" class="ph-drop ph-bold ph-lg"></i>
-		<i v-else-if="widgetProps.src === 'global'" class="ph-globe-hemisphere-west ph-bold ph-lg"></i>
-		<i v-else-if="widgetProps.src === 'list'" class="ph-list ph-bold ph-lg"></i>
-		<i v-else-if="widgetProps.src === 'antenna'" class="ph-flying-saucer ph-bold ph-lg"></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>
 	<template #header>
 		<button class="_button" @click="choose">
 			<span>{{ widgetProps.src === 'list' ? widgetProps.list.name : widgetProps.src === 'antenna' ? widgetProps.antenna.name : i18n.ts._timelines[widgetProps.src] }}</span>
-			<i :class="menuOpened ? 'ph-caret-up ph-bold ph-lg' : 'ph-caret-down ph-bold ph-lg'" style="margin-left: 8px;"></i>
+			<i :class="menuOpened ? 'ti ti-chevron-up' : 'ti ti-chevron-down'" style="margin-left: 8px;"></i>
 		</button>
 	</template>
 
-	<div v-if="(((widgetProps.src === 'local' || widgetProps.src === 'social') && !isLocalTimelineAvailable) || (widgetProps.src === 'bubble' && !isBubbleTimelineAvailable) || (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="ph-minus ph-bold ph-lg"></i>
+			<i class="ti ti-minus"></i>
 			{{ i18n.ts._disabledTimeline.title }}
 		</p>
 		<p :class="$style.disabledDescription">{{ i18n.ts._disabledTimeline.description }}</p>
@@ -43,13 +39,9 @@ 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';
 
 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 isBubbleTimelineAvailable = (($i == null && instance.policies.btlAvailable) || ($i != null && $i.policies.btlAvailable));
 
 const widgetPropsDef = {
 	showHeader: {
@@ -103,7 +95,7 @@ const choose = async (ev) => {
 	]);
 	const antennaItems = antennas.map(antenna => ({
 		text: antenna.name,
-		icon: 'ph-flying-saucer ph-bold ph-lg',
+		icon: 'ti ti-antenna',
 		action: () => {
 			widgetProps.antenna = antenna;
 			setSrc('antenna');
@@ -111,33 +103,17 @@ const choose = async (ev) => {
 	}));
 	const listItems = lists.map(list => ({
 		text: list.name,
-		icon: 'ph-list ph-bold ph-lg',
+		icon: 'ti ti-list',
 		action: () => {
 			widgetProps.list = list;
 			setSrc('list');
 		},
 	}));
-	os.popupMenu([{
-		text: i18n.ts._timelines.home,
-		icon: 'ph-house ph-bold ph-lg',
-		action: () => { setSrc('home'); },
-	}, {
-		text: i18n.ts._timelines.local,
-		icon: 'ph-planet ph-bold ph-lg',
-		action: () => { setSrc('local'); },
-	}, {
-		text: i18n.ts._timelines.social,
-		icon: 'ph-rocket-launch ph-bold ph-lg',
-		action: () => { setSrc('social'); },
-	}, {
-		text: 'Bubble',
-		icon: 'ph-drop ph-bold ph-lg',
-		action: () => { setSrc('bubble'); },
-	}, {
-		text: i18n.ts._timelines.global,
-		icon: 'ph-globe-hemisphere-west ph-bold ph-lg',
-		action: () => { setSrc('global'); },
-	}, antennaItems.length > 0 ? { type: 'divider' } : undefined, ...antennaItems, listItems.length > 0 ? { type: 'divider' } : undefined, ...listItems], ev.currentTarget ?? ev.target).then(() => {
+	os.popupMenu([...availableBasicTimelines().map(tl => ({
+		text: i18n.ts._timelines[tl],
+		icon: basicTimelineIconClass(tl),
+		action: () => { setSrc(tl); },
+	})), antennaItems.length > 0 ? { type: 'divider' } : undefined, ...antennaItems, listItems.length > 0 ? { type: 'divider' } : undefined, ...listItems], 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 978a1a86f7..4299181a27 100644
--- a/packages/frontend/src/widgets/WidgetTrends.vue
+++ b/packages/frontend/src/widgets/WidgetTrends.vue
@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <template>
 <MkContainer :showHeader="widgetProps.showHeader" data-cy-mkw-trends class="mkw-trends">
-	<template #icon><i class="ph-hash ph-bold ph-lg"></i></template>
+	<template #icon><i class="ti ti-hash"></i></template>
 	<template #header>{{ i18n.ts._widgets.trends }}</template>
 
 	<div class="wbrkwala">
diff --git a/packages/frontend/src/widgets/WidgetUserList.vue b/packages/frontend/src/widgets/WidgetUserList.vue
index 0e4fe2fbd3..d9f4dc49ea 100644
--- a/packages/frontend/src/widgets/WidgetUserList.vue
+++ b/packages/frontend/src/widgets/WidgetUserList.vue
@@ -5,9 +5,9 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <template>
 <MkContainer :showHeader="widgetProps.showHeader" class="mkw-userList">
-	<template #icon><i class="ph-users ph-bold ph-lg"></i></template>
+	<template #icon><i class="ti ti-users"></i></template>
 	<template #header>{{ list ? list.name : i18n.ts._widgets.userList }}</template>
-	<template #func="{ buttonStyleClass }"><button class="_button" :class="buttonStyleClass" @click="configure()"><i class="ph-gear ph-bold ph-lg"></i></button></template>
+	<template #func="{ buttonStyleClass }"><button class="_button" :class="buttonStyleClass" @click="configure()"><i class="ti ti-settings"></i></button></template>
 
 	<div :class="$style.root">
 		<div v-if="widgetProps.listId == null" class="init">
diff --git a/packages/frontend/src/widgets/server-metric/cpu.vue b/packages/frontend/src/widgets/server-metric/cpu.vue
index e00ef187f3..ba98a926ff 100644
--- a/packages/frontend/src/widgets/server-metric/cpu.vue
+++ b/packages/frontend/src/widgets/server-metric/cpu.vue
@@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 <div class="vrvdvrys">
 	<XPie class="pie" :value="usage"/>
 	<div>
-		<p><i class="ph-cpu ph-bold ph-lg"></i>CPU</p>
+		<p><i class="ti ti-cpu"></i>CPU</p>
 		<p>{{ meta.cpu.cores }} Logical cores</p>
 		<p>{{ meta.cpu.model }}</p>
 	</div>
diff --git a/packages/frontend/src/widgets/server-metric/disk.vue b/packages/frontend/src/widgets/server-metric/disk.vue
index e94a8b6848..0602b8cd27 100644
--- a/packages/frontend/src/widgets/server-metric/disk.vue
+++ b/packages/frontend/src/widgets/server-metric/disk.vue
@@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 <div class="zbwaqsat">
 	<XPie class="pie" :value="usage"/>
 	<div>
-		<p><i class="ph-database ph-bold ph-lg"></i>Disk</p>
+		<p><i class="ti ti-database"></i>Disk</p>
 		<p>Total: {{ bytes(total, 1) }}</p>
 		<p>Free: {{ bytes(available, 1) }}</p>
 		<p>Used: {{ bytes(used, 1) }}</p>
diff --git a/packages/frontend/src/widgets/server-metric/index.vue b/packages/frontend/src/widgets/server-metric/index.vue
index 1180a2a059..86d84b4f33 100644
--- a/packages/frontend/src/widgets/server-metric/index.vue
+++ b/packages/frontend/src/widgets/server-metric/index.vue
@@ -5,9 +5,9 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <template>
 <MkContainer :showHeader="widgetProps.showHeader" :naked="widgetProps.transparent">
-	<template #icon><i class="ph-hard-drives ph-bold ph-lg"></i></template>
+	<template #icon><i class="ti ti-server"></i></template>
 	<template #header>{{ i18n.ts._widgets.serverMetric }}</template>
-	<template #func="{ buttonStyleClass }"><button class="_button" :class="buttonStyleClass" @click="toggleView()"><i class="ph-caret-up-down ph-bold ph-lg"></i></button></template>
+	<template #func="{ buttonStyleClass }"><button class="_button" :class="buttonStyleClass" @click="toggleView()"><i class="ti ti-selector"></i></button></template>
 
 	<div v-if="meta" data-cy-mkw-serverMetric class="mkw-serverMetric">
 		<XCpuMemory v-if="widgetProps.view === 0" :connection="connection" :meta="meta"/>
diff --git a/packages/frontend/src/widgets/server-metric/mem.vue b/packages/frontend/src/widgets/server-metric/mem.vue
index ba56d14211..ff4c6a44b4 100644
--- a/packages/frontend/src/widgets/server-metric/mem.vue
+++ b/packages/frontend/src/widgets/server-metric/mem.vue
@@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 <div class="zlxnikvl">
 	<XPie class="pie" :value="usage"/>
 	<div>
-		<p><i class="ph-selection-all ph-bold ph-lg"></i>RAM</p>
+		<p><i class="ti ti-section"></i>RAM</p>
 		<p>Total: {{ bytes(total, 1) }}</p>
 		<p>Used: {{ bytes(used, 1) }}</p>
 		<p>Free: {{ bytes(free, 1) }}</p>
diff --git a/packages/frontend/tsconfig.json b/packages/frontend/tsconfig.json
index 819629a9cf..fe4d202894 100644
--- a/packages/frontend/tsconfig.json
+++ b/packages/frontend/tsconfig.json
@@ -37,13 +37,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 850515b59e..07cf3b4a69 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';
@@ -14,7 +16,15 @@ const { port } = yaml.load(await readFile('../../.config/default.yml', 'utf-8'))
 const httpUrl = `http://localhost:${port}/`;
 const websocketUrl = `ws://localhost:${port}/`;
 
-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',
@@ -53,17 +63,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 4d150aa357..674fdbf680 100644
--- a/packages/frontend/vite.config.ts
+++ b/packages/frontend/vite.config.ts
@@ -5,9 +5,10 @@ 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';
+import { pluginReplaceIcons } from './vite.replaceIcons.ts';
 
 const extensions = ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.json', '.json5', '.svg', '.sass', '.scss', '.css', '.vue', '.wasm'];
 
@@ -71,6 +72,7 @@ export function getConfig(): UserConfig {
 			pluginVue(),
 			pluginUnwindCssModuleClassName(),
 			pluginJson5(),
+			...pluginReplaceIcons(),
 			...process.env.NODE_ENV === 'production'
 				? [
 					pluginReplace({
diff --git a/packages/frontend/vite.replaceIcons.ts b/packages/frontend/vite.replaceIcons.ts
new file mode 100644
index 0000000000..92ac568ef3
--- /dev/null
+++ b/packages/frontend/vite.replaceIcons.ts
@@ -0,0 +1,376 @@
+import pluginReplace from '@rollup/plugin-replace';
+import type { RollupReplaceOptions } from '@rollup/plugin-replace';
+
+function iconsReplace(opts: RollupReplaceOptions) {
+	return pluginReplace({
+		...opts,
+		preventAssignment: false,
+		// only replace these strings at the start of strings, and make
+		// sure they're followed by a word-boundary that's not a dash
+		delimiters: ['(?<=["\'`])', '\\b(?!-)'],
+	});
+}
+
+export function pluginReplaceIcons() {
+	return [
+			iconsReplace({
+				values: {
+					'ti ti-alert-triangle': 'ph-warning ph-bold ph-lg',
+				},
+				exclude: [
+					'**/components/MkAnnouncementDialog.*',
+					'**/pages/announcement.*',
+				],
+			}),
+			iconsReplace({
+				values: {
+					'ti ti-alert-triangle': 'ph-warning-circle ph-bold ph-lg',
+				},
+				include: [
+					'**/components/MkAnnouncementDialog.*',
+					'**/pages/announcement.*',
+				],
+			}),
+			iconsReplace({
+				values: {
+					'ti ti-apps': 'ph-squares-four ph-bold ph-lg',
+				},
+				include: [
+					'**/pages/**',
+				],
+			}),
+			iconsReplace({
+				values: {
+					'ti ti-apps': 'ph-stack ph-bold ph-lg',
+				},
+				include: [
+					'**/ui/**',
+				],
+			}),
+			iconsReplace({
+				values: {
+					'ti ti-clock-play': 'ph-clock ph-bold ph-lg',
+				},
+				exclude: [
+					'**/components/MkMedia*',
+				],
+			}),
+			iconsReplace({
+				values: {
+					'ti ti-clock-play': 'ph ph-gauge ph-bold ph-lg',
+				},
+				include: [
+					'**/components/MkMedia*',
+				],
+			}),
+			iconsReplace({
+				values: {
+					'ti ti-photo': 'ph-image-square ph-bold ph-lg',
+				},
+				exclude: [
+					'**/pages/admin-user.*',
+				],
+			}),
+			iconsReplace({
+				values: {
+					'ti ti-photo': 'ph-image ph-bold ph-lg',
+				},
+				include: [
+					'**/pages/admin-user.*',
+				],
+			}),
+			iconsReplace({
+				values: {
+					'ti ti-reload': 'ph-arrow-clockwise ph-bold ph-lg',
+				},
+				exclude: [
+					'**/pages/settings/emoji-picker.*',
+					'**/pages/flash/flash.*',
+					'**/components/MkPageWindow.*',
+				],
+			}),
+			iconsReplace({
+				values: {
+					'ti ti-reload': 'ph-arrow-counter-clockwise ph-bold ph-lg',
+				},
+				include: [
+					'**/pages/settings/emoji-picker.*',
+				],
+			}),
+			iconsReplace({
+				values: {
+					'ti ti-reload': 'ph-arrows-clockwise ph-bold ph-lg',
+				},
+				include: [
+					'**/pages/flash/flash.*',
+					'**/components/MkPageWindow.*',
+				],
+			}),
+			iconsReplace({
+				values: {
+					'ti ti-repeat': 'ph-rocket-launch ph-bold ph-lg',
+				},
+				exclude: [
+					'**/components/MkMedia*',
+					'**/scripts/get-user-menu.*',
+					'**/pages/gallery/post.*',
+				],
+			}),
+			iconsReplace({
+				values: {
+					'ti ti-repeat': 'ph ph-repeat ph-bold ph-lg',
+				},
+				include: [
+					'**/components/MkMedia*',
+					'**/scripts/get-user-menu.*',
+					'**/pages/gallery/post.*',
+				],
+			}),
+			iconsReplace({
+				values: {
+					'icon ti ti-brand-youtube': 'icon ph-youtube-logo ph-bold ph-lg',
+					'ti ti ti-folder-symlink': 'sk-icons sk-foldermove sk-icons-lg',
+					'ti ti-123': 'ph-numpad ph-bold ph-lg',
+					'ti ti-access-point': 'ph-broadcast ph-bold ph-lg',
+					'ti ti-activity': 'ph-pulse ph-bold ph-lg',
+					'ti ti-ad': 'ph-flag ph-bold ph-lg',
+					'ti ti-adjustments': 'ph-faders ph-bold ph-lg',
+					'ti ti-align-box-left-bottom': 'ph-arrow-down-left ph-bold ph-lg',
+					'ti ti-align-box-left-top': 'ph-arrow-up-left ph-bold ph-lg',
+					'ti ti-align-box-right-bottom': 'ph-arrow-down-right ph-bold ph-lg',
+					'ti ti-align-box-right-top': 'ph-arrow-up-right ph-bold ph-lg',
+					'ti ti-align-left': 'ph-text-align-left ph-bold ph-lg',
+					'ti ti-antenna': 'ph-flying-saucer ph-bold ph-lg',
+					'ti ti-api': 'ph-key ph-bold ph-lg',
+					'ti ti-app-window': 'ph-app-window ph-bold ph-lg',
+					'ti ti-apple': 'ph-orange-slice ph-bold ph-lg',
+					'ti ti-arrow-back-up': 'ph-arrow-u-up-left ph-bold ph-lg',
+					'ti ti-arrow-bar-to-down': 'ph-arrow-line-down ph-bold ph-lg',
+					'ti ti-arrow-big-right': 'ph-arrow-fat-right ph-bold ph-lg',
+					'ti ti-arrow-down': 'ph-arrow-down ph-bold ph-lg',
+					'ti ti-arrow-left': 'ph-arrow-left ph-bold ph-lg',
+					'ti ti-arrow-narrow-up': 'ph-arrow-up ph-bold ph-lg',
+					'ti ti-arrow-right': 'ph-arrow-right ph-bold ph-lg',
+					'ti ti-arrow-up': 'ph-arrow-up ph-bold ph-lg',
+					'ti ti-arrows-maximize': 'ph-arrows-out ph-bold ph-lg',
+					'ti ti-arrows-minimize': 'ph-arrows-in ph-bold ph-lg',
+					'ti ti-arrows-move': 'ph-arrows-out-cardinal ph-bold ph-lg',
+					'ti ti-arrows-sort': 'ph-arrows-down-up ph-bold ph-lg',
+					'ti ti-arrows-up': 'ph-arrow-line-up ph-bold ph-lg',
+					'ti ti-asterisk': 'ph-asterisk ph-bold ph-lg',
+					'ti ti-at': 'ph-at ph-bold ph-lg',
+					'ti ti-backspace': 'ph-backspace ph-bold ph-lg',
+					'ti ti-badge': 'ph-seal-check ph-bold ph-lg',
+					'ti ti-badges': 'ph-seal-check ph-bold ph-lg',
+					'ti ti-ban': 'ph-prohibit ph-bold ph-lg',
+					'ti ti-bell': 'ph-bell ph-bold ph-lg',
+					'ti ti-bell-off': 'ph-bell ph-bold ph-lg',
+					'ti ti-bell-plus': 'ph-bell-ringing ph-bold ph-lg',
+					'ti ti-bell-ringing-2': 'ph-bell-ringing ph-bold ph-lg',
+					'ti ti-bolt': 'ph-lightning ph-bold ph-lg',
+					'ti ti-bookmark': 'ph-bookmark ph-bold ph-lg',
+					'ti ti-brand-x': 'ph-twitter-logo ph-bold ph-lg',
+					'ti ti-bulb': 'ph-lightbulb ph-bold ph-lg',
+					'ti ti-cake': 'ph-cake ph-bold ph-lg',
+					'ti ti-calendar': 'ph-calendar ph-bold ph-lg',
+					'ti ti-calendar-time': 'ph-calendar ph-bold ph-lg',
+					'ti ti-camera': 'ph-camera ph-bold ph-lg',
+					'ti ti-carousel-horizontal': 'ph-split-horizontal ph-bold ph-lg',
+					'ti ti-carousel-vertical': 'ph-split-vertical ph-bold ph-lg',
+					'ti ti-chart-arrows': 'ph-chart-bar-horizontal ph-bold ph-lg',
+					'ti ti-chart-line': 'ph-chart-line ph-bold ph-lg',
+					'ti ti-check': 'ph-check ph-bold ph-lg',
+					'ti ti-checkbox': 'ph-check ph-bold ph-lg',
+					'ti ti-checklist': 'ph-list-checks ph-bold ph-lg',
+					'ti ti-checkup-list': 'ph-list-checks ph-bold ph-lg',
+					'ti ti-chevron-double-right': 'ph-caret-double-right ph-bold ph-lg',
+					'ti ti-chevron-left': 'ph-caret-left ph-bold ph-lg',
+					'ti ti-chevron-right': 'ph-caret-right ph-bold ph-lg',
+					'ti ti-chevron-up': 'ph-caret-up ph-bold ph-lg',
+					'ti ti-chevrons-left': 'ph-caret-dobule-left ph-bold ph-lg',
+					'ti ti-chevrons-right': 'ph-caret-right ph-bold ph-lg',
+					'ti ti-circle': 'ph-circle ph-bold ph-lg',
+					'ti ti-circle-check': 'ph-seal-check ph-bold ph-lg',
+					'ti ti-circle-filled': 'ph-circle-half ph-bold ph-lg',
+					'ti ti-circle-minus': 'ph-minus-circle ph-bold ph-lg',
+					'ti ti-circle-x': 'ph-x-circle ph-bold ph-lg',
+					'ti ti-clock': 'ph-clock ph-bold ph-lg',
+					'ti ti-clock-edit': 'ph-pencil-simple ph-bold ph-lg',
+					'ti ti-cloud': 'ph-cloud ph-bold ph-lg',
+					'ti ti-code': 'ph-code ph-bold ph-lg',
+					'ti ti-columns': 'ph-text-columns ph-bold ph-lg',
+					'ti ti-comet': 'ph-shooting-star ph-bold ph-lg',
+					'ti ti-confetti': 'ph-confetti ph-bold ph-lg',
+					'ti ti-cookie': 'ph-cookie ph-bold ph-lg',
+					'ti ti-copy': 'ph-copy ph-bold ph-lg',
+					'ti ti-cpu': 'ph-cpu ph-bold ph-lg',
+					'ti ti-crop': 'ph-crop ph-bold ph-lg',
+					'ti ti-crown': 'ph-crown ph-bold ph-lg',
+					'ti ti-dashboard': 'ph-gauge ph-bold ph-lg',
+					'ti ti-database': 'ph-database ph-bold ph-lg',
+					'ti ti-device-desktop': 'ph-desktop ph-bold ph-lg',
+					'ti ti-device-floppy': 'ph-floppy-disk ph-bold ph-lg',
+					'ti ti-device-gamepad': 'ph-game-controller ph-bold ph-lg',
+					'ti ti-device-mobile': 'ph-device-mobile ph-bold ph-lg',
+					'ti ti-device-tablet': 'ph-device-tablet ph-bold ph-lg',
+					'ti ti-device-tv': 'ph-television ph-bold ph-lg',
+					'ti ti-devices': 'ph-devices ph-bold ph-lg',
+					'ti ti-dice': 'ph ph-dice-five ph-bold ph-lg',
+					'ti ti-dice-5': 'ph ph-dice-five ph-bold ph-lg',
+					'ti ti-dots': 'ph-dots-three ph-bold ph-lg',
+					'ti ti-download': 'ph-download ph-bold ph-lg',
+					'ti ti-edit': 'ph-pencil-simple-line ph-bold ph-lg',
+					'ti ti-equal-double': 'ph-equals ph-bold ph-lg',
+					'ti ti-equal-not': 'ph-prohibit ph-bold ph-lg',
+					'ti ti-exclamation-circle': 'ph-warning-circle ph-bold ph-lg',
+					'ti ti-external-link': 'ph-arrow-square-out ph-bold ph-lg',
+					'ti ti-eye': 'ph-eye ph-bold ph-lg',
+					'ti ti-eye-exclamation': 'ph-eye-slash ph-bold ph-lg',
+					'ti ti-eye-off': 'ti ti-eye-exclamation',
+					'ti ti-feather': 'ph-feather ph-bold ph-lg',
+					'ti ti-file': 'ph-file ph-bold ph-lg',
+					'ti ti-file-invoice': 'ph-newspaper-clipping ph-bold ph-lg',
+					'ti ti-file-music': 'ph-file-audio ph-bold ph-lg',
+					'ti ti-file-text': 'ph-file-text ph-bold ph-lg',
+					'ti ti-file-zip': 'ph-file-zip ph-bold ph-lg',
+					'ti ti-filter': 'ph-funnel ph-bold ph-lg',
+					'ti ti-flare': 'ph-fire ph-bold ph-lg',
+					'ti ti-flask': 'ph-flask ph-bold ph-lg',
+					'ti ti-folder': 'ph-folder ph-bold ph-lg',
+					'ti ti-folder-plus': 'ph-folder-plus ph-bold ph-lg',
+					'ti ti-folder-symlink': 'sk-icons sk-foldermove sk-icons-lg',
+					'ti ti-forms': 'ph-textbox ph-bold ph-lg',
+					'ti ti-ghost': 'ph-ghost ph-bold ph-lg',
+					'ti ti-grid-dots': 'ph-dots-nine ph-bold ph-lg',
+					'ti ti-hash': 'ph-hash ph-bold ph-lg',
+					'ti ti-heart': 'ph-heart ph-bold ph-lg',
+					'ti ti-heart-filled': 'ph-heart ph-bold ph-lg',
+					'ti ti-heart-off': 'ph-heart-break ph-bold ph-lg',
+					'ti ti-heart-plus': 'ph-heart ph-bold ph-lg',
+					'ti ti-help-circle': 'ph-question ph-bold ph-lg',
+					'ti ti-home': 'ph-house ph-bold ph-lg',
+					'ti ti-hourglass-empty': 'ph-hourglass ph-bold ph-lg',
+					'ti ti-id': 'ph-identification-card ph-bold ph-lg',
+					'ti ti-info-circle': 'ph-info ph-bold ph-lg',
+					'ti ti-key': 'ph-key ph-bold ph-lg',
+					'ti ti-language-hiragana': 'ph-translate ph-bold ph-lg',
+					'ti ti-leaf': 'ph-leaf ph-bold ph-lg',
+					'ti ti-license': 'ph-notebook ph-bold ph-lg',
+					'ti ti-link': 'ph-link ph-bold ph-lg',
+					'ti ti-link-off': 'ph-link-break ph-bold ph-lg',
+					'ti ti-list': 'ph-list ph-bold ph-lg',
+					'ti ti-list-search': 'ph-list ph-bold ph-lg-search',
+					'ti ti-lock': 'ph-lock ph-bold ph-lg',
+					'ti ti-lock-open': 'ph-lock-open ph-bold ph-lg',
+					'ti ti-mail': 'ph-envelope ph-bold ph-lg',
+					'ti ti-map-pin': 'ph-map-pin ph-bold ph-lg',
+					'ti ti-maximize': 'ph-frame-corners ph-bold ph-lg',
+					'ti ti-medal': 'ph-trophy ph-bold ph-lg',
+					'ti ti-menu': 'ph-list ph-bold ph-lg',
+					'ti ti-menu-2': 'ph-list ph-bold ph-lg',
+					'ti ti-message': 'ph-envelope ph-bold ph-lg',
+					'ti ti-message-off': 'ph-bell-slash ph-bold ph-lg',
+					'ti ti-messages': 'ph-envelope ph-bold ph-lg',
+					'ti ti-messages-off': 'ph-envelope-open ph-bold ph-lg',
+					'ti ti-minimize': 'ph-arrows-in-simple ph-bold ph-lg',
+					'ti ti-minus': 'ph-minus ph-bold ph-lg',
+					'ti ti-mood-happy': 'ph-smiley ph-bold ph-lg',
+					'ti ti-mood-smile': 'ph-smiley ph-bold pg-lg',
+					'ti ti-moon': 'ph-moon ph-bold ph-lg',
+					'ti ti-movie': 'ph-film-strip ph-bold ph-lg',
+					'ti ti-music': 'ph-music-notes ph-bold ph-lg',
+					'ti ti-news': 'ph-newspaper ph-bold ph-lg',
+					'ti ti-note': 'ph-note ph-bold ph-lg',
+					'ti ti-notebook': 'ph-notebook ph-bold ph-lg',
+					'ti ti-package': 'ph-package ph-bold ph-lg',
+					'ti ti-paint': 'ph-paint-roller ph-bold ph-lg',
+					'ti ti-palette': 'ph-palette ph-bold ph-lg',
+					'ti ti-paperclip': 'ph-paperclip ph-bold ph-lg',
+					'ti ti-password': 'ph-password ph-bold ph-lg',
+					'ti ti-pencil': 'ph-pencil-simple ph-bold ph-lg',
+					'ti ti-pencil-plus': 'ph-plus ph-bold pg-lg',
+					'ti ti-photo-plus': 'ph-image-square ph-bold ph-lg',
+					'ti ti-picture-in-picture': 'ph-picture-in-picture ph-bold ph-lg',
+					'ti ti-pin': 'ph-push-pin ph-bold ph-lg',
+					'ti ti-pinned-off': 'ph-push-pin-slash ph-bold ph-lg',
+					'ti ti-plane': 'ph-airplane ph-bold ph-lg',
+					'ti ti-plane-arrival': 'ph-airplane-landing ph-bold ph-lg',
+					'ti ti-plane-departure': 'ph-airplane-takeoff ph-bold ph-lg',
+					'ti ti-planet': 'ph-planet ph-bold ph-lg',
+					'ti ti-planet-off': 'ph-globe-simple ph-bold ph-lg',
+					'ti ti-player-eject': 'ph-eject ph-bold ph-lg',
+					'ti ti-player-pause': 'ph-pause ph-bold ph-lg',
+					'ti ti-player-pause-filled': 'ph-pause ph-bold ph-lg',
+					'ti ti-player-play': 'ph-play ph-bold ph-lg',
+					'ti ti-player-play-filled': 'ph-play ph-bold ph-lg',
+					'ti ti-player-stop': 'ph-stop ph-bold ph-lg',
+					'ti ti-player-track-next': 'ph-skip-forward ph-bold ph-lg',
+					'ti ti-plug': 'ph-plug ph-bold ph-lg',
+					'ti ti-plus': 'ph-plus ph-bold ph-lg',
+					'ti ti-point': 'ph-circle ph-bold ph-lg',
+					'ti ti-power': 'ph-power ph-bold ph-lg',
+					'ti ti-presentation': 'ph-presentation ph-bold ph-lg',
+					'ti ti-quote': 'ph-quotes ph-bold ph-lg',
+					'ti ti-rectangle': 'ph-frame-corners ph-bold ph-lg',
+					'ti ti-refresh': 'ph-arrows-counter-clockwise ph-bold ph-lg',
+					'ti ti-repeat-off': 'ph-repeat ph-bold ph-lg',
+					'ti ti-restore': 'ph-box-arrow-up ph-box ph-lg',
+					'ti ti-robot': 'ph-robot ph-bold ph-lg',
+					'ti ti-rocket': 'ph-rocket-launch ph-bold ph-lg',
+					'ti ti-rocket-off': 'ph-rocket ph-bold ph-lg',
+					'ti ti-rss': 'ph-rss ph-bold ph-lg',
+					'ti ti-search': 'ph-magnifying-glass ph-bold ph-lg',
+					'ti ti-section': 'ph-selection-all ph-bold ph-lg',
+					'ti ti-selector': 'ph-caret-up-down ph-bold ph-lg',
+					'ti ti-send': 'ph-paper-plane-tilt ph-bold ph-lg',
+					'ti ti-server': 'ph-hard-drives ph-bold ph-lg',
+					'ti ti-settings': 'ph-gear ph-bold ph-lg',
+					'ti ti-share': 'ph-share-network ph-bold ph-lg',
+					'ti ti-shield': 'ph-shield ph-bold ph-lg',
+					'ti ti-shield-lock': 'ph-shield ph-bold ph-lg',
+					'ti ti-snowflake': 'ph-snowflake ph-bold ph-lg',
+					'ti ti-sparkles': 'ph-sparkle ph-bold ph-lg',
+					'ti ti-speakerphone': 'ph-megaphone ph-bold ph-lg',
+					'ti ti-stack-2': 'ph-stack ph-bold ph-lg',
+					'ti ti-star': 'ph-star ph-bold ph-lg',
+					'ti ti-star-off': 'ph-star-half ph-bold ph-lg',
+					'ti ti-sun': 'ph-sun ph-bold ph-lg',
+					'ti ti-switch-horizontal': 'ph-arrows-left-right ph-bold ph-lg',
+					'ti ti-terminal-2': 'ph-terminal-window ph-bold ph-lg',
+					'ti ti-text-caption': 'ph-text-indent ph-bold ph-lg',
+					'ti ti-tool': 'ph-wrench ph-bold ph-lg',
+					'ti ti-trash': 'ph-trash ph-bold ph-lg',
+					'ti ti-trophy': 'ph-trophy ph-bold ph-lg',
+					'ti ti-universe': 'ph-rocket-launch ph-bold ph-lg',
+					'ti ti-upload': 'ph-upload ph-bold ph-lg',
+					'ti ti-user': 'ph-user ph-bold ph-lg',
+					'ti ti-user-check': 'ph-check ph-bold ph-lg',
+					'ti ti-user-circle': 'ph-user-circle ph-bold ph-lg',
+					'ti ti-user-edit': 'ph-user-list ph-bold ph-lg',
+					'ti ti-user-exclamation': 'ph-warning-circle ph-bold ph-lg',
+					'ti ti-user-off': 'ph-user-minus ph-bold ph-lg',
+					'ti ti-user-plus': 'ph-user-plus ph-bold ph-lg',
+					'ti ti-user-search': 'ph-user-circle ph-bold ph-lg',
+					'ti ti-user-shield': 'ph-newspaper-clipping ph-bold ph-lg',
+					'ti ti-users': 'ph-users ph-bold ph-lg',
+					'ti ti-video': 'ph-video ph-bold ph-lg',
+					'ti ti-volume': 'ph-speaker-high ph-bold ph-lg',
+					'ti ti-volume-3': 'ph-speaker-x ph-bold ph-lg',
+					'ti ti-webhook': 'ph-webhooks-logo ph-bold ph-lg',
+					'ti ti-whirl': 'ph-globe-hemisphere-west ph-bold ph-lg',
+					'ti ti-window-maximize': 'ph-frame-corners ph-bold ph-lg',
+					'ti ti-world': 'ph-globe-hemisphere-west ph-bold ph-lg',
+					'ti ti-world-download': 'ph-cloud-arrow-down ph-bold ph-lg',
+					'ti ti-world-search': 'ph-binoculars ph-bold ph-lg',
+					'ti ti-world-upload': 'ph-cloud-arrow-up ph-bold ph-lg',
+					'ti ti-world-x': 'ph-planet ph-bold ph-lg',
+					'ti ti-x': 'ph-x ph-bold ph-lg',
+					'ti-help': 'ph-question ph-bold ph-lg',
+					'ti-mail': 'ph-envelope ph-bold ph-lg',
+					'ti-webhook': 'ph-webhooks-logo ph-bold ph-lg',
+ 					'ti ti-caret-down': 'ph-caret-down ph-bold ph-lg',
+ 					'ti ti-chevron-down': 'ph-caret-down ph-bold ph-lg',
+				},
+			}),
+	];
+}
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..e626c97a59 100644
--- a/packages/misskey-bubble-game/build.js
+++ b/packages/misskey-bubble-game/build.js
@@ -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..86c21a22a3
--- /dev/null
+++ b/packages/misskey-bubble-game/eslint.config.js
@@ -0,0 +1,27 @@
+import tsParser from '@typescript-eslint/parser';
+import sharedConfig from '../shared/eslint.config.js';
+
+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-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 1b72ec247b..87fda5568e 100644
--- a/packages/misskey-js/etc/misskey-js.api.md
+++ b/packages/misskey-js/etc/misskey-js.api.md
@@ -6,6 +6,11 @@
 
 import { EventEmitter } from 'eventemitter3';
 
+// 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 = {
     username: string;
@@ -21,13 +26,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'];
 
@@ -316,6 +346,33 @@ type AdminSilenceUserRequest = operations['admin___silence-user']['requestBody']
 // @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 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 AdminUnnsfwUserRequest = operations['admin___unnsfw-user']['requestBody']['content']['application/json'];
 
@@ -509,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']> {
     constructor(stream: Stream, channel: string, name?: string);
     // (undocumented)
     channel: string;
@@ -745,12 +802,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: {
@@ -759,10 +816,7 @@ export type Channels = {
             };
             ready: boolean;
             cancel: null | Record<string, never>;
-            updateSettings: {
-                key: string;
-                value: any;
-            };
+            updateSettings: ReversiUpdateSettings<ReversiUpdateKey>;
             claimTimeIsUp: null | Record<string, never>;
         };
     };
@@ -1134,6 +1188,12 @@ export type Endpoints = Overwrite<Endpoints_2, {
         req: SigninRequest;
         res: SigninResponse;
     };
+    'admin/roles/create': {
+        req: Overwrite<AdminRolesCreateRequest, {
+            policies: PartialRolePolicyOverride;
+        }>;
+        res: AdminRolesCreateResponse;
+    };
 }>;
 
 // @public (undocumented)
@@ -1143,6 +1203,7 @@ declare namespace entities {
     export {
         ID,
         DateString,
+        PureRenote,
         PageEvent,
         ModerationLog,
         ServerStats,
@@ -1159,11 +1220,21 @@ declare namespace entities {
         SignupPendingResponse,
         SigninRequest,
         SigninResponse,
+        PartialRolePolicyOverride,
         EmptyRequest,
         EmptyResponse,
         AdminMetaResponse,
         AdminAbuseUserReportsRequest,
         AdminAbuseUserReportsResponse,
+        AdminAbuseReportNotificationRecipientListRequest,
+        AdminAbuseReportNotificationRecipientListResponse,
+        AdminAbuseReportNotificationRecipientShowRequest,
+        AdminAbuseReportNotificationRecipientShowResponse,
+        AdminAbuseReportNotificationRecipientCreateRequest,
+        AdminAbuseReportNotificationRecipientCreateResponse,
+        AdminAbuseReportNotificationRecipientUpdateRequest,
+        AdminAbuseReportNotificationRecipientUpdateResponse,
+        AdminAbuseReportNotificationRecipientDeleteRequest,
         AdminAccountsCreateRequest,
         AdminAccountsCreateResponse,
         AdminAccountsDeleteRequest,
@@ -1264,6 +1335,15 @@ declare namespace entities {
         AdminRolesUpdateDefaultPoliciesRequest,
         AdminRolesUsersRequest,
         AdminRolesUsersResponse,
+        AdminSystemWebhookCreateRequest,
+        AdminSystemWebhookCreateResponse,
+        AdminSystemWebhookDeleteRequest,
+        AdminSystemWebhookListRequest,
+        AdminSystemWebhookListResponse,
+        AdminSystemWebhookShowRequest,
+        AdminSystemWebhookShowResponse,
+        AdminSystemWebhookUpdateRequest,
+        AdminSystemWebhookUpdateResponse,
         AnnouncementsRequest,
         AnnouncementsResponse,
         AnnouncementsShowRequest,
@@ -1779,7 +1859,9 @@ declare namespace entities {
         ReversiGameDetailed,
         MetaLite,
         MetaDetailedOnly,
-        MetaDetailed
+        MetaDetailed,
+        SystemWebhook,
+        AbuseReportNotificationRecipient
     }
 }
 export { entities }
@@ -1838,7 +1920,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: {
@@ -2248,6 +2330,9 @@ 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)
 type IUnpinRequest = operations['i___unpin']['requestBody']['content']['application/json'];
 
@@ -2437,10 +2522,40 @@ type ModerationLog = {
 } | {
     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", "approve", "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", "approve", "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", "createSystemWebhook", "updateSystemWebhook", "deleteSystemWebhook", "createAbuseReportNotificationRecipient", "updateAbuseReportNotificationRecipient", "deleteAbuseReportNotificationRecipient", "deleteAccount", "deletePage", "deleteFlash", "deleteGalleryPost"];
 
 // @public (undocumented)
 type MuteCreateRequest = operations['mute___create']['requestBody']['content']['application/json'];
@@ -2469,6 +2584,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'];
 
@@ -2707,7 +2829,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:approve-user", "write:admin:nsfw-user", "write:admin:unnsfw-user", "write:admin:silence-user", "write:admin:unsilence-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"];
@@ -2721,6 +2852,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'];
 
@@ -2800,6 +2940,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'];
 
@@ -2940,7 +3083,7 @@ export class Stream extends EventEmitter<StreamEvents> {
     constructor(origin: string, user: {
         token: string;
     } | null, options?: {
-        WebSocket?: any;
+        WebSocket?: WebSocket;
     });
     // (undocumented)
     close(): void;
@@ -2963,9 +3106,9 @@ export class Stream extends EventEmitter<StreamEvents> {
     // (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)
@@ -3000,6 +3143,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'];
 
@@ -3197,7 +3343,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:49:2 - (ae-forgotten-export) The symbol "ModerationLogPayloads" needs to be exported by the entry point index.d.ts
+// src/streaming.types.ts:236:4 - (ae-forgotten-export) The symbol "ReversiUpdateKey" needs to be exported by the entry point index.d.ts
+// src/streaming.types.ts:246: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..4a02bcd8ff 100644
--- a/packages/misskey-js/generator/package.json
+++ b/packages/misskey-js/generator/package.json
@@ -4,15 +4,13 @@
 	"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",
 		"@types/node": "20.9.1",
 		"@typescript-eslint/eslint-plugin": "6.11.0",
 		"@typescript-eslint/parser": "6.11.0",
-		"eslint": "8.53.0",
 		"openapi-types": "12.1.3",
 		"openapi-typescript": "6.7.3",
 		"ts-case-convert": "2.0.2",
diff --git a/packages/misskey-js/generator/src/generator.ts b/packages/misskey-js/generator/src/generator.ts
index 78178d7c7e..4ae00a4522 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,13 +87,24 @@ 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);
+				endpointReqMediaTypesSet.add(reqType.getMediaType());
+				endpointReqMediaTypes.push(reqType);
+			} else {
+				endpointReqMediaTypesSet.add('application/json');
+				endpointReqMediaTypes.push(new EndpointReqMediaType(path, undefined, 'application/json'));
 			}
+		} else {
+			endpointReqMediaTypesSet.add('application/json');
+			endpointReqMediaTypes.push(new EndpointReqMediaType(path, undefined, 'application/json'));
 		}
 
 		if (operation.responses && isResponseObject(operation.responses['200']) && operation.responses['200'].content) {
@@ -137,6 +157,19 @@ async function generateEndpoints(
 	endpointOutputLine.push('}');
 	endpointOutputLine.push('');
 
+	function generateEndpointReqMediaTypesType() {
+		return `Record<keyof Endpoints, ${[...endpointReqMediaTypesSet].map((t) => `'${t}'`).join(' | ')}>`;
+	}
+
+	endpointOutputLine.push(`export const endpointReqTypes: ${generateEndpointReqMediaTypesType()} = {`);
+
+	endpointOutputLine.push(
+		...endpointReqMediaTypes.map(it => '\t' + it.toLine()),
+	);
+
+	endpointOutputLine.push('};');
+	endpointOutputLine.push('');
+
 	await writeFile(endpointOutputPath, endpointOutputLine.join('\n'));
 }
 
@@ -314,6 +347,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..39e687d4af 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.8.0",
 	"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,25 +35,23 @@
 		"directory": "packages/misskey-js"
 	},
 	"devDependencies": {
-		"@microsoft/api-extractor": "7.43.1",
-		"@misskey-dev/eslint-plugin": "1.0.0",
+		"@microsoft/api-extractor": "7.47.4",
 		"@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/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.4",
+		"execa": "9.3.0",
+		"tsd": "0.31.1",
+		"typescript": "5.5.4",
+		"esbuild": "0.23.0",
+		"glob": "11.0.0"
 	},
 	"files": [
 		"built"
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..ea1df57f3d 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,56 @@ 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);
+	}
+
 	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';
+			if (endpoint in endpointReqTypes) {
+				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': endpointReqTypes[endpoint],
 				},
 				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..5ee4194db2 100644
--- a/packages/misskey-js/src/api.types.ts
+++ b/packages/misskey-js/src/api.types.ts
@@ -1,7 +1,8 @@
 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 {
+	PartialRolePolicyOverride,
 	SigninRequest,
 	SigninResponse,
 	SignupPendingRequest,
@@ -27,11 +28,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
 
@@ -79,5 +82,9 @@ export type Endpoints = Overwrite<
 			req: SigninRequest;
 			res: SigninResponse;
 		},
+		'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 68137b103e..c13485621b 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.
      * 
@@ -895,6 +955,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**: *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.
      * 
diff --git a/packages/misskey-js/src/autogen/endpoint.ts b/packages/misskey-js/src/autogen/endpoint.ts
index 9f0ff8364c..628825d504 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,
@@ -104,6 +113,15 @@ import type {
 	AdminRolesUpdateDefaultPoliciesRequest,
 	AdminRolesUsersRequest,
 	AdminRolesUsersResponse,
+	AdminSystemWebhookCreateRequest,
+	AdminSystemWebhookCreateResponse,
+	AdminSystemWebhookDeleteRequest,
+	AdminSystemWebhookListRequest,
+	AdminSystemWebhookListResponse,
+	AdminSystemWebhookShowRequest,
+	AdminSystemWebhookShowResponse,
+	AdminSystemWebhookUpdateRequest,
+	AdminSystemWebhookUpdateResponse,
 	AnnouncementsRequest,
 	AnnouncementsResponse,
 	AnnouncementsShowRequest,
@@ -573,6 +591,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 };
@@ -652,6 +675,11 @@ 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 };
 	'announcements': { req: AnnouncementsRequest; res: AnnouncementsResponse };
 	'announcements/show': { req: AnnouncementsShowRequest; res: AnnouncementsShowResponse };
 	'antennas/create': { req: AntennasCreateRequest; res: AntennasCreateResponse };
@@ -954,3 +982,398 @@ export type Endpoints = {
 	'reversi/surrender': { req: ReversiSurrenderRequest; res: EmptyResponse };
 	'reversi/verify': { req: ReversiVerifyRequest; res: ReversiVerifyResponse };
 }
+
+export const endpointReqTypes: Record<keyof Endpoints, 'application/json' | 'multipart/form-data'> = {
+	'admin/meta': 'application/json',
+	'admin/abuse-user-reports': 'application/json',
+	'admin/abuse-report/notification-recipient/list': 'application/json',
+	'admin/abuse-report/notification-recipient/show': 'application/json',
+	'admin/abuse-report/notification-recipient/create': 'application/json',
+	'admin/abuse-report/notification-recipient/update': 'application/json',
+	'admin/abuse-report/notification-recipient/delete': 'application/json',
+	'admin/accounts/create': 'application/json',
+	'admin/accounts/delete': 'application/json',
+	'admin/accounts/find-by-email': 'application/json',
+	'admin/ad/create': 'application/json',
+	'admin/ad/delete': 'application/json',
+	'admin/ad/list': 'application/json',
+	'admin/ad/update': 'application/json',
+	'admin/announcements/create': 'application/json',
+	'admin/announcements/delete': 'application/json',
+	'admin/announcements/list': 'application/json',
+	'admin/announcements/update': 'application/json',
+	'admin/avatar-decorations/create': 'application/json',
+	'admin/avatar-decorations/delete': 'application/json',
+	'admin/avatar-decorations/list': 'application/json',
+	'admin/avatar-decorations/update': 'application/json',
+	'admin/delete-all-files-of-a-user': 'application/json',
+	'admin/unset-user-avatar': 'application/json',
+	'admin/unset-user-banner': 'application/json',
+	'admin/drive/clean-remote-files': 'application/json',
+	'admin/drive/cleanup': 'application/json',
+	'admin/drive/files': 'application/json',
+	'admin/drive/show-file': 'application/json',
+	'admin/emoji/add-aliases-bulk': 'application/json',
+	'admin/emoji/add': 'application/json',
+	'admin/emoji/copy': 'application/json',
+	'admin/emoji/delete-bulk': 'application/json',
+	'admin/emoji/delete': 'application/json',
+	'admin/emoji/import-zip': 'application/json',
+	'admin/emoji/list-remote': 'application/json',
+	'admin/emoji/list': 'application/json',
+	'admin/emoji/remove-aliases-bulk': 'application/json',
+	'admin/emoji/set-aliases-bulk': 'application/json',
+	'admin/emoji/set-category-bulk': 'application/json',
+	'admin/emoji/set-license-bulk': 'application/json',
+	'admin/emoji/update': 'application/json',
+	'admin/federation/delete-all-files': 'application/json',
+	'admin/federation/refresh-remote-instance-metadata': 'application/json',
+	'admin/federation/remove-all-following': 'application/json',
+	'admin/federation/update-instance': 'application/json',
+	'admin/get-index-stats': 'application/json',
+	'admin/get-table-stats': 'application/json',
+	'admin/get-user-ips': 'application/json',
+	'admin/invite/create': 'application/json',
+	'admin/invite/list': 'application/json',
+	'admin/promo/create': 'application/json',
+	'admin/queue/clear': 'application/json',
+	'admin/queue/deliver-delayed': 'application/json',
+	'admin/queue/inbox-delayed': 'application/json',
+	'admin/queue/promote': 'application/json',
+	'admin/queue/stats': 'application/json',
+	'admin/relays/add': 'application/json',
+	'admin/relays/list': 'application/json',
+	'admin/relays/remove': 'application/json',
+	'admin/reset-password': 'application/json',
+	'admin/resolve-abuse-user-report': 'application/json',
+	'admin/send-email': 'application/json',
+	'admin/server-info': 'application/json',
+	'admin/show-moderation-logs': 'application/json',
+	'admin/show-user': 'application/json',
+	'admin/show-users': 'application/json',
+	'admin/nsfw-user': 'application/json',
+	'admin/unnsfw-user': 'application/json',
+	'admin/silence-user': 'application/json',
+	'admin/unsilence-user': 'application/json',
+	'admin/suspend-user': 'application/json',
+	'admin/approve-user': 'application/json',
+	'admin/unsuspend-user': 'application/json',
+	'admin/update-meta': 'application/json',
+	'admin/delete-account': 'application/json',
+	'admin/update-user-note': 'application/json',
+	'admin/roles/create': 'application/json',
+	'admin/roles/delete': 'application/json',
+	'admin/roles/list': 'application/json',
+	'admin/roles/show': 'application/json',
+	'admin/roles/update': 'application/json',
+	'admin/roles/assign': 'application/json',
+	'admin/roles/unassign': 'application/json',
+	'admin/roles/update-default-policies': 'application/json',
+	'admin/roles/users': 'application/json',
+	'admin/system-webhook/create': 'application/json',
+	'admin/system-webhook/delete': 'application/json',
+	'admin/system-webhook/list': 'application/json',
+	'admin/system-webhook/show': 'application/json',
+	'admin/system-webhook/update': 'application/json',
+	'announcements': 'application/json',
+	'announcements/show': 'application/json',
+	'antennas/create': 'application/json',
+	'antennas/delete': 'application/json',
+	'antennas/list': 'application/json',
+	'antennas/notes': 'application/json',
+	'antennas/show': 'application/json',
+	'antennas/update': 'application/json',
+	'ap/get': 'application/json',
+	'ap/show': 'application/json',
+	'app/create': 'application/json',
+	'app/show': 'application/json',
+	'auth/accept': 'application/json',
+	'auth/session/generate': 'application/json',
+	'auth/session/show': 'application/json',
+	'auth/session/userkey': 'application/json',
+	'blocking/create': 'application/json',
+	'blocking/delete': 'application/json',
+	'blocking/list': 'application/json',
+	'channels/create': 'application/json',
+	'channels/featured': 'application/json',
+	'channels/follow': 'application/json',
+	'channels/followed': 'application/json',
+	'channels/owned': 'application/json',
+	'channels/show': 'application/json',
+	'channels/timeline': 'application/json',
+	'channels/unfollow': 'application/json',
+	'channels/update': 'application/json',
+	'channels/favorite': 'application/json',
+	'channels/unfavorite': 'application/json',
+	'channels/my-favorites': 'application/json',
+	'channels/search': 'application/json',
+	'charts/active-users': 'application/json',
+	'charts/ap-request': 'application/json',
+	'charts/drive': 'application/json',
+	'charts/federation': 'application/json',
+	'charts/instance': 'application/json',
+	'charts/notes': 'application/json',
+	'charts/user/drive': 'application/json',
+	'charts/user/following': 'application/json',
+	'charts/user/notes': 'application/json',
+	'charts/user/pv': 'application/json',
+	'charts/user/reactions': 'application/json',
+	'charts/users': 'application/json',
+	'clips/add-note': 'application/json',
+	'clips/remove-note': 'application/json',
+	'clips/create': 'application/json',
+	'clips/delete': 'application/json',
+	'clips/list': 'application/json',
+	'clips/notes': 'application/json',
+	'clips/show': 'application/json',
+	'clips/update': 'application/json',
+	'clips/favorite': 'application/json',
+	'clips/unfavorite': 'application/json',
+	'clips/my-favorites': 'application/json',
+	'drive': 'application/json',
+	'drive/files': 'application/json',
+	'drive/files/attached-notes': 'application/json',
+	'drive/files/check-existence': 'application/json',
+	'drive/files/create': 'multipart/form-data',
+	'drive/files/delete': 'application/json',
+	'drive/files/find-by-hash': 'application/json',
+	'drive/files/find': 'application/json',
+	'drive/files/show': 'application/json',
+	'drive/files/update': 'application/json',
+	'drive/files/upload-from-url': 'application/json',
+	'drive/folders': 'application/json',
+	'drive/folders/create': 'application/json',
+	'drive/folders/delete': 'application/json',
+	'drive/folders/find': 'application/json',
+	'drive/folders/show': 'application/json',
+	'drive/folders/update': 'application/json',
+	'drive/stream': 'application/json',
+	'email-address/available': 'application/json',
+	'endpoint': 'application/json',
+	'endpoints': 'application/json',
+	'export-custom-emojis': 'application/json',
+	'federation/followers': 'application/json',
+	'federation/following': 'application/json',
+	'federation/instances': 'application/json',
+	'federation/show-instance': 'application/json',
+	'federation/update-remote-user': 'application/json',
+	'federation/users': 'application/json',
+	'federation/stats': 'application/json',
+	'following/create': 'application/json',
+	'following/delete': 'application/json',
+	'following/update': 'application/json',
+	'following/update-all': 'application/json',
+	'following/invalidate': 'application/json',
+	'following/requests/accept': 'application/json',
+	'following/requests/cancel': 'application/json',
+	'following/requests/list': 'application/json',
+	'following/requests/reject': 'application/json',
+	'gallery/featured': 'application/json',
+	'gallery/popular': 'application/json',
+	'gallery/posts': 'application/json',
+	'gallery/posts/create': 'application/json',
+	'gallery/posts/delete': 'application/json',
+	'gallery/posts/like': 'application/json',
+	'gallery/posts/show': 'application/json',
+	'gallery/posts/unlike': 'application/json',
+	'gallery/posts/update': 'application/json',
+	'get-online-users-count': 'application/json',
+	'get-avatar-decorations': 'application/json',
+	'hashtags/list': 'application/json',
+	'hashtags/search': 'application/json',
+	'hashtags/show': 'application/json',
+	'hashtags/trend': 'application/json',
+	'hashtags/users': 'application/json',
+	'i': 'application/json',
+	'i/2fa/done': 'application/json',
+	'i/2fa/key-done': 'application/json',
+	'i/2fa/password-less': 'application/json',
+	'i/2fa/register-key': 'application/json',
+	'i/2fa/register': 'application/json',
+	'i/2fa/update-key': 'application/json',
+	'i/2fa/remove-key': 'application/json',
+	'i/2fa/unregister': 'application/json',
+	'i/apps': 'application/json',
+	'i/authorized-apps': 'application/json',
+	'i/claim-achievement': 'application/json',
+	'i/change-password': 'application/json',
+	'i/delete-account': 'application/json',
+	'i/export-data': 'application/json',
+	'i/export-blocking': 'application/json',
+	'i/export-following': 'application/json',
+	'i/export-mute': 'application/json',
+	'i/export-notes': 'application/json',
+	'i/export-clips': 'application/json',
+	'i/export-favorites': 'application/json',
+	'i/export-user-lists': 'application/json',
+	'i/export-antennas': 'application/json',
+	'i/favorites': 'application/json',
+	'i/gallery/likes': 'application/json',
+	'i/gallery/posts': 'application/json',
+	'i/import-blocking': 'application/json',
+	'i/import-following': 'application/json',
+	'i/import-notes': 'application/json',
+	'i/import-muting': 'application/json',
+	'i/import-user-lists': 'application/json',
+	'i/import-antennas': 'application/json',
+	'i/notifications': 'application/json',
+	'i/notifications-grouped': 'application/json',
+	'i/page-likes': 'application/json',
+	'i/pages': 'application/json',
+	'i/pin': 'application/json',
+	'i/read-all-unread-notes': 'application/json',
+	'i/read-announcement': 'application/json',
+	'i/regenerate-token': 'application/json',
+	'i/registry/get-all': 'application/json',
+	'i/registry/get-unsecure': 'application/json',
+	'i/registry/get-detail': 'application/json',
+	'i/registry/get': 'application/json',
+	'i/registry/keys-with-type': 'application/json',
+	'i/registry/keys': 'application/json',
+	'i/registry/remove': 'application/json',
+	'i/registry/scopes-with-domain': 'application/json',
+	'i/registry/set': 'application/json',
+	'i/revoke-token': 'application/json',
+	'i/signin-history': 'application/json',
+	'i/unpin': 'application/json',
+	'i/update-email': 'application/json',
+	'i/update': 'application/json',
+	'i/move': 'application/json',
+	'i/webhooks/create': 'application/json',
+	'i/webhooks/list': 'application/json',
+	'i/webhooks/show': 'application/json',
+	'i/webhooks/update': 'application/json',
+	'i/webhooks/delete': 'application/json',
+	'invite/create': 'application/json',
+	'invite/delete': 'application/json',
+	'invite/list': 'application/json',
+	'invite/limit': 'application/json',
+	'meta': 'application/json',
+	'emojis': 'application/json',
+	'emoji': 'application/json',
+	'miauth/gen-token': 'application/json',
+	'mute/create': 'application/json',
+	'mute/delete': 'application/json',
+	'mute/list': 'application/json',
+	'renote-mute/create': 'application/json',
+	'renote-mute/delete': 'application/json',
+	'renote-mute/list': 'application/json',
+	'my/apps': 'application/json',
+	'notes': 'application/json',
+	'notes/children': 'application/json',
+	'notes/clips': 'application/json',
+	'notes/conversation': 'application/json',
+	'notes/create': 'application/json',
+	'notes/delete': 'application/json',
+	'notes/favorites/create': 'application/json',
+	'notes/favorites/delete': 'application/json',
+	'notes/featured': 'application/json',
+	'notes/global-timeline': 'application/json',
+	'notes/bubble-timeline': 'application/json',
+	'notes/hybrid-timeline': 'application/json',
+	'notes/local-timeline': 'application/json',
+	'notes/mentions': 'application/json',
+	'notes/polls/recommendation': 'application/json',
+	'notes/polls/vote': 'application/json',
+	'notes/reactions': 'application/json',
+	'notes/reactions/create': 'application/json',
+	'notes/reactions/delete': 'application/json',
+	'notes/like': 'application/json',
+	'notes/renotes': 'application/json',
+	'notes/replies': 'application/json',
+	'notes/search-by-tag': 'application/json',
+	'notes/search': 'application/json',
+	'notes/show': 'application/json',
+	'notes/state': 'application/json',
+	'notes/thread-muting/create': 'application/json',
+	'notes/thread-muting/delete': 'application/json',
+	'notes/timeline': 'application/json',
+	'notes/translate': 'application/json',
+	'notes/unrenote': 'application/json',
+	'notes/user-list-timeline': 'application/json',
+	'notes/edit': 'application/json',
+	'notes/versions': 'application/json',
+	'notifications/create': 'application/json',
+	'notifications/flush': 'application/json',
+	'notifications/mark-all-as-read': 'application/json',
+	'notifications/test-notification': 'application/json',
+	'page-push': 'application/json',
+	'pages/create': 'application/json',
+	'pages/delete': 'application/json',
+	'pages/featured': 'application/json',
+	'pages/like': 'application/json',
+	'pages/show': 'application/json',
+	'pages/unlike': 'application/json',
+	'pages/update': 'application/json',
+	'flash/create': 'application/json',
+	'flash/delete': 'application/json',
+	'flash/featured': 'application/json',
+	'flash/like': 'application/json',
+	'flash/show': 'application/json',
+	'flash/unlike': 'application/json',
+	'flash/update': 'application/json',
+	'flash/my': 'application/json',
+	'flash/my-likes': 'application/json',
+	'ping': 'application/json',
+	'pinned-users': 'application/json',
+	'promo/read': 'application/json',
+	'roles/list': 'application/json',
+	'roles/show': 'application/json',
+	'roles/users': 'application/json',
+	'roles/notes': 'application/json',
+	'request-reset-password': 'application/json',
+	'reset-db': 'application/json',
+	'reset-password': 'application/json',
+	'server-info': 'application/json',
+	'stats': 'application/json',
+	'sw/show-registration': 'application/json',
+	'sw/update-registration': 'application/json',
+	'sw/register': 'application/json',
+	'sw/unregister': 'application/json',
+	'test': 'application/json',
+	'username/available': 'application/json',
+	'users': 'application/json',
+	'users/clips': 'application/json',
+	'users/followers': 'application/json',
+	'users/following': 'application/json',
+	'users/gallery/posts': 'application/json',
+	'users/get-frequently-replied-users': 'application/json',
+	'users/featured-notes': 'application/json',
+	'users/lists/create': 'application/json',
+	'users/lists/delete': 'application/json',
+	'users/lists/list': 'application/json',
+	'users/lists/pull': 'application/json',
+	'users/lists/push': 'application/json',
+	'users/lists/show': 'application/json',
+	'users/lists/favorite': 'application/json',
+	'users/lists/unfavorite': 'application/json',
+	'users/lists/update': 'application/json',
+	'users/lists/create-from-public': 'application/json',
+	'users/lists/update-membership': 'application/json',
+	'users/lists/get-memberships': 'application/json',
+	'users/notes': 'application/json',
+	'users/pages': 'application/json',
+	'users/flashs': 'application/json',
+	'users/reactions': 'application/json',
+	'users/recommendation': 'application/json',
+	'users/relation': 'application/json',
+	'users/report-abuse': 'application/json',
+	'users/search-by-username-and-host': 'application/json',
+	'users/search': 'application/json',
+	'users/show': 'application/json',
+	'users/achievements': 'application/json',
+	'users/update-memo': 'application/json',
+	'fetch-rss': 'application/json',
+	'fetch-external-resources': 'application/json',
+	'retention': 'application/json',
+	'sponsors': 'application/json',
+	'bubble-game/register': 'application/json',
+	'bubble-game/ranking': 'application/json',
+	'reversi/cancel-match': 'application/json',
+	'reversi/games': 'application/json',
+	'reversi/match': 'application/json',
+	'reversi/invitations': 'application/json',
+	'reversi/show-game': 'application/json',
+	'reversi/surrender': 'application/json',
+	'reversi/verify': 'application/json',
+};
diff --git a/packages/misskey-js/src/autogen/entities.ts b/packages/misskey-js/src/autogen/entities.ts
index 356cafae34..0228ad47e6 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'];
@@ -107,6 +116,15 @@ 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 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'];
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 f2d86d2ca5..562c498190 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
@@ -742,6 +792,56 @@ 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'];
+  };
   '/announcements': {
     /**
      * announcements
@@ -4121,7 +4221,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;
       };
@@ -4342,7 +4443,7 @@ export type components = {
       id: string;
       /** Format: date-time */
       createdAt: string;
-      /** @example lenna.jpg */
+      /** @example 192.jpg */
       name: string;
       /** @example image/jpeg */
       type: string;
@@ -4641,6 +4742,7 @@ export type components = {
       maintainerName: string | null;
       maintainerEmail: string | null;
       isSilenced: boolean;
+      isMediaSilenced: boolean;
       /** Format: url */
       iconUrl: string | null;
       /** Format: url */
@@ -4714,6 +4816,8 @@ export type components = {
       title: string;
       summary: string;
       script: string;
+      /** @enum {string} */
+      visibility: 'private' | 'public';
       likedCount: number | null;
       isLiked?: boolean;
     };
@@ -4831,6 +4935,7 @@ export type components = {
       canHideAds: boolean;
       driveCapacityMb: number;
       alwaysMarkNsfw: boolean;
+      canUpdateBioMedia: boolean;
       pinLimit: number;
       antennaLimit: number;
       wordMuteLimit: number;
@@ -4986,6 +5091,11 @@ export type components = {
       serverRules: string[];
       themeColor: string | null;
       policies: components['schemas']['RolePolicies'];
+      /**
+       * @default local
+       * @enum {string}
+       */
+      noteSearchableScope: 'local' | 'global';
     };
     MetaDetailedOnly: {
       features?: {
@@ -5008,6 +5118,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')[];
+      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;
@@ -5061,6 +5197,7 @@ export type operations = {
             enableServiceWorker: boolean;
             translatorAvailable: boolean;
             silencedHosts?: string[];
+            mediaSilencedHosts: string[];
             pinnedUsers: string[];
             hiddenTags: string[];
             blockedHosts: string[];
@@ -5281,6 +5418,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.
@@ -5625,15 +6048,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;
         };
       };
     };
@@ -5833,6 +6256,11 @@ export type operations = {
           untilId?: string;
           /** Format: misskey:id */
           userId?: string | null;
+          /**
+           * @default active
+           * @enum {string}
+           */
+          status?: 'all' | 'active' | 'archived';
         };
       };
     };
@@ -6543,7 +6971,7 @@ export type operations = {
              * @example 15eca7fba0480996e2245f5185bf39f2
              */
             md5: string;
-            /** @example lenna.jpg */
+            /** @example 192.jpg */
             name: string;
             /** @example image/jpeg */
             type: string;
@@ -9374,6 +9802,7 @@ export type operations = {
           perUserListTimelineCacheMax?: number;
           notesPerOneAd?: number;
           silencedHosts?: string[] | null;
+          mediaSilencedHosts?: string[] | null;
           /** @description [Deprecated] Use "urlPreviewSummaryProxyUrl" instead. */
           summalyProxy?: string | null;
           urlPreviewEnabled?: boolean;
@@ -9759,21 +10188,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>;
         };
       };
     };
@@ -10042,6 +10471,287 @@ 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')[];
+          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')[];
+        };
+      };
+    };
+    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')[];
+          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'];
+        };
+      };
+    };
+  };
   /**
    * announcements
    * @description No description provided.
@@ -13141,7 +13851,7 @@ export type operations = {
         'application/json': {
           /** Format: misskey:id */
           clipId: string;
-          name: string;
+          name?: string;
           isPublic?: boolean;
           description?: string | null;
         };
@@ -13409,6 +14119,8 @@ export type operations = {
           type?: string | null;
           /** @enum {string|null} */
           sort?: '+createdAt' | '-createdAt' | '+name' | '-name' | '+size' | '-size' | null;
+          /** @default */
+          searchQuery?: string;
         };
       };
     };
@@ -13591,7 +14303,7 @@ export type operations = {
            * Format: binary
            * @description The file contents.
            */
-          file: string;
+          file: Blob;
         };
       };
     };
@@ -14008,6 +14720,8 @@ export type operations = {
            * @default null
            */
           folderId?: string | null;
+          /** @default */
+          searchQuery?: string;
         };
       };
     };
@@ -15990,9 +16704,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;
         };
@@ -16590,6 +17304,12 @@ export type operations = {
           'application/json': components['schemas']['Error'];
         };
       };
+      /** @description To many requests */
+      429: {
+        content: {
+          'application/json': components['schemas']['Error'];
+        };
+      };
       /** @description Internal server error */
       500: {
         content: {
@@ -16731,6 +17451,12 @@ export type operations = {
           'application/json': components['schemas']['Error'];
         };
       };
+      /** @description To many requests */
+      429: {
+        content: {
+          'application/json': components['schemas']['Error'];
+        };
+      };
       /** @description Internal server error */
       500: {
         content: {
@@ -16792,6 +17518,12 @@ export type operations = {
           'application/json': components['schemas']['Error'];
         };
       };
+      /** @description To many requests */
+      429: {
+        content: {
+          'application/json': components['schemas']['Error'];
+        };
+      };
       /** @description Internal server error */
       500: {
         content: {
@@ -16899,6 +17631,12 @@ export type operations = {
           'application/json': components['schemas']['Error'];
         };
       };
+      /** @description To many requests */
+      429: {
+        content: {
+          'application/json': components['schemas']['Error'];
+        };
+      };
       /** @description Internal server error */
       500: {
         content: {
@@ -16952,6 +17690,12 @@ export type operations = {
           'application/json': components['schemas']['Error'];
         };
       };
+      /** @description To many requests */
+      429: {
+        content: {
+          'application/json': components['schemas']['Error'];
+        };
+      };
       /** @description Internal server error */
       500: {
         content: {
@@ -17191,6 +17935,12 @@ export type operations = {
           'application/json': components['schemas']['Error'];
         };
       };
+      /** @description To many requests */
+      429: {
+        content: {
+          'application/json': components['schemas']['Error'];
+        };
+      };
       /** @description Internal server error */
       500: {
         content: {
@@ -17244,6 +17994,12 @@ export type operations = {
           'application/json': components['schemas']['Error'];
         };
       };
+      /** @description To many requests */
+      429: {
+        content: {
+          'application/json': components['schemas']['Error'];
+        };
+      };
       /** @description Internal server error */
       500: {
         content: {
@@ -18705,6 +19461,12 @@ export type operations = {
           'application/json': components['schemas']['Error'];
         };
       };
+      /** @description To many requests */
+      429: {
+        content: {
+          'application/json': components['schemas']['Error'];
+        };
+      };
       /** @description Internal server error */
       500: {
         content: {
@@ -19942,12 +20704,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' | 'edited')[];
-          active: boolean;
+          name?: string;
+          url?: string;
+          secret?: string | null;
+          on?: ('mention' | 'unfollow' | 'follow' | 'followed' | 'note' | 'reply' | 'renote' | 'reaction' | 'edited')[];
+          active?: boolean;
         };
       };
     };
@@ -23604,16 +24365,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} */
diff --git a/packages/misskey-js/src/consts.ts b/packages/misskey-js/src/consts.ts
index 518fc75ec6..890b43639d 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', 'edited'] as const;
 
 export const noteVisibilities = ['public', 'home', 'followers', 'specified'] as const;
@@ -139,12 +157,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;
@@ -170,16 +218,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;
@@ -198,16 +246,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>;
@@ -222,39 +270,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;
@@ -292,37 +340,37 @@ export type ModerationLogPayloads = {
 	};
 	resolveAbuseReport: {
 		reportId: string;
-		report: any;
+		report: ReceivedAbuseReport;
 		forwarded: boolean;
 	};
 	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;
@@ -336,4 +384,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 5a19bf7446..6a405f81a5 100644
--- a/packages/misskey-js/src/entities.ts
+++ b/packages/misskey-js/src/entities.ts
@@ -1,5 +1,15 @@
 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';
 
 export * from './autogen/entities.js';
 export * from './autogen/models.js';
@@ -7,9 +17,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;
@@ -137,6 +161,36 @@ export type ModerationLog = {
 } | {
 	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 = {
@@ -224,3 +278,7 @@ export type SigninResponse = {
 	id: User['id'],
 	i: string,
 };
+
+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..ace9738e6a 100644
--- a/packages/misskey-js/src/index.ts
+++ b/packages/misskey-js/src/index.ts
@@ -22,6 +22,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 +30,5 @@ 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';
+export { api, entities, acct, note };
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/misskey-js/src/streaming.ts b/packages/misskey-js/src/streaming.ts
index 0f26857782..d1d131cfc1 100644
--- a/packages/misskey-js/src/streaming.ts
+++ b/packages/misskey-js/src/streaming.ts
@@ -15,7 +15,7 @@ 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 = {
 	_connected_: void;
@@ -25,6 +25,7 @@ type StreamEvents = {
 /**
  * Misskey stream connection
  */
+// eslint-disable-next-line import/no-default-export
 export default class Stream extends EventEmitter<StreamEvents> {
 	private stream: _ReconnectingWebsocket.default;
 	public state: 'initializing' | 'reconnecting' | 'connected' = 'initializing';
@@ -34,7 +35,7 @@ export default class Stream extends EventEmitter<StreamEvents> {
 	private idCounter = 0;
 
 	constructor(origin: string, user: { token: string; } | null, options?: {
-		WebSocket?: any;
+		WebSocket?: WebSocket;
 	}) {
 		super();
 
@@ -51,6 +52,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 +93,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 +108,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 +176,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 +213,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 +277,7 @@ class Pool {
 	}
 }
 
-export abstract class Connection<Channel extends AnyOf<Channels> = any> extends EventEmitter<Channel['events']> {
+export abstract class Connection<Channel extends AnyOf<Channels> = AnyOf<Channels>> extends EventEmitter<Channel['events']> {
 	public channel: string;
 	protected stream: Stream;
 	public abstract id: string;
@@ -291,7 +293,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 +311,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 +330,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 +361,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 912ad56f63..7455bdcd7f 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;
@@ -224,8 +233,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: {
@@ -234,10 +243,7 @@ export type Channels = {
 			};
 			ready: boolean;
 			cancel: null | Record<string, never>;
-			updateSettings: {
-				key: string;
-				value: any;
-			};
+			updateSettings: ReversiUpdateSettings<ReversiUpdateKey>;
 			claimTimeIsUp: null | Record<string, never>;
 		}
 	}
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..e626c97a59 100644
--- a/packages/misskey-reversi/build.js
+++ b/packages/misskey-reversi/build.js
@@ -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..3f81df7145
--- /dev/null
+++ b/packages/misskey-reversi/eslint.config.js
@@ -0,0 +1,23 @@
+import tsParser from '@typescript-eslint/parser';
+import sharedConfig from '../shared/eslint.config.js';
+
+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/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..e9d27c4a72
--- /dev/null
+++ b/packages/shared/eslint.config.js
@@ -0,0 +1,28 @@
+import globals from 'globals';
+import pluginMisskey from '@misskey-dev/eslint-plugin';
+
+export default [
+	...pluginMisskey.configs['recommended'],
+	{
+		files: ['**/*.cjs'],
+		languageOptions: {
+			parserOptions: {
+				sourceType: 'commonjs',
+			},
+		},
+	},
+	{
+		files: ['**/*.js', '**/*.jsx'],
+		languageOptions: {
+			parserOptions: {
+				sourceType: 'module',
+			},
+		},
+	},
+	{
+		files: ['build.js'],
+		languageOptions: {
+			globals: globals.node,
+		},
+	},
+];
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..9522d061e0 100644
--- a/packages/sw/build.js
+++ b/packages/sw/build.js
@@ -8,7 +8,7 @@
 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))
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..9174f50ae3 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.0",
 		"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"
+		"nodemon": "3.1.4",
+		"typescript": "5.5.4"
 	},
 	"type": "module"
 }
diff --git a/packages/sw/tsconfig.json b/packages/sw/tsconfig.json
index f3f3543013..50d4aae19d 100644
--- a/packages/sw/tsconfig.json
+++ b/packages/sw/tsconfig.json
@@ -2,7 +2,6 @@
 	"compilerOptions": {
 		"allowJs": true,
 		"noEmitOnError": false,
-		"noImplicitAny": false,
 		"noImplicitReturns": true,
 		"noUnusedParameters": false,
 		"noUnusedLocals": true,
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index a5ef08947c..d297feacf1 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.40)
       esbuild:
-        specifier: 0.20.2
-        version: 0.20.2
+        specifier: 0.23.0
+        version: 0.23.0
       execa:
         specifier: 8.0.1
         version: 8.0.1
@@ -25,69 +25,75 @@ 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.40
+        version: 8.4.40
       tar:
         specifier: 6.2.1
         version: 6.2.1
       terser:
-        specifier: 5.30.3
-        version: 5.30.3
+        specifier: 5.31.3
+        version: 5.31.3
       typescript:
-        specifier: 5.4.5
-        version: 5.4.5
+        specifier: 5.5.4
+        version: 5.5.4
     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.5.4))(eslint@9.8.0)(typescript@5.5.4))(@typescript-eslint/parser@7.17.0(eslint@9.8.0)(typescript@5.5.4))(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.17.0(eslint@9.8.0)(typescript@5.5.4))(eslint@9.8.0))(eslint@9.8.0)(globals@15.8.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.5.4))(eslint@9.8.0)(typescript@5.5.4)
       '@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.5.4)
       cross-env:
         specifier: 7.0.3
         version: 7.0.3
       cypress:
-        specifier: 13.7.3
-        version: 13.7.3
+        specifier: 13.13.1
+        version: 13.13.1
       eslint:
-        specifier: 8.57.0
-        version: 8.57.0
+        specifier: 9.8.0
+        version: 9.8.0
+      globals:
+        specifier: 15.8.0
+        version: 15.8.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.4
+        version: 2.0.4
 
   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: 5.21.1
+        version: 5.21.1(@bull-board/ui@5.21.1)
       '@bull-board/fastify':
-        specifier: 5.17.0
-        version: 5.17.0
+        specifier: 5.21.1
+        version: 5.21.1
       '@bull-board/ui':
-        specifier: 5.17.0
-        version: 5.17.0
+        specifier: 5.21.1
+        version: 5.21.1
       '@discordapp/twemoji':
         specifier: 15.0.3
         version: 15.0.3
@@ -107,11 +113,11 @@ importers:
         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
+        specifier: 8.3.0
+        version: 8.3.0
       '@fastify/static':
-        specifier: 7.0.3
-        version: 7.0.3
+        specifier: 7.0.4
+        version: 7.0.4
       '@fastify/view':
         specifier: 9.1.0
         version: 9.1.0
@@ -122,29 +128,29 @@ importers:
         specifier: 5.1.0
         version: 5.1.0
       '@napi-rs/canvas':
-        specifier: ^0.1.52
-        version: 0.1.52
+        specifier: ^0.1.53
+        version: 0.1.53
       '@nestjs/common':
-        specifier: 10.3.8
-        version: 10.3.8(reflect-metadata@0.2.2)(rxjs@7.8.1)
+        specifier: 10.3.10
+        version: 10.3.10(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.3.10
+        version: 10.3.10(@nestjs/common@10.3.10(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.3.10)(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.3.10
+        version: 10.3.10(@nestjs/common@10.3.10(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.3.10(@nestjs/common@10.3.10(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.3.10)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.3.10(@nestjs/common@10.3.10(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.3.10))
       '@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
@@ -153,10 +159,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
       '@transfem-org/sfm-js':
         specifier: 0.24.5
         version: 0.24.5
@@ -167,8 +173,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
@@ -188,8 +194,8 @@ importers:
         specifier: 1.20.2
         version: 1.20.2
       bullmq:
-        specifier: 5.7.8
-        version: 5.7.8
+        specifier: 5.10.4
+        version: 5.10.4
       cacheable-lookup:
         specifier: 7.0.0
         version: 7.0.0
@@ -220,9 +226,12 @@ importers:
       deep-email-validator:
         specifier: 0.1.21
         version: 0.1.21
+      fast-xml-parser:
+        specifier: ^4.4.0
+        version: 4.4.0
       fastify:
-        specifier: 4.26.2
-        version: 4.26.2
+        specifier: 4.28.1
+        version: 4.28.1
       fastify-multer:
         specifier: ^2.0.3
         version: 2.0.3
@@ -233,11 +242,11 @@ importers:
         specifier: 4.2.2
         version: 4.2.2
       file-type:
-        specifier: 19.0.0
-        version: 19.0.0
+        specifier: 19.3.0
+        version: 19.3.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
@@ -245,11 +254,11 @@ importers:
         specifier: 10.3.10
         version: 10.3.10
       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.6.1
+        version: 15.6.1
       hpagent:
         specifier: 1.2.0
         version: 1.2.0
@@ -263,26 +272,26 @@ importers:
         specifier: 5.4.1
         version: 5.4.1
       ip-cidr:
-        specifier: 3.1.0
-        version: 3.1.0
+        specifier: 4.0.1
+        version: 4.0.1
       ipaddr.js:
         specifier: 2.2.0
         version: 2.2.0
       is-svg:
-        specifier: 5.0.0
-        version: 5.0.0
+        specifier: 5.0.1
+        version: 5.0.1
       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
@@ -290,8 +299,8 @@ importers:
         specifier: workspace:*
         version: link:../megalodon
       meilisearch:
-        specifier: 0.38.0
-        version: 0.38.0(encoding@0.1.13)
+        specifier: 0.41.0
+        version: 0.41.0(encoding@0.1.13)
       microformats-parser:
         specifier: 2.0.2
         version: 2.0.2
@@ -317,8 +326,8 @@ importers:
         specifier: 3.3.2
         version: 3.3.2
       nodemailer:
-        specifier: 6.9.13
-        version: 6.9.13
+        specifier: 6.9.14
+        version: 6.9.14
       oauth:
         specifier: 0.10.0
         version: 0.10.0
@@ -332,14 +341,14 @@ importers:
         specifier: 0.0.14
         version: 0.0.14
       otpauth:
-        specifier: 9.2.3
-        version: 9.2.3
+        specifier: 9.3.1
+        version: 9.3.1
       parse5:
         specifier: 7.1.2
         version: 7.1.2
       pg:
-        specifier: 8.11.5
-        version: 8.11.5
+        specifier: 8.12.0
+        version: 8.12.0
       pkce-challenge:
         specifier: 4.1.0
         version: 4.1.0
@@ -349,9 +358,12 @@ importers:
       promise-limit:
         specifier: 2.7.0
         version: 2.7.0
+      proxy-addr:
+        specifier: ^2.0.7
+        version: 2.0.7
       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
@@ -365,8 +377,8 @@ importers:
         specifier: 3.4.1
         version: 3.4.1
       re2:
-        specifier: 1.20.10
-        version: 1.20.10
+        specifier: 1.21.3
+        version: 1.21.3
       redis-lock:
         specifier: 0.1.4
         version: 0.1.4
@@ -389,8 +401,8 @@ importers:
         specifier: 2.7.0
         version: 2.7.0
       sharp:
-        specifier: 0.33.3
-        version: 0.33.3
+        specifier: 0.33.4
+        version: 0.33.4
       slacc:
         specifier: 0.0.10
         version: 0.0.10
@@ -401,8 +413,8 @@ importers:
         specifier: 2.1.0
         version: 2.1.0
       systeminformation:
-        specifier: 5.22.7
-        version: 5.22.7
+        specifier: 5.22.11
+        version: 5.22.11
       tinycolor2:
         specifier: 1.6.0
         version: 1.6.0
@@ -410,17 +422,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.12.0)
       typescript:
-        specifier: 5.4.5
-        version: 5.4.5
+        specifier: 5.5.4
+        version: 5.5.4
       ulid:
         specifier: 2.3.0
         version: 2.3.0
@@ -434,8 +446,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
@@ -525,18 +537,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.3.10
+        version: 10.3.10(@nestjs/common@10.3.10(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.3.10)
       '@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
@@ -559,11 +568,11 @@ importers:
         specifier: 2.1.24
         version: 2.1.24
       '@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
@@ -571,11 +580,11 @@ importers:
         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
@@ -586,17 +595,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
       '@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
@@ -604,8 +610,11 @@ importers:
         specifier: 0.1.2
         version: 0.1.2
       '@types/pg':
-        specifier: 8.11.5
-        version: 8.11.5
+        specifier: 8.11.6
+        version: 8.11.6
+      '@types/proxy-addr':
+        specifier: ^2.0.3
+        version: 2.0.3
       '@types/pug':
         specifier: 2.0.10
         version: 2.0.10
@@ -652,47 +661,44 @@ importers:
         specifier: 3.6.3
         version: 3.6.3
       '@types/ws':
-        specifier: 8.5.10
-        version: 8.5.10
+        specifier: 8.5.11
+        version: 8.5.11
       '@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.5.4))(eslint@9.8.0)(typescript@5.5.4)
       '@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.5.4)
       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)
+        version: 2.29.1(@typescript-eslint/parser@7.17.0(eslint@9.8.0)(typescript@5.5.4))(eslint@9.8.0)
       execa:
-        specifier: 8.0.1
-        version: 8.0.1
+        specifier: 9.3.0
+        version: 9.3.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.4
+        version: 3.1.4
       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:
@@ -713,16 +719,16 @@ importers:
         version: 2.1.1
       '@rollup/plugin-json':
         specifier: 6.1.0
-        version: 6.1.0(rollup@4.17.2)
+        version: 6.1.0(rollup@4.19.1)
       '@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.19.1)
       '@rollup/pluginutils':
         specifier: 5.1.0
-        version: 5.1.0(rollup@4.17.2)
+        version: 5.1.0(rollup@4.19.1)
       '@syuilo/aiscript':
-        specifier: 0.18.0
-        version: 0.18.0
+        specifier: 0.19.0
+        version: 0.19.0
       '@transfem-org/sfm-js':
         specifier: 0.24.5
         version: 0.24.5
@@ -730,14 +736,14 @@ 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.0
+        version: 5.1.0(vite@5.3.5(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3))(vue@3.4.37(typescript@5.5.4))
       '@vue/compiler-sfc':
-        specifier: 3.4.26
-        version: 3.4.26
+        specifier: 3.4.37
+        version: 3.4.37
       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
@@ -751,29 +757,29 @@ importers:
         specifier: 1.9.3
         version: 1.9.3
       chart.js:
-        specifier: 4.4.2
-        version: 4.4.2
+        specifier: 4.4.3
+        version: 4.4.3
       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.3)(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.3)
       chartjs-plugin-gradient:
         specifier: 0.6.1
-        version: 0.6.1(chart.js@4.4.2)
+        version: 0.6.1(chart.js@4.4.3)
       chartjs-plugin-zoom:
         specifier: 2.0.1
-        version: 2.0.1(chart.js@4.4.2)
+        version: 2.0.1(chart.js@4.4.3)
       chromatic:
-        specifier: 11.3.0
-        version: 11.3.0
+        specifier: 11.5.6
+        version: 11.5.6
       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.1
+        version: 2.0.0-rc.1
       date-fns:
         specifier: 2.30.0
         version: 2.30.0
@@ -814,23 +820,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.19.1
+        version: 4.19.1
       sanitize-html:
         specifier: 2.13.0
         version: 2.13.0
       sass:
-        specifier: 1.76.0
-        version: 1.76.0
+        specifier: 1.77.8
+        version: 1.77.8
       shiki:
-        specifier: 1.4.0
-        version: 1.4.0
+        specifier: 1.12.0
+        version: 1.12.0
       strict-event-emitter-types:
         specifier: 2.0.0
         version: 2.0.0
@@ -838,102 +844,99 @@ importers:
         specifier: 3.1.0
         version: 3.1.0
       three:
-        specifier: 0.164.1
-        version: 0.164.1
+        specifier: 0.167.0
+        version: 0.167.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.5.4
+        version: 5.5.4
       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.12.0
+        version: 1.12.0(vue@3.4.37(typescript@5.5.4))
       vite:
-        specifier: 5.2.11
-        version: 5.2.11(@types/node@20.12.7)(sass@1.76.0)(terser@5.30.3)
+        specifier: 5.3.5
+        version: 5.3.5(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3)
       vue:
-        specifier: 3.4.26
-        version: 3.4.26(typescript@5.4.5)
+        specifier: 3.4.37
+        version: 3.4.37(typescript@5.5.4)
       vuedraggable:
         specifier: next
-        version: 4.1.0(vue@3.4.26(typescript@5.4.5))
+        version: 4.1.0(vue@3.4.37(typescript@5.5.4))
     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.2.6
+        version: 8.2.6(storybook@8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(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.2.6
+        version: 8.2.6(storybook@8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(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.2.6
+        version: 8.2.6(@jest/globals@29.7.0)(@types/jest@29.5.12)(jest@29.7.0(@types/node@20.14.12))(storybook@8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4))(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.77.8)(terser@5.31.3))
       '@storybook/addon-links':
-        specifier: 8.0.9
-        version: 8.0.9(react@18.3.1)
+        specifier: 8.2.6
+        version: 8.2.6(react@18.3.1)(storybook@8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4))
       '@storybook/addon-mdx-gfm':
-        specifier: 8.0.9
-        version: 8.0.9
+        specifier: 8.2.6
+        version: 8.2.6(storybook@8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4))
       '@storybook/addon-storysource':
-        specifier: 8.0.9
-        version: 8.0.9
+        specifier: 8.2.6
+        version: 8.2.6(storybook@8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(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.2.6
+        version: 8.2.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(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.2.6
+        version: 8.2.6(storybook@8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4))
       '@storybook/core-events':
-        specifier: 8.0.9
-        version: 8.0.9
+        specifier: 8.2.6
+        version: 8.2.6(storybook@8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(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.2.6
+        version: 8.2.6(storybook@8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4))
       '@storybook/preview-api':
-        specifier: 8.0.9
-        version: 8.0.9
+        specifier: 8.2.6
+        version: 8.2.6(storybook@8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(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.2.6
+        version: 8.2.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4))(typescript@5.5.4)
       '@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.2.6
+        version: 8.2.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@4.19.1)(storybook@8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4))(typescript@5.5.4)(vite@5.3.5(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3))
       '@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.2.6
+        version: 8.2.6(@jest/globals@29.7.0)(@types/jest@29.5.12)(jest@29.7.0(@types/node@20.14.12))(storybook@8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4))(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.77.8)(terser@5.31.3))
       '@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.2.6
+        version: 8.2.6(storybook@8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4))
       '@storybook/types':
-        specifier: 8.0.9
-        version: 8.0.9
+        specifier: 8.2.6
+        version: 8.2.6(storybook@8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(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.2.6
+        version: 8.2.6(storybook@8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4))(vue@3.4.37(typescript@5.5.4))
       '@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.1.11
+        version: 8.1.11(bufferutil@4.0.8)(encoding@0.1.13)(prettier@3.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(utf-8-validate@6.0.4)(vite@5.3.5(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3))(vue@3.4.37(typescript@5.5.4))
       '@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))
+        specifier: 8.1.0
+        version: 8.1.0(@vue/compiler-sfc@3.4.37)(@vue/server-renderer@3.4.37(vue@3.4.37(typescript@5.5.4)))(vue@3.4.37(typescript@5.5.4))
       '@types/escape-regexp':
         specifier: 0.0.3
         version: 0.0.3
@@ -941,20 +944,23 @@ importers:
         specifier: 1.0.5
         version: 1.0.5
       '@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
+      '@types/seedrandom':
+        specifier: 3.0.8
+        version: 3.0.8
       '@types/throttle-debounce':
         specifier: 5.0.2
         version: 5.0.2
@@ -962,41 +968,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.11
+        version: 8.5.11
       '@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.5.4))(eslint@9.8.0)(typescript@5.5.4)
       '@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.5.4)
       '@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.77.8)(terser@5.31.3))
       '@vue/runtime-core':
-        specifier: 3.4.26
-        version: 3.4.26
+        specifier: 3.4.37
+        version: 3.4.37
       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.13.1
+        version: 13.13.1
       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)
+        version: 2.29.1(@typescript-eslint/parser@7.17.0(eslint@9.8.0)(typescript@5.5.4))(eslint@9.8.0)
       eslint-plugin-vue:
-        specifier: 9.25.0
-        version: 9.25.0(eslint@8.57.0)
+        specifier: 9.27.0
+        version: 9.27.0(eslint@9.8.0)
       fast-glob:
         specifier: 3.3.2
         version: 3.3.2
@@ -1007,53 +1010,56 @@ importers:
         specifier: 0.12.2
         version: 0.12.2
       micromatch:
-        specifier: 4.0.5
-        version: 4.0.5
+        specifier: 4.0.7
+        version: 4.0.7
       msw:
-        specifier: 2.2.14
-        version: 2.2.14(typescript@5.4.5)
+        specifier: 2.3.4
+        version: 2.3.4(typescript@5.5.4)
       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.3.4(typescript@5.5.4))
       nodemon:
-        specifier: 3.1.0
-        version: 3.1.0
+        specifier: 3.1.4
+        version: 3.1.4
       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.4
+        version: 2.0.4
       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.2.6
+        version: 8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(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(3rvqj7p7l43ansgshs3zbslm7u)
       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.77.8)(terser@5.31.3)
       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.77.8)(terser@5.31.3))
       vue-component-type-helpers:
-        specifier: 2.0.16
-        version: 2.0.16
+        specifier: 2.0.29
+        version: 2.0.29
       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.8.0)
       vue-tsc:
-        specifier: 2.0.16
-        version: 2.0.16(typescript@5.4.5)
+        specifier: 2.0.29
+        version: 2.0.29(typescript@5.5.4)
 
   packages/megalodon:
     dependencies:
@@ -1068,7 +1074,7 @@ importers:
         version: 29.5.12
       '@types/oauth':
         specifier: ^0.9.4
-        version: 0.9.4
+        version: 0.9.5
       '@types/object-assign-deep':
         specifier: ^0.4.3
         version: 0.4.3
@@ -1080,7 +1086,7 @@ importers:
         version: 9.0.8
       '@types/ws':
         specifier: ^8.5.10
-        version: 8.5.10
+        version: 8.5.11
       axios:
         specifier: 1.6.0
         version: 1.6.0
@@ -1113,7 +1119,7 @@ importers:
         version: 9.0.1
       ws:
         specifier: 8.14.2
-        version: 8.14.2(bufferutil@4.0.7)(utf-8-validate@6.0.3)
+        version: 8.14.2(bufferutil@4.0.8)(utf-8-validate@6.0.4)
     devDependencies:
       '@typescript-eslint/eslint-plugin':
         specifier: ^6.12.0
@@ -1129,7 +1135,7 @@ importers:
         version: 9.1.0(eslint@8.57.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-worker:
         specifier: ^29.7.0
         version: 29.7.0
@@ -1138,10 +1144,10 @@ importers:
         version: 4.17.21
       prettier:
         specifier: ^3.1.0
-        version: 3.2.5
+        version: 3.3.3
       ts-jest:
         specifier: ^29.1.1
-        version: 29.1.2(@babel/core@7.23.5)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.5))(esbuild@0.20.2)(jest@29.7.0(@types/node@20.12.7))(typescript@5.1.6)
+        version: 29.1.2(@babel/core@7.24.7)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.24.7))(esbuild@0.23.0)(jest@29.7.0(@types/node@20.14.12))(typescript@5.1.6)
       typedoc:
         specifier: ^0.25.3
         version: 0.25.13(typescript@5.1.6)
@@ -1158,9 +1164,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
@@ -1172,16 +1175,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.8.0)(typescript@5.3.3))(eslint@9.8.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.8.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
@@ -1205,41 +1205,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.4
+        version: 7.47.4(@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
       '@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.5.4))(eslint@9.8.0)(typescript@5.5.4)
       '@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.5.4)
       esbuild:
-        specifier: 0.19.11
-        version: 0.19.11
-      eslint:
-        specifier: 8.57.0
-        version: 8.57.0
+        specifier: 0.23.0
+        version: 0.23.0
       execa:
-        specifier: 8.0.1
-        version: 8.0.1
+        specifier: 9.3.0
+        version: 9.3.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)
@@ -1253,20 +1247,17 @@ importers:
         specifier: 2.0.0
         version: 2.0.0
       nodemon:
-        specifier: 3.1.0
-        version: 3.1.0
+        specifier: 3.1.4
+        version: 3.1.4
       tsd:
-        specifier: 0.30.7
-        version: 0.30.7
+        specifier: 0.31.1
+        version: 0.31.1
       typescript:
-        specifier: 5.4.5
-        version: 5.4.5
+        specifier: 5.5.4
+        version: 5.5.4
 
   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)
@@ -1275,13 +1266,10 @@ importers:
         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)
+        version: 6.11.0(@typescript-eslint/parser@6.11.0(eslint@9.8.0)(typescript@5.3.3))(eslint@9.8.0)(typescript@5.3.3)
       '@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
+        version: 6.11.0(eslint@9.8.0)(typescript@5.3.3)
       openapi-types:
         specifier: 12.1.3
         version: 12.1.3
@@ -1304,24 +1292,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.8.0)(typescript@5.3.3))(eslint@9.8.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.8.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
@@ -1338,8 +1320,8 @@ importers:
   packages/sw:
     dependencies:
       esbuild:
-        specifier: 0.20.2
-        version: 0.20.2
+        specifier: 0.23.0
+        version: 0.23.0
       idb-keyval:
         specifier: 6.2.1
         version: 6.2.1
@@ -1347,34 +1329,24 @@ 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.8.0)(typescript@5.5.4)
       '@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)
+        version: 2.29.1(@typescript-eslint/parser@7.17.0(eslint@9.8.0)(typescript@5.5.4))(eslint@9.8.0)
       nodemon:
-        specifier: 3.1.0
-        version: 3.1.0
+        specifier: 3.1.4
+        version: 3.1.4
       typescript:
-        specifier: 5.4.5
-        version: 5.4.5
+        specifier: 5.5.4
+        version: 5.5.4
 
 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==}
 
@@ -1398,221 +1370,237 @@ packages:
     resolution: {integrity: sha512-Xk1sIhyNC/esHGGVjL/niHLowM0csl/kFO5uawBy4IrWwy0o1G8LGt3jP6nmWGz+USxeeqbihAmp/oVZju6wug==}
     hasBin: true
 
-  '@aws-crypto/crc32@3.0.0':
-    resolution: {integrity: sha512-IzSgsrxUcsrejQbPVilIKy16kAT52EwB6zSaI+M3xxIhKh5+aldEyvI+z6erM7TCLB2BJsFrtHjp6/4/sr+3dA==}
+  '@aws-crypto/crc32@5.2.0':
+    resolution: {integrity: sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==}
+    engines: {node: '>=16.0.0'}
 
-  '@aws-crypto/crc32c@3.0.0':
-    resolution: {integrity: sha512-ENNPPManmnVJ4BTXlOjAgD7URidbAznURqD0KvfREyc4o20DPYdEldU1f5cQ7Jbj0CJJSPaMIk/9ZshdB3210w==}
+  '@aws-crypto/crc32c@5.2.0':
+    resolution: {integrity: sha512-+iWb8qaHLYKrNvGRbiYRHSdKRWhto5XlZUEBwDjYNf+ly5SVYG6zEoYIdxvf5R3zyeP16w4PLBn3rH1xc74Rag==}
 
-  '@aws-crypto/ie11-detection@3.0.0':
-    resolution: {integrity: sha512-341lBBkiY1DfDNKai/wXM3aujNBkXR7tq1URPQDL9wi3AUbI80NR74uF1TXHMm7po1AcnFk8iu2S2IeU/+/A+Q==}
+  '@aws-crypto/sha1-browser@5.2.0':
+    resolution: {integrity: sha512-OH6lveCFfcDjX4dbAvCFSYUjJZjDr/3XJ3xHtjn3Oj5b9RjojQo8npoLeA/bNwkOkrSQ0wgrHzXk4tDRxGKJeg==}
 
-  '@aws-crypto/sha1-browser@3.0.0':
-    resolution: {integrity: sha512-NJth5c997GLHs6nOYTzFKTbYdMNA6/1XlKVgnZoaZcQ7z7UJlOgj2JdbHE8tiYLS3fzXNCguct77SPGat2raSw==}
+  '@aws-crypto/sha256-browser@5.2.0':
+    resolution: {integrity: sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==}
 
-  '@aws-crypto/sha256-browser@3.0.0':
-    resolution: {integrity: sha512-8VLmW2B+gjFbU5uMeqtQM6Nj0/F1bro80xQXCW6CQBWgosFWXTx77aeOF5CAIAmbOK64SdMBJdNr6J41yP5mvQ==}
+  '@aws-crypto/sha256-js@5.2.0':
+    resolution: {integrity: sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==}
+    engines: {node: '>=16.0.0'}
 
-  '@aws-crypto/sha256-js@3.0.0':
-    resolution: {integrity: sha512-PnNN7os0+yd1XvXAy23CFOmTbMaDxgxXtTKHybrJ39Y8kGzBATgBFibWJKH6BhytLI/Zyszs87xCOBNyBig6vQ==}
+  '@aws-crypto/supports-web-crypto@5.2.0':
+    resolution: {integrity: sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==}
 
-  '@aws-crypto/supports-web-crypto@3.0.0':
-    resolution: {integrity: sha512-06hBdMwUAb2WFTuGG73LSC0wfPu93xWwo5vL2et9eymgmu3Id5vFAHBbajVWiGhPO37qcsdCap/FqXvJGJWPIg==}
+  '@aws-crypto/util@5.2.0':
+    resolution: {integrity: sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==}
 
-  '@aws-crypto/util@3.0.0':
-    resolution: {integrity: sha512-2OJlpeJpCR48CC8r+uKVChzs9Iungj9wkZrl8Z041DWEWvyIHILYKCPNzJghKsivj+S3mLo6BVc7mBNzdxA46w==}
+  '@aws-sdk/client-s3@3.620.0':
+    resolution: {integrity: sha512-kf3Lqvuq/ciUn4myQjd1a9nhVg95+FEWkIq7pdkgxFoKow8HKj3nuiwI7zYBRTfk0RKXRkJca3GE+3RXpeZSiA==}
+    engines: {node: '>=16.0.0'}
 
-  '@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==}
+  '@babel/generator@7.24.7':
+    resolution: {integrity: sha512-oipXieGC3i45Y1A41t4tAqpnEZWgB/lC6Ehh6+rOviR5XWpTtMmLN+fGjz9vOiNRt0p6RtO6DtD0pdU3vpqdSA==}
     engines: {node: '>=6.9.0'}
 
-  '@babel/generator@7.23.6':
-    resolution: {integrity: sha512-qrSfCYxYQB5owCmGLbl8XRpX1ytXlpueOb0N0UmQwA073KZxejgQTzAmJezxvpwQD9uGtK2shHdi55QT+MbjIw==}
+  '@babel/helper-annotate-as-pure@7.24.7':
+    resolution: {integrity: sha512-BaDeOonYvhdKw+JoMVkAixAAJzG2jVPIwWoKBPdYuY9b452e2rPuI9QPYh3KpofZ3pW2akOmwZLOiOsHMiqRAg==}
     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/helper-builder-binary-assignment-operator-visitor@7.24.7':
+    resolution: {integrity: sha512-xZeCVVdwb4MsDBkkyZ64tReWYrLRHlMN72vP7Bdm3OUOuyFZExhsHUUnuWnm2/XOlAJzR0LfPpB56WXZn0X/lA==}
     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==}
+  '@babel/helper-create-class-features-plugin@7.24.7':
+    resolution: {integrity: sha512-kTkaDl7c9vO80zeX1rJxnuRpEsD5tA81yh11X1gQo+PhSti3JS+7qeZo9U4RHobKRiFPKaGK3svUAeb8D0Q7eg==}
     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==}
+  '@babel/helper-create-regexp-features-plugin@7.24.7':
+    resolution: {integrity: sha512-03TCmXy2FtXJEZfbXDTSqq1fRJArk7lX9DOFC/47VthYcxyIOx+eXQmdo6DOQvrbpIix+KfXwvuXdFDZHxt+rA==}
     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==}
+  '@babel/helper-define-polyfill-provider@0.6.2':
+    resolution: {integrity: sha512-LV76g+C502biUK6AyZ3LK10vDpDyCzZnhZFXkH1L75zHPj68+qc8Zfpx2th+gzwA2MzyK+1g/3EPl62yFnVttQ==}
     peerDependencies:
       '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0
 
@@ -1620,44 +1608,70 @@ packages:
     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-member-expression-to-functions@7.24.7':
+    resolution: {integrity: sha512-LGeMaf5JN4hAT471eJdBs/GK1DoYIJ5GCtZN/EsL6KUiiDZOvO/eKE11AMZJa2zP4zk4qe9V2O/hxAmkRc8p6w==}
     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-optimise-call-expression@7.24.7':
+    resolution: {integrity: sha512-jKiTsW2xmWwxT1ixIdfXUZp+P5yURx2suzLZr5Hi64rURpDYdMW0pv+Uf17EYk2Rd428Lx4tLsnjGJzYKDM/6A==}
     engines: {node: '>=6.9.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==}
+  '@babel/helper-plugin-utils@7.24.7':
+    resolution: {integrity: sha512-Rq76wjt7yz9AAc1KnlRKNAi/dMSVWgDRx43FHoJEbcYU6xOWaE2dVPwcdTukJrjxS65GITyfbvEYHvkirZ6uEg==}
+    engines: {node: '>=6.9.0'}
+
+  '@babel/helper-remap-async-to-generator@7.24.7':
+    resolution: {integrity: sha512-9pKLcTlZ92hNZMQfGCHImUpDOlAgkkpqalWEeftW5FBya75k8Li2ilerxkM/uBEj01iBZXcCIB/bwvDYgWyibA==}
     engines: {node: '>=6.9.0'}
     peerDependencies:
       '@babel/core': ^7.0.0
 
-  '@babel/helper-replace-supers@7.22.20':
-    resolution: {integrity: sha512-qsW0In3dbwQUbK8kejJ4R7IHVGwHJlV6lpG6UA7a9hSa2YEiAib+N1T2kr6PEeUT+Fl7najmSOS6SmAwCHK6Tw==}
+  '@babel/helper-replace-supers@7.24.7':
+    resolution: {integrity: sha512-qTAxxBM81VEyoAY0TtLrx1oAEJc09ZK67Q9ljQToqCnA+55eNwCORaxlKyu+rNfX86o8OXRUSNUnrtsAZXM9sg==}
     engines: {node: '>=6.9.0'}
     peerDependencies:
       '@babel/core': ^7.0.0
@@ -1666,71 +1680,83 @@ packages:
     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-skip-transparent-expression-wrappers@7.24.7':
+    resolution: {integrity: sha512-IO+DLT3LQUElMbpzlatRASEyQtfhSE0+m465v++3jyyXeBTBUjtVZg28/gHeV5mrTJqvEKhKroBGAvhW+qPHiQ==}
     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-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/helper-wrap-function@7.24.7':
+    resolution: {integrity: sha512-N9JIYk3TD+1vq/wn77YnJOqMtfWhNewNE+DJV4puD2X7Ew9J4JvrzrFDfTfyv5EgEXVy9/Wt8QiOErzEmv5Ifw==}
     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==}
-    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==}
+  '@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.24.7':
+    resolution: {integrity: sha512-TiT1ss81W80eQsN+722OaeQMY/G4yTb4G9JrqeiDADs3N8lbPMGldWi9x8tyqCW5NLx1Jh2AvkE6r6QvEltMMQ==}
     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==}
+  '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.24.7':
+    resolution: {integrity: sha512-unaQgZ/iRu/By6tsjMZzpeBZjChYfLYry6HrEXPoz3KmfF0sVBQ1l8zKMQ4xRGLWVsjuvB8nQfjNP/DcfEOCsg==}
+    engines: {node: '>=6.9.0'}
+    peerDependencies:
+      '@babel/core': ^7.0.0
+
+  '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.24.7':
+    resolution: {integrity: sha512-+izXIbke1T33mY4MSNnrqhPXDz01WYhEf3yF5NbnUtkiNnm+XBZJl3kNfoK6NKmYlz/D07+l2GWVK/QfDkNCuQ==}
     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==}
+  '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.24.7':
+    resolution: {integrity: sha512-utA4HuR6F4Vvcr+o4DnjL8fCOlgRFGbeeBEGNg3ZTrLFw6VWG5XmUrvcQ0FjIYMU2ST4XcR2Wsp7t9qOAPnxMg==}
     engines: {node: '>=6.9.0'}
     peerDependencies:
       '@babel/core': ^7.0.0
@@ -1778,14 +1804,14 @@ packages:
     peerDependencies:
       '@babel/core': ^7.0.0-0
 
-  '@babel/plugin-syntax-import-assertions@7.23.3':
-    resolution: {integrity: sha512-lPgDSU+SJLK3xmFDTV2ZRQAiM7UuUjGidwBywFavObCiZc1BeAAcMtHJKUya92hPHO+at63JJPLygilZard8jw==}
+  '@babel/plugin-syntax-import-assertions@7.24.7':
+    resolution: {integrity: sha512-Ec3NRUMoi8gskrkBe3fNmEQfxDvY8bgfQpz6jlk/41kX9eUjvpyqWU7PBP/pLAvMaSQjbMNKJmvX57jP+M6bPg==}
     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==}
+  '@babel/plugin-syntax-import-attributes@7.24.7':
+    resolution: {integrity: sha512-hbX+lKKeUMGihnK8nvKqmXBInriT3GVjzXKFriV3YC6APGxMbP8RZNFwy91+hocLXq90Mta+HshoB31802bb8A==}
     engines: {node: '>=6.9.0'}
     peerDependencies:
       '@babel/core': ^7.0.0-0
@@ -1860,92 +1886,92 @@ packages:
     peerDependencies:
       '@babel/core': ^7.0.0
 
-  '@babel/plugin-transform-arrow-functions@7.23.3':
-    resolution: {integrity: sha512-NzQcQrzaQPkaEwoTm4Mhyl8jI1huEL/WWIEvudjTCMJ9aBZNpsJbMASx7EQECtQQPS/DcnFpo0FIh3LvEO9cxQ==}
+  '@babel/plugin-transform-arrow-functions@7.24.7':
+    resolution: {integrity: sha512-Dt9LQs6iEY++gXUwY03DNFat5C2NbO48jj+j/bSAz6b3HgPs39qcPiYt77fDObIcFwj3/C2ICX9YMwGflUoSHQ==}
     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==}
+  '@babel/plugin-transform-async-generator-functions@7.24.7':
+    resolution: {integrity: sha512-o+iF77e3u7ZS4AoAuJvapz9Fm001PuD2V3Lp6OSE4FYQke+cSewYtnek+THqGRWyQloRCyvWL1OkyfNEl9vr/g==}
     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==}
+  '@babel/plugin-transform-async-to-generator@7.24.7':
+    resolution: {integrity: sha512-SQY01PcJfmQ+4Ash7NE+rpbLFbmqA2GPIgqzxfFTL4t1FKRq4zTms/7htKpoCUI9OcFYgzqfmCdH53s6/jn5fA==}
     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==}
+  '@babel/plugin-transform-block-scoped-functions@7.24.7':
+    resolution: {integrity: sha512-yO7RAz6EsVQDaBH18IDJcMB1HnrUn2FJ/Jslc/WtPPWcjhpUJXU/rjbwmluzp7v/ZzWcEhTMXELnnsz8djWDwQ==}
     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==}
+  '@babel/plugin-transform-block-scoping@7.24.7':
+    resolution: {integrity: sha512-Nd5CvgMbWc+oWzBsuaMcbwjJWAcp5qzrbg69SZdHSP7AMY0AbWFqFO0WTFCA1jxhMCwodRwvRec8k0QUbZk7RQ==}
     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==}
+  '@babel/plugin-transform-class-properties@7.24.7':
+    resolution: {integrity: sha512-vKbfawVYayKcSeSR5YYzzyXvsDFWU2mD8U5TFeXtbCPLFUqe7GyCgvO6XDHzje862ODrOwy6WCPmKeWHbCFJ4w==}
     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==}
+  '@babel/plugin-transform-class-static-block@7.24.7':
+    resolution: {integrity: sha512-HMXK3WbBPpZQufbMG4B46A90PkuuhN9vBCb5T8+VAHqvAqvcLi+2cKoukcpmUYkszLhScU3l1iudhrks3DggRQ==}
     engines: {node: '>=6.9.0'}
     peerDependencies:
       '@babel/core': ^7.12.0
 
-  '@babel/plugin-transform-classes@7.23.5':
-    resolution: {integrity: sha512-jvOTR4nicqYC9yzOHIhXG5emiFEOpappSJAl73SDSEDcybD+Puuze8Tnpb9p9qEyYup24tq891gkaygIFvWDqg==}
+  '@babel/plugin-transform-classes@7.24.7':
+    resolution: {integrity: sha512-CFbbBigp8ln4FU6Bpy6g7sE8B/WmCmzvivzUC6xDAdWVsjYTXijpuuGJmYkAaoWAzcItGKT3IOAbxRItZ5HTjw==}
     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==}
+  '@babel/plugin-transform-computed-properties@7.24.7':
+    resolution: {integrity: sha512-25cS7v+707Gu6Ds2oY6tCkUwsJ9YIDbggd9+cu9jzzDgiNq7hR/8dkzxWfKWnTic26vsI3EsCXNd4iEB6e8esQ==}
     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==}
+  '@babel/plugin-transform-destructuring@7.24.7':
+    resolution: {integrity: sha512-19eJO/8kdCQ9zISOf+SEUJM/bAUIsvY3YDnXZTupUCQ8LgrWnsG/gFB9dvXqdXnRXMAM8fvt7b0CBKQHNGy1mw==}
     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==}
+  '@babel/plugin-transform-dotall-regex@7.24.7':
+    resolution: {integrity: sha512-ZOA3W+1RRTSWvyqcMJDLqbchh7U4NRGqwRfFSVbOLS/ePIP4vHB5e8T8eXcuqyN1QkgKyj5wuW0lcS85v4CrSw==}
     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==}
+  '@babel/plugin-transform-duplicate-keys@7.24.7':
+    resolution: {integrity: sha512-JdYfXyCRihAe46jUIliuL2/s0x0wObgwwiGxw/UbgJBr20gQBThrokO4nYKgWkD7uBaqM7+9x5TU7NkExZJyzw==}
     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==}
+  '@babel/plugin-transform-dynamic-import@7.24.7':
+    resolution: {integrity: sha512-sc3X26PhZQDb3JhORmakcbvkeInvxz+A8oda99lj7J60QRuPZvNAk9wQlTBS1ZynelDrDmTU4pw1tyc5d5ZMUg==}
     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==}
+  '@babel/plugin-transform-exponentiation-operator@7.24.7':
+    resolution: {integrity: sha512-Rqe/vSc9OYgDajNIK35u7ot+KeCoetqQYFXM4Epf7M7ez3lWlOjrDjrwMei6caCVhfdw+mIKD4cgdGNy5JQotQ==}
     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==}
+  '@babel/plugin-transform-export-namespace-from@7.24.7':
+    resolution: {integrity: sha512-v0K9uNYsPL3oXZ/7F9NNIbAj2jv1whUEtyA6aujhekLs56R++JDQuzRcP2/z4WX5Vg/c5lE9uWZA0/iUoFhLTA==}
     engines: {node: '>=6.9.0'}
     peerDependencies:
       '@babel/core': ^7.0.0-0
@@ -1956,176 +1982,176 @@ packages:
     peerDependencies:
       '@babel/core': ^7.0.0-0
 
-  '@babel/plugin-transform-for-of@7.23.3':
-    resolution: {integrity: sha512-X8jSm8X1CMwxmK878qsUGJRmbysKNbdpTv/O1/v0LuY/ZkZrng5WYiekYSdg9m09OTmDDUWeEDsTE+17WYbAZw==}
+  '@babel/plugin-transform-for-of@7.24.7':
+    resolution: {integrity: sha512-wo9ogrDG1ITTTBsy46oGiN1dS9A7MROBTcYsfS8DtsImMkHk9JXJ3EWQM6X2SUw4x80uGPlwj0o00Uoc6nEE3g==}
     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==}
+  '@babel/plugin-transform-function-name@7.24.7':
+    resolution: {integrity: sha512-U9FcnA821YoILngSmYkW6FjyQe2TyZD5pHt4EVIhmcTkrJw/3KqcrRSxuOo5tFZJi7TE19iDyI1u+weTI7bn2w==}
     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==}
+  '@babel/plugin-transform-json-strings@7.24.7':
+    resolution: {integrity: sha512-2yFnBGDvRuxAaE/f0vfBKvtnvvqU8tGpMHqMNpTN2oWMKIR3NqFkjaAgGwawhqK/pIN2T3XdjGPdaG0vDhOBGw==}
     engines: {node: '>=6.9.0'}
     peerDependencies:
       '@babel/core': ^7.0.0-0
 
-  '@babel/plugin-transform-literals@7.23.3':
-    resolution: {integrity: sha512-wZ0PIXRxnwZvl9AYpqNUxpZ5BiTGrYt7kueGQ+N5FiQ7RCOD4cm8iShd6S6ggfVIWaJf2EMk8eRzAh52RfP4rQ==}
+  '@babel/plugin-transform-literals@7.24.7':
+    resolution: {integrity: sha512-vcwCbb4HDH+hWi8Pqenwnjy+UiklO4Kt1vfspcQYFhJdpthSnW8XvWGyDZWKNVrVbVViI/S7K9PDJZiUmP2fYQ==}
     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==}
+  '@babel/plugin-transform-logical-assignment-operators@7.24.7':
+    resolution: {integrity: sha512-4D2tpwlQ1odXmTEIFWy9ELJcZHqrStlzK/dAOWYyxX3zT0iXQB6banjgeOJQXzEc4S0E0a5A+hahxPaEFYftsw==}
     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==}
+  '@babel/plugin-transform-member-expression-literals@7.24.7':
+    resolution: {integrity: sha512-T/hRC1uqrzXMKLQ6UCwMT85S3EvqaBXDGf0FaMf4446Qx9vKwlghvee0+uuZcDUCZU5RuNi4781UQ7R308zzBw==}
     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==}
+  '@babel/plugin-transform-modules-amd@7.24.7':
+    resolution: {integrity: sha512-9+pB1qxV3vs/8Hdmz/CulFB8w2tuu6EB94JZFsjdqxQokwGa9Unap7Bo2gGBGIvPmDIVvQrom7r5m/TCDMURhg==}
     engines: {node: '>=6.9.0'}
     peerDependencies:
       '@babel/core': ^7.0.0-0
 
-  '@babel/plugin-transform-modules-commonjs@7.23.3':
-    resolution: {integrity: sha512-aVS0F65LKsdNOtcz6FRCpE4OgsP2OFnW46qNxNIX9h3wuzaNcSQsJysuMwqSibC98HPrf2vCgtxKNwS0DAlgcA==}
+  '@babel/plugin-transform-modules-commonjs@7.24.7':
+    resolution: {integrity: sha512-iFI8GDxtevHJ/Z22J5xQpVqFLlMNstcLXh994xifFwxxGslr2ZXXLWgtBeLctOD63UFDArdvN6Tg8RFw+aEmjQ==}
     engines: {node: '>=6.9.0'}
     peerDependencies:
       '@babel/core': ^7.0.0-0
 
-  '@babel/plugin-transform-modules-systemjs@7.23.3':
-    resolution: {integrity: sha512-ZxyKGTkF9xT9YJuKQRo19ewf3pXpopuYQd8cDXqNzc3mUNbOME0RKMoZxviQk74hwzfQsEe66dE92MaZbdHKNQ==}
+  '@babel/plugin-transform-modules-systemjs@7.24.7':
+    resolution: {integrity: sha512-GYQE0tW7YoaN13qFh3O1NCY4MPkUiAH3fiF7UcV/I3ajmDKEdG3l+UOcbAm4zUE3gnvUU+Eni7XrVKo9eO9auw==}
     engines: {node: '>=6.9.0'}
     peerDependencies:
       '@babel/core': ^7.0.0-0
 
-  '@babel/plugin-transform-modules-umd@7.23.3':
-    resolution: {integrity: sha512-zHsy9iXX2nIsCBFPud3jKn1IRPWg3Ing1qOZgeKV39m1ZgIdpJqvlWVeiHBZC6ITRG0MfskhYe9cLgntfSFPIg==}
+  '@babel/plugin-transform-modules-umd@7.24.7':
+    resolution: {integrity: sha512-3aytQvqJ/h9z4g8AsKPLvD4Zqi2qT+L3j7XoFFu1XBlZWEl2/1kWnhmAbxpLgPrHSY0M6UA02jyTiwUVtiKR6A==}
     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==}
+  '@babel/plugin-transform-named-capturing-groups-regex@7.24.7':
+    resolution: {integrity: sha512-/jr7h/EWeJtk1U/uz2jlsCioHkZk1JJZVcc8oQsJ1dUlaJD83f4/6Zeh2aHt9BIFokHIsSeDfhUmju0+1GPd6g==}
     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==}
+  '@babel/plugin-transform-new-target@7.24.7':
+    resolution: {integrity: sha512-RNKwfRIXg4Ls/8mMTza5oPF5RkOW8Wy/WgMAp1/F1yZ8mMbtwXW+HDoJiOsagWrAhI5f57Vncrmr9XeT4CVapA==}
     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==}
+  '@babel/plugin-transform-nullish-coalescing-operator@7.24.7':
+    resolution: {integrity: sha512-Ts7xQVk1OEocqzm8rHMXHlxvsfZ0cEF2yomUqpKENHWMF4zKk175Y4q8H5knJes6PgYad50uuRmt3UJuhBw8pQ==}
     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==}
+  '@babel/plugin-transform-numeric-separator@7.24.7':
+    resolution: {integrity: sha512-e6q1TiVUzvH9KRvicuxdBTUj4AdKSRwzIyFFnfnezpCfP2/7Qmbb8qbU2j7GODbl4JMkblitCQjKYUaX/qkkwA==}
     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==}
+  '@babel/plugin-transform-object-rest-spread@7.24.7':
+    resolution: {integrity: sha512-4QrHAr0aXQCEFni2q4DqKLD31n2DL+RxcwnNjDFkSG0eNQ/xCavnRkfCUjsyqGC2OviNJvZOF/mQqZBw7i2C5Q==}
     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==}
+  '@babel/plugin-transform-object-super@7.24.7':
+    resolution: {integrity: sha512-A/vVLwN6lBrMFmMDmPPz0jnE6ZGx7Jq7d6sT/Ev4H65RER6pZ+kczlf1DthF5N0qaPHBsI7UXiE8Zy66nmAovg==}
     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==}
+  '@babel/plugin-transform-optional-catch-binding@7.24.7':
+    resolution: {integrity: sha512-uLEndKqP5BfBbC/5jTwPxLh9kqPWWgzN/f8w6UwAIirAEqiIVJWWY312X72Eub09g5KF9+Zn7+hT7sDxmhRuKA==}
     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==}
+  '@babel/plugin-transform-optional-chaining@7.24.7':
+    resolution: {integrity: sha512-tK+0N9yd4j+x/4hxF3F0e0fu/VdcxU18y5SevtyM/PCFlQvXbR0Zmlo2eBrKtVipGNFzpq56o8WsIIKcJFUCRQ==}
     engines: {node: '>=6.9.0'}
     peerDependencies:
       '@babel/core': ^7.0.0-0
 
-  '@babel/plugin-transform-parameters@7.23.3':
-    resolution: {integrity: sha512-09lMt6UsUb3/34BbECKVbVwrT9bO6lILWln237z7sLaWnMsTi7Yc9fhX5DLpkJzAGfaReXI22wP41SZmnAA3Vw==}
+  '@babel/plugin-transform-parameters@7.24.7':
+    resolution: {integrity: sha512-yGWW5Rr+sQOhK0Ot8hjDJuxU3XLRQGflvT4lhlSY0DFvdb3TwKaY26CJzHtYllU0vT9j58hc37ndFPsqT1SrzA==}
     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==}
+  '@babel/plugin-transform-private-methods@7.24.7':
+    resolution: {integrity: sha512-COTCOkG2hn4JKGEKBADkA8WNb35TGkkRbI5iT845dB+NyqgO8Hn+ajPbSnIQznneJTa3d30scb6iz/DhH8GsJQ==}
     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==}
+  '@babel/plugin-transform-private-property-in-object@7.24.7':
+    resolution: {integrity: sha512-9z76mxwnwFxMyxZWEgdgECQglF2Q7cFLm0kMf8pGwt+GSJsY0cONKj/UuO4bOH0w/uAel3ekS4ra5CEAyJRmDA==}
     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==}
+  '@babel/plugin-transform-property-literals@7.24.7':
+    resolution: {integrity: sha512-EMi4MLQSHfd2nrCqQEWxFdha2gBCqU4ZcCng4WBGZ5CJL4bBRW0ptdqqDdeirGZcpALazVVNJqRmsO8/+oNCBA==}
     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==}
+  '@babel/plugin-transform-regenerator@7.24.7':
+    resolution: {integrity: sha512-lq3fvXPdimDrlg6LWBoqj+r/DEWgONuwjuOuQCSYgRroXDH/IdM1C0IZf59fL5cHLpjEH/O6opIRBbqv7ELnuA==}
     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==}
+  '@babel/plugin-transform-reserved-words@7.24.7':
+    resolution: {integrity: sha512-0DUq0pHcPKbjFZCfTss/pGkYMfy3vFWydkUBd9r0GHpIyfs2eCDENvqadMycRS9wZCXR41wucAfJHJmwA0UmoQ==}
     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==}
+  '@babel/plugin-transform-shorthand-properties@7.24.7':
+    resolution: {integrity: sha512-KsDsevZMDsigzbA09+vacnLpmPH4aWjcZjXdyFKGzpplxhbeB4wYtury3vglQkg6KM/xEPKt73eCjPPf1PgXBA==}
     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==}
+  '@babel/plugin-transform-spread@7.24.7':
+    resolution: {integrity: sha512-x96oO0I09dgMDxJaANcRyD4ellXFLLiWhuwDxKZX5g2rWP1bTPkBSwCYv96VDXVT1bD9aPj8tppr5ITIh8hBng==}
     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==}
+  '@babel/plugin-transform-sticky-regex@7.24.7':
+    resolution: {integrity: sha512-kHPSIJc9v24zEml5geKg9Mjx5ULpfncj0wRpYtxbvKyTtHCYDkVE3aHQ03FrpEo4gEe2vrJJS1Y9CJTaThA52g==}
     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==}
+  '@babel/plugin-transform-template-literals@7.24.7':
+    resolution: {integrity: sha512-AfDTQmClklHCOLxtGoP7HkeMw56k1/bTQjwsfhL6pppo/M4TOBSq+jjBUBLmV/4oeFg4GWMavIl44ZeCtmmZTw==}
     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==}
+  '@babel/plugin-transform-typeof-symbol@7.24.7':
+    resolution: {integrity: sha512-VtR8hDy7YLB7+Pet9IarXjg/zgCMSF+1mNS/EQEiEaUPoFXCVsHG64SIxcaaI2zJgRiv+YmgaQESUfWAdbjzgg==}
     engines: {node: '>=6.9.0'}
     peerDependencies:
       '@babel/core': ^7.0.0-0
@@ -2136,32 +2162,32 @@ packages:
     peerDependencies:
       '@babel/core': ^7.0.0-0
 
-  '@babel/plugin-transform-unicode-escapes@7.23.3':
-    resolution: {integrity: sha512-OMCUx/bU6ChE3r4+ZdylEqAjaQgHAgipgW8nsCfu5pGqDcFytVd91AwRvUJSBZDz0exPGgnjoqhgRYLRjFZc9Q==}
+  '@babel/plugin-transform-unicode-escapes@7.24.7':
+    resolution: {integrity: sha512-U3ap1gm5+4edc2Q/P+9VrBNhGkfnf+8ZqppY71Bo/pzZmXhhLdqgaUl6cuB07O1+AQJtCLfaOmswiNbSQ9ivhw==}
     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==}
+  '@babel/plugin-transform-unicode-property-regex@7.24.7':
+    resolution: {integrity: sha512-uH2O4OV5M9FZYQrwc7NdVmMxQJOCCzFeYudlZSzUAHRFeOujQefa92E74TQDVskNHCzOXoigEuoyzHDhaEaK5w==}
     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==}
+  '@babel/plugin-transform-unicode-regex@7.24.7':
+    resolution: {integrity: sha512-hlQ96MBZSAXUq7ltkjtu3FJCCSMx/j629ns3hA3pXnBXjanNP0LHi+JpPeA81zaWgVK1VGH95Xuy7u0RyQ8kMg==}
     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==}
+  '@babel/plugin-transform-unicode-sets-regex@7.24.7':
+    resolution: {integrity: sha512-2G8aAvF4wy1w/AGZkemprdGMRg5o6zPNhbHVImRz3lss55TYCBd6xStN19rt8XJHq20sqV0JbyWjOWwQRwV/wg==}
     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==}
+  '@babel/preset-env@7.24.7':
+    resolution: {integrity: sha512-1YZNsc+y6cTvWlDHidMBsQZrZfEFjRIo/BZCT906PMdzOyXtSLTgqGdrpcuTDCXyd11Am5uQULtDIcCfnTc8fQ==}
     engines: {node: '>=6.9.0'}
     peerDependencies:
       '@babel/core': ^7.0.0-0
@@ -2204,20 +2230,20 @@ 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==}
-    engines: {node: '>=6.9.0'}
-
-  '@babel/types@7.24.0':
-    resolution: {integrity: sha512-+j7a5c253RfKh8iABBhywc8NSfP5LURe7Uh4qpsh6jc+aLJguvmIUBdjSdEMQv2bENrCR5MfRdjGo7vzS/ob7w==}
+  '@babel/types@7.24.7':
+    resolution: {integrity: sha512-XEFXSlxiG5td2EJRe8vOmRbaXVgfcBlszKujvVmWIK/UpywWljQCfzAv3RQCGujWQ1RD4YYWEAqDXfuJiy8f5Q==}
     engines: {node: '>=6.9.0'}
 
   '@base2/pretty-print-object@1.0.1':
@@ -2226,16 +2252,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@5.21.1':
+    resolution: {integrity: sha512-anzTfhOJ93eraT/GYeyxWpxRMarHwuevn6pPBfdOj0LC2eg98OPnkfdMSjcrpL3qrqsxON0RslS7kuPfCEnX6A==}
     peerDependencies:
-      '@bull-board/ui': 5.17.0
+      '@bull-board/ui': 5.21.1
 
-  '@bull-board/fastify@5.17.0':
-    resolution: {integrity: sha512-73YrPc7ERTWSOQRgBP6a7BPscWfcHd8U+Zq0auMdL/KkjPhG9GxapbfnovGZDDahJL/p/4YQb6ULu03zdtOrEA==}
+  '@bull-board/fastify@5.21.1':
+    resolution: {integrity: sha512-We33yolc70SALjDdF3cjEaLn1L/vrw85eFCsrviESaW3dFVIdB+xn0fdqMFK6NnaC0JjBa3Ypfev4Co+eaZ+1A==}
 
-  '@bull-board/ui@5.17.0':
-    resolution: {integrity: sha512-Vj+yWPjrjx3Iqh2N/ZBDhK2d2yJD44dfvIxm+SnXQb4ne312j117TpViInceysxGtbbAOlAW6hq6JvsDoRl7KQ==}
+  '@bull-board/ui@5.21.1':
+    resolution: {integrity: sha512-JBDeCqG7j/c3WE0uGMN9snPkRJz9/D6MpTZzyVj7KOxIJwNKPOICNFZbCrCNi7bcJYHDJ2xGTN9OO1mw7i43BQ==}
 
   '@bundled-es-modules/cookie@2.0.0':
     resolution: {integrity: sha512-Or6YHg/kamKHpxULAdSqhGqnWFneIXu1NKvvfBBzKGwpVsYuFIQ5aBPHDnnoR3ghW1nvSkALd+EF9iMtY7Vjxw==}
@@ -2243,6 +2269,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==}
 
@@ -2250,38 +2279,38 @@ 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.1':
+    resolution: {integrity: sha512-jRt9OM7cls+zch8U2m7pA9wp8dNOz0EtedGKkqH+DInUYw1+UtonEJirrxyl1YHRgOme5M5DTsDmkUSrUiZN6g==}
 
-  '@cropper/element-crosshair@2.0.0-beta.5':
-    resolution: {integrity: sha512-en3EjiqS/O/uVPVLUanx2ZxvE2n3J5VxGABvBTwQimX4c3kNixq8TUVlsaLdcG7jbehxFpT3S19+tiuZudHqxg==}
+  '@cropper/element-crosshair@2.0.0-rc.1':
+    resolution: {integrity: sha512-xfLelqM8EnRZUf7xEE88RWQQx5erUv7jrzni52bAw3/Ua8HXIz3uAMnkrGKOTBj8K4Rv/mNJY8k1DVAEfHY6Lg==}
 
-  '@cropper/element-grid@2.0.0-beta.5':
-    resolution: {integrity: sha512-uKQExNTOMOGo5d6Tv1NJDbjJHRR/0NgqeROUSt2J8g9ymPP+/MoFdCCf+Nj/KM5pk7/fBEV3HhzUnO8jh1hZfQ==}
+  '@cropper/element-grid@2.0.0-rc.1':
+    resolution: {integrity: sha512-U/BYPl76upd9sXT+pZTFoQzUqWyNxdGs4YR2UtaVCfTMHLDTrssPAedmqEEnHgbqVcr325sIEfVmwWVA+v+8Dg==}
 
-  '@cropper/element-handle@2.0.0-beta.5':
-    resolution: {integrity: sha512-SlaV5/qbEBQLHnuaGD8J0EqSp797m/MMB8V10EUZpv6cznSRxg/SXOj6ROE0ePzo5+0i96Dw+8ZukLilDgc1Xw==}
+  '@cropper/element-handle@2.0.0-rc.1':
+    resolution: {integrity: sha512-GuOHbjkg5CP1+oFzWQeD7VZffUE86dp4gKv5egLxkBEwnQp1VQxjO7L1Wkgj+KsQymoDczsl+x4bF12KDyDg2g==}
 
-  '@cropper/element-image@2.0.0-beta.5':
-    resolution: {integrity: sha512-369ztVaoRS2DN8SaiHZ/bRCz0Snw9ss7PZrX1OQK86fwVhCoeRqCHj48ayfLMdchx+J3RbM5f2g8ePf7ejwOKw==}
+  '@cropper/element-image@2.0.0-rc.1':
+    resolution: {integrity: sha512-ttzawKbUkR2A9U3bc2AN/jbNdszBP/yb83PIc5jekjOs+Z7kUBVdOo1SLIewpQ0DjUzhfCRXWUowP1McVQUXZw==}
 
-  '@cropper/element-selection@2.0.0-beta.5':
-    resolution: {integrity: sha512-l8DvOBAZYytTarpkfhCglhxD+zDQ2acVwIzGwp5r9xR+ERleJHxr2rHYVhowRHT/JZRd94DJBlye91c1uO/XGg==}
+  '@cropper/element-selection@2.0.0-rc.1':
+    resolution: {integrity: sha512-AcRHRbsyt9xRfBD1QRyNDTS+vaYg6uAeuqhk/Ra58pqxlhtoimAV3oQ7uc/edwOlK60f/DxtKCc8rSOYFQ85bQ==}
 
-  '@cropper/element-shade@2.0.0-beta.5':
-    resolution: {integrity: sha512-LGSVLAD1lasFrS+Pd7JnQSJRCMSNnc40UCcjLhscDuRcRHK/ViMglnwCfFxeGnS26kugbDLF5IbYDCLCbykUog==}
+  '@cropper/element-shade@2.0.0-rc.1':
+    resolution: {integrity: sha512-nHv2WujETENoIfxWQn7TYiOnXm5YUnZsoG4r6njK5cxj0gIUfPudUSbjWCQSuB2oxxpeEK8oyTdfOZtP9cxK4g==}
 
-  '@cropper/element-viewer@2.0.0-beta.5':
-    resolution: {integrity: sha512-i4cc+L+j8Gq1L8g1BQWfQ842QxH5T9v2EkIeGuW66SVSBglafxu8gxmSOyRD3hDAMHM3wbJ+XVmFwBHZzlYCvQ==}
+  '@cropper/element-viewer@2.0.0-rc.1':
+    resolution: {integrity: sha512-xTj0BObCygbVWXc7t7FYZ9k2eFyWN360it5uGeAkImXcwINRQGTFcLLOjs6i3SwedI7F1a1yNcTBfoT1B/sNAg==}
 
-  '@cropper/element@2.0.0-beta.5':
-    resolution: {integrity: sha512-+pHX/iYw+Y/HxgpcjvSPBc3+hvJaycznbZdWifnChmDkpLStd6Xu9gO2ful9sSL0uGSjQxUYV4xPyikYJOnfug==}
+  '@cropper/element@2.0.0-rc.1':
+    resolution: {integrity: sha512-OPKgjUgYC2Xmv77vEqtAR6bdfKOW+v9FrSjr4re3u95rcVj6NJ0JidIta41Ipp8KydHTXSmLetq4XDrA+vuIJQ==}
 
-  '@cropper/elements@2.0.0-beta.5':
-    resolution: {integrity: sha512-KWa5/dmJpLcKDJpNlbEQzO9Shz+f4aB0I3y97CqqTf8JSGS6CEKOd9uLywd1eow1r4O0Hwo65ktXPwAEhMWDZg==}
+  '@cropper/elements@2.0.0-rc.1':
+    resolution: {integrity: sha512-6qbtCq3iL3dETVav2XA03a8iLkHXWMIqHFxViMjlLr9CSuDjjaS5wp0JDuGtPv5FHxjsjyQ8Yayt8Ak5p09Zxg==}
 
-  '@cropper/utils@2.0.0-beta.5':
-    resolution: {integrity: sha512-xE7Klel/4WSjhLUeldfROwbWqV/1A3YKrQLqTrs5/X0ath7B05Fmvhr3TNFvN51v2KSx46Ug6xDJzmbg772m1g==}
+  '@cropper/utils@2.0.0-rc.1':
+    resolution: {integrity: sha512-kreB3wdrAhmTEscfB8/j7ksGBgYSKN+28t37CAI0Vb5DvX/aUDPDH+3e2kyD7YE+DIZgdnuY2FsMYJAQ9sTThg==}
 
   '@cypress/request@3.0.0':
     resolution: {integrity: sha512-GKFCqwZwMYmL3IBoNeR2MM1SnxRIGERsQOTWeQKoYBt2JLqcqiy7JXqO894FLrpjZYqGxW92MNwRH2BN56obdQ==}
@@ -2315,12 +2344,18 @@ 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/android-arm64@0.18.20':
     resolution: {integrity: sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==}
     engines: {node: '>=12'}
@@ -2333,12 +2368,18 @@ 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-arm@0.18.20':
     resolution: {integrity: sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==}
     engines: {node: '>=12'}
@@ -2351,12 +2392,18 @@ 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-x64@0.18.20':
     resolution: {integrity: sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==}
     engines: {node: '>=12'}
@@ -2369,12 +2416,18 @@ 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/darwin-arm64@0.18.20':
     resolution: {integrity: sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==}
     engines: {node: '>=12'}
@@ -2387,12 +2440,18 @@ 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-x64@0.18.20':
     resolution: {integrity: sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==}
     engines: {node: '>=12'}
@@ -2405,12 +2464,18 @@ 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/freebsd-arm64@0.18.20':
     resolution: {integrity: sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==}
     engines: {node: '>=12'}
@@ -2423,12 +2488,18 @@ 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-x64@0.18.20':
     resolution: {integrity: sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==}
     engines: {node: '>=12'}
@@ -2441,12 +2512,18 @@ 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/linux-arm64@0.18.20':
     resolution: {integrity: sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==}
     engines: {node: '>=12'}
@@ -2459,12 +2536,18 @@ 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-arm@0.18.20':
     resolution: {integrity: sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==}
     engines: {node: '>=12'}
@@ -2477,12 +2560,18 @@ 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-ia32@0.18.20':
     resolution: {integrity: sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==}
     engines: {node: '>=12'}
@@ -2495,12 +2584,18 @@ 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-loong64@0.18.20':
     resolution: {integrity: sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==}
     engines: {node: '>=12'}
@@ -2513,12 +2608,18 @@ 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-mips64el@0.18.20':
     resolution: {integrity: sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==}
     engines: {node: '>=12'}
@@ -2531,12 +2632,18 @@ 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-ppc64@0.18.20':
     resolution: {integrity: sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==}
     engines: {node: '>=12'}
@@ -2549,12 +2656,18 @@ 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-riscv64@0.18.20':
     resolution: {integrity: sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==}
     engines: {node: '>=12'}
@@ -2567,12 +2680,18 @@ 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-s390x@0.18.20':
     resolution: {integrity: sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==}
     engines: {node: '>=12'}
@@ -2585,12 +2704,18 @@ 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-x64@0.18.20':
     resolution: {integrity: sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==}
     engines: {node: '>=12'}
@@ -2603,12 +2728,18 @@ 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/netbsd-x64@0.18.20':
     resolution: {integrity: sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==}
     engines: {node: '>=12'}
@@ -2621,12 +2752,24 @@ 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/openbsd-arm64@0.23.0':
+    resolution: {integrity: sha512-suXjq53gERueVWu0OKxzWqk7NxiUWSUlrxoZK7usiF50C6ipColGR5qie2496iKGYNLhDZkPxBI3erbnYkU0rQ==}
+    engines: {node: '>=18'}
+    cpu: [arm64]
+    os: [openbsd]
+
   '@esbuild/openbsd-x64@0.18.20':
     resolution: {integrity: sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==}
     engines: {node: '>=12'}
@@ -2639,12 +2782,18 @@ 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/sunos-x64@0.18.20':
     resolution: {integrity: sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==}
     engines: {node: '>=12'}
@@ -2657,12 +2806,18 @@ 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/win32-arm64@0.18.20':
     resolution: {integrity: sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==}
     engines: {node: '>=12'}
@@ -2675,12 +2830,18 @@ 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-ia32@0.18.20':
     resolution: {integrity: sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==}
     engines: {node: '>=12'}
@@ -2693,12 +2854,18 @@ 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-x64@0.18.20':
     resolution: {integrity: sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==}
     engines: {node: '>=12'}
@@ -2711,34 +2878,60 @@ 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]
+
   '@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/compat@1.1.1':
+    resolution: {integrity: sha512-lpHyRyplhGPL5mGEh6M9O5nnKk0Gz4bFI+Zu6tKlPpDUN7XshWvH9C/px4UVm87IAANE0W81CEsNGbS1KlzXpA==}
+    engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.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/eslintrc@2.1.4':
     resolution: {integrity: sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==}
     engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.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/eslintrc@3.1.0':
+    resolution: {integrity: sha512-4Bfj15dVJdoy3RfZmmo86RK1Fwzn6SstsvK9JS+BaVKqC6QQQQyXekNaC+g+LKNgkQ+2VhGAzm6hO40AhMR3zQ==}
+    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/js@9.8.0':
+    resolution: {integrity: sha512-MfluB7EUfxXtv3i/++oh89uzAr4PDI4nn201hsp+qaXqsjAWzinlZEHEfPgAX4doIlKvPG/i0A9dpKxOLII8yA==}
+    engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+  '@eslint/object-schema@2.1.4':
+    resolution: {integrity: sha512-BsWiH1yFGjXXS2yvrf5LyuoSIIbPrGUWob917o+BTKuZ7qJdxX8aJLRxs1fS9n6r7vESrq1OUqb68dANcFXuQQ==}
+    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==}
 
@@ -2781,8 +2974,8 @@ packages:
   '@fastify/http-proxy@9.5.0':
     resolution: {integrity: sha512-1iqIdV10d5k9YtfHq9ylX5zt1NiM50fG+rIX40qt00R694sqWso3ukyTFZVk33SDoSiBW8roB7n11RUVUoN+Ag==}
 
-  '@fastify/multipart@8.2.0':
-    resolution: {integrity: sha512-OZ8nsyyoS2TV7Yeu3ZdrdDGsKUTAbfjrKC9jSxGgT2qdgek+BxpWX31ZubTrWMNZyU5xwk4ox6AvTjAbYWjrWg==}
+  '@fastify/multipart@8.3.0':
+    resolution: {integrity: sha512-A8h80TTyqUzaMVH0Cr9Qcm6RxSkVqmhK/MVBYHYeRRSUbUYv08WecjWKSlG2aSnD4aGI841pVxAjC+G1GafUeQ==}
 
   '@fastify/reply-from@9.0.1':
     resolution: {integrity: sha512-q9vFNUiXZTY1x8omDPe59os2MYq+3y7KgO/kZoXpZlnud+45Nd8Ot/svEvrUATzjkizIggfS4K8LR9zXDyZZKg==}
@@ -2793,8 +2986,8 @@ packages:
   '@fastify/static@6.12.0':
     resolution: {integrity: sha512-KK1B84E6QD/FcQWxDI2aiUCwHxMJBI1KeCUzm1BwYpPY1b742+jeKruGHP2uOluuM6OkBPI8CIANrXcCRtC2oQ==}
 
-  '@fastify/static@7.0.3':
-    resolution: {integrity: sha512-2tmTdF+uFCykasutaO6k4/wOt7eXyi7m3dGuCPo5micXzv0qt6ttb/nWnDYL/BlXjYGfp1JI4a1gyluTIylvQA==}
+  '@fastify/static@7.0.4':
+    resolution: {integrity: sha512-p2uKtaf8BMOZWLs6wu+Ihg7bWNBdjNgCwDza4MJtTqg+5ovKmcbgbR9Xs5/smZ1YISfzKOCNYmZV8LaCj+eJ1Q==}
 
   '@fastify/view@8.2.0':
     resolution: {integrity: sha512-hBSiBofCnJNlPHEMZWpO1SL84eqOaqujJ1hR3jntFyZZCkweH5jMs12DKYyGesjVll7SJFRRxPUBB8kmUmneRQ==}
@@ -2812,11 +3005,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==}
@@ -2830,13 +3020,10 @@ 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'}
+    deprecated: Use @eslint/config-array instead
 
   '@humanwhocodes/module-importer@1.0.1':
     resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==}
@@ -2846,20 +3033,22 @@ 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/object-schema@2.0.3':
+    resolution: {integrity: sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==}
+    deprecated: Use @eslint/object-schema instead
 
-  '@humanwhocodes/object-schema@2.0.2':
-    resolution: {integrity: sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==}
+  '@humanwhocodes/retry@0.3.0':
+    resolution: {integrity: sha512-d2CGZR2o7fS6sWB7DG/3a95bGKQyHMACZ5aW8qGkkqQpUoZV6C0X7Pc7l4ZNMZkfNBf4VWNe9E1jRsf0G146Ew==}
+    engines: {node: '>=18.18'}
 
-  '@img/sharp-darwin-arm64@0.33.3':
-    resolution: {integrity: sha512-FaNiGX1MrOuJ3hxuNzWgsT/mg5OHG/Izh59WW2mk1UwYHUwtfbhk5QNKYZgxf0pLOhx9ctGiGa2OykD71vOnSw==}
+  '@img/sharp-darwin-arm64@0.33.4':
+    resolution: {integrity: sha512-p0suNqXufJs9t3RqLBO6vvrgr5OhgbWp76s5gTRvdmxmuv9E1rcaqGUsl3l4mKVmXPkTkTErXediAui4x+8PSA==}
     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'}
     cpu: [arm64]
     os: [darwin]
 
-  '@img/sharp-darwin-x64@0.33.3':
-    resolution: {integrity: sha512-2QeSl7QDK9ru//YBT4sQkoq7L0EAJZA3rtV+v9p8xTKl4U1bUqTIaCnoC7Ctx2kCjQgwFXDasOtPTCT8eCTXvw==}
+  '@img/sharp-darwin-x64@0.33.4':
+    resolution: {integrity: sha512-0l7yRObwtTi82Z6ebVI2PnHT8EB2NxBgpK2MiKJZJ7cz32R4lxd001ecMhzzsZig3Yv9oclvqqdV93jo9hy+Dw==}
     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'}
     cpu: [x64]
     os: [darwin]
@@ -2912,55 +3101,55 @@ packages:
     cpu: [x64]
     os: [linux]
 
-  '@img/sharp-linux-arm64@0.33.3':
-    resolution: {integrity: sha512-Zf+sF1jHZJKA6Gor9hoYG2ljr4wo9cY4twaxgFDvlG0Xz9V7sinsPp8pFd1XtlhTzYo0IhDbl3rK7P6MzHpnYA==}
+  '@img/sharp-linux-arm64@0.33.4':
+    resolution: {integrity: sha512-2800clwVg1ZQtxwSoTlHvtm9ObgAax7V6MTAB/hDT945Tfyy3hVkmiHpeLPCKYqYR1Gcmv1uDZ3a4OFwkdBL7Q==}
     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'}
     cpu: [arm64]
     os: [linux]
 
-  '@img/sharp-linux-arm@0.33.3':
-    resolution: {integrity: sha512-Q7Ee3fFSC9P7vUSqVEF0zccJsZ8GiiCJYGWDdhEjdlOeS9/jdkyJ6sUSPj+bL8VuOYFSbofrW0t/86ceVhx32w==}
+  '@img/sharp-linux-arm@0.33.4':
+    resolution: {integrity: sha512-RUgBD1c0+gCYZGCCe6mMdTiOFS0Zc/XrN0fYd6hISIKcDUbAW5NtSQW9g/powkrXYm6Vzwd6y+fqmExDuCdHNQ==}
     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'}
     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.4':
+    resolution: {integrity: sha512-h3RAL3siQoyzSoH36tUeS0PDmb5wINKGYzcLB5C6DIiAn2F3udeFAum+gj8IbA/82+8RGCTn7XW8WTFnqag4tQ==}
+    engines: {glibc: '>=2.31', node: ^18.17.0 || ^20.3.0 || >=21.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'}
     cpu: [s390x]
     os: [linux]
 
-  '@img/sharp-linux-x64@0.33.3':
-    resolution: {integrity: sha512-Q4I++herIJxJi+qmbySd072oDPRkCg/SClLEIDh5IL9h1zjhqjv82H0Seupd+q2m0yOfD+/fJnjSoDFtKiHu2g==}
+  '@img/sharp-linux-x64@0.33.4':
+    resolution: {integrity: sha512-GoR++s0XW9DGVi8SUGQ/U4AeIzLdNjHka6jidVwapQ/JebGVQIpi52OdyxCNVRE++n1FCLzjDovJNozif7w/Aw==}
     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'}
     cpu: [x64]
     os: [linux]
 
-  '@img/sharp-linuxmusl-arm64@0.33.3':
-    resolution: {integrity: sha512-qnDccehRDXadhM9PM5hLvcPRYqyFCBN31kq+ErBSZtZlsAc1U4Z85xf/RXv1qolkdu+ibw64fUDaRdktxTNP9A==}
+  '@img/sharp-linuxmusl-arm64@0.33.4':
+    resolution: {integrity: sha512-nhr1yC3BlVrKDTl6cO12gTpXMl4ITBUZieehFvMntlCXFzH2bvKG76tBL2Y/OqhupZt81pR7R+Q5YhJxW0rGgQ==}
     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'}
     cpu: [arm64]
     os: [linux]
 
-  '@img/sharp-linuxmusl-x64@0.33.3':
-    resolution: {integrity: sha512-Jhchim8kHWIU/GZ+9poHMWRcefeaxFIs9EBqf9KtcC14Ojk6qua7ghKiPs0sbeLbLj/2IGBtDcxHyjCdYWkk2w==}
+  '@img/sharp-linuxmusl-x64@0.33.4':
+    resolution: {integrity: sha512-uCPTku0zwqDmZEOi4ILyGdmW76tH7dm8kKlOIV1XC5cLyJ71ENAAqarOHQh0RLfpIpbV5KOpXzdU6XkJtS0daw==}
     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'}
     cpu: [x64]
     os: [linux]
 
-  '@img/sharp-wasm32@0.33.3':
-    resolution: {integrity: sha512-68zivsdJ0koE96stdUfM+gmyaK/NcoSZK5dV5CAjES0FUXS9lchYt8LAB5rTbM7nlWtxaU/2GON0HVN6/ZYJAQ==}
+  '@img/sharp-wasm32@0.33.4':
+    resolution: {integrity: sha512-Bmmauh4sXUsUqkleQahpdNXKvo+wa1V9KhT2pDA4VJGKwnKMJXiSTGphn0gnJrlooda0QxCtXc6RX1XAU6hMnQ==}
     engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'}
     cpu: [wasm32]
 
-  '@img/sharp-win32-ia32@0.33.3':
-    resolution: {integrity: sha512-CyimAduT2whQD8ER4Ux7exKrtfoaUiVr7HG0zZvO0XTFn2idUWljjxv58GxNTkFb8/J9Ub9AqITGkJD6ZginxQ==}
+  '@img/sharp-win32-ia32@0.33.4':
+    resolution: {integrity: sha512-99SJ91XzUhYHbx7uhK3+9Lf7+LjwMGQZMDlO/E/YVJ7Nc3lyDFZPGhjwiYdctoH2BOzW9+TnfqcaMKt0jHLdqw==}
     engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'}
     cpu: [ia32]
     os: [win32]
 
-  '@img/sharp-win32-x64@0.33.3':
-    resolution: {integrity: sha512-viT4fUIDKnli3IfOephGnolMzhz5VaTvDRkYqtZxOMIoMQ4MrAziO7pT1nVnOt2FAm7qW5aa+CCc13aEY6Le0g==}
+  '@img/sharp-win32-x64@0.33.4':
+    resolution: {integrity: sha512-3QLocdTRVIrFNye5YocZl+KKpYKP+fksi1QhmOArgx7GyhIbQp/WrJRu176jm8IxromS7RIkzMiMINVdBtC8Aw==}
     engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'}
     cpu: [x64]
     os: [win32]
@@ -3078,8 +3267,8 @@ packages:
     resolution: {integrity: sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==}
     engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
 
-  '@joshwooding/vite-plugin-react-docgen-typescript@0.3.0':
-    resolution: {integrity: sha512-2D6y7fNvFmsLmRt6UCOFJPvFoPMJGT0Uh1Wg0RaigUp7kdQPs6yYn8Dmx6GZkOH/NW0yMTwRz/p0SRMMRo50vA==}
+  '@joshwooding/vite-plugin-react-docgen-typescript@0.3.1':
+    resolution: {integrity: sha512-pdoMZ9QaPnVlSM+SdU/wgg0nyD/8wQ7y90ttO2CMCyrrm7RxveYIJ5eNfjPaoMFqW41LZra7QO9j+xV4Y18Glw==}
     peerDependencies:
       typescript: '>= 4.3.x'
       vite: ^3.0.0 || ^4.0.0 || ^5.0.0
@@ -3091,6 +3280,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'}
@@ -3099,8 +3292,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==}
@@ -3111,6 +3308,9 @@ packages:
   '@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==}
 
@@ -3140,29 +3340,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.4':
+    resolution: {integrity: sha512-LHOMxmT8/tU1IiiiHOdHFF83Qsi+V8d0kLfscG4EvQE9cafiR8blOYr8SfkQKWB1wgEilQgXJX3MIA4vetDLZw==}
 
-  '@microsoft/api-extractor@7.43.1':
-    resolution: {integrity: sha512-ohg40SsvFFgzHFAtYq5wKJc8ZDyY46bphjtnSvhSSlXpPTG7GHwyyXkn48UZiUCBwr2WC7TRC1Jfwz7nreuiyQ==}
+  '@microsoft/api-extractor@7.47.4':
+    resolution: {integrity: sha512-HKm+P4VNzWwvq1Ey+Jfhhj/3MjsD+ka2hbt8L5AcRM95lu1MFOYnz3XlU7Gr79Q/ZhOb7W/imAKeYrOI0bFydg==}
     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==}
@@ -3204,77 +3406,70 @@ 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==}
-    engines: {node: '>=18'}
-
-  '@napi-rs/canvas-android-arm64@0.1.52':
-    resolution: {integrity: sha512-x/K471KbASPVh5mfBUxokza66J0FNIlOgMNANWAf5C8HiATb487KecEhSkUQvvTS3WLYC9uSqIPHFgwF+tir3w==}
+  '@napi-rs/canvas-android-arm64@0.1.53':
+    resolution: {integrity: sha512-2YhxfVsZguATlRWE0fZdTx35SE9+r5D7HV5GPNDataZOKmHf+zZ5//dspuuBSbOriQdoicaFrgXKCUqI0pK3WQ==}
     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.53':
+    resolution: {integrity: sha512-ls+CWLMusf4RAGo5BvIIzA6dNcc0elwVp6LKjHfQECHA8KKmvdB58YuE5BQcTlb2rzk0SEKtBC/Th3NI2oNdfg==}
     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.53':
+    resolution: {integrity: sha512-ZAgcoCH5+5OKS2P8Lxx+jbkAPKkyLD2x6OvSrHg1U6ppdxmLA+CkJlRl8w45HCXwuyIiP7OeymECRtiNYTwznQ==}
     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.53':
+    resolution: {integrity: sha512-p9km/3C/loDxu3AvA8/vtpIS1BGMd/Ehkl2Iu/v/Gw8N/KUIt3HUvTS7AKApyVE28bxTfq96wJQjtcT8jzDncw==}
     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.53':
+    resolution: {integrity: sha512-QKK+sykEiYwjwd+ogyLcpcnH38DNZ8KViBlnfEpoGA2Wa+21/cWQKfMxnbgb/rbvm5tazJinZcihFvH577WQ5g==}
     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.53':
+    resolution: {integrity: sha512-2N41U0X8RnrTKzpTtPv1ozlYkJtPsUdbfF3uP/KEd/BsULGd8Y8ghkGMS6CM+821au4ex0dPrWOOdT9wC1rSqQ==}
     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.53':
+    resolution: {integrity: sha512-7XjuTvDKCODtf/vMwF43VGDrjfgwYKgS91ggdcX3UrJaBYWyWu/+eqNvNj+zdXSe/0x+YOjf5jG4m8xIXdBMQA==}
     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.53':
+    resolution: {integrity: sha512-970WEvB8vmj+uxvgdBZ+AGFV7uq9GJhXrqG5PGQ5lWciHX0P0d/OhS2F7TITgFR0LsKDQZ7XQgzMxsYOfwZ0FQ==}
     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.53':
+    resolution: {integrity: sha512-rLFQCSJaWg/sv54Aap9nAhaodi4Vyb4un50EgW+PNkk8icMziU6KLRKirGBdQr9ZdxnshAPeQXD1g2ArStujKA==}
     engines: {node: '>= 10'}
     cpu: [x64]
     os: [win32]
 
-  '@napi-rs/canvas@0.1.52':
-    resolution: {integrity: sha512-xeW9EghZLDPZuqWJ4l1+eG3ld0i9J7SpV2zlgi34MPt/FE9K2XWGCfnLr0gHGOBkcI3YOVhI13I0HqRAkMPdVw==}
+  '@napi-rs/canvas@0.1.53':
+    resolution: {integrity: sha512-XsEZi97+kKykmAiPpY+IpZoHxJY1srqFZp8jDt1/RySzC0kB0iZYt/VMIFqQKpLCARZjD7SOAz2AULtwYlesCA==}
     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.3.10':
+    resolution: {integrity: sha512-H8k0jZtxk1IdtErGDmxFRy0PfcOAUg41Prrqpx76DQusGGJjsaovs1zjXVD1rZWaVYchfT1uczJ6L4Kio10VNg==}
     peerDependencies:
       class-transformer: '*'
       class-validator: '*'
@@ -3286,8 +3481,8 @@ packages:
       class-validator:
         optional: true
 
-  '@nestjs/core@10.3.8':
-    resolution: {integrity: sha512-AxF4tpYLDNn5Wfb3C4bNaaHJ4pREH5FJrSisR2A5zkYpQFORFs0Tc36lOFPMwBTy8Iv2wUwWLUVc5ftBnxEv4w==}
+  '@nestjs/core@10.3.10':
+    resolution: {integrity: sha512-ZbQ4jovQyzHtCGCrzK5NdtW1SYO2fHSsgSY1+/9WdruYCUra+JDkWEXgZ4M3Hv480Dl3OXehAmY1wCOojeMyMQ==}
     peerDependencies:
       '@nestjs/common': ^10.0.0
       '@nestjs/microservices': ^10.0.0
@@ -3303,14 +3498,14 @@ packages:
       '@nestjs/websockets':
         optional: true
 
-  '@nestjs/platform-express@10.3.8':
-    resolution: {integrity: sha512-sifLoxgEJvAgbim1UuW6wyScMfkS9SVQRH+lN33N/9ZvZSjO6NSDLOe+wxqsnZkia+QrjFC0qy0ITRAsggfqbg==}
+  '@nestjs/platform-express@10.3.10':
+    resolution: {integrity: sha512-wK2ow3CZI2KFqWeEpPmoR300OB6BcBLxARV1EiClJLCj4S1mZsoCmS0YWgpk3j1j6mo0SI8vNLi/cC2iZPEPQA==}
     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.3.10':
+    resolution: {integrity: sha512-i3HAtVQJijxNxJq1k39aelyJlyEIBRONys7IipH/4r8W0J+M1V+y5EKDOyi4j1SdNSb/vmNyWpZ2/ewZjl3kRA==}
     peerDependencies:
       '@nestjs/common': ^10.0.0
       '@nestjs/core': ^10.0.0
@@ -3322,6 +3517,10 @@ packages:
       '@nestjs/platform-express':
         optional: true
 
+  '@noble/hashes@1.4.0':
+    resolution: {integrity: sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==}
+    engines: {node: '>= 16'}
+
   '@nodelib/fs.scandir@2.1.5':
     resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
     engines: {node: '>= 8'}
@@ -3359,19 +3558,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==}
@@ -3379,98 +3578,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
@@ -3485,22 +3696,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'}
@@ -3537,26 +3758,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==}
@@ -3582,8 +3785,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
@@ -3600,141 +3803,144 @@ 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.19.1':
+    resolution: {integrity: sha512-XzqSg714++M+FXhHfXpS1tDnNZNpgxxuGZWlRG/jSj+VEPmZ0yg6jV4E0AL3uyBKxO8mO3xtOsP5mQ+XLfrlww==}
     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.19.1':
+    resolution: {integrity: sha512-thFUbkHteM20BGShD6P08aungq4irbIZKUNbG70LN8RkO7YztcGPiKTTGZS7Kw+x5h8hOXs0i4OaHwFxlpQN6A==}
     cpu: [arm64]
     os: [android]
 
-  '@rollup/rollup-darwin-arm64@4.17.2':
-    resolution: {integrity: sha512-kcMLpE6uCwls023+kknm71ug7MZOrtXo+y5p/tsg6jltpDtgQY1Eq5sGfHcQfb+lfuKwhBmEURDga9N0ol4YPw==}
+  '@rollup/rollup-darwin-arm64@4.19.1':
+    resolution: {integrity: sha512-8o6eqeFZzVLia2hKPUZk4jdE3zW7LCcZr+MD18tXkgBBid3lssGVAYuox8x6YHoEPDdDa9ixTaStcmx88lio5Q==}
     cpu: [arm64]
     os: [darwin]
 
-  '@rollup/rollup-darwin-x64@4.17.2':
-    resolution: {integrity: sha512-AtKwD0VEx0zWkL0ZjixEkp5tbNLzX+FCqGG1SvOu993HnSz4qDI6S4kGzubrEJAljpVkhRSlg5bzpV//E6ysTQ==}
+  '@rollup/rollup-darwin-x64@4.19.1':
+    resolution: {integrity: sha512-4T42heKsnbjkn7ovYiAdDVRRWZLU9Kmhdt6HafZxFcUdpjlBlxj4wDrt1yFWLk7G4+E+8p2C9tcmSu0KA6auGA==}
     cpu: [x64]
     os: [darwin]
 
-  '@rollup/rollup-linux-arm-gnueabihf@4.17.2':
-    resolution: {integrity: sha512-3reX2fUHqN7sffBNqmEyMQVj/CKhIHZd4y631duy0hZqI8Qoqf6lTtmAKvJFYa6bhU95B1D0WgzHkmTg33In0A==}
+  '@rollup/rollup-linux-arm-gnueabihf@4.19.1':
+    resolution: {integrity: sha512-MXg1xp+e5GhZ3Vit1gGEyoC+dyQUBy2JgVQ+3hUrD9wZMkUw/ywgkpK7oZgnB6kPpGrxJ41clkPPnsknuD6M2Q==}
     cpu: [arm]
     os: [linux]
 
-  '@rollup/rollup-linux-arm-musleabihf@4.17.2':
-    resolution: {integrity: sha512-uSqpsp91mheRgw96xtyAGP9FW5ChctTFEoXP0r5FAzj/3ZRv3Uxjtc7taRQSaQM/q85KEKjKsZuiZM3GyUivRg==}
+  '@rollup/rollup-linux-arm-musleabihf@4.19.1':
+    resolution: {integrity: sha512-DZNLwIY4ftPSRVkJEaxYkq7u2zel7aah57HESuNkUnz+3bZHxwkCUkrfS2IWC1sxK6F2QNIR0Qr/YXw7nkF3Pw==}
     cpu: [arm]
     os: [linux]
 
-  '@rollup/rollup-linux-arm64-gnu@4.17.2':
-    resolution: {integrity: sha512-EMMPHkiCRtE8Wdk3Qhtciq6BndLtstqZIroHiiGzB3C5LDJmIZcSzVtLRbwuXuUft1Cnv+9fxuDtDxz3k3EW2A==}
+  '@rollup/rollup-linux-arm64-gnu@4.19.1':
+    resolution: {integrity: sha512-C7evongnjyxdngSDRRSQv5GvyfISizgtk9RM+z2biV5kY6S/NF/wta7K+DanmktC5DkuaJQgoKGf7KUDmA7RUw==}
     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.19.1':
+    resolution: {integrity: sha512-89tFWqxfxLLHkAthAcrTs9etAoBFRduNfWdl2xUs/yLV+7XDrJ5yuXMHptNqf1Zw0UCA3cAutkAiAokYCkaPtw==}
     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.19.1':
+    resolution: {integrity: sha512-PromGeV50sq+YfaisG8W3fd+Cl6mnOOiNv2qKKqKCpiiEke2KiKVyDqG/Mb9GWKbYMHj5a01fq/qlUR28PFhCQ==}
     cpu: [ppc64]
     os: [linux]
 
-  '@rollup/rollup-linux-riscv64-gnu@4.17.2':
-    resolution: {integrity: sha512-BOaNfthf3X3fOWAB+IJ9kxTgPmMqPPH5f5k2DcCsRrBIbWnaJCgX2ll77dV1TdSy9SaXTR5iDXRL8n7AnoP5cg==}
+  '@rollup/rollup-linux-riscv64-gnu@4.19.1':
+    resolution: {integrity: sha512-/1BmHYh+iz0cNCP0oHCuF8CSiNj0JOGf0jRlSo3L/FAyZyG2rGBuKpkZVH9YF+x58r1jgWxvm1aRg3DHrLDt6A==}
     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.19.1':
+    resolution: {integrity: sha512-0cYP5rGkQWRZKy9/HtsWVStLXzCF3cCBTRI+qRL8Z+wkYlqN7zrSYm6FuY5Kd5ysS5aH0q5lVgb/WbG4jqXN1Q==}
     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.19.1':
+    resolution: {integrity: sha512-XUXeI9eM8rMP8aGvii/aOOiMvTs7xlCosq9xCjcqI9+5hBxtjDpD+7Abm1ZhVIFE1J2h2VIg0t2DX/gjespC2Q==}
     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.19.1':
+    resolution: {integrity: sha512-V7cBw/cKXMfEVhpSvVZhC+iGifD6U1zJ4tbibjjN+Xi3blSXaj/rJynAkCFFQfoG6VZrAiP7uGVzL440Q6Me2Q==}
     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.19.1':
+    resolution: {integrity: sha512-88brja2vldW/76jWATlBqHEoGjJLRnP0WOEKAUbMcXaAZnemNhlAHSyj4jIwMoP2T750LE9lblvD4e2jXleZsA==}
     cpu: [arm64]
     os: [win32]
 
-  '@rollup/rollup-win32-ia32-msvc@4.17.2':
-    resolution: {integrity: sha512-7II/QCSTAHuE5vdZaQEwJq2ZACkBpQDOmQsE6D6XUbnBHW8IAhm4eTufL6msLJorzrHDFv3CF8oCA/hSIRuZeQ==}
+  '@rollup/rollup-win32-ia32-msvc@4.19.1':
+    resolution: {integrity: sha512-LdxxcqRVSXi6k6JUrTah1rHuaupoeuiv38du8Mt4r4IPer3kwlTo+RuvfE8KzZ/tL6BhaPlzJ3835i6CxrFIRQ==}
     cpu: [ia32]
     os: [win32]
 
-  '@rollup/rollup-win32-x64-msvc@4.17.2':
-    resolution: {integrity: sha512-TGGO7v7qOq4CYmSBVEYpI1Y5xDuCEnbVC5Vth8mOsW0gDSzxNrVERPc790IGHsrT2dQSimgMr9Ub3Y1Jci5/8w==}
+  '@rollup/rollup-win32-x64-msvc@4.19.1':
+    resolution: {integrity: sha512-2bIrL28PcK3YCqD9anGxDxamxdiJAxA+l7fWIwM5o8UqNy1t3d1NdAweO2XhA0KTDJ5aH1FsuiT5+7VhtHliXg==}
     cpu: [x64]
     os: [win32]
 
-  '@rushstack/node-core-library@4.1.0':
-    resolution: {integrity: sha512-qz4JFBZJCf1YN5cAXa1dP6Mki/HrsQxc/oYGAGx29dF2cwF2YMxHoly0FBhMw3IEnxo5fMj0boVfoHVBkpkx/w==}
+  '@rushstack/node-core-library@5.5.1':
+    resolution: {integrity: sha512-ZutW56qIzH8xIOlfyaLQJFx+8IBqdbVCZdnj+XT1MorQ1JqqxHse8vbCpEM+2MjsrqcbxcgDIbfggB1ZSQ2A3g==}
     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.13.3':
+    resolution: {integrity: sha512-fc3zjXOw8E0pXS5t9vTiIPx9gHA0fIdTXsu9mT4WbH+P3mYvnrX0iAQ5a6NvyK1+CqYWBTw/wVNx7SDJkI+WYQ==}
     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.3':
+    resolution: {integrity: sha512-edMpWB3QhFFZ4KtSzS8WNjBgR4PXPPOVrOHMbb7kNpmQ1UFS9HdVtjCXg1H5fG+xYAbeE+TMPcVPUyX2p84STA==}
 
-  '@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.12.0':
+    resolution: {integrity: sha512-mc1cLbm6UQ8RxLc0dZES7v5rkH+99LxQp/ZvTqV3NLyYsO/fD6JhEflP1H5b2SDq9gI0+0G36AVZWxvounfR9w==}
 
   '@sideway/address@4.1.4':
     resolution: {integrity: sha512-7vwq+rOHVWjyXxVlR76Agnvhy8I9rpzjosTESvmhNeXOXdZZB15Fl+TI9x1SiHZH5Jv2wTGduSxFDIaq0m3DUw==}
@@ -3745,8 +3951,8 @@ packages:
   '@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':
@@ -3763,9 +3969,17 @@ 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@2.3.0':
+    resolution: {integrity: sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==}
+    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==}
@@ -3785,275 +3999,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.2.6':
+    resolution: {integrity: sha512-iCsf3V28/jJ95w2zd8aSvR4denoA2UYV3fpNCTGOURqICyKOG3cyVxvqKp8Hhcwn7trNOsK+HlL6q5gpv56ViA==}
     peerDependencies:
-      react: ^16.8.0 || ^17.0.0 || ^18.0.0
+      storybook: ^8.2.6
+
+  '@storybook/addon-backgrounds@8.2.6':
+    resolution: {integrity: sha512-61NFowA6EmCw+Eyzp0U4fat9MlPDdnT7aoDyzqSImLwWLITY9IvmWuTeo7XKJZN3fe22z1r7cZseKdYrtaHcKw==}
+    peerDependencies:
+      storybook: ^8.2.6
+
+  '@storybook/addon-controls@8.2.6':
+    resolution: {integrity: sha512-EHUwHy+oZZv3pXzN7fuXWrS/meHFjqcELY3RBvOyEkGf21agl6co6R1tnf6d5N5QoYAGfIbDO7dkauSL2RfNAw==}
+    peerDependencies:
+      storybook: ^8.2.6
+
+  '@storybook/addon-docs@8.2.6':
+    resolution: {integrity: sha512-qe7hxntaezqjKdU9QS+Q9NFL6i/uNdBxdvOnCKgPhBAY/zY6yhk5t3sOvonynPK5nkaNAowfSNPIzNxAXlJ1sA==}
+    peerDependencies:
+      storybook: ^8.2.6
+
+  '@storybook/addon-essentials@8.2.6':
+    resolution: {integrity: sha512-diGjGZcZNov+RCAVQBTm8JKP2kUtMRuJIQFBeXdPWpu6hYBk6lw1FlAf2GywWGCvdny1pJT90hfoD33qUMNuDg==}
+    peerDependencies:
+      storybook: ^8.2.6
+
+  '@storybook/addon-highlight@8.2.6':
+    resolution: {integrity: sha512-03cV9USsfP3bS4wYV06DYcIaGPfoheQe53Q0Jr1B2yJUVyIPKvmO2nGjLBsqzeL3Wl7vSfLQn0/dUdxCcbqLsw==}
+    peerDependencies:
+      storybook: ^8.2.6
+
+  '@storybook/addon-interactions@8.2.6':
+    resolution: {integrity: sha512-YXpHf8jWPz9HJV+Fw4GaunaCWeE6uqF24aLXdAd8xuhN1UfWJeNV6AwAvFQ0hTLqvmz0yMhX/5JXDKeKESoYDA==}
+    peerDependencies:
+      storybook: ^8.2.6
+
+  '@storybook/addon-links@8.2.6':
+    resolution: {integrity: sha512-CUuU3nk8wyZ3bljCmOG/OCKazan+bPuNbCph8N763zyzdEx5M/CbBxV9d3pi3zjYpix7txlqrl2/YdMCejfyFw==}
+    peerDependencies:
+      react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta
+      storybook: ^8.2.6
     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.2.6':
+    resolution: {integrity: sha512-PFVfJeuydxlV1VmxEuKNQ7z2vCDvQtHa2GB0ANM11ahxDSUz8QxsO0Y/L3LOn2JjJGYiVFrsHAaC+8NW43iArQ==}
     peerDependencies:
-      react: ^16.8.0 || ^17.0.0 || ^18.0.0
-      react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
+      storybook: ^8.2.6
+
+  '@storybook/addon-measure@8.2.6':
+    resolution: {integrity: sha512-neI8YeSOAtOmzasLxo6O8ZLr2ebMaD7XVF+kYatl5+SpyuwwvUGcP9NkKe5S+mB8V2zxFUIsXS74XrhmQhRoaQ==}
+    peerDependencies:
+      storybook: ^8.2.6
+
+  '@storybook/addon-outline@8.2.6':
+    resolution: {integrity: sha512-uAlPtqDWlq7MQQ4zJT80qdjbSdLF/zsvtPhidX6h9cjLKNPWAv79xJQ14AJHaMv+Hzy5xKnM4wdEhgPbzKabQg==}
+    peerDependencies:
+      storybook: ^8.2.6
+
+  '@storybook/addon-storysource@8.2.6':
+    resolution: {integrity: sha512-8H2kvRIM12oXN4kO/oowABu88IOY9Je7PphCUUs/nIfTIB+Ck1GrLxx5fCNNCSwUrTBEZY8bDOfxmkf9d4qngw==}
+    peerDependencies:
+      storybook: ^8.2.6
+
+  '@storybook/addon-toolbars@8.2.6':
+    resolution: {integrity: sha512-0JmRirMpxHS6VZzBk0kY871xWTpkk3TN4S1sxoFf5fcnCfVTHDjEJ5Ws/QWru1RJlIZHuJKRdQIA6Vuq5X+KfQ==}
+    peerDependencies:
+      storybook: ^8.2.6
+
+  '@storybook/addon-viewport@8.2.6':
+    resolution: {integrity: sha512-IAxH9H8tVFzSmZhKf5E+EALiAdkp19RzGqP/rWluD8LH7oW5HumQE/4oN0ZhVMy1RxYsCKFYjWyAp7AuxeMRSw==}
+    peerDependencies:
+      storybook: ^8.2.6
+
+  '@storybook/blocks@8.2.6':
+    resolution: {integrity: sha512-nMlZJjVTyfOJ6xwORptsNuS1AZZlDbJUVXc2R8uukGd5GIXxxCdrPk4NvUsjfQslMT9LhYuFld3z62FATsM2rw==}
+    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.2.6
     peerDependenciesMeta:
       react:
         optional: true
       react-dom:
         optional: true
 
-  '@storybook/builder-manager@8.0.9':
-    resolution: {integrity: sha512-/PxDwZIfMc/PSRZcasb6SIdGr3azIlenzx7dBF7Imt8i4jLHiAf1t00GvghlfJsvsrn4DNp95rbRbXTDyTj7tQ==}
+  '@storybook/builder-manager@8.1.11':
+    resolution: {integrity: sha512-U7bmed4Ayg+OlJ8HPmLeGxLTHzDY7rxmxM4aAs4YL01fufYfBcjkIP9kFhJm+GJOvGm+YJEUAPe5mbM1P/bn0Q==}
 
-  '@storybook/builder-vite@8.0.9':
-    resolution: {integrity: sha512-7hEQFZIIz7VvxdySDpPE96iMvZxQvRZcRdhaNGeE+8Y2pyc3DgYE4WY3sjr+LUoB0a6TYLpAIKqbXwtLz0R+PQ==}
+  '@storybook/builder-vite@8.1.11':
+    resolution: {integrity: sha512-hG4eoNMCPgjZ2Ai+zSmk69zjsyEihe75XbJXtYfGRqjMWtz2+SAUFO54fLc2BD5svcUiTeN+ukWcTrwApyPsKg==}
     peerDependencies:
       '@preact/preset-vite': '*'
       typescript: '>= 4.3.x'
@@ -4067,48 +4333,80 @@ 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/builder-vite@8.2.6':
+    resolution: {integrity: sha512-3PrsPZAedpQUbzRBEl23Fi1zG5bkQD76JsygVwmfiSm4Est4K8kW2AIB2ht9cIfKXh3mfQkyQlxXKHeQEHeQwQ==}
     peerDependencies:
-      react: ^16.8.0 || ^17.0.0 || ^18.0.0
-      react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
+      '@preact/preset-vite': '*'
+      storybook: ^8.2.6
+      typescript: '>= 4.3.x'
+      vite: ^4.0.0 || ^5.0.0
+      vite-plugin-glimmerx: '*'
+    peerDependenciesMeta:
+      '@preact/preset-vite':
+        optional: true
+      typescript:
+        optional: true
+      vite-plugin-glimmerx:
+        optional: true
 
-  '@storybook/core-common@8.0.9':
-    resolution: {integrity: sha512-Jmue+sfHFb4GTYBzyWYw1MygoJiQSfISIrKmNIzAmZ+oR9EOr+jpu/i/bH+uetZ2Hqg1AGhj1VB7OtJp9HQyWw==}
+  '@storybook/channels@8.1.11':
+    resolution: {integrity: sha512-fu5FTqo6duOqtJFa6gFzKbiSLJoia+8Tibn3xFfB6BeifWrH81hc+AZq0lTmHo5qax2G5t8ZN8JooHjMw6k2RA==}
 
-  '@storybook/core-events@8.0.9':
-    resolution: {integrity: sha512-DxSUx7wG9Qe3OFUBnv3OrYq48J8UWNo2DUR5/JecJCtp3n++L4fAEW3J0IF5FfxpQDMQSp1yTNjZ2PaWCMd2ag==}
+  '@storybook/client-logger@8.1.11':
+    resolution: {integrity: sha512-DVMh2usz3yYmlqCLCiCKy5fT8/UR9aTh+gSqwyNFkGZrIM4otC5A8eMXajXifzotQLT5SaOEnM3WzHwmpvMIEA==}
 
-  '@storybook/core-server@8.0.9':
-    resolution: {integrity: sha512-BIe1T5YUBl0GYxEjRoTQsvXD2pyuzL8rPTUD41zlzSQM0R8U6Iant9SzRms4u0+rKUm2mGxxKuODlUo5ewqaGA==}
+  '@storybook/codemod@8.2.6':
+    resolution: {integrity: sha512-+mFJ6R+JhJLpU7VPDlXU5Yn6nqIBq745GaEosnIiFOdNo3jaxJ58wq/sGhbQvoCHPUxMA+sDQvR7pS62YFoLRQ==}
 
-  '@storybook/csf-plugin@8.0.9':
-    resolution: {integrity: sha512-pXaNCNi++kxKsqSWwvx215fPx8cNqvepLVxQ7B69qXLHj80DHn0Q3DFBO3sLXNiQMJ2JK4OYcTxMfuOiyzszKw==}
+  '@storybook/components@8.2.6':
+    resolution: {integrity: sha512-H8ckH1AnLkHtMtvJ3J8LxnmDtHxkJ7NJacGctHMRrsBIvdKTVwlT4su5nAVVJlan/PrEou+jESfw+OjjBYE5PA==}
+    peerDependencies:
+      storybook: ^8.2.6
 
-  '@storybook/csf-tools@8.0.9':
-    resolution: {integrity: sha512-PiNMhL97giLytTdQwuhsZ92buVk4gy9H/8DtrDhUc45/1OmF95gogm6T2Yap729SIFwgpOcuq/U3aVo6d6swVQ==}
+  '@storybook/core-common@8.1.11':
+    resolution: {integrity: sha512-Ix0nplD4I4DrV2t9B+62jaw1baKES9UbR/Jz9LVKFF9nsua3ON0aVe73dOjMxFWBngpzBYWe+zYBTZ7aQtDH4Q==}
+    peerDependencies:
+      prettier: ^2 || ^3
+    peerDependenciesMeta:
+      prettier:
+        optional: true
 
-  '@storybook/csf@0.1.6':
-    resolution: {integrity: sha512-JjWnBptVhBYJ14yq+cHs66BXjykRUWQ5TlD1RhPxMOtavynYyV/Q+QR98/N+XB+mcPtFMm5I2DvNkpj0/Dk8Mw==}
+  '@storybook/core-events@8.1.11':
+    resolution: {integrity: sha512-vXaNe2KEW9BGlLrg0lzmf5cJ0xt+suPjWmEODH5JqBbrdZ67X6ApA2nb6WcxDQhykesWCuFN5gp1l+JuDOBi7A==}
 
-  '@storybook/docs-mdx@3.0.0':
-    resolution: {integrity: sha512-NmiGXl2HU33zpwTv1XORe9XG9H+dRUC1Jl11u92L4xr062pZtrShLmD4VKIsOQujxhhOrbxpwhNOt+6TdhyIdQ==}
+  '@storybook/core-events@8.2.6':
+    resolution: {integrity: sha512-bmtm7sHBExKCSGiCIyhwfHKFIsdrRQqd8ZEb/iNWsR93AxHszcf/adYAVynencdWKipw1haIWBNaiDhnsOBVPA==}
+    peerDependencies:
+      storybook: ^8.2.6
 
-  '@storybook/docs-tools@8.0.9':
-    resolution: {integrity: sha512-OzogAeOmeHea/MxSPKRBWtOQVNSpoq+OOpimO9YRA5h5GBRJ2TUOGT44Gny6QT4ll5AvQA8fIiq9KezKcLekAg==}
+  '@storybook/core-server@8.1.11':
+    resolution: {integrity: sha512-L6dzQTmR0np/kagNONvvlm6lSvF1FNc9js3vxsEEPnEypLbhx8bDZaHmuhmBpYUzKyUMpRVQTE/WgjHLuBBuxA==}
+
+  '@storybook/core@8.2.6':
+    resolution: {integrity: sha512-XY71g3AcpD6IiER9k9Lt+vlUMYfPIYgWekd7e0Ggzz2gJkPuLunKEdQccLGDSHf5OFAobHhrTJc7ZsvWhmDMag==}
+
+  '@storybook/csf-plugin@8.1.11':
+    resolution: {integrity: sha512-hkA8gjFtSN/tabG0cuvmEqanMXtxPr3qTkp4UNSt1R6jBEgFHRG2y/KYLl367kDwOSFTT987ZgRfJJruU66Fvw==}
+
+  '@storybook/csf-plugin@8.2.6':
+    resolution: {integrity: sha512-USn7E/bMQYVqvFBuW6d9rKoSuCImjk0BAmc/0wIOuMQ/yQNp2Xze0m8eVkNHUIUDokyx0TXDjRjwq10Xxk16ag==}
+    peerDependencies:
+      storybook: ^8.2.6
+
+  '@storybook/csf-tools@8.1.11':
+    resolution: {integrity: sha512-6qMWAg/dBwCVIHzANM9lSHoirwqSS+wWmv+NwAs0t9S94M75IttHYxD3IyzwaSYCC5llp0EQFvtXXAuSfFbibg==}
+
+  '@storybook/csf@0.1.11':
+    resolution: {integrity: sha512-dHYFQH3mA+EtnCkHXzicbLgsvzYjcDJ1JWsogbItZogkPHgSJM/Wr71uMkcvw8v9mmCyP4NpXJuu6bPoVsOnzg==}
+
+  '@storybook/csf@0.1.9':
+    resolution: {integrity: sha512-JlZ6v/iFn+iKohKGpYXnMeNeTiiAMeFoDhYnPLIC8GnyyIWqEI9wJYrOK9i9rxlJ8NZAH/ojGC/u/xVC41qSgQ==}
+
+  '@storybook/docs-mdx@3.1.0-next.0':
+    resolution: {integrity: sha512-t4syFIeSyufieNovZbLruPt2DmRKpbwL4fERCZ1MifWDRIORCKLc4NCEHy+IqvIqd71/SJV2k4B51nF7vlJfmQ==}
+
+  '@storybook/docs-tools@8.1.11':
+    resolution: {integrity: sha512-mEXtR9rS7Y+OdKtT/QG6JBGYR1L41mcDhIqhnk7RmYl9qJstVAegrCKWR53sPKFdTVOHU7dmu6k+BD+TqHpyyw==}
 
   '@storybook/global@5.0.0':
     resolution: {integrity: sha512-FcOqPAXACP0I3oJ/ws6/rrPT9WGhu915Cg8D02a9YxLo0DE9zI+a9A5gRGvmQ09fiWPukqI8ZAEoQEdWUKMQdQ==}
@@ -4120,87 +4418,123 @@ packages:
       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.2.6':
+    resolution: {integrity: sha512-RxtpcMTUSq8/wPM6cR6EXVrPEiNuRbC71cIFVFZagOFYvnnOKwSPV+GOLPK0wxMbGB4c5/+Xe8ADefmZTvxOsA==}
     peerDependencies:
-      react: ^16.8.0 || ^17.0.0 || ^18.0.0
-      react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
+      storybook: ^8.2.6
 
-  '@storybook/react-vite@8.0.9':
-    resolution: {integrity: sha512-FT5KeulUH6grfzOJOxJCxpv9+81UVDrT9UPcgiFhQT9rKtsgmltezThwbHknByZNw3WWnf+ieidMLEis9hd73A==}
+  '@storybook/manager-api@8.1.11':
+    resolution: {integrity: sha512-QSgwKfAw01K9YvvZj30iGBMgQ4YaCT3vojmttuqdH5ukyXkiO7pENLJj4Y+alwUeSi0g+SJeadCI3PXySBHOGg==}
+
+  '@storybook/manager-api@8.2.6':
+    resolution: {integrity: sha512-uv36h/b5RhlajWtEg4cVPBYV8gZs6juux0nIE+6G9i7vt8Ild6gM9tW1KNabgZcaHFiyWJYCNWxJZoKjgUmXDg==}
+    peerDependencies:
+      storybook: ^8.2.6
+
+  '@storybook/manager@8.1.11':
+    resolution: {integrity: sha512-e02y9dmxowo7cTKYm9am7UO6NOHoHy6Xi7xZf/UA932qLwFZUtk5pnwIEFaZWI3OQsRUCGhP+FL5zizU7uVZeg==}
+
+  '@storybook/node-logger@8.1.11':
+    resolution: {integrity: sha512-wdzFo7B2naGhS52L3n1qBkt5BfvQjs8uax6B741yKRpiGgeAN8nz8+qelkD25MbSukxvbPgDot7WJvsMU/iCzg==}
+
+  '@storybook/preview-api@8.1.11':
+    resolution: {integrity: sha512-8ZChmFV56GKppCJ0hnBd/kNTfGn2gWVq1242kuet13pbJtBpvOhyq4W01e/Yo14tAPXvgz8dSnMvWLbJx4QfhQ==}
+
+  '@storybook/preview-api@8.2.6':
+    resolution: {integrity: sha512-5vTj2ndX5ng4nDntZYe+r8UwLjCIGFymhq5/r2adAvRKL+Bo4zQDWGO7bhvGJk16do2THb2JvPz49ComW9LLZw==}
+    peerDependencies:
+      storybook: ^8.2.6
+
+  '@storybook/preview@8.1.11':
+    resolution: {integrity: sha512-K/9NZmjnL0D1BROkTNWNoPqgL2UaocALRSqCARmkBLgU2Rn/FuZgEclHkWlYo6pUrmLNK+bZ+XzpNMu12iTbpg==}
+
+  '@storybook/react-dom-shim@8.2.6':
+    resolution: {integrity: sha512-B+x8UAEQPDp1yhN3tMh09NvSL38QNfJB7PAyLgKrfE7xIAzvewq+RLW2DfGkoZCy+Zr7QSHm1p7NOgud8+sQCg==}
+    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.2.6
+
+  '@storybook/react-vite@8.2.6':
+    resolution: {integrity: sha512-BpbteaIzsJZL1QN3iR7uuslrPfdtbZYXPhcU9awpfl5pW5MOQThuvl7728mwT8V7KdANeikJPgsnlETOb/afDA==}
     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.2.6
       vite: ^4.0.0 || ^5.0.0
 
-  '@storybook/react@8.0.9':
-    resolution: {integrity: sha512-NeQ6suZG3HKikwe3Tx9cAIaRx7uP8FKCmlVvIiBg4LTTI5orCt94PPakvuZukZcbkqvcCnEBkebAzwUpn8PiJw==}
+  '@storybook/react@8.2.6':
+    resolution: {integrity: sha512-awJlzfiAMrf8l9AgiLhjXEJ+HvS3VKPxNNQaRwBELGq/vigjJe656tMrhvg4OIlJXtlS+6XPshd2knLwjIWNLw==}
     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.2.6
       typescript: '>= 4.2.x'
     peerDependenciesMeta:
       typescript:
         optional: true
 
-  '@storybook/router@8.0.9':
-    resolution: {integrity: sha512-aAOWxbM9J4mt+cp4o88T2PB29mgBBTOzU37/pUsTHYnKnR9XI4npXEXdN8Gv+ryqM0kj0AbBpz/llFlnR2MNNA==}
+  '@storybook/router@8.1.11':
+    resolution: {integrity: sha512-nU5lsBvy0L8wBYOkjagh29ztZicDATpZNYrHuavlhQ2jznmmHdJvXKYk+VrMAbthjQ6ZBqfeeMNPR1UlnqR5Rw==}
 
-  '@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.2.6':
+    resolution: {integrity: sha512-mOVf+TJhlQywCymFMs7l604CxEZRKZRKVQojrrgU6CH6EhhLx/q6BT8tf1CakY9JO3Ey+PhUMBBCerYiDaHLcQ==}
     peerDependencies:
-      react: ^16.8.0 || ^17.0.0 || ^18.0.0
-      react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
+      storybook: ^8.2.6
+
+  '@storybook/telemetry@8.1.11':
+    resolution: {integrity: sha512-Jqvm7HcZismKzPuebhyLECO6KjGiSk4ycbca1WUM/TUvifxCXqgoUPlHHQEEfaRdHS63/MSqtMNjLsQRLC/vNQ==}
+
+  '@storybook/test@8.2.6':
+    resolution: {integrity: sha512-nTzNxReBcMRlX1+8PNU/MuA9ArFbeQhfZXMBIwJJoHOhnNe1knYpyn1++xINxAHKOh0BBhQ0NIMoKdcGmW3V6w==}
+    peerDependencies:
+      storybook: ^8.2.6
+
+  '@storybook/theming@8.1.11':
+    resolution: {integrity: sha512-Chn/opjO6Rl1isNobutYqAH2PjKNkj09YBw/8noomk6gElSa3JbUTyaG/+JCHA6OG/9kUsqoKDb5cZmAKNq/jA==}
+    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
     peerDependenciesMeta:
       react:
         optional: true
       react-dom:
         optional: true
 
-  '@storybook/types@8.0.9':
-    resolution: {integrity: sha512-ew0EXzk9k4B557P1qIWYrnvUcgaE0WWA5qQS0AU8l+fRTp5nvl9O3SP/zNIB0SN1qDFO7dXr3idTNTyIikTcEQ==}
+  '@storybook/theming@8.2.6':
+    resolution: {integrity: sha512-ICnYuLIVsYifVCMQljdHgrp+5vAquNybHxDGWiPeOxBicotwHF8rLhTckD2CdVQbMp0jk6r6jetvjXbFJ2MbvQ==}
+    peerDependencies:
+      storybook: ^8.2.6
 
-  '@storybook/vue3-vite@8.0.9':
-    resolution: {integrity: sha512-IkzYsEyCo5HIvLWbJeGrBu/VIN4u+LvdIAz7vcFqVVXBtTUhy+9/8caLx8fdnM0FWgKcBRQs8HnjBB2V0lOFcg==}
+  '@storybook/types@8.1.11':
+    resolution: {integrity: sha512-k9N5iRuY2+t7lVRL6xeu6diNsxO3YI3lS4Juv3RZ2K4QsE/b3yG5ElfJB8DjHDSHwRH4ORyrU71KkOCUVfvtnw==}
+
+  '@storybook/types@8.2.6':
+    resolution: {integrity: sha512-9Kb5+nui8M7TP/EDGwiuOAHYQPg9U6iQl0OWwgbDIYGBpldwlCwVKAoQWzXz/LlhQijULXIpe1cLvEvJN2Uwhg==}
+    peerDependencies:
+      storybook: ^8.2.6
+
+  '@storybook/vue3-vite@8.1.11':
+    resolution: {integrity: sha512-q0bqh8XEEunaTmp4YiDqM2+YZLwEIevTb5PnNe7G7f2qOiSCE1ncBDnBK717UlCd+iYr34NTztgV2/jIhz1i5w==}
     engines: {node: '>=18.0.0'}
     peerDependencies:
       vite: ^4.0.0 || ^5.0.0
 
-  '@storybook/vue3@8.0.9':
-    resolution: {integrity: sha512-EqVdS62YbOCAE0wJrQKW0sHpM90be8N8Mvmj+HzB0QYhJNtFqP9ehwbcTfwEKtaVGudisHgGBOzNoSKDlxFaag==}
+  '@storybook/vue3@8.1.11':
+    resolution: {integrity: sha512-xJtvfLiCOY3UqwDMd0hZdsadPm1q8dwjfM1UN2Q2ssRWNfXzww1oi+Msj902wz9zFZMYVZypfTfgrdRgWmfEjA==}
     engines: {node: '>=18.0.0'}
     peerDependencies:
       vue: ^3.0.0
 
+  '@storybook/vue3@8.2.6':
+    resolution: {integrity: sha512-j4gMuWc1ZDzqWSdf79YswcZmcbhmbByq/6upqxwqXtjv1mHAiBnEs8bbnnylDrzg4GOvBC8w+FjArkzlFA7uXg==}
+    engines: {node: '>=18.0.0'}
+    peerDependencies:
+      storybook: ^8.2.6
+      vue: ^3.0.0
+
   '@swc/cli@0.3.12':
     resolution: {integrity: sha512-h7bvxT+4+UDrLWJLFHt6V+vNAcUNii2G4aGSSotKz1ECEk4MyEh5CWxmeSscwuz5K3i+4DWTgm4+4EyMCQKn+g==}
     engines: {node: '>= 16.14.0'}
@@ -4224,8 +4558,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]
@@ -4236,8 +4576,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]
@@ -4254,8 +4600,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]
@@ -4266,8 +4618,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]
@@ -4278,8 +4636,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]
@@ -4290,8 +4654,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]
@@ -4302,8 +4672,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]
@@ -4314,8 +4690,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]
@@ -4326,8 +4708,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]
@@ -4338,17 +4726,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
@@ -4362,14 +4765,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==}
@@ -4379,16 +4782,16 @@ packages:
     resolution: {integrity: sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw==}
     engines: {node: '>=14.16'}
 
-  '@testing-library/dom@9.3.3':
-    resolution: {integrity: sha512-fB0R+fa3AUqbLHWyxXa2kGVtf1Fe1ZZFr0Zp6AIbIAzXb2mKbEXl+PCQNUOaq5lbTab5tfctfXRNsWXxa2f7Aw==}
-    engines: {node: '>=14'}
+  '@testing-library/dom@10.1.0':
+    resolution: {integrity: sha512-wdsYKy5zupPyLCW2Je5DLHSxSfbIp6h80WoHOQc+RPtmPGA52O9x5MJEkv92Sjonpq+poOAtUKhh1kBGAXBrNA==}
+    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.4.5':
+    resolution: {integrity: sha512-AguB9yvTXmCnySBP1lWjfNNUwpbElsaQ567lt2VdGqAdHtpieLgjmcVyv1q7PMIvLbgpDdkWV5Ydv3FEejyp2A==}
     engines: {node: '>=14', npm: '>=6', yarn: '>=1'}
     peerDependencies:
       '@jest/globals': '>= 28'
@@ -4414,8 +4817,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'
@@ -4434,8 +4837,8 @@ 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':
@@ -4480,12 +4883,6 @@ 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/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==}
 
@@ -4504,9 +4901,6 @@ packages:
   '@types/cookie@0.6.0':
     resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==}
 
-  '@types/cookies@0.9.0':
-    resolution: {integrity: sha512-40Zk8qR147RABiQ7NQnBzWzDcjKzNrntB5BAmeGCb2p/MIyOE+4BVvc17wumsUqUw00bJYqoXFHYygQnEFh4/Q==}
-
   '@types/core-js@2.5.8':
     resolution: {integrity: sha512-VgnAj6tIAhJhZdJ8/IpxdatM8G4OD3VWGlp6xIxUGENZlpbob9Ty4VVdC1FIEp0aK6DBscDDjyzy5FB60TuNqg==}
 
@@ -4519,6 +4913,9 @@ packages:
   '@types/detect-port@1.3.2':
     resolution: {integrity: sha512-xxgAGA2SAU4111QefXPSp5eGbDm/hW6zhvYl9IeEPZEry9F4d66QAHm5qpUXjb6IsevZV/7emAEx5MhP6O192g==}
 
+  '@types/diff@5.2.1':
+    resolution: {integrity: sha512-uxpcuwWJGhe2AR1g8hD9F5OYGCqjqWnBUQFD8gMZsDbv8oPHzxJF6iMO6n8Tk0AdzlxoaaoQhOYlIg/PukVU8g==}
+
   '@types/disposable-email-domains@1.0.2':
     resolution: {integrity: sha512-SDKwyYTjk3y5aZBxxc38yRecpJPjsqn57STz1bNxYYlv4k11bBe7QB8w4llXDTmQXKT1mFvgGmJv+8Zdu3YmJw==}
 
@@ -4555,6 +4952,9 @@ 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==}
 
@@ -4577,17 +4977,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==}
@@ -4604,8 +4998,8 @@ packages:
   '@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==}
@@ -4616,41 +5010,32 @@ 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==}
 
   '@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==}
@@ -4673,18 +5058,14 @@ packages:
   '@types/mysql@2.15.22':
     resolution: {integrity: sha512-wK1pzsJVVAjYCSZWQoWHziQZbNggXFDUEIGf54g4ZM/ERuP86uGdWeKZWMYlqTPMZfHJJvLPyogXGvCOg87yLQ==}
 
-  '@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@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==}
@@ -4701,8 +5082,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/object-assign-deep@0.4.3':
     resolution: {integrity: sha512-d9Gxaj5j1hzrxJ61EFEg13B4g4FgrT/DYtcDWFXPehR8DF2SUZbVMFtZIs8exkVRiqrqBpdTc/lUUZjncsPpMw==}
@@ -4713,8 +5094,8 @@ 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.6':
+    resolution: {integrity: sha512-/2WmmBXHLsfRqzfHW7BNZ8SbYzE8OSk7i3WjFYvfgRHj7S1xj+16Je5fUKv3lVdVzk/zn9TXOqf+avFCFIE0yQ==}
 
   '@types/pg@8.6.1':
     resolution: {integrity: sha512-1Kc4oAGzAl7uqUStZCDvaLFqZrW9qWSjXOmBfdgyBP5La7Us6Mg4GBvRlSoaZMhQF/zSj1C8CtKMBkoiT8eL8w==}
@@ -4725,6 +5106,9 @@ packages:
   '@types/prop-types@15.7.5':
     resolution: {integrity: sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==}
 
+  '@types/proxy-addr@2.0.3':
+    resolution: {integrity: sha512-TgAHHO4tNG3HgLTUhB+hM4iwW6JUNeQHCLnF1DjaDA9c69PN+IasoFu2MYDhubFc+ZIw5c5t9DMtjvrD6R3Egg==}
+
   '@types/pug@2.0.10':
     resolution: {integrity: sha512-Sk/uYFOBAB7mb74XcpizmH0KOR2Pv3D2Hmrh1Dmy5BmK3MpdSa5kqZcg6EKBdklU0bFXX9gCfzvpnyUehrPIuA==}
 
@@ -4815,9 +5199,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==}
 
@@ -4830,8 +5220,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.11':
+    resolution: {integrity: sha512-4+q7P5h3SpJxaBft0Dzpbr6lmMaqh0Jr2tbhJZ/luAwvD7ohSCniYkwz/pLxuT2h0EOa6QADgJj1Ko+TzRfZ+w==}
 
   '@types/yargs-parser@21.0.0':
     resolution: {integrity: sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==}
@@ -4875,8 +5265,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
@@ -4916,8 +5306,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
@@ -4938,8 +5328,8 @@ packages:
     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':
@@ -4972,8 +5362,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
@@ -4994,8 +5384,8 @@ packages:
     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':
@@ -5025,8 +5415,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: '*'
@@ -5052,8 +5442,8 @@ packages:
     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
@@ -5070,87 +5460,81 @@ packages:
     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.0':
+    resolution: {integrity: sha512-QMRxARyrdiwi1mj3AW4fLByoHTavreXq0itdEW696EihXglf1MB3D4C2gBvE0jMPH29ZjC3iK8aIaUMLf4EOGA==}
     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/runner@1.6.0':
+    resolution: {integrity: sha512-P4xgwPjwesuBiHisAVz/LSSZtDjOTPYZVmNAnpHHSR6ONrf8eCJOFRvUwdHn30F5M1fxhqtl7QZQUk2dprIXAg==}
 
-  '@vitest/runner@0.34.6':
-    resolution: {integrity: sha512-1CUQgtJSLF47NnhN+F9X2ycxUP0kLHQ/JWvNHbeBfwW8CzEGgeskzNnHDyv1ieKTltuR6sdIHV+nmR6kPxQqzQ==}
-
-  '@vitest/snapshot@0.34.6':
-    resolution: {integrity: sha512-B3OZqYn6k4VaN011D+ve+AA4whM4QkcwcrwaKwAbyyvS/NB1hCWjFIBQxAQQSQir9/RtyAAGuq+4RJmbn2dH4w==}
-
-  '@vitest/spy@0.34.6':
-    resolution: {integrity: sha512-xaCvneSaeBw/cz8ySmF7ZwGvL0lBjfvqc1LpQ/vcdHEvpLn3Ff1vAvjw+CoGn0802l++5L/pxb7whwcWAw+DUQ==}
-
-  '@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/utils@1.6.0':
     resolution: {integrity: sha512-21cPiuGMoMZwiOHa2i4LXkMkMkCGzA+MVFV70jRwHo95dL4x/ts5GZhML1QWuy7yfp3WzK3lRvZi3JnXTYqrBw==}
 
   '@volar/language-core@2.2.0':
     resolution: {integrity: sha512-a8WG9+4OdeNDW4ywABZIM6S6UN7em8uIlM/BZ2pWQUYrVmX+m8sj/X+QadvO+Li/t/LjAqbWJQtVgxdpEWLALQ==}
 
+  '@volar/language-core@2.4.0-alpha.18':
+    resolution: {integrity: sha512-JAYeJvYQQROmVRtSBIczaPjP3DX4QW1fOqW1Ebs0d3Y3EwSNRglz03dSv0Dm61dzd0Yx3WgTW3hndDnTQqgmyg==}
+
   '@volar/source-map@2.2.0':
     resolution: {integrity: sha512-HQlPRlHOVqCCHK8wI76ZldHkEwKsjp7E6idUc36Ekni+KJDNrqgSqPvyHQixybXPHNU7CI9Uxd9/IkxO7LuNBw==}
 
+  '@volar/source-map@2.4.0-alpha.18':
+    resolution: {integrity: sha512-MTeCV9MUwwsH0sNFiZwKtFrrVZUK6p8ioZs3xFzHc2cvDXHWlYN3bChdQtwKX+FY2HG6H3CfAu1pKijolzIQ8g==}
+
   '@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.0-alpha.18':
+    resolution: {integrity: sha512-sXh5Y8sqGUkgxpMWUGvRXggxYHAVxg0Pa1C42lQZuPDrW6vHJPR0VCK8Sr7WJsAW530HuNQT/ZIskmXtxjybMQ==}
 
-  '@vue/compiler-core@3.4.25':
-    resolution: {integrity: sha512-Y2pLLopaElgWnMNolgG8w3C5nNUVev80L7hdQ5iIKPtMJvhVpG0zhnBG/g3UajJmZdvW0fktyZTotEHD1Srhbg==}
+  '@vue/compiler-core@3.4.31':
+    resolution: {integrity: sha512-skOiodXWTV3DxfDhB4rOf3OGalpITLlgCeOwb+Y9GJpfQ8ErigdBUHomBzvG78JoVE8MJoQsb+qhZiHfKeNeEg==}
 
-  '@vue/compiler-core@3.4.26':
-    resolution: {integrity: sha512-N9Vil6Hvw7NaiyFUFBPXrAyETIGlQ8KcFMkyk6hW1Cl6NvoqvP+Y8p1Eqvx+UdqsnrnI9+HMUEJegzia3mhXmQ==}
+  '@vue/compiler-core@3.4.34':
+    resolution: {integrity: sha512-Z0izUf32+wAnQewjHu+pQf1yw00EGOmevl1kE+ljjjMe7oEfpQ+BI3/JNK7yMB4IrUsqLDmPecUrpj3mCP+yJQ==}
 
-  '@vue/compiler-dom@3.4.21':
-    resolution: {integrity: sha512-IZC6FKowtT1sl0CR5DpXSiEB5ayw75oT2bma1BEhV7RRR1+cfwLrxc2Z8Zq/RGFzJ8w5r9QtCOvTjQgdn0IKmA==}
+  '@vue/compiler-core@3.4.37':
+    resolution: {integrity: sha512-ZDDT/KiLKuCRXyzWecNzC5vTcubGz4LECAtfGPENpo0nrmqJHwuWtRLxk/Sb9RAKtR9iFflFycbkjkY+W/PZUQ==}
 
-  '@vue/compiler-dom@3.4.25':
-    resolution: {integrity: sha512-Ugz5DusW57+HjllAugLci19NsDK+VyjGvmbB2TXaTcSlQxwL++2PETHx/+Qv6qFwNLzSt7HKepPe4DcTE3pBWg==}
+  '@vue/compiler-dom@3.4.34':
+    resolution: {integrity: sha512-3PUOTS1h5cskdOJMExCu2TInXuM0j60DRPpSCJDqOCupCfUZCJoyQmKtRmA8EgDNZ5kcEE7vketamRZfrEuVDw==}
 
-  '@vue/compiler-dom@3.4.26':
-    resolution: {integrity: sha512-4CWbR5vR9fMg23YqFOhr6t6WB1Fjt62d6xdFPyj8pxrYub7d+OgZaObMsoxaF9yBUHPMiPFK303v61PwAuGvZA==}
+  '@vue/compiler-dom@3.4.37':
+    resolution: {integrity: sha512-rIiSmL3YrntvgYV84rekAtU/xfogMUJIclUMeIKEtVBFngOL3IeZHhsH3UaFEgB5iFGpj6IW+8YuM/2Up+vVag==}
 
-  '@vue/compiler-sfc@3.4.26':
-    resolution: {integrity: sha512-It1dp+FAOCgluYSVYlDn5DtZBxk1NCiJJfu2mlQqa/b+k8GL6NG/3/zRbJnHdhV2VhxFghaDq5L4K+1dakW6cw==}
+  '@vue/compiler-sfc@3.4.37':
+    resolution: {integrity: sha512-vCfetdas40Wk9aK/WWf8XcVESffsbNkBQwS5t13Y/PcfqKfIwJX2gF+82th6dOpnpbptNMlMjAny80li7TaCIg==}
 
-  '@vue/compiler-ssr@3.4.26':
-    resolution: {integrity: sha512-FNwLfk7LlEPRY/g+nw2VqiDKcnDTVdCfBREekF8X74cPLiWHUX6oldktf/Vx28yh4STNy7t+/yuLoMBBF7YDiQ==}
+  '@vue/compiler-ssr@3.4.37':
+    resolution: {integrity: sha512-TyAgYBWrHlFrt4qpdACh8e9Ms6C/AZQ6A6xLJaWrCL8GCX5DxMzxyeFAEMfU/VFr4tylHm+a2NpfJpcd7+20XA==}
+
+  '@vue/compiler-vue2@2.7.16':
+    resolution: {integrity: sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A==}
 
   '@vue/devtools-api@6.6.1':
     resolution: {integrity: sha512-LgPscpE3Vs0x96PzSSB4IGVSZXZBZHpfxs+ZA1d+VEPwHdOXowy/Y2CsvCAIFrf+ssVU1pD1jidj505EpUnfbA==}
@@ -5163,28 +5547,36 @@ 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.0.29':
+    resolution: {integrity: sha512-o2qz9JPjhdoVj8D2+9bDXbaI4q2uZTHQA/dbyZT4Bj1FR9viZxDJnLcKVHfxdn6wsOzRgpqIzJEEmSSvgMvDTQ==}
     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/runtime-core@3.4.37':
+    resolution: {integrity: sha512-MNjrVoLV/sirHZoD7QAilU1Ifs7m/KJv4/84QVbE6nyAZGQNVOa1HGxaOzp9YqCG+GpLt1hNDC4RbH+KtanV7w==}
 
-  '@vue/shared@3.4.26':
-    resolution: {integrity: sha512-Fg4zwR0GNnjzodMt3KRy2AWGMKQXByl56+4HjN87soxLNU9P5xcJkstAlIeEF3cU6UYOzmJl1tV0dVPGIljCnQ==}
+  '@vue/runtime-dom@3.4.37':
+    resolution: {integrity: sha512-Mg2EwgGZqtwKrqdL/FKMF2NEaOHuH+Ks9TQn3DHKyX//hQTYOun+7Tqp1eo0P4Ds+SjltZshOSRq6VsU0baaNg==}
+
+  '@vue/server-renderer@3.4.37':
+    resolution: {integrity: sha512-jZ5FAHDR2KBq2FsRUJW6GKDOAG9lUTX8aBEGq4Vf6B/35I9fPce66BornuwmqmKgfiSlecwuOb6oeoamYMohkg==}
+    peerDependencies:
+      vue: 3.4.37
+
+  '@vue/shared@3.4.31':
+    resolution: {integrity: sha512-Yp3wtJk//8cO4NItOPpi3QkLExAr/aLBGZMmTtW9WpdwBCJpRM6zj9WgWktXAl8IDIozwNMByT45JP3tO3ACWA==}
+
+  '@vue/shared@3.4.34':
+    resolution: {integrity: sha512-x5LmiRLpRsd9KTjAB8MPKf0CDPMcuItjP0gbNqFCIgL1I8iYp4zglhj9w9FPCdIbHG2M91RVeIbArFfFTz9I3A==}
+
+  '@vue/shared@3.4.37':
+    resolution: {integrity: sha512-nIh8P2fc3DflG8+5Uw8PT/1i17ccFn0xxN/5oE9RfV5SVnd7G0XEFRwakrnNFE/jlS95fpGXDVG5zDETS26nmg==}
 
   '@vue/test-utils@2.4.1':
     resolution: {integrity: sha512-VO8nragneNzUZUah6kOjiFmD/gwRjUauG9DROh6oaOeFwX1cZRUNHhdeogE8635cISigXFTtGLUQWx5KCb0xeg==}
@@ -5255,8 +5647,8 @@ 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
 
@@ -5280,9 +5672,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:
@@ -5301,12 +5693,26 @@ packages:
       ajv:
         optional: true
 
+  ajv-formats@3.0.1:
+    resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==}
+    peerDependencies:
+      ajv: ^8.0.0
+    peerDependenciesMeta:
+      ajv:
+        optional: true
+
   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'}
@@ -5389,6 +5795,9 @@ 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==}
 
@@ -5464,6 +5873,9 @@ packages:
   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==}
 
@@ -5485,8 +5897,8 @@ packages:
   avvio@8.3.0:
     resolution: {integrity: sha512-VBVH0jubFr9LdFASy/vNtm5giTrnbVquWBhT0fyizuNK2rQ7e7ONU2plZQWUNqtE1EmxFEb+kbSkFRkstiaS9Q==}
 
-  aws-sdk-client-mock@3.0.1:
-    resolution: {integrity: sha512-9VAzJLl8mz99KP9HjOm/93d8vznRRUTpJooPBOunRdUAnVYopCe9xmMuu7eVemu8fQ+w6rP7o5bBK1kAFkB2KQ==}
+  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==}
@@ -5525,18 +5937,18 @@ 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==}
+  babel-plugin-polyfill-corejs2@0.4.11:
+    resolution: {integrity: sha512-sMEJ27L0gRHShOh5G54uAAPaiCOygY/5ratXuiyb2G46FmlSpc9eFCzYVyDiPxfNbwzA7mYahmjQc5q+CZQ09Q==}
     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==}
+  babel-plugin-polyfill-corejs3@0.10.4:
+    resolution: {integrity: sha512-25J6I8NGfa5YkCDogHRID3fVCadIR8/pGl1/spvCkzb6lVn6SR3ojpx9nOn9iEBcUsjY24AmdKm5khcfKdylcg==}
     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==}
+  babel-plugin-polyfill-regenerator@0.6.2:
+    resolution: {integrity: sha512-2R25rQZWP63nGwaAswvDazbPXfrM3HwVoBXK6HcqeKrSrL/JqcC/rDcf95l4r7LXLyxDXc8uQDa064GubtCABg==}
     peerDependencies:
       '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0
 
@@ -5609,10 +6021,6 @@ 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==}
     engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16}
@@ -5637,15 +6045,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}
@@ -5689,8 +6098,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.10.4:
+    resolution: {integrity: sha512-YEssEbWBbPXvSW2YMjIBKZdkIPZsOaTGWo1y2wpCFv/wUY+tRLKiSVuHgv09x0QEieybx844f9//UWuarG1JHg==}
 
   buraha@0.0.1:
     resolution: {integrity: sha512-G563A0mTbzknm2jDaNxfZuNKIdeArs8T+XQN6t+KbmgnOoevXSXhKDkyf8Md/36Jrx99ikwbCag37VGe3myExQ==}
@@ -5727,6 +6140,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'}
@@ -5816,8 +6233,8 @@ packages:
   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.3:
+    resolution: {integrity: sha512-qK1gkGSRYcJzqrrzdR6a+I0vQ4/R+SoODXyAjscQ/4mzuNzySaMCd+hyVxitSY1+L2fjPD1Gbn+ibNqRmwQeLw==}
     engines: {pnpm: '>=8'}
 
   chartjs-adapter-date-fns@3.0.0:
@@ -5859,15 +6276,12 @@ packages:
     resolution: {integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==}
     engines: {node: '>= 8.10.0'}
 
-  chownr@1.1.4:
-    resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==}
-
   chownr@2.0.0:
     resolution: {integrity: sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==}
     engines: {node: '>=10'}
 
-  chromatic@11.3.0:
-    resolution: {integrity: sha512-q1ZtJDJrjLGnz60ivpC16gmd7KFzcaA4eTb7gcytCqbaKqlHhCFr1xQmcUDsm14CK7JsqdkFU6S+JQdOd2ZNJg==}
+  chromatic@11.5.6:
+    resolution: {integrity: sha512-ycX/hlZLs69BltwwBNsEXr+As6x5/0rlwp6W/CiHMZ3tpm7dmkd+hQCsb8JGHb1h49W3qPOKQ/Lh9evqcJ1yeQ==}
     hasBin: true
     peerDependencies:
       '@chromatic-com/cypress': ^0.*.* || ^1.0.0
@@ -5902,10 +6316,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'}
@@ -6018,8 +6428,8 @@ 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==}
@@ -6082,8 +6492,8 @@ packages:
     resolution: {integrity: sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==}
     engines: {node: '>= 0.6'}
 
-  core-js-compat@3.33.3:
-    resolution: {integrity: sha512-cNzGqFsh3Ot+529GIXacjTJ7kegdt5fPXxCBVS1G0iaZpuo/tBz399ymceLJveQhFFZ8qThHiP3fzuoQjKN2ow==}
+  core-js-compat@3.37.1:
+    resolution: {integrity: sha512-9TNiImhKvQqSUkOvk/mMRZzOANTiEVC7WaBNhHcKM7x+/5E1l5NvsysR19zuDQScE8k+kfQXWRN3AtS/eOSHpg==}
 
   core-util-is@1.0.2:
     resolution: {integrity: sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==}
@@ -6113,8 +6523,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.1:
+    resolution: {integrity: sha512-Y9ciurIuK6G1vy0ErHC8Gt6wHWvsHWJ5fgE60GL6vsuF2WzHwDpH7F1yof40XAEheeSN4v3rD09D1VZ7kiiSOA==}
 
   cross-env@7.0.3:
     resolution: {integrity: sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==}
@@ -6134,9 +6544,9 @@ 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'}
+  crypto-random-string@4.0.0:
+    resolution: {integrity: sha512-x8dy3RnvYdlUcPOjkEHqozhiwzKNSq7GcPuXFbnyMOCHxX8V3OgIg/pYuabl2sbUPfIJaeAQB7PMOK8DFIdoRA==}
+    engines: {node: '>=12'}
 
   css-declaration-sorter@7.2.0:
     resolution: {integrity: sha512-h70rUM+3PNFuaBDTLe8wF/cdWu+dOZmb7pJt8Z2sedYbAcQVQV/tEchueg3GWxwqS0cxtbxmaHEdkNACqcvsow==}
@@ -6196,13 +6606,8 @@ packages:
   csstype@3.1.3:
     resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
 
-  cypress@13.7.3:
-    resolution: {integrity: sha512-uoecY6FTCAuIEqLUYkTrxamDBjMHTYak/1O7jtgwboHiTnS1NaMOoR08KcTrbRZFCBvYOiS4tEkQRmsV+xcrag==}
-    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.13.1:
+    resolution: {integrity: sha512-8F9UjL5MDUdgC/S5hr8CGLHbS5gGht5UOV184qc2pFny43fnkoaKxlzH/U6//zmGu/xRTaKimNfjknLT8+UDFg==}
     engines: {node: ^16.0.0 || ^18.0.0 || >=20.0.0}
     hasBin: true
 
@@ -6253,6 +6658,15 @@ 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
+
   decamelize-keys@1.1.1:
     resolution: {integrity: sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg==}
     engines: {node: '>=0.10.0'}
@@ -6326,10 +6740,6 @@ packages:
   defu@6.1.4:
     resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==}
 
-  del@6.1.1:
-    resolution: {integrity: sha512-ua8BhapfP0JUJKC/zV9yHHDW/rDoDxP4Zhn3AkA6/xT6gY7jYXJiaeyBZznYVujhZZET+UgcbZiQ7sN3WqcImg==}
-    engines: {node: '>=10'}
-
   delayed-stream@1.0.0:
     resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
     engines: {node: '>=0.4.0'}
@@ -6384,6 +6794,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==}
 
@@ -6435,9 +6849,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==}
 
@@ -6455,8 +6866,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
 
@@ -6500,6 +6911,10 @@ 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'}
@@ -6522,8 +6937,8 @@ packages:
   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-set-tostringtag@2.0.1:
     resolution: {integrity: sha512-g3OMbtlwY3QewlqAiMLI47KywjWZoEytKr8pf6iTC8uJq5bIAH52Z9pnQ8pVL6whrCto53JZDuUIsifGeLorTg==}
@@ -6554,11 +6969,16 @@ 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
+
   escalade@3.1.1:
     resolution: {integrity: sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==}
     engines: {node: '>=6'}
@@ -6634,8 +7054,8 @@ packages:
       '@typescript-eslint/parser':
         optional: true
 
-  eslint-plugin-vue@9.25.0:
-    resolution: {integrity: sha512-tDWlx14bVe6Bs+Nnh3IGrD+hb11kf2nukfm6jLsmJIhmiRQ1SUaksvwY9U5MvPB0pcrg0QK0xapQkfITs3RKOA==}
+  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
@@ -6647,20 +7067,32 @@ 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}
-    hasBin: true
+  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@8.57.0:
     resolution: {integrity: sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==}
     engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
     hasBin: 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
+
+  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==}
     engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
@@ -6674,6 +7106,10 @@ packages:
     resolution: {integrity: sha512-JVSoLdTlTDkmjFmab7H/9SL9qGSyjElT3myyKp7krqjVFQCDLmj1QFaCLRFBszBKI0XVZaiiXvuPIX3ZwHe1Ng==}
     engines: {node: '>=0.10'}
 
+  esquery@1.6.0:
+    resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==}
+    engines: {node: '>=0.10'}
+
   esrecurse@4.3.0:
     resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==}
     engines: {node: '>=4.0'}
@@ -6736,6 +7172,10 @@ packages:
     resolution: {integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==}
     engines: {node: '>=16.17'}
 
+  execa@9.3.0:
+    resolution: {integrity: sha512-l6JFbqnHEadBoVAVpN5dl2yCyfX28WoBAGaoQcNmLLSedOxTxcn2Qa83s8I/PA5i56vWru2OHOtrwF7Om2vqlg==}
+    engines: {node: ^18.19.0 || >=20.5.0}
+
   executable@4.1.1:
     resolution: {integrity: sha512-8iA79xD3uAch729dUG8xaaBBFGaEa0wdD2VkYLFHwlqosEj/jT66AzcreRDSgV7ehnNLBW2WR5jIXwGKjVdTLg==}
     engines: {node: '>=4'}
@@ -6751,10 +7191,6 @@ 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==}
     engines: {node: '>= 0.10.0'}
@@ -6817,10 +7253,17 @@ packages:
   fast-uri@2.2.0:
     resolution: {integrity: sha512-cIusKBIt/R/oI6z/1nyfe2FvGKVTohVRfvkOhvx0nCEW+xf5NoCXjAHcWp93uOUBchzYcsvPlrapAdX1uW+YGg==}
 
+  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
 
+  fast-xml-parser@4.4.0:
+    resolution: {integrity: sha512-kLY3jFlwIYwBNDojclKsNAC12sfD6NwW74QB2CoNGPvtVxjliYehVunB3HYyNi+n4Tt1dAcgwYvmKF/Z18flqg==}
+    hasBin: true
+
   fastify-multer@2.0.3:
     resolution: {integrity: sha512-QnFqrRgxmUwWHTgX9uyQSu0C/hmVCfcxopqjApZ4uaZD5W9MJ+nHUlW4+9q7Yd3BRxDIuHvgiM5mjrh6XG8cAA==}
     engines: {node: '>=10.17.0'}
@@ -6835,11 +7278,8 @@ packages:
     resolution: {integrity: sha512-F4o8ZIMVx4YoxGfwrZys6wyjl40gF3Yv6AWWRy62ozFAyZBSS831/uyyCAqKYw3tR73g180ryG98yih6To1PUQ==}
     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@4.28.1:
+    resolution: {integrity: sha512-kFWUtpNr4i7t5vY2EJPCN2KgMVpuqfU4NjnJNCgiNB900oiDeYqaNDRcAfeBbOF5hGixixxcKnOU4KN9z6QncQ==}
 
   fastq@1.17.1:
     resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==}
@@ -6847,6 +7287,9 @@ packages:
   fb-watchman@2.0.2:
     resolution: {integrity: sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==}
 
+  fd-package-json@1.2.0:
+    resolution: {integrity: sha512-45LSPmWf+gC5tdCQMNH4s9Sr00bIkiD9aN7dc5hqkrEw1geRYyDQS1v1oMHAW3ysfxfndqGsrDREHHjNNbKUfA==}
+
   fd-slicer@1.1.0:
     resolution: {integrity: sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==}
 
@@ -6865,10 +7308,18 @@ packages:
     resolution: {integrity: sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==}
     engines: {node: '>=8'}
 
+  figures@6.1.0:
+    resolution: {integrity: sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==}
+    engines: {node: '>=18'}
+
   file-entry-cache@6.0.1:
     resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==}
     engines: {node: ^10.12.0 || >=12.0.0}
 
+  file-entry-cache@8.0.0:
+    resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==}
+    engines: {node: '>=16.0.0'}
+
   file-system-cache@2.3.0:
     resolution: {integrity: sha512-l4DMNdsIPsVnKrgEXbJwDJsA5mB8rGwHYERMgqQx/xAUtChPJMre1bXBzDEqqVbWv9AIbFezXMxeEkZDSrXUOQ==}
 
@@ -6876,8 +7327,8 @@ packages:
     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.3.0:
+    resolution: {integrity: sha512-mROwiKLZf/Kwa/2Rol+OOZQn1eyTkPB3ZTwC0ExY6OLFCbgxHYZvBm7xI77NvfZFMKBsmuXfmLJnD4eEftEhrA==}
     engines: {node: '>=18'}
 
   filelist@1.0.4:
@@ -6895,6 +7346,10 @@ packages:
     resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==}
     engines: {node: '>=8'}
 
+  fill-range@7.1.1:
+    resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==}
+    engines: {node: '>=8'}
+
   finalhandler@1.2.0:
     resolution: {integrity: sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==}
     engines: {node: '>= 0.8'}
@@ -6934,20 +7389,24 @@ packages:
     resolution: {integrity: sha512-MdYSsbdCaIRjzo5edthZtWmEZVMfr1qrtYZUHIdO3swCE+CoZA8S5l0s4jDsYlTa9ZiXv0pTgpzE7s4N8NeUOA==}
     engines: {node: '>=18'}
 
-  flat-cache@3.0.4:
-    resolution: {integrity: sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==}
+  flat-cache@3.2.0:
+    resolution: {integrity: sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==}
     engines: {node: ^10.12.0 || >=12.0.0}
 
-  flatted@3.2.7:
-    resolution: {integrity: sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==}
+  flat-cache@4.0.1:
+    resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==}
+    engines: {node: '>=16'}
+
+  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==}
@@ -6999,9 +7458,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'}
@@ -7022,8 +7478,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:
@@ -7058,10 +7514,6 @@ 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-package-type@0.1.0:
     resolution: {integrity: sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==}
     engines: {node: '>=8.0.0'}
@@ -7082,6 +7534,10 @@ 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'}
@@ -7124,17 +7580,24 @@ packages:
     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@10.4.2:
+    resolution: {integrity: sha512-GwMlUF6PkPo3Gk21UxkCohOv0PLcIXVtKyLlpEI28R/cO/4eNOdmLk3CMW1wROV/WR/EsZOWAfBbBOqYvs88/w==}
+    engines: {node: '>=16 || 14 >=14.18'}
+    hasBin: true
+
+  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==}
@@ -7148,6 +7611,14 @@ packages:
     resolution: {integrity: sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==}
     engines: {node: '>=8'}
 
+  globals@14.0.0:
+    resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==}
+    engines: {node: '>=18'}
+
+  globals@15.8.0:
+    resolution: {integrity: sha512-VZAJ4cewHTExBWDHR6yptdIBlx9YSSZuwojj9Nt5mBRXQzrKakDsVKQ1J63sklLvzAJm0X5+RpO4i3Y2hcOnFw==}
+    engines: {node: '>=18'}
+
   globalthis@1.0.3:
     resolution: {integrity: sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==}
     engines: {node: '>= 0.4'}
@@ -7156,6 +7627,10 @@ packages:
     resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==}
     engines: {node: '>=10'}
 
+  globby@14.0.1:
+    resolution: {integrity: sha512-jOMLD2Z7MAhyG8aJpNOpmziMOP4rPLcc95oQPKXBazW82z+CEgPFBQvEpRUa1KeIMUJo4Wsm+q6uzO/Q/4BksQ==}
+    engines: {node: '>=18'}
+
   gopd@1.0.1:
     resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==}
 
@@ -7167,8 +7642,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:
@@ -7184,10 +7659,6 @@ 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'}
@@ -7200,6 +7671,10 @@ packages:
   happy-dom@10.0.3:
     resolution: {integrity: sha512-WkCP+Z5fX6U5PY+yHP3ElV5D9PoxRAHRWPFq3pG9rg/6Hjf5ak7dozAgSCywsTRUq2qfa8vV8OQvUy5pRXy8EQ==}
 
+  happy-dom@15.6.1:
+    resolution: {integrity: sha512-dsMHLsJHZYhXeExP47B2siAfKNVxptlwFss3/bq/9sG3iBt0P2WYFBq68JgMR5vB5gsN2Ev0feTTPD/+rosUNQ==}
+    engines: {node: '>=18.0.0'}
+
   hard-rejection@2.1.0:
     resolution: {integrity: sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==}
     engines: {node: '>=6'}
@@ -7310,8 +7785,8 @@ 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.3.6:
@@ -7338,6 +7813,14 @@ packages:
     resolution: {integrity: sha512-NmLNjm6ucYwtcUmL7JQC1ZQ57LmHP4lT15FQ8D61nak1rO6DH+fz5qNK2Ap5UN4ZapYICE3/0KodcLYSPsPbaA==}
     engines: {node: '>= 14'}
 
+  https-proxy-agent@7.0.4:
+    resolution: {integrity: sha512-wlwpilI7YdjSkWaQ/7omYBMTliDcmCN8OLihO6I9B86g06lMyAoqgoDpV0XqoaPOKj+0DIdAvnsWfyAAhmimcg==}
+    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'}
@@ -7354,6 +7837,10 @@ packages:
     resolution: {integrity: sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==}
     engines: {node: '>=16.17.0'}
 
+  human-signals@7.0.0:
+    resolution: {integrity: sha512-74kytxOUSvNbjrT9KisAbaTZ/eJwD/LrbM/kh5j0IhPuJzwuA19dWvniFGwBzN9rVjg+O/e+F310PjObDXS+9Q==}
+    engines: {node: '>=18.18.0'}
+
   iconv-lite@0.4.24:
     resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==}
     engines: {node: '>=0.10.0'}
@@ -7371,8 +7858,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.3.1:
@@ -7386,11 +7873,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==}
@@ -7444,13 +7931,13 @@ packages:
     resolution: {integrity: sha512-2YZsvl7jopIa1gaePkeMtd9rAcSjOOjPtpcLlOeusyO+XH2SK5ZcT+UCrElPP+WVIInh2TzeI4XW9ENaSLVVHA==}
     engines: {node: '>=12.22.0'}
 
-  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.1:
+    resolution: {integrity: sha512-V5Nce94SVJ7NtyT/UKUeTM7sY3V7TEk48hURhtBgTiGduOa5t6p9Hd+zBOGvr4Gu7iWPxFVYNl017p0akQA84w==}
+    engines: {node: '>=16.14.0'}
 
   ip-regex@4.3.0:
     resolution: {integrity: sha512-B9ZWJxHHOHUhUjCPrMpLD4xEq35bUTClHM1S6CBU5ixQnkZmwipwgc96vAd7AAGM9TGHvJR+Uss+/Ak6UphK+Q==}
@@ -7514,9 +8001,6 @@ packages:
     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'}
@@ -7548,10 +8032,6 @@ 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'}
@@ -7589,10 +8069,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'}
@@ -7641,12 +8117,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.0.1:
+    resolution: {integrity: sha512-mLYxDsfisQWdS4+gSblAwhATDoNMS/tx8G7BKA+aBIf7F0m1iUwMvuKAo6mW4WMleQAEE50I1Zqef9yMMfHk3w==}
     engines: {node: '>=14.16'}
 
   is-symbol@1.0.4:
@@ -7664,6 +8144,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==}
 
@@ -7720,6 +8204,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'}
@@ -7732,6 +8220,14 @@ packages:
     resolution: {integrity: sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==}
     engines: {node: '>=14'}
 
+  jackspeak@3.4.0:
+    resolution: {integrity: sha512-JVYhQnN59LVPFCEcVa2C3CrEKYacvjRfqIQl+h8oi91aLYQVWRYbxjPcv1bUiUy/kLmQaANrYfNMCO3kuEDHfw==}
+    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'}
@@ -7889,6 +8385,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
@@ -7916,8 +8415,8 @@ packages:
       '@babel/preset-env':
         optional: true
 
-  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
@@ -7966,6 +8465,7 @@ packages:
   json5@2.2.3:
     resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==}
     engines: {node: '>=6'}
+    hasBin: true
 
   jsonc-parser@3.2.0:
     resolution: {integrity: sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==}
@@ -7998,9 +8498,6 @@ 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==}
 
@@ -8081,8 +8578,8 @@ 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:
@@ -8109,9 +8606,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==}
 
@@ -8157,6 +8651,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==}
 
@@ -8189,9 +8687,8 @@ 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'}
+  magicast@0.3.4:
+    resolution: {integrity: sha512-TyDF/Pn36bBji9rWKHlZe+PZb6Mx5V8IHCSxk7X4aljM4e/vyDvZZYwHewdVaqiA0nb3ghfHU/6AUpDxWoER2Q==}
 
   mailcheck@1.1.1:
     resolution: {integrity: sha512-3WjL8+ZDouZwKlyJBMp/4LeziLFXgleOdsYu87piGcMLqhBzCsy2QFdbtAwv757TFC/rtqd738fgJw1tFQCSgA==}
@@ -8235,8 +8732,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'
@@ -8292,8 +8789,8 @@ 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.41.0:
+    resolution: {integrity: sha512-5KcGLxEXD7E+uNO7R68rCbGSHgCqeM3Q3RFFLSsN7ZrIgr8HPDXVAIlP4LHggAZfk0FkSzo8VSXifHCwa2k80g==}
 
   memoizerific@1.11.3:
     resolution: {integrity: sha512-/EuHYwAPdLtXwAwSZkh/Gutery6pD2KYd44oQLhAvQp/50mpyduZh8Q7PYHXTCJ+wuXxt7oij2LXyIJOOYFPog==}
@@ -8404,8 +8901,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.7:
+    resolution: {integrity: sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==}
     engines: {node: '>=8.6'}
 
   mime-db@1.52.0:
@@ -8453,6 +8950,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==}
 
@@ -8514,13 +9015,14 @@ 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@2.1.2:
     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
@@ -8572,13 +9074,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:
@@ -8605,8 +9107,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==}
@@ -8701,8 +9203,8 @@ packages:
     resolution: {integrity: sha512-OSs33Z9yWr148JZcbZd5WiAXhh/n9z8TxQcdMhIOlpN9AhWpLfvVFO73+m77bBABQMaY9XSvIa+qk0jlI7Gcaw==}
     hasBin: true
 
-  node-gyp@10.0.1:
-    resolution: {integrity: sha512-gg3/bHehQfZivQVfqIyy8wTdSymF9yTyP4CJifK73imyNMU8AIGQE2pUa7dNWfmMeG9cDVF2eehiRMv0LC1iAg==}
+  node-gyp@10.1.0:
+    resolution: {integrity: sha512-B4J5M1cABxPc5PwfjhbV5hoy2DP9p8lFXASnEN6hugXOa61416tnTZ29x9sSwAd0o99XNIcpvDDy1swAExsVKA==}
     engines: {node: ^16.14.0 || >=18.0.0}
     hasBin: true
 
@@ -8712,8 +9214,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.14:
+    resolution: {integrity: sha512-Dobp/ebDKBvz91sbtRKhcznLThrKxKt97GI2FAlAyy+fk19j73Uz3sBXolVtmcXjaorivqsbbbjDY+Jkt4/bQA==}
     engines: {node: '>=6.0.0'}
 
   nodemon@3.0.2:
@@ -8721,8 +9223,8 @@ packages:
     engines: {node: '>=10'}
     hasBin: true
 
-  nodemon@3.1.0:
-    resolution: {integrity: sha512-xqlktYlDMCepBJd43ZQhjWwMw2obW/JRvkrLxq5RCNcuDDX1DbcPT+qT1IlIIdf+DhnWs90JpTMe+Y5KxOchvA==}
+  nodemon@3.1.4:
+    resolution: {integrity: sha512-wjPBbFhtpJwmIeY2yP7QF+UKzPfltVGtfce1g/bB15/8vCGZj8uxD62b/b9M9/WVgme0NZudpownKN+c0plXlQ==}
     engines: {node: '>=10'}
     hasBin: true
 
@@ -8763,6 +9265,10 @@ packages:
     resolution: {integrity: sha512-uVFpKhj5MheNBJRTiMZ9pE/7hD1QTeEvugSJW/OmLzAp78PB5O6adfMNTvmfKhXBkvCzC+rqifWcVYpGFwTjnw==}
     engines: {node: '>=14.16'}
 
+  normalize-url@8.0.1:
+    resolution: {integrity: sha512-IO9QvjUMWxPQQhs60oOu10CRkWCiZzSUkzbXGGV9pviYl1fXYcvkzQ5jV9z8Y6un8ARoVRl4EtC6v6jNqbaJ/w==}
+    engines: {node: '>=14.16'}
+
   npm-run-path@2.0.2:
     resolution: {integrity: sha512-lJxZYlT4DW/bRUtFh1MQIWqmLwQfAxnqWG4HhEdjMlkrJYnJn0Jrr2u3mgxqaWsdiBc76TYkTG/mhrnYTuzfHw==}
     engines: {node: '>=4'}
@@ -8775,11 +9281,15 @@ 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}
+
   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==}
 
   oauth2orize-pkce@0.1.2:
     resolution: {integrity: sha512-grto2UYhXHi9GLE3IBgBBbV87xci55+bCyjpVuxKyzol6I5Rg0K1MiTuXE+JZk54R86SG2wqXODMiZYHraPpxw==}
@@ -8868,12 +9378,14 @@ 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:
@@ -8890,8 +9402,8 @@ packages:
   ospath@1.2.2:
     resolution: {integrity: sha512-o6E5qJV5zkAbIDNhGSIlyOhScKXgQrSRMilfph0clDfM0nEnBOlKlH4sWDmG95BW/CvwNz0vmm7dJVtU2KlMiA==}
 
-  otpauth@9.2.3:
-    resolution: {integrity: sha512-oAG55Ch4MBL5Jdg+RXfKiRCZ2lCwa/UIQKsmSfYbGGLSI4dErY1HPZv0JGPPESIYGyDO3s9iJqM4HU/1IppMoQ==}
+  otpauth@9.3.1:
+    resolution: {integrity: sha512-E6d2tMxPofHNk4sRFp+kqW7vQ+WJGO9VLI2N/W00DnI+ThskU12Qa10kyNSGklrzhN5c+wRUsN4GijVgCU2N9w==}
 
   outvariant@1.4.2:
     resolution: {integrity: sha512-Ou3dJ6bA/UJ5GVHxah4LnqDwZRwAmWxrG3wtrHrbGnP4RnLCtA64A4F+ae7Y8ww660JaddSoArUR5HjipWSHAQ==}
@@ -8920,9 +9432,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-limit@5.0.0:
+    resolution: {integrity: sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ==}
+    engines: {node: '>=18'}
 
   p-locate@3.0.0:
     resolution: {integrity: sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==}
@@ -8952,8 +9464,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==}
@@ -8966,6 +9478,10 @@ packages:
   parse-link-header@2.0.0:
     resolution: {integrity: sha512-xjU87V0VyHZybn2RrCX5TIFGxTVZE6zqqZWMPlIKiSKuWh/X5WZdt+w1Ki1nXB+8L/KtL+nZ4iq+sfI6MrhhMw==}
 
+  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==}
 
@@ -9022,9 +9538,13 @@ 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@1.11.1:
+    resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==}
+    engines: {node: '>=16 || 14 >=14.18'}
+
+  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==}
@@ -9042,6 +9562,10 @@ packages:
     resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==}
     engines: {node: '>=8'}
 
+  path-type@5.0.0:
+    resolution: {integrity: sha512-5HviZNaZcfqP95rwpv+1HDgUamezbqdSYTyzjTvwtJSnIH+3vnbmWsItli8OFEndS984VT55M3jduxZbX351gg==}
+    engines: {node: '>=12'}
+
   pathe@1.1.2:
     resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==}
 
@@ -9055,8 +9579,9 @@ packages:
     resolution: {integrity: sha512-YtCKvLUOvwtMGmrniQPdO7MwPjgkFBtFIrmfSbYmYuq3tKDV/mcfAhBth1+C3ru7uXIZasc/pHnb+YDYNkkj4A==}
     engines: {node: '>=14.16'}
 
-  peek-stream@1.1.3:
-    resolution: {integrity: sha512-FhJ+YbOSBb9/rIl2ZeE/QHEsWn7PqNYt8ARAY3kIgNGOk13g9FGyIY6JIl/xB/3TFRVoTv5as0l11weORrTekA==}
+  peek-readable@5.1.3:
+    resolution: {integrity: sha512-kCsc9HwH5RgVA3H3VqkWFyGQwsxUxLdiSX1d5nqAm7hnMFjNFX1VhBLmJoUY0hZNc8gmDNgBkLjfhiWPsziXWA==}
+    engines: {node: '>=14.16'}
 
   pend@1.2.0:
     resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==}
@@ -9083,9 +9608,6 @@ packages:
     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==}
 
@@ -9097,8 +9619,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.12.0:
+    resolution: {integrity: sha512-A+LHUSnwnxrnL/tZ+OLfqR1SxLN3c/pgDztZ47Rpbsd4jUytsTtwQo/TLPRzPJMp/1pbhYVhH9cuSZLAajNfjQ==}
     engines: {node: '>= 8.0.0'}
     peerDependencies:
       pg-native: '>=3.0.1'
@@ -9109,13 +9631,16 @@ 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==}
+
   picomatch@2.3.1:
     resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==}
     engines: {node: '>=8.6'}
@@ -9132,14 +9657,14 @@ packages:
     resolution: {integrity: sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==}
     engines: {node: '>=6'}
 
-  pino-abstract-transport@1.1.0:
-    resolution: {integrity: sha512-lsleG3/2a/JIWUtf9Q5gUNErBqwIu1tUKTT3dUzaf5DySw9ra1wcqKjJjLX1VTY64Wk1eEOYsVGSaGfCK85ekA==}
+  pino-abstract-transport@1.2.0:
+    resolution: {integrity: sha512-Guhh8EZfPCfH+PMXAb6rKOjGQEoy0xlAIn+irODG5kgfYV+BQ0rGYYWTIel3P5mmyXqkYkPmdIkywsn6QKUR1Q==}
 
-  pino-std-serializers@6.1.0:
-    resolution: {integrity: sha512-KO0m2f1HkrPe9S0ldjx7za9BJjeHqBku5Ch8JyxETxT8dEFGz1PwgrHaOQupVYitpzbFSYm7nnljxD8dik2c+g==}
+  pino-std-serializers@7.0.0:
+    resolution: {integrity: sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA==}
 
-  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:
@@ -9360,6 +9885,10 @@ packages:
     resolution: {integrity: sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==}
     engines: {node: ^10 || ^12 || >=14}
 
+  postcss@8.4.40:
+    resolution: {integrity: sha512-YF2kKIUzAofPMpfH6hOi2cGnv/HrUlfucspc7pDyvv7kGdqXrfj8SCl/t8owkEgKEuu8ZcRjSOxFxVLqwChZ2Q==}
+    engines: {node: ^10 || ^12 || >=14}
+
   postgres-array@2.0.0:
     resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==}
     engines: {node: '>=4'}
@@ -9399,8 +9928,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
 
@@ -9420,6 +9949,10 @@ packages:
     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==}
 
@@ -9501,12 +10034,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==}
 
@@ -9531,18 +10067,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'}
@@ -9611,10 +10141,6 @@ 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'}
@@ -9623,8 +10149,8 @@ packages:
     resolution: {integrity: sha512-fUeWjrkOO0t1rg7B2fdyDTvngj+9RlUyL92vOdiB7c0FPguWVsniIMjEtHH+meLBO9rzkUlUzBVXgWrjI8P9LA==}
     engines: {node: '>=12'}
 
-  re2@1.20.10:
-    resolution: {integrity: sha512-/5JjSPXobSDaKFL6rD5Gb4qD4CVBITQb7NAxfQ/NA7o0HER3SJAPV3lPO2kvzw0/PN1pVJNVATEUk4y9j7oIIA==}
+  re2@1.21.3:
+    resolution: {integrity: sha512-GI+KoGkHT4kxTaX+9p0FgNB1XUnCndO9slG5qqeEoZ7kbf6Dk6ohQVpmwKVeSp7LPLn+g6Q3BaCopz4oHuBDuQ==}
 
   react-colorful@5.6.1:
     resolution: {integrity: sha512-1exovf0uGTGyq5mXQT0zgQ80uvj2PCwvF8zY1RN9/vbJVSjSo3fsB/4L3ObbF7u70NduSiK4xu4Y6q1MHoUGEw==}
@@ -9702,10 +10228,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'}
@@ -9821,9 +10343,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
@@ -9856,20 +10375,25 @@ packages:
 
   rimraf@2.6.3:
     resolution: {integrity: sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==}
+    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.19.1:
+    resolution: {integrity: sha512-K5vziVlg7hTpYfFBI+91zHBEMo6jafYXpkMlqZjg7/zhIG9iHqazBf4xz9AVdjS9BruRn280ROqLI7G3OFRIlw==}
     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==}
 
@@ -9905,8 +10429,8 @@ packages:
   sanitize-html@2.13.0:
     resolution: {integrity: sha512-Xff91Z+4Mz5QiNSLdLWwjgBDm5b1RU6xBT0+12rapjiaR7SwfRdjw8f+6Rir2MXKLrDicRFHdb51hGOAxmsUIA==}
 
-  sass@1.76.0:
-    resolution: {integrity: sha512-nc3LeqvF2FNW5xGF1zxZifdW3ffIz5aBb7I7tSvOoNu7z1RQ6pFt9MBuiPtjgaI62YWrM/txjWlOCFiGtf2xpw==}
+  sass@1.77.8:
+    resolution: {integrity: sha512-4UHg6prsrycW20fqLGPShtEvo/WyHRVRHwOP4DzkUrObWoWI05QBSfzU71TVB7PFaL104TwNaHpjlWXAZbQiNQ==}
     engines: {node: '>=14.0.0'}
     hasBin: true
 
@@ -9980,8 +10504,8 @@ packages:
     resolution: {integrity: sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==}
     engines: {node: '>=8'}
 
-  sharp@0.33.3:
-    resolution: {integrity: sha512-vHUeXJU1UvlO/BNwTpT0x/r53WkLUVxrmb5JTgW92fdFCFk0ispLMAeu/jPO2vjkXM1fYUi3K7/qcLF47pwM1A==}
+  sharp@0.33.4:
+    resolution: {integrity: sha512-7i/dt5kGl7qR4gwPRD2biwD2/SvBn3O04J77XKFgL2OnZtQw+AG9wnuS/csmu80nPRHLYE9E41fyEiG8nhH6/Q==}
     engines: {libvips: '>=8.15.2', node: ^18.17.0 || ^20.3.0 || >=21.0.0}
 
   shebang-command@1.2.0:
@@ -10003,8 +10527,8 @@ packages:
   shiki@0.14.7:
     resolution: {integrity: sha512-dNPAPrxSc87ua2sKJ3H5dQ/6ZaY8RNnaAqK+t0eG7p0Soi2ydiqbGOTaZCqaYvA/uZYfS1LJnemt3Q+mSfcPCg==}
 
-  shiki@1.4.0:
-    resolution: {integrity: sha512-5WIn0OL8PWm7JhnTwRWXniy6eEDY234mRrERVlFa646V2ErQqwIFd2UML7e0Pq9eqSKLoMa3Ke+xbsF+DAuy+Q==}
+  shiki@1.12.0:
+    resolution: {integrity: sha512-BuAxWOm5JhRcbSOl7XCei8wGjgJJonnV0oipUupPY58iULxUGyHhW5CF+9FRMuM1pcJ5cGEJGll1LusX6FwpPA==}
 
   shimmer@1.2.1:
     resolution: {integrity: sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw==}
@@ -10022,8 +10546,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==}
@@ -10123,6 +10647,10 @@ packages:
     resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==}
     engines: {node: '>=8'}
 
+  slash@5.1.0:
+    resolution: {integrity: sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==}
+    engines: {node: '>=14.16'}
+
   slice-ansi@3.0.0:
     resolution: {integrity: sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==}
     engines: {node: '>=8'}
@@ -10143,8 +10671,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==}
@@ -10157,10 +10685,6 @@ 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'}
@@ -10204,8 +10728,8 @@ 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==}
@@ -10226,8 +10750,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.4:
+    resolution: {integrity: sha512-CKNeBTcP0hVqIlNismHMudb9q3lLdAjcVPO13/7gfI66fcJpeIb/o4NzQd1JK/CD+lfWVqr10ZH9Y14+OwlJuw==}
     engines: {node: '>=16'}
     hasBin: true
 
@@ -10264,8 +10788,8 @@ packages:
       react-dom:
         optional: true
 
-  storybook@8.0.9:
-    resolution: {integrity: sha512-/Mvij0Br5bUwJpCvqAUZMEDIWmdRxEyllvVj8Ukw5lIWJePxfpSsz4px5jg9+R6B9tO8sQSqjg4HJvQ/pZk8Tg==}
+  storybook@8.2.6:
+    resolution: {integrity: sha512-8j30wDxQmkcqI0fWcSYFsUCjErsY1yTWbTW+yjbwM8DyW18Cud6CwbFRCxjFsH+2M0CjP6Pqs/m1PGI0vcQscQ==}
     hasBin: true
 
   stream-browserify@3.0.0:
@@ -10277,9 +10801,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'}
@@ -10360,6 +10881,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'}
@@ -10372,8 +10897,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==}
@@ -10386,6 +10911,10 @@ packages:
     resolution: {integrity: sha512-pQ+V+nYQdC5H3Q7qBZAz/MO6lwGhoC2gOAjuouGf/VO0m7vQRh8QNMl2Uf6SwAtzZ9bOw3UIeBukEGNJl5dtXQ==}
     engines: {node: '>=14.16'}
 
+  strtok3@8.0.1:
+    resolution: {integrity: sha512-HNkTAnNWQj2YBzfTtoC5OQyu1QwPsMwiB7VyQmNvQKCrmEDSvFB857Vh97UY9InGLNRAB91sdS1ztifRo/3hdA==}
+    engines: {node: '>=16'}
+
   stylehacks@6.1.1:
     resolution: {integrity: sha512-gSTTEQ670cJNoaeIp9KX6lZmm8LJ3jPB5yJmX8Zq/wQxOsAFXV3qjWzHas3YYk1qesuVIyYWWUpZ0vSE/dTSGg==}
     engines: {node: ^14 || ^16 || >=18.0}
@@ -10424,19 +10953,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.22.11:
+    resolution: {integrity: sha512-aLws5yi4KCHTb0BVvbodQY5bY8eW4asMRDTxTW46hqw9lGjACX6TlLdJrkdoHYRB0qs+MekqEq1zG7WDnWE8Ug==}
     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==}
 
@@ -10451,20 +10973,20 @@ 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-dir@3.0.0:
+    resolution: {integrity: sha512-nHc6S/bwIilKHNRgK/3jlhDoIHcp45YgyiwcAk46Tr0LfEqGBVpmiAyuiuxeVE44m3mXnEeVhaipLOEWmH+Njw==}
+    engines: {node: '>=14.16'}
 
   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'}
+  tempy@3.1.0:
+    resolution: {integrity: sha512-7jDLIdD2Zp0bDe5r3D2qtkd1QOCacylBuL7oa4udvN6v2pqr4+LcCr67C8DR1zkpaZ8XosF5m1yQSabKAW6f2g==}
+    engines: {node: '>=14.16'}
 
-  terser@5.30.3:
-    resolution: {integrity: sha512-STdUgOUx8rLbMGO9IOwHLpCqolkDITFFQSMYYwKE1N2lY6MVSaeoi10z/EhWxRc6ybqoVmKSkhKYH/XUpl7vSA==}
+  terser@5.31.3:
+    resolution: {integrity: sha512-pAfYn3NIZLyZpa83ZKigvj6Rn9c/vd5KfYGX7cN1mnzqgDcxWvrU5ZtAfIKhEXz9nRecw4z3LXkjaq96/qZqAA==}
     engines: {node: '>=10'}
     hasBin: true
 
@@ -10488,28 +11010,22 @@ 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.167.0:
+    resolution: {integrity: sha512-9Y1a66fpjqF3rhq7ivKTaKtjQLZ97Hj/lZ00DmZWaKHaQFH4uzYT1znwRDWQOcgMmCcOloQzo61gDmqO8l9xmA==}
 
-  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.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==}
 
@@ -10523,8 +11039,8 @@ packages:
   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'}
 
   tinyspy@2.2.0:
@@ -10549,17 +11065,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'}
@@ -10571,12 +11080,16 @@ 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
 
-  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:
@@ -10638,8 +11151,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:
@@ -10649,8 +11162,8 @@ 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.1:
+    resolution: {integrity: sha512-sSL84A0SFwx2xGMWrxlGaarKFSQszWjJS2vgNDDLwatytzg2aq6ShlwHsBYxRNmjzXISODwMva5ZOdAg/4AoOA==}
     engines: {node: '>=14.16'}
     hasBin: true
 
@@ -10660,6 +11173,9 @@ packages:
   tslib@2.6.2:
     resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==}
 
+  tslib@2.6.3:
+    resolution: {integrity: sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==}
+
   tsx@4.4.0:
     resolution: {integrity: sha512-4fwcEjRUxW20ciSaMB8zkpGwCPxuRGnadDuj/pBk5S9uT29zvWz15PK36GrKJo45mSJomDxVejZ73c6lr3811Q==}
     engines: {node: '>=18.0.0'}
@@ -10679,10 +11195,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'}
@@ -10703,12 +11215,16 @@ packages:
     resolution: {integrity: sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==}
     engines: {node: '>=8'}
 
+  type-fest@1.4.0:
+    resolution: {integrity: sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==}
+    engines: {node: '>=10'}
+
   type-fest@2.19.0:
     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:
@@ -10813,8 +11329,8 @@ 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
 
@@ -10833,6 +11349,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
@@ -10866,6 +11386,10 @@ packages:
     resolution: {integrity: sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==}
     engines: {node: '>=4'}
 
+  unicorn-magic@0.1.0:
+    resolution: {integrity: sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==}
+    engines: {node: '>=18'}
+
   unified@11.0.4:
     resolution: {integrity: sha512-apMPnyLjAX+ty4OrNap7yumyVAMlKx5IWU2wlzzUdYJO9A8f1p9m/gywF/GM2ZDFcjQPrx59Mc90KwmxsoklxQ==}
 
@@ -10877,9 +11401,9 @@ 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'}
+  unique-string@3.0.0:
+    resolution: {integrity: sha512-VGXBUVwxKMBUznyffQweQABPRRW1vHZAbadFZud4pLFAqRGvv/96vafgjWFqzourzr8YonlQiPgH0YCJfawoGQ==}
+    engines: {node: '>=12'}
 
   unist-util-is@6.0.0:
     resolution: {integrity: sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==}
@@ -10935,6 +11459,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==}
 
@@ -10945,6 +11473,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@8.3.2:
     resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==}
     hasBin: true
@@ -10953,8 +11485,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.12.0:
+    resolution: {integrity: sha512-vvdCBG02mIIiW6Gx6jF119hzxELt+6TlJIwchglR1JYzboHePNxIkVBjR/aoAOVlsGa+5Vtb77cd/N84nrXWPA==}
     peerDependencies:
       '@vue/composition-api': ^1.4.9
       vue: ^2.6.0 || >=3.0.0
@@ -10969,10 +11501,6 @@ packages:
   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'}
@@ -10987,16 +11515,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.3.5:
+    resolution: {integrity: sha512-MdjglKR6AQXQb9JGiS7Rc2wC6uMjcm7Go/NHNO63EwiJXfuk9PgqiP/n5IDJCziMkfw9n4Ubp7lttNwz+8ZVKA==}
     engines: {node: ^18.0.0 || >=20.0.0}
     hasBin: true
     peerDependencies:
@@ -11029,22 +11557,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':
@@ -11053,12 +11581,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==}
@@ -11091,6 +11613,9 @@ packages:
   vscode-textmate@8.0.0:
     resolution: {integrity: sha512-AFbieoL7a5LMqcnOF04ji+rpXadgOXnZsxQr//r83kLPr7biP7am3g9zbaZIaBGwBRWeSvoMD4mgPdX3e4NWBg==}
 
+  vscode-uri@3.0.8:
+    resolution: {integrity: sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==}
+
   vue-component-meta@2.0.16:
     resolution: {integrity: sha512-IyIMClUMYcKxAL34GqdPbR4V45MUeHXqQiZlHxeYMV5Qcqp4M+CEmtGpF//XBSS138heDkYkceHAtJQjLUB1Lw==}
     peerDependencies:
@@ -11105,8 +11630,11 @@ 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.0.29:
+    resolution: {integrity: sha512-58i+ZhUAUpwQ+9h5Hck0D+jr1qbYl4voRt5KffBx8qzELViQ4XdT/Tuo+mzq8u63teAG8K0lLaOiL5ofqW38rg==}
+
+  vue-component-type-helpers@2.1.2:
+    resolution: {integrity: sha512-URuxnrOhO9lUG4LOAapGWBaa/WOLDzzyAbL+uKZqT7RS+PFy0cdXI2mUSh7GaMts6vtHaeVbGk7trd0FPJi65Q==}
 
   vue-demi@0.14.7:
     resolution: {integrity: sha512-EOG8KXDQNwkJILkx/gPcoL/7vH+hORoBaKgGe+6W7VFMvCYJfmF2dGbvgDroVnI8LU7/kTu8mbjRZGBU1z9NTA==}
@@ -11124,8 +11652,8 @@ 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'
@@ -11144,14 +11672,14 @@ 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.0.29:
+    resolution: {integrity: sha512-MHhsfyxO3mYShZCGYNziSbc63x7cQ5g9kvijV7dRe1TTXBRLxXyL0FnXWpUF1xII2mJ86mwYpYsUmMwkmerq7Q==}
     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:
@@ -11172,6 +11700,9 @@ packages:
     engines: {node: '>=12.0.0'}
     hasBin: true
 
+  walk-up-path@3.0.1:
+    resolution: {integrity: sha512-9YlCL/ynK3CTlrSRrDxZvUauLzAswPCrsaCgilqFevUYpeEW0/3ScEjaa3kbW/T0ghhkEr7mv+fpjqn1Y1YuTA==}
+
   walker@1.0.8:
     resolution: {integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==}
 
@@ -11191,6 +11722,10 @@ packages:
     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==}
 
@@ -11264,6 +11799,10 @@ packages:
     resolution: {integrity: sha512-RNGKj82nUPg3g5ygxkQl0R937xLyho1J24ItRCBTr/m1YnZkzJy1hUiHUJrc/VlsDQzsCnInEGSg3bci0Lmd4w==}
     engines: {node: '>= 10.0.0'}
 
+  word-wrap@1.2.5:
+    resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==}
+    engines: {node: '>=0.10.0'}
+
   wordwrap@1.0.0:
     resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==}
 
@@ -11301,8 +11840,8 @@ packages:
       utf-8-validate:
         optional: true
 
-  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
@@ -11394,10 +11933,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==}
@@ -11408,8 +11946,6 @@ packages:
 
 snapshots:
 
-  '@aashutoshrathi/word-wrap@1.2.6': {}
-
   '@adobe/css-tools@4.3.3': {}
 
   '@aiscript-dev/aiscript-languageserver@https://github.com/aiscript-dev/aiscript-languageserver/releases/download/0.1.6/aiscript-dev-aiscript-languageserver-0.1.6.tgz':
@@ -11433,528 +11969,584 @@ snapshots:
     dependencies:
       default-browser-id: 3.0.0
 
-  '@aws-crypto/crc32@3.0.0':
+  '@aws-crypto/crc32@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.6.3
 
-  '@aws-crypto/crc32c@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.6.3
 
-  '@aws-crypto/ie11-detection@3.0.0':
+  '@aws-crypto/sha1-browser@5.2.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.6.3
 
-  '@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.6.3
+
+  '@aws-crypto/sha256-js@5.2.0':
+    dependencies:
+      '@aws-crypto/util': 5.2.0
+      '@aws-sdk/types': 3.609.0
+      tslib: 2.6.3
+
+  '@aws-crypto/supports-web-crypto@5.2.0':
+    dependencies:
+      tslib: 2.6.3
+
+  '@aws-crypto/util@5.2.0':
+    dependencies:
+      '@aws-sdk/types': 3.609.0
+      '@smithy/util-utf8': 2.0.0
+      tslib: 2.6.3
+
+  '@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.6.3
+    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.6.3
+    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.6.3
+    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.6.3
+
+  '@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.6.3
+
+  '@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.6.3
+
+  '@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.6.3
     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.6.3
     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.6.3
+
+  '@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.6.3
     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.6.3
 
-  '@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.6.3
 
-  '@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.6.3
 
-  '@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.6.3
 
-  '@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.6.3
 
-  '@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.6.3
 
-  '@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.6.3
 
-  '@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.6.3
 
-  '@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.6.3
 
-  '@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.6.3
 
-  '@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.6.3
 
-  '@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.6.3
 
-  '@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.6.3
 
-  '@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.6.3
 
-  '@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.6.3
 
-  '@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.6.3
 
-  '@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.6.3
 
-  '@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.6.3
 
   '@aws-sdk/util-locate-window@3.208.0':
     dependencies:
-      tslib: 2.6.2
+      tslib: 2.6.3
 
-  '@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.6.3
 
-  '@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.6.3
 
-  '@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.6.3
 
   '@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.0
+
   '@babel/compat-data@7.23.5': {}
 
+  '@babel/compat-data@7.24.7': {}
+
   '@babel/core@7.23.5':
     dependencies:
       '@ampproject/remapping': 2.2.1
       '@babel/code-frame': 7.23.5
-      '@babel/generator': 7.23.5
+      '@babel/generator': 7.24.7
       '@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.5(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.5
-      '@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.5(supports-color@8.1.1)
       gensync: 1.0.0-beta.2
       json5: 2.2.3
       semver: 6.3.1
     transitivePeerDependencies:
       - supports-color
 
-  '@babel/generator@7.23.5':
+  '@babel/generator@7.24.7':
     dependencies:
-      '@babel/types': 7.23.5
-      '@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/generator@7.23.6':
+  '@babel/helper-annotate-as-pure@7.24.7':
     dependencies:
-      '@babel/types': 7.24.0
-      '@jridgewell/gen-mapping': 0.3.2
-      '@jridgewell/trace-mapping': 0.3.18
-      jsesc: 2.5.2
+      '@babel/types': 7.24.7
 
-  '@babel/helper-annotate-as-pure@7.22.5':
+  '@babel/helper-builder-binary-assignment-operator-visitor@7.24.7':
     dependencies:
-      '@babel/types': 7.24.0
-
-  '@babel/helper-builder-binary-assignment-operator-visitor@7.22.15':
-    dependencies:
-      '@babel/types': 7.24.0
+      '@babel/traverse': 7.24.7
+      '@babel/types': 7.24.7
+    transitivePeerDependencies:
+      - supports-color
 
   '@babel/helper-compilation-targets@7.22.15':
     dependencies:
@@ -11964,40 +12556,42 @@ 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)':
+  '@babel/helper-create-class-features-plugin@7.24.7(@babel/core@7.24.7)':
     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
+      '@babel/core': 7.24.7
+      '@babel/helper-annotate-as-pure': 7.24.7
+      '@babel/helper-environment-visitor': 7.24.7
+      '@babel/helper-function-name': 7.24.7
+      '@babel/helper-member-expression-to-functions': 7.24.7
+      '@babel/helper-optimise-call-expression': 7.24.7
+      '@babel/helper-replace-supers': 7.24.7(@babel/core@7.24.7)
+      '@babel/helper-skip-transparent-expression-wrappers': 7.24.7
+      '@babel/helper-split-export-declaration': 7.24.7
       semver: 6.3.1
+    transitivePeerDependencies:
+      - supports-color
 
-  '@babel/helper-create-regexp-features-plugin@7.22.15(@babel/core@7.24.0)':
+  '@babel/helper-create-regexp-features-plugin@7.24.7(@babel/core@7.24.7)':
     dependencies:
-      '@babel/core': 7.24.0
-      '@babel/helper-annotate-as-pure': 7.22.5
+      '@babel/core': 7.24.7
+      '@babel/helper-annotate-as-pure': 7.24.7
       regexpu-core: 5.3.2
       semver: 6.3.1
 
-  '@babel/helper-define-polyfill-provider@0.4.3(@babel/core@7.24.0)':
+  '@babel/helper-define-polyfill-provider@0.6.2(@babel/core@7.24.7)':
     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)
+      '@babel/core': 7.24.7
+      '@babel/helper-compilation-targets': 7.24.7
+      '@babel/helper-plugin-utils': 7.24.7
+      debug: 4.3.5(supports-color@8.1.1)
       lodash.debounce: 4.0.8
       resolve: 1.22.8
     transitivePeerDependencies:
@@ -12005,22 +12599,45 @@ snapshots:
 
   '@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-member-expression-to-functions@7.24.7':
+    dependencies:
+      '@babel/traverse': 7.24.7
+      '@babel/types': 7.24.7
+    transitivePeerDependencies:
+      - supports-color
 
   '@babel/helper-module-imports@7.22.15':
     dependencies:
-      '@babel/types': 7.24.0
+      '@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:
@@ -12029,125 +12646,156 @@ 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/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-optimise-call-expression@7.22.5':
+  '@babel/helper-optimise-call-expression@7.24.7':
     dependencies:
-      '@babel/types': 7.24.0
+      '@babel/types': 7.24.7
 
   '@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-plugin-utils@7.24.7': {}
 
-  '@babel/helper-replace-supers@7.22.20(@babel/core@7.24.0)':
+  '@babel/helper-remap-async-to-generator@7.24.7(@babel/core@7.24.7)':
     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/core': 7.24.7
+      '@babel/helper-annotate-as-pure': 7.24.7
+      '@babel/helper-environment-visitor': 7.24.7
+      '@babel/helper-wrap-function': 7.24.7
+    transitivePeerDependencies:
+      - supports-color
+
+  '@babel/helper-replace-supers@7.24.7(@babel/core@7.24.7)':
+    dependencies:
+      '@babel/core': 7.24.7
+      '@babel/helper-environment-visitor': 7.24.7
+      '@babel/helper-member-expression-to-functions': 7.24.7
+      '@babel/helper-optimise-call-expression': 7.24.7
+    transitivePeerDependencies:
+      - supports-color
 
   '@babel/helper-simple-access@7.22.5':
     dependencies:
-      '@babel/types': 7.24.0
+      '@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-skip-transparent-expression-wrappers@7.24.7':
+    dependencies:
+      '@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.24.0
+      '@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-validator-identifier@7.24.7': {}
 
   '@babel/helper-validator-option@7.23.5': {}
 
-  '@babel/helper-wrap-function@7.22.20':
+  '@babel/helper-validator-option@7.24.7': {}
+
+  '@babel/helper-wrap-function@7.24.7':
     dependencies:
-      '@babel/helper-function-name': 7.23.0
-      '@babel/template': 7.24.0
-      '@babel/types': 7.24.0
+      '@babel/helper-function-name': 7.24.7
+      '@babel/template': 7.24.7
+      '@babel/traverse': 7.24.7
+      '@babel/types': 7.24.7
+    transitivePeerDependencies:
+      - supports-color
 
   '@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.24.0
+      '@babel/helper-validator-identifier': 7.24.7
+      chalk: 2.4.2
+      js-tokens: 4.0.0
+      picocolors: 1.0.0
 
-  '@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/plugin-bugfix-firefox-class-in-computed-class-key@7.24.7(@babel/core@7.24.7)':
     dependencies:
-      '@babel/types': 7.24.0
+      '@babel/core': 7.24.7
+      '@babel/helper-environment-visitor': 7.24.7
+      '@babel/helper-plugin-utils': 7.24.7
 
-  '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.23.3(@babel/core@7.24.0)':
+  '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.24.7(@babel/core@7.24.7)':
     dependencies:
-      '@babel/core': 7.24.0
-      '@babel/helper-plugin-utils': 7.22.5
+      '@babel/core': 7.24.7
+      '@babel/helper-plugin-utils': 7.24.7
 
-  '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.23.3(@babel/core@7.24.0)':
+  '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.24.7(@babel/core@7.24.7)':
     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/core': 7.24.7
+      '@babel/helper-plugin-utils': 7.24.7
+      '@babel/helper-skip-transparent-expression-wrappers': 7.24.7
+      '@babel/plugin-transform-optional-chaining': 7.24.7(@babel/core@7.24.7)
+    transitivePeerDependencies:
+      - supports-color
 
-  '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.23.3(@babel/core@7.24.0)':
+  '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.24.7(@babel/core@7.24.7)':
     dependencies:
-      '@babel/core': 7.24.0
-      '@babel/helper-environment-visitor': 7.22.20
-      '@babel/helper-plugin-utils': 7.22.5
+      '@babel/core': 7.24.7
+      '@babel/helper-environment-visitor': 7.24.7
+      '@babel/helper-plugin-utils': 7.24.7
 
-  '@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2(@babel/core@7.24.0)':
+  '@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2(@babel/core@7.24.7)':
     dependencies:
-      '@babel/core': 7.24.0
+      '@babel/core': 7.24.7
 
   '@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)':
+  '@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.24.7)':
     dependencies:
-      '@babel/core': 7.24.0
+      '@babel/core': 7.24.7
       '@babel/helper-plugin-utils': 7.22.5
 
   '@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.23.5)':
@@ -12155,54 +12803,60 @@ snapshots:
       '@babel/core': 7.23.5
       '@babel/helper-plugin-utils': 7.22.5
 
+  '@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.24.7)':
+    dependencies:
+      '@babel/core': 7.24.7
+      '@babel/helper-plugin-utils': 7.22.5
+    optional: true
+
   '@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.23.5)':
     dependencies:
       '@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)':
+  '@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.24.7)':
     dependencies:
-      '@babel/core': 7.24.0
+      '@babel/core': 7.24.7
       '@babel/helper-plugin-utils': 7.22.5
 
-  '@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.24.0)':
+  '@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.24.7)':
     dependencies:
-      '@babel/core': 7.24.0
-      '@babel/helper-plugin-utils': 7.22.5
+      '@babel/core': 7.24.7
+      '@babel/helper-plugin-utils': 7.24.7
 
-  '@babel/plugin-syntax-dynamic-import@7.8.3(@babel/core@7.24.0)':
+  '@babel/plugin-syntax-dynamic-import@7.8.3(@babel/core@7.24.7)':
     dependencies:
-      '@babel/core': 7.24.0
-      '@babel/helper-plugin-utils': 7.22.5
+      '@babel/core': 7.24.7
+      '@babel/helper-plugin-utils': 7.24.7
 
-  '@babel/plugin-syntax-export-namespace-from@7.8.3(@babel/core@7.24.0)':
+  '@babel/plugin-syntax-export-namespace-from@7.8.3(@babel/core@7.24.7)':
     dependencies:
-      '@babel/core': 7.24.0
-      '@babel/helper-plugin-utils': 7.22.5
+      '@babel/core': 7.24.7
+      '@babel/helper-plugin-utils': 7.24.7
 
-  '@babel/plugin-syntax-flow@7.23.3(@babel/core@7.24.0)':
+  '@babel/plugin-syntax-flow@7.23.3(@babel/core@7.24.7)':
     dependencies:
-      '@babel/core': 7.24.0
-      '@babel/helper-plugin-utils': 7.22.5
+      '@babel/core': 7.24.7
+      '@babel/helper-plugin-utils': 7.24.7
 
-  '@babel/plugin-syntax-import-assertions@7.23.3(@babel/core@7.24.0)':
+  '@babel/plugin-syntax-import-assertions@7.24.7(@babel/core@7.24.7)':
     dependencies:
-      '@babel/core': 7.24.0
-      '@babel/helper-plugin-utils': 7.22.5
+      '@babel/core': 7.24.7
+      '@babel/helper-plugin-utils': 7.24.7
 
-  '@babel/plugin-syntax-import-attributes@7.23.3(@babel/core@7.24.0)':
+  '@babel/plugin-syntax-import-attributes@7.24.7(@babel/core@7.24.7)':
     dependencies:
-      '@babel/core': 7.24.0
-      '@babel/helper-plugin-utils': 7.22.5
+      '@babel/core': 7.24.7
+      '@babel/helper-plugin-utils': 7.24.7
 
   '@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)':
+  '@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.24.7)':
     dependencies:
-      '@babel/core': 7.24.0
+      '@babel/core': 7.24.7
       '@babel/helper-plugin-utils': 7.22.5
 
   '@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.23.5)':
@@ -12210,9 +12864,9 @@ snapshots:
       '@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)':
+  '@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.24.7)':
     dependencies:
-      '@babel/core': 7.24.0
+      '@babel/core': 7.24.7
       '@babel/helper-plugin-utils': 7.22.5
 
   '@babel/plugin-syntax-jsx@7.23.3(@babel/core@7.23.5)':
@@ -12220,9 +12874,9 @@ snapshots:
       '@babel/core': 7.23.5
       '@babel/helper-plugin-utils': 7.22.5
 
-  '@babel/plugin-syntax-jsx@7.23.3(@babel/core@7.24.0)':
+  '@babel/plugin-syntax-jsx@7.23.3(@babel/core@7.24.7)':
     dependencies:
-      '@babel/core': 7.24.0
+      '@babel/core': 7.24.7
       '@babel/helper-plugin-utils': 7.22.5
 
   '@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.23.5)':
@@ -12230,9 +12884,9 @@ snapshots:
       '@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)':
+  '@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.24.7)':
     dependencies:
-      '@babel/core': 7.24.0
+      '@babel/core': 7.24.7
       '@babel/helper-plugin-utils': 7.22.5
 
   '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.23.5)':
@@ -12240,9 +12894,9 @@ snapshots:
       '@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)':
+  '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.24.7)':
     dependencies:
-      '@babel/core': 7.24.0
+      '@babel/core': 7.24.7
       '@babel/helper-plugin-utils': 7.22.5
 
   '@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.23.5)':
@@ -12250,9 +12904,9 @@ snapshots:
       '@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)':
+  '@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.24.7)':
     dependencies:
-      '@babel/core': 7.24.0
+      '@babel/core': 7.24.7
       '@babel/helper-plugin-utils': 7.22.5
 
   '@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.23.5)':
@@ -12260,9 +12914,9 @@ snapshots:
       '@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)':
+  '@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.24.7)':
     dependencies:
-      '@babel/core': 7.24.0
+      '@babel/core': 7.24.7
       '@babel/helper-plugin-utils': 7.22.5
 
   '@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.23.5)':
@@ -12270,9 +12924,9 @@ snapshots:
       '@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)':
+  '@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.24.7)':
     dependencies:
-      '@babel/core': 7.24.0
+      '@babel/core': 7.24.7
       '@babel/helper-plugin-utils': 7.22.5
 
   '@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.23.5)':
@@ -12280,24 +12934,24 @@ snapshots:
       '@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)':
+  '@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.24.7)':
     dependencies:
-      '@babel/core': 7.24.0
+      '@babel/core': 7.24.7
       '@babel/helper-plugin-utils': 7.22.5
 
-  '@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.24.0)':
+  '@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.24.7)':
     dependencies:
-      '@babel/core': 7.24.0
-      '@babel/helper-plugin-utils': 7.22.5
+      '@babel/core': 7.24.7
+      '@babel/helper-plugin-utils': 7.24.7
 
   '@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)':
+  '@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.24.7)':
     dependencies:
-      '@babel/core': 7.24.0
+      '@babel/core': 7.24.7
       '@babel/helper-plugin-utils': 7.22.5
 
   '@babel/plugin-syntax-typescript@7.23.3(@babel/core@7.23.5)':
@@ -12305,435 +12959,471 @@ snapshots:
       '@babel/core': 7.23.5
       '@babel/helper-plugin-utils': 7.22.5
 
-  '@babel/plugin-syntax-typescript@7.23.3(@babel/core@7.24.0)':
+  '@babel/plugin-syntax-typescript@7.23.3(@babel/core@7.24.7)':
     dependencies:
-      '@babel/core': 7.24.0
+      '@babel/core': 7.24.7
       '@babel/helper-plugin-utils': 7.22.5
 
-  '@babel/plugin-syntax-unicode-sets-regex@7.18.6(@babel/core@7.24.0)':
+  '@babel/plugin-syntax-unicode-sets-regex@7.18.6(@babel/core@7.24.7)':
     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/core': 7.24.7
+      '@babel/helper-create-regexp-features-plugin': 7.24.7(@babel/core@7.24.7)
+      '@babel/helper-plugin-utils': 7.24.7
 
-  '@babel/plugin-transform-arrow-functions@7.23.3(@babel/core@7.24.0)':
+  '@babel/plugin-transform-arrow-functions@7.24.7(@babel/core@7.24.7)':
     dependencies:
-      '@babel/core': 7.24.0
-      '@babel/helper-plugin-utils': 7.22.5
+      '@babel/core': 7.24.7
+      '@babel/helper-plugin-utils': 7.24.7
 
-  '@babel/plugin-transform-async-generator-functions@7.23.4(@babel/core@7.24.0)':
+  '@babel/plugin-transform-async-generator-functions@7.24.7(@babel/core@7.24.7)':
     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/core': 7.24.7
+      '@babel/helper-environment-visitor': 7.24.7
+      '@babel/helper-plugin-utils': 7.24.7
+      '@babel/helper-remap-async-to-generator': 7.24.7(@babel/core@7.24.7)
+      '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.24.7)
+    transitivePeerDependencies:
+      - supports-color
 
-  '@babel/plugin-transform-async-to-generator@7.23.3(@babel/core@7.24.0)':
+  '@babel/plugin-transform-async-to-generator@7.24.7(@babel/core@7.24.7)':
     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/core': 7.24.7
+      '@babel/helper-module-imports': 7.24.7
+      '@babel/helper-plugin-utils': 7.24.7
+      '@babel/helper-remap-async-to-generator': 7.24.7(@babel/core@7.24.7)
+    transitivePeerDependencies:
+      - supports-color
 
-  '@babel/plugin-transform-block-scoped-functions@7.23.3(@babel/core@7.24.0)':
+  '@babel/plugin-transform-block-scoped-functions@7.24.7(@babel/core@7.24.7)':
     dependencies:
-      '@babel/core': 7.24.0
-      '@babel/helper-plugin-utils': 7.22.5
+      '@babel/core': 7.24.7
+      '@babel/helper-plugin-utils': 7.24.7
 
-  '@babel/plugin-transform-block-scoping@7.23.4(@babel/core@7.24.0)':
+  '@babel/plugin-transform-block-scoping@7.24.7(@babel/core@7.24.7)':
     dependencies:
-      '@babel/core': 7.24.0
-      '@babel/helper-plugin-utils': 7.22.5
+      '@babel/core': 7.24.7
+      '@babel/helper-plugin-utils': 7.24.7
 
-  '@babel/plugin-transform-class-properties@7.23.3(@babel/core@7.24.0)':
+  '@babel/plugin-transform-class-properties@7.24.7(@babel/core@7.24.7)':
     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/core': 7.24.7
+      '@babel/helper-create-class-features-plugin': 7.24.7(@babel/core@7.24.7)
+      '@babel/helper-plugin-utils': 7.24.7
+    transitivePeerDependencies:
+      - supports-color
 
-  '@babel/plugin-transform-class-static-block@7.23.4(@babel/core@7.24.0)':
+  '@babel/plugin-transform-class-static-block@7.24.7(@babel/core@7.24.7)':
     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/core': 7.24.7
+      '@babel/helper-create-class-features-plugin': 7.24.7(@babel/core@7.24.7)
+      '@babel/helper-plugin-utils': 7.24.7
+      '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.24.7)
+    transitivePeerDependencies:
+      - supports-color
 
-  '@babel/plugin-transform-classes@7.23.5(@babel/core@7.24.0)':
+  '@babel/plugin-transform-classes@7.24.7(@babel/core@7.24.7)':
     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
+      '@babel/core': 7.24.7
+      '@babel/helper-annotate-as-pure': 7.24.7
+      '@babel/helper-compilation-targets': 7.24.7
+      '@babel/helper-environment-visitor': 7.24.7
+      '@babel/helper-function-name': 7.24.7
+      '@babel/helper-plugin-utils': 7.24.7
+      '@babel/helper-replace-supers': 7.24.7(@babel/core@7.24.7)
+      '@babel/helper-split-export-declaration': 7.24.7
       globals: 11.12.0
+    transitivePeerDependencies:
+      - supports-color
 
-  '@babel/plugin-transform-computed-properties@7.23.3(@babel/core@7.24.0)':
+  '@babel/plugin-transform-computed-properties@7.24.7(@babel/core@7.24.7)':
     dependencies:
-      '@babel/core': 7.24.0
-      '@babel/helper-plugin-utils': 7.22.5
-      '@babel/template': 7.24.0
+      '@babel/core': 7.24.7
+      '@babel/helper-plugin-utils': 7.24.7
+      '@babel/template': 7.24.7
 
-  '@babel/plugin-transform-destructuring@7.23.3(@babel/core@7.24.0)':
+  '@babel/plugin-transform-destructuring@7.24.7(@babel/core@7.24.7)':
     dependencies:
-      '@babel/core': 7.24.0
-      '@babel/helper-plugin-utils': 7.22.5
+      '@babel/core': 7.24.7
+      '@babel/helper-plugin-utils': 7.24.7
 
-  '@babel/plugin-transform-dotall-regex@7.23.3(@babel/core@7.24.0)':
+  '@babel/plugin-transform-dotall-regex@7.24.7(@babel/core@7.24.7)':
     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/core': 7.24.7
+      '@babel/helper-create-regexp-features-plugin': 7.24.7(@babel/core@7.24.7)
+      '@babel/helper-plugin-utils': 7.24.7
 
-  '@babel/plugin-transform-duplicate-keys@7.23.3(@babel/core@7.24.0)':
+  '@babel/plugin-transform-duplicate-keys@7.24.7(@babel/core@7.24.7)':
     dependencies:
-      '@babel/core': 7.24.0
-      '@babel/helper-plugin-utils': 7.22.5
+      '@babel/core': 7.24.7
+      '@babel/helper-plugin-utils': 7.24.7
 
-  '@babel/plugin-transform-dynamic-import@7.23.4(@babel/core@7.24.0)':
+  '@babel/plugin-transform-dynamic-import@7.24.7(@babel/core@7.24.7)':
     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/core': 7.24.7
+      '@babel/helper-plugin-utils': 7.24.7
+      '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.24.7)
 
-  '@babel/plugin-transform-exponentiation-operator@7.23.3(@babel/core@7.24.0)':
+  '@babel/plugin-transform-exponentiation-operator@7.24.7(@babel/core@7.24.7)':
     dependencies:
-      '@babel/core': 7.24.0
-      '@babel/helper-builder-binary-assignment-operator-visitor': 7.22.15
-      '@babel/helper-plugin-utils': 7.22.5
+      '@babel/core': 7.24.7
+      '@babel/helper-builder-binary-assignment-operator-visitor': 7.24.7
+      '@babel/helper-plugin-utils': 7.24.7
+    transitivePeerDependencies:
+      - supports-color
 
-  '@babel/plugin-transform-export-namespace-from@7.23.4(@babel/core@7.24.0)':
+  '@babel/plugin-transform-export-namespace-from@7.24.7(@babel/core@7.24.7)':
     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/core': 7.24.7
+      '@babel/helper-plugin-utils': 7.24.7
+      '@babel/plugin-syntax-export-namespace-from': 7.8.3(@babel/core@7.24.7)
 
-  '@babel/plugin-transform-flow-strip-types@7.23.3(@babel/core@7.24.0)':
+  '@babel/plugin-transform-flow-strip-types@7.23.3(@babel/core@7.24.7)':
     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/core': 7.24.7
+      '@babel/helper-plugin-utils': 7.24.7
+      '@babel/plugin-syntax-flow': 7.23.3(@babel/core@7.24.7)
 
-  '@babel/plugin-transform-for-of@7.23.3(@babel/core@7.24.0)':
+  '@babel/plugin-transform-for-of@7.24.7(@babel/core@7.24.7)':
     dependencies:
-      '@babel/core': 7.24.0
-      '@babel/helper-plugin-utils': 7.22.5
+      '@babel/core': 7.24.7
+      '@babel/helper-plugin-utils': 7.24.7
+      '@babel/helper-skip-transparent-expression-wrappers': 7.24.7
+    transitivePeerDependencies:
+      - supports-color
 
-  '@babel/plugin-transform-function-name@7.23.3(@babel/core@7.24.0)':
+  '@babel/plugin-transform-function-name@7.24.7(@babel/core@7.24.7)':
     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/core': 7.24.7
+      '@babel/helper-compilation-targets': 7.24.7
+      '@babel/helper-function-name': 7.24.7
+      '@babel/helper-plugin-utils': 7.24.7
 
-  '@babel/plugin-transform-json-strings@7.23.4(@babel/core@7.24.0)':
+  '@babel/plugin-transform-json-strings@7.24.7(@babel/core@7.24.7)':
     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/core': 7.24.7
+      '@babel/helper-plugin-utils': 7.24.7
+      '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.24.7)
 
-  '@babel/plugin-transform-literals@7.23.3(@babel/core@7.24.0)':
+  '@babel/plugin-transform-literals@7.24.7(@babel/core@7.24.7)':
     dependencies:
-      '@babel/core': 7.24.0
-      '@babel/helper-plugin-utils': 7.22.5
+      '@babel/core': 7.24.7
+      '@babel/helper-plugin-utils': 7.24.7
 
-  '@babel/plugin-transform-logical-assignment-operators@7.23.4(@babel/core@7.24.0)':
+  '@babel/plugin-transform-logical-assignment-operators@7.24.7(@babel/core@7.24.7)':
     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/core': 7.24.7
+      '@babel/helper-plugin-utils': 7.24.7
+      '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.24.7)
 
-  '@babel/plugin-transform-member-expression-literals@7.23.3(@babel/core@7.24.0)':
+  '@babel/plugin-transform-member-expression-literals@7.24.7(@babel/core@7.24.7)':
     dependencies:
-      '@babel/core': 7.24.0
-      '@babel/helper-plugin-utils': 7.22.5
+      '@babel/core': 7.24.7
+      '@babel/helper-plugin-utils': 7.24.7
 
-  '@babel/plugin-transform-modules-amd@7.23.3(@babel/core@7.24.0)':
+  '@babel/plugin-transform-modules-amd@7.24.7(@babel/core@7.24.7)':
     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/core': 7.24.7
+      '@babel/helper-module-transforms': 7.24.7(@babel/core@7.24.7)
+      '@babel/helper-plugin-utils': 7.24.7
+    transitivePeerDependencies:
+      - supports-color
 
-  '@babel/plugin-transform-modules-commonjs@7.23.3(@babel/core@7.24.0)':
+  '@babel/plugin-transform-modules-commonjs@7.24.7(@babel/core@7.24.7)':
     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/core': 7.24.7
+      '@babel/helper-module-transforms': 7.24.7(@babel/core@7.24.7)
+      '@babel/helper-plugin-utils': 7.24.7
+      '@babel/helper-simple-access': 7.24.7
+    transitivePeerDependencies:
+      - supports-color
 
-  '@babel/plugin-transform-modules-systemjs@7.23.3(@babel/core@7.24.0)':
+  '@babel/plugin-transform-modules-systemjs@7.24.7(@babel/core@7.24.7)':
     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/core': 7.24.7
+      '@babel/helper-hoist-variables': 7.24.7
+      '@babel/helper-module-transforms': 7.24.7(@babel/core@7.24.7)
+      '@babel/helper-plugin-utils': 7.24.7
+      '@babel/helper-validator-identifier': 7.24.7
+    transitivePeerDependencies:
+      - supports-color
 
-  '@babel/plugin-transform-modules-umd@7.23.3(@babel/core@7.24.0)':
+  '@babel/plugin-transform-modules-umd@7.24.7(@babel/core@7.24.7)':
     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/core': 7.24.7
+      '@babel/helper-module-transforms': 7.24.7(@babel/core@7.24.7)
+      '@babel/helper-plugin-utils': 7.24.7
+    transitivePeerDependencies:
+      - supports-color
 
-  '@babel/plugin-transform-named-capturing-groups-regex@7.22.5(@babel/core@7.24.0)':
+  '@babel/plugin-transform-named-capturing-groups-regex@7.24.7(@babel/core@7.24.7)':
     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/core': 7.24.7
+      '@babel/helper-create-regexp-features-plugin': 7.24.7(@babel/core@7.24.7)
+      '@babel/helper-plugin-utils': 7.24.7
 
-  '@babel/plugin-transform-new-target@7.23.3(@babel/core@7.24.0)':
+  '@babel/plugin-transform-new-target@7.24.7(@babel/core@7.24.7)':
     dependencies:
-      '@babel/core': 7.24.0
-      '@babel/helper-plugin-utils': 7.22.5
+      '@babel/core': 7.24.7
+      '@babel/helper-plugin-utils': 7.24.7
 
-  '@babel/plugin-transform-nullish-coalescing-operator@7.23.4(@babel/core@7.24.0)':
+  '@babel/plugin-transform-nullish-coalescing-operator@7.24.7(@babel/core@7.24.7)':
     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/core': 7.24.7
+      '@babel/helper-plugin-utils': 7.24.7
+      '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.24.7)
 
-  '@babel/plugin-transform-numeric-separator@7.23.4(@babel/core@7.24.0)':
+  '@babel/plugin-transform-numeric-separator@7.24.7(@babel/core@7.24.7)':
     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/core': 7.24.7
+      '@babel/helper-plugin-utils': 7.24.7
+      '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.24.7)
 
-  '@babel/plugin-transform-object-rest-spread@7.23.4(@babel/core@7.24.0)':
+  '@babel/plugin-transform-object-rest-spread@7.24.7(@babel/core@7.24.7)':
     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/core': 7.24.7
+      '@babel/helper-compilation-targets': 7.24.7
+      '@babel/helper-plugin-utils': 7.24.7
+      '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.24.7)
+      '@babel/plugin-transform-parameters': 7.24.7(@babel/core@7.24.7)
 
-  '@babel/plugin-transform-object-super@7.23.3(@babel/core@7.24.0)':
+  '@babel/plugin-transform-object-super@7.24.7(@babel/core@7.24.7)':
     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/core': 7.24.7
+      '@babel/helper-plugin-utils': 7.24.7
+      '@babel/helper-replace-supers': 7.24.7(@babel/core@7.24.7)
+    transitivePeerDependencies:
+      - supports-color
 
-  '@babel/plugin-transform-optional-catch-binding@7.23.4(@babel/core@7.24.0)':
+  '@babel/plugin-transform-optional-catch-binding@7.24.7(@babel/core@7.24.7)':
     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/core': 7.24.7
+      '@babel/helper-plugin-utils': 7.24.7
+      '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.24.7)
 
-  '@babel/plugin-transform-optional-chaining@7.23.4(@babel/core@7.24.0)':
+  '@babel/plugin-transform-optional-chaining@7.24.7(@babel/core@7.24.7)':
     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/core': 7.24.7
+      '@babel/helper-plugin-utils': 7.24.7
+      '@babel/helper-skip-transparent-expression-wrappers': 7.24.7
+      '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.24.7)
+    transitivePeerDependencies:
+      - supports-color
 
-  '@babel/plugin-transform-parameters@7.23.3(@babel/core@7.24.0)':
+  '@babel/plugin-transform-parameters@7.24.7(@babel/core@7.24.7)':
     dependencies:
-      '@babel/core': 7.24.0
-      '@babel/helper-plugin-utils': 7.22.5
+      '@babel/core': 7.24.7
+      '@babel/helper-plugin-utils': 7.24.7
 
-  '@babel/plugin-transform-private-methods@7.23.3(@babel/core@7.24.0)':
+  '@babel/plugin-transform-private-methods@7.24.7(@babel/core@7.24.7)':
     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/core': 7.24.7
+      '@babel/helper-create-class-features-plugin': 7.24.7(@babel/core@7.24.7)
+      '@babel/helper-plugin-utils': 7.24.7
+    transitivePeerDependencies:
+      - supports-color
 
-  '@babel/plugin-transform-private-property-in-object@7.23.4(@babel/core@7.24.0)':
+  '@babel/plugin-transform-private-property-in-object@7.24.7(@babel/core@7.24.7)':
     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/core': 7.24.7
+      '@babel/helper-annotate-as-pure': 7.24.7
+      '@babel/helper-create-class-features-plugin': 7.24.7(@babel/core@7.24.7)
+      '@babel/helper-plugin-utils': 7.24.7
+      '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.24.7)
+    transitivePeerDependencies:
+      - supports-color
 
-  '@babel/plugin-transform-property-literals@7.23.3(@babel/core@7.24.0)':
+  '@babel/plugin-transform-property-literals@7.24.7(@babel/core@7.24.7)':
     dependencies:
-      '@babel/core': 7.24.0
-      '@babel/helper-plugin-utils': 7.22.5
+      '@babel/core': 7.24.7
+      '@babel/helper-plugin-utils': 7.24.7
 
-  '@babel/plugin-transform-regenerator@7.23.3(@babel/core@7.24.0)':
+  '@babel/plugin-transform-regenerator@7.24.7(@babel/core@7.24.7)':
     dependencies:
-      '@babel/core': 7.24.0
-      '@babel/helper-plugin-utils': 7.22.5
+      '@babel/core': 7.24.7
+      '@babel/helper-plugin-utils': 7.24.7
       regenerator-transform: 0.15.2
 
-  '@babel/plugin-transform-reserved-words@7.23.3(@babel/core@7.24.0)':
+  '@babel/plugin-transform-reserved-words@7.24.7(@babel/core@7.24.7)':
     dependencies:
-      '@babel/core': 7.24.0
-      '@babel/helper-plugin-utils': 7.22.5
+      '@babel/core': 7.24.7
+      '@babel/helper-plugin-utils': 7.24.7
 
-  '@babel/plugin-transform-shorthand-properties@7.23.3(@babel/core@7.24.0)':
+  '@babel/plugin-transform-shorthand-properties@7.24.7(@babel/core@7.24.7)':
     dependencies:
-      '@babel/core': 7.24.0
-      '@babel/helper-plugin-utils': 7.22.5
+      '@babel/core': 7.24.7
+      '@babel/helper-plugin-utils': 7.24.7
 
-  '@babel/plugin-transform-spread@7.23.3(@babel/core@7.24.0)':
+  '@babel/plugin-transform-spread@7.24.7(@babel/core@7.24.7)':
     dependencies:
-      '@babel/core': 7.24.0
-      '@babel/helper-plugin-utils': 7.22.5
-      '@babel/helper-skip-transparent-expression-wrappers': 7.22.5
+      '@babel/core': 7.24.7
+      '@babel/helper-plugin-utils': 7.24.7
+      '@babel/helper-skip-transparent-expression-wrappers': 7.24.7
+    transitivePeerDependencies:
+      - supports-color
 
-  '@babel/plugin-transform-sticky-regex@7.23.3(@babel/core@7.24.0)':
+  '@babel/plugin-transform-sticky-regex@7.24.7(@babel/core@7.24.7)':
     dependencies:
-      '@babel/core': 7.24.0
-      '@babel/helper-plugin-utils': 7.22.5
+      '@babel/core': 7.24.7
+      '@babel/helper-plugin-utils': 7.24.7
 
-  '@babel/plugin-transform-template-literals@7.23.3(@babel/core@7.24.0)':
+  '@babel/plugin-transform-template-literals@7.24.7(@babel/core@7.24.7)':
     dependencies:
-      '@babel/core': 7.24.0
-      '@babel/helper-plugin-utils': 7.22.5
+      '@babel/core': 7.24.7
+      '@babel/helper-plugin-utils': 7.24.7
 
-  '@babel/plugin-transform-typeof-symbol@7.23.3(@babel/core@7.24.0)':
+  '@babel/plugin-transform-typeof-symbol@7.24.7(@babel/core@7.24.7)':
     dependencies:
-      '@babel/core': 7.24.0
-      '@babel/helper-plugin-utils': 7.22.5
+      '@babel/core': 7.24.7
+      '@babel/helper-plugin-utils': 7.24.7
 
-  '@babel/plugin-transform-typescript@7.23.5(@babel/core@7.24.0)':
+  '@babel/plugin-transform-typescript@7.23.5(@babel/core@7.24.7)':
     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/core': 7.24.7
+      '@babel/helper-annotate-as-pure': 7.24.7
+      '@babel/helper-create-class-features-plugin': 7.24.7(@babel/core@7.24.7)
+      '@babel/helper-plugin-utils': 7.24.7
+      '@babel/plugin-syntax-typescript': 7.23.3(@babel/core@7.24.7)
+    transitivePeerDependencies:
+      - supports-color
 
-  '@babel/plugin-transform-unicode-escapes@7.23.3(@babel/core@7.24.0)':
+  '@babel/plugin-transform-unicode-escapes@7.24.7(@babel/core@7.24.7)':
     dependencies:
-      '@babel/core': 7.24.0
-      '@babel/helper-plugin-utils': 7.22.5
+      '@babel/core': 7.24.7
+      '@babel/helper-plugin-utils': 7.24.7
 
-  '@babel/plugin-transform-unicode-property-regex@7.23.3(@babel/core@7.24.0)':
+  '@babel/plugin-transform-unicode-property-regex@7.24.7(@babel/core@7.24.7)':
     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/core': 7.24.7
+      '@babel/helper-create-regexp-features-plugin': 7.24.7(@babel/core@7.24.7)
+      '@babel/helper-plugin-utils': 7.24.7
 
-  '@babel/plugin-transform-unicode-regex@7.23.3(@babel/core@7.24.0)':
+  '@babel/plugin-transform-unicode-regex@7.24.7(@babel/core@7.24.7)':
     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/core': 7.24.7
+      '@babel/helper-create-regexp-features-plugin': 7.24.7(@babel/core@7.24.7)
+      '@babel/helper-plugin-utils': 7.24.7
 
-  '@babel/plugin-transform-unicode-sets-regex@7.23.3(@babel/core@7.24.0)':
+  '@babel/plugin-transform-unicode-sets-regex@7.24.7(@babel/core@7.24.7)':
     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/core': 7.24.7
+      '@babel/helper-create-regexp-features-plugin': 7.24.7(@babel/core@7.24.7)
+      '@babel/helper-plugin-utils': 7.24.7
 
-  '@babel/preset-env@7.23.5(@babel/core@7.24.0)':
+  '@babel/preset-env@7.24.7(@babel/core@7.24.7)':
     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
+      '@babel/compat-data': 7.24.7
+      '@babel/core': 7.24.7
+      '@babel/helper-compilation-targets': 7.24.7
+      '@babel/helper-plugin-utils': 7.24.7
+      '@babel/helper-validator-option': 7.24.7
+      '@babel/plugin-bugfix-firefox-class-in-computed-class-key': 7.24.7(@babel/core@7.24.7)
+      '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression': 7.24.7(@babel/core@7.24.7)
+      '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining': 7.24.7(@babel/core@7.24.7)
+      '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly': 7.24.7(@babel/core@7.24.7)
+      '@babel/plugin-proposal-private-property-in-object': 7.21.0-placeholder-for-preset-env.2(@babel/core@7.24.7)
+      '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.24.7)
+      '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.24.7)
+      '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.24.7)
+      '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.24.7)
+      '@babel/plugin-syntax-export-namespace-from': 7.8.3(@babel/core@7.24.7)
+      '@babel/plugin-syntax-import-assertions': 7.24.7(@babel/core@7.24.7)
+      '@babel/plugin-syntax-import-attributes': 7.24.7(@babel/core@7.24.7)
+      '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.24.7)
+      '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.24.7)
+      '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.24.7)
+      '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.24.7)
+      '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.24.7)
+      '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.24.7)
+      '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.24.7)
+      '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.24.7)
+      '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.24.7)
+      '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.24.7)
+      '@babel/plugin-syntax-unicode-sets-regex': 7.18.6(@babel/core@7.24.7)
+      '@babel/plugin-transform-arrow-functions': 7.24.7(@babel/core@7.24.7)
+      '@babel/plugin-transform-async-generator-functions': 7.24.7(@babel/core@7.24.7)
+      '@babel/plugin-transform-async-to-generator': 7.24.7(@babel/core@7.24.7)
+      '@babel/plugin-transform-block-scoped-functions': 7.24.7(@babel/core@7.24.7)
+      '@babel/plugin-transform-block-scoping': 7.24.7(@babel/core@7.24.7)
+      '@babel/plugin-transform-class-properties': 7.24.7(@babel/core@7.24.7)
+      '@babel/plugin-transform-class-static-block': 7.24.7(@babel/core@7.24.7)
+      '@babel/plugin-transform-classes': 7.24.7(@babel/core@7.24.7)
+      '@babel/plugin-transform-computed-properties': 7.24.7(@babel/core@7.24.7)
+      '@babel/plugin-transform-destructuring': 7.24.7(@babel/core@7.24.7)
+      '@babel/plugin-transform-dotall-regex': 7.24.7(@babel/core@7.24.7)
+      '@babel/plugin-transform-duplicate-keys': 7.24.7(@babel/core@7.24.7)
+      '@babel/plugin-transform-dynamic-import': 7.24.7(@babel/core@7.24.7)
+      '@babel/plugin-transform-exponentiation-operator': 7.24.7(@babel/core@7.24.7)
+      '@babel/plugin-transform-export-namespace-from': 7.24.7(@babel/core@7.24.7)
+      '@babel/plugin-transform-for-of': 7.24.7(@babel/core@7.24.7)
+      '@babel/plugin-transform-function-name': 7.24.7(@babel/core@7.24.7)
+      '@babel/plugin-transform-json-strings': 7.24.7(@babel/core@7.24.7)
+      '@babel/plugin-transform-literals': 7.24.7(@babel/core@7.24.7)
+      '@babel/plugin-transform-logical-assignment-operators': 7.24.7(@babel/core@7.24.7)
+      '@babel/plugin-transform-member-expression-literals': 7.24.7(@babel/core@7.24.7)
+      '@babel/plugin-transform-modules-amd': 7.24.7(@babel/core@7.24.7)
+      '@babel/plugin-transform-modules-commonjs': 7.24.7(@babel/core@7.24.7)
+      '@babel/plugin-transform-modules-systemjs': 7.24.7(@babel/core@7.24.7)
+      '@babel/plugin-transform-modules-umd': 7.24.7(@babel/core@7.24.7)
+      '@babel/plugin-transform-named-capturing-groups-regex': 7.24.7(@babel/core@7.24.7)
+      '@babel/plugin-transform-new-target': 7.24.7(@babel/core@7.24.7)
+      '@babel/plugin-transform-nullish-coalescing-operator': 7.24.7(@babel/core@7.24.7)
+      '@babel/plugin-transform-numeric-separator': 7.24.7(@babel/core@7.24.7)
+      '@babel/plugin-transform-object-rest-spread': 7.24.7(@babel/core@7.24.7)
+      '@babel/plugin-transform-object-super': 7.24.7(@babel/core@7.24.7)
+      '@babel/plugin-transform-optional-catch-binding': 7.24.7(@babel/core@7.24.7)
+      '@babel/plugin-transform-optional-chaining': 7.24.7(@babel/core@7.24.7)
+      '@babel/plugin-transform-parameters': 7.24.7(@babel/core@7.24.7)
+      '@babel/plugin-transform-private-methods': 7.24.7(@babel/core@7.24.7)
+      '@babel/plugin-transform-private-property-in-object': 7.24.7(@babel/core@7.24.7)
+      '@babel/plugin-transform-property-literals': 7.24.7(@babel/core@7.24.7)
+      '@babel/plugin-transform-regenerator': 7.24.7(@babel/core@7.24.7)
+      '@babel/plugin-transform-reserved-words': 7.24.7(@babel/core@7.24.7)
+      '@babel/plugin-transform-shorthand-properties': 7.24.7(@babel/core@7.24.7)
+      '@babel/plugin-transform-spread': 7.24.7(@babel/core@7.24.7)
+      '@babel/plugin-transform-sticky-regex': 7.24.7(@babel/core@7.24.7)
+      '@babel/plugin-transform-template-literals': 7.24.7(@babel/core@7.24.7)
+      '@babel/plugin-transform-typeof-symbol': 7.24.7(@babel/core@7.24.7)
+      '@babel/plugin-transform-unicode-escapes': 7.24.7(@babel/core@7.24.7)
+      '@babel/plugin-transform-unicode-property-regex': 7.24.7(@babel/core@7.24.7)
+      '@babel/plugin-transform-unicode-regex': 7.24.7(@babel/core@7.24.7)
+      '@babel/plugin-transform-unicode-sets-regex': 7.24.7(@babel/core@7.24.7)
+      '@babel/preset-modules': 0.1.6-no-external-plugins(@babel/core@7.24.7)
+      babel-plugin-polyfill-corejs2: 0.4.11(@babel/core@7.24.7)
+      babel-plugin-polyfill-corejs3: 0.10.4(@babel/core@7.24.7)
+      babel-plugin-polyfill-regenerator: 0.6.2(@babel/core@7.24.7)
+      core-js-compat: 3.37.1
       semver: 6.3.1
     transitivePeerDependencies:
       - supports-color
 
-  '@babel/preset-flow@7.23.3(@babel/core@7.24.0)':
+  '@babel/preset-flow@7.23.3(@babel/core@7.24.7)':
     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/core': 7.24.7
+      '@babel/helper-plugin-utils': 7.24.7
+      '@babel/helper-validator-option': 7.24.7
+      '@babel/plugin-transform-flow-strip-types': 7.23.3(@babel/core@7.24.7)
 
-  '@babel/preset-modules@0.1.6-no-external-plugins(@babel/core@7.24.0)':
+  '@babel/preset-modules@0.1.6-no-external-plugins(@babel/core@7.24.7)':
     dependencies:
-      '@babel/core': 7.24.0
-      '@babel/helper-plugin-utils': 7.22.5
-      '@babel/types': 7.24.0
+      '@babel/core': 7.24.7
+      '@babel/helper-plugin-utils': 7.24.7
+      '@babel/types': 7.24.7
       esutils: 2.0.3
 
-  '@babel/preset-typescript@7.23.3(@babel/core@7.24.0)':
+  '@babel/preset-typescript@7.23.3(@babel/core@7.24.7)':
     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/core': 7.24.7
+      '@babel/helper-plugin-utils': 7.24.7
+      '@babel/helper-validator-option': 7.24.7
+      '@babel/plugin-syntax-jsx': 7.23.3(@babel/core@7.24.7)
+      '@babel/plugin-transform-modules-commonjs': 7.24.7(@babel/core@7.24.7)
+      '@babel/plugin-transform-typescript': 7.23.5(@babel/core@7.24.7)
+    transitivePeerDependencies:
+      - supports-color
 
-  '@babel/register@7.22.15(@babel/core@7.24.0)':
+  '@babel/register@7.22.15(@babel/core@7.24.7)':
     dependencies:
-      '@babel/core': 7.24.0
+      '@babel/core': 7.24.7
       clone-deep: 4.0.1
       find-cache-dir: 2.1.0
       make-dir: 2.1.0
@@ -12749,77 +13439,77 @@ snapshots:
   '@babel/template@7.22.15':
     dependencies:
       '@babel/code-frame': 7.23.5
-      '@babel/parser': 7.23.9
-      '@babel/types': 7.24.0
+      '@babel/parser': 7.24.7
+      '@babel/types': 7.24.7
 
   '@babel/template@7.24.0':
     dependencies:
-      '@babel/code-frame': 7.23.5
-      '@babel/parser': 7.24.5
-      '@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.24.7
+      '@babel/types': 7.24.7
 
   '@babel/traverse@7.23.5':
     dependencies:
       '@babel/code-frame': 7.23.5
-      '@babel/generator': 7.23.5
+      '@babel/generator': 7.24.7
       '@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.24.7
+      '@babel/types': 7.24.7
+      debug: 4.3.5(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.5
-      '@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.24.7
+      '@babel/types': 7.24.7
+      debug: 4.3.5(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
-      to-fast-properties: 2.0.0
-
-  '@babel/types@7.24.0':
-    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
 
   '@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@5.21.1(@bull-board/ui@5.21.1)':
     dependencies:
-      '@bull-board/ui': 5.17.0
+      '@bull-board/ui': 5.21.1
       redis-info: 3.1.0
 
-  '@bull-board/fastify@5.17.0':
+  '@bull-board/fastify@5.21.1':
     dependencies:
-      '@bull-board/api': 5.17.0(@bull-board/ui@5.17.0)
-      '@bull-board/ui': 5.17.0
+      '@bull-board/api': 5.21.1(@bull-board/ui@5.21.1)
+      '@bull-board/ui': 5.21.1
       '@fastify/static': 6.12.0
       '@fastify/view': 8.2.0
-      ejs: 3.1.9
+      ejs: 3.1.10
 
-  '@bull-board/ui@5.17.0':
+  '@bull-board/ui@5.21.1':
     dependencies:
-      '@bull-board/api': 5.17.0(@bull-board/ui@5.17.0)
+      '@bull-board/api': 5.21.1(@bull-board/ui@5.21.1)
 
   '@bundled-es-modules/cookie@2.0.0':
     dependencies:
@@ -12829,76 +13519,81 @@ 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.1':
     dependencies:
-      '@cropper/element': 2.0.0-beta.5
-      '@cropper/utils': 2.0.0-beta.5
+      '@cropper/element': 2.0.0-rc.1
+      '@cropper/utils': 2.0.0-rc.1
 
-  '@cropper/element-crosshair@2.0.0-beta.5':
+  '@cropper/element-crosshair@2.0.0-rc.1':
     dependencies:
-      '@cropper/element': 2.0.0-beta.5
-      '@cropper/utils': 2.0.0-beta.5
+      '@cropper/element': 2.0.0-rc.1
+      '@cropper/utils': 2.0.0-rc.1
 
-  '@cropper/element-grid@2.0.0-beta.5':
+  '@cropper/element-grid@2.0.0-rc.1':
     dependencies:
-      '@cropper/element': 2.0.0-beta.5
-      '@cropper/utils': 2.0.0-beta.5
+      '@cropper/element': 2.0.0-rc.1
+      '@cropper/utils': 2.0.0-rc.1
 
-  '@cropper/element-handle@2.0.0-beta.5':
+  '@cropper/element-handle@2.0.0-rc.1':
     dependencies:
-      '@cropper/element': 2.0.0-beta.5
-      '@cropper/utils': 2.0.0-beta.5
+      '@cropper/element': 2.0.0-rc.1
+      '@cropper/utils': 2.0.0-rc.1
 
-  '@cropper/element-image@2.0.0-beta.5':
+  '@cropper/element-image@2.0.0-rc.1':
     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.1
+      '@cropper/element-canvas': 2.0.0-rc.1
+      '@cropper/utils': 2.0.0-rc.1
 
-  '@cropper/element-selection@2.0.0-beta.5':
+  '@cropper/element-selection@2.0.0-rc.1':
     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.1
+      '@cropper/element-canvas': 2.0.0-rc.1
+      '@cropper/element-image': 2.0.0-rc.1
+      '@cropper/utils': 2.0.0-rc.1
 
-  '@cropper/element-shade@2.0.0-beta.5':
+  '@cropper/element-shade@2.0.0-rc.1':
     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.1
+      '@cropper/element-canvas': 2.0.0-rc.1
+      '@cropper/element-selection': 2.0.0-rc.1
+      '@cropper/utils': 2.0.0-rc.1
 
-  '@cropper/element-viewer@2.0.0-beta.5':
+  '@cropper/element-viewer@2.0.0-rc.1':
     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.1
+      '@cropper/element-canvas': 2.0.0-rc.1
+      '@cropper/element-image': 2.0.0-rc.1
+      '@cropper/element-selection': 2.0.0-rc.1
+      '@cropper/utils': 2.0.0-rc.1
 
-  '@cropper/element@2.0.0-beta.5':
+  '@cropper/element@2.0.0-rc.1':
     dependencies:
-      '@cropper/utils': 2.0.0-beta.5
+      '@cropper/utils': 2.0.0-rc.1
 
-  '@cropper/elements@2.0.0-beta.5':
+  '@cropper/elements@2.0.0-rc.1':
     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.1
+      '@cropper/element-canvas': 2.0.0-rc.1
+      '@cropper/element-crosshair': 2.0.0-rc.1
+      '@cropper/element-grid': 2.0.0-rc.1
+      '@cropper/element-handle': 2.0.0-rc.1
+      '@cropper/element-image': 2.0.0-rc.1
+      '@cropper/element-selection': 2.0.0-rc.1
+      '@cropper/element-shade': 2.0.0-rc.1
+      '@cropper/element-viewer': 2.0.0-rc.1
 
-  '@cropper/utils@2.0.0-beta.5': {}
+  '@cropper/utils@2.0.0-rc.1': {}
 
   '@cypress/request@3.0.0':
     dependencies:
@@ -12917,7 +13612,7 @@ snapshots:
       performance-now: 2.1.0
       qs: 6.10.4
       safe-buffer: 5.2.1
-      tough-cookie: 4.1.3
+      tough-cookie: 4.1.4
       tunnel-agent: 0.6.0
       uuid: 8.3.2
 
@@ -12928,10 +13623,10 @@ 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
@@ -12947,7 +13642,7 @@ snapshots:
 
   '@emnapi/runtime@1.1.1':
     dependencies:
-      tslib: 2.6.2
+      tslib: 2.6.3
     optional: true
 
   '@emotion/use-insertion-effect-with-fallbacks@1.0.1(react@18.3.1)':
@@ -12957,7 +13652,10 @@ snapshots:
   '@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/android-arm64@0.18.20':
@@ -12966,7 +13664,10 @@ 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-arm@0.18.20':
@@ -12975,7 +13676,10 @@ 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-x64@0.18.20':
@@ -12984,7 +13688,10 @@ 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/darwin-arm64@0.18.20':
@@ -12993,7 +13700,10 @@ 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-x64@0.18.20':
@@ -13002,7 +13712,10 @@ 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/freebsd-arm64@0.18.20':
@@ -13011,7 +13724,10 @@ 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-x64@0.18.20':
@@ -13020,7 +13736,10 @@ 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/linux-arm64@0.18.20':
@@ -13029,7 +13748,10 @@ 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-arm@0.18.20':
@@ -13038,7 +13760,10 @@ 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-ia32@0.18.20':
@@ -13047,7 +13772,10 @@ 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-loong64@0.18.20':
@@ -13056,7 +13784,10 @@ 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-mips64el@0.18.20':
@@ -13065,7 +13796,10 @@ 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-ppc64@0.18.20':
@@ -13074,7 +13808,10 @@ 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-riscv64@0.18.20':
@@ -13083,7 +13820,10 @@ 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-s390x@0.18.20':
@@ -13092,7 +13832,10 @@ 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-x64@0.18.20':
@@ -13101,7 +13844,10 @@ 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/netbsd-x64@0.18.20':
@@ -13110,7 +13856,13 @@ 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/openbsd-arm64@0.23.0':
     optional: true
 
   '@esbuild/openbsd-x64@0.18.20':
@@ -13119,7 +13871,10 @@ 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/sunos-x64@0.18.20':
@@ -13128,7 +13883,10 @@ 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/win32-arm64@0.18.20':
@@ -13137,7 +13895,10 @@ 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-ia32@0.18.20':
@@ -13146,7 +13907,10 @@ 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-x64@0.18.20':
@@ -13155,25 +13919,40 @@ 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)':
-    dependencies:
-      eslint: 8.53.0
-      eslint-visitor-keys: 3.4.3
+  '@esbuild/win32-x64@0.23.0':
+    optional: true
 
   '@eslint-community/eslint-utils@4.4.0(eslint@8.57.0)':
     dependencies:
       eslint: 8.57.0
       eslint-visitor-keys: 3.4.3
 
-  '@eslint-community/regexpp@4.10.0': {}
+  '@eslint-community/eslint-utils@4.4.0(eslint@9.8.0)':
+    dependencies:
+      eslint: 9.8.0
+      eslint-visitor-keys: 3.4.3
+
+  '@eslint-community/regexpp@4.11.0': {}
+
+  '@eslint-community/regexpp@4.6.2': {}
+
+  '@eslint/compat@1.1.1': {}
+
+  '@eslint/config-array@0.17.1':
+    dependencies:
+      '@eslint/object-schema': 2.1.4
+      debug: 4.3.5(supports-color@8.1.1)
+      minimatch: 3.1.2
+    transitivePeerDependencies:
+      - supports-color
 
   '@eslint/eslintrc@2.1.4':
     dependencies:
       ajv: 6.12.6
-      debug: 4.3.4(supports-color@8.1.1)
+      debug: 4.3.5(supports-color@8.1.1)
       espree: 9.6.1
       globals: 13.24.0
       ignore: 5.3.1
@@ -13184,10 +13963,26 @@ snapshots:
     transitivePeerDependencies:
       - supports-color
 
-  '@eslint/js@8.53.0': {}
+  '@eslint/eslintrc@3.1.0':
+    dependencies:
+      ajv: 6.12.6
+      debug: 4.3.5(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
+      strip-json-comments: 3.1.1
+    transitivePeerDependencies:
+      - supports-color
 
   '@eslint/js@8.57.0': {}
 
+  '@eslint/js@9.8.0': {}
+
+  '@eslint/object-schema@2.1.4': {}
+
   '@fal-works/esbuild-plugin-global-externals@2.1.2': {}
 
   '@fastify/accept-negotiator@1.0.0': {}
@@ -13199,8 +13994,8 @@ snapshots:
 
   '@fastify/ajv-compiler@3.5.0':
     dependencies:
-      ajv: 8.13.0
-      ajv-formats: 2.1.1(ajv@8.13.0)
+      ajv: 8.17.1
+      ajv-formats: 2.1.1(ajv@8.17.1)
       fast-uri: 2.2.0
 
   '@fastify/busboy@1.2.1':
@@ -13239,12 +14034,12 @@ snapshots:
       '@fastify/reply-from': 9.0.1
       fast-querystring: 1.1.2
       fastify-plugin: 4.5.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)
     transitivePeerDependencies:
       - bufferutil
       - utf-8-validate
 
-  '@fastify/multipart@8.2.0':
+  '@fastify/multipart@8.3.0':
     dependencies:
       '@fastify/busboy': 2.1.0
       '@fastify/deepmerge': 1.3.0
@@ -13280,14 +14075,14 @@ snapshots:
       glob: 8.1.0
       p-limit: 3.1.0
 
-  '@fastify/static@7.0.3':
+  '@fastify/static@7.0.4':
     dependencies:
       '@fastify/accept-negotiator': 1.0.0
       '@fastify/send': 2.0.1
       content-disposition: 0.5.4
       fastify-plugin: 4.5.0
       fastq: 1.17.1
-      glob: 10.3.12
+      glob: 10.4.2
 
   '@fastify/view@8.2.0':
     dependencies:
@@ -13303,13 +14098,11 @@ snapshots:
 
   '@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': {}
 
@@ -13321,22 +14114,14 @@ 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)
+      '@humanwhocodes/object-schema': 2.0.3
+      debug: 4.3.5(supports-color@8.1.1)
       minimatch: 3.1.2
     transitivePeerDependencies:
       - supports-color
@@ -13345,16 +14130,16 @@ snapshots:
 
   '@humanwhocodes/momoa@2.0.4': {}
 
-  '@humanwhocodes/object-schema@2.0.1': {}
+  '@humanwhocodes/object-schema@2.0.3': {}
 
-  '@humanwhocodes/object-schema@2.0.2': {}
+  '@humanwhocodes/retry@0.3.0': {}
 
-  '@img/sharp-darwin-arm64@0.33.3':
+  '@img/sharp-darwin-arm64@0.33.4':
     optionalDependencies:
       '@img/sharp-libvips-darwin-arm64': 1.0.2
     optional: true
 
-  '@img/sharp-darwin-x64@0.33.3':
+  '@img/sharp-darwin-x64@0.33.4':
     optionalDependencies:
       '@img/sharp-libvips-darwin-x64': 1.0.2
     optional: true
@@ -13383,45 +14168,45 @@ snapshots:
   '@img/sharp-libvips-linuxmusl-x64@1.0.2':
     optional: true
 
-  '@img/sharp-linux-arm64@0.33.3':
+  '@img/sharp-linux-arm64@0.33.4':
     optionalDependencies:
       '@img/sharp-libvips-linux-arm64': 1.0.2
     optional: true
 
-  '@img/sharp-linux-arm@0.33.3':
+  '@img/sharp-linux-arm@0.33.4':
     optionalDependencies:
       '@img/sharp-libvips-linux-arm': 1.0.2
     optional: true
 
-  '@img/sharp-linux-s390x@0.33.3':
+  '@img/sharp-linux-s390x@0.33.4':
     optionalDependencies:
       '@img/sharp-libvips-linux-s390x': 1.0.2
     optional: true
 
-  '@img/sharp-linux-x64@0.33.3':
+  '@img/sharp-linux-x64@0.33.4':
     optionalDependencies:
       '@img/sharp-libvips-linux-x64': 1.0.2
     optional: true
 
-  '@img/sharp-linuxmusl-arm64@0.33.3':
+  '@img/sharp-linuxmusl-arm64@0.33.4':
     optionalDependencies:
       '@img/sharp-libvips-linuxmusl-arm64': 1.0.2
     optional: true
 
-  '@img/sharp-linuxmusl-x64@0.33.3':
+  '@img/sharp-linuxmusl-x64@0.33.4':
     optionalDependencies:
       '@img/sharp-libvips-linuxmusl-x64': 1.0.2
     optional: true
 
-  '@img/sharp-wasm32@0.33.3':
+  '@img/sharp-wasm32@0.33.4':
     dependencies:
       '@emnapi/runtime': 1.1.1
     optional: true
 
-  '@img/sharp-win32-ia32@0.33.3':
+  '@img/sharp-win32-ia32@0.33.4':
     optional: true
 
-  '@img/sharp-win32-x64@0.33.3':
+  '@img/sharp-win32-x64@0.33.4':
     optional: true
 
   '@inquirer/confirm@3.1.6':
@@ -13434,7 +14219,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,7 +14242,7 @@ snapshots:
   '@intlify/message-compiler@9.13.1':
     dependencies:
       '@intlify/shared': 9.13.1
-      source-map-js: 1.0.2
+      source-map-js: 1.2.0
 
   '@intlify/shared@9.13.1': {}
 
@@ -13485,7 +14270,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
@@ -13498,14 +14283,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
@@ -13517,7 +14302,7 @@ snapshots:
       jest-util: 29.7.0
       jest-validate: 29.7.0
       jest-watcher: 29.7.0
-      micromatch: 4.0.5
+      micromatch: 4.0.7
       pretty-format: 29.7.0
       slash: 3.0.0
       strip-ansi: 6.0.1
@@ -13534,7 +14319,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':
@@ -13552,7 +14337,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
@@ -13574,7 +14359,7 @@ snapshots:
       '@jest/transform': 29.7.0
       '@jest/types': 29.6.3
       '@jridgewell/trace-mapping': 0.3.18
-      '@types/node': 20.12.7
+      '@types/node': 20.14.12
       chalk: 4.1.2
       collect-v8-coverage: 1.0.1
       exit: 0.1.2
@@ -13621,7 +14406,7 @@ snapshots:
 
   '@jest/transform@29.7.0':
     dependencies:
-      '@babel/core': 7.24.0
+      '@babel/core': 7.24.7
       '@jest/types': 29.6.3
       '@jridgewell/trace-mapping': 0.3.18
       babel-plugin-istanbul: 6.1.1
@@ -13632,7 +14417,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.7
       pirates: 4.0.5
       slash: 3.0.0
       write-file-atomic: 4.0.2
@@ -13644,19 +14429,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.1(typescript@5.5.4)(vite@5.3.5(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3))':
     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.5.4)
+      vite: 5.3.5(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3)
     optionalDependencies:
-      typescript: 5.4.5
+      typescript: 5.5.4
 
   '@jridgewell/gen-mapping@0.3.2':
     dependencies:
@@ -13664,14 +14449,22 @@ 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': {}
 
@@ -13682,6 +14475,11 @@ snapshots:
       '@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': {}
@@ -13704,23 +14502,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.4(@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.5.1(@types/node@20.14.12)
     transitivePeerDependencies:
       - '@types/node'
 
-  '@microsoft/api-extractor@7.43.1(@types/node@20.12.7)':
+  '@microsoft/api-extractor@7.47.4(@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.4(@types/node@20.14.12)
+      '@microsoft/tsdoc': 0.15.0
+      '@microsoft/tsdoc-config': 0.17.0
+      '@rushstack/node-core-library': 5.5.1(@types/node@20.14.12)
+      '@rushstack/rig-package': 0.5.3
+      '@rushstack/terminal': 0.13.3(@types/node@20.14.12)
+      '@rushstack/ts-command-line': 4.22.3(@types/node@20.14.12)
       lodash: 4.17.21
       minimatch: 3.0.8
       resolve: 1.22.8
@@ -13730,43 +14528,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.5.4))(eslint@9.8.0)(typescript@5.5.4))(@typescript-eslint/parser@7.17.0(eslint@9.8.0)(typescript@5.5.4))(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.17.0(eslint@9.8.0)(typescript@5.5.4))(eslint@9.8.0))(eslint@9.8.0)(globals@15.8.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.5.4))(eslint@9.8.0)(typescript@5.5.4)
+      '@typescript-eslint/parser': 7.17.0(eslint@9.8.0)(typescript@5.5.4)
+      eslint: 9.8.0
+      eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.17.0(eslint@9.8.0)(typescript@5.5.4))(eslint@9.8.0)
+      globals: 15.8.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.4
 
   '@misskey-dev/summaly@5.1.0':
     dependencies:
@@ -13808,9 +14594,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
@@ -13819,94 +14603,90 @@ snapshots:
       outvariant: 1.4.2
       strict-event-emitter: 0.5.1
 
-  '@napi-rs/canvas-android-arm64@0.1.52':
+  '@napi-rs/canvas-android-arm64@0.1.53':
     optional: true
 
-  '@napi-rs/canvas-darwin-arm64@0.1.52':
+  '@napi-rs/canvas-darwin-arm64@0.1.53':
     optional: true
 
-  '@napi-rs/canvas-darwin-x64@0.1.52':
+  '@napi-rs/canvas-darwin-x64@0.1.53':
     optional: true
 
-  '@napi-rs/canvas-linux-arm-gnueabihf@0.1.52':
+  '@napi-rs/canvas-linux-arm-gnueabihf@0.1.53':
     optional: true
 
-  '@napi-rs/canvas-linux-arm64-gnu@0.1.52':
+  '@napi-rs/canvas-linux-arm64-gnu@0.1.53':
     optional: true
 
-  '@napi-rs/canvas-linux-arm64-musl@0.1.52':
+  '@napi-rs/canvas-linux-arm64-musl@0.1.53':
     optional: true
 
-  '@napi-rs/canvas-linux-x64-gnu@0.1.52':
+  '@napi-rs/canvas-linux-x64-gnu@0.1.53':
     optional: true
 
-  '@napi-rs/canvas-linux-x64-musl@0.1.52':
+  '@napi-rs/canvas-linux-x64-musl@0.1.53':
     optional: true
 
-  '@napi-rs/canvas-win32-x64-msvc@0.1.52':
+  '@napi-rs/canvas-win32-x64-msvc@0.1.53':
     optional: true
 
-  '@napi-rs/canvas@0.1.52':
+  '@napi-rs/canvas@0.1.53':
     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
+      '@napi-rs/canvas-android-arm64': 0.1.53
+      '@napi-rs/canvas-darwin-arm64': 0.1.53
+      '@napi-rs/canvas-darwin-x64': 0.1.53
+      '@napi-rs/canvas-linux-arm-gnueabihf': 0.1.53
+      '@napi-rs/canvas-linux-arm64-gnu': 0.1.53
+      '@napi-rs/canvas-linux-arm64-musl': 0.1.53
+      '@napi-rs/canvas-linux-x64-gnu': 0.1.53
+      '@napi-rs/canvas-linux-x64-musl': 0.1.53
+      '@napi-rs/canvas-win32-x64-msvc': 0.1.53
 
-  '@ndelangen/get-tarball@3.0.7':
-    dependencies:
-      gunzip-maybe: 1.4.2
-      pump: 3.0.0
-      tar-fs: 2.1.1
-
-  '@nestjs/common@10.3.8(reflect-metadata@0.2.2)(rxjs@7.8.1)':
+  '@nestjs/common@10.3.10(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.6.3
       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.3.10(@nestjs/common@10.3.10(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.3.10)(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.3.10(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
       reflect-metadata: 0.2.2
       rxjs: 7.8.1
-      tslib: 2.6.2
+      tslib: 2.6.3
       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.3.10(@nestjs/common@10.3.10(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.3.10)
     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.3.10(@nestjs/common@10.3.10(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.3.10)':
     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)
+      '@nestjs/common': 10.3.10(reflect-metadata@0.2.2)(rxjs@7.8.1)
+      '@nestjs/core': 10.3.10(@nestjs/common@10.3.10(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.3.10)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.1)
       body-parser: 1.20.2
       cors: 2.8.5
       express: 4.19.2
       multer: 1.4.4-lts.1
-      tslib: 2.6.2
+      tslib: 2.6.3
     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.3.10(@nestjs/common@10.3.10(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.3.10(@nestjs/common@10.3.10(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.3.10)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.3.10(@nestjs/common@10.3.10(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.3.10))':
     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.3.10(reflect-metadata@0.2.2)(rxjs@7.8.1)
+      '@nestjs/core': 10.3.10(@nestjs/common@10.3.10(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.3.10)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.1)
+      tslib: 2.6.3
     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.3.10(@nestjs/common@10.3.10(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.3.10)
+
+  '@noble/hashes@1.4.0': {}
 
   '@nodelib/fs.scandir@2.1.5':
     dependencies:
@@ -13918,13 +14698,13 @@ 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
+      http-proxy-agent: 7.0.2
+      https-proxy-agent: 7.0.4
       lru-cache: 10.2.2
       socks-proxy-agent: 8.0.2
     transitivePeerDependencies:
@@ -13953,155 +14733,167 @@ 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
+      '@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.0
     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
       shimmer: 1.2.1
@@ -14109,12 +14901,12 @@ snapshots:
       - 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
       shimmer: 1.2.1
@@ -14123,58 +14915,66 @@ snapshots:
 
   '@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.6.3
 
   '@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.6.3
 
   '@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.6.3
 
   '@peculiar/asn1-schema@2.3.8':
     dependencies:
       asn1js: 3.0.5
       pvtsutils: 1.3.5
-      tslib: 2.6.2
+      tslib: 2.6.3
 
   '@peculiar/asn1-x509@2.3.8':
     dependencies:
@@ -14182,7 +14982,7 @@ snapshots:
       asn1js: 3.0.5
       ipaddr.js: 2.2.0
       pvtsutils: 1.3.5
-      tslib: 2.6.2
+      tslib: 2.6.3
 
   '@peertube/http-signature@1.7.0':
     dependencies:
@@ -14197,35 +14997,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)':
-    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)':
+  '@readme/better-ajv-errors@1.6.0(ajv@8.17.1)':
     dependencies:
       '@babel/code-frame': 7.23.5
       '@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
@@ -14243,181 +15028,189 @@ snapshots:
       '@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)
+      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)':
+  '@rollup/plugin-json@6.1.0(rollup@4.19.1)':
     dependencies:
-      '@rollup/pluginutils': 5.1.0(rollup@4.17.2)
+      '@rollup/pluginutils': 5.1.0(rollup@4.19.1)
     optionalDependencies:
-      rollup: 4.17.2
+      rollup: 4.19.1
 
-  '@rollup/plugin-replace@5.0.5(rollup@4.17.2)':
+  '@rollup/plugin-replace@5.0.7(rollup@4.19.1)':
     dependencies:
-      '@rollup/pluginutils': 5.1.0(rollup@4.17.2)
-      magic-string: 0.30.7
+      '@rollup/pluginutils': 5.1.0(rollup@4.19.1)
+      magic-string: 0.30.10
     optionalDependencies:
-      rollup: 4.17.2
+      rollup: 4.19.1
 
-  '@rollup/pluginutils@5.1.0(rollup@4.17.2)':
+  '@rollup/pluginutils@5.1.0(rollup@4.19.1)':
     dependencies:
       '@types/estree': 1.0.5
       estree-walker: 2.0.2
       picomatch: 2.3.1
     optionalDependencies:
-      rollup: 4.17.2
+      rollup: 4.19.1
 
-  '@rollup/rollup-android-arm-eabi@4.17.2':
+  '@rollup/rollup-android-arm-eabi@4.19.1':
     optional: true
 
-  '@rollup/rollup-android-arm64@4.17.2':
+  '@rollup/rollup-android-arm64@4.19.1':
     optional: true
 
-  '@rollup/rollup-darwin-arm64@4.17.2':
+  '@rollup/rollup-darwin-arm64@4.19.1':
     optional: true
 
-  '@rollup/rollup-darwin-x64@4.17.2':
+  '@rollup/rollup-darwin-x64@4.19.1':
     optional: true
 
-  '@rollup/rollup-linux-arm-gnueabihf@4.17.2':
+  '@rollup/rollup-linux-arm-gnueabihf@4.19.1':
     optional: true
 
-  '@rollup/rollup-linux-arm-musleabihf@4.17.2':
+  '@rollup/rollup-linux-arm-musleabihf@4.19.1':
     optional: true
 
-  '@rollup/rollup-linux-arm64-gnu@4.17.2':
+  '@rollup/rollup-linux-arm64-gnu@4.19.1':
     optional: true
 
-  '@rollup/rollup-linux-arm64-musl@4.17.2':
+  '@rollup/rollup-linux-arm64-musl@4.19.1':
     optional: true
 
-  '@rollup/rollup-linux-powerpc64le-gnu@4.17.2':
+  '@rollup/rollup-linux-powerpc64le-gnu@4.19.1':
     optional: true
 
-  '@rollup/rollup-linux-riscv64-gnu@4.17.2':
+  '@rollup/rollup-linux-riscv64-gnu@4.19.1':
     optional: true
 
-  '@rollup/rollup-linux-s390x-gnu@4.17.2':
+  '@rollup/rollup-linux-s390x-gnu@4.19.1':
     optional: true
 
-  '@rollup/rollup-linux-x64-gnu@4.17.2':
+  '@rollup/rollup-linux-x64-gnu@4.19.1':
     optional: true
 
-  '@rollup/rollup-linux-x64-musl@4.17.2':
+  '@rollup/rollup-linux-x64-musl@4.19.1':
     optional: true
 
-  '@rollup/rollup-win32-arm64-msvc@4.17.2':
+  '@rollup/rollup-win32-arm64-msvc@4.19.1':
     optional: true
 
-  '@rollup/rollup-win32-ia32-msvc@4.17.2':
+  '@rollup/rollup-win32-ia32-msvc@4.19.1':
     optional: true
 
-  '@rollup/rollup-win32-x64-msvc@4.17.2':
+  '@rollup/rollup-win32-x64-msvc@4.19.1':
     optional: true
 
-  '@rushstack/node-core-library@4.1.0(@types/node@20.12.7)':
+  '@rushstack/node-core-library@5.5.1(@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.13.3(@types/node@20.14.12)':
     dependencies:
-      '@rushstack/node-core-library': 4.1.0(@types/node@20.12.7)
+      '@rushstack/node-core-library': 5.5.1(@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.3(@types/node@20.14.12)':
     dependencies:
-      '@rushstack/terminal': 0.10.1(@types/node@20.12.7)
+      '@rushstack/terminal': 0.13.3(@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.12.0':
+    dependencies:
+      '@types/hast': 3.0.4
 
   '@sideway/address@4.1.4':
     dependencies:
@@ -14427,7 +15220,7 @@ snapshots:
 
   '@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
@@ -14449,7 +15242,11 @@ snapshots:
 
   '@sindresorhus/is@5.3.0': {}
 
-  '@sindresorhus/is@6.1.0': {}
+  '@sindresorhus/is@7.0.0': {}
+
+  '@sindresorhus/merge-streams@2.3.0': {}
+
+  '@sindresorhus/merge-streams@4.0.0': {}
 
   '@sinonjs/commons@2.0.0':
     dependencies:
@@ -14475,155 +15272,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
 
-  '@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.6.3
 
-  '@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.6.3
 
-  '@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.6.3
 
-  '@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.6.3
 
-  '@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.6.3
 
-  '@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.6.3
 
-  '@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.6.3
 
-  '@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.6.3
 
-  '@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.6.3
 
-  '@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.6.3
 
-  '@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.6.3
 
-  '@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.6.3
 
-  '@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.6.3
 
-  '@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.6.3
+
+  '@smithy/hash-stream-node@3.1.2':
+    dependencies:
+      '@smithy/types': 3.3.0
+      '@smithy/util-utf8': 3.0.0
+      tslib: 2.6.3
+
+  '@smithy/invalid-dependency@3.0.3':
+    dependencies:
+      '@smithy/types': 3.3.0
+      tslib: 2.6.3
 
   '@smithy/is-array-buffer@2.0.0':
     dependencies:
-      tslib: 2.6.2
+      tslib: 2.6.3
 
-  '@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.6.3
 
-  '@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.6.3
 
-  '@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.6.3
 
-  '@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.6.3
 
-  '@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.6.3
+      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.6.3
 
-  '@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.6.3
+
+  '@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.6.3
 
   '@smithy/node-http-handler@2.5.0':
     dependencies:
@@ -14633,26 +15448,28 @@ 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.6.3
 
-  '@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.6.3
 
   '@smithy/protocol-http@3.3.0':
     dependencies:
       '@smithy/types': 2.12.0
       tslib: 2.6.2
 
-  '@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.6.3
 
   '@smithy/querystring-builder@2.2.0':
     dependencies:
@@ -14660,226 +15477,234 @@ snapshots:
       '@smithy/util-uri-escape': 2.2.0
       tslib: 2.6.2
 
-  '@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.6.3
 
-  '@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.6.3
 
-  '@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.6.3
 
-  '@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.6.3
+
+  '@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.6.3
 
   '@smithy/types@2.12.0':
     dependencies:
       tslib: 2.6.2
 
-  '@smithy/types@2.6.0':
+  '@smithy/types@3.3.0':
     dependencies:
-      tslib: 2.6.2
+      tslib: 2.6.3
 
-  '@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.6.3
 
-  '@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.6.3
 
-  '@smithy/util-body-length-browser@2.0.0':
+  '@smithy/util-body-length-browser@3.0.0':
     dependencies:
-      tslib: 2.6.2
+      tslib: 2.6.3
 
-  '@smithy/util-body-length-node@2.1.0':
+  '@smithy/util-body-length-node@3.0.0':
     dependencies:
-      tslib: 2.6.2
+      tslib: 2.6.3
 
   '@smithy/util-buffer-from@2.0.0':
     dependencies:
       '@smithy/is-array-buffer': 2.0.0
-      tslib: 2.6.2
+      tslib: 2.6.3
 
-  '@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.6.3
 
-  '@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.6.3
+
+  '@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.6.3
 
-  '@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.6.3
 
-  '@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.6.3
 
-  '@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.6.3
 
-  '@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.6.3
 
-  '@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.6.3
 
-  '@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.6.3
 
   '@smithy/util-uri-escape@2.2.0':
     dependencies:
       tslib: 2.6.2
 
+  '@smithy/util-uri-escape@3.0.0':
+    dependencies:
+      tslib: 2.6.3
+
   '@smithy/util-utf8@2.0.0':
     dependencies:
       '@smithy/util-buffer-from': 2.0.0
-      tslib: 2.6.2
+      tslib: 2.6.3
 
-  '@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.6.3
+
+  '@smithy/util-waiter@3.1.2':
+    dependencies:
+      '@smithy/abort-controller': 3.1.1
+      '@smithy/types': 3.3.0
+      tslib: 2.6.3
 
   '@sqltools/formatter@1.2.5': {}
 
-  '@storybook/addon-actions@8.0.9':
+  '@storybook/addon-actions@8.2.6(storybook@8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(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.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4)
       uuid: 9.0.1
 
-  '@storybook/addon-backgrounds@8.0.9':
+  '@storybook/addon-backgrounds@8.2.6(storybook@8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4))':
     dependencies:
       '@storybook/global': 5.0.0
       memoizerific: 1.11.3
+      storybook: 8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(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.2.6(storybook@8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(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)
+      dequal: 2.0.3
       lodash: 4.17.21
+      storybook: 8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(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-docs@8.0.9(encoding@0.1.13)':
+  '@storybook/addon-docs@8.2.6(storybook@8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4))':
     dependencies:
-      '@babel/core': 7.24.0
+      '@babel/core': 7.24.7
       '@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/blocks': 8.2.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4))
+      '@storybook/csf-plugin': 8.2.6(storybook@8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4))
       '@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
+      '@storybook/react-dom-shim': 8.2.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(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.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(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.2.6(storybook@8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(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.2.6(storybook@8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4))
+      '@storybook/addon-backgrounds': 8.2.6(storybook@8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4))
+      '@storybook/addon-controls': 8.2.6(storybook@8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4))
+      '@storybook/addon-docs': 8.2.6(storybook@8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4))
+      '@storybook/addon-highlight': 8.2.6(storybook@8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4))
+      '@storybook/addon-measure': 8.2.6(storybook@8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4))
+      '@storybook/addon-outline': 8.2.6(storybook@8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4))
+      '@storybook/addon-toolbars': 8.2.6(storybook@8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4))
+      '@storybook/addon-viewport': 8.2.6(storybook@8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4))
+      storybook: 8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(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.2.6(storybook@8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4))':
     dependencies:
       '@storybook/global': 5.0.0
+      storybook: 8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(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.2.6(@jest/globals@29.7.0)(@types/jest@29.5.12)(jest@29.7.0(@types/node@20.14.12))(storybook@8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4))(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.77.8)(terser@5.31.3))':
     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.2.6(storybook@8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4))
+      '@storybook/test': 8.2.6(@jest/globals@29.7.0)(@types/jest@29.5.12)(jest@29.7.0(@types/node@20.14.12))(storybook@8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4))(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.77.8)(terser@5.31.3))
       polished: 4.2.2
+      storybook: 8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4)
       ts-dedent: 2.2.0
     transitivePeerDependencies:
       - '@jest/globals'
@@ -14888,89 +15713,83 @@ snapshots:
       - jest
       - vitest
 
-  '@storybook/addon-links@8.0.9(react@18.3.1)':
+  '@storybook/addon-links@8.2.6(react@18.3.1)(storybook@8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(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.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(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.2.6(storybook@8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4))':
     dependencies:
-      '@storybook/node-logger': 8.0.9
       remark-gfm: 4.0.0
+      storybook: 8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(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.2.6(storybook@8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4))':
     dependencies:
       '@storybook/global': 5.0.0
-      tiny-invariant: 1.3.1
+      storybook: 8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(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.2.6(storybook@8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4))':
     dependencies:
       '@storybook/global': 5.0.0
+      storybook: 8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(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.2.6(storybook@8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4))':
     dependencies:
-      '@storybook/source-loader': 8.0.9
+      '@storybook/source-loader': 8.2.6(storybook@8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4))
       estraverse: 5.3.0
-      tiny-invariant: 1.3.1
+      storybook: 8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(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.2.6(storybook@8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4))':
+    dependencies:
+      storybook: 8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4)
 
-  '@storybook/addon-viewport@8.0.9':
+  '@storybook/addon-viewport@8.2.6(storybook@8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4))':
     dependencies:
       memoizerific: 1.11.3
+      storybook: 8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(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.2.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(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
       '@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.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(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-manager@8.1.11(encoding@0.1.13)(prettier@3.3.3)':
     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
+      '@storybook/core-common': 8.1.11(encoding@0.1.13)(prettier@3.3.3)
+      '@storybook/manager': 8.1.11
+      '@storybook/node-logger': 8.1.11
       '@types/ejs': 3.1.2
-      '@yarnpkg/esbuild-plugin-pnp': 3.0.0-rc.15(esbuild@0.20.2)
+      '@yarnpkg/esbuild-plugin-pnp': 3.0.0-rc.15(esbuild@0.19.11)
       browser-assert: 1.2.1
-      ejs: 3.1.9
-      esbuild: 0.20.2
+      ejs: 3.1.10
+      esbuild: 0.19.11
       esbuild-plugin-alias: 0.2.1
       express: 4.19.2
       fs-extra: 11.1.1
@@ -14978,187 +15797,158 @@ snapshots:
       util: 0.12.5
     transitivePeerDependencies:
       - encoding
+      - prettier
       - 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))':
+  '@storybook/builder-vite@8.1.11(encoding@0.1.13)(prettier@3.3.3)(typescript@5.5.4)(vite@5.3.5(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.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/channels': 8.1.11
+      '@storybook/client-logger': 8.1.11
+      '@storybook/core-common': 8.1.11(encoding@0.1.13)(prettier@3.3.3)
+      '@storybook/core-events': 8.1.11
+      '@storybook/csf-plugin': 8.1.11
+      '@storybook/node-logger': 8.1.11
+      '@storybook/preview': 8.1.11
+      '@storybook/preview-api': 8.1.11
+      '@storybook/types': 8.1.11
       '@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.19.2
       find-cache-dir: 3.3.2
       fs-extra: 11.1.1
-      magic-string: 0.30.7
+      magic-string: 0.30.10
       ts-dedent: 2.2.0
-      vite: 5.2.11(@types/node@20.12.7)(sass@1.76.0)(terser@5.30.3)
+      vite: 5.3.5(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3)
     optionalDependencies:
-      typescript: 5.4.5
+      typescript: 5.5.4
     transitivePeerDependencies:
       - encoding
+      - prettier
       - supports-color
 
-  '@storybook/channels@8.0.9':
+  '@storybook/builder-vite@8.2.6(storybook@8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4))(typescript@5.5.4)(vite@5.3.5(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3))':
     dependencies:
-      '@storybook/client-logger': 8.0.9
-      '@storybook/core-events': 8.0.9
+      '@storybook/csf-plugin': 8.2.6(storybook@8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(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: 1.5.4
+      express: 4.19.2
+      find-cache-dir: 3.3.2
+      fs-extra: 11.1.1
+      magic-string: 0.30.10
+      storybook: 8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4)
+      ts-dedent: 2.2.0
+      vite: 5.3.5(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3)
+    optionalDependencies:
+      typescript: 5.5.4
+    transitivePeerDependencies:
+      - supports-color
+
+  '@storybook/channels@8.1.11':
+    dependencies:
+      '@storybook/client-logger': 8.1.11
+      '@storybook/core-events': 8.1.11
       '@storybook/global': 5.0.0
       telejson: 7.2.0
-      tiny-invariant: 1.3.1
+      tiny-invariant: 1.3.3
 
-  '@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/client-logger@8.1.11':
     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
+      '@storybook/global': 5.0.0
+
+  '@storybook/codemod@8.2.6(bufferutil@4.0.8)(utf-8-validate@6.0.4)':
+    dependencies:
+      '@babel/core': 7.24.7
+      '@babel/preset-env': 7.24.7(@babel/core@7.24.7)
+      '@babel/types': 7.24.7
+      '@storybook/core': 8.2.6(bufferutil@4.0.8)(utf-8-validate@6.0.4)
+      '@storybook/csf': 0.1.11
+      '@types/cross-spawn': 6.0.2
       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
+      globby: 14.0.1
+      jscodeshift: 0.15.1(@babel/preset-env@7.24.7(@babel/core@7.24.7))
+      lodash: 4.17.21
+      prettier: 3.3.3
+      recast: 0.23.6
+      tiny-invariant: 1.3.3
     transitivePeerDependencies:
-      - '@babel/preset-env'
       - bufferutil
-      - encoding
-      - react
-      - react-dom
       - supports-color
       - utf-8-validate
 
-  '@storybook/client-logger@8.0.9':
+  '@storybook/components@8.2.6(storybook@8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4))':
     dependencies:
-      '@storybook/global': 5.0.0
+      storybook: 8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4)
 
-  '@storybook/codemod@8.0.9':
+  '@storybook/core-common@8.1.11(encoding@0.1.13)(prettier@3.3.3)':
     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
-    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)':
-    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/core-common@8.0.9(encoding@0.1.13)':
-    dependencies:
-      '@storybook/core-events': 8.0.9
-      '@storybook/csf-tools': 8.0.9
-      '@storybook/node-logger': 8.0.9
-      '@storybook/types': 8.0.9
+      '@storybook/core-events': 8.1.11
+      '@storybook/csf-tools': 8.1.11
+      '@storybook/node-logger': 8.1.11
+      '@storybook/types': 8.1.11
       '@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)
+      esbuild: 0.19.11
+      esbuild-register: 3.5.0(esbuild@0.19.11)
       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
+      glob: 10.3.10
       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
+      prettier-fallback: prettier@3.3.3
       pretty-hrtime: 1.0.3
       resolve-from: 5.0.0
       semver: 7.6.0
-      tempy: 1.0.1
-      tiny-invariant: 1.3.1
+      tempy: 3.1.0
+      tiny-invariant: 1.3.3
       ts-dedent: 2.2.0
       util: 0.12.5
+    optionalDependencies:
+      prettier: 3.3.3
     transitivePeerDependencies:
       - encoding
       - supports-color
 
-  '@storybook/core-events@8.0.9':
+  '@storybook/core-events@8.1.11':
     dependencies:
+      '@storybook/csf': 0.1.9
       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)':
+  '@storybook/core-events@8.2.6(storybook@8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4))':
+    dependencies:
+      storybook: 8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4)
+
+  '@storybook/core-server@8.1.11(bufferutil@4.0.8)(encoding@0.1.13)(prettier@3.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(utf-8-validate@6.0.4)':
     dependencies:
       '@aw-web-design/x-default-browser': 1.4.126
-      '@babel/core': 7.24.0
+      '@babel/core': 7.24.7
+      '@babel/parser': 7.24.7
       '@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/builder-manager': 8.1.11(encoding@0.1.13)(prettier@3.3.3)
+      '@storybook/channels': 8.1.11
+      '@storybook/core-common': 8.1.11(encoding@0.1.13)(prettier@3.3.3)
+      '@storybook/core-events': 8.1.11
+      '@storybook/csf': 0.1.9
+      '@storybook/csf-tools': 8.1.11
+      '@storybook/docs-mdx': 3.1.0-next.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
+      '@storybook/manager': 8.1.11
+      '@storybook/manager-api': 8.1.11(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+      '@storybook/node-logger': 8.1.11
+      '@storybook/preview-api': 8.1.11
+      '@storybook/telemetry': 8.1.11(encoding@0.1.13)(prettier@3.3.3)
+      '@storybook/types': 8.1.11
       '@types/detect-port': 1.3.2
+      '@types/diff': 5.2.1
       '@types/node': 18.17.15
       '@types/pretty-hrtime': 1.0.1
       '@types/semver': 7.5.8
@@ -15167,10 +15957,10 @@ snapshots:
       cli-table3: 0.6.3
       compression: 1.7.4
       detect-port: 1.5.1
-      express: 4.18.2
+      diff: 5.2.0
+      express: 4.19.2
       fs-extra: 11.1.1
-      globby: 11.1.0
-      ip: 2.0.1
+      globby: 14.0.1
       lodash: 4.17.21
       open: 8.4.2
       pretty-hrtime: 1.0.3
@@ -15178,59 +15968,88 @@ snapshots:
       read-pkg-up: 7.0.1
       semver: 7.6.0
       telejson: 7.2.0
-      tiny-invariant: 1.3.1
+      tiny-invariant: 1.3.3
       ts-dedent: 2.2.0
       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
+      - prettier
       - react
       - react-dom
       - supports-color
       - utf-8-validate
 
-  '@storybook/csf-plugin@8.0.9':
+  '@storybook/core@8.2.6(bufferutil@4.0.8)(utf-8-validate@6.0.4)':
     dependencies:
-      '@storybook/csf-tools': 8.0.9
+      '@storybook/csf': 0.1.11
+      '@types/express': 4.17.21
+      '@types/node': 18.17.15
+      browser-assert: 1.2.1
+      esbuild: 0.19.11
+      esbuild-register: 3.5.0(esbuild@0.19.11)
+      express: 4.19.2
+      process: 0.11.10
+      recast: 0.23.6
+      util: 0.12.5
+      ws: 8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4)
+    transitivePeerDependencies:
+      - bufferutil
+      - supports-color
+      - utf-8-validate
+
+  '@storybook/csf-plugin@8.1.11':
+    dependencies:
+      '@storybook/csf-tools': 8.1.11
       unplugin: 1.4.0
     transitivePeerDependencies:
       - supports-color
 
-  '@storybook/csf-tools@8.0.9':
+  '@storybook/csf-plugin@8.2.6(storybook@8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4))':
     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
+      storybook: 8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4)
+      unplugin: 1.4.0
+
+  '@storybook/csf-tools@8.1.11':
+    dependencies:
+      '@babel/generator': 7.24.7
+      '@babel/parser': 7.24.7
+      '@babel/traverse': 7.24.7
+      '@babel/types': 7.24.7
+      '@storybook/csf': 0.1.9
+      '@storybook/types': 8.1.11
       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)':
+  '@storybook/csf@0.1.9':
     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
+      type-fest: 2.19.0
+
+  '@storybook/docs-mdx@3.1.0-next.0': {}
+
+  '@storybook/docs-tools@8.1.11(encoding@0.1.13)(prettier@3.3.3)':
+    dependencies:
+      '@storybook/core-common': 8.1.11(encoding@0.1.13)(prettier@3.3.3)
+      '@storybook/core-events': 8.1.11
+      '@storybook/preview-api': 8.1.11
+      '@storybook/types': 8.1.11
       '@types/doctrine': 0.0.3
       assert: 2.1.0
       doctrine: 3.0.0
       lodash: 4.17.21
     transitivePeerDependencies:
       - encoding
+      - prettier
       - supports-color
 
   '@storybook/global@5.0.0': {}
@@ -15240,27 +16059,24 @@ snapshots:
       react: 18.3.1
       react-dom: 18.3.1(react@18.3.1)
 
-  '@storybook/instrumenter@8.0.9':
+  '@storybook/instrumenter@8.2.6(storybook@8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(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
+      storybook: 8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(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.1.11(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
     dependencies:
-      '@storybook/channels': 8.0.9
-      '@storybook/client-logger': 8.0.9
-      '@storybook/core-events': 8.0.9
-      '@storybook/csf': 0.1.6
+      '@storybook/channels': 8.1.11
+      '@storybook/client-logger': 8.1.11
+      '@storybook/core-events': 8.1.11
+      '@storybook/csf': 0.1.9
       '@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
+      '@storybook/router': 8.1.11
+      '@storybook/theming': 8.1.11(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+      '@storybook/types': 8.1.11
       dequal: 2.0.3
       lodash: 4.17.21
       memoizerific: 1.11.3
@@ -15271,65 +16087,73 @@ snapshots:
       - react
       - react-dom
 
-  '@storybook/manager@8.0.9': {}
-
-  '@storybook/node-logger@8.0.9': {}
-
-  '@storybook/preview-api@8.0.9':
+  '@storybook/manager-api@8.2.6(storybook@8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(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: 8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4)
+
+  '@storybook/manager@8.1.11': {}
+
+  '@storybook/node-logger@8.1.11': {}
+
+  '@storybook/preview-api@8.1.11':
+    dependencies:
+      '@storybook/channels': 8.1.11
+      '@storybook/client-logger': 8.1.11
+      '@storybook/core-events': 8.1.11
+      '@storybook/csf': 0.1.9
       '@storybook/global': 5.0.0
-      '@storybook/types': 8.0.9
+      '@storybook/types': 8.1.11
       '@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
+      tiny-invariant: 1.3.3
       ts-dedent: 2.2.0
       util-deprecate: 1.0.2
 
-  '@storybook/preview@8.0.9': {}
+  '@storybook/preview-api@8.2.6(storybook@8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4))':
+    dependencies:
+      storybook: 8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4)
 
-  '@storybook/react-dom-shim@8.0.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
+  '@storybook/preview@8.1.11': {}
+
+  '@storybook/react-dom-shim@8.2.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(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.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(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.2.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@4.19.1)(storybook@8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4))(typescript@5.5.4)(vite@5.3.5(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3))':
     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.1(typescript@5.5.4)(vite@5.3.5(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3))
+      '@rollup/pluginutils': 5.1.0(rollup@4.19.1)
+      '@storybook/builder-vite': 8.2.6(storybook@8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4))(typescript@5.5.4)(vite@5.3.5(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3))
+      '@storybook/react': 8.2.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4))(typescript@5.5.4)
       find-up: 5.0.0
-      magic-string: 0.30.7
+      magic-string: 0.30.10
       react: 18.3.1
       react-docgen: 7.0.1
       react-dom: 18.3.1(react@18.3.1)
       resolve: 1.22.8
+      storybook: 8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(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.3.5(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3)
     transitivePeerDependencies:
       - '@preact/preset-vite'
-      - encoding
       - 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.2.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4))(typescript@5.5.4)':
     dependencies:
-      '@storybook/client-logger': 8.0.9
-      '@storybook/docs-tools': 8.0.9(encoding@0.1.13)
+      '@storybook/components': 8.2.6(storybook@8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(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.2.6(storybook@8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4))
+      '@storybook/preview-api': 8.2.6(storybook@8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4))
+      '@storybook/react-dom-shim': 8.2.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4))
+      '@storybook/theming': 8.2.6(storybook@8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(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
@@ -15344,34 +16168,32 @@ snapshots:
       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
+      storybook: 8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(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
+      typescript: 5.5.4
 
-  '@storybook/router@8.0.9':
+  '@storybook/router@8.1.11':
     dependencies:
-      '@storybook/client-logger': 8.0.9
+      '@storybook/client-logger': 8.1.11
       memoizerific: 1.11.3
       qs: 6.11.1
 
-  '@storybook/source-loader@8.0.9':
+  '@storybook/source-loader@8.2.6(storybook@8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4))':
     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.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4)
 
-  '@storybook/telemetry@8.0.9(encoding@0.1.13)':
+  '@storybook/telemetry@8.1.11(encoding@0.1.13)(prettier@3.3.3)':
     dependencies:
-      '@storybook/client-logger': 8.0.9
-      '@storybook/core-common': 8.0.9(encoding@0.1.13)
-      '@storybook/csf-tools': 8.0.9
+      '@storybook/client-logger': 8.1.11
+      '@storybook/core-common': 8.1.11(encoding@0.1.13)(prettier@3.3.3)
+      '@storybook/csf-tools': 8.1.11
       chalk: 4.1.2
       detect-package-manager: 2.0.1
       fetch-retry: 5.0.4
@@ -15379,19 +16201,19 @@ snapshots:
       read-pkg-up: 7.0.1
     transitivePeerDependencies:
       - encoding
+      - prettier
       - 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))':
+  '@storybook/test@8.2.6(@jest/globals@29.7.0)(@types/jest@29.5.12)(jest@29.7.0(@types/node@20.14.12))(storybook@8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4))(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.77.8)(terser@5.31.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
+      '@storybook/csf': 0.1.11
+      '@storybook/instrumenter': 8.2.6(storybook@8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4))
+      '@testing-library/dom': 10.1.0
+      '@testing-library/jest-dom': 6.4.5(@jest/globals@29.7.0)(@types/jest@29.5.12)(jest@29.7.0(@types/node@20.14.12))(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.77.8)(terser@5.31.3))
+      '@testing-library/user-event': 14.5.2(@testing-library/dom@10.1.0)
+      '@vitest/expect': 1.6.0
       '@vitest/spy': 1.6.0
+      storybook: 8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4)
       util: 0.12.5
     transitivePeerDependencies:
       - '@jest/globals'
@@ -15400,37 +16222,47 @@ snapshots:
       - jest
       - vitest
 
-  '@storybook/theming@8.0.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
+  '@storybook/theming@8.1.11(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/client-logger': 8.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/types@8.0.9':
+  '@storybook/theming@8.2.6(storybook@8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4))':
     dependencies:
-      '@storybook/channels': 8.0.9
+      storybook: 8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4)
+
+  '@storybook/types@8.1.11':
+    dependencies:
+      '@storybook/channels': 8.1.11
       '@types/express': 4.17.17
       file-system-cache: 2.3.0
 
-  '@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.2.6(storybook@8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(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.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4)
+
+  '@storybook/vue3-vite@8.1.11(bufferutil@4.0.8)(encoding@0.1.13)(prettier@3.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(utf-8-validate@6.0.4)(vite@5.3.5(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3))(vue@3.4.37(typescript@5.5.4))':
+    dependencies:
+      '@storybook/builder-vite': 8.1.11(encoding@0.1.13)(prettier@3.3.3)(typescript@5.5.4)(vite@5.3.5(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3))
+      '@storybook/core-server': 8.1.11(bufferutil@4.0.8)(encoding@0.1.13)(prettier@3.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(utf-8-validate@6.0.4)
+      '@storybook/types': 8.1.11
+      '@storybook/vue3': 8.1.11(encoding@0.1.13)(prettier@3.3.3)(vue@3.4.37(typescript@5.5.4))
       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.10
+      typescript: 5.5.4
+      vite: 5.3.5(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3)
+      vue-component-meta: 2.0.16(typescript@5.5.4)
+      vue-docgen-api: 4.75.1(vue@3.4.37(typescript@5.5.4))
     transitivePeerDependencies:
       - '@preact/preset-vite'
       - bufferutil
       - encoding
+      - prettier
       - react
       - react-dom
       - supports-color
@@ -15438,26 +16270,42 @@ snapshots:
       - vite-plugin-glimmerx
       - vue
 
-  '@storybook/vue3@8.0.9(encoding@0.1.13)(vue@3.4.26(typescript@5.4.5))':
+  '@storybook/vue3@8.1.11(encoding@0.1.13)(prettier@3.3.3)(vue@3.4.37(typescript@5.5.4))':
     dependencies:
-      '@storybook/docs-tools': 8.0.9(encoding@0.1.13)
+      '@storybook/docs-tools': 8.1.11(encoding@0.1.13)(prettier@3.3.3)
       '@storybook/global': 5.0.0
-      '@storybook/preview-api': 8.0.9
-      '@storybook/types': 8.0.9
-      '@vue/compiler-core': 3.4.21
+      '@storybook/preview-api': 8.1.11
+      '@storybook/types': 8.1.11
+      '@vue/compiler-core': 3.4.34
       lodash: 4.17.21
       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
+      vue: 3.4.37(typescript@5.5.4)
+      vue-component-type-helpers: 2.1.2
     transitivePeerDependencies:
       - encoding
+      - prettier
       - supports-color
 
-  '@swc/cli@0.3.12(@swc/core@1.4.17)(chokidar@3.5.3)':
+  '@storybook/vue3@8.2.6(storybook@8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4))(vue@3.4.37(typescript@5.5.4))':
+    dependencies:
+      '@storybook/components': 8.2.6(storybook@8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4))
+      '@storybook/global': 5.0.0
+      '@storybook/manager-api': 8.2.6(storybook@8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4))
+      '@storybook/preview-api': 8.2.6(storybook@8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4))
+      '@storybook/theming': 8.2.6(storybook@8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4))
+      '@vue/compiler-core': 3.4.31
+      lodash: 4.17.21
+      storybook: 8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4)
+      ts-dedent: 2.2.0
+      type-fest: 2.19.0
+      vue: 3.4.37(typescript@5.5.4)
+      vue-component-type-helpers: 2.1.2
+
+  '@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
@@ -15477,13 +16325,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':
@@ -15494,82 +16348,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
@@ -15583,12 +16486,12 @@ snapshots:
     dependencies:
       defer-to-connect: 2.0.1
 
-  '@testing-library/dom@9.3.3':
+  '@testing-library/dom@10.1.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
@@ -15605,11 +16508,11 @@ 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.4.5(@jest/globals@29.7.0)(@types/jest@29.5.12)(jest@29.7.0(@types/node@20.14.12))(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.77.8)(terser@5.31.3))':
     dependencies:
       '@adobe/css-tools': 4.3.3
       '@babel/runtime': 7.23.4
-      aria-query: 5.1.3
+      aria-query: 5.3.0
       chalk: 3.0.0
       css.escape: 1.5.1
       dom-accessibility-api: 0.6.3
@@ -15618,21 +16521,21 @@ snapshots:
     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)
+      jest: 29.7.0(@types/node@20.14.12)
+      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.77.8)(terser@5.31.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.1.0)':
     dependencies:
-      '@testing-library/dom': 9.3.4
+      '@testing-library/dom': 10.1.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.4.37)(@vue/server-renderer@3.4.37(vue@3.4.37(typescript@5.5.4)))(vue@3.4.37(typescript@5.5.4))':
     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.4.37(vue@3.4.37(typescript@5.5.4)))(vue@3.4.37(typescript@5.5.4))
+      vue: 3.4.37(typescript@5.5.4)
     optionalDependencies:
-      '@vue/compiler-sfc': 3.4.26
+      '@vue/compiler-sfc': 3.4.37
     transitivePeerDependencies:
       - '@vue/server-renderer'
 
@@ -15644,7 +16547,7 @@ snapshots:
 
   '@trysound/sax@0.2.0': {}
 
-  '@tsd/typescript@5.3.3': {}
+  '@tsd/typescript@5.4.5': {}
 
   '@twemoji/parser@15.0.0': {}
 
@@ -15652,7 +16555,7 @@ snapshots:
 
   '@types/accepts@1.3.7':
     dependencies:
-      '@types/node': 20.12.7
+      '@types/node': 20.14.12
 
   '@types/archiver@6.0.2':
     dependencies:
@@ -15664,31 +16567,31 @@ snapshots:
 
   '@types/babel__core@7.20.0':
     dependencies:
-      '@babel/parser': 7.24.5
-      '@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.5
-      '@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': {}
 
@@ -15696,15 +16599,9 @@ 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/chai@4.3.11': {}
-
   '@types/color-convert@2.0.3':
     dependencies:
       '@types/color-name': 1.1.1
@@ -15713,28 +16610,21 @@ 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/core-js@2.5.8': {}
 
   '@types/cross-spawn@6.0.2':
     dependencies:
-      '@types/node': 20.12.7
+      '@types/node': 20.14.12
 
   '@types/debug@4.1.12':
     dependencies:
@@ -15742,6 +16632,8 @@ snapshots:
 
   '@types/detect-port@1.3.2': {}
 
+  '@types/diff@5.2.1': {}
+
   '@types/disposable-email-domains@1.0.2': {}
 
   '@types/doctrine@0.0.3': {}
@@ -15767,7 +16659,7 @@ snapshots:
 
   '@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
 
@@ -15778,11 +16670,18 @@ 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':
     dependencies:
-      '@types/node': 20.12.7
+      '@types/node': 20.14.12
 
   '@types/form-data@2.5.0':
     dependencies:
@@ -15791,11 +16690,11 @@ snapshots:
   '@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:
@@ -15803,15 +16702,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': {}
 
@@ -15830,9 +16725,9 @@ snapshots:
 
   '@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
 
@@ -15842,46 +16737,27 @@ 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': {}
 
   '@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
 
@@ -15897,15 +16773,11 @@ 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-fetch@3.0.3':
-    dependencies:
-      node-fetch: 3.3.2
+      '@types/node': 20.14.12
 
   '@types/node@18.17.15': {}
 
@@ -15913,7 +16785,7 @@ snapshots:
     dependencies:
       undici-types: 5.26.5
 
-  '@types/node@20.12.7':
+  '@types/node@20.14.12':
     dependencies:
       undici-types: 5.26.5
 
@@ -15923,7 +16795,7 @@ snapshots:
 
   '@types/nodemailer@6.4.15':
     dependencies:
-      '@types/node': 20.12.7
+      '@types/node': 20.14.12
 
   '@types/normalize-package-data@2.4.1': {}
 
@@ -15934,11 +16806,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/object-assign-deep@0.4.3': {}
 
@@ -15946,17 +16818,17 @@ snapshots:
 
   '@types/pg-pool@2.0.4':
     dependencies:
-      '@types/pg': 8.11.5
+      '@types/pg': 8.11.6
 
-  '@types/pg@8.11.5':
+  '@types/pg@8.11.6':
     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
 
@@ -15964,13 +16836,17 @@ snapshots:
 
   '@types/prop-types@15.7.5': {}
 
+  '@types/proxy-addr@2.0.3':
+    dependencies:
+      '@types/node': 20.14.12
+
   '@types/pug@2.0.10': {}
 
   '@types/punycode@2.1.4': {}
 
   '@types/qrcode@1.5.5':
     dependencies:
-      '@types/node': 20.12.7
+      '@types/node': 20.14.12
 
   '@types/qs@6.9.7': {}
 
@@ -15988,7 +16864,7 @@ snapshots:
 
   '@types/readdir-glob@1.1.1':
     dependencies:
-      '@types/node': 20.12.7
+      '@types/node': 20.14.12
 
   '@types/rename@1.0.7': {}
 
@@ -15996,7 +16872,7 @@ snapshots:
 
   '@types/responselike@1.0.0':
     dependencies:
-      '@types/node': 20.12.7
+      '@types/node': 20.14.12
 
   '@types/sanitize-html@2.11.0':
     dependencies:
@@ -16011,7 +16887,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': {}
 
@@ -16041,23 +16917,27 @@ 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/wrap-ansi@3.0.0': {}
 
-  '@types/ws@8.5.10':
+  '@types/ws@8.5.11':
     dependencies:
-      '@types/node': 20.12.7
+      '@types/node': 20.14.12
 
   '@types/yargs-parser@21.0.0': {}
 
@@ -16067,19 +16947,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@6.11.0(@typescript-eslint/parser@6.11.0(eslint@9.8.0)(typescript@5.3.3))(eslint@9.8.0)(typescript@5.3.3)':
     dependencies:
-      '@eslint-community/regexpp': 4.10.0
-      '@typescript-eslint/parser': 6.11.0(eslint@8.53.0)(typescript@5.3.3)
+      '@eslint-community/regexpp': 4.11.0
+      '@typescript-eslint/parser': 6.11.0(eslint@9.8.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/type-utils': 6.11.0(eslint@9.8.0)(typescript@5.3.3)
+      '@typescript-eslint/utils': 6.11.0(eslint@9.8.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
+      debug: 4.3.4(supports-color@5.5.0)
+      eslint: 9.8.0
       graphemer: 1.4.0
       ignore: 5.3.1
       natural-compare: 1.4.0
@@ -16092,13 +16972,13 @@ snapshots:
 
   '@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.1.6))(eslint@8.57.0)(typescript@5.1.6)':
     dependencies:
-      '@eslint-community/regexpp': 4.10.0
+      '@eslint-community/regexpp': 4.11.0
       '@typescript-eslint/parser': 6.21.0(eslint@8.57.0)(typescript@5.1.6)
       '@typescript-eslint/scope-manager': 6.21.0
       '@typescript-eslint/type-utils': 6.21.0(eslint@8.57.0)(typescript@5.1.6)
       '@typescript-eslint/utils': 6.21.0(eslint@8.57.0)(typescript@5.1.6)
       '@typescript-eslint/visitor-keys': 6.21.0
-      debug: 4.3.4(supports-color@8.1.1)
+      debug: 4.3.5(supports-color@8.1.1)
       eslint: 8.57.0
       graphemer: 1.4.0
       ignore: 5.3.1
@@ -16110,16 +16990,16 @@ snapshots:
     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)':
+  '@typescript-eslint/eslint-plugin@7.1.0(@typescript-eslint/parser@7.1.0(eslint@9.8.0)(typescript@5.3.3))(eslint@9.8.0)(typescript@5.3.3)':
     dependencies:
-      '@eslint-community/regexpp': 4.10.0
-      '@typescript-eslint/parser': 7.1.0(eslint@8.57.0)(typescript@5.3.3)
+      '@eslint-community/regexpp': 4.6.2
+      '@typescript-eslint/parser': 7.1.0(eslint@9.8.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.8.0)(typescript@5.3.3)
+      '@typescript-eslint/utils': 7.1.0(eslint@9.8.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.8.0
       graphemer: 1.4.0
       ignore: 5.3.1
       natural-compare: 1.4.0
@@ -16130,34 +17010,32 @@ 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.8.0)(typescript@5.5.4))(eslint@9.8.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.8.0)(typescript@5.5.4)
+      '@typescript-eslint/scope-manager': 7.17.0
+      '@typescript-eslint/type-utils': 7.17.0(eslint@9.8.0)(typescript@5.5.4)
+      '@typescript-eslint/utils': 7.17.0(eslint@9.8.0)(typescript@5.5.4)
+      '@typescript-eslint/visitor-keys': 7.17.0
+      eslint: 9.8.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/parser@6.11.0(eslint@9.8.0)(typescript@5.3.3)':
     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
+      debug: 4.3.4(supports-color@5.5.0)
+      eslint: 9.8.0
     optionalDependencies:
       typescript: 5.3.3
     transitivePeerDependencies:
@@ -16169,36 +17047,36 @@ snapshots:
       '@typescript-eslint/types': 6.21.0
       '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.1.6)
       '@typescript-eslint/visitor-keys': 6.21.0
-      debug: 4.3.4(supports-color@8.1.1)
+      debug: 4.3.5(supports-color@8.1.1)
       eslint: 8.57.0
     optionalDependencies:
       typescript: 5.1.6
     transitivePeerDependencies:
       - supports-color
 
-  '@typescript-eslint/parser@7.1.0(eslint@8.57.0)(typescript@5.3.3)':
+  '@typescript-eslint/parser@7.1.0(eslint@9.8.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.8.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.8.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@8.1.1)
+      eslint: 9.8.0
     optionalDependencies:
-      typescript: 5.4.5
+      typescript: 5.5.4
     transitivePeerDependencies:
       - supports-color
 
@@ -16217,17 +17095,17 @@ snapshots:
       '@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)':
+  '@typescript-eslint/type-utils@6.11.0(eslint@9.8.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
+      '@typescript-eslint/utils': 6.11.0(eslint@9.8.0)(typescript@5.3.3)
+      debug: 4.3.5(supports-color@8.1.1)
+      eslint: 9.8.0
       ts-api-utils: 1.3.0(typescript@5.3.3)
     optionalDependencies:
       typescript: 5.3.3
@@ -16238,7 +17116,7 @@ snapshots:
     dependencies:
       '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.1.6)
       '@typescript-eslint/utils': 6.21.0(eslint@8.57.0)(typescript@5.1.6)
-      debug: 4.3.4(supports-color@8.1.1)
+      debug: 4.3.5(supports-color@8.1.1)
       eslint: 8.57.0
       ts-api-utils: 1.3.0(typescript@5.1.6)
     optionalDependencies:
@@ -16246,27 +17124,27 @@ snapshots:
     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.8.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.8.0)(typescript@5.3.3)
+      debug: 4.3.4(supports-color@5.5.0)
+      eslint: 9.8.0
       ts-api-utils: 1.3.0(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.8.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.8.0)(typescript@5.5.4)
+      debug: 4.3.5(supports-color@8.1.1)
+      eslint: 9.8.0
+      ts-api-utils: 1.3.0(typescript@5.5.4)
     optionalDependencies:
-      typescript: 5.4.5
+      typescript: 5.5.4
     transitivePeerDependencies:
       - supports-color
 
@@ -16276,13 +17154,13 @@ snapshots:
 
   '@typescript-eslint/types@7.1.0': {}
 
-  '@typescript-eslint/types@7.7.1': {}
+  '@typescript-eslint/types@7.17.0': {}
 
   '@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)
+      debug: 4.3.5(supports-color@8.1.1)
       globby: 11.1.0
       is-glob: 4.0.3
       semver: 7.5.4
@@ -16296,7 +17174,7 @@ snapshots:
     dependencies:
       '@typescript-eslint/types': 6.21.0
       '@typescript-eslint/visitor-keys': 6.21.0
-      debug: 4.3.4(supports-color@8.1.1)
+      debug: 4.3.5(supports-color@8.1.1)
       globby: 11.1.0
       is-glob: 4.0.3
       minimatch: 9.0.3
@@ -16311,7 +17189,7 @@ snapshots:
     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.4(supports-color@5.5.0)
       globby: 11.1.0
       is-glob: 4.0.3
       minimatch: 9.0.3
@@ -16322,30 +17200,30 @@ 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@8.1.1)
       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/utils@6.11.0(eslint@9.8.0)(typescript@5.3.3)':
     dependencies:
-      '@eslint-community/eslint-utils': 4.4.0(eslint@8.53.0)
+      '@eslint-community/eslint-utils': 4.4.0(eslint@9.8.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
+      eslint: 9.8.0
       semver: 7.6.0
     transitivePeerDependencies:
       - supports-color
@@ -16365,30 +17243,27 @@ snapshots:
       - 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.8.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.8.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.8.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.8.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.8.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.8.0
     transitivePeerDependencies:
       - supports-color
       - typescript
@@ -16408,84 +17283,59 @@ snapshots:
       '@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.0(vite@5.3.5(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3))(vue@3.4.37(typescript@5.5.4))':
     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.3.5(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3)
+      vue: 3.4.37(typescript@5.5.4)
 
-  '@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))':
+  '@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.77.8)(terser@5.31.3))':
     dependencies:
       '@ampproject/remapping': 2.2.1
       '@bcoe/v8-coverage': 0.2.3
+      debug: 4.3.4(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
+      magic-string: 0.30.10
+      magicast: 0.3.4
       picocolors: 1.0.0
       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.77.8)(terser@5.31.3)
     transitivePeerDependencies:
       - supports-color
 
-  '@vitest/expect@0.34.6':
+  '@vitest/expect@1.6.0':
     dependencies:
-      '@vitest/spy': 0.34.6
-      '@vitest/utils': 0.34.6
+      '@vitest/spy': 1.6.0
+      '@vitest/utils': 1.6.0
       chai: 4.3.10
 
-  '@vitest/expect@1.3.1':
+  '@vitest/runner@1.6.0':
     dependencies:
-      '@vitest/spy': 1.3.1
-      '@vitest/utils': 1.3.1
-      chai: 4.3.10
-
-  '@vitest/runner@0.34.6':
-    dependencies:
-      '@vitest/utils': 0.34.6
-      p-limit: 4.0.0
+      '@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.10
       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':
-    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
-
   '@vitest/utils@1.6.0':
     dependencies:
       diff-sequences: 29.6.3
@@ -16497,124 +17347,150 @@ snapshots:
     dependencies:
       '@volar/source-map': 2.2.0
 
+  '@volar/language-core@2.4.0-alpha.18':
+    dependencies:
+      '@volar/source-map': 2.4.0-alpha.18
+
   '@volar/source-map@2.2.0':
     dependencies:
       muggle-string: 0.4.1
 
+  '@volar/source-map@2.4.0-alpha.18': {}
+
   '@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.0-alpha.18':
     dependencies:
-      '@babel/parser': 7.24.0
-      '@vue/shared': 3.4.21
-      entities: 4.5.0
-      estree-walker: 2.0.2
-      source-map-js: 1.0.2
+      '@volar/language-core': 2.4.0-alpha.18
+      path-browserify: 1.0.1
+      vscode-uri: 3.0.8
 
-  '@vue/compiler-core@3.4.25':
+  '@vue/compiler-core@3.4.31':
     dependencies:
-      '@babel/parser': 7.24.5
-      '@vue/shared': 3.4.25
+      '@babel/parser': 7.24.7
+      '@vue/shared': 3.4.31
       entities: 4.5.0
       estree-walker: 2.0.2
       source-map-js: 1.2.0
 
-  '@vue/compiler-core@3.4.26':
+  '@vue/compiler-core@3.4.34':
     dependencies:
-      '@babel/parser': 7.24.5
-      '@vue/shared': 3.4.26
+      '@babel/parser': 7.24.7
+      '@vue/shared': 3.4.34
       entities: 4.5.0
       estree-walker: 2.0.2
       source-map-js: 1.2.0
 
-  '@vue/compiler-dom@3.4.21':
+  '@vue/compiler-core@3.4.37':
     dependencies:
-      '@vue/compiler-core': 3.4.21
-      '@vue/shared': 3.4.21
+      '@babel/parser': 7.24.7
+      '@vue/shared': 3.4.37
+      entities: 5.0.0
+      estree-walker: 2.0.2
+      source-map-js: 1.2.0
 
-  '@vue/compiler-dom@3.4.25':
+  '@vue/compiler-dom@3.4.34':
     dependencies:
-      '@vue/compiler-core': 3.4.25
-      '@vue/shared': 3.4.25
+      '@vue/compiler-core': 3.4.34
+      '@vue/shared': 3.4.34
 
-  '@vue/compiler-dom@3.4.26':
+  '@vue/compiler-dom@3.4.37':
     dependencies:
-      '@vue/compiler-core': 3.4.26
-      '@vue/shared': 3.4.26
+      '@vue/compiler-core': 3.4.37
+      '@vue/shared': 3.4.37
 
-  '@vue/compiler-sfc@3.4.26':
+  '@vue/compiler-sfc@3.4.37':
     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.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.10
-      postcss: 8.4.38
+      postcss: 8.4.40
       source-map-js: 1.2.0
 
-  '@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/compiler-vue2@2.7.16':
+    dependencies:
+      de-indent: 1.0.2
+      he: 1.2.0
 
   '@vue/devtools-api@6.6.1': {}
 
-  '@vue/language-core@2.0.16(typescript@5.4.5)':
+  '@vue/language-core@2.0.16(typescript@5.5.4)':
     dependencies:
       '@volar/language-core': 2.2.0
-      '@vue/compiler-dom': 3.4.25
-      '@vue/shared': 3.4.25
+      '@vue/compiler-dom': 3.4.34
+      '@vue/shared': 3.4.34
       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.5.4
 
-  '@vue/reactivity@3.4.26':
+  '@vue/language-core@2.0.29(typescript@5.5.4)':
     dependencies:
-      '@vue/shared': 3.4.26
+      '@volar/language-core': 2.4.0-alpha.18
+      '@vue/compiler-dom': 3.4.34
+      '@vue/compiler-vue2': 2.7.16
+      '@vue/shared': 3.4.34
+      computeds: 0.0.1
+      minimatch: 9.0.4
+      muggle-string: 0.4.1
+      path-browserify: 1.0.1
+    optionalDependencies:
+      typescript: 5.5.4
 
-  '@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/runtime-core@3.4.37':
     dependencies:
-      '@vue/runtime-core': 3.4.26
-      '@vue/shared': 3.4.26
+      '@vue/reactivity': 3.4.37
+      '@vue/shared': 3.4.37
+
+  '@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/server-renderer@3.4.37(vue@3.4.37(typescript@5.5.4))':
     dependencies:
-      '@vue/compiler-ssr': 3.4.26
-      '@vue/shared': 3.4.26
-      vue: 3.4.26(typescript@5.4.5)
+      '@vue/compiler-ssr': 3.4.37
+      '@vue/shared': 3.4.37
+      vue: 3.4.37(typescript@5.5.4)
 
-  '@vue/shared@3.4.21': {}
+  '@vue/shared@3.4.31': {}
 
-  '@vue/shared@3.4.25': {}
+  '@vue/shared@3.4.34': {}
 
-  '@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/test-utils@2.4.1(@vue/server-renderer@3.4.37(vue@3.4.37(typescript@5.5.4)))(vue@3.4.37(typescript@5.5.4))':
     dependencies:
       js-beautify: 1.14.9
-      vue: 3.4.26(typescript@5.4.5)
+      vue: 3.4.37(typescript@5.5.4)
       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.4.37(vue@3.4.37(typescript@5.5.4))
 
-  '@yarnpkg/esbuild-plugin-pnp@3.0.0-rc.15(esbuild@0.20.2)':
+  '@yarnpkg/esbuild-plugin-pnp@3.0.0-rc.15(esbuild@0.19.11)':
     dependencies:
-      esbuild: 0.20.2
-      tslib: 2.6.2
+      esbuild: 0.19.11
+      tslib: 2.6.3
 
   '@yarnpkg/fslib@2.10.3':
     dependencies:
@@ -16641,22 +17517,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: {}
 
@@ -16664,19 +17540,19 @@ snapshots:
 
   acorn@7.4.1: {}
 
-  acorn@8.11.3: {}
+  acorn@8.12.1: {}
 
   address@1.2.2: {}
 
   agent-base@6.0.2:
     dependencies:
-      debug: 4.3.4(supports-color@8.1.1)
+      debug: 4.3.5(supports-color@8.1.1)
     transitivePeerDependencies:
       - supports-color
 
   agent-base@7.1.0:
     dependencies:
-      debug: 4.3.4(supports-color@8.1.1)
+      debug: 4.3.5(supports-color@8.1.1)
     transitivePeerDependencies:
       - supports-color
 
@@ -16690,7 +17566,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
@@ -16699,7 +17575,15 @@ 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@2.1.1(ajv@8.17.1):
+    optionalDependencies:
+      ajv: 8.17.1
+
+  ajv-formats@3.0.1(ajv@8.13.0):
     optionalDependencies:
       ajv: 8.13.0
 
@@ -16710,6 +17594,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
@@ -16717,6 +17608,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:
@@ -16758,7 +17656,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
@@ -16796,6 +17694,10 @@ 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
@@ -16863,7 +17765,7 @@ snapshots:
     dependencies:
       pvtsutils: 1.3.5
       pvutils: 1.1.3
-      tslib: 2.6.2
+      tslib: 2.6.3
 
   assert-never@1.2.1: {}
 
@@ -16881,7 +17783,7 @@ snapshots:
 
   ast-types@0.16.1:
     dependencies:
-      tslib: 2.6.2
+      tslib: 2.6.3
 
   astral-regex@2.0.0: {}
 
@@ -16891,6 +17793,8 @@ snapshots:
     dependencies:
       tslib: 2.6.2
 
+  async@0.2.10: {}
+
   async@3.2.4: {}
 
   asynckit@0.4.0: {}
@@ -16905,12 +17809,12 @@ snapshots:
     dependencies:
       '@fastify/error': 3.4.0
       archy: 1.0.0
-      debug: 4.3.4(supports-color@8.1.1)
+      debug: 4.3.5(supports-color@8.1.1)
       fastq: 1.17.1
     transitivePeerDependencies:
       - supports-color
 
-  aws-sdk-client-mock@3.0.1:
+  aws-sdk-client-mock@4.0.1:
     dependencies:
       '@types/sinon': 10.0.13
       sinon: 16.1.3
@@ -16922,21 +17826,21 @@ snapshots:
 
   axios@0.24.0:
     dependencies:
-      follow-redirects: 1.15.2(debug@4.3.4)
+      follow-redirects: 1.15.2(debug@4.3.5)
     transitivePeerDependencies:
       - debug
 
   axios@1.6.0:
     dependencies:
-      follow-redirects: 1.15.2(debug@4.3.4)
+      follow-redirects: 1.15.2(debug@4.3.5)
       form-data: 4.0.0
       proxy-from-env: 1.1.0
     transitivePeerDependencies:
       - debug
 
-  axios@1.6.2(debug@4.3.4):
+  axios@1.6.2(debug@4.3.5):
     dependencies:
-      follow-redirects: 1.15.2(debug@4.3.4)
+      follow-redirects: 1.15.2(debug@4.3.5)
       form-data: 4.0.0
       proxy-from-env: 1.1.0
     transitivePeerDependencies:
@@ -16944,9 +17848,9 @@ snapshots:
 
   b4a@1.6.4: {}
 
-  babel-core@7.0.0-bridge.0(@babel/core@7.24.0):
+  babel-core@7.0.0-bridge.0(@babel/core@7.24.7):
     dependencies:
-      '@babel/core': 7.24.0
+      '@babel/core': 7.24.7
 
   babel-jest@29.7.0(@babel/core@7.23.5):
     dependencies:
@@ -16961,6 +17865,20 @@ snapshots:
     transitivePeerDependencies:
       - supports-color
 
+  babel-jest@29.7.0(@babel/core@7.24.7):
+    dependencies:
+      '@babel/core': 7.24.7
+      '@jest/transform': 29.7.0
+      '@types/babel__core': 7.20.0
+      babel-plugin-istanbul: 6.1.1
+      babel-preset-jest: 29.6.3(@babel/core@7.24.7)
+      chalk: 4.1.2
+      graceful-fs: 4.2.11
+      slash: 3.0.0
+    transitivePeerDependencies:
+      - supports-color
+    optional: true
+
   babel-plugin-istanbul@6.1.1:
     dependencies:
       '@babel/helper-plugin-utils': 7.22.5
@@ -16974,31 +17892,31 @@ 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):
+  babel-plugin-polyfill-corejs2@0.4.11(@babel/core@7.24.7):
     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)
+      '@babel/compat-data': 7.24.7
+      '@babel/core': 7.24.7
+      '@babel/helper-define-polyfill-provider': 0.6.2(@babel/core@7.24.7)
       semver: 6.3.1
     transitivePeerDependencies:
       - supports-color
 
-  babel-plugin-polyfill-corejs3@0.8.6(@babel/core@7.24.0):
+  babel-plugin-polyfill-corejs3@0.10.4(@babel/core@7.24.7):
     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
+      '@babel/core': 7.24.7
+      '@babel/helper-define-polyfill-provider': 0.6.2(@babel/core@7.24.7)
+      core-js-compat: 3.37.1
     transitivePeerDependencies:
       - supports-color
 
-  babel-plugin-polyfill-regenerator@0.5.3(@babel/core@7.24.0):
+  babel-plugin-polyfill-regenerator@0.6.2(@babel/core@7.24.7):
     dependencies:
-      '@babel/core': 7.24.0
-      '@babel/helper-define-polyfill-provider': 0.4.3(@babel/core@7.24.0)
+      '@babel/core': 7.24.7
+      '@babel/helper-define-polyfill-provider': 0.6.2(@babel/core@7.24.7)
     transitivePeerDependencies:
       - supports-color
 
@@ -17018,15 +17936,39 @@ snapshots:
       '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.23.5)
       '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.23.5)
 
+  babel-preset-current-node-syntax@1.0.1(@babel/core@7.24.7):
+    dependencies:
+      '@babel/core': 7.24.7
+      '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.24.7)
+      '@babel/plugin-syntax-bigint': 7.8.3(@babel/core@7.24.7)
+      '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.24.7)
+      '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.24.7)
+      '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.24.7)
+      '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.24.7)
+      '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.24.7)
+      '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.24.7)
+      '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.24.7)
+      '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.24.7)
+      '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.24.7)
+      '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.24.7)
+    optional: true
+
   babel-preset-jest@29.6.3(@babel/core@7.23.5):
     dependencies:
       '@babel/core': 7.23.5
       babel-plugin-jest-hoist: 29.6.3
       babel-preset-current-node-syntax: 1.0.1(@babel/core@7.23.5)
 
+  babel-preset-jest@29.6.3(@babel/core@7.24.7):
+    dependencies:
+      '@babel/core': 7.24.7
+      babel-plugin-jest-hoist: 29.6.3
+      babel-preset-current-node-syntax: 1.0.1(@babel/core@7.24.7)
+    optional: true
+
   babel-walk@3.0.0-canary-5:
     dependencies:
-      '@babel/types': 7.24.0
+      '@babel/types': 7.24.7
 
   bail@2.0.2: {}
 
@@ -17078,23 +18020,6 @@ snapshots:
 
   bn.js@4.12.0: {}
 
-  body-parser@1.20.1:
-    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
-      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
@@ -17133,6 +18058,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
@@ -17142,10 +18071,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
@@ -17196,14 +18121,19 @@ snapshots:
       node-gyp-build: 4.6.0
     optional: true
 
-  bullmq@5.7.8:
+  bufferutil@4.0.8:
+    dependencies:
+      node-gyp-build: 4.8.1
+    optional: true
+
+  bullmq@5.10.4:
     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
+      tslib: 2.6.3
       uuid: 9.0.1
     transitivePeerDependencies:
       - supports-color
@@ -17223,10 +18153,10 @@ snapshots:
   cacache@18.0.0:
     dependencies:
       '@npmcli/fs': 3.1.0
-      fs-minipass: 3.0.2
-      glob: 10.3.12
+      fs-minipass: 3.0.3
+      glob: 10.3.10
       lru-cache: 10.2.2
-      minipass: 7.0.4
+      minipass: 7.1.2
       minipass-collect: 1.0.2
       minipass-flush: 1.0.5
       minipass-pipeline: 1.2.4
@@ -17249,6 +18179,16 @@ snapshots:
       normalize-url: 8.0.0
       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:
     dependencies:
       clone-response: 1.0.3
@@ -17343,26 +18283,26 @@ snapshots:
     dependencies:
       is-regex: 1.1.4
 
-  chart.js@4.4.2:
+  chart.js@4.4.3:
     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.3)(date-fns@2.30.0):
     dependencies:
-      chart.js: 4.4.2
+      chart.js: 4.4.3
       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.3):
     dependencies:
-      chart.js: 4.4.2
+      chart.js: 4.4.3
 
-  chartjs-plugin-gradient@0.6.1(chart.js@4.4.2):
+  chartjs-plugin-gradient@0.6.1(chart.js@4.4.3):
     dependencies:
-      chart.js: 4.4.2
+      chart.js: 4.4.3
 
-  chartjs-plugin-zoom@2.0.1(chart.js@4.4.2):
+  chartjs-plugin-zoom@2.0.1(chart.js@4.4.3):
     dependencies:
-      chart.js: 4.4.2
+      chart.js: 4.4.3
       hammerjs: 2.0.8
 
   check-error@1.0.3:
@@ -17402,11 +18342,9 @@ snapshots:
     optionalDependencies:
       fsevents: 2.3.3
 
-  chownr@1.1.4: {}
-
   chownr@2.0.0: {}
 
-  chromatic@11.3.0: {}
+  chromatic@11.5.6: {}
 
   ci-info@3.7.1: {}
 
@@ -17431,8 +18369,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:
@@ -17532,7 +18468,7 @@ snapshots:
 
   commondir@1.0.1: {}
 
-  compare-versions@6.1.0: {}
+  compare-versions@6.1.1: {}
 
   compress-commons@6.0.2:
     dependencies:
@@ -17585,8 +18521,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:
@@ -17604,7 +18540,7 @@ snapshots:
 
   cookie@0.6.0: {}
 
-  core-js-compat@3.33.3:
+  core-js-compat@3.37.1:
     dependencies:
       browserslist: 4.23.0
 
@@ -17624,13 +18560,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:
@@ -17643,10 +18579,10 @@ snapshots:
     dependencies:
       luxon: 3.3.0
 
-  cropperjs@2.0.0-beta.5:
+  cropperjs@2.0.0-rc.1:
     dependencies:
-      '@cropper/elements': 2.0.0-beta.5
-      '@cropper/utils': 2.0.0-beta.5
+      '@cropper/elements': 2.0.0-rc.1
+      '@cropper/utils': 2.0.0-rc.1
 
   cross-env@7.0.3:
     dependencies:
@@ -17676,11 +18612,13 @@ 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):
+  crypto-random-string@4.0.0:
     dependencies:
-      postcss: 8.4.38
+      type-fest: 1.4.0
+
+  css-declaration-sorter@7.2.0(postcss@8.4.40):
+    dependencies:
+      postcss: 8.4.40
 
   css-select@5.1.0:
     dependencies:
@@ -17693,12 +18631,12 @@ snapshots:
   css-tree@2.2.1:
     dependencies:
       mdn-data: 2.0.28
-      source-map-js: 1.0.2
+      source-map-js: 1.2.0
 
   css-tree@2.3.1:
     dependencies:
       mdn-data: 2.0.30
-      source-map-js: 1.0.2
+      source-map-js: 1.2.0
 
   css-what@6.1.0: {}
 
@@ -17706,49 +18644,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.40):
     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.40)
+      cssnano-utils: 4.0.2(postcss@8.4.40)
+      postcss: 8.4.40
+      postcss-calc: 9.0.1(postcss@8.4.40)
+      postcss-colormin: 6.1.0(postcss@8.4.40)
+      postcss-convert-values: 6.1.0(postcss@8.4.40)
+      postcss-discard-comments: 6.0.2(postcss@8.4.40)
+      postcss-discard-duplicates: 6.0.3(postcss@8.4.40)
+      postcss-discard-empty: 6.0.3(postcss@8.4.40)
+      postcss-discard-overridden: 6.0.2(postcss@8.4.40)
+      postcss-merge-longhand: 6.0.5(postcss@8.4.40)
+      postcss-merge-rules: 6.1.1(postcss@8.4.40)
+      postcss-minify-font-values: 6.1.0(postcss@8.4.40)
+      postcss-minify-gradients: 6.0.3(postcss@8.4.40)
+      postcss-minify-params: 6.1.0(postcss@8.4.40)
+      postcss-minify-selectors: 6.0.4(postcss@8.4.40)
+      postcss-normalize-charset: 6.0.2(postcss@8.4.40)
+      postcss-normalize-display-values: 6.0.2(postcss@8.4.40)
+      postcss-normalize-positions: 6.0.2(postcss@8.4.40)
+      postcss-normalize-repeat-style: 6.0.2(postcss@8.4.40)
+      postcss-normalize-string: 6.0.2(postcss@8.4.40)
+      postcss-normalize-timing-functions: 6.0.2(postcss@8.4.40)
+      postcss-normalize-unicode: 6.1.0(postcss@8.4.40)
+      postcss-normalize-url: 6.0.2(postcss@8.4.40)
+      postcss-normalize-whitespace: 6.0.2(postcss@8.4.40)
+      postcss-ordered-values: 6.0.2(postcss@8.4.40)
+      postcss-reduce-initial: 6.1.0(postcss@8.4.40)
+      postcss-reduce-transforms: 6.0.2(postcss@8.4.40)
+      postcss-svgo: 6.0.3(postcss@8.4.40)
+      postcss-unique-selectors: 6.0.4(postcss@8.4.40)
 
-  cssnano-utils@4.0.2(postcss@8.4.38):
+  cssnano-utils@4.0.2(postcss@8.4.40):
     dependencies:
-      postcss: 8.4.38
+      postcss: 8.4.40
 
-  cssnano@6.1.2(postcss@8.4.38):
+  cssnano@6.1.2(postcss@8.4.40):
     dependencies:
-      cssnano-preset-default: 6.1.2(postcss@8.4.38)
+      cssnano-preset-default: 6.1.2(postcss@8.4.40)
       lilconfig: 3.1.1
-      postcss: 8.4.38
+      postcss: 8.4.40
 
   csso@5.0.5:
     dependencies:
@@ -17760,7 +18698,7 @@ snapshots:
 
   csstype@3.1.3: {}
 
-  cypress@13.7.3:
+  cypress@13.13.1:
     dependencies:
       '@cypress/request': 3.0.0
       '@cypress/xvfb': 1.2.4(supports-color@8.1.1)
@@ -17778,52 +18716,7 @@ snapshots:
       commander: 6.2.1
       common-tags: 1.8.2
       dayjs: 1.11.10
-      debug: 4.3.4(supports-color@8.1.1)
-      enquirer: 2.3.6
-      eventemitter2: 6.4.7
-      execa: 4.1.0
-      executable: 4.1.1
-      extract-zip: 2.0.1(supports-color@8.1.1)
-      figures: 3.2.0
-      fs-extra: 9.1.0
-      getos: 3.2.1
-      is-ci: 3.0.1
-      is-installed-globally: 0.4.0
-      lazy-ass: 1.6.0
-      listr2: 3.14.0(enquirer@2.3.6)
-      lodash: 4.17.21
-      log-symbols: 4.1.0
-      minimist: 1.2.8
-      ospath: 1.2.2
-      pretty-bytes: 5.6.0
-      process: 0.11.10
-      proxy-from-env: 1.0.0
-      request-progress: 3.0.0
-      semver: 7.6.0
-      supports-color: 8.1.1
-      tmp: 0.2.3
-      untildify: 4.0.0
-      yauzl: 2.10.0
-
-  cypress@13.8.1:
-    dependencies:
-      '@cypress/request': 3.0.0
-      '@cypress/xvfb': 1.2.4(supports-color@8.1.1)
-      '@types/sinonjs__fake-timers': 8.1.1
-      '@types/sizzle': 2.3.3
-      arch: 2.2.0
-      blob-util: 2.0.2
-      bluebird: 3.7.2
-      buffer: 5.7.1
-      cachedir: 2.3.0
-      chalk: 4.1.2
-      check-more-types: 2.24.0
-      cli-cursor: 3.1.0
-      cli-table3: 0.6.3
-      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
@@ -17885,7 +18778,7 @@ snapshots:
     optionalDependencies:
       supports-color: 5.5.0
 
-  debug@4.3.4(supports-color@8.1.1):
+  debug@4.3.5(supports-color@8.1.1):
     dependencies:
       ms: 2.1.2
     optionalDependencies:
@@ -17978,17 +18871,6 @@ snapshots:
 
   defu@6.1.4: {}
 
-  del@6.1.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
-
   delayed-stream@1.0.0: {}
 
   denque@2.1.0: {}
@@ -18012,7 +18894,7 @@ snapshots:
   detect-port@1.5.1:
     dependencies:
       address: 1.2.2
-      debug: 4.3.4(supports-color@8.1.1)
+      debug: 4.3.5(supports-color@8.1.1)
     transitivePeerDependencies:
       - supports-color
 
@@ -18026,6 +18908,8 @@ snapshots:
 
   diff@5.1.0: {}
 
+  diff@5.2.0: {}
+
   dijkstrajs@1.0.2: {}
 
   dir-glob@3.0.1:
@@ -18072,13 +18956,6 @@ snapshots:
 
   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:
@@ -18099,7 +18976,7 @@ snapshots:
 
   ee-first@1.1.1: {}
 
-  ejs@3.1.9:
+  ejs@3.1.10:
     dependencies:
       jake: 10.8.5
 
@@ -18134,6 +19011,8 @@ snapshots:
 
   entities@4.5.0: {}
 
+  entities@5.0.0: {}
+
   env-paths@2.2.1: {}
 
   envinfo@7.8.1: {}
@@ -18198,7 +19077,7 @@ snapshots:
       isarray: 2.0.5
       stop-iteration-iterator: 1.0.0
 
-  es-module-lexer@0.9.3: {}
+  es-module-lexer@1.5.4: {}
 
   es-set-tostringtag@2.0.1:
     dependencies:
@@ -18218,10 +19097,10 @@ snapshots:
 
   esbuild-plugin-alias@0.2.1: {}
 
-  esbuild-register@3.5.0(esbuild@0.20.2):
+  esbuild-register@3.5.0(esbuild@0.19.11):
     dependencies:
-      debug: 4.3.4(supports-color@8.1.1)
-      esbuild: 0.20.2
+      debug: 4.3.5(supports-color@8.1.1)
+      esbuild: 0.19.11
     transitivePeerDependencies:
       - supports-color
 
@@ -18276,31 +19155,58 @@ 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
 
   escalade@3.1.1: {}
 
@@ -18347,37 +19253,17 @@ snapshots:
     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.8.0(@typescript-eslint/parser@7.17.0(eslint@9.8.0)(typescript@5.5.4))(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': 6.11.0(eslint@8.53.0)(typescript@5.3.3)
-      eslint: 8.53.0
+      '@typescript-eslint/parser': 7.17.0(eslint@9.8.0)(typescript@5.5.4)
+      eslint: 9.8.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):
-    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
-      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):
-    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
-      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.29.1(@typescript-eslint/parser@7.17.0(eslint@9.8.0)(typescript@5.5.4))(eslint@9.8.0):
     dependencies:
       array-includes: 3.1.7
       array.prototype.findlastindex: 1.2.3
@@ -18385,9 +19271,9 @@ snapshots:
       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.8.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)
+      eslint-module-utils: 2.8.0(@typescript-eslint/parser@7.17.0(eslint@9.8.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint@9.8.0)
       hasown: 2.0.0
       is-core-module: 2.13.1
       is-glob: 4.0.3
@@ -18398,76 +19284,22 @@ snapshots:
       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.8.0)(typescript@5.5.4)
     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-vue@9.27.0(eslint@9.8.0):
     dependencies:
-      array-includes: 3.1.7
-      array.prototype.findlastindex: 1.2.3
-      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-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
-      is-glob: 4.0.3
-      minimatch: 3.1.2
-      object.fromentries: 2.0.7
-      object.groupby: 1.0.1
-      object.values: 1.1.7
-      semver: 6.3.1
-      tsconfig-paths: 3.15.0
-    optionalDependencies:
-      '@typescript-eslint/parser': 7.1.0(eslint@8.57.0)(typescript@5.3.3)
-    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):
-    dependencies:
-      array-includes: 3.1.7
-      array.prototype.findlastindex: 1.2.3
-      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-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
-      is-glob: 4.0.3
-      minimatch: 3.1.2
-      object.fromentries: 2.0.7
-      object.groupby: 1.0.1
-      object.values: 1.1.7
-      semver: 6.3.1
-      tsconfig-paths: 3.15.0
-    optionalDependencies:
-      '@typescript-eslint/parser': 7.7.1(eslint@8.57.0)(typescript@5.4.5)
-    transitivePeerDependencies:
-      - eslint-import-resolver-typescript
-      - eslint-import-resolver-webpack
-      - supports-color
-
-  eslint-plugin-vue@9.25.0(eslint@8.57.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.8.0)
+      eslint: 9.8.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.8.0)
       xml-name-validator: 4.0.0
     transitivePeerDependencies:
       - supports-color
@@ -18479,55 +19311,19 @@ 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:
-    dependencies:
-      '@eslint-community/eslint-utils': 4.4.0(eslint@8.53.0)
-      '@eslint-community/regexpp': 4.10.0
-      '@eslint/eslintrc': 2.1.4
-      '@eslint/js': 8.53.0
-      '@humanwhocodes/config-array': 0.11.13
-      '@humanwhocodes/module-importer': 1.0.1
-      '@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
-      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
-      esutils: 2.0.3
-      fast-deep-equal: 3.1.3
-      file-entry-cache: 6.0.1
-      find-up: 5.0.0
-      glob-parent: 6.0.2
-      globals: 13.24.0
-      graphemer: 1.4.0
-      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
-      strip-ansi: 6.0.1
-      text-table: 0.2.0
-    transitivePeerDependencies:
-      - supports-color
+  eslint-visitor-keys@4.0.0: {}
 
   eslint@8.57.0:
     dependencies:
       '@eslint-community/eslint-utils': 4.4.0(eslint@8.57.0)
-      '@eslint-community/regexpp': 4.10.0
+      '@eslint-community/regexpp': 4.11.0
       '@eslint/eslintrc': 2.1.4
       '@eslint/js': 8.57.0
       '@humanwhocodes/config-array': 0.11.14
@@ -18537,13 +19333,13 @@ snapshots:
       ajv: 6.12.6
       chalk: 4.1.2
       cross-spawn: 7.0.3
-      debug: 4.3.4(supports-color@8.1.1)
+      debug: 4.3.5(supports-color@8.1.1)
       doctrine: 3.0.0
       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
+      esquery: 1.6.0
       esutils: 2.0.3
       fast-deep-equal: 3.1.3
       file-entry-cache: 6.0.1
@@ -18561,16 +19357,61 @@ snapshots:
       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@9.8.0:
+    dependencies:
+      '@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
+      ajv: 6.12.6
+      chalk: 4.1.2
+      cross-spawn: 7.0.3
+      debug: 4.3.5(supports-color@8.1.1)
+      escape-string-regexp: 4.0.0
+      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: 8.0.0
+      find-up: 5.0.0
+      glob-parent: 6.0.2
+      ignore: 5.3.1
+      imurmurhash: 0.1.4
+      is-glob: 4.0.3
+      is-path-inside: 3.0.3
+      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.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: {}
@@ -18579,6 +19420,10 @@ snapshots:
     dependencies:
       estraverse: 5.3.0
 
+  esquery@1.6.0:
+    dependencies:
+      estraverse: 5.3.0
+
   esrecurse@4.3.0:
     dependencies:
       estraverse: 5.3.0
@@ -18656,7 +19501,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
@@ -18673,6 +19518,21 @@ snapshots:
       signal-exit: 4.1.0
       strip-final-newline: 3.0.0
 
+  execa@9.3.0:
+    dependencies:
+      '@sindresorhus/merge-streams': 4.0.0
+      cross-spawn: 7.0.3
+      figures: 6.1.0
+      get-stream: 9.0.1
+      human-signals: 7.0.0
+      is-plain-obj: 4.1.0
+      is-stream: 4.0.1
+      npm-run-path: 5.3.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
@@ -18689,42 +19549,6 @@ snapshots:
 
   exponential-backoff@3.1.1: {}
 
-  express@4.18.2:
-    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
@@ -18774,7 +19598,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:
@@ -18798,15 +19622,15 @@ snapshots:
       '@nodelib/fs.walk': 1.2.8
       glob-parent: 5.1.2
       merge2: 1.4.1
-      micromatch: 4.0.5
+      micromatch: 4.0.7
 
   fast-json-stable-stringify@2.1.0: {}
 
   fast-json-stringify@5.8.0:
     dependencies:
       '@fastify/deepmerge': 1.3.0
-      ajv: 8.13.0
-      ajv-formats: 2.1.1(ajv@8.13.0)
+      ajv: 8.17.1
+      ajv-formats: 2.1.1(ajv@8.17.1)
       fast-deep-equal: 3.1.3
       fast-uri: 2.2.0
       rfdc: 1.3.0
@@ -18823,10 +19647,16 @@ snapshots:
 
   fast-uri@2.2.0: {}
 
+  fast-uri@3.0.1: {}
+
   fast-xml-parser@4.2.5:
     dependencies:
       strnum: 1.0.5
 
+  fast-xml-parser@4.4.0:
+    dependencies:
+      strnum: 1.0.5
+
   fastify-multer@2.0.3:
     dependencies:
       '@fastify/busboy': 1.2.1
@@ -18850,7 +19680,7 @@ snapshots:
       raw-body: 2.5.2
       secure-json-parse: 2.7.0
 
-  fastify@4.26.2:
+  fastify@4.28.1:
     dependencies:
       '@fastify/ajv-compiler': 3.5.0
       '@fastify/error': 3.4.0
@@ -18861,20 +19691,16 @@ snapshots:
       fast-json-stringify: 5.8.0
       find-my-way: 8.2.0
       light-my-request: 5.11.0
-      pino: 8.17.0
+      pino: 9.2.0
       process-warning: 3.0.0
       proxy-addr: 2.0.7
       rfdc: 1.3.0
       secure-json-parse: 2.7.0
       semver: 7.6.0
-      toad-cache: 3.3.0
+      toad-cache: 3.7.0
     transitivePeerDependencies:
       - supports-color
 
-  fastq@1.15.0:
-    dependencies:
-      reusify: 1.0.4
-
   fastq@1.17.1:
     dependencies:
       reusify: 1.0.4
@@ -18883,6 +19709,10 @@ snapshots:
     dependencies:
       bser: 2.1.1
 
+  fd-package-json@1.2.0:
+    dependencies:
+      walk-up-path: 3.0.1
+
   fd-slicer@1.1.0:
     dependencies:
       pend: 1.2.0
@@ -18902,9 +19732,17 @@ snapshots:
     dependencies:
       escape-string-regexp: 1.0.5
 
+  figures@6.1.0:
+    dependencies:
+      is-unicode-supported: 2.0.0
+
   file-entry-cache@6.0.1:
     dependencies:
-      flat-cache: 3.0.4
+      flat-cache: 3.2.0
+
+  file-entry-cache@8.0.0:
+    dependencies:
+      flat-cache: 4.0.1
 
   file-system-cache@2.3.0:
     dependencies:
@@ -18917,11 +19755,11 @@ snapshots:
       strtok3: 7.0.0
       token-types: 5.0.1
 
-  file-type@19.0.0:
+  file-type@19.3.0:
     dependencies:
-      readable-web-to-node-stream: 3.0.2
-      strtok3: 7.0.0
-      token-types: 5.0.1
+      strtok3: 8.0.1
+      token-types: 6.0.0
+      uint8array-extras: 1.4.0
 
   filelist@1.0.4:
     dependencies:
@@ -18939,6 +19777,10 @@ snapshots:
     dependencies:
       to-regex-range: 5.0.1
 
+  fill-range@7.1.1:
+    dependencies:
+      to-regex-range: 5.0.1
+
   finalhandler@1.2.0:
     dependencies:
       debug: 2.6.9
@@ -18998,23 +19840,29 @@ snapshots:
       ps-list: 8.1.1
       taskkill: 5.0.0
 
-  flat-cache@3.0.4:
+  flat-cache@3.2.0:
     dependencies:
-      flatted: 3.2.7
+      flatted: 3.3.1
+      keyv: 4.5.4
       rimraf: 3.0.2
 
-  flatted@3.2.7: {}
+  flat-cache@4.0.1:
+    dependencies:
+      flatted: 3.3.1
+      keyv: 4.5.4
+
+  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(debug@4.3.5):
     optionalDependencies:
-      debug: 4.3.4(supports-color@8.1.1)
+      debug: 4.3.5(supports-color@8.1.1)
 
   for-each@0.3.3:
     dependencies:
@@ -19053,8 +19901,6 @@ snapshots:
 
   from@0.1.7: {}
 
-  fs-constants@1.0.0: {}
-
   fs-extra@11.1.1:
     dependencies:
       graceful-fs: 4.2.11
@@ -19084,9 +19930,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: {}
 
@@ -19117,8 +19963,6 @@ snapshots:
       has-proto: 1.0.1
       has-symbols: 1.0.3
 
-  get-npm-tarball-url@2.0.3: {}
-
   get-package-type@0.1.0: {}
 
   get-stream@3.0.0: {}
@@ -19131,6 +19975,11 @@ 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
@@ -19185,13 +20034,23 @@ snapshots:
       minipass: 7.0.4
       path-scurry: 1.10.1
 
-  glob@10.3.12:
+  glob@10.4.2:
     dependencies:
       foreground-child: 3.1.1
-      jackspeak: 2.3.6
-      minimatch: 9.0.3
-      minipass: 7.0.4
-      path-scurry: 1.10.2
+      jackspeak: 3.4.0
+      minimatch: 9.0.4
+      minipass: 7.1.2
+      package-json-from-dist: 1.0.0
+      path-scurry: 1.11.1
+
+  glob@11.0.0:
+    dependencies:
+      foreground-child: 3.1.1
+      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:
@@ -19220,6 +20079,10 @@ snapshots:
     dependencies:
       type-fest: 0.20.2
 
+  globals@14.0.0: {}
+
+  globals@15.8.0: {}
+
   globalthis@1.0.3:
     dependencies:
       define-properties: 1.2.0
@@ -19233,6 +20096,15 @@ snapshots:
       merge2: 1.4.1
       slash: 3.0.0
 
+  globby@14.0.1:
+    dependencies:
+      '@sindresorhus/merge-streams': 2.3.0
+      fast-glob: 3.3.2
+      ignore: 5.3.1
+      path-type: 5.0.0
+      slash: 5.1.0
+      unicorn-magic: 0.1.0
+
   gopd@1.0.1:
     dependencies:
       get-intrinsic: 1.2.1
@@ -19265,19 +20137,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: {}
 
@@ -19287,15 +20159,6 @@ 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:
@@ -19316,6 +20179,12 @@ snapshots:
       whatwg-encoding: 2.0.0
       whatwg-mimetype: 3.0.0
 
+  happy-dom@15.6.1:
+    dependencies:
+      entities: 4.5.0
+      webidl-conversions: 7.0.0
+      whatwg-mimetype: 3.0.0
+
   hard-rejection@2.1.0: {}
 
   has-bigints@1.0.2: {}
@@ -19407,10 +20276,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@8.1.1)
     transitivePeerDependencies:
       - supports-color
 
@@ -19435,14 +20304,28 @@ 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@8.1.1)
     transitivePeerDependencies:
       - supports-color
 
   https-proxy-agent@7.0.2:
     dependencies:
       agent-base: 7.1.0
-      debug: 4.3.4(supports-color@8.1.1)
+      debug: 4.3.4(supports-color@5.5.0)
+    transitivePeerDependencies:
+      - supports-color
+
+  https-proxy-agent@7.0.4:
+    dependencies:
+      agent-base: 7.1.0
+      debug: 4.3.5(supports-color@8.1.1)
+    transitivePeerDependencies:
+      - supports-color
+
+  https-proxy-agent@7.0.5:
+    dependencies:
+      agent-base: 7.1.0
+      debug: 4.3.5(supports-color@8.1.1)
     transitivePeerDependencies:
       - supports-color
 
@@ -19454,6 +20337,8 @@ snapshots:
 
   human-signals@5.0.0: {}
 
+  human-signals@7.0.0: {}
+
   iconv-lite@0.4.24:
     dependencies:
       safer-buffer: 2.1.2
@@ -19468,9 +20353,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.3.1: {}
 
@@ -19481,21 +20366,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:
@@ -19536,7 +20421,7 @@ snapshots:
     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
@@ -19546,15 +20431,14 @@ snapshots:
     transitivePeerDependencies:
       - supports-color
 
-  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.1:
     dependencies:
-      ip-address: 7.1.0
-      jsbn: 1.1.0
+      ip-address: 9.0.5
 
   ip-regex@4.3.0: {}
 
@@ -19610,8 +20494,6 @@ snapshots:
     dependencies:
       has-tostringtag: 1.0.0
 
-  is-deflate@1.0.0: {}
-
   is-docker@2.2.1: {}
 
   is-expression@4.0.0:
@@ -19635,8 +20517,6 @@ snapshots:
     dependencies:
       is-extglob: 2.1.1
 
-  is-gzip@1.0.0: {}
-
   is-installed-globally@0.4.0:
     dependencies:
       global-dirs: 3.0.1
@@ -19667,8 +20547,6 @@ snapshots:
 
   is-number@7.0.0: {}
 
-  is-path-cwd@2.2.0: {}
-
   is-path-inside@3.0.3: {}
 
   is-plain-obj@1.1.0: {}
@@ -19702,13 +20580,15 @@ snapshots:
 
   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.0.1:
     dependencies:
-      fast-xml-parser: 4.2.5
+      fast-xml-parser: 4.4.0
 
   is-symbol@1.0.4:
     dependencies:
@@ -19726,6 +20606,8 @@ snapshots:
 
   is-unicode-supported@0.1.0: {}
 
+  is-unicode-supported@2.0.0: {}
+
   is-weakmap@2.0.1: {}
 
   is-weakref@1.0.2:
@@ -19759,8 +20641,8 @@ snapshots:
 
   istanbul-lib-instrument@5.2.1:
     dependencies:
-      '@babel/core': 7.24.0
-      '@babel/parser': 7.24.5
+      '@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
@@ -19769,8 +20651,8 @@ snapshots:
 
   istanbul-lib-instrument@6.0.0:
     dependencies:
-      '@babel/core': 7.24.0
-      '@babel/parser': 7.24.5
+      '@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
@@ -19785,12 +20667,20 @@ snapshots:
 
   istanbul-lib-source-maps@4.0.1:
     dependencies:
-      debug: 4.3.4(supports-color@8.1.1)
+      debug: 4.3.5(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@8.1.1)
+      istanbul-lib-coverage: 3.2.2
+    transitivePeerDependencies:
+      - supports-color
+
   istanbul-reports@3.1.6:
     dependencies:
       html-escaper: 2.0.2
@@ -19804,6 +20694,18 @@ snapshots:
     optionalDependencies:
       '@pkgjs/parseargs': 0.11.0
 
+  jackspeak@3.4.0:
+    dependencies:
+      '@isaacs/cliui': 8.0.2
+    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
@@ -19823,7 +20725,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
@@ -19843,16 +20745,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
@@ -19862,7 +20764,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
@@ -19881,13 +20783,13 @@ snapshots:
       jest-runner: 29.7.0
       jest-util: 29.7.0
       jest-validate: 29.7.0
-      micromatch: 4.0.5
+      micromatch: 4.0.7
       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
@@ -19916,7 +20818,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
 
@@ -19933,14 +20835,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.7
       walker: 1.0.8
     optionalDependencies:
       fsevents: 2.3.3
@@ -19964,7 +20866,7 @@ snapshots:
       '@types/stack-utils': 2.0.1
       chalk: 4.1.2
       graceful-fs: 4.2.11
-      micromatch: 4.0.5
+      micromatch: 4.0.7
       pretty-format: 29.7.0
       slash: 3.0.0
       stack-utils: 2.0.6
@@ -19972,7 +20874,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):
@@ -20007,7 +20909,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
@@ -20035,7 +20937,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
@@ -20056,10 +20958,10 @@ snapshots:
   jest-snapshot@29.7.0:
     dependencies:
       '@babel/core': 7.23.5
-      '@babel/generator': 7.23.6
+      '@babel/generator': 7.24.7
       '@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.24.0
+      '@babel/types': 7.24.7
       '@jest/expect-utils': 29.7.0
       '@jest/transform': 29.7.0
       '@jest/types': 29.6.3
@@ -20081,7 +20983,7 @@ snapshots:
   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
@@ -20100,7 +21002,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
@@ -20114,17 +21016,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
@@ -20152,6 +21054,8 @@ snapshots:
 
   js-tokens@4.0.0: {}
 
+  js-tokens@9.0.0: {}
+
   js-yaml@3.14.1:
     dependencies:
       argparse: 1.0.10
@@ -20167,61 +21071,90 @@ snapshots:
 
   jschardet@3.0.0: {}
 
-  jscodeshift@0.15.1(@babel/preset-env@7.23.5(@babel/core@7.24.0)):
+  jscodeshift@0.15.1(@babel/preset-env@7.24.7(@babel/core@7.24.7)):
     dependencies:
-      '@babel/core': 7.24.0
-      '@babel/parser': 7.24.5
-      '@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)
+      '@babel/core': 7.24.7
+      '@babel/parser': 7.24.7
+      '@babel/plugin-transform-class-properties': 7.24.7(@babel/core@7.24.7)
+      '@babel/plugin-transform-modules-commonjs': 7.24.7(@babel/core@7.24.7)
+      '@babel/plugin-transform-nullish-coalescing-operator': 7.24.7(@babel/core@7.24.7)
+      '@babel/plugin-transform-optional-chaining': 7.24.7(@babel/core@7.24.7)
+      '@babel/plugin-transform-private-methods': 7.24.7(@babel/core@7.24.7)
+      '@babel/preset-flow': 7.23.3(@babel/core@7.24.7)
+      '@babel/preset-typescript': 7.23.3(@babel/core@7.24.7)
+      '@babel/register': 7.22.15(@babel/core@7.24.7)
+      babel-core: 7.0.0-bridge.0(@babel/core@7.24.7)
       chalk: 4.1.2
       flow-parser: 0.202.0
       graceful-fs: 4.2.11
-      micromatch: 4.0.5
+      micromatch: 4.0.7
       neo-async: 2.6.2
       node-dir: 0.1.17
-      recast: 0.23.4
+      recast: 0.23.6
       temp: 0.8.4
       write-file-atomic: 2.4.3
     optionalDependencies:
-      '@babel/preset-env': 7.23.5(@babel/core@7.24.0)
+      '@babel/preset-env': 7.24.7(@babel/core@7.24.7)
     transitivePeerDependencies:
       - supports-color
 
-  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
 
+  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@0.5.0: {}
 
   jsesc@2.5.2: {}
@@ -20269,9 +21202,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
@@ -20296,8 +21229,6 @@ snapshots:
 
   jsrsasign@11.1.0: {}
 
-  jssha@3.3.1: {}
-
   jstransformer@1.0.0:
     dependencies:
       is-promise: 2.2.2
@@ -20328,13 +21259,13 @@ 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: {}
 
@@ -20380,7 +21311,10 @@ snapshots:
     optionalDependencies:
       enquirer: 2.3.6
 
-  local-pkg@0.4.3: {}
+  local-pkg@0.5.0:
+    dependencies:
+      mlly: 1.5.0
+      pkg-types: 1.0.3
 
   locate-path@3.0.0:
     dependencies:
@@ -20403,8 +21337,6 @@ snapshots:
 
   lodash.isarguments@3.1.0: {}
 
-  lodash.isequal@4.5.0: {}
-
   lodash.memoize@4.1.2: {}
 
   lodash.merge@4.6.2: {}
@@ -20443,6 +21375,8 @@ snapshots:
 
   lru-cache@10.2.2: {}
 
+  lru-cache@11.0.0: {}
+
   lru-cache@4.1.5:
     dependencies:
       pseudomap: 1.0.2
@@ -20472,9 +21406,11 @@ snapshots:
     dependencies:
       '@jridgewell/sourcemap-codec': 1.4.15
 
-  magic-string@0.30.7:
+  magicast@0.3.4:
     dependencies:
-      '@jridgewell/sourcemap-codec': 1.4.15
+      '@babel/parser': 7.24.7
+      '@babel/types': 7.24.7
+      source-map-js: 1.2.0
 
   mailcheck@1.1.1: {}
 
@@ -20499,7 +21435,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
@@ -20523,7 +21459,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
 
@@ -20638,7 +21574,7 @@ snapshots:
 
   media-typer@0.3.0: {}
 
-  meilisearch@0.38.0(encoding@0.1.13):
+  meilisearch@0.41.0(encoding@0.1.13):
     dependencies:
       cross-fetch: 3.1.6(encoding@0.1.13)
     transitivePeerDependencies:
@@ -20847,7 +21783,7 @@ snapshots:
   micromark@4.0.0:
     dependencies:
       '@types/debug': 4.1.12
-      debug: 4.3.4(supports-color@8.1.1)
+      debug: 4.3.5(supports-color@8.1.1)
       decode-named-character-reference: 1.0.2
       devlop: 1.1.0
       micromark-core-commonmark: 2.0.0
@@ -20866,9 +21802,9 @@ snapshots:
     transitivePeerDependencies:
       - supports-color
 
-  micromatch@4.0.5:
+  micromatch@4.0.7:
     dependencies:
-      braces: 3.0.2
+      braces: 3.0.3
       picomatch: 2.3.1
 
   mime-db@1.52.0: {}
@@ -20895,6 +21831,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
@@ -20959,13 +21899,13 @@ snapshots:
 
   minipass@7.0.4: {}
 
+  minipass@7.1.2: {}
+
   minizlib@2.1.2:
     dependencies:
       minipass: 3.3.6
       yallist: 4.0.0
 
-  mkdirp-classic@0.5.3: {}
-
   mkdirp@0.5.6:
     dependencies:
       minimist: 1.2.8
@@ -20976,7 +21916,7 @@ 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
@@ -21015,18 +21955,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.3.4(typescript@5.5.4)):
     dependencies:
       is-node-process: 1.2.0
-      msw: 2.2.14(typescript@5.4.5)
+      msw: 2.3.4(typescript@5.5.4)
 
-  msw@2.2.14(typescript@5.4.5):
+  msw@2.3.4(typescript@5.5.4):
     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
@@ -21037,10 +21977,10 @@ 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.5.4
 
   muggle-string@0.4.1: {}
 
@@ -21064,7 +22004,7 @@ snapshots:
       object-assign: 4.1.1
       thenify-all: 1.6.0
 
-  nan@2.18.0: {}
+  nan@2.20.0: {}
 
   nanoid@3.3.7: {}
 
@@ -21143,11 +22083,11 @@ snapshots:
 
   node-gyp-build@4.8.1: {}
 
-  node-gyp@10.0.1:
+  node-gyp@10.1.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
@@ -21162,7 +22102,7 @@ snapshots:
 
   node-releases@2.0.14: {}
 
-  nodemailer@6.9.13: {}
+  nodemailer@6.9.14: {}
 
   nodemon@3.0.2:
     dependencies:
@@ -21177,14 +22117,14 @@ snapshots:
       touch: 3.1.0
       undefsafe: 2.0.5
 
-  nodemon@3.1.0:
+  nodemon@3.1.4:
     dependencies:
       chokidar: 3.5.3
       debug: 4.3.4(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
@@ -21224,6 +22164,8 @@ snapshots:
 
   normalize-url@8.0.0: {}
 
+  normalize-url@8.0.1: {}
+
   npm-run-path@2.0.2:
     dependencies:
       path-key: 2.0.1
@@ -21236,11 +22178,15 @@ snapshots:
     dependencies:
       path-key: 4.0.0
 
+  npm-run-path@5.3.0:
+    dependencies:
+      path-key: 4.0.0
+
   nth-check@2.1.1:
     dependencies:
       boolbase: 1.0.0
 
-  nwsapi@2.2.9: {}
+  nwsapi@2.2.12: {}
 
   oauth2orize-pkce@0.1.2: {}
 
@@ -21336,30 +22282,30 @@ 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
+      word-wrap: 1.2.5
 
   ora@5.4.1:
     dependencies:
       bl: 4.1.0
       chalk: 4.1.2
       cli-cursor: 3.1.0
-      cli-spinners: 2.7.0
+      cli-spinners: 2.9.2
       is-interactive: 1.0.0
       is-unicode-supported: 0.1.0
       log-symbols: 4.1.0
@@ -21374,9 +22320,9 @@ snapshots:
 
   ospath@1.2.2: {}
 
-  otpauth@9.2.3:
+  otpauth@9.3.1:
     dependencies:
-      jssha: 3.3.1
+      '@noble/hashes': 1.4.0
 
   outvariant@1.4.2: {}
 
@@ -21396,7 +22342,7 @@ snapshots:
     dependencies:
       yocto-queue: 0.1.0
 
-  p-limit@4.0.0:
+  p-limit@5.0.0:
     dependencies:
       yocto-queue: 1.0.0
 
@@ -21427,7 +22373,7 @@ snapshots:
 
   p-try@2.2.0: {}
 
-  pako@0.2.9: {}
+  package-json-from-dist@1.0.0: {}
 
   parent-module@1.0.1:
     dependencies:
@@ -21435,7 +22381,7 @@ 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
@@ -21444,6 +22390,8 @@ snapshots:
     dependencies:
       xtend: 4.0.2
 
+  parse-ms@4.0.0: {}
+
   parse-srcset@1.0.2: {}
 
   parse5-htmlparser2-tree-adapter@6.0.1:
@@ -21486,10 +22434,15 @@ snapshots:
       lru-cache: 10.2.2
       minipass: 7.0.4
 
-  path-scurry@1.10.2:
+  path-scurry@1.11.1:
     dependencies:
       lru-cache: 10.2.2
-      minipass: 7.0.4
+      minipass: 7.1.2
+
+  path-scurry@2.0.0:
+    dependencies:
+      lru-cache: 11.0.0
+      minipass: 7.1.2
 
   path-to-regexp@0.1.7: {}
 
@@ -21503,6 +22456,8 @@ snapshots:
 
   path-type@4.0.0: {}
 
+  path-type@5.0.0: {}
+
   pathe@1.1.2: {}
 
   pathval@1.1.1: {}
@@ -21513,11 +22468,7 @@ snapshots:
 
   peek-readable@5.0.0: {}
 
-  peek-stream@1.1.3:
-    dependencies:
-      buffer-from: 1.1.2
-      duplexify: 3.7.1
-      through2: 2.0.5
+  peek-readable@5.1.3: {}
 
   pend@1.2.0: {}
 
@@ -21532,11 +22483,9 @@ snapshots:
 
   pg-numeric@1.0.2: {}
 
-  pg-pool@3.6.2(pg@8.11.5):
+  pg-pool@3.6.2(pg@8.12.0):
     dependencies:
-      pg: 8.11.5
-
-  pg-protocol@1.6.0: {}
+      pg: 8.12.0
 
   pg-protocol@1.6.1: {}
 
@@ -21558,10 +22507,10 @@ snapshots:
       postgres-interval: 3.0.0
       postgres-range: 1.1.3
 
-  pg@8.11.5:
+  pg@8.12.0:
     dependencies:
       pg-connection-string: 2.6.4
-      pg-pool: 3.6.2(pg@8.11.5)
+      pg-pool: 3.6.2(pg@8.12.0)
       pg-protocol: 1.6.1
       pg-types: 2.2.0
       pgpass: 1.0.5
@@ -21572,10 +22521,12 @@ snapshots:
     dependencies:
       split2: 4.1.0
 
-  photoswipe@5.4.3: {}
+  photoswipe@5.4.4: {}
 
   picocolors@1.0.0: {}
 
+  picocolors@1.0.1: {}
+
   picomatch@2.3.1: {}
 
   pid-port@1.0.0:
@@ -21586,26 +22537,26 @@ snapshots:
 
   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: {}
 
@@ -21647,140 +22598,140 @@ snapshots:
     dependencies:
       '@babel/runtime': 7.23.4
 
-  postcss-calc@9.0.1(postcss@8.4.38):
+  postcss-calc@9.0.1(postcss@8.4.40):
     dependencies:
-      postcss: 8.4.38
+      postcss: 8.4.40
       postcss-selector-parser: 6.0.15
       postcss-value-parser: 4.2.0
 
-  postcss-colormin@6.1.0(postcss@8.4.38):
+  postcss-colormin@6.1.0(postcss@8.4.40):
     dependencies:
       browserslist: 4.23.0
       caniuse-api: 3.0.0
       colord: 2.9.3
-      postcss: 8.4.38
+      postcss: 8.4.40
       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.40):
     dependencies:
       browserslist: 4.23.0
-      postcss: 8.4.38
+      postcss: 8.4.40
       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.40):
     dependencies:
-      postcss: 8.4.38
+      postcss: 8.4.40
 
-  postcss-discard-duplicates@6.0.3(postcss@8.4.38):
+  postcss-discard-duplicates@6.0.3(postcss@8.4.40):
     dependencies:
-      postcss: 8.4.38
+      postcss: 8.4.40
 
-  postcss-discard-empty@6.0.3(postcss@8.4.38):
+  postcss-discard-empty@6.0.3(postcss@8.4.40):
     dependencies:
-      postcss: 8.4.38
+      postcss: 8.4.40
 
-  postcss-discard-overridden@6.0.2(postcss@8.4.38):
+  postcss-discard-overridden@6.0.2(postcss@8.4.40):
     dependencies:
-      postcss: 8.4.38
+      postcss: 8.4.40
 
-  postcss-merge-longhand@6.0.5(postcss@8.4.38):
+  postcss-merge-longhand@6.0.5(postcss@8.4.40):
     dependencies:
-      postcss: 8.4.38
+      postcss: 8.4.40
       postcss-value-parser: 4.2.0
-      stylehacks: 6.1.1(postcss@8.4.38)
+      stylehacks: 6.1.1(postcss@8.4.40)
 
-  postcss-merge-rules@6.1.1(postcss@8.4.38):
+  postcss-merge-rules@6.1.1(postcss@8.4.40):
     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.40)
+      postcss: 8.4.40
       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.40):
     dependencies:
-      postcss: 8.4.38
+      postcss: 8.4.40
       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.40):
     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.40)
+      postcss: 8.4.40
       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.40):
     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.40)
+      postcss: 8.4.40
       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.40):
     dependencies:
-      postcss: 8.4.38
+      postcss: 8.4.40
       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.40):
     dependencies:
-      postcss: 8.4.38
+      postcss: 8.4.40
 
-  postcss-normalize-display-values@6.0.2(postcss@8.4.38):
+  postcss-normalize-display-values@6.0.2(postcss@8.4.40):
     dependencies:
-      postcss: 8.4.38
+      postcss: 8.4.40
       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.40):
     dependencies:
-      postcss: 8.4.38
+      postcss: 8.4.40
       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.40):
     dependencies:
-      postcss: 8.4.38
+      postcss: 8.4.40
       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.40):
     dependencies:
-      postcss: 8.4.38
+      postcss: 8.4.40
       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.40):
     dependencies:
-      postcss: 8.4.38
+      postcss: 8.4.40
       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.40):
     dependencies:
       browserslist: 4.23.0
-      postcss: 8.4.38
+      postcss: 8.4.40
       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.40):
     dependencies:
-      postcss: 8.4.38
+      postcss: 8.4.40
       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.40):
     dependencies:
-      postcss: 8.4.38
+      postcss: 8.4.40
       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.40):
     dependencies:
-      cssnano-utils: 4.0.2(postcss@8.4.38)
-      postcss: 8.4.38
+      cssnano-utils: 4.0.2(postcss@8.4.40)
+      postcss: 8.4.40
       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.40):
     dependencies:
       browserslist: 4.23.0
       caniuse-api: 3.0.0
-      postcss: 8.4.38
+      postcss: 8.4.40
 
-  postcss-reduce-transforms@6.0.2(postcss@8.4.38):
+  postcss-reduce-transforms@6.0.2(postcss@8.4.40):
     dependencies:
-      postcss: 8.4.38
+      postcss: 8.4.40
       postcss-value-parser: 4.2.0
 
   postcss-selector-parser@6.0.15:
@@ -21793,15 +22744,15 @@ snapshots:
       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.40):
     dependencies:
-      postcss: 8.4.38
+      postcss: 8.4.40
       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.40):
     dependencies:
-      postcss: 8.4.38
+      postcss: 8.4.40
       postcss-selector-parser: 6.0.16
 
   postcss-value-parser@4.2.0: {}
@@ -21812,6 +22763,12 @@ snapshots:
       picocolors: 1.0.0
       source-map-js: 1.2.0
 
+  postcss@8.4.40:
+    dependencies:
+      nanoid: 3.3.7
+      picocolors: 1.0.1
+      source-map-js: 1.2.0
+
   postgres-array@2.0.0: {}
 
   postgres-array@3.0.2: {}
@@ -21836,7 +22793,7 @@ snapshots:
 
   prelude-ls@1.2.1: {}
 
-  prettier@3.2.5: {}
+  prettier@3.3.3: {}
 
   pretty-bytes@5.6.0: {}
 
@@ -21854,6 +22811,10 @@ snapshots:
 
   pretty-hrtime@1.0.3: {}
 
+  pretty-ms@9.0.0:
+    dependencies:
+      parse-ms: 4.0.0
+
   private-ip@2.3.3:
     dependencies:
       ip-regex: 4.3.0
@@ -21936,19 +22897,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
@@ -21986,9 +22949,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
@@ -21997,29 +22960,18 @@ 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.6.3
 
   pvutils@1.1.3: {}
 
@@ -22066,13 +23018,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
@@ -22084,11 +23029,11 @@ snapshots:
     dependencies:
       setimmediate: 1.0.5
 
-  re2@1.20.10:
+  re2@1.21.3:
     dependencies:
       install-artifact-from-github: 1.3.5
-      nan: 2.18.0
-      node-gyp: 10.0.1
+      nan: 2.20.0
+      node-gyp: 10.1.0
     transitivePeerDependencies:
       - supports-color
 
@@ -22097,15 +23042,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.5.4):
     dependencies:
-      typescript: 5.4.5
+      typescript: 5.5.4
 
   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.24.7
       '@types/babel__core': 7.20.0
       '@types/babel__traverse': 7.20.0
       '@types/doctrine': 0.0.9
@@ -22192,21 +23137,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.6.3
 
   reconnecting-websocket@4.4.0: {}
 
@@ -22319,7 +23256,7 @@ snapshots:
 
   require-in-the-middle@7.3.0:
     dependencies:
-      debug: 4.3.4(supports-color@8.1.1)
+      debug: 4.3.5(supports-color@8.1.1)
       module-details-from-path: 1.0.3
       resolve: 1.22.8
     transitivePeerDependencies:
@@ -22343,11 +23280,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
@@ -22383,30 +23315,32 @@ snapshots:
     dependencies:
       glob: 7.2.3
 
-  rollup@4.17.2:
+  rollup@4.19.1:
     dependencies:
       '@types/estree': 1.0.5
     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.19.1
+      '@rollup/rollup-android-arm64': 4.19.1
+      '@rollup/rollup-darwin-arm64': 4.19.1
+      '@rollup/rollup-darwin-x64': 4.19.1
+      '@rollup/rollup-linux-arm-gnueabihf': 4.19.1
+      '@rollup/rollup-linux-arm-musleabihf': 4.19.1
+      '@rollup/rollup-linux-arm64-gnu': 4.19.1
+      '@rollup/rollup-linux-arm64-musl': 4.19.1
+      '@rollup/rollup-linux-powerpc64le-gnu': 4.19.1
+      '@rollup/rollup-linux-riscv64-gnu': 4.19.1
+      '@rollup/rollup-linux-s390x-gnu': 4.19.1
+      '@rollup/rollup-linux-x64-gnu': 4.19.1
+      '@rollup/rollup-linux-x64-musl': 4.19.1
+      '@rollup/rollup-win32-arm64-msvc': 4.19.1
+      '@rollup/rollup-win32-ia32-msvc': 4.19.1
+      '@rollup/rollup-win32-x64-msvc': 4.19.1
       fsevents: 2.3.3
 
   rrweb-cssom@0.6.0: {}
 
+  rrweb-cssom@0.7.1: {}
+
   rss-parser@3.13.0:
     dependencies:
       entities: 2.2.0
@@ -22454,7 +23388,7 @@ snapshots:
       parse-srcset: 1.0.2
       postcss: 8.4.38
 
-  sass@1.76.0:
+  sass@1.77.8:
     dependencies:
       chokidar: 3.5.3
       immutable: 4.2.2
@@ -22536,14 +23470,14 @@ snapshots:
     dependencies:
       kind-of: 6.0.3
 
-  sharp@0.33.3:
+  sharp@0.33.4:
     dependencies:
       color: 4.2.3
       detect-libc: 2.0.3
       semver: 7.6.0
     optionalDependencies:
-      '@img/sharp-darwin-arm64': 0.33.3
-      '@img/sharp-darwin-x64': 0.33.3
+      '@img/sharp-darwin-arm64': 0.33.4
+      '@img/sharp-darwin-x64': 0.33.4
       '@img/sharp-libvips-darwin-arm64': 1.0.2
       '@img/sharp-libvips-darwin-x64': 1.0.2
       '@img/sharp-libvips-linux-arm': 1.0.2
@@ -22552,15 +23486,15 @@ snapshots:
       '@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-linux-arm': 0.33.4
+      '@img/sharp-linux-arm64': 0.33.4
+      '@img/sharp-linux-s390x': 0.33.4
+      '@img/sharp-linux-x64': 0.33.4
+      '@img/sharp-linuxmusl-arm64': 0.33.4
+      '@img/sharp-linuxmusl-x64': 0.33.4
+      '@img/sharp-wasm32': 0.33.4
+      '@img/sharp-win32-ia32': 0.33.4
+      '@img/sharp-win32-x64': 0.33.4
 
   shebang-command@1.2.0:
     dependencies:
@@ -22581,9 +23515,10 @@ snapshots:
       vscode-oniguruma: 1.7.0
       vscode-textmate: 8.0.0
 
-  shiki@1.4.0:
+  shiki@1.12.0:
     dependencies:
-      '@shikijs/core': 1.4.0
+      '@shikijs/core': 1.12.0
+      '@types/hast': 3.0.4
 
   shimmer@1.2.1: {}
 
@@ -22599,11 +23534,11 @@ snapshots:
 
   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@8.1.1)
       joi: 17.11.0
     transitivePeerDependencies:
       - supports-color
@@ -22684,6 +23619,8 @@ snapshots:
 
   slash@3.0.0: {}
 
+  slash@5.1.0: {}
+
   slice-ansi@3.0.0:
     dependencies:
       ansi-styles: 4.3.0
@@ -22701,7 +23638,7 @@ snapshots:
   socks-proxy-agent@8.0.2:
     dependencies:
       agent-base: 7.1.0
-      debug: 4.3.4(supports-color@8.1.1)
+      debug: 4.3.5(supports-color@8.1.1)
       socks: 2.7.1
     transitivePeerDependencies:
       - supports-color
@@ -22711,7 +23648,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
 
@@ -22725,8 +23662,6 @@ snapshots:
 
   sortablejs@1.14.0: {}
 
-  source-map-js@1.0.2: {}
-
   source-map-js@1.2.0: {}
 
   source-map-support@0.5.13:
@@ -22767,7 +23702,7 @@ snapshots:
 
   sprintf-js@1.0.3: {}
 
-  sprintf-js@1.1.2: {}
+  sprintf-js@1.1.3: {}
 
   sshpk@1.17.0:
     dependencies:
@@ -22793,16 +23728,16 @@ snapshots:
 
   standard-as-callback@2.1.0: {}
 
-  start-server-and-test@2.0.3:
+  start-server-and-test@2.0.4:
     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.5(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: 7.2.0(debug@4.3.5)
     transitivePeerDependencies:
       - supports-color
 
@@ -22816,28 +23751,52 @@ snapshots:
 
   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(3rvqj7p7l43ansgshs3zbslm7u):
     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.2.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4))
+      '@storybook/components': 8.2.6(storybook@8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4))
+      '@storybook/core-events': 8.2.6(storybook@8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4))
+      '@storybook/manager-api': 8.2.6(storybook@8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4))
+      '@storybook/preview-api': 8.2.6(storybook@8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4))
+      '@storybook/theming': 8.2.6(storybook@8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4))
+      '@storybook/types': 8.2.6(storybook@8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(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.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(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)
+      '@babel/core': 7.24.7
+      '@babel/types': 7.24.7
+      '@storybook/codemod': 8.2.6(bufferutil@4.0.8)(utf-8-validate@6.0.4)
+      '@storybook/core': 8.2.6(bufferutil@4.0.8)(utf-8-validate@6.0.4)
+      '@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
+      fd-package-json: 1.2.0
+      find-up: 5.0.0
+      fs-extra: 11.1.1
+      giget: 1.1.2
+      globby: 14.0.1
+      jscodeshift: 0.15.1(@babel/preset-env@7.24.7(@babel/core@7.24.7))
+      leven: 3.1.0
+      ora: 5.4.1
+      prettier: 3.3.3
+      prompts: 2.4.2
+      semver: 7.6.0
+      strip-json-comments: 3.1.1
+      tempy: 3.1.0
+      tiny-invariant: 1.3.3
+      ts-dedent: 2.2.0
     transitivePeerDependencies:
       - '@babel/preset-env'
       - bufferutil
-      - encoding
-      - react
-      - react-dom
       - supports-color
       - utf-8-validate
 
@@ -22856,8 +23815,6 @@ snapshots:
     transitivePeerDependencies:
       - supports-color
 
-  stream-shift@1.0.1: {}
-
   stream-wormhole@1.1.0: {}
 
   streamsearch@1.1.0: {}
@@ -22938,6 +23895,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
@@ -22948,9 +23907,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: {}
 
@@ -22961,10 +23920,15 @@ snapshots:
       '@tokenizer/token': 0.3.0
       peek-readable: 5.0.0
 
-  stylehacks@6.1.1(postcss@8.4.38):
+  strtok3@8.0.1:
+    dependencies:
+      '@tokenizer/token': 0.3.0
+      peek-readable: 5.1.3
+
+  stylehacks@6.1.1(postcss@8.4.40):
     dependencies:
       browserslist: 4.23.0
-      postcss: 8.4.38
+      postcss: 8.4.40
       postcss-selector-parser: 6.0.16
 
   supports-color@5.5.0:
@@ -23000,22 +23964,7 @@ snapshots:
 
   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.22.11: {}
 
   tar-stream@3.1.6:
     dependencies:
@@ -23040,24 +23989,23 @@ snapshots:
     dependencies:
       memoizerific: 1.11.3
 
-  temp-dir@2.0.0: {}
+  temp-dir@3.0.0: {}
 
   temp@0.8.4:
     dependencies:
       rimraf: 2.6.3
 
-  tempy@1.0.1:
+  tempy@3.1.0:
     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
+      is-stream: 3.0.0
+      temp-dir: 3.0.0
+      type-fest: 2.19.0
+      unique-string: 3.0.0
 
-  terser@5.30.3:
+  terser@5.31.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
 
@@ -23081,25 +24029,18 @@ 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.167.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.8: {}
 
-  tiny-invariant@1.3.1: {}
-
   tiny-invariant@1.3.3: {}
 
   tiny-lru@10.0.1: {}
@@ -23108,7 +24049,7 @@ snapshots:
 
   tinycolor2@1.6.0: {}
 
-  tinypool@0.7.0: {}
+  tinypool@0.8.4: {}
 
   tinyspy@2.2.0: {}
 
@@ -23124,12 +24065,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: {}
@@ -23139,11 +24076,16 @@ 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
 
-  tough-cookie@4.1.3:
+  tough-cookie@4.1.4:
     dependencies:
       psl: 1.9.0
       punycode: 2.3.1
@@ -23174,19 +24116,19 @@ 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-dedent@2.2.0: {}
 
-  ts-jest@29.1.2(@babel/core@7.23.5)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.5))(esbuild@0.20.2)(jest@29.7.0(@types/node@20.12.7))(typescript@5.1.6):
+  ts-jest@29.1.2(@babel/core@7.24.7)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.24.7))(esbuild@0.23.0)(jest@29.7.0(@types/node@20.14.12))(typescript@5.1.6):
     dependencies:
       bs-logger: 0.2.6
       fast-json-stable-stringify: 2.1.0
-      jest: 29.7.0(@types/node@20.12.7)
+      jest: 29.7.0(@types/node@20.14.12)
       jest-util: 29.7.0
       json5: 2.2.3
       lodash.memoize: 4.1.2
@@ -23195,14 +24137,14 @@ snapshots:
       typescript: 5.1.6
       yargs-parser: 21.1.1
     optionalDependencies:
-      '@babel/core': 7.23.5
+      '@babel/core': 7.24.7
       '@jest/types': 29.6.3
-      babel-jest: 29.7.0(@babel/core@7.23.5)
-      esbuild: 0.20.2
+      babel-jest: 29.7.0(@babel/core@7.24.7)
+      esbuild: 0.23.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
@@ -23224,9 +24166,9 @@ snapshots:
       minimist: 1.2.8
       strip-bom: 3.0.0
 
-  tsd@0.30.7:
+  tsd@0.31.1:
     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
@@ -23238,6 +24180,8 @@ snapshots:
 
   tslib@2.6.2: {}
 
+  tslib@2.6.3: {}
+
   tsx@4.4.0:
     dependencies:
       esbuild: 0.18.20
@@ -23257,8 +24201,6 @@ snapshots:
 
   type-detect@4.0.8: {}
 
-  type-fest@0.16.0: {}
-
   type-fest@0.18.1: {}
 
   type-fest@0.20.2: {}
@@ -23269,9 +24211,11 @@ snapshots:
 
   type-fest@0.8.1: {}
 
+  type-fest@1.4.0: {}
+
   type-fest@2.19.0: {}
 
-  type-fest@4.9.0: {}
+  type-fest@4.20.1: {}
 
   type-is@1.6.18:
     dependencies:
@@ -23315,7 +24259,7 @@ snapshots:
       shiki: 0.14.7
       typescript: 5.1.6
 
-  typeorm@0.3.20(ioredis@5.4.1)(pg@8.11.5):
+  typeorm@0.3.20(ioredis@5.4.1)(pg@8.12.0):
     dependencies:
       '@sqltools/formatter': 1.2.5
       app-root-path: 3.1.0
@@ -23323,7 +24267,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
@@ -23334,7 +24278,7 @@ snapshots:
       yargs: 17.7.2
     optionalDependencies:
       ioredis: 5.4.1
-      pg: 8.11.5
+      pg: 8.12.0
     transitivePeerDependencies:
       - supports-color
 
@@ -23344,7 +24288,7 @@ snapshots:
 
   typescript@5.4.2: {}
 
-  typescript@5.4.5: {}
+  typescript@5.5.4: {}
 
   ufo@1.3.2: {}
 
@@ -23357,6 +24301,8 @@ snapshots:
     dependencies:
       '@lukeed/csprng': 1.0.1
 
+  uint8array-extras@1.4.0: {}
+
   ulid@2.3.0: {}
 
   unbox-primitive@1.0.2:
@@ -23385,6 +24331,8 @@ snapshots:
 
   unicode-property-aliases-ecmascript@2.1.0: {}
 
+  unicorn-magic@0.1.0: {}
+
   unified@11.0.4:
     dependencies:
       '@types/unist': 3.0.2
@@ -23403,9 +24351,9 @@ snapshots:
     dependencies:
       imurmurhash: 0.1.4
 
-  unique-string@2.0.0:
+  unique-string@3.0.0:
     dependencies:
-      crypto-random-string: 2.0.0
+      crypto-random-string: 4.0.0
 
   unist-util-is@6.0.0:
     dependencies:
@@ -23438,7 +24386,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
@@ -23471,6 +24419,11 @@ snapshots:
       node-gyp-build: 4.6.0
     optional: true
 
+  utf-8-validate@6.0.4:
+    dependencies:
+      node-gyp-build: 4.8.1
+    optional: true
+
   util-deprecate@1.0.2: {}
 
   util@0.12.5:
@@ -23483,18 +24436,20 @@ snapshots:
 
   utils-merge@1.0.1: {}
 
+  uuid@10.0.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.12.0(vue@3.4.37(typescript@5.5.4)):
     dependencies:
       diff: 5.1.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))
+      vue: 3.4.37(typescript@5.5.4)
+      vue-demi: 0.14.7(vue@3.4.37(typescript@5.5.4))
+      vue-i18n: 9.13.1(vue@3.4.37(typescript@5.5.4))
 
   v8-to-istanbul@9.2.0:
     dependencies:
@@ -23507,8 +24462,6 @@ snapshots:
       spdx-correct: 3.1.1
       spdx-expression-parse: 3.0.1
 
-  validator@13.9.0: {}
-
   vary@1.1.2: {}
 
   verror@1.10.0:
@@ -23528,14 +24481,13 @@ 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.77.8)(terser@5.31.3):
     dependencies:
       cac: 6.7.14
-      debug: 4.3.4(supports-color@8.1.1)
-      mlly: 1.5.0
+      debug: 4.3.5(supports-color@8.1.1)
       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)
+      vite: 5.3.5(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3)
     transitivePeerDependencies:
       - '@types/node'
       - less
@@ -23548,53 +24500,50 @@ 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.3.5(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3):
     dependencies:
-      esbuild: 0.20.2
-      postcss: 8.4.38
-      rollup: 4.17.2
+      esbuild: 0.21.5
+      postcss: 8.4.40
+      rollup: 4.19.1
     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.77.8
+      terser: 5.31.3
 
-  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)):
+  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.77.8)(terser@5.31.3)):
     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.77.8)(terser@5.31.3)
     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.77.8)(terser@5.31.3):
     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
+      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.3.5(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3)
+      vite-node: 1.6.0(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3)
       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
@@ -23631,98 +24580,102 @@ snapshots:
 
   vscode-textmate@8.0.0: {}
 
-  vue-component-meta@2.0.16(typescript@5.4.5):
+  vscode-uri@3.0.8: {}
+
+  vue-component-meta@2.0.16(typescript@5.5.4):
     dependencies:
       '@volar/typescript': 2.2.0
-      '@vue/language-core': 2.0.16(typescript@5.4.5)
+      '@vue/language-core': 2.0.16(typescript@5.5.4)
       path-browserify: 1.0.1
       vue-component-type-helpers: 2.0.16
     optionalDependencies:
-      typescript: 5.4.5
+      typescript: 5.5.4
 
   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.0.29: {}
 
-  vue-demi@0.14.7(vue@3.4.26(typescript@5.4.5)):
-    dependencies:
-      vue: 3.4.26(typescript@5.4.5)
+  vue-component-type-helpers@2.1.2: {}
 
-  vue-docgen-api@4.75.1(vue@3.4.26(typescript@5.4.5)):
+  vue-demi@0.14.7(vue@3.4.37(typescript@5.5.4)):
     dependencies:
-      '@babel/parser': 7.24.0
-      '@babel/types': 7.24.0
-      '@vue/compiler-dom': 3.4.21
-      '@vue/compiler-sfc': 3.4.26
+      vue: 3.4.37(typescript@5.5.4)
+
+  vue-docgen-api@4.75.1(vue@3.4.37(typescript@5.5.4)):
+    dependencies:
+      '@babel/parser': 7.24.7
+      '@babel/types': 7.24.7
+      '@vue/compiler-dom': 3.4.34
+      '@vue/compiler-sfc': 3.4.37
       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.4.37(typescript@5.5.4)
+      vue-inbrowser-compiler-independent-utils: 4.71.1(vue@3.4.37(typescript@5.5.4))
 
-  vue-eslint-parser@9.4.2(eslint@8.57.0):
+  vue-eslint-parser@9.4.3(eslint@9.8.0):
     dependencies:
-      debug: 4.3.4(supports-color@8.1.1)
-      eslint: 8.57.0
+      debug: 4.3.4(supports-color@5.5.0)
+      eslint: 9.8.0
       eslint-scope: 7.2.2
       eslint-visitor-keys: 3.4.3
       espree: 9.6.1
       esquery: 1.4.2
       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-i18n@9.13.1(vue@3.4.37(typescript@5.5.4)):
     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: 3.4.37(typescript@5.5.4)
 
-  vue-inbrowser-compiler-independent-utils@4.71.1(vue@3.4.26(typescript@5.4.5)):
+  vue-inbrowser-compiler-independent-utils@4.71.1(vue@3.4.37(typescript@5.5.4)):
     dependencies:
-      vue: 3.4.26(typescript@5.4.5)
+      vue: 3.4.37(typescript@5.5.4)
 
   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.0.29(typescript@5.5.4):
     dependencies:
-      '@volar/typescript': 2.2.0
-      '@vue/language-core': 2.0.16(typescript@5.4.5)
+      '@volar/typescript': 2.4.0-alpha.18
+      '@vue/language-core': 2.0.29(typescript@5.5.4)
       semver: 7.6.0
-      typescript: 5.4.5
+      typescript: 5.5.4
 
-  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)):
+  vuedraggable@4.1.0(vue@3.4.37(typescript@5.5.4)):
     dependencies:
       sortablejs: 1.14.0
-      vue: 3.4.26(typescript@5.4.5)
+      vue: 3.4.37(typescript@5.5.4)
 
   w3c-xmlserializer@5.0.0:
     dependencies:
       xml-name-validator: 5.0.0
 
-  wait-on@7.2.0(debug@4.3.4):
+  wait-on@7.2.0(debug@4.3.5):
     dependencies:
-      axios: 1.6.2(debug@4.3.4)
+      axios: 1.6.2(debug@4.3.5)
       joi: 17.11.0
       lodash: 4.17.21
       minimist: 1.2.8
@@ -23730,6 +24683,8 @@ snapshots:
     transitivePeerDependencies:
       - debug
 
+  walk-up-path@3.0.1: {}
+
   walker@1.0.8:
     dependencies:
       makeerror: 1.0.12
@@ -23755,6 +24710,9 @@ snapshots:
 
   web-streams-polyfill@3.2.1: {}
 
+  web-streams-polyfill@4.0.0:
+    optional: true
+
   webidl-conversions@3.0.1: {}
 
   webidl-conversions@7.0.0: {}
@@ -23829,11 +24787,13 @@ snapshots:
 
   with@7.0.2:
     dependencies:
-      '@babel/parser': 7.24.5
-      '@babel/types': 7.24.0
+      '@babel/parser': 7.24.7
+      '@babel/types': 7.24.7
       assert-never: 1.2.1
       babel-walk: 3.0.0-canary-5
 
+  word-wrap@1.2.5: {}
+
   wordwrap@1.0.0: {}
 
   wrap-ansi@6.2.0:
@@ -23867,15 +24827,20 @@ snapshots:
       imurmurhash: 0.1.4
       signal-exit: 3.0.7
 
-  ws@8.14.2(bufferutil@4.0.7)(utf-8-validate@6.0.3):
+  ws@8.14.2(bufferutil@4.0.8)(utf-8-validate@6.0.4):
+    optionalDependencies:
+      bufferutil: 4.0.8
+      utf-8-validate: 6.0.4
+
+  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.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):
     optionalDependencies:
-      bufferutil: 4.0.7
-      utf-8-validate: 6.0.3
+      bufferutil: 4.0.8
+      utf-8-validate: 6.0.4
 
   xev@3.0.2: {}
 
@@ -23960,13 +24925,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/scripts/build-assets.mjs b/scripts/build-assets.mjs
index c3c38cd9a6..fcf29cef22 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));
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/tarball.mjs b/scripts/tarball.mjs
index 707cd3e7e5..93cd78a06a 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 = [