diff --git a/docs/DECISION-LOG.md b/docs/DECISION-LOG.md
index 287d4fe..d38f73a 100644
--- a/docs/DECISION-LOG.md
+++ b/docs/DECISION-LOG.md
@@ -1446,3 +1446,45 @@ hairlines. The exemption says an inactive control need not meet AA. It
does not say it may be unreadable.
REVISIT WHEN: A theme is added. Both bounds are relative to that
theme's own live key, so the numbers do not transfer.
+
+DECISION: D-088 Hover and press are told by light, not by an outline
+STATUS: Confirmed
+CHOICE: The key template gains a housing lamp behind the cap, seen
+through the seam around it. Approaching a key brings that lamp up to a
+low glow; pressing it takes the lamp up with the press, continuously,
+rather than switching between two states. The hover outline that used
+to appear on the face, and then persist through the press, is gone.
+Owner report 2026-08-23: an outline popping up on hover and staying
+there is a UI convention wearing a console's clothes, where a real key
+in a lit housing shows more of that light as it moves.
+The asymmetry needed no code. The soft-body homography already shrinks
+and tilts a pressed face, so the cap pulls away from the housing
+further on one side, and more of the lamp shows exactly there. The
+physics had it; the lamp only had to be put behind it.
+COLOUR: warm amber-gold, `KeySeamColor`, #F0A24A on the dark bench and
+#C9762A on the light one. Warm on purpose: the console is cool
+green-grey with a teal accent, so a warm light reads as illumination
+from inside the box rather than as another status colour. A test pins
+both properties - that it is warm, and that it is far enough from the
+accent hue to read as a different fixture.
+RENDERED FIRST, TWICE. The first attempt sat the lamp 1.5 DIP proud of
+the face so a lit ring always existed. It rendered as an amber
+OUTLINE, which is the exact thing being removed. The fix was to drop
+the proud ring entirely and let the bloom carry it: light at a seam is
+diffuse, and a hard-edged stroke of any colour reads as a border.
+RESTRAINT: the elegance is in the opacity ramp, not the hue. 0 at
+rest, 0.34 on hover, 0.85 held. The C# ramp constants and the XAML
+style setters are pinned equal by test, because if they drift the lamp
+jumps the instant a key is touched and nothing else would catch it.
+BECAUSE: the outline was the last piece of the key that was not
+modelled on anything physical.
+REVISIT WHEN: the levels are by eye. If the glow reads as brash, the
+opacities are the knob, not the colour.
+
+AMENDMENT TO D-087 (2026-08-23, same session): the standby legend was
+turned down on owner report that it could be "quite a bit dimmer".
+KeyStandby is now #42786F dark and #577F79 light, sitting at the floor
+of its own contract - 3.2:1 to 3.8:1 across the resting and pressed
+faces - and the glow effect went from 0.30 to 0.20 opacity. It cannot
+go lower without moving that floor, and 3:1 is already the large-text
+threshold rather than the normal-text one.
diff --git a/src/CUETools.Linux.App/Controls/SoftBodyKey.cs b/src/CUETools.Linux.App/Controls/SoftBodyKey.cs
index 2ecce19..c35fb31 100644
--- a/src/CUETools.Linux.App/Controls/SoftBodyKey.cs
+++ b/src/CUETools.Linux.App/Controls/SoftBodyKey.cs
@@ -29,6 +29,12 @@ public static class SoftBodyKey
/// The bench is lit from above, so sinking also means sliding into shadow.
private const double Drop = 0.80;
+ /// Housing-lamp levels, matched to the style setters on keySeam so
+ /// the ramp starts exactly where hover left off and hands back to the same
+ /// value. If these drift apart the light jumps at the start of a press.
+ public const double SeamHover = 0.34;
+ public const double SeamPressed = 0.85;
+
public static void Attach(Button button)
{
if (!Enabled) return;
@@ -49,6 +55,20 @@ void Apply()
if (button.GetVisualDescendant("keyRecess") is Border recess)
recess.Opacity = amount <= 0.001 ? 0 : 1;
+ // The housing lamp comes up with the press, the way a console key
+ // brightens as it goes down, rather than switching between two
+ // style states. Handing it back matters: a locally set value
+ // outranks a style setter, so leaving one here would kill the
+ // hover glow permanently the first time the key was pressed. Same
+ // trap as D-084 (2), one property along.
+ if (button.GetVisualDescendant("keySeam") is Border seam)
+ {
+ if (amount <= 0.001)
+ seam.ClearValue(Visual.OpacityProperty);
+ else
+ seam.Opacity = SeamHover + amount * (SeamPressed - SeamHover);
+ }
+
if (amount <= 0.001)
{
face.RenderTransform = null;
diff --git a/src/CUETools.Linux.App/Theme/AnalogControls.axaml b/src/CUETools.Linux.App/Theme/AnalogControls.axaml
index 2dc3a98..aeb6d90 100644
--- a/src/CUETools.Linux.App/Theme/AnalogControls.axaml
+++ b/src/CUETools.Linux.App/Theme/AnalogControls.axaml
@@ -30,6 +30,33 @@
+
+
+
+
+
+
+
+
+
+
+
+
-
+
+
+
@@ -374,7 +418,7 @@
@@ -493,7 +537,7 @@
diff --git a/src/CUETools.Linux.App/Theme/Palette.axaml b/src/CUETools.Linux.App/Theme/Palette.axaml
index e1424ce..d9a8260 100644
--- a/src/CUETools.Linux.App/Theme/Palette.axaml
+++ b/src/CUETools.Linux.App/Theme/Palette.axaml
@@ -25,11 +25,19 @@
#34CFC0
-
+ translucent letters is not off, it is turned right down. Turned
+ down again on 08-23 owner note; this now sits at the floor of its
+ own contract, 3.2:1 to 3.8:1 across the resting and pressed faces,
+ against 14:1 for a live key and the 1.2:1 the old structural Line
+ gave, which could not be read at all. It cannot go lower without
+ moving that floor, and 3:1 is already the large-text threshold. -->
+
+
+ #F0A24A
#34CFC0
#2E000000
#5A000000
@@ -89,12 +97,17 @@
-
+ mid-press and takes the dead release. Turned down on the 08-23
+ owner note to the floor of its own contract. -->
+
+
+ #C9762A
#087067
diff --git a/tests/CUETools.Linux.Tests/SeamLightTests.cs b/tests/CUETools.Linux.Tests/SeamLightTests.cs
new file mode 100644
index 0000000..5caa9af
--- /dev/null
+++ b/tests/CUETools.Linux.Tests/SeamLightTests.cs
@@ -0,0 +1,87 @@
+using System.Text.RegularExpressions;
+using Avalonia.Headless.XUnit;
+using Avalonia.Media;
+using Avalonia.Styling;
+using CUETools.Linux.App.Controls;
+using Xunit;
+
+namespace CUETools.Linux.Tests;
+
+// D-088. Hover and press are told by light from the housing rather than by an
+// outline. Three things about that can break quietly, so all three are pinned.
+public class SeamLightTests
+{
+ private static string Theme() => File.ReadAllText(
+ Path.Combine(FindRepoRoot(), "src", "CUETools.Linux.App", "Theme", "AnalogControls.axaml"));
+
+ private static string FindRepoRoot()
+ {
+ string? dir = AppContext.BaseDirectory;
+ while (dir != null && !File.Exists(Path.Combine(dir, "eng", "build.sh")))
+ dir = Path.GetDirectoryName(dir);
+ Assert.NotNull(dir);
+ return dir!;
+ }
+
+ private static double SeamOpacityFor(string pseudoClass)
+ {
+ // the style block for keySeam under this pseudo-class, and the opacity it sets
+ var m = Regex.Match(
+ Theme(),
+ @"Selector=""Button:" + Regex.Escape(pseudoClass) +
+ @"\s*/template/\s*Border#keySeam""\s*>\s* SoftBodyKey.SeamHover,
+ "pressing a key has to brighten its lamp, not dim it");
+ }
+
+ [Fact]
+ public void HoverNoLongerDrawsAnOutlineOnTheKeyFace()
+ {
+ // the owner's report (2026-08-23): the hover outline popped up and then
+ // persisted through the press. An outline is a UI convention wearing a
+ // console's clothes; this guards the replacement from being undone.
+ Assert.DoesNotMatch(
+ new Regex(@"Selector=""Button:pointerover\s*/template/\s*Border#keyFace"""),
+ Theme());
+ }
+
+ [AvaloniaFact]
+ public void TheHousingLampIsWarmAndNotMistakableForTheAccent()
+ {
+ // the console is cool green-grey with a teal accent. The lamp has to read
+ // as light from inside the box, which means warm - and a lamp that drifted
+ // toward the accent hue would read as another status colour instead.
+ foreach (ThemeVariant variant in new[] { ThemeVariant.Dark, ThemeVariant.Light })
+ {
+ Assert.True(Avalonia.Application.Current!.TryGetResource(
+ "KeySeamColor", variant, out object? value), "KeySeamColor missing");
+ var seam = Assert.IsType(value);
+
+ Assert.True(seam.R > seam.B + 60,
+ $"{variant} KeySeamColor #{seam} is not warm: red must lead blue clearly");
+ Assert.True(seam.R >= seam.G && seam.G > seam.B,
+ $"{variant} KeySeamColor #{seam} is not on the amber side of warm");
+
+ Assert.True(Avalonia.Application.Current.TryGetResource(
+ "StatusAccentColor", variant, out object? accentValue), "StatusAccentColor missing");
+ var accent = Assert.IsType(accentValue);
+ int distance = Math.Abs(seam.R - accent.R) + Math.Abs(seam.G - accent.G) + Math.Abs(seam.B - accent.B);
+ Assert.True(distance > 150,
+ $"{variant} the housing lamp #{seam} is too close to the accent #{accent} to read as a different fixture");
+ }
+ }
+}