diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0a5e566263..6cda5537dc 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -11,19 +11,34 @@ You should also include the user name that made the change.
 
 ## 13.0.0 (unreleased)
 
+### TL;DR
+- New features (Play, new widgets, new charts, etc)
+- Rewriten backend
+- Better performance (backend and frontend)
+- Various usability improvements
+- Various UI tweaks
+
 ### Changes
+#### For server admins
 - Node.js 18.x or later is required
+- PostgreSQL 15.x is required
+	- Misskey not using 15 specific features at 13.0.0, but may do so in the future.
 - Elasticsearchのサポートが削除されました
 	- 代わりに今後任意の検索プロバイダを設定できる仕組みを構想しています。その仕組みを使えば今まで通りElasticsearchも利用できます
-- ノートのウォッチ機能が削除されました
 - Migrate to Yarn Berry (v3.2.1) @ThatOneCalculator
 	- You may have to `yarn run clean-all`, `sudo corepack enable` and `yarn set version berry` before running `yarn install` if you're still on yarn classic
+
+#### For users
+- ノートのウォッチ機能が削除されました
 - 新たに動的なPagesを作ることはできなくなりました
-	- 代わりに今後AiScriptを用いてより柔軟に動的なコンテンツを作成できるMisskey Play機能の実装を予定しています。
-- AiScriptが0.12.0にアップデートされました
-	- 0.12.0の変更点についてはこちら https://github.com/syuilo/aiscript/blob/master/CHANGELOG.md#0120
-	- 0.12.0未満のプラグインは読み込むことはできません
+	- 代わりにAiScriptを用いてより柔軟に動的なコンテンツを作成できるMisskey Play機能が実装されています。
+- AiScriptが0.12.1にアップデートされました
+	- 0.12.xの変更点についてはこちら https://github.com/syuilo/aiscript/blob/master/CHANGELOG.md#0120
+	- 0.12.1未満のプラグインは読み込むことはできません
 - iOS15以下のデバイスはサポートされなくなりました
+- Firefox109以下はサポートされなくなりました
+
+#### For app developers
 - API: カスタム絵文字エンティティに`url`プロパティが含まれなくなりました
 	- 絵文字画像を表示するには、`<instance host>/emoji/<emoji name>.webp`にリクエストすると画像が返ります。
 	- e.g. `https://p1.a9z.dev/emoji/misskey.webp`
@@ -33,12 +48,13 @@ You should also include the user name that made the change.
 - API: `instance`エンティティに`latestStatus`、`lastCommunicatedAt`、`latestRequestSentAt`プロパティが含まれなくなりました
 
 ### Improvements
-- Push notification of Antenna note @tamaina
-- AVIF support @tamaina
-- Add Cloudflare Turnstile CAPTCHA support @CyberRex0
+- Misskey Play @syuilo
 - Introduce retention-rate aggregation @syuilo
 - Make possible to export favorited notes @syuilo
 - Add per user pv chart @syuilo
+- Push notification of Antenna note @tamaina
+- AVIF support @tamaina
+- Add Cloudflare Turnstile CAPTCHA support @CyberRex0
 - Server: signToActivityPubGet is set to true by default @syuilo
 - Server: improve syslog performance @syuilo
 - Server: improve note scoring for featured notes @CyberRex0
@@ -47,6 +63,7 @@ You should also include the user name that made the change.
 - Server: delete outdated notes of antenna regularly to improve db performance @syuilo
 - Server: improve activitypub deliver performance @syuilo
 - Client: use tabler-icons instead of fontawesome to better design @syuilo
+- Client: Add AiScript App widget
 - Client: Add new gabber kick sounds (thanks for noizenecio)
 - Client: Add link to user RSS feed in profile menu @ssmucny
 - Client: Compress non-animated PNG files @saschanaz
@@ -57,11 +74,13 @@ You should also include the user name that made the change.
 - Client: Make widgets of universal/classic sync between devices @tamaina
 - Client: Implement the button to subscribe push notification @tamaina
 - Client: Implement the toggle to or not to close push notifications when notifications or messages are read @tamaina
+- Client: Improve RSS widget @tamaina
 - Client: show Unicode emoji tooltip with its name in MkReactionsViewer.reaction @saschanaz
 - Client: OpenSearch support @SoniEx2 @chaoticryptidz
 - Client: add user list widget @syuilo
 - Client: add heatmap of daily active users to about page @syuilo
 - Client: introduce fluent emoji @syuilo
+- Client: show fireworks when visit user who today is birthday @syuilo
 - Client: show bot warning on screen when logged in as bot account @syuilo
 - Client: improve overall performance of client @syuilo
 - Client: ui tweaks @syuilo
@@ -73,6 +92,8 @@ You should also include the user name that made the change.
 - Server: trim long text of note from ap @syuilo
 - Server: Ap inboxの最大ペイロードサイズを64kbに制限 @syuilo
 - Server: アンテナの作成数上限を追加 @syuilo
+- Server: pages/likeのエラーIDが重複しているのを修正 @syuilo
+- Server: pages/updateのパラメータによってはsummaryの値が更新されないのを修正 @syuilo
 - Client: case insensitive emoji search @saschanaz
 - Client: InAppウィンドウが操作できなくなることがあるのを修正 @tamaina
 - Client: use proxied image for instance icon @syuilo
diff --git a/locales/ar-SA.yml b/locales/ar-SA.yml
index eefb41007b..161a393bc2 100644
--- a/locales/ar-SA.yml
+++ b/locales/ar-SA.yml
@@ -164,7 +164,6 @@ annotation: "التعليقات"
 federation: "الفديرالية"
 instances: "مثيل الخادم"
 registeredAt: "مسجل منذ"
-latestRequestSentAt: "آخر طلب أرسِل في"
 latestRequestReceivedAt: "آخر طلب تُلقي في"
 latestStatus: "الحالات الأخيرة"
 storageUsage: "مساحة التخزين المستخدمة"
@@ -381,6 +380,7 @@ administrator: "المدير"
 token: "الرمز المميز"
 twoStepAuthentication: "الإستيثاق بعاملَيْن"
 moderator: "مشرِف"
+moderation: "الإشراف"
 nUsersMentioned: "{n} مستخدمين أُشير إليهم"
 securityKey: "مفتاح الأمان"
 securityKeyName: "اسم المفتاح"
@@ -814,6 +814,9 @@ colored: "ملوّن"
 label: "التسمية"
 localOnly: "المحلي فقط"
 account: "الحسابات"
+cannotLoad: "تعذر التحميل"
+like: "أعجبني"
+show: "المظهر"
 _emailUnavailable:
   used: "هذا البريد الإلكتروني مستخدم"
   format: "صيغة البريد الإلكتروني غير صالحة"
@@ -1229,6 +1232,11 @@ _timelines:
   local: "المحلي"
   social: "الاجتماعي"
   global: "الشامل"
+_play:
+  viewSource: "اظهر المصدر"
+  featured: "الأكثر شعبية"
+  title: "العنوان"
+  summary: "الوصف"
 _pages:
   newPage: "أنشئ صفحة جديدة"
   editPage: "عدّل الصفحة"
@@ -1294,6 +1302,7 @@ _notification:
   yourFollowRequestAccepted: "قُبل طلب المتابعة"
   youWereInvitedToGroup: "دُعيت إلى فريقٍ"
   pollEnded: "ظهرت نتائج الاستطلاع"
+  unreadAntennaNote: "هوائي {name}"
   _types:
     all: "الكل"
     follow: "متابِعون جدد"
diff --git a/locales/bn-BD.yml b/locales/bn-BD.yml
index 85ec1d9935..593cbb1b32 100644
--- a/locales/bn-BD.yml
+++ b/locales/bn-BD.yml
@@ -164,7 +164,6 @@ annotation: "মন্তব্য"
 federation: "ফেডিভার্স"
 instances: "ইন্সট্যান্স"
 registeredAt: "যোগ দিয়েছেন"
-latestRequestSentAt: "শেষ রিকুয়েস্ট পাঠানো হয়েছে"
 latestRequestReceivedAt: "শেষ রিকুয়েস্ট গৃহীত হয়েছে"
 latestStatus: "সর্বশেষ অবস্থা"
 storageUsage: "স্টোরেজের ব্যাবহার"
@@ -852,6 +851,8 @@ colored: "রঙ্গিন"
 label: "লেবেল"
 localOnly: "শুধুমাত্র লোকাল"
 account: "অ্যাকাউন্টগুলি"
+like: "পছন্দ করা"
+show: "প্রদর্শন"
 _emailUnavailable:
   used: "এই ইমেইল ঠিকানাটি ইতোমধ্যে ব্যবহৃত হয়েছে"
   format: "এই ইমেল ঠিকানাটি সঠিকভাবে লিখা হয়নি"
@@ -1320,6 +1321,12 @@ _timelines:
   local: "স্থানীয়"
   social: "সামাজিক"
   global: "গ্লোবাল"
+_play:
+  viewSource: "উৎস দেখুন"
+  featured: "জনপ্রিয়"
+  title: "শিরোনাম"
+  script: "স্ক্রিপ্ট"
+  summary: "বর্ণনা"
 _pages:
   newPage: "নতুন পৃষ্ঠা বানান"
   editPage: "পৃষ্ঠাটি সম্পাদনা করুন"
diff --git a/locales/ca-ES.yml b/locales/ca-ES.yml
index 406fdff0b4..5127803ebc 100644
--- a/locales/ca-ES.yml
+++ b/locales/ca-ES.yml
@@ -165,7 +165,6 @@ annotation: "Comentaris"
 federation: "Federació"
 instances: "Servidors"
 registeredAt: "Registrat a"
-latestRequestSentAt: "Darrera petició enviada"
 latestRequestReceivedAt: "Última petició rebuda"
 latestStatus: "Últim estat"
 storageUsage: "Emmagatzematge utilitzat"
diff --git a/locales/cs-CZ.yml b/locales/cs-CZ.yml
index 552b56a430..2d80008c47 100644
--- a/locales/cs-CZ.yml
+++ b/locales/cs-CZ.yml
@@ -161,7 +161,6 @@ annotation: "Komentáře"
 federation: "Federace"
 instances: "Instance"
 registeredAt: "Registrován"
-latestRequestSentAt: "Poslední požadavek poslán"
 latestRequestReceivedAt: "Poslední požadavek přijat"
 latestStatus: "Poslední status"
 storageUsage: "Využití úložiště"
@@ -611,6 +610,7 @@ speed: "Rychlost"
 slow: "Pomalá"
 fast: "Rychlá"
 account: "Účty"
+show: "Zobrazit"
 _ad:
   back: "Zpět"
 _gallery:
@@ -749,6 +749,9 @@ _charts:
 _timelines:
   home: "Domů"
   global: "Globální"
+_play:
+  script: "Skript"
+  summary: "Popis"
 _pages:
   newPage: "Vytvořit novou stránku"
   editPage: "Upravit stránku"
diff --git a/locales/de-DE.yml b/locales/de-DE.yml
index dfac4fb791..db6bd9ab05 100644
--- a/locales/de-DE.yml
+++ b/locales/de-DE.yml
@@ -167,7 +167,6 @@ annotation: "Anmerkung"
 federation: "Föderation"
 instances: "Instanzen"
 registeredAt: "Registriert am"
-latestRequestSentAt: "Letzte Anfrage gesendet"
 latestRequestReceivedAt: "Letzte Anfrage erhalten"
 latestStatus: "Neuster Status"
 storageUsage: "Verbrauchter Speicherplatz"
@@ -610,7 +609,7 @@ regexpErrorDescription: "Im regulären Ausdruck deiner {tab}en Wortstummschaltun
 instanceMute: "Instanzstummschaltungen"
 userSaysSomething: "{name} hat etwas gesagt"
 makeActive: "Aktivieren"
-display: "Anzeigeart"
+display: "Anzeigen"
 copy: "Kopieren"
 metrics: "Metriken"
 overview: "Übersicht"
@@ -916,6 +915,11 @@ caption: "Beschreibung"
 loggedInAsBot: "Momentan als Bot angemeldet"
 tools: "Werkzeuge"
 cannotLoad: "Kann nicht geladen werden"
+numberOfProfileView: "Profilaufrufe"
+like: "Gefällt mir"
+unlike: "\"Gefällt mir\" entfernen"
+numberOfLikes: "\"Gefällt mir\"-Anzahl"
+show: "Anzeigen"
 _sensitiveMediaDetection:
   description: "Ermöglicht eine Erleichterung der Servermoderation durch die automatische Erkennungen von NSFW-Medien unter Verwendung von Machine Learning. Hierdurch wird die Serverlast etwas erhöht."
   sensitivity: "Erkennungssensitivität"
@@ -1315,6 +1319,7 @@ _widgets:
   jobQueue: "Job-Warteschlange"
   serverMetric: "Servermetriken"
   aiscript: "AiScript-Konsole"
+  aiscriptApp: "AiScript-Anwendung"
   aichan: "Ai"
   userList: "Benutzerliste"
   _userList:
@@ -1420,6 +1425,21 @@ _timelines:
   local: "Lokal"
   social: "Sozial"
   global: "Global"
+_play:
+  new: "Play erstellen"
+  edit: "Play bearbeiten"
+  created: "Play erfolgreich erstellt"
+  updated: "Play erfolgreich aktualisiert"
+  deleted: "Play erfolgreich gelöscht"
+  pageSetting: "Play-Einstellungen"
+  editThisPage: "Dieses Play bearbeiten"
+  viewSource: "Quelltext anzeigen"
+  my: "Meine Plays"
+  liked: "Mit \"Gefällt mir\" markierte Plays"
+  featured: "Beliebt"
+  title: "Titel"
+  script: "Skript"
+  summary: "Beschreibung"
 _pages:
   newPage: "Seite erstellen"
   editPage: "Seite bearbeiten"
diff --git a/locales/en-US.yml b/locales/en-US.yml
index 414bc1df51..e2a7b32be8 100644
--- a/locales/en-US.yml
+++ b/locales/en-US.yml
@@ -167,7 +167,6 @@ annotation: "Comments"
 federation: "Federation"
 instances: "Instances"
 registeredAt: "Registered at"
-latestRequestSentAt: "Last request sent"
 latestRequestReceivedAt: "Last request received"
 latestStatus: "Latest status"
 storageUsage: "Storage usage"
@@ -916,6 +915,11 @@ caption: "Caption"
 loggedInAsBot: "Currently logged in as bot"
 tools: "Tools"
 cannotLoad: "Unable to load"
+numberOfProfileView: "Profile views"
+like: "Like"
+unlike: "Unlike"
+numberOfLikes: "Likes"
+show: "Show"
 _sensitiveMediaDetection:
   description: "Reduces the effort of server moderation through automatically recognizing NSFW media via Machine Learning. This will slightly increase the load on the server."
   sensitivity: "Detection sensitivity"
@@ -1315,6 +1319,7 @@ _widgets:
   jobQueue: "Job Queue"
   serverMetric: "Server metrics"
   aiscript: "AiScript console"
+  aiscriptApp: "AiScript App"
   aichan: "Ai"
   userList: "User list"
   _userList:
@@ -1420,6 +1425,21 @@ _timelines:
   local: "Local"
   social: "Social"
   global: "Global"
+_play:
+  new: "Create Play"
+  edit: "Edit Play"
+  created: "Play created"
+  updated: "Play edited"
+  deleted: "Play deleted"
+  pageSetting: "Play settings"
+  editThisPage: "Edit this Play"
+  viewSource: "View source"
+  my: "My Plays"
+  liked: "Liked Plays"
+  featured: "Popular"
+  title: "Title"
+  script: "Script"
+  summary: "Description"
 _pages:
   newPage: "Create a new Page"
   editPage: "Edit this Page"
diff --git a/locales/es-ES.yml b/locales/es-ES.yml
index 50db3fe306..c328737c4b 100644
--- a/locales/es-ES.yml
+++ b/locales/es-ES.yml
@@ -13,6 +13,7 @@ fetchingAsApObject: "Buscando en el fediverso"
 ok: "OK"
 gotIt: "¡Lo tengo!"
 cancel: "Cancelar"
+noThankYou: "No gracias"
 enterUsername: "Introduce el nombre de usuario"
 renotedBy: "Renotado por {user}"
 noNotes: "No hay notas"
@@ -48,6 +49,7 @@ deleteAndEdit: "Borrar y editar"
 deleteAndEditConfirm: "¿Estás seguro de que quieres borrar esta nota y editarla? Perderás todas las reacciones, renotas y respuestas."
 addToList: "Agregar a lista"
 sendMessage: "Enviar un mensaje"
+copyRSS: "Copiar RSS"
 copyUsername: "Copiar nombre de usuario"
 searchUser: "Buscar un usuario"
 reply: "Responder"
@@ -165,7 +167,6 @@ annotation: "Anotación"
 federation: "Federación"
 instances: "Instancia"
 registeredAt: "Registrado en"
-latestRequestSentAt: "Ultimo pedido enviado"
 latestRequestReceivedAt: "Ultimo pedido recibido"
 latestStatus: "Último status"
 storageUsage: "Almacenamiento usado"
@@ -455,6 +456,8 @@ language: "Idioma"
 uiLanguage: "Idioma de visualización de la interfaz"
 groupInvited: "Invitado al grupo"
 aboutX: "Acerca de {x}"
+emojiStyle: "Estilo de emoji"
+native: "Nativo"
 disableDrawer: "No mostrar los menús en cajones"
 youHaveNoGroups: "Sin grupos"
 joinOrCreateGroup: "Obtenga una invitación para unirse al grupos o puede crear su propio grupo."
@@ -713,6 +716,7 @@ accentColor: "Acento"
 textColor: "Texto"
 saveAs: "Guardar como…"
 advanced: "Avanzado"
+advancedSettings: "Configuración avanzada"
 value: "Valores"
 createdAt: "Fecha de creación"
 updatedAt: "Actualizado"
@@ -898,6 +902,22 @@ navbar: "Barra de navegación"
 shuffle: "Aleatorio"
 account: "Cuentas"
 move: "Mover"
+pushNotification: "Alerta emergente"
+subscribePushNotification: "Activar las notificaciones emergentes"
+unsubscribePushNotification: "Desactivar las notificaciones emergentes"
+pushNotificationAlreadySubscribed: "Notificaciones emergentes ya activadas"
+pushNotificationNotSupported: "El navegador o la instancia no admiten notificaciones push"
+sendPushNotificationReadMessage: "Eliminar las notificaciones push después de leer las notificaciones y los mensajes"
+sendPushNotificationReadMessageCaption: "La notificación \"{emptyPushNotificationMessage}\" aparecerá momentáneamente. Esto puede aumentar el consumo de batería del dispositivo."
+windowMaximize: "Maximizar"
+windowRestore: "Regresar"
+caption: "Pie de foto"
+loggedInAsBot: "Inicio sesión como cuenta bot."
+tools: "Utilidades"
+cannotLoad: "No se puede cargar."
+numberOfProfileView: "Número de vistas de perfil"
+like: "¡Muy bien!"
+show: "Apariencia"
 _sensitiveMediaDetection:
   description: "Reduce el esfuerzo de la moderación el el servidor a través del reconocimiento automático de contenido NSFW usando 'Machine Learning'. Esto puede incrementar ligeramente la carga en el servidor."
   sensitivity: "Sensibilidad de detección"
@@ -1208,6 +1228,9 @@ _tutorial:
   step7_1: "Así terminó la explicación del funcionamiento básico de Misskey. Eso fue todo."
   step7_2: "Si quieres conocer más sobre Misskey, prueba con la sección {help}."
   step7_3: "Así, disfruta de Misskey 🚀"
+  step8_1: "Por último, ¿por qué no activar las notificaciones emergentes?"
+  step8_2: "Al recibir notificaciones emergentes, estarás al tanto de reacciones, seguimientos y menciones incluso cuando Misskey no esté abierto."
+  step8_3: "La configuración de las notificaciones puede modificarse posteriormente."
 _2fa:
   alreadyRegistered: "Ya has completado la configuración."
   registerDevice: "Registrar dispositivo"
@@ -1295,6 +1318,7 @@ _widgets:
   serverMetric: "Estadísticas del servidor"
   aiscript: "Consola de AiScript"
   aichan: "indigo"
+  userList: "Lista de usuarios"
   _userList:
     chooseList: "Seleccione una lista"
 _cw:
@@ -1360,6 +1384,7 @@ _profile:
   changeBanner: "Cambiar banner"
 _exportOrImport:
   allNotes: "Todas las notas"
+  favoritedNotes: "Notas favoritas"
   followingList: "Siguiendo"
   muteList: "Silenciados"
   blockingList: "Bloqueados"
@@ -1397,6 +1422,12 @@ _timelines:
   local: "Local"
   social: "Social"
   global: "Global"
+_play:
+  viewSource: "Ver la fuente"
+  featured: "Popular"
+  title: "Título"
+  script: "Script"
+  summary: "Descripción"
 _pages:
   newPage: "Crear página"
   editPage: "Editar página"
@@ -1464,6 +1495,7 @@ _notification:
   yourFollowRequestAccepted: "Tu solicitud de seguimiento fue aceptada"
   youWereInvitedToGroup: "Invitado al grupo"
   pollEnded: "Estan disponibles los resultados de la encuesta"
+  unreadAntennaNote: "Antena {name}"
   emptyPushNotificationMessage: "Se han actualizado las notificaciones push"
   _types:
     all: "Todo"
diff --git a/locales/fr-FR.yml b/locales/fr-FR.yml
index 0df7ee2e12..0d7399533d 100644
--- a/locales/fr-FR.yml
+++ b/locales/fr-FR.yml
@@ -2,6 +2,7 @@
 _lang_: "Français"
 headlineMisskey: "Réseau relié par des notes"
 introMisskey: "Bienvenue ! Misskey est un service de microblogage décentralisé, libre et ouvert.\nÉcrivez des « notes » et partagez ce qui se passe à l’instant présent, autour de vous avec les autres 📡\nLa fonction « réactions », vous permet également d’ajouter une réaction rapide aux notes des autres utilisateur·rice·s 👍\nExplorons un nouveau monde 🚀"
+poweredByMisskeyDescription: "{nom} est l'un des services propulsés par la plateforme ouverte <b>Misskey</b> (appelée \"instance Misskey\")."
 monthAndDay: "{day}/{month}"
 search: "Rechercher"
 notifications: "Notifications"
@@ -12,6 +13,7 @@ fetchingAsApObject: "Récupération depuis le fédiverse …"
 ok: "OK"
 gotIt: "J’ai compris !"
 cancel: "Annuler"
+noThankYou: "Pas maintenant"
 enterUsername: "Entrer un nom d’utilisateur·rice"
 renotedBy: "Renoté par {user}"
 noNotes: "Aucune note"
@@ -47,6 +49,7 @@ deleteAndEdit: "Supprimer et réécrire"
 deleteAndEditConfirm: "Êtes-vous sûr·e de vouloir supprimer cette note et la reformuler ? Vous perdrez toutes les réactions, renotes et réponses y afférentes."
 addToList: "Ajouter à une liste"
 sendMessage: "Envoyer un message"
+copyRSS: "Copier le RSS"
 copyUsername: "Copier le nom d’utilisateur·rice"
 searchUser: "Chercher un·e utilisateur·rice"
 reply: "Répondre"
@@ -143,6 +146,7 @@ flagAsBotDescription: "Si ce compte est géré de manière automatisée, choisis
 flagAsCat: "Ce compte est un chat"
 flagAsCatDescription: "Activer l'option \" Je suis un chat \" pour ce compte."
 flagShowTimelineReplies: "Afficher les réponses dans le fil"
+flagShowTimelineRepliesDescription: "Affiche les réponses des utilisateurs aux notes des autres utilisateurs dans la timeline si cette option est activée."
 autoAcceptFollowed: "Accepter automatiquement les demandes d’abonnement venant d’utilisateur·rice·s que vous suivez"
 addAccount: "Ajouter un compte"
 loginFailed: "Échec de la connexion"
@@ -163,7 +167,6 @@ annotation: "Commentaires"
 federation: "Fédération"
 instances: "Instance"
 registeredAt: "Premier contact le"
-latestRequestSentAt: "Dernière requête envoyée"
 latestRequestReceivedAt: "Dernière requête reçue"
 latestStatus: "Dernier statut"
 storageUsage: "Stockage utilisé"
@@ -453,6 +456,8 @@ language: "Langue"
 uiLanguage: "Langue d’affichage de l’interface"
 groupInvited: "Invité au groupe"
 aboutX: "À propos de {x}"
+emojiStyle: "Style des émojis"
+native: "Natif"
 disableDrawer: "Les menus ne s'affichent pas dans le tiroir"
 youHaveNoGroups: "Vous n’avez aucun groupe"
 joinOrCreateGroup: "Vous pouvez être invité·e à rejoindre des groupes existants ou créer votre propre nouveau groupe."
@@ -600,6 +605,7 @@ smtpSecureInfo: "Désactiver cette option lorsque STARTTLS est utilisé"
 testEmail: "Tester la distribution de courriel"
 wordMute: "Filtre de mots"
 regexpError: "Erreur d’expression régulière"
+regexpErrorDescription: "Une erreur s'est produite dans l'expression régulière sur la ligne {ligne} de votre mot muet {tab} :"
 instanceMute: "Instance en sourdine"
 userSaysSomething: "{name} a dit quelque chose"
 makeActive: "Activer"
@@ -708,6 +714,7 @@ accentColor: "Accentuation"
 textColor: "Texte"
 saveAs: "Enregistrer sous ..."
 advanced: "Avancé"
+advancedSettings: "Paramètres avancés"
 value: "Valeur"
 createdAt: "Date de création"
 updatedAt: "Mis à jour le"
@@ -852,6 +859,7 @@ rateLimitExceeded: "Limite de taux dépassée"
 cropImage: "Recadrer l'image"
 cropImageAsk: "Voulez-vous recadrer cette image ?"
 file: "Fichiers"
+recentNHours: "Dernières {n} heures"
 noEmailServerWarning: "Serveur de courrier non configuré."
 thereIsUnresolvedAbuseReportWarning: "Il n’y a aucun rapport non résolu."
 recommended: "Recommandé"
@@ -891,6 +899,19 @@ navbar: "Barre de navigation"
 shuffle: "Lecture aléatoire"
 account: "Comptes"
 move: "Déplacer"
+pushNotification: "Notifications push"
+subscribePushNotification: "Autoriser les notifications push"
+unsubscribePushNotification: "Désactiver les notifications push"
+pushNotificationAlreadySubscribed: "Les notifications push sont déjà activées"
+pushNotificationNotSupported: "Votre navigateur ou votre instance ne prend pas en charge les notifications push"
+sendPushNotificationReadMessage: "Supprimer les notifications push une fois que les notifications ou messages pertinents ont été lus."
+windowRestore: "Restaurer"
+caption: "Libellé"
+loggedInAsBot: "Connecté actuellement en tant que bot"
+tools: "Outils"
+cannotLoad: "Chargement impossible"
+like: "J'aime"
+show: "Affichage"
 _sensitiveMediaDetection:
   description: "L'apprentissage automatique peut être utilisé pour détecter automatiquement les médias sensibles à modérer. La sollicitation des serveurs augmente légèrement."
   sensitivity: "Sensibilité de la détection"
@@ -1201,6 +1222,8 @@ _tutorial:
   step7_1: "Félicitations ! Vous avez atteint la fin du tutoriel de base pour l’utilisation de Misskey."
   step7_2: "Si vous désirez en savoir plus sur Misskey, jetez un œil sur la section {help}."
   step7_3: "Bon courage et amusez-vous bien sur Misskey ! 🚀"
+  step8_1: "Enfin, souhaitez-vous activer les notifications push ?"
+  step8_2: "En les activant, vous recevrez des notifications pour les mentions, les réactions, les suivis, etc., même lorsque Misskey n'est pas ouvert."
 _2fa:
   alreadyRegistered: "Configuration déjà achevée."
   registerDevice: "Ajouter un nouvel appareil"
@@ -1287,6 +1310,7 @@ _widgets:
   serverMetric: "Statistiques du serveur"
   aiscript: "Console AiScript"
   aichan: "Ai"
+  userList: "Liste utilisateur"
   _userList:
     chooseList: "Sélectionner une liste"
 _cw:
@@ -1389,6 +1413,12 @@ _timelines:
   local: "Local"
   social: "Social"
   global: "Global"
+_play:
+  viewSource: "Afficher la source"
+  featured: "Populaire"
+  title: "Titre"
+  script: "Script"
+  summary: "Description"
 _pages:
   newPage: "Créer une page"
   editPage: "Modifier une page"
@@ -1456,6 +1486,7 @@ _notification:
   yourFollowRequestAccepted: "Votre demande d’abonnement a été accepté"
   youWereInvitedToGroup: "Invité·e au groupe"
   pollEnded: "Les résultats du sondage sont disponibles"
+  unreadAntennaNote: "Antenne {name}"
   emptyPushNotificationMessage: "Les notifications push ont été mises à jour"
   _types:
     all: "Toutes"
@@ -1490,6 +1521,7 @@ _deck:
   newProfile: "Nouveau profil"
   deleteProfile: "Supprimer le profil"
   introduction: "Créez l’interface parfaite qui vous sied en arrangeant librement les colonnes !"
+  introduction2: "Cliquez sur le + à droite de l'écran pour ajouter de nouvelles colonnes quand vous le souhaitez."
   _columns:
     main: "Principale"
     widgets: "Widgets"
diff --git a/locales/id-ID.yml b/locales/id-ID.yml
index a73e108d7f..3a2bf69a72 100644
--- a/locales/id-ID.yml
+++ b/locales/id-ID.yml
@@ -164,7 +164,6 @@ annotation: "Keterangan konten"
 federation: "Federasi"
 instances: "Instansi"
 registeredAt: "Terdaftar"
-latestRequestSentAt: "Permintaan terakhir dikirim pada"
 latestRequestReceivedAt: "Permintaan terakhir diterima pada"
 latestStatus: "Status terakhir"
 storageUsage: "Penggunaan penyimpanan"
@@ -856,6 +855,10 @@ colored: "Diwarnai"
 label: "Label"
 localOnly: "Hanya lokal"
 account: "Akun"
+like: "Suka"
+unlike: "Tidak Suka"
+numberOfLikes: "Jumlah yang disukai"
+show: "Tampilkan"
 _emailUnavailable:
   used: "Alamat surel ini telah digunakan"
   format: "Format tidak valid."
@@ -1221,6 +1224,7 @@ _widgets:
   jobQueue: "Antrian kerja"
   serverMetric: "Statistik peladen"
   aiscript: "Konsol AiScript"
+  aiscriptApp: "Aplikasi AiScript"
   aichan: "Ai"
   _userList:
     chooseList: "Pilih daftar"
@@ -1324,6 +1328,21 @@ _timelines:
   local: "Lokal"
   social: "Sosial"
   global: "Global"
+_play:
+  new: "Membuat Permainan"
+  edit: "Menyunting Permainan"
+  created: "Permainan sudah dibuat"
+  updated: "Permainan sudah diperbaharui"
+  deleted: "Hapus permainan"
+  pageSetting: "Pengaturan permainan"
+  editThisPage: "Sunting Permainan ini"
+  viewSource: "Lihat sumber"
+  my: "Permainan saya"
+  liked: "Permainan Disukai"
+  featured: "Populer"
+  title: "Judul"
+  script: "Script"
+  summary: "Deskripsi"
 _pages:
   newPage: "Buat halaman baru"
   editPage: "Sunting halaman"
diff --git a/locales/it-IT.yml b/locales/it-IT.yml
index 6b1b47f4e2..3fba19985e 100644
--- a/locales/it-IT.yml
+++ b/locales/it-IT.yml
@@ -1,7 +1,7 @@
 ---
 _lang_: "Italiano"
 headlineMisskey: "Rete collegata tramite note"
-introMisskey: "Benvenut@! Misskey è un servizio di microblogging decentralizzato, libero e aperto. \nScrivi \"note\" per condividere ciò che sta succedendo adesso o per dire a tutti qualcosa di te. 📡\nGrazie alla funzione \"reazioni\" puoi anche mandare reazioni rapide alle note delle altre persone del Fediverso. 👍\nEsplora un nuovo mondo! 🚀"
+introMisskey: "Eccoci! Misskey è un servizio di microblogging decentralizzato, libero e aperto. \n📡 Puoi pubblicare «Note» per condividere ciò che sta succedendo o per dire a tutti qualcosa su di te. \n👍 Puoi reagire inviando emoji rapidi alle «Note» provenienti da altri profili nel Fediverso.\n🚀 Esplora un nuovo mondo insieme a noi!"
 poweredByMisskeyDescription: "{name} è uno dei servizi (chiamati istanze) che utilizzano la piattaforma open source <b>Misskey</b>."
 monthAndDay: "{day}/{month}"
 search: "Cerca"
@@ -49,6 +49,7 @@ deleteAndEdit: "Elimina e modifica"
 deleteAndEditConfirm: "Vuoi davvero cancellare questa nota e scriverla di nuovo? Verrano eliminate anche tutte le reazioni, Rinote e risposte collegate."
 addToList: "Aggiungi alla lista"
 sendMessage: "Invia messaggio"
+copyRSS: "Copia RSS"
 copyUsername: "Copia nome utente"
 searchUser: "Cerca utente"
 reply: "Rispondi"
@@ -166,7 +167,6 @@ annotation: "Descrizione"
 federation: "Federazione"
 instances: "Istanza"
 registeredAt: "Registrato presso"
-latestRequestSentAt: "Ultima richiesta inviata"
 latestRequestReceivedAt: "Ultima richiesta ricevuta"
 latestStatus: "Ultimo stato"
 storageUsage: "Capienza dei dischi"
@@ -456,6 +456,8 @@ language: "Lingua"
 uiLanguage: "Lingua di visualizzazione dell'interfaccia"
 groupInvited: "Invitat@ al gruppo"
 aboutX: "Informazioni su {x}"
+emojiStyle: "Stile emoji"
+native: "Nativo"
 disableDrawer: "Non mostrare il menù sul drawer"
 youHaveNoGroups: "Nessun gruppo"
 joinOrCreateGroup: "Puoi creare il tuo gruppo o essere invitat@ a gruppi che già esistono."
@@ -714,6 +716,7 @@ accentColor: "Colore principale"
 textColor: "Testo"
 saveAs: "Salva con nome"
 advanced: "Avanzato"
+advancedSettings: "Impostazioni avanzate"
 value: "Valore"
 createdAt: "Data di creazione"
 updatedAt: "Aggiornato il"
@@ -873,7 +876,7 @@ deleteAccount: "Eliminazione profilo"
 document: "Documento"
 numberOfPageCache: "Numero di pagine cache"
 numberOfPageCacheDescription: "Aumenta l'usabilità, ma aumenta anche il carico e l'utilizzo della memoria."
-logoutConfirm: "Sei sicuro di voler effettuare il logout?"
+logoutConfirm: "Vuoi davvero uscire da Misskey? "
 lastActiveDate: "Data dell'ultimo utilizzo"
 statusbar: "Barra di stato"
 pleaseSelect: "Scegli un'opzione"
@@ -910,6 +913,11 @@ windowMaximize: "Ingrandisci"
 windowRestore: "Ripristina"
 caption: "Didascalia"
 loggedInAsBot: "Connessione come Bot"
+tools: "Strumenti"
+cannotLoad: "Caricamento impossibile"
+numberOfProfileView: "Visualizzazioni profilo"
+like: "Mi piace!"
+show: "Visualizza"
 _sensitiveMediaDetection:
   description: "L'apprendimento automatico può essere utilizzato per individuare automaticamente i media sensibili da moderare. Il carico del server aumenta leggermente."
   sensitivity: "Sensibilità di rilevamento"
@@ -1061,7 +1069,7 @@ _mfm:
   sparkleDescription: "Aggiungere effetti particellari scintillanti."
   rotate: "Ruota"
   rotateDescription: "Ruota con un angolo specificato."
-  plain: "aereo"
+  plain: "Testo semplice"
   plainDescription: "Disattiva tutta la sintassi interna."
 _instanceTicker:
   none: "Nascondi"
@@ -1199,13 +1207,13 @@ _time:
   day: "giorni"
 _tutorial:
   title: "Come usare Misskey"
-  step1_1: "Benvenuto/a!"
+  step1_1: "Eccoci!"
   step1_2: "Questa pagina si chiama una \" Timeline \". Mostra in ordine cronologico le \" note \" delle persone che segui."
   step1_3: "Attualmente la tua Timeline è vuota perché non segui alcun profilo e non hai pubblicato alcuna nota ancora."
-  step2_1: "Prima di scrivere una nota o di seguire altri profili, imposta il tuo di profilo!"
+  step2_1: "Prima di scrivere una «Nota» o di seguire altri profili, prepara il tuo profilo!"
   step2_2: "Aggiungere qualche informazione su di te aumenterà le tue possibilità di essere seguit@ da altre persone. "
   step3_1: "Hai finito di impostare il tuo profilo?"
-  step3_2: "Ora, puoi pubblicare una nota. Facciamo una prova! Premi il pulsante a forma di penna in cima allo schermo per aprire una finestra di dialogo.  "
+  step3_2: "Ora puoi pubblicare una «Nota». Proviamo subito! Premi il bottone con l'icona «penna» per iniziare a scrivere in una finestra di dialogo.  "
   step3_3: "Scritto il testo della nota, puoi pubblicarla premendo il pulsante nella parte superiore destra della finestra di dialogo."
   step3_4: "Non ti viene niente in mente? Perché non scrivi semplicemente \"Ho appena cominciato a usare Misskey\"?"
   step4_1: "Hai pubblicato qualcosa?"
@@ -1217,7 +1225,7 @@ _tutorial:
   step6_1: "Adesso, dovresti essere in grado di vedere le note dagli altri profili sulla tua timeline."
   step6_2: "Puoi anche rispondere alle note con un click, scegliendo le reazioni immediate."
   step6_3: "Per inviare una reazione, premi l'icona + della nota e scegli l'emoji che vuoi mandare."
-  step7_1: "Complimenti! Sei arrivat@ alla fine dell'esercitazione di base su come usare Misskey. "
+  step7_1: "Congratulazioni! Hai completato l'esercitazione iniziale su come usare Misskey."
   step7_2: "Se vuoi saperne di più su Misskey, puoi dare un'occhiata alla sezione {help}."
   step7_3: "Da ultimo, buon divertimento su Misskey! 🚀"
   step8_1: "Per concludere, vuoi attivare le notifiche push?"
@@ -1309,7 +1317,8 @@ _widgets:
   jobQueue: "Coda di lavoro"
   serverMetric: "Statistiche server"
   aiscript: "Console AiScript"
-  aichan: "indaco (tintura)"
+  aichan: "Mascotte Ai"
+  userList: "Elenco utenti"
   _userList:
     chooseList: "Seleziona una lista"
 _cw:
@@ -1375,6 +1384,7 @@ _profile:
   changeBanner: "Cambia intestazione"
 _exportOrImport:
   allNotes: "Tutte le note"
+  favoritedNotes: "Note preferite"
   followingList: "Follows"
   muteList: "Elenco profili silenziati"
   blockingList: "Elenco profili bloccati"
@@ -1412,6 +1422,12 @@ _timelines:
   local: "Locale"
   social: "Sociale"
   global: "Federata"
+_play:
+  viewSource: "Visualizza sorgente"
+  featured: "Popolari"
+  title: "Titolo"
+  script: "Script"
+  summary: "Descrizione"
 _pages:
   newPage: "Crea pagina"
   editPage: "Modifica pagina"
diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index d6a5518196..b49d872a0b 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -916,6 +916,14 @@ loggedInAsBot: "Botアカウントでログイン中"
 tools: "ツール"
 cannotLoad: "読み込めません"
 numberOfProfileView: "プロフィール表示回数"
+like: "いいね!"
+unlike: "いいねを解除"
+numberOfLikes: "いいね数"
+show: "表示"
+neverShow: "今後表示しない"
+remindMeLater: "また後で"
+didYouLikeMisskey: "Misskeyを気に入っていただけましたか?"
+pleaseDonate: "Misskeyは{host}が使用している無料のソフトウェアです。これからも開発を続けられるように、ぜひ寄付をお願いします!"
 
 _sensitiveMediaDetection:
   description: "機械学習を使って自動でセンシティブなメディアを検出し、モデレーションに役立てることができます。サーバーの負荷が少し増えます。"
@@ -1348,6 +1356,7 @@ _widgets:
   jobQueue: "ジョブキュー"
   serverMetric: "サーバーメトリクス"
   aiscript: "AiScriptコンソール"
+  aiscriptApp: "AiScript App"
   aichan: "藍"
   userList: "ユーザーリスト"
   _userList:
@@ -1463,6 +1472,22 @@ _timelines:
   social: "ソーシャル"
   global: "グローバル"
 
+_play:
+  new: "Playの作成"
+  edit: "Playの編集"
+  created: "Playを作成しました"
+  updated: "Playを更新しました"
+  deleted: "Playを削除しました"
+  pageSetting: "Play設定"
+  editThisPage: "このPlayを編集"
+  viewSource: "ソースを表示"
+  my: "自分のPlay"
+  liked: "いいねしたPlay"
+  featured: "人気"
+  title: "タイトル"
+  script: "スクリプト"
+  summary: "説明"
+
 _pages:
   newPage: "ページの作成"
   editPage: "ページの編集"
diff --git a/locales/ja-KS.yml b/locales/ja-KS.yml
index 994fe9a195..f8c045db00 100644
--- a/locales/ja-KS.yml
+++ b/locales/ja-KS.yml
@@ -167,7 +167,6 @@ annotation: "注釈"
 federation: "連合"
 instances: "インスタンス"
 registeredAt: "初観測"
-latestRequestSentAt: "ちょっと前のリクエスト送信"
 latestRequestReceivedAt: "ちょっと前のリクエスト受信"
 latestStatus: "ちょっと前のステータス"
 storageUsage: "ストレージ使うた量"
@@ -916,6 +915,8 @@ caption: "キャプション"
 loggedInAsBot: "Botアカウントでログイン中やで"
 tools: "ツール"
 cannotLoad: "読み込めへんで"
+like: "ええやん!"
+show: "表示"
 _sensitiveMediaDetection:
   description: "機械学習を使って自動でセンシティブなメディアを検出して、モデレーションに役立てることができるで。サーバーの負荷が少し増えてまうなあ。"
   sensitivity: "検出感度やで"
@@ -1419,6 +1420,12 @@ _timelines:
   local: "ローカル"
   social: "ソーシャル"
   global: "グローバル"
+_play:
+  viewSource: "ソースを表示"
+  featured: "人気"
+  title: "タイトル"
+  script: "スクリプト"
+  summary: "説明"
 _pages:
   newPage: "ページを作る"
   editPage: "ページの編集"
diff --git a/locales/ko-KR.yml b/locales/ko-KR.yml
index fa73d49a34..d3a4a40b49 100644
--- a/locales/ko-KR.yml
+++ b/locales/ko-KR.yml
@@ -167,7 +167,6 @@ annotation: "내용에 대한 주석"
 federation: "연합"
 instances: "인스턴스"
 registeredAt: "등록 날짜"
-latestRequestSentAt: "마지막으로 요청을 보낸 시간"
 latestRequestReceivedAt: "마지막으로 요청을 받은 시간"
 latestStatus: "마지막 상태"
 storageUsage: "스토리지 사용량"
@@ -916,6 +915,11 @@ caption: "캡션"
 loggedInAsBot: "봇 계정으로 로그인중"
 tools: "도구"
 cannotLoad: "불러오지 못했습니다"
+numberOfProfileView: "프로필 뷰 수"
+like: "좋아요!"
+unlike: "좋아요 취소"
+numberOfLikes: "좋아요 수"
+show: "표시"
 _sensitiveMediaDetection:
   description: "기계학습을 통해 자동으로 민감한 미디어를 탐지하여, 모더레이션에 참고할 수 있도록 합니다. 서버의 부하를 약간 증가시킵니다."
   sensitivity: "탐지 민감도"
@@ -1315,6 +1319,7 @@ _widgets:
   jobQueue: "작업 대기열"
   serverMetric: "서버 통계"
   aiscript: "AiScript 콘솔"
+  aiscriptApp: "AiScript 앱"
   aichan: "아이"
   userList: "사용자 목록"
   _userList:
@@ -1382,6 +1387,7 @@ _profile:
   changeBanner: "배너 이미지 변경"
 _exportOrImport:
   allNotes: "모든 노트"
+  favoritedNotes: "즐겨찾기한 노트"
   followingList: "팔로잉"
   muteList: "뮤트"
   blockingList: "차단"
@@ -1419,6 +1425,21 @@ _timelines:
   local: "로컬"
   social: "소셜"
   global: "글로벌"
+_play:
+  new: "Play 만들기"
+  edit: "Play 수정하기"
+  created: "Play를 생성했습니다"
+  updated: "Play를 갱신했습니다"
+  deleted: "Play를 삭제했습니다"
+  pageSetting: "Play 설정"
+  editThisPage: "이 Play를 수정"
+  viewSource: "소스 보기"
+  my: "나의 Play"
+  liked: "좋아요 한 Play"
+  featured: "인기"
+  title: "제목"
+  script: "스크립트"
+  summary: "설명"
 _pages:
   newPage: "페이지 만들기"
   editPage: "페이지 수정"
diff --git a/locales/nl-NL.yml b/locales/nl-NL.yml
index ad7ab9723e..93ed3fa7e2 100644
--- a/locales/nl-NL.yml
+++ b/locales/nl-NL.yml
@@ -165,7 +165,6 @@ annotation: "Reacties"
 federation: "Federatie"
 instances: "Server"
 registeredAt: "Geregistreerd op"
-latestRequestSentAt: "Laatste aanvraag verstuurd"
 latestRequestReceivedAt: "Laatste aanvraag ontvangen"
 latestStatus: "Laatste status"
 storageUsage: "Gebruikte opslagruimte"
diff --git a/locales/pl-PL.yml b/locales/pl-PL.yml
index 6c3b100885..712c05bb78 100644
--- a/locales/pl-PL.yml
+++ b/locales/pl-PL.yml
@@ -165,7 +165,6 @@ annotation: "Komentarze"
 federation: "Federacja"
 instances: "Instancja"
 registeredAt: "Zarejestrowano"
-latestRequestSentAt: "Ostatnie żądanie wysłano o"
 latestRequestReceivedAt: "Ostatnie żądanie otrzymano o"
 latestStatus: "Najnowszy status"
 storageUsage: "Użycie pamięci"
@@ -867,6 +866,8 @@ pushNotificationNotSupported: "Przeglądarka lub instancja nie obsługuje powiad
 sendPushNotificationReadMessage: "Usuń powiadomienia push po przeczytaniu powiadomień i wiadomości."
 sendPushNotificationReadMessageCaption: "Chwilowo pojawi się powiadomienie \"{emptyPushNotificationMessage}\". Może wzrosnąć zużycie baterii urządzenia."
 loggedInAsBot: "Jesteś obecnie zalogowany/a jako bot"
+like: "Polub"
+show: "Wyświetlanie"
 _sensitiveMediaDetection:
   description: "Zmniejsza wysiłek związany z moderacją serwera dzięki automatycznemu rozpoznawaniu zawartości NSFW za pomocą uczenia maszynowego. To nieznacznie zwiększy obciążenie serwera."
   setSensitiveFlagAutomatically: "Oznacz jako NSFW"
@@ -1314,6 +1315,12 @@ _timelines:
   local: "Lokalne"
   social: "Społeczność"
   global: "Globalna"
+_play:
+  viewSource: "Zobacz źródło"
+  featured: "Wyróżnione"
+  title: "Tytuł"
+  script: "Skrypt"
+  summary: "Opis"
 _pages:
   newPage: "Utwórz stronę"
   editPage: "Edytuj tę stronę"
diff --git a/locales/pt-PT.yml b/locales/pt-PT.yml
index d333405316..dd1c2954b7 100644
--- a/locales/pt-PT.yml
+++ b/locales/pt-PT.yml
@@ -164,7 +164,6 @@ annotation: "Anotação"
 federation: "União"
 instances: "Instância"
 registeredAt: "Registrado em"
-latestRequestSentAt: "Enviar a solicitação mais recente"
 latestRequestReceivedAt: "Recebeu a última solicitação"
 latestStatus: "Status mais recente"
 storageUsage: "Uso de armazenamento"
diff --git a/locales/ro-RO.yml b/locales/ro-RO.yml
index 4bb1d4f09a..3c85045e5d 100644
--- a/locales/ro-RO.yml
+++ b/locales/ro-RO.yml
@@ -164,7 +164,6 @@ annotation: "Adnotări"
 federation: "Federație"
 instances: "Instanțe"
 registeredAt: "Înregistrat în"
-latestRequestSentAt: "Ultima cerere trimisă"
 latestRequestReceivedAt: "Ultima cerere primită"
 latestStatus: "Ultimul status"
 storageUsage: "Utilizare stocare"
@@ -648,6 +647,7 @@ middle: "Mediu"
 sent: "Trimite"
 searchByGoogle: "Caută"
 file: "Fișiere"
+show: "Arată"
 _email:
   _follow:
     title: "te-a urmărit"
@@ -691,6 +691,9 @@ _charts:
   federation: "Federație"
 _timelines:
   home: "Acasă"
+_play:
+  script: "Script"
+  summary: "Descriere"
 _pages:
   blocks:
     image: "Imagini"
diff --git a/locales/ru-RU.yml b/locales/ru-RU.yml
index 7c256b3bef..553fa9e811 100644
--- a/locales/ru-RU.yml
+++ b/locales/ru-RU.yml
@@ -164,7 +164,6 @@ annotation: "Описание"
 federation: "Федерация"
 instances: "Инстанс"
 registeredAt: "Первое наблюдение"
-latestRequestSentAt: "Последний отправленный запрос"
 latestRequestReceivedAt: "Последний полученный запрос"
 latestStatus: "Последний статус"
 storageUsage: "Использовано"
@@ -865,6 +864,8 @@ enableAutoSensitiveDescription: "Если доступно, используйт
 account: "Учётные записи"
 windowMaximize: "Развернуть"
 windowRestore: "Восстановить"
+like: "Нравится!"
+show: "Отображение"
 _sensitiveMediaDetection:
   description: "Машинное обучение может быть использовано для автоматического обнаружения чувствительных медиа для модерации. Нагрузка на сервер увеличивается незначительно."
   setSensitiveFlagAutomatically: "Установить флаг NSFW"
@@ -1333,6 +1334,12 @@ _timelines:
   local: "Местная"
   social: "Социальная"
   global: "Всеобщая"
+_play:
+  viewSource: "Просмотр исходника"
+  featured: "Популярные"
+  title: "Заголовок"
+  script: "Скрипт"
+  summary: "Описание"
 _pages:
   newPage: "Создать страницу"
   editPage: "Править страницу"
diff --git a/locales/sk-SK.yml b/locales/sk-SK.yml
index 03a531e63b..3abfd8609d 100644
--- a/locales/sk-SK.yml
+++ b/locales/sk-SK.yml
@@ -167,7 +167,6 @@ annotation: "Komentáre"
 federation: "Federácia"
 instances: "Inštancia"
 registeredAt: "Registrácia"
-latestRequestSentAt: "Posledná odoslaná požiadavka"
 latestRequestReceivedAt: "Posledná prijatá požiadavka"
 latestStatus: "Posledný status"
 storageUsage: "Využité úložisko"
@@ -912,6 +911,8 @@ windowRestore: "Obnoviť"
 caption: "Nadpis"
 tools: "Nástroje"
 cannotLoad: "Nedá sa načítať."
+like: "Páči sa mi"
+show: "Zobraziť"
 _sensitiveMediaDetection:
   description: "Strojové učenie sa použije na automatickú detekciu citlivých médií na účely ich moderovania. Mierne sa zvýši zaťaženie servera."
   sensitivity: "Citlivosť detekcie"
@@ -1414,6 +1415,12 @@ _timelines:
   local: "Lokálne"
   social: "Sociálne"
   global: "Globálne"
+_play:
+  viewSource: "Ukázať zdroj"
+  featured: "Význačné"
+  title: "Nadpis"
+  script: "Skript"
+  summary: "Popis"
 _pages:
   newPage: "Vytvoriť novú stránku"
   editPage: "Upraviť túto stránku"
diff --git a/locales/sv-SE.yml b/locales/sv-SE.yml
index b00808d3d0..8b87e36acd 100644
--- a/locales/sv-SE.yml
+++ b/locales/sv-SE.yml
@@ -2,6 +2,7 @@
 _lang_: "Svenska"
 headlineMisskey: "Ett nätverk kopplat av noter"
 introMisskey: "Välkommen! Misskey är en öppen och decentraliserad mikrobloggningstjänst.\nSkapa en \"not\" och dela dina tankar med alla runtomkring dig. 📡\nMed \"reaktioner\" kan du snabbt uttrycka dina känslor kring andras noter.👍\nLåt oss utforska en nya värld!🚀"
+poweredByMisskeyDescription: "{name} är en tjänst driven av den öppna källkodsplatformen <b>Misskey</b> (benämns \"Misskey instans\")."
 monthAndDay: "{day}/{month}"
 search: "Sök"
 notifications: "Notifikationer"
@@ -12,6 +13,7 @@ fetchingAsApObject: "Hämtar från Fediversum..."
 ok: "OK"
 gotIt: "Uppfattat!"
 cancel: "Avbryt"
+noThankYou: "Nej tack"
 enterUsername: "Ange användarnamn"
 renotedBy: "Omnoterad av {user}"
 noNotes: "Inga noteringar"
@@ -47,11 +49,13 @@ deleteAndEdit: "Radera och ändra"
 deleteAndEditConfirm: "Är du säker att du vill radera denna not och ändra den? Du kommer förlora alla reaktioner, omnoteringar och svar till den."
 addToList: "Lägg till i lista"
 sendMessage: "Skicka ett meddelande"
+copyRSS: "Kopiera RSS"
 copyUsername: "Kopiera användarnamn"
 searchUser: "Sök användare"
 reply: "Svara"
 loadMore: "Ladda mer"
 showMore: "Visa mer"
+showLess: "Stäng"
 youGotNewFollower: "följde dig"
 receiveFollowRequest: "Följarförfrågan mottagen"
 followRequestAccepted: "Följarförfrågan accepterad"
@@ -163,7 +167,6 @@ annotation: "Kommentarer"
 federation: "Federation"
 instances: "Instanser"
 registeredAt: "Registrerad på"
-latestRequestSentAt: "Senaste förfrågan skickad"
 latestRequestReceivedAt: "Senaste begäran mottagen"
 latestStatus: "Senaste status"
 storageUsage: "Använt lagringsutrymme"
@@ -239,6 +242,17 @@ saved: "Sparad"
 messaging: "Chatt"
 upload: "Ladda upp"
 keepOriginalUploading: "Behåll originalbild"
+keepOriginalUploadingDescription: "Sparar den originellt uppladdade bilden i sitt i befintliga skick. Om avstängd, kommer en webbversion bli genererad vid uppladdning."
+fromDrive: "Från Drive"
+fromUrl: "Från en länk"
+uploadFromUrl: "Ladda upp från länk"
+uploadFromUrlDescription: "Länken av filen du vill ladda upp"
+uploadFromUrlRequested: "Uppladdning begärd"
+uploadFromUrlMayTakeTime: "Det kan ta tid tills att uppladdningen blir klar."
+explore: "Utforska"
+messageRead: "Läs"
+noMoreHistory: "Det finns ingen mer historik"
+startMessaging: "Starta en chatt"
 nsfw: "Känsligt innehåll"
 pinnedNotes: "Fästad not"
 userList: "Listor"
diff --git a/locales/th-TH.yml b/locales/th-TH.yml
index 9dfcf0d2c4..58deeff6f1 100644
--- a/locales/th-TH.yml
+++ b/locales/th-TH.yml
@@ -167,7 +167,6 @@ annotation: "ความคิดเห็น"
 federation: "เฟดิเวิร์ส"
 instances: "ตัวอย่าง"
 registeredAt: "จดทะเบียนที่"
-latestRequestSentAt: "ส่งคำขอล่าสุดไปแล้ว"
 latestRequestReceivedAt: "ได้รับคำขอล่าสุดไปแล้ว"
 latestStatus: "สถานะล่าสุด"
 storageUsage: "พื้นที่จัดเก็บข้อมูลที่ใช้ไป"
@@ -916,6 +915,9 @@ caption: "รายละเอียด"
 loggedInAsBot: "ล็อกอินเป็นบอตอยู่ในขณะนี้"
 tools: "เครื่องมือ"
 cannotLoad: "ไม่สามารถโหลดได้"
+numberOfProfileView: "มุมมองโปรไฟล์"
+like: "ชื่นชอบ"
+show: "แสดงผล"
 _sensitiveMediaDetection:
   description: "ลดความพยายามในการดูแลเซิร์ฟเวอร์ผ่านการจดจำสื่อ NSFW โดยอัตโนมัติผ่านการเรียนรู้ของเครื่อง การทำสิ่งนี้อาจจะเพิ่มภาระบนเซิร์ฟเวอร์เล็กน้อย"
   sensitivity: "การตรวจจับความไว"
@@ -1420,6 +1422,12 @@ _timelines:
   local: "ในพื้นที่"
   social: "โซเชี่ยล"
   global: "ทั่วโลก"
+_play:
+  viewSource: "ดูต้นฉบับ"
+  featured: "เป็นที่นิยม"
+  title: "หัวข้อ"
+  script: "สคริปต์"
+  summary: "รายละเอียด"
 _pages:
   newPage: "สร้างหน้าเพจใหม่"
   editPage: "แก้ไขหน้าเพจ"
diff --git a/locales/uk-UA.yml b/locales/uk-UA.yml
index cb52a86d98..352fb354ee 100644
--- a/locales/uk-UA.yml
+++ b/locales/uk-UA.yml
@@ -166,7 +166,6 @@ annotation: "Коментарі"
 federation: "Федіверс"
 instances: "Інстанс"
 registeredAt: "Приєднався(лась)"
-latestRequestSentAt: "Останній запит надіслано"
 latestRequestReceivedAt: "Останній запит прийнято"
 latestStatus: "Останній статус"
 storageUsage: "Використання простору"
@@ -893,6 +892,8 @@ unsubscribePushNotification: "Вимкнути push-сповіщення"
 windowMaximize: "Розгорнути"
 windowRestore: "Відновити"
 caption: "Підпис"
+like: "Вподобати"
+show: "Відображення"
 _sensitiveMediaDetection:
   sensitivity: "Чутливість детектування"
   setSensitiveFlagAutomatically: "Позначити як NSFW"
@@ -1349,6 +1350,12 @@ _timelines:
   local: "Локальна"
   social: "Соціальна"
   global: "Глобальна"
+_play:
+  viewSource: "Переглянути вихідний код"
+  featured: "Популярні"
+  title: "Заголовок"
+  script: "Скрипт"
+  summary: "Опис"
 _pages:
   newPage: "Створити сторінку"
   editPage: "Редагувати сторінку"
diff --git a/locales/vi-VN.yml b/locales/vi-VN.yml
index fdb6ec2647..0070af56f0 100644
--- a/locales/vi-VN.yml
+++ b/locales/vi-VN.yml
@@ -164,7 +164,6 @@ annotation: "Bình luận"
 federation: "Liên hợp"
 instances: "Máy chủ"
 registeredAt: "Đăng ký vào"
-latestRequestSentAt: "Yêu cầu cuối gửi lúc"
 latestRequestReceivedAt: "Yêu cầu cuối nhận lúc"
 latestStatus: "Trạng thái cuối cùng"
 storageUsage: "Dung lượng lưu trữ"
@@ -895,6 +894,8 @@ navbar: "Thanh điều hướng"
 shuffle: "Xáo trộn"
 account: "Tài khoản của bạn"
 move: "Di chuyển"
+like: "Thích"
+show: "Hiển thị"
 _sensitiveMediaDetection:
   description: "Giảm nỗ lực kiểm duyệt máy chủ thông qua việc tự động nhận dạng media NSFW thông qua học máy. Điều này sẽ làm tăng một chút áp lực trên máy chủ."
   sensitivity: "Phát hiện nhạy cảm"
@@ -1394,6 +1395,12 @@ _timelines:
   local: "Máy chủ này"
   social: "Xã hội"
   global: "Liên hợp"
+_play:
+  viewSource: "Xem mã nguồn"
+  featured: "Nổi tiếng"
+  title: "Tựa đề"
+  script: "Kịch bản"
+  summary: "Mô tả"
 _pages:
   newPage: "Tạo Trang mới"
   editPage: "Sửa Trang này"
diff --git a/locales/zh-CN.yml b/locales/zh-CN.yml
index 80d367a284..7c3efec86c 100644
--- a/locales/zh-CN.yml
+++ b/locales/zh-CN.yml
@@ -167,7 +167,6 @@ annotation: "注解"
 federation: "联合"
 instances: "实例"
 registeredAt: "初次观测"
-latestRequestSentAt: "上次发送的请求"
 latestRequestReceivedAt: "上次收到的请求"
 latestStatus: "最后状态"
 storageUsage: "已用存储"
@@ -913,9 +912,14 @@ sendPushNotificationReadMessageCaption: "“{emptyPushNotificationMessage}”的
 windowMaximize: "最大化"
 windowRestore: "还原"
 caption: "标题"
-loggedInAsBot: "已登录的Bot"
+loggedInAsBot: "以Bot账户登录"
 tools: "工具"
 cannotLoad: "无法加载"
+numberOfProfileView: "个人资料展示次数"
+like: "点赞!"
+unlike: "取消赞"
+numberOfLikes: "点赞数"
+show: "显示"
 _sensitiveMediaDetection:
   description: "可以使用机器学习技术自动检测敏感媒体,以便进行审核。服务器负载将略微增加。"
   sensitivity: "检测敏感度"
@@ -1049,8 +1053,8 @@ _mfm:
   shakeDescription: "显示摇晃的动画效果。"
   twitch: "动画(颤抖)"
   twitchDescription: "显示强烈颤抖的动画效果。"
-  spin: "动画(回转)"
-  spinDescription: "显示回转的动画效果。"
+  spin: "动画(旋转)"
+  spinDescription: "显示旋转的动画效果。"
   x2: "大"
   x2Description: "以大尺寸显示内容。"
   x3: "非常大"
@@ -1315,6 +1319,7 @@ _widgets:
   jobQueue: "作业队列"
   serverMetric: "服务器指标"
   aiscript: "AiScript控制台"
+  aiscriptApp: "AiScript App"
   aichan: "小蓝"
   userList: "用户列表"
   _userList:
@@ -1382,6 +1387,7 @@ _profile:
   changeBanner: "修改横幅"
 _exportOrImport:
   allNotes: "所有帖子"
+  favoritedNotes: "收藏的帖子"
   followingList: "关注中"
   muteList: "屏蔽"
   blockingList: "拉黑"
@@ -1419,6 +1425,21 @@ _timelines:
   local: "本地"
   social: "社交"
   global: "全局"
+_play:
+  new: "创建Play"
+  edit: "编辑Play"
+  created: "创建了一个Play"
+  updated: "更新了Play"
+  deleted: "删除了Play"
+  pageSetting: "Play设置"
+  editThisPage: "编辑此Play"
+  viewSource: "查看源代码"
+  my: "我的Play"
+  liked: "点赞的Play"
+  featured: "热门"
+  title: "标题"
+  script: "脚本"
+  summary: "描述"
 _pages:
   newPage: "创建页面"
   editPage: "编辑页面"
diff --git a/locales/zh-TW.yml b/locales/zh-TW.yml
index a97593e59c..661325d506 100644
--- a/locales/zh-TW.yml
+++ b/locales/zh-TW.yml
@@ -167,7 +167,6 @@ annotation: "註解"
 federation: "站台聯邦"
 instances: "實例"
 registeredAt: "初次觀測"
-latestRequestSentAt: "上次發送的請求"
 latestRequestReceivedAt: "上次收到的請求"
 latestStatus: "最後狀態"
 storageUsage: "已使用容量"
@@ -917,6 +916,9 @@ loggedInAsBot: "以機器人帳號登入中"
 tools: "工具"
 cannotLoad: "無法載入"
 numberOfProfileView: "個人檔案檢視次數"
+like: "讚"
+unlike: "收回讚"
+show: "檢視"
 _sensitiveMediaDetection:
   description: "您可以使用機器學習自動檢測敏感媒體並將其用於審核。 伺服器的負荷會稍微增加。"
   sensitivity: "檢測敏感度"
@@ -1421,6 +1423,12 @@ _timelines:
   local: "本地"
   social: "社群"
   global: "公開"
+_play:
+  viewSource: "檢視原始碼"
+  featured: "人氣"
+  title: "標題"
+  script: "腳本"
+  summary: "描述"
 _pages:
   newPage: "建立頁面"
   editPage: "編輯頁面"
diff --git a/package.json b/package.json
index 1e3bf82a7b..1ee91c9fc1 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
 	"name": "misskey",
-	"version": "13.0.0-beta.20",
+	"version": "13.0.0-beta.26",
 	"codename": "indigo",
 	"repository": {
 		"type": "git",
@@ -53,10 +53,10 @@
 	"devDependencies": {
 		"@types/gulp": "4.0.10",
 		"@types/gulp-rename": "2.0.1",
-		"@typescript-eslint/eslint-plugin": "5.47.1",
-		"@typescript-eslint/parser": "5.47.1",
+		"@typescript-eslint/eslint-plugin": "5.48.0",
+		"@typescript-eslint/parser": "5.48.0",
 		"cross-env": "7.0.3",
-		"cypress": "12.2.0",
+		"cypress": "12.3.0",
 		"eslint": "^8.31.0",
 		"start-server-and-test": "1.15.2",
 		"typescript": "4.9.4"
diff --git a/packages/backend/migration/1672822262496-Flash.js b/packages/backend/migration/1672822262496-Flash.js
new file mode 100644
index 0000000000..6c2338fab2
--- /dev/null
+++ b/packages/backend/migration/1672822262496-Flash.js
@@ -0,0 +1,29 @@
+export class Flash1672822262496 {
+    name = 'Flash1672822262496'
+
+    async up(queryRunner) {
+        await queryRunner.query(`CREATE TABLE "flash" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL, "title" character varying(256) NOT NULL, "summary" character varying(1024) NOT NULL, "userId" character varying(32) NOT NULL, "script" character varying(16384) NOT NULL, "permissions" character varying(256) array NOT NULL DEFAULT '{}', "likedCount" integer NOT NULL DEFAULT '0', CONSTRAINT "PK_0c01a2c1c5f2266942dd1b3fdbc" PRIMARY KEY ("id")); COMMENT ON COLUMN "flash"."createdAt" IS 'The created date of the Flash.'; COMMENT ON COLUMN "flash"."updatedAt" IS 'The updated date of the Flash.'; COMMENT ON COLUMN "flash"."userId" IS 'The ID of author.'`);
+        await queryRunner.query(`CREATE INDEX "IDX_149d2e44785707548c82999b01" ON "flash" ("createdAt") `);
+        await queryRunner.query(`CREATE INDEX "IDX_3aa8ea9a8f15214ad91638c0a7" ON "flash" ("updatedAt") `);
+        await queryRunner.query(`CREATE INDEX "IDX_9b88250fc2fd009b8f1b5623ed" ON "flash" ("userId") `);
+        await queryRunner.query(`CREATE TABLE "flash_like" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "userId" character varying(32) NOT NULL, "flashId" character varying(32) NOT NULL, CONSTRAINT "PK_d110109ee310588d63d6183b233" PRIMARY KEY ("id"))`);
+        await queryRunner.query(`CREATE INDEX "IDX_60c4af1c19a7a75f1592f93b28" ON "flash_like" ("userId") `);
+        await queryRunner.query(`CREATE UNIQUE INDEX "IDX_cfbfeeccb0cbedcd660b17eb07" ON "flash_like" ("userId", "flashId") `);
+        await queryRunner.query(`ALTER TABLE "flash" ADD CONSTRAINT "FK_9b88250fc2fd009b8f1b5623ed5" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
+        await queryRunner.query(`ALTER TABLE "flash_like" ADD CONSTRAINT "FK_60c4af1c19a7a75f1592f93b287" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
+        await queryRunner.query(`ALTER TABLE "flash_like" ADD CONSTRAINT "FK_6c16fe0e93b7a1951eca624b76a" FOREIGN KEY ("flashId") REFERENCES "flash"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
+    }
+
+    async down(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "flash_like" DROP CONSTRAINT "FK_6c16fe0e93b7a1951eca624b76a"`);
+        await queryRunner.query(`ALTER TABLE "flash_like" DROP CONSTRAINT "FK_60c4af1c19a7a75f1592f93b287"`);
+        await queryRunner.query(`ALTER TABLE "flash" DROP CONSTRAINT "FK_9b88250fc2fd009b8f1b5623ed5"`);
+        await queryRunner.query(`DROP INDEX "public"."IDX_cfbfeeccb0cbedcd660b17eb07"`);
+        await queryRunner.query(`DROP INDEX "public"."IDX_60c4af1c19a7a75f1592f93b28"`);
+        await queryRunner.query(`DROP TABLE "flash_like"`);
+        await queryRunner.query(`DROP INDEX "public"."IDX_9b88250fc2fd009b8f1b5623ed"`);
+        await queryRunner.query(`DROP INDEX "public"."IDX_3aa8ea9a8f15214ad91638c0a7"`);
+        await queryRunner.query(`DROP INDEX "public"."IDX_149d2e44785707548c82999b01"`);
+        await queryRunner.query(`DROP TABLE "flash"`);
+    }
+}
diff --git a/packages/backend/package.json b/packages/backend/package.json
index d139c1db86..6c1a217b60 100644
--- a/packages/backend/package.json
+++ b/packages/backend/package.json
@@ -21,9 +21,9 @@
 		"@tensorflow/tfjs-node": "4.1.0"
 	},
 	"dependencies": {
-		"@bull-board/api": "^4.10.0",
-		"@bull-board/fastify": "^4.10.0",
-		"@bull-board/ui": "^4.10.0",
+		"@bull-board/api": "^4.10.1",
+		"@bull-board/fastify": "^4.10.1",
+		"@bull-board/ui": "^4.10.1",
 		"@discordapp/twemoji": "14.0.2",
 		"@fastify/accepts": "4.1.0",
 		"@fastify/cookie": "^8.3.0",
@@ -38,10 +38,10 @@
 		"@peertube/http-signature": "1.7.0",
 		"@sinonjs/fake-timers": "10.0.2",
 		"accepts": "^1.3.8",
-		"ajv": "8.11.2",
+		"ajv": "8.12.0",
 		"archiver": "5.3.1",
 		"autwh": "0.1.0",
-		"aws-sdk": "2.1286.0",
+		"aws-sdk": "2.1289.0",
 		"bcryptjs": "2.4.3",
 		"blurhash": "2.0.4",
 		"bull": "4.10.2",
@@ -109,8 +109,8 @@
 		"stringz": "2.1.0",
 		"summaly": "2.7.0",
 		"syslog-pro": "git+https://github.com/misskey-dev/SyslogPro#0.2.9-misskey.2",
-		"systeminformation": "5.16.9",
-		"tinycolor2": "1.5.1",
+		"systeminformation": "5.17.1",
+		"tinycolor2": "1.5.2",
 		"tmp": "0.2.1",
 		"tsc-alias": "1.8.2",
 		"tsconfig-paths": "4.1.2",
@@ -128,7 +128,7 @@
 	},
 	"devDependencies": {
 		"@redocly/openapi-core": "1.0.0-beta.117",
-		"@swc/core": "1.3.24",
+		"@swc/core": "1.3.25",
 		"@swc/jest": "0.2.24",
 		"@types/accepts": "1.3.5",
 		"@types/archiver": "5.3.1",
@@ -172,8 +172,8 @@
 		"@types/web-push": "3.3.2",
 		"@types/websocket": "1.0.5",
 		"@types/ws": "8.5.4",
-		"@typescript-eslint/eslint-plugin": "5.47.1",
-		"@typescript-eslint/parser": "5.47.1",
+		"@typescript-eslint/eslint-plugin": "5.48.0",
+		"@typescript-eslint/parser": "5.48.0",
 		"cross-env": "7.0.3",
 		"eslint": "8.31.0",
 		"eslint-plugin-import": "2.26.0",
diff --git a/packages/backend/src/core/CoreModule.ts b/packages/backend/src/core/CoreModule.ts
index 7c6d12abf8..2f17fa389a 100644
--- a/packages/backend/src/core/CoreModule.ts
+++ b/packages/backend/src/core/CoreModule.ts
@@ -95,6 +95,8 @@ import { UserEntityService } from './entities/UserEntityService.js';
 import { UserGroupEntityService } from './entities/UserGroupEntityService.js';
 import { UserGroupInvitationEntityService } from './entities/UserGroupInvitationEntityService.js';
 import { UserListEntityService } from './entities/UserListEntityService.js';
+import { FlashEntityService } from './entities/FlashEntityService.js';
+import { FlashLikeEntityService } from './entities/FlashLikeEntityService.js';
 import { ApAudienceService } from './activitypub/ApAudienceService.js';
 import { ApDbResolverService } from './activitypub/ApDbResolverService.js';
 import { ApDeliverManagerService } from './activitypub/ApDeliverManagerService.js';
@@ -216,6 +218,8 @@ const $UserEntityService: Provider = { provide: 'UserEntityService', useExisting
 const $UserGroupEntityService: Provider = { provide: 'UserGroupEntityService', useExisting: UserGroupEntityService };
 const $UserGroupInvitationEntityService: Provider = { provide: 'UserGroupInvitationEntityService', useExisting: UserGroupInvitationEntityService };
 const $UserListEntityService: Provider = { provide: 'UserListEntityService', useExisting: UserListEntityService };
+const $FlashEntityService: Provider = { provide: 'FlashEntityService', useExisting: FlashEntityService };
+const $FlashLikeEntityService: Provider = { provide: 'FlashLikeEntityService', useExisting: FlashLikeEntityService };
 
 const $ApAudienceService: Provider = { provide: 'ApAudienceService', useExisting: ApAudienceService };
 const $ApDbResolverService: Provider = { provide: 'ApDbResolverService', useExisting: ApDbResolverService };
@@ -338,6 +342,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
 		UserGroupEntityService,
 		UserGroupInvitationEntityService,
 		UserListEntityService,
+		FlashEntityService,
+		FlashLikeEntityService,
 		ApAudienceService,
 		ApDbResolverService,
 		ApDeliverManagerService,
@@ -455,6 +461,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
 		$UserGroupEntityService,
 		$UserGroupInvitationEntityService,
 		$UserListEntityService,
+		$FlashEntityService,
+		$FlashLikeEntityService,
 		$ApAudienceService,
 		$ApDbResolverService,
 		$ApDeliverManagerService,
@@ -572,6 +580,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
 		UserGroupEntityService,
 		UserGroupInvitationEntityService,
 		UserListEntityService,
+		FlashEntityService,
+		FlashLikeEntityService,
 		ApAudienceService,
 		ApDbResolverService,
 		ApDeliverManagerService,
@@ -688,6 +698,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
 		$UserGroupEntityService,
 		$UserGroupInvitationEntityService,
 		$UserListEntityService,
+		$FlashEntityService,
+		$FlashLikeEntityService,
 		$ApAudienceService,
 		$ApDbResolverService,
 		$ApDeliverManagerService,
diff --git a/packages/backend/src/core/PushNotificationService.ts b/packages/backend/src/core/PushNotificationService.ts
index 667dc9c1fa..b18b7bb2cd 100644
--- a/packages/backend/src/core/PushNotificationService.ts
+++ b/packages/backend/src/core/PushNotificationService.ts
@@ -47,26 +47,6 @@ function truncateBody<T extends keyof pushNotificationsTypes>(type: T, body: pus
 	return body;
 }
 
-function truncateUnreadAntennaNote(notification: pushNotificationsTypes['unreadAntennaNote']): pushNotificationsTypes['unreadAntennaNote'] {
-	if (notification.note) {
-		return {
-			...notification,
-			note: {
-				...notification.note,
-				// textをgetNoteSummaryしたものに置き換える
-				text: getNoteSummary(('type' in notification && notification.type === 'renote') ? notification.note.renote as Packed<'Note'> : notification.note),
-
-				cw: undefined,
-				reply: undefined,
-				renote: undefined,
-				user: undefined as any, // 通知を受け取ったユーザーである場合が多いのでこれも捨てる アンテナの場合も不要なのでいらない
-			},
-		};
-	}
-
-	return notification;
-}
-
 @Injectable()
 export class PushNotificationService {
 	constructor(
diff --git a/packages/backend/src/core/entities/FlashEntityService.ts b/packages/backend/src/core/entities/FlashEntityService.ts
new file mode 100644
index 0000000000..61bd18c04f
--- /dev/null
+++ b/packages/backend/src/core/entities/FlashEntityService.ts
@@ -0,0 +1,55 @@
+import { Inject, Injectable } from '@nestjs/common';
+import { DI } from '@/di-symbols.js';
+import type { FlashsRepository, FlashLikesRepository } from '@/models/index.js';
+import { awaitAll } from '@/misc/prelude/await-all.js';
+import type { Packed } from '@/misc/schema.js';
+import type { } from '@/models/entities/Blocking.js';
+import type { User } from '@/models/entities/User.js';
+import type { Flash } from '@/models/entities/Flash.js';
+import { bindThis } from '@/decorators.js';
+import { UserEntityService } from './UserEntityService.js';
+
+@Injectable()
+export class FlashEntityService {
+	constructor(
+		@Inject(DI.flashsRepository)
+		private flashsRepository: FlashsRepository,
+
+		@Inject(DI.flashLikesRepository)
+		private flashLikesRepository: FlashLikesRepository,
+
+		private userEntityService: UserEntityService,
+	) {
+	}
+
+	@bindThis
+	public async pack(
+		src: Flash['id'] | Flash,
+		me?: { id: User['id'] } | null | undefined,
+	): Promise<Packed<'Flash'>> {
+		const meId = me ? me.id : null;
+		const flash = typeof src === 'object' ? src : await this.flashsRepository.findOneByOrFail({ id: src });
+
+		return await awaitAll({
+			id: flash.id,
+			createdAt: flash.createdAt.toISOString(),
+			updatedAt: flash.updatedAt.toISOString(),
+			userId: flash.userId,
+			user: this.userEntityService.pack(flash.user ?? flash.userId, me), // { detail: true } すると無限ループするので注意
+			title: flash.title,
+			summary: flash.summary,
+			script: flash.script,
+			likedCount: flash.likedCount,
+			isLiked: meId ? await this.flashLikesRepository.findOneBy({ flashId: flash.id, userId: meId }).then(x => x != null) : undefined,
+		});
+	}
+
+	@bindThis
+	public packMany(
+		flashs: Flash[],
+		me?: { id: User['id'] } | null | undefined,
+	) {
+		return Promise.all(flashs.map(x => this.pack(x, me)));
+	}
+}
+
diff --git a/packages/backend/src/core/entities/FlashLikeEntityService.ts b/packages/backend/src/core/entities/FlashLikeEntityService.ts
new file mode 100644
index 0000000000..dcf12d53ea
--- /dev/null
+++ b/packages/backend/src/core/entities/FlashLikeEntityService.ts
@@ -0,0 +1,44 @@
+import { Inject, Injectable } from '@nestjs/common';
+import { DI } from '@/di-symbols.js';
+import type { FlashLikesRepository } from '@/models/index.js';
+import { awaitAll } from '@/misc/prelude/await-all.js';
+import type { Packed } from '@/misc/schema.js';
+import type { } from '@/models/entities/Blocking.js';
+import type { User } from '@/models/entities/User.js';
+import type { FlashLike } from '@/models/entities/FlashLike.js';
+import { bindThis } from '@/decorators.js';
+import { UserEntityService } from './UserEntityService.js';
+import { FlashEntityService } from './FlashEntityService.js';
+
+@Injectable()
+export class FlashLikeEntityService {
+	constructor(
+		@Inject(DI.flashLikesRepository)
+		private flashLikesRepository: FlashLikesRepository,
+
+		private flashEntityService: FlashEntityService,
+	) {
+	}
+
+	@bindThis
+	public async pack(
+		src: FlashLike['id'] | FlashLike,
+		me?: { id: User['id'] } | null | undefined,
+	) {
+		const like = typeof src === 'object' ? src : await this.flashLikesRepository.findOneByOrFail({ id: src });
+
+		return {
+			id: like.id,
+			flash: await this.flashEntityService.pack(like.flash ?? like.flashId, me),
+		};
+	}
+
+	@bindThis
+	public packMany(
+		likes: any[],
+		me: { id: User['id'] },
+	) {
+		return Promise.all(likes.map(x => this.pack(x, me)));
+	}
+}
+
diff --git a/packages/backend/src/di-symbols.ts b/packages/backend/src/di-symbols.ts
index d2a361405f..9719d773ca 100644
--- a/packages/backend/src/di-symbols.ts
+++ b/packages/backend/src/di-symbols.ts
@@ -69,5 +69,7 @@ export const DI = {
 	adsRepository: Symbol('adsRepository'),
 	passwordResetRequestsRepository: Symbol('passwordResetRequestsRepository'),
 	retentionAggregationsRepository: Symbol('retentionAggregationsRepository'),
+	flashsRepository: Symbol('flashsRepository'),
+	flashLikesRepository: Symbol('flashLikesRepository'),
 	//#endregion
 };
diff --git a/packages/backend/src/misc/is-mime-image.ts b/packages/backend/src/misc/is-mime-image.ts
index 168a9a7af6..acf5c1ede3 100644
--- a/packages/backend/src/misc/is-mime-image.ts
+++ b/packages/backend/src/misc/is-mime-image.ts
@@ -3,6 +3,7 @@ import { FILE_TYPE_BROWSERSAFE } from '@/const.js';
 const dictionary = {
 	'safe-file': FILE_TYPE_BROWSERSAFE,
 	'sharp-convertible-image': ['image/jpeg', 'image/png', 'image/gif', 'image/apng', 'image/vnd.mozilla.apng', 'image/webp', 'image/avif', 'image/svg+xml'],
+	'sharp-animation-convertible-image': ['image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/avif', 'image/svg+xml'],
 };
 
 export const isMimeImage = (mime: string, type: keyof typeof dictionary): boolean => dictionary[type].includes(mime);
diff --git a/packages/backend/src/models/RepositoryModule.ts b/packages/backend/src/models/RepositoryModule.ts
index e22f0517ca..a5d5a63931 100644
--- a/packages/backend/src/models/RepositoryModule.ts
+++ b/packages/backend/src/models/RepositoryModule.ts
@@ -1,6 +1,6 @@
 import { Module } from '@nestjs/common';
 import { DI } from '@/di-symbols.js';
-import { User, Note, Announcement, AnnouncementRead, App, NoteFavorite, NoteThreadMuting, NoteReaction, NoteUnread, Notification, Poll, PollVote, UserProfile, UserKeypair, UserPending, AttestationChallenge, UserSecurityKey, UserPublickey, UserList, UserListJoining, UserGroup, UserGroupJoining, UserGroupInvitation, UserNotePining, UserIp, UsedUsername, Following, FollowRequest, Instance, Emoji, DriveFile, DriveFolder, Meta, Muting, Blocking, SwSubscription, Hashtag, AbuseUserReport, RegistrationTicket, AuthSession, AccessToken, Signin, MessagingMessage, Page, PageLike, GalleryPost, GalleryLike, ModerationLog, Clip, ClipNote, Antenna, AntennaNote, PromoNote, PromoRead, Relay, MutedNote, Channel, ChannelFollowing, ChannelNotePining, RegistryItem, Webhook, Ad, PasswordResetRequest, RetentionAggregation } from './index.js';
+import { User, Note, Announcement, AnnouncementRead, App, NoteFavorite, NoteThreadMuting, NoteReaction, NoteUnread, Notification, Poll, PollVote, UserProfile, UserKeypair, UserPending, AttestationChallenge, UserSecurityKey, UserPublickey, UserList, UserListJoining, UserGroup, UserGroupJoining, UserGroupInvitation, UserNotePining, UserIp, UsedUsername, Following, FollowRequest, Instance, Emoji, DriveFile, DriveFolder, Meta, Muting, Blocking, SwSubscription, Hashtag, AbuseUserReport, RegistrationTicket, AuthSession, AccessToken, Signin, MessagingMessage, Page, PageLike, GalleryPost, GalleryLike, ModerationLog, Clip, ClipNote, Antenna, AntennaNote, PromoNote, PromoRead, Relay, MutedNote, Channel, ChannelFollowing, ChannelNotePining, RegistryItem, Webhook, Ad, PasswordResetRequest, RetentionAggregation, FlashLike, Flash } from './index.js';
 import type { DataSource } from 'typeorm';
 import type { Provider } from '@nestjs/common';
 
@@ -388,6 +388,18 @@ const $retentionAggregationsRepository: Provider = {
 	inject: [DI.db],
 };
 
+const $flashsRepository: Provider = {
+	provide: DI.flashsRepository,
+	useFactory: (db: DataSource) => db.getRepository(Flash),
+	inject: [DI.db],
+};
+
+const $flashLikesRepository: Provider = {
+	provide: DI.flashLikesRepository,
+	useFactory: (db: DataSource) => db.getRepository(FlashLike),
+	inject: [DI.db],
+};
+
 @Module({
 	imports: [
 	],
@@ -456,6 +468,8 @@ const $retentionAggregationsRepository: Provider = {
 		$adsRepository,
 		$passwordResetRequestsRepository,
 		$retentionAggregationsRepository,
+		$flashsRepository,
+		$flashLikesRepository,
 	],
 	exports: [
 		$usersRepository,
@@ -522,6 +536,8 @@ const $retentionAggregationsRepository: Provider = {
 		$adsRepository,
 		$passwordResetRequestsRepository,
 		$retentionAggregationsRepository,
+		$flashsRepository,
+		$flashLikesRepository,
 	],
 })
 export class RepositoryModule {}
diff --git a/packages/backend/src/models/entities/Flash.ts b/packages/backend/src/models/entities/Flash.ts
new file mode 100644
index 0000000000..d9a6ac987c
--- /dev/null
+++ b/packages/backend/src/models/entities/Flash.ts
@@ -0,0 +1,60 @@
+import { Entity, Index, JoinColumn, Column, PrimaryColumn, ManyToOne } from 'typeorm';
+import { id } from '../id.js';
+import { User } from './User.js';
+import { DriveFile } from './DriveFile.js';
+
+@Entity()
+export class Flash {
+	@PrimaryColumn(id())
+	public id: string;
+
+	@Index()
+	@Column('timestamp with time zone', {
+		comment: 'The created date of the Flash.',
+	})
+	public createdAt: Date;
+
+	@Index()
+	@Column('timestamp with time zone', {
+		comment: 'The updated date of the Flash.',
+	})
+	public updatedAt: Date;
+
+	@Column('varchar', {
+		length: 256,
+	})
+	public title: string;
+
+	@Column('varchar', {
+		length: 1024,
+	})
+	public summary: string;
+
+	@Index()
+	@Column({
+		...id(),
+		comment: 'The ID of author.',
+	})
+	public userId: User['id'];
+
+	@ManyToOne(type => User, {
+		onDelete: 'CASCADE',
+	})
+	@JoinColumn()
+	public user: User | null;
+
+	@Column('varchar', {
+		length: 16384,
+	})
+	public script: string;
+
+	@Column('varchar', {
+		length: 256, array: true, default: '{}',
+	})
+	public permissions: string[];
+
+	@Column('integer', {
+		default: 0,
+	})
+	public likedCount: number;
+}
diff --git a/packages/backend/src/models/entities/FlashLike.ts b/packages/backend/src/models/entities/FlashLike.ts
new file mode 100644
index 0000000000..81d39191ca
--- /dev/null
+++ b/packages/backend/src/models/entities/FlashLike.ts
@@ -0,0 +1,33 @@
+import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
+import { id } from '../id.js';
+import { User } from './User.js';
+import { Flash } from './Flash.js';
+
+@Entity()
+@Index(['userId', 'flashId'], { unique: true })
+export class FlashLike {
+	@PrimaryColumn(id())
+	public id: string;
+
+	@Column('timestamp with time zone')
+	public createdAt: Date;
+
+	@Index()
+	@Column(id())
+	public userId: User['id'];
+
+	@ManyToOne(type => User, {
+		onDelete: 'CASCADE',
+	})
+	@JoinColumn()
+	public user: User | null;
+
+	@Column(id())
+	public flashId: Flash['id'];
+
+	@ManyToOne(type => Flash, {
+		onDelete: 'CASCADE',
+	})
+	@JoinColumn()
+	public flash: Flash | null;
+}
diff --git a/packages/backend/src/models/index.ts b/packages/backend/src/models/index.ts
index ca7a7c9e56..b132475747 100644
--- a/packages/backend/src/models/index.ts
+++ b/packages/backend/src/models/index.ts
@@ -62,6 +62,8 @@ import { UserSecurityKey } from '@/models/entities/UserSecurityKey.js';
 import { Webhook } from '@/models/entities/Webhook.js';
 import { Channel } from '@/models/entities/Channel.js';
 import { RetentionAggregation } from '@/models/entities/RetentionAggregation.js';
+import { Flash } from '@/models/entities/Flash.js';
+import { FlashLike } from '@/models/entities/FlashLike.js';
 import type { Repository } from 'typeorm';
 
 export {
@@ -129,6 +131,8 @@ export {
 	Webhook,
 	Channel,
 	RetentionAggregation,
+	Flash,
+	FlashLike,
 };
 
 export type AbuseUserReportsRepository = Repository<AbuseUserReport>;
@@ -195,3 +199,5 @@ export type UserSecurityKeysRepository = Repository<UserSecurityKey>;
 export type WebhooksRepository = Repository<Webhook>;
 export type ChannelsRepository = Repository<Channel>;
 export type RetentionAggregationsRepository = Repository<RetentionAggregation>;
+export type FlashsRepository = Repository<Flash>;
+export type FlashLikesRepository = Repository<FlashLike>;
diff --git a/packages/backend/src/postgre.ts b/packages/backend/src/postgre.ts
index 4b4490a0c3..4f6b157d80 100644
--- a/packages/backend/src/postgre.ts
+++ b/packages/backend/src/postgre.ts
@@ -70,6 +70,8 @@ import { UserSecurityKey } from '@/models/entities/UserSecurityKey.js';
 import { Webhook } from '@/models/entities/Webhook.js';
 import { Channel } from '@/models/entities/Channel.js';
 import { RetentionAggregation } from '@/models/entities/RetentionAggregation.js';
+import { Flash } from '@/models/entities/Flash.js';
+import { FlashLike } from '@/models/entities/FlashLike.js';
 
 import { Config } from '@/config.js';
 import MisskeyLogger from '@/logger.js';
@@ -184,6 +186,8 @@ export const entities = [
 	Webhook,
 	UserIp,
 	RetentionAggregation,
+	Flash,
+	FlashLike,
 	...charts,
 ];
 
diff --git a/packages/backend/src/server/MediaProxyServerService.ts b/packages/backend/src/server/MediaProxyServerService.ts
index 75b5ec6936..731486ac23 100644
--- a/packages/backend/src/server/MediaProxyServerService.ts
+++ b/packages/backend/src/server/MediaProxyServerService.ts
@@ -89,24 +89,33 @@ export class MediaProxyServerService {
 				}
 			}
 			const isConvertibleImage = isMimeImage(mime, 'sharp-convertible-image');
+			const isAnimationConvertibleImage = isMimeImage(mime, 'sharp-animation-convertible-image');
 
 			let image: IImageStreamable | null = null;
 			if ('emoji' in request.query && isConvertibleImage) {
-				const data = pipeline(
-					Readable.fromWeb(response.body),
-					sharp({ animated: !('static' in request.query) })
-						.resize({
-							height: 128,
-							withoutEnlargement: true,
-						})
-						.webp(webpDefault),
-				);
+				if (!isAnimationConvertibleImage && !('static' in request.query)) {
+					image = {
+						data: Readable.fromWeb(response.body),
+						ext,
+						type: mime,
+					};
+				} else {
+					const data = pipeline(
+						Readable.fromWeb(response.body),
+						sharp({ animated: !('static' in request.query) })
+							.resize({
+								height: 128,
+								withoutEnlargement: true,
+							})
+							.webp(webpDefault),
+					);
 
-				image = {
-					data,
-					ext: 'webp',
-					type: 'image/webp',
-				};
+					image = {
+						data,
+						ext: 'webp',
+						type: 'image/webp',
+					};
+				}
 			} else if ('static' in request.query && isConvertibleImage) {
 				image = this.imageProcessingService.convertSharpToWebpStreamObj(Readable.fromWeb(response.body).pipe(sharp()), 498, 280);
 			} else if ('preview' in request.query && isConvertibleImage) {
diff --git a/packages/backend/src/server/api/EndpointsModule.ts b/packages/backend/src/server/api/EndpointsModule.ts
index 32eff7f312..60beca4f47 100644
--- a/packages/backend/src/server/api/EndpointsModule.ts
+++ b/packages/backend/src/server/api/EndpointsModule.ts
@@ -266,6 +266,15 @@ import * as ep___pages_like from './endpoints/pages/like.js';
 import * as ep___pages_show from './endpoints/pages/show.js';
 import * as ep___pages_unlike from './endpoints/pages/unlike.js';
 import * as ep___pages_update from './endpoints/pages/update.js';
+import * as ep___flash_create from './endpoints/flash/create.js';
+import * as ep___flash_delete from './endpoints/flash/delete.js';
+import * as ep___flash_featured from './endpoints/flash/featured.js';
+import * as ep___flash_like from './endpoints/flash/like.js';
+import * as ep___flash_show from './endpoints/flash/show.js';
+import * as ep___flash_unlike from './endpoints/flash/unlike.js';
+import * as ep___flash_update from './endpoints/flash/update.js';
+import * as ep___flash_my from './endpoints/flash/my.js';
+import * as ep___flash_myLikes from './endpoints/flash/my-likes.js';
 import * as ep___ping from './endpoints/ping.js';
 import * as ep___pinnedUsers from './endpoints/pinned-users.js';
 import * as ep___promo_read from './endpoints/promo/read.js';
@@ -587,6 +596,15 @@ const $pages_like: Provider = { provide: 'ep:pages/like', useClass: ep___pages_l
 const $pages_show: Provider = { provide: 'ep:pages/show', useClass: ep___pages_show.default };
 const $pages_unlike: Provider = { provide: 'ep:pages/unlike', useClass: ep___pages_unlike.default };
 const $pages_update: Provider = { provide: 'ep:pages/update', useClass: ep___pages_update.default };
+const $flash_create: Provider = { provide: 'ep:flash/create', useClass: ep___flash_create.default };
+const $flash_delete: Provider = { provide: 'ep:flash/delete', useClass: ep___flash_delete.default };
+const $flash_featured: Provider = { provide: 'ep:flash/featured', useClass: ep___flash_featured.default };
+const $flash_like: Provider = { provide: 'ep:flash/like', useClass: ep___flash_like.default };
+const $flash_show: Provider = { provide: 'ep:flash/show', useClass: ep___flash_show.default };
+const $flash_unlike: Provider = { provide: 'ep:flash/unlike', useClass: ep___flash_unlike.default };
+const $flash_update: Provider = { provide: 'ep:flash/update', useClass: ep___flash_update.default };
+const $flash_my: Provider = { provide: 'ep:flash/my', useClass: ep___flash_my.default };
+const $flash_myLikes: Provider = { provide: 'ep:flash/my-likes', useClass: ep___flash_myLikes.default };
 const $ping: Provider = { provide: 'ep:ping', useClass: ep___ping.default };
 const $pinnedUsers: Provider = { provide: 'ep:pinned-users', useClass: ep___pinnedUsers.default };
 const $promo_read: Provider = { provide: 'ep:promo/read', useClass: ep___promo_read.default };
@@ -912,6 +930,15 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
 		$pages_show,
 		$pages_unlike,
 		$pages_update,
+		$flash_create,
+		$flash_delete,
+		$flash_featured,
+		$flash_like,
+		$flash_show,
+		$flash_unlike,
+		$flash_update,
+		$flash_my,
+		$flash_myLikes,
 		$ping,
 		$pinnedUsers,
 		$promo_read,
@@ -1231,6 +1258,15 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
 		$pages_show,
 		$pages_unlike,
 		$pages_update,
+		$flash_create,
+		$flash_delete,
+		$flash_featured,
+		$flash_like,
+		$flash_show,
+		$flash_unlike,
+		$flash_update,
+		$flash_my,
+		$flash_myLikes,
 		$ping,
 		$pinnedUsers,
 		$promo_read,
diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts
index 49dc3b224f..d4f8be5b85 100644
--- a/packages/backend/src/server/api/endpoints.ts
+++ b/packages/backend/src/server/api/endpoints.ts
@@ -265,6 +265,15 @@ import * as ep___pages_like from './endpoints/pages/like.js';
 import * as ep___pages_show from './endpoints/pages/show.js';
 import * as ep___pages_unlike from './endpoints/pages/unlike.js';
 import * as ep___pages_update from './endpoints/pages/update.js';
+import * as ep___flash_create from './endpoints/flash/create.js';
+import * as ep___flash_delete from './endpoints/flash/delete.js';
+import * as ep___flash_featured from './endpoints/flash/featured.js';
+import * as ep___flash_like from './endpoints/flash/like.js';
+import * as ep___flash_show from './endpoints/flash/show.js';
+import * as ep___flash_unlike from './endpoints/flash/unlike.js';
+import * as ep___flash_update from './endpoints/flash/update.js';
+import * as ep___flash_my from './endpoints/flash/my.js';
+import * as ep___flash_myLikes from './endpoints/flash/my-likes.js';
 import * as ep___ping from './endpoints/ping.js';
 import * as ep___pinnedUsers from './endpoints/pinned-users.js';
 import * as ep___promo_read from './endpoints/promo/read.js';
@@ -584,6 +593,15 @@ const eps = [
 	['pages/show', ep___pages_show],
 	['pages/unlike', ep___pages_unlike],
 	['pages/update', ep___pages_update],
+	['flash/create', ep___flash_create],
+	['flash/delete', ep___flash_delete],
+	['flash/featured', ep___flash_featured],
+	['flash/like', ep___flash_like],
+	['flash/show', ep___flash_show],
+	['flash/unlike', ep___flash_unlike],
+	['flash/update', ep___flash_update],
+	['flash/my', ep___flash_my],
+	['flash/my-likes', ep___flash_myLikes],
 	['ping', ep___ping],
 	['pinned-users', ep___pinnedUsers],
 	['promo/read', ep___promo_read],
diff --git a/packages/backend/src/server/api/endpoints/admin/drive/files.ts b/packages/backend/src/server/api/endpoints/admin/drive/files.ts
index 53a37cb691..8a4498d5fa 100644
--- a/packages/backend/src/server/api/endpoints/admin/drive/files.ts
+++ b/packages/backend/src/server/api/endpoints/admin/drive/files.ts
@@ -8,7 +8,7 @@ import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.j
 export const meta = {
 	tags: ['admin'],
 
-	requireCredential: false,
+	requireCredential: true,
 	requireModerator: true,
 
 	res: {
diff --git a/packages/backend/src/server/api/endpoints/federation/instances.ts b/packages/backend/src/server/api/endpoints/federation/instances.ts
index 180887285a..5e2f204661 100644
--- a/packages/backend/src/server/api/endpoints/federation/instances.ts
+++ b/packages/backend/src/server/api/endpoints/federation/instances.ts
@@ -64,8 +64,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
 				case '-followers': query.orderBy('instance.followersCount', 'ASC'); break;
 				case '+caughtAt': query.orderBy('instance.caughtAt', 'DESC'); break;
 				case '-caughtAt': query.orderBy('instance.caughtAt', 'ASC'); break;
-				case '+latestRequestReceivedAt': query.orderBy('instance.latestRequestReceivedAt', 'DESC'); break;
-				case '-latestRequestReceivedAt': query.orderBy('instance.latestRequestReceivedAt', 'ASC'); break;
+				case '+latestRequestReceivedAt': query.orderBy('instance.latestRequestReceivedAt', 'DESC', 'NULLS LAST'); break;
+				case '-latestRequestReceivedAt': query.orderBy('instance.latestRequestReceivedAt', 'ASC', 'NULLS FIRST'); break;
 
 				default: query.orderBy('instance.id', 'DESC'); break;
 			}
diff --git a/packages/backend/src/server/api/endpoints/flash/create.ts b/packages/backend/src/server/api/endpoints/flash/create.ts
new file mode 100644
index 0000000000..a652047d98
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/flash/create.ts
@@ -0,0 +1,66 @@
+import ms from 'ms';
+import { Inject, Injectable } from '@nestjs/common';
+import type { DriveFilesRepository, FlashsRepository, PagesRepository } from '@/models/index.js';
+import { IdService } from '@/core/IdService.js';
+import { Page } from '@/models/entities/Page.js';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import { PageEntityService } from '@/core/entities/PageEntityService.js';
+import { DI } from '@/di-symbols.js';
+import { FlashEntityService } from '@/core/entities/FlashEntityService.js';
+import { ApiError } from '../../error.js';
+
+export const meta = {
+	tags: ['flash'],
+
+	requireCredential: true,
+
+	kind: 'write:flash',
+
+	limit: {
+		duration: ms('1hour'),
+		max: 10,
+	},
+
+	errors: {
+	},
+} as const;
+
+export const paramDef = {
+	type: 'object',
+	properties: {
+		title: { type: 'string' },
+		summary: { type: 'string' },
+		script: { type: 'string' },
+		permissions: { type: 'array', items: {
+			type: 'string',
+		} },
+	},
+	required: ['title', 'summary', 'script', 'permissions'],
+} as const;
+
+// eslint-disable-next-line import/no-default-export
+@Injectable()
+export default class extends Endpoint<typeof meta, typeof paramDef> {
+	constructor(
+		@Inject(DI.flashsRepository)
+		private flashsRepository: FlashsRepository,
+
+		private flashEntityService: FlashEntityService,
+		private idService: IdService,
+	) {
+		super(meta, paramDef, async (ps, me) => {
+			const flash = await this.flashsRepository.insert({
+				id: this.idService.genId(),
+				userId: me.id,
+				createdAt: new Date(),
+				updatedAt: new Date(),
+				title: ps.title,
+				summary: ps.summary,
+				script: ps.script,
+				permissions: ps.permissions,
+			}).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
new file mode 100644
index 0000000000..e94ede9f68
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/flash/delete.ts
@@ -0,0 +1,56 @@
+import { Inject, Injectable } from '@nestjs/common';
+import type { FlashsRepository } from '@/models/index.js';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import { DI } from '@/di-symbols.js';
+import { ApiError } from '../../error.js';
+
+export const meta = {
+	tags: ['flashs'],
+
+	requireCredential: true,
+
+	kind: 'write:flash',
+
+	errors: {
+		noSuchFlash: {
+			message: 'No such flash.',
+			code: 'NO_SUCH_FLASH',
+			id: 'de1623ef-bbb3-4289-a71e-14cfa83d9740',
+		},
+
+		accessDenied: {
+			message: 'Access denied.',
+			code: 'ACCESS_DENIED',
+			id: '1036ad7b-9f92-4fff-89c3-0e50dc941704',
+		},
+	},
+} as const;
+
+export const paramDef = {
+	type: 'object',
+	properties: {
+		flashId: { type: 'string', format: 'misskey:id' },
+	},
+	required: ['flashId'],
+} as const;
+
+// eslint-disable-next-line import/no-default-export
+@Injectable()
+export default class extends Endpoint<typeof meta, typeof paramDef> {
+	constructor(
+		@Inject(DI.flashsRepository)
+		private flashsRepository: FlashsRepository,
+	) {
+		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) {
+				throw new ApiError(meta.errors.accessDenied);
+			}
+
+			await this.flashsRepository.delete(flash.id);
+		});
+	}
+}
diff --git a/packages/backend/src/server/api/endpoints/flash/featured.ts b/packages/backend/src/server/api/endpoints/flash/featured.ts
new file mode 100644
index 0000000000..570aef96d2
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/flash/featured.ts
@@ -0,0 +1,48 @@
+import { Inject, Injectable } from '@nestjs/common';
+import type { FlashsRepository } from '@/models/index.js';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import { FlashEntityService } from '@/core/entities/FlashEntityService.js';
+import { DI } from '@/di-symbols.js';
+
+export const meta = {
+	tags: ['flash'],
+
+	requireCredential: false,
+
+	res: {
+		type: 'array',
+		optional: false, nullable: false,
+		items: {
+			type: 'object',
+			optional: false, nullable: false,
+			ref: 'Flash',
+		},
+	},
+} as const;
+
+export const paramDef = {
+	type: 'object',
+	properties: {},
+	required: [],
+} as const;
+
+// eslint-disable-next-line import/no-default-export
+@Injectable()
+export default class extends Endpoint<typeof meta, typeof paramDef> {
+	constructor(
+		@Inject(DI.flashsRepository)
+		private flashsRepository: FlashsRepository,
+
+		private flashEntityService: FlashEntityService,
+	) {
+		super(meta, paramDef, async (ps, me) => {
+			const query = this.flashsRepository.createQueryBuilder('flash')
+				.andWhere('flash.likedCount > 0')
+				.orderBy('flash.likedCount', 'DESC');
+
+			const flashs = await query.take(10).getMany();
+
+			return await this.flashEntityService.packMany(flashs, me);
+		});
+	}
+}
diff --git a/packages/backend/src/server/api/endpoints/flash/like.ts b/packages/backend/src/server/api/endpoints/flash/like.ts
new file mode 100644
index 0000000000..5581b8ec60
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/flash/like.ts
@@ -0,0 +1,87 @@
+import { Inject, Injectable } from '@nestjs/common';
+import type { FlashsRepository, FlashLikesRepository } from '@/models/index.js';
+import { IdService } from '@/core/IdService.js';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import { DI } from '@/di-symbols.js';
+import { ApiError } from '../../error.js';
+
+export const meta = {
+	tags: ['flash'],
+
+	requireCredential: true,
+
+	kind: 'write:flash-likes',
+
+	errors: {
+		noSuchFlash: {
+			message: 'No such flash.',
+			code: 'NO_SUCH_FLASH',
+			id: 'c07c1491-9161-4c5c-9d75-01906f911f73',
+		},
+
+		yourFlash: {
+			message: 'You cannot like your flash.',
+			code: 'YOUR_FLASH',
+			id: '3fd8a0e7-5955-4ba9-85bb-bf3e0c30e13b',
+		},
+
+		alreadyLiked: {
+			message: 'The flash has already been liked.',
+			code: 'ALREADY_LIKED',
+			id: '010065cf-ad43-40df-8067-abff9f4686e3',
+		},
+	},
+} as const;
+
+export const paramDef = {
+	type: 'object',
+	properties: {
+		flashId: { type: 'string', format: 'misskey:id' },
+	},
+	required: ['flashId'],
+} as const;
+
+// eslint-disable-next-line import/no-default-export
+@Injectable()
+export default class extends Endpoint<typeof meta, typeof paramDef> {
+	constructor(
+		@Inject(DI.flashsRepository)
+		private flashsRepository: FlashsRepository,
+
+		@Inject(DI.flashLikesRepository)
+		private flashLikesRepository: FlashLikesRepository,
+
+		private idService: IdService,
+	) {
+		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) {
+				throw new ApiError(meta.errors.yourFlash);
+			}
+
+			// if already liked
+			const exist = await this.flashLikesRepository.findOneBy({
+				flashId: flash.id,
+				userId: me.id,
+			});
+
+			if (exist != null) {
+				throw new ApiError(meta.errors.alreadyLiked);
+			}
+
+			// Create like
+			await this.flashLikesRepository.insert({
+				id: this.idService.genId(),
+				createdAt: new Date(),
+				flashId: flash.id,
+				userId: me.id,
+			});
+
+			this.flashsRepository.increment({ id: flash.id }, 'likedCount', 1);
+		});
+	}
+}
diff --git a/packages/backend/src/server/api/endpoints/flash/my-likes.ts b/packages/backend/src/server/api/endpoints/flash/my-likes.ts
new file mode 100644
index 0000000000..f7716ea74a
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/flash/my-likes.ts
@@ -0,0 +1,68 @@
+import { Inject, Injectable } from '@nestjs/common';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import type { FlashLikesRepository } from '@/models/index.js';
+import { QueryService } from '@/core/QueryService.js';
+import { FlashLikeEntityService } from '@/core/entities/FlashLikeEntityService.js';
+import { DI } from '@/di-symbols.js';
+
+export const meta = {
+	tags: ['account', 'flash'],
+
+	requireCredential: true,
+
+	kind: 'read:flash-likes',
+
+	res: {
+		type: 'array',
+		optional: false, nullable: false,
+		items: {
+			type: 'object',
+			properties: {
+				id: {
+					type: 'string',
+					optional: false, nullable: false,
+					format: 'id',
+				},
+				flash: {
+					type: 'object',
+					optional: false, nullable: false,
+					ref: 'Flash',
+				},
+			},
+		},
+	},
+} as const;
+
+export const paramDef = {
+	type: 'object',
+	properties: {
+		limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
+		sinceId: { type: 'string', format: 'misskey:id' },
+		untilId: { type: 'string', format: 'misskey:id' },
+	},
+	required: [],
+} as const;
+
+// eslint-disable-next-line import/no-default-export
+@Injectable()
+export default class extends Endpoint<typeof meta, typeof paramDef> {
+	constructor(
+		@Inject(DI.flashLikesRepository)
+		private flashLikesRepository: FlashLikesRepository,
+
+		private flashLikeEntityService: FlashLikeEntityService,
+		private queryService: QueryService,
+	) {
+		super(meta, paramDef, async (ps, me) => {
+			const query = this.queryService.makePaginationQuery(this.flashLikesRepository.createQueryBuilder('like'), ps.sinceId, ps.untilId)
+				.andWhere('like.userId = :meId', { meId: me.id })
+				.leftJoinAndSelect('like.flash', 'flash');
+
+			const likes = await query
+				.take(ps.limit)
+				.getMany();
+
+			return this.flashLikeEntityService.packMany(likes, me);
+		});
+	}
+}
diff --git a/packages/backend/src/server/api/endpoints/flash/my.ts b/packages/backend/src/server/api/endpoints/flash/my.ts
new file mode 100644
index 0000000000..baed7f000f
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/flash/my.ts
@@ -0,0 +1,57 @@
+import { Inject, Injectable } from '@nestjs/common';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import type { FlashsRepository } from '@/models/index.js';
+import { QueryService } from '@/core/QueryService.js';
+import { FlashEntityService } from '@/core/entities/FlashEntityService.js';
+import { DI } from '@/di-symbols.js';
+
+export const meta = {
+	tags: ['account', 'flash'],
+
+	requireCredential: true,
+
+	kind: 'read:flash',
+
+	res: {
+		type: 'array',
+		optional: false, nullable: false,
+		items: {
+			type: 'object',
+			optional: false, nullable: false,
+			ref: 'Flash',
+		},
+	},
+} as const;
+
+export const paramDef = {
+	type: 'object',
+	properties: {
+		limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
+		sinceId: { type: 'string', format: 'misskey:id' },
+		untilId: { type: 'string', format: 'misskey:id' },
+	},
+	required: [],
+} as const;
+
+// eslint-disable-next-line import/no-default-export
+@Injectable()
+export default class extends Endpoint<typeof meta, typeof paramDef> {
+	constructor(
+		@Inject(DI.flashsRepository)
+		private flashsRepository: FlashsRepository,
+
+		private flashEntityService: FlashEntityService,
+		private queryService: QueryService,
+	) {
+		super(meta, paramDef, async (ps, me) => {
+			const query = this.queryService.makePaginationQuery(this.flashsRepository.createQueryBuilder('flash'), ps.sinceId, ps.untilId)
+				.andWhere('flash.userId = :meId', { meId: me.id });
+
+			const flashs = await query
+				.take(ps.limit)
+				.getMany();
+
+			return await this.flashEntityService.packMany(flashs);
+		});
+	}
+}
diff --git a/packages/backend/src/server/api/endpoints/flash/show.ts b/packages/backend/src/server/api/endpoints/flash/show.ts
new file mode 100644
index 0000000000..48114c5a60
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/flash/show.ts
@@ -0,0 +1,60 @@
+import { IsNull } from 'typeorm';
+import { Inject, Injectable } from '@nestjs/common';
+import type { UsersRepository, FlashsRepository } from '@/models/index.js';
+import type { Flash } from '@/models/entities/Flash.js';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import { FlashEntityService } from '@/core/entities/FlashEntityService.js';
+import { DI } from '@/di-symbols.js';
+import { ApiError } from '../../error.js';
+
+export const meta = {
+	tags: ['flashs'],
+
+	requireCredential: false,
+
+	res: {
+		type: 'object',
+		optional: false, nullable: false,
+		ref: 'Flash',
+	},
+
+	errors: {
+		noSuchFlash: {
+			message: 'No such flash.',
+			code: 'NO_SUCH_FLASH',
+			id: 'f0d34a1a-d29a-401d-90ba-1982122b5630',
+		},
+	},
+} as const;
+
+export const paramDef = {
+	type: 'object',
+	properties: {
+		flashId: { type: 'string', format: 'misskey:id' },
+	},
+	required: ['flashId'],
+} as const;
+
+// eslint-disable-next-line import/no-default-export
+@Injectable()
+export default class extends Endpoint<typeof meta, typeof paramDef> {
+	constructor(
+		@Inject(DI.usersRepository)
+		private usersRepository: UsersRepository,
+
+		@Inject(DI.flashsRepository)
+		private flashsRepository: FlashsRepository,
+
+		private flashEntityService: FlashEntityService,
+	) {
+		super(meta, paramDef, async (ps, me) => {
+			const flash = await this.flashsRepository.findOneBy({ id: ps.flashId });
+
+			if (flash == null) {
+				throw new ApiError(meta.errors.noSuchFlash);
+			}
+
+			return await this.flashEntityService.pack(flash, me);
+		});
+	}
+}
diff --git a/packages/backend/src/server/api/endpoints/flash/unlike.ts b/packages/backend/src/server/api/endpoints/flash/unlike.ts
new file mode 100644
index 0000000000..b994f5d347
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/flash/unlike.ts
@@ -0,0 +1,68 @@
+import { Inject, Injectable } from '@nestjs/common';
+import type { FlashsRepository, FlashLikesRepository } from '@/models/index.js';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import { DI } from '@/di-symbols.js';
+import { ApiError } from '../../error.js';
+
+export const meta = {
+	tags: ['flash'],
+
+	requireCredential: true,
+
+	kind: 'write:flash-likes',
+
+	errors: {
+		noSuchFlash: {
+			message: 'No such flash.',
+			code: 'NO_SUCH_FLASH',
+			id: 'afe8424a-a69e-432d-a5f2-2f0740c62410',
+		},
+
+		notLiked: {
+			message: 'You have not liked that flash.',
+			code: 'NOT_LIKED',
+			id: '755f25a7-9871-4f65-9f34-51eaad9ae0ac',
+		},
+	},
+} as const;
+
+export const paramDef = {
+	type: 'object',
+	properties: {
+		flashId: { type: 'string', format: 'misskey:id' },
+	},
+	required: ['flashId'],
+} as const;
+
+// eslint-disable-next-line import/no-default-export
+@Injectable()
+export default class extends Endpoint<typeof meta, typeof paramDef> {
+	constructor(
+		@Inject(DI.flashsRepository)
+		private flashsRepository: FlashsRepository,
+
+		@Inject(DI.flashLikesRepository)
+		private flashLikesRepository: FlashLikesRepository,
+	) {
+		super(meta, paramDef, async (ps, me) => {
+			const flash = await this.flashsRepository.findOneBy({ id: ps.flashId });
+			if (flash == null) {
+				throw new ApiError(meta.errors.noSuchFlash);
+			}
+
+			const exist = await this.flashLikesRepository.findOneBy({
+				flashId: flash.id,
+				userId: me.id,
+			});
+
+			if (exist == null) {
+				throw new ApiError(meta.errors.notLiked);
+			}
+
+			// Delete like
+			await this.flashLikesRepository.delete(exist.id);
+
+			this.flashsRepository.decrement({ id: flash.id }, 'likedCount', 1);
+		});
+	}
+}
diff --git a/packages/backend/src/server/api/endpoints/flash/update.ts b/packages/backend/src/server/api/endpoints/flash/update.ts
new file mode 100644
index 0000000000..9ab17a61e8
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/flash/update.ts
@@ -0,0 +1,78 @@
+import ms from 'ms';
+import { Not } from 'typeorm';
+import { Inject, Injectable } from '@nestjs/common';
+import type { FlashsRepository, DriveFilesRepository } from '@/models/index.js';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import { DI } from '@/di-symbols.js';
+import { ApiError } from '../../error.js';
+
+export const meta = {
+	tags: ['flash'],
+
+	requireCredential: true,
+
+	kind: 'write:flash',
+
+	limit: {
+		duration: ms('1hour'),
+		max: 300,
+	},
+
+	errors: {
+		noSuchFlash: {
+			message: 'No such flash.',
+			code: 'NO_SUCH_FLASH',
+			id: '611e13d2-309e-419a-a5e4-e0422da39b02',
+		},
+
+		accessDenied: {
+			message: 'Access denied.',
+			code: 'ACCESS_DENIED',
+			id: '08e60c88-5948-478e-a132-02ec701d67b2',
+		},
+	},
+} as const;
+
+export const paramDef = {
+	type: 'object',
+	properties: {
+		flashId: { type: 'string', format: 'misskey:id' },
+		title: { type: 'string' },
+		summary: { type: 'string' },
+		script: { type: 'string' },
+		permissions: { type: 'array', items: {
+			type: 'string',
+		} },
+	},
+	required: ['flashId', 'title', 'summary', 'script', 'permissions'],
+} as const;
+
+// eslint-disable-next-line import/no-default-export
+@Injectable()
+export default class extends Endpoint<typeof meta, typeof paramDef> {
+	constructor(
+		@Inject(DI.flashsRepository)
+		private flashsRepository: FlashsRepository,
+
+		@Inject(DI.driveFilesRepository)
+		private driveFilesRepository: DriveFilesRepository,
+	) {
+		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) {
+				throw new ApiError(meta.errors.accessDenied);
+			}
+
+			await this.flashsRepository.update(flash.id, {
+				updatedAt: new Date(),
+				title: ps.title,
+				summary: ps.summary,
+				script: ps.script,
+				permissions: ps.permissions,
+			});
+		});
+	}
+}
diff --git a/packages/backend/src/server/api/endpoints/pages/like.ts b/packages/backend/src/server/api/endpoints/pages/like.ts
index 41a11d1a31..d27990f7e1 100644
--- a/packages/backend/src/server/api/endpoints/pages/like.ts
+++ b/packages/backend/src/server/api/endpoints/pages/like.ts
@@ -28,7 +28,7 @@ export const meta = {
 		alreadyLiked: {
 			message: 'The page has already been liked.',
 			code: 'ALREADY_LIKED',
-			id: 'cc98a8a2-0dc3-4123-b198-62c71df18ed3',
+			id: 'd4c1edbe-7da2-4eae-8714-1acfd2d63941',
 		},
 	},
 } as const;
diff --git a/packages/backend/src/server/api/endpoints/pages/update.ts b/packages/backend/src/server/api/endpoints/pages/update.ts
index 4db0f80b26..35b402ec56 100644
--- a/packages/backend/src/server/api/endpoints/pages/update.ts
+++ b/packages/backend/src/server/api/endpoints/pages/update.ts
@@ -111,7 +111,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
 				updatedAt: new Date(),
 				title: ps.title,
 				name: ps.name === undefined ? page.name : ps.name,
-				summary: ps.name === undefined ? page.summary : ps.summary,
+				summary: ps.summary === undefined ? page.summary : ps.summary,
 				content: ps.content,
 				variables: ps.variables,
 				script: ps.script,
diff --git a/packages/backend/src/server/web/ClientServerService.ts b/packages/backend/src/server/web/ClientServerService.ts
index 7ef178403b..a7701e1b24 100644
--- a/packages/backend/src/server/web/ClientServerService.ts
+++ b/packages/backend/src/server/web/ClientServerService.ts
@@ -26,9 +26,10 @@ import { PageEntityService } from '@/core/entities/PageEntityService.js';
 import { GalleryPostEntityService } from '@/core/entities/GalleryPostEntityService.js';
 import { ClipEntityService } from '@/core/entities/ClipEntityService.js';
 import { ChannelEntityService } from '@/core/entities/ChannelEntityService.js';
-import type { ChannelsRepository, ClipsRepository, EmojisRepository, GalleryPostsRepository, NotesRepository, PagesRepository, UserProfilesRepository, UsersRepository } from '@/models/index.js';
+import type { ChannelsRepository, ClipsRepository, EmojisRepository, FlashsRepository, GalleryPostsRepository, NotesRepository, PagesRepository, UserProfilesRepository, UsersRepository } from '@/models/index.js';
 import { deepClone } from '@/misc/clone.js';
 import { bindThis } from '@/decorators.js';
+import { FlashEntityService } from '@/core/entities/FlashEntityService.js';
 import manifest from './manifest.json' assert { type: 'json' };
 import { FeedService } from './FeedService.js';
 import { UrlPreviewService } from './UrlPreviewService.js';
@@ -73,6 +74,10 @@ export class ClientServerService {
 		@Inject(DI.emojisRepository)
 		private emojisRepository: EmojisRepository,
 
+		@Inject(DI.flashsRepository)
+		private flashsRepository: FlashsRepository,
+
+		private flashEntityService: FlashEntityService,
 		private userEntityService: UserEntityService,
 		private noteEntityService: NoteEntityService,
 		private pageEntityService: PageEntityService,
@@ -352,7 +357,7 @@ export class ClientServerService {
 			const name = meta.name || 'Misskey';
 			let content = '';
 			content += '<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/" xmlns:moz="http://www.mozilla.org/2006/browser/search/">';
-			content += `<ShortName>${name} Search</ShortName>`;
+			content += `<ShortName>${name}</ShortName>`;
 			content += `<Description>${name} Search</Description>`;
 			content += '<InputEncoding>UTF-8</InputEncoding>';
 			content += `<Image width="16" height="16" type="image/x-icon">${this.config.url}/favicon.ico</Image>`;
@@ -545,6 +550,30 @@ export class ClientServerService {
 			}
 		});
 
+		// Flash
+		fastify.get<{ Params: { id: string; } }>('/play/:id', async (request, reply) => {
+			const flash = await this.flashsRepository.findOneBy({
+				id: request.params.id,
+			});
+
+			if (flash) {
+				const _flash = await this.flashEntityService.pack(flash);
+				const profile = await this.userProfilesRepository.findOneByOrFail({ userId: flash.userId });
+				const meta = await this.metaService.fetch();
+				reply.header('Cache-Control', 'public, max-age=15');
+				return await reply.view('flash', {
+					flash: _flash,
+					profile,
+					avatarUrl: await this.userEntityService.getAvatarUrl(await this.usersRepository.findOneByOrFail({ id: flash.userId })),
+					instanceName: meta.name ?? 'Misskey',
+					icon: meta.iconUrl,
+					themeColor: meta.themeColor,
+				});
+			} else {
+				return await renderBase(reply);
+			}
+		});
+
 		// Clip
 		// TODO: 非publicなclipのハンドリング
 		fastify.get<{ Params: { clip: string; } }>('/clips/:clip', async (request, reply) => {
diff --git a/packages/backend/src/server/web/views/base.pug b/packages/backend/src/server/web/views/base.pug
index b472cff899..b27bbcbce0 100644
--- a/packages/backend/src/server/web/views/base.pug
+++ b/packages/backend/src/server/web/views/base.pug
@@ -31,7 +31,7 @@ html
 		link(rel='icon' href= icon || '/favicon.ico')
 		link(rel='apple-touch-icon' href= icon || '/apple-touch-icon.png')
 		link(rel='manifest' href='/manifest.json')
-		link(rel='search' type='application/opensearchdescription+xml' title=((title || "Misskey") + " Search") href=`${url}/opensearch.xml`)
+		link(rel='search' type='application/opensearchdescription+xml' title=(title || "Misskey") href=`${url}/opensearch.xml`)
 		link(rel='prefetch' href='https://xn--931a.moe/assets/info.jpg')
 		link(rel='prefetch' href='https://xn--931a.moe/assets/not-found.jpg')
 		link(rel='prefetch' href='https://xn--931a.moe/assets/error.jpg')
diff --git a/packages/backend/src/server/web/views/flash.pug b/packages/backend/src/server/web/views/flash.pug
new file mode 100644
index 0000000000..5166855ea2
--- /dev/null
+++ b/packages/backend/src/server/web/views/flash.pug
@@ -0,0 +1,31 @@
+extends ./base
+
+block vars
+	- const user = flash.user;
+	- const title = flash.title;
+	- const url = `${config.url}/play/${flash.id}`;
+
+block title
+	= `${title} | ${instanceName}`
+
+block desc
+	meta(name='description' content= flash.summary)
+
+block og
+	meta(property='og:type'        content='article')
+	meta(property='og:title'       content= title)
+	meta(property='og:description' content= flash.summary)
+	meta(property='og:url'         content= url)
+	meta(property='og:image'       content= avatarUrl)
+
+block meta
+	if profile.noCrawle
+		meta(name='robots' content='noindex')
+
+	meta(name='misskey:user-username' content=user.username)
+	meta(name='misskey:user-id' content=user.id)
+	meta(name='misskey:flash-id' content=flash.id)
+
+	// todo
+	if user.twitter
+		meta(name='twitter:creator' content=`@${user.twitter.screenName}`)
diff --git a/packages/frontend/package.json b/packages/frontend/package.json
index 2506e8e9d3..0dec916fa0 100644
--- a/packages/frontend/package.json
+++ b/packages/frontend/package.json
@@ -11,7 +11,7 @@
 		"@rollup/plugin-alias": "4.0.2",
 		"@rollup/plugin-json": "6.0.0",
 		"@rollup/pluginutils": "5.0.2",
-		"@syuilo/aiscript": "0.12.0",
+		"@syuilo/aiscript": "0.12.1",
 		"@tabler/icons": "^1.118.0",
 		"@vitejs/plugin-vue": "4.0.0",
 		"@vue/compiler-sfc": "3.2.45",
@@ -20,7 +20,8 @@
 		"blurhash": "2.0.4",
 		"broadcast-channel": "4.19.1",
 		"browser-image-resizer": "git+https://github.com/misskey-dev/browser-image-resizer#v2.2.1-misskey.3",
-		"chart.js": "4.1.1",
+		"canvas-confetti": "^1.6.0",
+		"chart.js": "4.1.2",
 		"chartjs-adapter-date-fns": "3.0.0",
 		"chartjs-chart-matrix": "^1.3.0",
 		"chartjs-plugin-gradient": "0.6.1",
@@ -35,7 +36,7 @@
 		"insert-text-at-cursor": "0.3.0",
 		"is-file-animated": "1.0.2",
 		"json5": "2.2.3",
-		"katex": "0.15.6",
+		"katex": "0.16.4",
 		"matter-js": "0.18.0",
 		"mfm-js": "0.23.0",
 		"misskey-js": "0.0.14",
@@ -44,7 +45,7 @@
 		"punycode": "2.1.1",
 		"querystring": "0.2.1",
 		"rndstr": "1.0.0",
-		"rollup": "3.9.0",
+		"rollup": "3.9.1",
 		"s-age": "1.1.2",
 		"sanitize-html": "^2.8.1",
 		"sass": "1.57.1",
@@ -55,14 +56,14 @@
 		"textarea-caret": "3.1.0",
 		"three": "0.148.0",
 		"throttle-debounce": "5.0.0",
-		"tinycolor2": "1.5.1",
+		"tinycolor2": "1.5.2",
 		"tsc-alias": "1.8.2",
 		"tsconfig-paths": "4.1.2",
 		"twemoji-parser": "14.0.0",
 		"typescript": "4.9.4",
 		"uuid": "9.0.0",
 		"vanilla-tilt": "1.8.0",
-		"vite": "4.0.3",
+		"vite": "4.0.4",
 		"vue": "3.2.45",
 		"vue-prism-editor": "2.0.0-alpha.2",
 		"vuedraggable": "next"
@@ -72,7 +73,7 @@
 		"@types/glob": "8.0.0",
 		"@types/gulp": "4.0.10",
 		"@types/gulp-rename": "2.0.1",
-		"@types/katex": "0.14.0",
+		"@types/katex": "0.16.0",
 		"@types/matter-js": "0.18.2",
 		"@types/punycode": "2.1.0",
 		"@types/sanitize-html": "^2.8.0",
@@ -82,16 +83,16 @@
 		"@types/uuid": "9.0.0",
 		"@types/websocket": "1.0.5",
 		"@types/ws": "8.5.4",
-		"@typescript-eslint/eslint-plugin": "5.47.1",
-		"@typescript-eslint/parser": "5.47.1",
+		"@typescript-eslint/eslint-plugin": "5.48.0",
+		"@typescript-eslint/parser": "5.48.0",
 		"@vue/runtime-core": "3.2.45",
 		"cross-env": "7.0.3",
-		"cypress": "12.2.0",
+		"cypress": "12.3.0",
 		"eslint": "8.31.0",
 		"eslint-plugin-import": "2.26.0",
 		"eslint-plugin-vue": "9.8.0",
 		"start-server-and-test": "1.15.2",
 		"vue-eslint-parser": "^9.1.0",
-		"vue-tsc": "^1.0.19"
+		"vue-tsc": "^1.0.22"
 	}
 }
diff --git a/packages/frontend/src/account.ts b/packages/frontend/src/account.ts
index 0e991cdfb5..93916ccf2f 100644
--- a/packages/frontend/src/account.ts
+++ b/packages/frontend/src/account.ts
@@ -6,12 +6,13 @@ import { del, get, set } from '@/scripts/idb-proxy';
 import { apiUrl } from '@/config';
 import { waiting, api, popup, popupMenu, success, alert } from '@/os';
 import { unisonReload, reloadChannel } from '@/scripts/unison-reload';
+import { miLocalStorage } from './local-storage';
 
 // TODO: 他のタブと永続化されたstateを同期
 
 type Account = misskey.entities.MeDetailed;
 
-const accountData = localStorage.getItem('account');
+const accountData = miLocalStorage.getItem('account');
 
 // TODO: 外部からはreadonlyに
 export const $i = accountData ? reactive(JSON.parse(accountData) as Account) : null;
@@ -21,7 +22,7 @@ export const iAmAdmin = $i != null && $i.isAdmin;
 
 export async function signout() {
 	waiting();
-	localStorage.removeItem('account');
+	miLocalStorage.removeItem('account');
 
 	await removeAccount($i.id);
 
@@ -119,7 +120,7 @@ export function updateAccount(accountData) {
 	for (const [key, value] of Object.entries(accountData)) {
 		$i[key] = value;
 	}
-	localStorage.setItem('account', JSON.stringify($i));
+	miLocalStorage.setItem('account', JSON.stringify($i));
 }
 
 export function refreshAccount() {
@@ -130,7 +131,7 @@ export async function login(token: Account['token'], redirect?: string) {
 	waiting();
 	if (_DEV_) console.log('logging as token ', token);
 	const me = await fetchAccount(token);
-	localStorage.setItem('account', JSON.stringify(me));
+	miLocalStorage.setItem('account', JSON.stringify(me));
 	document.cookie = `token=${token}; path=/; max-age=31536000`; // bull dashboardの認証とかで使う
 	await addAccount(me.id, token);
 
diff --git a/packages/frontend/src/components/MkAbuseReport.vue b/packages/frontend/src/components/MkAbuseReport.vue
index c065792882..cdfe323d50 100644
--- a/packages/frontend/src/components/MkAbuseReport.vue
+++ b/packages/frontend/src/components/MkAbuseReport.vue
@@ -1,5 +1,5 @@
 <template>
-<div class="bcekxzvu _gap _panel">
+<div class="bcekxzvu _margin _panel">
 	<div class="target">
 		<MkA v-user-preview="report.targetUserId" class="info" :to="`/user-info/${report.targetUserId}`">
 			<MkAvatar class="avatar" :user="report.targetUser" :show-indicator="true" :disable-link="true"/>
@@ -8,7 +8,7 @@
 				<MkAcct class="acct" :user="report.targetUser" style="display: block;"/>
 			</div>
 		</MkA>
-		<MkKeyValue class="_formBlock">
+		<MkKeyValue>
 			<template #key>{{ i18n.ts.registeredDate }}</template>
 			<template #value>{{ dateString(report.targetUser.createdAt) }} (<MkTime :time="report.targetUser.createdAt"/>)</template>
 		</MkKeyValue>
diff --git a/packages/frontend/src/components/MkAbuseReportWindow.vue b/packages/frontend/src/components/MkAbuseReportWindow.vue
index ab90ed357a..d4f9622fbf 100644
--- a/packages/frontend/src/components/MkAbuseReportWindow.vue
+++ b/packages/frontend/src/components/MkAbuseReportWindow.vue
@@ -1,5 +1,5 @@
 <template>
-<XWindow ref="uiWindow" :initial-width="400" :initial-height="500" :can-resize="true" @closed="emit('closed')">
+<MkWindow ref="uiWindow" :initial-width="400" :initial-height="500" :can-resize="true" @closed="emit('closed')">
 	<template #header>
 		<i class="ti ti-exclamation-circle" style="margin-right: 0.5em;"></i>
 		<I18n :src="i18n.ts.reportAbuseOf" tag="span">
@@ -8,24 +8,26 @@
 			</template>
 		</I18n>
 	</template>
-	<div class="dpvffvvy _monolithic_">
-		<div class="_section">
-			<MkTextarea v-model="comment">
-				<template #label>{{ i18n.ts.details }}</template>
-				<template #caption>{{ i18n.ts.fillAbuseReportDescription }}</template>
-			</MkTextarea>
+	<MkSpacer :margin-min="20" :margin-max="28">
+		<div class="dpvffvvy _gaps_m">
+			<div class="">
+				<MkTextarea v-model="comment">
+					<template #label>{{ i18n.ts.details }}</template>
+					<template #caption>{{ i18n.ts.fillAbuseReportDescription }}</template>
+				</MkTextarea>
+			</div>
+			<div class="">
+				<MkButton primary full :disabled="comment.length === 0" @click="send">{{ i18n.ts.send }}</MkButton>
+			</div>
 		</div>
-		<div class="_section">
-			<MkButton primary full :disabled="comment.length === 0" @click="send">{{ i18n.ts.send }}</MkButton>
-		</div>
-	</div>
-</XWindow>
+	</MkSpacer>
+</MkWindow>
 </template>
 
 <script setup lang="ts">
 import { ref, shallowRef } from 'vue';
 import * as Misskey from 'misskey-js';
-import XWindow from '@/components/MkWindow.vue';
+import MkWindow from '@/components/MkWindow.vue';
 import MkTextarea from '@/components/form/textarea.vue';
 import MkButton from '@/components/MkButton.vue';
 import * as os from '@/os';
@@ -40,7 +42,7 @@ const emit = defineEmits<{
 	(ev: 'closed'): void;
 }>();
 
-const uiWindow = shallowRef<InstanceType<typeof XWindow>>();
+const uiWindow = shallowRef<InstanceType<typeof MkWindow>>();
 const comment = ref(props.initialComment || '');
 
 function send() {
diff --git a/packages/frontend/src/components/MkAsUi.vue b/packages/frontend/src/components/MkAsUi.vue
new file mode 100644
index 0000000000..e2d8c010a2
--- /dev/null
+++ b/packages/frontend/src/components/MkAsUi.vue
@@ -0,0 +1,114 @@
+<template>
+<div>
+	<div v-if="c.type === 'root'" :class="$style.root">
+		<template v-for="child in c.children" :key="child">
+			<MkAsUi v-if="!g(child).hidden" :component="g(child)" :components="props.components" :size="size"/>
+		</template>
+	</div>
+	<span v-else-if="c.type === 'text'" :class="{ [$style.fontSerif]: c.font === 'serif', [$style.fontMonospace]: c.font === 'monospace' }" :style="{ fontSize: c.size ? `${c.size * 100}%` : null, fontWeight: c.bold ? 'bold' : null, color: c.color ?? null }">{{ c.text }}</span>
+	<Mfm v-else-if="c.type === 'mfm'" :class="{ [$style.fontSerif]: c.font === 'serif', [$style.fontMonospace]: c.font === 'monospace' }" :style="{ fontSize: c.size ? `${c.size * 100}%` : null, color: c.color ?? null }" :text="c.text"/>
+	<MkButton v-else-if="c.type === 'button'" :primary="c.primary" :rounded="c.rounded" :small="size === 'small'" @click="c.onClick">{{ c.text }}</MkButton>
+	<div v-else-if="c.type === 'buttons'" class="_buttons">
+		<MkButton v-for="button in c.buttons" :primary="button.primary" :rounded="button.rounded" :small="size === 'small'" @click="button.onClick">{{ button.text }}</MkButton>
+	</div>
+	<MkSwitch v-else-if="c.type === 'switch'" :model-value="valueForSwitch" @update:model-value="onSwitchUpdate">
+		<template v-if="c.label" #label>{{ c.label }}</template>
+		<template v-if="c.caption" #caption>{{ c.caption }}</template>
+	</MkSwitch>
+	<MkTextarea v-else-if="c.type === 'textarea'" :model-value="c.default" @update:model-value="c.onInput">
+		<template v-if="c.label" #label>{{ c.label }}</template>
+		<template v-if="c.caption" #caption>{{ c.caption }}</template>
+	</MkTextarea>
+	<MkInput v-else-if="c.type === 'textInput'" :small="size === 'small'" :model-value="c.default" @update:model-value="c.onInput">
+		<template v-if="c.label" #label>{{ c.label }}</template>
+		<template v-if="c.caption" #caption>{{ c.caption }}</template>
+	</MkInput>
+	<MkInput v-else-if="c.type === 'numberInput'" :small="size === 'small'" :model-value="c.default" type="number" @update:model-value="c.onInput">
+		<template v-if="c.label" #label>{{ c.label }}</template>
+		<template v-if="c.caption" #caption>{{ c.caption }}</template>
+	</MkInput>
+	<MkSelect v-else-if="c.type === 'select'" :small="size === 'small'" :model-value="c.default" @update:model-value="c.onChange">
+		<template v-if="c.label" #label>{{ c.label }}</template>
+		<template v-if="c.caption" #caption>{{ c.caption }}</template>
+		<option v-for="item in c.items" :key="item.value" :value="item.value">{{ item.text }}</option>
+	</MkSelect>
+	<MkButton v-else-if="c.type === 'postFormButton'" :primary="c.primary" :rounded="c.rounded" :small="size === 'small'" @click="openPostForm">{{ c.text }}</MkButton>
+	<FormFolder v-else-if="c.type === 'folder'" :default-open="c.opened">
+		<template #label>{{ c.title }}</template>
+		<template v-for="child in c.children" :key="child">
+			<MkAsUi v-if="!g(child).hidden" :component="g(child)" :components="props.components" :size="size"/>
+		</template>
+	</FormFolder>
+	<div v-else-if="c.type === 'container'" :class="[$style.container, { [$style.fontSerif]: c.font === 'serif', [$style.fontMonospace]: c.font === 'monospace', [$style.containerCenter]: c.align === 'center' }]" :style="{ backgroundColor: c.bgColor ?? null, color: c.fgColor ?? null, borderWidth: c.borderWidth ? `${c.borderWidth}px` : 0, borderColor: c.borderColor ?? 'var(--divider)', padding: c.padding ? `${c.padding}px` : 0, borderRadius: c.rounded ? '8px' : 0 }">
+		<template v-for="child in c.children" :key="child">
+			<MkAsUi v-if="!g(child).hidden" :component="g(child)" :components="props.components" :size="size"/>
+		</template>
+	</div>
+</div>
+</template>
+
+<script lang="ts" setup>
+import { computed, defineAsyncComponent, onMounted, onUnmounted, Ref } from 'vue';
+import * as os from '@/os';
+import MkButton from '@/components/MkButton.vue';
+import MkInput from '@/components/form/input.vue';
+import MkSwitch from '@/components/form/switch.vue';
+import MkTextarea from '@/components/form/textarea.vue';
+import MkSelect from '@/components/form/select.vue';
+import { AsUiComponent } from '@/scripts/aiscript/ui';
+import FormFolder from '@/components/form/folder.vue';
+
+const props = withDefaults(defineProps<{
+	component: AsUiComponent;
+	components: Ref<AsUiComponent>[];
+	size: 'small' | 'medium' | 'large';
+}>(), {
+	size: 'medium',
+});
+
+const c = props.component;
+
+function g(id) {
+	return props.components.find(x => x.value.id === id).value;
+}
+
+let valueForSwitch = $ref(c.default ?? false);
+
+function onSwitchUpdate(v) {
+	valueForSwitch = v;
+	if (c.onChange) c.onChange(v);
+}
+
+function openPostForm() {
+	os.post({
+		initialText: c.form.text,
+		instant: true,
+	});
+}
+</script>
+
+<style lang="scss" module>
+.root {
+	display: flex;
+	flex-direction: column;
+	gap: 12px;
+}
+
+.container {
+	display: flex;
+	flex-direction: column;
+	gap: 12px;
+}
+
+.containerCenter {
+	text-align: center;
+}
+
+.fontSerif {
+	font-family: serif;
+}
+
+.fontMonospace {
+	font-family: Fira code, Fira Mono, Consolas, Menlo, Courier, monospace;
+}
+</style>
diff --git a/packages/frontend/src/components/MkAutocomplete.vue b/packages/frontend/src/components/MkAutocomplete.vue
index 08e2c29de2..8ed60bc5dc 100644
--- a/packages/frontend/src/components/MkAutocomplete.vue
+++ b/packages/frontend/src/components/MkAutocomplete.vue
@@ -46,6 +46,7 @@ import { defaultStore } from '@/store';
 import { emojilist } from '@/scripts/emojilist';
 import { instance } from '@/instance';
 import { i18n } from '@/i18n';
+import { miLocalStorage } from '@/local-storage';
 
 type EmojiDef = {
 	emoji: string;
@@ -208,7 +209,7 @@ function exec() {
 		}
 	} else if (props.type === 'hashtag') {
 		if (!props.q || props.q === '') {
-			hashtags.value = JSON.parse(localStorage.getItem('hashtags') || '[]');
+			hashtags.value = JSON.parse(miLocalStorage.getItem('hashtags') || '[]');
 			fetching.value = false;
 		} else {
 			const cacheKey = `autocomplete:hashtag:${props.q}`;
diff --git a/packages/frontend/src/components/MkButton.vue b/packages/frontend/src/components/MkButton.vue
index daf47e12d4..f9602de787 100644
--- a/packages/frontend/src/components/MkButton.vue
+++ b/packages/frontend/src/components/MkButton.vue
@@ -2,7 +2,7 @@
 <button
 	v-if="!link"
 	ref="el" class="bghgjjyj _button"
-	:class="{ inline, primary, gradate, danger, rounded, full, small }"
+	:class="{ inline, primary, gradate, danger, rounded, full, small, large, asLike }"
 	:type="type"
 	@click="emit('click', $event)"
 	@mousedown="onMousedown"
@@ -41,6 +41,8 @@ const props = defineProps<{
 	danger?: boolean;
 	full?: boolean;
 	small?: boolean;
+	large?: boolean;
+	asLike?: boolean;
 }>();
 
 const emit = defineEmits<{
@@ -131,6 +133,11 @@ function onMousedown(evt: MouseEvent): void {
 		padding: 6px 12px;
 	}
 
+	&.large {
+		font-size: 100%;
+		padding: 8px 16px;
+	}
+
 	&.full {
 		width: 100%;
 	}
@@ -153,6 +160,37 @@ function onMousedown(evt: MouseEvent): void {
 		}
 	}
 
+	&.asLike {
+		background: rgba(255, 86, 125, 0.07);
+		color: #ff002f;
+
+		&:not(:disabled):hover {
+			background: rgba(255, 74, 116, 0.11);
+		}
+
+		&:not(:disabled):active {
+			background: rgba(224, 57, 96, 0.125);
+		}
+
+		> .ripples {
+			::v-deep(div) {
+				background: rgba(255, 60, 106, 0.15);
+			}
+		}
+
+		&.primary {
+			background: rgb(241 97 132);
+
+			&:not(:disabled):hover {
+				background: rgb(241 92 128);
+			}
+
+			&:not(:disabled):active {
+				background: rgb(241 92 128);
+			}
+		}
+	}
+
 	&.gradate {
 		font-weight: bold;
 		color: var(--fgOnAccent) !important;
diff --git a/packages/frontend/src/components/MkChart.vue b/packages/frontend/src/components/MkChart.vue
index ea28cfa794..57efda44b1 100644
--- a/packages/frontend/src/components/MkChart.vue
+++ b/packages/frontend/src/components/MkChart.vue
@@ -16,7 +16,6 @@
 */
 import { onMounted, ref, shallowRef, watch, PropType, onUnmounted } from 'vue';
 import { Chart } from 'chart.js';
-import { enUS } from 'date-fns/locale';
 import gradient from 'chartjs-plugin-gradient';
 import * as os from '@/os';
 import { defaultStore } from '@/store';
@@ -186,6 +185,10 @@ const render = () => {
 					time: {
 						stepSize: 1,
 						unit: props.span === 'day' ? 'month' : 'day',
+						displayFormats: {
+							day: 'M/d',
+							month: 'Y/M',
+						},
 					},
 					grid: {
 					},
@@ -194,11 +197,6 @@ const render = () => {
 						maxRotation: 0,
 						autoSkipPadding: 16,
 					},
-					adapters: {
-						date: {
-							locale: enUS,
-						},
-					},
 					min: getDate(props.limit).getTime(),
 				},
 				y: {
diff --git a/packages/frontend/src/components/MkChartLegend.vue b/packages/frontend/src/components/MkChartLegend.vue
index f33f753723..b950f2836e 100644
--- a/packages/frontend/src/components/MkChartLegend.vue
+++ b/packages/frontend/src/components/MkChartLegend.vue
@@ -59,7 +59,7 @@ defineExpose({
 
 			&.disabled {
 				text-decoration: line-through;
-				opacity: 0.6;
+				opacity: 0.5;
 			}
 
 			> .box {
@@ -72,4 +72,11 @@ defineExpose({
 		}
 	}
 }
+
+@container (max-width: 500px) {
+	.root {
+		font-size: 90%;
+		gap: 6px;
+	}
+}
 </style>
diff --git a/packages/frontend/src/components/MkCropperDialog.vue b/packages/frontend/src/components/MkCropperDialog.vue
index f00fef12f1..84adb790f9 100644
--- a/packages/frontend/src/components/MkCropperDialog.vue
+++ b/packages/frontend/src/components/MkCropperDialog.vue
@@ -1,5 +1,5 @@
 <template>
-<XModalWindow
+<MkModalWindow
 	ref="dialogEl"
 	:width="800"
 	:height="500"
@@ -22,7 +22,7 @@
 			</div>
 		</div>
 	</template>
-</XModalWindow>
+</MkModalWindow>
 </template>
 
 <script lang="ts" setup>
@@ -30,7 +30,7 @@ import { nextTick, onMounted } from 'vue';
 import * as misskey from 'misskey-js';
 import Cropper from 'cropperjs';
 import tinycolor from 'tinycolor2';
-import XModalWindow from '@/components/MkModalWindow.vue';
+import MkModalWindow from '@/components/MkModalWindow.vue';
 import * as os from '@/os';
 import { $i } from '@/account';
 import { defaultStore } from '@/store';
@@ -50,7 +50,7 @@ const props = defineProps<{
 }>();
 
 const imgUrl = getProxiedImageUrl(props.file.url);
-let dialogEl = $shallowRef<InstanceType<typeof XModalWindow>>();
+let dialogEl = $shallowRef<InstanceType<typeof MkModalWindow>>();
 let imgEl = $shallowRef<HTMLImageElement>();
 let cropper: Cropper | null = null;
 let loading = $ref(true);
diff --git a/packages/frontend/src/components/MkDonation.vue b/packages/frontend/src/components/MkDonation.vue
new file mode 100644
index 0000000000..707444abc9
--- /dev/null
+++ b/packages/frontend/src/components/MkDonation.vue
@@ -0,0 +1,109 @@
+<template>
+<div class="_panel _shadow" :class="$style.root">
+	<!-- TODO: インスタンス運営者が任意のテキストとリンクを設定できるようにする -->
+	<div :class="$style.icon">
+		<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-pig-money" width="40" height="40" viewBox="0 0 24 24" stroke-width="1" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
+			<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
+			<path d="M15 11v.01"></path>
+			<path d="M5.173 8.378a3 3 0 1 1 4.656 -1.377"></path>
+			<path d="M16 4v3.803a6.019 6.019 0 0 1 2.658 3.197h1.341a1 1 0 0 1 1 1v2a1 1 0 0 1 -1 1h-1.342c-.336 .95 -.907 1.8 -1.658 2.473v2.027a1.5 1.5 0 0 1 -3 0v-.583a6.04 6.04 0 0 1 -1 .083h-4a6.04 6.04 0 0 1 -1 -.083v.583a1.5 1.5 0 0 1 -3 0v-2l.001 -.027a6 6 0 0 1 3.999 -10.473h2.5l4.5 -3h.001z"></path>
+		</svg>
+	</div>
+	<div :class="$style.main">
+		<div :class="$style.title">{{ i18n.ts.didYouLikeMisskey }}</div>
+		<div :class="$style.text">
+			<I18n :src="i18n.ts.pleaseDonate" tag="span">
+				<template #host>
+					{{ $instance.name ?? host }}
+				</template>
+			</I18n>
+			<div style="margin-top: 0.2em;">
+				<MkLink target="_blank" url="https://misskey-hub.net/docs/donate.html">{{ i18n.ts.learnMore }}</MkLink>
+			</div>
+		</div>
+		<div class="_buttons">
+			<MkButton @click="close">{{ i18n.ts.remindMeLater }}</MkButton>
+			<MkButton @click="neverShow">{{ i18n.ts.neverShow }}</MkButton>
+		</div>
+	</div>
+	<button class="_button" :class="$style.close" @click="close"><i class="ti ti-x"></i></button>
+</div>
+</template>
+
+<script lang="ts" setup>
+import { onMounted, shallowRef } from 'vue';
+import MkButton from '@/components/MkButton.vue';
+import MkLink from '@/components/MkLink.vue';
+import { host } from '@/config';
+import { i18n } from '@/i18n';
+import * as os from '@/os';
+import { miLocalStorage } from '@/local-storage';
+
+const emit = defineEmits<{
+	(ev: 'closed'): void;
+}>();
+
+const zIndex = os.claimZIndex('low');
+
+function close() {
+	miLocalStorage.setItem('latestDonationInfoShownAt', Date.now().toString());
+	emit('closed');
+}
+
+function neverShow() {
+	miLocalStorage.setItem('neverShowDonationInfo', 'true')
+	close();
+}
+</script>
+
+<style lang="scss" module>
+.root {
+	position: fixed;
+	z-index: v-bind(zIndex);
+	bottom: var(--margin);
+	left: 0;
+	right: 0;
+	margin: auto;
+	box-sizing: border-box;
+	width: calc(100% - (var(--margin) * 2));
+	max-width: 500px;
+	display: flex;
+}
+
+.icon {
+	text-align: center;
+	padding-top: 25px;
+	width: 100px;
+	color: var(--accent);
+}
+@media (max-width: 500px) {
+	.icon {
+		width: 80px;
+	}
+}
+@media (max-width: 450px) {
+	.icon {
+		width: 70px;
+	}
+}
+
+.main {
+	padding: 25px 25px 25px 0;
+	flex: 1;
+}
+
+.close {
+	position: absolute;
+	top: 8px;
+	right: 8px;
+	padding: 8px;
+}
+
+.title {
+	font-weight: bold;
+}
+
+.text {
+	margin: 0.7em 0 1em 0;
+}
+</style>
diff --git a/packages/frontend/src/components/MkDriveSelectDialog.vue b/packages/frontend/src/components/MkDriveSelectDialog.vue
index 6a96e758fa..8d2b19c013 100644
--- a/packages/frontend/src/components/MkDriveSelectDialog.vue
+++ b/packages/frontend/src/components/MkDriveSelectDialog.vue
@@ -1,5 +1,5 @@
 <template>
-<XModalWindow
+<MkModalWindow
 	ref="dialog"
 	:width="800"
 	:height="500"
@@ -15,14 +15,14 @@
 		<span v-if="selected.length > 0" style="margin-left: 8px; opacity: 0.5;">({{ number(selected.length) }})</span>
 	</template>
 	<XDrive :multiple="multiple" :select="type" @change-selection="onChangeSelection" @selected="ok()"/>
-</XModalWindow>
+</MkModalWindow>
 </template>
 
 <script lang="ts" setup>
 import { ref, shallowRef } from 'vue';
 import * as Misskey from 'misskey-js';
 import XDrive from '@/components/MkDrive.vue';
-import XModalWindow from '@/components/MkModalWindow.vue';
+import MkModalWindow from '@/components/MkModalWindow.vue';
 import number from '@/filters/number';
 import { i18n } from '@/i18n';
 
@@ -38,7 +38,7 @@ const emit = defineEmits<{
 	(ev: 'closed'): void;
 }>();
 
-const dialog = shallowRef<InstanceType<typeof XModalWindow>>();
+const dialog = shallowRef<InstanceType<typeof MkModalWindow>>();
 
 const selected = ref<Misskey.entities.DriveFile[]>([]);
 
diff --git a/packages/frontend/src/components/MkDriveWindow.vue b/packages/frontend/src/components/MkDriveWindow.vue
index 617200321b..8b2abc15a3 100644
--- a/packages/frontend/src/components/MkDriveWindow.vue
+++ b/packages/frontend/src/components/MkDriveWindow.vue
@@ -1,5 +1,5 @@
 <template>
-<XWindow
+<MkWindow
 	ref="window"
 	:initial-width="800"
 	:initial-height="500"
@@ -10,14 +10,14 @@
 		{{ i18n.ts.drive }}
 	</template>
 	<XDrive :initial-folder="initialFolder"/>
-</XWindow>
+</MkWindow>
 </template>
 
 <script lang="ts" setup>
 import { } from 'vue';
 import * as Misskey from 'misskey-js';
 import XDrive from '@/components/MkDrive.vue';
-import XWindow from '@/components/MkWindow.vue';
+import MkWindow from '@/components/MkWindow.vue';
 import { i18n } from '@/i18n';
 
 defineProps<{
diff --git a/packages/frontend/src/components/MkFileCaptionEditWindow.vue b/packages/frontend/src/components/MkFileCaptionEditWindow.vue
index b3bd194dc3..f7b7430bff 100644
--- a/packages/frontend/src/components/MkFileCaptionEditWindow.vue
+++ b/packages/frontend/src/components/MkFileCaptionEditWindow.vue
@@ -1,5 +1,5 @@
 <template>
-<XModalWindow
+<MkModalWindow
 	ref="dialog"
 	:width="400"
 	:height="450"
@@ -16,13 +16,13 @@
 			<template #label>{{ i18n.ts.caption }}</template>
 		</MkTextarea>
 	</MkSpacer>
-</XModalWindow>
+</MkModalWindow>
 </template>
 
 <script lang="ts" setup>
 import { } from 'vue';
 import * as Misskey from 'misskey-js';
-import XModalWindow from '@/components/MkModalWindow.vue';
+import MkModalWindow from '@/components/MkModalWindow.vue';
 import MkTextarea from '@/components/form/textarea.vue';
 import MkDriveFileThumbnail from '@/components/MkDriveFileThumbnail.vue';
 import { i18n } from '@/i18n';
@@ -37,7 +37,7 @@ const emit = defineEmits<{
 	(ev: 'closed'): void;
 }>();
 
-const dialog = $shallowRef<InstanceType<typeof XModalWindow>>();
+const dialog = $shallowRef<InstanceType<typeof MkModalWindow>>();
 
 let caption = $ref(props.default);
 
diff --git a/packages/frontend/src/components/MkFileListForAdmin.vue b/packages/frontend/src/components/MkFileListForAdmin.vue
index 1335f88a7c..f340acaf2d 100644
--- a/packages/frontend/src/components/MkFileListForAdmin.vue
+++ b/packages/frontend/src/components/MkFileListForAdmin.vue
@@ -54,8 +54,6 @@ const props = defineProps<{
 }
 
 .urempief {
-	margin-top: var(--margin);
-
 	&.list {
 		> .file {
 			display: flex;
@@ -89,7 +87,6 @@ const props = defineProps<{
 		display: grid;
 		grid-template-columns: repeat(auto-fill, minmax(130px, 1fr));
 		grid-gap: 12px;
-		margin: var(--margin) 0;
 
 		> .file {
 			position: relative;
diff --git a/packages/frontend/src/components/MkFlashPreview.vue b/packages/frontend/src/components/MkFlashPreview.vue
new file mode 100644
index 0000000000..a96934ddb1
--- /dev/null
+++ b/packages/frontend/src/components/MkFlashPreview.vue
@@ -0,0 +1,112 @@
+<template>
+<MkA :to="`/play/${flash.id}`" class="vhpxefrk _panel" tabindex="-1">
+	<article>
+		<header>
+			<h1 :title="flash.title">{{ flash.title }}</h1>
+		</header>
+		<p v-if="flash.summary" :title="flash.summary">{{ flash.summary.length > 85 ? flash.summary.slice(0, 85) + '…' : flash.summary }}</p>
+		<footer>
+			<img class="icon" :src="flash.user.avatarUrl"/>
+			<p>{{ userName(flash.user) }}</p>
+		</footer>
+	</article>
+</MkA>
+</template>
+
+<script lang="ts" setup>
+import { } from 'vue';
+import * as misskey from 'misskey-js';
+import { userName } from '@/filters/user';
+import * as os from '@/os';
+
+const props = defineProps<{
+	//flash: misskey.entities.Flash;
+	flash: any;
+}>();
+</script>
+
+<style lang="scss" scoped>
+.vhpxefrk {
+	display: block;
+
+	&:hover {
+		text-decoration: none;
+		color: var(--accent);
+	}
+
+	> article {
+		padding: 16px;
+
+		> header {
+			margin-bottom: 8px;
+
+			> h1 {
+				margin: 0;
+				font-size: 1em;
+				color: var(--urlPreviewTitle);
+			}
+		}
+
+		> p {
+			margin: 0;
+			color: var(--urlPreviewText);
+			font-size: 0.8em;
+		}
+
+		> footer {
+			margin-top: 8px;
+			height: 16px;
+
+			> img {
+				display: inline-block;
+				width: 16px;
+				height: 16px;
+				margin-right: 4px;
+				vertical-align: top;
+			}
+
+			> p {
+				display: inline-block;
+				margin: 0;
+				color: var(--urlPreviewInfo);
+				font-size: 0.8em;
+				line-height: 16px;
+				vertical-align: top;
+			}
+		}
+	}
+
+	@media (max-width: 700px) {
+	}
+
+	@media (max-width: 550px) {
+		font-size: 12px;
+
+		> article {
+			padding: 12px;
+		}
+	}
+
+	@media (max-width: 500px) {
+		font-size: 10px;
+		
+		> article {
+			padding: 8px;
+
+			> header {
+				margin-bottom: 4px;
+			}
+
+			> footer {
+				margin-top: 4px;
+
+				> img {
+					width: 12px;
+					height: 12px;
+				}
+			}
+		}
+	}
+}
+
+</style>
diff --git a/packages/frontend/src/components/MkFolder.vue b/packages/frontend/src/components/MkFolder.vue
index 5a406c8635..dc10c7d3f3 100644
--- a/packages/frontend/src/components/MkFolder.vue
+++ b/packages/frontend/src/components/MkFolder.vue
@@ -25,8 +25,9 @@
 <script lang="ts">
 import { defineComponent } from 'vue';
 import tinycolor from 'tinycolor2';
+import { miLocalStorage } from '@/local-storage';
 
-const localStoragePrefix = 'ui:folder:';
+const miLocalStoragePrefix = 'ui:folder:' as const;
 
 export default defineComponent({
 	props: {
@@ -44,13 +45,13 @@ export default defineComponent({
 	data() {
 		return {
 			bg: null,
-			showBody: (this.persistKey && localStorage.getItem(localStoragePrefix + this.persistKey)) ? localStorage.getItem(localStoragePrefix + this.persistKey) === 't' : this.expanded,
+			showBody: (this.persistKey && miLocalStorage.getItem(`${miLocalStoragePrefix}${this.persistKey}`)) ? (miLocalStorage.getItem(`${miLocalStoragePrefix}${this.persistKey}`) === 't') : this.expanded,
 		};
 	},
 	watch: {
 		showBody() {
 			if (this.persistKey) {
-				localStorage.setItem(localStoragePrefix + this.persistKey, this.showBody ? 't' : 'f');
+				miLocalStorage.setItem(`${miLocalStoragePrefix}${this.persistKey}`, this.showBody ? 't' : 'f');
 			}
 		},
 	},
diff --git a/packages/frontend/src/components/MkForgotPassword.vue b/packages/frontend/src/components/MkForgotPassword.vue
index 1b55451c94..dc38e42779 100644
--- a/packages/frontend/src/components/MkForgotPassword.vue
+++ b/packages/frontend/src/components/MkForgotPassword.vue
@@ -1,5 +1,6 @@
 <template>
-<XModalWindow ref="dialog"
+<MkModalWindow
+	ref="dialog"
 	:width="370"
 	:height="400"
 	@close="dialog.close()"
@@ -8,18 +9,18 @@
 	<template #header>{{ i18n.ts.forgotPassword }}</template>
 
 	<form v-if="instance.enableEmail" class="bafeceda" @submit.prevent="onSubmit">
-		<div class="main _formRoot">
-			<MkInput v-model="username" class="_formBlock" type="text" pattern="^[a-zA-Z0-9_]+$" :spellcheck="false" autofocus required>
+		<div class="main _gaps_m">
+			<MkInput v-model="username" type="text" pattern="^[a-zA-Z0-9_]+$" :spellcheck="false" autofocus required>
 				<template #label>{{ i18n.ts.username }}</template>
 				<template #prefix>@</template>
 			</MkInput>
 
-			<MkInput v-model="email" class="_formBlock" type="email" :spellcheck="false" required>
+			<MkInput v-model="email" type="email" :spellcheck="false" required>
 				<template #label>{{ i18n.ts.emailAddress }}</template>
 				<template #caption>{{ i18n.ts._forgotPassword.enterEmail }}</template>
 			</MkInput>
 
-			<MkButton class="_formBlock" type="submit" :disabled="processing" primary style="margin: 0 auto;">{{ i18n.ts.send }}</MkButton>
+			<MkButton type="submit" :disabled="processing" primary style="margin: 0 auto;">{{ i18n.ts.send }}</MkButton>
 		</div>
 		<div class="sub">
 			<MkA to="/about" class="_link">{{ i18n.ts._forgotPassword.ifNoEmail }}</MkA>
@@ -28,12 +29,12 @@
 	<div v-else class="bafecedb">
 		{{ i18n.ts._forgotPassword.contactAdmin }}
 	</div>
-</XModalWindow>
+</MkModalWindow>
 </template>
 
 <script lang="ts" setup>
 import { } from 'vue';
-import XModalWindow from '@/components/MkModalWindow.vue';
+import MkModalWindow from '@/components/MkModalWindow.vue';
 import MkButton from '@/components/MkButton.vue';
 import MkInput from '@/components/form/input.vue';
 import * as os from '@/os';
@@ -45,7 +46,7 @@ const emit = defineEmits<{
 	(ev: 'closed'): void;
 }>();
 
-let dialog: InstanceType<typeof XModalWindow> = $ref();
+let dialog: InstanceType<typeof MkModalWindow> = $ref();
 
 let username = $ref('');
 let email = $ref('');
diff --git a/packages/frontend/src/components/MkFormDialog.vue b/packages/frontend/src/components/MkFormDialog.vue
index b2bf76a8c7..76c6c164d4 100644
--- a/packages/frontend/src/components/MkFormDialog.vue
+++ b/packages/frontend/src/components/MkFormDialog.vue
@@ -1,5 +1,5 @@
 <template>
-<XModalWindow
+<MkModalWindow
 	ref="dialog"
 	:width="450"
 	:can-close="false"
@@ -15,43 +15,43 @@
 	</template>
 
 	<MkSpacer :margin-min="20" :margin-max="32">
-		<div class="xkpnjxcv _formRoot">
+		<div class="xkpnjxcv _gaps_m">
 			<template v-for="item in Object.keys(form).filter(item => !form[item].hidden)">
-				<FormInput v-if="form[item].type === 'number'" v-model="values[item]" type="number" :step="form[item].step || 1" class="_formBlock">
+				<FormInput v-if="form[item].type === 'number'" v-model="values[item]" type="number" :step="form[item].step || 1">
 					<template #label><span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ $ts.optional }})</span></template>
 					<template v-if="form[item].description" #caption>{{ form[item].description }}</template>
 				</FormInput>
-				<FormInput v-else-if="form[item].type === 'string' && !form[item].multiline" v-model="values[item]" type="text" class="_formBlock">
+				<FormInput v-else-if="form[item].type === 'string' && !form[item].multiline" v-model="values[item]" type="text">
 					<template #label><span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ $ts.optional }})</span></template>
 					<template v-if="form[item].description" #caption>{{ form[item].description }}</template>
 				</FormInput>
-				<FormTextarea v-else-if="form[item].type === 'string' && form[item].multiline" v-model="values[item]" class="_formBlock">
+				<FormTextarea v-else-if="form[item].type === 'string' && form[item].multiline" v-model="values[item]">
 					<template #label><span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ $ts.optional }})</span></template>
 					<template v-if="form[item].description" #caption>{{ form[item].description }}</template>
 				</FormTextarea>
-				<FormSwitch v-else-if="form[item].type === 'boolean'" v-model="values[item]" class="_formBlock">
+				<FormSwitch v-else-if="form[item].type === 'boolean'" v-model="values[item]">
 					<span v-text="form[item].label || item"></span>
 					<template v-if="form[item].description" #caption>{{ form[item].description }}</template>
 				</FormSwitch>
-				<FormSelect v-else-if="form[item].type === 'enum'" v-model="values[item]" class="_formBlock">
+				<FormSelect v-else-if="form[item].type === 'enum'" v-model="values[item]">
 					<template #label><span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ $ts.optional }})</span></template>
 					<option v-for="item in form[item].enum" :key="item.value" :value="item.value">{{ item.label }}</option>
 				</FormSelect>
-				<FormRadios v-else-if="form[item].type === 'radio'" v-model="values[item]" class="_formBlock">
+				<FormRadios v-else-if="form[item].type === 'radio'" v-model="values[item]">
 					<template #label><span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ $ts.optional }})</span></template>
 					<option v-for="item in form[item].options" :key="item.value" :value="item.value">{{ item.label }}</option>
 				</FormRadios>
-				<FormRange v-else-if="form[item].type === 'range'" v-model="values[item]" :min="form[item].min" :max="form[item].max" :step="form[item].step" :text-converter="form[item].textConverter" class="_formBlock">
+				<FormRange v-else-if="form[item].type === 'range'" v-model="values[item]" :min="form[item].min" :max="form[item].max" :step="form[item].step" :text-converter="form[item].textConverter">
 					<template #label><span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ $ts.optional }})</span></template>
 					<template v-if="form[item].description" #caption>{{ form[item].description }}</template>
 				</FormRange>
-				<MkButton v-else-if="form[item].type === 'button'" class="_formBlock" @click="form[item].action($event, values)">
+				<MkButton v-else-if="form[item].type === 'button'" @click="form[item].action($event, values)">
 					<span v-text="form[item].content || item"></span>
 				</MkButton>
 			</template>
 		</div>
 	</MkSpacer>
-</XModalWindow>
+</MkModalWindow>
 </template>
 
 <script lang="ts">
@@ -63,11 +63,11 @@ import FormSelect from './form/select.vue';
 import FormRange from './form/range.vue';
 import MkButton from './MkButton.vue';
 import FormRadios from './form/radios.vue';
-import XModalWindow from '@/components/MkModalWindow.vue';
+import MkModalWindow from '@/components/MkModalWindow.vue';
 
 export default defineComponent({
 	components: {
-		XModalWindow,
+		MkModalWindow,
 		FormInput,
 		FormTextarea,
 		FormSwitch,
diff --git a/packages/frontend/src/components/MkHeatmap.vue b/packages/frontend/src/components/MkHeatmap.vue
index ec7f49beee..f222fca9a1 100644
--- a/packages/frontend/src/components/MkHeatmap.vue
+++ b/packages/frontend/src/components/MkHeatmap.vue
@@ -10,7 +10,6 @@
 <script lang="ts" setup>
 import { markRaw, version as vueVersion, onMounted, onBeforeUnmount, nextTick, watch } from 'vue';
 import { Chart } from 'chart.js';
-import { enUS } from 'date-fns/locale';
 import tinycolor from 'tinycolor2';
 import { MatrixController, MatrixElement } from 'chartjs-chart-matrix';
 import * as os from '@/os';
@@ -149,7 +148,9 @@ async function renderChart() {
 						round: 'week',
 						isoWeekday: 0,
 						displayFormats: {
-							week: 'MMM dd',
+							day: 'M/d',
+							month: 'Y/M',
+							week: 'M/d',
 						},
 					},
 					grid: {
diff --git a/packages/frontend/src/components/MkLaunchPad.vue b/packages/frontend/src/components/MkLaunchPad.vue
index 3ea90712a0..aab7631e36 100644
--- a/packages/frontend/src/components/MkLaunchPad.vue
+++ b/packages/frontend/src/components/MkLaunchPad.vue
@@ -50,7 +50,7 @@ const menu = defaultStore.state.menu;
 
 const items = Object.keys(navbarItemDef).filter(k => !menu.includes(k)).map(k => navbarItemDef[k]).filter(def => def.show == null ? true : def.show).map(def => ({
 	type: def.to ? 'link' : 'button',
-	text: i18n.ts[def.title],
+	text: def.title,
 	icon: def.icon,
 	to: def.to,
 	action: def.action,
diff --git a/packages/frontend/src/components/MkMenu.vue b/packages/frontend/src/components/MkMenu.vue
index 263030e015..e9076138c6 100644
--- a/packages/frontend/src/components/MkMenu.vue
+++ b/packages/frontend/src/components/MkMenu.vue
@@ -217,6 +217,7 @@ onBeforeUnmount(() => {
 			content: "";
 			display: block;
 			position: absolute;
+			z-index: -1;
 			top: 0;
 			left: 0;
 			right: 0;
diff --git a/packages/frontend/src/components/MkModal.vue b/packages/frontend/src/components/MkModal.vue
index 505b5e64bc..38b1ce7c1d 100644
--- a/packages/frontend/src/components/MkModal.vue
+++ b/packages/frontend/src/components/MkModal.vue
@@ -383,7 +383,6 @@ defineExpose({
 			mask-image: linear-gradient(0deg, rgba(0,0,0,0) 0%, rgba(0,0,0,1) 32px, rgba(0,0,0,1) calc(100% - 32px), rgba(0,0,0,0) 100%);
 			overflow: auto;
 			display: flex;
-			container-type: inline-size;
 
 			@media (max-width: 500px) {
 				padding: 16px;
diff --git a/packages/frontend/src/components/MkModalPageWindow.vue b/packages/frontend/src/components/MkModalPageWindow.vue
index 2791d5ceb9..b06fcb9ffa 100644
--- a/packages/frontend/src/components/MkModalPageWindow.vue
+++ b/packages/frontend/src/components/MkModalPageWindow.vue
@@ -1,6 +1,6 @@
 <template>
 <MkModal ref="modal" @click="$emit('click')" @closed="$emit('closed')">
-	<div ref="rootEl" class="hrmcaedk _narrow_" :style="{ width: `${width}px`, height: (height ? `min(${height}px, 100%)` : '100%') }">
+	<div ref="rootEl" class="hrmcaedk" :style="{ width: `${width}px`, height: (height ? `min(${height}px, 100%)` : '100%') }">
 		<div class="header" @contextmenu="onContextmenu">
 			<button v-if="history.length > 0" v-tooltip="$ts.goBack" class="_button" @click="back()"><i class="ti ti-arrow-left"></i></button>
 			<span v-else style="display: inline-block; width: 20px"></span>
diff --git a/packages/frontend/src/components/MkModalWindow.vue b/packages/frontend/src/components/MkModalWindow.vue
index 1e93f01c8d..ac428fa7b1 100644
--- a/packages/frontend/src/components/MkModalWindow.vue
+++ b/packages/frontend/src/components/MkModalWindow.vue
@@ -1,6 +1,6 @@
 <template>
 <MkModal ref="modal" :prefer-type="'dialog'" @click="onBgClick" @closed="$emit('closed')">
-	<div ref="rootEl" class="ebkgoccj _narrow_" :style="{ width: `${width}px`, height: scroll ? (height ? `${height}px` : null) : (height ? `min(${height}px, 100%)` : '100%') }" @keydown="onKeydown">
+	<div ref="rootEl" class="ebkgoccj" :style="{ width: `${width}px`, height: scroll ? (height ? `${height}px` : null) : (height ? `min(${height}px, 100%)` : '100%') }" @keydown="onKeydown">
 		<div ref="headerEl" class="header">
 			<button v-if="withOkButton" class="_button" @click="$emit('close')"><i class="ti ti-x"></i></button>
 			<span class="title">
@@ -89,6 +89,7 @@ defineExpose({
 	display: flex;
 	flex-direction: column;
 	contain: content;
+	container-type: inline-size;
 	border-radius: var(--radius);
 
 	--root-margin: 24px;
diff --git a/packages/frontend/src/components/MkNoteDetailed.vue b/packages/frontend/src/components/MkNoteDetailed.vue
index 7d01a7bf75..c0e1ca7215 100644
--- a/packages/frontend/src/components/MkNoteDetailed.vue
+++ b/packages/frontend/src/components/MkNoteDetailed.vue
@@ -4,7 +4,7 @@
 	v-show="!isDeleted"
 	ref="el"
 	v-hotkey="keymap"
-	class="lxwezrsl _block"
+	class="lxwezrsl"
 	:tabindex="!isDeleted ? '-1' : null"
 	:class="{ renote: isRenote }"
 >
diff --git a/packages/frontend/src/components/MkNotificationSettingWindow.vue b/packages/frontend/src/components/MkNotificationSettingWindow.vue
index 754d8d687b..cb054b1a29 100644
--- a/packages/frontend/src/components/MkNotificationSettingWindow.vue
+++ b/packages/frontend/src/components/MkNotificationSettingWindow.vue
@@ -1,5 +1,5 @@
 <template>
-<XModalWindow
+<MkModalWindow
 	ref="dialog"
 	:width="400"
 	:height="450"
@@ -12,24 +12,24 @@
 	<template #header>{{ i18n.ts.notificationSetting }}</template>
 
 	<MkSpacer :margin-min="20" :margin-max="28">
-		<div class="_formRoot">
+		<div class="_gaps_m">
 			<template v-if="showGlobalToggle">
-				<MkSwitch v-model="useGlobalSetting" class="_formBlock">
+				<MkSwitch v-model="useGlobalSetting">
 					{{ i18n.ts.useGlobalSetting }}
 					<template #caption>{{ i18n.ts.useGlobalSettingDesc }}</template>
 				</MkSwitch>
 			</template>
 			<template v-if="!useGlobalSetting">
-				<MkInfo class="_formBlock">{{ i18n.ts.notificationSettingDesc }}</MkInfo>
-				<div style="display: flex; gap: var(--margin); flex-wrap: wrap;">
+				<MkInfo>{{ i18n.ts.notificationSettingDesc }}</MkInfo>
+				<div class="_buttons">
 					<MkButton inline @click="disableAll">{{ i18n.ts.disableAll }}</MkButton>
 					<MkButton inline @click="enableAll">{{ i18n.ts.enableAll }}</MkButton>
 				</div>
-				<MkSwitch v-for="ntype in notificationTypes" class="_formBlock" :key="ntype" v-model="typesMap[ntype]">{{ i18n.t(`_notification._types.${ntype}`) }}</MkSwitch>
+				<MkSwitch v-for="ntype in notificationTypes" :key="ntype" v-model="typesMap[ntype]">{{ i18n.t(`_notification._types.${ntype}`) }}</MkSwitch>
 			</template>
 		</div>
 	</MkSpacer>
-</XModalWindow>
+</MkModalWindow>
 </template>
 
 <script lang="ts" setup>
@@ -38,7 +38,7 @@ import { notificationTypes } from 'misskey-js';
 import MkSwitch from './form/switch.vue';
 import MkInfo from './MkInfo.vue';
 import MkButton from './MkButton.vue';
-import XModalWindow from '@/components/MkModalWindow.vue';
+import MkModalWindow from '@/components/MkModalWindow.vue';
 import { i18n } from '@/i18n';
 
 const emit = defineEmits<{
@@ -56,7 +56,7 @@ const props = withDefaults(defineProps<{
 
 let includingTypes = $computed(() => props.includingTypes || []);
 
-const dialog = $shallowRef<InstanceType<typeof XModalWindow>>();
+const dialog = $shallowRef<InstanceType<typeof MkModalWindow>>();
 
 let typesMap = $ref<Record<typeof notificationTypes[number], boolean>>({});
 let useGlobalSetting = $ref((includingTypes === null || includingTypes.length === 0) && props.showGlobalToggle);
diff --git a/packages/frontend/src/components/MkPagePreview.vue b/packages/frontend/src/components/MkPagePreview.vue
index 009582e540..a78431e2a7 100644
--- a/packages/frontend/src/components/MkPagePreview.vue
+++ b/packages/frontend/src/components/MkPagePreview.vue
@@ -1,5 +1,5 @@
 <template>
-<MkA :to="`/@${page.user.username}/pages/${page.name}`" class="vhpxefrj _block" tabindex="-1">
+<MkA :to="`/@${page.user.username}/pages/${page.name}`" class="vhpxefrj" tabindex="-1">
 	<div v-if="page.eyeCatchingImage" class="thumbnail" :style="`background-image: url('${page.eyeCatchingImage.thumbnailUrl}')`"></div>
 	<article>
 		<header>
@@ -14,22 +14,15 @@
 </MkA>
 </template>
 
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { } from 'vue';
+import * as misskey from 'misskey-js';
 import { userName } from '@/filters/user';
 import * as os from '@/os';
 
-export default defineComponent({
-	props: {
-		page: {
-			type: Object,
-			required: true,
-		},
-	},
-	methods: {
-		userName,
-	},
-});
+const props = defineProps<{
+	page: misskey.entities.Page;
+}>();
 </script>
 
 <style lang="scss" scoped>
diff --git a/packages/frontend/src/components/MkPageWindow.vue b/packages/frontend/src/components/MkPageWindow.vue
index e25737d50c..f80974772b 100644
--- a/packages/frontend/src/components/MkPageWindow.vue
+++ b/packages/frontend/src/components/MkPageWindow.vue
@@ -1,5 +1,5 @@
 <template>
-<XWindow
+<MkWindow
 	ref="windowEl"
 	:initial-width="500"
 	:initial-height="500"
@@ -20,13 +20,13 @@
 	<div class="yrolvcoq" :style="{ background: pageMetadata?.value?.bg }" style="container-type: inline-size;">
 		<RouterView :router="router"/>
 	</div>
-</XWindow>
+</MkWindow>
 </template>
 
 <script lang="ts" setup>
 import { ComputedRef, inject, provide } from 'vue';
 import RouterView from '@/components/global/RouterView.vue';
-import XWindow from '@/components/MkWindow.vue';
+import MkWindow from '@/components/MkWindow.vue';
 import { popout as _popout } from '@/scripts/popout';
 import copyToClipboard from '@/scripts/copy-to-clipboard';
 import { url } from '@/config';
@@ -47,7 +47,7 @@ defineEmits<{
 const router = new Router(routes, props.initialPath);
 
 let pageMetadata = $ref<null | ComputedRef<PageMetadata>>();
-let windowEl = $shallowRef<InstanceType<typeof XWindow>>();
+let windowEl = $shallowRef<InstanceType<typeof MkWindow>>();
 const history = $ref<{ path: string; key: any; }[]>([{
 	path: router.getCurrentPath(),
 	key: router.getCurrentKey(),
diff --git a/packages/frontend/src/components/MkPagination.vue b/packages/frontend/src/components/MkPagination.vue
index 2c0a30a888..b92e6d2360 100644
--- a/packages/frontend/src/components/MkPagination.vue
+++ b/packages/frontend/src/components/MkPagination.vue
@@ -14,14 +14,14 @@
 	</div>
 
 	<div v-else ref="rootEl">
-		<div v-show="pagination.reversed && more" key="_more_" class="cxiknjgy _gap">
+		<div v-show="pagination.reversed && more" key="_more_" class="cxiknjgy _margin">
 			<MkButton v-if="!moreFetching" class="button" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }" primary @click="fetchMoreAhead">
 				{{ i18n.ts.loadMore }}
 			</MkButton>
 			<MkLoading v-else class="loading"/>
 		</div>
 		<slot :items="items"></slot>
-		<div v-show="!pagination.reversed && more" key="_more_" class="cxiknjgy _gap">
+		<div v-show="!pagination.reversed && more" key="_more_" class="cxiknjgy _margin">
 			<MkButton v-if="!moreFetching" v-appear="($store.state.enableInfiniteScroll && !disableAutoLoad) ? fetchMore : null" class="button" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }" primary @click="fetchMore">
 				{{ i18n.ts.loadMore }}
 			</MkButton>
diff --git a/packages/frontend/src/components/MkPopupMenu.vue b/packages/frontend/src/components/MkPopupMenu.vue
index b5987715a9..34af209c06 100644
--- a/packages/frontend/src/components/MkPopupMenu.vue
+++ b/packages/frontend/src/components/MkPopupMenu.vue
@@ -1,5 +1,5 @@
 <template>
-<MkModal ref="modal" v-slot="{ type, maxHeight }" :z-priority="'high'" :src="src" :transparent-bg="true" @click="modal.close()" @closed="emit('closed')">
+<MkModal ref="modal" v-slot="{ type, maxHeight }" :z-priority="'high'" :src="src" :transparent-bg="true" @click="modal.close()" @close="emit('closing')" @closed="emit('closed')">
 	<MkMenu :items="items" :align="align" :width="width" :max-height="maxHeight" :as-drawer="type === 'drawer'" class="sfhdhdhq" :class="{ drawer: type === 'drawer' }" @close="modal.close()"/>
 </MkModal>
 </template>
@@ -20,6 +20,7 @@ defineProps<{
 
 const emit = defineEmits<{
 	(ev: 'closed'): void;
+	(ev: 'closing'): void;
 }>();
 
 let modal = $shallowRef<InstanceType<typeof MkModal>>();
diff --git a/packages/frontend/src/components/MkPostForm.vue b/packages/frontend/src/components/MkPostForm.vue
index 883ad9f14f..ff3b7ec1f5 100644
--- a/packages/frontend/src/components/MkPostForm.vue
+++ b/packages/frontend/src/components/MkPostForm.vue
@@ -98,6 +98,7 @@ import { $i, getAccounts, openAccountMenu as openAccountMenu_ } from '@/account'
 import { uploadFile } from '@/scripts/upload';
 import { deepClone } from '@/scripts/clone';
 import MkRippleEffect from '@/components/MkRippleEffect.vue';
+import { miLocalStorage } from '@/local-storage';
 
 const modal = inject('modal');
 
@@ -156,7 +157,7 @@ let autocomplete = $ref(null);
 let draghover = $ref(false);
 let quoteId = $ref(null);
 let hasNotSpecifiedMentions = $ref(false);
-let recentHashtags = $ref(JSON.parse(localStorage.getItem('hashtags') || '[]'));
+let recentHashtags = $ref(JSON.parse(miLocalStorage.getItem('hashtags') || '[]'));
 let imeText = $ref('');
 
 const typing = throttle(3000, () => {
@@ -543,7 +544,7 @@ function onDrop(ev): void {
 }
 
 function saveDraft() {
-	const draftData = JSON.parse(localStorage.getItem('drafts') || '{}');
+	const draftData = JSON.parse(miLocalStorage.getItem('drafts') || '{}');
 
 	draftData[draftKey] = {
 		updatedAt: new Date(),
@@ -558,15 +559,15 @@ function saveDraft() {
 		},
 	};
 
-	localStorage.setItem('drafts', JSON.stringify(draftData));
+	miLocalStorage.setItem('drafts', JSON.stringify(draftData));
 }
 
 function deleteDraft() {
-	const draftData = JSON.parse(localStorage.getItem('drafts') ?? '{}');
+	const draftData = JSON.parse(miLocalStorage.getItem('drafts') ?? '{}');
 
 	delete draftData[draftKey];
 
-	localStorage.setItem('drafts', JSON.stringify(draftData));
+	miLocalStorage.setItem('drafts', JSON.stringify(draftData));
 }
 
 async function post(ev?: MouseEvent) {
@@ -622,8 +623,8 @@ async function post(ev?: MouseEvent) {
 			emit('posted');
 			if (postData.text && postData.text !== '') {
 				const hashtags_ = mfm.parse(postData.text).filter(x => x.type === 'hashtag').map(x => x.props.hashtag);
-				const history = JSON.parse(localStorage.getItem('hashtags') || '[]') as string[];
-				localStorage.setItem('hashtags', JSON.stringify(unique(hashtags_.concat(history))));
+				const history = JSON.parse(miLocalStorage.getItem('hashtags') || '[]') as string[];
+				miLocalStorage.setItem('hashtags', JSON.stringify(unique(hashtags_.concat(history))));
 			}
 			posting = false;
 			postAccount = null;
@@ -698,7 +699,7 @@ onMounted(() => {
 	nextTick(() => {
 		// 書きかけの投稿を復元
 		if (!props.instant && !props.mention && !props.specified) {
-			const draft = JSON.parse(localStorage.getItem('drafts') || '{}')[draftKey];
+			const draft = JSON.parse(miLocalStorage.getItem('drafts') || '{}')[draftKey];
 			if (draft) {
 				text = draft.data.text;
 				useCw = draft.data.useCw;
diff --git a/packages/frontend/src/components/MkRemoteCaution.vue b/packages/frontend/src/components/MkRemoteCaution.vue
index d5dc01c1f8..6d398e770d 100644
--- a/packages/frontend/src/components/MkRemoteCaution.vue
+++ b/packages/frontend/src/components/MkRemoteCaution.vue
@@ -1,5 +1,5 @@
 <template>
-<div class="jmgmzlwq _block"><i class="ti ti-alert-triangle" style="margin-right: 8px;"></i>{{ i18n.ts.remoteUserCaution }}<a class="link" :href="href" rel="nofollow noopener" target="_blank">{{ i18n.ts.showOnRemote }}</a></div>
+<div class="jmgmzlwq"><i class="ti ti-alert-triangle" style="margin-right: 8px;"></i>{{ i18n.ts.remoteUserCaution }}<a class="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/MkRetentionHeatmap.vue b/packages/frontend/src/components/MkRetentionHeatmap.vue
index e91b58a4a8..b7886d1dc2 100644
--- a/packages/frontend/src/components/MkRetentionHeatmap.vue
+++ b/packages/frontend/src/components/MkRetentionHeatmap.vue
@@ -10,7 +10,6 @@
 <script lang="ts" setup>
 import { markRaw, version as vueVersion, onMounted, onBeforeUnmount, nextTick } from 'vue';
 import { Chart } from 'chart.js';
-import { enUS } from 'date-fns/locale';
 import tinycolor from 'tinycolor2';
 import { MatrixController, MatrixElement } from 'chartjs-chart-matrix';
 import * as os from '@/os';
@@ -40,7 +39,7 @@ async function renderChart() {
 	const wide = rootEl.offsetWidth > 600;
 	const narrow = rootEl.offsetWidth < 400;
 
-	const maxDays = wide ? 20 : narrow ? 7 : 14;
+	const maxDays = wide ? 15 : narrow ? 5 : 10;
 
 	const raw = await os.api('retention', { });
 
diff --git a/packages/frontend/src/components/MkSample.vue b/packages/frontend/src/components/MkSample.vue
index 1d25ab54b5..ded5ac3f7a 100644
--- a/packages/frontend/src/components/MkSample.vue
+++ b/packages/frontend/src/components/MkSample.vue
@@ -1,6 +1,6 @@
 <template>
-<div class="_card">
-	<div class="_content">
+<div class="">
+	<div class="">
 		<MkInput v-model="text">
 			<template #label>Text</template>
 		</MkInput>
@@ -15,10 +15,10 @@
 		<MkButton inline>This is</MkButton>
 		<MkButton inline primary>the button</MkButton>
 	</div>
-	<div class="_content" style="pointer-events: none;">
+	<div class="" style="pointer-events: none;">
 		<Mfm :text="mfm"/>
 	</div>
-	<div class="_content">
+	<div class="">
 		<MkButton inline primary @click="openMenu">Open menu</MkButton>
 		<MkButton inline primary @click="openDialog">Open dialog</MkButton>
 		<MkButton inline primary @click="openForm">Open form</MkButton>
diff --git a/packages/frontend/src/components/MkSignin.vue b/packages/frontend/src/components/MkSignin.vue
index 96f18f8d61..b87f0643d7 100644
--- a/packages/frontend/src/components/MkSignin.vue
+++ b/packages/frontend/src/components/MkSignin.vue
@@ -1,20 +1,20 @@
 <template>
-<form class="eppvobhk _monolithic_" :class="{ signing, totpLogin }" @submit.prevent="onSubmit">
-	<div class="auth _section _formRoot">
+<form class="eppvobhk" :class="{ signing, totpLogin }" @submit.prevent="onSubmit">
+	<div class="auth _gaps_m">
 		<div v-show="withAvatar" class="avatar" :style="{ backgroundImage: user ? `url('${ user.avatarUrl }')` : null, marginBottom: message ? '1.5em' : null }"></div>
 		<MkInfo v-if="message">
 			{{ message }}
 		</MkInfo>
-		<div v-if="!totpLogin" class="normal-signin">
-			<MkInput v-model="username" class="_formBlock" :placeholder="i18n.ts.username" type="text" pattern="^[a-zA-Z0-9_]+$" :spellcheck="false" autofocus required data-cy-signin-username @update:model-value="onUsernameChange">
+		<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" autofocus required data-cy-signin-username @update:model-value="onUsernameChange">
 				<template #prefix>@</template>
 				<template #suffix>@{{ host }}</template>
 			</MkInput>
-			<MkInput v-if="!user || user && !user.usePasswordLessLogin" v-model="password" class="_formBlock" :placeholder="i18n.ts.password" type="password" :with-password-toggle="true" required data-cy-signin-password>
+			<MkInput v-if="!user || user && !user.usePasswordLessLogin" v-model="password" :placeholder="i18n.ts.password" type="password" :with-password-toggle="true" required data-cy-signin-password>
 				<template #prefix><i class="ti ti-lock"></i></template>
 				<template #caption><button class="_textButton" type="button" @click="resetPassword">{{ i18n.ts.forgotPassword }}</button></template>
 			</MkInput>
-			<MkButton class="_formBlock" type="submit" primary :disabled="signing" style="margin: 0 auto;">{{ signing ? i18n.ts.loggingIn : i18n.ts.login }}</MkButton>
+			<MkButton type="submit" primary :disabled="signing" style="margin: 0 auto;">{{ signing ? i18n.ts.loggingIn : i18n.ts.login }}</MkButton>
 		</div>
 		<div v-if="totpLogin" class="2fa-signin" :class="{ securityKeys: user && user.securityKeys }">
 			<div v-if="user && user.securityKeys" class="twofa-group tap-group">
@@ -40,10 +40,10 @@
 			</div>
 		</div>
 	</div>
-	<div class="social _section">
-		<a v-if="meta && meta.enableTwitterIntegration" class="_borderButton _gap" :href="`${apiUrl}/signin/twitter`"><i class="ti ti-brand-twitter" style="margin-right: 4px;"></i>{{ $t('signinWith', { x: 'Twitter' }) }}</a>
-		<a v-if="meta && meta.enableGithubIntegration" class="_borderButton _gap" :href="`${apiUrl}/signin/github`"><i class="ti ti-brand-github" style="margin-right: 4px;"></i>{{ $t('signinWith', { x: 'GitHub' }) }}</a>
-		<a v-if="meta && meta.enableDiscordIntegration" class="_borderButton _gap" :href="`${apiUrl}/signin/discord`"><i class="ti ti-brand-discord" style="margin-right: 4px;"></i>{{ $t('signinWith', { x: 'Discord' }) }}</a>
+	<div class="social">
+		<a v-if="meta && meta.enableTwitterIntegration" class="_borderButton _margin" :href="`${apiUrl}/signin/twitter`"><i class="ti ti-brand-twitter" style="margin-right: 4px;"></i>{{ $t('signinWith', { x: 'Twitter' }) }}</a>
+		<a v-if="meta && meta.enableGithubIntegration" class="_borderButton _margin" :href="`${apiUrl}/signin/github`"><i class="ti ti-brand-github" style="margin-right: 4px;"></i>{{ $t('signinWith', { x: 'GitHub' }) }}</a>
+		<a v-if="meta && meta.enableDiscordIntegration" class="_borderButton _margin" :href="`${apiUrl}/signin/discord`"><i class="ti ti-brand-discord" style="margin-right: 4px;"></i>{{ $t('signinWith', { x: 'Discord' }) }}</a>
 	</div>
 </form>
 </template>
diff --git a/packages/frontend/src/components/MkSigninDialog.vue b/packages/frontend/src/components/MkSigninDialog.vue
index 5015d09e64..83506b8f66 100644
--- a/packages/frontend/src/components/MkSigninDialog.vue
+++ b/packages/frontend/src/components/MkSigninDialog.vue
@@ -1,5 +1,5 @@
 <template>
-<XModalWindow
+<MkModalWindow
 	ref="dialog"
 	:width="370"
 	:height="400"
@@ -8,14 +8,16 @@
 >
 	<template #header>{{ i18n.ts.login }}</template>
 
-	<MkSignin :auto-set="autoSet" :message="message" @login="onLogin"/>
-</XModalWindow>
+	<MkSpacer :margin-min="20" :margin-max="28">
+		<MkSignin :auto-set="autoSet" :message="message" @login="onLogin"/>
+	</MkSpacer>
+</MkModalWindow>
 </template>
 
 <script lang="ts" setup>
 import { } from 'vue';
 import MkSignin from '@/components/MkSignin.vue';
-import XModalWindow from '@/components/MkModalWindow.vue';
+import MkModalWindow from '@/components/MkModalWindow.vue';
 import { i18n } from '@/i18n';
 
 const props = withDefaults(defineProps<{
@@ -32,7 +34,7 @@ const emit = defineEmits<{
 	(ev: 'cancelled'): void;
 }>();
 
-const dialog = $shallowRef<InstanceType<typeof XModalWindow>>();
+const dialog = $shallowRef<InstanceType<typeof MkModalWindow>>();
 
 function onClose() {
 	emit('cancelled');
diff --git a/packages/frontend/src/components/MkSignup.vue b/packages/frontend/src/components/MkSignup.vue
index d987425ca3..dcffee47bd 100644
--- a/packages/frontend/src/components/MkSignup.vue
+++ b/packages/frontend/src/components/MkSignup.vue
@@ -1,10 +1,10 @@
 <template>
-<form class="qlvuhzng _formRoot" autocomplete="new-password" @submit.prevent="onSubmit">
-	<MkInput v-if="instance.disableRegistration" v-model="invitationCode" class="_formBlock" type="text" :spellcheck="false" required>
+<form class="qlvuhzng _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="ti ti-key"></i></template>
 	</MkInput>
-	<MkInput v-model="username" class="_formBlock" type="text" pattern="^[a-zA-Z0-9_]{1,20}$" :spellcheck="false" required data-cy-signup-username @update:model-value="onChangeUsername">
+	<MkInput v-model="username" type="text" pattern="^[a-zA-Z0-9_]{1,20}$" :spellcheck="false" required data-cy-signup-username @update:model-value="onChangeUsername">
 		<template #label>{{ i18n.ts.username }} <div v-tooltip:dialog="i18n.ts.usernameInfo" class="_button _help"><i class="ti ti-question-circle"></i></div></template>
 		<template #prefix>@</template>
 		<template #suffix>@{{ host }}</template>
@@ -18,7 +18,7 @@
 			<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" class="_formBlock" :debounce="true" type="email" :spellcheck="false" required data-cy-signup-email @update:model-value="onChangeEmail">
+	<MkInput v-if="instance.emailRequiredForSignup" v-model="email" :debounce="true" type="email" :spellcheck="false" required data-cy-signup-email @update:model-value="onChangeEmail">
 		<template #label>{{ i18n.ts.emailAddress }} <div v-tooltip:dialog="i18n.ts._signup.emailAddressInfo" class="_button _help"><i class="ti ti-question-circle"></i></div></template>
 		<template #prefix><i class="ti ti-mail"></i></template>
 		<template #caption>
@@ -33,7 +33,7 @@
 			<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" class="_formBlock" type="password" autocomplete="new-password" required data-cy-signup-password @update:model-value="onChangePassword">
+	<MkInput v-model="password" type="password" autocomplete="new-password" required data-cy-signup-password @update:model-value="onChangePassword">
 		<template #label>{{ i18n.ts.password }}</template>
 		<template #prefix><i class="ti ti-lock"></i></template>
 		<template #caption>
@@ -42,7 +42,7 @@
 			<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" class="_formBlock" type="password" autocomplete="new-password" required data-cy-signup-password-retype @update:model-value="onChangePasswordRetype">
+	<MkInput v-model="retypedPassword" type="password" autocomplete="new-password" required data-cy-signup-password-retype @update:model-value="onChangePasswordRetype">
 		<template #label>{{ i18n.ts.password }} ({{ i18n.ts.retype }})</template>
 		<template #prefix><i class="ti ti-lock"></i></template>
 		<template #caption>
@@ -50,17 +50,17 @@
 			<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>
-	<MkSwitch v-if="instance.tosUrl" v-model="ToSAgreement" class="_formBlock tou">
+	<MkSwitch v-if="instance.tosUrl" v-model="ToSAgreement" class="tou">
 		<I18n :src="i18n.ts.agreeTo">
 			<template #0>
 				<a :href="instance.tosUrl" class="_link" target="_blank">{{ i18n.ts.tos }}</a>
 			</template>
 		</I18n>
 	</MkSwitch>
-	<MkCaptcha v-if="instance.enableHcaptcha" ref="hcaptcha" v-model="hCaptchaResponse" class="_formBlock captcha" provider="hcaptcha" :sitekey="instance.hcaptchaSiteKey"/>
-	<MkCaptcha v-if="instance.enableRecaptcha" ref="recaptcha" v-model="reCaptchaResponse" class="_formBlock captcha" provider="recaptcha" :sitekey="instance.recaptchaSiteKey"/>
-	<MkCaptcha v-if="instance.enableTurnstile" ref="turnstile" v-model="turnstileResponse" class="_formBlock captcha" provider="turnstile" :sitekey="instance.turnstileSiteKey"/>
-	<MkButton class="_formBlock" type="submit" :disabled="shouldDisableSubmitting" gradate data-cy-signup-submit>{{ i18n.ts.start }}</MkButton>
+	<MkCaptcha v-if="instance.enableHcaptcha" ref="hcaptcha" v-model="hCaptchaResponse" class="captcha" provider="hcaptcha" :sitekey="instance.hcaptchaSiteKey"/>
+	<MkCaptcha v-if="instance.enableRecaptcha" ref="recaptcha" v-model="reCaptchaResponse" class="captcha" provider="recaptcha" :sitekey="instance.recaptchaSiteKey"/>
+	<MkCaptcha v-if="instance.enableTurnstile" ref="turnstile" v-model="turnstileResponse" class="captcha" provider="turnstile" :sitekey="instance.turnstileSiteKey"/>
+	<MkButton type="submit" :disabled="shouldDisableSubmitting" gradate data-cy-signup-submit>{{ i18n.ts.start }}</MkButton>
 </form>
 </template>
 
diff --git a/packages/frontend/src/components/MkSignupDialog.vue b/packages/frontend/src/components/MkSignupDialog.vue
index e1b76474a0..790c1e94df 100644
--- a/packages/frontend/src/components/MkSignupDialog.vue
+++ b/packages/frontend/src/components/MkSignupDialog.vue
@@ -1,5 +1,5 @@
 <template>
-<XModalWindow
+<MkModalWindow
 	ref="dialog"
 	:width="366"
 	:height="500"
@@ -8,18 +8,16 @@
 >
 	<template #header>{{ i18n.ts.signup }}</template>
 
-	<div class="_monolithic_">
-		<div class="_section">
-			<XSignup :auto-set="autoSet" @signup="onSignup" @signup-email-pending="onSignupEmailPending"/>
-		</div>
-	</div>
-</XModalWindow>
+	<MkSpacer :margin-min="20" :margin-max="28">
+		<XSignup :auto-set="autoSet" @signup="onSignup" @signup-email-pending="onSignupEmailPending"/>
+	</MkSpacer>
+</MkModalWindow>
 </template>
 
 <script lang="ts" setup>
 import { } from 'vue';
 import XSignup from '@/components/MkSignup.vue';
-import XModalWindow from '@/components/MkModalWindow.vue';
+import MkModalWindow from '@/components/MkModalWindow.vue';
 import { i18n } from '@/i18n';
 
 const props = withDefaults(defineProps<{
@@ -33,7 +31,7 @@ const emit = defineEmits<{
 	(ev: 'closed'): void;
 }>();
 
-const dialog = $shallowRef<InstanceType<typeof XModalWindow>>();
+const dialog = $shallowRef<InstanceType<typeof MkModalWindow>>();
 
 function onSignup(res) {
 	emit('done', res);
diff --git a/packages/frontend/src/components/MkSuperMenu.vue b/packages/frontend/src/components/MkSuperMenu.vue
index e79794aea4..bb2a789b3f 100644
--- a/packages/frontend/src/components/MkSuperMenu.vue
+++ b/packages/frontend/src/components/MkSuperMenu.vue
@@ -62,7 +62,7 @@ export default defineComponent({
 				align-items: center;
 				width: 100%;
 				box-sizing: border-box;
-				padding: 10px 16px 10px 8px;
+				padding: 9px 16px 9px 8px;
 				border-radius: 9px;
 				font-size: 0.9em;
 
@@ -141,8 +141,8 @@ export default defineComponent({
 						margin-right: 0;
 						margin-bottom: 6px;
 						font-size: 1.5em;
-						width: 54px;
-						height: 54px;
+						width: 60px;
+						height: 60px;
 						aspect-ratio: 1;
 						background: var(--panel);
 						border-radius: 100%;
diff --git a/packages/frontend/src/components/MkTokenGenerateWindow.vue b/packages/frontend/src/components/MkTokenGenerateWindow.vue
index 8d5b6f8635..5e12aa8319 100644
--- a/packages/frontend/src/components/MkTokenGenerateWindow.vue
+++ b/packages/frontend/src/components/MkTokenGenerateWindow.vue
@@ -1,5 +1,5 @@
 <template>
-<XModalWindow
+<MkModalWindow
 	ref="dialog"
 	:width="400"
 	:height="450"
@@ -11,21 +11,26 @@
 	@ok="ok()"
 >
 	<template #header>{{ title || $ts.generateAccessToken }}</template>
-	<div v-if="information" class="_section">
-		<MkInfo warn>{{ information }}</MkInfo>
-	</div>
-	<div class="_section">
-		<MkInput v-model="name">
-			<template #label>{{ $ts.name }}</template>
-		</MkInput>
-	</div>
-	<div class="_section">
-		<div style="margin-bottom: 16px;"><b>{{ $ts.permission }}</b></div>
-		<MkButton inline @click="disableAll">{{ $ts.disableAll }}</MkButton>
-		<MkButton inline @click="enableAll">{{ $ts.enableAll }}</MkButton>
-		<MkSwitch v-for="kind in (initialPermissions || kinds)" :key="kind" v-model="permissions[kind]">{{ $t(`_permissions.${kind}`) }}</MkSwitch>
-	</div>
-</XModalWindow>
+
+	<MkSpacer :margin-min="20" :margin-max="28">
+		<div class="_gaps_m">
+			<div v-if="information">
+				<MkInfo warn>{{ information }}</MkInfo>
+			</div>
+			<div>
+				<MkInput v-model="name">
+					<template #label>{{ $ts.name }}</template>
+				</MkInput>
+			</div>
+			<div><b>{{ $ts.permission }}</b></div>
+			<div class="_buttons">
+				<MkButton inline @click="disableAll">{{ i18n.ts.disableAll }}</MkButton>
+				<MkButton inline @click="enableAll">{{ i18n.ts.enableAll }}</MkButton>
+			</div>
+			<MkSwitch v-for="kind in (initialPermissions || kinds)" :key="kind" v-model="permissions[kind]">{{ $t(`_permissions.${kind}`) }}</MkSwitch>
+		</div>
+	</MkSpacer>
+</MkModalWindow>
 </template>
 
 <script lang="ts" setup>
@@ -35,7 +40,8 @@ import MkInput from './form/input.vue';
 import MkSwitch from './form/switch.vue';
 import MkButton from './MkButton.vue';
 import MkInfo from './MkInfo.vue';
-import XModalWindow from '@/components/MkModalWindow.vue';
+import MkModalWindow from '@/components/MkModalWindow.vue';
+import { i18n } from '@/i18n';
 
 const props = withDefaults(defineProps<{
 	title?: string | null;
@@ -54,7 +60,7 @@ const emit = defineEmits<{
 	(ev: 'done', result: { name: string | null, permissions: string[] }): void;
 }>();
 
-const dialog = $shallowRef<InstanceType<typeof XModalWindow>>();
+const dialog = $shallowRef<InstanceType<typeof MkModalWindow>>();
 let name = $ref(props.initialName);
 let permissions = $ref({});
 
diff --git a/packages/frontend/src/components/MkUpdated.vue b/packages/frontend/src/components/MkUpdated.vue
index 2f2864220e..1f539b154c 100644
--- a/packages/frontend/src/components/MkUpdated.vue
+++ b/packages/frontend/src/components/MkUpdated.vue
@@ -10,12 +10,13 @@
 </template>
 
 <script lang="ts" setup>
-import { shallowRef } from 'vue';
+import { onMounted, shallowRef } from 'vue';
 import MkModal from '@/components/MkModal.vue';
 import MkButton from '@/components/MkButton.vue';
 import MkSparkle from '@/components/MkSparkle.vue';
 import { version } from '@/config';
 import { i18n } from '@/i18n';
+import { confetti } from '@/scripts/confetti';
 
 const modal = shallowRef<InstanceType<typeof MkModal>>();
 
@@ -23,6 +24,12 @@ const whatIsNew = () => {
 	modal.value.close();
 	window.open(`https://misskey-hub.net/docs/releases.html#_${version.replace(/\./g, '-')}`, '_blank');
 };
+
+onMounted(() => {
+	confetti({
+		duration: 1000 * 3,
+	});
+});
 </script>
 
 <style lang="scss" scoped>
diff --git a/packages/frontend/src/components/MkUserSelectDialog.vue b/packages/frontend/src/components/MkUserSelectDialog.vue
index 1d31769c30..c8a2fd8cc1 100644
--- a/packages/frontend/src/components/MkUserSelectDialog.vue
+++ b/packages/frontend/src/components/MkUserSelectDialog.vue
@@ -1,5 +1,5 @@
 <template>
-<XModalWindow
+<MkModalWindow
 	ref="dialogEl"
 	:with-ok-button="true"
 	:ok-button-disabled="selected == null"
@@ -48,7 +48,7 @@
 			</div>
 		</div>
 	</div>
-</XModalWindow>
+</MkModalWindow>
 </template>
 
 <script lang="ts" setup>
@@ -56,7 +56,7 @@ import { nextTick, onMounted } from 'vue';
 import * as misskey from 'misskey-js';
 import MkInput from '@/components/form/input.vue';
 import FormSplit from '@/components/form/split.vue';
-import XModalWindow from '@/components/MkModalWindow.vue';
+import MkModalWindow from '@/components/MkModalWindow.vue';
 import * as os from '@/os';
 import { defaultStore } from '@/store';
 import { i18n } from '@/i18n';
diff --git a/packages/frontend/src/components/MkWindow.vue b/packages/frontend/src/components/MkWindow.vue
index 0f7e0e4f2e..401f0f4a2e 100644
--- a/packages/frontend/src/components/MkWindow.vue
+++ b/packages/frontend/src/components/MkWindow.vue
@@ -1,7 +1,7 @@
 <template>
 <Transition :name="$store.state.animation ? 'window' : ''" appear @after-leave="$emit('closed')">
 	<div v-if="showing" ref="rootEl" class="ebkgocck" :class="{ maximized }">
-		<div class="body _shadow _narrow_" @mousedown="onBodyMousedown" @keydown="onKeydown">
+		<div class="body _shadow" @mousedown="onBodyMousedown" @keydown="onKeydown">
 			<div class="header" :class="{ mini }" @contextmenu.prevent.stop="onContextmenu">
 				<span class="left">
 					<button v-for="button in buttonsLeft" v-tooltip="button.title" class="button _button" :class="{ highlighted: button.highlighted }" @click="button.onClick"><i :class="button.icon"></i></button>
@@ -489,6 +489,7 @@ defineExpose({
 			flex: 1;
 			overflow: auto;
 			background: var(--panel);
+			container-type: inline-size;
 		}
 	}
 
diff --git a/packages/frontend/src/components/MkYoutubePlayer.vue b/packages/frontend/src/components/MkYoutubePlayer.vue
index c12b03572d..d1f1f9e9c5 100644
--- a/packages/frontend/src/components/MkYoutubePlayer.vue
+++ b/packages/frontend/src/components/MkYoutubePlayer.vue
@@ -1,5 +1,5 @@
 <template>
-<XWindow :initial-width="640" :initial-height="402" :can-resize="true" :close-button="true">
+<MkWindow :initial-width="640" :initial-height="402" :can-resize="true" :close-button="true">
 	<template #header>
 		<i class="icon ti ti-brand-youtube" style="margin-right: 0.5em;"></i>
 		<span>{{ title ?? 'YouTube' }}</span>
@@ -14,11 +14,11 @@
 		<MkLoading v-if="fetching"/>
 		<MkError v-else-if="!player.url" @retry="ytFetch()"/>
 	</div>
-</XWindow>
+</MkWindow>
 </template>
 
 <script lang="ts" setup>
-import XWindow from '@/components/MkWindow.vue';
+import MkWindow from '@/components/MkWindow.vue';
 import { versatileLang } from '@/scripts/intl-const';
 
 const props = defineProps<{
diff --git a/packages/frontend/src/components/form/folder.vue b/packages/frontend/src/components/form/folder.vue
index 40bbc97002..961f1c1cac 100644
--- a/packages/frontend/src/components/form/folder.vue
+++ b/packages/frontend/src/components/form/folder.vue
@@ -1,5 +1,5 @@
 <template>
-<div class="dwzlatin" :class="{ opened }">
+<div ref="rootEl" class="dwzlatin" :class="{ opened }">
 	<div class="header _button" @click="toggle">
 		<span class="icon"><slot name="icon"></slot></span>
 		<span class="text"><slot name="label"></slot></span>
@@ -9,7 +9,7 @@
 			<i v-else class="ti ti-chevron-down icon"></i>
 		</span>
 	</div>
-	<div v-if="openedAtLeastOnce" class="body">
+	<div v-if="openedAtLeastOnce" class="body" :class="{ bgSame }">
 		<Transition
 			:name="$store.state.animation ? 'folder-toggle' : ''"
 			@enter="enter"
@@ -30,7 +30,7 @@
 </template>
 
 <script lang="ts" setup>
-import { nextTick } from 'vue';
+import { nextTick, onMounted } from 'vue';
 
 const props = withDefaults(defineProps<{
 	defaultOpen: boolean;
@@ -38,6 +38,17 @@ const props = withDefaults(defineProps<{
 	defaultOpen: false,
 });
 
+const getBgColor = (el: HTMLElement) => {
+	const style = window.getComputedStyle(el);
+	if (style.backgroundColor && !['rgba(0, 0, 0, 0)', 'rgba(0,0,0,0)', 'transparent'].includes(style.backgroundColor)) {
+		return style.backgroundColor;
+	} else {
+		return el.parentElement ? getBgColor(el.parentElement) : 'transparent';
+	}
+};
+
+let rootEl = $ref<HTMLElement>();
+let bgSame = $ref(false);
 let opened = $ref(props.defaultOpen);
 let openedAtLeastOnce = $ref(props.defaultOpen);
 
@@ -72,6 +83,13 @@ function toggle() {
 		opened = !opened;
 	});
 }
+
+onMounted(() => {
+	const computedStyle = getComputedStyle(document.documentElement);
+	const parentBg = getBgColor(rootEl.parentElement);
+	const myBg = computedStyle.getPropertyValue('--panel');
+	bgSame = parentBg === myBg;
+});
 </script>
 
 <style lang="scss" scoped>
@@ -142,6 +160,10 @@ function toggle() {
 		background: var(--panel);
 		border-radius: 0 0 6px 6px;
 		container-type: inline-size;
+
+		&.bgSame {
+			background: var(--bg);
+		}
 	}
 
 	&.opened {
diff --git a/packages/frontend/src/components/form/input.vue b/packages/frontend/src/components/form/input.vue
index 4f3e50c31a..f72e1429f5 100644
--- a/packages/frontend/src/components/form/input.vue
+++ b/packages/frontend/src/components/form/input.vue
@@ -78,8 +78,8 @@ const inputEl = shallowRef<HTMLElement>();
 const prefixEl = shallowRef<HTMLElement>();
 const suffixEl = shallowRef<HTMLElement>();
 const height =
-	props.small ? 35 :
-	props.large ? 39 :
+	props.small ? 34 :
+	props.large ? 40 :
 	37;
 
 const focus = () => inputEl.value.focus();
diff --git a/packages/frontend/src/components/form/section.vue b/packages/frontend/src/components/form/section.vue
index c6e34ef1cc..a838164978 100644
--- a/packages/frontend/src/components/form/section.vue
+++ b/packages/frontend/src/components/form/section.vue
@@ -1,35 +1,27 @@
 <template>
-<div class="vrtktovh _formBlock">
+<div class="vrtktovh" :class="{ first }">
 	<div class="label"><slot name="label"></slot></div>
-	<div class="main _formRoot">
+	<div class="main">
 		<slot></slot>
 	</div>
 </div>
 </template>
 
 <script lang="ts" setup>
+defineProps<{
+	first?: boolean;
+}>();
 </script>
 
 <style lang="scss" scoped>
 .vrtktovh {
 	border-top: solid 0.5px var(--divider);
-	border-bottom: solid 0.5px var(--divider);
-
-	& + .vrtktovh {
-		border-top: none;
-	}
-
-	&:first-child {
-		border-top: none;
-	}
-
-	&:last-child {
-		border-bottom: none;
-	}
+	//border-bottom: solid 0.5px var(--divider);
 
 	> .label {
 		font-weight: bold;
-		margin: 1.5em 0 16px 0;
+		padding: 1.5em 0 0 0;
+		margin: 0 0 16px 0;
 
 		&:empty {
 			display: none;
@@ -37,7 +29,15 @@
 	}
 
 	> .main {
-		margin: 1.5em 0;
+		margin: 1.5em 0 0 0;
+	}
+
+	&.first {
+		border-top: none;
+
+		> .label {
+			padding-top: 0;
+		}
 	}
 }
 </style>
diff --git a/packages/frontend/src/components/form/select.vue b/packages/frontend/src/components/form/select.vue
index 2cd5ae6f4a..4b5a14f5be 100644
--- a/packages/frontend/src/components/form/select.vue
+++ b/packages/frontend/src/components/form/select.vue
@@ -18,7 +18,7 @@
 		>
 			<slot></slot>
 		</select>
-		<div ref="suffixEl" class="suffix"><i class="ti ti-chevron-down"></i></div>
+		<div ref="suffixEl" class="suffix"><i class="ti ti-chevron-down" :class="[$style.chevron, { [$style.chevronOpening]: opening }]"></i></div>
 	</div>
 	<div class="caption"><slot name="caption"></slot></div>
 
@@ -56,6 +56,7 @@ const slots = useSlots();
 const { modelValue, autofocus } = toRefs(props);
 const v = ref(modelValue.value);
 const focused = ref(false);
+const opening = ref(false);
 const changed = ref(false);
 const invalid = ref(false);
 const filled = computed(() => v.value !== '' && v.value != null);
@@ -64,8 +65,8 @@ const prefixEl = ref(null);
 const suffixEl = ref(null);
 const container = ref(null);
 const height =
-	props.small ? 35 :
-	props.large ? 39 :
+	props.small ? 34 :
+	props.large ? 40 :
 	37;
 
 const focus = () => inputEl.value.focus();
@@ -119,6 +120,7 @@ onMounted(() => {
 
 const onClick = (ev: MouseEvent) => {
 	focused.value = true;
+	opening.value = true;
 
 	const menu = [];
 	let options = slots.default!();
@@ -126,7 +128,7 @@ const onClick = (ev: MouseEvent) => {
 	const pushOption = (option: VNode) => {
 		menu.push({
 			text: option.children,
-			active: v.value === option.props.value,
+			active: computed(() => v.value === option.props.value),
 			action: () => {
 				v.value = option.props.value;
 			},
@@ -158,6 +160,9 @@ const onClick = (ev: MouseEvent) => {
 
 	os.popupMenu(menu, container.value, {
 		width: container.value.offsetWidth,
+		onClosing: () => {
+			opening.value = false;
+		},
 	}).then(() => {
 		focused.value = false;
 	});
@@ -277,3 +282,13 @@ const onClick = (ev: MouseEvent) => {
 	}
 }
 </style>
+
+<style lang="scss" module>
+.chevron {
+	transition: transform 0.5s ease;
+}
+
+.chevronOpening {
+	transform: rotateX(180deg);
+}
+</style>
diff --git a/packages/frontend/src/components/form/split.vue b/packages/frontend/src/components/form/split.vue
index 301a8a84e5..3cee41e3d6 100644
--- a/packages/frontend/src/components/form/split.vue
+++ b/packages/frontend/src/components/form/split.vue
@@ -1,5 +1,5 @@
 <template>
-<div class="terlnhxf _formBlock">
+<div class="terlnhxf">
 	<slot></slot>
 </div>
 </template>
diff --git a/packages/frontend/src/components/global/MkSpacer.vue b/packages/frontend/src/components/global/MkSpacer.vue
index 1ddb230bd6..78e9a1a9c2 100644
--- a/packages/frontend/src/components/global/MkSpacer.vue
+++ b/packages/frontend/src/components/global/MkSpacer.vue
@@ -38,13 +38,13 @@ const forceSpacerMin = inject('forceSpacerMin', false) || deviceKind === 'smartp
 	container-type: inline-size;
 }
 
-@container (max-width: 360px) {
+@container (max-width: 450px) {
 	.root {
 		padding: v-bind('props.marginMin + "px"');
 	}
 }
 
-@container (min-width: 361px) {
+@container (min-width: 451px) {
 	.root {
 		padding: v-bind('props.marginMax + "px"');
 	}
diff --git a/packages/frontend/src/components/global/MkTime.vue b/packages/frontend/src/components/global/MkTime.vue
index 0bbb0f5399..66c0bd5135 100644
--- a/packages/frontend/src/components/global/MkTime.vue
+++ b/packages/frontend/src/components/global/MkTime.vue
@@ -36,22 +36,21 @@ const relative = $computed(() => {
 		i18n.ts._ago.future);
 });
 
-function tick() {
-	// TODO: パフォーマンス向上のため、このコンポーネントが画面内に表示されている場合のみ更新する
-	now = new Date();
-
-	tickId = window.setTimeout(() => {
-		window.requestAnimationFrame(tick);
-	}, 10000);
-}
-
 let tickId: number;
 
+function tick() {
+	now = new Date();
+	const ago = (now.getTime() - _time.getTime()) / 1000/*ms*/;
+	const next = ago < 60 ? 10000 : ago < 3600 ? 60000 : 180000;
+
+	tickId = window.setTimeout(tick, next);
+}
+
 if (props.mode === 'relative' || props.mode === 'detail') {
-	tickId = window.requestAnimationFrame(tick);
+	tick();
 
 	onUnmounted(() => {
-		window.cancelAnimationFrame(tickId);
+		window.clearTimeout(tickId);
 	});
 }
 </script>
diff --git a/packages/frontend/src/config.ts b/packages/frontend/src/config.ts
index f2022b0f02..4b084d365b 100644
--- a/packages/frontend/src/config.ts
+++ b/packages/frontend/src/config.ts
@@ -1,3 +1,5 @@
+import { miLocalStorage } from "./local-storage";
+
 const address = new URL(location.href);
 const siteName = (document.querySelector('meta[property="og:site_name"]') as HTMLMetaElement)?.content;
 
@@ -6,10 +8,10 @@ export const hostname = address.hostname;
 export const url = address.origin;
 export const apiUrl = url + '/api';
 export const wsUrl = url.replace('http://', 'ws://').replace('https://', 'wss://') + '/streaming';
-export const lang = localStorage.getItem('lang');
+export const lang = miLocalStorage.getItem('lang');
 export const langs = _LANGS_;
-export const locale = JSON.parse(localStorage.getItem('locale'));
+export const locale = JSON.parse(miLocalStorage.getItem('locale'));
 export const version = _VERSION_;
 export const instanceName = siteName === 'Misskey' ? host : siteName;
-export const ui = localStorage.getItem('ui');
-export const debug = localStorage.getItem('debug') === 'true';
+export const ui = miLocalStorage.getItem('ui');
+export const debug = miLocalStorage.getItem('debug') === 'true';
diff --git a/packages/frontend/src/init.ts b/packages/frontend/src/init.ts
index 508d3262b3..e10315e1ad 100644
--- a/packages/frontend/src/init.ts
+++ b/packages/frontend/src/init.ts
@@ -9,9 +9,12 @@ import '@/style.scss';
 //#region account indexedDB migration
 import { set } from '@/scripts/idb-proxy';
 
-if (localStorage.getItem('accounts') != null) {
-	set('accounts', JSON.parse(localStorage.getItem('accounts')));
-	localStorage.removeItem('accounts');
+{
+	const accounts = miLocalStorage.getItem('accounts');
+	if (accounts) {
+		set('accounts', JSON.parse(accounts));
+		miLocalStorage.removeItem('accounts');
+	}
 }
 //#endregion
 
@@ -40,6 +43,7 @@ import { reloadChannel } from '@/scripts/unison-reload';
 import { reactionPicker } from '@/scripts/reaction-picker';
 import { getUrlWithoutLoginId } from '@/scripts/login-id';
 import { getAccountFromId } from '@/scripts/get-account-from-id';
+import { miLocalStorage } from './local-storage';
 
 (async () => {
 	console.info(`Misskey v${version}`);
@@ -154,7 +158,7 @@ import { getAccountFromId } from '@/scripts/get-account-from-id';
 	const fetchInstanceMetaPromise = fetchInstance();
 
 	fetchInstanceMetaPromise.then(() => {
-		localStorage.setItem('v', instance.version);
+		miLocalStorage.setItem('v', instance.version);
 
 		// Init service worker
 		initializeSw();
@@ -172,6 +176,7 @@ import { getAccountFromId } from '@/scripts/get-account-from-id';
 		app.config.performance = true;
 	}
 
+	// TODO: 廃止
 	app.config.globalProperties = {
 		$i,
 		$store: defaultStore,
@@ -222,12 +227,12 @@ import { getAccountFromId } from '@/scripts/get-account-from-id';
 	}
 
 	// クライアントが更新されたか?
-	const lastVersion = localStorage.getItem('lastVersion');
+	const lastVersion = miLocalStorage.getItem('lastVersion');
 	if (lastVersion !== version) {
-		localStorage.setItem('lastVersion', version);
+		miLocalStorage.setItem('lastVersion', version);
 
 		// テーマリビルドするため
-		localStorage.removeItem('theme');
+		miLocalStorage.removeItem('theme');
 
 		try { // 変なバージョン文字列来るとcompareVersionsでエラーになるため
 			if (lastVersion != null && compareVersions(version, lastVersion) === 1) {
@@ -243,7 +248,7 @@ import { getAccountFromId } from '@/scripts/get-account-from-id';
 	// NOTE: この処理は必ず↑のクライアント更新時処理より後に来ること(テーマ再構築のため)
 	watch(defaultStore.reactiveState.darkMode, (darkMode) => {
 		applyTheme(darkMode ? ColdDeviceStorage.get('darkTheme') : ColdDeviceStorage.get('lightTheme'));
-	}, { immediate: localStorage.theme == null });
+	}, { immediate: miLocalStorage.getItem('theme') == null });
 
 	const darkTheme = computed(ColdDeviceStorage.makeGetterSetter('darkTheme'));
 	const lightTheme = computed(ColdDeviceStorage.makeGetterSetter('lightTheme'));
@@ -340,7 +345,7 @@ import { getAccountFromId } from '@/scripts/get-account-from-id';
 			});
 		}
 
-		const lastUsed = localStorage.getItem('lastUsed');
+		const lastUsed = miLocalStorage.getItem('lastUsed');
 		if (lastUsed) {
 			const lastUsedDate = parseInt(lastUsed, 10);
 			// 二時間以上前なら
@@ -350,7 +355,15 @@ import { getAccountFromId } from '@/scripts/get-account-from-id';
 				}));
 			}
 		}
-		localStorage.setItem('lastUsed', Date.now().toString());
+		miLocalStorage.setItem('lastUsed', Date.now().toString());
+
+		const latestDonationInfoShownAt = miLocalStorage.getItem('latestDonationInfoShownAt');
+		const neverShowDonationInfo = miLocalStorage.getItem('neverShowDonationInfo');
+		if (neverShowDonationInfo !== 'true' && (new Date($i.createdAt).getTime() < (Date.now() - (1000 * 60 * 60 * 24 * 3)))) {
+			if (latestDonationInfoShownAt == null || (new Date(latestDonationInfoShownAt).getTime() < (Date.now() - (1000 * 60 * 60 * 24 * 30)))) {
+				popup(defineAsyncComponent(() => import('@/components/MkDonation.vue')), {}, {}, 'closed');
+			}
+		}
 
 		if ('Notification' in window) {
 			// 許可を得ていなかったらリクエスト
diff --git a/packages/frontend/src/instance.ts b/packages/frontend/src/instance.ts
index 51464f32fb..82d3e7aea2 100644
--- a/packages/frontend/src/instance.ts
+++ b/packages/frontend/src/instance.ts
@@ -1,10 +1,11 @@
 import { computed, reactive } from 'vue';
 import * as Misskey from 'misskey-js';
 import { api } from './os';
+import { miLocalStorage } from './local-storage';
 
 // TODO: 他のタブと永続化されたstateを同期
 
-const instanceData = localStorage.getItem('instance');
+const instanceData = miLocalStorage.getItem('instance');
 
 // TODO: instanceをリアクティブにするかは再考の余地あり
 
@@ -21,7 +22,7 @@ export async function fetchInstance() {
 		instance[k] = v;
 	}
 
-	localStorage.setItem('instance', JSON.stringify(instance));
+	miLocalStorage.setItem('instance', JSON.stringify(instance));
 }
 
 export const emojiCategories = computed(() => {
diff --git a/packages/frontend/src/local-storage.ts b/packages/frontend/src/local-storage.ts
new file mode 100644
index 0000000000..50e28d621f
--- /dev/null
+++ b/packages/frontend/src/local-storage.ts
@@ -0,0 +1,33 @@
+type Keys =
+	'v' |
+	'lastVersion' |
+	'instance' |
+	'account' |
+	'accounts' |
+	'latestDonationInfoShownAt' |
+	'neverShowDonationInfo' |
+	'lastUsed' |
+	'lang' |
+	'drafts' |
+	'hashtags' |
+	'wallpaper' |
+	'theme' |
+	'colorSchema' |
+	'useSystemFont' | 
+	'fontSize' |
+	'ui' |
+	'locale' |
+	'theme' |
+	'customCss' |
+	'message_drafts' |
+	'scratchpad' |
+	`miux:${string}` |
+	`ui:folder:${string}` |
+	`themes:${string}` |
+	`aiscript:${string}`;
+
+export const miLocalStorage = {
+	getItem: (key: Keys) => window.localStorage.getItem(key),
+	setItem: (key: Keys, value: string) => window.localStorage.setItem(key, value),
+	removeItem: (key: Keys) => window.localStorage.removeItem(key),
+};
diff --git a/packages/frontend/src/navbar.ts b/packages/frontend/src/navbar.ts
index 31e6cd64a4..9ee78741dc 100644
--- a/packages/frontend/src/navbar.ts
+++ b/packages/frontend/src/navbar.ts
@@ -5,128 +5,134 @@ import * as os from '@/os';
 import { i18n } from '@/i18n';
 import { ui } from '@/config';
 import { unisonReload } from '@/scripts/unison-reload';
+import { miLocalStorage } from './local-storage';
 
 export const navbarItemDef = reactive({
 	notifications: {
-		title: 'notifications',
+		title: i18n.ts.notifications,
 		icon: 'ti ti-bell',
 		show: computed(() => $i != null),
 		indicated: computed(() => $i != null && $i.hasUnreadNotification),
 		to: '/my/notifications',
 	},
 	messaging: {
-		title: 'messaging',
+		title: i18n.ts.messaging,
 		icon: 'ti ti-messages',
 		show: computed(() => $i != null),
 		indicated: computed(() => $i != null && $i.hasUnreadMessagingMessage),
 		to: '/my/messaging',
 	},
 	drive: {
-		title: 'drive',
+		title: i18n.ts.drive,
 		icon: 'ti ti-cloud',
 		show: computed(() => $i != null),
 		to: '/my/drive',
 	},
 	followRequests: {
-		title: 'followRequests',
+		title: i18n.ts.followRequests,
 		icon: 'ti ti-user-plus',
 		show: computed(() => $i != null && $i.isLocked),
 		indicated: computed(() => $i != null && $i.hasPendingReceivedFollowRequest),
 		to: '/my/follow-requests',
 	},
 	explore: {
-		title: 'explore',
+		title: i18n.ts.explore,
 		icon: 'ti ti-hash',
 		to: '/explore',
 	},
 	announcements: {
-		title: 'announcements',
+		title: i18n.ts.announcements,
 		icon: 'ti ti-speakerphone',
 		indicated: computed(() => $i != null && $i.hasUnreadAnnouncement),
 		to: '/announcements',
 	},
 	search: {
-		title: 'search',
+		title: i18n.ts.search,
 		icon: 'ti ti-search',
 		action: () => search(),
 	},
 	lists: {
-		title: 'lists',
+		title: i18n.ts.lists,
 		icon: 'ti ti-list',
 		show: computed(() => $i != null),
 		to: '/my/lists',
 	},
 	/*
 	groups: {
-		title: 'groups',
+		title: i18n.ts.groups,
 		icon: 'ti ti-users',
 		show: computed(() => $i != null),
 		to: '/my/groups',
 	},
 	*/
 	antennas: {
-		title: 'antennas',
+		title: i18n.ts.antennas,
 		icon: 'ti ti-antenna',
 		show: computed(() => $i != null),
 		to: '/my/antennas',
 	},
 	favorites: {
-		title: 'favorites',
+		title: i18n.ts.favorites,
 		icon: 'ti ti-star',
 		show: computed(() => $i != null),
 		to: '/my/favorites',
 	},
 	pages: {
-		title: 'pages',
+		title: i18n.ts.pages,
 		icon: 'ti ti-news',
 		to: '/pages',
 	},
+	play: {
+		title: 'Play',
+		icon: 'ti ti-player-play',
+		to: '/play',
+	},
 	gallery: {
-		title: 'gallery',
+		title: i18n.ts.gallery,
 		icon: 'ti ti-icons',
 		to: '/gallery',
 	},
 	clips: {
-		title: 'clip',
+		title: i18n.ts.clip,
 		icon: 'ti ti-paperclip',
 		show: computed(() => $i != null),
 		to: '/my/clips',
 	},
 	channels: {
-		title: 'channel',
+		title: i18n.ts.channel,
 		icon: 'ti ti-device-tv',
 		to: '/channels',
 	},
 	ui: {
-		title: 'switchUi',
+		title: i18n.ts.switchUi,
 		icon: 'ti ti-devices',
 		action: (ev) => {
 			os.popupMenu([{
 				text: i18n.ts.default,
 				active: ui === 'default' || ui === null,
 				action: () => {
-					localStorage.setItem('ui', 'default');
+					miLocalStorage.setItem('ui', 'default');
 					unisonReload();
 				},
 			}, {
 				text: i18n.ts.deck,
 				active: ui === 'deck',
 				action: () => {
-					localStorage.setItem('ui', 'deck');
+					miLocalStorage.setItem('ui', 'deck');
 					unisonReload();
 				},
 			}, {
 				text: i18n.ts.classic,
 				active: ui === 'classic',
 				action: () => {
-					localStorage.setItem('ui', 'classic');
+					miLocalStorage.setItem('ui', 'classic');
 					unisonReload();
 				},
 			}], ev.currentTarget ?? ev.target);
 		},
 	},
 	reload: {
-		title: 'reload',
+		title: i18n.ts.reload,
 		icon: 'ti ti-refresh',
 		action: (ev) => {
 			location.reload();
diff --git a/packages/frontend/src/os.ts b/packages/frontend/src/os.ts
index a4c34104c6..6e36f18374 100644
--- a/packages/frontend/src/os.ts
+++ b/packages/frontend/src/os.ts
@@ -515,6 +515,7 @@ export function popupMenu(items: MenuItem[] | Ref<MenuItem[]>, src?: HTMLElement
 	align?: string;
 	width?: number;
 	viaKeyboard?: boolean;
+	onClosing?: () => void;
 }) {
 	return new Promise((resolve, reject) => {
 		let dispose;
@@ -529,6 +530,9 @@ export function popupMenu(items: MenuItem[] | Ref<MenuItem[]>, src?: HTMLElement
 				resolve();
 				dispose();
 			},
+			closing: () => {
+				if (options?.onClosing) options.onClosing();
+			},
 		}).then(res => {
 			dispose = res.dispose;
 		});
diff --git a/packages/frontend/src/pages/_error_.vue b/packages/frontend/src/pages/_error_.vue
index da2889ba27..5001b5a8b4 100644
--- a/packages/frontend/src/pages/_error_.vue
+++ b/packages/frontend/src/pages/_error_.vue
@@ -26,6 +26,7 @@ import * as os from '@/os';
 import { unisonReload } from '@/scripts/unison-reload';
 import { i18n } from '@/i18n';
 import { definePageMetadata } from '@/scripts/page-metadata';
+import { miLocalStorage } from '@/local-storage';
 
 const props = withDefaults(defineProps<{
 	error?: Error;
@@ -42,7 +43,7 @@ os.api('meta', {
 	loaded = true;
 	serverIsDead = false;
 	meta = res;
-	localStorage.setItem('v', res.version);
+	miLocalStorage.setItem('v', res.version);
 }, () => {
 	loaded = true;
 	serverIsDead = true;
diff --git a/packages/frontend/src/pages/about-misskey.vue b/packages/frontend/src/pages/about-misskey.vue
index 5085b12527..1c3535a833 100644
--- a/packages/frontend/src/pages/about-misskey.vue
+++ b/packages/frontend/src/pages/about-misskey.vue
@@ -3,18 +3,17 @@
 	<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
 	<div style="overflow: clip;">
 		<MkSpacer :content-max="600" :margin-min="20">
-			<div class="_formRoot znqjceqz">
-				<div id="debug"></div>
-				<div ref="containerEl" v-panel class="_formBlock about" :class="{ playing: easterEggEngine != null }">
+			<div class="_gaps_m znqjceqz">
+				<div ref="containerEl" v-panel class="about" :class="{ playing: easterEggEngine != null }">
 					<img src="/client-assets/about-icon.png" alt="" class="icon" draggable="false" @load="iconLoaded" @click="gravity"/>
 					<div class="misskey">Misskey</div>
 					<div class="version">v{{ version }}</div>
 					<span v-for="emoji in easterEggEmojis" :key="emoji.id" class="emoji" :data-physics-x="emoji.left" :data-physics-y="emoji.top" :class="{ _physics_circle_: !emoji.emoji.startsWith(':') }"><MkEmoji class="emoji" :emoji="emoji.emoji" :is-reaction="false" :normal="true" :no-style="true"/></span>
 				</div>
-				<div class="_formBlock" style="text-align: center;">
+				<div style="text-align: center;">
 					{{ i18n.ts._aboutMisskey.about }}<br><a href="https://misskey-hub.net/docs/misskey.html" target="_blank" class="_link">{{ i18n.ts.learnMore }}</a>
 				</div>
-				<div class="_formBlock" style="text-align: center;">
+				<div style="text-align: center;">
 					<MkButton primary rounded inline @click="iLoveMisskey">I <Mfm text="$[jelly ❤]"/> #Misskey</MkButton>
 				</div>
 				<FormSection>
diff --git a/packages/frontend/src/pages/about.vue b/packages/frontend/src/pages/about.vue
index 1d6a844fbd..4d971c5a9f 100644
--- a/packages/frontend/src/pages/about.vue
+++ b/packages/frontend/src/pages/about.vue
@@ -2,8 +2,8 @@
 <MkStickyContainer>
 	<template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template>
 	<MkSpacer v-if="tab === 'overview'" :content-max="600" :margin-min="20">
-		<div class="_formRoot">
-			<div class="_formBlock fwhjspax" :style="{ backgroundImage: `url(${ $instance.bannerUrl })` }">
+		<div class="_gaps_m">
+			<div class="fwhjspax" :style="{ backgroundImage: `url(${ $instance.bannerUrl })` }">
 				<div class="content">
 					<img :src="$instance.iconUrl ?? $instance.faviconUrl ?? '/favicon.ico'" alt="" class="icon"/>
 					<div class="name">
@@ -12,44 +12,48 @@
 				</div>
 			</div>
 
-			<MkKeyValue class="_formBlock">
+			<MkKeyValue>
 				<template #key>{{ i18n.ts.description }}</template>
 				<template #value><div v-html="$instance.description"></div></template>
 			</MkKeyValue>
 
 			<FormSection>
-				<MkKeyValue class="_formBlock" :copy="version">
-					<template #key>Misskey</template>
-					<template #value>{{ version }}</template>
-				</MkKeyValue>
-				<div class="_formBlock" v-html="i18n.t('poweredByMisskeyDescription', { name: $instance.name ?? host })">
+				<div class="_gaps_m">
+					<MkKeyValue :copy="version">
+						<template #key>Misskey</template>
+						<template #value>{{ version }}</template>
+					</MkKeyValue>
+					<div v-html="i18n.t('poweredByMisskeyDescription', { name: $instance.name ?? host })">
+					</div>
+					<FormLink to="/about-misskey">{{ i18n.ts.aboutMisskey }}</FormLink>
 				</div>
-				<FormLink to="/about-misskey">{{ i18n.ts.aboutMisskey }}</FormLink>
 			</FormSection>
 
 			<FormSection>
-				<FormSplit>
-					<MkKeyValue class="_formBlock">
-						<template #key>{{ i18n.ts.administrator }}</template>
-						<template #value>{{ $instance.maintainerName }}</template>
-					</MkKeyValue>
-					<MkKeyValue class="_formBlock">
-						<template #key>{{ i18n.ts.contact }}</template>
-						<template #value>{{ $instance.maintainerEmail }}</template>
-					</MkKeyValue>
-				</FormSplit>
-				<FormLink v-if="$instance.tosUrl" :to="$instance.tosUrl" class="_formBlock" external>{{ i18n.ts.tos }}</FormLink>
+				<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.tosUrl" :to="$instance.tosUrl" external>{{ i18n.ts.tos }}</FormLink>
+				</div>
 			</FormSection>
 
 			<FormSuspense :p="initStats">
 				<FormSection>
 					<template #label>{{ i18n.ts.statistics }}</template>
 					<FormSplit>
-						<MkKeyValue class="_formBlock">
+						<MkKeyValue>
 							<template #key>{{ i18n.ts.users }}</template>
 							<template #value>{{ number(stats.originalUsersCount) }}</template>
 						</MkKeyValue>
-						<MkKeyValue class="_formBlock">
+						<MkKeyValue>
 							<template #key>{{ i18n.ts.notes }}</template>
 							<template #value>{{ number(stats.originalNotesCount) }}</template>
 						</MkKeyValue>
diff --git a/packages/frontend/src/pages/admin-file.vue b/packages/frontend/src/pages/admin-file.vue
index a11249e75d..f8a860e8b6 100644
--- a/packages/frontend/src/pages/admin-file.vue
+++ b/packages/frontend/src/pages/admin-file.vue
@@ -2,11 +2,11 @@
 <MkStickyContainer>
 	<template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template>
 	<MkSpacer v-if="file" :content-max="600" :margin-min="16" :margin-max="32">
-		<div v-if="tab === 'overview'" class="cxqhhsmd _formRoot">
-			<a class="_formBlock thumbnail" :href="file.url" target="_blank">
+		<div v-if="tab === 'overview'" class="cxqhhsmd _gaps_m">
+			<a class="thumbnail" :href="file.url" target="_blank">
 				<MkDriveFileThumbnail class="thumbnail" :file="file" fit="contain"/>
 			</a>
-			<div class="_formBlock">
+			<div>
 				<MkKeyValue :copy="file.type" oneline style="margin: 1em 0;">
 					<template #key>MIME Type</template>
 					<template #value><span class="_monospace">{{ file.type }}</span></template>
@@ -31,29 +31,29 @@
 			<MkA v-if="file.user" class="user" :to="`/user-info/${file.user.id}`">
 				<MkUserCardMini :user="file.user"/>
 			</MkA>
-			<div class="_formBlock">
+			<div>
 				<MkSwitch v-model="isSensitive" @update:model-value="toggleIsSensitive">NSFW</MkSwitch>
 			</div>
 
-			<div class="_formBlock">
+			<div>
 				<MkButton danger @click="del"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton>
 			</div>
 		</div>
-		<div v-else-if="tab === 'ip' && info" class="_formRoot">
+		<div v-else-if="tab === 'ip' && info" class="_gaps_m">
 			<MkInfo v-if="!iAmAdmin" warn>{{ i18n.ts.requireAdminForView }}</MkInfo>
-			<MkKeyValue v-if="info.requestIp" class="_formBlock _monospace" :copy="info.requestIp" oneline>
+			<MkKeyValue v-if="info.requestIp" class="_monospace" :copy="info.requestIp" oneline>
 				<template #key>IP</template>
 				<template #value>{{ info.requestIp }}</template>
 			</MkKeyValue>
 			<FormSection v-if="info.requestHeaders">
 				<template #label>Headers</template>
-				<MkKeyValue v-for="(v, k) in info.requestHeaders" :key="k" class="_formBlock _monospace">
+				<MkKeyValue v-for="(v, k) in info.requestHeaders" :key="k" class="_monospace">
 					<template #key>{{ k }}</template>
 					<template #value>{{ v }}</template>
 				</MkKeyValue>
 			</FormSection>
 		</div>
-		<div v-else-if="tab === 'raw'" class="_formRoot">
+		<div v-else-if="tab === 'raw'" class="_gaps_m">
 			<MkObjectView v-if="info" tall :value="info">
 			</MkObjectView>
 		</div>
diff --git a/packages/frontend/src/pages/admin/abuses.vue b/packages/frontend/src/pages/admin/abuses.vue
index 3bff312b8b..88535cc67c 100644
--- a/packages/frontend/src/pages/admin/abuses.vue
+++ b/packages/frontend/src/pages/admin/abuses.vue
@@ -3,8 +3,8 @@
 	<template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template>
 	<MkSpacer :content-max="900">
 		<div class="lcixvhis">
-			<div class="_section reports">
-				<div class="_content">
+			<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>
diff --git a/packages/frontend/src/pages/admin/ads.vue b/packages/frontend/src/pages/admin/ads.vue
index 2ec926c65c..d8776c9175 100644
--- a/packages/frontend/src/pages/admin/ads.vue
+++ b/packages/frontend/src/pages/admin/ads.vue
@@ -3,15 +3,15 @@
 	<template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template>
 	<MkSpacer :content-max="900">
 		<div class="uqshojas">
-			<div v-for="ad in ads" class="_panel _formRoot ad">
+			<div v-for="ad in ads" class="_panel _gaps_m ad">
 				<MkAd v-if="ad.url" :specify="ad"/>
-				<MkInput v-model="ad.url" type="url" class="_formBlock">
+				<MkInput v-model="ad.url" type="url">
 					<template #label>URL</template>
 				</MkInput>
-				<MkInput v-model="ad.imageUrl" class="_formBlock">
+				<MkInput v-model="ad.imageUrl">
 					<template #label>{{ i18n.ts.imageUrl }}</template>
 				</MkInput>
-				<FormRadios v-model="ad.place" class="_formBlock">
+				<FormRadios v-model="ad.place">
 					<template #label>Form</template>
 					<option value="square">square</option>
 					<option value="horizontal">horizontal</option>
@@ -33,10 +33,10 @@
 						<template #label>{{ i18n.ts.expiration }}</template>
 					</MkInput>
 				</FormSplit>
-				<MkTextarea v-model="ad.memo" class="_formBlock">
+				<MkTextarea v-model="ad.memo">
 					<template #label>{{ i18n.ts.memo }}</template>
 				</MkTextarea>
-				<div class="buttons _formBlock">
+				<div class="buttons">
 					<MkButton class="button" inline primary style="margin-right: 12px;" @click="save(ad)"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton>
 					<MkButton class="button" inline danger @click="remove(ad)"><i class="ti ti-trash"></i> {{ i18n.ts.remove }}</MkButton>
 				</div>
diff --git a/packages/frontend/src/pages/admin/announcements.vue b/packages/frontend/src/pages/admin/announcements.vue
index 607ad8aa02..71af03484b 100644
--- a/packages/frontend/src/pages/admin/announcements.vue
+++ b/packages/frontend/src/pages/admin/announcements.vue
@@ -2,9 +2,9 @@
 <MkStickyContainer>
 	<template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template>
 	<MkSpacer :content-max="900">
-		<div class="ztgjmzrw">
-			<section v-for="announcement in announcements" class="_card _gap announcements">
-				<div class="_content announcement">
+		<div class="ztgjmzrw _gaps_m">
+			<section v-for="announcement in announcements" class="">
+				<div class="_panel _gaps_m" style="padding: 24px;">
 					<MkInput v-model="announcement.title">
 						<template #label>{{ i18n.ts.title }}</template>
 					</MkInput>
@@ -15,9 +15,9 @@
 						<template #label>{{ i18n.ts.imageUrl }}</template>
 					</MkInput>
 					<p v-if="announcement.reads">{{ i18n.t('nUsersRead', { n: announcement.reads }) }}</p>
-					<div class="buttons">
+					<div class="buttons _buttons">
 						<MkButton class="button" inline primary @click="save(announcement)"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton>
-						<MkButton class="button" inline @click="remove(announcement)"><i class="ti ti-trash"></i> {{ i18n.ts.remove }}</MkButton>
+						<MkButton class="button" inline danger @click="remove(announcement)"><i class="ti ti-trash"></i> {{ i18n.ts.remove }}</MkButton>
 					</div>
 				</div>
 			</section>
diff --git a/packages/frontend/src/pages/admin/bot-protection.vue b/packages/frontend/src/pages/admin/bot-protection.vue
index d03961cf95..995ea483d7 100644
--- a/packages/frontend/src/pages/admin/bot-protection.vue
+++ b/packages/frontend/src/pages/admin/bot-protection.vue
@@ -1,8 +1,8 @@
 <template>
 <div>
 	<FormSuspense :p="init">
-		<div class="_formRoot">
-			<FormRadios v-model="provider" class="_formBlock">
+		<div class="_gaps_m">
+			<FormRadios v-model="provider">
 				<option :value="null">{{ i18n.ts.none }} ({{ i18n.ts.notRecommended }})</option>
 				<option value="hcaptcha">hCaptcha</option>
 				<option value="recaptcha">reCAPTCHA</option>
@@ -10,49 +10,49 @@
 			</FormRadios>
 
 			<template v-if="provider === 'hcaptcha'">
-				<FormInput v-model="hcaptchaSiteKey" class="_formBlock">
+				<FormInput v-model="hcaptchaSiteKey">
 					<template #prefix><i class="ti ti-key"></i></template>
 					<template #label>{{ i18n.ts.hcaptchaSiteKey }}</template>
 				</FormInput>
-				<FormInput v-model="hcaptchaSecretKey" class="_formBlock">
+				<FormInput v-model="hcaptchaSecretKey">
 					<template #prefix><i class="ti ti-key"></i></template>
 					<template #label>{{ i18n.ts.hcaptchaSecretKey }}</template>
 				</FormInput>
-				<FormSlot class="_formBlock">
+				<FormSlot>
 					<template #label>{{ i18n.ts.preview }}</template>
 					<MkCaptcha provider="hcaptcha" :sitekey="hcaptchaSiteKey || '10000000-ffff-ffff-ffff-000000000001'"/>
 				</FormSlot>
 			</template>
 			<template v-else-if="provider === 'recaptcha'">
-				<FormInput v-model="recaptchaSiteKey" class="_formBlock">
+				<FormInput v-model="recaptchaSiteKey">
 					<template #prefix><i class="ti ti-key"></i></template>
 					<template #label>{{ i18n.ts.recaptchaSiteKey }}</template>
 				</FormInput>
-				<FormInput v-model="recaptchaSecretKey" class="_formBlock">
+				<FormInput v-model="recaptchaSecretKey">
 					<template #prefix><i class="ti ti-key"></i></template>
 					<template #label>{{ i18n.ts.recaptchaSecretKey }}</template>
 				</FormInput>
-				<FormSlot v-if="recaptchaSiteKey" class="_formBlock">
+				<FormSlot v-if="recaptchaSiteKey">
 					<template #label>{{ i18n.ts.preview }}</template>
 					<MkCaptcha provider="recaptcha" :sitekey="recaptchaSiteKey"/>
 				</FormSlot>
 			</template>
 			<template v-else-if="provider === 'turnstile'">
-				<FormInput v-model="turnstileSiteKey" class="_formBlock">
+				<FormInput v-model="turnstileSiteKey">
 					<template #prefix><i class="ti ti-key"></i></template>
 					<template #label>{{ i18n.ts.turnstileSiteKey }}</template>
 				</FormInput>
-				<FormInput v-model="turnstileSecretKey" class="_formBlock">
+				<FormInput v-model="turnstileSecretKey">
 					<template #prefix><i class="ti ti-key"></i></template>
 					<template #label>{{ i18n.ts.turnstileSecretKey }}</template>
 				</FormInput>
-				<FormSlot class="_formBlock">
+				<FormSlot>
 					<template #label>{{ i18n.ts.preview }}</template>
 					<MkCaptcha provider="turnstile" :sitekey="turnstileSiteKey || '1x00000000000000000000AA'"/>
 				</FormSlot>
 			</template>
 
-			<FormButton primary @click="save"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</FormButton>
+			<MkButton primary @click="save"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton>
 		</div>
 	</FormSuspense>
 </div>
@@ -62,7 +62,7 @@
 import { defineAsyncComponent } from 'vue';
 import FormRadios from '@/components/form/radios.vue';
 import FormInput from '@/components/form/input.vue';
-import FormButton from '@/components/MkButton.vue';
+import MkButton from '@/components/MkButton.vue';
 import FormSuspense from '@/components/form/suspense.vue';
 import FormSlot from '@/components/form/slot.vue';
 import * as os from '@/os';
diff --git a/packages/frontend/src/pages/admin/email-settings.vue b/packages/frontend/src/pages/admin/email-settings.vue
index 6c9dee1704..ab19366ea6 100644
--- a/packages/frontend/src/pages/admin/email-settings.vue
+++ b/packages/frontend/src/pages/admin/email-settings.vue
@@ -3,40 +3,43 @@
 	<template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template>
 	<MkSpacer :content-max="700" :margin-min="16" :margin-max="32">
 		<FormSuspense :p="init">
-			<div class="_formRoot">
-				<FormSwitch v-model="enableEmail" class="_formBlock">
+			<div class="_gaps_m">
+				<FormSwitch v-model="enableEmail">
 					<template #label>{{ i18n.ts.enableEmail }} ({{ i18n.ts.recommended }})</template>
 					<template #caption>{{ i18n.ts.emailConfigInfo }}</template>
 				</FormSwitch>
 
 				<template v-if="enableEmail">
-					<FormInput v-model="email" type="email" class="_formBlock">
+					<FormInput v-model="email" type="email">
 						<template #label>{{ i18n.ts.emailAddress }}</template>
 					</FormInput>
 
 					<FormSection>
 						<template #label>{{ i18n.ts.smtpConfig }}</template>
-						<FormSplit :min-width="280">
-							<FormInput v-model="smtpHost" class="_formBlock">
-								<template #label>{{ i18n.ts.smtpHost }}</template>
-							</FormInput>
-							<FormInput v-model="smtpPort" type="number" class="_formBlock">
-								<template #label>{{ i18n.ts.smtpPort }}</template>
-							</FormInput>
-						</FormSplit>
-						<FormSplit :min-width="280">
-							<FormInput v-model="smtpUser" class="_formBlock">
-								<template #label>{{ i18n.ts.smtpUser }}</template>
-							</FormInput>
-							<FormInput v-model="smtpPass" type="password" class="_formBlock">
-								<template #label>{{ i18n.ts.smtpPass }}</template>
-							</FormInput>
-						</FormSplit>
-						<FormInfo class="_formBlock">{{ i18n.ts.emptyToDisableSmtpAuth }}</FormInfo>
-						<FormSwitch v-model="smtpSecure" class="_formBlock">
-							<template #label>{{ i18n.ts.smtpSecure }}</template>
-							<template #caption>{{ i18n.ts.smtpSecureInfo }}</template>
-						</FormSwitch>
+
+						<div class="_gaps_m">
+							<FormSplit :min-width="280">
+								<FormInput v-model="smtpHost">
+									<template #label>{{ i18n.ts.smtpHost }}</template>
+								</FormInput>
+								<FormInput v-model="smtpPort" type="number">
+									<template #label>{{ i18n.ts.smtpPort }}</template>
+								</FormInput>
+							</FormSplit>
+							<FormSplit :min-width="280">
+								<FormInput v-model="smtpUser">
+									<template #label>{{ i18n.ts.smtpUser }}</template>
+								</FormInput>
+								<FormInput v-model="smtpPass" type="password">
+									<template #label>{{ i18n.ts.smtpPass }}</template>
+								</FormInput>
+							</FormSplit>
+							<FormInfo>{{ i18n.ts.emptyToDisableSmtpAuth }}</FormInfo>
+							<FormSwitch v-model="smtpSecure">
+								<template #label>{{ i18n.ts.smtpSecure }}</template>
+								<template #caption>{{ i18n.ts.smtpSecureInfo }}</template>
+							</FormSwitch>
+						</div>
 					</FormSection>
 				</template>
 			</div>
diff --git a/packages/frontend/src/pages/admin/emoji-edit-dialog.vue b/packages/frontend/src/pages/admin/emoji-edit-dialog.vue
index 610958e95e..46011876ee 100644
--- a/packages/frontend/src/pages/admin/emoji-edit-dialog.vue
+++ b/packages/frontend/src/pages/admin/emoji-edit-dialog.vue
@@ -1,5 +1,5 @@
 <template>
-<XModalWindow
+<MkModalWindow
 	ref="dialog"
 	:width="370"
 	:with-ok-button="true"
@@ -9,28 +9,28 @@
 >
 	<template #header>:{{ emoji.name }}:</template>
 
-	<div class="_monolithic_">
-		<div class="yigymqpb _section">
+	<MkSpacer :margin-min="20" :margin-max="28">
+		<div class="yigymqpb _gaps_m">
 			<img :src="`/emoji/${emoji.name}.webp`" class="img"/>
-			<MkInput v-model="name" class="_formBlock">
+			<MkInput v-model="name">
 				<template #label>{{ i18n.ts.name }}</template>
 			</MkInput>
-			<MkInput v-model="category" class="_formBlock" :datalist="categories">
+			<MkInput v-model="category" :datalist="categories">
 				<template #label>{{ i18n.ts.category }}</template>
 			</MkInput>
-			<MkInput v-model="aliases" class="_formBlock">
+			<MkInput v-model="aliases">
 				<template #label>{{ i18n.ts.tags }}</template>
 				<template #caption>{{ i18n.ts.setMultipleBySeparatingWithSpace }}</template>
 			</MkInput>
 			<MkButton danger @click="del()"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton>
 		</div>
-	</div>
-</XModalWindow>
+	</MkSpacer>
+</MkModalWindow>
 </template>
 
 <script lang="ts" setup>
 import { } from 'vue';
-import XModalWindow from '@/components/MkModalWindow.vue';
+import MkModalWindow from '@/components/MkModalWindow.vue';
 import MkButton from '@/components/MkButton.vue';
 import MkInput from '@/components/form/input.vue';
 import * as os from '@/os';
diff --git a/packages/frontend/src/pages/admin/emojis.vue b/packages/frontend/src/pages/admin/emojis.vue
index 3d56ab1962..68ad51bce3 100644
--- a/packages/frontend/src/pages/admin/emojis.vue
+++ b/packages/frontend/src/pages/admin/emojis.vue
@@ -12,7 +12,7 @@
 					<MkSwitch v-model="selectMode" style="margin: 8px 0;">
 						<template #label>Select mode</template>
 					</MkSwitch>
-					<div v-if="selectMode" style="display: flex; gap: var(--margin); flex-wrap: wrap;">
+					<div v-if="selectMode" class="_buttons">
 						<MkButton inline @click="selectAll">Select all</MkButton>
 						<MkButton inline @click="setCategoryBulk">Set category</MkButton>
 						<MkButton inline @click="addTagBulk">Add tag</MkButton>
diff --git a/packages/frontend/src/pages/admin/instance-block.vue b/packages/frontend/src/pages/admin/instance-block.vue
index 1bdd174de4..a45588e005 100644
--- a/packages/frontend/src/pages/admin/instance-block.vue
+++ b/packages/frontend/src/pages/admin/instance-block.vue
@@ -3,12 +3,12 @@
 	<template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template>
 	<MkSpacer :content-max="700" :margin-min="16" :margin-max="32">
 		<FormSuspense :p="init">
-			<FormTextarea v-model="blockedHosts" class="_formBlock">
+			<FormTextarea v-model="blockedHosts">
 				<span>{{ i18n.ts.blockedInstances }}</span>
 				<template #caption>{{ i18n.ts.blockedInstancesDescription }}</template>
 			</FormTextarea>
 
-			<FormButton primary class="_formBlock" @click="save"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</FormButton>
+			<MkButton primary @click="save"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton>
 		</FormSuspense>
 	</MkSpacer>
 </MkStickyContainer>
@@ -17,7 +17,7 @@
 <script lang="ts" setup>
 import { } from 'vue';
 import XHeader from './_header_.vue';
-import FormButton from '@/components/MkButton.vue';
+import MkButton from '@/components/MkButton.vue';
 import FormTextarea from '@/components/form/textarea.vue';
 import FormSuspense from '@/components/form/suspense.vue';
 import * as os from '@/os';
diff --git a/packages/frontend/src/pages/admin/integrations.discord.vue b/packages/frontend/src/pages/admin/integrations.discord.vue
index 0a69c44c93..6e2156160c 100644
--- a/packages/frontend/src/pages/admin/integrations.discord.vue
+++ b/packages/frontend/src/pages/admin/integrations.discord.vue
@@ -1,25 +1,25 @@
 <template>
 <FormSuspense :p="init">
-	<div class="_formRoot">
-		<FormSwitch v-model="enableDiscordIntegration" class="_formBlock">
+	<div class="_gaps_m">
+		<FormSwitch v-model="enableDiscordIntegration">
 			<template #label>{{ i18n.ts.enable }}</template>
 		</FormSwitch>
 
 		<template v-if="enableDiscordIntegration">
-			<FormInfo class="_formBlock">Callback URL: {{ `${uri}/api/dc/cb` }}</FormInfo>
+			<FormInfo>Callback URL: {{ `${uri}/api/dc/cb` }}</FormInfo>
 		
-			<FormInput v-model="discordClientId" class="_formBlock">
+			<FormInput v-model="discordClientId">
 				<template #prefix><i class="ti ti-key"></i></template>
 				<template #label>Client ID</template>
 			</FormInput>
 
-			<FormInput v-model="discordClientSecret" class="_formBlock">
+			<FormInput v-model="discordClientSecret">
 				<template #prefix><i class="ti ti-key"></i></template>
 				<template #label>Client Secret</template>
 			</FormInput>
 		</template>
 
-		<FormButton primary class="_formBlock" @click="save"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</FormButton>
+		<MkButton primary @click="save"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton>
 	</div>
 </FormSuspense>
 </template>
@@ -28,7 +28,7 @@
 import { } from 'vue';
 import FormSwitch from '@/components/form/switch.vue';
 import FormInput from '@/components/form/input.vue';
-import FormButton from '@/components/MkButton.vue';
+import MkButton from '@/components/MkButton.vue';
 import FormInfo from '@/components/MkInfo.vue';
 import FormSuspense from '@/components/form/suspense.vue';
 import * as os from '@/os';
diff --git a/packages/frontend/src/pages/admin/integrations.github.vue b/packages/frontend/src/pages/admin/integrations.github.vue
index 66419d5891..352840adcf 100644
--- a/packages/frontend/src/pages/admin/integrations.github.vue
+++ b/packages/frontend/src/pages/admin/integrations.github.vue
@@ -1,25 +1,25 @@
 <template>
 <FormSuspense :p="init">
-	<div class="_formRoot">
-		<FormSwitch v-model="enableGithubIntegration" class="_formBlock">
+	<div class="_gaps_m">
+		<FormSwitch v-model="enableGithubIntegration">
 			<template #label>{{ i18n.ts.enable }}</template>
 		</FormSwitch>
 
 		<template v-if="enableGithubIntegration">
-			<FormInfo class="_formBlock">Callback URL: {{ `${uri}/api/gh/cb` }}</FormInfo>
+			<FormInfo>Callback URL: {{ `${uri}/api/gh/cb` }}</FormInfo>
 		
-			<FormInput v-model="githubClientId" class="_formBlock">
+			<FormInput v-model="githubClientId">
 				<template #prefix><i class="ti ti-key"></i></template>
 				<template #label>Client ID</template>
 			</FormInput>
 
-			<FormInput v-model="githubClientSecret" class="_formBlock">
+			<FormInput v-model="githubClientSecret">
 				<template #prefix><i class="ti ti-key"></i></template>
 				<template #label>Client Secret</template>
 			</FormInput>
 		</template>
 
-		<FormButton primary class="_formBlock" @click="save"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</FormButton>
+		<MkButton primary @click="save"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton>
 	</div>
 </FormSuspense>
 </template>
@@ -28,7 +28,7 @@
 import { } from 'vue';
 import FormSwitch from '@/components/form/switch.vue';
 import FormInput from '@/components/form/input.vue';
-import FormButton from '@/components/MkButton.vue';
+import MkButton from '@/components/MkButton.vue';
 import FormInfo from '@/components/MkInfo.vue';
 import FormSuspense from '@/components/form/suspense.vue';
 import * as os from '@/os';
diff --git a/packages/frontend/src/pages/admin/integrations.twitter.vue b/packages/frontend/src/pages/admin/integrations.twitter.vue
index 1e8d882b9c..a7b56fbad0 100644
--- a/packages/frontend/src/pages/admin/integrations.twitter.vue
+++ b/packages/frontend/src/pages/admin/integrations.twitter.vue
@@ -1,25 +1,25 @@
 <template>
 <FormSuspense :p="init">
-	<div class="_formRoot">
-		<FormSwitch v-model="enableTwitterIntegration" class="_formBlock">
+	<div class="_gaps_m">
+		<FormSwitch v-model="enableTwitterIntegration">
 			<template #label>{{ i18n.ts.enable }}</template>
 		</FormSwitch>
 
 		<template v-if="enableTwitterIntegration">
-			<FormInfo class="_formBlock">Callback URL: {{ `${uri}/api/tw/cb` }}</FormInfo>
+			<FormInfo>Callback URL: {{ `${uri}/api/tw/cb` }}</FormInfo>
 		
-			<FormInput v-model="twitterConsumerKey" class="_formBlock">
+			<FormInput v-model="twitterConsumerKey">
 				<template #prefix><i class="ti ti-key"></i></template>
 				<template #label>Consumer Key</template>
 			</FormInput>
 
-			<FormInput v-model="twitterConsumerSecret" class="_formBlock">
+			<FormInput v-model="twitterConsumerSecret">
 				<template #prefix><i class="ti ti-key"></i></template>
 				<template #label>Consumer Secret</template>
 			</FormInput>
 		</template>
 
-		<FormButton primary class="_formBlock" @click="save"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</FormButton>
+		<MkButton primary @click="save"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton>
 	</div>
 </FormSuspense>
 </template>
@@ -28,7 +28,7 @@
 import { defineComponent } from 'vue';
 import FormSwitch from '@/components/form/switch.vue';
 import FormInput from '@/components/form/input.vue';
-import FormButton from '@/components/MkButton.vue';
+import MkButton from '@/components/MkButton.vue';
 import FormInfo from '@/components/MkInfo.vue';
 import FormSuspense from '@/components/form/suspense.vue';
 import * as os from '@/os';
diff --git a/packages/frontend/src/pages/admin/integrations.vue b/packages/frontend/src/pages/admin/integrations.vue
index 9cc35baefd..e319d48617 100644
--- a/packages/frontend/src/pages/admin/integrations.vue
+++ b/packages/frontend/src/pages/admin/integrations.vue
@@ -1,27 +1,31 @@
-<template><MkStickyContainer>
+<template>
+<MkStickyContainer>
 	<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
-		<MkSpacer :content-max="700" :margin-min="16" :margin-max="32">
-	<FormSuspense :p="init">
-		<FormFolder class="_formBlock">
-			<template #icon><i class="ti ti-brand-twitter"></i></template>
-			<template #label>Twitter</template>
-			<template #suffix>{{ enableTwitterIntegration ? i18n.ts.enabled : i18n.ts.disabled }}</template>
-			<XTwitter/>
-		</FormFolder>
-		<FormFolder class="_formBlock">
-			<template #icon><i class="ti ti-brand-github"></i></template>
-			<template #label>GitHub</template>
-			<template #suffix>{{ enableGithubIntegration ? i18n.ts.enabled : i18n.ts.disabled }}</template>
-			<XGithub/>
-		</FormFolder>
-		<FormFolder class="_formBlock">
-			<template #icon><i class="ti ti-brand-discord"></i></template>
-			<template #label>Discord</template>
-			<template #suffix>{{ enableDiscordIntegration ? i18n.ts.enabled : i18n.ts.disabled }}</template>
-			<XDiscord/>
-		</FormFolder>
-	</FormSuspense>
-</MkSpacer></MkStickyContainer>
+	<MkSpacer :content-max="700" :margin-min="16" :margin-max="32">
+		<FormSuspense :p="init">
+			<div class="_gaps_m">
+				<FormFolder>
+					<template #icon><i class="ti ti-brand-twitter"></i></template>
+					<template #label>Twitter</template>
+					<template #suffix>{{ enableTwitterIntegration ? i18n.ts.enabled : i18n.ts.disabled }}</template>
+					<XTwitter/>
+				</FormFolder>
+				<FormFolder>
+					<template #icon><i class="ti ti-brand-github"></i></template>
+					<template #label>GitHub</template>
+					<template #suffix>{{ enableGithubIntegration ? i18n.ts.enabled : i18n.ts.disabled }}</template>
+					<XGithub/>
+				</FormFolder>
+				<FormFolder>
+					<template #icon><i class="ti ti-brand-discord"></i></template>
+					<template #label>Discord</template>
+					<template #suffix>{{ enableDiscordIntegration ? i18n.ts.enabled : i18n.ts.disabled }}</template>
+					<XDiscord/>
+				</FormFolder>
+			</div>
+		</FormSuspense>
+	</MkSpacer>
+</MkStickyContainer>
 </template>
 
 <script lang="ts" setup>
diff --git a/packages/frontend/src/pages/admin/object-storage.vue b/packages/frontend/src/pages/admin/object-storage.vue
index f2ab30eaa5..f56e8bab93 100644
--- a/packages/frontend/src/pages/admin/object-storage.vue
+++ b/packages/frontend/src/pages/admin/object-storage.vue
@@ -3,62 +3,62 @@
 	<template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template>
 	<MkSpacer :content-max="700" :margin-min="16" :margin-max="32">
 		<FormSuspense :p="init">
-			<div class="_formRoot">
-				<FormSwitch v-model="useObjectStorage" class="_formBlock">{{ i18n.ts.useObjectStorage }}</FormSwitch>
+			<div class="_gaps_m">
+				<FormSwitch v-model="useObjectStorage">{{ i18n.ts.useObjectStorage }}</FormSwitch>
 
 				<template v-if="useObjectStorage">
-					<FormInput v-model="objectStorageBaseUrl" class="_formBlock">
+					<FormInput v-model="objectStorageBaseUrl">
 						<template #label>{{ i18n.ts.objectStorageBaseUrl }}</template>
 						<template #caption>{{ i18n.ts.objectStorageBaseUrlDesc }}</template>
 					</FormInput>
 
-					<FormInput v-model="objectStorageBucket" class="_formBlock">
+					<FormInput v-model="objectStorageBucket">
 						<template #label>{{ i18n.ts.objectStorageBucket }}</template>
 						<template #caption>{{ i18n.ts.objectStorageBucketDesc }}</template>
 					</FormInput>
 
-					<FormInput v-model="objectStoragePrefix" class="_formBlock">
+					<FormInput v-model="objectStoragePrefix">
 						<template #label>{{ i18n.ts.objectStoragePrefix }}</template>
 						<template #caption>{{ i18n.ts.objectStoragePrefixDesc }}</template>
 					</FormInput>
 
-					<FormInput v-model="objectStorageEndpoint" class="_formBlock">
+					<FormInput v-model="objectStorageEndpoint">
 						<template #label>{{ i18n.ts.objectStorageEndpoint }}</template>
 						<template #caption>{{ i18n.ts.objectStorageEndpointDesc }}</template>
 					</FormInput>
 
-					<FormInput v-model="objectStorageRegion" class="_formBlock">
+					<FormInput v-model="objectStorageRegion">
 						<template #label>{{ i18n.ts.objectStorageRegion }}</template>
 						<template #caption>{{ i18n.ts.objectStorageRegionDesc }}</template>
 					</FormInput>
 
 					<FormSplit :min-width="280">
-						<FormInput v-model="objectStorageAccessKey" class="_formBlock">
+						<FormInput v-model="objectStorageAccessKey">
 							<template #prefix><i class="ti ti-key"></i></template>
 							<template #label>Access key</template>
 						</FormInput>
 
-						<FormInput v-model="objectStorageSecretKey" class="_formBlock">
+						<FormInput v-model="objectStorageSecretKey">
 							<template #prefix><i class="ti ti-key"></i></template>
 							<template #label>Secret key</template>
 						</FormInput>
 					</FormSplit>
 
-					<FormSwitch v-model="objectStorageUseSSL" class="_formBlock">
+					<FormSwitch v-model="objectStorageUseSSL">
 						<template #label>{{ i18n.ts.objectStorageUseSSL }}</template>
 						<template #caption>{{ i18n.ts.objectStorageUseSSLDesc }}</template>
 					</FormSwitch>
 
-					<FormSwitch v-model="objectStorageUseProxy" class="_formBlock">
+					<FormSwitch v-model="objectStorageUseProxy">
 						<template #label>{{ i18n.ts.objectStorageUseProxy }}</template>
 						<template #caption>{{ i18n.ts.objectStorageUseProxyDesc }}</template>
 					</FormSwitch>
 
-					<FormSwitch v-model="objectStorageSetPublicRead" class="_formBlock">
+					<FormSwitch v-model="objectStorageSetPublicRead">
 						<template #label>{{ i18n.ts.objectStorageSetPublicRead }}</template>
 					</FormSwitch>
 
-					<FormSwitch v-model="objectStorageS3ForcePathStyle" class="_formBlock">
+					<FormSwitch v-model="objectStorageS3ForcePathStyle">
 						<template #label>s3ForcePathStyle</template>
 					</FormSwitch>
 				</template>
diff --git a/packages/frontend/src/pages/admin/overview.active-users.vue b/packages/frontend/src/pages/admin/overview.active-users.vue
index 14b09f34e9..e20b8221cf 100644
--- a/packages/frontend/src/pages/admin/overview.active-users.vue
+++ b/packages/frontend/src/pages/admin/overview.active-users.vue
@@ -10,7 +10,6 @@
 <script lang="ts" setup>
 import { markRaw, version as vueVersion, onMounted, onBeforeUnmount, nextTick } from 'vue';
 import { Chart } from 'chart.js';
-import { enUS } from 'date-fns/locale';
 import tinycolor from 'tinycolor2';
 import gradient from 'chartjs-plugin-gradient';
 import * as os from '@/os';
@@ -114,11 +113,6 @@ async function renderChart() {
 						maxRotation: 0,
 						autoSkipPadding: 8,
 					},
-					adapters: {
-						date: {
-							locale: enUS,
-						},
-					},
 				},
 				y: {
 					position: 'left',
diff --git a/packages/frontend/src/pages/admin/overview.ap-requests.vue b/packages/frontend/src/pages/admin/overview.ap-requests.vue
index 61a0667080..9bc08ef6b6 100644
--- a/packages/frontend/src/pages/admin/overview.ap-requests.vue
+++ b/packages/frontend/src/pages/admin/overview.ap-requests.vue
@@ -18,7 +18,6 @@
 import { onMounted, onUnmounted, ref } from 'vue';
 import { Chart } from 'chart.js';
 import gradient from 'chartjs-plugin-gradient';
-import { enUS } from 'date-fns/locale';
 import tinycolor from 'tinycolor2';
 import MkMiniChart from '@/components/MkMiniChart.vue';
 import * as os from '@/os';
@@ -135,11 +134,6 @@ onMounted(async () => {
 						maxRotation: 0,
 						autoSkipPadding: 16,
 					},
-					adapters: {
-						date: {
-							locale: enUS,
-						},
-					},
 					min: getDate(chartLimit).getTime(),
 				},
 				y: {
diff --git a/packages/frontend/src/pages/admin/proxy-account.vue b/packages/frontend/src/pages/admin/proxy-account.vue
index 5d0d67980e..6ad566187a 100644
--- a/packages/frontend/src/pages/admin/proxy-account.vue
+++ b/packages/frontend/src/pages/admin/proxy-account.vue
@@ -1,22 +1,24 @@
-<template><MkStickyContainer>
+<template>
+<MkStickyContainer>
 	<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
-		<MkSpacer :content-max="700" :margin-min="16" :margin-max="32">
-	<FormSuspense :p="init">
-		<MkInfo class="_formBlock">{{ i18n.ts.proxyAccountDescription }}</MkInfo>
-		<MkKeyValue class="_formBlock">
-			<template #key>{{ i18n.ts.proxyAccount }}</template>
-			<template #value>{{ proxyAccount ? `@${proxyAccount.username}` : i18n.ts.none }}</template>
-		</MkKeyValue>
+	<MkSpacer :content-max="700" :margin-min="16" :margin-max="32">
+		<FormSuspense :p="init">
+			<MkInfo>{{ i18n.ts.proxyAccountDescription }}</MkInfo>
+			<MkKeyValue>
+				<template #key>{{ i18n.ts.proxyAccount }}</template>
+				<template #value>{{ proxyAccount ? `@${proxyAccount.username}` : i18n.ts.none }}</template>
+			</MkKeyValue>
 
-		<FormButton primary class="_formBlock" @click="chooseProxyAccount">{{ i18n.ts.selectAccount }}</FormButton>
-	</FormSuspense>
-</MkSpacer></MkStickyContainer>
+			<MkButton primary @click="chooseProxyAccount">{{ i18n.ts.selectAccount }}</MkButton>
+		</FormSuspense>
+	</MkSpacer>
+</MkStickyContainer>
 </template>
 
 <script lang="ts" setup>
 import { } from 'vue';
 import MkKeyValue from '@/components/MkKeyValue.vue';
-import FormButton from '@/components/MkButton.vue';
+import MkButton from '@/components/MkButton.vue';
 import MkInfo from '@/components/MkInfo.vue';
 import FormSuspense from '@/components/form/suspense.vue';
 import * as os from '@/os';
diff --git a/packages/frontend/src/pages/admin/relays.vue b/packages/frontend/src/pages/admin/relays.vue
index 4768ae67b1..eb2788fdeb 100644
--- a/packages/frontend/src/pages/admin/relays.vue
+++ b/packages/frontend/src/pages/admin/relays.vue
@@ -2,7 +2,7 @@
 <MkStickyContainer>
 	<template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template>
 	<MkSpacer :content-max="800">
-		<div v-for="relay in relays" :key="relay.inbox" class="relaycxt _panel _block" style="padding: 16px;">
+		<div v-for="relay in relays" :key="relay.inbox" class="relaycxt _panel" style="padding: 16px;">
 			<div>{{ relay.inbox }}</div>
 			<div class="status">
 				<i v-if="relay.status === 'accepted'" class="ti ti-check icon accepted"></i>
diff --git a/packages/frontend/src/pages/admin/security.vue b/packages/frontend/src/pages/admin/security.vue
index 2682bda337..997ee31fe3 100644
--- a/packages/frontend/src/pages/admin/security.vue
+++ b/packages/frontend/src/pages/admin/security.vue
@@ -3,8 +3,8 @@
 	<template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template>
 	<MkSpacer :content-max="700" :margin-min="16" :margin-max="32">
 		<FormSuspense :p="init">
-			<div class="_formRoot">
-				<FormFolder class="_formBlock">
+			<div class="_gaps_m">
+				<FormFolder>
 					<template #icon><i class="ti ti-shield"></i></template>
 					<template #label>{{ i18n.ts.botProtection }}</template>
 					<template v-if="enableHcaptcha" #suffix>hCaptcha</template>
@@ -15,7 +15,7 @@
 					<XBotProtection/>
 				</FormFolder>
 
-				<FormFolder class="_formBlock">
+				<FormFolder>
 					<template #icon><i class="ti ti-eye-off"></i></template>
 					<template #label>{{ i18n.ts.sensitiveMediaDetection }}</template>
 					<template v-if="sensitiveMediaDetection === 'all'" #suffix>{{ i18n.ts.all }}</template>
@@ -23,76 +23,76 @@
 					<template v-else-if="sensitiveMediaDetection === 'remote'" #suffix>{{ i18n.ts.remoteOnly }}</template>
 					<template v-else #suffix>{{ i18n.ts.none }}</template>
 
-					<div class="_formRoot">
-						<span class="_formBlock">{{ i18n.ts._sensitiveMediaDetection.description }}</span>
+					<div class="_gaps_m">
+						<span>{{ i18n.ts._sensitiveMediaDetection.description }}</span>
 
-						<FormRadios v-model="sensitiveMediaDetection" class="_formBlock">
+						<FormRadios v-model="sensitiveMediaDetection">
 							<option value="none">{{ i18n.ts.none }}</option>
 							<option value="all">{{ i18n.ts.all }}</option>
 							<option value="local">{{ i18n.ts.localOnly }}</option>
 							<option value="remote">{{ i18n.ts.remoteOnly }}</option>
 						</FormRadios>
 
-						<FormRange v-model="sensitiveMediaDetectionSensitivity" :min="0" :max="4" :step="1" :text-converter="(v) => `${v + 1}`" class="_formBlock">
+						<FormRange v-model="sensitiveMediaDetectionSensitivity" :min="0" :max="4" :step="1" :text-converter="(v) => `${v + 1}`">
 							<template #label>{{ i18n.ts._sensitiveMediaDetection.sensitivity }}</template>
 							<template #caption>{{ i18n.ts._sensitiveMediaDetection.sensitivityDescription }}</template>
 						</FormRange>
 
-						<FormSwitch v-model="enableSensitiveMediaDetectionForVideos" class="_formBlock">
+						<FormSwitch v-model="enableSensitiveMediaDetectionForVideos">
 							<template #label>{{ i18n.ts._sensitiveMediaDetection.analyzeVideos }}<span class="_beta">{{ i18n.ts.beta }}</span></template>
 							<template #caption>{{ i18n.ts._sensitiveMediaDetection.analyzeVideosDescription }}</template>
 						</FormSwitch>
 
-						<FormSwitch v-model="setSensitiveFlagAutomatically" class="_formBlock">
+						<FormSwitch v-model="setSensitiveFlagAutomatically">
 							<template #label>{{ i18n.ts._sensitiveMediaDetection.setSensitiveFlagAutomatically }} ({{ i18n.ts.notRecommended }})</template>
 							<template #caption>{{ i18n.ts._sensitiveMediaDetection.setSensitiveFlagAutomaticallyDescription }}</template>
 						</FormSwitch>
 
 						<!-- 現状 false positive が多すぎて実用に耐えない
-						<FormSwitch v-model="disallowUploadWhenPredictedAsPorn" class="_formBlock">
+						<FormSwitch v-model="disallowUploadWhenPredictedAsPorn">
 							<template #label>{{ i18n.ts._sensitiveMediaDetection.disallowUploadWhenPredictedAsPorn }}</template>
 						</FormSwitch>
 						-->
 
-						<FormButton primary class="_formBlock" @click="save"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</FormButton>
+						<MkButton primary @click="save"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton>
 					</div>
 				</FormFolder>
 
-				<FormFolder class="_formBlock">
+				<FormFolder>
 					<template #label>Active Email Validation</template>
 					<template v-if="enableActiveEmailValidation" #suffix>Enabled</template>
 					<template v-else #suffix>Disabled</template>
 
-					<div class="_formRoot">
-						<span class="_formBlock">{{ i18n.ts.activeEmailValidationDescription }}</span>
-						<FormSwitch v-model="enableActiveEmailValidation" class="_formBlock" @update:model-value="save">
+					<div class="_gaps_m">
+						<span>{{ i18n.ts.activeEmailValidationDescription }}</span>
+						<FormSwitch v-model="enableActiveEmailValidation" @update:model-value="save">
 							<template #label>Enable</template>
 						</FormSwitch>
 					</div>
 				</FormFolder>
 
-				<FormFolder class="_formBlock">
+				<FormFolder>
 					<template #label>Log IP address</template>
 					<template v-if="enableIpLogging" #suffix>Enabled</template>
 					<template v-else #suffix>Disabled</template>
 
-					<div class="_formRoot">
-						<FormSwitch v-model="enableIpLogging" class="_formBlock" @update:model-value="save">
+					<div class="_gaps_m">
+						<FormSwitch v-model="enableIpLogging" @update:model-value="save">
 							<template #label>Enable</template>
 						</FormSwitch>
 					</div>
 				</FormFolder>
 
-				<FormFolder class="_formBlock">
+				<FormFolder>
 					<template #label>Summaly Proxy</template>
 
-					<div class="_formRoot">
-						<FormInput v-model="summalyProxy" class="_formBlock">
+					<div class="_gaps_m">
+						<FormInput v-model="summalyProxy">
 							<template #prefix><i class="ti ti-link"></i></template>
 							<template #label>Summaly Proxy URL</template>
 						</FormInput>
 
-						<FormButton primary class="_formBlock" @click="save"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</FormButton>
+						<MkButton primary @click="save"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton>
 					</div>
 				</FormFolder>
 			</div>
@@ -112,7 +112,7 @@ import FormInfo from '@/components/MkInfo.vue';
 import FormSuspense from '@/components/form/suspense.vue';
 import FormRange from '@/components/form/range.vue';
 import FormInput from '@/components/form/input.vue';
-import FormButton from '@/components/MkButton.vue';
+import MkButton from '@/components/MkButton.vue';
 import * as os from '@/os';
 import { fetchInstance } from '@/instance';
 import { i18n } from '@/i18n';
diff --git a/packages/frontend/src/pages/admin/settings.vue b/packages/frontend/src/pages/admin/settings.vue
index 460eb92694..2d7822efbe 100644
--- a/packages/frontend/src/pages/admin/settings.vue
+++ b/packages/frontend/src/pages/admin/settings.vue
@@ -4,141 +4,153 @@
 		<template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template>
 		<MkSpacer :content-max="700" :margin-min="16" :margin-max="32">
 			<FormSuspense :p="init">
-				<div class="_formRoot">
-					<FormInput v-model="name" class="_formBlock">
+				<div class="_gaps_m">
+					<FormInput v-model="name">
 						<template #label>{{ i18n.ts.instanceName }}</template>
 					</FormInput>
 
-					<FormTextarea v-model="description" class="_formBlock">
+					<FormTextarea v-model="description">
 						<template #label>{{ i18n.ts.instanceDescription }}</template>
 					</FormTextarea>
 
-					<FormInput v-model="tosUrl" class="_formBlock">
+					<FormInput v-model="tosUrl">
 						<template #prefix><i class="ti ti-link"></i></template>
 						<template #label>{{ i18n.ts.tosUrl }}</template>
 					</FormInput>
 
 					<FormSplit :min-width="300">
-						<FormInput v-model="maintainerName" class="_formBlock">
+						<FormInput v-model="maintainerName">
 							<template #label>{{ i18n.ts.maintainerName }}</template>
 						</FormInput>
 
-						<FormInput v-model="maintainerEmail" type="email" class="_formBlock">
+						<FormInput v-model="maintainerEmail" type="email">
 							<template #prefix><i class="ti ti-mail"></i></template>
 							<template #label>{{ i18n.ts.maintainerEmail }}</template>
 						</FormInput>
 					</FormSplit>
 
-					<FormTextarea v-model="pinnedUsers" class="_formBlock">
+					<FormTextarea v-model="pinnedUsers">
 						<template #label>{{ i18n.ts.pinnedUsers }}</template>
 						<template #caption>{{ i18n.ts.pinnedUsersDescription }}</template>
 					</FormTextarea>
 
 					<FormSection>
-						<FormSwitch v-model="enableRegistration" class="_formBlock">
-							<template #label>{{ i18n.ts.enableRegistration }}</template>
-						</FormSwitch>
+						<div class="_gaps_s">
+							<FormSwitch v-model="enableRegistration">
+								<template #label>{{ i18n.ts.enableRegistration }}</template>
+							</FormSwitch>
 
-						<FormSwitch v-model="emailRequiredForSignup" class="_formBlock">
-							<template #label>{{ i18n.ts.emailRequiredForSignup }}</template>
-						</FormSwitch>
+							<FormSwitch v-model="emailRequiredForSignup">
+								<template #label>{{ i18n.ts.emailRequiredForSignup }}</template>
+							</FormSwitch>
+						</div>
 					</FormSection>
 
 					<FormSection>
-						<FormSwitch v-model="enableLocalTimeline" class="_formBlock">{{ i18n.ts.enableLocalTimeline }}</FormSwitch>
-						<FormSwitch v-model="enableGlobalTimeline" class="_formBlock">{{ i18n.ts.enableGlobalTimeline }}</FormSwitch>
-						<FormInfo class="_formBlock">{{ i18n.ts.disablingTimelinesInfo }}</FormInfo>
+						<div class="_gaps_s">
+							<FormSwitch v-model="enableLocalTimeline">{{ i18n.ts.enableLocalTimeline }}</FormSwitch>
+							<FormSwitch v-model="enableGlobalTimeline">{{ i18n.ts.enableGlobalTimeline }}</FormSwitch>
+							<FormInfo>{{ i18n.ts.disablingTimelinesInfo }}</FormInfo>
+						</div>
 					</FormSection>
 
 					<FormSection>
 						<template #label>{{ i18n.ts.theme }}</template>
 
-						<FormInput v-model="iconUrl" class="_formBlock">
-							<template #prefix><i class="ti ti-link"></i></template>
-							<template #label>{{ i18n.ts.iconUrl }}</template>
-						</FormInput>
+						<div class="_gaps_m">
+							<FormInput v-model="iconUrl">
+								<template #prefix><i class="ti ti-link"></i></template>
+								<template #label>{{ i18n.ts.iconUrl }}</template>
+							</FormInput>
 
-						<FormInput v-model="bannerUrl" class="_formBlock">
-							<template #prefix><i class="ti ti-link"></i></template>
-							<template #label>{{ i18n.ts.bannerUrl }}</template>
-						</FormInput>
+							<FormInput v-model="bannerUrl">
+								<template #prefix><i class="ti ti-link"></i></template>
+								<template #label>{{ i18n.ts.bannerUrl }}</template>
+							</FormInput>
 
-						<FormInput v-model="backgroundImageUrl" class="_formBlock">
-							<template #prefix><i class="ti ti-link"></i></template>
-							<template #label>{{ i18n.ts.backgroundImageUrl }}</template>
-						</FormInput>
+							<FormInput v-model="backgroundImageUrl">
+								<template #prefix><i class="ti ti-link"></i></template>
+								<template #label>{{ i18n.ts.backgroundImageUrl }}</template>
+							</FormInput>
 
-						<FormInput v-model="themeColor" class="_formBlock">
-							<template #prefix><i class="ti ti-palette"></i></template>
-							<template #label>{{ i18n.ts.themeColor }}</template>
-							<template #caption>#RRGGBB</template>
-						</FormInput>
+							<FormInput v-model="themeColor">
+								<template #prefix><i class="ti ti-palette"></i></template>
+								<template #label>{{ i18n.ts.themeColor }}</template>
+								<template #caption>#RRGGBB</template>
+							</FormInput>
 
-						<FormTextarea v-model="defaultLightTheme" class="_formBlock">
-							<template #label>{{ i18n.ts.instanceDefaultLightTheme }}</template>
-							<template #caption>{{ i18n.ts.instanceDefaultThemeDescription }}</template>
-						</FormTextarea>
+							<FormTextarea v-model="defaultLightTheme">
+								<template #label>{{ i18n.ts.instanceDefaultLightTheme }}</template>
+								<template #caption>{{ i18n.ts.instanceDefaultThemeDescription }}</template>
+							</FormTextarea>
 
-						<FormTextarea v-model="defaultDarkTheme" class="_formBlock">
-							<template #label>{{ i18n.ts.instanceDefaultDarkTheme }}</template>
-							<template #caption>{{ i18n.ts.instanceDefaultThemeDescription }}</template>
-						</FormTextarea>
+							<FormTextarea v-model="defaultDarkTheme">
+								<template #label>{{ i18n.ts.instanceDefaultDarkTheme }}</template>
+								<template #caption>{{ i18n.ts.instanceDefaultThemeDescription }}</template>
+							</FormTextarea>
+						</div>
 					</FormSection>
 
 					<FormSection>
 						<template #label>{{ i18n.ts.files }}</template>
 
-						<FormSwitch v-model="cacheRemoteFiles" class="_formBlock">
-							<template #label>{{ i18n.ts.cacheRemoteFiles }}</template>
-							<template #caption>{{ i18n.ts.cacheRemoteFilesDescription }}</template>
-						</FormSwitch>
+						<div class="_gaps_m">
+							<FormSwitch v-model="cacheRemoteFiles">
+								<template #label>{{ i18n.ts.cacheRemoteFiles }}</template>
+								<template #caption>{{ i18n.ts.cacheRemoteFilesDescription }}</template>
+							</FormSwitch>
 
-						<FormSplit :min-width="280">
-							<FormInput v-model="localDriveCapacityMb" type="number" class="_formBlock">
-								<template #label>{{ i18n.ts.driveCapacityPerLocalAccount }}</template>
-								<template #suffix>MB</template>
-								<template #caption>{{ i18n.ts.inMb }}</template>
-							</FormInput>
+							<FormSplit :min-width="280">
+								<FormInput v-model="localDriveCapacityMb" type="number">
+									<template #label>{{ i18n.ts.driveCapacityPerLocalAccount }}</template>
+									<template #suffix>MB</template>
+									<template #caption>{{ i18n.ts.inMb }}</template>
+								</FormInput>
 
-							<FormInput v-model="remoteDriveCapacityMb" type="number" :disabled="!cacheRemoteFiles" class="_formBlock">
-								<template #label>{{ i18n.ts.driveCapacityPerRemoteAccount }}</template>
-								<template #suffix>MB</template>
-								<template #caption>{{ i18n.ts.inMb }}</template>
-							</FormInput>
-						</FormSplit>
+								<FormInput v-model="remoteDriveCapacityMb" type="number" :disabled="!cacheRemoteFiles">
+									<template #label>{{ i18n.ts.driveCapacityPerRemoteAccount }}</template>
+									<template #suffix>MB</template>
+									<template #caption>{{ i18n.ts.inMb }}</template>
+								</FormInput>
+							</FormSplit>
+						</div>
 					</FormSection>
 
 					<FormSection>
 						<template #label>ServiceWorker</template>
 
-						<FormSwitch v-model="enableServiceWorker" class="_formBlock">
-							<template #label>{{ i18n.ts.enableServiceworker }}</template>
-							<template #caption>{{ i18n.ts.serviceworkerInfo }}</template>
-						</FormSwitch>
+						<div class="_gaps_m">
+							<FormSwitch v-model="enableServiceWorker">
+								<template #label>{{ i18n.ts.enableServiceworker }}</template>
+								<template #caption>{{ i18n.ts.serviceworkerInfo }}</template>
+							</FormSwitch>
 
-						<template v-if="enableServiceWorker">
-							<FormInput v-model="swPublicKey" class="_formBlock">
-								<template #prefix><i class="ti ti-key"></i></template>
-								<template #label>Public key</template>
-							</FormInput>
+							<template v-if="enableServiceWorker">
+								<FormInput v-model="swPublicKey">
+									<template #prefix><i class="ti ti-key"></i></template>
+									<template #label>Public key</template>
+								</FormInput>
 
-							<FormInput v-model="swPrivateKey" class="_formBlock">
-								<template #prefix><i class="ti ti-key"></i></template>
-								<template #label>Private key</template>
-							</FormInput>
-						</template>
+								<FormInput v-model="swPrivateKey">
+									<template #prefix><i class="ti ti-key"></i></template>
+									<template #label>Private key</template>
+								</FormInput>
+							</template>
+						</div>
 					</FormSection>
 
 					<FormSection>
 						<template #label>DeepL Translation</template>
 
-						<FormInput v-model="deeplAuthKey" class="_formBlock">
-							<template #prefix><i class="ti ti-key"></i></template>
-							<template #label>DeepL Auth Key</template>
-						</FormInput>
-						<FormSwitch v-model="deeplIsPro" class="_formBlock">
-							<template #label>Pro account</template>
-						</FormSwitch>
+						<div class="_gaps_m">
+							<FormInput v-model="deeplAuthKey">
+								<template #prefix><i class="ti ti-key"></i></template>
+								<template #label>DeepL Auth Key</template>
+							</FormInput>
+							<FormSwitch v-model="deeplIsPro">
+								<template #label>Pro account</template>
+							</FormSwitch>
+						</div>
 					</FormSection>
 				</div>
 			</FormSuspense>
diff --git a/packages/frontend/src/pages/announcements.vue b/packages/frontend/src/pages/announcements.vue
index 6a93b3b9fa..b06bd30245 100644
--- a/packages/frontend/src/pages/announcements.vue
+++ b/packages/frontend/src/pages/announcements.vue
@@ -2,14 +2,14 @@
 <MkStickyContainer>
 	<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
 	<MkSpacer :content-max="800">
-		<MkPagination v-slot="{items}" :pagination="pagination" class="ruryvtyk _content">
-			<section v-for="(announcement, i) in items" :key="announcement.id" class="_card announcement">
-				<div class="_title"><span v-if="$i && !announcement.isRead">🆕 </span>{{ announcement.title }}</div>
-				<div class="_content">
+		<MkPagination v-slot="{items}" :pagination="pagination" class="ruryvtyk _gaps_m">
+			<section v-for="(announcement, i) in items" :key="announcement.id" class="announcement _panel">
+				<div class="header"><span v-if="$i && !announcement.isRead">🆕 </span>{{ announcement.title }}</div>
+				<div class="content">
 					<Mfm :text="announcement.text"/>
 					<img v-if="announcement.imageUrl" :src="announcement.imageUrl"/>
 				</div>
-				<div v-if="$i && !announcement.isRead" class="_footer">
+				<div v-if="$i && !announcement.isRead" class="footer">
 					<MkButton primary @click="read(items, announcement, i)"><i class="ti ti-check"></i> {{ $ts.gotIt }}</MkButton>
 				</div>
 			</section>
@@ -53,17 +53,24 @@ definePageMetadata({
 <style lang="scss" scoped>
 .ruryvtyk {
 	> .announcement {
-		&:not(:last-child) {
-			margin-bottom: var(--margin);
+		> .header {
+			padding: 16px;
+			font-weight: bold;
 		}
 
-		> ._content {
+		> .content {
+			padding: 0 16px;
+		
 			> img {
 				display: block;
 				max-height: 300px;
 				max-width: 100%;
 			}
 		}
+
+		> .footer {
+			padding: 16px;
+		}
 	}
 }
 </style>
diff --git a/packages/frontend/src/pages/antenna-timeline.vue b/packages/frontend/src/pages/antenna-timeline.vue
index af7e95d543..bf3004f280 100644
--- a/packages/frontend/src/pages/antenna-timeline.vue
+++ b/packages/frontend/src/pages/antenna-timeline.vue
@@ -3,7 +3,7 @@
 	<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
 	<div ref="rootEl" v-hotkey.global="keymap" class="tqmomfks">
 		<div v-if="queue > 0" class="new"><button class="_buttonPrimary" @click="top()">{{ $ts.newNoteRecived }}</button></div>
-		<div class="tl _block">
+		<div class="tl">
 			<XTimeline
 				ref="tlEl" :key="antennaId"
 				class="tl"
diff --git a/packages/frontend/src/pages/api-console.vue b/packages/frontend/src/pages/api-console.vue
index 1d5339b44c..c8ca3fffa7 100644
--- a/packages/frontend/src/pages/api-console.vue
+++ b/packages/frontend/src/pages/api-console.vue
@@ -2,23 +2,23 @@
 <MkStickyContainer>
 	<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
 	<MkSpacer :content-max="700">
-		<div class="_formRoot">
-			<div class="_formBlock">
-				<MkInput v-model="endpoint" :datalist="endpoints" class="_formBlock" @update:model-value="onEndpointChange()">
+		<div class="_gaps_m">
+			<div class="_gaps_m">
+				<MkInput v-model="endpoint" :datalist="endpoints" @update:model-value="onEndpointChange()">
 					<template #label>Endpoint</template>
 				</MkInput>
-				<MkTextarea v-model="body" class="_formBlock" code>
+				<MkTextarea v-model="body" code>
 					<template #label>Params (JSON or JSON5)</template>
 				</MkTextarea>
-				<MkSwitch v-model="withCredential" class="_formBlock">
+				<MkSwitch v-model="withCredential">
 					With credential
 				</MkSwitch>
-				<MkButton class="_formBlock" primary :disabled="sending" @click="send">
+				<MkButton primary :disabled="sending" @click="send">
 					<template v-if="sending"><MkEllipsis/></template>
 					<template v-else><i class="ti ti-send"></i> Send</template>
 				</MkButton>
 			</div>
-			<div v-if="res" class="_formBlock">
+			<div v-if="res">
 				<MkTextarea v-model="res" code readonly tall>
 					<template #label>Response</template>
 				</MkTextarea>
diff --git a/packages/frontend/src/pages/auth.form.vue b/packages/frontend/src/pages/auth.form.vue
index 1546735266..801295fce9 100644
--- a/packages/frontend/src/pages/auth.form.vue
+++ b/packages/frontend/src/pages/auth.form.vue
@@ -1,18 +1,18 @@
 <template>
-<section class="_section">
-	<div class="_title">{{ $t('_auth.shareAccess', { name: app.name }) }}</div>
-	<div class="_content">
+<section class="">
+	<div class="">{{ $t('_auth.shareAccess', { name: app.name }) }}</div>
+	<div class="">
 		<h2>{{ app.name }}</h2>
 		<p class="id">{{ app.id }}</p>
 		<p class="description">{{ app.description }}</p>
 	</div>
-	<div class="_content">
+	<div class="">
 		<h2>{{ $ts._auth.permissionAsk }}</h2>
 		<ul>
 			<li v-for="p in app.permission" :key="p">{{ $t(`_permissions.${p}`) }}</li>
 		</ul>
 	</div>
-	<div class="_footer">
+	<div class="">
 		<MkButton inline @click="cancel">{{ $ts.cancel }}</MkButton>
 		<MkButton inline primary @click="accept">{{ $ts.accept }}</MkButton>
 	</div>
diff --git a/packages/frontend/src/pages/channel-editor.vue b/packages/frontend/src/pages/channel-editor.vue
index 5ae7e63f99..8eaea86917 100644
--- a/packages/frontend/src/pages/channel-editor.vue
+++ b/packages/frontend/src/pages/channel-editor.vue
@@ -2,12 +2,12 @@
 <MkStickyContainer>
 	<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
 	<MkSpacer :content-max="700">
-		<div class="_formRoot">
-			<MkInput v-model="name" class="_formBlock">
+		<div class="_gaps_m">
+			<MkInput v-model="name">
 				<template #label>{{ i18n.ts.name }}</template>
 			</MkInput>
 
-			<MkTextarea v-model="description" class="_formBlock">
+			<MkTextarea v-model="description">
 				<template #label>{{ i18n.ts.description }}</template>
 			</MkTextarea>
 
@@ -18,7 +18,7 @@
 					<MkButton @click="removeBannerImage()"><i class="ti ti-trash"></i> {{ i18n.ts._channel.removeBanner }}</MkButton>
 				</div>
 			</div>
-			<div class="_formBlock">
+			<div>
 				<MkButton primary @click="save()"><i class="ti ti-device-floppy"></i> {{ channelId ? i18n.ts.save : i18n.ts.create }}</MkButton>
 			</div>
 		</div>
diff --git a/packages/frontend/src/pages/channel.vue b/packages/frontend/src/pages/channel.vue
index f271bb270f..96340a36b9 100644
--- a/packages/frontend/src/pages/channel.vue
+++ b/packages/frontend/src/pages/channel.vue
@@ -3,7 +3,7 @@
 	<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
 	<MkSpacer :content-max="700">
 		<div v-if="channel">
-			<div class="wpgynlbz _panel _gap" :class="{ hide: !showBanner }">
+			<div class="wpgynlbz _panel _margin" :class="{ hide: !showBanner }">
 				<XChannelFollowButton :channel="channel" :full="true" class="subscribe"/>
 				<button class="_button toggle" @click="() => showBanner = !showBanner">
 					<template v-if="showBanner"><i class="ti ti-chevron-up"></i></template>
@@ -23,9 +23,9 @@
 				</div>
 			</div>
 
-			<XPostForm v-if="$i" :channel="channel" class="post-form _panel _gap" fixed/>
+			<XPostForm v-if="$i" :channel="channel" class="post-form _panel _margin" fixed/>
 
-			<XTimeline :key="channelId" class="_gap" src="channel" :channel="channelId" @before="before" @after="after"/>
+			<XTimeline :key="channelId" class="_margin" src="channel" :channel="channelId" @before="before" @after="after"/>
 		</div>
 	</MkSpacer>
 </MkStickyContainer>
diff --git a/packages/frontend/src/pages/channels.vue b/packages/frontend/src/pages/channels.vue
index 34e9dac196..9043d06c52 100644
--- a/packages/frontend/src/pages/channels.vue
+++ b/packages/frontend/src/pages/channels.vue
@@ -2,20 +2,20 @@
 <MkStickyContainer>
 	<template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template>
 	<MkSpacer :content-max="700">
-		<div v-if="tab === 'featured'" class="_content grwlizim featured">
+		<div v-if="tab === 'featured'" class="grwlizim featured">
 			<MkPagination v-slot="{items}" :pagination="featuredPagination">
-				<MkChannelPreview v-for="channel in items" :key="channel.id" class="_gap" :channel="channel"/>
+				<MkChannelPreview v-for="channel in items" :key="channel.id" class="_margin" :channel="channel"/>
 			</MkPagination>
 		</div>
-		<div v-else-if="tab === 'following'" class="_content grwlizim following">
+		<div v-else-if="tab === 'following'" class="grwlizim following">
 			<MkPagination v-slot="{items}" :pagination="followingPagination">
-				<MkChannelPreview v-for="channel in items" :key="channel.id" class="_gap" :channel="channel"/>
+				<MkChannelPreview v-for="channel in items" :key="channel.id" class="_margin" :channel="channel"/>
 			</MkPagination>
 		</div>
-		<div v-else-if="tab === 'owned'" class="_content grwlizim owned">
+		<div v-else-if="tab === 'owned'" class="grwlizim owned">
 			<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="_gap" :channel="channel"/>
+				<MkChannelPreview v-for="channel in items" :key="channel.id" class="_margin" :channel="channel"/>
 			</MkPagination>
 		</div>
 	</MkSpacer>
diff --git a/packages/frontend/src/pages/explore.users.vue b/packages/frontend/src/pages/explore.users.vue
index 611ca0f003..f1bb0cc62e 100644
--- a/packages/frontend/src/pages/explore.users.vue
+++ b/packages/frontend/src/pages/explore.users.vue
@@ -6,26 +6,26 @@
 	</MkTab>
 	<div v-if="origin === 'local'">
 		<template v-if="tag == null">
-			<MkFolder class="_gap" persist-key="explore-pinned-users">
+			<MkFolder class="_margin" persist-key="explore-pinned-users">
 				<template #header><i class="fas fa-bookmark ti-fw" style="margin-right: 0.5em;"></i>{{ i18n.ts.pinnedUsers }}</template>
 				<XUserList :pagination="pinnedUsers"/>
 			</MkFolder>
-			<MkFolder class="_gap" persist-key="explore-popular-users">
+			<MkFolder class="_margin" persist-key="explore-popular-users">
 				<template #header><i class="fas fa-chart-line ti-fw" style="margin-right: 0.5em;"></i>{{ i18n.ts.popularUsers }}</template>
 				<XUserList :pagination="popularUsers"/>
 			</MkFolder>
-			<MkFolder class="_gap" persist-key="explore-recently-updated-users">
+			<MkFolder class="_margin" persist-key="explore-recently-updated-users">
 				<template #header><i class="fas fa-comment-alt ti-fw" style="margin-right: 0.5em;"></i>{{ i18n.ts.recentlyUpdatedUsers }}</template>
 				<XUserList :pagination="recentlyUpdatedUsers"/>
 			</MkFolder>
-			<MkFolder class="_gap" persist-key="explore-recently-registered-users">
+			<MkFolder class="_margin" persist-key="explore-recently-registered-users">
 				<template #header><i class="ti ti-plus ti-fw" style="margin-right: 0.5em;"></i>{{ i18n.ts.recentlyRegisteredUsers }}</template>
 				<XUserList :pagination="recentlyRegisteredUsers"/>
 			</MkFolder>
 		</template>
 	</div>
 	<div v-else>
-		<MkFolder ref="tagsEl" :foldable="true" :expanded="false" class="_gap">
+		<MkFolder ref="tagsEl" :foldable="true" :expanded="false" class="_margin">
 			<template #header><i class="ti ti-hash ti-fw" style="margin-right: 0.5em;"></i>{{ i18n.ts.popularTags }}</template>
 
 			<div class="vxjfqztj">
@@ -34,21 +34,21 @@
 			</div>
 		</MkFolder>
 
-		<MkFolder v-if="tag != null" :key="`${tag}`" class="_gap">
+		<MkFolder v-if="tag != null" :key="`${tag}`" class="_margin">
 			<template #header><i class="ti ti-hash ti-fw" style="margin-right: 0.5em;"></i>{{ tag }}</template>
 			<XUserList :pagination="tagUsers"/>
 		</MkFolder>
 
 		<template v-if="tag == null">
-			<MkFolder class="_gap">
+			<MkFolder class="_margin">
 				<template #header><i class="fas fa-chart-line ti-fw" style="margin-right: 0.5em;"></i>{{ i18n.ts.popularUsers }}</template>
 				<XUserList :pagination="popularUsersF"/>
 			</MkFolder>
-			<MkFolder class="_gap">
+			<MkFolder class="_margin">
 				<template #header><i class="fas fa-comment-alt ti-fw" style="margin-right: 0.5em;"></i>{{ i18n.ts.recentlyUpdatedUsers }}</template>
 				<XUserList :pagination="recentlyUpdatedUsersF"/>
 			</MkFolder>
-			<MkFolder class="_gap">
+			<MkFolder class="_margin">
 				<template #header><i class="fas fa-rocket ti-fw" style="margin-right: 0.5em;"></i>{{ i18n.ts.recentlyDiscoveredUsers }}</template>
 				<XUserList :pagination="recentlyRegisteredUsersF"/>
 			</MkFolder>
diff --git a/packages/frontend/src/pages/explore.vue b/packages/frontend/src/pages/explore.vue
index 4494f6154d..cb62af469a 100644
--- a/packages/frontend/src/pages/explore.vue
+++ b/packages/frontend/src/pages/explore.vue
@@ -11,18 +11,18 @@
 		<div v-else-if="tab === 'search'">
 			<MkSpacer :content-max="1200">
 				<div>
-					<MkInput v-model="searchQuery" :debounce="true" type="search" class="_formBlock">
+					<MkInput v-model="searchQuery" :debounce="true" type="search">
 						<template #prefix><i class="ti ti-search"></i></template>
 						<template #label>{{ i18n.ts.searchUser }}</template>
 					</MkInput>
-					<MkRadios v-model="searchOrigin" class="_formBlock">
+					<MkRadios v-model="searchOrigin">
 						<option value="combined">{{ i18n.ts.all }}</option>
 						<option value="local">{{ i18n.ts.local }}</option>
 						<option value="remote">{{ i18n.ts.remote }}</option>
 					</MkRadios>
 				</div>
 
-				<XUserList v-if="searchQuery" ref="searchEl" class="_gap" :pagination="searchPagination"/>
+				<XUserList v-if="searchQuery" ref="searchEl" class="_margin" :pagination="searchPagination"/>
 			</MkSpacer>
 		</div>
 	</div>
diff --git a/packages/frontend/src/pages/flash/flash-edit.vue b/packages/frontend/src/pages/flash/flash-edit.vue
new file mode 100644
index 0000000000..3b7535071f
--- /dev/null
+++ b/packages/frontend/src/pages/flash/flash-edit.vue
@@ -0,0 +1,113 @@
+<template>
+<MkStickyContainer>
+	<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
+	<MkSpacer :content-max="700">
+		<div class="_gaps_m">
+			<MkInput v-model="title">
+				<template #label>{{ i18n.ts._play.title }}</template>
+			</MkInput>
+			<MkTextarea v-model="summary">
+				<template #label>{{ i18n.ts._play.summary }}</template>
+			</MkTextarea>
+			<MkTextarea v-model="script" class="_monospace" tall spellcheck="false">
+				<template #label>{{ i18n.ts._play.script }}</template>
+			</MkTextarea>
+			<div class="_buttons">
+				<MkButton primary @click="save"><i class="ti ti-check"></i> {{ i18n.ts.save }}</MkButton>
+				<MkButton @click="show"><i class="ti ti-eye"></i> {{ i18n.ts.show }}</MkButton>
+			</div>
+		</div>
+	</MkSpacer>
+</MkStickyContainer>
+</template>
+
+<script lang="ts" setup>
+import { computed, onDeactivated, onUnmounted, Ref, ref, watch } from 'vue';
+import MkButton from '@/components/MkButton.vue';
+import * as os from '@/os';
+import { url } from '@/config';
+import { i18n } from '@/i18n';
+import { definePageMetadata } from '@/scripts/page-metadata';
+import MkTextarea from '@/components/form/textarea.vue';
+import MkInput from '@/components/form/input.vue';
+import { useRouter } from '@/router';
+
+const router = useRouter();
+
+const props = defineProps<{
+	id?: string;
+}>();
+
+let flash = $ref(null);
+
+if (props.id) {
+	flash = await os.api('flash/show', {
+		flashId: props.id,
+	});
+}
+
+let title = $ref(flash?.title ?? 'New Play');
+let summary = $ref(flash?.summary ?? '');
+let permissions = $ref(flash?.permissions ?? []);
+let script = $ref(flash?.script ?? `/// @ 0.12.1
+
+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}!\`)
+		}
+	})
+])
+`);
+
+async function save() {
+	if (flash) {
+		os.apiWithDialog('flash/update', {
+			flashId: props.id,
+			title,
+			summary,
+			permissions,
+			script,
+		});
+	} else {
+		const created = await os.apiWithDialog('flash/create', {
+			title,
+			summary,
+			permissions,
+			script,
+		});
+		router.push('/play/' + created.id + '/edit');
+	}
+}
+
+function show() {
+	if (flash == null) {
+		os.alert({
+			text: 'Please save',
+		});
+	} else {
+		os.pageWindow(`/play/${flash.id}`);
+	}
+}
+
+const headerActions = $computed(() => []);
+
+const headerTabs = $computed(() => []);
+
+definePageMetadata(computed(() => flash ? {
+	title: i18n.ts._play.edit + ': ' + flash.title,
+} : {
+	title: i18n.ts._play.new,
+}));
+</script>
+
+<style lang="scss" scoped>
+
+</style>
diff --git a/packages/frontend/src/pages/flash/flash-index.vue b/packages/frontend/src/pages/flash/flash-index.vue
new file mode 100644
index 0000000000..fb377be579
--- /dev/null
+++ b/packages/frontend/src/pages/flash/flash-index.vue
@@ -0,0 +1,105 @@
+<template>
+<MkStickyContainer>
+	<template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template>
+	<MkSpacer :content-max="700">
+		<div v-if="tab === 'featured'" class="">
+			<MkPagination v-slot="{items}" :pagination="featuredFlashsPagination">
+				<div class="_gaps_s">
+					<MkFlashPreview v-for="flash in items" :key="flash.id" class="" :flash="flash"/>
+				</div>
+			</MkPagination>
+		</div>
+
+		<div v-else-if="tab === 'my'" class="my">
+			<MkButton class="new" @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" class="" :flash="flash"/>
+				</div>
+			</MkPagination>
+		</div>
+
+		<div v-else-if="tab === 'liked'" class="">
+			<MkPagination v-slot="{items}" :pagination="likedFlashsPagination">
+				<div class="_gaps_s">
+					<MkFlashPreview v-for="like in items" :key="like.flash.id" class="" :flash="like.flash"/>
+				</div>
+			</MkPagination>
+		</div>
+	</MkSpacer>
+</MkStickyContainer>
+</template>
+
+<script lang="ts" setup>
+import { computed, inject } from 'vue';
+import MkFlashPreview from '@/components/MkFlashPreview.vue';
+import MkPagination from '@/components/MkPagination.vue';
+import MkButton from '@/components/MkButton.vue';
+import { useRouter } from '@/router';
+import { i18n } from '@/i18n';
+import { definePageMetadata } from '@/scripts/page-metadata';
+
+const router = useRouter();
+
+let tab = $ref('featured');
+
+const featuredFlashsPagination = {
+	endpoint: 'flash/featured' as const,
+	noPaging: true,
+};
+const myFlashsPagination = {
+	endpoint: 'flash/my' as const,
+	limit: 5,
+};
+const likedFlashsPagination = {
+	endpoint: 'flash/my-likes' as const,
+	limit: 5,
+};
+
+function create() {
+	router.push('/play/new');
+}
+
+const headerActions = $computed(() => [{
+	icon: 'ti ti-plus',
+	text: i18n.ts.create,
+	handler: create,
+}]);
+
+const headerTabs = $computed(() => [{
+	key: 'featured',
+	title: i18n.ts._play.featured,
+	icon: 'fas fa-fire-alt',
+}, {
+	key: 'my',
+	title: i18n.ts._play.my,
+	icon: 'ti ti-edit',
+}, {
+	key: 'liked',
+	title: i18n.ts._play.liked,
+	icon: 'ti ti-heart',
+}]);
+
+definePageMetadata(computed(() => ({
+	title: 'Play',
+	icon: 'ti ti-player-play',
+})));
+</script>
+
+<style lang="scss" scoped>
+.rknalgpo {
+	&.my .ckltabjg:first-child {
+		margin-top: 16px;
+	}
+
+	.ckltabjg:not(:last-child) {
+		margin-bottom: 8px;
+	}
+
+	@media (min-width: 500px) {
+		.ckltabjg:not(:last-child) {
+			margin-bottom: 16px;
+		}
+	}
+}
+</style>
diff --git a/packages/frontend/src/pages/flash/flash.vue b/packages/frontend/src/pages/flash/flash.vue
new file mode 100644
index 0000000000..63a1e47038
--- /dev/null
+++ b/packages/frontend/src/pages/flash/flash.vue
@@ -0,0 +1,293 @@
+<template>
+<MkStickyContainer>
+	<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
+	<MkSpacer :content-max="700">
+		<Transition :name="$store.state.animation ? 'fade' : ''" mode="out-in">
+			<div v-if="flash" :key="flash.id">
+				<Transition :name="$store.state.animation ? 'zoom' : ''" mode="out-in">
+					<div v-if="started" :class="$style.started">
+						<div class="main _panel">
+							<MkAsUi v-if="root" :component="root" :components="components"/>
+						</div>
+						<div class="actions _panel">
+							<MkButton v-if="flash.isLiked" v-tooltip="i18n.ts.unlike" as-like class="button" rounded primary @click="unlike()"><i class="ti ti-heart"></i><span v-if="flash.likedCount > 0" style="margin-left: 6px;">{{ flash.likedCount }}</span></MkButton>
+							<MkButton v-else v-tooltip="i18n.ts.like" as-like class="button" rounded @click="like()"><i class="ti ti-heart"></i><span v-if="flash.likedCount > 0" style="margin-left: 6px;">{{ flash.likedCount }}</span></MkButton>
+							<MkButton v-tooltip="i18n.ts.shareWithNote" class="button" rounded @click="shareWithNote"><i class="ti ti-repeat ti-fw"></i></MkButton>
+							<MkButton v-tooltip="i18n.ts.share" class="button" rounded @click="share"><i class="ti ti-share ti-fw"></i></MkButton>
+						</div>
+					</div>
+					<div v-else :class="$style.ready">
+						<div class="_panel main">
+							<div class="title">{{ flash.title }}</div>
+							<div class="summary">{{ flash.summary }}</div>
+							<MkButton class="start" gradate rounded large @click="start">Play</MkButton>
+							<div class="info">
+								<span v-tooltip="i18n.ts.numberOfLikes"><i class="ti ti-heart"></i> {{ flash.likedCount }}</span>
+							</div>
+						</div>
+					</div>
+				</Transition>
+				<FormFolder class="_margin">
+					<template #icon><i class="ti ti-code"></i></template>
+					<template #label>{{ i18n.ts._play.viewSource }}</template>
+
+					<MkTextarea :model-value="flash.script" readonly tall class="_monospace" spellcheck="false"></MkTextarea>
+				</FormFolder>
+				<div :class="$style.footer">
+					<Mfm :text="`By @${flash.user.username}`"/>
+					<div class="date">
+						<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>
+				<MkAd :prefer="['horizontal', 'horizontal-big']"/>
+			</div>
+			<MkError v-else-if="error" @retry="fetchPage()"/>
+			<MkLoading v-else/>
+		</Transition>
+	</MkSpacer>
+</MkStickyContainer>
+</template>
+
+<script lang="ts" setup>
+import { computed, onDeactivated, onUnmounted, Ref, ref, watch } from 'vue';
+import { Interpreter, Parser, utils, values } from '@syuilo/aiscript';
+import MkButton from '@/components/MkButton.vue';
+import * as os from '@/os';
+import { url } from '@/config';
+import MkFollowButton from '@/components/MkFollowButton.vue';
+import MkContainer from '@/components/MkContainer.vue';
+import MkPagination from '@/components/MkPagination.vue';
+import MkPagePreview from '@/components/MkPagePreview.vue';
+import { i18n } from '@/i18n';
+import { definePageMetadata } from '@/scripts/page-metadata';
+import MkAsUi from '@/components/MkAsUi.vue';
+import { AsUiComponent, AsUiRoot, patch, registerAsUiLib, render } from '@/scripts/aiscript/ui';
+import { createAiScriptEnv } from '@/scripts/aiscript/api';
+import FormFolder from '@/components/form/folder.vue';
+import MkTextarea from '@/components/form/textarea.vue';
+
+const props = defineProps<{
+	id: string;
+}>();
+
+let flash = $ref(null);
+let error = $ref(null);
+
+function fetchFlash() {
+	flash = null;
+	os.api('flash/show', {
+		flashId: props.id,
+	}).then(_flash => {
+		flash = _flash;
+	}).catch(err => {
+		error = err;
+	});
+}
+
+function share() {
+	navigator.share({
+		title: flash.title,
+		text: flash.summary,
+		url: `${url}/play/${flash.id}`,
+	});
+}
+
+function shareWithNote() {
+	os.post({
+		initialText: `${flash.title} ${url}/play/${flash.id}`,
+	});
+}
+
+function like() {
+	os.apiWithDialog('flash/like', {
+		flashId: flash.id,
+	}).then(() => {
+		flash.isLiked = true;
+		flash.likedCount++;
+	});
+}
+
+async function unlike() {
+	const confirm = await os.confirm({
+		type: 'warning',
+		text: i18n.ts.unlikeConfirm,
+	});
+	if (confirm.canceled) return;
+	os.apiWithDialog('flash/unlike', {
+		flashId: flash.id,
+	}).then(() => {
+		flash.isLiked = false;
+		flash.likedCount--;
+	});
+}
+
+watch(() => props.id, fetchFlash, { immediate: true });
+
+const parser = new Parser();
+
+let started = $ref(false);
+let aiscript = $shallowRef<Interpreter | null>(null);
+const root = ref<AsUiRoot>();
+const components: Ref<AsUiComponent>[] = [];
+
+function start() {
+	started = true;
+	run();
+}
+
+async function run() {
+	if (aiscript) aiscript.abort();
+
+	aiscript = new Interpreter({
+		...createAiScriptEnv({
+			storageKey: 'flash:' + flash.id,
+		}),
+		...registerAsUiLib(components, (_root) => {
+			root.value = _root.value;
+		}),
+		THIS_ID: values.STR(flash.id),
+		THIS_URL: values.STR(`${url}/play/${flash.id}`),
+	}, {
+		in: (q) => {
+			return new Promise(ok => {
+				os.inputText({
+					title: q,
+				}).then(({ canceled, result: a }) => {
+					ok(a);
+				});
+			});
+		},
+		out: (value) => {
+			// nop
+		},
+		log: (type, params) => {
+			// nop
+		},
+	});
+
+	let ast;
+	try {
+		ast = parser.parse(flash.script);
+	} catch (err) {
+		os.alert({
+			type: 'error',
+			text: 'Syntax error :(',
+		});
+		return;
+	}
+	try {
+		await aiscript.exec(ast);
+	} catch (err) {
+		os.alert({
+			type: 'error',
+			title: 'AiScript Error',
+			text: err.message,
+		});
+	}
+}
+
+onDeactivated(() => {
+	if (aiscript) aiscript.abort();
+});
+
+onUnmounted(() => {
+	if (aiscript) aiscript.abort();
+});
+
+const headerActions = $computed(() => []);
+
+const headerTabs = $computed(() => []);
+
+definePageMetadata(computed(() => flash ? {
+	title: flash.title,
+	avatar: flash.user,
+	path: `/play/${flash.id}`,
+	share: {
+		title: flash.title,
+		text: flash.summary,
+	},
+} : null));
+</script>
+
+<style lang="scss" module>
+.ready {
+	&:global {
+		> .main {
+			padding: 32px;
+
+			> .title {
+				font-size: 1.4em;
+				font-weight: bold;
+				margin-bottom: 1rem;
+				text-align: center;
+			}
+
+			> .summary {
+				font-size: 1.1em;
+				text-align: center;
+			}
+
+			> .start {
+				margin: 1em auto 1em auto;
+			}
+
+			> .info {
+				text-align: center;
+			}
+		}
+	}
+}
+
+.footer {
+	margin-top: 16px;
+
+	&:global {
+		> .date {
+			margin: 8px 0;
+			opacity: 0.6;
+		}
+	}
+}
+
+.started {
+	&:global {
+		> .main {
+			padding: 32px;
+		}
+
+		> .actions {
+			display: flex;
+			justify-content: center;
+			gap: 12px;
+			margin-top: 16px;
+			padding: 16px;
+		}
+	}
+}
+</style>
+
+<style lang="scss" scoped>
+.fade-enter-active,
+.fade-leave-active {
+	transition: opacity 0.125s ease;
+}
+.fade-enter-from,
+.fade-leave-to {
+	opacity: 0;
+}
+
+.zoom-enter-active,
+.zoom-leave-active {
+	transition: opacity 0.3s cubic-bezier(0,0,.35,1), transform 0.3s cubic-bezier(0,0,.35,1);
+}
+.zoom-enter-from {
+	opacity: 0;
+	transform: scale(0.7);
+}
+.zoom-leave-to {
+	opacity: 0;
+	transform: scale(1.3);
+}
+</style>
diff --git a/packages/frontend/src/pages/gallery/edit.vue b/packages/frontend/src/pages/gallery/edit.vue
index c8111d7890..0e3a1ce061 100644
--- a/packages/frontend/src/pages/gallery/edit.vue
+++ b/packages/frontend/src/pages/gallery/edit.vue
@@ -16,15 +16,15 @@
 					<div class="name">{{ file.name }}</div>
 					<button v-tooltip="i18n.ts.remove" class="remove _button" @click="remove(file)"><i class="ti ti-x"></i></button>
 				</div>
-				<FormButton primary @click="selectFile"><i class="ti ti-plus"></i> {{ i18n.ts.attachFile }}</FormButton>
+				<MkButton primary @click="selectFile"><i class="ti ti-plus"></i> {{ i18n.ts.attachFile }}</MkButton>
 			</div>
 
 			<FormSwitch v-model="isSensitive">{{ i18n.ts.markAsSensitive }}</FormSwitch>
 
-			<FormButton v-if="postId" primary @click="save"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</FormButton>
-			<FormButton v-else primary @click="save"><i class="ti ti-device-floppy"></i> {{ i18n.ts.publish }}</FormButton>
+			<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>
 
-			<FormButton v-if="postId" danger @click="del"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</FormButton>
+			<MkButton v-if="postId" danger @click="del"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton>
 		</FormSuspense>
 	</MkSpacer>
 </MkStickyContainer>
@@ -32,7 +32,7 @@
 
 <script lang="ts" setup>
 import { computed, inject, watch } from 'vue';
-import FormButton from '@/components/MkButton.vue';
+import MkButton from '@/components/MkButton.vue';
 import FormInput from '@/components/form/input.vue';
 import FormTextarea from '@/components/form/textarea.vue';
 import FormSwitch from '@/components/form/switch.vue';
diff --git a/packages/frontend/src/pages/gallery/index.vue b/packages/frontend/src/pages/gallery/index.vue
index 24a634bab5..b29a6a5310 100644
--- a/packages/frontend/src/pages/gallery/index.vue
+++ b/packages/frontend/src/pages/gallery/index.vue
@@ -4,7 +4,7 @@
 	<MkSpacer :content-max="1400">
 		<div class="_root">
 			<div v-if="tab === 'explore'">
-				<MkFolder class="_gap">
+				<MkFolder class="_margin">
 					<template #header><i class="ti ti-clock"></i>{{ i18n.ts.recentPosts }}</template>
 					<MkPagination v-slot="{items}" :pagination="recentPostsPagination" :disable-auto-load="true">
 						<div class="vfpdbgtk">
@@ -12,7 +12,7 @@
 						</div>
 					</MkPagination>
 				</MkFolder>
-				<MkFolder class="_gap">
+				<MkFolder class="_margin">
 					<template #header><i class="ti ti-comet"></i>{{ i18n.ts.popularPosts }}</template>
 					<MkPagination v-slot="{items}" :pagination="popularPostsPagination" :disable-auto-load="true">
 						<div class="vfpdbgtk">
diff --git a/packages/frontend/src/pages/gallery/post.vue b/packages/frontend/src/pages/gallery/post.vue
index b1dc872ff9..30f63cf3aa 100644
--- a/packages/frontend/src/pages/gallery/post.vue
+++ b/packages/frontend/src/pages/gallery/post.vue
@@ -10,7 +10,7 @@
 							<img :src="file.url"/>
 						</div>
 					</div>
-					<div class="body _block">
+					<div class="body">
 						<div class="title">{{ post.title }}</div>
 						<div class="description"><Mfm :text="post.description"/></div>
 						<div class="info">
diff --git a/packages/frontend/src/pages/instance-info.vue b/packages/frontend/src/pages/instance-info.vue
index 55771b0e30..e0f0d855eb 100644
--- a/packages/frontend/src/pages/instance-info.vue
+++ b/packages/frontend/src/pages/instance-info.vue
@@ -2,23 +2,25 @@
 <MkStickyContainer>
 	<template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template>
 	<MkSpacer v-if="instance" :content-max="600" :margin-min="16" :margin-max="32">
-		<div v-if="tab === 'overview'" class="_formRoot">
+		<div v-if="tab === 'overview'" class="_gaps_m">
 			<div class="fnfelxur">
 				<img :src="faviconUrl" alt="" class="icon"/>
 				<span class="name">{{ instance.name || `(${i18n.ts.unknown})` }}</span>
 			</div>
-			<MkKeyValue :copy="host" oneline style="margin: 1em 0;">
-				<template #key>Host</template>
-				<template #value><span class="_monospace"><MkLink :url="`https://${host}`">{{ host }}</MkLink></span></template>
-			</MkKeyValue>
-			<MkKeyValue oneline style="margin: 1em 0;">
-				<template #key>{{ i18n.ts.software }}</template>
-				<template #value><span class="_monospace">{{ instance.softwareName || `(${i18n.ts.unknown})` }} / {{ instance.softwareVersion || `(${i18n.ts.unknown})` }}</span></template>
-			</MkKeyValue>
-			<MkKeyValue oneline style="margin: 1em 0;">
-				<template #key>{{ i18n.ts.administrator }}</template>
-				<template #value>{{ instance.maintainerName || `(${i18n.ts.unknown})` }} ({{ instance.maintainerEmail || `(${i18n.ts.unknown})` }})</template>
-			</MkKeyValue>
+			<div style="display: flex; flex-direction: column; gap: 1em;">
+				<MkKeyValue :copy="host" oneline>
+					<template #key>Host</template>
+					<template #value><span class="_monospace"><MkLink :url="`https://${host}`">{{ host }}</MkLink></span></template>
+				</MkKeyValue>
+				<MkKeyValue oneline>
+					<template #key>{{ i18n.ts.software }}</template>
+					<template #value><span class="_monospace">{{ instance.softwareName || `(${i18n.ts.unknown})` }} / {{ instance.softwareVersion || `(${i18n.ts.unknown})` }}</span></template>
+				</MkKeyValue>
+				<MkKeyValue oneline>
+					<template #key>{{ i18n.ts.administrator }}</template>
+					<template #value>{{ instance.maintainerName || `(${i18n.ts.unknown})` }} ({{ instance.maintainerEmail || `(${i18n.ts.unknown})` }})</template>
+				</MkKeyValue>
+			</div>
 			<MkKeyValue>
 				<template #key>{{ i18n.ts.description }}</template>
 				<template #value>{{ instance.description }}</template>
@@ -26,9 +28,11 @@
 
 			<FormSection v-if="iAmModerator">
 				<template #label>Moderation</template>
-				<FormSwitch v-model="suspended" class="_formBlock" @update:model-value="toggleSuspend">{{ i18n.ts.stopActivityDelivery }}</FormSwitch>
-				<FormSwitch v-model="isBlocked" class="_formBlock" @update:model-value="toggleBlock">{{ i18n.ts.blockThisInstance }}</FormSwitch>
-				<MkButton @click="refreshMetadata"><i class="ti ti-refresh"></i> Refresh metadata</MkButton>
+				<div class="_gaps_s">
+					<FormSwitch v-model="suspended" @update:model-value="toggleSuspend">{{ i18n.ts.stopActivityDelivery }}</FormSwitch>
+					<FormSwitch v-model="isBlocked" @update:model-value="toggleBlock">{{ i18n.ts.blockThisInstance }}</FormSwitch>
+					<MkButton @click="refreshMetadata"><i class="ti ti-refresh"></i> Refresh metadata</MkButton>
+				</div>
 			</FormSection>
 
 			<FormSection>
@@ -66,7 +70,7 @@
 				<FormLink :to="`https://${host}/manifest.json`" external style="margin-bottom: 8px;">manifest.json</FormLink>
 			</FormSection>
 		</div>
-		<div v-else-if="tab === 'chart'" class="_formRoot">
+		<div v-else-if="tab === 'chart'" class="_gaps_m">
 			<div class="cmhjzshl">
 				<div class="selects">
 					<MkSelect v-model="chartSrc" style="margin: 0 10px 0 0; flex: 1;">
@@ -91,14 +95,14 @@
 				</div>
 			</div>
 		</div>
-		<div v-else-if="tab === 'users'" class="_formRoot">
+		<div v-else-if="tab === 'users'" class="_gaps_m">
 			<MkPagination v-slot="{items}" :pagination="usersPagination" style="display: grid; grid-template-columns: repeat(auto-fill,minmax(270px,1fr)); grid-gap: 12px;">
 				<MkA v-for="user in items" :key="user.id" v-tooltip.mfm="`Last posted: ${dateString(user.updatedAt)}`" class="user" :to="`/user-info/${user.id}`">
 					<MkUserCardMini :user="user"/>
 				</MkA>
 			</MkPagination>
 		</div>
-		<div v-else-if="tab === 'raw'" class="_formRoot">
+		<div v-else-if="tab === 'raw'" class="_gaps_m">
 			<MkObjectView tall :value="instance">
 			</MkObjectView>
 		</div>
diff --git a/packages/frontend/src/pages/messaging/index.vue b/packages/frontend/src/pages/messaging/index.vue
index 3fb5047a04..e751754503 100644
--- a/packages/frontend/src/pages/messaging/index.vue
+++ b/packages/frontend/src/pages/messaging/index.vue
@@ -10,7 +10,7 @@
 					v-for="(message, i) in messages"
 					:key="message.id"
 					v-anim="i"
-					class="message _block"
+					class="message"
 					:class="{ isMe: isMe(message), isRead: message.groupId ? message.reads.includes($i.id) : message.isRead }"
 					:to="message.groupId ? `/my/messaging/group/${message.groupId}` : `/my/messaging/${getAcct(isMe(message) ? message.recipient : message.user)}`"
 					:data-index="i"
diff --git a/packages/frontend/src/pages/messaging/messaging-room.form.vue b/packages/frontend/src/pages/messaging/messaging-room.form.vue
index 2c54c6f71f..e880129033 100644
--- a/packages/frontend/src/pages/messaging/messaging-room.form.vue
+++ b/packages/frontend/src/pages/messaging/messaging-room.form.vue
@@ -1,6 +1,6 @@
 <template>
 <div
-	class="pemppnzi _block"
+	class="pemppnzi"
 	@dragover.stop="onDragover"
 	@drop.stop="onDrop"
 >
@@ -40,6 +40,7 @@ import { defaultStore } from '@/store';
 import { i18n } from '@/i18n';
 //import { Autocomplete } from '@/scripts/autocomplete';
 import { uploadFile } from '@/scripts/upload';
+import { miLocalStorage } from '@/local-storage';
 
 const props = defineProps<{
 	user?: Misskey.entities.UserDetailed | null;
@@ -188,7 +189,7 @@ function clear() {
 }
 
 function saveDraft() {
-	const drafts = JSON.parse(localStorage.getItem('message_drafts') || '{}');
+	const drafts = JSON.parse(miLocalStorage.getItem('message_drafts') || '{}');
 
 	drafts[draftKey] = {
 		updatedAt: new Date(),
@@ -199,15 +200,15 @@ function saveDraft() {
 		},
 	};
 
-	localStorage.setItem('message_drafts', JSON.stringify(drafts));
+	miLocalStorage.setItem('message_drafts', JSON.stringify(drafts));
 }
 
 function deleteDraft() {
-	const drafts = JSON.parse(localStorage.getItem('message_drafts') || '{}');
+	const drafts = JSON.parse(miLocalStorage.getItem('message_drafts') || '{}');
 
 	delete drafts[draftKey];
 
-	localStorage.setItem('message_drafts', JSON.stringify(drafts));
+	miLocalStorage.setItem('message_drafts', JSON.stringify(drafts));
 }
 
 async function insertEmoji(ev: MouseEvent) {
@@ -222,7 +223,7 @@ onMounted(() => {
 	//new Autocomplete(textEl, this, { model: 'text' });
 
 	// 書きかけの投稿を復元
-	const draft = JSON.parse(localStorage.getItem('message_drafts') || '{}')[draftKey];
+	const draft = JSON.parse(miLocalStorage.getItem('message_drafts') || '{}')[draftKey];
 	if (draft) {
 		text = draft.data.text;
 		file = draft.data.file;
diff --git a/packages/frontend/src/pages/messaging/messaging-room.vue b/packages/frontend/src/pages/messaging/messaging-room.vue
index f0a36fb8b1..fa08b1cb72 100644
--- a/packages/frontend/src/pages/messaging/messaging-room.vue
+++ b/packages/frontend/src/pages/messaging/messaging-room.vue
@@ -1,11 +1,11 @@
 <template>
 <div
 	ref="rootEl"
-	class="_section"
+	class=""
 	@dragover.prevent.stop="onDragover"
 	@drop.prevent.stop="onDrop"
 >
-	<div class="_content mk-messaging-room">
+	<div class="mk-messaging-room">
 		<div class="body">
 			<MkPagination v-if="pagination" ref="pagingComponent" :key="userAcct || groupId" :pagination="pagination">
 				<template #empty>
diff --git a/packages/frontend/src/pages/mfm-cheat-sheet.vue b/packages/frontend/src/pages/mfm-cheat-sheet.vue
index 7c85dfb7ad..2683affc42 100644
--- a/packages/frontend/src/pages/mfm-cheat-sheet.vue
+++ b/packages/frontend/src/pages/mfm-cheat-sheet.vue
@@ -4,7 +4,7 @@
 	<MkSpacer :content-max="800">
 		<div class="mwysmxbg">
 			<div>{{ i18n.ts._mfm.intro }}</div>
-			<div class="section _block">
+			<div class="section">
 				<div class="title">{{ i18n.ts._mfm.mention }}</div>
 				<div class="content">
 					<p>{{ i18n.ts._mfm.mentionDescription }}</p>
@@ -14,7 +14,7 @@
 					</div>
 				</div>
 			</div>
-			<div class="section _block">
+			<div class="section">
 				<div class="title">{{ i18n.ts._mfm.hashtag }}</div>
 				<div class="content">
 					<p>{{ i18n.ts._mfm.hashtagDescription }}</p>
@@ -24,7 +24,7 @@
 					</div>
 				</div>
 			</div>
-			<div class="section _block">
+			<div class="section">
 				<div class="title">{{ i18n.ts._mfm.url }}</div>
 				<div class="content">
 					<p>{{ i18n.ts._mfm.urlDescription }}</p>
@@ -34,7 +34,7 @@
 					</div>
 				</div>
 			</div>
-			<div class="section _block">
+			<div class="section">
 				<div class="title">{{ i18n.ts._mfm.link }}</div>
 				<div class="content">
 					<p>{{ i18n.ts._mfm.linkDescription }}</p>
@@ -44,7 +44,7 @@
 					</div>
 				</div>
 			</div>
-			<div class="section _block">
+			<div class="section">
 				<div class="title">{{ i18n.ts._mfm.emoji }}</div>
 				<div class="content">
 					<p>{{ i18n.ts._mfm.emojiDescription }}</p>
@@ -54,7 +54,7 @@
 					</div>
 				</div>
 			</div>
-			<div class="section _block">
+			<div class="section">
 				<div class="title">{{ i18n.ts._mfm.bold }}</div>
 				<div class="content">
 					<p>{{ i18n.ts._mfm.boldDescription }}</p>
@@ -64,7 +64,7 @@
 					</div>
 				</div>
 			</div>
-			<div class="section _block">
+			<div class="section">
 				<div class="title">{{ i18n.ts._mfm.small }}</div>
 				<div class="content">
 					<p>{{ i18n.ts._mfm.smallDescription }}</p>
@@ -74,7 +74,7 @@
 					</div>
 				</div>
 			</div>
-			<div class="section _block">
+			<div class="section">
 				<div class="title">{{ i18n.ts._mfm.quote }}</div>
 				<div class="content">
 					<p>{{ i18n.ts._mfm.quoteDescription }}</p>
@@ -84,7 +84,7 @@
 					</div>
 				</div>
 			</div>
-			<div class="section _block">
+			<div class="section">
 				<div class="title">{{ i18n.ts._mfm.center }}</div>
 				<div class="content">
 					<p>{{ i18n.ts._mfm.centerDescription }}</p>
@@ -94,7 +94,7 @@
 					</div>
 				</div>
 			</div>
-			<div class="section _block">
+			<div class="section">
 				<div class="title">{{ i18n.ts._mfm.inlineCode }}</div>
 				<div class="content">
 					<p>{{ i18n.ts._mfm.inlineCodeDescription }}</p>
@@ -104,7 +104,7 @@
 					</div>
 				</div>
 			</div>
-			<div class="section _block">
+			<div class="section">
 				<div class="title">{{ i18n.ts._mfm.blockCode }}</div>
 				<div class="content">
 					<p>{{ i18n.ts._mfm.blockCodeDescription }}</p>
@@ -114,7 +114,7 @@
 					</div>
 				</div>
 			</div>
-			<div class="section _block">
+			<div class="section">
 				<div class="title">{{ i18n.ts._mfm.inlineMath }}</div>
 				<div class="content">
 					<p>{{ i18n.ts._mfm.inlineMathDescription }}</p>
@@ -125,7 +125,7 @@
 				</div>
 			</div>
 			<!-- deprecated
-		<div class="section _block">
+		<div class="section">
 			<div class="title">{{ i18n.ts._mfm.search }}</div>
 			<div class="content">
 				<p>{{ i18n.ts._mfm.searchDescription }}</p>
@@ -136,7 +136,7 @@
 			</div>
 		</div>
 		-->
-			<div class="section _block">
+			<div class="section">
 				<div class="title">{{ i18n.ts._mfm.flip }}</div>
 				<div class="content">
 					<p>{{ i18n.ts._mfm.flipDescription }}</p>
@@ -146,7 +146,7 @@
 					</div>
 				</div>
 			</div>
-			<div class="section _block">
+			<div class="section">
 				<div class="title">{{ i18n.ts._mfm.font }}</div>
 				<div class="content">
 					<p>{{ i18n.ts._mfm.fontDescription }}</p>
@@ -156,7 +156,7 @@
 					</div>
 				</div>
 			</div>
-			<div class="section _block">
+			<div class="section">
 				<div class="title">{{ i18n.ts._mfm.x2 }}</div>
 				<div class="content">
 					<p>{{ i18n.ts._mfm.x2Description }}</p>
@@ -166,7 +166,7 @@
 					</div>
 				</div>
 			</div>
-			<div class="section _block">
+			<div class="section">
 				<div class="title">{{ i18n.ts._mfm.x3 }}</div>
 				<div class="content">
 					<p>{{ i18n.ts._mfm.x3Description }}</p>
@@ -176,7 +176,7 @@
 					</div>
 				</div>
 			</div>
-			<div class="section _block">
+			<div class="section">
 				<div class="title">{{ i18n.ts._mfm.x4 }}</div>
 				<div class="content">
 					<p>{{ i18n.ts._mfm.x4Description }}</p>
@@ -186,7 +186,7 @@
 					</div>
 				</div>
 			</div>
-			<div class="section _block">
+			<div class="section">
 				<div class="title">{{ i18n.ts._mfm.blur }}</div>
 				<div class="content">
 					<p>{{ i18n.ts._mfm.blurDescription }}</p>
@@ -196,7 +196,7 @@
 					</div>
 				</div>
 			</div>
-			<div class="section _block">
+			<div class="section">
 				<div class="title">{{ i18n.ts._mfm.jelly }}</div>
 				<div class="content">
 					<p>{{ i18n.ts._mfm.jellyDescription }}</p>
@@ -206,7 +206,7 @@
 					</div>
 				</div>
 			</div>
-			<div class="section _block">
+			<div class="section">
 				<div class="title">{{ i18n.ts._mfm.tada }}</div>
 				<div class="content">
 					<p>{{ i18n.ts._mfm.tadaDescription }}</p>
@@ -216,7 +216,7 @@
 					</div>
 				</div>
 			</div>
-			<div class="section _block">
+			<div class="section">
 				<div class="title">{{ i18n.ts._mfm.jump }}</div>
 				<div class="content">
 					<p>{{ i18n.ts._mfm.jumpDescription }}</p>
@@ -226,7 +226,7 @@
 					</div>
 				</div>
 			</div>
-			<div class="section _block">
+			<div class="section">
 				<div class="title">{{ i18n.ts._mfm.bounce }}</div>
 				<div class="content">
 					<p>{{ i18n.ts._mfm.bounceDescription }}</p>
@@ -236,7 +236,7 @@
 					</div>
 				</div>
 			</div>
-			<div class="section _block">
+			<div class="section">
 				<div class="title">{{ i18n.ts._mfm.spin }}</div>
 				<div class="content">
 					<p>{{ i18n.ts._mfm.spinDescription }}</p>
@@ -246,7 +246,7 @@
 					</div>
 				</div>
 			</div>
-			<div class="section _block">
+			<div class="section">
 				<div class="title">{{ i18n.ts._mfm.shake }}</div>
 				<div class="content">
 					<p>{{ i18n.ts._mfm.shakeDescription }}</p>
@@ -256,7 +256,7 @@
 					</div>
 				</div>
 			</div>
-			<div class="section _block">
+			<div class="section">
 				<div class="title">{{ i18n.ts._mfm.twitch }}</div>
 				<div class="content">
 					<p>{{ i18n.ts._mfm.twitchDescription }}</p>
@@ -266,7 +266,7 @@
 					</div>
 				</div>
 			</div>
-			<div class="section _block">
+			<div class="section">
 				<div class="title">{{ i18n.ts._mfm.rainbow }}</div>
 				<div class="content">
 					<p>{{ i18n.ts._mfm.rainbowDescription }}</p>
@@ -276,7 +276,7 @@
 					</div>
 				</div>
 			</div>
-			<div class="section _block">
+			<div class="section">
 				<div class="title">{{ i18n.ts._mfm.sparkle }}</div>
 				<div class="content">
 					<p>{{ i18n.ts._mfm.sparkleDescription }}</p>
@@ -286,7 +286,7 @@
 					</div>
 				</div>
 			</div>
-			<div class="section _block">
+			<div class="section">
 				<div class="title">{{ i18n.ts._mfm.rotate }}</div>
 				<div class="content">
 					<p>{{ i18n.ts._mfm.rotateDescription }}</p>
@@ -296,7 +296,7 @@
 					</div>
 				</div>
 			</div>
-			<div class="section _block">
+			<div class="section">
 				<div class="title">{{ i18n.ts._mfm.plain }}</div>
 				<div class="content">
 					<p>{{ i18n.ts._mfm.plainDescription }}</p>
diff --git a/packages/frontend/src/pages/miauth.vue b/packages/frontend/src/pages/miauth.vue
index 5de072cbfa..a01c7c5c4b 100644
--- a/packages/frontend/src/pages/miauth.vue
+++ b/packages/frontend/src/pages/miauth.vue
@@ -1,32 +1,32 @@
 <template>
 <MkSpacer :content-max="800">
 	<div v-if="$i">
-		<div v-if="state == 'waiting'" class="waiting _section">
-			<div class="_content">
+		<div v-if="state == 'waiting'" class="waiting">
+			<div class="">
 				<MkLoading/>
 			</div>
 		</div>
-		<div v-if="state == 'denied'" class="denied _section">
-			<div class="_content">
+		<div v-if="state == 'denied'" class="denied">
+			<div class="">
 				<p>{{ i18n.ts._auth.denied }}</p>
 			</div>
 		</div>
-		<div v-else-if="state == 'accepted'" class="accepted _section">
-			<div class="_content">
+		<div v-else-if="state == 'accepted'" class="accepted">
+			<div class="">
 				<p v-if="callback">{{ i18n.ts._auth.callback }}<MkEllipsis/></p>
 				<p v-else>{{ i18n.ts._auth.pleaseGoBack }}</p>
 			</div>
 		</div>
-		<div v-else class="_section">
-			<div v-if="name" class="_title">{{ $t('_auth.shareAccess', { name: name }) }}</div>
-			<div v-else class="_title">{{ i18n.ts._auth.shareAccessAsk }}</div>
-			<div class="_content">
+		<div v-else class="">
+			<div v-if="name" class="">{{ $t('_auth.shareAccess', { name: name }) }}</div>
+			<div v-else class="">{{ i18n.ts._auth.shareAccessAsk }}</div>
+			<div class="">
 				<p>{{ i18n.ts._auth.permissionAsk }}</p>
 				<ul>
 					<li v-for="p in _permissions" :key="p">{{ $t(`_permissions.${p}`) }}</li>
 				</ul>
 			</div>
-			<div class="_footer">
+			<div class="">
 				<MkButton inline @click="deny">{{ i18n.ts.cancel }}</MkButton>
 				<MkButton inline primary @click="accept">{{ i18n.ts.accept }}</MkButton>
 			</div>
@@ -44,7 +44,6 @@ import MkSignin from '@/components/MkSignin.vue';
 import MkButton from '@/components/MkButton.vue';
 import * as os from '@/os';
 import { $i, login } from '@/account';
-import { appendQuery, query } from '@/scripts/url';
 import { i18n } from '@/i18n';
 
 const props = defineProps<{
@@ -70,9 +69,9 @@ async function accept(): Promise<void> {
 
 	state = 'accepted';
 	if (props.callback) {
-		location.href = appendQuery(props.callback, query({
-			session: props.session,
-		}));
+		const cbUrl = new URL(props.callback);
+		cbUrl.searchParams.set('session', props.session);
+		location.href = cbUrl.href;
 	}
 }
 
diff --git a/packages/frontend/src/pages/my-antennas/editor.vue b/packages/frontend/src/pages/my-antennas/editor.vue
index a409a734b5..6e7405735e 100644
--- a/packages/frontend/src/pages/my-antennas/editor.vue
+++ b/packages/frontend/src/pages/my-antennas/editor.vue
@@ -1,47 +1,49 @@
 <template>
-<div class="shaynizk">
-	<div class="form">
-		<MkInput v-model="name" class="_formBlock">
-			<template #label>{{ i18n.ts.name }}</template>
-		</MkInput>
-		<MkSelect v-model="src" class="_formBlock">
-			<template #label>{{ i18n.ts.antennaSource }}</template>
-			<option value="all">{{ i18n.ts._antennaSources.all }}</option>
-			<!--<option value="home">{{ i18n.ts._antennaSources.homeTimeline }}</option>-->
-			<option value="users">{{ i18n.ts._antennaSources.users }}</option>
-			<!--<option value="list">{{ i18n.ts._antennaSources.userList }}</option>-->
-			<!--<option value="group">{{ i18n.ts._antennaSources.userGroup }}</option>-->
-		</MkSelect>
-		<MkSelect v-if="src === 'list'" v-model="userListId" class="_formBlock">
-			<template #label>{{ i18n.ts.userList }}</template>
-			<option v-for="list in userLists" :key="list.id" :value="list.id">{{ list.name }}</option>
-		</MkSelect>
-		<MkSelect v-else-if="src === 'group'" v-model="userGroupId" class="_formBlock">
-			<template #label>{{ i18n.ts.userGroup }}</template>
-			<option v-for="group in userGroups" :key="group.id" :value="group.id">{{ group.name }}</option>
-		</MkSelect>
-		<MkTextarea v-else-if="src === 'users'" v-model="users" class="_formBlock">
-			<template #label>{{ i18n.ts.users }}</template>
-			<template #caption>{{ i18n.ts.antennaUsersDescription }} <button class="_textButton" @click="addUser">{{ i18n.ts.addUser }}</button></template>
-		</MkTextarea>
-		<MkSwitch v-model="withReplies" class="_formBlock">{{ i18n.ts.withReplies }}</MkSwitch>
-		<MkTextarea v-model="keywords" class="_formBlock">
-			<template #label>{{ i18n.ts.antennaKeywords }}</template>
-			<template #caption>{{ i18n.ts.antennaKeywordsDescription }}</template>
-		</MkTextarea>
-		<MkTextarea v-model="excludeKeywords" class="_formBlock">
-			<template #label>{{ i18n.ts.antennaExcludeKeywords }}</template>
-			<template #caption>{{ i18n.ts.antennaKeywordsDescription }}</template>
-		</MkTextarea>
-		<MkSwitch v-model="caseSensitive" class="_formBlock">{{ i18n.ts.caseSensitive }}</MkSwitch>
-		<MkSwitch v-model="withFile" class="_formBlock">{{ i18n.ts.withFileAntenna }}</MkSwitch>
-		<MkSwitch v-model="notify" class="_formBlock">{{ i18n.ts.notifyAntenna }}</MkSwitch>
+<MkSpacer :content-max="700">
+	<div class="shaynizk">
+		<div class="_gaps_m">
+			<MkInput v-model="name">
+				<template #label>{{ i18n.ts.name }}</template>
+			</MkInput>
+			<MkSelect v-model="src">
+				<template #label>{{ i18n.ts.antennaSource }}</template>
+				<option value="all">{{ i18n.ts._antennaSources.all }}</option>
+				<!--<option value="home">{{ i18n.ts._antennaSources.homeTimeline }}</option>-->
+				<option value="users">{{ i18n.ts._antennaSources.users }}</option>
+				<!--<option value="list">{{ i18n.ts._antennaSources.userList }}</option>-->
+				<!--<option value="group">{{ i18n.ts._antennaSources.userGroup }}</option>-->
+			</MkSelect>
+			<MkSelect v-if="src === 'list'" v-model="userListId">
+				<template #label>{{ i18n.ts.userList }}</template>
+				<option v-for="list in userLists" :key="list.id" :value="list.id">{{ list.name }}</option>
+			</MkSelect>
+			<MkSelect v-else-if="src === 'group'" v-model="userGroupId">
+				<template #label>{{ i18n.ts.userGroup }}</template>
+				<option v-for="group in userGroups" :key="group.id" :value="group.id">{{ group.name }}</option>
+			</MkSelect>
+			<MkTextarea v-else-if="src === 'users'" v-model="users">
+				<template #label>{{ i18n.ts.users }}</template>
+				<template #caption>{{ i18n.ts.antennaUsersDescription }} <button class="_textButton" @click="addUser">{{ i18n.ts.addUser }}</button></template>
+			</MkTextarea>
+			<MkSwitch v-model="withReplies">{{ i18n.ts.withReplies }}</MkSwitch>
+			<MkTextarea v-model="keywords">
+				<template #label>{{ i18n.ts.antennaKeywords }}</template>
+				<template #caption>{{ i18n.ts.antennaKeywordsDescription }}</template>
+			</MkTextarea>
+			<MkTextarea v-model="excludeKeywords">
+				<template #label>{{ i18n.ts.antennaExcludeKeywords }}</template>
+				<template #caption>{{ i18n.ts.antennaKeywordsDescription }}</template>
+			</MkTextarea>
+			<MkSwitch v-model="caseSensitive">{{ i18n.ts.caseSensitive }}</MkSwitch>
+			<MkSwitch v-model="withFile">{{ i18n.ts.withFileAntenna }}</MkSwitch>
+			<MkSwitch v-model="notify">{{ i18n.ts.notifyAntenna }}</MkSwitch>
+		</div>
+		<div class="actions">
+			<MkButton inline primary @click="saveAntenna()"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton>
+			<MkButton v-if="antenna.id != null" inline danger @click="deleteAntenna()"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton>
+		</div>
 	</div>
-	<div class="actions">
-		<MkButton inline primary @click="saveAntenna()"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton>
-		<MkButton v-if="antenna.id != null" inline danger @click="deleteAntenna()"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton>
-	</div>
-</div>
+</MkSpacer>
 </template>
 
 <script lang="ts" setup>
@@ -143,12 +145,9 @@ function addUser() {
 
 <style lang="scss" scoped>
 .shaynizk {
-	> .form {
-		padding: 32px;
-	}
-
 	> .actions {
-		padding: 24px 32px;
+		margin-top: 16px;
+		padding: 24px 0;
 		border-top: solid 0.5px var(--divider);
 	}
 }
diff --git a/packages/frontend/src/pages/my-clips/index.vue b/packages/frontend/src/pages/my-clips/index.vue
index 6c0508134f..a79601f32f 100644
--- a/packages/frontend/src/pages/my-clips/index.vue
+++ b/packages/frontend/src/pages/my-clips/index.vue
@@ -6,7 +6,7 @@
 			<MkButton primary class="add" @click="create"><i class="ti ti-plus"></i> {{ i18n.ts.add }}</MkButton>
 
 			<MkPagination v-slot="{items}" ref="pagingComponent" :pagination="pagination" class="list">
-				<MkA v-for="item in items" :key="item.id" :to="`/clips/${item.id}`" class="item _panel _gap">
+				<MkA v-for="item in items" :key="item.id" :to="`/clips/${item.id}`" class="item _panel _margin">
 					<b>{{ item.name }}</b>
 					<div v-if="item.description" class="description">{{ item.description }}</div>
 				</MkA>
diff --git a/packages/frontend/src/pages/my-lists/index.vue b/packages/frontend/src/pages/my-lists/index.vue
index 510e0173df..8a96b54881 100644
--- a/packages/frontend/src/pages/my-lists/index.vue
+++ b/packages/frontend/src/pages/my-lists/index.vue
@@ -5,7 +5,7 @@
 		<div class="qkcjvfiv">
 			<MkButton primary class="add" @click="create"><i class="ti ti-plus"></i> {{ i18n.ts.createList }}</MkButton>
 
-			<MkPagination v-slot="{items}" ref="pagingComponent" :pagination="pagination" class="lists _content">
+			<MkPagination v-slot="{items}" ref="pagingComponent" :pagination="pagination" class="lists">
 				<MkA v-for="list in items" :key="list.id" class="list _panel" :to="`/my/lists/${ list.id }`">
 					<div class="name">{{ list.name }}</div>
 					<MkAvatars :user-ids="list.userIds"/>
diff --git a/packages/frontend/src/pages/my-lists/list.vue b/packages/frontend/src/pages/my-lists/list.vue
index 714a8d4458..2c624d68f4 100644
--- a/packages/frontend/src/pages/my-lists/list.vue
+++ b/packages/frontend/src/pages/my-lists/list.vue
@@ -4,8 +4,8 @@
 	<MkSpacer :content-max="700">
 		<div class="mk-list-page">
 			<Transition :name="$store.state.animation ? '_transition_zoom' : ''" mode="out-in">
-				<div v-if="list" class="_section">
-					<div class="_content">
+				<div v-if="list" class="">
+					<div class="">
 						<MkButton inline @click="addUser()">{{ i18n.ts.addUser }}</MkButton>
 						<MkButton inline @click="renameList()">{{ i18n.ts.rename }}</MkButton>
 						<MkButton inline @click="deleteList()">{{ i18n.ts.delete }}</MkButton>
@@ -14,9 +14,9 @@
 			</Transition>
 
 			<Transition :name="$store.state.animation ? '_transition_zoom' : ''" mode="out-in">
-				<div v-if="list" class="_section members _gap">
-					<div class="_title">{{ i18n.ts.members }}</div>
-					<div class="_content">
+				<div v-if="list" class="members _margin">
+					<div class="">{{ i18n.ts.members }}</div>
+					<div class="">
 						<div class="users">
 							<div v-for="user in users" :key="user.id" class="user _panel">
 								<MkAvatar :user="user" class="avatar" :show-indicator="true"/>
diff --git a/packages/frontend/src/pages/note.vue b/packages/frontend/src/pages/note.vue
index 3019b6eb4f..7fd74d2aee 100644
--- a/packages/frontend/src/pages/note.vue
+++ b/packages/frontend/src/pages/note.vue
@@ -5,19 +5,19 @@
 		<div class="fcuexfpr">
 			<Transition :name="$store.state.animation ? 'fade' : ''" mode="out-in">
 				<div v-if="note" class="note">
-					<div v-if="showNext" class="_gap">
-						<XNotes class="_content" :pagination="nextPagination" :no-gap="true"/>
+					<div v-if="showNext" class="_margin">
+						<XNotes class="" :pagination="nextPagination" :no-gap="true"/>
 					</div>
 
-					<div class="main _gap">
+					<div class="main _margin">
 						<MkButton v-if="!showNext && hasNext" class="load next" @click="showNext = true"><i class="ti ti-chevron-up"></i></MkButton>
-						<div class="note _gap">
+						<div class="note _margin">
 							<MkRemoteCaution v-if="note.user.host != null" :href="note.url ?? note.uri"/>
 							<XNoteDetailed :key="note.id" v-model:note="note" class="note"/>
 						</div>
-						<div v-if="clips && clips.length > 0" class="_content clips _gap">
+						<div v-if="clips && clips.length > 0" class="clips _margin">
 							<div class="title">{{ i18n.ts.clip }}</div>
-							<MkA v-for="item in clips" :key="item.id" :to="`/clips/${item.id}`" class="item _panel _gap">
+							<MkA v-for="item in clips" :key="item.id" :to="`/clips/${item.id}`" class="item _panel _margin">
 								<b>{{ item.name }}</b>
 								<div v-if="item.description" class="description">{{ item.description }}</div>
 								<div class="user">
@@ -28,8 +28,8 @@
 						<MkButton v-if="!showPrev && hasPrev" class="load prev" @click="showPrev = true"><i class="ti ti-chevron-down"></i></MkButton>
 					</div>
 
-					<div v-if="showPrev" class="_gap">
-						<XNotes class="_content" :pagination="prevPagination" :no-gap="true"/>
+					<div v-if="showPrev" class="_margin">
+						<XNotes class="" :pagination="prevPagination" :no-gap="true"/>
 					</div>
 				</div>
 				<MkError v-else-if="error" @retry="fetch()"/>
diff --git a/packages/frontend/src/pages/page-editor/page-editor.vue b/packages/frontend/src/pages/page-editor/page-editor.vue
index 968aa12de2..a01daf9a27 100644
--- a/packages/frontend/src/pages/page-editor/page-editor.vue
+++ b/packages/frontend/src/pages/page-editor/page-editor.vue
@@ -10,29 +10,29 @@
 		</div>
 
 		<div v-if="tab === 'settings'">
-			<div class="_formRoot">
-				<MkInput v-model="title" class="_formBlock">
+			<div class="_gaps_m">
+				<MkInput v-model="title">
 					<template #label>{{ $ts._pages.title }}</template>
 				</MkInput>
 
-				<MkInput v-model="summary" class="_formBlock">
+				<MkInput v-model="summary">
 					<template #label>{{ $ts._pages.summary }}</template>
 				</MkInput>
 
-				<MkInput v-model="name" class="_formBlock">
+				<MkInput v-model="name">
 					<template #prefix>{{ url }}/@{{ author.username }}/pages/</template>
 					<template #label>{{ $ts._pages.url }}</template>
 				</MkInput>
 
-				<MkSwitch v-model="alignCenter" class="_formBlock">{{ $ts._pages.alignCenter }}</MkSwitch>
+				<MkSwitch v-model="alignCenter">{{ $ts._pages.alignCenter }}</MkSwitch>
 
-				<MkSelect v-model="font" class="_formBlock">
+				<MkSelect v-model="font">
 					<template #label>{{ $ts._pages.font }}</template>
 					<option value="serif">{{ $ts._pages.fontSerif }}</option>
 					<option value="sans-serif">{{ $ts._pages.fontSansSerif }}</option>
 				</MkSelect>
 
-				<MkSwitch v-model="hideTitleWhenPinned" class="_formBlock">{{ $ts._pages.hideTitleWhenPinned }}</MkSwitch>
+				<MkSwitch v-model="hideTitleWhenPinned">{{ $ts._pages.hideTitleWhenPinned }}</MkSwitch>
 
 				<div class="eyeCatch">
 					<MkButton v-if="eyeCatchingImageId == null && !readonly" @click="setEyeCatchingImage"><i class="ti ti-plus"></i> {{ $ts._pages.eyeCatchingImageSet }}</MkButton>
diff --git a/packages/frontend/src/pages/page.vue b/packages/frontend/src/pages/page.vue
index e01dae2cd9..7f0871a5fb 100644
--- a/packages/frontend/src/pages/page.vue
+++ b/packages/frontend/src/pages/page.vue
@@ -4,7 +4,7 @@
 	<MkSpacer :content-max="700">
 		<Transition :name="$store.state.animation ? 'fade' : ''" mode="out-in">
 			<div v-if="page" :key="page.id" class="xcukqgmh">
-				<div class="_block main">
+				<div class="main">
 					<!--
 				<div class="header">
 					<h1>{{ page.title }}</h1>
@@ -18,8 +18,8 @@
 					</div>
 					<div class="actions">
 						<div class="like">
-							<MkButton v-if="page.isLiked" v-tooltip="i18n.ts._pages.unlike" class="button" 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" @click="like()"><i class="ti ti-heart"></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" as-like 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" as-like @click="like()"><i class="ti ti-heart"></i><span v-if="page.likedCount > 0" class="count">{{ page.likedCount }}</span></MkButton>
 						</div>
 						<div class="other">
 							<button v-tooltip="i18n.ts.shareWithNote" v-click-anime class="_button" @click="shareWithNote"><i class="ti ti-repeat ti-fw"></i></button>
@@ -51,7 +51,7 @@
 				<MkContainer :max-height="300" :foldable="true" class="other">
 					<template #header><i class="ti ti-clock"></i> {{ i18n.ts.recentPosts }}</template>
 					<MkPagination v-slot="{items}" :pagination="otherPostsPagination">
-						<MkPagePreview v-for="page in items" :key="page.id" :page="page" class="_gap"/>
+						<MkPagePreview v-for="page in items" :key="page.id" :page="page" class="_margin"/>
 					</MkPagination>
 				</MkContainer>
 			</div>
@@ -207,20 +207,6 @@ definePageMetadata(computed(() => page ? {
 			padding: 16px 0 0 0;
 			border-top: solid 0.5px var(--divider);
 
-			> .like {
-				> .button {
-					--accent: rgb(241 97 132);
-					--X8: rgb(241 92 128);
-					--buttonBg: rgb(216 71 106 / 5%);
-					--buttonHoverBg: rgb(216 71 106 / 10%);
-					color: #ff002f;
-
-					::v-deep(.count) {
-						margin-left: 0.5em;
-					}
-				}
-			}
-
 			> .other {
 				margin-left: auto;
 
diff --git a/packages/frontend/src/pages/registry.keys.vue b/packages/frontend/src/pages/registry.keys.vue
index f179fbe957..2c2a1444c1 100644
--- a/packages/frontend/src/pages/registry.keys.vue
+++ b/packages/frontend/src/pages/registry.keys.vue
@@ -2,25 +2,27 @@
 <MkStickyContainer>
 	<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
 	<MkSpacer :content-max="600" :margin-min="16">
-		<FormSplit>
-			<MkKeyValue class="_formBlock">
-				<template #key>{{ i18n.ts._registry.domain }}</template>
-				<template #value>{{ i18n.ts.system }}</template>
-			</MkKeyValue>
-			<MkKeyValue class="_formBlock">
-				<template #key>{{ i18n.ts._registry.scope }}</template>
-				<template #value>{{ scope.join('/') }}</template>
-			</MkKeyValue>
-		</FormSplit>
-		
-		<MkButton primary @click="createKey">{{ i18n.ts._registry.createKey }}</MkButton>
+		<div class="_gaps_m">
+			<FormSplit>
+				<MkKeyValue>
+					<template #key>{{ i18n.ts._registry.domain }}</template>
+					<template #value>{{ i18n.ts.system }}</template>
+				</MkKeyValue>
+				<MkKeyValue>
+					<template #key>{{ i18n.ts._registry.scope }}</template>
+					<template #value>{{ scope.join('/') }}</template>
+				</MkKeyValue>
+			</FormSplit>
+			
+			<MkButton primary @click="createKey">{{ i18n.ts._registry.createKey }}</MkButton>
 
-		<FormSection v-if="keys">
-			<template #label>{{ i18n.ts.keys }}</template>
-			<div class="_formLinks">
-				<FormLink v-for="key in keys" :to="`/registry/value/system/${scope.join('/')}/${key[0]}`" class="_monospace">{{ key[0] }}<template #suffix>{{ key[1].toUpperCase() }}</template></FormLink>
-			</div>
-		</FormSection>
+			<FormSection v-if="keys">
+				<template #label>{{ i18n.ts.keys }}</template>
+				<div class="_formLinks">
+					<FormLink v-for="key in keys" :to="`/registry/value/system/${scope.join('/')}/${key[0]}`" class="_monospace">{{ key[0] }}<template #suffix>{{ key[1].toUpperCase() }}</template></FormLink>
+				</div>
+			</FormSection>
+		</div>
 	</MkSpacer>
 </MkStickyContainer>
 </template>
diff --git a/packages/frontend/src/pages/registry.value.vue b/packages/frontend/src/pages/registry.value.vue
index 378420b1ba..5c747564f6 100644
--- a/packages/frontend/src/pages/registry.value.vue
+++ b/packages/frontend/src/pages/registry.value.vue
@@ -2,37 +2,39 @@
 <MkStickyContainer>
 	<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
 	<MkSpacer :content-max="600" :margin-min="16">
-		<FormInfo warn>{{ i18n.ts.editTheseSettingsMayBreakAccount }}</FormInfo>
+		<div class="_gaps_m">
+			<FormInfo warn>{{ i18n.ts.editTheseSettingsMayBreakAccount }}</FormInfo>
 
-		<template v-if="value">
-			<FormSplit>
-				<MkKeyValue class="_formBlock">
-					<template #key>{{ i18n.ts._registry.domain }}</template>
-					<template #value>{{ i18n.ts.system }}</template>
+			<template v-if="value">
+				<FormSplit>
+					<MkKeyValue>
+						<template #key>{{ i18n.ts._registry.domain }}</template>
+						<template #value>{{ i18n.ts.system }}</template>
+					</MkKeyValue>
+					<MkKeyValue>
+						<template #key>{{ i18n.ts._registry.scope }}</template>
+						<template #value>{{ scope.join('/') }}</template>
+					</MkKeyValue>
+					<MkKeyValue>
+						<template #key>{{ i18n.ts._registry.key }}</template>
+						<template #value>{{ key }}</template>
+					</MkKeyValue>
+				</FormSplit>
+				
+				<FormTextarea v-model="valueForEditor" tall class="_monospace">
+					<template #label>{{ i18n.ts.value }} (JSON)</template>
+				</FormTextarea>
+
+				<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>
-				<MkKeyValue class="_formBlock">
-					<template #key>{{ i18n.ts._registry.scope }}</template>
-					<template #value>{{ scope.join('/') }}</template>
-				</MkKeyValue>
-				<MkKeyValue class="_formBlock">
-					<template #key>{{ i18n.ts._registry.key }}</template>
-					<template #value>{{ key }}</template>
-				</MkKeyValue>
-			</FormSplit>
-			
-			<FormTextarea v-model="valueForEditor" tall class="_formBlock _monospace">
-				<template #label>{{ i18n.ts.value }} (JSON)</template>
-			</FormTextarea>
 
-			<MkButton class="_formBlock" primary @click="save"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton>
-
-			<MkKeyValue class="_formBlock">
-				<template #key>{{ i18n.ts.updatedAt }}</template>
-				<template #value><MkTime :time="value.updatedAt" mode="detail"/></template>
-			</MkKeyValue>
-
-			<MkButton danger @click="del"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton>
-		</template>
+				<MkButton danger @click="del"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton>
+			</template>
+		</div>
 	</MkSpacer>
 </MkStickyContainer>
 </template>
diff --git a/packages/frontend/src/pages/reset-password.vue b/packages/frontend/src/pages/reset-password.vue
index 8ec15f6425..1ecf883c14 100644
--- a/packages/frontend/src/pages/reset-password.vue
+++ b/packages/frontend/src/pages/reset-password.vue
@@ -2,13 +2,13 @@
 <MkStickyContainer>
 	<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
 	<MkSpacer v-if="token" :content-max="700" :margin-min="16" :margin-max="32">
-		<div class="_formRoot">
-			<FormInput v-model="password" type="password" class="_formBlock">
+		<div class="_gaps_m">
+			<FormInput v-model="password" type="password">
 				<template #prefix><i class="ti ti-lock"></i></template>
 				<template #label>{{ i18n.ts.newPassword }}</template>
 			</FormInput>
 		
-			<FormButton primary class="_formBlock" @click="save">{{ i18n.ts.save }}</FormButton>
+			<MkButton primary @click="save">{{ i18n.ts.save }}</MkButton>
 		</div>
 	</MkSpacer>
 </MkStickyContainer>
@@ -17,7 +17,7 @@
 <script lang="ts" setup>
 import { defineAsyncComponent, onMounted } from 'vue';
 import FormInput from '@/components/form/input.vue';
-import FormButton from '@/components/MkButton.vue';
+import MkButton from '@/components/MkButton.vue';
 import * as os from '@/os';
 import { i18n } from '@/i18n';
 import { mainRouter } from '@/router';
diff --git a/packages/frontend/src/pages/scratchpad.vue b/packages/frontend/src/pages/scratchpad.vue
index 9db17efc03..ff5f06c8da 100644
--- a/packages/frontend/src/pages/scratchpad.vue
+++ b/packages/frontend/src/pages/scratchpad.vue
@@ -1,25 +1,34 @@
 <template>
-<div class="iltifgqe">
-	<div class="editor _panel _gap">
-		<PrismEditor v-model="code" class="_code code" :highlight="highlighter" :line-numbers="false"/>
-		<MkButton style="position: absolute; top: 8px; right: 8px;" primary @click="run()"><i class="ti ti-player-play"></i></MkButton>
-	</div>
-
-	<MkContainer :foldable="true" class="_gap">
-		<template #header>{{ i18n.ts.output }}</template>
-		<div class="bepmlvbi">
-			<div v-for="log in logs" :key="log.id" class="log" :class="{ print: log.print }">{{ log.text }}</div>
+<MkSpacer :content-max="800">
+	<div :class="$style.root">
+		<div :class="$style.editor" class="_panel">
+			<PrismEditor v-model="code" class="_code code" :highlight="highlighter" :line-numbers="false"/>
+			<MkButton style="position: absolute; top: 8px; right: 8px;" primary @click="run()"><i class="ti ti-player-play"></i></MkButton>
 		</div>
-	</MkContainer>
 
-	<div class="_gap">
-		{{ i18n.ts.scratchpadDescription }}
+		<MkContainer v-if="root && components.length > 1" :key="uiKey" :foldable="true">
+			<template #header>UI</template>
+			<div :class="$style.ui">
+				<MkAsUi :component="root" :components="components" size="small"/>
+			</div>
+		</MkContainer>
+
+		<MkContainer :foldable="true" class="">
+			<template #header>{{ i18n.ts.output }}</template>
+			<div :class="$style.logs">
+				<div v-for="log in logs" :key="log.id" class="log" :class="{ print: log.print }">{{ log.text }}</div>
+			</div>
+		</MkContainer>
+
+		<div class="">
+			{{ i18n.ts.scratchpadDescription }}
+		</div>
 	</div>
-</div>
+</MkSpacer>
 </template>
 
 <script lang="ts" setup>
-import { ref, watch } from 'vue';
+import { onDeactivated, onUnmounted, Ref, ref, watch } from 'vue';
 import 'prismjs';
 import { highlight, languages } from 'prismjs/components/prism-core';
 import 'prismjs/components/prism-clike';
@@ -35,26 +44,41 @@ import * as os from '@/os';
 import { $i } from '@/account';
 import { i18n } from '@/i18n';
 import { definePageMetadata } from '@/scripts/page-metadata';
+import { AsUiComponent, AsUiRoot, patch, registerAsUiLib, render } from '@/scripts/aiscript/ui';
+import MkAsUi from '@/components/MkAsUi.vue';
+import { miLocalStorage } from '@/local-storage';
 
 const parser = new Parser();
-
+let aiscript: Interpreter;
 const code = ref('');
 const logs = ref<any[]>([]);
+const root = ref<AsUiRoot>();
+let components: Ref<AsUiComponent>[] = [];
+let uiKey = $ref(0);
 
-const saved = localStorage.getItem('scratchpad');
+const saved = miLocalStorage.getItem('scratchpad');
 if (saved) {
 	code.value = saved;
 }
 
 watch(code, () => {
-	localStorage.setItem('scratchpad', code.value);
+	miLocalStorage.setItem('scratchpad', code.value);
 });
 
 async function run() {
+	if (aiscript) aiscript.abort();
+	root.value = undefined;
+	components = [];
+	uiKey++;
 	logs.value = [];
-	const aiscript = new Interpreter(createAiScriptEnv({
-		storageKey: 'scratchpad',
-		token: $i?.token,
+	aiscript = new Interpreter(({
+		...createAiScriptEnv({
+			storageKey: 'widget',
+			token: $i?.token,
+		}),
+		...registerAsUiLib(components, (_root) => {
+			root.value = _root.value;
+		}),
 	}), {
 		in: (q) => {
 			return new Promise(ok => {
@@ -96,10 +120,11 @@ async function run() {
 	}
 	try {
 		await aiscript.exec(ast);
-	} catch (error: any) {
+	} catch (err: any) {
 		os.alert({
 			type: 'error',
-			text: error.message,
+			title: 'AiScript Error',
+			text: err.message,
 		});
 	}
 }
@@ -108,6 +133,14 @@ function highlighter(code) {
 	return highlight(code, languages.js, 'javascript');
 }
 
+onDeactivated(() => {
+	if (aiscript) aiscript.abort();
+});
+
+onUnmounted(() => {
+	if (aiscript) aiscript.abort();
+});
+
 const headerActions = $computed(() => []);
 
 const headerTabs = $computed(() => []);
@@ -118,21 +151,29 @@ definePageMetadata({
 });
 </script>
 
-<style lang="scss" scoped>
-.iltifgqe {
-	padding: 16px;
-
-	> .editor {
-		position: relative;
-	}
+<style lang="scss" module>
+.root {
+	display: flex;
+	flex-direction: column;
+	gap: var(--margin);
 }
 
-.bepmlvbi {
+.editor {
+	position: relative;
+}
+
+.ui {
+	padding: 32px;
+}
+
+.logs {
 	padding: 16px;
 
-	> .log {
-		&:not(.print) {
-			opacity: 0.7;
+	&:global {
+		> .log {
+			&:not(.print) {
+				opacity: 0.7;
+			}
 		}
 	}
 }
diff --git a/packages/frontend/src/pages/settings/account-info.vue b/packages/frontend/src/pages/settings/account-info.vue
index ccd99c162a..584808b0b4 100644
--- a/packages/frontend/src/pages/settings/account-info.vue
+++ b/packages/frontend/src/pages/settings/account-info.vue
@@ -1,5 +1,5 @@
 <template>
-<div class="_formRoot">
+<div class="_gaps_m">
 	<MkKeyValue>
 		<template #key>ID</template>
 		<template #value><span class="_monospace">{{ $i.id }}</span></template>
diff --git a/packages/frontend/src/pages/settings/accounts.vue b/packages/frontend/src/pages/settings/accounts.vue
index 493d3b2618..c66cc12925 100644
--- a/packages/frontend/src/pages/settings/accounts.vue
+++ b/packages/frontend/src/pages/settings/accounts.vue
@@ -1,7 +1,7 @@
 <template>
-<div class="_formRoot">
+<div class="_gaps_m">
 	<FormSuspense :p="init">
-		<FormButton primary @click="addAccount"><i class="ti ti-plus"></i> {{ i18n.ts.addAccount }}</FormButton>
+		<MkButton primary @click="addAccount"><i class="ti ti-plus"></i> {{ i18n.ts.addAccount }}</MkButton>
 
 		<div v-for="account in accounts" :key="account.id" class="_panel _button lcjjdxlm" @click="menu(account, $event)">
 			<div class="avatar">
@@ -23,7 +23,7 @@
 <script lang="ts" setup>
 import { defineAsyncComponent, ref } from 'vue';
 import FormSuspense from '@/components/form/suspense.vue';
-import FormButton from '@/components/MkButton.vue';
+import MkButton from '@/components/MkButton.vue';
 import * as os from '@/os';
 import { getAccounts, addAccount as addAccounts, removeAccount as _removeAccount, login, $i } from '@/account';
 import { i18n } from '@/i18n';
diff --git a/packages/frontend/src/pages/settings/api.vue b/packages/frontend/src/pages/settings/api.vue
index 8d7291cd10..0fb44d9fad 100644
--- a/packages/frontend/src/pages/settings/api.vue
+++ b/packages/frontend/src/pages/settings/api.vue
@@ -1,15 +1,15 @@
 <template>
-<div class="_formRoot">
-	<FormButton primary class="_formBlock" @click="generateToken">{{ i18n.ts.generateAccessToken }}</FormButton>
-	<FormLink to="/settings/apps" class="_formBlock">{{ i18n.ts.manageAccessTokens }}</FormLink>
-	<FormLink to="/api-console" :behavior="isDesktop ? 'window' : null" class="_formBlock">API console</FormLink>
+<div class="_gaps_m">
+	<MkButton primary @click="generateToken">{{ i18n.ts.generateAccessToken }}</MkButton>
+	<FormLink to="/settings/apps">{{ i18n.ts.manageAccessTokens }}</FormLink>
+	<FormLink to="/api-console" :behavior="isDesktop ? 'window' : null">API console</FormLink>
 </div>
 </template>
 
 <script lang="ts" setup>
 import { defineAsyncComponent, ref } from 'vue';
 import FormLink from '@/components/form/link.vue';
-import FormButton from '@/components/MkButton.vue';
+import MkButton from '@/components/MkButton.vue';
 import * as os from '@/os';
 import { i18n } from '@/i18n';
 import { definePageMetadata } from '@/scripts/page-metadata';
diff --git a/packages/frontend/src/pages/settings/apps.vue b/packages/frontend/src/pages/settings/apps.vue
index 0154c0c951..861414cef8 100644
--- a/packages/frontend/src/pages/settings/apps.vue
+++ b/packages/frontend/src/pages/settings/apps.vue
@@ -1,5 +1,5 @@
 <template>
-<div class="_formRoot">
+<div class="_gaps_m">
 	<FormPagination ref="list" :pagination="pagination">
 		<template #empty>
 			<div class="_fullinfo">
@@ -13,23 +13,23 @@
 				<div class="body">
 					<div class="name">{{ token.name }}</div>
 					<div class="description">{{ token.description }}</div>
-					<div class="">
-						<div>{{ i18n.ts.installedDate }}:</div>
-						<div><MkTime :time="token.createdAt"/></div>
-					</div>
-					<div class="">
-						<div>{{ i18n.ts.lastUsedDate }}:</div>
-						<div><MkTime :time="token.lastUsedAt"/></div>
-					</div>
-					<div class="actions">
-						<button class="_button" @click="revoke(token)"><i class="ti ti-trash"></i></button>
-					</div>
+					<MkKeyValue oneline>
+						<template #key>{{ i18n.ts.installedDate }}</template>
+						<template #value><MkTime :time="token.createdAt"/></template>
+					</MkKeyValue>
+					<MkKeyValue oneline>
+						<template #key>{{ i18n.ts.lastUsedDate }}</template>
+						<template #value><MkTime :time="token.lastUsedAt"/></template>
+					</MkKeyValue>
 					<details>
 						<summary>{{ i18n.ts.details }}</summary>
 						<ul>
 							<li v-for="p in token.permission" :key="p">{{ $t(`_permissions.${p}`) }}</li>
 						</ul>
 					</details>
+					<div class="actions">
+						<MkButton inline danger @click="revoke(token)"><i class="ti ti-trash"></i></MkButton>
+					</div>
 				</div>
 			</div>
 		</template>
@@ -43,6 +43,8 @@ import FormPagination from '@/components/MkPagination.vue';
 import * as os from '@/os';
 import { i18n } from '@/i18n';
 import { definePageMetadata } from '@/scripts/page-metadata';
+import MkKeyValue from '@/components/MkKeyValue.vue';
+import MkButton from '@/components/MkButton.vue';
 
 const list = ref<any>(null);
 
diff --git a/packages/frontend/src/pages/settings/custom-css.vue b/packages/frontend/src/pages/settings/custom-css.vue
index 2caad22b7b..be2ec32ac2 100644
--- a/packages/frontend/src/pages/settings/custom-css.vue
+++ b/packages/frontend/src/pages/settings/custom-css.vue
@@ -1,8 +1,8 @@
 <template>
-<div class="_formRoot">
-	<FormInfo warn class="_formBlock">{{ i18n.ts.customCssWarn }}</FormInfo>
+<div class="_gaps_m">
+	<FormInfo warn>{{ i18n.ts.customCssWarn }}</FormInfo>
 
-	<FormTextarea v-model="localCustomCss" manual-save tall class="_monospace _formBlock" style="tab-size: 2;">
+	<FormTextarea v-model="localCustomCss" manual-save tall class="_monospace" style="tab-size: 2;">
 		<template #label>CSS</template>
 	</FormTextarea>
 </div>
@@ -16,11 +16,12 @@ import * as os from '@/os';
 import { unisonReload } from '@/scripts/unison-reload';
 import { i18n } from '@/i18n';
 import { definePageMetadata } from '@/scripts/page-metadata';
+import { miLocalStorage } from '@/local-storage';
 
-const localCustomCss = ref(localStorage.getItem('customCss') ?? '');
+const localCustomCss = ref(miLocalStorage.getItem('customCss') ?? '');
 
 async function apply() {
-	localStorage.setItem('customCss', localCustomCss.value);
+	miLocalStorage.setItem('customCss', localCustomCss.value);
 
 	const { canceled } = await os.confirm({
 		type: 'info',
diff --git a/packages/frontend/src/pages/settings/deck.vue b/packages/frontend/src/pages/settings/deck.vue
index 82cefe05d5..4455b90c0b 100644
--- a/packages/frontend/src/pages/settings/deck.vue
+++ b/packages/frontend/src/pages/settings/deck.vue
@@ -1,10 +1,10 @@
 <template>
-<div class="_formRoot">
+<div class="_gaps_m">
 	<FormSwitch v-model="navWindow">{{ i18n.ts.defaultNavigationBehaviour }}: {{ i18n.ts.openInWindow }}</FormSwitch>
 
-	<FormSwitch v-model="alwaysShowMainColumn" class="_formBlock">{{ i18n.ts._deck.alwaysShowMainColumn }}</FormSwitch>
+	<FormSwitch v-model="alwaysShowMainColumn">{{ i18n.ts._deck.alwaysShowMainColumn }}</FormSwitch>
 
-	<FormRadios v-model="columnAlign" class="_formBlock">
+	<FormRadios v-model="columnAlign">
 		<template #label>{{ i18n.ts._deck.columnAlign }}</template>
 		<option value="left">{{ i18n.ts.left }}</option>
 		<option value="center">{{ i18n.ts.center }}</option>
diff --git a/packages/frontend/src/pages/settings/delete-account.vue b/packages/frontend/src/pages/settings/delete-account.vue
index 8a25ff39f0..bbd5513954 100644
--- a/packages/frontend/src/pages/settings/delete-account.vue
+++ b/packages/frontend/src/pages/settings/delete-account.vue
@@ -1,15 +1,15 @@
 <template>
-<div class="_formRoot">
-	<FormInfo warn class="_formBlock">{{ i18n.ts._accountDelete.mayTakeTime }}</FormInfo>
-	<FormInfo class="_formBlock">{{ i18n.ts._accountDelete.sendEmail }}</FormInfo>
-	<FormButton v-if="!$i.isDeleted" danger class="_formBlock" @click="deleteAccount">{{ i18n.ts._accountDelete.requestAccountDelete }}</FormButton>
-	<FormButton v-else disabled>{{ i18n.ts._accountDelete.inProgress }}</FormButton>
+<div class="_gaps_m">
+	<FormInfo warn>{{ i18n.ts._accountDelete.mayTakeTime }}</FormInfo>
+	<FormInfo>{{ i18n.ts._accountDelete.sendEmail }}</FormInfo>
+	<MkButton v-if="!$i.isDeleted" danger @click="deleteAccount">{{ i18n.ts._accountDelete.requestAccountDelete }}</MkButton>
+	<MkButton v-else disabled>{{ i18n.ts._accountDelete.inProgress }}</MkButton>
 </div>
 </template>
 
 <script lang="ts" setup>
 import FormInfo from '@/components/MkInfo.vue';
-import FormButton from '@/components/MkButton.vue';
+import MkButton from '@/components/MkButton.vue';
 import * as os from '@/os';
 import { signout } from '@/account';
 import { i18n } from '@/i18n';
diff --git a/packages/frontend/src/pages/settings/drive.vue b/packages/frontend/src/pages/settings/drive.vue
index 2d45b1add8..acfee9537b 100644
--- a/packages/frontend/src/pages/settings/drive.vue
+++ b/packages/frontend/src/pages/settings/drive.vue
@@ -1,20 +1,23 @@
 <template>
-<div class="_formRoot">
-	<FormSection v-if="!fetching">
+<div class="_gaps_m">
+	<FormSection v-if="!fetching" first>
 		<template #label>{{ i18n.ts.usageAmount }}</template>
-		<div class="_formBlock uawsfosz">
-			<div class="meter"><div :style="meterStyle"></div></div>
+
+		<div class="_gaps_m">
+			<div class="uawsfosz">
+				<div class="meter"><div :style="meterStyle"></div></div>
+			</div>
+			<FormSplit>
+				<MkKeyValue>
+					<template #key>{{ i18n.ts.capacity }}</template>
+					<template #value>{{ bytes(capacity, 1) }}</template>
+				</MkKeyValue>
+				<MkKeyValue>
+					<template #key>{{ i18n.ts.inUse }}</template>
+					<template #value>{{ bytes(usage, 1) }}</template>
+				</MkKeyValue>
+			</FormSplit>
 		</div>
-		<FormSplit>
-			<MkKeyValue class="_formBlock">
-				<template #key>{{ i18n.ts.capacity }}</template>
-				<template #value>{{ bytes(capacity, 1) }}</template>
-			</MkKeyValue>
-			<MkKeyValue class="_formBlock">
-				<template #key>{{ i18n.ts.inUse }}</template>
-				<template #value>{{ bytes(usage, 1) }}</template>
-			</MkKeyValue>
-		</FormSplit>
 	</FormSection>
 
 	<FormSection>
@@ -23,22 +26,24 @@
 	</FormSection>
 
 	<FormSection>
-		<FormLink @click="chooseUploadFolder()">
-			{{ i18n.ts.uploadFolder }}
-			<template #suffix>{{ uploadFolder ? uploadFolder.name : '-' }}</template>
-			<template #suffixIcon><i class="fas fa-folder-open"></i></template>
-		</FormLink>
-		<FormSwitch v-model="keepOriginalUploading" class="_formBlock">
-			<template #label>{{ i18n.ts.keepOriginalUploading }}</template>
-			<template #caption>{{ i18n.ts.keepOriginalUploadingDescription }}</template>
-		</FormSwitch>
-		<FormSwitch v-model="alwaysMarkNsfw" class="_formBlock" @update:model-value="saveProfile()">
-			<template #label>{{ i18n.ts.alwaysMarkSensitive }}</template>
-		</FormSwitch>
-		<FormSwitch v-model="autoSensitive" class="_formBlock" @update:model-value="saveProfile()">
-			<template #label>{{ i18n.ts.enableAutoSensitive }}<span class="_beta">{{ i18n.ts.beta }}</span></template>
-			<template #caption>{{ i18n.ts.enableAutoSensitiveDescription }}</template>
-		</FormSwitch>
+		<div class="_gaps_m">
+			<FormLink @click="chooseUploadFolder()">
+				{{ i18n.ts.uploadFolder }}
+				<template #suffix>{{ uploadFolder ? uploadFolder.name : '-' }}</template>
+				<template #suffixIcon><i class="fas fa-folder-open"></i></template>
+			</FormLink>
+			<FormSwitch v-model="keepOriginalUploading">
+				<template #label>{{ i18n.ts.keepOriginalUploading }}</template>
+				<template #caption>{{ i18n.ts.keepOriginalUploadingDescription }}</template>
+			</FormSwitch>
+			<FormSwitch v-model="alwaysMarkNsfw" @update:model-value="saveProfile()">
+				<template #label>{{ i18n.ts.alwaysMarkSensitive }}</template>
+			</FormSwitch>
+			<FormSwitch v-model="autoSensitive" @update:model-value="saveProfile()">
+				<template #label>{{ i18n.ts.enableAutoSensitive }}<span class="_beta">{{ i18n.ts.beta }}</span></template>
+				<template #caption>{{ i18n.ts.enableAutoSensitiveDescription }}</template>
+			</FormSwitch>
+		</div>
 	</FormSection>
 </div>
 </template>
diff --git a/packages/frontend/src/pages/settings/email.vue b/packages/frontend/src/pages/settings/email.vue
index 3fff8c6b1d..569115bda8 100644
--- a/packages/frontend/src/pages/settings/email.vue
+++ b/packages/frontend/src/pages/settings/email.vue
@@ -1,6 +1,6 @@
 <template>
-<div class="_formRoot">
-	<FormSection>
+<div class="_gaps_m">
+	<FormSection first>
 		<template #label>{{ i18n.ts.emailAddress }}</template>
 		<FormInput v-model="emailAddress" type="email" manual-save>
 			<template #prefix><i class="ti ti-mail"></i></template>
@@ -17,24 +17,27 @@
 
 	<FormSection>
 		<template #label>{{ i18n.ts.emailNotification }}</template>
-		<FormSwitch v-model="emailNotification_mention" class="_formBlock">
-			{{ i18n.ts._notification._types.mention }}
-		</FormSwitch>
-		<FormSwitch v-model="emailNotification_reply" class="_formBlock">
-			{{ i18n.ts._notification._types.reply }}
-		</FormSwitch>
-		<FormSwitch v-model="emailNotification_quote" class="_formBlock">
-			{{ i18n.ts._notification._types.quote }}
-		</FormSwitch>
-		<FormSwitch v-model="emailNotification_follow" class="_formBlock">
-			{{ i18n.ts._notification._types.follow }}
-		</FormSwitch>
-		<FormSwitch v-model="emailNotification_receiveFollowRequest" class="_formBlock">
-			{{ i18n.ts._notification._types.receiveFollowRequest }}
-		</FormSwitch>
-		<FormSwitch v-model="emailNotification_groupInvited" class="_formBlock">
-			{{ i18n.ts._notification._types.groupInvited }}
-		</FormSwitch>
+
+		<div class="_gaps_s">
+			<FormSwitch v-model="emailNotification_mention">
+				{{ i18n.ts._notification._types.mention }}
+			</FormSwitch>
+			<FormSwitch v-model="emailNotification_reply">
+				{{ i18n.ts._notification._types.reply }}
+			</FormSwitch>
+			<FormSwitch v-model="emailNotification_quote">
+				{{ i18n.ts._notification._types.quote }}
+			</FormSwitch>
+			<FormSwitch v-model="emailNotification_follow">
+				{{ i18n.ts._notification._types.follow }}
+			</FormSwitch>
+			<FormSwitch v-model="emailNotification_receiveFollowRequest">
+				{{ i18n.ts._notification._types.receiveFollowRequest }}
+			</FormSwitch>
+			<FormSwitch v-model="emailNotification_groupInvited">
+				{{ i18n.ts._notification._types.groupInvited }}
+			</FormSwitch>
+		</div>
 	</FormSection>
 </div>
 </template>
diff --git a/packages/frontend/src/pages/settings/general.vue b/packages/frontend/src/pages/settings/general.vue
index b426ccfa0a..580c38149a 100644
--- a/packages/frontend/src/pages/settings/general.vue
+++ b/packages/frontend/src/pages/settings/general.vue
@@ -1,6 +1,6 @@
 <template>
-<div class="_formRoot">
-	<FormSelect v-model="lang" class="_formBlock">
+<div class="_gaps_m">
+	<FormSelect v-model="lang">
 		<template #label>{{ i18n.ts.uiLanguage }}</template>
 		<option v-for="x in langs" :key="x[0]" :value="x[0]">{{ x[1] }}</option>
 		<template #caption>
@@ -12,7 +12,7 @@
 		</template>
 	</FormSelect>
 
-	<FormRadios v-model="overridedDeviceKind" class="_formBlock">
+	<FormRadios v-model="overridedDeviceKind">
 		<template #label>{{ i18n.ts.overridedDeviceKind }}</template>
 		<option :value="null">{{ i18n.ts.auto }}</option>
 		<option value="smartphone"><i class="ti ti-device-mobile"/> {{ i18n.ts.smartphone }}</option>
@@ -20,80 +20,88 @@
 		<option value="desktop"><i class="ti ti-device-desktop"/> {{ i18n.ts.desktop }}</option>
 	</FormRadios>
 
-	<FormSwitch v-model="showFixedPostForm" class="_formBlock">{{ i18n.ts.showFixedPostForm }}</FormSwitch>
+	<FormSwitch v-model="showFixedPostForm">{{ i18n.ts.showFixedPostForm }}</FormSwitch>
 
 	<FormSection>
 		<template #label>{{ i18n.ts.behavior }}</template>
-		<FormSwitch v-model="imageNewTab" class="_formBlock">{{ i18n.ts.openImageInNewTab }}</FormSwitch>
-		<FormSwitch v-model="enableInfiniteScroll" class="_formBlock">{{ i18n.ts.enableInfiniteScroll }}</FormSwitch>
-		<FormSwitch v-model="useReactionPickerForContextMenu" class="_formBlock">{{ i18n.ts.useReactionPickerForContextMenu }}</FormSwitch>
 
-		<FormSelect v-model="serverDisconnectedBehavior" class="_formBlock">
-			<template #label>{{ i18n.ts.whenServerDisconnected }}</template>
-			<option value="reload">{{ i18n.ts._serverDisconnectedBehavior.reload }}</option>
-			<option value="dialog">{{ i18n.ts._serverDisconnectedBehavior.dialog }}</option>
-			<option value="quiet">{{ i18n.ts._serverDisconnectedBehavior.quiet }}</option>
-		</FormSelect>
+		<div class="_gaps_m">
+			<div class="_gaps_s">
+				<FormSwitch v-model="imageNewTab">{{ i18n.ts.openImageInNewTab }}</FormSwitch>
+				<FormSwitch v-model="enableInfiniteScroll">{{ i18n.ts.enableInfiniteScroll }}</FormSwitch>
+				<FormSwitch v-model="useReactionPickerForContextMenu">{{ i18n.ts.useReactionPickerForContextMenu }}</FormSwitch>
+			</div>
+			<FormSelect v-model="serverDisconnectedBehavior">
+				<template #label>{{ i18n.ts.whenServerDisconnected }}</template>
+				<option value="reload">{{ i18n.ts._serverDisconnectedBehavior.reload }}</option>
+				<option value="dialog">{{ i18n.ts._serverDisconnectedBehavior.dialog }}</option>
+				<option value="quiet">{{ i18n.ts._serverDisconnectedBehavior.quiet }}</option>
+			</FormSelect>
+		</div>
 	</FormSection>
 
 	<FormSection>
 		<template #label>{{ i18n.ts.appearance }}</template>
-		<FormSwitch v-model="disableAnimatedMfm" class="_formBlock">{{ i18n.ts.disableAnimatedMfm }}</FormSwitch>
-		<FormSwitch v-model="reduceAnimation" class="_formBlock">{{ i18n.ts.reduceUiAnimation }}</FormSwitch>
-		<FormSwitch v-model="useBlurEffect" class="_formBlock">{{ i18n.ts.useBlurEffect }}</FormSwitch>
-		<FormSwitch v-model="useBlurEffectForModal" class="_formBlock">{{ i18n.ts.useBlurEffectForModal }}</FormSwitch>
-		<FormSwitch v-model="showGapBetweenNotesInTimeline" class="_formBlock">{{ i18n.ts.showGapBetweenNotesInTimeline }}</FormSwitch>
-		<FormSwitch v-model="loadRawImages" class="_formBlock">{{ i18n.ts.loadRawImages }}</FormSwitch>
-		<FormSwitch v-model="disableShowingAnimatedImages" class="_formBlock">{{ i18n.ts.disableShowingAnimatedImages }}</FormSwitch>
-		<FormSwitch v-model="squareAvatars" class="_formBlock">{{ i18n.ts.squareAvatars }}</FormSwitch>
-		<FormSwitch v-model="useSystemFont" class="_formBlock">{{ i18n.ts.useSystemFont }}</FormSwitch>
-		<div class="_formBlock">
-			<FormRadios v-model="emojiStyle">
-				<template #label>{{ i18n.ts.emojiStyle }}</template>
-				<option value="native">{{ i18n.ts.native }}</option>
-				<option value="fluentEmoji">Fluent Emoji</option>
-				<option value="twemoji">Twemoji</option>
+
+		<div class="_gaps_m">
+			<div class="_gaps_s">
+				<FormSwitch v-model="disableAnimatedMfm">{{ i18n.ts.disableAnimatedMfm }}</FormSwitch>
+				<FormSwitch v-model="reduceAnimation">{{ i18n.ts.reduceUiAnimation }}</FormSwitch>
+				<FormSwitch v-model="useBlurEffect">{{ i18n.ts.useBlurEffect }}</FormSwitch>
+				<FormSwitch v-model="useBlurEffectForModal">{{ i18n.ts.useBlurEffectForModal }}</FormSwitch>
+				<FormSwitch v-model="showGapBetweenNotesInTimeline">{{ i18n.ts.showGapBetweenNotesInTimeline }}</FormSwitch>
+				<FormSwitch v-model="loadRawImages">{{ i18n.ts.loadRawImages }}</FormSwitch>
+				<FormSwitch v-model="disableShowingAnimatedImages">{{ i18n.ts.disableShowingAnimatedImages }}</FormSwitch>
+				<FormSwitch v-model="squareAvatars">{{ i18n.ts.squareAvatars }}</FormSwitch>
+				<FormSwitch v-model="useSystemFont">{{ i18n.ts.useSystemFont }}</FormSwitch>
+				<FormSwitch v-model="disableDrawer">{{ i18n.ts.disableDrawer }}</FormSwitch>
+			</div>
+			<div>
+				<FormRadios v-model="emojiStyle">
+					<template #label>{{ i18n.ts.emojiStyle }}</template>
+					<option value="native">{{ i18n.ts.native }}</option>
+					<option value="fluentEmoji">Fluent Emoji</option>
+					<option value="twemoji">Twemoji</option>
+				</FormRadios>
+				<div style="margin: 8px 0 0 0; font-size: 1.5em;"><Mfm :key="emojiStyle" text="🍮🍦🍭🍩🍰🍫🍬🥞🍪"/></div>
+			</div>
+
+			<FormRadios v-model="fontSize">
+				<template #label>{{ i18n.ts.fontSize }}</template>
+				<option :value="null"><span style="font-size: 14px;">Aa</span></option>
+				<option value="1"><span style="font-size: 15px;">Aa</span></option>
+				<option value="2"><span style="font-size: 16px;">Aa</span></option>
+				<option value="3"><span style="font-size: 17px;">Aa</span></option>
 			</FormRadios>
-			<div style="margin: 8px 0 0 0; font-size: 1.5em;"><Mfm :key="emojiStyle" text="🍮🍦🍭🍩🍰🍫🍬🥞🍪"/></div>
 		</div>
-
-		<FormSwitch v-model="disableDrawer" class="_formBlock">{{ i18n.ts.disableDrawer }}</FormSwitch>
-
-		<FormRadios v-model="fontSize" class="_formBlock">
-			<template #label>{{ i18n.ts.fontSize }}</template>
-			<option :value="null"><span style="font-size: 14px;">Aa</span></option>
-			<option value="1"><span style="font-size: 15px;">Aa</span></option>
-			<option value="2"><span style="font-size: 16px;">Aa</span></option>
-			<option value="3"><span style="font-size: 17px;">Aa</span></option>
-		</FormRadios>
 	</FormSection>
 
 	<FormSection>
 		<FormSwitch v-model="aiChanMode">{{ i18n.ts.aiChanMode }}</FormSwitch>
 	</FormSection>
 
-	<FormSelect v-model="instanceTicker" class="_formBlock">
+	<FormSelect v-model="instanceTicker">
 		<template #label>{{ i18n.ts.instanceTicker }}</template>
 		<option value="none">{{ i18n.ts._instanceTicker.none }}</option>
 		<option value="remote">{{ i18n.ts._instanceTicker.remote }}</option>
 		<option value="always">{{ i18n.ts._instanceTicker.always }}</option>
 	</FormSelect>
 
-	<FormSelect v-model="nsfw" class="_formBlock">
+	<FormSelect v-model="nsfw">
 		<template #label>{{ i18n.ts.nsfw }}</template>
 		<option value="respect">{{ i18n.ts._nsfw.respect }}</option>
 		<option value="ignore">{{ i18n.ts._nsfw.ignore }}</option>
 		<option value="force">{{ i18n.ts._nsfw.force }}</option>
 	</FormSelect>
 
-	<FormRange v-model="numberOfPageCache" :min="1" :max="10" :step="1" easing class="_formBlock">
+	<FormRange v-model="numberOfPageCache" :min="1" :max="10" :step="1" easing>
 		<template #label>{{ i18n.ts.numberOfPageCache }}</template>
 		<template #caption>{{ i18n.ts.numberOfPageCacheDescription }}</template>
 	</FormRange>
 
-	<FormLink to="/settings/deck" class="_formBlock">{{ i18n.ts.deck }}</FormLink>
+	<FormLink to="/settings/deck">{{ i18n.ts.deck }}</FormLink>
 
-	<FormLink to="/settings/custom-css" class="_formBlock"><template #icon><i class="ti ti-code"></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>
 </template>
 
@@ -112,10 +120,11 @@ import * as os from '@/os';
 import { unisonReload } from '@/scripts/unison-reload';
 import { i18n } from '@/i18n';
 import { definePageMetadata } from '@/scripts/page-metadata';
+import { miLocalStorage } from '@/local-storage';
 
-const lang = ref(localStorage.getItem('lang'));
-const fontSize = ref(localStorage.getItem('fontSize'));
-const useSystemFont = ref(localStorage.getItem('useSystemFont') != null);
+const lang = ref(miLocalStorage.getItem('lang'));
+const fontSize = ref(miLocalStorage.getItem('fontSize'));
+const useSystemFont = ref(miLocalStorage.getItem('useSystemFont') != null);
 
 async function reloadAsk() {
 	const { canceled } = await os.confirm({
@@ -149,23 +158,23 @@ const squareAvatars = computed(defaultStore.makeGetterSetter('squareAvatars'));
 const aiChanMode = computed(defaultStore.makeGetterSetter('aiChanMode'));
 
 watch(lang, () => {
-	localStorage.setItem('lang', lang.value as string);
-	localStorage.removeItem('locale');
+	miLocalStorage.setItem('lang', lang.value as string);
+	miLocalStorage.removeItem('locale');
 });
 
 watch(fontSize, () => {
 	if (fontSize.value == null) {
-		localStorage.removeItem('fontSize');
+		miLocalStorage.removeItem('fontSize');
 	} else {
-		localStorage.setItem('fontSize', fontSize.value);
+		miLocalStorage.setItem('fontSize', fontSize.value);
 	}
 });
 
 watch(useSystemFont, () => {
 	if (useSystemFont.value) {
-		localStorage.setItem('useSystemFont', 't');
+		miLocalStorage.setItem('useSystemFont', 't');
 	} else {
-		localStorage.removeItem('useSystemFont');
+		miLocalStorage.removeItem('useSystemFont');
 	}
 });
 
diff --git a/packages/frontend/src/pages/settings/import-export.vue b/packages/frontend/src/pages/settings/import-export.vue
index 3012c3f283..d055304824 100644
--- a/packages/frontend/src/pages/settings/import-export.vue
+++ b/packages/frontend/src/pages/settings/import-export.vue
@@ -1,6 +1,6 @@
 <template>
-<div class="_formRoot">
-	<FormSection>
+<div class="_gaps_m">
+	<FormSection first>
 		<template #label><i class="ti ti-pencil"></i> {{ i18n.ts._exportOrImport.allNotes }}</template>
 		<FormFolder>
 			<template #label>{{ i18n.ts.export }}</template>
@@ -18,61 +18,71 @@
 	</FormSection>
 	<FormSection>
 		<template #label><i class="ti ti-users"></i> {{ i18n.ts._exportOrImport.followingList }}</template>
-		<FormFolder class="_formBlock">
-			<template #label>{{ i18n.ts.export }}</template>
-			<template #icon><i class="ti ti-download"></i></template>
-			<FormSwitch v-model="excludeMutingUsers" class="_formBlock">
-				{{ i18n.ts._exportOrImport.excludeMutingUsers }}
-			</FormSwitch>
-			<FormSwitch v-model="excludeInactiveUsers" class="_formBlock">
-				{{ i18n.ts._exportOrImport.excludeInactiveUsers }}
-			</FormSwitch>
-			<MkButton primary :class="$style.button" inline @click="exportFollowing()"><i class="ti ti-download"></i> {{ i18n.ts.export }}</MkButton>
-		</FormFolder>
-		<FormFolder class="_formBlock">
-			<template #label>{{ i18n.ts.import }}</template>
-			<template #icon><i class="ti ti-upload"></i></template>
-			<MkButton primary :class="$style.button" inline @click="importFollowing($event)"><i class="ti ti-upload"></i> {{ i18n.ts.import }}</MkButton>
-		</FormFolder>
+		<div class="_gaps_s">
+			<FormFolder>
+				<template #label>{{ i18n.ts.export }}</template>
+				<template #icon><i class="ti ti-download"></i></template>
+				<div class="_gaps_s">
+					<FormSwitch v-model="excludeMutingUsers">
+						{{ i18n.ts._exportOrImport.excludeMutingUsers }}
+					</FormSwitch>
+					<FormSwitch v-model="excludeInactiveUsers">
+						{{ i18n.ts._exportOrImport.excludeInactiveUsers }}
+					</FormSwitch>
+					<MkButton primary :class="$style.button" inline @click="exportFollowing()"><i class="ti ti-download"></i> {{ i18n.ts.export }}</MkButton>
+				</div>
+			</FormFolder>
+			<FormFolder>
+				<template #label>{{ i18n.ts.import }}</template>
+				<template #icon><i class="ti ti-upload"></i></template>
+				<MkButton primary :class="$style.button" inline @click="importFollowing($event)"><i class="ti ti-upload"></i> {{ i18n.ts.import }}</MkButton>
+			</FormFolder>
+		</div>
 	</FormSection>
 	<FormSection>
 		<template #label><i class="ti ti-users"></i> {{ i18n.ts._exportOrImport.userLists }}</template>
-		<FormFolder class="_formBlock">
-			<template #label>{{ i18n.ts.export }}</template>
-			<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>
-		</FormFolder>
-		<FormFolder class="_formBlock">
-			<template #label>{{ i18n.ts.import }}</template>
-			<template #icon><i class="ti ti-upload"></i></template>
-			<MkButton primary :class="$style.button" inline @click="importUserLists($event)"><i class="ti ti-upload"></i> {{ i18n.ts.import }}</MkButton>
-		</FormFolder>
+		<div class="_gaps_s">
+			<FormFolder>
+				<template #label>{{ i18n.ts.export }}</template>
+				<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>
+			</FormFolder>
+			<FormFolder>
+				<template #label>{{ i18n.ts.import }}</template>
+				<template #icon><i class="ti ti-upload"></i></template>
+				<MkButton primary :class="$style.button" inline @click="importUserLists($event)"><i class="ti ti-upload"></i> {{ i18n.ts.import }}</MkButton>
+			</FormFolder>
+		</div>
 	</FormSection>
 	<FormSection>
 		<template #label><i class="ti ti-user-off"></i> {{ i18n.ts._exportOrImport.muteList }}</template>
-		<FormFolder class="_formBlock">
-			<template #label>{{ i18n.ts.export }}</template>
-			<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>
-		</FormFolder>
-		<FormFolder class="_formBlock">
-			<template #label>{{ i18n.ts.import }}</template>
-			<template #icon><i class="ti ti-upload"></i></template>
-			<MkButton primary :class="$style.button" inline @click="importMuting($event)"><i class="ti ti-upload"></i> {{ i18n.ts.import }}</MkButton>
-		</FormFolder>
+		<div class="_gaps_s">
+			<FormFolder>
+				<template #label>{{ i18n.ts.export }}</template>
+				<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>
+			</FormFolder>
+			<FormFolder>
+				<template #label>{{ i18n.ts.import }}</template>
+				<template #icon><i class="ti ti-upload"></i></template>
+				<MkButton primary :class="$style.button" inline @click="importMuting($event)"><i class="ti ti-upload"></i> {{ i18n.ts.import }}</MkButton>
+			</FormFolder>
+		</div>
 	</FormSection>
 	<FormSection>
 		<template #label><i class="ti ti-user-off"></i> {{ i18n.ts._exportOrImport.blockingList }}</template>
-		<FormFolder class="_formBlock">
-			<template #label>{{ i18n.ts.export }}</template>
-			<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>
-		</FormFolder>
-		<FormFolder class="_formBlock">
-			<template #label>{{ i18n.ts.import }}</template>
-			<template #icon><i class="ti ti-upload"></i></template>
-			<MkButton primary :class="$style.button" inline @click="importBlocking($event)"><i class="ti ti-upload"></i> {{ i18n.ts.import }}</MkButton>
-		</FormFolder>
+		<div class="_gaps_s">
+			<FormFolder>
+				<template #label>{{ i18n.ts.export }}</template>
+				<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>
+			</FormFolder>
+			<FormFolder>
+				<template #label>{{ i18n.ts.import }}</template>
+				<template #icon><i class="ti ti-upload"></i></template>
+				<MkButton primary :class="$style.button" inline @click="importBlocking($event)"><i class="ti ti-upload"></i> {{ i18n.ts.import }}</MkButton>
+			</FormFolder>
+		</div>
 	</FormSection>
 </div>
 </template>
diff --git a/packages/frontend/src/pages/settings/index.vue b/packages/frontend/src/pages/settings/index.vue
index 119a75b650..3468d44e00 100644
--- a/packages/frontend/src/pages/settings/index.vue
+++ b/packages/frontend/src/pages/settings/index.vue
@@ -33,6 +33,7 @@ import { instance } from '@/instance';
 import { useRouter } from '@/router';
 import { definePageMetadata, provideMetadataReceiver, setPageMetadata } from '@/scripts/page-metadata';
 import * as os from '@/os';
+import { miLocalStorage } from '@/local-storage';
 
 const indexInfo = {
 	title: i18n.ts.settings,
@@ -180,8 +181,8 @@ const menuDef = computed(() => [{
 		icon: 'ti ti-trash',
 		text: i18n.ts.clearCache,
 		action: () => {
-			localStorage.removeItem('locale');
-			localStorage.removeItem('theme');
+			miLocalStorage.removeItem('locale');
+			miLocalStorage.removeItem('theme');
 			unisonReload();
 		},
 	}, {
diff --git a/packages/frontend/src/pages/settings/instance-mute.vue b/packages/frontend/src/pages/settings/instance-mute.vue
index 54504de188..ccfbc89b87 100644
--- a/packages/frontend/src/pages/settings/instance-mute.vue
+++ b/packages/frontend/src/pages/settings/instance-mute.vue
@@ -1,11 +1,11 @@
 <template>
-<div class="_formRoot">
+<div class="_gaps_m">
 	<MkInfo>{{ i18n.ts._instanceMute.title }}</MkInfo>
-	<FormTextarea v-model="instanceMutes" class="_formBlock">
+	<FormTextarea v-model="instanceMutes">
 		<template #label>{{ i18n.ts._instanceMute.heading }}</template>
 		<template #caption>{{ i18n.ts._instanceMute.instanceMuteDescription }}<br>{{ i18n.ts._instanceMute.instanceMuteDescription2 }}</template>
 	</FormTextarea>
-	<MkButton primary :disabled="!changed" class="_formBlock" @click="save()"><i class="ti ti-device-floppy"></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/integration.vue b/packages/frontend/src/pages/settings/integration.vue
index 557fe778e6..1e5a785465 100644
--- a/packages/frontend/src/pages/settings/integration.vue
+++ b/packages/frontend/src/pages/settings/integration.vue
@@ -1,5 +1,5 @@
 <template>
-<div class="_formRoot">
+<div class="_gaps_m">
 	<FormSection v-if="instance.enableTwitterIntegration">
 		<template #label><i class="ti ti-brand-twitter"></i> Twitter</template>
 		<p v-if="integrations.twitter">{{ i18n.ts.connectedTo }}: <a :href="`https://twitter.com/${integrations.twitter.screenName}`" rel="nofollow noopener" target="_blank">@{{ integrations.twitter.screenName }}</a></p>
diff --git a/packages/frontend/src/pages/settings/mute-block.vue b/packages/frontend/src/pages/settings/mute-block.vue
index 1cf33d34db..48579aa069 100644
--- a/packages/frontend/src/pages/settings/mute-block.vue
+++ b/packages/frontend/src/pages/settings/mute-block.vue
@@ -1,5 +1,5 @@
 <template>
-<div class="_formRoot">
+<div class="_gaps_m">
 	<MkTab v-model="tab" style="margin-bottom: var(--margin);">
 		<option value="mute">{{ i18n.ts.mutedUsers }}</option>
 		<option value="block">{{ i18n.ts.blockedUsers }}</option>
diff --git a/packages/frontend/src/pages/settings/navbar.vue b/packages/frontend/src/pages/settings/navbar.vue
index 0b2776ec90..0492f2e8af 100644
--- a/packages/frontend/src/pages/settings/navbar.vue
+++ b/packages/frontend/src/pages/settings/navbar.vue
@@ -1,11 +1,11 @@
 <template>
-<div class="_formRoot">
-	<FormTextarea v-model="items" tall manual-save class="_formBlock">
+<div class="_gaps_m">
+	<FormTextarea v-model="items" tall manual-save>
 		<template #label>{{ i18n.ts.navbar }}</template>
 		<template #caption><button class="_textButton" @click="addItem">{{ i18n.ts.addItem }}</button></template>
 	</FormTextarea>
 
-	<FormRadios v-model="menuDisplay" class="_formBlock">
+	<FormRadios v-model="menuDisplay">
 		<template #label>{{ i18n.ts.display }}</template>
 		<option value="sideFull">{{ i18n.ts._menuDisplay.sideFull }}</option>
 		<option value="sideIcon">{{ i18n.ts._menuDisplay.sideIcon }}</option>
@@ -13,7 +13,7 @@
 		<!-- <MkRadio v-model="menuDisplay" value="hide" disabled>{{ i18n.ts._menuDisplay.hide }}</MkRadio>--> <!-- TODO: サイドバーを完全に隠せるようにすると、別途ハンバーガーボタンのようなものをUIに表示する必要があり面倒 -->
 	</FormRadios>
 
-	<FormButton danger class="_formBlock" @click="reset()"><i class="ti ti-reload"></i> {{ i18n.ts.default }}</FormButton>
+	<MkButton danger @click="reset()"><i class="ti ti-reload"></i> {{ i18n.ts.default }}</MkButton>
 </div>
 </template>
 
@@ -21,7 +21,7 @@
 import { computed, ref, watch } from 'vue';
 import FormTextarea from '@/components/form/textarea.vue';
 import FormRadios from '@/components/form/radios.vue';
-import FormButton from '@/components/MkButton.vue';
+import MkButton from '@/components/MkButton.vue';
 import * as os from '@/os';
 import { navbarItemDef } from '@/navbar';
 import { defaultStore } from '@/store';
@@ -49,7 +49,7 @@ async function addItem() {
 	const { canceled, result: item } = await os.select({
 		title: i18n.ts.addItem,
 		items: [...menu.map(k => ({
-			value: k, text: i18n.ts[navbarItemDef[k].title],
+			value: k, text: navbarItemDef[k].title,
 		})), {
 			value: '-', text: i18n.ts.divider,
 		}],
diff --git a/packages/frontend/src/pages/settings/notifications.vue b/packages/frontend/src/pages/settings/notifications.vue
index c1b7130245..8799f44041 100644
--- a/packages/frontend/src/pages/settings/notifications.vue
+++ b/packages/frontend/src/pages/settings/notifications.vue
@@ -1,22 +1,27 @@
 <template>
-<div class="_formRoot">
-	<FormLink class="_formBlock" @click="configure"><template #icon><i class="ti ti-settings"></i></template>{{ i18n.ts.notificationSetting }}</FormLink>
+<div class="_gaps_m">
+	<FormLink @click="configure"><template #icon><i class="ti ti-settings"></i></template>{{ i18n.ts.notificationSetting }}</FormLink>
 	<FormSection>
-		<FormLink class="_formBlock" @click="readAllNotifications">{{ i18n.ts.markAsReadAllNotifications }}</FormLink>
-		<FormLink class="_formBlock" @click="readAllUnreadNotes">{{ i18n.ts.markAsReadAllUnreadNotes }}</FormLink>
-		<FormLink class="_formBlock" @click="readAllMessagingMessages">{{ i18n.ts.markAsReadAllTalkMessages }}</FormLink>
+		<div class="_gaps_m">
+			<FormLink @click="readAllNotifications">{{ i18n.ts.markAsReadAllNotifications }}</FormLink>
+			<FormLink @click="readAllUnreadNotes">{{ i18n.ts.markAsReadAllUnreadNotes }}</FormLink>
+			<FormLink @click="readAllMessagingMessages">{{ i18n.ts.markAsReadAllTalkMessages }}</FormLink>
+		</div>
 	</FormSection>
 	<FormSection>
 		<template #label>{{ i18n.ts.pushNotification }}</template>
-		<MkPushNotificationAllowButton ref="allowButton" />
-		<FormSwitch class="_formBlock" :disabled="!pushRegistrationInServer" :model-value="sendReadMessage" @update:model-value="onChangeSendReadMessage">
-			<template #label>{{ i18n.ts.sendPushNotificationReadMessage }}</template>
-			<template #caption>
-				<I18n :src="i18n.ts.sendPushNotificationReadMessageCaption">
-					<template #emptyPushNotificationMessage>{{ i18n.ts._notification.emptyPushNotificationMessage }}</template>
-				</I18n>
-			</template>
-		</FormSwitch>
+
+		<div class="_gaps_m">
+			<MkPushNotificationAllowButton ref="allowButton"/>
+			<FormSwitch :disabled="!pushRegistrationInServer" :model-value="sendReadMessage" @update:model-value="onChangeSendReadMessage">
+				<template #label>{{ i18n.ts.sendPushNotificationReadMessage }}</template>
+				<template #caption>
+					<I18n :src="i18n.ts.sendPushNotificationReadMessageCaption">
+						<template #emptyPushNotificationMessage>{{ i18n.ts._notification.emptyPushNotificationMessage }}</template>
+					</I18n>
+				</template>
+			</FormSwitch>
+		</div>
 	</FormSection>
 </div>
 </template>
@@ -24,7 +29,7 @@
 <script lang="ts" setup>
 import { defineAsyncComponent } from 'vue';
 import { notificationTypes } from 'misskey-js';
-import FormButton from '@/components/MkButton.vue';
+import MkButton from '@/components/MkButton.vue';
 import FormLink from '@/components/form/link.vue';
 import FormSection from '@/components/form/section.vue';
 import FormSwitch from '@/components/form/switch.vue';
diff --git a/packages/frontend/src/pages/settings/other.vue b/packages/frontend/src/pages/settings/other.vue
index 40bb202789..f3c1b3dc2d 100644
--- a/packages/frontend/src/pages/settings/other.vue
+++ b/packages/frontend/src/pages/settings/other.vue
@@ -1,18 +1,18 @@
 <template>
-<div class="_formRoot">
-	<FormSwitch v-model="$i.injectFeaturedNote" class="_formBlock" @update:model-value="onChangeInjectFeaturedNote">
+<div class="_gaps_m">
+	<FormSwitch v-model="$i.injectFeaturedNote" @update:model-value="onChangeInjectFeaturedNote">
 		{{ i18n.ts.showFeaturedNotesInTimeline }}
 	</FormSwitch>
 
 	<!--
-	<FormSwitch v-model="reportError" class="_formBlock">{{ i18n.ts.sendErrorReports }}<template #caption>{{ i18n.ts.sendErrorReportsDescription }}</template></FormSwitch>
+	<FormSwitch v-model="reportError">{{ i18n.ts.sendErrorReports }}<template #caption>{{ i18n.ts.sendErrorReportsDescription }}</template></FormSwitch>
 	-->
 
-	<FormLink to="/settings/account-info" class="_formBlock">{{ i18n.ts.accountInfo }}</FormLink>
+	<FormLink to="/settings/account-info">{{ i18n.ts.accountInfo }}</FormLink>
 
-	<FormLink to="/registry" class="_formBlock"><template #icon><i class="ti ti-adjustments"></i></template>{{ i18n.ts.registry }}</FormLink>
+	<FormLink to="/registry"><template #icon><i class="ti ti-adjustments"></i></template>{{ i18n.ts.registry }}</FormLink>
 
-	<FormLink to="/settings/delete-account" class="_formBlock"><template #icon><i class="ti ti-alert-triangle"></i></template>{{ i18n.ts.closeAccount }}</FormLink>
+	<FormLink to="/settings/delete-account"><template #icon><i class="ti ti-alert-triangle"></i></template>{{ i18n.ts.closeAccount }}</FormLink>
 </div>
 </template>
 
diff --git a/packages/frontend/src/pages/settings/plugin.install.vue b/packages/frontend/src/pages/settings/plugin.install.vue
index 40ad9a95dd..09bd9dd9e1 100644
--- a/packages/frontend/src/pages/settings/plugin.install.vue
+++ b/packages/frontend/src/pages/settings/plugin.install.vue
@@ -1,13 +1,13 @@
 <template>
-<div class="_formRoot">
-	<FormInfo warn class="_formBlock">{{ i18n.ts._plugin.installWarn }}</FormInfo>
+<div class="_gaps_m">
+	<FormInfo warn>{{ i18n.ts._plugin.installWarn }}</FormInfo>
 
-	<FormTextarea v-model="code" tall class="_formBlock">
+	<FormTextarea v-model="code" tall>
 		<template #label>{{ i18n.ts.code }}</template>
 	</FormTextarea>
 
-	<div class="_formBlock">
-		<FormButton :disabled="code == null" primary inline @click="install"><i class="ti ti-check"></i> {{ i18n.ts.install }}</FormButton>
+	<div>
+		<MkButton :disabled="code == null" primary inline @click="install"><i class="ti ti-check"></i> {{ i18n.ts.install }}</MkButton>
 	</div>
 </div>
 </template>
@@ -17,7 +17,7 @@ import { defineAsyncComponent, nextTick, ref } from 'vue';
 import { Interpreter, Parser, utils } from '@syuilo/aiscript';
 import { v4 as uuid } from 'uuid';
 import FormTextarea from '@/components/form/textarea.vue';
-import FormButton from '@/components/MkButton.vue';
+import MkButton from '@/components/MkButton.vue';
 import FormInfo from '@/components/MkInfo.vue';
 import * as os from '@/os';
 import { ColdDeviceStorage } from '@/store';
@@ -49,7 +49,7 @@ async function install() {
 			text: 'No language version annotation found :(',
 		});
 		return;
-	} else if (lv !== '0.12.0') {
+	} else if (!lv.startsWith('0.12.')) {
 		os.alert({
 			type: 'error',
 			text: `aiscript version '${lv}' is not supported :(`,
diff --git a/packages/frontend/src/pages/settings/plugin.vue b/packages/frontend/src/pages/settings/plugin.vue
index 905efd833d..c2f80ceac2 100644
--- a/packages/frontend/src/pages/settings/plugin.vue
+++ b/packages/frontend/src/pages/settings/plugin.vue
@@ -1,28 +1,28 @@
 <template>
-<div class="_formRoot">
+<div class="_gaps_m">
 	<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>
-		<div v-for="plugin in plugins" :key="plugin.id" class="_formBlock _panel" style="padding: 20px;">
+		<div v-for="plugin in plugins" :key="plugin.id" class="_panel _gaps_s" style="padding: 20px;">
 			<span style="display: flex;"><b>{{ plugin.name }}</b><span style="margin-left: auto;">v{{ plugin.version }}</span></span>
 
-			<FormSwitch class="_formBlock" :model-value="plugin.active" @update:model-value="changeActive(plugin, $event)">{{ i18n.ts.makeActive }}</FormSwitch>
+			<FormSwitch :model-value="plugin.active" @update:model-value="changeActive(plugin, $event)">{{ i18n.ts.makeActive }}</FormSwitch>
 
-			<MkKeyValue class="_formBlock">
+			<MkKeyValue>
 				<template #key>{{ i18n.ts.author }}</template>
 				<template #value>{{ plugin.author }}</template>
 			</MkKeyValue>
-			<MkKeyValue class="_formBlock">
+			<MkKeyValue>
 				<template #key>{{ i18n.ts.description }}</template>
 				<template #value>{{ plugin.description }}</template>
 			</MkKeyValue>
-			<MkKeyValue class="_formBlock">
+			<MkKeyValue>
 				<template #key>{{ i18n.ts.permission }}</template>
 				<template #value>{{ plugin.permission }}</template>
 			</MkKeyValue>
 
-			<div style="display: flex; gap: var(--margin); flex-wrap: wrap;">
+			<div class="_buttons">
 				<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>
diff --git a/packages/frontend/src/pages/settings/preferences-backups.vue b/packages/frontend/src/pages/settings/preferences-backups.vue
index a713c1262d..87a08612fc 100644
--- a/packages/frontend/src/pages/settings/preferences-backups.vue
+++ b/packages/frontend/src/pages/settings/preferences-backups.vue
@@ -1,5 +1,5 @@
 <template>
-<div class="_formRoot">
+<div class="_gaps_m">
 	<div :class="$style.buttons">
 		<MkButton inline primary @click="saveNew">{{ ts._preferencesBackups.saveNew }}</MkButton>
 		<MkButton inline @click="loadFile">{{ ts._preferencesBackups.loadFile }}</MkButton>
@@ -8,17 +8,19 @@
 	<FormSection>
 		<template #label>{{ ts._preferencesBackups.list }}</template>
 		<template v-if="profiles && Object.keys(profiles).length > 0">
-			<div
-				v-for="(profile, id) in profiles"
-				:key="id"
-				class="_formBlock _panel"
-				:class="$style.profile"
-				@click="$event => menu($event, id)"
-				@contextmenu.prevent.stop="$event => menu($event, id)"
-			>
-				<div :class="$style.profileName">{{ profile.name }}</div>
-				<div :class="$style.profileTime">{{ t('_preferencesBackups.createdAt', { date: (new Date(profile.createdAt)).toLocaleDateString(), time: (new Date(profile.createdAt)).toLocaleTimeString() }) }}</div>
-				<div v-if="profile.updatedAt" :class="$style.profileTime">{{ t('_preferencesBackups.updatedAt', { date: (new Date(profile.updatedAt)).toLocaleDateString(), time: (new Date(profile.updatedAt)).toLocaleTimeString() }) }}</div>
+			<div class="_gaps_s">
+				<div
+					v-for="(profile, id) in profiles"
+					:key="id"
+					class="_panel"
+					:class="$style.profile"
+					@click="$event => menu($event, id)"
+					@contextmenu.prevent.stop="$event => menu($event, id)"
+				>
+					<div :class="$style.profileName">{{ profile.name }}</div>
+					<div :class="$style.profileTime">{{ t('_preferencesBackups.createdAt', { date: (new Date(profile.createdAt)).toLocaleDateString(), time: (new Date(profile.createdAt)).toLocaleTimeString() }) }}</div>
+					<div v-if="profile.updatedAt" :class="$style.profileTime">{{ t('_preferencesBackups.updatedAt', { date: (new Date(profile.updatedAt)).toLocaleDateString(), time: (new Date(profile.updatedAt)).toLocaleTimeString() }) }}</div>
+				</div>
 			</div>
 		</template>
 		<div v-else-if="profiles">
@@ -43,6 +45,7 @@ import { $i } from '@/account';
 import { i18n } from '@/i18n';
 import { version, host } from '@/config';
 import { definePageMetadata } from '@/scripts/page-metadata';
+import { miLocalStorage } from '@/local-storage';
 const { t, ts } = i18n;
 
 useCssModule();
@@ -168,9 +171,9 @@ function getSettings(): Profile['settings'] {
 	return {
 		hot,
 		cold,
-		fontSize: localStorage.getItem('fontSize'),
-		useSystemFont: localStorage.getItem('useSystemFont') as 't' | null,
-		wallpaper: localStorage.getItem('wallpaper'),
+		fontSize: miLocalStorage.getItem('fontSize'),
+		useSystemFont: miLocalStorage.getItem('useSystemFont') as 't' | null,
+		wallpaper: miLocalStorage.getItem('wallpaper'),
 	};
 }
 
@@ -277,23 +280,23 @@ async function applyProfile(id: string): Promise<void> {
 
 	// fontSize
 	if (settings.fontSize) {
-		localStorage.setItem('fontSize', settings.fontSize);
+		miLocalStorage.setItem('fontSize', settings.fontSize);
 	} else {
-		localStorage.removeItem('fontSize');
+		miLocalStorage.removeItem('fontSize');
 	}
 
 	// useSystemFont
 	if (settings.useSystemFont) {
-		localStorage.setItem('useSystemFont', settings.useSystemFont);
+		miLocalStorage.setItem('useSystemFont', settings.useSystemFont);
 	} else {
-		localStorage.removeItem('useSystemFont');
+		miLocalStorage.removeItem('useSystemFont');
 	}
 
 	// wallpaper
 	if (settings.wallpaper != null) {
-		localStorage.setItem('wallpaper', settings.wallpaper);
+		miLocalStorage.setItem('wallpaper', settings.wallpaper);
 	} else {
-		localStorage.removeItem('wallpaper');
+		miLocalStorage.removeItem('wallpaper');
 	}
 
 	const { canceled: cancel2 } = await os.confirm({
diff --git a/packages/frontend/src/pages/settings/privacy.vue b/packages/frontend/src/pages/settings/privacy.vue
index 915ca05767..b0e59e7967 100644
--- a/packages/frontend/src/pages/settings/privacy.vue
+++ b/packages/frontend/src/pages/settings/privacy.vue
@@ -1,14 +1,14 @@
 <template>
-<div class="_formRoot">
-	<FormSwitch v-model="isLocked" class="_formBlock" @update:model-value="save()">{{ i18n.ts.makeFollowManuallyApprove }}<template #caption>{{ i18n.ts.lockedAccountInfo }}</template></FormSwitch>
-	<FormSwitch v-if="isLocked" v-model="autoAcceptFollowed" class="_formBlock" @update:model-value="save()">{{ i18n.ts.autoAcceptFollowed }}</FormSwitch>
+<div class="_gaps_m">
+	<FormSwitch v-model="isLocked" @update:model-value="save()">{{ i18n.ts.makeFollowManuallyApprove }}<template #caption>{{ i18n.ts.lockedAccountInfo }}</template></FormSwitch>
+	<FormSwitch v-if="isLocked" v-model="autoAcceptFollowed" @update:model-value="save()">{{ i18n.ts.autoAcceptFollowed }}</FormSwitch>
 
-	<FormSwitch v-model="publicReactions" class="_formBlock" @update:model-value="save()">
+	<FormSwitch v-model="publicReactions" @update:model-value="save()">
 		{{ i18n.ts.makeReactionsPublic }}
 		<template #caption>{{ i18n.ts.makeReactionsPublicDescription }}</template>
 	</FormSwitch>
 		
-	<FormSelect v-model="ffVisibility" class="_formBlock" @update:model-value="save()">
+	<FormSelect v-model="ffVisibility" @update:model-value="save()">
 		<template #label>{{ i18n.ts.ffVisibility }}</template>
 		<option value="public">{{ i18n.ts._ffVisibility.public }}</option>
 		<option value="followers">{{ i18n.ts._ffVisibility.followers }}</option>
@@ -16,39 +16,43 @@
 		<template #caption>{{ i18n.ts.ffVisibilityDescription }}</template>
 	</FormSelect>
 		
-	<FormSwitch v-model="hideOnlineStatus" class="_formBlock" @update:model-value="save()">
+	<FormSwitch v-model="hideOnlineStatus" @update:model-value="save()">
 		{{ i18n.ts.hideOnlineStatus }}
 		<template #caption>{{ i18n.ts.hideOnlineStatusDescription }}</template>
 	</FormSwitch>
-	<FormSwitch v-model="noCrawle" class="_formBlock" @update:model-value="save()">
+	<FormSwitch v-model="noCrawle" @update:model-value="save()">
 		{{ i18n.ts.noCrawle }}
 		<template #caption>{{ i18n.ts.noCrawleDescription }}</template>
 	</FormSwitch>
-	<FormSwitch v-model="isExplorable" class="_formBlock" @update:model-value="save()">
+	<FormSwitch v-model="isExplorable" @update:model-value="save()">
 		{{ i18n.ts.makeExplorable }}
 		<template #caption>{{ i18n.ts.makeExplorableDescription }}</template>
 	</FormSwitch>
 
 	<FormSection>
-		<FormSwitch v-model="rememberNoteVisibility" class="_formBlock" @update:model-value="save()">{{ i18n.ts.rememberNoteVisibility }}</FormSwitch>
-		<FormFolder v-if="!rememberNoteVisibility" class="_formBlock">
-			<template #label>{{ i18n.ts.defaultNoteVisibility }}</template>
-			<template v-if="defaultNoteVisibility === 'public'" #suffix>{{ i18n.ts._visibility.public }}</template>
-			<template v-else-if="defaultNoteVisibility === 'home'" #suffix>{{ i18n.ts._visibility.home }}</template>
-			<template v-else-if="defaultNoteVisibility === 'followers'" #suffix>{{ i18n.ts._visibility.followers }}</template>
-			<template v-else-if="defaultNoteVisibility === 'specified'" #suffix>{{ i18n.ts._visibility.specified }}</template>
+		<div class="_gaps_m">
+			<FormSwitch v-model="rememberNoteVisibility" @update:model-value="save()">{{ i18n.ts.rememberNoteVisibility }}</FormSwitch>
+			<FormFolder v-if="!rememberNoteVisibility">
+				<template #label>{{ i18n.ts.defaultNoteVisibility }}</template>
+				<template v-if="defaultNoteVisibility === 'public'" #suffix>{{ i18n.ts._visibility.public }}</template>
+				<template v-else-if="defaultNoteVisibility === 'home'" #suffix>{{ i18n.ts._visibility.home }}</template>
+				<template v-else-if="defaultNoteVisibility === 'followers'" #suffix>{{ i18n.ts._visibility.followers }}</template>
+				<template v-else-if="defaultNoteVisibility === 'specified'" #suffix>{{ i18n.ts._visibility.specified }}</template>
 
-			<FormSelect v-model="defaultNoteVisibility" class="_formBlock">
-				<option value="public">{{ i18n.ts._visibility.public }}</option>
-				<option value="home">{{ i18n.ts._visibility.home }}</option>
-				<option value="followers">{{ i18n.ts._visibility.followers }}</option>
-				<option value="specified">{{ i18n.ts._visibility.specified }}</option>
-			</FormSelect>
-			<FormSwitch v-model="defaultNoteLocalOnly" class="_formBlock">{{ i18n.ts._visibility.localOnly }}</FormSwitch>
-		</FormFolder>
+				<div class="_gaps_m">
+					<FormSelect v-model="defaultNoteVisibility">
+						<option value="public">{{ i18n.ts._visibility.public }}</option>
+						<option value="home">{{ i18n.ts._visibility.home }}</option>
+						<option value="followers">{{ i18n.ts._visibility.followers }}</option>
+						<option value="specified">{{ i18n.ts._visibility.specified }}</option>
+					</FormSelect>
+					<FormSwitch v-model="defaultNoteLocalOnly">{{ i18n.ts._visibility.localOnly }}</FormSwitch>
+				</div>
+			</FormFolder>
+		</div>
 	</FormSection>
 
-	<FormSwitch v-model="keepCw" class="_formBlock" @update:model-value="save()">{{ i18n.ts.keepCw }}</FormSwitch>
+	<FormSwitch v-model="keepCw" @update:model-value="save()">{{ i18n.ts.keepCw }}</FormSwitch>
 </div>
 </template>
 
diff --git a/packages/frontend/src/pages/settings/profile.vue b/packages/frontend/src/pages/settings/profile.vue
index 14eeeaaa11..9727cf9c95 100644
--- a/packages/frontend/src/pages/settings/profile.vue
+++ b/packages/frontend/src/pages/settings/profile.vue
@@ -1,5 +1,5 @@
 <template>
-<div class="_formRoot">
+<div class="_gaps_m">
 	<div class="llvierxe" :style="{ backgroundImage: $i.bannerUrl ? `url(${ $i.bannerUrl })` : null }">
 		<div class="avatar">
 			<MkAvatar class="avatar" :user="$i" :disable-link="true" @click="changeAvatar"/>
@@ -8,37 +8,37 @@
 		<MkButton primary rounded class="bannerEdit" @click="changeBanner">{{ i18n.ts._profile.changeBanner }}</MkButton>
 	</div>
 
-	<FormInput v-model="profile.name" :max="30" manual-save class="_formBlock">
+	<FormInput v-model="profile.name" :max="30" manual-save>
 		<template #label>{{ i18n.ts._profile.name }}</template>
 	</FormInput>
 
-	<FormTextarea v-model="profile.description" :max="500" tall manual-save class="_formBlock">
+	<FormTextarea v-model="profile.description" :max="500" tall manual-save>
 		<template #label>{{ i18n.ts._profile.description }}</template>
 		<template #caption>{{ i18n.ts._profile.youCanIncludeHashtags }}</template>
 	</FormTextarea>
 
-	<FormInput v-model="profile.location" manual-save class="_formBlock">
+	<FormInput v-model="profile.location" manual-save>
 		<template #label>{{ i18n.ts.location }}</template>
 		<template #prefix><i class="ti ti-map-pin"></i></template>
 	</FormInput>
 
-	<FormInput v-model="profile.birthday" type="date" manual-save class="_formBlock">
+	<FormInput v-model="profile.birthday" type="date" manual-save>
 		<template #label>{{ i18n.ts.birthday }}</template>
 		<template #prefix><i class="ti ti-cake"></i></template>
 	</FormInput>
 
-	<FormSelect v-model="profile.lang" class="_formBlock">
+	<FormSelect v-model="profile.lang">
 		<template #label>{{ i18n.ts.language }}</template>
 		<option v-for="x in Object.keys(langmap)" :key="x" :value="x">{{ langmap[x].nativeName }}</option>
 	</FormSelect>
 
-	<FormSlot class="_formBlock">
+	<FormSlot>
 		<FormFolder>
 			<template #icon><i class="ti ti-list"></i></template>
 			<template #label>{{ i18n.ts._profile.metadataEdit }}</template>
 
-			<div class="_formRoot">
-				<FormSplit v-for="(record, i) in fields" :min-width="250" class="_formBlock">
+			<div class="_gaps_m">
+				<FormSplit v-for="(record, i) in fields" :min-width="250">
 					<FormInput v-model="record.name" small>
 						<template #label>{{ i18n.ts._profile.metadataLabel }} #{{ i + 1 }}</template>
 					</FormInput>
@@ -46,8 +46,10 @@
 						<template #label>{{ i18n.ts._profile.metadataContent }} #{{ i + 1 }}</template>
 					</FormInput>
 				</FormSplit>
-				<MkButton :disabled="fields.length >= 16" inline style="margin-right: 8px;" @click="addField"><i class="ti ti-plus"></i> {{ i18n.ts.add }}</MkButton>
-				<MkButton inline primary @click="saveFields"><i class="ti ti-check"></i> {{ i18n.ts.save }}</MkButton>
+				<div>
+					<MkButton :disabled="fields.length >= 16" inline style="margin-right: 8px;" @click="addField"><i class="ti ti-plus"></i> {{ i18n.ts.add }}</MkButton>
+					<MkButton inline primary @click="saveFields"><i class="ti ti-check"></i> {{ i18n.ts.save }}</MkButton>
+				</div>
 			</div>
 		</FormFolder>
 		<template #caption>{{ i18n.ts._profile.metadataDescription }}</template>
@@ -56,13 +58,13 @@
 	<FormFolder>
 		<template #label>{{ i18n.ts.advancedSettings }}</template>
 
-		<div class="_formRoot">
-			<FormSwitch v-model="profile.isCat" class="_formBlock">{{ i18n.ts.flagAsCat }}<template #caption>{{ i18n.ts.flagAsCatDescription }}</template></FormSwitch>
-			<FormSwitch v-model="profile.isBot" class="_formBlock">{{ i18n.ts.flagAsBot }}<template #caption>{{ i18n.ts.flagAsBotDescription }}</template></FormSwitch>
+		<div class="_gaps_m">
+			<FormSwitch v-model="profile.isCat">{{ i18n.ts.flagAsCat }}<template #caption>{{ i18n.ts.flagAsCatDescription }}</template></FormSwitch>
+			<FormSwitch v-model="profile.isBot">{{ i18n.ts.flagAsBot }}<template #caption>{{ i18n.ts.flagAsBotDescription }}</template></FormSwitch>
 		</div>
 	</FormFolder>
 
-	<FormSwitch v-model="profile.showTimelineReplies" class="_formBlock">{{ i18n.ts.flagShowTimelineReplies }}<template #caption>{{ i18n.ts.flagShowTimelineRepliesDescription }} {{ i18n.ts.reflectMayTakeTime }}</template></FormSwitch>
+	<FormSwitch v-model="profile.showTimelineReplies">{{ i18n.ts.flagShowTimelineReplies }}<template #caption>{{ i18n.ts.flagShowTimelineRepliesDescription }} {{ i18n.ts.reflectMayTakeTime }}</template></FormSwitch>
 </div>
 </template>
 
diff --git a/packages/frontend/src/pages/settings/reaction.vue b/packages/frontend/src/pages/settings/reaction.vue
index 2748cd7d4e..95c3a8f5bb 100644
--- a/packages/frontend/src/pages/settings/reaction.vue
+++ b/packages/frontend/src/pages/settings/reaction.vue
@@ -1,6 +1,6 @@
 <template>
-<div class="_formRoot">
-	<FromSlot class="_formBlock">
+<div class="_gaps_m">
+	<FromSlot>
 		<template #label>{{ i18n.ts.reactionSettingDescription }}</template>
 		<div v-panel style="border-radius: 6px;">
 			<Sortable v-model="reactions" class="zoaiodol" :item-key="item => item" :animation="150" :delay="100" :delay-on-touch-only="true">
@@ -17,13 +17,13 @@
 		<template #caption>{{ i18n.ts.reactionSettingDescription2 }} <button class="_textButton" @click="preview">{{ i18n.ts.preview }}</button></template>
 	</FromSlot>
 
-	<FormRadios v-model="reactionPickerSize" class="_formBlock">
+	<FormRadios v-model="reactionPickerSize">
 		<template #label>{{ i18n.ts.size }}</template>
 		<option :value="1">{{ i18n.ts.small }}</option>
 		<option :value="2">{{ i18n.ts.medium }}</option>
 		<option :value="3">{{ i18n.ts.large }}</option>
 	</FormRadios>
-	<FormRadios v-model="reactionPickerWidth" class="_formBlock">
+	<FormRadios v-model="reactionPickerWidth">
 		<template #label>{{ i18n.ts.numberOfColumn }}</template>
 		<option :value="1">5</option>
 		<option :value="2">6</option>
@@ -31,7 +31,7 @@
 		<option :value="4">8</option>
 		<option :value="5">9</option>
 	</FormRadios>
-	<FormRadios v-model="reactionPickerHeight" class="_formBlock">
+	<FormRadios v-model="reactionPickerHeight">
 		<template #label>{{ i18n.ts.height }}</template>
 		<option :value="1">{{ i18n.ts.small }}</option>
 		<option :value="2">{{ i18n.ts.medium }}</option>
@@ -39,15 +39,15 @@
 		<option :value="4">{{ i18n.ts.large }}+</option>
 	</FormRadios>
 
-	<FormSwitch v-model="reactionPickerUseDrawerForMobile" class="_formBlock">
+	<FormSwitch v-model="reactionPickerUseDrawerForMobile">
 		{{ i18n.ts.useDrawerReactionPickerForMobile }}
 		<template #caption>{{ i18n.ts.needReloadToApply }}</template>
 	</FormSwitch>
 
 	<FormSection>
-		<div style="display: flex; gap: var(--margin); flex-wrap: wrap;">
-			<FormButton inline @click="preview"><i class="ti ti-eye"></i> {{ i18n.ts.preview }}</FormButton>
-			<FormButton inline danger @click="setDefault"><i class="ti ti-reload"></i> {{ i18n.ts.default }}</FormButton>
+		<div class="_buttons">
+			<MkButton inline @click="preview"><i class="ti ti-eye"></i> {{ i18n.ts.preview }}</MkButton>
+			<MkButton inline danger @click="setDefault"><i class="ti ti-reload"></i> {{ i18n.ts.default }}</MkButton>
 		</div>
 	</FormSection>
 </div>
@@ -59,7 +59,7 @@ import Sortable from 'vuedraggable';
 import FormInput from '@/components/form/input.vue';
 import FormRadios from '@/components/form/radios.vue';
 import FromSlot from '@/components/form/slot.vue';
-import FormButton from '@/components/MkButton.vue';
+import MkButton from '@/components/MkButton.vue';
 import FormSection from '@/components/form/section.vue';
 import FormSwitch from '@/components/form/switch.vue';
 import * as os from '@/os';
diff --git a/packages/frontend/src/pages/settings/security.vue b/packages/frontend/src/pages/settings/security.vue
index 33f49eb3ef..0f9ef99713 100644
--- a/packages/frontend/src/pages/settings/security.vue
+++ b/packages/frontend/src/pages/settings/security.vue
@@ -1,8 +1,8 @@
 <template>
-<div class="_formRoot">
-	<FormSection>
+<div class="_gaps_m">
+	<FormSection first>
 		<template #label>{{ i18n.ts.password }}</template>
-		<FormButton primary @click="change()">{{ i18n.ts.changePassword }}</FormButton>
+		<MkButton primary @click="change()">{{ i18n.ts.changePassword }}</MkButton>
 	</FormSection>
 
 	<FormSection>
@@ -30,7 +30,7 @@
 
 	<FormSection>
 		<FormSlot>
-			<FormButton danger @click="regenerateToken"><i class="ti ti-refresh"></i> {{ i18n.ts.regenerateLoginToken }}</FormButton>
+			<MkButton danger @click="regenerateToken"><i class="ti ti-refresh"></i> {{ i18n.ts.regenerateLoginToken }}</MkButton>
 			<template #caption>{{ i18n.ts.regenerateLoginTokenDescription }}</template>
 		</FormSlot>
 	</FormSection>
@@ -41,7 +41,7 @@
 import X2fa from './2fa.vue';
 import FormSection from '@/components/form/section.vue';
 import FormSlot from '@/components/form/slot.vue';
-import FormButton from '@/components/MkButton.vue';
+import MkButton from '@/components/MkButton.vue';
 import MkPagination from '@/components/MkPagination.vue';
 import * as os from '@/os';
 import { i18n } from '@/i18n';
diff --git a/packages/frontend/src/pages/settings/sounds.sound.vue b/packages/frontend/src/pages/settings/sounds.sound.vue
index 62627c6333..23ecea86cc 100644
--- a/packages/frontend/src/pages/settings/sounds.sound.vue
+++ b/packages/frontend/src/pages/settings/sounds.sound.vue
@@ -1,16 +1,16 @@
 <template>
-<div class="_formRoot">
+<div class="_gaps_m">
 	<FormSelect v-model="type">
 		<template #label>{{ i18n.ts.sound }}</template>
 		<option v-for="x in soundsTypes" :key="x" :value="x">{{ x == null ? i18n.ts.none : x }}</option>
 	</FormSelect>
-	<FormRange v-model="volume" :min="0" :max="1" :step="0.05" :text-converter="(v) => `${Math.floor(v * 100)}%`" class="_formBlock">
+	<FormRange v-model="volume" :min="0" :max="1" :step="0.05" :text-converter="(v) => `${Math.floor(v * 100)}%`">
 		<template #label>{{ i18n.ts.volume }}</template>
 	</FormRange>
 
-	<div style="display: flex; gap: var(--margin); flex-wrap: wrap;">
-		<FormButton inline @click="listen"><i class="ti ti-player-play"></i> {{ i18n.ts.listen }}</FormButton>
-		<FormButton inline primary @click="save"><i class="ti ti-check"></i> {{ i18n.ts.save }}</FormButton>
+	<div class="_buttons">
+		<MkButton inline @click="listen"><i class="ti ti-player-play"></i> {{ i18n.ts.listen }}</MkButton>
+		<MkButton inline primary @click="save"><i class="ti ti-check"></i> {{ i18n.ts.save }}</MkButton>
 	</div>
 </div>
 </template>
@@ -18,7 +18,7 @@
 <script lang="ts" setup>
 import { } from 'vue';
 import FormSelect from '@/components/form/select.vue';
-import FormButton from '@/components/MkButton.vue';
+import MkButton from '@/components/MkButton.vue';
 import FormRange from '@/components/form/range.vue';
 import { i18n } from '@/i18n';
 import { playFile, soundsTypes } from '@/scripts/sound';
diff --git a/packages/frontend/src/pages/settings/sounds.vue b/packages/frontend/src/pages/settings/sounds.vue
index ef60b2c3c9..dae6ad3037 100644
--- a/packages/frontend/src/pages/settings/sounds.vue
+++ b/packages/frontend/src/pages/settings/sounds.vue
@@ -1,20 +1,22 @@
 <template>
-<div class="_formRoot">
-	<FormRange v-model="masterVolume" :min="0" :max="1" :step="0.05" :text-converter="(v) => `${Math.floor(v * 100)}%`" class="_formBlock">
+<div class="_gaps_m">
+	<FormRange v-model="masterVolume" :min="0" :max="1" :step="0.05" :text-converter="(v) => `${Math.floor(v * 100)}%`">
 		<template #label>{{ i18n.ts.masterVolume }}</template>
 	</FormRange>
 
 	<FormSection>
 		<template #label>{{ i18n.ts.sounds }}</template>
-		<FormFolder v-for="type in Object.keys(sounds)" :key="type" style="margin-bottom: 8px;">
-			<template #label>{{ $t('_sfx.' + type) }}</template>
-			<template #suffix>{{ sounds[type].type ?? i18n.ts.none }}</template>
+		<div class="_gaps_s">
+			<FormFolder v-for="type in Object.keys(sounds)" :key="type">
+				<template #label>{{ $t('_sfx.' + type) }}</template>
+				<template #suffix>{{ sounds[type].type ?? i18n.ts.none }}</template>
 
-			<XSound :type="sounds[type].type" :volume="sounds[type].volume" @update="(res) => updated(type, res)"/>
-		</FormFolder>
+				<XSound :type="sounds[type].type" :volume="sounds[type].volume" @update="(res) => updated(type, res)"/>
+			</FormFolder>
+		</div>
 	</FormSection>
 
-	<FormButton danger class="_formBlock" @click="reset()"><i class="ti ti-reload"></i> {{ i18n.ts.default }}</FormButton>
+	<MkButton danger @click="reset()"><i class="ti ti-reload"></i> {{ i18n.ts.default }}</MkButton>
 </div>
 </template>
 
@@ -22,7 +24,7 @@
 import { computed, ref } from 'vue';
 import XSound from './sounds.sound.vue';
 import FormRange from '@/components/form/range.vue';
-import FormButton from '@/components/MkButton.vue';
+import MkButton from '@/components/MkButton.vue';
 import FormLink from '@/components/form/link.vue';
 import FormSection from '@/components/form/section.vue';
 import FormFolder from '@/components/form/folder.vue';
diff --git a/packages/frontend/src/pages/settings/statusbar.statusbar.vue b/packages/frontend/src/pages/settings/statusbar.statusbar.vue
index 608222386e..890f92672e 100644
--- a/packages/frontend/src/pages/settings/statusbar.statusbar.vue
+++ b/packages/frontend/src/pages/settings/statusbar.statusbar.vue
@@ -1,21 +1,21 @@
 <template>
-<div class="_formRoot">
-	<FormSelect v-model="statusbar.type" placeholder="Please select" class="_formBlock">
+<div class="_gaps_m">
+	<FormSelect v-model="statusbar.type" placeholder="Please select">
 		<template #label>{{ i18n.ts.type }}</template>
 		<option value="rss">RSS</option>
 		<option value="federation">Federation</option>
 		<option value="userList">User list timeline</option>
 	</FormSelect>
 
-	<MkInput v-model="statusbar.name" manual-save class="_formBlock">
+	<MkInput v-model="statusbar.name" manual-save>
 		<template #label>{{ i18n.ts.label }}</template>
 	</MkInput>
 
-	<MkSwitch v-model="statusbar.black" class="_formBlock">
+	<MkSwitch v-model="statusbar.black">
 		<template #label>Black</template>
 	</MkSwitch>
 
-	<FormRadios v-model="statusbar.size" class="_formBlock">
+	<FormRadios v-model="statusbar.size">
 		<template #label>{{ i18n.ts.size }}</template>
 		<option value="verySmall">{{ i18n.ts.small }}+</option>
 		<option value="small">{{ i18n.ts.small }}</option>
@@ -25,57 +25,57 @@
 	</FormRadios>
 
 	<template v-if="statusbar.type === 'rss'">
-		<MkInput v-model="statusbar.props.url" manual-save class="_formBlock" type="url">
+		<MkInput v-model="statusbar.props.url" manual-save type="url">
 			<template #label>URL</template>
 		</MkInput>
-		<MkSwitch v-model="statusbar.props.shuffle" class="_formBlock">
+		<MkSwitch v-model="statusbar.props.shuffle">
 			<template #label>{{ i18n.ts.shuffle }}</template>
 		</MkSwitch>
-		<MkInput v-model="statusbar.props.refreshIntervalSec" manual-save class="_formBlock" type="number">
+		<MkInput v-model="statusbar.props.refreshIntervalSec" manual-save type="number">
 			<template #label>{{ i18n.ts.refreshInterval }}</template>
 		</MkInput>
-		<FormRange v-model="statusbar.props.marqueeDuration" :min="5" :max="150" :step="1" class="_formBlock">
+		<FormRange v-model="statusbar.props.marqueeDuration" :min="5" :max="150" :step="1">
 			<template #label>{{ i18n.ts.speed }}</template>
 			<template #caption>{{ i18n.ts.fast }} &lt;-&gt; {{ i18n.ts.slow }}</template>
 		</FormRange>
-		<MkSwitch v-model="statusbar.props.marqueeReverse" class="_formBlock">
+		<MkSwitch v-model="statusbar.props.marqueeReverse">
 			<template #label>{{ i18n.ts.reverse }}</template>
 		</MkSwitch>
 	</template>
 	<template v-else-if="statusbar.type === 'federation'">
-		<MkInput v-model="statusbar.props.refreshIntervalSec" manual-save class="_formBlock" type="number">
+		<MkInput v-model="statusbar.props.refreshIntervalSec" manual-save type="number">
 			<template #label>{{ i18n.ts.refreshInterval }}</template>
 		</MkInput>
-		<FormRange v-model="statusbar.props.marqueeDuration" :min="5" :max="150" :step="1" class="_formBlock">
+		<FormRange v-model="statusbar.props.marqueeDuration" :min="5" :max="150" :step="1">
 			<template #label>{{ i18n.ts.speed }}</template>
 			<template #caption>{{ i18n.ts.fast }} &lt;-&gt; {{ i18n.ts.slow }}</template>
 		</FormRange>
-		<MkSwitch v-model="statusbar.props.marqueeReverse" class="_formBlock">
+		<MkSwitch v-model="statusbar.props.marqueeReverse">
 			<template #label>{{ i18n.ts.reverse }}</template>
 		</MkSwitch>
-		<MkSwitch v-model="statusbar.props.colored" class="_formBlock">
+		<MkSwitch v-model="statusbar.props.colored">
 			<template #label>{{ i18n.ts.colored }}</template>
 		</MkSwitch>
 	</template>
 	<template v-else-if="statusbar.type === 'userList' && userLists != null">
-		<FormSelect v-model="statusbar.props.userListId" class="_formBlock">
+		<FormSelect v-model="statusbar.props.userListId">
 			<template #label>{{ i18n.ts.userList }}</template>
 			<option v-for="list in userLists" :value="list.id">{{ list.name }}</option>
 		</FormSelect>
-		<MkInput v-model="statusbar.props.refreshIntervalSec" manual-save class="_formBlock" type="number">
+		<MkInput v-model="statusbar.props.refreshIntervalSec" manual-save type="number">
 			<template #label>{{ i18n.ts.refreshInterval }}</template>
 		</MkInput>
-		<FormRange v-model="statusbar.props.marqueeDuration" :min="5" :max="150" :step="1" class="_formBlock">
+		<FormRange v-model="statusbar.props.marqueeDuration" :min="5" :max="150" :step="1">
 			<template #label>{{ i18n.ts.speed }}</template>
 			<template #caption>{{ i18n.ts.fast }} &lt;-&gt; {{ i18n.ts.slow }}</template>
 		</FormRange>
-		<MkSwitch v-model="statusbar.props.marqueeReverse" class="_formBlock">
+		<MkSwitch v-model="statusbar.props.marqueeReverse">
 			<template #label>{{ i18n.ts.reverse }}</template>
 		</MkSwitch>
 	</template>
 
-	<div style="display: flex; gap: var(--margin); flex-wrap: wrap;">
-		<FormButton danger @click="del">{{ i18n.ts.remove }}</FormButton>
+	<div class="_buttons">
+		<MkButton danger @click="del">{{ i18n.ts.remove }}</MkButton>
 	</div>
 </div>
 </template>
@@ -86,7 +86,7 @@ import FormSelect from '@/components/form/select.vue';
 import MkInput from '@/components/form/input.vue';
 import MkSwitch from '@/components/form/switch.vue';
 import FormRadios from '@/components/form/radios.vue';
-import FormButton from '@/components/MkButton.vue';
+import MkButton from '@/components/MkButton.vue';
 import FormRange from '@/components/form/range.vue';
 import * as os from '@/os';
 import { defaultStore } from '@/store';
diff --git a/packages/frontend/src/pages/settings/statusbar.vue b/packages/frontend/src/pages/settings/statusbar.vue
index 86c69fa2c3..26cb75e938 100644
--- a/packages/frontend/src/pages/settings/statusbar.vue
+++ b/packages/frontend/src/pages/settings/statusbar.vue
@@ -1,11 +1,11 @@
 <template>
-<div class="_formRoot">
-	<FormFolder v-for="x in statusbars" :key="x.id" class="_formBlock">
+<div class="_gaps_m">
+	<FormFolder v-for="x in statusbars" :key="x.id">
 		<template #label>{{ x.type ?? i18n.ts.notSet }}</template>
 		<template #suffix>{{ x.name }}</template>
 		<XStatusbar :_id="x.id" :user-lists="userLists"/>
 	</FormFolder>
-	<FormButton primary @click="add">{{ i18n.ts.add }}</FormButton>
+	<MkButton primary @click="add">{{ i18n.ts.add }}</MkButton>
 </div>
 </template>
 
@@ -15,7 +15,7 @@ import { v4 as uuid } from 'uuid';
 import XStatusbar from './statusbar.statusbar.vue';
 import FormRadios from '@/components/form/radios.vue';
 import FormFolder from '@/components/form/folder.vue';
-import FormButton from '@/components/MkButton.vue';
+import MkButton from '@/components/MkButton.vue';
 import * as os from '@/os';
 import { defaultStore } from '@/store';
 import { unisonReload } from '@/scripts/unison-reload';
diff --git a/packages/frontend/src/pages/settings/theme.install.vue b/packages/frontend/src/pages/settings/theme.install.vue
index 52a436e18d..633792195b 100644
--- a/packages/frontend/src/pages/settings/theme.install.vue
+++ b/packages/frontend/src/pages/settings/theme.install.vue
@@ -1,12 +1,12 @@
 <template>
-<div class="_formRoot">
-	<FormTextarea v-model="installThemeCode" class="_formBlock">
+<div class="_gaps_m">
+	<FormTextarea v-model="installThemeCode">
 		<template #label>{{ i18n.ts._theme.code }}</template>
 	</FormTextarea>
 
-	<div class="_formBlock" style="display: flex; gap: var(--margin); flex-wrap: wrap;">
-		<FormButton :disabled="installThemeCode == null" inline @click="() => preview(installThemeCode)"><i class="ti ti-eye"></i> {{ i18n.ts.preview }}</FormButton>
-		<FormButton :disabled="installThemeCode == null" primary inline @click="() => install(installThemeCode)"><i class="ti ti-check"></i> {{ i18n.ts.install }}</FormButton>
+	<div class="_buttons">
+		<MkButton :disabled="installThemeCode == null" inline @click="() => preview(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>
@@ -15,7 +15,7 @@
 import { } from 'vue';
 import JSON5 from 'json5';
 import FormTextarea from '@/components/form/textarea.vue';
-import FormButton from '@/components/MkButton.vue';
+import MkButton from '@/components/MkButton.vue';
 import { applyTheme, validateTheme } from '@/scripts/theme';
 import * as os from '@/os';
 import { addTheme, getThemes } from '@/theme-store';
diff --git a/packages/frontend/src/pages/settings/theme.manage.vue b/packages/frontend/src/pages/settings/theme.manage.vue
index 409f0af650..215482065a 100644
--- a/packages/frontend/src/pages/settings/theme.manage.vue
+++ b/packages/frontend/src/pages/settings/theme.manage.vue
@@ -1,6 +1,6 @@
 <template>
-<div class="_formRoot">
-	<FormSelect v-model="selectedThemeId" class="_formBlock">
+<div class="_gaps_m">
+	<FormSelect v-model="selectedThemeId">
 		<template #label>{{ i18n.ts.theme }}</template>
 		<optgroup :label="i18n.ts._theme.installedThemes">
 			<option v-for="x in installedThemes" :key="x.id" :value="x.id">{{ x.name }}</option>
@@ -10,17 +10,17 @@
 		</optgroup>
 	</FormSelect>
 	<template v-if="selectedTheme">
-		<FormInput readonly :model-value="selectedTheme.author" class="_formBlock">
+		<FormInput readonly :model-value="selectedTheme.author">
 			<template #label>{{ i18n.ts.author }}</template>
 		</FormInput>
-		<FormTextarea v-if="selectedTheme.desc" readonly :model-value="selectedTheme.desc" class="_formBlock">
+		<FormTextarea v-if="selectedTheme.desc" readonly :model-value="selectedTheme.desc">
 			<template #label>{{ i18n.ts._theme.description }}</template>
 		</FormTextarea>
-		<FormTextarea readonly tall :model-value="selectedThemeCode" class="_formBlock">
+		<FormTextarea readonly tall :model-value="selectedThemeCode">
 			<template #label>{{ i18n.ts._theme.code }}</template>
 			<template #caption><button class="_textButton" @click="copyThemeCode()">{{ i18n.ts.copy }}</button></template>
 		</FormTextarea>
-		<FormButton v-if="!builtinThemes.some(t => t.id == selectedTheme.id)" class="_formBlock" danger @click="uninstall()"><i class="ti ti-trash"></i> {{ i18n.ts.uninstall }}</FormButton>
+		<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>
@@ -31,7 +31,7 @@ import JSON5 from 'json5';
 import FormTextarea from '@/components/form/textarea.vue';
 import FormSelect from '@/components/form/select.vue';
 import FormInput from '@/components/form/input.vue';
-import FormButton from '@/components/MkButton.vue';
+import MkButton from '@/components/MkButton.vue';
 import { Theme, getBuiltinThemesRef } from '@/scripts/theme';
 import copyToClipboard from '@/scripts/copy-to-clipboard';
 import * as os from '@/os';
diff --git a/packages/frontend/src/pages/settings/theme.vue b/packages/frontend/src/pages/settings/theme.vue
index f37c213b06..a2dc9bc95f 100644
--- a/packages/frontend/src/pages/settings/theme.vue
+++ b/packages/frontend/src/pages/settings/theme.vue
@@ -1,6 +1,6 @@
 <template>
-<div class="_formRoot rsljpzjq">
-	<div v-adaptive-border class="rfqxtzch _panel _formBlock">
+<div class="_gaps_m rsljpzjq">
+	<div v-adaptive-border class="rfqxtzch _panel">
 		<div class="toggle">
 			<div class="toggleWrapper">
 				<input id="dn" v-model="darkMode" type="checkbox" class="dn"/>
@@ -26,7 +26,7 @@
 		</div>
 	</div>
 
-	<div class="selects _formBlock">
+	<div class="selects">
 		<FormSelect v-model="lightThemeId" large class="select">
 			<template #label>{{ i18n.ts.themeForLightMode }}</template>
 			<template #prefix><i class="ti ti-sun"></i></template>
@@ -60,8 +60,8 @@
 		</div>
 	</FormSection>
 
-	<FormButton v-if="wallpaper == null" class="_formBlock" @click="setWallpaper">{{ i18n.ts.setWallpaper }}</FormButton>
-	<FormButton v-else class="_formBlock" @click="wallpaper = null">{{ i18n.ts.removeWallpaper }}</FormButton>
+	<MkButton v-if="wallpaper == null" @click="setWallpaper">{{ i18n.ts.setWallpaper }}</MkButton>
+	<MkButton v-else @click="wallpaper = null">{{ i18n.ts.removeWallpaper }}</MkButton>
 </div>
 </template>
 
@@ -72,7 +72,7 @@ import FormSwitch from '@/components/form/switch.vue';
 import FormSelect from '@/components/form/select.vue';
 import FormSection from '@/components/form/section.vue';
 import FormLink from '@/components/form/link.vue';
-import FormButton from '@/components/MkButton.vue';
+import MkButton from '@/components/MkButton.vue';
 import { getBuiltinThemesRef } from '@/scripts/theme';
 import { selectFile } from '@/scripts/select-file';
 import { isDeviceDarkmode } from '@/scripts/is-device-darkmode';
@@ -82,6 +82,7 @@ import { instance } from '@/instance';
 import { uniqueBy } from '@/scripts/array';
 import { fetchThemes, getThemes } from '@/theme-store';
 import { definePageMetadata } from '@/scripts/page-metadata';
+import { miLocalStorage } from '@/local-storage';
 
 const installedThemes = ref(getThemes());
 const builtinThemes = getBuiltinThemesRef();
@@ -120,7 +121,7 @@ const lightThemeId = computed({
 });
 const darkMode = computed(defaultStore.makeGetterSetter('darkMode'));
 const syncDeviceDarkMode = computed(ColdDeviceStorage.makeGetterSetter('syncDeviceDarkMode'));
-const wallpaper = ref(localStorage.getItem('wallpaper'));
+const wallpaper = ref(miLocalStorage.getItem('wallpaper'));
 const themesCount = installedThemes.value.length;
 
 watch(syncDeviceDarkMode, () => {
@@ -131,9 +132,9 @@ watch(syncDeviceDarkMode, () => {
 
 watch(wallpaper, () => {
 	if (wallpaper.value == null) {
-		localStorage.removeItem('wallpaper');
+		miLocalStorage.removeItem('wallpaper');
 	} else {
-		localStorage.setItem('wallpaper', wallpaper.value);
+		miLocalStorage.setItem('wallpaper', wallpaper.value);
 	}
 	location.reload();
 });
diff --git a/packages/frontend/src/pages/settings/webhook.edit.vue b/packages/frontend/src/pages/settings/webhook.edit.vue
index c8ec1ea586..8c8492ba5f 100644
--- a/packages/frontend/src/pages/settings/webhook.edit.vue
+++ b/packages/frontend/src/pages/settings/webhook.edit.vue
@@ -1,14 +1,14 @@
 <template>
-<div class="_formRoot">
-	<FormInput v-model="name" class="_formBlock">
+<div class="_gaps_m">
+	<FormInput v-model="name">
 		<template #label>Name</template>
 	</FormInput>
 
-	<FormInput v-model="url" type="url" class="_formBlock">
+	<FormInput v-model="url" type="url">
 		<template #label>URL</template>
 	</FormInput>
 
-	<FormInput v-model="secret" class="_formBlock">
+	<FormInput v-model="secret">
 		<template #prefix><i class="ti ti-lock"></i></template>
 		<template #label>Secret</template>
 	</FormInput>
@@ -16,19 +16,21 @@
 	<FormSection>
 		<template #label>Events</template>
 
-		<FormSwitch v-model="event_follow" class="_formBlock">Follow</FormSwitch>
-		<FormSwitch v-model="event_followed" class="_formBlock">Followed</FormSwitch>
-		<FormSwitch v-model="event_note" class="_formBlock">Note</FormSwitch>
-		<FormSwitch v-model="event_reply" class="_formBlock">Reply</FormSwitch>
-		<FormSwitch v-model="event_renote" class="_formBlock">Renote</FormSwitch>
-		<FormSwitch v-model="event_reaction" class="_formBlock">Reaction</FormSwitch>
-		<FormSwitch v-model="event_mention" class="_formBlock">Mention</FormSwitch>
+		<div class="_gaps_s">
+			<FormSwitch v-model="event_follow">Follow</FormSwitch>
+			<FormSwitch v-model="event_followed">Followed</FormSwitch>
+			<FormSwitch v-model="event_note">Note</FormSwitch>
+			<FormSwitch v-model="event_reply">Reply</FormSwitch>
+			<FormSwitch v-model="event_renote">Renote</FormSwitch>
+			<FormSwitch v-model="event_reaction">Reaction</FormSwitch>
+			<FormSwitch v-model="event_mention">Mention</FormSwitch>
+		</div>
 	</FormSection>
 
-	<FormSwitch v-model="active" class="_formBlock">Active</FormSwitch>
+	<FormSwitch v-model="active">Active</FormSwitch>
 
-	<div class="_formBlock" style="display: flex; gap: var(--margin); flex-wrap: wrap;">
-		<FormButton primary inline @click="save"><i class="ti ti-check"></i> {{ i18n.ts.save }}</FormButton>
+	<div class="_buttons">
+		<MkButton primary inline @click="save"><i class="ti ti-check"></i> {{ i18n.ts.save }}</MkButton>
 	</div>
 </div>
 </template>
@@ -38,7 +40,7 @@ import { } from 'vue';
 import FormInput from '@/components/form/input.vue';
 import FormSection from '@/components/form/section.vue';
 import FormSwitch from '@/components/form/switch.vue';
-import FormButton from '@/components/MkButton.vue';
+import MkButton from '@/components/MkButton.vue';
 import * as os from '@/os';
 import { i18n } from '@/i18n';
 import { definePageMetadata } from '@/scripts/page-metadata';
diff --git a/packages/frontend/src/pages/settings/webhook.new.vue b/packages/frontend/src/pages/settings/webhook.new.vue
index 00a547da69..b408b6cb88 100644
--- a/packages/frontend/src/pages/settings/webhook.new.vue
+++ b/packages/frontend/src/pages/settings/webhook.new.vue
@@ -1,14 +1,14 @@
 <template>
-<div class="_formRoot">
-	<FormInput v-model="name" class="_formBlock">
+<div class="_gaps_m">
+	<FormInput v-model="name">
 		<template #label>Name</template>
 	</FormInput>
 
-	<FormInput v-model="url" type="url" class="_formBlock">
+	<FormInput v-model="url" type="url">
 		<template #label>URL</template>
 	</FormInput>
 
-	<FormInput v-model="secret" class="_formBlock">
+	<FormInput v-model="secret">
 		<template #prefix><i class="ti ti-lock"></i></template>
 		<template #label>Secret</template>
 	</FormInput>
@@ -16,17 +16,19 @@
 	<FormSection>
 		<template #label>Events</template>
 
-		<FormSwitch v-model="event_follow" class="_formBlock">Follow</FormSwitch>
-		<FormSwitch v-model="event_followed" class="_formBlock">Followed</FormSwitch>
-		<FormSwitch v-model="event_note" class="_formBlock">Note</FormSwitch>
-		<FormSwitch v-model="event_reply" class="_formBlock">Reply</FormSwitch>
-		<FormSwitch v-model="event_renote" class="_formBlock">Renote</FormSwitch>
-		<FormSwitch v-model="event_reaction" class="_formBlock">Reaction</FormSwitch>
-		<FormSwitch v-model="event_mention" class="_formBlock">Mention</FormSwitch>
+		<div class="_gaps_s">
+			<FormSwitch v-model="event_follow">Follow</FormSwitch>
+			<FormSwitch v-model="event_followed">Followed</FormSwitch>
+			<FormSwitch v-model="event_note">Note</FormSwitch>
+			<FormSwitch v-model="event_reply">Reply</FormSwitch>
+			<FormSwitch v-model="event_renote">Renote</FormSwitch>
+			<FormSwitch v-model="event_reaction">Reaction</FormSwitch>
+			<FormSwitch v-model="event_mention">Mention</FormSwitch>
+		</div>
 	</FormSection>
 
-	<div class="_formBlock" style="display: flex; gap: var(--margin); flex-wrap: wrap;">
-		<FormButton primary inline @click="create"><i class="ti ti-check"></i> {{ i18n.ts.create }}</FormButton>
+	<div class="_buttons">
+		<MkButton primary inline @click="create"><i class="ti ti-check"></i> {{ i18n.ts.create }}</MkButton>
 	</div>
 </div>
 </template>
@@ -36,7 +38,7 @@ import { } from 'vue';
 import FormInput from '@/components/form/input.vue';
 import FormSection from '@/components/form/section.vue';
 import FormSwitch from '@/components/form/switch.vue';
-import FormButton from '@/components/MkButton.vue';
+import MkButton from '@/components/MkButton.vue';
 import * as os from '@/os';
 import { i18n } from '@/i18n';
 import { definePageMetadata } from '@/scripts/page-metadata';
diff --git a/packages/frontend/src/pages/settings/webhook.vue b/packages/frontend/src/pages/settings/webhook.vue
index 9be23ee4f0..01c31688cc 100644
--- a/packages/frontend/src/pages/settings/webhook.vue
+++ b/packages/frontend/src/pages/settings/webhook.vue
@@ -1,15 +1,13 @@
 <template>
-<div class="_formRoot">
-	<FormSection>
-		<FormLink :to="`/settings/webhook/new`">
-			Create webhook
-		</FormLink>
-	</FormSection>
-	
+<div class="_gaps_m">
+	<FormLink :to="`/settings/webhook/new`">
+		Create webhook
+	</FormLink>
+
 	<FormSection>
 		<MkPagination :pagination="pagination">
 			<template #default="{items}">
-				<FormLink v-for="webhook in items" :key="webhook.id" :to="`/settings/webhook/edit/${webhook.id}`" class="_formBlock">
+				<FormLink v-for="webhook in items" :key="webhook.id" :to="`/settings/webhook/edit/${webhook.id}`" class="_margin">
 					<template #icon>
 						<i v-if="webhook.active === false" class="ti ti-player-pause"></i>
 						<i v-else-if="webhook.latestStatus === null" class="ti ti-circle"></i>
diff --git a/packages/frontend/src/pages/settings/word-mute.vue b/packages/frontend/src/pages/settings/word-mute.vue
index 6961d8151d..d3242f30bf 100644
--- a/packages/frontend/src/pages/settings/word-mute.vue
+++ b/packages/frontend/src/pages/settings/word-mute.vue
@@ -1,24 +1,24 @@
 <template>
-<div class="_formRoot">
-	<MkTab v-model="tab" class="_formBlock">
+<div class="_gaps_m">
+	<MkTab v-model="tab">
 		<option value="soft">{{ i18n.ts._wordMute.soft }}</option>
 		<option value="hard">{{ i18n.ts._wordMute.hard }}</option>
 	</MkTab>
-	<div class="_formBlock">
-		<div v-show="tab === 'soft'">
-			<MkInfo class="_formBlock">{{ i18n.ts._wordMute.softDescription }}</MkInfo>
-			<FormTextarea v-model="softMutedWords" class="_formBlock">
+	<div>
+		<div v-show="tab === 'soft'" class="_gaps_m">
+			<MkInfo>{{ i18n.ts._wordMute.softDescription }}</MkInfo>
+			<FormTextarea v-model="softMutedWords">
 				<span>{{ i18n.ts._wordMute.muteWords }}</span>
 				<template #caption>{{ i18n.ts._wordMute.muteWordsDescription }}<br>{{ i18n.ts._wordMute.muteWordsDescription2 }}</template>
 			</FormTextarea>
 		</div>
-		<div v-show="tab === 'hard'">
-			<MkInfo class="_formBlock">{{ i18n.ts._wordMute.hardDescription }} {{ i18n.ts.reflectMayTakeTime }}</MkInfo>
-			<FormTextarea v-model="hardMutedWords" class="_formBlock">
+		<div v-show="tab === 'hard'" class="_gaps_m">
+			<MkInfo>{{ i18n.ts._wordMute.hardDescription }} {{ i18n.ts.reflectMayTakeTime }}</MkInfo>
+			<FormTextarea v-model="hardMutedWords">
 				<span>{{ i18n.ts._wordMute.muteWords }}</span>
 				<template #caption>{{ i18n.ts._wordMute.muteWordsDescription }}<br>{{ i18n.ts._wordMute.muteWordsDescription2 }}</template>
 			</FormTextarea>
-			<MkKeyValue v-if="hardWordMutedNotesCount != null" class="_formBlock">
+			<MkKeyValue v-if="hardWordMutedNotesCount != null">
 				<template #key>{{ i18n.ts._wordMute.mutedNotes }}</template>
 				<template #value>{{ number(hardWordMutedNotesCount) }}</template>
 			</MkKeyValue>
diff --git a/packages/frontend/src/pages/tag.vue b/packages/frontend/src/pages/tag.vue
index 72775ed5c9..5d6d01d2ae 100644
--- a/packages/frontend/src/pages/tag.vue
+++ b/packages/frontend/src/pages/tag.vue
@@ -2,7 +2,7 @@
 <MkStickyContainer>
 	<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
 	<MkSpacer :content-max="800">
-		<XNotes class="_content" :pagination="pagination"/>
+		<XNotes class="" :pagination="pagination"/>
 	</MkSpacer>
 </MkStickyContainer>
 </template>
diff --git a/packages/frontend/src/pages/theme-editor.vue b/packages/frontend/src/pages/theme-editor.vue
index d8ff170ca2..4820bbb763 100644
--- a/packages/frontend/src/pages/theme-editor.vue
+++ b/packages/frontend/src/pages/theme-editor.vue
@@ -2,8 +2,8 @@
 <MkStickyContainer>
 	<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
 	<MkSpacer :content-max="800" :margin-min="16" :margin-max="32">
-		<div class="cwepdizn _formRoot">
-			<FormFolder :default-open="true" class="_formBlock">
+		<div class="cwepdizn _gaps_m">
+			<FormFolder :default-open="true">
 				<template #label>{{ i18n.ts.backgroundColor }}</template>
 				<div class="cwepdizn-colors">
 					<div class="row">
@@ -19,7 +19,7 @@
 				</div>
 			</FormFolder>
 
-			<FormFolder :default-open="true" class="_formBlock">
+			<FormFolder :default-open="true">
 				<template #label>{{ i18n.ts.accentColor }}</template>
 				<div class="cwepdizn-colors">
 					<div class="row">
@@ -30,7 +30,7 @@
 				</div>
 			</FormFolder>
 
-			<FormFolder :default-open="true" class="_formBlock">
+			<FormFolder :default-open="true">
 				<template #label>{{ i18n.ts.textColor }}</template>
 				<div class="cwepdizn-colors">
 					<div class="row">
@@ -41,22 +41,22 @@
 				</div>
 			</FormFolder>
 
-			<FormFolder :default-open="false" class="_formBlock">
+			<FormFolder :default-open="false">
 				<template #icon><i class="ti ti-code"></i></template>
 				<template #label>{{ i18n.ts.editCode }}</template>
 
-				<div class="_formRoot">
-					<FormTextarea v-model="themeCode" tall class="_formBlock">
+				<div class="_gaps_m">
+					<FormTextarea v-model="themeCode" tall>
 						<template #label>{{ i18n.ts._theme.code }}</template>
 					</FormTextarea>
-					<FormButton primary class="_formBlock" @click="applyThemeCode">{{ i18n.ts.apply }}</FormButton>
+					<MkButton primary @click="applyThemeCode">{{ i18n.ts.apply }}</MkButton>
 				</div>
 			</FormFolder>
 
-			<FormFolder :default-open="false" class="_formBlock">
+			<FormFolder :default-open="false">
 				<template #label>{{ i18n.ts.addDescription }}</template>
 
-				<div class="_formRoot">
+				<div class="_gaps_m">
 					<FormTextarea v-model="description">
 						<template #label>{{ i18n.ts._theme.description }}</template>
 					</FormTextarea>
@@ -74,7 +74,7 @@ import tinycolor from 'tinycolor2';
 import { v4 as uuid } from 'uuid';
 import JSON5 from 'json5';
 
-import FormButton from '@/components/MkButton.vue';
+import MkButton from '@/components/MkButton.vue';
 import FormTextarea from '@/components/form/textarea.vue';
 import FormFolder from '@/components/form/folder.vue';
 
diff --git a/packages/frontend/src/pages/timeline.vue b/packages/frontend/src/pages/timeline.vue
index 75f62d38f7..eaeb7d686e 100644
--- a/packages/frontend/src/pages/timeline.vue
+++ b/packages/frontend/src/pages/timeline.vue
@@ -3,11 +3,11 @@
 	<template #header><MkPageHeader v-model:tab="src" :actions="headerActions" :tabs="headerTabs" :display-my-avatar="true"/></template>
 	<MkSpacer :content-max="800">
 		<div ref="rootEl" v-hotkey.global="keymap" class="cmuxhskf">
-			<XTutorial v-if="$store.reactiveState.tutorial.value != -1" class="tutorial _block"/>
-			<XPostForm v-if="$store.reactiveState.showFixedPostForm.value" class="post-form _block" fixed/>
+			<XTutorial v-if="$store.reactiveState.tutorial.value != -1" class="tutorial _panel" style="margin-bottom: var(--margin);"/>
+			<XPostForm v-if="$store.reactiveState.showFixedPostForm.value" class="post-form _panel" fixed style="margin-bottom: var(--margin);"/>
 
 			<div v-if="queue > 0" class="new"><button class="_buttonPrimary" @click="top()">{{ i18n.ts.newNoteRecived }}</button></div>
-			<div class="tl _block">
+			<div class="tl">
 				<XTimeline
 					ref="tl" :key="src"
 					class="tl"
diff --git a/packages/frontend/src/pages/user-info.vue b/packages/frontend/src/pages/user-info.vue
index addc8db9e6..5812eae9ff 100644
--- a/packages/frontend/src/pages/user-info.vue
+++ b/packages/frontend/src/pages/user-info.vue
@@ -3,8 +3,8 @@
 	<template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template>
 	<MkSpacer :content-max="600" :margin-min="16" :margin-max="32">
 		<FormSuspense :p="init">
-			<div v-if="tab === 'overview'" class="_formRoot">
-				<div class="_formBlock aeakzknw">
+			<div v-if="tab === 'overview'" class="_gaps_m">
+				<div class="aeakzknw">
 					<MkAvatar class="avatar" :user="user" :show-indicator="true"/>
 					<div class="body">
 						<span class="name"><MkUserName class="name" :user="user"/></span>
@@ -17,36 +17,36 @@
 					</div>
 				</div>
 
-				<MkInfo v-if="user.username.includes('.')" class="_formBlock">{{ i18n.ts.isSystemAccount }}</MkInfo>
+				<MkInfo v-if="user.username.includes('.')">{{ i18n.ts.isSystemAccount }}</MkInfo>
 
-				<div v-if="user.url" class="_formLinksGrid _formBlock">
+				<div v-if="user.url" class="_formLinksGrid">
 					<FormLink :to="userPage(user)">Profile</FormLink>
 					<FormLink :to="user.url" :external="true">Profile (remote)</FormLink>
 				</div>
-				<FormLink v-else class="_formBlock" :to="userPage(user)">Profile</FormLink>
+				<FormLink v-else :to="userPage(user)">Profile</FormLink>
 
-				<FormLink v-if="user.host" class="_formBlock" :to="`/instance-info/${user.host}`">{{ i18n.ts.instanceInfo }}</FormLink>
+				<FormLink v-if="user.host" :to="`/instance-info/${user.host}`">{{ i18n.ts.instanceInfo }}</FormLink>
 
-				<div class="_formBlock">
-					<MkKeyValue :copy="user.id" oneline style="margin: 1em 0;">
+				<div style="display: flex; flex-direction: column; gap: 1em;">
+					<MkKeyValue :copy="user.id" oneline>
 						<template #key>ID</template>
 						<template #value><span class="_monospace">{{ user.id }}</span></template>
 					</MkKeyValue>
 					<!-- 要る?
-					<MkKeyValue v-if="ips.length > 0" :copy="user.id" oneline style="margin: 1em 0;">
+					<MkKeyValue v-if="ips.length > 0" :copy="user.id" oneline>
 						<template #key>IP (recent)</template>
 						<template #value><span class="_monospace">{{ ips[0].ip }}</span></template>
 					</MkKeyValue>
 					-->
-					<MkKeyValue oneline style="margin: 1em 0;">
+					<MkKeyValue oneline>
 						<template #key>{{ i18n.ts.createdAt }}</template>
 						<template #value><span class="_monospace"><MkTime :time="user.createdAt" :mode="'detail'"/></span></template>
 					</MkKeyValue>
-					<MkKeyValue v-if="info" oneline style="margin: 1em 0;">
+					<MkKeyValue v-if="info" oneline>
 						<template #key>{{ i18n.ts.lastActiveDate }}</template>
 						<template #value><span class="_monospace"><MkTime :time="info.lastActiveDate" :mode="'detail'"/></span></template>
 					</MkKeyValue>
-					<MkKeyValue v-if="info" oneline style="margin: 1em 0;">
+					<MkKeyValue v-if="info" oneline>
 						<template #key>{{ i18n.ts.email }}</template>
 						<template #value><span class="_monospace">{{ info.email }}</span></template>
 					</MkKeyValue>
@@ -55,48 +55,50 @@
 				<FormSection>
 					<template #label>ActivityPub</template>
 
-					<div class="_formBlock">
-						<MkKeyValue v-if="user.host" oneline style="margin: 1em 0;">
-							<template #key>{{ i18n.ts.instanceInfo }}</template>
-							<template #value><MkA :to="`/instance-info/${user.host}`" class="_link">{{ user.host }} <i class="ti ti-chevron-right"></i></MkA></template>
-						</MkKeyValue>
-						<MkKeyValue v-else oneline style="margin: 1em 0;">
-							<template #key>{{ i18n.ts.instanceInfo }}</template>
-							<template #value>(Local user)</template>
-						</MkKeyValue>
-						<MkKeyValue oneline style="margin: 1em 0;">
-							<template #key>{{ i18n.ts.updatedAt }}</template>
-							<template #value><MkTime v-if="user.lastFetchedAt" mode="detail" :time="user.lastFetchedAt"/><span v-else>N/A</span></template>
-						</MkKeyValue>
-						<MkKeyValue v-if="ap" oneline style="margin: 1em 0;">
-							<template #key>Type</template>
-							<template #value><span class="_monospace">{{ ap.type }}</span></template>
-						</MkKeyValue>
+					<div class="_gaps_m">
+						<div style="display: flex; flex-direction: column; gap: 1em;">
+							<MkKeyValue v-if="user.host" oneline>
+								<template #key>{{ i18n.ts.instanceInfo }}</template>
+								<template #value><MkA :to="`/instance-info/${user.host}`" class="_link">{{ user.host }} <i class="ti ti-chevron-right"></i></MkA></template>
+							</MkKeyValue>
+							<MkKeyValue v-else oneline>
+								<template #key>{{ i18n.ts.instanceInfo }}</template>
+								<template #value>(Local user)</template>
+							</MkKeyValue>
+							<MkKeyValue oneline>
+								<template #key>{{ i18n.ts.updatedAt }}</template>
+								<template #value><MkTime v-if="user.lastFetchedAt" mode="detail" :time="user.lastFetchedAt"/><span v-else>N/A</span></template>
+							</MkKeyValue>
+							<MkKeyValue v-if="ap" oneline>
+								<template #key>Type</template>
+								<template #value><span class="_monospace">{{ ap.type }}</span></template>
+							</MkKeyValue>
+						</div>
+
+						<MkButton v-if="user.host != null" @click="updateRemoteUser"><i class="ti ti-refresh"></i> {{ i18n.ts.updateRemoteUser }}</MkButton>
+
+						<FormFolder>
+							<template #label>Raw</template>
+
+							<MkObjectView v-if="ap" tall :value="ap">
+							</MkObjectView>
+						</FormFolder>
 					</div>
-
-					<FormButton v-if="user.host != null" class="_formBlock" @click="updateRemoteUser"><i class="ti ti-refresh"></i> {{ i18n.ts.updateRemoteUser }}</FormButton>
-
-					<FormFolder class="_formBlock">
-						<template #label>Raw</template>
-
-						<MkObjectView v-if="ap" tall :value="ap">
-						</MkObjectView>
-					</FormFolder>
 				</FormSection>
 			</div>
-			<div v-else-if="tab === 'moderation'" class="_formRoot">
-				<FormSwitch v-if="user.host == null && $i.isAdmin && (moderator || !user.isAdmin)" v-model="moderator" class="_formBlock" @update:model-value="toggleModerator">{{ i18n.ts.moderator }}</FormSwitch>
-				<FormSwitch v-model="silenced" class="_formBlock" @update:model-value="toggleSilence">{{ i18n.ts.silence }}</FormSwitch>
-				<FormSwitch v-model="suspended" class="_formBlock" @update:model-value="toggleSuspend">{{ i18n.ts.suspend }}</FormSwitch>
+			<div v-else-if="tab === 'moderation'" class="_gaps_m">
+				<FormSwitch v-if="user.host == null && $i.isAdmin && (moderator || !user.isAdmin)" v-model="moderator" @update:model-value="toggleModerator">{{ i18n.ts.moderator }}</FormSwitch>
+				<FormSwitch v-model="silenced" @update:model-value="toggleSilence">{{ i18n.ts.silence }}</FormSwitch>
+				<FormSwitch v-model="suspended" @update:model-value="toggleSuspend">{{ i18n.ts.suspend }}</FormSwitch>
 				{{ i18n.ts.reflectMayTakeTime }}
-				<div class="_formBlock">
-					<FormButton v-if="user.host == null && iAmModerator" inline style="margin-right: 8px;" @click="resetPassword"><i class="ti ti-key"></i> {{ i18n.ts.resetPassword }}</FormButton>
-					<FormButton v-if="$i.isAdmin" inline danger @click="deleteAccount">{{ i18n.ts.deleteAccount }}</FormButton>
+				<div>
+					<MkButton v-if="user.host == null && iAmModerator" inline style="margin-right: 8px;" @click="resetPassword"><i class="ti ti-key"></i> {{ i18n.ts.resetPassword }}</MkButton>
+					<MkButton v-if="$i.isAdmin" inline danger @click="deleteAccount">{{ i18n.ts.deleteAccount }}</MkButton>
 				</div>
-				<FormTextarea v-model="moderationNote" manual-save class="_formBlock">
+				<FormTextarea v-model="moderationNote" manual-save>
 					<template #label>Moderation note</template>
 				</FormTextarea>
-				<FormFolder class="_formBlock">
+				<FormFolder>
 					<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>
@@ -107,7 +109,7 @@
 						</div>
 					</template>
 				</FormFolder>
-				<FormFolder class="_formBlock">
+				<FormFolder>
 					<template #label>{{ i18n.ts.files }}</template>
 
 					<MkFileListForAdmin :pagination="filesPagination" view-mode="grid"/>
@@ -124,7 +126,7 @@
 					</FormInput>
 				</FormSection>
 			</div>
-			<div v-else-if="tab === 'chart'" class="_formRoot">
+			<div v-else-if="tab === 'chart'" class="_gaps_m">
 				<div class="cmhjzshm">
 					<div class="selects">
 						<MkSelect v-model="chartSrc" style="margin: 0 10px 0 0; flex: 1;">
@@ -139,7 +141,7 @@
 					</div>
 				</div>
 			</div>
-			<div v-else-if="tab === 'raw'" class="_formRoot">
+			<div v-else-if="tab === 'raw'" class="_gaps_m">
 				<MkObjectView v-if="info && $i.isAdmin" tall :value="info">
 				</MkObjectView>
 
@@ -160,7 +162,7 @@ import FormTextarea from '@/components/form/textarea.vue';
 import FormSwitch from '@/components/form/switch.vue';
 import FormLink from '@/components/form/link.vue';
 import FormSection from '@/components/form/section.vue';
-import FormButton from '@/components/MkButton.vue';
+import MkButton from '@/components/MkButton.vue';
 import FormInput from '@/components/form/input.vue';
 import FormSplit from '@/components/form/split.vue';
 import FormFolder from '@/components/form/folder.vue';
diff --git a/packages/frontend/src/pages/user-list-timeline.vue b/packages/frontend/src/pages/user-list-timeline.vue
index 542c280594..6817d44d8c 100644
--- a/packages/frontend/src/pages/user-list-timeline.vue
+++ b/packages/frontend/src/pages/user-list-timeline.vue
@@ -3,7 +3,7 @@
 	<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
 	<div ref="rootEl" class="eqqrhokj">
 		<div v-if="queue > 0" class="new"><button class="_buttonPrimary" @click="top()">{{ i18n.ts.newNoteRecived }}</button></div>
-		<div class="tl _block">
+		<div class="tl">
 			<XTimeline
 				ref="tlEl" :key="listId"
 				class="tl"
diff --git a/packages/frontend/src/pages/user/activity.heatmap.vue b/packages/frontend/src/pages/user/activity.heatmap.vue
index 252985d113..202201afb5 100644
--- a/packages/frontend/src/pages/user/activity.heatmap.vue
+++ b/packages/frontend/src/pages/user/activity.heatmap.vue
@@ -1,7 +1,7 @@
 <template>
 <div ref="rootEl">
 	<MkLoading v-if="fetching"/>
-	<div v-else>
+	<div v-else :class="$style.root" class="_panel">
 		<canvas ref="chartEl"></canvas>
 	</div>
 </div>
@@ -10,7 +10,6 @@
 <script lang="ts" setup>
 import { markRaw, version as vueVersion, onMounted, onBeforeUnmount, nextTick, watch } from 'vue';
 import { Chart } from 'chart.js';
-import { enUS } from 'date-fns/locale';
 import tinycolor from 'tinycolor2';
 import * as misskey from 'misskey-js';
 import * as os from '@/os';
@@ -138,7 +137,9 @@ async function renderChart() {
 						round: 'week',
 						isoWeekday: 0,
 						displayFormats: {
-							week: 'MMM dd',
+							day: 'M/d',
+							month: 'Y/M',
+							week: 'M/d',
 						},
 					},
 					grid: {
@@ -204,3 +205,9 @@ onMounted(async () => {
 	renderChart();
 });
 </script>
+
+<style lang="scss" module>
+.root {
+	padding: 20px;
+}
+</style>
diff --git a/packages/frontend/src/pages/user/activity.pv.vue b/packages/frontend/src/pages/user/activity.pv.vue
index 7715b66673..d74b641dac 100644
--- a/packages/frontend/src/pages/user/activity.pv.vue
+++ b/packages/frontend/src/pages/user/activity.pv.vue
@@ -11,7 +11,6 @@
 <script lang="ts" setup>
 import { markRaw, version as vueVersion, onMounted, onBeforeUnmount, nextTick } from 'vue';
 import { Chart } from 'chart.js';
-import { enUS } from 'date-fns/locale';
 import tinycolor from 'tinycolor2';
 import * as misskey from 'misskey-js';
 import gradient from 'chartjs-plugin-gradient';
@@ -65,6 +64,8 @@ async function renderChart() {
 
 	const colorUser = '#3498db';
 	const colorVisitor = '#2ecc71';
+	const colorUser2 = '#3498db88';
+	const colorVisitor2 = '#2ecc7188';
 
 	chartInstance = new Chart(chartEl, {
 		type: 'bar',
@@ -79,8 +80,9 @@ async function renderChart() {
 				borderRadius: 4,
 				backgroundColor: colorUser,
 				barPercentage: 0.7,
-				categoryPercentage: 1,
+				categoryPercentage: 0.7,
 				fill: true,
+				stack: 'u',
 			}, {
 				parsing: false,
 				label: 'UPV (visitor)',
@@ -91,8 +93,35 @@ async function renderChart() {
 				borderRadius: 4,
 				backgroundColor: colorVisitor,
 				barPercentage: 0.7,
-				categoryPercentage: 1,
+				categoryPercentage: 0.7,
 				fill: true,
+				stack: 'u',
+			}, {
+				parsing: false,
+				label: 'NPV (user)',
+				data: format(raw.pv.user).slice().reverse(),
+				pointRadius: 0,
+				borderWidth: 0,
+				borderJoinStyle: 'round',
+				borderRadius: 4,
+				backgroundColor: colorUser2,
+				barPercentage: 0.7,
+				categoryPercentage: 0.7,
+				fill: true,
+				stack: 'n',
+			}, {
+				parsing: false,
+				label: 'NPV (visitor)',
+				data: format(raw.pv.visitor).slice().reverse(),
+				pointRadius: 0,
+				borderWidth: 0,
+				borderJoinStyle: 'round',
+				borderRadius: 4,
+				backgroundColor: colorVisitor2,
+				barPercentage: 0.7,
+				categoryPercentage: 0.7,
+				fill: true,
+				stack: 'n',
 			}],
 		},
 		options: {
@@ -113,6 +142,10 @@ async function renderChart() {
 					time: {
 						stepSize: 1,
 						unit: 'day',
+						displayFormats: {
+							day: 'M/d',
+							month: 'Y/M',
+						},
 					},
 					grid: {
 						display: false,
@@ -122,11 +155,6 @@ async function renderChart() {
 						maxRotation: 0,
 						autoSkipPadding: 8,
 					},
-					adapters: {
-						date: {
-							locale: enUS,
-						},
-					},
 				},
 				y: {
 					position: 'left',
@@ -148,7 +176,7 @@ async function renderChart() {
 			plugins: {
 				title: {
 					display: true,
-					text: 'Unique PV',
+					text: 'Unique/Natural PV',
 					padding: {
 						left: 0,
 						right: 0,
diff --git a/packages/frontend/src/pages/user/activity.vue b/packages/frontend/src/pages/user/activity.vue
index f9dce3a9e8..3def414674 100644
--- a/packages/frontend/src/pages/user/activity.vue
+++ b/packages/frontend/src/pages/user/activity.vue
@@ -1,13 +1,15 @@
 <template>
 <MkSpacer :content-max="700">
-	<MkFolder class="item">
-		<template #header>Heatmap</template>
-		<XHeatmap :user="user" :src="'notes'"/>
-	</MkFolder>
-	<MkFolder class="item">
-		<template #header>PV</template>
-		<XPv :user="user"/>
-	</MkFolder>
+	<div class="_gaps">
+		<MkFolder class="item">
+			<template #header>Heatmap</template>
+			<XHeatmap :user="user" :src="'notes'"/>
+		</MkFolder>
+		<MkFolder class="item">
+			<template #header>PV</template>
+			<XPv :user="user"/>
+		</MkFolder>
+	</div>
 </MkSpacer>
 </template>
 
diff --git a/packages/frontend/src/pages/user/clips.vue b/packages/frontend/src/pages/user/clips.vue
index 8c71aacb0c..95f8cbc296 100644
--- a/packages/frontend/src/pages/user/clips.vue
+++ b/packages/frontend/src/pages/user/clips.vue
@@ -2,7 +2,7 @@
 <MkSpacer :content-max="700">
 	<div class="pages-user-clips">
 		<MkPagination v-slot="{items}" ref="list" :pagination="pagination" class="list">
-			<MkA v-for="item in items" :key="item.id" :to="`/clips/${item.id}`" class="item _panel _gap">
+			<MkA v-for="item in items" :key="item.id" :to="`/clips/${item.id}`" class="item _panel _margin">
 				<b>{{ item.name }}</b>
 				<div v-if="item.description" class="description">{{ item.description }}</div>
 			</MkA>
diff --git a/packages/frontend/src/pages/user/home.vue b/packages/frontend/src/pages/user/home.vue
index 4a92074d93..53ae6a2c53 100644
--- a/packages/frontend/src/pages/user/home.vue
+++ b/packages/frontend/src/pages/user/home.vue
@@ -1,15 +1,15 @@
 <template>
 <MkSpacer :content-max="narrow ? 800 : 1100">
 	<div ref="rootEl" class="ftskorzw" :class="{ wide: !narrow }" style="container-type: inline-size;">
-		<div class="main">
+		<div class="main _gaps">
 			<!-- TODO -->
 			<!-- <div class="punished" v-if="user.isSuspended"><i class="ti ti-alert-triangle" style="margin-right: 8px;"></i> {{ i18n.ts.userSuspended }}</div> -->
 			<!-- <div class="punished" v-if="user.isSilenced"><i class="ti ti-alert-triangle" style="margin-right: 8px;"></i> {{ i18n.ts.userSilenced }}</div> -->
 
-			<div class="profile">
+			<div class="profile _gaps">
 				<MkRemoteCaution v-if="user.host != null" :href="user.url" class="warn"/>
 
-				<div :key="user.id" class="_block main">
+				<div :key="user.id" class="main _panel">
 					<div class="banner-container" :style="style">
 						<div ref="bannerEl" class="banner" :style="style"></div>
 						<div class="fade"></div>
@@ -85,23 +85,23 @@
 				</div>
 			</div>
 
-			<div class="contents">
-				<div v-if="user.pinnedNotes.length > 0" class="_gap">
-					<XNote v-for="note in user.pinnedNotes" :key="note.id" class="note _block" :note="note" :pinned="true"/>
+			<div class="contents _gaps">
+				<div v-if="user.pinnedNotes.length > 0" class="_gaps">
+					<XNote 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">
 					<XPhotos :key="user.id" :user="user"/>
-					<XActivity :key="user.id" :user="user" style="margin-top: var(--margin);"/>
+					<XActivity :key="user.id" :user="user"/>
 				</template>
 			</div>
 			<div>
 				<XUserTimeline :user="user"/>
 			</div>
 		</div>
-		<div v-if="!narrow" class="sub" style="container-type: inline-size;">
+		<div v-if="!narrow" class="sub _gaps" style="container-type: inline-size;">
 			<XPhotos :key="user.id" :user="user"/>
-			<XActivity :key="user.id" :user="user" style="margin-top: var(--margin);"/>
+			<XActivity :key="user.id" :user="user"/>
 		</div>
 	</div>
 </MkSpacer>
@@ -128,6 +128,7 @@ import { useRouter } from '@/router';
 import { i18n } from '@/i18n';
 import { $i } from '@/account';
 import { dateString } from '@/filters/date';
+import { confetti } from '@/scripts/confetti';
 
 const XPhotos = defineAsyncComponent(() => import('./index.photos.vue'));
 const XActivity = defineAsyncComponent(() => import('./index.activity.vue'));
@@ -180,6 +181,18 @@ function parallax() {
 onMounted(() => {
 	window.requestAnimationFrame(parallaxLoop);
 	narrow = rootEl!.clientWidth < 1000;
+
+	if (props.user.birthday) {
+		const m = new Date().getMonth() + 1;
+		const d = new Date().getDate();
+		const bm = parseInt(props.user.birthday.split('-')[1]);
+		const bd = parseInt(props.user.birthday.split('-')[2]);
+		if (m === bm && d === bd) {
+			confetti({
+				duration: 1000 * 4
+			});
+		}
+	}
 });
 
 onUnmounted(() => {
diff --git a/packages/frontend/src/pages/user/pages.vue b/packages/frontend/src/pages/user/pages.vue
index 7833d6c42c..7ea1d75f43 100644
--- a/packages/frontend/src/pages/user/pages.vue
+++ b/packages/frontend/src/pages/user/pages.vue
@@ -1,7 +1,7 @@
 <template>
 <MkSpacer :content-max="700">
 	<MkPagination v-slot="{items}" ref="list" :pagination="pagination">
-		<MkPagePreview v-for="page in items" :key="page.id" :page="page" class="_gap"/>
+		<MkPagePreview v-for="page in items" :key="page.id" :page="page" class="_margin"/>
 	</MkPagination>
 </MkSpacer>
 </template>
diff --git a/packages/frontend/src/pages/user/reactions.vue b/packages/frontend/src/pages/user/reactions.vue
index bc4f39a74f..24129ec024 100644
--- a/packages/frontend/src/pages/user/reactions.vue
+++ b/packages/frontend/src/pages/user/reactions.vue
@@ -1,7 +1,7 @@
 <template>
 <MkSpacer :content-max="700">
 	<MkPagination v-slot="{items}" ref="list" :pagination="pagination">
-		<div v-for="item in items" :key="item.id" :to="`/clips/${item.id}`" class="item _panel _gap afdcfbfb">
+		<div v-for="item in items" :key="item.id" :to="`/clips/${item.id}`" class="item _panel _margin afdcfbfb">
 			<div class="header">
 				<MkAvatar class="avatar" :user="user"/>
 				<MkReactionIcon class="reaction" :reaction="item.type" :no-style="true"/>
diff --git a/packages/frontend/src/pages/welcome.setup.vue b/packages/frontend/src/pages/welcome.setup.vue
index 2729d30d4b..ea03bd4a85 100644
--- a/packages/frontend/src/pages/welcome.setup.vue
+++ b/packages/frontend/src/pages/welcome.setup.vue
@@ -1,18 +1,18 @@
 <template>
 <form class="mk-setup" @submit.prevent="submit()">
 	<h1>Welcome to Misskey!</h1>
-	<div class="_formRoot">
+	<div class="_gaps_m">
 		<p>{{ $ts.intro }}</p>
-		<MkInput v-model="username" pattern="^[a-zA-Z0-9_]{1,20}$" :spellcheck="false" required data-cy-admin-username class="_formBlock">
+		<MkInput v-model="username" pattern="^[a-zA-Z0-9_]{1,20}$" :spellcheck="false" required data-cy-admin-username>
 			<template #label>{{ $ts.username }}</template>
 			<template #prefix>@</template>
 			<template #suffix>@{{ host }}</template>
 		</MkInput>
-		<MkInput v-model="password" type="password" data-cy-admin-password class="_formBlock">
+		<MkInput v-model="password" type="password" data-cy-admin-password>
 			<template #label>{{ $ts.password }}</template>
 			<template #prefix><i class="ti ti-lock"></i></template>
 		</MkInput>
-		<div class="bottom _formBlock">
+		<div class="bottom">
 			<MkButton gradate type="submit" :disabled="submitting" data-cy-admin-ok>
 				{{ submitting ? $ts.processing : $ts.done }}<MkEllipsis v-if="submitting"/>
 			</MkButton>
diff --git a/packages/frontend/src/router.ts b/packages/frontend/src/router.ts
index 9001f0f37f..63c753de22 100644
--- a/packages/frontend/src/router.ts
+++ b/packages/frontend/src/router.ts
@@ -262,6 +262,20 @@ export const routes = [{
 }, {
 	path: '/pages',
 	component: page(() => import('./pages/pages.vue')),
+}, {
+	path: '/play/:id/edit',
+	component: page(() => import('./pages/flash/flash-edit.vue')),
+	loginRequired: true,
+}, {
+	path: '/play/new',
+	component: page(() => import('./pages/flash/flash-edit.vue')),
+	loginRequired: true,
+}, {
+	path: '/play/:id',
+	component: page(() => import('./pages/flash/flash.vue')),
+}, {
+	path: '/play',
+	component: page(() => import('./pages/flash/flash-index.vue')),
 }, {
 	path: '/gallery/:postId/edit',
 	component: page(() => import('./pages/gallery/edit.vue')),
diff --git a/packages/frontend/src/scripts/aiscript/api.ts b/packages/frontend/src/scripts/aiscript/api.ts
index 6debcb8a13..2a44223080 100644
--- a/packages/frontend/src/scripts/aiscript/api.ts
+++ b/packages/frontend/src/scripts/aiscript/api.ts
@@ -1,6 +1,7 @@
 import { utils, values } from '@syuilo/aiscript';
 import * as os from '@/os';
 import { $i } from '@/account';
+import { miLocalStorage } from '@/local-storage';
 
 export function createAiScriptEnv(opts) {
 	let apiRequests = 0;
@@ -32,12 +33,12 @@ export function createAiScriptEnv(opts) {
 		}),
 		'Mk:save': values.FN_NATIVE(([key, value]) => {
 			utils.assertString(key);
-			localStorage.setItem('aiscript:' + opts.storageKey + ':' + key.value, JSON.stringify(utils.valToJs(value)));
+			miLocalStorage.setItem(`aiscript:${opts.storageKey}:${key.value}`, JSON.stringify(utils.valToJs(value)));
 			return values.NULL;
 		}),
 		'Mk:load': values.FN_NATIVE(([key]) => {
 			utils.assertString(key);
-			return utils.jsToVal(JSON.parse(localStorage.getItem('aiscript:' + opts.storageKey + ':' + key.value)));
+			return utils.jsToVal(JSON.parse(miLocalStorage.getItem(`aiscript:${opts.storageKey}:${key.value}`)));
 		}),
 	};
 }
diff --git a/packages/frontend/src/scripts/aiscript/ui.ts b/packages/frontend/src/scripts/aiscript/ui.ts
new file mode 100644
index 0000000000..6e0e312116
--- /dev/null
+++ b/packages/frontend/src/scripts/aiscript/ui.ts
@@ -0,0 +1,557 @@
+import { Interpreter, Parser, utils, values } from '@syuilo/aiscript';
+import { v4 as uuid } from 'uuid';
+import { ref, Ref } from 'vue';
+
+export type AsUiComponentBase = {
+	id: string;
+	hidden?: boolean;
+};
+
+export type AsUiRoot = AsUiComponentBase & {
+	type: 'root';
+	children: AsUiComponent['id'][];
+};
+
+export type AsUiContainer = AsUiComponentBase & {
+	type: 'container';
+	children?: AsUiComponent['id'][];
+	align?: 'left' | 'center' | 'right';
+	bgColor?: string;
+	fgColor?: string;
+	font?: 'serif' | 'sans-serif' | 'monospace';
+	borderWidth?: number;
+	borderColor?: string;
+	padding?: number;
+	rounded?: boolean;
+	hidden?: boolean;
+};
+
+export type AsUiText = AsUiComponentBase & {
+	type: 'text';
+	text?: string;
+	size?: number;
+	bold?: boolean;
+	color?: string;
+	font?: 'serif' | 'sans-serif' | 'monospace';
+};
+
+export type AsUiMfm = AsUiComponentBase & {
+	type: 'mfm';
+	text?: string;
+	size?: number;
+	color?: string;
+	font?: 'serif' | 'sans-serif' | 'monospace';
+};
+
+export type AsUiButton = AsUiComponentBase & {
+	type: 'button';
+	text?: string;
+	onClick?: () => void;
+	primary?: boolean;
+	rounded?: boolean;
+};
+
+export type AsUiButtons = AsUiComponentBase & {
+	type: 'buttons';
+	buttons?: AsUiButton[];
+};
+
+export type AsUiSwitch = AsUiComponentBase & {
+	type: 'switch';
+	onChange?: (v: boolean) => void;
+	default?: boolean;
+	label?: string;
+	caption?: string;
+};
+
+export type AsUiTextarea = AsUiComponentBase & {
+	type: 'textarea';
+	onInput?: (v: string) => void;
+	default?: string;
+	label?: string;
+	caption?: string;
+};
+
+export type AsUiTextInput = AsUiComponentBase & {
+	type: 'textInput';
+	onInput?: (v: string) => void;
+	default?: string;
+	label?: string;
+	caption?: string;
+};
+
+export type AsUiNumberInput = AsUiComponentBase & {
+	type: 'numberInput';
+	onInput?: (v: number) => void;
+	default?: number;
+	label?: string;
+	caption?: string;
+};
+
+export type AsUiSelect = AsUiComponentBase & {
+	type: 'select';
+	items?: {
+		text: string;
+		value: string;
+	}[];
+	onChange?: (v: string) => void;
+	default?: string;
+	label?: string;
+	caption?: string;
+};
+
+export type AsUiFolder = AsUiComponentBase & {
+	type: 'folder';
+	children?: AsUiComponent['id'][];
+	title?: string;
+	opened?: boolean;
+};
+
+export type AsUiPostFormButton = AsUiComponentBase & {
+	type: 'postFormButton';
+	text?: string;
+	primary?: boolean;
+	rounded?: boolean;
+	form?: {
+		text: string;
+	};
+};
+
+export type AsUiComponent = AsUiRoot | AsUiContainer | AsUiText | AsUiMfm | AsUiButton | AsUiButtons | AsUiSwitch | AsUiTextarea | AsUiTextInput | AsUiNumberInput | AsUiSelect | AsUiFolder | AsUiPostFormButton;
+
+export function patch(id: string, def: values.Value, call: (fn: values.VFn, args: values.Value[]) => Promise<values.Value>) {
+	// TODO
+}
+
+function getRootOptions(def: values.Value | undefined): Omit<AsUiRoot, 'id' | 'type'> {
+	utils.assertObject(def);
+
+	const children = def.value.get('children');
+	utils.assertArray(children);
+
+	return {
+		children: children.value.map(v => {
+			utils.assertObject(v);
+			return v.value.get('id').value;
+		}),
+	};
+}
+
+function getContainerOptions(def: values.Value | undefined): Omit<AsUiContainer, 'id' | 'type'> {
+	utils.assertObject(def);
+
+	const children = def.value.get('children');
+	if (children) utils.assertArray(children);
+	const align = def.value.get('align');
+	if (align) utils.assertString(align);
+	const bgColor = def.value.get('bgColor');
+	if (bgColor) utils.assertString(bgColor);
+	const fgColor = def.value.get('fgColor');
+	if (fgColor) utils.assertString(fgColor);
+	const font = def.value.get('font');
+	if (font) utils.assertString(font);
+	const borderWidth = def.value.get('borderWidth');
+	if (borderWidth) utils.assertNumber(borderWidth);
+	const borderColor = def.value.get('borderColor');
+	if (borderColor) utils.assertString(borderColor);
+	const padding = def.value.get('padding');
+	if (padding) utils.assertNumber(padding);
+	const rounded = def.value.get('rounded');
+	if (rounded) utils.assertBoolean(rounded);
+	const hidden = def.value.get('hidden');
+	if (hidden) utils.assertBoolean(hidden);
+
+	return {
+		children: children ? children.value.map(v => {
+			utils.assertObject(v);
+			return v.value.get('id').value;
+		}) : [],
+		align: align?.value,
+		fgColor: fgColor?.value,
+		bgColor: bgColor?.value,
+		font: font?.value,
+		borderWidth: borderWidth?.value,
+		borderColor: borderColor?.value,
+		padding: padding?.value,
+		rounded: rounded?.value,
+		hidden: hidden?.value,
+	};
+}
+
+function getTextOptions(def: values.Value | undefined): Omit<AsUiText, 'id' | 'type'> {
+	utils.assertObject(def);
+
+	const text = def.value.get('text');
+	if (text) utils.assertString(text);
+	const size = def.value.get('size');
+	if (size) utils.assertNumber(size);
+	const bold = def.value.get('bold');
+	if (bold) utils.assertBoolean(bold);
+	const color = def.value.get('color');
+	if (color) utils.assertString(color);
+	const font = def.value.get('font');
+	if (font) utils.assertString(font);
+
+	return {
+		text: text?.value,
+		size: size?.value,
+		bold: bold?.value,
+		color: color?.value,
+		font: font?.value,
+	};
+}
+
+function getMfmOptions(def: values.Value | undefined): Omit<AsUiMfm, 'id' | 'type'> {
+	utils.assertObject(def);
+
+	const text = def.value.get('text');
+	if (text) utils.assertString(text);
+	const size = def.value.get('size');
+	if (size) utils.assertNumber(size);
+	const color = def.value.get('color');
+	if (color) utils.assertString(color);
+	const font = def.value.get('font');
+	if (font) utils.assertString(font);
+
+	return {
+		text: text?.value,
+		size: size?.value,
+		color: color?.value,
+		font: font?.value,
+	};
+}
+
+function getTextInputOptions(def: values.Value | undefined, call: (fn: values.VFn, args: values.Value[]) => Promise<values.Value>): Omit<AsUiTextInput, 'id' | 'type'> {
+	utils.assertObject(def);
+
+	const onInput = def.value.get('onInput');
+	if (onInput) utils.assertFunction(onInput);
+	const defaultValue = def.value.get('default');
+	if (defaultValue) utils.assertString(defaultValue);
+	const label = def.value.get('label');
+	if (label) utils.assertString(label);
+	const caption = def.value.get('caption');
+	if (caption) utils.assertString(caption);
+
+	return {
+		onInput: (v) => {
+			if (onInput) call(onInput, [utils.jsToVal(v)]);
+		},
+		default: defaultValue?.value,
+		label: label?.value,
+		caption: caption?.value,
+	};
+}
+
+function getTextareaOptions(def: values.Value | undefined, call: (fn: values.VFn, args: values.Value[]) => Promise<values.Value>): Omit<AsUiTextarea, 'id' | 'type'> {
+	utils.assertObject(def);
+
+	const onInput = def.value.get('onInput');
+	if (onInput) utils.assertFunction(onInput);
+	const defaultValue = def.value.get('default');
+	if (defaultValue) utils.assertString(defaultValue);
+	const label = def.value.get('label');
+	if (label) utils.assertString(label);
+	const caption = def.value.get('caption');
+	if (caption) utils.assertString(caption);
+
+	return {
+		onInput: (v) => {
+			if (onInput) call(onInput, [utils.jsToVal(v)]);
+		},
+		default: defaultValue?.value,
+		label: label?.value,
+		caption: caption?.value,
+	};
+}
+
+function getNumberInputOptions(def: values.Value | undefined, call: (fn: values.VFn, args: values.Value[]) => Promise<values.Value>): Omit<AsUiNumberInput, 'id' | 'type'> {
+	utils.assertObject(def);
+
+	const onInput = def.value.get('onInput');
+	if (onInput) utils.assertFunction(onInput);
+	const defaultValue = def.value.get('default');
+	if (defaultValue) utils.assertNumber(defaultValue);
+	const label = def.value.get('label');
+	if (label) utils.assertString(label);
+	const caption = def.value.get('caption');
+	if (caption) utils.assertString(caption);
+
+	return {
+		onInput: (v) => {
+			if (onInput) call(onInput, [utils.jsToVal(v)]);
+		},
+		default: defaultValue?.value,
+		label: label?.value,
+		caption: caption?.value,
+	};
+}
+
+function getButtonOptions(def: values.Value | undefined, call: (fn: values.VFn, args: values.Value[]) => Promise<values.Value>): Omit<AsUiButton, 'id' | 'type'> {
+	utils.assertObject(def);
+
+	const text = def.value.get('text');
+	if (text) utils.assertString(text);
+	const onClick = def.value.get('onClick');
+	if (onClick) utils.assertFunction(onClick);
+	const primary = def.value.get('primary');
+	if (primary) utils.assertBoolean(primary);
+	const rounded = def.value.get('rounded');
+	if (rounded) utils.assertBoolean(rounded);
+
+	return {
+		text: text?.value,
+		onClick: () => {
+			if (onClick) call(onClick, []);
+		},
+		primary: primary?.value,
+		rounded: rounded?.value,
+	};
+}
+
+function getButtonsOptions(def: values.Value | undefined, call: (fn: values.VFn, args: values.Value[]) => Promise<values.Value>): Omit<AsUiButtons, 'id' | 'type'> {
+	utils.assertObject(def);
+
+	const buttons = def.value.get('buttons');
+	if (buttons) utils.assertArray(buttons);
+
+	return {
+		buttons: buttons ? buttons.value.map(button => {
+			utils.assertObject(button);
+			const text = button.value.get('text');
+			utils.assertString(text);
+			const onClick = button.value.get('onClick');
+			utils.assertFunction(onClick);
+			const primary = button.value.get('primary');
+			if (primary) utils.assertBoolean(primary);
+			const rounded = button.value.get('rounded');
+			if (rounded) utils.assertBoolean(rounded);
+
+			return {
+				text: text.value,
+				onClick: () => {
+					call(onClick, []);
+				},
+				primary: primary?.value,
+				rounded: rounded?.value,
+			};
+		}) : [],
+	};
+}
+
+function getSwitchOptions(def: values.Value | undefined, call: (fn: values.VFn, args: values.Value[]) => Promise<values.Value>): Omit<AsUiSwitch, 'id' | 'type'> {
+	utils.assertObject(def);
+
+	const onChange = def.value.get('onChange');
+	if (onChange) utils.assertFunction(onChange);
+	const defaultValue = def.value.get('default');
+	if (defaultValue) utils.assertBoolean(defaultValue);
+	const label = def.value.get('label');
+	if (label) utils.assertString(label);
+	const caption = def.value.get('caption');
+	if (caption) utils.assertString(caption);
+
+	return {
+		onChange: (v) => {
+			if (onChange) call(onChange, [utils.jsToVal(v)]);
+		},
+		default: defaultValue?.value,
+		label: label?.value,
+		caption: caption?.value,
+	};
+}
+
+function getSelectOptions(def: values.Value | undefined, call: (fn: values.VFn, args: values.Value[]) => Promise<values.Value>): Omit<AsUiSelect, 'id' | 'type'> {
+	utils.assertObject(def);
+
+	const items = def.value.get('items');
+	if (items) utils.assertArray(items);
+	const onChange = def.value.get('onChange');
+	if (onChange) utils.assertFunction(onChange);
+	const defaultValue = def.value.get('default');
+	if (defaultValue) utils.assertString(defaultValue);
+	const label = def.value.get('label');
+	if (label) utils.assertString(label);
+	const caption = def.value.get('caption');
+	if (caption) utils.assertString(caption);
+
+	return {
+		items: items ? items.value.map(item => {
+			utils.assertObject(item);
+			const text = item.value.get('text');
+			utils.assertString(text);
+			const value = item.value.get('value');
+			if (value) utils.assertString(value);
+			return {
+				text: text.value,
+				value: value ? value.value : text.value,
+			};
+		}) : [],
+		onChange: (v) => {
+			if (onChange) call(onChange, [utils.jsToVal(v)]);
+		},
+		default: defaultValue?.value,
+		label: label?.value,
+		caption: caption?.value,
+	};
+}
+
+function getFolderOptions(def: values.Value | undefined): Omit<AsUiFolder, 'id' | 'type'> {
+	utils.assertObject(def);
+
+	const children = def.value.get('children');
+	if (children) utils.assertArray(children);
+	const title = def.value.get('title');
+	if (title) utils.assertString(title);
+	const opened = def.value.get('opened');
+	if (opened) utils.assertBoolean(opened);
+
+	return {
+		children: children ? children.value.map(v => {
+			utils.assertObject(v);
+			return v.value.get('id').value;
+		}) : [],
+		title: title?.value ?? '',
+		opened: opened?.value ?? true,
+	};
+}
+
+function getPostFormButtonOptions(def: values.Value | undefined, call: (fn: values.VFn, args: values.Value[]) => Promise<values.Value>): Omit<AsUiPostFormButton, 'id' | 'type'> {
+	utils.assertObject(def);
+
+	const text = def.value.get('text');
+	if (text) utils.assertString(text);
+	const primary = def.value.get('primary');
+	if (primary) utils.assertBoolean(primary);
+	const rounded = def.value.get('rounded');
+	if (rounded) utils.assertBoolean(rounded);
+	const form = def.value.get('form');
+	if (form) utils.assertObject(form);
+
+	const getForm = () => {
+		const text = form!.value.get('text');
+		utils.assertString(text);
+		return {
+			text: text.value,
+		};
+	};
+
+	return {
+		text: text?.value,
+		primary: primary?.value,
+		rounded: rounded?.value,
+		form: form ? getForm() : {
+			text: '',
+		},
+	};
+}
+
+export function registerAsUiLib(components: Ref<AsUiComponent>[], done: (root: Ref<AsUiRoot>) => void) {
+	const instances = {};
+
+	function createComponentInstance(type: AsUiComponent['type'], def: values.Value | undefined, id: values.Value | undefined, getOptions: (def: values.Value | undefined, call: (fn: values.VFn, args: values.Value[]) => Promise<values.Value>) => any, call: (fn: values.VFn, args: values.Value[]) => Promise<values.Value>) {
+		if (id) utils.assertString(id);
+		const _id = id?.value ?? uuid();
+		const component = ref({
+			...getOptions(def, call),
+			type,
+			id: _id,
+		});
+		components.push(component);
+		const instance = values.OBJ(new Map([
+			['id', values.STR(_id)],
+			['update', values.FN_NATIVE(async ([def], opts) => {
+				utils.assertObject(def);
+				const updates = getOptions(def, call);
+				for (const update of def.value.keys()) {
+					if (!Object.hasOwn(updates, update)) continue;
+					component.value[update] = updates[update];
+				}
+			})],
+		]));
+		instances[_id] = instance;
+		return instance;
+	}
+
+	const rootInstance = createComponentInstance('root', utils.jsToVal({ children: [] }), utils.jsToVal('___root___'), getRootOptions, () => {});
+	const rootComponent = components[0] as Ref<AsUiRoot>;
+	done(rootComponent);
+
+	return {
+		'Ui:root': rootInstance,
+
+		'Ui:patch': values.FN_NATIVE(async ([id, val], opts) => {
+			utils.assertString(id);
+			utils.assertArray(val);
+			patch(id.value, val.value, opts.call);
+		}),
+
+		'Ui:get': values.FN_NATIVE(async ([id], opts) => {
+			utils.assertString(id);
+			const instance = instances[id.value];
+			if (instance) {
+				return instance;
+			} else {
+				return values.NULL;
+			}
+		}),
+
+		// Ui:root.update({ children: [...] }) の糖衣構文
+		'Ui:render': values.FN_NATIVE(async ([children], opts) => {
+			utils.assertArray(children);
+		
+			rootComponent.value.children = children.value.map(v => {
+				utils.assertObject(v);
+				return v.value.get('id').value;
+			});
+		}),
+
+		'Ui:C:container': values.FN_NATIVE(async ([def, id], opts) => {
+			return createComponentInstance('container', def, id, getContainerOptions, opts.call);
+		}),
+
+		'Ui:C:text': values.FN_NATIVE(async ([def, id], opts) => {
+			return createComponentInstance('text', def, id, getTextOptions, opts.call);
+		}),
+
+		'Ui:C:mfm': values.FN_NATIVE(async ([def, id], opts) => {
+			return createComponentInstance('mfm', def, id, getMfmOptions, opts.call);
+		}),
+
+		'Ui:C:textarea': values.FN_NATIVE(async ([def, id], opts) => {
+			return createComponentInstance('textarea', def, id, getTextareaOptions, opts.call);
+		}),
+
+		'Ui:C:textInput': values.FN_NATIVE(async ([def, id], opts) => {
+			return createComponentInstance('textInput', def, id, getTextInputOptions, opts.call);
+		}),
+
+		'Ui:C:numberInput': values.FN_NATIVE(async ([def, id], opts) => {
+			return createComponentInstance('numberInput', def, id, getNumberInputOptions, opts.call);
+		}),
+
+		'Ui:C:button': values.FN_NATIVE(async ([def, id], opts) => {
+			return createComponentInstance('button', def, id, getButtonOptions, opts.call);
+		}),
+
+		'Ui:C:buttons': values.FN_NATIVE(async ([def, id], opts) => {
+			return createComponentInstance('buttons', def, id, getButtonsOptions, opts.call);
+		}),
+
+		'Ui:C:switch': values.FN_NATIVE(async ([def, id], opts) => {
+			return createComponentInstance('switch', def, id, getSwitchOptions, opts.call);
+		}),
+
+		'Ui:C:select': values.FN_NATIVE(async ([def, id], opts) => {
+			return createComponentInstance('select', def, id, getSelectOptions, opts.call);
+		}),
+
+		'Ui:C:folder': values.FN_NATIVE(async ([def, id], opts) => {
+			return createComponentInstance('folder', def, id, getFolderOptions, opts.call);
+		}),
+
+		'Ui:C:postFormButton': values.FN_NATIVE(async ([def, id], opts) => {
+			return createComponentInstance('postFormButton', def, id, getPostFormButtonOptions, opts.call);
+		}),
+	};
+}
diff --git a/packages/frontend/src/scripts/confetti.ts b/packages/frontend/src/scripts/confetti.ts
new file mode 100644
index 0000000000..9e03acbf8d
--- /dev/null
+++ b/packages/frontend/src/scripts/confetti.ts
@@ -0,0 +1,25 @@
+import _confetti from 'canvas-confetti';
+import * as os from '@/os';
+
+export function confetti(options: { duration?: number; } = {}) {
+	const duration = options.duration ?? 1000 * 4;
+	const animationEnd = Date.now() + duration;
+	const defaults = { startVelocity: 30, spread: 360, ticks: 60, zIndex: os.claimZIndex('high') };
+
+	function randomInRange(min, max) {
+		return Math.random() * (max - min) + min;
+	}
+
+	const interval = setInterval(() => {
+		const timeLeft = animationEnd - Date.now();
+
+		if (timeLeft <= 0) {
+			return clearInterval(interval);
+		}
+
+		const particleCount = 50 * (timeLeft / duration);
+		// since particles fall down, start a bit higher than random
+		_confetti(Object.assign({}, defaults, { particleCount, origin: { x: randomInRange(0.1, 0.3), y: Math.random() - 0.2 } }));
+		_confetti(Object.assign({}, defaults, { particleCount, origin: { x: randomInRange(0.7, 0.9), y: Math.random() - 0.2 } }));
+	}, 250);
+}
diff --git a/packages/frontend/src/scripts/get-note-menu.ts b/packages/frontend/src/scripts/get-note-menu.ts
index 7656770894..1b723220ee 100644
--- a/packages/frontend/src/scripts/get-note-menu.ts
+++ b/packages/frontend/src/scripts/get-note-menu.ts
@@ -9,6 +9,7 @@ import copyToClipboard from '@/scripts/copy-to-clipboard';
 import { url } from '@/config';
 import { noteActions } from '@/store';
 import { notePage } from '@/filters/note';
+import { miLocalStorage } from '@/local-storage';
 
 export function getNoteMenu(props: {
 	note: misskey.entities.Note;
@@ -181,7 +182,7 @@ export function getNoteMenu(props: {
 		props.translating.value = true;
 		const res = await os.api('notes/translate', {
 			noteId: appearNote.id,
-			targetLang: localStorage.getItem('lang') || navigator.language,
+			targetLang: miLocalStorage.getItem('lang') || navigator.language,
 		});
 		props.translating.value = false;
 		props.translation.value = res;
diff --git a/packages/frontend/src/scripts/idb-proxy.ts b/packages/frontend/src/scripts/idb-proxy.ts
index 77bb84463c..218682bb56 100644
--- a/packages/frontend/src/scripts/idb-proxy.ts
+++ b/packages/frontend/src/scripts/idb-proxy.ts
@@ -22,15 +22,15 @@ if (idbAvailable) {
 
 export async function get(key: string) {
 	if (idbAvailable) return iget(key);
-	return JSON.parse(localStorage.getItem(fallbackName(key)));
+	return JSON.parse(window.localStorage.getItem(fallbackName(key)));
 }
 
 export async function set(key: string, val: any) {
 	if (idbAvailable) return iset(key, val);
-	return localStorage.setItem(fallbackName(key), JSON.stringify(val));
+	return window.localStorage.setItem(fallbackName(key), JSON.stringify(val));
 }
 
 export async function del(key: string) {
 	if (idbAvailable) return idel(key);
-	return localStorage.removeItem(fallbackName(key));
+	return window.localStorage.removeItem(fallbackName(key));
 }
diff --git a/packages/frontend/src/scripts/theme.ts b/packages/frontend/src/scripts/theme.ts
index 62a2b9459a..42cb00265d 100644
--- a/packages/frontend/src/scripts/theme.ts
+++ b/packages/frontend/src/scripts/theme.ts
@@ -14,6 +14,7 @@ export type Theme = {
 import lightTheme from '@/themes/_light.json5';
 import darkTheme from '@/themes/_dark.json5';
 import { deepClone } from './clone';
+import { miLocalStorage } from '@/local-storage';
 
 export const themeProps = Object.keys(lightTheme.props).filter(key => !key.startsWith('X'));
 
@@ -84,8 +85,8 @@ export function applyTheme(theme: Theme, persist = true) {
 	document.documentElement.style.setProperty('color-schema', colorSchema);
 
 	if (persist) {
-		localStorage.setItem('theme', JSON.stringify(props));
-		localStorage.setItem('colorSchema', colorSchema);
+		miLocalStorage.setItem('theme', JSON.stringify(props));
+		miLocalStorage.setItem('colorSchema', colorSchema);
 	}
 
 	// 色計算など再度行えるようにクライアント全体に通知
diff --git a/packages/frontend/src/scripts/use-interval.ts b/packages/frontend/src/scripts/use-interval.ts
index 201ba417ef..601dea6724 100644
--- a/packages/frontend/src/scripts/use-interval.ts
+++ b/packages/frontend/src/scripts/use-interval.ts
@@ -3,7 +3,7 @@ import { onMounted, onUnmounted } from 'vue';
 export function useInterval(fn: () => void, interval: number, options: {
 	immediate: boolean;
 	afterMounted: boolean;
-}): void {
+}): (() => void) | undefined {
 	if (Number.isNaN(interval)) return;
 
 	let intervalId: number | null = null;
@@ -18,7 +18,14 @@ export function useInterval(fn: () => void, interval: number, options: {
 		intervalId = window.setInterval(fn, interval);
 	}
 
-	onUnmounted(() => {
+	const clear = () => {
 		if (intervalId) window.clearInterval(intervalId);
+		intervalId = null;
+	};
+
+	onUnmounted(() => {
+		clear();
 	});
+
+	return clear;
 }
diff --git a/packages/frontend/src/store.ts b/packages/frontend/src/store.ts
index 4b1f47c2bc..b2bf8db646 100644
--- a/packages/frontend/src/store.ts
+++ b/packages/frontend/src/store.ts
@@ -274,7 +274,7 @@ export const defaultStore = markRaw(new Storage('base', {
 
 // TODO: 他のタブと永続化されたstateを同期
 
-const PREFIX = 'miux:';
+const PREFIX = 'miux:' as const;
 
 type Plugin = {
 	id: string;
@@ -296,6 +296,7 @@ interface Watcher {
 import lightTheme from '@/themes/l-light.json5';
 import darkTheme from '@/themes/d-green-lime.json5';
 import { Note, UserDetailed } from 'misskey-js/built/entities';
+import { miLocalStorage } from './local-storage';
 
 export class ColdDeviceStorage {
 	public static default = {
@@ -320,7 +321,7 @@ export class ColdDeviceStorage {
 		// TODO: indexedDBにする
 		//       ただしその際はnullチェックではなくキー存在チェックにしないとダメ
 		//       (indexedDBはnullを保存できるため、ユーザーが意図してnullを格納した可能性がある)
-		const value = localStorage.getItem(PREFIX + key);
+		const value = miLocalStorage.getItem(`${PREFIX}${key}`);
 		if (value == null) {
 			return ColdDeviceStorage.default[key];
 		} else {
@@ -330,14 +331,14 @@ export class ColdDeviceStorage {
 
 	public static set<T extends keyof typeof ColdDeviceStorage.default>(key: T, value: typeof ColdDeviceStorage.default[T]): void {
 		// 呼び出し側のバグ等で undefined が来ることがある
-		// undefined を文字列として localStorage に入れると参照する際の JSON.parse でコケて不具合の元になるため無視
+		// undefined を文字列として miLocalStorage に入れると参照する際の JSON.parse でコケて不具合の元になるため無視
 		// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
 		if (value === undefined) {
 			console.error(`attempt to store undefined value for key '${key}'`);
 			return;
 		}
 
-		localStorage.setItem(PREFIX + key, JSON.stringify(value));
+		miLocalStorage.setItem(`${PREFIX}${key}`, JSON.stringify(value));
 
 		for (const watcher of this.watchers) {
 			if (watcher.key === key) watcher.callback(value);
diff --git a/packages/frontend/src/style.scss b/packages/frontend/src/style.scss
index f4369884f7..3cd7602423 100644
--- a/packages/frontend/src/style.scss
+++ b/packages/frontend/src/style.scss
@@ -252,64 +252,32 @@ hr {
 	overflow: clip;
 }
 
-._block {
-	@extend ._panel;
-
-	& + ._block {
-		margin-top: var(--margin);
-	}
-}
-
-._gap {
+._margin {
 	margin: var(--margin) 0;
 }
 
-// TODO: 廃止
-._card {
-	@extend ._panel;
+._gaps_m {
+	display: flex;
+	flex-direction: column;
+	gap: 1.5em;
+}
 
-	// TODO: _cardTitle に
-	> ._title {
-		margin: 0;
-		padding: 22px 32px;
-		font-size: 1em;
-		border-bottom: solid 1px var(--panelHeaderDivider);
-		font-weight: bold;
-		background: var(--panelHeaderBg);
-		color: var(--panelHeaderFg);
+._gaps_s {
+	display: flex;
+	flex-direction: column;
+	gap: 0.75em;
+}
 
-		@media (max-width: 500px) {
-			padding: 16px;
-			font-size: 1em;
-		}
-	}
+._gaps {
+	display: flex;
+	flex-direction: column;
+	gap: var(--margin);
+}
 
-	// TODO: _cardContent に
-	> ._content {
-		padding: 32px;
-
-		@media (max-width: 500px) {
-			padding: 16px;
-		}
-
-		&._noPad {
-			padding: 0 !important;
-		}
-
-		& + ._content {
-			border-top: solid 0.5px var(--divider);
-		}
-	}
-
-	// TODO: _cardFooter に
-	> ._footer {
-		border-top: solid 0.5px var(--divider);
-		padding: 24px 32px;
-
-		@media (max-width: 500px) {
-			padding: 16px;
-		}
-	}
+._buttons {
+	display: flex;
+	gap: 8px;
+	flex-wrap: wrap;
 }
 
 ._borderButton {
@@ -333,57 +301,12 @@ hr {
 	contain: content;
 }
 
-// TODO: 廃止
-._monolithic_ {
-	._section:not(:empty) {
-		box-sizing: border-box;
-		padding: var(--root-margin, 32px);
-	
-		@media (max-width: 500px) {
-			--root-margin: 10px;
-		}
-	
-		& + ._section:not(:empty) {
-			border-top: solid 0.5px var(--divider);
-		}
-	}
-}
-
-._narrow_ ._card {
-	> ._title {
-		padding: 16px;
-		font-size: 1em;
-	}
-
-	> ._content {
-		padding: 16px;
-	}
-
-	> ._footer {
-		padding: 16px;
-	}
-}
-
 ._acrylic {
 	background: var(--acrylicPanel);
 	-webkit-backdrop-filter: var(--blur, blur(15px));
 	backdrop-filter: var(--blur, blur(15px));
 }
 
-._formBlock {
-	margin: 1.5em 0;
-}
-
-._formRoot {
-	> ._formBlock:first-child {
-		margin-top: 0;
-	}
-
-	> ._formBlock:last-child {
-		margin-bottom: 0;
-	}
-}
-
 ._formLinksGrid {
 	display: grid;
 	grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
@@ -534,7 +457,7 @@ hr {
 	transition: transform 0.1s ease;
 }
 
-@keyframes bounce{
+@keyframes bounce {
   0% {
     transform:  scaleX(0.90) scaleY(0.90) ;
   }
diff --git a/packages/frontend/src/theme-store.ts b/packages/frontend/src/theme-store.ts
index fdc92ed793..aa1244665b 100644
--- a/packages/frontend/src/theme-store.ts
+++ b/packages/frontend/src/theme-store.ts
@@ -1,11 +1,13 @@
 import { api } from '@/os';
 import { $i } from '@/account';
 import { Theme } from './scripts/theme';
+import { miLocalStorage } from './local-storage';
 
-const lsCacheKey = $i ? `themes:${$i.id}` : '';
+const lsCacheKey = $i ? `themes:${$i.id}` as const : null;
 
 export function getThemes(): Theme[] {
-	return JSON.parse(localStorage.getItem(lsCacheKey) || '[]');
+	if ($i == null) return [];
+	return JSON.parse(miLocalStorage.getItem(lsCacheKey!) || '[]');
 }
 
 export async function fetchThemes(): Promise<void> {
@@ -13,7 +15,7 @@ export async function fetchThemes(): Promise<void> {
 
 	try {
 		const themes = await api('i/registry/get', { scope: ['client'], key: 'themes' });
-		localStorage.setItem(lsCacheKey, JSON.stringify(themes));
+		miLocalStorage.setItem(lsCacheKey!, JSON.stringify(themes));
 	} catch (err) {
 		if (err.code === 'NO_SUCH_KEY') return;
 		throw err;
@@ -21,14 +23,16 @@ export async function fetchThemes(): Promise<void> {
 }
 
 export async function addTheme(theme: Theme): Promise<void> {
+	if ($i == null) return;
 	await fetchThemes();
 	const themes = getThemes().concat(theme);
 	await api('i/registry/set', { scope: ['client'], key: 'themes', value: themes });
-	localStorage.setItem(lsCacheKey, JSON.stringify(themes));
+	miLocalStorage.setItem(lsCacheKey!, JSON.stringify(themes));
 }
 
 export async function removeTheme(theme: Theme): Promise<void> {
+	if ($i == null) return;
 	const themes = getThemes().filter(t => t.id !== theme.id);
 	await api('i/registry/set', { scope: ['client'], key: 'themes', value: themes });
-	localStorage.setItem(lsCacheKey, JSON.stringify(themes));
+	miLocalStorage.setItem(lsCacheKey!, JSON.stringify(themes));
 }
diff --git a/packages/frontend/src/ui/_common_/navbar-for-mobile.vue b/packages/frontend/src/ui/_common_/navbar-for-mobile.vue
index ac109d9def..989d861d27 100644
--- a/packages/frontend/src/ui/_common_/navbar-for-mobile.vue
+++ b/packages/frontend/src/ui/_common_/navbar-for-mobile.vue
@@ -14,7 +14,7 @@
 			<template v-for="item in menu">
 				<div v-if="item === '-'" class="divider"></div>
 				<component :is="navbarItemDef[item].to ? 'MkA' : 'button'" v-else-if="navbarItemDef[item] && (navbarItemDef[item].show !== false)" v-click-anime class="item _button" :class="[item, { active: navbarItemDef[item].active }]" active-class="active" :to="navbarItemDef[item].to" v-on="navbarItemDef[item].action ? { click: navbarItemDef[item].action } : {}">
-					<i class="icon ti-fw" :class="navbarItemDef[item].icon"></i><span class="text">{{ i18n.ts[navbarItemDef[item].title] }}</span>
+					<i class="icon ti-fw" :class="navbarItemDef[item].icon"></i><span class="text">{{ navbarItemDef[item].title }}</span>
 					<span v-if="navbarItemDef[item].indicated" class="indicator"><i class="icon _indicatorCircle"></i></span>
 				</component>
 			</template>
diff --git a/packages/frontend/src/ui/_common_/navbar.vue b/packages/frontend/src/ui/_common_/navbar.vue
index 7c859bf3aa..e90098397a 100644
--- a/packages/frontend/src/ui/_common_/navbar.vue
+++ b/packages/frontend/src/ui/_common_/navbar.vue
@@ -17,14 +17,14 @@
 					:is="navbarItemDef[item].to ? 'MkA' : 'button'"
 					v-else-if="navbarItemDef[item] && (navbarItemDef[item].show !== false)"
 					v-click-anime
-					v-tooltip.noDelay.right="i18n.ts[navbarItemDef[item].title]"
+					v-tooltip.noDelay.right="navbarItemDef[item].title"
 					class="item _button"
 					:class="[item, { active: navbarItemDef[item].active }]"
 					active-class="active"
 					:to="navbarItemDef[item].to"
 					v-on="navbarItemDef[item].action ? { click: navbarItemDef[item].action } : {}"
 				>
-					<i class="icon ti-fw" :class="navbarItemDef[item].icon"></i><span class="text">{{ i18n.ts[navbarItemDef[item].title] }}</span>
+					<i class="icon ti-fw" :class="navbarItemDef[item].icon"></i><span class="text">{{ navbarItemDef[item].title }}</span>
 					<span v-if="navbarItemDef[item].indicated" class="indicator"><i class="icon _indicatorCircle"></i></span>
 				</component>
 			</template>
diff --git a/packages/frontend/src/ui/classic.header.vue b/packages/frontend/src/ui/classic.header.vue
index 77a64aac37..34ddfa1d32 100644
--- a/packages/frontend/src/ui/classic.header.vue
+++ b/packages/frontend/src/ui/classic.header.vue
@@ -10,7 +10,7 @@
 			</MkA>
 			<template v-for="item in menu">
 				<div v-if="item === '-'" class="divider"></div>
-				<component :is="navbarItemDef[item].to ? 'MkA' : 'button'" v-else-if="navbarItemDef[item] && (navbarItemDef[item].show !== false)" v-click-anime v-tooltip="$ts[navbarItemDef[item].title]" class="item _button" :class="item" active-class="active" :to="navbarItemDef[item].to" v-on="navbarItemDef[item].action ? { click: navbarItemDef[item].action } : {}">
+				<component :is="navbarItemDef[item].to ? 'MkA' : 'button'" v-else-if="navbarItemDef[item] && (navbarItemDef[item].show !== false)" v-click-anime v-tooltip="navbarItemDef[item].title" class="item _button" :class="item" active-class="active" :to="navbarItemDef[item].to" v-on="navbarItemDef[item].action ? { click: navbarItemDef[item].action } : {}">
 					<i class="ti-fw" :class="navbarItemDef[item].icon"></i>
 					<span v-if="navbarItemDef[item].indicated" class="indicator"><i class="_indicatorCircle"></i></span>
 				</component>
diff --git a/packages/frontend/src/ui/classic.sidebar.vue b/packages/frontend/src/ui/classic.sidebar.vue
index ec379fbaa7..a11c2ba10e 100644
--- a/packages/frontend/src/ui/classic.sidebar.vue
+++ b/packages/frontend/src/ui/classic.sidebar.vue
@@ -15,7 +15,7 @@
 	<template v-for="item in menu">
 		<div v-if="item === '-'" class="divider"></div>
 		<component :is="navbarItemDef[item].to ? 'MkA' : 'button'" v-else-if="navbarItemDef[item] && (navbarItemDef[item].show !== false)" v-click-anime class="item _button" :class="item" active-class="active" :to="navbarItemDef[item].to" v-on="navbarItemDef[item].action ? { click: navbarItemDef[item].action } : {}">
-			<i class="ti-fw" :class="navbarItemDef[item].icon"></i><span class="text">{{ $ts[navbarItemDef[item].title] }}</span>
+			<i class="ti-fw" :class="navbarItemDef[item].icon"></i><span class="text">{{ navbarItemDef[item].title }}</span>
 			<span v-if="navbarItemDef[item].indicated" class="indicator"><i class="_indicatorCircle"></i></span>
 		</component>
 	</template>
diff --git a/packages/frontend/src/ui/classic.vue b/packages/frontend/src/ui/classic.vue
index 280e69e7dd..f220501ee2 100644
--- a/packages/frontend/src/ui/classic.vue
+++ b/packages/frontend/src/ui/classic.vue
@@ -51,6 +51,7 @@ import { mainRouter } from '@/router';
 import { PageMetadata, provideMetadataReceiver, setPageMetadata } from '@/scripts/page-metadata';
 import { defaultStore } from '@/store';
 import { i18n } from '@/i18n';
+import { miLocalStorage } from '@/local-storage';
 const XHeaderMenu = defineAsyncComponent(() => import('./classic.header.vue'));
 const XWidgets = defineAsyncComponent(() => import('./universal.widgets.vue'));
 
@@ -62,7 +63,7 @@ let pageMetadata = $ref<null | ComputedRef<PageMetadata>>();
 let widgetsShowing = $ref(false);
 let fullView = $ref(false);
 let globalHeaderHeight = $ref(0);
-const wallpaper = localStorage.getItem('wallpaper') != null;
+const wallpaper = miLocalStorage.getItem('wallpaper') != null;
 const showMenuOnTop = $computed(() => defaultStore.state.menuDisplay === 'top');
 let live2d = $shallowRef<HTMLIFrameElement>();
 let widgetsLeft = $ref();
@@ -123,7 +124,7 @@ function onAiClick(ev) {
 }
 
 if (window.innerWidth < 1024) {
-	localStorage.setItem('ui', 'default');
+	miLocalStorage.setItem('ui', 'default');
 	location.reload();
 }
 
diff --git a/packages/frontend/src/ui/deck/column.vue b/packages/frontend/src/ui/deck/column.vue
index 775bdf6c1e..cb945aa14b 100644
--- a/packages/frontend/src/ui/deck/column.vue
+++ b/packages/frontend/src/ui/deck/column.vue
@@ -1,7 +1,7 @@
 <template>
 <!-- sectionを利用しているのは、deck.vue側でcolumnに対してfirst-of-typeを効かせるため -->
 <section
-	v-hotkey="keymap" class="dnpfarvg _narrow_"
+	v-hotkey="keymap" class="dnpfarvg"
 	:class="{ paged: isMainColumn, naked, active, isStacked, draghover, dragging, dropready }"
 	@dragover.prevent.stop="onDragover"
 	@dragleave="onDragleave"
diff --git a/packages/frontend/src/ui/universal.vue b/packages/frontend/src/ui/universal.vue
index 9e1fee5b6b..06129ffc87 100644
--- a/packages/frontend/src/ui/universal.vue
+++ b/packages/frontend/src/ui/universal.vue
@@ -71,6 +71,7 @@ import { Router } from '@/nirax';
 import { mainRouter } from '@/router';
 import { PageMetadata, provideMetadataReceiver, setPageMetadata } from '@/scripts/page-metadata';
 import { deviceKind } from '@/scripts/device-kind';
+import { miLocalStorage } from '@/local-storage';
 const XWidgets = defineAsyncComponent(() => import('./universal.widgets.vue'));
 const XSidebar = defineAsyncComponent(() => import('@/ui/_common_/navbar.vue'));
 const XStatusBars = defineAsyncComponent(() => import('@/ui/_common_/statusbars.vue'));
@@ -170,7 +171,7 @@ function top() {
 	window.scroll({ top: 0, behavior: 'smooth' });
 }
 
-const wallpaper = localStorage.getItem('wallpaper') != null;
+const wallpaper = miLocalStorage.getItem('wallpaper') != null;
 </script>
 
 <style lang="scss" scoped>
diff --git a/packages/frontend/src/widgets/aiscript-app.vue b/packages/frontend/src/widgets/aiscript-app.vue
new file mode 100644
index 0000000000..1445e5b1ad
--- /dev/null
+++ b/packages/frontend/src/widgets/aiscript-app.vue
@@ -0,0 +1,122 @@
+<template>
+<MkContainer :show-header="widgetProps.showHeader" class="mkw-aiscriptApp">
+	<template #header>App</template>
+	<div :class="$style.root">
+		<MkAsUi v-if="root" :component="root" :components="components" size="small"/>
+	</div>
+</MkContainer>
+</template>
+
+<script lang="ts" setup>
+import { onMounted, onUnmounted, Ref, ref, watch } from 'vue';
+import { Interpreter, Parser, utils, values } from '@syuilo/aiscript';
+import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget';
+import { GetFormResultType } from '@/scripts/form';
+import * as os from '@/os';
+import { createAiScriptEnv } from '@/scripts/aiscript/api';
+import { $i } from '@/account';
+import MkAsUi from '@/components/MkAsUi.vue';
+import MkContainer from '@/components/MkContainer.vue';
+import { AsUiComponent, AsUiRoot, patch, registerAsUiLib, render } from '@/scripts/aiscript/ui';
+
+const name = 'aiscriptApp';
+
+const widgetPropsDef = {
+	script: {
+		type: 'string' as const,
+		multiline: true,
+		default: '',
+	},
+	showHeader: {
+		type: 'boolean' as const,
+		default: true,
+	},
+};
+
+type WidgetProps = GetFormResultType<typeof widgetPropsDef>;
+
+// 現時点ではvueの制限によりimportしたtypeをジェネリックに渡せない
+//const props = defineProps<WidgetComponentProps<WidgetProps>>();
+//const emit = defineEmits<WidgetComponentEmits<WidgetProps>>();
+const props = defineProps<{ widget?: Widget<WidgetProps>; }>();
+const emit = defineEmits<{ (ev: 'updateProps', props: WidgetProps); }>();
+
+const { widgetProps, configure } = useWidgetPropsManager(name,
+	widgetPropsDef,
+	props,
+	emit,
+);
+
+const parser = new Parser();
+
+const root = ref<AsUiRoot>();
+const components: Ref<AsUiComponent>[] = [];
+
+async function run() {
+	const aiscript = new Interpreter({
+		...createAiScriptEnv({
+			storageKey: 'widget',
+			token: $i?.token,
+		}),
+		...registerAsUiLib(components, (_root) => {
+			root.value = _root.value;
+		}),
+	}, {
+		in: (q) => {
+			return new Promise(ok => {
+				os.inputText({
+					title: q,
+				}).then(({ canceled, result: a }) => {
+					ok(a);
+				});
+			});
+		},
+		out: (value) => {
+			// nop
+		},
+		log: (type, params) => {
+			// nop
+		},
+	});
+
+	let ast;
+	try {
+		ast = parser.parse(widgetProps.script);
+	} catch (err) {
+		os.alert({
+			type: 'error',
+			text: 'Syntax error :(',
+		});
+		return;
+	}
+	try {
+		await aiscript.exec(ast);
+	} catch (err) {
+		os.alert({
+			type: 'error',
+			title: 'AiScript Error',
+			text: err.message,
+		});
+	}
+}
+
+watch(() => widgetProps.script, () => {
+	run();
+});
+
+onMounted(() => {
+	run();
+});
+
+defineExpose<WidgetComponentExpose>({
+	name,
+	configure,
+	id: props.widget ? props.widget.id : null,
+});
+</script>
+
+<style lang="scss" module>
+.root {
+	padding: 16px;
+}
+</style>
diff --git a/packages/frontend/src/widgets/federation.vue b/packages/frontend/src/widgets/federation.vue
index a701ca5673..23f7cd4411 100644
--- a/packages/frontend/src/widgets/federation.vue
+++ b/packages/frontend/src/widgets/federation.vue
@@ -8,7 +8,7 @@
 			<div v-for="(instance, i) in instances" :key="instance.id" class="instance">
 				<img :src="getInstanceIcon(instance)" alt=""/>
 				<div class="body">
-					<a class="a" :href="'https://' + instance.host" target="_blank" :title="instance.host">{{ instance.host }}</a>
+					<MkA class="a" :to="`/instance-info/${instance.host}`" behavior="window" :title="instance.host">{{ instance.host }}</MkA>
 					<p>{{ instance.softwareName || '?' }} {{ instance.softwareVersion }}</p>
 				</div>
 				<MkMiniChart class="chart" :src="charts[i].requests.received"/>
diff --git a/packages/frontend/src/widgets/index.ts b/packages/frontend/src/widgets/index.ts
index 39826f13c8..3966649da4 100644
--- a/packages/frontend/src/widgets/index.ts
+++ b/packages/frontend/src/widgets/index.ts
@@ -22,6 +22,7 @@ export default function(app: App) {
 	app.component('MkwInstanceCloud', defineAsyncComponent(() => import('./instance-cloud.vue')));
 	app.component('MkwButton', defineAsyncComponent(() => import('./button.vue')));
 	app.component('MkwAiscript', defineAsyncComponent(() => import('./aiscript.vue')));
+	app.component('MkwAiscriptApp', defineAsyncComponent(() => import('./aiscript-app.vue')));
 	app.component('MkwAichan', defineAsyncComponent(() => import('./aichan.vue')));
 	app.component('MkwUserList', defineAsyncComponent(() => import('./user-list.vue')));
 }
@@ -48,6 +49,7 @@ export const widgets = [
 	'jobQueue',
 	'button',
 	'aiscript',
+	'aiscriptApp',
 	'aichan',
 	'userList',
 ];
diff --git a/packages/frontend/src/widgets/rss-ticker.vue b/packages/frontend/src/widgets/rss-ticker.vue
index c2d6dd2873..37672e13cf 100644
--- a/packages/frontend/src/widgets/rss-ticker.vue
+++ b/packages/frontend/src/widgets/rss-ticker.vue
@@ -3,13 +3,15 @@
 	<template #header><i class="ti ti-rss"></i>RSS</template>
 	<template #func><button class="_button" @click="configure"><i class="ti ti-settings"></i></button></template>
 
-	<div class="ekmkgxbk">
-		<MkLoading v-if="fetching"/>
-		<div v-else class="feed">
-			<Transition name="change" mode="default">
+	<div :class="$style.feed">
+		<div v-if="fetching" :class="$style.loading">
+			<MkEllipsis/>
+		</div>
+		<div v-else>
+			<Transition :name="$style.change" mode="default" appear>
 				<MarqueeText :key="key" :duration="widgetProps.duration" :reverse="widgetProps.reverse">
-					<span v-for="item in items" class="item">
-						<a class="link" :href="item.link" rel="nofollow noopener" target="_blank" :title="item.title">{{ item.title }}</a><span class="divider"></span>
+					<span v-for="item in items" :class="$style.item" :key="item.link">
+						<a :class="$style.link" :href="item.link" rel="nofollow noopener" target="_blank" :title="item.title">{{ item.title }}</a><span :class="$style.divider"></span>
 					</span>
 				</MarqueeText>
 			</Transition>
@@ -19,14 +21,14 @@
 </template>
 
 <script lang="ts" setup>
-import { onMounted, onUnmounted, ref, watch } from 'vue';
+import { ref, watch, computed } from 'vue';
 import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget';
 import MarqueeText from '@/components/MkMarquee.vue';
 import { GetFormResultType } from '@/scripts/form';
-import * as os from '@/os';
 import MkContainer from '@/components/MkContainer.vue';
-import { useInterval } from '@/scripts/use-interval';
 import { shuffle } from '@/scripts/shuffle';
+import { url as base } from '@/config';
+import { useInterval } from '@/scripts/use-interval';
 
 const name = 'rssTicker';
 
@@ -43,6 +45,10 @@ const widgetPropsDef = {
 		type: 'number' as const,
 		default: 60,
 	},
+	maxEntries: {
+		type: 'number' as const,
+		default: 15,
+	},
 	duration: {
 		type: 'range' as const,
 		default: 70,
@@ -78,29 +84,49 @@ const { widgetProps, configure } = useWidgetPropsManager(name,
 	emit,
 );
 
-const items = ref([]);
+const rawItems = ref([]);
+const items = computed(() => {
+	const newItems = rawItems.value.slice(0, widgetProps.maxEntries)
+	if (widgetProps.shuffle) {
+		shuffle(newItems);
+	}
+	return newItems;
+});
 const fetching = ref(true);
+const fetchEndpoint = computed(() => {
+	const url = new URL('/api/fetch-rss', base);
+	url.searchParams.set('url', widgetProps.url);
+	return url;
+});
+let intervalClear = $ref<(() => void) | undefined>();
+
 let key = $ref(0);
 
 const tick = () => {
-	window.fetch(`/api/fetch-rss?url=${widgetProps.url}`, {}).then(res => {
-		res.json().then(feed => {
-			if (widgetProps.shuffle) {
-				shuffle(feed.items);
-			}
-			items.value = feed.items;
-			fetching.value = false;
-			key++;
-		});
+	if (document.visibilityState === 'hidden' && rawItems.value.length !== 0) return;
+
+	window.fetch(fetchEndpoint.value, {})
+	.then(res => res.json())
+	.then(feed => {
+		if (widgetProps.shuffle) {
+			shuffle(feed.items);
+		}
+		rawItems.value = feed.items;
+		fetching.value = false;
+		key++;
 	});
 };
 
-watch(() => widgetProps.url, tick);
-
-useInterval(tick, Math.max(10000, widgetProps.refreshIntervalSec * 1000), {
-	immediate: true,
-	afterMounted: true,
-});
+watch(() => fetchEndpoint, tick);
+watch(() => widgetProps.refreshIntervalSec, () => {
+	if (intervalClear) {
+		intervalClear();
+	}
+	intervalClear = useInterval(tick, Math.max(10000, widgetProps.refreshIntervalSec * 1000), {
+		immediate: true,
+		afterMounted: true,
+	});
+}, { immediate: true });
 
 defineExpose<WidgetComponentExpose>({
 	name,
@@ -109,44 +135,49 @@ defineExpose<WidgetComponentExpose>({
 });
 </script>
 
-<style lang="scss" scoped>
-.change-enter-active, .change-leave-active {
-	position: absolute;
-	top: 0;
-  transition: all 1s ease;
-}
-.change-enter-from {
-  opacity: 0;
-	transform: translateY(-100%);
-}
-.change-leave-to {
-  opacity: 0;
-	transform: translateY(100%);
-}
-
-.ekmkgxbk {
-	> .feed {
-		--height: 42px;
-		padding: 0;
-		font-size: 0.9em;
-		line-height: var(--height);
-		height: var(--height);
-		contain: strict;
-
-		::v-deep(.item) {
-			display: inline-flex;
-			align-items: center;
-			vertical-align: bottom;
-			color: var(--fg);
-
-			> .divider {
-				display: inline-block;
-				width: 0.5px;
-				height: 16px;
-				margin: 0 1em;
-				background: var(--divider);
-			}
-		}
+<style lang="scss" module>
+.change {
+	&:global(-enter-active),
+	&:global(-leave-active) {
+		position: absolute;
+		top: 0;
+		transition: all 1s ease;
+	}
+	&:global(-enter-from) {
+		opacity: 0;
+		transform: translateY(-100%);
+	}
+	&:global(-leave-to) {
+		opacity: 0;
+		transform: translateY(100%);
 	}
 }
+
+.feed {
+	--height: 42px;
+	padding: 0;
+	font-size: 0.9em;
+	line-height: var(--height);
+	height: var(--height);
+	contain: strict;
+}
+
+.loading {
+	text-align: center;
+}
+
+.item {
+	display: inline-flex;
+	align-items: center;
+	vertical-align: bottom;
+	color: var(--fg);
+}
+
+.divider {
+	display: inline-block;
+	width: 0.5px;
+	height: 16px;
+	margin: 0 1em;
+	background: var(--divider);
+}
 </style>
diff --git a/packages/frontend/src/widgets/rss.vue b/packages/frontend/src/widgets/rss.vue
index c0338c8e47..554413cd1e 100644
--- a/packages/frontend/src/widgets/rss.vue
+++ b/packages/frontend/src/widgets/rss.vue
@@ -5,19 +5,24 @@
 
 	<div class="ekmkgxbj">
 		<MkLoading v-if="fetching"/>
-		<div v-else class="feed">
-			<a v-for="item in items" class="item" :href="item.link" rel="nofollow noopener" target="_blank" :title="item.title">{{ item.title }}</a>
+		<div class="_fullinfo" v-else-if="(!items || items.length === 0) && widgetProps.showHeader">
+			<img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/>
+			<div>{{ i18n.ts.nothing }}</div>
+		</div>
+		<div v-else :class="$style.feed">
+			<a v-for="item in items" :class="$style.item" :href="item.link" :key="item.link" rel="nofollow noopener" target="_blank" :title="item.title">{{ item.title }}</a>
 		</div>
 	</div>
 </MkContainer>
 </template>
 
 <script lang="ts" setup>
-import { onMounted, onUnmounted, ref, watch } from 'vue';
+import { ref, watch, computed } from 'vue';
 import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget';
 import { GetFormResultType } from '@/scripts/form';
-import * as os from '@/os';
 import MkContainer from '@/components/MkContainer.vue';
+import { url as base } from '@/config';
+import { i18n } from '@/i18n';
 import { useInterval } from '@/scripts/use-interval';
 
 const name = 'rss';
@@ -27,6 +32,14 @@ const widgetPropsDef = {
 		type: 'string' as const,
 		default: 'http://feeds.afpbb.com/rss/afpbb/afpbbnews',
 	},
+	refreshIntervalSec: {
+		type: 'number' as const,
+		default: 60,
+	},
+	maxEntries: {
+		type: 'number' as const,
+		default: 15,
+	},
 	showHeader: {
 		type: 'boolean' as const,
 		default: true,
@@ -47,24 +60,37 @@ const { widgetProps, configure } = useWidgetPropsManager(name,
 	emit,
 );
 
-const items = ref([]);
+const rawItems = ref([]);
+const items = computed(() => rawItems.value.slice(0, widgetProps.maxEntries));
 const fetching = ref(true);
+const fetchEndpoint = computed(() => {
+	const url = new URL('/api/fetch-rss', base);
+	url.searchParams.set('url', widgetProps.url);
+	return url;
+});
+let intervalClear = $ref<(() => void) | undefined>();
 
 const tick = () => {
-	window.fetch(`/api/fetch-rss?url=${widgetProps.url}`, {}).then(res => {
-		res.json().then(feed => {
-			items.value = feed.items;
-			fetching.value = false;
-		});
+	if (document.visibilityState === 'hidden' && rawItems.value.length !== 0) return;
+
+	window.fetch(fetchEndpoint.value, {})
+	.then(res => res.json())
+	.then(feed => {
+		rawItems.value = feed.items ?? [];
+		fetching.value = false;
 	});
 };
 
-watch(() => widgetProps.url, tick);
-
-useInterval(tick, 60000, {
-	immediate: true,
-	afterMounted: true,
-});
+watch(() => fetchEndpoint, tick);
+watch(() => widgetProps.refreshIntervalSec, () => {
+	if (intervalClear) {
+		intervalClear();
+	}
+	intervalClear = useInterval(tick, Math.max(10000, widgetProps.refreshIntervalSec * 1000), {
+		immediate: true,
+		afterMounted: true,
+	});
+}, { immediate: true });
 
 defineExpose<WidgetComponentExpose>({
 	name,
@@ -73,24 +99,22 @@ defineExpose<WidgetComponentExpose>({
 });
 </script>
 
-<style lang="scss" scoped>
-.ekmkgxbj {
-	> .feed {
-		padding: 0;
-		font-size: 0.9em;
+<style lang="scss" module>
+.feed {
+	padding: 0;
+	font-size: 0.9em;
+}
 
-		> .item {
-			display: block;
-			padding: 8px 16px;
-			color: var(--fg);
-			white-space: nowrap;
-			text-overflow: ellipsis;
-			overflow: hidden;
+.item {
+	display: block;
+	padding: 8px 16px;
+	color: var(--fg);
+	white-space: nowrap;
+	text-overflow: ellipsis;
+	overflow: hidden;
 
-			&:nth-child(even) {
-				background: rgba(#000, 0.05);
-			}
-		}
+	&:nth-child(even) {
+		background: rgba(#000, 0.05);
 	}
 }
 </style>
diff --git a/scripts/clean-all.js b/scripts/clean-all.js
index c65a1c3a32..563b6bc922 100644
--- a/scripts/clean-all.js
+++ b/scripts/clean-all.js
@@ -1,3 +1,4 @@
+const { execSync } = require('child_process');
 const fs = require('fs');
 
 (async () => {
@@ -12,5 +13,9 @@ const fs = require('fs');
 
 	fs.rmSync(__dirname + '/../built', { recursive: true, force: true });
 	fs.rmSync(__dirname + '/../node_modules', { recursive: true, force: true });
-	fs.rmSync(__dirname + '/../.yarn/cache', { recursive: true, force: true });
+
+	execSync('yarn cache clean --all', {
+		cwd: __dirname + '/../',
+		stdio: 'inherit',
+	});
 })();
diff --git a/yarn.lock b/yarn.lock
index 9eaf29f1b3..eb7ff21cec 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -417,34 +417,34 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@bull-board/api@npm:4.10.0, @bull-board/api@npm:^4.10.0":
-  version: 4.10.0
-  resolution: "@bull-board/api@npm:4.10.0"
+"@bull-board/api@npm:4.10.1, @bull-board/api@npm:^4.10.1":
+  version: 4.10.1
+  resolution: "@bull-board/api@npm:4.10.1"
   dependencies:
     redis-info: ^3.0.8
-  checksum: 886ee7955283056fd40b75bfa63bd174c06bedd8086a3a54804548015ebfc1ba9ff8ec5e76ed2f0a3beeda4b5b5ab32531f35cd77cd6ab5ed7e4978fe426ae84
+  checksum: 32c9e88df3dd73f513f8d917df3896a58f182417d75bf43d3446c00cb3c0d17a27ef26a6da40d2304350a614ed1be8d1de30002184a03c6c334c6f84faa85a83
   languageName: node
   linkType: hard
 
-"@bull-board/fastify@npm:^4.10.0":
-  version: 4.10.0
-  resolution: "@bull-board/fastify@npm:4.10.0"
+"@bull-board/fastify@npm:^4.10.1":
+  version: 4.10.1
+  resolution: "@bull-board/fastify@npm:4.10.1"
   dependencies:
-    "@bull-board/api": 4.10.0
-    "@bull-board/ui": 4.10.0
+    "@bull-board/api": 4.10.1
+    "@bull-board/ui": 4.10.1
     "@fastify/static": ^6.4.0
     "@fastify/view": ^7.0.0
     ejs: ^3.1.8
-  checksum: c71d52a49bae6025a1ef4bd3b76bf6a15fb12d8e52f89128c6a9bbbd0e71195d55b3e9b7ccb4608476ac99befe334c6d7fbbea9a7227662047ad7e30ea9174a1
+  checksum: e6a0597fc682ff89a31a09d61861f0ec84941538a1ca10f55ffaef1f53b8989c16923725515b1e74112ea169a74fb357765a64e7e75d78d1628edb586958bc0f
   languageName: node
   linkType: hard
 
-"@bull-board/ui@npm:4.10.0, @bull-board/ui@npm:^4.10.0":
-  version: 4.10.0
-  resolution: "@bull-board/ui@npm:4.10.0"
+"@bull-board/ui@npm:4.10.1, @bull-board/ui@npm:^4.10.1":
+  version: 4.10.1
+  resolution: "@bull-board/ui@npm:4.10.1"
   dependencies:
-    "@bull-board/api": 4.10.0
-  checksum: 0322523c486ebeae7c31af26e286ce253ed01f09808b4370bde12ba19ac1b0cc888ecc099dc6dba74a63e65b1b14664dd216f1f9880dc893396b24a7a4b1ea90
+    "@bull-board/api": 4.10.1
+  checksum: b221444ccd836d26a66501b5cd1702710f65ab7a5f0f6d92a0168b91ffde11a75a41ef334ea77a7f0c2b1caa7b5e136fe489242028eca1a5c7ed11ace24365d9
   languageName: node
   linkType: hard
 
@@ -1850,90 +1850,90 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@swc/core-darwin-arm64@npm:1.3.24":
-  version: 1.3.24
-  resolution: "@swc/core-darwin-arm64@npm:1.3.24"
+"@swc/core-darwin-arm64@npm:1.3.25":
+  version: 1.3.25
+  resolution: "@swc/core-darwin-arm64@npm:1.3.25"
   conditions: os=darwin & cpu=arm64
   languageName: node
   linkType: hard
 
-"@swc/core-darwin-x64@npm:1.3.24":
-  version: 1.3.24
-  resolution: "@swc/core-darwin-x64@npm:1.3.24"
+"@swc/core-darwin-x64@npm:1.3.25":
+  version: 1.3.25
+  resolution: "@swc/core-darwin-x64@npm:1.3.25"
   conditions: os=darwin & cpu=x64
   languageName: node
   linkType: hard
 
-"@swc/core-linux-arm-gnueabihf@npm:1.3.24":
-  version: 1.3.24
-  resolution: "@swc/core-linux-arm-gnueabihf@npm:1.3.24"
+"@swc/core-linux-arm-gnueabihf@npm:1.3.25":
+  version: 1.3.25
+  resolution: "@swc/core-linux-arm-gnueabihf@npm:1.3.25"
   conditions: os=linux & cpu=arm
   languageName: node
   linkType: hard
 
-"@swc/core-linux-arm64-gnu@npm:1.3.24":
-  version: 1.3.24
-  resolution: "@swc/core-linux-arm64-gnu@npm:1.3.24"
+"@swc/core-linux-arm64-gnu@npm:1.3.25":
+  version: 1.3.25
+  resolution: "@swc/core-linux-arm64-gnu@npm:1.3.25"
   conditions: os=linux & cpu=arm64 & libc=glibc
   languageName: node
   linkType: hard
 
-"@swc/core-linux-arm64-musl@npm:1.3.24":
-  version: 1.3.24
-  resolution: "@swc/core-linux-arm64-musl@npm:1.3.24"
+"@swc/core-linux-arm64-musl@npm:1.3.25":
+  version: 1.3.25
+  resolution: "@swc/core-linux-arm64-musl@npm:1.3.25"
   conditions: os=linux & cpu=arm64 & libc=musl
   languageName: node
   linkType: hard
 
-"@swc/core-linux-x64-gnu@npm:1.3.24":
-  version: 1.3.24
-  resolution: "@swc/core-linux-x64-gnu@npm:1.3.24"
+"@swc/core-linux-x64-gnu@npm:1.3.25":
+  version: 1.3.25
+  resolution: "@swc/core-linux-x64-gnu@npm:1.3.25"
   conditions: os=linux & cpu=x64 & libc=glibc
   languageName: node
   linkType: hard
 
-"@swc/core-linux-x64-musl@npm:1.3.24":
-  version: 1.3.24
-  resolution: "@swc/core-linux-x64-musl@npm:1.3.24"
+"@swc/core-linux-x64-musl@npm:1.3.25":
+  version: 1.3.25
+  resolution: "@swc/core-linux-x64-musl@npm:1.3.25"
   conditions: os=linux & cpu=x64 & libc=musl
   languageName: node
   linkType: hard
 
-"@swc/core-win32-arm64-msvc@npm:1.3.24":
-  version: 1.3.24
-  resolution: "@swc/core-win32-arm64-msvc@npm:1.3.24"
+"@swc/core-win32-arm64-msvc@npm:1.3.25":
+  version: 1.3.25
+  resolution: "@swc/core-win32-arm64-msvc@npm:1.3.25"
   conditions: os=win32 & cpu=arm64
   languageName: node
   linkType: hard
 
-"@swc/core-win32-ia32-msvc@npm:1.3.24":
-  version: 1.3.24
-  resolution: "@swc/core-win32-ia32-msvc@npm:1.3.24"
+"@swc/core-win32-ia32-msvc@npm:1.3.25":
+  version: 1.3.25
+  resolution: "@swc/core-win32-ia32-msvc@npm:1.3.25"
   conditions: os=win32 & cpu=ia32
   languageName: node
   linkType: hard
 
-"@swc/core-win32-x64-msvc@npm:1.3.24":
-  version: 1.3.24
-  resolution: "@swc/core-win32-x64-msvc@npm:1.3.24"
+"@swc/core-win32-x64-msvc@npm:1.3.25":
+  version: 1.3.25
+  resolution: "@swc/core-win32-x64-msvc@npm:1.3.25"
   conditions: os=win32 & cpu=x64
   languageName: node
   linkType: hard
 
-"@swc/core@npm:1.3.24":
-  version: 1.3.24
-  resolution: "@swc/core@npm:1.3.24"
+"@swc/core@npm:1.3.25":
+  version: 1.3.25
+  resolution: "@swc/core@npm:1.3.25"
   dependencies:
-    "@swc/core-darwin-arm64": 1.3.24
-    "@swc/core-darwin-x64": 1.3.24
-    "@swc/core-linux-arm-gnueabihf": 1.3.24
-    "@swc/core-linux-arm64-gnu": 1.3.24
-    "@swc/core-linux-arm64-musl": 1.3.24
-    "@swc/core-linux-x64-gnu": 1.3.24
-    "@swc/core-linux-x64-musl": 1.3.24
-    "@swc/core-win32-arm64-msvc": 1.3.24
-    "@swc/core-win32-ia32-msvc": 1.3.24
-    "@swc/core-win32-x64-msvc": 1.3.24
+    "@swc/core-darwin-arm64": 1.3.25
+    "@swc/core-darwin-x64": 1.3.25
+    "@swc/core-linux-arm-gnueabihf": 1.3.25
+    "@swc/core-linux-arm64-gnu": 1.3.25
+    "@swc/core-linux-arm64-musl": 1.3.25
+    "@swc/core-linux-x64-gnu": 1.3.25
+    "@swc/core-linux-x64-musl": 1.3.25
+    "@swc/core-win32-arm64-msvc": 1.3.25
+    "@swc/core-win32-ia32-msvc": 1.3.25
+    "@swc/core-win32-x64-msvc": 1.3.25
   dependenciesMeta:
     "@swc/core-darwin-arm64":
       optional: true
@@ -1955,9 +1955,7 @@ __metadata:
       optional: true
     "@swc/core-win32-x64-msvc":
       optional: true
-  bin:
-    swcx: run_swcx.js
-  checksum: a27b842be129b83c116f804e63deaa51dbd5d9b77d6260888d549f6408df1dd05aeef20046ceacc9fd7458e6afda6723545249bd77f77086b98bd9bf84738c19
+  checksum: de45a7dd871cc9497ad998d6a320d3c95cb9c74fdcb70590ff1f631e75001820d021bbfd5c463e9172afcb5ee47bffaa8fb893230e1329538c9f7afbd5ed45cf
   languageName: node
   linkType: hard
 
@@ -1973,15 +1971,15 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@syuilo/aiscript@npm:0.12.0":
-  version: 0.12.0
-  resolution: "@syuilo/aiscript@npm:0.12.0"
+"@syuilo/aiscript@npm:0.12.1":
+  version: 0.12.1
+  resolution: "@syuilo/aiscript@npm:0.12.1"
   dependencies:
     autobind-decorator: 2.4.0
     seedrandom: 3.0.5
     stringz: 2.1.0
-    uuid: 8.3.2
-  checksum: 82b52a6c602a8c3090b9457a0e9de99898b03cd8f054855b2f57439534257ef2780013a53eaeeef68c9893d96d3ec02fc6d0ede56396c2bcf054cf43b2297b67
+    uuid: 9.0.0
+  checksum: 0bee3031cbc8358e159fc8fde6e1ab7204e1a8e17e07f394f337d70cd3a8558e591145ca03afe37c76bbb91f84b2b2af70b892935e9c98507f9d1455c7be1107
   languageName: node
   linkType: hard
 
@@ -2447,10 +2445,10 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@types/katex@npm:0.14.0":
-  version: 0.14.0
-  resolution: "@types/katex@npm:0.14.0"
-  checksum: 330e0d0337ba48c87f5b793965fbad673653789bf6e50dfe8d726a7b0cbefd37195055e31503aae629814aa79447e4f23a4b87ad1ac565c0d9a9d9978836f39b
+"@types/katex@npm:0.16.0":
+  version: 0.16.0
+  resolution: "@types/katex@npm:0.16.0"
+  checksum: f93ceb2496621d18a28252264c0b7f5b0bdf125f9dc92d1adfbd9bf00942cd2918de336fae628d3929e615aaf84b7adb1781711c4e4605664be0827b1013ec14
   languageName: node
   linkType: hard
 
@@ -2886,13 +2884,13 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@typescript-eslint/eslint-plugin@npm:5.47.1":
-  version: 5.47.1
-  resolution: "@typescript-eslint/eslint-plugin@npm:5.47.1"
+"@typescript-eslint/eslint-plugin@npm:5.48.0":
+  version: 5.48.0
+  resolution: "@typescript-eslint/eslint-plugin@npm:5.48.0"
   dependencies:
-    "@typescript-eslint/scope-manager": 5.47.1
-    "@typescript-eslint/type-utils": 5.47.1
-    "@typescript-eslint/utils": 5.47.1
+    "@typescript-eslint/scope-manager": 5.48.0
+    "@typescript-eslint/type-utils": 5.48.0
+    "@typescript-eslint/utils": 5.48.0
     debug: ^4.3.4
     ignore: ^5.2.0
     natural-compare-lite: ^1.4.0
@@ -2905,24 +2903,24 @@ __metadata:
   peerDependenciesMeta:
     typescript:
       optional: true
-  checksum: 59fe719a8fbad14c37b8ce0dd292f6b8066bba370090f5e40eeab03033b97a12df1f1d0963c7070ac8cf4f7f319974fa6747e70932660055d222fa993c239b6a
+  checksum: cb9cd62fd56670414795e30d30c9fa11ec7ad3a8b0abda48dd17625053a1c26ba1767184b096149bdd0ccb457bec6392306f22211b75f802f4b27366398d16eb
   languageName: node
   linkType: hard
 
-"@typescript-eslint/parser@npm:5.47.1":
-  version: 5.47.1
-  resolution: "@typescript-eslint/parser@npm:5.47.1"
+"@typescript-eslint/parser@npm:5.48.0":
+  version: 5.48.0
+  resolution: "@typescript-eslint/parser@npm:5.48.0"
   dependencies:
-    "@typescript-eslint/scope-manager": 5.47.1
-    "@typescript-eslint/types": 5.47.1
-    "@typescript-eslint/typescript-estree": 5.47.1
+    "@typescript-eslint/scope-manager": 5.48.0
+    "@typescript-eslint/types": 5.48.0
+    "@typescript-eslint/typescript-estree": 5.48.0
     debug: ^4.3.4
   peerDependencies:
     eslint: ^6.0.0 || ^7.0.0 || ^8.0.0
   peerDependenciesMeta:
     typescript:
       optional: true
-  checksum: 36806686a2c5cc60558c09b13e885861aa21ec6250539d8d3d3c8abb90b321662e57dacec44915c87726a5a0d74187b58a65880a0613024eaeeb7ad0197a345d
+  checksum: 41d5ce5c8742d286fb083523295a4f186e57bbe4e3da63b6b2de1edbafbcbf6d5225ed3405da2c56e2b0fe1d52bb72babc37508d2ee9b86f6fadad3c4a7950d0
   languageName: node
   linkType: hard
 
@@ -2953,22 +2951,22 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@typescript-eslint/scope-manager@npm:5.47.1":
-  version: 5.47.1
-  resolution: "@typescript-eslint/scope-manager@npm:5.47.1"
+"@typescript-eslint/scope-manager@npm:5.48.0":
+  version: 5.48.0
+  resolution: "@typescript-eslint/scope-manager@npm:5.48.0"
   dependencies:
-    "@typescript-eslint/types": 5.47.1
-    "@typescript-eslint/visitor-keys": 5.47.1
-  checksum: 73e2e2949b6e0122d89cfd44e1d24eda38d774899b834746700a4f1eb096effd1432c953f8be743a3ea3c7fc8fbf6e0882b11ee0f39b7ced6d8abf6a8665f1c8
+    "@typescript-eslint/types": 5.48.0
+    "@typescript-eslint/visitor-keys": 5.48.0
+  checksum: 96c0ce33d613490690ae6f34e4152f05dbddf3196a6dec89afba4a63cd2d828ae23a98262920b521fe461e7655d38f3a01e9e43588c12392a27bf8cb4f8ae201
   languageName: node
   linkType: hard
 
-"@typescript-eslint/type-utils@npm:5.47.1":
-  version: 5.47.1
-  resolution: "@typescript-eslint/type-utils@npm:5.47.1"
+"@typescript-eslint/type-utils@npm:5.48.0":
+  version: 5.48.0
+  resolution: "@typescript-eslint/type-utils@npm:5.48.0"
   dependencies:
-    "@typescript-eslint/typescript-estree": 5.47.1
-    "@typescript-eslint/utils": 5.47.1
+    "@typescript-eslint/typescript-estree": 5.48.0
+    "@typescript-eslint/utils": 5.48.0
     debug: ^4.3.4
     tsutils: ^3.21.0
   peerDependencies:
@@ -2976,7 +2974,7 @@ __metadata:
   peerDependenciesMeta:
     typescript:
       optional: true
-  checksum: 84a1e6c8fd47d419dc66430e31b818774d4c0329a5f355a5a9e8af94378be4c0c24a89916d5cc1380fdbb640693527b906c2e6adee486a2e6786cb5e08bd9eb3
+  checksum: 0d57e3bbcaa46e29b588b86b2271341b264f063e71ff5b6d4d35f50f2fe11bd6cdc3c4c95d78493fd17673ecdbd712992b84da1600947ed3bf6ae09de7b99464
   languageName: node
   linkType: hard
 
@@ -2987,10 +2985,10 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@typescript-eslint/types@npm:5.47.1":
-  version: 5.47.1
-  resolution: "@typescript-eslint/types@npm:5.47.1"
-  checksum: 9b3df8661862a8927ec29d21d6b5826cae7dd8b4797b5b54d66289d8abcf46081453a5cbaf9cc0a5b6c8249ca381dda61c2623da2a704e47f9d86175639a8cea
+"@typescript-eslint/types@npm:5.48.0":
+  version: 5.48.0
+  resolution: "@typescript-eslint/types@npm:5.48.0"
+  checksum: fa27bd9ec7ec5f256b79a371bb05cfbc26902b6a395f38b0cff0e281633ebd76775ad18e41be1bb156868859287295f6833a2a671da57c6347ac7c6bc08a553b
   languageName: node
   linkType: hard
 
@@ -3012,12 +3010,12 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@typescript-eslint/typescript-estree@npm:5.47.1":
-  version: 5.47.1
-  resolution: "@typescript-eslint/typescript-estree@npm:5.47.1"
+"@typescript-eslint/typescript-estree@npm:5.48.0":
+  version: 5.48.0
+  resolution: "@typescript-eslint/typescript-estree@npm:5.48.0"
   dependencies:
-    "@typescript-eslint/types": 5.47.1
-    "@typescript-eslint/visitor-keys": 5.47.1
+    "@typescript-eslint/types": 5.48.0
+    "@typescript-eslint/visitor-keys": 5.48.0
     debug: ^4.3.4
     globby: ^11.1.0
     is-glob: ^4.0.3
@@ -3026,25 +3024,25 @@ __metadata:
   peerDependenciesMeta:
     typescript:
       optional: true
-  checksum: 803214a53fd4faf19b6b325dd4e8ddaa5bb1ebb9b52358d26ebeaeb86b431cea5bc09f3b43ca8abfdd3a72fdea667467a1abfda50cbad866696ec5739afae2ac
+  checksum: 2444632243111e51bc83b56140514cb5978bef4d7151fede0dfcff8808afc1ad335b0c60ca86c2811bcc82273b87e59e2e0360bf1b8c014825ff818a1731d127
   languageName: node
   linkType: hard
 
-"@typescript-eslint/utils@npm:5.47.1":
-  version: 5.47.1
-  resolution: "@typescript-eslint/utils@npm:5.47.1"
+"@typescript-eslint/utils@npm:5.48.0":
+  version: 5.48.0
+  resolution: "@typescript-eslint/utils@npm:5.48.0"
   dependencies:
     "@types/json-schema": ^7.0.9
     "@types/semver": ^7.3.12
-    "@typescript-eslint/scope-manager": 5.47.1
-    "@typescript-eslint/types": 5.47.1
-    "@typescript-eslint/typescript-estree": 5.47.1
+    "@typescript-eslint/scope-manager": 5.48.0
+    "@typescript-eslint/types": 5.48.0
+    "@typescript-eslint/typescript-estree": 5.48.0
     eslint-scope: ^5.1.1
     eslint-utils: ^3.0.0
     semver: ^7.3.7
   peerDependencies:
     eslint: ^6.0.0 || ^7.0.0 || ^8.0.0
-  checksum: 5167d26b8d5579de4d9aae36e18f980b33e01006ecc87cff59b761e15f69234092ac555bcf64a9f18d7c3e68a971df2a37b3912fc523c2586c2ba3f4544cc3d3
+  checksum: 53f512ae61f72c2b29f2daf8adbc1f37c400cc71156557f69f0745b62c1265d99917a168245e2ee3d88ae458144818d1bf41ced4a764d7d9534b466b29d362fd
   languageName: node
   linkType: hard
 
@@ -3058,13 +3056,13 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@typescript-eslint/visitor-keys@npm:5.47.1":
-  version: 5.47.1
-  resolution: "@typescript-eslint/visitor-keys@npm:5.47.1"
+"@typescript-eslint/visitor-keys@npm:5.48.0":
+  version: 5.48.0
+  resolution: "@typescript-eslint/visitor-keys@npm:5.48.0"
   dependencies:
-    "@typescript-eslint/types": 5.47.1
+    "@typescript-eslint/types": 5.48.0
     eslint-visitor-keys: ^3.3.0
-  checksum: b4d1f4daa67e962d22c41325d9dcb6b2efde1caf354a2edb5bf682b92ab8c6205435d0b12f39ce9771955250e26f2a6f03adabb37e62e5aac8225691a59ef153
+  checksum: 8d41fb7c93b79df415b43c31da7c9007074d78ab6f16c2d318c23e7974b578ce510f466a9584bd67c526367666974091cb5cfbf6670d29e36fb4ab2e57137515
   languageName: node
   linkType: hard
 
@@ -3085,57 +3083,57 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@volar/language-core@npm:1.0.19":
-  version: 1.0.19
-  resolution: "@volar/language-core@npm:1.0.19"
+"@volar/language-core@npm:1.0.22":
+  version: 1.0.22
+  resolution: "@volar/language-core@npm:1.0.22"
   dependencies:
-    "@volar/source-map": 1.0.19
+    "@volar/source-map": 1.0.22
     muggle-string: ^0.1.0
-  checksum: f49f235e07a2393337c679afaa232d9fd70bf6b6eb0752d574cb99f75c5996af70d3344fe324f3553117b7650c8b3e31331c81fabf8fde6289f2a5f97459e686
+  checksum: 809b697a3349ed2ee735c684ee796492b6dab6c7bfe0a82fce2b4c3139b7bb32e7ad9339a1aaf1ce3aa15ccc0826eae27ac77d02dfe73d61a6ce3d20d45fbf92
   languageName: node
   linkType: hard
 
-"@volar/source-map@npm:1.0.19":
-  version: 1.0.19
-  resolution: "@volar/source-map@npm:1.0.19"
+"@volar/source-map@npm:1.0.22":
+  version: 1.0.22
+  resolution: "@volar/source-map@npm:1.0.22"
   dependencies:
     muggle-string: ^0.1.0
-  checksum: a80596333f7ebba33caa2c007ab1b442e5c3ce1d9990364aa10da8e46ebdc736d939b562b5095f268151a02fc45cd09671d8ca8f377b615f3d9d8b876cbcd407
+  checksum: 40b33138fd6fe7189a5782c69283d4320e9ab7aa68e9546ce245e4b73035c6d284bb4aa0736080e7c5c7e1532966585cb674fa4c8adba941a6c68f0cbb3905f3
   languageName: node
   linkType: hard
 
-"@volar/typescript@npm:1.0.19":
-  version: 1.0.19
-  resolution: "@volar/typescript@npm:1.0.19"
+"@volar/typescript@npm:1.0.22":
+  version: 1.0.22
+  resolution: "@volar/typescript@npm:1.0.22"
   dependencies:
-    "@volar/language-core": 1.0.19
-  checksum: 1dcce9a6386a205c8708308e664ebb76b846a076a0726a833f6e9416af323e666b6061b28a5a0351e56021be83374af07cb93cb5990e26b6e31a4808a4821847
+    "@volar/language-core": 1.0.22
+  checksum: d88cb63b32e22ca924853964f2989db894ef4325ee66591be0a5c5e9f2546a8d964b63ee9481d6d6f2e1d2b6d4492a09e9d1e400ef7637583b2f49601087f3e5
   languageName: node
   linkType: hard
 
-"@volar/vue-language-core@npm:1.0.19":
-  version: 1.0.19
-  resolution: "@volar/vue-language-core@npm:1.0.19"
+"@volar/vue-language-core@npm:1.0.22":
+  version: 1.0.22
+  resolution: "@volar/vue-language-core@npm:1.0.22"
   dependencies:
-    "@volar/language-core": 1.0.19
-    "@volar/source-map": 1.0.19
+    "@volar/language-core": 1.0.22
+    "@volar/source-map": 1.0.22
     "@vue/compiler-dom": ^3.2.45
     "@vue/compiler-sfc": ^3.2.45
     "@vue/reactivity": ^3.2.45
     "@vue/shared": ^3.2.45
     minimatch: ^5.1.1
     vue-template-compiler: ^2.7.14
-  checksum: a3efd990c2cfe661b21c3d0b2536dae504a98358d29cc8ae77a93c37468feafc47b326f20e9cb6ac936fb8dc0d9fc602a1ab25d1f8783791090d76fa3e073828
+  checksum: e0eeb347af5f99a5c93bd4cd18b3e5f984ecaec04a21e59f319f466c344feecb79bcc64b3389b8c5e338ac6b57aff2dc02dfaba67505910f09775930f815f37e
   languageName: node
   linkType: hard
 
-"@volar/vue-typescript@npm:1.0.19":
-  version: 1.0.19
-  resolution: "@volar/vue-typescript@npm:1.0.19"
+"@volar/vue-typescript@npm:1.0.22":
+  version: 1.0.22
+  resolution: "@volar/vue-typescript@npm:1.0.22"
   dependencies:
-    "@volar/typescript": 1.0.19
-    "@volar/vue-language-core": 1.0.19
-  checksum: 01ab7eab3e4c237beca10280c1c79b93579791db37f14874c78c9129f6a8e34f8ed794828129e0114d087f59826e34ae0066c1efc4548e1094eb5b2bdbb5c09f
+    "@volar/typescript": 1.0.22
+    "@volar/vue-language-core": 1.0.22
+  checksum: fc63ba3d579305e0aa8642b76494b12b56c10f63dfd3a225e4d1d094769f0e7de62494a3a9c2443724ccfd8de49df651a866f305f9953de6ccb9f96f2cf5e51c
   languageName: node
   linkType: hard
 
@@ -3411,15 +3409,15 @@ __metadata:
   languageName: node
   linkType: hard
 
-"ajv@npm:8.11.2, ajv@npm:^8.0.0, ajv@npm:^8.10.0, ajv@npm:^8.11.0":
-  version: 8.11.2
-  resolution: "ajv@npm:8.11.2"
+"ajv@npm:8.12.0":
+  version: 8.12.0
+  resolution: "ajv@npm:8.12.0"
   dependencies:
     fast-deep-equal: ^3.1.1
     json-schema-traverse: ^1.0.0
     require-from-string: ^2.0.2
     uri-js: ^4.2.2
-  checksum: 53435bf79ee7d1eabba8085962dba4c08d08593334b304db7772887f0b7beebc1b3d957432f7437ed4b60e53b5d966a57b439869890209c50fed610459999e3e
+  checksum: 4dc13714e316e67537c8b31bc063f99a1d9d9a497eb4bbd55191ac0dcd5e4985bbb71570352ad6f1e76684fb6d790928f96ba3b2d4fd6e10024be9612fe3f001
   languageName: node
   linkType: hard
 
@@ -3435,6 +3433,18 @@ __metadata:
   languageName: node
   linkType: hard
 
+"ajv@npm:^8.0.0, ajv@npm:^8.10.0, ajv@npm:^8.11.0":
+  version: 8.11.2
+  resolution: "ajv@npm:8.11.2"
+  dependencies:
+    fast-deep-equal: ^3.1.1
+    json-schema-traverse: ^1.0.0
+    require-from-string: ^2.0.2
+    uri-js: ^4.2.2
+  checksum: 53435bf79ee7d1eabba8085962dba4c08d08593334b304db7772887f0b7beebc1b3d957432f7437ed4b60e53b5d966a57b439869890209c50fed610459999e3e
+  languageName: node
+  linkType: hard
+
 "alphanum-sort@npm:^1.0.1, alphanum-sort@npm:^1.0.2":
   version: 1.0.2
   resolution: "alphanum-sort@npm:1.0.2"
@@ -3960,9 +3970,9 @@ __metadata:
   languageName: node
   linkType: hard
 
-"aws-sdk@npm:2.1286.0":
-  version: 2.1286.0
-  resolution: "aws-sdk@npm:2.1286.0"
+"aws-sdk@npm:2.1289.0":
+  version: 2.1289.0
+  resolution: "aws-sdk@npm:2.1289.0"
   dependencies:
     buffer: 4.9.2
     events: 1.1.1
@@ -3974,7 +3984,7 @@ __metadata:
     util: ^0.12.4
     uuid: 8.0.0
     xml2js: 0.4.19
-  checksum: 589494899ee008c2acb5dde44c036501dcfe51bfc537eddb4dcbf6c8a937fb5bd29fb4f2433d75ef080bef79c4ead486d230ba558ab89f54e2cc92b88a500de7
+  checksum: e4985b3bf4d06b56711012ed2a05949e51ebc82e3316039268ef54ff774e7b8ba8fa6780a6ca0be222b11b5329b92ca08f27bdc6d410177d9675a45e53a62eb3
   languageName: node
   linkType: hard
 
@@ -4116,9 +4126,9 @@ __metadata:
   version: 0.0.0-use.local
   resolution: "backend@workspace:packages/backend"
   dependencies:
-    "@bull-board/api": ^4.10.0
-    "@bull-board/fastify": ^4.10.0
-    "@bull-board/ui": ^4.10.0
+    "@bull-board/api": ^4.10.1
+    "@bull-board/fastify": ^4.10.1
+    "@bull-board/ui": ^4.10.1
     "@discordapp/twemoji": 14.0.2
     "@fastify/accepts": 4.1.0
     "@fastify/cookie": ^8.3.0
@@ -4133,7 +4143,7 @@ __metadata:
     "@peertube/http-signature": 1.7.0
     "@redocly/openapi-core": 1.0.0-beta.117
     "@sinonjs/fake-timers": 10.0.2
-    "@swc/core": 1.3.24
+    "@swc/core": 1.3.25
     "@swc/jest": 0.2.24
     "@tensorflow/tfjs": ^4.1.0
     "@tensorflow/tfjs-node": 4.1.0
@@ -4179,13 +4189,13 @@ __metadata:
     "@types/web-push": 3.3.2
     "@types/websocket": 1.0.5
     "@types/ws": 8.5.4
-    "@typescript-eslint/eslint-plugin": 5.47.1
-    "@typescript-eslint/parser": 5.47.1
+    "@typescript-eslint/eslint-plugin": 5.48.0
+    "@typescript-eslint/parser": 5.48.0
     accepts: ^1.3.8
-    ajv: 8.11.2
+    ajv: 8.12.0
     archiver: 5.3.1
     autwh: 0.1.0
-    aws-sdk: 2.1286.0
+    aws-sdk: 2.1289.0
     bcryptjs: 2.4.3
     blurhash: 2.0.4
     bull: 4.10.2
@@ -4260,8 +4270,8 @@ __metadata:
     stringz: 2.1.0
     summaly: 2.7.0
     syslog-pro: "git+https://github.com/misskey-dev/SyslogPro#0.2.9-misskey.2"
-    systeminformation: 5.16.9
-    tinycolor2: 1.5.1
+    systeminformation: 5.17.1
+    tinycolor2: 1.5.2
     tmp: 0.2.1
     tsc-alias: 1.8.2
     tsconfig-paths: 4.1.2
@@ -4834,6 +4844,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"canvas-confetti@npm:^1.6.0":
+  version: 1.6.0
+  resolution: "canvas-confetti@npm:1.6.0"
+  checksum: be19e3be736ab28aa8af7b53cba9f4146f5714edadbf4d5a7d7b1899914dc59a8ac5574260fe6b7d9995c51df5787b0b707adfbb72dbacbc61fc03f9f2b25291
+  languageName: node
+  linkType: hard
+
 "caseless@npm:~0.12.0":
   version: 0.12.0
   resolution: "caseless@npm:0.12.0"
@@ -4925,12 +4942,12 @@ __metadata:
   languageName: node
   linkType: hard
 
-"chart.js@npm:4.1.1":
-  version: 4.1.1
-  resolution: "chart.js@npm:4.1.1"
+"chart.js@npm:4.1.2":
+  version: 4.1.2
+  resolution: "chart.js@npm:4.1.2"
   dependencies:
     "@kurkle/color": ^0.3.0
-  checksum: e195abc7b1271a0d4f616b90ac56465b639bffef8e965d26bf1fd65050011bf359fdc25e43dac1576420c1173dc1430c576fd0f953ae8b62bf257ec571424882
+  checksum: 938ac88baf04eb8d784a8d89064696375bc12545421186f5ec9b69ba9b6f7b7da960754f351b59b1df2b1c314dfa848aab44267076888e664d5253bc6a4059dd
   languageName: node
   linkType: hard
 
@@ -5782,9 +5799,9 @@ __metadata:
   languageName: node
   linkType: hard
 
-"cypress@npm:12.2.0":
-  version: 12.2.0
-  resolution: "cypress@npm:12.2.0"
+"cypress@npm:12.3.0":
+  version: 12.3.0
+  resolution: "cypress@npm:12.3.0"
   dependencies:
     "@cypress/request": ^2.88.10
     "@cypress/xvfb": ^1.2.4
@@ -5830,7 +5847,7 @@ __metadata:
     yauzl: ^2.10.0
   bin:
     cypress: bin/cypress
-  checksum: fae721114f2001705cdd59a031d5d41d93092e4bc66561021e7890cd01004ac8e037dbd22e22fe0fac325e8e4420a52e1d75254f4e8b7f7a63e84d3fdb86626c
+  checksum: 00658996bcca918254348eb42bc03079ccf2d583e5c9c04190267edcbc542d4a22835d7399711c99f7aa7334412104b23cc5a1aa7f02b8b541c12298bf3f63f0
   languageName: node
   linkType: hard
 
@@ -8027,13 +8044,13 @@ __metadata:
     "@rollup/plugin-alias": 4.0.2
     "@rollup/plugin-json": 6.0.0
     "@rollup/pluginutils": 5.0.2
-    "@syuilo/aiscript": 0.12.0
+    "@syuilo/aiscript": 0.12.1
     "@tabler/icons": ^1.118.0
     "@types/escape-regexp": 0.0.1
     "@types/glob": 8.0.0
     "@types/gulp": 4.0.10
     "@types/gulp-rename": 2.0.1
-    "@types/katex": 0.14.0
+    "@types/katex": 0.16.0
     "@types/matter-js": 0.18.2
     "@types/punycode": 2.1.0
     "@types/sanitize-html": ^2.8.0
@@ -8043,8 +8060,8 @@ __metadata:
     "@types/uuid": 9.0.0
     "@types/websocket": 1.0.5
     "@types/ws": 8.5.4
-    "@typescript-eslint/eslint-plugin": 5.47.1
-    "@typescript-eslint/parser": 5.47.1
+    "@typescript-eslint/eslint-plugin": 5.48.0
+    "@typescript-eslint/parser": 5.48.0
     "@vitejs/plugin-vue": 4.0.0
     "@vue/compiler-sfc": 3.2.45
     "@vue/runtime-core": 3.2.45
@@ -8053,7 +8070,8 @@ __metadata:
     blurhash: 2.0.4
     broadcast-channel: 4.19.1
     browser-image-resizer: "git+https://github.com/misskey-dev/browser-image-resizer#v2.2.1-misskey.3"
-    chart.js: 4.1.1
+    canvas-confetti: ^1.6.0
+    chart.js: 4.1.2
     chartjs-adapter-date-fns: 3.0.0
     chartjs-chart-matrix: ^1.3.0
     chartjs-plugin-gradient: 0.6.1
@@ -8061,7 +8079,7 @@ __metadata:
     compare-versions: 5.0.1
     cropperjs: 2.0.0-beta.2
     cross-env: 7.0.3
-    cypress: 12.2.0
+    cypress: 12.3.0
     date-fns: 2.29.3
     escape-regexp: 0.0.1
     eslint: 8.31.0
@@ -8073,7 +8091,7 @@ __metadata:
     insert-text-at-cursor: 0.3.0
     is-file-animated: 1.0.2
     json5: 2.2.3
-    katex: 0.15.6
+    katex: 0.16.4
     matter-js: 0.18.0
     mfm-js: 0.23.0
     misskey-js: 0.0.14
@@ -8082,7 +8100,7 @@ __metadata:
     punycode: 2.1.1
     querystring: 0.2.1
     rndstr: 1.0.0
-    rollup: 3.9.0
+    rollup: 3.9.1
     s-age: 1.1.2
     sanitize-html: ^2.8.1
     sass: 1.57.1
@@ -8094,18 +8112,18 @@ __metadata:
     textarea-caret: 3.1.0
     three: 0.148.0
     throttle-debounce: 5.0.0
-    tinycolor2: 1.5.1
+    tinycolor2: 1.5.2
     tsc-alias: 1.8.2
     tsconfig-paths: 4.1.2
     twemoji-parser: 14.0.0
     typescript: 4.9.4
     uuid: 9.0.0
     vanilla-tilt: 1.8.0
-    vite: 4.0.3
+    vite: 4.0.4
     vue: 3.2.45
     vue-eslint-parser: ^9.1.0
     vue-prism-editor: 2.0.0-alpha.2
-    vue-tsc: ^1.0.19
+    vue-tsc: ^1.0.22
     vuedraggable: next
   languageName: unknown
   linkType: soft
@@ -10904,14 +10922,14 @@ __metadata:
   languageName: node
   linkType: hard
 
-"katex@npm:0.15.6":
-  version: 0.15.6
-  resolution: "katex@npm:0.15.6"
+"katex@npm:0.16.4":
+  version: 0.16.4
+  resolution: "katex@npm:0.16.4"
   dependencies:
     commander: ^8.0.0
   bin:
     katex: cli.js
-  checksum: 2da808bbd1d3be27715006cd86767dd3fcce3e317fb3bbd64d407328d2d90de17b5d83062b2cfd0e0d0de32e340efbac214862bc96892a5d1492462e553728d4
+  checksum: 94eaf1fbd8365792308527695c09baa6d2d84e2d0170e4af44fb12be3ed403fb3430caff2410117f2f1a9dfdb329f61ab9611d97e645d9c89ee60940698a45cc
   languageName: node
   linkType: hard
 
@@ -11822,10 +11840,10 @@ __metadata:
   dependencies:
     "@types/gulp": 4.0.10
     "@types/gulp-rename": 2.0.1
-    "@typescript-eslint/eslint-plugin": 5.47.1
-    "@typescript-eslint/parser": 5.47.1
+    "@typescript-eslint/eslint-plugin": 5.48.0
+    "@typescript-eslint/parser": 5.48.0
     cross-env: 7.0.3
-    cypress: 12.2.0
+    cypress: 12.3.0
     eslint: ^8.31.0
     execa: 5.1.1
     gulp: 4.0.2
@@ -14790,9 +14808,9 @@ __metadata:
   languageName: node
   linkType: hard
 
-"rollup@npm:3.9.0":
-  version: 3.9.0
-  resolution: "rollup@npm:3.9.0"
+"rollup@npm:3.9.1":
+  version: 3.9.1
+  resolution: "rollup@npm:3.9.1"
   dependencies:
     fsevents: ~2.3.2
   dependenciesMeta:
@@ -14800,7 +14818,7 @@ __metadata:
       optional: true
   bin:
     rollup: dist/bin/rollup
-  checksum: b0ce4baa8db8ee77ab096a4e066b20fb0719efb9cbd84f230838517d35bf159311487112852cfa687126896b58084c8e6cb9ab222f7559c4b6138ca693d63439
+  checksum: 929cfab6b8bb2e20c28d7a4c3909b53729f4a63d8cc14f3b1a217d5f8e550737ee0903124ba58a1f2e7efd45c596e044a968aa379411731d0e76c910621d7d3f
   languageName: node
   linkType: hard
 
@@ -15953,12 +15971,12 @@ __metadata:
   languageName: node
   linkType: hard
 
-"systeminformation@npm:5.16.9":
-  version: 5.16.9
-  resolution: "systeminformation@npm:5.16.9"
+"systeminformation@npm:5.17.1":
+  version: 5.17.1
+  resolution: "systeminformation@npm:5.17.1"
   bin:
     systeminformation: lib/cli.js
-  checksum: e590134391ba727b4988ae828483344ae90ee03551ffb578defd3d6d489a8faa519aee563df18fea5f98c2b9e74ca55d29f1ad096d3a70933672338b9a78f03c
+  checksum: f13052ef52be86431a045d545f2b641a80e1becd46a0610dacd3f5773ebca0866b0087bf7706c1539e61945007354c4da344479f09a69b6dffe7a4f42d712a90
   conditions: (os=darwin | os=linux | os=win32 | os=freebsd | os=openbsd | os=netbsd | os=sunos | os=android)
   languageName: node
   linkType: hard
@@ -16182,10 +16200,10 @@ __metadata:
   languageName: node
   linkType: hard
 
-"tinycolor2@npm:1.5.1":
-  version: 1.5.1
-  resolution: "tinycolor2@npm:1.5.1"
-  checksum: 0fffbe217217f819e0ef79524fda8813c20dac89b647bfc0f1bc0c7d5a80884dad0d74a414b865d2ce867d76d2303f251f7527eaebfe2838251e195fc4b0287c
+"tinycolor2@npm:1.5.2":
+  version: 1.5.2
+  resolution: "tinycolor2@npm:1.5.2"
+  checksum: 9df1ea9a986b03f1aebb1c1ac17fc561e358493f61b56d73ef2d7207fe7bd74eb71cf745b70487b2b5bb1ce33c9e8af7101088bb0b5fc532eaa1f9d1eda4ef31
   languageName: node
   linkType: hard
 
@@ -16913,15 +16931,6 @@ __metadata:
   languageName: node
   linkType: hard
 
-"uuid@npm:8.3.2, uuid@npm:^8.3.0, uuid@npm:^8.3.2":
-  version: 8.3.2
-  resolution: "uuid@npm:8.3.2"
-  bin:
-    uuid: dist/bin/uuid
-  checksum: 5575a8a75c13120e2f10e6ddc801b2c7ed7d8f3c8ac22c7ed0c7b2ba6383ec0abda88c905085d630e251719e0777045ae3236f04c812184b7c765f63a70e58df
-  languageName: node
-  linkType: hard
-
 "uuid@npm:9.0.0":
   version: 9.0.0
   resolution: "uuid@npm:9.0.0"
@@ -16940,6 +16949,15 @@ __metadata:
   languageName: node
   linkType: hard
 
+"uuid@npm:^8.3.0, uuid@npm:^8.3.2":
+  version: 8.3.2
+  resolution: "uuid@npm:8.3.2"
+  bin:
+    uuid: dist/bin/uuid
+  checksum: 5575a8a75c13120e2f10e6ddc801b2c7ed7d8f3c8ac22c7ed0c7b2ba6383ec0abda88c905085d630e251719e0777045ae3236f04c812184b7c765f63a70e58df
+  languageName: node
+  linkType: hard
+
 "v8-to-istanbul@npm:^9.0.1":
   version: 9.0.1
   resolution: "v8-to-istanbul@npm:9.0.1"
@@ -17072,9 +17090,9 @@ __metadata:
   languageName: node
   linkType: hard
 
-"vite@npm:4.0.3":
-  version: 4.0.3
-  resolution: "vite@npm:4.0.3"
+"vite@npm:4.0.4":
+  version: 4.0.4
+  resolution: "vite@npm:4.0.4"
   dependencies:
     esbuild: ^0.16.3
     fsevents: ~2.3.2
@@ -17106,7 +17124,7 @@ __metadata:
       optional: true
   bin:
     vite: bin/vite.js
-  checksum: 7df71d955f78cbe0dd8e1eb0851fc75070346a0426b8e3e913bf2e05d1053ca8a50619d550fab4f1ed52c68dfcc2921e6421504e9669fc5ed77497a77f84e33e
+  checksum: eb86c8cdfe8dcb6644005486b31cb60bc596f2aa683cb194abb5c0afca7c2a5dfdb02bbc7f83f419ad170227ac9c3b898f4406a6d1433105fb61d79d78e47d52
   languageName: node
   linkType: hard
 
@@ -17153,17 +17171,17 @@ __metadata:
   languageName: node
   linkType: hard
 
-"vue-tsc@npm:^1.0.19":
-  version: 1.0.19
-  resolution: "vue-tsc@npm:1.0.19"
+"vue-tsc@npm:^1.0.22":
+  version: 1.0.22
+  resolution: "vue-tsc@npm:1.0.22"
   dependencies:
-    "@volar/vue-language-core": 1.0.19
-    "@volar/vue-typescript": 1.0.19
+    "@volar/vue-language-core": 1.0.22
+    "@volar/vue-typescript": 1.0.22
   peerDependencies:
     typescript: "*"
   bin:
     vue-tsc: bin/vue-tsc.js
-  checksum: 7fe6287d7a12b906f8b7a111cc5b88e3df09f919bbfc04dff1d5c46edcdd925b7650e51fc2de819fbc9ae7bc081807ca27d8f86bd3141886ee9f3fff6cdf417a
+  checksum: 630a372fef2cd6ce830c540fe81b29c657f1b8caaffb181a84af5c10c9daec9c977860ab30cdfb7c80166c5211d3cbfa912f9212f87a763b20d3e91887b1318c
   languageName: node
   linkType: hard