WHICH CODE SNIPPETS FROM THIS CODE WOULD I NEED TO UNDERSTAND HOW TO ADD A PREFIX IMAGE BEFORE TITLE. DON'T CHANGE ANY CODE, JUST GIVE ME THE SNIPPETS I NEED.

```
//
//  ExportSheet.swift
//  AllMyWalks
//
//  Copyright © Aelptos. All rights reserved.
//

import SwiftUI
@preconcurrency import MapLibre
import CoreLocation

// MARK: - Export Sheet

struct ExportSheet: View {
    let segments: [StyledSegment]  // Full-detail segments for actual export
    let previewSegments: [StyledSegment]?  // Simplified segments for preview (nil = use segments)
    let bounds: MLNCoordinateBounds?  // Auto-fit bounds from all routes
    let viewportState: MapViewportState?  // Current viewport from map (bounds + rotation, nil if not available)
    let routeStyle: RouteStyle
    let cityName: String
    let countryCode: String
    let totalDistance: Double
    let dateRange: (earliest: Date, latest: Date)?
    let workoutCount: Int
    /// When true, the view is pushed into an existing NavigationStack (no internal NavigationStack)
    var embeddedInNavigation: Bool = false

    /// Whether we're using simplified preview segments
    private var isUsingSimplifiedPreview: Bool {
        previewSegments != nil
    }

    /// Segments to use for preview rendering
    private var segmentsForPreview: [StyledSegment] {
        previewSegments ?? segments
    }

    @Environment(\.dismiss) private var dismiss
    @Environment(SubscriptionManager.self) private var subscriptionManager

    // Namespace for matched geometry effect (fullscreen transition)
    @Namespace private var fullscreenNamespace

    // Paywall state for fullscreen preview (premium feature)
    @State private var showPaywall = false

    // Template selection
    @State private var selectedTemplate: ExportTemplate = .artDeco
    @State private var isCustomizing: Bool = false
    @State private var showTemplateResetAlert: Bool = false
    @State private var pendingTemplate: ExportTemplate?

    // Custom framing (use viewport bounds instead of auto-fit)
    @State private var useCustomFraming: Bool = false

    // Export settings
    @State private var titleType: ExportTitleType = .city
    @State private var subtitleType: ExportSubtitleType = .distance
    @State private var customTitle: String = ""
    @State private var customSubtitle: String = ""
    @State private var titleFontName: String?
    @State private var subtitleFontName: String?
    @State private var titleColor: Color = .black
    @State private var subtitleColor: Color = .black
    @State private var titleFontSizeRatio: CGFloat = 0.055
    @State private var subtitleFontSizeRatio: CGFloat = 0.028
    @State private var titleAlignment: ExportTextAlignment = .center
    @State private var subtitleAlignment: ExportTextAlignment = .center
    // Outer border
    @State private var outerBorderType: BorderLineType = .normal
    @State private var outerBorderColor: Color = .black
    @State private var outerBorderWidthRatio: CGFloat = 0.002
    @State private var outerBorderCornerRadiusRatio: CGFloat = 0
    // Inner border
    @State private var innerBorderType: BorderLineType = .normal
    @State private var innerBorderColor: Color = .black
    @State private var innerBorderWidthRatio: CGFloat = 0.004
    @State private var innerBorderCornerRadiusRatio: CGFloat = 0
    // Border gap and padding
    @State private var borderGapRatio: CGFloat = 0.012
    @State private var borderGapColor: Color?  // nil = use background color
    @State private var paddingRatio: CGFloat = 0.04
    // Poster outer border (around entire document)
    @State private var posterOuterBorderType: BorderLineType = .none
    @State private var posterOuterBorderColor: Color = .black
    @State private var posterOuterBorderWidthRatio: CGFloat = 0.004
    @State private var posterOuterBorderCornerRadiusRatio: CGFloat = 0
    // Poster inner border (around entire document)
    @State private var posterInnerBorderType: BorderLineType = .none
    @State private var posterInnerBorderColor: Color = .black
    @State private var posterInnerBorderWidthRatio: CGFloat = 0.002
    @State private var posterInnerBorderCornerRadiusRatio: CGFloat = 0
    // Poster border gap
    @State private var posterBorderGapRatio: CGFloat = 0.008
    @State private var posterBorderGapColor: Color?
    // Poster margin (space between document edge and poster border)
    @State private var posterMarginRatio: CGFloat = 0
    // Title position
    @State private var titlePosition: TitlePosition = .bottom
    // Poster decoration
    @State private var decorationType: DecorationType = .none
    @State private var decorationPosition: DecorationPosition = .bottom
    @State private var decorationAlignment: DecorationAlignment = .right
    @State private var decorationColor1: Color = .red
    @State private var decorationColor2: Color = .blue
    @State private var decorationColor3: Color = .yellow
    @State private var decorationSizeRatio: CGFloat = 0.08
    @State private var decorationSpacingRatio: CGFloat = 0.015
    // Layout settings
    @State private var backgroundColor: Color = .white
    @State private var titleAreaHeightRatio: CGFloat = 0.12
    @State private var elementSpacingRatio: CGFloat = 0.015
    // Map style settings (for export)
    @State private var exportMapStyle: MapStyle = .alidadeSmooth
    @State private var exportMapLanguage: MapLanguage = .local
    @State private var exportHideLabels: Bool = false
    @State private var exportFilter: ExportFilter = .none
    @State private var exportLineWidth: CGFloat = 3.0
    @State private var exportDefaultColor: Color = .blue
    @State private var exportUseColorPerWorkoutType: Bool = false
    @State private var exportUseLineWidthPerWorkoutType: Bool = false
    @State private var exportWorkoutTypeColors: [WorkoutType: Color] = [
        .walking: .blue,
        .running: .orange,
        .hiking: .green,
        .cycling: .purple,
        .swimming: .cyan,
        .other: .gray
    ]
    @State private var exportWorkoutTypeLineWidths: [WorkoutType: CGFloat] = [
        .walking: 3.0,
        .running: 3.0,
        .hiking: 3.0,
        .cycling: 3.0,
        .swimming: 3.0,
        .other: 3.0
    ]
    @State private var previewImage: UIImage?
    @State private var isGeneratingPreview = false
    @State private var isExporting = false
    @State private var exportedImage: UIImage?
    @State private var showShareSheet = false
    @State private var showFullscreenPreview = false
    @State private var showExportSizeSheet = false
    @State private var snapshotter: MLNMapSnapshotter?

    private let previewSize = CGSize(width: 400, height: 400)

    /// Extra height to render for cropping out attribution (capped at 10%).
    private let attributionCropFraction: CGFloat = 0.10

    /// Combined hash of all preview-affecting state variables.
    /// Used with a single onChange to replace 55+ individual onChange modifiers,
    /// preventing stack overflow on devices with limited stack size.
    private var previewTriggerHash: Int {
        var hasher = Hasher()
        hasher.combine(titleType)
        hasher.combine(subtitleType)
        hasher.combine(customTitle)
        hasher.combine(customSubtitle)
        hasher.combine(titleFontName)
        hasher.combine(subtitleFontName)
        hasher.combine(titleColor)
        hasher.combine(subtitleColor)
        hasher.combine(titleFontSizeRatio)
        hasher.combine(subtitleFontSizeRatio)
        hasher.combine(titleAlignment)
        hasher.combine(subtitleAlignment)
        hasher.combine(outerBorderType)
        hasher.combine(outerBorderColor)
        hasher.combine(outerBorderWidthRatio)
        hasher.combine(outerBorderCornerRadiusRatio)
        hasher.combine(innerBorderType)
        hasher.combine(innerBorderColor)
        hasher.combine(innerBorderWidthRatio)
        hasher.combine(innerBorderCornerRadiusRatio)
        hasher.combine(borderGapRatio)
        hasher.combine(borderGapColor)
        hasher.combine(paddingRatio)
        hasher.combine(posterOuterBorderType)
        hasher.combine(posterOuterBorderColor)
        hasher.combine(posterOuterBorderWidthRatio)
        hasher.combine(posterOuterBorderCornerRadiusRatio)
        hasher.combine(posterInnerBorderType)
        hasher.combine(posterInnerBorderColor)
        hasher.combine(posterInnerBorderWidthRatio)
        hasher.combine(posterInnerBorderCornerRadiusRatio)
        hasher.combine(posterBorderGapRatio)
        hasher.combine(posterBorderGapColor)
        hasher.combine(posterMarginRatio)
        hasher.combine(titlePosition)
        hasher.combine(decorationType)
        hasher.combine(decorationPosition)
        hasher.combine(decorationAlignment)
        hasher.combine(decorationColor1)
        hasher.combine(decorationColor2)
        hasher.combine(decorationColor3)
        hasher.combine(decorationSizeRatio)
        hasher.combine(decorationSpacingRatio)
        hasher.combine(backgroundColor)
        hasher.combine(titleAreaHeightRatio)
        hasher.combine(elementSpacingRatio)
        hasher.combine(exportMapStyle)
        hasher.combine(exportMapLanguage)
        hasher.combine(exportHideLabels)
        hasher.combine(exportFilter)
        hasher.combine(exportLineWidth)
        hasher.combine(exportDefaultColor)
        hasher.combine(exportUseColorPerWorkoutType)
        hasher.combine(exportUseLineWidthPerWorkoutType)
        hasher.combine(selectedTemplate)
        hasher.combine(useCustomFraming)
        return hasher.finalize()
    }

    /// Get country name from country code
    private var countryName: String {
        Locale.current.localizedString(forRegionCode: countryCode) ?? countryCode
    }

    /// Get the active bounds based on custom framing toggle
    private var activeBounds: MLNCoordinateBounds? {
        if useCustomFraming, let viewport = viewportState {
            return viewport.bounds
        }
        return bounds
    }

    /// Get the active heading (rotation) for custom framing, 0 for auto-fit
    private var activeHeading: CLLocationDirection {
        if useCustomFraming, let viewport = viewportState {
            return viewport.heading
        }
        return 0  // North up for auto-fit
    }

    /// Get the active center for custom framing
    private var activeCenter: CLLocationCoordinate2D? {
        if useCustomFraming, let viewport = viewportState {
            return viewport.center
        }
        return nil
    }

    /// Get the active zoom level for custom framing
    private var activeZoomLevel: Double? {
        if useCustomFraming, let viewport = viewportState {
            return viewport.zoomLevel
        }
        return nil
    }

    /// Check if the map is rotated (heading not north-up)
    private var isMapRotated: Bool {
        activeHeading != 0
    }

    /// Get the title text based on selected type
    private var titleText: String {
        switch titleType {
        case .hide:
            return ""
        case .city:
            return cityName.uppercased()
        case .country:
            return countryName.uppercased()
        case .countryAndCity:
            return "\(countryName), \(cityName)".uppercased()
        case .custom:
            return customTitle.uppercased()
        }
    }

    /// Get the subtitle content based on selected type
    private var subtitleContent: ExportSubtitleContent {
        switch subtitleType {
        case .hide:
            return .none
        case .distance:
            return .distance(totalDistance)
        case .dateRange:
            if let range = dateRange {
                return .dateRange(range.earliest, range.latest)
            }
            return .none
        case .workoutCount:
            return .workoutCount(workoutCount)
        case .all:
            // Combine all info into a custom string
            var parts: [String] = []
            parts.append(ExportSubtitleContent.distance(totalDistance).formatted() ?? "")
            if let range = dateRange {
                parts.append(ExportSubtitleContent.dateRange(range.earliest, range.latest).formatted() ?? "")
            }
            parts.append(ExportSubtitleContent.workoutCount(workoutCount).formatted() ?? "")
            return .custom(parts.filter { !$0.isEmpty }.joined(separator: " • "))
        case .custom:
            return .custom(customSubtitle)
        }
    }

    /// Get the route style for export (combines template/custom settings)
    private var exportRouteStyle: RouteStyle {
        RouteStyle(
            lineWidth: exportLineWidth,
            useColorPerWorkoutType: exportUseColorPerWorkoutType,
            useLineWidthPerWorkoutType: exportUseLineWidthPerWorkoutType,
            defaultColor: exportDefaultColor,
            mapLanguage: exportMapLanguage,
            mapStyle: exportMapStyle,
            hideLabels: exportHideLabels,
            workoutTypeColors: exportWorkoutTypeColors,
            workoutTypeLineWidths: exportWorkoutTypeLineWidths
        )
    }

    /// Available workout types from the segments being exported
    private var availableExportWorkoutTypes: [WorkoutType] {
        Array(Set(segments.map { $0.workoutType })).sorted { $0.displayName < $1.displayName }
    }

    /// Get the current filter from customization or template configuration
    private var currentFilter: ExportFilter {
        if isCustomizing {
            return exportFilter
        }
        if let filterRaw = selectedTemplate.configuration.filterRaw,
           let filter = ExportFilter(rawValue: filterRaw) {
            return filter
        }
        return .none
    }

    /// Get the decorator configuration for current settings
    private var decoratorConfig: MapExportDecorator.Configuration {
        if isCustomizing {
            // Use custom settings
            let outerBorder = BorderConfig(
                lineType: outerBorderType,
                color: UIColor(outerBorderColor),
                widthRatio: outerBorderWidthRatio,
                cornerRadiusRatio: outerBorderCornerRadiusRatio
            )
            let innerBorder = BorderConfig(
                lineType: innerBorderType,
                color: UIColor(innerBorderColor),
                widthRatio: innerBorderWidthRatio,
                cornerRadiusRatio: innerBorderCornerRadiusRatio
            )

            // Convert workout type colors to raw format
            var colorsRaw: [String: UIColor] = [:]
            for (type, color) in exportWorkoutTypeColors {
                colorsRaw[type.rawValue] = UIColor(color)
            }

            // Convert workout type line widths to raw format
            var widthsRaw: [String: CGFloat] = [:]
            for (type, width) in exportWorkoutTypeLineWidths {
                widthsRaw[type.rawValue] = width
            }

            // Poster borders (around entire document)
            let posterOuter = BorderConfig(
                lineType: posterOuterBorderType,
                color: UIColor(posterOuterBorderColor),
                widthRatio: posterOuterBorderWidthRatio,
                cornerRadiusRatio: posterOuterBorderCornerRadiusRatio
            )
            let posterInner = BorderConfig(
                lineType: posterInnerBorderType,
                color: UIColor(posterInnerBorderColor),
                widthRatio: posterInnerBorderWidthRatio,
                cornerRadiusRatio: posterInnerBorderCornerRadiusRatio
            )

            var config = selectedTemplate.configuration
            config.withShowTitle(titleType != .hide)
            config.withShowSubtitle(subtitleType != .hide)
            config.withTitleAlignmentRaw(titleAlignment.rawValue)
            config.withSubtitleAlignmentRaw(subtitleAlignment.rawValue)
            config.withTitleFontName(titleFontName)
            config.withSubtitleFontName(subtitleFontName)
            config.withTitleFontRatio(titleFontSizeRatio)
            config.withSubtitleFontRatio(subtitleFontSizeRatio)
            config.withTitleColor(UIColor(titleColor))
            config.withSubtitleColor(UIColor(subtitleColor))
            config.withOuterBorder(outerBorder)
            config.withInnerBorder(innerBorder)
            config.withBorderGapRatio(borderGapRatio)
            config.withBorderGapColor(borderGapColor.map { UIColor($0) })
            config.withPosterOuterBorder(posterOuter)
            config.withPosterInnerBorder(posterInner)
            config.withPosterBorderGapRatio(posterBorderGapRatio)
            config.withPosterBorderGapColor(posterBorderGapColor.map { UIColor($0) })
            config.withPosterMarginRatio(posterMarginRatio)
            config.withTitlePosition(titlePosition)
            config.withPosterDecoration(PosterDecoration(
                type: decorationType,
                position: decorationPosition,
                alignment: decorationAlignment,
                color1: UIColor(decorationColor1),
                color2: UIColor(decorationColor2),
                color3: UIColor(decorationColor3),
                sizeRatio: decorationSizeRatio,
                spacingRatio: decorationSpacingRatio
            ))
            config.withPaddingRatio(paddingRatio)
            config.withBackgroundColor(UIColor(backgroundColor))
            config.withTitleAreaHeightRatio(titleAreaHeightRatio)
            config.withElementSpacingRatio(elementSpacingRatio)
            config.withMapStyleRaw(exportMapStyle.rawValue)
            config.withMapLanguageRaw(exportMapLanguage.rawValue)
            config.withHideLabels(exportHideLabels)
            config.withLineWidth(exportLineWidth)
            config.withDefaultLineColor(UIColor(exportDefaultColor))
            config.withUseColorPerWorkoutType(exportUseColorPerWorkoutType)
            config.withUseLineWidthPerWorkoutType(exportUseLineWidthPerWorkoutType)
            config.withWorkoutTypeColorsRaw(colorsRaw)
            config.withWorkoutTypeLineWidths(widthsRaw)
            return config
        } else {
            // Use template settings directly
            var config = selectedTemplate.configuration
            config.withShowTitle(titleType != .hide)
            config.withShowSubtitle(subtitleType != .hide)
            return config
        }
    }

    /// Apply the current template's settings to all state variables
    private func applyTemplateSettings(_ template: ExportTemplate) {
        let config = template.configuration

        // Apply colors
        titleColor = Color(config.titleColor)
        subtitleColor = Color(config.subtitleColor)

        // Apply font settings
        titleFontName = config.titleFontName
        subtitleFontName = config.subtitleFontName
        titleFontSizeRatio = config.titleFontRatio
        subtitleFontSizeRatio = config.subtitleFontRatio

        // Apply alignment
        if let alignmentRaw = config.titleAlignmentRaw,
           let alignment = ExportTextAlignment(rawValue: alignmentRaw) {
            titleAlignment = alignment
        } else {
            titleAlignment = .center
        }
        if let alignmentRaw = config.subtitleAlignmentRaw,
           let alignment = ExportTextAlignment(rawValue: alignmentRaw) {
            subtitleAlignment = alignment
        } else {
            subtitleAlignment = .center
        }

        // Apply outer border
        outerBorderType = config.outerBorder.lineType
        outerBorderColor = Color(config.outerBorder.color)
        outerBorderWidthRatio = config.outerBorder.widthRatio
        outerBorderCornerRadiusRatio = config.outerBorder.cornerRadiusRatio

        // Apply inner border
        innerBorderType = config.innerBorder.lineType
        innerBorderColor = Color(config.innerBorder.color)
        innerBorderWidthRatio = config.innerBorder.widthRatio
        innerBorderCornerRadiusRatio = config.innerBorder.cornerRadiusRatio

        // Apply spacing
        borderGapRatio = config.borderGapRatio
        borderGapColor = config.borderGapColor.map { Color($0) }
        paddingRatio = config.paddingRatio

        // Apply poster borders
        posterOuterBorderType = config.posterOuterBorder.lineType
        posterOuterBorderColor = Color(config.posterOuterBorder.color)
        posterOuterBorderWidthRatio = config.posterOuterBorder.widthRatio
        posterOuterBorderCornerRadiusRatio = config.posterOuterBorder.cornerRadiusRatio

        posterInnerBorderType = config.posterInnerBorder.lineType
        posterInnerBorderColor = Color(config.posterInnerBorder.color)
        posterInnerBorderWidthRatio = config.posterInnerBorder.widthRatio
        posterInnerBorderCornerRadiusRatio = config.posterInnerBorder.cornerRadiusRatio

        posterBorderGapRatio = config.posterBorderGapRatio
        posterBorderGapColor = config.posterBorderGapColor.map { Color($0) }
        posterMarginRatio = config.posterMarginRatio

        // Apply title position
        titlePosition = config.titlePosition

        // Apply poster decoration settings
        decorationType = config.posterDecoration.type
        decorationPosition = config.posterDecoration.position
        decorationAlignment = config.posterDecoration.alignment
        decorationColor1 = Color(config.posterDecoration.color1)
        decorationColor2 = Color(config.posterDecoration.color2)
        decorationColor3 = Color(config.posterDecoration.color3)
        decorationSizeRatio = config.posterDecoration.sizeRatio
        decorationSpacingRatio = config.posterDecoration.spacingRatio

        // Apply layout settings
        backgroundColor = Color(config.backgroundColor)
        titleAreaHeightRatio = config.titleAreaHeightRatio
        elementSpacingRatio = config.elementSpacingRatio

        // Apply map style settings
        if let mapStyleRaw = config.mapStyleRaw, let mapStyle = MapStyle(rawValue: mapStyleRaw) {
            exportMapStyle = mapStyle
        } else {
            exportMapStyle = routeStyle.mapStyle
        }
        if let mapLanguageRaw = config.mapLanguageRaw, let mapLanguage = MapLanguage(rawValue: mapLanguageRaw) {
            exportMapLanguage = mapLanguage
        } else {
            exportMapLanguage = routeStyle.mapLanguage
        }
        exportHideLabels = config.hideLabels
        if let filterRaw = config.filterRaw, let filter = ExportFilter(rawValue: filterRaw) {
            exportFilter = filter
        } else {
            exportFilter = .none
        }
        exportLineWidth = config.lineWidth
        exportDefaultColor = Color(config.defaultLineColor)
        exportUseColorPerWorkoutType = config.useColorPerWorkoutType
        exportUseLineWidthPerWorkoutType = config.useLineWidthPerWorkoutType

        // Apply workout type colors
        var colors: [WorkoutType: Color] = [:]
        for (key, uiColor) in config.workoutTypeColorsRaw {
            if let workoutType = WorkoutType(rawValue: key) {
                colors[workoutType] = Color(uiColor)
            }
        }
        if !colors.isEmpty {
            exportWorkoutTypeColors = colors
        } else {
            exportWorkoutTypeColors = routeStyle.workoutTypeColors
        }

        // Apply workout type line widths
        var widths: [WorkoutType: CGFloat] = [:]
        for (key, width) in config.workoutTypeLineWidths {
            if let workoutType = WorkoutType(rawValue: key) {
                widths[workoutType] = width
            }
        }
        if !widths.isEmpty {
            exportWorkoutTypeLineWidths = widths
        } else {
            exportWorkoutTypeLineWidths = routeStyle.workoutTypeLineWidths
        }
    }

    /// Select a template, showing confirmation if customizing
    private func selectTemplate(_ template: ExportTemplate) {
        if isCustomizing {
            pendingTemplate = template
            showTemplateResetAlert = true
        } else {
            selectedTemplate = template
            applyTemplateSettings(template)
            generatePreview()
        }
    }

    /// Confirm template change and reset customization
    private func confirmTemplateChange() {
        if let template = pendingTemplate {
            selectedTemplate = template
            applyTemplateSettings(template)
            isCustomizing = false
            pendingTemplate = nil
            generatePreview()
        }
    }

    /// Save current export settings to UserDefaults
    private func saveSettings() {
        // Convert workout type colors to components
        var colorComponents: [String: ExportSettings.ColorComponents] = [:]
        for (type, color) in exportWorkoutTypeColors {
            colorComponents[type.rawValue] = ExportSettings.ColorComponents(from: color)
        }

        // Convert workout type line widths
        var widthsRaw: [String: CGFloat] = [:]
        for (type, width) in exportWorkoutTypeLineWidths {
            widthsRaw[type.rawValue] = width
        }

        let settings = ExportSettings(
            templateRaw: selectedTemplate.rawValue,
            isCustomizing: isCustomizing,
            titleTypeRaw: titleType.rawValue,
            subtitleTypeRaw: subtitleType.rawValue,
            customTitle: customTitle,
            customSubtitle: customSubtitle,
            titleFontName: titleFontName,
            subtitleFontName: subtitleFontName,
            titleColorComponents: ExportSettings.ColorComponents(from: titleColor),
            subtitleColorComponents: ExportSettings.ColorComponents(from: subtitleColor),
            titleFontSizeRatio: titleFontSizeRatio,
            subtitleFontSizeRatio: subtitleFontSizeRatio,
            titleAlignmentRaw: titleAlignment.rawValue,
            subtitleAlignmentRaw: subtitleAlignment.rawValue,
            outerBorderTypeRaw: outerBorderType.rawValue,
            outerBorderColorComponents: ExportSettings.ColorComponents(from: outerBorderColor),
            outerBorderWidthRatio: outerBorderWidthRatio,
            outerBorderCornerRadiusRatio: outerBorderCornerRadiusRatio,
            innerBorderTypeRaw: innerBorderType.rawValue,
            innerBorderColorComponents: ExportSettings.ColorComponents(from: innerBorderColor),
            innerBorderWidthRatio: innerBorderWidthRatio,
            innerBorderCornerRadiusRatio: innerBorderCornerRadiusRatio,
            borderGapRatio: borderGapRatio,
            borderGapColorComponents: borderGapColor.map { ExportSettings.ColorComponents(from: $0) },
            paddingRatio: paddingRatio,
            backgroundColorComponents: ExportSettings.ColorComponents(from: backgroundColor),
            titleAreaHeightRatio: titleAreaHeightRatio,
            elementSpacingRatio: elementSpacingRatio,
            mapStyleRaw: exportMapStyle.rawValue,
            mapLanguageRaw: exportMapLanguage.rawValue,
            hideLabels: exportHideLabels,
            filterRaw: exportFilter == .none ? nil : exportFilter.rawValue,
            lineWidth: exportLineWidth,
            defaultColorComponents: ExportSettings.ColorComponents(from: exportDefaultColor),
            useColorPerWorkoutType: exportUseColorPerWorkoutType,
            useLineWidthPerWorkoutType: exportUseLineWidthPerWorkoutType,
            workoutTypeColors: colorComponents,
            workoutTypeLineWidths: widthsRaw
        )
        settings.save()
    }

    /// Load saved export settings from UserDefaults
    private func loadSavedSettings() {
        guard let settings = ExportSettings.load() else { return }

        // Load template
        selectedTemplate = settings.template
        isCustomizing = settings.isCustomizing

        // Load content settings
        titleType = settings.titleType
        subtitleType = settings.subtitleType
        customTitle = settings.customTitle
        customSubtitle = settings.customSubtitle

        // Load typography
        titleFontName = settings.titleFontName
        subtitleFontName = settings.subtitleFontName
        titleColor = settings.titleColorComponents.color
        subtitleColor = settings.subtitleColorComponents.color
        titleFontSizeRatio = settings.titleFontSizeRatio
        subtitleFontSizeRatio = settings.subtitleFontSizeRatio
        titleAlignment = settings.titleAlignment
        subtitleAlignment = settings.subtitleAlignment

        // Load outer border
        outerBorderType = settings.outerBorderType
        outerBorderColor = settings.outerBorderColorComponents.color
        outerBorderWidthRatio = settings.outerBorderWidthRatio
        outerBorderCornerRadiusRatio = settings.outerBorderCornerRadiusRatio

        // Load inner border
        innerBorderType = settings.innerBorderType
        innerBorderColor = settings.innerBorderColorComponents.color
        innerBorderWidthRatio = settings.innerBorderWidthRatio
        innerBorderCornerRadiusRatio = settings.innerBorderCornerRadiusRatio

        // Load spacing
        borderGapRatio = settings.borderGapRatio
        borderGapColor = settings.borderGapColorComponents?.color
        paddingRatio = settings.paddingRatio

        // Load layout
        backgroundColor = settings.backgroundColor
        if let ratio = settings.titleAreaHeightRatio {
            titleAreaHeightRatio = ratio
        }
        if let ratio = settings.elementSpacingRatio {
            elementSpacingRatio = ratio
        }

        // Load map style
        exportMapStyle = settings.mapStyle
        exportMapLanguage = settings.mapLanguage
        exportHideLabels = settings.hideLabels
        exportFilter = settings.filter

        // Load line settings
        exportLineWidth = settings.lineWidth
        exportDefaultColor = settings.defaultColorComponents.color
        exportUseColorPerWorkoutType = settings.useColorPerWorkoutType
        exportUseLineWidthPerWorkoutType = settings.useLineWidthPerWorkoutType

        // Load workout type colors
        var colors: [WorkoutType: Color] = [:]
        for (key, components) in settings.workoutTypeColors {
            if let type = WorkoutType(rawValue: key) {
                colors[type] = components.color
            }
        }
        if !colors.isEmpty {
            exportWorkoutTypeColors = colors
        }

        // Load workout type line widths
        var widths: [WorkoutType: CGFloat] = [:]
        for (key, width) in settings.workoutTypeLineWidths {
            if let type = WorkoutType(rawValue: key) {
                widths[type] = width
            }
        }
        if !widths.isEmpty {
            exportWorkoutTypeLineWidths = widths
        }
    }

    #if DEBUG
    /// Export current configuration as JSON to console and clipboard
    private func exportConfigAsJSON() {
        let config = decoratorConfig
        if let json = config.toJSON() {
            // Print to console
            print("=== Export Configuration JSON ===")
            print(json)
            print("=================================")

            // Copy to clipboard
            UIPasteboard.general.string = json

            // Show brief feedback (could add a toast/alert here)
            print("Configuration copied to clipboard!")
        } else {
            print("Failed to encode configuration as JSON")
        }
    }
    #endif

    private var borderGapLabel: String {
        if borderGapRatio == 0 {
            return "None"
        } else if borderGapRatio <= 0.008 {
            return "Small"
        } else if borderGapRatio <= 0.016 {
            return "Medium"
        } else {
            return "Large"
        }
    }

    private var paddingLabel: String {
        if paddingRatio <= 0.02 {
            return "Small"
        } else if paddingRatio <= 0.05 {
            return "Medium"
        } else {
            return "Large"
        }
    }

    private var titleAreaHeightLabel: String {
        if titleAreaHeightRatio <= 0.08 {
            return "Compact"
        } else if titleAreaHeightRatio <= 0.12 {
            return "Normal"
        } else if titleAreaHeightRatio <= 0.16 {
            return "Large"
        } else {
            return "Extra Large"
        }
    }

    private var elementSpacingLabel: String {
        if elementSpacingRatio <= 0.01 {
            return "Tight"
        } else if elementSpacingRatio <= 0.02 {
            return "Normal"
        } else {
            return "Loose"
        }
    }

    var body: some View {
        ZStack {
            if embeddedInNavigation {
                exportContent
            } else {
                NavigationStack {
                    exportContent
                }
            }

            // Fullscreen preview overlay with matched geometry effect
            if showFullscreenPreview, let image = previewImage {
                fullscreenOverlay(image: image)
            }
        }
    }

    // MARK: - Fullscreen Overlay

    @State private var fullscreenScale: CGFloat = 1.0
    @State private var fullscreenLastScale: CGFloat = 1.0
    @State private var fullscreenOffset: CGSize = .zero
    @State private var fullscreenLastOffset: CGSize = .zero

    @ViewBuilder
    private func fullscreenOverlay(image: UIImage) -> some View {
        GeometryReader { geometry in
            ZStack {
                // Black background
                Color.black
                    .ignoresSafeArea()

                // Image with matched geometry effect
                VStack(spacing: 0) {
                    Spacer()

                    Image(uiImage: image)
                        .resizable()
                        .aspectRatio(contentMode: .fit)
                        .matchedGeometryEffect(id: "previewImage", in: fullscreenNamespace)
                        .scaleEffect(fullscreenScale)
                        .offset(fullscreenOffset)
                        .gesture(
                            MagnificationGesture()
                                .onChanged { value in
                                    let delta = value / fullscreenLastScale
                                    fullscreenLastScale = value
                                    fullscreenScale = min(max(fullscreenScale * delta, 1), 5)
                                }
                                .onEnded { _ in
                                    fullscreenLastScale = 1.0
                                    if fullscreenScale <= 1 {
                                        withAnimation(.spring(response: 0.3)) {
                                            fullscreenOffset = .zero
                                        }
                                    }
                                }
                        )
                        .simultaneousGesture(
                            DragGesture()
                                .onChanged { value in
                                    if fullscreenScale > 1 {
                                        fullscreenOffset = CGSize(
                                            width: fullscreenLastOffset.width + value.translation.width,
                                            height: fullscreenLastOffset.height + value.translation.height
                                        )
                                    }
                                }
                                .onEnded { _ in
                                    fullscreenLastOffset = fullscreenOffset
                                }
                        )
                        .onTapGesture(count: 2) {
                            withAnimation(.spring(response: 0.3)) {
                                if fullscreenScale > 1 {
                                    fullscreenScale = 1
                                    fullscreenOffset = .zero
                                    fullscreenLastOffset = .zero
                                } else {
                                    fullscreenScale = 2.5
                                }
                            }
                        }
                        .padding(.horizontal, DS.Spacing.lg)

                    Spacer()

                    // Template picker at the bottom
                    ScrollView(.horizontal, showsIndicators: false) {
                        HStack(spacing: DS.Spacing.md) {
                            ForEach(ExportTemplate.orderedCases) { template in
                                TemplateButton(
                                    template: template,
                                    isSelected: selectedTemplate == template,
                                    action: { selectTemplate(template) }
                                )
                            }
                        }
                        .padding(.horizontal, DS.Spacing.lg)
                    }
                    .padding(.vertical, DS.Spacing.md)
                    .background(Color.black.opacity(0.8))
                }

                // Close button
                VStack {
                    HStack {
                        Spacer()
                        Button {
                            // Reset zoom state before closing
                            fullscreenScale = 1.0
                            fullscreenLastScale = 1.0
                            fullscreenOffset = .zero
                            fullscreenLastOffset = .zero
                            withAnimation(.spring(response: 0.4, dampingFraction: 0.85)) {
                                showFullscreenPreview = false
                            }
                        } label: {
                            Image(systemName: "xmark")
                                .font(.system(size: 16, weight: .semibold))
                                .foregroundStyle(.white)
                                .frame(width: 36, height: 36)
                                .background(Color.white.opacity(0.2))
                                .clipShape(Circle())
                        }
                        .padding()
                    }
                    Spacer()
                }
            }
        }
    }

    @ViewBuilder
    private var exportContent: some View {
        VStack(spacing: 0) {
            // Preview area
            ZStack {
                RoundedRectangle(cornerRadius: 12)
                    .fill(DS.Colors.secondaryBackground)

                if isGeneratingPreview {
                    VStack(spacing: DS.Spacing.md) {
                        ProgressView()
                        Text("Generating preview...")
                            .font(.caption)
                            .foregroundStyle(DS.Colors.secondaryText)
                    }
                } else if let image = previewImage {
                    VStack(spacing: 0) { // DS.Spacing.xs
                        if !showFullscreenPreview {
                            Image(uiImage: image)
                                .resizable()
                                .aspectRatio(contentMode: .fit)
                                .clipShape(RoundedRectangle(cornerRadius: 8))
                                .matchedGeometryEffect(id: "previewImage", in: fullscreenNamespace)
                                .onTapGesture {
                                    // Fullscreen preview is a premium feature
                                    if subscriptionManager.isPremium {
                                        withAnimation(.spring(response: 0.4, dampingFraction: 0.85)) {
                                            showFullscreenPreview = true
                                        }
                                    } else {
                                        showPaywall = true
                                    }
                                }
                                .overlay(alignment: .bottomTrailing) {
                                    HStack(spacing: 4) {
                                        if !subscriptionManager.isPremium {
                                            Image(systemName: "lock.fill")
                                                .font(.system(size: 10))
                                        }
                                        Image(systemName: "arrow.up.left.and.arrow.down.right")
                                            .font(.system(size: 12, weight: .semibold))
                                    }
                                    .foregroundStyle(.white)
                                    .padding(6)
                                    .background(Color.black.opacity(0.5))
                                    .clipShape(Capsule())
                                    .padding(DS.Spacing.md)
                                }
                        }

                        if isUsingSimplifiedPreview && !showFullscreenPreview {
                            Text("Huge map. Simplified preview.")
                                .font(.caption)
                                .foregroundStyle(DS.Colors.secondaryText)
                        }
                    }
                } else {
                    VStack(spacing: DS.Spacing.md) {
                        Image(systemName: "map")
                            .font(.system(size: 40))
                            .foregroundStyle(DS.Colors.secondaryText)
                        Text("No preview available")
                            .font(.caption)
                            .foregroundStyle(DS.Colors.secondaryText)
                    }
                }
            }
            .frame(maxWidth: .infinity)
            .frame(maxHeight: .infinity)
            .padding(.horizontal, DS.Spacing.lg)
            .padding(.top, DS.Spacing.md)

                // Template picker
                ScrollView(.horizontal, showsIndicators: false) {
                    HStack(spacing: DS.Spacing.md) {
                        ForEach(ExportTemplate.orderedCases) { template in
                            TemplateButton(
                                template: template,
                                isSelected: selectedTemplate == template,
                                action: { selectTemplate(template) }
                            )
                        }
                    }
                    .padding(.horizontal, DS.Spacing.lg)
                }
                .padding(.vertical, DS.Spacing.sm)

                // Settings
                List {
                    // Content settings (title/subtitle selection always visible)
                    Section {
                        Picker("Title", selection: $titleType) {
                            ForEach(ExportTitleType.allCases) { type in
                                Text(type.rawValue).tag(type)
                            }
                        }

                        if titleType == .custom {
                            TextField("Enter title", text: $customTitle)
                                .textInputAutocapitalization(.words)
                        }

                        Picker("Subtitle", selection: $subtitleType) {
                            ForEach(ExportSubtitleType.allCases) { type in
                                Text(type.rawValue).tag(type)
                            }
                        }

                        if subtitleType == .custom {
                            TextField("Enter subtitle", text: $customSubtitle)
                        }
                    } header: {
                        Text("Content")
                    }

                    // Map Area section (custom framing)
                    Section {
                        Toggle(isOn: $useCustomFraming) {
                            HStack {
                                Image(systemName: "crop")
                                    .font(.system(size: 18))
                                    .foregroundStyle(DS.Colors.accent)
                                    .frame(width: 24)

                                VStack(alignment: .leading, spacing: 2) {
                                    Text("Custom Framing")
                                        .foregroundStyle(DS.Colors.primaryText)
                                    Text("Export your chosen map position")
                                        .font(.caption)
                                        .foregroundStyle(DS.Colors.secondaryText)
                                }
                            }
                        }
                        .tint(DS.Colors.accent)
                        .disabled(viewportState == nil)
                    } header: {
                        Text("Map Area")
                    } footer: {
                        if viewportState == nil {
                            Text("Navigate on the map first to enable this option")
                        } else {
                            Text(useCustomFraming ? "Exports your chosen map position" : "Automatically fits all routes")
                        }
                    }

                    // Customize button
                    Section {
                        Button {
                            withAnimation(.easeInOut(duration: 0.2)) {
                                if !isCustomizing {
                                    // Load current template settings when starting customization
                                    applyTemplateSettings(selectedTemplate)
                                }
                                isCustomizing.toggle()
                            }
                        } label: {
                            HStack {
                                Image(systemName: isCustomizing ? "paintbrush.fill" : "paintbrush")
                                    .foregroundStyle(DS.Colors.accent)
                                Text(isCustomizing ? "Hide Customization" : "Customize Style")
                                    .foregroundStyle(DS.Colors.primaryText)
                                Spacer()
                                Image(systemName: isCustomizing ? "chevron.up" : "chevron.down")
                                    .font(.system(size: 12, weight: .semibold))
                                    .foregroundStyle(DS.Colors.secondaryText)
                            }
                        }
                    }

                    // Customization settings (only visible when customizing)
                    if isCustomizing {
                        Section {
                            NavigationLink {
                                TitleSettingsView(
                                    titleType: $titleType,
                                    customTitle: $customTitle,
                                    titleFontName: $titleFontName,
                                    titleColor: $titleColor,
                                    titleFontSizeRatio: $titleFontSizeRatio,
                                    previewText: titleText.isEmpty ? "TITLE" : titleText
                                )
                            } label: {
                                HStack {
                                    Text("Title Style")
                                    Spacer()
                                    Text(titleFontName?.components(separatedBy: "-").first ?? "System")
                                        .foregroundStyle(DS.Colors.secondaryText)
                                }
                            }

                            NavigationLink {
                                SubtitleSettingsView(
                                    subtitleType: $subtitleType,
                                    customSubtitle: $customSubtitle,
                                    subtitleFontName: $subtitleFontName,
                                    subtitleColor: $subtitleColor,
                                    subtitleFontSizeRatio: $subtitleFontSizeRatio,
                                    previewText: subtitleContent.formatted() ?? "Subtitle"
                                )
                            } label: {
                                HStack {
                                    Text("Subtitle Style")
                                    Spacer()
                                    Text(subtitleFontName?.components(separatedBy: "-").first ?? "System")
                                        .foregroundStyle(DS.Colors.secondaryText)
                                }
                            }

                            // Title alignment picker
                            HStack {
                                Text("Title Alignment")
                                Spacer()
                                Picker("", selection: $titleAlignment) {
                                    ForEach(ExportTextAlignment.allCases) { alignment in
                                        Image(systemName: alignment.icon)
                                            .tag(alignment)
                                    }
                                }
                                .pickerStyle(.segmented)
                                .frame(width: 120)
                            }

                            // Subtitle alignment picker
                            HStack {
                                Text("Subtitle Alignment")
                                Spacer()
                                Picker("", selection: $subtitleAlignment) {
                                    ForEach(ExportTextAlignment.allCases) { alignment in
                                        Image(systemName: alignment.icon)
                                            .tag(alignment)
                                    }
                                }
                                .pickerStyle(.segmented)
                                .frame(width: 120)
                            }
                        } header: {
                            Text("Typography")
                        }

                        Section {
                            NavigationLink {
                                BorderSettingsView(
                                    borderType: $outerBorderType,
                                    borderColor: $outerBorderColor,
                                    borderWidthRatio: $outerBorderWidthRatio,
                                    borderCornerRadiusRatio: $outerBorderCornerRadiusRatio,
                                    title: "Outer Border"
                                )
                            } label: {
                                HStack {
                                    Text("Outer Border")
                                    Spacer()
                                    Text(outerBorderType.rawValue)
                                        .foregroundStyle(DS.Colors.secondaryText)
                                }
                            }

                            NavigationLink {
                                BorderSettingsView(
                                    borderType: $innerBorderType,
                                    borderColor: $innerBorderColor,
                                    borderWidthRatio: $innerBorderWidthRatio,
                                    borderCornerRadiusRatio: $innerBorderCornerRadiusRatio,
                                    title: "Inner Border"
                                )
                            } label: {
                                HStack {
                                    Text("Inner Border")
                                    Spacer()
                                    Text(innerBorderType.rawValue)
                                        .foregroundStyle(DS.Colors.secondaryText)
                                }
                            }

                            VStack(alignment: .leading, spacing: DS.Spacing.sm) {
                                HStack {
                                    Text("Border Gap")
                                    Spacer()
                                    Text(borderGapLabel)
                                        .foregroundStyle(DS.Colors.secondaryText)
                                }
                                Slider(value: $borderGapRatio, in: 0...0.03, step: 0.002)
                                    .tint(DS.Colors.accent)
                            }

                            // Gap fill color (only shown when both borders are active and gap > 0)
                            if outerBorderType != .none && innerBorderType != .none && borderGapRatio > 0 {
                                NavigationLink {
                                    BorderGapColorSettingsView(
                                        gapColor: $borderGapColor,
                                        presetColors: DS.Palette.all
                                    )
                                } label: {
                                    HStack {
                                        Text("Gap Fill Color")
                                        Spacer()
                                        if let color = borderGapColor {
                                            Circle()
                                                .fill(color)
                                                .frame(width: 20, height: 20)
                                        } else {
                                            Text("Background")
                                                .foregroundStyle(DS.Colors.secondaryText)
                                        }
                                    }
                                }
                            }
                        } header: {
                            Text("Map Border")
                        }

                        Section {
                            // Poster margin (space from document edge to poster border)
                            VStack(alignment: .leading, spacing: DS.Spacing.sm) {
                                HStack {
                                    Text("Margin")
                                    Spacer()
                                    Text(posterMarginRatio == 0 ? "None" : posterMarginRatio <= 0.02 ? "Small" : posterMarginRatio <= 0.04 ? "Medium" : "Large")
                                        .foregroundStyle(DS.Colors.secondaryText)
                                }
                                Slider(value: $posterMarginRatio, in: 0...0.06, step: 0.005)
                                    .tint(DS.Colors.accent)
                            }

                            NavigationLink {
                                BorderSettingsView(
                                    borderType: $posterOuterBorderType,
                                    borderColor: $posterOuterBorderColor,
                                    borderWidthRatio: $posterOuterBorderWidthRatio,
                                    borderCornerRadiusRatio: $posterOuterBorderCornerRadiusRatio,
                                    title: "Poster Outer Border"
                                )
                            } label: {
                                HStack {
                                    Text("Outer Border")
                                    Spacer()
                                    Text(posterOuterBorderType.rawValue)
                                        .foregroundStyle(DS.Colors.secondaryText)
                                }
                            }

                            NavigationLink {
                                BorderSettingsView(
                                    borderType: $posterInnerBorderType,
                                    borderColor: $posterInnerBorderColor,
                                    borderWidthRatio: $posterInnerBorderWidthRatio,
                                    borderCornerRadiusRatio: $posterInnerBorderCornerRadiusRatio,
                                    title: "Poster Inner Border"
                                )
                            } label: {
                                HStack {
                                    Text("Inner Border")
                                    Spacer()
                                    Text(posterInnerBorderType.rawValue)
                                        .foregroundStyle(DS.Colors.secondaryText)
                                }
                            }

                            // Poster border gap (only shown when both poster borders are active)
                            if posterOuterBorderType != .none && posterInnerBorderType != .none {
                                VStack(alignment: .leading, spacing: DS.Spacing.sm) {
                                    HStack {
                                        Text("Border Gap")
                                        Spacer()
                                        Text(posterBorderGapRatio == 0 ? "None" : posterBorderGapRatio <= 0.008 ? "Small" : posterBorderGapRatio <= 0.016 ? "Medium" : "Large")
                                            .foregroundStyle(DS.Colors.secondaryText)
                                    }
                                    Slider(value: $posterBorderGapRatio, in: 0...0.03, step: 0.002)
                                        .tint(DS.Colors.accent)
                                }

                                if posterBorderGapRatio > 0 {
                                    NavigationLink {
                                        BorderGapColorSettingsView(
                                            gapColor: $posterBorderGapColor,
                                            presetColors: DS.Palette.all
                                        )
                                    } label: {
                                        HStack {
                                            Text("Gap Fill Color")
                                            Spacer()
                                            if let color = posterBorderGapColor {
                                                Circle()
                                                    .fill(color)
                                                    .frame(width: 20, height: 20)
                                            } else {
                                                Text("Background")
                                                    .foregroundStyle(DS.Colors.secondaryText)
                                            }
                                        }
                                    }
                                }
                            }
                        } header: {
                            Text("Poster Border")
                        } footer: {
                            Text("Border around the entire poster, outside the content")
                        }

                        Section {
                            VStack(alignment: .leading, spacing: DS.Spacing.sm) {
                                HStack {
                                    Text("Padding")
                                    Spacer()
                                    Text(paddingLabel)
                                        .foregroundStyle(DS.Colors.secondaryText)
                                }
                                Slider(value: $paddingRatio, in: 0.01...0.1, step: 0.005)
                                    .tint(DS.Colors.accent)
                            }
                        } header: {
                            Text("Spacing")
                        }

                        Section {
                            // Background color picker
                            NavigationLink {
                                BackgroundColorSettingsView(
                                    backgroundColor: $backgroundColor,
                                    presetColors: DS.Palette.forBackgrounds
                                )
                            } label: {
                                HStack {
                                    Text("Background Color")
                                    Spacer()
                                    Circle()
                                        .fill(backgroundColor)
                                        .frame(width: 20, height: 20)
                                        .overlay(
                                            Circle()
                                                .stroke(DS.Colors.secondaryText.opacity(0.3), lineWidth: 1)
                                        )
                                }
                            }

                            // Title position picker
                            HStack {
                                Text("Title Position")
                                Spacer()
                                Picker("", selection: $titlePosition) {
                                    ForEach(TitlePosition.allCases) { position in
                                        Text(position.displayName)
                                            .tag(position)
                                    }
                                }
                                .pickerStyle(.segmented)
                                .frame(width: 140)
                            }

                            VStack(alignment: .leading, spacing: DS.Spacing.sm) {
                                HStack {
                                    Text("Title Area Height")
                                    Spacer()
                                    Text(titleAreaHeightLabel)
                                        .foregroundStyle(DS.Colors.secondaryText)
                                }
                                Slider(value: $titleAreaHeightRatio, in: 0.06...0.20, step: 0.01)
                                    .tint(DS.Colors.accent)
                            }

                            VStack(alignment: .leading, spacing: DS.Spacing.sm) {
                                HStack {
                                    Text("Title Spacing")
                                    Spacer()
                                    Text(elementSpacingLabel)
                                        .foregroundStyle(DS.Colors.secondaryText)
                                }
                                Slider(value: $elementSpacingRatio, in: 0.005...0.04, step: 0.005)
                                    .tint(DS.Colors.accent)
                            }
                        } header: {
                            Text("Layout")
                        }

                        // Poster Decoration Section
                        Section {
                            // Decoration type picker
                            HStack {
                                Text("Decoration")
                                Spacer()
                                Picker("", selection: $decorationType) {
                                    ForEach(DecorationType.allCases) { type in
                                        Text(type.displayName)
                                            .tag(type)
                                    }
                                }
                                .pickerStyle(.menu)
                            }

                            if decorationType != .none {
                                // Position picker
                                HStack {
                                    Text("Position")
                                    Spacer()
                                    Picker("", selection: $decorationPosition) {
                                        ForEach(DecorationPosition.allCases) { position in
                                            Text(position.displayName)
                                                .tag(position)
                                        }
                                    }
                                    .pickerStyle(.segmented)
                                    .frame(width: 140)
                                }

                                // Alignment picker
                                HStack {
                                    Text("Alignment")
                                    Spacer()
                                    Picker("", selection: $decorationAlignment) {
                                        ForEach(DecorationAlignment.allCases) { alignment in
                                            Text(alignment.displayName)
                                                .tag(alignment)
                                        }
                                    }
                                    .pickerStyle(.segmented)
                                    .frame(width: 160)
                                }

                                // Color 1 (always shown for non-none types)
                                ColorPicker("Color 1", selection: $decorationColor1)

                                // Colors 2 and 3 (only for types that use them)
                                if decorationType.colorCount >= 2 {
                                    ColorPicker("Color 2", selection: $decorationColor2)
                                }
                                if decorationType.colorCount >= 3 {
                                    ColorPicker("Color 3", selection: $decorationColor3)
                                }

                                // Size slider
                                VStack(alignment: .leading, spacing: DS.Spacing.sm) {
                                    HStack {
                                        Text("Size")
                                        Spacer()
                                        Text(decorationSizeRatio <= 0.05 ? "Small" : decorationSizeRatio <= 0.08 ? "Medium" : "Large")
                                            .foregroundStyle(DS.Colors.secondaryText)
                                    }
                                    Slider(value: $decorationSizeRatio, in: 0.04...0.12, step: 0.01)
                                        .tint(DS.Colors.accent)
                                }
                            }
                        } header: {
                            Text("Poster Decoration")
                        }

                        Section {
                            NavigationLink {
                                ExportMapStylePickerView(selectedStyle: $exportMapStyle)
                            } label: {
                                HStack {
                                    Image(systemName: exportMapStyle.icon)
                                        .font(.system(size: 18))
                                        .foregroundStyle(DS.Colors.accent)
                                        .frame(width: 24)

                                    Text("Map Style")
                                        .foregroundStyle(DS.Colors.primaryText)

                                    Spacer()

                                    Text(exportMapStyle.displayName)
                                        .foregroundStyle(DS.Colors.secondaryText)
                                }
                            }

                            NavigationLink {
                                ExportLanguagePickerView(selectedLanguage: $exportMapLanguage)
                            } label: {
                                HStack {
                                    Image(systemName: "globe")
                                        .font(.system(size: 18))
                                        .foregroundStyle(DS.Colors.accent)
                                        .frame(width: 24)

                                    Text("Map Language")
                                        .foregroundStyle(DS.Colors.primaryText)

                                    Spacer()

                                    Text("\(exportMapLanguage.flag) \(exportMapLanguage.displayName)")
                                        .foregroundStyle(DS.Colors.secondaryText)
                                }
                            }

                            Toggle(isOn: $exportHideLabels) {
                                HStack {
                                    Image(systemName: "textformat.alt")
                                        .font(.system(size: 18))
                                        .foregroundStyle(DS.Colors.accent)
                                        .frame(width: 24)

                                    Text("Hide Labels")
                                        .foregroundStyle(DS.Colors.primaryText)
                                }
                            }
                            .tint(DS.Colors.accent)

                            // Filter picker
                            HStack {
                                Image(systemName: "camera.filters")
                                    .font(.system(size: 18))
                                    .foregroundStyle(DS.Colors.accent)
                                    .frame(width: 24)

                                Picker("Filter", selection: $exportFilter) {
                                    ForEach(ExportFilter.allCases) { filter in
                                        Label(filter.rawValue, systemImage: filter.icon)
                                            .tag(filter)
                                    }
                                }
                                .pickerStyle(.menu)
                            }
                        } header: {
                            Text("Map Style")
                        }

                        Section {
                            VStack(alignment: .leading, spacing: DS.Spacing.sm) {
                                HStack {
                                    Text("Default Width")
                                    Spacer()
                                    Text(String(format: "%.1f pt", exportLineWidth))
                                        .foregroundStyle(DS.Colors.secondaryText)
                                }
                                Slider(value: $exportLineWidth, in: 1...10, step: 0.5)
                                    .tint(DS.Colors.accent)

                                HStack {
                                    Spacer()
                                    RoundedRectangle(cornerRadius: exportLineWidth / 2)
                                        .fill(exportDefaultColor)
                                        .frame(width: 100, height: exportLineWidth)
                                    Spacer()
                                }
                                .padding(.top, DS.Spacing.sm)
                            }

                            Toggle("Width per Activity Type", isOn: $exportUseLineWidthPerWorkoutType)
                                .tint(DS.Colors.accent)

                            if exportUseLineWidthPerWorkoutType {
                                ForEach(availableExportWorkoutTypes, id: \.self) { workoutType in
                                    ExportWorkoutTypeLineWidthRow(
                                        workoutType: workoutType,
                                        lineWidth: Binding(
                                            get: { exportWorkoutTypeLineWidths[workoutType] ?? 3.0 },
                                            set: { exportWorkoutTypeLineWidths[workoutType] = $0 }
                                        ),
                                        color: exportUseColorPerWorkoutType
                                            ? (exportWorkoutTypeColors[workoutType] ?? .gray)
                                            : exportDefaultColor
                                    )
                                }
                            }
                        } header: {
                            Text("Stroke Width")
                        }

                        Section {
                            ExportColorPickerGrid(
                                selectedColor: $exportDefaultColor,
                                colors: DS.Palette.forRoutes
                            )
                        } header: {
                            Text("Default Color")
                        } footer: {
                            Text("Used when 'Color per Activity' is off")
                        }

                        Section {
                            Toggle("Color per Activity Type", isOn: $exportUseColorPerWorkoutType)
                                .tint(DS.Colors.accent)

                            if exportUseColorPerWorkoutType {
                                ForEach(availableExportWorkoutTypes, id: \.self) { workoutType in
                                    ExportWorkoutTypeColorRow(
                                        workoutType: workoutType,
                                        color: Binding(
                                            get: { exportWorkoutTypeColors[workoutType] ?? .gray },
                                            set: { exportWorkoutTypeColors[workoutType] = $0 }
                                        ),
                                        presetColors: DS.Palette.forRoutes
                                    )
                                }
                            }
                        } header: {
                            Text("Activity Colors")
                        }
                    }
                }
                .listStyle(.insetGrouped)
                .scrollContentBackground(.hidden)
                // Single onChange using combined hash of all preview-affecting state
                // to avoid stack overflow from 55+ nested view modifiers on device
                .onChange(of: previewTriggerHash) { _, _ in
                    generatePreview()
                }

                // Export button - Premium feature
                Button {
                    if subscriptionManager.isPremium {
                        showExportSizeSheet = true
                    } else {
                        showPaywall = true
                    }
                } label: {
                    HStack {
                        if isExporting {
                            ProgressView()
                                .tint(.white)
                        } else {
                            Image(systemName: "square.and.arrow.up")
                        }
                        Text(isExporting ? "Exporting..." : "Export")
                    }
                    .font(.headline)
                    .foregroundStyle(.white)
                    .frame(maxWidth: .infinity)
                    .frame(height: 50)
                    .background(isExporting ? DS.Colors.accent.opacity(0.6) : DS.Colors.accent)
                    .clipShape(RoundedRectangle(cornerRadius: 12))
                }
                .disabled(isExporting || previewImage == nil)
                .padding(.horizontal, DS.Spacing.lg)
                .padding(.bottom, DS.Spacing.lg)
        }
        .background(DS.Colors.viewBackground)
        .toolbar {
            // Only show Cancel button when presented as a sheet (not embedded in navigation)
            if !embeddedInNavigation {
                ToolbarItem(placement: .topBarLeading) {
                    Button("Cancel") {
                        snapshotter?.cancel()
                        dismiss()
                    }
                }
            }

            #if DEBUG
            ToolbarItem(placement: .topBarTrailing) {
                Button {
                    exportConfigAsJSON()
                } label: {
                    Image(systemName: "doc.on.clipboard")
                }
            }
            #endif
        }
        .onAppear {
            // Load saved settings first, then apply template settings if not customizing
            loadSavedSettings()
            if !isCustomizing {
                applyTemplateSettings(selectedTemplate)
            }
            generatePreview()
        }
        .onDisappear {
            // Clean up snapshotter when leaving the view
            snapshotter?.cancel()
        }
        .sheet(isPresented: $showShareSheet) {
            if let image = exportedImage {
                ShareSheet(items: [image])
            }
        }
        .sheet(isPresented: $showPaywall) {
            PaywallSheet()
        }
        .alert("Reset Customization?", isPresented: $showTemplateResetAlert) {
            Button("Cancel", role: .cancel) {
                pendingTemplate = nil
            }
            Button("Reset", role: .destructive) {
                confirmTemplateChange()
            }
        } message: {
            Text("Changing the template will reset all your custom style settings.")
        }
        .sheet(isPresented: $showExportSizeSheet) {
            ExportSizeSelectionSheet(
                segmentCount: segments.count,
                onExport: { size in
                    showExportSizeSheet = false
                    exportImage(size: size)
                }
            )
            .presentationDetents([.medium])
            .presentationDragIndicator(.visible)
        }
    }

    private func generatePreview() {
        guard let boundsToUse = activeBounds else { return }

        // Cancel any existing snapshotter to avoid concurrent style loads that can crash
        snapshotter?.cancel()
        snapshotter = nil

        // Always use exportRouteStyle - it contains template's map style settings
        // (applyTemplateSettings populates exportMapStyle from config.mapStyleRaw)
        let activeRouteStyle = exportRouteStyle

        isGeneratingPreview = true

        guard let styleURL = activeRouteStyle.mapStyle.styleURL(apiKey: stadiaAPIKey) else {
            isGeneratingPreview = false
            return
        }

        // Make snapshot taller to include attribution area
        let expandedSize = CGSize(width: previewSize.width, height: previewSize.height * (1 + attributionCropFraction))

        let options: MLNMapSnapshotOptions

        // Handle rotated maps differently - use camera with zoom level instead of bounds
        if isMapRotated, let center = activeCenter, let zoomLevel = activeZoomLevel {
            // For rotated maps, use camera-based snapshotting with the original center, zoom, and heading
            let camera = MLNMapCamera(lookingAtCenter: center, altitude: 0, pitch: 0, heading: activeHeading)
            options = MLNMapSnapshotOptions(styleURL: styleURL, camera: camera, size: expandedSize)
            options.zoomLevel = zoomLevel
        } else {
            // For non-rotated maps, use bounds-based snapshotting
            let paddedBounds: MLNCoordinateBounds
            if useCustomFraming {
                paddedBounds = adjustBoundsForCustomFraming(boundsToUse, targetSize: previewSize)
            } else {
                paddedBounds = padBoundsForSnapshot(boundsToUse, targetSize: previewSize)
            }

            // Expand bounds at the bottom to capture attribution area for cropping
            let expandedBounds = expandBoundsForCropping(
                paddedBounds,
                targetAspectRatio: previewSize.height / previewSize.width
            )

            let center = CLLocationCoordinate2D(
                latitude: (expandedBounds.sw.latitude + expandedBounds.ne.latitude) / 2,
                longitude: (expandedBounds.sw.longitude + expandedBounds.ne.longitude) / 2
            )

            let camera = MLNMapCamera(lookingAtCenter: center, altitude: 0, pitch: 0, heading: 0)
            options = MLNMapSnapshotOptions(styleURL: styleURL, camera: camera, size: expandedSize)
            options.coordinateBounds = expandedBounds
        }
        options.showsLogo = false

        let newSnapshotter = MLNMapSnapshotter(options: options)
        snapshotter = newSnapshotter

        newSnapshotter.start { snapshot, error in
            // Capture snapshot as nonisolated(unsafe) to cross isolation boundary safely
            // This is safe because we immediately dispatch to main thread
            nonisolated(unsafe) let capturedSnapshot = snapshot
            DispatchQueue.main.async {
                if let snapshot = capturedSnapshot {
                    let strokeScaleFactor = strokeScale(for: previewSize)
                    // Use simplified segments for preview if available
                    // Filter is applied to map before routes are drawn, so routes keep their original color
                    let fullImage = drawRoutesOnSnapshot(
                        snapshot,
                        size: expandedSize,
                        style: activeRouteStyle,
                        using: segmentsForPreview,
                        strokeScale: strokeScaleFactor,
                        filter: currentFilter
                    )
                    let croppedMap = cropAttribution(from: fullImage, targetSize: previewSize)

                    // Apply decoration
                    let decorator = MapExportDecorator(configuration: decoratorConfig)
                    let decoratedImage = decorator.decorate(
                        mapImage: croppedMap,
                        title: titleText,
                        subtitle: subtitleContent,
                        targetSize: previewSize
                    )

                    previewImage = decoratedImage
                }
                isGeneratingPreview = false
            }
        }
    }

    // MARK: - Tiled Export Support

    /// Represents the 4 tile bounds for a 2x2 grid export
    private struct TileBounds {
        let topLeft: MLNCoordinateBounds
        let topRight: MLNCoordinateBounds
        let bottomLeft: MLNCoordinateBounds
        let bottomRight: MLNCoordinateBounds
    }

    /// Converts latitude to Web Mercator Y value
    private func latitudeToMercatorY(_ latitude: Double) -> Double {
        let latRad = latitude * .pi / 180.0
        return log(tan(.pi / 4 + latRad / 2))
    }

    /// Converts longitude to Web Mercator X value
    private func longitudeToMercatorX(_ longitude: Double) -> Double {
        longitude * .pi / 180.0
    }

    /// Converts Web Mercator X value back to longitude
    private func mercatorXToLongitude(_ x: Double) -> Double {
        x * 180.0 / .pi
    }

    /// Converts Web Mercator Y value back to latitude
    private func mercatorYToLatitude(_ y: Double) -> Double {
        return (2 * atan(exp(y)) - .pi / 2) * 180.0 / .pi
    }

    /// Splits coordinate bounds into a 2x2 grid for tiled rendering
    /// Uses Mercator projection to ensure tiles are equal pixel sizes
    private func splitBoundsIntoTiles(_ bounds: MLNCoordinateBounds) -> TileBounds {
        // Longitude is linear in Mercator, so simple midpoint works
        let midLon = (bounds.sw.longitude + bounds.ne.longitude) / 2

        // Latitude requires Mercator projection for correct pixel-space midpoint
        let mercatorYSW = latitudeToMercatorY(bounds.sw.latitude)
        let mercatorYNE = latitudeToMercatorY(bounds.ne.latitude)
        let mercatorYMid = (mercatorYSW + mercatorYNE) / 2
        let midLat = mercatorYToLatitude(mercatorYMid)

        return TileBounds(
            topLeft: MLNCoordinateBounds(
                sw: CLLocationCoordinate2D(latitude: midLat, longitude: bounds.sw.longitude),
                ne: CLLocationCoordinate2D(latitude: bounds.ne.latitude, longitude: midLon)
            ),
            topRight: MLNCoordinateBounds(
                sw: CLLocationCoordinate2D(latitude: midLat, longitude: midLon),
                ne: bounds.ne
            ),
            bottomLeft: MLNCoordinateBounds(
                sw: bounds.sw,
                ne: CLLocationCoordinate2D(latitude: midLat, longitude: midLon)
            ),
            bottomRight: MLNCoordinateBounds(
                sw: CLLocationCoordinate2D(latitude: bounds.sw.latitude, longitude: midLon),
                ne: CLLocationCoordinate2D(latitude: midLat, longitude: bounds.ne.longitude)
            )
        )
    }

    /// Expands bounds so their Mercator aspect ratio matches the snapshot size.
    /// Prevents MapLibre from adding implicit padding when fitting bounds.
    private func adjustBoundsToAspectRatio(_ bounds: MLNCoordinateBounds, targetSize: CGSize) -> MLNCoordinateBounds {
        guard targetSize.width > 0, targetSize.height > 0 else { return bounds }

        let targetAspect = Double(targetSize.width / targetSize.height)

        let mercXMin = longitudeToMercatorX(bounds.sw.longitude)
        let mercXMax = longitudeToMercatorX(bounds.ne.longitude)
        let mercYMin = latitudeToMercatorY(bounds.sw.latitude)
        let mercYMax = latitudeToMercatorY(bounds.ne.latitude)

        let mercWidth = mercXMax - mercXMin
        let mercHeight = mercYMax - mercYMin
        let currentAspect = mercWidth / mercHeight

        var adjustedXMin = mercXMin
        var adjustedXMax = mercXMax
        var adjustedYMin = mercYMin
        var adjustedYMax = mercYMax

        if currentAspect < targetAspect {
            // Bounds are too tall—widen longitude span to match the snapshot aspect
            let targetWidth = mercHeight * targetAspect
            let extraWidth = targetWidth - mercWidth
            adjustedXMin -= extraWidth / 2
            adjustedXMax += extraWidth / 2
        } else if currentAspect > targetAspect {
            // Bounds are too wide—increase latitude span
            let targetHeight = mercWidth / targetAspect
            let extraHeight = targetHeight - mercHeight
            adjustedYMin -= extraHeight / 2
            adjustedYMax += extraHeight / 2
        }

        return MLNCoordinateBounds(
            sw: CLLocationCoordinate2D(
                latitude: mercatorYToLatitude(adjustedYMin),
                longitude: mercatorXToLongitude(adjustedXMin)
            ),
            ne: CLLocationCoordinate2D(
                latitude: mercatorYToLatitude(adjustedYMax),
                longitude: mercatorXToLongitude(adjustedXMax)
            )
        )
    }

    /// Renders a single tile of the map with routes drawn on it
    private func renderTile(
        bounds: MLNCoordinateBounds,
        size: CGSize,
        styleURL: URL,
        activeRouteStyle: RouteStyle,
        segments: [StyledSegment],
        strokeScale: CGFloat,
        filter: ExportFilter = .none
    ) async -> UIImage {
        await withCheckedContinuation { continuation in
            let center = CLLocationCoordinate2D(
                latitude: (bounds.sw.latitude + bounds.ne.latitude) / 2,
                longitude: (bounds.sw.longitude + bounds.ne.longitude) / 2
            )

            let camera = MLNMapCamera(lookingAtCenter: center, altitude: 0, pitch: 0, heading: 0)
            let options = MLNMapSnapshotOptions(styleURL: styleURL, camera: camera, size: size)
            options.coordinateBounds = bounds
            options.showsLogo = false

            let tileSnapshotter = MLNMapSnapshotter(options: options)
            tileSnapshotter.start { snapshot, error in
                nonisolated(unsafe) let capturedSnapshot = snapshot
                DispatchQueue.main.async {
                    guard let snapshot = capturedSnapshot else {
                        continuation.resume(returning: UIImage())
                        return
                    }

                    // Draw routes - snapshot.point(for:) handles coordinate conversion
                    // Core Graphics clips lines outside tile bounds automatically
                    // Filter is applied to map before routes are drawn
                    let tileImage = autoreleasepool {
                        self.drawRoutesOnSnapshot(
                            snapshot,
                            size: size,
                            style: activeRouteStyle,
                            using: segments,
                            strokeScale: strokeScale,
                            filter: filter
                        )
                    }

                    continuation.resume(returning: tileImage)
                }
            }
        }
    }

    /// Combines 4 tiles into a single final image
    private func combineTiles(
        topLeft: UIImage,
        topRight: UIImage,
        bottomLeft: UIImage,
        bottomRight: UIImage,
        finalSize: CGSize
    ) -> UIImage {
        let renderer = UIGraphicsImageRenderer(size: finalSize)
        return renderer.image { context in
            let halfWidth = finalSize.width / 2
            let halfHeight = finalSize.height / 2

            topLeft.draw(in: CGRect(x: 0, y: 0, width: halfWidth, height: halfHeight))
            topRight.draw(in: CGRect(x: halfWidth, y: 0, width: halfWidth, height: halfHeight))
            bottomLeft.draw(in: CGRect(x: 0, y: halfHeight, width: halfWidth, height: halfHeight))
            bottomRight.draw(in: CGRect(x: halfWidth, y: halfHeight, width: halfWidth, height: halfHeight))
        }
    }

    /// Exports image using tiled rendering for reduced peak memory usage.
    /// Renders the map as a 2x2 grid of tiles, then combines them.
    private func exportImageTiled(size: ExportSize) {
        guard let boundsToUse = activeBounds else { return }

        // Release preview resources
        snapshotter?.cancel()
        snapshotter = nil
        previewImage = nil

        // Always use exportRouteStyle - it contains template's map style settings
        let activeRouteStyle = exportRouteStyle
        isExporting = true

        let exportSize = size.dimensions
        let tileSize = CGSize(width: exportSize.width / 2, height: exportSize.height / 2)
        let strokeScaleFactor = strokeScale(for: exportSize)

        // For custom framing, preserve horizontal bounds exactly; for auto-fit, add padding
        let paddedBounds: MLNCoordinateBounds
        if useCustomFraming {
            paddedBounds = adjustBoundsForCustomFraming(boundsToUse, targetSize: exportSize)
        } else {
            paddedBounds = padBoundsForSnapshot(boundsToUse, targetSize: exportSize)
        }
        let tiles = splitBoundsIntoTiles(paddedBounds)

        guard let styleURL = activeRouteStyle.mapStyle.styleURL(apiKey: stadiaAPIKey) else {
            isExporting = false
            return
        }

        // Capture segments for async task
        let capturedSegments = segments

        Task { @MainActor in
            // Render tiles SEQUENTIALLY to minimize peak memory
            // Each tile is ~17MB snapshot + ~17MB renderer = ~35MB peak per tile
            // Filter is applied per-tile before routes are drawn, so routes keep their original color
            var topLeftImage = await renderTile(
                bounds: tiles.topLeft, size: tileSize,
                styleURL: styleURL, activeRouteStyle: activeRouteStyle,
                segments: capturedSegments, strokeScale: strokeScaleFactor,
                filter: currentFilter
            )
            var topRightImage = await renderTile(
                bounds: tiles.topRight, size: tileSize,
                styleURL: styleURL, activeRouteStyle: activeRouteStyle,
                segments: capturedSegments, strokeScale: strokeScaleFactor,
                filter: currentFilter
            )
            var bottomLeftImage = await renderTile(
                bounds: tiles.bottomLeft, size: tileSize,
                styleURL: styleURL, activeRouteStyle: activeRouteStyle,
                segments: capturedSegments, strokeScale: strokeScaleFactor,
                filter: currentFilter
            )
            var bottomRightImage = await renderTile(
                bounds: tiles.bottomRight, size: tileSize,
                styleURL: styleURL, activeRouteStyle: activeRouteStyle,
                segments: capturedSegments, strokeScale: strokeScaleFactor,
                filter: currentFilter
            )

            // Combine tiles into final image
            let combinedMap = autoreleasepool {
                combineTiles(
                    topLeft: topLeftImage, topRight: topRightImage,
                    bottomLeft: bottomLeftImage, bottomRight: bottomRightImage,
                    finalSize: exportSize
                )
            }

            // Release tile images to free memory before decoration
            topLeftImage = UIImage()
            topRightImage = UIImage()
            bottomLeftImage = UIImage()
            bottomRightImage = UIImage()

            // Decorate the combined map (filter already applied per-tile)
            let finalImage: UIImage? = autoreleasepool {
                let decorator = MapExportDecorator(configuration: decoratorConfig)
                let decoratedImage = decorator.decorate(
                    mapImage: combinedMap,
                    title: titleText,
                    subtitle: subtitleContent,
                    targetSize: exportSize
                )

                return decoratedImage
            }

            if let image = finalImage {
                exportedImage = image
                showShareSheet = true
                saveSettings()  // Save settings on successful export
            }
            isExporting = false
        }
    }

    private func exportImage(size: ExportSize) {
        guard let boundsToUse = activeBounds else { return }

        // Use tiled rendering for large exports to reduce peak memory (only for non-rotated maps)
        if size == .print && !isMapRotated {
            exportImageTiled(size: size)
            return
        }

        // Memory optimization: Release preview resources before starting large export
        // This frees ~50MB of memory from the preview snapshotter and image
        snapshotter?.cancel()
        snapshotter = nil
        previewImage = nil

        // Always use exportRouteStyle - it contains template's map style settings
        // (applyTemplateSettings populates exportMapStyle from config.mapStyleRaw)
        let activeRouteStyle = exportRouteStyle

        isExporting = true

        let exportSize = size.dimensions

        guard let styleURL = activeRouteStyle.mapStyle.styleURL(apiKey: stadiaAPIKey) else {
            isExporting = false
            return
        }

        // Make snapshot taller to include attribution area
        let expandedSize = CGSize(width: exportSize.width, height: exportSize.height * (1 + attributionCropFraction))
        let strokeScaleFactor = strokeScale(for: exportSize)

        let options: MLNMapSnapshotOptions

        // Handle rotated maps differently - use camera with zoom level instead of bounds
        if isMapRotated, let center = activeCenter, let zoomLevel = activeZoomLevel {
            // For rotated maps, use camera-based snapshotting with the original center, zoom, and heading
            let camera = MLNMapCamera(lookingAtCenter: center, altitude: 0, pitch: 0, heading: activeHeading)
            options = MLNMapSnapshotOptions(styleURL: styleURL, camera: camera, size: expandedSize)
            options.zoomLevel = zoomLevel
        } else {
            // For non-rotated maps, use bounds-based snapshotting
            let paddedBounds: MLNCoordinateBounds
            if useCustomFraming {
                paddedBounds = adjustBoundsForCustomFraming(boundsToUse, targetSize: exportSize)
            } else {
                paddedBounds = padBoundsForSnapshot(boundsToUse, targetSize: exportSize)
            }

            // Expand bounds at the bottom to capture attribution area for cropping
            let expandedBounds = expandBoundsForCropping(
                paddedBounds,
                targetAspectRatio: exportSize.height / exportSize.width
            )

            let center = CLLocationCoordinate2D(
                latitude: (expandedBounds.sw.latitude + expandedBounds.ne.latitude) / 2,
                longitude: (expandedBounds.sw.longitude + expandedBounds.ne.longitude) / 2
            )

            let camera = MLNMapCamera(lookingAtCenter: center, altitude: 0, pitch: 0, heading: 0)
            options = MLNMapSnapshotOptions(styleURL: styleURL, camera: camera, size: expandedSize)
            options.coordinateBounds = expandedBounds
        }
        options.showsLogo = false

        let exportSnapshotter = MLNMapSnapshotter(options: options)
        exportSnapshotter.start { snapshot, error in
            // Capture snapshot as nonisolated(unsafe) to cross isolation boundary safely
            // This is safe because we immediately dispatch to main thread
            nonisolated(unsafe) let capturedSnapshot = snapshot
            DispatchQueue.main.async {
                guard let snapshot = capturedSnapshot else {
                    isExporting = false
                    return
                }

                // Use autoreleasepool to release intermediate images as soon as possible
                // This significantly reduces peak memory during 4K exports
                let finalImage: UIImage? = autoreleasepool {
                    // Segments are already pre-simplified for large cities (in styledSegmentsForExportOptimized)
                    // Filter is applied to map before routes are drawn, so routes keep their original color
                    let fullImage = drawRoutesOnSnapshot(
                        snapshot,
                        size: exportSize,
                        style: activeRouteStyle,
                        using: segments,
                        strokeScale: strokeScaleFactor,
                        filter: currentFilter
                    )
                    let croppedMap = cropAttribution(from: fullImage, targetSize: exportSize)

                    // Apply decoration
                    let decorator = MapExportDecorator(configuration: decoratorConfig)
                    let decoratedImage = decorator.decorate(
                        mapImage: croppedMap,
                        title: titleText,
                        subtitle: subtitleContent,
                        targetSize: exportSize
                    )

                    return decoratedImage
                }
                // fullImage, croppedMap, and intermediate decorator buffers are released here

                if let image = finalImage {
                    exportedImage = image
                    showShareSheet = true
                    saveSettings()  // Save settings on successful export
                }
                isExporting = false
            }
        }
    }

    /// Compute a stroke scale for a target output size so stroke thickness stays consistent across tiles.
    private func strokeScale(for targetSize: CGSize) -> CGFloat {
        let referenceScreenWidth: CGFloat = 390.0 // Baseline phone width
        return targetSize.width / referenceScreenWidth
    }

    private func drawRoutesOnSnapshot(
        _ snapshot: MLNMapSnapshot,
        size: CGSize,
        style: RouteStyle,
        using drawSegments: [StyledSegment],
        strokeScale: CGFloat,
        filter: ExportFilter = .none
    ) -> UIImage {
        var image = snapshot.image
        _ = size  // size is kept for call site clarity; renderer uses snapshot.image dimensions

        // Apply filter to map before drawing routes so routes stay their original color
        if filter != .none {
            image = applyFilter(filter, to: image)
        }

        let renderer = UIGraphicsImageRenderer(size: image.size)
        let result = renderer.image { context in
            image.draw(at: .zero)

            let cgContext = context.cgContext

            for segment in drawSegments where segment.coordinates.count >= 2 {
                let color = UIColor(style.color(for: segment.workoutType))
                let lineWidth = style.lineWidth(for: segment.workoutType)

                // Scale line width using a consistent factor derived from the final export size
                let scaledLineWidth = lineWidth * strokeScale

                cgContext.setStrokeColor(color.cgColor)
                cgContext.setLineWidth(scaledLineWidth)
                cgContext.setLineCap(.round)
                cgContext.setLineJoin(.round)

                let points = segment.coordinates.map { snapshot.point(for: $0) }

                if let first = points.first {
                    cgContext.move(to: first)
                    for point in points.dropFirst() {
                        cgContext.addLine(to: point)
                    }
                    cgContext.strokePath()
                }
            }
        }

        return result
    }

    /// Expands coordinate bounds vertically to capture extra area for attribution cropping
    private func expandBoundsForCropping(_ bounds: MLNCoordinateBounds, targetAspectRatio: CGFloat) -> MLNCoordinateBounds {
        let latSpan = bounds.ne.latitude - bounds.sw.latitude
        let lonSpan = bounds.ne.longitude - bounds.sw.longitude

        // Use the span that actually constrains the visible region for the given aspect ratio.
        // Otherwise we only shift the center instead of adding true visible area.
        let dominantVerticalSpan = max(latSpan, lonSpan * Double(targetAspectRatio))
        let extraLat = dominantVerticalSpan * Double(attributionCropFraction)

        return MLNCoordinateBounds(
            sw: CLLocationCoordinate2D(
                latitude: bounds.sw.latitude - extraLat,
                longitude: bounds.sw.longitude
            ),
            ne: CLLocationCoordinate2D(
                latitude: bounds.ne.latitude,
                longitude: bounds.ne.longitude
            )
        )
    }

    private func padBoundsForSnapshot(_ bounds: MLNCoordinateBounds, targetSize: CGSize) -> MLNCoordinateBounds {
        let padding = snapshotEdgePadding(for: targetSize)
        let paddedBounds = padBounds(bounds, targetSize: targetSize, padding: padding)
        return adjustBoundsToAspectRatio(paddedBounds, targetSize: targetSize)
    }

    /// Adjusts bounds for custom framing export.
    /// Preserves horizontal (longitude) bounds exactly as the user set them,
    /// and only adjusts vertical (latitude) to match the target aspect ratio.
    private func adjustBoundsForCustomFraming(_ bounds: MLNCoordinateBounds, targetSize: CGSize) -> MLNCoordinateBounds {
        guard targetSize.width > 0, targetSize.height > 0 else { return bounds }

        let targetAspect = Double(targetSize.width / targetSize.height)

        // Convert to Mercator space for accurate pixel-space calculations
        let mercXMin = longitudeToMercatorX(bounds.sw.longitude)
        let mercXMax = longitudeToMercatorX(bounds.ne.longitude)
        let mercYMin = latitudeToMercatorY(bounds.sw.latitude)
        let mercYMax = latitudeToMercatorY(bounds.ne.latitude)

        // Keep horizontal (Mercator X / longitude) exactly as-is
        let mercWidth = mercXMax - mercXMin

        // Calculate required Mercator height to match target aspect ratio
        let requiredMercHeight = mercWidth / targetAspect

        // Center the new height on the original vertical center
        let mercYCenter = (mercYMin + mercYMax) / 2
        let adjustedYMin = mercYCenter - requiredMercHeight / 2
        let adjustedYMax = mercYCenter + requiredMercHeight / 2

        return MLNCoordinateBounds(
            sw: CLLocationCoordinate2D(
                latitude: mercatorYToLatitude(adjustedYMin),
                longitude: bounds.sw.longitude  // Preserve exact longitude
            ),
            ne: CLLocationCoordinate2D(
                latitude: mercatorYToLatitude(adjustedYMax),
                longitude: bounds.ne.longitude  // Preserve exact longitude
            )
        )
    }

    private func snapshotEdgePadding(for targetSize: CGSize) -> UIEdgeInsets {
        let horizontal = min(max(targetSize.width * 0.08, 20), 120)
        let vertical = min(max(targetSize.height * 0.08, 20), 160)
        return UIEdgeInsets(top: vertical, left: horizontal, bottom: vertical, right: horizontal)
    }

    private func padBounds(_ bounds: MLNCoordinateBounds, targetSize: CGSize, padding: UIEdgeInsets) -> MLNCoordinateBounds {
        let latSpan = abs(bounds.ne.latitude - bounds.sw.latitude)
        let lonSpan = abs(bounds.ne.longitude - bounds.sw.longitude)

        // Ensure even near-straight routes get some horizontal/vertical breathing room.
        let baseLatSpan = max(latSpan, lonSpan * 0.10)
        let baseLonSpan = max(lonSpan, latSpan * 0.10)

        let usableWidth = max(targetSize.width - padding.left - padding.right, 1)
        let usableHeight = max(targetSize.height - padding.top - padding.bottom, 1)

        let scaleX = Double(targetSize.width / usableWidth)
        let scaleY = Double(targetSize.height / usableHeight)

        let expandedLatSpan = baseLatSpan * scaleY
        let expandedLonSpan = baseLonSpan * scaleX

        let centerLat = (bounds.ne.latitude + bounds.sw.latitude) / 2
        let centerLon = (bounds.ne.longitude + bounds.sw.longitude) / 2

        return MLNCoordinateBounds(
            sw: CLLocationCoordinate2D(
                latitude: centerLat - expandedLatSpan / 2,
                longitude: centerLon - expandedLonSpan / 2
            ),
            ne: CLLocationCoordinate2D(
                latitude: centerLat + expandedLatSpan / 2,
                longitude: centerLon + expandedLonSpan / 2
            )
        )
    }

    /// Crops out the attribution area by trimming the extra height from the bottom.
    private func cropAttribution(from image: UIImage, targetSize: CGSize) -> UIImage {
        let scale = image.scale

        let cropRect = CGRect(
            x: 0,
            y: 0,
            width: targetSize.width * scale,
            height: targetSize.height * scale
        )

        guard let cgImage = image.cgImage?.cropping(to: cropRect) else {
            return image
        }

        return UIImage(cgImage: cgImage, scale: scale, orientation: image.imageOrientation)
    }
}

// MARK: - Share Sheet

private struct ShareSheet: UIViewControllerRepresentable {
    let items: [Any]

    func makeUIViewController(context: Context) -> UIActivityViewController {
        UIActivityViewController(activityItems: items, applicationActivities: nil)
    }

    func updateUIViewController(_ uiViewController: UIActivityViewController, context: Context) {}
}

// MARK: - Template Button

private struct TemplateButton: View {
    let template: ExportTemplate
    let isSelected: Bool
    let action: () -> Void

    var body: some View {
        Button(action: action) {
            VStack(spacing: DS.Spacing.xs) {
                Image(systemName: template.icon)
                    .font(.system(size: 20))
                    .frame(width: 44, height: 44)
                    .background(isSelected ? DS.Colors.accent : DS.Colors.secondaryBackground)
                    .foregroundStyle(isSelected ? .white : DS.Colors.primaryText)
                    .clipShape(RoundedRectangle(cornerRadius: 10))

                Text(template.rawValue)
                    .font(.caption2)
                    .fontWeight(isSelected ? .semibold : .regular)
                    .foregroundStyle(isSelected ? DS.Colors.accent : DS.Colors.secondaryText)
            }
        }
        .buttonStyle(.plain)
    }
}

// MARK: - Export Map Style Picker View

struct ExportMapStylePickerView: View {
    @Binding var selectedStyle: MapStyle

    var body: some View {
        List {
            ForEach(MapStyle.availableCases) { style in
                Button {
                    selectedStyle = style
                } label: {
                    HStack(spacing: DS.Spacing.md) {
                        Image(systemName: style.icon)
                            .font(.system(size: 20))
                            .foregroundStyle(DS.Colors.accent)
                            .frame(width: 28)

                        Text(style.displayName)
                            .foregroundStyle(DS.Colors.primaryText)

                        Spacer()

                        if selectedStyle == style {
                            Image(systemName: "checkmark")
                                .foregroundStyle(DS.Colors.accent)
                                .fontWeight(.semibold)
                        }
                    }
                }
                .contentShape(Rectangle())
            }
        }
        .navigationTitle("Map Style")
        .navigationBarTitleDisplayMode(.inline)
    }
}

// MARK: - Export Language Picker View

struct ExportLanguagePickerView: View {
    @Binding var selectedLanguage: MapLanguage

    var body: some View {
        List {
            ForEach(MapLanguage.allCases) { language in
                Button {
                    selectedLanguage = language
                } label: {
                    HStack(spacing: DS.Spacing.md) {
                        Text(language.flag)
                            .font(.title2)

                        Text(language.displayName)
                            .foregroundStyle(DS.Colors.primaryText)

                        Spacer()

                        if selectedLanguage == language {
                            Image(systemName: "checkmark")
                                .foregroundStyle(DS.Colors.accent)
                                .fontWeight(.semibold)
                        }
                    }
                }
                .contentShape(Rectangle())
            }
        }
        .navigationTitle("Map Language")
        .navigationBarTitleDisplayMode(.inline)
    }
}

// MARK: - Export Workout Type Line Width Row

struct ExportWorkoutTypeLineWidthRow: View {
    let workoutType: WorkoutType
    @Binding var lineWidth: CGFloat
    let color: Color

    var body: some View {
        VStack(alignment: .leading, spacing: DS.Spacing.sm) {
            HStack(spacing: DS.Spacing.md) {
                Image(systemName: workoutType.sfSymbol)
                    .font(.system(size: 18))
                    .foregroundStyle(color)
                    .frame(width: 24)

                Text(workoutType.displayName)
                    .foregroundStyle(DS.Colors.primaryText)

                Spacer()

                Text(String(format: "%.1f pt", lineWidth))
                    .foregroundStyle(DS.Colors.secondaryText)
                    .font(.subheadline)
            }

            HStack(spacing: DS.Spacing.md) {
                Slider(value: $lineWidth, in: 1...10, step: 0.5)
                    .tint(color)

                RoundedRectangle(cornerRadius: lineWidth / 2)
                    .fill(color)
                    .frame(width: 40, height: lineWidth)
            }
        }
        .padding(.vertical, DS.Spacing.xs)
    }
}

// MARK: - Export Color Picker Grid

struct ExportColorPickerGrid: View {
    @Binding var selectedColor: Color
    let colors: [Color]

    var body: some View {
        LazyVGrid(columns: Array(repeating: GridItem(.flexible()), count: 6), spacing: DS.Spacing.md) {
            ForEach(colors, id: \.self) { color in
                Button {
                    selectedColor = color
                } label: {
                    Circle()
                        .fill(color)
                        .frame(width: 36, height: 36)
                        .overlay {
                            if selectedColor == color {
                                Circle()
                                    .strokeBorder(.white, lineWidth: 3)
                                Circle()
                                    .strokeBorder(color, lineWidth: 1)
                                    .padding(2)
                            }
                        }
                }
                .buttonStyle(.plain)
            }
        }
        .padding(.vertical, DS.Spacing.sm)
    }
}

// MARK: - Export Workout Type Color Row

struct ExportWorkoutTypeColorRow: View {
    let workoutType: WorkoutType
    @Binding var color: Color
    let presetColors: [Color]

    @State private var showColorPicker = false

    var body: some View {
        Button {
            showColorPicker = true
        } label: {
            HStack(spacing: DS.Spacing.md) {
                Image(systemName: workoutType.sfSymbol)
                    .font(.system(size: 18))
                    .foregroundStyle(color)
                    .frame(width: 24)

                Text(workoutType.displayName)
                    .foregroundStyle(DS.Colors.primaryText)

                Spacer()

                Circle()
                    .fill(color)
                    .frame(width: 24, height: 24)

                Image(systemName: "chevron.right")
                    .font(.system(size: 12, weight: .semibold))
                    .foregroundStyle(DS.Colors.secondaryText)
            }
        }
        .contentShape(Rectangle())
        .sheet(isPresented: $showColorPicker) {
            NavigationStack {
                List {
                    Section {
                        ExportColorPickerGrid(selectedColor: $color, colors: presetColors)
                    }
                }
                .navigationTitle(workoutType.displayName)
                .navigationBarTitleDisplayMode(.inline)
                .toolbar {
                    ToolbarItem(placement: .topBarTrailing) {
                        Button("Done") {
                            showColorPicker = false
                        }
                        .fontWeight(.semibold)
                    }
                }
            }
            .presentationDetents([.medium])
            .presentationDragIndicator(.visible)
        }
    }
}

// MARK: - Export Size Selection Sheet

private struct ExportSizeSelectionSheet: View {
    let segmentCount: Int
    let onExport: (ExportSize) -> Void

    @Environment(\.dismiss) private var dismiss
    @State private var showMemoryWarning = false
    @State private var selectedSize: ExportSize?

    /// Check if we should warn about memory for large 4K exports
    private func shouldWarnAboutMemory(for size: ExportSize) -> Bool {
        size == .print && segmentCount > 5000
    }

    var body: some View {
        NavigationStack {
            List {
                Section {
                    exportSizeButton(.socialMedia)
                    exportSizeButton(.desktop)
                    exportSizeButton(.print)
                } header: {
                    Text("Select Export Size")
                } footer: {
                    Text("Larger sizes take longer to generate and use more memory.")
                }
            }
            .navigationTitle("Export Size")
            .navigationBarTitleDisplayMode(.inline)
            .toolbar {
                ToolbarItem(placement: .topBarLeading) {
                    Button("Cancel") {
                        dismiss()
                    }
                }
            }
            .alert("Large Export Warning", isPresented: $showMemoryWarning) {
                Button("Cancel", role: .cancel) {
                    selectedSize = nil
                }
                Button("Export Anyway") {
                    if let size = selectedSize {
                        onExport(size)
                    }
                }
            } message: {
                Text("Exporting this large map at 4K resolution may use significant memory. Consider closing other apps before exporting.")
            }
        }
    }

    private func sizeDescription(for size: ExportSize) -> String {
        switch size {
        case .socialMedia:
            return "Best for Instagram, Twitter, and social sharing"
        case .desktop:
            return "Great for wallpapers and presentations"
        case .print:
            return "High quality for printing posters"
        }
    }

    private func sizeIcon(for size: ExportSize) -> String {
        switch size {
        case .socialMedia:
            return "iphone"
        case .desktop:
            return "desktopcomputer"
        case .print:
            return "printer"
        }
    }

    @ViewBuilder
    private func exportSizeButton(_ size: ExportSize) -> some View {
        Button {
            if shouldWarnAboutMemory(for: size) {
                selectedSize = size
                showMemoryWarning = true
            } else {
                onExport(size)
            }
        } label: {
            HStack {
                VStack(alignment: .leading, spacing: DS.Spacing.xs) {
                    Text(size.rawValue)
                        .font(.headline)
                        .foregroundStyle(DS.Colors.primaryText)
                    Text(size.displayDescription)
                        .font(.subheadline)
                        .foregroundStyle(DS.Colors.secondaryText)
                    Text(sizeDescription(for: size))
                        .font(.caption)
                        .foregroundStyle(DS.Colors.secondaryText.opacity(0.8))
                }
                Spacer()
                Image(systemName: sizeIcon(for: size))
                    .font(.system(size: 24))
                    .foregroundStyle(DS.Colors.accent)
            }
            .padding(.vertical, DS.Spacing.xs)
        }
    }
}

//
//  MapExportDecorator.swift
//  AllMyWalks
//
//  Copyright © Aelptos. All rights reserved.
//

import UIKit
import SwiftUI

// MARK: - UIColor Safe Init Extension

private extension UIColor {
    /// Creates a UIColor with component values clamped to 0-1 range.
    /// This prevents crashes when colors from wide gamut color spaces (like Display P3)
    /// have component values outside the standard sRGB range.
    static func safeColor(red: CGFloat, green: CGFloat, blue: CGFloat, alpha: CGFloat) -> UIColor {
        UIColor(
            red: min(max(red, 0), 1),
            green: min(max(green, 0), 1),
            blue: min(max(blue, 0), 1),
            alpha: min(max(alpha, 0), 1)
        )
    }
}

// MARK: - Subtitle Content

/// Content options for the export subtitle
enum ExportSubtitleContent {
    case distance(Double)
    case dateRange(Date, Date)
    case workoutCount(Int)
    case custom(String)
    case none

    /// Format the content as a display string
    func formatted() -> String? {
        switch self {
        case .distance(let meters):
            return DistanceFormatter.string(fromMeters: meters, style: .rounded)
        case .dateRange(let start, let end):
            let formatter = DateFormatter()
            formatter.dateFormat = "MMM yyyy"
            return "\(formatter.string(from: start)) – \(formatter.string(from: end))"
        case .workoutCount(let count):
            return count == 1 ? "1 workout" : "\(count) workouts"
        case .custom(let text):
            return text.isEmpty ? nil : text
        case .none:
            return nil
        }
    }
}

// MARK: - Border Line Type

/// Line style options for borders
enum BorderLineType: String, CaseIterable, Identifiable, Codable {
    case none = "None"
    case normal = "Normal"
    case dashed = "Dashed"
    case dotted = "Dotted"

    var id: String { rawValue }
}

// MARK: - Title Position

/// Position options for title/subtitle area
enum TitlePosition: String, CaseIterable, Identifiable, Codable {
    case top = "top"
    case bottom = "bottom"

    var id: String { rawValue }

    var displayName: String {
        switch self {
        case .top: return "Top"
        case .bottom: return "Bottom"
        }
    }
}

// MARK: - Decoration Position

/// Position options for poster decorations
enum DecorationPosition: String, CaseIterable, Identifiable, Codable {
    case top = "top"
    case bottom = "bottom"

    var id: String { rawValue }

    var displayName: String {
        switch self {
        case .top: return "Top"
        case .bottom: return "Bottom"
        }
    }
}

// MARK: - Decoration Alignment

/// Alignment options for poster decorations
enum DecorationAlignment: String, CaseIterable, Identifiable, Codable {
    case left = "left"
    case center = "center"
    case right = "right"

    var id: String { rawValue }

    var displayName: String {
        switch self {
        case .left: return "Left"
        case .center: return "Center"
        case .right: return "Right"
        }
    }
}

// MARK: - Decoration Type

/// Shape options for poster decorations
enum DecorationType: String, CaseIterable, Identifiable, Codable {
    case none = "none"
    case threeSquares = "threeSquares"
    case oneRectangle = "oneRectangle"
    case threeCircles = "threeCircles"
    case oneCircle = "oneCircle"
    case oneSquare = "oneSquare"
    case fullWidthRectangle = "fullWidthRectangle"

    var id: String { rawValue }

    var displayName: String {
        switch self {
        case .none: return "None"
        case .threeSquares: return "Three Squares"
        case .oneRectangle: return "Rectangle"
        case .threeCircles: return "Three Circles"
        case .oneCircle: return "Circle"
        case .oneSquare: return "Square"
        case .fullWidthRectangle: return "Full Width Bar"
        }
    }

    /// Number of colors used by this decoration type
    var colorCount: Int {
        switch self {
        case .none: return 0
        case .threeSquares, .threeCircles: return 3
        case .oneRectangle, .oneCircle, .oneSquare, .fullWidthRectangle: return 1
        }
    }
}

// MARK: - Poster Decoration

/// Configuration for decorative shapes on the poster
struct PosterDecoration: Codable {
    var type: DecorationType
    var position: DecorationPosition
    var alignment: DecorationAlignment
    var color1: UIColor
    var color2: UIColor
    var color3: UIColor
    var sizeRatio: CGFloat  // Size relative to poster width (e.g., 0.08 = 8% of width for square size)
    var spacingRatio: CGFloat  // Spacing between shapes (e.g., 0.015 = 1.5% of width)

    static let none = PosterDecoration(
        type: .none,
        position: .bottom,
        alignment: .right,
        color1: .red,
        color2: .blue,
        color3: .yellow,
        sizeRatio: 0.08,
        spacingRatio: 0.015
    )

    // MARK: - Codable

    enum CodingKeys: String, CodingKey {
        case type, position, alignment
        case color1Red, color1Green, color1Blue, color1Alpha
        case color2Red, color2Green, color2Blue, color2Alpha
        case color3Red, color3Green, color3Blue, color3Alpha
        case sizeRatio, spacingRatio
    }

    init(type: DecorationType, position: DecorationPosition, alignment: DecorationAlignment,
         color1: UIColor, color2: UIColor, color3: UIColor,
         sizeRatio: CGFloat, spacingRatio: CGFloat) {
        self.type = type
        self.position = position
        self.alignment = alignment
        self.color1 = color1
        self.color2 = color2
        self.color3 = color3
        self.sizeRatio = sizeRatio
        self.spacingRatio = spacingRatio
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        type = try container.decode(DecorationType.self, forKey: .type)
        position = try container.decode(DecorationPosition.self, forKey: .position)
        alignment = try container.decode(DecorationAlignment.self, forKey: .alignment)

        let r1 = try container.decode(CGFloat.self, forKey: .color1Red)
        let g1 = try container.decode(CGFloat.self, forKey: .color1Green)
        let b1 = try container.decode(CGFloat.self, forKey: .color1Blue)
        let a1 = try container.decode(CGFloat.self, forKey: .color1Alpha)
        color1 = UIColor.safeColor(red: r1, green: g1, blue: b1, alpha: a1)

        let r2 = try container.decode(CGFloat.self, forKey: .color2Red)
        let g2 = try container.decode(CGFloat.self, forKey: .color2Green)
        let b2 = try container.decode(CGFloat.self, forKey: .color2Blue)
        let a2 = try container.decode(CGFloat.self, forKey: .color2Alpha)
        color2 = UIColor.safeColor(red: r2, green: g2, blue: b2, alpha: a2)

        let r3 = try container.decode(CGFloat.self, forKey: .color3Red)
        let g3 = try container.decode(CGFloat.self, forKey: .color3Green)
        let b3 = try container.decode(CGFloat.self, forKey: .color3Blue)
        let a3 = try container.decode(CGFloat.self, forKey: .color3Alpha)
        color3 = UIColor.safeColor(red: r3, green: g3, blue: b3, alpha: a3)

        sizeRatio = try container.decode(CGFloat.self, forKey: .sizeRatio)
        spacingRatio = try container.decode(CGFloat.self, forKey: .spacingRatio)
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(type, forKey: .type)
        try container.encode(position, forKey: .position)
        try container.encode(alignment, forKey: .alignment)

        var r: CGFloat = 0, g: CGFloat = 0, b: CGFloat = 0, a: CGFloat = 0
        color1.getRed(&r, green: &g, blue: &b, alpha: &a)
        try container.encode(r, forKey: .color1Red)
        try container.encode(g, forKey: .color1Green)
        try container.encode(b, forKey: .color1Blue)
        try container.encode(a, forKey: .color1Alpha)

        color2.getRed(&r, green: &g, blue: &b, alpha: &a)
        try container.encode(r, forKey: .color2Red)
        try container.encode(g, forKey: .color2Green)
        try container.encode(b, forKey: .color2Blue)
        try container.encode(a, forKey: .color2Alpha)

        color3.getRed(&r, green: &g, blue: &b, alpha: &a)
        try container.encode(r, forKey: .color3Red)
        try container.encode(g, forKey: .color3Green)
        try container.encode(b, forKey: .color3Blue)
        try container.encode(a, forKey: .color3Alpha)

        try container.encode(sizeRatio, forKey: .sizeRatio)
        try container.encode(spacingRatio, forKey: .spacingRatio)
    }
}

// MARK: - Border Configuration

/// Configuration for a single border (outer or inner)
struct BorderConfig: Codable {
    var lineType: BorderLineType
    var color: UIColor
    var widthRatio: CGFloat
    var cornerRadiusRatio: CGFloat

    static let none = BorderConfig(lineType: .none, color: .clear, widthRatio: 0, cornerRadiusRatio: 0)

    // MARK: - Codable

    enum CodingKeys: String, CodingKey {
        case lineType, widthRatio, cornerRadiusRatio
        case colorRed, colorGreen, colorBlue, colorAlpha
    }

    init(lineType: BorderLineType, color: UIColor, widthRatio: CGFloat, cornerRadiusRatio: CGFloat) {
        self.lineType = lineType
        self.color = color
        self.widthRatio = widthRatio
        self.cornerRadiusRatio = cornerRadiusRatio
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        lineType = try container.decode(BorderLineType.self, forKey: .lineType)
        widthRatio = try container.decode(CGFloat.self, forKey: .widthRatio)
        cornerRadiusRatio = try container.decode(CGFloat.self, forKey: .cornerRadiusRatio)

        let red = try container.decode(CGFloat.self, forKey: .colorRed)
        let green = try container.decode(CGFloat.self, forKey: .colorGreen)
        let blue = try container.decode(CGFloat.self, forKey: .colorBlue)
        let alpha = try container.decode(CGFloat.self, forKey: .colorAlpha)
        color = UIColor.safeColor(red: red, green: green, blue: blue, alpha: alpha)
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(lineType, forKey: .lineType)
        try container.encode(widthRatio, forKey: .widthRatio)
        try container.encode(cornerRadiusRatio, forKey: .cornerRadiusRatio)

        var r: CGFloat = 0, g: CGFloat = 0, b: CGFloat = 0, a: CGFloat = 0
        color.getRed(&r, green: &g, blue: &b, alpha: &a)
        try container.encode(r, forKey: .colorRed)
        try container.encode(g, forKey: .colorGreen)
        try container.encode(b, forKey: .colorBlue)
        try container.encode(a, forKey: .colorAlpha)
    }
}

// MARK: - Map Export Decorator

/// Decorates map images with padding, borders, and titles for export
class MapExportDecorator {

    // MARK: - Configuration

    struct Configuration: Codable {
        // Layout ratios (relative to image width)
        var paddingRatio: CGFloat
        var titleAreaHeightRatio: CGFloat
        var titleFontRatio: CGFloat
        var subtitleFontRatio: CGFloat
        var elementSpacingRatio: CGFloat

        // Map Borders (around the map image)
        var outerBorder: BorderConfig
        var innerBorder: BorderConfig
        var borderGapRatio: CGFloat
        var borderGapColor: UIColor?  // Fill color between borders (nil = use backgroundColor)

        // Poster Borders (around the entire poster/document)
        var posterOuterBorder: BorderConfig
        var posterInnerBorder: BorderConfig
        var posterBorderGapRatio: CGFloat
        var posterBorderGapColor: UIColor?
        var posterMarginRatio: CGFloat  // Space between document edge and poster outer border

        // Title Position
        var titlePositionRaw: String?  // "top" or "bottom" (default: "bottom")

        var titlePosition: TitlePosition {
            get { TitlePosition(rawValue: titlePositionRaw ?? "bottom") ?? .bottom }
            set { titlePositionRaw = newValue.rawValue }
        }

        // Poster Decoration
        var posterDecoration: PosterDecoration

        // Colors
        var backgroundColor: UIColor
        var titleColor: UIColor
        var subtitleColor: UIColor

        // Fonts
        var titleFontWeight: UIFont.Weight
        var subtitleFontWeight: UIFont.Weight
        var titleFontName: String?
        var subtitleFontName: String?

        // Options
        var showTitle: Bool
        var showSubtitle: Bool

        // Text alignment
        var titleAlignmentRaw: String?
        var subtitleAlignmentRaw: String?

        // Subtitle vertical offset (relative to image width)
        // Negative values move subtitle up (closer to/overlapping title)
        // Positive values move subtitle down (further from title)
        var subtitleOffsetRatio: CGFloat

        // Map Style (route rendering)
        var mapStyleRaw: String?
        var mapLanguageRaw: String?
        var hideLabels: Bool
        var lineWidth: CGFloat
        var defaultLineColor: UIColor
        var useColorPerWorkoutType: Bool
        var useLineWidthPerWorkoutType: Bool
        var workoutTypeColorsRaw: [String: UIColor]
        var workoutTypeLineWidths: [String: CGFloat]

        // Image filter (applied after rendering)
        var filterRaw: String?

        // MARK: - Codable

        /// Helper struct for serializing UIColor
        private struct ColorComponents: Codable {
            let red: CGFloat
            let green: CGFloat
            let blue: CGFloat
            let alpha: CGFloat

            init(from color: UIColor) {
                var r: CGFloat = 0, g: CGFloat = 0, b: CGFloat = 0, a: CGFloat = 0
                color.getRed(&r, green: &g, blue: &b, alpha: &a)
                self.red = r
                self.green = g
                self.blue = b
                self.alpha = a
            }

            var uiColor: UIColor {
                UIColor.safeColor(red: red, green: green, blue: blue, alpha: alpha)
            }
        }

        /// Helper to convert font weight to/from string
        private static func fontWeightToString(_ weight: UIFont.Weight) -> String {
            switch weight {
            case .ultraLight: return "ultraLight"
            case .thin: return "thin"
            case .light: return "light"
            case .regular: return "regular"
            case .medium: return "medium"
            case .semibold: return "semibold"
            case .bold: return "bold"
            case .heavy: return "heavy"
            case .black: return "black"
            default: return "regular"
            }
        }

        private static func stringToFontWeight(_ string: String) -> UIFont.Weight {
            switch string {
            case "ultraLight": return .ultraLight
            case "thin": return .thin
            case "light": return .light
            case "regular": return .regular
            case "medium": return .medium
            case "semibold": return .semibold
            case "bold": return .bold
            case "heavy": return .heavy
            case "black": return .black
            default: return .regular
            }
        }

        enum CodingKeys: String, CodingKey {
            case paddingRatio, titleAreaHeightRatio, titleFontRatio, subtitleFontRatio, elementSpacingRatio
            case outerBorder, innerBorder, borderGapRatio, borderGapColor
            case posterOuterBorder, posterInnerBorder, posterBorderGapRatio, posterBorderGapColor, posterMarginRatio
            case titlePositionRaw
            case posterDecoration
            case backgroundColor, titleColor, subtitleColor
            case titleFontWeight, subtitleFontWeight, titleFontName, subtitleFontName
            case showTitle, showSubtitle
            case titleAlignmentRaw, subtitleAlignmentRaw, subtitleOffsetRatio
            case mapStyleRaw, mapLanguageRaw, hideLabels, lineWidth, defaultLineColor
            case useColorPerWorkoutType, useLineWidthPerWorkoutType
            case workoutTypeColorsRaw, workoutTypeLineWidths
            case filterRaw
        }

        init(from decoder: Decoder) throws {
            let container = try decoder.container(keyedBy: CodingKeys.self)

            paddingRatio = try container.decode(CGFloat.self, forKey: .paddingRatio)
            titleAreaHeightRatio = try container.decode(CGFloat.self, forKey: .titleAreaHeightRatio)
            titleFontRatio = try container.decode(CGFloat.self, forKey: .titleFontRatio)
            subtitleFontRatio = try container.decode(CGFloat.self, forKey: .subtitleFontRatio)
            elementSpacingRatio = try container.decode(CGFloat.self, forKey: .elementSpacingRatio)

            outerBorder = try container.decode(BorderConfig.self, forKey: .outerBorder)
            innerBorder = try container.decode(BorderConfig.self, forKey: .innerBorder)
            borderGapRatio = try container.decode(CGFloat.self, forKey: .borderGapRatio)
            if let gapColorComponents = try container.decodeIfPresent(ColorComponents.self, forKey: .borderGapColor) {
                borderGapColor = gapColorComponents.uiColor
            } else {
                borderGapColor = nil
            }

            // Poster borders (with backward-compatible defaults)
            posterOuterBorder = try container.decodeIfPresent(BorderConfig.self, forKey: .posterOuterBorder) ?? .none
            posterInnerBorder = try container.decodeIfPresent(BorderConfig.self, forKey: .posterInnerBorder) ?? .none
            posterBorderGapRatio = try container.decodeIfPresent(CGFloat.self, forKey: .posterBorderGapRatio) ?? 0
            if let posterGapColorComponents = try container.decodeIfPresent(ColorComponents.self, forKey: .posterBorderGapColor) {
                posterBorderGapColor = posterGapColorComponents.uiColor
            } else {
                posterBorderGapColor = nil
            }
            posterMarginRatio = try container.decodeIfPresent(CGFloat.self, forKey: .posterMarginRatio) ?? 0

            // Title position (with backward-compatible default)
            titlePositionRaw = try container.decodeIfPresent(String.self, forKey: .titlePositionRaw)

            // Poster decoration (with backward-compatible default)
            posterDecoration = try container.decodeIfPresent(PosterDecoration.self, forKey: .posterDecoration) ?? .none

            let bgColor = try container.decode(ColorComponents.self, forKey: .backgroundColor)
            backgroundColor = bgColor.uiColor
            let titleCol = try container.decode(ColorComponents.self, forKey: .titleColor)
            titleColor = titleCol.uiColor
            let subtitleCol = try container.decode(ColorComponents.self, forKey: .subtitleColor)
            subtitleColor = subtitleCol.uiColor

            let titleWeightStr = try container.decode(String.self, forKey: .titleFontWeight)
            titleFontWeight = Self.stringToFontWeight(titleWeightStr)
            let subtitleWeightStr = try container.decode(String.self, forKey: .subtitleFontWeight)
            subtitleFontWeight = Self.stringToFontWeight(subtitleWeightStr)
            titleFontName = try container.decodeIfPresent(String.self, forKey: .titleFontName)
            subtitleFontName = try container.decodeIfPresent(String.self, forKey: .subtitleFontName)

            showTitle = try container.decode(Bool.self, forKey: .showTitle)
            showSubtitle = try container.decode(Bool.self, forKey: .showSubtitle)

            titleAlignmentRaw = try container.decodeIfPresent(String.self, forKey: .titleAlignmentRaw)
            subtitleAlignmentRaw = try container.decodeIfPresent(String.self, forKey: .subtitleAlignmentRaw)
            subtitleOffsetRatio = try container.decodeIfPresent(CGFloat.self, forKey: .subtitleOffsetRatio) ?? 0

            mapStyleRaw = try container.decodeIfPresent(String.self, forKey: .mapStyleRaw)
            mapLanguageRaw = try container.decodeIfPresent(String.self, forKey: .mapLanguageRaw)
            hideLabels = try container.decode(Bool.self, forKey: .hideLabels)
            lineWidth = try container.decode(CGFloat.self, forKey: .lineWidth)
            let defaultLineCol = try container.decode(ColorComponents.self, forKey: .defaultLineColor)
            defaultLineColor = defaultLineCol.uiColor
            useColorPerWorkoutType = try container.decode(Bool.self, forKey: .useColorPerWorkoutType)
            useLineWidthPerWorkoutType = try container.decode(Bool.self, forKey: .useLineWidthPerWorkoutType)

            let colorsDict = try container.decode([String: ColorComponents].self, forKey: .workoutTypeColorsRaw)
            workoutTypeColorsRaw = colorsDict.mapValues { $0.uiColor }
            workoutTypeLineWidths = try container.decode([String: CGFloat].self, forKey: .workoutTypeLineWidths)

            filterRaw = try container.decodeIfPresent(String.self, forKey: .filterRaw)
        }

        func encode(to encoder: Encoder) throws {
            var container = encoder.container(keyedBy: CodingKeys.self)

            try container.encode(paddingRatio, forKey: .paddingRatio)
            try container.encode(titleAreaHeightRatio, forKey: .titleAreaHeightRatio)
            try container.encode(titleFontRatio, forKey: .titleFontRatio)
            try container.encode(subtitleFontRatio, forKey: .subtitleFontRatio)
            try container.encode(elementSpacingRatio, forKey: .elementSpacingRatio)

            try container.encode(outerBorder, forKey: .outerBorder)
            try container.encode(innerBorder, forKey: .innerBorder)
            try container.encode(borderGapRatio, forKey: .borderGapRatio)
            if let gapColor = borderGapColor {
                try container.encode(ColorComponents(from: gapColor), forKey: .borderGapColor)
            }

            // Poster borders
            try container.encode(posterOuterBorder, forKey: .posterOuterBorder)
            try container.encode(posterInnerBorder, forKey: .posterInnerBorder)
            try container.encode(posterBorderGapRatio, forKey: .posterBorderGapRatio)
            if let posterGapColor = posterBorderGapColor {
                try container.encode(ColorComponents(from: posterGapColor), forKey: .posterBorderGapColor)
            }
            try container.encode(posterMarginRatio, forKey: .posterMarginRatio)

            // Title position
            try container.encodeIfPresent(titlePositionRaw, forKey: .titlePositionRaw)

            // Poster decoration
            try container.encode(posterDecoration, forKey: .posterDecoration)

            try container.encode(ColorComponents(from: backgroundColor), forKey: .backgroundColor)
            try container.encode(ColorComponents(from: titleColor), forKey: .titleColor)
            try container.encode(ColorComponents(from: subtitleColor), forKey: .subtitleColor)

            try container.encode(Self.fontWeightToString(titleFontWeight), forKey: .titleFontWeight)
            try container.encode(Self.fontWeightToString(subtitleFontWeight), forKey: .subtitleFontWeight)
            try container.encodeIfPresent(titleFontName, forKey: .titleFontName)
            try container.encodeIfPresent(subtitleFontName, forKey: .subtitleFontName)

            try container.encode(showTitle, forKey: .showTitle)
            try container.encode(showSubtitle, forKey: .showSubtitle)

            try container.encodeIfPresent(titleAlignmentRaw, forKey: .titleAlignmentRaw)
            try container.encodeIfPresent(subtitleAlignmentRaw, forKey: .subtitleAlignmentRaw)
            try container.encode(subtitleOffsetRatio, forKey: .subtitleOffsetRatio)

            try container.encodeIfPresent(mapStyleRaw, forKey: .mapStyleRaw)
            try container.encodeIfPresent(mapLanguageRaw, forKey: .mapLanguageRaw)
            try container.encode(hideLabels, forKey: .hideLabels)
            try container.encode(lineWidth, forKey: .lineWidth)
            try container.encode(ColorComponents(from: defaultLineColor), forKey: .defaultLineColor)
            try container.encode(useColorPerWorkoutType, forKey: .useColorPerWorkoutType)
            try container.encode(useLineWidthPerWorkoutType, forKey: .useLineWidthPerWorkoutType)

            let colorsDict = workoutTypeColorsRaw.mapValues { ColorComponents(from: $0) }
            try container.encode(colorsDict, forKey: .workoutTypeColorsRaw)
            try container.encode(workoutTypeLineWidths, forKey: .workoutTypeLineWidths)

            try container.encodeIfPresent(filterRaw, forKey: .filterRaw)
        }

        /// Convert to pretty-printed JSON string
        func toJSON() -> String? {
            let encoder = JSONEncoder()
            encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
            guard let data = try? encoder.encode(self) else { return nil }
            return String(data: data, encoding: .utf8)
        }

        /// Create from JSON string
        static func fromJSON(_ json: String) -> Configuration? {
            guard let data = json.data(using: .utf8) else { return nil }
            return try? JSONDecoder().decode(Configuration.self, from: data)
        }

        // MARK: - Memberwise Initializer

        init(
            paddingRatio: CGFloat,
            titleAreaHeightRatio: CGFloat,
            titleFontRatio: CGFloat,
            subtitleFontRatio: CGFloat,
            elementSpacingRatio: CGFloat,
            outerBorder: BorderConfig,
            innerBorder: BorderConfig,
            borderGapRatio: CGFloat,
            borderGapColor: UIColor? = nil,
            posterOuterBorder: BorderConfig = .none,
            posterInnerBorder: BorderConfig = .none,
            posterBorderGapRatio: CGFloat = 0,
            posterBorderGapColor: UIColor? = nil,
            posterMarginRatio: CGFloat = 0,
            titlePositionRaw: String? = nil,
            posterDecoration: PosterDecoration = .none,
            backgroundColor: UIColor,
            titleColor: UIColor,
            subtitleColor: UIColor,
            titleFontWeight: UIFont.Weight,
            subtitleFontWeight: UIFont.Weight,
            titleFontName: String?,
            subtitleFontName: String?,
            showTitle: Bool,
            showSubtitle: Bool,
            titleAlignmentRaw: String? = nil,
            subtitleAlignmentRaw: String? = nil,
            subtitleOffsetRatio: CGFloat = 0,
            mapStyleRaw: String?,
            mapLanguageRaw: String?,
            hideLabels: Bool,
            lineWidth: CGFloat,
            defaultLineColor: UIColor,
            useColorPerWorkoutType: Bool,
            useLineWidthPerWorkoutType: Bool,
            workoutTypeColorsRaw: [String: UIColor],
            workoutTypeLineWidths: [String: CGFloat],
            filterRaw: String?
        ) {
            self.paddingRatio = paddingRatio
            self.titleAreaHeightRatio = titleAreaHeightRatio
            self.titleFontRatio = titleFontRatio
            self.subtitleFontRatio = subtitleFontRatio
            self.elementSpacingRatio = elementSpacingRatio
            self.outerBorder = outerBorder
            self.innerBorder = innerBorder
            self.borderGapRatio = borderGapRatio
            self.borderGapColor = borderGapColor
            self.posterOuterBorder = posterOuterBorder
            self.posterInnerBorder = posterInnerBorder
            self.posterBorderGapRatio = posterBorderGapRatio
            self.posterBorderGapColor = posterBorderGapColor
            self.posterMarginRatio = posterMarginRatio
            self.titlePositionRaw = titlePositionRaw
            self.posterDecoration = posterDecoration
            self.backgroundColor = backgroundColor
            self.titleColor = titleColor
            self.subtitleColor = subtitleColor
            self.titleFontWeight = titleFontWeight
            self.subtitleFontWeight = subtitleFontWeight
            self.titleFontName = titleFontName
            self.subtitleFontName = subtitleFontName
            self.showTitle = showTitle
            self.showSubtitle = showSubtitle
            self.titleAlignmentRaw = titleAlignmentRaw
            self.subtitleAlignmentRaw = subtitleAlignmentRaw
            self.subtitleOffsetRatio = subtitleOffsetRatio
            self.mapStyleRaw = mapStyleRaw
            self.mapLanguageRaw = mapLanguageRaw
            self.hideLabels = hideLabels
            self.lineWidth = lineWidth
            self.defaultLineColor = defaultLineColor
            self.useColorPerWorkoutType = useColorPerWorkoutType
            self.useLineWidthPerWorkoutType = useLineWidthPerWorkoutType
            self.workoutTypeColorsRaw = workoutTypeColorsRaw
            self.workoutTypeLineWidths = workoutTypeLineWidths
            self.filterRaw = filterRaw
        }

        /// Mutate showTitle in place and return self for chaining
        @discardableResult
        mutating func withShowTitle(_ show: Bool) -> Configuration {
            showTitle = show
            return self
        }

        /// Mutate showSubtitle in place and return self for chaining
        @discardableResult
        mutating func withShowSubtitle(_ show: Bool) -> Configuration {
            showSubtitle = show
            return self
        }

        /// Mutate titleAlignmentRaw in place and return self for chaining
        @discardableResult
        mutating func withTitleAlignmentRaw(_ alignment: String?) -> Configuration {
            titleAlignmentRaw = alignment
            return self
        }

        /// Mutate subtitleAlignmentRaw in place and return self for chaining
        @discardableResult
        mutating func withSubtitleAlignmentRaw(_ alignment: String?) -> Configuration {
            subtitleAlignmentRaw = alignment
            return self
        }

        /// Mutate subtitleOffsetRatio in place and return self for chaining
        /// Negative values move subtitle up (closer to/overlapping title)
        /// Positive values move subtitle down (further from title)
        @discardableResult
        mutating func withSubtitleOffsetRatio(_ ratio: CGFloat) -> Configuration {
            subtitleOffsetRatio = ratio
            return self
        }

        /// Mutate titleFontName in place and return self for chaining
        @discardableResult
        mutating func withTitleFontName(_ fontName: String?) -> Configuration {
            titleFontName = fontName
            return self
        }

        /// Mutate subtitleFontName in place and return self for chaining
        @discardableResult
        mutating func withSubtitleFontName(_ fontName: String?) -> Configuration {
            subtitleFontName = fontName
            return self
        }

        /// Mutate titleFontRatio in place and return self for chaining
        @discardableResult
        mutating func withTitleFontRatio(_ ratio: CGFloat) -> Configuration {
            titleFontRatio = ratio
            return self
        }

        /// Mutate subtitleFontRatio in place and return self for chaining
        @discardableResult
        mutating func withSubtitleFontRatio(_ ratio: CGFloat) -> Configuration {
            subtitleFontRatio = ratio
            return self
        }

        /// Mutate titleColor in place and return self for chaining
        @discardableResult
        mutating func withTitleColor(_ color: UIColor) -> Configuration {
            titleColor = color
            return self
        }

        /// Mutate subtitleColor in place and return self for chaining
        @discardableResult
        mutating func withSubtitleColor(_ color: UIColor) -> Configuration {
            subtitleColor = color
            return self
        }

        /// Mutate outerBorder in place and return self for chaining
        @discardableResult
        mutating func withOuterBorder(_ border: BorderConfig) -> Configuration {
            outerBorder = border
            return self
        }

        /// Mutate innerBorder in place and return self for chaining
        @discardableResult
        mutating func withInnerBorder(_ border: BorderConfig) -> Configuration {
            innerBorder = border
            return self
        }

        /// Mutate borderGapRatio in place and return self for chaining
        @discardableResult
        mutating func withBorderGapRatio(_ ratio: CGFloat) -> Configuration {
            borderGapRatio = ratio
            return self
        }

        /// Mutate borderGapColor in place and return self for chaining
        @discardableResult
        mutating func withBorderGapColor(_ color: UIColor?) -> Configuration {
            borderGapColor = color
            return self
        }

        /// Mutate paddingRatio in place and return self for chaining
        @discardableResult
        mutating func withPaddingRatio(_ ratio: CGFloat) -> Configuration {
            paddingRatio = ratio
            return self
        }

        /// Mutate backgroundColor in place and return self for chaining
        @discardableResult
        mutating func withBackgroundColor(_ color: UIColor) -> Configuration {
            backgroundColor = color
            return self
        }

        /// Mutate titleAreaHeightRatio in place and return self for chaining
        @discardableResult
        mutating func withTitleAreaHeightRatio(_ ratio: CGFloat) -> Configuration {
            titleAreaHeightRatio = ratio
            return self
        }

        /// Mutate elementSpacingRatio in place and return self for chaining
        @discardableResult
        mutating func withElementSpacingRatio(_ ratio: CGFloat) -> Configuration {
            elementSpacingRatio = ratio
            return self
        }

        /// Mutate mapStyleRaw in place and return self for chaining
        @discardableResult
        mutating func withMapStyleRaw(_ style: String?) -> Configuration {
            mapStyleRaw = style
            return self
        }

        /// Mutate mapLanguageRaw in place and return self for chaining
        @discardableResult
        mutating func withMapLanguageRaw(_ language: String?) -> Configuration {
            mapLanguageRaw = language
            return self
        }

        /// Mutate hideLabels in place and return self for chaining
        @discardableResult
        mutating func withHideLabels(_ hide: Bool) -> Configuration {
            hideLabels = hide
            return self
        }

        /// Mutate lineWidth in place and return self for chaining
        @discardableResult
        mutating func withLineWidth(_ width: CGFloat) -> Configuration {
            lineWidth = width
            return self
        }

        /// Mutate defaultLineColor in place and return self for chaining
        @discardableResult
        mutating func withDefaultLineColor(_ color: UIColor) -> Configuration {
            defaultLineColor = color
            return self
        }

        /// Mutate useColorPerWorkoutType in place and return self for chaining
        @discardableResult
        mutating func withUseColorPerWorkoutType(_ use: Bool) -> Configuration {
            useColorPerWorkoutType = use
            return self
        }

        /// Mutate useLineWidthPerWorkoutType in place and return self for chaining
        @discardableResult
        mutating func withUseLineWidthPerWorkoutType(_ use: Bool) -> Configuration {
            useLineWidthPerWorkoutType = use
            return self
        }

        /// Mutate workoutTypeColorsRaw in place and return self for chaining
        @discardableResult
        mutating func withWorkoutTypeColorsRaw(_ colors: [String: UIColor]) -> Configuration {
            workoutTypeColorsRaw = colors
            return self
        }

        /// Mutate workoutTypeLineWidths in place and return self for chaining
        @discardableResult
        mutating func withWorkoutTypeLineWidths(_ widths: [String: CGFloat]) -> Configuration {
            workoutTypeLineWidths = widths
            return self
        }

        /// Mutate filterRaw in place and return self for chaining
        @discardableResult
        mutating func withFilterRaw(_ filter: String?) -> Configuration {
            filterRaw = filter
            return self
        }

        /// Mutate posterOuterBorder in place and return self for chaining
        @discardableResult
        mutating func withPosterOuterBorder(_ border: BorderConfig) -> Configuration {
            posterOuterBorder = border
            return self
        }

        /// Mutate posterInnerBorder in place and return self for chaining
        @discardableResult
        mutating func withPosterInnerBorder(_ border: BorderConfig) -> Configuration {
            posterInnerBorder = border
            return self
        }

        /// Mutate posterBorderGapRatio in place and return self for chaining
        @discardableResult
        mutating func withPosterBorderGapRatio(_ ratio: CGFloat) -> Configuration {
            posterBorderGapRatio = ratio
            return self
        }

        /// Mutate posterBorderGapColor in place and return self for chaining
        @discardableResult
        mutating func withPosterBorderGapColor(_ color: UIColor?) -> Configuration {
            posterBorderGapColor = color
            return self
        }

        /// Mutate posterMarginRatio in place and return self for chaining
        @discardableResult
        mutating func withPosterMarginRatio(_ ratio: CGFloat) -> Configuration {
            posterMarginRatio = ratio
            return self
        }

        /// Mutate titlePositionRaw in place and return self for chaining
        @discardableResult
        mutating func withTitlePositionRaw(_ position: String?) -> Configuration {
            titlePositionRaw = position
            return self
        }

        /// Mutate titlePosition in place and return self for chaining
        @discardableResult
        mutating func withTitlePosition(_ position: TitlePosition) -> Configuration {
            titlePositionRaw = position.rawValue
            return self
        }

        /// Mutate posterDecoration in place and return self for chaining
        @discardableResult
        mutating func withPosterDecoration(_ decoration: PosterDecoration) -> Configuration {
            posterDecoration = decoration
            return self
        }
    }

    // MARK: - Properties

    let configuration: Configuration

    // MARK: - Initialization

    init(configuration: Configuration) {
        self.configuration = configuration
    }

    // MARK: - Public Methods

    /// Decorate a map image with borders, padding, and title
    func decorate(
        mapImage: UIImage,
        title: String,
        subtitle: ExportSubtitleContent,
        targetSize: CGSize
    ) -> UIImage {
        let config = configuration
        let width = targetSize.width

        // Calculate dimensions from ratios
        let padding = width * config.paddingRatio
        let titleFontSize = width * config.titleFontRatio
        let subtitleFontSize = width * config.subtitleFontRatio
        let elementSpacing = width * config.elementSpacingRatio

        // Calculate poster margin (space between document edge and poster borders)
        let posterMargin = width * config.posterMarginRatio

        // Calculate poster border dimensions (around entire poster)
        let posterBorderMetrics = calculatePosterBorderMetrics(for: width)

        // Calculate map border dimensions (around the map image)
        let borderMetrics = calculateBorderMetrics(for: width)

        // Calculate content area (inside poster margin and poster borders)
        let contentInset = posterMargin + posterBorderMetrics.totalInset
        let contentWidth = width - (2 * contentInset)

        let maxTextWidth = max(0, contentWidth - (2 * padding))
        let subtitleText = subtitle.formatted()
        let titleFont = fittedFont(
            for: title,
            named: config.titleFontName,
            baseSize: titleFontSize,
            weight: config.titleFontWeight,
            maxWidth: maxTextWidth
        )
        let subtitleFont = fittedFont(
            for: subtitleText ?? "",
            named: config.subtitleFontName,
            baseSize: subtitleFontSize,
            weight: config.subtitleFontWeight,
            maxWidth: maxTextWidth
        )

        // Calculate title area height (use max of configured ratio and actual content height)
        let titleAreaHeight: CGFloat
        if config.showTitle || config.showSubtitle {
            let configuredHeight = width * config.titleAreaHeightRatio

            // Calculate actual content height needed
            var actualHeight = elementSpacing // Top padding
            if config.showTitle {
                let titleHeight = "Ag".size(withAttributes: [.font: titleFont]).height
                actualHeight += titleHeight
                if config.showSubtitle {
                    actualHeight += elementSpacing // Spacing between title and subtitle
                }
            }
            if config.showSubtitle {
                let subtitleHeight = "Ag".size(withAttributes: [.font: subtitleFont]).height
                actualHeight += subtitleHeight
                // Account for negative subtitle offset (which pushes subtitle up, reducing space needed)
                // but don't account for positive offset (which would extend below)
                let subtitleOffset = width * config.subtitleOffsetRatio
                if subtitleOffset > 0 {
                    actualHeight += subtitleOffset
                }
            }
            actualHeight += elementSpacing // Bottom padding

            titleAreaHeight = max(configuredHeight, actualHeight)
        } else {
            titleAreaHeight = padding // Just bottom padding if no title
        }

        // Calculate decoration height
        let decorationHeight = calculateDecorationHeight(for: width)
        let decorationSpacing = decorationHeight > 0 ? elementSpacing : 0

        // Calculate map area (inside map borders, inside content area)
        let mapAreaWidth = width - (2 * padding) - (2 * contentInset) - (2 * borderMetrics.totalInset)
        let mapAreaHeight = mapAreaWidth // Square map

        // Calculate extra height for decorations
        // If decoration is at same position as title, they share space; otherwise decoration adds extra height
        let decorationPosition = config.posterDecoration.position
        let titlePosition = config.titlePosition
        let extraDecorationHeight: CGFloat
        if decorationHeight > 0 {
            if (decorationPosition == .bottom && titlePosition == .bottom) ||
               (decorationPosition == .top && titlePosition == .top) {
                // Same position - decoration goes after title in same area
                extraDecorationHeight = decorationHeight + decorationSpacing
            } else {
                // Different positions - decoration needs its own area
                extraDecorationHeight = decorationHeight + decorationSpacing + padding
            }
        } else {
            extraDecorationHeight = 0
        }

        // Final canvas size
        let canvasWidth = width
        let canvasHeight = mapAreaHeight + (2 * padding) + (2 * contentInset) + (2 * borderMetrics.totalInset) + titleAreaHeight + extraDecorationHeight

        let finalSize = CGSize(width: canvasWidth, height: canvasHeight)

        let renderer = UIGraphicsImageRenderer(size: finalSize)
        let result = renderer.image { context in
            let cgContext = context.cgContext

            // 1. Fill background
            config.backgroundColor.setFill()
            cgContext.fill(CGRect(origin: .zero, size: finalSize))

            // 2. Draw poster borders (outermost borders around entire poster, inside the margin)
            let posterFrame = CGRect(
                x: posterMargin,
                y: posterMargin,
                width: finalSize.width - (2 * posterMargin),
                height: finalSize.height - (2 * posterMargin)
            )
            drawPosterBorders(in: cgContext, frame: posterFrame, metrics: posterBorderMetrics, imageWidth: width)

            // 3. Calculate positions based on title position
            let titlePosition = config.titlePosition

            // Content area starts after poster borders
            let contentX = contentInset

            let mapFrameWidth = contentWidth - (2 * padding)
            let mapFrameHeight = mapAreaHeight + (2 * borderMetrics.totalInset)

            let mapFrameX = contentX + padding
            let mapFrameY: CGFloat
            let titleAreaY: CGFloat
            var decorationAreaY: CGFloat = 0

            // Layout depends on title position and decoration position
            // Rule: title always comes before decoration when they share the same area
            if titlePosition == .top && decorationPosition == .top {
                // Both at top: title first, then decoration, then map
                titleAreaY = contentInset + padding
                decorationAreaY = titleAreaY + titleAreaHeight
                mapFrameY = decorationAreaY + decorationHeight + decorationSpacing
            } else if titlePosition == .top && decorationPosition == .bottom {
                // Title at top, decoration at bottom: title, map, decoration
                titleAreaY = contentInset + padding
                mapFrameY = titleAreaY + titleAreaHeight
                decorationAreaY = mapFrameY + mapFrameHeight + elementSpacing + padding
            } else if titlePosition == .bottom && decorationPosition == .top {
                // Decoration at top, title at bottom: decoration, map, title
                decorationAreaY = contentInset + padding
                mapFrameY = decorationAreaY + decorationHeight + decorationSpacing
                titleAreaY = mapFrameY + mapFrameHeight + elementSpacing
            } else {
                // Both at bottom (default): map, title, then decoration
                mapFrameY = contentInset + padding
                titleAreaY = mapFrameY + mapFrameHeight + elementSpacing
                decorationAreaY = titleAreaY + titleAreaHeight + decorationSpacing
            }

            let mapFrame = CGRect(x: mapFrameX, y: mapFrameY, width: mapFrameWidth, height: mapFrameHeight)

            // 4. Draw map borders
            drawBorders(in: cgContext, frame: mapFrame, metrics: borderMetrics, imageWidth: width)

            // 5. Draw map image (clipped to inner border corner radius if set)
            let mapImageRect = CGRect(
                x: mapFrame.minX + borderMetrics.totalInset,
                y: mapFrame.minY + borderMetrics.totalInset,
                width: mapAreaWidth,
                height: mapAreaHeight
            )

            if borderMetrics.mapClipCornerRadius > 0 {
                // Clip to rounded rect matching border corner radius
                cgContext.saveGState()
                let clipPath = UIBezierPath(roundedRect: mapImageRect, cornerRadius: borderMetrics.mapClipCornerRadius)
                cgContext.addPath(clipPath.cgPath)
                cgContext.clip()
                mapImage.draw(in: mapImageRect)
                cgContext.restoreGState()
            } else {
                mapImage.draw(in: mapImageRect)
            }

            // 6. Draw attribution in bottom-right corner of map area
            let attributionText = "© Stadia Maps © OpenMapTiles © OpenStreetMap"
            let attributionFontSize = width * 0.012 // Small font
            let attributionFont = UIFont.systemFont(ofSize: attributionFontSize, weight: .regular)
            let attributionColor = UIColor(white: 0.5, alpha: 0.8) // Gray
            let attributionAttributes: [NSAttributedString.Key: Any] = [
                .font: attributionFont,
                .foregroundColor: attributionColor
            ]
            let attributionSize = attributionText.size(withAttributes: attributionAttributes)
            let attributionPadding = width * 0.008
            let attributionX = mapImageRect.maxX - attributionSize.width - attributionPadding
            let attributionY = mapImageRect.maxY - attributionSize.height - attributionPadding
            attributionText.draw(at: CGPoint(x: attributionX, y: attributionY), withAttributes: attributionAttributes)

            // 7. Draw title and subtitle
            // Helper to calculate X position based on alignment
            func alignedX(textWidth: CGFloat, alignment: String?) -> CGFloat {
                switch alignment {
                case "Left":
                    return contentX + padding
                case "Right":
                    return contentX + contentWidth - padding - textWidth
                default: // Center
                    return contentX + (contentWidth - textWidth) / 2
                }
            }

            if config.showTitle {
                let titleAttributes: [NSAttributedString.Key: Any] = [
                    .font: titleFont,
                    .foregroundColor: config.titleColor
                ]
                let titleSize = title.size(withAttributes: titleAttributes)
                let titleX = alignedX(textWidth: titleSize.width, alignment: config.titleAlignmentRaw)
                let titleY = titleAreaY + elementSpacing

                title.draw(at: CGPoint(x: titleX, y: titleY), withAttributes: titleAttributes)

                // Draw subtitle below title
                if config.showSubtitle, let subtitleText = subtitleText {
                    let subtitleAttributes: [NSAttributedString.Key: Any] = [
                        .font: subtitleFont,
                        .foregroundColor: config.subtitleColor
                    ]
                    let subtitleSize = subtitleText.size(withAttributes: subtitleAttributes)
                    let subtitleX = alignedX(textWidth: subtitleSize.width, alignment: config.subtitleAlignmentRaw)
                    let subtitleOffset = width * config.subtitleOffsetRatio
                    let subtitleY = titleY + titleSize.height + elementSpacing + subtitleOffset

                    subtitleText.draw(at: CGPoint(x: subtitleX, y: subtitleY), withAttributes: subtitleAttributes)
                }
            } else if config.showSubtitle, let subtitleText = subtitleText {
                // Only subtitle, no title
                let subtitleAttributes: [NSAttributedString.Key: Any] = [
                    .font: subtitleFont,
                    .foregroundColor: config.subtitleColor
                ]
                let subtitleSize = subtitleText.size(withAttributes: subtitleAttributes)
                let subtitleX = alignedX(textWidth: subtitleSize.width, alignment: config.subtitleAlignmentRaw)
                let subtitleY = titleAreaY + elementSpacing

                subtitleText.draw(at: CGPoint(x: subtitleX, y: subtitleY), withAttributes: subtitleAttributes)
            }

            // 8. Draw poster decorations
            if decorationHeight > 0 {
                let decorationAreaRect = CGRect(
                    x: contentX + padding,
                    y: decorationAreaY,
                    width: contentWidth - (2 * padding),
                    height: decorationHeight
                )
                drawDecorations(in: cgContext, decorationAreaRect: decorationAreaRect, imageWidth: width)
            }
        }

        return result
    }

    // MARK: - Private Methods

    /// Create a font with the given name, falling back to system font if not found
    private func font(named fontName: String?, size: CGFloat, weight: UIFont.Weight) -> UIFont {
        if let fontName = fontName, let font = UIFont(name: fontName, size: size) {
            return font
        }
        return UIFont.systemFont(ofSize: size, weight: weight)
    }

    private func fittedFont(
        for text: String,
        named fontName: String?,
        baseSize: CGFloat,
        weight: UIFont.Weight,
        maxWidth: CGFloat
    ) -> UIFont {
        let baseFont = font(named: fontName, size: baseSize, weight: weight)
        guard maxWidth > 0 else { return baseFont }
        let baseWidth = text.size(withAttributes: [.font: baseFont]).width
        guard baseWidth > maxWidth, baseWidth > 0 else { return baseFont }
        let scale = maxWidth / baseWidth
        return font(named: fontName, size: baseSize * scale, weight: weight)
    }

    private struct BorderMetrics {
        let totalInset: CGFloat
        let outerWidth: CGFloat
        let outerCornerRadius: CGFloat
        let innerWidth: CGFloat
        let innerCornerRadius: CGFloat
        let gap: CGFloat
        let mapClipCornerRadius: CGFloat  // Corner radius to use when clipping the map image
    }

    private func calculateBorderMetrics(for width: CGFloat) -> BorderMetrics {
        let config = configuration
        let outerWidth = config.outerBorder.lineType != .none ? width * config.outerBorder.widthRatio : 0
        let outerCornerRadius = width * config.outerBorder.cornerRadiusRatio
        let innerWidth = config.innerBorder.lineType != .none ? width * config.innerBorder.widthRatio : 0
        let innerCornerRadius = width * config.innerBorder.cornerRadiusRatio
        let gap = (outerWidth > 0 && innerWidth > 0) ? width * config.borderGapRatio : 0

        let totalInset = outerWidth + gap + innerWidth

        // Calculate the corner radius to use when clipping the map image
        // If there's an inner border with corner radius, use that
        // Otherwise, derive from outer corner radius minus total inset
        let mapClipCornerRadius: CGFloat
        if innerCornerRadius > 0 {
            mapClipCornerRadius = innerCornerRadius
        } else if outerCornerRadius > 0 {
            mapClipCornerRadius = max(0, outerCornerRadius - totalInset)
        } else {
            mapClipCornerRadius = 0
        }

        return BorderMetrics(
            totalInset: totalInset,
            outerWidth: outerWidth,
            outerCornerRadius: outerCornerRadius,
            innerWidth: innerWidth,
            innerCornerRadius: innerCornerRadius,
            gap: gap,
            mapClipCornerRadius: mapClipCornerRadius
        )
    }

    private func drawBorders(in context: CGContext, frame: CGRect, metrics: BorderMetrics, imageWidth: CGFloat) {
        let config = configuration

        // Fill gap between borders if both borders are visible and gap color is specified
        if config.outerBorder.lineType != .none && config.innerBorder.lineType != .none &&
           metrics.gap > 0 && config.borderGapColor != nil {
            let gapColor = config.borderGapColor ?? config.backgroundColor
            gapColor.setFill()

            // The gap area is between outer border (inside edge) and inner border (outside edge)
            let outerGapInset = metrics.outerWidth
            let innerGapInset = metrics.outerWidth + metrics.gap

            let outerGapRect = frame.insetBy(dx: outerGapInset, dy: outerGapInset)
            let innerGapRect = frame.insetBy(dx: innerGapInset, dy: innerGapInset)

            // Create a path for the gap area (ring between outer and inner)
            let outerPath: UIBezierPath
            let innerPath: UIBezierPath

            if metrics.outerCornerRadius > 0 {
                // Adjust corner radius for the inside of outer border
                let adjustedOuterRadius = max(0, metrics.outerCornerRadius - metrics.outerWidth / 2)
                outerPath = UIBezierPath(roundedRect: outerGapRect, cornerRadius: adjustedOuterRadius)
            } else {
                outerPath = UIBezierPath(rect: outerGapRect)
            }

            if metrics.innerCornerRadius > 0 {
                // Adjust corner radius for the outside of inner border
                let adjustedInnerRadius = max(0, metrics.innerCornerRadius + metrics.innerWidth / 2)
                innerPath = UIBezierPath(roundedRect: innerGapRect, cornerRadius: adjustedInnerRadius)
            } else {
                innerPath = UIBezierPath(rect: innerGapRect)
            }

            // Use even-odd fill rule to create the ring shape
            outerPath.append(innerPath.reversing())
            context.addPath(outerPath.cgPath)
            context.fillPath(using: .evenOdd)
        }

        // Draw outer border
        if config.outerBorder.lineType != .none && metrics.outerWidth > 0 {
            drawSingleBorder(
                in: context,
                rect: frame.insetBy(dx: metrics.outerWidth / 2, dy: metrics.outerWidth / 2),
                borderConfig: config.outerBorder,
                width: metrics.outerWidth,
                cornerRadius: metrics.outerCornerRadius
            )
        }

        // Draw inner border
        if config.innerBorder.lineType != .none && metrics.innerWidth > 0 {
            let innerInset = metrics.outerWidth + metrics.gap + (metrics.innerWidth / 2)
            drawSingleBorder(
                in: context,
                rect: frame.insetBy(dx: innerInset, dy: innerInset),
                borderConfig: config.innerBorder,
                width: metrics.innerWidth,
                cornerRadius: metrics.innerCornerRadius
            )
        }

        // Reset line dash
        context.setLineDash(phase: 0, lengths: [])
    }

    private func drawSingleBorder(
        in context: CGContext,
        rect: CGRect,
        borderConfig: BorderConfig,
        width: CGFloat,
        cornerRadius: CGFloat
    ) {
        borderConfig.color.setStroke()
        context.setLineWidth(width)

        // Configure line style
        switch borderConfig.lineType {
        case .none:
            return
        case .normal:
            context.setLineDash(phase: 0, lengths: [])
        case .dashed:
            let dashLength = width * 4
            let gapLength = width * 3
            context.setLineDash(phase: 0, lengths: [dashLength, gapLength])
        case .dotted:
            let dotLength = width
            let gapLength = width * 2
            context.setLineDash(phase: 0, lengths: [dotLength, gapLength])
        }

        // Draw with or without corner radius
        if cornerRadius > 0 {
            let path = UIBezierPath(roundedRect: rect, cornerRadius: cornerRadius)
            context.addPath(path.cgPath)
            context.strokePath()
        } else {
            context.stroke(rect)
        }
    }

    // MARK: - Poster Border Methods

    private func calculatePosterBorderMetrics(for width: CGFloat) -> BorderMetrics {
        let config = configuration
        let outerWidth = config.posterOuterBorder.lineType != .none ? width * config.posterOuterBorder.widthRatio : 0
        let outerCornerRadius = width * config.posterOuterBorder.cornerRadiusRatio
        let innerWidth = config.posterInnerBorder.lineType != .none ? width * config.posterInnerBorder.widthRatio : 0
        let innerCornerRadius = width * config.posterInnerBorder.cornerRadiusRatio
        let gap = (outerWidth > 0 && innerWidth > 0) ? width * config.posterBorderGapRatio : 0

        let totalInset = outerWidth + gap + innerWidth

        return BorderMetrics(
            totalInset: totalInset,
            outerWidth: outerWidth,
            outerCornerRadius: outerCornerRadius,
            innerWidth: innerWidth,
            innerCornerRadius: innerCornerRadius,
            gap: gap,
            mapClipCornerRadius: 0  // Not used for poster borders
        )
    }

    private func drawPosterBorders(in context: CGContext, frame: CGRect, metrics: BorderMetrics, imageWidth: CGFloat) {
        let config = configuration

        // Fill gap between poster borders if both borders are visible and gap color is specified
        if config.posterOuterBorder.lineType != .none && config.posterInnerBorder.lineType != .none &&
           metrics.gap > 0 && config.posterBorderGapColor != nil {
            let gapColor = config.posterBorderGapColor ?? config.backgroundColor
            gapColor.setFill()

            let outerGapInset = metrics.outerWidth
            let innerGapInset = metrics.outerWidth + metrics.gap

            let outerGapRect = frame.insetBy(dx: outerGapInset, dy: outerGapInset)
            let innerGapRect = frame.insetBy(dx: innerGapInset, dy: innerGapInset)

            let outerPath: UIBezierPath
            let innerPath: UIBezierPath

            if metrics.outerCornerRadius > 0 {
                let adjustedOuterRadius = max(0, metrics.outerCornerRadius - metrics.outerWidth / 2)
                outerPath = UIBezierPath(roundedRect: outerGapRect, cornerRadius: adjustedOuterRadius)
            } else {
                outerPath = UIBezierPath(rect: outerGapRect)
            }

            if metrics.innerCornerRadius > 0 {
                let adjustedInnerRadius = max(0, metrics.innerCornerRadius + metrics.innerWidth / 2)
                innerPath = UIBezierPath(roundedRect: innerGapRect, cornerRadius: adjustedInnerRadius)
            } else {
                innerPath = UIBezierPath(rect: innerGapRect)
            }

            outerPath.append(innerPath.reversing())
            context.addPath(outerPath.cgPath)
            context.fillPath(using: .evenOdd)
        }

        // Draw poster outer border
        if config.posterOuterBorder.lineType != .none && metrics.outerWidth > 0 {
            drawSingleBorder(
                in: context,
                rect: frame.insetBy(dx: metrics.outerWidth / 2, dy: metrics.outerWidth / 2),
                borderConfig: config.posterOuterBorder,
                width: metrics.outerWidth,
                cornerRadius: metrics.outerCornerRadius
            )
        }

        // Draw poster inner border
        if config.posterInnerBorder.lineType != .none && metrics.innerWidth > 0 {
            let innerInset = metrics.outerWidth + metrics.gap + (metrics.innerWidth / 2)
            drawSingleBorder(
                in: context,
                rect: frame.insetBy(dx: innerInset, dy: innerInset),
                borderConfig: config.posterInnerBorder,
                width: metrics.innerWidth,
                cornerRadius: metrics.innerCornerRadius
            )
        }

        // Reset line dash
        context.setLineDash(phase: 0, lengths: [])
    }

    // MARK: - Poster Decoration Methods

    /// Calculate the height needed for decorations
    private func calculateDecorationHeight(for width: CGFloat) -> CGFloat {
        let config = configuration
        guard config.posterDecoration.type != .none else { return 0 }

        let decoration = config.posterDecoration
        let size = width * decoration.sizeRatio

        switch decoration.type {
        case .none:
            return 0
        case .threeSquares, .oneSquare:
            return size
        case .threeCircles, .oneCircle:
            return size
        case .oneRectangle:
            return size * 0.6  // Rectangle is shorter than square
        case .fullWidthRectangle:
            return size * 0.3  // Full width bar is thin
        }
    }

    /// Draw poster decorations
    private func drawDecorations(
        in context: CGContext,
        decorationAreaRect: CGRect,
        imageWidth: CGFloat
    ) {
        let config = configuration
        let decoration = config.posterDecoration
        guard decoration.type != .none else { return }

        let size = imageWidth * decoration.sizeRatio
        let spacing = imageWidth * decoration.spacingRatio
        let colors = [decoration.color1, decoration.color2, decoration.color3]

        // Calculate total width of decoration shapes
        let totalWidth: CGFloat
        switch decoration.type {
        case .none:
            return
        case .threeSquares, .threeCircles:
            totalWidth = (size * 3) + (spacing * 2)
        case .oneRectangle:
            totalWidth = size * 2.5  // Rectangle is wider than tall
        case .oneCircle, .oneSquare:
            totalWidth = size
        case .fullWidthRectangle:
            totalWidth = decorationAreaRect.width  // Full width
        }

        // Calculate starting X position based on alignment
        let startX: CGFloat
        switch decoration.alignment {
        case .left:
            startX = decorationAreaRect.minX
        case .center:
            startX = decorationAreaRect.midX - (totalWidth / 2)
        case .right:
            startX = decorationAreaRect.maxX - totalWidth
        }

        // Center decorations vertically in the decoration area
        let centerY = decorationAreaRect.midY

        // Draw based on type
        switch decoration.type {
        case .none:
            break

        case .threeSquares:
            for i in 0..<3 {
                let x = startX + (CGFloat(i) * (size + spacing))
                let y = centerY - (size / 2)
                let rect = CGRect(x: x, y: y, width: size, height: size)
                colors[i].setFill()
                context.fill(rect)
            }

        case .threeCircles:
            for i in 0..<3 {
                let x = startX + (CGFloat(i) * (size + spacing))
                let y = centerY - (size / 2)
                let rect = CGRect(x: x, y: y, width: size, height: size)
                colors[i].setFill()
                context.fillEllipse(in: rect)
            }

        case .oneSquare:
            let y = centerY - (size / 2)
            let rect = CGRect(x: startX, y: y, width: size, height: size)
            colors[0].setFill()
            context.fill(rect)

        case .oneCircle:
            let y = centerY - (size / 2)
            let rect = CGRect(x: startX, y: y, width: size, height: size)
            colors[0].setFill()
            context.fillEllipse(in: rect)

        case .oneRectangle:
            let rectHeight = size * 0.6
            let rectWidth = size * 2.5
            let y = centerY - (rectHeight / 2)
            let rect = CGRect(x: startX, y: y, width: rectWidth, height: rectHeight)
            colors[0].setFill()
            context.fill(rect)

        case .fullWidthRectangle:
            let rectHeight = size * 0.3
            let y = centerY - (rectHeight / 2)
            let rect = CGRect(x: decorationAreaRect.minX, y: y, width: decorationAreaRect.width, height: rectHeight)
            colors[0].setFill()
            context.fill(rect)
        }
    }
}

// MARK: - Preset Configurations

extension MapExportDecorator.Configuration {

    // MARK: - Raw
    /// Raw - Just the map with minimal white padding. No text, no borders.
    static let raw = MapExportDecorator.Configuration(
        paddingRatio: 0.03,
        titleAreaHeightRatio: 0,
        titleFontRatio: 0,
        subtitleFontRatio: 0,
        elementSpacingRatio: 0,
        outerBorder: .none,
        innerBorder: .none,
        borderGapRatio: 0,
        borderGapColor: nil,
        backgroundColor: .white,
        titleColor: .black,
        subtitleColor: .black,
        titleFontWeight: .regular,
        subtitleFontWeight: .regular,
        titleFontName: nil,
        subtitleFontName: nil,
        showTitle: false,
        showSubtitle: false,
        titleAlignmentRaw: "Center",
        subtitleAlignmentRaw: "Center",
        mapStyleRaw: nil,
        mapLanguageRaw: nil,
        hideLabels: false,
        lineWidth: 3.0,
        defaultLineColor: .systemBlue,
        useColorPerWorkoutType: true,
        useLineWidthPerWorkoutType: false,
        workoutTypeColorsRaw: [:],
        workoutTypeLineWidths: [:],
        filterRaw: nil
    )

    // MARK: - Art Deco
    /// Art Deco - Gold on navy, double border, sepia filter. Inspired by 1920s-1930s glamour.
    static let artDeco = MapExportDecorator.Configuration(
        paddingRatio: 0.04,
        titleAreaHeightRatio: 0.15,
        titleFontRatio: 0.066,
        subtitleFontRatio: 0.040,
        elementSpacingRatio: 0.004,
        outerBorder: BorderConfig(lineType: .normal, color: UIColor(red: 0.831, green: 0.686, blue: 0.216, alpha: 1), widthRatio: 0.012, cornerRadiusRatio: 0.008),
        innerBorder: BorderConfig(lineType: .normal, color: UIColor(red: 0.831, green: 0.686, blue: 0.216, alpha: 1), widthRatio: 0.004, cornerRadiusRatio: 0.004),
        borderGapRatio: 0.008,
        borderGapColor: UIColor(red: 0.788, green: 0.635, blue: 0.153, alpha: 1),
        backgroundColor: UIColor(red: 0.051, green: 0.106, blue: 0.165, alpha: 1),
        titleColor: UIColor(red: 0.831, green: 0.686, blue: 0.216, alpha: 1),
        subtitleColor: UIColor(red: 0.788, green: 0.635, blue: 0.153, alpha: 1),
        titleFontWeight: .bold,
        subtitleFontWeight: .regular,
        titleFontName: "PlayfairDisplayRoman-Bold",
        subtitleFontName: "PlayfairDisplay-Regular",
        showTitle: true,
        showSubtitle: true,
        titleAlignmentRaw: "Center",
        subtitleAlignmentRaw: "Center",
        mapStyleRaw: "stamen_watercolor",
        mapLanguageRaw: nil,
        hideLabels: false,
        lineWidth: 3.0,
        defaultLineColor: UIColor(red: 0.051, green: 0.106, blue: 0.165, alpha: 1), // Gold to match borders
        useColorPerWorkoutType: false,
        useLineWidthPerWorkoutType: false,
        workoutTypeColorsRaw: [:],
        workoutTypeLineWidths: [:],
        filterRaw: "Sepia"
    )

    // MARK: - Art Nouveau
    /// Art Nouveau - Olive/gold tones, organic curves, sepia filter. Inspired by 1890-1910 movement.
    static let artNouveau = MapExportDecorator.Configuration(
        paddingRatio: 0.05,
        titleAreaHeightRatio: 0.15,
        titleFontRatio: 0.074,
        subtitleFontRatio: 0.044,
        elementSpacingRatio: 0.008,
        outerBorder: BorderConfig(lineType: .normal, color: UIColor(red: 0.361, green: 0.478, blue: 0.290, alpha: 1), widthRatio: 0.006, cornerRadiusRatio: 0.016),
        innerBorder: BorderConfig(lineType: .normal, color: UIColor(red: 0.361, green: 0.478, blue: 0.290, alpha: 1), widthRatio: 0.002, cornerRadiusRatio: 0.010),
        borderGapRatio: 0.012,
        borderGapColor: UIColor(red: 0.659, green: 0.584, blue: 0.431, alpha: 1),
        backgroundColor: UIColor(red: 0.961, green: 0.941, blue: 0.882, alpha: 1),
        titleColor: UIColor(red: 0.361, green: 0.478, blue: 0.290, alpha: 1),
        subtitleColor: UIColor(red: 0.545, green: 0.451, blue: 0.333, alpha: 1),
        titleFontWeight: .regular,
        subtitleFontWeight: .regular,
        titleFontName: "Cinzel-Regular",
        subtitleFontName: "CormorantGaramond-Italic",
        showTitle: true,
        showSubtitle: true,
        titleAlignmentRaw: "Center",
        subtitleAlignmentRaw: "Center",
        subtitleOffsetRatio: -0.02,
        mapStyleRaw: nil,
        mapLanguageRaw: nil,
        hideLabels: false,
        lineWidth: 3.0,
        defaultLineColor: UIColor(red: 0.361, green: 0.478, blue: 0.290, alpha: 1), // Olive green to match borders
        useColorPerWorkoutType: false,
        useLineWidthPerWorkoutType: false,
        workoutTypeColorsRaw: [:],
        workoutTypeLineWidths: [:],
        filterRaw: "Sepia"
    )

    // MARK: - Bauhaus
    /// Bauhaus - Thick black border, left-aligned, primary colors. Inspired by German design school (1919-1933).
    static let bauhaus = MapExportDecorator.Configuration(
        paddingRatio: 0.04,
        titleAreaHeightRatio: 0.14,
        titleFontRatio: 0.100,
        subtitleFontRatio: 0.044,
        elementSpacingRatio: 0.010,
        outerBorder: BorderConfig(lineType: .normal, color: UIColor(red: 0.0, green: 51.0/255.0, blue: 151.0/255.0, alpha: 1), widthRatio: 0.020, cornerRadiusRatio: 0),
        innerBorder: .none,
        borderGapRatio: 0.012,
        borderGapColor: nil,
        posterOuterBorder: BorderConfig(lineType: .normal, color: .black, widthRatio: 0.010, cornerRadiusRatio: 0),
        titlePositionRaw: "top",
        posterDecoration: PosterDecoration(
            type: .oneSquare,
            position: .bottom,
            alignment: .right,
            color1: UIColor(red: 0.0, green: 51.0/255.0, blue: 151.0/255.0, alpha: 1),
            color2: UIColor.systemCyan,
            color3: UIColor.systemMint,
            sizeRatio: 0.08,
            spacingRatio: 0.02
        ),
        backgroundColor: .white,
        titleColor: .black,
        subtitleColor: UIColor(red: 0.898, green: 0.224, blue: 0.208, alpha: 1),
        titleFontWeight: .regular,
        subtitleFontWeight: .regular,
        titleFontName: "Futura-CondensedExtraBold",
        subtitleFontName: "Futura-Medium",
        showTitle: true,
        showSubtitle: true,
        titleAlignmentRaw: "Left",
        subtitleAlignmentRaw: "Left",
        subtitleOffsetRatio: -0.025,
        mapStyleRaw: "bauhaus",
        mapLanguageRaw: nil,
        hideLabels: false,
        lineWidth: 4.0,
        defaultLineColor: UIColor(red: 1.0, green: 0.87, blue: 0.0, alpha: 1.0), // UIColor(red: 0.898, green: 0.224, blue: 0.208, alpha: 1), // Bauhaus red to match subtitle
        useColorPerWorkoutType: false,
        useLineWidthPerWorkoutType: false,
        workoutTypeColorsRaw: [:],
        workoutTypeLineWidths: [:],
        filterRaw: nil
    )

    // MARK: - Cyberpunk
    /// Cyberpunk - Neon pink/green on black. Inspired by dystopian tech-noir aesthetics.
    static let cyberpunk = MapExportDecorator.Configuration(
        paddingRatio: 0.028,
        titleAreaHeightRatio: 0.12,
        titleFontRatio: 0.052,
        subtitleFontRatio: 0.035,
        elementSpacingRatio: 0.015,
        outerBorder: BorderConfig(lineType: .normal, color: UIColor(red: 0.0, green: 1.0, blue: 0.624, alpha: 1), widthRatio: 0.004, cornerRadiusRatio: 0),
        innerBorder: .none,
        borderGapRatio: 0.012,
        borderGapColor: nil,
        backgroundColor: UIColor(red: 0.051, green: 0.051, blue: 0.071, alpha: 1),
        titleColor: .white,
        subtitleColor: UIColor(red: 0.0, green: 1.0, blue: 0.624, alpha: 1),
        titleFontWeight: .black,
        subtitleFontWeight: .regular,
        titleFontName: "VT323-Regular",
        subtitleFontName: "ShareTechMono-Regular",
        showTitle: true,
        showSubtitle: true,
        titleAlignmentRaw: "Left",
        subtitleAlignmentRaw: "Right",
        subtitleOffsetRatio: -0.015,
        mapStyleRaw: nil,
        mapLanguageRaw: nil,
        hideLabels: false,
        lineWidth: 3.0,
        defaultLineColor: UIColor(red: 1.0, green: 0.0, blue: 0.502, alpha: 1),
        useColorPerWorkoutType: false,
        useLineWidthPerWorkoutType: false,
        workoutTypeColorsRaw: [:],
        workoutTypeLineWidths: [:],
        filterRaw: nil
    )

    // MARK: - Glitch
    /// Glitch - RGB aesthetics, white on black. Digital corruption as aesthetic.
    static let glitch = MapExportDecorator.Configuration(
        paddingRatio: 0.07,
        titleAreaHeightRatio: 0.14,
        titleFontRatio: 0.096,
        subtitleFontRatio: 0.052,
        elementSpacingRatio: 0.035,
        outerBorder: BorderConfig(lineType: .normal, color: .white, widthRatio: 0.007, cornerRadiusRatio: 0),
        innerBorder: .none,
        borderGapRatio: 0.012,
        borderGapColor: nil,
        posterOuterBorder: BorderConfig(lineType: .normal, color: .white, widthRatio: 0.015, cornerRadiusRatio: 0),
        posterDecoration: PosterDecoration(
            type: .threeCircles,
            position: .top,
            alignment: .left,
            color1: UIColor.systemBlue,
            color2: UIColor.systemCyan,
            color3: UIColor.systemMint,
            sizeRatio: 0.08,
            spacingRatio: 0.02
        ),
        backgroundColor: UIColor(red: 0.167, green: 0.167, blue: 0.367, alpha: 1),
        titleColor: .white,
        subtitleColor: UIColor(red: 1.0, green: 1.0, blue: 1.0, alpha: 1),
        titleFontWeight: .regular,
        subtitleFontWeight: .regular,
        titleFontName: "AvenirNext-Heavy",
        subtitleFontName: "AvenirNext-UltraLight",
        showTitle: true,
        showSubtitle: true,
        titleAlignmentRaw: "Left",
        subtitleAlignmentRaw: "Left",
        subtitleOffsetRatio: -0.045,
        mapStyleRaw: nil,
        mapLanguageRaw: nil,
        hideLabels: false,
        lineWidth: 6.0,
        defaultLineColor: UIColor(red: 0.167, green: 0.167, blue: 0.367, alpha: 1),
        useColorPerWorkoutType: false,
        useLineWidthPerWorkoutType: false,
        workoutTypeColorsRaw: [:],
        workoutTypeLineWidths: [:],
        filterRaw: "Film"
    )

    // MARK: - Japanese
    /// Japanese Minimalism - Extra padding, right-aligned, minimal. Inspired by Zen aesthetics.
    static let japanese = MapExportDecorator.Configuration(
        paddingRatio: 0.08,
        titleAreaHeightRatio: 0.14,
        titleFontRatio: 0.064,
        subtitleFontRatio: 0.028,
        elementSpacingRatio: 0.015,
        outerBorder: BorderConfig(lineType: .normal, color: UIColor(red: 0.816, green: 0.816, blue: 0.816, alpha: 1), widthRatio: 0.002, cornerRadiusRatio: 0),
        innerBorder: .none,
        borderGapRatio: 0.012,
        borderGapColor: nil,
        backgroundColor: UIColor(red: 0.980, green: 0.976, blue: 0.965, alpha: 1),
        titleColor: UIColor(red: 0.173, green: 0.173, blue: 0.173, alpha: 1),
        subtitleColor: UIColor(red: 0.545, green: 0.545, blue: 0.545, alpha: 1),
        titleFontWeight: .ultraLight,
        subtitleFontWeight: .light,
        titleFontName: "WorkSansRoman-ExtraLight",
        subtitleFontName: "WorkSansRoman-Light",
        showTitle: true,
        showSubtitle: true,
        titleAlignmentRaw: "Right",
        subtitleAlignmentRaw: "Right",
        mapStyleRaw: "nihon",
        mapLanguageRaw: nil,
        hideLabels: false,
        lineWidth: 4.0,
        defaultLineColor: UIColor.black,
        useColorPerWorkoutType: false,
        useLineWidthPerWorkoutType: false,
        workoutTypeColorsRaw: [:],
        workoutTypeLineWidths: [:],
        filterRaw: "Faded"
    )

    // MARK: - Mondrian
    /// Mondrian / De Stijl - Title on top, three-square decorations. Inspired by Piet Mondrian (1917-1931).
    static let mondrian = MapExportDecorator.Configuration(
        paddingRatio: 0.06,
        titleAreaHeightRatio: 0.14,
        titleFontRatio: 0.096,
        subtitleFontRatio: 0.036,
        elementSpacingRatio: 0.012,
        outerBorder: BorderConfig(lineType: .normal, color: UIColor(red: 0.816, green: 0.816, blue: 0.816, alpha: 1), widthRatio: 0.002, cornerRadiusRatio: 0),
        innerBorder: .none,
        borderGapRatio: 0.012,
        borderGapColor: nil,
        titlePositionRaw: "top",
        posterDecoration: PosterDecoration(
            type: .threeSquares,
            position: .bottom,
            alignment: .left,
            color1: UIColor(red: 0.898, green: 0.722, blue: 0.165, alpha: 1),
            color2: UIColor(red: 0.180, green: 0.314, blue: 0.565, alpha: 1),
            color3: UIColor(red: 0.769, green: 0.271, blue: 0.212, alpha: 1),
            sizeRatio: 0.06,
            spacingRatio: 0.0
        ),
        backgroundColor: UIColor(red: 0.961, green: 0.953, blue: 0.933, alpha: 1),
        titleColor: UIColor(red: 0.102, green: 0.102, blue: 0.102, alpha: 1),
        subtitleColor: UIColor(red: 0.102, green: 0.102, blue: 0.102, alpha: 1),
        titleFontWeight: .black,
        subtitleFontWeight: .medium,
        titleFontName: "Inter-Black",
        subtitleFontName: "Inter-Medium",
        showTitle: true,
        showSubtitle: true,
        titleAlignmentRaw: "Left",
        subtitleAlignmentRaw: "Left",
        mapStyleRaw: "toner_lite",
        mapLanguageRaw: nil,
        hideLabels: false,
        lineWidth: 3.0,
        defaultLineColor: UIColor(red: 0.180, green: 0.314, blue: 0.565, alpha: 1),
        useColorPerWorkoutType: false,
        useLineWidthPerWorkoutType: false,
        workoutTypeColorsRaw: [:],
        workoutTypeLineWidths: [:],
        filterRaw: "Grayscale"
    )

    // MARK: - Newspaper
    /// Vintage Newspaper - Grayscale, sepia, serif style. Inspired by early 20th century broadsheets.
    static let newspaper = MapExportDecorator.Configuration(
        paddingRatio: 0.04,
        titleAreaHeightRatio: 0.14,
        titleFontRatio: 0.086,
        subtitleFontRatio: 0.035,
        elementSpacingRatio: 0.015,
        outerBorder: BorderConfig(lineType: .normal, color: UIColor(red: 0.102, green: 0.102, blue: 0.102, alpha: 1), widthRatio: 0.002, cornerRadiusRatio: 0),
        innerBorder: .none,
        borderGapRatio: 0.012,
        borderGapColor: nil,
        titlePositionRaw: "top",
        posterDecoration: PosterDecoration(
            type: .fullWidthRectangle,
            position: .top,
            alignment: .right,
            color1: UIColor.black,
            color2: UIColor.systemCyan,
            color3: UIColor.systemMint,
            sizeRatio: 0.04,
            spacingRatio: 0.02
        ),
        backgroundColor: UIColor(red: 0.961, green: 0.941, blue: 0.902, alpha: 1),
        titleColor: UIColor(red: 0.102, green: 0.102, blue: 0.102, alpha: 1),
        subtitleColor: UIColor(red: 0.200, green: 0.200, blue: 0.200, alpha: 1),
        titleFontWeight: .black,
        subtitleFontWeight: .regular,
        titleFontName: "Didot-Bold",
        subtitleFontName: "TimesNewRomanPSMT",
        showTitle: true,
        showSubtitle: true,
        titleAlignmentRaw: "Center",
        subtitleAlignmentRaw: "Center",
        //mapStyleRaw: "stamen_toner",
        mapStyleRaw: "toner_minimal",
        mapLanguageRaw: nil,
        hideLabels: false,
        lineWidth: 4.0,
        defaultLineColor: UIColor.black,
        useColorPerWorkoutType: false,
        useLineWidthPerWorkoutType: false,
        workoutTypeColorsRaw: [:],
        workoutTypeLineWidths: [:],
        filterRaw: "Faded"
    )

    // MARK: - Noir
    /// Film Noir - Black/white, high contrast, grayscale filter. Inspired by 1940s-50s cinema.
    static let noir = MapExportDecorator.Configuration(
        paddingRatio: 0.04,
        titleAreaHeightRatio: 0.15,
        titleFontRatio: 0.100,
        subtitleFontRatio: 0.040,
        elementSpacingRatio: 0.020,
        outerBorder: BorderConfig(lineType: .normal, color: .white, widthRatio: 0.010, cornerRadiusRatio: 0),
        innerBorder: BorderConfig(lineType: .normal, color: .white, widthRatio: 0.004, cornerRadiusRatio: 0),
        borderGapRatio: 0.008,
        borderGapColor: UIColor(red: 0.05, green: 0.05, blue: 0.05, alpha: 1.0),
        backgroundColor: UIColor(red: 0.05, green: 0.05, blue: 0.05, alpha: 1.0),
        titleColor: .white,
        subtitleColor: UIColor(red: 0.800, green: 0.800, blue: 0.800, alpha: 1),
        titleFontWeight: .regular,
        subtitleFontWeight: .regular,
        titleFontName: "SpaceGrotesk-Light_Bold",
        subtitleFontName: "SpaceGrotesk-Light_Regular",
        showTitle: true,
        showSubtitle: true,
        titleAlignmentRaw: "Center",
        subtitleAlignmentRaw: "Center",
        mapStyleRaw: "toner_black",
        mapLanguageRaw: nil,
        hideLabels: false,
        lineWidth: 6.0,
        defaultLineColor: .white,
        useColorPerWorkoutType: false,
        useLineWidthPerWorkoutType: false,
        workoutTypeColorsRaw: [:],
        workoutTypeLineWidths: [:],
        filterRaw: "Grayscale"
    )

    // MARK: - Nordic
    /// Nordic - Sage green, dusty rose, rounded corners. Inspired by Scandinavian design.
    static let nordic = MapExportDecorator.Configuration(
        paddingRatio: 0.04,
        titleAreaHeightRatio: 0.14,
        titleFontRatio: 0.080,
        subtitleFontRatio: 0.036,
        elementSpacingRatio: 0.012,
        outerBorder: BorderConfig(lineType: .normal, color: UIColor(red: 0.290, green: 0.365, blue: 0.322, alpha: 1), widthRatio: 0.006, cornerRadiusRatio: 0.03),
        innerBorder: .none,
        borderGapRatio: 0.012,
        borderGapColor: nil,
        backgroundColor: UIColor(red: 0.949, green: 0.941, blue: 0.922, alpha: 1),
        titleColor: UIColor(red: 0.290, green: 0.365, blue: 0.322, alpha: 1),
        subtitleColor: UIColor(red: 0.490, green: 0.561, blue: 0.522, alpha: 1),
        titleFontWeight: .medium,
        subtitleFontWeight: .regular,
        titleFontName: "Nunito-Medium",
        subtitleFontName: "Nunito-Regular",
        showTitle: true,
        showSubtitle: true,
        titleAlignmentRaw: "Center",
        subtitleAlignmentRaw: "Center",
        subtitleOffsetRatio: -0.015,
        mapStyleRaw: "osm_bright",
        mapLanguageRaw: nil,
        hideLabels: false,
        lineWidth: 3.0,
        defaultLineColor: UIColor(red: 0.290, green: 0.365, blue: 0.322, alpha: 1),
        useColorPerWorkoutType: false,
        useLineWidthPerWorkoutType: false,
        workoutTypeColorsRaw: [:],
        workoutTypeLineWidths: [:],
        filterRaw: "Faded"
    )

    // MARK: - Polaroid
    /// Polaroid - White border, extra bottom padding. Inspired by instant film photography.
    static let polaroid = MapExportDecorator.Configuration(
        paddingRatio: 0.04,
        titleAreaHeightRatio: 0.18,
        titleFontRatio: 0.064,
        subtitleFontRatio: 0.044,
        elementSpacingRatio: 0.004,
        outerBorder: .none,
        innerBorder: .none,
        borderGapRatio: 0.012,
        borderGapColor: nil,
        backgroundColor: UIColor(red: 0.980, green: 0.980, blue: 0.980, alpha: 1),
        titleColor: UIColor(red: 0.173, green: 0.243, blue: 0.314, alpha: 1),
        subtitleColor: UIColor(red: 0.365, green: 0.427, blue: 0.494, alpha: 1),
        titleFontWeight: .regular,
        subtitleFontWeight: .regular,
        titleFontName: "PermanentMarker-Regular",
        subtitleFontName: "Caveat-Regular",
        showTitle: true,
        showSubtitle: true,
        titleAlignmentRaw: "Left",
        subtitleAlignmentRaw: "Left",
        mapStyleRaw: "outdoors",
        mapLanguageRaw: nil,
        hideLabels: false,
        lineWidth: 3.0,
        defaultLineColor: UIColor(red: 0.173, green: 0.243, blue: 0.314, alpha: 1), // Navy blue to match title
        useColorPerWorkoutType: false,
        useLineWidthPerWorkoutType: false,
        workoutTypeColorsRaw: [:],
        workoutTypeLineWidths: [:],
        filterRaw: "Faded"
    )

    // MARK: - Pop Art
    /// Pop Art - Bold primary colors, thick black borders. Inspired by Warhol and Lichtenstein.
    static let popArt = MapExportDecorator.Configuration(
        paddingRatio: 0.03,
        titleAreaHeightRatio: 0.16,
        titleFontRatio: 0.104,
        subtitleFontRatio: 0.056,
        elementSpacingRatio: 0.020,
        outerBorder: BorderConfig(lineType: .normal, color: .black, widthRatio: 0.010, cornerRadiusRatio: 0),
        innerBorder: BorderConfig(lineType: .normal, color: .black, widthRatio: 0.006, cornerRadiusRatio: 0),
        borderGapRatio: 0.008,
        borderGapColor: UIColor(red: 1.0, green: 0.090, blue: 0.267, alpha: 1),
        posterOuterBorder: BorderConfig(lineType: .normal, color: .black, widthRatio: 0.012, cornerRadiusRatio: 0),
        backgroundColor: .white,
        titleColor: .black,
        subtitleColor: UIColor(red: 1.0, green: 0.090, blue: 0.267, alpha: 1),
        titleFontWeight: .regular,
        subtitleFontWeight: .bold,
        titleFontName: "Bangers-Regular",
        subtitleFontName: "PassionOne-Bold",
        showTitle: true,
        showSubtitle: true,
        titleAlignmentRaw: "Center",
        subtitleAlignmentRaw: "Center",
        subtitleOffsetRatio: -0.025,
        mapStyleRaw: nil,
        mapLanguageRaw: nil,
        hideLabels: false,
        lineWidth: 3.0,
        defaultLineColor: UIColor(red: 0.129, green: 0.588, blue: 0.953, alpha: 1),
        useColorPerWorkoutType: false,
        useLineWidthPerWorkoutType: false,
        workoutTypeColorsRaw: [:],
        workoutTypeLineWidths: [:],
        filterRaw: nil
    )

    // MARK: - Retro 70s
    /// Retro 70s - Burnt orange/brown, large corner radius, sepia. Inspired by 1970s graphic design.
    static let retro70s = MapExportDecorator.Configuration(
        paddingRatio: 0.04,
        titleAreaHeightRatio: 0.15,
        titleFontRatio: 0.096,
        subtitleFontRatio: 0.040,
        elementSpacingRatio: 0.015,
        outerBorder: BorderConfig(lineType: .normal, color: UIColor(red: 0.545, green: 0.271, blue: 0.075, alpha: 1), widthRatio: 0.010, cornerRadiusRatio: 0.06),
        innerBorder: BorderConfig(lineType: .normal, color: UIColor(red: 0.800, green: 0.333, blue: 0.0, alpha: 1), widthRatio: 0.004, cornerRadiusRatio: 0.048),
        borderGapRatio: 0.008,
        borderGapColor: UIColor(red: 0.961, green: 0.902, blue: 0.827, alpha: 1),
        backgroundColor: UIColor(red: 0.961, green: 0.902, blue: 0.827, alpha: 1),
        titleColor: UIColor(red: 0.800, green: 0.333, blue: 0.0, alpha: 1),
        subtitleColor: UIColor(red: 0.545, green: 0.271, blue: 0.075, alpha: 1),
        titleFontWeight: .regular,
        subtitleFontWeight: .semibold,
        titleFontName: "Righteous-Regular",
        subtitleFontName: "Quicksand-SemiBold",
        showTitle: true,
        showSubtitle: true,
        titleAlignmentRaw: "Center",
        subtitleAlignmentRaw: "Center",
        subtitleOffsetRatio: -0.025,
        mapStyleRaw: nil,
        mapLanguageRaw: nil,
        hideLabels: false,
        lineWidth: 3.0,
        defaultLineColor: UIColor(red: 0.545, green: 0.271, blue: 0.075, alpha: 1),
        useColorPerWorkoutType: false,
        useLineWidthPerWorkoutType: false,
        workoutTypeColorsRaw: [:],
        workoutTypeLineWidths: [:],
        filterRaw: "Sepia"
    )

    // MARK: - Risograph
    /// Risograph - Fluorescent colors, paper texture look. Inspired by indie zine printing.
//    static let risograph = MapExportDecorator.Configuration(
//        paddingRatio: 0.05,
//        titleAreaHeightRatio: 0.14,
//        titleFontRatio: 0.096,
//        subtitleFontRatio: 0.040,
//        elementSpacingRatio: 0.015,
//        outerBorder: BorderConfig(lineType: .normal, color: UIColor(red: 0.102, green: 0.102, blue: 0.102, alpha: 1), widthRatio: 0.006, cornerRadiusRatio: 0),
//        innerBorder: .none,
//        borderGapRatio: 0.012,
//        borderGapColor: nil,
//        backgroundColor: UIColor(red: 0.961, green: 0.949, blue: 0.922, alpha: 1),
//        titleColor: UIColor(red: 0.102, green: 0.102, blue: 0.102, alpha: 1),
//        subtitleColor: UIColor(red: 0.102, green: 0.102, blue: 0.102, alpha: 1),
//        titleFontWeight: .bold,
//        subtitleFontWeight: .medium,
//        titleFontName: "SpaceGrotesk-Light_Bold",
//        subtitleFontName: "SpaceGrotesk-Light_Medium",
//        showTitle: true,
//        showSubtitle: true,
//        titleAlignmentRaw: "Left",
//        subtitleAlignmentRaw: "Left",
//        mapStyleRaw: nil,
//        mapLanguageRaw: nil,
//        hideLabels: false,
//        lineWidth: 3.0,
//        defaultLineColor: UIColor(red: 1.0, green: 0.361, blue: 0.361, alpha: 1),
//        useColorPerWorkoutType: false,
//        useLineWidthPerWorkoutType: false,
//        workoutTypeColorsRaw: [:],
//        workoutTypeLineWidths: [:],
//        filterRaw: nil
//    )

    // MARK: - Swiss
    /// Swiss International - Clean minimal, thin border, light font weight. Inspired by 1950s Swiss design.
    static let swiss = MapExportDecorator.Configuration(
        paddingRatio: 0.06,
        titleAreaHeightRatio: 0.14,
        titleFontRatio: 0.084,
        subtitleFontRatio: 0.036,
        elementSpacingRatio: 0.015,
        outerBorder: BorderConfig(lineType: .normal, color: UIColor(red: 0.102, green: 0.102, blue: 0.102, alpha: 1), widthRatio: 0.002, cornerRadiusRatio: 0),
        innerBorder: .none,
        borderGapRatio: 0.012,
        borderGapColor: nil,
        backgroundColor: .white,
        titleColor: UIColor(red: 0.102, green: 0.102, blue: 0.102, alpha: 1),
        subtitleColor: UIColor(red: 0.400, green: 0.400, blue: 0.400, alpha: 1),
        titleFontWeight: .black,
        subtitleFontWeight: .regular,
        titleFontName: "HelveticaNeue-Bold",
        subtitleFontName: "HelveticaNeue",
        showTitle: true,
        showSubtitle: true,
        titleAlignmentRaw: "Left",
        subtitleAlignmentRaw: "Left",
        mapStyleRaw: "test_custom",
        mapLanguageRaw: nil,
        hideLabels: false,
        lineWidth: 4.0,
        defaultLineColor: UIColor(red: 218.0/255.0, green: 41.0/255.0, blue: 28.0/255.0, alpha: 1.0),
        useColorPerWorkoutType: false,
        useLineWidthPerWorkoutType: false,
        workoutTypeColorsRaw: [:],
        workoutTypeLineWidths: [:],
        filterRaw: "Grayscale"
    )

    // MARK: - Transit Map
    /// Transit Map - Title on top, three-square decorations. Clean transit system style.
    static let transitMap = MapExportDecorator.Configuration(
        paddingRatio: 0.06,
        titleAreaHeightRatio: 0.14,
        titleFontRatio: 0.072,
        subtitleFontRatio: 0.028,
        elementSpacingRatio: 0.012,
        outerBorder: .none,
        innerBorder: .none,
        borderGapRatio: 0.012,
        borderGapColor: nil,
        titlePositionRaw: "top",
        posterDecoration: PosterDecoration(
            type: .fullWidthRectangle,
            position: .top,
            alignment: .right,
            color1: UIColor(red: 0.851, green: 0.306, blue: 0.247, alpha: 1),
            color2: UIColor(red: 0.180, green: 0.431, blue: 0.710, alpha: 1),
            color3: UIColor(red: 0.961, green: 0.722, blue: 0.200, alpha: 1),
            sizeRatio: 0.08,
            spacingRatio: 0.012
        ),
        backgroundColor: UIColor(red: 0.973, green: 0.969, blue: 0.961, alpha: 1),
        titleColor: UIColor(red: 0.102, green: 0.102, blue: 0.102, alpha: 1),
        subtitleColor: UIColor(red: 0.400, green: 0.400, blue: 0.400, alpha: 1),
        titleFontWeight: .black,
        subtitleFontWeight: .medium,
        titleFontName: "Inter-Black",
        subtitleFontName: "Inter-Medium",
        showTitle: true,
        showSubtitle: true,
        titleAlignmentRaw: "Left",
        subtitleAlignmentRaw: "Left",
        mapStyleRaw: "osm_bright",
        mapLanguageRaw: nil,
        hideLabels: false,
        lineWidth: 3.0,
        defaultLineColor: UIColor(red: 0.180, green: 0.431, blue: 0.710, alpha: 1),
        useColorPerWorkoutType: false,
        useLineWidthPerWorkoutType: false,
        workoutTypeColorsRaw: [:],
        workoutTypeLineWidths: [:],
        filterRaw: nil
    )

    // MARK: - Vaporwave
    /// Vaporwave - Neon pink/cyan, dark background. Inspired by 1980s-90s retro-futurism.
//    static let vaporwave = MapExportDecorator.Configuration(
//        paddingRatio: 0.04,
//        titleAreaHeightRatio: 0.14,
//        titleFontRatio: 0.056,
//        subtitleFontRatio: 0.048,
//        elementSpacingRatio: 0.024,
//        outerBorder: BorderConfig(lineType: .normal, color: UIColor(red: 1.0, green: 0.416, blue: 0.835, alpha: 1), widthRatio: 0.008, cornerRadiusRatio: 0),
//        innerBorder: BorderConfig(lineType: .normal, color: UIColor(red: 0.0, green: 1.0, blue: 1.0, alpha: 1), widthRatio: 0.004, cornerRadiusRatio: 0),
//        borderGapRatio: 0.006,
//        borderGapColor: UIColor(red: 0.071, green: 0.012, blue: 0.141, alpha: 1),
//        backgroundColor: UIColor(red: 0.071, green: 0.012, blue: 0.141, alpha: 1),
//        titleColor: UIColor(red: 1.0, green: 0.416, blue: 0.835, alpha: 1),
//        subtitleColor: UIColor(red: 0.0, green: 1.0, blue: 1.0, alpha: 1),
//        titleFontWeight: .regular,
//        subtitleFontWeight: .regular,
//        titleFontName: "PressStart2P-Regular",
//        subtitleFontName: "VT323-Regular",
//        showTitle: true,
//        showSubtitle: true,
//        titleAlignmentRaw: "Center",
//        subtitleAlignmentRaw: "Center",
//        mapStyleRaw: nil,
//        mapLanguageRaw: nil,
//        hideLabels: false,
//        lineWidth: 3.0,
//        defaultLineColor: UIColor(red: 0.580, green: 0.816, blue: 1.0, alpha: 1),
//        useColorPerWorkoutType: false,
//        useLineWidthPerWorkoutType: false,
//        workoutTypeColorsRaw: [:],
//        workoutTypeLineWidths: [:],
//        filterRaw: nil
//    )

    // MARK: - Vintage Travel
    /// Vintage Travel Poster - Multi-layer poster borders, warm earth tones. Inspired by 1970s travel posters.
    static let vintageTravel = MapExportDecorator.Configuration(
        paddingRatio: 0.04,
        titleAreaHeightRatio: 0.14,
        titleFontRatio: 0.084,
        subtitleFontRatio: 0.032,
        elementSpacingRatio: 0.012,
        outerBorder: BorderConfig(lineType: .normal, color: UIColor(red: 0.231, green: 0.420, blue: 0.647, alpha: 1), widthRatio: 0.006, cornerRadiusRatio: 0),
        innerBorder: BorderConfig(lineType: .normal, color: UIColor(red: 0.231, green: 0.420, blue: 0.647, alpha: 1), widthRatio: 0.002, cornerRadiusRatio: 0),
        borderGapRatio: 0.006,
        borderGapColor: UIColor(red: 0.961, green: 0.929, blue: 0.847, alpha: 1),
        posterOuterBorder: BorderConfig(lineType: .normal, color: UIColor(red: 0.231, green: 0.420, blue: 0.647, alpha: 1), widthRatio: 0.008, cornerRadiusRatio: 0),
        posterInnerBorder: BorderConfig(lineType: .normal, color: UIColor(red: 0.769, green: 0.361, blue: 0.243, alpha: 1), widthRatio: 0.006, cornerRadiusRatio: 0),
        posterBorderGapRatio: 0.008,
        posterBorderGapColor: UIColor(red: 0.898, green: 0.784, blue: 0.478, alpha: 1),
        posterMarginRatio: 0.024,
        backgroundColor: UIColor(red: 0.961, green: 0.929, blue: 0.847, alpha: 1),
        titleColor: UIColor(red: 0.165, green: 0.165, blue: 0.165, alpha: 1),
        subtitleColor: UIColor(red: 0.353, green: 0.353, blue: 0.353, alpha: 1),
        titleFontWeight: .bold,
        subtitleFontWeight: .regular,
        titleFontName: "Oswald-Regular_Bold",
        subtitleFontName: "SourceSans3-Roman_Regular",
        showTitle: true,
        showSubtitle: true,
        titleAlignmentRaw: "Left",
        subtitleAlignmentRaw: "Left",
        mapStyleRaw: nil,
        mapLanguageRaw: nil,
        hideLabels: false,
        lineWidth: 4.0,
        defaultLineColor: UIColor(red: 0.231, green: 0.420, blue: 0.647, alpha: 1),
        useColorPerWorkoutType: false,
        useLineWidthPerWorkoutType: false,
        workoutTypeColorsRaw: [:],
        workoutTypeLineWidths: [:],
        filterRaw: "Sepia"
    )
}

// MARK: - SwiftUI Previews

#if DEBUG

/// Helper view that renders a theme preview
private struct ThemePreviewView: View {
    let configuration: MapExportDecorator.Configuration
    let themeName: String

    var body: some View {
        VStack(spacing: 0) {
            if let image = renderPreview() {
                Image(uiImage: image)
                    .resizable()
                    .aspectRatio(contentMode: .fit)
            } else {
                Text("Failed to render preview")
                    .foregroundColor(.red)
            }
        }
        .navigationTitle(themeName)
    }

    private func renderPreview() -> UIImage? {
        // Create a sample map image (gray placeholder with some visual interest)
        let mapSize = CGSize(width: 500, height: 500)
        let mapRenderer = UIGraphicsImageRenderer(size: mapSize)
        let mapImage = mapRenderer.image { context in
            // Draw a gradient background to simulate a map
            let colors = [
                UIColor(red: 0.85, green: 0.90, blue: 0.85, alpha: 1).cgColor,
                UIColor(red: 0.75, green: 0.82, blue: 0.78, alpha: 1).cgColor,
                UIColor(red: 0.80, green: 0.85, blue: 0.80, alpha: 1).cgColor
            ]
            let gradient = CGGradient(colorsSpace: CGColorSpaceCreateDeviceRGB(), colors: colors as CFArray, locations: [0, 0.5, 1])!
            context.cgContext.drawLinearGradient(gradient, start: .zero, end: CGPoint(x: mapSize.width, y: mapSize.height), options: [])

            // Draw some "roads" to make it look more map-like
            context.cgContext.setStrokeColor(UIColor(white: 0.95, alpha: 0.8).cgColor)
            context.cgContext.setLineWidth(3)

            // Horizontal roads
            for y in stride(from: 50, to: Int(mapSize.height), by: 80) {
                context.cgContext.move(to: CGPoint(x: 0, y: y))
                context.cgContext.addLine(to: CGPoint(x: mapSize.width, y: CGFloat(y)))
            }

            // Vertical roads
            for x in stride(from: 60, to: Int(mapSize.width), by: 90) {
                context.cgContext.move(to: CGPoint(x: x, y: 0))
                context.cgContext.addLine(to: CGPoint(x: CGFloat(x), y: mapSize.height))
            }
            context.cgContext.strokePath()

            // Draw a sample route
            context.cgContext.setStrokeColor(configuration.defaultLineColor.cgColor)
            context.cgContext.setLineWidth(4)
            context.cgContext.setLineCap(.round)
            context.cgContext.setLineJoin(.round)

            let routePoints: [CGPoint] = [
                CGPoint(x: 80, y: 420),
                CGPoint(x: 120, y: 380),
                CGPoint(x: 180, y: 350),
                CGPoint(x: 220, y: 280),
                CGPoint(x: 280, y: 250),
                CGPoint(x: 320, y: 200),
                CGPoint(x: 380, y: 180),
                CGPoint(x: 420, y: 120),
                CGPoint(x: 450, y: 80)
            ]

            context.cgContext.move(to: routePoints[0])
            for point in routePoints.dropFirst() {
                context.cgContext.addLine(to: point)
            }
            context.cgContext.strokePath()
        }

        // Apply the decorator
        let decorator = MapExportDecorator(configuration: configuration)
        let targetSize = CGSize(width: 500, height: 500)
        return decorator.decorate(
            mapImage: mapImage,
            title: "Brussels is a beautiful city with many gardens and people",
            subtitle: .distance(127400),
            targetSize: targetSize
        )
    }
}

/// Grid view showing all themes
private struct AllThemesGridView: View {
    let themes: [(String, MapExportDecorator.Configuration)] = [
        ("Berlin", .glitch),
        ("Japanese", .japanese),
        ("Mondrian", .mondrian),
        ("Newspaper", .newspaper),
        ("Noir", .noir),
        ("Swiss", .swiss),
        ("Transit Map", .transitMap),
        ("Vintage Travel", .vintageTravel),
        ("Art Nouveau", .artNouveau),
        ("Bauhaus", .bauhaus),
        ("Nordic", .nordic),
        ("Retro 70s", .retro70s),
        ("Polaroid", .polaroid),
        ("Pop Art", .popArt),
        ("Cyberpunk", .cyberpunk),
        ("Art Deco", .artDeco)
    ]

    let columns = [
        GridItem(.flexible()),
        GridItem(.flexible())
    ]

    var body: some View {
        ScrollView {
            LazyVGrid(columns: columns, spacing: 16) {
                ForEach(themes, id: \.0) { name, config in
                    VStack(spacing: 8) {
                        ThemePreviewView(configuration: config, themeName: name)
                            .frame(height: 280)
                            .clipShape(RoundedRectangle(cornerRadius: 8))
                            .shadow(radius: 2)
                        Text(name)
                            .font(.caption)
                            .fontWeight(.medium)
                    }
                }
            }
            .padding()
        }
        .navigationTitle("All Themes")
    }
}

// MARK: - Individual Theme Previews

#Preview("All Themes Grid") {
    NavigationStack {
        AllThemesGridView()
    }
}

#Preview("Art Deco") {
    ThemePreviewView(configuration: .artDeco, themeName: "Art Deco")
}

#Preview("Art Nouveau") {
    ThemePreviewView(configuration: .artNouveau, themeName: "Art Nouveau")
}

#Preview("Bauhaus") {
    ThemePreviewView(configuration: .bauhaus, themeName: "Bauhaus")
}

#Preview("Cyberpunk") {
    ThemePreviewView(configuration: .cyberpunk, themeName: "Cyberpunk")
}


#Preview("Glitch") {
    ThemePreviewView(configuration: .glitch, themeName: "glitch")
}


#Preview("Japanese") {
    ThemePreviewView(configuration: .japanese, themeName: "Japanese")
}

#Preview("Mondrian") {
    ThemePreviewView(configuration: .mondrian, themeName: "Mondrian")
}

#Preview("Newspaper") {
    ThemePreviewView(configuration: .newspaper, themeName: "Newspaper")
}

#Preview("Noir") {
    ThemePreviewView(configuration: .noir, themeName: "Noir")
}

#Preview("Nordic") {
    ThemePreviewView(configuration: .nordic, themeName: "Nordic")
}

#Preview("Polaroid") {
    ThemePreviewView(configuration: .polaroid, themeName: "Polaroid")
}

#Preview("Pop Art") {
    ThemePreviewView(configuration: .popArt, themeName: "Pop Art")
}

#Preview("Retro 70s") {
    ThemePreviewView(configuration: .retro70s, themeName: "Retro 70s")
}

//#Preview("Risograph") {
//    ThemePreviewView(configuration: .risograph, themeName: "Risograph")
//}

#Preview("Swiss") {
    ThemePreviewView(configuration: .swiss, themeName: "Swiss")
}

#Preview("Transit Map") {
    ThemePreviewView(configuration: .transitMap, themeName: "Transit Map")
}

//#Preview("Vaporwave") {
//    ThemePreviewView(configuration: .vaporwave, themeName: "Vaporwave")
//}

#Preview("Vintage Travel") {
    ThemePreviewView(configuration: .vintageTravel, themeName: "Vintage Travel")
}

#endif

```
WHICH CODE SNIPPETS FROM THIS CODE WOULD I NEED TO UNDERSTAND HOW TO ADD A PREFIX IMAGE BEFORE TITLE. DON'T CHANGE ANY CODE, JUST GIVE ME THE SNIPPETS I NEED.
