#!/usr/bin/env python3
"""
HTML Generator for Capcat - Static Site Generation
Creates self-contained HTML files from markdown content with embedded CSS and JavaScript.
Follows minimalist design principles with dark/light theme support using SVG icon toggle.
"""

import re
from pathlib import Path
from string import Template
from typing import Any, Dict, List

import yaml
import markdown


from capcat.core.config import get_config, find_project_root, NoProjectError
from capcat.core.logging_config import get_logger
from capcat.core.template_renderer import TemplateRenderer
from capcat.core.design_system_compiler import DesignSystemCompiler
from capcat.core.utils import truncate_title_intelligently
from capcat.core.storage_manager import find_article_md, find_comments_md


class ArticleHTMLGenerator:
    """
    Static HTML generator for Capcat archives.
    Creates self-contained HTML files with embedded CSS and JavaScript.
    """

    def __init__(self):
        """Initialize the HTML generator with config, markdown, and template dependencies."""
        self.logger = get_logger(__name__)
        self.config = get_config()
        self.markdown_processor = self._setup_markdown_processor()
        self.template_renderer = TemplateRenderer()
        # Resolve user themes dir once; pass to DesignSystemCompiler if present
        self._user_themes_dir = self._resolve_user_themes_dir()
        user_ds = self._user_themes_dir / "design-system.css" if self._user_themes_dir else None
        self.design_system_compiler = (
            DesignSystemCompiler(themes_dir=self._user_themes_dir)
            if user_ds and user_ds.exists()
            else DesignSystemCompiler()
        )

    def _resolve_user_themes_dir(self) -> "Path | None":
        """Return Config/themes/ path if it exists in the project root, else None."""
        try:
            project_root = find_project_root()
            user_themes = project_root / "Config" / "themes"
            return user_themes if user_themes.is_dir() else None
        except NoProjectError:
            return None

    def _resolve_base_css_path(self, app_dir: "Path") -> "Path":
        """Return the base.css path, preferring Config/themes/ over the package copy."""
        user_base = self._user_themes_dir / "base.css" if self._user_themes_dir else None
        return (
            user_base if user_base and user_base.exists()
            else app_dir / "themes" / "base.css"
        )

    def _setup_markdown_processor(self):
        """Configure markdown processor with syntax highlighting and extensions."""
        return markdown.Markdown(
            extensions=[
                "codehilite",
                "toc",
                "tables",
                "fenced_code",
                "attr_list",
            ],
            extension_configs={
                "codehilite": {
                    "css_class": "highlight",
                    "use_pygments": True,
                    "noclasses": False,
                },
                "toc": {
                    "permalink": True,
                    "permalink_title": "Link to this section",
                },
            },
        )

    def _get_embedded_css(self, app_dir: Path) -> str:
        """
        Read and embed CSS files into a single <style> tag with compiled design system.

        Args:
            app_dir: Path to Application directory

        Returns:
            HTML style tag with embedded CSS content including compiled design system
        """
        try:
            # Read base.css (includes all styles and syntax highlighting)
            base_css_path = self._resolve_base_css_path(app_dir)
            base_css_content = ""
            if base_css_path.exists():
                with open(base_css_path, "r", encoding="utf-8") as f:
                    base_css_content = f.read()

            # Compile CSS: replace all var() references with hardcoded values
            # Color variables preserved for theme switching
            combined_css = self.design_system_compiler.replace_css_variables(base_css_content)

            self.logger.debug("Successfully embedded CSS with compiled design system")
            return f"<style>\n{combined_css}\n</style>"

        except Exception as e:
            self.logger.error(f"Error reading embedded CSS: {e}")
            return "<!-- Error loading embedded styles -->"

    def _get_embedded_assets_for_template(self, app_dir: Path) -> dict:
        """
        Read and embed CSS and JavaScript assets for template rendering with design system.

        Args:
            app_dir: Path to Application directory

        Returns:
            Dictionary containing embedded_styles and embedded_script
        """
        try:
            # Read base.css (includes all styles and syntax highlighting)
            base_css_path = self._resolve_base_css_path(app_dir)
            base_css_content = ""
            if base_css_path.exists():
                with open(base_css_path, "r", encoding="utf-8") as f:
                    base_css_content = f.read()

            # Compile CSS: replace all var() references with hardcoded values
            # Color variables preserved for theme switching
            combined_css = self.design_system_compiler.replace_css_variables(base_css_content)

            # Read JavaScript
            js_path = app_dir / "themes" / "js" / "capcat.js"
            js_content = ""
            if js_path.exists():
                with open(js_path, "r", encoding="utf-8") as f:
                    js_content = f.read()

            # Add design tokens to JavaScript for runtime access
            design_tokens = self.design_system_compiler.get_design_tokens_for_js()
            design_tokens_js = f"window.designTokens = {design_tokens};"

            # Combine JavaScript content
            combined_js = design_tokens_js + "\n\n" + js_content

            # Return embedded assets wrapped in appropriate HTML tags
            return {
                "embedded_styles": f"<style>\n{combined_css}\n</style>",
                "embedded_script": f"<script>\n{combined_js}\n</script>",
            }

        except Exception as e:
            self.logger.error(f"Error reading embedded assets: {e}")
            return {
                "embedded_styles": "<!-- Error loading embedded styles -->",
                "embedded_script": "<!-- Error loading embedded script -->",
            }

    def _get_base_template(
        self, html_output_path: str = None, html_subfolder: bool = False
    ) -> Template:
        """
        Get the base HTML template with embedded CSS and JavaScript.
        Returns a Template object that uses $variable syntax to avoid brace conflicts.

        Args:
            html_output_path: Path where the HTML file will be saved
            html_subfolder: True if HTML is in html/ subfolder
        """
        # Get absolute path to Application directory (where themes/ and Webfonts/ are located)
        app_dir = Path(__file__).parent.parent.absolute()

        # Read and embed CSS instead of using external links
        embedded_styles = self._get_embedded_css(app_dir)

        # Build template using Template class to avoid brace conflicts
        template_string = f"""<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>$title</title>
    {embedded_styles}
    <!-- Inline Theme Initialization - Prevents Flash of Wrong Theme -->
    <script>
      (function () {{
        var hashMatch = window.location.hash.match(/theme=(light|dark)/);
        var hashTheme = hashMatch ? hashMatch[1] : null;
        var savedTheme = localStorage.getItem("capcat-theme");
        var theme = hashTheme || savedTheme ||
          (window.matchMedia && window.matchMedia("(prefers-color-scheme: light)").matches ? "light" : "dark");
        document.documentElement.setAttribute("data-theme", theme);
        window.capcat_initial_theme = theme;
        window.addEventListener("DOMContentLoaded", function () {{ injectThemeIntoLinks(theme); }});
        function injectThemeIntoLinks(t) {{
          document.querySelectorAll("a[href]").forEach(function (link) {{
            var href = link.getAttribute("href");
            if (!href || /^(https?:|\/\/|mailto:|javascript:|tel:)/.test(href)) return;
            if (href.indexOf("#theme=") !== -1) href = href.replace(/(#|&)theme=(light|dark)/, "$$1theme=" + t);
            else if (href.indexOf("#") !== -1) href = href + "&theme=" + t;
            else href = href + "#theme=" + t;
            link.setAttribute("href", href);
          }});
        }}
        window.injectThemeIntoLinks = injectThemeIntoLinks;
      }})();
    </script>
</head>
<body>
    <div class="page-layout">
        <div class="header-section">
            <button class="theme-toggle" onclick="toggleTheme()" title="Switch between light and dark themes">
            <svg width="auto" height="100%" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
                <g transform="matrix(1.11813,0,0,1.11813,-1.41761,-1.41761)">
                    <path d="M12,16C14.209,16 16,14.209 16,12C16,9.791 14.209,8 12,8L12,16Z" style="fill-rule:nonzero;"/>
                </g>
                <g transform="matrix(1.11813,0,0,1.11813,-1.41761,-1.41761)">
                    <path d="M12,2C17.523,2 22,6.477 22,12C22,17.523 17.523,22 12,22C6.477,22 2,17.523 2,12C2,6.477 6.477,2 12,2ZM11.953,8C11.616,7.647 11.173,7.365 10.665,7.277C10.556,7.257 10.443,7.247 10.328,7.247C8.951,7.247 8.142,8.474 7.827,9.843C7.724,10.29 7.674,10.752 7.674,11.191C7.674,12.058 7.899,12.918 8.318,13.667C8.41,13.831 8.511,13.99 8.622,14.142C9.331,15.259 10.579,16 12,16L12,20C16.418,20 20,16.418 20,12C20,7.582 16.418,4 12,4L12,8C11.984,8 11.969,8 11.953,8Z"/>
                </g>
                <g transform="matrix(1.08138,0,0,1.08138,-0.976516,-0.976516)">
                    <circle cx="12" cy="12" r="10.34" style="fill:rgb(234,94,52);"/>
                </g>
            </svg>
            </button>

            <div class="header">
                <div class="progress"></div>
                <div class="container">
                    <nav class="breadcrumb">$breadcrumb</nav>
                    <h1>$page_title</h1>
                </div>
            </div>
        </div>
        
        <div class="main-layout">
            <div class="content-area">
                $top_navigation
                <main class="content">
                    $content
                </main>
                $bottom_navigation
            </div>
        </div>

        <footer class="footer">
            <div class="footer-content">
                <div class="footer-logo">
                    <svg class="capcat-logo" width="100%" height="100%" viewBox="0 0 120 27" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
                        <g transform="matrix(1,0,0,1,-15056.2,-6254.9)">
                                <g transform="matrix(0.0719766,0,0,0.0690735,13931.8,4306.55)">
                                    <g transform="matrix(1.12301,0,0,1.17021,10164.7,26457.1)">
                                        <path d="M5657.17,1659.5L5657.17,1620.07C5657.17,1614.05 5656.53,1609.85 5655.48,1606.94C5658.58,1606.55 5660.71,1605 5665.3,1600.83C5670.57,1596.04 5677.38,1590.61 5684.9,1585.19C5688.47,1582.62 5691.52,1580.43 5694.16,1578.52C5699.61,1584.19 5702.74,1591.31 5704.51,1596.99C5703.07,1598.66 5701.61,1600.42 5699.84,1602.6C5697.6,1605.34 5695.63,1607.72 5695.46,1607.88C5694.7,1608.62 5681.93,1625.76 5674.01,1636.67C5667.41,1645.78 5661.9,1653.27 5657.17,1659.5Z" style="fill:rgb(234,94,52);"/>
                                    </g>
                                    <g transform="matrix(1.12301,0,0,1.17021,10164.7,26457.1)">
                                        <path d="M5657.17,1772.77L5657.17,1719.4C5658.99,1717.05 5660.83,1714.44 5661.87,1712.69L5664.62,1708.07L5670.02,1708.25C5675.14,1708.42 5675.59,1708.33 5678.82,1706.56C5689.19,1700.86 5697.33,1694.4 5707.53,1682.86C5708.48,1696.5 5711.63,1712.06 5721.4,1721.14C5719.07,1722.62 5716.51,1724.24 5713.66,1726.01C5680.92,1746.4 5667.24,1757.59 5657.17,1772.77Z" style="fill:rgb(234,94,52);"/>
                                    </g>
                                    <g transform="matrix(1.12301,0,0,1.17021,10164.7,26457.1)">
                                        <path d="M5829.67,1671.48C5821.72,1718.11 5788.62,1749.18 5744.39,1749.18C5742.93,1749.18 5741.48,1749.14 5740.03,1749.07C5742.94,1746.3 5746.26,1743.16 5750.1,1739.57C5779.7,1711.81 5787.12,1705.2 5799.83,1695.26C5809.84,1687.44 5813.56,1684.49 5815.9,1682.51C5822.51,1676.93 5826.76,1673.5 5829.67,1671.48Z" style="fill:rgb(234,94,52);"/>
                                    </g>
                                    <g transform="matrix(1.12301,0,0,1.17021,10164.7,26457.1)">
                                        <path d="M5706.95,1780.11C5709.18,1808.27 5730.12,1782.01 5730.12,1803.7C5730.12,1812.89 5712.73,1829.1 5689.13,1825.92C5681.05,1824.97 5674.98,1822.11 5670.43,1818.32C5672.61,1814.74 5674.9,1810.85 5676.23,1808.48C5680.9,1800.14 5683.71,1796.53 5693.2,1786.64C5702.53,1776.92 5702.38,1777.02 5705.18,1779.15C5705.78,1779.61 5706.36,1779.94 5706.95,1780.11Z" style="fill:rgb(234,94,52);"/>
                                    </g>
                                    <g transform="matrix(1.12301,0,0,1.17021,10164.7,26457.1)">
                                        <path d="M5783.58,1676.51C5784.33,1670.76 5784.66,1665.12 5784.66,1659.94C5784.66,1638.52 5779.89,1609.05 5758.32,1602.3C5758.97,1601.49 5759.68,1600.58 5760.44,1599.58C5765.82,1592.47 5772.7,1585.53 5786.81,1573.3C5802.54,1579.32 5814.35,1591.95 5821.87,1607.23C5811.28,1618.83 5806.87,1628.54 5799.7,1646.96C5795.51,1657.71 5790.08,1667.64 5783.58,1676.51Z" style="fill:rgb(234,94,52);"/>
                                    </g>
                                    <g transform="matrix(1.67716,0,0,1.74765,7163.9,25463.9)">
                                        <path d="M5388.66,1711.53C5401.96,1704.98 5410.16,1696.29 5415,1696.29C5417.72,1696.29 5420.84,1699.01 5420.84,1701.35C5420.84,1717.91 5386.76,1741.82 5349.49,1747.79C5361.44,1738.27 5376.28,1724.5 5388.66,1711.53Z" style="fill:rgb(234,94,52);"/>
                                    </g>
                                    <g transform="matrix(1.67716,0,0,1.74765,7163.9,25463.9)">
                                        <path d="M5277.1,1729.19C5272.07,1724.76 5267.65,1719.68 5263.88,1714.07L5264.06,1713.93C5265.67,1712.73 5268.23,1710.8 5269.77,1709.64C5273.88,1706.53 5277.15,1705.06 5280.04,1705.01L5282.4,1704.97L5286.39,1701.08C5291.61,1696 5296.16,1690.07 5299.93,1683.48C5303.09,1689.27 5307.01,1694.61 5311.61,1699.3C5317.19,1704.96 5323.77,1709.66 5331.26,1713.02C5329.86,1714.88 5328.58,1716.93 5327.63,1718.9C5324.84,1724.75 5324.95,1724.6 5323.45,1723.85C5320.81,1722.53 5318.58,1724.13 5308.53,1734.56C5305.58,1737.62 5302.81,1740.52 5300.2,1743.28C5291.55,1739.88 5283.79,1735.11 5277.1,1729.19Z" style="fill:rgb(234,94,52);"/>
                                    </g>
                                    <g transform="matrix(1.67716,0,0,1.74765,7163.9,25463.9)">
                                        <path d="M5355.24,1569.78C5371.57,1571.31 5385.94,1579.28 5395.44,1587.3C5394.11,1589.23 5393.21,1589.78 5390.21,1590.81C5389.29,1591.13 5387.57,1592.4 5386,1593.92L5383.36,1596.5L5380.81,1596.59C5374.36,1596.83 5371.53,1598.49 5360.59,1609.16C5356.16,1599.45 5346.52,1589.62 5334.4,1587.45C5339.8,1582.28 5346.8,1575.78 5347.71,1575.16C5349.38,1574.02 5350.63,1573.82 5352.36,1574.42C5353.89,1574.96 5354.58,1574.38 5354.6,1572.54C5354.6,1571.63 5354.84,1570.69 5355.24,1569.78ZM5292.72,1626.02C5291.21,1632.73 5290.47,1639.68 5290.47,1646.28C5290.47,1659.3 5293.77,1672.23 5299.93,1683.48C5296.16,1690.07 5291.61,1696 5286.39,1701.08L5282.4,1704.97L5280.04,1705.01C5277.15,1705.06 5273.88,1706.53 5269.77,1709.64C5268.23,1710.8 5265.67,1712.73 5264.06,1713.93L5263.88,1714.07C5258.32,1705.81 5254.2,1696.38 5251.71,1686.12C5253.55,1683.75 5255.52,1681.15 5257.64,1678.3C5265.62,1667.57 5265.92,1666.95 5268.02,1656.66C5270.01,1646.93 5270.67,1645.27 5272.4,1645.65C5274.06,1646.02 5275.66,1644.68 5279.29,1639.88C5282.31,1635.9 5286.03,1631.99 5292.72,1626.02Z" style="fill:rgb(255,115,73);"/>
                                    </g>
                                    <g transform="matrix(1.67716,0,0,1.74765,7163.9,25463.9)">
                                        <path d="M5299.08,1582.76C5314.45,1574.29 5331.99,1569.51 5349.67,1569.51C5351.55,1569.51 5353.4,1569.61 5355.24,1569.78C5354.84,1570.69 5354.6,1571.63 5354.6,1572.54C5354.58,1574.38 5353.89,1574.96 5352.36,1574.42C5350.63,1573.82 5349.38,1574.02 5347.71,1575.16C5346.8,1575.78 5339.8,1582.28 5334.4,1587.45C5332.79,1587.17 5331.14,1587.01 5329.45,1587.01C5309.22,1587.01 5297.34,1605.45 5292.72,1626.02C5286.03,1631.99 5282.31,1635.9 5279.29,1639.88C5275.66,1644.68 5274.06,1646.02 5272.4,1645.65C5270.67,1645.27 5270.01,1646.93 5268.02,1656.66C5265.92,1666.95 5265.62,1667.57 5257.64,1678.3C5255.52,1681.15 5253.55,1683.75 5251.71,1686.12C5249.89,1678.63 5248.95,1670.71 5248.95,1662.46C5248.95,1646.34 5253.5,1631.47 5261.27,1618.56C5270.26,1603.62 5283.56,1591.31 5299.08,1582.76Z" style="fill:rgb(234,94,52);"/>
                                    </g>
                                    <g transform="matrix(1.67716,0,0,1.74765,7163.9,25463.9)">
                                        <path d="M5118.15,1669.94C5117.94,1668.72 5117.83,1667.47 5117.83,1666.2C5117.84,1659.69 5120.7,1653.51 5125.67,1649.3C5133.4,1642.83 5137.39,1641.8 5142.56,1636.14C5142.91,1635.76 5143.25,1635.35 5143.57,1634.93C5146.9,1630.61 5148.54,1624.94 5149.77,1622.93C5152.06,1619.2 5155.05,1615.6 5159.77,1613.41C5159.47,1614.11 5159.14,1614.94 5158.74,1615.94C5153.28,1629.61 5148.76,1637.6 5145.09,1640.07C5140.76,1642.99 5134.41,1652.45 5133.2,1657.78C5132.52,1660.8 5132,1661.23 5130.49,1660.05L5129.45,1659.23L5127.83,1660.5C5126.01,1661.93 5122.38,1665.48 5118.15,1669.94Z" style="fill:rgb(234,94,52);"/>
                                    </g>
                                    <g transform="matrix(1.67716,0,0,1.74765,7163.9,25463.9)">
                                        <path d="M5152.2,1712.96C5151.3,1711.84 5150.5,1710.67 5149.77,1709.48C5148.54,1707.47 5146.9,1701.79 5143.57,1697.48C5143.25,1697.06 5142.91,1696.65 5142.56,1696.26C5141.4,1695 5140.31,1693.97 5139.22,1693.06C5147.9,1680.84 5151.4,1676.37 5155.72,1671.81C5164.87,1662.16 5166.61,1660.01 5169.96,1654.21C5171.15,1652.14 5173.19,1648.83 5174.48,1646.86C5177.06,1642.95 5179.47,1638.37 5180.73,1635.01C5181.65,1632.56 5182.17,1632.35 5183.14,1634.06L5183.82,1635.25L5185.44,1634.35C5186.33,1633.86 5188.24,1632.28 5189.68,1630.85C5193.44,1627.12 5194.15,1626.83 5198.59,1627.16C5198.94,1627.19 5199.26,1627.21 5199.56,1627.22C5199.75,1627.51 5199.94,1627.8 5200.12,1628.09C5201.2,1630.08 5202.19,1632.12 5203.11,1634.22C5204.8,1638.11 5206.28,1641.7 5207.45,1645.55C5203.59,1649.16 5201.1,1651.62 5198.13,1654.66C5192.44,1660.46 5190.74,1662.34 5186.94,1667C5185.44,1668.83 5184.12,1670.43 5184.01,1670.54C5183.5,1671.03 5174.95,1682.5 5169.65,1689.81C5162.01,1700.35 5156.56,1707.65 5152.2,1712.96Z" style="fill:rgb(234,94,52);"/>
                                    </g>
                                    <g transform="matrix(1.67716,0,0,1.74765,7163.9,25463.9)">
                                        <path d="M5139.71,1590.6C5141.52,1602.46 5134.59,1614.57 5129.79,1619.37C5126.48,1622.67 5122.44,1625.8 5117.79,1625.88C5114.25,1625.82 5111.07,1624 5108.29,1621.67C5109.91,1619.42 5111.62,1616.89 5112.53,1615.39C5119.6,1603.69 5130.48,1593.36 5138.05,1591.14C5138.63,1590.97 5139.19,1590.79 5139.71,1590.6Z" style="fill:rgb(234,94,52);"/>
                                    </g>
                                    <g transform="matrix(1.67716,0,0,1.74765,7163.9,25463.9)">
                                        <path d="M5095.8,1598.19C5095.08,1592.42 5095.94,1586.32 5099.72,1580.8C5103.12,1575.82 5109.7,1571.48 5117.28,1571.02C5110.11,1581.16 5103.52,1589.51 5095.8,1598.19Z" style="fill:rgb(234,94,52);"/>
                                    </g>
                                    <g transform="matrix(1.67716,0,0,1.74765,7163.9,25463.9)">
                                        <path d="M5047.99,1647.8C5047.29,1646.93 5046.64,1645.99 5046.04,1644.97C5042.98,1639.72 5041.87,1631.84 5044.78,1624.76L5044.77,1624.75L5044.81,1624.67L5044.84,1624.59L5044.85,1624.6L5044.93,1624.44L5044.99,1624.29L5044.99,1624.29L5045.03,1624.21L5045.07,1624.13L5045.07,1624.13C5048.71,1617.39 5055.51,1613.26 5061.51,1612.3C5068.07,1611.26 5073.93,1613.06 5078.82,1616.14C5075.13,1619.93 5072.03,1623.16 5071.29,1623.99C5070.33,1625.07 5068.19,1627.37 5066.54,1629.11C5059.44,1636.57 5058.33,1637.86 5057.68,1639.35C5057.31,1640.22 5056.96,1640.98 5056.91,1641.05L5056.91,1641.06L5056.9,1641.06C5056.83,1641.1 5056.44,1640.8 5056,1640.37C5054.35,1638.71 5052.8,1639.81 5050.19,1644.49C5049.8,1645.21 5049.01,1646.38 5047.99,1647.8Z" style="fill:rgb(234,94,52);"/>
                                    </g>
                                    <g transform="matrix(1.67716,0,0,1.74765,7163.9,25463.9)">
                                        <path d="M5095.41,1640.81C5095.7,1643.26 5095.53,1645.69 5094.52,1647.92C5093.84,1649.29 5092.9,1650.46 5091.78,1651.47C5092.25,1650.36 5092.52,1649.28 5092.78,1647.92C5093.17,1645.86 5094.01,1643.55 5095.41,1640.81Z" style="fill:rgb(234,94,52);"/>
                                    </g>
                                    <g transform="matrix(1.67716,0,0,1.74765,7163.9,25463.9)">
                                        <path d="M5126.78,1710.32C5127.84,1711.16 5128.85,1712.09 5129.79,1713.04C5133.91,1717.16 5139.6,1726.66 5139.99,1736.76C5139.24,1737.32 5138.58,1737.82 5138.02,1738.24C5135.86,1739.85 5131.1,1743.45 5127.43,1746.26C5123.76,1749.06 5118.29,1753.13 5115.27,1755.31C5113.33,1756.72 5111.38,1758.12 5109.48,1759.49C5105.32,1757.67 5101.88,1754.76 5099.72,1751.6C5098.16,1749.33 5097.1,1746.95 5096.44,1744.54C5097.22,1743.75 5098.03,1742.91 5098.88,1742.04C5112.05,1728.48 5118.5,1721.33 5126.78,1710.32Z" style="fill:rgb(234,94,52);"/>
                                    </g>
                                    <g transform="matrix(1.67716,0,0,1.74765,7163.9,25463.9)">
                                        <path d="M5073.8,1674.71C5077.68,1674.89 5081.11,1675.57 5083.5,1676.42C5087.9,1678 5092.45,1680.32 5094.52,1684.49C5095.3,1686.22 5095.58,1688.08 5095.53,1689.97C5092.68,1692.17 5089.75,1694.6 5087.64,1696.53C5086.77,1697.32 5084.29,1699.57 5082.13,1701.51C5079.97,1703.46 5078.11,1705.15 5078,1705.27C5075.05,1708.52 5066.43,1714.78 5058.69,1719.44C5056.3,1718.69 5053.88,1717.48 5051.66,1715.83C5051.87,1715.3 5052.06,1714.79 5052.25,1714.3C5054.89,1707.32 5056.47,1704.34 5058.32,1702.86C5059.33,1702.05 5060.12,1700.95 5060.54,1699.78C5061.93,1695.95 5063.09,1693.74 5065.47,1690.41C5066.84,1688.48 5068.91,1684.79 5070.15,1682.04C5071.56,1678.93 5072.61,1676.74 5073.8,1674.71Z" style="fill:rgb(234,94,52);"/>
                                    </g>
                                    <g transform="matrix(1.12418,0,0,1.17143,10841.4,26455)">
                                        <path d="M5298.79,1677.5C5294.36,1667.59 5292,1656.67 5292,1645.68C5292,1629.34 5295.94,1611.16 5305.06,1599.43C5304.03,1600.2 5302.98,1600.99 5302.04,1601.71C5275.22,1622.12 5260.47,1635.09 5251.93,1645.76C5251.34,1646.49 5250.77,1647.2 5250.21,1647.89C5249.38,1652.62 5248.95,1657.48 5248.95,1662.46C5248.95,1683.17 5254.92,1701.86 5265.61,1716.55C5265.72,1716.34 5265.84,1716.11 5265.97,1715.88C5269.6,1709.08 5278.4,1697.66 5284.58,1691.73C5291.28,1685.3 5295.51,1681.09 5298.79,1677.5Z" style="fill:rgb(234,94,52);"/>
                                    </g>
                                    <g transform="matrix(1.12418,0,0,1.17143,10841.4,26455)">
                                        <path d="M5385.5,1713.04C5378.5,1715.95 5370.12,1718.08 5360.05,1718.08C5355.36,1718.08 5350.87,1717.63 5346.59,1716.78L5345.82,1717.65C5337.14,1727.32 5327.02,1737.74 5316.59,1747.84C5321.7,1748.72 5327.03,1749.18 5332.56,1749.18C5339.73,1749.18 5346.97,1748.38 5354.04,1746.97C5362.94,1736.89 5373.12,1725.95 5385.5,1713.04Z" style="fill:rgb(234,94,52);"/>
                                    </g>
                                    <g transform="matrix(1.12418,0,0,1.17143,10841.4,26455)">
                                        <path d="M5319.69,1588.58C5322.67,1587.57 5325.92,1587.01 5329.45,1587.01C5342.44,1587.01 5353.22,1596.06 5358.9,1605.89C5364.94,1598.97 5375.81,1584.81 5378.09,1580.72C5378.85,1579.36 5379.55,1578.19 5380.2,1577.23C5371.34,1572.73 5360.87,1569.51 5349.67,1569.51C5346.94,1569.51 5344.21,1569.63 5341.49,1569.85L5334.4,1576.31L5334.21,1576.44L5329.74,1580.54C5327.35,1582.61 5323.8,1585.45 5319.69,1588.58Z" style="fill:rgb(234,94,52);"/>
                                    </g>
                                    <g transform="matrix(1.12301,0,0,1.17021,10170.5,26457.1)">
                                        <path d="M5516.65,1642.29C5514.66,1644.36 5512.55,1646.6 5510.39,1648.94C5503.69,1656.19 5494.64,1664.12 5484.8,1671.34C5479.47,1675.26 5473.17,1680.37 5469,1684.18C5467.7,1685.36 5464,1688.7 5460.77,1691.61C5457.55,1694.52 5454.78,1697.05 5454.61,1697.22C5452.43,1699.63 5448.16,1703.14 5443.01,1706.91C5443.57,1663.38 5479.19,1647.33 5516.65,1642.29Z" style="fill:rgb(255,115,73);"/>
                                    </g>
                                    <g transform="matrix(1.12301,0,0,1.17021,10170.3,26457)">
                                        <path d="M5566.33,1743.87C5557.35,1740.5 5548.91,1733.56 5546.45,1725.07C5531.41,1738.64 5509.46,1748.06 5488.75,1749.09C5501.21,1736.21 5509.49,1727.23 5517.39,1717.61C5536.97,1715 5547.72,1693.88 5548.62,1683.47C5548.87,1680.58 5549.2,1677.58 5549.11,1674.68C5559.88,1659.63 5564.76,1653.5 5570.68,1647.25C5584.34,1632.84 5586.94,1629.63 5591.94,1620.96C5592.68,1619.69 5593.63,1618.1 5594.62,1616.47C5595.7,1622.35 5596.22,1628.74 5596.22,1635.62L5596.22,1701.49C5596.22,1704.65 5596.52,1707.11 5597.05,1709.01C5596.43,1709.62 5595.84,1710.25 5595.25,1710.89C5593.52,1712.78 5591.78,1715.42 5591.22,1717.02C5590.68,1718.56 5589.82,1720.81 5589.31,1722.03C5588.33,1724.41 5582.19,1730.56 5573.66,1737.75C5571.57,1739.5 5569.05,1741.61 5566.33,1743.87Z" style="fill:rgb(255,115,73);"/>
                                    </g>
                                    <g transform="matrix(1.12301,0,0,1.17021,10170.5,26457.1)">
                                        <path d="M5498.93,1573.64C5511.36,1570.55 5523.64,1569.51 5531.17,1569.51C5546.79,1569.51 5559.39,1572.36 5569.19,1577.84C5563.66,1589.8 5558.88,1597.1 5554.81,1599.85C5552.55,1601.37 5549.93,1604.08 5547.39,1607.31C5542.51,1592.35 5527.92,1583.85 5512.22,1585.07C5503.67,1585.74 5489.44,1592.62 5500.17,1603.35C5510.89,1614.07 5489.44,1633.45 5467.26,1628.01C5474.37,1619.68 5475.63,1617.15 5476.68,1611.58C5478.2,1603.58 5484.26,1593.11 5497.67,1575.33C5498.07,1574.8 5498.49,1574.23 5498.93,1573.64Z" style="fill:rgb(255,115,73);"/>
                                    </g>
                                    <g transform="matrix(1.12301,0,0,1.17021,10847.9,26456.7)">
                                        <path d="M5600.25,1714.02C5606.9,1718.44 5619.46,1707.91 5619.46,1723.56C5619.46,1732.38 5600.66,1747.67 5578.27,1746.06C5578.04,1746.07 5577.8,1746.07 5577.56,1746.07C5577.08,1746.07 5576.6,1746.06 5576.11,1746.03C5577.67,1744.24 5579.32,1742.45 5581.18,1740.47C5585.46,1735.94 5588.77,1731.77 5590.65,1728.56C5593.73,1723.32 5596.61,1718.9 5600.25,1714.02Z" style="fill:rgb(234,94,52);"/>
                                    </g>
                                    <g transform="matrix(1.12301,0,0,1.17021,10847.9,26456.7)">
                                        <path d="M5494.33,1748.61C5491.2,1748.99 5488.08,1749.18 5485,1749.18C5468.98,1749.18 5455.44,1741.37 5448.34,1728.88L5450.59,1726.56C5466.68,1710.01 5476.05,1699.85 5485.7,1687.69C5485.28,1693.73 5486.46,1700.41 5490.2,1706.19L5490.19,1706.19L5490.26,1706.28L5490.32,1706.37L5490.33,1706.37C5496.32,1714.11 5505.74,1717.92 5513.5,1717.88C5535.54,1717.77 5547.66,1694.55 5548.62,1683.47C5549.14,1677.4 5550.04,1670.81 5546.53,1665.83L5546.51,1665.79L5546.43,1665.68L5546.4,1665.65C5542.87,1660.68 5536.68,1659.07 5530.58,1658.92C5522.35,1658.72 5512.63,1659.33 5504.32,1662.6C5505.29,1661.25 5506.28,1659.85 5507.3,1658.4C5512.04,1651.7 5515.85,1646.37 5519.06,1641.98C5529.21,1640.75 5539.45,1640.29 5549.11,1640.29L5549.11,1618.51C5549.11,1614.88 5548.62,1611.48 5547.72,1608.36C5551.7,1603.67 5553.81,1600.47 5556.74,1595.39C5558.53,1592.3 5561.57,1587.36 5563.5,1584.42C5565.04,1582.09 5566.53,1579.6 5567.9,1577.14C5587.3,1587.27 5596.22,1607.27 5596.22,1635.62L5596.22,1658.17C5592.29,1661.92 5588.71,1664.6 5584.7,1667.09C5569.93,1676.3 5564.85,1680.06 5560.05,1685.32C5558.32,1687.21 5556.59,1689.85 5556.02,1691.44C5555.48,1692.98 5554.62,1695.24 5554.11,1696.46C5553.13,1698.83 5546.99,1704.99 5538.46,1712.17C5530.17,1719.14 5514.85,1731.83 5508.43,1737.02C5505.54,1739.36 5499.51,1744.32 5494.33,1748.61Z" style="fill:rgb(234,94,52);"/>
                                    </g>
                                    <g transform="matrix(1.12301,0,0,1.17021,10847.9,26456.7)">
                                        <path d="M5470.09,1628.58C5469.14,1628.43 5468.18,1628.24 5467.22,1628C5461.15,1626.51 5456.8,1621.44 5456.8,1611.33C5456.8,1580.41 5500.61,1571.29 5524.11,1569.76C5522.52,1571.82 5521.02,1573.32 5519.61,1574.28C5516.52,1576.36 5512.75,1580.67 5509.45,1585.47C5501.01,1587.19 5490.54,1593.72 5500.17,1603.35C5500.4,1603.58 5500.62,1603.82 5500.82,1604.05C5500.12,1605.39 5499.28,1605.28 5497.81,1604.11L5496.25,1602.89L5493.83,1604.79C5490.52,1607.39 5483.23,1614.66 5475.19,1623.36C5473.61,1625.07 5471.91,1626.81 5470.09,1628.58Z" style="fill:rgb(234,94,52);"/>
                                    </g>
                                    <g transform="matrix(0.748168,-0.0796221,-0.0596073,-0.60817,13182.8,29726.4)">
                                        <path d="M5551.44,1345.14C5564.24,1327.5 5597.16,1362.04 5594.5,1328.68C5593.11,1311.19 5568.88,1280.7 5522.18,1286.81C5514.33,1287.84 5507.65,1289.74 5501.96,1292.3C5502.75,1293.26 5503.58,1294.28 5504.45,1295.37C5514.9,1308.48 5524.54,1318.74 5551.44,1345.14Z" style="fill:rgb(234,94,52);"/>
                                    </g>
                                    <g transform="matrix(0.748168,-0.0796221,-0.0596073,-0.60817,13182.8,29726.4)">
                                        <path d="M5560.84,1554.81L5553.68,1464.97L5553.63,1465.01L5549.66,1414.15L5543.23,1409.17C5535.59,1403.27 5524.33,1395.73 5505.12,1383.68C5491.92,1375.39 5480.66,1368.01 5470.92,1361.16C5471.02,1362.35 5471.11,1363.38 5471.18,1364.25C5471.31,1365.95 5474.1,1400.69 5477.38,1441.56C5493.95,1455.12 5510.57,1475.28 5537.05,1512.38C5552.58,1534.12 5556.61,1540.09 5560.84,1554.81Z" style="fill:rgb(234,94,52);"/>
                                    </g>
                                    <g transform="matrix(0.748168,-0.0796221,-0.0596073,-0.60817,13182.8,29726.4)">
                                        <path d="M5593.53,1614.99C5598.25,1615.36 5603.09,1615.46 5608.11,1614.65C5612.5,1613.94 5616.51,1608.33 5615.57,1602.27C5613.77,1590.57 5593.83,1570.23 5564.64,1569.54C5564.75,1569.98 5564.86,1570.44 5564.98,1570.9C5571.59,1598 5573.43,1602.53 5577.19,1600.94C5580.76,1599.43 5584.56,1602.68 5593.53,1614.99Z" style="fill:rgb(234,94,52);"/>
                                    </g>
                                    <g transform="matrix(0.748168,-0.0796221,-0.0596073,-0.60817,13182.8,29726.4)">
                                        <path d="M5473.98,1581.7C5464.44,1587.36 5458.05,1596.51 5458.05,1611.25C5458.05,1621.37 5465.91,1629.73 5471.98,1631.23C5479.17,1633 5486.47,1633.11 5492.62,1631.77L5493.15,1638.42L5493.11,1638.43L5495.57,1669.31C5495.57,1669.31 5495.62,1669.28 5495.63,1669.3C5497.86,1697.38 5527.07,1734.22 5558.76,1735.35C5562.91,1735.47 5566.9,1733.27 5569.69,1729.33C5572.48,1725.39 5573.81,1720.1 5573.32,1714.81C5572.85,1708.56 5572.3,1701.25 5571.73,1693.72C5550.31,1673.41 5542.02,1665.06 5530.92,1653.21C5516.99,1638.37 5512.78,1633.51 5503.3,1621.37C5499.56,1616.58 5496.27,1612.43 5495.99,1612.15C5495.14,1611.3 5484.76,1596.88 5473.98,1581.7Z" style="fill:rgb(234,94,52);"/>
                                    </g>
                                    <g transform="matrix(1.12301,0,0,1.17021,15317.1,28026)">
                                        <path d="M801.648,240.559L801.443,240.735C799.124,241.903 796.576,242.792 793.813,243.345C781.713,245.765 758.366,243.511 746.171,215.024C746.002,214.628 745.826,214.232 745.645,213.835C761.972,197.9 766.21,195.416 775.836,195.061L779.637,194.92L783.592,191.076C785.938,188.796 788.497,186.905 789.879,186.43C794.355,184.889 795.696,184.06 797.683,181.177C801.045,184.013 803.999,186.852 806.486,189.567L806.572,189.612C809.21,192.501 811.318,195.247 812.826,197.696C822.101,212.764 816.941,232.689 801.73,240.555L801.648,240.559Z" style="fill:rgb(234,94,52);"/>
                                    </g>
                                    <g transform="matrix(1.12301,0,0,1.17021,15317.1,28026)">
                                        <path d="M415.034,339.128C409.367,334.382 403.773,331.776 394.798,324.271C388.819,319.197 384.88,312.214 383.572,304.604C389.885,297.945 395.302,292.646 398.023,290.506L400.443,288.604L402.002,289.828C404.251,291.597 405.023,290.949 406.049,286.434C407.856,278.477 417.34,264.356 423.806,259.993C429.28,256.3 436.028,244.367 444.185,223.957C444.78,222.469 445.279,221.227 445.721,220.181C446.135,219.99 446.557,219.805 446.988,219.629C447.318,219.494 447.653,219.363 447.994,219.237C453.301,217.274 459.944,216.433 468.441,217.362C483.154,218.97 493.248,225.002 500.133,233.45C500.448,233.836 500.756,234.227 501.057,234.624C501.828,235.747 502.576,236.888 503.301,238.048C503.611,238.52 503.917,238.994 504.221,239.469C504.538,239.909 504.848,240.355 505.151,240.808C504.696,240.787 504.216,240.755 503.705,240.717C497.062,240.219 496.006,240.658 490.394,246.225C488.243,248.359 485.394,250.711 484.064,251.452L481.646,252.799L480.631,251.014C479.182,248.469 478.397,248.78 477.027,252.442C475.151,257.455 471.548,264.291 467.701,270.139C465.767,273.079 462.724,278.012 460.939,281.104C455.935,289.772 453.338,292.983 439.681,307.393C433.222,314.208 428.002,320.884 415.034,339.128Z" style="fill:rgb(255,115,73);"/>
                                    </g>
                                    <g transform="matrix(1.12301,0,0,1.17021,15317.1,28026)">
                                        <path d="M349.79,334.513C349.683,338.604 348.544,342.801 347.155,346.689C342.78,358.927 324.417,383.572 298.973,379.521C297.593,379.302 296.185,378.969 294.771,378.527C306.323,371.567 319.202,362.212 323.611,357.366C323.773,357.188 326.545,354.662 329.771,351.754C332.997,348.846 336.7,345.5 337.998,344.317C341.159,341.437 345.535,337.807 349.79,334.513Z" style="fill:rgb(255,115,73);"/>
                                    </g>
                                    <g transform="matrix(1.12301,0,0,1.17021,15317.1,28026)">
                                        <path d="M344.178,277.013C340.752,280.097 336.232,282.179 331.816,283.758C320.415,287.833 292.99,289.343 278.784,271.544C280.308,269.415 281.486,267.664 282.08,266.598C285.97,259.611 288.286,257.966 290.756,260.436C291.403,261.083 291.993,261.534 292.101,261.473L292.107,261.467L292.109,261.463C292.181,261.355 292.699,260.211 293.26,258.918C294.227,256.69 295.888,254.769 306.49,243.623C308.959,241.028 312.149,237.591 313.578,235.984C314.688,234.737 319.318,229.915 324.823,224.261C336.486,231.612 344.465,243.829 347.155,251.354C348.27,254.476 349.224,257.797 349.612,261.102C347.52,265.192 346.263,268.639 345.68,271.721C345.297,273.745 344.886,275.367 344.178,277.013Z" style="fill:rgb(255,115,73);"/>
                                    </g>
                                    <g transform="matrix(1.12301,0,0,1.17021,15317.1,28026)">
                                        <path d="M382.27,156.867C382.35,156.862 382.43,156.857 382.511,156.853L382.511,156.843L382.64,156.846L382.77,156.84L382.77,156.85L383.025,156.859L383.281,156.85L383.281,156.84L383.411,156.846L383.54,156.843L383.54,156.853C394.961,157.464 404.89,163.992 410.012,171.48C413.212,176.158 415.011,181.121 415.771,186.115C414.989,186.395 414.158,186.665 413.285,186.92C401.983,190.229 385.737,205.666 375.176,223.133C373.818,225.379 371.263,229.15 368.839,232.521C367.531,231.427 366.284,230.259 365.1,229.076C359.47,223.446 351.878,211.079 350.186,197.453C361.724,184.489 371.556,172.014 382.27,156.867Z" style="fill:rgb(255,115,73);"/>
                                    </g>
                                    <g transform="matrix(1.12301,0,0,1.17021,15317.1,28026)">
                                        <path d="M284.269,373.131C280.38,370.237 276.912,366.45 274.427,361.853L274.418,361.857L274.365,361.739L274.305,361.624L274.313,361.62L274.212,361.385L274.095,361.158L274.086,361.162L274.036,361.042L273.978,360.927L273.987,360.923C269.653,350.338 271.305,338.571 275.881,330.738C285.094,314.967 303.206,311.09 317.329,311.728C315.549,314.752 313.986,318.03 311.887,322.67C310.025,326.784 306.945,332.285 304.885,335.172C301.335,340.147 299.605,343.441 297.531,349.172C296.9,350.916 295.718,352.549 294.207,353.764C291.446,355.983 289.092,360.422 285.146,370.852C284.871,371.58 284.575,372.348 284.269,373.131Z" style="fill:rgb(255,115,73);"/>
                                    </g>
                                    <g transform="matrix(1.12301,0,0,1.17021,15317.1,28026)">
                                        <path d="M351.146,416.019C345.965,397.085 357.378,376.689 365.1,368.967C370.037,364.031 376.081,359.365 383.025,359.24C387.986,359.33 392.487,361.736 396.453,364.902C384.091,381.356 374.457,392.034 354.787,412.278C353.522,413.58 352.309,414.826 351.146,416.019Z" style="fill:rgb(255,115,73);"/>
                                    </g>
                                    <g transform="matrix(1.11931,0,0,1.07334,15319.3,28048)">
                                        <path d="M415.578,420.332C415.867,428.461 414.219,436.829 409.392,444.496C404.253,452.659 394.292,459.776 382.832,460.443L382.832,460.454L382.703,460.45L382.573,460.457L382.573,460.446L382.316,460.436L382.06,460.446L382.059,460.457L381.93,460.45L381.8,460.454L381.8,460.443C377.561,460.196 373.527,459.067 369.866,457.328C372.72,455.1 375.639,452.817 378.551,450.535C383.075,446.989 391.272,440.353 396.766,435.789C402.26,431.225 409.399,425.35 412.631,422.732C413.472,422.051 414.466,421.241 415.578,420.332Z" style="fill:rgb(255,115,73);"/>
                                    </g>
                                    <g transform="matrix(1.12301,0,0,1.17021,15317.1,28026)">
                                        <path d="M518.846,322.404C516.979,331.225 514.034,338.555 510.446,346.792C509.081,349.924 507.593,352.975 505.986,355.939C505.425,356.843 504.836,357.721 504.221,358.574C503.917,359.05 503.611,359.523 503.301,359.995C502.576,361.155 501.828,362.297 501.057,363.42C500.756,363.816 500.448,364.207 500.133,364.593C493.248,373.041 483.154,379.073 468.441,380.681C464.918,381.066 461.714,381.147 458.792,380.97C459.262,379.77 459.823,378.276 460.219,377.158C460.783,375.566 462.52,372.924 464.244,371.035C469.044,365.776 474.128,362.018 488.9,352.809C498.035,347.114 504.956,340.485 518.846,322.404Z" style="fill:rgb(234,94,52);"/>
                                    </g>
                                    <g transform="matrix(1.12301,0,0,1.17021,15317.1,28026)">
                                        <path d="M729.065,420.867C720.69,422.209 712.207,422.945 703.774,422.945C686.18,422.945 669.964,419.838 655.449,414.137C659.35,410.011 663.484,405.686 667.887,401.115C682.892,385.539 686.225,383.145 690.176,385.115C692.42,386.235 692.24,386.451 696.418,377.723C697.828,374.776 699.749,371.718 701.837,368.932C713.905,374.337 727.542,377.398 742.541,377.398C760.72,377.398 775.533,372.642 787.557,366.718C769.07,386.087 746.904,406.646 729.065,420.867Z" style="fill:rgb(255,115,73);"/>
                                    </g>
                                    <g transform="matrix(1.12301,0,0,1.17021,15932.5,28025)">
                                        <path d="M357.744,409.228C356.491,409.291 355.242,409.322 354,409.322C330.278,409.322 312,392.211 312,368.1C312,367.749 312.002,367.401 312.007,367.053C317.16,363.277 321.426,359.767 323.611,357.366C323.773,357.188 326.545,354.662 329.771,351.754C332.997,348.846 336.7,345.5 337.998,344.317C342.172,340.514 348.464,335.404 353.799,331.485C363.637,324.256 372.69,316.329 379.387,309.078C381.547,306.739 383.654,304.503 385.643,302.434C396.585,300.964 407.684,300.433 418.111,300.433L418.111,278.656C418.111,274.61 417.503,270.861 416.389,267.448C418.926,264.226 421.55,261.515 423.806,259.993C427.877,257.246 432.654,249.94 438.191,237.978C452.1,245.752 460.371,258.829 463.621,276.611C462.624,278.243 461.675,279.83 460.939,281.104C455.935,289.772 453.338,292.983 439.681,307.393C433.758,313.644 428.876,319.776 418.103,334.824C417.998,331.634 417.371,328.58 415.53,325.968L415.507,325.936L415.425,325.82L415.402,325.787C411.865,320.825 405.676,319.211 399.581,319.062C383.585,318.672 361.953,321.355 356.016,340.652C353.733,348.07 353.878,358.116 359.196,366.33L359.189,366.335L359.256,366.423L359.317,366.516L359.324,366.511C365.312,374.25 374.737,378.059 382.498,378.019C383.831,378.012 385.129,377.921 386.389,377.753C378.489,387.376 370.205,396.347 357.744,409.228Z" style="fill:rgb(234,94,52);"/>
                                    </g>
                                    <g transform="matrix(1.12301,0,0,1.17021,15932.5,28024.9)">
                                        <path d="M466.047,369.149C465.43,369.763 464.835,370.388 464.244,371.035C462.52,372.924 460.783,375.566 460.219,377.158C459.673,378.699 458.814,380.955 458.311,382.172C457.326,384.549 451.192,390.705 442.654,397.887C440.573,399.638 438.047,401.749 435.331,404.01C439.11,405.427 442.986,406.211 446.555,406.211C446.798,406.211 447.034,406.209 447.264,406.205C469.655,407.813 488.456,392.516 488.456,383.699C488.456,364.82 470.182,384.045 466.047,369.149Z" style="fill:rgb(234,94,52);"/>
                                    </g>
                                    <g transform="matrix(1.12301,0,0,1.17021,15932.7,28025)">
                                        <path d="M336.255,288.153C336.241,288.149 336.227,288.146 336.213,288.143C330.15,286.648 325.798,281.583 325.798,271.472C325.798,249.927 347.073,238.965 367.924,233.782C367.492,234.371 367.071,234.938 366.668,235.473C353.252,253.255 347.194,263.719 345.68,271.721C344.626,277.289 343.365,279.82 336.255,288.153Z" style="fill:rgb(234,94,52);"/>
                                    </g>
                                    <g transform="matrix(1.12301,0,0,1.17021,15932.7,28025)">
                                        <path d="M520.953,379.544L520.953,319.644C525.683,313.411 531.193,305.921 537.795,296.816C545.711,285.899 558.48,268.757 559.242,268.025C559.41,267.864 561.381,265.483 563.621,262.736C565.391,260.566 566.857,258.8 568.294,257.133C570.045,262.767 570.453,266.989 570.453,266.989L571.23,266.989C576.675,247.545 603.508,229.656 629.564,229.656C637.202,229.656 644.221,231.007 650.596,233.445C636.489,245.673 629.602,252.614 624.223,259.717C623.459,260.725 622.757,261.63 622.105,262.44C619.169,261.521 615.923,261.022 612.335,261.022C585.501,261.022 571.091,293.144 571.091,319.977C571.091,324.742 570.654,333.396 571.317,343.003C561.114,354.542 552.972,361.002 542.605,366.697C539.37,368.474 538.927,368.56 533.803,368.389L528.406,368.207L525.656,372.83C524.611,374.586 522.772,377.194 520.953,379.544Z" style="fill:rgb(255,115,73);"/>
                                    </g>
                                    <g transform="matrix(1.12301,0,0,1.17021,15230.3,28069.8)">
                                        <path d="M594.25,230.715C596.822,239.228 598.357,248.597 598.357,261.554C598.357,270.862 597.565,278.319 596.165,284.937C582.275,303.018 575.355,309.647 566.22,315.341C551.447,324.551 546.364,328.309 541.564,333.568C539.84,335.457 538.102,338.099 537.538,339.691C537.143,340.808 536.582,342.302 536.112,343.503C531.985,343.253 528.42,342.488 525.313,341.339C524.973,341.213 524.638,341.082 524.308,340.947C518.865,338.718 514.883,335.275 511.746,331.388C518.245,323.453 526.391,312.544 537.795,296.816C545.711,285.899 558.48,268.757 559.242,268.025C559.41,267.864 561.381,265.483 563.621,262.736C569.29,255.785 571.84,252.974 580.328,244.311C584.773,239.774 588.489,236.101 594.25,230.715Z" style="fill:rgb(255,115,73);"/>
                                    </g>
                                    <g transform="matrix(1.12301,0,0,1.17021,15945,28055.8)">
                                        <path d="M508.361,240.74C502.991,225.947 486.684,244.135 486.684,224.995C486.684,215.812 504.078,199.601 527.68,202.778C536.177,203.771 542.44,207.531 547.045,212.321C544.403,214.23 541.355,216.424 537.785,218.995C530.263,224.411 523.454,229.841 518.181,234.629C513.592,238.797 511.463,240.353 508.361,240.74Z" style="fill:rgb(255,115,73);"/>
                                    </g>
                                    <g transform="matrix(1.12301,0,0,1.17021,15932.7,28025)">
                                        <path d="M534.213,478.461C522.704,468.873 520.951,453.38 521.039,448.189C521.076,445.974 520.86,443.716 520.953,441.545L520.953,432.907C531.027,417.731 544.702,406.543 577.447,386.146C580.29,384.376 582.856,382.763 585.181,381.286C590.295,386.044 597.222,389.026 606.592,389.026C632.854,389.026 644.09,361.758 647.366,336.649C653.868,327.786 659.297,317.849 663.482,307.1C670.656,288.676 675.069,278.968 685.652,267.373C691.999,280.278 695.286,295.074 695.286,309.378C695.286,317.115 694.658,324.549 693.451,331.624C690.547,333.637 686.297,337.075 679.688,342.654C677.348,344.629 673.628,347.58 663.617,355.404C650.908,365.337 643.484,371.952 613.885,399.707C610.047,403.306 606.726,406.437 603.815,409.207C592.89,408.62 582.126,405.734 570.453,399.211L570.453,422.551L570.539,422.509L570.539,434.967C570.539,436.914 570.605,438.671 570.731,440.256C570.146,440.08 569.568,439.753 568.965,439.293C566.166,437.158 566.315,437.065 556.982,446.785C547.49,456.671 544.683,460.285 540.018,468.621C538.689,470.995 536.389,474.886 534.213,478.461Z" style="fill:rgb(255,115,73);"/>
                                    </g>
                                    <g transform="matrix(1.12301,0,0,1.17021,15932.7,28025)">
                                        <path d="M806.332,230.693C808.681,230.333 811.043,230.054 813.413,229.859L801.648,240.559C799.251,242.632 795.695,245.469 791.582,248.609C785.69,250.62 780.843,254.454 776.944,259.465C775.91,260.24 774.861,261.032 773.92,261.748C747.072,282.177 732.301,295.16 723.752,305.848C723.167,306.579 722.595,307.29 722.035,307.982C729.204,267.223 765.802,236.904 806.332,230.693Z" style="fill:rgb(255,115,73);"/>
                                    </g>
                                    <g transform="matrix(1.12301,0,0,1.17021,16150.2,27987.3)">
                                        <path d="M632.296,439.346C667.777,432.247 699.166,409.579 699.166,393.675C699.166,391.339 696.052,388.614 693.326,388.614C687.97,388.614 679.24,398.936 663.792,405.376C651.403,418.303 641.214,429.257 632.296,439.346Z" style="fill:rgb(255,115,73);"/>
                                    </g>
                                    <g transform="matrix(1.12301,0,0,1.17021,15954.4,28051.7)">
                                        <path d="M718.132,353.884C729.954,370.129 747.535,381.469 769.171,385.209C779.614,375.102 789.744,364.672 798.424,354.986L799.2,354.12C776.66,349.669 760.027,334.18 751.351,314.793C748.07,318.388 743.828,322.603 737.121,329.043C730.943,334.975 722.129,346.411 718.49,353.217C718.366,353.45 718.246,353.672 718.132,353.884Z" style="fill:rgb(255,115,73);"/>
                                    </g>
                                    <g transform="matrix(1.12301,0,0,1.17021,16570.5,27994.7)">
                                        <path d="M284.197,263.1C283.543,264.067 282.841,265.23 282.08,266.598C279.801,270.691 268.92,284.866 262.873,291.794C263.653,293.143 264.337,294.507 264.918,295.864C273.092,314.959 288.741,316.469 296.851,314.847C311.02,312.014 316.784,295.964 309.59,284.276C305.847,278.196 296.587,269.391 284.197,263.1Z" style="fill:rgb(255,115,73);"/>
                                    </g>
                                    <g transform="matrix(1.12301,0,0,1.17021,16570.5,27994.7)">
                                        <path d="M352.536,414.592C349.124,408.587 347.199,401.499 347.199,393.674C347.199,348.411 384.631,332.403 423.252,327.699C420.045,332.085 416.234,337.418 411.498,344.118C410.476,345.563 409.484,346.962 408.519,348.319C400.679,351.405 394.097,356.857 391.215,366.226C390.537,368.427 390.074,370.86 389.896,373.407C380.251,385.561 370.876,395.719 354.787,412.278C354.017,413.07 353.268,413.841 352.536,414.592Z" style="fill:rgb(255,115,73);"/>
                                    </g>
                                    <g transform="matrix(1.12301,0,0,1.17021,16570.5,27994.7)">
                                        <path d="M405.015,289.769C413.757,300.133 394.93,317.451 374.29,314.29C376.106,312.527 377.811,310.784 379.387,309.078C387.425,300.374 394.716,293.107 398.023,290.506L400.443,288.604L402.002,289.828C403.481,290.992 404.321,291.109 405.015,289.769ZM451.912,294.072C447.384,278.464 432.478,269.532 416.421,270.785C415.544,270.853 414.607,270.987 413.644,271.183C416.943,266.384 420.719,262.076 423.806,259.993C425.22,259.038 426.72,257.535 428.306,255.476C430.946,255.305 433.329,255.229 435.366,255.229C450.305,255.229 462.484,257.831 472.093,262.85C470.73,265.318 469.235,267.807 467.701,270.139C465.767,273.079 462.724,278.012 460.939,281.104C458.007,286.183 455.901,289.389 451.912,294.072Z" style="fill:rgb(255,115,73);"/>
                                    </g>
                                    <g transform="matrix(1.12301,0,0,1.17021,16570.5,27994.7)">
                                        <path d="M480.307,431.743C468.486,431.071 454.005,422.363 450.643,410.785C436.954,423.139 417.531,432.053 398.528,434.327C403.703,430.038 409.735,425.078 412.631,422.732C419.042,417.539 434.365,404.859 442.654,397.887C451.192,390.705 457.326,384.549 458.311,382.172C458.814,380.955 459.673,378.699 460.219,377.158C460.783,375.566 462.52,372.924 464.244,371.035C469.044,365.776 474.128,362.018 488.9,352.809C492.908,350.31 496.49,347.632 500.421,343.885L500.421,387.206C500.421,394.606 502.051,398.141 504.448,399.734C500.808,404.614 497.927,409.035 494.852,414.275C492.967,417.487 489.656,421.65 485.377,426.188C483.514,428.163 481.871,429.956 480.307,431.743Z" style="fill:rgb(255,115,73);"/>
                                    </g>
                                    <g transform="matrix(1.12301,0,0,1.17021,16570.5,27994.7)">
                                        <path d="M605.487,293.414C606.05,291.221 606.589,288.679 607.239,285.503C606.661,285.573 606.077,285.634 605.487,285.687L605.487,293.414Z" style="fill:rgb(255,122,0);fill-opacity:0.61;"/>
                                    </g>
                                    <g transform="matrix(1.12301,0,0,1.17021,16570.5,27994.7)">
                                        <path d="M555.895,357.949C555.92,326.16 555.951,287.311 555.951,287.311C552.441,286.977 549.131,286.361 546.195,285.351C552.566,276.726 558.72,268.527 559.242,268.025C559.41,267.864 561.381,265.483 563.621,262.736C569.29,255.785 571.84,252.974 580.328,244.311C587.099,237.399 592.179,232.494 605.37,220.484C605.449,233.467 605.56,247.739 605.56,247.739C605.497,248.927 605.464,250.204 605.464,251.575L605.464,254.567L605.487,254.567L605.487,261.962L605.547,261.964C611.74,262.576 617.708,260.97 624.07,259.918C618.751,266.92 616.392,268.87 613.934,268.33C611.342,267.761 610.355,270.239 607.389,284.771C607.338,285.019 607.288,285.263 607.239,285.503C606.661,285.573 606.077,285.634 605.487,285.687L605.487,293.414C603.448,301.354 601.082,304.728 591.891,317.088C576.214,338.168 566.215,349.776 555.895,357.949Z" style="fill:rgb(255,115,73);"/>
                                    </g>
                                    <g transform="matrix(1.12301,0,0,1.17021,16570.5,27994.7)">
                                        <path d="M580.195,433.849C556.774,428.221 555.868,408.04 555.86,400.173C561.987,395.95 569.097,391.348 577.447,386.146C589.605,378.574 596.709,373.891 601.482,370.303C602.607,369.457 604.05,368.371 605.502,367.278L605.537,385.33C605.537,395.215 607.441,400.469 610.354,403.023C593.831,418.571 587.953,424.56 581.688,432.086C581.166,432.712 580.669,433.3 580.195,433.849Z" style="fill:rgb(255,115,73);"/>
                                    </g>
                                    <g transform="matrix(1.67716,0,0,1.74765,7163.9,25463.9)">
                                        <path d="M5401.33,1592.94L5401.33,1592.91C5401.35,1592.93 5401.37,1592.95 5401.38,1592.97L5401.33,1592.94Z" style="fill:rgb(255,122,0);"/>
                                    </g>
                                    <g transform="matrix(1.67716,0,0,1.74765,7163.9,25463.9)">
                                        <path d="M5398.14,1627.08C5398.1,1627.1 5398.06,1627.12 5398.01,1627.15L5398.09,1627.08L5398.14,1627.08Z" style="fill:rgb(255,122,0);"/>
                                    </g>
                                </g>
                            </g>
                        </svg>
                </div>
                <div class="footer-credit">
                    <a href="https://stayux.com" target="_blank" rel="noopener noreferrer" title="Product Designer with Holistic Production Expertise ∙ AI-Powered MVP Prototyping & Systems Thinking">Product by Stayux.com</a>
                </div>
            </div>
        </footer>
    </div>

    <button id="backToTopBtn" onclick="scrollToTop()" title="Back to top">
        <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
            <polyline points="18 15 12 9 6 15"></polyline>
        </svg>
    </button>

    <!-- External JavaScript - Permanent Application Assets -->
    <script src="{app_dir}/themes/js/capcat.js"></script>
</body>
</html>"""

        return Template(template_string)

    def generate_article_html(
        self,
        markdown_path: str,
        article_title: str,
        breadcrumb_path: List[str],
        html_subfolder: bool = False,
        html_output_path: str = None,
    ) -> str:
        """
        Generate HTML for an article from its markdown file.

        Args:
            markdown_path: Path to the markdown file
            article_title: Title of the article
            breadcrumb_path: List of breadcrumb elements
            html_subfolder: True if HTML is in html/ subfolder (affects relative paths)
            html_output_path: Path where the HTML file will be saved (for dynamic CSS path calculation)

        Returns:
            Generated HTML content
        """
        try:
            with open(markdown_path, "r", encoding="utf-8") as f:
                markdown_content = f.read()

            # Clean up problematic template tags in markdown content
            markdown_content = self._clean_markdown_content(markdown_content)

            # Convert markdown to HTML
            html_content = self.markdown_processor.convert(markdown_content)

            # Remove grey-placeholder images from HTML
            html_content = self._remove_grey_placeholder_images(html_content)

            # Remove anchor wrappers around downloaded images
            html_content = self._remove_image_anchor_wrappers(html_content)

            # Remove headerlink anchor tags
            html_content = self._remove_headerlink_anchors(html_content)

            # Remove duplicate H1 title if it matches the header title
            html_content = self._remove_duplicate_h1_title(
                html_content, article_title
            )

            # Wrap source URL in a div with CSS class
            html_content = self._wrap_source_url_in_div(html_content)

            # Adjust relative paths if HTML is in subfolder
            if html_subfolder:
                html_content = self._adjust_paths_for_subfolder(html_content)

            # Generate breadcrumb navigation
            current_file = (
                "article.html" if html_subfolder else str(markdown_path)
            )
            breadcrumb_html = self._generate_breadcrumb(
                breadcrumb_path,
                html_subfolder=html_subfolder,
                current_file_path=current_file,
            )

            # Fill template using Template.substitute with dynamic CSS paths
            template = self._get_base_template(
                html_output_path, html_subfolder
            )
            # Generate navigation for articles
            index_nav = self._generate_index_navigation(
                str(markdown_path), show=True, html_subfolder=html_subfolder
            )
            comments_nav = self._generate_comments_navigation(
                markdown_path, html_subfolder=html_subfolder
            )
            navigation_container = self._create_navigation_container(
                index_nav, comments_nav
            )
            top_navigation = navigation_container
            bottom_navigation = navigation_container

            return template.safe_substitute(
                title=f"Capcat - {article_title}",
                page_title=article_title,
                breadcrumb=breadcrumb_html,
                content=html_content,
                top_navigation=top_navigation,
                bottom_navigation=bottom_navigation,  # Show navigation at both top and bottom
            )

        except Exception as e:
            self.logger.error(
                f"Error generating HTML for {markdown_path}: {e}"
            )
            return self._generate_error_page(f"Error loading article: {e}")

    def generate_article_html_from_template(
        self,
        markdown_path: str,
        article_title: str,
        breadcrumb_path: List[str],
        source_config: Dict[str, Any],
        html_subfolder: bool = False,
        html_output_path: str = None,
        index_filename: str = "news.html",
        is_single_article: bool = False,
    ) -> str:
        """
        Generate HTML for an article using the template system.
        This is the new, reliable template-based approach.

        Args:
            markdown_path: Path to the markdown file
            article_title: Title of the article
            breadcrumb_path: List of breadcrumb elements
            source_config: Source configuration containing template metadata
            html_subfolder: True if HTML is in html/ subfolder

        Returns:
            Generated HTML content
        """
        try:
            # Read and process markdown content
            with open(markdown_path, "r", encoding="utf-8") as f:
                markdown_content = f.read()

            # Clean up problematic template tags in markdown content
            markdown_content = self._clean_markdown_content(markdown_content)

            # Convert markdown to HTML
            html_content = self.markdown_processor.convert(markdown_content)

            # Apply content transformations
            html_content = self._remove_grey_placeholder_images(html_content)
            html_content = self._remove_image_anchor_wrappers(html_content)
            html_content = self._remove_headerlink_anchors(html_content)
            html_content = self._remove_duplicate_h1_title(
                html_content, article_title
            )
            html_content = self._wrap_source_url_in_div(html_content)
            html_content = self._classify_svg_elements(html_content)   # ← new step

            # Adjust relative paths if HTML is in subfolder
            if html_subfolder:
                html_content = self._adjust_paths_for_subfolder(html_content)

            # Determine if this is a comments page (must precede breadcrumb call).
            # The third clause checks element equality — post_processor appends
            # the exact string "Comments" as breadcrumb_path[2] for comments pages.
            is_comments_page = (
                article_title.endswith(" - Comments")
                or "comments" in markdown_path.lower()
                or "Comments" in breadcrumb_path
            )

            # Generate breadcrumb navigation
            current_file = (
                "comments.html"
                if "comments" in markdown_path.lower()
                else "article.html"
            )
            if is_comments_page:
                base_article_title = re.sub(
                    r"\s*-\s*Comments$", "", article_title, flags=re.IGNORECASE
                ).strip()
                breadcrumb_for_render = list(breadcrumb_path[:2]) + [base_article_title]
            else:
                breadcrumb_for_render = breadcrumb_path
            breadcrumb_html = self._generate_breadcrumb(
                breadcrumb_for_render,
                html_subfolder=html_subfolder,
                current_file_path=current_file,
            )

            # Select appropriate template
            comments_button_html = ""
            if is_comments_page:
                template_variant = "comments-with-navigation"
            elif is_single_article:
                # Single articles (both Capcats and custom output) use standalone template
                # No navigation, no "Back to News" button - pure standalone article
                template_variant = "article-capcats"
            else:
                # Batch mode articles get navigation
                # Check if comments.md exists in the same directory as the article
                article_dir = Path(markdown_path).parent
                comments_file = find_comments_md(article_dir)
                has_comments = comments_file is not None

                if has_comments:
                    template_variant = "article-with-comments"
                    comment_count = self._count_comments(comments_file)
                    if comment_count > 0:
                        comments_button_html = """<a href="comments.html" class="comments-link" title="View all comments for this article">
                            <span>View Comments</span>
                        </a>"""
                else:
                    template_variant = "article-no-comments"

            # Debug logging
            self.logger.debug(f"Using template: {template_variant} for {article_title}")

            # Generate navigation HTML using design-system methods so CSS classes
            # (navigation-container, index-nav, index-link, comments-nav, comments-link)
            # are always correct and consistent with the established design.
            if is_single_article:
                navigation_html = ""
            elif is_comments_page:
                # Comments page: only "Back to Article" on the LEFT using orange
                # index-nav/index-link styling — no "Back to News" button.
                navigation_html = """<div class="navigation-container">
            <div class="index-nav">
                <a href="article.html" class="index-link" title="Back to the main article">
                    <span>Back to Article</span>
                </a>
            </div>
            <div class="comments-nav"></div>
        </div>"""
            else:
                index_nav = self._generate_index_navigation(
                    str(markdown_path), show=True, html_subfolder=html_subfolder
                )
                comments_nav = self._generate_comments_navigation(
                    str(markdown_path), html_subfolder=html_subfolder
                )
                navigation_html = self._create_navigation_container(
                    index_nav, comments_nav
                )

            # Build PDF link bar if local PDF files exist
            article_folder = Path(markdown_path).parent
            files_dir = article_folder / "files"
            pdf_link_html = ""
            if files_dir.is_dir():
                pdf_prefix = "../files/" if html_subfolder else "files/"
                pdfs = sorted(files_dir.glob("*.pdf"))
                if pdfs:
                    links = " ".join(
                        f'<a href="{pdf_prefix}{p.name}" target="_blank" '
                        f'rel="noopener">{p.stem}</a>'
                        for p in pdfs
                    )
                    pdf_link_html = (
                        f'<div class="pdf-download-bar">'
                        f'<span class="pdf-download-label">PDF</span> {links}'
                        f'</div>'
                    )

            # Prepare template context
            context = {
                "page_title": f"Capcat - {article_title}",
                "article_title": article_title,
                "breadcrumb": breadcrumb_html,
                "article_content": html_content,
                "comments_button": comments_button_html,
                "index_filename": index_filename,
                "top_navigation": navigation_html,
                "bottom_navigation": navigation_html,
                "pdf_link": pdf_link_html,
            }

            # Render template with dynamic CSS paths
            return self.template_renderer.render_template(
                template_variant, context, html_output_path, html_subfolder
            )

        except Exception as e:
            self.logger.error(
                f"Error generating template-based HTML for {markdown_path}: {e}"
            )
            return self._generate_error_page(f"Error loading article: {e}")

    def _get_display_name_without_date(self, name: str) -> str:
        """Removes a date pattern (DD-MM-YYYY) from the end of a string."""
        import re
        return re.sub(r"\s+\d{2}-\d{2}-\d{4}$", "", name).strip()

    def _get_article_title_from_markdown(self, article_dir: Path) -> str:
        """
        Extract article title from markdown file's H1 heading.
        Falls back to folder name if extraction fails.

        Args:
            article_dir: Path to article directory

        Returns:
            Article title string
        """
        article_md = find_article_md(article_dir)
        if article_md is None:
            return self._get_display_name_without_date(
                self._clean_title_for_display(article_dir.name)
            )

        try:
            with open(article_md, 'r', encoding='utf-8') as f:
                first_line = f.readline().strip()

            # Match H1 heading (# Title)
            match = re.match(r'^#\s+(.+)$', first_line)
            if match:
                full_title = match.group(1).strip()

                # Handle duplicated titles using same logic as html_post_processor
                words = full_title.split()

                # If title is short, no duplication likely
                if len(words) < 10:
                    return full_title

                # Look for sequences of repeated words (minimum 2 words)
                for i in range(len(words) - 3):
                    word1 = words[i]
                    word2 = words[i + 1]

                    if len(word1) < 3 or len(word2) < 3:
                        continue

                    # Search for this 2-word sequence later in the title
                    for j in range(i + 2, len(words) - 1):
                        if words[j] == word1 and words[j + 1] == word2:
                            # Found duplicate - trim at this point
                            return ' '.join(words[:j])

                return full_title
        except Exception as e:
            self.logger.debug(f"Could not extract title from {article_md}: {e}")

        # Fallback to folder name
        return self._get_display_name_without_date(
            self._clean_title_for_display(article_dir.name)
        )

    def generate_directory_index(
        self, directory_path: str, title: str, breadcrumb_path: List[str]
    ) -> str:
        """
        Generate HTML index for a directory listing using template system.

        Args:
            directory_path: Path to the directory
            title: Page title
            breadcrumb_path: List of breadcrumb elements

        Returns:
            Generated HTML content
        """
        try:
            # Get absolute path to Application directory
            app_dir = Path(__file__).parent.parent.absolute()

            # Determine which template to use based on directory level
            # Root level (News_DD-MM-YYYY) lists sources -> use root-index.html
            # Source level (within a source directory) lists articles -> use source-index.html
            directory_path_obj = Path(directory_path)
            directory_name = directory_path_obj.name

            # Check if this is a root directory (News archives or Capcats)
            is_news_root = directory_name.startswith(("News_", "news_")) and "-" in directory_name
            is_capcats_root = directory_name.lower() == "capcats"
            is_root_level = is_news_root or is_capcats_root

            # Select appropriate template
            if is_root_level:
                template_filename = "root-index.html"
            else:
                template_filename = "source-index.html"

            # Load the directory index template
            template_path = app_dir / "templates" / template_filename
            if not template_path.exists():
                self.logger.error(f"Template not found: {template_path}")
                return self._generate_error_page(
                    f"Template not found: {template_path}"
                )

            with open(template_path, "r", encoding="utf-8") as f:
                template_content = f.read()

            # Get embedded assets (CSS and JavaScript)
            embedded_assets = self._get_embedded_assets_for_template(app_dir)

            # Generate content HTML (pass is_root_level to enable bundle headers)
            content_html = self._generate_directory_listing(directory_path, is_root_level)

            # Generate breadcrumb HTML
            breadcrumb_html = self._generate_breadcrumb(
                breadcrumb_path,
                html_subfolder=False,
                current_file_path="news.html",
            )

            # For directory indices, determine navigation type
            # Already have directory_path_obj and directory_name from template selection above
            # is_root_level is equivalent to is_news_root

            # Check if we're in a subfolder within a news folder
            is_news_subfolder = (
                any(
                    part.startswith(("News_", "news_")) and "-" in part
                    for part in directory_path_obj.parts
                )
                and not is_root_level
            )

            if is_news_subfolder:
                # In news subfolder - use global index navigation
                index_nav = self._generate_global_index_navigation()
            else:
                # Standard navigation (including news root)
                index_nav = self._generate_index_navigation(
                    str(directory_path), show=not is_root_level
                )

            # Replace template placeholders
            html = template_content
            html = html.replace("{{page_title}}", f"Capcat - {title}")
            html = html.replace("{{embedded_styles}}", embedded_assets["embedded_styles"])
            html = html.replace("{{breadcrumb}}", breadcrumb_html)
            html = html.replace("{{page_heading}}", self._format_date_for_h1_header(title))
            html = html.replace("{{content}}", content_html)
            html = html.replace("{{top_navigation}}", index_nav)
            html = html.replace("{{bottom_navigation}}", index_nav)
            html = html.replace("{{embedded_script}}", embedded_assets["embedded_script"])

            return html

        except Exception as e:
            self.logger.error(
                f"Error generating directory index for {directory_path}: {e}"
            )
            return self._generate_error_page(f"Error loading directory: {e}")

    def _get_index_filename(self, levels_up: int, total_levels: int) -> str:
        """Determine whether to use index.html (root) or news.html (directory)."""
        # If we're going back to the root level (levels_up == total_levels - 1), use index.html
        # Otherwise use news.html for directory indices
        return "index.html" if levels_up == total_levels - 1 else "news.html"

    def _generate_breadcrumb(
        self,
        breadcrumb_path: List[str],
        html_subfolder: bool = False,
        current_file_path: str = None,
    ) -> str:
        """Generate breadcrumb navigation HTML.

        Renders 1–3 links depending on page type:
          news.html  (source dir)      → date only (1 link)
          article.html                 → date + source (2 links)
          html/article.html            → date + source (2 links)
          html/comments.html           → date + source + article (3 links)

        Display format:
          Level 0 (date):    "News 15 March 2026"
          Level 1 (source):  "Hacker News"
          Level 2 (article): article title as-is

        Depths are fixed by physical directory structure, not breadcrumb length.
        """
        if not breadcrumb_path or len(breadcrumb_path) < 2:
            return ""

        is_source_index = current_file_path and "news.html" in current_file_path
        is_comments = current_file_path == "comments.html"

        if is_source_index:
            show_items = breadcrumb_path[:1]
            base_depth = 1
        elif html_subfolder and is_comments and len(breadcrumb_path) >= 3:
            show_items = breadcrumb_path[:3]
            base_depth = 3
        elif html_subfolder:
            show_items = breadcrumb_path[:2]
            base_depth = 3
        else:
            show_items = breadcrumb_path[:2]
            base_depth = 2

        filenames = ["index.html", "news.html", "article.html"]
        months = [
            "January", "February", "March", "April", "May", "June",
            "July", "August", "September", "October", "November", "December",
        ]

        breadcrumb_items = []
        for i, item in enumerate(show_items):
            levels_up = base_depth - i
            # comments.html and article.html both live in html/ — same directory.
            # The formula gives levels_up=1 for the article link, but that would
            # point to ../article.html (wrong directory). Override to 0.
            if is_comments and i == 2:
                levels_up = 0
            href = "../" * levels_up + filenames[i]

            if i == 0:
                # Date folder: "News 15-03-2026" → "News 15 March 2026"
                m = re.search(r"(\d{2})-(\d{2})-(\d{4})", item)
                if m:
                    day, month, year = m.groups()
                    try:
                        display = f"News {int(day)} {months[int(month) - 1]} {year}"
                    except (ValueError, IndexError):
                        display = item
                else:
                    display = item
            elif i == 1:
                # Source folder: strip trailing date, normalise hyphens to spaces
                display = re.sub(r"\s+\d{2}-\d{2}-\d{4}$", "", item).strip()
                display = display.replace("-", " ")
            else:
                # Article title (comments only): use as-is
                display = item

            breadcrumb_items.append(
                f'<a href="{href}" title="Navigate to {display}">{display}</a>'
            )

        return "".join(breadcrumb_items)

    def _generate_directory_listing(self, directory_path: str, is_root_level: bool = False) -> str:
        """
        Generate HTML listing for directory contents with intelligent content detection.
        Auto-discovers categories from source configurations.
        """
        path = Path(directory_path)
        items = []

        if not path.exists():
            return "<p>Directory not found.</p>"

        # --- Start of Auto-Discovery Logic ---

        # 1. Category display names and order (only UI labels, not source assignments)
        category_display_names = {
            "techpro": "Tech Pro",
            "tech": "Tech",
            "news": "News",
            "science": "Science",
            "ai": "AI",
            "sports": "Sports",
        }

        # 2. Get source registry for auto-discovery
        from capcat.core.source_system.source_registry import get_source_registry
        registry = get_source_registry()
        all_source_ids = registry.get_available_sources()

        # 3. Build category-to-sources mapping from source configs (auto-discovery)
        category_sources = {}
        for source_id in all_source_ids:
            config = registry.get_source_config(source_id)
            if config and config.category:
                category = config.category.lower()
                if category not in category_sources:
                    category_sources[category] = []
                category_sources[category].append(source_id)

        self.logger.debug(f"Auto-discovered categories: {list(category_sources.keys())}")

        # 4. Load bundle order preferences (optional, for sorting within categories)
        bundle_orders = {}
        try:
            bundles_file = Path(__file__).parent.parent / "sources" / "active" / "bundles.yml"
            if bundles_file.exists():
                with open(bundles_file, 'r', encoding='utf-8') as f:
                    bundles_data = yaml.safe_load(f)
                    for bundle_name, bundle_info in bundles_data.get('bundles', {}).items():
                        if 'sources' in bundle_info:
                            bundle_orders[bundle_name] = bundle_info['sources']
        except Exception as e:
            self.logger.debug(f"Bundles not loaded (optional): {e}")

        # 5. Helper: Extract source ID from folder name
        def extract_source_id(name):
            """
            Extract source ID from folder name.
            Folder names use display_name (e.g., 'Hacker-News_19-10-2025')
            Need to map back to source_id (e.g., 'hn')
            """
            # Build reverse mapping using the same logic as get_source_folder_name()
            # which replaces spaces with hyphens, preserving special chars like |

            for source_id in all_source_ids:
                config = registry.get_source_config(source_id)
                if config and config.display_name:
                    # Match the exact logic from capcat.core.utils.get_source_folder_name
                    folder_prefix = config.display_name.replace(' ', '-')
                    if name.startswith(folder_prefix + '_'):
                        return source_id

            # Fallback: try matching source_id directly (for legacy folders)
            for source_id in sorted(all_source_ids, key=len, reverse=True):
                if name.lower().startswith(source_id.lower() + '_'):
                    return source_id

            return None

        # 6. Helper: Find category for a source (using auto-discovered mapping)
        def find_category(source_id):
            """Return the category name for a source ID, or None if unknown.

            Args:
                source_id: Source identifier string to look up.

            Returns:
                Category string (e.g. ``"tech"``), or ``None`` if not found.
            """
            if not source_id:
                return None
            for category, sources in category_sources.items():
                if source_id in sources:
                    return category
            return None

        # 7. Custom sorting function for category-based organization
        def sort_items(item):
            """Return a sort key for a directory entry based on category order.

            Args:
                item: A ``pathlib.Path``-like directory entry with a ``name``
                    attribute.

            Returns:
                Tuple used as a sort key: ``(category_index, source_order, name)``.
            """
            source_id = extract_source_id(item.name)
            category = find_category(source_id)

            if category and source_id and category in category_display_names:
                # Primary sort: by category order
                category_priority = list(category_display_names.keys()).index(category)

                # Secondary sort: by bundle order if defined, otherwise alphabetical
                if category in bundle_orders and source_id in bundle_orders[category]:
                    source_position = bundle_orders[category].index(source_id)
                else:
                    source_position = 999  # Unordered sources at end

                return (0, category_priority, source_position, source_id)

            # Non-categorized items sorted by modification time
            return (1, 0, 0, item.stat().st_mtime)

        # --- End of Auto-Discovery Logic ---

        # Process all items in directory, sorted correctly
        for item in sorted(path.iterdir(), key=sort_items):
            if item.is_dir():
                if find_article_md(item) is not None:
                    meta_text = "Article"
                    _comments_md = find_comments_md(item)
                    if _comments_md is not None:
                        comment_count = self._count_comments(_comments_md)
                        if comment_count > 0:
                            meta_text += f" | {comment_count} Comments"

                    # Only validate source_id at root level (source folders)
                    # At source level, we're listing articles which don't have source_id in folder names
                    if is_root_level:
                        source_id = extract_source_id(item.name)

                        # Determine if we're in Capcats (single article captures)
                        is_capcats = path.name.lower() == "capcats"

                        # For Capcats, allow all directories — source_id matching does not apply
                        # For News archives, only allow directories with valid source_id
                        if not is_capcats:
                            # Skip orphaned directories for deleted or unknown sources
                            if source_id is None:
                                self.logger.debug(
                                    f"Skipping orphaned article directory: {item.name} "
                                    f"(source could not be identified)"
                                )
                                continue

                            if source_id not in all_source_ids:
                                self.logger.debug(
                                    f"Skipping orphaned article directory: {item.name} "
                                    f"(source '{source_id}' no longer exists)"
                                )
                                continue

                        category = find_category(source_id) if source_id else None
                    else:
                        # At source level, articles don't need orphan filtering
                        source_id = None
                        category = None

                    # Extract title from markdown instead of using folder name
                    article_title = self._get_article_title_from_markdown(item)

                    items.append({
                        "type": "article",
                        "name": item.name,
                        "display_title": article_title,
                        "meta": meta_text,
                        "href": f"{item.name}/html/article.html",
                        "category": category,
                    })
                else:
                    article_count = sum(1 for d in item.rglob("*") if d.is_dir() and find_article_md(d) is not None)
                    source_id = extract_source_id(item.name)

                    # Determine if we're in Capcats (single article captures)
                    is_capcats = path.name.lower() == "capcats"

                    # For Capcats, allow all directories — source_id matching does not apply
                    # For News archives, only allow directories with valid source_id
                    if not is_capcats:
                        # Skip orphaned directories for deleted or unknown sources
                        if source_id is None:
                            self.logger.debug(
                                f"Skipping orphaned source directory: {item.name} "
                                f"(source could not be identified)"
                            )
                            continue

                        if source_id not in all_source_ids:
                            self.logger.debug(
                                f"Skipping orphaned source directory: {item.name} "
                                f"(source '{source_id}' no longer exists)"
                            )
                            continue

                    category = find_category(source_id) if source_id else None

                    # Skip empty directories (skipped sources with 0 articles)
                    if article_count == 0:
                        self.logger.debug(
                            f"Skipping empty source directory: {item.name} "
                            f"(0 articles - likely source was unavailable)"
                        )
                        continue

                    items.append({
                        "type": "directory",
                        "name": item.name,
                        "meta": f"{article_count} articles",
                        "href": f"{item.name}/news.html",
                        "category": category,
                    })

        # Generate HTML with category headers (auto-discovered)
        html_items = []
        if is_root_level:
            current_category = None
            list_started = False

            for item in items:
                item_category = item.get("category")

                # Start new category section if category changed
                if item_category != current_category:
                    if list_started:
                        html_items.append('</ul>')

                    # Add category header
                    if item_category and item_category in category_display_names:
                        html_items.append(f'<h2 class="bundle-header">{category_display_names[item_category]}</h2>')
                    elif item_category:
                        # Auto-discovered category not in display names - use titlecase
                        html_items.append(f'<h2 class="bundle-header">{item_category.title()}</h2>')

                    html_items.append('<ul class="directory-list">')
                    list_started = True
                    current_category = item_category

                if not list_started:
                    html_items.append('<ul class="directory-list">')
                    list_started = True

                # Use display_title for articles (extracted from markdown), folder name for directories
                # Truncate article titles to 200 characters for HTML cards
                display_name = truncate_title_intelligently(item.get("display_title")) if item["type"] == "article" else self._get_display_name_without_date(self._clean_title_for_display(item["name"]))

                html_items.append(f'''
            <li class="directory-item">
                <div class="directory-info">
                    <div class="directory-name">
                        <a href="{item["href"]}" title="Open {display_name}">{display_name}</a>
                    </div>
                    <div class="directory-meta">{item["meta"]}</div>
                </div>
            </li>''')

            if list_started:
                html_items.append('</ul>')
        else:
            html_items.append('<ul class="directory-list">')
            for item in items:
                 # Use display_title for articles (extracted from markdown), folder name for directories
                 # Truncate article titles to 200 characters for HTML cards
                 display_name = truncate_title_intelligently(item.get("display_title")) if item["type"] == "article" else self._get_display_name_without_date(self._clean_title_for_display(item["name"]))

                 html_items.append(f'''
            <li class="directory-item">
                <div class="directory-info">
                    <div class="directory-name">
                        <a href="{item["href"]}" title="Open {display_name}">{display_name}</a>
                    </div>
                    <div class="directory-meta">{item["meta"]}</div>
                </div>
            </li>''')
            html_items.append('</ul>')

        if not html_items:
            return "<p>No items found in this directory.</p>"

        return "".join(html_items)

    def _generate_index_navigation(
        self,
        current_path: str,
        show: bool = True,
        html_subfolder: bool = False,
    ) -> str:
        """Generate index navigation HTML."""
        if not show:
            return ""

        current_path_obj = Path(current_path)
        path_parts = current_path_obj.parts

        # Find the news date folder and its position in the path
        news_date_folder = None
        news_date_index = -1

        for i, part in enumerate(path_parts):
            # Check for both lowercase 'news_' and capitalized 'News_' formats
            if (part.startswith("news_") or part.startswith("News_")) and len(
                part
            ) == 15:  # news_DD-MM-YYYY or News_DD-MM-YYYY format
                news_date_folder = part
                news_date_index = i
                break

        if not news_date_folder or news_date_index == -1:
            return ""  # No news date folder found

        # Find the source folder (e.g., Hacker-News_10-09-2025) in the path
        source_folder = None
        source_index = -1

        # Get actual configured sources instead of hardcoded list
        from capcat.core.source_configs import SOURCE_CONFIGURATIONS
        from capcat.core.utils import get_source_folder_name

        source_codes = list(SOURCE_CONFIGURATIONS.keys())

        for i, part in enumerate(path_parts):
            # Look for source patterns (old format with underscores and new format with spaces)
            for source_code in source_codes:
                folder_name = get_source_folder_name(source_code)

                # Check old format (hn_DD-MM-YYYY)
                if "_" in part and part.startswith(f"{source_code}_"):
                    source_folder = part
                    source_index = i
                    break
                # Check underscore format (Hacker-News_DD-MM-YYYY)
                if "_" in part and part.startswith(f"{folder_name}_"):
                    source_folder = part
                    source_index = i
                    break
                # Check new space format (Hacker News 26-09-2025)
                if " " in part and part.startswith(f"{folder_name} "):
                    source_folder = part
                    source_index = i
                    break
            if source_folder:
                break

        if not source_folder or source_index == -1:
            return ""  # No source folder found

        # Calculate levels up to reach the source folder
        current_depth = (
            len(path_parts) - 1
        )  # -1 because last part might be a file

        # If current path is a file (e.g., article.md), adjust depth
        if current_path_obj.is_file() or str(current_path).endswith(".md"):
            levels_up = current_depth - source_index
        else:
            # It's a directory
            levels_up = current_depth - source_index

        # Generate the relative path to the source index
        if levels_up <= 0:
            return ""  # We're at or above the source level

        # The levels_up calculation is based on .md file path
        # For HTML in subfolder, the .md is in article_dir, HTML is in article_dir/html/
        # So from HTML to source folder is same levels_up as from .md to source folder
        index_url = "../" * levels_up + "news.html"
        index_text = "Back to News"

        return f"""<div class="index-nav">
            <a href="{index_url}" class="index-link" title="Go to {index_text}">
                <span>{index_text}</span>
            </a>
        </div>"""

    def _generate_global_index_navigation(self) -> str:
        """Generate global index navigation HTML."""

        return """<div class="navigation-container">
            <div class="index-nav">
                <a href="../index.html" class="global-index-link" title="Return to main news archive index">
                    <span>Back to Global Index</span>
                </a>
            </div>
            <div class="comments-nav">
                <!-- Empty for consistency with article navigation structure -->
            </div>
        </div>"""

    def _generate_comments_navigation(
        self, current_article_path: str, html_subfolder: bool = False
    ) -> str:
        """Generate comments navigation HTML if comments exist."""
        article_path = Path(current_article_path)
        article_dir = article_path.parent

        # Check if comments file exists in the same directory
        comments_path = find_comments_md(article_dir)
        if comments_path is None:
            return ""

        # Check if we're already on the comments page
        if article_path.stem.endswith("-Comments"):
            # On comments page - show "Back to Article" link
            article_url = "article.html"
            return f"""<div class="comments-nav">
                <a href="{article_url}" class="article-link" title="Back to the main article">
                    <span>Back to Article</span>
                </a>
            </div>"""

        # On article page - show "View Comments" link
        comments_url = "comments.html"
        return f"""<div class="comments-nav">
            <a href="{comments_url}" class="comments-link" title="View all comments for this article">
                <span>View Comments</span>
            </a>
        </div>"""

    def _create_navigation_container(
        self, index_nav: str, comments_nav: str
    ) -> str:
        """Create a navigation container with both index and comments navigation."""

        # Extract content from div wrappers more carefully
        def extract_content(html_content: str, div_class: str) -> str:
            """Extract the inner HTML from a known div wrapper, if present.

            Args:
                html_content: HTML string that may contain a ``<div class="…">``
                    wrapper.
                div_class: CSS class name of the wrapper div to strip.

            Returns:
                Inner content string with the outer div removed, or the original
                *html_content* if the wrapper is not found.
            """
            if not html_content:
                return ""
            # If the content already has the div wrapper, extract just the inner content
            start_tag = f'<div class="{div_class}">'
            end_tag = "</div>"
            if start_tag in html_content and end_tag in html_content:
                start_pos = html_content.find(start_tag) + len(start_tag)
                end_pos = html_content.rfind(end_tag)
                return html_content[start_pos:end_pos].strip()
            else:
                # Content doesn't have wrapper, return as-is (already extracted)
                return html_content.strip()

        index_content = extract_content(index_nav, "index-nav")
        comments_content = extract_content(comments_nav, "comments-nav")

        return f"""<div class="navigation-container">
            <div class="index-nav">{index_content}</div>
            <div class="comments-nav">{comments_content}</div>
        </div>"""

    def _generate_article_navigation(self, markdown_path: str, html_subfolder: bool = False) -> Dict[str, str]:
        """
        Generate navigation content for articles, including Back to News button.

        Args:
            markdown_path: Path to the markdown file
            html_subfolder: True if HTML is in html/ subfolder

        Returns:
            Dictionary with 'top' and 'bottom' navigation HTML
        """
        try:
            # Generate the "Back to News" navigation
            index_nav_full = self._generate_index_navigation(markdown_path)

            # Extract just the inner content (remove the div wrapper)
            import re
            match = re.search(r'<div class="index-nav">(.*?)</div>', index_nav_full, re.DOTALL)
            index_nav = match.group(1).strip() if match else ""

            # Check if comments exist for this article
            comments_nav = ""
            article_dir = Path(markdown_path).parent
            comments_file = find_comments_md(article_dir)

            if comments_file is not None:
                # Generate comments navigation
                if html_subfolder:
                    comments_url = "comments.html"
                else:
                    comments_url = "html/comments.html"

                comments_nav = f"""<a href="{comments_url}" class="comments-link" title="View all comments for this article">
                    <span>View Comments</span>
                </a>"""

            # Combine into navigation container
            navigation_html = f"""<div class="navigation-container">
            <div class="index-nav">{index_nav}</div>
            <div class="comments-nav">{comments_nav}</div>
        </div>"""

            return {
                'top': navigation_html,
                'bottom': navigation_html
            }

        except Exception as e:
            self.logger.error(f"Error generating article navigation: {e}")
            # Return empty navigation as fallback
            return {'top': '', 'bottom': ''}

    def _clean_markdown_content(self, content: str) -> str:
        """Clean up problematic content in markdown that could interfere with templates."""
        # Strip YAML frontmatter block before any other processing
        if content.startswith("---"):
            end = content.find("\n---", 3)
            if end != -1:
                content = content[end + 4:].lstrip("\n")

        # Remove {{ message }} tags that appear in GitHub and other sites
        content = re.sub(r"\{\{\s*message\s*\}\}", "", content)

        # Remove other common template-like patterns that cause issues
        content = re.sub(r"\{\{\s*[^}]+\s*\}\}", "", content)

        # Strip lines containing Obsidian wikilinks — HTML has its own navigation
        content = re.sub(r"^[^\S\n]*.*\[\[.*?\]\].*$", "", content, flags=re.MULTILINE)

        # Clean up any resulting empty lines
        content = re.sub(r"\n\s*\n\s*\n", "\n\n", content)

        return content.strip()

    def _remove_grey_placeholder_images(self, html_content: str) -> str:
        """Remove grey-placeholder images from HTML content."""
        import re

        # Remove img tags with grey-placeholder sources
        html_content = re.sub(
            r'<img[^>]*src="[^"]*grey-placeholder[^"]*"[^>]*/?>',
            "",
            html_content,
        )

        return html_content

    def _remove_headerlink_anchors(self, html_content: str) -> str:
        """Remove headerlink anchor tags from HTML content."""
        import re

        # Remove headerlink anchor tags like: <a class="headerlink" href="#section" title="Link to this section">¶</a>
        pattern = r'<a\s+class="headerlink"[^>]*>.*?</a>'
        return re.sub(
            pattern, "", html_content, flags=re.IGNORECASE | re.DOTALL
        )

    def _remove_image_anchor_wrappers(self, html_content: str) -> str:
        """Remove anchor tags that wrap image tags.

        Substack and other sources download images locally but markdown-to-HTML
        conversion wraps them in <a href> linking to original URL. This removes
        those anchor wrappers while preserving the image tags and their attributes.

        Args:
            html_content: HTML content to process

        Returns:
            HTML with image anchor wrappers removed
        """
        import re

        # Pattern matches: <a ...href="..."...><optional whitespace><img ...><optional whitespace></a>
        # Captures the img tag to preserve it
        pattern = r'<a[^>]*href="[^"]*"[^>]*>\s*(<img[^>]*>)\s*</a>'

        # Replace with just the captured img tag
        return re.sub(pattern, r'\1', html_content, flags=re.IGNORECASE | re.DOTALL)

    def _remove_duplicate_h1_title(
        self, html_content: str, article_title: str
    ) -> str:
        """Remove duplicate H1 title from article content if it matches header title."""
        import re

        # Clean the article title to match what might be in the content
        # Handle both the cleaned version and original version
        clean_title = article_title.strip()

        # Look for H1 tags at the beginning of content that match our title
        # Match various formats: with/without | symbols, slightly different text
        h1_patterns = [
            # Exact match
            rf"<h1[^>]*>\s*{re.escape(clean_title)}\s*</h1>",
            # With pipe separator (common in titles)
            rf'<h1[^>]*>[^<]*{re.escape(clean_title.split("|")[0].strip() if "|" in clean_title else clean_title)}[^<]*</h1>',
            # Generic first H1 at start of content (more aggressive)
            r"^\s*<h1[^>]*>.*?</h1>\s*",
        ]

        # Try each pattern
        for pattern in h1_patterns:
            if re.search(
                pattern, html_content, flags=re.IGNORECASE | re.DOTALL
            ):
                html_content = re.sub(
                    pattern,
                    "",
                    html_content,
                    count=1,
                    flags=re.IGNORECASE | re.DOTALL,
                )
                break

        return html_content.strip()

    def _wrap_source_url_in_div(self, html_content: str) -> str:
        """
        Transform source/comments URL paragraphs into semantic div elements for better styling.

        This function converts markdown-generated paragraph tags containing source URLs
        and comments URLs into div elements with CSS class 'source-url' for consistent
        styling across the application.

        Args:
            html_content: HTML string from markdown conversion

        Returns:
            HTML string with URL paragraphs converted to divs

        Regex Pattern Explanation:
            - (<p[^>]*>): Captures opening <p> tag with any attributes
            - \\s*: Matches optional whitespace
            - (<strong>(?:Source URL|Comments URL):</strong>\\s*<a\\s+href="[^"]*"[^>]*>[^<]*</a>):
              Captures the strong tag + link structure for both source and comments URLs
            - \\s*: Matches optional whitespace
            - (</p>): Captures closing </p> tag

        Transform: <p><strong>Source URL:</strong> <a href="...">...</a></p>
                → <div class="source-url"><strong>Source URL:</strong> <a href="...">...</a></div>
        """
        import re

        # Regex pattern to match source/comments URL paragraphs - handles both variants
        pattern = r'(<p[^>]*>)\s*(<strong>(?:Source URL|Comments URL):</strong>\s*<a\s+href="[^"]*"[^>]*>[^<]*</a>)\s*(</p>)'

        # Replacement function - extracts content and wraps in semantic div
        # Also truncates long display URLs to keep them readable
        _MAX_URL_DISPLAY = 80

        def _truncate_anchor_text(anchor_match):
            """Shorten anchor display text to ``_MAX_URL_DISPLAY`` characters.

            Args:
                anchor_match: Regex match with groups ``(href, text)``.

            Returns:
                Replacement ``<a>`` tag with truncated display text.
            """
            href = anchor_match.group(1)
            text = anchor_match.group(2)
            if len(text) > _MAX_URL_DISPLAY:
                text = text[:_MAX_URL_DISPLAY - 3] + "..."
            return f'<a href="{href}">{text}</a>'

        def replace_source_url(match):
            """Replace a ``<p>`` containing a source URL with a semantic div.

            Args:
                match: Regex match with groups ``(p_open, content, p_close)``.

            Returns:
                ``<div class="source-url">`` wrapping the (possibly truncated)
                anchor content.
            """
            p_open, content, p_close = match.groups()
            content = re.sub(
                r'<a\s+href="([^"]*)"[^>]*>([^<]*)</a>',
                _truncate_anchor_text,
                content,
                flags=re.IGNORECASE,
            )
            return f'<div class="source-url">{content}</div>'

        # Apply transformation with case-insensitive matching
        html_content = re.sub(
            pattern, replace_source_url, html_content, flags=re.IGNORECASE
        )

        return html_content

    def _extract_viewbox(self, el) -> "tuple[float, float] | None":
        """Extract (width, height) from a BeautifulSoup SVG/img element, or None."""
        import base64
        import urllib.parse

        # Inline <svg>: read viewBox attribute directly
        if getattr(el, "name", "") == "svg":
            vb_attr = el.get("viewBox") or el.get("viewbox", "")
            m = re.search(r'[\d.]+\s+[\d.]+\s+([\d.]+)\s+([\d.]+)', vb_attr)
            if m:
                return float(m.group(1)), float(m.group(2))
            w = el.get("width")
            h = el.get("height")
            if w and h:
                try:
                    return float(w), float(h)
                except ValueError:
                    pass
            return None

        # <img>: try data URI first
        src = el.get("src", "")
        if src.startswith("data:image/svg"):
            comma = src.find(",")
            if comma == -1:
                return None
            data_part = src[comma + 1:]

            if ";base64," in src:
                try:
                    decoded = base64.b64decode(data_part).decode("utf-8", errors="replace")
                except Exception:
                    decoded = ""
            else:
                decoded = urllib.parse.unquote(data_part)

            vb_match = re.search(
                r'viewBox=["\']?\s*([\d.]+)\s+([\d.]+)\s+([\d.]+)\s+([\d.]+)',
                decoded
            )
            if vb_match:
                return float(vb_match.group(3)), float(vb_match.group(4))

        # Fall back to explicit width/height attrs on <img>
        w_attr = el.get("width")
        h_attr = el.get("height")
        if w_attr and h_attr:
            try:
                return float(w_attr), float(h_attr)
            except ValueError:
                pass

        return None

    def _score_svg_element(self, el, soup) -> "tuple[int, int]":
        """Return (icon_score, illustration_score) for a BeautifulSoup element."""
        from bs4 import NavigableString

        icon = 0
        illus = 0

        # --- Size/shape signals ---
        vb = self._extract_viewbox(el)
        if vb:
            w, h = vb
            if w == h:
                icon += 3               # square
            else:
                illus += 4              # non-square
            if w > 200 or h > 200:
                illus += 4              # large
            if w <= 64 and h <= 64:
                icon += 3               # small

        # --- src signal ---
        src = el.get("src", "")
        if src.startswith("data:image/svg"):
            icon += 2

        # --- Accessibility attributes (one combined check, max 2 pts) ---
        if el.get("aria-label") or el.get("role") == "img":
            icon += 2

        # --- Parent context ---
        parent = el.parent
        if parent:
            parent_name = getattr(parent, "name", "")
            if parent_name in ("li", "button"):
                icon += 2
            elif parent_name == "a":
                # Only award points if <a> is inside nav/footer/header/li
                ancestors = {getattr(a, "name", "") for a in parent.parents}
                if ancestors & {"nav", "footer", "header", "li"}:
                    icon += 2

            # --- Sole child of <p> between sibling <p> elements ---
            if parent_name == "p":
                meaningful = [
                    s for s in parent.children
                    if not (isinstance(s, NavigableString) and str(s).strip() == "")
                ]
                if len(meaningful) == 1 and parent.parent is not None:
                    p_siblings = [
                        s for s in parent.parent.children
                        if hasattr(s, "name") and s.name == "p" and s is not parent
                    ]
                    if p_siblings:
                        illus += 2

            # Non-whitespace sibling text → inline icon context (any parent type)
            text_siblings = [
                s for s in parent.children
                if isinstance(s, NavigableString) and str(s).strip()
            ]
            if text_siblings:
                icon += 1

        return icon, illus

    def _classify_svg_elements(self, html_content: str) -> str:
        """Classify <img src="*.svg"> and inline <svg> elements as icons or
        illustrations using weighted heuristics. Appends class 'capcat-icon'
        to elements classified as icons. Uses html.parser (no extra deps).
        """
        from bs4 import BeautifulSoup

        soup = BeautifulSoup(html_content, "html.parser")

        for el in soup.find_all(["img", "svg"]):
            src = el.get("src", "")
            name = getattr(el, "name", "")
            # Only process SVG-related elements
            if name == "svg" or src.endswith(".svg") or src.startswith("data:image/svg"):
                icon_score, illus_score = self._score_svg_element(el, soup)
                if icon_score > illus_score:
                    existing = el.get("class") or []
                    el["class"] = existing + ["capcat-icon"]

        return str(soup)

    def _adjust_paths_for_subfolder(self, html_content: str) -> str:
        """Adjust relative paths in HTML content when HTML files are in html/ subfolder."""
        import re

        # Adjust image paths: images/ -> ../images/
        html_content = re.sub(r'src="images/', 'src="../images/', html_content)
        html_content = re.sub(
            r"src=\'images/", "src='../images/", html_content
        )

        # Adjust file paths: files/ -> ../files/
        html_content = re.sub(r'href="files/', 'href="../files/', html_content)
        html_content = re.sub(
            r"href=\'files/", "href='../files/", html_content
        )

        # Adjust audio paths: audio/ -> ../audio/ (both src and href)
        html_content = re.sub(r'src="audio/', 'src="../audio/', html_content)
        html_content = re.sub(r"src=\'audio/", "src='../audio/", html_content)
        html_content = re.sub(r'href="audio/', 'href="../audio/', html_content)
        html_content = re.sub(
            r"href=\'audio/", "href='../audio/", html_content
        )

        # Adjust video paths: video/ -> ../video/ (both src and href)
        html_content = re.sub(r'src="video/', 'src="../video/', html_content)
        html_content = re.sub(r"src=\'video/", "src='../video/", html_content)
        html_content = re.sub(r'href="video/', 'href="../video/', html_content)
        html_content = re.sub(
            r"href=\'video/", "href='../video/", html_content
        )

        # Adjust PDF file paths in article folder: filename.pdf -> ../filename.pdf
        # Only adjust PDFs that are direct files (not in subfolders like files/ or already ../)
        html_content = re.sub(
            r'href="([^/"\']+\.pdf)"',
            r'href="../\1"',
            html_content
        )
        html_content = re.sub(
            r"href='([^/'\"]+\.pdf)'",
            r"href='../\1'",
            html_content
        )

        return html_content

    def _is_main_news_index(self, directory_path: str) -> bool:
        """Check if current directory is the main news index (news_DD-MM-YYYY or News_DD-MM-YYYY level)."""
        path = Path(directory_path)
        # Check if directory name matches news_DD-MM-YYYY or News_DD-MM-YYYY pattern
        return bool(re.match(r"[Nn]ews_\d{2}-\d{2}-\d{4}$", path.name))

    def _is_news_source_directory(self, directory_name: str) -> bool:
        """Check if directory is a news source directory (e.g., Hacker-News_DD-MM-YYYY, BBC-News_DD-MM-YYYY)."""
        # Check if directory name matches source_DD-MM-YYYY pattern (old and new formats)
        from capcat.core.utils import get_source_folder_name

        news_sources = [
            "hn",
            "lb",
            "iq",
            "bbc",
            "cnn",
            "reuters",
            "techcrunch",
            "theverge",
            "wired",
            "gn",
            "ap",
            "theguardian",
            "nytimes",
            "washingtonpost",
            "aljazeera",
            "bloomberg",
            "financialtimes",
            "nature",
            "scientificamerican",
            "mittechreview",
            "engadget",
            "gizmodo",
            "cnet",
            "venturebeat",
            "mashable",
            "recode",
            "euronews",
            "straitstimes",
            "newsmap",
            "upday",
        ]

        for source in news_sources:
            # Check old format (hn_DD-MM-YYYY)
            if re.match(
                f"^{source}_\\d{{2}}-\\d{{2}}-\\d{{4}}$", directory_name
            ):
                return True
            # Check new format (Hacker-News_DD-MM-YYYY)
            folder_name = get_source_folder_name(source)
            escaped_folder_name = re.escape(folder_name)
            if re.match(
                f"^{escaped_folder_name}_\\d{{2}}-\\d{{2}}-\\d{{4}}$",
                directory_name,
            ):
                return True
        return False

    def _clean_title_for_display(self, title: str) -> str:
        """
        Clean title for display by removing date stamps and replacing underscores.
        Converts folder names like "IEEE_Spectrum_25-09-2025" to "IEEE Spectrum".
        """
        # Check if this is a source directory name (e.g., IEEE_Spectrum_13-10-2025)
        source_date_match = re.match(r"^([a-zA-Z][\w_-]*?)_\d{2}-\d{2}-\d{4}$", title)
        if source_date_match:
            source_part = source_date_match.group(1)
            # The source_part is the display name with spaces replaced by underscores.
            # Just reverse that transformation to get the proper name and casing.
            return source_part.replace('_', ' ').replace('-', ' ')

        # Fallback for article titles (e.g., "01_My_Article_Title")
        cleaned = re.sub(r"^\d+_", "", title)
        cleaned = cleaned.replace("_", " ")
        cleaned = re.sub(r"--+", " ", cleaned)
        cleaned = re.sub(r"\s+", " ", cleaned)
        return cleaned.strip()

    def _count_comments(self, comments_file_path: Path) -> int:
        """Count the number of comments in a comments.md file."""
        try:
            if not comments_file_path.exists():
                return 0

            with open(comments_file_path, "r", encoding="utf-8") as f:
                content = f.read()

            # Count comments by looking for different username patterns used by various sources:
            # LessWrong: **username** (<a href="...">profile</a>)
            # HN/Lobsters: **username** ([profile](URL))
            import re

            # Try LessWrong format first
            lesswrong_pattern = r"\*\*[^*]+\*\*\s*\(<a[^>]*>profile</a>\)"
            lesswrong_matches = re.findall(lesswrong_pattern, content)

            # Try HN/Lobsters format
            hn_lobsters_pattern = r"\*\*[^*]+\*\*\s*\(\[profile\]\([^)]*\)\)"
            hn_lobsters_matches = re.findall(hn_lobsters_pattern, content)

            # Return the count from whichever pattern matched
            total_matches = len(lesswrong_matches) + len(hn_lobsters_matches)
            return total_matches
        except Exception as e:
            self.logger.debug(
                f"Error counting comments in {comments_file_path}: {e}"
            )
            return 0

    def _format_date_for_h1_header(self, title: str) -> str:
        """
        Format dates in titles specifically for h1 headers only.
        Converts "InfoQ 11-09-2025" to "InfoQ 11 September 2025"
        Only affects the date portion, leaves source names unchanged.
        """

        # Month names mapping
        months = {
            "01": "January",
            "02": "February",
            "03": "March",
            "04": "April",
            "05": "May",
            "06": "June",
            "07": "July",
            "08": "August",
            "09": "September",
            "10": "October",
            "11": "November",
            "12": "December",
        }

        # Pattern to match dates in DD-MM-YYYY or MM-DD-YYYY format at the end of title
        date_pattern = r"(\d{1,2})-(\d{1,2})-(\d{4})(?=\s*$)"
        match = re.search(date_pattern, title)

        if match:
            day, month, year = match.groups()

            # Assume DD-MM-YYYY format (European style)
            if month in months:
                month_name = months[month]
                formatted_date = f"{day} {month_name} {year}"
                # Replace the date portion with formatted version
                return re.sub(date_pattern, formatted_date, title)

        return title

    def _generate_error_page(self, error_message: str) -> str:
        """Generate error page HTML."""
        template = self._get_base_template(
            None, False
        )  # Error pages don't use subfolders
        return template.substitute(
            title="Capcat - Error",
            page_title="Error",
            breadcrumb="",
            content=f"<div class='error'><h2>Error</h2><p>{error_message}</p></div>",
            top_navigation="",
            bottom_navigation="",
        )
