diff --git a/src/nostalgia/modules/core/include/nostalgia/core/gfx.hpp b/src/nostalgia/modules/core/include/nostalgia/core/gfx.hpp
index 0d3e34e5..37974d61 100644
--- a/src/nostalgia/modules/core/include/nostalgia/core/gfx.hpp
+++ b/src/nostalgia/modules/core/include/nostalgia/core/gfx.hpp
@@ -112,12 +112,12 @@ int tileRows(Context&) noexcept;
 ox::Error loadBgPalette(
 		Context &ctx,
 		size_t palBank,
-		Palette const&palette,
+		CompactPalette const&palette,
 		size_t page = 0) noexcept;
 
 ox::Error loadSpritePalette(
 		Context &ctx,
-		Palette const&palette,
+		CompactPalette const&palette,
 		size_t page = 0) noexcept;
 
 ox::Error loadBgPalette(
diff --git a/src/nostalgia/modules/core/include/nostalgia/core/palette.hpp b/src/nostalgia/modules/core/include/nostalgia/core/palette.hpp
index 72f97859..454c8745 100644
--- a/src/nostalgia/modules/core/include/nostalgia/core/palette.hpp
+++ b/src/nostalgia/modules/core/include/nostalgia/core/palette.hpp
@@ -36,7 +36,43 @@ struct PaletteV2 {
 	ox::Vector<ox::Vector<Color16>> pages;
 };
 
-using Palette = PaletteV2;
+struct PaletteV3 {
+	static constexpr auto TypeName = "net.drinkingtea.nostalgia.core.Palette";
+	static constexpr auto TypeVersion = 3;
+	static constexpr auto Preloadable = true;
+	struct ColorInfo {
+		static constexpr auto TypeName = "net.drinkingtea.nostalgia.core.Palette.ColorInfo";
+		static constexpr auto TypeVersion = 3;
+		ox::String name;
+		constexpr ColorInfo() noexcept = default;
+		constexpr explicit ColorInfo(ox::StringView pName) noexcept:
+			name(pName) {}
+		constexpr explicit ColorInfo(ox::String &&pName) noexcept:
+			name(std::move(pName)) {}
+	};
+	ox::Vector<ColorInfo> colorInfo;
+	ox::Vector<ox::Vector<Color16>> pages;
+};
+
+using Palette = PaletteV3;
+
+
+struct CompactPaletteV1 {
+	static constexpr auto TypeName = "net.drinkingtea.nostalgia.core.Palette";
+	static constexpr auto TypeVersion = 2;
+	static constexpr auto Preloadable = true;
+	ox::Vector<ox::Vector<Color16>> pages{};
+};
+
+using CompactPalette = CompactPaletteV1;
+
+[[nodiscard]]
+constexpr bool valid(Palette const&p) noexcept {
+	auto const colors = p.colorInfo.size();
+	return ox::all_of(p.pages.begin(), p.pages.end(), [colors](auto const&page) {
+		return page.size() == colors;
+	});
+}
 
 [[nodiscard]]
 constexpr Color16 color(Palette const&pal, size_t page, size_t idx) noexcept {
@@ -47,13 +83,53 @@ constexpr Color16 color(Palette const&pal, size_t page, size_t idx) noexcept {
 }
 
 [[nodiscard]]
-constexpr Color16 color(Palette const&pal, size_t idx) noexcept {
-	auto constexpr page = 0;
-	return color(pal, page, idx);
+constexpr Color16 color(CompactPalette const&pal, size_t page, size_t idx) noexcept {
+	if (page < pal.pages.size() && idx < pal.pages[page].size()) [[likely]] {
+		return pal.pages[page][idx];
+	}
+	return 0;
 }
 
 [[nodiscard]]
-constexpr size_t colors(Palette const&pal, size_t page = 0) noexcept {
+constexpr Color16 color(Palette const&pal, size_t idx) noexcept {
+	return color(pal, 0, idx);
+}
+
+[[nodiscard]]
+constexpr Color16 color(CompactPalette const&pal, size_t idx) noexcept {
+	return color(pal, 0, idx);
+}
+
+[[nodiscard]]
+constexpr auto &colors(Palette &pal, size_t page = 0) noexcept {
+	return pal.pages[page];
+}
+
+[[nodiscard]]
+constexpr auto &colors(CompactPalette &pal, size_t page = 0) noexcept {
+	return pal.pages[page];
+}
+
+[[nodiscard]]
+constexpr auto &colors(Palette const&pal, size_t page = 0) noexcept {
+	return pal.pages[page];
+}
+
+[[nodiscard]]
+constexpr auto &colors(CompactPalette const&pal, size_t page = 0) noexcept {
+	return pal.pages[page];
+}
+
+[[nodiscard]]
+constexpr size_t colorCnt(Palette const&pal, size_t page = 0) noexcept {
+	if (page < pal.pages.size()) [[likely]] {
+		return pal.pages[page].size();
+	}
+	return 0;
+}
+
+[[nodiscard]]
+constexpr size_t colorCnt(CompactPalette const&pal, size_t page = 0) noexcept {
 	if (page < pal.pages.size()) [[likely]] {
 		return pal.pages[page].size();
 	}
@@ -69,6 +145,15 @@ constexpr size_t largestPage(Palette const&pal) noexcept {
 	return out;
 }
 
+[[nodiscard]]
+constexpr size_t largestPage(CompactPalette const&pal) noexcept {
+	size_t out{};
+	for (auto const&page : pal.pages) {
+		out = ox::max(out, page.size());
+	}
+	return out;
+}
+
 oxModelBegin(NostalgiaPalette)
 	oxModelField(colors)
 oxModelEnd()
@@ -81,4 +166,17 @@ oxModelBegin(PaletteV2)
 	oxModelField(pages)
 oxModelEnd()
 
+oxModelBegin(PaletteV3::ColorInfo)
+	oxModelField(name)
+oxModelEnd()
+
+oxModelBegin(PaletteV3)
+	oxModelField(colorInfo)
+	oxModelField(pages)
+oxModelEnd()
+
+oxModelBegin(CompactPaletteV1)
+	oxModelField(pages)
+oxModelEnd()
+
 }
diff --git a/src/nostalgia/modules/core/src/gba/gfx.cpp b/src/nostalgia/modules/core/src/gba/gfx.cpp
index 5ac60af1..fb738660 100644
--- a/src/nostalgia/modules/core/src/gba/gfx.cpp
+++ b/src/nostalgia/modules/core/src/gba/gfx.cpp
@@ -136,13 +136,13 @@ ox::Error initGfx(Context&, InitParams const&) noexcept {
 ox::Error loadBgPalette(
 		Context&,
 		size_t palBank,
-		Palette const&palette,
+		CompactPalette const&palette,
 		size_t page) noexcept {
 	if (palette.pages.empty()) {
 		return {};
 	}
 	auto const paletteMem = MEM_BG_PALETTE + palBank * 16;
-	for (auto i = 0u; i < colors(palette, page); ++i) {
+	for (auto i = 0u; i < colorCnt(palette, page); ++i) {
 		paletteMem[i] = color(palette, page, i);
 	}
 	return {};
@@ -150,13 +150,13 @@ ox::Error loadBgPalette(
 
 ox::Error loadSpritePalette(
 		Context&,
-		Palette const&palette,
+		CompactPalette const&palette,
 		size_t page) noexcept {
 	if (palette.pages.empty()) {
 		return {};
 	}
 	auto const paletteMem = MEM_SPRITE_PALETTE;
-	for (auto i = 0u; i < colors(palette, page); ++i) {
+	for (auto i = 0u; i < colorCnt(palette, page); ++i) {
 		paletteMem[i] = color(palette, page, i);
 	}
 	return {};
@@ -166,14 +166,14 @@ ox::Error loadBgPalette(
 		Context &ctx,
 		size_t palBank,
 		ox::FileAddress const&paletteAddr) noexcept {
-	oxRequire(pal, keel::readObj<Palette>(keelCtx(ctx), paletteAddr));
+	oxRequire(pal, keel::readObj<CompactPalette>(keelCtx(ctx), paletteAddr));
 	return loadBgPalette(ctx, palBank, *pal, 0);
 }
 
 ox::Error loadSpritePalette(
 		Context &ctx,
 		ox::FileAddress const&paletteAddr) noexcept {
-	oxRequire(pal, keel::readObj<Palette>(keelCtx(ctx), paletteAddr));
+	oxRequire(pal, keel::readObj<CompactPalette>(keelCtx(ctx), paletteAddr));
 	return loadSpritePalette(ctx, *pal, 0);
 }
 
diff --git a/src/nostalgia/modules/core/src/keel/keelmodule.cpp b/src/nostalgia/modules/core/src/keel/keelmodule.cpp
index 140dfa71..ff457c4c 100644
--- a/src/nostalgia/modules/core/src/keel/keelmodule.cpp
+++ b/src/nostalgia/modules/core/src/keel/keelmodule.cpp
@@ -18,6 +18,8 @@ static class: public keel::Module {
 	private:
 		NostalgiaPaletteToPaletteV1Converter m_nostalgiaPaletteToPaletteV1Converter;
 		PaletteV1ToPaletteV2Converter m_paletteV1ToPaletteV2Converter;
+		PaletteV2ToPaletteV3Converter m_paletteV2ToPaletteV3Converter;
+		PaletteToCompactPaletteConverter m_paletteToCompactPaletteConverter;
 		TileSheetV1ToTileSheetV2Converter m_tileSheetV1ToTileSheetV2Converter;
 		TileSheetV2ToTileSheetV3Converter m_tileSheetV2ToTileSheetV3Converter;
 		TileSheetV3ToTileSheetV4Converter m_tileSheetV3ToTileSheetV4Converter;
@@ -47,6 +49,8 @@ static class: public keel::Module {
 			return {
 				&m_nostalgiaPaletteToPaletteV1Converter,
 				&m_paletteV1ToPaletteV2Converter,
+				&m_paletteV2ToPaletteV3Converter,
+				&m_paletteToCompactPaletteConverter,
 				&m_tileSheetV1ToTileSheetV2Converter,
 				&m_tileSheetV2ToTileSheetV3Converter,
 				&m_tileSheetV3ToTileSheetV4Converter,
@@ -71,8 +75,10 @@ static class: public keel::Module {
 				},
 				[](keel::Context &ctx, ox::Buffer &buff, ox::StringView typeId) -> ox::Result<bool> {
 					if (typeId == ox::ModelTypeId_v<NostalgiaPalette> ||
-					    typeId == ox::ModelTypeId_v<PaletteV1>) {
-						oxReturnError(keel::convertBuffToBuff<Palette>(
+					    typeId == ox::ModelTypeId_v<PaletteV1> ||
+						typeId == ox::ModelTypeId_v<PaletteV2> ||
+						typeId == ox::ModelTypeId_v<PaletteV3>) {
+						oxReturnError(keel::convertBuffToBuff<CompactPalette>(
 								ctx, buff, ox::ClawFormat::Metal).moveTo(buff));
 						return true;
 					}
diff --git a/src/nostalgia/modules/core/src/keel/typeconv.cpp b/src/nostalgia/modules/core/src/keel/typeconv.cpp
index 7df8d0a2..61287324 100644
--- a/src/nostalgia/modules/core/src/keel/typeconv.cpp
+++ b/src/nostalgia/modules/core/src/keel/typeconv.cpp
@@ -22,6 +22,27 @@ ox::Error PaletteV1ToPaletteV2Converter::convert(
 	return {};
 }
 
+ox::Error PaletteV2ToPaletteV3Converter::convert(
+		keel::Context&,
+		PaletteV2 &src,
+		PaletteV3 &dst) const noexcept {
+	dst.pages = std::move(src.pages);
+	if (!dst.pages.empty()) {
+		for (size_t i = 0; i < dst.pages[0].size(); ++i) {
+			dst.colorInfo.emplace_back(ox::sfmt("Color {}", i));
+		}
+	}
+	return {};
+}
+
+ox::Error PaletteToCompactPaletteConverter::convert(
+		keel::Context&,
+		Palette &src,
+		CompactPalette &dst) const noexcept {
+	dst.pages = std::move(src.pages);
+	return {};
+}
+
 ox::Error TileSheetV1ToTileSheetV2Converter::convert(
 		keel::Context&,
 		TileSheetV1 &src,
diff --git a/src/nostalgia/modules/core/src/keel/typeconv.hpp b/src/nostalgia/modules/core/src/keel/typeconv.hpp
index b7b47722..30347b11 100644
--- a/src/nostalgia/modules/core/src/keel/typeconv.hpp
+++ b/src/nostalgia/modules/core/src/keel/typeconv.hpp
@@ -21,7 +21,15 @@ class NostalgiaPaletteToPaletteV1Converter: public keel::Converter<NostalgiaPale
 };
 
 class PaletteV1ToPaletteV2Converter: public keel::Converter<PaletteV1, PaletteV2> {
-	ox::Error convert(keel::Context&, PaletteV1 &src, PaletteV2 &dst) const noexcept final;
+		ox::Error convert(keel::Context&, PaletteV1 &src, PaletteV2 &dst) const noexcept final;
+};
+
+class PaletteV2ToPaletteV3Converter: public keel::Converter<PaletteV2, PaletteV3> {
+		ox::Error convert(keel::Context&, PaletteV2 &src, PaletteV3 &dst) const noexcept final;
+};
+
+class PaletteToCompactPaletteConverter: public keel::Converter<Palette, CompactPalette> {
+	ox::Error convert(keel::Context&, Palette &src, CompactPalette &dst) const noexcept final;
 };
 
 class TileSheetV1ToTileSheetV2Converter: public keel::Converter<TileSheetV1, TileSheetV2> {
diff --git a/src/nostalgia/modules/core/src/opengl/gfx.cpp b/src/nostalgia/modules/core/src/opengl/gfx.cpp
index b9e04721..19f77a4a 100644
--- a/src/nostalgia/modules/core/src/opengl/gfx.cpp
+++ b/src/nostalgia/modules/core/src/opengl/gfx.cpp
@@ -364,7 +364,7 @@ static void loadPalette(
 		ox::Array<GLfloat, 1024> &palette,
 		size_t palOffset,
 		GLuint shaderPgrm,
-		Palette const&pal,
+		CompactPalette const&pal,
 		size_t page = 0) noexcept {
 	static constexpr std::size_t ColorCnt = 256;
 	for (auto i = palOffset; auto const c : pal.pages[page]) {
@@ -516,7 +516,7 @@ static ox::Result<TileSheetData> normalizeTileSheet(
 ox::Error loadBgPalette(
 		Context &ctx,
 		size_t palBank,
-		Palette const&palette,
+		CompactPalette const&palette,
 		size_t page) noexcept {
 	renderer::loadPalette(ctx.bgPalette, palBank * 16 * 4, ctx.bgShader, palette, page);
 	return {};
@@ -524,7 +524,7 @@ ox::Error loadBgPalette(
 
 ox::Error loadSpritePalette(
 		Context &ctx,
-		Palette const&palette,
+		CompactPalette const&palette,
 		size_t page) noexcept {
 	ox::Array<GLfloat, 1024> pal;
 	renderer::loadPalette(pal, 0, ctx.spriteShader, palette, page);
@@ -536,7 +536,7 @@ ox::Error loadBgPalette(
 		size_t palBank,
 		ox::FileAddress const&paletteAddr) noexcept {
 	auto &kctx = keelCtx(ctx.turbineCtx);
-	oxRequire(palette, readObj<Palette>(kctx, paletteAddr));
+	oxRequire(palette, readObj<CompactPalette>(kctx, paletteAddr));
 	renderer::loadPalette(ctx.bgPalette, palBank * 16 * 4, ctx.bgShader, *palette);
 	return {};
 }
@@ -545,7 +545,7 @@ ox::Error loadSpritePalette(
 		Context &ctx,
 		ox::FileAddress const&paletteAddr) noexcept {
 	auto &kctx = keelCtx(ctx.turbineCtx);
-	oxRequire(palette, readObj<Palette>(kctx, paletteAddr));
+	oxRequire(palette, readObj<CompactPalette>(kctx, paletteAddr));
 	ox::Array<GLfloat, 1024> pal;
 	renderer::loadPalette(pal, 0, ctx.spriteShader, *palette);
 	return {};
diff --git a/src/nostalgia/modules/core/src/studio/paletteeditor/paletteeditor-imgui.cpp b/src/nostalgia/modules/core/src/studio/paletteeditor/paletteeditor-imgui.cpp
index c21a50d6..383cd61d 100644
--- a/src/nostalgia/modules/core/src/studio/paletteeditor/paletteeditor-imgui.cpp
+++ b/src/nostalgia/modules/core/src/studio/paletteeditor/paletteeditor-imgui.cpp
@@ -15,11 +15,16 @@
 
 namespace nostalgia::core {
 
-PaletteEditorImGui::PaletteEditorImGui(studio::StudioContext &sctx, ox::CRStringView path):
-	Editor(path),
-	m_sctx(sctx),
-	m_tctx(sctx.tctx),
-	m_pal(*keel::readObj<Palette>(keelCtx(m_tctx), ox::FileAddress(itemPath())).unwrapThrow()) {
+namespace ig = studio::ig;
+
+PaletteEditorImGui::PaletteEditorImGui(studio::StudioContext &sctx, ox::StringView const path):
+		Editor(path),
+		m_sctx(sctx),
+		m_tctx(sctx.tctx),
+		m_pal(*keel::readObj<Palette>(keelCtx(m_tctx), itemPath()).unwrapThrow()) {
+	if (!valid(m_pal)) {
+		throw OxException(1, "PaletteEditorImGui: invalid Palette object");
+	}
 	undoStack()->changeTriggered.connect(this, &PaletteEditorImGui::handleCommand);
 }
 
@@ -36,10 +41,9 @@ void PaletteEditorImGui::keyStateChanged(turbine::Key key, bool down) {
 		if (turbine::buttonDown(m_tctx, turbine::Key::Mod_Alt)) {
 			m_selectedColorRow =
 					ox::min<std::size_t>(
-					  static_cast<uint32_t>(key - turbine::Key::Num_1 + 9), m_pal.pages.size() - 1);
+						static_cast<uint32_t>(key - turbine::Key::Num_1 + 9), m_pal.pages.size() - 1);
 		}
 	}
-
 }
 
 void PaletteEditorImGui::draw(studio::StudioContext&) noexcept {
@@ -58,7 +62,7 @@ void PaletteEditorImGui::draw(studio::StudioContext&) noexcept {
 }
 
 ox::Error PaletteEditorImGui::saveItem() noexcept {
-	return m_sctx.project->writeObj(itemPath(), m_pal);
+	return m_sctx.project->writeObj(itemPath(), m_pal, ox::ClawFormat::Organic);
 }
 
 void PaletteEditorImGui::drawColumn(ox::CStringView txt) noexcept {
@@ -71,42 +75,39 @@ void PaletteEditorImGui::drawColumn(ox::CStringView txt) noexcept {
 void PaletteEditorImGui::drawColorsEditor() noexcept {
 	constexpr auto tableFlags = ImGuiTableFlags_RowBg;
 	auto const colorsSz = ImGui::GetContentRegionAvail();
-	auto const colorEditor = m_selectedColorRow < colors(m_pal, m_page);
+	auto const colorEditor = m_selectedColorRow < colorCnt(m_pal, m_page);
 	auto const colorEditorWidth = 220;
 	static constexpr auto toolbarHeight = 40;
 	{
 		auto const sz = ImVec2(70, 24);
 		if (ImGui::Button("Add", sz)) {
-			auto const colorSz = static_cast<int>(colors(m_pal, m_page));
+			auto const colorSz = colorCnt(m_pal, m_page);
 			constexpr Color16 c = 0;
-			std::ignore = undoStack()->push(ox::make_unique<AddColorCommand>(&m_pal, c, m_page, colorSz));
+			std::ignore = pushCommand<AddColorCommand>(m_pal, c, colorSz);
 		}
 		ImGui::SameLine();
-		ImGui::BeginDisabled(m_selectedColorRow >= colors(m_pal, m_page));
+		ImGui::BeginDisabled(m_selectedColorRow >= colorCnt(m_pal, m_page));
 		{
 			if (ImGui::Button("Remove", sz)) {
-				std::ignore = undoStack()->push(
-						ox::make_unique<RemoveColorCommand>(
-								&m_pal,
-								color(m_pal, m_page, static_cast<std::size_t>(m_selectedColorRow)),
-								m_page,
-								static_cast<int>(m_selectedColorRow)));
-				m_selectedColorRow = ox::min(colors(m_pal, m_page) - 1, m_selectedColorRow);
+				std::ignore = pushCommand<RemoveColorCommand>(m_pal, m_selectedColorRow);
+				m_selectedColorRow = ox::min(colorCnt(m_pal, m_page) - 1, m_selectedColorRow);
 			}
 			ImGui::SameLine();
 			ImGui::BeginDisabled(m_selectedColorRow <= 0);
 			{
 				if (ImGui::Button("Move Up", sz)) {
-					std::ignore = undoStack()->push(ox::make_unique<MoveColorCommand>(&m_pal, m_page, m_selectedColorRow, -1));
+					std::ignore = pushCommand<MoveColorCommand>(
+							m_pal, m_page, m_selectedColorRow, m_selectedColorRow - 1);
 					--m_selectedColorRow;
 				}
 			}
 			ImGui::EndDisabled();
 			ImGui::SameLine();
-			ImGui::BeginDisabled(m_selectedColorRow >= colors(m_pal, m_page) - 1);
+			ImGui::BeginDisabled(m_selectedColorRow >= colorCnt(m_pal, m_page) - 1);
 			{
 				if (ImGui::Button("Move Down", sz)) {
-					std::ignore = undoStack()->push(ox::make_unique<MoveColorCommand>(&m_pal, m_page, m_selectedColorRow, 1));
+					std::ignore = pushCommand<MoveColorCommand>(
+							m_pal, m_page, m_selectedColorRow, m_selectedColorRow + 1);
 					++m_selectedColorRow;
 				}
 			}
@@ -114,8 +115,13 @@ void PaletteEditorImGui::drawColorsEditor() noexcept {
 		}
 		ImGui::EndDisabled();
 	}
-	auto const tblWidth = (colorsSz.x - colorEditorWidth - 8) * colorEditor;
-	ImGui::BeginTable("Colors", 5, tableFlags, ImVec2(tblWidth, colorsSz.y - (toolbarHeight + 5)));
+	auto const tblWidth = (colorsSz.x - static_cast<float>(colorEditorWidth) - 8.f)
+			* static_cast<float>(colorEditor);
+	ImGui::BeginTable(
+			"Colors",
+			5,
+			tableFlags,
+			ImVec2(tblWidth, colorsSz.y - (toolbarHeight + 5)));
 	{
 		ImGui::TableSetupColumn("Idx", ImGuiTableColumnFlags_WidthFixed, 25);
 		ImGui::TableSetupColumn("Red", ImGuiTableColumnFlags_WidthFixed, 50);
@@ -123,17 +129,19 @@ void PaletteEditorImGui::drawColorsEditor() noexcept {
 		ImGui::TableSetupColumn("Blue", ImGuiTableColumnFlags_WidthFixed, 50);
 		ImGui::TableSetupColumn("Color Preview", ImGuiTableColumnFlags_NoHide);
 		ImGui::TableHeadersRow();
-		for (auto i = 0u; auto const c : m_pal.pages[m_page]) {
+		for (auto i = 0u; auto const&c : m_pal.pages[m_page]) {
 			ImGui::PushID(static_cast<int>(i));
 			ImGui::TableNextRow();
-			drawColumn(i);
+			drawColumn(i + 1);
 			drawColumn(red16(c));
 			drawColumn(green16(c));
 			drawColumn(blue16(c));
 			ImGui::TableNextColumn();
-			auto const ic = ImGui::GetColorU32(ImVec4(redf(c), greenf(c), bluef(c), 1));
+			auto const ic = ImGui::GetColorU32(
+					ImVec4(redf(c), greenf(c), bluef(c), 1));
 			ImGui::TableSetBgColor(ImGuiTableBgTarget_CellBg, ic);
-			if (ImGui::Selectable("##ColorRow", i == m_selectedColorRow, ImGuiSelectableFlags_SpanAllColumns)) {
+			if (ImGui::Selectable(
+					"##ColorRow", i == m_selectedColorRow, ImGuiSelectableFlags_SpanAllColumns)) {
 				m_selectedColorRow = i;
 			}
 			ImGui::PopID();
@@ -155,28 +163,30 @@ void PaletteEditorImGui::drawPagesEditor() noexcept {
 	constexpr auto toolbarHeight = 40;
 	auto const btnSz = ImVec2(paneSz.x / 3 - 5.5f, 24);
 	if (ImGui::Button("Add", btnSz)) {
-		std::ignore = undoStack()->push(ox::make_unique<AddPageCommand>(m_pal));
+		std::ignore = pushCommand<DuplicatePageCommand>(m_pal, 0u, m_pal.pages.size());
 		m_page = m_pal.pages.size() - 1;
 	}
 	ImGui::SameLine();
 	if (ImGui::Button("Remove", btnSz)) {
-		std::ignore = undoStack()->push(ox::make_unique<RemovePageCommand>(m_pal, m_page));
+		std::ignore = pushCommand<RemovePageCommand>(m_pal, m_page);
 		m_page = std::min(m_page, m_pal.pages.size() - 1);
 	}
 	ImGui::SameLine();
 	if (ImGui::Button("Duplicate", btnSz)) {
-		std::ignore = undoStack()->push(ox::make_unique<DuplicatePageCommand>(m_pal, m_page, m_pal.pages.size()));
+		std::ignore = pushCommand<DuplicatePageCommand>(m_pal, m_page, m_pal.pages.size());
 	}
-	ImGui::BeginTable("PageSelect", 2, tableFlags, ImVec2(paneSz.x, paneSz.y - (toolbarHeight + 5)));
+	ImGui::BeginTable(
+			"PageSelect",
+			2,
+			tableFlags,
+			ImVec2(paneSz.x, paneSz.y - (toolbarHeight + 5)));
 	{
 		ImGui::TableSetupColumn("Page", ImGuiTableColumnFlags_WidthFixed, 60);
-		ImGui::TableSetupColumn("Colors", ImGuiTableColumnFlags_NoHide);
 		ImGui::TableHeadersRow();
 		for (auto i = 0u; i < m_pal.pages.size(); ++i) {
 			ImGui::PushID(static_cast<int>(i));
 			ImGui::TableNextRow();
 			drawColumn(i + 1);
-			drawColumn(colors(m_pal, i));
 			ImGui::SameLine();
 			if (ImGui::Selectable("##PageRow", i == m_page, ImGuiSelectableFlags_SpanAllColumns)) {
 				m_page = i;
@@ -193,20 +203,33 @@ void PaletteEditorImGui::drawColorEditor() noexcept {
 	int g = green16(c);
 	int b = blue16(c);
 	int const a = alpha16(c);
+	auto const&currentName = m_pal.colorInfo[m_selectedColorRow].name;
+	ox::IString<50> name;
+	name = currentName;
+	ImGui::InputText("Name", name.data(), name.cap() + 1);
+	ImGui::Separator();
 	ImGui::InputInt("Red", &r, 1, 5);
 	ImGui::InputInt("Green", &g, 1, 5);
 	ImGui::InputInt("Blue", &b, 1, 5);
+	if (ig::PushButton("Apply to all pages", {-1, ig::BtnSz.y})) {
+		std::ignore = pushCommand<ApplyColorAllPagesCommand>(
+				m_pal, m_page, m_selectedColorRow);
+	}
+	r = ox::max(r, 0);
+	g = ox::max(g, 0);
+	b = ox::max(b, 0);
 	auto const newColor = color16(r, g, b, a);
 	if (c != newColor) {
-		std::ignore = undoStack()->push(ox::make_unique<UpdateColorCommand>(
-				&m_pal, m_page, static_cast<int>(m_selectedColorRow), c, newColor));
+		std::ignore = pushCommand<UpdateColorCommand>(m_pal, m_page, m_selectedColorRow, newColor);
+	}
+	if (currentName != name.data()) {
+		std::ignore = pushCommand<UpdateColorInfoCommand>(
+				m_pal, m_selectedColorRow, Palette::ColorInfo{name.data()});
 	}
 }
 
 ox::Error PaletteEditorImGui::handleCommand(studio::UndoCommand const*cmd) noexcept {
-	if (dynamic_cast<AddPageCommand const*>(cmd)) {
-		m_page = m_pal.pages.size() - 1;
-	} else if (dynamic_cast<RemovePageCommand const*>(cmd)) {
+	if (dynamic_cast<RemovePageCommand const*>(cmd)) {
 		m_page = ox::min(m_page, m_pal.pages.size() - 1);
 	} else if (auto const dupPageCmd = dynamic_cast<DuplicatePageCommand const*>(cmd)) {
 		m_page = ox::clamp<size_t>(dupPageCmd->insertIdx(), 0, m_pal.pages.size() - 1);
diff --git a/src/nostalgia/modules/core/src/studio/paletteeditor/paletteeditor-imgui.hpp b/src/nostalgia/modules/core/src/studio/paletteeditor/paletteeditor-imgui.hpp
index 1a828efe..649f32ae 100644
--- a/src/nostalgia/modules/core/src/studio/paletteeditor/paletteeditor-imgui.hpp
+++ b/src/nostalgia/modules/core/src/studio/paletteeditor/paletteeditor-imgui.hpp
@@ -21,7 +21,7 @@ class PaletteEditorImGui: public studio::Editor {
 		size_t m_page = 0;
 
 	public:
-		PaletteEditorImGui(studio::StudioContext &sctx, ox::CRStringView path);
+		PaletteEditorImGui(studio::StudioContext &sctx, ox::StringView path);
 
 		void keyStateChanged(turbine::Key key, bool down) override;
 
diff --git a/src/nostalgia/modules/core/src/studio/paletteeditor/paletteeditor.cpp b/src/nostalgia/modules/core/src/studio/paletteeditor/paletteeditor.cpp
index 6cc3d691..1a4acd05 100644
--- a/src/nostalgia/modules/core/src/studio/paletteeditor/paletteeditor.cpp
+++ b/src/nostalgia/modules/core/src/studio/paletteeditor/paletteeditor.cpp
@@ -6,21 +6,44 @@
 
 namespace nostalgia::core {
 
-AddPageCommand::AddPageCommand(Palette &pal) noexcept:
-	m_pal(pal) {
+ApplyColorAllPagesCommand::ApplyColorAllPagesCommand(Palette &pal, size_t const page, size_t const idx):
+	m_pal(pal),
+	m_page(page),
+	m_idx(idx),
+	m_origColors([this] {
+		ox::Vector<Color16> colors;
+		colors.reserve(m_pal.pages.size());
+		for (auto const&p : m_pal.pages) {
+			colors.emplace_back(p[m_idx]);
+		}
+		return colors;
+	}()) {
+	auto const c = color(m_pal, m_page, m_idx);
+	if (ox::all_of(m_pal.pages.begin(), m_pal.pages.end(), [this, c](ox::SpanView<Color16> const page) {
+			return page[m_idx] == c;
+		})) {
+		throw studio::NoChangesException();
+	}
 }
 
-int AddPageCommand::commandId() const noexcept {
-	return static_cast<int>(PaletteEditorCommandId::AddPage);
+int ApplyColorAllPagesCommand::commandId() const noexcept {
+	return static_cast<int>(PaletteEditorCommandId::ApplyColorAllPages);
 }
 
-ox::Error AddPageCommand::redo() noexcept {
-	m_pal.pages.emplace_back();
+ox::Error ApplyColorAllPagesCommand::redo() noexcept {
+	auto const c = color(m_pal, m_page, m_idx);
+	for (auto &page : m_pal.pages) {
+		page[m_idx] = c;
+	}
 	return {};
 }
 
-ox::Error AddPageCommand::undo() noexcept {
-	return m_pal.pages.erase(static_cast<std::size_t>(m_pal.pages.size() - 1)).error;
+ox::Error ApplyColorAllPagesCommand::undo() noexcept {
+	for (size_t p = 0u; auto &page : m_pal.pages) {
+		page[m_idx] = m_origColors[p];
+		++p;
+	}
+	return {};
 }
 
 
@@ -55,78 +78,134 @@ size_t DuplicatePageCommand::insertIdx() const noexcept {
 
 RemovePageCommand::RemovePageCommand(Palette &pal, size_t idx) noexcept:
 	m_pal(pal),
-	m_idx(idx) {
-}
+	m_idx(idx) {}
 
 int RemovePageCommand::commandId() const noexcept {
 	return static_cast<int>(PaletteEditorCommandId::RemovePage);
 }
 
 ox::Error RemovePageCommand::redo() noexcept {
-	m_page = std::move(m_pal.pages[m_idx]);
+	m_page = std::move(colors(m_pal, m_idx));
 	return m_pal.pages.erase(static_cast<std::size_t>(m_idx)).error;
 }
 
 ox::Error RemovePageCommand::undo() noexcept {
-	m_pal.pages.insert(m_idx, std::move(m_page));
+	m_pal.pages.emplace(m_idx, std::move(m_page));
 	return {};
 }
 
 
-AddColorCommand::AddColorCommand(Palette *pal, Color16 color, size_t page, int idx) noexcept {
-	m_pal = pal;
-	m_color = color;
-	m_page = page;
-	m_idx = idx;
-}
+AddColorCommand::AddColorCommand(Palette &pal, Color16 const color, size_t const idx) noexcept:
+	m_pal(pal),
+	m_color(color),
+	m_idx(idx) {}
 
 int AddColorCommand::commandId() const noexcept {
 	return static_cast<int>(PaletteEditorCommandId::AddColor);
 }
 
 ox::Error AddColorCommand::redo() noexcept {
-	m_pal->pages[m_page].insert(static_cast<std::size_t>(m_idx), m_color);
+	for (auto &page : m_pal.pages) {
+		page.emplace(static_cast<size_t>(m_idx), m_color);
+	}
 	return {};
 }
 
 ox::Error AddColorCommand::undo() noexcept {
-	return m_pal->pages[m_page].erase(static_cast<std::size_t>(m_idx)).error;
+	for (auto &page : m_pal.pages) {
+		oxReturnError(page.erase(static_cast<std::size_t>(m_idx)));
+	}
+	return {};
 }
 
 
-RemoveColorCommand::RemoveColorCommand(Palette *pal, Color16 color, size_t page, int idx) noexcept {
-	m_pal = pal;
-	m_color = color;
-	m_page = page;
-	m_idx = idx;
-}
+RemoveColorCommand::RemoveColorCommand(Palette &pal, size_t const idx) noexcept:
+	m_pal(pal),
+	m_idx(idx),
+	m_colors([this] {
+		ox::Vector<Color16> colors;
+		colors.reserve(m_pal.pages.size());
+		for (auto const&p : m_pal.pages) {
+			colors.emplace_back(p[m_idx]);
+		}
+		return colors;
+	}()) {}
 
 int RemoveColorCommand::commandId() const noexcept {
 	return static_cast<int>(PaletteEditorCommandId::RemoveColor);
 }
 
 ox::Error RemoveColorCommand::redo() noexcept {
-	return m_pal->pages[m_page].erase(static_cast<std::size_t>(m_idx)).error;
+	for (auto &page : m_pal.pages) {
+		oxReturnError(page.erase(m_idx));
+	}
+	return {};
 }
 
 ox::Error RemoveColorCommand::undo() noexcept {
-	m_pal->pages[m_page].insert(static_cast<std::size_t>(m_idx), m_color);
+	for (size_t p = 0; auto &page : m_pal.pages) {
+		page.emplace(m_idx, m_colors[p]);
+		++p;
+	}
 	return {};
 }
 
 
+UpdateColorInfoCommand::UpdateColorInfoCommand(
+		Palette &pal,
+		size_t idx,
+		Palette::ColorInfo newColorInfo):
+		m_pal(pal),
+		m_idx(idx),
+		m_altColorInfo(std::move(newColorInfo)) {
+	if (m_pal.colorInfo[m_idx].name == m_altColorInfo.name) {
+		throw studio::NoChangesException();
+	}
+}
+
+bool UpdateColorInfoCommand::mergeWith(UndoCommand const&cmd) noexcept {
+	if (cmd.commandId() != static_cast<int>(PaletteEditorCommandId::UpdateColor)) {
+		return false;
+	}
+	auto ucCmd = static_cast<UpdateColorInfoCommand const*>(&cmd);
+	if (m_idx != ucCmd->m_idx) {
+		return false;
+	}
+	return true;
+}
+
+[[nodiscard]]
+int UpdateColorInfoCommand::commandId() const noexcept {
+	return static_cast<int>(PaletteEditorCommandId::UpdateColor);
+}
+
+ox::Error UpdateColorInfoCommand::redo() noexcept {
+	swap();
+	return {};
+}
+
+ox::Error UpdateColorInfoCommand::undo() noexcept {
+	swap();
+	return {};
+}
+
+void UpdateColorInfoCommand::swap() noexcept {
+	std::swap(m_pal.colorInfo[m_idx], m_altColorInfo);
+}
+
+
 UpdateColorCommand::UpdateColorCommand(
-		Palette *pal,
-		size_t page,
-		int idx,
-		Color16 oldColor,
-		Color16 newColor) noexcept {
-	m_pal = pal;
-	m_page = page;
-	m_idx = idx;
-	m_oldColor = oldColor;
-	m_newColor = newColor;
-	//setObsolete(m_oldColor == m_newColor);
+	Palette &pal,
+	size_t page,
+	size_t idx,
+	Color16 newColor):
+		m_pal(pal),
+		m_page(page),
+		m_idx(idx),
+		m_altColor(newColor) {
+	if (color(m_pal, m_page, m_idx) == newColor) {
+		throw studio::NoChangesException();
+	}
 }
 
 bool UpdateColorCommand::mergeWith(UndoCommand const&cmd) noexcept {
@@ -137,7 +216,6 @@ bool UpdateColorCommand::mergeWith(UndoCommand const&cmd) noexcept {
 	if (m_idx != ucCmd->m_idx) {
 		return false;
 	}
-	m_newColor = ucCmd->m_newColor;
 	return true;
 }
 
@@ -147,41 +225,46 @@ int UpdateColorCommand::commandId() const noexcept {
 }
 
 ox::Error UpdateColorCommand::redo() noexcept {
-	m_pal->pages[m_page][static_cast<std::size_t>(m_idx)] = m_newColor;
+	swap();
 	return {};
 }
 
 ox::Error UpdateColorCommand::undo() noexcept {
-	m_pal->pages[m_page][static_cast<std::size_t>(m_idx)] = m_oldColor;
+	swap();
 	return {};
 }
 
-
-MoveColorCommand::MoveColorCommand(Palette *pal, size_t page, std::size_t idx, int offset) noexcept {
-	m_pal = pal;
-	m_page = page;
-	m_idx = idx;
-	m_offset = offset;
+void UpdateColorCommand::swap() noexcept {
+	auto &dst = colors(m_pal, m_page)[m_idx];
+	std::swap(dst, m_altColor);
 }
 
+
+MoveColorCommand::MoveColorCommand(
+		Palette &pal, size_t page, size_t srcIdx, size_t dstIdx) noexcept:
+	m_pal(pal),
+	m_page(page),
+	m_srcIdx(srcIdx),
+	m_dstIdx(dstIdx) {}
+
 int MoveColorCommand::commandId() const noexcept {
 	return static_cast<int>(PaletteEditorCommandId::MoveColor);
 }
 
 ox::Error MoveColorCommand::redo() noexcept {
-	moveColor(static_cast<int>(m_idx), m_offset);
+	moveColor(m_srcIdx, m_dstIdx);
 	return {};
 }
 
 ox::Error MoveColorCommand::undo() noexcept {
-	moveColor(static_cast<int>(m_idx) + m_offset, -m_offset);
+	moveColor(m_dstIdx, m_srcIdx);
 	return {};
 }
 
-void MoveColorCommand::moveColor(int idx, int offset) noexcept {
-	const auto c = m_pal->pages[m_page][static_cast<std::size_t>(idx)];
-	std::ignore = m_pal->pages[m_page].erase(static_cast<std::size_t>(idx));
-	m_pal->pages[m_page].insert(static_cast<std::size_t>(idx + offset), c);
+void MoveColorCommand::moveColor(size_t srcIdx, size_t dstIdx) noexcept {
+	auto const c = color(m_pal, m_page, srcIdx);
+	std::ignore = colors(m_pal, m_page).erase(srcIdx);
+	colors(m_pal, m_page).emplace(dstIdx, c);
 }
 
 }
diff --git a/src/nostalgia/modules/core/src/studio/paletteeditor/paletteeditor.hpp b/src/nostalgia/modules/core/src/studio/paletteeditor/paletteeditor.hpp
index 75c0115e..a12db670 100644
--- a/src/nostalgia/modules/core/src/studio/paletteeditor/paletteeditor.hpp
+++ b/src/nostalgia/modules/core/src/studio/paletteeditor/paletteeditor.hpp
@@ -13,24 +13,28 @@
 namespace nostalgia::core {
 
 enum class PaletteEditorCommandId {
-	AddPage,
+	ApplyColorAllPages,
 	DuplicatePage,
 	RemovePage,
 	AddColor,
 	RemoveColor,
+	UpdateColorInfo,
 	UpdateColor,
 	MoveColor,
 };
 
 
-class AddPageCommand: public studio::UndoCommand {
+class ApplyColorAllPagesCommand: public studio::UndoCommand {
 	private:
 		Palette &m_pal;
+		size_t const m_page{};
+		size_t const m_idx{};
+		ox::Vector<Color16> const m_origColors;
 
 	public:
-		AddPageCommand(Palette &pal) noexcept;
+		ApplyColorAllPagesCommand(Palette &pal, size_t page, size_t idx);
 
-		~AddPageCommand() noexcept override = default;
+		~ApplyColorAllPagesCommand() noexcept override = default;
 
 		[[nodiscard]]
 		int commandId() const noexcept final;
@@ -38,7 +42,6 @@ class AddPageCommand: public studio::UndoCommand {
 		ox::Error redo() noexcept final;
 
 		ox::Error undo() noexcept final;
-
 };
 
 class DuplicatePageCommand: public studio::UndoCommand {
@@ -86,13 +89,12 @@ class RemovePageCommand: public studio::UndoCommand {
 
 class AddColorCommand: public studio::UndoCommand {
 	private:
-		Palette *m_pal = nullptr;
+		Palette &m_pal;
 		Color16 m_color = 0;
-		int m_idx = -1;
-		size_t m_page = 0;
+		size_t const m_idx = 0;
 
 	public:
-		AddColorCommand(Palette *pal, Color16 color, size_t page, int idx) noexcept;
+		AddColorCommand(Palette &pal, Color16 color, size_t idx) noexcept;
 
 		~AddColorCommand() noexcept override = default;
 
@@ -107,13 +109,12 @@ class AddColorCommand: public studio::UndoCommand {
 
 class RemoveColorCommand: public studio::UndoCommand {
 	private:
-		Palette *m_pal = nullptr;
-		Color16 m_color = 0;
-		size_t m_page = 0;
-		int m_idx = -1;
+		Palette &m_pal;
+		size_t const m_idx = 0;
+		ox::Vector<Color16> const m_colors;
 
 	public:
-		RemoveColorCommand(Palette *pal, Color16 color, size_t page, int idx) noexcept;
+		RemoveColorCommand(Palette &pal, size_t idx) noexcept;
 
 		~RemoveColorCommand() noexcept override = default;
 
@@ -126,16 +127,48 @@ class RemoveColorCommand: public studio::UndoCommand {
 
 };
 
-class UpdateColorCommand: public studio::UndoCommand {
+class UpdateColorInfoCommand: public studio::UndoCommand {
 	private:
-		Palette *m_pal = nullptr;
-		Color16 m_oldColor = 0;
-		Color16 m_newColor = 0;
-		size_t m_page = 0;
-		int m_idx = -1;
+		Palette &m_pal;
+		size_t const m_idx{};
+		Palette::ColorInfo m_altColorInfo;
 
 	public:
-		UpdateColorCommand(Palette *pal, size_t page, int idx, Color16 oldColor, Color16 newColor) noexcept;
+		UpdateColorInfoCommand(
+				Palette &pal,
+				size_t idx,
+				Palette::ColorInfo newColorInfo);
+
+		~UpdateColorInfoCommand() noexcept override = default;
+
+		[[nodiscard]]
+		bool mergeWith(const UndoCommand &cmd) noexcept final;
+
+		[[nodiscard]]
+		int commandId() const noexcept final;
+
+		ox::Error redo() noexcept final;
+
+		ox::Error undo() noexcept final;
+
+	private:
+		void swap() noexcept;
+
+};
+
+class UpdateColorCommand: public studio::UndoCommand {
+	private:
+		Palette &m_pal;
+		size_t const m_page = 0;
+		size_t const m_idx{};
+		Color16 m_altColor{};
+
+	public:
+		UpdateColorCommand(
+				Palette &pal,
+				size_t page,
+				size_t idx,
+				Color16 newColor);
 
 		~UpdateColorCommand() noexcept override = default;
 
@@ -149,17 +182,20 @@ class UpdateColorCommand: public studio::UndoCommand {
 
 		ox::Error undo() noexcept final;
 
+	private:
+		void swap() noexcept;
+
 };
 
 class MoveColorCommand: public studio::UndoCommand {
 	private:
-		Palette *m_pal = nullptr;
-		size_t m_page = 0;
-		std::size_t m_idx = 0;
-		int m_offset = 0;
+		Palette &m_pal;
+		size_t const m_page = 0;
+		std::size_t const m_srcIdx = 0;
+		std::size_t const m_dstIdx = 0;
 
 	public:
-		MoveColorCommand(Palette *pal, size_t page, std::size_t idx, int offset) noexcept;
+		MoveColorCommand(Palette &pal, size_t page, size_t srcIdx, size_t dstIdx) noexcept;
 
 		~MoveColorCommand() noexcept override = default;
 
@@ -172,7 +208,7 @@ class MoveColorCommand: public studio::UndoCommand {
 		ox::Error undo() noexcept override;
 
 	private:
-		void moveColor(int idx, int offset) noexcept;
+		void moveColor(size_t srcIdx, size_t dstIdx) noexcept;
 };
 
 }
diff --git a/src/nostalgia/modules/core/src/studio/tilesheeteditor/tilesheeteditor-imgui.cpp b/src/nostalgia/modules/core/src/studio/tilesheeteditor/tilesheeteditor-imgui.cpp
index 4d6260e9..75a03783 100644
--- a/src/nostalgia/modules/core/src/studio/tilesheeteditor/tilesheeteditor-imgui.cpp
+++ b/src/nostalgia/modules/core/src/studio/tilesheeteditor/tilesheeteditor-imgui.cpp
@@ -7,6 +7,7 @@
 
 #include <ox/std/point.hpp>
 #include <keel/media.hpp>
+#include <studio/studio.hpp>
 
 #include "tilesheeteditor-imgui.hpp"
 
@@ -14,6 +15,36 @@ namespace nostalgia::core {
 
 namespace ig = studio::ig;
 
+static ox::String configName(ox::StringView str) noexcept {
+	auto out = ox::String{str};
+	for (auto &c : out) {
+		if (c == '/' || c == '\\') {
+			c = '%';
+		}
+	}
+	return out;
+}
+
+struct TileSheetEditorConfig {
+	static constexpr auto TypeName = "net.drinkingtea.nostalgia.core.studio.TileSheetEditorConfig";
+	static constexpr auto TypeVersion = 1;
+	TileSheet::SubSheetIdx activeSubsheet{};
+};
+
+oxModelBegin(TileSheetEditorConfig)
+	oxModelFieldRename(activeSubsheet, active_subsheet)
+oxModelEnd()
+
+struct TileSheetEditorConfigs {
+	static constexpr auto TypeName = "net.drinkingtea.nostalgia.core.studio.TileSheetEditorConfigs";
+	static constexpr auto TypeVersion = 1;
+	ox::HashMap<ox::String, TileSheetEditorConfig> configs;
+};
+
+oxModelBegin(TileSheetEditorConfigs)
+	oxModelField(configs)
+oxModelEnd()
+
 static ox::Vector<uint32_t> normalizePixelSizes(
 		ox::Vector<uint8_t> const&inPixels,
 		int const bpp) noexcept {
@@ -75,7 +106,7 @@ static ox::Error toPngFile(
 					8)));
 }
 
-TileSheetEditorImGui::TileSheetEditorImGui(studio::StudioContext &sctx, ox::CRStringView path):
+TileSheetEditorImGui::TileSheetEditorImGui(studio::StudioContext &sctx, ox::StringView const path):
 		Editor(path),
 		m_sctx(sctx),
 		m_tctx(m_sctx.tctx),
@@ -87,6 +118,12 @@ TileSheetEditorImGui::TileSheetEditorImGui(studio::StudioContext &sctx, ox::CRSt
 	m_subsheetEditor.inputSubmitted.connect(this, &TileSheetEditorImGui::updateActiveSubsheet);
 	m_exportMenu.inputSubmitted.connect(this, &TileSheetEditorImGui::exportSubhseetToPng);
 	m_model.paletteChanged.connect(this, &TileSheetEditorImGui::setPaletteSelection);
+	// load config
+	auto const&config = studio::readConfig<TileSheetEditorConfig>(
+			keelCtx(m_sctx), configName(itemPath()));
+	if (config.ok()) {
+		m_model.setActiveSubsheet(validateSubSheetIdx(m_model.img(), config.value.activeSubsheet));
+	}
 }
 
 void TileSheetEditorImGui::exportFile() {
@@ -120,7 +157,7 @@ void TileSheetEditorImGui::keyStateChanged(turbine::Key key, bool down) {
 	auto const popupOpen = m_subsheetEditor.isOpen() && m_exportMenu.isOpen();
 	auto const pal = m_model.pal();
 	if (!popupOpen) {
-		auto const colorCnt = pal.pages[m_model.palettePage()].size();
+		auto const colorCnt = core::colorCnt(pal, m_model.palettePage());
 		if (key == turbine::Key::Alpha_D) {
 			m_tool = TileSheetTool::Draw;
 			setCopyEnabled(false);
@@ -144,7 +181,8 @@ void TileSheetEditorImGui::keyStateChanged(turbine::Key key, bool down) {
 						static_cast<uint32_t>(key - turbine::Key::Num_1), m_model.pal().pages.size() - 1);
 				m_model.setPalettePage(idx);
 			} else if (key <= turbine::Key::Num_0 + colorCnt) {
-				auto const idx = ox::min<std::size_t>(static_cast<uint32_t>(key - turbine::Key::Num_1), colorCnt - 1);
+				auto const idx = ox::min<std::size_t>(
+						static_cast<uint32_t>(key - turbine::Key::Num_1), colorCnt - 1);
 				m_view.setPalIdx(idx);
 			}
 		} else if (key == turbine::Key::Num_0) {
@@ -208,7 +246,7 @@ void TileSheetEditorImGui::draw(studio::StudioContext&) noexcept {
 				auto const&parent = m_model.activeSubSheet();
 				m_model.addSubsheet(insertOnIdx);
 				insertOnIdx.emplace_back(parent.subsheets.size() - 1);
-				m_model.setActiveSubsheet(insertOnIdx);
+				setActiveSubsheet(insertOnIdx);
 			}
 			ImGui::SameLine();
 			if (ig::PushButton("-", btnSize)) {
@@ -241,7 +279,7 @@ void TileSheetEditorImGui::draw(studio::StudioContext&) noexcept {
 	}
 	ImGui::EndChild();
 	m_subsheetEditor.draw(m_tctx);
-	m_exportMenu.draw(m_sctx);
+	m_exportMenu.draw(m_tctx);
 }
 
 void TileSheetEditorImGui::drawSubsheetSelector(
@@ -261,7 +299,7 @@ void TileSheetEditorImGui::drawSubsheetSelector(
 	auto const open = ImGui::TreeNodeEx(lbl.c_str(), flags);
 	ImGui::SameLine();
 	if (ImGui::IsItemClicked()) {
-		m_model.setActiveSubsheet(path);
+		setActiveSubsheet(path);
 	}
 	if (ImGui::IsMouseDoubleClicked(0) && ImGui::IsItemHovered()) {
 		showSubsheetEditor();
@@ -432,31 +470,35 @@ void TileSheetEditorImGui::drawPaletteSelector() noexcept {
 		ImGui::Indent(-20);
 	}
 	// header
-	if (ImGui::BeginTable("PaletteTable", 3, ImGuiTableFlags_RowBg | ImGuiTableFlags_SizingStretchProp)) {
+	if (ImGui::BeginTable(
+			"PaletteTable", 3, ImGuiTableFlags_RowBg | ImGuiTableFlags_SizingStretchProp)) {
 		ImGui::TableSetupColumn("No.", 0, 0.45f);
 		ImGui::TableSetupColumn("", 0, 0.22f);
 		ImGui::TableSetupColumn("Color16", 0, 3);
 		ImGui::TableHeadersRow();
 		{
 			auto const&pal = m_model.pal();
-			for (auto i = 0u; auto c: pal.pages[m_model.palettePage()]) {
-				ImGui::PushID(static_cast<int>(i));
-				// Column: color idx
-				ImGui::TableNextColumn();
-				auto const label = ox::itoa(i + 1);
-				auto const rowSelected = i == m_view.palIdx();
-				if (ImGui::Selectable(label.c_str(), rowSelected, ImGuiSelectableFlags_SpanAllColumns)) {
-					m_view.setPalIdx(i);
+			if (pal.pages.size() > m_model.palettePage()) {
+				for (auto i = 0u; auto const&c: pal.pages[m_model.palettePage()]) {
+					ImGui::PushID(static_cast<int>(i));
+					// Column: color idx
+					ImGui::TableNextColumn();
+					auto const label = ox::itoa(i + 1);
+					auto const rowSelected = i == m_view.palIdx();
+					if (ImGui::Selectable(
+							label.c_str(), rowSelected, ImGuiSelectableFlags_SpanAllColumns)) {
+						m_view.setPalIdx(i);
+					}
+					// Column: color RGB
+					ImGui::TableNextColumn();
+					auto ic = ImGui::GetColorU32(ImVec4(redf(c), greenf(c), bluef(c), 1));
+					ImGui::TableSetBgColor(ImGuiTableBgTarget_CellBg, ic);
+					ImGui::TableNextColumn();
+					ImGui::Text("(%02d, %02d, %02d)", red16(c), green16(c), blue16(c));
+					ImGui::TableNextRow();
+					ImGui::PopID();
+					++i;
 				}
-				// Column: color RGB
-				ImGui::TableNextColumn();
-				auto ic = ImGui::GetColorU32(ImVec4(redf(c), greenf(c), bluef(c), 1));
-				ImGui::TableSetBgColor(ImGuiTableBgTarget_CellBg, ic);
-				ImGui::TableNextColumn();
-				ImGui::Text("(%02d, %02d, %02d)", red16(c), green16(c), blue16(c));
-				ImGui::TableNextRow();
-				ImGui::PopID();
-				++i;
 			}
 		}
 		ImGui::EndTable();
@@ -480,12 +522,20 @@ ox::Error TileSheetEditorImGui::setPaletteSelection() noexcept {
 	return {};
 }
 
+void TileSheetEditorImGui::setActiveSubsheet(TileSheet::SubSheetIdx path) noexcept {
+	m_model.setActiveSubsheet(path);
+	studio::editConfig<TileSheetEditorConfig>(keelCtx(m_sctx), configName(itemPath()),
+		[&path](TileSheetEditorConfig *config) {
+			config->activeSubsheet = std::move(path);
+		});
+}
+
 ox::Error TileSheetEditorImGui::markUnsavedChanges(studio::UndoCommand const*) noexcept {
 	setUnsavedChanges(true);
 	return {};
 }
 
-void TileSheetEditorImGui::SubSheetEditor::draw(turbine::Context &sctx) noexcept {
+void TileSheetEditorImGui::SubSheetEditor::draw(turbine::Context &tctx) noexcept {
 	constexpr auto popupName = "Edit Subsheet";
 	if (!m_show) {
 		return;
@@ -494,7 +544,7 @@ void TileSheetEditorImGui::SubSheetEditor::draw(turbine::Context &sctx) noexcept
 	auto constexpr popupWidth = 235.f;
 	auto const popupHeight = modSize ? 130.f : 85.f;
 	auto const popupSz = ImVec2(popupWidth, popupHeight);
-	if (ig::BeginPopup(sctx, popupName, m_show, popupSz)) {
+	if (ig::BeginPopup(tctx, popupName, m_show, popupSz)) {
 		ImGui::InputText("Name", m_name.data(), m_name.cap());
 		if (modSize) {
 			ImGui::InputInt("Columns", &m_cols);
@@ -511,7 +561,7 @@ void TileSheetEditorImGui::SubSheetEditor::close() noexcept {
 	m_show = false;
 }
 
-void TileSheetEditorImGui::ExportMenu::draw(studio::StudioContext &sctx) noexcept {
+void TileSheetEditorImGui::ExportMenu::draw(turbine::Context &tctx) noexcept {
 	constexpr auto popupName = "Export Tile Sheet";
 	if (!m_show) {
 		return;
@@ -519,7 +569,7 @@ void TileSheetEditorImGui::ExportMenu::draw(studio::StudioContext &sctx) noexcep
 	constexpr auto popupWidth = 235.f;
 	constexpr auto popupHeight = 85.f;
 	constexpr auto popupSz = ImVec2(popupWidth, popupHeight);
-	if (ig::BeginPopup(sctx.tctx, popupName, m_show, popupSz)) {
+	if (ig::BeginPopup(tctx, popupName, m_show, popupSz)) {
 		ImGui::InputInt("Scale", &m_scale);
 		m_scale = ox::clamp(m_scale, 1, 50);
 		if (ig::PopupControlsOkCancel(popupWidth, m_show) == ig::PopupResponse::OK) {
diff --git a/src/nostalgia/modules/core/src/studio/tilesheeteditor/tilesheeteditor-imgui.hpp b/src/nostalgia/modules/core/src/studio/tilesheeteditor/tilesheeteditor-imgui.hpp
index fbdebb7f..deb45d47 100644
--- a/src/nostalgia/modules/core/src/studio/tilesheeteditor/tilesheeteditor-imgui.hpp
+++ b/src/nostalgia/modules/core/src/studio/tilesheeteditor/tilesheeteditor-imgui.hpp
@@ -48,7 +48,7 @@ class TileSheetEditorImGui: public studio::Editor {
 					m_show = true;
 					m_scale = 5;
 				}
-				void draw(studio::StudioContext &sctx) noexcept;
+				void draw(turbine::Context &sctx) noexcept;
 				void close() noexcept;
 				[[nodiscard]]
 				inline bool isOpen() const noexcept { return m_show; }
@@ -67,7 +67,7 @@ class TileSheetEditorImGui: public studio::Editor {
 		TileSheetTool m_tool = TileSheetTool::Draw;
 
 	public:
-		TileSheetEditorImGui(studio::StudioContext &sctx, ox::CRStringView path);
+		TileSheetEditorImGui(studio::StudioContext &sctx, ox::StringView path);
 
 		~TileSheetEditorImGui() override = default;
 
@@ -111,6 +111,8 @@ class TileSheetEditorImGui: public studio::Editor {
 	private:
 		ox::Error markUnsavedChanges(studio::UndoCommand const*) noexcept;
 
+		void setActiveSubsheet(TileSheet::SubSheetIdx path) noexcept;
+
 };
 
 }
diff --git a/src/nostalgia/modules/core/src/studio/tilesheeteditor/tilesheeteditormodel.cpp b/src/nostalgia/modules/core/src/studio/tilesheeteditor/tilesheeteditormodel.cpp
index 510532ac..f94d68d1 100644
--- a/src/nostalgia/modules/core/src/studio/tilesheeteditor/tilesheeteditormodel.cpp
+++ b/src/nostalgia/modules/core/src/studio/tilesheeteditor/tilesheeteditormodel.cpp
@@ -27,7 +27,8 @@
 namespace nostalgia::core {
 
 Palette const TileSheetEditorModel::s_defaultPalette = {
-	.pages = {ox::Vector<Color16>(128)},
+	.colorInfo = {ox::Vector<Palette::ColorInfo>{{}}},
+	.pages = {{ox::Vector<Color16>(128)}},
 };
 
 // delete pixels of all non-leaf nodes