diff --git a/BondageClub/Assets/Female3DCG/AssetStrings.csv b/BondageClub/Assets/Female3DCG/AssetStrings.csv
index 36421d330f..b4f1f66716 100644
--- a/BondageClub/Assets/Female3DCG/AssetStrings.csv
+++ b/BondageClub/Assets/Female3DCG/AssetStrings.csv
@@ -6448,7 +6448,8 @@ ItemHoodCreepyIronMaskSelectBase,Creepy Iron Mask main menu:
 ItemHoodCreepyIronMaskModuleMode,Mask style
 ItemHoodCreepyIronMaskSelectMode,Mask menu:
 ItemHoodCreepyIronMaskOptionm0,Just mask
-ItemHoodCreepyIronMaskOptionm1,Full hood
+ItemHoodCreepyIronMaskOptionm1,Semi-hood
+ItemHoodCreepyIronMaskOptionm2,Full hood
 ItemHoodCreepyIronMaskModuleBlindfold,Blindfold
 ItemHoodCreepyIronMaskSelectBlindfold,Metal blindfold option:
 ItemHoodCreepyIronMaskOptionb0,None
@@ -6462,8 +6463,13 @@ ItemHoodCreepyIronMaskModuleNose,Nose guard
 ItemHoodCreepyIronMaskSelectNose,Nose guard option:
 ItemHoodCreepyIronMaskOptionn0,None
 ItemHoodCreepyIronMaskOptionn1,Add nose guard
+ItemHoodCreepyIronMaskModuleSpeech,Speech
+ItemHoodCreepyIronMaskSelectSpeech,Speech option:
+ItemHoodCreepyIronMaskOptionp0,Loose
+ItemHoodCreepyIronMaskOptionp1,Tight
 ItemHoodCreepyIronMaskSetm0,SourceCharacter puts the Creepy Iron Mask on DestinationCharacterName head.
 ItemHoodCreepyIronMaskSetm1,SourceCharacter locks DestinationCharacterName head with the Creepy Iron Hood version.
+ItemHoodCreepyIronMaskSetm2,SourceCharacter locks DestinationCharacterName head with the Creepy Iron Hood version.
 ItemHoodCreepyIronMaskSetb0,SourceCharacter removes a metal blindfold from DestinationCharacterName Creepy Iron Mask.
 ItemHoodCreepyIronMaskSetb1,SourceCharacter equips a perforated metal blindfold on DestinationCharacterName Creepy Iron Mask.
 ItemHoodCreepyIronMaskSetb2,SourceCharacter equips a full metal blindfold on DestinationCharacterName Creepy Iron Mask.
@@ -6471,6 +6477,8 @@ ItemHoodCreepyIronMaskSets0,SourceCharacter removes a spiked collar from Destina
 ItemHoodCreepyIronMaskSets1,SourceCharacter equips a spiked collar on DestinationCharacterName neck.
 ItemHoodCreepyIronMaskSetn0,SourceCharacter removes a nose guard from DestinationCharacterName Creepy Iron Mask.
 ItemHoodCreepyIronMaskSetn1,SourceCharacter equips a nose guard on DestinationCharacterName Creepy Iron Mask.
+ItemHoodCreepyIronMaskSetp0,SourceCharacter loosens DestinationCharacterName Creepy Iron Mask.
+ItemHoodCreepyIronMaskSetp1,SourceCharacter tightens DestinationCharacterName Creepy Iron Mask.
 ItemMouthHorrorMuzzleSelect,Horror Muzzle style option menu:
 ItemMouthHorrorMuzzleNone,Plain
 ItemMouthHorrorMuzzleRivets,With rivets
@@ -6521,12 +6529,12 @@ ItemHandheldKyosensuType3,Sun
 ItemHandheldKyosensuType4,Wave art
 ItemHandheldKyosensuType5,Moon
 ItemHandheldKyosensuType6,Flowers
-ItemItemHandheldKyosensuSetType1,SourceCharacter chooses a cherry blossooms art for DestinationCharacter Kyo-sensu fan.
-ItemItemHandheldKyosensuSetType2,SourceCharacter chooses a lightning bolt art for DestinationCharacter Kyo-sensu fan.
-ItemItemHandheldKyosensuSetType3,SourceCharacter chooses a sun art for DestinationCharacter Kyo-sensu fan.
-ItemItemHandheldKyosensuSetType4,SourceCharacter chooses a wave art for DestinationCharacter Kyo-sensu fan.
-ItemItemHandheldKyosensuSetType5,SourceCharacter chooses a moon art for DestinationCharacter Kyo-sensu fan.
-ItemItemHandheldKyosensuSetType6,SourceCharacter chooses a flowers art for DestinationCharacter Kyo-sensu fan.
+ItemHandheldKyosensuSetType1,SourceCharacter chooses a cherry blossooms art for DestinationCharacter Kyo-sensu fan.
+ItemHandheldKyosensuSetType2,SourceCharacter chooses a lightning bolt art for DestinationCharacter Kyo-sensu fan.
+ItemHandheldKyosensuSetType3,SourceCharacter chooses a sun art for DestinationCharacter Kyo-sensu fan.
+ItemHandheldKyosensuSetType4,SourceCharacter chooses a wave art for DestinationCharacter Kyo-sensu fan.
+ItemHandheldKyosensuSetType5,SourceCharacter chooses a moon art for DestinationCharacter Kyo-sensu fan.
+ItemHandheldKyosensuSetType6,SourceCharacter chooses a flowers art for DestinationCharacter Kyo-sensu fan.
 ItemHandheldUchiwaSelect,Choose your Uchiwa paper art:
 ItemHandheldUchiwaType1,Cherry blossoms
 ItemHandheldUchiwaType2,Lightning bolt
@@ -6534,9 +6542,9 @@ ItemHandheldUchiwaType3,Sun
 ItemHandheldUchiwaType4,Wave art
 ItemHandheldUchiwaType5,Moon
 ItemHandheldUchiwaType6,Flowers
-ItemItemHandheldUchiwaSetType1,SourceCharacter chooses a cherry blossooms art for DestinationCharacter Uchiwa fan.
-ItemItemHandheldUchiwaSetType2,SourceCharacter chooses a lightning bolt art for DestinationCharacter Uchiwa fan.
-ItemItemHandheldUchiwaSetType3,SourceCharacter chooses a sun art for DestinationCharacter Uchiwa fan.
-ItemItemHandheldUchiwaSetType4,SourceCharacter chooses a wave art for DestinationCharacter Uchiwa fan.
-ItemItemHandheldUchiwaSetType5,SourceCharacter chooses a moon art for DestinationCharacter Uchiwa fan.
-ItemItemHandheldUchiwaSetType6,SourceCharacter chooses a flowers art for DestinationCharacter Uchiwa fan.
\ No newline at end of file
+ItemHandheldUchiwaSetType1,SourceCharacter chooses a cherry blossooms art for DestinationCharacter Uchiwa fan.
+ItemHandheldUchiwaSetType2,SourceCharacter chooses a lightning bolt art for DestinationCharacter Uchiwa fan.
+ItemHandheldUchiwaSetType3,SourceCharacter chooses a sun art for DestinationCharacter Uchiwa fan.
+ItemHandheldUchiwaSetType4,SourceCharacter chooses a wave art for DestinationCharacter Uchiwa fan.
+ItemHandheldUchiwaSetType5,SourceCharacter chooses a moon art for DestinationCharacter Uchiwa fan.
+ItemHandheldUchiwaSetType6,SourceCharacter chooses a flowers art for DestinationCharacter Uchiwa fan.
diff --git a/BondageClub/Assets/Female3DCG/Female3DCG.js b/BondageClub/Assets/Female3DCG/Female3DCG.js
index 551ddeec31..a37b542a64 100644
--- a/BondageClub/Assets/Female3DCG/Female3DCG.js
+++ b/BondageClub/Assets/Female3DCG/Female3DCG.js
@@ -17386,36 +17386,6 @@ var AssetFemale3DCG = [
 					},
 				],
 			},
-			{
-				Name: "AsylumBlindfold",
-				InventoryID: 1223,
-				BuyGroup: "AsylumMuzzle",
-				Top: 25,
-				Value: -1,
-				Difficulty: 3,
-				Time: 10,
-				DefaultColor: ["#BFBFB1", "#95866D", "#A0A0A0", "#877C66"],
-				Extended: true,
-				Random: true,
-				AllowLock: true,
-				AllowTighten: true,
-				DrawLocks: false,
-				Audio: "Buckle",
-				Hide: ["Glasses"],
-				ExpressionTrigger: [ { Name: "Soft", Group: "Eyebrows", Timer: 15 },{ Name: "Shy", Group: "Eyes", Timer: 15 } ],
-				Layer: [
-					{ Name: "Main", Priority: 35, AllowColorize: true },
-					{
-						Name: "Pad",
-						Priority: 34,
-						CreateLayerTypes: ["typed"],
-						AllowTypes: { typed: 1 },
-						AllowColorize: true,
-					},
-					{ Name: "Metal", Priority: 35, AllowColorize: true },
-					{ Name: "Lock", Priority: 35, LockLayer: true, AllowColorize: true },
-				],
-			},
 		],
 		Color: [
 			"Default",
@@ -38081,7 +38051,10 @@ var AssetFemale3DCG = [
 				AllowTighten: true,
 				DrawLocks: false,
 				Audio: "Buckle",
-				ExpressionTrigger: [ { Name: "Soft", Group: "Eyebrows", Timer: 15 },{ Name: "Shy", Group: "Eyes", Timer: 15 } ],
+				ExpressionTrigger: [
+					{ Name: "Soft", Group: "Eyebrows", Timer: 15 },
+					{ Name: "Shy", Group: "Eyes", Timer: 15 },
+				],
 				Layer: [
 					{ Name: "Main", Priority: 35, AllowColorize: true },
 					{
@@ -41813,7 +41786,10 @@ var AssetFemale3DCG = [
 				Effect: [E.BlockMouth],
 				Prerequisite: ["GagFlat"],
 				Hide: ["ItemNose"],
-				ExpressionTrigger: [ { Name: "Soft", Group: "Eyebrows", Timer: 15 },{ Name: "Shy", Group: "Eyes", Timer: 15 } ],
+				ExpressionTrigger: [
+					{ Name: "Soft", Group: "Eyebrows", Timer: 15 },
+					{ Name: "Shy", Group: "Eyes", Timer: 15 },
+				],
 				Layer: [
 					{ Name: "Main", Priority: 38, AllowColorize: true },
 					{
@@ -45775,7 +45751,10 @@ var AssetFemale3DCG = [
 				Effect: [E.BlockMouth],
 				Prerequisite: ["GagFlat"],
 				Hide: ["ItemNose"],
-				ExpressionTrigger: [ { Name: "Soft", Group: "Eyebrows", Timer: 15 },{ Name: "Shy", Group: "Eyes", Timer: 15 } ],
+				ExpressionTrigger: [
+					{ Name: "Soft", Group: "Eyebrows", Timer: 15 },
+					{ Name: "Shy", Group: "Eyes", Timer: 15 },
+				],
 				Layer: [
 					{ Name: "Main", Priority: 39, AllowColorize: true },
 					{
@@ -47022,6 +47001,39 @@ var AssetFemale3DCG = [
 					},
 				],
 			},
+			{
+				Name: "AsylumBlindfold",
+				InventoryID: 1223,
+				BuyGroup: "AsylumMuzzle",
+				Top: 25,
+				Value: -1,
+				Difficulty: 3,
+				Time: 10,
+				DefaultColor: ["#BFBFB1", "#95866D", "#A0A0A0", "#877C66"],
+				Extended: true,
+				Random: true,
+				AllowLock: true,
+				AllowTighten: true,
+				DrawLocks: false,
+				Audio: "Buckle",
+				Hide: ["Glasses"],
+				ExpressionTrigger: [
+					{ Name: "Soft", Group: "Eyebrows", Timer: 15 },
+					{ Name: "Shy", Group: "Eyes", Timer: 15 },
+				],
+				Layer: [
+					{ Name: "Main", Priority: 35, AllowColorize: true },
+					{
+						Name: "Pad",
+						Priority: 34,
+						CreateLayerTypes: ["typed"],
+						AllowTypes: { typed: 1 },
+						AllowColorize: true,
+					},
+					{ Name: "Metal", Priority: 35, AllowColorize: true },
+					{ Name: "Lock", Priority: 35, LockLayer: true, AllowColorize: true },
+				],
+			},
 		],
 		Color: [
 			"Default",
diff --git a/BondageClub/Assets/Female3DCG/Female3DCGExtended.js b/BondageClub/Assets/Female3DCG/Female3DCGExtended.js
index 840656d0a0..3b1e54ab50 100644
--- a/BondageClub/Assets/Female3DCG/Female3DCGExtended.js
+++ b/BondageClub/Assets/Female3DCG/Female3DCGExtended.js
@@ -9067,7 +9067,7 @@ var AssetFemale3DCGExtended = {
 							// Semi-Hood
 							Property: {
 								Effect: [E.BlockMouth],
-								Hide: ["HairFront","HairAccessory1"],
+								Hide: ["HairFront", "HairAccessory1"],
 							},
 						},
 						{
diff --git a/BondageClub/Assets/Female3DCG/Female3DCG_Types.d.ts b/BondageClub/Assets/Female3DCG/Female3DCG_Types.d.ts
index 823909e627..e481b21222 100644
--- a/BondageClub/Assets/Female3DCG/Female3DCG_Types.d.ts
+++ b/BondageClub/Assets/Female3DCG/Female3DCG_Types.d.ts
@@ -657,6 +657,7 @@ interface AssetDefinitionBase extends AssetCommonPropertiesGroupAsset, AssetComm
 	AllowHideItem?: string[];
 	/** @deprecated */
 	AllowTypes?: never;
+	/** A list of {@link TypeRecord} keys for which a single layer expects multiple type-specific .png files. */
 	CreateLayerTypes?: string[];
 	/**
 	 * Whether an item can be tightened or not.
@@ -819,7 +820,7 @@ interface AssetLayerDefinition extends AssetCommonPropertiesGroupAssetLayer, Ass
 	/** Whether the layer is hidden in the Color Picker UI. Defaults to false. */
 	HideColoring?: boolean;
 
-	/** A record (or a list thereof) with all screen names + option indices that should make the layer visible */
+	/** A record (or a list thereof) with all screen names + option indices, _i.e._ {@link TypeRecord} keys + values, that should make the layer visible */
 	AllowTypes?: AllowTypes.Definition;
 
 	/**
@@ -867,6 +868,11 @@ interface AssetLayerDefinition extends AssetCommonPropertiesGroupAssetLayer, Ass
 	 */
 	CopyLayerPoseMapping?: string;
 
+	/**
+	 * A list of {@link TypeRecord} keys for which a single layer expects multiple type-specific .png files.
+	 *
+	 * By default files are expected for _all_ option indices associated with the key(s), unless the valid option set has been narrowed down according to {@link AssetLayerDefinition.AllowTypes}.
+	 */
 	CreateLayerTypes?: string[];
 	/* Specifies that this layer should not be drawn if the character is wearing any item with the given attributes */
 	HideForAttribute?: AssetAttribute[];
diff --git a/BondageClub/Assets/Female3DCG/ItemHandheld/Kyosensu_FanStencil.png b/BondageClub/Assets/Female3DCG/ItemHandheld/Kyosensu_FanStencil.png
deleted file mode 100644
index 64349c5e31..0000000000
Binary files a/BondageClub/Assets/Female3DCG/ItemHandheld/Kyosensu_FanStencil.png and /dev/null differ
diff --git a/BondageClub/Assets/Female3DCG/ItemHandheld/Uchiwa_FanStencil.png b/BondageClub/Assets/Female3DCG/ItemHandheld/Uchiwa_FanStencil.png
deleted file mode 100644
index 686704de3e..0000000000
Binary files a/BondageClub/Assets/Female3DCG/ItemHandheld/Uchiwa_FanStencil.png and /dev/null differ
diff --git a/BondageClub/CSS/FriendList.css b/BondageClub/CSS/FriendList.css
index 5745a58e42..1029007321 100644
--- a/BondageClub/CSS/FriendList.css
+++ b/BondageClub/CSS/FriendList.css
@@ -33,6 +33,7 @@
 	padding: var(--small-gap);
 	width: 20%;
 	text-align: center;
+	user-select: none;
 }
 
 #friend-list-search-input {
@@ -61,17 +62,19 @@
 /* #endregion */
 
 /* #region HEADER */
+
 #friend-list-header {
-	min-height: var(--row-height);
-	display: flex;
-	align-items: center;
-	color: var(--text-color);
+	user-select: none;
 }
 
 #friend-list-header .friend-list-link {
 	text-decoration: none;
 }
 
+#friend-list-header .friend-list-row:hover {
+	color: var(--text-color);
+}
+
 #friend-list-header-hr {
 	width: 80%;
 }
@@ -105,6 +108,60 @@
 #friend-list-beep-dialog:not([data-received]) .fl-beep-sent-content {
 	display: block;
 }
+
+.ChatRoomType {
+	display: flex;
+	justify-content: center;
+	gap: 0.15em;
+	user-select: none;
+}
+
+.friend-list-icon-container {
+	position: relative;
+	height: var(--row-height);
+	width: var(--row-height);
+	max-width: 86px;
+	max-height: 86px;
+	aspect-ratio: 1 / 1;
+}
+
+.friend-list-icon-container > .button-tooltip {
+	--tooltip-gap: 0.15em;
+}
+
+.friend-list-icon {
+	pointer-events: none;
+	position: absolute;
+	top: 0;
+	left: 0;
+	width: 100%;
+	height: 100%;
+}
+
+@media (hover: hover) {
+	@supports selector(:has(*)) {
+		.friend-list-icon-container:hover:not(:has(.button-tooltip:hover)) > .button-tooltip {
+			visibility: visible;
+		}
+	}
+
+	@supports not selector(:has(*)) {
+		.friend-list-icon-container:hover > .button-tooltip {
+			visibility: visible;
+		}
+	}
+}
+
+.friend-list-icon-small {
+	pointer-events: none;
+	height: var(--row-height);
+	width: var(--row-height);
+	max-width: 50px;
+	max-height: 50px;
+	margin-inline: 0.15em;
+	aspect-ratio: 1 / 1;
+}
+
 /* #endregion */
 
 /* #region FRIENDLIST */
@@ -124,17 +181,27 @@
 	width: calc(100% - var(--button-size));
 }
 
-.friend-list-row {
-	color: var(--text-color);
-	display: flex;
-	flex-direction: row;
-	justify-content: space-evenly;
+#friend-list-table th {
+	font-weight: normal;
 }
 
-.friend-list-row * {
+.friend-list-row {
+	color: var(--text-color);
+	min-height: var(--row-height);
+	display: flex;
+	align-items: center;
+	flex-direction: row;
+	justify-content: space-evenly;
+	padding: unset;
 	margin: var(--small-gap) 0;
 }
 
+.friend-list-row > * {
+	vertical-align: middle;
+	text-justify: center;
+	padding: unset;
+}
+
 .friend-list-row:hover {
 	color: yellow;
 }
@@ -147,10 +214,6 @@
 	white-space: preserve;
 }
 
-#friend-list .friend-list-column {
-	height: 100%;
-}
-
 #friend-list-member-number,
 .MemberNumber {
 	width: 10%;
@@ -168,8 +231,8 @@
 	gap: var(--small-gap);
 }
 
-.RelationType img {
-	height: min(5dvh, 2.5dvw);
+.RelationType {
+	user-select: none;
 }
 
 .friend-list-link {
diff --git a/BondageClub/CSS/Styles.css b/BondageClub/CSS/Styles.css
index 2a5633327e..891e1bca3c 100644
--- a/BondageClub/CSS/Styles.css
+++ b/BondageClub/CSS/Styles.css
@@ -223,41 +223,58 @@ select:invalid:not(:disabled):not(:read-only) {
 	height: 1.2em;
 }
 
-input[type="checkbox"] {
+.checkbox {
+	/* Default background color */
+	--checkbox-color: white;
+	/* Background color for disabled buttons */
+	--disabled-color: grey;
+	/* Background color for hovered and/or active buttons */
+	--hover-color: cyan;
+
 	-webkit-appearance: none;
 	appearance: none;
-	background-color: #fff;
-	margin: 0;
+	background-color: var(--checkbox-color);
+	position: relative;
 	font: inherit;
 	color: black;
 	width: min(6vh, 3vw);
 	height: min(6vh, 3vw);
-	border: min(0.3vh, 0.15vw) solid black;
-	display: grid;
-	place-content: center;
+	border: min(0.2vh, 0.1vw) solid black;
 	cursor: pointer;
 }
 
-input[type="checkbox"]:hover {
-	background-color: cyan;
+.checkbox:hover {
+	background-color: var(--hover-color);
 }
 
-input[type="checkbox"]:disabled {
-	background-color: lightgray;
+.checkbox:disabled {
+	background-color: var(--disabled-color);
 	cursor: auto;
 }
 
-input[type="checkbox"]::before {
+.checkbox::before {
 	content: "";
-	width: min(4.6vh, 2.3vw);
-	height: min(4.6vh, 2.3vw);
+	position: absolute;
+	top: 50%;
+	left: 50%;
+	transform: translate(-50%, -50%);
 	clip-path: polygon(14% 44%, 0 65%, 50% 100%, 100% 16%, 80% 0%, 43% 62%);
-	transform: scale(0);
 	background-color: black;
+	width: 90%;
+	height: 90%;
+	visibility: hidden;
 }
 
-input[type="checkbox"]:checked::before {
-	transform: scale(1);
+.checkbox:checked::before {
+	visibility: visible;
+}
+
+@supports (height: 100dvh) {
+	.checkbox {
+		width: min(6dvh, 3dvw);
+		height: min(6dvh, 3dvw);
+		border-width: min(0.2dvh, 0.1dvw);
+	}
 }
 
 /* Dropdown */
diff --git a/BondageClub/Icons/FemaleInverted.png b/BondageClub/Icons/FemaleInverted.png
new file mode 100644
index 0000000000..1d89fee7c7
Binary files /dev/null and b/BondageClub/Icons/FemaleInverted.png differ
diff --git a/BondageClub/Icons/GenderInvert.png b/BondageClub/Icons/GenderInvert.png
new file mode 100644
index 0000000000..60a4145fc9
Binary files /dev/null and b/BondageClub/Icons/GenderInvert.png differ
diff --git a/BondageClub/Icons/MaleInverted.png b/BondageClub/Icons/MaleInverted.png
new file mode 100644
index 0000000000..f32bddc6b9
Binary files /dev/null and b/BondageClub/Icons/MaleInverted.png differ
diff --git a/BondageClub/Icons/PrivateInvert.png b/BondageClub/Icons/PrivateInvert.png
new file mode 100644
index 0000000000..aea74c9ba4
Binary files /dev/null and b/BondageClub/Icons/PrivateInvert.png differ
diff --git a/BondageClub/Screens/Character/FriendList/FriendList.d.ts b/BondageClub/Screens/Character/FriendList/FriendList.d.ts
index dc71913876..f6ef4e1525 100644
--- a/BondageClub/Screens/Character/FriendList/FriendList.d.ts
+++ b/BondageClub/Screens/Character/FriendList/FriendList.d.ts
@@ -1,6 +1,6 @@
 type FriendListModes = FriendListMode[];
 type FriendListMode = "OnlineFriends" | "Beeps" | "AllFriends";
-type FriendListSortingMode = 'None' | 'MemberName' | 'MemberNickname' | 'MemberNumber' | 'ChatRoomName' | 'RelationType';
+type FriendListSortingMode = 'None' | 'MemberName' | 'MemberNickname' | 'MemberNumber' | 'ChatRoomName' | 'RelationType' | 'ChatRoomType';
 type FriendListSortingDirection = 'Asc' | 'Desc';
 
 type FriendListReturn<T extends ModuleType> = { Screen: ModuleScreens[T], Module: T, IsInChatRoom?: boolean, hasScrolledChat?: boolean };
@@ -19,6 +19,7 @@ type FriendRawRoom = {
   name?: string;
   caption: string;
   canSearchRoom: boolean;
+  types: FriendListIcon[];
 };
 
 type FriendRawBeep = {
@@ -27,3 +28,12 @@ type FriendRawBeep = {
   hasMessage?: boolean;
   canBeep?: boolean;
 };
+
+interface FriendListIcon {
+  /** The {@link HTMLImageElement.src} of the icon */
+  src: string;
+  /** The `Character/FriendList` {@link TextGet} key of the icon's tooltip */
+  tooltipKey: string;
+  /** A string to-be used for sorting the icon-containing column cells */
+  sortKey: string;
+}
diff --git a/BondageClub/Screens/Character/FriendList/FriendList.js b/BondageClub/Screens/Character/FriendList/FriendList.js
index accc1b3117..a4db0c57dd 100644
--- a/BondageClub/Screens/Character/FriendList/FriendList.js
+++ b/BondageClub/Screens/Character/FriendList/FriendList.js
@@ -90,7 +90,7 @@ function FriendListLoad() {
 						tag: 'input',
 						attributes: {
 							id: FriendListIDs.searchInput,
-							type: 'text',
+							type: 'search',
 							maxLength: 100,
 						},
 						eventListeners: {
@@ -198,71 +198,87 @@ function FriendListLoad() {
 				}
 			),
 			ElementCreate({
-				tag: 'div',
+				tag: 'table',
 				attributes: {
-					id: FriendListIDs.friendListTable
+					id: FriendListIDs.friendListTable,
+					"aria-labelledby": FriendListIDs.modeTitle,
 				},
 				children: [
 					{
-						tag: 'div',
+						tag: 'thead',
 						attributes: {
 							id: FriendListIDs.header
 						},
-						classList: ['friend-list-row'],
 						children: [
-							ElementButton.Create(
-								"friend-list-member-name",
-								() => FriendListChangeSortingMode("MemberName"),
-								{ noStyling: true },
-								{ button: {
-									classList: ['friend-list-column', 'friend-list-link'],
-								}},
-							),
-							ElementButton.Create(
-								"friend-list-member-number",
-								() => FriendListChangeSortingMode("MemberNumber"),
-								{ noStyling: true },
-								{ button: {
-									classList: ['friend-list-column', 'friend-list-link'],
-								}},
-							),
-							ElementButton.Create(
-								"friend-list-chat-room-name",
-								() => FriendListChangeSortingMode("ChatRoomName"),
-								{ noStyling: true },
-								{ button: {
-									classList: ['friend-list-column', 'friend-list-link', 'mode-specific-content', 'fl-online-friends-content', 'fl-beeps-content'],
-								}},
-							),
-							ElementButton.Create(
-								"friend-list-relation-type",
-								() => FriendListChangeSortingMode("RelationType"),
-								{ noStyling: true },
-								{ button: {
-									classList: ['friend-list-column', 'friend-list-link', 'mode-specific-content', 'fl-all-friends-content'],
-								}},
-							),
 							{
-								tag: "span",
-								classList: ['friend-list-column', 'mode-specific-content', 'fl-online-friends-content'],
+								tag: "tr",
+								classList: ["friend-list-row"],
 								children: [
-									TextGet("ActionFriends")
+									ElementButton.Create(
+										"friend-list-member-name",
+										() => FriendListChangeSortingMode("MemberName"),
+										{ noStyling: true },
+										{ button: {
+											classList: ['friend-list-column', 'friend-list-link'],
+											attributes: { role: "columnheader" },
+										}},
+									),
+									ElementButton.Create(
+										"friend-list-member-number",
+										() => FriendListChangeSortingMode("MemberNumber"),
+										{ noStyling: true },
+										{ button: {
+											classList: ['friend-list-column', 'friend-list-link'],
+											attributes: { role: "columnheader" },
+										}},
+									),
+									ElementButton.Create(
+										"friend-list-chat-room-type",
+										() => FriendListChangeSortingMode("ChatRoomType"),
+										{ noStyling: true },
+										{ button: {
+											classList: ['friend-list-column', 'friend-list-link', 'mode-specific-content', 'fl-online-friends-content', 'fl-beeps-content'],
+											attributes: { role: "columnheader" },
+										}},
+									),
+									ElementButton.Create(
+										"friend-list-chat-room-name",
+										() => FriendListChangeSortingMode("ChatRoomName"),
+										{ noStyling: true },
+										{ button: {
+											classList: ['friend-list-column', 'friend-list-link', 'mode-specific-content', 'fl-online-friends-content', 'fl-beeps-content'],
+											attributes: { role: "columnheader" },
+										}},
+									),
+									ElementButton.Create(
+										"friend-list-relation-type",
+										() => FriendListChangeSortingMode("RelationType"),
+										{ noStyling: true },
+										{ button: {
+											classList: ['friend-list-column', 'friend-list-link', 'mode-specific-content', 'fl-all-friends-content'],
+											attributes: { role: "columnheader" },
+										}},
+									),
+									{
+										tag: "th",
+										classList: ['friend-list-column', 'mode-specific-content', 'fl-online-friends-content'],
+										attributes: { scope: "col" },
+										children: [TextGet("ActionFriends")],
+									},
+									{
+										tag: "th",
+										classList: ['friend-list-column', 'mode-specific-content', 'fl-beeps-content'],
+										attributes: { scope: "col" },
+										children: [TextGet("ActionRead")],
+									},
+									{
+										tag: "th",
+										classList: ['friend-list-column', 'mode-specific-content', 'fl-all-friends-content'],
+										attributes: { scope: "col" },
+										children: [TextGet("ActionDelete")],
+									},
 								],
 							},
-							{
-								tag: "span",
-								classList: ['friend-list-column', 'mode-specific-content', 'fl-beeps-content'],
-								children: [
-									TextGet("ActionRead")
-								],
-							},
-							{
-								tag: "span",
-								classList: ['friend-list-column', 'mode-specific-content', 'fl-all-friends-content'],
-								children: [
-									TextGet("ActionDelete")
-								],
-							}
 						]
 					},
 					{
@@ -272,7 +288,7 @@ function FriendListLoad() {
 						}
 					},
 					{
-						tag: 'div',
+						tag: 'tbody',
 						classList: ["scroll-box"],
 						attributes: {
 							id: FriendListIDs.friendList
@@ -504,7 +520,7 @@ function FriendListBeepMenuSend() {
 			ChatRoomName: FriendListBeepShowRoom ? ChatRoomData?.Name : undefined,
 			ChatRoomSpace: FriendListBeepShowRoom ? ChatRoomData?.Space : undefined,
 			Sent: true,
-			Private: false,
+			Private: FriendListBeepShowRoom ? !ChatRoomData?.Visibility.includes("All") : undefined,
 			Time: new Date(),
 			Message: msg || undefined
 		});
@@ -537,6 +553,15 @@ function FriendListChatSearch(room) {
 	ElementValue("InputSearch", ChatSearchMuffle(room));
 }
 
+/** @satisfies {{ [key in (ServerChatRoomSpace | "Private")]: FriendListIcon }} */
+const FriendListIconMapping = {
+	"": { src: "./Icons/FemaleInvert.png", tooltipKey: "TypeFemale", sortKey: "F " },
+	M: { src: "./Icons/MaleInvert.png", tooltipKey: "TypeMale", sortKey: "M " },
+	X: { src: "./Icons/GenderInvert.png", tooltipKey: "TypeMixed", sortKey: "FM" },
+	Asylum: { src: "./Icons/Asylum.png", tooltipKey: "TypeAsylum", sortKey: "A " },
+	Private: { src: "./Icons/PrivateInvert.png", tooltipKey: "TypePrivate", sortKey: "P" },
+};
+
 /**
  * Loads the friend list data into the HTML div element.
  * @param {ServerFriendInfo[]} data - An array of data, we receive from the server
@@ -559,7 +584,6 @@ function FriendListLoadFriendList(data) {
 	const BeepCaption = InterfaceTextGet("Beep");
 	const DeleteCaption = InterfaceTextGet("Delete");
 	const ConfirmDeleteCaption = InterfaceTextGet("ConfirmDelete");
-	const PrivateRoomCaption = InterfaceTextGet("PrivateRoom");
 	const SentCaption = InterfaceTextGet("SentBeep");
 	const ReceivedCaption = InterfaceTextGet("ReceivedBeep");
 	const MailCaption = InterfaceTextGet("BeepWithMail");
@@ -601,15 +625,28 @@ function FriendListLoadFriendList(data) {
 	});
 	if (infoChanged) ServerPlayerRelationsSync();
 
+	/** @satisfies {Record<string, FriendListSortingMode>} */
 	const columnHeaders = {
-		"friend-list-member-name": `${TextGet("MemberName")} ${FriendListSortingMode === "MemberName" ? sortingSymbol : "↕"}`,
-		"friend-list-member-number": `${TextGet("MemberNumber")} ${FriendListSortingMode === "MemberNumber" ? sortingSymbol : "↕"}`,
-		"friend-list-chat-room-name": `${TextGet("ChatRoomName")} ${FriendListSortingMode === "ChatRoomName" ? sortingSymbol : "↕"}`,
-		"friend-list-relation-type": `${TextGet("FriendType")} ${FriendListSortingMode === "RelationType" ? sortingSymbol : "↕"}`,
+		"friend-list-member-name": "MemberName",
+		"friend-list-member-number": "MemberNumber",
+		"friend-list-chat-room-name": "ChatRoomName",
+		"friend-list-chat-room-type": "ChatRoomType",
+		"friend-list-relation-type": "RelationType",
 	};
-	CommonEntries(columnHeaders).forEach(([id, textContent]) => {
+	CommonEntries(columnHeaders).forEach(([id, modeName]) => {
 		const elem = document.getElementById(id);
-		elem.textContent = textContent;
+		const elemSortingSymbol = FriendListSortingMode === modeName ? sortingSymbol : "↕";
+		elem.textContent = `${TextGet(modeName)} ${elemSortingSymbol}`;
+		switch (elemSortingSymbol) {
+			case "↑":
+				elem.setAttribute("aria-sort", "ascending");
+				break;
+			case "↓":
+				elem.setAttribute("aria-sort", "descending");
+				break;
+			default:
+				elem.setAttribute("aria-sort", "none");
+		}
 	});
 
 	/** @type {FriendRawData[]} */
@@ -621,25 +658,20 @@ function FriendListLoadFriendList(data) {
 			const originalChatRoomName = friend.ChatRoomName || '';
 			const chatRoomSpaceCaption = InterfaceTextGet(`ChatRoomSpace${friend.ChatRoomSpace || "F"}`);
 			const chatRoomName = ChatSearchMuffle(friend.ChatRoomName?.replaceAll('<', '&lt;').replaceAll('>', '&gt;') || undefined);
-			let caption = '';
 			const canSearchRoom = FriendListReturn?.Screen === 'ChatSearch' && ChatRoomSpace === (friend.ChatRoomSpace || '');
 			const canBeep = true;
 
-			const rawCaption = [];
-			if (chatRoomSpaceCaption && chatRoomName) rawCaption.push(`<i>${chatRoomSpaceCaption}</i>`);
-			if (friend.Private) rawCaption.push(PrivateRoomCaption);
-			if (chatRoomName) rawCaption.push(chatRoomName);
-			if (rawCaption.length === 0) rawCaption.push('-');
-
-			caption = rawCaption.join(' - ');
-
 			friendRawData.push({
 				memberName: friend.MemberName,
 				memberNumber: friend.MemberNumber,
 				chatRoom: {
 					name: originalChatRoomName,
-					caption: caption,
+					caption: chatRoomName || "-",
 					canSearchRoom: canSearchRoom,
+					types: [
+						chatRoomSpaceCaption && chatRoomName ? FriendListIconMapping[friend.ChatRoomSpace ?? ""] : null,
+						friend.Private ? FriendListIconMapping.Private : null,
+					].filter(Boolean),
 				},
 				beep: {
 					canBeep: canBeep,
@@ -653,18 +685,9 @@ function FriendListLoadFriendList(data) {
 			const beepData = FriendListBeepLog[i];
 			const chatRoomSpaceCaption = InterfaceTextGet(`ChatRoomSpace${beepData.ChatRoomSpace || "F"}`);
 			const chatRoomName = ChatSearchMuffle(beepData.ChatRoomName?.replaceAll('<', '&lt;').replaceAll('>', '&gt;') || undefined);
-			let chatRoomCaption = '';
 			let beepCaption = '';
 			const canSearchRoom = FriendListReturn?.Screen === 'ChatSearch' && ChatRoomSpace === (beepData.ChatRoomSpace || '');
 
-			const rawRoomCaption = [];
-			if (chatRoomSpaceCaption && chatRoomName) rawRoomCaption.push(`<i>${chatRoomSpaceCaption}</i>`);
-			if (beepData.Private) rawRoomCaption.push(PrivateRoomCaption);
-			if (chatRoomName) rawRoomCaption.push(chatRoomName);
-			if (rawRoomCaption.length === 0) rawRoomCaption.push('-');
-
-			chatRoomCaption = rawRoomCaption.join(' - ');
-
 			const rawBeepCaption = [];
 			if (beepData.Sent) {
 				rawBeepCaption.push(SentCaption);
@@ -683,8 +706,12 @@ function FriendListLoadFriendList(data) {
 				memberNumber: beepData.MemberNumber,
 				chatRoom: {
 					name: beepData.ChatRoomName,
-					caption: chatRoomCaption,
+					caption: chatRoomName || "-",
 					canSearchRoom: canSearchRoom,
+					types: [
+						chatRoomSpaceCaption && chatRoomName ? FriendListIconMapping[beepData.ChatRoomSpace ?? ""] : null,
+						beepData.Private ? FriendListIconMapping.Private : null,
+					].filter(Boolean),
 				},
 				beep: {
 					beepIndex: i,
@@ -718,18 +745,18 @@ function FriendListLoadFriendList(data) {
 
 	friendRawData.forEach(friend => {
 		const row = ElementCreate({
-			tag: "div",
+			tag: "tr",
 			classList: ['friend-list-row'],
 			children: [
 				{
-					tag: "span",
+					tag: "td",
 					classList: ['friend-list-column', 'MemberName'],
 					children: [
 						friend.memberName
 					],
 				},
 				{
-					tag: "span",
+					tag: "td",
 					classList: ['friend-list-column', 'MemberNumber'],
 					children: [
 						friend.memberNumber.toString()
@@ -740,20 +767,96 @@ function FriendListLoadFriendList(data) {
 
 		if (friend.chatRoom) {
 			if (!friend.chatRoom.name || !friend.chatRoom.canSearchRoom) {
-				row.appendChild(ElementCreate({
-					tag: "span",
-					classList: ['friend-list-column', 'ChatRoomName'],
-					innerHTML: friend.chatRoom.caption,
-				}));
+				// Sorting is performed via each cell's `textContent`,
+				// so explicitly prepend an invisible node with some sorting key
+				let totalSortKey = "";
+				const imgContainer = ElementCreate({
+					tag: "td",
+					classList: ['friend-list-column', 'ChatRoomType'],
+					children: [
+						{ tag: "span", style: { display: "none" }, classList: ["friend-list-sorting-node"] },
+						...friend.chatRoom.types.map(({ src, tooltipKey, sortKey }) => {
+							totalSortKey += sortKey;
+							return {
+								tag: /** @type {const} */("div"),
+								classList: ["friend-list-icon-container"],
+								children: [
+									{
+										tag: /** @type {const} */("img"),
+										attributes: { src, decoding: "async", loading: "lazy", alt: TextGet(tooltipKey) },
+										classList: ["friend-list-icon"],
+									},
+									{
+										tag: /** @type {const} */("div"),
+										attributes: { role: "tooltip", "aria-hidden": "true" },
+										children: [TextGet(tooltipKey)],
+										classList: ["button-tooltip", "button-tooltip-right"],
+									},
+								],
+							};
+						}),
+					],
+				});
+				imgContainer.children[0].textContent = totalSortKey + " ";
+				if (imgContainer.children.length === 1) {
+					imgContainer.append("-");
+				}
+				row.append(
+					imgContainer,
+					ElementCreate({
+						tag: "td",
+						classList: ['friend-list-column', 'ChatRoomName'],
+						children: [friend.chatRoom.caption],
+						style: { "user-select": friend.chatRoom.caption === "-" ? "none" : undefined },
+					}),
+				);
 			} else if (friend.chatRoom.canSearchRoom) {
-				row.appendChild(ElementCreate({
-					tag: "button",
-					classList: ['friend-list-column', 'friend-list-link', 'blank-button', 'ChatRoomName'],
-					innerHTML: friend.chatRoom.caption,
-					eventListeners: {
-						click: () => FriendListChatSearch(friend.chatRoom.name),
-					},
-				}));
+				// Sorting is performed via each cell's `textContent`,
+				// so explicitly prepend an invisible node with some sorting key
+				let totalSortKey = "";
+				const imgContainer = ElementCreate({
+					tag: "td",
+					classList: ['friend-list-column', 'ChatRoomType'],
+					children: [
+						{ tag: "span", style: { display: "none" }, classList: ["friend-list-sorting-node"] },
+						...friend.chatRoom.types.map(({ src, tooltipKey, sortKey }) => {
+							totalSortKey += sortKey;
+							return {
+								tag: /** @type {const} */("div"),
+								classList: ["friend-list-icon-container"],
+								children: [
+									{
+										tag: /** @type {const} */("img"),
+										attributes: { src, decoding: "async", loading: "lazy", alt: TextGet(tooltipKey) },
+										classList: ["friend-list-icon"],
+									},
+									{
+										tag: /** @type {const} */("div"),
+										attributes: { role: "tooltip", "aria-hidden": "true" },
+										children: [TextGet(tooltipKey)],
+										classList: ["button-tooltip", "button-tooltip-right"],
+									},
+								],
+							};
+						}),
+					],
+				});
+				imgContainer.children[0].textContent = totalSortKey + " ";
+				if (imgContainer.children.length === 1) {
+					imgContainer.append("-");
+				}
+				row.append(
+					imgContainer,
+					ElementCreate({
+						tag: "td",
+						classList: ['friend-list-column', 'friend-list-link', 'blank-button', 'ChatRoomName'],
+						innerHTML: friend.chatRoom.caption,
+						style: { "user-select": friend.chatRoom.caption === "-" ? "none" : undefined },
+						eventListeners: {
+							click: () => FriendListChatSearch(friend.chatRoom.name),
+						},
+					}),
+				);
 			}
 		}
 
@@ -767,6 +870,7 @@ function FriendListLoadFriendList(data) {
 						{ button: {
 							classList: ['friend-list-column', 'friend-list-link', 'mode-specific-content', 'fl-online-friends-content'],
 							children: [friend.beep.caption],
+							attributes: { role: "cell" },
 						}},
 					),
 					ElementButton.Create(
@@ -776,12 +880,13 @@ function FriendListLoadFriendList(data) {
 						{ button: {
 							classList: ['friend-list-column', 'friend-list-link', 'mode-specific-content', 'fl-beeps-content'],
 							children: [friend.beep.caption],
+							attributes: { role: "cell" },
 						}},
 					),
 				);
 			} else {
 				row.appendChild(ElementCreate({
-					tag: "span",
+					tag: "td",
 					classList: ['friend-list-column'],
 					children: [
 						friend.beep.caption
@@ -792,14 +897,18 @@ function FriendListLoadFriendList(data) {
 
 		if (friend.relationType) {
 			row.appendChild(ElementCreate({
-				tag: "span",
+				tag: "td",
 				classList: ['friend-list-column', 'RelationType', 'mode-specific-content', 'fl-all-friends-content'],
 				children: [
 					{
-						tag: 'img',
+						tag: "img",
 						attributes: {
 							src: relationTypeIcons[friend.relationType],
-						}
+							decoding: "async",
+							loading: "lazy",
+							"aria-hidden": "true",
+						},
+						classList: ["friend-list-icon-small"],
 					},
 					FriendTypeCaption[friend.relationType]
 				],
@@ -813,7 +922,7 @@ function FriendListLoadFriendList(data) {
 			{ button: {
 				classList: ['friend-list-column', 'friend-list-link', 'mode-specific-content', 'fl-all-friends-content'],
 				children: [FriendListConfirmDelete.includes(friend.memberNumber) ? ConfirmDeleteCaption : DeleteCaption],
-				attributes: { disabled: !friend.canDelete },
+				attributes: { disabled: !friend.canDelete, role: "cell" },
 			}}
 		));
 
diff --git a/BondageClub/Screens/Character/FriendList/Text_FriendList.csv b/BondageClub/Screens/Character/FriendList/Text_FriendList.csv
index 4dbfda8561..74eb3b78ce 100644
--- a/BondageClub/Screens/Character/FriendList/Text_FriendList.csv
+++ b/BondageClub/Screens/Character/FriendList/Text_FriendList.csv
@@ -5,7 +5,8 @@ MemberName,Name
 MemberNickname,Nickname
 MemberNumber,Member number
 ChatRoomName,Chat room
-FriendType,Relation type
+ChatRoomType,Room type
+RelationType,Relation type
 ActionFriends,Send a Beep
 ActionRead,Read a Beep
 ActionDelete,Delete a Friend
@@ -21,3 +22,8 @@ TypeOwner,Owner
 TypeLover,Lover
 TypeSubmissive,Submissive
 TypeFriend,Friend
+TypeFemale,Femaly-only room
+TypeMale,Male-only room
+TypeMixed,Mixed male/female room
+TypeAsylum,Asylum room
+TypePrivate,Private room
diff --git a/BondageClub/Screens/Character/Preference/Arousal.js b/BondageClub/Screens/Character/Preference/Arousal.js
index e1281be1f7..c9773756a5 100644
--- a/BondageClub/Screens/Character/Preference/Arousal.js
+++ b/BondageClub/Screens/Character/Preference/Arousal.js
@@ -276,8 +276,11 @@ function PreferenceDecrementArousalFactor(factor) {
  * Exits the preference screen
  */
 function PreferenceSubscreenArousalExit() {
+	return true;
+}
+
+function PreferenceSubscreenArousalUnload() {
 	Player.FocusGroup = null;
 	CharacterAppearanceForceUpCharacter = -1;
 	CharacterLoadCanvas(Player);
-	return true;
 }
diff --git a/BondageClub/Screens/Character/Preference/Audio.js b/BondageClub/Screens/Character/Preference/Audio.js
index b7974f1098..9c349a4fd6 100644
--- a/BondageClub/Screens/Character/Preference/Audio.js
+++ b/BondageClub/Screens/Character/Preference/Audio.js
@@ -78,6 +78,10 @@ function PreferenceSubscreenAudioClick() {
  * Exits the preference screen.
  */
 function PreferenceSubscreenAudioExit() {
+	return true;
+}
+
+function PreferenceSubscreenAudioUnload() {
 	// If audio has been disabled for notifications, disable each individual notification audio setting
 	if (!Player.AudioSettings.Notifications) {
 		for (const setting in Player.NotificationSettings) {
@@ -85,5 +89,4 @@ function PreferenceSubscreenAudioExit() {
 			if (typeof audio === 'number' && audio > 0) Player.NotificationSettings[setting].Audio = NotificationAudioType.NONE;
 		}
 	}
-	return true;
 }
diff --git a/BondageClub/Screens/Character/Preference/CensoredWords.js b/BondageClub/Screens/Character/Preference/CensoredWords.js
index 4eaa4b0704..e03a50075d 100644
--- a/BondageClub/Screens/Character/Preference/CensoredWords.js
+++ b/BondageClub/Screens/Character/Preference/CensoredWords.js
@@ -88,7 +88,10 @@ function PreferenceSubscreenCensoredWordsClick() {
  * Exits the preference screen
  */
 function PreferenceSubscreenCensoredWordsExit() {
-	ElementRemove("InputWord");
-	Player.ChatSettings.CensoredWordsList = PreferenceCensoredWordsList.join("|");
 	return true;
 }
+
+function PreferenceSubscreenCensoredWordsUnload() {
+	ElementRemove("InputWord");
+	Player.ChatSettings.CensoredWordsList = PreferenceCensoredWordsList.join("|");
+}
diff --git a/BondageClub/Screens/Character/Preference/Controller.js b/BondageClub/Screens/Character/Preference/Controller.js
index b820313d1d..aa9d7a7483 100644
--- a/BondageClub/Screens/Character/Preference/Controller.js
+++ b/BondageClub/Screens/Character/Preference/Controller.js
@@ -82,6 +82,9 @@ function PreferenceSubscreenControllerClick() {
  * Exits the preference screen
  */
 function PreferenceSubscreenControllerExit() {
-	ControllerStopCalibration(true);
 	return true;
 }
+
+function PreferenceSubscreenControllerUnload() {
+	ControllerStopCalibration(true);
+}
diff --git a/BondageClub/Screens/Character/Preference/Extensions.js b/BondageClub/Screens/Character/Preference/Extensions.js
index caab2f4a21..1f600cb060 100644
--- a/BondageClub/Screens/Character/Preference/Extensions.js
+++ b/BondageClub/Screens/Character/Preference/Extensions.js
@@ -68,17 +68,12 @@ function PreferenceSubscreenExtensionsClick() {
 	});
 
 }
-
-function PreferenceSubscreenExtensionsUnload() {
-	PreferenceExtensionsCurrent?.unload?.();
-}
-
 function PreferenceSubscreenExtensionsExit() {
 	if (PreferenceExtensionsCurrent) {
-		if (PreferenceExtensionsCurrent.exit() ?? true) {
-			PreferenceSubscreenExtensionsClear();
-			return false;
-		}
+		const validExit = PreferenceExtensionsCurrent.exit();
+		if (validExit === false) return false;
+		PreferenceSubscreenExtensionsClear();
+		return false;
 	}
 	return true;
 }
@@ -95,3 +90,13 @@ function PreferenceSubscreenExtensionsClear() {
 	// Reload the extension settings
 	PreferenceSubscreenExtensionsLoad();
 }
+
+/**
+ * Unloads the preference subscreen for extensions
+ * Cleans up the current extension, and reset the current extension to null
+ */
+function PreferenceSubscreenExtensionsUnload() {
+	PreferenceExtensionsCurrent?.unload?.();
+	PreferenceExtensionsCurrent = null;
+}
+
diff --git a/BondageClub/Screens/Character/Preference/General.js b/BondageClub/Screens/Character/Preference/General.js
index 409e32e5cf..112b478501 100644
--- a/BondageClub/Screens/Character/Preference/General.js
+++ b/BondageClub/Screens/Character/Preference/General.js
@@ -103,8 +103,8 @@ function PreferenceSubscreenGeneralClick() {
 }
 
 /**
- * Exits the preference screen. Cleans up elements that are not needed anymore
- * If the selected color is invalid, the player cannot leave the screen.
+ * Exits the preference screen. Block exit when the color picker is active.
+ * @returns {boolean} - Returns false if the color picker is active and input is not valid
  */
 function PreferenceSubscreenGeneralExit() {
 	if (PreferenceSubscreenGeneralColorPicker) return false;
@@ -115,13 +115,23 @@ function PreferenceSubscreenGeneralExit() {
 		return false;
 	}
 
-	if (color !== Player.LabelColor) {
-		Player.LabelColor = color;
-		const elems = /** @type {HTMLElement[]} */(Array.from(document.querySelectorAll(`[style*="--label-color"][data-sender="${Player.MemberNumber}"]`)));
-		elems.forEach(e => e.style.setProperty("--label-color", color));
-	}
-
-	PreferenceMessage = "";
-	ElementRemove("InputCharacterLabelColor");
 	return true;
 }
+
+/**
+ * Cleans up elements that are not needed anymore
+ * If the selected color is invalid, the player cannot leave the screen.
+ */
+function PreferenceSubscreenGeneralUnload() {
+	const color = ElementValue("InputCharacterLabelColor");
+	if (CommonIsColor(color)) {
+		if (color !== Player.LabelColor) {
+			Player.LabelColor = color;
+			const elems = /** @type {HTMLElement[]} */(Array.from(document.querySelectorAll(`[style*="--label-color"][data-sender="${Player.MemberNumber}"]`)));
+			elems.forEach(e => e.style.setProperty("--label-color", color));
+		}
+	}
+	PreferenceMessage = "";
+	ElementRemove("InputCharacterLabelColor");
+}
+
diff --git a/BondageClub/Screens/Character/Preference/Graphics.js b/BondageClub/Screens/Character/Preference/Graphics.js
index 7ba950b7bd..0234c38363 100644
--- a/BondageClub/Screens/Character/Preference/Graphics.js
+++ b/BondageClub/Screens/Character/Preference/Graphics.js
@@ -211,6 +211,13 @@ function PreferenceSubscreenGraphicsClick() {
 }
 
 function PreferenceSubscreenGraphicsExit() {
+	return true;
+}
+
+/**
+ * Finalize graphics setting when the screen is unloaded
+ */
+function PreferenceSubscreenGraphicsUnload() {
 	// Reload WebGL if graphic settings have changed.
 	const currentOptions = GLDrawGetOptions();
 	if (
@@ -223,5 +230,4 @@ function PreferenceSubscreenGraphicsExit() {
 		GLDrawSetOptions(PreferenceGraphicsWebGLOptions);
 		GLDrawResetCanvas();
 	}
-	return true;
 }
diff --git a/BondageClub/Screens/Character/Preference/Notifications.js b/BondageClub/Screens/Character/Preference/Notifications.js
index 6a99c12b9f..a63328e8c8 100644
--- a/BondageClub/Screens/Character/Preference/Notifications.js
+++ b/BondageClub/Screens/Character/Preference/Notifications.js
@@ -185,7 +185,13 @@ function PreferenceNotificationsClickSetting(Left, Top, Setting, EventType) {
  * Exits the preference screen. Resets the test notifications.
  */
 function PreferenceSubscreenNotificationsExit() {
+	return true;
+}
 
+/**
+ * Finalize notification setting when the screen is unloaded
+ */
+function PreferenceSubscreenNotificationsUnload() {
 	// If any of the settings now have audio enabled, enable the AudioSettings setting as well
 	let enableAudio = false;
 	for (const setting in Player.NotificationSettings) {
@@ -195,5 +201,4 @@ function PreferenceSubscreenNotificationsExit() {
 	if (enableAudio) Player.AudioSettings.Notifications = true;
 
 	NotificationReset(NotificationEventType.TEST);
-	return true;
 }
diff --git a/BondageClub/Screens/Character/Preference/Preference.js b/BondageClub/Screens/Character/Preference/Preference.js
index ae375612bd..47a16e314e 100644
--- a/BondageClub/Screens/Character/Preference/Preference.js
+++ b/BondageClub/Screens/Character/Preference/Preference.js
@@ -40,6 +40,7 @@ const PreferenceSubscreens = [
 		run: () => PreferenceSubscreenGeneralRun(),
 		click: () => PreferenceSubscreenGeneralClick(),
 		exit: () => PreferenceSubscreenGeneralExit(),
+		unload: () => PreferenceSubscreenGeneralUnload(),
 	},
 	{
 		name: "Difficulty",
@@ -64,6 +65,7 @@ const PreferenceSubscreens = [
 		run: () => PreferenceSubscreenCensoredWordsRun(),
 		click: () => PreferenceSubscreenCensoredWordsClick(),
 		exit: () => PreferenceSubscreenCensoredWordsExit(),
+		unload: () => PreferenceSubscreenCensoredWordsUnload(),
 	},
 	{
 		name: "Audio",
@@ -71,6 +73,7 @@ const PreferenceSubscreens = [
 		run: () => PreferenceSubscreenAudioRun(),
 		click: () => PreferenceSubscreenAudioClick(),
 		exit: () => PreferenceSubscreenAudioExit(),
+		unload: () => PreferenceSubscreenAudioUnload(),
 	},
 	{
 		name: "Arousal",
@@ -78,6 +81,7 @@ const PreferenceSubscreens = [
 		run: () => PreferenceSubscreenArousalRun(),
 		click: () => PreferenceSubscreenArousalClick(),
 		exit: () => PreferenceSubscreenArousalExit(),
+		unload: () => PreferenceSubscreenArousalUnload(),
 	},
 	{
 		name: "Security",
@@ -85,6 +89,7 @@ const PreferenceSubscreens = [
 		run: () => PreferenceSubscreenSecurityRun(),
 		click: () => PreferenceSubscreenSecurityClick(),
 		exit: () => PreferenceSubscreenSecurityExit(),
+		unload: () => PreferenceSubscreenSecurityUnload(),
 	},
 	{
 		name: "Online",
@@ -97,6 +102,7 @@ const PreferenceSubscreens = [
 		run: () => PreferenceSubscreenVisibilityRun(),
 		click: () => PreferenceSubscreenVisibilityClick(),
 		exit: () => PreferenceSubscreenVisibilityExit(),
+		unload: () => PreferenceSubscreenVisibilityUnload(),
 	},
 	{
 		name: "Immersion",
@@ -110,12 +116,14 @@ const PreferenceSubscreens = [
 		run: () => PreferenceSubscreenGraphicsRun(),
 		click: () => PreferenceSubscreenGraphicsClick(),
 		exit: () => PreferenceSubscreenGraphicsExit(),
+		unload: () => PreferenceSubscreenGraphicsUnload(),
 	},
 	{
 		name: "Controller",
 		run: () => PreferenceSubscreenControllerRun(),
 		click: () => PreferenceSubscreenControllerClick(),
 		exit: () => PreferenceSubscreenControllerExit(),
+		unload: () => PreferenceSubscreenControllerUnload(),
 	},
 	{
 		name: "Notifications",
@@ -123,6 +131,7 @@ const PreferenceSubscreens = [
 		run: () => PreferenceSubscreenNotificationsRun(),
 		click: () => PreferenceSubscreenNotificationsClick(),
 		exit: () => PreferenceSubscreenNotificationsExit(),
+		unload: () => PreferenceSubscreenNotificationsUnload(),
 	},
 	{
 		name: "Gender",
@@ -135,6 +144,7 @@ const PreferenceSubscreens = [
 		run: () => PreferenceSubscreenScriptsRun(),
 		click: () => PreferenceSubscreenScriptsClick(),
 		exit: () => PreferenceSubscreenScriptsExit(),
+		unload: () => PreferenceSubscreenScriptsUnload(),
 	},
 	{
 		name: "Extensions",
@@ -142,6 +152,7 @@ const PreferenceSubscreens = [
 		run: () => PreferenceSubscreenExtensionsRun(),
 		click: () => PreferenceSubscreenExtensionsClick(),
 		exit: () => PreferenceSubscreenExtensionsExit(),
+		unload: () => PreferenceSubscreenExtensionsUnload(),
 	},
 ];
 
@@ -217,11 +228,14 @@ function PreferenceClick() {
  */
 function PreferenceExit() {
 	if (PreferenceSubscreen.name !== "Main") {
-		if (PreferenceSubscreenExit())
-			return;
+		// If we are in a subscreen, the only exit is to the main preference screen
+		PreferenceSubscreenExit();
+		return;
 	}
 
 	// Exit the preference menus
+	// Only a normal exit triggers an update to server. so we don't send data in unload function,
+	// which could be called from disconnects
 	const P = {
 		ArousalSettings: Player.ArousalSettings,
 		ChatSettings: Player.ChatSettings,
@@ -245,19 +259,31 @@ function PreferenceExit() {
 	CommonSetScreen("Character", "InformationSheet");
 }
 
+/**
+ * Clear all GUI data and DOM elements creates by the preference screen load function
+ * We don't do this in exit function for disconnects do not trigger the exit function
+ */
+function PreferenceUnload() {
+	if (PreferenceSubscreen.name !== "Main") {
+		PreferenceSubscreen.unload?.();
+	}
+}
+
 /**
  * Exit from a specific subscreen by running its handler and checking its validity
  */
 function PreferenceSubscreenExit() {
-	let valid = true;
-	if (PreferenceSubscreen.exit)
-		valid = PreferenceSubscreen.exit();
+	const validExit = PreferenceSubscreen.exit?.();
 
-	if (!valid) return valid;
+	// Only when the results is false (not undefined)
+	// The exit is just a exit of the subscreen's substate, return to block more exit.
+	if(validExit === false) return;
+
+	// The exit is a full exit of the subscreen, unload resources
+	PreferenceSubscreen.unload?.();
 
 	PreferenceMessage = "";
 	PreferenceSubscreen = PreferenceSubscreens.find(s => s.name === "Main");
-	return valid;
 }
 
 /**
diff --git a/BondageClub/Screens/Character/Preference/Scripts.js b/BondageClub/Screens/Character/Preference/Scripts.js
index d555333efb..ea98fd97bb 100644
--- a/BondageClub/Screens/Character/Preference/Scripts.js
+++ b/BondageClub/Screens/Character/Preference/Scripts.js
@@ -190,6 +190,10 @@ function PreferenceSubscreenScriptsClick() {
 }
 
 function PreferenceSubscreenScriptsExit() {
+	return true;
+}
+
+function PreferenceSubscreenScriptsUnload() {
 	if (PreferenceScriptTimeoutHandle != null) {
 		clearTimeout(PreferenceScriptTimeoutHandle);
 		PreferenceScriptTimeoutHandle = null;
@@ -215,7 +219,6 @@ function PreferenceSubscreenScriptsExit() {
 			}
 		}
 	}
-	return true;
 }
 
 function PreferenceSubscreenScriptsWarningClick() {
diff --git a/BondageClub/Screens/Character/Preference/Security.js b/BondageClub/Screens/Character/Preference/Security.js
index 4adde3c055..19850bfde7 100644
--- a/BondageClub/Screens/Character/Preference/Security.js
+++ b/BondageClub/Screens/Character/Preference/Security.js
@@ -54,7 +54,10 @@ function PreferenceSubscreenSecurityClick() {
  * Exits the preference screen
  */
 function PreferenceSubscreenSecurityExit() {
-	ElementRemove("InputEmailOld");
-	ElementRemove("InputEmailNew");
 	return true;
 }
+
+function PreferenceSubscreenSecurityUnload() {
+	ElementRemove("InputEmailOld");
+	ElementRemove("InputEmailNew");
+}
diff --git a/BondageClub/Screens/Character/Preference/Visibility.js b/BondageClub/Screens/Character/Preference/Visibility.js
index 0be66f37e7..9beb081970 100644
--- a/BondageClub/Screens/Character/Preference/Visibility.js
+++ b/BondageClub/Screens/Character/Preference/Visibility.js
@@ -233,9 +233,13 @@ function PreferenceVisibilityCheckboxChanged(permissionRecord, CheckSetting, Typ
  * Exits the preference screen
  */
 function PreferenceSubscreenVisibilityExit() {
+	return true;
+}
+
+
+function PreferenceSubscreenVisibilityUnload() {
 	PreferenceVisibilityGroupList = [];
 	PreferenceVisibilityRecord = {};
-	return true;
 }
 
 /**
diff --git a/BondageClub/Screens/Inventory/ItemHandheld/Kyosensu/kyosensuScreenbase.png b/BondageClub/Screens/Inventory/ItemHandheld/Kyosensu/kyosensuScreenbase.png
deleted file mode 100644
index 1078a7ffc7..0000000000
Binary files a/BondageClub/Screens/Inventory/ItemHandheld/Kyosensu/kyosensuScreenbase.png and /dev/null differ
diff --git a/BondageClub/Screens/Inventory/ItemHandheld/Uchiwa/UchiwaScreenbase.png b/BondageClub/Screens/Inventory/ItemHandheld/Uchiwa/UchiwaScreenbase.png
deleted file mode 100644
index ae84f33f81..0000000000
Binary files a/BondageClub/Screens/Inventory/ItemHandheld/Uchiwa/UchiwaScreenbase.png and /dev/null differ
diff --git a/BondageClub/Screens/Room/Crafting/Crafting.js b/BondageClub/Screens/Room/Crafting/Crafting.js
index 0062237f4e..6d6b7d9e56 100644
--- a/BondageClub/Screens/Room/Crafting/Crafting.js
+++ b/BondageClub/Screens/Room/Crafting/Crafting.js
@@ -1159,11 +1159,7 @@ function CraftingLoad() {
 					attributes: { id: CraftingID.privateLabel },
 					classList: ["crafting-label"],
 					children: [
-						{
-							tag: "input",
-							attributes: { id: CraftingID.privateCheckbox, type: "checkbox" },
-							eventListeners: { click: CraftingEventListeners._ClickPrivate },
-						},
+						ElementCheckbox.Create(CraftingID.privateCheckbox, CraftingEventListeners._ClickPrivate),
 						{ tag: "span", children: [TextGet("EnterPrivate")] },
 					],
 				},
@@ -1190,11 +1186,7 @@ function CraftingLoad() {
 					attributes: { id: CraftingID.asciidescriptionLabel },
 					classList: ["crafting-label"],
 					children: [
-						{
-							tag: "input",
-							attributes: { id: CraftingID.asciiDescriptionCheckbox, type: "checkbox" },
-							eventListeners: { click: CraftingEventListeners._ClickAsciiDescription },
-						},
+						ElementCheckbox.Create(CraftingID.asciiDescriptionCheckbox, CraftingEventListeners._ClickAsciiDescription),
 						{ tag: "span", children: [TextGet("EnterExtendedDescription")] },
 					],
 				},
diff --git a/BondageClub/Scripts/Activity.js b/BondageClub/Scripts/Activity.js
index 6b352d915d..bda6defd73 100644
--- a/BondageClub/Scripts/Activity.js
+++ b/BondageClub/Scripts/Activity.js
@@ -258,7 +258,7 @@ function ActivityCheckPrerequisites(activity, acting, acted, group) {
  * @param {ItemActivity[]} allowed
  * @param {Character} acting
  * @param {Character} acted
- * @param {string} needsItem
+ * @param {ActivityNameItem} needsItem
  * @param {Activity} activity
  * @param {AssetGroup} targetGroup
  */
@@ -342,7 +342,8 @@ function ActivityAllowedForGroup(character, groupname) {
 		let needsItem = activity.Prerequisite.find(p => p.startsWith("Needs-"));
 
 		if (needsItem) {
-			handled = ActivityGenerateItemActivitiesFromNeed(allowed, Player, character, needsItem.substring(6), activity, group);
+			const needsItemActivity = /** @type {ActivityNameItem} */(needsItem.substring(6));
+			handled = ActivityGenerateItemActivitiesFromNeed(allowed, Player, character, needsItemActivity, activity, group);
 		}
 
 		if (activity.Name === "ShockItem" && InventoryItemHasEffect(targetedItem, "ReceiveShock")) {
diff --git a/BondageClub/Scripts/Character.js b/BondageClub/Scripts/Character.js
index 3167d87298..31ae89cfac 100644
--- a/BondageClub/Scripts/Character.js
+++ b/BondageClub/Scripts/Character.js
@@ -1930,7 +1930,7 @@ function CharacterHasItemWithAttribute(C, Attribute) {
 /**
  * Checks if the character is wearing an item that allows for a specific activity
  * @param {Character} C - The character to test for
- * @param {string} Activity - The name of the activity that must be allowed
+ * @param {ActivityName} Activity - The name of the activity that must be allowed
  * @returns {Item[]} - A list of items allowing that activity
  */
 function CharacterItemsForActivity(C, Activity) {
@@ -1942,7 +1942,7 @@ function CharacterItemsForActivity(C, Activity) {
 /**
  * Checks if the character is wearing an item that allows for a specific activity
  * @param {Character} C - The character to test for
- * @param {String} Activity - The name of the activity that must be allowed
+ * @param {ActivityName} Activity - The name of the activity that must be allowed
  * @returns {boolean} - TRUE if at least one item allows that activity
  */
 function CharacterHasItemForActivity(C, Activity) {
diff --git a/BondageClub/Scripts/Dialog.js b/BondageClub/Scripts/Dialog.js
index 9ddf5e4003..6b3cbb3eef 100644
--- a/BondageClub/Scripts/Dialog.js
+++ b/BondageClub/Scripts/Dialog.js
@@ -2731,6 +2731,23 @@ function DialogDrawItemMenu(C) {
 }
 
 /**
+ * Abstract base class for a simplistic DOM subscreen with three-ish components:
+ * - A menubar with a set of buttons which are generally heterogeneous in function (_e.g._ perform arbitrary, unrelated task #1, #2 or #3)
+ * - A status message of some sort
+ * - A grid with some type of misc element, generally a set of buttons homogeneous in function (e.g. equip item #1, #2 or #3). See below for more details.
+ *
+ * Grid button clicks
+ * ------------------
+ * Grid button clicks in the {@link ids|ids.grid}-referenced element generally involve the following four steps:
+ * 1) The click listener (see {@link eventListeners|eventListeners._ClickButton}) performs some basic generic validation, like checking whether the character has been initialized.
+ *    A validation failure is considered an internal error, and will lead to a premature termination of the click event.
+ * 2) The click listener retrieves some type of underlying object associated with the grid button, like an item or activity (see {@link _GetClickedObject}).
+ * 3) The click listener performs more extensive, subscreen-/class-specific validation (see {@link GetClickStatus}), like checking whether an item has not been blacklisted.
+ *    A validation failure here will trigger a soft reload, updating the status message and re-evaluating the enabled/disabled state of _all_ pre-existing grid buttons.
+ * 4) The click listener finally performs a subscreen-/class-specific action based on the grid button click, like equipping an item (see {@link _ClickButton}).
+ *
+ * Parameters
+ * ----------
  * @abstract
  * @template {string} [ModeType=string] - The name of the mode associated with this instance (_e.g._ {@link DialogMenuMode})
  * @template [ClickedObj=any] - The underlying item or activity object of the clicked grid buttons (if applicable)
diff --git a/BondageClub/Scripts/Drawing.js b/BondageClub/Scripts/Drawing.js
index dd8e7f5ddd..fb155a8305 100644
--- a/BondageClub/Scripts/Drawing.js
+++ b/BondageClub/Scripts/Drawing.js
@@ -338,7 +338,7 @@ function DrawCharacter(C, X, Y, Zoom, IsHeightResizeAllowed, DrawCanvas) {
 		// If we must dark the Canvas characters
 		if (!C.IsPlayer() && !OverrideDark && (Player.IsBlind() || Player.HasTints())) {
 			TempCanvas.canvas.width = CanvasDrawWidth;
-		    TempCanvas.canvas.height = CanvasDrawHeight;
+			TempCanvas.canvas.height = CanvasDrawHeight;
 
 			TempCanvas.globalCompositeOperation = "copy";
 			TempCanvas.drawImage(Canvas, 0, 0);
@@ -352,7 +352,7 @@ function DrawCharacter(C, X, Y, Zoom, IsHeightResizeAllowed, DrawCanvas) {
 			}
 
 			const Tints = Player.GetTints();
-			for (const {r, g, b, a} of Tints) {
+			for (const { r, g, b, a } of Tints) {
 				TempCanvas.fillStyle = `rgba(${r},${g},${b},${a})`;
 				TempCanvas.fillRect(0, 0, Canvas.width, Canvas.height);
 			}
@@ -376,7 +376,8 @@ function DrawCharacter(C, X, Y, Zoom, IsHeightResizeAllowed, DrawCanvas) {
 
 		// Apply blur filter if needed
 		const BlurLevel = Player.GetBlurLevel();
-		if (!C.IsPlayer() && !OverrideDark && BlurLevel > 0) {
+		const needsBlur = !C.IsPlayer() && !OverrideDark && BlurLevel > 0;
+		if (needsBlur) {
 			MainCanvas.filter = `blur(${BlurLevel}px)`;
 		}
 		// Draw the character
@@ -387,7 +388,10 @@ function DrawCharacter(C, X, Y, Zoom, IsHeightResizeAllowed, DrawCanvas) {
 			Invert: IsInverted,
 			Mirror: IsInverted
 		});
-		MainCanvas.filter = 'none';
+		// Resetting the filter is expensive, even if there's no change. Only change the filter if we need to.
+		if (needsBlur) {
+			MainCanvas.filter = 'none';
+		}
 
 		// Draw the arousal meter & game images on certain conditions
 		if (CurrentScreen != "ChatRoom" || ChatRoomHideIconState <= 1) {
@@ -683,24 +687,21 @@ function DrawImageEx(
 	}
 
 	// Blit the transformed image to the main canvas, applying opacity and zoom
-	Canvas.save();
+
+	// Instead of expensive save/restore, we store the relevant state info and restore it manually
+	let savedCompositeOperation = Canvas.globalCompositeOperation;
+	let savedAlpha = Canvas.globalAlpha;
 
 	Canvas.globalCompositeOperation = BlendingMode;
 	Canvas.globalAlpha = Alpha;
 
-	Canvas.translate(X, Y);
+	// Performance benefits from combining transforms is usually minimal to none but in cases with multiple transforms it adds up
+	const scaleHoriz = Zoom * (Mirror ? -1 : 1);   // Scaling and horizontal mirroring
+	const scaleVert = Zoom * (Invert ? -1 : 1);    // Scaling and vertical inversion
+	const translateX = X + (Mirror ? Width : 0);    // Translation in x
+	const translateY = Y + (Invert ? Height : 0);   // Translation in y
 
-	if (Zoom != 1) {
-		Canvas.scale(Zoom, Zoom);
-	}
-
-	if (Invert) {
-		Canvas.transform(1, 0, 0, -1, 0, Height);
-	}
-
-	if (Mirror) {
-		Canvas.transform(-1, 0, 0, 1, Width, 0);
-	}
+	Canvas.transform(scaleHoriz, 0, 0, scaleVert, translateX, translateY);
 
 	if (SourcePos) {
 		Canvas.drawImage(destCanvas, SourcePos[0], SourcePos[1], SourcePos[2], SourcePos[3], 0, 0, Width, Height);
@@ -710,7 +711,9 @@ function DrawImageEx(
 		Canvas.drawImage(destCanvas, 0, 0);
 	}
 
-	Canvas.restore();
+	Canvas.globalCompositeOperation = savedCompositeOperation;
+	Canvas.globalAlpha = savedAlpha;
+	Canvas.resetTransform();
 	return true;
 }
 
@@ -799,7 +802,7 @@ function GetWrapTextSize(Text, Width, MaxLine) {
  * @param {"Center" | "Top"} Alignment - How the text should be alligned w.r.t. the Y position when wrapped over multiple lines
  * @returns {void} - Nothing
  */
-function DrawTextWrap(Text, X, Y, Width, Height, ForeColor, BackColor, MaxLine, LineSpacing=23, Alignment="Center") {
+function DrawTextWrap(Text, X, Y, Width, Height, ForeColor, BackColor, MaxLine, LineSpacing = 23, Alignment = "Center") {
 	ControllerAddActiveArea(X, Y);
 
 	// Draw the rectangle if we need too
@@ -912,7 +915,7 @@ function DrawTextFit(Text, X, Y, Width, Color, BackColor) {
 const DrawingGetTextSize = CommonMemoize(
 	/** @type {(Text: string, width: number) => [text: string, size: number]} */
 	(Text, Width) => {
-	// If it doesn't fit, test with smaller and smaller fonts until it fits
+		// If it doesn't fit, test with smaller and smaller fonts until it fits
 		let S;
 		for (S = 36; S >= 10; S = S - 2) {
 			MainCanvas.font = CommonGetFont(S.toString());
@@ -1277,7 +1280,10 @@ function DrawRoomBackground(URL, bounds, opts) {
 			DrawRect(...destRect, ChatRoomCustomFilter);
 		}
 
-		MainCanvas.filter = 'none';
+		// Resetting the filter is expensive, even if there's no change. Only change the filter if we need to.
+		if (blur > 0) {
+			MainCanvas.filter = 'none';
+		}
 
 		// Draw an overlay if the character is partially blinded
 		if (darken < 1) {
@@ -1288,7 +1294,7 @@ function DrawRoomBackground(URL, bounds, opts) {
 		DrawRect(...RectGetFrame(bounds), "#000");
 	}
 
-	for (const {r, g, b, a} of tints) {
+	for (const { r, g, b, a } of tints) {
 		DrawRect(bounds.x, bounds.y, bounds.w, bounds.h, `rgba(${r},${g},${b},${a})`);
 	}
 }
@@ -1325,7 +1331,7 @@ function DrawFlashScreen(Color, Duration, Intensity) {
  * @returns {string} - alpha of screen flash
  */
 function DrawGetScreenFlashAlpha(FlashTime) {
-	let alpha = Math.max(0, Math.min(255, Math.floor(DrawScreenFlashStrength * (1 - Math.exp(-FlashTime/2500))))).toString(16);
+	let alpha = Math.max(0, Math.min(255, Math.floor(DrawScreenFlashStrength * (1 - Math.exp(-FlashTime / 2500))))).toString(16);
 	if (alpha.length < 2) alpha = "0" + alpha;
 	return alpha;
 }
@@ -1457,7 +1463,7 @@ function DrawProcessScreenFlash() {
 	if (BlindFlash == true && CurrentTime < DrawingBlindFlashTimer) {
 		if (Player.GetBlindLevel() == 0) {
 			let FlashTime = DrawingBlindFlashTimer - CurrentTime;
-			DrawRect(0, 0, 2000, 1000, "#ffffff" + DrawGetScreenFlashAlpha(FlashTime/Math.max(1, 4 - DrawLastDarkFactor)));
+			DrawRect(0, 0, 2000, 1000, "#ffffff" + DrawGetScreenFlashAlpha(FlashTime / Math.max(1, 4 - DrawLastDarkFactor)));
 		}
 	}
 
@@ -1527,7 +1533,7 @@ function DrawAssetPreview(X, Y, A, Options) {
 
 	if (Description == null) Description = C ? A.DynamicDescription(C) : A.Description;
 
-	DrawPreviewBox(X, Y, Path, Description, { Background, Foreground, Vibrating, Border, Hover, HoverBackground, Disabled, Icons, Width, Height});
+	DrawPreviewBox(X, Y, Path, Description, { Background, Foreground, Vibrating, Border, Hover, HoverBackground, Disabled, Icons, Width, Height });
 }
 
 /** The default width of item previews */
@@ -1679,7 +1685,7 @@ function DrawCharacterSegment(C, Left, Top, Width, Height) {
  * @param {number} [y] - The y-position on the target canvas that the final image should be drawn at
  */
 function DrawImageTrapezify(image, targetCanvas, topToBottomRatio, x = 0, y = 0) {
-	const {width, height} = image;
+	const { width, height } = image;
 	let xStartTop = 0;
 	let xStartBottom = 0;
 
diff --git a/BondageClub/Scripts/Element.js b/BondageClub/Scripts/Element.js
index 4a06362f66..dc7379e24d 100644
--- a/BondageClub/Scripts/Element.js
+++ b/BondageClub/Scripts/Element.js
@@ -686,12 +686,7 @@ var ElementCheckboxDropdown = {
 			classList: ["dropdown-checkbox-grid"],
 			attributes: { id: `${idPrefix}-pair-${idSuffix}` },
 			children: [
-				{
-					tag: "input",
-					classList: ["dropdown-checkbox"],
-					attributes: { id: `${idPrefix}-checkbox-${idSuffix}`, type: "checkbox", checked },
-					eventListeners: { click: listener },
-				},
+				ElementCheckbox.Create(`${idPrefix}-checkbox-${idSuffix}`, listener, { checked }),
 				{
 					tag: "span",
 					classList: ["dropdown-checkbox-label"],
@@ -807,6 +802,12 @@ function ElementCreateSearchInput(id, dataCallback, options=null) {
  * @namespace
  */
 var ElementButton = {
+	/**
+	 * A unique element ID-suffix to-be assigned to buttons without an explicit ID.
+	 * @private
+	 */
+	_idCounter: 0,
+
 	/**
 	 * @private
 	 * @readonly
@@ -1217,6 +1218,7 @@ var ElementButton = {
 	 * @returns {HTMLButtonElement} - The created button
 	 */
 	Create: function Create(id, onClick, options=null, htmlOptions=null) {
+		id ??= `button-${ElementButton._idCounter++}`;
 		let elem = /** @type {HTMLButtonElement | null} */(document.getElementById(id));
 		if (elem) {
 			console.error(`Element "${id}" already exists`);
@@ -1227,7 +1229,6 @@ var ElementButton = {
 		const buttonOptions = htmlOptions.button ?? {};
 		const tooltipOptions = htmlOptions.tooltip ?? {};
 
-		id ??= `button-${Date.now()}`;
 		options ??= {};
 		const image = this._ParseImage(id, options.image, htmlOptions.img);
 		const label = this._ParseLabel(id, options.label, options.labelPosition, htmlOptions.label);
@@ -1798,6 +1799,53 @@ var ElementMenu = {
 	},
 };
 
+/**
+ * Namespace for creating DOM checkboxes.
+ */
+var ElementCheckbox = {
+	/**
+	 * A unique element ID-suffix to-be assigned to checkboxes without an explicit ID.
+	 * @private
+	 */
+	_idCounter: 0,
+
+	/**
+	 * Construct an return a DOM checkbox element (`<input type="checkbox">`)
+	 * @param {null | string} id - The ID of the element, or `null` if one must be assigned automatically
+	 * @param {(this: HTMLInputElement, ev: Event) => any} onChange - The change event listener to-be fired upon checkbox clicks
+	 * @param {null | ElementCheckbox.Options} options - High level options for the to-be created checkbox
+	 * @param {null | Partial<Record<"checkbox", Omit<HTMLOptions<any>, "tag">>>} htmlOptions - Additional {@link ElementCreate} options to-be applied to the respective (child) element
+	 */
+	Create: function Create(id, onChange, options=null, htmlOptions=null) {
+		id ??= `checkbox-${ElementCheckbox._idCounter++}`;
+		const checkbox = document.getElementById(id);
+		if (checkbox) {
+			console.error(`Element "${id}" already exists`);
+			return checkbox;
+		}
+
+		options ??= {};
+		const checkboxOptions = htmlOptions?.checkbox ?? {};
+		return ElementCreate({
+			...checkboxOptions,
+			tag: "input",
+			attributes: {
+				id,
+				type: "checkbox",
+				disabled: options.disabled,
+				checked: options.checked,
+				value: options.value,
+				...(checkboxOptions.attributes ?? {}),
+			},
+			classList: ["checkbox", ...(checkboxOptions.classList ?? [])],
+			eventListeners: {
+				change: onChange,
+				...(checkboxOptions.eventListeners ?? {}),
+			},
+		});
+	},
+};
+
 /**
  * Return whether an element is visible or not.
  *
diff --git a/BondageClub/Scripts/Typedef.d.ts b/BondageClub/Scripts/Typedef.d.ts
index 28d64d6ef9..311a12d784 100644
--- a/BondageClub/Scripts/Typedef.d.ts
+++ b/BondageClub/Scripts/Typedef.d.ts
@@ -179,6 +179,23 @@ declare namespace ElementButton {
 	}
 }
 
+declare namespace ElementCheckbox {
+
+	/** Various options that can be passed along to {@link ElementCheckbox.Create} */
+	interface Options {
+		/** Whether the checkbox is checked by default or not */
+		checked?: boolean;
+		/** Whether the checkbox should be disabled or not */
+		disabled?: boolean;
+		/**
+		 * The {@link HTMLInputElement.value}/{@link HTMLInputElement.valueAsNumber} associated with the checkbox when checked.
+		 *
+		 * Defaults to `"on"` if not specified.
+		 */
+		value?: string | number;
+	}
+}
+
 type Rect = { x: number, y: number, w: number, h: number };
 
 /** A 4-tuple with X & Y coordinates, width and height */
@@ -568,6 +585,7 @@ interface PreferenceSubscreen {
 	run: () => void;
 	click: () => void;
 	exit?: () => boolean;
+	unload?: () => void;
 }
 
 interface PreferenceGenderSetting {
@@ -883,7 +901,7 @@ interface IFriendListBeepLogMessage {
 	MemberName: string;
 	ChatRoomName?: string;
 	Private: boolean;
-	ChatRoomSpace?: string;
+	ChatRoomSpace?: ServerChatRoomSpace;
 	Sent: boolean;
 	Time: Date;
 	Message?: string;
@@ -1056,6 +1074,11 @@ interface AssetLayer {
 	readonly GroupAlpha?: readonly Alpha.Data[];
 	/** @deprecated - Superceded by {@link CreateLayerTypes} */
 	readonly ModuleType?: never;
+	/**
+	 * A list of {@link TypeRecord} keys for which a single layer expects multiple type-specific .png files.
+	 *
+	 * By default files are expected for _all_ option indices associated with the key(s), unless the valid option set has been narrowed down according to {@link AssetLayer.AllowTypes}.
+	 */
 	readonly CreateLayerTypes: readonly string[];
 	/* Specifies that this layer should not be drawn if the character is wearing any item with the given attributes */
 	readonly HideForAttribute: readonly AssetAttribute[] | null;
@@ -1213,10 +1236,7 @@ interface Asset {
 	readonly DynamicScriptDraw: boolean;
 	/** @deprecated - superceded by {@link CreateLayerTypes} */
 	readonly HasType?: never;
-	/**
-	 * A module for which the layer can have types.
-	 * Allows one to define different module-specific assets for a single layer.
-	 */
+	/** A list of {@link TypeRecord} keys for which a single layer expects multiple type-specific .png files. */
 	readonly CreateLayerTypes: readonly string[];
 	/** A record that maps {@link ExtendedItemData.name} to a set with all option indices that support locks */
 	readonly AllowLockType: null | Record<string, Set<number>>;
@@ -2810,7 +2830,7 @@ interface AssetDefinitionProperties {
 	 * A list of allowed activities
 	 * @see {@link Asset.AllowActivity}
 	 */
-	AllowActivity?: string[];
+	AllowActivity?: ActivityName[];
 	/**
 	 * A list of groups allowed activities
 	 * @see {@link Asset.AllowActivityOn}
@@ -2917,7 +2937,7 @@ interface AssetDefinitionProperties {
 type PartialType = `${string}${number}`;
 
 /**
- * A record mapping screen names to option indices.
+ * A record mapping extended item data/module names (see {@link ExtendedItemData.Name} and {@link ModularItemModule.Key}) to option indices.
  * @see {@link PartialType} A concatenation of a single `TypeRecord` key/value pair.
  */
 type TypeRecord = Record<string, number>;
@@ -4359,6 +4379,8 @@ interface PreferenceExtensionsSettingItem {
 	 * Called when the extension screen is about to exit.
 	 *
 	 * Happens either through a click of the exit button, or the ESC key.
+	 * This will **not** be called if a disconnect happens, so clean up should be 
+	 * done in {@link PreferenceExtensionsSettingItem.unload}.
 	 *
 	 * @returns {boolean | void} If you have some validation that needs to happen
 	 * (for example, ensuring that a textfield contains a valid color code), return false;
@@ -4367,7 +4389,7 @@ interface PreferenceExtensionsSettingItem {
 	 * either through your own means or by setting `PreferenceMessage` to a string.
 	 *
 	 * If you return true or nothing, your screen's {@link PreferenceExtensionsSettingItem.unload} handler
-	 * will be called afterward.
+	 * will be called afterward. And the setting screen for the extension will be closed.
 	 */
 	exit: () => boolean | void;
 }