"""Layout engine for structured slide types (columns, pros/cons, etc.)."""

from __future__ import annotations

from pptx.dml.color import RGBColor
from pptx.enum.shapes import MSO_SHAPE
from pptx.enum.text import PP_ALIGN, MSO_ANCHOR
from pptx.slide import Slide
from pptx.util import Inches, Pt

from pptx_mcp.engine.composition import LayoutGrid, Margin
from pptx_mcp.engine.shapes import parse_hex_color
from pptx_mcp.models.brand import BrandConfig
from pptx_mcp.models.slides import (
    AgendaItem,
    ColumnContent,
    FunnelStage,
    IconItem,
    KpiCard,
    ProcessStep,
    SlideDefinition,
    SwotData,
    TeamMember,
)
from pptx_mcp.utils.logging import get_logger

logger = get_logger("engine.layouts")


def _make_grid(brand: BrandConfig, slide_width: float, slide_height: float = 7.5) -> LayoutGrid:
    """Build a LayoutGrid from brand spacing config."""
    sp = brand.spacing
    return LayoutGrid(
        slide_width=slide_width,
        slide_height=slide_height,
        margin=Margin(
            left=sp.margin_left,
            right=sp.margin_right,
            top=sp.content_top,
            bottom=0.5,
        ),
    )


def _apply_text_frame_padding(tf: object, brand: BrandConfig) -> None:
    """Apply internal padding to a text frame from brand spacing config."""
    padding = Inches(brand.spacing.padding)
    tf.margin_left = padding   # type: ignore[attr-defined]
    tf.margin_right = padding  # type: ignore[attr-defined]
    tf.margin_top = padding    # type: ignore[attr-defined]
    tf.margin_bottom = padding  # type: ignore[attr-defined]


def _apply_line_spacing(paragraph: object, brand: BrandConfig) -> None:
    """Apply line spacing from brand font config to a paragraph."""
    paragraph.line_spacing = brand.fonts.line_spacing  # type: ignore[attr-defined]


def render_title_slide(
    slide: Slide,
    definition: SlideDefinition,
    brand: BrandConfig,
    slide_width: float,
) -> None:
    """Render a title slide with centered title and subtitle."""
    sp = brand.spacing
    width = slide_width - sp.margin_left - sp.margin_right

    title_box = slide.shapes.add_textbox(
        Inches(sp.margin_left),
        Inches(2.5),
        Inches(width),
        Inches(1.5),
    )
    tf = title_box.text_frame
    tf.word_wrap = True
    p = tf.paragraphs[0]
    p.text = definition.title or ""
    p.alignment = PP_ALIGN.CENTER
    _apply_line_spacing(p, brand)
    run = p.runs[0]
    run.font.name = brand.fonts.heading_font
    run.font.size = Pt(brand.fonts.title_size)
    run.font.bold = True
    run.font.color.rgb = parse_hex_color(brand.colors.primary)

    if definition.subtitle:
        sub_box = slide.shapes.add_textbox(
            Inches(sp.margin_left + 1.0),
            Inches(4.2),
            Inches(width - 2.0),
            Inches(1.0),
        )
        tf2 = sub_box.text_frame
        tf2.word_wrap = True
        p2 = tf2.paragraphs[0]
        p2.text = definition.subtitle
        p2.alignment = PP_ALIGN.CENTER
        _apply_line_spacing(p2, brand)
        run2 = p2.runs[0]
        run2.font.name = brand.fonts.body_font
        run2.font.size = Pt(brand.fonts.subheading_size)
        run2.font.color.rgb = parse_hex_color(brand.colors.text_secondary)


def render_section_slide(
    slide: Slide,
    definition: SlideDefinition,
    brand: BrandConfig,
    slide_width: float,
) -> None:
    """Render a section divider slide."""
    sp = brand.spacing
    width = slide_width - sp.margin_left - sp.margin_right

    # Accent bar
    bar = slide.shapes.add_shape(
        MSO_SHAPE.RECTANGLE,
        Inches(sp.margin_left),
        Inches(3.2),
        Inches(2.0),
        Inches(0.06),
    )
    bar.fill.solid()
    bar.fill.fore_color.rgb = parse_hex_color(brand.colors.accent)
    bar.line.fill.background()

    title_box = slide.shapes.add_textbox(
        Inches(sp.margin_left),
        Inches(3.4),
        Inches(width),
        Inches(1.2),
    )
    tf = title_box.text_frame
    tf.word_wrap = True
    p = tf.paragraphs[0]
    p.text = definition.title or ""
    p.alignment = PP_ALIGN.LEFT
    _apply_line_spacing(p, brand)
    run = p.runs[0]
    run.font.name = brand.fonts.heading_font
    run.font.size = Pt(brand.fonts.heading_size)
    run.font.bold = True
    run.font.color.rgb = parse_hex_color(brand.colors.primary)

    if definition.subtitle:
        sub_box = slide.shapes.add_textbox(
            Inches(sp.margin_left),
            Inches(4.7),
            Inches(width * 0.6),
            Inches(0.8),
        )
        tf2 = sub_box.text_frame
        tf2.word_wrap = True
        p2 = tf2.paragraphs[0]
        p2.text = definition.subtitle
        _apply_line_spacing(p2, brand)
        run2 = p2.runs[0]
        run2.font.name = brand.fonts.body_font
        run2.font.size = Pt(brand.fonts.body_size)
        run2.font.color.rgb = parse_hex_color(brand.colors.text_secondary)


def render_content_slide(
    slide: Slide,
    definition: SlideDefinition,
    brand: BrandConfig,
    slide_width: float,
) -> None:
    """Render a standard content slide with title and bullets."""
    sp = brand.spacing
    width = slide_width - sp.margin_left - sp.margin_right

    _add_slide_title(slide, definition.title, brand, slide_width)

    if definition.body:
        body_box = slide.shapes.add_textbox(
            Inches(sp.margin_left),
            Inches(sp.content_top),
            Inches(width),
            Inches(5.5 - sp.content_top),
        )
        tf = body_box.text_frame
        tf.word_wrap = True
        _apply_text_frame_padding(tf, brand)
        p = tf.paragraphs[0]
        p.text = definition.body
        _apply_line_spacing(p, brand)
        run = p.runs[0]
        run.font.name = brand.fonts.body_font
        run.font.size = Pt(brand.fonts.body_size)
        run.font.color.rgb = parse_hex_color(brand.colors.text_primary)

    if definition.bullets:
        bullet_box = slide.shapes.add_textbox(
            Inches(sp.margin_left),
            Inches(sp.content_top + (0.8 if definition.body else 0.0)),
            Inches(width),
            Inches(5.0),
        )
        tf = bullet_box.text_frame
        tf.word_wrap = True
        _apply_text_frame_padding(tf, brand)

        for i, bullet in enumerate(definition.bullets):
            if i == 0:
                p = tf.paragraphs[0]
            else:
                p = tf.add_paragraph()
            p.text = bullet
            p.level = 0
            p.space_after = Pt(8)
            _apply_line_spacing(p, brand)
            _set_bullet(p)
            run = p.runs[0]
            run.font.name = brand.fonts.body_font
            run.font.size = Pt(brand.fonts.body_size)
            run.font.color.rgb = parse_hex_color(brand.colors.text_primary)


def render_two_column_slide(
    slide: Slide,
    definition: SlideDefinition,
    brand: BrandConfig,
    slide_width: float,
) -> None:
    """Render a two-column layout slide."""
    sp = brand.spacing
    total_width = slide_width - sp.margin_left - sp.margin_right
    col_width = (total_width - sp.column_gap) / 2

    _add_slide_title(slide, definition.title, brand, slide_width)

    columns = definition.columns[:2] if definition.columns else []

    for col_idx, col in enumerate(columns):
        left = sp.margin_left + col_idx * (col_width + sp.column_gap)
        _render_column(slide, col, left, sp.content_top, col_width, brand)


def render_pros_cons_slide(
    slide: Slide,
    definition: SlideDefinition,
    brand: BrandConfig,
    slide_width: float,
) -> None:
    """Render a pros/cons comparison slide."""
    sp = brand.spacing
    total_width = slide_width - sp.margin_left - sp.margin_right
    col_width = (total_width - sp.column_gap) / 2

    _add_slide_title(slide, definition.title, brand, slide_width)

    # Pros column
    _render_labeled_list(
        slide,
        label="Pros",
        items=definition.pros,
        left=sp.margin_left,
        top=sp.content_top,
        width=col_width,
        brand=brand,
        color=brand.colors.success,
        bullet_char="\u2713",
    )

    # Cons column
    _render_labeled_list(
        slide,
        label="Cons",
        items=definition.cons,
        left=sp.margin_left + col_width + sp.column_gap,
        top=sp.content_top,
        width=col_width,
        brand=brand,
        color=brand.colors.danger,
        bullet_char="\u2717",
    )


def render_comparison_slide(
    slide: Slide,
    definition: SlideDefinition,
    brand: BrandConfig,
    slide_width: float,
) -> None:
    """Render a multi-column comparison slide."""
    sp = brand.spacing
    total_width = slide_width - sp.margin_left - sp.margin_right
    num_cols = len(definition.columns) or 2
    col_width = (total_width - sp.column_gap * (num_cols - 1)) / num_cols

    _add_slide_title(slide, definition.title, brand, slide_width)

    for col_idx, col in enumerate(definition.columns):
        left = sp.margin_left + col_idx * (col_width + sp.column_gap)
        _render_column(slide, col, left, sp.content_top, col_width, brand, show_header_bar=True)


def _add_slide_title(
    slide: Slide,
    title: str | None,
    brand: BrandConfig,
    slide_width: float,
) -> None:
    """Add a title bar to a slide."""
    if not title:
        return
    sp = brand.spacing
    width = slide_width - sp.margin_left - sp.margin_right

    title_box = slide.shapes.add_textbox(
        Inches(sp.margin_left),
        Inches(sp.title_top),
        Inches(width),
        Inches(0.8),
    )
    tf = title_box.text_frame
    tf.word_wrap = True
    p = tf.paragraphs[0]
    p.text = title
    p.alignment = PP_ALIGN.LEFT
    _apply_line_spacing(p, brand)
    run = p.runs[0]
    run.font.name = brand.fonts.heading_font
    run.font.size = Pt(brand.fonts.heading_size)
    run.font.bold = True
    run.font.color.rgb = parse_hex_color(brand.colors.primary)

    # Accent underline
    bar = slide.shapes.add_shape(
        MSO_SHAPE.RECTANGLE,
        Inches(sp.margin_left),
        Inches(sp.title_top + 0.85),
        Inches(1.5),
        Inches(0.04),
    )
    bar.fill.solid()
    bar.fill.fore_color.rgb = parse_hex_color(brand.colors.accent)
    bar.line.fill.background()


def _render_column(
    slide: Slide,
    col: ColumnContent,
    left: float,
    top: float,
    width: float,
    brand: BrandConfig,
    show_header_bar: bool = False,
) -> None:
    """Render a single column of content."""
    current_top = top

    if show_header_bar and col.title:
        bar = slide.shapes.add_shape(
            MSO_SHAPE.RECTANGLE,
            Inches(left),
            Inches(current_top),
            Inches(width),
            Inches(0.5),
        )
        bar.fill.solid()
        bar.fill.fore_color.rgb = parse_hex_color(brand.colors.primary)
        bar.line.fill.background()

        tf = bar.text_frame
        tf.vertical_anchor = MSO_ANCHOR.MIDDLE
        p = tf.paragraphs[0]
        p.text = col.title
        p.alignment = PP_ALIGN.CENTER
        run = p.runs[0]
        run.font.name = brand.fonts.heading_font
        run.font.size = Pt(brand.fonts.subheading_size - 2)
        run.font.bold = True
        run.font.color.rgb = RGBColor(0xFF, 0xFF, 0xFF)
        current_top += 0.6
    elif col.title:
        header = slide.shapes.add_textbox(
            Inches(left), Inches(current_top), Inches(width), Inches(0.5)
        )
        tf = header.text_frame
        tf.word_wrap = True
        p = tf.paragraphs[0]
        p.text = col.title
        p.alignment = PP_ALIGN.LEFT
        run = p.runs[0]
        run.font.name = brand.fonts.heading_font
        run.font.size = Pt(brand.fonts.subheading_size)
        run.font.bold = True
        run.font.color.rgb = parse_hex_color(brand.colors.secondary)
        current_top += 0.6

    if col.bullets:
        box = slide.shapes.add_textbox(
            Inches(left),
            Inches(current_top),
            Inches(width),
            Inches(4.5),
        )
        tf = box.text_frame
        tf.word_wrap = True
        _apply_text_frame_padding(tf, brand)
        for i, bullet in enumerate(col.bullets):
            p = tf.paragraphs[0] if i == 0 else tf.add_paragraph()
            p.text = bullet
            p.space_after = Pt(6)
            _apply_line_spacing(p, brand)
            _set_bullet(p)
            run = p.runs[0]
            run.font.name = brand.fonts.body_font
            run.font.size = Pt(brand.fonts.body_size)
            run.font.color.rgb = parse_hex_color(brand.colors.text_primary)


def _render_labeled_list(
    slide: Slide,
    label: str,
    items: list[str],
    left: float,
    top: float,
    width: float,
    brand: BrandConfig,
    color: str,
    bullet_char: str = "\u2022",
) -> None:
    """Render a labeled list (for pros/cons)."""
    header = slide.shapes.add_textbox(
        Inches(left), Inches(top), Inches(width), Inches(0.5)
    )
    tf = header.text_frame
    p = tf.paragraphs[0]
    p.text = label
    p.alignment = PP_ALIGN.LEFT
    run = p.runs[0]
    run.font.name = brand.fonts.heading_font
    run.font.size = Pt(brand.fonts.subheading_size)
    run.font.bold = True
    run.font.color.rgb = parse_hex_color(color)

    if items:
        box = slide.shapes.add_textbox(
            Inches(left), Inches(top + 0.6), Inches(width), Inches(4.0)
        )
        tf2 = box.text_frame
        tf2.word_wrap = True
        _apply_text_frame_padding(tf2, brand)
        for i, item in enumerate(items):
            p = tf2.paragraphs[0] if i == 0 else tf2.add_paragraph()
            p.text = f"{bullet_char}  {item}"
            p.space_after = Pt(6)
            _apply_line_spacing(p, brand)
            run = p.runs[0]
            run.font.name = brand.fonts.body_font
            run.font.size = Pt(brand.fonts.body_size)
            run.font.color.rgb = parse_hex_color(brand.colors.text_primary)


def _set_bullet(paragraph: object) -> None:
    """Enable bullet formatting on a paragraph via XML manipulation."""
    from lxml import etree

    pPr = paragraph._p.get_or_add_pPr()  # type: ignore[attr-defined]
    nsmap = {"a": "http://schemas.openxmlformats.org/drawingml/2006/main"}
    buNone = pPr.findall("a:buNone", nsmap)
    for el in buNone:
        pPr.remove(el)
    buChar = etree.SubElement(pPr, f"{{{nsmap['a']}}}buChar")
    buChar.set("char", "\u2022")


# ---------------------------------------------------------------------------
# Advanced layout renderers
# ---------------------------------------------------------------------------


def render_quote_slide(
    slide: Slide,
    definition: SlideDefinition,
    brand: BrandConfig,
    slide_width: float,
) -> None:
    """Render a quote slide with large quote text and attribution."""
    sp = brand.spacing
    width = slide_width - sp.margin_left - sp.margin_right

    # Background decorative circle (subtle)
    circle = slide.shapes.add_shape(
        MSO_SHAPE.OVAL,
        Inches(slide_width - 5.0),
        Inches(1.0),
        Inches(6.0),
        Inches(6.0),
    )
    circle.fill.solid()
    circle.fill.fore_color.rgb = parse_hex_color(brand.colors.secondary)
    # Set 8% opacity via XML
    _set_shape_opacity(circle, 8)
    circle.line.fill.background()

    # Open quote mark
    quote_mark = slide.shapes.add_textbox(
        Inches(sp.margin_left + 1.5),
        Inches(1.4),
        Inches(1.0),
        Inches(1.0),
    )
    tf = quote_mark.text_frame
    p = tf.paragraphs[0]
    p.text = "\u201C"
    p.alignment = PP_ALIGN.LEFT
    run = p.runs[0]
    run.font.name = brand.fonts.heading_font
    run.font.size = Pt(72)
    run.font.color.rgb = parse_hex_color(brand.colors.accent)

    # Quote text (body field)
    quote_text = definition.body or definition.title or ""
    quote_box = slide.shapes.add_textbox(
        Inches(sp.margin_left + 1.75),
        Inches(2.2),
        Inches(width - 3.5),
        Inches(2.0),
    )
    tf = quote_box.text_frame
    tf.word_wrap = True
    p = tf.paragraphs[0]
    p.text = quote_text
    p.alignment = PP_ALIGN.LEFT
    _apply_line_spacing(p, brand)
    run = p.runs[0]
    run.font.name = brand.fonts.body_font
    run.font.size = Pt(28)
    run.font.italic = True
    run.font.color.rgb = parse_hex_color(brand.colors.primary)

    # Accent bar before attribution
    bar = slide.shapes.add_shape(
        MSO_SHAPE.RECTANGLE,
        Inches(sp.margin_left + 1.75),
        Inches(4.5),
        Inches(1.5),
        Inches(0.035),
    )
    bar.fill.solid()
    bar.fill.fore_color.rgb = parse_hex_color(brand.colors.accent)
    bar.line.fill.background()

    # Attribution name
    attribution = definition.quote_attribution or definition.subtitle or ""
    if attribution:
        attr_box = slide.shapes.add_textbox(
            Inches(sp.margin_left + 1.75),
            Inches(4.7),
            Inches(width - 3.5),
            Inches(0.4),
        )
        tf = attr_box.text_frame
        p = tf.paragraphs[0]
        p.text = f"\u2014 {attribution}"
        p.alignment = PP_ALIGN.LEFT
        run = p.runs[0]
        run.font.name = brand.fonts.heading_font
        run.font.size = Pt(16)
        run.font.bold = True
        run.font.color.rgb = parse_hex_color(brand.colors.text_primary)

    # Attribution role
    role = definition.quote_attribution_role
    if role:
        role_box = slide.shapes.add_textbox(
            Inches(sp.margin_left + 1.75),
            Inches(5.15),
            Inches(width - 3.5),
            Inches(0.35),
        )
        tf = role_box.text_frame
        p = tf.paragraphs[0]
        p.text = role
        p.alignment = PP_ALIGN.LEFT
        run = p.runs[0]
        run.font.name = brand.fonts.body_font
        run.font.size = Pt(14)
        run.font.color.rgb = parse_hex_color(brand.colors.text_secondary)


def render_big_number_slide(
    slide: Slide,
    definition: SlideDefinition,
    brand: BrandConfig,
    slide_width: float,
) -> None:
    """Render a big number / hero metric slide.

    Uses body as the large number text, title as the label,
    and subtitle as optional context line above the number.
    """
    grid = LayoutGrid(
        slide_width=slide_width, slide_height=7.5,
        margin=Margin(left=0.75, right=0.75, top=1.5, bottom=0.5),
    )
    center = grid.centered_cell(width=10.0, height=4.0, vertical_offset=-0.3)

    # Context text (above the number)
    if definition.subtitle:
        ctx_box = slide.shapes.add_textbox(
            Inches(center.left), Inches(center.top),
            Inches(center.width), Inches(0.45),
        )
        tf = ctx_box.text_frame
        tf.word_wrap = True
        p = tf.paragraphs[0]
        p.text = definition.subtitle
        p.alignment = PP_ALIGN.CENTER
        run = p.runs[0]
        run.font.name = brand.fonts.body_font
        run.font.size = Pt(16)
        run.font.color.rgb = parse_hex_color(brand.colors.text_secondary)

    # Hero number (body field)
    number_text = definition.body or "0"
    num_box = slide.shapes.add_textbox(
        Inches(center.left), Inches(center.top + 0.6),
        Inches(center.width), Inches(1.6),
    )
    tf = num_box.text_frame
    tf.word_wrap = True
    p = tf.paragraphs[0]
    p.text = number_text
    p.alignment = PP_ALIGN.CENTER
    run = p.runs[0]
    run.font.name = brand.fonts.heading_font
    run.font.size = Pt(96)
    run.font.bold = True
    run.font.color.rgb = parse_hex_color(brand.colors.accent)

    # Label (title)
    if definition.title:
        label_box = slide.shapes.add_textbox(
            Inches(center.left), Inches(center.top + 2.4),
            Inches(center.width), Inches(0.5),
        )
        tf = label_box.text_frame
        tf.word_wrap = True
        p = tf.paragraphs[0]
        p.text = definition.title
        p.alignment = PP_ALIGN.CENTER
        run = p.runs[0]
        run.font.name = brand.fonts.body_font
        run.font.size = Pt(20)
        run.font.color.rgb = parse_hex_color(brand.colors.text_secondary)

    # Optional comparison text (first bullet)
    if definition.bullets:
        cmp_box = slide.shapes.add_textbox(
            Inches(center.left + 1.0), Inches(center.top + 3.1),
            Inches(center.width - 2.0), Inches(0.4),
        )
        tf = cmp_box.text_frame
        p = tf.paragraphs[0]
        p.text = definition.bullets[0]
        p.alignment = PP_ALIGN.CENTER
        run = p.runs[0]
        run.font.name = brand.fonts.body_font
        run.font.size = Pt(14)
        run.font.color.rgb = parse_hex_color(brand.colors.text_secondary)


def render_process_slide(
    slide: Slide,
    definition: SlideDefinition,
    brand: BrandConfig,
    slide_width: float,
) -> None:
    """Render a horizontal chevron process flow (2-5 steps)."""
    _add_slide_title(slide, definition.title, brand, slide_width)

    steps = definition.steps or []
    if not steps:
        return

    n = min(len(steps), 5)
    grid = _make_grid(brand, slide_width)
    chevron_h = 1.3
    desc_h = 1.0
    chevron_top = grid.content_top + 0.3
    cells = grid.columns(n, gap=0.2, row_top=chevron_top, row_height=chevron_h)

    active = definition.active_step if definition.active_step is not None else -1

    for i, step in enumerate(steps[:n]):
        cell = cells[i]

        # Color based on status
        if step.status == "completed" or i < active:
            fill = brand.colors.primary
        elif step.status == "active" or i == active:
            fill = brand.colors.accent
        else:
            fill = brand.colors.text_secondary

        # Chevron shape
        chev = slide.shapes.add_shape(
            MSO_SHAPE.CHEVRON,
            Inches(cell.left), Inches(cell.top),
            Inches(cell.width), Inches(cell.height),
        )
        chev.fill.solid()
        chev.fill.fore_color.rgb = parse_hex_color(fill)
        chev.line.fill.background()

        tf = chev.text_frame
        tf.vertical_anchor = MSO_ANCHOR.MIDDLE
        p = tf.paragraphs[0]
        p.text = f"{i + 1}. {step.title}"
        p.alignment = PP_ALIGN.CENTER
        run = p.runs[0]
        run.font.name = brand.fonts.heading_font
        run.font.size = Pt(14)
        run.font.bold = True
        run.font.color.rgb = RGBColor(0xFF, 0xFF, 0xFF)

        # Description below chevron
        if step.description:
            desc_box = slide.shapes.add_textbox(
                Inches(cell.left), Inches(cell.top + chevron_h + 0.15),
                Inches(cell.width), Inches(desc_h),
            )
            tf = desc_box.text_frame
            tf.word_wrap = True
            _apply_text_frame_padding(tf, brand)
            p = tf.paragraphs[0]
            p.text = step.description
            p.alignment = PP_ALIGN.CENTER
            _apply_line_spacing(p, brand)
            run = p.runs[0]
            run.font.name = brand.fonts.body_font
            run.font.size = Pt(12)
            run.font.color.rgb = parse_hex_color(brand.colors.text_secondary)


def render_timeline_slide(
    slide: Slide,
    definition: SlideDefinition,
    brand: BrandConfig,
    slide_width: float,
) -> None:
    """Render a vertical timeline with milestone dots and content."""
    sp = brand.spacing
    _add_slide_title(slide, definition.title, brand, slide_width)

    steps = definition.steps or []
    if not steps:
        return

    n = min(len(steps), 5)
    active = definition.active_step if definition.active_step is not None else -1

    # Timeline spine
    timeline_x = 3.0
    timeline_top = sp.content_top + 0.2
    timeline_bottom = 6.5
    spine_height = timeline_bottom - timeline_top
    milestone_gap = spine_height / max(n - 1, 1) if n > 1 else 0

    spine = slide.shapes.add_shape(
        MSO_SHAPE.RECTANGLE,
        Inches(timeline_x - 0.02),
        Inches(timeline_top),
        Inches(0.04),
        Inches(spine_height),
    )
    spine.fill.solid()
    spine.fill.fore_color.rgb = parse_hex_color(brand.colors.text_secondary)
    spine.line.fill.background()

    dot_r = 0.167

    for i, step in enumerate(steps[:n]):
        y = timeline_top + i * milestone_gap

        # Determine color
        if step.status == "completed" or i < active:
            dot_fill = brand.colors.primary
        elif step.status == "active" or i == active:
            dot_fill = brand.colors.accent
        else:
            dot_fill = "#C0C0C0"

        # Milestone dot
        dot = slide.shapes.add_shape(
            MSO_SHAPE.OVAL,
            Inches(timeline_x - dot_r),
            Inches(y - dot_r),
            Inches(dot_r * 2),
            Inches(dot_r * 2),
        )
        dot.fill.solid()
        dot.fill.fore_color.rgb = parse_hex_color(dot_fill)
        dot.line.fill.background()

        # Date label (left side)
        if step.date:
            date_box = slide.shapes.add_textbox(
                Inches(sp.margin_left),
                Inches(y - 0.15),
                Inches(1.8),
                Inches(0.35),
            )
            tf = date_box.text_frame
            p = tf.paragraphs[0]
            p.text = step.date
            p.alignment = PP_ALIGN.RIGHT
            run = p.runs[0]
            run.font.name = brand.fonts.body_font
            run.font.size = Pt(12)
            run.font.bold = True
            run.font.color.rgb = parse_hex_color(dot_fill)

        # Title (right side)
        content_left = timeline_x + dot_r + 0.35
        content_w = slide_width - sp.margin_right - content_left
        title_box = slide.shapes.add_textbox(
            Inches(content_left),
            Inches(y - 0.2),
            Inches(content_w),
            Inches(0.35),
        )
        tf = title_box.text_frame
        p = tf.paragraphs[0]
        p.text = step.title
        p.alignment = PP_ALIGN.LEFT
        run = p.runs[0]
        run.font.name = brand.fonts.heading_font
        run.font.size = Pt(18)
        run.font.bold = True
        run.font.color.rgb = parse_hex_color(brand.colors.text_primary)

        # Description (right side, below title)
        if step.description:
            desc_box = slide.shapes.add_textbox(
                Inches(content_left),
                Inches(y + 0.2),
                Inches(content_w),
                Inches(0.5),
            )
            tf = desc_box.text_frame
            tf.word_wrap = True
            p = tf.paragraphs[0]
            p.text = step.description
            p.alignment = PP_ALIGN.LEFT
            _apply_line_spacing(p, brand)
            run = p.runs[0]
            run.font.name = brand.fonts.body_font
            run.font.size = Pt(13)
            run.font.color.rgb = parse_hex_color(brand.colors.text_secondary)


def render_step_cards_slide(
    slide: Slide,
    definition: SlideDefinition,
    brand: BrandConfig,
    slide_width: float,
) -> None:
    """Render numbered step cards in a horizontal row (2-5 cards)."""
    _add_slide_title(slide, definition.title, brand, slide_width)

    steps = definition.steps or []
    if not steps:
        return

    n = min(len(steps), 5)
    grid = _make_grid(brand, slide_width)
    card_h = 3.5
    card_top = grid.content_top + 0.4
    cells = grid.columns(n, gap=0.333, row_top=card_top, row_height=card_h)

    for i, step in enumerate(steps[:n]):
        cell = cells[i]

        # Card background
        card = slide.shapes.add_shape(
            MSO_SHAPE.ROUNDED_RECTANGLE,
            Inches(cell.left), Inches(cell.top),
            Inches(cell.width), Inches(cell.height),
        )
        card.fill.solid()
        card.fill.fore_color.rgb = parse_hex_color("#F7F7F7")
        card.line.color.rgb = parse_hex_color("#E0E0E0")

        # Top accent border
        accent = slide.shapes.add_shape(
            MSO_SHAPE.RECTANGLE,
            Inches(cell.left), Inches(cell.top),
            Inches(cell.width), Inches(0.05),
        )
        accent.fill.solid()
        accent.fill.fore_color.rgb = parse_hex_color(brand.colors.primary)
        accent.line.fill.background()

        inner = cell.inset(0.22)

        # Step number
        num_box = slide.shapes.add_textbox(
            Inches(inner.left), Inches(inner.top + 0.08),
            Inches(inner.width), Inches(0.65),
        )
        tf = num_box.text_frame
        p = tf.paragraphs[0]
        p.text = str(i + 1).zfill(2)
        p.alignment = PP_ALIGN.LEFT
        run = p.runs[0]
        run.font.name = brand.fonts.heading_font
        run.font.size = Pt(44)
        run.font.bold = True
        run.font.color.rgb = parse_hex_color(brand.colors.accent)

        # Step title
        title_box = slide.shapes.add_textbox(
            Inches(inner.left), Inches(inner.top + 0.88),
            Inches(inner.width), Inches(0.4),
        )
        tf = title_box.text_frame
        tf.word_wrap = True
        p = tf.paragraphs[0]
        p.text = step.title
        p.alignment = PP_ALIGN.LEFT
        run = p.runs[0]
        run.font.name = brand.fonts.heading_font
        run.font.size = Pt(18)
        run.font.bold = True
        run.font.color.rgb = parse_hex_color(brand.colors.text_primary)

        # Step description
        if step.description:
            desc_box = slide.shapes.add_textbox(
                Inches(inner.left), Inches(inner.top + 1.38),
                Inches(inner.width), Inches(1.5),
            )
            tf = desc_box.text_frame
            tf.word_wrap = True
            p = tf.paragraphs[0]
            p.text = step.description
            p.alignment = PP_ALIGN.LEFT
            _apply_line_spacing(p, brand)
            run = p.runs[0]
            run.font.name = brand.fonts.body_font
            run.font.size = Pt(13)
            run.font.color.rgb = parse_hex_color(brand.colors.text_secondary)


def render_dashboard_slide(
    slide: Slide,
    definition: SlideDefinition,
    brand: BrandConfig,
    slide_width: float,
) -> None:
    """Render a KPI dashboard with metric cards arranged in an auto-grid."""
    _add_slide_title(slide, definition.title, brand, slide_width)

    kpis = definition.kpis or []
    if not kpis:
        return

    n = min(len(kpis), 12)
    grid = _make_grid(brand, slide_width)
    cells = grid.auto_layout(n, max_cols=4, h_gap=0.35, v_gap=0.35)

    for i, kpi in enumerate(kpis[:n]):
        cell = cells[i]
        accent = kpi.color or brand.colors.accent

        # Card background
        card = slide.shapes.add_shape(
            MSO_SHAPE.ROUNDED_RECTANGLE,
            Inches(cell.left), Inches(cell.top),
            Inches(cell.width), Inches(cell.height),
        )
        card.fill.solid()
        card.fill.fore_color.rgb = parse_hex_color("#F8F9FA")
        card.line.color.rgb = parse_hex_color("#E8E8E8")

        inner = cell.inset(0.2)

        # Value (large)
        val_box = slide.shapes.add_textbox(
            Inches(inner.left), Inches(inner.top),
            Inches(inner.width), Inches(inner.height * 0.5),
        )
        tf = val_box.text_frame
        tf.word_wrap = True
        p = tf.paragraphs[0]
        p.text = kpi.value
        p.alignment = PP_ALIGN.CENTER
        run = p.runs[0]
        run.font.name = brand.fonts.heading_font
        run.font.size = Pt(36)
        run.font.bold = True
        run.font.color.rgb = parse_hex_color(accent)

        # Label
        lbl_box = slide.shapes.add_textbox(
            Inches(inner.left), Inches(inner.top + inner.height * 0.5),
            Inches(inner.width), Inches(inner.height * 0.25),
        )
        tf = lbl_box.text_frame
        tf.word_wrap = True
        p = tf.paragraphs[0]
        p.text = kpi.label
        p.alignment = PP_ALIGN.CENTER
        run = p.runs[0]
        run.font.name = brand.fonts.body_font
        run.font.size = Pt(12)
        run.font.color.rgb = parse_hex_color(brand.colors.text_secondary)

        # Change indicator
        if kpi.change:
            chg_box = slide.shapes.add_textbox(
                Inches(inner.left), Inches(inner.top + inner.height * 0.75),
                Inches(inner.width), Inches(inner.height * 0.2),
            )
            tf = chg_box.text_frame
            p = tf.paragraphs[0]
            arrow = {"up": "\u25B2", "down": "\u25BC", "neutral": "\u25CF"}.get(kpi.trend, "")
            p.text = f"{arrow} {kpi.change}"
            p.alignment = PP_ALIGN.CENTER
            run = p.runs[0]
            run.font.name = brand.fonts.body_font
            run.font.size = Pt(11)
            trend_color = {
                "up": brand.colors.success,
                "down": brand.colors.danger,
                "neutral": brand.colors.text_secondary,
            }.get(kpi.trend, brand.colors.text_secondary)
            run.font.color.rgb = parse_hex_color(trend_color)


def render_image_text_slide(
    slide: Slide,
    definition: SlideDefinition,
    brand: BrandConfig,
    slide_width: float,
) -> None:
    """Render a split image + text layout.

    Uses image_path + body/bullets for content. image_position controls
    whether image is left or right.
    """
    _add_slide_title(slide, definition.title, brand, slide_width)

    grid = _make_grid(brand, slide_width)
    img_on_left = definition.image_position == "left"
    left_cell, right_cell = grid.side_by_side(left_ratio=0.45, gap=0.5)
    img_cell = left_cell if img_on_left else right_cell
    txt_cell = right_cell if img_on_left else left_cell

    # Image placeholder (gray box if no image_path, or actual image)
    if definition.image_path:
        from pptx_mcp.utils.paths import validate_image_path

        try:
            path = validate_image_path(definition.image_path)
            slide.shapes.add_picture(
                str(path),
                Inches(img_cell.left), Inches(img_cell.top),
                Inches(img_cell.width), Inches(img_cell.height),
            )
        except Exception:
            logger.warning("image_text: invalid image path '%s'", definition.image_path)
    else:
        # Gray placeholder
        ph = slide.shapes.add_shape(
            MSO_SHAPE.ROUNDED_RECTANGLE,
            Inches(img_cell.left), Inches(img_cell.top),
            Inches(img_cell.width), Inches(img_cell.height),
        )
        ph.fill.solid()
        ph.fill.fore_color.rgb = parse_hex_color("#E8E8E8")
        ph.line.fill.background()

    # Text content
    if definition.body:
        body_box = slide.shapes.add_textbox(
            Inches(txt_cell.left), Inches(txt_cell.top),
            Inches(txt_cell.width), Inches(txt_cell.height * 0.4),
        )
        tf = body_box.text_frame
        tf.word_wrap = True
        _apply_text_frame_padding(tf, brand)
        p = tf.paragraphs[0]
        p.text = definition.body
        _apply_line_spacing(p, brand)
        run = p.runs[0]
        run.font.name = brand.fonts.body_font
        run.font.size = Pt(brand.fonts.body_size)
        run.font.color.rgb = parse_hex_color(brand.colors.text_primary)

    if definition.bullets:
        bullet_top = txt_cell.top + (txt_cell.height * 0.4 if definition.body else 0.0)
        box = slide.shapes.add_textbox(
            Inches(txt_cell.left), Inches(bullet_top),
            Inches(txt_cell.width), Inches(txt_cell.height * 0.6),
        )
        tf = box.text_frame
        tf.word_wrap = True
        _apply_text_frame_padding(tf, brand)
        for i, bullet in enumerate(definition.bullets):
            p = tf.paragraphs[0] if i == 0 else tf.add_paragraph()
            p.text = bullet
            p.space_after = Pt(6)
            _apply_line_spacing(p, brand)
            _set_bullet(p)
            run = p.runs[0]
            run.font.name = brand.fonts.body_font
            run.font.size = Pt(brand.fonts.body_size)
            run.font.color.rgb = parse_hex_color(brand.colors.text_primary)


def render_icon_grid_slide(
    slide: Slide,
    definition: SlideDefinition,
    brand: BrandConfig,
    slide_width: float,
) -> None:
    """Render an icon grid with icon + title + description cards."""
    _add_slide_title(slide, definition.title, brand, slide_width)

    icons = definition.icons or []
    if not icons:
        return

    n = min(len(icons), 12)
    grid = _make_grid(brand, slide_width)
    cells = grid.auto_layout(n, max_cols=4, h_gap=0.35, v_gap=0.3)

    for i, item in enumerate(icons[:n]):
        cell = cells[i]

        # Icon (emoji or character)
        icon_box = slide.shapes.add_textbox(
            Inches(cell.left), Inches(cell.top),
            Inches(cell.width), Inches(cell.height * 0.4),
        )
        tf = icon_box.text_frame
        p = tf.paragraphs[0]
        p.text = item.icon
        p.alignment = PP_ALIGN.CENTER
        run = p.runs[0]
        run.font.size = Pt(36)

        # Title
        title_box = slide.shapes.add_textbox(
            Inches(cell.left), Inches(cell.top + cell.height * 0.4),
            Inches(cell.width), Inches(cell.height * 0.25),
        )
        tf = title_box.text_frame
        tf.word_wrap = True
        p = tf.paragraphs[0]
        p.text = item.title
        p.alignment = PP_ALIGN.CENTER
        run = p.runs[0]
        run.font.name = brand.fonts.heading_font
        run.font.size = Pt(16)
        run.font.bold = True
        run.font.color.rgb = parse_hex_color(brand.colors.text_primary)

        # Description
        if item.description:
            desc_box = slide.shapes.add_textbox(
                Inches(cell.left), Inches(cell.top + cell.height * 0.65),
                Inches(cell.width), Inches(cell.height * 0.35),
            )
            tf = desc_box.text_frame
            tf.word_wrap = True
            p = tf.paragraphs[0]
            p.text = item.description
            p.alignment = PP_ALIGN.CENTER
            _apply_line_spacing(p, brand)
            run = p.runs[0]
            run.font.name = brand.fonts.body_font
            run.font.size = Pt(12)
            run.font.color.rgb = parse_hex_color(brand.colors.text_secondary)


def _set_shape_opacity(shape: object, opacity_pct: int) -> None:
    """Set fill opacity on a shape (0-100)."""
    from lxml import etree

    spPr = shape._element.spPr  # type: ignore[attr-defined]
    nsmap = {"a": "http://schemas.openxmlformats.org/drawingml/2006/main"}
    fills = spPr.findall("a:solidFill", nsmap)
    for fill_el in fills:
        clr_els = fill_el.findall("a:srgbClr", nsmap)
        for clr in clr_els:
            # Remove existing alpha
            for alpha in clr.findall("a:alpha", nsmap):
                clr.remove(alpha)
            alpha_el = etree.SubElement(clr, f"{{{nsmap['a']}}}alpha")
            alpha_el.set("val", str(opacity_pct * 1000))


# ---------------------------------------------------------------------------
# Production layout renderers (Wave 1)
# ---------------------------------------------------------------------------


def render_team_slide(
    slide: Slide,
    definition: SlideDefinition,
    brand: BrandConfig,
    slide_width: float,
) -> None:
    """Render a team/bio slide with 2-4 member cards in a horizontal row."""
    _add_slide_title(slide, definition.title, brand, slide_width)

    members = definition.team_members or []
    if not members:
        return

    n = min(len(members), 4)
    grid = _make_grid(brand, slide_width)
    card_h = 3.8
    card_top = grid.content_top + 0.3
    cells = grid.columns(n, gap=0.4, row_top=card_top, row_height=card_h)

    for i, member in enumerate(members[:n]):
        cell = cells[i]

        # Card background
        card = slide.shapes.add_shape(
            MSO_SHAPE.ROUNDED_RECTANGLE,
            Inches(cell.left), Inches(cell.top),
            Inches(cell.width), Inches(cell.height),
        )
        card.fill.solid()
        card.fill.fore_color.rgb = parse_hex_color("#F7F7F7")
        card.line.color.rgb = parse_hex_color("#E0E0E0")

        inner = cell.inset(0.25)

        # Avatar circle with initials
        avatar_size = 0.9
        avatar_left = inner.left + (inner.width - avatar_size) / 2
        avatar = slide.shapes.add_shape(
            MSO_SHAPE.OVAL,
            Inches(avatar_left), Inches(inner.top + 0.15),
            Inches(avatar_size), Inches(avatar_size),
        )
        avatar.fill.solid()
        avatar.fill.fore_color.rgb = parse_hex_color(brand.colors.primary)
        avatar.line.fill.background()

        # Initials text inside avatar
        tf = avatar.text_frame
        tf.vertical_anchor = MSO_ANCHOR.MIDDLE
        p = tf.paragraphs[0]
        initials = "".join(w[0].upper() for w in member.name.split()[:2])
        p.text = initials
        p.alignment = PP_ALIGN.CENTER
        run = p.runs[0]
        run.font.name = brand.fonts.heading_font
        run.font.size = Pt(24)
        run.font.bold = True
        run.font.color.rgb = RGBColor(0xFF, 0xFF, 0xFF)

        # Name
        name_box = slide.shapes.add_textbox(
            Inches(inner.left), Inches(inner.top + 1.25),
            Inches(inner.width), Inches(0.4),
        )
        tf = name_box.text_frame
        tf.word_wrap = True
        p = tf.paragraphs[0]
        p.text = member.name
        p.alignment = PP_ALIGN.CENTER
        run = p.runs[0]
        run.font.name = brand.fonts.heading_font
        run.font.size = Pt(16)
        run.font.bold = True
        run.font.color.rgb = parse_hex_color(brand.colors.text_primary)

        # Role
        role_box = slide.shapes.add_textbox(
            Inches(inner.left), Inches(inner.top + 1.65),
            Inches(inner.width), Inches(0.35),
        )
        tf = role_box.text_frame
        tf.word_wrap = True
        p = tf.paragraphs[0]
        p.text = member.role
        p.alignment = PP_ALIGN.CENTER
        run = p.runs[0]
        run.font.name = brand.fonts.body_font
        run.font.size = Pt(12)
        run.font.color.rgb = parse_hex_color(brand.colors.accent)

        # Bio
        if member.bio:
            bio_box = slide.shapes.add_textbox(
                Inches(inner.left), Inches(inner.top + 2.1),
                Inches(inner.width), Inches(1.2),
            )
            tf = bio_box.text_frame
            tf.word_wrap = True
            _apply_text_frame_padding(tf, brand)
            p = tf.paragraphs[0]
            p.text = member.bio
            p.alignment = PP_ALIGN.CENTER
            _apply_line_spacing(p, brand)
            run = p.runs[0]
            run.font.name = brand.fonts.body_font
            run.font.size = Pt(11)
            run.font.color.rgb = parse_hex_color(brand.colors.text_secondary)


def render_closing_slide(
    slide: Slide,
    definition: SlideDefinition,
    brand: BrandConfig,
    slide_width: float,
) -> None:
    """Render a closing / CTA slide with centered message and optional contact info."""
    grid = LayoutGrid(
        slide_width=slide_width, slide_height=7.5,
        margin=Margin(left=0.75, right=0.75, top=1.0, bottom=0.5),
    )
    center = grid.centered_cell(width=10.0, height=4.5, vertical_offset=-0.3)

    # Accent bar divider
    bar_width = 2.0
    bar = slide.shapes.add_shape(
        MSO_SHAPE.RECTANGLE,
        Inches(center.left + (center.width - bar_width) / 2),
        Inches(center.top),
        Inches(bar_width), Inches(0.05),
    )
    bar.fill.solid()
    bar.fill.fore_color.rgb = parse_hex_color(brand.colors.accent)
    bar.line.fill.background()

    # Title (large, centered)
    title_text = definition.title or "Thank You"
    title_box = slide.shapes.add_textbox(
        Inches(center.left), Inches(center.top + 0.3),
        Inches(center.width), Inches(1.2),
    )
    tf = title_box.text_frame
    tf.word_wrap = True
    p = tf.paragraphs[0]
    p.text = title_text
    p.alignment = PP_ALIGN.CENTER
    run = p.runs[0]
    run.font.name = brand.fonts.heading_font
    run.font.size = Pt(40)
    run.font.bold = True
    run.font.color.rgb = parse_hex_color(brand.colors.primary)

    # Subtitle
    if definition.subtitle:
        sub_box = slide.shapes.add_textbox(
            Inches(center.left + 1.0), Inches(center.top + 1.5),
            Inches(center.width - 2.0), Inches(0.6),
        )
        tf = sub_box.text_frame
        tf.word_wrap = True
        p = tf.paragraphs[0]
        p.text = definition.subtitle
        p.alignment = PP_ALIGN.CENTER
        _apply_line_spacing(p, brand)
        run = p.runs[0]
        run.font.name = brand.fonts.body_font
        run.font.size = Pt(18)
        run.font.color.rgb = parse_hex_color(brand.colors.text_secondary)

    # CTA button
    if definition.cta_text:
        btn_w = 4.0
        btn_h = 0.6
        btn = slide.shapes.add_shape(
            MSO_SHAPE.ROUNDED_RECTANGLE,
            Inches(center.left + (center.width - btn_w) / 2),
            Inches(center.top + 2.3),
            Inches(btn_w), Inches(btn_h),
        )
        btn.fill.solid()
        btn.fill.fore_color.rgb = parse_hex_color(brand.colors.accent)
        btn.line.fill.background()

        tf = btn.text_frame
        tf.vertical_anchor = MSO_ANCHOR.MIDDLE
        p = tf.paragraphs[0]
        p.text = definition.cta_text
        p.alignment = PP_ALIGN.CENTER
        run = p.runs[0]
        run.font.name = brand.fonts.heading_font
        run.font.size = Pt(18)
        run.font.bold = True
        run.font.color.rgb = RGBColor(0xFF, 0xFF, 0xFF)

    # Contact info row
    if definition.contact_info:
        contact_text = "  |  ".join(definition.contact_info)
        contact_box = slide.shapes.add_textbox(
            Inches(center.left), Inches(center.top + 3.3),
            Inches(center.width), Inches(0.4),
        )
        tf = contact_box.text_frame
        tf.word_wrap = True
        p = tf.paragraphs[0]
        p.text = contact_text
        p.alignment = PP_ALIGN.CENTER
        run = p.runs[0]
        run.font.name = brand.fonts.body_font
        run.font.size = Pt(13)
        run.font.color.rgb = parse_hex_color(brand.colors.text_secondary)


def render_swot_slide(
    slide: Slide,
    definition: SlideDefinition,
    brand: BrandConfig,
    slide_width: float,
) -> None:
    """Render a SWOT analysis 2x2 quadrant matrix."""
    _add_slide_title(slide, definition.title, brand, slide_width)

    swot = definition.swot
    if not swot:
        return

    grid = _make_grid(brand, slide_width)
    quadrants = grid.grid(2, 2, h_gap=0.3, v_gap=0.3)

    quadrant_data = [
        ("Strengths", swot.strengths, brand.colors.success),
        ("Weaknesses", swot.weaknesses, brand.colors.danger),
        ("Opportunities", swot.opportunities, brand.colors.accent),
        ("Threats", swot.threats, brand.colors.secondary),
    ]

    for idx, (label, items, color) in enumerate(quadrant_data):
        row, col = divmod(idx, 2)
        cell = quadrants[row][col]

        # Header bar
        header_h = 0.45
        header = slide.shapes.add_shape(
            MSO_SHAPE.RECTANGLE,
            Inches(cell.left), Inches(cell.top),
            Inches(cell.width), Inches(header_h),
        )
        header.fill.solid()
        header.fill.fore_color.rgb = parse_hex_color(color)
        header.line.fill.background()

        tf = header.text_frame
        tf.vertical_anchor = MSO_ANCHOR.MIDDLE
        p = tf.paragraphs[0]
        p.text = label
        p.alignment = PP_ALIGN.CENTER
        run = p.runs[0]
        run.font.name = brand.fonts.heading_font
        run.font.size = Pt(14)
        run.font.bold = True
        run.font.color.rgb = RGBColor(0xFF, 0xFF, 0xFF)

        # Body area with bullet items
        if items:
            body_box = slide.shapes.add_textbox(
                Inches(cell.left), Inches(cell.top + header_h + 0.1),
                Inches(cell.width), Inches(cell.height - header_h - 0.1),
            )
            tf = body_box.text_frame
            tf.word_wrap = True
            _apply_text_frame_padding(tf, brand)
            for i, item in enumerate(items[:6]):
                p = tf.paragraphs[0] if i == 0 else tf.add_paragraph()
                p.text = item
                p.space_after = Pt(4)
                _apply_line_spacing(p, brand)
                _set_bullet(p)
                run = p.runs[0]
                run.font.name = brand.fonts.body_font
                run.font.size = Pt(12)
                run.font.color.rgb = parse_hex_color(brand.colors.text_primary)


def render_funnel_slide(
    slide: Slide,
    definition: SlideDefinition,
    brand: BrandConfig,
    slide_width: float,
) -> None:
    """Render a vertical funnel with 3-6 stages of decreasing width."""
    _add_slide_title(slide, definition.title, brand, slide_width)

    stages = definition.funnel_stages or []
    if not stages:
        return

    n = min(len(stages), 6)
    sp = brand.spacing
    content_w = slide_width - sp.margin_left - sp.margin_right
    funnel_area_top = sp.content_top + 0.2
    funnel_area_h = 5.5 - sp.content_top
    stage_h = (funnel_area_h - 0.1 * (n - 1)) / n
    max_width = content_w * 0.65
    min_width = content_w * 0.25

    for i, stage in enumerate(stages[:n]):
        # Width decreases linearly from widest to narrowest
        ratio = 1.0 - (i / max(n - 1, 1))
        w = min_width + (max_width - min_width) * ratio
        left = sp.margin_left + (content_w * 0.65 - w) / 2  # centered in left portion
        top = funnel_area_top + i * (stage_h + 0.1)

        # Gradient from primary to accent across stages
        t = i / max(n - 1, 1)
        primary_rgb = parse_hex_color(brand.colors.primary)
        accent_rgb = parse_hex_color(brand.colors.accent)
        r = int(primary_rgb[0] + (accent_rgb[0] - primary_rgb[0]) * t)
        g = int(primary_rgb[1] + (accent_rgb[1] - primary_rgb[1]) * t)
        b = int(primary_rgb[2] + (accent_rgb[2] - primary_rgb[2]) * t)
        fill_color = RGBColor(r, g, b)

        # Stage rectangle
        rect = slide.shapes.add_shape(
            MSO_SHAPE.ROUNDED_RECTANGLE,
            Inches(left), Inches(top),
            Inches(w), Inches(stage_h),
        )
        rect.fill.solid()
        rect.fill.fore_color.rgb = fill_color
        rect.line.fill.background()

        tf = rect.text_frame
        tf.vertical_anchor = MSO_ANCHOR.MIDDLE
        p = tf.paragraphs[0]
        p.text = stage.label
        p.alignment = PP_ALIGN.CENTER
        run = p.runs[0]
        run.font.name = brand.fonts.heading_font
        run.font.size = Pt(14)
        run.font.bold = True
        run.font.color.rgb = RGBColor(0xFF, 0xFF, 0xFF)

        # Value/description to the right
        desc_left = sp.margin_left + content_w * 0.65 + 0.4
        desc_w = content_w * 0.35 - 0.4
        if stage.value or stage.description:
            desc_box = slide.shapes.add_textbox(
                Inches(desc_left), Inches(top),
                Inches(desc_w), Inches(stage_h),
            )
            tf = desc_box.text_frame
            tf.word_wrap = True
            tf.vertical_anchor = MSO_ANCHOR.MIDDLE
            p = tf.paragraphs[0]
            if stage.value:
                p.text = stage.value
                run = p.runs[0]
                run.font.name = brand.fonts.heading_font
                run.font.size = Pt(20)
                run.font.bold = True
                run.font.color.rgb = parse_hex_color(brand.colors.text_primary)
            if stage.description:
                p2 = tf.add_paragraph() if stage.value else tf.paragraphs[0]
                p2.text = stage.description
                _apply_line_spacing(p2, brand)
                run2 = p2.runs[0]
                run2.font.name = brand.fonts.body_font
                run2.font.size = Pt(12)
                run2.font.color.rgb = parse_hex_color(brand.colors.text_secondary)


def render_agenda_slide(
    slide: Slide,
    definition: SlideDefinition,
    brand: BrandConfig,
    slide_width: float,
) -> None:
    """Render a visual agenda with numbered circles and status indicators."""
    _add_slide_title(slide, definition.title, brand, slide_width)

    items = definition.agenda_items or []
    if not items:
        return

    n = min(len(items), 10)
    sp = brand.spacing
    active = definition.active_agenda_item if definition.active_agenda_item is not None else -1
    content_w = slide_width - sp.margin_left - sp.margin_right

    item_top = sp.content_top + 0.2
    item_h = min(0.7, (6.5 - item_top) / n)
    dot_r = 0.2
    dot_x = sp.margin_left + 0.5

    for i, item in enumerate(items[:n]):
        y = item_top + i * (item_h + 0.12)

        # Determine color based on status
        if item.status == "completed" or i < active:
            dot_fill = brand.colors.primary
        elif item.status == "active" or i == active:
            dot_fill = brand.colors.accent
        else:
            dot_fill = "#C0C0C0"

        # Numbered circle
        dot = slide.shapes.add_shape(
            MSO_SHAPE.OVAL,
            Inches(dot_x - dot_r), Inches(y),
            Inches(dot_r * 2), Inches(dot_r * 2),
        )
        dot.fill.solid()
        dot.fill.fore_color.rgb = parse_hex_color(dot_fill)
        dot.line.fill.background()

        tf = dot.text_frame
        tf.vertical_anchor = MSO_ANCHOR.MIDDLE
        p = tf.paragraphs[0]
        p.text = str(i + 1)
        p.alignment = PP_ALIGN.CENTER
        run = p.runs[0]
        run.font.name = brand.fonts.heading_font
        run.font.size = Pt(12)
        run.font.bold = True
        run.font.color.rgb = RGBColor(0xFF, 0xFF, 0xFF)

        # Title
        title_left = dot_x + dot_r + 0.35
        title_w = content_w - (title_left - sp.margin_left)
        title_box = slide.shapes.add_textbox(
            Inches(title_left), Inches(y - 0.02),
            Inches(title_w), Inches(0.35),
        )
        tf = title_box.text_frame
        tf.word_wrap = True
        p = tf.paragraphs[0]
        p.text = item.title
        p.alignment = PP_ALIGN.LEFT
        run = p.runs[0]
        run.font.name = brand.fonts.heading_font
        run.font.size = Pt(16)
        run.font.bold = True
        is_active = item.status == "active" or i == active
        run.font.color.rgb = parse_hex_color(
            brand.colors.accent if is_active else brand.colors.text_primary
        )

        # Description
        if item.description:
            desc_box = slide.shapes.add_textbox(
                Inches(title_left), Inches(y + 0.32),
                Inches(title_w), Inches(0.3),
            )
            tf = desc_box.text_frame
            tf.word_wrap = True
            p = tf.paragraphs[0]
            p.text = item.description
            p.alignment = PP_ALIGN.LEFT
            _apply_line_spacing(p, brand)
            run = p.runs[0]
            run.font.name = brand.fonts.body_font
            run.font.size = Pt(12)
            run.font.color.rgb = parse_hex_color(brand.colors.text_secondary)
