Internal/StateController.cs
using System.Text;
using System.Threading;
using Sandbox;
using Sandbox.UI;

namespace Goo.Internal;

internal sealed class StateController
{
    static int _nextKey;

    readonly Panel _host;
    readonly string _key;
    StyleSheet? _sheet;

    public StateController(Panel host)
    {
        _host = host;
        _key = $"goo-v-{Interlocked.Increment(ref _nextKey)}";
        _host.AddClass(_key);
    }

    public void ApplyVariants(
        Color? baseBg,  Color? baseFg,
        Color? hoverBg, Color? activeBg, Color? focusBg,
        Color? hoverFg, Color? activeFg, Color? focusFg,
        int? transitionMs)
    {
        // Engine routes input to the nearest Panel whose ComputedStyle.PointerEvents is All
        // (Panel.WantsMouseInput). Without this the :hover/:active pseudoclasses never flip.
        _host.Style.PointerEvents = PointerEvents.All;

        if (focusBg.HasValue || focusFg.HasValue)
            _host.AcceptsFocus = true;

        var css = BuildCss(
            _key,
            baseBg, baseFg,
            hoverBg, activeBg, focusBg,
            hoverFg, activeFg, focusFg,
            transitionMs);

        // Remove-by-instance sidesteps the engine Parse(string) leak: StyleSheet.FromString
        // never sets FileName, so the built-in Remove("string") wildcard no-ops and sheets
        // accumulate forever, producing nondeterministic cascade.
        if (_sheet is not null) _host.StyleSheet.Remove(_sheet);
        _sheet = StyleSheet.FromString(css, "goo-state-variants");
        _host.StyleSheet.Add(_sheet);
    }

    internal static string BuildCss(
        string key,
        Color? baseBg, Color? baseFg,
        Color? hoverBg, Color? activeBg, Color? focusBg,
        Color? hoverFg, Color? activeFg, Color? focusFg,
        int? transitionMs)
    {
        var sb = new StringBuilder();
        // Source order is the tiebreaker for same-specificity selectors: later wins.
        // Emitting base, :focus, :hover, :active gives Active > Hover > Focus > Base.
        AppendRule(sb, $".{key}",        baseBg,   baseFg,   transitionMs);
        AppendRule(sb, $".{key}:focus",  focusBg,  focusFg,  null);
        AppendRule(sb, $".{key}:hover",  hoverBg,  hoverFg,  null);
        AppendRule(sb, $".{key}:active", activeBg, activeFg, null);
        return sb.ToString();
    }

    static void AppendRule(StringBuilder sb, string selector, Color? bg, Color? fg, int? transitionMs)
    {
        bool hasTransition = transitionMs is int ms0 && ms0 > 0;
        if (bg is null && fg is null && !hasTransition) return;

        sb.Append(selector).Append(" { ");
        if (bg.HasValue) sb.Append("background-color: ").Append(CssColor(bg.Value)).Append("; ");
        if (fg.HasValue) sb.Append("color: ").Append(CssColor(fg.Value)).Append("; ");
        if (hasTransition)
        {
            int ms = transitionMs!.Value;
            sb.Append("transition: background-color ").Append(ms).Append("ms, color ").Append(ms).Append("ms; ");
        }
        sb.Append("}\n");
    }

    static string CssColor(Color c)
    {
        int r = (int)(c.r * 255);
        int g = (int)(c.g * 255);
        int b = (int)(c.b * 255);
        return $"rgba({r},{g},{b},{c.a:0.###})";
    }
}