diff --git a/LICENSE.txt b/LICENSE.txt
index de76c7a80..03ca35100 100644
--- a/LICENSE.txt
+++ b/LICENSE.txt
@@ -57,12 +57,10 @@ srifqi:
   textures/base/pack/minimap_btn.png
 
 Zughy:
-  textures/base/pack/cdb_add.png
   textures/base/pack/cdb_downloading.png
   textures/base/pack/cdb_queued.png
   textures/base/pack/cdb_update.png
   textures/base/pack/cdb_update_cropped.png
-  textures/base/pack/cdb_viewonline.png
   textures/base/pack/settings_btn.png
   textures/base/pack/settings_info.png
   textures/base/pack/settings_reset.png
@@ -79,7 +77,6 @@ kilbith:
   textures/base/pack/progress_bar_bg.png
 
 SmallJoker:
-  textures/base/pack/cdb_clear.png
   textures/base/pack/server_favorite_delete.png (based on server_favorite.png)
 
 DS:
diff --git a/builtin/mainmenu/content/contentdb.lua b/builtin/mainmenu/content/contentdb.lua
index 4d59826dd..5d6d6c482 100644
--- a/builtin/mainmenu/content/contentdb.lua
+++ b/builtin/mainmenu/content/contentdb.lua
@@ -641,3 +641,27 @@ function contentdb.get_full_package_info(package, callback)
 		callback(nil)
 	end
 end
+
+
+function contentdb.get_formspec_padding()
+	-- Padding is increased on Android to account for notches
+	-- TODO: use Android API to determine size of cut outs
+	return { x = PLATFORM == "Android" and 1 or 0.5, y = PLATFORM == "Android" and 0.25 or 0.5 }
+end
+
+
+function contentdb.get_formspec_size()
+	local window = core.get_window_info()
+	local size = { x = window.max_formspec_size.x, y = window.max_formspec_size.y }
+
+	-- Minimum formspec size
+	local min_x = 15.5
+	local min_y = 10
+	if size.x < min_x or size.y < min_y then
+		local scale = math.max(min_x / size.x, min_y / size.y)
+		size.x = size.x * scale
+		size.y = size.y * scale
+	end
+
+	return size
+end
diff --git a/builtin/mainmenu/content/dlg_contentdb.lua b/builtin/mainmenu/content/dlg_contentdb.lua
index 025430bfa..a86815b77 100644
--- a/builtin/mainmenu/content/dlg_contentdb.lua
+++ b/builtin/mainmenu/content/dlg_contentdb.lua
@@ -26,23 +26,17 @@ end
 -- Filter
 local search_string = ""
 local cur_page = 1
-local num_per_page = 5
-local filter_type = 1
-local filter_types_titles = {
-	fgettext("All packages"),
-	fgettext("Games"),
-	fgettext("Mods"),
-	fgettext("Texture packs"),
-}
+local filter_type
 
 -- Automatic package installation
 local auto_install_spec = nil
 
-local filter_types_type = {
-	nil,
-	"game",
-	"mod",
-	"txp",
+
+local filter_type_names = {
+	{ "type_all", nil },
+	{ "type_game", "game" },
+	{ "type_mod", "mod" },
+	{ "type_txp", "txp" },
 }
 
 
@@ -103,7 +97,7 @@ end
 local function sort_and_filter_pkgs()
 	contentdb.update_paths()
 	contentdb.sort_packages()
-	contentdb.filter_packages(search_string, filter_types_type[filter_type])
+	contentdb.filter_packages(search_string, filter_type)
 
 	local auto_install_pkg = resolve_auto_install_spec()
 	if auto_install_pkg then
@@ -134,72 +128,151 @@ local function load()
 end
 
 
-local function get_info_formspec(text)
-	local H = 9.5
+local function get_info_formspec(size, padding, text)
 	return table.concat({
 		"formspec_version[6]",
-		"size[15.75,9.5]",
-		core.settings:get_bool("touch_gui") and "padding[0.01,0.01]" or "position[0.5,0.55]",
+		"size[", size.x, ",", size.y, "]",
+		"padding[0,0]",
+		"bgcolor[;true]",
 
-		"label[4,4.35;", text, "]",
-		"container[0,", H - 0.8 - 0.375, "]",
-		"button[0.375,0;5,0.8;back;", fgettext("Back to Main Menu"), "]",
+		"label[", padding.x + 3.625, ",4.35;", text, "]",
+		"container[", padding.x, ",", size.y - 0.8 - padding.y, "]",
+		"button[0,0;2,0.8;back;", fgettext("Back"), "]",
 		"container_end[]",
 	})
 end
 
 
+-- Determines how to fit `num_per_page` into `size` space
+local function fit_cells(num_per_page, size)
+	local cell_spacing = 0.5
+	local columns = 1
+	local cell_w, cell_h
+	-- Fit cells into the available height
+	while true do
+		cell_w = (size.x - (columns-1)*cell_spacing) / columns
+		cell_h = cell_w / 4
+
+		local required_height = math.ceil(num_per_page / columns) * (cell_h + cell_spacing) - cell_spacing
+		-- Add 0.1 to be more lenient
+		if required_height <= size.y + 0.1 then
+			break
+		end
+
+		columns = columns + 1
+	end
+
+	return cell_spacing, columns, cell_w, cell_h
+end
+
+
+local function calculate_num_per_page()
+	local size = contentdb.get_formspec_size()
+	local padding = contentdb.get_formspec_padding()
+	local window = core.get_window_info()
+
+	size.x = size.x - padding.x * 2
+	size.y = size.y - padding.y * 2 - 1.425 - 0.25 - 0.8
+
+	local coordToPx = window.size.x / window.max_formspec_size.x / window.real_gui_scaling
+
+	local num_per_page = 12
+	while num_per_page > 2 do
+		local _, _, cell_w, _ = fit_cells(num_per_page, size)
+		if cell_w * coordToPx > 350 then
+			break
+		end
+
+		num_per_page = num_per_page - 1
+	end
+	return num_per_page
+end
+
+
 local function get_formspec(dlgdata)
+	local window_padding = contentdb.get_formspec_padding()
+	local size = contentdb.get_formspec_size()
+
 	if contentdb.loading then
-		return get_info_formspec(fgettext("Loading..."))
+		return get_info_formspec(size, window_padding, fgettext("Loading..."))
 	end
 	if contentdb.load_error then
-		return get_info_formspec(fgettext("No packages could be retrieved"))
+		return get_info_formspec(size, window_padding, fgettext("No packages could be retrieved"))
 	end
 	assert(contentdb.load_ok)
 
 	contentdb.update_paths()
 
+	local num_per_page = dlgdata.num_per_page
 	dlgdata.pagemax = math.max(math.ceil(#contentdb.packages / num_per_page), 1)
 	if cur_page > dlgdata.pagemax then
 		cur_page = 1
 	end
 
-	local W = 15.75
-	local H = 9.5
+	local W = size.x - window_padding.x * 2
+	local H = size.y - window_padding.y * 2
+
+	local category_x = 0
+	local number_category_buttons = 4
+	local max_button_w = (W - 0.375 - 0.25 - 7) / number_category_buttons
+	local category_button_w = math.min(max_button_w, 3)
+	local function make_category_button(name, label, selected)
+		category_x = category_x + 1
+		local color = selected and mt_color_green or ""
+		return ("style[%s;bgcolor=%s]button[%f,0;%f,0.8;%s;%s]"):format(name, color,
+				(category_x - 1) * category_button_w, category_button_w, name, label)
+	end
+
+
+	local selected_type = filter_type
+
+	local search_box_width = W - 0.375 - 0.25 - 2*0.8
+			- number_category_buttons * category_button_w
 	local formspec = {
-		"formspec_version[6]",
-		"size[15.75,9.5]",
-		core.settings:get_bool("touch_gui") and "padding[0.01,0.01]" or "position[0.5,0.55]",
+		"formspec_version[7]",
+		"size[", size.x, ",", size.y, "]",
+		"padding[0,0]",
+		"bgcolor[;true]",
 
-		"style[status,downloading,queued;border=false]",
+		"container[", window_padding.x, ",", window_padding.y, "]",
 
-		"container[0.375,0.375]",
-		"field[0,0;7.225,0.8;search_string;;", core.formspec_escape(search_string), "]",
+		-- Top-left: categories
+		make_category_button("type_all", fgettext("All"), selected_type == nil),
+		make_category_button("type_game", fgettext("Games"), selected_type == "game"),
+		make_category_button("type_mod", fgettext("Mods"), selected_type == "mod"),
+		make_category_button("type_txp", fgettext("Texture Packs"), selected_type == "txp"),
+
+		-- Top-right: Search
+		"container[", W - search_box_width - 0.8*2, ",0]",
+		"field[0,0;", search_box_width, ",0.8;search_string;;", core.formspec_escape(search_string), "]",
 		"field_enter_after_edit[search_string;true]",
-		"image_button[7.3,0;0.8,0.8;", core.formspec_escape(defaulttexturedir .. "search.png"), ";search;]",
-		"image_button[8.125,0;0.8,0.8;", core.formspec_escape(defaulttexturedir .. "clear.png"), ";clear;]",
-		"dropdown[9.175,0;2.7875,0.8;type;", table.concat(filter_types_titles, ","), ";", filter_type, "]",
+		"image_button[", search_box_width, ",0;0.8,0.8;",
+			core.formspec_escape(defaulttexturedir .. "search.png"), ";search;]",
+		"image_button[", search_box_width + 0.8, ",0;0.8,0.8;",
+			core.formspec_escape(defaulttexturedir .. "clear.png"), ";clear;]",
 		"container_end[]",
 
-		-- Page nav buttons
-		"container[0,", H - 0.8 - 0.375, "]",
-		"button[0.375,0;5,0.8;back;", fgettext("Back to Main Menu"), "]",
+		-- Bottom strip start
+		"container[0,", H - 0.8, "]",
+		"button[0,0;2,0.8;back;", fgettext("Back"), "]",
 
-		"container[", W - 0.375 - 0.8*4 - 2,  ",0]",
-		"image_button[0,0;0.8,0.8;", core.formspec_escape(defaulttexturedir), "start_icon.png;pstart;]",
-		"image_button[0.8,0;0.8,0.8;", core.formspec_escape(defaulttexturedir), "prev_icon.png;pback;]",
+		-- Bottom-center: Page nav buttons
+		"container[", (W - 1*4 - 2) / 2, ",0]",
+		"image_button[0,0;1,0.8;", core.formspec_escape(defaulttexturedir), "start_icon.png;pstart;]",
+		"image_button[1,0;1,0.8;", core.formspec_escape(defaulttexturedir), "prev_icon.png;pback;]",
 		"style[pagenum;border=false]",
-		"button[1.6,0;2,0.8;pagenum;", tonumber(cur_page), " / ", tonumber(dlgdata.pagemax), "]",
-		"image_button[3.6,0;0.8,0.8;", core.formspec_escape(defaulttexturedir), "next_icon.png;pnext;]",
-		"image_button[4.4,0;0.8,0.8;", core.formspec_escape(defaulttexturedir), "end_icon.png;pend;]",
-		"container_end[]",
+		"button[2,0;2,0.8;pagenum;", tonumber(cur_page), " / ", tonumber(dlgdata.pagemax), "]",
+		"image_button[4,0;1,0.8;", core.formspec_escape(defaulttexturedir), "next_icon.png;pnext;]",
+		"image_button[5,0;1,0.8;", core.formspec_escape(defaulttexturedir), "end_icon.png;pend;]",
+		"container_end[]", -- page nav end
 
-		"container_end[]",
+		-- Bottom-right: updating
+		"container[", W - 3, ",0]",
+		"style[status,downloading,queued;border=false]",
 	}
 
 	if contentdb.number_downloading > 0 then
-		formspec[#formspec + 1] = "button[12.5875,0.375;2.7875,0.8;downloading;"
+		formspec[#formspec + 1] = "button[0,0;3,0.8;downloading;"
 		if #contentdb.download_queue > 0 then
 			formspec[#formspec + 1] = fgettext("$1 downloading,\n$2 queued",
 					contentdb.number_downloading, #contentdb.download_queue)
@@ -218,16 +291,19 @@ local function get_formspec(dlgdata)
 		end
 
 		if num_avail_updates == 0 then
-			formspec[#formspec + 1] = "button[12.5875,0.375;2.7875,0.8;status;"
+			formspec[#formspec + 1] = "button[0,0;3,0.8;status;"
 			formspec[#formspec + 1] = fgettext("No updates")
 			formspec[#formspec + 1] = "]"
 		else
-			formspec[#formspec + 1] = "button[12.5875,0.375;2.7875,0.8;update_all;"
+			formspec[#formspec + 1] = "button[0,0;3,0.8;update_all;"
 			formspec[#formspec + 1] = fgettext("Update All [$1]", num_avail_updates)
 			formspec[#formspec + 1] = "]"
 		end
 	end
 
+	formspec[#formspec + 1] = "container_end[]" -- updating end
+	formspec[#formspec + 1] = "container_end[]" -- bottom strip end
+
 	if #contentdb.packages == 0 then
 		formspec[#formspec + 1] = "label[4,4.75;"
 		formspec[#formspec + 1] = fgettext("No results")
@@ -239,46 +315,85 @@ local function get_formspec(dlgdata)
 	formspec[#formspec + 1] = "tooltip[downloading;" .. fgettext("Downloading...") .. tooltip_colors
 	formspec[#formspec + 1] = "tooltip[queued;" .. fgettext("Queued") .. tooltip_colors
 
+	formspec[#formspec + 1] = "container[0,1.425]"
+
+	local cell_spacing, columns, cell_w, cell_h = fit_cells(num_per_page, {
+		x = W,
+		y = H - 1.425 - 0.25 - 0.8
+	})
+	local img_w = cell_h * 3 / 2
+
 	local start_idx = (cur_page - 1) * num_per_page + 1
 	for i=start_idx, math.min(#contentdb.packages, start_idx+num_per_page-1) do
 		local package = contentdb.packages[i]
-		local container_y = (i - start_idx) * 1.375 + (2*0.375 + 0.8)
-		formspec[#formspec + 1] = "container[0.375,"
-		formspec[#formspec + 1] = container_y
-		formspec[#formspec + 1] = "]"
 
-		-- image
-		formspec[#formspec + 1] = "image[0,0;1.5,1;"
-		formspec[#formspec + 1] = core.formspec_escape(get_screenshot(package, package.thumbnail, 1))
-		formspec[#formspec + 1] = "]"
+		table.insert_all(formspec, {
+			"container[",
+			(cell_w + cell_spacing) * ((i - start_idx) % columns),
+			",",
+			(cell_h + cell_spacing) * math.floor((i - start_idx) / columns),
+			"]",
 
-		-- title
-		formspec[#formspec + 1] = "label[1.875,0.1;"
-		formspec[#formspec + 1] = core.formspec_escape(
-				core.colorize(mt_color_green, package.title) ..
-				core.colorize("#BFBFBF", " by " .. package.author))
-		formspec[#formspec + 1] = "]"
+			"box[0,0;", cell_w, ",", cell_h, ";#ffffff11]",
 
-		-- button
-		formspec[#formspec + 1] = "button["
-		formspec[#formspec + 1] = W-0.375*2-2
-		formspec[#formspec + 1] = ",0.1;2,0.7;view_"
-		formspec[#formspec + 1] = i
-		formspec[#formspec + 1] = ";"
-		formspec[#formspec + 1] = fgettext("View")
-		formspec[#formspec + 1] = "]"
+			-- image,
+			"image[0,0;", img_w, ",", cell_h, ";",
+				core.formspec_escape(get_screenshot(package, package.thumbnail, 2)), "]",
 
-		-- description
-		local description_width = W - 2.625 - 2 * 0.7 - 2 * 0.15
-		formspec[#formspec + 1] = "textarea[1.855,0.3;"
-		formspec[#formspec + 1] = tostring(description_width)
-		formspec[#formspec + 1] = ",0.8;;;"
-		formspec[#formspec + 1] = core.formspec_escape(package.short_description)
-		formspec[#formspec + 1] = "]"
+			"label[", img_w + 0.25 + 0.05, ",0.5;",
+				core.formspec_escape(
+					core.colorize(mt_color_green, package.title) ..
+							core.colorize("#BFBFBF", " by " .. package.author)), "]",
 
-		formspec[#formspec + 1] = "container_end[]"
+			"textarea[", img_w + 0.25, ",0.75;", cell_w - img_w - 0.25, ",", cell_h - 0.75, ";;;",
+				core.formspec_escape(package.short_description), "]",
+
+			"style[view_", i, ";border=false]",
+			"style[view_", i, ":hovered;bgimg=", core.formspec_escape(defaulttexturedir .. "button_hover_semitrans.png"), "]",
+			"style[view_", i, ":pressed;bgimg=", core.formspec_escape(defaulttexturedir .. "button_press_semitrans.png"), "]",
+			"button[0,0;", cell_w, ",", cell_h, ";view_", i, ";]",
+		})
+
+		if package.featured then
+			table.insert_all(formspec, {
+				"tooltip[0,0;0.8,0.8;", fgettext("Featured"), "]",
+				"image[0.2,0.2;0.4,0.4;", defaulttexturedir, "server_favorite.png]",
+			})
+		end
+
+		table.insert_all(formspec, {
+			"container[", cell_w - 0.625,",", 0.25, "]",
+		})
+
+		if package.downloading then
+			table.insert_all(formspec, {
+				"animated_image[0,0;0.5,0.5;downloading;", defaulttexturedir, "cdb_downloading.png;3;400;;]",
+			})
+		elseif package.queued then
+			table.insert_all(formspec, {
+				"image[0,0;0.5,0.5;", defaulttexturedir, "cdb_queued.png]",
+			})
+		elseif package.path then
+			if package.installed_release < package.release then
+				table.insert_all(formspec, {
+					"image[0,0;0.5,0.5;", defaulttexturedir, "cdb_update.png]",
+				})
+			else
+				table.insert_all(formspec, {
+					"image[0.1,0.1;0.3,0.3;", defaulttexturedir, "checkbox_64.png]",
+				})
+			end
+		end
+
+		table.insert_all(formspec, {
+			"container_end[]",
+			"container_end[]",
+		})
 	end
 
+	formspec[#formspec + 1] = "container_end[]"
+	formspec[#formspec + 1] = "container_end[]"
+
 	return table.concat(formspec)
 end
 
@@ -287,14 +402,14 @@ local function handle_submit(this, fields)
 	if fields.search or fields.key_enter_field == "search_string" then
 		search_string = fields.search_string:trim()
 		cur_page = 1
-		contentdb.filter_packages(search_string, filter_types_type[filter_type])
+		contentdb.filter_packages(search_string, filter_type)
 		return true
 	end
 
 	if fields.clear then
 		search_string = ""
 		cur_page = 1
-		contentdb.filter_packages("", filter_types_type[filter_type])
+		contentdb.filter_packages("", filter_type)
 		return true
 	end
 
@@ -330,12 +445,11 @@ local function handle_submit(this, fields)
 		return true
 	end
 
-	if fields.type then
-		local new_type = table.indexof(filter_types_titles, fields.type)
-		if new_type ~= filter_type then
-			filter_type = new_type
+	for _, pair in ipairs(filter_type_names) do
+		if fields[pair[1]] then
+			filter_type = pair[2]
 			cur_page = 1
-			contentdb.filter_packages(search_string, filter_types_type[filter_type])
+			contentdb.filter_packages(search_string, filter_type)
 			return true
 		end
 	end
@@ -351,13 +465,14 @@ local function handle_submit(this, fields)
 		return true
 	end
 
+	local num_per_page = this.data.num_per_page
 	local start_idx = (cur_page - 1) * num_per_page + 1
 	assert(start_idx ~= nil)
 	for i=start_idx, math.min(#contentdb.packages, start_idx+num_per_page-1) do
 		local package = contentdb.packages[i]
 		assert(package)
 
-		if fields["view_" .. i] then
+		if fields["view_" .. i] or fields["title_" .. i] or fields["author_" .. i] then
 			local dlg = create_package_dialog(package)
 			dlg:set_parent(this)
 			this:hide()
@@ -372,8 +487,8 @@ end
 
 local function handle_events(event)
 	if event == "DialogShow" then
-		-- On touchscreen, don't show the "MINETEST" header behind the dialog.
-		mm_game_theme.set_engine(core.settings:get_bool("touch_gui"))
+		-- Don't show the "MINETEST" header behind the dialog.
+		mm_game_theme.set_engine(true)
 
 		-- If ContentDB is already loaded, auto-install packages here.
 		do_auto_install()
@@ -395,17 +510,7 @@ end
 function create_contentdb_dlg(type, install_spec)
 	search_string = ""
 	cur_page = 1
-	if type then
-		-- table.indexof does not work on tables that contain `nil`
-		for i, v in pairs(filter_types_type) do
-			if v == type then
-				filter_type = i
-				break
-			end
-		end
-	else
-		filter_type = 1
-	end
+	filter_type = type
 
 	-- Keep the old auto_install_spec if the caller doesn't specify one.
 	if install_spec then
@@ -414,8 +519,10 @@ function create_contentdb_dlg(type, install_spec)
 
 	load()
 
-	return dialog_create("contentdb",
+	local dlg = dialog_create("contentdb",
 			get_formspec,
 			handle_submit,
 			handle_events)
+	dlg.data.num_per_page = calculate_num_per_page()
+	return dlg
 end
diff --git a/builtin/mainmenu/content/dlg_package.lua b/builtin/mainmenu/content/dlg_package.lua
index 78bdf2e71..5b9db4860 100644
--- a/builtin/mainmenu/content/dlg_package.lua
+++ b/builtin/mainmenu/content/dlg_package.lua
@@ -32,11 +32,8 @@ end
 
 
 local function get_formspec(data)
-	-- Padding is increased on Android to account for notches
-	-- TODO: use Android API to determine size of cut outs
-	local window_padding = { x = PLATFORM == "Android" and 1 or 0.5, y = PLATFORM == "Android" and 0.25 or 0.5 }
-	local window = core.get_window_info()
-	local size = { x = window.max_formspec_size.x, y = window.max_formspec_size.y }
+	local window_padding =  contentdb.get_formspec_padding()
+	local size = contentdb.get_formspec_size()
 	size.x = math.min(size.x, 20)
 	local W = size.x - window_padding.x * 2
 	local H = size.y - window_padding.y * 2
diff --git a/textures/base/pack/button_hover_semitrans.png b/textures/base/pack/button_hover_semitrans.png
new file mode 100644
index 000000000..5cf294ead
Binary files /dev/null and b/textures/base/pack/button_hover_semitrans.png differ
diff --git a/textures/base/pack/button_press_semitrans.png b/textures/base/pack/button_press_semitrans.png
new file mode 100644
index 000000000..ba0ddd510
Binary files /dev/null and b/textures/base/pack/button_press_semitrans.png differ
diff --git a/textures/base/pack/cdb_add.png b/textures/base/pack/cdb_add.png
deleted file mode 100644
index 3e3d067e3..000000000
Binary files a/textures/base/pack/cdb_add.png and /dev/null differ
diff --git a/textures/base/pack/cdb_clear.png b/textures/base/pack/cdb_clear.png
deleted file mode 100644
index d5df4a067..000000000
Binary files a/textures/base/pack/cdb_clear.png and /dev/null differ
diff --git a/textures/base/pack/cdb_viewonline.png b/textures/base/pack/cdb_viewonline.png
deleted file mode 100644
index ae2a146b8..000000000
Binary files a/textures/base/pack/cdb_viewonline.png and /dev/null differ