From 0a64c8163445e27f5123fdcf083fb3a5449b5c49 Mon Sep 17 00:00:00 2001
From: Ariver <shanghai3168@gmail.com>
Date: Sat, 20 Jun 2026 03:43:08 +0800
Subject: [PATCH] Fix dark Space hover readability

---
 C3.tools/round1-main-ui-qa.sh                       |    4 
 C3.tools/round1-horizontal-waterfall-fixture-qa.sh  |   25 +++
 C3.tools/round1-space-lane-dark-hover-fixture-qa.sh |  390 ++++++++++++++++++++++++++++++++++++++++++++++++
 C1.source/Sources/Aligner/QuickSwitchRootView.swift |   50 ++++-
 C1.source/Resources/Aligner-Info.plist              |    4 
 CHANGELOG.md                                        |    5 
 6 files changed, 462 insertions(+), 16 deletions(-)

diff --git a/C1.source/Resources/Aligner-Info.plist b/C1.source/Resources/Aligner-Info.plist
index 0c386e9..f821f95 100644
--- a/C1.source/Resources/Aligner-Info.plist
+++ b/C1.source/Resources/Aligner-Info.plist
@@ -17,9 +17,9 @@
 	<key>CFBundlePackageType</key>
 	<string>APPL</string>
 	<key>CFBundleShortVersionString</key>
-	<string>0.1.1</string>
+	<string>0.1.2</string>
 	<key>CFBundleVersion</key>
-	<string>20260620.0136</string>
+	<string>20260620.0319</string>
 	<key>LSMinimumSystemVersion</key>
 	<string>26.0</string>
 	<key>NSHighResolutionCapable</key>
diff --git a/C1.source/Sources/Aligner/QuickSwitchRootView.swift b/C1.source/Sources/Aligner/QuickSwitchRootView.swift
index f137091..81e6310 100644
--- a/C1.source/Sources/Aligner/QuickSwitchRootView.swift
+++ b/C1.source/Sources/Aligner/QuickSwitchRootView.swift
@@ -4454,17 +4454,41 @@
             )
             : nil
 
-        applySpaceLaneContentVisualState(segment, isLocked: isLocked)
+        applySpaceLaneContentVisualState(
+            segment,
+            isLocked: isLocked,
+            usesLightActiveFill: resolvedTheme == .dark && hasWindows && !isLocked && (isActive || isAssociated)
+        )
     }
 
-    private func applySpaceLaneContentVisualState(_ segment: SpaceLaneSegmentLayers, isLocked: Bool) {
-        let primaryTextColor: NSColor = isLocked ? .white : themePalette.spaceLanePrimaryText
+    private func applySpaceLaneContentVisualState(
+        _ segment: SpaceLaneSegmentLayers,
+        isLocked: Bool,
+        usesLightActiveFill: Bool
+    ) {
+        let primaryTextColor: NSColor = isLocked
+            ? .white
+            : usesLightActiveFill
+                ? NSColor.black.withAlphaComponent(0.82)
+                : themePalette.spaceLanePrimaryText
         let secondaryTextColor: NSColor = isLocked
             ? .white
-            : themePalette.spaceLaneSecondaryText
+            : usesLightActiveFill
+                ? NSColor.black.withAlphaComponent(0.58)
+                : themePalette.spaceLaneSecondaryText
+        let markerTextColor: NSColor = isLocked
+            ? .white
+            : usesLightActiveFill
+                ? NSColor.black.withAlphaComponent(0.62)
+                : themePalette.spaceLaneSecondaryText.withAlphaComponent(segment.space.isCurrent ? 0.72 : 0.54)
+        let blockColor: NSColor = isLocked
+            ? .white
+            : usesLightActiveFill
+                ? NSColor.black.withAlphaComponent(0.20)
+                : spaceLaneOccupiedColor.withAlphaComponent(segment.space.isCurrent ? 0.72 : 0.58)
 
         segment.labelLayer.foregroundColor = primaryTextColor.cgColor
-        segment.countLayer.foregroundColor = (isLocked ? NSColor.white : themePalette.spaceLaneSecondaryText).cgColor
+        segment.countLayer.foregroundColor = secondaryTextColor.cgColor
         segment.appLayer.foregroundColor = (segment.space.type == .fullscreen
             ? primaryTextColor
             : secondaryTextColor
@@ -4474,16 +4498,10 @@
         }
 
         for block in segment.windowBlocks {
-            block.backgroundColor = (isLocked
-                ? NSColor.white
-                : spaceLaneOccupiedColor.withAlphaComponent(segment.space.isCurrent ? 0.72 : 0.58)
-            ).cgColor
+            block.backgroundColor = blockColor.cgColor
         }
 
-        (segment.fullscreenMarkerLayer as? CAShapeLayer)?.strokeColor = (isLocked
-            ? NSColor.white
-            : themePalette.spaceLaneSecondaryText.withAlphaComponent(segment.space.isCurrent ? 0.72 : 0.54)
-        ).cgColor
+        (segment.fullscreenMarkerLayer as? CAShapeLayer)?.strokeColor = markerTextColor.cgColor
     }
 
     private func shouldScrollSpaceLane(for event: NSEvent) -> Bool {
@@ -7035,6 +7053,10 @@
                 }
                 let isAppAssociated = appHoverAssociatedSpaceIDs.contains(space.id)
                 let isWindowAssociated = space.id == windowHoverAssociatedSpaceID
+                let isLocked = space.id == currentViewModel?.lockedSpaceID
+                let isActive = space.id == activeSpaceFocusID || isLocked
+                let isAssociated = (isAppAssociated || isWindowAssociated) && !isActive
+                let usesLightActiveFill = resolvedTheme == .dark && space.windowCount > 0 && !isLocked && (isActive || isAssociated)
                 let group = spaceLaneDisplayGroup(containingSpaceID: space.id)
                 let visibleFrame = segment.containerLayer.frame.offsetBy(
                     dx: (group?.containerLayer.frame.minX ?? 0) + spaceLaneContentLayer.frame.minX,
@@ -7053,6 +7075,8 @@
                     "label": space.label,
                     "type": spaceTypeName(space.type),
                     "visualStates": visualStates(for: space),
+                    "usesLightActiveFill": usesLightActiveFill,
+                    "foregroundContrastStyle": usesLightActiveFill ? "darkOnLight" : "themeDefault",
                     "fullscreenMarkerVisible": segment.fullscreenMarkerLayer != nil,
                     "fullscreenMarkerKind": fullscreenMarkerKind(
                         markerLayer: segment.fullscreenMarkerLayer,
diff --git a/C3.tools/round1-horizontal-waterfall-fixture-qa.sh b/C3.tools/round1-horizontal-waterfall-fixture-qa.sh
index 3f8ec00..c25b508 100755
--- a/C3.tools/round1-horizontal-waterfall-fixture-qa.sh
+++ b/C3.tools/round1-horizontal-waterfall-fixture-qa.sh
@@ -11,6 +11,8 @@
 # shellcheck source=build-output-paths.sh
 source "$SCRIPT_DIR/build-output-paths.sh"
 APP="$BUILD_CURRENT_APP"
+DOMAIN="com.ar.Aligner"
+THEME_KEY="com.ar.Aligner.preferences.appearance.theme"
 LAYOUT_REPORT="$BUILD_REPORT_ROOT/round01-horizontal-waterfall-layout-report.json"
 KEYBOARD_REPORT="$BUILD_REPORT_ROOT/round01-horizontal-waterfall-keyboard-report.json"
 APP_ENTER_REPORT="$BUILD_REPORT_ROOT/round01-horizontal-waterfall-app-enter-report.json"
@@ -34,6 +36,8 @@
 WINDOW_SHORTCUT_TARGET_WINDOW_INDEX="${ALIGNER_ROUND1_HORIZONTAL_WATERFALL_WINDOW_SHORTCUT_TARGET_WINDOW_INDEX:-5}"
 ARROW_WRAP_KEY_SEQUENCE="${ALIGNER_ROUND1_HORIZONTAL_WATERFALL_ARROW_WRAP_KEY_SEQUENCE:-app:1,down,up,left,down,enter}"
 REPORT_WAIT="${ALIGNER_ROUND1_HORIZONTAL_WATERFALL_REPORT_WAIT:-6.0}"
+OLD_THEME_SET=0
+OLD_THEME=""
 
 fail() {
   echo "Round01.1 horizontal Waterfall fixture QA failed: $*" >&2
@@ -59,6 +63,23 @@
   done
 
   fail "current Aligner app did not exit before QA"
+}
+
+preserve_user_theme() {
+  if OLD_THEME="$(/usr/bin/defaults read "$DOMAIN" "$THEME_KEY" 2>/dev/null)"; then
+    OLD_THEME_SET=1
+  else
+    OLD_THEME_SET=0
+    OLD_THEME=""
+  fi
+}
+
+restore_user_theme() {
+  if [ "$OLD_THEME_SET" -eq 1 ]; then
+    /usr/bin/defaults write "$DOMAIN" "$THEME_KEY" -string "$OLD_THEME"
+  else
+    /usr/bin/defaults delete "$DOMAIN" "$THEME_KEY" >/dev/null 2>&1 || true
+  fi
 }
 
 wait_for_loaded_report() {
@@ -688,8 +709,11 @@
     wait "$APP_PID" 2>/dev/null || true
   fi
   stop_current_aligner
+  restore_user_theme
 }
 trap cleanup EXIT
+preserve_user_theme
+/usr/bin/defaults write "$DOMAIN" "$THEME_KEY" -string "light"
 
 run_fixture "$LAYOUT_REPORT" --round01-debug-mouse-sequence="hover-app:$HOVER_APP_INDEX"
 wait_for_loaded_report "$LAYOUT_REPORT"
@@ -754,4 +778,5 @@
 
 trap - EXIT
 stop_current_aligner
+restore_user_theme
 echo "Round01.1 horizontal Waterfall fixture QA passed"
diff --git a/C3.tools/round1-main-ui-qa.sh b/C3.tools/round1-main-ui-qa.sh
index 05fe694..99c1b6d 100755
--- a/C3.tools/round1-main-ui-qa.sh
+++ b/C3.tools/round1-main-ui-qa.sh
@@ -116,6 +116,7 @@
 run_step app-column-alignment "$SCRIPT_DIR/round1-app-column-alignment-fixture-qa.sh"
 run_step horizontal-waterfall "$SCRIPT_DIR/round1-horizontal-waterfall-fixture-qa.sh"
 run_step theme-toggle "$SCRIPT_DIR/round1-theme-toggle-fixture-qa.sh"
+run_step space-lane-dark-hover "$SCRIPT_DIR/round1-space-lane-dark-hover-fixture-qa.sh"
 run_step no-quick-switch-residual swift "$SCRIPT_DIR/window-logic-qa.swift" --expect-no-quick-switch
 
 assert_no_current_aligner
@@ -128,5 +129,6 @@
 Covered: trigger, overlay uniqueness, keyboard navigation, candidate filtering,
 screenshots/fallbacks, minimized-window restore/activation, mouse activation,
 multi-page precise activation/close targeting, App column alignment,
-Split View Space Lane rendering, theme toggle, and final no-residual check.
+Split View Space Lane rendering, theme toggle, Space Lane dark hover readability,
+and final no-residual check.
 EOF
diff --git a/C3.tools/round1-space-lane-dark-hover-fixture-qa.sh b/C3.tools/round1-space-lane-dark-hover-fixture-qa.sh
new file mode 100755
index 0000000..e20a9dc
--- /dev/null
+++ b/C3.tools/round1-space-lane-dark-hover-fixture-qa.sh
@@ -0,0 +1,390 @@
+#!/bin/bash
+# Round01.1 Space Lane dark-theme hover readability QA.
+# Verifies that dark theme keeps the light hover surface but flips Space tile
+# foreground elements to dark colors only when the tile actually uses a light
+# occupied active/associated fill.
+
+set -euo pipefail
+
+SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
+OUTPUT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
+# shellcheck source=build-output-paths.sh
+source "$SCRIPT_DIR/build-output-paths.sh"
+
+APP="$BUILD_CURRENT_APP"
+DOMAIN="com.ar.Aligner"
+THEME_KEY="com.ar.Aligner.preferences.appearance.theme"
+REPORT_WAIT="${ALIGNER_ROUND1_SPACE_DARK_HOVER_REPORT_WAIT:-6.0}"
+REPORT_DARK_BASE="$BUILD_REPORT_ROOT/round01-space-dark-hover-base-report.json"
+REPORT_DARK_HOVER="$BUILD_REPORT_ROOT/round01-space-dark-hover-report.json"
+REPORT_DARK_SPLIT_BASE="$BUILD_REPORT_ROOT/round01-space-dark-hover-split-base-report.json"
+REPORT_DARK_SPLIT_HOVER="$BUILD_REPORT_ROOT/round01-space-dark-hover-split-report.json"
+REPORT_DARK_EMPTY_BASE="$BUILD_REPORT_ROOT/round01-space-dark-hover-empty-base-report.json"
+REPORT_DARK_EMPTY_HOVER="$BUILD_REPORT_ROOT/round01-space-dark-hover-empty-report.json"
+REPORT_LIGHT_HOVER="$BUILD_REPORT_ROOT/round01-space-dark-hover-light-report.json"
+OLD_THEME_SET=0
+OLD_THEME=""
+APP_PID=""
+
+fail() {
+  echo "Round01.1 Space Lane dark hover QA failed: $*" >&2
+  exit 1
+}
+
+aligner_pids_for_current_app() {
+  ps -axo pid=,args= | while read -r pid command; do
+    case "$command" in
+      "$APP/Contents/MacOS/Aligner"*) echo "$pid" ;;
+    esac
+  done
+}
+
+stop_current_aligner() {
+  if [ -n "${APP_PID:-}" ]; then
+    kill "$APP_PID" 2>/dev/null || true
+    wait "$APP_PID" 2>/dev/null || true
+    APP_PID=""
+  fi
+
+  for pid in $(aligner_pids_for_current_app); do
+    kill "$pid" 2>/dev/null || true
+  done
+
+  for _ in {1..30}; do
+    [ -z "$(aligner_pids_for_current_app)" ] && return
+    sleep 0.1
+  done
+
+  fail "current Aligner app did not exit before QA"
+}
+
+preserve_user_theme() {
+  if OLD_THEME="$(/usr/bin/defaults read "$DOMAIN" "$THEME_KEY" 2>/dev/null)"; then
+    OLD_THEME_SET=1
+  else
+    OLD_THEME_SET=0
+    OLD_THEME=""
+  fi
+}
+
+restore_user_theme() {
+  if [ "$OLD_THEME_SET" -eq 1 ]; then
+    /usr/bin/defaults write "$DOMAIN" "$THEME_KEY" -string "$OLD_THEME"
+  else
+    /usr/bin/defaults delete "$DOMAIN" "$THEME_KEY" >/dev/null 2>&1 || true
+  fi
+}
+
+wait_for_loaded_report() {
+  local report="$1"
+  local expected_theme="$2"
+  local expected_mouse_command="${3:-}"
+
+  /usr/bin/python3 - "$report" "$expected_theme" "$expected_mouse_command" "$REPORT_WAIT" <<'PY'
+import json
+import sys
+import time
+
+path = sys.argv[1]
+expected_theme = sys.argv[2]
+expected_mouse_command = sys.argv[3]
+timeout = float(sys.argv[4])
+deadline = time.monotonic() + timeout
+last_report = None
+
+while time.monotonic() < deadline:
+    try:
+        with open(path, "r", encoding="utf-8") as file:
+            report = json.load(file)
+        last_report = report
+        root = report.get("rootView", {})
+        if (
+            report.get("snapshotLoaded") is True
+            and report.get("quickSwitchVisible") is True
+            and report.get("resolvedQuickSwitchTheme") == expected_theme
+            and root.get("themeResolved") == expected_theme
+            and (not expected_mouse_command or root.get("lastMouseCommand") == expected_mouse_command)
+        ):
+            sys.exit(0)
+    except FileNotFoundError:
+        pass
+    except json.JSONDecodeError:
+        pass
+    time.sleep(0.2)
+
+if last_report is not None:
+    print(json.dumps(last_report, indent=2, ensure_ascii=False), file=sys.stderr)
+print(f"report did not reach {expected_theme} within {timeout:.1f}s", file=sys.stderr)
+sys.exit(1)
+PY
+}
+
+select_space_id() {
+  local report="$1"
+  local selector="$2"
+
+  /usr/bin/python3 - "$report" "$selector" <<'PY'
+import json
+import sys
+
+with open(sys.argv[1], "r", encoding="utf-8") as file:
+    report = json.load(file)
+
+selector = sys.argv[2]
+segments = report.get("rootView", {}).get("spaceLaneSegments", [])
+
+def unlocked(segment):
+    return "locked" not in segment.get("visualStates", [])
+
+for segment in segments:
+    if selector == "occupied" and segment.get("windowCount", 0) > 0 and unlocked(segment):
+        print(segment["spaceID"])
+        sys.exit(0)
+    if selector == "split" and len(segment.get("splitViewAppNames", [])) >= 2 and segment.get("windowCount", 0) > 0 and unlocked(segment):
+        print(segment["spaceID"])
+        sys.exit(0)
+    if selector == "empty" and segment.get("windowCount", 0) == 0 and unlocked(segment):
+        print(segment["spaceID"])
+        sys.exit(0)
+
+print("", end="")
+PY
+}
+
+run_fixture() {
+  local theme="$1"
+  local report="$2"
+  shift 2
+
+  stop_current_aligner
+  /usr/bin/defaults write "$DOMAIN" "$THEME_KEY" -string "$theme"
+  rm -f "$report"
+
+  "$APP/Contents/MacOS/Aligner" \
+    --round0-skip-permissions \
+    --round01-open-quick-switch \
+    --round01-fixture-app-count=4 \
+    --round01-fixture-windows-per-app=2 \
+    --round01-disable-screenshot-refresh \
+    --round01-quick-switch-report="$report" \
+    --round01-quick-switch-quit-after=1.8 \
+    "$@" >/dev/null 2>&1 &
+
+  APP_PID=$!
+}
+
+run_split_fixture() {
+  local theme="$1"
+  local report="$2"
+  shift 2
+
+  stop_current_aligner
+  /usr/bin/defaults write "$DOMAIN" "$THEME_KEY" -string "$theme"
+  rm -f "$report"
+
+  "$APP/Contents/MacOS/Aligner" \
+    --round0-skip-permissions \
+    --round01-open-quick-switch \
+    --round01-fixture-split-view \
+    --round01-disable-screenshot-refresh \
+    --round01-quick-switch-report="$report" \
+    --round01-quick-switch-quit-after=1.8 \
+    "$@" >/dev/null 2>&1 &
+
+  APP_PID=$!
+}
+
+run_space_filter_fixture() {
+  local theme="$1"
+  local report="$2"
+  shift 2
+
+  stop_current_aligner
+  /usr/bin/defaults write "$DOMAIN" "$THEME_KEY" -string "$theme"
+  rm -f "$report"
+
+  "$APP/Contents/MacOS/Aligner" \
+    --round0-skip-permissions \
+    --round01-open-quick-switch \
+    --round01-fixture-space-filter \
+    --round01-disable-screenshot-refresh \
+    --round01-quick-switch-report="$report" \
+    --round01-quick-switch-quit-after=1.8 \
+    "$@" >/dev/null 2>&1 &
+
+  APP_PID=$!
+}
+
+assert_dark_on_light_hover() {
+  local report="$1"
+  local space_id="$2"
+  local require_split="${3:-0}"
+
+  /usr/bin/python3 - "$report" "$space_id" "$require_split" <<'PY'
+import json
+import sys
+
+path = sys.argv[1]
+space_id = int(sys.argv[2])
+require_split = sys.argv[3] == "1"
+
+with open(path, "r", encoding="utf-8") as file:
+    report = json.load(file)
+
+root = report.get("rootView", {})
+segments = root.get("spaceLaneSegments", [])
+segment = next((item for item in segments if int(item.get("spaceID")) == space_id), None)
+
+def fail(message):
+    print(message, file=sys.stderr)
+    print(json.dumps(segment or report, indent=2, ensure_ascii=False), file=sys.stderr)
+    sys.exit(1)
+
+def avg(color):
+    return (color.get("red", 0) + color.get("green", 0) + color.get("blue", 0)) / 3
+
+def require_dark(color, name, max_avg=0.36, min_alpha=0.12):
+    if color.get("alpha", 0) < min_alpha or avg(color) > max_avg:
+        fail(f"{name} must be dark enough on light hover fill")
+
+def require_light(color, name, min_avg=0.44, min_alpha=0.50):
+    if color.get("alpha", 0) < min_alpha or avg(color) < min_avg:
+        fail(f"{name} must remain a light hover fill")
+
+if root.get("themeResolved") != "dark" or report.get("resolvedQuickSwitchTheme") != "dark":
+    fail("report must be in dark theme")
+if segment is None:
+    fail("target space segment not found")
+if segment.get("isHovered") is not True or "hover" not in segment.get("visualStates", []):
+    fail("target space must be hovered")
+if segment.get("usesLightActiveFill") is not True:
+    fail("hovered occupied dark tile must report usesLightActiveFill")
+if segment.get("foregroundContrastStyle") != "darkOnLight":
+    fail("hovered occupied dark tile must use darkOnLight foreground style")
+if segment.get("backgroundKind") != "solid":
+    fail("hovered occupied dark tile must use a solid light hover background")
+
+require_light(segment.get("backgroundColor", {}), "hover background")
+require_dark(segment.get("labelColor", {}), "space label")
+require_dark(segment.get("countColor", {}), "window count")
+require_dark(segment.get("appColor", {}), "app text")
+
+if require_split and len(segment.get("splitViewAppNames", [])) < 2:
+    fail("split fixture target must expose split view app names")
+for index, color in enumerate(segment.get("splitViewLabelColors", [])):
+    require_dark(color, f"split app label {index}")
+
+if segment.get("fullscreenMarkerVisible"):
+    require_dark(segment.get("fullscreenMarkerStrokeColor", {}), "fullscreen/split marker")
+
+for index, color in enumerate(segment.get("windowBlockColors", [])):
+    require_dark(color, f"window block {index}", max_avg=0.38, min_alpha=0.05)
+
+print(json.dumps({
+    "spaceID": space_id,
+    "label": segment.get("label"),
+    "foregroundContrastStyle": segment.get("foregroundContrastStyle"),
+    "backgroundKind": segment.get("backgroundKind"),
+    "labelColor": segment.get("labelColor"),
+    "countColor": segment.get("countColor"),
+    "appColor": segment.get("appColor")
+}, indent=2, ensure_ascii=False))
+PY
+}
+
+assert_no_light_active_fill() {
+  local report="$1"
+  local space_id="$2"
+  local expected_theme="$3"
+
+  /usr/bin/python3 - "$report" "$space_id" "$expected_theme" <<'PY'
+import json
+import sys
+
+with open(sys.argv[1], "r", encoding="utf-8") as file:
+    report = json.load(file)
+
+space_id = int(sys.argv[2])
+expected_theme = sys.argv[3]
+root = report.get("rootView", {})
+segment = next((item for item in root.get("spaceLaneSegments", []) if int(item.get("spaceID")) == space_id), None)
+
+def fail(message):
+    print(message, file=sys.stderr)
+    print(json.dumps(segment or report, indent=2, ensure_ascii=False), file=sys.stderr)
+    sys.exit(1)
+
+if root.get("themeResolved") != expected_theme or report.get("resolvedQuickSwitchTheme") != expected_theme:
+    fail("theme mismatch")
+if segment is None:
+    fail("target space segment not found")
+if segment.get("isHovered") is not True:
+    fail("target space must be hovered")
+if segment.get("usesLightActiveFill") is not False:
+    fail("segment must not use the dark-theme light active fill path")
+if segment.get("foregroundContrastStyle") != "themeDefault":
+    fail("segment must keep theme default foreground style")
+
+print(json.dumps({
+    "theme": expected_theme,
+    "spaceID": space_id,
+    "label": segment.get("label"),
+    "foregroundContrastStyle": segment.get("foregroundContrastStyle"),
+    "backgroundKind": segment.get("backgroundKind"),
+    "windowCount": segment.get("windowCount")
+}, indent=2, ensure_ascii=False))
+PY
+}
+
+cleanup() {
+  stop_current_aligner || true
+  restore_user_theme
+}
+
+preserve_user_theme
+trap cleanup EXIT
+stop_current_aligner
+
+run_fixture "dark" "$REPORT_DARK_BASE"
+wait_for_loaded_report "$REPORT_DARK_BASE" "dark"
+OCCUPIED_SPACE_ID="$(select_space_id "$REPORT_DARK_BASE" occupied)"
+[ -n "$OCCUPIED_SPACE_ID" ] || fail "no occupied Space found in layout fixture"
+
+run_fixture "dark" "$REPORT_DARK_HOVER" --round01-debug-mouse-sequence="hover-space:$OCCUPIED_SPACE_ID"
+wait_for_loaded_report "$REPORT_DARK_HOVER" "dark" "hover-space:$OCCUPIED_SPACE_ID"
+assert_dark_on_light_hover "$REPORT_DARK_HOVER" "$OCCUPIED_SPACE_ID"
+
+run_split_fixture "dark" "$REPORT_DARK_SPLIT_BASE"
+wait_for_loaded_report "$REPORT_DARK_SPLIT_BASE" "dark"
+SPLIT_SPACE_ID="$(select_space_id "$REPORT_DARK_SPLIT_BASE" split)"
+[ -n "$SPLIT_SPACE_ID" ] || fail "no Split View Space found in split fixture"
+
+run_split_fixture "dark" "$REPORT_DARK_SPLIT_HOVER" --round01-debug-mouse-sequence="hover-space:$SPLIT_SPACE_ID"
+wait_for_loaded_report "$REPORT_DARK_SPLIT_HOVER" "dark" "hover-space:$SPLIT_SPACE_ID"
+assert_dark_on_light_hover "$REPORT_DARK_SPLIT_HOVER" "$SPLIT_SPACE_ID" 1
+
+run_space_filter_fixture "dark" "$REPORT_DARK_EMPTY_BASE"
+wait_for_loaded_report "$REPORT_DARK_EMPTY_BASE" "dark"
+EMPTY_SPACE_ID="$(select_space_id "$REPORT_DARK_EMPTY_BASE" empty)"
+[ -n "$EMPTY_SPACE_ID" ] || fail "no empty Space found in space-filter fixture"
+
+run_space_filter_fixture "dark" "$REPORT_DARK_EMPTY_HOVER" --round01-debug-mouse-sequence="hover-space:$EMPTY_SPACE_ID"
+wait_for_loaded_report "$REPORT_DARK_EMPTY_HOVER" "dark" "hover-space:$EMPTY_SPACE_ID"
+assert_no_light_active_fill "$REPORT_DARK_EMPTY_HOVER" "$EMPTY_SPACE_ID" "dark"
+
+run_fixture "light" "$REPORT_LIGHT_HOVER" --round01-debug-mouse-sequence="hover-space:$OCCUPIED_SPACE_ID"
+wait_for_loaded_report "$REPORT_LIGHT_HOVER" "light" "hover-space:$OCCUPIED_SPACE_ID"
+assert_no_light_active_fill "$REPORT_LIGHT_HOVER" "$OCCUPIED_SPACE_ID" "light"
+
+stop_current_aligner
+restore_user_theme
+trap - EXIT
+
+cat <<EOF
+Round01.1 Space Lane dark hover QA passed
+Reports:
+- $REPORT_DARK_HOVER
+- $REPORT_DARK_SPLIT_HOVER
+- $REPORT_DARK_EMPTY_HOVER
+- $REPORT_LIGHT_HOVER
+EOF
diff --git a/CHANGELOG.md b/CHANGELOG.md
index aa68b51..87858d1 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,10 @@
 # Changelog
 
+## 0.1.2 / 20260620.0319
+
+- 修复深色模式下 Space tile hover 浅色高亮时,Space 编号、窗口数量、App 文本、Split View 标签、角标线和占用条仍为浅色导致不可读的问题。
+- 增加 Space Lane 深色 hover 可读性 fixture QA,并纳入主 UI 回归入口。
+
 ## 0.1.1 / 20260620.0136
 
 - 增加 Quick Switch 浅色 / 深色皮肤切换。

--
Gitblit v1.9.3