diff --git a/pygitweb/actions.py b/pygitweb/actions.py
index 3ea0e15..8d9e10c 100644
--- a/pygitweb/actions.py
+++ b/pygitweb/actions.py
@@ -9,7 +9,7 @@ import base64
 import mimetypes
 import os
 from datetime import UTC, datetime, timedelta, timezone
-from typing import Any
+from typing import Any, TypedDict
 from urllib.parse import quote, urlencode
 
 import pygit2
@@ -26,8 +26,8 @@ from pygitweb.git_helpers import (
 	get_commits_in_range,
 	get_readme_at_ref_path,
 	get_tree_at_ref_path,
-	git_get_head_hash,
 	git_get_heads_list,
+	git_get_references,
 	git_get_project_description,
 	git_get_remotes_info,
 	git_get_tags_list,
@@ -218,6 +218,73 @@ def _commit_unified_diff_or_raise(project: str, h: str) -> tuple[str, str]:
 		raise HTTPException(status_code=404, detail="Commit not found") from e
 
 
+class SummaryRefOption(TypedDict):
+	label: str
+	value: str
+	kind: str
+
+
+class SummaryRefState(TypedDict):
+	ref: str
+	display: str
+	commit_href: str
+	browse_href: str
+	log_href: str
+	shortlog_href: str
+	readme_html: str
+
+
+def summary_ref_options(project: str) -> list[SummaryRefOption]:
+	options: list[SummaryRefOption] = []
+	for name, ref, _oid in git_get_heads_list(project):
+		options.append({"label": name, "value": ref, "kind": "branch"})
+	for name, ref, _oid in git_get_tags_list(project):
+		options.append({"label": name, "value": ref, "kind": "tag"})
+	return options
+
+
+def summary_ref_state(project: str, requested_ref: str | None) -> SummaryRefState:
+	ref = requested_ref or "HEAD"
+	commit_oid = ""
+	browse_ref = ref
+	try:
+		repo = open_repo(project)
+		obj = repo.revparse_single(ref)
+	except (KeyError, pygit2.GitError, OSError):
+		ref = "HEAD"
+		try:
+			repo = open_repo(project)
+			obj = repo.revparse_single(ref)
+		except (KeyError, pygit2.GitError, OSError):
+			obj = None
+	if isinstance(obj, pygit2.Commit):
+		commit_oid = str(obj.id)
+	elif isinstance(obj, pygit2.Tag):
+		try:
+			target_obj = repo[obj.target]
+		except (KeyError, pygit2.GitError):
+			target_obj = None
+		if isinstance(target_obj, pygit2.Commit):
+			commit_oid = str(target_obj.id)
+		elif target_obj is not None:
+			browse_ref = str(target_obj.id)
+	readme_html = ""
+	readme = get_readme_at_ref_path(project, ref, "")
+	if readme:
+		readme_filename, readme_content = readme
+		readme_html = _render_readme_card(project, ref, readme_filename, readme_content, "")
+	log_ref = commit_oid or ref
+	return {
+		"ref": ref,
+		"display": (commit_oid[:7] if commit_oid else ref) or "N/A",
+		"commit_href": f"/project/{project}?a=commit&h={quote(commit_oid or ref, safe='')}",
+		"browse_href": f"/project/{project}?a=tree&h={quote(browse_ref, safe='')}",
+		"log_href": f"/project/{project}?a=log&h={quote(log_ref, safe='')}",
+		"shortlog_href": f"/project/{project}?a=shortlog&h={quote(log_ref, safe='')}",
+		"readme_html": readme_html,
+	}
+
+
 # ---------- Action handlers ----------
 
 
@@ -271,41 +338,57 @@ def git_object(project: str, h: str | None) -> Response:
 
 def git_summary(project: str) -> HTMLResponse:
 	"""Project summary page. Port of git_summary."""
-	descr = git_get_project_description(project) or "none"
+	descr = git_get_project_description(project) or ""
 	owner = git_get_project_owner(project) or ""
-	head = git_get_head_hash(project)
-	head_short = head[:7] if head else ""
+	initial_ref_state = summary_ref_state(project, "HEAD")
+	project_enc = quote(project, safe="/")
+
+	try:
+		board_refs = git_get_references(project, "refs/tags/boards")
+		has_boards = len(board_refs) > 0
+	except Exception:
+		has_boards = False
+	if has_boards:
+		board_value = f'<a href="/project/{project_enc}/board/">project board</a>'
+	else:
+		grey_style = ' style="color: #999; cursor: not-allowed;"' if settings.AUTH else ""
+		proj_q = quote(project, safe="")
+		board_value = f'<a href="/board/create?project={proj_q}"{grey_style}>create board</a>'
 
 	table = env.get_template("table.html").render(
 		cols=["Field", "Value"],
 		rows=[
-			["Description", esc_html(descr)],
 			["Owner", esc_html(owner)],
 			[
-				esc_html("HEAD"),
-				f"<a href='/project/{project}?a=commit&h={quote(head or '', safe='')}'>{head_short or 'N/A'}</a>",
-			],
-			[
-				esc_html("tree"),
-				f"<a href='/project/{project}?a=tree&h={head or ''}'>browse</a>",
+				(
+					'<label for="summary-ref-select">Ref</label> '
+					'<select id="summary-ref-select">'
+					'<option value="HEAD" selected>HEAD</option>'
+					"</select>"
+				),
+				(
+					f'<a id="summary-ref-commit-link" href="{initial_ref_state["commit_href"]}">'
+					f'{initial_ref_state["display"]}</a>: '
+					f'<a id="summary-ref-browse-link" href="{initial_ref_state["browse_href"]}">browse</a> - '
+					f'<a id="summary-ref-log-link" href="{initial_ref_state["log_href"]}">log</a> - '
+					f'<a id="summary-ref-shortlog-link" href="{initial_ref_state["shortlog_href"]}">shortlog</a>'
+				),
 			],
-			["Log", f"<a href='/project/{project}?a=log&h={head or ''}'>view log</a>"],
-			[
-				"Shortlog",
-				f"<a href='/project/{project}?a=shortlog&h={head or ''}'>view shortlog</a>",
-			],
-			["Heads", f"<a href='/project/{project}?a=heads'>view heads</a>"],
+			["Branches", f"<a href='/project/{project}?a=heads'>view branches</a>"],
 			["Tags", f"<a href='/project/{project}?a=tags'>view tags</a>"],
 			["Remotes", f"<a href='/project/{project}?a=remotes'>view remotes</a>"],
+			["Board", board_value],
 		],
 	)
 	pre = PREAMBLE.render(title=f"{esc_html(settings.SITE_NAME)} - {project}", site_name=settings.SITE_NAME)
-	body_parts = [f"{pre}<h1>{esc_html(project)}</h1>{table}"]
-
-	readme = get_readme_at_ref_path(project, head, "")
-	if readme:
-		readme_filename, readme_content = readme
-		body_parts.append(_render_readme_card(project, head or "", readme_filename, readme_content, ""))
+	body_parts = [f"{pre}<h1>{esc_html(project)}</h1>"]
+	if descr:
+		body_parts.append(f"<p>{esc_html(descr)}</p>")
+	body_parts.append(table)
+	body_parts.append(
+		f'<div id="summary-readme-container" data-project="{esc_html(project)}">{initial_ref_state["readme_html"]}</div>'
+	)
+	body_parts.append('<script src="/static/summary-ref-switcher.js"></script>')
 
 	body_parts.append(POSTAMBLE)
 	return HTMLResponse("".join(body_parts))
diff --git a/pygitweb/main.py b/pygitweb/main.py
index e079406..52cfed3 100644
--- a/pygitweb/main.py
+++ b/pygitweb/main.py
@@ -39,6 +39,8 @@ from pygitweb.actions import (
 	git_tags,
 	git_tree,
 	parse_pagination,
+	summary_ref_options,
+	summary_ref_state,
 )
 from pygitweb.config import ACTIONS, check_loadavg, settings
 from pygitweb.formatting import esc_html
@@ -423,6 +425,21 @@ def project_board(
 	return HTMLResponse(f"{pre}{body}{POSTAMBLE}")
 
 
+@app.get("/project/{project:path}/summary-refs")
+def project_summary_refs(project: str) -> dict[str, list[dict[str, str]]]:
+	_validate_project(project)
+	return {"options": summary_ref_options(project)}
+
+
+@app.get("/project/{project:path}/summary-ref-state")
+def project_summary_ref_state(
+	project: str,
+	ref: Annotated[str | None, Query(alias="ref")] = None,
+) -> dict[str, str]:
+	_validate_project(project)
+	return summary_ref_state(project, ref)
+
+
 @app.get("/project/{project:path}", response_class=HTMLResponse)
 def dispatch(
 	request: Request,
diff --git a/pygitweb/static/readme-render.js b/pygitweb/static/readme-render.js
index f50fb59..86a114d 100644
--- a/pygitweb/static/readme-render.js
+++ b/pygitweb/static/readme-render.js
@@ -105,11 +105,16 @@
     if (!page) return;
     var tocPlaceholder = document.createElement("div");
     tocPlaceholder.className = "readme-toc-mount";
+    tocPlaceholder.hidden = true;
     page.appendChild(tocPlaceholder);
     tocPlaceholder.appendChild(tocEl);
-    var links = tocEl.querySelectorAll(".readme-toc-link");
     var items = tocEl.querySelectorAll(".readme-toc-item");
+    function updateTocVisibility() {
+      var readmeTop = container.getBoundingClientRect().top;
+      tocPlaceholder.hidden = readmeTop >= 0;
+    }
     function updateActive() {
+      updateTocVisibility();
       var fromTop = window.scrollY + 8;
       var current = null;
       var headings = container.querySelectorAll("h1[id], h2[id], h3[id], h4[id], h5[id], h6[id]");
@@ -126,7 +131,7 @@
       }
     }
     var ticking = false;
-    function onScroll() {
+    function onScrollOrResize() {
       if (!ticking) {
         requestAnimationFrame(function() {
           updateActive();
@@ -135,7 +140,8 @@
         ticking = true;
       }
     }
-    window.addEventListener("scroll", onScroll, { passive: true });
+    window.addEventListener("scroll", onScrollOrResize, { passive: true });
+    window.addEventListener("resize", onScrollOrResize, { passive: true });
     updateActive();
   }
   function b64ToUtf8(b64) {
diff --git a/pygitweb/static/summary-ref-switcher.js b/pygitweb/static/summary-ref-switcher.js
new file mode 100644
index 0000000..9eb9f82
--- /dev/null
+++ b/pygitweb/static/summary-ref-switcher.js
@@ -0,0 +1,73 @@
+(() => {
+	const selectEl = document.getElementById("summary-ref-select");
+	const commitLinkEl = document.getElementById("summary-ref-commit-link");
+	const browseLinkEl = document.getElementById("summary-ref-browse-link");
+	const logLinkEl = document.getElementById("summary-ref-log-link");
+	const shortlogLinkEl = document.getElementById("summary-ref-shortlog-link");
+	const readmeContainerEl = document.getElementById("summary-readme-container");
+	if (!selectEl || !commitLinkEl || !browseLinkEl || !readmeContainerEl) {
+		return;
+	}
+
+	const project = readmeContainerEl.dataset.project;
+	if (!project) {
+		return;
+	}
+
+	const projectApiBase = () =>
+		"/project/" + project.split("/").map((segment) => encodeURIComponent(segment)).join("/");
+
+	const rerunScripts = () => {
+		for (const oldScript of readmeContainerEl.querySelectorAll("script")) {
+			const scriptEl = document.createElement("script");
+			if (oldScript.src) {
+				scriptEl.src = oldScript.src;
+			} else if (oldScript.textContent) {
+				scriptEl.textContent = oldScript.textContent;
+			}
+			oldScript.replaceWith(scriptEl);
+		}
+	};
+
+	const loadState = async (ref) => {
+		const response = await fetch(
+			`${projectApiBase()}/summary-ref-state?ref=${encodeURIComponent(ref)}`,
+		);
+		if (!response.ok) {
+			return;
+		}
+		const state = await response.json();
+		commitLinkEl.href = state.commit_href;
+		commitLinkEl.textContent = state.display;
+		browseLinkEl.href = state.browse_href;
+		if (logLinkEl) {
+			logLinkEl.href = state.log_href;
+		}
+		if (shortlogLinkEl) {
+			shortlogLinkEl.href = state.shortlog_href;
+		}
+		readmeContainerEl.innerHTML = state.readme_html || "";
+		if (state.readme_html) {
+			rerunScripts();
+		}
+	};
+
+	const loadOptions = async () => {
+		const response = await fetch(`${projectApiBase()}/summary-refs`);
+		if (!response.ok) {
+			return;
+		}
+		const data = await response.json();
+		for (const optionData of data.options || []) {
+			const optionEl = document.createElement("option");
+			optionEl.value = optionData.value;
+			optionEl.textContent = `${optionData.kind}: ${optionData.label}`;
+			selectEl.appendChild(optionEl);
+		}
+	};
+
+	selectEl.addEventListener("change", () => {
+		void loadState(selectEl.value);
+	});
+	void loadOptions();
+})();
