Files
intercept/docs/superpowers/plans/2026-04-13-design-system-uplift.md
James Smith 8c61af2863 docs: add Sub-Project 1 implementation plan (design system uplift)
6-task plan covering token deepening, nav active state glow, panel pulse
animation, scanline texture, and localStorage nav group persistence.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-13 16:50:22 +01:00

16 KiB

Design System Uplift + Sidebar Polish — Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Establish the "Mission Control" visual language as the new baseline — deeper blacks, cyan-glow active states, panel polish, scanline texture, and nav group state persistence — so every subsequent sub-project inherits a consistent, polished foundation.

Architecture: Four targeted CSS file edits (variables, layout, components, index) plus one JS enhancement for localStorage-backed nav state. No structural changes; all changes are additive or small replacements within existing rules.

Tech Stack: CSS custom properties, Jinja2 templates, vanilla JS, localStorage API


File Map

File Action Responsibility
static/css/core/variables.css Modify Deepen bg tokens; add --accent-cyan-glow and --scanline
static/css/core/layout.css Modify Nav active state → left-border glow; hover → cyan-glow bg; group header accent line
static/css/core/components.css Modify Panel indicator pulse animation; card vignette; scanline texture on visuals containers
static/css/index.css Modify Scanline texture on existing .visuals-container if not covered by components.css
templates/partials/nav.html Modify Add data-group attributes to dropdown containers (already present); verify toggleNavDropdown call exists
templates/index.html Modify Add initNavGroupState() call in page init; add the JS function

Task 1: Deepen Background Tokens

Files:

  • Modify: static/css/core/variables.css

Note on current values (from file, not the UI guide which is out of date):

  • --bg-primary is currently #0b1118

  • --bg-secondary is currently #101823

  • --bg-tertiary is currently #151f2b

  • --bg-card is currently #121a25

  • Step 1: Update background tokens in dark theme

In static/css/core/variables.css, replace the four background values in the :root block:

/* Backgrounds - layered depth system */
--bg-primary: #07090e;
--bg-secondary: #0b1018;
--bg-tertiary: #101520;
--bg-card: #0d1219;
--bg-elevated: #161d28;
  • Step 2: Add --accent-cyan-glow and --scanline tokens

In static/css/core/variables.css, after the --accent-amber-dim line, add:

--accent-cyan-glow: rgba(74, 163, 255, 0.12);

After the --noise-image line, add a new comment block and token:

/* Scanline overlay texture */
--scanline: repeating-linear-gradient(
    0deg,
    transparent,
    transparent 2px,
    rgba(0, 0, 0, 0.04) 2px,
    rgba(0, 0, 0, 0.04) 4px
);
  • Step 3: Update light theme overrides to match

In static/css/core/variables.css, in the [data-theme="light"] block, the bg values don't need changing (they're already light colours). But add the missing --accent-cyan-glow override so it doesn't bleed through in light mode:

--accent-cyan-glow: rgba(31, 95, 168, 0.08);
--scanline: none;
  • Step 4: Verify no visual regressions

Start the dev server: sudo -E venv/bin/python intercept.py

Open http://localhost:5000 and check:

  • Background is noticeably deeper/richer without being pure black

  • Text remains readable

  • No elements that used bg colours are now invisible (white text on near-white, etc.)

  • Step 5: Commit

git add static/css/core/variables.css
git commit -m "style: deepen background tokens and add scanline/glow variables"

Task 2: Nav Active State → Left-Border Glow

Files:

  • Modify: static/css/core/layout.css

The current active state uses box-shadow: inset 0 -2px 0 var(--accent-cyan) (a bottom underline). We're replacing this with a left-border glow — more ops-center, less browser-tab.

  • Step 1: Replace .mode-nav-btn.active style

In static/css/core/layout.css, find and replace the .mode-nav-btn.active block (currently at line ~749):

.mode-nav-btn.active {
    background: var(--accent-cyan-glow);
    color: var(--text-primary);
    border-color: transparent;
    border-left: 2px solid var(--accent-cyan);
    box-shadow: -2px 0 8px rgba(74, 163, 255, 0.2);
    padding-left: 12px; /* compensate for 2px border */
}
  • Step 2: Replace .mode-nav-dropdown.has-active .mode-nav-dropdown-btn style

Find and replace the .mode-nav-dropdown.has-active .mode-nav-dropdown-btn block (currently at line ~856):

.mode-nav-dropdown.has-active .mode-nav-dropdown-btn {
    background: var(--accent-cyan-glow);
    color: var(--text-primary);
    border-color: transparent;
    border-left: 2px solid var(--accent-cyan);
    box-shadow: -2px 0 8px rgba(74, 163, 255, 0.2);
}
  • Step 3: Replace .mode-nav-dropdown-menu .mode-nav-btn.active style

Find and replace the block at line ~903:

.mode-nav-dropdown-menu .mode-nav-btn.active {
    background: var(--accent-cyan-glow);
    color: var(--text-primary);
    border-left: 2px solid var(--accent-cyan);
    box-shadow: -2px 0 6px rgba(74, 163, 255, 0.15);
    padding-left: 10px;
}
  • Step 4: Enhance hover state with cyan-glow background

Find .mode-nav-btn:hover (line ~743) and replace:

.mode-nav-btn:hover {
    background: var(--accent-cyan-glow);
    color: var(--text-primary);
    border-color: var(--border-color);
}

Find .mode-nav-dropdown-btn:hover (line ~840) and replace:

.mode-nav-dropdown-btn:hover {
    background: var(--accent-cyan-glow);
    color: var(--text-primary);
    border-color: var(--border-color);
}
  • Step 5: Update light theme active state overrides

Find the [data-theme="light"] .mode-nav-btn.active block (line ~1105) and replace:

[data-theme="light"] .mode-nav-btn.active {
    background: rgba(31, 95, 168, 0.08);
    border-left: 2px solid var(--accent-cyan);
    box-shadow: -2px 0 6px rgba(31, 95, 168, 0.15);
    padding-left: 12px;
}

[data-theme="light"] .mode-nav-dropdown-btn:hover,
[data-theme="light"] .mode-nav-dropdown.open .mode-nav-dropdown-btn,
[data-theme="light"] .mode-nav-dropdown.has-active .mode-nav-dropdown-btn {
    background: rgba(31, 95, 168, 0.06);
    border-left: 2px solid var(--accent-cyan);
    box-shadow: -2px 0 6px rgba(31, 95, 168, 0.12);
}

[data-theme="light"] .mode-nav-dropdown-menu .mode-nav-btn.active {
    background: rgba(31, 95, 168, 0.08);
    border-left: 2px solid var(--accent-cyan);
    padding-left: 10px;
}
  • Step 6: Verify in browser

Open http://localhost:5000, switch between a few modes. Verify:

  • Active mode button has visible left-border cyan glow

  • Hover on inactive buttons shows subtle cyan-glow background

  • Light theme still works (toggle via moon/sun icon)

  • Step 7: Commit

git add static/css/core/layout.css
git commit -m "style: nav active state → left-border cyan glow, hover → glow bg"

Task 3: Panel Indicator Pulse Animation

Files:

  • Modify: static/css/core/components.css

The .panel-indicator.active currently has a static green dot with glow. We're adding a CSS pulse animation so active panels visually breathe.

  • Step 1: Write a test for the panel indicator class
# Check that panel-indicator.active elements exist in the rendered HTML
# (manual spot-check — open any mode and inspect the DOM)
# Confirm .panel-indicator.active is present on the panel header dot
  • Step 2: Add pulse keyframes and apply to active indicator

In static/css/core/components.css, find .panel-indicator.active (line ~236) and replace the block:

@keyframes panel-pulse {
    0%, 100% {
        box-shadow: 0 0 4px var(--status-online), 0 0 8px rgba(56, 193, 128, 0.4);
        opacity: 1;
    }
    50% {
        box-shadow: 0 0 8px var(--status-online), 0 0 16px rgba(56, 193, 128, 0.6);
        opacity: 0.85;
    }
}

.panel-indicator.active {
    background: var(--status-online);
    box-shadow: 0 0 8px var(--status-online);
    animation: panel-pulse 2s ease-in-out infinite;
}
  • Step 3: Respect reduced-motion preference

Add below the above block:

@media (prefers-reduced-motion: reduce) {
    .panel-indicator.active {
        animation: none;
    }
}
  • Step 4: Verify in browser

Open http://localhost:5000, start any mode (e.g. Pager). Verify the panel indicator dot pulses gently when active, is static when inactive.

  • Step 5: Commit
git add static/css/core/components.css
git commit -m "style: add pulse animation to active panel indicators"

Task 4: Card Vignette + Scanline Texture

Files:

  • Modify: static/css/core/components.css

  • Modify: static/css/index.css (for .visuals-container if not in components.css)

  • Step 1: Add inner vignette to .panel cards

In static/css/core/components.css, find the .panel rule. After the existing properties, add box-shadow:

.panel {
    /* ... existing properties ... */
    box-shadow: var(--shadow-sm), inset 0 0 40px rgba(0, 0, 0, 0.25);
}

If a .panel rule doesn't exist at the top level in components.css, search for it:

grep -n "^\.panel {" static/css/core/components.css static/css/index.css

Add the box-shadow to whichever file defines it.

  • Step 2: Add scanline texture to visuals containers

Visuals containers are mode-specific and likely defined in static/css/index.css. Search for the class:

grep -n "visuals-container" static/css/index.css static/css/core/components.css

In whichever file defines .visuals-container, add an ::after pseudo-element:

.visuals-container {
    position: relative; /* ensure this is set */
}

.visuals-container::after {
    content: '';
    position: absolute;
    inset: 0;
    background: var(--scanline);
    pointer-events: none;
    z-index: 1;
    border-radius: inherit;
}
  • Step 3: Verify scanline doesn't block interactions

Open a mode with a visuals container (e.g. Bluetooth radar, TSCM). Verify:

  • Subtle horizontal scanline texture is visible

  • Clicking/interacting with the visual still works (pointer-events: none is set)

  • Light theme: scanline is none (set in Task 1 Step 3)

  • Step 4: Commit

git add static/css/core/components.css static/css/index.css
git commit -m "style: add card vignette and scanline texture to visuals containers"

Task 5: Nav Group State Persistence

Files:

  • Modify: templates/index.html

The nav HTML already has data-group attributes on .mode-nav-dropdown containers and calls toggleNavDropdown() on button clicks. We need to persist which groups are open/closed so the state survives page reloads.

  • Step 1: Find where toggleNavDropdown is defined
grep -n "toggleNavDropdown" templates/index.html

Note the line number where the function is defined (it will be inside a <script> block).

  • Step 2: Write a unit test for state persistence logic

Create tests/test_nav_state.py:

"""Tests for nav group localStorage persistence (JS logic verified via structure check)."""
import pytest
from app import create_app


@pytest.fixture
def client():
    app = create_app({'TESTING': True})
    with app.test_client() as client:
        yield client


def test_index_page_includes_nav_state_init(client):
    """nav group init function must be present in the index page."""
    resp = client.get('/')
    assert resp.status_code == 200
    html = resp.data.decode()
    assert 'initNavGroupState' in html
    assert 'localStorage' in html


def test_nav_groups_have_data_group_attributes(client):
    """Each nav group must have a data-group attribute for state keying."""
    resp = client.get('/')
    html = resp.data.decode()
    for group in ['signals', 'tracking', 'space', 'wireless', 'intel']:
        assert f'data-group="{group}"' in html, f"Missing data-group={group}"
  • Step 3: Run the test to confirm it fails (function not yet added)
pytest tests/test_nav_state.py -v

Expected: FAIL — initNavGroupState not in HTML yet.

  • Step 4: Add initNavGroupState function to index.html

Locate the toggleNavDropdown function in templates/index.html. Immediately after it, add:

function initNavGroupState() {
    const NAV_STATE_KEY = 'intercept_nav_groups';
    let savedState = {};
    try {
        savedState = JSON.parse(localStorage.getItem(NAV_STATE_KEY) || '{}');
    } catch (e) {
        savedState = {};
    }

    document.querySelectorAll('.mode-nav-dropdown[data-group]').forEach(dropdown => {
        const group = dropdown.dataset.group;
        // If saved state says closed AND this group has no active item, close it
        if (savedState[group] === false) {
            const hasActive = dropdown.classList.contains('has-active');
            if (!hasActive) {
                dropdown.classList.remove('open');
                const btn = dropdown.querySelector('.mode-nav-dropdown-btn');
                if (btn) btn.setAttribute('aria-expanded', 'false');
            }
        } else if (savedState[group] === true) {
            dropdown.classList.add('open');
            const btn = dropdown.querySelector('.mode-nav-dropdown-btn');
            if (btn) btn.setAttribute('aria-expanded', 'true');
        }
    });
}

function saveNavGroupState() {
    const NAV_STATE_KEY = 'intercept_nav_groups';
    const state = {};
    document.querySelectorAll('.mode-nav-dropdown[data-group]').forEach(dropdown => {
        state[dropdown.dataset.group] = dropdown.classList.contains('open');
    });
    try {
        localStorage.setItem(NAV_STATE_KEY, JSON.stringify(state));
    } catch (e) { /* storage full or unavailable */ }
}
  • Step 5: Update toggleNavDropdown to call saveNavGroupState

Find the existing toggleNavDropdown function. At the end of its body (before the closing }), add:

saveNavGroupState();
  • Step 6: Call initNavGroupState on page load

Find where other init functions are called on page load (look for DOMContentLoaded or a function like initApp()). Add:

initNavGroupState();
  • Step 7: Run tests to confirm they pass
pytest tests/test_nav_state.py -v

Expected: PASS — both tests green.

  • Step 8: Verify in browser

Open http://localhost:5000. Open the Signals group, close the Tracking group. Reload the page. Verify state is restored. Verify that a group containing the active mode never closes even if saved as closed.

  • Step 9: Commit
git add templates/index.html tests/test_nav_state.py
git commit -m "feat: persist nav group open/closed state to localStorage"

Task 6: Final Visual Review

  • Step 1: Run the full test suite
pytest

Expected: all tests pass (0 failures).

  • Step 2: Full visual walkthrough

Open http://localhost:5000 and check each of the following:

  1. Backgrounds — page feels deeper/richer. Cards have subtle inner vignette.
  2. Active nav item — left-border cyan glow. Looks crisp, not garish.
  3. Hover states — subtle cyan glow background on hover.
  4. Active panels — indicator dot pulses gently.
  5. Visuals containers — faint scanline texture visible on radar, waterfall, etc.
  6. Nav group persistence — collapse groups, reload, state is preserved.
  7. Light theme — toggle via header icon. No scanlines, no dark backgrounds bleeding through.
  8. Reduced motion — in DevTools > Rendering, enable "Emulate CSS prefers-reduced-motion: reduce". Pulse animation stops.
  • Step 3: Final commit if any touch-up needed
git add -p  # stage only intentional changes
git commit -m "style: design system uplift visual touch-ups"

Self-Review Checklist

  • Spec coverage: Token deepening ✓, --accent-cyan-glow ✓, --scanline ✓, active left-border glow ✓, hover glow bg ✓, panel indicator pulse ✓, card vignette ✓, scanline on visuals ✓, nav group persistence ✓, localStorage state ✓
  • No placeholders: All steps have exact code or exact commands
  • Type consistency: initNavGroupState / saveNavGroupState / toggleNavDropdown names consistent throughout
  • Light theme: Every dark-theme change has a corresponding light-theme override or is guarded
  • Reduced motion: Pulse animation explicitly disabled under prefers-reduced-motion