Browse Source

scheme update

spacexerq 3 weeks ago
parent
commit
5da408843f
3 changed files with 438 additions and 93 deletions
  1. 172 71
      apps/gui/src/app_window.py
  2. 5 22
      apps/gui/src/gui/scheme_panel.py
  3. 261 0
      apps/gui/src/theme.py

+ 172 - 71
apps/gui/src/app_window.py

@@ -10,7 +10,6 @@ from __future__ import annotations
 import os
 
 from PySide6.QtCore import Qt, QSize, QTimer
-from src import i18n
 from PySide6.QtWidgets import (
     QApplication,
     QButtonGroup,
@@ -25,6 +24,7 @@ from PySide6.QtWidgets import (
     QWidget,
 )
 
+from src import i18n, theme
 from src.tabs.fid_tab import FidTab
 from src.tabs.scanner_tab import ScannerTab
 from src.tabs.scanning_tab import ScanningTab
@@ -39,33 +39,60 @@ _TAB_NAV_KEYS = [
     "tab_nav_fid",
 ]
 
-_SPEC_TAB_IDX = 3  # index of the Spectroscopy tab
-
-_NAV_BG = "#0f0f1e"
+_SPEC_TAB_IDX = 3
 _NAV_H = 38
-_TAB_BTN_CSS = """
-QPushButton {
+
+
+# -- dynamic CSS helpers -------------------------------------------------------
+
+def _nav_bg(dark: bool) -> str:
+    return "#0f0f1e" if dark else "#f0f0f8"
+
+
+def _nav_toolbar_css(dark: bool) -> str:
+    bg     = _nav_bg(dark)
+    border = "#1e1e38" if dark else "#d0d0e8"
+    return (
+        f"QToolBar {{ background: {bg}; border: none; "
+        f"border-bottom: 1px solid {border}; spacing: 0px; padding: 0px; }}"
+    )
+
+
+def _tab_btn_css(dark: bool) -> str:
+    inactive  = "#7777aa" if dark else "#666688"
+    active    = "#ffffff" if dark else "#1a1a2e"
+    accent    = "#f0c040" if dark else "#c09000"
+    hover_bg  = "#17172e" if dark else "#e0e0f4"
+    hover_txt = "#aaaacc" if dark else "#333355"
+    return f"""
+QPushButton {{
     background: transparent;
-    color: #7777aa;
+    color: {inactive};
     border: none;
     border-bottom: 2px solid transparent;
     padding: 0px 20px;
     font-size: 12px;
     min-height: 36px;
-}
-QPushButton:checked {
-    color: #ffffff;
-    border-bottom: 2px solid #f0c040;
-}
-QPushButton:hover:!checked {
-    color: #aaaacc;
-    background: #17172e;
-}
+}}
+QPushButton:checked {{
+    color: {active};
+    border-bottom: 2px solid {accent};
+}}
+QPushButton:hover:!checked {{
+    color: {hover_txt};
+    background: {hover_bg};
+}}
 """
-_LANG_BTN_CSS = f"""
+
+
+def _lang_btn_css(dark: bool) -> str:
+    inactive = "#555577" if dark else "#666688"
+    active   = "#f0c040" if dark else "#c09000"
+    hover    = "#aaaacc" if dark else "#1a1a2e"
+    return f"""
 QPushButton {{
     background: transparent;
-    color: #555577;
+    color: {inactive};
     border: 1px solid transparent;
     border-radius: 3px;
     padding: 0px 7px;
@@ -75,26 +102,21 @@ QPushButton {{
     max-height: 22px;
 }}
 QPushButton:checked {{
-    color: #f0c040;
-    border-color: #f0c040;
+    color: {active};
+    border-color: {active};
 }}
 QPushButton:hover:!checked {{
-    color: #aaaacc;
+    color: {hover};
 }}
 """
 
-_NAV_TOOLBAR_CSS = f"""
-QToolBar {{
-    background: {_NAV_BG};
-    border: none;
-    border-bottom: 1px solid #1e1e38;
-    spacing: 0px;
-    padding: 0px;
-}}
-"""
 
-_SPEC_BTN_BLINK_CSS = """
-QPushButton {
+def _spec_btn_blink_css(dark: bool) -> str:
+    active_txt = "#ffffff" if dark else "#1a1a2e"
+    accent     = "#f0c040" if dark else "#c09000"
+    hover_bg   = "#17172e" if dark else "#e0e0f4"
+    return f"""
+QPushButton {{
     background: transparent;
     color: #e65100;
     border: none;
@@ -102,15 +124,15 @@ QPushButton {
     padding: 0px 20px;
     font-size: 12px;
     min-height: 36px;
-}
-QPushButton:checked {
-    color: #ffffff;
-    border-bottom: 2px solid #f0c040;
-}
-QPushButton:hover:!checked {
+}}
+QPushButton:checked {{
+    color: {active_txt};
+    border-bottom: 2px solid {accent};
+}}
+QPushButton:hover:!checked {{
     color: #ff7733;
-    background: #17172e;
-}
+    background: {hover_bg};
+}}
 """
 
 
@@ -155,11 +177,11 @@ class LFMRIWindow(QMainWindow):
         self._tabs = QTabWidget()
         self._tabs.tabBar().hide()
         self._tabs.setDocumentMode(True)
-        self._tabs.addTab(self._scanning_tab,      i18n.tr(_TAB_NAV_KEYS[0]))
-        self._tabs.addTab(self._seq_tab,           i18n.tr(_TAB_NAV_KEYS[1]))
-        self._tabs.addTab(self._scanner_tab,        i18n.tr(_TAB_NAV_KEYS[2]))
-        self._tabs.addTab(self._spectroscopy_tab,  i18n.tr(_TAB_NAV_KEYS[3]))
-        self._tabs.addTab(self._fid_tab,           i18n.tr(_TAB_NAV_KEYS[4]))
+        self._tabs.addTab(self._scanning_tab,     i18n.tr(_TAB_NAV_KEYS[0]))
+        self._tabs.addTab(self._seq_tab,          i18n.tr(_TAB_NAV_KEYS[1]))
+        self._tabs.addTab(self._scanner_tab,      i18n.tr(_TAB_NAV_KEYS[2]))
+        self._tabs.addTab(self._spectroscopy_tab, i18n.tr(_TAB_NAV_KEYS[3]))
+        self._tabs.addTab(self._fid_tab,          i18n.tr(_TAB_NAV_KEYS[4]))
         self._tabs.currentChanged.connect(self._on_tab_changed)
         self.setCentralWidget(self._tabs)
 
@@ -179,27 +201,30 @@ class LFMRIWindow(QMainWindow):
         self._build_status_bar()
         self._size_and_center()
 
+        # Apply default dark theme to the whole application
+        self._apply_theme(theme.is_dark())
+
         if seq_file and os.path.isfile(seq_file):
             self._seq_tab.load_seq_file(os.path.abspath(seq_file))
 
+    # ------------------------------------------------------------------ #
+    #  Nav bar                                                             #
+    # ------------------------------------------------------------------ #
+
     def _build_nav_bar(self) -> None:
-        tb = QToolBar("Navigation", self)
+        self._nav_toolbar = QToolBar("Navigation", self)
+        tb = self._nav_toolbar
         tb.setMovable(False)
         tb.setFloatable(False)
         tb.setIconSize(QSize(0, 0))
-        tb.setStyleSheet(_NAV_TOOLBAR_CSS)
         tb.setFixedHeight(_NAV_H)
         self.addToolBar(Qt.TopToolBarArea, tb)
 
-        lbl = QLabel("  LF-MRI  ")
-        lbl.setStyleSheet(
-            f"color: #444466; font-weight: bold; font-size: 11px; "
-            f"background: {_NAV_BG}; padding: 0 4px;"
-        )
-        tb.addWidget(lbl)
+        self._nav_logo_lbl = QLabel("  LF-MRI  ")
+        tb.addWidget(self._nav_logo_lbl)
 
-        sep = _VSep(tb)
-        tb.addWidget(sep)
+        self._nav_sep1 = _VSep(tb)
+        tb.addWidget(self._nav_sep1)
 
         self._nav_btn_group = QButtonGroup(self)
         self._nav_btn_group.setExclusive(True)
@@ -208,7 +233,6 @@ class LFMRIWindow(QMainWindow):
         for i, key in enumerate(_TAB_NAV_KEYS):
             btn = QPushButton(i18n.tr(key))
             btn.setCheckable(True)
-            btn.setStyleSheet(_TAB_BTN_CSS)
             btn.setFixedHeight(_NAV_H)
             btn.setCursor(Qt.PointingHandCursor)
             self._nav_btn_group.addButton(btn, i)
@@ -218,10 +242,9 @@ class LFMRIWindow(QMainWindow):
 
         self._nav_tab_buttons[0].setChecked(True)
 
-        spacer = QWidget()
-        spacer.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred)
-        spacer.setStyleSheet(f"background: {_NAV_BG};")
-        tb.addWidget(spacer)
+        self._nav_spacer = QWidget()
+        self._nav_spacer.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred)
+        tb.addWidget(self._nav_spacer)
 
         # Language toggle (EN / RU)
         self._lang_btn_group = QButtonGroup(self)
@@ -229,11 +252,9 @@ class LFMRIWindow(QMainWindow):
         self._btn_lang_en = QPushButton("EN")
         self._btn_lang_en.setCheckable(True)
         self._btn_lang_en.setChecked(True)
-        self._btn_lang_en.setStyleSheet(_LANG_BTN_CSS)
         self._btn_lang_en.setCursor(Qt.PointingHandCursor)
         self._btn_lang_ru = QPushButton("RU")
         self._btn_lang_ru.setCheckable(True)
-        self._btn_lang_ru.setStyleSheet(_LANG_BTN_CSS)
         self._btn_lang_ru.setCursor(Qt.PointingHandCursor)
         self._lang_btn_group.addButton(self._btn_lang_en)
         self._lang_btn_group.addButton(self._btn_lang_ru)
@@ -242,17 +263,84 @@ class LFMRIWindow(QMainWindow):
         tb.addWidget(self._btn_lang_en)
         tb.addWidget(self._btn_lang_ru)
 
+        # Theme toggle (☽ dark / ☀ light)
+        self._nav_sep2 = _VSep(tb)
+        tb.addWidget(self._nav_sep2)
+
+        self._theme_btn_group = QButtonGroup(self)
+        self._theme_btn_group.setExclusive(True)
+        self._btn_theme_dark  = QPushButton("☽")
+        self._btn_theme_light = QPushButton("☀")
+        self._btn_theme_dark.setCheckable(True)
+        self._btn_theme_light.setCheckable(True)
+        self._btn_theme_dark.setChecked(True)  # default dark
+        self._btn_theme_dark.setToolTip("Dark theme")
+        self._btn_theme_light.setToolTip("Light theme")
+        self._btn_theme_dark.setCursor(Qt.PointingHandCursor)
+        self._btn_theme_light.setCursor(Qt.PointingHandCursor)
+        self._theme_btn_group.addButton(self._btn_theme_dark)
+        self._theme_btn_group.addButton(self._btn_theme_light)
+        self._btn_theme_dark.clicked.connect(lambda: self._on_theme_toggle(True))
+        self._btn_theme_light.clicked.connect(lambda: self._on_theme_toggle(False))
+        tb.addWidget(self._btn_theme_dark)
+        tb.addWidget(self._btn_theme_light)
+
+    # ------------------------------------------------------------------ #
+    #  Theme                                                               #
+    # ------------------------------------------------------------------ #
+
+    def _on_theme_toggle(self, dark: bool) -> None:
+        theme.set_dark(dark)
+        self._apply_theme(dark)
+
+    def _apply_theme(self, dark: bool) -> None:
+        app = QApplication.instance()
+        if app is not None:
+            app.setStyleSheet(theme.make_app_stylesheet())
+
+        bg       = _nav_bg(dark)
+        border   = "#1e1e38" if dark else "#d0d0e8"
+        sep_clr  = "#1e1e38" if dark else "#d0d0e8"
+        logo_clr = "#444466" if dark else "#555577"
+        tab_css  = _tab_btn_css(dark)
+        lang_css = _lang_btn_css(dark)
+
+        self._nav_toolbar.setStyleSheet(_nav_toolbar_css(dark))
+        self._nav_sep1.setStyleSheet(f"background: {sep_clr}; border: none;")
+        self._nav_sep2.setStyleSheet(f"background: {sep_clr}; border: none;")
+        self._nav_logo_lbl.setStyleSheet(
+            f"color: {logo_clr}; font-weight: bold; font-size: 11px; "
+            f"background: {bg}; padding: 0 4px;"
+        )
+        self._nav_spacer.setStyleSheet(f"background: {bg};")
+
+        for btn in self._nav_tab_buttons:
+            btn.setStyleSheet(tab_css)
+
+        self._btn_lang_en.setStyleSheet(lang_css)
+        self._btn_lang_ru.setStyleSheet(lang_css)
+        self._btn_theme_dark.setStyleSheet(lang_css)
+        self._btn_theme_light.setStyleSheet(lang_css)
+
+        sb_bg    = "#0c0c1a" if dark else "#e4e4f0"
+        sb_color = "#555577" if dark else "#666688"
+        self.statusBar().setStyleSheet(
+            f"QStatusBar {{ background: {sb_bg}; color: {sb_color}; font-size: 11px; }}"
+        )
+
+    # ------------------------------------------------------------------ #
+    #  Navigation                                                          #
+    # ------------------------------------------------------------------ #
+
     def _switch_tab(self, index: int) -> None:
         self._tabs.setCurrentIndex(index)
         self._nav_tab_buttons[index].setChecked(True)
 
     def _build_status_bar(self) -> None:
-        sb = QStatusBar()
-        sb.setStyleSheet(
-            "QStatusBar { background: #0c0c1a; color: #555577; font-size: 11px; }"
+        self.setStatusBar(QStatusBar())
+        self.statusBar().showMessage(
+            f"{i18n.tr('active_tab')}: {i18n.tr(_TAB_NAV_KEYS[0])}"
         )
-        self.setStatusBar(sb)
-        sb.showMessage(f"{i18n.tr('active_tab')}: {i18n.tr(_TAB_NAV_KEYS[0])}")
 
     def _size_and_center(self) -> None:
         screen = QApplication.primaryScreen()
@@ -262,7 +350,7 @@ class LFMRIWindow(QMainWindow):
             h = min(940, max(640, int(ag.height() * 0.90)))
             self.resize(w, h)
             self.move(
-                ag.x() + (ag.width() - w) // 2,
+                ag.x() + (ag.width()  - w) // 2,
                 ag.y() + (ag.height() - h) // 2,
             )
         else:
@@ -296,6 +384,10 @@ class LFMRIWindow(QMainWindow):
             if hasattr(tab, "retranslate_ui"):
                 tab.retranslate_ui()
 
+    # ------------------------------------------------------------------ #
+    #  Cross-tab signals                                                   #
+    # ------------------------------------------------------------------ #
+
     def _on_fid_generated(self, path: str) -> None:
         self._seq_tab.load_seq_file(path)
         self._switch_tab(1)
@@ -307,19 +399,28 @@ class LFMRIWindow(QMainWindow):
 
     def _on_scan_raw_data_ready(self, json_path: str) -> None:
         self._spectroscopy_tab.receive_scan_data(json_path)
-        # Blink the Spectroscopy nav button until the user opens the tab
         if self._tabs.currentIndex() != _SPEC_TAB_IDX:
             self._spec_blink_on = False
             self._spec_blink_timer.start()
 
+    # ------------------------------------------------------------------ #
+    #  Spectroscopy blink                                                  #
+    # ------------------------------------------------------------------ #
+
     def _tick_spec_blink(self) -> None:
         self._spec_blink_on = not self._spec_blink_on
         btn = self._nav_tab_buttons[_SPEC_TAB_IDX]
-        btn.setStyleSheet(_SPEC_BTN_BLINK_CSS if self._spec_blink_on else _TAB_BTN_CSS)
+        btn.setStyleSheet(
+            _spec_btn_blink_css(theme.is_dark())
+            if self._spec_blink_on
+            else _tab_btn_css(theme.is_dark())
+        )
 
     def _stop_spec_blink(self) -> None:
         self._spec_blink_timer.stop()
-        self._nav_tab_buttons[_SPEC_TAB_IDX].setStyleSheet(_TAB_BTN_CSS)
+        self._nav_tab_buttons[_SPEC_TAB_IDX].setStyleSheet(
+            _tab_btn_css(theme.is_dark())
+        )
 
 
 class _VSep(QFrame):

+ 5 - 22
apps/gui/src/gui/scheme_panel.py

@@ -8,38 +8,21 @@ Click or hover to inspect; emits blockClicked(sync_index).
 """
 from __future__ import annotations
 
-import sys
 from typing import List, Optional
 
 from PySide6.QtCore import Signal, Qt, QRect
 from PySide6.QtGui import (
-    QPainter, QColor, QPen, QFont, QFontMetrics, QPalette,
+    QPainter, QColor, QPen, QFont, QFontMetrics,
 )
-from PySide6.QtWidgets import QWidget, QToolTip, QSizePolicy, QApplication
+from PySide6.QtWidgets import QWidget, QToolTip, QSizePolicy
 
 from src.gui.adapters import BlockRow
 
 
 def system_is_dark() -> bool:
-    """Return True when the OS is running in dark mode.
-
-    On Windows the registry is authoritative - Qt's widget-area palette may
-    stay light even when the title bar is dark (Windows styles them separately).
-    Other platforms fall back to QPalette.Window lightness.
-    """
-    if sys.platform == "win32":
-        try:
-            import winreg
-            key = winreg.OpenKey(
-                winreg.HKEY_CURRENT_USER,
-                r"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize",
-            )
-            val, _ = winreg.QueryValueEx(key, "AppsUseLightTheme")
-            winreg.CloseKey(key)
-            return val == 0          # 0 = dark, 1 = light
-        except Exception:
-            pass
-    return QApplication.palette().color(QPalette.Window).lightness() < 128
+    """Return True when the app is running in dark mode."""
+    from src import theme as _theme
+    return _theme.is_dark()
 
 # -- block colour map (semantic, theme-neutral vivid hues) --------------------
 

+ 261 - 0
apps/gui/src/theme.py

@@ -0,0 +1,261 @@
+"""Application-level dark/light theme management.
+
+Provides an explicit toggle independent of the host OS setting.
+Default is dark regardless of OS theme.
+"""
+from __future__ import annotations
+from typing import Callable
+
+_dark: bool = True
+_listeners: list[Callable[[], None]] = []
+
+
+def is_dark() -> bool:
+    return _dark
+
+
+def set_dark(dark: bool) -> None:
+    global _dark
+    if _dark == dark:
+        return
+    _dark = dark
+    for cb in list(_listeners):
+        try:
+            cb()
+        except Exception:
+            pass
+
+
+def add_listener(cb: Callable[[], None]) -> None:
+    if cb not in _listeners:
+        _listeners.append(cb)
+
+
+def remove_listener(cb: Callable[[], None]) -> None:
+    try:
+        _listeners.remove(cb)
+    except ValueError:
+        pass
+
+
+def palette() -> dict[str, str]:
+    if _dark:
+        return {
+            "bg_deep":          "#0f0f1e",
+            "bg":               "#1a1a2e",
+            "panel":            "#2a2a2a",
+            "surface":          "#16162a",
+            "border":           "#1e1e38",
+            "border2":          "#333355",
+            "accent":           "#f0c040",
+            "text":             "#ddddff",
+            "text_dim":         "#7777aa",
+            "text_muted":       "#555577",
+            "btn_bg":           "#252535",
+            "input_bg":         "#1e1e38",
+            "scrollbar_bg":     "#1a1a2e",
+            "scrollbar_handle": "#3a3a5a",
+        }
+    return {
+        "bg_deep":          "#e4e4f0",
+        "bg":               "#f0f0f8",
+        "panel":            "#ffffff",
+        "surface":          "#f5f5fc",
+        "border":           "#d0d0e0",
+        "border2":          "#aaaacc",
+        "accent":           "#c09000",
+        "text":             "#1a1a2e",
+        "text_dim":         "#444466",
+        "text_muted":       "#777799",
+        "btn_bg":           "#e8e8f4",
+        "input_bg":         "#ffffff",
+        "scrollbar_bg":     "#e0e0ef",
+        "scrollbar_handle": "#aaaacc",
+    }
+
+
+def make_app_stylesheet() -> str:
+    p = palette()
+    return f"""
+QWidget {{
+    background-color: {p['bg']};
+    color: {p['text']};
+    selection-background-color: {p['accent']};
+    selection-color: #000000;
+}}
+QMainWindow {{
+    background-color: {p['bg_deep']};
+}}
+QDialog {{
+    background-color: {p['bg']};
+}}
+QLabel {{
+    background-color: transparent;
+    color: {p['text']};
+}}
+QGroupBox {{
+    background-color: {p['panel']};
+    border: 1px solid {p['border2']};
+    border-radius: 4px;
+    margin-top: 8px;
+    padding-top: 4px;
+    color: {p['text']};
+    font-weight: bold;
+}}
+QGroupBox::title {{
+    subcontrol-origin: margin;
+    left: 8px;
+    padding: 0 3px;
+    color: {p['text_dim']};
+    font-weight: normal;
+}}
+QPushButton {{
+    background-color: {p['btn_bg']};
+    color: {p['text']};
+    border: 1px solid {p['border2']};
+    border-radius: 3px;
+    padding: 3px 10px;
+    min-height: 22px;
+}}
+QPushButton:hover {{
+    border-color: {p['accent']};
+}}
+QPushButton:pressed {{
+    background-color: {p['bg_deep']};
+}}
+QPushButton:disabled {{
+    color: {p['text_muted']};
+    border-color: {p['border']};
+    background-color: {p['surface']};
+}}
+QComboBox {{
+    background-color: {p['input_bg']};
+    color: {p['text']};
+    border: 1px solid {p['border2']};
+    border-radius: 3px;
+    padding: 2px 24px 2px 6px;
+    min-height: 22px;
+}}
+QComboBox:hover {{ border-color: {p['accent']}; }}
+QComboBox::drop-down {{ border: none; width: 18px; }}
+QComboBox QAbstractItemView {{
+    background-color: {p['panel']};
+    color: {p['text']};
+    border: 1px solid {p['border2']};
+    selection-background-color: {p['accent']};
+    selection-color: #000000;
+}}
+QLineEdit, QTextEdit, QPlainTextEdit {{
+    background-color: {p['input_bg']};
+    color: {p['text']};
+    border: 1px solid {p['border2']};
+    border-radius: 3px;
+    padding: 2px 4px;
+}}
+QLineEdit:hover, QTextEdit:hover {{ border-color: {p['accent']}; }}
+QSpinBox, QDoubleSpinBox {{
+    background-color: {p['input_bg']};
+    color: {p['text']};
+    border: 1px solid {p['border2']};
+    border-radius: 3px;
+    padding: 2px 4px;
+}}
+QTableWidget, QTableView {{
+    background-color: {p['panel']};
+    color: {p['text']};
+    gridline-color: {p['border']};
+    border: 1px solid {p['border2']};
+    alternate-background-color: {p['surface']};
+}}
+QTableWidget QHeaderView::section, QTableView QHeaderView::section {{
+    background-color: {p['surface']};
+    color: {p['text_dim']};
+    border: 1px solid {p['border']};
+    padding: 3px 6px;
+    font-weight: bold;
+}}
+QListWidget {{
+    background-color: {p['panel']};
+    color: {p['text']};
+    border: 1px solid {p['border2']};
+}}
+QListWidget::item:selected {{
+    background-color: {p['accent']};
+    color: #000000;
+}}
+QScrollBar:vertical {{
+    background: {p['scrollbar_bg']};
+    width: 8px;
+    border: none;
+    margin: 0;
+}}
+QScrollBar::handle:vertical {{
+    background: {p['scrollbar_handle']};
+    border-radius: 4px;
+    min-height: 20px;
+}}
+QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical {{ height: 0; border: none; }}
+QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical {{ background: none; }}
+QScrollBar:horizontal {{
+    background: {p['scrollbar_bg']};
+    height: 8px;
+    border: none;
+    margin: 0;
+}}
+QScrollBar::handle:horizontal {{
+    background: {p['scrollbar_handle']};
+    border-radius: 4px;
+    min-width: 20px;
+}}
+QScrollBar::add-line:horizontal, QScrollBar::sub-line:horizontal {{ width: 0; border: none; }}
+QScrollBar::add-page:horizontal, QScrollBar::sub-page:horizontal {{ background: none; }}
+QSplitter::handle {{ background: {p['border']}; }}
+QStatusBar {{
+    background: {p['bg_deep']};
+    color: {p['text_muted']};
+    font-size: 11px;
+}}
+QTabWidget::pane {{
+    border: 1px solid {p['border2']};
+    background: {p['bg']};
+}}
+QTabBar::tab {{
+    background: {p['surface']};
+    color: {p['text_dim']};
+    border: 1px solid {p['border']};
+    border-bottom: none;
+    padding: 4px 12px;
+    margin-right: 2px;
+    border-top-left-radius: 3px;
+    border-top-right-radius: 3px;
+}}
+QTabBar::tab:selected {{
+    background: {p['bg']};
+    color: {p['text']};
+    border-bottom: 2px solid {p['accent']};
+}}
+QTabBar::tab:hover:!selected {{ background: {p['panel']}; }}
+QCheckBox {{ color: {p['text']}; spacing: 6px; }}
+QCheckBox::indicator {{
+    width: 14px; height: 14px;
+    border: 1px solid {p['border2']};
+    border-radius: 2px;
+    background: {p['input_bg']};
+}}
+QCheckBox::indicator:checked {{ background: {p['accent']}; border-color: {p['accent']}; }}
+QProgressBar {{
+    background-color: {p['input_bg']};
+    color: {p['text']};
+    border: 1px solid {p['border2']};
+    border-radius: 3px;
+    text-align: center;
+}}
+QProgressBar::chunk {{ background-color: {p['accent']}; border-radius: 2px; }}
+QToolTip {{
+    background-color: {p['panel']};
+    color: {p['text']};
+    border: 1px solid {p['border2']};
+    padding: 2px 6px;
+}}
+QScrollArea {{ border: none; background: transparent; }}
+"""