diff --git a/integration/projects_page_test.py b/integration/projects_page_test.py
index 3022455..9003820 100644
--- a/integration/projects_page_test.py
+++ b/integration/projects_page_test.py
@@ -125,7 +125,7 @@ def test_projects_page_loads_without_auth(tmp_path: Path) -> None:
 				page = browser.new_page()
 				page.goto(base, wait_until="domcontentloaded")
 				sync_api.expect(page).to_have_title("PyGitWeb - Projects")
-				sync_api.expect(page.get_by_role("heading", name="Project List")).to_be_visible()
+				sync_api.expect(page.get_by_role("heading", name="Projects")).to_be_visible()
 				sync_api.expect(page.get_by_role("link", name="demo", exact=True)).to_be_visible()
 			finally:
 				browser.close()
diff --git a/pygitweb/src/pygitweb/actions.py b/pygitweb/src/pygitweb/actions.py
index ea327b4..ed77ea1 100644
--- a/pygitweb/src/pygitweb/actions.py
+++ b/pygitweb/src/pygitweb/actions.py
@@ -6,6 +6,7 @@ Ported from gitweb/gitweb.perl action handlers. Invoked by main.dispatch.
 from __future__ import annotations
 
 import base64
+import json
 import mimetypes
 import os
 from datetime import UTC, datetime, timedelta, timezone
@@ -16,6 +17,7 @@ from urllib.parse import parse_qsl, quote, urlencode
 import pygit2
 from fastapi import HTTPException, Request
 from fastapi.responses import HTMLResponse, JSONResponse, PlainTextResponse, RedirectResponse, Response
+from pygittools.merge import MR_REF_PREFIX, MergeRequestStatus, get_merge_request_by_oid
 from python_ripgrep import PySortMode, PySortModeKind
 from python_ripgrep import search as rg_search
 
@@ -41,7 +43,11 @@ from pygitweb.git_helpers import (
 	parse_tag,
 )
 from pygitweb.hooks_install import list_installable_hooks, list_installed_hooks
-from pygitweb.merge_requests import merge_request_tag_response, resolve_merge_request_tag_to_tip
+from pygitweb.merge_requests import (
+	merge_request_status_fields,
+	merge_request_tag_response,
+	resolve_merge_request_tag_to_tip,
+)
 from pygitweb.projects import git_get_project_owner
 from pygitweb.templates_env import (
 	Cell,
@@ -311,6 +317,7 @@ def _table_page(
 	pagination: dict[str, Any] | None = None,
 	empty_message: str | None = None,
 	empty_muted: bool = False,
+	nav: str | None = None,
 ) -> HTMLResponse:
 	ctx: dict[str, object] = {
 		"title": title,
@@ -319,6 +326,7 @@ def _table_page(
 		"pagination": pagination,
 		"empty_message": empty_message,
 		"empty_muted": empty_muted,
+		"nav": nav or ("projects" if project else None),
 	}
 	if cols is not None:
 		ctx["cols"] = cols
@@ -574,6 +582,62 @@ def _hooks_context(project: str, project_enc: str) -> dict[str, Any]:
 	}
 
 
+_OPEN_MR_BADGE = {
+	"can_fast_forward": "green",
+	"blocked_diverged": "yellow",
+	"ours_ref_missing": "red",
+	"mr_head_missing": "red",
+	"error": "red",
+}
+
+
+def _ref_short(ref: str | None) -> str:
+	if not ref:
+		return ""
+	if ref.startswith("refs/heads/"):
+		return ref.removeprefix("refs/heads/")
+	return ref
+
+
+def _open_merge_requests(project: str) -> list[dict[str, str]]:
+	"""Merge requests whose head is not already contained in the destination branch."""
+	try:
+		repo = open_repo(project)
+	except (OSError, pygit2.GitError, ValueError):
+		return []
+	project_enc = quote(project, safe="/")
+	open_mrs: list[dict[str, str]] = []
+	for ref_name in repo.references:
+		if not ref_name.startswith(MR_REF_PREFIX):
+			continue
+		try:
+			oid = repo.references[ref_name].resolve().target
+			obj = repo[oid]
+			if not isinstance(obj, pygit2.Tag):
+				continue
+			mr = get_merge_request_by_oid(repo, pygit2.Oid(hex=str(oid)))
+		except (ValueError, KeyError, TypeError, json.JSONDecodeError, pygit2.GitError):
+			continue
+		try:
+			status = mr.check_status(repo)
+		except ValueError:
+			status = None
+		if status == MergeRequestStatus.UP_TO_DATE:
+			continue
+		fields = merge_request_status_fields(repo, mr)
+		status_code = str(fields["status_code"])
+		open_mrs.append({
+			"title": mr.title or ref_name.removeprefix(MR_REF_PREFIX),
+			"href": f"/project/{project_enc}?a=tag&h={quote(ref_name, safe='')}",
+			"theirs": _ref_short(mr.theirs),
+			"ours": _ref_short(mr.ours),
+			"status_label": str(fields["status_label"]),
+			"badge": _OPEN_MR_BADGE.get(status_code, "secondary"),
+		})
+	open_mrs.sort(key=lambda item: item["title"].lower())
+	return open_mrs
+
+
 def _merge_request_context(project: str) -> dict[str, Any] | None:
 	heads = git_get_heads_list(project)
 	if not heads:
@@ -658,7 +722,9 @@ def git_summary(project: str, extra_rows: list[list[Cell]] | None = None) -> HTM
 		board=board_link_spec(project),
 		hooks=_hooks_context(project, project_enc),
 		merge_request=_merge_request_context(project),
+		open_merge_requests=_open_merge_requests(project),
 		extra_rows=extra_rows or [],
+		nav="projects",
 	)
 
 
diff --git a/pygitweb/src/pygitweb/main.py b/pygitweb/src/pygitweb/main.py
index b946a45..c10fd06 100644
--- a/pygitweb/src/pygitweb/main.py
+++ b/pygitweb/src/pygitweb/main.py
@@ -116,6 +116,7 @@ from pygitweb.tasks import (
 	board_router,
 	comment_router,
 	create_board_for_project,
+	get_all_project_tasks,
 	get_board_tasks_grouped,
 	list_project_boards,
 	task_router,
@@ -248,6 +249,46 @@ def _project_subpage_rows(project: str) -> list[list[Cell]]:
 
 # ---------- Routes (no project) ----------
 
+_OPEN_TASK_STATUSES = frozenset({"TODO", "IN_PROGRESS", "IN_REVIEW"})
+
+
+def _home_stats(projects: list[dict[str, object]]) -> dict[str, int]:
+	"""Counts for the project-list dashboard cards."""
+	from pygitweb.capacity_config import current_iso_week, get_user_week_capacity
+
+	visible = {str(pr.get("path") or "") for pr in projects if pr.get("path")}
+	points_by_assignee: dict[str, int] = {}
+	open_tasks = 0
+	for path in visible:
+		try:
+			tasks = get_all_project_tasks(path)
+		except Exception:
+			continue
+		for task in tasks:
+			if task.get("status") not in _OPEN_TASK_STATUSES:
+				continue
+			open_tasks += 1
+			assignee = str(task.get("assignee") or "").strip()
+			if not assignee:
+				continue
+			points = task.get("points") or 0
+			points_by_assignee[assignee] = points_by_assignee.get(assignee, 0) + int(points)
+	week = current_iso_week()
+	over_capacity = 0
+	for assignee, points in points_by_assignee.items():
+		capacity = get_user_week_capacity(assignee, week)
+		if capacity is not None and points > capacity:
+			over_capacity += 1
+	recent_commits = sum(
+		1 for item in get_all_timeline_events() if item["project"] in visible and item["event"]["kind"] == "commit"
+	)
+	return {
+		"projects": len(visible),
+		"open_tasks": open_tasks,
+		"over_capacity": over_capacity,
+		"recent_commits": recent_commits,
+	}
+
 
 @app.get("/", response_class=HTMLResponse)
 def git_project_list(
@@ -288,6 +329,8 @@ def git_project_list(
 		title=f"{settings.SITE_NAME} - Projects",
 		notice=notice,
 		projects=projects,
+		stats=_home_stats(list_),
+		nav="projects",
 	)
 
 
@@ -332,18 +375,17 @@ def activity_page(
 	has_next = len(events_page) > pc
 	if has_next:
 		events_page = events_page[:pc]
-	rows: list[list[Cell]] = []
+	events: list[dict[str, str]] = []
 	for item in events_page:
 		project_name = item["project"]
 		event = item["event"]
 		oid = event["oid"]
-		if event["kind"] == "commit":
-			event_label: Cell = html_link(
-				f"/project/{quote(project_name, safe='/')}?a=commit&h={quote(oid, safe='')}",
-				oid[:7],
-			)
-		else:
-			event_label = event["kind"] or ""
+		kind = event["kind"] or ""
+		event_href = ""
+		label: Literal["commit", "ref"] | str = kind
+		if kind == "commit":
+			label = oid[:7]
+			event_href = f"/project/{quote(project_name, safe='/')}?a=commit&h={quote(oid, safe='')}"
 		description_raw = event.get("description")
 		if not description_raw:
 			description = ""
@@ -358,12 +400,14 @@ def activity_page(
 			activity_time = "right now" if age_seconds <= 0 else age_string(age_seconds)
 		except (ValueError, OSError):
 			activity_time = ""
-		rows.append([
-			activity_time,
-			html_link(f"/project/{quote(project_name, safe='/')}", project_name),
-			event_label,
-			description,
-		])
+		events.append({
+			"time": activity_time,
+			"project": project_name,
+			"project_href": f"/project/{quote(project_name, safe='/')}",
+			"label": label,
+			"event_href": event_href,
+			"description": description,
+		})
 	prev_url = ""
 	next_url = ""
 	if p > 1:
@@ -371,11 +415,10 @@ def activity_page(
 	if has_next:
 		next_url = f"/activity?{urlencode({'page': str(p + 1), 'pagecount': str(pc)})}"
 	return render_page(
-		"table_page.html",
+		"activity.html",
 		title=f"{settings.SITE_NAME} - Activity",
-		heading="Activity",
-		cols=["Time", "Project Name", "Event", "Description"],
-		rows=rows,
+		events=events,
+		nav="activity",
 		pagination={
 			"current_page": p,
 			"pagecount": pc,
@@ -427,6 +470,7 @@ def boards_overview_page(
 		rows=rows,
 		empty_message="No projects found." if not rows else None,
 		empty_muted=True,
+		nav="boards",
 	)
 
 
@@ -497,6 +541,7 @@ def addproject_page(
 		show_sign_in_notice=show_sign_in_notice,
 		empty_repo_form_content=Project.form_content(),
 		upload_zip_form_content=ProjectZip.form_content(),
+		nav="add",
 	)
 
 
@@ -577,6 +622,7 @@ def project_board(
 		project=project,
 		project_url=project_url,
 		board_name=board_name,
+		nav="boards",
 		board_url=board_url,
 		columns=columns,
 	)
diff --git a/pygitweb/src/pygitweb/merge_requests_test.py b/pygitweb/src/pygitweb/merge_requests_test.py
index 13d6bdd..da0e4f3 100644
--- a/pygitweb/src/pygitweb/merge_requests_test.py
+++ b/pygitweb/src/pygitweb/merge_requests_test.py
@@ -59,6 +59,24 @@ class TestMergeRequestTagView:
 				"tip_b": str(b),
 			}
 
+	def test_summary_lists_open_merge_request_until_it_is_merged(self, mr_env: dict[str, str]) -> None:
+		client = mr_env["client"]
+		before = client.get(f"/project/{mr_env['project']}")
+		assert before.status_code == 200
+		assert "Feature MR" in before.text
+		assert "Can fast-forward" in before.text
+		assert "step-item" not in before.text
+		post = client.post(
+			"/mr/ff",
+			params={"project": mr_env["project"], "h": mr_env["mr_tag_oid"]},
+			follow_redirects=False,
+		)
+		assert post.status_code == 303
+		after = client.get(f"/project/{mr_env['project']}")
+		assert after.status_code == 200
+		assert "Feature MR" not in after.text
+		assert "No open merge requests." in after.text
+
 	def test_tag_action_renders_merge_request_template(self, mr_env: dict[str, str]) -> None:
 		client = mr_env["client"]
 		resp = client.get(
diff --git a/pygitweb/src/pygitweb/project_list_routes_test.py b/pygitweb/src/pygitweb/project_list_routes_test.py
index 7d37dc8..c2f348f 100644
--- a/pygitweb/src/pygitweb/project_list_routes_test.py
+++ b/pygitweb/src/pygitweb/project_list_routes_test.py
@@ -52,5 +52,7 @@ def test_home_page_shows_auth_visibility_notice_when_no_readable_projects(
 	client = TestClient(app)
 	resp = client.get("/")
 	assert resp.status_code == 200
-	assert "No projects are visible with your current authentication" in resp.text
-	assert "<table" in resp.text
+	assert "No projects are visible" in resp.text
+	assert "Sign in or ask an administrator for repository access." in resp.text
+	assert 'href="/login?next=/"' in resp.text
+	assert "<table" not in resp.text
diff --git a/pygitweb/src/pygitweb/settings.py b/pygitweb/src/pygitweb/settings.py
index a8de0b9..2c5e3f7 100644
--- a/pygitweb/src/pygitweb/settings.py
+++ b/pygitweb/src/pygitweb/settings.py
@@ -313,6 +313,7 @@ def settings_pygitweb_page(request: Request):
 		title=f"{settings.SITE_NAME} - PyGitWeb settings",
 		form_body=form_body,
 		plugin_rows=plugin_rows,
+		nav="settings",
 	)
 
 
@@ -377,6 +378,7 @@ def settings_pygit2_page(request: Request):
 		"pygit2_settings.html",
 		title=f"{settings.SITE_NAME} - PyGit2 settings",
 		form_body=form_body,
+		nav="settings",
 	)
 
 
diff --git a/pygitweb/src/pygitweb/static/main.css b/pygitweb/src/pygitweb/static/main.css
index 5fe8547..775aa36 100644
--- a/pygitweb/src/pygitweb/static/main.css
+++ b/pygitweb/src/pygitweb/static/main.css
@@ -956,9 +956,9 @@ pre {
 	margin-right: auto;
 }
 
-/* Board view: center the column group when there's space; still scroll when many columns */
+/* Center the columns when they fit. `safe` falls back to the start when they overflow, so the first column stays reachable by scrolling left. */
 .board-columns {
-	justify-content: center;
+	justify-content: safe center;
 }
 
 /* Board view columns: use theme variables so they respect light/dark */
diff --git a/pygitweb/src/pygitweb/static/navbar-auth.js b/pygitweb/src/pygitweb/static/navbar-auth.js
index 964c0a0..ac49e8b 100644
--- a/pygitweb/src/pygitweb/static/navbar-auth.js
+++ b/pygitweb/src/pygitweb/static/navbar-auth.js
@@ -11,7 +11,27 @@
 	var avatar = document.getElementById("navbar-user-avatar");
 	var nameEl = nav.querySelector(".navbar-user-name");
 	var metaEl = nav.querySelector(".navbar-user-meta");
+	var menu = document.getElementById("navbar-user-menu");
+	var profile = document.getElementById("navbar-user-profile");
+	var logout = document.getElementById("navbar-user-logout");
 	if (!link || !avatar || !nameEl || !metaEl) return;
+	function showAccountMenu() {
+		if (!menu) return;
+		nav.classList.add("dropdown", "dropup");
+		link.setAttribute("data-bs-toggle", "dropdown");
+		link.setAttribute("aria-expanded", "false");
+		link.href = "#";
+		menu.classList.remove("d-none");
+		if (profile) profile.href = "/user";
+		if (logout) logout.href = "/logout?next=/";
+	}
+	function hideAccountMenu() {
+		if (!menu) return;
+		nav.classList.remove("dropdown", "dropup");
+		link.removeAttribute("data-bs-toggle");
+		link.removeAttribute("aria-expanded");
+		menu.classList.add("d-none");
+	}
 	function resetGuestAvatar() {
 		avatar.classList.remove("bg-primary", "bg-teal", "text-white");
 		avatar.classList.add("bg-secondary");
@@ -47,6 +67,7 @@
 		})
 		.then(function (s) {
 			if (!s.auth_enabled) {
+				hideAccountMenu();
 				link.removeAttribute("href");
 				link.setAttribute("aria-disabled", "true");
 				link.classList.add("pe-none", "opacity-75");
@@ -61,6 +82,7 @@
 				link.href = "/user";
 				nameEl.textContent = s.username;
 				metaEl.textContent = "Signed in";
+				showAccountMenu();
 				var isAdmin = String(s.username).toLowerCase() === "admin";
 				if (s.gravatar_url) {
 					setGravatarAvatar(s.gravatar_url, isAdmin);
@@ -70,12 +92,14 @@
 				return;
 			}
 			nav.setAttribute("data-logged-in", "false");
+			hideAccountMenu();
 			link.href = "/login";
 			nameEl.textContent = "Guest";
 			metaEl.textContent = "Not signed in";
 			resetGuestAvatar();
 		})
 		.catch(function () {
+			hideAccountMenu();
 			link.href = "/login";
 			resetGuestAvatar();
 		});
diff --git a/pygitweb/src/pygitweb/static_tests/test_navbar-auth.py b/pygitweb/src/pygitweb/static_tests/test_navbar-auth.py
index 17a99d2..8c845f3 100644
--- a/pygitweb/src/pygitweb/static_tests/test_navbar-auth.py
+++ b/pygitweb/src/pygitweb/static_tests/test_navbar-auth.py
@@ -71,6 +71,27 @@ def test_signed_out_visitor_links_to_login(js: JsPage) -> None:
 	assert "bg-secondary" in js.classes("#navbar-user-avatar")
 
 
+def test_signed_in_user_gets_profile_and_sign_out(js: JsPage) -> None:
+	js.route("/auth/status", body={"auth_enabled": True, "username": "ada"})
+	js.set_html(
+		'<div id="navbar-user">'
+		'<a id="navbar-user-link" href="/login"></a>'
+		'<span id="navbar-user-avatar" class="bg-secondary"></span>'
+		'<div class="navbar-user-name"></div>'
+		'<div class="navbar-user-meta"></div>'
+		'<div id="navbar-user-menu" class="d-none">'
+		'<a id="navbar-user-profile" href="/user"></a>'
+		'<a id="navbar-user-logout" href="/logout"></a>'
+		"</div></div>"
+	)
+	js.load("navbar-auth.js")
+	assert "dropdown" in js.classes("#navbar-user")
+	assert js.attr("#navbar-user-link", "data-bs-toggle") == "dropdown"
+	assert js.prop("#navbar-user-profile", "href").endswith("/user")
+	assert "next=/" in js.prop("#navbar-user-logout", "href")
+	assert "d-none" not in js.classes("#navbar-user-menu")
+
+
 def test_status_fetch_failure_sends_the_link_to_login(js: JsPage) -> None:
 	js.route("/auth/status", error="offline")
 	js.set_html(_NAV)
diff --git a/pygitweb/src/pygitweb/tasks.py b/pygitweb/src/pygitweb/tasks.py
index ca2f349..587ea90 100644
--- a/pygitweb/src/pygitweb/tasks.py
+++ b/pygitweb/src/pygitweb/tasks.py
@@ -41,7 +41,7 @@ from pygitweb.dependencies import ValidatedReadableQueryProject, filter_projects
 from pygitweb.gravatar import gravatar_url
 from pygitweb.permissions import Permission, PermissionPrincipal
 from pygitweb.projects import git_get_projects_list
-from pygitweb.templates_env import Cell, html_link, render_page
+from pygitweb.templates_env import Cell, html_link, person_label, priority_badge, render_page, status_badge
 
 # Well-known empty tree OID for boards/tasks when repo has no HEAD
 EMPTY_TREE_OID = pygit2.Oid(hex="4b825dc642cb6eb9a060e54bf8d69288fbee4904")
@@ -350,9 +350,9 @@ def all_tasks_page(
 				html_link(task_url, t.get("title") or "(untitled)"),
 				html_link(project_url, project),
 				t.get("board") or "",
-				task_status.replace("_", " ").title(),
-				(t.get("priority") or "LOW").title(),
-				t.get("assignee") or "Unassigned",
+				status_badge(task_status),
+				priority_badge(t.get("priority") or "LOW"),
+				person_label(t.get("assignee") or ""),
 				str(t.get("points") if t.get("points") is not None else ""),
 			])
 	statuses = [
@@ -366,6 +366,7 @@ def all_tasks_page(
 		statuses=statuses,
 		cols=["Task", "Project", "Board", "Status", "Priority", "Assignee", "Points"],
 		rows=rows,
+		nav="tasks",
 	)
 
 
diff --git a/pygitweb/src/pygitweb/team.py b/pygitweb/src/pygitweb/team.py
index 8a2401e..01c4bd8 100644
--- a/pygitweb/src/pygitweb/team.py
+++ b/pygitweb/src/pygitweb/team.py
@@ -109,10 +109,14 @@ def team_page(
 			])
 		can_edit = (not is_unassigned) and (can_edit_others or assignee == current_username)
 		capacity_url = f"/team/capacity?user={quote(assignee, safe='')}&week={quote(iso_week, safe='')}"
+		capacity_pct = 0
+		if capacity:
+			capacity_pct = min(100, round(100 * points_total / capacity))
 		groups.append({
 			"assignee": assignee,
 			"points_total": points_total,
 			"capacity": capacity,
+			"capacity_pct": capacity_pct,
 			"over_capacity": capacity is not None and points_total > capacity,
 			"can_edit": can_edit,
 			"capacity_url": capacity_url,
@@ -124,6 +128,7 @@ def team_page(
 		title=f"{settings.SITE_NAME} - Team",
 		iso_week=iso_week,
 		groups=groups,
+		nav="team",
 	)
 
 
@@ -149,6 +154,7 @@ def team_capacity_form(
 		target_user=target_user,
 		iso_week=iso_week,
 		current_points=current_points,
+		nav="team",
 	)
 
 
diff --git a/pygitweb/src/pygitweb/templates/activity.html b/pygitweb/src/pygitweb/templates/activity.html
new file mode 100644
index 0000000..fdf71aa
--- /dev/null
+++ b/pygitweb/src/pygitweb/templates/activity.html
@@ -0,0 +1,44 @@
+{% extends "base.html" %}
+{% from "macros.html" import page_header %}
+{% block content %}
+{{ page_header("Activity", "Timeline") }}
+{% if events %}
+<div class="card">
+  <div class="list-group list-group-flush">
+    {% for event in events %}
+    <div class="list-group-item">
+      <div class="row align-items-center">
+        <div class="col-auto">
+          <span class="avatar avatar-sm">{{ event.project | initials }}</span>
+        </div>
+        <div class="col text-truncate">
+          <a href="{{ event.project_href }}" class="text-reset d-block">{{ event.project }}</a>
+          <div class="text-secondary text-truncate">
+            {% if event.event_href %}
+            <a href="{{ event.event_href }}">{{ event.label }}</a>
+            {% else %}
+            <span class="badge bg-secondary-lt">{{ event.label }}</span>
+            {% endif %}
+            {% if event.description %}
+            <span class="ms-1">{{ event.description }}</span>
+            {% endif %}
+          </div>
+        </div>
+        <div class="col-auto text-secondary">{{ event.time }}</div>
+      </div>
+    </div>
+    {% endfor %}
+  </div>
+</div>
+{% else %}
+<div class="empty">
+  <p class="empty-title">No activity yet</p>
+  <p class="empty-subtitle text-secondary">Commits and ref updates from repositories you can read show up here.</p>
+</div>
+{% endif %}
+{% if pagination %}
+{% with current_page=pagination.current_page, pagecount=pagination.pagecount, has_prev=pagination.has_prev, has_next=pagination.has_next, prev_url=pagination.prev_url, next_url=pagination.next_url, total_pages=pagination.total_pages, page_links=pagination.page_links %}
+{% include "pagination.html" %}
+{% endwith %}
+{% endif %}
+{% endblock %}
diff --git a/pygitweb/src/pygitweb/templates/addproject.html b/pygitweb/src/pygitweb/templates/addproject.html
index 92b1c32..04d9869 100644
--- a/pygitweb/src/pygitweb/templates/addproject.html
+++ b/pygitweb/src/pygitweb/templates/addproject.html
@@ -1,6 +1,7 @@
 {% extends "base.html" %}
+{% from "macros.html" import page_header %}
 {% block content %}
-<h1 class="page-title">Add project</h1>
+{{ page_header("Add project", "Repositories") }}
 {% if show_sign_in_notice %}
 <div class="alert board-task-error-hint py-2 px-3 mb-3" role="alert">
   You must be signed in to add a project (sign in via the sidebar or <a href="/login?next=/addproject">open the sign-in page</a>).
diff --git a/pygitweb/src/pygitweb/templates/base.html b/pygitweb/src/pygitweb/templates/base.html
index c928beb..bf720fd 100644
--- a/pygitweb/src/pygitweb/templates/base.html
+++ b/pygitweb/src/pygitweb/templates/base.html
@@ -12,7 +12,7 @@
 </head><body>
 <div class="page">
   <aside class="navbar navbar-vertical navbar-expand-sm position-fixed">
-    <div class="container-fluid d-flex flex-column vh-100 overflow-y-auto">
+    <div class="container-fluid d-flex flex-column vh-100">
       <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#sidebar-menu" aria-controls="sidebar-menu" aria-expanded="false" aria-label="Toggle navigation">
         <span class="navbar-toggler-icon"></span>
       </button>
@@ -22,34 +22,34 @@
           <span class="nav-link-title">{{ site_name | default('PyGitWeb') }}</span>
         </a>
       </h1>
-      <div class="collapse navbar-collapse flex-grow-1" id="sidebar-menu">
+      <div class="collapse navbar-collapse flex-grow-1 overflow-y-auto" id="sidebar-menu">
         <ul class="navbar-nav pt-lg-3">
           <li class="nav-item">
-            <a class="nav-link" href="/">
+            <a class="nav-link{% if nav == 'projects' %} active{% endif %}" href="/" {% if nav == 'projects' %}aria-current="page"{% endif %}>
               <span class="nav-link-icon"><svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-folder" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M5 4h4l3 3h7a2 2 0 0 1 2 2v8a2 2 0 0 1 -2 2h-14a2 2 0 0 1 -2 -2v-11a2 2 0 0 1 2 -2z" /></svg></span>
               <span class="nav-link-title">Projects</span>
             </a>
           </li>
           <li class="nav-item">
-            <a class="nav-link" href="/addproject">
+            <a class="nav-link{% if nav == 'add' %} active{% endif %}" href="/addproject" {% if nav == 'add' %}aria-current="page"{% endif %}>
               <span class="nav-link-icon"><svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-plus" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M12 5l0 14" /><path d="M5 12l14 0" /></svg></span>
               <span class="nav-link-title">Add project</span>
             </a>
           </li>
           <li class="nav-item">
-            <a class="nav-link" href="/boards">
+            <a class="nav-link{% if nav == 'boards' %} active{% endif %}" href="/boards" {% if nav == 'boards' %}aria-current="page"{% endif %}>
               <span class="nav-link-icon"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-layout-kanban"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M4 4m0 1a1 1 0 0 1 1 -1h14a1 1 0 0 1 1 1v14a1 1 0 0 1 -1 1h-14a1 1 0 0 1 -1 -1z" /><path d="M9 8v7" /><path d="M15 8v4" /></svg></span>
               <span class="nav-link-title">Boards</span>
             </a>
           </li>
           <li class="nav-item">
-            <a class="nav-link" href="/activity">
+            <a class="nav-link{% if nav == 'activity' %} active{% endif %}" href="/activity" {% if nav == 'activity' %}aria-current="page"{% endif %}>
               <span class="nav-link-icon"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-activity"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M3 12h4l3 8l4 -16l3 8h4" /></svg></span>
               <span class="nav-link-title">Activity</span>
             </a>
           </li>
           <li class="nav-item">
-            <a class="nav-link" href="/team">
+            <a class="nav-link{% if nav == 'team' %} active{% endif %}" href="/team" {% if nav == 'team' %}aria-current="page"{% endif %}>
               <span class="nav-link-icon"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-users"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M9 7m-4 0a4 4 0 1 0 8 0a4 4 0 1 0 -8 0" /><path d="M3 21v-2a4 4 0 0 1 4 -4h4a4 4 0 0 1 4 4v2" /><path d="M16 3.13a4 4 0 0 1 0 7.75" /><path d="M21 21v-2a4 4 0 0 0 -3 -3.85" /></svg></span>
               <span class="nav-link-title">Team</span>
             </a>
@@ -61,13 +61,13 @@
             </a>
           </li>
           <li class="nav-item">
-            <a class="nav-link" href="/tasks">
+            <a class="nav-link{% if nav == 'tasks' %} active{% endif %}" href="/tasks" {% if nav == 'tasks' %}aria-current="page"{% endif %}>
               <span class="nav-link-icon"><svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-checklist" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M9 11l3 3l8 -8" /><path d="M20 12v6a2 2 0 0 1 -2 2h-12a2 2 0 0 1 -2 -2v-12a2 2 0 0 1 2 -2h9" /></svg></span>
               <span class="nav-link-title">Tasks</span>
             </a>
           </li>
           <li class="nav-item dropdown">
-            <a class="nav-link dropdown-toggle" href="#" data-bs-toggle="dropdown" data-bs-auto-close="outside" aria-expanded="false" role="button">
+            <a class="nav-link dropdown-toggle{% if nav == 'settings' %} active{% endif %}" href="#" data-bs-toggle="dropdown" data-bs-auto-close="outside" aria-expanded="false" role="button" {% if nav == 'settings' %}aria-current="page"{% endif %}>
               <span class="nav-link-icon"><svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-settings" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M10.325 4.317c.426 -1.756 2.924 -1.756 3.35 0a1.724 1.724 0 0 0 2.573 1.066c1.543 -.94 3.31 .826 2.37 2.37a1.724 1.724 0 0 0 1.065 2.572c1.756 .426 1.756 2.924 0 3.35a1.724 1.724 0 0 0 -1.066 2.573c.94 1.543 -.826 3.31 -2.37 2.37a1.724 1.724 0 0 0 -2.572 1.065c-.426 1.756 -2.924 1.756 -3.35 0a1.724 1.724 0 0 0 -2.573 -1.066c-1.543 .94 -3.31 -.826 -2.37 -2.37a1.724 1.724 0 0 0 -1.065 -2.572c-1.756 -.426 -1.756 -2.924 0 -3.35a1.724 1.724 0 0 0 1.066 -2.573c-.94 -1.543 .826 -3.31 2.37 -2.37c1 .608 2.296 .07 2.572 -1.065z" /><path d="M9 12a3 3 0 1 0 6 0a3 3 0 0 0 -6 0" /></svg></span>
               <span class="nav-link-title">Settings</span>
             </a>
@@ -88,6 +88,10 @@
             <div class="navbar-user-meta text-muted small">Not signed in</div>
           </div>
         </a>
+        <div class="dropdown-menu d-none" id="navbar-user-menu">
+          <a class="dropdown-item" id="navbar-user-profile" href="/user">Profile</a>
+          <a class="dropdown-item" id="navbar-user-logout" href="/logout?next=/">Sign out</a>
+        </div>
       </div>
     </div>
   </aside>
diff --git a/pygitweb/src/pygitweb/templates/board.html b/pygitweb/src/pygitweb/templates/board.html
index 19b44c7..0380fb2 100644
--- a/pygitweb/src/pygitweb/templates/board.html
+++ b/pygitweb/src/pygitweb/templates/board.html
@@ -6,22 +6,15 @@
   {% include "board_card.html" without context %}
 </template>
 
-<h1 class="page-title d-flex align-items-center gap-2 flex-wrap">
-  <a href="{{ project_url }}" class="text-reset">{{ project }}</a>
-  <span class="text-muted">/</span>
-  <label class="visually-hidden" for="board-board-select">Board</label>
-  <select id="board-board-select" class="form-select form-select-sm board-board-select" aria-label="Board" style="width: auto; min-width: 10rem;">
-    <option value="{{ board_name }}" selected>{{ board_name }}</option>
-  </select>
-  <span class="text-muted">board</span>
-</h1>
-
-<div class="d-flex flex-wrap align-items-center justify-content-end gap-2 mb-3 board-toolbar">
-  <div class="d-flex align-items-center gap-2">
-    <button type="button" class="btn btn-sm btn-outline-secondary" id="board-new-board-btn">+ Board</button>
-    <button type="button" class="btn btn-sm btn-outline-primary" id="board-new-sprint-btn">+ Sprint</button>
-  </div>
-</div>
+{% from "macros.html" import page_header %}
+{% call page_header(project, "Board") %}
+<label class="visually-hidden" for="board-board-select">Board</label>
+<select id="board-board-select" class="form-select board-board-select" aria-label="Board" style="width: auto; min-width: 10rem;">
+  <option value="{{ board_name }}" selected>{{ board_name }}</option>
+</select>
+<button type="button" class="btn btn-outline-secondary" id="board-new-board-btn">New board</button>
+<button type="button" class="btn btn-primary" id="board-new-sprint-btn">New sprint</button>
+{% endcall %}
 <form id="board-new-form" class="d-none mb-3 d-flex flex-wrap gap-2 align-items-center" autocomplete="off">
   <input type="text" id="board-new-name" class="form-control form-control-sm" style="max-width: 260px;" placeholder="Board name" required>
   <button type="submit" class="btn btn-sm btn-primary">Create</button>
@@ -31,20 +24,20 @@
 
 <div class="board-columns d-flex gap-3 overflow-auto pb-2" style="min-height: 70vh;" data-project="{{ project }}" data-board="{{ board_name }}">
   {% for col in columns %}
-  <div class="board-column flex-shrink-0 rounded border p-2 dropzone"
+  <div class="card board-column flex-shrink-0 dropzone"
        data-status="{{ col.status }}"
        style="min-width: 280px; width: 280px;">
-    <div class="d-flex align-items-center justify-content-between mb-2">
-      <h3 class="h6 mb-0 text-muted">{{ col.label }}</h3>
+    <div class="card-header d-flex align-items-center justify-content-between">
+      <h3 class="card-title mb-0">{{ col.label }}</h3>
       <div class="d-flex align-items-center gap-1">
-        <span class="badge bg-light text-dark border board-column-points-total" title="Sprint points in this column">{{ col.points_total | default(0) }} pts</span>
-        <button type="button" class="board-column-header-control board-column-add-card" aria-label="Add task">
+        <span class="badge bg-blue-lt board-column-points-total" title="Sprint points in this column">{{ col.points_total | default(0) }} pts</span>
+        <button type="button" class="btn btn-ghost-primary btn-sm board-column-header-control board-column-add-card" aria-label="Add task">
           <span class="board-column-add-label">(+ Add Task)</span>
           <span class="board-column-task-count">{{ col.tasks | length }}</span>
         </button>
       </div>
     </div>
-    <div class="board-column-cards d-flex flex-column">
+    <div class="card-body board-column-cards d-flex flex-column">
       {% for task in col.tasks %}
       {% include "board_card.html" %}
       {% endfor %}
diff --git a/pygitweb/src/pygitweb/templates/login.html b/pygitweb/src/pygitweb/templates/login.html
index 4b2539b..23b8759 100644
--- a/pygitweb/src/pygitweb/templates/login.html
+++ b/pygitweb/src/pygitweb/templates/login.html
@@ -1,7 +1,10 @@
 {% extends "base.html" %}
+{% from "macros.html" import page_header %}
 {% block content %}
-<h1 class="page-title">Sign in</h1>
-<div class="card">
+{{ page_header("Sign in") }}
+<div class="row justify-content-center">
+<div class="col-md-6 col-lg-5">
+<div class="card card-md">
   <div class="card-body">
     {% if error %}
     <div class="alert alert-danger mb-3" role="alert">{{ error }}</div>
@@ -37,4 +40,6 @@
     {% endif %}
   </div>
 </div>
+</div>
+</div>
 {% endblock %}
diff --git a/pygitweb/src/pygitweb/templates/macros.html b/pygitweb/src/pygitweb/templates/macros.html
index 7a55fcd..acd246c 100644
--- a/pygitweb/src/pygitweb/templates/macros.html
+++ b/pygitweb/src/pygitweb/templates/macros.html
@@ -2,6 +2,25 @@
 <a href="{{ href }}"{% if id %} id="{{ id }}"{% endif %}{% if style %} style="{{ style }}"{% endif %}>{{ text }}</a>
 {%- endmacro %}
 
+{% macro page_header(title, pretitle=none) %}
+<div class="page-header d-print-none">
+  <div class="row g-2 align-items-center">
+    <div class="col">
+      {% if pretitle %}<div class="page-pretitle">{{ pretitle }}</div>{% endif %}
+      <h2 class="page-title">{{ title }}</h2>
+    </div>
+    {% if caller is defined %}
+    {% set actions = caller() %}
+    {% if actions|trim %}
+    <div class="col-auto ms-auto">
+      <div class="btn-list">{{ actions }}</div>
+    </div>
+    {% endif %}
+    {% endif %}
+  </div>
+</div>
+{% endmacro %}
+
 {% macro section(title, flush=false, open=true) %}
 <details class="summary-section card mt-3"{% if open %} open{% endif %}>
 <summary class="card-header summary-section-toggle">
diff --git a/pygitweb/src/pygitweb/templates/project_list.html b/pygitweb/src/pygitweb/templates/project_list.html
index 730d1d4..f60079a 100644
--- a/pygitweb/src/pygitweb/templates/project_list.html
+++ b/pygitweb/src/pygitweb/templates/project_list.html
@@ -1,28 +1,87 @@
 {% extends "base.html" %}
-{% from "macros.html" import link %}
+{% from "macros.html" import page_header %}
 {% block content %}
-<h1>Project List</h1>
+{% call page_header("Projects", "Repositories") %}
+<a class="btn btn-primary" href="/addproject">Add project</a>
+{% endcall %}
+{% if not notice and stats %}
+<div class="row row-deck row-cards mb-3">
+  <div class="col-sm-6 col-lg-3">
+    <div class="card card-sm">
+      <div class="card-body">
+        <div class="subheader">Projects</div>
+        <div class="h1 mb-0">{{ stats.projects }}</div>
+        <div class="text-secondary">visible to you</div>
+      </div>
+    </div>
+  </div>
+  <div class="col-sm-6 col-lg-3">
+    <div class="card card-sm">
+      <div class="card-body">
+        <div class="subheader">Open tasks</div>
+        <div class="h1 mb-0">{{ stats.open_tasks }}</div>
+        <div class="text-secondary">todo, in progress, in review</div>
+      </div>
+    </div>
+  </div>
+  <div class="col-sm-6 col-lg-3">
+    <div class="card card-sm">
+      <div class="card-body">
+        <div class="subheader">Over capacity</div>
+        <div class="h1 mb-0{% if stats.over_capacity %} text-red{% endif %}">{{ stats.over_capacity }}</div>
+        <div class="text-secondary">people above this week's points</div>
+      </div>
+    </div>
+  </div>
+  <div class="col-sm-6 col-lg-3">
+    <div class="card card-sm">
+      <div class="card-body">
+        <div class="subheader">Recent commits</div>
+        <div class="h1 mb-0">{{ stats.recent_commits }}</div>
+        <div class="text-secondary">in the activity cache</div>
+      </div>
+    </div>
+  </div>
+</div>
+{% endif %}
 {% if notice == "hidden" %}
-<p class="text-warning">No projects are visible with your current authentication and permissions. Sign in or ask an administrator for repository access.</p>
+<div class="empty">
+  <p class="empty-title">No projects are visible</p>
+  <p class="empty-subtitle text-secondary">Sign in or ask an administrator for repository access.</p>
+  <div class="empty-action">
+    <a href="/login?next=/" class="btn btn-primary">Sign in</a>
+  </div>
+</div>
 {% elif notice == "empty" %}
-<p>No projects found.</p>
+<div class="empty">
+  <p class="empty-title">No projects found</p>
+  <p class="empty-subtitle text-secondary">Create a repository to get started.</p>
+  <div class="empty-action">
+    <a href="/addproject" class="btn btn-primary">Add project</a>
+  </div>
+</div>
+{% elif projects %}
+<div class="card">
+  <div class="card-body p-0">
+    <table class="table table-vcenter card-table">
+      <thead>
+        <tr>
+          <th>Project</th>
+          <th>Description</th>
+          <th>Board</th>
+        </tr>
+      </thead>
+      <tbody>
+        {% for project in projects %}
+        <tr>
+          <td><a href="{{ project.href }}">{{ project.path }}</a></td>
+          <td class="text-secondary">{{ project.descr }}</td>
+          <td><a href="{{ project.board_href }}" class="badge {{ 'bg-secondary-lt' if project.board_text == 'create board' else 'bg-blue-lt' }}"{% if project.board_style %} style="{{ project.board_style }}"{% endif %}>{{ project.board_text }}</a></td>
+        </tr>
+        {% endfor %}
+      </tbody>
+    </table>
+  </div>
+</div>
 {% endif %}
-<table class="table table-vcenter card-table">
-<thead>
-<tr>
-<th>Project</th>
-<th>Description</th>
-<th>Board</th>
-</tr>
-</thead>
-<tbody>
-{% for project in projects %}
-<tr>
-<td>{{ link(project.href, project.path) }}</td>
-<td>{{ project.descr }}</td>
-<td>{{ link(project.board_href, project.board_text, style=project.board_style) }}</td>
-</tr>
-{% endfor %}
-</tbody>
-</table>
 {% endblock %}
diff --git a/pygitweb/src/pygitweb/templates/pygit2_settings.html b/pygitweb/src/pygitweb/templates/pygit2_settings.html
index c6622d6..bd9d265 100644
--- a/pygitweb/src/pygitweb/templates/pygit2_settings.html
+++ b/pygitweb/src/pygitweb/templates/pygit2_settings.html
@@ -1,6 +1,7 @@
 {% extends "base.html" %}
+{% from "macros.html" import page_header %}
 {% block content %}
-<h1 class="page-title">PyGit2 settings</h1>
+{{ page_header("PyGit2 settings", "Settings") }}
 <div class="card">
   <div class="card-body">
     <form action="/settings/pygit2/submit" method="post" class="needs-validation" novalidate>
diff --git a/pygitweb/src/pygitweb/templates/pygitweb_settings.html b/pygitweb/src/pygitweb/templates/pygitweb_settings.html
index bb83e23..7450e94 100644
--- a/pygitweb/src/pygitweb/templates/pygitweb_settings.html
+++ b/pygitweb/src/pygitweb/templates/pygitweb_settings.html
@@ -1,6 +1,7 @@
 {% extends "base.html" %}
+{% from "macros.html" import page_header %}
 {% block content %}
-<h1 class="page-title">PyGitWeb settings</h1>
+{{ page_header("PyGitWeb settings", "Settings") }}
 <div class="card">
   <div class="card-body">
     <form action="/settings/pygitweb/submit" method="post" class="needs-validation" novalidate>
@@ -12,7 +13,8 @@
     </form>
   </div>
 </div>
-<div class="card mt-3">
+<div class="hr-text">Plugins</div>
+<div class="card">
   <div class="card-body">
     <h2 class="card-title">Plugins</h2>
     <p class="text-secondary">Installed plugins are off by default. Enable only those you want active; changes apply immediately.</p>
@@ -35,7 +37,8 @@
     </form>
   </div>
 </div>
-<div class="card mt-3">
+<div class="hr-text">Server</div>
+<div class="card">
   <div class="card-body">
     <h2 class="card-title">Server</h2>
     <p class="text-secondary">Stop the PyGitWeb process gracefully. Long-poll connections are released, then the server exits.</p>
diff --git a/pygitweb/src/pygitweb/templates/readme_card.html b/pygitweb/src/pygitweb/templates/readme_card.html
index 34284e5..9ff2955 100644
--- a/pygitweb/src/pygitweb/templates/readme_card.html
+++ b/pygitweb/src/pygitweb/templates/readme_card.html
@@ -1,7 +1,7 @@
 {% from "macros.html" import section %}
 {% call section("README") %}
 {% if markdown %}
-<div id="readme-container" class="markdown-body" data-readme-b64="{{ readme_b64 }}" data-blob-base="{{ blob_base }}"{% if blob_dir %} data-blob-dir="{{ blob_dir }}"{% endif %}></div>
+<div id="readme-container" class="markdown markdown-body" data-readme-b64="{{ readme_b64 }}" data-blob-base="{{ blob_base }}"{% if blob_dir %} data-blob-dir="{{ blob_dir }}"{% endif %}></div>
 <script type="module" src="/static/readme-render.js"></script>
 {% else %}
 <pre class="readme-plain"><code>{{ plain }}</code></pre>
diff --git a/pygitweb/src/pygitweb/templates/summary.html b/pygitweb/src/pygitweb/templates/summary.html
index f6b6afc..8180888 100644
--- a/pygitweb/src/pygitweb/templates/summary.html
+++ b/pygitweb/src/pygitweb/templates/summary.html
@@ -1,83 +1,79 @@
 {% extends "base.html" %}
-{% from "macros.html" import link, section %}
+{% from "macros.html" import link, page_header, section %}
 {% block head %}
 <script defer src="/static/vendor/showdown-2.1.0.min.js"></script>
 {% endblock %}
 {% block content %}
-<h1>{{ project }}</h1>
+{% call page_header(project, "Project") %}
+<a class="btn btn-primary" href="{{ project_url_enc }}/search">Search</a>
+{% endcall %}
 {% if descr %}
-<p>{{ descr }}</p>
-{% endif %}
-{% call section("Project", flush=true) %}
-<table class="table table-vcenter card-table">
-<thead>
-<tr><th>Field</th><th>Value</th></tr>
-</thead>
-<tbody>
-<tr>
-<td>Owner</td>
-<td>{{ owner }}</td>
-</tr>
-<tr>
-<td>
-<label for="summary-ref-select">Ref</label>
-<select id="summary-ref-select">
-<option value="HEAD" selected>HEAD</option>
-</select>
-</td>
-<td>
-{{ link(ref.commit_href, ref.display, id="summary-ref-commit-link") }}:
-{{ link(ref.browse_href, "browse", id="summary-ref-browse-link") }} -
-{{ link(ref.log_href, "log", id="summary-ref-log-link") }} -
-{{ link(ref.shortlog_href, "shortlog", id="summary-ref-shortlog-link") }}
-</td>
-</tr>
-<tr>
-<td>Branches</td>
-<td>{{ link(project_url ~ "?a=heads", "view branches") }}</td>
-</tr>
-<tr>
-<td>Merge request</td>
-<td>
-{% if merge_request %}
-{% with project=merge_request.project, branches=merge_request.branches, theirs_default_ref=merge_request.theirs_default_ref, ours_default_ref=merge_request.ours_default_ref %}
-{% include "merge_request_summary_cell.html" %}
-{% endwith %}
-{% else %}
-<span class="text-muted">No branches.</span>
+<p class="text-secondary">{{ descr }}</p>
 {% endif %}
-</td>
-</tr>
-<tr>
-<td>Tags</td>
-<td>{{ link(project_url ~ "?a=tags", "view tags") }}</td>
-</tr>
-<tr>
-<td>Remotes</td>
-<td>{{ link(project_url ~ "?a=remotes", "view remotes") }}</td>
-</tr>
-<tr>
-<td>Board</td>
-<td>{{ link(board.href, board.text, style=board.style) }}</td>
-</tr>
-<tr>
-<td>Search</td>
-<td>{{ link(project_url_enc ~ "/search", "search files") }}</td>
-</tr>
-<tr>
-<td>Hooks</td>
-<td>
-{% with mode=hooks.mode, project_enc=hooks.project_enc, installed=hooks.installed, available=hooks.available %}
-{% include "hooks_summary_cell.html" %}
-{% endwith %}
-</td>
-</tr>
-{% for row in extra_rows %}
-<tr>{% for cell in row %}<td>{{ cell }}</td>{% endfor %}</tr>
-{% endfor %}
-</tbody>
-</table>
-{% endcall %}
+{% macro overview_card(title, wide=false) %}
+<div class="{{ 'col-12' if wide else 'col-md-6 col-xl-4' }}">
+  <div class="card">
+    <div class="card-header"><h3 class="card-title mb-0">{{ title }}</h3></div>
+    <div class="card-body">{{ caller() }}</div>
+  </div>
+</div>
+{% endmacro %}
+<div class="row row-deck row-cards">
+  {% call overview_card("Owner") %}{{ owner }}{% endcall %}
+  {% call overview_card("Ref") %}
+  <div class="d-flex flex-wrap align-items-center gap-2">
+    <label class="visually-hidden" for="summary-ref-select">Ref</label>
+    <select id="summary-ref-select" class="form-select form-select-sm" style="width: auto; min-width: 10rem;">
+      <option value="HEAD" selected>HEAD</option>
+    </select>
+    <div class="btn-list">
+      <a class="btn btn-sm btn-outline-secondary" id="summary-ref-commit-link" href="{{ ref.commit_href }}">{{ ref.display }}</a>
+      <a class="btn btn-sm btn-outline-secondary" id="summary-ref-browse-link" href="{{ ref.browse_href }}">browse</a>
+      <a class="btn btn-sm btn-outline-secondary" id="summary-ref-log-link" href="{{ ref.log_href }}">log</a>
+      <a class="btn btn-sm btn-outline-secondary" id="summary-ref-shortlog-link" href="{{ ref.shortlog_href }}">shortlog</a>
+    </div>
+  </div>
+  {% endcall %}
+  {% call overview_card("Branches") %}{{ link(project_url ~ "?a=heads", "view branches") }}{% endcall %}
+  {% call overview_card("Merge requests", wide=true) %}
+  {% if open_merge_requests %}
+  <div class="list-group list-group-flush mb-3">
+    {% for mr in open_merge_requests %}
+    <a class="list-group-item" href="{{ mr.href }}">
+      <div class="d-flex flex-wrap align-items-center justify-content-between gap-2">
+        <span>{{ mr.title }}</span>
+        <span class="badge bg-{{ mr.badge }}-lt">{{ mr.status_label }}</span>
+      </div>
+      {% if mr.theirs or mr.ours %}
+      <div class="text-secondary small">{{ mr.theirs or "head" }} → {{ mr.ours }}</div>
+      {% endif %}
+    </a>
+    {% endfor %}
+  </div>
+  {% elif merge_request %}
+  <p class="text-secondary">No open merge requests.</p>
+  {% endif %}
+  {% if merge_request %}
+  {% with project=merge_request.project, branches=merge_request.branches, theirs_default_ref=merge_request.theirs_default_ref, ours_default_ref=merge_request.ours_default_ref %}
+  {% include "merge_request_summary_cell.html" %}
+  {% endwith %}
+  {% elif not open_merge_requests %}
+  <span class="badge bg-secondary-lt">No branches</span>
+  {% endif %}
+  {% endcall %}
+  {% call overview_card("Tags") %}{{ link(project_url ~ "?a=tags", "view tags") }}{% endcall %}
+  {% call overview_card("Remotes") %}{{ link(project_url ~ "?a=remotes", "view remotes") }}{% endcall %}
+  {% call overview_card("Board") %}{{ link(board.href, board.text, style=board.style) }}{% endcall %}
+  {% call overview_card("Search") %}{{ link(project_url_enc ~ "/search", "search files") }}{% endcall %}
+  {% call overview_card("Hooks") %}
+  {% with mode=hooks.mode, project_enc=hooks.project_enc, installed=hooks.installed, available=hooks.available %}
+  {% include "hooks_summary_cell.html" %}
+  {% endwith %}
+  {% endcall %}
+  {% for row in extra_rows %}
+  {% call overview_card(row[0]) %}{% for cell in row[1:] %}{{ cell }}{% endfor %}{% endcall %}
+  {% endfor %}
+</div>
 {% call section("Files") %}
 <div id="summary-tree-container" class="summary-tree-table">{{ ref.tree_html }}</div>
 <p class="px-3 pb-3 mb-0">{{ link(ref.browse_href, "Browse full tree", id="summary-tree-browse-link") }}</p>
diff --git a/pygitweb/src/pygitweb/templates/table_page.html b/pygitweb/src/pygitweb/templates/table_page.html
index a245a01..7e3b683 100644
--- a/pygitweb/src/pygitweb/templates/table_page.html
+++ b/pygitweb/src/pygitweb/templates/table_page.html
@@ -1,16 +1,31 @@
 {% extends "base.html" %}
-{% from "macros.html" import link %}
+{% from "macros.html" import link, page_header %}
 {% block content %}
-{% if heading %}<h1>{{ heading }}</h1>{% endif %}
+{% if heading %}
 {% if project %}
-<p>Project: {{ link("/project/" ~ project, project) }}</p>
+{% call page_header(heading, "Project") %}
+<a class="btn btn-outline-secondary" href="/project/{{ project }}">{{ project }}</a>
+{% endcall %}
+{% else %}
+{{ page_header(heading) }}
+{% endif %}
 {% endif %}
 {% if rows %}
-{% include "table.html" %}
+<div class="card">
+  <div class="card-body p-0">
+    {% include "table.html" %}
+  </div>
+</div>
 {% elif empty_message %}
-<p{% if empty_muted %} class="text-muted"{% endif %}>{{ empty_message }}</p>
+<div class="empty">
+  <p class="empty-title">{{ empty_message }}</p>
+</div>
 {% elif cols is defined %}
-{% include "table.html" %}
+<div class="card">
+  <div class="card-body p-0">
+    {% include "table.html" %}
+  </div>
+</div>
 {% endif %}
 {% if pagination %}
 {% with current_page=pagination.current_page, pagecount=pagination.pagecount, has_prev=pagination.has_prev, has_next=pagination.has_next, prev_url=pagination.prev_url, next_url=pagination.next_url, total_pages=pagination.total_pages, page_links=pagination.page_links %}
diff --git a/pygitweb/src/pygitweb/templates/tasks.html b/pygitweb/src/pygitweb/templates/tasks.html
index 07878a9..6e440a9 100644
--- a/pygitweb/src/pygitweb/templates/tasks.html
+++ b/pygitweb/src/pygitweb/templates/tasks.html
@@ -1,18 +1,27 @@
 {% extends "base.html" %}
+{% from "macros.html" import page_header %}
 {% block content %}
-<h1>Tasks</h1>
-<form method="get" action="/tasks" class="d-flex align-items-center gap-2 mb-3">
-<label class="form-label mb-0" for="tasks-status-filter">Status</label>
-<select id="tasks-status-filter" name="status" class="form-select form-select-sm" style="width: auto;" onchange="this.form.submit()">
-<option value=""{% if not status_filter %} selected{% endif %}>All</option>
-{% for status in statuses %}
-<option value="{{ status.value }}"{% if status.selected %} selected{% endif %}>{{ status.label }}</option>
-{% endfor %}
-</select>
+{% call page_header("Tasks", "Work") %}
+<form method="get" action="/tasks">
+  <label class="visually-hidden" for="tasks-status-filter">Status</label>
+  <select id="tasks-status-filter" name="status" class="form-select" style="width: auto;" onchange="this.form.submit()">
+    <option value=""{% if not status_filter %} selected{% endif %}>All statuses</option>
+    {% for status in statuses %}
+    <option value="{{ status.value }}"{% if status.selected %} selected{% endif %}>{{ status.label }}</option>
+    {% endfor %}
+  </select>
 </form>
+{% endcall %}
 {% if rows %}
-{% include "table.html" %}
+<div class="card">
+  <div class="card-body p-0">
+    {% include "table.html" %}
+  </div>
+</div>
 {% else %}
-<p class="text-muted">No tasks found.</p>
+<div class="empty">
+  <p class="empty-title">No tasks found</p>
+  <p class="empty-subtitle text-secondary">Tasks from boards you can read show up here.</p>
+</div>
 {% endif %}
 {% endblock %}
diff --git a/pygitweb/src/pygitweb/templates/team.html b/pygitweb/src/pygitweb/templates/team.html
index 82090a0..37da7f3 100644
--- a/pygitweb/src/pygitweb/templates/team.html
+++ b/pygitweb/src/pygitweb/templates/team.html
@@ -1,32 +1,42 @@
 {% extends "base.html" %}
+{% from "macros.html" import page_header %}
 {% block content %}
-{# Team overview: open tasks across projects, grouped by assignee. Expects: iso_week, groups [ { assignee, points_total, capacity, over_capacity, can_edit, capacity_url, rows } ] #}
-
-<h1 class="page-title">Team</h1>
-<p class="text-muted">What everyone is currently working on, across every project you can read.</p>
-
-<form method="get" action="/team" class="d-flex align-items-center gap-2 mb-3">
-  <label class="form-label mb-0" for="team-week">Week</label>
-  <input type="text" id="team-week" class="form-control form-control-sm" style="max-width: 140px;" name="week" value="{{ iso_week }}" placeholder="YYYY-Www">
-  <button type="submit" class="btn btn-sm btn-outline-secondary">Go</button>
+{% call page_header("Team", "Workload") %}
+<form method="get" action="/team" class="input-group" style="width: auto;">
+  <label class="input-group-text" for="team-week">Week</label>
+  <input type="text" id="team-week" class="form-control" name="week" value="{{ iso_week }}" placeholder="YYYY-Www">
+  <button type="submit" class="btn btn-outline-secondary">Go</button>
 </form>
+{% endcall %}
+<p class="text-secondary">What everyone is currently working on, across every project you can read.</p>
 
 {% if not groups %}
-<p class="text-muted">No open tasks found.</p>
+<div class="empty">
+  <p class="empty-title">No open tasks</p>
+  <p class="empty-subtitle text-secondary">Tasks in todo, in progress, or in review show up here.</p>
+</div>
 {% endif %}
 
 {% for group in groups %}
 <div class="card mb-3">
   <div class="card-header d-flex align-items-center justify-content-between flex-wrap gap-2">
-    <h3 class="card-title mb-0">{{ group.assignee }}</h3>
+    <h3 class="card-title mb-0 d-flex align-items-center gap-2">
+      <span class="avatar avatar-sm">{{ group.assignee | initials }}</span>
+      {{ group.assignee }}
+    </h3>
     <div class="d-flex align-items-center gap-2">
       {% if group.capacity is not none %}
-      <span class="badge {{ 'bg-danger' if group.over_capacity else 'bg-success' }}">{{ group.points_total }} / {{ group.capacity }} pts this week</span>
+      <div style="width: 8rem;">
+        <div class="progress progress-sm" title="{{ group.points_total }} / {{ group.capacity }} pts this week">
+          <div class="progress-bar {{ 'bg-danger' if group.over_capacity else 'bg-success' }}" style="width: {{ group.capacity_pct }}%"></div>
+        </div>
+      </div>
+      <span class="text-secondary small">{{ group.points_total }} / {{ group.capacity }} pts this week</span>
       {% else %}
-      <span class="badge bg-secondary">{{ group.points_total }} pts</span>
+      <span class="badge bg-secondary-lt">{{ group.points_total }} pts</span>
       {% endif %}
       {% if group.can_edit %}
-      <a class="small" href="{{ group.capacity_url }}">set capacity</a>
+      <a class="btn btn-sm btn-outline-secondary" href="{{ group.capacity_url }}">Set capacity</a>
       {% endif %}
     </div>
   </div>
diff --git a/pygitweb/src/pygitweb/templates/team_capacity_form.html b/pygitweb/src/pygitweb/templates/team_capacity_form.html
index 49ed594..1678828 100644
--- a/pygitweb/src/pygitweb/templates/team_capacity_form.html
+++ b/pygitweb/src/pygitweb/templates/team_capacity_form.html
@@ -1,8 +1,9 @@
 {% extends "base.html" %}
+{% from "macros.html" import page_header %}
 {% block content %}
 {# Set a user's sprint point capacity for an ISO week. Expects: target_user, iso_week, current_points #}
 
-<h1 class="page-title">Set weekly capacity</h1>
+{{ page_header("Set weekly capacity", "Team") }}
 <div class="card" style="max-width: 480px;">
   <div class="card-body">
     <form action="/team/capacity/submit" method="post">
diff --git a/pygitweb/src/pygitweb/templates_env.py b/pygitweb/src/pygitweb/templates_env.py
index 4ccc7d8..d70a507 100644
--- a/pygitweb/src/pygitweb/templates_env.py
+++ b/pygitweb/src/pygitweb/templates_env.py
@@ -55,3 +55,53 @@ def html_join(parts: Iterable[str | Markup], sep: str = "") -> Markup:
 def trusted_html(value: str) -> Markup:
 	"""Mark HTML that a template or plugin already escaped. Do not pass raw text."""
 	return Markup(value)
+
+
+def initials(name: str) -> str:
+	parts = [part for part in name.replace("_", " ").split() if part]
+	if len(parts) >= 2:
+		return (parts[0][0] + parts[-1][0]).upper()
+	if parts:
+		return parts[0][:2].upper()
+	return "?"
+
+
+_STATUS_BADGE = {
+	"TODO": "secondary",
+	"IN_PROGRESS": "blue",
+	"IN_REVIEW": "yellow",
+	"DONE": "green",
+	"CANCELLED": "red",
+}
+_PRIORITY_BADGE = {
+	"LOW": "secondary",
+	"MEDIUM": "azure",
+	"HIGH": "orange",
+	"CRITICAL": "red",
+}
+
+
+def status_badge(status: str) -> Markup:
+	key = (status or "TODO").upper()
+	color = _STATUS_BADGE.get(key, "secondary")
+	label = key.replace("_", " ").title()
+	return html_format('<span class="badge bg-{}-lt">{}</span>', color, label)
+
+
+def priority_badge(priority: str) -> Markup:
+	key = (priority or "LOW").upper()
+	color = _PRIORITY_BADGE.get(key, "secondary")
+	label = key.replace("_", " ").title()
+	return html_format('<span class="badge bg-{}-lt">{}</span>', color, label)
+
+
+def person_label(name: str) -> Markup:
+	label = (name or "").strip() or "Unassigned"
+	return html_format(
+		'<span class="d-inline-flex align-items-center gap-1"><span class="avatar avatar-xs">{}</span>{}</span>',
+		initials(label),
+		label,
+	)
+
+
+env.filters["initials"] = initials
diff --git a/pygitweb/src/pygitweb/templates_env_test.py b/pygitweb/src/pygitweb/templates_env_test.py
index ad7b92e..61a6e96 100644
--- a/pygitweb/src/pygitweb/templates_env_test.py
+++ b/pygitweb/src/pygitweb/templates_env_test.py
@@ -70,7 +70,8 @@ def test_project_list_template_emits_links() -> None:
 	assert ">a&lt;b</a>" in text
 	assert "d&amp;d" in text
 	assert ">create board</a>" in text
-	assert 'href="/board/create?project=a%3Cb">create board</a>' in text
+	assert 'href="/board/create?project=a%3Cb"' in text
+	assert "bg-secondary-lt" in text
 
 
 def test_project_board_link_points_at_existing_board(monkeypatch) -> None:
