"""
renderer.py — All draw_* methods for the Game scene.
"""

from __future__ import annotations

import re
from pathlib import Path
from typing import Optional

import pygame

from helpers.game_data import (
    BUILDING_TYPES,
    CATEGORY_LABELS,
    MAX_LEVEL,
    PlacedBuilding,
)
from .constants import (
    TILE_SIZE,
    TOOLBAR_H, PICKER_H, ANIM_FPS,
    GRASS_A, GRASS_B, GRID_LINE_COL,
    PH_PURPLE, PH_BLACK,
    WIRE_COLORS, WIRE_TIERS, WIRE_W, SHADOW_OFF,
    HUD_BG, PANEL_BG, CARD_BG, CARD_LOCK,
    CAT_COLORS, PICKER_PAD,
    CARD_W, CARD_GAP, CARD_STRIDE, ARROW_W,
    ZOOM_TEXT_HIDE,
    _S,
)

_TEX_BASE = Path(__file__).parent.parent.parent / "assets" / "textures" / "other"


def _fmt_mw(mw: float) -> str:
    abs_v = abs(mw)
    if abs_v < 1_000:
        return f"{mw:.1f} MW"
    elif abs_v < 1_000_000:
        return f"{mw / 1_000:.2f} GW"
    else:
        return f"{mw / 1_000_000:.2f} TW"


def _fmt_xp(xp: float) -> str:
    if xp < 1_000:
        return f"{xp:.0f}"
    elif xp < 1_000_000:
        return f"{xp / 1_000:.1f}k"
    return f"{xp / 1_000_000:.2f}M"


def _scale_fit(raw: pygame.Surface, tw: int, th: int) -> pygame.Surface:
    rw, rh = raw.get_size()
    if rw == 0 or rh == 0:
        return raw
    scale = min(tw / rw, th / rh)
    nw = max(1, round(rw * scale))
    nh = max(1, round(rh * scale))
    scaled = pygame.transform.scale(raw, (nw, nh))
    if nw == tw and nh == th:
        return scaled
    result = pygame.Surface((tw, th), pygame.SRCALPHA)
    result.blit(scaled, ((tw - nw) // 2, (th - nh) // 2))
    return result


class GameRendererMixin:
    """All rendering methods mixed into Game."""

    # ------------------------------------------------------------------ textures

    def _load_tex_frames(self, type_id: str) -> list[pygame.Surface]:
        td = BUILDING_TYPES.get(type_id)
        if td is None or not td.texture_name:
            return []
        base_path = _TEX_BASE / td.texture_name
        if not base_path.exists():
            return []
        pw = td.size[0] * TILE_SIZE
        ph = td.size[1] * TILE_SIZE
        stem   = base_path.stem
        suffix = base_path.suffix
        parent = base_path.parent
        m = re.match(r"^(.*?)-(\d+)$", stem)
        if m:
            base_stem   = m.group(1)
            frame_paths: list[Path] = []
            i = 1
            while True:
                candidate = parent / f"{base_stem}-{i}{suffix}"
                if candidate.exists():
                    frame_paths.append(candidate)
                    i += 1
                else:
                    break
            if len(frame_paths) < 2:
                frame_paths = [base_path]
        else:
            frame_paths = [base_path]
        result: list[pygame.Surface] = []
        for p in frame_paths:
            try:
                raw    = pygame.image.load(str(p)).convert_alpha()
                scaled = _scale_fit(raw, pw, ph)
                result.append(scaled)
            except Exception:
                pass
        return result

    def _get_tex(self, type_id: str) -> Optional[pygame.Surface]:
        if type_id not in self._tex_cache:  # type: ignore[attr-defined]
            self._tex_cache[type_id] = self._load_tex_frames(type_id)  # type: ignore[attr-defined]
        frames = self._tex_cache[type_id]  # type: ignore[attr-defined]
        if not frames:
            return None
        idx = int(self._anim_timer * ANIM_FPS) % len(frames)  # type: ignore[attr-defined]
        return frames[idx]

    def _get_tex_scaled(self, type_id: str, w: int, h: int) -> Optional[pygame.Surface]:
        key = (type_id, w, h)
        if key in self._scaled_cache:  # type: ignore[attr-defined]
            return self._scaled_cache[key]  # type: ignore[attr-defined]
        td = BUILDING_TYPES.get(type_id)
        if td is None or not td.texture_name:
            return None
        base_path = _TEX_BASE / td.texture_name
        if not base_path.exists():
            return None
        try:
            raw    = pygame.image.load(str(base_path)).convert_alpha()
            result = _scale_fit(raw, w, h)
            self._scaled_cache[key] = result  # type: ignore[attr-defined]
            return result
        except Exception:
            return None

    def _placeholder(self, w: int, h: int) -> pygame.Surface:
        key = (w, h)
        if key not in self._ph_cache:  # type: ignore[attr-defined]
            surf = pygame.Surface((w, h))
            hw, hh = max(1, w // 2), max(1, h // 2)
            surf.fill(PH_PURPLE, (0,  0,  hw,     hh))
            surf.fill(PH_BLACK,  (hw, 0,  w - hw, hh))
            surf.fill(PH_BLACK,  (0,  hh, hw,     h - hh))
            surf.fill(PH_PURPLE, (hw, hh, w - hw, h - hh))
            pygame.draw.rect(surf, (60, 10, 100), surf.get_rect(), 1)
            self._ph_cache[key] = surf  # type: ignore[attr-defined]
        return self._ph_cache[key]  # type: ignore[attr-defined]

    # ------------------------------------------------------------------ building helpers

    def _building_rect(self, b: PlacedBuilding) -> pygame.Rect:
        sw2, sh2 = b.size
        bx, by   = self._w2s(b.gx, b.gy)  # type: ignore[attr-defined]
        ts       = self._tile_px()  # type: ignore[attr-defined]
        return pygame.Rect(int(bx), int(by), sw2 * ts, sh2 * ts)

    def _building_center(self, b: PlacedBuilding) -> tuple[int, int]:
        r = self._building_rect(b)
        return r.centerx, r.centery

    # ------------------------------------------------------------------ draw

    def draw(self, screen: pygame.Surface) -> None:
        screen.fill(self._theme.BG)  # type: ignore[attr-defined]

        gah       = self._game_h()  # type: ignore[attr-defined]
        game_rect = pygame.Rect(0, 0, self._sw, gah)  # type: ignore[attr-defined]

        screen.set_clip(game_rect)
        self._draw_grid(screen, gah)
        self._draw_pollution_zones(screen)
        self._draw_wires(screen)
        self._draw_buildings(screen)
        self._draw_overlay(screen, gah)
        screen.set_clip(None)

        self._draw_hud(screen)
        self._draw_picker(screen)
        self._draw_toolbar(screen)

        if self._hov_building is not None:  # type: ignore[attr-defined]
            self._draw_info_panel(screen, gah)

        if self._alert_timer > 0:  # type: ignore[attr-defined]
            self._draw_alert(screen)

        if self._levelup_timer > 0:  # type: ignore[attr-defined]
            self._draw_levelup(screen)

        if self._state == _S.PAUSED:  # type: ignore[attr-defined]
            self._draw_pause(screen)

        if self._debug_open:  # type: ignore[attr-defined]
            self._draw_debug(screen)

    # ---- Grid ----

    def _draw_grid(self, screen: pygame.Surface, gah: int) -> None:
        gs = self._gs  # type: ignore[attr-defined]
        ts = self._tile_px()  # type: ignore[attr-defined]
        cx = int(self._cam_x)  # type: ignore[attr-defined]
        cy = int(self._cam_y)  # type: ignore[attr-defined]
        x0 = max(0, cx // ts)
        y0 = max(0, cy // ts)
        x1 = min(gs.grid_w, (cx + self._sw) // ts + 2)  # type: ignore[attr-defined]
        y1 = min(gs.grid_h, (cy + gah)       // ts + 2)

        for gy in range(y0, y1):
            for gx in range(x0, x1):
                sx  = gx * ts - cx
                sy  = gy * ts - cy
                col = GRASS_A if (gx + gy) % 2 == 0 else GRASS_B
                screen.fill(col, (sx, sy, ts, ts))

        lc = GRID_LINE_COL
        for gx in range(x0, x1 + 1):
            sx = gx * ts - cx
            if 0 <= sx <= self._sw:  # type: ignore[attr-defined]
                pygame.draw.line(screen, lc, (sx, 0), (sx, gah))
        for gy in range(y0, y1 + 1):
            sy = gy * ts - cy
            if 0 <= sy <= gah:
                pygame.draw.line(screen, lc, (0, sy), (self._sw, sy))  # type: ignore[attr-defined]

        # Grid border
        bx0 = -cx
        by0 = -cy
        bx1 = gs.grid_w * ts - cx
        by1 = gs.grid_h * ts - cy
        pygame.draw.rect(screen, (60, 120, 50), (bx0, by0, bx1 - bx0, by1 - by0), 2)

    # ---- Wires ----

    def _draw_wires(self, screen: pygame.Surface) -> None:
        anim_t = self._anim_timer  # type: ignore[attr-defined]
        th = self._theme  # type: ignore[attr-defined]
        for w in self._gs.wires:  # type: ignore[attr-defined]
            src = self._gs.get_building_by_id(w.from_id)  # type: ignore[attr-defined]
            dst = self._gs.get_building_by_id(w.to_id)  # type: ignore[attr-defined]
            if src is None or dst is None:
                continue
            ax, ay = self._building_center(src)
            bx, by = self._building_center(dst)

            tier    = w.layer
            col     = WIRE_COLORS[tier % len(WIRE_COLORS)]
            is_hov  = w is self._hov_wire  # type: ignore[attr-defined]

            # Look up wire flow
            flow_key1 = w.from_id + "_" + w.to_id
            flow_key2 = w.to_id + "_" + w.from_id
            flow_mw   = self._wire_flows.get(flow_key1,  # type: ignore[attr-defined]
                         self._wire_flows.get(flow_key2, 0.0))  # type: ignore[attr-defined]

            # Check if wire is overloaded (flow > capacity)
            tier_cap  = WIRE_TIERS[tier][1] if tier < len(WIRE_TIERS) else WIRE_TIERS[-1][1]
            is_wire_overloaded = flow_mw > tier_cap

            if is_wire_overloaded:
                # Flash red/orange for overloaded wires
                flash  = (int(anim_t * 6) % 2 == 0)
                ol_col = (255, 80, 30) if flash else (200, 50, 20)
                pygame.draw.line(screen, ol_col, (ax, ay), (bx, by), WIRE_W + 4)
            elif is_hov:
                pygame.draw.line(screen, (255, 240, 60), (ax, ay), (bx, by), WIRE_W + 4)

            wire_col = (220, 50, 50) if is_wire_overloaded else col
            pygame.draw.line(screen, wire_col, (ax, ay), (bx, by),
                             WIRE_W + (2 if is_hov else 0))
            pygame.draw.circle(screen, wire_col, (ax, ay), 5)
            pygame.draw.circle(screen, wire_col, (bx, by), 5)

            # Draw flow label at wire midpoint (hide when zoomed out)
            if flow_mw > 0 and self._zoom >= ZOOM_TEXT_HIDE:  # type: ignore[attr-defined]
                mx2 = (ax + bx) // 2
                my2 = (ay + by) // 2
                flow_str = _fmt_mw(flow_mw)
                flow_col = (230, 80, 50) if is_wire_overloaded else (200, 215, 230)
                fs = th.font(15).render(flow_str, True, flow_col)
                fbg = pygame.Surface((fs.get_width() + 6, fs.get_height() + 4), pygame.SRCALPHA)
                fbg.fill((5, 10, 20, 200))
                screen.blit(fbg, (mx2 - fs.get_width() // 2 - 3,
                                   my2 - fs.get_height() // 2 - 2))
                screen.blit(fs, (mx2 - fs.get_width() // 2,
                                  my2 - fs.get_height() // 2))

    # ---- Pollution zones ----

    def _draw_pollution_zones(self, screen: pygame.Surface) -> None:
        """Draw semi-transparent pollution circles under polluting buildings."""
        ts = self._tile_px()  # type: ignore[attr-defined]
        for b in self._gs.buildings:  # type: ignore[attr-defined]
            td = b.typedef
            if td.pollution_radius <= 0:
                continue
            sx, sy = self._w2s(b.gx, b.gy)  # type: ignore[attr-defined]
            # Center on the building's pixel center
            cx = int(sx + td.size[0] * ts * 0.5)
            cy = int(sy + td.size[1] * ts * 0.5)
            r_px = int(td.pollution_radius * ts)
            if r_px < 2:
                continue
            diam = r_px * 2 + 4
            circ = pygame.Surface((diam, diam), pygame.SRCALPHA)
            pygame.draw.circle(circ, (180, 60, 20, 45), (r_px + 2, r_px + 2), r_px)
            pygame.draw.circle(circ, (220, 80, 30, 160), (r_px + 2, r_px + 2), r_px, 2)
            screen.blit(circ, (cx - r_px - 2, cy - r_px - 2))

    # ---- Buildings ----

    def _draw_buildings(self, screen: pygame.Surface) -> None:
        th   = self._theme  # type: ignore[attr-defined]
        clip = screen.get_clip()
        # Depth sort for isometric-readiness (back to front by gx+gy)
        for b in sorted(self._gs.buildings, key=lambda b: b.gx + b.gy):  # type: ignore[attr-defined]
            r = self._building_rect(b)
            if not clip.colliderect(r):
                continue
            pw, ph = r.width, r.height

            # Drop shadow
            sh_surf = pygame.Surface((pw, ph), pygame.SRCALPHA)
            sh_surf.fill((0, 0, 0, 70))
            screen.blit(sh_surf, (r.x + SHADOW_OFF, r.y + SHADOW_OFF))

            # Body texture (or placeholder), scaled to current zoom size
            tex = self._get_tex(b.type_id)
            if tex:
                if tex.get_size() != (pw, ph):
                    tex = pygame.transform.scale(tex, (pw, ph))
                screen.blit(tex, r.topleft)
            else:
                screen.blit(self._placeholder(pw, ph), r.topleft)

            # Category border colour
            cat  = b.typedef.category
            ccol: tuple[int, int, int] = CAT_COLORS.get(cat) or th.BORDER
            td   = b.typedef

            # Check generator overload
            is_gen_overloaded = (
                td.power_mw > 0
                and b.build_id in self._overloaded  # type: ignore[attr-defined]
            )

            # Highlight / hover
            if (b is self._hov_building  # type: ignore[attr-defined]
                    and self._state in (_S.EXPLORE, _S.BULLDOZE, _S.WIRE_FROM)):  # type: ignore[attr-defined]
                hl  = pygame.Surface((pw, ph), pygame.SRCALPHA)
                col = (255, 50, 50, 60) if self._state == _S.BULLDOZE else (255, 255, 255, 30)  # type: ignore[attr-defined]
                hl.fill(col)
                screen.blit(hl, r.topleft)
                pygame.draw.rect(screen, th.ACCENT, r, 2)
            elif is_gen_overloaded:
                # Amber/yellow border for overloaded generators
                pygame.draw.rect(screen, (240, 200, 30), r, 3)
            else:
                pygame.draw.rect(screen, ccol, r, 1)

            # Powered status dot
            is_powered = b.build_id in self._powered  # type: ignore[attr-defined]
            dot_col = (0, 200, 80) if is_powered else (200, 50, 50)
            pygame.draw.circle(screen, (0, 0, 0, 180), (r.right - 8, r.top + 8), 6)
            pygame.draw.circle(screen, dot_col, (r.right - 8, r.top + 8), 4)

            # Generator overload badge ("!" in top-left)
            if is_gen_overloaded:
                overload_mw = self._overloaded[b.build_id]  # type: ignore[attr-defined]
                # Yellow circle with "!"
                badge_cx = r.x + 10
                badge_cy = r.y + 10
                pygame.draw.circle(screen, (0, 0, 0, 180), (badge_cx, badge_cy), 9)
                pygame.draw.circle(screen, (240, 200, 30), (badge_cx, badge_cy), 8)
                exclam = th.font(15).render("!", True, (30, 20, 0))
                screen.blit(exclam, (badge_cx - exclam.get_width() // 2,
                                      badge_cy - exclam.get_height() // 2))
                # Overload amount below badge
                ol_str = f"-{_fmt_mw(overload_mw)}"
                ol_s = th.font(15).render(ol_str, True, (230, 60, 60))
                ol_x = r.x + 2
                ol_y = r.y + 22
                ol_bg = pygame.Surface((ol_s.get_width() + 4, ol_s.get_height()), pygame.SRCALPHA)
                ol_bg.fill((0, 0, 0, 160))
                screen.blit(ol_bg, (ol_x - 2, ol_y))
                screen.blit(ol_s,  (ol_x, ol_y))

            # Skip text labels when zoomed out far
            if self._zoom >= ZOOM_TEXT_HIDE:  # type: ignore[attr-defined]
                # Name tag
                name_s = th.font(16).render(td.name, True, (220, 225, 235))
                nx = r.centerx - name_s.get_width() // 2
                ny = r.bottom - name_s.get_height() - 3
                if ny > r.top + 2:
                    bg = pygame.Surface((name_s.get_width() + 4, name_s.get_height()), pygame.SRCALPHA)
                    bg.fill((0, 0, 0, 140))
                    screen.blit(bg,     (nx - 2, ny))
                    screen.blit(name_s, (nx,     ny))

                # XP rate badge
                if td.xp_rate > 0 and is_powered:
                    xp_s = th.font(16).render(f"+{td.xp_rate:.0f} XP/s", True, (126, 211, 33))
                    xp_bg = pygame.Surface((xp_s.get_width() + 4, xp_s.get_height()), pygame.SRCALPHA)
                    xp_bg.fill((0, 0, 0, 140))
                    screen.blit(xp_bg, (r.x,     r.y + 2))
                    screen.blit(xp_s,  (r.x + 2, r.y + 2))

    # ---- Placement / wire overlay ----

    def _draw_overlay(self, screen: pygame.Surface, gah: int) -> None:
        mx, my = pygame.mouse.get_pos()

        if self._state == _S.PLACE and self._sel_type and my < gah:  # type: ignore[attr-defined]
            td = BUILDING_TYPES.get(self._sel_type)  # type: ignore[attr-defined]
            if td:
                ts = self._tile_px()  # type: ignore[attr-defined]
                gx, gy = self._s2g(mx, my)  # type: ignore[attr-defined]
                sw2, sh2 = td.size
                px_w = sw2 * ts
                px_h = sh2 * ts
                sx, sy = self._w2s(gx, gy)  # type: ignore[attr-defined]
                sx, sy = int(sx), int(sy)
                can  = (self._gs.in_bounds(gx, gy, (sw2, sh2))  # type: ignore[attr-defined]
                        and not self._gs.is_occupied(gx, gy, (sw2, sh2)))  # type: ignore[attr-defined]

                # Pollution preview circle (drawn first, under building)
                if td.pollution_radius > 0:
                    r_px = int(td.pollution_radius * ts)
                    cx_p = sx + px_w // 2
                    cy_p = sy + px_h // 2
                    diam = r_px * 2 + 4
                    circ = pygame.Surface((diam, diam), pygame.SRCALPHA)
                    pygame.draw.circle(circ, (180, 60, 20, 60), (r_px + 2, r_px + 2), r_px)
                    pygame.draw.circle(circ, (220, 80, 30, 200), (r_px + 2, r_px + 2), r_px, 2)
                    screen.blit(circ, (cx_p - r_px - 2, cy_p - r_px - 2))

                tex = self._get_tex(self._sel_type)  # type: ignore[attr-defined]
                if tex:
                    prev = pygame.transform.scale(tex, (px_w, px_h)) if tex.get_size() != (px_w, px_h) else tex.copy()
                else:
                    prev = self._placeholder(px_w, px_h).copy()
                prev.set_alpha(180)
                screen.blit(prev, (sx, sy))
                pygame.draw.rect(screen,
                                 (0, 200, 70) if can else (220, 50, 50),
                                 (sx, sy, px_w, px_h), 2)

        elif self._state == _S.WIRE_TO and self._wire_src and my < gah:  # type: ignore[attr-defined]
            src = self._gs.get_building_by_id(self._wire_src)  # type: ignore[attr-defined]
            if src:
                ax, ay = self._building_center(src)
                tier   = self._wire_layer  # type: ignore[attr-defined]
                col    = WIRE_COLORS[tier % len(WIRE_COLORS)]
                b      = self._gs.get_building_at(*self._s2g(mx, my))  # type: ignore[attr-defined]
                if b and b.build_id != self._wire_src:  # type: ignore[attr-defined]
                    bx, by = self._building_center(b)
                    pygame.draw.line(screen, col, (ax, ay), (bx, by), WIRE_W)
                    pygame.draw.circle(screen, col, (bx, by), 5)
                else:
                    pygame.draw.line(screen, col, (ax, ay), (mx, my), WIRE_W)
                pygame.draw.circle(screen, col, (ax, ay), 5)

    # ---- HUD ----

    def _draw_hud(self, screen: pygame.Surface) -> None:
        th      = self._theme  # type: ignore[attr-defined]
        gs      = self._gs  # type: ignore[attr-defined]
        lv      = gs.level()
        prog    = gs.xp_progress()
        xps     = gs.xp_per_second(self._powered)  # type: ignore[attr-defined]
        gen     = gs.total_generation_mw()
        con     = gs.total_consumption_mw(self._powered)  # type: ignore[attr-defined]
        net     = gs.net_power_mw(self._powered)  # type: ignore[attr-defined]
        happy   = gs.happiness(self._powered)  # type: ignore[attr-defined]
        net_inc = gs.net_income_per_minute(self._powered, self._polluted)  # type: ignore[attr-defined]

        # Capacity display (powered consumption vs total generation)
        cap_used = gs.total_consumption_mw(self._powered)  # type: ignore[attr-defined]
        cap_total = gen

        hud_w = 320
        hud_h = 250
        hud   = pygame.Surface((hud_w, hud_h), pygame.SRCALPHA)
        hud.fill(HUD_BG)
        pygame.draw.rect(hud, th.ACCENT, hud.get_rect(), 1)

        y2 = 8

        # Level header (24→28)
        lv_lbl = th.font(17).render("LEVEL", True, th.TEXT_DIM)
        lv_num = th.font(28).render(str(lv), True, th.ACCENT)
        hud.blit(lv_lbl, (10, y2 + 6))
        hud.blit(lv_num, (10 + lv_lbl.get_width() + 6, y2))

        if lv < MAX_LEVEL:
            nxt_xp  = gs.xp_for_next_level()
            cur_xp  = gs.xp_for_current_level()
            xp_lbl  = th.font(17).render(
                f"{_fmt_xp(gs.total_xp - cur_xp)} / {_fmt_xp(nxt_xp - cur_xp)} XP",
                True, th.TEXT_DIM,
            )
            hud.blit(xp_lbl, (hud_w - xp_lbl.get_width() - 8, y2 + 8))
        else:
            mx_lbl = th.font(17).render("MAX LEVEL", True, th.ACCENT)
            hud.blit(mx_lbl, (hud_w - mx_lbl.get_width() - 8, y2 + 8))

        y2 += 36

        # XP progress bar
        bar_x, bar_w, bar_h = 10, hud_w - 20, 10
        pygame.draw.rect(hud, (20, 35, 55), (bar_x, y2, bar_w, bar_h), border_radius=4)
        fill_w = int(bar_w * prog)
        if fill_w > 0:
            bar_col = (
                int(232 * (1 - prog) + 126 * prog),
                int(137 * (1 - prog) + 211 * prog),
                int(12  * (1 - prog) + 33  * prog),
            )
            pygame.draw.rect(hud, bar_col, (bar_x, y2, fill_w, bar_h), border_radius=4)
        pygame.draw.rect(hud, th.BORDER, (bar_x, y2, bar_w, bar_h), 1, border_radius=4)

        y2 += bar_h + 4

        if xps > 0:
            xp_rate_s = th.font(17).render(f"+{xps:.1f} XP/s", True, (126, 211, 33))
            hud.blit(xp_rate_s, (10, y2))
        y2 += 18

        pygame.draw.line(hud, th.BORDER, (10, y2), (hud_w - 10, y2))
        y2 += 6

        # Money row (14→17)
        money_s = th.font(19).render(f"€{gs.money:,.0f}", True,
                                     (80, 220, 120) if gs.money >= 0 else (230, 60, 60))
        hud.blit(money_s, (10, y2))

        # Happiness
        hap_pct = int(happy * 100)
        hap_col = (
            int(230 * (1 - happy) + 40 * happy),
            int(60  * (1 - happy) + 220 * happy),
            int(60  * (1 - happy) + 80 * happy),
        )
        hap_lbl = th.font(17).render(f"Happiness: {hap_pct}%", True, hap_col)
        hud.blit(hap_lbl, (hud_w - hap_lbl.get_width() - 8, y2 + 2))
        y2 += 22

        # Income/expense (11→14)
        inc_sign = "+" if net_inc >= 0 else "-"
        inc_col  = (80, 220, 120) if net_inc >= 0 else (230, 100, 60)
        inc_s = th.font(16).render(
            f"{inc_sign}€{abs(net_inc):,.1f}/min", True, inc_col
        )
        hud.blit(inc_s, (10, y2))
        y2 += 18

        pygame.draw.line(hud, th.BORDER, (10, y2), (hud_w - 10, y2))
        y2 += 5

        net_col = (0, 200, 80) if net >= 0 else (230, 60, 60)
        net_pfx = "▲" if net >= 0 else "▼"

        rows = [
            ("Output",      _fmt_mw(gen),        th.ACCENT),
            ("Consumption", _fmt_mw(con),         (79, 195, 247)),
            (f"{net_pfx} Net", _fmt_mw(abs(net)), net_col),
            ("Capacity",    f"{_fmt_mw(cap_used)} / {_fmt_mw(cap_total)}", (160, 180, 210)),
        ]
        fk = th.font(17)
        fv = th.font(17)
        for key, val, vcol in rows:
            ks = fk.render(key + ":", True, th.TEXT_DIM)
            vs = fv.render(val, True, vcol)
            hud.blit(ks, (10, y2))
            hud.blit(vs, (hud_w - vs.get_width() - 8, y2))
            y2 += ks.get_height() + 3

        screen.blit(hud, (10, 10))

        # Time-paused indicator
        if self._time_paused:  # type: ignore[attr-defined]
            from helpers.i18n import t
            tp_s  = th.font(19).render(t("time_paused"), True, (255, 230, 60))
            tp_bg = pygame.Surface((tp_s.get_width() + 14, tp_s.get_height() + 8), pygame.SRCALPHA)
            tp_bg.fill((30, 25, 5, 220))
            pygame.draw.rect(tp_bg, (220, 200, 50, 200), tp_bg.get_rect(), 1)
            tp_x = (self._sw - tp_bg.get_width()) // 2  # type: ignore[attr-defined]
            tp_y = 10
            screen.blit(tp_bg, (tp_x, tp_y))
            screen.blit(tp_s,  (tp_x + 7, tp_y + 4))

    # ---- Picker panel ----

    def _draw_picker(self, screen: pygame.Surface) -> None:
        if self._picker_t < 0.01:  # type: ignore[attr-defined]
            return
        th  = self._theme  # type: ignore[attr-defined]
        py  = self._picker_y()  # type: ignore[attr-defined]
        ph  = int(self._picker_t * PICKER_H)  # type: ignore[attr-defined]
        tab = self._active_tab  # type: ignore[attr-defined]

        surf = pygame.Surface((self._sw, ph), pygame.SRCALPHA)  # type: ignore[attr-defined]
        surf.fill(PANEL_BG)

        line_col: tuple[int, int, int] = (CAT_COLORS.get(tab) or th.ACCENT) if tab else th.ACCENT
        pygame.draw.line(surf, line_col, (0, 0), (self._sw, 0), 2)  # type: ignore[attr-defined]
        screen.blit(surf, (0, py))

        if tab is not None and tab != "wires":
            self._draw_building_picker(screen, py, ph, tab)

    def _draw_building_picker(self, screen: pygame.Surface,
                               py: int, ph: int, tab: str) -> None:
        th       = self._theme  # type: ignore[attr-defined]
        cards    = self._cards_for_tab(tab)  # type: ignore[attr-defined]
        scroll   = self._scroll_for_tab(tab)  # type: ignore[attr-defined]
        lv       = self._gs.level()  # type: ignore[attr-defined]
        cx_start = self._picker_content_x()  # type: ignore[attr-defined]
        vis_w    = self._picker_visible_w()  # type: ignore[attr-defined]
        card_h   = ph - 20

        ar_y = py + (ph - ARROW_W) // 2
        lr   = pygame.Rect(PICKER_PAD, ar_y, ARROW_W, ARROW_W)
        pygame.draw.rect(screen, (18, 30, 50), lr, border_radius=5)
        pygame.draw.rect(screen, th.BORDER,    lr, 1, border_radius=5)
        ls   = th.font(22).render("<", True, th.TEXT if scroll > 0 else th.TEXT_DIM)
        screen.blit(ls, (lr.centerx - ls.get_width() // 2, lr.centery - ls.get_height() // 2))
        self._picker_left_r = lr  # type: ignore[attr-defined]

        total_w  = max(0, len(cards) * CARD_STRIDE - CARD_GAP)
        max_sc   = max(0, total_w - vis_w)
        rr       = pygame.Rect(cx_start + vis_w + 4, ar_y, ARROW_W, ARROW_W)
        pygame.draw.rect(screen, (18, 30, 50), rr, border_radius=5)
        pygame.draw.rect(screen, th.BORDER,    rr, 1, border_radius=5)
        rs       = th.font(22).render(">", True, th.TEXT if scroll < max_sc else th.TEXT_DIM)
        screen.blit(rs, (rr.centerx - rs.get_width() // 2, rr.centery - rs.get_height() // 2))
        self._picker_right_r = rr  # type: ignore[attr-defined]

        clip = pygame.Rect(cx_start, py, vis_w, ph)
        screen.set_clip(clip)

        for i, tid in enumerate(cards):
            x = cx_start + i * CARD_STRIDE - scroll
            if x + CARD_W < cx_start or x > cx_start + vis_w:
                continue
            self._draw_building_card(screen, x, py + 10, CARD_W, card_h,
                                     tid, self._sel_type == tid, lv)  # type: ignore[attr-defined]

        screen.set_clip(None)

    def _draw_building_card(self, screen: pygame.Surface,
                             x: int, y: int, w: int, h: int,
                             type_id: str, selected: bool, player_level: int) -> None:
        th     = self._theme  # type: ignore[attr-defined]
        td     = BUILDING_TYPES.get(type_id)
        if td is None:
            return

        locked = td.unlock_level > player_level
        cat    = td.category
        ccol: tuple[int, int, int] = CAT_COLORS.get(cat) or th.ACCENT

        if locked:
            bg   = CARD_LOCK
            bord = (30, 40, 60)
        elif selected:
            bg   = (35, 18, 5)
            bord = th.ACCENT
        else:
            bg   = CARD_BG
            bord = ccol

        rect = pygame.Rect(x, y, w, h)
        pygame.draw.rect(screen, bg,   rect, border_radius=6)
        pygame.draw.rect(screen, bord, rect, 1 if not selected else 2, border_radius=6)

        # Texture preview
        img_h    = h - 68
        img_rect = pygame.Rect(x + 5, y + 5, w - 10, max(1, img_h))
        tex      = self._get_tex_scaled(type_id, img_rect.w, img_rect.h)
        if tex:
            if locked:
                dim = tex.copy()
                dim.set_alpha(60)
                screen.blit(dim, img_rect.topleft)
            else:
                screen.blit(tex, img_rect.topleft)
        else:
            ph_surf = self._placeholder(img_rect.w, img_rect.h)
            if locked:
                ph_surf = ph_surf.copy()
                ph_surf.set_alpha(60)
            screen.blit(ph_surf, img_rect.topleft)

        bottom_y = y + h - 60

        # Power / XP label (11→14)
        mw    = td.power_mw
        abs_v = abs(mw)
        if abs_v >= 1000:
            pwr_str = f"{mw / 1000:.1f} GW"
        elif abs_v < 1:
            pwr_str = f"{mw * 1000:.2f} kW"
        else:
            pwr_str = f"{mw:.0f} MW"

        if td.xp_rate > 0:
            pwr_str = f"+{td.xp_rate:.0f} XP/s"
            pwr_col = (126, 211, 33)
        elif mw > 0:
            pwr_col = th.ACCENT
        elif mw < 0:
            pwr_col = (79, 195, 247)
        else:
            pwr_col = th.TEXT_DIM

        pwr_s = th.font(16).render(pwr_str, True,
                                   th.TEXT_DIM if locked else pwr_col)
        screen.blit(pwr_s, (x + w // 2 - pwr_s.get_width() // 2, bottom_y))
        bottom_y += 18

        # Name
        nm_col = (50, 70, 100) if locked else (th.TEXT if selected else (160, 175, 200))
        name_s = th.font(16).render(td.name, True, nm_col)
        if name_s.get_width() > w - 6:
            font   = th.font(16)
            trunc  = td.name
            while font.size(trunc + "…")[0] > w - 6 and trunc:
                trunc = trunc[:-1]
            name_s = font.render(trunc + "…", True, nm_col)
        screen.blit(name_s, (x + w // 2 - name_s.get_width() // 2, bottom_y))
        bottom_y += 18

        # Build cost
        if td.build_cost > 0:
            can_afford = self._gs.money >= td.build_cost  # type: ignore[attr-defined]
            cost_col = (80, 220, 120) if (not locked and can_afford) else (200, 80, 60)
            cost_s = th.font(15).render(f"€{td.build_cost:,.0f}", True,
                                        (60, 80, 100) if locked else cost_col)
            screen.blit(cost_s, (x + w // 2 - cost_s.get_width() // 2, bottom_y))
            bottom_y += 17

        # Operating cost
        if td.operating_cost != 0:
            op_sign = "-" if td.operating_cost > 0 else "+"
            op_val  = abs(td.operating_cost)
            op_str  = f"{op_sign}€{op_val:.1f}/min"
            op_col  = (200, 90, 60) if td.operating_cost > 0 else (80, 220, 120)
            op_s = th.font(14).render(op_str, True,
                                      (60, 80, 100) if locked else op_col)
            screen.blit(op_s, (x + w // 2 - op_s.get_width() // 2, bottom_y))
            bottom_y += 16

        # Level badge (10→13)
        if locked:
            badge_col = (60, 80, 120)
        else:
            badge_col = ccol
        badge_txt = f"LVL {td.unlock_level}"
        badge_s   = th.font(15).render(badge_txt, True,
                                        (80, 100, 140) if locked else th.BG)
        badge_w   = badge_s.get_width() + 8
        badge_h   = badge_s.get_height() + 4
        badge_x   = x + w // 2 - badge_w // 2
        pygame.draw.rect(screen, badge_col,
                         (badge_x, bottom_y, badge_w, badge_h), border_radius=3)
        screen.blit(badge_s, (badge_x + 4, bottom_y + 2))

        # Lock overlay
        if locked:
            lx = x + w // 2 - 5
            ly = y + 8
            pygame.draw.rect(screen, (40, 60, 90), (lx, ly + 5, 10, 8), border_radius=2)
            pygame.draw.arc(screen, (40, 60, 90),
                            pygame.Rect(lx, ly, 10, 10), 0, 3.14159, 2)

    # ---- Toolbar ----

    def _draw_toolbar(self, screen: pygame.Surface) -> None:
        th = self._theme  # type: ignore[attr-defined]
        ty = self._sh - TOOLBAR_H  # type: ignore[attr-defined]

        bar = pygame.Surface((self._sw, TOOLBAR_H), pygame.SRCALPHA)  # type: ignore[attr-defined]
        bar.fill((8, 16, 28, 252))
        pygame.draw.line(bar, th.ACCENT, (0, 0), (self._sw, 0), 2)  # type: ignore[attr-defined]
        screen.blit(bar, (0, ty))

        tab_labels_map = {
            "generators": "1: Generators",
            "research":   "2: Research",
            "logistics":  "3: Logistics",
            "wires":      "4: Wires",
        }
        for tab, rect in self._tab_rects.items():  # type: ignore[attr-defined]
            active  = self._active_tab == tab  # type: ignore[attr-defined]
            ccol: tuple[int, int, int] = CAT_COLORS.get(tab) or th.ACCENT
            bg      = (25, 18, 8) if active else th.BTN_NORMAL_BG
            bord    = ccol        if active else th.BORDER
            pygame.draw.rect(screen, bg,   rect, border_radius=5)
            pygame.draw.rect(screen, bord, rect, 1 if not active else 2, border_radius=5)

            if active:
                tb = pygame.Rect(rect.x + 2, rect.y, rect.w - 4, 2)
                pygame.draw.rect(screen, ccol, tb, border_radius=1)

            lbl = tab_labels_map.get(tab, tab)
            ls  = th.font(18).render(lbl, True, ccol if active else th.TEXT_DIM)
            screen.blit(ls, (rect.centerx - ls.get_width() // 2,
                              rect.centery - ls.get_height() // 2))

        # Separator
        if self._tab_rects:  # type: ignore[attr-defined]
            sep_x = max(r.right for r in self._tab_rects.values()) + 8  # type: ignore[attr-defined]
            pygame.draw.line(screen, th.BORDER, (sep_x, ty + 6), (sep_x, self._sh - 6))  # type: ignore[attr-defined]

        # Bulldoze button
        for name, rect in self._tool_rects.items():  # type: ignore[attr-defined]
            active  = (name == "bulldoze" and self._state == _S.BULLDOZE)  # type: ignore[attr-defined]
            bg      = (40, 12, 12)  if active else th.BTN_NORMAL_BG
            bord    = (220, 60, 60) if active else th.BORDER
            pygame.draw.rect(screen, bg,   rect, border_radius=5)
            pygame.draw.rect(screen, bord, rect, 1, border_radius=5)
            lbl_col = (220, 80, 80) if active else th.TEXT
            ls = th.font(18).render("Bulldoze", True, lbl_col)
            screen.blit(ls, (rect.centerx - ls.get_width() // 2,
                              rect.centery - ls.get_height() // 2))

        # Wire tier indicator + clickable tier buttons in wire mode
        if self._state in (_S.WIRE_FROM, _S.WIRE_TO):  # type: ignore[attr-defined]
            tier    = self._wire_layer  # type: ignore[attr-defined]
            tier_lv = self._gs.level()  # type: ignore[attr-defined]
            tname, tcap, tunlock, _ = WIRE_TIERS[tier] if tier < len(WIRE_TIERS) else WIRE_TIERS[-1]
            locked_str = f" [LVL {tunlock}]" if tier_lv < tunlock else ""

            # Draw clickable tier buttons and update rects list
            self._wire_tier_btn_rects = []  # type: ignore[attr-defined]
            btn_w   = 110
            btn_h   = 44
            btn_gap = 6
            # Position buttons to the right of toolbar tools
            tool_right = max((r.right for r in self._tool_rects.values()), default=0)  # type: ignore[attr-defined]
            sep_x2 = tool_right + 16
            pygame.draw.line(screen, th.BORDER, (sep_x2 - 8, ty + 6), (sep_x2 - 8, self._sh - 6))  # type: ignore[attr-defined]
            bx2 = sep_x2
            by2 = ty + (TOOLBAR_H - btn_h) // 2

            for ti, tier_def in enumerate(WIRE_TIERS):
                tname2, tcap2, tunlock2, tcol2 = tier_def
                is_locked   = tier_lv < tunlock2
                is_selected = ti == self._wire_layer  # type: ignore[attr-defined]

                if is_selected:
                    tbg   = (30, 22, 5)
                    tbord = tcol2
                elif is_locked:
                    tbg   = (8, 12, 20)
                    tbord = (30, 40, 60)
                else:
                    tbg   = th.BTN_NORMAL_BG
                    tbord = (60, 70, 90)

                btn_rect = pygame.Rect(bx2, by2, btn_w, btn_h)
                pygame.draw.rect(screen, tbg,   btn_rect, border_radius=4)
                pygame.draw.rect(screen, tbord, btn_rect, 2 if is_selected else 1, border_radius=4)

                # Tier colour swatch
                swatch_col = (*tcol2, 80 if is_locked else 200)
                sw_surf = pygame.Surface((6, btn_h - 8), pygame.SRCALPHA)
                sw_surf.fill(swatch_col)
                screen.blit(sw_surf, (bx2 + 4, by2 + 4))

                # Tier name (truncated)
                short_names = ["Low", "Med", "High", "UHV"]
                short_name = short_names[ti] if ti < len(short_names) else tname2
                cap_str = _fmt_mw(tcap2)
                line1 = th.font(15).render(short_name, True,
                    (80, 100, 130) if is_locked else (tcol2 if is_selected else th.TEXT))
                line2 = th.font(14).render(cap_str, True,
                    (60, 80, 100) if is_locked else th.TEXT_DIM)
                lvl_str = f"LVL {tunlock2}" if is_locked else ""
                line3 = th.font(13).render(lvl_str, True, (100, 120, 160)) if lvl_str else None

                tx = bx2 + 14
                ty2 = by2 + 4
                screen.blit(line1, (tx, ty2))
                screen.blit(line2, (tx, ty2 + 16))
                if line3:
                    screen.blit(line3, (tx, ty2 + 30))

                self._wire_tier_btn_rects.append((ti, btn_rect))  # type: ignore[attr-defined]
                bx2 += btn_w + btn_gap

            if self._state == _S.WIRE_TO:  # type: ignore[attr-defined]
                hint = (f"{tname} ({_fmt_mw(tcap)}){locked_str}"
                        f" | Click target building | ESC to cancel")
            else:
                hint = (f"{tname} ({_fmt_mw(tcap)}){locked_str}"
                        f" | Click source building")
        else:
            self._wire_tier_btn_rects = []  # type: ignore[attr-defined]
            # Wire-mode hint displayed inline above; for non-wire modes show
            # a minimal action hint in the toolbar
            action_hints = {
                _S.PLACE:    "LMB: Place  ·  RMB/ESC: Cancel",
                _S.BULLDOZE: "Click to demolish  ·  ESC: Cancel",
            }
            hint = action_hints.get(self._state, "")  # type: ignore[attr-defined]
            if hint:
                hs = th.font(17).render(hint, True, th.TEXT_DIM)
                tool_right = max((r.right for r in self._tool_rects.values()), default=0) + 12  # type: ignore[attr-defined]
                hx = tool_right
                hy = ty + TOOLBAR_H // 2 - hs.get_height() // 2
                screen.blit(hs, (hx, hy))

        # Wire-mode hint (shown above tier buttons when in wire mode)
        if self._state in (_S.WIRE_FROM, _S.WIRE_TO) and self._wire_tier_btn_rects:  # type: ignore[attr-defined]
            pass  # hint already embedded in tier rendering above

        # Money + income display in far right of toolbar
        gs       = self._gs  # type: ignore[attr-defined]
        money    = gs.money
        net_inc  = gs.net_income_per_minute(self._powered, self._polluted)  # type: ignore[attr-defined]
        mc       = (80, 220, 120) if money >= 0 else (230, 60, 60)
        ic       = (80, 220, 120) if net_inc >= 0 else (230, 60, 60)
        inc_sign = "+" if net_inc >= 0 else ""
        m_s  = th.font(20).render(f"€{money:,.0f}", True, mc)
        i_s  = th.font(16).render(f"{inc_sign}€{net_inc:,.1f}/min", True, ic)
        rx   = self._sw - 12  # type: ignore[attr-defined]
        m_x  = rx - m_s.get_width()
        i_x  = rx - i_s.get_width()
        screen.blit(m_s, (m_x, ty + 5))
        screen.blit(i_s, (i_x, ty + 28))

        # Now Playing indicator (bottom right, above toolbar)
        from helpers.music import music_manager
        track = music_manager.track_name
        if track:
            np_s  = th.font(15).render(f"♫ {track}", True, (120, 140, 180))
            np_x  = self._sw - np_s.get_width() - 10  # type: ignore[attr-defined]
            np_y  = ty - np_s.get_height() - 4
            np_bg = pygame.Surface((np_s.get_width() + 8, np_s.get_height() + 4), pygame.SRCALPHA)
            np_bg.fill((8, 16, 28, 180))
            screen.blit(np_bg, (np_x - 4, np_y - 2))
            screen.blit(np_s,  (np_x, np_y))

    # ---- Building info panel ----

    def _draw_info_panel(self, screen: pygame.Surface, gah: int) -> None:
        b = self._hov_building  # type: ignore[attr-defined]
        if b is None:
            return
        th    = self._theme  # type: ignore[attr-defined]
        td    = b.typedef
        wires = self._gs.wires_for_building(b.build_id)  # type: ignore[attr-defined]
        cat   = td.category
        ccol: tuple[int, int, int] = CAT_COLORS.get(cat) or th.ACCENT

        pw = 310
        px = 10
        fk = th.font(17)
        fv = th.font(17)
        row_h = fk.size("A")[1] + 4

        is_powered = b.build_id in self._powered  # type: ignore[attr-defined]

        # Pollution check for this building
        polluted = self._polluted  # type: ignore[attr-defined]
        sw_b, sh_b = td.size
        is_polluted = bool(polluted and any(
            (b.gx + dx, b.gy + dy) in polluted
            for dx in range(sw_b) for dy in range(sh_b)
        ))

        rows: list[tuple[str, str, tuple[int, int, int]]] = []
        if td.power_mw > 0:
            rows.append(("Output",    _fmt_mw(td.power_mw),  th.ACCENT))
        elif td.power_mw < 0:
            rows.append(("Consumes",  _fmt_mw(abs(td.power_mw)), (79, 195, 247)))
        if td.xp_rate > 0:
            rows.append(("XP/s",      f"+{td.xp_rate:.1f}", (126, 211, 33)))
        if td.tax_rate > 0:
            if is_polluted:
                actual = td.tax_rate * 0.5
                rows.append(("Tax/min", f"€{actual:.1f}  (-50% polluted)", (220, 140, 40)))
            else:
                rows.append(("Tax/min", f"€{td.tax_rate:.1f}", (80, 220, 120)))
        if td.operating_cost != 0:
            sign    = "-" if td.operating_cost > 0 else "+"
            opc_col = (200, 100, 60) if td.operating_cost > 0 else (80, 220, 120)
            rows.append(("Op. Cost",  f"{sign}€{abs(td.operating_cost):.1f}/min", opc_col))
        if td.pollution_radius > 0:
            rows.append(("Pollution", f"r={td.pollution_radius} tiles", (200, 100, 50)))
        if td.build_cost > 0:
            rows.append(("Build Cost", f"€{td.build_cost:,.0f}", th.TEXT))
        # Show demolish cost for consumer buildings in bulldoze mode
        if not td.player_buildable and self._state == _S.BULLDOZE:  # type: ignore[attr-defined]
            from helpers.game_data import BULLDOZE_CONSUMER_COST
            rows.append(("Demolish", f"€{BULLDOZE_CONSUMER_COST:,.0f}", (220, 80, 60)))
        rows += [
            ("Unlock Lvl",  str(td.unlock_level),   th.TEXT),
            ("Powered",     "Yes" if is_powered else "No",
             (0, 200, 80) if is_powered else (200, 50, 50)),
            ("Connections", str(len(wires)),          th.TEXT),
            ("Grid Pos",    f"({b.gx}, {b.gy})",     th.TEXT_DIM),
        ]

        ph = 44 + len(rows) * row_h + 8
        py2 = gah - ph - 10

        panel = pygame.Surface((pw, ph), pygame.SRCALPHA)
        panel.fill(PANEL_BG)
        pygame.draw.rect(panel, ccol, panel.get_rect(), 1)
        pygame.draw.rect(panel, ccol, (0, 0, pw, 3))

        # Title
        title = th.font(19).render(td.name, True, ccol)
        panel.blit(title, (10, 10))

        # Category badge
        cat_s  = th.font(15).render((CATEGORY_LABELS.get(cat) or cat).upper(), True, th.BG)
        cat_bw = cat_s.get_width() + 8
        cat_bh = cat_s.get_height() + 4
        cat_bx = pw - cat_bw - 8
        pygame.draw.rect(panel, ccol, (cat_bx, 10, cat_bw, cat_bh), border_radius=3)
        panel.blit(cat_s, (cat_bx + 4, 12))

        y2 = 38
        for key, val, vcol in rows:
            ks = fk.render(key + ":", True, th.TEXT_DIM)
            vs = fv.render(val, True, vcol)
            panel.blit(ks, (10, y2))
            panel.blit(vs, (pw - vs.get_width() - 8, y2))
            y2 += row_h

        screen.blit(panel, (px, py2))

    # ---- Alert banner ----

    def _draw_alert(self, screen: pygame.Surface) -> None:
        th    = self._theme  # type: ignore[attr-defined]
        s     = th.font(20).render(self._alert_text, True, th.TEXT)  # type: ignore[attr-defined]
        w, h  = s.get_width() + 24, s.get_height() + 14
        x     = (self._sw - w) // 2  # type: ignore[attr-defined]
        y     = 72
        alpha = min(255, int(min(self._alert_timer, 0.8) / 0.8 * 255))  # type: ignore[attr-defined]
        bg    = pygame.Surface((w, h), pygame.SRCALPHA)
        bg.fill((10, 18, 32, alpha))
        pygame.draw.rect(bg, (*th.ACCENT, alpha), bg.get_rect(), 1)
        screen.blit(bg, (x, y))
        ts = s.copy()
        ts.set_alpha(alpha)
        screen.blit(ts, (x + 12, y + 7))

    # ---- Level-up notification (full-screen overlay) ----

    def _draw_levelup(self, screen: pygame.Surface) -> None:
        th    = self._theme  # type: ignore[attr-defined]
        timer = self._levelup_timer  # type: ignore[attr-defined]
        lv    = self._levelup_level  # type: ignore[attr-defined]

        total    = 6.0
        fade_in  = 0.5
        fade_out = 1.0
        elapsed  = total - timer

        if elapsed < fade_in:
            alpha = int(elapsed / fade_in * 255)
        elif timer < fade_out:
            alpha = int(timer / fade_out * 255)
        else:
            alpha = 255

        # Full-screen dim overlay
        dim = pygame.Surface((self._sw, self._sh), pygame.SRCALPHA)  # type: ignore[attr-defined]
        dim.fill((0, 0, 0, int(alpha * 0.65)))
        screen.blit(dim, (0, 0))

        # Central panel
        pw = min(700, self._sw - 40)  # type: ignore[attr-defined]

        # Newly unlocked buildings at this level
        newly_unlocked = [
            (tid, td) for tid, td in BUILDING_TYPES.items()
            if td.unlock_level == lv and td.player_buildable
        ]

        # Panel height: base + cards row
        card_rows = (len(newly_unlocked) + 3) // 4  # up to 4 per row
        ph = 180 + card_rows * 120
        px = (self._sw - pw) // 2  # type: ignore[attr-defined]
        py2 = (self._sh - ph) // 2  # type: ignore[attr-defined]

        surf = pygame.Surface((pw, ph), pygame.SRCALPHA)
        surf.fill((8, 20, 12, int(alpha * 0.95)))
        pygame.draw.rect(surf, (126, 211, 33, alpha), surf.get_rect(), 2)

        # "LEVEL UP!" title (30→34)
        t1 = th.font(34).render("LEVEL UP!", True, (126, 211, 33))
        t1.set_alpha(alpha)
        surf.blit(t1, (pw // 2 - t1.get_width() // 2, 16))

        # Level text (18→21)
        lv_txt = th.font(21).render(f"Now at Level {lv}", True, th.TEXT)
        lv_txt.set_alpha(alpha)
        surf.blit(lv_txt, (pw // 2 - lv_txt.get_width() // 2, 58))

        # Divider
        pygame.draw.line(surf, (126, 211, 33, alpha), (20, 90), (pw - 20, 90), 1)

        if newly_unlocked:
            # "Newly Unlocked:" label
            ul_lbl = th.font(18).render("Newly Unlocked:", True, (160, 200, 160))
            ul_lbl.set_alpha(alpha)
            surf.blit(ul_lbl, (20, 100))

            # Draw building cards in a grid
            card_w  = min(140, (pw - 40) // min(4, len(newly_unlocked)))
            card_h2 = 110
            for ci, (tid, td) in enumerate(newly_unlocked):
                col_i = ci % 4
                row_i = ci // 4
                cx2   = 20 + col_i * (card_w + 8)
                cy2   = 124 + row_i * (card_h2 + 8)

                # Category-coloured border
                ccol: tuple[int, int, int] = CAT_COLORS.get(td.category) or th.ACCENT
                card_rect = pygame.Rect(cx2, cy2, card_w, card_h2)
                pygame.draw.rect(surf, (16, 30, 20), card_rect, border_radius=5)
                pygame.draw.rect(surf, ccol, card_rect, 2, border_radius=5)

                # Building texture
                tex = self._get_tex_scaled(tid, card_w - 10, card_h2 - 36)
                if tex:
                    tsurf = tex.copy()
                    tsurf.set_alpha(alpha)
                    surf.blit(tsurf, (cx2 + 5, cy2 + 4))

                # Building name (11→14)
                nm_s = th.font(16).render(td.name, True, th.TEXT)
                nm_s.set_alpha(alpha)
                surf.blit(nm_s, (cx2 + card_w // 2 - nm_s.get_width() // 2,
                                  cy2 + card_h2 - 30))

                # Power stat (11→14)
                if td.xp_rate > 0:
                    stat_str = f"+{td.xp_rate:.0f} XP/s"
                    stat_col = (126, 211, 33)
                elif td.power_mw > 0:
                    stat_str = _fmt_mw(td.power_mw)
                    stat_col = th.ACCENT
                else:
                    stat_str = _fmt_mw(td.power_mw)
                    stat_col = (79, 195, 247)
                stat_s = th.font(16).render(stat_str, True, stat_col)
                stat_s.set_alpha(alpha)
                surf.blit(stat_s, (cx2 + card_w // 2 - stat_s.get_width() // 2,
                                    cy2 + card_h2 - 16))
        else:
            # No new buildings — show simple hint
            hs = th.font(18).render("Keep expanding your network!", True, (160, 200, 160))
            hs.set_alpha(alpha)
            surf.blit(hs, (pw // 2 - hs.get_width() // 2, 110))

        # Newly unlocked wire tiers at this level
        new_wire_tiers = [(i, t) for i, t in enumerate(WIRE_TIERS) if t[2] == lv]
        if new_wire_tiers:
            wire_y = 124 + card_rows * (110 + 8) + 8
            wt_lbl = th.font(17).render("New Wire Tier:", True, (220, 220, 60))
            wt_lbl.set_alpha(alpha)
            surf.blit(wt_lbl, (20, wire_y))
            wire_y += 20
            for _, tier_def in new_wire_tiers:
                tname_w, tcap_w, _, tcol_w = tier_def
                tier_s = th.font(16).render(
                    f"  {tname_w}  —  {_fmt_mw(tcap_w)} capacity",
                    True, tcol_w,
                )
                tier_s.set_alpha(alpha)
                surf.blit(tier_s, (20, wire_y))
                wire_y += 18

        # "Click to continue" hint at bottom
        cont_s = th.font(18).render("Click to continue or wait 6 seconds", True, th.TEXT_DIM)
        cont_s.set_alpha(int(alpha * 0.7))
        surf.blit(cont_s, (pw // 2 - cont_s.get_width() // 2, ph - 22))

        screen.blit(surf, (px, py2))

    # ---- Pause overlay ----

    def _draw_pause(self, screen: pygame.Surface) -> None:
        th = self._theme  # type: ignore[attr-defined]

        overlay = pygame.Surface((self._sw, self._sh), pygame.SRCALPHA)  # type: ignore[attr-defined]
        overlay.fill((0, 0, 0, 170))
        screen.blit(overlay, (0, 0))

        if self._pause_rect is None:  # type: ignore[attr-defined]
            return
        r     = self._pause_rect  # type: ignore[attr-defined]
        panel = pygame.Surface((r.w, r.h), pygame.SRCALPHA)
        panel.fill((10, 18, 32, 252))
        pygame.draw.rect(panel, th.ACCENT, panel.get_rect(), 2)
        screen.blit(panel, r.topleft)

        # "PAUSED" title (30→34)
        title = th.font(34).render("PAUSED", True, th.ACCENT)
        screen.blit(title, (r.centerx - title.get_width() // 2, r.y + 18))

        mx2, my2 = pygame.mouse.get_pos()
        for rect, label, base_bg in [
            (self._pause_resume_r,   "Resume",              th.BTN_NORMAL_BG),  # type: ignore[attr-defined]
            (self._pause_settings_r, "Settings",            (12, 28, 52)),       # type: ignore[attr-defined]
            (self._pause_quit_r,     "Save & Quit to Menu", (40, 15, 15)),       # type: ignore[attr-defined]
        ]:
            if rect is None:
                continue
            hov = rect.collidepoint(mx2, my2)
            bg  = th.BTN_HOVER_BG if hov else base_bg
            pygame.draw.rect(screen, bg,        rect, border_radius=6)
            pygame.draw.rect(screen, th.BORDER, rect, 1, border_radius=6)
            ls = th.font(22).render(label, True, th.TEXT)
            screen.blit(ls, (rect.centerx - ls.get_width() // 2,
                              rect.centery - ls.get_height() // 2))

    # ---- Debug overlay ----

    def _draw_debug(self, screen: pygame.Surface) -> None:
        th = self._theme  # type: ignore[attr-defined]
        lv = self._gs.level()  # type: ignore[attr-defined]

        overlay = pygame.Surface((self._sw, self._sh), pygame.SRCALPHA)  # type: ignore[attr-defined]
        overlay.fill((0, 0, 0, 120))
        screen.blit(overlay, (0, 0))

        pw, ph = 540, 260
        px = (self._sw - pw) // 2  # type: ignore[attr-defined]
        py = (self._sh - ph) // 2  # type: ignore[attr-defined]

        panel = pygame.Surface((pw, ph), pygame.SRCALPHA)
        panel.fill((10, 18, 32, 252))
        pygame.draw.rect(panel, th.ACCENT, panel.get_rect(), 2)
        screen.blit(panel, (px, py))

        # Title (22→26)
        title_s = th.font(26).render("DEBUG", True, th.ACCENT)
        screen.blit(title_s, (px + pw // 2 - title_s.get_width() // 2, py + 10))

        # Level / XP info (14→17)
        info_s = th.font(19).render(
            f"Level {lv}   XP: {_fmt_xp(self._gs.total_xp)}   Money: €{self._gs.money:,.0f}",  # type: ignore[attr-defined]
            True, th.TEXT,
        )
        screen.blit(info_s, (px + 16, py + 40))

        btn_w   = 64
        btn_h   = 34
        btn_gap = 8
        mx2, my2 = pygame.mouse.get_pos()

        self._debug_btn_rects = []  # type: ignore[attr-defined]

        level_labels = ["LVL 1", "LVL 3", "LVL 5", "LVL 7", "LVL 10", "LVL 15", "MAX"]
        bx = px + 16
        by = py + 72
        for label in level_labels:
            r = pygame.Rect(bx, by, btn_w, btn_h)
            hov = r.collidepoint(mx2, my2)
            bg  = th.BTN_HOVER_BG if hov else th.BTN_NORMAL_BG
            pygame.draw.rect(screen, bg,        r, border_radius=4)
            pygame.draw.rect(screen, th.ACCENT if hov else th.BORDER,
                             r, 1, border_radius=4)
            ls = th.font(18).render(label, True, th.TEXT)
            screen.blit(ls, (r.centerx - ls.get_width() // 2,
                              r.centery - ls.get_height() // 2))
            self._debug_btn_rects.append((label, r))  # type: ignore[attr-defined]
            bx += btn_w + btn_gap

        ry = by + btn_h + 14
        money_labels = [("Reset XP", 100, (40, 15, 15), (200, 60, 60), (220, 80, 80)),
                        ("+€10k",    80,  (15, 40, 20), (60, 200, 80), (80, 220, 120)),
                        ("Reset €",  80,  (30, 30, 15), (160, 160, 40), (200, 200, 60))]
        mx3 = px + 16
        for mlabel, mw, mbg, mbord, mtxt in money_labels:
            r = pygame.Rect(mx3, ry, mw, btn_h)
            hov = r.collidepoint(mx2, my2)
            bg2 = (min(255, mbg[0] + 20), min(255, mbg[1] + 20), min(255, mbg[2] + 20)) if hov else mbg
            pygame.draw.rect(screen, bg2,   r, border_radius=4)
            pygame.draw.rect(screen, mbord, r, 1, border_radius=4)
            ls = th.font(18).render(mlabel, True, mtxt)
            screen.blit(ls, (r.centerx - ls.get_width() // 2,
                              r.centery - ls.get_height() // 2))
            self._debug_btn_rects.append((mlabel, r))  # type: ignore[attr-defined]
            mx3 += mw + btn_gap

        hint_s = th.font(18).render("F5 or ESC to close", True, th.TEXT_DIM)
        screen.blit(hint_s, (px + pw // 2 - hint_s.get_width() // 2, py + ph - 30))
