12:00:00
KHIEMSTUDIO.

APP.PY

#!/usr/bin/env python3
"""
Nomi Configurator - Combined backend with cross-platform system keys and live profile updates.

Dependencies:
    pip install pyserial flask flask-socketio pywebview pynput psutil pywin32

Notes:
 - On Windows this script uses ctypes to send media key events (VK_MEDIA_* & VK_VOLUME_*).
 - On macOS this script uses osascript (AppleScript) calls for volume and music control.
 - On Linux, this script may require 'xdotool' for app detection (`sudo apt-get install xdotool`).
 - Ensure Nomi_Profile.json exists in same folder (the app will create a default if missing).
 - Uses pynput for reliable hotkey combinations.

--- NEW FEATURES IN THIS VERSION ---
1. Smooth LED Fade: Layer changes now trigger a smooth color transition by sending
   a "FADE:" command to the firmware.
2. Disconnected State (Firmware): The script supports a blinking red LED state.
   - FIRMWARE REQUIREMENT: The device should blink red on its own when not connected.
   - The first command sent from this script upon connection will stop the blinking.
3. Cycle Layers on Hold (Firmware): Holding Button 1 and turning the encoder cycles layers.
   - FIRMWARE REQUIREMENT: The device must be programmed to send "CMD:CYCLE_LAYER"
     for each encoder click while Button 1 is held.
4. Automatic App-Based Layer Switching: The backend now monitors the active window
   and switches to a layer if it's linked to that application.
5. Dynamic App List: The UI can request a list of running applications for easier linking.
"""

import os
import sys
import json
import time
import threading
import platform
import subprocess
import serial
import serial.tools.list_ports
import webview
from flask import Flask, render_template
from flask_socketio import SocketIO
from pynput.keyboard import Controller, Key
import psutil # For listing processes

# --- Flask App & WebSocket Setup ---
basedir = os.path.abspath(os.path.dirname(__file__))
app = Flask(__name__, template_folder=os.path.join(basedir, 'web'))
socketio = SocketIO(app, cors_allowed_origins="*")

# --- Globals ---
keyboard = Controller()
serial_connection = None
PROFILE_FILE = os.path.join(basedir, 'Nomi_Profile.json')
state = {}

# --- Platform info ---
PLATFORM = platform.system()  # 'Windows', 'Darwin' (macOS), 'Linux'

# Platform-specific imports for active window detection
if PLATFORM == "Windows":
    import win32process
    import win32gui

# --- Profile Management ---
def load_profile():
    global state
    if os.path.exists(PROFILE_FILE):
        try:
            with open(PROFILE_FILE, 'r') as f:
                state = json.load(f)
            print(f"Loaded profile: {PROFILE_FILE}")
        except Exception as e:
            print(f"Error loading profile JSON: {e}")
            state = {}
    else:
        print("No profile found, creating default profile.")
        state = {
            "last_port": None,
            "currentView": "configure",
            "currentLayer": 0,
            "layers": [
                {
                    "name": "Default Layer",
                    "linkedApp": "",
                    "color": "#000000",
                    "ledColor": "#FFFFFF",
                    "keymap": [{"k":"..."} for _ in range(6)],
                    "encoder": {"cw":{"k":"..."}, "ccw":{"k":"..."}, "press":{"k":"..."}}
                }
            ],
            "macros": [],
            "keycodes": {}
        }
        save_profile()

def save_profile():
    try:
        with open(PROFILE_FILE, 'w') as f:
            json.dump(state, f, indent=4)
        # print("Profile saved.") # Commented out to reduce console spam
    except Exception as e:
        print(f"Error saving profile: {e}")

# --- Utility: normalize incoming serial line ---
def normalize_device_line(line: str) -> str:
    """Extract the payload like BTN:1 or ENC:CW from various formatted lines."""
    if not line:
        return ''
    line = line.strip()
    prefix = 'Received from device:'
    if line.startswith(prefix):
        return line[len(prefix):].strip()
    parts = line.split()
    for p in parts:
        if ':' in p:
            token = p.strip().strip('"').strip("'").strip()
            if token.upper().startswith(('BTN:', 'ENC:', 'CMD:')):
                return token
    return line

# --- Cross-platform system key helpers ---
def send_system_key_by_name(name: str):
    """Send system/media key by a short name (e.g. VOLU, VOLD, MUTE, PLAY, NEXT, PREV)."""
    n = str(name).upper()
    try:
        if PLATFORM == "Windows":
            _win_send_media_key(n)
        elif PLATFORM == "Darwin":  # macOS
            _mac_send_media_key(n)
        else:
            _linux_send_media_key(n)
    except Exception as e:
        print(f"Error sending system key {n}: {e}")

# --- Windows implementation using ctypes keybd_event ---
if PLATFORM == "Windows":
    import ctypes
    user32 = ctypes.windll.user32
    VK_VOLUME_MUTE = 0xAD
    VK_VOLUME_DOWN = 0xAE
    VK_VOLUME_UP = 0xAF
    VK_MEDIA_NEXT_TRACK = 0xB0
    VK_MEDIA_PREV_TRACK = 0xB1
    VK_MEDIA_STOP = 0xB2
    VK_MEDIA_PLAY_PAUSE = 0xB3

    def _win_send_key(vk):
        user32.keybd_event(vk, 0, 0, 0)
        time.sleep(0.02)
        user32.keybd_event(vk, 0, 2, 0)

    def _win_send_media_key(name: str):
        if name == "VOLU": _win_send_key(VK_VOLUME_UP)
        elif name == "VOLD": _win_send_key(VK_VOLUME_DOWN)
        elif name in ("MUTE", "VOLM"): _win_send_key(VK_VOLUME_MUTE)
        elif name == "NEXT": _win_send_key(VK_MEDIA_NEXT_TRACK)
        elif name == "PREV": _win_send_key(VK_MEDIA_PREV_TRACK)
        elif name == "PLAY": _win_send_key(VK_MEDIA_PLAY_PAUSE)
        elif name == "STOP": _win_send_key(VK_MEDIA_STOP)
        else: print(f"[Windows] Unknown media key: {name}")

# --- macOS implementation using osascript ---
def _mac_send_media_key(name: str):
    name = name.upper()
    if name == "VOLU":
        cmd = 'set o to output volume of (get volume settings)\nset o to o + 6\nif o > 100 then set o to 100\nset volume output volume o'
        subprocess.run(['osascript', '-e', cmd])
    elif name == "VOLD":
        cmd = 'set o to output volume of (get volume settings)\nset o to o - 6\nif o < 0 then set o to 0\nset volume output volume o'
        subprocess.run(['osascript', '-e', cmd])
    elif name in ("MUTE", "VOLM"):
        try:
            res = subprocess.check_output(['osascript', '-e', 'output muted of (get volume settings)']).decode().strip()
            if res.lower() in ('true', '1'):
                subprocess.run(['osascript', '-e', 'set volume without output muted'])
            else:
                subprocess.run(['osascript', '-e', 'set volume with output muted'])
        except Exception:
            subprocess.run(['osascript', '-e', 'set volume with output muted'])
    elif name == "PLAY":
        for app in ("Music", "Spotify"):
            try: subprocess.run(['osascript', '-e', f'tell application "{app}" to playpause'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
            except Exception: pass
    elif name == "STOP":
        for app in ("Music", "Spotify"):
            try: subprocess.run(['osascript', '-e', f'tell application "{app}" to stop'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
            except Exception: pass
    elif name == "NEXT":
        for app in ("Music", "Spotify"):
            try: subprocess.run(['osascript', '-e', f'tell application "{app}" to next track'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
            except Exception: pass
    elif name == "PREV":
        for app in ("Music", "Spotify"):
            try: subprocess.run(['osascript', '-e', f'tell application "{app}" to previous track'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
            except Exception: pass
    else:
        print(f"[macOS] Unknown media key: {name}")

# --- Linux best-effort fallback ---
def _linux_send_media_key(name: str):
    name = name.upper()
    def _run(cmd):
        try:
            subprocess.run(cmd, shell=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
            return True
        except Exception: return False
    if name in ("PLAY","STOP","NEXT","PREV"):
        if _run("playerctl play-pause") and name == "PLAY": return
        if name == "NEXT": _run("playerctl next")
        elif name == "PREV": _run("playerctl previous")
        elif name == "STOP": _run("playerctl stop")
    elif name in ("VOLU","VOLD","MUTE","VOLM"):
        if name == "VOLU": _run("amixer -D pulse sset Master 5%+")
        elif name == "VOLD": _run("amixer -D pulse sset Master 5%-")
        else: _run("amixer -D pulse sset Master toggle")
    else:
        print(f"[Linux] Unknown media key: {name}")

# --- NEW: Active Application Monitoring ---
def get_active_process_name():
    """Gets the executable name of the currently focused window."""
    try:
        if PLATFORM == "Windows":
            hwnd = win32gui.GetForegroundWindow()
            _, pid = win32process.GetWindowThreadProcessId(hwnd)
            return psutil.Process(pid).name()
        elif PLATFORM == "Darwin": # macOS
            cmd = 'tell application "System Events" to get name of first process whose frontmost is true'
            result = subprocess.check_output(['osascript', '-e', cmd]).decode().strip()
            return result
        elif PLATFORM == "Linux":
            # Requires 'xdotool' to be installed (sudo apt-get install xdotool)
            pid_cmd = 'xdotool getwindowfocus getwindowpid'
            pid = subprocess.check_output(pid_cmd, shell=True).decode().strip()
            return psutil.Process(int(pid)).name()
    except Exception:
        return None
    return None

def app_monitoring_thread():
    """A thread that monitors the active application and switches layers."""
    last_app_name = None
    while True:
        app_name = get_active_process_name()

        if app_name and app_name != last_app_name:
            print(f"Active App Changed: {app_name}")
            last_app_name = app_name
            
            matched_layer_index = -1
            for i, layer in enumerate(state.get('layers', [])):
                linked_app = layer.get('linkedApp', '')
                if linked_app and linked_app.lower() in app_name.lower():
                    matched_layer_index = i
                    break

            if matched_layer_index != -1 and state.get('currentLayer') != matched_layer_index:
                print(f"Switching to layer {matched_layer_index} for {app_name}")
                
                state['currentLayer'] = matched_layer_index
                save_profile()
                
                new_led_color = state['layers'][matched_layer_index].get('ledColor')
                fade_led_color(new_led_color)
                
                socketio.emit('layer_changed', {'layer': matched_layer_index})
        
        time.sleep(1) # Check once per second

# --- Serial reader thread ---
def serial_reader():
    global serial_connection
    while True:
        if serial_connection and serial_connection.is_open:
            try:
                raw = serial_connection.readline()
                if not raw:
                    time.sleep(0.01)
                    continue
                try:
                    line = raw.decode('utf-8', errors='ignore').strip()
                except Exception:
                    line = raw.decode('latin-1', errors='ignore').strip()
                if line:
                    # print(f"Raw from device: {line}") # Can be noisy
                    payload = normalize_device_line(line)
                    if payload:
                        try:
                            handle_device_input({'data': payload})
                        except Exception as e:
                            print(f"Error processing device input locally: {e}")
                        try:
                            socketio.emit('handle_device_input_event', {'data': payload})
                        except Exception as e:
                            print(f"Error emitting to UI: {e}")
            except Exception as e:
                print(f"Serial read error / disconnected: {e}")
                try:
                    if serial_connection: serial_connection.close()
                except Exception: pass
                serial_connection = None
                try:
                    socketio.emit('device_status', {'status': 'disconnected', 'port': ''})
                except Exception: pass
        time.sleep(0.01)

# --- Device input handling (Socket event + local calls) ---
@socketio.on('handle_device_input_event')
def handle_device_input(json_data):
    data = None
    if isinstance(json_data, dict): data = json_data.get('data')
    elif isinstance(json_data, str): data = json_data
    if not data:
        print("handle_device_input called with no data")
        return
    if not state.get('layers'):
        print("No layers configured in state")
        return
    try:
        parts = data.split(':', 1)
        if len(parts) != 2:
            # print(f"Unexpected input format: {data}") # Can be noisy
            return
        input_type, value = parts[0].upper(), parts[1]
        current_layer_index = int(state.get('currentLayer', 0))
        layers = state.get('layers', [])
        if not (0 <= current_layer_index < len(layers)):
            print(f"Invalid currentLayer index: {current_layer_index}")
            return
        current_layer = layers[current_layer_index]
        
        if input_type == 'CMD' and value.upper() == 'CYCLE_LAYER':
            print("Received command to cycle layer.")
            num_layers = len(layers)
            if num_layers > 1:
                next_layer_index = (current_layer_index + 1) % num_layers
                state['currentLayer'] = next_layer_index
                save_profile()
                
                new_led_color = layers[next_layer_index].get('ledColor')
                fade_led_color(new_led_color)

                socketio.emit('layer_changed', {'layer': next_layer_index})
            return
            
        action_key = None
        if input_type == 'BTN':
            try:
                idx = int(value) - 1
                if 0 <= idx < len(current_layer.get('keymap', [])):
                    action_key = current_layer['keymap'][idx].get('k')
            except ValueError: print(f"Invalid BTN value: {value}")
        elif input_type == 'ENC':
            action_map = {'CW':'cw', 'CCW':'ccw', 'P':'press'}
            mapped = action_map.get(value.upper())
            if mapped:
                action_key = current_layer.get('encoder', {}).get(mapped, {}).get('k')
        if action_key:
            execute_action(action_key)
        else:
            # print(f"No action mapped for input {data}") # Can be noisy
            pass
    except Exception as e:
        print(f"Error handling device input: {e}")

# --- Action execution ---
def execute_action(key):
    """
    Executes a macro with intelligent modifier key handling,
    a layer switch, or a single key/system action.
    """
    if not key:
        return
    print(f"Executing action: {key}")

    MODIFIER_KEYS = {
        'CTRL', 'CONTROL', 'SHIFT', 'SHFT', 'ALT', 'OPTION',
        'WIN', 'WINDOWS', 'CMD', 'COMMAND', 'SUPER'
    }

    macro = next((m for m in state.get('macros', []) if m.get('name') == key), None)

    if macro:
        print(f"Running macro: {macro.get('name')}")
        actions = macro.get('actions', [])
        i = 0
        while i < len(actions):
            act = actions[i]
            atype = act.get('type')
            val = str(act.get('value', '')).upper()
            if atype == 'keystroke' and val in MODIFIER_KEYS:
                hotkey_parts = [val]
                j = i + 1
                final_key_found = False
                while j < len(actions):
                    next_act = actions[j]
                    if next_act.get('type') == 'keystroke':
                        next_val = str(next_act.get('value', '')).upper()
                        hotkey_parts.append(next_val)
                        if next_val not in MODIFIER_KEYS:
                            final_key_found = True
                            break
                        j += 1
                    else:
                        break
                if final_key_found:
                    hotkey_string = "+".join(hotkey_parts)
                    print(f"Executing hotkey: {hotkey_string}")
                    press_key(hotkey_string)
                    i = j
                else:
                    press_key(hotkey_parts[0])
            else:
                if atype == 'keystroke':
                    press_key(act.get('value'))
                elif atype == 'text':
                    keyboard.type(act.get('value', ''))
                elif atype == 'delay':
                    try:
                        time.sleep(int(act.get('value', 0)) / 1000.0)
                    except (ValueError, TypeError):
                        pass
            i += 1

    elif isinstance(key, str) and key.startswith('Lyr '):
        try:
            li = int(key.split(' ')[1])
            if 0 <= li < len(state.get('layers', [])):
                state['currentLayer'] = li
                save_profile()
                
                new_led_color = state['layers'][li].get('ledColor')
                fade_led_color(new_led_color)
                
                socketio.emit('layer_changed', {'layer': li})
        except Exception as e:
            print(f"Layer switch error: {e}")
    else:
        press_key(key)

# --- Enhanced press_key with proper modifier key support ---
def press_key(key_string):
    if not key_string: return
    
    system_keys = {'VOLU', 'VOLD', 'MUTE', 'VOLM', 'PLAY', 'STOP', 'NEXT', 'PREV'}
    if key_string.upper() in system_keys:
        send_system_key_by_name(key_string.upper())
        return

    key_map = {
        'CTRL': Key.ctrl, 'CONTROL': Key.ctrl, 'SHIFT': Key.shift, 'SHFT': Key.shift,
        'ALT': Key.alt, 'OPTION': Key.alt, 'WIN': Key.cmd, 'WINDOWS': Key.cmd,
        'CMD': Key.cmd, 'COMMAND': Key.cmd, 'SUPER': Key.cmd, 'ENTER': Key.enter,
        'ENT': Key.enter, 'RETURN': Key.enter, 'BACKSPACE': Key.backspace,
        'BKSP': Key.backspace, 'DELETE': Key.delete, 'DEL': Key.delete,
        'TAB': Key.tab, 'SPACE': Key.space,
        'ESC': Key.esc if hasattr(Key, 'esc') else Key.escape,
        'ESCAPE': Key.esc if hasattr(Key, 'esc') else Key.escape,
        'UP': Key.up, 'DOWN': Key.down, 'LEFT': Key.left, 'RIGHT': Key.right,
        'RGHT': Key.right, 'F1': Key.f1, 'F2': Key.f2, 'F3': Key.f3, 'F4': Key.f4,
        'F5': Key.f5, 'F6': Key.f6, 'F7': Key.f7, 'F8': Key.f8, 'F9': Key.f9,
        'F10': Key.f10, 'F11': Key.f11, 'F12': Key.f12, 'HOME': Key.home,
        'END': Key.end, 'PAGE_UP': Key.page_up, 'PAGEUP': Key.page_up,
        'PAGE_DOWN': Key.page_down, 'PAGEDOWN': Key.page_down,
        'CAPS_LOCK': Key.caps_lock if hasattr(Key, 'caps_lock') else None,
        'CAPSLOCK': Key.caps_lock if hasattr(Key, 'caps_lock') else None,
    }

    try:
        if '+' in key_string:
            parts = [p.strip().upper() for p in key_string.split('+')]
            modifiers = []
            main_key_str = None
            for part in parts:
                if part in key_map and key_map[part] in {Key.ctrl, Key.shift, Key.alt, Key.cmd}:
                    modifiers.append(key_map[part])
                else:
                    main_key_str = part
            if not main_key_str:
                print(f"No main key found in combination: {key_string}")
                return
            key_to_press = key_map.get(main_key_str, main_key_str.lower())
            with keyboard.pressed(*modifiers):
                keyboard.press(key_to_press)
                keyboard.release(key_to_press)
        else:
            ks_upper = key_string.upper()
            key_obj = key_map.get(ks_upper)
            if key_obj:
                keyboard.press(key_obj)
                keyboard.release(key_obj)
            elif len(key_string) == 1:
                keyboard.press(key_string)
                keyboard.release(key_string)
            else:
                print(f"Unknown key string: {key_string}")
    except Exception as e:
        print(f"Error in press_key('{key_string}'): {e}")

# --- Serial write helper ---
def send_command_to_device(command):
    if serial_connection and serial_connection.is_open:
        try:
            serial_connection.write((command + '\n').encode('utf-8'))
            print(f"Sent to device: {command}")
        except Exception as e:
            print(f"Write error: {e}")

# --- MODIFIED: Helper function for smooth LED color transitions ---
def fade_led_color(end_hex):
    """Sends a command to the device to smoothly fade to a new color."""
    if end_hex and isinstance(end_hex, str) and end_hex.startswith('#'):
        send_command_to_device(f"FADE:{end_hex}")
    else:
        print(f"Invalid color for fade: {end_hex}. Setting instantly.")
        if end_hex and isinstance(end_hex, str) and end_hex.startswith('#'):
            send_command_to_device(f"LED:{end_hex}")


# --- Web routes & socket events ---
@app.route('/')
def index():
    return render_template('index.html')

@socketio.on('connect')
def on_connect():
    print("UI connected")
    socketio.emit('full_profile', state)

@socketio.on('update_profile')
def on_update_profile(data):
    global state
    if isinstance(data, dict):
        state = data
        save_profile()
        try:
            led = state['layers'][state['currentLayer']]['ledColor']
            if led: send_command_to_device(f"LED:{led}")
        except (KeyError, IndexError): pass
        # print("Profile updated from UI and saved.") # Can be noisy
    else:
        print("Ignored update_profile: data not dict")

@socketio.on('scan_ports')
def on_scan_ports():
    ports = serial.tools.list_ports.comports()
    port_list = [{'device': p.device, 'description': p.description} for p in ports]
    socketio.emit('port_list', {'ports': port_list})

@socketio.on('connect_device')
def connect_device(data):
    global serial_connection
    port = data.get('port') if isinstance(data, dict) else None
    if not port:
        socketio.emit('device_status', {'status': 'error', 'port': ''})
        return
    if serial_connection and serial_connection.is_open:
        try: serial_connection.close()
        except Exception: pass
    try:
        serial_connection = serial.Serial(port, 9600, timeout=1)
        state['last_port'] = port
        save_profile()
        socketio.emit('device_status', {'status': 'connected', 'port': port})
        print(f"Connected to {port}")
        
        try:
            led = state['layers'][state['currentLayer']]['ledColor']
            if led: send_command_to_device(f"LED:{led}")
        except (KeyError, IndexError): pass
        
    except serial.SerialException as e:
        print(f"Failed to connect: {e}")
        socketio.emit('device_status', {'status': 'error', 'port': port})

# --- NEW: Socket handler for getting running applications ---
@socketio.on('get_running_apps')
def on_get_running_apps():
    """Scans for running processes and sends a list back to the UI."""
    try:
        app_names = {p.info['name'] for p in psutil.process_iter(['name'])}
        sorted_apps = sorted(list(app_names), key=str.lower)
        socketio.emit('running_apps_list', {'apps': sorted_apps})
    except Exception as e:
        print(f"Could not get app list: {e}")

# --- Run server ---
def run_flask():
    socketio.run(app, host='127.0.0.1', port=5000, allow_unsafe_werkzeug=True)

# --- Main ---
if __name__ == '__main__':
    load_profile()
    last_port = state.get('last_port')
    if last_port:
        try:
            print(f"Attempting to auto-connect to last known port: {last_port}")
            connect_device({'port': last_port})
        except Exception as e:
            print(f"Could not auto-connect to {last_port}: {e}")

    # Start all background threads
    threading.Thread(target=serial_reader, daemon=True).start()
    threading.Thread(target=app_monitoring_thread, daemon=True).start()
    threading.Thread(target=run_flask, daemon=True).start()

    # Create and start the UI window
    webview.create_window('Nomi Configurator', 'http://127.0.0.1:5000', width=1280, height=800)
    webview.start()

INDEX.HTML

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Nomi Configurator</title>
    <!-- Tailwind CSS -->
    <script src="https://cdn.tailwindcss.com"></script>
    <!-- Font Awesome for Icons -->
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/all.min.css">
    <!-- Google Fonts: Inter -->
    <link rel="preconnect" href="https://fonts.googleapis.com">
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
    <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700;900&display=swap" rel="stylesheet">
    <!-- Socket.IO Client -->
    <script src="https://cdn.socket.io/4.7.5/socket.io.min.js"></script>
    <style>
        /* Custom Styles */
        body {
            font-family: 'Inter', sans-serif;
            background-color: #000000;
            color: #E5E7EB;
            overflow: hidden;
        }
        
        @keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
        .spin-animation { animation: spin 8s linear infinite; }

        /* General interactive element styling */
        .interactive-element {
            display: inline-flex; align-items: center; justify-content: center;
            border-radius: 0.5rem; background-color: #27272A;
            border: 1px solid #3F3F46; border-bottom-width: 3px;
            font-weight: 600; transition: all 0.1s ease-in-out;
            user-select: none; cursor: pointer;
        }
        .interactive-element:hover { background-color: #3F3F46; border-color: #52525B; }
        .interactive-element.selected { background-color: #3B82F6; border-color: #60A5FA; color: white; }

        /* Encoder dial styling */
        .encoder-dial {
            width: 100px; height: 100px; border-radius: 50%;
            background: radial-gradient(circle, #3F3F46 0%, #18181B 100%);
            border: 2px solid #52525B;
            position: relative;
        }
        .encoder-dial::after {
            content: ''; position: absolute; top: 10px; left: 50%;
            transform: translateX(-50%); width: 4px; height: 20px;
            background-color: #A1A1AA; border-radius: 2px;
        }

        /* Layer Tab styling */
        .layer-tab {
            background-color: #18181B;
            border-top-left-radius: 0.5rem;
            border-top-right-radius: 0.5rem;
            border: 1px solid #3F3F46;
            border-bottom: none;
            border-top-width: 3px; /* For color tagging */
        }
        .layer-tab.active {
            background-color: #000000;
            border-color: #52525B;
        }

        /* Hide elements by default */
        .view, .modal, #context-menu { display: none; }
        .view.active, .modal.active, #context-menu.active { display: flex; }
        
        /* Macro Action Type Button Styling */
        .action-type-btn.selected {
            background-color: #3B82F6;
            border-color: #60A5FA;
        }
        
        /* Drag and Drop Styling */
        .draggable-action.dragging {
            opacity: 0.5;
            background-color: #3F3F46;
        }
        .drop-indicator {
            height: 2px;
            background-color: #3B82F6;
            margin: 4px 0;
        }
        
        /* Custom styling for color input to remove default appearance */
        input[type="color"] {
            -webkit-appearance: none;
            -moz-appearance: none;
            appearance: none;
            padding: 0;
            border: none;
            background-color: transparent;
        }
        input[type="color"]::-webkit-color-swatch-wrapper {
            padding: 0;
        }
        input[type="color"]::-webkit-color-swatch {
            border: 1px solid #52525B;
            border-radius: 0.25rem;
        }

        /* NEW: Solid style ONLY for the app link dropdown */
        #link-app-select {
            -webkit-appearance: none; -moz-appearance: none; appearance: none;
            background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%239CA3AF' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e");
            background-position: right 0.5rem center;
            background-repeat: no-repeat;
            background-size: 1.5em 1.5em;
            padding-right: 2.5rem;
        }

        /* Toggle Switch Styling */
        .toggle-bg:after {
            content: '';
            @apply absolute top-0.5 left-0.5 bg-white border border-gray-300 rounded-full h-5 w-5 transition shadow-sm;
        }
        input:checked + .toggle-bg:after {
            @apply transform translate-x-full;
        }
        input:checked + .toggle-bg {
            @apply bg-blue-600;
        }
        
        /* Lighting page specific styles */
        .lighting-list-item.selected {
            background-color: #3B82F6;
        }
        .led-diode {
            transition: fill 0.2s ease-in-out;
        }
    </style>
</head>
<body class="bg-black text-gray-300">

    <div class="flex h-screen">
        <!-- Sidebar -->
        <aside class="w-20 bg-[#111111] flex flex-col justify-between items-center py-5 z-20">
            <div id="nav-icons" class="flex flex-col space-y-4">
                <a href="#" data-view="home" class="nav-icon text-gray-900 bg-green-500 p-3 rounded-lg"><i class="fa-solid fa-house fa-lg"></i></a>
                <a href="#" data-view="configure" class="nav-icon text-gray-400 hover:text-white hover:bg-gray-700 p-3 rounded-lg"><i class="fa-solid fa-wrench fa-lg"></i></a>
                <a href="#" data-view="macros" class="nav-icon text-gray-400 hover:text-white hover:bg-gray-700 p-3 rounded-lg"><i class="fa-solid fa-share-nodes fa-lg"></i></a>
                <a href="#" data-view="lighting" class="nav-icon text-gray-400 hover:text-white hover:bg-gray-700 p-3 rounded-lg"><i class="fa-solid fa-lightbulb fa-lg"></i></a>
            </div>
            <div>
                <a href="#" data-view="settings" class="nav-icon text-gray-400 hover:text-white hover:bg-gray-700 p-3 rounded-lg"><i class="fa-solid fa-cog fa-lg"></i></a>
            </div>
        </aside>

        <!-- Main Content Area -->
        <div class="flex-1 relative">
            <!-- Home/Connect View -->
            <main id="home-view" class="view active flex-col h-full">
                 <div class="absolute top-6 right-8 flex items-center space-x-4">
                    <span id="connection-text" class="text-sm text-gray-400">NO DEVICE CONNECTED</span>
                    <div class="relative">
                        <button id="connect-btn" class="bg-gray-800 text-white text-xs font-semibold px-4 py-2 rounded-full flex items-center space-x-2 hover:bg-gray-700 transition-colors">
                            <span>SELECT COM PORT</span>
                            <i class="fa-solid fa-chevron-down text-xs"></i>
                        </button>
                        <div id="port-dropdown" class="absolute right-0 mt-2 w-64 bg-[#18181B] rounded-md shadow-lg z-50 hidden">
                            <!-- Port list will be generated here -->
                        </div>
                    </div>
                </div>
                <div class="flex-grow flex flex-col justify-center items-center text-center">
                    <!-- SVG Illustration -->
                    <div class="relative">
                        <svg width="400" height="400" viewBox="0 0 500 500" xmlns="http://www.w3.org/2000/svg">
                           <defs>
                                <linearGradient id="bodyGradient" x1="0%" y1="0%" x2="0%" y2="100%"><stop offset="0%" style="stop-color:#4A5568;stop-opacity:1" /><stop offset="100%" style="stop-color:#2D3748;stop-opacity:1" /></linearGradient>
                                <radialGradient id="dialGradient"><stop offset="0%" stop-color="#A0AEC0" /><stop offset="90%" stop-color="#4A5568" /><stop offset="100%" stop-color="#2D3748" /></radialGradient>
                                <filter id="innerShadow"><feFlood flood-color="black" flood-opacity="0.5"/><feComposite in2="SourceAlpha" operator="out"/><feGaussianBlur stdDeviation="3"/><feComposite in2="SourceAlpha" operator="in"/><feOffset dx="2" dy="2"/></filter>
                            </defs>
                            <rect x="25" y="25" width="450" height="450" rx="40" ry="40" fill="url(#bodyGradient)" stroke="#1A202C" stroke-width="2"/>
                            <circle cx="300" cy="300" r="140" fill="#1A202C" filter="url(#innerShadow)"/><circle cx="300" cy="300" r="135" fill="none" stroke="url(#dialGradient)" stroke-width="5"/>
                            <rect x="60" y="60" width="80" height="380" rx="15" ry="15" fill="#1A202C" filter="url(#innerShadow)"/>
                            <rect x="75" y="75" width="50" height="50" rx="5" ry="5" fill="#2D3748" stroke="#4A5568" stroke-width="2"/><rect x="75" y="165" width="50" height="50" rx="5" ry="5" fill="#2D3748" stroke="#4A5568" stroke-width="2"/><rect x="75" y="255" width="50" height="50" rx="5" ry="5" fill="#2D3748" stroke="#4A5568" stroke-width="2"/><rect x="75" y="345" width="50" height="50" rx="5" ry="5" fill="#2D3748" stroke="#4A5568" stroke-width="2"/>
                            <rect x="170" y="75" width="50" height="80" rx="15" ry="15" fill="url(#bodyGradient)" stroke="#1A202C" stroke-width="2"/><rect x="175" y="80" width="40" height="70" rx="10" ry="10" fill="#2D3748" stroke="#4A5568" stroke-width="2"/>
                        </svg>
                    </div>
                    <div id="connection-status-indicator" class="mt-8">
                        <button id="configure-btn" class="mt-6 text-gray-500 tracking-widest text-sm font-semibold hover:text-white transition-colors">CONFIGURE &rarr;</button>
                    </div>
                </div>
                 <div class="absolute bottom-6 right-8 text-right text-xs text-gray-600 font-semibold leading-tight">
                    <p>DEV VERSION UI V1</p><p>SPARKLAB</p><p>RIGHTS RESERVED</p>
                </div>
            </main>

            <!-- Configure View -->
            <main id="configure-view" class="view h-full p-8 space-x-8">
                <div class="w-2/3 flex flex-col">
                    <div id="layer-tabs-container" class="flex items-end -mb-px"></div>
                    <div class="flex-grow bg-black border border-gray-700 rounded-lg rounded-tl-none p-6 flex flex-col space-y-8">
                        <div>
                            <h2 class="text-2xl font-bold mb-4">Macro Pad</h2>
                            <div id="macropad-layout" class="grid grid-cols-3 gap-4 w-max"></div>
                        </div>
                        <div>
                            <h2 class="text-2xl font-bold mb-4">Encoder</h2>
                            <div class="flex items-center space-x-8">
                                <div class="encoder-dial"></div>
                                <div id="encoder-controls" class="space-y-3"></div>
                            </div>
                        </div>
                    </div>
                </div>
                <div class="w-1/3 bg-[#111111] rounded-lg p-4 overflow-y-auto">
                    <h3 class="text-xl font-bold mb-4">Keycodes</h3>
                    <div id="keycode-palette"></div>
                </div>
            </main>

            <!-- Macros View -->
            <main id="macros-view" class="view h-full p-8 space-x-8">
                <!-- Left: Macro List -->
                <div class="w-1/3 bg-[#111111] rounded-lg p-4 flex flex-col">
                    <div class="flex justify-between items-center mb-4">
                        <h3 class="text-xl font-bold">Macros</h3>
                        <button id="new-macro-btn" class="px-3 py-1 bg-blue-600 rounded hover:bg-blue-500 text-sm font-semibold">New</button>
                    </div>
                    <div id="macro-list" class="flex-grow overflow-y-auto space-y-2"></div>
                </div>
                <!-- Right: Macro Editor -->
                <div id="macro-editor" class="w-2/3 flex-col hidden">
                     <div class="flex justify-between items-center mb-4">
                        <h3 id="macro-editor-title" class="text-2xl font-bold"></h3>
                        <div class="flex space-x-2">
                           <button id="record-macro-btn" class="px-4 py-2 bg-red-600 rounded hover:bg-red-500 font-semibold"><i class="fa-solid fa-circle mr-2"></i>Record</button>
                           <button id="add-action-btn" class="px-4 py-2 bg-green-600 rounded hover:bg-green-500 font-semibold"><i class="fa-solid fa-plus mr-2"></i>Add Action</button>
                        </div>
                    </div>
                    <div id="macro-action-list" class="flex-grow bg-black border border-gray-700 rounded-lg p-4 space-y-2 overflow-y-auto"></div>
                </div>
                <div id="macro-editor-placeholder" class="w-2/3 flex justify-center items-center text-gray-500">
                    <p>Select a macro or create a new one to begin.</p>
                </div>
            </main>

            <!-- Lighting View -->
            <main id="lighting-view" class="view h-full p-8 space-x-8">
                <div class="w-1/3 bg-[#111111] rounded-lg p-4 flex flex-col">
                    <h3 class="text-xl font-bold mb-4">Lighting Targets</h3>
                    <div id="lighting-target-list" class="flex-grow overflow-y-auto space-y-2"></div>
                </div>
                <div class="w-2/3 flex flex-col items-center justify-center">
                    <div id="lighting-visualizer" class="mb-8">
                        <!-- SVG LED Ring will be generated here -->
                    </div>
                    <div id="lighting-color-picker-area" class="flex flex-col items-center hidden">
                         <h4 id="lighting-target-name" class="text-lg font-semibold mb-2"></h4>
                         <input type="color" id="lighting-color-picker" class="w-48 h-24 cursor-pointer">
                    </div>
                </div>
            </main>

            <!-- Settings View -->
            <main id="settings-view" class="view h-full p-8 flex-col max-w-4xl mx-auto">
                <h2 class="text-3xl font-bold mb-6">Settings</h2>
                <div class="space-y-8">
                     <!-- Profile Management -->
                    <div>
                        <h3 class="text-xl font-bold mb-4 text-gray-300">Profile Management</h3>
                        <div class="bg-[#111111] p-4 rounded-lg space-y-4">
                            <div class="flex justify-between items-center">
                                <div>
                                    <h4 class="font-semibold">Export Configuration</h4>
                                    <p class="text-sm text-gray-500">Save all your layers, macros, and settings to a JSON file.</p>
                                </div>
                                <button id="export-btn" class="px-4 py-2 bg-blue-600 rounded hover:bg-blue-500 font-semibold">Export Profile</button>
                            </div>
                            <div class="border-t border-gray-700"></div>
                            <div class="flex justify-between items-center">
                                <div>
                                    <h4 class="font-semibold">Import Configuration</h4>
                                    <p class="text-sm text-gray-500">Load a profile from a JSON file. This will overwrite current settings.</p>
                                </div>
                                <button id="import-btn" class="px-4 py-2 bg-gray-600 rounded hover:bg-gray-500 font-semibold">Import Profile</button>
                            </div>
                        </div>
                    </div>
                    <!-- Application Settings -->
                    <div>
                        <h3 class="text-xl font-bold mb-4 text-gray-300">Application</h4>
                        <div class="bg-[#111111] p-4 rounded-lg space-y-4">
                            <label for="start-with-os" class="flex justify-between items-center cursor-pointer">
                                <div>
                                    <h4 class="font-semibold">Start with OS</h4>
                                    <p class="text-sm text-gray-500">Automatically launch Nomi when you log in.</p>
                                </div>
                                <div class="relative"><input type="checkbox" id="start-with-os" class="sr-only"><div class="toggle-bg bg-gray-700 border-2 border-transparent h-6 w-11 rounded-full"></div></div>
                            </label>
                            <label for="check-updates" class="flex justify-between items-center cursor-pointer">
                                <div>
                                    <h4 class="font-semibold">Check for Updates</h4>
                                    <p class="text-sm text-gray-500">Automatically check for new software versions on startup.</p>
                                </div>
                                <div class="relative"><input type="checkbox" id="check-updates" class="sr-only" checked><div class="toggle-bg bg-gray-700 border-2 border-transparent h-6 w-11 rounded-full"></div></div>
                            </label>
                        </div>
                    </div>
                    <!-- Device Info -->
                    <div>
                        <h3 class="text-xl font-bold mb-4 text-gray-300">Device</h4>
                        <div class="bg-[#111111] p-4 rounded-lg">
                            <p class="text-sm"><span class="font-semibold text-gray-400">Device:</span> <span id="device-name">Nomi Macro Pad</span></p>
                            <p class="text-sm"><span class="font-semibold text-gray-400">Firmware Version:</span> <span id="firmware-version">1.0.0 (Placeholder)</span></p>
                        </div>
                    </div>
                </div>
            </main>
        </div>
    </div>

    <!-- Modals -->
    <div id="new-macro-modal" class="modal absolute inset-0 bg-black bg-opacity-75 justify-center items-center z-30">
        <div class="bg-[#18181B] p-6 rounded-lg shadow-xl w-full max-w-md">
            <h3 class="text-xl font-bold mb-4">New Macro</h3>
            <input type="text" id="new-macro-name-input" class="w-full bg-gray-800 border border-gray-600 rounded px-3 py-2 mb-4" placeholder="Enter macro name...">
            <div class="flex justify-end space-x-3"><button id="new-macro-cancel" class="px-4 py-2 bg-gray-600 rounded hover:bg-gray-500">Cancel</button><button id="new-macro-save" class="px-4 py-2 bg-blue-600 rounded hover:bg-blue-500">Create</button></div>
        </div>
    </div>

    <!-- Link App Modal with updated styling for dropdown -->
    <div id="link-app-modal" class="modal absolute inset-0 bg-black bg-opacity-75 justify-center items-center z-30">
        <div class="bg-[#18181B] p-6 rounded-lg shadow-xl w-full max-w-md">
            <h3 class="text-xl font-bold mb-4">Link Layer to Application</h3>
            
            <label class="text-sm font-semibold text-gray-400">Select from running applications</label>
            <div class="flex space-x-2 mt-1 mb-4">
                <select id="link-app-select" class="w-full bg-[#27272A] border border-gray-600 rounded-md px-3 py-2">
                    <option value="">-- Select an App --</option>
                </select>
                <button id="link-app-refresh-btn" class="p-2 bg-gray-600 rounded-md hover:bg-gray-500" title="Refresh List">
                    <i class="fa-solid fa-sync"></i>
                </button>
            </div>
            
            <label class="text-sm font-semibold text-gray-400">Or enter executable name manually</label>
            <input type="text" id="link-app-input" class="w-full bg-gray-800 border border-gray-600 rounded px-3 py-2 mt-1" placeholder="e.g., Photoshop.exe">
            
            <div class="flex justify-end space-x-3 mt-6">
                <button id="link-app-cancel" class="px-4 py-2 bg-gray-600 rounded-md hover:bg-gray-500">Cancel</button>
                <button id="link-app-save" class="px-4 py-2 bg-blue-600 rounded-md hover:bg-blue-500">Save</button>
            </div>
        </div>
    </div>

    <div id="add-action-modal" class="modal absolute inset-0 bg-black bg-opacity-75 justify-center items-center z-30">
        <div class="bg-[#18181B] p-6 rounded-lg shadow-xl w-full max-w-md">
            <h3 class="text-xl font-bold mb-4">Add Macro Action</h3>
            <div class="space-y-4">
                <div>
                    <label class="font-semibold">Action Type</label>
                    <div id="action-type-selector" class="grid grid-cols-3 gap-2 mt-2">
                        <button data-action="keystroke" class="action-type-btn interactive-element p-3 flex-col text-xs"><i class="fa-solid fa-keyboard fa-lg mb-1"></i>Keystroke</button>
                        <button data-action="text" class="action-type-btn interactive-element p-3 flex-col text-xs"><i class="fa-solid fa-font fa-lg mb-1"></i>Text</button>
                        <button data-action="delay" class="action-type-btn interactive-element p-3 flex-col text-xs"><i class="fa-solid fa-clock fa-lg mb-1"></i>Delay</button>
                    </div>
                </div>
                <div id="action-config-area"></div>
            </div>
            <div class="flex justify-end space-x-3 mt-6"><button id="add-action-cancel" class="px-4 py-2 bg-gray-600 rounded hover:bg-gray-500">Cancel</button><button id="add-action-save" class="px-4 py-2 bg-blue-600 rounded hover:bg-blue-500">Add Action</button></div>
        </div>
    </div>

    <!-- Layer Right-Click Context Menu -->
    <div id="context-menu" class="absolute bg-[#18181B] rounded-md shadow-xl z-40 flex-col text-sm py-1">
        <button id="ctx-rename" class="w-full text-left px-4 py-2 hover:bg-gray-700">Rename</button>
        <div class="px-4 py-2">
            <label for="ctx-color-input" class="text-xs text-gray-400 block mb-1">Tag Color</label>
            <input type="color" id="ctx-color-input" class="w-full h-8 cursor-pointer">
        </div>
        <div class="border-t border-gray-700 my-1"></div>
        <button id="ctx-clear-color" class="w-full text-left px-4 py-2 hover:bg-gray-700">Clear Color</button>
    </div>
    
    <!-- Hidden file input for import -->
    <input type="file" id="import-file-input" class="hidden" accept=".json">


<script>
// Establish Socket.IO connection
const socket = io();

document.addEventListener('DOMContentLoaded', () => {
    // --- STATE MANAGEMENT ---
    const MAX_LAYERS = 6;
    const KEY_MAP = { 'Control': 'CTRL', 'Shift': 'SHFT', 'Alt': 'ALT', 'Meta': 'WIN', 'Enter': 'ENT', 'Backspace': 'BKSP', 'Delete': 'DEL', 'Tab': 'TAB', 'Escape': 'ESC', 'ArrowUp': 'UP', 'ArrowDown': 'DOWN', 'ArrowLeft': 'LEFT', 'ArrowRight': 'RGHT', ' ': 'SPACE' };
    
    const DEFAULT_KEYCODES = {
        "Basic": ['A', 'B', 'C', '1', '2', '3', 'ENT', 'ESC', 'BKSP', 'TAB', 'SPACE', 'SHFT', 'CTRL', 'ALT', 'WIN'],
        "System": ['MUTE', 'VOLU', 'VOLD', 'PLAY', 'STOP', 'NEXT', 'PREV'],
        "Navigation": ['UP', 'DOWN', 'LEFT', 'RGHT', 'HOME', 'END'],
        "Layers": ['Lyr 0', 'Lyr 1', 'Lyr 2', 'Lyr 3', 'Lyr 4', 'Lyr 5']
    };

    let state = {
        connected: false,
        currentView: 'home',
        currentLayer: 0,
        selectedControl: { type: null, index: null },
        linkingLayerIndex: null,
        contextMenu: { active: false, targetLayer: null },
        selectedMacro: null,
        isRecording: false,
        newAction: { type: null, value: null },
        draggedAction: { fromIndex: null },
        selectedLightingTarget: { type: null, index: null },
        layers: [],
        macros: [],
        keycodes: DEFAULT_KEYCODES
    };

    // --- DOM ELEMENTS ---
    const allViews = document.querySelectorAll('.view');
    const navIcons = document.querySelectorAll('.nav-icon');
    const connectBtn = document.getElementById('connect-btn');
    const configureBtn = document.getElementById('configure-btn');
    const layerTabsContainer = document.getElementById('layer-tabs-container');
    const macropadLayoutContainer = document.getElementById('macropad-layout');
    const encoderControlsContainer = document.getElementById('encoder-controls');
    const keycodePaletteContainer = document.getElementById('keycode-palette');
    const connectionText = document.getElementById('connection-text');
    const portDropdown = document.getElementById('port-dropdown');
    const connectionStatusIndicator = document.getElementById('connection-status-indicator');
    
    // Modals
    const newMacroModal = document.getElementById('new-macro-modal');
    const newMacroNameInput = document.getElementById('new-macro-name-input');
    const newMacroSaveBtn = document.getElementById('new-macro-save');
    const newMacroCancelBtn = document.getElementById('new-macro-cancel');
    const linkAppModal = document.getElementById('link-app-modal');
    const linkAppInput = document.getElementById('link-app-input');
    const linkAppSelect = document.getElementById('link-app-select');
    const linkAppRefreshBtn = document.getElementById('link-app-refresh-btn');
    const linkAppSaveBtn = document.getElementById('link-app-save');
    const linkAppCancelBtn = document.getElementById('link-app-cancel');
    const addActionModal = document.getElementById('add-action-modal');
    const addActionSaveBtn = document.getElementById('add-action-save');
    const addActionCancelBtn = document.getElementById('add-action-cancel');
    const actionTypeSelector = document.getElementById('action-type-selector');
    const actionConfigArea = document.getElementById('action-config-area');

    // Context Menu
    const contextMenu = document.getElementById('context-menu');
    const ctxRenameBtn = document.getElementById('ctx-rename');
    const ctxColorInput = document.getElementById('ctx-color-input');
    const ctxClearColorBtn = document.getElementById('ctx-clear-color');
    
    // Macro View
    const macroListContainer = document.getElementById('macro-list');
    const newMacroBtn = document.getElementById('new-macro-btn');
    const macroEditor = document.getElementById('macro-editor');
    const macroEditorPlaceholder = document.getElementById('macro-editor-placeholder');
    const macroEditorTitle = document.getElementById('macro-editor-title');
    const recordMacroBtn = document.getElementById('record-macro-btn');
    const addActionBtn = document.getElementById('add-action-btn');
    const macroActionListContainer = document.getElementById('macro-action-list');
    
    // Settings & Lighting
    const exportBtn = document.getElementById('export-btn');
    const importBtn = document.getElementById('import-btn');
    const importFileInput = document.getElementById('import-file-input');
    const lightingTargetList = document.getElementById('lighting-target-list');
    const lightingVisualizer = document.getElementById('lighting-visualizer');
    const lightingColorPickerArea = document.getElementById('lighting-color-picker-area');
    const lightingColorPicker = document.getElementById('lighting-color-picker');
    const lightingTargetName = document.getElementById('lighting-target-name');
   // --- SOCKET.IO EVENT LISTENERS ---
    socket.on('connect', () => {
        console.log('Connected to backend server.');
    });

    socket.on('full_profile', (data) => {
        console.log('Received full profile from backend.');
        state.layers = data.layers || [];
        state.macros = data.macros || [];
        state.keycodes = data.keycodes || DEFAULT_KEYCODES;
        state.currentLayer = data.currentLayer || 0;
        if (state.layers.length === 0) {
            addLayer();
        } else {
            renderAll();
        }
    });
    
    socket.on('device_status', (data) => {
        state.connected = data.status === 'connected';
        if (state.connected) {
            connectionText.textContent = `CONNECTED: ${data.port}`;
            connectionText.classList.add('text-green-400');
            connectionStatusIndicator.innerHTML = `<span class="bg-green-500 text-black text-sm font-bold px-5 py-2 rounded-full inline-flex items-center space-x-2"><i class="fa-solid fa-link"></i><span>CONNECTED</span></span>`;
            setTimeout(() => switchView('configure'), 500);
        } else {
            connectionText.textContent = data.status === 'error' ? `FAILED: ${data.port}` : 'NO DEVICE CONNECTED';
            connectionText.classList.remove('text-green-400');
            connectionStatusIndicator.innerHTML = `<button id="configure-btn" class="mt-6 text-gray-500 tracking-widest text-sm font-semibold hover:text-white transition-colors">CONFIGURE &rarr;</button>`;
        }
    });

    socket.on('port_list', (data) => {
        portDropdown.innerHTML = '';
        if (data.ports.length > 0) {
            data.ports.forEach(port => {
                const item = document.createElement('a');
                item.href = '#';
                item.className = 'block px-4 py-2 text-sm text-gray-300 hover:bg-gray-700';
                item.textContent = `${port.device}: ${port.description}`;
                item.addEventListener('click', (e) => {
                    e.preventDefault();
                    socket.emit('connect_device', { port: port.device });
                    portDropdown.classList.add('hidden');
                });
                portDropdown.appendChild(item);
            });
        } else {
            portDropdown.innerHTML = '<span class="block px-4 py-2 text-sm text-gray-500">No devices found</span>';
        }
        portDropdown.classList.remove('hidden');
    });
    
    socket.on('layer_changed', (data) => {
        state.currentLayer = data.layer;
        renderAll();
    });
    
    socket.on('running_apps_list', (data) => {
        linkAppSelect.innerHTML = '<option value="">-- Select an App --</option>';
        if (data && data.apps) {
            data.apps.forEach(app => {
                const option = document.createElement('option');
                option.value = app;
                option.textContent = app;
                linkAppSelect.appendChild(option);
            });
        }
    });
    
    // --- RENDER FUNCTIONS ---
    const renderAll = () => {
        if (state.currentView === 'configure') {
            renderLayerTabs();
            renderMacroPad();
            renderEncoder();
            renderKeycodePalette();
        } else if (state.currentView === 'macros') {
            renderMacroList();
            renderMacroEditor();
        } else if (state.currentView === 'lighting') {
            renderLightingView();
        }
    };
    
    const renderLayerTabs = () => {
        layerTabsContainer.innerHTML = '';
        state.layers.forEach((layer, index) => {
            const tabEl = document.createElement('div');
            tabEl.className = 'layer-tab flex items-center space-x-2 px-4 py-2 text-sm font-semibold text-gray-300 cursor-pointer';
            tabEl.dataset.layerIndex = index;
            if (index === state.currentLayer) tabEl.classList.add('active');
            tabEl.style.borderTopColor = layer.color || '#3F3F46';
            const nameEl = document.createElement('span');
            nameEl.textContent = layer.name;
            nameEl.className = 'layer-name-span';
            nameEl.addEventListener('click', () => { state.currentLayer = index; state.selectedControl = { type: null, index: null }; renderAll(); updateProfileOnBackend(); });
            nameEl.addEventListener('dblclick', (e) => { e.stopPropagation(); editLayerName(index, nameEl); });
            const linkIcon = document.createElement('i');
            linkIcon.className = 'fa-solid fa-link text-xs';
            linkIcon.classList.toggle('text-gray-500', !layer.linkedApp);
            linkIcon.classList.toggle('text-green-500', !!layer.linkedApp);
            linkIcon.addEventListener('click', (e) => { e.stopPropagation(); openLinkAppModal(index); });
            tabEl.appendChild(nameEl);
            tabEl.appendChild(linkIcon);
            layerTabsContainer.appendChild(tabEl);
            tabEl.addEventListener('contextmenu', (e) => {
                e.preventDefault();
                e.stopPropagation();
                openContextMenu(e.clientX, e.clientY, index);
            });
        });
        if (state.layers.length < MAX_LAYERS) {
            const addLayerBtn = document.createElement('button');
            addLayerBtn.className = 'px-3 py-2 text-gray-400 hover:text-white';
            addLayerBtn.innerHTML = '<i class="fa-solid fa-plus"></i>';
            addLayerBtn.addEventListener('click', addLayer);
            layerTabsContainer.appendChild(addLayerBtn);
        }
    };
    const renderMacroPad = () => {
        macropadLayoutContainer.innerHTML = '';
        const currentKeymap = state.layers[state.currentLayer].keymap;
        currentKeymap.forEach((key, index) => {
            const keyEl = document.createElement('button');
            keyEl.className = 'interactive-element text-lg';
            keyEl.textContent = key.k;
            keyEl.style.width = '6rem';
            keyEl.style.height = '6rem';
            if (state.selectedControl.type === 'key' && state.selectedControl.index === index) keyEl.classList.add('selected');
            keyEl.addEventListener('click', () => { state.selectedControl = { type: 'key', index: index }; renderAll(); });
            macropadLayoutContainer.appendChild(keyEl);
        });
    };
    const renderEncoder = () => {
        encoderControlsContainer.innerHTML = '';
        const currentEncoderMap = state.layers[state.currentLayer].encoder;
        const encoderActions = [ { id: 'cw', label: 'Clockwise' }, { id: 'ccw', label: 'Counter-Clockwise' }, { id: 'press', label: 'Press' } ];
        encoderActions.forEach(action => {
            const wrapper = document.createElement('div');
            wrapper.className = 'flex items-center space-x-4';
            const labelEl = document.createElement('span');
            labelEl.className = 'w-40 text-gray-400';
            labelEl.textContent = action.label;
            const btnEl = document.createElement('button');
            btnEl.className = 'interactive-element px-4 py-2 w-32';
            btnEl.textContent = currentEncoderMap[action.id].k;
            if (state.selectedControl.type === 'encoder' && state.selectedControl.index === action.id) btnEl.classList.add('selected');
            btnEl.addEventListener('click', () => { state.selectedControl = { type: 'encoder', index: action.id }; renderAll(); });
            wrapper.appendChild(labelEl);
            wrapper.appendChild(btnEl);
            encoderControlsContainer.appendChild(wrapper);
        });
    };
    const renderKeycodePalette = () => {
        keycodePaletteContainer.innerHTML = '';
        if (!state.keycodes) state.keycodes = DEFAULT_KEYCODES;
        const keycodesWithMacros = {...state.keycodes, "Macros": state.macros.map(m => m.name)};
        for (const category in keycodesWithMacros) {
            const categoryEl = document.createElement('div');
            categoryEl.className = 'mb-4';
            categoryEl.innerHTML = `<h4 class="font-bold text-gray-400 mb-2">${category}</h4>`;
            const gridEl = document.createElement('div');
            gridEl.className = 'grid grid-cols-3 gap-2';
            keycodesWithMacros[category].forEach(keycode => {
                const keycodeBtn = document.createElement('button');
                keycodeBtn.className = 'interactive-element p-2 text-xs';
                keycodeBtn.textContent = keycode;
                keycodeBtn.addEventListener('click', () => assignKeycode(keycode));
                gridEl.appendChild(keycodeBtn);
            });
            categoryEl.appendChild(gridEl);
            keycodePaletteContainer.appendChild(categoryEl);
        }
    };

    // --- MACRO RENDER FUNCTIONS ---
    const renderMacroList = () => {
        macroListContainer.innerHTML = '';
        state.macros.forEach((macro, index) => {
            const item = document.createElement('div');
            item.className = 'flex justify-between items-center p-2 rounded-md cursor-pointer hover:bg-gray-700';
            if(state.selectedMacro === index) item.classList.add('bg-blue-600', 'hover:bg-blue-500');
            const nameSpan = document.createElement('span');
            nameSpan.textContent = macro.name;
            item.appendChild(nameSpan);
            const deleteBtn = document.createElement('button');
            deleteBtn.className = 'text-gray-500 hover:text-red-500 px-2';
            deleteBtn.innerHTML = '<i class="fa-solid fa-trash-alt fa-xs"></i>';
            deleteBtn.onclick = (e) => { e.stopPropagation(); deleteMacro(index); };
            item.appendChild(deleteBtn);
            item.onclick = () => { state.selectedMacro = index; renderAll(); };
            macroListContainer.appendChild(item);
        });
    };
    const renderMacroEditor = () => {
        if (state.selectedMacro === null) {
            macroEditor.classList.add('hidden');
            macroEditorPlaceholder.classList.remove('hidden');
            macroEditorPlaceholder.classList.add('flex');
            return;
        }
        macroEditor.classList.remove('hidden');
        macroEditor.classList.add('flex');
        macroEditorPlaceholder.classList.add('hidden');
        macroEditorPlaceholder.classList.remove('flex');
        
        if (state.isRecording) {
            recordMacroBtn.innerHTML = '<i class="fa-solid fa-stop mr-2"></i>Stop';
            recordMacroBtn.classList.add('animate-pulse');
        } else {
            recordMacroBtn.innerHTML = '<i class="fa-solid fa-circle mr-2"></i>Record';
            recordMacroBtn.classList.remove('animate-pulse');
        }

        const macro = state.macros[state.selectedMacro];
        macroEditorTitle.textContent = macro.name;
        macroActionListContainer.innerHTML = '';
        if (macro.actions.length === 0) {
            macroActionListContainer.innerHTML = '<p class="text-gray-500 text-center">No actions yet. Click "Add Action" or "Record" to start.</p>';
            return;
        }
        macro.actions.forEach((action, index) => {
            const actionEl = document.createElement('div');
            actionEl.className = 'draggable-action bg-gray-800 p-3 rounded-md flex items-center justify-between cursor-grab';
            actionEl.draggable = true;
            actionEl.dataset.actionIndex = index;
            let icon, text;
            switch(action.type) {
                case 'keystroke': icon = 'fa-keyboard'; text = `Key: ${action.value}`; break;
                case 'text': icon = 'fa-font'; text = `Text: "${action.value}"`; break;
                case 'delay': icon = 'fa-clock'; text = `Delay: ${action.value}ms`; break;
            }
            actionEl.innerHTML = `
                <div class="flex items-center space-x-3 pointer-events-none">
                    <i class="fa-solid fa-grip-vertical text-gray-500 mr-2"></i>
                    <i class="fa-solid ${icon} text-gray-400"></i>
                    <span>${text}</span>
                </div>
                <button data-action-index="${index}" class="delete-action-btn text-gray-500 hover:text-red-500 px-2">
                    <i class="fa-solid fa-trash-alt fa-xs"></i>
                </button>
            `;
            macroActionListContainer.appendChild(actionEl);
        });
        document.querySelectorAll('.delete-action-btn').forEach(btn => {
            btn.onclick = () => deleteMacroAction(parseInt(btn.dataset.actionIndex));
        });
    };
    const renderActionConfig = (type) => {
        actionConfigArea.innerHTML = '';
        let content = '';
        switch(type) {
            case 'keystroke': 
                content = `<label class="font-semibold block mb-2">Key</label><input id="action-value-input" type="text" readonly class="w-full bg-gray-700 p-2 rounded cursor-pointer text-center" placeholder="Click here and press a key...">`; 
                break;
            case 'text': 
                content = `<label class="font-semibold block mb-2">Text to type</label><input id="action-value-input" type="text" class="w-full bg-gray-700 p-2 rounded">`; 
                break;
            case 'delay': 
                content = `<label class="font-semibold block mb-2">Delay (in milliseconds)</label><input id="action-value-input" type="number" class="w-full bg-gray-700 p-2 rounded" value="100">`; 
                break;
        }
        actionConfigArea.innerHTML = content;
        
        if (type === 'keystroke') {
            const input = document.getElementById('action-value-input');
            input.addEventListener('keydown', handleKeystrokeInput);
        }
    };
    
    // --- LIGHTING RENDER FUNCTIONS ---
    const renderLightingView = () => {
        lightingTargetList.innerHTML = '';
        
        const targets = [];
        state.layers.forEach((layer, index) => {
            targets.push({ type: 'layer', index: index, name: layer.name, subtext: `Layer ${index}` });
            if (layer.linkedApp) {
                targets.push({ type: 'app', index: index, name: layer.name, subtext: layer.linkedApp });
            }
        });

        targets.forEach(target => {
            const item = document.createElement('div');
            item.className = 'lighting-list-item flex items-center space-x-3 p-2 rounded-md cursor-pointer hover:bg-gray-700';
            item.dataset.targetType = target.type;
            item.dataset.targetIndex = target.index;

            if (state.selectedLightingTarget.type === target.type && state.selectedLightingTarget.index === target.index) {
                item.classList.add('selected');
            }

            const layer = state.layers[target.index];
            item.innerHTML = `
                <div class="w-4 h-4 rounded-full" style="background-color: ${layer.color || '#3F3F46'}; border: 1px solid #52525B;"></div>
                <div>
                    <p class="font-semibold">${target.name}</p>
                    <p class="text-xs text-gray-400">${target.subtext}</p>
                </div>
            `;
            lightingTargetList.appendChild(item);
            item.addEventListener('click', () => selectLightingTarget(target.type, target.index));
        });

        renderLightingVisualizer();
    };

    const renderLightingVisualizer = () => {
        const { type, index } = state.selectedLightingTarget;
        let targetLayer = null;
        let targetName = "Select a target";
        
        if (type && index !== null) {
            targetLayer = state.layers[index];
            targetName = targetLayer.linkedApp && type === 'app' ? `${targetLayer.name} (${targetLayer.linkedApp})` : targetLayer.name;
            lightingColorPickerArea.classList.remove('hidden');
        } else {
            lightingColorPickerArea.classList.add('hidden');
        }

        const ledColor = targetLayer ? targetLayer.ledColor : '#27272A';
        lightingTargetName.textContent = targetName;
        lightingColorPicker.value = ledColor;

        const numDiodes = 12;
        const radius = 100;
        const center = 120;
        let diodesSVG = '';
        for (let i = 0; i < numDiodes; i++) {
            const angle = (i / numDiodes) * 2 * Math.PI;
            const x = center + radius * Math.cos(angle);
            const y = center + radius * Math.sin(angle);
            diodesSVG += `<circle class="led-diode" cx="${x}" cy="${y}" r="10" fill="${ledColor}" stroke="#18181B" stroke-width="2" />`;
        }
        
        lightingVisualizer.innerHTML = `
            <svg width="240" height="240" viewbox="0 0 240 240">
                <circle cx="${center}" cy="${center}" r="${radius}" fill="none" stroke="#3F3F46" stroke-width="10" />
                ${diodesSVG}
            </svg>
        `;
    };


    // --- LOGIC & EVENT HANDLERS ---
    function updateProfileOnBackend() {
        socket.emit('update_profile', state);
    }
    
    const addLayer = () => {
        if (state.layers.length >= MAX_LAYERS) return;
        const newLayerIndex = state.layers.length;
        state.layers.push({ name: `Layer ${newLayerIndex}`, linkedApp: "", color: null, ledColor: "#FFFFFF", keymap: [ { k: '...' }, { k: '...' }, { k: '...' }, { k: '...' }, { k: '...' }, { k: '...' } ], encoder: { cw: { k: '...' }, ccw: { k: '...' }, press: { k: '...' } } });
        state.currentLayer = newLayerIndex;
        state.selectedControl = { type: null, index: null };
        renderAll();
        updateProfileOnBackend();
    };
    const editLayerName = (index, nameEl) => {
        const input = document.createElement('input');
        input.type = 'text';
        input.value = state.layers[index].name;
        input.className = 'bg-gray-800 text-white w-24';
        nameEl.replaceWith(input);
        input.focus();
        input.select();
        const save = () => { state.layers[index].name = input.value.trim() || `Layer ${index}`; renderAll(); updateProfileOnBackend(); };
        input.addEventListener('blur', save);
        input.addEventListener('keydown', (e) => { if (e.key === 'Enter') input.blur(); });
    };

    const openLinkAppModal = (layerIndex) => {
        state.linkingLayerIndex = layerIndex;
        linkAppInput.value = state.layers[layerIndex].linkedApp;
        linkAppModal.classList.add('active');
        linkAppInput.focus();
        socket.emit('get_running_apps');
    };
    const closeLinkAppModal = () => linkAppModal.classList.remove('active');

    const saveLinkedApp = () => {
        if (state.linkingLayerIndex === null) return;
        const selectedApp = linkAppSelect.value;
        const manualApp = linkAppInput.value.trim();
        state.layers[state.linkingLayerIndex].linkedApp = selectedApp || manualApp;
        closeLinkAppModal();
        renderAll();
        updateProfileOnBackend();
    };
    
    const openContextMenu = (x, y, layerIndex) => {
        closeContextMenu();
        state.contextMenu.targetLayer = layerIndex;
        contextMenu.style.left = `${x}px`;
        contextMenu.style.top = `${y}px`;
        ctxColorInput.value = state.layers[layerIndex].color || '#ffffff';
        contextMenu.classList.add('active');
        state.contextMenu.active = true;
    };
    const closeContextMenu = () => {
        if (!state.contextMenu.active) return;
        contextMenu.classList.remove('active');
        state.contextMenu.active = false;
        state.contextMenu.targetLayer = null;
    };
    const setLayerColor = (color) => {
        if (state.contextMenu.targetLayer !== null) {
            state.layers[state.contextMenu.targetLayer].color = color;
            renderAll();
            updateProfileOnBackend();
        }
    };
    ctxRenameBtn.addEventListener('click', () => {
        if (state.contextMenu.targetLayer !== null) {
            const tabEl = document.querySelector(`.layer-tab[data-layer-index="${state.contextMenu.targetLayer}"]`);
            const nameSpan = tabEl.querySelector('.layer-name-span');
            editLayerName(state.contextMenu.targetLayer, nameSpan);
            closeContextMenu();
        }
    });
    ctxColorInput.addEventListener('input', (e) => {
        if (state.contextMenu.targetLayer !== null) {
            const tabEl = document.querySelector(`.layer-tab[data-layer-index="${state.contextMenu.targetLayer}"]`);
            if (tabEl) tabEl.style.borderTopColor = e.target.value;
        }
    });
    ctxColorInput.addEventListener('change', (e) => {
        setLayerColor(e.target.value);
        closeContextMenu();
    });
    ctxClearColorBtn.addEventListener('click', () => {
        setLayerColor(null);
        closeContextMenu();
    });

    const switchView = (viewId) => {
        if (state.isRecording) toggleRecording();
        state.currentView = viewId;
        allViews.forEach(v => v.classList.remove('active'));
        document.getElementById(`${viewId}-view`).classList.add('active');
        navIcons.forEach(icon => {
            icon.classList.toggle('bg-green-500', icon.dataset.view === viewId);
            icon.classList.toggle('text-gray-900', icon.dataset.view === viewId);
            icon.classList.toggle('text-gray-400', icon.dataset.view !== viewId);
            icon.classList.toggle('hover:bg-gray-700', icon.dataset.view !== viewId);
        });
        renderAll();
    };
    
    const assignKeycode = (keycode) => {
        const { type, index } = state.selectedControl;
        if (!type) { alert("Please select a macro key or encoder action first."); return; }
        const currentLayerData = state.layers[state.currentLayer];
        if (type === 'key') currentLayerData.keymap[index].k = keycode;
        else if (type === 'encoder') currentLayerData.encoder[index].k = keycode;
        state.selectedControl = { type: null, index: null };
        renderAll();
        updateProfileOnBackend();
    };

    // --- MACRO LOGIC ---
    newMacroBtn.onclick = () => {
        newMacroNameInput.value = `Macro ${state.macros.length + 1}`;
        newMacroModal.classList.add('active');
        newMacroNameInput.focus();
        newMacroNameInput.select();
    };
    newMacroCancelBtn.onclick = () => newMacroModal.classList.remove('active');
    newMacroSaveBtn.onclick = () => {
        const name = newMacroNameInput.value.trim();
        if (name) {
            state.macros.push({ name, actions: [] });
            state.selectedMacro = state.macros.length - 1;
            newMacroModal.classList.remove('active');
            renderAll();
            updateProfileOnBackend();
        }
    };
    const deleteMacro = (index) => {
        if (confirm(`Are you sure you want to delete "${state.macros[index].name}"?`)) {
            state.macros.splice(index, 1);
            if (state.selectedMacro === index) state.selectedMacro = null;
            else if (state.selectedMacro > index) state.selectedMacro--;
            renderAll();
            updateProfileOnBackend();
        }
    };
    addActionBtn.onclick = () => {
        state.newAction = { type: 'keystroke', value: null };
        renderActionConfig('keystroke');
        actionTypeSelector.querySelector('.selected')?.classList.remove('selected');
        actionTypeSelector.querySelector('[data-action="keystroke"]').classList.add('selected');
        addActionModal.classList.add('active');
    };
    addActionCancelBtn.onclick = () => addActionModal.classList.remove('active');
    actionTypeSelector.onclick = (e) => {
        const btn = e.target.closest('.action-type-btn');
        if (btn) {
            const type = btn.dataset.action;
            state.newAction.type = type;
            actionTypeSelector.querySelector('.selected')?.classList.remove('selected');
            btn.classList.add('selected');
            renderActionConfig(type);
        }
    };
    addActionSaveBtn.onclick = () => {
        const input = document.getElementById('action-value-input');
        if (state.newAction.type === 'keystroke') {
            if (!state.newAction.value) { alert("Please press a key first."); return; }
        } else {
            if (!input.value) { alert("Please enter a value for the action."); return; }
            state.newAction.value = input.value;
        }
        state.macros[state.selectedMacro].actions.push({...state.newAction});
        addActionModal.classList.remove('active');
        renderAll();
        updateProfileOnBackend();
    };
    const deleteMacroAction = (actionIndex) => {
        state.macros[state.selectedMacro].actions.splice(actionIndex, 1);
        renderAll();
        updateProfileOnBackend();
    };
    const handleKeystrokeInput = (e) => {
        e.preventDefault();
        const displayKey = KEY_MAP[e.key] || e.key.toUpperCase();
        e.target.value = displayKey;
        state.newAction.value = displayKey;
    };

    // --- MACRO RECORDING LOGIC ---
    const handleMacroRecordKey = (e) => {
        if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
        e.preventDefault();
        const displayKey = KEY_MAP[e.key] || e.key.toUpperCase();
        state.macros[state.selectedMacro].actions.push({ type: 'keystroke', value: displayKey });
        renderMacroEditor();
        updateProfileOnBackend();
    };
    const toggleRecording = () => {
        state.isRecording = !state.isRecording;
        if (state.isRecording) window.addEventListener('keydown', handleMacroRecordKey, true);
        else window.removeEventListener('keydown', handleMacroRecordKey, true);
        renderMacroEditor();
    };
    recordMacroBtn.addEventListener('click', toggleRecording);

    // --- DRAG AND DROP LOGIC ---
    macroActionListContainer.addEventListener('dragstart', e => {
        if (e.target.classList.contains('draggable-action')) {
            e.target.classList.add('dragging');
            state.draggedAction.fromIndex = parseInt(e.target.dataset.actionIndex);
        }
    });
    macroActionListContainer.addEventListener('dragend', e => {
        if (e.target.classList.contains('dragging')) e.target.classList.remove('dragging');
        const indicator = macroActionListContainer.querySelector('.drop-indicator');
        if (indicator) indicator.remove();
    });
    macroActionListContainer.addEventListener('dragover', e => {
        e.preventDefault();
        const indicator = macroActionListContainer.querySelector('.drop-indicator') || document.createElement('div');
        indicator.className = 'drop-indicator';
        const afterElement = getDragAfterElement(macroActionListContainer, e.clientY);
        if (afterElement == null) macroActionListContainer.appendChild(indicator);
        else macroActionListContainer.insertBefore(indicator, afterElement);
    });
    macroActionListContainer.addEventListener('drop', e => {
        e.preventDefault();
        const indicator = macroActionListContainer.querySelector('.drop-indicator');
        if (!indicator) return;
        const children = [...macroActionListContainer.querySelectorAll('.draggable-action, .drop-indicator')];
        const toIndex = children.indexOf(indicator);
        const fromIndex = state.draggedAction.fromIndex;
        if (fromIndex === null) return;
        const [removed] = state.macros[state.selectedMacro].actions.splice(fromIndex, 1);
        state.macros[state.selectedMacro].actions.splice(toIndex, 0, removed);
        indicator.remove();
        state.draggedAction.fromIndex = null;
        renderAll();
        updateProfileOnBackend();
    });

    function getDragAfterElement(container, y) {
        const draggableElements = [...container.querySelectorAll('.draggable-action:not(.dragging)')];
        return draggableElements.reduce((closest, child) => {
            const box = child.getBoundingClientRect();
            const offset = y - box.top - box.height / 2;
            if (offset < 0 && offset > closest.offset) return { offset: offset, element: child };
            else return closest;
        }, { offset: Number.NEGATIVE_INFINITY }).element;
    }
    
    // --- SETTINGS LOGIC ---
    exportBtn.addEventListener('click', () => {
        const profileData = {
            layers: state.layers,
            macros: state.macros,
            keycodes: state.keycodes
        };
        const dataStr = "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(profileData, null, 2));
        const downloadAnchorNode = document.createElement('a');
        downloadAnchorNode.setAttribute("href", dataStr);
        downloadAnchorNode.setAttribute("download", "Nomi_Profile.json");
        document.body.appendChild(downloadAnchorNode);
        downloadAnchorNode.click();
        downloadAnchorNode.remove();
    });
    importBtn.addEventListener('click', () => {
        importFileInput.click();
    });
    importFileInput.addEventListener('change', (e) => {
        const file = e.target.files[0];
        if (!file) return;

        if (!confirm("Importing a profile will overwrite all current settings. Are you sure you want to continue?")) {
            e.target.value = '';
            return;
        }

        const reader = new FileReader();
        reader.onload = (event) => {
            try {
                const importedState = JSON.parse(event.target.result);
                if (Array.isArray(importedState.layers) && Array.isArray(importedState.macros)) {
                    state.layers = importedState.layers;
                    state.macros = importedState.macros;
                    state.keycodes = importedState.keycodes || DEFAULT_KEYCODES;
                    state.currentLayer = 0;
                    state.selectedMacro = null;
                    alert("Profile imported successfully!");
                    renderAll();
                    updateProfileOnBackend();
                } else {
                    throw new Error("Invalid profile format.");
                }
            } catch (error) {
                alert("Error: Could not import profile. The file may be corrupt or in the wrong format.");
                console.error("Import error:", error);
            } finally {
                e.target.value = '';
            }
        };
        reader.readAsText(file);
    });
    
    // --- LIGHTING LOGIC ---
    const selectLightingTarget = (type, index) => {
        state.selectedLightingTarget = { type, index };
        renderLightingView();
    };
    
    lightingColorPicker.addEventListener('input', (e) => {
        const { type, index } = state.selectedLightingTarget;
        if (type && index !== null) {
            state.layers[index].ledColor = e.target.value;
            const diodes = lightingVisualizer.querySelectorAll('.led-diode');
            diodes.forEach(diode => diode.style.fill = e.target.value);
            updateProfileOnBackend();
        }
    });

    // --- GLOBAL LISTENERS ---
    navIcons.forEach(icon => icon.addEventListener('click', (e) => { e.preventDefault(); switchView(icon.dataset.view); }));
    connectBtn.addEventListener('click', () => socket.emit('scan_ports'));
    configureBtn.addEventListener('click', () => {
        if (state.connected) switchView('configure');
        else alert("Please connect a device first.");
    });
    linkAppSaveBtn.addEventListener('click', saveLinkedApp);
    linkAppCancelBtn.addEventListener('click', closeLinkAppModal);
    
    linkAppRefreshBtn.addEventListener('click', () => socket.emit('get_running_apps'));
    linkAppSelect.addEventListener('change', () => {
        if (linkAppSelect.value) {
            linkAppInput.value = linkAppSelect.value;
        }
    });
    
    window.addEventListener('click', (e) => {
        if (!connectBtn.contains(e.target) && !portDropdown.contains(e.target)) {
            portDropdown.classList.add('hidden');
        }
        if (state.contextMenu.active && !contextMenu.contains(e.target)) {
            closeContextMenu();
        }
    });
    window.addEventListener('contextmenu', (e) => {
        if (!e.target.closest('.layer-tab')) {
            closeContextMenu();
        }
    });

    // --- INITIALIZATION ---
    switchView('home');
});
</script>

</body>
</ht