import base64
import calendar as pycal
import math
import tkinter as tk
from datetime import date, datetime, timedelta
from io import BytesIO
from tkinter import filedialog, messagebox, simpledialog

from PIL import Image, ImageTk

from core.i18n import holiday_text
from core.lunar import lunar_cell_text, lunar_full_text
from core.storage import save_notes, save_settings
from core.ui import center_window, create_button, create_card, show_empty


def fmt_date_key(y, m, d):
    return f"{y:04d}-{m:02d}-{d:02d}"


class CalendarPage:
    def __init__(self, app, parent):
        self.app = app
        self.colors = app.colors
        self.frame = tk.Frame(parent, bg=self.colors["bg"])
        self.show_china = tk.BooleanVar(value=True)
        self.show_indonesia = tk.BooleanVar(value=True)
        self.show_solar = tk.BooleanVar(value=True)
        self.show_lunar = tk.BooleanVar(value=True)
        self.show_notes = tk.BooleanVar(value=True)
        self.selected_date = date.today().strftime("%Y-%m-%d")
        self.current_offset = 0
        self.year_offset = 0
        self.is_year_view = False
        self.view_mode = "month"
        self.year_view_style = str(self.app.user_settings.get("calendar_year_view_style", "overview") or "overview")
        if self.year_view_style not in ("classic", "overview"):
            # v2026.06.19.02: default to overview, keep classic as holiday view

            self.year_view_style = "overview"
        try:
            self.month_count = 2 if int(self.app.user_settings.get("calendar_month_count", 1)) == 2 else 1
        except Exception:
            self.month_count = 1
        self.preview_images = []
        self.cell_cache = {}
        self._lunar_cache = {}
        self._year_holiday_cache = {}
        self._nav_locked = False
        self._month_count_switching = False
        self._month_transition_cover = None
        self._month_transition_job = None
        self._active_scroll_canvas = None
        self._scroll_canvases = []
        self._year_scrollbar_visible = False
        self._side_panel_collapsed = False
        self._calendar_resize_job = None
        self._build()

    def _build(self):
        toolbar = create_card(self.frame, self.colors, fill=tk.X, pady=(0, 12))
        inner = tk.Frame(toolbar, bg=self.colors["card"])
        inner.pack(fill=tk.X, padx=16, pady=12)
        self.filter_label = tk.Label(inner, text=f"{self.app.t('filter_label')}：", font=("Microsoft YaHei UI", 10), fg=self.colors["text_secondary"], bg=self.colors["card"])
        self.filter_label.pack(side=tk.LEFT, padx=(0, 8))
        self.china_btn = self._filter_button(inner, "CN", self.colors["danger_light"], self.colors["danger"], lambda: self._toggle("china"))
        self.indo_btn = self._filter_button(inner, "ID", self.colors["warning_light"], self.colors["warning"], lambda: self._toggle("indonesia"))
        self.solar_btn = self._filter_button(inner, "☘", self.colors["solar_light"], self.colors["solar"], lambda: self._toggle("solar"))
        # Keep the toolbar order consistent with the legend:
        # CN / ID / Season / Notes / Lunar.
        self.notes_btn = self._filter_button(inner, self.app.t("holiday_type_notes"), self.colors["success_light"], self.colors["success"], lambda: self._toggle("notes"))
        self.lunar_btn = self._filter_button(inner, self.app.t("lunar_mode"), self.colors["primary_light"], self.colors["primary"], lambda: self._toggle("lunar"))

        self.view_segment = tk.Frame(inner, bg=self.colors["card"])
        self.view_segment.pack(side=tk.LEFT, padx=18)
        self.year_mode_btn = self._view_button(self.view_segment, "view_year", "year")
        self.month_mode_btn = self._view_button(self.view_segment, "view_month", "month")
        self.week_mode_btn = self._view_button(self.view_segment, "view_week", "week")
        self.day_mode_btn = self._view_button(self.view_segment, "view_day", "day")

        self.month_count_segment = tk.Frame(inner, bg=self.colors["card"])
        self.month_count_segment.pack(side=tk.LEFT, padx=(0, 10))
        self.one_month_btn = self._month_count_button(self.month_count_segment, "month_count_one", 1)
        self.two_month_btn = self._month_count_button(self.month_count_segment, "month_count_two", 2)

        right_tools = tk.Frame(inner, bg=self.colors["card"])
        right_tools.pack(side=tk.RIGHT)
        self.goto_btn = create_button(right_tools, self.app.t("goto_date"), self._goto_dialog, self.colors)
        self.goto_btn.pack(side=tk.LEFT)

        self.content = tk.Frame(self.frame, bg=self.colors["bg"])
        self.content.pack(fill=tk.BOTH, expand=True)
        self.content.grid_columnconfigure(0, weight=1)
        side_min = 300 if getattr(self.app, "compact_mode", False) else 340
        self.side_panel_width = side_min
        self.content.grid_columnconfigure(1, weight=0, minsize=side_min)
        self.content.grid_rowconfigure(0, weight=1)

        self.left_panel = tk.Frame(self.content, bg=self.colors["bg"])
        self.left_panel.grid(row=0, column=0, sticky="nsew", padx=(0, 14))

        # Top utility bar: move the legend to the top and place month navigation on the right.
        self.nav = tk.Frame(self.left_panel, bg=self.colors["bg"])
        self.nav.pack(fill=tk.X, pady=(0, 10))
        self.legend = tk.Frame(self.nav, bg=self.colors["bg"])
        self.legend.pack(side=tk.LEFT)
        self._build_legend()
        self.month_nav = tk.Frame(self.nav, bg=self.colors["bg"])
        self.month_nav.pack(side=tk.RIGHT)
        self.prev_btn = create_button(self.month_nav, "◀", self.prev_month, self.colors)
        self.prev_btn.pack(side=tk.LEFT)
        self.today_btn = create_button(self.month_nav, self.app.t("today_btn"), self.go_today, self.colors, filled=True)
        self.today_btn.pack(side=tk.LEFT, padx=8)
        self.next_btn = create_button(self.month_nav, "▶", self.next_month, self.colors)
        self.next_btn.pack(side=tk.LEFT)

        right = create_card(self.content, self.colors)
        right.grid(row=0, column=1, sticky="nsew")
        right.configure(width=side_min)
        right.grid_propagate(False)
        right.pack_propagate(False)
        self.side_panel = right

        self.normal_view = tk.Frame(self.left_panel, bg=self.colors["bg"])
        self.normal_view.pack(fill=tk.BOTH, expand=True)
        self.normal_view.grid_columnconfigure(0, weight=1, uniform="months")
        self.normal_view.grid_columnconfigure(1, weight=1, uniform="months")
        self.normal_view.grid_rowconfigure(0, weight=1, uniform="months_rows")
        self.year_view_frame = tk.Frame(self.left_panel, bg=self.colors["bg"])
        self.agenda_view_frame = tk.Frame(self.left_panel, bg=self.colors["bg"])

        self.cal_frames = []
        for i in range(2):
            card = create_card(self.normal_view, self.colors)
            card.grid(row=0, column=i, sticky="nsew", padx=(0, 7) if i == 0 else (7, 0))
            hdr = tk.Frame(card, bg=self.colors["primary_light"])
            hdr.pack(fill=tk.X)
            month_label = tk.Label(hdr, text="", font=("Microsoft YaHei UI", 14, "bold"), fg=self.colors["primary"], bg=self.colors["primary_light"])
            month_label.pack(side=tk.LEFT, padx=14, pady=12)
            week_bar = tk.Frame(card, bg=self.colors["card"])
            week_bar.pack(fill=tk.X, padx=8, pady=(10, 4))
            week_labels = []
            for j, day_text in enumerate(self.app.t("week_days")):
                lbl = tk.Label(week_bar, text=day_text, font=("Microsoft YaHei UI", 9, "bold"), fg=self.colors["danger"] if j >= 5 else self.colors["text_light"], bg=self.colors["card"])
                lbl.grid(row=0, column=j, sticky="nsew", padx=2, pady=2)
                week_bar.grid_columnconfigure(j, weight=1)
                week_labels.append(lbl)
            grid = tk.Frame(card, bg=self.colors["card"])
            grid.pack(fill=tk.BOTH, expand=True, padx=8, pady=(0, 10))
            cells = []
            for r in range(6):
                grid.grid_rowconfigure(r, weight=1, uniform="calendar_rows", minsize=54)
                row = []
                for c in range(7):
                    grid.grid_columnconfigure(c, weight=1, uniform="calendar_cols", minsize=52)
                    cell = tk.Frame(grid, bg=self.colors["card"], highlightthickness=1, highlightbackground=self.colors["border"], width=52, height=60)
                    cell.grid(row=r, column=c, sticky="nsew", padx=2, pady=2)
                    cell.grid_propagate(False)
                    cell.pack_propagate(False)
                    num = tk.Label(cell, text="", font=("Microsoft YaHei UI", 9, "bold"), bg=self.colors["card"], fg=self.colors["text"], cursor="hand2")
                    num.place(x=7, y=6)
                    sub = tk.Label(cell, text="", font=("Microsoft YaHei UI", 7), bg=self.colors["card"], fg=self.colors["text_secondary"], cursor="hand2", justify=tk.LEFT, wraplength=42)
                    sub.place(x=6, y=27)
                    marker = tk.Label(cell, text="", font=("Microsoft YaHei UI", 8), bg=self.colors["card"])
                    marker.place(relx=0.5, rely=0.90, anchor="center")
                    for w in (cell, num, sub):
                        w.bind("<Button-1>", lambda e, idx=i, rr=r, cc=c: self.day_click(idx, rr, cc))
                    row.append((cell, num, sub, marker))
                cells.append(row)
            self.cal_frames.append({"card": card, "label": month_label, "week_labels": week_labels, "grid": grid, "cells": cells, "year": 0, "month": 0})

        self.notes_title = tk.Label(
            right,
            text=self.app.t("notes_section"),
            font=("Microsoft YaHei UI", 16, "bold"),
            fg=self.colors["text"],
            bg=self.colors["card"],
            anchor="w",
            justify=tk.LEFT,
            wraplength=250 if getattr(self.app, "compact_mode", False) else 280,
        )
        self.notes_title.pack(fill=tk.X, padx=16, pady=(16, 10))

        self.date_bar = tk.Frame(right, bg=self.colors["primary_light"])
        self.date_bar.pack(fill=tk.X, padx=16)
        self.date_label = tk.Label(
            self.date_bar,
            text="",
            font=("Microsoft YaHei UI", 10, "bold"),
            fg=self.colors["primary"],
            bg=self.colors["primary_light"],
            anchor="w",
            justify=tk.LEFT,
            wraplength=250 if getattr(self.app, "compact_mode", False) else 280,
        )
        self.date_label.pack(fill=tk.X, padx=10, pady=10)

        note_container = tk.Frame(right, bg=self.colors["card"])
        note_container.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
        self.notes_canvas = tk.Canvas(note_container, bg=self.colors["card"], highlightthickness=0)
        self.notes_scrollbar = tk.Scrollbar(note_container, orient="vertical", command=self.notes_canvas.yview)
        self.notes_inner = tk.Frame(self.notes_canvas, bg=self.colors["card"])
        self.notes_canvas.configure(yscrollcommand=self.notes_scrollbar.set)
        self.notes_canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
        self.notes_scrollbar_visible = False
        self.notes_scroll_needed = False
        self.notes_window = self.notes_canvas.create_window((0, 0), window=self.notes_inner, anchor="nw", tags="inner")
        self.notes_inner.bind("<Configure>", self._update_notes_scrollbar)
        self.notes_canvas.bind("<Configure>", self._update_notes_scrollbar)
        self._bind_canvas_scroll(self.notes_canvas, self.notes_inner)

        self.add_wrap = tk.Frame(right, bg=self.colors["card"])
        self.add_wrap.pack(fill=tk.X, padx=14, pady=(0, 14))
        note_box = tk.Frame(self.add_wrap, bg=self.colors["bg"], highlightthickness=1, highlightbackground=self.colors["border"])
        note_box.pack(side=tk.LEFT, fill=tk.X, expand=True)
        tk.Frame(note_box, width=12, bg=self.colors["bg"]).pack(side=tk.LEFT)
        self.note_input = tk.Entry(note_box, font=("Microsoft YaHei UI", 10), relief=tk.FLAT, bd=0, bg=self.colors["bg"], fg=self.colors["text_light"])
        self.note_input.pack(side=tk.LEFT, fill=tk.X, expand=True, ipady=8, pady=1)
        self.note_input.insert(0, self.app.t("placeholder"))
        self.note_input.bind("<FocusIn>", self._input_focus_in)
        self.note_input.bind("<FocusOut>", self._input_focus_out)
        self.note_input.bind("<Return>", lambda e: self.quick_add())
        self.add_btn = create_button(self.add_wrap, self.app.t("add_note_btn"), self.open_note_dialog, self.colors, filled=True)
        self.add_btn.pack(side=tk.LEFT, padx=(8, 0))

        # Initial visual state: make sure Month and 1 Month are highlighted immediately.
        self._update_view_buttons()
        self._update_month_count_buttons()
        self._update_top_nav_visibility()
        self.on_app_resize(getattr(self.app.root, "winfo_width", lambda: 0)() or getattr(self.app, "screen_width", 1440), getattr(self.app.root, "winfo_height", lambda: 0)() or getattr(self.app, "screen_height", 900))

    def on_app_resize(self, width, height):
        try:
            # Never fake a 1220px layout. Use the real window size so every
            # module can compact itself on the current PC / DPI setting.
            width = max(int(width), 720)
        except Exception:
            width = 1440
        try:
            height = max(int(height), 520)
        except Exception:
            height = 900

        # Toolbar compaction: the biggest adaptation problems came from fixed
        # width buttons in the calendar toolbar. Narrow windows now use smaller
        # tabs and hide non-essential segments while preserving shortcuts.
        compact = width < 1240 or height < 720 or getattr(self.app, "runtime_compact_mode", False)
        narrow = width < 1080 or getattr(self.app, "runtime_ultra_compact_mode", False)
        try:
            for btn in (self.year_mode_btn, self.month_mode_btn, self.week_mode_btn, self.day_mode_btn):
                btn.configure(width=6 if narrow else (8 if compact else 10), pady=6 if compact else 8, font=("Microsoft YaHei UI", 8 if narrow else 9, "bold"))
            for btn in (self.one_month_btn, self.two_month_btn):
                btn.configure(width=6 if narrow else (8 if compact else 10), pady=6 if compact else 8, font=("Microsoft YaHei UI", 8 if narrow else 9, "bold"))
            if narrow and self.month_count_segment.winfo_manager():
                self.month_count_segment.pack_forget()
            elif not narrow and not self.month_count_segment.winfo_manager():
                self.month_count_segment.pack(side=tk.LEFT, padx=(0, 10), before=getattr(self, "goto_btn", None).master if hasattr(self, "goto_btn") else None)
        except Exception:
            pass

        # When the year view is active on narrow screens, collapse the right
        # notes panel so the 12-month view always has the full available width.
        collapse_side = bool(getattr(self, "view_mode", "month") == "year" and width < 1160)
        if collapse_side:
            try:
                if self.side_panel.winfo_manager():
                    self.side_panel.grid_remove()
                self.content.grid_columnconfigure(1, weight=0, minsize=0)
                self.left_panel.grid_configure(padx=(0, 0))
                self._side_panel_collapsed = True
            except Exception:
                pass
        else:
            if width < 1080:
                side_w = 252
            elif width < 1180:
                side_w = 276
            elif width < 1380:
                side_w = 304
            elif width < 1520:
                side_w = 342
            elif width < 1680:
                side_w = 374
            else:
                side_w = 410
            gap = 4 if width < 1080 else (6 if width < 1180 else 8 if width < 1400 else 12)
            self.side_panel_width = side_w
            try:
                if not self.side_panel.winfo_manager():
                    self.side_panel.grid(row=0, column=1, sticky="nsew")
                self.content.grid_columnconfigure(1, weight=0, minsize=side_w)
                self.left_panel.grid_configure(padx=(0, gap))
                self.side_panel.configure(width=side_w)
                self._side_panel_collapsed = False
            except Exception:
                pass
            wrap = max(side_w - 32, 220)
            title_wrap = max(side_w - 26, 220)
            try:
                self.notes_title.configure(wraplength=title_wrap, font=("Microsoft YaHei UI", 14 if compact else 16, "bold"))
                self.date_label.configure(wraplength=title_wrap, font=("Microsoft YaHei UI", 9 if compact else 10, "bold"))
                self.notes_canvas.itemconfig(self.notes_window, width=max(self.notes_canvas.winfo_width() - 4, wrap))
            except Exception:
                pass

        try:
            self._update_notes_scrollbar()
        except Exception:
            pass
        try:
            if getattr(self, "view_mode", "month") == "year" and getattr(self, "year_view_style", "overview") == "overview":
                redraw = getattr(self, "_year_overview_redraw", None)
                if callable(redraw):
                    if self._calendar_resize_job:
                        try:
                            self.app.root.after_cancel(self._calendar_resize_job)
                        except Exception:
                            pass
                    self._calendar_resize_job = self.app.root.after(80 if compact else 35, lambda: [setattr(self, "_calendar_resize_job", None), redraw()])
        except Exception:
            pass

    def _view_button(self, parent, key, mode):
        btn = tk.Button(
            parent,
            text=self.app.t(key),
            command=lambda m=mode: self._set_view(m),
            relief=tk.FLAT,
            bd=0,
            width=10,
            padx=0,
            pady=8,
            cursor="hand2",
            font=("Microsoft YaHei UI", 9, "bold"),
        )
        btn.pack(side=tk.LEFT, padx=2)
        return btn

    def _month_count_button(self, parent, key, count):
        btn = tk.Button(
            parent,
            text=self.app.t(key),
            command=lambda c=count: self._set_month_count(c),
            relief=tk.FLAT,
            bd=0,
            width=10,
            padx=0,
            pady=8,
            cursor="hand2",
            font=("Microsoft YaHei UI", 9, "bold"),
        )
        btn.pack(side=tk.LEFT, padx=2)
        return btn

    def _show_month_transition_cover(self):
        try:
            if self._month_transition_cover is not None and self._month_transition_cover.winfo_exists():
                self._month_transition_cover.lift()
                return
        except Exception:
            self._month_transition_cover = None
        try:
            cover = tk.Frame(self.normal_view, bg=self.colors["bg"], highlightthickness=0, bd=0)
            cover.place(x=0, y=0, relwidth=1, relheight=1)
            cover.lift()
            self._month_transition_cover = cover
        except Exception:
            self._month_transition_cover = None

    def _hide_month_transition_cover(self):
        try:
            cover = getattr(self, "_month_transition_cover", None)
            if cover is not None and cover.winfo_exists():
                cover.destroy()
        except Exception:
            pass
        self._month_transition_cover = None
        self._month_count_switching = False
        try:
            self.one_month_btn.config(state=tk.NORMAL)
            self.two_month_btn.config(state=tk.NORMAL)
        except Exception:
            pass

    def _schedule_hide_month_transition_cover(self, ms=55):
        job = getattr(self, "_month_transition_job", None)
        if job:
            try:
                self.app.root.after_cancel(job)
            except Exception:
                pass
        try:
            if getattr(self.app, "is_low_power_mode", lambda: False)():
                ms = min(ms, 25)
        except Exception:
            pass
        self._month_transition_job = self.app.root.after(ms, self._hide_month_transition_cover)

    def _set_month_count(self, count):
        new_count = 1 if int(count) == 1 else 2
        if self._month_count_switching:
            return
        if new_count == self.month_count and self.view_mode == "month":
            return
        self._month_count_switching = True
        try:
            self.one_month_btn.config(state=tk.DISABLED)
            self.two_month_btn.config(state=tk.DISABLED)
        except Exception:
            pass
        self._show_month_transition_cover()
        self.month_count = new_count
        self.app.user_settings["calendar_month_count"] = self.month_count
        try:
            save_settings(self.app.user_settings)
        except Exception:
            pass
        self.view_mode = "month"
        self.is_year_view = False
        self._update_view_buttons()
        self._update_month_count_buttons()
        self.year_view_frame.pack_forget()
        self.agenda_view_frame.pack_forget()
        self.normal_view.pack(fill=tk.BOTH, expand=True)
        self.app.root.after(1, self._finish_month_count_change)

    def _finish_month_count_change(self):
        try:
            self._apply_month_layout()
            self.cell_cache.clear()
            self._refresh_calendar()
        finally:
            self._schedule_hide_month_transition_cover(55)

    def _update_month_count_buttons(self):
        if not hasattr(self, "one_month_btn"):
            return
        if self.view_mode == "month":
            if not self.month_count_segment.winfo_manager():
                self.month_count_segment.pack(side=tk.LEFT, padx=(0, 10))
        else:
            if self.month_count_segment.winfo_manager():
                self.month_count_segment.pack_forget()
        for btn, count in [(self.one_month_btn, 1), (self.two_month_btn, 2)]:
            active = self.month_count == count and self.view_mode == "month"
            btn.config(
                bg=self.colors["primary"] if active else self.colors["card_soft"],
                fg="white" if active else self.colors["text_secondary"],
                activebackground=self.colors["primary_dark"] if active else self.colors["primary_light"],
                activeforeground="white" if active else self.colors["primary"],
            )

    def _apply_month_layout(self):
        if not hasattr(self, "cal_frames") or len(self.cal_frames) < 2:
            return
        self.normal_view.grid_columnconfigure(0, weight=1, uniform="months")
        self.normal_view.grid_rowconfigure(0, weight=1, uniform="months_rows")
        single = self.month_count == 1
        if single:
            self.normal_view.grid_columnconfigure(1, weight=0, minsize=0, uniform="")
            self.cal_frames[1]["card"].grid_remove()
            self.cal_frames[0]["card"].grid_configure(row=0, column=0, sticky="nsew", padx=0)
        else:
            self.normal_view.grid_columnconfigure(1, weight=1, uniform="months")
            self.cal_frames[0]["card"].grid_configure(row=0, column=0, sticky="nsew", padx=(0, 7))
            self.cal_frames[1]["card"].grid_configure(row=0, column=1, sticky="nsew", padx=(7, 0))
            self.cal_frames[1]["card"].grid()

        month_font = 18 if single else 14
        week_font = 11 if single else 9
        num_font = 16 if single else 9
        sub_font = 10 if single else 7
        cell_w = 96 if single else 52
        cell_h = 118 if single else 60
        row_min = 104 if single else 54
        col_min = 92 if single else 52
        sub_wrap = 118 if single else 42
        sub_y = 42 if single else 27
        marker_font = 10 if single else 8

        for cf in self.cal_frames:
            cf["label"].config(font=("Microsoft YaHei UI", month_font, "bold"))
            for lbl in cf["week_labels"]:
                lbl.config(font=("Microsoft YaHei UI", week_font, "bold"))
            grid = cf.get("grid")
            if grid is not None:
                for r in range(6):
                    grid.grid_rowconfigure(r, weight=1, uniform="calendar_rows", minsize=row_min)
                for c in range(7):
                    grid.grid_columnconfigure(c, weight=1, uniform="calendar_cols", minsize=col_min)
            for row in cf["cells"]:
                for cell, num, sub, marker in row:
                    cell.config(width=cell_w, height=cell_h)
                    num.config(font=("Microsoft YaHei UI", num_font, "bold"))
                    num.place(x=10 if single else 7, y=8 if single else 6)
                    sub.config(font=("Microsoft YaHei UI", sub_font), wraplength=sub_wrap)
                    sub.place(x=8 if single else 6, y=sub_y)
                    marker.config(font=("Microsoft YaHei UI", marker_font))
                    marker.place(relx=0.5, rely=0.89 if single else 0.90, anchor="center")

    def _selected_date_obj(self):
        try:
            return datetime.strptime(self.selected_date, "%Y-%m-%d").date()
        except Exception:
            return date.today()

    def _sync_month_offset_to_selected(self):
        d = self._selected_date_obj()
        today = date.today()
        self.current_offset = (d.year - today.year) * 12 + (d.month - today.month)

    def _shift_selected_year(self, delta_year):
        d = self._selected_date_obj()
        target_year = d.year + delta_year
        day = d.day
        while day > 28:
            try:
                new_d = date(target_year, d.month, day)
                break
            except Exception:
                day -= 1
        else:
            new_d = date(target_year, d.month, 1)
        self.selected_date = new_d.strftime("%Y-%m-%d")
        self._sync_month_offset_to_selected()

    def _set_view(self, mode):
        self.view_mode = mode
        self.is_year_view = (mode == "year")
        if mode in ("month", "week", "day", "year"):
            self._sync_month_offset_to_selected()
        if mode == "year":
            self.year_offset = 0
        self._update_view_buttons()
        self._update_month_count_buttons()
        self._update_top_nav_visibility()
        if mode == "year":
            self.normal_view.pack_forget()
            self.agenda_view_frame.pack_forget()
            self.on_app_resize(getattr(self.app.root, "winfo_width", lambda: 0)(), getattr(self.app.root, "winfo_height", lambda: 0)())
            self._show_year_view()
            self.update_notes_panel()
        elif mode in ("week", "day"):
            self.year_view_frame.pack_forget()
            self.normal_view.pack_forget()
            self._show_agenda_view(mode)
            self.update_notes_panel()
        else:
            self.year_view_frame.pack_forget()
            self.agenda_view_frame.pack_forget()
            self.on_app_resize(getattr(self.app.root, "winfo_width", lambda: 0)(), getattr(self.app.root, "winfo_height", lambda: 0)())
            self.normal_view.pack(fill=tk.BOTH, expand=True)
            self._apply_month_layout()
            self.cell_cache.clear()
            self._refresh_calendar()

    def _update_top_nav_visibility(self):
        if not hasattr(self, "nav"):
            return
        # Keep the legend row visible at the top, but only show prev/next/today in month view.
        if not self.nav.winfo_manager():
            self.nav.pack(fill=tk.X, pady=(0, 10), before=self.normal_view)
        if hasattr(self, "month_nav"):
            if self.view_mode == "month":
                if not self.month_nav.winfo_manager():
                    self.month_nav.pack(side=tk.RIGHT)
            else:
                if self.month_nav.winfo_manager():
                    self.month_nav.pack_forget()

    def _update_view_buttons(self):
        mapping = [
            (self.year_mode_btn, "year"),
            (self.month_mode_btn, "month"),
            (self.week_mode_btn, "week"),
            (self.day_mode_btn, "day"),
        ]
        for btn, mode in mapping:
            active = self.view_mode == mode
            btn.config(
                bg=self.colors["primary"] if active else self.colors["card_soft"],
                fg="white" if active else self.colors["text_secondary"],
                activebackground=self.colors["primary_dark"] if active else self.colors["primary_light"],
                activeforeground="white" if active else self.colors["primary"],
            )

    def _filter_button(self, parent, text, bg, fg, command):
        btn = tk.Button(parent, text=text, command=command, bg=bg, fg=fg, relief=tk.FLAT, bd=0, cursor="hand2", font=("Microsoft YaHei UI", 9, "bold"), width=8, padx=0, pady=8)
        btn.pack(side=tk.LEFT, padx=4)
        return btn

    def _build_legend(self):
        for child in self.legend.winfo_children():
            child.destroy()
        items = [
            (self.colors["danger"], "holiday_type_china"),
            (self.colors["warning"], "holiday_type_indonesia"),
            (self.colors["solar"], "holiday_type_solar"),
            (self.colors["success"], "holiday_type_notes"),
            (self.colors["text_secondary"], "holiday_type_lunar"),
        ]
        for color, key in items:
            item = tk.Frame(self.legend, bg=self.colors["bg"])
            item.pack(side=tk.LEFT, padx=8)
            tk.Label(item, text="●", fg=color, bg=self.colors["bg"], font=("Microsoft YaHei UI", 10)).pack(side=tk.LEFT)
            tk.Label(item, text=self.app.t(key), fg=self.colors["text_secondary"], bg=self.colors["bg"], font=("Microsoft YaHei UI", 9)).pack(side=tk.LEFT, padx=(4, 0))

    def _bind_canvas_scroll(self, canvas, inner=None):
        if not hasattr(self, "_scroll_canvases"):
            self._scroll_canvases = []
        if canvas not in self._scroll_canvases:
            self._scroll_canvases.append(canvas)
        widgets = [canvas]
        if inner is not None:
            widgets.append(inner)
        for widget in widgets:
            widget.bind("<Enter>", lambda e, c=canvas: setattr(self, "_active_scroll_canvas", c), add="+")

    def _pointer_inside_canvas(self, canvas, event):
        try:
            x1, y1 = canvas.winfo_rootx(), canvas.winfo_rooty()
            x2 = x1 + canvas.winfo_width()
            y2 = y1 + canvas.winfo_height()
            return x1 <= event.x_root <= x2 and y1 <= event.y_root <= y2
        except Exception:
            return False

    def _canvas_under_pointer(self, event):
        for canvas in reversed(getattr(self, "_scroll_canvases", [])):
            try:
                if canvas.winfo_exists() and self._pointer_inside_canvas(canvas, event):
                    return canvas
            except Exception:
                pass
        return getattr(self, "_active_scroll_canvas", None)

    def _canvas_can_scroll(self, canvas):
        try:
            lo, hi = canvas.yview()
            return (hi - lo) < 0.999
        except Exception:
            return False

    def _global_mousewheel(self, event):
        canvas = self._canvas_under_pointer(event)
        if not canvas or not self.frame.winfo_ismapped() or not self._canvas_can_scroll(canvas):
            return
        delta = getattr(event, "delta", 0)
        if delta == 0:
            return
        # Windows precision touchpads can send small deltas; treat every event as a scroll step.
        steps = -1 * int(delta / 120) if abs(delta) >= 120 else (-1 if delta > 0 else 1)
        canvas.yview_scroll(steps, "units")
        return "break"

    def _global_mousewheel_linux(self, direction, event):
        canvas = self._canvas_under_pointer(event)
        if canvas and self.frame.winfo_ismapped() and self._canvas_can_scroll(canvas):
            canvas.yview_scroll(direction, "units")
            return "break"

    def _update_notes_scrollbar(self, event=None):
        if not hasattr(self, "notes_canvas"):
            return
        self.notes_canvas.configure(scrollregion=self.notes_canvas.bbox("all"))
        try:
            self.notes_canvas.itemconfig(self.notes_window, width=max(self.notes_canvas.winfo_width() - 4, 260))
            needed = self.notes_inner.winfo_reqheight() > self.notes_canvas.winfo_height() + 2
            self.notes_scroll_needed = needed
            if needed and not self.notes_scrollbar_visible:
                self.notes_scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
                self.notes_scrollbar_visible = True
            elif not needed and self.notes_scrollbar_visible:
                self.notes_scrollbar.pack_forget()
                self.notes_scrollbar_visible = False
        except Exception:
            pass

    def _show_or_hide_add_wrap(self, visible=True):
        if visible and not self.add_wrap.winfo_manager():
            self.add_wrap.pack(fill=tk.X, padx=14, pady=(0, 14))
        elif not visible and self.add_wrap.winfo_manager():
            self.add_wrap.pack_forget()

    def _holiday_visible(self, info):
        htype = (info or {}).get("type", "")
        return (
            (htype == "china" and self.show_china.get()) or
            (htype == "indonesia" and self.show_indonesia.get()) or
            (htype == "solar_term" and self.show_solar.get()) or
            (htype == "note" and self.show_notes.get())
        )

    def _iter_year_holidays(self, year, include_notes=True):
        cache_key = (
            year,
            include_notes,
            self.app.lang_code,
            bool(self.show_china.get()),
            bool(self.show_indonesia.get()),
            bool(self.show_solar.get()),
            bool(self.show_notes.get()),
            self.app.search_keyword or "",
        )
        cached = self._year_holiday_cache.get(cache_key)
        if cached is not None:
            return cached

        items = []
        prefix = f"{year}-"

        for ds, info in self.app.holidays.items():
            if not ds.startswith(prefix):
                continue
            if self.app.search_keyword and not self.app.matches_search(ds, holiday_text(info.get("name"), self.app.lang_code), holiday_text(info.get("desc"), self.app.lang_code), info.get("type")):
                continue
            if self._holiday_visible(info):
                items.append((ds, info))

        if include_notes and self.show_notes.get():
            for ds, note in self.app.notes.items():
                if not str(ds).startswith(prefix):
                    continue
                note_text = str((note or {}).get("text", ""))
                note_time = str((note or {}).get("time", ""))
                if self.app.search_keyword and not self.app.matches_search(ds, note_text, note_time, self.app.t("holiday_type_notes")):
                    continue
                items.append((ds, {
                    "type": "note",
                    "emoji": "●",
                    "name": self.app.t("holiday_type_notes"),
                    "desc": f"{note_time}  {note_text}".strip(),
                }))

        items.sort(key=lambda x: (x[0], 1 if x[1].get("type") == "note" else 0))
        self._year_holiday_cache[cache_key] = items
        return items

    def _render_holiday_row(self, parent, ds, info, compact=False):
        htype = info.get("type", "")
        fg, bg = self.colors["primary"], self.colors["primary_light"]
        if htype == "china":
            fg, bg = self.colors["danger"], self.colors["danger_light"]
        elif htype == "indonesia":
            fg, bg = self.colors["warning"], self.colors["warning_light"]
        elif htype == "solar_term":
            fg, bg = self.colors["solar"], self.colors["solar_light"]
        elif htype == "note":
            fg, bg = self.colors["success"], self.colors["success_light"]
        item = tk.Frame(parent, bg=self.colors["card_soft"], highlightthickness=1, highlightbackground=self.colors["border"], cursor="hand2")
        item.pack(fill=tk.X, pady=4)
        item.bind("<Button-1>", lambda e, date_key=ds: self.open_date(date_key))
        top = tk.Frame(item, bg=self.colors["card_soft"])
        top.pack(fill=tk.X, padx=10, pady=(8, 4))
        badge = tk.Label(top, text=self.app.t(self._holiday_type_key(htype)), font=("Microsoft YaHei UI", 9, "bold"), fg=fg, bg=bg, padx=7, pady=2)
        badge.pack(side=tk.LEFT)
        badge.bind("<Button-1>", lambda e, date_key=ds: self.open_date(date_key))
        date_text = ds if compact else self.app.format_date(datetime.strptime(ds, "%Y-%m-%d").date())
        date_lbl = tk.Label(top, text=date_text, font=("Microsoft YaHei UI", 9, "bold"), fg=self.colors["text_secondary"], bg=self.colors["card_soft"])
        date_lbl.pack(side=tk.RIGHT)
        date_lbl.bind("<Button-1>", lambda e, date_key=ds: self.open_date(date_key))
        row_name = " ".join(part for part in [str(info.get("emoji", "") or "").strip(), holiday_text(info.get("name"), self.app.lang_code)] if part)
        name = tk.Label(item, text=row_name, font=("Microsoft YaHei UI", 12, "bold"), fg=fg, bg=self.colors["card_soft"], wraplength=max(getattr(self, "side_panel_width", 310) - 34, 240), justify=tk.LEFT, anchor="w")
        name.pack(fill=tk.X, padx=10)
        name.bind("<Button-1>", lambda e, date_key=ds: self.open_date(date_key))
        desc = holiday_text(info.get("desc"), self.app.lang_code) or self.app.t(self._holiday_default_intro_key(htype))
        desc_lbl = tk.Label(item, text=desc, font=("Microsoft YaHei UI", 10), fg=self.colors["text_secondary"], bg=self.colors["card_soft"], wraplength=max(getattr(self, "side_panel_width", 310) - 34, 240), justify=tk.LEFT, anchor="w")
        desc_lbl.pack(fill=tk.X, padx=10, pady=(5, 10))
        desc_lbl.bind("<Button-1>", lambda e, date_key=ds: self.open_date(date_key))
        return item

    def _render_year_side_panel(self, year):
        self.notes_title.config(text=self.app.t("holiday_list_title"))
        items = self._iter_year_holidays(year, include_notes=False)
        summary = self.app.t("holiday_list_summary").format(year=year, count=len(items))
        self.date_bar.config(bg=self.colors["primary_light"])
        self.date_label.config(text=summary, bg=self.colors["primary_light"], fg=self.colors["primary"])

        action_card = self._note_card()
        create_button(action_card, self.app.t("year_open_all_holidays"), lambda: self._show_all_year_holidays_popup(year), self.colors, filled=True).pack(fill=tk.X, padx=10, pady=10)

        if items:
            for ds, info in items:
                self._render_holiday_row(self.notes_inner, ds, info, compact=True)
        else:
            show_empty(self.notes_inner, self.app.empty_text("holiday_list_empty"), self.colors)

    def _show_all_year_holidays_popup(self, year):
        popup = tk.Toplevel(self.app.root)
        popup.title(f"{year} · {self.app.t('holiday_list_title')}")
        popup.configure(bg=self.colors["bg"])
        self.app._set_window_icon(popup)
        try:
            popup.transient(self.app.root)
        except Exception:
            pass
        center_window(popup, self.app.root, 520, 680, min_width=460, min_height=520)
        card = create_card(popup, self.colors)
        card.pack(fill=tk.BOTH, expand=True, padx=14, pady=14)
        tk.Label(card, text=self.app.t("holiday_list_summary").format(year=year, count=len(self._iter_year_holidays(year, include_notes=False))), bg=self.colors["card"], fg=self.colors["text"], font=("Microsoft YaHei UI", 14, "bold")).pack(anchor="w", padx=16, pady=(14, 8))
        wrap = tk.Frame(card, bg=self.colors["card"])
        wrap.pack(fill=tk.BOTH, expand=True, padx=12, pady=(0, 12))
        canvas = tk.Canvas(wrap, bg=self.colors["card"], highlightthickness=0)
        scroll = tk.Scrollbar(wrap, orient="vertical", command=canvas.yview)
        inner = tk.Frame(canvas, bg=self.colors["card"])
        canvas.configure(yscrollcommand=scroll.set)
        canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
        scroll.pack(side=tk.RIGHT, fill=tk.Y)
        win = canvas.create_window((0, 0), window=inner, anchor="nw")
        canvas.bind("<Configure>", lambda e: canvas.itemconfig(win, width=max(e.width - 4, 360)))
        inner.bind("<Configure>", lambda e: canvas.configure(scrollregion=canvas.bbox("all")))
        for ds, info in self._iter_year_holidays(year, include_notes=False):
            self._render_holiday_row(inner, ds, info, compact=True)
        create_button(card, self.app.t("close"), popup.destroy, self.colors).pack(anchor="e", padx=16, pady=(0, 14))

    def refresh(self):
        self._update_view_buttons()
        self._update_month_count_buttons()
        self._update_top_nav_visibility()
        self._refresh_calendar()

    def update_language(self):
        self.filter_label.config(text=f"{self.app.t('filter_label')}：")
        self.goto_btn.config(text=self.app.t("goto_date"))
        self.one_month_btn.config(text=self.app.t("month_count_one"))
        self.two_month_btn.config(text=self.app.t("month_count_two"))
        self._update_month_count_buttons()
        self.today_btn.config(text=self.app.t("today_btn"))
        self.lunar_btn.config(text=self.app.t("lunar_mode"))
        self.notes_btn.config(text=self.app.t("holiday_type_notes"))
        self.year_mode_btn.config(text=self.app.t("view_year"))
        self.month_mode_btn.config(text=self.app.t("view_month"))
        self.week_mode_btn.config(text=self.app.t("view_week"))
        self.day_mode_btn.config(text=self.app.t("view_day"))
        self.notes_title.config(text=self.app.t("notes_section"))
        self.add_btn.config(text=self.app.t("add_note_btn"))
        if self.note_input.get().strip() in self.app.all_values("placeholder") or not self.note_input.get().strip():
            self.note_input.delete(0, tk.END)
            self.note_input.insert(0, self.app.t("placeholder"))
            self.note_input.config(fg=self.colors["text_light"])
        self._build_legend()
        self._update_view_buttons()
        self._update_month_count_buttons()
        self._update_top_nav_visibility()
        self.cell_cache.clear()
        self._lunar_cache.clear()
        self._year_holiday_cache.clear()
        self._refresh_calendar()

    def _refresh_calendar(self):
        # Do not redraw the hidden month grid when the active view is Year/Week/Day.
        # This is one of the main performance fixes for lower-spec laptops.
        if getattr(self, "view_mode", "month") == "year":
            self._show_year_view()
            self.update_notes_panel()
            return
        if getattr(self, "view_mode", "month") in ("week", "day"):
            self._show_agenda_view(self.view_mode)
            self.update_notes_panel()
            return

        today = date.today()
        base_month = today.month + self.current_offset
        base_year = today.year
        while base_month < 1:
            base_month += 12
            base_year -= 1
        while base_month > 12:
            base_month -= 12
            base_year += 1
        next_year = base_year if base_month < 12 else base_year + 1
        next_month = base_month + 1 if base_month < 12 else 1
        months = [(base_year, base_month)] if self.month_count == 1 else [(base_year, base_month), (next_year, next_month)]
        self._apply_month_layout()
        week_days = self.app.t("week_days")

        for idx, (y, m) in enumerate(months):
            cf = self.cal_frames[idx]
            cf["year"], cf["month"] = y, m
            cf["label"].config(text=f"{self.app.t('month_names')[m - 1]} {y}")
            for j, lbl in enumerate(cf["week_labels"]):
                lbl.config(text=week_days[j])
            first = date(y, m, 1)
            start_weekday = first.weekday()
            days_in_month = (date(y, m + 1, 1) - timedelta(days=1)).day if m < 12 else 31
            prev_m, prev_y = (12, y - 1) if m == 1 else (m - 1, y)
            prev_dim = (date(prev_y, prev_m + 1, 1) - timedelta(days=1)).day if prev_m < 12 else 31
            for r in range(6):
                for c in range(7):
                    idx_num = r * 7 + c
                    if idx_num < start_weekday:
                        day_num = prev_dim - start_weekday + idx_num + 1
                        ds, other = fmt_date_key(prev_y, prev_m, day_num), True
                    elif idx_num - start_weekday < days_in_month:
                        day_num = idx_num - start_weekday + 1
                        ds, other = fmt_date_key(y, m, day_num), False
                    else:
                        day_num = idx_num - start_weekday - days_in_month + 1
                        next_m, next_y = (1, y + 1) if m == 12 else (m + 1, y)
                        ds, other = fmt_date_key(next_y, next_m, day_num), True
                    self._style_cell(cf["cells"][r][c], day_num, ds, other)
        if self.is_year_view:
            self._show_year_view()
        self.update_notes_panel()

    def _short_holiday_label(self, text, compact=True):
        text = str(text or "").strip()
        if not text:
            return ""
        if compact:
            if self.app.lang_code == "zh_CN":
                return text[:4]
            words = text.replace("/", " ").split()
            if len(words) >= 2:
                return (words[0][:5] + "\n" + words[1][:5]).strip()
            return text[:8]

        # Full mode for 1-month view: show more readable text because the card has more space.
        if self.app.lang_code == "zh_CN":
            text = text.replace("/", " / ")
            return text[:12]

        normalized = " ".join(text.replace("/", " / ").split())
        limit = 20 if self.app.lang_code == "en_US" else 22
        if len(normalized) <= limit:
            return normalized
        words = normalized.split()
        line1, line2 = [], []
        current_len = 0
        for word in words:
            next_len = current_len + (1 if line1 else 0) + len(word)
            if next_len <= 11:
                line1.append(word)
                current_len = next_len
            else:
                line2.append(word)
        line1_text = " ".join(line1).strip()
        line2_text = " ".join(line2).strip()
        if len(line2_text) > 12:
            line2_text = line2_text[:9].rstrip() + "..."
        if not line2_text:
            return normalized[:limit]
        return f"{line1_text}\n{line2_text}".strip()

    def _style_cell(self, cell_info, day_num, ds, other_month):
        cell, num, sub, marker = cell_info
        selected = ds == self.selected_date
        today_key = date.today().strftime("%Y-%m-%d")
        is_today = ds == today_key
        holiday = self.app.holidays.get(ds, {})
        htype = holiday.get("type", "")
        show_holiday = (
            (htype == "china" and self.show_china.get()) or
            (htype == "indonesia" and self.show_indonesia.get()) or
            (htype == "solar_term" and self.show_solar.get())
        )
        if selected:
            bg, fg, sub_fg = self.colors["primary"], "white", "white"
        elif is_today:
            bg, fg, sub_fg = self.colors["primary_light"], self.colors["primary"], self.colors["primary"]
        elif show_holiday:
            if htype == "china":
                bg, fg = self.colors["danger_light"], self.colors["danger"]
            elif htype == "indonesia":
                bg, fg = self.colors["warning_light"], self.colors["warning"]
            else:
                bg, fg = self.colors["solar_light"], self.colors["solar"]
            sub_fg = fg
        elif other_month:
            bg, fg, sub_fg = self.colors["other_month"], self.colors["other_month_text"], self.colors["other_month_text"]
        else:
            bg, fg, sub_fg = self.colors["card"], self.colors["text"], self.colors["text_secondary"]
        try:
            weekday = date.fromisoformat(ds).weekday()
        except Exception:
            weekday = 0
        if weekday >= 5 and not (selected or is_today or show_holiday or other_month):
            fg = self.colors["danger"]
        holiday_name = holiday_text(holiday.get("name"), self.app.lang_code) if show_holiday else ""
        if self.show_lunar.get():
            lunar_key = (ds, self.app.lang_code)
            lunar_name = self._lunar_cache.get(lunar_key)
            if lunar_name is None:
                lunar_name = lunar_cell_text(ds, self.app.lang_code)
                self._lunar_cache[lunar_key] = lunar_name
        else:
            lunar_name = ""
        full_mode = self.view_mode == "month" and self.month_count == 1 and not other_month
        holiday_short = self._short_holiday_label(holiday_name, compact=not full_mode)
        lunar_short = str(lunar_name or "")[:8 if full_mode else 6]
        if holiday_short and lunar_short:
            sub_text = f"{holiday_short}\n{lunar_short[:6] if full_mode else lunar_short[:4]}"
        else:
            sub_text = holiday_short or (lunar_short[:8] if full_mode else lunar_short[:6])
        marker_text = "●" if self.show_notes.get() and ds in self.app.notes else ""
        state = (str(day_num), bg, fg, sub_fg, sub_text, marker_text, selected)
        key = id(cell)
        if self.cell_cache.get(key) == state:
            return
        self.cell_cache[key] = state
        cell.config(bg=bg, highlightbackground=self.colors["border"])
        num.config(text=str(day_num), bg=bg, fg=fg)
        sub.config(text=sub_text, bg=bg, fg=sub_fg)
        marker.config(text=marker_text, bg=bg, fg="white" if selected else self.colors["primary"])

    def day_click(self, idx, row, col):
        cf = self.cal_frames[idx]
        y, m = cf["year"], cf["month"]
        first = date(y, m, 1)
        start_weekday = first.weekday()
        dim = (date(y, m + 1, 1) - timedelta(days=1)).day if m < 12 else 31
        idx_num = row * 7 + col
        clicked_other_month = False
        if idx_num < start_weekday:
            prev_m, prev_y = (12, y - 1) if m == 1 else (m - 1, y)
            prev_dim = (date(prev_y, prev_m + 1, 1) - timedelta(days=1)).day if prev_m < 12 else 31
            ds = fmt_date_key(prev_y, prev_m, prev_dim - start_weekday + idx_num + 1)
            clicked_other_month = True
        elif idx_num - start_weekday < dim:
            ds = fmt_date_key(y, m, idx_num - start_weekday + 1)
        else:
            next_m, next_y = (1, y + 1) if m == 12 else (m + 1, y)
            ds = fmt_date_key(next_y, next_m, idx_num - start_weekday - dim + 1)
            clicked_other_month = True

        self.selected_date = ds

        # Month display logic:
        # - If clicking a day inside the visible months, keep the current two-month page.
        # - If clicking a previous-month day on the left month, shift to previous + current.
        # - If clicking a next-month day on the right month, shift to current + next.
        if self.view_mode == "month" and clicked_other_month:
            clicked = datetime.strptime(ds, "%Y-%m-%d").date()
            today = date.today()
            if self.month_count == 1:
                self.current_offset = (clicked.year - today.year) * 12 + (clicked.month - today.month)
            else:
                base_year, base_month = self.cal_frames[0]["year"], self.cal_frames[0]["month"]
                base_first = date(base_year, base_month, 1)
                second_year, second_month = self.cal_frames[1]["year"], self.cal_frames[1]["month"]
                second_last = date(second_year, second_month + 1, 1) - timedelta(days=1) if second_month < 12 else date(second_year, 12, 31)
                if clicked < base_first:
                    new_base = date(clicked.year, clicked.month, 1)
                    self.current_offset = (new_base.year - today.year) * 12 + (new_base.month - today.month)
                elif clicked > second_last:
                    prev_month = clicked.month - 1
                    prev_year = clicked.year
                    if prev_month < 1:
                        prev_month = 12
                        prev_year -= 1
                    new_base = date(prev_year, prev_month, 1)
                    self.current_offset = (new_base.year - today.year) * 12 + (new_base.month - today.month)

        self.cell_cache.clear()
        self._refresh_calendar()
        self._update_year_compact_info()
        self.app.switch_page("calendar")

    def _select_year_date(self, ds, open_note=False):
        self.selected_date = ds
        self.year_offset = 0
        self._sync_month_offset_to_selected()
        self.cell_cache.clear()
        # Keep year-view clicks fast, but redraw the canvas selection layer so
        # the clicked date is visibly highlighted immediately. This fixes the
        # old behavior where the right panel changed but the date cell did not
        # show an active state.
        try:
            if getattr(self, "view_mode", "month") == "year" and getattr(self, "year_view_style", "overview") == "overview":
                redraw = getattr(self, "_year_overview_redraw", None)
                if callable(redraw):
                    old_job = getattr(self, "_year_select_redraw_job", None)
                    if old_job:
                        try:
                            self.app.root.after_cancel(old_job)
                        except Exception:
                            pass
                    self._year_select_redraw_job = self.app.root.after(35, lambda: [setattr(self, "_year_select_redraw_job", None), redraw()])
        except Exception:
            pass
        self.update_notes_panel()
        self._update_year_compact_info()
        if open_note:
            self.open_note_dialog(False)

    def open_date(self, ds):
        self.selected_date = ds
        self.year_offset = 0
        self._sync_month_offset_to_selected()
        self.cell_cache.clear()
        self._refresh_calendar()
        self.app.switch_page("calendar")

    def _smooth_month_change(self, delta):
        if self._nav_locked:
            return
        self._nav_locked = True
        self.prev_btn.config(state=tk.DISABLED)
        self.next_btn.config(state=tk.DISABLED)
        self.current_offset += delta
        self.cell_cache.clear()
        self.app.set_status("status_searching")
        self.app.root.after(20, self._finish_month_change)

    def _finish_month_change(self):
        self._refresh_calendar()
        self.prev_btn.config(state=tk.NORMAL)
        self.next_btn.config(state=tk.NORMAL)
        self._nav_locked = False
        self.app.set_status("status_ready")

    def prev_month(self):
        self._smooth_month_change(-1)

    def next_month(self):
        self._smooth_month_change(1)

    def go_today(self):
        self.current_offset = 0
        self.year_offset = 0
        self.selected_date = date.today().strftime("%Y-%m-%d")
        self._set_view("month")
        self.cell_cache.clear()
        self._refresh_calendar()

    def _center_dialog(self, dlg, width, height):
        center_window(dlg, self.app.root, width, height, min_width=width, min_height=height)

    def _goto_dialog(self):
        dlg = tk.Toplevel(self.app.root)
        dlg.title(self.app.t("goto_date"))
        dlg.configure(bg=self.colors["card"])
        dlg.transient(self.app.root)
        dlg.grab_set()
        dlg.resizable(False, False)

        # The old 380x220 dialog was too small after UI font scaling and some
        # languages clipped the input/buttons. Use a roomier fixed size so the
        # modal stays fully visible on normal laptop screens.
        dlg_w = 460
        dlg_h = 300
        dlg.minsize(dlg_w, dlg_h)
        self._center_dialog(dlg, dlg_w, dlg_h)

        head = tk.Frame(dlg, bg=self.colors["card"])
        head.pack(fill=tk.X, padx=28, pady=(24, 10))
        tk.Label(head, text=self.app.t("goto_date"), font=("Microsoft YaHei UI", 16, "bold"), fg=self.colors["text"], bg=self.colors["card"]).pack(side=tk.LEFT)

        body = tk.Frame(dlg, bg=self.colors["card"])
        body.pack(fill=tk.BOTH, expand=True, padx=28, pady=(0, 24))
        tk.Label(body, text=self.app.t("goto_hint"), font=("Microsoft YaHei UI", 10, "bold"), fg=self.colors["text_secondary"], bg=self.colors["card"], wraplength=390, justify=tk.LEFT).pack(anchor="w")
        date_var = tk.StringVar(value=self.selected_date)
        entry_box = tk.Frame(body, bg=self.colors["bg"], highlightthickness=1, highlightbackground=self.colors["border"])
        entry_box.pack(fill=tk.X, pady=(12, 22))
        entry = tk.Entry(entry_box, textvariable=date_var, font=("Microsoft YaHei UI", 18, "bold"), relief=tk.FLAT, bd=0, justify=tk.CENTER, bg=self.colors["bg"], fg=self.colors["text"])
        entry.pack(fill=tk.X, ipady=12, padx=12, pady=2)
        footer = tk.Frame(body, bg=self.colors["card"])
        footer.pack(fill=tk.X, side=tk.BOTTOM)

        def confirm():
            try:
                self.open_date(datetime.strptime(date_var.get().strip(), "%Y-%m-%d").strftime("%Y-%m-%d"))
                dlg.destroy()
            except Exception:
                messagebox.showwarning(self.app.t("warning_title"), self.app.t("goto_hint"))

        create_button(footer, self.app.t("schedule_cancel"), dlg.destroy, self.colors).pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(0, 10), ipady=4)
        create_button(footer, self.app.t("schedule_done"), confirm, self.colors, filled=True).pack(side=tk.LEFT, fill=tk.X, expand=True, ipady=4)
        entry.focus_set()
        entry.select_range(0, tk.END)
        entry.bind("<Return>", lambda e: confirm())

    def _toggle(self, item_type):
        if item_type == "china":
            self.show_china.set(not self.show_china.get())
            self.china_btn.config(bg=self.colors["danger_light"] if self.show_china.get() else self.colors["card"])
        elif item_type == "indonesia":
            self.show_indonesia.set(not self.show_indonesia.get())
            self.indo_btn.config(bg=self.colors["warning_light"] if self.show_indonesia.get() else self.colors["card"])
        elif item_type == "solar":
            self.show_solar.set(not self.show_solar.get())
            self.solar_btn.config(bg=self.colors["solar_light"] if self.show_solar.get() else self.colors["card"])
        elif item_type == "notes":
            self.show_notes.set(not self.show_notes.get())
            self.notes_btn.config(bg=self.colors["success_light"] if self.show_notes.get() else self.colors["card"])
        else:
            self.show_lunar.set(not self.show_lunar.get())
            self.lunar_btn.config(bg=self.colors["primary_light"] if self.show_lunar.get() else self.colors["card"])
        self.cell_cache.clear()
        self._year_holiday_cache.clear()
        self._refresh_calendar()

    def _toggle_year(self):
        self._set_view("month" if self.view_mode == "year" else "year")

    def _show_agenda_view(self, mode):
        if mode == "week":
            self._show_week_timeline_view()
            return

        for child in self.agenda_view_frame.winfo_children():
            child.destroy()
        self.agenda_view_frame.pack(fill=tk.BOTH, expand=True)

        try:
            selected = datetime.strptime(self.selected_date, "%Y-%m-%d").date()
        except Exception:
            selected = date.today()

        dates = [selected]
        title = f"{self.app.t('calendar_title_day')} · {selected.strftime('%Y-%m-%d')}"

        top = tk.Frame(self.agenda_view_frame, bg=self.colors["bg"])
        top.pack(fill=tk.X, pady=(0, 12))
        create_button(top, "◀", lambda: self._agenda_move(-1), self.colors).pack(side=tk.LEFT)
        tk.Label(top, text=title, font=("Microsoft YaHei UI", 18, "bold"), fg=self.colors["text"], bg=self.colors["bg"]).pack(side=tk.LEFT, padx=14)
        create_button(top, "▶", lambda: self._agenda_move(1), self.colors).pack(side=tk.LEFT)

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

        has_item = False
        for d in dates:
            ds = d.strftime("%Y-%m-%d")
            holiday = self.app.holidays.get(ds)
            note = self.app.notes.get(ds)
            if holiday or note:
                has_item = True
                row = tk.Frame(inner, bg=self.colors["card_soft"], highlightthickness=1, highlightbackground=self.colors["border"], cursor="hand2")
                row.pack(fill=tk.X, pady=6)
                tk.Label(row, text=self.app.format_date(d), font=("Microsoft YaHei UI", 11, "bold"), fg=self.colors["primary"], bg=self.colors["card_soft"]).pack(anchor="w", padx=12, pady=(10, 2))
                if holiday:
                    tk.Label(row, text=f"{holiday.get('emoji','')} {self.app.holiday_name(holiday)}", font=("Microsoft YaHei UI", 10, "bold"), fg=self.colors["warning"], bg=self.colors["card_soft"]).pack(anchor="w", padx=12, pady=2)
                if note:
                    tk.Label(row, text=f"◷ {note.get('time','--:--')}  {note.get('text','')}", font=("Microsoft YaHei UI", 10), fg=self.colors["text"], bg=self.colors["card_soft"], wraplength=720, justify=tk.LEFT).pack(anchor="w", padx=12, pady=(2, 10))
                row.bind("<Button-1>", lambda e, key=ds: self.open_date(key))
        if not has_item:
            show_empty(inner, self.app.t("calendar_no_agenda"), self.colors)

    def _week_dates(self):
        try:
            selected = datetime.strptime(self.selected_date, "%Y-%m-%d").date()
        except Exception:
            selected = date.today()
        start_day = selected - timedelta(days=selected.weekday())
        return [start_day + timedelta(days=i) for i in range(7)]

    def _note_time_to_minutes(self, note):
        if note.get("all_day"):
            return 9 * 60
        value = str(note.get("time") or "09:00")
        try:
            hh, mm = value.split(":")[:2]
            return max(0, min(23 * 60 + 59, int(hh) * 60 + int(mm)))
        except Exception:
            return 9 * 60

    def _show_week_timeline_view(self):
        for child in self.agenda_view_frame.winfo_children():
            child.destroy()
        self.agenda_view_frame.pack(fill=tk.BOTH, expand=True)

        dates = self._week_dates()
        title = f"{dates[0].strftime('%Y/%m/%d')} - {dates[-1].strftime('%Y/%m/%d')}"

        top = tk.Frame(self.agenda_view_frame, bg=self.colors["bg"])
        top.pack(fill=tk.X, pady=(0, 10))
        create_button(top, "◀", lambda: self._agenda_move(-7), self.colors).pack(side=tk.LEFT)
        tk.Label(top, text=title, font=("Microsoft YaHei UI", 18, "bold"), fg=self.colors["text"], bg=self.colors["bg"]).pack(side=tk.LEFT, padx=14)
        create_button(top, "▶", lambda: self._agenda_move(7), self.colors).pack(side=tk.LEFT)
        create_button(top, self.app.t("today_btn"), self.go_today, self.colors, filled=True).pack(side=tk.RIGHT)

        body = create_card(self.agenda_view_frame, self.colors, fill=tk.BOTH, expand=True)
        body.pack_propagate(False)
        grid_shell = tk.Frame(body, bg=self.colors["border"], highlightthickness=1, highlightbackground=self.colors["border"])
        grid_shell.pack(fill=tk.BOTH, expand=True, padx=12, pady=(12, 12))

        scroll_gutter = 18
        # v45: one fixed rail width for header, all-day row, and timeline canvas.
        # The previous rail could grow in header because of label requested width,
        # while the canvas stayed at 70px, causing visible misalignment.
        time_col_w = 108
        self._week_time_col_w = time_col_w
        weekdays = self.app.t("week_days")

        # v46: Header and all-day rows are drawn with canvas using the same
        # left rail and day-width math as the scrollable timeline. This avoids
        # Tk grid requested-width differences across DPI/font settings.
        header_wrap = tk.Frame(grid_shell, bg=self.colors["card"])
        header_wrap.pack(fill=tk.X)
        header_canvas = tk.Canvas(header_wrap, bg=self.colors["card"], height=82, highlightthickness=0, bd=0)
        header_canvas.pack(side=tk.LEFT, fill=tk.X, expand=True)
        tk.Frame(header_wrap, bg=self.colors["card"], width=scroll_gutter).pack(side=tk.RIGHT, fill=tk.Y)

        all_day_wrap = tk.Frame(grid_shell, bg=self.colors["card"])
        all_day_wrap.pack(fill=tk.X)
        all_day_canvas = tk.Canvas(all_day_wrap, bg=self.colors["card"], height=42, highlightthickness=0, bd=0)
        all_day_canvas.pack(side=tk.LEFT, fill=tk.X, expand=True)
        tk.Frame(all_day_wrap, bg=self.colors["card"], width=scroll_gutter).pack(side=tk.RIGHT, fill=tk.Y)

        def _week_geometry(canvas_width):
            left_w = getattr(self, "_week_time_col_w", 108)
            width = canvas_width if canvas_width and canvas_width > left_w + 160 else left_w + 7 * 96
            day_w = max((width - left_w) / 7, 64)
            return left_w, day_w, left_w + day_w * 7

        def draw_header(event=None):
            header_canvas.delete("all")
            width = event.width if event is not None else header_canvas.winfo_width()
            left_w, day_w, total_w = _week_geometry(width)
            header_canvas.configure(scrollregion=(0, 0, total_w, 82))
            header_canvas.create_rectangle(0, 0, left_w, 82, fill=self.colors["card"], outline=self.colors["border"])
            header_canvas.create_text(left_w / 2, 40, text=f"{dates[0].year}/{dates[0].month:02d}", fill=self.colors["text"], font=("Microsoft YaHei UI", 14, "bold"), anchor="center")
            for i, d in enumerate(dates):
                ds = d.strftime("%Y-%m-%d")
                x = left_w + i * day_w
                box_bg = self.colors["card_soft"] if i % 2 == 0 else self.colors["card"]
                is_selected = ds == self.selected_date
                is_today = d == date.today()
                fg = self.colors["primary"] if is_selected else (self.colors["danger"] if d.weekday() >= 5 else self.colors["text"])
                tag = f"week_header_{i}"
                header_canvas.create_rectangle(x, 0, x + day_w, 82, fill=box_bg, outline=self.colors["border"], tags=(tag,))
                # Move tiny header text slightly right for better optical alignment.
                cx = x + day_w / 2 + 3
                header_canvas.create_text(cx, 18, text=weekdays[i], fill=self.colors["text_light"], font=("Microsoft YaHei UI", 10, "bold"), anchor="center", tags=(tag,))
                header_canvas.create_text(cx, 42, text=str(d.day), fill=fg, font=("Microsoft YaHei UI", 19, "bold"), anchor="center", tags=(tag,))
                lunar = lunar_cell_text(ds, self.app.lang_code) if self.show_lunar.get() else ""
                header_canvas.create_text(cx, 64, text=lunar or " ", fill=self.colors["text_light"], font=("Microsoft YaHei UI", 9), anchor="center", tags=(tag,))
                indicator_color = self.colors["primary"] if is_selected else (self.colors["danger"] if is_today else box_bg)
                header_canvas.create_rectangle(x + max(day_w * 0.28, 24), 78, x + day_w - max(day_w * 0.28, 24), 82, fill=indicator_color, outline="", tags=(tag,))
                header_canvas.tag_bind(tag, "<Button-1>", lambda e, key=ds: self.open_date(key))
                header_canvas.tag_bind(tag, "<Double-Button-1>", lambda e, key=ds: (self.open_date(key), self.open_note_dialog(False)))

        def draw_all_day(event=None):
            all_day_canvas.delete("all")
            width = event.width if event is not None else all_day_canvas.winfo_width()
            left_w, day_w, total_w = _week_geometry(width)
            all_day_canvas.configure(scrollregion=(0, 0, total_w, 42))
            all_day_canvas.create_rectangle(0, 0, left_w, 42, fill=self.colors["card"], outline=self.colors["border"])
            all_day_canvas.create_text(left_w / 2, 21, text=self.app.t("calendar_all_day"), fill=self.colors["text_light"], font=("Microsoft YaHei UI", 9, "bold"), anchor="center", width=left_w - 12)
            for i, d in enumerate(dates):
                ds = d.strftime("%Y-%m-%d")
                x = left_w + i * day_w
                cell_bg = self.colors["card_soft"] if i % 2 == 0 else self.colors["card"]
                tag = f"week_allday_{i}"
                all_day_canvas.create_rectangle(x, 0, x + day_w, 42, fill=cell_bg, outline=self.colors["border"], tags=(tag,))
                text_parts = []
                holiday = self.app.holidays.get(ds)
                note = self.app.notes.get(ds)
                if holiday and self._holiday_visible(holiday):
                    text_parts.append(f"{holiday.get('emoji','')} {self.app.holiday_name(holiday)}")
                if note and note.get("all_day"):
                    text_parts.append(note.get("text", "").split("\n")[0])
                label = " · ".join(text_parts)
                all_day_canvas.create_text(x + 8, 21, text=label, fill=self.colors["danger"] if holiday else self.colors["text"], font=("Microsoft YaHei UI", 9, "bold"), anchor="w", width=max(day_w - 16, 48), tags=(tag,))
                all_day_canvas.tag_bind(tag, "<Button-1>", lambda e, key=ds: self.open_date(key))
                all_day_canvas.tag_bind(tag, "<Double-Button-1>", lambda e, key=ds: (self.open_date(key), self.open_note_dialog(False)))

        header_canvas.bind("<Configure>", draw_header)
        all_day_canvas.bind("<Configure>", draw_all_day)
        self.app.root.after_idle(draw_header)
        self.app.root.after_idle(draw_all_day)

        timeline_wrap = tk.Frame(grid_shell, bg=self.colors["card"])
        timeline_wrap.pack(fill=tk.BOTH, expand=True)
        canvas = tk.Canvas(timeline_wrap, bg=self.colors["card"], highlightthickness=0)
        scroll = tk.Scrollbar(timeline_wrap, orient="vertical", command=canvas.yview)
        canvas.configure(yscrollcommand=scroll.set)
        canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
        scroll.pack(side=tk.RIGHT, fill=tk.Y)

        def _open_week_note_at(ds, y_pos):
            """Open note dialog with date and clicked time prefilled."""
            self.open_date(ds)
            try:
                top_pad = getattr(self, "_week_timeline_top_pad", 8)
                hour_h = getattr(self, "_week_timeline_hour_h", 54)
                minutes = int(max(0, min(23 * 60 + 59, (y_pos - top_pad) / max(hour_h, 1) * 60)))
                # Snap to 15-minute blocks for cleaner scheduling.
                minutes = int(round(minutes / 15) * 15)
                minutes = max(0, min(23 * 60 + 45, minutes))
                self._week_prefill_time = f"{minutes // 60:02d}:{minutes % 60:02d}"
            except Exception:
                self._week_prefill_time = "09:00"
            self.open_note_dialog(False)

        def render_timeline(event=None):
            canvas.delete("all")
            hour_h = 54
            top_pad = 10
            left_w = getattr(self, "_week_time_col_w", 108)
            right_pad = 0
            raw_width = canvas.winfo_width()
            width = raw_width if raw_width and raw_width > left_w + 140 else left_w + 7 * 96
            timeline_w = max(width - left_w - right_pad, 7 * 64)
            day_w = timeline_w / 7
            start_hour, end_hour = 0, 23
            total_hours = end_hour - start_hour + 1
            height = top_pad + total_hours * hour_h + 24
            self._week_timeline_top_pad = top_pad
            self._week_timeline_hour_h = hour_h
            canvas.configure(scrollregion=(0, 0, max(width, left_w + day_w * 7 + right_pad), height))

            # Time label column and day backgrounds first, then grid, then events.
            canvas.create_rectangle(0, 0, left_w, height, fill=self.colors["card"], outline=self.colors["border"], tags="week_bg")
            for i, d in enumerate(dates):
                x = left_w + i * day_w
                bg = self.colors["card_soft"] if i % 2 == 0 else self.colors["card"]
                ds = d.strftime("%Y-%m-%d")
                tag = f"week_day_bg_{i}"
                canvas.create_rectangle(x, 0, x + day_w, height, fill=bg, outline=self.colors["border"], width=1, tags=("week_bg", tag))
                canvas.tag_bind(tag, "<Button-1>", lambda e, key=ds: self.open_date(key))
                canvas.tag_bind(tag, "<Double-Button-1>", lambda e, key=ds: _open_week_note_at(key, e.y))

            for h in range(start_hour, end_hour + 1):
                y = top_pad + (h - start_hour) * hour_h
                canvas.create_text(left_w - 12, y + 2, text=f"{h:02d}:00", anchor="ne", fill=self.colors["text_light"], font=("Microsoft YaHei UI", 9))
                canvas.create_line(left_w, y, left_w + day_w * 7, y, fill=self.colors["border"])
            # Bottom 24:00 boundary for a complete 00:00-24:00 timeline.
            bottom_y = top_pad + total_hours * hour_h
            canvas.create_line(left_w, bottom_y, left_w + day_w * 7, bottom_y, fill=self.colors["border"])
            canvas.create_text(left_w - 12, bottom_y - 12, text="24:00", anchor="ne", fill=self.colors["text_light"], font=("Microsoft YaHei UI", 9))
            for i in range(8):
                x = left_w + i * day_w
                canvas.create_line(x, top_pad, x, bottom_y, fill=self.colors["border"])

            for i, d in enumerate(dates):
                ds = d.strftime("%Y-%m-%d")
                note = self.app.notes.get(ds)
                if note and not note.get("all_day") and self.show_notes.get():
                    minutes = self._note_time_to_minutes(note)
                    y = top_pad + minutes / 60 * hour_h
                    x = left_w + i * day_w
                    item_h = 46
                    fill = self.colors["primary_light"]
                    outline = self.colors["primary"]
                    tag = f"week_note_{i}"
                    canvas.create_rectangle(x+8, y+4, x+day_w-8, y+item_h, fill=fill, outline=outline, width=1, tags=(tag,))
                    canvas.create_text(
                        x+14,
                        y+11,
                        text=f"{note.get('time','--:--')}  {note.get('text','').splitlines()[0]}",
                        anchor="nw",
                        fill=self.colors["text"],
                        font=("Microsoft YaHei UI", 9, "bold"),
                        width=max(day_w-28, 70),
                        tags=(tag,),
                    )
                    canvas.tag_bind(tag, "<Button-1>", lambda e, key=ds: self.open_date(key))
                    canvas.tag_bind(tag, "<Double-Button-1>", lambda e, key=ds: (self.open_date(key), self.open_note_dialog(True)))

            today = date.today()
            if dates[0] <= today <= dates[-1]:
                now = datetime.now()
                minutes = now.hour * 60 + now.minute
                i = (today - dates[0]).days
                x = left_w + i * day_w
                y = top_pad + minutes / 60 * hour_h
                if top_pad <= y <= bottom_y:
                    canvas.create_line(left_w, y, left_w + day_w * 7, y, fill=self.colors["danger"], width=2)
                    canvas.create_polygon(x+day_w/2-7, y-6, x+day_w/2+7, y, x+day_w/2-7, y+6, fill=self.colors["danger"])
                    canvas.create_text(left_w - 12, y, text=now.strftime("%H:%M"), anchor="e", fill=self.colors["danger"], font=("Microsoft YaHei UI", 9, "bold"))

            # Always show the full day from 00:00 at the top; no automatic jump to morning/current time.
            try:
                canvas.yview_moveto(0)
            except Exception:
                pass
        self._week_auto_scrolled = False
        canvas.bind("<Configure>", render_timeline)
        self._bind_canvas_scroll(canvas, timeline_wrap)
        self.update_notes_panel()

    def _agenda_move(self, delta_days):
        try:
            d = datetime.strptime(self.selected_date, "%Y-%m-%d").date()
        except Exception:
            d = date.today()
        self.selected_date = (d + timedelta(days=delta_days)).strftime("%Y-%m-%d")
        self._sync_month_offset_to_selected()
        self._show_agenda_view(self.view_mode)
        self.update_notes_panel()

    def _show_year_month_popup(self, year, month):
        items = [(ds, info) for ds, info in self._iter_year_holidays(year) if ds.startswith(f"{year}-{month:02d}")]
        win = tk.Toplevel(self.app.root)
        win.title(f"{self.app.t('month_names')[month - 1]} {year}")
        win.configure(bg=self.colors["bg"])
        try:
            w, h = 620, 560
            win.transient(self.app.root)
            center_window(win, self.app.root, w, h, min_width=520, min_height=440)
        except Exception:
            win.geometry("620x560")

        card = create_card(win, self.colors)
        card.pack(fill=tk.BOTH, expand=True, padx=18, pady=18)
        tk.Label(
            card,
            text=f"{self.app.t('month_names')[month - 1]} {year}",
            font=("Microsoft YaHei UI", 18, "bold"),
            fg=self.colors["text"],
            bg=self.colors["card"],
        ).pack(anchor="w", padx=18, pady=(16, 4))
        tk.Label(
            card,
            text=self.app.t("year_month_popup_hint"),
            font=("Microsoft YaHei UI", 9),
            fg=self.colors["text_secondary"],
            bg=self.colors["card"],
        ).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))
        win_id = canvas.create_window((0, 0), window=inner, anchor="nw")
        inner.bind("<Configure>", lambda e: canvas.configure(scrollregion=canvas.bbox("all")))
        canvas.bind("<Configure>", lambda e: canvas.itemconfig(win_id, width=max(e.width - 4, 420)))
        self._bind_canvas_scroll(canvas, inner)

        if not items:
            show_empty(inner, self.app.empty_text("holiday_list_empty"), self.colors)
            return

        for ds, info in items:
            htype = info.get("type", "")
            fg, bg = self.colors["primary"], self.colors["primary_light"]
            if htype == "china":
                fg, bg = self.colors["danger"], self.colors["danger_light"]
            elif htype == "indonesia":
                fg, bg = self.colors["warning"], self.colors["warning_light"]
            elif htype == "solar_term":
                fg, bg = self.colors["solar"], self.colors["solar_light"]
            row = tk.Frame(inner, bg=self.colors["card_soft"], highlightthickness=1, highlightbackground=self.colors["border"], cursor="hand2")
            row.pack(fill=tk.X, pady=6)
            tk.Label(row, text=ds, font=("Microsoft YaHei UI", 10, "bold"), fg=self.colors["primary"], bg=self.colors["card_soft"]).pack(anchor="w", padx=12, pady=(10, 2))
            tk.Label(row, text=f"{info.get('emoji', '')} {holiday_text(info.get('name'), self.app.lang_code)}", font=("Microsoft YaHei UI", 11, "bold"), fg=fg, bg=self.colors["card_soft"], wraplength=520, justify=tk.LEFT).pack(anchor="w", padx=12, pady=2)
            intro_key = self._holiday_default_intro_key(htype)
            intro = holiday_text(info.get("intro"), self.app.lang_code) or self.app.t(intro_key)
            tk.Label(row, text=intro, font=("Microsoft YaHei UI", 9), fg=self.colors["text_secondary"], bg=self.colors["card_soft"], wraplength=520, justify=tk.LEFT).pack(anchor="w", padx=12, pady=(2, 10))
            row.bind("<Button-1>", lambda e, date_key=ds, w=win: [w.destroy(), self.open_date(date_key)])
            for child in row.winfo_children():
                child.bind("<Button-1>", lambda e, date_key=ds, w=win: [w.destroy(), self.open_date(date_key)])

    def _ellipsis(self, text, limit):
        text = str(text or "").strip()
        if len(text) <= limit:
            return text
        return text[: max(0, limit - 3)].rstrip() + "..."

    def _year_item_name(self, info):
        name = holiday_text(info.get("name"), self.app.lang_code)
        emoji = str(info.get("emoji", "") or "").strip()
        if self.app.lang_code == "zh_CN":
            name = self._ellipsis(name, 10)
        elif self.app.lang_code == "id_ID":
            name = self._ellipsis(name, 18)
        else:
            name = self._ellipsis(name, 16)
        return f"{emoji} {name}".strip() if emoji else name

    def _set_year_view_style(self, style):
        style = "overview" if style == "overview" else "classic"
        if self.year_view_style == style and self.view_mode == "year":
            return
        self.year_view_style = style
        self.app.user_settings["calendar_year_view_style"] = style
        try:
            save_settings(self.app.user_settings)
        except Exception:
            pass
        if self.view_mode == "year":
            self._show_year_view()

    def _year_style_button(self, parent, text_key, style):
        active = self.year_view_style == style
        btn = tk.Button(
            parent,
            text=self.app.t(text_key),
            command=lambda: self._set_year_view_style(style),
            relief=tk.FLAT,
            bd=0,
            padx=12,
            pady=7,
            bg=self.colors["primary"] if active else self.colors["card_soft"],
            fg="white" if active else self.colors["text_secondary"],
            activebackground=self.colors["primary"],
            activeforeground="white",
            font=("Microsoft YaHei UI", 9, "bold"),
            cursor="hand2",
        )
        btn.pack(side=tk.LEFT, padx=(0, 6))
        return btn

    def _year_compact_summary_text(self):
        try:
            d = datetime.strptime(self.selected_date, "%Y-%m-%d").date()
            date_text = self.app.format_date(d)
        except Exception:
            date_text = self.selected_date
        parts = [date_text]
        try:
            lunar = lunar_full_text(self.selected_date, self.app.lang_code)
            if lunar:
                parts.append(lunar)
        except Exception:
            pass
        info = self.app.holidays.get(self.selected_date, {})
        if info and self._holiday_visible(info):
            name = holiday_text(info.get("name"), self.app.lang_code)
            if name:
                parts.append(name)
        note = self.app.notes.get(self.selected_date) if self.show_notes.get() else None
        if note:
            parts.append(str(note.get("text", ""))[:80])
        return "  ·  ".join([p for p in parts if p])

    def _update_year_compact_info(self):
        try:
            label = getattr(self, "year_compact_info_label", None)
            if label is not None:
                label.configure(text=self._year_compact_summary_text())
        except Exception:
            pass

    def _show_year_view(self):
        # v51: Year view now has two modes:
        # Classic = the previous v49 yearly cards with right note panel.
        # Overview = a clean 12-month annual overview inspired by native calendar apps.
        if self.year_view_style == "overview":
            self._show_year_overview_view()
        else:
            self._show_year_classic_view()

    def _show_year_overview_view(self):
        """Fast annual overview drawn mostly on one Canvas.

        This avoids creating hundreds of Label widgets for every day, which was
        the main reason the year page felt slow on lower-end machines.
        """
        self._year_scrollbar_visible = False
        for child in self.year_view_frame.winfo_children():
            child.destroy()
        self.year_view_frame.pack(fill=tk.BOTH, expand=True)
        year = self._selected_date_obj().year

        top = tk.Frame(self.year_view_frame, bg=self.colors["bg"])
        top.pack(fill=tk.X, pady=(0, 8))
        compact_header = bool(getattr(self.app, "runtime_compact_mode", False) or getattr(self, "_side_panel_collapsed", False))
        create_button(top, "◀", lambda: [self._shift_selected_year(-1), self._show_year_view(), self.update_notes_panel()], self.colors).pack(side=tk.LEFT)
        tk.Label(top, text=str(year), font=("Microsoft YaHei UI", 22 if compact_header else 26, "bold"), fg=self.colors["text"], bg=self.colors["bg"]).pack(side=tk.LEFT, padx=(10 if compact_header else 16, 12 if compact_header else 18))
        create_button(top, "▶", lambda: [self._shift_selected_year(1), self._show_year_view(), self.update_notes_panel()], self.colors).pack(side=tk.LEFT)
        style_wrap = tk.Frame(top, bg=self.colors["bg"])
        style_wrap.pack(side=tk.LEFT, padx=(10 if compact_header else 18, 0))
        self._year_style_button(style_wrap, "year_view_overview", "overview")
        self._year_style_button(style_wrap, "year_view_classic", "classic")
        create_button(top, self.app.t("today_btn"), self.go_today, self.colors, filled=True).pack(side=tk.RIGHT)
        if not compact_header:
            create_button(top, self.app.t("year_open_all_holidays"), lambda: self._set_year_view_style("classic"), self.colors).pack(side=tk.RIGHT, padx=(8, 0))

        legend = tk.Frame(self.year_view_frame, bg=self.colors["bg"])
        legend.pack(fill=tk.X, pady=(0, 8))
        for dot, label_key, color in [
            ("●", "holiday_type_china", self.colors["danger"]),
            ("●", "holiday_type_indonesia", self.colors["warning"]),
            ("●", "holiday_type_solar", self.colors["solar"]),
            ("●", "holiday_type_notes", self.colors["success"]),
        ]:
            tk.Label(legend, text=f"{dot} {self.app.t(label_key)}", fg=color, bg=self.colors["bg"], font=("Microsoft YaHei UI", 9, "bold")).pack(side=tk.LEFT, padx=(0, 14))
        tk.Label(legend, text=self.app.t("year_overview_click_hint"), fg=self.colors["text_secondary"], bg=self.colors["bg"], font=("Microsoft YaHei UI", 8 if compact_header else 9)).pack(side=tk.LEFT, padx=(8, 0))

        self.year_compact_info_label = None
        if getattr(self, "_side_panel_collapsed", False):
            info_card = tk.Frame(self.year_view_frame, bg=self.colors["primary_light"], highlightthickness=1, highlightbackground=self.colors["border"])
            info_card.pack(fill=tk.X, pady=(0, 8))
            self.year_compact_info_label = tk.Label(info_card, text=self._year_compact_summary_text(), bg=self.colors["primary_light"], fg=self.colors["primary"], font=("Microsoft YaHei UI", 9, "bold"), anchor="w", justify=tk.LEFT, wraplength=760)
            self.year_compact_info_label.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=12, pady=8)
            create_button(info_card, self.app.t("add_note_btn"), self.open_note_dialog, self.colors).pack(side=tk.RIGHT, padx=(0, 8), pady=6)

        wrap = tk.Frame(self.year_view_frame, bg=self.colors["bg"])
        wrap.pack(fill=tk.BOTH, expand=True)
        canvas = tk.Canvas(wrap, bg=self.colors["bg"], highlightthickness=0)
        scrollbar = tk.Scrollbar(wrap, orient="vertical", command=canvas.yview)
        canvas.configure(yscrollcommand=scrollbar.set)
        canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)

        months = self.app.t("month_names")
        weekdays = [w[:1] for w in self.app.t("week_days")]
        today_key = date.today().strftime("%Y-%m-%d")

        def item_color(ds):
            info = self.app.holidays.get(ds)
            note = self.app.notes.get(ds) if self.show_notes.get() else None
            if ds == today_key:
                return self.colors["primary_light"], self.colors["primary"], "today"
            if info and self._holiday_visible(info):
                htype = info.get("type", "")
                if htype == "china":
                    return self.colors["danger_light"], self.colors["danger"], "holiday"
                if htype == "indonesia":
                    return self.colors["warning_light"], self.colors["warning"], "holiday"
                if htype == "solar_term":
                    return self.colors["solar_light"], self.colors["solar"], "solar"
            if note:
                return self.colors["success_light"], self.colors["success"], "note"
            return self.colors["card"], self.colors["text"], ""

        def _date_from_canvas_event(event):
            try:
                items = event.widget.find_withtag("current")
                if not items:
                    return ""
                for tag in event.widget.gettags(items[0]):
                    if tag.startswith("date:"):
                        return tag.split(":", 1)[1]
            except Exception:
                return ""
            return ""

        def _on_year_date_click(event, open_note=False):
            ds = _date_from_canvas_event(event)
            if ds:
                self._select_year_date(ds, open_note)

        def draw(event=None):
            canvas.delete("all")
            low_power = getattr(self.app, "is_low_power_mode", lambda: False)()
            # Fit to the actual canvas width instead of forcing a wide virtual
            # canvas. On 125%/150% DPI laptops the old min-width caused clipped
            # months because there was no horizontal scrollbar.
            width = max(canvas.winfo_width() - 8, 320)
            # v2026.06.20.06: use the real available viewport, not only the
            # Canvas height reported by the first Configure event.  On some
            # Windows DPI/window combinations the Canvas can briefly report a
            # small early height, then keep the year cards short while the
            # parent frame has already grown, leaving a visible blank footer.
            # We choose the largest stable runtime height from the Canvas,
            # wrapper, and year-view frame after subtracting visible header
            # rows, then redraw again after layout settles below.
            # v2026.06.20.08: do not call update_idletasks() inside every Canvas
            # repaint.  It can recursively flush Tk layout work and make the app
            # feel stuck when Configure events arrive in bursts.  Debounced redraws
            # below are enough for DPI/window settling.
            try:
                header_h = sum(max(0, child.winfo_height()) for child in self.year_view_frame.winfo_children() if child is not wrap)
                frame_h = max(0, self.year_view_frame.winfo_height() - header_h - 10)
            except Exception:
                frame_h = 0
            try:
                # v2026.06.20.07: include root-relative remaining height.
                # On some Windows scaling combinations, Canvas.winfo_height()
                # can lag behind the visible space below it; using the root and
                # canvas Y positions prevents the year grid from sitting at the
                # top with a large blank footer.
                root_remaining_h = int(self.app.root.winfo_height() - max(0, canvas.winfo_rooty() - self.app.root.winfo_rooty()) - 12)
            except Exception:
                root_remaining_h = 0
            height_candidates = [canvas.winfo_height() - 8, wrap.winfo_height() - 8, frame_h, root_remaining_h]
            height = max([h for h in height_candidates if h and h > 0] + [420])
            if width >= 1220:
                cols = 4
            elif width >= 860:
                cols = 3
            elif width >= 620:
                cols = 2
            else:
                cols = 1
            # v2026.06.20.04: do not shrink the year view just because the
            # usable height is small, or because low-power mode is enabled.
            # The previous adaptive pass used height < 640 / low_power as a
            # compact trigger, which made 12-month cards look too tiny on many
            # 1366x768 and 125%/150% DPI laptops.  Width decides the column
            # count; height should be handled by vertical scrolling instead.
            compact_year = width < 620
            gap = 10 if compact_year else 14
            card_w = max(190, (width - gap * (cols + 1)) / cols)
            rows = math.ceil(12 / cols)
            # v2026.06.20.05: fill available vertical space when there is
            # enough room.  v2026.06.20.04 fixed over-shrinking, but on 1080p
            # displays the 4x3 year grid could still sit at the top and leave a
            # large empty band under December.  The year page now grows month
            # cards to the viewport first, and scrolls only when the viewport is
            # genuinely shorter than the readable card height.
            min_card_h = 218 if compact_year else (262 if cols >= 3 else 248)
            max_card_h = 390 if compact_year else 560
            ratio = 0.78 if compact_year else 0.84
            width_based_h = card_w * ratio
            visible_h = max(height, canvas.winfo_height() - 2, wrap.winfo_height() - 2, root_remaining_h, 420)
            viewport_fill_h = (visible_h - gap * (rows + 1)) / rows if rows else min_card_h
            fill_based_h = max(min_card_h, viewport_fill_h)
            card_h = max(min_card_h, min(max_card_h, max(width_based_h, fill_based_h)))
            total_h = gap + rows * (card_h + gap)
            # If a stable layout pass still leaves visible padding, stretch the
            # month cards to the viewport.  If the viewport is shorter than the
            # readable minimum, total_h becomes larger and vertical scrolling is
            # used instead of shrinking the calendar.
            blank_footer = visible_h - total_h
            if blank_footer > max(12, gap) and rows > 0:
                card_h = max(min_card_h, min(max_card_h, viewport_fill_h))
                total_h = gap + rows * (card_h + gap)
                blank_footer = visible_h - total_h
            # When there is still a few pixels of rounding remainder, distribute
            # it as outer/row spacing so the final December row touches the
            # visual bottom naturally instead of leaving a separate empty band.
            draw_gap = gap
            if 0 < blank_footer <= max(80, gap * 6) and rows > 0 and total_h <= visible_h:
                draw_gap = max(gap, (visible_h - rows * card_h) / (rows + 1))
                total_h = draw_gap + rows * (card_h + draw_gap)
            canvas.configure(scrollregion=(0, 0, width, max(total_h, visible_h)))
            if total_h > canvas.winfo_height() + 2:
                if not scrollbar.winfo_manager():
                    scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
            else:
                if scrollbar.winfo_manager():
                    scrollbar.pack_forget()

            for m in range(1, 13):
                idx = m - 1
                r, c = divmod(idx, cols)
                x0 = gap + c * (card_w + gap)
                y0 = draw_gap + r * (card_h + draw_gap)
                x1 = x0 + card_w
                y1 = y0 + card_h
                month_tag = f"month:{year}-{m:02d}"
                canvas.create_rectangle(x0, y0, x1, y1, fill=self.colors["card"], outline=self.colors["border"], tags=(month_tag,))
                title_font = 15 if compact_year else (18 if card_h >= 285 else 17)
                month_font = 9 if compact_year else (11 if card_h >= 285 else 10)
                canvas.create_text(x0 + 12, y0 + 17, text=f"{m:02d}", anchor="w", fill=self.colors["text"], font=("Microsoft YaHei UI", title_font, "bold"), tags=(month_tag,))
                canvas.create_text(x0 + (50 if compact_year else 58), y0 + 17, text=months[idx], anchor="w", fill=self.colors["text_secondary"], font=("Microsoft YaHei UI", month_font, "bold"), tags=(month_tag,))

                grid_x = x0 + (10 if compact_year else 14)
                grid_y = y0 + (42 if compact_year else 48)
                grid_w = card_w - (20 if compact_year else 28)
                grid_h = card_h - (54 if compact_year else 62)
                cell_w = grid_w / 7
                cell_h = grid_h / 7

                weekday_font = 8 if compact_year else (10 if card_h >= 285 else 9)
                day_font = 10 if compact_year else (12 if card_h >= 270 else 11)
                for cc, wd in enumerate(weekdays):
                    canvas.create_text(grid_x + cc * cell_w + cell_w / 2, grid_y + cell_h * 0.35, text=wd, anchor="center", fill=self.colors["text_light"], font=("Microsoft YaHei UI", weekday_font, "bold"))

                month_days = pycal.monthcalendar(year, m)
                for rr, week in enumerate(month_days[:6]):
                    for cc, day in enumerate(week):
                        if not day:
                            continue
                        ds = f"{year}-{m:02d}-{day:02d}"
                        bg, fg, kind = item_color(ds)
                        is_selected = ds == self.selected_date
                        cx0 = grid_x + cc * cell_w + 2
                        cy0 = grid_y + (rr + 1) * cell_h + 2
                        cx1 = grid_x + (cc + 1) * cell_w - 2
                        cy1 = grid_y + (rr + 2) * cell_h - 2
                        date_tag = f"date:{ds}"
                        if is_selected:
                            bg, fg, kind = self.colors["primary"], "white", "selected"
                        # Always draw a hit area. This makes clicking a day feel
                        # reliable even when the date has no holiday background.
                        tags = (date_tag, "year_date")
                        canvas.create_rectangle(cx0, cy0, cx1, cy1, fill=bg, outline=self.colors["primary_dark"] if is_selected else "", width=2 if is_selected else 1, tags=tags)
                        canvas.create_text((cx0 + cx1) / 2, (cy0 + cy1) / 2, text=str(day), fill=fg, font=("Microsoft YaHei UI", day_font, "bold" if kind else "normal"), tags=tags)
                canvas.tag_bind(month_tag, "<Double-Button-1>", lambda e, yy=year, mm=m: self._show_year_month_popup(yy, mm))
            # Bind common date behavior once per repaint instead of binding 365+
            # individual tags.  This makes year-view redraws much smoother.
            canvas.tag_bind("year_date", "<Button-1>", lambda e: _on_year_date_click(e, False))
            canvas.tag_bind("year_date", "<Double-Button-1>", lambda e: _on_year_date_click(e, True))
            canvas.tag_bind("year_date", "<Enter>", lambda e: canvas.configure(cursor="hand2"))
            canvas.tag_bind("year_date", "<Leave>", lambda e: canvas.configure(cursor=""))

        draw_job = {"id": None}

        def schedule_draw(event=None):
            if draw_job["id"]:
                try:
                    self.app.root.after_cancel(draw_job["id"])
                except Exception:
                    pass
            delay = 160 if getattr(self.app, "is_low_power_mode", lambda: False)() else 90
            draw_job["id"] = self.app.root.after(delay, lambda: [draw_job.update({"id": None}), draw(event)])

        self._year_overview_canvas = canvas
        self._year_overview_redraw = draw
        canvas.bind("<Configure>", schedule_draw)
        self.app.root.after_idle(draw)
        # v2026.06.20.08: one delayed stabilization repaint is enough.  The old
        # five delayed repaints kept redrawing the annual Canvas after the user had
        # already started working, which made the whole calendar feel laggy.
        try:
            self.app.root.after(220 if getattr(self.app, "is_low_power_mode", lambda: False)() else 140, draw)
        except Exception:
            pass
        self.update_notes_panel()

    def _show_year_classic_view(self):
        self._year_scrollbar_visible = False
        for child in self.year_view_frame.winfo_children():
            child.destroy()
        self.year_view_frame.pack(fill=tk.BOTH, expand=True)
        year = self._selected_date_obj().year
        top = tk.Frame(self.year_view_frame, bg=self.colors["bg"])
        top.pack(fill=tk.X, pady=(0, 10))
        create_button(top, "◀", lambda: [self._shift_selected_year(-1), self._show_year_view(), self.update_notes_panel()], self.colors).pack(side=tk.LEFT)
        tk.Label(top, text=str(year), font=("Microsoft YaHei UI", 22, "bold"), fg=self.colors["text"], bg=self.colors["bg"]).pack(side=tk.LEFT, padx=16)
        create_button(top, "▶", lambda: [self._shift_selected_year(1), self._show_year_view(), self.update_notes_panel()], self.colors).pack(side=tk.LEFT)
        style_wrap = tk.Frame(top, bg=self.colors["bg"])
        style_wrap.pack(side=tk.LEFT, padx=(18, 0))
        self._year_style_button(style_wrap, "year_view_overview", "overview")
        self._year_style_button(style_wrap, "year_view_classic", "classic")
        create_button(top, self.app.t("today_btn"), self.go_today, self.colors, filled=True).pack(side=tk.RIGHT)

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

        def sync_year_scroll(event=None):
            canvas.configure(scrollregion=canvas.bbox("all"))
            try:
                canvas.itemconfig(year_window, width=max(canvas.winfo_width() - 4, 640))
                needed = inner.winfo_reqheight() > canvas.winfo_height() + 2
                if needed and not self._year_scrollbar_visible:
                    scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
                    self._year_scrollbar_visible = True
                elif not needed and self._year_scrollbar_visible:
                    scrollbar.pack_forget()
                    self._year_scrollbar_visible = False
            except Exception:
                pass

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

        grid = tk.Frame(inner, bg=self.colors["bg"])
        grid.pack(fill=tk.BOTH, expand=True)
        months = self.app.t("month_names")
        cols = 3 if getattr(self.app, "screen_height", 0) >= 840 else 4
        visible_limit = 8 if cols == 3 else 7
        rows = (12 + cols - 1) // cols
        for i in range(12):
            r, c = divmod(i, cols)
            grid.grid_rowconfigure(r, weight=1, uniform="year_rows")
            grid.grid_columnconfigure(c, weight=1, uniform="year_cols")
            card = create_card(grid, self.colors)
            card.grid(row=r, column=c, sticky="nsew", padx=6, pady=6)
            card.configure(height=214 if cols == 3 else 194)
            card.grid_propagate(False)
            header = tk.Frame(card, bg=self.colors["card"])
            header.pack(fill=tk.X, padx=12, pady=(12, 8))
            tk.Label(header, text=months[i], font=("Microsoft YaHei UI", 12, "bold"), fg=self.colors["primary"], bg=self.colors["card"]).pack(side=tk.LEFT)
            box = tk.Frame(card, bg=self.colors["card"])
            box.pack(fill=tk.BOTH, expand=True, padx=10, pady=(0, 10))
            prefix = f"{year}-{i + 1:02d}"
            holidays = [(ds, info) for ds, info in self._iter_year_holidays(year) if ds.startswith(prefix)]
            if not holidays:
                tk.Label(box, text="—", fg=self.colors["text_light"], bg=self.colors["card"], font=("Microsoft YaHei UI", 10)).pack(expand=True, pady=12)
                card.bind("<Double-Button-1>", lambda e, yy=year, mm=i + 1: self._show_year_month_popup(yy, mm))
                continue
            for ds, info in holidays[:visible_limit]:
                htype = info.get("type", "")
                fg, bg = self.colors["primary"], self.colors["primary_light"]
                if htype == "china":
                    fg, bg = self.colors["danger"], self.colors["danger_light"]
                elif htype == "indonesia":
                    fg, bg = self.colors["warning"], self.colors["warning_light"]
                elif htype == "solar_term":
                    fg, bg = self.colors["solar"], self.colors["solar_light"]
                elif htype == "note":
                    fg, bg = self.colors["success"], self.colors["success_light"]

                item = tk.Frame(box, bg=bg, cursor="hand2")
                item.pack(fill=tk.X, pady=2)
                date_badge = tk.Label(
                    item,
                    text=ds[8:10],
                    width=3,
                    anchor="center",
                    font=("Microsoft YaHei UI", 8, "bold"),
                    fg=fg,
                    bg=bg,
                )
                date_badge.pack(side=tk.LEFT, padx=(8, 4), pady=4)
                name_lbl = tk.Label(
                    item,
                    text=self._year_item_name(info),
                    anchor="w",
                    cursor="hand2",
                    font=("Microsoft YaHei UI", 8),
                    fg=fg,
                    bg=bg,
                    justify=tk.LEFT,
                )
                name_lbl.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(0, 8), pady=4)
                for w in (item, date_badge, name_lbl):
                    w.bind("<Button-1>", lambda e, date_key=ds: self.open_date(date_key))
            extra = len(holidays) - visible_limit
            if extra > 0:
                more = tk.Label(
                    box,
                    text=self.app.t("holiday_list_more").format(count=extra),
                    fg=self.colors["primary"],
                    bg=self.colors["card"],
                    font=("Microsoft YaHei UI", 9, "bold"),
                    cursor="hand2",
                )
                more.pack(anchor="w", padx=2, pady=(4, 0))
                more.bind("<Button-1>", lambda e, yy=year, mm=i + 1: self._show_year_month_popup(yy, mm))
            # Double-click a month card to show the complete holiday/date list.
            card.bind("<Double-Button-1>", lambda e, yy=year, mm=i + 1: self._show_year_month_popup(yy, mm))
        sync_year_scroll()
        self.update_notes_panel()

    def _input_focus_in(self, event=None):
        if self.note_input.get() in self.app.all_values("placeholder"):
            self.note_input.delete(0, tk.END)
            self.note_input.config(fg=self.colors["text"])

    def _input_focus_out(self, event=None):
        if not self.note_input.get().strip():
            self.note_input.insert(0, self.app.t("placeholder"))
            self.note_input.config(fg=self.colors["text_light"])

    def quick_add(self):
        text = self.note_input.get().strip()
        if not text or text in self.app.all_values("placeholder"):
            return
        now_text = datetime.now().strftime("%H:%M")
        existing = self.app.notes.get(self.selected_date, {})
        old_text = existing.get("text", "").strip()
        if old_text:
            merged_text = f"{old_text}\n{now_text}  {text}"
        else:
            merged_text = text
        note_data = dict(existing)
        note_data.update({
            "text": merged_text,
            "time": existing.get("time") or now_text,
            "image": existing.get("image"),
            "category": existing.get("category", "work"),
            "all_day": bool(existing.get("all_day", False)),
            "location": existing.get("location", ""),
            "updated_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
        })
        self.app.notes[self.selected_date] = note_data
        save_notes(self.app.notes)
        self.note_input.delete(0, tk.END)
        self.note_input.insert(0, self.app.t("placeholder"))
        self.note_input.config(fg=self.colors["text_light"])
        self.cell_cache.clear()
        self._refresh_calendar()
        self.update_notes_panel()
        self.app.refresh_home()
        self.app.set_status("status_saved")

    def _holiday_type_key(self, htype):
        if htype == "china":
            return "holiday_type_china"
        if htype == "indonesia":
            return "holiday_type_indonesia"
        if htype == "solar_term":
            return "holiday_type_solar"
        if htype == "note":
            return "holiday_type_notes"
        return "holiday_type_china"

    def _holiday_default_intro_key(self, htype):
        if htype == "china":
            return "holiday_intro_china"
        if htype == "indonesia":
            return "holiday_intro_indonesia"
        if htype == "solar_term":
            return "holiday_intro_solar"
        if htype == "note":
            return "holiday_intro_note"
        return "holiday_intro_china"

    def update_notes_panel(self):
        for child in self.notes_inner.winfo_children():
            child.destroy()
        self.preview_images = []

        if self.view_mode == "year":
            self._show_or_hide_add_wrap(True)
            if getattr(self, "year_view_style", "overview") == "overview":
                self._render_selected_date_panel()
            else:
                self._show_or_hide_add_wrap(False)
                year = self._selected_date_obj().year
                self._render_year_side_panel(year)
            self._update_notes_scrollbar()
            return

        self._render_selected_date_panel()
        self._update_notes_scrollbar()
        return

    def _render_selected_date_panel(self):
        self._show_or_hide_add_wrap(True)
        self.notes_title.config(text=self.app.t("notes_section"))
        d = datetime.strptime(self.selected_date, "%Y-%m-%d").date()
        header = f"{self.app.format_date(d)} · {self.app.t('weekday_names')[d.weekday()]}"
        if self.show_lunar.get():
            lunar_text = lunar_full_text(self.selected_date, self.app.lang_code)
            if lunar_text:
                header += f"\n{lunar_text}"
        holiday = self.app.holidays.get(self.selected_date, {})
        htype = holiday.get("type", "")
        show_holiday = self._holiday_visible(holiday)
        bar_bg, bar_fg = self.colors["primary_light"], self.colors["primary"]
        if show_holiday:
            header += f"\n{holiday.get('emoji', '')} {holiday_text(holiday.get('name'), self.app.lang_code)}"
            if htype == "china":
                bar_bg, bar_fg = self.colors["danger_light"], self.colors["danger"]
            elif htype == "indonesia":
                bar_bg, bar_fg = self.colors["warning_light"], self.colors["warning"]
            elif htype == "solar_term":
                bar_bg, bar_fg = self.colors["solar_light"], self.colors["solar"]
        self.date_bar.config(bg=bar_bg)
        self.date_label.config(text=header, bg=bar_bg, fg=bar_fg)

        if show_holiday:
            self._render_holiday_row(self.notes_inner, self.selected_date, holiday)
        elif self.show_lunar.get():
            lunar_text = lunar_full_text(self.selected_date, self.app.lang_code)
            if lunar_text:
                card = self._note_card()
                tk.Label(card, text=lunar_text, font=("Microsoft YaHei UI", 11, "bold"), fg=self.colors["text_secondary"], bg=self.colors["card_soft"], wraplength=max(getattr(self, "side_panel_width", 310) - 34, 240), justify=tk.LEFT).pack(anchor="w", padx=12, pady=10)

        note = self.app.notes.get(self.selected_date) if self.show_notes.get() else None
        if self.app.search_keyword and note and not self.app.matches_search(self.selected_date, note.get("time"), note.get("text")):
            note = None
        if note:
            card = self._note_card()
            top = tk.Frame(card, bg=self.colors["card_soft"])
            top.pack(fill=tk.X, padx=12, pady=(10, 6))
            tk.Label(top, text=f"◷ {note.get('time', '--:--')}", font=("Microsoft YaHei UI", 10, "bold"), fg=self.colors["text_secondary"], bg=self.colors["card_soft"]).pack(side=tk.LEFT)
            action = tk.Frame(top, bg=self.colors["card_soft"])
            action.pack(side=tk.RIGHT)
            tk.Button(action, text="✎", command=lambda: self.open_note_dialog(True), bg=self.colors["primary"], fg="white", relief=tk.FLAT, bd=0, width=3, cursor="hand2").pack(side=tk.LEFT, padx=2)
            tk.Button(action, text="+1", command=self.duplicate_note_to_tomorrow, bg=self.colors["primary_light"], fg=self.colors["primary"], relief=tk.FLAT, bd=0, width=3, cursor="hand2", font=("Microsoft YaHei UI", 9, "bold")).pack(side=tk.LEFT, padx=2)
            tk.Button(action, text="+7", command=self.duplicate_note_to_next_week, bg=self.colors["success_light"], fg=self.colors["success"], relief=tk.FLAT, bd=0, width=3, cursor="hand2", font=("Microsoft YaHei UI", 9, "bold")).pack(side=tk.LEFT, padx=2)
            tk.Button(action, text="×", command=self.delete_note, bg=self.colors["danger"], fg="white", relief=tk.FLAT, bd=0, width=3, cursor="hand2", font=("Microsoft YaHei UI", 10, "bold")).pack(side=tk.LEFT, padx=2)
            tk.Label(card, text=note.get("text", ""), font=("Microsoft YaHei UI", 11), fg=self.colors["text"], bg=self.colors["card_soft"], wraplength=max(getattr(self, "side_panel_width", 310) - 34, 240), justify=tk.LEFT).pack(anchor="w", padx=12, pady=(0, 8))
            if note.get("image"):
                self._render_image(card, note["image"])
        elif not show_holiday:
            show_empty(self.notes_inner, self.app.empty_text("empty_notes_panel"), self.colors)


    def _note_card(self):
        card = tk.Frame(self.notes_inner, bg=self.colors["card_soft"], highlightthickness=1, highlightbackground=self.colors["border"])
        card.pack(fill=tk.X, pady=6)
        return card

    def _render_image(self, parent, encoded):
        try:
            img = Image.open(BytesIO(base64.b64decode(encoded)))
            img.thumbnail((280, 180))
            photo = ImageTk.PhotoImage(img)
            self.preview_images.append(photo)
            tk.Label(parent, text=self.app.t("note_image"), font=("Microsoft YaHei UI", 9, "bold"), fg=self.colors["text_secondary"], bg=self.colors["card_soft"]).pack(anchor="w", padx=12)
            tk.Label(parent, image=photo, bg=self.colors["card_soft"]).pack(anchor="w", padx=12, pady=(8, 12))
        except Exception:
            pass

    def _duplicate_note_by_days(self, days):
        """Copy selected note by N days without destroying the original."""
        current = self.app.notes.get(self.selected_date)
        if not current:
            messagebox.showwarning(self.app.t("warning_title"), self.app.t("empty_notes_panel"))
            return
        try:
            target_date = datetime.strptime(self.selected_date, "%Y-%m-%d").date() + timedelta(days=days)
        except Exception:
            target_date = date.today() + timedelta(days=days)
        target_key = target_date.strftime("%Y-%m-%d")
        if target_key in self.app.notes:
            if not messagebox.askyesno(self.app.t("confirm_title"), self.app.t("note_replace_confirm")):
                return
        copied = dict(current)
        copied["updated_at"] = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
        self.app.notes[target_key] = copied
        save_notes(self.app.notes)
        self.selected_date = target_key
        self._sync_month_offset_to_selected()
        self.cell_cache.clear()
        self._refresh_calendar()
        self.update_notes_panel()
        self.app.refresh_home()
        self.app.set_status("status_saved")

    def duplicate_note_to_tomorrow(self):
        self._duplicate_note_by_days(1)

    def duplicate_note_to_next_week(self):
        self._duplicate_note_by_days(7)

    def delete_note(self):
        if self.selected_date in self.app.notes and messagebox.askyesno(self.app.t("confirm_title"), self.app.t("confirm_delete")):
            del self.app.notes[self.selected_date]
            save_notes(self.app.notes)
            self.cell_cache.clear()
            self._refresh_calendar()
            self.update_notes_panel()
            self.app.refresh_home()
            self.app.set_status("status_saved")

    def open_note_dialog(self, edit=False):
        current = self.app.notes.get(self.selected_date, {}) if edit else {}
        dlg = tk.Toplevel(self.app.root)
        dlg.title(self.app.t("schedule_edit_title") if edit else self.app.t("schedule_new_title"))
        dlg.configure(bg=self.colors["card"])
        dlg.transient(self.app.root)
        dlg.grab_set()
        dlg.resizable(True, True)
        self._center_dialog(dlg, 660, 720)
        dlg.minsize(560, 620)

        head = tk.Frame(dlg, bg=self.colors["card"])
        head.pack(fill=tk.X, padx=22, pady=(18, 8))
        tk.Label(head, text=self.app.t("schedule_edit_title") if edit else self.app.t("schedule_new_title"), font=("Microsoft YaHei UI", 17, "bold"), fg=self.colors["text"], bg=self.colors["card"]).pack(side=tk.LEFT)
        tk.Label(head, text=self.app.t("schedule_dialog_hint"), font=("Microsoft YaHei UI", 9), fg=self.colors["text_secondary"], bg=self.colors["card"]).pack(side=tk.RIGHT)

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

        if self.view_mode == "week":
            week_row = tk.Frame(body, bg=self.colors["card"])
            week_row.pack(fill=tk.X, pady=(0, 12))
            tk.Label(week_row, text=self.app.t("schedule_select_day"), font=("Microsoft YaHei UI", 10, "bold"), fg=self.colors["text_secondary"], bg=self.colors["card"]).pack(anchor="w", pady=(0, 6))
            day_btns = tk.Frame(week_row, bg=self.colors["card"])
            day_btns.pack(fill=tk.X)
            date_var = tk.StringVar(value=self.selected_date)
            def choose_day(ds):
                date_var.set(ds)
                for child in day_btns.winfo_children():
                    child.destroy()
                render_day_buttons()
            def render_day_buttons():
                weekdays = self.app.t("week_days")
                for i, d in enumerate(self._week_dates()):
                    ds = d.strftime("%Y-%m-%d")
                    selected = ds == date_var.get()
                    text = f"{weekdays[i]}\n{d.month}/{d.day}"
                    tk.Button(
                        day_btns,
                        text=text,
                        command=lambda key=ds: choose_day(key),
                        relief=tk.FLAT,
                        bd=0,
                        bg=self.colors["primary"] if selected else self.colors["card_soft"],
                        fg="white" if selected else self.colors["text"],
                        activebackground=self.colors["primary_light"],
                        activeforeground=self.colors["primary"],
                        font=("Microsoft YaHei UI", 9, "bold"),
                        cursor="hand2",
                        padx=8,
                        pady=7,
                    ).pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(0, 6))
            render_day_buttons()
        else:
            date_var = tk.StringVar(value=self.selected_date)

        title_var = tk.StringVar(value=current.get("text", "").split("\n")[0] if current.get("text") else "")
        title_label = tk.Label(body, text=self.app.t("schedule_subject_label"), bg=self.colors["card"], fg=self.colors["text_secondary"], font=("Microsoft YaHei UI", 10, "bold"))
        title_label.pack(anchor="w", pady=(0, 6))
        title_entry = tk.Entry(body, textvariable=title_var, font=("Microsoft YaHei UI", 12), relief=tk.FLAT, bd=0, bg=self.colors["bg"], fg=self.colors["text"])
        title_entry.pack(fill=tk.X, ipady=12, pady=(0, 14))

        category = tk.StringVar(value=current.get("category", "work"))
        cat_row = tk.Frame(body, bg=self.colors["card"])
        cat_row.pack(fill=tk.X, pady=(0, 14))
        for label_key, value, color in [
            ("schedule_category_personal", "personal", self.colors["primary"]),
            ("schedule_category_work", "work", self.colors["danger"]),
            ("schedule_category_birthday", "birthday", self.colors["warning"]),
        ]:
            tk.Radiobutton(
                cat_row,
                text=self.app.t(label_key),
                value=value,
                variable=category,
                indicatoron=False,
                bg=self.colors["card_soft"],
                fg=self.colors["text"],
                activebackground=self.colors["primary_light"],
                selectcolor=self.colors["primary_light"],
                relief=tk.FLAT,
                bd=0,
                padx=16,
                pady=8,
                font=("Microsoft YaHei UI", 10, "bold"),
            ).pack(side=tk.LEFT, padx=(0, 8))

        all_day_var = tk.BooleanVar(value=bool(current.get("all_day")))
        date_time_row = tk.Frame(body, bg=self.colors["card"])
        date_time_row.pack(fill=tk.X, pady=(0, 12))
        left_dt = tk.Frame(date_time_row, bg=self.colors["card"])
        left_dt.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(0, 10))
        tk.Label(left_dt, text=self.app.t("schedule_date"), bg=self.colors["card"], fg=self.colors["text_secondary"], font=("Microsoft YaHei UI", 10, "bold")).pack(anchor="w", pady=(0, 6))
        tk.Entry(left_dt, textvariable=date_var, relief=tk.FLAT, bd=0, bg=self.colors["bg"], fg=self.colors["text"], font=("Microsoft YaHei UI", 10), justify=tk.CENTER).pack(fill=tk.X, ipady=8)
        right_dt = tk.Frame(date_time_row, bg=self.colors["card"])
        right_dt.pack(side=tk.LEFT)
        tk.Label(right_dt, text=self.app.t("schedule_time"), bg=self.colors["card"], fg=self.colors["text_secondary"], font=("Microsoft YaHei UI", 10, "bold")).pack(anchor="w", pady=(0, 6))
        time_text = current.get("time") or getattr(self, "_week_prefill_time", None) or datetime.now().strftime("%H:%M")
        if hasattr(self, "_week_prefill_time"):
            try:
                delattr(self, "_week_prefill_time")
            except Exception:
                pass
        hour_var = tk.StringVar(value=time_text.split(":")[0] if ":" in time_text else "09")
        minute_var = tk.StringVar(value=time_text.split(":")[1] if ":" in time_text else "00")
        time_box = tk.Frame(right_dt, bg=self.colors["card"])
        time_box.pack(fill=tk.X)
        hour_spin = tk.Spinbox(time_box, from_=0, to=23, width=4, format="%02.0f", textvariable=hour_var, relief=tk.FLAT, bg=self.colors["bg"], font=("Microsoft YaHei UI", 10))
        hour_spin.pack(side=tk.LEFT, ipady=5)
        tk.Label(time_box, text=":", bg=self.colors["card"], fg=self.colors["text_secondary"], font=("Microsoft YaHei UI", 12, "bold")).pack(side=tk.LEFT, padx=4)
        minute_spin = tk.Spinbox(time_box, from_=0, to=59, width=4, format="%02.0f", textvariable=minute_var, relief=tk.FLAT, bg=self.colors["bg"], font=("Microsoft YaHei UI", 10))
        minute_spin.pack(side=tk.LEFT, ipady=5)
        quick_time_row = tk.Frame(right_dt, bg=self.colors["card"])
        quick_time_row.pack(fill=tk.X, pady=(6, 0))
        def set_quick_time(value):
            if value == "now":
                value = datetime.now().strftime("%H:%M")
            hh, mm = value.split(":")
            hour_var.set(hh)
            minute_var.set(mm)
        for label, value in [
            (self.app.t("note_time_now"), "now"),
            ("09:00", "09:00"),
            ("14:00", "14:00"),
            ("18:00", "18:00"),
        ]:
            tk.Button(
                quick_time_row,
                text=label,
                command=lambda v=value: set_quick_time(v),
                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", 8, "bold"),
                padx=6,
                pady=3,
                cursor="hand2",
            ).pack(side=tk.LEFT, padx=(0, 4))

        option_row = tk.Frame(body, bg=self.colors["card"])
        option_row.pack(fill=tk.X, pady=(0, 12))
        tk.Checkbutton(option_row, text=self.app.t("schedule_all_day"), variable=all_day_var, bg=self.colors["card"], activebackground=self.colors["card"], selectcolor=self.colors["card"], font=("Microsoft YaHei UI", 10, "bold")).pack(side=tk.LEFT)

        loc_var = tk.StringVar(value=current.get("location", ""))
        tk.Label(body, text=self.app.t("schedule_location"), bg=self.colors["card"], fg=self.colors["text_secondary"], font=("Microsoft YaHei UI", 10, "bold")).pack(anchor="w", pady=(0, 6))
        tk.Entry(body, textvariable=loc_var, relief=tk.FLAT, bd=0, bg=self.colors["bg"], fg=self.colors["text"], font=("Microsoft YaHei UI", 10)).pack(fill=tk.X, ipady=10, pady=(0, 12))

        detail_text = "\n".join(current.get("text", "").split("\n")[1:]) if current.get("text") else ""
        if current.get("location") and detail_text.endswith(current.get("location")):
            detail_text = detail_text.replace(current.get("location"), "").strip(" ·\n")
        tk.Label(body, text=self.app.t("schedule_note"), bg=self.colors["card"], fg=self.colors["text_secondary"], font=("Microsoft YaHei UI", 10, "bold")).pack(anchor="w", pady=(0, 6))
        detail_box = tk.Frame(body, bg=self.colors["bg"], highlightthickness=1, highlightbackground=self.colors["border"])
        detail_box.pack(fill=tk.BOTH, expand=True, pady=(0, 12))
        detail = tk.Text(detail_box, height=5, relief=tk.FLAT, bd=0, bg=self.colors["bg"], fg=self.colors["text"], font=("Microsoft YaHei UI", 10), wrap=tk.WORD)
        detail.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=10, pady=8)
        scroll = tk.Scrollbar(detail_box, command=detail.yview)
        scroll.pack(side=tk.RIGHT, fill=tk.Y)
        detail.configure(yscrollcommand=scroll.set)
        if detail_text:
            detail.insert("1.0", detail_text)

        image_state = {"data": current.get("image")}
        image_label = tk.Label(body, text=self.app.t("note_image_attached") if image_state["data"] else self.app.t("note_image_none"), bg=self.colors["card"], fg=self.colors["text_secondary"], font=("Microsoft YaHei UI", 9))
        image_row = tk.Frame(body, bg=self.colors["card"])
        image_row.pack(fill=tk.X, pady=(0, 10))
        def attach_image():
            path = filedialog.askopenfilename(filetypes=[("Images", "*.png *.jpg *.jpeg *.gif *.bmp")])
            if not path:
                return
            try:
                with open(path, "rb") as f:
                    image_state["data"] = base64.b64encode(f.read()).decode("ascii")
                image_label.config(text=self.app.t("note_image_attached"))
            except Exception:
                messagebox.showwarning(self.app.t("warning_title"), self.app.t("note_image_failed"))
        def remove_image():
            image_state["data"] = None
            image_label.config(text=self.app.t("note_image_none"))
        create_button(image_row, self.app.t("note_attach_image"), attach_image, self.colors).pack(side=tk.LEFT)
        create_button(image_row, self.app.t("note_remove_image"), remove_image, self.colors).pack(side=tk.LEFT, padx=8)
        image_label.pack(side=tk.LEFT, padx=8)

        footer = tk.Frame(dlg, bg=self.colors["card"])
        footer.pack(fill=tk.X, padx=22, pady=(0, 20))

        def save_note():
            text = title_var.get().strip()
            if not text:
                messagebox.showwarning(self.app.t("warning_title"), self.app.t("enter_content"))
                return
            try:
                target_date = datetime.strptime(date_var.get().strip(), "%Y-%m-%d").date()
            except Exception:
                messagebox.showwarning(self.app.t("warning_title"), self.app.t("goto_hint"))
                return
            try:
                hh = max(0, min(23, int(hour_var.get())))
                mm = max(0, min(59, int(minute_var.get())))
            except Exception:
                messagebox.showwarning(self.app.t("warning_title"), self.app.t("schedule_time_invalid"))
                return
            detail_value = detail.get("1.0", tk.END).strip()
            location = loc_var.get().strip()
            lines = [text]
            if detail_value:
                lines.append(detail_value)
            note_data = {
                "text": "\n".join(lines),
                "time": f"{hh:02d}:{mm:02d}",
                "image": image_state["data"],
                "category": category.get(),
                "all_day": bool(all_day_var.get()),
                "location": location,
                "updated_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
            }
            self.selected_date = target_date.strftime("%Y-%m-%d")
            self.app.notes[self.selected_date] = note_data
            save_notes(self.app.notes)
            self._sync_month_offset_to_selected()
            dlg.destroy()
            self.cell_cache.clear()
            self._refresh_calendar()
            self.update_notes_panel()
            self.app.refresh_home()
            self.app.set_status("status_saved")

        create_button(footer, self.app.t("schedule_cancel"), dlg.destroy, self.colors).pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(0, 10))
        create_button(footer, self.app.t("schedule_done"), save_note, self.colors, filled=True).pack(side=tk.LEFT, fill=tk.X, expand=True)
        dlg.bind("<Control-Return>", lambda e: save_note())
        title_entry.focus_set()

