diff --git a/database/2025_02_08_175705_create_profile_background_settings_table.php b/database/2025_02_08_175705_create_profile_background_settings_table.php
new file mode 100644
index 00000000..6e071de4
--- /dev/null
+++ b/database/2025_02_08_175705_create_profile_background_settings_table.php
@@ -0,0 +1,42 @@
+<?php
+use Index\Db\DbConnection;
+use Index\Db\Migration\DbMigration;
+
+final class CreateProfileBackgroundSettingsTable_20250208_175705 implements DbMigration {
+    public function migrate(DbConnection $conn): void {
+        $conn->execute(<<<SQL
+            CREATE TABLE msz_profile_backgrounds (
+                user_id   INT UNSIGNED     NOT NULL AUTO_INCREMENT,
+                bg_attach ENUM('cover','stretch','tile','contain') NOT NULL,
+                bg_blend  TINYINT UNSIGNED NOT NULL,
+                bg_slide  TINYINT UNSIGNED NOT NULL,
+                PRIMARY KEY (user_id),
+                CONSTRAINT profile_backgrounds_users_foreign
+                    FOREIGN KEY (user_id)
+                    REFERENCES msz_users (user_id)
+                    ON UPDATE CASCADE
+                    ON DELETE CASCADE
+            ) COLLATE='utf8mb4_bin';
+        SQL);
+
+        $conn->execute(<<<SQL
+            INSERT INTO msz_profile_backgrounds
+            SELECT user_id,
+                CASE (user_background_settings & 0x0F)
+                    WHEN 1 THEN 'cover'
+                    WHEN 2 THEN 'stretch'
+                    WHEN 3 THEN 'tile'
+                    WHEN 4 THEN 'contain'
+                END,
+                IF(user_background_settings & 0x10, 1, 0),
+                IF(user_background_settings & 0x20, 1, 0)
+            FROM msz_users
+            WHERE (user_background_settings & 0x0F) BETWEEN 1 AND 4;
+        SQL);
+
+        $conn->execute(<<<SQL
+            ALTER TABLE msz_users
+                DROP COLUMN user_background_settings;
+        SQL);
+    }
+}
diff --git a/public-legacy/profile.php b/public-legacy/profile.php
index 13a6695d..d81427be 100644
--- a/public-legacy/profile.php
+++ b/public-legacy/profile.php
@@ -6,6 +6,7 @@ use InvalidArgumentException;
 use RuntimeException;
 use Index\ByteFormat;
 use Misuzu\Parsers\TextFormat;
+use Misuzu\Profile\ProfileBackgroundAttach;
 use Misuzu\Users\{User,UsersContext};
 use Misuzu\Users\Assets\UserAvatarAsset;
 use Misuzu\Users\Assets\UserBackgroundAsset;
@@ -74,8 +75,9 @@ $canManageWarnings = $viewerPermsUser->check(Perm::U_WARNINGS_MANAGE);
 $canEdit = !$viewingAsGuest && ((!$isBanned && $viewingOwnProfile) || $viewerInfo->super || (
     $viewerPermsUser->check(Perm::U_USERS_MANAGE) && ($viewingOwnProfile || $viewerRank > $userRank)
 ));
-$avatarInfo = new UserAvatarAsset($userInfo);
-$backgroundInfo = new UserBackgroundAsset($userInfo);
+$avatarAsset = new UserAvatarAsset($userInfo);
+$backgroundInfo = $msz->profileCtx->backgrounds->getProfileBackground($userInfo);
+$backgroundAsset = new UserBackgroundAsset($userInfo, $backgroundInfo);
 
 if($isEditing) {
     if(!$canEdit)
@@ -92,7 +94,6 @@ if($isEditing) {
 
     Template::set([
         'perms' => $perms,
-        'background_attachments' => UserBackgroundAsset::getAttachmentStringOptions(),
         'birthdate_info' => $msz->usersCtx->birthdates->getUserBirthdate($userInfo),
     ]);
 
@@ -106,7 +107,7 @@ if($isEditing) {
                 if(!$perms->edit_profile) {
                     $notices[] = 'You\'re not allowed to edit your profile';
                 } else {
-                    $profileFieldInfos = iterator_to_array($msz->profileFields->getFields());
+                    $profileFieldInfos = iterator_to_array($msz->profileCtx->fields->getFields());
                     $profileFieldsSetInfos = [];
                     $profileFieldsSetValues = [];
                     $profileFieldsRemove = [];
@@ -130,9 +131,9 @@ if($isEditing) {
                     }
 
                     if(!empty($profileFieldsRemove))
-                        $msz->profileFields->removeFieldValues($userInfo, $profileFieldsRemove);
+                        $msz->profileCtx->fields->removeFieldValues($userInfo, $profileFieldsRemove);
                     if(!empty($profileFieldsSetInfos))
-                        $msz->profileFields->setFieldValues($userInfo, $profileFieldsSetInfos, $profileFieldsSetValues);
+                        $msz->profileCtx->fields->setFieldValues($userInfo, $profileFieldsSetInfos, $profileFieldsSetValues);
                 }
             }
 
@@ -187,7 +188,7 @@ if($isEditing) {
 
             if(!empty($_FILES['avatar'])) {
                 if(!empty($_POST['avatar']['delete'])) {
-                    $avatarInfo->delete();
+                    $avatarAsset->delete();
                 } else {
                     if(!$perms->edit_avatar) {
                         $notices[] = 'You aren\'t allow to change your avatar.';
@@ -204,7 +205,7 @@ if($isEditing) {
                                     break;
                                 case UPLOAD_ERR_INI_SIZE:
                                 case UPLOAD_ERR_FORM_SIZE:
-                                    $notices[] = sprintf('Your avatar is not allowed to be larger in file size than %s!', ByteFormat::format($avatarInfo->getMaxBytes()));
+                                    $notices[] = sprintf('Your avatar is not allowed to be larger in file size than %s!', ByteFormat::format($avatarAsset->getMaxBytes()));
                                     break;
                                 default:
                                     $notices[] = 'Unable to save your avatar, contact an administator!';
@@ -212,14 +213,14 @@ if($isEditing) {
                             }
                         } else {
                             try {
-                                $avatarInfo->setFromPath($_FILES['avatar']['tmp_name']['file']);
+                                $avatarAsset->setFromPath($_FILES['avatar']['tmp_name']['file']);
                             } catch(InvalidArgumentException $ex) {
                                 $exMessage = $ex->getMessage();
                                 $notices[] = match($exMessage) {
                                     '$path is not a valid image.' => 'The file you uploaded was not an image!',
                                     '$path is not an allowed image file.' => 'This type of image is not supported, keep to PNG, JPG or GIF!',
-                                    'Dimensions of $path are too large.' => sprintf('Your avatar can\'t be larger than %dx%d!', $avatarInfo->getMaxWidth(), $avatarInfo->getMaxHeight()),
-                                    'File size of $path is too large.' => sprintf('Your avatar is not allowed to be larger in file size than %s!', ByteFormat::format($avatarInfo->getMaxBytes())),
+                                    'Dimensions of $path are too large.' => sprintf('Your avatar can\'t be larger than %dx%d!', $avatarAsset->getMaxWidth(), $avatarAsset->getMaxHeight()),
+                                    'File size of $path is too large.' => sprintf('Your avatar is not allowed to be larger in file size than %s!', ByteFormat::format($avatarAsset->getMaxBytes())),
                                     default => $exMessage,
                                 };
                             } catch(RuntimeException $ex) {
@@ -230,16 +231,20 @@ if($isEditing) {
                 }
             }
 
-            if(!empty($_FILES['background'])) {
-                if((int)($_POST['background']['attach'] ?? -1) === 0) {
-                    $backgroundInfo->delete();
+            if(filter_has_var(INPUT_POST, 'bg_attach')) {
+                $bgFormat = ProfileBackgroundAttach::tryFrom((string)filter_input(INPUT_POST, 'bg_attach'));
+
+                if($bgFormat === null) {
+                    $backgroundAsset->delete();
+                    $msz->profileCtx->backgrounds->deleteProfileBackground($userInfo);
+                    $backgroundAsset = null;
                 } else {
                     if(!$perms->edit_background) {
                         $notices[] = 'You aren\'t allow to change your background.';
-                    } elseif(!empty($_FILES['background']) && is_array($_FILES['background'])) {
-                        if(!empty($_FILES['background']['name']['file'])) {
-                            if($_FILES['background']['error']['file'] !== UPLOAD_ERR_OK) {
-                                switch($_FILES['background']['error']['file']) {
+                    } elseif(!empty($_FILES['bg_file']) && is_array($_FILES['bg_file'])) {
+                        if(!empty($_FILES['bg_file']['name'])) {
+                            if($_FILES['bg_file']['error'] !== UPLOAD_ERR_OK) {
+                                switch($_FILES['bg_file']['error']) {
                                     case UPLOAD_ERR_NO_FILE:
                                         $notices[] = 'Select a file before hitting upload!';
                                         break;
@@ -256,14 +261,14 @@ if($isEditing) {
                                 }
                             } else {
                                 try {
-                                    $backgroundInfo->setFromPath($_FILES['background']['tmp_name']['file']);
+                                    $backgroundAsset->setFromPath($_FILES['bg_file']['tmp_name']);
                                 } catch(InvalidArgumentException $ex) {
                                     $exMessage = $ex->getMessage();
                                     $notices[] = match($exMessage) {
                                         '$path is not a valid image.' => 'The file you uploaded was not an image!',
                                         '$path is not an allowed image file.' => 'This type of image is not supported, keep to PNG, JPG or GIF!',
-                                        'Dimensions of $path are too large.' => sprintf('Your background can\'t be larger than %dx%d!', $backgroundInfo->getMaxWidth(), $backgroundInfo->getMaxHeight()),
-                                        'File size of $path is too large.' => sprintf('Your background is not allowed to be larger in file size than %s!', ByteFormat::format($backgroundInfo->getMaxBytes())),
+                                        'Dimensions of $path are too large.' => sprintf('Your background can\'t be larger than %dx%d!', $backgroundAsset->getMaxWidth(), $backgroundAsset->getMaxHeight()),
+                                        'File size of $path is too large.' => sprintf('Your background is not allowed to be larger in file size than %s!', ByteFormat::format($backgroundAsset->getMaxBytes())),
                                         default => $exMessage,
                                     };
                                 } catch(RuntimeException $ex) {
@@ -272,13 +277,16 @@ if($isEditing) {
                             }
                         }
 
-                        $backgroundInfo->setAttachment((int)($_POST['background']['attach'] ?? 0))
-                            ->setBlend(!empty($_POST['background']['attr']['blend']))
-                            ->setSlide(!empty($_POST['background']['attr']['slide']));
+                        $backgroundInfo = $msz->profileCtx->backgrounds->updateProfileBackground(
+                            $userInfo,
+                            $bgFormat,
+                            filter_has_var(INPUT_POST, 'bg_blend'),
+                            filter_has_var(INPUT_POST, 'bg_slide')
+                        );
                     }
                 }
 
-                $msz->usersCtx->users->updateUser($userInfo, backgroundSettings: $backgroundInfo->getSettings());
+                $backgroundAsset = new UserBackgroundAsset($userInfo, $backgroundInfo);
             }
         }
 
@@ -319,9 +327,9 @@ if(!$viewingAsGuest) {
         );
         $activeTopicInfo = $activeTopicStats->success ? $msz->forumCtx->topics->getTopic(topicId: $activeTopicStats->topicId) : null;
 
-        $profileFieldValues = iterator_to_array($msz->profileFields->getFieldValues($userInfo));
-        $profileFieldInfos = $profileFieldInfos ?? iterator_to_array($msz->profileFields->getFields(fieldValueInfos: $isEditing ? null : $profileFieldValues));
-        $profileFieldFormats = iterator_to_array($msz->profileFields->getFieldFormats(fieldValueInfos: $profileFieldValues));
+        $profileFieldValues = iterator_to_array($msz->profileCtx->fields->getFieldValues($userInfo));
+        $profileFieldInfos = $profileFieldInfos ?? iterator_to_array($msz->profileCtx->fields->getFields(fieldValueInfos: $isEditing ? null : $profileFieldValues));
+        $profileFieldFormats = iterator_to_array($msz->profileCtx->fields->getFieldFormats(fieldValueInfos: $profileFieldValues));
 
         $profileFieldRawValues = [];
         $profileFieldLinkValues = [];
@@ -382,8 +390,9 @@ Template::render('profile.index', [
     'profile_is_guest' => $viewingAsGuest,
     'profile_is_deleted' => false,
     'profile_ban_info' => $activeBanInfo,
-    'profile_avatar_info' => $avatarInfo,
+    'profile_avatar_asset' => $avatarAsset,
     'profile_background_info' => $backgroundInfo,
+    'profile_background_asset' => $backgroundAsset,
     'profile_can_send_messages' => $viewerPermsGlobal->check(Perm::G_MESSAGES_SEND),
     'profile_age' => $msz->usersCtx->birthdates->getUserAge($userInfo),
 ]);
diff --git a/public-legacy/settings/data.php b/public-legacy/settings/data.php
index 2b4cd89a..fd919016 100644
--- a/public-legacy/settings/data.php
+++ b/public-legacy/settings/data.php
@@ -146,9 +146,10 @@ if(isset($_POST['action']) && is_string($_POST['action'])) {
                             $tmpFiles[] = db_to_zip($archive, $userInfo, 'oauth2_refresh',         ['ref_id:s', 'app_id:s', 'user_id:s:n', 'acc_id:s:n', 'ref_token:n', 'ref_scope:s', 'ref_created:t', 'ref_expires:t']);
                             $tmpFiles[] = db_to_zip($archive, $userInfo, 'perms',                  ['user_id:s:n', 'role_id:s:n', 'forum_id:s:n', 'perms_category:s', 'perms_allow:i', 'perms_deny:i']);
                             $tmpFiles[] = db_to_zip($archive, $userInfo, 'perms_calculated',       ['user_id:s:n', 'forum_id:s:n', 'perms_category:s', 'perms_calculated:i']);
+                            $tmpFiles[] = db_to_zip($archive, $userInfo, 'profile_backgrounds',    ['user_id:s', 'bg_attach:s', 'bg_blend:i', 'bg_slide:i']);
                             $tmpFiles[] = db_to_zip($archive, $userInfo, 'profile_fields_values',  ['field_id:s', 'user_id:s', 'format_id:s', 'field_value:s']);
                             $tmpFiles[] = db_to_zip($archive, $userInfo, 'sessions',               ['session_id:s', 'user_id:s', 'session_key:n', 'session_remote_addr_first:a', 'session_remote_addr_last:a:n', 'session_user_agent:s', 'session_country:s', 'session_expires:t', 'session_expires_bump:b', 'session_created:t', 'session_active:t:n']);
-                            $tmpFiles[] = db_to_zip($archive, $userInfo, 'users',                  ['user_id:s', 'user_name:s', 'user_password:n', 'user_email:s', 'user_remote_addr_first:a', 'user_remote_addr_last:a', 'user_super:b', 'user_country:s', 'user_colour:i:n', 'user_created:t', 'user_active:t:n', 'user_deleted:t:n', 'user_display_role_id:s:n', 'user_about_content:s:n', 'user_about_content_format:s', 'user_signature_content:s:n', 'user_signature_content_format:s', 'user_background_settings:i:n', 'user_title:s:n']);
+                            $tmpFiles[] = db_to_zip($archive, $userInfo, 'users',                  ['user_id:s', 'user_name:s', 'user_password:n', 'user_email:s', 'user_remote_addr_first:a', 'user_remote_addr_last:a', 'user_super:b', 'user_country:s', 'user_colour:i:n', 'user_created:t', 'user_active:t:n', 'user_deleted:t:n', 'user_display_role_id:s:n', 'user_about_content:s:n', 'user_about_content_format:s', 'user_signature_content:s:n', 'user_signature_content_format:s', 'user_title:s:n']);
                             $tmpFiles[] = db_to_zip($archive, $userInfo, 'users_bans',             ['ban_id:s', 'user_id:s', 'mod_id:n', 'ban_severity:i', 'ban_reason_public:s', 'ban_reason_private:s', 'ban_created:t', 'ban_expires:t:n']);
                             $tmpFiles[] = db_to_zip($archive, $userInfo, 'users_birthdates',       ['user_id:s', 'birth_year:i:n', 'birth_month:i', 'birth_day:i']);
                             $tmpFiles[] = db_to_zip($archive, $userInfo, 'users_password_resets',  ['reset_id:s', 'user_id:s', 'reset_remote_addr:a', 'reset_requested:t', 'reset_code:n']);
diff --git a/src/MisuzuContext.php b/src/MisuzuContext.php
index 34504316..735ccb27 100644
--- a/src/MisuzuContext.php
+++ b/src/MisuzuContext.php
@@ -21,7 +21,7 @@ use Misuzu\Messages\MessagesContext;
 use Misuzu\News\NewsData;
 use Misuzu\OAuth2\OAuth2Context;
 use Misuzu\Perms\PermissionsData;
-use Misuzu\Profile\ProfileFieldsData;
+use Misuzu\Profile\ProfileContext;
 use Misuzu\Redirects\RedirectsContext;
 use Misuzu\Routing\{BackedRoutingContext,RoutingContext};
 use Misuzu\Users\{UsersContext,UserInfo};
@@ -52,11 +52,10 @@ class MisuzuContext {
     public private(set) ForumContext $forumCtx;
     public private(set) MessagesContext $messagesCtx;
     public private(set) OAuth2Context $oauth2Ctx;
+    public private(set) ProfileContext $profileCtx;
     public private(set) UsersContext $usersCtx;
     public private(set) RedirectsContext $redirectsCtx;
 
-    public private(set) ProfileFieldsData $profileFields;
-
     public private(set) PermissionsData $perms;
     public private(set) AuthInfo $authInfo;
     public private(set) SiteInfo $siteInfo;
@@ -84,6 +83,7 @@ class MisuzuContext {
         $this->deps->register($this->forumCtx = $this->deps->constructLazy(ForumContext::class));
         $this->deps->register($this->messagesCtx = $this->deps->constructLazy(MessagesContext::class));
         $this->deps->register($this->oauth2Ctx = $this->deps->constructLazy(OAuth2Context::class, config: $config->scopeTo('oauth2')));
+        $this->deps->register($this->profileCtx = $this->deps->constructLazy(ProfileContext::class));
         $this->deps->register($this->usersCtx = $this->deps->constructLazy(UsersContext::class));
         $this->deps->register($this->redirectsCtx = $this->deps->constructLazy(RedirectsContext::class, config: $config->scopeTo('redirects')));
 
@@ -93,7 +93,6 @@ class MisuzuContext {
         $this->deps->register($this->counters = $this->deps->constructLazy(CountersData::class));
         $this->deps->register($this->emotes = $this->deps->constructLazy(EmotesData::class));
         $this->deps->register($this->news = $this->deps->constructLazy(NewsData::class));
-        $this->deps->register($this->profileFields = $this->deps->constructLazy(ProfileFieldsData::class));
     }
 
     public function getDbQueryCount(): int {
diff --git a/src/Profile/ProfileBackgroundAttach.php b/src/Profile/ProfileBackgroundAttach.php
new file mode 100644
index 00000000..0572ef15
--- /dev/null
+++ b/src/Profile/ProfileBackgroundAttach.php
@@ -0,0 +1,9 @@
+<?php
+namespace Misuzu\Profile;
+
+enum ProfileBackgroundAttach: string {
+    case Cover = 'cover';
+    case Stretch = 'stretch';
+    case Tile = 'tile';
+    case Contain = 'contain';
+}
diff --git a/src/Profile/ProfileBackgroundInfo.php b/src/Profile/ProfileBackgroundInfo.php
new file mode 100644
index 00000000..29657e63
--- /dev/null
+++ b/src/Profile/ProfileBackgroundInfo.php
@@ -0,0 +1,38 @@
+<?php
+namespace Misuzu\Profile;
+
+use Index\Db\DbResult;
+
+class ProfileBackgroundInfo {
+    public function __construct(
+        public private(set) string $userId,
+        public private(set) ProfileBackgroundAttach $attach,
+        public private(set) bool $blend,
+        public private(set) bool $slide,
+    ) {}
+
+    public static function fromResult(DbResult $result): ProfileBackgroundInfo {
+        return new ProfileBackgroundInfo(
+            userId: $result->getString(0),
+            attach: ProfileBackgroundAttach::tryFrom($result->getString(1)) ?? ProfileBackgroundAttach::Cover,
+            blend: $result->getBoolean(2),
+            slide: $result->getBoolean(3),
+        );
+    }
+
+    public bool $cover {
+        get => $this->attach === ProfileBackgroundAttach::Cover;
+    }
+
+    public bool $stretch {
+        get => $this->attach === ProfileBackgroundAttach::Stretch;
+    }
+
+    public bool $tile {
+        get => $this->attach === ProfileBackgroundAttach::Tile;
+    }
+
+    public bool $contain {
+        get => $this->attach === ProfileBackgroundAttach::Contain;
+    }
+}
diff --git a/src/Profile/ProfileBackgroundsData.php b/src/Profile/ProfileBackgroundsData.php
new file mode 100644
index 00000000..2fe17be5
--- /dev/null
+++ b/src/Profile/ProfileBackgroundsData.php
@@ -0,0 +1,63 @@
+<?php
+namespace Misuzu\Profile;
+
+use RuntimeException;
+use Index\Db\{DbConnection,DbStatementCache};
+use Misuzu\Users\UserInfo;
+
+class ProfileBackgroundsData {
+    private DbStatementCache $cache;
+
+    public function __construct(DbConnection $dbConn) {
+        $this->cache = new DbStatementCache($dbConn);
+    }
+
+    public function getProfileBackground(UserInfo|string $userInfo): ?ProfileBackgroundInfo {
+        $stmt = $this->cache->get(<<<SQL
+            SELECT user_id, bg_attach, bg_blend, bg_slide
+            FROM msz_profile_backgrounds
+            WHERE user_id = ?
+        SQL);
+        $stmt->nextParameter($userInfo instanceof UserInfo ? $userInfo->id : $userInfo);
+        $stmt->execute();
+
+        $result = $stmt->getResult();
+        return $result->next() ? ProfileBackgroundInfo::fromResult($result) : null;
+    }
+
+    public function deleteProfileBackground(UserInfo|string $userInfo): void {
+        $stmt = $this->cache->get(<<<SQL
+            DELETE FROM msz_profile_backgrounds
+            WHERE user_id = ?
+        SQL);
+        $stmt->nextParameter($userInfo instanceof UserInfo ? $userInfo->id : $userInfo);
+        $stmt->execute();
+    }
+
+    public function updateProfileBackground(
+        UserInfo|string $userInfo,
+        ProfileBackgroundAttach|string $attach,
+        bool $blend,
+        bool $slide
+    ): ProfileBackgroundInfo {
+        if(is_string($attach))
+            $attach = ProfileBackgroundAttach::from($attach);
+
+        $stmt = $this->cache->get(<<<SQL
+            REPLACE INTO msz_profile_backgrounds (
+                user_id, bg_attach, bg_blend, bg_slide
+            ) VALUES (?, ?, ?, ?)
+        SQL);
+        $stmt->nextParameter($userInfo instanceof UserInfo ? $userInfo->id : $userInfo);
+        $stmt->nextParameter($attach->value);
+        $stmt->nextParameter($blend ? 1 : 0);
+        $stmt->nextParameter($slide ? 1 : 0);
+        $stmt->execute();
+
+        $bgInfo = $this->getProfileBackground($userInfo);
+        if($bgInfo === null)
+            throw new RuntimeException('failed to update profile background');
+
+        return $bgInfo;
+    }
+}
diff --git a/src/Profile/ProfileContext.php b/src/Profile/ProfileContext.php
new file mode 100644
index 00000000..ed0ed3a1
--- /dev/null
+++ b/src/Profile/ProfileContext.php
@@ -0,0 +1,14 @@
+<?php
+namespace Misuzu\Profile;
+
+use Index\Db\DbConnection;
+
+class ProfileContext {
+    public private(set) ProfileBackgroundsData $backgrounds;
+    public private(set) ProfileFieldsData $fields;
+
+    public function __construct(DbConnection $dbConn) {
+        $this->backgrounds = new ProfileBackgroundsData($dbConn);
+        $this->fields = new ProfileFieldsData($dbConn);
+    }
+}
diff --git a/src/Satori/SatoriRoutes.php b/src/Satori/SatoriRoutes.php
index 8877fdb7..45667b1b 100644
--- a/src/Satori/SatoriRoutes.php
+++ b/src/Satori/SatoriRoutes.php
@@ -8,7 +8,7 @@ use Index\Http\{HttpRequest,HttpResponseBuilder};
 use Index\Http\Routing\{HttpGet,HttpMiddleware,RouteHandler,RouteHandlerCommon};
 use Misuzu\Pagination;
 use Misuzu\Forum\ForumContext;
-use Misuzu\Profile\ProfileFieldsData;
+use Misuzu\Profile\ProfileContext;
 use Misuzu\Users\UsersContext;
 
 final class SatoriRoutes implements RouteHandler {
@@ -18,7 +18,7 @@ final class SatoriRoutes implements RouteHandler {
         private Config $config,
         private UsersContext $usersCtx,
         private ForumContext $forumCtx,
-        private ProfileFieldsData $profileFields
+        private ProfileContext $profileCtx
     ) {}
 
     /** @return void|int */
@@ -49,7 +49,7 @@ final class SatoriRoutes implements RouteHandler {
         $fieldId = (string)$request->getParam('field', FILTER_SANITIZE_NUMBER_INT);
 
         try {
-            $fieldValue = $this->profileFields->getFieldValue($fieldId, $userId);
+            $fieldValue = $this->profileCtx->fields->getFieldValue($fieldId, $userId);
         } catch(RuntimeException $ex) {
             return ['error' => 105];
         }
diff --git a/src/Users/Assets/AssetsRoutes.php b/src/Users/Assets/AssetsRoutes.php
index 4982e17b..f3292abe 100644
--- a/src/Users/Assets/AssetsRoutes.php
+++ b/src/Users/Assets/AssetsRoutes.php
@@ -8,6 +8,7 @@ use Index\Http\Routing\{HttpGet,RouteHandler,RouteHandlerCommon};
 use Index\Urls\{UrlFormat,UrlRegistry,UrlSource,UrlSourceCommon};
 use Misuzu\Perm;
 use Misuzu\Auth\AuthInfo;
+use Misuzu\Profile\ProfileContext;
 use Misuzu\Users\{UsersContext,UserInfo};
 
 class AssetsRoutes implements RouteHandler, UrlSource {
@@ -16,7 +17,8 @@ class AssetsRoutes implements RouteHandler, UrlSource {
     public function __construct(
         private AuthInfo $authInfo,
         private UrlRegistry $urls,
-        private UsersContext $usersCtx
+        private UsersContext $usersCtx,
+        private ProfileContext $profileCtx
     ) {}
 
     private function canViewAsset(HttpRequest $request, UserInfo $assetUser): bool {
@@ -63,7 +65,10 @@ class AssetsRoutes implements RouteHandler, UrlSource {
         } catch(InvalidArgumentException $ex) {}
 
         if(!empty($userInfo)) {
-            $userAssetInfo = new UserBackgroundAsset($userInfo);
+            $userAssetInfo = new UserBackgroundAsset(
+                $userInfo,
+                $this->profileCtx->backgrounds->getProfileBackground($userInfo)
+            );
             if($userAssetInfo->isPresent() && $this->canViewAsset($request, $userInfo))
                 $assetInfo = $userAssetInfo;
         }
diff --git a/src/Users/Assets/UserBackgroundAsset.php b/src/Users/Assets/UserBackgroundAsset.php
index 29bd45af..1aba37fb 100644
--- a/src/Users/Assets/UserBackgroundAsset.php
+++ b/src/Users/Assets/UserBackgroundAsset.php
@@ -1,14 +1,9 @@
 <?php
 namespace Misuzu\Users\Assets;
 
-use InvalidArgumentException;
+use Misuzu\Profile\ProfileBackgroundInfo;
 use Misuzu\Users\UserInfo;
 
-// attachment and attributes are to be stored in the same byte
-// left half is for attributes, right half is for attachments
-// this makes for 16 possible attachments and 4 possible attributes
-// since attachments are just an incrementing number and attrs are flags
-
 class UserBackgroundAsset extends UserImageAsset {
     private const FORMAT = 'backgrounds/original/%d.msz';
 
@@ -16,46 +11,11 @@ class UserBackgroundAsset extends UserImageAsset {
     private const MAX_HEIGHT = 2160;
     private const MAX_BYTES  = 1500000;
 
-    public const ATTACH_NONE    = 0x00;
-    public const ATTACH_COVER   = 0x01;
-    public const ATTACH_STRETCH = 0x02;
-    public const ATTACH_TILE    = 0x03;
-    public const ATTACH_CONTAIN = 0x04;
-
-    public const ATTRIB_BLEND = 0x10;
-    public const ATTRIB_SLIDE = 0x20;
-
-    public const ATTACHMENT_STRINGS = [
-        self::ATTACH_COVER   => 'cover',
-        self::ATTACH_STRETCH => 'stretch',
-        self::ATTACH_TILE    => 'tile',
-        self::ATTACH_CONTAIN => 'contain',
-    ];
-
-    public const ATTRIBUTE_STRINGS = [
-        self::ATTRIB_BLEND => 'blend',
-        self::ATTRIB_SLIDE => 'slide',
-    ];
-
-    /** @return array<int, string> */
-    public static function getAttachmentStringOptions(): array {
-        return [
-            self::ATTACH_COVER   => 'Cover',
-            self::ATTACH_STRETCH => 'Stretch',
-            self::ATTACH_TILE    => 'Tile',
-            self::ATTACH_CONTAIN => 'Contain',
-        ];
-    }
-
-    private int $settings;
-
-    public function __construct(UserInfo $userInfo) {
+    public function __construct(
+        UserInfo $userInfo,
+        private ?ProfileBackgroundInfo $bgInfo
+    ) {
         parent::__construct($userInfo);
-        $this->settings = (int)$userInfo->backgroundSettings;
-    }
-
-    public function getSettings(): int {
-        return $this->settings;
     }
 
     public function getMaxWidth(): int {
@@ -78,68 +38,4 @@ class UserBackgroundAsset extends UserImageAsset {
     public function getRelativePath(): string {
         return sprintf(self::FORMAT, $this->getUserId());
     }
-
-    public function getAttachment(): int {
-        return $this->settings & 0x0F;
-    }
-    public function getAttachmentString(): string {
-        return self::ATTACHMENT_STRINGS[$this->getAttachment()] ?? '';
-    }
-    public function setAttachment(int $attach): self {
-        $this->settings = $this->getAttributes() | ($attach & 0x0F);
-        return $this;
-    }
-    public function setAttachmentString(string $attach): self {
-        if(!in_array($attach, self::ATTACHMENT_STRINGS))
-            throw new InvalidArgumentException;
-        $this->setAttachment(array_flip(self::ATTACHMENT_STRINGS)[$attach]);
-        return $this;
-    }
-
-    public function getAttributes(): int {
-        return $this->settings & 0xF0;
-    }
-    public function setAttributes(int $attrib): self {
-        $this->settings = $this->getAttachment() | ($attrib & 0xF0);
-        return $this;
-    }
-    public function isBlend(): bool {
-        return ($this->getAttributes() & self::ATTRIB_BLEND) > 0;
-    }
-    public function setBlend(bool $blend): self {
-        $this->settings = $blend
-            ? ($this->settings |  self::ATTRIB_BLEND)
-            : ($this->settings & ~self::ATTRIB_BLEND);
-        return $this;
-    }
-    public function isSlide(): bool {
-        return ($this->getAttributes() & self::ATTRIB_SLIDE) > 0;
-    }
-    public function setSlide(bool $slide): self {
-        $this->settings = $slide
-            ? ($this->settings |  self::ATTRIB_SLIDE)
-            : ($this->settings & ~self::ATTRIB_SLIDE);
-        return $this;
-    }
-
-    /** @return string[] */
-    public function getClassNames(string $format = '%s'): array {
-        $names = [];
-        $attachment = $this->getAttachment();
-        $attributes = $this->getAttributes();
-
-        if(array_key_exists($attachment, self::ATTACHMENT_STRINGS))
-            $names[] = sprintf($format, self::ATTACHMENT_STRINGS[$attachment]);
-
-        foreach(self::ATTRIBUTE_STRINGS as $flag => $name)
-            if(($attributes & $flag) > 0)
-                $names[] = sprintf($format, $name);
-
-        return $names;
-    }
-
-    public function delete(): void {
-        parent::delete();
-        $this->settings = 0;
-    }
 }
diff --git a/src/Users/UserInfo.php b/src/Users/UserInfo.php
index 201d964a..95ecf190 100644
--- a/src/Users/UserInfo.php
+++ b/src/Users/UserInfo.php
@@ -25,7 +25,6 @@ class UserInfo {
         public private(set) TextFormat $aboutBodyFormat,
         public private(set) ?string $signatureBody,
         public private(set) TextFormat $signatureBodyFormat,
-        public private(set) ?int $backgroundSettings,
         public private(set) ?string $title,
     ) {}
 
@@ -48,8 +47,7 @@ class UserInfo {
             aboutBodyFormat: TextFormat::tryFrom($result->getString(14)) ?? TextFormat::Plain,
             signatureBody: $result->getStringOrNull(15),
             signatureBodyFormat: TextFormat::tryFrom($result->getString(16)) ?? TextFormat::Plain,
-            backgroundSettings: $result->getIntegerOrNull(17),
-            title: $result->getString(18),
+            title: $result->getString(17),
         );
     }
 
diff --git a/src/Users/UsersData.php b/src/Users/UsersData.php
index c98d0c41..45f58e7e 100644
--- a/src/Users/UsersData.php
+++ b/src/Users/UsersData.php
@@ -9,7 +9,7 @@ use Index\Colour\Colour;
 use Index\Db\{DbConnection,DbStatementCache,DbTools};
 use Misuzu\Pagination;
 use Misuzu\Tools;
-use Misuzu\Parsers\Parsers;
+use Misuzu\Parsers\TextFormat;
 
 class UsersData {
     private DbStatementCache $cache;
@@ -179,7 +179,7 @@ class UsersData {
                 u.user_display_role_id,
                 u.user_about_content, u.user_about_content_format,
                 u.user_signature_content, u.user_signature_content_format,
-                u.user_background_settings, u.user_title
+                u.user_title
             FROM msz_users AS u
         SQL;
         if($hasRoleInfo)
@@ -288,7 +288,7 @@ class UsersData {
                 user_display_role_id,
                 user_about_content, user_about_content_format,
                 user_signature_content, user_signature_content_format,
-                user_background_settings, user_title
+                user_title
             FROM msz_users
         SQL;
         if($selectId) {
@@ -358,10 +358,9 @@ class UsersData {
         ?Colour $colour = null,
         RoleInfo|string|null $displayRoleInfo = null,
         ?string $aboutBody = null,
-        ?int $aboutBodyFormat = null,
+        TextFormat|string|null $aboutBodyFormat = null,
         ?string $signatureBody = null,
-        ?int $signatureBodyFormat = null,
-        ?int $backgroundSettings = null,
+        TextFormat|string|null $signatureBodyFormat = null,
         ?string $title = null
     ): void {
         if($userInfo instanceof UserInfo)
@@ -409,28 +408,27 @@ class UsersData {
         }
 
         if($aboutBody !== null && $aboutBodyFormat !== null) {
+            if(is_string($aboutBodyFormat))
+                $aboutBodyFormat = TextFormat::tryFrom($aboutBodyFormat) ?? null;
             if(self::validateProfileAbout($aboutBodyFormat, $aboutBody) !== '')
                 throw new InvalidArgumentException('$aboutBody and $aboutBodyFormat contain invalid data!');
 
             $fields[] = 'user_about_content = ?';
             $values[] = $aboutBody;
             $fields[] = 'user_about_content_format = ?';
-            $values[] = $aboutBodyFormat;
+            $values[] = $aboutBodyFormat->value;
         }
 
         if($signatureBody !== null && $signatureBodyFormat !== null) {
+            if(is_string($signatureBodyFormat))
+                $signatureBodyFormat = TextFormat::tryFrom($signatureBodyFormat) ?? null;
             if(self::validateForumSignature($signatureBodyFormat, $signatureBody) !== '')
                 throw new InvalidArgumentException('$signatureBody and $signatureBodyFormat contain invalid data!');
 
             $fields[] = 'user_signature_content = ?';
             $values[] = $signatureBody;
             $fields[] = 'user_signature_content_format = ?';
-            $values[] = $signatureBodyFormat;
-        }
-
-        if($backgroundSettings !== null) {
-            $fields[] = 'user_background_settings = ?';
-            $values[] = $backgroundSettings;
+            $values[] = $signatureBodyFormat->value;
         }
 
         if($title !== null) {
@@ -709,8 +707,8 @@ class UsersData {
         };
     }
 
-    public static function validateProfileAbout(int $parser, string $text): string {
-        if(!Parsers::isValid($parser))
+    public static function validateProfileAbout(?TextFormat $format, string $text): string {
+        if($format === null)
             return 'parser';
 
         $length = strlen($text);
@@ -729,8 +727,8 @@ class UsersData {
         };
     }
 
-    public static function validateForumSignature(int $parser, string $text): string {
-        if(!Parsers::isValid($parser))
+    public static function validateForumSignature(?TextFormat $format, string $text): string {
+        if($format === null)
             return 'parser';
 
         $length = strlen($text);
diff --git a/templates/_layout/input.twig b/templates/_layout/input.twig
index 27f60d27..304b700b 100644
--- a/templates/_layout/input.twig
+++ b/templates/_layout/input.twig
@@ -30,7 +30,7 @@
         {% if name|length > 0 %}name="{{ name }}"{% endif %}
         {% if checked %}checked{% endif %}
         {% if disabled %}disabled{% endif %}
-        {% if value|length > 0 %}value="{{ value }}"{% endif %}
+        {% if value is defined and value is not null %}value="{{ value }}"{% endif %}
         {% for name, value in attributes|default([]) %}
             {{ name }}{% if value|length > 0 %}="{{ value }}"{% endif %}
         {% endfor %}>
@@ -74,9 +74,9 @@
             Click here to select a file!
         </div>
         <script>
-            const parent = document.currentScript.parentNode,
-                input = parent.querySelector('input[type="file"]'),
-                display = parent.querySelector('.input__upload__selection');
+            const parent = document.currentScript.parentNode;
+            const input = parent.querySelector('input[type="file"]');
+            const display = parent.querySelector('.input__upload__selection');
             input.addEventListener('change', ev => display.textContent = Array.from(ev.target.files).map(f => f.name).join(', '));
         </script>
     </label>
diff --git a/templates/manage/master.twig b/templates/manage/master.twig
index 623a1c6a..7f60bd8d 100644
--- a/templates/manage/master.twig
+++ b/templates/manage/master.twig
@@ -4,7 +4,7 @@
 
 {% set is_in_manage = true %}
 {% set title = title|default('Broom Closet') %}
-{% set site_logo = '/images/logos/imouto-broom.png' %}
+{% set main_css_vars = main_css_vars|default([])|merge({'--site-logo': "url('/images/logos/imouto-broom.png')"}) %}
 
 {% block content %}
     <div class="manage">
diff --git a/templates/master.twig b/templates/master.twig
index 962c13e0..cadf6249 100644
--- a/templates/master.twig
+++ b/templates/master.twig
@@ -5,26 +5,19 @@
     <link href="/vendor/fontawesome/css/all.min.css" type="text/css" rel="stylesheet">
     <link href="{{ asset('common.css') }}" type="text/css" rel="stylesheet">
     <link href="{{ asset('misuzu.css') }}" type="text/css" rel="stylesheet">
-    {% if site_background is defined %}
+    {% if main_css_vars is defined and main_css_vars is iterable and main_css_vars is not empty %}
         <style>
             :root {
-                --background-width: {{ site_background.width }}px;
-                --background-height: {{ site_background.height }}px;
-                --background-image: url('{{ site_background_url|raw }}');
-            }
-        </style>
-    {% endif %}
-    {% if site_logo is defined %}
-        <style>
-            :root {
-                --site-logo: url('{{ site_logo }}');
+                {% for name, value in main_css_vars %}
+                    {{ name }}: {{ value|raw }};
+                {% endfor %}
             }
         </style>
     {% endif %}
 {% endblock %}
 
 {% set html_body_attrs = {
-    'class': 'main' ~ (site_background is defined ? (' ' ~ site_background.classNames('main--bg-%s')|join(' ')) : ''),
+    'class': html_classes('main', main_body_classes|default([])),
     'style': global_accent_colour is defined ? ('--accent-colour: ' ~ global_accent_colour) : '',
 } %}
 
diff --git a/templates/profile/index.twig b/templates/profile/index.twig
index 889809b3..18cd786d 100644
--- a/templates/profile/index.twig
+++ b/templates/profile/index.twig
@@ -47,8 +47,8 @@
                     {% if perms.edit_avatar %}
                         <ul class="profile__guidelines__section">
                             <li class="profile__guidelines__line profile__guidelines__line--header">Avatar</li>
-                            <li class="profile__guidelines__line">May not exceed the <span class="profile__guidelines__emphasis">{{ profile_avatar_info.maxBytes|format_filesize }}</span> file size limit.</li>
-                            <li class="profile__guidelines__line">May not be larger than <span class="profile__guidelines__emphasis">{{ profile_avatar_info.maxWidth }}x{{ profile_avatar_info.maxHeight }}</span>.</li>
+                            <li class="profile__guidelines__line">May not exceed the <span class="profile__guidelines__emphasis">{{ profile_avatar_asset.maxBytes|format_filesize }}</span> file size limit.</li>
+                            <li class="profile__guidelines__line">May not be larger than <span class="profile__guidelines__emphasis">{{ profile_avatar_asset.maxWidth }}x{{ profile_avatar_asset.maxHeight }}</span>.</li>
                             <li class="profile__guidelines__line">Will be centre cropped and scaled to at most <span class="profile__guidelines__emphasis">240x240</span>.</li>
                             <li class="profile__guidelines__line">Animated GIF images are allowed.</li>
                         </ul>
@@ -57,8 +57,8 @@
                     {% if perms.edit_background %}
                         <ul class="profile__guidelines__section">
                             <li class="profile__guidelines__line profile__guidelines__line--header">Background</li>
-                            <li class="profile__guidelines__line">May not exceed the <span class="profile__guidelines__emphasis">{{ profile_background_info.maxBytes|format_filesize }}</span> file size limit.</li>
-                            <li class="profile__guidelines__line">May not be larger than <span class="profile__guidelines__emphasis">{{ profile_background_info.maxWidth }}x{{ profile_background_info.maxHeight }}</span>.</li>
+                            <li class="profile__guidelines__line">May not exceed the <span class="profile__guidelines__emphasis">{{ profile_background_asset.maxBytes|format_filesize }}</span> file size limit.</li>
+                            <li class="profile__guidelines__line">May not be larger than <span class="profile__guidelines__emphasis">{{ profile_background_asset.maxWidth }}x{{ profile_background_asset.maxHeight }}</span>.</li>
                             <li class="profile__guidelines__line">GIF images, in general, are only allowed when tiling.</li>
                         </ul>
                     {% endif %}
@@ -90,15 +90,14 @@
                                 {{ container_title('Background') }}
 
                                 <div class="profile__background-settings__content">
-                                    {{ input_file('background[file]', '', ['image/png', 'image/jpeg', 'image/gif'], {'id':'background-selection'}) }}
+                                    {{ input_file('bg_file', '', ['image/png', 'image/jpeg', 'image/gif'], {'id':'background-selection'}) }}
 
-                                    {{ input_checkbox('background[attach]', 'None', true, '', 0, true, {'onchange':'profileChangeBackgroundAttach(this.value)'}) }}
-                                    {% for key, value in background_attachments %}
-                                        {{ input_checkbox('background[attach]', value, key == profile_background_info.attachment, '', key, true, {'onchange':'profileChangeBackgroundAttach(this.value)'}) }}
+                                    {% for key, value in { '': 'None', 'cover': 'Cover', 'stretch': 'Stretch', 'tile': 'Tile', 'contain': 'Contain' } %}
+                                        {{ input_checkbox('bg_attach', value, key == profile_background_info.attach.value|default(''), '', key, true, {'onchange':'profileChangeBackgroundAttach(this.value)'}) }}
                                     {% endfor %}
 
-                                    {{ input_checkbox('background[attr][blend]', 'Blend', profile_background_info.blend, '', '', false, {'onchange':'profileToggleBackgroundAttr(\'blend\', this.checked)'}) }}
-                                    {{ input_checkbox('background[attr][slide]', 'Slide', profile_background_info.slide, '', '', false, {'onchange':'profileToggleBackgroundAttr(\'slide\', this.checked)'}) }}
+                                    {{ input_checkbox('bg_blend', 'Blend', profile_background_info.blend|default(false), '', '', false, {'onchange':'profileToggleBackgroundAttr(\'blend\', this.checked)'}) }}
+                                    {{ input_checkbox('bg_slide', 'Slide', profile_background_info.slide|default(false), '', '', false, {'onchange':'profileToggleBackgroundAttr(\'slide\', this.checked)'}) }}
                                 </div>
                             </div>
                         {% endif %}
@@ -304,10 +303,10 @@
             function profileToggleBackground(checked) {
                 let currentBg = document.body.style.getPropertyValue('--background-image');
 
-                if(currentBg != 'initial' && checked) {
+                if(currentBg != '' && checked) {
                     profilePreviousBackground = currentBg;
-                    currentBg = 'initial';
-                } else if(currentBg == 'initial' && !checked) {
+                    currentBg = '';
+                } else if(currentBg == '' && !checked) {
                     currentBg = profilePreviousBackground;
                 }
 
@@ -315,47 +314,23 @@
             }
 
             function profileChangeBackgroundAttach(mode) {
-                const modes = {
-                    1: 'cover',
-                    2: 'stretch',
-                    3: 'tile',
-                    4: 'contain',
-                };
+                const modes = ['cover', 'stretch', 'tile', 'contain'];
 
-                profileToggleBackground(mode == 0);
+                for(const m of modes)
+                    document.body.classList.remove(`main--bg-${m}`);
 
-                for(let i = 1; i <= Object.keys(modes).length; i++)
-                    document.body.classList.remove('main--bg-' + modes[i]);
-
-                if(!modes[mode])
-                    return;
-
-                document.body.classList.add('main--bg-' + modes[mode]);
+                const valid = modes.includes(mode);
+                profileToggleBackground(valid);
+                if(valid)
+                    document.body.classList.add(`main--bg-${mode}`);
             }
 
             function profileToggleBackgroundAttr(attr, mode) {
-                let className = '';
-
-                switch(attr) {
-                    case 'blend':
-                        className = 'main--bg-blend';
-                        break;
-
-                    case 'slide':
-                        className = 'main--bg-slide';
-                        break;
-                }
-
-                if(className) {
-                    if(mode)
-                        document.body.classList.add(className);
-                    else
-                        document.body.classList.remove(className);
-                }
+                document.body.classList.toggle(`main--bg-${attr}`, mode);
             }
 
             document.getElementById('background-selection').addEventListener('change', ev => {
-                const image = new Image();
+                const image = new Image;
                 image.src = URL.createObjectURL(ev.target.files[0]);
                 image.addEventListener('load', () => {
                     document.body.style.setProperty('--background-image', 'url(%)'.replace('%', image.src));
diff --git a/templates/profile/master.twig b/templates/profile/master.twig
index 2523bf5f..85fd69e8 100644
--- a/templates/profile/master.twig
+++ b/templates/profile/master.twig
@@ -3,9 +3,20 @@
 {% if profile_user is defined %}
     {% set image = url('user-avatar', {'user': profile_user.id, 'res': 200}) %}
     {% set manage_link = url('manage-user', {'user': profile_user.id}) %}
-    {% if (not profile_is_banned or profile_can_edit) %}
-        {% set site_background = profile_background_info %}
-        {% set site_background_url = url('user-background', {'user': profile_user.id}) %}
+    {% if (not profile_is_banned or profile_can_edit) and profile_background_info is not null %}
+        {% set main_body_classes = main_body_classes|default([])|merge({
+            'main--bg-cover': profile_background_info.cover,
+            'main--bg-stretch': profile_background_info.stretch,
+            'main--bg-tile': profile_background_info.tile,
+            'main--bg-contain': profile_background_info.contain,
+            'main--bg-blend': profile_background_info.blend,
+            'main--bg-slide': profile_background_info.slide,
+        }) %}
+        {% set main_css_vars = main_css_vars|default([])|merge({
+            '--background-width': '%dpx'|format(profile_background_asset.width),
+            '--background-height': '%dpx'|format(profile_background_asset.height),
+            '--background-image': "url('%s')"|format(url('user-background', {'user': profile_user.id})),
+        }) %}
     {% endif %}
     {% set stats = [
         {