diff --git a/pygittools/pyproject.toml b/pygittools/pyproject.toml
index bd9e696..3d1c340 100644
--- a/pygittools/pyproject.toml
+++ b/pygittools/pyproject.toml
@@ -27,4 +27,4 @@ packages = ["pygittools", "pygittools.tui", "pygittools.tui.pages"]
 pygittools = "."
 
 [tool.setuptools.package-data]
-pygittools = ["hook_samples/**/*", "help.md"]
+pygittools = ["hook_samples/**/*", "help.md", "tui/colors.json"]
diff --git a/pygittools/tui/app.py b/pygittools/tui/app.py
index f74b2a5..075285e 100644
--- a/pygittools/tui/app.py
+++ b/pygittools/tui/app.py
@@ -18,10 +18,11 @@ from unicurses import (  # type: ignore[import-untyped]
 	refresh,
 )
 
+from pygittools.tui.git_colors import init_tui_color_theme, status_bar_attr
 from pygittools.tui.pages.home import HomePage, home_page
 from pygittools.tui.pages.no_repo import NoRepoPage
 from pygittools.tui.repo import try_open_page_context
-from pygittools.tui.status_bar import content_height, draw_status_bar, init_status_bar
+from pygittools.tui.status_bar import content_height, draw_status_bar
 from pygittools.tui.tabs import init_tab_colors
 from pygittools.tui.types import Page, PageAction, PageContext, PageResult
 
@@ -38,7 +39,7 @@ def run_tui(repo_path: Path | None = None) -> int:
 		cbreak()
 		curs_set(0)
 		keypad(stdscr, True)
-		status_attr = init_status_bar()
+		init_tui_color_theme(ctx.repo if ctx is not None else None)
 		init_tab_colors()
 
 		if ctx is not None:
@@ -50,7 +51,7 @@ def run_tui(repo_path: Path | None = None) -> int:
 			clear()
 			page.draw(stdscr, content_height(height), width)
 			if height > 0:
-				draw_status_bar(height - 1, width, page.status_text(), status_attr)
+				draw_status_bar(height - 1, width, page.status_text(), status_bar_attr())
 			refresh()
 
 			try:
@@ -92,6 +93,8 @@ def _apply_result(
 		new_ctx = try_open_page_context(result.switch_repo)
 		if new_ctx is not None:
 			ctx = new_ctx
+			init_tui_color_theme(ctx.repo)
+			init_tab_colors()
 
 	match result.action:
 		case PageAction.REPLACE:
diff --git a/pygittools/tui/app_test.py b/pygittools/tui/app_test.py
index a756b6b..a089e32 100644
--- a/pygittools/tui/app_test.py
+++ b/pygittools/tui/app_test.py
@@ -103,9 +103,10 @@ def _run_tui_once(repo_dir: Path, keys: list[int]) -> int:
 			patch("pygittools.tui.app.clear"),
 			patch("pygittools.tui.app.refresh"),
 			patch("pygittools.tui.app.endwin"),
-			patch("pygittools.tui.app.init_status_bar", return_value=0),
+			patch("pygittools.tui.app.init_tui_color_theme"),
 			patch("pygittools.tui.app.init_tab_colors"),
 			patch("pygittools.tui.app.draw_status_bar"),
+			patch("pygittools.tui.app.status_bar_attr", return_value=0),
 		):
 			return run_tui(repo_dir)
 
diff --git a/pygittools/tui/changes_list.py b/pygittools/tui/changes_list.py
index 2ae84a9..5113ebe 100644
--- a/pygittools/tui/changes_list.py
+++ b/pygittools/tui/changes_list.py
@@ -10,7 +10,6 @@ from typing import Literal
 from pygit2 import Repository
 from unicurses import (  # type: ignore[import-untyped]
 	A_BOLD,
-	A_REVERSE,
 	KEY_BACKSPACE,
 	KEY_DOWN,
 	KEY_ENTER,
@@ -22,6 +21,7 @@ from unicurses import (  # type: ignore[import-untyped]
 
 from pygittools.tui.commit_draft import save_commit_draft
 from pygittools.tui.draw import clear_row, draw_line, draw_text
+from pygittools.tui.git_colors import current_theme, focus_attr, row_attr_for_section
 from pygittools.tui.worktree import (
 	categorize_status,
 	commit_staged,
@@ -152,7 +152,17 @@ class ChangesList:
 					commit_input,
 				)
 				continue
-			attr = A_REVERSE if focused and highlight else 0
+			if row.kind == "header" and row.section is not None:
+				attr = row_attr_for_section(
+					row.section.value,
+					focused=focused,
+					highlight=highlight,
+					is_header=True,
+				)
+			elif row.section is not None:
+				attr = row_attr_for_section(row.section.value, focused=focused, highlight=highlight)
+			else:
+				attr = 0
 			draw_line(screen_row, 0, row.label, width, attr)
 
 	def handle_nav_key(self, key: int, repo: Repository) -> NavKeyResult:
@@ -264,7 +274,8 @@ class ChangesList:
 
 	def _draw_commit_row(self, row: int, width: int, focused: bool, commit_input: bool) -> None:
 		clear_row(row)
-		row_attr = A_REVERSE if focused else 0
+		theme = current_theme()
+		row_attr = focus_attr(theme.commit) if focused else theme.commit
 		show_cursor = focused
 		message = self._commit_message_text(width, show_cursor)
 		col = 0
diff --git a/pygittools/tui/colors.json b/pygittools/tui/colors.json
new file mode 100644
index 0000000..b3a7044
--- /dev/null
+++ b/pygittools/tui/colors.json
@@ -0,0 +1,258 @@
+[
+    "#000000",
+    "#800000",
+    "#008000",
+    "#808000",
+    "#000080",
+    "#800080",
+    "#008080",
+    "#c0c0c0",
+    "#808080",
+    "#ff0000",
+    "#00ff00",
+    "#ffff00",
+    "#0000ff",
+    "#ff00ff",
+    "#00ffff",
+    "#ffffff",
+    "#000000",
+    "#00005f",
+    "#000087",
+    "#0000af",
+    "#0000d7",
+    "#0000ff",
+    "#005f00",
+    "#005f5f",
+    "#005f87",
+    "#005faf",
+    "#005fd7",
+    "#005fff",
+    "#008700",
+    "#00875f",
+    "#008787",
+    "#0087af",
+    "#0087d7",
+    "#0087ff",
+    "#00af00",
+    "#00af5f",
+    "#00af87",
+    "#00afaf",
+    "#00afd7",
+    "#00afff",
+    "#00d700",
+    "#00d75f",
+    "#00d787",
+    "#00d7af",
+    "#00d7d7",
+    "#00d7ff",
+    "#00ff00",
+    "#00ff5f",
+    "#00ff87",
+    "#00ffaf",
+    "#00ffd7",
+    "#00ffff",
+    "#5f0000",
+    "#5f005f",
+    "#5f0087",
+    "#5f00af",
+    "#5f00d7",
+    "#5f00ff",
+    "#5f5f00",
+    "#5f5f5f",
+    "#5f5f87",
+    "#5f5faf",
+    "#5f5fd7",
+    "#5f5fff",
+    "#5f8700",
+    "#5f875f",
+    "#5f8787",
+    "#5f87af",
+    "#5f87d7",
+    "#5f87ff",
+    "#5faf00",
+    "#5faf5f",
+    "#5faf87",
+    "#5fafaf",
+    "#5fafd7",
+    "#5fafff",
+    "#5fd700",
+    "#5fd75f",
+    "#5fd787",
+    "#5fd7af",
+    "#5fd7d7",
+    "#5fd7ff",
+    "#5fff00",
+    "#5fff5f",
+    "#5fff87",
+    "#5fffaf",
+    "#5fffd7",
+    "#5fffff",
+    "#870000",
+    "#87005f",
+    "#870087",
+    "#8700af",
+    "#8700d7",
+    "#8700ff",
+    "#875f00",
+    "#875f5f",
+    "#875f87",
+    "#875faf",
+    "#875fd7",
+    "#875fff",
+    "#878700",
+    "#87875f",
+    "#878787",
+    "#8787af",
+    "#8787d7",
+    "#8787ff",
+    "#87af00",
+    "#87af5f",
+    "#87af87",
+    "#87afaf",
+    "#87afd7",
+    "#87afff",
+    "#87d700",
+    "#87d75f",
+    "#87d787",
+    "#87d7af",
+    "#87d7d7",
+    "#87d7ff",
+    "#87ff00",
+    "#87ff5f",
+    "#87ff87",
+    "#87ffaf",
+    "#87ffd7",
+    "#87ffff",
+    "#af0000",
+    "#af005f",
+    "#af0087",
+    "#af00af",
+    "#af00d7",
+    "#af00ff",
+    "#af5f00",
+    "#af5f5f",
+    "#af5f87",
+    "#af5faf",
+    "#af5fd7",
+    "#af5fff",
+    "#af8700",
+    "#af875f",
+    "#af8787",
+    "#af87af",
+    "#af87d7",
+    "#af87ff",
+    "#afaf00",
+    "#afaf5f",
+    "#afaf87",
+    "#afafaf",
+    "#afafd7",
+    "#afafff",
+    "#afd700",
+    "#afd75f",
+    "#afd787",
+    "#afd7af",
+    "#afd7d7",
+    "#afd7ff",
+    "#afff00",
+    "#afff5f",
+    "#afff87",
+    "#afffaf",
+    "#afffd7",
+    "#afffff",
+    "#d70000",
+    "#d7005f",
+    "#d70087",
+    "#d700af",
+    "#d700d7",
+    "#d700ff",
+    "#d75f00",
+    "#d75f5f",
+    "#d75f87",
+    "#d75faf",
+    "#d75fd7",
+    "#d75fff",
+    "#d78700",
+    "#d7875f",
+    "#d78787",
+    "#d787af",
+    "#d787d7",
+    "#d787ff",
+    "#d7af00",
+    "#d7af5f",
+    "#d7af87",
+    "#d7afaf",
+    "#d7afd7",
+    "#d7afff",
+    "#d7d700",
+    "#d7d75f",
+    "#d7d787",
+    "#d7d7af",
+    "#d7d7d7",
+    "#d7d7ff",
+    "#d7ff00",
+    "#d7ff5f",
+    "#d7ff87",
+    "#d7ffaf",
+    "#d7ffd7",
+    "#d7ffff",
+    "#ff0000",
+    "#ff005f",
+    "#ff0087",
+    "#ff00af",
+    "#ff00d7",
+    "#ff00ff",
+    "#ff5f00",
+    "#ff5f5f",
+    "#ff5f87",
+    "#ff5faf",
+    "#ff5fd7",
+    "#ff5fff",
+    "#ff8700",
+    "#ff875f",
+    "#ff8787",
+    "#ff87af",
+    "#ff87d7",
+    "#ff87ff",
+    "#ffaf00",
+    "#ffaf5f",
+    "#ffaf87",
+    "#ffafaf",
+    "#ffafd7",
+    "#ffafff",
+    "#ffd700",
+    "#ffd75f",
+    "#ffd787",
+    "#ffd7af",
+    "#ffd7d7",
+    "#ffd7ff",
+    "#ffff00",
+    "#ffff5f",
+    "#ffff87",
+    "#ffffaf",
+    "#ffffd7",
+    "#ffffff",
+    "#080808",
+    "#121212",
+    "#1c1c1c",
+    "#262626",
+    "#303030",
+    "#3a3a3a",
+    "#444444",
+    "#4e4e4e",
+    "#585858",
+    "#606060",
+    "#666666",
+    "#767676",
+    "#808080",
+    "#8a8a8a",
+    "#949494",
+    "#9e9e9e",
+    "#a8a8a8",
+    "#b2b2b2",
+    "#bcbcbc",
+    "#c6c6c6",
+    "#d0d0d0",
+    "#dadada",
+    "#e4e4e4",
+    "#eeeeee"
+]
diff --git a/pygittools/tui/conftest.py b/pygittools/tui/conftest.py
index fd58dde..7e0f39a 100644
--- a/pygittools/tui/conftest.py
+++ b/pygittools/tui/conftest.py
@@ -38,14 +38,17 @@ def committed_repo(repo: Repository) -> Repository:
 
 @pytest.fixture(autouse=True)
 def _mock_unicurses_io(monkeypatch: pytest.MonkeyPatch) -> None:
-	"""Avoid requiring a real terminal for status/tab helpers."""
+	"""Avoid requiring a real terminal for draw/status/tab helpers."""
 	mock = MagicMock()
 	for name in (
 		"has_colors",
 		"start_color",
 		"use_default_colors",
 		"init_pair",
+		"can_change_color",
+		"init_color",
 	):
+		monkeypatch.setattr(f"pygittools.tui.git_colors.{name}", mock, raising=False)
 		monkeypatch.setattr(f"pygittools.tui.status_bar.{name}", mock, raising=False)
 		monkeypatch.setattr(f"pygittools.tui.tabs.{name}", mock, raising=False)
 	for name in ("move", "clrtoeol", "mvaddstr"):
diff --git a/pygittools/tui/coverage_gaps_test.py b/pygittools/tui/coverage_gaps_test.py
index 8c7c9d5..41605d0 100644
--- a/pygittools/tui/coverage_gaps_test.py
+++ b/pygittools/tui/coverage_gaps_test.py
@@ -270,9 +270,10 @@ def test_run_tui_keyboard_interrupt(committed_repo: Repository) -> None:
 			patch("pygittools.tui.app.clear"),
 			patch("pygittools.tui.app.refresh"),
 			patch("pygittools.tui.app.endwin"),
-			patch("pygittools.tui.app.init_status_bar", return_value=0),
+			patch("pygittools.tui.app.init_tui_color_theme"),
 			patch("pygittools.tui.app.init_tab_colors"),
 			patch("pygittools.tui.app.draw_status_bar"),
+			patch("pygittools.tui.app.status_bar_attr", return_value=0),
 		):
 			assert run_tui(repo_dir) == 0
 
@@ -291,8 +292,9 @@ def test_run_tui_resize_key(committed_repo: Repository) -> None:
 			patch("pygittools.tui.app.clear"),
 			patch("pygittools.tui.app.refresh"),
 			patch("pygittools.tui.app.endwin"),
-			patch("pygittools.tui.app.init_status_bar", return_value=0),
+			patch("pygittools.tui.app.init_tui_color_theme"),
 			patch("pygittools.tui.app.init_tab_colors"),
 			patch("pygittools.tui.app.draw_status_bar"),
+			patch("pygittools.tui.app.status_bar_attr", return_value=0),
 		):
 			assert run_tui(repo_dir) == 0
diff --git a/pygittools/tui/git_colors.py b/pygittools/tui/git_colors.py
new file mode 100644
index 0000000..1da1534
--- /dev/null
+++ b/pygittools/tui/git_colors.py
@@ -0,0 +1,570 @@
+"""Load pgt terminal colors from the user's Git configuration."""
+
+from __future__ import annotations
+
+import json
+import math
+from dataclasses import dataclass
+from importlib.resources import files
+from typing import TYPE_CHECKING
+
+from pygit2 import Config
+from unicurses import (  # type: ignore[import-untyped]
+	A_BOLD,
+	A_DIM,
+	A_REVERSE,
+	A_UNDERLINE,
+	COLOR_BLACK,
+	COLOR_BLUE,
+	COLOR_CYAN,
+	COLOR_GREEN,
+	COLOR_PAIR,
+	COLOR_RED,
+	COLOR_WHITE,
+	COLOR_YELLOW,
+	can_change_color,
+	has_colors,
+	init_color,
+	init_pair,
+	start_color,
+	use_default_colors,
+)
+
+if TYPE_CHECKING:
+	from pygit2 import Repository
+
+_PAIR_STATUS = 1
+_PAIR_TAB_ACTIVE = 2
+_PAIR_STAGED = 3
+_PAIR_UNSTAGED = 4
+_PAIR_UNTRACKED = 5
+_PAIR_HEADER = 6
+_PAIR_BRANCH = 7
+_PAIR_SECTION_HEADER = 8
+_PAIR_LOG_HASH = 9
+_PAIR_LOG_DATE = 10
+_PAIR_LOG_SUBJECT = 11
+
+_NAMED_RGB: dict[str, tuple[int, int, int]] = {
+	"normal": (204, 204, 204),
+	"black": (0, 0, 0),
+	"red": (170, 0, 0),
+	"green": (0, 170, 0),
+	"yellow": (170, 170, 0),
+	"blue": (0, 0, 170),
+	"magenta": (170, 0, 170),
+	"cyan": (0, 170, 170),
+	"white": (204, 204, 204),
+}
+
+_BRIGHT_NAMED_RGB: dict[str, tuple[int, int, int]] = {
+	"black": (85, 85, 85),
+	"red": (255, 0, 0),
+	"green": (0, 255, 0),
+	"yellow": (255, 255, 0),
+	"blue": (0, 0, 255),
+	"magenta": (255, 0, 255),
+	"cyan": (0, 255, 255),
+	"white": (255, 255, 255),
+}
+
+_CURSES_RGB: tuple[tuple[int, int, int], ...] = (
+	(0, 0, 0),
+	(128, 0, 0),
+	(0, 128, 0),
+	(128, 128, 0),
+	(0, 0, 128),
+	(128, 0, 128),
+	(0, 128, 128),
+	(192, 192, 192),
+)
+
+_DEFAULT_SPECS: dict[str, str] = {
+	"color.status.added": "green",
+	"color.status.changed": "red",
+	"color.status.untracked": "red",
+	"color.status.header": "normal",
+	"color.status.branch": "cyan",
+	"color.status.nobranch": "red bold",
+	"color.branch.current": "green bold",
+	"color.branch.local": "cyan",
+	"color.diff.new": "green",
+	"color.diff.old": "red",
+	"color.diff.context": "normal",
+	"color.decorate.commit": "yellow",
+}
+
+# Light grey section dropdown labels; file rows keep color.status.* from Git.
+_DEFAULT_SECTION_HEADER_SPEC = "252"
+
+# Log date column default when Git has no matching slot.
+_DEFAULT_LOG_DATE_SPEC = "246"
+
+_FALSE_VALUES = frozenset({"0", "false", "never", "no", "off"})
+
+
+@dataclass(frozen=True, slots=True)
+class ParsedGitColor:
+	foreground: tuple[int, int, int] | None
+	background: tuple[int, int, int] | None
+	bold: bool = False
+	dim: bool = False
+	reverse: bool = False
+	underline: bool = False
+
+
+@dataclass(frozen=True, slots=True)
+class TuiColorTheme:
+	enabled: bool
+	status_bar: int
+	tab_active: int
+	tab_inactive: int
+	focus: int
+	staged: int
+	unstaged: int
+	untracked: int
+	section_header: int
+	header: int
+	branch: int
+	commit: int
+	log_hash: int
+	log_date: int
+	log_subject: int
+	normal: int
+
+
+_theme = TuiColorTheme(
+	enabled=False,
+	status_bar=A_REVERSE,
+	tab_active=A_BOLD,
+	tab_inactive=0,
+	focus=A_REVERSE,
+	staged=0,
+	unstaged=0,
+	untracked=0,
+	section_header=0,
+	header=A_BOLD,
+	branch=0,
+	commit=0,
+	log_hash=0,
+	log_date=0,
+	log_subject=0,
+	normal=0,
+)
+
+
+def current_theme() -> TuiColorTheme:
+	return _theme
+
+
+def status_bar_attr() -> int:
+	return _theme.status_bar
+
+
+def active_tab_attr() -> int:
+	return _theme.tab_active
+
+
+def init_tui_color_theme(repo: Repository | None) -> TuiColorTheme:
+	"""Read Git color settings and initialize curses color pairs."""
+	global _theme
+	config = _load_config(repo)
+	if not _colors_enabled(config):
+		_theme = _monochrome_theme()
+		return _theme
+
+	palette = _load_palette()
+	specs = _load_color_specs(config)
+	if not has_colors():
+		_theme = _attr_only_theme(specs, palette)
+		return _theme
+
+	start_color()
+	use_default_colors()
+	slots = _ColorSlotAllocator()
+	header_attr = _build_pair_attr(
+		specs.get("color.status.header", _DEFAULT_SPECS["color.status.header"]),
+		palette,
+		slots,
+		_PAIR_HEADER,
+		fallback_fg=COLOR_WHITE,
+		extra_attr=A_BOLD,
+	)
+	_theme = TuiColorTheme(
+		enabled=True,
+		status_bar=_build_pair_attr(
+			specs.get("color.status.header", _DEFAULT_SPECS["color.status.header"]),
+			palette,
+			slots,
+			_PAIR_STATUS,
+			fallback_fg=COLOR_WHITE,
+			fallback_bg=COLOR_BLUE,
+			background_rgb=(0, 0, 128),
+		),
+		tab_active=_build_pair_attr(
+			specs.get("color.branch.current", _DEFAULT_SPECS["color.branch.current"]),
+			palette,
+			slots,
+			_PAIR_TAB_ACTIVE,
+			fallback_fg=COLOR_BLACK,
+			fallback_bg=COLOR_YELLOW,
+		),
+		tab_inactive=0,
+		focus=A_REVERSE,
+		staged=_build_pair_attr(
+			specs.get("color.status.added", _DEFAULT_SPECS["color.status.added"]),
+			palette,
+			slots,
+			_PAIR_STAGED,
+			fallback_fg=COLOR_GREEN,
+		),
+		unstaged=_build_pair_attr(
+			specs.get("color.status.changed", _DEFAULT_SPECS["color.status.changed"]),
+			palette,
+			slots,
+			_PAIR_UNSTAGED,
+			fallback_fg=COLOR_RED,
+		),
+		untracked=_build_pair_attr(
+			specs.get("color.status.untracked", _DEFAULT_SPECS["color.status.untracked"]),
+			palette,
+			slots,
+			_PAIR_UNTRACKED,
+			fallback_fg=COLOR_RED,
+		),
+		section_header=_build_pair_attr(
+			_DEFAULT_SECTION_HEADER_SPEC,
+			palette,
+			slots,
+			_PAIR_SECTION_HEADER,
+			fallback_fg=COLOR_WHITE,
+		),
+		header=header_attr,
+		branch=_build_pair_attr(
+			specs.get("color.status.branch", _DEFAULT_SPECS["color.status.branch"]),
+			palette,
+			slots,
+			_PAIR_BRANCH,
+			fallback_fg=COLOR_CYAN,
+		),
+		commit=header_attr,
+		log_hash=_build_pair_attr(
+			specs.get("color.decorate.commit", _DEFAULT_SPECS["color.decorate.commit"]),
+			palette,
+			slots,
+			_PAIR_LOG_HASH,
+			fallback_fg=COLOR_YELLOW,
+		),
+		log_date=_build_pair_attr(
+			_DEFAULT_LOG_DATE_SPEC,
+			palette,
+			slots,
+			_PAIR_LOG_DATE,
+			fallback_fg=COLOR_WHITE,
+		),
+		log_subject=_build_pair_attr(
+			specs.get("color.diff.context", _DEFAULT_SPECS["color.diff.context"]),
+			palette,
+			slots,
+			_PAIR_LOG_SUBJECT,
+			fallback_fg=COLOR_WHITE,
+		),
+		normal=0,
+	)
+	return _theme
+
+
+def parse_git_color_spec(spec: str | None) -> ParsedGitColor:
+	if spec is None or not spec.strip():
+		return ParsedGitColor(foreground=None, background=None)
+	foreground: tuple[int, int, int] | None = None
+	background: tuple[int, int, int] | None = None
+	bold = False
+	dim = False
+	reverse = False
+	underline = False
+	palette = _load_palette()
+	for raw_token in spec.strip().split():
+		token = raw_token.casefold()
+		if token.startswith("no-"):
+			token = token[3:]
+		if token in {"bold", "standout"}:
+			bold = not raw_token.casefold().startswith("no")
+			continue
+		if token == "dim":
+			dim = not raw_token.casefold().startswith("no")
+			continue
+		if token == "reverse":
+			reverse = not raw_token.casefold().startswith("no")
+			continue
+		if token in {"ul", "underline"}:
+			underline = not raw_token.casefold().startswith("no")
+			continue
+		if token in {"italic", "blink", "strike"}:
+			continue
+		rgb = _token_to_rgb(raw_token, palette)
+		if rgb is None:
+			continue
+		if foreground is None:
+			foreground = rgb
+		elif background is None:
+			background = rgb
+	return ParsedGitColor(
+		foreground=foreground,
+		background=background,
+		bold=bold,
+		dim=dim,
+		reverse=reverse,
+		underline=underline,
+	)
+
+
+def _load_config(repo: Repository | None) -> Config:
+	if repo is not None:
+		return repo.config.snapshot()
+	return Config.get_global_config()
+
+
+def _config_value(config: Config, key: str) -> str | None:
+	try:
+		values = list(config.get_multivar(key))
+	except KeyError:
+		return None
+	if not values:
+		return None
+	return values[-1].strip()
+
+
+def _colors_enabled(config: Config) -> bool:
+	ui = _config_value(config, "color.ui")
+	if ui is not None and ui.casefold() in _FALSE_VALUES:
+		return False
+	status = _config_value(config, "color.status")
+	return status is None or status.casefold() not in _FALSE_VALUES
+
+
+def _load_color_specs(config: Config) -> dict[str, str]:
+	specs = dict(_DEFAULT_SPECS)
+	for key in _DEFAULT_SPECS:
+		value = _config_value(config, key)
+		if value:
+			specs[key] = value
+	return specs
+
+
+def _load_palette() -> tuple[tuple[int, int, int], ...]:
+	raw = json.loads(files("pygittools.tui").joinpath("colors.json").read_text(encoding="utf-8"))
+	palette: list[tuple[int, int, int]] = []
+	for entry in raw:
+		text = str(entry).strip()
+		if not text.startswith("#") or len(text) not in {4, 7}:
+			palette.append((0, 0, 0))
+			continue
+		if len(text) == 4:
+			text = f"#{text[1]}{text[1]}{text[2]}{text[2]}{text[3]}{text[3]}"
+		palette.append((int(text[1:3], 16), int(text[3:5], 16), int(text[5:7], 16)))
+	return tuple(palette)
+
+
+def _token_to_rgb(token: str, palette: tuple[tuple[int, int, int], ...]) -> tuple[int, int, int] | None:
+	lowered = token.casefold()
+	if lowered == "default":
+		return None
+	if token.startswith("#"):
+		text = token
+		if len(text) == 4:
+			text = f"#{text[1]}{text[1]}{text[2]}{text[2]}{text[3]}{text[3]}"
+		if len(text) == 7:
+			return (int(text[1:3], 16), int(text[3:5], 16), int(text[5:7], 16))
+		return None
+	if token.isdecimal():
+		index = int(token)
+		if 0 <= index < len(palette):
+			return palette[index]
+		return None
+	bright = False
+	name = lowered
+	if name.startswith("bright"):
+		bright = True
+		name = name[6:]
+	if name in _NAMED_RGB:
+		if bright and name in _BRIGHT_NAMED_RGB:
+			return _BRIGHT_NAMED_RGB[name]
+		red, green, blue = _NAMED_RGB[name]
+		return (red, green, blue)
+	return None
+
+
+class _ColorSlotAllocator:
+	def __init__(self) -> None:
+		self._next_custom = 1
+		self._rgb_by_slot: dict[int, tuple[int, int, int]] = {}
+
+	def slot_for(self, rgb: tuple[int, int, int]) -> int:
+		for slot, existing in self._rgb_by_slot.items():
+			if existing == rgb:
+				return slot
+		if can_change_color() and self._next_custom <= 6:
+			slot = self._next_custom
+			self._next_custom += 1
+			self._rgb_by_slot[slot] = rgb
+			init_color(slot, _curses_component(rgb[0]), _curses_component(rgb[1]), _curses_component(rgb[2]))
+			return slot
+		return _nearest_curses_index(rgb)
+
+
+def _build_pair_attr(
+	spec: str,
+	palette: tuple[tuple[int, int, int], ...],
+	slots: _ColorSlotAllocator,
+	pair_id: int,
+	*,
+	fallback_fg: int,
+	fallback_bg: int = COLOR_BLACK,
+	background_rgb: tuple[int, int, int] | None = None,
+	extra_attr: int = 0,
+) -> int:
+	parsed = parse_git_color_spec(spec)
+	fg_rgb = parsed.foreground
+	bg_rgb = parsed.background or background_rgb
+	fg = fallback_fg if fg_rgb is None else slots.slot_for(fg_rgb)
+	bg = fallback_bg if bg_rgb is None else slots.slot_for(bg_rgb)
+	init_pair(pair_id, fg, bg)
+	attr = COLOR_PAIR(pair_id)
+	if parsed.bold:
+		attr |= A_BOLD
+	if parsed.dim:
+		attr |= A_DIM
+	if parsed.underline:
+		attr |= A_UNDERLINE
+	if parsed.reverse:
+		attr |= A_REVERSE
+	return attr | extra_attr
+
+
+def _monochrome_theme() -> TuiColorTheme:
+	return TuiColorTheme(
+		enabled=False,
+		status_bar=A_REVERSE,
+		tab_active=A_BOLD,
+		tab_inactive=0,
+		focus=A_REVERSE,
+		staged=0,
+		unstaged=0,
+		untracked=0,
+		section_header=0,
+		header=A_BOLD,
+		branch=0,
+		commit=0,
+		log_hash=0,
+		log_date=0,
+		log_subject=0,
+		normal=0,
+	)
+
+
+def _attr_only_theme(specs: dict[str, str], palette: tuple[tuple[int, int, int], ...]) -> TuiColorTheme:
+	def attrs(key: str, *, extra: int = 0) -> int:
+		parsed = parse_git_color_spec(specs.get(key, _DEFAULT_SPECS[key]))
+		value = extra
+		if parsed.bold:
+			value |= A_BOLD
+		if parsed.dim:
+			value |= A_DIM
+		if parsed.underline:
+			value |= A_UNDERLINE
+		if parsed.reverse:
+			value |= A_REVERSE
+		return value
+
+	return TuiColorTheme(
+		enabled=False,
+		status_bar=attrs("color.status.header") or A_REVERSE,
+		tab_active=attrs("color.branch.current", extra=A_BOLD) or A_BOLD,
+		tab_inactive=0,
+		focus=A_REVERSE,
+		staged=attrs("color.status.added"),
+		unstaged=attrs("color.status.changed"),
+		untracked=attrs("color.status.untracked"),
+		section_header=_spec_attrs(_DEFAULT_SECTION_HEADER_SPEC),
+		header=attrs("color.status.header", extra=A_BOLD) or A_BOLD,
+		branch=attrs("color.status.branch"),
+		commit=attrs("color.diff.new"),
+		log_hash=attrs("color.decorate.commit"),
+		log_date=_spec_attrs(_DEFAULT_LOG_DATE_SPEC),
+		log_subject=attrs("color.diff.context"),
+		normal=0,
+	)
+
+
+def _spec_attrs(spec: str, *, extra: int = 0) -> int:
+	parsed = parse_git_color_spec(spec)
+	value = extra
+	if parsed.bold:
+		value |= A_BOLD
+	if parsed.dim:
+		value |= A_DIM
+	if parsed.underline:
+		value |= A_UNDERLINE
+	if parsed.reverse:
+		value |= A_REVERSE
+	return value
+
+
+def _nearest_curses_index(rgb: tuple[int, int, int]) -> int:
+	best_index = 0
+	best_distance = math.inf
+	for index, candidate in enumerate(_CURSES_RGB):
+		distance = _rgb_distance(rgb, candidate)
+		if distance < best_distance:
+			best_distance = distance
+			best_index = index
+	return best_index
+
+
+def _rgb_distance(left: tuple[int, int, int], right: tuple[int, int, int]) -> float:
+	return math.sqrt(sum((a - b) ** 2 for a, b in zip(left, right, strict=True)))
+
+
+def _curses_component(value: int) -> int:
+	return max(0, min(1000, round(value * 1000 / 255)))
+
+
+def focus_attr(base: int) -> int:
+	if base & A_REVERSE:
+		return base
+	return base | A_REVERSE
+
+
+def row_attr_for_section(
+	section: str | None,
+	*,
+	focused: bool,
+	highlight: bool,
+	is_header: bool = False,
+) -> int:
+	theme = _theme
+	if is_header:
+		base = theme.section_header
+	elif section == "staged":
+		base = theme.staged
+	elif section == "unstaged":
+		base = theme.unstaged
+	elif section == "untracked":
+		base = theme.untracked
+	else:
+		base = theme.normal
+	if focused and highlight:
+		return focus_attr(base)
+	return base
+
+
+def log_entry_attrs(*, focused: bool, highlight: bool) -> tuple[int, int, int]:
+	theme = _theme
+	if focused and highlight:
+		return (
+			focus_attr(theme.log_hash),
+			focus_attr(theme.log_date),
+			focus_attr(theme.log_subject),
+		)
+	return (theme.log_hash, theme.log_date, theme.log_subject)
diff --git a/pygittools/tui/git_colors_test.py b/pygittools/tui/git_colors_test.py
new file mode 100644
index 0000000..63641a7
--- /dev/null
+++ b/pygittools/tui/git_colors_test.py
@@ -0,0 +1,77 @@
+from __future__ import annotations
+
+from pygit2 import Repository
+
+from pygittools.tui.git_colors import (
+	_colors_enabled,
+	_load_color_specs,
+	_load_palette,
+	_token_to_rgb,
+	init_tui_color_theme,
+	log_entry_attrs,
+	parse_git_color_spec,
+)
+
+
+def test_parse_git_color_spec_256_index() -> None:
+	palette = _load_palette()
+	parsed = parse_git_color_spec("36")
+	assert parsed.foreground == palette[36]
+	assert parsed.bold is False
+
+
+def test_parse_git_color_spec_named_bold() -> None:
+	parsed = parse_git_color_spec("cyan bold")
+	assert parsed.foreground == (0, 170, 170)
+	assert parsed.bold is True
+
+
+def test_parse_git_color_spec_hex_and_background() -> None:
+	parsed = parse_git_color_spec("#ff0000 #0000ff reverse")
+	assert parsed.foreground == (255, 0, 0)
+	assert parsed.background == (0, 0, 255)
+	assert parsed.reverse is True
+
+
+def test_token_to_rgb_bright_named() -> None:
+	palette = _load_palette()
+	rgb = _token_to_rgb("brightred", palette)
+	assert rgb == (255, 0, 0)
+
+
+def test_load_color_specs_from_repo_config(committed_repo: Repository) -> None:
+	committed_repo.config["color.status.added"] = "33"
+	committed_repo.config["color.status.changed"] = "38 bold"
+	specs = _load_color_specs(committed_repo.config.snapshot())
+	assert specs["color.status.added"] == "33"
+	assert specs["color.status.changed"] == "38 bold"
+	assert specs["color.status.untracked"] == "red"
+
+
+def test_colors_disabled_when_ui_false(committed_repo: Repository) -> None:
+	committed_repo.config["color.ui"] = "false"
+	assert _colors_enabled(committed_repo.config.snapshot()) is False
+
+
+def test_init_theme_from_repo(committed_repo: Repository) -> None:
+	committed_repo.config["color.status.added"] = "36"
+	committed_repo.config["color.status.changed"] = "38"
+	committed_repo.config["color.status.untracked"] = "7"
+	theme = init_tui_color_theme(committed_repo)
+	assert theme.staged != 0 or theme.enabled is False
+
+
+def test_init_theme_without_repo() -> None:
+	theme = init_tui_color_theme(None)
+	assert theme.status_bar != 0
+
+
+def test_section_header_defaults_differ_from_file_rows() -> None:
+	theme = init_tui_color_theme(None)
+	assert theme.section_header != theme.staged or theme.staged == 0
+
+
+def test_log_entry_attrs_returns_three_values() -> None:
+	init_tui_color_theme(None)
+	hash_attr, date_attr, subject_attr = log_entry_attrs(focused=False, highlight=True)
+	assert hash_attr != 0 or date_attr != 0 or subject_attr != 0
diff --git a/pygittools/tui/log_list.py b/pygittools/tui/log_list.py
index e3184b4..f7ef5f5 100644
--- a/pygittools/tui/log_list.py
+++ b/pygittools/tui/log_list.py
@@ -7,13 +7,13 @@ from datetime import UTC, datetime
 
 from pygit2 import GIT_SORT_TIME, Commit, GitError, Repository
 from unicurses import (  # type: ignore[import-untyped]
-	A_REVERSE,
 	KEY_DOWN,
 	KEY_RESIZE,
 	KEY_UP,
 )
 
-from pygittools.tui.draw import draw_line
+from pygittools.tui.draw import clear_row, draw_text
+from pygittools.tui.git_colors import current_theme, log_entry_attrs
 
 _MAX_COMMITS = 500
 
@@ -49,7 +49,7 @@ class CommitLogList:
 		if height <= 0:
 			return
 		if not self._entries:
-			draw_line(top_row, 0, "No commits", width)
+			draw_text(top_row, 0, "No commits", width, current_theme().log_date)
 			return
 		self._ensure_cursor_visible(height)
 		for view_row in range(height):
@@ -57,9 +57,8 @@ class CommitLogList:
 			if index >= len(self._entries):
 				break
 			entry = self._entries[index]
-			label = _format_entry(entry, width)
-			attr = A_REVERSE if highlight and index == self._cursor else 0
-			draw_line(top_row + view_row, 0, label, width, attr)
+			focused = highlight and index == self._cursor
+			_draw_entry(top_row + view_row, width, entry, focused=focused, highlight=highlight)
 
 	def handle_nav_key(self, key: int, repo: Repository) -> LogNavKeyResult:
 		if key == KEY_RESIZE:
@@ -86,6 +85,18 @@ class CommitLogList:
 			self._scroll_offset = self._cursor - height + 1
 
 
+def _draw_entry(row: int, width: int, entry: _LogEntry, *, focused: bool, highlight: bool) -> None:
+	clear_row(row)
+	hash_attr, date_attr, subject_attr = log_entry_attrs(focused=focused, highlight=highlight)
+	col = draw_text(row, 0, f"{entry.short_id} ", width, hash_attr)
+	col = draw_text(row, col, f"{entry.date} ", width, date_attr)
+	remaining = max(0, width - col)
+	subject = entry.subject
+	if len(subject) > remaining:
+		subject = subject[: max(0, remaining - 1)] + "…"
+	draw_text(row, col, subject, width, subject_attr)
+
+
 def _load_entries(repo: Repository) -> list[_LogEntry]:
 	try:
 		head = repo.head.target
diff --git a/pygittools/tui/log_list_test.py b/pygittools/tui/log_list_test.py
index 81e297f..125b6f0 100644
--- a/pygittools/tui/log_list_test.py
+++ b/pygittools/tui/log_list_test.py
@@ -7,13 +7,12 @@ from pathlib import Path
 
 import pygit2
 import pytest
-from pygit2 import Signature
-from unicurses import KEY_DOWN, KEY_RESIZE, KEY_UP
 
-from pygittools.tui.log_list import CommitLogList, _format_entry, _load_entries
+from pygittools.tui.git_colors import init_tui_color_theme
+from pygittools.tui.log_list import CommitLogList, _draw_entry, _format_entry, _LogEntry
 from pygittools.tui.project_label import repo_project_label
 
-SIG = Signature("t", "t@example.com", 0)
+SIG = pygit2.Signature("t", "t@example.com", 0)
 
 
 def test_repo_project_label_falls_back_to_worktree(tmp_path: Path) -> None:
@@ -52,12 +51,20 @@ def test_commit_log_list_navigation(tmp_path: Path) -> None:
 	repo.create_commit("HEAD", SIG, SIG, "one", tree, [])
 	log = CommitLogList()
 	log.refresh(repo)
+	from unicurses import KEY_DOWN, KEY_RESIZE, KEY_UP
+
 	log.handle_nav_key(KEY_DOWN, repo)
 	log.handle_nav_key(KEY_UP, repo)
 	log.handle_nav_key(KEY_RESIZE, repo)
 	log.handle_nav_key(ord("x"), repo)
 
 
+def test_draw_entry_uses_git_colors() -> None:
+	init_tui_color_theme(None)
+	entry = _LogEntry(short_id="abc1234", date="2026-01-01", subject="feat: hello")
+	_draw_entry(0, 80, entry, focused=True, highlight=True)
+
+
 def test_format_entry_truncates_long_subject() -> None:
 	from pygittools.tui.log_list import _LogEntry
 
@@ -67,7 +74,11 @@ def test_format_entry_truncates_long_subject() -> None:
 
 
 def test_load_entries_without_head(repo: pygit2.Repository) -> None:
-	empty = pygit2.init_repository(str(Path(repo.workdir).parent / "bare"), bare=True)
+	from pygit2 import init_repository
+
+	from pygittools.tui.log_list import _load_entries
+
+	empty = init_repository(str(Path(repo.workdir).parent / "bare"), bare=True)
 	assert _load_entries(empty) == []
 
 
diff --git a/pygittools/tui/pages/home.py b/pygittools/tui/pages/home.py
index 23c70c1..2eabe74 100644
--- a/pygittools/tui/pages/home.py
+++ b/pygittools/tui/pages/home.py
@@ -5,7 +5,6 @@ from __future__ import annotations
 from typing import Literal
 
 from unicurses import (  # type: ignore[import-untyped]
-	A_REVERSE,
 	KEY_RESIZE,
 )
 
@@ -13,6 +12,7 @@ from pygittools.tui.branches import current_branch_name
 from pygittools.tui.changes_list import ChangesList
 from pygittools.tui.commit_draft import load_commit_draft
 from pygittools.tui.draw import draw_line
+from pygittools.tui.git_colors import current_theme, focus_attr
 from pygittools.tui.input_context import InputContext, InputMode, input_context, nav_context
 from pygittools.tui.log_list import CommitLogList
 from pygittools.tui.project_label import repo_project_label
@@ -89,10 +89,14 @@ class HomePage:
 
 		if height > _PROJECT_ROW:
 			active_index = 0 if self._active_tab == "status" else 1
-			prefix_attr = A_REVERSE if self._header_focus == "repo" else 0
+			theme = current_theme()
+			prefix_attr = focus_attr(theme.branch) if self._header_focus == "repo" else 0
 			draw_tab_row(_PROJECT_ROW, width, project_label, _TABS, active_index, prefix_attr=prefix_attr)
 		if height > _BRANCH_ROW:
-			branch_attr = A_REVERSE if self._header_focus == "branch" else 0
+			theme = current_theme()
+			branch_attr = theme.branch
+			if self._header_focus == "branch":
+				branch_attr = focus_attr(branch_attr)
 			draw_line(_BRANCH_ROW, 0, f"Branch: {branch}", width, branch_attr)
 
 		list_height = max(0, height - _HEADER_ROWS)
diff --git a/pygittools/tui/status_bar.py b/pygittools/tui/status_bar.py
index c1c1a39..a63eeb7 100644
--- a/pygittools/tui/status_bar.py
+++ b/pygittools/tui/status_bar.py
@@ -2,30 +2,9 @@
 
 from __future__ import annotations
 
-from unicurses import (  # type: ignore[import-untyped]
-	A_REVERSE,
-	COLOR_BLUE,
-	COLOR_PAIR,
-	COLOR_WHITE,
-	has_colors,
-	init_pair,
-	mvaddstr,
-	start_color,
-	use_default_colors,
-)
-
-_STATUS_PAIR = 1
-_DEFAULT_HINT = "q quit"
-
+from unicurses import mvaddstr  # type: ignore[import-untyped]
 
-def init_status_bar() -> int:
-	"""Initialize status bar colors and return the text attribute to use."""
-	if has_colors():
-		start_color()
-		use_default_colors()
-		init_pair(_STATUS_PAIR, COLOR_WHITE, COLOR_BLUE)
-		return COLOR_PAIR(_STATUS_PAIR)
-	return A_REVERSE
+_DEFAULT_HINT = "q quit"
 
 
 def content_height(full_height: int) -> int:
diff --git a/pygittools/tui/status_bar_test.py b/pygittools/tui/status_bar_test.py
index 3a26319..d9f8ba4 100644
--- a/pygittools/tui/status_bar_test.py
+++ b/pygittools/tui/status_bar_test.py
@@ -1,50 +1,38 @@
 from __future__ import annotations
 
-from unittest.mock import MagicMock
-
-import pytest
-
-from pygittools.tui import status_bar
+from pygittools.tui.git_colors import init_tui_color_theme, status_bar_attr
+from pygittools.tui.status_bar import _format_line, content_height, draw_status_bar
 
 
 def test_content_height_reserves_status_row() -> None:
-	assert status_bar.content_height(10) == 9
-	assert status_bar.content_height(0) == 0
+	assert content_height(10) == 9
+	assert content_height(0) == 0
 
 
 def test_format_line_with_hint() -> None:
-	line = status_bar._format_line("left text", "q quit", 40)
+	line = _format_line("left text", "q quit", 40)
 	assert line.endswith("q quit")
 	assert "left" in line
 
 
 def test_format_line_without_hint() -> None:
-	assert status_bar._format_line("hello", "", 10) == "hello     "
+	assert _format_line("hello", "", 10) == "hello     "
 
 
 def test_format_line_hint_wider_than_width() -> None:
-	line = status_bar._format_line("ignored", "longhint", 4)
+	line = _format_line("ignored", "longhint", 4)
 	assert line.strip() == "long"
 
 
 def test_format_line_tight_width() -> None:
-	line = status_bar._format_line("left", "hint", 4)
+	line = _format_line("left", "hint", 4)
 	assert len(line) == 4
 
 
 def test_draw_status_bar_skips_zero_width() -> None:
-	status_bar.draw_status_bar(0, 0, "x", 0)
-
-
-def test_init_status_bar_without_colors(monkeypatch: pytest.MonkeyPatch) -> None:
-	monkeypatch.setattr(status_bar, "has_colors", lambda: False)
-	assert status_bar.init_status_bar() == status_bar.A_REVERSE
+	draw_status_bar(0, 0, "x", 0)
 
 
-def test_init_status_bar_with_colors(monkeypatch: pytest.MonkeyPatch) -> None:
-	monkeypatch.setattr(status_bar, "has_colors", lambda: True)
-	monkeypatch.setattr(status_bar, "start_color", MagicMock())
-	monkeypatch.setattr(status_bar, "use_default_colors", MagicMock())
-	monkeypatch.setattr(status_bar, "init_pair", MagicMock())
-	attr = status_bar.init_status_bar()
-	assert attr == status_bar.COLOR_PAIR(status_bar._STATUS_PAIR)
+def test_status_bar_attr_after_init() -> None:
+	init_tui_color_theme(None)
+	assert status_bar_attr() != 0
diff --git a/pygittools/tui/tabs.py b/pygittools/tui/tabs.py
index c144e56..024bbfd 100644
--- a/pygittools/tui/tabs.py
+++ b/pygittools/tui/tabs.py
@@ -2,20 +2,10 @@
 
 from __future__ import annotations
 
-from unicurses import (  # type: ignore[import-untyped]
-	A_BOLD,
-	COLOR_BLACK,
-	COLOR_PAIR,
-	COLOR_YELLOW,
-	has_colors,
-	init_pair,
-)
-
 from pygittools.tui.draw import clear_row, draw_text
+from pygittools.tui.git_colors import active_tab_attr, current_theme
 
 _TAB_GAP = "  "
-_TAB_ACTIVE_PAIR = 2
-_active_tab_attr = 0
 
 
 def tab_row_layout(prefix: str, tab_labels: tuple[str, ...]) -> list[tuple[int, str]]:
@@ -31,13 +21,8 @@ def tab_row_layout(prefix: str, tab_labels: tuple[str, ...]) -> list[tuple[int, 
 
 
 def init_tab_colors() -> None:
-	"""Initialize the color pair used to highlight the active tab."""
-	global _active_tab_attr
-	if has_colors():
-		init_pair(_TAB_ACTIVE_PAIR, COLOR_BLACK, COLOR_YELLOW)
-		_active_tab_attr = COLOR_PAIR(_TAB_ACTIVE_PAIR)
-	else:
-		_active_tab_attr = A_BOLD
+	"""Refresh cached tab attributes from the active Git color theme."""
+	return None
 
 
 def draw_tab_row(
@@ -57,10 +42,12 @@ def draw_tab_row(
 	col = 0
 	col = draw_text(row, col, clipped_prefix, width, prefix_attr)
 	col = draw_text(row, col, _TAB_GAP, width, 0)
+	tab_active = active_tab_attr()
+	tab_inactive = current_theme().tab_inactive
 	for index, label in enumerate(tab_labels):
 		if index > 0:
 			col = draw_text(row, col, _TAB_GAP, width, 0)
-		tab_attr = _active_tab_attr if index == active_index else 0
+		tab_attr = tab_active if index == active_index else tab_inactive
 		col = draw_text(row, col, label, width, tab_attr)
 
 
diff --git a/pygittools/tui/tabs_test.py b/pygittools/tui/tabs_test.py
index b14057f..bb2b0c3 100644
--- a/pygittools/tui/tabs_test.py
+++ b/pygittools/tui/tabs_test.py
@@ -1,44 +1,16 @@
-from __future__ import annotations
-
-from unittest.mock import MagicMock
-
-import pytest
-
-from pygittools.tui import tabs
-
-
-def test_init_tab_colors_without_colors(monkeypatch: pytest.MonkeyPatch) -> None:
-	monkeypatch.setattr(tabs, "has_colors", lambda: False)
-	tabs.init_tab_colors()
-	assert tabs._active_tab_attr == tabs.A_BOLD
-
-
-def test_init_tab_colors_with_colors(monkeypatch: pytest.MonkeyPatch) -> None:
-	monkeypatch.setattr(tabs, "has_colors", lambda: True)
-	monkeypatch.setattr(tabs, "init_pair", MagicMock())
-	tabs.init_tab_colors()
-	assert tabs._active_tab_attr == tabs.COLOR_PAIR(tabs._TAB_ACTIVE_PAIR)
-
-
-def test_draw_tab_row_zero_width() -> None:
-	tabs.draw_tab_row(0, 0, "prefix", ("A",), 0)
-
-
-def test_draw_tab_row_renders_tabs() -> None:
-	tabs.draw_tab_row(0, 80, "project", ("Status", "Log"), 1, prefix_attr=1)
+"""Tests for tab bar layout."""
 
+from __future__ import annotations
 
-def test_tabs_reserved_width_empty() -> None:
-	assert tabs._tabs_reserved_width(()) == 0
+from pygittools.tui.git_colors import init_tui_color_theme
+from pygittools.tui.tabs import draw_tab_row, tab_row_layout
 
 
-def test_clip_prefix_truncates_with_ellipsis() -> None:
-	clipped = tabs._clip_prefix("very-long-project-name", 20, ("Status", "Log"))
-	assert clipped.endswith("…")
-	assert len(clipped) <= 20 - tabs._tabs_reserved_width(("Status", "Log"))
+def test_tab_row_layout_places_tabs_after_prefix() -> None:
+	layout = tab_row_layout("pygitweb", ("Status", "Log"))
+	assert layout == [(10, "Status"), (18, "Log")]
 
 
-def test_clip_prefix_short_max() -> None:
-	clipped = tabs._clip_prefix("hello", 10, ("Status",))
-	assert len(clipped) <= 10
-	assert clipped.startswith("h")
+def test_draw_tab_row_renders_without_error() -> None:
+	init_tui_color_theme(None)
+	draw_tab_row(0, 80, "project", ("Status", "Log"), 1, prefix_attr=0)
