"""
Events service s-expression page components.

Renders all events, page summary, calendars, calendar month, day, day admin,
calendar admin, tickets, ticket admin, and markets pages.
Called from route handlers in place of ``render_template()``.
"""
from __future__ import annotations

import os
from typing import Any
from markupsafe import escape

from shared.sx.jinja_bridge import load_service_components
from shared.sx.helpers import (
    call_url, get_asset_url, sx_call,
    root_header_sx, post_header_sx, post_admin_header_sx,
    oob_header_sx, header_child_sx,
    full_page_sx, oob_page_sx,
    search_mobile_sx, search_desktop_sx,
)
from shared.sx.parser import SxExpr

# Load events-specific .sx components + handlers at import time
load_service_components(os.path.dirname(os.path.dirname(__file__)),
                        service_name="events")


# ---------------------------------------------------------------------------
# OOB header helper — delegates to shared
# ---------------------------------------------------------------------------



# ---------------------------------------------------------------------------
# Post header helpers — thin wrapper over shared post_header_sx
# ---------------------------------------------------------------------------

def _clear_oob(*ids: str) -> str:
    """Generate OOB swaps to remove orphaned header rows/children."""
    return "".join(f'(div :id "{i}" :hx-swap-oob "outerHTML")' for i in ids)


# All possible header row/child IDs at each depth (deepest first)
_EVENTS_DEEP_IDS = [
    "entry-admin-row", "entry-admin-header-child",
    "entry-row", "entry-header-child",
    "day-admin-row", "day-admin-header-child",
    "day-row", "day-header-child",
    "calendar-admin-row", "calendar-admin-header-child",
    "calendar-row", "calendar-header-child",
    "calendars-row", "calendars-header-child",
    "post-admin-row", "post-admin-header-child",
]


def _clear_deeper_oob(*keep_ids: str) -> str:
    """Clear all events header rows/children NOT in keep_ids."""
    to_clear = [i for i in _EVENTS_DEEP_IDS if i not in keep_ids]
    return _clear_oob(*to_clear)


async def _ensure_container_nav(ctx: dict) -> dict:
    """Fetch container_nav if not already present (for post header row)."""
    if ctx.get("container_nav"):
        return ctx
    post = ctx.get("post") or {}
    post_id = post.get("id")
    slug = post.get("slug", "")
    if not post_id:
        return ctx
    from quart import g
    from shared.infrastructure.fragments import fetch_fragments
    current_cal = getattr(g, "calendar_slug", "") or ""
    nav_params = {
        "container_type": "page",
        "container_id": str(post_id),
        "post_slug": slug,
        "current_calendar": current_cal,
    }
    events_nav, market_nav = await fetch_fragments([
        ("events", "container-nav", nav_params),
        ("market", "container-nav", nav_params),
    ], required=False)
    return {**ctx, "container_nav": events_nav + market_nav}


def _post_header_sx(ctx: dict, *, oob: bool = False) -> str:
    """Build the post-level header row — delegates to shared sx helper."""
    return post_header_sx(ctx, oob=oob)


def _post_nav_sx(ctx: dict) -> str:
    """Post desktop nav: calendar links + container nav (markets, etc.)."""
    from quart import url_for, g

    calendars = ctx.get("calendars") or []
    select_colours = ctx.get("select_colours", "")
    current_cal_slug = getattr(g, "calendar_slug", None)

    parts = []
    for cal in calendars:
        cal_slug = getattr(cal, "slug", "") if hasattr(cal, "slug") else cal.get("slug", "")
        cal_name = getattr(cal, "name", "") if hasattr(cal, "name") else cal.get("name", "")
        href = url_for("calendar.get", calendar_slug=cal_slug)
        is_sel = (cal_slug == current_cal_slug)
        parts.append(sx_call("nav-link", href=href, icon="fa fa-calendar",
                            label=cal_name, select_colours=select_colours,
                            is_selected=is_sel))
    # Container nav fragments (markets, etc.)
    container_nav = ctx.get("container_nav", "")
    if container_nav:
        parts.append(container_nav)

    # Admin cog → blog admin for this post (cross-domain, no HTMX)
    rights = ctx.get("rights") or {}
    has_admin = rights.get("admin") if isinstance(rights, dict) else getattr(rights, "admin", False)
    if has_admin:
        post = ctx.get("post") or {}
        slug = post.get("slug", "")
        styles = ctx.get("styles") or {}
        nav_btn = styles.get("nav_button", "") if isinstance(styles, dict) else getattr(styles, "nav_button", "")
        select_colours = ctx.get("select_colours", "")
        admin_href = call_url(ctx, "blog_url", f"/{slug}/admin/")
        aclass = f"{nav_btn} {select_colours}".strip() or (
            "justify-center cursor-pointer flex flex-row items-center gap-2 "
            "rounded bg-stone-200 text-black p-3"
        )
        parts.append(
            f'<div class="relative nav-group">'
            f'<a href="{admin_href}" class="{aclass}">'
            f'<i class="fa fa-cog" aria-hidden="true"></i></a></div>'
        )

    return "".join(parts)


# ---------------------------------------------------------------------------
# Post admin header
# ---------------------------------------------------------------------------

# ---------------------------------------------------------------------------
# Calendars header
# ---------------------------------------------------------------------------

def _calendars_header_sx(ctx: dict, *, oob: bool = False) -> str:
    """Build the calendars section header row."""
    from quart import url_for
    link_href = url_for("calendars.home")
    return sx_call("menu-row-sx", id="calendars-row", level=3,
                  link_href=link_href,
                  link_label_content=SxExpr(sx_call("events-calendars-label")),
                  child_id="calendars-header-child", oob=oob)


# ---------------------------------------------------------------------------
# Calendar header
# ---------------------------------------------------------------------------

def _calendar_header_sx(ctx: dict, *, oob: bool = False) -> str:
    """Build a single calendar's header row."""
    from quart import url_for
    calendar = ctx.get("calendar")
    if not calendar:
        return ""
    cal_slug = getattr(calendar, "slug", "")
    cal_name = getattr(calendar, "name", "")
    cal_desc = getattr(calendar, "description", "") or ""

    link_href = url_for("calendar.get", calendar_slug=cal_slug)
    label_html = sx_call("events-calendar-label",
                        name=cal_name, description=cal_desc)

    # Desktop nav: slots + admin
    nav_html = _calendar_nav_sx(ctx)

    return sx_call("menu-row-sx", id="calendar-row", level=3,
                  link_href=link_href, link_label_content=SxExpr(label_html),
                  nav=SxExpr(nav_html) if nav_html else None, child_id="calendar-header-child", oob=oob)


def _calendar_nav_sx(ctx: dict) -> str:
    """Calendar desktop nav: Slots + admin link."""
    from quart import url_for
    calendar = ctx.get("calendar")
    if not calendar:
        return ""
    cal_slug = getattr(calendar, "slug", "")
    rights = ctx.get("rights") or {}
    is_admin = getattr(rights, "admin", False) if hasattr(rights, "admin") else rights.get("admin", False)
    select_colours = ctx.get("select_colours", "")

    parts = []
    slots_href = url_for("calendar.slots.defpage_slots_listing", calendar_slug=cal_slug)
    parts.append(sx_call("nav-link", href=slots_href, icon="fa fa-clock",
                        label="Slots", select_colours=select_colours))
    if is_admin:
        admin_href = url_for("calendar.admin.defpage_calendar_admin", calendar_slug=cal_slug)
        parts.append(sx_call("nav-link", href=admin_href, icon="fa fa-cog",
                            select_colours=select_colours))
    return "".join(parts)


# ---------------------------------------------------------------------------
# Day header
# ---------------------------------------------------------------------------

def _day_header_sx(ctx: dict, *, oob: bool = False) -> str:
    """Build day detail header row."""
    from quart import url_for
    calendar = ctx.get("calendar")
    if not calendar:
        return ""
    cal_slug = getattr(calendar, "slug", "")
    day_date = ctx.get("day_date")
    if not day_date:
        return ""

    link_href = url_for(
        "calendar.day.show_day",
        calendar_slug=cal_slug,
        year=day_date.year,
        month=day_date.month,
        day=day_date.day,
    )
    label_html = sx_call("events-day-label",
                        date_str=day_date.strftime("%A %d %B %Y"))

    nav_html = _day_nav_sx(ctx)

    return sx_call("menu-row-sx", id="day-row", level=4,
                  link_href=link_href, link_label_content=SxExpr(label_html),
                  nav=SxExpr(nav_html) if nav_html else None, child_id="day-header-child", oob=oob)


def _day_nav_sx(ctx: dict) -> str:
    """Day desktop nav: confirmed entries scrolling menu + admin link."""
    from quart import url_for
    calendar = ctx.get("calendar")
    if not calendar:
        return ""
    cal_slug = getattr(calendar, "slug", "")
    day_date = ctx.get("day_date")
    confirmed_entries = ctx.get("confirmed_entries") or []
    rights = ctx.get("rights") or {}
    is_admin = getattr(rights, "admin", False) if hasattr(rights, "admin") else rights.get("admin", False)

    parts = []
    # Confirmed entries nav (scrolling menu)
    if confirmed_entries:
        entry_links = []
        for entry in confirmed_entries:
            href = url_for(
                "calendar.day.calendar_entries.calendar_entry.get",
                calendar_slug=cal_slug,
                year=day_date.year,
                month=day_date.month,
                day=day_date.day,
                entry_id=entry.id,
            )
            start = entry.start_at.strftime("%H:%M") if entry.start_at else ""
            end = f" \u2013 {entry.end_at.strftime('%H:%M')}" if entry.end_at else ""
            entry_links.append(sx_call("events-day-entry-link",
                                      href=href, name=entry.name,
                                      time_str=f"{start}{end}"))
        inner = "".join(entry_links)
        parts.append(sx_call("events-day-entries-nav", inner=SxExpr(inner)))

    if is_admin and day_date:
        admin_href = url_for(
            "calendar.day.admin.admin",
            calendar_slug=cal_slug,
            year=day_date.year,
            month=day_date.month,
            day=day_date.day,
        )
        parts.append(sx_call("nav-link", href=admin_href, icon="fa fa-cog"))
    return "".join(parts)


# ---------------------------------------------------------------------------
# Day admin header
# ---------------------------------------------------------------------------

def _day_admin_header_sx(ctx: dict, *, oob: bool = False) -> str:
    """Build day admin header row."""
    from quart import url_for
    calendar = ctx.get("calendar")
    if not calendar:
        return ""
    cal_slug = getattr(calendar, "slug", "")
    day_date = ctx.get("day_date")
    if not day_date:
        return ""

    link_href = url_for(
        "calendar.day.admin.admin",
        calendar_slug=cal_slug,
        year=day_date.year,
        month=day_date.month,
        day=day_date.day,
    )
    return sx_call("menu-row-sx", id="day-admin-row", level=5,
                  link_href=link_href, link_label="admin", icon="fa fa-cog",
                  child_id="day-admin-header-child", oob=oob)


# ---------------------------------------------------------------------------
# Calendar admin header
# ---------------------------------------------------------------------------

def _calendar_admin_header_sx(ctx: dict, *, oob: bool = False) -> str:
    """Build calendar admin header row with nav links."""
    from quart import url_for
    calendar = ctx.get("calendar")
    cal_slug = getattr(calendar, "slug", "") if calendar else ""
    select_colours = ctx.get("select_colours", "")

    nav_parts = []
    if cal_slug:
        for endpoint, label in [
            ("calendar.slots.defpage_slots_listing", "slots"),
            ("calendar.admin.calendar_description_edit", "description"),
        ]:
            href = url_for(endpoint, calendar_slug=cal_slug)
            nav_parts.append(sx_call("nav-link", href=href, label=label,
                                    select_colours=select_colours))

    nav_html = "".join(nav_parts)
    return sx_call("menu-row-sx", id="calendar-admin-row", level=4,
                  link_label="admin", icon="fa fa-cog",
                  nav=SxExpr(nav_html) if nav_html else None, child_id="calendar-admin-header-child", oob=oob)


# ---------------------------------------------------------------------------
# Markets header
# ---------------------------------------------------------------------------

def _markets_header_sx(ctx: dict, *, oob: bool = False) -> str:
    """Build the markets section header row."""
    from quart import url_for
    link_href = url_for("markets.defpage_events_markets")
    return sx_call("menu-row-sx", id="markets-row", level=3,
                  link_href=link_href,
                  link_label_content=SxExpr(sx_call("events-markets-label")),
                  child_id="markets-header-child", oob=oob)


# ---------------------------------------------------------------------------
# Calendars main panel
# ---------------------------------------------------------------------------

def _calendars_main_panel_sx(ctx: dict) -> str:
    """Render the calendars list + create form panel."""
    from quart import url_for
    rights = ctx.get("rights") or {}
    is_admin = getattr(rights, "admin", False) if hasattr(rights, "admin") else rights.get("admin", False)
    has_access = ctx.get("has_access")
    can_create = has_access("calendars.create_calendar") if callable(has_access) else is_admin
    csrf_token = ctx.get("csrf_token")
    csrf = csrf_token() if callable(csrf_token) else (csrf_token or "")

    calendars = ctx.get("calendars") or []

    form_html = ""
    if can_create:
        create_url = url_for("calendars.create_calendar")
        form_html = sx_call("crud-create-form",
                           create_url=create_url, csrf=csrf,
                           errors_id="cal-create-errors", list_id="calendars-list",
                           placeholder="e.g. Events, Gigs, Meetings", btn_label="Add calendar")

    list_html = _calendars_list_sx(ctx, calendars)
    return sx_call("crud-panel",
                  form=SxExpr(form_html), list=SxExpr(list_html),
                  list_id="calendars-list")


def _calendars_list_sx(ctx: dict, calendars: list) -> str:
    """Render the calendars list items."""
    from quart import url_for
    from shared.utils import route_prefix
    csrf_token = ctx.get("csrf_token")
    csrf = csrf_token() if callable(csrf_token) else (csrf_token or "")
    prefix = route_prefix()

    if not calendars:
        return sx_call("empty-state", message="No calendars yet. Create one above.",
                      cls="text-gray-500 mt-4")

    parts = []
    for cal in calendars:
        cal_slug = getattr(cal, "slug", "")
        cal_name = getattr(cal, "name", "")
        href = prefix + url_for("calendar.get", calendar_slug=cal_slug)
        del_url = url_for("calendar.delete", calendar_slug=cal_slug)
        csrf_hdr = f'{{"X-CSRFToken":"{csrf}"}}'
        parts.append(sx_call("crud-item",
                            href=href, name=cal_name, slug=cal_slug,
                            del_url=del_url, csrf_hdr=csrf_hdr,
                            list_id="calendars-list",
                            confirm_title="Delete calendar?",
                            confirm_text="Entries will be hidden (soft delete)"))
    return "".join(parts)


# ---------------------------------------------------------------------------
# Calendar month grid
# ---------------------------------------------------------------------------

def _calendar_main_panel_html(ctx: dict) -> str:
    """Render the calendar month grid."""
    from quart import url_for
    from quart import session as qsession

    calendar = ctx.get("calendar")
    if not calendar:
        return ""
    cal_slug = getattr(calendar, "slug", "")
    hx_select = ctx.get("hx_select_search", "#main-panel")
    styles = ctx.get("styles") or {}
    pill_cls = getattr(styles, "pill", "") if hasattr(styles, "pill") else styles.get("pill", "")

    year = ctx.get("year", 2024)
    month = ctx.get("month", 1)
    month_name = ctx.get("month_name", "")
    weekday_names = ctx.get("weekday_names", [])
    weeks = ctx.get("weeks", [])
    prev_month = ctx.get("prev_month", 1)
    prev_month_year = ctx.get("prev_month_year", year)
    next_month = ctx.get("next_month", 1)
    next_month_year = ctx.get("next_month_year", year)
    prev_year = ctx.get("prev_year", year - 1)
    next_year = ctx.get("next_year", year + 1)
    month_entries = ctx.get("month_entries") or []
    user = ctx.get("user")
    qs = qsession if "qsession" not in ctx else ctx["qsession"]

    def nav_link(y, m):
        return url_for("calendar.get", calendar_slug=cal_slug, year=y, month=m)

    # Month navigation arrows
    nav_arrows = []
    for label, yr, mn in [
        ("\u00ab", prev_year, month),
        ("\u2039", prev_month_year, prev_month),
    ]:
        href = nav_link(yr, mn)
        nav_arrows.append(sx_call("events-calendar-nav-arrow",
                                 pill_cls=pill_cls, href=href, label=label))

    nav_arrows.append(sx_call("events-calendar-month-label",
                             month_name=month_name, year=str(year)))

    for label, yr, mn in [
        ("\u203a", next_month_year, next_month),
        ("\u00bb", next_year, month),
    ]:
        href = nav_link(yr, mn)
        nav_arrows.append(sx_call("events-calendar-nav-arrow",
                                 pill_cls=pill_cls, href=href, label=label))

    # Weekday headers
    wd_html = "".join(sx_call("events-calendar-weekday", name=wd) for wd in weekday_names)

    # Day cells
    cells = []
    for week in weeks:
        for day_cell in week:
            if isinstance(day_cell, dict):
                in_month = day_cell.get("in_month", True)
                is_today = day_cell.get("is_today", False)
                day_date = day_cell.get("date")
            else:
                in_month = getattr(day_cell, "in_month", True)
                is_today = getattr(day_cell, "is_today", False)
                day_date = getattr(day_cell, "date", None)

            cell_cls = "min-h-20 sm:min-h-24 bg-white px-3 py-2 text-xs"
            if not in_month:
                cell_cls += " bg-stone-50 text-stone-400"
            if is_today:
                cell_cls += " ring-2 ring-blue-500 z-10 relative"

            # Day number link
            day_num_html = ""
            day_short_html = ""
            if day_date:
                day_href = url_for(
                    "calendar.day.show_day",
                    calendar_slug=cal_slug,
                    year=day_date.year, month=day_date.month, day=day_date.day,
                )
                day_short_html = sx_call("events-calendar-day-short",
                                        day_str=day_date.strftime("%a"))
                day_num_html = sx_call("events-calendar-day-num",
                                      pill_cls=pill_cls, href=day_href,
                                      num=str(day_date.day))

            # Entry badges for this day
            entry_badges = []
            if day_date:
                for e in month_entries:
                    if e.start_at and e.start_at.date() == day_date:
                        is_mine = (
                            (user and e.user_id == user.id)
                            or (not user and e.session_id == qs.get("calendar_sid"))
                        )
                        if e.state == "confirmed":
                            bg_cls = "bg-emerald-200 text-emerald-900" if is_mine else "bg-emerald-100 text-emerald-800"
                        else:
                            bg_cls = "bg-sky-100 text-sky-800" if is_mine else "bg-stone-100 text-stone-700"
                        state_label = (e.state or "pending").replace("_", " ")
                        entry_badges.append(sx_call("events-calendar-entry-badge",
                                                   bg_cls=bg_cls, name=e.name,
                                                   state_label=state_label))

            badges_html = "(<> " + "".join(entry_badges) + ")" if entry_badges else ""
            cells.append(sx_call("events-calendar-cell",
                                cell_cls=cell_cls, day_short=SxExpr(day_short_html),
                                day_num=SxExpr(day_num_html),
                                badges=SxExpr(badges_html) if badges_html else None))

    cells_html = "(<> " + "".join(cells) + ")"
    arrows_html = "(<> " + "".join(nav_arrows) + ")"
    wd_html = "(<> " + wd_html + ")"
    return sx_call("events-calendar-grid",
                  arrows=SxExpr(arrows_html), weekdays=SxExpr(wd_html),
                  cells=SxExpr(cells_html))


# ---------------------------------------------------------------------------
# Day main panel
# ---------------------------------------------------------------------------

def _day_main_panel_html(ctx: dict) -> str:
    """Render the day entries table + add button."""
    from quart import url_for

    calendar = ctx.get("calendar")
    if not calendar:
        return ""
    cal_slug = getattr(calendar, "slug", "")
    day_entries = ctx.get("day_entries") or []
    day = ctx.get("day")
    month = ctx.get("month")
    year = ctx.get("year")
    hx_select = ctx.get("hx_select_search", "#main-panel")
    styles = ctx.get("styles") or {}
    list_container = getattr(styles, "list_container", "") if hasattr(styles, "list_container") else styles.get("list_container", "")
    pill_cls = getattr(styles, "pill", "") if hasattr(styles, "pill") else styles.get("pill", "")
    tr_cls = getattr(styles, "tr", "") if hasattr(styles, "tr") else styles.get("tr", "")
    pre_action = getattr(styles, "pre_action_button", "") if hasattr(styles, "pre_action_button") else styles.get("pre_action_button", "")

    rows_html = ""
    if day_entries:
        rows_html = "".join(_day_row_html(ctx, entry) for entry in day_entries)
    else:
        rows_html = sx_call("events-day-empty-row")

    add_url = url_for(
        "calendar.day.calendar_entries.add_form",
        calendar_slug=cal_slug,
        day=day, month=month, year=year,
    )

    return sx_call("events-day-table",
                  list_container=list_container, rows=SxExpr(rows_html),
                  pre_action=pre_action, add_url=add_url)


def _day_row_html(ctx: dict, entry) -> str:
    """Render a single day table row."""
    from quart import url_for
    calendar = ctx.get("calendar")
    cal_slug = getattr(calendar, "slug", "")
    day = ctx.get("day")
    month = ctx.get("month")
    year = ctx.get("year")
    hx_select = ctx.get("hx_select_search", "#main-panel")
    styles = ctx.get("styles") or {}
    pill_cls = getattr(styles, "pill", "") if hasattr(styles, "pill") else styles.get("pill", "")
    tr_cls = getattr(styles, "tr", "") if hasattr(styles, "tr") else styles.get("tr", "")

    entry_href = url_for(
        "calendar.day.calendar_entries.calendar_entry.get",
        calendar_slug=cal_slug, day=day, month=month, year=year, entry_id=entry.id,
    )

    # Name
    name_html = sx_call("events-day-row-name",
                       href=entry_href, pill_cls=pill_cls, name=entry.name)

    # Slot/Time
    slot = getattr(entry, "slot", None)
    if slot:
        slot_href = url_for("calendar.slots.slot.defpage_slot_detail", calendar_slug=cal_slug, slot_id=slot.id)
        time_start = slot.time_start.strftime("%H:%M") if slot.time_start else ""
        time_end = f" \u2192 {slot.time_end.strftime('%H:%M')}" if slot.time_end else ""
        slot_html = sx_call("events-day-row-slot",
                           href=slot_href, pill_cls=pill_cls, slot_name=slot.name,
                           time_str=f"({time_start}{time_end})")
    else:
        start = entry.start_at.strftime("%H:%M") if entry.start_at else ""
        end = f" \u2192 {entry.end_at.strftime('%H:%M')}" if entry.end_at else ""
        slot_html = sx_call("events-day-row-time", start=start, end=end)

    # State
    state = getattr(entry, "state", "pending") or "pending"
    state_badge = _entry_state_badge_html(state)
    state_td = sx_call("events-day-row-state",
                      state_id=f"entry-state-{entry.id}", badge=SxExpr(state_badge))

    # Cost
    cost = getattr(entry, "cost", None)
    cost_str = f"\u00a3{cost:.2f}" if cost is not None else "\u00a30.00"
    cost_td = sx_call("events-day-row-cost", cost_str=cost_str)

    # Tickets
    tp = getattr(entry, "ticket_price", None)
    if tp is not None:
        tc = getattr(entry, "ticket_count", None)
        tc_str = f"{tc} tickets" if tc is not None else "Unlimited"
        tickets_td = sx_call("events-day-row-tickets",
                            price_str=f"\u00a3{tp:.2f}", count_str=tc_str)
    else:
        tickets_td = sx_call("events-day-row-no-tickets")

    actions_td = sx_call("events-day-row-actions")

    return sx_call("events-day-row",
                  tr_cls=tr_cls, name=SxExpr(name_html), slot=SxExpr(slot_html),
                  state=SxExpr(state_td), cost=SxExpr(cost_td),
                  tickets=SxExpr(tickets_td), actions=SxExpr(actions_td))


def _entry_state_badge_html(state: str) -> str:
    """Render an entry state badge."""
    state_classes = {
        "confirmed": "bg-emerald-100 text-emerald-800",
        "provisional": "bg-amber-100 text-amber-800",
        "ordered": "bg-sky-100 text-sky-800",
        "pending": "bg-stone-100 text-stone-700",
        "declined": "bg-red-100 text-red-800",
    }
    cls = state_classes.get(state, "bg-stone-100 text-stone-700")
    label = state.replace("_", " ").capitalize()
    return sx_call("badge", cls=cls, label=label)


# ---------------------------------------------------------------------------
# Day admin main panel
# ---------------------------------------------------------------------------

def _day_admin_main_panel_html(ctx: dict) -> str:
    """Render day admin panel (placeholder nav)."""
    return sx_call("events-day-admin-panel")


# ---------------------------------------------------------------------------
# Calendar admin main panel
# ---------------------------------------------------------------------------

def _calendar_admin_main_panel_html(ctx: dict) -> str:
    """Render calendar admin config panel with description editor."""
    from quart import url_for
    calendar = ctx.get("calendar")
    if not calendar:
        return ""
    csrf_token = ctx.get("csrf_token")
    csrf = csrf_token() if callable(csrf_token) else (csrf_token or "")
    cal_slug = getattr(calendar, "slug", "")
    desc = getattr(calendar, "description", "") or ""
    hx_select = ctx.get("hx_select_search", "#main-panel")

    desc_edit_url = url_for("calendar.admin.calendar_description_edit", calendar_slug=cal_slug)
    description_html = _calendar_description_display_html(calendar, desc_edit_url)

    return sx_call("events-calendar-admin-panel",
                  description_content=SxExpr(description_html), csrf=csrf,
                  description=desc)


def _calendar_description_display_html(calendar, edit_url: str) -> str:
    """Render calendar description display with edit button."""
    desc = getattr(calendar, "description", "") or ""
    return sx_call("events-calendar-description-display",
                  description=desc, edit_url=edit_url)


# ---------------------------------------------------------------------------
# Markets main panel
# ---------------------------------------------------------------------------

def _markets_main_panel_html(ctx: dict) -> str:
    """Render markets list + create form panel."""
    from quart import url_for
    rights = ctx.get("rights") or {}
    is_admin = getattr(rights, "admin", False) if hasattr(rights, "admin") else rights.get("admin", False)
    has_access = ctx.get("has_access")
    can_create = has_access("markets.create_market") if callable(has_access) else is_admin
    csrf_token = ctx.get("csrf_token")
    csrf = csrf_token() if callable(csrf_token) else (csrf_token or "")
    markets = ctx.get("markets") or []

    form_html = ""
    if can_create:
        create_url = url_for("markets.create_market")
        form_html = sx_call("crud-create-form",
                           create_url=create_url, csrf=csrf,
                           errors_id="market-create-errors", list_id="markets-list",
                           placeholder="e.g. Farm Shop, Bakery", btn_label="Add market")

    list_html = _markets_list_html(ctx, markets)
    return sx_call("crud-panel",
                  form=SxExpr(form_html), list=SxExpr(list_html),
                  list_id="markets-list")


def _markets_list_html(ctx: dict, markets: list) -> str:
    """Render markets list items."""
    from quart import url_for
    csrf_token = ctx.get("csrf_token")
    csrf = csrf_token() if callable(csrf_token) else (csrf_token or "")
    post = ctx.get("post") or {}
    slug = post.get("slug", "")

    if not markets:
        return sx_call("empty-state", message="No markets yet. Create one above.",
                      cls="text-gray-500 mt-4")

    parts = []
    for m in markets:
        m_slug = getattr(m, "slug", "") if hasattr(m, "slug") else m.get("slug", "")
        m_name = getattr(m, "name", "") if hasattr(m, "name") else m.get("name", "")
        market_href = call_url(ctx, "market_url", f"/{slug}/{m_slug}/")
        del_url = url_for("markets.delete_market", market_slug=m_slug)
        csrf_hdr = f'{{"X-CSRFToken":"{csrf}"}}'
        parts.append(sx_call("crud-item",
                            href=market_href, name=m_name,
                            slug=m_slug, del_url=del_url,
                            csrf_hdr=csrf_hdr,
                            list_id="markets-list",
                            confirm_title="Delete market?",
                            confirm_text="Products will be hidden (soft delete)"))
    return "".join(parts)


# ---------------------------------------------------------------------------
# Ticket state badge helper
# ---------------------------------------------------------------------------

def _ticket_state_badge_html(state: str) -> str:
    """Render a ticket state badge."""
    cls_map = {
        "confirmed": "bg-emerald-100 text-emerald-800",
        "checked_in": "bg-blue-100 text-blue-800",
        "reserved": "bg-amber-100 text-amber-800",
        "cancelled": "bg-red-100 text-red-800",
    }
    cls = cls_map.get(state, "bg-stone-100 text-stone-700")
    label = (state or "").replace("_", " ").capitalize()
    return sx_call("badge", cls=cls, label=label)


# ---------------------------------------------------------------------------
# Tickets main panel (my tickets)
# ---------------------------------------------------------------------------

def _tickets_main_panel_html(ctx: dict, tickets: list) -> str:
    """Render my tickets list."""
    from quart import url_for

    ticket_cards = []
    if tickets:
        for ticket in tickets:
            href = url_for("tickets.defpage_ticket_detail", code=ticket.code)
            entry = getattr(ticket, "entry", None)
            entry_name = entry.name if entry else "Unknown event"
            tt = getattr(ticket, "ticket_type", None)
            state = getattr(ticket, "state", "")
            cal = getattr(entry, "calendar", None) if entry else None

            time_str = ""
            if entry and entry.start_at:
                time_str = entry.start_at.strftime("%A, %B %d, %Y at %H:%M")
                if entry.end_at:
                    time_str += f" \u2013 {entry.end_at.strftime('%H:%M')}"

            ticket_cards.append(sx_call("events-ticket-card",
                                       href=href, entry_name=entry_name,
                                       type_name=tt.name if tt else None,
                                       time_str=time_str or None,
                                       cal_name=cal.name if cal else None,
                                       badge=SxExpr(_ticket_state_badge_html(state)),
                                       code_prefix=ticket.code[:8]))

    cards_html = "".join(ticket_cards)
    return sx_call("events-tickets-panel",
                  list_container=_list_container(ctx),
                  has_tickets=bool(tickets), cards=SxExpr(cards_html))


# ---------------------------------------------------------------------------
# Ticket detail panel
# ---------------------------------------------------------------------------

def _ticket_detail_panel_html(ctx: dict, ticket) -> str:
    """Render a single ticket detail with QR code."""
    from quart import url_for

    entry = getattr(ticket, "entry", None)
    tt = getattr(ticket, "ticket_type", None)
    state = getattr(ticket, "state", "")
    code = ticket.code
    cal = getattr(entry, "calendar", None) if entry else None
    checked_in_at = getattr(ticket, "checked_in_at", None)

    bg_map = {"confirmed": "bg-emerald-50", "checked_in": "bg-blue-50", "reserved": "bg-amber-50"}
    header_bg = bg_map.get(state, "bg-stone-50")
    entry_name = entry.name if entry else "Ticket"
    back_href = url_for("tickets.defpage_my_tickets")

    # Badge with larger sizing
    badge = _ticket_state_badge_html(state).replace('px-2 py-0.5 text-xs', 'px-3 py-1 text-sm')

    # Time info
    time_date = entry.start_at.strftime("%A, %B %d, %Y") if entry and entry.start_at else None
    time_range = entry.start_at.strftime("%H:%M") if entry and entry.start_at else None
    if time_range and entry.end_at:
        time_range += f" \u2013 {entry.end_at.strftime('%H:%M')}"

    tt_desc = f"{tt.name} \u2014 \u00a3{tt.cost:.2f}" if tt and getattr(tt, "cost", None) else None
    checkin_str = checked_in_at.strftime("Checked in: %B %d, %Y at %H:%M") if checked_in_at else None

    qr_script = (
        f"(function(){{var c=document.getElementById('ticket-qr-{code}');"
        "if(c&&typeof QRCode!=='undefined'){"
        "var cv=document.createElement('canvas');"
        f"QRCode.toCanvas(cv,'{code}',{{width:200,margin:2,color:{{dark:'#1c1917',light:'#ffffff'}}}},function(e){{if(!e)c.appendChild(cv)}});"
        "}})()"
    )

    return sx_call("events-ticket-detail",
                  list_container=_list_container(ctx), back_href=back_href,
                  header_bg=header_bg, entry_name=entry_name,
                  badge=SxExpr(badge), type_name=tt.name if tt else None,
                  code=code, time_date=time_date, time_range=time_range,
                  cal_name=cal.name if cal else None,
                  type_desc=tt_desc, checkin_str=checkin_str,
                  qr_script=qr_script)


# ---------------------------------------------------------------------------
# Ticket admin main panel
# ---------------------------------------------------------------------------

def _ticket_admin_main_panel_html(ctx: dict, tickets: list, stats: dict) -> str:
    """Render ticket admin dashboard."""
    from quart import url_for
    csrf_token = ctx.get("csrf_token")
    csrf = csrf_token() if callable(csrf_token) else (csrf_token or "")
    lookup_url = url_for("ticket_admin.lookup")

    # Stats cards
    stats_html = ""
    for label, key, border, bg, text_cls in [
        ("Total", "total", "border-stone-200", "", "text-stone-900"),
        ("Confirmed", "confirmed", "border-emerald-200", "bg-emerald-50", "text-emerald-700"),
        ("Checked In", "checked_in", "border-blue-200", "bg-blue-50", "text-blue-700"),
        ("Reserved", "reserved", "border-amber-200", "bg-amber-50", "text-amber-700"),
    ]:
        val = stats.get(key, 0)
        lbl_cls = text_cls.replace("700", "600").replace("900", "500") if "stone" not in text_cls else "text-stone-500"
        stats_html += sx_call("events-ticket-admin-stat",
                             border=border, bg=bg, text_cls=text_cls,
                             label_cls=lbl_cls, value=str(val), label=label)

    # Ticket rows
    rows_html = ""
    for ticket in tickets:
        entry = getattr(ticket, "entry", None)
        tt = getattr(ticket, "ticket_type", None)
        state = getattr(ticket, "state", "")
        code = ticket.code

        date_html = ""
        if entry and entry.start_at:
            date_html = sx_call("events-ticket-admin-date",
                               date_str=entry.start_at.strftime("%d %b %Y, %H:%M"))

        action_html = ""
        if state in ("confirmed", "reserved"):
            checkin_url = url_for("ticket_admin.do_checkin", code=code)
            action_html = sx_call("events-ticket-admin-checkin-form",
                                 checkin_url=checkin_url, code=code, csrf=csrf)
        elif state == "checked_in":
            checked_in_at = getattr(ticket, "checked_in_at", None)
            t_str = checked_in_at.strftime("%H:%M") if checked_in_at else ""
            action_html = sx_call("events-ticket-admin-checked-in",
                                 time_str=t_str)

        rows_html += sx_call("events-ticket-admin-row",
                            code=code, code_short=code[:12] + "...",
                            entry_name=entry.name if entry else "\u2014",
                            date=SxExpr(date_html),
                            type_name=tt.name if tt else "\u2014",
                            badge=SxExpr(_ticket_state_badge_html(state)),
                            action=SxExpr(action_html))

    return sx_call("events-ticket-admin-panel",
                  list_container=_list_container(ctx), stats=SxExpr(stats_html),
                  lookup_url=lookup_url, has_tickets=bool(tickets),
                  rows=SxExpr(rows_html))


# ---------------------------------------------------------------------------
# All events / page summary entry cards
# ---------------------------------------------------------------------------

def _entry_card_html(entry, page_info: dict, pending_tickets: dict,
                     ticket_url: str, events_url_fn, *, is_page_scoped: bool = False,
                     post: dict | None = None) -> str:
    """Render a list card for one event entry."""
    pi = page_info.get(getattr(entry, "calendar_container_id", 0), {})
    if is_page_scoped and post:
        page_slug = pi.get("slug", post.get("slug", ""))
    else:
        page_slug = pi.get("slug", "")
    page_title = pi.get("title")

    day_href = ""
    if page_slug and entry.start_at:
        day_href = events_url_fn(f"/{page_slug}/{entry.calendar_slug}/day/{entry.start_at.strftime('%Y/%-m/%-d')}/")
    entry_href = f"{day_href}entries/{entry.id}/" if day_href else ""

    # Title (linked or plain)
    if entry_href:
        title_html = sx_call("events-entry-title-linked",
                            href=entry_href, name=entry.name)
    else:
        title_html = sx_call("events-entry-title-plain", name=entry.name)

    # Badges
    badges_html = ""
    if page_title and (not is_page_scoped or page_title != (post or {}).get("title")):
        page_href = events_url_fn(f"/{page_slug}/")
        badges_html += sx_call("events-entry-page-badge",
                              href=page_href, title=page_title)
    cal_name = getattr(entry, "calendar_name", "")
    if cal_name:
        badges_html += sx_call("events-entry-cal-badge", name=cal_name)

    # Time line
    time_parts = ""
    if day_href and not is_page_scoped:
        time_parts += sx_call("events-entry-time-linked",
                             href=day_href,
                             date_str=entry.start_at.strftime("%a %-d %b"))
    elif not is_page_scoped:
        time_parts += sx_call("events-entry-time-plain",
                             date_str=entry.start_at.strftime("%a %-d %b"))
    time_parts += entry.start_at.strftime("%H:%M")
    if entry.end_at:
        time_parts += f' \u2013 {entry.end_at.strftime("%H:%M")}'

    cost = getattr(entry, "cost", None)
    cost_html = sx_call("events-entry-cost",
                       cost=f"£{cost:.2f}") if cost else ""

    # Ticket widget
    tp = getattr(entry, "ticket_price", None)
    widget_html = ""
    if tp is not None:
        qty = pending_tickets.get(entry.id, 0)
        widget_html = sx_call("events-entry-widget-wrapper",
                             widget=SxExpr(_ticket_widget_html(entry, qty, ticket_url, ctx={})))

    return sx_call("events-entry-card",
                  title=SxExpr(title_html), badges=SxExpr(badges_html),
                  time_parts=SxExpr(time_parts), cost=SxExpr(cost_html),
                  widget=SxExpr(widget_html))


def _entry_card_tile_html(entry, page_info: dict, pending_tickets: dict,
                          ticket_url: str, events_url_fn, *, is_page_scoped: bool = False,
                          post: dict | None = None) -> str:
    """Render a tile card for one event entry."""
    pi = page_info.get(getattr(entry, "calendar_container_id", 0), {})
    if is_page_scoped and post:
        page_slug = pi.get("slug", post.get("slug", ""))
    else:
        page_slug = pi.get("slug", "")
    page_title = pi.get("title")

    day_href = ""
    if page_slug and entry.start_at:
        day_href = events_url_fn(f"/{page_slug}/{entry.calendar_slug}/day/{entry.start_at.strftime('%Y/%-m/%-d')}/")
    entry_href = f"{day_href}entries/{entry.id}/" if day_href else ""

    # Title
    if entry_href:
        title_html = sx_call("events-entry-title-tile-linked",
                            href=entry_href, name=entry.name)
    else:
        title_html = sx_call("events-entry-title-tile-plain", name=entry.name)

    # Badges
    badges_html = ""
    if page_title and (not is_page_scoped or page_title != (post or {}).get("title")):
        page_href = events_url_fn(f"/{page_slug}/")
        badges_html += sx_call("events-entry-page-badge",
                              href=page_href, title=page_title)
    cal_name = getattr(entry, "calendar_name", "")
    if cal_name:
        badges_html += sx_call("events-entry-cal-badge", name=cal_name)

    # Time
    time_html = ""
    if day_href:
        time_html += sx_call("events-entry-time-linked",
                            href=day_href,
                            date_str=entry.start_at.strftime("%a %-d %b")).replace(" &middot; ", "")
    else:
        time_html += entry.start_at.strftime("%a %-d %b")
    time_html += f' \u00b7 {entry.start_at.strftime("%H:%M")}'
    if entry.end_at:
        time_html += f' \u2013 {entry.end_at.strftime("%H:%M")}'

    cost = getattr(entry, "cost", None)
    cost_html = sx_call("events-entry-cost",
                       cost=f"£{cost:.2f}") if cost else ""

    # Ticket widget
    tp = getattr(entry, "ticket_price", None)
    widget_html = ""
    if tp is not None:
        qty = pending_tickets.get(entry.id, 0)
        widget_html = sx_call("events-entry-tile-widget-wrapper",
                             widget=SxExpr(_ticket_widget_html(entry, qty, ticket_url, ctx={})))

    return sx_call("events-entry-card-tile",
                  title=SxExpr(title_html), badges=SxExpr(badges_html),
                  time=SxExpr(time_html), cost=SxExpr(cost_html),
                  widget=SxExpr(widget_html))


def _ticket_widget_html(entry, qty: int, ticket_url: str, *, ctx: dict) -> str:
    """Render the inline +/- ticket widget."""
    csrf_token_val = ""
    if ctx:
        ct = ctx.get("csrf_token")
        csrf_token_val = ct() if callable(ct) else (ct or "")
    else:
        try:
            from flask_wtf.csrf import generate_csrf
            csrf_token_val = generate_csrf()
        except Exception:
            pass

    eid = entry.id
    tp = getattr(entry, "ticket_price", 0) or 0
    tgt = f"#page-ticket-{eid}"

    def _tw_form(count_val, btn_html):
        return sx_call("events-tw-form",
                      ticket_url=ticket_url, target=tgt,
                      csrf=csrf_token_val, entry_id=str(eid),
                      count_val=str(count_val), btn=SxExpr(btn_html))

    if qty == 0:
        inner = _tw_form(1, sx_call("events-tw-cart-plus"))
    else:
        minus = _tw_form(qty - 1, sx_call("events-tw-minus"))
        cart_icon = sx_call("events-tw-cart-icon", qty=str(qty))
        plus = _tw_form(qty + 1, sx_call("events-tw-plus"))
        inner = minus + cart_icon + plus

    return sx_call("events-tw-widget",
                  entry_id=str(eid), price=f"£{tp:.2f}",
                  inner=SxExpr(inner))


def _entry_cards_html(entries, page_info, pending_tickets, ticket_url,
                      events_url_fn, view, page, has_more, next_url,
                      *, is_page_scoped=False, post=None) -> str:
    """Render entry cards (list or tile) with sentinel."""
    parts = []
    last_date = None
    for entry in entries:
        if view == "tile":
            parts.append(_entry_card_tile_html(
                entry, page_info, pending_tickets, ticket_url, events_url_fn,
                is_page_scoped=is_page_scoped, post=post,
            ))
        else:
            entry_date = entry.start_at.strftime("%A %-d %B %Y") if entry.start_at else ""
            if entry_date != last_date:
                parts.append(sx_call("events-date-separator",
                                    date_str=entry_date))
                last_date = entry_date
            parts.append(_entry_card_html(
                entry, page_info, pending_tickets, ticket_url, events_url_fn,
                is_page_scoped=is_page_scoped, post=post,
            ))

    if has_more:
        parts.append(sx_call("sentinel-simple",
                            id=f"sentinel-{page}", next_url=next_url))
    return "".join(parts)


# ---------------------------------------------------------------------------
# All events / page summary main panels
# ---------------------------------------------------------------------------

_LIST_SVG = None
_TILE_SVG = None


def _get_list_svg():
    global _LIST_SVG
    if _LIST_SVG is None:
        _LIST_SVG = sx_call("list-svg")
    return _LIST_SVG


def _get_tile_svg():
    global _TILE_SVG
    if _TILE_SVG is None:
        _TILE_SVG = sx_call("tile-svg")
    return _TILE_SVG


def _view_toggle_html(ctx: dict, view: str) -> str:
    """Render the list/tile view toggle bar."""
    from shared.utils import route_prefix
    prefix = route_prefix()
    clh = ctx.get("current_local_href", "/")
    hx_select = ctx.get("hx_select_search", "#main-panel")

    list_href = prefix + str(clh)
    tile_href = prefix + str(clh)
    if "?" in list_href:
        list_href = list_href.split("?")[0]
    if "?" in tile_href:
        tile_href = tile_href.split("?")[0] + "?view=tile"
    else:
        tile_href = tile_href + "?view=tile"

    list_active = 'bg-stone-200 text-stone-800' if view != 'tile' else 'text-stone-400 hover:text-stone-600'
    tile_active = 'bg-stone-200 text-stone-800' if view == 'tile' else 'text-stone-400 hover:text-stone-600'

    return sx_call("view-toggle",
                  list_href=list_href, tile_href=tile_href,
                  hx_select=hx_select, list_cls=list_active,
                  tile_cls=tile_active, storage_key="events_view",
                  list_svg=SxExpr(_get_list_svg()), tile_svg=SxExpr(_get_tile_svg()))


def _events_main_panel_html(ctx: dict, entries, has_more, pending_tickets, page_info,
                             page, view, ticket_url, next_url, events_url_fn,
                             *, is_page_scoped=False, post=None) -> str:
    """Render the events main panel with view toggle + cards."""
    toggle = _view_toggle_html(ctx, view)

    if entries:
        cards = _entry_cards_html(
            entries, page_info, pending_tickets, ticket_url, events_url_fn,
            view, page, has_more, next_url,
            is_page_scoped=is_page_scoped, post=post,
        )
        grid_cls = ("max-w-full px-3 py-3 grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4"
                    if view == "tile" else "max-w-full px-3 py-3 space-y-3")
        body = sx_call("events-grid", grid_cls=grid_cls, cards=SxExpr(cards))
    else:
        body = sx_call("empty-state", icon="fa fa-calendar-xmark",
                      message="No upcoming events",
                      cls="px-3 py-12 text-center text-stone-400")

    return sx_call("events-main-panel-body",
                  toggle=SxExpr(toggle), body=SxExpr(body))


# ---------------------------------------------------------------------------
# Utility
# ---------------------------------------------------------------------------

def _list_container(ctx: dict) -> str:
    styles = ctx.get("styles") or {}
    return getattr(styles, "list_container", "") if hasattr(styles, "list_container") else styles.get("list_container", "")


# ===========================================================================
# PUBLIC API
# ===========================================================================


# ---------------------------------------------------------------------------
# All events
# ---------------------------------------------------------------------------

async def render_all_events_page(ctx: dict, entries, has_more, pending_tickets,
                                  page_info, page, view) -> str:
    """Full page: all events listing."""
    from quart import url_for
    from shared.utils import route_prefix
    from shared.infrastructure.urls import events_url

    prefix = route_prefix()
    view_param = f"&view={view}" if view != "list" else ""
    ticket_url = url_for("all_events.adjust_ticket")
    next_url = prefix + url_for("all_events.entries_fragment", page=page + 1) + (f"?view={view}" if view != "list" else "")

    content = _events_main_panel_html(
        ctx, entries, has_more, pending_tickets, page_info,
        page, view, ticket_url, next_url, events_url,
    )
    hdr = root_header_sx(ctx)
    return full_page_sx(ctx, header_rows=hdr, content=content)


async def render_all_events_oob(ctx: dict, entries, has_more, pending_tickets,
                                 page_info, page, view) -> str:
    """OOB response: all events listing (htmx nav)."""
    from quart import url_for
    from shared.utils import route_prefix
    from shared.infrastructure.urls import events_url

    prefix = route_prefix()
    ticket_url = url_for("all_events.adjust_ticket")
    next_url = prefix + url_for("all_events.entries_fragment", page=page + 1) + (f"?view={view}" if view != "list" else "")

    content = _events_main_panel_html(
        ctx, entries, has_more, pending_tickets, page_info,
        page, view, ticket_url, next_url, events_url,
    )
    return oob_page_sx(content=content)


async def render_all_events_cards(entries, has_more, pending_tickets,
                                   page_info, page, view) -> str:
    """Pagination fragment: all events cards only."""
    from quart import url_for
    from shared.utils import route_prefix
    from shared.infrastructure.urls import events_url

    prefix = route_prefix()
    ticket_url = url_for("all_events.adjust_ticket")
    next_url = prefix + url_for("all_events.entries_fragment", page=page + 1) + (f"?view={view}" if view != "list" else "")

    return _entry_cards_html(
        entries, page_info, pending_tickets, ticket_url, events_url,
        view, page, has_more, next_url,
    )


# ---------------------------------------------------------------------------
# Page summary
# ---------------------------------------------------------------------------

async def render_page_summary_page(ctx: dict, entries, has_more, pending_tickets,
                                    page_info, page, view) -> str:
    """Full page: page-scoped events listing."""
    from quart import url_for
    from shared.utils import route_prefix
    from shared.infrastructure.urls import events_url

    prefix = route_prefix()
    post = ctx.get("post") or {}
    ticket_url = url_for("page_summary.adjust_ticket")
    next_url = prefix + url_for("page_summary.entries_fragment", page=page + 1) + (f"?view={view}" if view != "list" else "")

    content = _events_main_panel_html(
        ctx, entries, has_more, pending_tickets, page_info,
        page, view, ticket_url, next_url, events_url,
        is_page_scoped=True, post=post,
    )

    hdr = root_header_sx(ctx)
    hdr += header_child_sx(_post_header_sx(ctx))
    return full_page_sx(ctx, header_rows=hdr, content=content)


async def render_page_summary_oob(ctx: dict, entries, has_more, pending_tickets,
                                   page_info, page, view) -> str:
    """OOB response: page-scoped events (htmx nav)."""
    from quart import url_for
    from shared.utils import route_prefix
    from shared.infrastructure.urls import events_url

    prefix = route_prefix()
    post = ctx.get("post") or {}
    ticket_url = url_for("page_summary.adjust_ticket")
    next_url = prefix + url_for("page_summary.entries_fragment", page=page + 1) + (f"?view={view}" if view != "list" else "")

    content = _events_main_panel_html(
        ctx, entries, has_more, pending_tickets, page_info,
        page, view, ticket_url, next_url, events_url,
        is_page_scoped=True, post=post,
    )

    oobs = _post_header_sx(ctx, oob=True)
    oobs += _clear_deeper_oob("post-row", "post-header-child")
    return oob_page_sx(oobs=oobs, content=content)


async def render_page_summary_cards(entries, has_more, pending_tickets,
                                     page_info, page, view, post) -> str:
    """Pagination fragment: page-scoped events cards only."""
    from quart import url_for
    from shared.utils import route_prefix
    from shared.infrastructure.urls import events_url

    prefix = route_prefix()
    ticket_url = url_for("page_summary.adjust_ticket")
    next_url = prefix + url_for("page_summary.entries_fragment", page=page + 1) + (f"?view={view}" if view != "list" else "")

    return _entry_cards_html(
        entries, page_info, pending_tickets, ticket_url, events_url,
        view, page, has_more, next_url,
        is_page_scoped=True, post=post,
    )


# ---------------------------------------------------------------------------
# Calendars home
# ---------------------------------------------------------------------------

async def render_calendars_page(ctx: dict) -> str:
    """Full page: calendars listing."""
    content = _calendars_main_panel_sx(ctx)
    ctx = await _ensure_container_nav(ctx)
    slug = (ctx.get("post") or {}).get("slug", "")
    root_hdr = root_header_sx(ctx)
    post_hdr = _post_header_sx(ctx)
    admin_hdr = post_admin_header_sx(ctx, slug, selected="calendars")
    return full_page_sx(ctx, header_rows=root_hdr + post_hdr + admin_hdr, content=content)


async def render_calendars_oob(ctx: dict) -> str:
    """OOB response: calendars listing."""
    content = _calendars_main_panel_sx(ctx)
    ctx = await _ensure_container_nav(ctx)
    slug = (ctx.get("post") or {}).get("slug", "")
    oobs = post_admin_header_sx(ctx, slug, oob=True, selected="calendars")
    oobs += _clear_deeper_oob("post-row", "post-header-child",
                               "post-admin-row", "post-admin-header-child")
    return oob_page_sx(oobs=oobs, content=content)


# ---------------------------------------------------------------------------
# Calendar month view
# ---------------------------------------------------------------------------

async def render_calendar_page(ctx: dict) -> str:
    """Full page: calendar month view."""
    content = _calendar_main_panel_html(ctx)
    hdr = root_header_sx(ctx)
    child = _post_header_sx(ctx) + _calendar_header_sx(ctx)
    hdr += header_child_sx(child)
    return full_page_sx(ctx, header_rows=hdr, content=content)


async def render_calendar_oob(ctx: dict) -> str:
    """OOB response: calendar month view."""
    content = _calendar_main_panel_html(ctx)
    oobs = _post_header_sx(ctx, oob=True)
    oobs += oob_header_sx("post-header-child", "calendar-header-child",
                              _calendar_header_sx(ctx))
    oobs += _clear_deeper_oob("post-row", "post-header-child",
                               "calendar-row", "calendar-header-child")
    return oob_page_sx(oobs=oobs, content=content)


# ---------------------------------------------------------------------------
# Day detail
# ---------------------------------------------------------------------------

async def render_day_page(ctx: dict) -> str:
    """Full page: day detail."""
    content = _day_main_panel_html(ctx)
    hdr = root_header_sx(ctx)
    child = (_post_header_sx(ctx)
             + _calendar_header_sx(ctx) + _day_header_sx(ctx))
    hdr += header_child_sx(child)
    return full_page_sx(ctx, header_rows=hdr, content=content)


async def render_day_oob(ctx: dict) -> str:
    """OOB response: day detail."""
    content = _day_main_panel_html(ctx)
    oobs = _calendar_header_sx(ctx, oob=True)
    oobs += oob_header_sx("calendar-header-child", "day-header-child",
                              _day_header_sx(ctx))
    oobs += _clear_deeper_oob("post-row", "post-header-child",
                               "calendar-row", "calendar-header-child",
                               "day-row", "day-header-child")
    return oob_page_sx(oobs=oobs, content=content)


# ---------------------------------------------------------------------------
# Calendar admin helper
# ---------------------------------------------------------------------------

def _events_post_admin_header_sx(ctx: dict, *, oob: bool = False,
                                    selected: str = "") -> str:
    """Post-level admin row for events — delegates to shared helper."""
    slug = (ctx.get("post") or {}).get("slug", "")
    return post_admin_header_sx(ctx, slug, oob=oob, selected=selected)


# ===========================================================================
# POST / PUT / DELETE response components
# ===========================================================================


# ---------------------------------------------------------------------------
# Ticket widget (public wrapper for _ticket_widget_html)
# ---------------------------------------------------------------------------

def render_ticket_widget(entry, qty: int, ticket_url: str) -> str:
    """Render the +/- ticket widget for page_summary / all_events adjust_ticket."""
    return _ticket_widget_html(entry, qty, ticket_url, ctx={})


# ---------------------------------------------------------------------------
# Ticket admin: checkin result
# ---------------------------------------------------------------------------

def render_checkin_result(success: bool, error: str | None, ticket) -> str:
    """Render checkin result: table row on success, error div on failure."""
    if not success:
        return sx_call("events-checkin-error",
                      message=error or "Check-in failed")
    if not ticket:
        return ""
    code = ticket.code
    entry = getattr(ticket, "entry", None)
    tt = getattr(ticket, "ticket_type", None)
    checked_in_at = getattr(ticket, "checked_in_at", None)
    time_str = checked_in_at.strftime("%H:%M") if checked_in_at else "Just now"

    date_html = ""
    if entry and entry.start_at:
        date_html = sx_call("events-ticket-admin-date",
                           date_str=entry.start_at.strftime("%d %b %Y, %H:%M"))

    return sx_call("events-checkin-success-row",
                  code=code, code_short=code[:12] + "...",
                  entry_name=entry.name if entry else "\u2014",
                  date=SxExpr(date_html),
                  type_name=tt.name if tt else "\u2014",
                  badge=SxExpr(_ticket_state_badge_html("checked_in")),
                  time_str=time_str)


# ---------------------------------------------------------------------------
# Ticket admin: lookup result
# ---------------------------------------------------------------------------

def render_lookup_result(ticket, error: str | None) -> str:
    """Render ticket lookup result: error div or ticket info card."""
    from quart import url_for
    from shared.browser.app.csrf import generate_csrf_token

    if error:
        return sx_call("events-lookup-error", message=error)
    if not ticket:
        return ""

    entry = getattr(ticket, "entry", None)
    tt = getattr(ticket, "ticket_type", None)
    state = getattr(ticket, "state", "")
    code = ticket.code
    checked_in_at = getattr(ticket, "checked_in_at", None)
    csrf = generate_csrf_token()

    # Info section
    info_html = sx_call("events-lookup-info",
                       entry_name=entry.name if entry else "Unknown event")
    if tt:
        info_html += sx_call("events-lookup-type", type_name=tt.name)
    if entry and entry.start_at:
        info_html += sx_call("events-lookup-date",
                            date_str=entry.start_at.strftime("%A, %B %d, %Y at %H:%M"))
    cal = getattr(entry, "calendar", None) if entry else None
    if cal:
        info_html += sx_call("events-lookup-cal", cal_name=cal.name)
    info_html += sx_call("events-lookup-status",
                        badge=SxExpr(_ticket_state_badge_html(state)), code=code)
    if checked_in_at:
        info_html += sx_call("events-lookup-checkin-time",
                            date_str=checked_in_at.strftime("%B %d, %Y at %H:%M"))

    # Action area
    action_html = ""
    if state in ("confirmed", "reserved"):
        checkin_url = url_for("ticket_admin.do_checkin", code=code)
        action_html = sx_call("events-lookup-checkin-btn",
                             checkin_url=checkin_url, code=code, csrf=csrf)
    elif state == "checked_in":
        action_html = sx_call("events-lookup-checked-in")
    elif state == "cancelled":
        action_html = sx_call("events-lookup-cancelled")

    return sx_call("events-lookup-card",
                  info=SxExpr(info_html), code=code, action=SxExpr(action_html))


# ---------------------------------------------------------------------------
# Ticket admin: entry tickets table
# ---------------------------------------------------------------------------

def render_entry_tickets_admin(entry, tickets: list) -> str:
    """Render admin ticket table for a specific entry."""
    from quart import url_for
    from shared.browser.app.csrf import generate_csrf_token
    csrf = generate_csrf_token()

    count = len(tickets)
    suffix = "s" if count != 1 else ""

    rows_html = ""
    for ticket in tickets:
        tt = getattr(ticket, "ticket_type", None)
        state = getattr(ticket, "state", "")
        code = ticket.code
        checked_in_at = getattr(ticket, "checked_in_at", None)

        action_html = ""
        if state in ("confirmed", "reserved"):
            checkin_url = url_for("ticket_admin.do_checkin", code=code)
            action_html = sx_call("events-entry-tickets-admin-checkin",
                                 checkin_url=checkin_url, code=code, csrf=csrf)
        elif state == "checked_in":
            t_str = checked_in_at.strftime("%H:%M") if checked_in_at else ""
            action_html = sx_call("events-ticket-admin-checked-in",
                                 time_str=t_str)

        rows_html += sx_call("events-entry-tickets-admin-row",
                            code=code, code_short=code[:12] + "...",
                            type_name=tt.name if tt else "\u2014",
                            badge=SxExpr(_ticket_state_badge_html(state)),
                            action=SxExpr(action_html))

    if tickets:
        body_html = sx_call("events-entry-tickets-admin-table",
                           rows=SxExpr(rows_html))
    else:
        body_html = sx_call("events-entry-tickets-admin-empty")

    return sx_call("events-entry-tickets-admin-panel",
                  entry_name=entry.name,
                  count_label=f"{count} ticket{suffix}",
                  body=SxExpr(body_html))


# ---------------------------------------------------------------------------
# Day main panel -- public API
# ---------------------------------------------------------------------------

def render_day_main_panel(ctx: dict) -> str:
    """Public wrapper for day main panel rendering."""
    return _day_main_panel_html(ctx)


# ---------------------------------------------------------------------------
# Entry main panel
# ---------------------------------------------------------------------------

def _entry_main_panel_html(ctx: dict) -> str:
    """Render the entry detail panel (name, slot, time, state, cost, tickets,
    buy form, date, posts, options + edit button)."""
    from quart import url_for

    entry = ctx.get("entry")
    if not entry:
        return ""

    calendar = ctx.get("calendar")
    cal_slug = getattr(calendar, "slug", "") if calendar else ""
    day = ctx.get("day")
    month = ctx.get("month")
    year = ctx.get("year")
    styles = ctx.get("styles") or {}
    list_container = getattr(styles, "list_container", "") if hasattr(styles, "list_container") else styles.get("list_container", "")
    pre_action = getattr(styles, "pre_action_button", "") if hasattr(styles, "pre_action_button") else styles.get("pre_action_button", "")

    eid = entry.id
    state = getattr(entry, "state", "pending") or "pending"

    def _field(label, content_html):
        return sx_call("events-entry-field", label=label, content=SxExpr(content_html))

    # Name
    name_html = _field("Name", sx_call("events-entry-name-field", name=entry.name))

    # Slot
    slot = getattr(entry, "slot", None)
    if slot:
        flex_label = "(flexible)" if getattr(slot, "flexible", False) else "(fixed)"
        slot_inner = sx_call("events-entry-slot-assigned",
                            slot_name=slot.name, flex_label=flex_label)
    else:
        slot_inner = sx_call("events-entry-slot-none")
    slot_html = _field("Slot", slot_inner)

    # Time Period
    start_str = entry.start_at.strftime("%H:%M") if entry.start_at else ""
    end_str = f" \u2013 {entry.end_at.strftime('%H:%M')}" if entry.end_at else " \u2013 open-ended"
    time_html = _field("Time Period", sx_call("events-entry-time-field",
                                              time_str=start_str + end_str))

    # State
    state_html = _field("State", sx_call("events-entry-state-field",
                                         entry_id=str(eid),
                                         badge=SxExpr(_entry_state_badge_html(state))))

    # Cost
    cost = getattr(entry, "cost", None)
    cost_str = f"{cost:.2f}" if cost is not None else "0.00"
    cost_html = _field("Cost", sx_call("events-entry-cost-field",
                                       cost=f"£{cost_str}"))

    # Ticket Configuration (admin)
    tickets_html = _field("Tickets", sx_call("events-entry-tickets-field",
                                             entry_id=str(eid),
                                             tickets_config=SxExpr(render_entry_tickets_config(entry, calendar, day, month, year))))

    # Buy Tickets (public-facing)
    ticket_remaining = ctx.get("ticket_remaining")
    ticket_sold_count = ctx.get("ticket_sold_count", 0)
    user_ticket_count = ctx.get("user_ticket_count", 0)
    user_ticket_counts_by_type = ctx.get("user_ticket_counts_by_type") or {}
    buy_html = render_buy_form(
        entry, ticket_remaining, ticket_sold_count,
        user_ticket_count, user_ticket_counts_by_type,
    )

    # Date
    date_str = entry.start_at.strftime("%A, %B %d, %Y") if entry.start_at else ""
    date_html = _field("Date", sx_call("events-entry-date-field", date_str=date_str))

    # Associated Posts
    entry_posts = ctx.get("entry_posts") or []
    posts_html = _field("Associated Posts", sx_call("events-entry-posts-field",
                                                    entry_id=str(eid),
                                                    posts_panel=SxExpr(render_entry_posts_panel(entry_posts, entry, calendar, day, month, year))))

    # Options and Edit Button
    edit_url = url_for(
        "calendar.day.calendar_entries.calendar_entry.get_edit",
        entry_id=eid, calendar_slug=cal_slug,
        day=day, month=month, year=year,
    )

    return sx_call("events-entry-panel",
                  entry_id=str(eid), list_container=list_container,
                  name=SxExpr(name_html), slot=SxExpr(slot_html),
                  time=SxExpr(time_html), state=SxExpr(state_html),
                  cost=SxExpr(cost_html), tickets=SxExpr(tickets_html),
                  buy=SxExpr(buy_html), date=SxExpr(date_html),
                  posts=SxExpr(posts_html),
                  options=SxExpr(_entry_options_html(entry, calendar, day, month, year)),
                  pre_action=pre_action, edit_url=edit_url)


# ---------------------------------------------------------------------------
# Entry header row
# ---------------------------------------------------------------------------

def _entry_header_html(ctx: dict, *, oob: bool = False) -> str:
    """Build entry detail header row."""
    from quart import url_for

    calendar = ctx.get("calendar")
    if not calendar:
        return ""
    cal_slug = getattr(calendar, "slug", "")
    entry = ctx.get("entry")
    if not entry:
        return ""
    day = ctx.get("day")
    month = ctx.get("month")
    year = ctx.get("year")

    link_href = url_for(
        "calendar.day.calendar_entries.calendar_entry.get",
        calendar_slug=cal_slug,
        year=year, month=month, day=day,
        entry_id=entry.id,
    )
    label_html = sx_call("events-entry-label",
                        entry_id=str(entry.id),
                        title=SxExpr(_entry_title_html(entry)),
                        times=SxExpr(_entry_times_html(entry)))

    nav_html = _entry_nav_html(ctx)

    return sx_call("menu-row-sx", id="entry-row", level=5,
                  link_href=link_href, link_label_content=SxExpr(label_html),
                  nav=SxExpr(nav_html) if nav_html else None, child_id="entry-header-child", oob=oob)


def _entry_times_html(entry) -> str:
    """Render entry times label."""
    start = entry.start_at
    end = entry.end_at
    if not start:
        return ""
    start_str = start.strftime("%H:%M")
    end_str = f" \u2192 {end.strftime('%H:%M')}" if end else ""
    return sx_call("events-entry-times", time_str=start_str + end_str)


# ---------------------------------------------------------------------------
# Entry nav (desktop + admin link)
# ---------------------------------------------------------------------------

def _entry_nav_html(ctx: dict) -> str:
    """Entry desktop nav: associated posts scrolling menu + admin link."""
    from quart import url_for

    calendar = ctx.get("calendar")
    if not calendar:
        return ""
    cal_slug = getattr(calendar, "slug", "")
    entry = ctx.get("entry")
    if not entry:
        return ""
    day = ctx.get("day")
    month = ctx.get("month")
    year = ctx.get("year")
    entry_posts = ctx.get("entry_posts") or []
    rights = ctx.get("rights") or {}
    is_admin = getattr(rights, "admin", False) if hasattr(rights, "admin") else rights.get("admin", False)

    blog_url_fn = ctx.get("blog_url")

    parts = []

    # Associated Posts scrolling menu
    if entry_posts:
        post_links = ""
        for ep in entry_posts:
            slug = getattr(ep, "slug", "")
            title = getattr(ep, "title", "")
            feat = getattr(ep, "feature_image", None)
            href = blog_url_fn(f"/{slug}/") if blog_url_fn else f"/{slug}/"
            if feat:
                img_html = sx_call("events-post-img", src=feat, alt=title)
            else:
                img_html = sx_call("events-post-img-placeholder")
            post_links += sx_call("events-entry-nav-post-link",
                                 href=href, img=SxExpr(img_html), title=title)
        parts.append(sx_call("events-entry-posts-nav-oob",
                            items=SxExpr(post_links)).replace(' :hx-swap-oob "true"', ''))

    # Admin link
    if is_admin:
        admin_url = url_for(
            "calendar.day.calendar_entries.calendar_entry.admin.admin",
            calendar_slug=cal_slug,
            day=day, month=month, year=year,
            entry_id=entry.id,
        )
        parts.append(sx_call("events-entry-admin-link", href=admin_url))

    return "".join(parts)


# ---------------------------------------------------------------------------
# Entry optioned (confirm/decline/provisional response)
# ---------------------------------------------------------------------------

def render_entry_optioned(entry, calendar, day, month, year) -> str:
    """Render entry options buttons + OOB title & state swaps."""
    options = _entry_options_html(entry, calendar, day, month, year)
    title = _entry_title_html(entry)
    state = _entry_state_badge_html(getattr(entry, "state", "pending") or "pending")

    return options + sx_call("events-entry-optioned-oob",
                            entry_id=str(entry.id),
                            title=SxExpr(title), state=SxExpr(state))


def _entry_title_html(entry) -> str:
    """Render entry title (icon + name + state badge)."""
    state = getattr(entry, "state", "pending") or "pending"
    return sx_call("events-entry-title",
                  name=entry.name,
                  badge=SxExpr(_entry_state_badge_html(state)))


def _entry_options_html(entry, calendar, day, month, year) -> str:
    """Render confirm/decline/provisional buttons based on entry state."""
    from quart import url_for, g
    from shared.browser.app.csrf import generate_csrf_token
    csrf = generate_csrf_token()

    styles = getattr(g, "styles", None) or {}
    action_btn = getattr(styles, "action_button", "") if hasattr(styles, "action_button") else styles.get("action_button", "")

    cal_slug = getattr(calendar, "slug", "")
    eid = entry.id
    state = getattr(entry, "state", "pending") or "pending"
    target = f"#calendar_entry_options_{eid}"

    def _make_button(action_name, label, confirm_title, confirm_text, *, trigger_type="submit"):
        url = url_for(
            f"calendar.day.calendar_entries.calendar_entry.{action_name}",
            calendar_slug=cal_slug, day=day, month=month, year=year, entry_id=eid,
        )
        btn_type = "button" if trigger_type == "button" else "submit"
        return sx_call("events-entry-option-button",
                      url=url, target=target, csrf=csrf, btn_type=btn_type,
                      action_btn=action_btn, confirm_title=confirm_title,
                      confirm_text=confirm_text, label=label,
                      is_btn=trigger_type == "button")

    buttons_html = ""
    if state == "provisional":
        buttons_html += _make_button(
            "confirm_entry", "confirm",
            "Confirm entry?", "Are you sure you want to confirm this entry?",
        )
        buttons_html += _make_button(
            "decline_entry", "decline",
            "Decline entry?", "Are you sure you want to decline this entry?",
        )
    elif state == "confirmed":
        buttons_html += _make_button(
            "provisional_entry", "provisional",
            "Provisional entry?", "Are you sure you want to provisional this entry?",
            trigger_type="button",
        )

    return sx_call("events-entry-options",
                  entry_id=str(eid), buttons=SxExpr(buttons_html))


# ---------------------------------------------------------------------------
# Entry tickets config (display + form)
# ---------------------------------------------------------------------------

def render_entry_tickets_config(entry, calendar, day, month, year) -> str:
    """Render ticket config display + edit form for admin entry view."""
    from quart import url_for
    from shared.browser.app.csrf import generate_csrf_token
    csrf = generate_csrf_token()

    cal_slug = getattr(calendar, "slug", "")
    eid = entry.id
    tp = getattr(entry, "ticket_price", None)
    tc = getattr(entry, "ticket_count", None)
    eid_s = str(eid)
    show_js = f"document.getElementById('ticket-form-{eid}').classList.remove('hidden'); this.classList.add('hidden');"
    hide_js = (f"document.getElementById('ticket-form-{eid}').classList.add('hidden'); "
               f"document.getElementById('entry-tickets-{eid}').querySelectorAll('button:not([type=submit])').forEach(btn => btn.classList.remove('hidden'));")

    if tp is not None:
        tc_str = f"{tc} tickets" if tc is not None else "Unlimited"
        display_html = sx_call("events-ticket-config-display",
                              price_str=f"£{tp:.2f}",
                              count_str=tc_str, show_js=show_js)
    else:
        display_html = sx_call("events-ticket-config-none", show_js=show_js)

    update_url = url_for(
        "calendar.day.calendar_entries.calendar_entry.update_tickets",
        entry_id=eid, calendar_slug=cal_slug, day=day, month=month, year=year,
    )
    hidden_cls = "" if tp is None else "hidden"
    tp_val = f"{tp:.2f}" if tp is not None else ""
    tc_val = str(tc) if tc is not None else ""

    form_html = sx_call("events-ticket-config-form",
                       entry_id=eid_s, hidden_cls=hidden_cls,
                       update_url=update_url, csrf=csrf,
                       price_val=tp_val, count_val=tc_val, hide_js=hide_js)
    return display_html + form_html


# ---------------------------------------------------------------------------
# Entry posts panel
# ---------------------------------------------------------------------------

def render_entry_posts_panel(entry_posts, entry, calendar, day, month, year) -> str:
    """Render associated posts list with remove buttons and search input."""
    from quart import url_for
    from shared.browser.app.csrf import generate_csrf_token
    csrf = generate_csrf_token()

    cal_slug = getattr(calendar, "slug", "")
    eid = entry.id
    eid_s = str(eid)

    posts_html = ""
    if entry_posts:
        items = ""
        for ep in entry_posts:
            ep_title = getattr(ep, "title", "")
            ep_id = getattr(ep, "id", 0)
            feat = getattr(ep, "feature_image", None)
            img_html = (sx_call("events-post-img", src=feat, alt=ep_title)
                        if feat else sx_call("events-post-img-placeholder"))

            del_url = url_for(
                "calendar.day.calendar_entries.calendar_entry.remove_post",
                calendar_slug=cal_slug, day=day, month=month, year=year,
                entry_id=eid, post_id=ep_id,
            )
            items += sx_call("events-entry-post-item",
                            img=SxExpr(img_html), title=ep_title,
                            del_url=del_url, entry_id=eid_s,
                            csrf_hdr=f'{{"X-CSRFToken": "{csrf}"}}')
        posts_html = sx_call("events-entry-posts-list", items=SxExpr(items))
    else:
        posts_html = sx_call("events-entry-posts-none")

    search_url = url_for(
        "calendar.day.calendar_entries.calendar_entry.search_posts",
        calendar_slug=cal_slug, day=day, month=month, year=year, entry_id=eid,
    )

    return sx_call("events-entry-posts-panel",
                  posts=SxExpr(posts_html), search_url=search_url,
                  entry_id=eid_s)


# ---------------------------------------------------------------------------
# Entry posts nav OOB
# ---------------------------------------------------------------------------

def render_entry_posts_nav_oob(entry_posts) -> str:
    """Render OOB nav for entry posts (scrolling menu)."""
    from quart import g
    styles = getattr(g, "styles", None) or {}
    nav_btn = getattr(styles, "nav_button", "") if hasattr(styles, "nav_button") else styles.get("nav_button", "")
    blog_url_fn = getattr(g, "blog_url", None)

    if not entry_posts:
        return sx_call("events-entry-posts-nav-oob-empty")

    items = ""
    for ep in entry_posts:
        slug = getattr(ep, "slug", "")
        title = getattr(ep, "title", "")
        feat = getattr(ep, "feature_image", None)
        href = blog_url_fn(f"/{slug}/") if blog_url_fn else f"/{slug}/"
        img_html = (sx_call("events-post-img", src=feat, alt=title)
                    if feat else sx_call("events-post-img-placeholder"))
        items += sx_call("events-entry-nav-post",
                        href=href, nav_btn=nav_btn,
                        img=SxExpr(img_html), title=title)

    return sx_call("events-entry-posts-nav-oob", items=SxExpr(items))


# ---------------------------------------------------------------------------
# Day entries nav OOB
# ---------------------------------------------------------------------------

def render_day_entries_nav_oob(confirmed_entries, calendar, day_date) -> str:
    """Render OOB nav for confirmed entries in a day."""
    from quart import url_for, g

    styles = getattr(g, "styles", None) or {}
    nav_btn = getattr(styles, "nav_button", "") if hasattr(styles, "nav_button") else styles.get("nav_button", "")
    cal_slug = getattr(calendar, "slug", "")

    if not confirmed_entries:
        return sx_call("events-day-entries-nav-oob-empty")

    items = ""
    for entry in confirmed_entries:
        href = url_for(
            "calendar.day.calendar_entries.calendar_entry.get",
            calendar_slug=cal_slug,
            year=day_date.year, month=day_date.month, day=day_date.day,
            entry_id=entry.id,
        )
        start = entry.start_at.strftime("%H:%M") if entry.start_at else ""
        end = f" \u2013 {entry.end_at.strftime('%H:%M')}" if entry.end_at else ""
        items += sx_call("events-day-nav-entry",
                        href=href, nav_btn=nav_btn,
                        name=entry.name, time_str=start + end)

    return sx_call("events-day-entries-nav-oob", items=SxExpr(items))


# ---------------------------------------------------------------------------
# Post nav entries OOB
# ---------------------------------------------------------------------------

def render_post_nav_entries_oob(associated_entries, calendars, post) -> str:
    """Render OOB nav for associated entries and calendars of a post."""
    from quart import g
    from shared.infrastructure.urls import events_url

    styles = getattr(g, "styles", None) or {}
    nav_btn = getattr(styles, "nav_button_less_pad", "") if hasattr(styles, "nav_button_less_pad") else styles.get("nav_button_less_pad", "")

    has_entries = associated_entries and getattr(associated_entries, "entries", None)
    has_items = has_entries or calendars

    if not has_items:
        return sx_call("events-post-nav-oob-empty")

    slug = post.get("slug", "") if isinstance(post, dict) else getattr(post, "slug", "")

    items = ""
    if has_entries:
        for entry in associated_entries.entries:
            entry_path = (
                f"/{slug}/{entry.calendar_slug}/"
                f"{entry.start_at.year}/{entry.start_at.month}/{entry.start_at.day}/"
                f"entries/{entry.id}/"
            )
            href = events_url(entry_path)
            time_str = entry.start_at.strftime("%b %d, %Y at %H:%M")
            end_str = f" \u2013 {entry.end_at.strftime('%H:%M')}" if entry.end_at else ""
            items += sx_call("events-post-nav-entry",
                            href=href, nav_btn=nav_btn,
                            name=entry.name, time_str=time_str + end_str)

    if calendars:
        for cal in calendars:
            cs = getattr(cal, "slug", "")
            local_href = events_url(f"/{slug}/{cs}/")
            items += sx_call("events-post-nav-calendar",
                            href=local_href, nav_btn=nav_btn, name=cal.name)

    hs = ("on load or scroll "
          "if window.innerWidth >= 640 and my.scrollWidth > my.clientWidth "
          "remove .hidden from .entries-nav-arrow add .flex to .entries-nav-arrow "
          "else add .hidden to .entries-nav-arrow remove .flex from .entries-nav-arrow end")

    return sx_call("events-post-nav-wrapper",
                  items=SxExpr(items), hyperscript=hs)


# ---------------------------------------------------------------------------
# Calendar description display + edit form
# ---------------------------------------------------------------------------

def render_calendar_description(calendar, *, oob: bool = False) -> str:
    """Render calendar description display with edit button, optionally with OOB title."""
    from quart import url_for

    cal_slug = getattr(calendar, "slug", "")
    edit_url = url_for("calendar.admin.calendar_description_edit", calendar_slug=cal_slug)
    html = _calendar_description_display_html(calendar, edit_url)

    if oob:
        desc = getattr(calendar, "description", "") or ""
        html += sx_call("events-calendar-description-title-oob",
                       description=desc)
    return html


def render_calendar_description_edit(calendar) -> str:
    """Render calendar description edit form."""
    from quart import url_for
    from shared.browser.app.csrf import generate_csrf_token
    csrf = generate_csrf_token()
    cal_slug = getattr(calendar, "slug", "")
    desc = getattr(calendar, "description", "") or ""

    save_url = url_for("calendar.admin.calendar_description_save", calendar_slug=cal_slug)
    cancel_url = url_for("calendar.admin.calendar_description_view", calendar_slug=cal_slug)

    return sx_call("events-calendar-description-edit-form",
                  save_url=save_url, cancel_url=cancel_url,
                  csrf=csrf, description=desc)


# ---------------------------------------------------------------------------
# Calendars list panel (for POST create / DELETE)
# ---------------------------------------------------------------------------

def render_calendars_list_panel(ctx: dict) -> str:
    """Render the calendars main panel HTML for POST/DELETE response."""
    return _calendars_main_panel_sx(ctx)


# ---------------------------------------------------------------------------
# Markets list panel (for POST create / DELETE)
# ---------------------------------------------------------------------------

def render_markets_list_panel(ctx: dict) -> str:
    """Render the markets main panel HTML for POST/DELETE response."""
    return _markets_main_panel_html(ctx)


# ---------------------------------------------------------------------------
# Slot main panel
# ---------------------------------------------------------------------------

def render_slot_main_panel(slot, calendar, *, oob: bool = False) -> str:
    """Render slot detail view."""
    from quart import url_for, g

    styles = getattr(g, "styles", None) or {}
    list_container = getattr(styles, "list_container", "") if hasattr(styles, "list_container") else styles.get("list_container", "")
    pre_action = getattr(styles, "pre_action_button", "") if hasattr(styles, "pre_action_button") else styles.get("pre_action_button", "")
    cal_slug = getattr(calendar, "slug", "")

    days_display = getattr(slot, "days_display", "\u2014")
    days = days_display.split(", ")
    flexible = getattr(slot, "flexible", False)
    time_start = slot.time_start.strftime("%H:%M") if slot.time_start else ""
    time_end = slot.time_end.strftime("%H:%M") if slot.time_end else ""
    cost = getattr(slot, "cost", None)
    cost_str = f"{cost:.2f}" if cost is not None else ""
    desc = getattr(slot, "description", "") or ""

    edit_url = url_for("calendar.slots.slot.get_edit", slot_id=slot.id, calendar_slug=cal_slug)

    # Days pills
    if days and days[0] != "\u2014":
        days_inner = "".join(
            sx_call("events-slot-day-pill", day=d) for d in days
        )
        days_html = sx_call("events-slot-days-pills", days_inner=SxExpr(days_inner))
    else:
        days_html = sx_call("events-slot-no-days")

    sid = str(slot.id)

    result = sx_call("events-slot-panel",
                    slot_id=sid, list_container=list_container,
                    days=SxExpr(days_html),
                    flexible="yes" if flexible else "no",
                    time_str=f"{time_start} \u2014 {time_end}",
                    cost_str=cost_str,
                    pre_action=pre_action, edit_url=edit_url)

    if oob:
        result += sx_call("events-slot-description-oob", description=desc)

    return result


# ---------------------------------------------------------------------------
# Slots table
# ---------------------------------------------------------------------------

def render_slots_table(slots, calendar) -> str:
    """Render slots table with rows and add button."""
    from quart import url_for, g
    from shared.browser.app.csrf import generate_csrf_token
    csrf = generate_csrf_token()

    styles = getattr(g, "styles", None) or {}
    list_container = getattr(styles, "list_container", "") if hasattr(styles, "list_container") else styles.get("list_container", "")
    tr_cls = getattr(styles, "tr", "") if hasattr(styles, "tr") else styles.get("tr", "")
    pill_cls = getattr(styles, "pill", "") if hasattr(styles, "pill") else styles.get("pill", "")
    action_btn = getattr(styles, "action_button", "") if hasattr(styles, "action_button") else styles.get("action_button", "")
    pre_action = getattr(styles, "pre_action_button", "") if hasattr(styles, "pre_action_button") else styles.get("pre_action_button", "")
    hx_select = getattr(g, "hx_select_search", "#main-panel")
    cal_slug = getattr(calendar, "slug", "")

    rows_html = ""
    if slots:
        for s in slots:
            slot_href = url_for("calendar.slots.slot.defpage_slot_detail", calendar_slug=cal_slug, slot_id=s.id)
            del_url = url_for("calendar.slots.slot.slot_delete", calendar_slug=cal_slug, slot_id=s.id)
            desc = getattr(s, "description", "") or ""

            days_display = getattr(s, "days_display", "\u2014")
            day_list = days_display.split(", ")
            if day_list and day_list[0] != "\u2014":
                days_inner = "".join(
                    sx_call("events-slot-day-pill", day=d) for d in day_list
                )
                days_html = sx_call("events-slot-days-pills", days_inner=SxExpr(days_inner))
            else:
                days_html = sx_call("events-slot-no-days")

            time_start = s.time_start.strftime("%H:%M") if s.time_start else ""
            time_end = s.time_end.strftime("%H:%M") if s.time_end else ""
            cost = getattr(s, "cost", None)
            cost_str = f"{cost:.2f}" if cost is not None else ""

            rows_html += sx_call("events-slots-row",
                                tr_cls=tr_cls, slot_href=slot_href,
                                pill_cls=pill_cls, hx_select=hx_select,
                                slot_name=s.name, description=desc,
                                flexible="yes" if s.flexible else "no",
                                days=SxExpr(days_html),
                                time_str=f"{time_start} - {time_end}",
                                cost_str=cost_str, action_btn=action_btn,
                                del_url=del_url,
                                csrf_hdr=f'{{"X-CSRFToken": "{csrf}"}}')
    else:
        rows_html = sx_call("events-slots-empty-row")

    add_url = url_for("calendar.slots.add_form", calendar_slug=cal_slug)

    return sx_call("events-slots-table",
                  list_container=list_container, rows=SxExpr(rows_html),
                  pre_action=pre_action, add_url=add_url)


# ---------------------------------------------------------------------------
# Ticket type main panel
# ---------------------------------------------------------------------------

def render_ticket_type_main_panel(ticket_type, entry, calendar, day, month, year, *, oob: bool = False) -> str:
    """Render ticket type detail view."""
    from quart import url_for, g

    styles = getattr(g, "styles", None) or {}
    list_container = getattr(styles, "list_container", "") if hasattr(styles, "list_container") else styles.get("list_container", "")
    pre_action = getattr(styles, "pre_action_button", "") if hasattr(styles, "pre_action_button") else styles.get("pre_action_button", "")
    cal_slug = getattr(calendar, "slug", "")

    cost = getattr(ticket_type, "cost", None)
    cost_str = f"\u00a3{cost:.2f}" if cost is not None else "\u00a30.00"
    count = getattr(ticket_type, "count", 0)
    tid = str(ticket_type.id)

    edit_url = url_for(
        "calendar.day.calendar_entries.calendar_entry.ticket_types.ticket_type.get_edit",
        ticket_type_id=ticket_type.id, calendar_slug=cal_slug,
        year=year, month=month, day=day, entry_id=entry.id,
    )

    def _col(label, val):
        return sx_call("events-ticket-type-col", label=label, value=val)

    return sx_call("events-ticket-type-panel",
                  ticket_id=tid, list_container=list_container,
                  c1=_col("Name", ticket_type.name),
                  c2=_col("Cost", cost_str),
                  c3=_col("Count", str(count)),
                  pre_action=pre_action, edit_url=edit_url)


# ---------------------------------------------------------------------------
# Ticket types table
# ---------------------------------------------------------------------------

def render_ticket_types_table(ticket_types, entry, calendar, day, month, year) -> str:
    """Render ticket types table with rows and add button."""
    from quart import url_for, g
    from shared.browser.app.csrf import generate_csrf_token
    csrf = generate_csrf_token()

    styles = getattr(g, "styles", None) or {}
    list_container = getattr(styles, "list_container", "") if hasattr(styles, "list_container") else styles.get("list_container", "")
    tr_cls = getattr(styles, "tr", "") if hasattr(styles, "tr") else styles.get("tr", "")
    pill_cls = getattr(styles, "pill", "") if hasattr(styles, "pill") else styles.get("pill", "")
    action_btn = getattr(styles, "action_button", "") if hasattr(styles, "action_button") else styles.get("action_button", "")
    hx_select = getattr(g, "hx_select_search", "#main-panel")
    cal_slug = getattr(calendar, "slug", "")
    eid = entry.id

    rows_html = ""
    if ticket_types:
        for tt in ticket_types:
            tt_href = url_for(
                "calendar.day.calendar_entries.calendar_entry.ticket_types.ticket_type.get",
                calendar_slug=cal_slug, year=year, month=month, day=day,
                entry_id=eid, ticket_type_id=tt.id,
            )
            del_url = url_for(
                "calendar.day.calendar_entries.calendar_entry.ticket_types.ticket_type.delete",
                calendar_slug=cal_slug, year=year, month=month, day=day,
                entry_id=eid, ticket_type_id=tt.id,
            )
            cost = getattr(tt, "cost", None)
            cost_str = f"\u00a3{cost:.2f}" if cost is not None else "\u00a30.00"

            rows_html += sx_call("events-ticket-types-row",
                                tr_cls=tr_cls, tt_href=tt_href,
                                pill_cls=pill_cls, hx_select=hx_select,
                                tt_name=tt.name, cost_str=cost_str,
                                count=str(tt.count), action_btn=action_btn,
                                del_url=del_url,
                                csrf_hdr=f'{{"X-CSRFToken": "{csrf}"}}')
    else:
        rows_html = sx_call("events-ticket-types-empty-row")

    add_url = url_for(
        "calendar.day.calendar_entries.calendar_entry.ticket_types.add_form",
        calendar_slug=cal_slug, entry_id=eid, year=year, month=month, day=day,
    )

    return sx_call("events-ticket-types-table",
                  list_container=list_container, rows=SxExpr(rows_html),
                  action_btn=action_btn, add_url=add_url)


# ---------------------------------------------------------------------------
# Buy result (ticket purchase confirmation)
# ---------------------------------------------------------------------------

def render_buy_result(entry, created_tickets, remaining, cart_count) -> str:
    """Render buy result card with created tickets + OOB cart icon."""
    from quart import url_for

    cart_html = _cart_icon_oob(cart_count)

    count = len(created_tickets)
    suffix = "s" if count != 1 else ""

    tickets_html = ""
    for ticket in created_tickets:
        href = url_for("tickets.defpage_ticket_detail", code=ticket.code)
        tickets_html += sx_call("events-buy-result-ticket",
                               href=href, code_short=ticket.code[:12] + "...")

    remaining_html = ""
    if remaining is not None:
        r_suffix = "s" if remaining != 1 else ""
        remaining_html = sx_call("events-buy-result-remaining",
                                text=f"{remaining} ticket{r_suffix} remaining")

    my_href = url_for("tickets.defpage_my_tickets")

    return cart_html + sx_call("events-buy-result",
                              entry_id=str(entry.id),
                              count_label=f"{count} ticket{suffix} reserved",
                              tickets=SxExpr(tickets_html),
                              remaining=SxExpr(remaining_html),
                              my_tickets_href=my_href)


# ---------------------------------------------------------------------------
# Buy form (ticket +/- controls)
# ---------------------------------------------------------------------------

def render_buy_form(entry, ticket_remaining, ticket_sold_count,
                    user_ticket_count, user_ticket_counts_by_type) -> str:
    """Render the ticket buy/adjust form with +/- controls."""
    from quart import url_for
    from shared.browser.app.csrf import generate_csrf_token
    csrf = generate_csrf_token()

    eid = entry.id
    eid_s = str(eid)
    tp = getattr(entry, "ticket_price", None)
    state = getattr(entry, "state", "")
    ticket_types = getattr(entry, "ticket_types", None) or []

    if tp is None:
        return ""

    if state != "confirmed":
        return sx_call("events-buy-not-confirmed", entry_id=eid_s)

    adjust_url = url_for("tickets.adjust_quantity")
    target = f"#ticket-buy-{eid}"

    # Info line
    info_html = ""
    info_items = ""
    if ticket_sold_count:
        info_items += sx_call("events-buy-info-sold",
                             count=str(ticket_sold_count))
    if ticket_remaining is not None:
        info_items += sx_call("events-buy-info-remaining",
                             count=str(ticket_remaining))
    if user_ticket_count:
        info_items += sx_call("events-buy-info-basket",
                             count=str(user_ticket_count))
    if info_items:
        info_html = sx_call("events-buy-info-bar", items=SxExpr(info_items))

    active_types = [tt for tt in ticket_types if getattr(tt, "deleted_at", None) is None]

    body_html = ""
    if active_types:
        type_items = ""
        for tt in active_types:
            type_count = user_ticket_counts_by_type.get(tt.id, 0) if user_ticket_counts_by_type else 0
            cost_str = f"\u00a3{tt.cost:.2f}" if tt.cost is not None else "\u00a30.00"
            type_items += sx_call("events-buy-type-item",
                                 type_name=tt.name, cost_str=cost_str,
                                 adjust_controls=SxExpr(_ticket_adjust_controls(csrf, adjust_url, target, eid, type_count, ticket_type_id=tt.id)))
        body_html = sx_call("events-buy-types-wrapper", items=SxExpr(type_items))
    else:
        qty = user_ticket_count or 0
        body_html = sx_call("events-buy-default",
                           price_str=f"\u00a3{tp:.2f}",
                           adjust_controls=SxExpr(_ticket_adjust_controls(csrf, adjust_url, target, eid, qty)))

    return sx_call("events-buy-panel",
                  entry_id=eid_s, info=SxExpr(info_html), body=SxExpr(body_html))


def _ticket_adjust_controls(csrf, adjust_url, target, entry_id, count, *, ticket_type_id=None):
    """Render +/- ticket controls for buy form."""
    from quart import url_for

    tt_html = sx_call("events-adjust-tt-hidden",
                     ticket_type_id=str(ticket_type_id)) if ticket_type_id else ""
    eid_s = str(entry_id)

    def _adj_form(count_val, btn_html, *, extra_cls=""):
        return sx_call("events-adjust-form",
                      adjust_url=adjust_url, target=target,
                      extra_cls=extra_cls, csrf=csrf,
                      entry_id=eid_s, tt=SxExpr(tt_html) if tt_html else None,
                      count_val=str(count_val), btn=SxExpr(btn_html))

    if count == 0:
        return _adj_form(1, sx_call("events-adjust-cart-plus"),
                         extra_cls="flex items-center")

    my_tickets_href = url_for("tickets.defpage_my_tickets")
    minus = _adj_form(count - 1, sx_call("events-adjust-minus"))
    cart_icon = sx_call("events-adjust-cart-icon",
                       href=my_tickets_href, count=str(count))
    plus = _adj_form(count + 1, sx_call("events-adjust-plus"))

    return sx_call("events-adjust-controls",
                  minus=SxExpr(minus), cart_icon=SxExpr(cart_icon), plus=SxExpr(plus))


# ---------------------------------------------------------------------------
# Adjust response (OOB cart icon + buy form)
# ---------------------------------------------------------------------------

def render_adjust_response(entry, ticket_remaining, ticket_sold_count,
                           user_ticket_count, user_ticket_counts_by_type,
                           cart_count) -> str:
    """Render ticket adjust response: OOB cart icon + buy form."""
    cart_html = _cart_icon_oob(cart_count)
    form_html = render_buy_form(
        entry, ticket_remaining, ticket_sold_count,
        user_ticket_count, user_ticket_counts_by_type,
    )
    return cart_html + form_html


def _cart_icon_oob(count: int) -> str:
    """Render the OOB cart icon/badge swap."""
    from quart import g

    blog_url_fn = getattr(g, "blog_url", None)
    cart_url_fn = getattr(g, "cart_url", None)
    site_fn = getattr(g, "site", None)
    logo = ""
    if site_fn:
        site_obj = site_fn() if callable(site_fn) else site_fn
        logo = getattr(site_obj, "logo", "") if site_obj else ""

    if count == 0:
        blog_href = blog_url_fn("/") if blog_url_fn else "/"
        return sx_call("events-cart-icon-logo",
                      blog_href=blog_href, logo=logo)

    cart_href = cart_url_fn("/") if cart_url_fn else "/"
    return sx_call("events-cart-icon-badge",
                  cart_href=cart_href, count=str(count))


# ===========================================================================
# SLOT PICKER JS — shared by entry edit + entry add forms
# ===========================================================================

_SLOT_PICKER_JS = """\
<script>
  (function () {
    function timeToMinutes(timeStr) {
      if (!timeStr) return 0;
      const [hours, minutes] = timeStr.split(':').map(Number);
      return hours * 60 + minutes;
    }

    function calculateCost(slotCost, slotStart, slotEnd, actualStart, actualEnd, flexible) {
      if (!flexible) {
        return parseFloat(slotCost);
      }
      if (!actualStart || !actualEnd) return 0;
      const slotStartMin = timeToMinutes(slotStart);
      const slotEndMin = timeToMinutes(slotEnd);
      const actualStartMin = timeToMinutes(actualStart);
      const actualEndMin = timeToMinutes(actualEnd);
      const slotDuration = slotEndMin - slotStartMin;
      const actualDuration = actualEndMin - actualStartMin;
      if (slotDuration <= 0 || actualDuration <= 0) return 0;
      const ratio = actualDuration / slotDuration;
      return parseFloat(slotCost) * ratio;
    }

    function initEntrySlotPicker(root, applyInitial) {
      if (applyInitial === undefined) applyInitial = false;
      const select = root.querySelector('[data-slot-picker]');
      if (!select) return;
      const timeFields = root.querySelector('[data-time-fields]');
      const startInput = root.querySelector('[data-entry-start]');
      const endInput = root.querySelector('[data-entry-end]');
      const helper = root.querySelector('[data-slot-boundary]');
      const costDisplay = root.querySelector('[data-cost-display]');
      const costRow = root.querySelector('[data-cost-row]');
      const fixedSummary = root.querySelector('[data-fixed-summary]');
      if (!startInput || !endInput) return;

      function updateCost() {
        const opt = select.selectedOptions[0];
        if (!opt || !opt.value) {
          if (costDisplay) costDisplay.textContent = '\\u00a30.00';
          return;
        }
        const cost = opt.dataset.cost || '0';
        const s = opt.dataset.start || '';
        const e = opt.dataset.end || '';
        const flexible = opt.dataset.flexible === '1';
        const calculatedCost = calculateCost(cost, s, e, startInput.value, endInput.value, flexible);
        if (costDisplay) costDisplay.textContent = '\\u00a3' + calculatedCost.toFixed(2);
      }

      function applyFromOption(opt) {
        if (!opt || !opt.value) {
          if (timeFields) timeFields.classList.add('hidden');
          if (costRow) costRow.classList.add('hidden');
          if (fixedSummary) fixedSummary.classList.add('hidden');
          return;
        }
        const s = opt.dataset.start || '';
        const e = opt.dataset.end || '';
        const flexible = opt.dataset.flexible === '1';
        if (!flexible) {
          if (s) startInput.value = s;
          if (e) endInput.value = e;
          if (timeFields) timeFields.classList.add('hidden');
          if (fixedSummary) {
            fixedSummary.classList.remove('hidden');
            fixedSummary.textContent = e ? s + ' \\u2013 ' + e : 'From ' + s + ' (open-ended)';
          }
          if (costRow) costRow.classList.remove('hidden');
        } else {
          if (timeFields) timeFields.classList.remove('hidden');
          if (fixedSummary) fixedSummary.classList.add('hidden');
          if (costRow) costRow.classList.remove('hidden');
          if (helper) {
            helper.textContent = e ? 'Times must be between ' + s + ' and ' + e + '.' : 'Start at or after ' + s + '.';
          }
        }
        updateCost();
      }

      if (applyInitial) applyFromOption(select.selectedOptions[0]);

      if (select._slotChangeHandler) select.removeEventListener('change', select._slotChangeHandler);
      select._slotChangeHandler = () => applyFromOption(select.selectedOptions[0]);
      select.addEventListener('change', select._slotChangeHandler);
      startInput.addEventListener('input', updateCost);
      endInput.addEventListener('input', updateCost);
    }

    document.addEventListener('DOMContentLoaded', () => initEntrySlotPicker(document, true));
    if (window.htmx) htmx.onLoad((content) => initEntrySlotPicker(content, true));
  })();
</script>"""


# ===========================================================================
# Entry edit form
# ===========================================================================

def _slot_options_html(day_slots, selected_slot_id=None) -> str:
    """Build slot <option> elements."""
    parts = []
    for slot in day_slots:
        start = slot.time_start.strftime("%H:%M") if slot.time_start else ""
        end = slot.time_end.strftime("%H:%M") if slot.time_end else ""
        flexible = getattr(slot, "flexible", False)
        cost = getattr(slot, "cost", None)
        cost_str = str(cost) if cost is not None else "0"
        label_parts = [slot.name, f"({start}"]
        if end:
            label_parts.append(f"\u2013{end})")
        else:
            label_parts.append("\u2013open-ended)")
        if flexible:
            label_parts.append("[flexible]")
        label = " ".join(label_parts)

        parts.append(sx_call("events-slot-option",
                            value=str(slot.id),
                            data_start=start, data_end=end,
                            data_flexible="1" if flexible else "0",
                            data_cost=cost_str,
                            selected="selected" if selected_slot_id == slot.id else None,
                            label=label))
    return "".join(parts)


def render_entry_edit_form(entry, calendar, day, month, year, day_slots) -> str:
    """Render entry edit form (replaces _types/entry/_edit.html)."""
    from quart import url_for, g
    from shared.browser.app.csrf import generate_csrf_token
    csrf = generate_csrf_token()

    styles = getattr(g, "styles", None) or {}
    list_container = styles.get("list_container", "") if isinstance(styles, dict) else getattr(styles, "list_container", "")
    action_btn = styles.get("action_button", "") if isinstance(styles, dict) else getattr(styles, "action_button", "")
    cancel_btn = styles.get("cancel_button", "") if isinstance(styles, dict) else getattr(styles, "cancel_button", "")
    cal_slug = getattr(calendar, "slug", "")
    eid = entry.id

    put_url = url_for("calendar.day.calendar_entries.calendar_entry.put",
                       calendar_slug=cal_slug, day=day, month=month, year=year, entry_id=eid)
    cancel_url = url_for("calendar.day.calendar_entries.calendar_entry.get",
                          calendar_slug=cal_slug, day=day, month=month, year=year, entry_id=eid)

    # Slot picker
    if day_slots:
        options_html = _slot_options_html(day_slots, selected_slot_id=getattr(entry, "slot_id", None))
        slot_picker_html = sx_call("events-slot-picker",
                                  id=f"entry-slot-{eid}", options=SxExpr(options_html))
    else:
        slot_picker_html = sx_call("events-no-slots")

    # Values
    start_val = entry.start_at.strftime("%H:%M") if entry.start_at else ""
    end_val = entry.end_at.strftime("%H:%M") if entry.end_at else ""
    cost = getattr(entry, "cost", None)
    cost_display = f"\u00a3{cost:.2f}" if cost is not None else "\u00a30.00"
    tp = getattr(entry, "ticket_price", None)
    tc = getattr(entry, "ticket_count", None)
    tp_val = f"{tp:.2f}" if tp is not None else ""
    tc_val = str(tc) if tc is not None else ""

    html = sx_call("events-entry-edit-form",
                  entry_id=str(eid), list_container=list_container,
                  put_url=put_url, cancel_url=cancel_url, csrf=csrf,
                  name_val=entry.name or "", slot_picker=SxExpr(slot_picker_html),
                  start_val=start_val, end_val=end_val, cost_display=cost_display,
                  ticket_price_val=tp_val, ticket_count_val=tc_val,
                  action_btn=action_btn, cancel_btn=cancel_btn)
    return html + _SLOT_PICKER_JS


# ===========================================================================
# Post search results
# ===========================================================================

def render_post_search_results(search_posts, search_query, page, total_pages,
                                entry, calendar, day, month, year) -> str:
    """Render post search results (replaces _types/entry/_post_search_results.html)."""
    from quart import url_for
    from shared.browser.app.csrf import generate_csrf_token
    csrf = generate_csrf_token()

    cal_slug = getattr(calendar, "slug", "")
    eid = entry.id

    parts = []
    for sp in search_posts:
        post_url = url_for("calendar.day.calendar_entries.calendar_entry.add_post",
                            calendar_slug=cal_slug, day=day, month=month, year=year,
                            entry_id=eid)
        feat = getattr(sp, "feature_image", None)
        title = getattr(sp, "title", "")
        if feat:
            img_html = sx_call("events-post-img", src=feat, alt=title)
        else:
            img_html = sx_call("events-post-img-placeholder")

        parts.append(sx_call("events-post-search-item",
                            post_url=post_url, entry_id=str(eid), csrf=csrf,
                            post_id=str(sp.id), img=SxExpr(img_html), title=title))

    result = "".join(parts)

    if page < int(total_pages):
        next_url = url_for("calendar.day.calendar_entries.calendar_entry.search_posts",
                            calendar_slug=cal_slug, day=day, month=month, year=year,
                            entry_id=eid, q=search_query, page=page + 1)
        result += sx_call("events-post-search-sentinel",
                         page=str(page), next_url=next_url)
    elif search_posts:
        result += sx_call("events-post-search-end")

    return result


# ===========================================================================
# Entry admin page / OOB
# ===========================================================================

def _entry_admin_header_html(ctx: dict, *, oob: bool = False) -> str:
    """Build the entry admin header row."""
    from quart import url_for

    calendar = ctx.get("calendar")
    if not calendar:
        return ""
    cal_slug = getattr(calendar, "slug", "")
    entry = ctx.get("entry")
    if not entry:
        return ""
    day = ctx.get("day")
    month = ctx.get("month")
    year = ctx.get("year")

    link_href = url_for(
        "calendar.day.calendar_entries.calendar_entry.admin.admin",
        calendar_slug=cal_slug, day=day, month=month, year=year, entry_id=entry.id,
    )
    # Nav: ticket_types link
    nav_html = _entry_admin_nav_html(ctx)

    return sx_call("menu-row-sx", id="entry-admin-row", level=6,
                  link_href=link_href, link_label="admin", icon="fa fa-cog",
                  nav=SxExpr(nav_html) if nav_html else None, child_id="entry-admin-header-child", oob=oob)


def _entry_admin_nav_html(ctx: dict) -> str:
    """Entry admin nav: ticket_types link."""
    from quart import url_for

    calendar = ctx.get("calendar")
    if not calendar:
        return ""
    cal_slug = getattr(calendar, "slug", "")
    entry = ctx.get("entry")
    if not entry:
        return ""
    day = ctx.get("day")
    month = ctx.get("month")
    year = ctx.get("year")
    select_colours = ctx.get("select_colours", "")

    href = url_for("calendar.day.calendar_entries.calendar_entry.ticket_types.get",
                    calendar_slug=cal_slug, entry_id=entry.id,
                    year=year, month=month, day=day)
    return sx_call("nav-link", href=href, label="ticket_types",
                  select_colours=select_colours)


def _entry_admin_main_panel_html(ctx: dict) -> str:
    """Entry admin main panel: just a ticket_types link."""
    from quart import url_for

    calendar = ctx.get("calendar")
    entry = ctx.get("entry")
    if not calendar or not entry:
        return ""
    cal_slug = getattr(calendar, "slug", "")
    day = ctx.get("day")
    month = ctx.get("month")
    year = ctx.get("year")
    styles = ctx.get("styles") or {}
    nav_btn = styles.get("nav_button", "") if isinstance(styles, dict) else getattr(styles, "nav_button", "")
    hx_select = ctx.get("hx_select_search", "#main-panel")
    select_colours = ctx.get("select_colours", "")

    href = url_for("calendar.day.calendar_entries.calendar_entry.ticket_types.get",
                    calendar_slug=cal_slug, entry_id=entry.id,
                    year=year, month=month, day=day)
    return sx_call("nav-link", href=href, label="ticket_types",
                  select_colours=select_colours, aclass=nav_btn,
                  is_selected=False)


# ===========================================================================
# Slot page / OOB (extends slots)
# ===========================================================================

def _slot_header_html(ctx: dict, *, oob: bool = False) -> str:
    """Build the slot detail header row."""
    from quart import url_for

    calendar = ctx.get("calendar")
    if not calendar:
        return ""
    cal_slug = getattr(calendar, "slug", "")
    slot = ctx.get("slot")
    if not slot:
        return ""

    # Label: icon + name + description
    desc = getattr(slot, "description", "") or ""
    label_inner = (
        f'<div class="flex flex-col md:flex-row md:gap-2 items-center">'
        f'<div class="flex flex-row items-center gap-2">'
        f'<i class="fa fa-clock"></i>'
        f'<div class="shrink-0">{escape(slot.name)}</div>'
        f'</div>'
        f'<p class="text-stone-500 whitespace-pre-line break-all w-full">{escape(desc)}</p>'
        f'</div>'
    )

    return sx_call("menu-row-sx", id="slot-row", level=5,
                  link_label_content=SxExpr(label_inner),
                  child_id="slot-header-child", oob=oob)


# ===========================================================================
# Slot edit form
# ===========================================================================

def render_slot_edit_form(slot, calendar) -> str:
    """Render slot edit form (replaces _types/slot/_edit.html)."""
    from quart import url_for, g
    from shared.browser.app.csrf import generate_csrf_token
    csrf = generate_csrf_token()

    styles = getattr(g, "styles", None) or {}
    list_container = styles.get("list_container", "") if isinstance(styles, dict) else getattr(styles, "list_container", "")
    action_btn = styles.get("action_button", "") if isinstance(styles, dict) else getattr(styles, "action_button", "")
    cancel_btn = styles.get("cancel_button", "") if isinstance(styles, dict) else getattr(styles, "cancel_button", "")
    cal_slug = getattr(calendar, "slug", "")
    sid = slot.id

    put_url = url_for("calendar.slots.slot.put", calendar_slug=cal_slug, slot_id=sid)
    cancel_url = url_for("calendar.slots.slot.get_view", calendar_slug=cal_slug, slot_id=sid)

    cost = getattr(slot, "cost", None)
    cost_val = f"{cost:.2f}" if cost is not None else ""
    start_val = slot.time_start.strftime("%H:%M") if slot.time_start else ""
    end_val = slot.time_end.strftime("%H:%M") if slot.time_end else ""
    desc_val = getattr(slot, "description", "") or ""

    # Days checkboxes
    day_keys = [("mon", "Mon"), ("tue", "Tue"), ("wed", "Wed"), ("thu", "Thu"),
                ("fri", "Fri"), ("sat", "Sat"), ("sun", "Sun")]
    all_checked = all(getattr(slot, k, False) for k, _ in day_keys)

    days_parts = [sx_call("events-day-all-checkbox",
                         checked="checked" if all_checked else None)]
    for key, label in day_keys:
        checked = getattr(slot, key, False)
        days_parts.append(sx_call("events-day-checkbox",
                                 name=key, label=label,
                                 checked="checked" if checked else None))
    days_html = "".join(days_parts)

    flexible = getattr(slot, "flexible", False)

    return sx_call("events-slot-edit-form",
                  slot_id=str(sid), list_container=list_container,
                  put_url=put_url, cancel_url=cancel_url, csrf=csrf,
                  name_val=slot.name or "", cost_val=cost_val,
                  start_val=start_val, end_val=end_val,
                  desc_val=desc_val, days=SxExpr(days_html),
                  flexible_checked="checked" if flexible else None,
                  action_btn=action_btn, cancel_btn=cancel_btn)


# ===========================================================================
# Slot add form / button
# ===========================================================================

def render_slot_add_form(calendar) -> str:
    """Render slot add form (replaces _types/slots/_add.html)."""
    from quart import url_for, g
    from shared.browser.app.csrf import generate_csrf_token
    csrf = generate_csrf_token()

    styles = getattr(g, "styles", None) or {}
    action_btn = styles.get("action_button", "") if isinstance(styles, dict) else getattr(styles, "action_button", "")
    cancel_btn = styles.get("cancel_button", "") if isinstance(styles, dict) else getattr(styles, "cancel_button", "")
    cal_slug = getattr(calendar, "slug", "")

    post_url = url_for("calendar.slots.post", calendar_slug=cal_slug)
    cancel_url = url_for("calendar.slots.add_button", calendar_slug=cal_slug)
    csrf_hdr = f'{{"X-CSRFToken": "{csrf}"}}'

    # Days checkboxes (all unchecked for add)
    day_keys = [("mon", "Mon"), ("tue", "Tue"), ("wed", "Wed"), ("thu", "Thu"),
                ("fri", "Fri"), ("sat", "Sat"), ("sun", "Sun")]
    days_parts = [sx_call("events-day-all-checkbox", checked=None)]
    for key, label in day_keys:
        days_parts.append(sx_call("events-day-checkbox", name=key, label=label, checked=None))
    days_html = "".join(days_parts)

    return sx_call("events-slot-add-form",
                  post_url=post_url, csrf=csrf_hdr,
                  days=SxExpr(days_html),
                  action_btn=action_btn, cancel_btn=cancel_btn,
                  cancel_url=cancel_url)


def render_slot_add_button(calendar) -> str:
    """Render slot add button (replaces _types/slots/_add_button.html)."""
    from quart import url_for, g

    styles = getattr(g, "styles", None) or {}
    pre_action = styles.get("pre_action_button", "") if isinstance(styles, dict) else getattr(styles, "pre_action_button", "")
    cal_slug = getattr(calendar, "slug", "")
    add_url = url_for("calendar.slots.add_form", calendar_slug=cal_slug)

    return sx_call("events-slot-add-button", pre_action=pre_action, add_url=add_url)


# ===========================================================================
# Entry add form / button
# ===========================================================================

def render_entry_add_form(calendar, day, month, year, day_slots) -> str:
    """Render entry add form (replaces _types/day/_add.html)."""
    from quart import url_for, g
    from shared.browser.app.csrf import generate_csrf_token
    csrf = generate_csrf_token()

    styles = getattr(g, "styles", None) or {}
    action_btn = styles.get("action_button", "") if isinstance(styles, dict) else getattr(styles, "action_button", "")
    cancel_btn = styles.get("cancel_button", "") if isinstance(styles, dict) else getattr(styles, "cancel_button", "")
    cal_slug = getattr(calendar, "slug", "")

    post_url = url_for("calendar.day.calendar_entries.add_entry",
                        calendar_slug=cal_slug, day=day, month=month, year=year)
    cancel_url = url_for("calendar.day.calendar_entries.add_button",
                          calendar_slug=cal_slug, day=day, month=month, year=year)

    # Slot picker
    if day_slots:
        options_html = _slot_options_html(day_slots)
        slot_picker_html = sx_call("events-slot-picker",
                                  id="entry-slot-new", options=SxExpr(options_html))
    else:
        slot_picker_html = sx_call("events-no-slots")

    html = sx_call("events-entry-add-form",
                  post_url=post_url, csrf=csrf,
                  slot_picker=SxExpr(slot_picker_html),
                  action_btn=action_btn, cancel_btn=cancel_btn,
                  cancel_url=cancel_url)
    return html + _SLOT_PICKER_JS


def render_entry_add_button(calendar, day, month, year) -> str:
    """Render entry add button (replaces _types/day/_add_button.html)."""
    from quart import url_for, g

    styles = getattr(g, "styles", None) or {}
    pre_action = styles.get("pre_action_button", "") if isinstance(styles, dict) else getattr(styles, "pre_action_button", "")
    cal_slug = getattr(calendar, "slug", "")
    add_url = url_for("calendar.day.calendar_entries.add_form",
                       calendar_slug=cal_slug, day=day, month=month, year=year)

    return sx_call("events-entry-add-button", pre_action=pre_action, add_url=add_url)


# ===========================================================================
# Ticket types page / OOB
# ===========================================================================

def _ticket_types_header_html(ctx: dict, *, oob: bool = False) -> str:
    """Build the ticket types header row."""
    from quart import url_for

    calendar = ctx.get("calendar")
    entry = ctx.get("entry")
    if not calendar or not entry:
        return ""
    cal_slug = getattr(calendar, "slug", "")
    day = ctx.get("day")
    month = ctx.get("month")
    year = ctx.get("year")

    link_href = url_for("calendar.day.calendar_entries.calendar_entry.ticket_types.get",
                         calendar_slug=cal_slug, entry_id=entry.id,
                         year=year, month=month, day=day)

    label_html = '<i class="fa fa-ticket"></i><div class="shrink-0">ticket types</div>'

    nav_html = sx_call("events-admin-placeholder-nav")

    return sx_call("menu-row-sx", id="ticket_types-row", level=7,
                  link_href=link_href, link_label_content=SxExpr(label_html),
                  nav=SxExpr(nav_html) if nav_html else None, child_id="ticket_type-header-child", oob=oob)



# ===========================================================================
# Ticket type page / OOB
# ===========================================================================

def _ticket_type_header_html(ctx: dict, *, oob: bool = False) -> str:
    """Build the single ticket type header row."""
    from quart import url_for

    calendar = ctx.get("calendar")
    entry = ctx.get("entry")
    ticket_type = ctx.get("ticket_type")
    if not calendar or not entry or not ticket_type:
        return ""
    cal_slug = getattr(calendar, "slug", "")
    day = ctx.get("day")
    month = ctx.get("month")
    year = ctx.get("year")

    link_href = url_for(
        "calendar.day.calendar_entries.calendar_entry.ticket_types.ticket_type.get",
        calendar_slug=cal_slug, year=year, month=month, day=day,
        entry_id=entry.id, ticket_type_id=ticket_type.id,
    )

    label_html = (
        f'<div class="flex flex-col md:flex-row md:gap-2 items-center">'
        f'<div class="flex flex-row items-center gap-2">'
        f'<i class="fa fa-ticket"></i>'
        f'<div class="shrink-0">{escape(ticket_type.name)}</div>'
        f'</div></div>'
    )

    nav_html = sx_call("events-admin-placeholder-nav")

    return sx_call("menu-row-sx", id="ticket_type-row", level=8,
                  link_href=link_href, link_label_content=SxExpr(label_html),
                  nav=SxExpr(nav_html) if nav_html else None, child_id="ticket_type-header-child-inner", oob=oob)


# ===========================================================================
# Ticket type edit form
# ===========================================================================

def render_ticket_type_edit_form(ticket_type, entry, calendar, day, month, year) -> str:
    """Render ticket type edit form (replaces _types/ticket_type/_edit.html)."""
    from quart import url_for, g
    from shared.browser.app.csrf import generate_csrf_token
    csrf = generate_csrf_token()

    styles = getattr(g, "styles", None) or {}
    list_container = styles.get("list_container", "") if isinstance(styles, dict) else getattr(styles, "list_container", "")
    action_btn = styles.get("action_button", "") if isinstance(styles, dict) else getattr(styles, "action_button", "")
    cancel_btn = styles.get("cancel_button", "") if isinstance(styles, dict) else getattr(styles, "cancel_button", "")
    cal_slug = getattr(calendar, "slug", "")
    tid = ticket_type.id

    put_url = url_for("calendar.day.calendar_entries.calendar_entry.ticket_types.ticket_type.put",
                       calendar_slug=cal_slug, year=year, month=month, day=day,
                       entry_id=entry.id, ticket_type_id=tid)
    cancel_url = url_for("calendar.day.calendar_entries.calendar_entry.ticket_types.ticket_type.get_view",
                          calendar_slug=cal_slug, entry_id=entry.id,
                          year=year, month=month, day=day, ticket_type_id=tid)

    cost = getattr(ticket_type, "cost", None)
    cost_val = f"{cost:.2f}" if cost is not None else ""
    count = getattr(ticket_type, "count", 0)

    return sx_call("events-ticket-type-edit-form",
                  ticket_id=str(tid), list_container=list_container,
                  put_url=put_url, cancel_url=cancel_url, csrf=csrf,
                  name_val=ticket_type.name or "",
                  cost_val=cost_val, count_val=str(count),
                  action_btn=action_btn, cancel_btn=cancel_btn)


# ===========================================================================
# Ticket type add form / button
# ===========================================================================

def render_ticket_type_add_form(entry, calendar, day, month, year) -> str:
    """Render ticket type add form (replaces _types/ticket_types/_add.html)."""
    from quart import url_for, g
    from shared.browser.app.csrf import generate_csrf_token
    csrf = generate_csrf_token()

    styles = getattr(g, "styles", None) or {}
    action_btn = styles.get("action_button", "") if isinstance(styles, dict) else getattr(styles, "action_button", "")
    cancel_btn = styles.get("cancel_button", "") if isinstance(styles, dict) else getattr(styles, "cancel_button", "")
    cal_slug = getattr(calendar, "slug", "")

    post_url = url_for("calendar.day.calendar_entries.calendar_entry.ticket_types.post",
                        calendar_slug=cal_slug, entry_id=entry.id,
                        year=year, month=month, day=day)
    cancel_url = url_for("calendar.day.calendar_entries.calendar_entry.ticket_types.add_button",
                          calendar_slug=cal_slug, entry_id=entry.id,
                          year=year, month=month, day=day)
    csrf_hdr = f'{{"X-CSRFToken": "{csrf}"}}'

    return sx_call("events-ticket-type-add-form",
                  post_url=post_url, csrf=csrf_hdr,
                  action_btn=action_btn, cancel_btn=cancel_btn,
                  cancel_url=cancel_url)


def render_ticket_type_add_button(entry, calendar, day, month, year) -> str:
    """Render ticket type add button (replaces _types/ticket_types/_add_button.html)."""
    from quart import url_for, g

    styles = getattr(g, "styles", None) or {}
    action_btn = styles.get("action_button", "") if isinstance(styles, dict) else getattr(styles, "action_button", "")
    cal_slug = getattr(calendar, "slug", "")

    add_url = url_for("calendar.day.calendar_entries.calendar_entry.ticket_types.add_form",
                       calendar_slug=cal_slug, entry_id=entry.id,
                       year=year, month=month, day=day)

    return sx_call("events-ticket-type-add-button",
                  action_btn=action_btn, add_url=add_url)


# ===========================================================================
# Fragment: container cards entries
# ===========================================================================

def render_fragment_container_cards(batch, post_ids, slug_map) -> str:
    """Render container cards entries (replaces fragments/container_cards_entries.html)."""
    from shared.infrastructure.urls import events_url

    parts = []
    for post_id in post_ids:
        parts.append(f"<!-- card-widget:{post_id} -->")
        widget_entries = batch.get(post_id, [])
        if widget_entries:
            cards_html = ""
            for entry in widget_entries:
                _post_slug = slug_map.get(post_id, "")
                _entry_path = (
                    f"/{_post_slug}/{entry.calendar_slug}/"
                    f"{entry.start_at.year}/{entry.start_at.month}/"
                    f"{entry.start_at.day}/entries/{entry.id}/"
                )
                time_str = entry.start_at.strftime("%H:%M")
                if entry.end_at:
                    time_str += f" \u2013 {entry.end_at.strftime('%H:%M')}"
                cards_html += sx_call("events-frag-entry-card",
                                     href=events_url(_entry_path),
                                     name=entry.name,
                                     date_str=entry.start_at.strftime("%a, %b %d"),
                                     time_str=time_str)
            parts.append(sx_call("events-frag-entries-widget", cards=SxExpr(cards_html)))
        parts.append(f"<!-- /card-widget:{post_id} -->")

    return "\n".join(parts)


# ===========================================================================
# Fragment: account page tickets
# ===========================================================================

def render_fragment_account_tickets(tickets) -> str:
    """Render account page tickets (replaces fragments/account_page_tickets.html)."""
    from shared.infrastructure.urls import events_url

    if tickets:
        items_html = ""
        for ticket in tickets:
            href = events_url(f"/tickets/{ticket.code}/")
            date_str = ticket.entry_start_at.strftime("%d %b %Y, %H:%M")
            cal_name = ""
            if getattr(ticket, "calendar_name", None):
                cal_name = f'<span>&middot; {escape(ticket.calendar_name)}</span>'
            type_name = ""
            if getattr(ticket, "ticket_type_name", None):
                type_name = f'<span>&middot; {escape(ticket.ticket_type_name)}</span>'
            badge_html = sx_call("status-pill",
                                status=getattr(ticket, "state", ""))
            items_html += sx_call("events-frag-ticket-item",
                                 href=href, entry_name=ticket.entry_name,
                                 date_str=date_str, calendar_name=cal_name,
                                 type_name=type_name, badge=SxExpr(badge_html))
        body = sx_call("events-frag-tickets-list", items=SxExpr(items_html))
    else:
        body = sx_call("empty-state", message="No tickets yet.",
                      cls="text-sm text-stone-500")

    return sx_call("events-frag-tickets-panel", items=SxExpr(body))


# ===========================================================================
# Fragment: account page bookings
# ===========================================================================

def render_fragment_account_bookings(bookings) -> str:
    """Render account page bookings (replaces fragments/account_page_bookings.html)."""
    if bookings:
        items_html = ""
        for booking in bookings:
            date_str = booking.start_at.strftime("%d %b %Y, %H:%M")
            if getattr(booking, "end_at", None):
                date_str_extra = f'<span>&ndash; {escape(booking.end_at.strftime("%H:%M"))}</span>'
            else:
                date_str_extra = ""
            cal_name = ""
            if getattr(booking, "calendar_name", None):
                cal_name = f'<span>&middot; {escape(booking.calendar_name)}</span>'
            cost_str = ""
            if getattr(booking, "cost", None):
                cost_str = f'<span>&middot; &pound;{escape(str(booking.cost))}</span>'
            badge_html = sx_call("status-pill",
                                status=getattr(booking, "state", ""))
            items_html += sx_call("events-frag-booking-item",
                                 name=booking.name,
                                 date_str=date_str + date_str_extra,
                                 calendar_name=cal_name, cost_str=cost_str,
                                 badge=SxExpr(badge_html))
        body = sx_call("events-frag-bookings-list", items=SxExpr(items_html))
    else:
        body = sx_call("empty-state", message="No bookings yet.",
                      cls="text-sm text-stone-500")

    return sx_call("events-frag-bookings-panel", items=SxExpr(body))
