diff --git a/CLAUDE.md b/CLAUDE.md
index 9bd6581..bc95b44 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -80,6 +80,7 @@ The constitution (`.specify/memory/constitution.md`) governs all feature work:
- TypeScript 5.8 (strict mode, verbatimModuleSyntax) + React 19, Tailwind CSS v4, Lucide React (icons), Vite 6 (026-roll-initiative)
- N/A (no storage changes — existing localStorage persistence handles initiative via `setInitiativeUseCase`) (026-roll-initiative)
- N/A (no storage changes — purely presentational) (027-ui-polish)
+- TypeScript 5.8 (strict mode, verbatimModuleSyntax) + React 19, Tailwind CSS v4, Vite 6 + Tailwind CSS v4 (CSS-native `@theme` theming), Lucide React (icons) (028-semantic-hover-tokens)
## Recent Changes
- 003-remove-combatant: Added TypeScript 5.x (strict mode, verbatimModuleSyntax) + React 19, Vite
diff --git a/apps/web/src/components/ac-shield.tsx b/apps/web/src/components/ac-shield.tsx
index a7cc913..e276c41 100644
--- a/apps/web/src/components/ac-shield.tsx
+++ b/apps/web/src/components/ac-shield.tsx
@@ -12,7 +12,7 @@ export function AcShield({ value, onClick, className }: AcShieldProps) {
type="button"
onClick={onClick}
className={cn(
- "relative inline-flex items-center justify-center text-sm tabular-nums text-muted-foreground transition-colors hover:text-primary",
+ "relative inline-flex items-center justify-center text-sm tabular-nums text-muted-foreground transition-colors hover:text-hover-neutral",
className,
)}
style={{ width: 28, height: 32 }}
diff --git a/apps/web/src/components/action-bar.tsx b/apps/web/src/components/action-bar.tsx
index fcdcebf..c1c6bad 100644
--- a/apps/web/src/components/action-bar.tsx
+++ b/apps/web/src/components/action-bar.tsx
@@ -107,7 +107,7 @@ export function ActionBar({
className={`flex w-full items-center justify-between px-3 py-1.5 text-left text-sm ${
i === suggestionIndex
? "bg-accent/20 text-foreground"
- : "text-foreground hover:bg-accent/10"
+ : "text-foreground hover:bg-hover-neutral-bg"
}`}
onClick={() => handleSelectSuggestion(creature)}
onMouseEnter={() => setSuggestionIndex(i)}
diff --git a/apps/web/src/components/bestiary-search.tsx b/apps/web/src/components/bestiary-search.tsx
index 2d1c24d..9ceb438 100644
--- a/apps/web/src/components/bestiary-search.tsx
+++ b/apps/web/src/components/bestiary-search.tsx
@@ -87,7 +87,7 @@ export function BestiarySearch({
@@ -108,7 +108,7 @@ export function BestiarySearch({
className={`flex w-full items-center justify-between px-3 py-1.5 text-left text-sm ${
i === highlightIndex
? "bg-accent/20 text-foreground"
- : "text-foreground hover:bg-accent/10"
+ : "text-foreground hover:bg-hover-neutral-bg"
}`}
onClick={() => onSelectCreature(creature)}
onMouseEnter={() => setHighlightIndex(i)}
diff --git a/apps/web/src/components/combatant-row.tsx b/apps/web/src/components/combatant-row.tsx
index 5fb2c9d..02b59eb 100644
--- a/apps/web/src/components/combatant-row.tsx
+++ b/apps/web/src/components/combatant-row.tsx
@@ -91,7 +91,7 @@ function EditableName({
e.stopPropagation();
startEditing();
}}
- className="truncate text-left text-sm text-foreground hover:text-primary transition-colors"
+ className="truncate text-left text-sm text-foreground hover:text-hover-neutral transition-colors"
>
{name}
@@ -150,7 +150,7 @@ function MaxHpDisplay({
@@ -190,7 +190,7 @@ function ClickableHp({
type="button"
onClick={() => setPopoverOpen(true)}
className={cn(
- "inline-block h-7 min-w-[3ch] text-center text-sm font-medium leading-7 tabular-nums transition-colors hover:text-primary",
+ "inline-block h-7 min-w-[3ch] text-center text-sm font-medium leading-7 tabular-nums transition-colors hover:text-hover-neutral",
status === "bloodied" && "text-amber-400",
status === "unconscious" && "text-red-400",
status === "healthy" && "text-foreground",
@@ -324,7 +324,7 @@ function InitiativeDisplay({
type="button"
onClick={() => onRollInitiative(combatantId)}
className={cn(
- "flex h-7 w-full items-center justify-center text-muted-foreground transition-colors hover:text-primary",
+ "flex h-7 w-full items-center justify-center text-muted-foreground transition-colors hover:text-hover-neutral",
dimmed && "opacity-50",
)}
title="Roll initiative"
@@ -344,8 +344,8 @@ function InitiativeDisplay({
className={cn(
"h-7 w-full text-center text-sm leading-7 tabular-nums transition-colors",
initiative !== undefined
- ? "font-medium text-foreground hover:text-primary"
- : "text-muted-foreground hover:text-primary",
+ ? "font-medium text-foreground hover:text-hover-neutral"
+ : "text-muted-foreground hover:text-hover-neutral",
dimmed && "opacity-50",
)}
>
@@ -429,7 +429,7 @@ export function CombatantRow({
title="Concentrating"
aria-label="Toggle concentration"
className={cn(
- "flex w-full items-center justify-center self-stretch -my-2 -ml-[2px] pl-[14px] transition-opacity",
+ "flex w-full items-center justify-center self-stretch -my-2 -ml-[2px] pl-[14px] transition-opacity hover:text-hover-neutral hover:opacity-100",
combatant.isConcentrating
? dimmed
? "opacity-50 text-purple-400"
@@ -519,7 +519,7 @@ export function CombatantRow({