'use strict';

const { createApp, ref, reactive, computed, watch, onMounted, onUnmounted, nextTick } = Vue;

// ── i18n ─────────────────────────────────────────────────────────────────────
const LANGS = { de: window.HW_LANG_DE, en: window.HW_LANG_EN };

function useLang() {
    const uiLang = ref(localStorage.getItem('hw_ui_lang') || 'de');
    const t = (key) => LANGS[uiLang.value]?.[key] ?? LANGS['de']?.[key] ?? key;
    watch(uiLang, v => localStorage.setItem('hw_ui_lang', v));
    return { uiLang, t };
}

// ── API ───────────────────────────────────────────────────────────────────────
function useApi() {
    const loading = ref(false);
    const error   = ref(null);
    const apiBase = window.location.origin + '/api/index.php';

    async function request(method, path, body = null) {
        loading.value = true;
        error.value   = null;
        try {
            const opts = {
                method,
                headers: { 'Content-Type': 'application/json' },
                credentials: 'same-origin',
            };
            if (body !== null) opts.body = JSON.stringify(body);
            const res  = await fetch(apiBase + path, opts);
            const ct   = (res.headers.get('content-type') || '').toLowerCase();
            const json = ct.includes('application/json') ? await res.json() : null;
            if (!json) throw new Error('API did not return JSON');
            if (!json.success) throw new Error(json.error || 'API error');
            return json.data;
        } catch(e) {
            error.value = e.message;
            throw e;
        } finally {
            loading.value = false;
        }
    }

    return {
        loading, error,
        get:  (path)       => request('GET',    path),
        post: (path, body) => request('POST',   path, body),
        put:  (path, body) => request('PUT',    path, body),
        del:  (path)       => request('DELETE', path),
    };
}

// ── Design Presets (modul-level, shared with FormField color type) ────────────
const DESIGN_PRESETS = {
    classic:       { color_primary:'#b8962e', color_secondary:'#d4af72', color_bg:'#ffffff', color_headline:'#1a1a1a', color_body:'#444444' },
    romantic:      { color_primary:'#c97b8a', color_secondary:'#e8aab7', color_bg:'#fdf5f6', color_headline:'#3d1a20', color_body:'#555555' },
    modern_dark:   { color_primary:'#c0a060', color_secondary:'#8a7040', color_bg:'#1a1a1a', color_headline:'#f0f0f0', color_body:'#bbbbbb' },
    autumn:        { color_primary:'#c0622a', color_secondary:'#e8a060', color_bg:'#faf3ec', color_headline:'#2a1a0a', color_body:'#555555' },

    sage_green:  { color_primary:'#7b8a6e', color_secondary:'#c3a49b', color_bg:'#f8f7f3', color_headline:'#3a453d', color_body:'#5f6862' },
    midnight_blue: { color_primary:'#2c3e50', color_secondary:'#9fa8da', color_bg:'#f3f5f7', color_headline:'#1a2433', color_body:'#556170' },
    sleek_dark: { color_primary:'#1c1c1e', color_secondary:'#b3b3b3', color_bg:'#111114', color_headline:'#ffffff', color_body:'#aaaaaa' },
    berry_jewel: { color_primary:'#800020', color_secondary:'#663366', color_bg:'#fff6fa', color_headline:'#3a1e33', color_body:'#5a405a' },
    ivory_blush:   { color_primary:'#d8a7b1', color_secondary:'#f3d7dd', color_bg:'#fffaf8', color_headline:'#2f1f24', color_body:'#5b4a4f' },
    navy_luxe:     { color_primary:'#1f3a5f', color_secondary:'#c7a76c', color_bg:'#f6f7fb', color_headline:'#142238', color_body:'#4d5b6c' },
    burgundy_mood: { color_primary:'#7a1f33', color_secondary:'#b46b7a', color_bg:'#f8f1f3', color_headline:'#2b1118', color_body:'#625056' },
    charcoal_emerald:{ color_primary:'#0f5b4f', color_secondary:'#6f8f84', color_bg:'#111315', color_headline:'#f2f4f3', color_body:'#c0c6c4' }
};

// Aktives Design – wird beim App-Start geladen, von FormField reaktiv gelesen
const _activeDesign = reactive({ preset: 'classic' });

// Gibt nur die Farben des aktuell aktiven Presets zurück
const COLOR_ROLE_VAR = {
    color_primary:   'var(--primary)',
    color_secondary: 'var(--secondary)',
    color_bg:        'var(--bg-body)',
    color_headline:  'var(--text-title)',
    color_body:      'var(--text-body)',
};
function buildColorPresetOpts() {
    const roleLabel = {
        color_primary:   'Primärfarbe',
        color_secondary: 'Sekundärfarbe',
        color_bg:        'Hintergrund',
        color_headline:  'Überschrift',
        color_body:      'Fließtext',
    };
    const preset = DESIGN_PRESETS[_activeDesign.preset] || DESIGN_PRESETS.classic;
    return Object.entries(preset).map(([role, hex]) => ({
        value: COLOR_ROLE_VAR[role] || hex,
        label: roleLabel[role] || role,
    }));
}
// Löst CSS-Variablen-Tokens (var(--primary) etc.) für Swatch-Vorschau im Editor auf
function resolveDesignColor(val) {
    if (!val || !val.startsWith('var(--')) return val;
    const preset = DESIGN_PRESETS[_activeDesign.preset] || DESIGN_PRESETS.classic;
    const entry = Object.entries(COLOR_ROLE_VAR).find(([, v]) => v === val);
    return entry ? (preset[entry[0]] || val) : val;
}

// ── Section Schemas ───────────────────────────────────────────────────────────
const BG_POS_OPTS = [
    { value: 'top left',    icon: 'hgi-arrow-up-left-02'    },
    { value: 'top center',  icon: 'hgi-arrow-up-04'         },
    { value: 'top right',   icon: 'hgi-arrow-up-right-02'   },
    { value: 'center left', icon: 'hgi-arrow-left-04'       },
    { value: 'center',      icon: 'hgi-stop'                },
    { value: 'center right',icon: 'hgi-arrow-right-04'      },
    { value: 'bottom left', icon: 'hgi-arrow-down-left-02'  },
    { value: 'bottom center',icon:'hgi-arrow-down-04'       },
    { value: 'bottom right',icon: 'hgi-arrow-down-right-02' },
];
const COLS_OPTS   = ['1','2'].map(v => ({ value: v, labelKey: 'opt_' + v }));
const COLS3_OPTS  = ['2','3','4','5','6','7','8','9','10','11','12'].map(v => ({ value: v, labelKey: 'opt_' + v }));

// Icon-options for align types
const ALIGN_OPTS = [
    { value: 'left',   icon: 'hgi-align-left'   },
    { value: 'center', icon: 'hgi-align-horizontal-center'  },
    { value: 'right',  icon: 'hgi-align-right'   },
];
const NAV_JUSTIFY_OPTS = [
    { value: 'flex-start', icon: 'hgi-align-left'              },
    { value: 'center',     icon: 'hgi-align-horizontal-center' },
    { value: 'flex-end',   icon: 'hgi-align-right'             },
];
const TEXT_ALIGN_OPTS = [
    { value: 'left',    icon: 'hgi-text-align-left'    },
    { value: 'center',  icon: 'hgi-text-align-center'  },
    { value: 'right',   icon: 'hgi-text-align-right'   },
    { value: 'justify', icon: 'hgi-text-align-justify-center' },
];
const SIZE_OPTS = [
    { value: 's',   label: 'S',   labelKey: 'opt_s'   },
    { value: 'm',   label: 'M',   labelKey: 'opt_m'   },
    { value: 'l',   label: 'L',   labelKey: 'opt_l'   },
    { value: 'xl',  label: 'XL',  labelKey: 'opt_xl'  },
    { value: '2xl', label: '2XL', labelKey: 'opt_2xl' },
    { value: '3xl', label: '3XL', labelKey: 'opt_3xl' },
    { value: '4xl', label: '4XL', labelKey: 'opt_4xl' },
];
const RADIUS_OPTS = [
    { value: 's',    label: 'S',  labelKey: 'opt_s'    },
    { value: 'm',    label: 'M',  labelKey: 'opt_m'    },
    { value: 'l',    label: 'L',  labelKey: 'opt_l'    },
    { value: 'xl',   label: 'XL', labelKey: 'opt_xl'   },
    { value: 'full', label: '◯',  labelKey: 'opt_rund' },
];
const LOCATIONS_LAYOUT_OPTS = [
    { value: 'cards',       labelKey: 'opt_cards'      },
    { value: 'image-left',  labelKey: 'opt_img_left'   },
    { value: 'image-right', labelKey: 'opt_img_right'  },
];
const IMG_SIDE_OPTS = [
    { value: 'image-left',  labelKey: 'opt_img_left'  },
    { value: 'image-right', labelKey: 'opt_img_right' },
];
const WITNESSES_LAYOUT_OPTS = [
    { value: 'cards',       labelKey: 'opt_cards'       },
    { value: 'image-left',  labelKey: 'opt_img_left'    },
    { value: 'alternating', labelKey: 'opt_alternating' },
    { value: 'image-top',   labelKey: 'opt_img_top'     },
];
const COUPLE_LAYOUT_OPTS = [
    { value: 'image-top',   labelKey: 'opt_img_top'     },
    { value: 'image-left',  labelKey: 'opt_img_left'    },
    { value: 'alternating', labelKey: 'opt_alternating' },
];
const CONTENT_POS_OPTS = [
    { value: 'top-left',      icon: 'hgi-arrow-up-left-02'     },
    { value: 'top-center',    icon: 'hgi-arrow-up-04'          },
    { value: 'top-right',     icon: 'hgi-arrow-up-right-02'    },
    { value: 'center-left',   icon: 'hgi-arrow-left-04'        },
    { value: 'center',        icon: 'hgi-stop'                 },
    { value: 'center-right',  icon: 'hgi-arrow-right-04'       },
    { value: 'bottom-left',   icon: 'hgi-arrow-down-left-02'   },
    { value: 'bottom-center', icon: 'hgi-arrow-down-04'        },
    { value: 'bottom-right',  icon: 'hgi-arrow-down-right-02'  },
];
const IMG_POS_OPTS = [
    { value: 'left',  icon: 'hgi-arrow-left-05',  labelKey: 'opt_img_left'  },
    { value: 'top',   icon: 'hgi-arrow-up-05',    labelKey: 'opt_img_top'   },
    { value: 'right', icon: 'hgi-arrow-right-05', labelKey: 'opt_img_right' },
    { value: 'none',  icon: 'hgi-cancel-01',      labelKey: 'opt_none'      },
];
const SIDE_OPTS = [
    { value: 'left',  icon: 'hgi-arrow-left-05',  labelKey: 'opt_left'  },
    { value: 'right', icon: 'hgi-arrow-right-05', labelKey: 'opt_right' },
];
const VALIGN_OPTS = [
    { value: 'top',    icon: 'hgi-arrow-up-04'    },
    { value: 'middle', icon: 'hgi-stop'           },
    { value: 'bottom', icon: 'hgi-arrow-down-04'  },
];
function headlineFields(group) {
    const g = group || 'group_headline';
    return [
        { key: 'headline',       type: 'text',    labelKey: 'field_headline', i18n: true,  group: g, primary: true },
        { key: 'headline_align', type: 'align',   labelKey: 'field_align',    i18n: false, group: g, options: ALIGN_OPTS },
        { key: 'headline_size',  type: 'size',    labelKey: 'field_size',     i18n: false, group: g, options: SIZE_OPTS, nullable: true  },
        { key: 'headline_color', type: 'color',   labelKey: 'field_color',    i18n: false, group: g },
    ];
}

function bodyFields(group) {
    const g = group || 'group_body';
    return [
        { key: 'body',       type: 'textarea',   labelKey: 'field_body',       i18n: true,  group: g, primary: true },
        { key: 'body_cols',  type: 'select',     labelKey: 'field_body_cols',  i18n: false, group: g, options: COLS_OPTS },
        { key: 'body_align', type: 'text-align', labelKey: 'field_body_align', i18n: false, group: g, options: TEXT_ALIGN_OPTS },
        { key: 'body_size',  type: 'size',       labelKey: 'field_size',       i18n: false, group: g, options: SIZE_OPTS, nullable: true },
        { key: 'body_color', type: 'color',      labelKey: 'field_color',      i18n: false, group: g },
    ];
}

const SECTION_SCHEMAS = {
    'hero-header': [
        ...headlineFields('group_headline').filter(f => f.key !== 'headline_align'),
        { key: 'headline2',       type: 'text',  labelKey: 'field_headline2',  i18n: true,  group: 'group_subline', primary: true },
        { key: 'headline2_size',  type: 'size',  labelKey: 'field_size',       i18n: false, group: 'group_subline', options: SIZE_OPTS, nullable: true },
        { key: 'headline2_color', type: 'color', labelKey: 'field_color',      i18n: false, group: 'group_subline' },
        ...bodyFields(),
        { key: 'image',            type: 'image',       labelKey: 'field_image',          i18n: false, group: 'group_bg', primary: true },
        { key: 'image_position',  type: 'image-pos',   labelKey: 'field_bg_position',    i18n: false, group: 'group_bg' },
        { key: 'image_side',      type: 'image-align', labelKey: 'field_image_side',      i18n: false, group: 'group_bg', options: SIDE_OPTS },
        { key: 'background_image', type: 'image',      labelKey: 'field_bg_image',        i18n: false, group: 'group_bg', primary: true },
        { key: 'container_color', type: 'color',       labelKey: 'field_container_color', i18n: false, group: 'group_bg' },
        { key: 'min_height',      type: 'range-dvh',   labelKey: 'field_min_height',      i18n: false, group: 'group_bg', min: 0, max: 100, adv: true },
        { key: 'section_padding', type: 'size',        labelKey: 'field_section_padding', i18n: false, group: 'group_bg', options: SIZE_OPTS, adv: true, nullable: true },
        { key: 'text_padding',    type: 'size',        labelKey: 'field_padding',         i18n: false, group: 'group_bg', options: SIZE_OPTS, adv: true, nullable: true },
        { key: 'content_bg_color', type: 'color',      labelKey: 'field_bg_color',        i18n: false, group: 'group_bg', adv: true },
        { key: 'content_valign',  type: 'image-align', labelKey: 'field_valign',          i18n: false, group: 'group_bg', options: VALIGN_OPTS, adv: true },
        { key: 'content_halign',  type: 'align',       labelKey: 'field_halign',          i18n: false, group: 'group_bg', options: ALIGN_OPTS, adv: true },
        { key: 'border_width',    type: 'range',       labelKey: 'field_border_width',    i18n: false, group: 'group_bg', min: 0, max: 10, adv: true },
        { key: 'border_color',    type: 'color',       labelKey: 'field_border_color',    i18n: false, group: 'group_bg', adv: true },
    ],
    'intro': [
        ...headlineFields(),
        ...bodyFields(),
        { key: 'image',            type: 'image',       labelKey: 'field_image',          i18n: false, group: 'group_bg', primary: true },
        { key: 'image_position',   type: 'image-align', labelKey: 'field_image_position', i18n: false, group: 'group_bg', options: IMG_POS_OPTS, adv: true },
        { key: 'section_bg_color', type: 'color',       labelKey: 'field_bg_color',       i18n: false, group: 'group_bg', adv: true },
    ],
    'big-header': [
        ...headlineFields().filter(f => f.key !== 'headline_align'),
        { key: 'subline',       type: 'text',  labelKey: 'field_subline',  i18n: true,  group: 'group_subline', primary: true },
        { key: 'subline_size',  type: 'size',  labelKey: 'field_size',     i18n: false, group: 'group_subline', options: SIZE_OPTS, nullable: true },
        { key: 'subline_color', type: 'color', labelKey: 'field_color',    i18n: false, group: 'group_subline' },
        { key: 'background_image',    type: 'image',       labelKey: 'field_bg_image',      i18n: false, group: 'group_bg', primary: true },
        { key: 'background_position', type: 'image-pos',  labelKey: 'field_bg_position',   i18n: false, group: 'group_bg' },
        { key: 'overlay_opacity',     type: 'range',       labelKey: 'field_overlay',       i18n: false, group: 'group_bg', min: 0, max: 100 },
        { key: 'overlay_color',       type: 'color',       labelKey: 'field_overlay_color', i18n: false, group: 'group_bg' },
        { key: 'min_height', type: 'range-dvh', labelKey: 'field_min_height', i18n: false, group: 'group_bg', min: 0, max: 100, adv: true },
        { key: 'text_align',          type: 'bg-position', labelKey: 'field_content_pos',   i18n: false, group: 'group_bg', options: CONTENT_POS_OPTS, adv: true },
        { key: 'padding',    type: 'size',      labelKey: 'field_padding',    i18n: false, group: 'group_bg', options: SIZE_OPTS, adv: true, nullable: true },
    ],
    'countdown': [
        ...headlineFields(),
        { key: 'countdown_date',        type: 'date',  labelKey: 'field_countdown_date', i18n: false, group: 'group_timer', primary: true },
        { key: 'countdown_value_size',  type: 'size',  labelKey: 'field_value_size',     i18n: false, group: 'group_timer', options: SIZE_OPTS, nullable: true },
        { key: 'countdown_value_color', type: 'color', labelKey: 'field_value_color',    i18n: false, group: 'group_timer' },
        { key: 'countdown_label_size',  type: 'size',  labelKey: 'field_label_size',     i18n: false, group: 'group_timer', options: SIZE_OPTS, nullable: true },
        { key: 'countdown_label_color', type: 'color', labelKey: 'field_label_color',    i18n: false, group: 'group_timer' },
        { key: 'after_text',            type: 'text',  labelKey: 'field_closing_text',   i18n: true,  group: 'group_closing', primary: true },
        { key: 'after_text_size',       type: 'size',  labelKey: 'field_size',           i18n: false, group: 'group_closing', options: SIZE_OPTS, nullable: true },
        { key: 'after_text_color',      type: 'color', labelKey: 'field_color',          i18n: false, group: 'group_closing' },
        // Erweitertes Styling — alle Felder mit last:true damit die Gruppe nach den ungrouped-Feldern erscheint
        { key: 'section_padding',         type: 'size',      labelKey: 'field_section_padding',   i18n: false, group: 'group_countdown', options: SIZE_OPTS, adv: true, last: true, nullable: true },
        { key: 'section_bg_color',        type: 'color',     labelKey: 'field_section_bg',        i18n: false, group: 'group_countdown', adv: true, last: true },
        { key: 'background_image',        type: 'image',     labelKey: 'field_bg_image',          i18n: false, group: 'group_countdown', adv: true, last: true },
        { key: 'background_position',     type: 'image-pos', labelKey: 'field_bg_position',       i18n: false, group: 'group_countdown', adv: true, last: true },
        { key: 'overlay_color',           type: 'color',     labelKey: 'field_overlay_color',     i18n: false, group: 'group_countdown', adv: true, last: true },
        { key: 'overlay_opacity',         type: 'range',     labelKey: 'field_overlay',           i18n: false, group: 'group_countdown', min: 0, max: 100, adv: true, last: true },
        { key: 'countdown_padding',       type: 'size',      labelKey: 'field_content_padding',   i18n: false, group: 'group_countdown', options: SIZE_OPTS, adv: true, last: true, nullable: true },
        { key: 'countdown_bg_color',      type: 'color',     labelKey: 'field_content_overlay_color', i18n: false, group: 'group_countdown', adv: true, last: true },
        { key: 'countdown_bg_opacity',    type: 'range',     labelKey: 'field_content_overlay',   i18n: false, group: 'group_countdown', min: 0, max: 100, adv: true, last: true },
        { key: 'countdown_radius',        type: 'size',      labelKey: 'field_content_radius',    i18n: false, group: 'group_countdown', options: RADIUS_OPTS, adv: true, last: true, nullable: true },
    ],
    'locations': [
        ...headlineFields(),
        { key: 'intro_text',  type: 'textarea',   labelKey: 'field_body',  i18n: true,  group: 'group_intro', primary: true },
        { key: 'intro_align', type: 'text-align', labelKey: 'field_align', i18n: false, group: 'group_intro', options: TEXT_ALIGN_OPTS },
        { key: 'intro_size',  type: 'size',       labelKey: 'field_size',  i18n: false, group: 'group_intro', options: SIZE_OPTS, nullable: true },
        { key: 'intro_color', type: 'color',      labelKey: 'field_color', i18n: false, group: 'group_intro' },
        { key: 'items', type: 'repeater', labelKey: 'repeater_add', i18n: false, group: null,
          subSchema: [
              { key: 'name',        type: 'text',     labelKey: 'field_name',        i18n: true  },
              { key: 'address',     type: 'text',     labelKey: 'field_address',     i18n: true  },
              { key: 'description', type: 'textarea', labelKey: 'field_description', i18n: true  },
              { key: 'image',       type: 'image',    labelKey: 'field_image',       i18n: false },
              { key: 'maps_url',    type: 'url',      labelKey: 'field_link',        i18n: false },
              { key: 'link_label',  type: 'text',     labelKey: 'field_link_label',   i18n: false },
          ] },
        { key: 'layout',              type: 'select', labelKey: 'field_layout',              i18n: false, group: 'group_adv_locations', options: LOCATIONS_LAYOUT_OPTS, adv: true, last: true },
        { key: 'section_bg_color',    type: 'color', labelKey: 'field_section_bg',          i18n: false, group: 'group_adv_locations', adv: true, last: true },
        { key: 'card_bg_color',       type: 'color',  labelKey: 'field_card_bg',             i18n: false, group: 'group_adv_locations', adv: true, last: true },
        { key: 'card_radius',         type: 'size',   labelKey: 'field_card_radius',          i18n: false, group: 'group_adv_locations', adv: true, last: true, options: RADIUS_OPTS },
        { key: 'name_color',          type: 'color', labelKey: 'field_name_color',          i18n: false, group: 'group_adv_locations', adv: true, last: true },
        { key: 'address_color',       type: 'color', labelKey: 'field_address_color',       i18n: false, group: 'group_adv_locations', adv: true, last: true },
        { key: 'description_color',   type: 'color', labelKey: 'field_description_color',   i18n: false, group: 'group_adv_locations', adv: true, last: true },
    ],
    'quote': [
        { key: 'quote',       type: 'textarea',   labelKey: 'field_body',     i18n: true,  group: 'group_quote', primary: true },
        { key: 'quote_align', type: 'text-align', labelKey: 'field_align',    i18n: false, group: 'group_quote', options: TEXT_ALIGN_OPTS },
        { key: 'quote_size',  type: 'size',       labelKey: 'field_size',     i18n: false, group: 'group_quote', options: SIZE_OPTS, nullable: true },
        { key: 'quote_color', type: 'color',      labelKey: 'field_color',    i18n: false, group: 'group_quote' },
        { key: 'author',       type: 'text',       labelKey: 'field_author', i18n: true,  group: 'group_author', primary: true },
        { key: 'author_align', type: 'text-align', labelKey: 'field_align',  i18n: false, group: 'group_author', options: TEXT_ALIGN_OPTS },
        { key: 'author_size',  type: 'size',       labelKey: 'field_size',   i18n: false, group: 'group_author', options: SIZE_OPTS, nullable: true },
        { key: 'author_color', type: 'color',      labelKey: 'field_color',  i18n: false, group: 'group_author' },
        { key: 'section_bg_color',    type: 'color',     labelKey: 'field_section_bg',      i18n: false, group: 'group_adv_quote', adv: true, last: true },
        { key: 'section_padding',     type: 'size',      labelKey: 'field_section_padding', i18n: false, group: 'group_adv_quote', options: SIZE_OPTS, adv: true, last: true, nullable: true },
        { key: 'background_image',    type: 'image',     labelKey: 'field_bg_image',        i18n: false, group: 'group_adv_quote', adv: true, last: true },
        { key: 'background_position', type: 'image-pos', labelKey: 'field_bg_position',     i18n: false, group: 'group_adv_quote', adv: true, last: true },
        { key: 'overlay_opacity',     type: 'range',     labelKey: 'field_overlay',         i18n: false, group: 'group_adv_quote', min: 0, max: 100, adv: true, last: true },
        { key: 'overlay_color',       type: 'color',     labelKey: 'field_overlay_color',   i18n: false, group: 'group_adv_quote', adv: true, last: true },
    ],
    'witnesses': [
        ...headlineFields(),
        { key: 'items', type: 'repeater', labelKey: 'repeater_add', i18n: false, group: null,
          subSchema: [
              { key: 'name',        type: 'text',     labelKey: 'field_name',        i18n: true  },
              { key: 'role',        type: 'text',     labelKey: 'field_role',        i18n: true  },
              { key: 'description', type: 'textarea', labelKey: 'field_description', i18n: true  },
              { key: 'photo',       type: 'image',    labelKey: 'field_photo',       i18n: false },
          ] },
        // ── Erweitertes Styling ────────────────────────────────────────────────
        { key: 'layout',              type: 'select', labelKey: 'field_layout',         i18n: false, group: 'group_adv_witnesses', options: WITNESSES_LAYOUT_OPTS, adv: true, last: true },
        { key: 'photo_border_radius', type: 'size',   labelKey: 'field_photo_border_radius', i18n: false, group: 'group_adv_witnesses', options: RADIUS_OPTS, adv: true, last: true },
        { key: 'photo_border_width',  type: 'range',  labelKey: 'field_photo_border_width',  i18n: false, group: 'group_adv_witnesses', min: 0, max: 10, adv: true, last: true },
        { key: 'photo_border_color',  type: 'color',  labelKey: 'field_photo_border_color',  i18n: false, group: 'group_adv_witnesses', adv: true, last: true },
        { key: 'name_size',           type: 'size',   labelKey: 'field_name_size',           i18n: false, group: 'group_adv_witnesses', options: SIZE_OPTS, nullable: true, adv: true, last: true },
        { key: 'name_color',          type: 'color',  labelKey: 'field_name_color',          i18n: false, group: 'group_adv_witnesses', adv: true, last: true },
        { key: 'role_size',           type: 'size',   labelKey: 'field_role_size',           i18n: false, group: 'group_adv_witnesses', options: SIZE_OPTS, nullable: true, adv: true, last: true },
        { key: 'role_color',          type: 'color',  labelKey: 'field_role_color',          i18n: false, group: 'group_adv_witnesses', adv: true, last: true },
        { key: 'desc_size',           type: 'size',   labelKey: 'field_desc_size',           i18n: false, group: 'group_adv_witnesses', options: SIZE_OPTS, nullable: true, adv: true, last: true },
        { key: 'desc_color',          type: 'color',  labelKey: 'field_desc_color',          i18n: false, group: 'group_adv_witnesses', adv: true, last: true },
    ],
    'couple': [
        ...headlineFields(),
        { key: 'intro_text',  type: 'textarea',   labelKey: 'field_body',  i18n: true,  group: 'group_intro', primary: true },
        { key: 'intro_align', type: 'text-align', labelKey: 'field_align', i18n: false, group: 'group_intro', options: TEXT_ALIGN_OPTS },
        { key: 'intro_size',  type: 'size',       labelKey: 'field_size',  i18n: false, group: 'group_intro', options: SIZE_OPTS, nullable: true },
        { key: 'intro_color', type: 'color',      labelKey: 'field_color', i18n: false, group: 'group_intro' },
        { key: 'items', type: 'repeater', labelKey: 'repeater_add', max: 2, i18n: false, group: null,
          subSchema: [
              { key: 'name',  type: 'text',     labelKey: 'field_name',  i18n: false },
              { key: 'text',  type: 'textarea', labelKey: 'field_body',  i18n: true  },
              { key: 'photo', type: 'image',    labelKey: 'field_photo', i18n: false },
          ] },
        // ── Erweitertes Styling ───────────────────────────────────────────────
        { key: 'layout',              type: 'select', labelKey: 'field_layout',             i18n: false, group: 'group_adv_couple', options: COUPLE_LAYOUT_OPTS, adv: true, last: true },
        { key: 'name_size',           type: 'size',  labelKey: 'field_name_size',           i18n: false, group: 'group_adv_couple', options: SIZE_OPTS,   nullable: true, adv: true, last: true },
        { key: 'name_color',          type: 'color', labelKey: 'field_name_color',          i18n: false, group: 'group_adv_couple',                        adv: true, last: true },
        { key: 'text_size',           type: 'size',  labelKey: 'field_text_size',           i18n: false, group: 'group_adv_couple', options: SIZE_OPTS,   nullable: true, adv: true, last: true },
        { key: 'text_color',          type: 'color', labelKey: 'field_text_color',          i18n: false, group: 'group_adv_couple',                        adv: true, last: true },
        { key: 'photo_border_radius', type: 'size',  labelKey: 'field_photo_border_radius', i18n: false, group: 'group_adv_couple', options: RADIUS_OPTS, adv: true, last: true },
        { key: 'photo_border_width',  type: 'range', labelKey: 'field_photo_border_width',  i18n: false, group: 'group_adv_couple', min: 0, max: 10,      adv: true, last: true },
        { key: 'photo_border_color',  type: 'color', labelKey: 'field_photo_border_color',  i18n: false, group: 'group_adv_couple',                        adv: true, last: true },
    ],
    'program': [
        ...headlineFields(),
        { key: 'intro_text',  type: 'textarea',   labelKey: 'field_body',  i18n: true,  group: 'group_intro', primary: true },
        { key: 'intro_align', type: 'text-align', labelKey: 'field_align', i18n: false, group: 'group_intro', options: TEXT_ALIGN_OPTS },
        { key: 'intro_size',  type: 'size',       labelKey: 'field_size',  i18n: false, group: 'group_intro', options: SIZE_OPTS, nullable: true },
        { key: 'intro_color', type: 'color',      labelKey: 'field_color', i18n: false, group: 'group_intro' },
        { key: 'items', type: 'repeater', labelKey: 'repeater_add', i18n: false, group: null,
          subSchema: [
              { key: 'time',        type: 'text',     labelKey: 'field_time',        i18n: false },
              { key: 'title',       type: 'text',     labelKey: 'field_title',       i18n: true  },
              { key: 'description', type: 'textarea', labelKey: 'field_description', i18n: true  },
              { key: 'icon',        type: 'emoji',    labelKey: 'field_icon',        i18n: false },
          ] },
        { key: 'section_bg_color', type: 'color', labelKey: 'field_section_bg',    i18n: false, group: 'group_adv_program', adv: true, last: true },
        { key: 'section_padding',  type: 'size',  labelKey: 'field_section_padding', i18n: false, group: 'group_adv_program', options: SIZE_OPTS, adv: true, last: true, nullable: true },
        { key: 'timeline_color',   type: 'color', labelKey: 'field_timeline_color',  i18n: false, group: 'group_adv_program', adv: true, last: true },
        { key: 'emoji_bg_color',   type: 'color', labelKey: 'field_emoji_bg_color',  i18n: false, group: 'group_adv_program', adv: true, last: true },
        { key: 'time_color',       type: 'color', labelKey: 'field_time_color',      i18n: false, group: 'group_adv_program', adv: true, last: true },
        { key: 'title_color',      type: 'color', labelKey: 'field_title_color',   i18n: false, group: 'group_adv_program', adv: true, last: true },
        { key: 'title_size',       type: 'size',  labelKey: 'field_title_size',    i18n: false, group: 'group_adv_program', options: SIZE_OPTS, adv: true, last: true, nullable: true },
        { key: 'desc_color',       type: 'color', labelKey: 'field_description_color', i18n: false, group: 'group_adv_program', adv: true, last: true },
        { key: 'desc_size',        type: 'size',  labelKey: 'field_desc_size',     i18n: false, group: 'group_adv_program', options: SIZE_OPTS, nullable: true, adv: true, last: true },
    ],
    'accommodation': [
        ...headlineFields(),
        { key: 'intro_text',  type: 'textarea',   labelKey: 'field_body',  i18n: true,  group: 'group_intro', primary: true },
        { key: 'intro_align', type: 'text-align', labelKey: 'field_align', i18n: false, group: 'group_intro', options: TEXT_ALIGN_OPTS },
        { key: 'intro_size',  type: 'size',       labelKey: 'field_size',  i18n: false, group: 'group_intro', options: SIZE_OPTS, nullable: true },
        { key: 'intro_color', type: 'color',      labelKey: 'field_color', i18n: false, group: 'group_intro' },
        { key: 'image',       type: 'image',       labelKey: 'field_image', i18n: false, group: null },
        { key: 'items', type: 'repeater', labelKey: 'repeater_add', i18n: false, group: null,
          subSchema: [
              { key: 'icon',        type: 'emoji', labelKey: 'field_icon',        i18n: false },
              { key: 'name',        type: 'text',  labelKey: 'field_name',        i18n: true  },
              { key: 'address',     type: 'text',  labelKey: 'field_address',     i18n: true  },
              { key: 'website_url', type: 'url',   labelKey: 'field_website_url', i18n: false },
          ] },
        { key: 'layout',           type: 'select', labelKey: 'field_layout',       i18n: false, group: 'group_adv_accommodation', options: IMG_SIDE_OPTS, adv: true, last: true },
        { key: 'section_bg_color', type: 'color', labelKey: 'field_section_bg',    i18n: false, group: 'group_adv_accommodation', adv: true, last: true },
        { key: 'name_color',       type: 'color', labelKey: 'field_name_color',    i18n: false, group: 'group_adv_accommodation', adv: true, last: true },
        { key: 'address_color',    type: 'color', labelKey: 'field_address_color', i18n: false, group: 'group_adv_accommodation', adv: true, last: true },
    ],
    'rsvp': [
        ...headlineFields(),
        { key: 'intro_text',   type: 'textarea',   labelKey: 'field_body',    i18n: true,  group: 'group_intro', primary: true },
        { key: 'intro_align',  type: 'text-align', labelKey: 'field_align',        i18n: false, group: 'group_intro', options: TEXT_ALIGN_OPTS },
        { key: 'intro_size',   type: 'size',       labelKey: 'field_size',         i18n: false, group: 'group_intro', options: SIZE_OPTS, nullable: true },
        { key: 'intro_color',  type: 'color',      labelKey: 'field_color',        i18n: false, group: 'group_intro' },
        { key: '__var_hint',   type: 'var-hint',   i18n: false },
        { key: 'deadline',     type: 'date',       labelKey: 'field_deadline',     i18n: false },
        { key: 'show_persons', type: 'toggle',     labelKey: 'field_show_persons', i18n: false },
        { key: 'show_meal',    type: 'toggle',     labelKey: 'field_show_meal',    i18n: false },
        { key: 'meal_options', type: 'repeater',   labelKey: 'field_meal_options', i18n: false, showWhen: 'show_meal',
          subSchema: [{ key: 'label', type: 'text', labelKey: 'field_name', i18n: true }] },
        { key: 'show_message', type: 'toggle',     labelKey: 'field_show_message', i18n: false },
        { key: 'show_email',   type: 'toggle',     labelKey: 'field_show_email',   i18n: false },
        { key: 'form_labels',  type: 'repeater',   labelKey: 'field_form_labels',  i18n: false, group: 'group_form_labels', last: true, cssClass: 'hw-repeater--labels-only',
          rowLabels: ['Name', 'E-Mail', 'Frage', 'Zusagen', 'Absagen', 'Personen', 'Speise', 'Nachricht', 'Absenden', 'Danke-Text', 'Frist abgelaufen'],
          subSchema: [{ key: 'text', type: 'text', labelKey: 'field_name', i18n: true }] },
        { key: 'layout',                type: 'select', labelKey: 'field_layout',          i18n: false, group: 'group_adv_rsvp',
          options: [{ value: 'form-right', labelKey: 'opt_form_right' }, { value: 'form-left', labelKey: 'opt_form_left' }], adv: true, last: true },
        { key: 'intro_bg_image',        type: 'image',  labelKey: 'field_intro_bg_image',  i18n: false, group: 'group_adv_rsvp', adv: true, last: true },
        { key: 'intro_bg_color',        type: 'color',  labelKey: 'field_intro_bg',        i18n: false, group: 'group_adv_rsvp', adv: true, last: true },
        { key: 'intro_overlay_color',   type: 'color',  labelKey: 'field_intro_overlay_color', i18n: false, group: 'group_adv_rsvp', adv: true, last: true },
        { key: 'intro_overlay_opacity', type: 'range',  labelKey: 'field_overlay_opacity', i18n: false, group: 'group_adv_rsvp', min: 0, max: 100, adv: true, last: true },
        { key: 'form_bg_color',         type: 'color',  labelKey: 'field_form_bg',         i18n: false, group: 'group_adv_rsvp', adv: true, last: true },
        { key: 'section_padding',       type: 'size',   labelKey: 'field_column_padding',  i18n: false, group: 'group_adv_rsvp', options: SIZE_OPTS, nullable: true, adv: true, last: true },
    ],
    'gallery-section': [
        ...headlineFields(),
        { key: 'intro_text',  type: 'textarea',   labelKey: 'field_body',  i18n: true,  group: 'group_intro', primary: true },
        { key: 'intro_align', type: 'text-align', labelKey: 'field_align', i18n: false, group: 'group_intro', options: TEXT_ALIGN_OPTS },
        { key: 'intro_size',  type: 'size',       labelKey: 'field_size',  i18n: false, group: 'group_intro', options: SIZE_OPTS, nullable: true },
        { key: 'intro_color', type: 'color',      labelKey: 'field_color', i18n: false, group: 'group_intro' },
        { key: 'album',   type: 'select', labelKey: 'field_album',   i18n: false, group: null, options: [{ value: 'all', labelKey: 'opt_all' }] },
        { key: 'layout',  type: 'select', labelKey: 'field_layout',  i18n: false, group: null,
          options: [{ value: 'gallery-bottom', labelKey: 'opt_gallery_bottom' }, { value: 'gallery-right', labelKey: 'opt_gallery_right' }, { value: 'gallery-left', labelKey: 'opt_gallery_left' }] },
        { key: 'columns', type: 'select', labelKey: 'field_columns', i18n: false, group: null, options: COLS3_OPTS },
        { key: 'section_bg_color', type: 'color', labelKey: 'field_section_bg',      i18n: false, group: 'group_adv_gallery', adv: true, last: true },
        { key: 'section_padding',  type: 'size',  labelKey: 'field_section_padding', i18n: false, group: 'group_adv_gallery', options: SIZE_OPTS, nullable: true, adv: true, last: true },
    ],
    'text-image': [
        ...headlineFields(),
        ...bodyFields(),
        { key: 'image',       type: 'image',  labelKey: 'field_image',       i18n: false, group: null },
        { key: 'layout',      type: 'select', labelKey: 'field_layout',      i18n: false, group: null,
          options: [
              { value: 'image-left',   labelKey: 'opt_img_left'   },
              { value: 'image-right',  labelKey: 'opt_img_right'  },
              { value: 'image-top',    labelKey: 'opt_img_top'    },
              { value: 'image-bottom', labelKey: 'opt_img_bottom' },
          ] },
        { key: 'button_text', type: 'text',   labelKey: 'field_button_text', i18n: true,  group: null },
        { key: 'button_url',  type: 'url',    labelKey: 'field_button_url',  i18n: false, group: null },
        { key: 'image_border_width', type: 'range', labelKey: 'field_image_border_width', i18n: false, group: 'group_adv_ti', min: 0, max: 10, adv: true, last: true },
        { key: 'image_border_color', type: 'color', labelKey: 'field_image_border_color', i18n: false, group: 'group_adv_ti', adv: true, last: true },
        { key: 'section_bg_color',   type: 'color', labelKey: 'field_section_bg',      i18n: false, group: 'group_adv_ti', adv: true, last: true },
        { key: 'section_padding',    type: 'size',  labelKey: 'field_section_padding', i18n: false, group: 'group_adv_ti', options: SIZE_OPTS, nullable: true, adv: true, last: true },
    ],
    'hochzeits-abc': [
        ...headlineFields(),
        { key: 'items', type: 'repeater', labelKey: 'repeater_add', i18n: false, group: null, cssClass: 'hw-repeater--labels-only',
          subSchema: [
              { key: 'term',        type: 'text',     labelKey: 'field_term',        i18n: false, hidden: true },
              { key: 'description', type: 'textarea', labelKey: 'field_description', i18n: true  },
              { key: 'hidden',      type: 'toggle',   labelKey: 'field_hide',        i18n: false },
          ] },
        { key: 'letter_size',      type: 'size',  labelKey: 'field_letter_size', i18n: false, group: 'group_adv_abc', options: SIZE_OPTS, nullable: true, adv: true, last: true },
        { key: 'desc_size',        type: 'size',  labelKey: 'field_desc_size',   i18n: false, group: 'group_adv_abc', options: SIZE_OPTS, nullable: true, adv: true, last: true },
        { key: 'letter_color',     type: 'color', labelKey: 'field_letter_color', i18n: false, group: 'group_adv_abc', adv: true, last: true },
        { key: 'text_color',       type: 'color', labelKey: 'field_text_color',   i18n: false, group: 'group_adv_abc', adv: true, last: true },
        { key: 'section_bg_color', type: 'color', labelKey: 'field_section_bg',   i18n: false, group: 'group_adv_abc', adv: true, last: true },
    ],
    'geschenketisch': [
        ...headlineFields(),
        { key: 'intro_text',  type: 'textarea',   labelKey: 'field_body',  i18n: true,  group: 'group_intro', primary: true },
        { key: 'intro_align', type: 'text-align', labelKey: 'field_align', i18n: false, group: 'group_intro', options: TEXT_ALIGN_OPTS },
        { key: 'intro_size',  type: 'size',       labelKey: 'field_size',  i18n: false, group: 'group_intro', options: SIZE_OPTS, nullable: true },
        { key: 'intro_color', type: 'color',      labelKey: 'field_color', i18n: false, group: 'group_intro' },
        { key: 'image',         type: 'image',    labelKey: 'field_image',          i18n: false, group: null },
        { key: 'items', type: 'repeater', labelKey: 'repeater_add', i18n: false, group: null,
          subSchema: [
              { key: 'title', type: 'text',     labelKey: 'field_title', i18n: true  },
              { key: 'note',  type: 'textarea', labelKey: 'field_note',  i18n: true  },
              { key: 'link',  type: 'url',      labelKey: 'field_link',  i18n: false },
          ] },
        { key: 'form_labels', type: 'repeater', labelKey: 'group_form_labels', i18n: false, group: 'group_form_labels', primary: true, cssClass: 'hw-repeater--labels-only',
          subSchema: [
            { key: 'text', type: 'text', labelKey: 'field_label_text', i18n: true },
          ],
        },
        { key: 'layout',           type: 'select', labelKey: 'field_layout',          i18n: false, group: 'group_adv_gifts',
          options: [{ value: 'list-right', labelKey: 'opt_list_right' }, { value: 'list-left', labelKey: 'opt_list_left' }], adv: true, last: true },
        { key: 'title_color',      type: 'color',  labelKey: 'field_title_color',     i18n: false, group: 'group_adv_gifts', adv: true, last: true },
        { key: 'title_size',       type: 'size',   labelKey: 'field_title_size',      i18n: false, group: 'group_adv_gifts', options: SIZE_OPTS, nullable: true, adv: true, last: true },
        { key: 'desc_color',       type: 'color',  labelKey: 'field_desc_color',      i18n: false, group: 'group_adv_gifts', adv: true, last: true },
        { key: 'desc_size',        type: 'size',   labelKey: 'field_desc_size',       i18n: false, group: 'group_adv_gifts', options: SIZE_OPTS, nullable: true, adv: true, last: true },
        { key: 'section_bg_color', type: 'color',  labelKey: 'field_section_bg',      i18n: false, group: 'group_adv_gifts', adv: true, last: true },
        { key: 'section_padding',  type: 'size',   labelKey: 'field_section_padding', i18n: false, group: 'group_adv_gifts', options: SIZE_OPTS, nullable: true, adv: true, last: true },
    ],
    'kuchenliste': [
        ...headlineFields(),
        { key: 'intro_text', type: 'textarea', labelKey: 'field_body',      i18n: true, group: 'group_intro', primary: true },
        { key: 'form_labels', type: 'repeater', labelKey: 'group_form_labels', i18n: false, group: 'group_form_labels', primary: true, cssClass: 'hw-repeater--labels-only',
          subSchema: [
            { key: 'text', type: 'text', labelKey: 'field_label_text', i18n: true },
          ],
        },
{ key: 'background_image',    type: 'image',      labelKey: 'field_bg_image',    i18n: false, group: 'group_bg', primary: true },
        { key: 'background_position', type: 'bg-position',labelKey: 'field_bg_position', i18n: false, group: 'group_bg', options: BG_POS_OPTS },
        { key: 'layout',           type: 'select', labelKey: 'field_layout',          i18n: false, group: 'group_adv_kuchen',
          options: [{ value: 'list-right', labelKey: 'opt_list_right' }, { value: 'list-left', labelKey: 'opt_list_left' }], adv: true, last: true },
        { key: 'section_padding',  type: 'size',  labelKey: 'field_section_padding', i18n: false, group: 'group_adv_kuchen', options: SIZE_OPTS, nullable: true, adv: true, last: true },
        { key: 'padding',          type: 'size',  labelKey: 'field_form_padding',    i18n: false, group: 'group_adv_kuchen', options: SIZE_OPTS, nullable: true, adv: true, last: true },
        { key: 'container_radius', type: 'size',  labelKey: 'field_radius',          i18n: false, group: 'group_adv_kuchen', options: RADIUS_OPTS, adv: true, last: true },
        { key: 'form_bg_color',    type: 'color', labelKey: 'field_form_bg',         i18n: false, group: 'group_adv_kuchen', adv: true, last: true },
        { key: 'col2_bg_color',    type: 'color', labelKey: 'field_col2_bg',         i18n: false, group: 'group_adv_kuchen', adv: true, last: true },
        { key: 'col2_opacity',     type: 'range', labelKey: 'field_opacity',         i18n: false, group: 'group_adv_kuchen', adv: true, last: true },
        { key: 'list_text_color',  type: 'color', labelKey: 'field_list_color',      i18n: false, group: 'group_adv_kuchen', adv: true, last: true },
    ],
    'abendprogramm': [
        ...headlineFields(),
        ...bodyFields(),
        { key: 'background_image',    type: 'image',       labelKey: 'field_bg_image',        i18n: false, group: 'group_bg', primary: true },
        { key: 'background_position', type: 'bg-position', labelKey: 'field_bg_position',     i18n: false, group: 'group_bg', options: BG_POS_OPTS },
        { key: 'content_align',       type: 'bg-position', labelKey: 'field_content_pos',     i18n: false, group: 'group_adv_abend', options: CONTENT_POS_OPTS, adv: true, last: true },
        { key: 'min_height',          type: 'range-dvh',   labelKey: 'field_min_height',      i18n: false, group: 'group_adv_abend', adv: true, last: true },
        { key: 'content_padding',     type: 'size',        labelKey: 'field_padding',         i18n: false, group: 'group_adv_abend', options: SIZE_OPTS, nullable: true, adv: true, last: true },
        { key: 'content_radius',      type: 'size',        labelKey: 'field_radius',          i18n: false, group: 'group_adv_abend', options: RADIUS_OPTS, adv: true, last: true },
        { key: 'overlay_color',       type: 'color',       labelKey: 'field_overlay_color',   i18n: false, group: 'group_adv_abend', adv: true, last: true },
        { key: 'overlay_opacity',     type: 'range',       labelKey: 'field_overlay_opacity', i18n: false, group: 'group_adv_abend', min: 0, max: 100, adv: true, last: true },
    ],
    'faq': [
        ...headlineFields(),
        { key: 'intro_text',  type: 'textarea',   labelKey: 'field_body',  i18n: true,  group: 'group_intro', primary: true },
        { key: 'intro_align', type: 'text-align', labelKey: 'field_align', i18n: false, group: 'group_intro', options: TEXT_ALIGN_OPTS },
        { key: 'intro_size',  type: 'size',       labelKey: 'field_size',  i18n: false, group: 'group_intro', options: SIZE_OPTS, nullable: true },
        { key: 'intro_color', type: 'color',      labelKey: 'field_color', i18n: false, group: 'group_intro' },
        { key: 'items', type: 'repeater', labelKey: 'repeater_add', i18n: false, group: null,
          subSchema: [
              { key: 'question', type: 'text',     labelKey: 'field_question', i18n: true },
              { key: 'answer',   type: 'textarea', labelKey: 'field_answer',   i18n: true },
          ] },
        { key: 'section_padding',      type: 'size',   labelKey: 'field_section_padding', i18n: false, group: 'group_adv_faq', options: SIZE_OPTS, nullable: true, adv: true, last: true },
        { key: 'layout',               type: 'select', labelKey: 'field_layout',          i18n: false, group: 'group_adv_faq',
          options: [{ value: 'acc-right', labelKey: 'opt_acc_right' }, { value: 'acc-left', labelKey: 'opt_acc_left' }], adv: true, last: true },
        { key: 'intro_bg_image',       type: 'image',  labelKey: 'field_intro_bg_image',  i18n: false, group: 'group_adv_faq', adv: true, last: true },
        { key: 'intro_bg_color',       type: 'color',  labelKey: 'field_intro_bg',        i18n: false, group: 'group_adv_faq', adv: true, last: true },
        { key: 'intro_overlay_color',  type: 'color',  labelKey: 'field_overlay_color',   i18n: false, group: 'group_adv_faq', adv: true, last: true },
        { key: 'intro_overlay_opacity',type: 'range',  labelKey: 'field_overlay_opacity', i18n: false, group: 'group_adv_faq', adv: true, last: true },
        { key: 'section_bg_color',     type: 'color',  labelKey: 'field_section_bg',      i18n: false, group: 'group_adv_faq', adv: true, last: true },
    ],
    'save-the-date': [
        ...headlineFields(),
        { key: 'date',          type: 'date',  labelKey: 'field_date',          i18n: false, group: null },
        { key: 'image_day',     type: 'image', labelKey: 'field_image_day',     i18n: false, group: null },
        { key: 'image_month',   type: 'image', labelKey: 'field_image_month',   i18n: false, group: null },
        { key: 'image_year',    type: 'image', labelKey: 'field_image_year',    i18n: false, group: null },
        { key: 'section_padding', type: 'size', labelKey: 'field_section_padding', i18n: false, group: null, options: SIZE_OPTS, nullable: true },
        { key: 'text1',         type: 'text',  labelKey: 'field_text1',         i18n: true,  group: null },
        { key: 'text2',         type: 'text',  labelKey: 'field_text2',         i18n: true,  group: null },
        { key: 'section_bg_color', type: 'color', labelKey: 'field_section_bg',      i18n: false, group: 'group_adv_std', adv: true, last: true },
        { key: 'overlay_opacity',  type: 'range', labelKey: 'field_overlay_opacity', i18n: false, group: 'group_adv_std', min: 0, max: 100, adv: true, last: true },
    ],
    'navigation': [
        ...headlineFields(),
        { key: 'items', type: 'repeater', labelKey: 'field_nav_items', i18n: false, group: null, cssClass: 'hw-repeater--labels-only',
          subSchema: [
              { key: 'label',  type: 'text',   labelKey: 'field_label', i18n: true  },
              { key: 'hidden', type: 'toggle', labelKey: 'field_hide',  i18n: false },
          ] },
        { key: 'section_bg_color',  type: 'color', labelKey: 'field_section_bg',      i18n: false, group: 'group_adv_nav', adv: true, last: true },
        { key: 'section_padding',   type: 'size',  labelKey: 'field_section_padding', i18n: false, group: 'group_adv_nav', options: SIZE_OPTS, nullable: true, adv: true, last: true },
        { key: 'link_color',       type: 'color', labelKey: 'field_link_color',       i18n: false, group: 'group_adv_nav', adv: true, last: true },
        { key: 'link_hover_color', type: 'color', labelKey: 'field_link_hover_color', i18n: false, group: 'group_adv_nav', adv: true, last: true },
        { key: 'nav_justify', type: 'align',  labelKey: 'field_nav_justify', i18n: false, group: 'group_adv_nav', options: NAV_JUSTIFY_OPTS, nullable: true, adv: true, last: true },
    ],
};

const ALL_LANGS = [
    { code: 'de', label: 'Deutsch' },
    { code: 'en', label: 'English' },
    { code: 'es', label: 'Español' },
    { code: 'fr', label: 'Français' },
    { code: 'it', label: 'Italiano' },
    { code: 'pt', label: 'Português' },
    { code: 'nl', label: 'Nederlands' },
    { code: 'pl', label: 'Polski' },
    { code: 'ru', label: 'Русский' },
    { code: 'tr', label: 'Türkçe' },
];

const SECTION_TYPES = [
    { type: 'hero-header',     icon: 'hgi-insert-top-image',      labelKey: 'section_hero',          descKey: 'section_hero_desc' },
    { type: 'intro',           icon: 'hgi-text-centerline-right',     labelKey: 'section_intro',         descKey: 'section_intro_desc' },
    { type: 'big-header',      icon: 'hgi-insert-top-image',     labelKey: 'section_bigheader',     descKey: 'section_bigheader_desc' },
    { type: 'countdown',       icon: 'hgi-hourglass',       labelKey: 'section_countdown',     descKey: 'section_countdown_desc' },
    { type: 'locations',       icon: 'hgi-location-01',   labelKey: 'section_locations',     descKey: 'section_locations_desc' },
    { type: 'quote',           icon: 'hgi-bubble-chat',      labelKey: 'section_quote',         descKey: 'section_quote_desc' },
    { type: 'witnesses',       icon: 'hgi-user-group',    labelKey: 'section_witnesses',     descKey: 'section_witnesses_desc' },
    { type: 'couple',          icon: 'hgi-user-love-02',  labelKey: 'section_couple',        descKey: 'section_couple_desc' },
    { type: 'program',         icon: 'hgi-calendar-02',   labelKey: 'section_program',       descKey: 'section_program_desc' },
    { type: 'accommodation',   icon: 'hgi-home-wifi',     labelKey: 'section_accommodation', descKey: 'section_accommodation_desc' },
    { type: 'rsvp',            icon: 'hgi-mail-01',       labelKey: 'section_rsvp',          descKey: 'section_rsvp_desc' },
    { type: 'gallery-section', icon: 'hgi-camera-01',          labelKey: 'section_gallery',       descKey: 'section_gallery_desc'       },
    { type: 'text-image',      icon: 'hgi-text-firstline-left',  labelKey: 'section_textimage',     descKey: 'section_textimage_desc'     },
    { type: 'save-the-date',   icon: 'hgi-calendar-love-02',     labelKey: 'section_savethedate',   descKey: 'section_savethedate_desc'   },
    { type: 'hochzeits-abc',   icon: 'hgi-book-open-01',         labelKey: 'section_abc',           descKey: 'section_abc_desc'           },
    { type: 'faq',             icon: 'hgi-help-circle',          labelKey: 'section_faq',           descKey: 'section_faq_desc'           },
    { type: 'geschenketisch',  icon: 'hgi-gift',                 labelKey: 'section_gifts',         descKey: 'section_gifts_desc'         },
    { type: 'kuchenliste',     icon: 'hgi-birthday-cake',        labelKey: 'section_cakes',         descKey: 'section_cakes_desc'         },
    { type: 'abendprogramm',   icon: 'hgi-moon-02',              labelKey: 'section_evening',       descKey: 'section_evening_desc'       },
    { type: 'navigation',      icon: 'hgi-menu-09',              labelKey: 'section_nav',           descKey: 'section_nav_desc'           },
];

// ── useSections ───────────────────────────────────────────────────────────────
function useSections() {
    const api      = useApi();
    const sections = ref([]);

    async function load() {
        sections.value = await api.get('/sections');
    }
    async function create(type) {
        const s = await api.post('/sections', { type });
        sections.value.push(s);
        return s;
    }
    async function update(id, payload) {
        const updated = await api.put('/sections/' + id, payload);
        const idx = sections.value.findIndex(s => s.id === id);
        if (idx !== -1) sections.value[idx] = updated;
        return updated;
    }
    async function remove(id) {
        await api.del('/sections/' + id);
        sections.value = sections.value.filter(s => s.id !== id);
    }
    async function reorder(ids) {
        await api.put('/sections/order', { ids });
    }

    return { sections, api, load, create, update, remove, reorder };
}

// ── useEditorDraft ────────────────────────────────────────────────────────────
const _editorDraft = reactive({
    isDirty: false, saving: false, saveOk: false,
    _saveFn: null, _discardFn: null,
});
function useEditorDraft() {
    const register   = (s, d) => { _editorDraft._saveFn = s; _editorDraft._discardFn = d; };
    const unregister = () => { _editorDraft._saveFn = null; _editorDraft._discardFn = null; _editorDraft.isDirty = false; _editorDraft.saving = false; _editorDraft.saveOk = false; };
    const markDirty  = () => { _editorDraft.isDirty = true;  _editorDraft.saveOk = false; };
    const markClean  = () => { _editorDraft.isDirty = false; };
    async function save() {
        if (!_editorDraft._saveFn) return;
        _editorDraft.saving = true;
        try { await _editorDraft._saveFn(); _editorDraft.isDirty = false; _editorDraft.saveOk = true; setTimeout(() => { _editorDraft.saveOk = false; }, 2000); }
        finally { _editorDraft.saving = false; }
    }
    function discard() { if (_editorDraft._discardFn) _editorDraft._discardFn(); _editorDraft.isDirty = false; _editorDraft.saveOk = false; }
    return { state: _editorDraft, register, unregister, markDirty, markClean, save, discard };
}

// ── MediaModal ────────────────────────────────────────────────────────────────
const MediaModal = {
    props: ['t', 'onSelect', 'onClose', 'currentUrl', 'folder'],  // folder: 'samples'|'mailheader'
    setup(props) {
        const api        = useApi();
        const tab        = ref('upload');
        const samples    = ref([]);
        const uploads    = ref([]);
        const loading    = ref(true);
        const uploading   = ref(false);
        const dragOver    = ref(false);
        const fileInput   = ref(null);
        const uploadError = ref(null);
        const _base      = window.HW_APP_BASE || window.location.origin;

        onMounted(async () => {
            try {
                const f    = props.folder || 'samples';
                const data = await api.get('/media' + (f !== 'samples' ? '?folder=' + encodeURIComponent(f) : ''));
                samples.value = data.samples || [];
                uploads.value = data.uploads || [];
            } finally { loading.value = false; }
        });

        function imgSrc(url, thumb) { return _base + '/' + (thumb || url); }

        async function deletePrev() {
            const prev = props.currentUrl;
            if (!prev || !prev.startsWith('uploads/user/')) return;
            try {
                await fetch(_base + '/api/index.php/media/delete', {
                    method: 'DELETE', credentials: 'same-origin',
                    headers: { 'Content-Type': 'application/json' },
                    body: JSON.stringify({ url: prev }),
                });
            } catch (_) { /* best-effort */ }
        }

        async function selectImg(img) {
            const prev = props.currentUrl;
            // Only delete a user upload if it's a different one than the selected image
            if (prev && prev.startsWith('uploads/user/') && prev !== img.url) {
                await deletePrev();
            }
            props.onSelect(img.url, img.thumb_url || null);
            props.onClose();
        }

        function openPicker() { fileInput.value?.click(); }
        function onDragOver(e)  { e.preventDefault(); dragOver.value = true; }
        function onDragLeave()  { dragOver.value = false; }
        function onDrop(e) {
            e.preventDefault();
            dragOver.value = false;
            const files = [...(e.dataTransfer?.files || [])].filter(f => f.type.startsWith('image/'));
            if (files.length) uploadFile(files[0]);
        }
        function onFileChange(e) {
            const files = [...(e.target.files || [])];
            if (files.length) uploadFile(files[0]);
            e.target.value = '';
        }

        async function uploadFile(file) {
            if (uploading.value) return;
            uploading.value = true;
            uploadError.value = null;
            try {
                const fd = new FormData();
                fd.append('file', file);
                const res  = await fetch(_base + '/api/index.php/media/upload', {
                    method: 'POST', credentials: 'same-origin', body: fd,
                });
                const json = await res.json();
                if (!json.success) throw new Error(json.error || 'Upload failed');
                await deletePrev();
                props.onSelect(json.data.url, json.data.thumb_url || null);
                props.onClose();
            } catch (err) {
                uploadError.value = err.message || 'Upload fehlgeschlagen';
            } finally {
                uploading.value = false;
            }
        }

        return { tab, samples, uploads, loading, uploading, uploadError, dragOver, fileInput,
                 imgSrc, selectImg, openPicker, onDragOver, onDragLeave, onDrop, onFileChange };
    },
    template: `
    <div class="hw-modal-backdrop" @click.self="onClose">
      <div class="hw-modal">
        <div class="hw-modal__header">
          <h3 class="h5 text-title">{{ t('media_title') }}</h3>
          <button class="button primitive hw-modal__close" @click="onClose">
            <i class="hgi-stroke hgi-cancel-01"></i>
          </button>
        </div>
        <div class="hw-modal__tabs">
          <button class="hw-modal__tab" :class="{'hw-modal__tab--active': tab==='upload'}"  @click="tab='upload'">{{ t('media_tab_upload') }}</button>
          <button class="hw-modal__tab" :class="{'hw-modal__tab--active': tab==='gallery'}" @click="tab='gallery'">{{ t('media_tab_gallery') }}</button>
        </div>
        <div class="hw-modal__body">

          <!-- Upload tab -->
          <template v-if="tab==='upload'">
            <div class="hw-upload-zone"
              :class="{'hw-upload-zone--over': dragOver, 'hw-upload-zone--busy': uploading}"
              @dragover="onDragOver" @dragleave="onDragLeave" @drop="onDrop"
              @click="openPicker"
            >
              <template v-if="uploading">
                <i class="hgi-stroke hgi-loading-03 hw-upload-zone__icon hw-upload-zone__icon--spin"></i>
                <span class="hw-upload-zone__label">{{ t('media_uploading') }}</span>
              </template>
              <template v-else>
                <i class="hgi-stroke hgi-upload-01 hw-upload-zone__icon"></i>
                <span class="hw-upload-zone__label">{{ t('media_drop_hint') }}</span>
                <div class="hw-upload-zone__btns">
                  <button type="button" class="primary is-small" @click.stop="openPicker">{{ t('media_upload_btn') }}</button>
                </div>
              </template>
              <input ref="fileInput" type="file" accept="image/jpeg,image/png,image/gif,image/webp"
                class="hw-upload-input" @change="onFileChange">
            </div>
            <p v-if="uploadError" class="hw-upload-error">{{ uploadError }}</p>
          </template>

          <!-- Gallery tab -->
          <template v-else>
            <div v-if="loading" class="hw-modal__empty">…</div>
            <template v-else>
              <template v-if="uploads.length">
                <p class="hw-media-section-label">{{ t('media_section_uploads') }}</p>
                <div class="hw-media-grid">
                  <button v-for="img in uploads" :key="img.url"
                    class="hw-media-item" :class="{'hw-media-item--active': currentUrl === img.url}"
                    @click="selectImg(img)" :title="img.name">
                    <img :src="imgSrc(img.url, img.thumb_url)" :alt="img.name" class="hw-media-item__img" loading="lazy">
                  </button>
                </div>
              </template>
              <template v-if="samples.length">
                <p class="hw-media-section-label" :class="{'hw-media-section-label--spaced': uploads.length}">{{ t('media_section_samples') }}</p>
                <div class="hw-media-grid">
                  <button v-for="img in samples" :key="img.url"
                    class="hw-media-item" :class="{'hw-media-item--active': currentUrl === img.url}"
                    @click="selectImg(img)" :title="img.name">
                    <img :src="imgSrc(img.url, img.thumb_url)" :alt="img.name" class="hw-media-item__img" loading="lazy">
                  </button>
                </div>
              </template>
              <div v-if="!uploads.length && !samples.length" class="hw-modal__empty">{{ t('media_empty') }}</div>
            </template>
          </template>

        </div>
      </div>
    </div>
    `,
};

// ── EmojiPicker ───────────────────────────────────────────────────────────────
const EmojiPickerWrapper = {
    props: ['modelValue', 'onUpdate'],
    setup(props) {
        const open = ref(false);
        const triggerRef = ref(null);
        const dropdownRef = ref(null);
        const dropdownStyle = ref({});

        function openPicker() {
            const rect = triggerRef.value.getBoundingClientRect();
            dropdownStyle.value = {
                position: 'fixed',
                top:  (rect.bottom + 4) + 'px',
                left: rect.left + 'px',
                zIndex: 200,
            };
            open.value = !open.value;
        }

        function onSelect(emoji) {
            // emoji.i is the character in vue3-emoji-picker; fall back for other builds
            const char = typeof emoji === 'string' ? emoji : emoji.i;
            if (char) props.onUpdate(char);
            open.value = false;
        }

        function onClickOutside(e) {
            const inTrigger  = triggerRef.value  && triggerRef.value.contains(e.target);
            const inDropdown = dropdownRef.value && dropdownRef.value.contains(e.target);
            if (!inTrigger && !inDropdown) open.value = false;
        }

        onMounted(() => document.addEventListener('click', onClickOutside, true));
        onUnmounted(() => document.removeEventListener('click', onClickOutside, true));

        return { open, openPicker, onSelect, triggerRef, dropdownRef, dropdownStyle };
    },
    template: `
    <div class="hw-emoji-picker" ref="triggerRef">
      <button type="button" class="hw-emoji-trigger" @click.stop="openPicker">
        <span v-if="modelValue" class="hw-emoji-current">{{ modelValue }}</span>
        <span v-else class="hw-emoji-placeholder"><i class="hgi-stroke hgi-smile"></i></span>
        <i class="hgi-stroke hgi-arrow-down-01" style="font-size:1.2rem"></i>
      </button>
      <teleport to="body">
        <div v-if="open" ref="dropdownRef" class="hw-emoji-dropdown" :style="dropdownStyle">
          <emoji-picker-lib
            :native="true"
            :disable-skin-tones="true"
            :display-recent="true"
            theme="dark"
            @select="onSelect"
          />
        </div>
      </teleport>
    </div>
    `,
};

// ── FormField ─────────────────────────────────────────────────────────────────
const FormField = {
    name: 'FormField',
    props: ['field', 'modelValue', 't', 'onUpdate', 'onOpenMedia'],
    components: { EmojiPickerWrapper },
    setup(props) {
        const onInput   = (e) => props.onUpdate(e.target.value);
        const onToggle  = ()  => props.onUpdate(!props.modelValue);
        const rmImage   = ()  => props.onUpdate('');

        // Color field: preset-Farben reaktiv auf aktives Design
        const colorPresetOpts = computed(() => buildColorPresetOpts());
        const knownPresetValues = computed(() => new Set(colorPresetOpts.value.map(o => o.value.toLowerCase())));
        // Lokaler State: true sobald Nutzer "Eigene Farbe" wählt ODER aktueller Wert kein Preset ist
        const isCustomColor = ref(
            typeof props.modelValue === 'string' && !!props.modelValue && !knownPresetValues.value.has(props.modelValue.toLowerCase())
        );
        function onColorSelect(e) {
            const v = e.target.value;
            if (v === '__custom__') {
                isCustomColor.value = true;
                // Behalte aktuellen Wert oder setze sinnvollen Default
                props.onUpdate(props.modelValue || '#b99954');
            } else {
                isCustomColor.value = false;
                props.onUpdate(v);
            }
        }

        const appBase = window.HW_APP_BASE || window.location.origin;
        function absUrl(url) { return url && !url.startsWith('http') ? appBase + '/' + url : (url || ''); }

        // image-pos: X/Y Slider (Wert: "50% 50%")
        const posX = computed(() => parseInt((props.modelValue || '50% 50%').split(' ')[0])  || 50);
        const posY = computed(() => parseInt((props.modelValue || '50% 50%').split(' ')[1])  || 50);
        const updatePosX = (e) => props.onUpdate(e.target.value + '% ' + posY.value + '%');
        const updatePosY = (e) => props.onUpdate(posX.value + '% ' + e.target.value + '%');

        // range/range-dvh: lokaler Ref stellt sicher, dass die Anzeige sofort beim Drag aktualisiert wird
        const rangeVal = ref(+(props.modelValue ?? props.field?.min ?? 0) || 0);
        watch(() => props.modelValue, v => { rangeVal.value = +(v ?? props.field?.min ?? 0) || 0; });
        const onRangeInput = (e) => { rangeVal.value = +e.target.value; props.onUpdate(e.target.value); };

        function copyVar(v) { navigator.clipboard && navigator.clipboard.writeText(v).catch(() => {}); }

        return { onInput, onToggle, rmImage, colorPresetOpts, isCustomColor, onColorSelect, absUrl, posX, posY, updatePosX, updatePosY, rangeVal, onRangeInput, resolveDesignColor, copyVar };
    },
    template: `
    <div class="hw-field">
      <label v-if="field.labelKey && field.type !== 'toggle'" class="hw-field__label">{{ t(field.labelKey) }}</label>

      <input v-if="field.type==='text'" type="text" class="select hw-field__input" :value="modelValue??''" @input="onInput">
      <input v-else-if="field.type==='url'"  type="url"  class="select hw-field__input" :value="modelValue??''" @input="onInput" placeholder="https://">
      <input v-else-if="field.type==='date'" type="date" class="select hw-field__input" :value="modelValue??''" @input="onInput">
      <input v-else-if="field.type==='time'" type="time" class="select hw-field__input" :value="modelValue??''" @input="onInput">

      <textarea v-else-if="field.type==='textarea'" class="select hw-field__input hw-field__textarea" :value="modelValue??''" @input="onInput" rows="4"></textarea>

      <select v-else-if="field.type==='select'" class="select hw-field__input" :value="modelValue??''" @change="onInput">
        <option v-for="opt in field.options" :key="opt.value" :value="opt.value">{{ opt.label ?? t(opt.labelKey) }}</option>
      </select>

      <!-- align: icon button-group (hgi-align-*) -->
      <div v-else-if="field.type==='align'" class="hw-btngroup">
        <button
          v-for="opt in field.options" :key="opt.value"
          type="button"
          class="hw-btngroup__btn"
          :class="{'hw-btngroup__btn--active': (modelValue??field.options[0].value) === opt.value}"
          :title="opt.value"
          @click="$props.onUpdate(opt.value)"
        ><i :class="['hgi-stroke', opt.icon]"></i></button>
      </div>

      <!-- text-align: icon button-group (hgi-text-align-*) -->
      <div v-else-if="field.type==='text-align'" class="hw-btngroup">
        <button
          v-for="opt in field.options" :key="opt.value"
          type="button"
          class="hw-btngroup__btn"
          :class="{'hw-btngroup__btn--active': (modelValue??'left') === opt.value}"
          :title="opt.value"
          @click="$props.onUpdate(opt.value)"
        ><i :class="['hgi-stroke', opt.icon]"></i></button>
      </div>

      <!-- image-align: icon button-group for image position -->
      <div v-else-if="field.type==='image-align'" class="hw-btngroup">
        <button
          v-for="opt in field.options" :key="opt.value"
          type="button"
          class="hw-btngroup__btn"
          :class="{'hw-btngroup__btn--active': (modelValue??'right') === opt.value}"
          :title="t(opt.labelKey)"
          @click="$props.onUpdate(opt.value)"
        ><i :class="['hgi-stroke', opt.icon]"></i></button>
      </div>

      <!-- size: S / M / XL / 2XL as stepper -->
      <div v-else-if="field.type==='size'" class="hw-stepper">
        <button type="button" class="hw-stepper__btn"
          :disabled="field.nullable ? !modelValue : field.options.findIndex(o=>o.value===(modelValue??field.options[1].value))===0"
          @click="field.nullable
            ? (field.options.findIndex(o=>o.value===modelValue)===0 ? $props.onUpdate(null) : $props.onUpdate(field.options[Math.max(0,field.options.findIndex(o=>o.value===modelValue)-1)].value))
            : $props.onUpdate(field.options[Math.max(0,field.options.findIndex(o=>o.value===(modelValue??field.options[1].value))-1)].value)">
          <i class="hgi-stroke hgi-minus-sign"></i>
        </button>
        <span class="hw-stepper__label">
          <template v-if="field.nullable && !modelValue"><i class="hgi-stroke hgi-cancel-01"></i></template>
          <template v-else>{{ field.options.find(o=>o.value===(modelValue??field.options[1].value))?.label }}</template>
        </span>
        <button type="button" class="hw-stepper__btn"
          :disabled="field.nullable
            ? field.options.findIndex(o=>o.value===modelValue)===field.options.length-1
            : field.options.findIndex(o=>o.value===(modelValue??field.options[1].value))===field.options.length-1"
          @click="field.nullable
            ? $props.onUpdate(!modelValue ? field.options[0].value : field.options[Math.min(field.options.length-1,field.options.findIndex(o=>o.value===modelValue)+1)].value)
            : $props.onUpdate(field.options[Math.min(field.options.length-1,field.options.findIndex(o=>o.value===(modelValue??field.options[1].value))+1)].value)">
          <i class="hgi-stroke hgi-plus-sign"></i>
        </button>
      </div>

      <!-- bg-position: 3×3 icon grid for background-position -->
      <div v-else-if="field.type==='bg-position'" class="hw-bgpos-grid">
        <button
          v-for="opt in field.options" :key="opt.value"
          type="button"
          class="hw-bgpos-btn"
          :class="{'hw-bgpos-btn--active': (modelValue??'center') === opt.value}"
          :title="opt.value"
          @click="$props.onUpdate(opt.value)"
        ><i :class="['hgi-stroke', opt.icon]"></i></button>
      </div>

      <div v-else-if="field.type==='color'" class="hw-color-field">
        <div class="hw-color-swatch-preview" :style="{background: resolveDesignColor(modelValue)||'#000'}"></div>
        <select class="select hw-field__input hw-color-select" :value="isCustomColor ? '__custom__' : (modelValue??'')" @change="onColorSelect">
          <option v-for="opt in colorPresetOpts" :key="opt.value" :value="opt.value">{{ opt.label }}</option>
          <option value="__custom__">Eigene Farbe …</option>
        </select>
        <template v-if="isCustomColor">
            <input type="color" class="hw-color-native" :value="modelValue??'#b99954'" @input="e => $props.onUpdate(e.target.value)">
            <input type="text" class="select hw-color-hex" :value="modelValue??''" @input="onInput" placeholder="#000000" maxlength="7">
        </template>
      </div>

      <div v-else-if="field.type==='range'" class="hw-range-field">
        <input type="range" class="hw-range" :min="field.min??0" :max="field.max??100" :value="rangeVal" @input="onRangeInput">
        <span class="hw-range-value">{{ rangeVal }}%</span>
      </div>

      <!-- range-dvh: wie range, aber Einheit dvh statt % -->
      <div v-else-if="field.type==='range-dvh'" class="hw-range-field">
        <input type="range" class="hw-range" :min="field.min||0" :max="field.max||100" :value="rangeVal" @input="onRangeInput">
        <span class="hw-range-value">{{ rangeVal }}<small>dvh</small></span>
      </div>

      <!-- image-pos: kompakte X/Y-Slider, Wert "X% Y%" -->
      <div v-else-if="field.type==='image-pos'" class="hw-imgpos-sliders">
        <div class="hw-imgpos-row">
          <span class="hw-imgpos-axis">X</span>
          <input type="range" class="hw-range" min="0" max="100" :value="posX" @input="updatePosX">
          <span class="hw-range-value">{{ posX }}%</span>
        </div>
        <div class="hw-imgpos-row">
          <span class="hw-imgpos-axis">Y</span>
          <input type="range" class="hw-range" min="0" max="100" :value="posY" @input="updatePosY">
          <span class="hw-range-value">{{ posY }}%</span>
        </div>
      </div>

      <label v-else-if="field.type==='toggle'" class="hw-toggle hw-toggle--labeled">
        <input type="checkbox" class="hw-toggle__input" :checked="!!modelValue" @change="onToggle">
        <span class="hw-toggle__track"><span class="hw-toggle__thumb"></span></span>
        <span v-if="field.labelKey" class="hw-toggle__label">{{ t(field.labelKey) }}</span>
      </label>

      <div v-else-if="field.type==='image'" class="hw-image-field">
        <div v-if="modelValue" class="hw-image-preview">
          <img :src="absUrl(modelValue)" class="hw-image-preview__img" alt="" loading="lazy">
          <div class="hw-image-preview__actions">
            <button type="button" class="hw-image-action" @click="onOpenMedia(field.key)">
              <i class="hgi-stroke hgi-image-01"></i> {{ t('media_select') }}
            </button>
            <button type="button" class="hw-image-action hw-image-action--remove" @click="rmImage">
              <i class="hgi-stroke hgi-cancel-01"></i>
            </button>
          </div>
        </div>
        <button v-else type="button" class="hw-image-placeholder" @click="onOpenMedia(field.key)">
          <i class="hgi-stroke hgi-image-01"></i>
          <span>{{ t('media_select') }}</span>
        </button>
      </div>

      <emoji-picker v-else-if="field.type==='emoji'" :modelValue="modelValue" :onUpdate="v => $props.onUpdate(v)" />

      <div v-else-if="field.type==='var-hint'" class="hw-var-hint">
        <span class="hw-var-hint__label">Variablen:</span>
        <code v-for="h in [
            {label:'{{name}}',           copy:'{{name | Lieber Gast}}'},
            {label:'{{begrüssung}}',     copy:'{{begrüssung | Herzlich willkommen}}'},
            {label:'{{hochzeitsdatum}}', copy:'{{hochzeitsdatum | bald}}'}
          ]" :key="h.label"
          @click="copyVar(h.copy)"
          :title="h.copy"
        >{{ h.label }}</code>
      </div>
    </div>
    `,
};

// ── FieldGroup ────────────────────────────────────────────────────────────────
// Jedes primary-Feld bildet eine "Unit" und sammelt die nachfolgenden
// non-primary non-adv Felder als Styling-Kinder (paint-board-Icon-Toggle).
// Felder mit adv:true erscheinen im "Erweitertes Styling"-Toggle ganz unten.
const FieldGroup = {
    props: ['groupKey', 'fields', 't', 'getVal', 'setVal', 'onOpenMedia'],
    components: { FormField },
    setup(props) {
        // Interleaving: primary → start unit; non-primary non-adv → append to last unit
        const units = computed(() => {
            const result = [];
            let cur = null;
            for (const f of props.fields) {
                if (f.primary) {
                    cur = { primary: f, styling: [] };
                    result.push(cur);
                } else if (!f.adv && cur) {
                    cur.styling.push(f);
                }
            }
            return result;
        });

        const advFields = computed(() => props.fields.filter(f => f.adv));
        const hasAdv    = computed(() => advFields.value.length > 0);

        // Per-unit open state (ephemeral)
        const unitOpen = reactive({});
        function toggleUnit(key) { unitOpen[key] = !unitOpen[key]; }

        // Adv toggle (persisted per group)
        const advKey  = 'hw_adv_' + props.groupKey;
        const advOpen = ref(localStorage.getItem(advKey) === 'true');
        function toggleAdv() {
            advOpen.value = !advOpen.value;
            localStorage.setItem(advKey, String(advOpen.value));
        }

        return { units, advFields, hasAdv, unitOpen, toggleUnit, advOpen, toggleAdv };
    },
    template: `
    <div class="hw-styling-group">
      <template v-for="unit in units" :key="unit.primary.key">
        <div :class="unit.styling.length ? 'hw-field-unit' : ''">
          <form-field
            :field="unit.primary" :modelValue="getVal(unit.primary)" :t="t"
            :onUpdate="val => setVal(unit.primary, val)" :onOpenMedia="onOpenMedia"
          />
          <button v-if="unit.styling.length"
            type="button"
            class="hw-unit-style-btn"
            :class="{'hw-unit-style-btn--open': unitOpen[unit.primary.key]}"
            :title="t('adv_styling')"
            @click="toggleUnit(unit.primary.key)"
          ><i class="hgi-stroke hgi-paint-board"></i></button>
        </div>
        <div v-if="unit.styling.length && unitOpen[unit.primary.key]" class="hw-unit-style-fields">
          <form-field
            v-for="sf in unit.styling" :key="sf.key"
            :field="sf" :modelValue="getVal(sf)" :t="t"
            :onUpdate="val => setVal(sf, val)" :onOpenMedia="onOpenMedia"
          />
        </div>
      </template>
      <button v-if="hasAdv"
        type="button"
        class="hw-adv-toggle"
        :class="{'hw-adv-toggle--open': advOpen}"
        @click="toggleAdv"
      >
        <span>{{ t('adv_styling') }}</span>
        <i class="hgi-stroke" :class="advOpen ? 'hgi-arrow-up-01' : 'hgi-arrow-down-01'"></i>
      </button>
      <div v-if="hasAdv && advOpen" class="hw-styling-group__extras">
        <form-field
          v-for="f in advFields" :key="f.key"
          :field="f" :modelValue="getVal(f)" :t="t"
          :onUpdate="val => setVal(f, val)" :onOpenMedia="onOpenMedia"
        />
      </div>
    </div>
    `,
};

// ── RepeaterField ─────────────────────────────────────────────────────────────
const RepeaterField = {
    props: ['field', 'items', 't', 'activeLang', 'onUpdate', 'onOpenMedia', 'appConfig'],
    components: { FormField },
    setup(props) {
        const confirmIdx = ref(null);
        const openIdx    = ref(null);
        const visibleSubSchema = computed(() =>
            (props.field.subSchema || []).filter(sf => !sf.hidden && (!sf.hideWhen || !props.appConfig?.[sf.hideWhen]))
        );

        function toggle(idx) {
            openIdx.value = openIdx.value === idx ? null : idx;
        }
        function previewLabel(item) {
            const sf = props.field.subSchema?.find(f => f.type === 'text' || f.type === 'textarea');
            if (!sf) return null;
            const val = sf.i18n ? (item[props.activeLang]?.[sf.key] || '') : (item[sf.key] || '');
            return val || null;
        }
        function get(item, sf) {
            if (sf.i18n) return item[props.activeLang]?.[sf.key] ?? '';
            return item[sf.key] ?? '';
        }
        function set(item, sf, val) {
            if (sf.i18n) {
                if (!item[props.activeLang]) item[props.activeLang] = {};
                item[props.activeLang][sf.key] = val;
            } else { item[sf.key] = val; }
            props.onUpdate([...props.items]);
        }
        function add() {
            if (props.field.max && props.items.length >= props.field.max) return;
            const newItem = {};
            for (const sf of props.field.subSchema) {
                if (sf.i18n) { newItem[props.activeLang] = newItem[props.activeLang] || {}; newItem[props.activeLang][sf.key] = ''; }
                else newItem[sf.key] = sf.type === 'toggle' ? false : '';
            }
            openIdx.value = props.items.length;
            props.onUpdate([...props.items, newItem]);
        }
        function del(idx) {
            if (confirmIdx.value === idx) {
                props.onUpdate(props.items.filter((_,i) => i !== idx));
                confirmIdx.value = null;
                if (openIdx.value === idx) openIdx.value = null;
                else if (openIdx.value !== null && openIdx.value > idx) openIdx.value--;
            } else {
                confirmIdx.value = idx;
                setTimeout(() => { if (confirmIdx.value === idx) confirmIdx.value = null; }, 3000);
            }
        }
        function move(idx, dir) {
            const arr = [...props.items];
            const to  = idx + dir;
            if (to < 0 || to >= arr.length) return;
            [arr[idx], arr[to]] = [arr[to], arr[idx]];
            if (openIdx.value === idx) openIdx.value = to;
            else if (openIdx.value === to) openIdx.value = idx;
            props.onUpdate(arr);
        }

        return { confirmIdx, openIdx, get, set, add, del, move, toggle, previewLabel, visibleSubSchema };
    },
    template: `
    <div :class="['hw-repeater', field.cssClass || '']">
      <div v-for="(item, idx) in items" :key="idx" class="hw-repeater__item">
        <div class="hw-repeater__item-header" :class="{'hw-repeater__item-header--open': openIdx===idx}" @click="toggle(idx)">
          <span class="hw-repeater__item-label">
            <span class="hw-repeater__item-num">#{{ idx + 1 }}</span>
            <span v-if="previewLabel(item) && openIdx!==idx" class="hw-repeater__item-preview">{{ previewLabel(item) }}</span>
          </span>
          <div class="hw-repeater__item-actions">
            <button type="button" class="button primitive hw-repeater__btn" @click.stop="move(idx,-1)" :disabled="idx===0"><i class="hgi-stroke hgi-arrow-up-01"></i></button>
            <button type="button" class="button primitive hw-repeater__btn" @click.stop="move(idx,1)" :disabled="idx===items.length-1"><i class="hgi-stroke hgi-arrow-down-01"></i></button>
            <button type="button" class="button primitive hw-repeater__btn hw-repeater__btn--delete" :class="{'hw-repeater__btn--confirm':confirmIdx===idx}" @click.stop="del(idx)">
              <i class="hgi-stroke hgi-delete-01"></i>
              <span v-if="confirmIdx===idx" class="hw-repeater__confirm-text">{{ t('repeater_confirm') }}</span>
            </button>
            <i class="hgi-stroke hw-repeater__chevron" :class="openIdx===idx ? 'hgi-arrow-up-01' : 'hgi-arrow-down-01'"></i>
          </div>
        </div>
        <div v-if="openIdx===idx" class="hw-repeater__item-fields">
          <form-field
            v-for="sf in visibleSubSchema" :key="sf.key"
            :field="sf" :modelValue="get(item,sf)" :t="t"
            :onUpdate="val => set(item,sf,val)"
            :onOpenMedia="key => onOpenMedia(key, field.key, idx)"
          />
        </div>
      </div>
      <button type="button" class="hw-repeater__add" @click="add"
        v-if="!field.max || items.length < field.max">
        <i class="hgi-stroke hgi-add-01"></i> {{ t('repeater_add') }}
      </button>
    </div>
    `,
};

// ── CollapsibleRepeaterGroup ──────────────────────────────────────────────────
const CollapsibleRepeaterGroup = {
    props: ['groupKey', 'field', 't', 'items', 'onUpdate', 'onOpenMedia', 'activeLang', 'appConfig'],
    components: { RepeaterField },
    setup(props) {
        const storageKey = 'hw_grp_' + props.groupKey;
        const open = ref(localStorage.getItem(storageKey) === 'true');
        function toggle() { open.value = !open.value; localStorage.setItem(storageKey, String(open.value)); }
        return { open, toggle };
    },
    template: `
    <div class="hw-styling-group">
      <button type="button" class="hw-adv-toggle" :class="{'hw-adv-toggle--open': open}" @click="toggle">
        <span>{{ t(groupKey) }}</span>
        <i class="hgi-stroke" :class="open ? 'hgi-arrow-up-01' : 'hgi-arrow-down-01'"></i>
      </button>
      <div v-if="open" class="hw-styling-group__extras">
        <repeater-field :field="field" :items="items" :t="t" :activeLang="activeLang"
          :onUpdate="onUpdate" :onOpenMedia="onOpenMedia" :appConfig="appConfig" />
      </div>
    </div>
    `,
};

// ── SectionEditor ─────────────────────────────────────────────────────────────
const SectionEditor = {
    props: ['section', 'draftData', 'sections', 't', 'activeLang', 'langs', 'onSave', 'onBack', 'onDelete', 'onDraftChange', 'onDiscard', 'onLangChange'],
    components: { FormField, FieldGroup, RepeaterField, MediaModal },
    setup(props) {
        // Draft aus persistiertem draftData laden (überlebt Navigation), fallback auf section
        const draft    = ref(JSON.parse(JSON.stringify(props.draftData || props.section)));
        const isDirty  = ref(false);
        const saving   = ref(false);
        const saveOk   = ref(false);
        const mediaKey = ref(null);
        const deleteConfirm = ref(false);

        const _gDraft = useEditorDraft();
        function checkDirty() {
            isDirty.value = JSON.stringify(draft.value.data) !== JSON.stringify(props.section.data)
                         || draft.value.visible !== props.section.visible;
            if (isDirty.value) _gDraft.markDirty(); else _gDraft.markClean();
        }

        const schema  = computed(() => {
            const base = SECTION_SCHEMAS[draft.value.type] || [];
            if (draft.value.type !== 'gallery-section') return base;
            return base.map(f => {
                if (f.key !== 'album') return f;
                const albumOpts = _gallState.albums.map(a => ({ value: String(a.id), label: a.name }));
                return { ...f, options: [{ value: 'all', labelKey: 'opt_all' }, ...albumOpts] };
            });
        });
        const hasI18n = computed(() => schema.value.some(f => f.i18n || (f.subSchema || []).some(sf => sf.i18n)));

        // isDirty initial prüfen (Draft könnte bereits Änderungen enthalten)
        checkDirty();

        // ── Navigation: sync repeater items with current sections list ────────
        function syncNavItems() {
            if (draft.value.type !== 'navigation') return;
            const otherSections = (props.sections || []).filter(s => s.id !== draft.value.id && s.type !== 'navigation');
            const existing      = draft.value.data.items || [];
            const existingMap   = Object.fromEntries(existing.map(item => [item.section_id, item]));

            draft.value.data.items = otherSections.map(s => {
                if (existingMap[s.id]) {
                    const item = existingMap[s.id];
                    const found = SECTION_TYPES.find(st => st.type === s.type);
                    if (found) {
                        Object.keys(LANGS).forEach(lang => {
                            if (!item[lang]?.label) {
                                item[lang] = { ...(item[lang] || {}), label: LANGS[lang][found.labelKey] || s.type };
                            }
                        });
                    }
                    return item;
                }
                const found = SECTION_TYPES.find(st => st.type === s.type);
                const langDefaults = {};
                Object.keys(LANGS).forEach(lang => {
                    const dict = LANGS[lang];
                    langDefaults[lang] = { label: found ? (dict[found.labelKey] || s.type) : s.type };
                });
                return { section_id: s.id, ...langDefaults, hidden: false };
            });
        }
        syncNavItems();

        const groupedFields = computed(() => {
            const groups = new Map();
            const lastGroups = new Map();
            const repeaterLastGroups = [];
            const ungrouped = [];
            for (const field of schema.value) {
                if (field.showWhen && !draft.value.data?.[field.showWhen]) continue;
                if (field.type === 'repeater' && field.last && field.group) {
                    repeaterLastGroups.push({ groupKey: field.group, field });
                } else if (field.type === 'repeater') {
                    ungrouped.push({ field, isRepeater: true });
                } else if (field.group) {
                    const target = field.last ? lastGroups : groups;
                    if (!target.has(field.group)) target.set(field.group, []);
                    target.get(field.group).push(field);
                } else {
                    ungrouped.push({ field, isRepeater: false });
                }
            }
            return { groups, lastGroups, repeaterLastGroups, ungrouped };
        });

        function getVal(field) {
            if (field.i18n) {
                const lang = props.activeLang || 'de';
                return draft.value.data?.[lang]?.[field.key] ?? '';
            }
            return draft.value.data?.[field.key] ?? '';
        }

        function setVal(field, val) {
            if (field.i18n) {
                const lang = props.activeLang || 'de';
                if (!draft.value.data[lang]) draft.value.data[lang] = {};
                draft.value.data[lang][field.key] = val;
            } else {
                draft.value.data[field.key] = val;
            }
            saveOk.value = false;
            props.onDraftChange?.(draft.value.id, draft.value);
            checkDirty();
            sendDraft();
        }

        function setRepeater(field, val) {
            draft.value.data[field.key] = val;
            saveOk.value = false;
            props.onDraftChange?.(draft.value.id, draft.value);
            checkDirty();
            sendDraft();
        }

        async function toggleVisible() {
            draft.value.visible = !draft.value.visible;
            props.onDraftChange?.(draft.value.id, draft.value);
            sendDraft();
            await props.onSave(draft.value.id, { visible: draft.value.visible, data: draft.value.data });
            checkDirty();
        }

        function sendDraft() {
            const frame = document.getElementById('hw-preview-frame');
            if (frame) frame.contentWindow.postMessage({ type: 'hw-draft-update', section: JSON.parse(JSON.stringify(draft.value)) }, '*');
        }

        async function save() {
            saving.value = true;
            try { await props.onSave(draft.value.id, {visible: draft.value.visible, data: draft.value.data}); isDirty.value = false; saveOk.value = true; _gDraft.markClean(); setTimeout(() => saveOk.value = false, 2000); }
            finally { saving.value = false; }
        }
        function discard() {
            props.onDiscard?.(draft.value.id);
            draft.value = JSON.parse(JSON.stringify(props.section));
            isDirty.value = false; _gDraft.markClean(); sendDraft();
        }
        onMounted(() => {
            _gDraft.register(save, discard);
            if (draft.value.type === 'gallery-section') _loadGallery();
        });
        onUnmounted(() => _gDraft.unregister());

        function openMedia(key, repeaterKey = null, repeaterIdx = null) {
            mediaKey.value = repeaterKey !== null ? { key, repeaterKey, repeaterIdx } : key;
        }
        function closeMedia() { mediaKey.value = null; }
        const currentMediaUrl = computed(() => {
            const ctx = mediaKey.value;
            if (!ctx) return null;
            if (typeof ctx === 'object') {
                const items = draft.value.data[ctx.repeaterKey] || [];
                return items[ctx.repeaterIdx]?.[ctx.key] || null;
            }
            return draft.value.data[ctx] || null;
        });
        function selectMedia(url) {
            const ctx = mediaKey.value;
            if (!ctx) return;
            if (typeof ctx === 'object') {
                // Image field inside a repeater item
                const items = [...(draft.value.data[ctx.repeaterKey] || [])];
                if (ctx.repeaterIdx < items.length) {
                    items[ctx.repeaterIdx] = { ...items[ctx.repeaterIdx], [ctx.key]: url };
                    const rf = schema.value.find(f => f.key === ctx.repeaterKey);
                    if (rf) setRepeater(rf, items);
                }
            } else {
                const field = schema.value.find(f => f.key === ctx);
                if (field) setVal(field, url);
            }
        }

        function tryDelete() {
            if (deleteConfirm.value) {
                props.onDelete(draft.value.id);
            } else {
                deleteConfirm.value = true;
                setTimeout(() => deleteConfirm.value = false, 3000);
            }
        }

        const sectionLabel = computed(() => {
            const meta = SECTION_TYPES.find(s => s.type === draft.value.type);
            const base = meta ? props.t(meta.labelKey) : draft.value.type;
            const list = props.sections || [];
            const siblings = list.filter(x => x.type === draft.value.type);
            if (siblings.length <= 1) return base;
            return base + ' (' + (siblings.findIndex(x => x.id === draft.value.id) + 1) + ')';
        });

        const showVars  = ref(false);
        const hasGuests = computed(() => _guestState.guests.length > 0);
        function copyVarClip(v) { navigator.clipboard && navigator.clipboard.writeText(v).catch(() => {}); }
        const varHints  = [
            { label: '{{name}}',           copy: '{{name | Lieber Gast}}' },
            { label: '{{begrüssung}}',     copy: '{{begrüssung | Herzlich willkommen}}' },
            { label: '{{hochzeitsdatum}}', copy: '{{hochzeitsdatum | bald}}' },
            { label: '{{websitelink}}',    copy: '{{websitelink | Link}}' },
            { label: '{{rsvp_link}}',      copy: '{{rsvp_link | RSVP-Link}}' },
        ];
        onMounted(() => { _loadGuests(); });

        return {
            draft, isDirty, saving, saveOk, mediaKey, deleteConfirm,
            schema, groupedFields, hasI18n,
            getVal, setVal, setRepeater,
            toggleVisible, save, discard,
            openMedia, closeMedia, selectMedia,
            sectionLabel, tryDelete,
            globalSettings: _dashState.settings,
            showVars, hasGuests, copyVarClip, varHints,
        };
    },
    template: `
    <div class="hw-section-editor">
      <div class="hw-section-editor__header">
        <button type="button" class="button primitive hw-back-btn" @click="onBack">
          <i class="hgi-stroke hgi-arrow-left-01"></i>
          <span class="hw-section-editor__title">{{ sectionLabel }}</span>
        </button>
        <label class="hw-toggle hw-toggle--small" :title="draft.visible ? t('section_visible') : t('section_hidden')">
          <input type="checkbox" class="hw-toggle__input" :checked="draft.visible" @change="toggleVisible">
          <span class="hw-toggle__track"><span class="hw-toggle__thumb"></span></span>
        </label>
      </div>
      <div v-if="hasI18n && langs && langs.length > 1" class="hw-section-editor__langbar">
        <button v-for="l in langs" :key="l.code" type="button"
          class="hw-langbar__btn"
          :class="{'hw-langbar__btn--active': activeLang === l.code}"
          @click="onLangChange && onLangChange(l.code)"
        >{{ l.code.toUpperCase() }}</button>
      </div>

      <div class="hw-section-editor__fields">
        <field-group
          v-for="[gk, fields] in groupedFields.groups" :key="gk"
          :groupKey="gk" :fields="fields" :t="t"
          :getVal="getVal" :setVal="setVal" :onOpenMedia="openMedia"
        />

        <template v-for="item in groupedFields.ungrouped" :key="item.field.key">
          <repeater-field v-if="item.isRepeater" :field="item.field" :items="draft.data[item.field.key]||[]" :t="t" :activeLang="activeLang" :onUpdate="val=>setRepeater(item.field,val)" :onOpenMedia="openMedia" :appConfig="globalSettings" />
          <form-field v-else :field="item.field" :modelValue="getVal(item.field)" :t="t" :onUpdate="val=>setVal(item.field,val)" :onOpenMedia="openMedia" />
        </template>

        <collapsible-repeater-group
          v-for="item in groupedFields.repeaterLastGroups" :key="item.groupKey"
          :groupKey="item.groupKey" :field="item.field"
          :items="draft.data[item.field.key]||[]"
          :t="t" :activeLang="activeLang"
          :onUpdate="val => setRepeater(item.field, val)"
          :onOpenMedia="openMedia"
          :appConfig="globalSettings"
        />

        <field-group
          v-for="[gk, fields] in groupedFields.lastGroups" :key="gk"
          :groupKey="gk" :fields="fields" :t="t"
          :getVal="getVal" :setVal="setVal" :onOpenMedia="openMedia"
        />

        <div v-if="hasGuests" class="hw-var-toggle">
          <button type="button"
            class="hw-var-toggle__btn"
            :class="{'hw-var-toggle__btn--active': showVars}"
            @click="showVars = !showVars"
          >
            <i class="hgi-stroke hgi-third-bracket"></i>
            {{ t('var_hint_toggle') }}
          </button>
          <div v-if="showVars" class="hw-var-hint hw-var-hint--editor">
            <span class="hw-var-hint__label">{{ t('var_hint_label') }}</span>
            <code v-for="h in varHints" :key="h.label"
              @click="copyVarClip(h.copy)"
              :title="h.copy"
            >{{ h.label }}</code>
          </div>
        </div>
        <div class="hw-section-editor__delete">
          <button type="button" class="hw-delete-btn" :class="{'hw-delete-btn--confirm':deleteConfirm}" @click="tryDelete">
            <i class="hgi-stroke hgi-delete-01"></i>
            {{ deleteConfirm ? t('confirm_delete') : t('delete') }}
          </button>
        </div>
      </div>

      <!-- save/discard via global panel footer -->

      <media-modal v-if="mediaKey" :t="t" :onSelect="selectMedia" :onClose="closeMedia" :currentUrl="currentMediaUrl" />
    </div>
    `,
};

// ── SectionsTab ───────────────────────────────────────────────────────────────
const SectionsTab = {
    props: ['t', 'config'],
    components: { SectionEditor },
    setup(props) {
        const { sections, load, create, update, remove, reorder } = useSections();
        const view        = ref('list');   // 'list' | 'add' | 'edit'
        const editSection = ref(null);
        const listEl      = ref(null);
        const activeLang  = ref('de');
        const activeLangs = ref([{ code: 'de', label: 'Deutsch' }]);
        const drafts      = reactive({});  // { [id]: draftData } — überlebt Navigation
        const addErr      = ref('');
        let sortableInst  = null;

        onMounted(async () => { await load(); nextTick(initSortable); });

        // Load active languages as soon as multilang config is known (may arrive after mount)
        watch(() => props.config?.addons?.multilang, async (enabled) => {
            if (!enabled) return;
            try {
                const data = await useApi().get('/languages');
                activeLangs.value = (data.available || []).filter(l => l.active);
                activeLang.value  = data.default || 'de';
            } catch(e) {}
        }, { immediate: true });
        watch(view, v => { if (v === 'list') nextTick(initSortable); });

        function initSortable() {
            if (!listEl.value) return;
            if (sortableInst) { sortableInst.destroy(); sortableInst = null; }
            sortableInst = Sortable.create(listEl.value, {
                handle: '.hw-section-card__handle',
                animation: 150,
                onEnd: async () => {
                    const ids = [...listEl.value.querySelectorAll('[data-id]')].map(el => el.dataset.id);
                    await reorder(ids);
                    await load();
                    reloadPreview();
                }
            });
        }

        const sectionLabel = (s, list) => {
            const meta = SECTION_TYPES.find(m => m.type === s.type);
            const base = meta ? props.t(meta.labelKey) : s.type;
            const siblings = (list || sections.value).filter(x => x.type === s.type);
            if (siblings.length <= 1) return base;
            return base + ' (' + (siblings.findIndex(x => x.id === s.id) + 1) + ')';
        };

        async function addSection(type) {
            addErr.value = '';
            try {
                const s = await create(type);
                drafts[s.id] = JSON.parse(JSON.stringify(s));
                editSection.value = s;
                view.value = 'edit';
            } catch(e) {
                addErr.value = e.message || 'Fehler beim Erstellen des Abschnitts';
            }
        }

        function openEdit(s) {
            // Draft vorhanden? Laden. Sonst frischer Clone aus API-Stand.
            if (!drafts[s.id]) {
                drafts[s.id] = JSON.parse(JSON.stringify(s));
            }
            editSection.value = s;
            view.value = 'edit';
            const frame = document.getElementById('hw-preview-frame');
            if (frame) frame.contentWindow.postMessage({ type: 'hw-scroll-to-section', id: s.id }, '*');
        }

        function onDraftChange(id, draftData) {
            drafts[id] = JSON.parse(JSON.stringify(draftData));
        }

        function onDiscard(id) {
            const saved = sections.value.find(s => s.id === id);
            if (saved) drafts[id] = JSON.parse(JSON.stringify(saved));
        }

        async function onSave(id, payload) {
            const updated = await update(id, payload);
            // Draft nach Speichern mit gespeichertem Stand synchronisieren
            drafts[id] = JSON.parse(JSON.stringify(updated));
            editSection.value = updated;
            reloadPreview();
        }

        async function onDelete(id) {
            await remove(id);
            delete drafts[id];
            view.value = 'list';
            editSection.value = null;
            reloadPreview();
        }

        async function toggleVisible(s) {
            await update(s.id, { visible: !s.visible, data: s.data });
            reloadPreview();
        }

        function reloadPreview() {
            const frame = document.getElementById('hw-preview-frame');
            if (frame) frame.contentWindow.location.reload();
        }

        return {
            sections, view, editSection, activeLang, activeLangs, listEl, drafts, addErr,
            SECTION_TYPES, sectionLabel,
            addSection, openEdit, onSave, onDelete, toggleVisible,
            onDraftChange, onDiscard,
        };
    },
    template: `
    <!-- LIST -->
    <div v-if="view==='list'" class="hw-tab-content">
      <div class="hw-sections-header">
        <h2 class="hw-tabtitle">{{ t('nav_sections') }}</h2>
        <button class="hw-sections-add-btn" @click="view='add'">
          <i class="hgi-stroke hgi-add-01"></i> {{ t('editor_add_section') }}
        </button>
      </div>

      <div class="hw-section-list" ref="listEl">
        <div v-for="s in sections" :key="s.id" :data-id="s.id" class="hw-section-card" :class="{'hw-section-card--hidden':!s.visible}">
          <span class="hw-section-card__handle"><i class="hgi-stroke hgi-drag-drop-vertical"></i></span>
          <span class="hw-section-card__name">{{ sectionLabel(s) }}</span>
          <div class="hw-section-card__actions">
            <button class="button hw-section-card__btn" @click="toggleVisible(s)">
              <i :class="['hgi-stroke', s.visible ? 'hgi-view' : 'hgi-view-off']"></i>
            </button>
            <button class="button hw-section-card__btn" @click="openEdit(s)">
              <i class="hgi-stroke hgi-pencil-edit-01"></i>
            </button>
          </div>
        </div>
        <div v-if="sections.length===0" class="hw-sections-empty">
          <i class="hgi-stroke hgi-layout-02"></i>
          <p>Noch keine Abschnitte. Füge deinen ersten hinzu!</p>
        </div>
      </div>
    </div>

    <!-- ADD -->
    <div v-else-if="view==='add'" class="hw-tab-content">
      <div class="hw-section-editor__header hw-section-editor__header--flush">
        <button type="button" class="button primitive hw-back-btn" @click="view='list'">
          <i class="hgi-stroke hgi-arrow-left-01"></i>
          <span class="hw-section-editor__title">{{ t('editor_add_section') }}</span>
        </button>
      </div>
      <div v-if="addErr" class="hw-upload-errors"><i class="hgi-stroke hgi-alert-circle"></i> {{ addErr }}</div>
      <div class="hw-section-picker">
        <button v-for="meta in SECTION_TYPES" :key="meta.type" class="hw-section-picker-item" @click="addSection(meta.type)">
          <i :class="['hgi-stroke', meta.icon, 'hw-section-picker-item__icon']"></i>
          <span class="hw-section-picker-item__label">{{ t(meta.labelKey) }}</span>
          <span class="hw-section-picker-item__desc">{{ t(meta.descKey) }}</span>
        </button>
      </div>
    </div>

    <!-- EDIT -->
    <section-editor
      v-else-if="view==='edit' && editSection"
      :section="editSection"
      :draft-data="drafts[editSection.id]"
      :sections="sections"
      :t="t"
      :activeLang="activeLang"
      :langs="activeLangs"
      :onSave="onSave"
      :onBack="() => view='list'"
      :onDelete="onDelete"
      :onDraftChange="onDraftChange"
      :onDiscard="onDiscard"
      :onLangChange="l => activeLang = l"
    />
    `,
};

// ── LanguagesTab ──────────────────────────────────────────────────────────────
const LanguagesTab = {
    props: ['t'],
    template: `
    <div class="hw-tab-content">
      <h2 class="hw-tabtitle">{{ t('nav_languages') }}</h2>
      <p class="hw-tabdescription">{{ t('lang_desc_main') }}</p>
    </div>
    `,
};

// ── LanguagesMain ─────────────────────────────────────────────────────────────
const LanguagesMain = {
    props: ['t'],
    setup() {
        const api         = useApi();
        const defaultLang = ref('de');
        const active      = reactive({});
        const _gDraft     = useEditorDraft();
        let   _orig       = {};

        ALL_LANGS.forEach(l => { active[l.code] = false; });

        onMounted(async () => {
            try {
                const data = await api.get('/languages');
                defaultLang.value = data.default || 'de';
                (data.available || []).forEach(l => { active[l.code] = !!l.active; });
                _orig = { default: defaultLang.value, active: { ...active } };
            } catch(e) {}
            _gDraft.register(save, discard);
        });
        onUnmounted(() => _gDraft.unregister());

        function toggle(code) {
            if (code === defaultLang.value) return;
            active[code] = !active[code];
            _gDraft.markDirty();
        }
        function setDefault(code) {
            active[code] = true;
            defaultLang.value = code;
            _gDraft.markDirty();
        }

        async function save() {
            try {
                const available = ALL_LANGS.map(l => ({ code: l.code, active: !!active[l.code] }));
                const result    = await api.put('/languages', { default: defaultLang.value, available });
                defaultLang.value = result.default || defaultLang.value;
                (result.available || []).forEach(l => { active[l.code] = !!l.active; });
                _orig = { default: defaultLang.value, active: { ...active } };
                const _nowActive = ALL_LANGS.filter(l => !!active[l.code]);
                _guestState.langs = _nowActive.length ? _nowActive : [ALL_LANGS.find(l => l.code === defaultLang.value)];
                _gDraft.markClean();
            } catch(e) {}
        }
        function discard() {
            defaultLang.value = _orig.default || 'de';
            Object.assign(active, _orig.active || {});
            _gDraft.markClean();
        }

        return { ALL_LANGS, defaultLang, active, toggle, setDefault };
    },
    template: `
    <div class="hw-dash-main">
      <div class="grid-min-40 grid-gap">
        <div class="card hw-dash-card">
          <h3 class="h5 text-title">{{ t('nav_languages') }}</h3>
          <div class="hw-lang-list">
            <div v-for="l in ALL_LANGS" :key="l.code" class="hw-lang-row">
              <label class="hw-toggle hw-toggle--labeled">
                <input type="checkbox" class="hw-toggle__input"
                  :checked="!!active[l.code]"
                  :disabled="l.code === defaultLang"
                  @change="toggle(l.code)"
                >
                <span class="hw-toggle__track"><span class="hw-toggle__thumb"></span></span>
                <span class="hw-toggle__label">{{ l.label }}</span>
              </label>
              <span v-if="l.code === defaultLang" class="hw-lang-badge">{{ t('lang_badge_default') }}</span>
              <button v-else-if="active[l.code]" type="button" class="hw-lang-default-btn" @click="setDefault(l.code)">
                {{ t('lang_set_default') }}
              </button>
            </div>
          </div>
          <p class="text-s" style="color:var(--dark-50);margin:0">{{ t('lang_desc_fallback') }}</p>
        </div>
      </div>
    </div>
    `,
};

// ── DesignTab ─────────────────────────────────────────────────────────────────
const DesignTab = {
    props: ['t'],
    setup(props) {
        const api    = useApi();
        const design = reactive({ font_heading:'Playfair Display', font_body:'Inter', preset:'classic', color_primary:'#b99954', color_secondary:'#efb954', color_bg:'#ffffff', color_headline:'#1a1a1a', color_body:'#444444' });
        const saving = ref(false);
        const saveOk = ref(false);

        const PRESETS = DESIGN_PRESETS;
        const FONTS = [
            // Serif / Display
            'Playfair Display','Cormorant Garamond','Lora','Merriweather',
            'EB Garamond','Libre Baskerville','Crimson Pro',
            // Sans-Serif
            'Inter','Montserrat','Raleway','Josefin Sans','Nunito',
            'Open Sans','Lato','Poppins','Work Sans','DM Sans',
            'Outfit','Plus Jakarta Sans','Mulish',
            // Script
            'Great Vibes','Tangerine','Pinyon Script',
            'La Belle Aurore','Over the Rainbow',
        ];
        const presetOpts = computed(() => [
            ...Object.keys(PRESETS).map(k => ({ value: k, label: k.charAt(0).toUpperCase() + k.slice(1).replace('_',' ') })),
            { value: 'custom', label: props.t('design_custom') },
        ]);
        const colorFields = computed(() => [
            ['color_primary',   props.t('design_color_primary')],
            ['color_secondary', props.t('design_color_secondary')],
            ['color_bg',        props.t('design_color_bg')],
            ['color_headline',  props.t('design_color_headline')],
            ['color_body',      props.t('design_color_body')],
        ]);
        const isCustom = computed(() => design.preset === 'custom');

        onMounted(async () => {
            try { const d = await api.get('/design'); if(d) { Object.assign(design, d); _activeDesign.preset = design.preset||'classic'; } } catch(e) {}
            Object.assign(_origDesign, JSON.parse(JSON.stringify(design)));
            _dDraft.register(async () => { await save(); }, () => { Object.assign(design, JSON.parse(JSON.stringify(_origDesign))); _activeDesign.preset = design.preset||'classic'; broadcast(); });
            // Wait for Vue to flush the watcher triggered by Object.assign above,
            // so it fires while _dReady is still false and does not mark dirty.
            await nextTick();
            _dReady = true;
        });
        onUnmounted(() => _dDraft.unregister());

        const _savedCustomColors = {};
        function applyPreset(p) {
            // Leaving custom → save current custom colors
            if (design.preset === 'custom' && p !== 'custom') {
                Object.assign(_savedCustomColors, {
                    color_primary:   design.color_primary,
                    color_secondary: design.color_secondary,
                    color_bg:        design.color_bg,
                    color_headline:  design.color_headline,
                    color_body:      design.color_body,
                });
            }
            design.preset = p;
            _activeDesign.preset = p;
            if (p === 'custom') {
                // Restore saved custom colors if available
                if (Object.keys(_savedCustomColors).length) Object.assign(design, _savedCustomColors);
            } else if (PRESETS[p]) {
                Object.assign(design, PRESETS[p]);
            }
            broadcast();
        }
        function broadcast() {
            const frame = document.getElementById('hw-preview-frame');
            if (frame) frame.contentWindow.postMessage({ type: 'hw-design-update', design: { ...design } }, '*');
        }
        const _dDraft = useEditorDraft();
        const _origDesign = {};
        let _dReady = false;
        watch(design, () => { broadcast(); if (_dReady) _dDraft.markDirty(); }, { deep: true });

        async function save() {
            saving.value = true;
            try { await api.put('/design', { ...design }); saveOk.value = true; setTimeout(() => saveOk.value = false, 2000); }
            finally { saving.value = false; }
        }

        return { design, saving, saveOk, FONTS, presetOpts, colorFields, isCustom, applyPreset, save };
    },
    template: `
    <div class="hw-tab-content">
      <h2 class="hw-tabtitle">{{ t('nav_design') }}</h2>

      <div class="hw-field-group">
        <div class="hw-field-group__header hw-field-group__header--static"><span>{{ t('design_fonts') }}</span></div>
        <div class="hw-field-group__body">
          <div class="hw-field"><label class="hw-field__label">Überschrift</label>
            <select class="select hw-field__input" v-model="design.font_heading"><option v-for="f in FONTS" :key="f" :value="f">{{ f }}</option></select>
          </div>
          <div class="hw-field"><label class="hw-field__label">Fließtext</label>
            <select class="select hw-field__input" v-model="design.font_body"><option v-for="f in FONTS" :key="f" :value="f">{{ f }}</option></select>
          </div>
        </div>
      </div>

      <div class="hw-field-group">
        <div class="hw-field-group__header hw-field-group__header--static"><span>{{ t('design_colors') }}</span></div>
        <div class="hw-field-group__body">
          <div class="hw-field"><label class="hw-field__label">Vorgabe</label>
            <select class="select hw-field__input" :value="design.preset" @change="applyPreset($event.target.value)">
              <option v-for="p in presetOpts" :key="p.value" :value="p.value">{{ p.label }}</option>
            </select>
          </div>
          <div class="hw-field" v-for="([key,label]) in colorFields" :key="key">
            <label class="hw-field__label">{{ label }}</label>
            <div class="hw-color-field">
              <input type="color" class="hw-color-swatch" v-model="design[key]" :disabled="!isCustom">
              <div class="hw-color-text-wrap" :class="{'hw-color-text-wrap--locked': !isCustom}">
                <input type="text" class="select hw-field__input hw-color-text" v-model="design[key]" maxlength="7" placeholder="#000000" :readonly="!isCustom">
                <i v-if="!isCustom" class="hgi-stroke hgi-square-lock-02 hw-color-text-lock"></i>
              </div>
            </div>
          </div>
        </div>
      </div>

    </div>
    `,
};

// ── DashboardState (shared between panel label + main cards) ──────────────────
// We use a module-level reactive object so panel and main can share the same data.
const _dashState = reactive({
    settings:     { password_protection: { enabled: false } },
    impressum:    {},
    nav:          { enabled: false, labels: {} },
    saving:       false,
    saved:        false,
    newPw:        '',
    ppNewPw:      '',
    loaded:       false,
    sectionCount: null,
    photoCount:   null,
    sections:     [],
    langs:        { active: [], default: 'de' },
});

async function _loadDash() {
    if (_dashState.loaded) return;
    const api = useApi();
    const [settings, impressum, sections, gallery, langs, nav] = await Promise.all([
        api.get('/settings'),
        api.get('/impressum'),
        api.get('/sections'),
        api.get('/gallery'),
        api.get('/languages'),
        api.get('/navigation'),
    ]);
    Object.assign(_dashState.settings,  settings);
    Object.assign(_dashState.impressum, impressum);
    Object.assign(_dashState.nav, nav);
    _dashState.sections     = sections;
    _dashState.sectionCount = sections.filter(s => s.visible).length;
    _dashState.photoCount   = (gallery.images || []).length;
    _dashState.langs = {
        active:  (langs.available || []).filter(l => l.active),
        default: langs.default || 'de',
    };
    _dashState.loaded = true;
}

async function _saveDash() {
    const api = useApi();
    _dashState.saving = true;
    const body = {
        couple_name:          _dashState.settings.couple_name,
        wedding_date:         _dashState.settings.wedding_date,
        wedding_time:         _dashState.settings.wedding_time,
        email:                _dashState.settings.email,
        reply_to:             _dashState.settings.reply_to,
        surprise_mode_gifts:  !!_dashState.settings.surprise_mode_gifts,
    };
    if (_dashState.newPw) body.new_password = _dashState.newPw;
    const pp = { enabled: _dashState.settings.password_protection?.enabled ?? false };
    if (_dashState.ppNewPw) pp.password = _dashState.ppNewPw;
    body.password_protection = pp;
    await api.put('/settings', body);
    await api.put('/impressum', { ..._dashState.impressum });
    _dashState.saving = false;
    _dashState.saved  = true;
    _dashState.newPw  = '';
    _dashState.ppNewPw = '';
    setTimeout(() => _dashState.saved = false, 2000);
}

const DashboardTab = {
    props: ['t'],
    setup() {
        const draft  = useEditorDraft();
        const _orig  = { settings: {}, impressum: {} };
        let _ready   = false;
        async function loadAndRegister() {
            await _loadDash();
            _orig.settings  = JSON.parse(JSON.stringify(_dashState.settings));
            _orig.impressum = JSON.parse(JSON.stringify(_dashState.impressum));
            draft.register(
                async () => { await _saveDash(); },
                () => { Object.assign(_dashState.settings, JSON.parse(JSON.stringify(_orig.settings))); Object.assign(_dashState.impressum, JSON.parse(JSON.stringify(_orig.impressum))); _dashState.newPw = ''; _dashState.ppNewPw = ''; }
            );
            _ready = true;
        }
        watch(() => JSON.stringify({s:_dashState.settings,i:_dashState.impressum,p:_dashState.newPw,pp:_dashState.ppNewPw,n:_dashState.nav}), (n,o) => { if(_ready && o!==undefined && n!==o) draft.markDirty(); });
        watch(() => JSON.parse(JSON.stringify(_dashState.nav)), (nav) => {
            const frame = document.getElementById('hw-preview-frame');
            if (frame) frame.contentWindow.postMessage({ type: 'hw-nav-update', nav }, '*');
        }, { deep: true });
        onMounted(loadAndRegister);
        onUnmounted(() => draft.unregister());
        const siteUrl = computed(() => window.HW_APP_BASE || window.location.origin);
        return { siteUrl };
    },
    template: `
    <div class="hw-tab-content">
      <h2 class="hw-tabtitle">Dashboard</h2>
      <p class="hw-tabdescription">{{ t('dash_description') }}</p>
      <a :href="siteUrl" target="_blank" rel="noopener" class="button is-small outline" style="margin-top:var(--space-xs)">{{ t('dash_open_site') }} <i class="hgi-stroke hgi-arrow-up-right-01"></i></a>
      <a href="https://hochzeitswebseite.de/" target="_blank" rel="noopener" class="margin-top-s"><img src="/assets/img/howe-logo@400px.png" class="dashboard_panel_logo" width="180" alt="Hochzeitswebseite.de"></a>
    </div>
    `,
};

// Main content: all the cards
// ── Kuchenliste Dashboard Card ────────────────────────────────────────────────
const KuchenlisteCard = {
    props: ['t'],
    setup(props) {
        const api     = useApi();
        const entries = ref([]);
        const loading = ref(false);
        const editing = reactive({});

        async function load() {
            loading.value = true;
            try { entries.value = await api.get('/kuchenliste/list'); }
            catch(e) { /* non-fatal */ }
            finally { loading.value = false; }
        }

        async function saveEdit(id) {
            const e = editing[id];
            if (!e) return;
            try {
                const updated = await api.put(`/kuchenliste/item/${id}`, { name: e.name, kuchen: e.kuchen, anmerkung: e.anmerkung });
                const idx = entries.value.findIndex(x => x.id === id);
                if (idx >= 0) entries.value[idx] = updated;
                delete editing[id];
            } catch(err) { alert(err.message); }
        }

        async function del(id) {
            if (!confirm(props.t('confirm_delete'))) return;
            try {
                await api.del(`/kuchenliste/item/${id}`);
                entries.value = entries.value.filter(e => e.id !== id);
            } catch(err) { alert(err.message); }
        }

        function startEdit(entry) {
            editing[entry.id] = { name: entry.name, kuchen: entry.kuchen, anmerkung: entry.anmerkung || '' };
        }
        function cancelEdit(id) { delete editing[id]; }

        onMounted(load);
        return { entries, loading, editing, saveEdit, del, startEdit, cancelEdit };
    },
    template: `
    <div class="card hw-dash-card" data-card-id="kuchenliste">
      <div class="flex-row flex-between flex-center-v" style="margin-bottom:var(--space-xs)">
        <h3 class="h5 text-title" style="margin:0"><i class="hgi-stroke hgi-cheese-cake-01"></i> {{ t('section_cakes') }}</h3>
        <span class="hw-dash-card__handle" style="cursor:grab;color:var(--dark-40);line-height:1"><i class="hgi-stroke hgi-drag-drop-vertical"></i></span>
      </div>
      <p v-if="loading" class="text-s" style="color:var(--dark-50)">{{ t('dash_loading') }}</p>
      <p v-else-if="!entries.length" class="text-s" style="color:var(--dark-50)">{{ t('cakes_empty') }}</p>
      <div v-else class="hw-kuchen-admin-list">
        <div v-for="e in entries" :key="e.id" class="hw-kuchen-admin-item">
          <template v-if="editing[e.id]">
            <input v-model="editing[e.id].name"       class="select is-small" :placeholder="t('field_name')">
            <input v-model="editing[e.id].kuchen"     class="select is-small" :placeholder="t('cakes_cake')">
            <input v-model="editing[e.id].anmerkung"  class="select is-small" :placeholder="t('field_note')">
            <div class="flex-row gap-xs">
              <button class="primary is-small" @click="saveEdit(e.id)">{{ t('dash_save') }}</button>
              <button class="outline is-small"  @click="cancelEdit(e.id)">{{ t('cancel') }}</button>
            </div>
          </template>
          <template v-else>
            <span class="text-s kl_name"><strong>{{ e.name }}</strong>: {{ e.kuchen }}</span>
            <span v-if="e.anmerkung" class="text-xs kl_note" style="color:var(--dark-50)">{{ e.anmerkung }}</span>
            <div class="flex-row gap-xs kl_actions">
              <button class="primitive is-small" @click="startEdit(e)"><i class="hgi-stroke hgi-pencil-edit-01"></i></button>
              <button class="primitive is-small" style="color:var(--error)" @click="del(e.id)"><i class="hgi-stroke hgi-delete-02"></i></button>
            </div>
          </template>
        </div>
      </div>
    </div>`,
};

// ── Geschenketisch Dashboard Card ─────────────────────────────────────────────
const GeschenketischCard = {
    props: ['t', 'sections', 'settings'],
    setup(props) {
        const api     = useApi();
        const editing = reactive({});

        const giftSections = computed(() =>
            (props.sections || []).filter(s => s.type === 'geschenketisch')
        );

        function editKey(secId, i) { return `${secId}_${i}`; }

        function startEdit(sec, i, item) {
            editing[editKey(sec.id, i)] = {
                status:     item.status     || 'available',
                giver_name: item.giver_name || '',
            };
        }
        function cancelEdit(secId, i) { delete editing[editKey(secId, i)]; }

        async function saveEdit(sec, i) {
            const key = editKey(sec.id, i);
            const e   = editing[key];
            if (!e) return;
            try {
                const updated = await api.put(`/geschenketisch/item/${sec.id}`, { item_index: i, ...e });
                sec.data.items[i].status     = updated.status     ?? 'available';
                sec.data.items[i].giver_name = updated.giver_name ?? '';
                if (updated.status === 'available') delete sec.data.items[i].giver_name;
                delete editing[key];
            } catch(err) { alert(err.message); }
        }

        return { giftSections, editing, startEdit, cancelEdit, saveEdit, editKey };
    },
    template: `
    <div class="card hw-dash-card" data-card-id="geschenketisch">
      <div class="flex-row flex-between flex-center-v" style="margin-bottom:var(--space-xs)">
        <h3 class="h5 text-title" style="margin:0"><i class="hgi-stroke hgi-gift"></i> {{ t('section_gifts') }}</h3>
        <span class="hw-dash-card__handle" style="cursor:grab;color:var(--dark-40);line-height:1"><i class="hgi-stroke hgi-drag-drop-vertical"></i></span>
      </div>
      <div class="flex-row flex-between flex-center-v" style="margin-bottom:var(--space-s)">
        <span class="text-s">{{ t('field_surprise_mode') }}<span class="hw-info-tip" style="margin-left:0.3em"><i class="hgi-stroke hgi-information-circle"></i><span class="hw-info-tip__bubble">{{ t('field_surprise_mode_tip') }}</span></span></span>
        <label class="hw-toggle hw-toggle--small">
          <input type="checkbox" class="hw-toggle__input" :checked="!!settings?.surprise_mode_gifts" @change="settings.surprise_mode_gifts = $event.target.checked">
          <span class="hw-toggle__track"><span class="hw-toggle__thumb"></span></span>
        </label>
      </div>
      <p v-if="!giftSections.length" class="text-s" style="color:var(--dark-50)">{{ t('gifts_no_section') }}</p>
      <template v-for="sec in giftSections" :key="sec.id">
        <p v-if="giftSections.length > 1" class="text-xs" style="color:var(--dark-50);margin-bottom:var(--space-2xs)">{{ sec.data?.de?.headline || sec.id }}</p>
        <div class="hw-gift-admin-list">
          <template v-for="(item,i) in (sec.data?.items||[])" :key="i">
            <div class="hw-kuchen-admin-item">
              <template v-if="editing[editKey(sec.id,i)]">
                <select v-model="editing[editKey(sec.id,i)].status" class="select is-small">
                  <option value="available">{{ t('opt_available') }}</option>
                  <option value="reserved">{{ t('opt_reserved') }}</option>
                </select>
                <input v-if="!settings?.surprise_mode_gifts"
                  v-model="editing[editKey(sec.id,i)].giver_name"
                  class="select is-small"
                  :placeholder="t('field_giver_name')"
                >
                <div class="flex-row gap-xs">
                  <button class="primary is-small" @click="saveEdit(sec,i)">{{ t('dash_save') }}</button>
                  <button class="outline is-small" @click="cancelEdit(sec.id,i)">{{ t('cancel') }}</button>
                </div>
              </template>
              <template v-else>
                <span class="text-s kl_name">
                  <strong>{{ item.de?.title || item.title || '—' }}</strong>
                  <template v-if="!settings?.surprise_mode_gifts && item.giver_name">
                    &nbsp;<span style="color:var(--dark-50);font-weight:400">{{ t('gifts_reserved_by') }} {{ item.giver_name }}</span>
                  </template>
                </span>
                <span v-if="item.status==='reserved'" class="text-xs" style="color:var(--success,#2d7a2d)">
                  <i class="hgi-stroke hgi-checkmark-circle-01"></i> {{ t('opt_reserved') }}
                </span>
                <span v-else class="text-xs" style="color:var(--dark-50)">{{ t('opt_available') }}</span>
                <div class="flex-row gap-xs kl_actions">
                  <button class="primitive is-small" @click="startEdit(sec,i,item)"><i class="hgi-stroke hgi-pencil-edit-01"></i></button>
                </div>
              </template>
            </div>
          </template>
          <p v-if="!(sec.data?.items||[]).length" class="text-xs" style="color:var(--dark-50)">{{ t('gifts_empty') }}</p>
        </div>
      </template>
    </div>`,
};

const DashboardMain = {
    props: ['t', 'uiLang'],
    emits: ['update:uiLang'],
    components: { KuchenlisteCard, GeschenketischCard },
    setup(props) {
        const d = _dashState;
        const draft = useEditorDraft();
        const publicUrl = computed(() => window.location.origin);
        function daysLeft() {
            if (!d.settings.expires_at) return null;
            return Math.max(0, Math.ceil((new Date(d.settings.expires_at) - new Date()) / 86400000));
        }

        // ── Update state ──────────────────────────────────────────────────────
        const api          = useApi();
        const checkState   = reactive({ loading: false, error: null, data: null });
        const updateState  = reactive({ loading: false, error: null, phase: 'idle', percent: 0, message: '', done: false });
        const rollbackState = reactive({ loading: false, error: null, done: false });
        let pollInterval   = null;

        async function checkForUpdate() {
            checkState.loading = true; checkState.error = null;
            try { checkState.data = await api.get('/update/check'); }
            catch(e) { checkState.error = e.message; }
            finally { checkState.loading = false; }
        }

        async function startUpdate() {
            updateState.loading = true; updateState.error = null;
            updateState.phase = 'starting'; updateState.percent = 0;
            updateState.message = ''; updateState.done = false;
            try {
                await api.post('/update/start', {});
                pollInterval = setInterval(pollStatus, 1500);
            } catch(e) {
                updateState.error = e.message;
                updateState.loading = false;
            }
        }

        async function pollStatus() {
            try {
                const p = await api.get('/update/status');
                updateState.phase   = p.phase   ?? 'idle';
                updateState.percent = p.percent ?? 0;
                updateState.message = p.message ?? '';
                if (p.error) {
                    updateState.error = p.error;
                    updateState.loading = false;
                    clearInterval(pollInterval); pollInterval = null;
                } else if (p.phase === 'done') {
                    updateState.done    = true;
                    updateState.loading = false;
                    clearInterval(pollInterval); pollInterval = null;
                    checkState.data = null;
                }
            } catch(e) { /* keep polling */ }
        }

        async function doRollback() {
            if (!confirm('Wirklich zur vorherigen Version zurückrollen?')) return;
            rollbackState.loading = true; rollbackState.error = null; rollbackState.done = false;
            try {
                await api.post('/update/rollback', {});
                rollbackState.done = true;
                checkState.data = null;
            } catch(e) { rollbackState.error = e.message; }
            finally { rollbackState.loading = false; }
        }

        onMounted(() => { _loadDash(); checkForUpdate(); });
        onUnmounted(() => { if (pollInterval) clearInterval(pollInterval); });
        const thumbGen = reactive({ loading: false, done: false, count: 0, error: '' });
        async function generateThumbs() {
            thumbGen.loading = true; thumbGen.done = false; thumbGen.error = '';
            try {
                const res = await useApi().post('/media/generate-thumbs', {});
                thumbGen.count = res.count || 0; thumbGen.done = true;
                setTimeout(() => { thumbGen.done = false; }, 3000);
            } catch(e) { thumbGen.error = e.message; }
            finally { thumbGen.loading = false; }
        }
        const mailheaderGen = reactive({ loading: false, done: false, count: 0, error: '' });
        async function generateMailheaderThumbs() {
            mailheaderGen.loading = true; mailheaderGen.done = false; mailheaderGen.error = '';
            try {
                const res = await useApi().post('/media/generate-mailheader-thumbs', {});
                mailheaderGen.count = res.count || 0; mailheaderGen.done = true;
                setTimeout(() => { mailheaderGen.done = false; }, 3000);
            } catch(e) { mailheaderGen.error = e.message; }
            finally { mailheaderGen.loading = false; }
        }
        const showNewPw = ref(false);
        const showPpPw  = ref(false);
        function fmtDate(iso) {
            if (!iso) return '—';
            const p = iso.split('-');
            return `${parseInt(p[2])}.${parseInt(p[1])}.${p[0]}`;
        }
        const statsRefreshing = ref(false);
        async function refreshStats() {
            statsRefreshing.value = true;
            _dashState.sectionCount = null;
            _dashState.photoCount   = null;
            const api = useApi();
            const [sections, gallery] = await Promise.all([api.get('/sections'), api.get('/gallery')]);
            _dashState.sectionCount = sections.filter(s => s.visible).length;
            _dashState.photoCount   = (gallery.images || []).length;
            statsRefreshing.value = false;
        }
        // ── Dashboard card ordering ───────────────────────────────────────────
        const cardsContainer = ref(null);
        onMounted(() => {
            // Restore saved card order
            const stored = JSON.parse(localStorage.getItem('hw_dash_card_order') || 'null');
            if (stored && cardsContainer.value) {
                stored.forEach(id => {
                    const el = cardsContainer.value.querySelector(`[data-card-id="${id}"]`);
                    if (el) cardsContainer.value.appendChild(el);
                });
            }
            // Init Sortable for card reordering
            if (typeof Sortable !== 'undefined' && cardsContainer.value) {
                Sortable.create(cardsContainer.value, {
                    handle: '.hw-dash-card__handle',
                    animation: 150,
                    onEnd: () => {
                        const order = [...cardsContainer.value.querySelectorAll('[data-card-id]')]
                            .map(el => el.dataset.cardId);
                        localStorage.setItem('hw_dash_card_order', JSON.stringify(order));
                    },
                });
            }
        });

        const isAdmin = window.HW_ROLE === 'admin';
        return { d, publicUrl, daysLeft, save: _saveDash, thumbGen, generateThumbs, mailheaderGen, generateMailheaderThumbs, showNewPw, showPpPw, fmtDate, statsRefreshing, refreshStats, checkState, updateState, rollbackState, checkForUpdate, startUpdate, doRollback, cardsContainer, isAdmin };
    },
    template: `
    <div class="hw-dash-main">
      <div class="grid-min-40 grid-gap" ref="cardsContainer">

        <div class="card hw-dash-card" data-card-id="overview">
          <div class="flex-row flex-between flex-center-v">
            <h3 class="h5 text-title" style="margin:0">{{ t('dash_overview') }}</h3>
            <div class="flex-row gap-xs flex-center-v">
              <button class="primitive is-small" type="button" @click="refreshStats" :disabled="statsRefreshing" :class="{ 'hw-spin': statsRefreshing }"><i class="hgi-stroke hgi-refresh-03"></i></button>
              <span class="hw-dash-card__handle" style="cursor:grab;color:var(--dark-40);line-height:1"><i class="hgi-stroke hgi-drag-drop-vertical"></i></span>
            </div>
          </div>
          <div class="grid-3 gap-s">
            <div class="hw-stat"><span class="hw-stat__value">{{ d.sectionCount ?? '—' }}</span><span class="hw-stat__label">{{ t('dash_sections') }}</span></div>
            <div class="hw-stat"><span class="hw-stat__value">{{ d.photoCount ?? '—' }}</span><span class="hw-stat__label">{{ t('dash_photos') }}</span></div>
            <div class="hw-stat"><span class="hw-stat__value">{{ daysLeft()??'—' }}</span><span class="hw-stat__label">{{ t('dash_days_left') }}</span></div>
          </div>
          <div v-if="d.settings.expires_at" class="text-s" style="color:var(--dark-60)">{{ t('dash_expires') }}: {{ fmtDate(d.settings.expires_at) }}</div>
          <div class="hw-panel-divider" style="margin:var(--space-s) 0"></div>
          <div class="flex-row flex-between flex-center-v">
            <span class="text-s text-title">{{ t('dash_pw_protection') }}</span>
            <label class="hw-toggle hw-toggle--small">
              <input type="checkbox" class="hw-toggle__input" :checked="!!d.settings.password_protection?.enabled" @change="d.settings.password_protection.enabled = $event.target.checked">
              <span class="hw-toggle__track"><span class="hw-toggle__thumb"></span></span>
            </label>
          </div>
          <div v-if="d.settings.password_protection?.enabled" class="hw-field" style="margin-top:var(--space-xs)">
            <div class="flex-row flex-between flex-center-v" style="margin-bottom:var(--space-2xs)">
              <label class="hw-field__label" style="margin:0">{{ t('dash_pp_new_pw') }}</label>
              <span v-if="d.settings.password_protection?.has_password && !showPpPw" class="text-xs" style="color:var(--success,#2d7a2d)"><i class="hgi-stroke hgi-checkmark-circle-01"></i> {{ t('dash_pp_is_set') }}</span>
            </div>
            <div v-if="!showPpPw">
              <button class="outline is-small" type="button" @click="showPpPw = true"><i class="hgi-stroke hgi-lock-01"></i> {{ t('dash_pp_new_pw') }}</button>
            </div>
            <div v-else class="flex-row gap-xs">
              <input v-model="d.ppNewPw" type="text" class="select hw-field__input width-100" autocomplete="new-password" autofocus>
              <button class="primitive is-small" type="button" @click="showPpPw = false; d.ppNewPw = ''" :title="t('cancel')"><i class="hgi-stroke hgi-cancel-01"></i></button>
            </div>
          </div>
        </div>

        <div class="card hw-dash-card" data-card-id="account">
          <div class="flex-row flex-between flex-center-v" style="margin-bottom:var(--space-xs)">
            <h3 class="h5 text-title" style="margin:0">{{ t('dash_account') }}</h3>
            <span class="hw-dash-card__handle" style="cursor:grab;color:var(--dark-40);line-height:1"><i class="hgi-stroke hgi-drag-drop-vertical"></i></span>
          </div>
          <div class="hw-field"><label class="hw-field__label">{{ t('dash_couple_name') }}</label><input v-model="d.settings.couple_name" type="text" class="select hw-field__input"></div>
          <div class="hw-field"><label class="hw-field__label">{{ t('dash_email') }}</label><input v-model="d.settings.email" type="email" class="select hw-field__input"></div>
          <div class="hw-field"><label class="hw-field__label">{{ t('dash_wedding_date') }}</label><input v-model="d.settings.wedding_date" type="date" class="select hw-field__input"></div>
          <div class="hw-field"><label class="hw-field__label">{{ t('dash_wedding_time') }}</label><input v-model="d.settings.wedding_time" type="time" class="select hw-field__input"></div>
          <div class="hw-field">
            <label class="hw-field__label">{{ t('dash_change_pw') }}</label>
            <div v-if="!showNewPw">
              <button class="outline is-small" type="button" @click="showNewPw = true"><i class="hgi-stroke hgi-lock-01"></i> {{ t('dash_set_pw') }}</button>
            </div>
            <div v-else class="flex-row gap-xs">
              <input v-model="d.newPw" type="password" class="select hw-field__input width-100" autocomplete="new-password" autofocus>
              <button class="primitive is-small" type="button" @click="showNewPw = false; d.newPw = ''" :title="t('cancel')"><i class="hgi-stroke hgi-cancel-01"></i></button>
            </div>
          </div>
        </div>

        <div class="card hw-dash-card" data-card-id="impressum">
          <div class="flex-row flex-between flex-center-v" style="margin-bottom:var(--space-xs)">
            <h3 class="h5 text-title" style="margin:0">{{ t('dash_impressum') }}</h3>
            <span class="hw-dash-card__handle" style="cursor:grab;color:var(--dark-40);line-height:1"><i class="hgi-stroke hgi-drag-drop-vertical"></i></span>
          </div>
          <div class="hw-field"><label class="hw-field__label">{{ t('dash_imp_name') }}</label><input v-model="d.impressum.name" type="text" class="select hw-field__input"></div>
          <div class="hw-field"><label class="hw-field__label">{{ t('dash_imp_street') }}</label><input v-model="d.impressum.street" type="text" class="select hw-field__input"></div>
          <div class="hw-field" style="display:grid;grid-template-columns:8rem 1fr;gap:var(--space-xs);align-items:end">
            <div><label class="hw-field__label">{{ t('dash_imp_zip') }}</label><input v-model="d.impressum.zip" type="text" class="select hw-field__input"></div>
            <div><label class="hw-field__label">{{ t('dash_imp_city') }}</label><input v-model="d.impressum.city" type="text" class="select hw-field__input"></div>
          </div>
          <div class="hw-field"><label class="hw-field__label">{{ t('dash_imp_country') }}</label><input v-model="d.impressum.country" type="text" class="select hw-field__input"></div>
          <div class="hw-field"><label class="hw-field__label">{{ t('dash_imp_email') }}</label><input v-model="d.impressum.email" type="email" class="select hw-field__input"></div>
          <div class="hw-field"><label class="hw-field__label">{{ t('dash_imp_phone') }}</label><input v-model="d.impressum.phone" type="tel" class="select hw-field__input"></div>
          <div class="hw-field"><label class="hw-field__label">{{ t('dash_imp_additional') }}</label>
            <textarea v-model="d.impressum.additional" class="select hw-field__input" rows="3"></textarea>
          </div>
        </div>

        <div class="card hw-dash-card" data-card-id="share">
          <div class="flex-row flex-between flex-center-v" style="margin-bottom:var(--space-xs)">
            <h3 class="h5 text-title" style="margin:0">{{ t('dash_share') }}</h3>
            <span class="hw-dash-card__handle" style="cursor:grab;color:var(--dark-40);line-height:1"><i class="hgi-stroke hgi-drag-drop-vertical"></i></span>
          </div>
          <div class="hw-field">
            <label class="hw-field__label">{{ t('dash_public_url') }}</label>
            <div class="flex-row gap-xs">
              <input :value="publicUrl" readonly class="select hw-field__input width-100">
              <button class="outline is-small" @click="navigator.clipboard.writeText(publicUrl)">{{ t('dash_copy') }}</button>
              <button class="secondary is-small" @click="window.open('https://wa.me/?text='+encodeURIComponent(publicUrl), '_blank')">{{ t('dash_whatsapp') }}</button>
            </div>
          </div>
        </div>

        <div class="card hw-dash-card" data-card-id="ui_lang">
          <div class="flex-row flex-between flex-center-v" style="margin-bottom:var(--space-xs)">
            <h3 class="h5 text-title" style="margin:0">{{ t('dash_ui_language') }}</h3>
            <span class="hw-dash-card__handle" style="cursor:grab;color:var(--dark-40);line-height:1"><i class="hgi-stroke hgi-drag-drop-vertical"></i></span>
          </div>
          <p class="text-s" style="color:var(--dark-50);margin:0">{{ t('dash_ui_language_desc') }}</p>
          <div class="hw-lang-selector">
            <button class="hw-lang-selector__btn" :class="{'hw-lang-selector__btn--active': uiLang==='de'}" @click="$emit('update:uiLang','de')">
              <span class="hw-lang-selector__code">DE</span>
              <span class="hw-lang-selector__label">Deutsch</span>
            </button>
            <button class="hw-lang-selector__btn" :class="{'hw-lang-selector__btn--active': uiLang==='en'}" @click="$emit('update:uiLang','en')">
              <span class="hw-lang-selector__code">EN</span>
              <span class="hw-lang-selector__label">English</span>
            </button>
          </div>
        </div>

        <div v-if="isAdmin" class="card hw-dash-card" data-card-id="thumbgen">
          <div class="flex-row flex-between flex-center-v" style="margin-bottom:var(--space-xs)">
            <h3 class="h5 text-title" style="margin:0">🖼 Vorschaubilder</h3>
            <span class="hw-dash-card__handle" style="cursor:grab;color:var(--dark-40);line-height:1"><i class="hgi-stroke hgi-drag-drop-vertical"></i></span>
          </div>
          <p class="text-s" style="color:var(--dark-50);margin:0">Thumbs-Ordner leeren und alle Bilder frisch generieren. Neue Bilder ohne Size-Suffix durchlaufen die komplette Pipeline.</p>
          <div style="margin-top:var(--space-xs);display:flex;align-items:center;gap:var(--space-s)">
            <button class="outline is-small" @click="generateThumbs" :disabled="thumbGen.loading || mailheaderGen.loading">
              <i class="hgi-stroke hgi-image-01"></i>
              {{ thumbGen.loading ? 'Generiere…' : 'Thumbnails regenerieren' }}
            </button>
            <span v-if="thumbGen.done" style="color:var(--success);font-size:var(--text-xs)">✓ {{ thumbGen.count }} erstellt</span>
            <span v-if="thumbGen.error" style="color:var(--error);font-size:var(--text-xs)">{{ thumbGen.error }}</span>
          </div>
          <div style="margin-top:var(--space-xs);display:flex;align-items:center;gap:var(--space-s)">
            <button class="outline is-small" @click="generateMailheaderThumbs" :disabled="thumbGen.loading || mailheaderGen.loading">
              <i class="hgi-stroke hgi-mail-at-sign-01"></i>
              {{ mailheaderGen.loading ? 'Generiere…' : 'Mailheader-Thumbnails regenerieren' }}
            </button>
            <span v-if="mailheaderGen.done" style="color:var(--success);font-size:var(--text-xs)">✓ {{ mailheaderGen.count }} erstellt</span>
            <span v-if="mailheaderGen.error" style="color:var(--error);font-size:var(--text-xs)">{{ mailheaderGen.error }}</span>
          </div>
        </div>

        <!-- ── Kuchenliste card ────────────────────────────────────────────── -->
        <kuchenliste-card :t="t" />

        <!-- ── Geschenketisch card ─────────────────────────────────────────── -->
        <geschenketisch-card :t="t" :sections="d.sections" :settings="d.settings" />

        <!-- ── Update card ──────────────────────────────────────────────────── -->
        <div class="card hw-dash-card" data-card-id="update">
          <div class="flex-row flex-between flex-center-v" style="margin-bottom:var(--space-xs)">
            <h3 class="h5 text-title" style="margin:0">{{ t('update_version_title') }}</h3>
            <span class="hw-dash-card__handle" style="cursor:grab;color:var(--dark-40);line-height:1"><i class="hgi-stroke hgi-drag-drop-vertical"></i></span>
          </div>

          <p v-if="checkState.loading" class="text-s" style="color:var(--dark-50)">{{ t('update_checking') }}</p>
          <p v-else-if="checkState.error" style="color:var(--error);font-size:var(--text-s)">{{ checkState.error }}</p>

          <template v-else-if="checkState.data">
            <div class="text-s" style="display:grid;grid-template-columns:auto 1fr;gap:.25rem .75rem;color:var(--dark-70)">
              <span style="color:var(--dark-50)">{{ t('update_current_version') }}</span>
              <strong>{{ checkState.data.current_version }}</strong>
              <template v-if="checkState.data.available_version">
                <span style="color:var(--dark-50)">{{ t('update_available_version') }}</span>
                <strong>{{ checkState.data.available_version }}</strong>
              </template>
              <template v-if="checkState.data.description">
                <span style="color:var(--dark-50)">{{ t('update_release_notes') }}</span>
                <span>{{ checkState.data.description }}</span>
              </template>
            </div>
            <p v-if="!checkState.data.update_available && !updateState.done"
              style="color:var(--success);font-size:var(--text-s);margin-top:.5rem">
              ✓ {{ t('update_up_to_date') }}
            </p>
            <button v-if="checkState.data.update_available && !updateState.loading && !updateState.done"
              class="primary is-small" style="margin-top:.5rem" @click="startUpdate">
              {{ t('update_install_btn') }} {{ checkState.data.available_version }}
            </button>
          </template>

          <template v-if="updateState.loading || updateState.done || updateState.error">
            <div style="margin-top:.75rem">
              <div v-if="!updateState.error" class="hw-update-bar">
                <div class="hw-update-bar__fill" :style="{ width: updateState.percent + '%' }"></div>
              </div>
              <p v-if="updateState.message" class="text-xs" style="color:var(--dark-60);margin:.25rem 0 0">{{ updateState.message }}</p>
              <p v-if="updateState.error" style="color:var(--error);font-size:var(--text-s);margin:.5rem 0 0">{{ updateState.error }}</p>
              <div v-if="updateState.done" style="margin-top:.5rem;display:flex;align-items:center;gap:.5rem;flex-wrap:wrap">
                <span style="color:var(--success);font-size:var(--text-s)">✓ {{ t('update_success') }}</span>
                <button class="primary is-small" @click="window.location.reload()">{{ t('update_reload_btn') }}</button>
              </div>
            </div>
          </template>

          <div style="display:flex;gap:.5rem;align-items:center;flex-wrap:wrap;margin-top:.75rem;padding-top:.75rem;border-top:1px solid var(--border-primary)">
            <button class="outline is-small" @click="checkForUpdate" :disabled="checkState.loading">
              <i class="hgi-stroke hgi-refresh-03"></i> {{ t('update_check_btn') }}
            </button>
            <template v-if="checkState.data?.rollback_available">
              <span class="text-xs" style="color:var(--dark-50);margin-left:auto">
                {{ t('update_previous_version') }}: {{ checkState.data.previous_version }}
              </span>
              <button class="outline is-small" @click="doRollback" :disabled="rollbackState.loading" style="color:var(--error);border-color:var(--error)">
                {{ rollbackState.loading ? t('update_rolling_back') : t('update_rollback_btn') }}
              </button>
            </template>
          </div>

          <p v-if="rollbackState.error" style="color:var(--error);font-size:var(--text-s);margin:.5rem 0 0">{{ rollbackState.error }}</p>
          <div v-if="rollbackState.done" style="color:var(--success);font-size:var(--text-s);margin:.5rem 0 0;display:flex;align-items:center;gap:.5rem">
            ✓ {{ t('update_rollback_done') }}
            <button class="outline is-small" @click="window.location.reload()">{{ t('update_reload_btn') }}</button>
          </div>
        </div>

      </div>
    </div>
    `,
};

// ── Gallery shared state ──────────────────────────────────────────────────────
const _gallState = reactive({
    images:        [],
    albums:        [],
    loaded:        false,
    activeAlbumId: null,
    uploading:     false,
    uploadDone:    0,
    uploadTotal:   0,
    uploadErrors:  0,
});
// Selection: plain reactive object {[id]: true} — reliable Vue 3 reactivity
const _gallSel       = reactive({});
const _gallDragAlbum = ref(null);   // album ID currently hovered during image drag
const _gallDragging  = ref(false);  // true while user drags images toward album list

function gallSelect(id)     { _gallSel[id] = true; }
function gallDeselect(id)   { delete _gallSel[id]; }
function gallIsSelected(id) { return !!_gallSel[id]; }
function gallSelIds()       { return Object.keys(_gallSel); }
function gallClearSel()     { for (const id in _gallSel) delete _gallSel[id]; }
function gallSelCount()     { return Object.keys(_gallSel).length; }

async function _loadGallery(force = false) {
    if (_gallState.loaded && !force) return;
    const api  = useApi();
    const data = await api.get('/gallery');
    _gallState.images = data.images || [];
    _gallState.albums = data.albums || [];
    _gallState.loaded = true;
}

// ── GalleryTab (panel) ────────────────────────────────────────────────────────
const GalleryTab = {
    props: ['t'],
    setup() {
        const api             = useApi();
        const newAlbumName    = ref('');
        const editId          = ref(null);
        const editName        = ref('');
        const deleteConfirmId = ref(null);
        const saving          = ref(false);
        const albumListEl     = ref(null);
        let   albumSortable   = null;

        onMounted(async () => {
            if (!_gallState.loaded) await _loadGallery();
            nextTick(initAlbumSort);
        });

        watch(() => _gallState.albums.length, () => nextTick(initAlbumSort));
        onUnmounted(() => { if (albumSortable) albumSortable.destroy(); });

        function initAlbumSort() {
            if (!albumListEl.value) return;
            if (albumSortable) { albumSortable.destroy(); albumSortable = null; }
            albumSortable = Sortable.create(albumListEl.value, {
                handle: '.hw-album-row__handle',
                animation: 150,
                onEnd: async () => {
                    const ids = [...albumListEl.value.querySelectorAll('[data-album-id]')].map(el => el.dataset.albumId);
                    await useApi().put('/gallery/albums/order', { ids });
                    const map = Object.fromEntries(_gallState.albums.map(a => [a.id, a]));
                    _gallState.albums = ids.map(id => map[id]).filter(Boolean);
                },
            });
        }

        // Single O(n) pass over images — replaces per-album filter calls in v-for
        const albumCounts = computed(() => {
            const c = { '__all__': _gallState.images.length, '__unassigned__': 0 };
            for (const img of _gallState.images) {
                if (!img.album_id) c['__unassigned__']++;
                else c[img.album_id] = (c[img.album_id] || 0) + 1;
            }
            return c;
        });

        async function createAlbum() {
            const name = newAlbumName.value.trim();
            if (!name) return;
            saving.value = true;
            try {
                const album = await api.post('/gallery/albums', { name });
                _gallState.albums.push(album);
                newAlbumName.value = '';
                nextTick(initAlbumSort);
            } finally { saving.value = false; }
        }

        function startEdit(album) { editId.value = album.id; editName.value = album.name; }

        async function saveEdit(album) {
            const name = editName.value.trim();
            editId.value = null;
            if (!name || name === album.name) return;
            const updated = await api.put('/gallery/albums/' + album.id, { name });
            const idx = _gallState.albums.findIndex(a => a.id === album.id);
            if (idx !== -1) _gallState.albums[idx] = updated;
        }

        async function deleteAlbum(id) {
            if (deleteConfirmId.value === id) {
                await api.del('/gallery/albums/' + id);
                _gallState.albums = _gallState.albums.filter(a => a.id !== id);
                if (_gallState.activeAlbumId === id) _gallState.activeAlbumId = null;
                deleteConfirmId.value = null;
            } else {
                deleteConfirmId.value = id;
                setTimeout(() => { if (deleteConfirmId.value === id) deleteConfirmId.value = null; }, 3000);
            }
        }

        // ── Drop targets (images dragged from main to album) ──────────────────
        function isGallDrag(e) { return e.dataTransfer.types.includes('application/gallery-ids'); }

        function onAlbumDragOver(e, dropKey) {
            if (!isGallDrag(e)) return;
            e.preventDefault();
            e.dataTransfer.dropEffect = 'move';
            _gallDragAlbum.value = dropKey;
        }

        function onAlbumDragLeave(e, dropKey) {
            if (!e.currentTarget.contains(e.relatedTarget) && _gallDragAlbum.value === dropKey) {
                _gallDragAlbum.value = null;
            }
        }

        async function onAlbumDrop(e, dropKey) {
            e.preventDefault();
            _gallDragAlbum.value = null;
            _gallDragging.value  = false;
            const raw = e.dataTransfer.getData('application/gallery-ids');
            if (!raw) return;
            const ids = JSON.parse(raw);
            if (!ids.length) return;
            // '__all__' and '__unassigned__' both unassign (album_id = null)
            const albumId = (dropKey === '__all__' || dropKey === '__unassigned__') ? null : dropKey;
            await useApi().put('/gallery/bulk', { ids, album_id: albumId });
            ids.forEach(id => {
                const img = _gallState.images.find(i => i.id === id);
                if (img) img.album_id = albumId;
            });
            gallClearSel();
        }

        return {
            gall: _gallState,
            gallDragAlbum: _gallDragAlbum,
            gallDragging:  _gallDragging,
            albumListEl,
            newAlbumName, editId, editName, deleteConfirmId, saving,
            albumCounts, createAlbum, startEdit, saveEdit, deleteAlbum,
            onAlbumDragOver, onAlbumDragLeave, onAlbumDrop,
        };
    },
    template: `
    <div class="hw-tab-content">
      <h2 class="hw-tabtitle">{{ t('nav_gallery') }}</h2>

      <p v-if="gallDragging" class="hw-album-drag-hint">{{ t('gallery_drag_hint') }}</p>

      <div class="hw-albums">
        <!-- Alle Bilder -->
        <button
          class="hw-album-filter-btn"
          :class="{
            'hw-album-filter-btn--active':     gall.activeAlbumId === null,
            'hw-album-filter-btn--droptarget': gallDragging,
            'hw-album-filter-btn--drop':       gallDragAlbum === '__all__',
          }"
          @click="gall.activeAlbumId = null"
          @dragover="onAlbumDragOver($event, '__all__')"
          @dragleave="onAlbumDragLeave($event, '__all__')"
          @drop="onAlbumDrop($event, '__all__')"
        >
          <i class="hgi-stroke hgi-image-01"></i>
          <span>{{ t('gallery_all') }}</span>
          <span class="hw-album-count">{{ albumCounts['__all__'] ?? 0 }}</span>
        </button>

        <!-- Ohne Album -->
        <button
          class="hw-album-filter-btn"
          :class="{
            'hw-album-filter-btn--active':     gall.activeAlbumId === '__unassigned__',
            'hw-album-filter-btn--droptarget': gallDragging,
            'hw-album-filter-btn--drop':       gallDragAlbum === '__unassigned__',
          }"
          @click="gall.activeAlbumId = '__unassigned__'"
          @dragover="onAlbumDragOver($event, '__unassigned__')"
          @dragleave="onAlbumDragLeave($event, '__unassigned__')"
          @drop="onAlbumDrop($event, '__unassigned__')"
        >
          <i class="hgi-stroke hgi-image-not-found-01"></i>
          <span>{{ t('gallery_no_album') }}</span>
          <span class="hw-album-count">{{ albumCounts['__unassigned__'] ?? 0 }}</span>
        </button>

        <!-- Sortable album list -->
        <div ref="albumListEl">
          <div v-for="album in gall.albums" :key="album.id" :data-album-id="album.id" class="hw-album-row">
            <template v-if="editId === album.id">
              <span class="hw-album-row__handle hw-album-row__handle--edit"></span>
              <input
                class="select hw-field__input hw-album-row__edit"
                v-model="editName"
                @keydown.enter="saveEdit(album)"
                @keydown.escape="editId = null"
                @blur="saveEdit(album)"
                autofocus
              >
            </template>
            <template v-else>
              <span class="hw-album-row__handle" title="Sortieren">
                <i class="hgi-stroke hgi-drag-drop-vertical"></i>
              </span>
              <button
                class="hw-album-filter-btn"
                :class="{
                  'hw-album-filter-btn--active':     gall.activeAlbumId === album.id,
                  'hw-album-filter-btn--droptarget': gallDragging,
                  'hw-album-filter-btn--drop':       gallDragAlbum === album.id,
                }"
                @click="gall.activeAlbumId = album.id"
                @dragover="onAlbumDragOver($event, album.id)"
                @dragleave="onAlbumDragLeave($event, album.id)"
                @drop="onAlbumDrop($event, album.id)"
              >
                <i class="hgi-stroke hgi-folder-01"></i>
                <span class="hw-album-filter-btn__name">{{ album.name }}</span>
                <span class="hw-album-count">{{ albumCounts[album.id] ?? 0 }}</span>
              </button>
              <div class="hw-album-row__actions">
                <button type="button" class="button primitive hw-album-action"
                  :title="t('gallery_rename_album')" @click.stop="startEdit(album)">
                  <i class="hgi-stroke hgi-pencil-edit-01"></i>
                </button>
                <button type="button" class="button primitive hw-album-action hw-album-action--delete"
                  :class="{'hw-album-action--confirm': deleteConfirmId === album.id}"
                  :title="deleteConfirmId === album.id ? t('confirm_delete') : t('delete')"
                  @click.stop="deleteAlbum(album.id)"
                ><i class="hgi-stroke hgi-delete-01"></i></button>
              </div>
            </template>
          </div>
        </div>

        <div class="hw-album-create">
          <input class="select hw-field__input input" v-model="newAlbumName"
            :placeholder="t('gallery_album_placeholder')"
            @keydown.enter="createAlbum">
          <button type="button" class="button primary is-small" :disabled="!newAlbumName.trim() || saving" @click="createAlbum">
            <i class="hgi-stroke hgi-add-01"></i>
          </button>
        </div>
      </div>
    </div>
    `,
};

// ── GalleryMain (main content) ────────────────────────────────────────────────
const GalleryMain = {
    props: ['t'],
    setup(props) {
        const gall            = _gallState;
        const sel             = _gallSel;
        const appBase         = window.HW_APP_BASE || window.location.origin;
        const gridEl          = ref(null);
        const fileInputRef    = ref(null);
        const uploadDragOver  = ref(false);
        const deleteConfirmId  = ref(null);
        const deleteSelConfirm = ref(false);
        const deleteErrMsg     = ref('');
        let lastClickIdx      = -1;

        // Lasso state (viewport-relative coordinates for position:fixed rect)
        const lasso = reactive({ active: false, x1: 0, y1: 0, x2: 0, y2: 0 });

        // ── Sort mode (pure HTML5 drag — no SortableJS, no lasso conflict) ─────
        const sortMode    = ref(false);
        const reorderDrag = reactive({ ids: [], targetId: null, position: 'after', indX: 0, indY: 0, indH: 0 });

        function enterSortMode() { sortMode.value = true; }
        function exitSortMode()  { sortMode.value = false; reorderDrag.targetId = null; reorderDrag.ids = []; }

        function onSortDragStart(e, img) {
            // Drag all selected images (in visual order), or just this one
            const ids = gallIsSelected(img.id) && gallSelCount() > 0
                ? filteredImages.value.filter(i => gallIsSelected(i.id)).map(i => i.id)
                : [img.id];
            reorderDrag.ids = ids;
            e.dataTransfer.setData('application/gallery-reorder', JSON.stringify(ids));
            e.dataTransfer.effectAllowed = 'move';
            // Badge
            const count = ids.length;
            const badge = document.createElement('div');
            badge.className = 'hw-drag-badge';
            badge.textContent = count === 1
                ? props.t('gallery_drag_one')
                : props.t('gallery_drag_many').replace('{n}', count);
            document.body.appendChild(badge);
            e.dataTransfer.setDragImage(badge, badge.offsetWidth / 2, 20);
            setTimeout(() => badge.parentNode?.removeChild(badge), 0);
        }

        function onSortDragOver(e, img) {
            if (!e.dataTransfer.types.includes('application/gallery-reorder')) return;
            if (reorderDrag.ids.includes(img.id)) return;
            e.preventDefault();
            e.dataTransfer.dropEffect = 'move';
            const rect    = e.currentTarget.getBoundingClientRect();
            const before  = e.clientX < rect.left + rect.width / 2;
            const pos     = before ? 'before' : 'after';
            // Only trigger reactivity when something actually changed
            if (reorderDrag.targetId !== img.id || reorderDrag.position !== pos) {
                reorderDrag.targetId = img.id;
                reorderDrag.position = pos;
                reorderDrag.indX = before ? rect.left - 2 : rect.right - 2;
                reorderDrag.indY = rect.top;
                reorderDrag.indH = rect.height;
            }
        }

        function onSortDragLeave(e) {
            if (!e.currentTarget.contains(e.relatedTarget)) reorderDrag.targetId = null;
        }

        function onSortDragEnd() { reorderDrag.targetId = null; reorderDrag.ids = []; }

        async function onSortDrop(e, img) {
            e.preventDefault();
            const ids      = [...reorderDrag.ids];
            const targetId = reorderDrag.targetId ?? img.id;
            const position = reorderDrag.position;
            reorderDrag.targetId = null; reorderDrag.ids = [];
            if (!ids.length || ids.includes(targetId)) return;

            const dragSet   = new Set(ids);
            const remaining = filteredImages.value.filter(i => !dragSet.has(i.id)).map(i => i.id);
            const targetIdx = remaining.indexOf(targetId);
            if (targetIdx === -1) return;
            const insertAt  = position === 'after' ? targetIdx + 1 : targetIdx;
            const newVisible = [...remaining.slice(0, insertAt), ...ids, ...remaining.slice(insertAt)];

            // Merge back into full image list, preserving positions of non-visible images
            const visibleSet = new Set(newVisible);
            let vi = 0;
            const fullOrder  = gall.images.map(i => visibleSet.has(i.id) ? newVisible[vi++] : i.id);
            const imgMap     = Object.fromEntries(gall.images.map(i => [i.id, i]));
            gall.images      = fullOrder.map(id => imgMap[id]);
            await useApi().put('/gallery/order', { ids: fullOrder });
            gallClearSel();
        }

        onMounted(async () => {
            if (!gall.loaded) await _loadGallery();
            else _loadGallery(true); // background refresh on re-entry
        });
        onUnmounted(() => {
            sortMode.value       = false;
            _gallDragging.value  = false;   // fix: stuck if tab switched mid-drag
            _gallDragAlbum.value = null;
            gallClearSel();                 // fix: stale selection across tab switches
            document.removeEventListener('mousemove', onLassoMove);
            document.removeEventListener('mouseup',   onLassoEnd);
            document.removeEventListener('keydown',   onLightboxKey);
        });

        // Reset selection + lastClickIdx when album filter changes
        watch(() => gall.activeAlbumId, () => { lastClickIdx = -1; gallClearSel(); });

        const filteredImages = computed(() => {
            if (gall.activeAlbumId === null)              return gall.images;
            if (gall.activeAlbumId === '__unassigned__')  return gall.images.filter(i => !i.album_id);
            return gall.images.filter(i => i.album_id === gall.activeAlbumId);
        });

        const selectedCount = computed(() => gallSelCount());

        function thumbUrl(img) { return appBase + '/' + img.file_thumb; }

        // ── Multi-select ──────────────────────────────────────────────────────
        function onImageClick(e, img) {
            const imgs = filteredImages.value;
            const idx  = imgs.findIndex(i => i.id === img.id);
            if (e.shiftKey && lastClickIdx !== -1) {
                const from = Math.min(lastClickIdx, idx);
                const to   = Math.max(lastClickIdx, idx);
                for (let i = from; i <= to; i++) gallSelect(imgs[i].id);
            } else {
                gallIsSelected(img.id) ? gallDeselect(img.id) : gallSelect(img.id);
            }
            lastClickIdx = idx;
        }

        function selectAll() {
            filteredImages.value.forEach(img => gallSelect(img.id));
            lastClickIdx = filteredImages.value.length - 1;
        }

        // ── Lasso (rubber-band) ───────────────────────────────────────────────
        const lassoStyle = computed(() => {
            if (!lasso.active) return { display: 'none' };
            return {
                left:   Math.min(lasso.x1, lasso.x2) + 'px',
                top:    Math.min(lasso.y1, lasso.y2) + 'px',
                width:  Math.abs(lasso.x2 - lasso.x1) + 'px',
                height: Math.abs(lasso.y2 - lasso.y1) + 'px',
            };
        });

        function onMainMouseDown(e) {
            if (e.button !== 0) return;
            // Skip if clicking on/inside an image item or any interactive element
            if (e.target.closest('.hw-gallery-item, button, input, select, textarea, label, a')) return;
            if (!e.shiftKey) gallClearSel();
            lasso.active = true;
            lasso.x1 = lasso.x2 = e.clientX;
            lasso.y1 = lasso.y2 = e.clientY;
            document.addEventListener('mousemove', onLassoMove);
            document.addEventListener('mouseup',  onLassoEnd);
            e.preventDefault();
        }

        function onLassoMove(e) {
            if (!lasso.active) return;
            lasso.x2 = e.clientX; lasso.y2 = e.clientY;
        }

        function onLassoEnd() {
            if (!lasso.active) return;
            const x1 = Math.min(lasso.x1, lasso.x2), x2 = Math.max(lasso.x1, lasso.x2);
            const y1 = Math.min(lasso.y1, lasso.y2), y2 = Math.max(lasso.y1, lasso.y2);
            if (x2 - x1 > 4 || y2 - y1 > 4) {
                gridEl.value?.querySelectorAll('[data-id]').forEach(el => {
                    const r = el.getBoundingClientRect();
                    if (r.left < x2 && r.right > x1 && r.top < y2 && r.bottom > y1) gallSelect(el.dataset.id);
                });
            }
            lasso.active = false;
            document.removeEventListener('mousemove', onLassoMove);
            document.removeEventListener('mouseup',  onLassoEnd);
        }

        // ── Refresh ───────────────────────────────────────────────────────────
        const refreshing = ref(false);
        async function refreshGallery() {
            refreshing.value = true;
            try { await _loadGallery(true); } finally { refreshing.value = false; }
        }

        // ── Lightbox ──────────────────────────────────────────────────────────
        const lightbox = reactive({ open: false, imgId: null });
        const lightboxImg = computed(() =>
            lightbox.imgId ? gall.images.find(i => i.id === lightbox.imgId) ?? null : null
        );
        const lightboxUrl = computed(() =>
            lightboxImg.value
                ? appBase + '/' + (lightboxImg.value.file_xl ?? lightboxImg.value.file_webp ?? '')
                : ''
        );

        function openLightbox(img) { lightbox.open = true; lightbox.imgId = img.id; }
        function closeLightbox()   { lightbox.open = false; lightbox.imgId = null; }

        function lightboxStep(dir) {
            const imgs = filteredImages.value;
            const idx  = imgs.findIndex(i => i.id === lightbox.imgId);
            const next = idx + dir;
            if (next >= 0 && next < imgs.length) lightbox.imgId = imgs[next].id;
        }

        function onLightboxKey(e) {
            if (!lightbox.open) return;
            if (e.key === 'Escape')     { closeLightbox(); e.stopPropagation(); }
            if (e.key === 'ArrowLeft')  lightboxStep(-1);
            if (e.key === 'ArrowRight') lightboxStep(1);
        }

        watch(() => lightbox.open, open => {
            if (open) document.addEventListener('keydown', onLightboxKey);
            else      document.removeEventListener('keydown', onLightboxKey);
        });

        // ── Drag images → album (HTML5 drag API) ──────────────────────────────
        function onImageDragStart(e, img) {
            if (!gallIsSelected(img.id)) {
                gallClearSel();
                gallSelect(img.id);
                lastClickIdx = filteredImages.value.findIndex(i => i.id === img.id);
            }
            const ids = gallSelIds();
            e.dataTransfer.setData('application/gallery-ids', JSON.stringify(ids));
            e.dataTransfer.effectAllowed = 'move';

            // Custom drag badge showing count
            const count = ids.length;
            const badge = document.createElement('div');
            badge.className = 'hw-drag-badge';
            badge.textContent = count === 1
                ? props.t('gallery_drag_one')
                : props.t('gallery_drag_many').replace('{n}', count);
            document.body.appendChild(badge);
            // must be in DOM when setDragImage is called; remove on next tick
            e.dataTransfer.setDragImage(badge, badge.offsetWidth / 2, 20);
            setTimeout(() => badge.parentNode?.removeChild(badge), 0);

            _gallDragging.value = true;
        }

        function onImageDragEnd() {
            _gallDragging.value  = false;
            _gallDragAlbum.value = null;
        }

        // ── Upload ────────────────────────────────────────────────────────────
        function openPicker() { fileInputRef.value?.click(); }

        function onUploadDragOver(e) {
            if (e.dataTransfer.types.includes('application/gallery-ids')) return;
            e.preventDefault(); uploadDragOver.value = true;
        }
        function onUploadDragLeave() { uploadDragOver.value = false; }
        function onUploadDrop(e) {
            e.preventDefault(); uploadDragOver.value = false;
            const files = [...e.dataTransfer.files].filter(f => f.type.startsWith('image/'));
            if (files.length) uploadFiles(files);
        }
        function onFileChange(e) {
            const files = [...e.target.files];
            if (files.length) uploadFiles(files);
            e.target.value = '';
        }

        async function uploadFiles(files) {
            gall.uploading = true; gall.uploadDone = 0; gall.uploadErrors = 0; gall.uploadTotal = files.length;
            const BATCH = 5;
            for (let i = 0; i < files.length; i += BATCH) {
                await Promise.all(files.slice(i, i + BATCH).map(uploadOne));
            }
            gall.uploading = false;
        }

        async function uploadOne(file) {
            try {
                const fd  = new FormData();
                fd.append('file', file);
                const res  = await fetch(window.location.origin + '/api/index.php/gallery/upload', {
                    method: 'POST', credentials: 'same-origin', body: fd,
                });
                const json = await res.json();
                if (json.success && Array.isArray(json.data)) gall.images.push(...json.data);
            } catch { gall.uploadErrors++; }
            gall.uploadDone++;
        }

        // ── Toolbar: bulk assign album ────────────────────────────────────────
        async function onToolbarAlbum(e) {
            const val = e.target.value;
            if (!val) return;
            e.target.value = '';
            const albumId = val === '__none__' ? null : val;
            const ids = gallSelIds();
            if (!ids.length) return;
            await useApi().put('/gallery/bulk', { ids, album_id: albumId });
            ids.forEach(id => {
                const img = gall.images.find(i => i.id === id);
                if (img) img.album_id = albumId;
            });
            gallClearSel();
        }

        // ── Toolbar: bulk delete ──────────────────────────────────────────────
        async function deleteSelected() {
            if (!deleteSelConfirm.value) {
                deleteSelConfirm.value = true;
                setTimeout(() => deleteSelConfirm.value = false, 3000);
                return;
            }
            deleteSelConfirm.value = false;
            const ids  = gallSelIds();
            const BATCH = 10;
            for (let i = 0; i < ids.length; i += BATCH) {
                await Promise.all(ids.slice(i, i + BATCH).map(id => useApi().del('/gallery/' + id).catch(() => {})));
            }
            const delSet = new Set(ids);
            if (lightbox.open && delSet.has(lightbox.imgId)) closeLightbox();
            gall.images = gall.images.filter(i => !delSet.has(i.id));
            gallClearSel();
        }

        // ── Individual delete ─────────────────────────────────────────────────
        async function deleteImage(id) {
            if (deleteConfirmId.value === id) {
                try {
                    await useApi().del('/gallery/' + id);
                    if (lightbox.open && lightbox.imgId === id) closeLightbox();
                    gall.images = gall.images.filter(i => i.id !== id);
                    gallDeselect(id);
                    deleteConfirmId.value = null;
                } catch {
                    deleteConfirmId.value = null;
                    deleteErrMsg.value = props.t('gallery_delete_error');
                    setTimeout(() => { deleteErrMsg.value = ''; }, 4000);
                }
            } else {
                deleteConfirmId.value = id;
                setTimeout(() => { if (deleteConfirmId.value === id) deleteConfirmId.value = null; }, 3000);
            }
        }

        return {
            gall, sel, filteredImages, selectedCount,
            gridEl, fileInputRef, uploadDragOver, deleteConfirmId, deleteSelConfirm, deleteErrMsg,
            lasso, lassoStyle,
            sortMode, reorderDrag, enterSortMode, exitSortMode,
            onSortDragStart, onSortDragOver, onSortDragLeave, onSortDragEnd, onSortDrop,
            lightbox, lightboxImg, lightboxUrl, openLightbox, closeLightbox, lightboxStep,
            refreshing, refreshGallery,
            thumbUrl,
            onImageClick, selectAll, gallClearSel,
            onMainMouseDown,
            onImageDragStart, onImageDragEnd,
            openPicker, onUploadDragOver, onUploadDragLeave, onUploadDrop, onFileChange,
            onToolbarAlbum, deleteSelected, deleteImage,
        };
    },
    template: `
    <div class="hw-gallery-main" @mousedown="onMainMouseDown($event)">

      <!-- Upload zone -->
      <div class="hw-upload-zone"
        :class="{
          'hw-upload-zone--over':    uploadDragOver,
          'hw-upload-zone--busy':    gall.uploading,
          'hw-upload-zone--compact': !gall.uploading && gall.images.length > 0,
        }"
        @dragover="onUploadDragOver" @dragleave="onUploadDragLeave" @drop="onUploadDrop"
        @click="openPicker"
      >
        <template v-if="gall.uploading">
          <div class="hw-upload-bar">
            <div class="hw-upload-bar__fill" :style="{width: (gall.uploadTotal ? gall.uploadDone/gall.uploadTotal*100 : 0)+'%'}"></div>
          </div>
          <span class="hw-upload-zone__label">{{ gall.uploadDone }} / {{ gall.uploadTotal }} {{ t('gallery_uploading') }}</span>
        </template>
        <template v-else>
          <i class="hgi-stroke hgi-upload-01 hw-upload-zone__icon"></i>
          <span class="hw-upload-zone__label">{{ t('gallery_drop_hint') }}</span>
          <div class="hw-upload-zone__btns">
            <button type="button" class="primary is-small" @click.stop="openPicker">{{ t('gallery_select_files') }}</button>
            <button type="button" class="primitive is-small hw-upload-zone__refresh"
              @click.stop="refreshGallery" :disabled="refreshing"
              :title="t('gallery_refresh')"
            ><i class="hgi-stroke" :class="refreshing ? 'hgi-loading-03' : 'hgi-refresh'"></i></button>
            <button v-if="filteredImages.length > 1" type="button" class="outline is-small hw-sort-toggle"
              @click.stop="enterSortMode"
            ><i class="hgi hgi-stroke hgi-arrow-left-right"></i> {{ t('gallery_sort_mode') }}</button>
          </div>
        </template>
        <input ref="fileInputRef" type="file" multiple accept="image/jpeg,image/png,image/gif,image/webp"
          class="hw-upload-input" @change="onFileChange">
      </div>

      <!-- Delete error -->
      <div v-if="deleteErrMsg" class="hw-upload-errors">
        <i class="hgi-stroke hgi-alert-circle"></i> {{ deleteErrMsg }}
      </div>

      <!-- Upload errors -->
      <div v-if="!gall.uploading && gall.uploadErrors > 0" class="hw-upload-errors">
        <i class="hgi-stroke hgi-alert-circle"></i>
        {{ gall.uploadErrors }} {{ t('gallery_upload_errors') }}
      </div>

      <!-- Sort mode banner (independent of grid visibility) -->
      <div v-if="sortMode" class="hw-sort-banner">
        <i class="hgi hgi-stroke hgi-arrow-left-right"></i>
        <span>{{ t('gallery_sort_hint') }}</span>
        <button type="button" class="primary is-small" @click="exitSortMode">{{ t('gallery_sort_done') }}</button>
      </div>

      <!-- Empty state -->
      <div v-if="!sortMode && filteredImages.length === 0 && !gall.uploading" class="hw-gallery-empty">
        <i class="hgi-stroke hgi-image-01"></i>
        <p>{{ t('gallery_empty') }}</p>
      </div>

      <!-- Image grid -->
      <div v-if="filteredImages.length > 0" class="hw-gallery-grid" ref="gridEl"
        :class="{'hw-gallery-grid--sort': sortMode}"
      >
        <div
          v-for="img in filteredImages"
          :key="img.id"
          :data-id="img.id"
          class="hw-gallery-item"
          :class="{
            'hw-gallery-item--selected':  !!sel[img.id],
            'hw-gallery-item--sortable':  sortMode,
            'hw-gallery-item--dragging':  sortMode && reorderDrag.ids.includes(img.id),
          }"
          draggable="true"
          @click.stop="onImageClick($event, img)"
          @dblclick.stop="!sortMode && openLightbox(img)"
          @dragstart="sortMode ? onSortDragStart($event, img) : onImageDragStart($event, img)"
          @dragend="sortMode ? onSortDragEnd() : onImageDragEnd()"
          @dragover="sortMode && onSortDragOver($event, img)"
          @dragleave="sortMode && onSortDragLeave($event)"
          @drop.stop="sortMode && onSortDrop($event, img)"
        >
          <img :src="thumbUrl(img)" class="hw-gallery-item__img" loading="lazy" alt="">
          <div class="hw-gallery-item__check">
            <i v-if="sel[img.id]" class="hgi-stroke hgi-checkmark-circle-01"></i>
          </div>
          <div v-if="!sortMode" class="hw-gallery-item__actions">
            <button type="button"
              class="hw-gallery-item__del"
              :class="{'hw-gallery-item__del--confirm': deleteConfirmId === img.id}"
              :title="deleteConfirmId === img.id ? t('confirm_delete') : t('delete')"
              @click.stop="deleteImage(img.id)"
            ><i class="hgi-stroke hgi-delete-01"></i></button>
          </div>
          <div v-if="img.album_id && !sortMode" class="hw-gallery-item__dot"
            :title="gall.albums.find(a => a.id === img.album_id)?.name ?? ''"></div>
        </div>
      </div>

      <!-- Selection toolbar -->
      <div v-if="selectedCount > 0" class="hw-selection-bar">
        <span class="hw-selection-bar__count">{{ selectedCount }} {{ t('gallery_selected') }}</span>
        <button type="button" class="outline is-small" @click="selectAll">{{ t('gallery_select_all') }}</button>
        <div class="hw-selection-bar__spacer"></div>
        <template v-if="!sortMode">
          <select class="select hw-selection-bar__album" @change="onToolbarAlbum">
            <option value="">{{ t('gallery_assign_album') }}</option>
            <option value="__none__">{{ t('gallery_no_album') }}</option>
            <option v-for="album in gall.albums" :key="album.id" :value="album.id">{{ album.name }}</option>
          </select>
          <button type="button"
            class="hw-selection-bar__delete"
            :class="{'hw-selection-bar__delete--confirm': deleteSelConfirm}"
            @click="deleteSelected"
          ><i class="hgi-stroke hgi-delete-01"></i> {{ deleteSelConfirm ? t('confirm_delete') : t('delete') }}</button>
        </template>
        <button type="button" class="button primitive hw-selection-bar__clear" @click="gallClearSel" :title="t('cancel')">
          <i class="hgi-stroke hgi-cancel-01"></i>
        </button>
      </div>

      <!-- Sort drop indicator (position:fixed, sits in the gap between items) -->
      <div v-if="sortMode && reorderDrag.targetId" class="hw-drop-indicator" :style="{
        left:   reorderDrag.indX + 'px',
        top:    reorderDrag.indY + 'px',
        height: reorderDrag.indH + 'px',
      }"></div>

      <!-- Lasso rect (position:fixed, viewport coords) -->
      <div class="hw-lasso" :style="lassoStyle"></div>

      <!-- Lightbox -->
      <teleport to="body">
        <div v-if="lightbox.open" class="hw-lightbox" @click.self="closeLightbox">
          <button type="button" class="hw-lightbox__close" @click="closeLightbox">
            <i class="hgi-stroke hgi-cancel-01"></i>
          </button>
          <button type="button" class="hw-lightbox__nav hw-lightbox__nav--prev"
            @click="lightboxStep(-1)"
            :disabled="filteredImages.findIndex(i => i.id === lightbox.imgId) === 0"
          ><i class="hgi-stroke hgi-arrow-left-01"></i></button>
          <img :src="lightboxUrl" class="hw-lightbox__img" alt="" draggable="false">
          <button type="button" class="hw-lightbox__nav hw-lightbox__nav--next"
            @click="lightboxStep(1)"
            :disabled="filteredImages.findIndex(i => i.id === lightbox.imgId) === filteredImages.length - 1"
          ><i class="hgi-stroke hgi-arrow-right-01"></i></button>
          <div class="hw-lightbox__counter" v-if="lightboxImg">
            {{ filteredImages.findIndex(i => i.id === lightbox.imgId) + 1 }} / {{ filteredImages.length }}
          </div>
        </div>
      </teleport>

    </div>
    `,
};

// ── HwPanel ───────────────────────────────────────────────────────────────────
const HwPanel = {
    props: ['activeTab', 't', 'config', 'tabs', 'logout'],
    emits: ['update:activeTab'],
    setup() {
        const width     = ref(parseInt(localStorage.getItem('hw_panel_width') || '320'));
        const collapsed = ref(localStorage.getItem('hw_panel_collapsed') === '1');
        const minW = 310, maxW = 480;
        let dragging = false, startX = 0, startW = 0;

        function onPointerDown(e) {
            if (e.target.closest('.hw-panel__collapse-btn')) return;
            if (collapsed.value) return;
            e.currentTarget.setPointerCapture(e.pointerId);
            dragging = true; startX = e.clientX; startW = width.value;
        }
        function onPointerMove(e) {
            if (!dragging) return;
            width.value = Math.min(maxW, Math.max(minW, startW + (e.clientX - startX)));
        }
        function onPointerUp() {
            if (!dragging) return;
            dragging = false;
            localStorage.setItem('hw_panel_width', String(width.value));
        }
        function toggleCollapse() {
            collapsed.value = !collapsed.value;
            localStorage.setItem('hw_panel_collapsed', collapsed.value ? '1' : '0');
        }

        const _gd = useEditorDraft();
        const onSave    = () => _gd.save();
        const onDiscard = () => _gd.discard();
        const editorDraft = _editorDraft;
        return { width, collapsed, onPointerDown, onPointerMove, onPointerUp, toggleCollapse, onSave, onDiscard, editorDraft };
    },
    template: `
    <div class="hw-panel-outer" :class="{'hw-panel-outer--collapsed': collapsed}"
         :style="collapsed ? {width:'0px'} : {width: width+'px'}">
      <aside class="hw-panel theme-always-dark">
        <div class="hw-panel__header">
          <nav class="hw-tabnav">
            <button v-for="tab in tabs" :key="tab.id"
              class="hw-tabnav__btn" :class="[{'hw-tabnav__btn--active': activeTab===tab.id}, tab.btnClass||'']"
              :title="tab.label" @click="$emit('update:activeTab', tab.id)" v-html="tab.icon">
            </button>
            <div class="hw-tabnav__spacer"></div>
            <button class="hw-tabnav__btn hw-tabnav__btn--logout" :title="t('logout')" @click="logout">
              <i class="hgi-stroke hgi-logout-02"></i>
            </button>
          </nav>
        </div>

        <div class="hw-panel__body">
          <slot name="content"></slot>
        </div>

        <div class="hw-panel__footer">
            <button
              class="hw-discard-btn"
              :disabled="!editorDraft.isDirty"
              :title="t('editor_discard')"
              @click="onDiscard"
            ><i class="hgi-stroke hgi-cancel-01"></i></button>
            <button
              class="hw-save-btn"
              :class="{'hw-save-btn--dirty': editorDraft.isDirty, 'hw-save-btn--ok': editorDraft.saveOk}"
              :disabled="!editorDraft.isDirty || editorDraft.saving"
              :title="editorDraft.isDirty ? t('editor_unsaved') : (editorDraft.saveOk ? t('draft_saved') : t('draft_save'))"
              @click="onSave"
            >
              <span v-if="editorDraft.isDirty && !editorDraft.saving" class="hw-save-btn__dot">●</span>
              <i v-else-if="editorDraft.saving" class="hgi-stroke hgi-loading-01 hw-save-btn__spin"></i>
              <i v-else-if="editorDraft.saveOk" class="hgi-stroke hgi-tick-double-03"></i>
              <span>{{ editorDraft.saving ? t('draft_saving') : (editorDraft.saveOk ? t('draft_saved') : t('draft_save')) }}</span>
            </button>
        </div>
      </aside>

      <div class="hw-panel__resize-handle"
           tabindex="0"
           @pointerdown="onPointerDown"
           @pointermove="onPointerMove"
           @pointerup="onPointerUp"
           @keydown.space.prevent="toggleCollapse">
        <button class="hw-panel__collapse-btn" tabindex="-1"
                :title="collapsed ? 'Panel einblenden' : 'Panel ausblenden'"
                @click.stop="toggleCollapse">
          <i class="hgi-stroke hgi-arrow-left-01"></i>
        </button>
      </div>
    </div>
    `,
};

// ── GuestState (module-level reactive, shared between GuestsTab + GuestsMain) ──
const _guestState = reactive({
    groups:        [],
    guests:        [],
    langs:         [{ code: 'de', label: 'Deutsch' }],
    mealOptions:   [],
    loaded:        false,
    activeGroupId: null,
    activeView:    'list',   // 'list' | 'new' | 'mail'
    search:        '',
    rsvpFilter:    '',
});

async function _loadGuests(force = false) {
    if (_guestState.loaded && !force) return;
    const api = useApi();
    const [data, sections, langData] = await Promise.all([
        api.get('/guests'),
        api.get('/sections').catch(() => []),
        api.get('/languages').catch(() => null),
    ]);
    _guestState.groups = data.groups || [];
    _guestState.guests = data.guests || [];

    if (langData?.available) {
        const active = langData.available.filter(l => l.active);
        if (active.length) _guestState.langs = active;
    }

    // Meal options from RSVP section
    const rsvpSec = (Array.isArray(sections) ? sections : []).find(s => s.type === 'rsvp');
    if (rsvpSec?.data?.show_meal && Array.isArray(rsvpSec.data.meal_options)) {
        _guestState.mealOptions = rsvpSec.data.meal_options
            .map(o => o.de?.label || o.en?.label || o.label || '')
            .filter(Boolean);
    } else {
        _guestState.mealOptions = [];
    }
    _guestState.loaded = true;
}

// ── GuestsTab (panel) ─────────────────────────────────────────────────────────
const GuestsTab = {
    props: ['t'],
    setup(props) {
        const api             = useApi();
        const gs              = _guestState;
        const newGroupName    = ref('');
        const addingGroup     = ref(false);
        const deleteConfirmId = ref(null);
        const editId          = ref(null);
        const editName        = ref('');
        const saving          = ref(false);

        onMounted(async () => { if (!gs.loaded) await _loadGuests(); });

        const groupCounts = computed(() => {
            const c = { __all__: gs.guests.length };
            for (const g of gs.groups) c[g.id] = 0;
            for (const guest of gs.guests) {
                for (const gid of guest.group_ids || []) {
                    if (c[gid] !== undefined) c[gid]++;
                }
            }
            return c;
        });

        function setGroup(id) { gs.activeGroupId = id; gs.activeView = 'list'; }
        function setMail()    { gs.activeView = 'mail'; }

        async function addGroup() {
            if (!newGroupName.value.trim()) return;
            saving.value = true;
            try {
                const g = await api.post('/guests/groups', { name: newGroupName.value.trim() });
                gs.groups.push(g);
                newGroupName.value = '';
                addingGroup.value  = false;
            } finally { saving.value = false; }
        }

        function startEdit(group) { editId.value = group.id; editName.value = group.name; }

        async function saveEdit(group) {
            const name = editName.value.trim();
            editId.value = null;
            if (!name || name === group.name) return;
            const updated = await api.put('/guests/groups/' + group.id, { name });
            const idx = gs.groups.findIndex(g => g.id === group.id);
            if (idx !== -1) gs.groups[idx] = updated;
        }

        async function deleteGroup(id) {
            if (deleteConfirmId.value !== id) { deleteConfirmId.value = id; return; }
            await api.del('/guests/groups/' + id);
            gs.groups = gs.groups.filter(g => g.id !== id);
            for (const guest of gs.guests) {
                guest.group_ids = (guest.group_ids || []).filter(gid => gid !== id);
            }
            if (gs.activeGroupId === id) gs.activeGroupId = null;
            deleteConfirmId.value = null;
        }

        return { gs, newGroupName, addingGroup, saving, deleteConfirmId, editId, editName,
                 groupCounts, setGroup, setMail, addGroup, startEdit, saveEdit, deleteGroup };
    },
    template: `
    <div class="hw-tab-content">
    <h2 class="hw-tabtitle">{{ t('nav_guests') }}</h2>
    <div class="hw-albums">
      <button class="hw-album-filter-btn"
        :class="{'hw-album-filter-btn--active': gs.activeGroupId === null && gs.activeView === 'list'}"
        @click="setGroup(null)">
        <i class="hgi-stroke hgi-user-group"></i>
        <span class="hw-album-filter-btn__name">{{ t('guests_all') }}</span>
        <span class="hw-album-count">{{ groupCounts['__all__'] || 0 }}</span>
      </button>

      <div v-for="group in gs.groups" :key="group.id" class="hw-album-row">
        <template v-if="editId === group.id">
          <input
            class="select hw-field__input hw-album-row__edit"
            v-model="editName"
            @keydown.enter="saveEdit(group)"
            @keydown.escape="editId = null"
            @blur="saveEdit(group)"
            autofocus
          >
        </template>
        <template v-else>
          <button class="hw-album-filter-btn"
            :class="{'hw-album-filter-btn--active': gs.activeGroupId === group.id && gs.activeView === 'list'}"
            @click="setGroup(group.id)">
            <i class="hgi-stroke hgi-user-multiple-02"></i>
            <span class="hw-album-filter-btn__name">{{ group.name }}</span>
            <span class="hw-album-count">{{ groupCounts[group.id] || 0 }}</span>
          </button>
          <div class="hw-album-row__actions">
            <button type="button" class="button primitive hw-album-action"
              :title="t('gallery_rename_album')" @click.stop="startEdit(group)">
              <i class="hgi-stroke hgi-pencil-edit-01"></i>
            </button>
            <button type="button" class="button primitive hw-album-action hw-album-action--delete"
              :class="{'hw-album-action--confirm': deleteConfirmId === group.id}"
              :title="deleteConfirmId === group.id ? t('confirm_delete') : t('delete')"
              @click.stop="deleteGroup(group.id)">
              <i class="hgi-stroke hgi-delete-01"></i>
            </button>
          </div>
        </template>
      </div>

      <div v-if="addingGroup" class="hw-album-create">
        <input class="hw-field__input input" v-model="newGroupName"
          :placeholder="t('guests_group_placeholder')"
          @keyup.enter="addGroup" @keyup.escape="addingGroup=false; newGroupName=''" />
        <button class="primary is-small" @click="addGroup" :disabled="saving">{{ t('guests_group_add') }}</button>
        <button class="outline is-small" @click="addingGroup=false; newGroupName=''">{{ t('cancel') }}</button>
      </div>
      <button v-else class="hw-album-filter-btn" @click="addingGroup=true" style="color:var(--dark-50)">
        <i class="hgi-stroke hgi-plus-sign"></i>
        <span class="hw-album-filter-btn__name">{{ t('guests_group_new') }}</span>
      </button>
    </div>

    <div class="hw-panel-divider"></div>

    <button class="hw-album-filter-btn"
      :class="{'hw-album-filter-btn--active': gs.activeView === 'mail'}"
      @click="setMail">
      <i class="hgi-stroke hgi-mail-01"></i>
      <span class="hw-album-filter-btn__name">{{ t('guests_mail_compose') }}</span>
    </button>
    </div>
    `,
};

// ── GuestsMain ────────────────────────────────────────────────────────────────
const GuestsMain = {
    props: ['t', 'uiLang'],
    setup(props) {
        const api = useApi();
        const gs  = _guestState;

        // Edit state
        const saving          = ref(false);
        const copyDone        = ref(false);
        const deleteConfirmId = ref(null);

        // editDraft with explicit schema for reliable Vue 3 reactivity
        const editDraft = reactive({
            id: null, token: null, name: '', greeting: '', email: '',
            language: 'de', group_ids: [], persons: 1, rsvp: 'pending',
            meal_persons: [], message: '', created_at: null,
        });

        onMounted(async () => { if (!gs.loaded) await _loadGuests(); });

        watch(() => editDraft.persons, () => syncMealPersons());

        const filteredGuests = computed(() => {
            let list = gs.guests;
            if (gs.activeGroupId) {
                list = list.filter(g => (g.group_ids || []).includes(gs.activeGroupId));
            }
            if (gs.rsvpFilter) {
                list = list.filter(g => (g.rsvp || 'pending') === gs.rsvpFilter);
            }
            if (gs.search.trim()) {
                const q = gs.search.trim().toLowerCase();
                list = list.filter(g =>
                    (g.name || '').toLowerCase().includes(q) ||
                    (g.email || '').toLowerCase().includes(q)
                );
            }
            return list;
        });

        function loadDraft(guest) {
            const c = JSON.parse(JSON.stringify(guest));
            editDraft.id           = c.id;
            editDraft.token        = c.token || null;
            editDraft.name         = c.name || '';
            editDraft.greeting     = c.greeting || '';
            editDraft.email        = c.email || '';
            editDraft.language     = c.language || gs.langs[0]?.code || 'de';
            editDraft.group_ids    = c.group_ids || [];
            editDraft.persons      = c.persons || 1;
            editDraft.rsvp         = c.rsvp || 'pending';
            editDraft.meal_persons = c.meal_persons || [];
            editDraft.message      = c.message || '';
            editDraft.created_at   = c.created_at || null;
            syncMealPersons();
        }

        function syncMealPersons() {
            const n    = Math.max(1, parseInt(editDraft.persons) || 1);
            const curr = editDraft.meal_persons;
            while (curr.length < n) curr.push({ name: '', meal: gs.mealOptions[0] || '' });
            while (curr.length > n) curr.pop();
        }

        function openEdit(id) {
            const guest = gs.guests.find(g => g.id === id);
            if (!guest) return;
            loadDraft(guest);
            deleteConfirmId.value = null;
            gs.activeView = 'edit';
        }

        function startNew() {
            editDraft.id           = null;
            editDraft.token        = null;
            editDraft.name         = '';
            editDraft.greeting     = '';
            editDraft.email        = '';
            editDraft.language     = gs.langs[0]?.code || 'de';
            editDraft.group_ids    = gs.activeGroupId ? [gs.activeGroupId] : [];
            editDraft.persons      = 1;
            editDraft.rsvp         = 'pending';
            editDraft.meal_persons = gs.mealOptions.length ? [{ name: '', meal: gs.mealOptions[0] }] : [];
            editDraft.message      = '';
            editDraft.created_at   = null;
            gs.activeView          = 'new';
        }

        function cancelEdit() {
            gs.activeView         = 'list';
            deleteConfirmId.value = null;
        }

        async function saveGuest() {
            saving.value = true;
            try {
                const payload = {
                    name: editDraft.name, greeting: editDraft.greeting,
                    email: editDraft.email, language: editDraft.language,
                    group_ids: editDraft.group_ids, persons: editDraft.persons,
                    rsvp: editDraft.rsvp,
                    meal_persons: JSON.parse(JSON.stringify(editDraft.meal_persons)),
                    message: editDraft.message,
                };
                if (gs.activeView === 'new') {
                    const created = await api.post('/guests', payload);
                    gs.guests.unshift(created);
                } else {
                    const updated = await api.put('/guests/' + editDraft.id, payload);
                    const idx = gs.guests.findIndex(g => g.id === editDraft.id);
                    if (idx >= 0) gs.guests[idx] = updated;
                }
                gs.activeView = 'list';
            } finally { saving.value = false; }
        }

        async function deleteGuest(id) {
            if (deleteConfirmId.value !== id) { deleteConfirmId.value = id; return; }
            await api.del('/guests/' + id);
            gs.guests = gs.guests.filter(g => g.id !== id);
            deleteConfirmId.value = null;
        }

        function groupName(id) {
            return gs.groups.find(g => g.id === id)?.name || id;
        }

        async function copyToken(token) {
            const base = window.HW_APP_BASE || window.location.origin;
            const url  = base + '/?g=' + token;
            try {
                await navigator.clipboard.writeText(url);
                copyDone.value = true;
                setTimeout(() => { copyDone.value = false; }, 2000);
            } catch(e) {}
        }

        const publicBase = window.HW_APP_BASE || window.location.origin;

        function csvExport() {
            const p = gs.activeGroupId ? '?group=' + encodeURIComponent(gs.activeGroupId) : '';
            window.open(window.location.origin + '/api/index.php/guests/export' + p, '_blank');
        }

        // ── Mail state ────────────────────────────────────────────────────────
        const mailFilter     = ref('all');   // 'all' | 'group' | 'rsvp'
        const mailFilterVal  = ref('');
        const mailTemplates  = ref({});      // { de: { subject, body }, en: { ... }, ... }
        const mailActiveLang = ref('de');
        const mailBodyRef    = ref(null);    // textarea ref for cursor-position insert
        const mailHeaderImg   = ref('');      // relative URL, e.g. "uploads/user/xxx.webp"
        const mailHeaderThumb = ref('');      // thumb URL if available, else ''
        const mailShowMedia  = ref(false);
        const mailStatus     = ref(null);
        const mailSending    = ref(false);
        const mailPollTimer  = ref(null);
        const mailPreviewIdx = ref(0);

        const mailDefaultLang     = computed(() => gs.langs[0]?.code || 'de');
        const mailCurrentTemplate = computed(() => {
            const lang = mailActiveLang.value;
            if (!mailTemplates.value[lang]) mailTemplates.value[lang] = { subject: '', body: '' };
            return mailTemplates.value[lang];
        });

        // Variable chips — language-appropriate names, both work in PHP/preview.
        // Stored as plain strings to avoid Vue template compiler misreading {{ }} as interpolations.
        const _varsDE = ['{{name}}', '{{begrüssung}}', '{{hochzeitsdatum}}', '{{websitelink}}', '{{rsvp_link}}', '{{rsvp_button}}'];
        const _varsEN = ['{{name}}', '{{greeting}}',   '{{wedding_date}}',   '{{website_link}}','{{rsvp_link}}', '{{rsvp_button}}'];
        const mailVarsList = computed(() => props.uiLang === 'de' ? _varsDE : _varsEN);

        // Textarea placeholder — kept in setup to avoid {{ }} being parsed by Vue's template compiler
        const mailBodyPlaceholder = computed(() =>
            mailActiveLang.value === 'de'
                ? 'Hallo {{begrüssung}},\n\nwir freuen uns, euch zu unserer Hochzeit einzuladen!\n\nRSVP: {{rsvp_link}}'
                : 'Dear {{greeting}},\n\nwe are delighted to invite you to our wedding!\n\nRSVP: {{rsvp_link}}'
        );

        const mailDraftStatus          = ref('');  // '' | 'saving' | 'saved'
        const mailPreviewHtmlDebounced = ref('');
        const mailWeddingDate          = ref('');
        const mailInfoOpen             = ref(false);
        const mailDesignOpen           = ref(false);
        const mailDesign               = ref({ bg: '#f4f2f1', body_bg: '#ffffff', body_color: '#3a3530', btn_bg: '#B99954', btn_color: '#ffffff' });
        let   _draftTimer              = null;

        async function loadMailConfig() {
            try {
                const cfg = await api.get('/guests/mail/config');
                if (cfg.wedding_date !== undefined) mailWeddingDate.value = cfg.wedding_date;
            } catch (_) { /* config not yet set — fine */ }
        }

        async function loadMailDraft() {
            try {
                const d = await api.get('/guests/mail/draft');
                // Migration: flat format (old) → templates object (new)
                if (d.templates !== undefined) {
                    mailTemplates.value = d.templates;
                } else if (d.subject !== undefined || d.body !== undefined) {
                    const defLang = d.default_lang || mailDefaultLang.value;
                    mailTemplates.value = { [defLang]: { subject: d.subject || '', body: d.body || '' } };
                }
                if (d.default_lang       !== undefined) mailActiveLang.value    = d.default_lang;
                if (d.header_image       !== undefined) mailHeaderImg.value     = d.header_image;
                if (d.header_image_thumb !== undefined) mailHeaderThumb.value   = d.header_image_thumb;
                if (d.filter             !== undefined) mailFilter.value         = d.filter;
                if (d.filter_value       !== undefined) mailFilterVal.value      = d.filter_value;
                if (d.design             !== undefined) mailDesign.value         = { ...mailDesign.value, ...d.design };
            } catch (_) { /* no draft yet — that's fine */ }
            await nextTick();
            mailPreviewHtmlDebounced.value = mailPreviewHtml.value;
        }

        async function saveMailDraft() {
            mailPreviewHtmlDebounced.value = mailPreviewHtml.value;
            mailDraftStatus.value = 'saving';
            try {
                await api.post('/guests/mail/draft', {
                    templates:          mailTemplates.value,
                    default_lang:       mailDefaultLang.value,
                    header_image:       mailHeaderImg.value,
                    header_image_thumb: mailHeaderThumb.value,
                    filter:             mailFilter.value,
                    filter_value:       mailFilterVal.value,
                    design:             mailDesign.value,
                });
                mailDraftStatus.value = 'saved';
                setTimeout(() => { if (mailDraftStatus.value === 'saved') mailDraftStatus.value = ''; }, 2500);
            } catch (_) { mailDraftStatus.value = ''; }
        }

        function scheduleDraftSave() {
            clearTimeout(_draftTimer);
            _draftTimer = setTimeout(saveMailDraft, 1500);
        }

        const mailRecipients = computed(() => {
            let list = gs.guests.filter(g => g.email);
            if (mailFilter.value === 'group' && mailFilterVal.value) {
                list = list.filter(g => (g.group_ids || []).includes(mailFilterVal.value));
            }
            if (mailFilter.value === 'rsvp' && mailFilterVal.value) {
                list = list.filter(g => (g.rsvp || 'pending') === mailFilterVal.value);
            }
            return list;
        });

        const mailPreviewGuest = computed(() => mailRecipients.value[mailPreviewIdx.value] || null);

        /** Build full HTML email preview — mirrors buildEmailHtml() in PHP */
        const mailPreviewHtml = computed(() => {
            const g    = mailPreviewGuest.value;
            const base = publicBase;
            const token = g?.token    || '';
            const lang  = g?.language || mailDefaultLang.value;
            const tpl   = mailTemplates.value[lang]
                       || mailTemplates.value[mailDefaultLang.value]
                       || Object.values(mailTemplates.value)[0]
                       || {};
            let body = tpl.body || '';

            // Detect HTML mode on the raw template BEFORE variable substitution
            // to avoid double-escaping when plain-text body contains escaped var values.
            const rawIsHtml = /(<\/?(p|br|div|table|h[1-6]|ul|ol|li)\b)/i.test(body);

            const d        = mailDesign.value;
            const btnBg    = d.btn_bg    || '#B99954';
            const btnColor = d.btn_color || '#ffffff';
            const rsvpButtonHtml =
                `<table role="presentation" cellpadding="0" cellspacing="0" border="0" style="margin:24px 0;">` +
                `<tr><td align="center" bgcolor="${btnBg}" style="border-radius:4px;">` +
                `<a href="${base}/?g=${token}&lang=${lang}" target="_blank" ` +
                `style="display:inline-block;padding:14px 36px;font-family:Arial,Helvetica,sans-serif;font-size:15px;font-weight:700;color:${btnColor};text-decoration:none;border-radius:4px;">Zu unserer Hochzeitswebseite</a>` +
                `</td></tr></table>`;

            if (!rawIsHtml) {
                // Plain-text mode: escape body first, then insert values as-is
                // (escHtml on values handles any special chars, no second pass needed)
                body = body.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
            }

            if (g) {
                const weddingEsc = escHtml(mailWeddingDate.value || '–');
                const siteLink   = base + '/?g=' + token;
                const rsvpLink   = base + '/?g=' + token + '&lang=' + lang;
                body = body
                    .replace(/\{\{name\}\}/g,                           escHtml(g.name     || ''))
                    .replace(/\{\{greeting\}\}|\{\{begrüssung\}\}/g,    escHtml(g.greeting || ''))
                    .replace(/\{\{wedding_date\}\}|\{\{hochzeitsdatum\}\}/g, weddingEsc)
                    .replace(/\{\{website_link\}\}|\{\{websitelink\}\}/g,    siteLink)
                    .replace(/\{\{rsvp_link\}\}/g,                       rsvpLink)
                    .replace(/\{\{rsvp_button\}\}/g,                     rsvpButtonHtml);
            }

            if (!rawIsHtml) {
                body = body.replace(/\n/g, '<br>');
            }

            const headerRow = mailHeaderImg.value
                ? `<tr><td style="padding:0;line-height:0;font-size:0;">` +
                  `<img src="${base}/${mailHeaderImg.value}" width="600" alt="" border="0" ` +
                  `style="display:block;width:100%;max-width:600px;height:auto;border-radius:4px 4px 0 0;"/></td></tr>`
                : '';

            const outerBg   = d.bg         || '#f4f2f1';
            const bodyBg    = d.body_bg     || '#ffffff';
            const bodyColor = d.body_color  || '#3a3530';
            return `<!DOCTYPE html><html lang="de"><head><meta charset="UTF-8">
<style>body{margin:0;padding:0;}img{border:0;display:block;}</style></head>
<body style="margin:0;padding:0;background:${outerBg};font-family:Arial,Helvetica,sans-serif;">
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background:${outerBg};">
<tr><td align="center" style="padding:40px 16px;">
<table role="presentation" width="600" cellpadding="0" cellspacing="0"
  style="max-width:600px;width:100%;background:${bodyBg};border-radius:4px;overflow:hidden;">
${headerRow}
<tr><td style="padding:40px 48px 32px;font-family:Arial,Helvetica,sans-serif;font-size:17px;line-height:1.75;color:${bodyColor};">${body}</td></tr>
<tr><td style="padding:0 40px;"><table role="presentation" width="100%" cellpadding="0" cellspacing="0"><tr><td style="height:1px;background:#e8e4e0;font-size:1px;line-height:1px;">&nbsp;</td></tr></table></td></tr>
<tr><td style="padding:20px 48px 8px;font-family:Arial,Helvetica,sans-serif;font-size:12px;color:#c8c0b8;text-align:center;">Mit Liebe gemacht von<br><img src="${base}/assets/img/howe-logo@400px.png" width="120" alt="" style="display:inline-block;width:120px;height:auto;margin-top:8px;" /></td></tr>
<tr><td style="height:24px;"></td></tr>
</table></td></tr></table>
</body></html>`;
        });

        function escHtml(s) {
            return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
        }

        /** Insert template variable at cursor position in the body textarea */
        function mailInsertVar(v) {
            const ta  = mailBodyRef.value;
            const tpl = mailCurrentTemplate.value;
            if (!ta) { tpl.body += v; return; }
            const start = ta.selectionStart ?? tpl.body.length;
            const end   = ta.selectionEnd   ?? start;
            tpl.body = tpl.body.slice(0, start) + v + tpl.body.slice(end);
            nextTick(() => {
                ta.selectionStart = ta.selectionEnd = start + v.length;
                ta.focus();
            });
        }

        function openMailMedia()  { mailShowMedia.value = true; }
        function closeMailMedia() { mailShowMedia.value = false; }
        function selectMailMedia(url, thumbUrl) {
            mailHeaderImg.value   = url;
            mailHeaderThumb.value = thumbUrl || '';
            mailShowMedia.value   = false;
        }
        function removeMailHeader() { mailHeaderImg.value = ''; mailHeaderThumb.value = ''; }

        function stopMailPoll() {
            if (mailPollTimer.value) { clearInterval(mailPollTimer.value); mailPollTimer.value = null; }
        }

        function startMailPoll() {
            stopMailPoll();
            mailPollTimer.value = setInterval(async () => {
                try {
                    const res = await api.get('/guests/mail/status');
                    mailStatus.value = res;
                    if (!res.running) { stopMailPoll(); mailSending.value = false; }
                } catch (e) { /* ignore */ }
            }, 1500);
        }

        async function sendMail() {
            const { t } = props;
            const defTpl = mailTemplates.value[mailDefaultLang.value] || {};
            if (!defTpl.subject?.trim()) { alert(t('mail_error_subject')); return; }
            if (!defTpl.body?.trim())    { alert(t('mail_error_body'));    return; }
            if (!mailRecipients.value.length) { alert(t('mail_no_recipients')); return; }
            const n = mailRecipients.value.length;
            if (!confirm(t('mail_confirm_send').replace('{n}', n))) return;

            mailSending.value = true;
            mailStatus.value  = { running: true, total: n, sent: 0, failed: 0, errors: [] };

            // Fire-and-forget; PHP closes HTTP connection then keeps sending
            api.post('/guests/mail', {
                templates:    mailTemplates.value,
                default_lang: mailDefaultLang.value,
                header_image: mailHeaderImg.value,
                filter:       mailFilter.value,
                filter_value: mailFilterVal.value,
                design:       mailDesign.value,
            }).catch(e => {
                mailSending.value = false;
                mailStatus.value  = { running: false, total: n, sent: 0, failed: n, errors: [e.message] };
            });

            await new Promise(r => setTimeout(r, 600));
            startMailPoll();
        }

        watch(() => gs.activeView, v => {
            if (v !== 'mail') { stopMailPoll(); return; }
            loadMailConfig();
            loadMailDraft();
        }, { immediate: true });

        // Sofortiges Preview-Update nur beim Gasten-Wechsel (kein Tippen)
        watch(mailPreviewIdx, () => { mailPreviewHtmlDebounced.value = mailPreviewHtml.value; });

        // Auto-save + Preview-Update 1.5 s nach letzter Änderung
        watch([mailTemplates, mailHeaderImg, mailFilter, mailFilterVal, mailDesign], scheduleDraftSave, { deep: true });

        onUnmounted(() => { stopMailPoll(); clearTimeout(_draftTimer); });

        return { gs, editDraft, saving, copyDone, deleteConfirmId,
                 filteredGuests, publicBase,
                 openEdit, startNew, cancelEdit, saveGuest, deleteGuest, groupName, copyToken, csvExport,
                 // mail
                 mailFilter, mailFilterVal, mailTemplates, mailActiveLang, mailCurrentTemplate,
                 mailDefaultLang, mailBodyRef,
                 mailHeaderImg, mailHeaderThumb, mailShowMedia, mailVarsList, mailBodyPlaceholder,
                 mailStatus, mailSending, mailPreviewIdx,
                 mailRecipients, mailPreviewGuest, mailPreviewHtml,
                 mailInsertVar, sendMail,
                 openMailMedia, closeMailMedia, selectMailMedia, removeMailHeader,
                 mailDraftStatus, mailPreviewHtmlDebounced, mailInfoOpen,
                 mailDesign, mailDesignOpen };
    },
    template: `
    <!-- ── List view ─────────────────────────────────────────────────────────── -->
    <div v-if="gs.activeView === 'list'" class="hw-guests-main">

      <!-- Toolbar -->
      <div class="hw-guests-toolbar">
        <div class="hw-guests-toolbar__left">
          <h2 class="hw-tabtitle">{{ t('nav_guests') }} <span class="badge primary">{{ filteredGuests.length }}</span></h2>
        </div>
        <div class="hw-guests-toolbar__right">
          <select v-model="gs.rsvpFilter" class="select hw-field__input hw-guests-filter">
            <option value="">{{ t('guests_rsvp_all') }}</option>
            <option value="pending">{{ t('rsvp_pending') }}</option>
            <option value="confirmed">{{ t('rsvp_confirmed') }}</option>
            <option value="declined">{{ t('rsvp_declined') }}</option>
          </select>
          <button type="button" class="outline" @click="csvExport">
            <i class="hgi-stroke hgi-download-02"></i> {{ t('guests_csv_export') }}
          </button>
          <button type="button" class="primary" @click="startNew">
            <i class="hgi-stroke hgi-plus-sign"></i> {{ t('guests_add') }}
          </button>
        </div>
      </div>

      <!-- Table -->
      <div class="hw-guest-table-wrap">
        <table class="hw-guest-table">
          <thead>
            <tr>
              <th>{{ t('guests_col_name') }}</th>
              <th>{{ t('guests_col_email') }}</th>
              <th>{{ t('guests_col_groups') }}</th>
              <th>{{ t('guests_col_persons') }}</th>
              <th>{{ t('guests_col_rsvp') }}</th>
              <th></th>
            </tr>
          </thead>
          <tbody>
            <tr v-if="filteredGuests.length === 0">
              <td colspan="6" class="hw-guest-empty">{{ t('guests_empty') }}</td>
            </tr>
            <tr v-for="guest in filteredGuests" :key="guest.id"
              class="hw-guest-row"
              @click="openEdit(guest.id)">
              <td class="hw-guest-row__name">{{ guest.name }}</td>
              <td class="hw-guest-row__email">{{ guest.email }}</td>
              <td class="hw-guest-row__groups">
                <span v-for="gid in (guest.group_ids||[])" :key="gid" class="hw-guest-tag">{{ groupName(gid) }}</span>
              </td>
              <td>{{ guest.persons }}</td>
              <td>
                <span class="hw-rsvp-badge" :class="'hw-rsvp-badge--' + (guest.rsvp||'pending')">
                  {{ t('rsvp_' + (guest.rsvp||'pending')) }}
                </span>
              </td>
              <td class="hw-guest-row__actions" @click.stop>
                <button class="hw-album-action" :title="t('guests_edit')" @click="openEdit(guest.id)">
                  <i class="hgi-stroke hgi-pencil-edit-01"></i>
                </button>
                <button class="hw-album-action hw-album-action--delete"
                  :class="{'hw-album-action--confirm': deleteConfirmId === guest.id}"
                  :title="deleteConfirmId === guest.id ? t('confirm_delete') : t('delete')"
                  @click="deleteGuest(guest.id)">
                  <i class="hgi-stroke"
                    :class="deleteConfirmId === guest.id ? 'hgi-checkmark-circle-02' : 'hgi-delete-02'"></i>
                </button>
              </td>
            </tr>
          </tbody>
        </table>
      </div>

    </div>

    <!-- ── New guest view (full-page, like a tab) ──────────────────────────── -->
    <div v-else-if="gs.activeView === 'new'" class="hw-guests-main">
      <div class="hw-guest-table-wrap">
        <div class="hw-tab-content">
          <div class="hw-sections-header">
            <button type="button" class="button primitive hw-back-btn" @click="cancelEdit">
              <i class="hgi-stroke hgi-arrow-left-01"></i> <h2 class="hw-tabtitle">{{ t('guests_add') }}</h2>
            </button>
          </div>
          <div class="hw-guest-form">
            <div class="hw-guest-form__grid">
              <div class="hw-guest-form__field">
                <label>{{ t('guests_field_name') }}</label>
                <input type="text" v-model="editDraft.name" class="input" />
              </div>
              <div class="hw-guest-form__field">
                <label>{{ t('guests_field_greeting') }}</label>
                <input type="text" v-model="editDraft.greeting" class="input"
                  placeholder="z.B. Liebe Familie Müller oder Liebe Saskia, lieber Stefan" />
              </div>
              <div class="hw-guest-form__field">
                <label>{{ t('guests_field_email') }}</label>
                <input type="email" v-model="editDraft.email" class="input" />
              </div>
              <div class="hw-guest-form__field">
                <label>{{ t('guests_field_language') }}</label>
                <select v-model="editDraft.language" class="select">
                  <option v-for="lang in gs.langs" :key="lang.code" :value="lang.code">{{ lang.label }}</option>
                </select>
              </div>
              <div class="hw-guest-form__field">
                <label>{{ t('guests_field_persons') }}</label>
                <input type="number" v-model.number="editDraft.persons" min="1" max="30" class="input hw-guest-num-input" />
              </div>
              <div class="hw-guest-form__field">
                <label>{{ t('guests_field_rsvp') }}</label>
                <div class="hw-rsvp-options">
                  <label class="hw-rsvp-option hw-rsvp-option--pending"
                    :class="{'hw-rsvp-option--active': editDraft.rsvp === 'pending'}">
                    <input type="radio" class="radio" name="guest-rsvp" value="pending" v-model="editDraft.rsvp" />
                    {{ t('rsvp_pending') }}
                  </label>
                  <label class="hw-rsvp-option hw-rsvp-option--confirmed"
                    :class="{'hw-rsvp-option--active': editDraft.rsvp === 'confirmed'}">
                    <input type="radio" class="radio" name="guest-rsvp" value="confirmed" v-model="editDraft.rsvp" />
                    {{ t('rsvp_confirmed') }}
                  </label>
                  <label class="hw-rsvp-option hw-rsvp-option--declined"
                    :class="{'hw-rsvp-option--active': editDraft.rsvp === 'declined'}">
                    <input type="radio" class="radio" name="guest-rsvp" value="declined" v-model="editDraft.rsvp" />
                    {{ t('rsvp_declined') }}
                  </label>
                </div>
              </div>
              <div class="hw-guest-form__field hw-guest-form__field--full">
                <label>{{ t('guests_field_groups') }}</label>
                <div class="hw-guest-groups">
                  <span v-if="gs.groups.length === 0" class="hw-guest-groups__empty">–</span>
                  <label v-for="group in gs.groups" :key="group.id" class="hw-guest-group-check">
                    <input type="checkbox" class="checkbox" :value="group.id" v-model="editDraft.group_ids" />
                    {{ group.name }}
                  </label>
                </div>
              </div>
            </div>
            <div v-if="gs.mealOptions.length > 0 && editDraft.meal_persons.length > 0" class="hw-guest-meals">
              <div class="hw-guest-meals__title">{{ t('guests_field_meal') }}</div>
              <div v-for="(person, idx) in editDraft.meal_persons" :key="idx" class="hw-guest-meal-row">
                <input type="text" v-model="person.name"
                  :placeholder="t('guests_person_name') + ' ' + (idx + 1)"
                  class="input hw-guest-meal-row__name" />
                <select v-model="person.meal" class="select hw-guest-meal-row__meal">
                  <option value="">{{ t('guests_meal_none') }}</option>
                  <option v-for="opt in gs.mealOptions" :key="opt" :value="opt">{{ opt }}</option>
                </select>
              </div>
            </div>
            <div class="hw-guest-form__field hw-guest-form__field--full hw-guest-form__note">
              <label>{{ t('guests_field_message') }}</label>
              <textarea v-model="editDraft.message" class="input" rows="2"></textarea>
            </div>
            <div class="hw-guest-form__actions">
              <button type="button" class="primary" @click="saveGuest" :disabled="saving">
                {{ saving ? t('draft_saving') : t('save') }}
              </button>
              <button class="button outline" type="button" @click="cancelEdit">{{ t('cancel') }}</button>
            </div>
          </div>
        </div>
      </div>
    </div>

    <!-- ── Edit guest view (full-page, like new) ──────────────────────────── -->
    <div v-else-if="gs.activeView === 'edit'" class="hw-guests-main">
      <div class="hw-guest-table-wrap">
        <div class="hw-tab-content">
          <div class="hw-sections-header">
            <button type="button" class="button primitive hw-back-btn" @click="cancelEdit">
              <i class="hgi-stroke hgi-arrow-left-01"></i> <h2 class="hw-tabtitle">{{ t('guests_edit') }}: {{ editDraft.name }}</h2>
            </button>
          </div>
          <div class="hw-guest-form">
            <div class="hw-guest-form__grid">
              <div class="hw-guest-form__field">
                <label>{{ t('guests_field_name') }}</label>
                <input type="text" v-model="editDraft.name" class="input" />
              </div>
              <div class="hw-guest-form__field">
                <label>{{ t('guests_field_greeting') }}</label>
                <input type="text" v-model="editDraft.greeting" class="input"
                  placeholder="z.B. Liebe Familie Müller oder Liebe Saskia, lieber Stefan" />
              </div>
              <div class="hw-guest-form__field">
                <label>{{ t('guests_field_email') }}</label>
                <input type="email" v-model="editDraft.email" class="input" />
              </div>
              <div class="hw-guest-form__field">
                <label>{{ t('guests_field_language') }}</label>
                <select v-model="editDraft.language" class="select">
                  <option v-for="lang in gs.langs" :key="lang.code" :value="lang.code">{{ lang.label }}</option>
                </select>
              </div>
              <div class="hw-guest-form__field">
                <label>{{ t('guests_field_persons') }}</label>
                <input type="number" v-model.number="editDraft.persons" min="1" max="30" class="input hw-guest-num-input" />
              </div>
              <div class="hw-guest-form__field">
                <label>{{ t('guests_field_rsvp') }}</label>
                <div class="hw-rsvp-options">
                  <label class="hw-rsvp-option hw-rsvp-option--pending"
                    :class="{'hw-rsvp-option--active': editDraft.rsvp === 'pending'}">
                    <input type="radio" class="radio" name="guest-rsvp" value="pending" v-model="editDraft.rsvp" />
                    {{ t('rsvp_pending') }}
                  </label>
                  <label class="hw-rsvp-option hw-rsvp-option--confirmed"
                    :class="{'hw-rsvp-option--active': editDraft.rsvp === 'confirmed'}">
                    <input type="radio" class="radio" name="guest-rsvp" value="confirmed" v-model="editDraft.rsvp" />
                    {{ t('rsvp_confirmed') }}
                  </label>
                  <label class="hw-rsvp-option hw-rsvp-option--declined"
                    :class="{'hw-rsvp-option--active': editDraft.rsvp === 'declined'}">
                    <input type="radio" class="radio" name="guest-rsvp" value="declined" v-model="editDraft.rsvp" />
                    {{ t('rsvp_declined') }}
                  </label>
                </div>
              </div>
              <div class="hw-guest-form__field hw-guest-form__field--full">
                <label>{{ t('guests_field_groups') }}</label>
                <div class="hw-guest-groups">
                  <span v-if="gs.groups.length === 0" class="hw-guest-groups__empty">–</span>
                  <label v-for="group in gs.groups" :key="group.id" class="hw-guest-group-check">
                    <input type="checkbox" class="checkbox" :value="group.id" v-model="editDraft.group_ids" />
                    {{ group.name }}
                  </label>
                </div>
              </div>
            </div>
            <div v-if="gs.mealOptions.length > 0 && editDraft.meal_persons.length > 0" class="hw-guest-meals">
              <div class="hw-guest-meals__title">{{ t('guests_field_meal') }}</div>
              <div v-for="(person, idx) in editDraft.meal_persons" :key="idx" class="hw-guest-meal-row">
                <input type="text" v-model="person.name"
                  :placeholder="t('guests_person_name') + ' ' + (idx + 1)"
                  class="input hw-guest-meal-row__name" />
                <select v-model="person.meal" class="select hw-guest-meal-row__meal">
                  <option value="">{{ t('guests_meal_none') }}</option>
                  <option v-for="opt in gs.mealOptions" :key="opt" :value="opt">{{ opt }}</option>
                </select>
              </div>
            </div>
            <div class="hw-guest-form__field hw-guest-form__field--full hw-guest-form__note">
              <label>{{ t('guests_field_message') }}</label>
              <textarea v-model="editDraft.message" class="input" rows="2"></textarea>
            </div>
            <div v-if="editDraft.token" class="hw-guest-token">
              <span class="hw-guest-token__label">{{ t('guests_token_url') }}</span>
              <code class="hw-guest-token__url">{{ publicBase }}/?g={{ editDraft.token }}</code>
              <button type="button" class="" :title="copyDone ? t('guests_token_copied') : t('guests_token_copy')"
                @click="copyToken(editDraft.token)">
                <i class="hgi-stroke" :class="copyDone ? 'hgi-checkmark-circle-02' : 'hgi-copy-01'"></i>
              </button>
            </div>
            <div class="hw-guest-form__actions">
              <button type="button" class="primary" @click="saveGuest" :disabled="saving">
                {{ saving ? t('draft_saving') : t('save') }}
              </button>
              <button type="button" class="button outline" @click="cancelEdit">{{ t('cancel') }}</button>
            </div>
          </div>
        </div>
      </div>
    </div>

    <!-- ── Mail view ─────────────────────────────────────────────────────── -->
    <div v-else-if="gs.activeView === 'mail'" class="hw-guests-main hw-mail-wrap">

      <!-- Two-column layout -->
      <div class="hw-mail-columns">

        <!-- ── Left: compose ──────────────────────────────────────────────── -->
        <div class="hw-mail-compose">

          <!-- Recipients -->
          <div class="hw-mail-section">
            <label class="hw-mail-section__label">{{ t('mail_recipients') }}</label>
            <div class="hw-mail-radio-group">
              <label class="hw-mail-radio">
                <input type="radio" class="radio" v-model="mailFilter" value="all" @change="mailFilterVal = ''" />
                <span>{{ t('mail_recipients_all') }}</span>
              </label>
              <label class="hw-mail-radio">
                <input type="radio" class="radio" v-model="mailFilter" value="group" />
                <span>{{ t('mail_recipients_group') }}</span>
              </label>
              <label class="hw-mail-radio">
                <input type="radio" class="radio" v-model="mailFilter" value="rsvp" />
                <span>{{ t('mail_recipients_rsvp') }}</span>
              </label>
            </div>
            <select v-if="mailFilter === 'group'" v-model="mailFilterVal" class="select hw-mail-filter-select">
              <option value="">–</option>
              <option v-for="g in gs.groups" :key="g.id" :value="g.id">{{ g.name }}</option>
            </select>
            <select v-if="mailFilter === 'rsvp'" v-model="mailFilterVal" class="select hw-mail-filter-select">
              <option value="">–</option>
              <option value="pending">{{ t('rsvp_pending') }}</option>
              <option value="confirmed">{{ t('rsvp_confirmed') }}</option>
              <option value="declined">{{ t('rsvp_declined') }}</option>
            </select>
            <div class="hw-mail-count">
              <span class="badge" :class="mailRecipients.length ? 'primary' : ''">{{ mailRecipients.length }}</span>
              {{ t('mail_recipient_count') }}
            </div>
          </div>

          <!-- Header image -->
          <div class="hw-mail-section">
            <label class="hw-mail-section__label">{{ t('mail_header_image') }}</label>
            <div class="hw-mail-header-img">
              <div v-if="mailHeaderImg" class="hw-mail-header-img__preview">
                <img :src="publicBase + '/' + (mailHeaderThumb || mailHeaderImg)" alt="" class="hw-mail-header-img__thumb" />
                <button type="button" class="hw-mail-header-img__remove" @click="removeMailHeader" :title="t('media_remove')">
                  <i class="hgi-stroke hgi-cancel-01"></i>
                </button>
              </div>
              <button type="button" class="outline hw-mail-header-img__btn" @click="openMailMedia">
                <i class="hgi-stroke hgi-image-add-01"></i>
                {{ mailHeaderImg ? t('media_replace') : t('mail_header_choose') }}
              </button>
            </div>
          </div>

          <!-- Language bar (only when more than one language is active) -->
          <div v-if="gs.langs.length > 1" class="hw-section-editor__langbar hw-mail-langbar">
            <button v-for="lang in gs.langs" :key="lang.code"
              type="button"
              class="hw-langbar__btn"
              :class="{ 'hw-langbar__btn--active': mailActiveLang === lang.code }"
              @click="mailActiveLang = lang.code">
              {{ lang.code.toUpperCase() }}
              <span v-if="!(mailTemplates[lang.code]?.subject?.trim() || mailTemplates[lang.code]?.body?.trim())"
                class="hw-mail-lang-dot"></span>
            </button>
            <button type="button" class="button primitive is-small hw-mail-lang-info-btn"
              :class="{ 'hw-mail-lang-info-btn--open': mailInfoOpen }"
              :title="t('mail_info_toggle')"
              @click="mailInfoOpen = !mailInfoOpen">
              <i class="hgi-stroke hgi-information-circle"></i>
            </button>
          </div>
          <div v-if="gs.langs.length > 1 && mailInfoOpen" class="hw-mail-lang-info">
            {{ t('mail_info_text') }}
          </div>

          <!-- Subject -->
          <div class="hw-mail-section">
            <label class="hw-mail-section__label" for="hw-mail-subject">{{ t('mail_subject') }}</label>
            <input id="hw-mail-subject" type="text" v-model="mailCurrentTemplate.subject" class="input" />
          </div>

          <!-- Body -->
          <div class="hw-mail-section hw-mail-section--grow">
            <label class="hw-mail-section__label">{{ t('mail_body') }}</label>
            <textarea ref="mailBodyRef" v-model="mailCurrentTemplate.body" class="input hw-mail-textarea" rows="12"
              :placeholder="mailBodyPlaceholder"></textarea>
            <div class="hw-mail-vars">
              <span class="hw-mail-vars__title">{{ t('mail_vars_title') }}:</span>
              <button v-for="v in mailVarsList" :key="v" type="button"
                class="hw-mail-var-btn" @click="mailInsertVar(v)">{{ v }}</button>
            </div>
          </div>


          <!-- Draft status -->
          <div v-if="mailDraftStatus" class="hw-mail-draft-status">
            <i v-if="mailDraftStatus === 'saving'" class="hgi-stroke hgi-loading-03 hw-spin"></i>
            <i v-else class="hgi-stroke hgi-checkmark-circle-02"></i>
            {{ mailDraftStatus === 'saving' ? t('mail_draft_saving') : t('mail_draft_saved') }}
          </div>

          <!-- Send button -->
          <div class="hw-mail-actions">
            <button type="button" class="primary"
              :disabled="mailSending || !mailRecipients.length"
              @click="sendMail">
              <i class="hgi-stroke hgi-mail-send-01"></i>
              {{ mailSending ? t('mail_sending') : t('mail_send') }}
            </button>
          </div>

          <!-- Progress -->
          <div v-if="mailStatus" class="hw-mail-progress">
            <div class="hw-mail-progress__bar-wrap">
              <div class="hw-mail-progress__bar"
                :style="{ width: mailStatus.total > 0 ? (((mailStatus.sent + mailStatus.failed) / mailStatus.total) * 100) + '%' : '0%' }">
              </div>
            </div>
            <div class="hw-mail-progress__info">
              <span v-if="mailStatus.running">
                <i class="hgi-stroke hgi-loading-03 hw-spin"></i>
                {{ t('mail_sent_of').replace('{sent}', mailStatus.sent).replace('{total}', mailStatus.total) }}
              </span>
              <span v-else class="hw-mail-progress__done">
                <i class="hgi-stroke hgi-checkmark-circle-02"></i>
                {{ t('mail_done') }} —
                {{ t('mail_sent_of').replace('{sent}', mailStatus.sent).replace('{total}', mailStatus.total) }}
                <span v-if="mailStatus.failed > 0" class="hw-mail-progress__failed">
                  · {{ t('mail_failed').replace('{n}', mailStatus.failed) }}
                </span>
              </span>
            </div>
            <div v-if="mailStatus.errors && mailStatus.errors.length" class="hw-mail-errors">
              <div class="hw-mail-errors__title">{{ t('mail_errors_title') }}</div>
              <ul class="hw-mail-errors__list">
                <li v-for="(err, i) in mailStatus.errors" :key="i">{{ err }}</li>
              </ul>
            </div>
          </div>
        </div>

        <!-- ── Right: preview ─────────────────────────────────────────────── -->
        <div class="flex-column gap-s">        

          <!-- Design -->
          <div class="hw-mail-section gap-0">
            <button type="button" class="button outline is-mini" @click="mailDesignOpen = !mailDesignOpen">
              <i class="hgi-stroke hgi-paint-bucket"></i>
              <span>{{ t('mail_design') }}</span>
              <i class="hgi-stroke" :class="mailDesignOpen ? 'hgi-arrow-up-01' : 'hgi-arrow-down-01'" style="margin-left:auto;"></i>
            </button>
            <div v-if="mailDesignOpen" class="hw-mail-design-panel">
              <div class="hw-mail-design-row">
                <label>{{ t('mail_design_bg') }}</label>
                <input type="color" v-model="mailDesign.bg" />
              </div>
              <div class="hw-mail-design-row">
                <label>{{ t('mail_design_body_bg') }}</label>
                <input type="color" v-model="mailDesign.body_bg" />
              </div>
              <div class="hw-mail-design-row">
                <label>{{ t('mail_design_body_color') }}</label>
                <input type="color" v-model="mailDesign.body_color" />
              </div>
              <div class="hw-mail-design-row">
                <label>{{ t('mail_design_btn_bg') }}</label>
                <input type="color" v-model="mailDesign.btn_bg" />
              </div>
              <div class="hw-mail-design-row">
                <label>{{ t('mail_design_btn_color') }}</label>
                <input type="color" v-model="mailDesign.btn_color" />
              </div>
            </div>
          </div>


        <div class="hw-mail-preview">
          <div class="hw-mail-preview__header">
            <span class="hw-mail-preview__title">{{ t('mail_preview') }}</span>
            <span v-if="mailRecipients.length > 1" class="hw-mail-preview__nav">
              <button type="button" class="hw-mail-preview__nav-btn"
                :disabled="mailPreviewIdx <= 0"
                @click="mailPreviewIdx = Math.max(0, mailPreviewIdx - 1)">
                <i class="hgi-stroke hgi-arrow-left-02"></i>
              </button>
              <span>{{ mailPreviewGuest ? mailPreviewGuest.name : '–' }}</span>
              <button type="button" class="hw-mail-preview__nav-btn"
                :disabled="mailPreviewIdx >= mailRecipients.length - 1"
                @click="mailPreviewIdx = Math.min(mailRecipients.length - 1, mailPreviewIdx + 1)">
                <i class="hgi-stroke hgi-arrow-right-02"></i>
              </button>
            </span>
            <span v-else-if="mailPreviewGuest" class="hw-mail-preview__guest-name">
              {{ mailPreviewGuest.name }}
            </span>
          </div>
          <div class="hw-mail-preview__body">
            <iframe v-if="mailPreviewHtmlDebounced"
              :srcdoc="mailPreviewHtmlDebounced"
              class="hw-mail-preview__iframe"
              frameborder="0"
              sandbox="allow-same-origin"></iframe>
            <div v-else class="hw-mail-preview__empty">{{ t('mail_preview_none') }}</div>
          </div>
        </div>
        </div>
      </div><!-- /.hw-mail-columns -->

      <!-- Header image media modal -->
      <media-modal v-if="mailShowMedia"
        :t="t"
        :onSelect="selectMailMedia"
        :onClose="closeMailMedia"
        :currentUrl="mailHeaderImg"
        folder="mailheader" />

    </div>
    `,
};

// ── HwApp ─────────────────────────────────────────────────────────────────────
const HwApp = {
    components: { DashboardTab, DashboardMain, SectionsTab, DesignTab, GalleryTab, GalleryMain, LanguagesTab, LanguagesMain, GuestsTab, GuestsMain },
    setup() {
        const { uiLang, t } = useLang();
        const activeTab = ref(sessionStorage.getItem('hw_active_tab') || 'dashboard');
        const config    = reactive({ addons: { multilang: false } });

        const tabs = computed(() => {
            const base = [
                { id: 'dashboard', icon: '<i class="hgi-stroke hgi-home-01"></i>',     label: t('nav_dashboard') },
                { id: 'sections',  icon: '<i class="hgi-stroke hgi-pause"></i>',       label: t('nav_sections'), btnClass: 'hw-tabnav__btn--sections' },
                { id: 'design',    icon: '<i class="hgi-stroke hgi-paint-board"></i>', label: t('nav_design') },
                { id: 'gallery',   icon: '<i class="hgi-stroke hgi-image-01"></i>',    label: t('nav_gallery') },
                { id: 'guests',    icon: '<i class="hgi-stroke hgi-user-group"></i>',  label: t('nav_guests') },
            ];
            if (config.addons?.multilang) base.push({ id: 'languages', icon: '<i class="hgi-stroke hgi-language-circle"></i>', label: t('nav_languages') });
            return base;
        });

        const showPreview = computed(() => ['sections','design'].includes(activeTab.value));

        onMounted(async () => {
            try { Object.assign(config, await useApi().get('/settings')); } catch(e) {}
            try { const d = await useApi().get('/design'); if (d?.preset) _activeDesign.preset = d.preset; } catch(e) {}
        });

        async function logout() {
            await fetch(window.location.origin + '/api/index.php/auth/logout', { method: 'POST', credentials: 'same-origin' });
            window.location.reload();
        }

        function onTabSwitch(tab) {
            if (_editorDraft.isDirty) {
                const msg = LANGS[uiLang.value]?.editor_unsaved_confirm
                    || (uiLang.value === 'en' ? 'You have unsaved changes. Switch anyway?' : 'Du hast ungespeicherte Änderungen. Trotzdem wechseln?');
                if (!confirm(msg)) return;
                _editorDraft._discardFn?.();
                _editorDraft.isDirty = false;
            }
            activeTab.value = tab;
            sessionStorage.setItem('hw_active_tab', tab);
        }
        return { uiLang, t, activeTab, tabs, config, logout, showPreview, onTabSwitch };
    },
    template: `
    <div class="hw-editor">
      <hw-panel :active-tab="activeTab" :t="t" :config="config" :tabs="tabs" :logout="logout"
        @update:activeTab="onTabSwitch($event)">
        <template #content>
          <dashboard-tab v-if="activeTab==='dashboard'" :t="t" />
          <sections-tab  v-else-if="activeTab==='sections'" :t="t" :config="config" />
          <design-tab    v-else-if="activeTab==='design'"   :t="t" />
          <gallery-tab    v-else-if="activeTab==='gallery'"   :t="t" />
          <guests-tab     v-else-if="activeTab==='guests'"    :t="t" />
          <languages-tab  v-else-if="activeTab==='languages'" :t="t" />
          <div v-else class="hw-tab-content">
            <h2 class="hw-tabtitle">{{ activeTab }}</h2>
            <p class="hw-tabdescription">Coming soon.</p>
          </div>
        </template>
      </hw-panel>

      <div class="hw-main theme-always-light">
        <div v-if="showPreview" class="hw-main__content hw-main__content--preview">
          <div class="hw-preview">
            <iframe id="hw-preview-frame" src="/?editor_preview=1" class="hw-preview__frame"></iframe>
          </div>
        </div>
        <div v-else class="hw-main__content">
          <dashboard-main  v-if="activeTab==='dashboard'"      :t="t" :ui-lang="uiLang" @update:uiLang="uiLang=$event" />
          <gallery-main    v-else-if="activeTab==='gallery'"    :t="t" />
          <guests-main     v-else-if="activeTab==='guests'"     :t="t" :ui-lang="uiLang" />
          <languages-main  v-else-if="activeTab==='languages'"  :t="t" />
          <div v-else class="hw-main__placeholder">
            <p style="color:var(--dark-50)">{{ activeTab }} – coming soon</p>
          </div>
        </div>
      </div>
    </div>
    `,
};

// ── Mount ─────────────────────────────────────────────────────────────────────
const app = createApp({});
app.component('hw-app',         HwApp);
app.component('hw-panel',       HwPanel);
app.component('dashboard-tab',  DashboardTab);
app.component('dashboard-main', DashboardMain);
app.component('sections-tab',   SectionsTab);
app.component('design-tab',     DesignTab);
app.component('section-editor', SectionEditor);
app.component('form-field',     FormField);
app.component('field-group',    FieldGroup);
app.component('collapsible-repeater-group', CollapsibleRepeaterGroup);
app.component('repeater-field', RepeaterField);
app.component('media-modal',    MediaModal);
app.component('emoji-picker-lib', window.EmojiPicker);   // CDN-Library
app.component('emoji-picker',     EmojiPickerWrapper);    // lokaler Wrapper
app.component('gallery-tab',    GalleryTab);
app.component('gallery-main',   GalleryMain);
app.component('guests-tab',     GuestsTab);
app.component('guests-main',    GuestsMain);
app.component('languages-tab',  LanguagesTab);
app.component('languages-main', LanguagesMain);
app.mount('#app');
