import json
import math
import hashlib
import os
import re
import subprocess
import sys
import threading
import time
import uuid
import tkinter as tk
import zipfile
import shutil
from pathlib import Path
from datetime import date, datetime, timedelta
from tkinter import filedialog, messagebox, ttk
import tkinter.font as tkfont

from core.notify import desktop_notify
from PIL import Image, ImageTk

from core.constants import APP_VERSION, BASE_DIR, DATA_DIR, COLORS, LANG_SHORT, LANG_DISP, LANG_FULL, LANG_REV, THEME_PRESETS, get_theme_colors
from core.data_integrity import checkpoint_health_report, checkpoint_restore_dry_run, create_preflight_data_checkpoint, ensure_preflight_data_checkpoint, data_integrity_report
from core.i18n import all_lang_values, clean_label, is_placeholder_value, load_all_langs, load_lang
from core.network import NetworkClient, build_network_config, network_readiness_report, network_deployment_plan
from core.preflight_maintenance import build_offline_maintenance_report
from core.responsive import clamp_ui_font_scale, collect_widget_fit_issues, make_layout_runtime_report, runtime_layout_bucket
from core.sync_queue import backup_sync_queue, enqueue_sync_event, load_sync_queue, queue_summary, repair_sync_queue, save_sync_queue, sync_queue_integrity_report, sync_cutover_dry_run
from core.system_server_config import apply_system_server_config
from core.storage import BACKUP_DIR, load_action_logs, load_attendance, load_chat, load_crm, load_data, load_enterprise, load_reports, load_settings, save_action_logs, save_documents, save_notes, save_settings, save_todos, save_crm, save_enterprise, save_chat, save_reports, save_attendance, save_json
from core.ui import ToolTip, center_window, create_button, create_dialog_window, prompt_text_dialog

APP_USER_MODEL_ID = "NexaOffice.WorkSuite.Desktop." + APP_VERSION.replace(".", "")
from pages.ai_page import AIPage
from pages.calendar_page import CalendarPage
from pages.chat_page import ChatPage
from pages.cloud_page import CloudPage
from pages.home_page import HomePage
from pages.enterprise_page import EnterprisePage
from pages.files_page import FilesPage
from pages.settings_page import SettingsPage


class OfficeAssistantApp:
    def __init__(self, root):
        self.root = root
        self.colors = COLORS
        self.lang_code = "zh_CN"
        self.lang = load_lang(self.lang_code)
        self.all_langs = load_all_langs()
        self.notes, self.todos, self.documents, self.holidays = load_data()
        self.user_settings = load_settings()
        self.user_settings.setdefault("theme", "blue")
        self.user_settings.setdefault("app_mode", "personal")
        self.user_settings.setdefault("font_scale", 1.0)
        self.colors = get_theme_colors(self.user_settings.get("theme", "blue"))
        self.lang_code = self.user_settings.get("language", self.lang_code)
        self.lang = load_lang(self.lang_code)
        self.all_langs = load_all_langs()
        self.enterprise = load_enterprise()
        self.crm = load_crm()
        self._ensure_identity_puids()
        self.chat = load_chat()
        self.reports = load_reports()
        self.attendance = load_attendance()
        self.action_logs = load_action_logs()
        self.user_settings.setdefault("quick_actions", {"focus": True, "calendar": True, "task": True, "note": True, "chat": True, "cloud": True, "enterprise": True, "ai": True})
        self.user_settings.setdefault("quick_action_labels", {"focus": "", "calendar": "", "task": "", "note": "", "chat": "", "cloud": "", "enterprise": "", "ai": ""})
        self.user_settings.setdefault("notification_read_ids", [])
        self.user_settings.setdefault("notification_settings", {"show_tasks": True, "show_calendar": True, "show_holidays": True, "show_enterprise": True})
        self.search_keyword = ""
        self.search_after_id = None
        self.current_page = "home"
        self.more_popup = None
        self.module_fullscreen = False
        self._tray_last_error = ""
        self._tray_retry_count = 0
        self._minimized_without_tray = False
        self.tray_events = []
        self.reminder_sent = set()
        self.user_settings.setdefault("holiday_reminders", {"china": True, "indonesia": True, "days_before": 1})
        self.user_settings.setdefault("accounts", [])
        self.user_settings.setdefault("puid", "")
        self.user_settings.setdefault("personal_puid", "")
        self.user_settings.setdefault("enterprise_puid", "")
        self.user_settings.setdefault("remember_me", False)
        self.user_settings.setdefault("auto_login", False)
        self.user_settings.setdefault("saved_password", "")
        self.user_settings.setdefault("smooth_restore", True)
        self.user_settings.setdefault("lightweight_page_switch", True)
        self.user_settings.setdefault("performance_tier", "auto")  # auto / balanced / low_power
        self.user_settings.setdefault("chat_scope", "personal")
        self.user_settings.setdefault("close_to_tray", True)
        self.user_settings.setdefault("source_mode_close_direct", True)
        self.user_settings.setdefault("source_mode_force_os_exit", True)
        self.user_settings.setdefault("login_background_motion", True)
        self.user_settings.setdefault("login_background_motion_intensity", "vivid")  # calm / balanced / vivid
        self.user_settings.setdefault("v100_login_quality_track", "stable_dynamic")
        self.tray_icon = None
        self.tray_thread = None
        self.tray_available = None
        self._really_exit = False
        self._exit_requested = False
        self._exit_started_at = 0.0
        self._exit_strategy = ""
        self._last_exit_save_status = {}
        self._closing_to_tray = False
        self._tray_notice_shown = False
        # v39: Windows WS_EX_COMPOSITED caused black surfaces on some Tk/Windows builds.
        # Keep it opt-in only; the safer restore guard below handles normal flicker.
        self.user_settings.setdefault("windows_composited_flicker_fix", False)
        self.user_settings.setdefault("restore_overlay_enabled", True)
        self.user_settings.setdefault("restore_full_window_cover", True)
        server_defaults = {
            "enabled": False,
            "offline_mode": True,
            "base_url": "",
            "workspace_id": "",
            "api_token": "",
            "timeout": 5,
            "retry_count": 1,
            "allow_insecure_localhost": True,
            "managed_by_system": True,
            "user_configurable": False,
            "config_source": "not_configured",
            "auto_connect": False,
            "sync_events_enabled": False,
            "deployment_mode": "local",
        }
        server_sync = self.user_settings.get("server_sync")
        if not isinstance(server_sync, dict):
            server_sync = {}
            self.user_settings["server_sync"] = server_sync
        for key, value in server_defaults.items():
            server_sync.setdefault(key, value)
        apply_system_server_config(self.user_settings)
        responsive_defaults = {
            "auto_compact_header": True,
            "auto_compact_sidebar": True,
            "safe_min_window": True,
            "layout_guard": True,
            # v2026.06.20.08: full widget-fit scanning is useful for diagnostics,
            # but doing it automatically after every resize/page switch made the
            # whole app feel heavy on some PCs.  Keep the guard available and
            # switch normal runtime to on-demand.  Set to "auto" only when
            # actively debugging a layout problem.
            "layout_guard_mode": "on_demand",
            # v2026.06.20.09: keep normal UI interactions smooth.  Resize and
            # mouse-wheel work are throttled/cached, while diagnostic checks stay
            # available from the health/diagnostic entry points.
            "resize_min_delta": 24,
            "resize_debounce_ms": 340,
            "scroll_target_cache_ms": 900,
            "idle_layout_after_login_ms": 600,
            "auto_safe_font_scale": True,
            "last_profile": {},
            "last_runtime_fit": {},
        }
        responsive_layout = self.user_settings.get("responsive_layout")
        if not isinstance(responsive_layout, dict):
            responsive_layout = {}
            self.user_settings["responsive_layout"] = responsive_layout
        for key, value in responsive_defaults.items():
            responsive_layout.setdefault(key, value)
        self.layout_profile = {"name": "initial", "width": 0, "height": 0, "sidebar": "normal", "header": "normal"}
        self.sync_queue = load_sync_queue()
        self.network_client = NetworkClient(build_network_config(self.user_settings))

        self.app_version = APP_VERSION
        self.base_dir = BASE_DIR
        self.root.title(self.window_title())
        self._set_window_icon(self.root)
        self._configure_display()
        self.device_profile = self._detect_device_profile()
        self.root.configure(bg=self.colors["bg"])
        self._setup_style()
        self._responsive_after_id = None
        self._restore_job = None
        self._window_was_iconic = False
        self._restore_in_progress = False
        self._restore_suppressed_until = 0.0
        self._last_restore_at = 0.0
        self._last_configure_size = (0, 0)
        self._last_window_state = None
        self._state_watch_job = None
        self._restore_cover = None
        self._restore_cover_job = None
        self._restore_root_cover = None
        self._restore_root_cover_job = None
        self._post_restore_layout_job = None
        self._layout_guard_after_id = None
        self._layout_guard_last_at = 0.0
        self._layout_guard_last_issue_count = 0
        self._runtime_layout_fit_issues = []
        self._last_responsive_signature = None
        self._scroll_widget_cache = []
        self._scroll_widget_cache_at = 0.0
        self._last_page_refresh_at = {}
        self._pending_page_refresh_jobs = {}
        self._home_refresh_job = None
        self._last_action_log_at = {}
        self._page_switch_started_at = {}
        self._active_nav_page = None
        self._network_test_in_progress = False
        self._network_bootstrap_job = None
        self.performance_metrics = {"page_switches": [], "slow_events": []}
        self._flicker_style_applied = False
        self.root.bind("<Unmap>", self._on_root_unmap, add="+")
        self.root.bind("<Map>", self._on_root_map, add="+")
        self.root.bind("<Visibility>", self._on_root_visibility, add="+")
        self.root.protocol("WM_DELETE_WINDOW", self.on_window_close)
        self._install_centered_dialog_defaults()
        try:
            self.root.bind_all("<Control-q>", lambda event: self.source_direct_force_exit("Ctrl+Q") if self.should_force_source_exit() else self.exit_app())
            self.root.bind_all("<Control-Q>", lambda event: self.source_direct_force_exit("Ctrl+Q") if self.should_force_source_exit() else self.exit_app())
        except Exception:
            pass
        self._bind_global_shortcuts()
        self.root.after(350, self._watch_window_state)
        self.root.after(180, self._enable_windows_flicker_reduction)
        self.pending_auto_login = bool(
            self.user_settings.get("auto_login")
            and self.user_settings.get("remember_me")
            and self.user_settings.get("username")
            and self.user_settings.get("saved_password")
        )
        # Always show the front login screen first so users can still switch
        # accounts and log in manually. Remembered credentials remain prefilled.
        self.user_settings["logged_in"] = False
        self._build_login_screen()
        threading.Thread(target=self._check_reminders, daemon=True).start()

    def _install_centered_dialog_defaults(self):
        """Make native message dialogs center on the app window by default."""
        try:
            if getattr(messagebox, "_nexaoffice_centered", False):
                return
            for name in ("showinfo", "showwarning", "showerror", "askyesno", "askokcancel", "askretrycancel"):
                original = getattr(messagebox, name, None)
                if not callable(original):
                    continue
                def wrapper(*args, _orig=original, **kwargs):
                    try:
                        kwargs.setdefault("parent", self.root)
                    except Exception:
                        pass
                    return _orig(*args, **kwargs)
                setattr(messagebox, name, wrapper)
            messagebox._nexaoffice_centered = True
        except Exception:
            pass

    def _record_tray_event(self, event, detail=""):
        try:
            item = {
                "time": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
                "event": str(event),
                "detail": str(detail or ""),
                "tray_icon_running": self.tray_icon is not None,
                "tray_available": self.tray_available,
            }
            self.tray_events.append(item)
            self.tray_events = self.tray_events[-60:]
        except Exception:
            pass

    def _make_tray_image(self):
        try:
            # Use the ICO/highest-quality PNG as tray source. Small 16/32 icons
            # can look blurry on Windows 11 taskbar/tray when DPI scaling is high.
            ico_path = BASE_DIR / "assets" / "nexaoffice.ico"
            if ico_path.exists():
                img = Image.open(ico_path).convert("RGBA")
            else:
                path = BASE_DIR / "assets" / "nexaoffice_icon_256.png"
                if not path.exists():
                    path = BASE_DIR / "assets" / "logo_nexa_mark_256.png"
                img = Image.open(path).convert("RGBA")
            img = img.resize((64, 64), Image.LANCZOS)
            return img
        except Exception:
            return Image.new("RGBA", (64, 64), (37, 99, 235, 255))

    def check_tray_backend(self):
        """Return local tray capability without changing window state."""
        result = {
            "pystray_import": False,
            "backend": "",
            "ok": False,
            "error": "",
        }
        try:
            import pystray
            result["pystray_import"] = True
            backend = str(getattr(pystray, "_backend", "") or getattr(pystray, "backend", "") or "")
            if sys.platform.startswith("win"):
                try:
                    import pystray._win32
                    backend = backend or "pystray._win32"
                    result["win32_backend_import"] = True
                except Exception as backend_exc:
                    result["win32_backend_import"] = False
                    result["backend_error"] = str(backend_exc)
            result["backend"] = backend
            result["ok"] = True
        except Exception as exc:
            result["error"] = str(exc)
        return result

    def _build_tray_menu(self, pystray):
        return pystray.Menu(
            pystray.MenuItem(self.t("tray_show"), lambda icon, item: self.root.after(0, self.show_from_tray), default=True),
            pystray.MenuItem(self.t("tray_exit"), lambda icon, item: self.root.after(0, self.exit_app)),
        )

    def _ensure_tray_icon(self):
        if getattr(self, "_exit_requested", False):
            return False
        if self.tray_icon is not None:
            return True
        try:
            import pystray
            if sys.platform.startswith("win"):
                try:
                    import pystray._win32  # ensure Windows backend is bundled/loaded
                except Exception:
                    pass
            image = self._make_tray_image()
            menu = self._build_tray_menu(pystray)
            icon_ref = pystray.Icon("NexaOffice", image, self.window_title(), menu)
            self.tray_icon = icon_ref
            self.tray_available = True
            self._tray_last_error = ""

            def run_icon():
                try:
                    icon_ref.run()
                except Exception as exc:
                    self.tray_available = False
                    self._tray_last_error = str(exc)
                    if self.tray_icon is icon_ref:
                        self.tray_icon = None

            self.tray_thread = threading.Thread(target=run_icon, daemon=True)
            self.tray_thread.start()
            self._record_tray_event("tray_created")
            try:
                self.root.after(1200, self._verify_tray_health_later)
            except Exception:
                pass
            return True
        except Exception as exc:
            self.tray_available = False
            self.tray_icon = None
            self._tray_last_error = str(exc)
            self._record_tray_event("tray_create_failed", str(exc))
            return False

    def _fallback_minimize_without_tray(self):
        """Last-resort fallback if system tray cannot be created.
        Keep the app alive by minimizing to the taskbar instead of exiting.
        """
        try:
            self._minimized_without_tray = True
            self._closing_to_tray = True
            self._hide_restore_root_cover()
            self._hide_restore_cover()
            self.root.iconify()
            self._record_tray_event("minimized_without_tray")
            try:
                self.set_status("tray_fallback_status")
            except Exception:
                pass
        except Exception:
            self.exit_app()

    def is_source_mode(self):
        """True when running from python source, False in packaged EXE."""
        return not bool(getattr(sys, "frozen", False))

    def effective_close_to_tray(self):
        """Close-to-tray is kept for EXE, while source mode can exit directly for testing."""
        if self.is_source_mode() and bool(self.user_settings.get("source_mode_close_direct", True)):
            return False
        return bool(self.user_settings.get("close_to_tray", True))

    def should_force_source_exit(self):
        """Source testing should close immediately and not leave a Python process behind."""
        if not self.is_source_mode():
            return False
        if not bool(self.user_settings.get("source_mode_close_direct", True)):
            return False
        if str(os.environ.get("NEXAOFFICE_SOURCE_FORCE_EXIT", "")).strip() == "1":
            return True
        return bool(self.user_settings.get("source_mode_force_os_exit", True))

    def on_window_close(self):
        if getattr(self, "_exit_requested", False) or getattr(self, "_really_exit", False):
            self.exit_app()
            return
        if self.effective_close_to_tray():
            self.hide_to_tray()
        else:
            if self.should_force_source_exit():
                self._record_tray_event("source_mode_force_exit", "WM_DELETE_WINDOW")
                self.source_direct_force_exit("WM_DELETE_WINDOW")
                return
            if self.is_source_mode() and bool(self.user_settings.get("source_mode_close_direct", True)):
                self._record_tray_event("source_mode_direct_exit", "WM_DELETE_WINDOW")
            self.exit_app()

    def hide_to_tray(self):
        if getattr(self, "_exit_requested", False):
            return
        try:
            tray_ok = self._ensure_tray_icon()
            if not tray_ok and self._tray_retry_count < 1:
                self._tray_retry_count += 1
                try:
                    self._record_tray_event("tray_retry", str(getattr(self, "_tray_retry_count", 0)))
                    self.root.after(180, self.hide_to_tray)
                except Exception:
                    pass
                return
            if not tray_ok:
                self._record_tray_event("tray_fallback_minimize", getattr(self, "_tray_last_error", ""))
                self._fallback_minimize_without_tray()
                return
            self._tray_retry_count = 0
            self._closing_to_tray = True
            self._minimized_without_tray = False
            self._hide_restore_root_cover()
            self._hide_restore_cover()
            self.root.withdraw()
            self._record_tray_event("hidden_to_tray")
            self._window_was_iconic = False
            self._restore_in_progress = False
            try:
                self.set_status("tray_hidden_status")
            except Exception:
                pass
            if not getattr(self, "_tray_notice_shown", False):
                self._tray_notice_shown = True
                try:
                    desktop_notify(
                        title=self.t("tray_hidden_title"),
                        message=self.t("tray_hidden_message"),
                        timeout=3,
                    )
                except Exception:
                    pass
        except Exception:
            self._fallback_minimize_without_tray()

    def show_from_tray(self):
        if getattr(self, "_exit_requested", False):
            return
        try:
            self._closing_to_tray = False
            self._minimized_without_tray = False
            self._window_was_iconic = False
            self._restore_in_progress = False
            self._minimized_without_tray = False
            self._hide_restore_root_cover()
            self._hide_restore_cover()
            self.root.deiconify()
            try:
                self.root.state("normal")
            except Exception:
                pass
            self.root.lift()
            self.root.focus_force()
            self._reapply_root_icon()
            self.root.after(220, self._reapply_root_icon)
            self._record_tray_event("show_from_tray")
        except Exception:
            pass

    def _save_runtime_state_before_exit(self):
        """Best-effort save of runtime data before real quit/source watchdog."""
        status = {}
        try:
            current_obj = self.pages.get(getattr(self, "current_page", ""))
            if current_obj is not None and hasattr(current_obj, "flush_pending_changes"):
                current_obj.flush_pending_changes("exit_app")
                status["current_page_flush"] = "ok"
        except Exception as exc:
            status["current_page_flush"] = f"error: {exc}"
        tasks = [
            ("settings", lambda: save_settings(self.user_settings)),
            ("notes", lambda: save_notes(self.notes)),
            ("todos", lambda: save_todos(self.todos)),
            ("documents", lambda: save_documents(self.documents)),
            ("enterprise", lambda: save_enterprise(self.enterprise)),
            ("crm", lambda: save_crm(self.crm)),
            ("chat", lambda: save_chat(self.chat)),
            ("reports", lambda: save_reports(self.reports)),
            ("attendance", lambda: save_attendance(self.attendance)),
            ("action_logs", lambda: save_action_logs(self.action_logs)),
        ]
        for name, fn in tasks:
            try:
                fn()
                status[name] = "ok"
            except Exception as exc:
                status[name] = f"error: {exc}"
        self._last_exit_save_status = status
        self._record_tray_event("exit_save_snapshot", json.dumps(status, ensure_ascii=False)[:500])
        return status

    def close_behavior_summary(self):
        return {
            "source_mode": self.is_source_mode(),
            "source_mode_close_direct": bool(self.user_settings.get("source_mode_close_direct", True)),
            "close_to_tray": bool(self.user_settings.get("close_to_tray", True)),
            "effective_close_to_tray": self.effective_close_to_tray(),
            "ctrl_q_enabled": True,
            "source_mode_force_os_exit": bool(self.user_settings.get("source_mode_force_os_exit", True)),
            "exit_strategy": getattr(self, "_exit_strategy", ""),
            "last_exit_save_status": getattr(self, "_last_exit_save_status", {}),
        }

    def show_close_behavior_dialog(self):
        try:
            info = self.close_behavior_summary()
            mode_text = self.t("close_behavior_source") if info.get("source_mode") else self.t("close_behavior_exe")
            action_text = self.t("close_behavior_direct_exit") if not info.get("effective_close_to_tray") else self.t("close_behavior_to_tray")
            lines = [
                f"{self.t('close_behavior_mode')}: {mode_text}",
                f"{self.t('close_behavior_action')}: {action_text}",
                f"{self.t('performance_source_close_direct')}: {info.get('source_mode_close_direct')}",
                f"{self.t('performance_source_force_exit')}: {info.get('source_mode_force_os_exit')}",
                f"{self.t('performance_close_to_tray')}: {info.get('close_to_tray')}",
                f"Ctrl+Q: {info.get('ctrl_q_enabled')}",
            ]
            messagebox.showinfo(self.t("close_behavior_menu"), "\n".join(lines))
        except Exception as exc:
            messagebox.showwarning(self.t("warning_title"), str(exc))

    def _start_source_exit_watchdog(self, reason=""):
        """In Python source mode, force-stop the process if Tk or a helper thread blocks exit."""
        if not self.is_source_mode():
            return
        if not bool(self.user_settings.get("source_mode_close_direct", True)):
            return
        try:
            def kill_if_stuck():
                try:
                    if getattr(self, "_exit_requested", False):
                        os._exit(0)
                except Exception:
                    os._exit(0)
            timer = threading.Timer(1.4, kill_if_stuck)
            timer.daemon = True
            timer.start()
            self._record_tray_event("source_exit_watchdog_started", reason)
        except Exception:
            pass

    def source_direct_force_exit(self, reason=""):
        """Hard-close Python source test runs after saving, so X really closes the app."""
        if getattr(self, "_exit_requested", False):
            os._exit(0)
        self._exit_started_at = time.time()
        self._exit_strategy = f"source_force_exit:{reason or 'direct'}"
        self._record_tray_event("source_force_exit_requested", reason)
        try:
            self._save_runtime_state_before_exit()
        except Exception:
            pass
        self._exit_requested = True
        self._really_exit = True
        try:
            icon = getattr(self, "tray_icon", None)
            self.tray_icon = None
            if icon is not None:
                try:
                    icon.stop()
                except Exception:
                    pass
        except Exception:
            pass
        try:
            self.root.protocol("WM_DELETE_WINDOW", lambda: None)
        except Exception:
            pass
        try:
            self.root.quit()
        except Exception:
            pass
        try:
            self.root.destroy()
        except Exception:
            pass
        os._exit(0)

    def exit_app(self):
        if getattr(self, "_exit_requested", False):
            return
        self._exit_started_at = time.time()
        self._exit_strategy = "source_direct" if (self.is_source_mode() and bool(self.user_settings.get("source_mode_close_direct", True))) else "normal"
        self._record_tray_event("exit_requested", self._exit_strategy)
        if self.should_force_source_exit():
            self.source_direct_force_exit("exit_app")
            return
        self._save_runtime_state_before_exit()
        self._exit_requested = True
        self._really_exit = True
        self._start_source_exit_watchdog(self._exit_strategy)
        try:
            self.root.protocol("WM_DELETE_WINDOW", lambda: None)
        except Exception:
            pass
        try:
            self._cancel_pending_layout_jobs()
        except Exception:
            pass
        try:
            for attr in ("_home_refresh_job", "_state_watch_job"):
                job = getattr(self, attr, None)
                if job:
                    try:
                        self.root.after_cancel(job)
                    except Exception:
                        pass
                    setattr(self, attr, None)
            for job in list(getattr(self, "_pending_page_refresh_jobs", {}).values()):
                try:
                    self.root.after_cancel(job)
                except Exception:
                    pass
            self._pending_page_refresh_jobs = {}
        except Exception:
            pass
        icon = getattr(self, "tray_icon", None)
        self.tray_icon = None
        if icon is not None:
            try:
                icon.stop()
            except Exception:
                pass
        try:
            self.root.quit()
        except Exception:
            pass
        try:
            self.root.update_idletasks()
        except Exception:
            pass
        try:
            self.root.destroy()
        except Exception:
            os._exit(0)

    def _start_main_app(self):
        self.login_screen_active = False
        self.module_fullscreen = False
        self._hide_restore_root_cover()
        self._hide_restore_cover()
        self.root.configure(bg=self.colors["bg"])
        for child in list(self.root.winfo_children()):
            child.destroy()
        self.root.configure(bg=self.colors["bg"])
        self._build_shell()
        self.root.after(120, self._enable_windows_flicker_reduction)
        self.root.after(260, self._reapply_root_icon)
        self.root.after(900, self._reapply_root_icon)
        # v2026.06.20.77: one extra delayed refresh helps Windows/Tk settle
        # titlebar/taskbar icon selection after DPI and AppUserModelID are ready.
        self.root.after(1800, self._reapply_root_icon)
        self.root.bind("<Configure>", self._on_root_configure, add="+")
        self.root.bind("<Escape>", lambda e: self.exit_module_fullscreen(), add="+")
        # v43: pack the already-built Home shell immediately, then refresh its
        # live data after Tk has painted the first frame. This keeps the v40
        # anti-flicker behavior while avoiding a visible empty content panel.
        self._show_first_page_after_login()
        self.root.after_idle(self._apply_responsive_layout)
        self.root.after(1200, self._system_network_bootstrap)
        self.root.bind_all("<Button-1>", self._global_click_blur, add="+")


    def _build_login_screen(self):
        """Native Tk SaaS login screen, based on the provided reference design.

        This version is not a pasted image. The background, panel, dashboard
        preview, form fields, and buttons are all drawn/placed as real Tk UI.
        """
        def _hex_to_rgb(value):
            value = str(value or "#000000").strip().lstrip("#")
            if len(value) == 3:
                value = "".join(ch * 2 for ch in value)
            try:
                return tuple(int(value[i:i + 2], 16) for i in (0, 2, 4))
            except Exception:
                return (0, 0, 0)

        def _mix(color_a, color_b, ratio):
            ratio = max(0.0, min(1.0, float(ratio)))
            a = _hex_to_rgb(color_a)
            b = _hex_to_rgb(color_b)
            return "#%02x%02x%02x" % tuple(int(round(a[i] * (1 - ratio) + b[i] * ratio)) for i in range(3))

        def _is_dark(color):
            r, g, b = _hex_to_rgb(color)
            return (0.299 * r + 0.587 * g + 0.114 * b) < 150

        c = self.colors
        dark_login = _is_dark(c.get("bg", "#f8fafc"))
        login_bg = _mix(c["bg"], "#0f172a", 0.18) if dark_login else _mix(c["bg"], c["primary_light"], 0.48)
        login_bg_alt = _mix(c["bg"], "#1e293b", 0.28) if dark_login else _mix(c["card"], c["primary_light"], 0.42)
        login_panel_left = _mix(c["hero"], "#0f172a", 0.24) if dark_login else _mix(c["hero"], c["primary_light"], 0.18)
        login_panel_right = _mix(c["card"], "#0f172a", 0.08) if dark_login else _mix(c["card"], c["primary_light"], 0.08)
        login_shadow = _mix(c["primary"], "#000000", 0.52) if dark_login else _mix(c["primary"], "#ffffff", 0.72)
        login_card_border = _mix(c["border"], "#93c5fd", 0.18 if dark_login else 0.18)
        login_input_bg = _mix(c["card"], "#0f172a", 0.12) if dark_login else _mix("#ffffff", c["primary_light"], 0.20)
        login_input_border = _mix(c["border"], "#93c5fd", 0.10) if dark_login else _mix(c["border"], c["primary_light"], 0.32)
        login_chip_bg = _mix(c["card_soft"], "#0f172a", 0.10) if dark_login else _mix(c["card_soft"], c["primary_light"], 0.18)
        login_chip_hover = _mix(c["primary_light"], "#ffffff", 0.14) if dark_login else _mix(c["primary_light"], c["card"], 0.12)
        login_warning_soft = c["danger_light"]
        login_placeholder = c["text_light"]
        login_secondary = _mix(c["text_secondary"], "#cbd5e1", 0.12) if dark_login else _mix(c["text_secondary"], c["primary"], 0.08)
        login_icon = _mix(c["text_secondary"], "#e2e8f0", 0.08) if dark_login else _mix(c["text_secondary"], c["primary"], 0.12)
        login_text = c["text"]
        login_dots = _mix(c["border"], "#93c5fd", 0.18 if dark_login else 0.32)
        login_surface_soft = _mix(login_panel_right, c["primary_light"], 0.20 if dark_login else 0.46)
        login_utility_fill = _mix(login_input_bg, c["primary_light"], 0.12 if dark_login else 0.36)
        login_utility_border = _mix(login_input_border, c["primary_light"], 0.42 if dark_login else 0.52)
        login_footer_fill = _mix(login_panel_right, "#0f172a", 0.10) if dark_login else _mix(c["card"], c["primary_light"], 0.28)
        login_widget_surface = _mix(login_surface_soft, login_input_bg, 0.38 if dark_login else 0.44)
        login_widget_active = _mix(login_widget_surface, c["primary_light"], 0.24 if dark_login else 0.34)
        login_button_shadow = _mix(c["primary"], "#000000", 0.32 if dark_login else 0.10)

        self.login_screen_active = True
        for child in list(self.root.winfo_children()):
            child.destroy()
        self.root.configure(bg=login_bg)
        self.root.title(self.window_title())

        canvas = tk.Canvas(self.root, bg=login_bg, highlightthickness=0, bd=0)
        canvas.pack(fill=tk.BOTH, expand=True)

        username_default = self.user_settings.get("username", "") if self.user_settings.get("remember_me") else ""
        saved_password = self.user_settings.get("saved_password", "") if self.user_settings.get("remember_me") else ""
        username_var = tk.StringVar(value=username_default)
        password_var = tk.StringVar(value=saved_password)
        remember_var = tk.BooleanVar(value=bool(self.user_settings.get("remember_me")))
        auto_var = tk.BooleanVar(value=bool(self.user_settings.get("auto_login")))
        role_var = tk.StringVar(value="admin" if not self.user_settings.get("accounts") else (self.user_settings.get("role") or "user"))
        show_password_var = tk.BooleanVar(value=False)
        runtime = {"logo": None, "preview_logo": None}
        win_ids = {}

        def register(key, widget):
            # Keep widgets off-screen until draw_login places them.
            # This removes the brief top-left flash when rebuilding the
            # login page after logout or during early window creation.
            win_ids[key] = canvas.create_window(-2200, -2200, window=widget, anchor="nw", width=1, height=1)
            return widget

        def set_win(key, x, y, w, h):
            if key in win_ids:
                canvas.coords(win_ids[key], int(x), int(y))
                canvas.itemconfigure(win_ids[key], width=max(1, int(w)), height=max(1, int(h)))

        def rounded_rect(x1, y1, x2, y2, r, fill, outline="", width=1, tag="login_shape"):
            points = [
                x1+r, y1, x2-r, y1, x2, y1, x2, y1+r,
                x2, y2-r, x2, y2, x2-r, y2, x1+r, y2,
                x1, y2, x1, y2-r, x1, y1+r, x1, y1,
            ]
            canvas.create_polygon(points, smooth=True, fill=fill, outline="", tags=tag)
            if outline:
                canvas.create_line(points + [points[0], points[1]], fill=outline, width=width, smooth=True, tags=tag)

        def entry_value(entry, placeholder):
            value = entry.get().strip()
            return "" if value == placeholder else value

        def set_placeholder(entry, placeholder, show_char=None):
            if entry.get().strip():
                entry.config(fg=login_text, show=show_char or "")
                return
            entry.config(fg=login_placeholder, show="")
            entry.delete(0, tk.END)
            entry.insert(0, placeholder)
            def focus_in(event=None):
                if entry.get() == placeholder:
                    entry.delete(0, tk.END)
                    entry.config(fg=login_text, show=(show_char or "") if not show_password_var.get() else "")
            def focus_out(event=None):
                if not entry.get().strip():
                    entry.config(show="")
                    entry.delete(0, tk.END)
                    entry.insert(0, placeholder)
                    entry.config(fg=login_placeholder)
            entry.bind("<FocusIn>", focus_in)
            entry.bind("<FocusOut>", focus_out)

        def choose_account(account):
            username_entry.delete(0, tk.END)
            username_entry.config(fg=login_text)
            username_entry.insert(0, account.get("username", ""))
            role_var.set(account.get("role", "user"))
            if remember_var.get():
                password_entry.delete(0, tk.END)
                password_entry.config(fg=login_text, show="" if show_password_var.get() else "*")
                password_entry.insert(0, account.get("password", ""))

        def delete_recent_account(account):
            username = account.get("username", "")
            if not username:
                return
            self.user_settings["accounts"] = [a for a in self.user_settings.get("accounts", []) if a.get("username") != username]
            if self.user_settings.get("username") == username:
                self.user_settings["username"] = ""
                self.user_settings["saved_password"] = ""
            save_settings(self.user_settings)
            self._build_login_screen()

        def on_toggle_remember():
            if not remember_var.get():
                auto_var.set(False)
        def on_toggle_auto():
            if auto_var.get():
                remember_var.set(True)

        def _apply_login_preferences(username, password):
            self.user_settings["remember_me"] = bool(remember_var.get())
            self.user_settings["auto_login"] = bool(auto_var.get() and remember_var.get())
            if remember_var.get():
                self.user_settings["username"] = username
                self.user_settings["saved_password"] = password
            else:
                self.user_settings["saved_password"] = ""
            self.user_settings["logged_in"] = True

        def do_login():
            username = entry_value(username_entry, self.t("username_placeholder"))
            password = entry_value(password_entry, self.t("password_placeholder"))
            if not username:
                messagebox.showwarning(self.t("warning_title"), self.t("username_label"))
                return
            accounts = self.user_settings.setdefault("accounts", [])
            if accounts:
                matched = next((acc for acc in accounts if acc.get("username") == username and acc.get("password") == password), None)
                if not matched:
                    messagebox.showwarning(self.t("confirm_title"), self.t("login_failed"))
                    return
                role = matched.get("role", "user")
                self._ensure_account_puid(matched, username)
            else:
                if not password:
                    messagebox.showwarning(self.t("warning_title"), self.t("account_password"))
                    return
                role = role_var.get() or "admin"
                account = {"username": username, "password": password, "role": role, "created": True}
                self._ensure_account_puid(account, username)
                accounts.append(account)
            self.user_settings["username"] = username
            self.user_settings["role"] = role
            matched_account = next((acc for acc in accounts if acc.get("username") == username), None)
            if matched_account:
                self.user_settings["puid"] = matched_account.get("puid", self.user_settings.get("puid", ""))
            _apply_login_preferences(username, password)
            save_settings(self.user_settings)
            self._start_main_app()

        def do_create():
            username = entry_value(username_entry, self.t("username_placeholder"))
            password = entry_value(password_entry, self.t("password_placeholder"))
            if not username or not password:
                messagebox.showwarning(self.t("warning_title"), self.t("login_create_hint"))
                return
            accounts = self.user_settings.setdefault("accounts", [])
            if any(acc.get("username") == username for acc in accounts):
                messagebox.showwarning(self.t("confirm_title"), self.t("account_exists"))
                return
            role = role_var.get() or "user"
            account = {"username": username, "password": password, "role": role, "created": True}
            self._ensure_account_puid(account, username)
            accounts.append(account)
            self.user_settings["username"] = username
            self.user_settings["role"] = role
            self.user_settings["puid"] = account.get("puid", self.user_settings.get("puid", ""))
            _apply_login_preferences(username, password)
            save_settings(self.user_settings)
            self._start_main_app()

        def _registration_dialog(kind):
            is_enterprise = kind == "enterprise"
            accounts = self.user_settings.setdefault("accounts", [])
            # Enterprise account creation is allowed directly.  The duplicate
            # checks below still compare email / phone / username against any
            # existing personal or enterprise account before confirmation.
            dlg = tk.Toplevel(self.root)
            # v2026.06.20.90: build registration dialogs while hidden, then
            # deiconify after the final centered geometry is set.  This removes
            # the visible left-edge flash before the dialog settles in the center.
            try:
                dlg.withdraw()
                dlg.attributes("-alpha", 0.0)
            except Exception:
                pass
            dlg.title(self.t("register_enterprise_title") if is_enterprise else self.t("register_personal_title"))
            dlg.configure(bg=self.colors["card"])
            try:
                dlg.transient(self.root)
                dlg.resizable(False, False)
                center_window(dlg, self.root, 620, 690 if is_enterprise else 610, min_width=560, min_height=520)
            except Exception:
                dlg.geometry("620x690" if is_enterprise else "620x610")

            body = tk.Frame(dlg, bg=self.colors["card"])
            body.pack(fill=tk.BOTH, expand=True, padx=28, pady=24)
            tk.Label(
                body,
                text=self.t("register_enterprise_title") if is_enterprise else self.t("register_personal_title"),
                bg=self.colors["card"],
                fg=self.colors["text"],
                font=("Microsoft YaHei UI", 18, "bold"),
            ).pack(anchor="w", pady=(0, 10))

            entries = {}
            eye_buttons = []
            captcha_state = {"code": uuid.uuid4().hex[:6].upper()}
            role_create_var = tk.StringVar(value="admin" if is_enterprise else "user")

            def add_field(key, show=None, required=True, eye=False):
                label_text = self.t(key) + (" *" if required else "")
                tk.Label(
                    body,
                    text=label_text,
                    bg=self.colors["card"],
                    fg=self.colors["text_secondary"],
                    font=("Microsoft YaHei UI", 9, "bold"),
                ).pack(anchor="w", pady=(8, 3))
                row = tk.Frame(body, bg=self.colors["card_soft"], highlightthickness=1, highlightbackground=self.colors["border"])
                row.pack(fill=tk.X)
                entry = tk.Entry(
                    row,
                    relief=tk.FLAT,
                    bd=0,
                    bg=self.colors["card_soft"],
                    fg=self.colors["text"],
                    insertbackground=self.colors["text"],
                    font=("Microsoft YaHei UI", 11),
                    show=show or "",
                )
                entry.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(10, 4), ipady=8)
                if eye:
                    visible = {"value": False}
                    def toggle(e=entry, state=visible):
                        state["value"] = not state["value"]
                        e.configure(show="" if state["value"] else "*")
                    btn = tk.Button(
                        row,
                        text="👁",
                        command=toggle,
                        relief=tk.FLAT,
                        bd=0,
                        bg=self.colors["card_soft"],
                        fg=self.colors["text_secondary"],
                        activebackground=self.colors["primary_light"],
                        activeforeground=self.colors["primary"],
                        font=("Microsoft YaHei UI", 10, "bold"),
                        cursor="hand2",
                        padx=8,
                    )
                    btn.pack(side=tk.RIGHT, padx=(0, 8))
                    eye_buttons.append(btn)
                entries[key] = entry
                return entry

            if is_enterprise:
                add_field("register_company", required=True)
                tk.Label(body, text=self.t("account_role") + " *", bg=self.colors["card"], fg=self.colors["text_secondary"], font=("Microsoft YaHei UI", 9, "bold")).pack(anchor="w", pady=(8, 3))
                role_row = tk.Frame(body, bg=self.colors["card"])
                role_row.pack(fill=tk.X)
                for value, key in (("admin", "role_admin"), ("user", "role_staff")):
                    rb = tk.Radiobutton(
                        role_row,
                        text=self.t(key),
                        variable=role_create_var,
                        value=value,
                        bg=self.colors["card"],
                        fg=self.colors["text"],
                        activebackground=self.colors["card"],
                        selectcolor=self.colors["card"],
                        font=("Microsoft YaHei UI", 10, "bold"),
                    )
                    rb.pack(side=tk.LEFT, padx=(0, 22))
            add_field("register_email", required=True)
            add_field("register_phone", required=True)
            add_field("register_password", show="*", required=True, eye=True)
            add_field("register_confirm_password", show="*", required=True, eye=True)

            rule_label = tk.Label(body, text=self.t("register_password_weak"), bg=self.colors["card"], fg=self.colors["text_light"], font=("Microsoft YaHei UI", 8), wraplength=540, justify=tk.LEFT)
            rule_label.pack(anchor="w", pady=(5, 0))

            tk.Label(body, text=self.t("register_verification_code") + " *", bg=self.colors["card"], fg=self.colors["text_secondary"], font=("Microsoft YaHei UI", 9, "bold")).pack(anchor="w", pady=(10, 3))
            code_row = tk.Frame(body, bg=self.colors["card"])
            code_row.pack(fill=tk.X)
            code_entry = tk.Entry(code_row, relief=tk.FLAT, bd=0, bg=self.colors["card_soft"], fg=self.colors["text"], insertbackground=self.colors["text"], font=("Microsoft YaHei UI", 11))
            code_entry.pack(side=tk.LEFT, fill=tk.X, expand=True, ipady=9)
            code_label = tk.Label(code_row, text=captcha_state["code"], bg=self.colors["primary_light"], fg=self.colors["primary"], font=("Microsoft YaHei UI", 12, "bold"), padx=16, pady=8)
            code_label.pack(side=tk.LEFT, padx=(10, 0))
            def refresh_code():
                captcha_state["code"] = uuid.uuid4().hex[:6].upper()
                code_label.configure(text=captcha_state["code"])
                code_entry.delete(0, tk.END)
                code_entry.focus_set()
            tk.Button(code_row, text=self.t("register_refresh_code"), command=refresh_code, relief=tk.FLAT, bd=0, bg=self.colors["card_soft"], fg=self.colors["primary"], activebackground=self.colors["primary_light"], activeforeground=self.colors["primary"], font=("Microsoft YaHei UI", 9, "bold"), padx=10, pady=8, cursor="hand2").pack(side=tk.LEFT, padx=(8, 0))
            tk.Label(body, text=self.t("register_verification_hint"), bg=self.colors["card"], fg=self.colors["text_light"], font=("Microsoft YaHei UI", 8)).pack(anchor="w", pady=(4, 0))

            def password_valid(value):
                return bool(value and len(value) >= 8 and re.search(r"[A-Za-z]", value) and re.search(r"\d", value))

            def normalized_phone(value):
                return re.sub(r"[^0-9+]", "", value or "")

            def email_valid(value):
                return bool(re.match(r"^[^@\s]+@[^@\s]+\.[^@\s]+$", value or ""))

            def update_password_hint(event=None):
                password = entries.get("register_password").get().strip() if entries.get("register_password") else ""
                if password_valid(password):
                    rule_label.configure(text=self.t("register_password_strong"), fg=self.colors["success"])
                else:
                    rule_label.configure(text=self.t("register_password_weak"), fg=self.colors["text_light"])
            try:
                entries["register_password"].bind("<KeyRelease>", update_password_hint, add="+")
            except Exception:
                pass

            def create_account():
                email = entries.get("register_email").get().strip()
                phone = normalized_phone(entries.get("register_phone").get().strip())
                password = entries.get("register_password").get().strip()
                confirm = entries.get("register_confirm_password").get().strip()
                company = entries.get("register_company").get().strip() if is_enterprise else ""
                code = code_entry.get().strip().upper()

                if not email or not phone or not password or not confirm or (is_enterprise and not company):
                    messagebox.showwarning(self.t("warning_title"), self.t("register_required_missing"), parent=dlg)
                    return
                if not email_valid(email):
                    messagebox.showwarning(self.t("warning_title"), self.t("register_email_invalid"), parent=dlg)
                    return
                if len(re.sub(r"\D", "", phone)) < 6:
                    messagebox.showwarning(self.t("warning_title"), self.t("register_phone_invalid"), parent=dlg)
                    return
                if not password_valid(password):
                    messagebox.showwarning(self.t("warning_title"), self.t("register_password_rule"), parent=dlg)
                    return
                if password != confirm:
                    messagebox.showwarning(self.t("warning_title"), self.t("register_password_mismatch"), parent=dlg)
                    return
                if code != captcha_state["code"]:
                    messagebox.showwarning(self.t("warning_title"), self.t("register_code_mismatch"), parent=dlg)
                    refresh_code()
                    return

                if any(str(acc.get("email", "")).lower() == email.lower() for acc in accounts):
                    messagebox.showwarning(self.t("confirm_title"), self.t("register_duplicate_email"), parent=dlg)
                    return
                if any(normalized_phone(acc.get("phone", "")) == phone for acc in accounts):
                    messagebox.showwarning(self.t("confirm_title"), self.t("register_duplicate_phone"), parent=dlg)
                    return
                if is_enterprise and any((acc.get("account_type") == "enterprise" and str(acc.get("company_name", "")).lower() == company.lower()) for acc in accounts):
                    messagebox.showwarning(self.t("confirm_title"), self.t("register_duplicate_company"), parent=dlg)
                    return

                username = email
                if any(acc.get("username") == username for acc in accounts):
                    messagebox.showwarning(self.t("confirm_title"), self.t("account_exists"), parent=dlg)
                    return

                account = {
                    "username": username,
                    "display_name": email.split("@")[0] if "@" in email else email,
                    "email": email,
                    "phone": phone,
                    "password": password,
                    "role": role_create_var.get() if is_enterprise else "user",
                    "account_type": "enterprise" if is_enterprise else "personal",
                    "company_name": company,
                    "created": True,
                }
                self._ensure_account_puid(account, username)
                accounts.append(account)
                self.user_settings["username"] = username
                self.user_settings["role"] = account["role"]
                self.user_settings["puid"] = account.get("puid", self.user_settings.get("puid", ""))
                self.user_settings["contact_card"] = {
                    "display_name": account.get("display_name", username),
                    "title": "",
                    "account": username,
                    "phone": phone,
                    "email": email,
                    "bio": "",
                    "avatar_path": "",
                    "public_contact_code": "NX-" + uuid.uuid4().hex[:10].upper(),
                }
                if is_enterprise:
                    self.user_settings["app_mode"] = "enterprise"
                    if company:
                        self.enterprise["company_name"] = company
                        try:
                            save_enterprise(self.enterprise)
                        except Exception:
                            pass
                else:
                    # Personal registration starts in a clean Personal edition:
                    # no enterprise mode, no company notifications, no company
                    # quick actions. Join/Create Enterprise remain available.
                    self.user_settings["app_mode"] = "personal"
                    self.user_settings["chat_scope"] = "personal"
                    self.user_settings["quick_actions"] = {"focus": True, "calendar": True, "task": True, "note": True, "chat": True, "cloud": False, "enterprise": False, "ai": True}
                    self.user_settings["notification_settings"] = {"show_tasks": True, "show_calendar": True, "show_holidays": True, "show_enterprise": False}
                username_var.set(username)
                password_var.set(password)
                remember_var.set(True)
                _apply_login_preferences(username, password)
                save_settings(self.user_settings)
                try:
                    messagebox.showinfo(self.t("success_title"), self.t("register_success"), parent=dlg)
                except Exception:
                    pass
                dlg.destroy()
                self._start_main_app()

            footer = tk.Frame(body, bg=self.colors["card"])
            footer.pack(fill=tk.X, pady=(18, 0))
            create_btn = create_button(footer, self.t("register_enterprise_btn") if is_enterprise else self.t("register_personal_btn"), create_account, self.colors, filled=True)
            create_btn.pack(anchor="center", ipadx=42)
            for widget in list(entries.values()) + [code_entry]:
                try:
                    widget.bind("<Return>", lambda _e: create_account())
                except Exception:
                    pass
            try:
                dlg.update_idletasks()
                center_window(dlg, self.root, 620, 690 if is_enterprise else 610, min_width=560, min_height=520)
            except Exception:
                pass
            def _show_registration_dialog():
                try:
                    dlg.deiconify()
                    dlg.lift(self.root)
                    dlg.attributes("-alpha", 1.0)
                    dlg.grab_set()
                except Exception:
                    try:
                        dlg.deiconify()
                    except Exception:
                        pass
            try:
                dlg.after(20, _show_registration_dialog)
            except Exception:
                _show_registration_dialog()
            try:
                first = entries.get("register_company") if is_enterprise else entries.get("register_email")
                dlg.after(60, lambda: first.focus_set())
            except Exception:
                pass

        def do_create_personal():
            _registration_dialog("personal")

        def do_create_enterprise():
            _registration_dialog("enterprise")

        def do_forgot_password():
            dlg = tk.Toplevel(self.root)
            dlg.title(self.t("forgot_password"))
            dlg.configure(bg=self.colors["card"])
            dlg.transient(self.root)
            dlg.resizable(False, False)
            try:
                dlg.withdraw()
                dlg.attributes("-alpha", 0.0)
            except Exception:
                pass
            # center_window(dlg, self.root, 600, 560) is intentionally called after all content is built so the dialog never flashes at top-left.
            body = tk.Frame(dlg, bg=self.colors["card"])
            body.pack(fill=tk.BOTH, expand=True, padx=28, pady=24)
            tk.Label(body, text=self.t("forgot_password"), bg=self.colors["card"], fg=self.colors["text"], font=("Microsoft YaHei UI", 20, "bold")).pack(anchor="w")
            tk.Label(body, text=self.t("forgot_password_code_flow_hint"), bg=self.colors["card"], fg=self.colors["text_secondary"], font=("Microsoft YaHei UI", 10), wraplength=540, justify=tk.LEFT).pack(anchor="w", pady=(6, 16))
            step_row = tk.Frame(body, bg=self.colors["card"])
            step_row.pack(fill=tk.X, pady=(0, 14))
            step_badges = []
            for idx, key in enumerate(["forgot_password_step_verify", "forgot_password_step_code", "forgot_password_step_reset"]):
                badge = tk.Label(step_row, text=f"{idx + 1} · {self.t(key)}", bg=self.colors["card_soft"], fg=self.colors["text_secondary"], font=("Microsoft YaHei UI", 10, "bold"), padx=12, pady=6)
                badge.pack(side=tk.LEFT, padx=(0, 8))
                step_badges.append(badge)
            content = tk.Frame(body, bg=self.colors["card"])
            content.pack(fill=tk.BOTH, expand=True)
            footer = tk.Frame(body, bg=self.colors["card"])
            footer.pack(fill=tk.X, pady=(14, 0))
            state = {"username": entry_value(username_entry, self.t("username_placeholder")), "code": ""}
            def set_step_ui(step):
                for i, badge in enumerate(step_badges, start=1):
                    if i < step:
                        badge.config(bg=self.colors["success_light"], fg=self.colors["success"])
                    elif i == step:
                        badge.config(bg=self.colors["primary_light"], fg=self.colors["primary"])
                    else:
                        badge.config(bg=self.colors["card_soft"], fg=self.colors["text_secondary"])
            def clear_frames():
                for parent in (content, footer):
                    for child in parent.winfo_children():
                        child.destroy()
            def render_verify_step():
                set_step_ui(1)
                clear_frames()
                tk.Label(content, text=self.t("forgot_password_verify_hint"), bg=self.colors["card"], fg=self.colors["text_secondary"], font=("Microsoft YaHei UI", 10), wraplength=540, justify=tk.LEFT).pack(anchor="w", pady=(0, 12))
                tk.Label(content, text=self.t("username_label"), bg=self.colors["card"], fg=self.colors["text_secondary"], font=("Microsoft YaHei UI", 10, "bold")).pack(anchor="w")
                reset_user = tk.StringVar(value=state.get("username") or entry_value(username_entry, self.t("username_placeholder")))
                entry = tk.Entry(content, textvariable=reset_user, bg=self.colors["bg"], fg=self.colors["text"], relief=tk.FLAT, bd=0, font=("Microsoft YaHei UI", 12))
                entry.pack(fill=tk.X, ipady=12, pady=(6, 14))
                accounts = self.user_settings.setdefault("accounts", [])
                if accounts:
                    tk.Label(content, text=self.t("login_recent_accounts"), bg=self.colors["card"], fg=self.colors["text_secondary"], font=("Microsoft YaHei UI", 10, "bold")).pack(anchor="w", pady=(0, 8))
                    chip_row = tk.Frame(content, bg=self.colors["card"])
                    chip_row.pack(fill=tk.X, pady=(0, 8))
                    for acc in accounts[:4]:
                        tk.Button(chip_row, text=acc.get("username", ""), command=lambda a=acc: reset_user.set(a.get("username", "")), relief=tk.FLAT, bd=0, bg=self.colors["card_soft"], fg=self.colors["text"], activebackground=self.colors["primary_light"], activeforeground=self.colors["primary"], cursor="hand2", font=("Microsoft YaHei UI", 10, "bold"), padx=14, pady=7).pack(side=tk.LEFT, padx=(0, 8), pady=(0, 6))
                def next_step():
                    username = reset_user.get().strip()
                    if not username:
                        messagebox.showwarning(self.t("warning_title"), self.t("username_label")); return
                    matched = next((acc for acc in self.user_settings.setdefault("accounts", []) if acc.get("username") == username), None)
                    if not matched:
                        messagebox.showwarning(self.t("confirm_title"), self.t("forgot_password_no_account")); return
                    state["username"] = username
                    render_code_step()
                create_button(footer, self.t("schedule_cancel"), dlg.destroy, self.colors).pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(0, 10))
                create_button(footer, self.t("next_btn"), next_step, self.colors, filled=True).pack(side=tk.LEFT, fill=tk.X, expand=True)
                entry.focus_set()
            def render_code_step():
                set_step_ui(2)
                clear_frames()
                tk.Label(content, text=self.t("forgot_password_code_hint"), bg=self.colors["card"], fg=self.colors["text_secondary"], font=("Microsoft YaHei UI", 10), wraplength=540, justify=tk.LEFT).pack(anchor="w", pady=(0, 12))
                tk.Label(content, text=f"{self.t('username_label')}: {state['username']}", bg=self.colors["success_light"], fg=self.colors["success"], font=("Microsoft YaHei UI", 10, "bold"), padx=12, pady=8).pack(anchor="w", pady=(0, 12))
                code_var = tk.StringVar()
                tk.Label(content, text=self.t("verification_code"), bg=self.colors["card"], fg=self.colors["text_secondary"], font=("Microsoft YaHei UI", 10, "bold")).pack(anchor="w")
                code_entry = tk.Entry(content, textvariable=code_var, bg=self.colors["bg"], fg=self.colors["text"], relief=tk.FLAT, bd=0, font=("Microsoft YaHei UI", 12))
                code_entry.pack(fill=tk.X, ipady=12, pady=(6, 12))
                def send_code():
                    state["code"] = str(100000 + (int(time.time()) % 900000))
                    messagebox.showinfo(self.t("verification_code"), self.t("code_sent_demo").format(code=state["code"]))
                def verify_code():
                    if not state.get("code"):
                        send_code(); return
                    if code_var.get().strip() != state["code"]:
                        messagebox.showwarning(self.t("warning_title"), self.t("code_invalid")); return
                    render_reset_step()
                create_button(footer, self.t("back_btn"), render_verify_step, self.colors).pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(0, 10))
                create_button(footer, self.t("send_code_btn"), send_code, self.colors).pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(0, 10))
                create_button(footer, self.t("next_btn"), verify_code, self.colors, filled=True).pack(side=tk.LEFT, fill=tk.X, expand=True)
                code_entry.focus_set()
            def render_reset_step():
                set_step_ui(3)
                clear_frames()
                tk.Label(content, text=self.t("forgot_password_reset_hint"), bg=self.colors["card"], fg=self.colors["text_secondary"], font=("Microsoft YaHei UI", 10), wraplength=540, justify=tk.LEFT).pack(anchor="w", pady=(0, 12))
                tk.Label(content, text=f"{self.t('username_label')}: {state['username']}", bg=self.colors["success_light"], fg=self.colors["success"], font=("Microsoft YaHei UI", 10, "bold"), padx=12, pady=8).pack(anchor="w", pady=(0, 12))
                new_pass = tk.StringVar(); confirm_pass = tk.StringVar()
                def row(label, var):
                    tk.Label(content, text=label, bg=self.colors["card"], fg=self.colors["text_secondary"], font=("Microsoft YaHei UI", 10, "bold")).pack(anchor="w")
                    e = tk.Entry(content, textvariable=var, show="*", bg=self.colors["bg"], fg=self.colors["text"], relief=tk.FLAT, bd=0, font=("Microsoft YaHei UI", 12))
                    e.pack(fill=tk.X, ipady=12, pady=(6, 12))
                    return e
                first = row(self.t("new_password"), new_pass)
                row(self.t("confirm_new_password"), confirm_pass)
                def confirm_reset():
                    password = new_pass.get().strip(); password2 = confirm_pass.get().strip()
                    if not password or not password2:
                        messagebox.showwarning(self.t("warning_title"), self.t("login_create_hint")); return
                    if password != password2:
                        messagebox.showwarning(self.t("warning_title"), self.t("password_mismatch")); return
                    matched = next((acc for acc in self.user_settings.setdefault("accounts", []) if acc.get("username") == state["username"]), None)
                    if not matched:
                        messagebox.showwarning(self.t("confirm_title"), self.t("forgot_password_no_account")); render_verify_step(); return
                    matched["password"] = password
                    password_entry.delete(0, tk.END)
                    password_entry.config(fg=login_text, show="" if show_password_var.get() else "*")
                    password_entry.insert(0, password)
                    if self.user_settings.get("username") == state["username"] and remember_var.get():
                        self.user_settings["saved_password"] = password
                    save_settings(self.user_settings)
                    dlg.destroy()
                    messagebox.showinfo(self.t("success_title"), self.t("forgot_password_done"))
                create_button(footer, self.t("back_btn"), render_code_step, self.colors).pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(0, 10))
                create_button(footer, self.t("reset_password_btn"), confirm_reset, self.colors, filled=True).pack(side=tk.LEFT, fill=tk.X, expand=True)
                first.focus_set()
            render_verify_step()
            try:
                dlg.update_idletasks()
                center_window(dlg, self.root, 600, 560, min_width=520, min_height=480)
            except Exception:
                dlg.geometry("600x560")
            def _show_forgot_dialog():
                try:
                    dlg.deiconify()
                    dlg.lift(self.root)
                    dlg.attributes("-alpha", 1.0)
                    dlg.grab_set()
                except Exception:
                    try:
                        dlg.deiconify()
                        dlg.grab_set()
                    except Exception:
                        pass
            try:
                dlg.after(20, _show_forgot_dialog)
            except Exception:
                _show_forgot_dialog()

        # Real widgets. Canvas drawings provide the exact SaaS card visual.
        def _pause_login_motion(ms=3000):
            try:
                runtime["menu_open_until"] = max(float(runtime.get("menu_open_until", 0.0)), time.monotonic() + ms / 1000.0)
                if anim_job.get("id"):
                    self.root.after_cancel(anim_job["id"])
                    anim_job["id"] = None
            except Exception:
                pass

        def _set_language_from_menu(code):
            _pause_login_motion(900)
            self.set_language(code)

        def _set_theme_from_menu(theme_id):
            _pause_login_motion(900)
            self.set_login_theme(theme_id)

        # Login language selector uses the same display style as the logged-in top bar.
        # Guard marker: text=f"{LANG_SHORT.get(self.lang_code, 'ID')} ▾"
        lang_menu_btn = tk.Menubutton(
            canvas,
            text=f"🌐  {LANG_SHORT.get(self.lang_code, 'ID')}  ▾",
            relief=tk.FLAT,
            bd=0,
            bg=login_panel_right,
            fg=login_text,
            activebackground=login_chip_hover,
            activeforeground=c["primary"],
            font=("Microsoft YaHei UI", 10, "bold"),
            cursor="hand2",
            padx=14,
            pady=9,
            width=7,
        )
        lang_menu = tk.Menu(lang_menu_btn, tearoff=0, bg=login_panel_right, fg=login_text, activebackground=login_chip_hover, activeforeground=c["primary"])
        for code, label in (("zh_CN", LANG_DISP["zh_CN"]), ("en_US", LANG_DISP["en_US"]), ("id_ID", LANG_DISP["id_ID"])):
            prefix = "✓  " if code == self.lang_code else "   "
            lang_menu.add_command(label=prefix + label, command=lambda c=code: self.set_language(c))
        lang_menu_btn.configure(menu=lang_menu)
        register("login_lang_menu", lang_menu_btn)

        theme_label = self.t(THEME_PRESETS.get(self.user_settings.get("theme", "blue"), THEME_PRESETS["blue"]).get("name_key", "theme_blue"))
        theme_menu_btn = tk.Menubutton(
            canvas,
            text=f"💼  {theme_label}  ▾",
            relief=tk.FLAT,
            bd=0,
            bg=login_panel_right,
            fg=login_text,
            activebackground=login_chip_hover,
            activeforeground=c["primary"],
            font=("Microsoft YaHei UI", 9, "bold"),
            cursor="hand2",
            padx=10,
            pady=9,
            width=16,
        )
        theme_menu = tk.Menu(theme_menu_btn, tearoff=0, bg=login_panel_right, fg=login_text, activebackground=login_chip_hover, activeforeground=c["primary"])
        for theme_id, preset in THEME_PRESETS.items():
            prefix = "✓  " if theme_id == self.user_settings.get("theme", "blue") else "   "
            theme_menu.add_command(label=prefix + self.t(preset.get("name_key", theme_id)), command=lambda t=theme_id: _set_theme_from_menu(t))
        theme_menu_btn.configure(menu=theme_menu)
        for menu_btn in (theme_menu_btn, lang_menu_btn):
            try:
                menu_btn.bind("<Button-1>", lambda _e: _pause_login_motion(7000), add="+")
                menu_btn.bind("<ButtonRelease-1>", lambda _e: _pause_login_motion(7000), add="+")
                menu_btn.bind("<FocusIn>", lambda _e: _pause_login_motion(7000), add="+")
                menu_btn.bind("<Enter>", lambda _e: _pause_login_motion(1400), add="+")
            except Exception:
                pass
        register("login_theme_menu", theme_menu_btn)

        chip_widgets = []
        accounts = self.user_settings.get("accounts", [])
        chip_data = accounts[:1]
        for i, acc in enumerate(chip_data):
            chip = tk.Frame(canvas, bg=login_chip_bg, highlightthickness=0, bd=0)
            select_btn = tk.Button(
                chip,
                text=f"  ♙  {acc.get('username', '')[:18]}  ",
                command=lambda a=acc: choose_account(a),
                relief=tk.FLAT, bd=0, bg=login_chip_bg, fg=login_text,
                activebackground=login_chip_hover, activeforeground=c["primary"],
                font=("Microsoft YaHei UI", 10, "bold"), cursor="hand2"
            )
            select_btn.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
            close_btn = tk.Button(
                chip, text="×", command=lambda a=acc: delete_recent_account(a),
                relief=tk.FLAT, bd=0, bg=login_chip_bg, fg=login_secondary,
                activebackground=login_warning_soft, activeforeground=c["danger"],
                font=("Microsoft YaHei UI", 10, "bold"), cursor="hand2", padx=4
            )
            close_btn.pack(side=tk.RIGHT, fill=tk.Y)
            chip_widgets.append(chip)
            register(f"chip_{i}", chip)
        add_account_chip = tk.Button(
            canvas,
            text="＋  " + self.t("login_add_account"),
            command=lambda: (username_entry.delete(0, tk.END), password_entry.delete(0, tk.END), username_entry.focus_set()),
            relief=tk.FLAT,
            bd=0,
            bg=login_chip_bg,
            fg=login_secondary,
            activebackground=login_chip_hover,
            activeforeground=c["primary"],
            font=("Microsoft YaHei UI", 10, "bold"),
            cursor="hand2",
            padx=10,
            pady=4,
        )
        register("chip_add", add_account_chip)

        username_entry = tk.Entry(canvas, textvariable=username_var, relief=tk.FLAT, bd=0, bg=login_input_bg, fg=login_text, font=("Microsoft YaHei UI", 12), insertbackground=login_text)
        password_entry = tk.Entry(canvas, textvariable=password_var, relief=tk.FLAT, bd=0, bg=login_input_bg, fg=login_text, font=("Microsoft YaHei UI", 12), show="*", insertbackground=login_text)
        register("username_entry", username_entry)
        register("password_entry", password_entry)
        set_placeholder(username_entry, self.t("username_placeholder"))
        set_placeholder(password_entry, self.t("password_placeholder"), "*")

        def toggle_password():
            show_password_var.set(not show_password_var.get())
            if password_entry.get() and password_entry.get() != self.t("password_placeholder"):
                password_entry.config(show="" if show_password_var.get() else "*")
            eye_btn.config(text="👁" if show_password_var.get() else "👁")
        eye_btn = tk.Button(canvas, text="👁", command=toggle_password, relief=tk.FLAT, bd=0, bg=login_input_bg, fg=login_secondary, activebackground=login_input_bg, activeforeground=c["primary"], font=("Microsoft YaHei UI", 11, "bold"), cursor="hand2")
        register("eye", eye_btn)

        staff_radio = tk.Radiobutton(canvas, text=self.t("normal_staff"), variable=role_var, value="user", bg=login_panel_right, activebackground=login_panel_right, selectcolor=login_panel_right, fg=login_text, font=("Microsoft YaHei UI", 10))
        admin_radio = tk.Radiobutton(canvas, text=self.t("admin_badge"), variable=role_var, value="admin", bg=login_panel_right, activebackground=login_panel_right, selectcolor=login_panel_right, fg=login_text, font=("Microsoft YaHei UI", 10))
        register("staff_radio", staff_radio)
        register("admin_radio", admin_radio)
        remember_cb = tk.Checkbutton(canvas, text=self.t("login_remember_me"), variable=remember_var, command=on_toggle_remember, bg=login_panel_right, activebackground=login_panel_right, selectcolor=login_panel_right, fg=login_text, font=("Microsoft YaHei UI", 10))
        auto_cb = tk.Checkbutton(canvas, text=self.t("login_auto_login"), variable=auto_var, command=on_toggle_auto, bg=login_panel_right, activebackground=login_panel_right, selectcolor=login_panel_right, fg=login_text, font=("Microsoft YaHei UI", 10))
        register("remember_cb", remember_cb)
        register("auto_cb", auto_cb)

        def _bind_login_press_feedback(button, normal_bg, normal_fg, active_bg, active_fg, hold_ms=260):
            def _press(_event=None):
                try:
                    button.configure(bg=active_bg, fg=active_fg, relief=tk.FLAT)
                except Exception:
                    pass
            def _release(_event=None):
                _pause_login_motion(1100)
                try:
                    button.after(hold_ms, lambda: button.winfo_exists() and button.configure(bg=normal_bg, fg=normal_fg, relief=tk.FLAT))
                except Exception:
                    pass
            try:
                button.bind("<ButtonPress-1>", _press, add="+")
                button.bind("<ButtonRelease-1>", _release, add="+")
                button.bind("<Leave>", lambda _e: button.configure(bg=normal_bg, fg=normal_fg, relief=tk.FLAT), add="+")
            except Exception:
                pass

        def _login_with_feedback():
            original_text = "🔒  " + self.t("login_confirm_btn")
            try:
                login_btn.configure(bg=c["primary_dark"], fg="white", relief=tk.FLAT, text="⏳  " + self.t("login_confirm_btn"))
            except Exception:
                pass
            _pause_login_motion(1400)
            def _run_login_action():
                try:
                    do_login()
                finally:
                    try:
                        login_btn.after(520, lambda: login_btn.winfo_exists() and login_btn.configure(bg=c["primary"], fg="white", relief=tk.FLAT, text=original_text))
                    except Exception:
                        pass
            try:
                self.root.after(180, _run_login_action)
            except Exception:
                _run_login_action()

        login_btn = tk.Button(canvas, text="🔒  " + self.t("login_confirm_btn"), command=_login_with_feedback, relief=tk.FLAT, bd=0, bg=c["primary"], fg="white", activebackground=c["primary_dark"], activeforeground="white", font=("Microsoft YaHei UI", 15, "bold"), cursor="hand2")
        forgot_btn = tk.Button(canvas, text=self.t("forgot_password"), command=do_forgot_password, relief=tk.FLAT, bd=0, bg=login_footer_fill, fg=c["primary"], activebackground=login_widget_active, activeforeground=c["primary_dark"], font=("Microsoft YaHei UI", 11, "bold"), cursor="hand2", anchor="center", padx=8, pady=4)
        personal_create_btn = tk.Button(canvas, text=self.t("register_personal_btn"), command=do_create_personal, relief=tk.FLAT, bd=0, bg=login_footer_fill, fg=c["primary"], activebackground=login_widget_active, activeforeground=c["primary_dark"], font=("Microsoft YaHei UI", 10, "bold"), cursor="hand2", anchor="center", padx=8, pady=4)
        enterprise_create_btn = tk.Button(canvas, text=self.t("register_enterprise_btn"), command=do_create_enterprise, relief=tk.FLAT, bd=0, bg=login_footer_fill, fg=c["primary"], activebackground=login_widget_active, activeforeground=c["primary_dark"], font=("Microsoft YaHei UI", 10, "bold"), cursor="hand2", anchor="center", padx=8, pady=4)
        register("login_btn", login_btn)
        register("forgot_btn", forgot_btn)
        register("personal_create_btn", personal_create_btn)
        register("enterprise_create_btn", enterprise_create_btn)
        _bind_login_press_feedback(login_btn, c["primary"], "white", c["primary_dark"], "white", hold_ms=420)
        _bind_login_press_feedback(forgot_btn, login_footer_fill, c["primary"], login_widget_active, c["primary_dark"], hold_ms=280)
        _bind_login_press_feedback(personal_create_btn, login_footer_fill, c["primary"], login_widget_active, c["primary_dark"], hold_ms=280)
        _bind_login_press_feedback(enterprise_create_btn, login_footer_fill, c["primary"], login_widget_active, c["primary_dark"], hold_ms=280)

        def _bind_login_hover_feedback(button, normal_bg, normal_fg, hover_bg, hover_fg):
            try:
                button.bind("<Enter>", lambda _e: button.configure(bg=hover_bg, fg=hover_fg), add="+")
                button.bind("<Leave>", lambda _e: button.configure(bg=normal_bg, fg=normal_fg, relief=tk.FLAT), add="+")
            except Exception:
                pass

        for _action_btn in (forgot_btn, personal_create_btn, enterprise_create_btn):
            _bind_login_hover_feedback(_action_btn, login_footer_fill, c["primary"], _mix(login_widget_active, c["primary_light"], 0.35), c["primary_dark"])

        def draw_dashboard(x, y, w, h, s):
            """Clean native dashboard preview used on the login page.

            This is intentionally a simplified mock dashboard so the preview
            reads like the reference design without tiny text collisions.
            """
            c = self.colors
            rounded_rect(x, y, x+w, y+h, 18*s, login_panel_right, login_card_border, tag="login_shape")
            # subtle shadow/highlight inside preview
            canvas.create_rectangle(x+1*s, y+1*s, x+w-1*s, y+2*s, fill=login_panel_right, outline="", tags="login_shape")

            side_w = 160*s
            rounded_rect(x+12*s, y+12*s, x+side_w, y+h-12*s, 12*s, login_panel_left, login_card_border, tag="login_shape")
            # Mini NexaOffice logo mark + full brand in the preview sidebar.
            mini_logo_x, mini_logo_y = x+18*s, y+18*s
            try:
                preview_logo_size = max(24, int(28*s))
                mini_logo = Image.open(BASE_DIR / "assets" / "logo_nexa_mark_256.png").convert("RGBA")
                mini_logo.thumbnail((preview_logo_size, preview_logo_size), Image.LANCZOS)
                runtime["preview_logo"] = ImageTk.PhotoImage(mini_logo)
                canvas.create_image(mini_logo_x, mini_logo_y, anchor="nw", image=runtime["preview_logo"], tags="login_shape")
            except Exception:
                canvas.create_text(x+31*s, y+35*s, text="N", fill=c["primary"], font=("Microsoft YaHei UI", max(10, int(15*s)), "bold"), anchor="center", tags="login_shape")
            canvas.create_text(mini_logo_x+38*s, mini_logo_y+4*s, text="NexaOffice", fill=c["text"], font=("Microsoft YaHei UI", max(8, int(10*s)), "bold"), anchor="nw", tags="login_shape")
            canvas.create_text(mini_logo_x+39*s, mini_logo_y+22*s, text="Workspace", fill=login_secondary, font=("Microsoft YaHei UI", max(5, int(6*s))), anchor="nw", tags="login_shape")

            nav = [
                ("⌂", "Dashboard", True),
                ("◷", "Calendar", False),
                ("▤", "Documents", False),
                ("◌", "Chat", False),
                ("⚭", "Team", False),
                ("AI", "AI Assistant", False),
                ("⚙", "Settings", False),
            ]
            yy = y + 86*s
            for icon, label, active in nav:
                fill = c["primary_light"] if active else login_panel_left
                fg = c["primary"] if active else login_secondary
                rounded_rect(x+18*s, yy-14*s, x+side_w-10*s, yy+14*s, 7*s, fill, "", tag="login_shape")
                canvas.create_text(x+30*s, yy, text=icon, fill=fg, font=("Microsoft YaHei UI", max(6, int(8*s)), "bold"), anchor="w", tags="login_shape")
                canvas.create_text(x+54*s, yy, text=label[:14], fill=fg, font=("Microsoft YaHei UI", max(5, int(7*s)), "bold" if active else "normal"), anchor="w", width=side_w-68*s, tags="login_shape")
                yy += 28*s

            main_x = x + side_w + 24*s
            main_w = w - side_w - 42*s
            canvas.create_text(main_x, y+36*s, text=self.t("login_dashboard_preview_greeting"), fill=c["text"], font=("Microsoft YaHei UI", max(8, int(11*s)), "bold"), anchor="w", tags="login_shape")
            canvas.create_oval(x+w-42*s, y+25*s, x+w-22*s, y+45*s, fill=login_warning_soft, outline=login_panel_right, tags="login_shape")

            stats = [
                ("12", self.t("login_stat_tasks"), c["primary"]),
                ("8", self.t("login_stat_docs"), c["success"]),
                ("5", self.t("login_stat_messages"), "#7c3aed"),
                ("3", self.t("login_stat_events"), c["warning"]),
            ]
            gap = 10*s
            tile_w = (main_w - 3*gap) / 4
            tile_y = y + 64*s
            for i, (num, lab, fg) in enumerate(stats):
                tx = main_x + i * (tile_w + gap)
                rounded_rect(tx, tile_y, tx+tile_w, tile_y+58*s, 9*s, login_panel_right, login_card_border, tag="login_shape")
                canvas.create_text(tx+12*s, tile_y+23*s, text=num, fill=fg, font=("Microsoft YaHei UI", max(10, int(17*s)), "bold"), anchor="w", tags="login_shape")
                canvas.create_text(tx+12*s, tile_y+43*s, text=lab[:15], fill=login_secondary, font=("Microsoft YaHei UI", max(5, int(6*s))), anchor="w", tags="login_shape")

            body_y = tile_y + 76*s
            body_h = max(92*s, h - (body_y - y) - 22*s)
            doc_w = main_w * 0.54
            cal_w = main_w - doc_w - 12*s
            doc_x = main_x
            cal_x = doc_x + doc_w + 12*s
            rounded_rect(doc_x, body_y, doc_x+doc_w, body_y+body_h, 10*s, login_panel_right, login_card_border, tag="login_shape")
            rounded_rect(cal_x, body_y, cal_x+cal_w, body_y+body_h, 10*s, login_panel_right, login_card_border, tag="login_shape")

            canvas.create_text(doc_x+14*s, body_y+22*s, text=self.t("login_recent_docs"), fill=c["text"], font=("Microsoft YaHei UI", max(7, int(9*s)), "bold"), anchor="w", tags="login_shape")
            docs = [
                ("Project Proposal Q2.pdf", "2 hours ago", "#ef4444"),
                ("Marketing Report May.xlsx", "5 hours ago", "#22c55e"),
                ("Product Roadmap 2026.pptx", "1 day ago", "#f97316"),
                ("Meeting Notes 06-15.docx", "2 days ago", "#2563eb"),
            ]
            row_y = body_y + 50*s
            for name, when, color in docs:
                if row_y > body_y + body_h - 28*s:
                    break
                canvas.create_rectangle(doc_x+14*s, row_y-7*s, doc_x+23*s, row_y+3*s, fill=color, outline="", tags="login_shape")
                canvas.create_text(doc_x+32*s, row_y, text=name[:22], fill=c["text"], font=("Microsoft YaHei UI", max(5, int(6*s))), anchor="w", tags="login_shape")
                canvas.create_text(doc_x+doc_w-70*s, row_y, text=when, fill=login_placeholder, font=("Microsoft YaHei UI", max(4, int(5*s))), anchor="w", tags="login_shape")
                row_y += 23*s
            canvas.create_text(doc_x+14*s, body_y+body_h-18*s, text=self.t("login_view_all_documents"), fill=c["primary"], font=("Microsoft YaHei UI", max(5, int(6*s)), "bold"), anchor="w", tags="login_shape")

            canvas.create_text(cal_x+12*s, body_y+20*s, text="Calendar", fill=c["text"], font=("Microsoft YaHei UI", max(6, int(8*s)), "bold"), anchor="w", tags="login_shape")
            canvas.create_text(cal_x+cal_w-72*s, body_y+20*s, text="June 2026", fill=c["text"], font=("Microsoft YaHei UI", max(5, int(6*s)), "bold"), anchor="w", tags="login_shape")
            days = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]
            grid_x = cal_x + 12*s
            grid_y = body_y + 44*s
            cell_w = (cal_w - 24*s) / 7
            for i, d in enumerate(days):
                canvas.create_text(grid_x+i*cell_w+cell_w/2, grid_y, text=d, fill=login_secondary, font=("Microsoft YaHei UI", max(4, int(5*s))), tags="login_shape")
            nums = [[31,1,2,3,4,5,6],[7,8,9,10,11,12,13],[14,15,16,17,18,19,20],[21,22,23,24,25,26,27],[28,29,30,1,2,3,4]]
            for r, row in enumerate(nums):
                for col, num in enumerate(row):
                    cx = grid_x + col*cell_w + cell_w/2
                    cy = grid_y + (r+1)*17*s
                    if num == 17:
                        canvas.create_oval(cx-8*s, cy-8*s, cx+8*s, cy+8*s, fill=c["primary"], outline="", tags="login_shape")
                        fill = "white"
                    else:
                        fill = c["text"]
                    canvas.create_text(cx, cy, text=str(num), fill=fill, font=("Microsoft YaHei UI", max(4, int(6*s)), "bold" if num == 17 else "normal"), tags="login_shape")

            team_y = body_y + body_h - 45*s
            if team_y > grid_y + 95*s:
                canvas.create_text(cal_x+12*s, team_y, text=self.t("login_team_activity"), fill=c["text"], font=("Microsoft YaHei UI", max(5, int(7*s)), "bold"), anchor="w", tags="login_shape")
                for i, color in enumerate(["#f97316", "#22c55e", "#3b82f6"]):
                    canvas.create_oval(cal_x+12*s+i*13*s, team_y+16*s, cal_x+23*s+i*13*s, team_y+27*s, fill=color, outline="white", tags="login_shape")
                canvas.create_text(cal_x+58*s, team_y+20*s, text="Rapat Mingguan Tim Produk", fill=c["text"], font=("Microsoft YaHei UI", max(4, int(5*s))), anchor="w", tags="login_shape")
        def draw_login(event=None):
            canvas.configure(bg=login_bg)
            self.root.configure(bg=login_bg)
            canvas.delete("login_shape")
            # v2026.06.20.16: login uses the real viewport, not a fake
            # minimum canvas. The previous min(900x620) fallback could draw a
            # card larger than a split-screen / high-DPI window, which made
            # some controls look clipped while others looked over-scaled.
            view_w = int(canvas.winfo_width() or self.root.winfo_width() or 760)
            view_h = int(canvas.winfo_height() or self.root.winfo_height() or 520)
            cw = max(720, view_w)
            ch = max(500, view_h)
            # v2026.06.20.43: responsive login profiles.  The previous cap
            # kept fonts/controls small even when the card itself was enlarged.
            compact_login = cw < 1180 or ch < 720
            tiny_login = cw < 900 or ch < 610
            base_scale = min(cw / 1366.0, ch / 768.0)
            if tiny_login:
                login_profile = "tiny"
                s = max(0.86, min(1.02, base_scale))
            elif compact_login:
                login_profile = "compact"
                s = max(0.94, min(1.12, base_scale))
            else:
                login_profile = "wide"
                s = max(1.02, min(1.26, base_scale))
            text_scale = max(0.96, min(1.20, s))
            c = self.colors
            def f(size, weight="normal"):
                return ("Microsoft YaHei UI", max(8, int(round(size * text_scale))), weight)
            # background - v2026.06.20.100: milestone track, upgraded kinetic glass aurora style.
            # This keeps the larger visible motion the user requested, and adds
            # an adaptive intensity layer so all themes can feel alive while slow
            # devices automatically back off instead of freezing the animation.
            canvas.create_rectangle(0, 0, cw, ch, fill=login_bg, outline="", tags="login_shape")
            motion_enabled = bool(self.user_settings.get("login_background_motion", True)) and login_profile in {"wide", "compact"}
            intensity_name = str(self.user_settings.get("login_background_motion_intensity", "vivid") or "vivid").lower()
            intensity_scale = {"calm": 0.88, "balanced": 1.20, "vivid": 1.58}.get(intensity_name, 1.58)
            if str(self.user_settings.get("performance_tier", "auto")) == "low_power":
                intensity_scale = min(intensity_scale, 0.78)
            anim_phase = (time.monotonic() * (0.24 * max(0.70, min(1.35, intensity_scale)))) if motion_enabled else 0.0
            motion = intensity_scale if motion_enabled else 0.0
            for pad in (0, 24, 52):
                canvas.create_arc(-160*s-pad*s, -118*s-pad*s, 272*s+pad*s, 268*s+pad*s, start=20, extent=126, style=tk.ARC, outline=_mix(login_card_border, c["primary_light"], 0.30), width=max(1, int(1.8*s)), tags="login_shape")
            canvas.create_oval(-0.07*cw, 0.03*ch, 0.20*cw, 0.36*ch, fill=_mix(login_bg_alt, c["card"], 0.12), outline="", tags="login_shape")
            canvas.create_oval(0.34*cw, -0.08*ch, 0.60*cw, 0.22*ch, fill=_mix(c["primary_light"], "#ffffff", 0.74 if dark_login else 0.82), outline="", tags="login_shape")
            glow_fill = _mix(c["primary_light"], "#ffffff", 0.74 if dark_login else 0.88)
            canvas.create_oval(0.60*cw, 0.12*ch, 0.82*cw, 0.34*ch, fill=_mix(glow_fill, login_bg_alt, 0.82), outline="", tags="login_shape")
            canvas.create_oval(0.18*cw, 0.42*ch, 0.36*cw, 0.64*ch, fill=_mix(glow_fill, login_bg, 0.92 if dark_login else 0.78), outline="", tags="login_shape")
            canvas.create_oval(0.84*cw, 0.16*ch, 1.04*cw, 0.48*ch, fill=_mix(login_bg_alt, c["primary_light"], 0.18), outline="", tags="login_shape")
            canvas.create_oval(0.80*cw, 0.72*ch, 1.02*cw, 1.02*ch, fill=_mix(login_bg_alt, c["card"], 0.18), outline="", tags="login_shape")
            canvas.create_oval(-0.05*cw, 0.84*ch, 0.15*cw, 1.04*ch, fill=_mix(login_bg_alt, c["card"], 0.16), outline="", tags="login_shape")
            top_shift = math.sin(anim_phase * 1.20) * 34.0 * s * motion
            mid_shift = math.cos(anim_phase * 0.96) * 36.0 * s * motion
            bottom_shift = math.sin(anim_phase * 1.06 + 0.9) * 42.0 * s * motion
            center_shift = math.cos(anim_phase * 0.86 + 0.35) * 30.0 * s * motion
            bottom_fill = _mix(c["primary_light"], login_bg, 0.78 if dark_login else 0.54)
            accent_fill = _mix(c["primary_light"], "#ffffff", 0.86 if dark_login else 0.92)
            canvas.create_oval(cw*0.88 + mid_shift*0.10, ch*0.24, cw*1.06 + mid_shift*0.10, ch*0.48, fill=_mix(accent_fill, login_bg_alt, 0.72), outline="", tags="login_shape")
            canvas.create_oval(cw*0.02 - center_shift*0.08, ch*0.84, cw*0.18 - center_shift*0.08, ch*1.02, fill=_mix(accent_fill, login_bg_alt, 0.78), outline="", tags="login_shape")
            rounded_rect(cw*0.80 + mid_shift*0.04, ch*0.32, cw*0.98 + mid_shift*0.04, ch*0.36, 18*s, _mix(c["primary_light"], "#ffffff", 0.88), "", tag="login_shape")
            rounded_rect(cw*0.06 + top_shift*0.04, ch*0.80, cw*0.16 + top_shift*0.04, ch*0.84, 18*s, _mix(c["primary_light"], "#ffffff", 0.84), "", tag="login_shape")
            band_y = ch*0.90 + bottom_shift*0.14
            canvas.create_polygon(
                0, band_y,
                cw*0.18, ch*0.95,
                cw*0.42, ch*0.92 - bottom_shift*0.14,
                cw*0.68, ch*0.97,
                cw, ch*0.90 + bottom_shift*0.10,
                cw, ch,
                0, ch,
                smooth=True, splinesteps=28, fill=bottom_fill, outline="", tags="login_shape")
            orb1_x = cw*0.73 + math.sin(anim_phase * 0.96) * 46*s*motion
            orb1_y = ch*0.19 + math.cos(anim_phase * 0.78) * 32*s*motion
            orb2_x = cw*0.26 + math.cos(anim_phase * 0.72) * 40*s*motion
            orb2_y = ch*0.72 + math.sin(anim_phase * 0.66) * 30*s*motion
            orb3_x = cw*0.11 + math.sin(anim_phase * 0.58 + 0.8) * 34*s*motion
            orb3_y = ch*0.22 + math.cos(anim_phase * 0.62 + 0.4) * 24*s*motion
            canvas.create_oval(orb1_x-15*s, orb1_y-15*s, orb1_x+15*s, orb1_y+15*s, fill=_mix(c["primary_light"], "#ffffff", 0.80), outline="", tags="login_shape")
            canvas.create_oval(orb2_x-10*s, orb2_y-10*s, orb2_x+10*s, orb2_y+10*s, fill=_mix(c["primary_light"], login_bg_alt, 0.68), outline="", tags="login_shape")
            canvas.create_oval(orb3_x-8*s, orb3_y-8*s, orb3_x+8*s, orb3_y+8*s, fill=_mix(c["primary_light"], "#ffffff", 0.86), outline="", tags="login_shape")
            for row in range(4):
                for col in range(5):
                    offset = (math.sin(anim_phase * 1.14 + (row+col)*0.35) * 1.1 * s * motion)
                    canvas.create_oval(cw*0.915+col*16*s, ch*0.06+row*16*s+offset, cw*0.915+col*16*s+4*s, ch*0.06+row*16*s+4*s+offset, fill=login_dots, outline="", tags="login_shape")
            for row in range(4):
                for col in range(7):
                    offset = (math.cos(anim_phase * 0.92 + (row+col)*0.30) * 0.9 * s * motion)
                    canvas.create_oval(cw*0.034+col*16*s, ch*0.78+row*16*s+offset, cw*0.034+col*16*s+4*s, ch*0.78+row*16*s+4*s+offset, fill=login_dots, outline="", tags="login_shape")
            def configure_login_widget_fonts():
                # Keep real Tk widgets in the same scale as the drawn canvas.
                # This prevents mixed sizes on different Windows DPI settings.
                for key in ("username_entry", "password_entry"):
                    if key in win_ids:
                        try:
                            canvas.nametowidget(canvas.itemcget(win_ids[key], "window")).configure(font=f(12))
                        except Exception:
                            pass
                for key, size in (("login_lang_menu", 9), ("login_theme_menu", 9), ("staff_radio", 9), ("admin_radio", 9), ("remember_cb", 9), ("auto_cb", 9), ("eye", 10)):
                    if key in win_ids:
                        try:
                            canvas.nametowidget(canvas.itemcget(win_ids[key], "window")).configure(font=f(size, "bold" if key in {"login_lang_menu", "login_theme_menu", "eye"} else "normal"))
                        except Exception:
                            pass
                try:
                    login_btn.configure(font=f(14, "bold"), relief=tk.FLAT, bd=0, highlightthickness=0)
                    forgot_btn.configure(font=f(10, "bold"))
                    personal_create_btn.configure(font=f(9, "bold"))
                    enterprise_create_btn.configure(font=f(9, "bold"))
                except Exception:
                    pass
                for chip in chip_widgets:
                    try:
                        for child in chip.winfo_children():
                            child.configure(font=f(9, "bold"))
                    except Exception:
                        pass

            def place_recent_chips(x, y, chip_w, chip_h, gap):
                if "chip_0" in win_ids:
                    set_win("chip_0", x, y, chip_w, chip_h)
                if "chip_add" in win_ids:
                    set_win("chip_add", x + chip_w + gap, y, chip_w, chip_h)
                for i in range(1, 2):
                    key = f"chip_{i}"
                    if key in win_ids:
                        set_win(key, -1000, -1000, 1, 1)

            if compact_login:
                # Compact / split-screen login: hide the marketing preview and
                # use a single readable card. Controls stay large enough, and
                # the layout fits both low-height laptops and high-DPI scaling.
                pad = max(20, 26 * s)
                # v2026.06.20.55: compact profile uses a smaller but still safe
                # centered shell.  The goal is to avoid a full-window feeling
                # while preserving all required controls on split-screen and
                # high-DPI laptops.
                target_w = cw * (1.00 if tiny_login else 0.96)
                target_h = ch * (0.95 if tiny_login else 0.91)
                shell_w = min(cw - pad * 2, max(650 if tiny_login else 820, target_w))
                shell_h = min(ch - pad * 2, max(540 if tiny_login else 620, target_h))
                x0 = (cw - shell_w) / 2
                y0 = (ch - shell_h) / 2
                rounded_rect(x0+7*s, y0+9*s, x0+shell_w+7*s, y0+shell_h+9*s, 24*s, login_shadow, "", tag="login_shape")
                rounded_rect(x0, y0, x0+shell_w, y0+shell_h, 24*s, login_panel_right, login_card_border, tag="login_shape")
                rounded_rect(x0+18*s, y0+15*s, x0+shell_w-18*s, y0+21*s, 4*s, _mix(c["primary_light"], c["primary"], 0.30), "", tag="login_shape")
                # Top brand / controls
                top_y = y0 + 22*s
                logo_x = x0 + 26*s
                try:
                    logo_size = max(34, int(42*s))
                    logo = Image.open(BASE_DIR / "assets" / "logo_nexa_mark_256.png").convert("RGBA")
                    logo.thumbnail((logo_size, logo_size), Image.LANCZOS)
                    runtime["logo"] = ImageTk.PhotoImage(logo)
                    canvas.create_image(logo_x, top_y+2*s, anchor="nw", image=runtime["logo"], tags="login_shape")
                except Exception:
                    canvas.create_text(logo_x, top_y+9*s, text="N", fill=c["primary"], font=f(20, "bold"), anchor="nw", tags="login_shape")
                canvas.create_text(logo_x+54*s, top_y-2*s, text="NexaOffice", fill=c["text"], font=f(20, "bold"), anchor="nw", tags="login_shape")
                canvas.create_text(logo_x+56*s, top_y+34*s, text=f"Work Suite · {self.app_version}", fill=login_secondary, font=f(8), anchor="nw", tags="login_shape")
                menu_y = y0 + 20*s
                lang_w = 76*s if not tiny_login else 64*s
                theme_w = min(140*s, max(108*s, shell_w - 270*s))
                set_win("login_lang_menu", x0 + shell_w - 24*s - lang_w, menu_y, lang_w, 34*s)
                if not tiny_login and shell_w > 610:
                    set_win("login_theme_menu", x0 + shell_w - 32*s - lang_w - theme_w, menu_y, theme_w, 34*s)
                elif "login_theme_menu" in win_ids:
                    set_win("login_theme_menu", -1000, -1000, 1, 1)

                content_max_w = min(540*s, shell_w - max(40*s, 34))
                content_x = x0 + (shell_w - content_max_w) / 2
                field_w = content_max_w
                title_y = y0 + (82*s if not tiny_login else 74*s)
                canvas.create_text(content_x, title_y, text=self.t("login_btn"), fill=c["text"], font=f(23 if not tiny_login else 21, "bold"), anchor="nw", tags="login_shape")
                show_subtitle = ch >= 570 and shell_h >= 520
                if show_subtitle:
                    canvas.create_text(content_x, title_y+44*s, text=self.t("login_panel_subtitle"), fill=login_secondary, font=f(9), anchor="nw", width=field_w, tags="login_shape")
                cursor_y = title_y + (86*s if show_subtitle else 56*s)

                if chip_data and ch >= 600 and shell_h >= 540:
                    canvas.create_text(content_x, cursor_y, text=self.t("login_recent_accounts"), fill=c["text"], font=f(9, "bold"), anchor="nw", tags="login_shape")
                    cursor_y += 26*s
                    chip_gap = 10*s
                    chip_w = (field_w - chip_gap) / 2
                    place_recent_chips(content_x, cursor_y, chip_w, 34*s, chip_gap)
                    cursor_y += 50*s
                else:
                    place_recent_chips(-1000, -1000, 1, 1, 0)

                canvas.create_text(content_x, cursor_y, text=self.t("username_label"), fill=c["text"], font=f(9, "bold"), anchor="nw", tags="login_shape")
                box_y = cursor_y + 23*s
                input_h = 40*s
                rounded_rect(content_x, box_y, content_x+field_w, box_y+input_h, 8*s, login_input_bg, login_input_border, tag="login_shape")
                canvas.create_text(content_x+18*s, box_y+input_h/2, text="♙", fill=login_icon, font=f(13), anchor="w", tags="login_shape")
                set_win("username_entry", content_x+50*s, box_y+7*s, field_w-64*s, 30*s)
                cursor_y = box_y + 56*s

                canvas.create_text(content_x, cursor_y, text=self.t("account_password"), fill=c["text"], font=f(9, "bold"), anchor="nw", tags="login_shape")
                pass_box_y = cursor_y + 23*s
                rounded_rect(content_x, pass_box_y, content_x+field_w, pass_box_y+input_h, 8*s, login_input_bg, login_input_border, tag="login_shape")
                canvas.create_text(content_x+18*s, pass_box_y+input_h/2, text="▣", fill=login_icon, font=f(11), anchor="w", tags="login_shape")
                set_win("password_entry", content_x+50*s, pass_box_y+7*s, field_w-98*s, 30*s)
                set_win("eye", content_x+field_w-38*s, pass_box_y+7*s, 32*s, 30*s)
                cursor_y = pass_box_y + 50*s

                set_win("staff_radio", -1000, -1000, 1, 1)
                set_win("admin_radio", -1000, -1000, 1, 1)
                opts_y = cursor_y + 8*s
                cb_gap = min(208*s, field_w * 0.52)
                set_win("remember_cb", content_x, opts_y, min(188*s, field_w*0.48), 30*s)
                set_win("auto_cb", content_x + cb_gap, opts_y, min(188*s, field_w*0.45), 30*s)
                btn_y = opts_y + 50*s
                min_footer_space = 120*s if shell_h >= 555 else 78*s
                btn_y = min(btn_y, y0 + shell_h - min_footer_space)
                btn_y = max(btn_y, opts_y + 42*s)
                btn_h = 46*s
                set_win("login_btn", content_x, btn_y, field_w, btn_h)
                if btn_y + 116*s < y0 + shell_h:
                    footer_card_y = btn_y + 50*s
                    rounded_rect(content_x, footer_card_y, content_x+field_w, footer_card_y+74*s, 12*s, login_footer_fill, login_card_border, tag="login_shape")
                    rounded_rect(content_x+8*s, footer_card_y+8*s, content_x+field_w-8*s, footer_card_y+34*s, 8*s, login_footer_fill, login_card_border, tag="login_shape")
                    rounded_rect(content_x+8*s, footer_card_y+40*s, content_x+field_w/2-4*s, footer_card_y+66*s, 8*s, login_footer_fill, login_card_border, tag="login_shape")
                    rounded_rect(content_x+field_w/2+4*s, footer_card_y+40*s, content_x+field_w-8*s, footer_card_y+66*s, 8*s, login_footer_fill, login_card_border, tag="login_shape")
                    set_win("forgot_btn", content_x + 10*s, footer_card_y+10*s, field_w-20*s, 22*s)
                    set_win("personal_create_btn", content_x+10*s, footer_card_y+42*s, field_w/2-18*s, 22*s)
                    set_win("enterprise_create_btn", content_x+field_w/2+8*s, footer_card_y+42*s, field_w/2-18*s, 22*s)
                elif btn_y + 74*s < y0 + shell_h:
                    rounded_rect(content_x, btn_y+50*s, content_x+field_w, btn_y+84*s, 12*s, login_footer_fill, login_card_border, tag="login_shape")
                    set_win("forgot_btn", content_x + 8*s, btn_y+56*s, field_w-16*s, 22*s)
                    set_win("personal_create_btn", -1000, -1000, 1, 1)
                    set_win("enterprise_create_btn", -1000, -1000, 1, 1)
                else:
                    set_win("forgot_btn", -1000, -1000, 1, 1)
                    set_win("personal_create_btn", -1000, -1000, 1, 1)
                    set_win("enterprise_create_btn", -1000, -1000, 1, 1)
                configure_login_widget_fonts()
                return

            # v2026.06.20.101: desktop login page redesigned to match the latest
            # visual reference more closely.  The page uses a centered glass card,
            # a cleaner split panel, six capability tiles, and a softer form area.
            shell_margin_x = max(56, int(68 * s))
            shell_margin_y = max(24, int(30 * s))
            shell_w = min(cw - shell_margin_x * 2, max(1360, cw * 0.785))
            shell_h = min(ch - shell_margin_y * 2, max(800, ch * 0.895))
            x0 = (cw - shell_w) / 2
            y0 = (ch - shell_h) / 2
            left_w = shell_w * 0.505
            right_w = shell_w - left_w
            rounded_rect(x0+10*s, y0+12*s, x0+shell_w+10*s, y0+shell_h+12*s, 28*s, _mix(login_shadow, "#ffffff", 0.58), "", tag="login_shape")
            rounded_rect(x0, y0, x0+shell_w, y0+shell_h, 28*s, login_panel_right, login_card_border, tag="login_shape")
            canvas.create_rectangle(x0+1*s, y0+1*s, x0+left_w, y0+shell_h-1*s, fill=_mix(login_panel_left, "#f7f9fd", 0.76), outline="", tags="login_shape")
            canvas.create_rectangle(x0+left_w, y0+1*s, x0+shell_w-1*s, y0+shell_h-1*s, fill=login_panel_right, outline="", tags="login_shape")
            canvas.create_rectangle(x0+left_w-1*s, y0+30*s, x0+left_w+1*s, y0+shell_h-30*s, fill=_mix(login_card_border, "#ffffff", 0.40), outline="", tags="login_shape")

            # left hero panel
            lx = x0 + 52*s
            ly = y0 + 48*s
            logo_path = BASE_DIR / "assets" / "logo_nexa_mark_256.png"
            try:
                logo_size = max(52, int(58*s))
                logo = Image.open(logo_path).convert("RGBA")
                logo.thumbnail((logo_size, logo_size), Image.LANCZOS)
                runtime["logo"] = ImageTk.PhotoImage(logo)
                canvas.create_image(lx, ly+2*s, anchor="nw", image=runtime["logo"], tags="login_shape")
            except Exception:
                canvas.create_text(lx, ly+18*s, text="N", fill=c["primary"], font=f(34, "bold"), anchor="nw", tags="login_shape")
            canvas.create_text(lx+104*s, ly+6*s, text="NexaOffice", fill=c["text"], font=f(25, "bold"), anchor="nw", tags="login_shape")
            canvas.create_text(lx+106*s, ly+44*s, text=f"Work Suite · {self.app_version}", fill=login_secondary, font=f(8.6), anchor="nw", tags="login_shape")

            pill_w = min(350*s, left_w-116*s)
            pill_h = 40*s
            pill_x = lx
            pill_y = y0 + 118*s
            rounded_rect(pill_x, pill_y, pill_x+pill_w, pill_y+pill_h, 18*s, _mix(c["primary_light"], "#ffffff", 0.82), _mix(c["primary_light"], "#ffffff", 0.52), tag="login_shape")
            canvas.create_text(pill_x+18*s, pill_y+pill_h/2, text="👋", fill=c["primary"], font=f(10, "bold"), anchor="w", tags="login_shape")
            canvas.create_text(pill_x+42*s, pill_y+pill_h/2, text=self.t("login_enterprise_welcome"), fill=c["primary"], font=f(9.4, "bold"), anchor="w", tags="login_shape")

            title_x = lx
            title_y = y0 + 172*s
            title_width = left_w - 110*s
            canvas.create_text(title_x, title_y, text=self.t("login_hero_title"), fill=c["text"], font=f(28, "bold"), anchor="nw", width=title_width, tags="login_shape")
            subtitle_y = title_y + 118*s
            canvas.create_text(title_x, subtitle_y, text=self.t("login_hero_subtitle"), fill=login_secondary, font=f(10.2), anchor="nw", width=title_width+10*s, tags="login_shape")

            feature_y = subtitle_y + 98*s
            drive_label = self.t("login_stat_drive")
            feature_specs = [
                ("✓", self.t("login_stat_tasks")),
                ("◎", self.t("login_stat_docs")),
                ("✉", self.t("login_stat_messages")),
                ("◷", self.t("login_stat_events")),
                ("☁", drive_label),
                ("AI", self.t("login_stat_assistant")),
            ]
            grid_x = lx
            grid_w = left_w - 104*s
            chip_gap_x = 16*s
            chip_gap_y = 14*s
            chip_cols = 3
            chip_w = (grid_w - chip_gap_x * (chip_cols - 1)) / chip_cols
            chip_h = 58*s
            for idx, (icon, label) in enumerate(feature_specs):
                row = idx // chip_cols
                col = idx % chip_cols
                fx = grid_x + col * (chip_w + chip_gap_x)
                fy = feature_y + row * (chip_h + chip_gap_y)
                rounded_rect(fx, fy, fx+chip_w, fy+chip_h, 14*s, login_panel_right, _mix(login_card_border, "#ffffff", 0.40), tag="login_shape")
                canvas.create_text(fx+20*s, fy+chip_h/2, text=icon, fill=c["primary"], font=f(10.2, "bold"), anchor="w", tags="login_shape")
                canvas.create_text(fx+48*s, fy+chip_h/2, text=label, fill=c["text"], font=f(9.2, "bold"), anchor="w", width=chip_w-62*s, tags="login_shape")

            pv_h = max(324*s, min(376*s, shell_h * 0.438))
            pv_x = lx
            pv_w = left_w - 106*s
            pv_target_y = feature_y + (chip_h + chip_gap_y) * 2 + 88*s
            pv_bottom_y = y0 + shell_h - pv_h - 32*s
            pv_y = min(max(pv_target_y, y0 + shell_h * 0.492), pv_bottom_y)
            pv_y = max(pv_y, feature_y + (chip_h + chip_gap_y) * 2 + 46*s)
            draw_dashboard(pv_x, pv_y, pv_w, pv_h, min(s * 0.72, pv_h / 336.0))

            # right form panel
            field_w = min(right_w - 120*s, max(548*s, right_w * 0.84))
            form_x = x0 + left_w + 54*s
            rx = form_x
            ry = y0 + 36*s
            util_theme_w = 198*s
            util_lang_w = 94*s
            util_gap = 14*s
            util_y = ry + 6*s
            util_theme_x = x0 + shell_w - 56*s - util_lang_w - util_gap - util_theme_w
            util_lang_x = util_theme_x + util_theme_w + util_gap
            rounded_rect(util_theme_x, util_y, util_theme_x+util_theme_w, util_y+42*s, 12*s, login_panel_right, login_card_border, tag="login_shape")
            rounded_rect(util_lang_x, util_y, util_lang_x+util_lang_w, util_y+42*s, 12*s, login_panel_right, login_card_border, tag="login_shape")
            set_win("login_theme_menu", util_theme_x+8*s, util_y+6*s, util_theme_w-16*s, 28*s)
            set_win("login_lang_menu", util_lang_x+6*s, util_y+6*s, util_lang_w-12*s, 28*s)
            try:
                theme_menu_btn.configure(bg=login_panel_right, activebackground=login_widget_active, fg=login_text, activeforeground=c["primary"], highlightthickness=0, bd=0, relief=tk.FLAT, padx=8, pady=4)
                lang_menu_btn.configure(bg=login_panel_right, activebackground=login_widget_active, fg=login_text, activeforeground=c["primary"], highlightthickness=0, bd=0, relief=tk.FLAT, padx=8, pady=4)
                theme_menu.configure(bg=login_panel_right, fg=login_text, activebackground=login_widget_active, activeforeground=c["primary"], relief=tk.FLAT, bd=0)
                lang_menu.configure(bg=login_panel_right, fg=login_text, activebackground=login_widget_active, activeforeground=c["primary"], relief=tk.FLAT, bd=0)
                remember_cb.configure(bg=login_panel_right, activebackground=login_panel_right, selectcolor=login_panel_right, fg=login_text, activeforeground=c["primary"], highlightthickness=0)
                auto_cb.configure(bg=login_panel_right, activebackground=login_panel_right, selectcolor=login_panel_right, fg=login_text, activeforeground=c["primary"], highlightthickness=0)
                action_hover_bg = _mix(login_widget_active, c["primary_light"], 0.35)
                forgot_btn.configure(bg=login_footer_fill, activebackground=action_hover_bg, fg=c["primary"], activeforeground=c["primary_dark"], highlightthickness=0, relief=tk.FLAT, bd=0, cursor="hand2")
                personal_create_btn.configure(bg=login_footer_fill, activebackground=action_hover_bg, fg=c["primary"], activeforeground=c["primary_dark"], highlightthickness=0, relief=tk.FLAT, bd=0, cursor="hand2")
                enterprise_create_btn.configure(bg=login_footer_fill, activebackground=action_hover_bg, fg=c["primary"], activeforeground=c["primary_dark"], highlightthickness=0, relief=tk.FLAT, bd=0, cursor="hand2")
            except Exception:
                pass

            title_x = rx
            title_y = y0 + 138*s
            canvas.create_text(title_x, title_y, text=self.t("login_btn"), fill=c["text"], font=f(34, "bold"), anchor="nw", tags="login_shape")
            canvas.create_text(title_x, title_y+74*s, text=self.t("login_panel_subtitle"), fill=login_secondary, font=f(10.6), anchor="nw", width=field_w, tags="login_shape")

            recent_y = title_y + 172*s
            canvas.create_text(rx, recent_y, text=self.t("login_recent_accounts"), fill=c["text"], font=f(10.4, "bold"), anchor="nw", tags="login_shape")
            chip_y = recent_y + 34*s
            recent_chip_w = min(184*s, (field_w - 14*s) / 2)
            place_recent_chips(rx, chip_y, recent_chip_w, 38*s, 14*s)

            label_y = chip_y + 62*s
            canvas.create_text(rx, label_y, text=self.t("username_label"), fill=c["text"], font=f(10.2, "bold"), anchor="nw", tags="login_shape")
            box_y = label_y + 30*s
            input_h = 48*s
            rounded_rect(rx, box_y, rx+field_w, box_y+input_h, 10*s, login_input_bg, login_input_border, tag="login_shape")
            canvas.create_text(rx+18*s, box_y+input_h/2, text="◉", fill=login_icon, font=f(10), anchor="w", tags="login_shape")
            set_win("username_entry", rx+50*s, box_y+8*s, field_w-64*s, 32*s)
            pass_label_y = box_y + 78*s
            canvas.create_text(rx, pass_label_y, text=self.t("account_password"), fill=c["text"], font=f(10.2, "bold"), anchor="nw", tags="login_shape")
            pass_box_y = pass_label_y + 28*s
            rounded_rect(rx, pass_box_y, rx+field_w, pass_box_y+input_h, 10*s, login_input_bg, login_input_border, tag="login_shape")
            canvas.create_text(rx+18*s, pass_box_y+input_h/2, text="◍", fill=login_icon, font=f(10), anchor="w", tags="login_shape")
            set_win("password_entry", rx+50*s, pass_box_y+8*s, field_w-100*s, 32*s)
            set_win("eye", rx+field_w-40*s, pass_box_y+8*s, 32*s, 32*s)
            set_win("staff_radio", -1000, -1000, 1, 1)
            set_win("admin_radio", -1000, -1000, 1, 1)

            opts_y = pass_box_y + 78*s
            remember_w = 180*s
            auto_w = 210*s
            opts_gap = max(22*s, 26*s)
            opts_total_w = remember_w + auto_w + opts_gap
            opts_x = rx + max(0, (field_w - opts_total_w) / 2)
            rounded_rect(opts_x-24*s, opts_y-10*s, opts_x+opts_total_w+24*s, opts_y+40*s, 12*s, _mix(login_panel_right, c["primary_light"], 0.10), _mix(login_card_border, "#ffffff", 0.28), tag="login_shape")
            set_win("remember_cb", opts_x, opts_y, remember_w, 30*s)
            set_win("auto_cb", opts_x+remember_w+opts_gap, opts_y, auto_w, 30*s)

            btn_y = opts_y + 54*s
            btn_h = 58*s
            # No canvas button layer here: the real Tk button is the single visible layer.
            # This prevents the login button from looking doubled/ghosted on high-DPI Windows.
            set_win("login_btn", rx, btn_y, field_w, btn_h)

            # Guard baseline retained for release-policy tooling:
            # footer_y = btn_y + btn_h + 96*s
            # footer_h = 142*s
            footer_h = 148*s
            footer_y = max(btn_y + btn_h + 40*s, y0 + shell_h - footer_h - 28*s)
            footer_y = min(footer_y, y0 + shell_h - footer_h - 16*s)
            rounded_rect(rx, footer_y, rx+field_w, footer_y+footer_h, 14*s, login_footer_fill, login_card_border, tag="login_shape")
            forgot_card_x1 = rx + 16*s
            forgot_card_y1 = footer_y + 16*s
            forgot_card_x2 = rx + field_w - 16*s
            forgot_card_y2 = footer_y + 58*s
            personal_card_x1 = rx + 16*s
            personal_card_y1 = footer_y + 74*s
            personal_card_x2 = rx + field_w/2 - 8*s
            personal_card_y2 = footer_y + 126*s
            enterprise_card_x1 = rx + field_w/2 + 8*s
            enterprise_card_y1 = footer_y + 74*s
            enterprise_card_x2 = rx + field_w - 16*s
            enterprise_card_y2 = footer_y + 126*s
            rounded_rect(forgot_card_x1, forgot_card_y1, forgot_card_x2, forgot_card_y2, 10*s, login_footer_fill, login_card_border, tag="login_shape")
            rounded_rect(personal_card_x1, personal_card_y1, personal_card_x2, personal_card_y2, 10*s, login_footer_fill, login_card_border, tag="login_shape")
            rounded_rect(enterprise_card_x1, enterprise_card_y1, enterprise_card_x2, enterprise_card_y2, 10*s, login_footer_fill, login_card_border, tag="login_shape")
            set_win("forgot_btn", forgot_card_x1+2*s, forgot_card_y1+2*s, forgot_card_x2-forgot_card_x1-4*s, forgot_card_y2-forgot_card_y1-4*s)
            set_win("personal_create_btn", personal_card_x1+2*s, personal_card_y1+2*s, personal_card_x2-personal_card_x1-4*s, personal_card_y2-personal_card_y1-4*s)
            set_win("enterprise_create_btn", enterprise_card_x1+2*s, enterprise_card_y1+2*s, enterprise_card_x2-enterprise_card_x1-4*s, enterprise_card_y2-enterprise_card_y1-4*s)
            copyright_x = x0 + 12*s
            rounded_rect(copyright_x, y0 + shell_h - 34*s, copyright_x + 192*s, y0 + shell_h - 8*s, 10*s, _mix(login_panel_right, c["primary_light"], 0.16), login_card_border, tag="login_shape")
            canvas.create_text(copyright_x + 96*s, y0 + shell_h - 10*s, text="Copyright © NexaOffice 2026", fill=login_secondary, font=f(8.6), anchor="s", tags="login_shape")
            configure_login_widget_fonts()
        # v2026.06.20.55: keep debounced redraws while the window is being resized.
        # Tk can emit many Configure events in a short burst; redrawing the
        # full login illustration for every event made the pre-login page feel
        # less smooth on lower-power laptops.
        draw_job = {"id": None}
        draw_state = {"sig": None, "when": 0.0}
        def schedule_draw_login(event=None, delay=48):
            try:
                if draw_job.get("id"):
                    canvas.after_cancel(draw_job["id"])
            except Exception:
                pass
            if event is not None:
                try:
                    if int(getattr(event, "width", 0) or 0) <= 1 or int(getattr(event, "height", 0) or 0) <= 1:
                        return
                except Exception:
                    pass
            # v2026.06.20.62: smoother pre-login resizing. Skip zero-size configure
            # events and slightly stretch the debounce while the window is moving
            # so redraw frequency remains stable on lower-power Windows laptops.
            now_ts = time.monotonic()
            adaptive_delay = max(delay, 60 if now_ts - float(draw_state.get("when", 0.0)) < 0.18 else 40)
            def _run():
                draw_job["id"] = None
                view_w = int(canvas.winfo_width() or self.root.winfo_width() or 0)
                view_h = int(canvas.winfo_height() or self.root.winfo_height() or 0)
                sig = (max(0, view_w // 8), max(0, view_h // 8), self.lang_code, self.user_settings.get("theme", "blue"))
                if sig == draw_state.get("sig") and now_ts - float(draw_state.get("when", 0.0)) < 0.22:
                    return
                draw_state["sig"] = sig
                draw_state["when"] = time.monotonic()
                draw_login()
            draw_job["id"] = canvas.after(adaptive_delay, _run)
        canvas.bind("<Configure>", schedule_draw_login)
        anim_job = {"id": None}
        anim_state = {
            "enabled": bool(self.user_settings.get("login_background_motion", True)),
            "slow_frames": 0,
            "after_ms": 500,
        }
        def pulse_login_background():
            if not getattr(self, "login_screen_active", False):
                return
            if not anim_state.get("enabled", False):
                return
            try:
                view_w = int(canvas.winfo_width() or self.root.winfo_width() or 0)
                view_h = int(canvas.winfo_height() or self.root.winfo_height() or 0)
                # v2026.06.20.89: the login background stays visibly animated,
                # but the cadence now adapts to frame cost instead of hard-stopping
                # quickly on slower machines.
                if time.monotonic() < float(runtime.get("menu_open_until", 0.0)):
                    anim_job["id"] = self.root.after(900, pulse_login_background)
                    return
                if view_w < 960 or view_h < 620 or str(self.root.state()) == "iconic":
                    anim_job["id"] = self.root.after(1500, pulse_login_background)
                    return
                if not draw_job.get("id"):
                    start_ts = time.perf_counter()
                    draw_login()
                    frame_cost = time.perf_counter() - start_ts
                    after_ms = int(anim_state.get("after_ms", 620))
                    if frame_cost > 0.105:
                        anim_state["slow_frames"] = int(anim_state.get("slow_frames", 0)) + 1
                        after_ms = min(1280, after_ms + 140)
                    else:
                        anim_state["slow_frames"] = max(0, int(anim_state.get("slow_frames", 0)) - 1)
                        after_ms = max(430, after_ms - 60)
                    if anim_state.get("slow_frames", 0) >= 4:
                        if str(self.user_settings.get("performance_tier", "auto")) == "low_power":
                            anim_state["enabled"] = False
                            return
                        after_ms = min(1500, after_ms + 220)
                        anim_state["slow_frames"] = 2
                    anim_state["after_ms"] = after_ms
            except Exception:
                pass
            try:
                anim_job["id"] = self.root.after(int(anim_state.get("after_ms", 620)), pulse_login_background)
            except Exception:
                anim_job["id"] = None
        self.root.after_idle(draw_login)
        self.root.after(58, draw_login)
        if anim_state.get("enabled", False):
            self.root.after(360, pulse_login_background)
        username_entry.focus_set()
        username_entry.bind("<Return>", lambda e: password_entry.focus_set())
        password_entry.bind("<Return>", lambda e: do_login())
        self.root.after(80, draw_login)

    def logout_to_login(self):
        self.user_settings["logged_in"] = False
        save_settings(self.user_settings)
        self.pages = {}
        self.current_page = "home"
        try:
            self.login_screen_active = False
        except Exception:
            pass
        def _rebuild_login_after_logout():
            try:
                self._build_login_screen()
                self.root.after(90, lambda: self.root.focus_force())
                self.root.after(150, lambda: self.root.update_idletasks())
            except Exception:
                self._build_login_screen()
        try:
            self.root.after_idle(_rebuild_login_after_logout)
        except Exception:
            _rebuild_login_after_logout()

    def _configure_windows_app_identity(self):
        try:
            if sys.platform.startswith("win"):
                import ctypes
                ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(APP_USER_MODEL_ID)
        except Exception:
            pass

    def _reapply_root_icon(self):
        try:
            self._set_window_icon(self.root)
            try:
                self.root.update_idletasks()
            except Exception:
                pass
        except Exception:
            pass

    def _set_window_icon(self, window):
        """Apply the real NexaOffice application icon to title bar and taskbar.

        Important: this is for the Windows title bar / taskbar icon, not the
        in-app sidebar logo. For packaged EXE builds, PyInstaller also embeds
        assets/nexaoffice.ico through the .spec file.
        """
        try:
            ico_path = BASE_DIR / "assets" / "nexaoffice.ico"
            if ico_path.exists():
                try:
                    window.iconbitmap(default=str(ico_path))
                except TypeError:
                    window.iconbitmap(str(ico_path))
                except Exception:
                    pass
        except Exception:
            pass
        try:
            refs = []
            # Use a balanced small-first list after the multi-size ICO so
            # Windows/Tk can reuse crisp native frames without altering the brand.
            for size in (32, 24, 20, 16, 48, 64, 96, 128, 256):
                icon_path = BASE_DIR / "assets" / f"nexaoffice_icon_{size}.png"
                if icon_path.exists():
                    img = Image.open(icon_path).convert("RGBA")
                else:
                    fallback = BASE_DIR / "assets" / "logo_nexa_mark_256.png"
                    if not fallback.exists():
                        fallback = BASE_DIR / "assets" / "logo_nexa_mark_256.png"
                    img = Image.open(fallback).convert("RGBA").resize((size, size), Image.LANCZOS)
                refs.append(ImageTk.PhotoImage(img))
            self.app_icon_images = refs
            window.iconphoto(True, *refs)
            window._nexaoffice_icon_ref = refs
        except Exception:
            pass

    def _configure_display(self):
        """Adapt the app to the current monitor without making fonts tiny.

        Important: Tk's normal 100% DPI scaling is about 1.333, not 1.0.
        The previous version forced scaling to ~1.0, which made all fonts look
        small and blurry on many Windows laptops. This version reads the real
        DPI value and clamps it to a safe range.
        """
        try:
            sw = max(int(self.root.winfo_screenwidth()), 1024)
            sh = max(int(self.root.winfo_screenheight()), 720)
        except Exception:
            sw, sh = 1440, 900

        self.screen_width = sw
        self.screen_height = sh
        # Screen-level density buckets are only the starting point. Runtime
        # layout is recalculated from the actual window size after login so the
        # same build fits laptops, high-DPI displays, split-screen windows, and
        # external monitors without clipping modules.
        self._screen_compact_mode = sw < 1366 or sh < 800
        self._screen_ultra_compact_mode = sw < 1180 or sh < 700
        self.compact_mode = self._screen_compact_mode
        self.ultra_compact_mode = self._screen_ultra_compact_mode
        self.layout_scale = max(0.78, min(1.10, min(sw / 1440.0, sh / 860.0)))

        try:
            dpi_scaling = float(self.root.winfo_fpixels("1i")) / 72.0
        except Exception:
            dpi_scaling = 1.333

        # Keep text crisp but do NOT let Tk font scaling grow too much.
        # On some 125% / 150% laptops, Windows reports large DPI; if Tk uses
        # that value directly, fixed-position controls overlap. The app canvas
        # already scales by available window size, so font scaling is clamped.
        self.detected_dpi_scaling = dpi_scaling
        # Keep Tk scaling stable; canvas/card layout adapts to real window size.
        # On very high DPI monitors, over-scaling fonts causes overlap, so clamp harder.
        self.base_tk_scaling = max(1.18, min(1.50, dpi_scaling))
        try:
            requested_font_scale = float(self.user_settings.get("font_scale", 1.0))
        except Exception:
            requested_font_scale = 1.0
        self.requested_font_scale = max(0.95, min(1.25, requested_font_scale))
        if bool(self.user_settings.get("responsive_layout", {}).get("auto_safe_font_scale", True)):
            self.font_scale = clamp_ui_font_scale(self.requested_font_scale, sw, sh, dpi_scaling)
        else:
            self.font_scale = max(0.95, min(1.10, self.requested_font_scale))
        self.tk_scaling = max(1.16, min(1.56, self.base_tk_scaling * self.font_scale))
        try:
            self.root.tk.call("tk", "scaling", self.tk_scaling)
        except Exception:
            pass

        # Font family optimized for Chinese/English/Indonesian on Windows.
        self.font_family = "Microsoft YaHei UI"
        self.font_family_fallback = "Microsoft YaHei UI"

        # Use a size that fits the real monitor. Smaller / high-DPI laptops
        # should not be forced into a layout wider than the usable work area.
        # This fixes the common case where one PC clips a page while another
        # PC with the same resolution but different Windows scale looks fine.
        target_w = int(sw * (0.94 if sw >= 1366 else 0.98))
        target_h = int(sh * (0.90 if sh >= 800 else 0.96))
        usable_w = max(760, sw - 32)
        usable_h = max(520, sh - 64)
        w = min(1680, max(860, target_w), usable_w)
        h = min(980, max(560, target_h), usable_h)
        x = max((sw - w) // 2, 0)
        y = max((sh - h) // 2, 0)
        self.root.geometry(f"{w}x{h}+{x}+{y}")
        if bool(self.user_settings.get("responsive_layout", {}).get("safe_min_window", True)):
            min_w = 760 if sw < 1100 else (860 if sw < 1366 else 940)
            min_h = 520 if sh < 700 else (560 if sh < 820 else 600)
        else:
            min_w, min_h = 940, 600
        self.root.minsize(min(min_w, usable_w), min(min_h, usable_h))

        try:
            self.root.state("zoomed")
        except Exception:
            try:
                self.root.attributes("-zoomed", True)
            except Exception:
                pass

    def _detect_device_profile(self):
        """Small local device profile used for smoother low-end/DPI behavior."""
        try:
            cpu_count = os.cpu_count() or 2
        except Exception:
            cpu_count = 2
        try:
            sw = int(getattr(self, "screen_width", self.root.winfo_screenwidth()))
            sh = int(getattr(self, "screen_height", self.root.winfo_screenheight()))
        except Exception:
            sw, sh = 1366, 768
        try:
            dpi = float(getattr(self, "detected_dpi_scaling", self.root.tk.call("tk", "scaling")))
        except Exception:
            dpi = 1.333
        low_screen = sw <= 1366 or sh <= 768
        high_dpi = dpi >= 1.65
        low_cpu = cpu_count <= 4
        recommended = "low_power" if (low_screen or high_dpi or low_cpu) else "balanced"
        return {
            "cpu_count": cpu_count,
            "screen_width": sw,
            "screen_height": sh,
            "dpi_scaling": round(dpi, 3),
            "low_screen": low_screen,
            "high_dpi": high_dpi,
            "low_cpu": low_cpu,
            "recommended_tier": recommended,
        }

    def _effective_performance_tier(self):
        tier = str(self.user_settings.get("performance_tier", "auto") or "auto")
        if tier in ("balanced", "low_power"):
            return tier
        try:
            if not getattr(self, "device_profile", None):
                self.device_profile = self._detect_device_profile()
            # Conservative auto-detection for lower-end / unusual DPI machines:
            # use lighter redraw and longer debounce on small screens, high DPI,
            # or lower CPU count machines.
            if getattr(self, "ultra_compact_mode", False):
                return "low_power"
            if self.device_profile.get("recommended_tier") == "low_power":
                return "low_power"
        except Exception:
            pass
        return "balanced"

    def is_low_power_mode(self):
        return self._effective_performance_tier() == "low_power"

    def performance_refresh_interval(self, page=""):
        if bool(self.user_settings.get("lightweight_page_switch", True)) is False:
            return 0.0
        heavy_pages = {"calendar", "chat", "enterprise", "cloud", "files"}
        if self.is_low_power_mode():
            return 0.95 if page in heavy_pages else 0.65
        return 0.55 if page in heavy_pages else 0.35

    def performance_defer_ms(self, page=""):
        # Let Tk paint the destination page before heavy data redraw.
        if self.is_low_power_mode():
            return 70 if page in {"calendar", "chat", "enterprise", "cloud", "files"} else 35
        return 28 if page in {"calendar", "chat", "enterprise", "cloud", "files"} else 12

    def _record_page_switch_metric(self, page, started_at):
        try:
            elapsed_ms = int((time.monotonic() - float(started_at)) * 1000)
        except Exception:
            return
        item = {
            "page": page,
            "elapsed_ms": elapsed_ms,
            "tier": self._effective_performance_tier(),
            "time": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
        }
        metrics = self.performance_metrics.setdefault("page_switches", [])
        metrics.append(item)
        self.performance_metrics["page_switches"] = metrics[-40:]
        threshold = 900 if self.is_low_power_mode() else 650
        if elapsed_ms >= threshold:
            slow = self.performance_metrics.setdefault("slow_events", [])
            slow.append(item)
            self.performance_metrics["slow_events"] = slow[-20:]

    def performance_summary(self):
        metrics = self.performance_metrics.get("page_switches", []) if isinstance(getattr(self, "performance_metrics", None), dict) else []
        if not metrics:
            return {"count": 0, "avg_ms": 0, "max_ms": 0, "slow_count": 0}
        vals = [int(x.get("elapsed_ms", 0)) for x in metrics[-20:]]
        return {
            "count": len(metrics),
            "avg_ms": int(sum(vals) / max(len(vals), 1)),
            "max_ms": max(vals) if vals else 0,
            "slow_count": len(self.performance_metrics.get("slow_events", [])),
        }


    def system_health_summary(self):
        """Return a lightweight local health summary for the new v40 health center.

        The check intentionally avoids network calls and heavy file scans. It is
        safe to run from the UI thread and focuses on issues that previously made
        testing difficult: slow page switches, oversized backups, broken data
        JSON, tray/icon availability and antivirus-friendly build settings.
        """
        checks = []

        def add(key, ok=True, detail=""):
            checks.append({"key": key, "ok": bool(ok), "detail": str(detail or "")})

        try:
            summary = self.performance_summary()
            add("system_health_perf", summary.get("slow_count", 0) <= 3, self.t("system_health_perf_detail").format(
                avg=summary.get("avg_ms", 0), max=summary.get("max_ms", 0), slow=summary.get("slow_count", 0),
            ))
        except Exception as exc:
            add("system_health_perf", False, str(exc))

        try:
            json_files = list(DATA_DIR.glob("*.json")) if DATA_DIR.exists() else []
            bad = []
            total_kb = 0
            for fp in json_files:
                try:
                    total_kb += int(fp.stat().st_size / 1024)
                    json.loads(fp.read_text(encoding="utf-8"))
                except Exception:
                    bad.append(fp.name)
            add("system_health_data", not bad, self.t("system_health_data_detail").format(count=len(json_files), size=total_kb, bad=", ".join(bad) or "-"))
        except Exception as exc:
            add("system_health_data", False, str(exc))

        try:
            backup_count = 0
            backup_mb = 0.0
            if BACKUP_DIR.exists():
                for fp in BACKUP_DIR.rglob("*"):
                    if fp.is_file():
                        backup_count += 1
                        backup_mb += fp.stat().st_size / (1024 * 1024)
            add("system_health_backups", backup_count <= 80 and backup_mb <= 300, self.t("system_health_backup_detail").format(count=backup_count, size=round(backup_mb, 1)))
        except Exception as exc:
            add("system_health_backups", False, str(exc))

        try:
            tray = self.tray_status_summary()
            add("system_health_tray", bool(tray.get("tray_available") is not False), self.t("system_health_tray_detail").format(error=tray.get("last_error") or "-"))
        except Exception as exc:
            add("system_health_tray", False, str(exc))

        try:
            icon = self.icon_status_summary()
            add("system_health_icon", bool(icon.get("ico_exists")) and len(icon.get("png_sizes", [])) >= 12, self.t("system_health_icon_detail").format(count=len(icon.get("png_sizes", []))))
        except Exception as exc:
            add("system_health_icon", False, str(exc))

        try:
            spec = BASE_DIR / "nexaoffice_worksuite.spec"
            text = spec.read_text(encoding="utf-8", errors="ignore") if spec.exists() else ""
            friendly = ("upx=False" in text) and ("noarchive=True" in text) and ("console=False" in text)
            add("system_health_security", friendly, self.t("system_health_security_detail"))
        except Exception as exc:
            add("system_health_security", False, str(exc))

        warn_count = sum(1 for item in checks if not item.get("ok"))
        return {
            "ok": warn_count == 0,
            "warn_count": warn_count,
            "checks": checks,
            "profile": self._detect_device_profile(),
            "tier": self._effective_performance_tier(),
            "version": self.app_version,
            "generated_at": datetime.now().isoformat(timespec="seconds"),
        }

    def optimize_system_now(self):
        """Apply safe local optimizations without deleting user documents."""
        try:
            self.apply_performance_preset("low_power" if self.system_health_summary().get("warn_count", 0) else "balanced")
            # Keep recent quick backups and JSON safety backups only. This avoids
            # build/test machines getting slower because the AppData backup folder
            # keeps growing after many versions.
            if BACKUP_DIR.exists():
                groups = {}
                for fp in BACKUP_DIR.glob("*"):
                    if fp.is_file():
                        prefix = fp.name.split("_20", 1)[0]
                        groups.setdefault(prefix, []).append(fp)
                for items in groups.values():
                    for old in sorted(items, key=lambda x: x.stat().st_mtime, reverse=True)[10:]:
                        try:
                            old.unlink()
                        except Exception:
                            pass
            try:
                self.refresh_home()
            except Exception:
                pass
            self.log_action("system_health_optimized", self.app_version, "settings")
            self.set_status("system_health_optimized")
            messagebox.showinfo(self.t("system_health_title"), self.t("system_health_optimize_done"))
        except Exception as exc:
            messagebox.showwarning(self.t("warning_title"), str(exc))

    def show_system_health_center(self):
        if not self._workspace_ready():
            return "break"
        payload = self.system_health_summary()
        win = tk.Toplevel(self.root)
        win.title(self.t("system_health_title"))
        win.configure(bg=self.colors["bg"])
        self._set_window_icon(win)
        try:
            w, h = 760, 600
            win.transient(self.root)
            center_window(win, self.root, w, h, min_width=620, min_height=460)
        except Exception:
            win.geometry("760x600")
        card = tk.Frame(win, bg=self.colors["card"], highlightthickness=1, highlightbackground=self.colors["border"])
        card.pack(fill=tk.BOTH, expand=True, padx=18, pady=18)
        tk.Label(card, text=self.t("system_health_title"), bg=self.colors["card"], fg=self.colors["text"], font=("Microsoft YaHei UI", 18, "bold")).pack(anchor="w", padx=18, pady=(16, 4))
        status_key = "system_health_all_good" if payload.get("ok") else "system_health_has_warnings"
        status_fg = self.colors["success"] if payload.get("ok") else self.colors["warning"]
        tk.Label(card, text=self.t(status_key).format(count=payload.get("warn_count", 0)), bg=self.colors["card"], fg=status_fg, font=("Microsoft YaHei UI", 11, "bold"), wraplength=700, justify=tk.LEFT).pack(anchor="w", padx=18, pady=(0, 12))
        info = self.t("system_health_profile").format(version=payload.get("version"), tier=payload.get("tier"), time=payload.get("generated_at"))
        tk.Label(card, text=info, bg=self.colors["card_soft"], fg=self.colors["text_secondary"], font=("Microsoft YaHei UI", 9), wraplength=700, justify=tk.LEFT).pack(fill=tk.X, padx=18, pady=(0, 12))

        list_frame = tk.Frame(card, bg=self.colors["card"])
        list_frame.pack(fill=tk.BOTH, expand=True, padx=18, pady=(0, 12))
        for item in payload.get("checks", []):
            row = tk.Frame(list_frame, bg=self.colors["success_light"] if item.get("ok") else self.colors["warning_light"], highlightthickness=1, highlightbackground=self.colors["border"])
            row.pack(fill=tk.X, pady=4)
            mark = "✓" if item.get("ok") else "!"
            tk.Label(row, text=mark, bg=row["bg"], fg=self.colors["success"] if item.get("ok") else self.colors["warning"], font=("Microsoft YaHei UI", 13, "bold"), width=3).pack(side=tk.LEFT, padx=(8, 2), pady=9)
            text_wrap = tk.Frame(row, bg=row["bg"])
            text_wrap.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(0, 10), pady=8)
            tk.Label(text_wrap, text=self.t(item.get("key")), bg=row["bg"], fg=self.colors["text"], font=("Microsoft YaHei UI", 10, "bold")).pack(anchor="w")
            tk.Label(text_wrap, text=item.get("detail", ""), bg=row["bg"], fg=self.colors["text_secondary"], font=("Microsoft YaHei UI", 9), wraplength=620, justify=tk.LEFT).pack(anchor="w", pady=(2, 0))

        actions = tk.Frame(card, bg=self.colors["card"])
        actions.pack(fill=tk.X, padx=18, pady=(0, 16))
        create_button(actions, self.t("system_health_optimize"), self.optimize_system_now, self.colors, filled=True).pack(side=tk.LEFT, padx=(0, 8))
        create_button(actions, self.t("diagnostics_export_menu"), self.export_diagnostics, self.colors).pack(side=tk.LEFT, padx=(0, 8))
        create_button(actions, self.t("open_data_folder_menu"), self.open_data_folder, self.colors).pack(side=tk.LEFT, padx=(0, 8))
        create_button(actions, self.t("close"), win.destroy, self.colors).pack(side=tk.RIGHT)
        return "break"

    def _show_first_page_after_login(self):
        """Paint Home immediately after login, then refresh data after first frame."""
        try:
            self._switching = True
            self.current_page = "home"
            page_obj = self.ensure_page("home")
            if page_obj is not None:
                for loaded_page in self.pages.values():
                    if loaded_page is not page_obj:
                        loaded_page.frame.pack_forget()
                if not page_obj.frame.winfo_manager():
                    page_obj.frame.pack(fill=tk.BOTH, expand=True)
                page_obj.frame.lift()
            self.page_title.config(text=clean_label(self.t("tab_home")))
            self.page_subtitle.config(text=self.t("app_subtitle"))
            for key, btn in getattr(self, "nav_buttons", {}).items():
                active = key == "home"
                btn.configure(
                    bg="#EAF2FF" if active else self.colors["sidebar"],
                    fg=self.colors["primary"] if active else "#4B5568",
                    activebackground="#EAF2FF",
                    activeforeground=self.colors["primary"],
                    image=self.sidebar_icon_images.get((key, "active" if active else "normal")),
                    highlightthickness=0,
                )
            self._last_page_refresh_at["home"] = time.monotonic()
            self.root.after(45, self._finish_first_page_after_login)
        except Exception:
            try:
                self.switch_page("home")
            except Exception:
                pass
        finally:
            self._switching = False

    def _finish_first_page_after_login(self):
        try:
            page_obj = self.pages.get("home")
            if page_obj is not None and hasattr(page_obj, "refresh") and not self._is_restore_suppressed():
                page_obj.refresh()
                self._last_page_refresh_at["home"] = time.monotonic()
        except Exception:
            pass

    def _setup_style(self):
        # Make default Tk/ttk fonts readable and crisp on Windows high-DPI screens.
        try:
            base_default = 10 if getattr(self, "compact_mode", False) else 11
            default_size = max(9, min(13, int(round(base_default * getattr(self, "font_scale", 1.0)))))
            named_fonts = [
                "TkDefaultFont",
                "TkTextFont",
                "TkFixedFont",
                "TkMenuFont",
                "TkHeadingFont",
                "TkCaptionFont",
                "TkSmallCaptionFont",
                "TkIconFont",
                "TkTooltipFont",
            ]
            for font_name in named_fonts:
                try:
                    f = tkfont.nametofont(font_name)
                    f.configure(family=self.font_family, size=default_size)
                except Exception:
                    pass
            try:
                tkfont.nametofont("TkHeadingFont").configure(family=self.font_family, size=12, weight="bold")
            except Exception:
                pass
        except Exception:
            pass

        style = ttk.Style()
        try:
            style.theme_use("clam")
        except Exception:
            pass
        style.configure(
            "TCombobox",
            fieldbackground=self.colors["card"],
            background=self.colors["card"],
            foreground=self.colors["text"],
            arrowcolor=self.colors["text_secondary"],
            font=(self.font_family, 11),
        )

    def t(self, key):
        return self.lang.get(key, key)

    def _new_puid(self, prefix="P"):
        return f"{prefix}-{uuid.uuid4().hex[:12].upper()}"

    def _normalize_puid(self, value, prefix="P"):
        value = str(value or "").strip().upper()
        if value:
            return value
        return self._new_puid(prefix)

    def _ensure_account_puid(self, account, username=""):
        if not isinstance(account, dict):
            return ""
        role = str(account.get("role") or self.user_settings.get("role") or "user")
        prefix = "EPU" if role == "admin" else "PPU"
        account["puid"] = self._normalize_puid(account.get("puid"), prefix)
        account.setdefault("account_type", "enterprise" if role == "admin" else "personal")
        account.setdefault("display_name", username or account.get("username", ""))
        return account["puid"]

    def _ensure_identity_puids(self):
        changed = False
        accounts = self.user_settings.setdefault("accounts", [])
        if isinstance(accounts, list):
            for account in accounts:
                before = dict(account) if isinstance(account, dict) else {}
                self._ensure_account_puid(account, account.get("username", "") if isinstance(account, dict) else "")
                if account != before:
                    changed = True
        if not self.user_settings.get("personal_puid"):
            self.user_settings["personal_puid"] = self._new_puid("PPU")
            changed = True
        if not self.user_settings.get("enterprise_puid"):
            self.user_settings["enterprise_puid"] = self._new_puid("EPU")
            changed = True
        if not self.user_settings.get("puid"):
            username = self.user_settings.get("username", "")
            matched = next((acc for acc in accounts if isinstance(acc, dict) and acc.get("username") == username), None) if isinstance(accounts, list) else None
            self.user_settings["puid"] = matched.get("puid") if matched else self.user_settings.get("personal_puid")
            changed = True
        if isinstance(self.enterprise, dict) and not self.enterprise.get("workspace_puid"):
            self.enterprise["workspace_puid"] = self._new_puid("WPU")
            changed = True
            try:
                from core.storage import save_enterprise
                save_enterprise(self.enterprise)
            except Exception:
                pass
        if changed:
            try:
                save_settings(self.user_settings)
            except Exception:
                pass

    def current_puid(self):
        self._ensure_identity_puids()
        username = self.user_settings.get("username", "")
        for account in self.user_settings.get("accounts", []):
            if isinstance(account, dict) and account.get("username") == username:
                return account.get("puid") or self.user_settings.get("puid", "")
        return self.user_settings.get("puid") or self.user_settings.get("personal_puid", "")

    def current_workspace_puid(self):
        self._ensure_identity_puids()
        return str(self.enterprise.get("workspace_puid") or self.user_settings.get("enterprise_puid") or "").strip()

    def puid_context(self):
        return {
            "puid": self.current_puid(),
            "personal_puid": self.user_settings.get("personal_puid", ""),
            "enterprise_puid": self.user_settings.get("enterprise_puid", ""),
            "workspace_puid": self.current_workspace_puid(),
            "mode": self.get_app_mode() if hasattr(self, "get_app_mode") else self.user_settings.get("app_mode", "enterprise"),
        }

    def window_title(self):
        return f"{self.t('app_title')} {self.app_version}"

    def sidebar_subtitle_text(self):
        return f"{self.t('sidebar_subtitle')} · {self.app_version}"

    def sidebar_icon_key(self, page):
        return {
            "home": "home",
            "calendar": "calendar",
            "files": "note",
            "chat": "chat",
            "cloud": "cloud",
            "enterprise": "building",
            "ai": "ai",
            "settings": "settings",
        }.get(page, "home")

    def sidebar_label_key(self, page):
        return {
            "home": "tab_home",
            "calendar": "tab_calendar",
            "files": "tab_files",
            "chat": "tab_chat",
            "cloud": "tab_cloud",
            "enterprise": "tab_enterprise",
            "ai": "tab_ai",
            "settings": "tab_settings",
        }.get(page, page)


    def _build_shell(self):
        self._enforce_account_mode_boundaries()
        scale = getattr(self, "font_scale", 1.0)
        sw = getattr(self, "screen_width", 1440)
        if getattr(self, "ultra_compact_mode", False):
            self.sidebar_width = 198
        elif getattr(self, "compact_mode", False):
            self.sidebar_width = 218
        else:
            self.sidebar_width = max(236, min(300, int(sw * 0.16)))
        self.nav_font_size = max(12, min(17, int(round((13 if getattr(self, "compact_mode", False) else 15) * scale))))
        self.header_title_size = max(20, min(30, int(round((23 if getattr(self, "compact_mode", False) else 26) * min(scale, 1.10)))))
        self.brand_title_size = max(13, min(18, int(round((14 if getattr(self, "compact_mode", False) else 15) * min(scale, 1.08)))))
        self.sidebar = tk.Frame(self.root, bg=self.colors["sidebar"], width=getattr(self, "sidebar_width", 260), highlightthickness=1, highlightbackground=self.colors["border"])
        self.sidebar.pack(side=tk.LEFT, fill=tk.Y)
        self.sidebar.pack_propagate(False)

        self.sidebar_brand_block = tk.Frame(self.sidebar, bg=self.colors["sidebar"])
        brand = self.sidebar_brand_block
        brand.pack(fill=tk.X, padx=(20, 14), pady=(14, 12))

        logo_path = BASE_DIR / "assets" / "logo_nexa_mark_256.png"
        try:
            logo_image = Image.open(logo_path).convert("RGBA").resize((44, 44), Image.LANCZOS)
            self.brand_logo_image = ImageTk.PhotoImage(logo_image)
            logo_widget = tk.Label(brand, image=self.brand_logo_image, bg=self.colors["sidebar"])
            logo_widget.pack(side=tk.LEFT, padx=(4, 0))
        except Exception:
            logo_widget = tk.Canvas(brand, width=48, height=48, bg=self.colors["sidebar"], highlightthickness=0)
            logo_widget.pack(side=tk.LEFT)
            logo_widget.create_oval(4, 4, 44, 44, fill=self.colors["primary"], outline="")
            logo_widget.create_text(24, 24, text="N", fill="white", font=("Microsoft YaHei UI", 18, "bold"))

        brand_text = tk.Frame(brand, bg=self.colors["sidebar"])
        brand_text.pack(side=tk.LEFT, padx=(12, 0), fill=tk.X, expand=True)
        self.sidebar_brand_text = brand_text
        self.sidebar_brand_logo = logo_widget
        self.sidebar_brand_title = tk.Label(brand_text, text=self.t("app_title"), bg=self.colors["sidebar"], fg=self.colors["text"], font=("Microsoft YaHei UI", self.brand_title_size, "bold"), anchor="w", justify=tk.LEFT)
        self.sidebar_brand_title.pack(anchor="w")
        self.sidebar_brand_subtitle = tk.Label(brand_text, text=self.sidebar_subtitle_text(), bg=self.colors["sidebar"], fg=self.colors["text_secondary"], font=("Microsoft YaHei UI", 8), anchor="w", justify=tk.LEFT, wraplength=max(self.sidebar_width - 110, 120))
        self.sidebar_brand_subtitle.pack(anchor="w", pady=(2, 0))
        ToolTip(brand, lambda: self.t("app_title"))

        divider = tk.Frame(self.sidebar, height=1, bg=self.colors["border"])
        divider.pack(fill=tk.X, padx=18, pady=(0, 12))

        self.nav_buttons = {}
        self._active_nav_page = None
        self.sidebar_icon_images = {}
        nav_items = [
            ("home", "tab_home"),
            ("calendar", "tab_calendar"),
            ("files", "tab_files"),
            ("chat", "tab_chat"),
            ("cloud", "tab_cloud"),
            ("enterprise", "tab_enterprise"),
            ("ai", "tab_ai"),
            ("settings", "tab_settings"),
        ]
        nav_items = [item for item in nav_items if self.is_page_available(item[0])]
        for page, key in nav_items:
            icon_key = self.sidebar_icon_key(page)
            for state in ("normal", "active"):
                path = BASE_DIR / "assets" / f"sidebar_{icon_key}_{state}.png"
                try:
                    icon_img = Image.open(path).convert("RGBA").resize((26, 26), Image.LANCZOS)
                    self.sidebar_icon_images[(page, state)] = ImageTk.PhotoImage(icon_img)
                except Exception:
                    self.sidebar_icon_images[(page, state)] = None

        self.sidebar_nav_wrap = tk.Frame(self.sidebar, bg=self.colors["sidebar"])
        nav_wrap = self.sidebar_nav_wrap
        nav_wrap.pack(fill=tk.BOTH, expand=True, padx=0, pady=(10, 0))
        self.sidebar_nav_items = []

        for page, key in nav_items:
            img = self.sidebar_icon_images.get((page, "normal"))
            btn = tk.Button(
                nav_wrap,
                text=f"    {clean_label(self.t(key))}",
                image=img,
                compound=tk.LEFT,
                anchor="w",
                relief=tk.FLAT,
                bd=0,
                bg=self.colors["sidebar"],
                fg="#4B5568",
                activebackground="#EAF2FF",
                activeforeground=self.colors["primary"],
                font=("Microsoft YaHei UI", self.nav_font_size, "bold"),
                padx=24 if not getattr(self, "compact_mode", False) else 18,
                pady=18 if not getattr(self, "compact_mode", False) else 13,
                cursor="hand2",
                command=lambda page_name=page: self.switch_page(page_name),
                highlightthickness=0,
                borderwidth=0,
            )
            btn.pack(fill=tk.X, pady=7 if not getattr(self, "compact_mode", False) else 4)
            self.nav_buttons[page] = btn
            self.sidebar_nav_items.append(btn)
            ToolTip(btn, lambda k=key: clean_label(self.t(k)))

        self.personal_enterprise_gate = None
        if not self.has_enterprise_account():
            self.personal_enterprise_gate = tk.Frame(nav_wrap, bg=self.colors["sidebar"], highlightthickness=1, highlightbackground=self.colors["border"])
            self.personal_enterprise_gate.pack(fill=tk.X, padx=16, pady=(10, 6))
            tk.Label(self.personal_enterprise_gate, text=self.t("personal_enterprise_gate_title"), bg=self.colors["sidebar"], fg=self.colors["text"], font=("Microsoft YaHei UI", 10, "bold"), anchor="w").pack(anchor="w", fill=tk.X, padx=12, pady=(12, 2))
            tk.Label(self.personal_enterprise_gate, text=self.t("personal_enterprise_gate_hint"), bg=self.colors["sidebar"], fg=self.colors["text_secondary"], font=("Microsoft YaHei UI", 8), anchor="w", justify=tk.LEFT, wraplength=max(150, int(self.sidebar_width - 54))).pack(anchor="w", fill=tk.X, padx=12, pady=(0, 10))
            join_btn = tk.Button(self.personal_enterprise_gate, text=self.t("join_enterprise_btn"), command=self.show_join_enterprise_dialog, relief=tk.FLAT, bd=0, bg=self.colors["card"], fg=self.colors["primary"], activebackground=self.colors["primary_light"], activeforeground=self.colors["primary"], font=("Microsoft YaHei UI", 9, "bold"), cursor="hand2", padx=8, pady=7)
            join_btn.pack(fill=tk.X, padx=12, pady=(0, 7))
            create_ent_btn = tk.Button(self.personal_enterprise_gate, text=self.t("create_enterprise_btn"), command=self.show_create_enterprise_dialog, relief=tk.FLAT, bd=0, bg=self.colors["primary"], fg="white", activebackground=self.colors["primary_dark"], activeforeground="white", font=("Microsoft YaHei UI", 9, "bold"), cursor="hand2", padx=8, pady=7)
            create_ent_btn.pack(fill=tk.X, padx=12, pady=(0, 12))

        self.sidebar_bottom = tk.Frame(self.sidebar, bg=self.colors["sidebar"])
        bottom = self.sidebar_bottom
        bottom.pack(side=tk.BOTTOM, fill=tk.X, padx=12, pady=(8, 10))
        self.sidebar_footer_line = tk.Frame(bottom, height=1, bg=self.colors["border"])
        footer_line = self.sidebar_footer_line
        footer_line.pack(fill=tk.X, pady=(0, 10))
        user_row = tk.Frame(bottom, bg=self.colors["sidebar"], cursor="hand2")
        user_row.pack(fill=tk.X)
        self.sidebar_avatar_box = tk.Frame(user_row, bg=self.colors["primary_light"], width=44, height=44, cursor="hand2")
        self.sidebar_avatar_box.pack(side=tk.LEFT)
        self.sidebar_avatar_box.pack_propagate(False)
        try:
            avatar_img = Image.open(BASE_DIR / "assets" / "logo_nexa_mark_256.png").convert("RGBA").resize((34, 34), Image.LANCZOS)
            self.sidebar_avatar_image = ImageTk.PhotoImage(avatar_img)
            self.sidebar_avatar = tk.Label(self.sidebar_avatar_box, image=self.sidebar_avatar_image, bg=self.colors["primary_light"], cursor="hand2")
        except Exception:
            self.sidebar_avatar = tk.Label(self.sidebar_avatar_box, text="NX", bg=self.colors["primary_light"], fg=self.colors["primary"], font=("Microsoft YaHei UI", 11, "bold"), cursor="hand2")
        self.sidebar_avatar.pack(expand=True)
        avatar = self.sidebar_avatar_box
        info = tk.Frame(user_row, bg=self.colors["sidebar"], cursor="hand2")
        info.pack(side=tk.LEFT, padx=(8, 0), fill=tk.X, expand=True)
        self.sidebar_user_info = info
        self.sidebar_user_label = tk.Label(info, text=self.current_user_display_name(), bg=self.colors["sidebar"], fg=self.colors["text"], font=("Microsoft YaHei UI", 9, "bold"))
        self.sidebar_user_label.pack(anchor="w")
        self.status_text = self.t("status_ready")
        self.status_label = tk.Label(info, text=f"●  {self.status_text}", font=("Microsoft YaHei UI", 9, "bold"), fg=self.colors["success"], bg=self.colors["sidebar"], wraplength=max(self.sidebar_width - 92, 120), justify=tk.LEFT, anchor="w")
        self.status_label.pack(anchor="w", pady=(2, 0), fill=tk.X)
        self.sidebar_copyright_label = tk.Label(bottom, text="Copyright © NexaOffice 2026", bg=self.colors["sidebar"], fg=self.colors["text_light"], font=("Microsoft YaHei UI", 8), anchor="w", justify=tk.LEFT)
        self.sidebar_copyright_label.pack(anchor="w", fill=tk.X, pady=(10, 0))
        ToolTip(self.status_label, lambda: self.t("status_hover").format(status=self.status_text))
        for w in (user_row, avatar, getattr(self, "sidebar_avatar", None), info, self.sidebar_user_label, self.status_label):
            if w is not None:
                w.bind("<Button-1>", lambda e: self.show_profile())

        self.main_area = tk.Frame(self.root, bg=self.colors["bg"])
        self.main_area.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)

        self.header = tk.Frame(self.main_area, bg=self.colors["bg"])
        self.header.pack(fill=tk.X, padx=16, pady=(14, 8))
        header = self.header
        left_header = tk.Frame(header, bg=self.colors["bg"])
        left_header.pack(side=tk.LEFT)
        self.page_title = tk.Label(left_header, text=self.t("app_title"), font=("Microsoft YaHei UI", self.header_title_size, "bold"), fg=self.colors["text"], bg=self.colors["bg"])
        self.page_title.pack(anchor="w")
        self.page_subtitle = tk.Label(left_header, text=self.t("app_subtitle"), font=("Microsoft YaHei UI", 9), fg=self.colors["text_secondary"], bg=self.colors["bg"], anchor="w", justify=tk.LEFT)
        self.page_subtitle.pack(anchor="w", pady=(2, 0))

        right_header = tk.Frame(header, bg=self.colors["bg"])
        right_header.pack(side=tk.RIGHT)
        self.right_header = right_header
        # Global search was removed to keep the top bar clean and give module titles more space.
        self.search_wrap = None
        self.search_entry = None

        self.quick_launcher_btn = tk.Button(
            right_header,
            text=self.t("quick_launcher_button"),
            command=self.show_quick_launcher,
            bg=self.colors["card"],
            fg=self.colors["primary"],
            activebackground=self.colors["primary_light"],
            activeforeground=self.colors["primary"],
            relief=tk.FLAT,
            bd=0,
            font=("Microsoft YaHei UI", 10, "bold"),
            padx=12,
            pady=10,
            cursor="hand2",
        )
        self.quick_launcher_btn.pack(side=tk.LEFT, padx=(0, 10))
        ToolTip(self.quick_launcher_btn, lambda: self.t("quick_launcher_tip"))

        self.notification_btn = tk.Button(
            right_header,
            text=self._notification_button_text(),
            command=self.show_notification_center,
            bg=self.colors["card"],
            fg=self.colors["primary"],
            activebackground=self.colors["primary_light"],
            activeforeground=self.colors["primary"],
            relief=tk.FLAT,
            bd=0,
            font=("Microsoft YaHei UI", 10, "bold"),
            padx=12,
            pady=10,
            cursor="hand2",
        )
        self.notification_btn.pack(side=tk.LEFT, padx=(0, 10))
        ToolTip(self.notification_btn, lambda: self.t("notification_button_tip"))
        self.refresh_notification_badge()

        # Product mode switch: Personal / Enterprise stays visible and independent
        # from module-level file/drive scope switches.
        self.mode_switch = tk.Frame(right_header, bg=self.colors["card"], highlightthickness=1, highlightbackground=self.colors["border"])
        self.mode_personal_btn = tk.Button(self.mode_switch, text=self.t("mode_personal"), command=lambda: self.set_app_mode("personal"), relief=tk.FLAT, bd=0, font=("Microsoft YaHei UI", 9, "bold"), padx=12, pady=8, cursor="hand2")
        self.mode_enterprise_btn = tk.Button(self.mode_switch, text=self.t("mode_enterprise"), command=lambda: self.set_app_mode("enterprise"), relief=tk.FLAT, bd=0, font=("Microsoft YaHei UI", 9, "bold"), padx=12, pady=8, cursor="hand2")
        self.mode_personal_btn.pack(side=tk.LEFT)
        self.mode_enterprise_btn.pack(side=tk.LEFT)
        if self.has_enterprise_account():
            self.mode_switch.pack(side=tk.LEFT, padx=(0, 10))
        self._refresh_mode_switch()

        self.lang_var = tk.StringVar(value=LANG_SHORT.get(self.lang_code, self.lang_code[:2].upper()))
        self.lang_btn = tk.Button(
            right_header,
            text=f"{LANG_SHORT.get(self.lang_code, self.lang_code[:2].upper())} ▾",
            command=self.show_language_popup,
            bg=self.colors["card"],
            fg=self.colors["text"],
            activebackground=self.colors["primary_light"],
            activeforeground=self.colors["primary"],
            relief=tk.FLAT,
            bd=0,
            font=("Microsoft YaHei UI", 10, "bold"),
            padx=14,
            pady=10,
            width=5,
            cursor="hand2",
        )
        self.lang_btn.pack(side=tk.LEFT, padx=(0, 10))
        ToolTip(self.lang_btn, lambda: self.t("language_hint"))

        # The real Quit App button belongs in Profile / More / Settings, not in
        # the always-visible top-right header.  Keeping it out of the header
        # prevents accidental exits and avoids crowding language/mode controls.
        self.fullscreen_btn = None

        self.pages_frame = tk.Frame(self.main_area, bg=self.colors["bg"])
        self.pages_frame.pack(fill=tk.BOTH, expand=True, padx=16, pady=(0, 14))
        self.page_factories = {
            "home": HomePage,
            "calendar": CalendarPage,
            "files": FilesPage,
            "chat": ChatPage,
            "cloud": CloudPage,
            "enterprise": EnterprisePage,
            "ai": AIPage,
            "settings": SettingsPage,
        }
        # Lazy-load pages: create Home immediately, build other heavy pages only
        # when the user opens them for the first time.
        self.pages = {"home": HomePage(self, self.pages_frame)}
        self.page_load_order = ["home"]
        self.root.bind_all("<MouseWheel>", self._route_mousewheel, add="+")
        self.root.bind_all("<Shift-MouseWheel>", self._route_shift_mousewheel, add="+")
        self.root.bind_all("<Button-4>", lambda e: self._route_mousewheel_linux(-1, e), add="+")
        self.root.bind_all("<Button-5>", lambda e: self._route_mousewheel_linux(1, e), add="+")
        try:
            self.root.bind_all("<Button-6>", lambda e: self._route_horizontal_linux(-1, e), add="+")
            self.root.bind_all("<Button-7>", lambda e: self._route_horizontal_linux(1, e), add="+")
        except tk.TclError:
            # Windows Tk usually has no Button-6 / Button-7 events.
            # Horizontal scrolling still works with Shift + mouse wheel / touchpad routing.
            pass


    def _refresh_mode_switch(self):
        try:
            if hasattr(self, "mode_switch"):
                if self.has_enterprise_account():
                    # Enterprise switch is shown only for accounts that have joined
                    # or created an enterprise. Pure personal accounts still see
                    # only the Join / Create Enterprise gate in the sidebar.
                    if not self.mode_switch.winfo_manager():
                        self.mode_switch.pack(side=tk.LEFT, padx=(0, 10), before=self.lang_btn if hasattr(self, "lang_btn") else None)
                else:
                    self.mode_switch.pack_forget()
                    self.user_settings["app_mode"] = "personal"
            mode = self.get_app_mode()
            for key, btn in (("personal", getattr(self, "mode_personal_btn", None)), ("enterprise", getattr(self, "mode_enterprise_btn", None))):
                if not btn:
                    continue
                active = key == mode
                btn.configure(
                    bg=self.colors["primary"] if active else self.colors["card"],
                    fg="white" if active else self.colors["text_secondary"],
                    activebackground=self.colors["primary"] if active else self.colors["primary_light"],
                    activeforeground="white" if active else self.colors["primary"],
                )
        except Exception:
            pass

    def toggle_module_fullscreen(self):
        self.module_fullscreen = not bool(getattr(self, "module_fullscreen", False))
        self._apply_module_fullscreen()
        self._apply_responsive_layout()
        self.set_status("module_fullscreen_on" if self.module_fullscreen else "module_fullscreen_off")

    def exit_module_fullscreen(self):
        if getattr(self, "module_fullscreen", False):
            self.module_fullscreen = False
            self._apply_module_fullscreen()
            self._apply_responsive_layout()
            self.set_status("module_fullscreen_off")

    def _apply_module_fullscreen(self):
        full = bool(getattr(self, "module_fullscreen", False))
        try:
            if full:
                if self.sidebar.winfo_manager():
                    self.sidebar.pack_forget()
                if hasattr(self, "header") and self.header.winfo_manager():
                    self.header.pack_forget()
                if hasattr(self, "pages_frame"):
                    self.pages_frame.pack_configure(padx=8, pady=8)
                if hasattr(self, "fullscreen_btn"):
                    self.fullscreen_btn.config(text="⛶")
            else:
                if not self.sidebar.winfo_manager():
                    self.sidebar.pack(side=tk.LEFT, fill=tk.Y, before=self.main_area)
                if hasattr(self, "header") and not self.header.winfo_manager():
                    self.header.pack(fill=tk.X, padx=16, pady=(14, 8), before=self.pages_frame)
                if hasattr(self, "pages_frame"):
                    self.pages_frame.pack_configure(padx=16, pady=(0, 14))
                if hasattr(self, "fullscreen_btn"):
                    self.fullscreen_btn.config(text="⛶")
        except Exception:
            pass



    def _smooth_restore_enabled(self):
        return bool(self.user_settings.get("smooth_restore", True))

    def set_smooth_restore(self, enabled):
        self.user_settings["smooth_restore"] = bool(enabled)
        save_settings(self.user_settings)
        if enabled:
            self._enable_windows_flicker_reduction()
        else:
            self._hide_restore_root_cover()
            self._hide_restore_cover()
        self.set_status("status_saved")

    def set_restore_full_window_cover(self, enabled):
        self.user_settings["restore_full_window_cover"] = bool(enabled)
        save_settings(self.user_settings)
        if not enabled:
            self._hide_restore_root_cover()
        self.set_status("status_saved")

    def set_close_to_tray(self, enabled):
        self.user_settings["close_to_tray"] = bool(enabled)
        save_settings(self.user_settings)
        self.set_status("status_saved")

    def set_source_mode_close_direct(self, enabled):
        self.user_settings["source_mode_close_direct"] = bool(enabled)
        save_settings(self.user_settings)
        self.set_status("status_saved")

    def set_source_mode_force_os_exit(self, enabled):
        self.user_settings["source_mode_force_os_exit"] = bool(enabled)
        save_settings(self.user_settings)
        self.set_status("status_saved")

    def icon_status_summary(self):
        sizes = []
        try:
            ico_path = BASE_DIR / "assets" / "nexaoffice.ico"
            if ico_path.exists():
                img = Image.open(ico_path)
                if getattr(img, "ico", None):
                    sizes = sorted([f"{w}x{h}" for (w, h) in img.ico.sizes()], key=lambda x: int(x.split("x")[0]))
        except Exception:
            sizes = []
        return {
            "icon_dir": str(BASE_DIR / "assets"),
            "ico_exists": (BASE_DIR / "assets" / "nexaoffice.ico").exists(),
            "png_sizes": [size for size in ["16","20","24","30","32","40","48","60","64","72","80","96","128","256"] if (BASE_DIR / "assets" / f"nexaoffice_icon_{size}.png").exists()],
            "ico_sizes": sizes,
            "cache_tool_exists": (BASE_DIR / "tools" / "refresh_windows_icon_cache.bat").exists(),
            "generator_exists": (BASE_DIR / "tools" / "generate_app_icons.py").exists(),
            "verify_tool_exists": (BASE_DIR / "tools" / "verify_icon_assets.py").exists(),
        }

    def test_tray_icon(self, show_result=True):
        """Create the tray icon without hiding the window, so users can right-click-test Exit."""
        ok = self._ensure_tray_icon()
        status = self.tray_status_summary()
        if show_result:
            try:
                if ok:
                    messagebox.showinfo(self.t("tray_test_title"), self.t("tray_test_success"))
                else:
                    messagebox.showwarning(self.t("tray_test_title"), self.t("tray_test_failed").format(error=status.get("last_error", "")))
            except Exception:
                pass
        return ok

    def _verify_tray_health_later(self):
        try:
            if getattr(self, "_exit_requested", False):
                return
            if self.tray_icon is None or not self.tray_available:
                self._record_tray_event("tray_health_failed", getattr(self, "_tray_last_error", ""))
            else:
                self._record_tray_event("tray_health_ok")
        except Exception:
            pass

    def tray_status_summary(self):
        return {
            "close_to_tray": bool(self.user_settings.get("close_to_tray", True)),
            "effective_close_to_tray": self.effective_close_to_tray(),
            "source_mode": self.is_source_mode(),
            "source_mode_close_direct": bool(self.user_settings.get("source_mode_close_direct", True)),
            "source_mode_force_os_exit": bool(self.user_settings.get("source_mode_force_os_exit", True)),
            "close_behavior": self.close_behavior_summary(),
            "tray_available": self.tray_available,
            "tray_icon_running": self.tray_icon is not None,
            "closing_to_tray": bool(getattr(self, "_closing_to_tray", False)),
            "exit_requested": bool(getattr(self, "_exit_requested", False)),
            "exit_strategy": getattr(self, "_exit_strategy", ""),
            "exit_started_at": getattr(self, "_exit_started_at", 0.0),
            "minimized_without_tray": bool(getattr(self, "_minimized_without_tray", False)),
            "retry_count": int(getattr(self, "_tray_retry_count", 0)),
            "last_error": getattr(self, "_tray_last_error", ""),
            "backend_check": self.check_tray_backend(),
            "recent_events": self.tray_events[-20:],
        }

    def show_tray_status_dialog(self):
        try:
            status = self.tray_status_summary()
            backend = status.get("backend_check", {})
            lines = [
                f"{self.t('tray_status_close_to_tray')}: {status.get('close_to_tray')}",
                f"{self.t('tray_status_effective_close')}: {status.get('effective_close_to_tray')}",
                f"{self.t('tray_status_source_mode')}: {status.get('source_mode')}",
                f"{self.t('tray_status_exit_strategy')}: {status.get('exit_strategy') or '-'}",
                f"{self.t('tray_status_available')}: {status.get('tray_available')}",
                f"{self.t('tray_status_running')}: {status.get('tray_icon_running')}",
                f"{self.t('tray_status_backend')}: {backend.get('backend') or '-'}",
                f"{self.t('tray_status_win32')}: {backend.get('win32_backend_import', '-')}",
                f"{self.t('tray_status_error')}: {status.get('last_error') or backend.get('error') or '-'}",
                "",
                self.t("tray_status_recent_events"),
            ]
            for item in status.get("recent_events", [])[-8:]:
                lines.append(f"- {item.get('time')} · {item.get('event')} · {item.get('detail')}")
            messagebox.showinfo(self.t("tray_status_menu"), "\n".join(lines))
        except Exception as exc:
            messagebox.showwarning(self.t("warning_title"), str(exc))

    def confirm_exit_app(self):
        # Explicit quit entry. This is different from clicking the X when
        # close-to-tray is enabled.
        try:
            if not messagebox.askyesno(self.t("exit_app_confirm_title"), self.t("exit_app_confirm_message")):
                return
        except Exception:
            pass
        self.exit_app()

    def set_lightweight_page_switch(self, enabled):
        self.user_settings["lightweight_page_switch"] = bool(enabled)
        save_settings(self.user_settings)
        self.set_status("status_saved")

    def set_performance_tier(self, tier):
        tier = str(tier or "auto")
        if tier not in ("auto", "balanced", "low_power"):
            tier = "auto"
        self.user_settings["performance_tier"] = tier
        save_settings(self.user_settings)
        self.set_status("status_saved")

    def apply_performance_preset(self, preset):
        """Apply a safe local performance preset without changing user data."""
        preset = str(preset or "auto")
        if preset == "low_power":
            self.user_settings["performance_tier"] = "low_power"
            self.user_settings["lightweight_page_switch"] = True
            self.user_settings["smooth_restore"] = True
            self.user_settings["restore_full_window_cover"] = True
            self.user_settings["windows_composited_flicker_fix"] = False
        elif preset == "balanced":
            self.user_settings["performance_tier"] = "balanced"
            self.user_settings["lightweight_page_switch"] = True
            self.user_settings["smooth_restore"] = True
            self.user_settings["restore_full_window_cover"] = True
            self.user_settings["windows_composited_flicker_fix"] = False
        else:
            self.user_settings["performance_tier"] = "auto"
            self.user_settings["lightweight_page_switch"] = True
            self.user_settings["smooth_restore"] = True
            self.user_settings["restore_full_window_cover"] = True
            self.user_settings["windows_composited_flicker_fix"] = False
        save_settings(self.user_settings)
        self.device_profile = self._detect_device_profile()
        try:
            if "settings" in self.pages:
                self.pages["settings"].refresh()
        except Exception:
            pass
        self.set_status("status_saved")

    def _enable_windows_flicker_reduction(self):
        # v39: Do not apply WS_EX_COMPOSITED by default. On several Windows
        # + Tk combinations it paints the whole client area black after restore
        # or while the login canvas is redrawn. Keep it as an emergency opt-in
        # only, while the normal restore guard stays active.
        if not self._smooth_restore_enabled():
            return
        if not bool(self.user_settings.get("windows_composited_flicker_fix", False)):
            return
        if not sys.platform.startswith("win"):
            return
        if getattr(self, "_flicker_style_applied", False):
            return
        try:
            import ctypes
            hwnd = int(self.root.winfo_id())
            GWL_EXSTYLE = -20
            WS_EX_COMPOSITED = 0x02000000
            if sys.maxsize > 2 ** 32:
                get_style = ctypes.windll.user32.GetWindowLongPtrW
                set_style = ctypes.windll.user32.SetWindowLongPtrW
            else:
                get_style = ctypes.windll.user32.GetWindowLongW
                set_style = ctypes.windll.user32.SetWindowLongW
            style = int(get_style(hwnd, GWL_EXSTYLE))
            if not (style & WS_EX_COMPOSITED):
                set_style(hwnd, GWL_EXSTYLE, style | WS_EX_COMPOSITED)
            self._flicker_style_applied = True
        except Exception:
            pass

    def _show_restore_root_cover(self):
        """Show a very short theme-colored root cover during Windows restore.

        This is different from the content-area cover: it only appears after
        login, only for a short moment, and it uses the current theme color so
        Windows does not expose a black native surface while Tk repaints.
        """
        if not self._smooth_restore_enabled():
            return
        if getattr(self, "login_screen_active", False):
            return
        if not bool(self.user_settings.get("restore_full_window_cover", True)):
            return
        try:
            if self._restore_root_cover is not None and self._restore_root_cover.winfo_exists():
                self._restore_root_cover.lift()
                return
        except Exception:
            self._restore_root_cover = None
        try:
            cover = tk.Frame(self.root, bg=self.colors["bg"], highlightthickness=0, bd=0)
            cover.place(x=0, y=0, relwidth=1, relheight=1)
            cover.lift()
            self._restore_root_cover = cover
        except Exception:
            self._restore_root_cover = None

    def _hide_restore_root_cover(self):
        try:
            cover = getattr(self, "_restore_root_cover", None)
            if cover is not None and cover.winfo_exists():
                cover.destroy()
        except Exception:
            pass
        self._restore_root_cover = None

    def _schedule_restore_root_cover_hide(self, ms=150):
        job = getattr(self, "_restore_root_cover_job", None)
        if job:
            try:
                self.root.after_cancel(job)
            except Exception:
                pass
        self._restore_root_cover_job = self.root.after(ms, self._hide_restore_root_cover)

    def _restore_cover_parent(self):
        if getattr(self, "login_screen_active", False):
            return None
        # Only cover the content canvas area. Covering the full root caused
        # black top/bottom edges on some Windows systems when the non-client
        # frame was being restored.
        for name in ("pages_frame", "main_area"):
            parent = getattr(self, name, None)
            try:
                if parent is not None and parent.winfo_exists() and parent.winfo_ismapped():
                    return parent
            except Exception:
                pass
        return None

    def _show_restore_cover(self):
        if not self._smooth_restore_enabled():
            return
        if not bool(self.user_settings.get("restore_overlay_enabled", True)):
            return
        parent = self._restore_cover_parent()
        if parent is None:
            return
        try:
            if self._restore_cover is not None and self._restore_cover.winfo_exists():
                self._restore_cover.lift()
                return
        except Exception:
            self._restore_cover = None
        try:
            bg = parent.cget("bg") or self.colors["bg"]
        except Exception:
            bg = self.colors["bg"]
        try:
            cover = tk.Frame(parent, bg=bg, highlightthickness=0, bd=0)
            cover.place(x=0, y=0, relwidth=1, relheight=1)
            cover.lift()
            self._restore_cover = cover
        except Exception:
            self._restore_cover = None

    def _hide_restore_cover(self):
        try:
            cover = getattr(self, "_restore_cover", None)
            if cover is not None and cover.winfo_exists():
                cover.destroy()
        except Exception:
            pass
        self._restore_cover = None

    def _schedule_restore_cover_hide(self, ms=720):
        job = getattr(self, "_restore_cover_job", None)
        if job:
            try:
                self.root.after_cancel(job)
            except Exception:
                pass
        self._restore_cover_job = self.root.after(ms, self._hide_restore_cover)

    def _post_restore_light_repaint(self):
        if self.root.state() == "iconic":
            return
        try:
            self.root.configure(bg=self.colors["bg"])
            if hasattr(self, "main_area"):
                self.main_area.configure(bg=self.colors["bg"])
            if hasattr(self, "header"):
                self.header.configure(bg=self.colors["bg"])
            if hasattr(self, "pages_frame"):
                self.pages_frame.configure(bg=self.colors["bg"])
            page = getattr(self, "current_page", "home")
            page_obj = self.pages.get(page) if hasattr(self, "pages") else None
            if page_obj is not None and not page_obj.frame.winfo_manager():
                page_obj.frame.pack(fill=tk.BOTH, expand=True)
                page_obj.frame.lift()
        except Exception:
            pass

    def _watch_window_state(self):
        try:
            state = self.root.state()
            last = getattr(self, "_last_window_state", None)
            if state == "iconic" and last != "iconic":
                self._window_was_iconic = True
                self._begin_restore_guard(1600, show_cover=False)
            elif last == "iconic" and state != "iconic":
                self._window_was_iconic = True
                self._begin_restore_guard(1100, show_cover=True)
                self._post_restore_light_repaint()
                self._schedule_restore_root_cover_hide(70)
                self._schedule_restore_cover_hide(140)
                self._restore_job = self.root.after(360, self._finish_restore_guard)
            self._last_window_state = state
        except Exception:
            pass
        try:
            # v2026.06.20.09: the restore watcher no longer needs to poll at
            # ~5 times/second forever.  Slower polling keeps restore protection
            # while reducing background CPU on low-end PCs.
            # v2026.06.20.17: window-state watcher is a safety net only.
            # Polling too frequently made the whole app feel busy on low-end PCs.
            interval = 1150 if self.is_low_power_mode() else 850
            if getattr(self, "login_screen_active", False):
                interval = max(interval, 1200)
            self._state_watch_job = self.root.after(interval, self._watch_window_state)
        except Exception:
            pass

    def _on_root_visibility(self, event=None):
        try:
            if event is not None and event.widget is not self.root:
                return
            if self.root.state() != "iconic" and getattr(self, "_window_was_iconic", False):
                self._begin_restore_guard(900, show_cover=True)
                self._post_restore_light_repaint()
                self._schedule_restore_root_cover_hide(70)
                self._schedule_restore_cover_hide(140)
                self._restore_job = self.root.after(360, self._finish_restore_guard)
        except Exception:
            pass

    def _is_restore_suppressed(self):
        try:
            return time.monotonic() < float(getattr(self, "_restore_suppressed_until", 0.0))
        except Exception:
            return False

    def _on_root_configure(self, event=None):
        try:
            if event is not None and event.widget is not self.root:
                return
            if self.root.state() == "iconic" or getattr(self, "_restore_in_progress", False) or self._is_restore_suppressed():
                return
            width = int(self.root.winfo_width())
            height = int(self.root.winfo_height())
            if width < 360 or height < 260:
                return
            last_w, last_h = getattr(self, "_last_configure_size", (0, 0))
            try:
                rl = self.user_settings.get("responsive_layout", {})
                min_delta = int(rl.get("resize_min_delta", 24) or 24)
                min_delta = max(16, min(72, min_delta))
            except Exception:
                min_delta = 24
            # v2026.06.20.09: Windows/Tk can emit many Configure events while
            # dragging or when DPI settles. Ignore small jitter so the app does
            # not keep reconfiguring the sidebar/header during normal use.
            if abs(width - last_w) < min_delta and abs(height - last_h) < min_delta:
                return
            self._last_configure_size = (width, height)
            self._invalidate_scroll_widget_cache()
            if getattr(self, "_responsive_after_id", None):
                try:
                    self.root.after_cancel(self._responsive_after_id)
                except Exception:
                    pass
            try:
                rl = self.user_settings.get("responsive_layout", {})
                base_delay = int(rl.get("resize_debounce_ms", 340) or 340)
            except Exception:
                base_delay = 340
            delay = max(420, base_delay + 160) if self.is_low_power_mode() else max(260, base_delay)
            self._responsive_after_id = self.root.after(delay, self._apply_responsive_layout)
        except Exception:
            pass

    def _apply_responsive_layout(self):
        self._responsive_after_id = None
        try:
            if self.root.state() == "iconic" or getattr(self, "_restore_in_progress", False) or self._is_restore_suppressed():
                return
        except Exception:
            pass
        try:
            width = max(int(self.root.winfo_width()), 720)
            height = max(int(self.root.winfo_height()), 520)
        except Exception:
            width = getattr(self, "screen_width", 1440)
            height = getattr(self, "screen_height", 900)

        self._update_runtime_layout_profile(width, height)
        profile_name = self.layout_profile.get("name", "balanced")
        sidebar_mode = self.layout_profile.get("sidebar", "normal")
        header_mode = self.layout_profile.get("header", "normal")

        # v2026.06.20.09: skip duplicate responsive passes.  A resize burst can
        # schedule several jobs with the same effective bucket; applying the same
        # fonts, pack options and page resize repeatedly is wasted work.
        responsive_signature = (profile_name, sidebar_mode, header_mode, int(width // 24), int(height // 24), getattr(self, "current_page", ""), bool(getattr(self, "module_fullscreen", False)))
        if responsive_signature == getattr(self, "_last_responsive_signature", None):
            return
        self._last_responsive_signature = responsive_signature

        if sidebar_mode == "icon":
            sidebar_width = 76
            search_chars = 8
            page_title_size = max(17, self.header_title_size - 4)
            brand_title_size = max(12, self.brand_title_size - 2)
        elif profile_name == "narrow":
            sidebar_width = 188
            search_chars = 10
            page_title_size = max(18, self.header_title_size - 3)
            brand_title_size = max(12, self.brand_title_size - 2)
        elif profile_name == "compact":
            sidebar_width = 208
            search_chars = 14
            page_title_size = max(19, self.header_title_size - 2)
            brand_title_size = max(12, self.brand_title_size - 1)
        elif width < 1520:
            sidebar_width = 228
            search_chars = 22
            page_title_size = self.header_title_size
            brand_title_size = self.brand_title_size
        elif width < 1700:
            sidebar_width = 244
            search_chars = 26
            page_title_size = self.header_title_size
            brand_title_size = self.brand_title_size
        else:
            sidebar_width = getattr(self, "sidebar_width", 286)
            search_chars = 30 if width < 1880 else 34
            page_title_size = self.header_title_size
            brand_title_size = self.brand_title_size

        if getattr(self, "module_fullscreen", False):
            try:
                if self.sidebar.winfo_manager():
                    self.sidebar.pack_forget()
                if hasattr(self, "header") and self.header.winfo_manager():
                    self.header.pack_forget()
                if hasattr(self, "pages_frame"):
                    self.pages_frame.pack_configure(padx=8, pady=8)
            except Exception:
                pass
        else:
            try:
                if hasattr(self, "sidebar") and not self.sidebar.winfo_manager():
                    self.sidebar.pack(side=tk.LEFT, fill=tk.Y, before=self.main_area)
                self.sidebar.configure(width=sidebar_width)
                self.sidebar_width = sidebar_width
            except Exception:
                pass

        self._apply_sidebar_compaction(sidebar_mode)
        self._apply_header_compaction(header_mode, width, height)

        try:
            self.sidebar_brand_title.configure(font=("Microsoft YaHei UI", brand_title_size, "bold"))
        except Exception:
            pass
        try:
            self.sidebar_brand_subtitle.configure(wraplength=max(sidebar_width - 90, 92), font=("Microsoft YaHei UI", 8))
        except Exception:
            pass
        try:
            if height < 680:
                nav_pady, btn_pady, btn_font, brand_pad, bottom_pad, footer_pad = 0, 5, max(8, self.nav_font_size - 4), (6, 4), (3, 4), (0, 4)
                avatar_w, avatar_h = 2, 1
            elif height < 790:
                nav_pady, btn_pady, btn_font, brand_pad, bottom_pad, footer_pad = 1, 7, max(9, self.nav_font_size - 2), (8, 6), (4, 6), (0, 6)
                avatar_w, avatar_h = 3, 1
            elif height < 880:
                nav_pady, btn_pady, btn_font, brand_pad, bottom_pad, footer_pad = 2, 9, max(10, self.nav_font_size - 1), (10, 8), (6, 8), (0, 8)
                avatar_w, avatar_h = 3, 2
            else:
                nav_pady, btn_pady, btn_font, brand_pad, bottom_pad, footer_pad = 4, (11 if getattr(self, "compact_mode", False) else 14), self.nav_font_size, (14, 12), (8, 10), (0, 10)
                avatar_w, avatar_h = 3, 2
            if sidebar_mode == "icon":
                nav_pady, btn_pady, btn_font = 3, 11, max(10, btn_font)
            if hasattr(self, "sidebar_brand_block"):
                self.sidebar_brand_block.pack_configure(pady=brand_pad, padx=(10 if sidebar_mode == "icon" else 20, 10 if sidebar_mode == "icon" else 14))
            if hasattr(self, "sidebar_bottom"):
                self.sidebar_bottom.pack_configure(pady=bottom_pad, padx=8 if sidebar_mode == "icon" else 12)
            if hasattr(self, "sidebar_footer_line"):
                self.sidebar_footer_line.pack_configure(pady=footer_pad)
            if hasattr(self, "sidebar_avatar_box"):
                size = 40 if sidebar_mode == "icon" else 44
                self.sidebar_avatar_box.configure(width=size, height=size)
            if hasattr(self, "sidebar_avatar") and not hasattr(self, "sidebar_avatar_image"):
                self.sidebar_avatar.configure(font=("Microsoft YaHei UI", 11, "bold"))
            for btn in getattr(self, "sidebar_nav_items", []):
                btn.configure(pady=btn_pady, font=("Microsoft YaHei UI", btn_font, "bold"))
                btn.pack_configure(pady=nav_pady)
            if hasattr(self, "status_label"):
                self.status_label.configure(wraplength=max(sidebar_width - 92, 90), font=("Microsoft YaHei UI", 8 if height < 880 else 9, "bold"))
            if hasattr(self, "sidebar_user_label"):
                self.sidebar_user_label.configure(font=("Microsoft YaHei UI", 9 if height < 880 else 10, "bold"))
        except Exception:
            pass
        try:
            self.page_title.configure(font=("Microsoft YaHei UI", page_title_size, "bold"), wraplength=max(220, width - sidebar_width - 420))
            self.page_subtitle.configure(wraplength=max(220, width - sidebar_width - 420))
        except Exception:
            pass
        try:
            if getattr(self, "search_entry", None) is not None:
                self.search_entry.configure(width=search_chars)
        except Exception:
            pass
        try:
            mode_font = max(8, min(10, page_title_size - 15))
            compact_header = header_mode in {"compact", "tiny"}
            self.mode_personal_btn.configure(font=("Microsoft YaHei UI", mode_font, "bold"), padx=6 if compact_header else 12, pady=6 if compact_header else 8)
            self.mode_enterprise_btn.configure(font=("Microsoft YaHei UI", mode_font, "bold"), padx=6 if compact_header else 12, pady=6 if compact_header else 8)
        except Exception:
            pass
        try:
            header_padx = 8 if profile_name in {"narrow", "tiny"} else (12 if width < 1400 else 16)
            pages_pady = (0, 8 if height < 760 else 12 if height < 800 else 14)
            if hasattr(self, "header"):
                self.header.pack_configure(padx=header_padx, pady=(8 if height < 740 else 14, 6 if height < 740 else 8))
            for widget in (getattr(self, "pages_frame", None),):
                if widget is not None:
                    widget.pack_configure(padx=header_padx, pady=pages_pady)
        except Exception:
            pass

        # v2026.06.20.08: resize only the visible page.  Previous versions sent
        # every resize/page switch to all loaded pages, so hidden heavy modules
        # (calendar/home canvases, file lists, chat views) could redraw even when
        # they were not visible.  This was a main cause of the "overall very
        # laggy" feeling after many adaptive updates.
        current_obj = getattr(self, "pages", {}).get(getattr(self, "current_page", ""))
        try:
            if current_obj is not None and hasattr(current_obj, "on_app_resize"):
                current_obj.on_app_resize(width, height)
        except Exception:
            pass
        self._schedule_layout_guard(1200 if self.is_low_power_mode() else 900)

    def _schedule_layout_guard(self, delay=280, force=False):
        try:
            if getattr(self, "login_screen_active", False):
                return
            rl = self.user_settings.get("responsive_layout", {})
            if not bool(rl.get("layout_guard", True)):
                return
            # v2026.06.20.08: by default this guard is on-demand only.  The guard
            # walks many widgets and writes a report, so running it repeatedly in
            # the normal UI path can cause lag.  Diagnostic pages can still call
            # _run_layout_guard() directly or pass force=True.
            if not force and str(rl.get("layout_guard_mode", "on_demand")) != "auto":
                return
            if self.root.state() == "iconic" or self._is_restore_suppressed():
                return
            old_job = getattr(self, "_layout_guard_after_id", None)
            if old_job:
                try:
                    self.root.after_cancel(old_job)
                except Exception:
                    pass
            self._layout_guard_after_id = self.root.after(max(600, int(delay)), self._run_layout_guard)
        except Exception:
            pass

    def _run_layout_guard(self):
        self._layout_guard_after_id = None
        try:
            now = time.monotonic()
            if now - float(getattr(self, "_layout_guard_last_at", 0.0)) < 2.8:
                return
            self._layout_guard_last_at = now
            if self.root.state() == "iconic" or self._is_restore_suppressed():
                return
            width = max(int(self.root.winfo_width()), 720)
            height = max(int(self.root.winfo_height()), 520)
            target = getattr(self, "pages_frame", self.root)
            issues = collect_widget_fit_issues(target, max_items=10, tolerance=20)
            self._runtime_layout_fit_issues = issues
            report = make_layout_runtime_report(self.app_version, width, height, getattr(self, "layout_profile", {}), issues)
            save_json(DATA_DIR / "layout_runtime_latest.json", report)
            rl = self.user_settings.setdefault("responsive_layout", {})
            last_summary = {
                "time": report.get("time"),
                "status": report.get("status"),
                "issue_count": report.get("issue_count", 0),
                "profile": report.get("profile", {}),
            }
            if rl.get("last_runtime_fit") != last_summary:
                rl["last_runtime_fit"] = last_summary
                save_settings(self.user_settings)
            if not issues:
                self._layout_guard_last_issue_count = 0
                return
            previous_count = int(getattr(self, "_layout_guard_last_issue_count", 0) or 0)
            self._layout_guard_last_issue_count = len(issues)
            # Apply a gentle compact pass only after repeated clipping. This avoids
            # fighting normal Tk geometry negotiation while still protecting users
            # whose PC/DPI consistently cuts off a page.
            if previous_count > 0:
                profile = getattr(self, "layout_profile", {}) or {}
                header_mode = "tiny" if width < 980 or height < 640 else "compact"
                sidebar_mode = "icon" if width < 980 else ("compact" if width < 1180 else profile.get("sidebar", "normal"))
                self._apply_header_compaction(header_mode, width, height)
                self._apply_sidebar_compaction(sidebar_mode)
                current_obj = getattr(self, "pages", {}).get(getattr(self, "current_page", ""))
                if current_obj and hasattr(current_obj, "on_app_resize"):
                    current_obj.on_app_resize(width, height)
        except Exception:
            pass

    def _update_runtime_layout_profile(self, width, height):
        """Recalculate layout flags from the real window size, not only monitor size."""
        width = int(width or 0)
        height = int(height or 0)
        bucket = runtime_layout_bucket(width, height, getattr(self, "detected_dpi_scaling", 1.333))
        narrow = bool(bucket.get("narrow"))
        tiny = bool(bucket.get("tiny"))
        compact = bool(bucket.get("compact"))
        name = str(bucket.get("name", "balanced"))
        self.runtime_compact_mode = compact or narrow or tiny
        self.runtime_ultra_compact_mode = tiny
        self.compact_mode = bool(getattr(self, "_screen_compact_mode", False) or self.runtime_compact_mode)
        self.ultra_compact_mode = bool(getattr(self, "_screen_ultra_compact_mode", False) or self.runtime_ultra_compact_mode)
        sidebar_mode = "icon" if (tiny and bool(self.user_settings.get("responsive_layout", {}).get("auto_compact_sidebar", True))) else ("compact" if narrow else "normal")
        header_mode = "tiny" if tiny else ("compact" if (narrow or compact) else "normal")
        self.layout_profile = {"name": name, "width": width, "height": height, "sidebar": sidebar_mode, "header": header_mode, "high_dpi_fit_risk": bool(bucket.get("high_dpi_fit_risk"))}
        try:
            rl = self.user_settings.setdefault("responsive_layout", {})
            rl["last_profile"] = dict(self.layout_profile)
        except Exception:
            pass
        return self.layout_profile

    def _apply_sidebar_compaction(self, sidebar_mode="normal"):
        """Make the sidebar safe on small/high-DPI screens without rebuilding it."""
        icon_only = sidebar_mode == "icon"
        try:
            brand_text = getattr(self, "sidebar_brand_text", None)
            if brand_text is not None:
                if icon_only and brand_text.winfo_manager():
                    brand_text.pack_forget()
                elif not icon_only and not brand_text.winfo_manager():
                    brand_text.pack(side=tk.LEFT, padx=(12, 0), fill=tk.X, expand=True)
        except Exception:
            pass
        try:
            info = getattr(self, "sidebar_user_info", None)
            if info is not None:
                if icon_only and info.winfo_manager():
                    info.pack_forget()
                elif not icon_only and not info.winfo_manager():
                    info.pack(side=tk.LEFT, padx=(8, 0), fill=tk.X, expand=True)
        except Exception:
            pass
        try:
            for page, btn in getattr(self, "nav_buttons", {}).items():
                key = self.sidebar_label_key(page)
                if icon_only:
                    btn.configure(text="", anchor="center", compound=tk.TOP, padx=8)
                else:
                    btn.configure(text=f"    {clean_label(self.t(key))}", anchor="w", compound=tk.LEFT, padx=18 if sidebar_mode == "compact" else 24)
        except Exception:
            pass
        try:
            gate = getattr(self, "personal_enterprise_gate", None)
            if gate is not None:
                if icon_only and gate.winfo_manager():
                    gate.pack_forget()
                elif not icon_only and not gate.winfo_manager() and not self.has_enterprise_account():
                    gate.pack(fill=tk.X, padx=16, pady=(10, 6))
        except Exception:
            pass

    def _apply_header_compaction(self, header_mode="normal", width=1440, height=900):
        """Keep top-right controls inside the visible window on narrow layouts."""
        compact = header_mode in {"compact", "tiny"}
        tiny = header_mode == "tiny"
        try:
            if hasattr(self, "quick_launcher_btn"):
                self.quick_launcher_btn.configure(
                    text=("⌘K" if tiny else ("快速" if compact and self.lang_code == "zh_CN" else "Quick" if compact and self.lang_code == "en_US" else "Cepat" if compact else self.t("quick_launcher_button"))),
                    padx=8 if compact else 12,
                    pady=7 if compact else 10,
                    font=("Microsoft YaHei UI", 9 if compact else 10, "bold"),
                )
            if hasattr(self, "notification_btn"):
                self.notification_btn.configure(
                    text=self._notification_button_text(compact=True if compact else False),
                    padx=8 if compact else 12,
                    pady=7 if compact else 10,
                    font=("Microsoft YaHei UI", 9 if compact else 10, "bold"),
                )
            if hasattr(self, "lang_btn"):
                lang_code_text = LANG_SHORT.get(self.lang_code, self.lang_code[:2].upper())
                lang_text = lang_code_text if tiny else f"{lang_code_text} ▾"
                self.lang_btn.configure(text=lang_text, width=3 if tiny else 5, padx=8 if compact else 12, pady=7 if compact else 10)
            if getattr(self, "exit_btn", None):
                self.exit_btn.configure(text=("⏻" if compact else self.t("exit_app_menu")), padx=8 if compact else 12, pady=7 if compact else 10, font=("Microsoft YaHei UI", 9 if compact else 10, "bold"))
        except Exception:
            pass
        try:
            if hasattr(self, "mode_switch"):
                if tiny and self.mode_switch.winfo_manager():
                    self.mode_switch.pack_forget()
                elif not tiny and not self.mode_switch.winfo_manager():
                    before_widget = getattr(self, "lang_btn", None)
                    if before_widget:
                        self.mode_switch.pack(side=tk.LEFT, padx=(0, 8 if compact else 10), before=before_widget)
                    else:
                        self.mode_switch.pack(side=tk.LEFT, padx=(0, 8 if compact else 10))
        except Exception:
            pass

    def _invalidate_scroll_widget_cache(self):
        try:
            self._scroll_widget_cache = []
            self._scroll_widget_cache_at = 0.0
            self._last_scroll_target_y = None
            self._last_scroll_target_x = None
        except Exception:
            pass

    def _all_scroll_widgets(self, widget=None):
        root_call = widget is None
        widget = widget or self.root
        if root_call:
            try:
                cache_ms = int(self.user_settings.get("responsive_layout", {}).get("scroll_target_cache_ms", 700) or 700)
            except Exception:
                cache_ms = 700
            now = time.monotonic()
            cached = getattr(self, "_scroll_widget_cache", [])
            if cached and (now - float(getattr(self, "_scroll_widget_cache_at", 0.0))) * 1000 <= max(150, cache_ms):
                return list(cached)

        result = []

        def visit(node):
            try:
                cls = node.winfo_class()
                if cls in ("Canvas", "Text", "Listbox") and hasattr(node, "yview"):
                    result.append(node)
                for child in node.winfo_children():
                    visit(child)
            except Exception:
                pass

        visit(widget)
        if root_call:
            try:
                self._scroll_widget_cache = list(result)
                self._scroll_widget_cache_at = time.monotonic()
            except Exception:
                pass
        return result

    def _scroll_widget_under_pointer(self, event, axis="y"):
        # v2026.06.20.17: reuse the last wheel target while the pointer stays
        # inside it.  This avoids scanning the widget tree for every wheel tick
        # on complex pages such as Calendar, Files and Chat.
        cache_attr = "_last_scroll_target_x" if axis == "x" else "_last_scroll_target_y"
        try:
            cached = getattr(self, cache_attr, None)
            if cached is not None and cached.winfo_exists() and cached.winfo_ismapped():
                x1, y1 = cached.winfo_rootx(), cached.winfo_rooty()
                x2, y2 = x1 + cached.winfo_width(), y1 + cached.winfo_height()
                if x1 <= event.x_root <= x2 and y1 <= event.y_root <= y2:
                    view = cached.xview() if axis == "x" and hasattr(cached, "xview") else cached.yview()
                    lo, hi = view
                    if (hi - lo) < 0.999:
                        return cached
        except Exception:
            pass
        candidates = []
        for widget in self._all_scroll_widgets():
            try:
                if not widget.winfo_ismapped():
                    continue
                x1, y1 = widget.winfo_rootx(), widget.winfo_rooty()
                x2, y2 = x1 + widget.winfo_width(), y1 + widget.winfo_height()
                if x1 <= event.x_root <= x2 and y1 <= event.y_root <= y2:
                    view = widget.xview() if axis == "x" and hasattr(widget, "xview") else widget.yview()
                    lo, hi = view
                    if (hi - lo) < 0.999:
                        area = max(widget.winfo_width(), 1) * max(widget.winfo_height(), 1)
                        candidates.append((area, widget))
            except Exception:
                pass
        if not candidates:
            return None
        candidates.sort(key=lambda item: item[0])
        target = candidates[0][1]
        try:
            setattr(self, cache_attr, target)
        except Exception:
            pass
        return target

    def _route_mousewheel(self, event):
        widget = self._scroll_widget_under_pointer(event, "y")
        if not widget:
            return
        delta = getattr(event, "delta", 0)
        if delta == 0:
            return
        step = -1 * int(delta / 120) if abs(delta) >= 120 else (-1 if delta > 0 else 1)
        try:
            widget.yview_scroll(step, "units")
            return "break"
        except Exception:
            return

    def _route_shift_mousewheel(self, event):
        widget = self._scroll_widget_under_pointer(event, "x")
        if not widget:
            return
        delta = getattr(event, "delta", 0)
        if delta == 0:
            return
        step = -1 * int(delta / 120) if abs(delta) >= 120 else (-1 if delta > 0 else 1)
        try:
            widget.xview_scroll(step, "units")
            return "break"
        except Exception:
            return

    def _route_mousewheel_linux(self, direction, event):
        widget = self._scroll_widget_under_pointer(event, "y")
        if widget:
            try:
                widget.yview_scroll(direction, "units")
                return "break"
            except Exception:
                return

    def _route_horizontal_linux(self, direction, event):
        widget = self._scroll_widget_under_pointer(event, "x")
        if widget:
            try:
                widget.xview_scroll(direction, "units")
                return "break"
            except Exception:
                return


    def module_label(self, page):
        mapping = {
            "home": "module_home",
            "calendar": "module_calendar",
            "files": "module_files",
            "chat": "module_chat",
            "cloud": "module_cloud",
            "enterprise": "module_enterprise",
            "ai": "module_ai",
            "settings": "module_settings",
        }
        return self.t(mapping.get(page, page)) if mapping.get(page, page) in self.lang else str(page)

    def log_action(self, action, detail="", module=None):
        try:
            from datetime import datetime
            text = self.t(action) if isinstance(action, str) and action in self.lang else str(action)
            module_name = self.module_label(module or getattr(self, "current_page", "")) if (module or getattr(self, "current_page", "")) else ""
            username = self.user_settings.get("username") or self.t("not_logged_in")
            # v2026.06.19.10: throttle very frequent repeated module-open logs.
            # This reduces small JSON writes during fast navigation on lower-end PCs.
            raw_key = (str(action), str(module_name), str(detail or ""))
            now_mono = time.monotonic()
            if str(action) == "opened_module":
                last = self._last_action_log_at.get(raw_key, 0.0)
                if now_mono - last < (28.0 if self.is_low_power_mode() else 12.0):
                    return
                self._last_action_log_at[raw_key] = now_mono
            item = {
                "time": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
                "user": username,
                "module": module_name,
                "action": text,
                "detail": str(detail or ""),
            }
            self.action_logs.setdefault("items", []).append(item)
            self.action_logs["items"] = self.action_logs["items"][-240:]
            save_action_logs(self.action_logs)
        except Exception:
            pass

    def clear_action_logs(self):
        self.action_logs = {"items": []}
        save_action_logs(self.action_logs)
        self.set_status("action_log_cleared")
        if "settings" in getattr(self, "pages", {}):
            self.pages["settings"].refresh()

    def current_account(self):
        """Return the currently logged-in account record.

        Personal / enterprise mode is decided from the current account only.
        Other accounts saved on the same PC must not unlock enterprise UI for a
        personal login.
        """
        try:
            username = str(self.user_settings.get("username", "") or "")
            accounts = self.user_settings.get("accounts", []) or []
            for account in accounts:
                if str(account.get("username", "") or "") == username:
                    return account
            return {}
        except Exception:
            return {}

    def current_account_has_enterprise_access(self):
        try:
            account = self.current_account()
            if account:
                if account.get("account_type") == "enterprise":
                    return True
                if account.get("enterprise_joined") and account.get("company_name"):
                    return True
                if account.get("role") == "admin" and account.get("company_name"):
                    return True
                return False
            accounts = self.user_settings.get("accounts", []) or []
            if accounts:
                return False
            return bool(str(self.user_settings.get("enterprise_puid", "") or "").strip() and str(self.enterprise.get("company_name", "") or "").strip())
        except Exception:
            return False

    def has_enterprise_account(self):
        return self.current_account_has_enterprise_access()

    def personal_allowed_pages(self):
        # v2026.06.20.66: strict Personal edition surface.
        # Personal accounts must not see company-only modules. The only
        # enterprise entry points are Join Enterprise and Create Enterprise.
        return {"home", "calendar", "files", "chat", "ai", "settings"}

    def enterprise_only_pages(self):
        return {"enterprise", "cloud"}

    def _enforce_account_mode_boundaries(self):
        """Keep saved settings inside the current account's allowed edition."""
        changed = False
        if not self.has_enterprise_account() and self.user_settings.get("app_mode") != "personal":
            self.user_settings["app_mode"] = "personal"
            changed = True
        if not self.has_enterprise_account() and self.user_settings.get("chat_scope") != "personal":
            self.user_settings["chat_scope"] = "personal"
            changed = True
        try:
            actions = self.user_settings.setdefault("quick_actions", {})
            notifications = self.user_settings.setdefault("notification_settings", {})
            if not self.has_enterprise_account() or self.get_app_mode() == "personal":
                # Full Personal surface: no enterprise modules after login either.
                for key in ("cloud", "enterprise"):
                    if actions.get(key):
                        actions[key] = False
                        changed = True
                if actions.get("chat") is not True:
                    actions["chat"] = True
                    changed = True
                if notifications.get("show_enterprise"):
                    notifications["show_enterprise"] = False
                    changed = True
                if self.user_settings.get("chat_scope") != "personal":
                    self.user_settings["chat_scope"] = "personal"
                    changed = True
            elif self.get_app_mode() == "enterprise":
                if self.user_settings.get("chat_scope") != "enterprise":
                    self.user_settings["chat_scope"] = "enterprise"
                    changed = True
                for key in ("chat", "cloud", "enterprise"):
                    if actions.get(key) is not True:
                        actions[key] = True
                        changed = True
        except Exception:
            pass
        if changed:
            try:
                save_settings(self.user_settings)
            except Exception:
                pass
        return changed

    def _rebuild_shell_after_account_scope_change(self, page="home"):
        self.module_fullscreen = False
        save_settings(self.user_settings)
        try:
            self.reload_scope_data()
        except Exception:
            pass
        try:
            self._hide_restore_cover()
            self.root.configure(bg=self.colors["bg"])
            for child in list(self.root.winfo_children()):
                child.destroy()
            self._build_shell()
            self.switch_page(page if self.is_page_available(page) else "home")
        except Exception:
            self._start_main_app()

    def _activate_enterprise_for_current_account(self, company_name, role="user", invite_code=""):
        company_name = str(company_name or "").strip()
        if not company_name:
            return False
        accounts = self.user_settings.setdefault("accounts", [])
        account = self.current_account()
        username = str(self.user_settings.get("username", "") or "")
        if not account and username:
            account = {"username": username, "display_name": username.split("@")[0], "created": True}
            self._ensure_account_puid(account, username)
            accounts.append(account)
        if account:
            account["account_type"] = "enterprise"
            account["enterprise_joined"] = True
            account["company_name"] = company_name
            account["role"] = role
            if invite_code:
                account["enterprise_invite_code"] = str(invite_code).strip()
            self.user_settings["role"] = role
            self.user_settings["enterprise_puid"] = account.get("enterprise_puid") or account.get("puid") or self.user_settings.get("puid", "")
        self.enterprise["company_name"] = company_name
        try:
            save_enterprise(self.enterprise)
        except Exception:
            pass
        self.user_settings["app_mode"] = "enterprise"
        self.user_settings["chat_scope"] = "enterprise"
        try:
            notifications = self.user_settings.setdefault("notification_settings", {})
            notifications["show_enterprise"] = True
            actions = self.user_settings.setdefault("quick_actions", {})
            for key in ("chat", "cloud", "enterprise"):
                actions[key] = True
        except Exception:
            pass
        return True

    def show_join_enterprise_dialog(self):
        if not self._workspace_ready():
            return
        dlg = tk.Toplevel(self.root)
        dlg.title(self.t("join_enterprise_title"))
        dlg.configure(bg=self.colors["card"])
        self._set_window_icon(dlg)
        try:
            dlg.withdraw()
        except Exception:
            pass
        try:
            dlg.transient(self.root)
            dlg.grab_set()
            dlg.resizable(False, False)
            center_window(dlg, self.root, 520, 360, min_width=480, min_height=320)
        except Exception:
            dlg.geometry("520x360")
        body = tk.Frame(dlg, bg=self.colors["card"])
        body.pack(fill=tk.BOTH, expand=True, padx=26, pady=22)
        tk.Label(body, text=self.t("join_enterprise_title"), bg=self.colors["card"], fg=self.colors["text"], font=("Microsoft YaHei UI", 17, "bold")).pack(anchor="w")
        tk.Label(body, text=self.t("join_enterprise_hint"), bg=self.colors["card"], fg=self.colors["text_secondary"], font=("Microsoft YaHei UI", 9), wraplength=460, justify=tk.LEFT).pack(anchor="w", pady=(6, 16))
        tk.Label(body, text=self.t("enterprise_company_name"), bg=self.colors["card"], fg=self.colors["text_secondary"], font=("Microsoft YaHei UI", 9, "bold")).pack(anchor="w", pady=(0, 4))
        company_entry = tk.Entry(body, relief=tk.FLAT, bd=0, bg=self.colors["card_soft"], fg=self.colors["text"], insertbackground=self.colors["text"], font=("Microsoft YaHei UI", 11))
        company_entry.pack(fill=tk.X, ipady=9)
        tk.Label(body, text=self.t("enterprise_invite_code"), bg=self.colors["card"], fg=self.colors["text_secondary"], font=("Microsoft YaHei UI", 9, "bold")).pack(anchor="w", pady=(12, 4))
        invite_entry = tk.Entry(body, relief=tk.FLAT, bd=0, bg=self.colors["card_soft"], fg=self.colors["text"], insertbackground=self.colors["text"], font=("Microsoft YaHei UI", 11))
        invite_entry.pack(fill=tk.X, ipady=9)
        actions = tk.Frame(body, bg=self.colors["card"])
        actions.pack(fill=tk.X, pady=(20, 0))
        def submit():
            company = company_entry.get().strip()
            if not company:
                messagebox.showwarning(self.t("warning_title"), self.t("enterprise_required_company"), parent=dlg)
                return
            self._activate_enterprise_for_current_account(company, role="user", invite_code=invite_entry.get().strip())
            try:
                messagebox.showinfo(self.t("success_title"), self.t("enterprise_joined_success"), parent=dlg)
            except Exception:
                pass
            dlg.destroy()
            self._rebuild_shell_after_account_scope_change("home")
        create_button(actions, self.t("join_enterprise_btn"), submit, self.colors, filled=True).pack(side=tk.RIGHT)
        create_button(actions, self.t("cancel"), dlg.destroy, self.colors).pack(side=tk.RIGHT, padx=(0, 8))
        company_entry.focus_set()
        company_entry.bind("<Return>", lambda _e: invite_entry.focus_set())
        invite_entry.bind("<Return>", lambda _e: submit())
        try:
            def _show_join_dialog():
                try:
                    dlg.update_idletasks()
                    center_window(dlg, self.root, 520, 360, min_width=480, min_height=320)
                    dlg.deiconify()
                    dlg.lift(self.root)
                    dlg.grab_set()
                except Exception:
                    try:
                        dlg.deiconify()
                    except Exception:
                        pass
            dlg.after(20, _show_join_dialog)
        except Exception:
            pass

    def show_create_enterprise_dialog(self):
        if not self._workspace_ready():
            return
        dlg = tk.Toplevel(self.root)
        dlg.title(self.t("create_enterprise_title"))
        dlg.configure(bg=self.colors["card"])
        self._set_window_icon(dlg)
        try:
            dlg.withdraw()
        except Exception:
            pass
        try:
            dlg.transient(self.root)
            dlg.grab_set()
            dlg.resizable(False, False)
            center_window(dlg, self.root, 520, 300, min_width=480, min_height=280)
        except Exception:
            dlg.geometry("520x300")
        body = tk.Frame(dlg, bg=self.colors["card"])
        body.pack(fill=tk.BOTH, expand=True, padx=26, pady=22)
        tk.Label(body, text=self.t("create_enterprise_title"), bg=self.colors["card"], fg=self.colors["text"], font=("Microsoft YaHei UI", 17, "bold")).pack(anchor="w")
        tk.Label(body, text=self.t("create_enterprise_hint"), bg=self.colors["card"], fg=self.colors["text_secondary"], font=("Microsoft YaHei UI", 9), wraplength=460, justify=tk.LEFT).pack(anchor="w", pady=(6, 16))
        tk.Label(body, text=self.t("enterprise_company_name"), bg=self.colors["card"], fg=self.colors["text_secondary"], font=("Microsoft YaHei UI", 9, "bold")).pack(anchor="w", pady=(0, 4))
        company_entry = tk.Entry(body, relief=tk.FLAT, bd=0, bg=self.colors["card_soft"], fg=self.colors["text"], insertbackground=self.colors["text"], font=("Microsoft YaHei UI", 11))
        company_entry.pack(fill=tk.X, ipady=9)
        actions = tk.Frame(body, bg=self.colors["card"])
        actions.pack(fill=tk.X, pady=(20, 0))
        def submit():
            company = company_entry.get().strip()
            if not company:
                messagebox.showwarning(self.t("warning_title"), self.t("enterprise_required_company"), parent=dlg)
                return
            self._activate_enterprise_for_current_account(company, role="admin")
            try:
                messagebox.showinfo(self.t("success_title"), self.t("enterprise_created_success"), parent=dlg)
            except Exception:
                pass
            dlg.destroy()
            self._rebuild_shell_after_account_scope_change("enterprise")
        create_button(actions, self.t("create_enterprise_btn"), submit, self.colors, filled=True).pack(side=tk.RIGHT)
        create_button(actions, self.t("cancel"), dlg.destroy, self.colors).pack(side=tk.RIGHT, padx=(0, 8))
        company_entry.focus_set()
        company_entry.bind("<Return>", lambda _e: submit())
        try:
            def _show_create_dialog():
                try:
                    dlg.update_idletasks()
                    center_window(dlg, self.root, 520, 300, min_width=480, min_height=280)
                    dlg.deiconify()
                    dlg.lift(self.root)
                    dlg.grab_set()
                except Exception:
                    try:
                        dlg.deiconify()
                    except Exception:
                        pass
            dlg.after(20, _show_create_dialog)
        except Exception:
            pass

    def get_app_mode(self):
        mode = self.user_settings.get("app_mode", "personal")
        if not self.has_enterprise_account():
            return "personal"
        return mode if mode in ("personal", "enterprise") else "personal"

    def current_data_scope(self):
        return self.get_app_mode()

    def is_item_visible_in_current_scope(self, item, default_scope="personal"):
        """Return True only when a local item belongs to the active edition.

        v2026.06.20.80: post-login Personal / Enterprise isolation must also
        apply to shared lists, global search, notifications, AI-created tasks,
        and future sync metadata. Legacy items without a scope are treated as
        Personal by default so a pure personal login never sees company data.
        """
        if not isinstance(item, dict):
            return False
        scope = str(item.get("scope") or default_scope or "personal").strip()
        if scope not in ("personal", "enterprise"):
            scope = "personal"
        return scope == self.current_data_scope()

    def visible_todo_items(self):
        rows = []
        for idx, item in enumerate(self.todos.get("items", []) if isinstance(self.todos, dict) else []):
            if isinstance(item, dict) and self.is_item_visible_in_current_scope(item, default_scope="personal"):
                rows.append((idx, item))
        return rows

    def scope_sync_context(self):
        return {
            "scope": self.current_data_scope(),
            "mode": self.get_app_mode(),
            "has_enterprise_access": bool(self.has_enterprise_account()),
            "workspace_puid": self.current_workspace_puid() if self.is_enterprise_mode() else self.user_settings.get("personal_puid", ""),
            "account_username": self.user_settings.get("username", ""),
        }

    def scope_label(self):
        return self.t("mode_enterprise") if self.is_enterprise_mode() else self.t("mode_personal")

    def reload_scope_data(self):
        # Reload notes/todos/files from the currently selected personal/enterprise scope.
        self.notes, self.todos, self.documents, self.holidays = load_data()
        try:
            for key in ("home", "calendar", "files"):
                page = self.pages.get(key)
                if page and hasattr(page, "refresh"):
                    page.refresh()
        except Exception:
            pass

    def document_scope(self, doc):
        if not isinstance(doc, dict):
            return "enterprise"
        return doc.get("scope") or "enterprise"

    def is_document_visible(self, doc):
        return self.document_scope(doc) == self.current_data_scope()

    def visible_documents(self):
        return {doc_id: doc for doc_id, doc in self.documents.items() if self.is_document_visible(doc)}

    def is_enterprise_mode(self):
        return self.get_app_mode() == "enterprise"

    def is_page_available(self, page):
        page = "files" if page == "notepad" else str(page or "")
        if self.is_enterprise_mode():
            return True
        return page in self.personal_allowed_pages()

    def set_app_mode(self, mode):
        if mode not in ("personal", "enterprise"):
            return
        if mode == "enterprise" and not self.has_enterprise_account():
            self.user_settings["app_mode"] = "personal"
            save_settings(self.user_settings)
            self.set_status("account_info_locked")
            return
        if mode == self.get_app_mode():
            return
        self.user_settings["app_mode"] = mode
        self.module_fullscreen = False
        try:
            if mode == "personal":
                self.user_settings["chat_scope"] = "personal"
                notifications = self.user_settings.setdefault("notification_settings", {})
                notifications["show_enterprise"] = False
                actions = self.user_settings.setdefault("quick_actions", {})
                for key in ("cloud", "enterprise"):
                    actions[key] = False
                actions["chat"] = True
            else:
                self.user_settings["chat_scope"] = "enterprise"
                notifications = self.user_settings.setdefault("notification_settings", {})
                notifications["show_enterprise"] = True
                actions = self.user_settings.setdefault("quick_actions", {})
                for key in ("chat", "cloud", "enterprise"):
                    actions[key] = True
        except Exception:
            pass
        # Keep Personal and Enterprise surfaces fully separated after login.
        self.get_quick_actions()
        save_settings(self.user_settings)
        self.reload_scope_data()
        current = self.current_page
        if not self.is_page_available(current):
            current = "home"
        self._hide_restore_cover()
        self.root.configure(bg=self.colors["bg"])
        for child in list(self.root.winfo_children()):
            child.destroy()
        self._build_shell()
        self.switch_page(current)
        self.log_action("changed_app_mode", mode, "settings")
        self.set_status("status_saved")

    def _build_page_load_error_page(self, page, exc):
        """Show an in-app recovery page instead of freezing on hidden Tk errors.

        Packaged Tkinter apps often hide callback exceptions from users. If a
        module fails to construct, this card keeps the shell usable and gives
        the user retry/diagnostic actions.
        """
        frame = tk.Frame(self.pages_frame, bg=self.colors["bg"])
        card = tk.Frame(frame, bg=self.colors["card"], highlightthickness=1, highlightbackground=self.colors["border"])
        card.pack(fill=tk.BOTH, expand=True, padx=18, pady=18)
        tk.Label(
            card,
            text=self.t("page_load_failed_title"),
            bg=self.colors["card"],
            fg=self.colors["danger"],
            font=("Microsoft YaHei UI", 18, "bold"),
        ).pack(anchor="w", padx=20, pady=(20, 8))
        tk.Label(
            card,
            text=self.t("page_load_failed_desc").format(module=self.module_label(page)),
            bg=self.colors["card"],
            fg=self.colors["text_secondary"],
            font=("Microsoft YaHei UI", 10),
            wraplength=760,
            justify=tk.LEFT,
        ).pack(anchor="w", fill=tk.X, padx=20, pady=(0, 12))
        error_text = f"{exc.__class__.__name__}: {exc}"
        tk.Label(
            card,
            text=self.t("page_load_failed_error").format(error=error_text),
            bg=self.colors["danger_light"],
            fg=self.colors["danger"],
            font=("Microsoft YaHei UI", 9, "bold"),
            wraplength=760,
            justify=tk.LEFT,
            padx=12,
            pady=10,
        ).pack(anchor="w", fill=tk.X, padx=20, pady=(0, 16))

        actions = tk.Frame(card, bg=self.colors["card"])
        actions.pack(anchor="w", padx=20, pady=(0, 20))

        def retry_page():
            try:
                self.pages.pop(page, None)
                frame.destroy()
            except Exception:
                pass
            self.current_page = ""
            self.switch_page(page)

        create_button(actions, self.t("page_load_retry"), retry_page, self.colors, filled=True).pack(side=tk.LEFT, padx=(0, 8))
        create_button(actions, self.t("page_load_export_diagnostics"), self.export_diagnostics, self.colors).pack(side=tk.LEFT, padx=(0, 8))
        create_button(actions, self.t("page_load_open_data"), self.open_data_folder, self.colors).pack(side=tk.LEFT)

        page_obj = type("PageLoadErrorPage", (), {})()
        page_obj.frame = frame
        page_obj.refresh = lambda: None
        page_obj.update_language = lambda: None
        return page_obj

    def ensure_page(self, page):
        if page == "notepad":
            page = "files"
        if page in self.pages:
            return self.pages[page]
        factory = getattr(self, "page_factories", {}).get(page)
        if not factory:
            return None
        self.root.config(cursor="watch")
        try:
            page_obj = factory(self, self.pages_frame)
        except Exception as exc:
            self._last_page_load_error = {"page": page, "error": repr(exc), "time": datetime.now().strftime("%Y-%m-%d %H:%M:%S")}
            page_obj = self._build_page_load_error_page(page, exc)
        self.pages[page] = page_obj
        self.page_load_order.append(page)
        return page_obj

    def _schedule_page_refresh(self, page, page_obj, force=False):
        if not page_obj or not hasattr(page_obj, "refresh"):
            return
        if self._is_restore_suppressed() and not force:
            return
        now = time.monotonic()
        interval = self.performance_refresh_interval(page)
        last = self._last_page_refresh_at.get(page, 0.0)
        if not force and interval > 0 and (now - last) <= interval:
            return
        self._last_page_refresh_at[page] = now
        old_job = self._pending_page_refresh_jobs.pop(page, None)
        if old_job:
            try:
                self.root.after_cancel(old_job)
            except Exception:
                pass

        def do_refresh():
            self._pending_page_refresh_jobs.pop(page, None)
            try:
                if page != self.current_page and not force:
                    return
                if self.root.state() == "iconic":
                    return
                page_obj.refresh()
            except Exception:
                pass

        delay = self.performance_defer_ms(page)
        self._pending_page_refresh_jobs[page] = self.root.after(delay, do_refresh)

    def switch_page(self, page):
        if page == "notepad":
            page = "files"
        if not self.is_page_available(page):
            page = "home"
        if page not in getattr(self, "page_factories", {}) and page not in self.pages:
            return
        try:
            if page == self.current_page and page in self.pages and self.pages[page].frame.winfo_manager():
                return
        except Exception:
            pass
        if getattr(self, "_switching", False):
            self.root.after(40, lambda: self.switch_page(page))
            return
        # Save lightweight pending edits from the currently visible page before
        # switching.  This is especially important for Files autosave jobs that
        # may still be waiting in Tk's after queue.
        try:
            current_obj = self.pages.get(getattr(self, "current_page", ""))
            if current_obj is not None and hasattr(current_obj, "flush_pending_changes"):
                current_obj.flush_pending_changes("switch_page")
        except Exception:
            pass
        self._switching = True
        self._page_switch_started_at[page] = time.monotonic()
        self.current_page = page
        if not self._is_restore_suppressed():
            self.root.config(cursor="watch")
        # Update only the previously active and newly active sidebar buttons.
        # Reconfiguring every button on every click causes visible micro-stutter
        # on slower PCs, especially when icon images are attached.
        previous_nav = getattr(self, "_active_nav_page", None)
        keys_to_update = set(self.nav_buttons.keys()) if previous_nav is None else {previous_nav, page}
        for key in keys_to_update:
            btn = self.nav_buttons.get(key)
            if not btn:
                continue
            active = key == page
            btn.configure(
                bg="#EAF2FF" if active else self.colors["sidebar"],
                fg=self.colors["primary"] if active else "#4B5568",
                activebackground="#EAF2FF",
                activeforeground=self.colors["primary"],
                image=self.sidebar_icon_images.get((key, "active" if active else "normal")),
                highlightthickness=0
            )
        self._active_nav_page = page
        delay = 35 if self._is_restore_suppressed() else 10
        self.root.after(delay, lambda p=page: self._finish_switch_page(p))

    def _finish_switch_page(self, page):
        page_obj = self.ensure_page(page)
        if page_obj is None:
            page = "home"
            page_obj = self.ensure_page("home")
        for loaded_page in self.pages.values():
            if loaded_page is not page_obj:
                loaded_page.frame.pack_forget()
        if not page_obj.frame.winfo_manager():
            page_obj.frame.pack(fill=tk.BOTH, expand=True)
        self._invalidate_scroll_widget_cache()
        self._last_responsive_signature = None
        should_refresh = not self._is_restore_suppressed()
        if should_refresh and hasattr(page_obj, "refresh"):
            self._schedule_page_refresh(page, page_obj)
        if page == "home":
            # Avoid repeating the brand name in the title bar, sidebar, and page header.
            self.page_title.config(text=clean_label(self.t("tab_home")))
            self.page_subtitle.config(text=self.t("app_subtitle"))
        elif page == "calendar":
            self.page_title.config(text=clean_label(self.t("tab_calendar")))
            self.page_subtitle.config(text=self.t("notes_section"))
        elif page == "files":
            # Files uses the global top header; the internal card only keeps actions/scope.
            self.page_title.config(text=clean_label(self.t("shared_files_title")))
            self.page_subtitle.config(text=self.t("shared_files_desc"))
        elif page == "chat":
            self.page_title.config(text=clean_label(self.t("chat_title")))
            self.page_subtitle.config(text=self.t("chat_subtitle"))
        elif page == "cloud":
            # Drive uses the global top header; the internal card only keeps actions/scope.
            self.page_title.config(text=clean_label(self.t("cloud_title")))
            self.page_subtitle.config(text=self.t("cloud_subtitle"))
        elif page == "enterprise":
            self.page_title.config(text=clean_label(self.t("enterprise_title")))
            self.page_subtitle.config(text=self.t("enterprise_subtitle"))
        elif page == "ai":
            self.page_title.config(text=clean_label(self.t("ai_title")))
            self.page_subtitle.config(text=self.t("ai_subtitle"))
        else:
            self.page_title.config(text=clean_label(self.t("tab_settings")))
            self.page_subtitle.config(text=self.t("settings_subtitle"))
        self.root.config(cursor="")
        self._switching = False
        self._apply_module_fullscreen()
        if not self._is_restore_suppressed():
            # Do not synchronously run the full responsive pass while switching
            # modules.  Let the new page paint first, then coalesce any pending
            # resize work into one lightweight pass.
            try:
                if getattr(self, "_responsive_after_id", None):
                    self.root.after_cancel(self._responsive_after_id)
            except Exception:
                pass
            self._responsive_after_id = self.root.after(130 if self.is_low_power_mode() else 70, self._apply_responsive_layout)
            self._schedule_layout_guard(1500 if self.is_low_power_mode() else 1100)
        started = self._page_switch_started_at.pop(page, None)
        if started is not None:
            self._record_page_switch_metric(page, started)
        self.log_action("opened_module", self.module_label(page), page)

    def _cancel_pending_layout_jobs(self):
        for attr in ("_responsive_after_id", "_layout_guard_after_id", "_restore_job", "_post_restore_layout_job", "_restore_cover_job", "_restore_root_cover_job"):
            job = getattr(self, attr, None)
            if job:
                try:
                    self.root.after_cancel(job)
                except Exception:
                    pass
                setattr(self, attr, None)

    def _begin_restore_guard(self, ms=900, show_cover=True):
        """Temporarily block heavy re-layout while Windows restores the Tk window.

        v39 changes this guard to be less visually intrusive: minimization does
        not create any overlay, and restore only covers the content area with
        the current theme background instead of covering the entire root window.
        """
        now = time.monotonic()
        self._restore_in_progress = True
        self._restore_suppressed_until = max(float(getattr(self, "_restore_suppressed_until", 0.0)), now + max(0.2, ms / 1000.0))
        self._last_restore_at = now
        self._cancel_pending_layout_jobs()
        if show_cover:
            self._show_restore_root_cover()
            self._show_restore_cover()
        else:
            self._hide_restore_root_cover()
            self._hide_restore_cover()

    def _finish_restore_guard(self):
        try:
            if self.root.state() == "iconic":
                return
        except Exception:
            pass
        self._window_was_iconic = False
        self._restore_in_progress = False
        try:
            self._last_configure_size = (int(self.root.winfo_width()), int(self.root.winfo_height()))
        except Exception:
            pass
        self._schedule_restore_root_cover_hide(40)
        self._schedule_restore_cover_hide(60)
        try:
            if not getattr(self, "login_screen_active", False):
                self._post_restore_layout_job = self.root.after(160, self._apply_responsive_layout)
        except Exception:
            pass

    def _on_root_unmap(self, event=None):
        try:
            if getattr(self, "_exit_requested", False) or getattr(self, "_closing_to_tray", False):
                return
            if event is None or event.widget is not self.root:
                return
            self._window_was_iconic = True
            self._begin_restore_guard(1600, show_cover=False)
        except Exception:
            pass

    def _on_root_map(self, event=None):
        try:
            if getattr(self, "_exit_requested", False):
                return
            if event is None or event.widget is not self.root:
                return
            if self.root.state() == "iconic":
                return
            if not getattr(self, "_window_was_iconic", False):
                return
            self._begin_restore_guard(1100, show_cover=True)
            # Do not call root.update(), update_idletasks(), page.refresh(), or
            # _apply_responsive_layout here. Keeping the existing rendered page
            # untouched prevents repeated black flashing while Windows remaps it.
            self._post_restore_light_repaint()
            self._schedule_restore_root_cover_hide(70)
            self._schedule_restore_cover_hide(140)
            self._restore_job = self.root.after(360, self._finish_restore_guard)
        except Exception:
            self._restore_in_progress = False
            self._hide_restore_root_cover()
            self._hide_restore_cover()

    def restore_window_from_second_instance(self):
        """Bring the existing app window back when the EXE is clicked again."""
        try:
            if getattr(self, "_exit_requested", False):
                return
            self._closing_to_tray = False
            self._window_was_iconic = True
            self._begin_restore_guard(1100, show_cover=True)
            if self.root.state() == "iconic":
                self.root.deiconify()
            self.root.lift()
            try:
                self.root.attributes("-topmost", True)
                self.root.after(260, lambda: self.root.attributes("-topmost", False))
            except Exception:
                pass
            self.root.after(120, self.root.focus_force)
            self._post_restore_light_repaint()
            self._schedule_restore_root_cover_hide(70)
            self._schedule_restore_cover_hide(140)
            self._restore_job = self.root.after(360, self._finish_restore_guard)
        except Exception:
            self._restore_in_progress = False

    def get_server_sync_config(self):
        cfg = self.user_settings.setdefault("server_sync", {})
        if not isinstance(cfg, dict):
            cfg = {}
            self.user_settings["server_sync"] = cfg
        cfg.setdefault("enabled", False)
        cfg.setdefault("offline_mode", True)
        cfg.setdefault("base_url", "")
        cfg.setdefault("workspace_id", "")
        cfg.setdefault("api_token", "")
        cfg.setdefault("timeout", 5)
        cfg.setdefault("retry_count", 1)
        cfg.setdefault("allow_insecure_localhost", True)
        cfg.setdefault("managed_by_system", True)
        cfg.setdefault("user_configurable", False)
        cfg.setdefault("config_source", "not_configured")
        cfg.setdefault("auto_connect", False)
        cfg.setdefault("sync_events_enabled", False)
        cfg.setdefault("deployment_mode", "local")
        apply_system_server_config(self.user_settings)
        return cfg

    def save_server_sync_config(self, enabled=None, offline_mode=None, base_url=None, workspace_id=None, api_token=None, timeout=None, retry_count=None):
        cfg = self.get_server_sync_config()
        if cfg.get("managed_by_system") and not cfg.get("user_configurable"):
            # Company deployment config is read from environment or nexaoffice_server.json.
            # Normal users cannot accidentally change the server used by the whole system.
            self.network_client = NetworkClient(build_network_config(self.user_settings))
            self.set_status("server_sync_system_managed")
            return cfg
        if enabled is not None:
            cfg["enabled"] = bool(enabled)
        if offline_mode is not None:
            cfg["offline_mode"] = bool(offline_mode)
        if base_url is not None:
            cfg["base_url"] = str(base_url or "").strip()
        if workspace_id is not None:
            cfg["workspace_id"] = str(workspace_id or "").strip()
        if api_token is not None:
            # Kept local-only for now. Do not log or display this value.
            cfg["api_token"] = str(api_token or "").strip()
        if timeout is not None:
            try:
                cfg["timeout"] = max(2, min(15, int(float(timeout))))
            except Exception:
                cfg["timeout"] = 5
        if retry_count is not None:
            try:
                cfg["retry_count"] = max(0, min(2, int(float(retry_count))))
            except Exception:
                cfg["retry_count"] = 1
        save_settings(self.user_settings)
        self.network_client = NetworkClient(build_network_config(self.user_settings))
        self.set_status("status_saved")
        return cfg

    def test_server_connection_async(self, callback):
        # Prevent duplicated network threads when users click Test repeatedly or
        # when future server bootstrap and Settings checks overlap.
        if getattr(self, "_network_test_in_progress", False):
            try:
                self.root.after(0, lambda: callback(False, "server_sync_status_configured", "connection check already running"))
            except Exception:
                pass
            return
        self._network_test_in_progress = True

        def worker():
            try:
                client = NetworkClient(build_network_config(self.user_settings))
                ok, status_key, detail = client.health_check()
            except Exception as exc:
                ok, status_key, detail = False, "server_sync_status_failed", str(exc)[:160]
            finally:
                try:
                    self.root.after(0, lambda: setattr(self, "_network_test_in_progress", False))
                except Exception:
                    self._network_test_in_progress = False
            try:
                self.root.after(0, lambda: callback(ok, status_key, detail))
            except Exception:
                pass
        threading.Thread(target=worker, daemon=True).start()

    def _system_network_bootstrap(self):
        """Run one lightweight system-managed connection check after login.

        This is intentionally controlled by deployment config, not by end users.
        If no server config exists, nothing happens and the app remains local.
        """
        try:
            cfg = self.get_server_sync_config()
            self.get_data_integrity_report(write_report=True)
            self.create_network_preflight_checkpoint("network_bootstrap", force=False, min_interval_seconds=6 * 60 * 60)
            self.get_checkpoint_health_report(write_report=True)
            self.get_checkpoint_restore_dry_run(write_report=True)
            self.sync_queue = repair_sync_queue(self.sync_queue, make_backup=False)
            self.get_offline_maintenance_report(write_report=True)
            readiness = self.get_network_readiness_report(write_report=True)
            self.get_sync_cutover_dry_run()
            plan = self.get_network_deployment_plan(write_report=True)
            self.network_client = NetworkClient(build_network_config(self.user_settings))
            if not plan.get("ready_for_system_cutover") and not readiness.get("local_only"):
                return
            if readiness.get("status") == "blocked":
                return
            if not (cfg.get("managed_by_system") and cfg.get("auto_connect") and self.network_client.config.can_connect):
                return
            # v2026.06.20.17: bootstrap is automatic system behavior, not a UI
            # feature.  Keep it quiet and rate-limited so future online mode
            # cannot make login feel stuck when a server is slow/offline.
            now = time.time()
            last = float(cfg.get("last_bootstrap_check_at", 0) or 0)
            if now - last < 90:
                return
            cfg["last_bootstrap_check_at"] = now
        except Exception:
            return

        def done(ok, status_key, detail):
            try:
                sync_cfg = self.user_settings.setdefault("server_sync", {})
                changed = (
                    sync_cfg.get("last_bootstrap_status") != status_key
                    or bool(sync_cfg.get("last_bootstrap_ok")) != bool(ok)
                )
                sync_cfg["last_bootstrap_status"] = status_key
                sync_cfg["last_bootstrap_ok"] = bool(ok)
                if changed:
                    save_settings(self.user_settings)
                if ok:
                    self.set_status("server_sync_status_ready")
            except Exception:
                pass

        self.test_server_connection_async(done)


    def get_sync_queue_summary(self):
        try:
            self.sync_queue = load_sync_queue()
            return queue_summary(self.sync_queue)
        except Exception:
            return {"total": 0, "pending": 0, "failed": 0, "done": 0}

    def get_network_readiness_report(self, write_report=True):
        try:
            summary = self.get_sync_queue_summary()
            return network_readiness_report(self.user_settings, summary, write_report=write_report)
        except Exception:
            return {
                "status": "local_ready",
                "checks": {},
                "queue": {"total": 0, "pending": 0, "failed": 0, "done": 0},
                "policy": {"status_key": "server_sync_status_disabled"},
                "cutover": {"ready": False, "will_connect_now": False, "gates": {}},
            }

    def get_data_integrity_report(self, write_report=True):
        try:
            return data_integrity_report(write_report=write_report)
        except Exception:
            return {"ok": True, "status": "OK", "will_connect": False, "unreadable": []}

    def create_network_preflight_checkpoint(self, reason="network_preflight", *, force=False, min_interval_seconds=6 * 60 * 60):
        try:
            result = ensure_preflight_data_checkpoint(
                reason,
                force=force,
                min_interval_seconds=min_interval_seconds,
            )
            self.get_checkpoint_health_report(write_report=True)
            return result
        except Exception:
            try:
                if force:
                    return create_preflight_data_checkpoint(reason)
            except Exception:
                pass
            return {"ok": False, "will_connect": False, "files": 0, "created": False, "reused": False}

    def get_checkpoint_health_report(self, write_report=True):
        try:
            return checkpoint_health_report(write_report=write_report)
        except Exception:
            return {"ok": True, "checkpoint_count": 0, "will_connect": False, "latest": {}}

    def get_checkpoint_restore_dry_run(self, checkpoint_name=None, write_report=True):
        try:
            return checkpoint_restore_dry_run(checkpoint_name, write_report=write_report)
        except Exception:
            return {"mode": "dry_run", "will_restore_now": False, "will_connect": False, "selected": {}}

    def get_sync_queue_integrity_report(self):
        try:
            return sync_queue_integrity_report(self.sync_queue)
        except Exception:
            return {"ok": True, "total": 0, "pending": 0, "failed": 0, "done": 0}

    def get_network_deployment_plan(self, write_report=True):
        try:
            integrity = self.get_sync_queue_integrity_report()
            return network_deployment_plan(self.user_settings, integrity, write_report=write_report)
        except Exception:
            return {"mode": "dry_run", "will_connect_now": False, "ready_for_system_cutover": False, "gates": {}}

    def get_sync_cutover_dry_run(self):
        try:
            return sync_cutover_dry_run(self.sync_queue)
        except Exception:
            return {"ok": True, "mode": "dry_run", "will_connect": False, "pending": 0, "batches": []}

    def get_offline_maintenance_report(self, write_report=True):
        try:
            data_report = self.get_data_integrity_report(write_report=True)
            checkpoint_report = self.get_checkpoint_health_report(write_report=True)
            sync_report = self.get_sync_queue_integrity_report()
            readiness = self.get_network_readiness_report(write_report=True)
            deployment = self.get_network_deployment_plan(write_report=True)
            cutover = self.get_sync_cutover_dry_run()
            return build_offline_maintenance_report(
                app_version=self.app_version,
                data_integrity=data_report,
                checkpoint_health=checkpoint_report,
                sync_integrity=sync_report,
                network_readiness=readiness,
                deployment_plan=deployment,
                cutover_dry_run=cutover,
                write_report=write_report,
            )
        except Exception:
            return {"ok": True, "status": "OK", "will_connect": False, "local_only": True, "summary": {}}

    def repair_sync_queue_local(self):
        try:
            backup_sync_queue(self.sync_queue, reason="manual_repair")
            self.sync_queue = repair_sync_queue(self.sync_queue, make_backup=False)
            self.get_network_readiness_report(write_report=True)
            self.set_status("server_sync_queue_repaired")
            return True
        except Exception:
            return False

    def prepare_sync_snapshot(self):
        payload = {
            "app_version": self.app_version,
            "username": self.user_settings.get("username", ""),
            "app_mode": self.get_app_mode(),
            "puid_context": self.puid_context(),
            "counts": {
                "active_scope": self.current_data_scope(),
                "notes": len(self.notes if isinstance(self.notes, dict) else {}),
                "todos": len(self.todos.get("items", [])) if isinstance(self.todos, dict) else 0,
                "files": len(self.documents if isinstance(self.documents, dict) else {}),
                "documents": len(self.documents if isinstance(self.documents, dict) else {}),
                "enterprise_notifications": len(self.enterprise.get("notifications", [])) if isinstance(self.enterprise, dict) else 0,
                "enterprise_employees": len(self.enterprise.get("employees", [])) if isinstance(self.enterprise, dict) else 0,
                "crm_customers": len(self.crm.get("customers", [])) if isinstance(getattr(self, "crm", None), dict) else 0,
                "crm_tickets": len(self.crm.get("tickets", [])) if isinstance(getattr(self, "crm", None), dict) else 0,
            },
            "prepared_at": datetime.now().isoformat(timespec="seconds"),
        }
        self.get_data_integrity_report(write_report=True)
        checkpoint = self.create_network_preflight_checkpoint("prepare_sync_snapshot", force=True)
        self.get_checkpoint_health_report(write_report=True)
        self.get_checkpoint_restore_dry_run(write_report=True)
        self.get_offline_maintenance_report(write_report=True)
        backup_sync_queue(self.sync_queue, reason="prepare_snapshot")
        self.sync_queue, item = enqueue_sync_event(
            self.sync_queue,
            "workspace_snapshot",
            "prepare",
            payload=payload,
            user=self.user_settings.get("username", ""),
            local_id="workspace",
        )
        try:
            self.get_sync_cutover_dry_run()
            self.get_network_deployment_plan(write_report=True)
        except Exception:
            pass
        self.log_action("sync_snapshot_prepared", item.get("id", ""), "settings")
        self.set_status("server_sync_snapshot_prepared")
        return item

    def _current_account_record(self):
        username = self.user_settings.get("username", "")
        for acc in self.user_settings.get("accounts", []) or []:
            if acc.get("username") == username:
                return acc
        return {}

    def current_user_display_name(self):
        username = (self.user_settings.get("username") or "").strip()
        if not username:
            return self.t("not_logged_in")
        try:
            account = self._current_account_record() or {}
        except Exception:
            account = {}
        display_name = str(account.get("display_name") or "").strip()
        if display_name:
            return display_name
        card = self.user_settings.get("contact_card", {})
        if isinstance(card, dict) and str(card.get("account") or "").strip() == username:
            display_name = str(card.get("display_name") or "").strip()
            if display_name:
                return display_name
        return username

    def contact_card(self):
        card = self.user_settings.setdefault("contact_card", {})
        if not isinstance(card, dict):
            card = {}
            self.user_settings["contact_card"] = card
        account = self._current_account_record()
        login_account = self.user_settings.get("username", "")
        changed = False
        defaults = {
            "display_name": account.get("display_name") or login_account,
            "title": account.get("title", ""),
            "account": login_account,
            "phone": account.get("phone", ""),
            "email": account.get("email", ""),
            "bio": account.get("bio", ""),
            "avatar_path": account.get("avatar_path", ""),
        }
        for key, value in defaults.items():
            if key == "account":
                if card.get(key) != value:
                    card[key] = value
                    changed = True
            elif key not in card or not str(card.get(key) or "").strip():
                card[key] = value
                changed = True
        # Keep account/contact info aligned with the created login account.
        if account:
            for key in ("phone", "email"):
                if account.get(key) and card.get(key) != account.get(key):
                    card[key] = account.get(key)
                    changed = True
        if not card.get("public_contact_code"):
            card["public_contact_code"] = "NX-" + uuid.uuid4().hex[:10].upper()
            changed = True
        if changed:
            save_settings(self.user_settings)
        return card

    def save_contact_card(self, values):
        card = self.contact_card()
        # Display name is editable; account is locked to the login account.
        for key in ("display_name", "title", "phone", "email", "bio", "avatar_path"):
            card[key] = str(values.get(key, card.get(key, "")) or "").strip()
        card["account"] = self.user_settings.get("username", "")
        account = self._current_account_record()
        if account:
            for key in ("display_name", "title", "phone", "email", "bio", "avatar_path"):
                account[key] = card.get(key, "")
        if not card.get("public_contact_code"):
            card["public_contact_code"] = "NX-" + uuid.uuid4().hex[:10].upper()
        save_settings(self.user_settings)
        self.set_status("status_saved")
        self.refresh_user_state()

    def copy_contact_card(self):
        card = self.contact_card()
        text = "\n".join([
            f"{self.t('profile_card_display_name')}: {card.get('display_name') or self.user_settings.get('username', '')}",
            f"{self.t('profile_card_title')}: {card.get('title', '')}",
            f"{self.t('profile_card_account')}: {card.get('account') or self.user_settings.get('username', '')}",
            f"{self.t('profile_card_phone')}: {card.get('phone', '')}",
            f"{self.t('profile_card_email')}: {card.get('email', '')}",
            f"{self.t('profile_card_public_code')}: {card.get('public_contact_code', '')}",
        ])
        try:
            self.root.clipboard_clear()
            self.root.clipboard_append(text)
            self.set_status("status_saved")
        except Exception:
            pass

    def edit_contact_card_dialog(self, parent=None):
        parent = parent or self.root
        win = tk.Toplevel(parent)
        win.title(self.t("profile_card_edit"))
        win.configure(bg=self.colors["bg"])
        self._set_window_icon(win)
        # Hide first, build content, then show after a final center pass.
        # This prevents the editor from appearing on the left side first.
        try:
            win.withdraw()
        except Exception:
            pass

        def force_center():
            try:
                win.update_idletasks()
                screen_w = max(int(win.winfo_screenwidth() or 1280), 900)
                screen_h = max(int(win.winfo_screenheight() or 720), 620)
                w = min(640, max(580, int(screen_w * 0.34)))
                h = min(760, max(640, int(screen_h * 0.74)))
                x = max(8, (screen_w - w) // 2)
                y = max(8, (screen_h - h) // 2)
                win.geometry(f"{w}x{h}+{x}+{y}")
                win.minsize(540, 560)
                try:
                    win.lift(parent)
                except Exception:
                    win.lift()
            except Exception:
                center_window(win, self.root, 620, 700, min_width=540, min_height=560)

        try:
            win.transient(parent)
            win.grab_set()
            win.resizable(True, True)
            force_center()
            for delay in (60, 220, 520):
                win.after(delay, force_center)
        except Exception:
            win.geometry("600x680")

        card = tk.Frame(win, bg=self.colors["card"], highlightthickness=1, highlightbackground=self.colors["border"])
        card.pack(fill=tk.BOTH, expand=True, padx=18, pady=18)

        header = tk.Frame(card, bg=self.colors["card"])
        header.pack(fill=tk.X, padx=18, pady=(16, 8))
        tk.Label(header, text=self.t("profile_card_edit"), bg=self.colors["card"], fg=self.colors["text"], font=("Microsoft YaHei UI", 16, "bold")).pack(anchor="w")
        tk.Label(header, text=self.t("profile_card_hint"), bg=self.colors["card"], fg=self.colors["text_secondary"], font=("Microsoft YaHei UI", 9), wraplength=520, justify=tk.LEFT).pack(anchor="w", pady=(6, 0))

        scroll_wrap = tk.Frame(card, bg=self.colors["card"])
        scroll_wrap.pack(fill=tk.BOTH, expand=True, padx=0, pady=(0, 0))
        canvas = tk.Canvas(scroll_wrap, bg=self.colors["card"], highlightthickness=0)
        ybar = tk.Scrollbar(scroll_wrap, orient="vertical", command=canvas.yview)
        canvas.configure(yscrollcommand=ybar.set)
        canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
        ybar.pack(side=tk.RIGHT, fill=tk.Y)
        inner = tk.Frame(canvas, bg=self.colors["card"])
        inner_id = canvas.create_window((0, 0), window=inner, anchor="nw")

        def sync_scroll(event=None):
            try:
                canvas.configure(scrollregion=canvas.bbox("all"))
                canvas.itemconfig(inner_id, width=max(canvas.winfo_width() - 4, 420))
            except Exception:
                pass

        inner.bind("<Configure>", sync_scroll)
        canvas.bind("<Configure>", sync_scroll)

        def wheel(event):
            try:
                delta = getattr(event, "delta", 0)
                if delta:
                    canvas.yview_scroll(-1 * int(delta / 120) if abs(delta) >= 120 else (-1 if delta > 0 else 1), "units")
                    return "break"
            except Exception:
                return None

        for target in (canvas, inner):
            target.bind("<MouseWheel>", wheel, add="+")

        current = self.contact_card()
        entries = {}

        def add_field(key, multi=False):
            tk.Label(inner, text=self.t(f"profile_card_{key}"), bg=self.colors["card"], fg=self.colors["text_secondary"], font=("Microsoft YaHei UI", 9, "bold")).pack(anchor="w", padx=18, pady=(8, 2))
            if multi:
                widget = tk.Text(inner, height=5, relief=tk.FLAT, bd=0, bg=self.colors["card_soft"], fg=self.colors["text"], insertbackground=self.colors["text"], font=("Microsoft YaHei UI", 10), wrap=tk.WORD)
                widget.insert("1.0", current.get(key, ""))
                widget.pack(fill=tk.X, padx=18, ipady=6)
                widget.bind("<MouseWheel>", wheel, add="+")
            else:
                widget = tk.Entry(inner, relief=tk.FLAT, bd=0, bg=self.colors["card_soft"], fg=self.colors["text"], insertbackground=self.colors["text"], font=("Microsoft YaHei UI", 10))
                widget.insert(0, current.get(key, ""))
                widget.pack(fill=tk.X, padx=18, ipady=8)
                widget.bind("<MouseWheel>", wheel, add="+")
            entries[key] = widget

        for key in ("display_name", "title"):
            add_field(key)
        tk.Label(inner, text=self.t("profile_card_account"), bg=self.colors["card"], fg=self.colors["text_secondary"], font=("Microsoft YaHei UI", 9, "bold")).pack(anchor="w", padx=18, pady=(8, 2))
        account_locked = tk.Label(
            inner,
            text=current.get("account") or self.user_settings.get("username", ""),
            bg=self.colors["card_soft"],
            fg=self.colors["text_secondary"],
            font=("Microsoft YaHei UI", 10, "bold"),
            anchor="w",
            padx=12,
            pady=9,
        )
        account_locked.pack(fill=tk.X, padx=18)
        tk.Label(inner, text=self.t("profile_account_locked_hint"), bg=self.colors["card"], fg=self.colors["text_light"], font=("Microsoft YaHei UI", 8), wraplength=520, justify=tk.LEFT).pack(anchor="w", padx=18, pady=(4, 0))
        for key in ("phone", "email", "avatar_path"):
            add_field(key)
        add_field("bio", multi=True)

        def choose_avatar():
            try:
                path = filedialog.askopenfilename(parent=win, title=self.t("profile_card_avatar_path"), filetypes=[("Images", "*.png *.jpg *.jpeg *.webp"), ("All files", "*.*")])
                if path:
                    entries["avatar_path"].delete(0, tk.END)
                    entries["avatar_path"].insert(0, path)
            except Exception:
                pass

        create_button(inner, self.t("profile_card_choose_avatar"), choose_avatar, self.colors).pack(anchor="w", padx=18, pady=(10, 16))

        def save():
            values = {}
            for key, widget in entries.items():
                if key == "bio":
                    values[key] = widget.get("1.0", tk.END).strip()
                else:
                    values[key] = widget.get().strip()
            self.save_contact_card(values)
            win.destroy()
            try:
                parent.destroy()
            except Exception:
                pass
            self.show_profile()

        footer = tk.Frame(card, bg=self.colors["card"])
        footer.pack(fill=tk.X, padx=18, pady=(14, 18))
        save_btn = create_button(footer, self.t("profile_card_save"), save, self.colors, filled=True)
        save_btn.pack(anchor="center", ipadx=34)
        sync_scroll()
        try:
            force_center()
            win.deiconify()
            force_center()
            try:
                win.grab_set()
            except Exception:
                pass
            win.focus_force()
            for delay in (120, 360, 800):
                win.after(delay, force_center)
        except Exception:
            pass

    def show_profile(self):
        win = tk.Toplevel(self.root)  # center_window( fallback is used by force_center_profile below.
        win.title(self.t("profile_title"))
        win.configure(bg=self.colors["bg"])
        self._set_window_icon(win)
        # Hide first, build content, then show after forced centering. This
        # prevents the dialog from flashing or staying on the left edge on
        # high-DPI Windows/Tk setups.
        try:
            win.withdraw()
        except Exception:
            pass
        def force_center_profile():
            try:
                win.update_idletasks()
                self.root.update_idletasks()

                def parse_geom(geom):
                    m = re.match(r"^(\d+)x(\d+)([+-]\d+)([+-]\d+)", str(geom or ""))
                    if not m:
                        return None
                    return tuple(int(v) for v in m.groups())

                parsed = parse_geom(self.root.winfo_geometry())
                if parsed:
                    rw, rh, rx, ry = parsed
                else:
                    rw = max(int(self.root.winfo_width() or 1280), 960)
                    rh = max(int(self.root.winfo_height() or 760), 640)
                    rx = int(self.root.winfo_rootx() or 0)
                    ry = int(self.root.winfo_rooty() or 0)

                screen_w = max(int(win.winfo_screenwidth() or 1280), 800)
                screen_h = max(int(win.winfo_screenheight() or 720), 600)
                w = min(820, max(720, int(rw * 0.46)))
                h = min(820, max(660, int(rh * 0.80)))

                # If root geometry is unreliable, center on screen.  Otherwise
                # center over the actual app window.
                if rw < 300 or rh < 300 or rx < -screen_w or rx > screen_w * 2:
                    x = (screen_w - w) // 2
                    y = (screen_h - h) // 2
                else:
                    x = rx + (rw - w) // 2
                    y = ry + (rh - h) // 2

                x = max(8, min(int(x), screen_w - w - 8))
                y = max(8, min(int(y), screen_h - h - 48))
                win.geometry(f"{w}x{h}+{x}+{y}")
                win.minsize(640, 560)
                try:
                    win.lift(self.root)
                except Exception:
                    win.lift()
            except Exception:
                center_window(win, self.root, 780, 720, min_width=640, min_height=560)

        try:
            win.transient(self.root)
            try:
                win.grab_set()
            except Exception:
                pass
            try:
                win.resizable(True, True)
            except Exception:
                pass
            force_center_profile()
            for delay in (40, 160, 420, 800):
                win.after(delay, force_center_profile)
        except Exception:
            win.geometry("760x720")
            win.minsize(640, 560)

        card = tk.Frame(win, bg=self.colors["card"], highlightthickness=1, highlightbackground=self.colors["border"])
        card.pack(fill=tk.BOTH, expand=True, padx=18, pady=18)

        scroll_area = tk.Frame(card, bg=self.colors["card"])
        scroll_area.pack(fill=tk.BOTH, expand=True, padx=0, pady=(0, 0))

        canvas = tk.Canvas(scroll_area, bg=self.colors["card"], highlightthickness=0)
        inner = tk.Frame(canvas, bg=self.colors["card"])
        scrollbar = tk.Scrollbar(scroll_area, orient="vertical", command=canvas.yview)
        canvas.configure(yscrollcommand=scrollbar.set)
        canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
        inner_window = canvas.create_window((0, 0), window=inner, anchor="nw")

        scrollbar_visible = {"value": False}

        def sync_scroll(event=None):
            canvas.configure(scrollregion=canvas.bbox("all"))
            try:
                canvas.itemconfig(inner_window, width=max(canvas.winfo_width() - 4, 420))
                needed = inner.winfo_reqheight() > canvas.winfo_height() + 2
                if needed and not scrollbar_visible["value"]:
                    scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
                    scrollbar_visible["value"] = True
                elif not needed and scrollbar_visible["value"]:
                    scrollbar.pack_forget()
                    scrollbar_visible["value"] = False
            except Exception:
                pass

        inner.bind("<Configure>", sync_scroll)
        canvas.bind("<Configure>", sync_scroll)

        def on_wheel(event):
            try:
                delta = getattr(event, "delta", 0)
                if delta == 0:
                    return
                step = -1 * int(delta / 120) if abs(delta) >= 120 else (-1 if delta > 0 else 1)
                canvas.yview_scroll(step, "units")
                return "break"
            except Exception:
                return None

        canvas.bind("<MouseWheel>", on_wheel, add="+")
        inner.bind("<MouseWheel>", on_wheel, add="+")

        top = tk.Frame(inner, bg=self.colors["card"])
        top.pack(fill=tk.X, padx=22, pady=(4, 16))
        avatar_box = tk.Frame(top, bg=self.colors["primary_light"], width=86, height=72)
        avatar_box.pack(side=tk.LEFT)
        avatar_box.pack_propagate(False)
        try:
            card_info = self.contact_card()
            avatar_path = Path(card_info.get("avatar_path", "")) if card_info.get("avatar_path") else None
            if avatar_path and avatar_path.exists():
                profile_img = Image.open(avatar_path).convert("RGBA")
            else:
                profile_img = Image.open(BASE_DIR / "assets" / "logo_nexa_mark_256.png").convert("RGBA")
            profile_img.thumbnail((62, 62), Image.LANCZOS)
            self.profile_avatar_image = ImageTk.PhotoImage(profile_img)
            avatar = tk.Label(avatar_box, image=self.profile_avatar_image, bg=self.colors["primary_light"])
        except Exception:
            avatar = tk.Label(avatar_box, text="NX", bg=self.colors["primary_light"], fg=self.colors["primary"], font=("Microsoft YaHei UI", 28, "bold"))
        avatar.pack(expand=True)
        info = tk.Frame(top, bg=self.colors["card"])
        info.pack(side=tk.LEFT, padx=16, fill=tk.X, expand=True)
        username = self.user_settings.get("username") or self.t("not_logged_in")
        role = self.user_settings.get("role", "staff")
        card_info = self.contact_card()
        display_name = card_info.get("display_name") or username
        profile_name_label = tk.Label(info, text=display_name, bg=self.colors["card"], fg=self.colors["text"], font=("Microsoft YaHei UI", 18, "bold"), wraplength=420, justify=tk.LEFT)
        profile_name_label.pack(anchor="w")
        subtitle = card_info.get("title") or (self.t("profile_role_admin") if role == "admin" else self.t("profile_role_staff"))
        profile_subtitle_label = tk.Label(info, text=subtitle, bg=self.colors["card"], fg=self.colors["text_secondary"], font=("Microsoft YaHei UI", 10), wraplength=420, justify=tk.LEFT)
        profile_subtitle_label.pack(anchor="w", pady=(4, 0))
        tk.Label(info, text="●  " + self.status_text, bg=self.colors["card"], fg=self.colors["success"], font=("Microsoft YaHei UI", 10, "bold")).pack(anchor="w", pady=(4, 0))

        body = tk.Frame(inner, bg=self.colors["card"])
        body.pack(fill=tk.BOTH, expand=True, padx=22, pady=(0, 16))
        profile_value_labels = []

        def row(label_key, value):
            item = tk.Frame(body, bg=self.colors["card_soft"], highlightthickness=1, highlightbackground=self.colors["border"])
            item.pack(fill=tk.X, pady=6)
            tk.Label(
                item,
                text=self.t(label_key),
                bg=self.colors["card_soft"],
                fg=self.colors["text_secondary"],
                font=("Microsoft YaHei UI", 9, "bold"),
            ).pack(anchor="w", padx=14, pady=(9, 2))
            value_label = tk.Label(
                item,
                text=value or "-",
                bg=self.colors["card_soft"],
                fg=self.colors["text"],
                font=("Microsoft YaHei UI", 10),
                wraplength=360,
                justify=tk.LEFT,
            )
            value_label.pack(anchor="w", padx=14, pady=(0, 9), fill=tk.X)
            profile_value_labels.append(value_label)

        row("profile_card_account", card_info.get("account") or self.user_settings.get("username", ""))
        row("profile_card_public_code", card_info.get("public_contact_code", ""))
        row("profile_card_phone", card_info.get("phone", ""))
        row("profile_card_email", card_info.get("email", ""))
        row("profile_card_bio", card_info.get("bio", ""))
        if self.has_enterprise_account():
            row("profile_company", self.enterprise.get("company_name") or self.t("company_not_created"))
            row("profile_app_mode", self.t("mode_enterprise") if self.is_enterprise_mode() else self.t("mode_personal"))
        row("profile_language", LANG_FULL.get(self.lang_code, self.lang_code))
        theme_id = self.user_settings.get("theme", "blue")
        theme_info = THEME_PRESETS.get(theme_id, {})
        theme_name = self.t(theme_info.get("name_key", theme_id)) if isinstance(theme_info, dict) else str(theme_id).title()
        row("profile_theme", theme_name)
        row("profile_version", self.app_version)
        row("profile_local_data", str(DATA_DIR))

        def refresh_profile_wrap(event=None):
            try:
                wrap = max(240, min(620, body.winfo_width() - 42))
                for label in profile_value_labels:
                    label.configure(wraplength=wrap)
                profile_name_label.configure(wraplength=max(220, min(520, info.winfo_width() - 8)))
                profile_subtitle_label.configure(wraplength=max(220, min(520, info.winfo_width() - 8)))
            except Exception:
                pass

        body.bind("<Configure>", refresh_profile_wrap, add="+")
        info.bind("<Configure>", refresh_profile_wrap, add="+")

        def confirm_logout_from_profile():
            # Profile "Exit" means signing out of the current account, not
            # closing the desktop application.
            if messagebox.askyesno(self.t("logout_confirm_title"), self.t("logout_confirm_message"), parent=win):
                try:
                    self._save_runtime_state_before_exit()
                except Exception:
                    pass
                try:
                    win.destroy()
                except Exception:
                    pass
                self.logout_to_login()

        footer = tk.Frame(card, bg=self.colors["card"])
        footer.pack(fill=tk.X, padx=20, pady=(10, 20))
        button_grid = tk.Frame(footer, bg=self.colors["card"])
        button_grid.pack(fill=tk.X)
        for col in range(2):
            button_grid.grid_columnconfigure(col, weight=1, uniform="profile_btns")

        btn_specs = [
            (self.t("profile_card_edit"), lambda: self.edit_contact_card_dialog(win), True),
            (self.t("profile_card_copy"), self.copy_contact_card, False),
            (self.t("profile_open_settings"), lambda: [win.destroy(), self.switch_page("settings")], False),
            (self.t("open_data_folder_menu"), self.open_data_folder, False),
        ]
        for idx, (label, command, filled) in enumerate(btn_specs):
            btn = create_button(button_grid, label, command, self.colors, filled=filled)
            try:
                btn.configure(wraplength=190, justify=tk.CENTER)
            except Exception:
                pass
            btn.grid(row=idx // 2, column=idx % 2, sticky="ew", padx=4, pady=4)

        logout_row = tk.Frame(footer, bg=self.colors["card"])
        logout_row.pack(fill=tk.X, pady=(14, 0))
        logout_btn = tk.Button(
            logout_row,
            text=self.t("profile_logout_btn"),
            command=confirm_logout_from_profile,
            bg=self.colors["danger_light"],
            fg=self.colors["danger"],
            activebackground=self.colors["danger"],
            activeforeground="white",
            relief=tk.FLAT,
            bd=0,
            font=("Microsoft YaHei UI", 10, "bold"),
            padx=14,
            pady=11,
            cursor="hand2",
            justify=tk.CENTER,
        )
        logout_btn.pack(fill=tk.X, padx=4)
        logout_btn.bind("<Enter>", lambda _e: logout_btn.configure(bg=self.colors["danger"], fg="white"))
        logout_btn.bind("<Leave>", lambda _e: logout_btn.configure(bg=self.colors["danger_light"], fg=self.colors["danger"]))

        refresh_profile_wrap()
        sync_scroll()
        try:
            force_center_profile()
            win.deiconify()
            force_center_profile()
            try:
                win.grab_set()
            except Exception:
                pass
            win.focus_force()
            for delay in (120, 360, 900, 1400):
                win.after(delay, force_center_profile)
        except Exception:
            pass

    def refresh_user_state(self):
        username = self.user_settings.get("username") if self.user_settings.get("logged_in") else ""
        display_name = self.current_user_display_name() if self.user_settings.get("logged_in") else self.t("not_logged_in")
        if hasattr(self, "sidebar_user_label"):
            self.sidebar_user_label.config(text=display_name)
        if hasattr(self, "pages"):
            for key in ("enterprise", "chat", "settings"):
                try:
                    if key in self.pages and hasattr(self.pages[key], "refresh"):
                        self.pages[key].refresh()
                except Exception:
                    pass

    def is_admin(self):
        accounts = self.user_settings.get("accounts", [])
        # Demo fallback: if no accounts exist yet, allow professional features.
        if not accounts:
            return True
        if not self.user_settings.get("logged_in"):
            return False
        username = self.user_settings.get("username", "")
        for acc in accounts:
            if acc.get("username") == username:
                return acc.get("role") == "admin"
        return self.user_settings.get("role") == "admin"

    def require_admin(self):
        if self.is_admin():
            return True
        messagebox.showwarning(self.t("permission_admin_required"), self.t("permission_admin_hint"))
        self.set_status("permission_admin_required")
        return False


    def _bind_global_shortcuts(self):
        """Keyboard-first workflow shortcuts.

        Ctrl+K opens the command palette, Ctrl+Shift+F opens Today Focus,
        Ctrl+Shift+P opens the Focus Timer, Ctrl+Alt+H opens the System Health
        Center, Ctrl+T adds a task, Ctrl+N adds a calendar note, and Ctrl+1..8
        switches modules. Handlers are safe on
        the login screen: they only run after the main workspace is visible.
        """
        try:
            self.root.bind_all("<Control-k>", lambda event: self.show_quick_launcher())
            self.root.bind_all("<Control-K>", lambda event: self.show_quick_launcher())
            self.root.bind_all("<Control-t>", lambda event: self.quick_add_task_dialog())
            self.root.bind_all("<Control-T>", lambda event: self.quick_add_task_dialog())
            self.root.bind_all("<Control-n>", lambda event: self.quick_add_note_dialog())
            self.root.bind_all("<Control-N>", lambda event: self.quick_add_note_dialog())
            self.root.bind_all("<Control-j>", lambda event: self.show_notification_center())
            self.root.bind_all("<Control-J>", lambda event: self.show_notification_center())
            self.root.bind_all("<Control-Shift-f>", lambda event: self.open_focus_planner())
            self.root.bind_all("<Control-Shift-F>", lambda event: self.open_focus_planner())
            self.root.bind_all("<Control-Shift-p>", lambda event: self.show_focus_timer())
            self.root.bind_all("<Control-Shift-P>", lambda event: self.show_focus_timer())
            self.root.bind_all("<Control-Alt-h>", lambda event: self.show_system_health_center())
            self.root.bind_all("<Control-Alt-H>", lambda event: self.show_system_health_center())
            for index, page in enumerate(["home", "calendar", "files", "chat", "cloud", "enterprise", "ai", "settings"], start=1):
                self.root.bind_all(f"<Control-Key-{index}>", lambda event, page_name=page: self.quick_switch_page(page_name))
        except Exception:
            pass

    def _workspace_ready(self):
        return bool(not getattr(self, "login_screen_active", True) and hasattr(self, "pages_frame"))

    def quick_switch_page(self, page):
        if not self._workspace_ready():
            return "break"
        if not self.is_page_available(page):
            page = "home"
        self.switch_page(page)
        return "break"

    def open_today_calendar(self):
        if not self._workspace_ready():
            return "break"
        page_obj = self.ensure_page("calendar")
        self.switch_page("calendar")
        try:
            page_obj.go_today()
        except Exception:
            pass
        return "break"

    def open_focus_planner(self):
        if not self._workspace_ready():
            return "break"
        try:
            page_obj = self.ensure_page("home")
            self.switch_page("home")
            if hasattr(page_obj, "open_focus_planner"):
                page_obj.open_focus_planner()
        except Exception as exc:
            try:
                messagebox.showwarning(self.t("warning_title"), str(exc))
            except Exception:
                pass
        return "break"

    def show_focus_timer(self):
        """Open a lightweight focus timer without blocking the main Tk loop."""
        if not self._workspace_ready():
            return "break"
        try:
            existing = getattr(self, "focus_timer_window", None)
            if existing is not None and existing.winfo_exists():
                existing.lift()
                existing.focus_force()
                return "break"
        except Exception:
            self.focus_timer_window = None

        timer_cfg = self.user_settings.setdefault("focus_timer", {})
        try:
            work_default = int(timer_cfg.get("work_minutes", 25))
        except Exception:
            work_default = 25
        try:
            break_default = int(timer_cfg.get("break_minutes", 5))
        except Exception:
            break_default = 5
        work_default = max(1, min(180, work_default))
        break_default = max(1, min(60, break_default))

        win = create_dialog_window(
            self.root,
            self.t("focus_timer_title"),
            self.colors,
            width=560,
            height=520,
            modal=False,
            resizable=False,
            min_width=520,
            min_height=480,
        )
        self.focus_timer_window = win
        self._set_window_icon(win)
        state = {"mode": "work", "remaining": work_default * 60, "running": False, "job": None}

        def safe_minutes(value, fallback, low, high):
            try:
                return max(low, min(high, int(str(value).strip())))
            except Exception:
                return fallback

        def cancel_job():
            job = state.get("job")
            if job:
                try:
                    self.root.after_cancel(job)
                except Exception:
                    pass
            state["job"] = None

        header = tk.Frame(win, bg=self.colors["card"])
        header.pack(fill=tk.X, padx=22, pady=(20, 8))
        tk.Label(header, text=self.t("focus_timer_title"), bg=self.colors["card"], fg=self.colors["text"], font=("Microsoft YaHei UI", 18, "bold")).pack(anchor="w")
        tk.Label(header, text=self.t("focus_timer_hint"), bg=self.colors["card"], fg=self.colors["text_secondary"], font=("Microsoft YaHei UI", 10), wraplength=500, justify=tk.LEFT).pack(anchor="w", pady=(6, 0))

        settings_row = tk.Frame(win, bg=self.colors["card"])
        settings_row.pack(fill=tk.X, padx=22, pady=(4, 14))
        work_var = tk.StringVar(value=str(work_default))
        break_var = tk.StringVar(value=str(break_default))

        def minute_field(parent, label, var, to_value):
            box = tk.Frame(parent, bg=self.colors["card_soft"], highlightthickness=1, highlightbackground=self.colors["border"])
            box.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(0, 10))
            tk.Label(box, text=label, bg=self.colors["card_soft"], fg=self.colors["text_secondary"], font=("Microsoft YaHei UI", 8, "bold")).pack(anchor="w", padx=12, pady=(8, 2))
            row = tk.Frame(box, bg=self.colors["card_soft"])
            row.pack(fill=tk.X, padx=12, pady=(0, 8))
            entry = tk.Entry(row, textvariable=var, relief=tk.FLAT, bd=0, bg=self.colors["card_soft"], fg=self.colors["text"], insertbackground=self.colors["text"], font=("Microsoft YaHei UI", 15, "bold"), width=5)
            entry.pack(side=tk.LEFT, ipady=2)
            tk.Label(row, text=self.t("focus_timer_minutes"), bg=self.colors["card_soft"], fg=self.colors["text_secondary"], font=("Microsoft YaHei UI", 9)).pack(side=tk.LEFT, padx=(6, 0))
            create_button(row, self.t("focus_timer_use"), to_value, self.colors).pack(side=tk.RIGHT)
            return entry

        display_card = tk.Frame(win, bg=self.colors["card_soft"], highlightthickness=1, highlightbackground=self.colors["border"])
        display_card.pack(fill=tk.BOTH, expand=True, padx=22, pady=(0, 14))
        mode_label = tk.Label(display_card, text="", bg=self.colors["primary_light"], fg=self.colors["primary"], font=("Microsoft YaHei UI", 10, "bold"), padx=12, pady=5)
        mode_label.pack(anchor="center", pady=(24, 8))
        time_label = tk.Label(display_card, text="25:00", bg=self.colors["card_soft"], fg=self.colors["text"], font=("Microsoft YaHei UI", 48, "bold"))
        time_label.pack(anchor="center")
        status_label = tk.Label(display_card, text="", bg=self.colors["card_soft"], fg=self.colors["text_secondary"], font=("Microsoft YaHei UI", 10), wraplength=480, justify=tk.CENTER)
        status_label.pack(anchor="center", pady=(4, 20))

        def mode_text(mode=None):
            return self.t("focus_timer_mode_break") if (mode or state["mode"]) == "break" else self.t("focus_timer_mode_work")

        def render():
            remaining = max(0, int(state.get("remaining", 0)))
            minutes, seconds = divmod(remaining, 60)
            time_label.configure(text=f"{minutes:02d}:{seconds:02d}")
            mode_label.configure(text=mode_text())
            if state.get("running"):
                status_label.configure(text=self.t("focus_timer_running"))
                start_btn.configure(text=self.t("focus_timer_running_btn"), state=tk.DISABLED)
                pause_btn.configure(state=tk.NORMAL)
            else:
                status_label.configure(text=self.t("focus_timer_paused"))
                start_btn.configure(text=self.t("focus_timer_start"), state=tk.NORMAL)
                pause_btn.configure(state=tk.DISABLED)

        def save_timer_settings():
            work_minutes = safe_minutes(work_var.get(), work_default, 1, 180)
            break_minutes = safe_minutes(break_var.get(), break_default, 1, 60)
            work_var.set(str(work_minutes))
            break_var.set(str(break_minutes))
            timer_cfg["work_minutes"] = work_minutes
            timer_cfg["break_minutes"] = break_minutes
            save_settings(self.user_settings)
            return work_minutes, break_minutes

        def set_mode(mode):
            cancel_job()
            work_minutes, break_minutes = save_timer_settings()
            state["mode"] = mode
            state["remaining"] = (break_minutes if mode == "break" else work_minutes) * 60
            state["running"] = False
            render()

        def complete_current_round():
            finished_mode = state.get("mode", "work")
            state["running"] = False
            cancel_job()
            next_mode = "break" if finished_mode == "work" else "work"
            self.log_action("focus_timer_finished", mode_text(finished_mode), "home")
            try:
                desktop_notify(self.t("focus_timer_done_title"), self.t("focus_timer_done_body").format(mode=mode_text(finished_mode)))
            except Exception:
                pass
            set_mode(next_mode)
            try:
                messagebox.showinfo(self.t("focus_timer_done_title"), self.t("focus_timer_next_hint").format(mode=mode_text(next_mode)))
            except Exception:
                pass

        def tick():
            state["job"] = None
            if not state.get("running"):
                return
            if state.get("remaining", 0) <= 0:
                complete_current_round()
                return
            state["remaining"] = int(state.get("remaining", 0)) - 1
            render()
            state["job"] = self.root.after(1000, tick)

        def start_timer():
            save_timer_settings()
            if state.get("remaining", 0) <= 0:
                set_mode(state.get("mode", "work"))
            state["running"] = True
            render()
            cancel_job()
            state["job"] = self.root.after(1000, tick)
            self.set_status("focus_timer_started")

        def pause_timer():
            state["running"] = False
            cancel_job()
            render()
            self.set_status("focus_timer_paused_status")

        def reset_timer():
            set_mode(state.get("mode", "work"))
            self.set_status("focus_timer_reset_status")

        minute_field(settings_row, self.t("focus_timer_work_minutes"), work_var, lambda: set_mode("work"))
        minute_field(settings_row, self.t("focus_timer_break_minutes"), break_var, lambda: set_mode("break"))

        buttons = tk.Frame(win, bg=self.colors["card"])
        buttons.pack(fill=tk.X, padx=22, pady=(0, 18))
        start_btn = create_button(buttons, self.t("focus_timer_start"), start_timer, self.colors, filled=True)
        start_btn.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(0, 8))
        pause_btn = create_button(buttons, self.t("focus_timer_pause"), pause_timer, self.colors)
        pause_btn.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(0, 8))
        create_button(buttons, self.t("focus_timer_reset"), reset_timer, self.colors).pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(0, 8))
        create_button(buttons, self.t("focus_timer_close"), win.destroy, self.colors).pack(side=tk.LEFT, fill=tk.X, expand=True)

        win.bind("<space>", lambda e: pause_timer() if state.get("running") else start_timer())
        win.bind("<Escape>", lambda e: win.destroy())
        win.bind("<Destroy>", lambda event: (cancel_job(), setattr(self, "focus_timer_window", None)) if event.widget is win else None)
        render()
        return "break"

    def quick_add_task_dialog(self):
        if not self._workspace_ready():
            return "break"
        text = prompt_text_dialog(
            self.root,
            self.t("quick_task_title"),
            self.t("quick_task_prompt"),
            self.colors,
            confirm_text=self.t("quick_save"),
            cancel_text=self.t("quick_cancel"),
            width=560,
            height=240,
        )
        if not text:
            return "break"
        item = self.make_todo_item(text.strip())
        item.setdefault("scope", self.current_data_scope())
        self.todos.setdefault("items", []).append(item)
        save_todos(self.todos)
        self.refresh_home()
        try:
            if "calendar" in self.pages and hasattr(self.pages["calendar"], "refresh"):
                self.pages["calendar"].refresh()
            if "home" in self.pages and hasattr(self.pages["home"], "refresh"):
                self.pages["home"].refresh()
        except Exception:
            pass
        self.log_action("quick_task_added", text[:80], "home")
        self.refresh_notification_badge()
        self.set_status("quick_task_added")
        return "break"

    def quick_add_note_dialog(self):
        if not self._workspace_ready():
            return "break"
        today_key = date.today().strftime("%Y-%m-%d")
        text = prompt_text_dialog(
            self.root,
            self.t("quick_note_title"),
            self.t("quick_note_prompt").format(date=today_key),
            self.colors,
            confirm_text=self.t("quick_save"),
            cancel_text=self.t("quick_cancel"),
            width=620,
            height=360,
            multiline=True,
        )
        if not text:
            return "break"
        now_text = datetime.now().strftime("%H:%M")
        old_note = self.notes.get(today_key, {}) if isinstance(self.notes.get(today_key, {}), dict) else {}
        if old_note.get("text"):
            merged = str(old_note.get("text", "")).rstrip() + "\n" + text.strip()
        else:
            merged = text.strip()
        self.notes[today_key] = {
            "text": merged,
            "time": old_note.get("time") or now_text,
            "image": old_note.get("image"),
            "category": old_note.get("category") or "quick",
        }
        save_notes(self.notes)
        self.refresh_home()
        try:
            if "calendar" in self.pages and hasattr(self.pages["calendar"], "refresh"):
                self.pages["calendar"].selected_date = today_key
                self.pages["calendar"].refresh()
            if "home" in self.pages and hasattr(self.pages["home"], "refresh"):
                self.pages["home"].refresh()
        except Exception:
            pass
        self.log_action("quick_note_added", today_key, "calendar")
        self.refresh_notification_badge()
        self.set_status("quick_note_added")
        return "break"

    def _quick_launcher_commands(self):
        commands = [
            {"id": "home", "title": self.t("quick_cmd_home"), "hint": "Ctrl+1", "keywords": "home dashboard beranda 首页", "action": lambda: self.switch_page("home")},
            {"id": "calendar", "title": self.t("quick_cmd_calendar"), "hint": "Ctrl+2", "keywords": "calendar 日历 jadwal", "action": lambda: self.switch_page("calendar")},
            {"id": "files", "title": self.t("quick_cmd_files"), "hint": "Ctrl+3", "keywords": "files drive documents 文件", "action": lambda: self.switch_page("files")},
            {"id": "chat", "title": self.t("quick_cmd_chat"), "hint": "Ctrl+4", "keywords": "chat message 聊天 pesan", "action": lambda: self.switch_page("chat")},
            {"id": "cloud", "title": self.t("quick_cmd_cloud"), "hint": "Ctrl+5", "keywords": "cloud drive 云端", "action": lambda: self.switch_page("cloud")},
            {"id": "ai", "title": self.t("quick_cmd_ai"), "hint": "Ctrl+7", "keywords": "ai assistant asisten", "action": lambda: self.switch_page("ai")},
            {"id": "settings", "title": self.t("quick_cmd_settings"), "hint": "Ctrl+8", "keywords": "settings 设置 pengaturan", "action": lambda: self.switch_page("settings")},
            {"id": "today", "title": self.t("quick_cmd_today"), "hint": self.format_date(date.today()), "keywords": "today 今天 hari ini calendar", "action": self.open_today_calendar},
            {"id": "focus", "title": self.t("quick_cmd_focus"), "hint": "Ctrl+Shift+F", "keywords": "focus planner today plan 专注 今日计划 fokus rencana", "action": self.open_focus_planner},
            {"id": "focus_timer", "title": self.t("quick_cmd_focus_timer"), "hint": "Ctrl+Shift+P", "keywords": "timer pomodoro focus countdown 专注计时 番茄钟 fokus timer", "action": self.show_focus_timer},
            {"id": "add_task", "title": self.t("quick_cmd_add_task"), "hint": "Ctrl+T", "keywords": "task todo 任务 tugas", "action": self.quick_add_task_dialog},
            {"id": "add_note", "title": self.t("quick_cmd_add_note"), "hint": "Ctrl+N", "keywords": "note calendar 备注 日程 catatan", "action": self.quick_add_note_dialog},
            {"id": "notifications", "title": self.t("quick_cmd_notifications"), "hint": "Ctrl+J", "keywords": "notification notice reminder 通知 提醒 pemberitahuan", "action": self.show_notification_center},
            {"id": "system_health", "title": self.t("quick_cmd_system_health"), "hint": "Ctrl+Alt+H", "keywords": "health performance check repair 系统健康 体检 优化 kesehatan sistem", "action": self.show_system_health_center},
            {"id": "fullscreen", "title": self.t("quick_cmd_fullscreen"), "hint": "Esc", "keywords": "fullscreen focus 全屏", "action": self.toggle_module_fullscreen},
            {"id": "profile", "title": self.t("quick_cmd_profile"), "hint": self.user_settings.get("username", ""), "keywords": "profile user card 个人 名片", "action": self.show_profile},
        ]
        if self.is_page_available("enterprise"):
            commands.insert(5, {"id": "enterprise", "title": self.t("quick_cmd_enterprise"), "hint": "Ctrl+6", "keywords": "enterprise company perusahaan 企业", "action": lambda: self.switch_page("enterprise")})
        elif not self.is_enterprise_mode():
            commands = [cmd for cmd in commands if cmd.get("id") not in {"cloud", "enterprise"}]
            commands.insert(5, {"id": "join_enterprise", "title": self.t("join_enterprise_btn"), "hint": self.t("mode_enterprise"), "keywords": "join enterprise company perusahaan 加入企业", "action": self.show_join_enterprise_dialog})
            commands.insert(6, {"id": "create_enterprise", "title": self.t("create_enterprise_btn"), "hint": self.t("mode_enterprise"), "keywords": "create enterprise company perusahaan 创建企业", "action": self.show_create_enterprise_dialog})
        return commands

    def show_quick_launcher(self):
        if not self._workspace_ready():
            return "break"
        try:
            if getattr(self, "quick_launcher_window", None) is not None and self.quick_launcher_window.winfo_exists():
                self.quick_launcher_window.lift()
                return "break"
        except Exception:
            self.quick_launcher_window = None

        commands = self._quick_launcher_commands()
        win = tk.Toplevel(self.root)
        self.quick_launcher_window = win
        win.title(self.t("quick_launcher_title"))
        win.configure(bg=self.colors["bg"])
        self._set_window_icon(win)
        try:
            w, h = 720, 560
            win.transient(self.root)
            center_window(win, self.root, w, h, min_width=560, min_height=420)
        except Exception:
            win.geometry("720x560")

        card = tk.Frame(win, bg=self.colors["card"], highlightthickness=1, highlightbackground=self.colors["border"])
        card.pack(fill=tk.BOTH, expand=True, padx=18, pady=18)
        tk.Label(card, text=self.t("quick_launcher_title"), bg=self.colors["card"], fg=self.colors["text"], font=("Microsoft YaHei UI", 18, "bold")).pack(anchor="w", padx=18, pady=(16, 4))
        tk.Label(card, text=self.t("quick_launcher_subtitle"), bg=self.colors["card"], fg=self.colors["text_secondary"], font=("Microsoft YaHei UI", 10), wraplength=650, justify=tk.LEFT).pack(anchor="w", padx=18, pady=(0, 12))

        search_box = tk.Frame(card, bg=self.colors["card_soft"], highlightthickness=1, highlightbackground=self.colors["border"])
        search_box.pack(fill=tk.X, padx=18, pady=(0, 12))
        entry = tk.Entry(search_box, relief=tk.FLAT, bd=0, bg=self.colors["card_soft"], fg=self.colors["text"], insertbackground=self.colors["text"], font=("Microsoft YaHei UI", 12))
        entry.pack(fill=tk.X, padx=12, pady=10, ipady=4)
        entry.insert(0, "")

        list_frame = tk.Frame(card, bg=self.colors["card"])
        list_frame.pack(fill=tk.BOTH, expand=True, padx=18, pady=(0, 10))
        scrollbar = tk.Scrollbar(list_frame, orient=tk.VERTICAL)
        listbox = tk.Listbox(
            list_frame,
            activestyle="none",
            relief=tk.FLAT,
            bd=0,
            highlightthickness=1,
            highlightbackground=self.colors["border"],
            bg=self.colors["card_soft"],
            fg=self.colors["text"],
            selectbackground=self.colors["primary"],
            selectforeground="white",
            font=("Microsoft YaHei UI", 11),
            yscrollcommand=scrollbar.set,
        )
        scrollbar.config(command=listbox.yview)
        listbox.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
        scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
        visible = []

        def render():
            query = entry.get().strip().lower()
            visible.clear()
            listbox.delete(0, tk.END)
            for command in commands:
                haystack = " ".join([command.get("title", ""), command.get("hint", ""), command.get("keywords", "")]).lower()
                if query and query not in haystack:
                    continue
                visible.append(command)
                hint = f"    {command.get('hint', '')}" if command.get("hint") else ""
                listbox.insert(tk.END, f"{command.get('title', '')}{hint}")
            if visible:
                listbox.selection_clear(0, tk.END)
                listbox.selection_set(0)
                listbox.activate(0)
            else:
                listbox.insert(tk.END, self.t("quick_launcher_empty"))

        def move(delta):
            if not visible:
                return "break"
            current = listbox.curselection()
            idx = current[0] if current else 0
            idx = max(0, min(len(visible) - 1, idx + delta))
            listbox.selection_clear(0, tk.END)
            listbox.selection_set(idx)
            listbox.activate(idx)
            listbox.see(idx)
            return "break"

        def run_selected(event=None):
            if not visible:
                return "break"
            current = listbox.curselection()
            idx = current[0] if current else 0
            if not (0 <= idx < len(visible)):
                return "break"
            command = visible[idx]
            try:
                win.destroy()
            except Exception:
                pass
            try:
                command.get("action", lambda: None)()
                self.log_action("quick_launcher_executed", command.get("title", ""), "home")
            except Exception as exc:
                try:
                    messagebox.showwarning(self.t("warning_title"), str(exc))
                except Exception:
                    pass
            return "break"

        def close(event=None):
            try:
                win.destroy()
            except Exception:
                pass
            return "break"

        entry.bind("<KeyRelease>", lambda event: render())
        entry.bind("<Down>", lambda event: move(1))
        entry.bind("<Up>", lambda event: move(-1))
        entry.bind("<Return>", run_selected)
        listbox.bind("<Double-Button-1>", run_selected)
        listbox.bind("<Return>", run_selected)
        listbox.bind("<Escape>", close)
        entry.bind("<Escape>", close)
        win.bind("<Escape>", close)
        win.bind("<Destroy>", lambda event: setattr(self, "quick_launcher_window", None) if event.widget is win else None)
        render()
        entry.focus_set()
        return "break"

    def get_quick_actions(self):
        actions = self.user_settings.setdefault("quick_actions", {"focus": True, "calendar": True, "task": True, "note": True, "chat": True, "cloud": False, "enterprise": False, "ai": True})
        for key in ["focus", "calendar", "task", "note", "chat", "cloud", "enterprise", "ai"]:
            actions.setdefault(key, key not in {"cloud", "enterprise"})
        if not self.is_enterprise_mode():
            # In Personal edition, company-only quick actions stay hidden. The
            # only enterprise entry points are the Join/Create Enterprise buttons.
            for key in ("cloud", "enterprise"):
                actions[key] = False
            actions["chat"] = True
        return actions

    def set_quick_action(self, key, value):
        self.get_quick_actions()[key] = bool(value)
        save_settings(self.user_settings)
        if "home" in self.pages:
            self.pages["home"].update_language()
        if "settings" in self.pages:
            self.pages["settings"].refresh()
        self.log_action("changed_quick_button", f"{key}: {value}", "settings")
        self.set_status("status_saved")

    def get_quick_label_raw(self, key):
        labels = self.user_settings.setdefault("quick_action_labels", {"focus": "", "calendar": "", "task": "", "note": "", "chat": "", "cloud": "", "enterprise": "", "ai": ""})
        labels.setdefault(key, "")
        return str(labels.get(key, "") or "")

    def get_quick_label(self, key, fallback):
        value = self.get_quick_label_raw(key).strip()
        return value or fallback

    def set_quick_label(self, key, value, silent=False):
        self.user_settings.setdefault("quick_action_labels", {"focus": "", "calendar": "", "task": "", "note": "", "chat": "", "cloud": "", "enterprise": "", "ai": ""})[key] = value
        save_settings(self.user_settings)
        if "home" in self.pages:
            self.pages["home"].update_language()
        if "settings" in self.pages:
            self.pages["settings"].refresh()
        if not silent:
            self.set_status("quick_saved")

    def get_notification_settings(self):
        settings = self.user_settings.setdefault("notification_settings", {"show_tasks": True, "show_calendar": True, "show_holidays": True, "show_enterprise": False})
        for key in ["show_tasks", "show_calendar", "show_holidays"]:
            settings.setdefault(key, True)
        settings.setdefault("show_enterprise", bool(self.is_enterprise_mode()))
        if not self.is_enterprise_mode():
            settings["show_enterprise"] = False
        return settings

    def _notification_id(self, item):
        raw = "|".join(str(item.get(k, "")) for k in ("kind", "date", "time", "title", "content"))
        return hashlib.sha1(raw.encode("utf-8")).hexdigest()[:16]

    def _notification_read_set(self):
        raw = self.user_settings.setdefault("notification_read_ids", [])
        if not isinstance(raw, list):
            raw = []
            self.user_settings["notification_read_ids"] = raw
        return set(str(x) for x in raw)

    def _notification_button_text(self, compact=False):
        try:
            count = self.get_unread_notification_count()
        except Exception:
            count = 0
        if compact:
            return f"🔔{count}" if count else "🔔"
        return f"🔔 {count}" if count else "🔔"

    def refresh_notification_badge(self):
        try:
            btn = getattr(self, "notification_btn", None)
            if btn:
                count = self.get_unread_notification_count()
                compact = self.layout_profile.get("header") in {"compact", "tiny"} if hasattr(self, "layout_profile") else False
                btn.configure(
                    text=self._notification_button_text(compact=compact),
                    fg=self.colors["danger"] if count else self.colors["primary"],
                    bg=self.colors["danger_light"] if count else self.colors["card"],
                )
        except Exception:
            pass

    def _first_line(self, text, limit=120):
        value = " ".join(str(text or "").strip().split())
        if len(value) > limit:
            value = value[: limit - 1] + "…"
        return value

    def task_now_text(self):
        return datetime.now().strftime("%Y-%m-%d %H:%M:%S")

    def task_date_text(self, value):
        value = str(value or "").strip()[:10]
        try:
            datetime.strptime(value, "%Y-%m-%d")
            return value
        except Exception:
            return ""

    def parse_task_text(self, raw_text):
        """Parse lightweight task syntax used by quick add, Home, and reports.

        Syntax kept intentionally simple before online sync is enabled:
        - !!任务 = urgent, !任务 = high priority
        - @YYYY-MM-DD = due date
        - today/tomorrow/今天/明天/hari ini/besok = relative due date
        - #标签 = tag, +负责人 = assignee
        """
        original = str(raw_text or "").strip()
        text = original
        priority = "normal"
        if text.startswith("!!"):
            priority = "urgent"
            text = text[2:].strip()
        elif text.startswith("!"):
            priority = "high"
            text = text[1:].strip()

        due_date = ""
        due_match = re.search(r"(?:^|\s)@(\d{4}-\d{2}-\d{2})(?=\s|$)", text)
        if due_match:
            due_date = self.task_date_text(due_match.group(1))
            text = (text[:due_match.start()] + " " + text[due_match.end():]).strip()

        tags = []
        for tag in re.findall(r"(?<!\S)#([\w一-鿿\-]{1,24})", text):
            if tag not in tags:
                tags.append(tag)
        text = re.sub(r"(?<!\S)#[\w一-鿿\-]{1,24}", " ", text).strip()

        assignee = ""
        owner_match = re.search(r"(?:^|\s)\+([\w一-鿿\-]{1,24})(?=\s|$)", text)
        if owner_match:
            assignee = owner_match.group(1)
            text = (text[:owner_match.start()] + " " + text[owner_match.end():]).strip()

        lowered = text.lower()
        if not due_date:
            for words, offset in [(("今天", "今日", "today", "hari ini"), 0), (("明天", "tomorrow", "besok"), 1)]:
                if any(word in lowered for word in words):
                    due_date = (date.today() + timedelta(days=offset)).strftime("%Y-%m-%d")
                    break

        now = self.task_now_text()
        return {
            "text": text or original,
            "done": False,
            "priority": priority,
            "due_date": due_date,
            "tags": tags,
            "assignee": assignee,
            "created_at": now,
            "updated_at": now,
            "scope": self.current_data_scope() if hasattr(self, "current_data_scope") else "personal",
        }

    def make_todo_item(self, raw_text):
        item = self.parse_task_text(raw_text)
        if not item.get("text"):
            item["text"] = str(raw_text or "").strip()
        return item

    def task_due_bucket(self, item):
        due = self.task_date_text(item.get("due_date") if isinstance(item, dict) else "")
        if not due:
            return 4
        try:
            due_day = datetime.strptime(due, "%Y-%m-%d").date()
        except Exception:
            return 4
        today = date.today()
        if due_day < today:
            return 0
        if due_day == today:
            return 1
        if due_day == today + timedelta(days=1):
            return 2
        return 3

    def task_priority_rank(self, item):
        return {"urgent": 0, "high": 1, "normal": 2, "low": 3}.get((item or {}).get("priority", "normal"), 2)

    def task_sort_key(self, idx, item):
        if not isinstance(item, dict):
            item = {"text": str(item), "done": False}
        done_bucket = 1 if item.get("done") else 0
        time_key = str(item.get("due_date") or item.get("created_at") or item.get("updated_at") or "9999")
        return (done_bucket, self.task_due_bucket(item), self.task_priority_rank(item), time_key, idx)

    def ordered_todos(self):
        return sorted(self.visible_todo_items(), key=lambda pair: self.task_sort_key(pair[0], pair[1]))

    def task_meta_parts(self, item):
        if not isinstance(item, dict):
            return []
        parts = []
        priority = item.get("priority", "normal")
        if priority in ("urgent", "high"):
            parts.append(self.t(f"task_priority_{priority}"))
        due = self.task_date_text(item.get("due_date"))
        if due:
            try:
                due_day = datetime.strptime(due, "%Y-%m-%d").date()
                today = date.today()
                if due_day < today and not item.get("done"):
                    parts.append(self.t("task_meta_overdue").format(date=due))
                elif due_day == today:
                    parts.append(self.t("task_meta_due_today"))
                elif due_day == today + timedelta(days=1):
                    parts.append(self.t("task_meta_due_tomorrow"))
                else:
                    parts.append(self.t("task_meta_due").format(date=due))
            except Exception:
                parts.append(self.t("task_meta_due").format(date=due))
        assignee = str(item.get("assignee") or "").strip()
        if assignee:
            parts.append(self.t("task_meta_assignee").format(name=assignee))
        tags = [str(tag).strip() for tag in item.get("tags", []) if str(tag).strip()]
        if tags:
            parts.append(self.t("task_meta_tags").format(tags=", ".join("#" + tag for tag in tags[:3])))
        if item.get("done") and item.get("completed_at"):
            parts.append(self.t("task_meta_completed").format(time=str(item.get("completed_at"))[:16]))
        return parts

    def task_meta_text(self, item, separator=" · "):
        return separator.join(self.task_meta_parts(item))

    def get_task_notifications(self, limit=12):
        if not self.get_notification_settings().get("show_tasks", True):
            return []
        items = []
        ordered = [(idx, item) for idx, item in self.ordered_todos() if isinstance(item, dict) and not item.get("done")]
        for idx, item in ordered[:limit]:
            text = self._first_line(item.get("text"), 110)
            if not text:
                continue
            created = item.get("created_at") or item.get("time") or ""
            due = self.task_date_text(item.get("due_date"))
            meta = self.task_meta_text(item)
            content = text + (f" · {meta}" if meta else "")
            items.append({
                "kind": "task",
                "date": due or (created[:10] if created else date.today().strftime("%Y-%m-%d")),
                "title": self.t("notification_task_title"),
                "content": content,
                "time": due or created or self.t("notification_pending"),
                "target": "home",
                "importance": self.task_due_bucket(item) * 10 + self.task_priority_rank(item),
                "task_index": idx,
            })
        return items

    def get_calendar_notifications(self, days=7):
        if not self.get_notification_settings().get("show_calendar", True):
            return []
        today = date.today()
        items = []
        for offset in range(0, days + 1):
            ds = (today + timedelta(days=offset)).strftime("%Y-%m-%d")
            note = self.notes.get(ds)
            if not isinstance(note, dict) or not str(note.get("text", "")).strip():
                continue
            rel = self.get_relative_day_text(offset)
            title = self.t("notification_calendar_title").format(date=ds)
            content = f"{rel} · {self._first_line(note.get('text'), 120)}"
            if note.get("time"):
                content = f"{note.get('time')} · {content}"
            items.append({
                "kind": "calendar",
                "date": ds,
                "title": title,
                "content": content,
                "time": ds,
                "target": "calendar",
            })
        return items

    def get_all_notifications(self, include_read=True, limit=80):
        read = self._notification_read_set()
        settings = self.get_notification_settings()
        items = []
        if settings.get("show_enterprise", True) and self.is_enterprise_mode():
            for notice in reversed(self.enterprise.get("notifications", [])):
                if self.matches_search(notice.get("title"), notice.get("content"), notice.get("audience_label")):
                    items.append({
                        "kind": "enterprise",
                        "date": notice.get("time", ""),
                        "title": notice.get("title", ""),
                        "content": notice.get("content", ""),
                        "time": notice.get("time", ""),
                        "target": "enterprise",
                    })
        if settings.get("show_holidays", True):
            items.extend(self.get_upcoming_holiday_notifications(days=14)[:20])
        items.extend(self.get_calendar_notifications(days=7)[:20])
        items.extend(self.get_task_notifications(limit=20))
        normalized = []
        seen = set()
        for item in items:
            nid = self._notification_id(item)
            if nid in seen:
                continue
            seen.add(nid)
            item = dict(item)
            item["id"] = nid
            item["read"] = nid in read
            if include_read or not item["read"]:
                normalized.append(item)
        kind_order = {"task": 0, "calendar": 1, "holiday": 2, "enterprise": 3}
        normalized.sort(key=lambda x: (
            x.get("read", False),
            int(x.get("importance", 50)) if x.get("kind") == "task" else 20,
            kind_order.get(x.get("kind"), 9),
            str(x.get("date") or x.get("time") or ""),
        ))
        return normalized[:limit]

    def get_unread_notification_count(self):
        return len(self.get_all_notifications(include_read=False, limit=200))

    def mark_notification_read(self, notification_id=None, all_current=False):
        ids = list(self.user_settings.setdefault("notification_read_ids", []))
        current = [item.get("id") for item in self.get_all_notifications(include_read=True, limit=200) if item.get("id")]
        if all_current:
            ids.extend(current)
        elif notification_id:
            ids.append(notification_id)
        unique = []
        seen = set()
        for item in ids[-300:]:
            if item and item not in seen:
                seen.add(item)
                unique.append(item)
        self.user_settings["notification_read_ids"] = unique[-200:]
        save_settings(self.user_settings)
        self.refresh_notification_badge()

    def _open_notification_target(self, item):
        try:
            self.mark_notification_read(item.get("id"))
            kind = item.get("kind")
            if kind in ("holiday", "calendar") and item.get("date"):
                self.open_calendar_date(item.get("date"))
            elif kind == "task":
                self.switch_page("home")
                page = self.pages.get("home") if hasattr(self, "pages") else None
                if page and hasattr(page, "open_all_tasks"):
                    try:
                        page.open_all_tasks()
                    except Exception:
                        pass
            elif kind == "enterprise" and self.is_page_available("enterprise"):
                self.switch_page("enterprise")
            else:
                self.switch_page("home")
        except Exception:
            pass

    def show_notification_center(self):
        if not self._workspace_ready():
            return "break"
        try:
            if getattr(self, "notification_window", None) is not None and self.notification_window.winfo_exists():
                self.notification_window.lift()
                self.notification_window.focus_force()
                return "break"
        except Exception:
            self.notification_window = None

        win = create_dialog_window(self.root, self.t("notification_center"), self.colors, width=620, height=680, modal=False, resizable=True, min_width=540, min_height=520)
        self.notification_window = win
        win.bind("<Destroy>", lambda event: setattr(self, "notification_window", None) if event.widget is win else None)
        try:
            win.bind("<Escape>", lambda e: [win.destroy(), "break"])
        except Exception:
            pass

        header = tk.Frame(win, bg=self.colors["card"])
        header.pack(fill=tk.X, padx=22, pady=(18, 8))
        tk.Label(header, text=self.t("notification_center"), font=("Microsoft YaHei UI", 18, "bold"), fg=self.colors["text"], bg=self.colors["card"]).pack(side=tk.LEFT)
        unread_label = tk.Label(header, text="", font=("Microsoft YaHei UI", 10, "bold"), fg=self.colors["danger"], bg=self.colors["danger_light"], padx=10, pady=5)
        unread_label.pack(side=tk.RIGHT)
        tk.Label(win, text=self.t("notification_center_hint"), font=("Microsoft YaHei UI", 10), fg=self.colors["text_secondary"], bg=self.colors["card"], wraplength=560, justify=tk.LEFT).pack(anchor="w", padx=22, pady=(0, 12))

        actions = tk.Frame(win, bg=self.colors["card"])
        actions.pack(fill=tk.X, padx=22, pady=(0, 10))
        show_unread_only = tk.BooleanVar(value=False)

        def render():
            for child in inner.winfo_children():
                child.destroy()
            all_items = self.get_all_notifications(include_read=True, limit=120)
            unread_count = sum(1 for item in all_items if not item.get("read"))
            unread_label.configure(text=self.t("notification_unread_count").format(count=unread_count))
            items = [item for item in all_items if not item.get("read")] if show_unread_only.get() else all_items
            if not items:
                tk.Label(inner, text=self.t("notification_empty"), font=("Microsoft YaHei UI", 10), fg=self.colors["text_light"], bg=self.colors["card"]).pack(pady=28)
                return
            for item in items:
                row = tk.Frame(inner, bg=self.colors["card_soft"], highlightthickness=1, highlightbackground=self.colors["border"], cursor="hand2")
                row.pack(fill=tk.X, pady=6)
                top = tk.Frame(row, bg=self.colors["card_soft"])
                top.pack(fill=tk.X, padx=12, pady=(10, 3))
                dot_color = self.colors["text_light"] if item.get("read") else self.colors["danger"]
                tk.Label(top, text="●", fg=dot_color, bg=self.colors["card_soft"], font=("Microsoft YaHei UI", 9, "bold")).pack(side=tk.LEFT, padx=(0, 6))
                tk.Label(top, text=kind_label(item.get("kind")), fg=self.colors["primary"], bg=self.colors["primary_light"], font=("Microsoft YaHei UI", 8, "bold"), padx=7, pady=2).pack(side=tk.LEFT, padx=(0, 8))
                tk.Label(top, text=str(item.get("time") or item.get("date") or ""), fg=self.colors["text_secondary"], bg=self.colors["card_soft"], font=("Microsoft YaHei UI", 8)).pack(side=tk.RIGHT)
                tk.Label(row, text=item.get("title", ""), fg=self.colors["text"], bg=self.colors["card_soft"], font=("Microsoft YaHei UI", 11, "bold"), wraplength=500, justify=tk.LEFT).pack(anchor="w", padx=12, pady=(0, 3))
                tk.Label(row, text=item.get("content", ""), fg=self.colors["text_secondary"], bg=self.colors["card_soft"], font=("Microsoft YaHei UI", 9), wraplength=500, justify=tk.LEFT).pack(anchor="w", padx=12, pady=(0, 10))
                def bind_click(widget, value=item):
                    try:
                        widget.bind("<Button-1>", lambda e, payload=value: [win.destroy(), self._open_notification_target(payload)])
                    except Exception:
                        pass
                    for child in widget.winfo_children():
                        bind_click(child, value)
                bind_click(row)
            self.refresh_notification_badge()

        tk.Checkbutton(actions, text=self.t("notification_unread_only"), variable=show_unread_only, bg=self.colors["card"], activebackground=self.colors["card"], selectcolor=self.colors["card"], fg=self.colors["text"], font=("Microsoft YaHei UI", 9, "bold"), command=render).pack(side=tk.LEFT)
        create_button(actions, self.t("notification_mark_all_read"), lambda: [self.mark_notification_read(all_current=True), render()], self.colors).pack(side=tk.RIGHT)

        outer = tk.Frame(win, bg=self.colors["card"])
        outer.pack(fill=tk.BOTH, expand=True, padx=22, pady=(0, 18))
        canvas = tk.Canvas(outer, bg=self.colors["card"], highlightthickness=0)
        scrollbar = tk.Scrollbar(outer, orient="vertical", command=canvas.yview)
        inner = tk.Frame(canvas, bg=self.colors["card"])
        inner_id = canvas.create_window((0, 0), window=inner, anchor="nw")
        canvas.configure(yscrollcommand=scrollbar.set)
        canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
        scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
        inner.bind("<Configure>", lambda e: canvas.configure(scrollregion=canvas.bbox("all")))
        canvas.bind("<Configure>", lambda e: canvas.itemconfig(inner_id, width=max(e.width - 4, 420)))

        def kind_label(kind):
            return {
                "task": self.t("notification_kind_task"),
                "calendar": self.t("notification_kind_calendar"),
                "holiday": self.t("notification_kind_holiday"),
                "enterprise": self.t("notification_kind_enterprise"),
            }.get(kind, self.t("notification_kind_general"))

        render()
        self.log_action("notification_center_opened", "", "home")
        return "break"

    def get_holiday_reminders(self):
        reminders = self.user_settings.setdefault("holiday_reminders", {"china": True, "indonesia": True, "days_before": 1})
        self.user_settings.setdefault("accounts", [])
        self.user_settings.setdefault("puid", "")
        self.user_settings.setdefault("personal_puid", "")
        self.user_settings.setdefault("enterprise_puid", "")
        reminders.setdefault("china", True)
        reminders.setdefault("indonesia", True)
        reminders.setdefault("days_before", 1)
        return reminders

    def set_holiday_reminder(self, key, value):
        reminders = self.get_holiday_reminders()
        if key == "days_before":
            try:
                reminders[key] = max(0, min(14, int(value)))
            except Exception:
                reminders[key] = 1
        else:
            reminders[key] = bool(value)
        save_settings(self.user_settings)
        if "settings" in self.pages:
            self.pages["settings"].refresh()
        if "home" in self.pages:
            self.pages["home"].refresh()
        self.log_action("changed_holiday_reminder", f"{key}: {value}", "settings")
        self.set_status("status_saved")

    def _holiday_enabled(self, htype):
        reminders = self.get_holiday_reminders()
        return (htype == "china" and reminders.get("china", True)) or (htype == "indonesia" and reminders.get("indonesia", True))

    def get_relative_day_text(self, days):
        if days == 0:
            return self.t("holiday_today")
        if days == 1:
            return self.t("holiday_tomorrow")
        return self.t("holiday_in_days").format(days=days)

    def get_upcoming_holiday_notifications(self, days=7):
        today = date.today()
        items = []
        for offset in range(0, days + 1):
            d = today + timedelta(days=offset)
            ds = d.strftime("%Y-%m-%d")
            info = self.holidays.get(ds)
            if not info:
                continue
            htype = info.get("type", "")
            if htype not in ("china", "indonesia") or not self._holiday_enabled(htype):
                continue
            name = self.holiday_name(info)
            type_key = "holiday_type_china" if htype == "china" else "holiday_type_indonesia"
            items.append({
                "kind": "holiday",
                "date": ds,
                "title": f"{self.t('notification_holiday')} · {name}",
                "content": f"{self.t(type_key)} · {self.get_relative_day_text(offset)}",
                "time": ds,
                "type": htype,
            })
        return items

    def get_home_notifications(self):
        # Home uses the same normalized feed as the header notification center,
        # but keeps read items visible so users can still find recent reminders.
        return self.get_all_notifications(include_read=True, limit=40)

    def refresh_home(self):
        if "home" not in self.pages:
            return
        if self._home_refresh_job:
            try:
                self.root.after_cancel(self._home_refresh_job)
            except Exception:
                pass
        delay = 220 if self.is_low_power_mode() else 90

        def do_refresh():
            self._home_refresh_job = None
            try:
                if "home" in self.pages:
                    self.pages["home"].refresh()
                self.refresh_notification_badge()
            except Exception:
                pass

        self._home_refresh_job = self.root.after(delay, do_refresh)

    def run_desktop_preflight(self):
        """Run desktop preflight: icon generation, icon verification, desktop doctor."""
        try:
            tool = BASE_DIR / "tools" / "desktop_preflight.py"
            if not tool.exists():
                messagebox.showwarning(self.t("warning_title"), self.t("desktop_preflight_missing"))
                return
            result = subprocess.run([sys.executable, str(tool)], cwd=BASE_DIR, capture_output=True, text=True, timeout=35)
            raw = (result.stdout or "").strip()
            ok = result.returncode == 0
            try:
                payload = json.loads(raw)
                ok = bool(payload.get("ok"))
            except Exception:
                payload = {"ok": ok, "stdout": raw, "stderr": result.stderr[-1000:]}
            try:
                (DATA_DIR / "desktop_preflight_latest.json").write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8")
            except Exception:
                pass
            if ok:
                self._reapply_root_icon()
                messagebox.showinfo(self.t("desktop_preflight_menu"), self.t("desktop_preflight_done"))
            else:
                messagebox.showwarning(self.t("desktop_preflight_menu"), self.t("desktop_preflight_failed").format(path=str(DATA_DIR / "desktop_preflight_latest.json")))
        except subprocess.TimeoutExpired:
            try:
                (DATA_DIR / "desktop_preflight_latest.json").write_text(json.dumps({"ok": False, "timeout": True}, ensure_ascii=False, indent=2), encoding="utf-8")
            except Exception:
                pass
            messagebox.showwarning(self.t("desktop_preflight_menu"), self.t("desktop_preflight_timeout"))
        except Exception as exc:
            messagebox.showwarning(self.t("warning_title"), str(exc))

    def open_latest_build_folder(self):
        """Open latest dist output folder if it exists."""
        try:
            tool = BASE_DIR / "tools" / "open_latest_build.bat"
            if not tool.exists():
                messagebox.showwarning(self.t("warning_title"), self.t("open_latest_build_missing"))
                return
            if sys.platform.startswith("win"):
                os.startfile(str(tool))
                return
            path = BASE_DIR / "dist" / "NexaOffice_WorkSuite"
            if path.exists():
                open_file_path(path)
            else:
                messagebox.showinfo(self.t("open_latest_build_menu"), self.t("open_latest_build_not_found"))
        except Exception as exc:
            messagebox.showwarning(self.t("warning_title"), str(exc))

    def open_build_cleanup_tool(self):
        """Open helper to clean temporary build folders/backups."""
        try:
            tool = BASE_DIR / "tools" / "cleanup_build_artifacts.bat"
            if not tool.exists():
                messagebox.showwarning(self.t("warning_title"), self.t("build_cleanup_tool_missing"))
                return
            if not sys.platform.startswith("win"):
                messagebox.showinfo(self.t("build_cleanup_tool_menu"), self.t("build_cleanup_tool_windows_only"))
                return
            os.startfile(str(tool))
        except Exception as exc:
            messagebox.showwarning(self.t("warning_title"), str(exc))

    def open_force_exit_tool(self):
        """Open a Windows helper that force-closes NexaOffice if tray exit is unreachable."""
        try:
            tool = BASE_DIR / "tools" / "force_exit_nexaoffice.bat"
            if not tool.exists():
                messagebox.showwarning(self.t("warning_title"), self.t("force_exit_tool_missing"))
                return
            if not sys.platform.startswith("win"):
                messagebox.showinfo(self.t("force_exit_tool_menu"), self.t("force_exit_tool_windows_only"))
                return
            os.startfile(str(tool))
        except Exception as exc:
            messagebox.showwarning(self.t("warning_title"), str(exc))

    def run_desktop_doctor(self):
        """Run desktop doctor for tray/icon/build diagnostics and save latest result."""
        try:
            tool = BASE_DIR / "tools" / "desktop_doctor.py"
            if not tool.exists():
                messagebox.showwarning(self.t("warning_title"), self.t("desktop_doctor_missing"))
                return
            result = subprocess.run([sys.executable, str(tool)], cwd=BASE_DIR, capture_output=True, text=True)
            raw = (result.stdout or "").strip()
            payload = {}
            try:
                payload = json.loads(raw)
            except Exception:
                payload = {"ok": result.returncode == 0, "raw": raw, "stderr": result.stderr[-1000:]}
            try:
                out_path = DATA_DIR / "desktop_doctor_latest.json"
                out_path.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8")
            except Exception:
                pass

            pystray_ok = payload.get("pystray", {}).get("ok")
            icons_ok = payload.get("verify_icon_assets", {}).get("ok")
            lines = [
                f"{self.t('desktop_doctor_result')}: {'OK' if payload.get('ok') else 'WARN'}",
                f"{self.t('self_check_tray')}: {pystray_ok}",
                f"{self.t('self_check_icon')}: {icons_ok}",
                f"{self.t('desktop_doctor_saved')}: {DATA_DIR / 'desktop_doctor_latest.json'}",
            ]
            err = payload.get("pystray", {}).get("error") or payload.get("verify_icon_assets", {}).get("stderr") or ""
            if err:
                lines.append("")
                lines.append(str(err)[-500:])
            messagebox.showinfo(self.t("desktop_doctor_menu"), "\n".join(lines))
        except Exception as exc:
            messagebox.showwarning(self.t("warning_title"), str(exc))

    def regenerate_icon_assets(self):
        """Regenerate icon PNG/ICO resources from bundled master images."""
        try:
            tool = BASE_DIR / "tools" / "generate_app_icons.py"
            if not tool.exists():
                messagebox.showwarning(self.t("warning_title"), self.t("icon_generator_missing"))
                return
            result = subprocess.run([sys.executable, str(tool)], cwd=BASE_DIR, capture_output=True, text=True)
            if result.returncode == 0:
                self._reapply_root_icon()
                try:
                    self.root.after(300, self._reapply_root_icon)
                except Exception:
                    pass
                messagebox.showinfo(self.t("icon_generator_title"), self.t("icon_generator_done"))
            else:
                msg = (result.stderr or result.stdout or "").strip()
                messagebox.showwarning(self.t("icon_generator_title"), self.t("icon_generator_failed").format(error=msg[-800:]))
        except Exception as exc:
            messagebox.showwarning(self.t("warning_title"), str(exc))

    def open_icon_cache_tool(self):
        """Open the Windows icon cache refresh helper script."""
        try:
            tool = BASE_DIR / "tools" / "refresh_windows_icon_cache.bat"
            if not tool.exists():
                messagebox.showwarning(self.t("warning_title"), self.t("icon_cache_tool_missing"))
                return
            if not sys.platform.startswith("win"):
                messagebox.showinfo(self.t("icon_cache_tool_title"), self.t("icon_cache_tool_windows_only"))
                return
            os.startfile(str(tool))
        except Exception as exc:
            messagebox.showwarning(self.t("warning_title"), str(exc))

    def open_data_folder(self):
        try:
            DATA_DIR.mkdir(parents=True, exist_ok=True)
            if sys.platform.startswith("win"):
                os.startfile(str(DATA_DIR))
            elif sys.platform == "darwin":
                subprocess.Popen(["open", str(DATA_DIR)])
            else:
                subprocess.Popen(["xdg-open", str(DATA_DIR)])
            self.set_status("status_ready")
        except Exception as exc:
            messagebox.showwarning(self.t("warning_title"), str(exc))

    def open_calendar_date(self, date_key):
        self.switch_page("calendar")
        page = self.ensure_page("calendar")
        if page and hasattr(page, "open_date"):
            self.root.after(60, lambda: page.open_date(date_key))

    def open_document(self, doc_id):
        self.switch_page("files")
        page = self.ensure_page("files")
        if page and hasattr(page, "open_doc"):
            self.root.after(60, lambda: page.open_doc(doc_id))

    def _status_style(self, key_or_text):
        if key_or_text in ("status_ready", "status_saved", "profile_saved", "language_changed", "notification_sent", "sync_success"):
            return self.colors["success"], self.colors["success_light"]
        if key_or_text == "status_searching":
            return self.colors["warning"], self.colors["warning_light"]
        if key_or_text in ("status_reset",):
            return self.colors["danger"], self.colors["danger_light"]
        return self.colors["primary"], self.colors["primary_light"]

    def _global_click_blur(self, event=None):
        widget = getattr(event, "widget", None)
        cls = widget.winfo_class() if widget else ""
        if cls not in ("Entry", "Text", "TEntry", "Spinbox"):
            try:
                self.root.focus_set()
            except Exception:
                pass

    def set_status(self, key_or_text="status_ready"):
        text = self.t(key_or_text) if key_or_text in self.lang else str(key_or_text)
        fg, bg = self._status_style(key_or_text)
        self.status_text = text
        self.status_label.config(text=f"●  {text}", fg=fg)
        if key_or_text not in ("status_ready", "status_searching") and hasattr(self, "action_logs"):
            self.log_action(text, "", getattr(self, "current_page", ""))
        if "home" in getattr(self, "pages", {}) and hasattr(self.pages["home"], "set_status"):
            self.pages["home"].set_status(text, fg, bg)

    def _search_focus_in(self, event=None):
        if self.search_entry.get() == self.t("search_placeholder") or self.search_entry.get() in self.all_values("search_placeholder"):
            self.search_entry.delete(0, tk.END)
            self.search_entry.config(fg=self.colors["text"])

    def _search_focus_out(self, event=None):
        if not self.search_entry.get().strip():
            self.search_entry.insert(0, self.t("search_placeholder"))
            self.search_entry.config(fg=self.colors["text_secondary"])

    def _on_search_change(self, event=None):
        value = self.search_entry.get().strip()
        if value == self.t("search_placeholder"):
            value = ""
        self.search_keyword = value.lower()
        self.set_status("status_searching" if self.search_keyword else "status_ready")
        if self.search_after_id:
            self.root.after_cancel(self.search_after_id)
        self.search_after_id = self.root.after(180, self.apply_search)

    def apply_search(self):
        self.search_after_id = None
        page = self.ensure_page(self.current_page)
        if page and hasattr(page, "refresh"):
            page.refresh()
        if self.search_keyword:
            self.log_action("changed_search", self.search_keyword, self.current_page)
        self.set_status("status_ready")

    def clear_search(self):
        self.log_action("cleared_search", "", self.current_page)
        self.search_keyword = ""
        if getattr(self, "search_entry", None) is not None:
            self.search_entry.delete(0, tk.END)
            self.search_entry.insert(0, self.t("search_placeholder"))
            self.search_entry.config(fg=self.colors["text_secondary"])
        self.apply_search()

    def _global_search_items(self, keyword):
        keyword = (keyword or "").lower().strip()
        items = []
        if not keyword:
            return items
        for ds, note in self.notes.items():
            text = f"{ds} {note.get('time','')} {note.get('text','')}"
            if keyword in text.lower():
                items.append(("calendar", self.t("tab_calendar"), f"{ds} · {note.get('text','')}"))
        for _idx, todo in self.visible_todo_items() if hasattr(self, "visible_todo_items") else []:
            text = todo.get("text", "")
            if keyword in text.lower():
                items.append(("home", self.t("tasks_title"), text))
        docs_source = self.visible_documents() if hasattr(self, "visible_documents") else self.documents
        for doc_id, doc in docs_source.items():
            title = self.display_doc_title(doc.get("title", ""))
            content = str(doc.get("content", ""))
            if keyword in f"{title} {content}".lower():
                items.append(("files", self.t("tab_files"), title))
        if self.is_enterprise_mode():
            for emp in self.enterprise.get("employees", []):
                text = " ".join(str(emp.get(k, "")) for k in ("name", "position", "department", "email"))
                if keyword in text.lower():
                    items.append(("enterprise", self.t("tab_enterprise"), text))
            for notice in self.enterprise.get("notifications", []):
                text = f"{notice.get('title','')} {notice.get('content','')}"
                if keyword in text.lower():
                    items.append(("home", self.t("notification_center"), text))
        return items[:50]

    def show_search_results(self):
        if getattr(self, "search_entry", None) is None:
            return
        value = self.search_entry.get().strip()
        if value == self.t("search_placeholder") or value in self.all_values("search_placeholder"):
            value = ""
        if not value:
            messagebox.showinfo(self.t("search_help_title"), self.t("search_help_text"))
            return
        items = self._global_search_items(value)
        win = tk.Toplevel(self.root)
        win.title(self.t("search_results_title"))
        win.configure(bg=self.colors["bg"])
        try:
            win.transient(self.root)
            w, h = 760, 560
            center_window(win, self.root, w, h, min_width=560, min_height=420)
        except Exception:
            win.geometry("760x560")
        card = tk.Frame(win, bg=self.colors["card"], highlightthickness=1, highlightbackground=self.colors["border"])
        card.pack(fill=tk.BOTH, expand=True, padx=18, pady=18)
        tk.Label(card, text=self.t("search_results_title"), bg=self.colors["card"], fg=self.colors["text"], font=("Microsoft YaHei UI", 18, "bold")).pack(anchor="w", padx=18, pady=(16, 4))
        tk.Label(card, text=self.t("search_results_hint").format(keyword=value, count=len(items)), bg=self.colors["card"], fg=self.colors["text_secondary"], font=("Microsoft YaHei UI", 10)).pack(anchor="w", padx=18, pady=(0, 12))
        canvas = tk.Canvas(card, bg=self.colors["card"], highlightthickness=0)
        inner = tk.Frame(canvas, bg=self.colors["card"])
        canvas.pack(fill=tk.BOTH, expand=True, padx=18, pady=(0, 18))
        canvas.create_window((0, 0), window=inner, anchor="nw")
        inner.bind("<Configure>", lambda e: canvas.configure(scrollregion=canvas.bbox("all")))
        if not items:
            tk.Label(inner, text=self.t("search_empty"), bg=self.colors["card"], fg=self.colors["text_secondary"], font=("Microsoft YaHei UI", 11), pady=30).pack(fill=tk.X)
        for page, module, detail in items:
            row = tk.Frame(inner, bg=self.colors["card_soft"], highlightthickness=1, highlightbackground=self.colors["border"], cursor="hand2")
            row.pack(fill=tk.X, pady=5)
            tk.Label(row, text=module, bg=self.colors["card_soft"], fg=self.colors["primary"], font=("Microsoft YaHei UI", 10, "bold")).pack(anchor="w", padx=12, pady=(9, 2))
            tk.Label(row, text=detail, bg=self.colors["card_soft"], fg=self.colors["text"], font=("Microsoft YaHei UI", 10), wraplength=650, justify=tk.LEFT).pack(anchor="w", padx=12, pady=(0, 9))
            row.bind("<Button-1>", lambda e, p=page, w=win: [w.destroy(), self.switch_page(p)])
            for ch in row.winfo_children():
                ch.bind("<Button-1>", lambda e, p=page, w=win: [w.destroy(), self.switch_page(p)])

    def matches_search(self, *parts):
        if not self.search_keyword:
            return True
        haystack = " ".join(str(p or "") for p in parts).lower()
        return self.search_keyword in haystack

    def empty_text(self, fallback_key):
        return self.t("search_empty") if self.search_keyword else self.t(fallback_key)

    def format_date(self, d):
        if self.lang_code == "zh_CN":
            return f"{d.year}年{d.month}月{d.day}日"
        if self.lang_code == "id_ID":
            months = ["Jan", "Feb", "Mar", "Apr", "Mei", "Jun", "Jul", "Agu", "Sep", "Okt", "Nov", "Des"]
            return f"{d.day} {months[d.month - 1]} {d.year}"
        months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
        return f"{months[d.month - 1]} {d.day}, {d.year}"

    def all_values(self, key):
        return all_lang_values(self.all_langs, key)

    def holiday_name(self, info):
        name = info.get("name", "")
        if isinstance(name, dict):
            key = "zh" if self.lang_code == "zh_CN" else ("id" if self.lang_code == "id_ID" else "en")
            return name.get(key) or name.get("zh") or name.get("en") or name.get("id") or ""
        return str(name or "")

    def display_doc_title(self, title):
        raw = str(title or "").strip()
        for key in ("notepad_untitled", "shared_untitled_doc", "shared_untitled_sheet"):
            for value in self.all_values(key):
                if raw == value:
                    return self.t(key)
                if raw.startswith(value) and raw[len(value):].strip().isdigit():
                    return self.t(key) + raw[len(value):]
        return raw or self.t("shared_untitled_doc")

    def display_doc_content(self, content):
        raw = str(content or "").strip()
        if raw in self.all_values("notepad_placeholder"):
            return self.t("notepad_placeholder")
        return str(content or "")

    def set_login_theme(self, theme_id):
        if theme_id not in THEME_PRESETS:
            return
        self.user_settings["theme"] = theme_id
        save_settings(self.user_settings)
        self.colors = get_theme_colors(theme_id)
        if getattr(self, "login_screen_active", False):
            self._build_login_screen()
        else:
            self.set_theme(theme_id)

    def set_theme(self, theme_id):
        if theme_id not in THEME_PRESETS:
            return
        self.user_settings["theme"] = theme_id
        save_settings(self.user_settings)
        self.reload_scope_data()
        current = self.current_page
        self.colors = get_theme_colors(theme_id)
        try:
            for seq in ("<MouseWheel>", "<Shift-MouseWheel>", "<Button-4>", "<Button-5>", "<Button-6>", "<Button-7>"):
                self.root.unbind_all(seq)
        except Exception:
            pass
        for child in list(self.root.winfo_children()):
            child.destroy()
        self.root.configure(bg=self.colors["bg"])
        self._setup_style()
        self._build_shell()
        self.switch_page(current if current in getattr(self, "page_factories", {}) else "home")
        self.set_status("theme_saved")


    def set_font_scale(self, scale_value):
        try:
            scale = float(scale_value)
        except Exception:
            return
        scale = max(0.95, min(1.25, scale))
        self.user_settings["font_scale"] = round(scale, 2)
        save_settings(self.user_settings)
        self.reload_scope_data()
        current = self.current_page
        try:
            for seq in ("<MouseWheel>", "<Shift-MouseWheel>", "<Button-4>", "<Button-5>", "<Button-6>", "<Button-7>"):
                self.root.unbind_all(seq)
        except Exception:
            pass
        for child in list(self.root.winfo_children()):
            child.destroy()
        self._configure_display()
        self.root.configure(bg=self.colors["bg"])
        self._setup_style()
        self._build_shell()
        self.switch_page(current if current in getattr(self, "page_factories", {}) else "home")
        self.set_status("font_scale_saved")


    def show_language_popup(self):
        # Use native Tk menu instead of a custom Toplevel.
        # This is much more reliable on Windows and avoids the language picker being locked.
        menu = tk.Menu(self.root, tearoff=0, bg=self.colors["card"], fg=self.colors["text"], activebackground=self.colors["primary_light"], activeforeground=self.colors["primary"], bd=0)
        items = [("zh_CN", LANG_DISP["zh_CN"]), ("en_US", LANG_DISP["en_US"]), ("id_ID", LANG_DISP["id_ID"])]
        def _choose_language(code):
            try:
                menu.unpost()
            except Exception:
                pass
            try:
                self.root.after(10, lambda c=code: self.set_language(c))
            except Exception:
                self.set_language(code)

        for code, label in items:
            prefix = "✓  " if code == self.lang_code else "   "
            menu.add_command(label=prefix + label, command=lambda c=code: _choose_language(c))
        try:
            x = self.lang_btn.winfo_rootx()
            y = self.lang_btn.winfo_rooty() + self.lang_btn.winfo_height() + 4
            menu.tk_popup(x, y)
        finally:
            try:
                menu.grab_release()
            except Exception:
                pass

    def _refresh_current_page(self):
        current = getattr(self, "current_page", "home")
        target = current if current in getattr(self, "page_factories", {}) else "home"
        if target in getattr(self, "pages", {}):
            page_obj = self.pages.get(target)
            try:
                if hasattr(page_obj, "update_language"):
                    page_obj.update_language()
                elif hasattr(page_obj, "refresh"):
                    page_obj.refresh()
            except Exception:
                pass
        try:
            self._finish_switch_page(target)
        except Exception:
            pass

    def set_language(self, code):
        if code not in LANG_DISP:
            return
        self.lang_code = code
        self.lang = load_lang(self.lang_code)
        self.all_langs = load_all_langs()
        self.user_settings["language"] = self.lang_code
        save_settings(self.user_settings)
        self.app_version = APP_VERSION
        self.root.title(self.window_title())

        # Login screen has no main shell widgets yet. Rebuild it immediately so
        # Full language labels visibly switch all login texts.
        if getattr(self, "login_screen_active", False) or not hasattr(self, "pages_frame") or not self.user_settings.get("logged_in"):
            self._build_login_screen()
            return

        if hasattr(self, "lang_var"):
            self.lang_var.set(LANG_SHORT.get(self.lang_code, self.lang_code[:2].upper()))
        self.lang_btn.config(text=f"{LANG_SHORT.get(self.lang_code, self.lang_code[:2].upper())} ▾")
        try:
            if hasattr(self, "quick_launcher_btn"):
                self.quick_launcher_btn.config(text=self.t("quick_launcher_button"))
            if getattr(self, "exit_btn", None):
                self.exit_btn.config(text=self.t("exit_app_menu"))
            self.mode_personal_btn.config(text=self.t("mode_personal"))
            self.mode_enterprise_btn.config(text=self.t("mode_enterprise"))
            self._refresh_mode_switch()
        except Exception:
            pass
        self.page_title.config(text=clean_label(self.t("tab_home")) if self.current_page == "home" else self.page_title.cget("text"))

        nav_meta = {
            "home": "tab_home",
            "calendar": "tab_calendar",
            "files": "tab_files",
            "chat": "tab_chat",
            "cloud": "tab_cloud",
            "enterprise": "tab_enterprise",
            "ai": "tab_ai",
            "settings": "tab_settings",
        }
        if hasattr(self, "sidebar_brand_title"):
            self.sidebar_brand_title.config(text=self.t("app_title"))
        if hasattr(self, "sidebar_brand_subtitle"):
            self.sidebar_brand_subtitle.config(text=self.sidebar_subtitle_text())
        if hasattr(self, "sidebar_user_label"):
            self.sidebar_user_label.config(text=self.current_user_display_name() if self.user_settings.get("logged_in") else self.t("not_logged_in"))
        if hasattr(self, "sidebar_copyright_label"):
            self.sidebar_copyright_label.config(text="Copyright © NexaOffice 2026")
        for page, btn in self.nav_buttons.items():
            key = nav_meta.get(page, page)
            btn.config(text=f"    {clean_label(self.t(key))}")
            try:
                ToolTip(btn, lambda k=key: clean_label(self.t(k)))
            except Exception:
                pass
        try:
            self._apply_sidebar_compaction(self.layout_profile.get("sidebar", "normal"))
            self._apply_header_compaction(self.layout_profile.get("header", "normal"), self.layout_profile.get("width", 1440), self.layout_profile.get("height", 900))
        except Exception:
            pass

        if not self.search_keyword and getattr(self, "search_entry", None) is not None:
            self.search_entry.delete(0, tk.END)
            self.search_entry.insert(0, self.t("search_placeholder"))
            self.search_entry.config(fg=self.colors["text_secondary"])

        for page_obj in list(self.pages.values()):
            if hasattr(page_obj, "update_language"):
                page_obj.update_language()
            elif hasattr(page_obj, "refresh"):
                page_obj.refresh()
        def _safe_language_refresh():
            try:
                if getattr(self.root, "winfo_exists", lambda: False)() and self.user_settings.get("logged_in"):
                    self._refresh_current_page()
            except Exception:
                try:
                    self._finish_switch_page(self.current_page)
                except Exception:
                    pass

        try:
            _safe_language_refresh()
            self.root.after(80, _safe_language_refresh)
            self.root.after(220, _safe_language_refresh)
        except Exception:
            try:
                self._finish_switch_page(self.current_page)
            except Exception:
                pass
        self.set_status("language_changed")

    def change_language(self, event=None):
        self.set_language(LANG_REV.get(self.lang_var.get(), "zh_CN"))

    def show_more_popup(self):
        if self.more_popup and self.more_popup.winfo_exists():
            self.more_popup.destroy()
            return
        popup = tk.Toplevel(self.root)
        self.more_popup = popup
        popup.overrideredirect(True)
        popup.configure(bg=self.colors["border"])
        x = self.more_btn.winfo_rootx() - 250
        y = self.more_btn.winfo_rooty() + self.more_btn.winfo_height() + 8
        popup.geometry(f"390x830+{max(x, 0)}+{y}")
        card = tk.Frame(popup, bg=self.colors["card"], highlightthickness=1, highlightbackground=self.colors["border"])
        card.pack(fill=tk.BOTH, expand=True, padx=1, pady=1)
        top = tk.Frame(card, bg=self.colors["card"])
        top.pack(fill=tk.X, padx=16, pady=(14, 8))
        tk.Label(top, text=self.t("more_title"), font=("Microsoft YaHei UI", 13, "bold"), fg=self.colors["text"], bg=self.colors["card"]).pack(anchor="w")
        tk.Label(top, text=self.t("more_subtitle"), font=("Microsoft YaHei UI", 9), fg=self.colors["text_secondary"], bg=self.colors["card"]).pack(anchor="w", pady=(3, 0))

        def add_item(title_key, desc_key, command, danger=False):
            item = tk.Button(
                card,
                text=f"{self.t(title_key)}\n{self.t(desc_key)}",
                justify=tk.LEFT,
                anchor="w",
                command=lambda: [popup.destroy(), command()],
                bg=self.colors["danger_light"] if danger else self.colors["card_soft"],
                fg=self.colors["danger"] if danger else self.colors["text"],
                activebackground=self.colors["primary_light"],
                activeforeground=self.colors["primary"],
                relief=tk.FLAT,
                bd=0,
                padx=14,
                pady=9,
                font=("Microsoft YaHei UI", 10, "bold"),
                cursor="hand2",
            )
            item.pack(fill=tk.X, padx=14, pady=5)

        add_item("open_settings", "open_settings_desc", lambda: self.switch_page("settings"))
        add_item("tray_test_menu", "tray_test_desc", self.test_tray_icon)
        add_item("system_health_menu", "system_health_desc", self.show_system_health_center)
        add_item("desktop_doctor_menu", "desktop_doctor_desc", self.run_desktop_doctor)
        add_item("desktop_preflight_menu", "desktop_preflight_desc", self.run_desktop_preflight)
        add_item("close_behavior_menu", "close_behavior_desc", self.show_close_behavior_dialog)
        add_item("tray_status_menu", "tray_status_desc", self.show_tray_status_dialog)
        add_item("icon_generator_menu", "icon_generator_desc", self.regenerate_icon_assets)
        add_item("icon_cache_tool_menu", "icon_cache_tool_desc", self.open_icon_cache_tool)
        add_item("open_latest_build_menu", "open_latest_build_desc", self.open_latest_build_folder)
        add_item("build_cleanup_tool_menu", "build_cleanup_tool_desc", self.open_build_cleanup_tool)
        add_item("force_exit_tool_menu", "force_exit_tool_desc", self.open_force_exit_tool, danger=True)
        add_item("diagnostics_export_menu", "diagnostics_export_desc", self.export_diagnostics)
        add_item("exit_app_menu", "exit_app_desc", self.confirm_exit_app, danger=True)
        tk.Button(card, text=self.t("close"), command=popup.destroy, bg=self.colors["card"], fg=self.colors["text_secondary"], activebackground=self.colors["primary_light"], relief=tk.FLAT, bd=0, cursor="hand2").pack(side=tk.BOTTOM, fill=tk.X, padx=14, pady=(4, 12))
        popup.focus_force()

    def quick_backup(self):
        """Create a quick backup in the user data folder without asking for a path."""
        try:
            BACKUP_DIR.mkdir(parents=True, exist_ok=True)
            path = BACKUP_DIR / f"nexaoffice_quick_backup_{datetime.now().strftime('%Y%m%d_%H%M%S')}.zip"
            with zipfile.ZipFile(path, "w", zipfile.ZIP_DEFLATED) as zf:
                for file_name in [
                    "notes.json", "todos.json", "documents.json", "settings.json",
                    "enterprise.json", "chat.json", "action_logs.json", "holidays.json",
                ]:
                    fp = DATA_DIR / file_name
                    if fp.exists():
                        zf.write(fp, f"data/{file_name}")
                uploads = DATA_DIR / "uploads"
                if uploads.exists():
                    for fp in uploads.rglob("*"):
                        if fp.is_file():
                            zf.write(fp, f"data/uploads/{fp.relative_to(uploads)}")
                meta = {
                    "app": self.t("app_title"),
                    "exported_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
                    "version": self.app_version,
                    "type": "quick_backup",
                }
                zf.writestr("backup_meta.json", json.dumps(meta, ensure_ascii=False, indent=2))
            # Keep only recent quick backup packages.
            old = sorted(BACKUP_DIR.glob("nexaoffice_quick_backup_*.zip"), key=lambda fp: fp.stat().st_mtime, reverse=True)
            for fp in old[8:]:
                try:
                    fp.unlink()
                except Exception:
                    pass
            self.log_action("quick_backup_created", str(path), "settings")
            self.set_status("quick_backup_created")
            messagebox.showinfo(self.t("success_title"), self.t("quick_backup_done").format(path=str(path)))
        except Exception as e:
            messagebox.showerror(self.t("error_title"), f"{self.t('quick_backup_failed')}\n{e}")

    def export_backup(self):
        default_name = f"nexaoffice_backup_{datetime.now().strftime('%Y%m%d_%H%M%S')}.zip"
        path = filedialog.asksaveasfilename(
            title=self.t("backup_export_title"),
            defaultextension=".zip",
            initialfile=default_name,
            filetypes=[("Zip Backup", "*.zip")],
        )
        if not path:
            return
        try:
            with zipfile.ZipFile(path, "w", zipfile.ZIP_DEFLATED) as zf:
                for file_name in [
                    "notes.json", "todos.json", "documents.json", "settings.json",
                    "enterprise.json", "chat.json", "action_logs.json", "holidays.json",
                ]:
                    fp = DATA_DIR / file_name
                    if fp.exists():
                        zf.write(fp, f"data/{file_name}")
                uploads = DATA_DIR / "uploads"
                if uploads.exists():
                    for fp in uploads.rglob("*"):
                        if fp.is_file():
                            zf.write(fp, f"data/uploads/{fp.relative_to(uploads)}")
                meta = {
                    "app": self.t("app_title"),
                    "exported_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
                    "version": self.app_version,
                }
                zf.writestr("backup_meta.json", json.dumps(meta, ensure_ascii=False, indent=2))
            self.log_action("backup_exported", str(path), "settings")
            self.set_status("backup_exported")
            messagebox.showinfo(self.t("success_title"), self.t("backup_export_done"))
        except Exception as e:
            messagebox.showerror(self.t("error_title"), f"{self.t('backup_export_failed')}\n{e}")

    def export_diagnostics(self):
        """Export lightweight local diagnostics for performance / device matching."""
        default_name = f"nexaoffice_diagnostics_{datetime.now().strftime('%Y%m%d_%H%M%S')}.zip"
        path = filedialog.asksaveasfilename(
            title=self.t("diagnostics_export_title"),
            defaultextension=".zip",
            initialfile=default_name,
            filetypes=[("Zip Diagnostics", "*.zip")],
        )
        if not path:
            return
        try:
            profile = self._detect_device_profile()
            summary = self.get_sync_queue_summary()
            payload = {
                "app_version": self.app_version,
                "exported_at": datetime.now().isoformat(timespec="seconds"),
                "language": self.lang_code,
                "app_mode": self.get_app_mode(),
                "effective_performance_tier": self._effective_performance_tier(),
                "device_profile": profile,
                "window": {
                    "width": int(self.root.winfo_width()),
                    "height": int(self.root.winfo_height()),
                    "state": self.root.state(),
                    "layout_profile": getattr(self, "layout_profile", {}),
                    "runtime_fit_issues": getattr(self, "_runtime_layout_fit_issues", []),
                },
                "settings": {
                    "performance_tier": self.user_settings.get("performance_tier", "auto"),
                    "lightweight_page_switch": self.user_settings.get("lightweight_page_switch", True),
                    "smooth_restore": self.user_settings.get("smooth_restore", True),
                    "close_to_tray": self.user_settings.get("close_to_tray", True),
                    "source_mode_close_direct": self.user_settings.get("source_mode_close_direct", True),
                    "source_mode_force_os_exit": self.user_settings.get("source_mode_force_os_exit", True),
                    "effective_close_to_tray": self.effective_close_to_tray(),
                    "source_mode": self.is_source_mode(),
                    "theme": self.user_settings.get("theme", "blue"),
                    "font_scale": self.user_settings.get("font_scale", 1.0),
                    "effective_font_scale": getattr(self, "font_scale", 1.0),
                    "requested_font_scale": getattr(self, "requested_font_scale", self.user_settings.get("font_scale", 1.0)),
                    "responsive_layout": self.user_settings.get("responsive_layout", {}),
                },
                "tray_status": self.tray_status_summary(),
                "tray_events": self.tray_events[-50:],
                "exit_status": {
                    "exit_requested": bool(getattr(self, "_exit_requested", False)),
                    "exit_strategy": getattr(self, "_exit_strategy", ""),
                    "exit_started_at": getattr(self, "_exit_started_at", 0.0),
                    "source_mode": self.is_source_mode(),
                    "source_mode_close_direct": bool(self.user_settings.get("source_mode_close_direct", True)),
                    "source_mode_force_os_exit": bool(self.user_settings.get("source_mode_force_os_exit", True)),
                    "last_exit_save_status": getattr(self, "_last_exit_save_status", {}),
                    "close_behavior": self.close_behavior_summary(),
                },
                "icon_status": self.icon_status_summary(),
                "desktop_doctor_latest_exists": (DATA_DIR / "desktop_doctor_latest.json").exists(),
                "desktop_preflight_latest_exists": (DATA_DIR / "desktop_preflight_latest.json").exists(),
                "force_exit_tool_exists": (BASE_DIR / "tools" / "force_exit_nexaoffice.bat").exists(),
                "build_tools": {
                    "build_result_check_exists": (BASE_DIR / "tools" / "build_result_check.py").exists(),
                    "cleanup_build_artifacts_exists": (BASE_DIR / "tools" / "cleanup_build_artifacts.bat").exists(),
                    "open_latest_build_exists": (BASE_DIR / "tools" / "open_latest_build.bat").exists(),
                    "build_result_latest_exists": (DATA_DIR / "build_result_latest.json").exists(),
                    "build_manifest_latest_exists": (DATA_DIR / "build_manifest_latest.json").exists(),
                    "build_backups_exists": (BASE_DIR / "build_backups").exists(),
                },
                "counts": {
                    "notes": len(self.notes if isinstance(self.notes, dict) else {}),
                    "todos": len(self.todos.get("items", [])) if isinstance(self.todos, dict) else 0,
                    "files": len(self.documents if isinstance(self.documents, dict) else {}),
                    "chat_enterprise_threads": len(self.chat.get("enterprise_threads", {})) if isinstance(self.chat, dict) else 0,
                    "chat_personal_threads": len(self.chat.get("personal_threads", {})) if isinstance(self.chat, dict) else 0,
                },
                "sync_queue": summary,
                "performance_metrics": {
                    "summary": self.performance_summary(),
                    "recent_page_switches": self.performance_metrics.get("page_switches", [])[-20:],
                    "slow_events": self.performance_metrics.get("slow_events", [])[-20:],
                },
                "migration": {
                    "files_primary_route": True,
                    "notepad_alias_active": False,
                    "notepad_page_removed": not (BASE_DIR / "pages" / "notepad_page.py").exists(),
                    "active_file_label_keys": ["tab_files", "module_files"],
                },
            }
            with zipfile.ZipFile(path, "w", zipfile.ZIP_DEFLATED) as zf:
                zf.writestr("diagnostics.json", json.dumps(payload, ensure_ascii=False, indent=2))
                for file_name in ["settings.json", "action_logs.json", "sync_queue.json", "scoped_workspace.json"]:
                    fp = DATA_DIR / file_name
                    if fp.exists():
                        zf.write(fp, f"data/{file_name}")
            self.log_action("diagnostics_exported", str(path), "settings")
            self.set_status("diagnostics_exported")
            messagebox.showinfo(self.t("success_title"), self.t("diagnostics_export_done").format(path=str(path)))
        except Exception as e:
            messagebox.showerror(self.t("error_title"), f"{self.t('diagnostics_export_failed')}\n{e}")

    def import_backup(self):
        path = filedialog.askopenfilename(
            title=self.t("backup_import_title"),
            filetypes=[("Zip Backup", "*.zip"), ("All files", "*.*")],
        )
        if not path:
            return
        if not messagebox.askyesno(self.t("confirm_title"), self.t("backup_import_confirm")):
            return
        try:
            with zipfile.ZipFile(path, "r") as zf:
                for info in zf.infolist():
                    name = info.filename.replace("\\", "/")
                    if not name.startswith("data/") or name.endswith("/"):
                        continue
                    rel = Path(name[5:])
                    if rel.is_absolute() or ".." in rel.parts:
                        continue
                    target = DATA_DIR / rel
                    target.parent.mkdir(parents=True, exist_ok=True)
                    with zf.open(info) as source, open(target, "wb") as dest:
                        shutil.copyfileobj(source, dest)
            # Reload local data after restore.
            self.notes, self.todos, self.documents, self.holidays = load_data()
            self.user_settings = load_settings()
            self.user_settings.setdefault("theme", "blue")
            self.enterprise = load_enterprise()
            self.chat = load_chat()
            self.reports = load_reports()
            self.attendance = load_attendance()
            self.action_logs = load_action_logs()
            self.clear_search()
            for page in self.pages.values():
                try:
                    page.refresh()
                except Exception:
                    pass
            self.refresh_user_state()
            self.log_action("backup_imported", str(path), "settings")
            self.set_status("backup_imported")
            messagebox.showinfo(self.t("success_title"), self.t("backup_import_done"))
        except Exception as e:
            messagebox.showerror(self.t("error_title"), f"{self.t('backup_import_failed')}\n{e}")

    def reset_notes(self):
        if not messagebox.askyesno(self.t("confirm_title"), self.t("reset_notes_confirm_1")):
            return
        if not messagebox.askyesno(self.t("confirm_title"), self.t("reset_notes_confirm_2")):
            return
        self.notes = {}
        save_notes(self.notes)
        if "calendar" in self.pages:
            if hasattr(self.pages["calendar"], "cell_cache"):
                self.pages["calendar"].cell_cache.clear()
            self.pages["calendar"].refresh()
        self.refresh_home()
        self.log_action("reset_notes_action", "", "settings")
        self.set_status("status_reset")
        messagebox.showinfo(self.t("success_title"), self.t("reset_notes_done"))

    def restore_demo_data(self):
        if not messagebox.askyesno(self.t("confirm_title"), self.t("restore_confirm_1")):
            return
        if not messagebox.askyesno(self.t("confirm_title"), self.t("restore_confirm_2")):
            return
        self.notes, self.todos, self.documents = self._sample_payload()
        save_notes(self.notes)
        save_todos(self.todos)
        save_documents(self.documents)
        self.clear_search()
        for page in list(self.pages.values()):
            if hasattr(page, "refresh"):
                page.refresh()
        self.log_action("restore_demo_action", "", "settings")
        self.set_status("status_saved")
        messagebox.showinfo(self.t("success_title"), self.t("restore_done"))

    def _sample_payload(self):
        today = date.today()
        d1 = today.strftime("%Y-%m-%d")
        d2 = (today + timedelta(days=1)).strftime("%Y-%m-%d")
        d3 = (today + timedelta(days=7)).strftime("%Y-%m-%d")
        doc_id = str(int(time.time() * 1000))
        if self.lang_code == "id_ID":
            notes = {d1: {"text": "Tinjau Pekerjaan Hari Ini", "time": "09:30", "image": None}, d2: {"text": "Rapat Sinkronisasi Tim", "time": "14:00", "image": None}, d3: {"text": "Kirim Ringkasan Mingguan", "time": "17:30", "image": None}}
            todos = {"items": [{"text": "Siapkan Materi Rapat", "done": False}, {"text": "Cek Pesan Penting", "done": False}, {"text": "Perbarui Arsip Dokumen", "done": True}]}
            docs = {doc_id: {"title": "Memo Proyek", "content": "Target, progres, risiko, dan langkah berikutnya.", "updated": datetime.now().strftime("%Y-%m-%d %H:%M")}}
        elif self.lang_code == "en_US":
            notes = {d1: {"text": "Review today’s work", "time": "09:30", "image": None}, d2: {"text": "Team sync meeting", "time": "14:00", "image": None}, d3: {"text": "Submit weekly summary", "time": "17:30", "image": None}}
            todos = {"items": [{"text": "Prepare meeting materials", "done": False}, {"text": "Check important messages", "done": False}, {"text": "Update document archive", "done": True}]}
            docs = {doc_id: {"title": "Project Memo", "content": "Goals, progress, risks, and next steps.", "updated": datetime.now().strftime("%Y-%m-%d %H:%M")}}
        else:
            notes = {d1: {"text": "复盘今日工作", "time": "09:30", "image": None}, d2: {"text": "团队同步会议", "time": "14:00", "image": None}, d3: {"text": "提交本周工作总结", "time": "17:30", "image": None}}
            todos = {"items": [{"text": "准备会议资料", "done": False}, {"text": "检查重要消息", "done": False}, {"text": "更新文档归档", "done": True}]}
            docs = {doc_id: {"title": "项目备忘录", "content": "目标、进度、风险和下一步计划。", "updated": datetime.now().strftime("%Y-%m-%d %H:%M")}}
        return notes, todos, docs

    def _check_reminders(self):
        while True:
            now = datetime.now()
            date_key = now.strftime("%Y-%m-%d")
            time_key = now.strftime("%H:%M")
            note = self.notes.get(date_key)
            reminder_key = f"{date_key} {time_key}"
            if note and note.get("time") == time_key and reminder_key not in self.reminder_sent:
                self.reminder_sent.add(reminder_key)
                try:
                    desktop_notify(title=f"⏰ {self.t('reminder_title')}", message=note.get("text", ""), timeout=10)
                except Exception:
                    pass

            holiday_key = f"holiday-reminder-{date_key}"
            if holiday_key not in self.reminder_sent:
                reminders = self.get_holiday_reminders()
                days_before = int(reminders.get("days_before", 1))
                upcoming = self.get_upcoming_holiday_notifications(days=days_before)
                if upcoming:
                    self.reminder_sent.add(holiday_key)
                    try:
                        first = upcoming[0]
                        desktop_notify(title=first["title"], message=first["content"], timeout=8)
                    except Exception:
                        pass

            if len(self.reminder_sent) > 300:
                self.reminder_sent = set(list(self.reminder_sent)[-100:])
            time.sleep(30)
