diff --git a/pygitweb/actions.py b/pygitweb/actions.py
index faf3cb3..bf4c74b 100644
--- a/pygitweb/actions.py
+++ b/pygitweb/actions.py
@@ -270,6 +270,55 @@ def _render_pagination(
 	)
 
 
+def _render_collapsible_section(
+	title: str,
+	body: str,
+	*,
+	open_default: bool = True,
+	flush_table: bool = False,
+) -> str:
+	open_attr = " open" if open_default else ""
+	body_class = "card-body p-0" if flush_table else "card-body"
+	return (
+		f'<details class="summary-section card mt-3"{open_attr}>'
+		f'<summary class="card-header summary-section-toggle">'
+		f'<h2 class="card-title mb-0">{jinja_escape(title)}</h2>'
+		f"</summary>"
+		f'<div class="{body_class}">{body}</div>'
+		f"</details>"
+	)
+
+
+def _render_tree_table(project: str, ref: str | None, f: str | None) -> str:
+	"""Return an HTML table listing files and directories at ref/path."""
+	if f is not None and not is_valid_pathname(f):
+		return ""
+	result = get_tree_at_ref_path(project, ref, f)
+	if not result:
+		return '<p class="text-muted mb-0">No files found.</p>'
+	tree, ref_oid = result
+	entries = [(obj.name, obj.type_str, str(obj.id)) for obj in tree]
+	dirs = sorted((n, t, o) for n, t, o in entries if t == "tree")
+	blobs = sorted((n, t, o) for n, t, o in entries if t == "blob")
+	rows: list[list[str]] = []
+	for name, _, _ in dirs:
+		sub_path = f"{f.strip('/')}/{name}".strip("/") if f else name
+		link = _tree_url(project, ref_oid, sub_path)
+		rows.append([f'<a href="{link}">{jinja_escape(name)}/</a>', "tree", ""])
+	for name, _, _ in blobs:
+		sub_path = f"{f.strip('/')}/{name}".strip("/") if f else name
+		link = _tree_url(project, ref_oid, sub_path, a="blob")
+		blame_link = _tree_url(project, ref_oid, sub_path, a="blame")
+		rows.append([
+			f'<a href="{link}">{jinja_escape(name)}</a>',
+			"blob",
+			f'<a href="{blame_link}">blame</a>',
+		])
+	if not rows:
+		return '<p class="text-muted mb-0">Empty directory.</p>'
+	return env.get_template("table.html").render(cols=["Name", "Type", ""], rows=rows)
+
+
 def _render_readme_card(
 	project: str,
 	ref_oid: str,
@@ -277,7 +326,7 @@ def _render_readme_card(
 	readme_content: str,
 	blob_dir: str = "",
 ) -> str:
-	"""Return HTML for the README card (and script for markdown).
+	"""Return HTML for the README section (and script for markdown).
 
 	blob_dir is the current tree path for relative links.
 	"""
@@ -287,24 +336,19 @@ def _render_readme_card(
 	blob_dir_attr = blob_dir.replace("&", "&amp;").replace('"', "&quot;") if blob_dir else ""
 	if is_markdown:
 		readme_b64 = base64.b64encode(readme_content.encode("utf-8")).decode("ascii")
-		card = (
-			'<div class="card mt-3"><div class="card-header"><h2 class="card-title">README</h2></div>'
-			'<div class="card-body"><div id="readme-container" class="markdown-body" data-readme-b64="'
+		body = (
+			'<div id="readme-container" class="markdown-body" data-readme-b64="'
 			+ readme_b64
 			+ '" data-blob-base="'
 			+ blob_base_attr
 			+ '"'
 		)
 		if blob_dir_attr:
-			card += ' data-blob-dir="' + blob_dir_attr + '"'
-		card += '></div></div></div><script src="/static/readme-render.js"></script>'
-		return card
-	return (
-		'<div class="card mt-3"><div class="card-header"><h2 class="card-title">README</h2></div>'
-		+ '<div class="card-body"><pre class="readme-plain"><code>'
-		+ (jinja_escape(sanitize(readme_content)) or "")
-		+ "</code></pre></div></div>"
-	)
+			body += ' data-blob-dir="' + blob_dir_attr + '"'
+		body += '></div><script src="/static/readme-render.js"></script>'
+		return _render_collapsible_section("README", body)
+	body = '<pre class="readme-plain"><code>' + (jinja_escape(sanitize(readme_content)) or "") + "</code></pre>"
+	return _render_collapsible_section("README", body)
 
 
 def _commit_unified_diff_or_raise(project: str, h: str) -> tuple[str, str]:
@@ -332,6 +376,7 @@ class SummaryRefState(TypedDict):
 	browse_href: str
 	log_href: str
 	shortlog_href: str
+	tree_html: str
 	readme_html: str
 
 
@@ -369,6 +414,7 @@ def summary_ref_state(project: str, requested_ref: str | None) -> SummaryRefStat
 			commit_oid = str(target_obj.id)
 		elif target_obj is not None:
 			browse_ref = str(target_obj.id)
+	tree_html = _render_tree_table(project, ref, None)
 	readme_html = ""
 	readme = get_readme_at_ref_path(project, ref, "")
 	if readme:
@@ -382,6 +428,7 @@ def summary_ref_state(project: str, requested_ref: str | None) -> SummaryRefStat
 		"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='')}",
+		"tree_html": tree_html,
 		"readme_html": readme_html,
 	}
 
@@ -609,7 +656,14 @@ def git_summary(project: str, extra_rows: list[list[str]] | None = None) -> HTML
 	body_parts = [f"{pre}<h1>{jinja_escape(project)}</h1>"]
 	if descr:
 		body_parts.append(f"<p>{jinja_escape(descr)}</p>")
-	body_parts.append(table)
+	body_parts.append(_render_collapsible_section("Project", table, flush_table=True))
+	files_body = (
+		f'<div id="summary-tree-container" class="summary-tree-table">'
+		f"{initial_ref_state['tree_html']}</div>"
+		f'<p class="px-3 pb-3 mb-0"><a id="summary-tree-browse-link" href="{initial_ref_state["browse_href"]}">'
+		f"Browse full tree</a></p>"
+	)
+	body_parts.append(_render_collapsible_section("Files", files_body))
 	body_parts.append(f'<div id="summary-readme-container" data-project="{jinja_escape(project)}">')
 	body_parts.append(f"{initial_ref_state['readme_html']}</div>")
 	body_parts.append('<script src="/static/summary-ref-switcher.js"></script>')
@@ -815,7 +869,7 @@ def git_tree(project: str, h: str | None, f: str | None) -> HTMLResponse:
 	result = get_tree_at_ref_path(project, h, f)
 	if not result:
 		raise HTTPException(status_code=404, detail="Tree or path not found")
-	tree, ref_oid = result
+	_tree, ref_oid = result
 	base = f"/project/{project}"
 	breadcrumbs = [f'<a href="{base}">{jinja_escape(project)}</a>']
 	if f:
@@ -824,29 +878,12 @@ def git_tree(project: str, h: str | None, f: str | None) -> HTMLResponse:
 			prefix = "/".join(parts[: i + 1])
 			breadcrumbs.append(f' / <a href="{_tree_url(project, ref_oid, prefix)}">{jinja_escape(seg)}</a>')
 	breadcrumb_html = "".join(breadcrumbs)
-	entries = [(obj.name, obj.type_str, str(obj.id)) for obj in tree]
-	dirs = sorted((n, t, o) for n, t, o in entries if t == "tree")
-	blobs = sorted((n, t, o) for n, t, o in entries if t == "blob")
-	rows = []
-	for name, _, _ in dirs:
-		sub_path = f"{f.strip('/')}/{name}".strip("/") if f else name
-		link = _tree_url(project, ref_oid, sub_path)
-		rows.append([f'<a href="{link}">{jinja_escape(name)}/</a>', "tree", ""])
-	for name, _, _ in blobs:
-		sub_path = f"{f.strip('/')}/{name}".strip("/") if f else name
-		link = _tree_url(project, ref_oid, sub_path, a="blob")
-		blame_link = _tree_url(project, ref_oid, sub_path, a="blame")
-		rows.append([
-			f'<a href="{link}">{jinja_escape(name)}</a>',
-			"blob",
-			f'<a href="{blame_link}">blame</a>',
-		])
 	title_path = f" / {f}" if f else ""
 	pre = PREAMBLE.render(
 		title=f"{jinja_escape(project)}{jinja_escape(title_path)} - Tree",
 		site_name=settings.SITE_NAME,
 	)
-	table = env.get_template("table.html").render(cols=["Name", "Type", ""], rows=rows)
+	table = _render_tree_table(project, h, f)
 	body_parts = [f"{pre}<p class='breadcrumb'>{breadcrumb_html}</p><h1>Tree{jinja_escape(title_path)}</h1>{table}"]
 	readme = get_readme_at_ref_path(project, ref_oid, f or "")
 	if readme:
diff --git a/pygitweb/hooks_install_test.py b/pygitweb/hooks_install_test.py
index 2c7ec00..0962837 100644
--- a/pygitweb/hooks_install_test.py
+++ b/pygitweb/hooks_install_test.py
@@ -359,3 +359,5 @@ class TestSummaryRefSwitcherStaticScript:
 		assert "AbortController" in body
 		assert "loadOptions" in body
 		assert "loadState" in body
+		assert "summary-tree-container" in body
+		assert "tree_html" in body
diff --git a/pygitweb/settings.py b/pygitweb/settings.py
index fcec930..6823fa2 100644
--- a/pygitweb/settings.py
+++ b/pygitweb/settings.py
@@ -16,9 +16,9 @@ from fastapi.responses import HTMLResponse, RedirectResponse
 
 from pygitweb.auth import require_permission
 from pygitweb.config import settings
-from pygitweb.settings_config import settings_batch_update
 from pygitweb.dependencies import ValidatedSettingsProject
 from pygitweb.permissions import Permission
+from pygitweb.settings_config import settings_batch_update
 from pygitweb.templates_env import POSTAMBLE, PREAMBLE, env, jinja_escape
 
 
diff --git a/pygitweb/static/main.css b/pygitweb/static/main.css
index 9428120..c3e6e8a 100644
--- a/pygitweb/static/main.css
+++ b/pygitweb/static/main.css
@@ -221,6 +221,32 @@ body {
 	}
 }
 
+.summary-section {
+	& > summary {
+		cursor: pointer;
+		list-style: none;
+
+		&::-webkit-details-marker {
+			display: none;
+		}
+
+		&::before {
+			content: "▸";
+			display: inline-block;
+			margin-right: 0.5rem;
+			transition: transform 0.15s ease;
+		}
+	}
+
+	&[open] > summary::before {
+		transform: rotate(90deg);
+	}
+
+	& .summary-tree-table:has(.table) {
+		padding: 0;
+	}
+}
+
 /* Table: override Tabler .table / .card-table so tables use color-scheme variables */
 .table,
 .table.card-table {
diff --git a/pygitweb/static/summary-ref-switcher.js b/pygitweb/static/summary-ref-switcher.js
index e2ced67..f894711 100644
--- a/pygitweb/static/summary-ref-switcher.js
+++ b/pygitweb/static/summary-ref-switcher.js
@@ -4,6 +4,8 @@
 	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 treeContainerEl = document.getElementById("summary-tree-container");
+	const treeBrowseLinkEl = document.getElementById("summary-tree-browse-link");
 	const readmeContainerEl = document.getElementById("summary-readme-container");
 	if (!selectEl || !commitLinkEl || !browseLinkEl || !readmeContainerEl) {
 		return;
@@ -48,6 +50,12 @@
 		if (shortlogLinkEl) {
 			shortlogLinkEl.href = state.shortlog_href;
 		}
+		if (treeContainerEl) {
+			treeContainerEl.innerHTML = state.tree_html || "";
+		}
+		if (treeBrowseLinkEl) {
+			treeBrowseLinkEl.href = state.browse_href;
+		}
 		readmeContainerEl.innerHTML = state.readme_html || "";
 		if (state.readme_html) {
 			rerunScripts();
