diff --git a/Hooks/ModCardHandOutlinePatchHelper.cs b/Hooks/ModCardHandOutlinePatchHelper.cs
new file mode 100644
index 0000000..2287430
--- /dev/null
+++ b/Hooks/ModCardHandOutlinePatchHelper.cs
@@ -0,0 +1,53 @@
+using Godot;
+using HarmonyLib;
+using MegaCrit.Sts2.Core.Combat;
+using MegaCrit.Sts2.Core.Models;
+using MegaCrit.Sts2.Core.Nodes.Cards.Holders;
+
+namespace BaseLib.Hooks;
+
+internal static class ModCardHandOutlinePatchHelper
+{
+ internal static bool TryGetRule(NHandCardHolder holder, out CardModel model, out ModCardHandOutlineRule rule)
+ {
+ model = null!;
+ rule = default;
+
+ if (!holder.IsNodeReady() || holder.CardNode?.Model is not { } m)
+ return false;
+
+ var evaluated = ModCardHandOutlineRegistry.EvaluateBest(m);
+ if (evaluated is not { } r)
+ return false;
+
+ model = m;
+ rule = r;
+ return true;
+ }
+
+ internal static void ApplyHighlight(NHandCardHolder holder, CardModel model, ModCardHandOutlineRule rule)
+ {
+ if (CombatManager.Instance is not { IsInProgress: true })
+ return;
+
+ var vanillaShow = model.CanPlay() || model.ShouldGlowRed || model.ShouldGlowGold;
+ var force = rule.VisibleWhenUnplayable && !vanillaShow;
+ if (!vanillaShow && !force)
+ return;
+
+ var highlight = holder.CardNode!.CardHighlight;
+ if (force)
+ highlight.AnimShow();
+
+ highlight.Modulate = rule.Color;
+ }
+
+ internal static void ApplyFlash(NHandCardHolder holder, ModCardHandOutlineRule rule)
+ {
+ if (AccessTools.Field(typeof(NHandCardHolder), "_flash")?.GetValue(holder) is not Control flash ||
+ !GodotObject.IsInstanceValid(flash))
+ return;
+
+ flash.Modulate = rule.Color;
+ }
+}
\ No newline at end of file
diff --git a/Hooks/ModCardHandOutlineRegistry.cs b/Hooks/ModCardHandOutlineRegistry.cs
new file mode 100644
index 0000000..01a02e0
--- /dev/null
+++ b/Hooks/ModCardHandOutlineRegistry.cs
@@ -0,0 +1,194 @@
+using System.Collections.Concurrent;
+using Godot;
+using MegaCrit.Sts2.Core.Models;
+using MegaCrit.Sts2.Core.Nodes.Cards.Holders;
+
+namespace BaseLib.Hooks;
+
+///
+/// Per–card-type custom outline colors for the in-hand .
+/// Applied after vanilla via Harmony. Foreign providers (e.g. RitsuLib)
+/// merge via .
+///
+public static class ModCardHandOutlineRegistry
+{
+ private static readonly Func ForeignPredicateAlreadySatisfied = static _ => true;
+
+ private static int _sequence;
+ private static int _foreignOrder;
+
+ private static readonly ConcurrentDictionary> RulesByCardType = new();
+ private static readonly Lock ForeignLock = new();
+ private static readonly List ForeignProviders = [];
+
+ ///
+ /// Registers a rule for .
+ ///
+ public static void Register(ModCardHandOutlineRule rule) where TCard : CardModel
+ {
+ Register(typeof(TCard), rule);
+ }
+
+ ///
+ /// Registers a rule for (concrete subtype).
+ ///
+ public static void Register(Type cardType, ModCardHandOutlineRule rule)
+ {
+ ArgumentNullException.ThrowIfNull(cardType);
+ ArgumentNullException.ThrowIfNull(rule.When);
+
+ if (cardType.IsAbstract || !typeof(CardModel).IsAssignableFrom(cardType))
+ throw new ArgumentException(
+ $"Type '{cardType.FullName}' must be a concrete subtype of {typeof(CardModel).FullName}.",
+ nameof(cardType));
+
+ var seq = Interlocked.Increment(ref _sequence);
+ var wrapped = new RegisteredRule(rule, seq);
+
+ RulesByCardType.AddOrUpdate(
+ cardType,
+ _ => [wrapped],
+ (_, existing) =>
+ {
+ var copy = new List(existing) { wrapped };
+ return copy;
+ });
+ }
+
+ ///
+ /// Merges outline evaluation from another assembly (e.g. RitsuLib). The delegate must return
+ /// when no rule applies, otherwise paint fields only — the foreign registry has already
+ /// evaluated When. Uses so the boundary stays a nullable struct (no heap boxing).
+ ///
+ public static void RegisterForeign(string modId, string sourceId,
+ Func evaluateBestFromForeign)
+ {
+ ArgumentException.ThrowIfNullOrWhiteSpace(modId);
+ ArgumentException.ThrowIfNullOrWhiteSpace(sourceId);
+ ArgumentNullException.ThrowIfNull(evaluateBestFromForeign);
+
+ var order = Interlocked.Increment(ref _foreignOrder);
+ lock (ForeignLock)
+ {
+ ForeignProviders.Add(new ForeignProvider(evaluateBestFromForeign, order));
+ }
+ }
+
+ ///
+ /// Clears all rules and foreign providers (tests / tooling).
+ ///
+ public static void ClearForTests()
+ {
+ RulesByCardType.Clear();
+ lock (ForeignLock)
+ {
+ ForeignProviders.Clear();
+ }
+ }
+
+ ///
+ /// Applies the best matching registered outline for this holder.
+ ///
+ /// if a rule was applied.
+ public static bool TryRefreshOutlineForHolder(NHandCardHolder? holder)
+ {
+ if (holder == null || !holder.IsNodeReady() || holder.CardNode?.Model is not { } model)
+ return false;
+
+ var rule = EvaluateBest(model);
+ if (!rule.HasValue)
+ return false;
+
+ ModCardHandOutlinePatchHelper.ApplyHighlight(holder, model, rule.Value);
+ return true;
+ }
+
+ internal static ModCardHandOutlineRule? EvaluateBest(CardModel model)
+ {
+ var local = EvaluateLocalBest(model);
+ ForeignCandidate? foreignBest = null;
+
+ List snapshot;
+ lock (ForeignLock)
+ {
+ snapshot = [..ForeignProviders];
+ }
+
+ foreach (var provider in snapshot)
+ {
+ (Color Color, int Priority, bool VisibleWhenUnplayable)? foreignPaint;
+ try
+ {
+ foreignPaint = provider.Evaluate(model);
+ }
+ catch
+ {
+ continue;
+ }
+
+ if (foreignPaint is not { } paint)
+ continue;
+
+ var candidate = new ModCardHandOutlineRule(ForeignPredicateAlreadySatisfied, paint.Color, paint.Priority,
+ paint.VisibleWhenUnplayable);
+
+ if (foreignBest is null ||
+ RuleWins(candidate, provider.Order, foreignBest.Value.Rule, foreignBest.Value.Order))
+ foreignBest = new ForeignCandidate(candidate, provider.Order);
+ }
+
+ switch (local)
+ {
+ case null when foreignBest is null:
+ return null;
+ case null:
+ return foreignBest.Value.Rule;
+ }
+
+ if (foreignBest is null)
+ return local.Value.Rule;
+
+ return RuleWins(foreignBest.Value.Rule, foreignBest.Value.Order, local.Value.Rule, local.Value.Sequence)
+ ? foreignBest.Value.Rule
+ : local.Value.Rule;
+ }
+
+ private static RegisteredRule? EvaluateLocalBest(CardModel model)
+ {
+ RegisteredRule? best = null;
+
+ for (var t = model.GetType();
+ t != null && typeof(CardModel).IsAssignableFrom(t);
+ t = t.BaseType)
+ {
+ if (!RulesByCardType.TryGetValue(t, out var list))
+ continue;
+
+ foreach (var entry in list.Where(entry => entry.Rule.When(model)).Where(entry => best is null
+ || entry.Rule.Priority > best.Value.Rule.Priority
+ || (entry.Rule.Priority == best.Value.Rule.Priority &&
+ entry.Sequence > best.Value.Sequence)))
+ best = entry;
+ }
+
+ return best;
+ }
+
+ private static bool RuleWins(ModCardHandOutlineRule challenger, int challengerOrder,
+ ModCardHandOutlineRule incumbent,
+ int incumbentOrder)
+ {
+ if (challenger.Priority != incumbent.Priority)
+ return challenger.Priority > incumbent.Priority;
+
+ return challengerOrder > incumbentOrder;
+ }
+
+ private readonly record struct RegisteredRule(ModCardHandOutlineRule Rule, int Sequence);
+
+ private readonly record struct ForeignProvider(
+ Func Evaluate,
+ int Order);
+
+ private readonly record struct ForeignCandidate(ModCardHandOutlineRule Rule, int Order);
+}
\ No newline at end of file
diff --git a/Hooks/ModCardHandOutlineRule.cs b/Hooks/ModCardHandOutlineRule.cs
new file mode 100644
index 0000000..0595ef4
--- /dev/null
+++ b/Hooks/ModCardHandOutlineRule.cs
@@ -0,0 +1,24 @@
+using Godot;
+using MegaCrit.Sts2.Core.Models;
+
+namespace BaseLib.Hooks;
+
+///
+/// Custom hand-card outline tint for after vanilla
+/// playable / gold / red. Register with .
+///
+/// When this returns true for the card instance, the outline color may apply.
+/// Godot modulate color (alpha is respected; vanilla highlights use ~0.98).
+///
+/// When several rules match, the highest wins; ties favor the most recently registered
+/// rule.
+///
+///
+/// If true, the highlight is forced visible with this color even when the card is not playable and vanilla would not
+/// show gold/red (still only while combat is in progress).
+///
+public readonly record struct ModCardHandOutlineRule(
+ Func When,
+ Color Color,
+ int Priority = 0,
+ bool VisibleWhenUnplayable = false);
\ No newline at end of file
diff --git a/Patches/Cards/NHandCardHolderHandOutlinePatches.cs b/Patches/Cards/NHandCardHolderHandOutlinePatches.cs
new file mode 100644
index 0000000..c13dc30
--- /dev/null
+++ b/Patches/Cards/NHandCardHolderHandOutlinePatches.cs
@@ -0,0 +1,31 @@
+using BaseLib.Hooks;
+using HarmonyLib;
+using MegaCrit.Sts2.Core.Nodes.Cards.Holders;
+
+namespace BaseLib.Patches.Cards;
+
+[HarmonyPatch(typeof(NHandCardHolder), nameof(NHandCardHolder.UpdateCard))]
+internal static class NHandCardHolderUpdateCardHandOutlinePatch
+{
+ [HarmonyPostfix]
+ public static void Postfix(NHandCardHolder __instance)
+ {
+ if (!ModCardHandOutlinePatchHelper.TryGetRule(__instance, out var model, out var rule))
+ return;
+
+ ModCardHandOutlinePatchHelper.ApplyHighlight(__instance, model, rule);
+ }
+}
+
+[HarmonyPatch(typeof(NHandCardHolder), nameof(NHandCardHolder.Flash))]
+internal static class NHandCardHolderFlashHandOutlinePatch
+{
+ [HarmonyPostfix]
+ public static void Postfix(NHandCardHolder __instance)
+ {
+ if (!ModCardHandOutlinePatchHelper.TryGetRule(__instance, out _, out var rule))
+ return;
+
+ ModCardHandOutlinePatchHelper.ApplyFlash(__instance, rule);
+ }
+}
\ No newline at end of file