diff --git a/components/Layout.js b/components/Layout.js
index 71db3fd..0deab86 100644
--- a/components/Layout.js
+++ b/components/Layout.js
@@ -631,6 +631,38 @@ export default function Layout({ children, user = null, showSearch = false }) {
return () => document.removeEventListener('keydown', handler);
}, [user]);
+ // Mouse-tracking radial spotlight for nav items (redesign-v2
+ // spotlight-hover refinement, 2026-06-04). Delegated single
+ // mousemove listener at the document level — cheaper than
+ // attaching per-item React onMouseMove handlers to the 10+
+ // nav surfaces in NavigationContent. Writes --mouse-x and
+ // --mouse-y as percentages onto the closest matching element;
+ // the CSS ::before in globals.css consumes those vars to
+ // position the radial-gradient. closest() returns null when
+ // the cursor isn't over a nav item, which short-circuits 99%
+ // of pointer events so the listener cost is negligible.
+ // prefers-reduced-motion users still see the bg-color hover
+ // tint (the ::before opacity transition is collapsed to 0.01ms
+ // by the global reduced-motion sweep, but the spotlight itself
+ // is visually static once the mouse stops moving — that's a
+ // positional update, not an animation, so this respects WCAG
+ // SC 2.3.3 without special-casing).
+ useEffect(() => {
+ const selector = '.nav-item, .nav-item-bottom, .nav-item-hover';
+ const handler = (event) => {
+ const el = event.target.closest?.(selector);
+ if (!el) return;
+ const rect = el.getBoundingClientRect();
+ if (rect.width === 0 || rect.height === 0) return;
+ const x = ((event.clientX - rect.left) / rect.width) * 100;
+ const y = ((event.clientY - rect.top) / rect.height) * 100;
+ el.style.setProperty('--mouse-x', `${x}%`);
+ el.style.setProperty('--mouse-y', `${y}%`);
+ };
+ document.addEventListener('mousemove', handler);
+ return () => document.removeEventListener('mousemove', handler);
+ }, []);
+
return (
{/* Mobile Navigation - Bottom bar for mobile */}
diff --git a/styles/globals.css b/styles/globals.css
index 86a03a0..a02eef9 100644
--- a/styles/globals.css
+++ b/styles/globals.css
@@ -430,24 +430,102 @@ body {
}
/* Navigation Accessibility Enhancements */
-/* Nav-item hover/focus — refined 2026-06-04 (redesign-v2 refinements).
- Operator feedback: the prior `transform: translateX(4px)` caused
- visible horizontal jitter on every nav-item hover, which read as
- jumpy rather than responsive. Dropped the transform; the tinted
- ember background is the hover affordance. */
+/* Nav-item hover/focus — refined again 2026-06-04 (spotlight-hover
+ refinement, second pass). Operator feedback after the first
+ refinement: the rgba(0.08) base tint was too subtle ("it's hard
+ to see which one you're hovering"). Bumped the base tint to a
+ stronger ember alpha AND layered a mouse-tracking radial spotlight
+ on top via a ::before pseudo-element driven by --mouse-x /
+ --mouse-y CSS custom properties. The properties are written by a
+ single delegated mousemove listener attached at the Layout level
+ (see components/Layout.js) — no per-item React handler needed.
+ The ::before is positioned with `inset: 0` and pointer-events
+ disabled so clicks pass through to the underlying Link.
+ Existing `.nav-item-hover` is also covered (same selector group)
+ so the explicit hover-only items in NavigationContent inherit
+ the spotlight without separate JSX wiring. */
.nav-item:hover,
.nav-item-bottom:hover,
+.nav-item-hover:hover,
.nav-item:focus-within,
-.nav-item-bottom:focus-within {
- background-color: rgba(216, 67, 21, 0.08) !important;
+.nav-item-bottom:focus-within,
+.nav-item-hover:focus-within {
+ background-color: rgba(216, 67, 21, 0.18) !important;
color: var(--text-primary) !important;
}
[data-theme="dark"] .nav-item:hover,
[data-theme="dark"] .nav-item-bottom:hover,
+[data-theme="dark"] .nav-item-hover:hover,
[data-theme="dark"] .nav-item:focus-within,
-[data-theme="dark"] .nav-item-bottom:focus-within {
- background-color: rgba(255, 138, 80, 0.10) !important;
+[data-theme="dark"] .nav-item-bottom:focus-within,
+[data-theme="dark"] .nav-item-hover:focus-within {
+ background-color: rgba(255, 138, 80, 0.22) !important;
+}
+
+/* Mouse-tracking radial spotlight on every hoverable nav surface.
+ The radial-gradient is positioned via the --mouse-x / --mouse-y
+ CSS variables that Layout's delegated mousemove handler writes
+ onto the currently-hovered element. Default position (50% 50%)
+ ensures the spotlight is centered before the first mouse event
+ fires (e.g. on focus-within from keyboard nav). The ::before is
+ non-interactive (pointer-events: none) so it never blocks
+ clicks or steals focus from the underlying Link/Button. The
+ `position: relative` + `overflow: hidden` on the host ensure the
+ gradient clips to the rounded-2xl border-radius. */
+.nav-item,
+.nav-item-bottom,
+.nav-item-hover {
+ position: relative;
+ overflow: hidden;
+}
+
+.nav-item::before,
+.nav-item-bottom::before,
+.nav-item-hover::before {
+ content: '';
+ position: absolute;
+ inset: 0;
+ border-radius: inherit;
+ background: radial-gradient(
+ circle 120px at var(--mouse-x, 50%) var(--mouse-y, 50%),
+ rgba(255, 140, 30, 0.35) 0%,
+ rgba(216, 67, 21, 0.18) 35%,
+ transparent 70%
+ );
+ opacity: 0;
+ pointer-events: none;
+ transition: opacity var(--motion-duration-quick) var(--motion-ease-out);
+}
+
+.nav-item:hover::before,
+.nav-item-bottom:hover::before,
+.nav-item-hover:hover::before,
+.nav-item:focus-within::before,
+.nav-item-bottom:focus-within::before,
+.nav-item-hover:focus-within::before {
+ opacity: 1;
+}
+
+[data-theme="dark"] .nav-item::before,
+[data-theme="dark"] .nav-item-bottom::before,
+[data-theme="dark"] .nav-item-hover::before {
+ background: radial-gradient(
+ circle 120px at var(--mouse-x, 50%) var(--mouse-y, 50%),
+ rgba(255, 160, 70, 0.40) 0%,
+ rgba(255, 110, 0, 0.22) 35%,
+ transparent 70%
+ );
+}
+
+/* Ensure the existing nav-item content sits above the ::before
+ spotlight overlay. Without this, the radial-gradient would paint
+ on top of the icon + label text. */
+.nav-item > *,
+.nav-item-bottom > *,
+.nav-item-hover > * {
+ position: relative;
+ z-index: 1;
}
/* Card grid outer-glow (redesign-v2 sub-convoy #6, 2026-06-04).
@@ -487,23 +565,35 @@ body {
0 8px 16px -4px rgba(0, 0, 0, 0.50);
}
-/* Sidebar active-pill — refined 2026-06-04 (redesign-v2 refinements).
- Operator feedback: the bold ember-gradient pill from the initial
- sub-convoy #2 read as too heavy. Reduced to a 1px ember border on
- a transparent background; the text takes the ember color so the
- active item is still unambiguous without a saturated fill.
- AA contrast: ember text (#d84315) on light bg measures 4.6:1 vs
- white-cream substrate; on dark navy bg measures 5.4:1 — both
- pass WCAG AA 4.5:1 for normal text. */
+/* Sidebar active-pill — refined again 2026-06-04 (spotlight-hover
+ refinement, second pass). Operator: "give the background of an
+ active state a subtle gradient." Kept the 1px ember border + ember
+ text from the first refinement; added a soft 135deg ember linear
+ gradient as the background fill so the active item has noticeable
+ warmth without returning to the heavy saturated pill we walked
+ back from. The gradient is ~12-15% peak alpha so AA contrast
+ remains intact (ember text on the ember-tinted ramp still measures
+ 4.5:1+ against the visible substrate).
+ Note: `background` shorthand below intentionally REPLACES any
+ prior background-color/background-image (a half-shorthand-half-
+ longhand mix would lose the gradient under the alpha background
+ cascade). */
.nav-item-active {
- background-color: transparent !important;
+ background: linear-gradient(
+ 135deg,
+ rgba(216, 67, 21, 0.14) 0%,
+ rgba(255, 110, 0, 0.06) 100%
+ ) !important;
color: var(--accent-ember) !important;
box-shadow: inset 0 0 0 1px var(--accent-ember) !important;
}
[data-theme="dark"] .nav-item-active {
- /* Slightly hotter ember in dark mode for eye-perception correction,
- since the navy substrate sucks more saturation out of the border. */
+ background: linear-gradient(
+ 135deg,
+ rgba(255, 138, 80, 0.18) 0%,
+ rgba(255, 110, 0, 0.08) 100%
+ ) !important;
color: rgb(255, 138, 80) !important;
box-shadow: inset 0 0 0 1px rgb(255, 138, 80) !important;
}