diff --git a/pygittools/tui/git_colors.py b/pygittools/tui/git_colors.py
index 2416f43..14fed8e 100644
--- a/pygittools/tui/git_colors.py
+++ b/pygittools/tui/git_colors.py
@@ -3,50 +3,16 @@
 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 pygittools.tui.ucurses 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,
-)
+from pyratatui import Color, Modifier, Style
 
 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
-_PAIR_COMMIT = 12
-
 _NAMED_RGB: dict[str, tuple[int, int, int]] = {
 	"normal": (204, 204, 204),
 	"black": (0, 0, 0),
@@ -70,17 +36,6 @@ _BRIGHT_NAMED_RGB: dict[str, tuple[int, int, int]] = {
 	"white": (255, 255, 255),
 }
 
-_CURSES_RGB: tuple[tuple[int, int, int], ...] = (
-	(0, 0, 0),
-	(205, 0, 0),
-	(0, 205, 0),
-	(205, 205, 0),
-	(0, 0, 238),
-	(205, 0, 205),
-	(0, 205, 205),
-	(229, 229, 229),
-)
-
 _DEFAULT_SPECS: dict[str, str] = {
 	"color.status.added": "green",
 	"color.status.changed": "red",
@@ -96,12 +51,8 @@ _DEFAULT_SPECS: dict[str, str] = {
 	"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"})
 
 
@@ -118,40 +69,44 @@ class ParsedGitColor:
 @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
+	status_bar: Style
+	tab_active: Style
+	tab_inactive: Style
+	focus: Style
+	staged: Style
+	unstaged: Style
+	untracked: Style
+	section_header: Style
+	header: Style
+	branch: Style
+	commit: Style
+	log_hash: Style
+	log_date: Style
+	log_subject: Style
+	normal: Style
+
+
+def _style() -> Style:
+	return Style()
 
 
 _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,
+	status_bar=_style().add_modifier(Modifier.reversed()),
+	tab_active=_style().bold(),
+	tab_inactive=_style(),
+	focus=_style().add_modifier(Modifier.reversed()),
+	staged=_style(),
+	unstaged=_style(),
+	untracked=_style(),
+	section_header=_style(),
+	header=_style().bold(),
+	branch=_style(),
+	commit=_style(),
+	log_hash=_style(),
+	log_date=_style(),
+	log_subject=_style(),
+	normal=_style(),
 )
 
 
@@ -159,16 +114,16 @@ def current_theme() -> TuiColorTheme:
 	return _theme
 
 
-def status_bar_attr() -> int:
+def status_bar_style() -> Style:
 	return _theme.status_bar
 
 
-def active_tab_attr() -> int:
+def active_tab_style() -> Style:
 	return _theme.tab_active
 
 
 def init_tui_color_theme(repo: Repository | None) -> TuiColorTheme:
-	"""Read Git color settings and initialize curses color pairs."""
+	"""Read Git color settings and build pyratatui styles."""
 	global _theme
 	config = _load_config(repo)
 	if not _colors_enabled(config):
@@ -177,108 +132,76 @@ def init_tui_color_theme(repo: Repository | None) -> TuiColorTheme:
 
 	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(
+	header = _style_from_spec(
 		specs.get("color.status.header", _DEFAULT_SPECS["color.status.header"]),
 		palette,
-		slots,
-		_PAIR_HEADER,
-		fallback_fg=COLOR_WHITE,
-		extra_attr=A_BOLD,
+		fallback_fg=Color.white(),
+		extra_bold=True,
 	)
 	_theme = TuiColorTheme(
 		enabled=True,
-		status_bar=_build_pair_attr(
+		status_bar=_style_from_spec(
 			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),
+			fallback_fg=Color.white(),
+			fallback_bg=Color.blue(),
 		),
-		tab_active=_build_pair_attr(
+		tab_active=_style_from_spec(
 			specs.get("color.branch.current", _DEFAULT_SPECS["color.branch.current"]),
 			palette,
-			slots,
-			_PAIR_TAB_ACTIVE,
-			fallback_fg=39,
-			fallback_bg=8,
+			fallback_fg=Color.indexed(39),
+			fallback_bg=Color.indexed(8),
 			ignore_spec_background=True,
 		),
-		tab_inactive=0,
-		focus=A_REVERSE,
-		staged=_build_pair_attr(
+		tab_inactive=_style(),
+		focus=_style().add_modifier(Modifier.reversed()),
+		staged=_style_from_spec(
 			specs.get("color.status.added", _DEFAULT_SPECS["color.status.added"]),
 			palette,
-			slots,
-			_PAIR_STAGED,
-			fallback_fg=COLOR_GREEN,
+			fallback_fg=Color.green(),
 		),
-		unstaged=_build_pair_attr(
+		unstaged=_style_from_spec(
 			specs.get("color.status.changed", _DEFAULT_SPECS["color.status.changed"]),
 			palette,
-			slots,
-			_PAIR_UNSTAGED,
-			fallback_fg=COLOR_RED,
+			fallback_fg=Color.red(),
 		),
-		untracked=_build_pair_attr(
+		untracked=_style_from_spec(
 			specs.get("color.status.untracked", _DEFAULT_SPECS["color.status.untracked"]),
 			palette,
-			slots,
-			_PAIR_UNTRACKED,
-			fallback_fg=COLOR_RED,
+			fallback_fg=Color.red(),
 		),
-		section_header=_build_pair_attr(
+		section_header=_style_from_spec(
 			_DEFAULT_SECTION_HEADER_SPEC,
 			palette,
-			slots,
-			_PAIR_SECTION_HEADER,
-			fallback_fg=COLOR_WHITE,
+			fallback_fg=Color.white(),
 		),
-		header=header_attr,
-		branch=_build_pair_attr(
+		header=header,
+		branch=_style_from_spec(
 			specs.get("color.status.branch", _DEFAULT_SPECS["color.status.branch"]),
 			palette,
-			slots,
-			_PAIR_BRANCH,
-			fallback_fg=COLOR_CYAN,
+			fallback_fg=Color.cyan(),
 		),
-		commit=_build_pair_attr(
+		commit=_style_from_spec(
 			specs.get("color.diff.new", _DEFAULT_SPECS["color.diff.new"]),
 			palette,
-			slots,
-			_PAIR_COMMIT,
-			fallback_fg=COLOR_GREEN,
+			fallback_fg=Color.green(),
 		),
-		log_hash=_build_pair_attr(
+		log_hash=_style_from_spec(
 			specs.get("color.decorate.commit", _DEFAULT_SPECS["color.decorate.commit"]),
 			palette,
-			slots,
-			_PAIR_LOG_HASH,
-			fallback_fg=COLOR_YELLOW,
+			fallback_fg=Color.yellow(),
 		),
-		log_date=_build_pair_attr(
+		log_date=_style_from_spec(
 			_DEFAULT_LOG_DATE_SPEC,
 			palette,
-			slots,
-			_PAIR_LOG_DATE,
-			fallback_fg=COLOR_WHITE,
+			fallback_fg=Color.white(),
 		),
-		log_subject=_build_pair_attr(
+		log_subject=_style_from_spec(
 			specs.get("color.diff.context", _DEFAULT_SPECS["color.diff.context"]),
 			palette,
-			slots,
-			_PAIR_LOG_SUBJECT,
-			fallback_fg=COLOR_WHITE,
+			fallback_fg=Color.white(),
 		),
-		normal=0,
+		normal=_style(),
 	)
 	return _theme
 
@@ -328,6 +251,44 @@ def parse_git_color_spec(spec: str | None) -> ParsedGitColor:
 	)
 
 
+def focus_style(base: Style) -> Style:
+	return base.patch(_theme.focus)
+
+
+def row_style_for_section(
+	section: str | None,
+	*,
+	focused: bool,
+	highlight: bool,
+	is_header: bool = False,
+) -> Style:
+	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_style(base)
+	return base
+
+
+def log_entry_styles(*, focused: bool, highlight: bool) -> tuple[Style, Style, Style]:
+	theme = _theme
+	if focused and highlight:
+		return (
+			focus_style(theme.log_hash),
+			focus_style(theme.log_date),
+			focus_style(theme.log_subject),
+		)
+	return (theme.log_hash, theme.log_date, theme.log_subject)
+
+
 def _load_config(repo: Repository | None) -> Config:
 	if repo is not None:
 		return repo.config.snapshot()
@@ -399,209 +360,60 @@ def _token_to_rgb(token: str, palette: tuple[tuple[int, int, int], ...]) -> tupl
 	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 _NAMED_RGB[name]
 	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 _color_from_rgb(rgb: tuple[int, int, int] | None, fallback: Color) -> Color:
+	if rgb is None:
+		return fallback
+	return Color.rgb(rgb[0], rgb[1], rgb[2])
 
 
-def _nearest_palette_index(
-	rgb: tuple[int, int, int],
-	palette: tuple[tuple[int, int, int], ...],
-) -> int:
-	best_index = 0
-	best_distance = math.inf
-	for index, candidate in enumerate(palette):
-		distance = _rgb_distance(rgb, candidate)
-		if distance < best_distance:
-			best_distance = distance
-			best_index = index
-	return best_index
-
-
-def _build_pair_attr(
+def _style_from_spec(
 	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,
+	fallback_fg: Color,
+	fallback_bg: Color | None = None,
+	extra_bold: bool = False,
 	ignore_spec_background: bool = False,
-) -> int:
+) -> Style:
+	del palette
 	parsed = parse_git_color_spec(spec)
-	fg_rgb = parsed.foreground
-	bg_rgb = (None if ignore_spec_background else parsed.background) or background_rgb
-
-	def index_for(
-		value: tuple[int, int, int] | None,
-		fallback: int,
-	) -> int:
-		if value is None:
-			return fallback
-		for idx, candidate in enumerate(palette):
-			if candidate == value:
-				return idx
-		return _nearest_palette_index(value, palette)
-
-	fg_index = index_for(fg_rgb, fallback_fg)
-	bg_index = index_for(bg_rgb, fallback_bg)
-
-	init_pair(pair_id, fg_index, bg_index)
-	attr = int(COLOR_PAIR(pair_id))
-	if parsed.bold:
-		attr |= A_BOLD
+	style = _style().fg(_color_from_rgb(parsed.foreground, fallback_fg))
+	bg = None if ignore_spec_background else parsed.background
+	if bg is not None:
+		style = style.bg(Color.rgb(bg[0], bg[1], bg[2]))
+	elif fallback_bg is not None:
+		style = style.bg(fallback_bg)
+	if parsed.bold or extra_bold:
+		style = style.bold()
 	if parsed.dim:
-		attr |= A_DIM
+		style = style.dim()
 	if parsed.underline:
-		attr |= A_UNDERLINE
+		style = style.underlined()
 	if parsed.reverse:
-		attr |= A_REVERSE
-	return attr | extra_attr
+		style = style.add_modifier(Modifier.reversed())
+	return style
 
 
 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,
+		status_bar=_style().add_modifier(Modifier.reversed()),
+		tab_active=_style().bold(),
+		tab_inactive=_style(),
+		focus=_style().add_modifier(Modifier.reversed()),
+		staged=_style(),
+		unstaged=_style(),
+		untracked=_style(),
+		section_header=_style(),
+		header=_style().bold(),
+		branch=_style(),
+		commit=_style(),
+		log_hash=_style(),
+		log_date=_style(),
+		log_subject=_style(),
+		normal=_style(),
 	)
-
-
-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
index 39d95a0..e2053c4 100644
--- a/pygittools/tui/git_colors_test.py
+++ b/pygittools/tui/git_colors_test.py
@@ -11,7 +11,7 @@ from pygittools.tui.git_colors import (
 	_load_palette,
 	_token_to_rgb,
 	init_tui_color_theme,
-	log_entry_attrs,
+	log_entry_styles,
 	parse_git_color_spec,
 )
 
@@ -66,20 +66,23 @@ def test_init_theme_from_repo(committed_repo: Repository) -> None:
 	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
+	assert theme.staged is not None
 
 
 def test_init_theme_without_repo() -> None:
 	theme = init_tui_color_theme(None)
-	assert theme.status_bar != 0
+	assert theme.status_bar is not None
 
 
 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
+	assert theme.section_header is not None
+	assert theme.staged is not None
 
 
-def test_log_entry_attrs_returns_three_values() -> None:
+def test_log_entry_styles_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
+	hash_style, date_style, subject_style = log_entry_styles(focused=False, highlight=True)
+	assert hash_style is not None
+	assert date_style is not None
+	assert subject_style is not None
