'use strict';

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

// ── Bootstrap data ─────────────────────────────────────────────────────────────
const D      = window.HW_DATA || {};
const BASE   = D.base_url || '';
const CONFIG = D.config || {};

let _csrfToken = '';
async function fetchCsrfToken() {
    if (_csrfToken) return _csrfToken;
    try {
        const r = await fetch(BASE + '/api/csrf');
        if (r.ok) _csrfToken = (await r.json()).data?.token || '';
    } catch (_) {}
    return _csrfToken;
}

// ── Utilities ──────────────────────────────────────────────────────────────────

function getText(data, key, lang) {
    return (data?.[lang]?.[key] ?? data?.de?.[key]) || '';
}

function getImg(url) {
    if (!url) return '';
    if (/^https?:\/\//.test(url)) return url;
    return BASE + '/' + url;
}

/**
 * Returns an attribute object for v-bind on <img>.
 * Detects --size-m.(avif|webp) URLs and automatically generates srcset + sizes.
 *
 * Supports two layouts:
 *  - Same-dir (uploads): all 3 sizes in the same directory
 *    e.g. "uploads/user/abc--size-m.avif"
 *  - Thumbs-dir (samples): m+l in thumbs/, xl in parent dir
 *    e.g. "assets/img/samples/thumbs/name--size-m.avif"
 *
 * @param {string} url   - stored image URL
 * @param {string} sizes - CSS sizes attribute
 */
function imgAttrs(url, sizes = '(max-width:768px) 100vw, 900px') {
    if (!url) return { src: '' };
    const full = getImg(url);
    const m = full.match(/^(.+)(--size-m)(\.avif|\.webp)$/);
    if (m) {
        const base = m[1]; // path prefix before "--size-m"
        const ext  = m[3];
        // If m+l live in a thumbs/ subdir, xl lives one level up
        const xlBase = base.includes('/thumbs/') ? base.replace('/thumbs/', '/') : base;
        const srcset = [
            `${base}--size-m${ext} 920w`,
            `${base}--size-l${ext} 1440w`,
            `${xlBase}--size-xl${ext} 2700w`,
        ].join(', ');
        return { src: full, srcset, sizes };
    }
    return { src: full };
}

/**
 * Returns the best URL for CSS background-image use (--size-xl variant).
 * Handles both same-dir and thumbs-dir layouts.
 */
function imgBg(url) {
    if (!url) return '';
    const full = getImg(url);
    const m = full.match(/^(.+)(--size-m)(\.avif|\.webp)$/);
    if (m) {
        const base   = m[1];
        const ext    = m[3];
        const xlBase = base.includes('/thumbs/') ? base.replace('/thumbs/', '/') : base;
        return xlBase + '--size-xl' + ext;
    }
    return full;
}

/** Convert #rrggbb hex + 0-100 opacity to rgba() string. */
function hexToRgba(hex, opacity) {
    const h = String(hex || '#000000').replace('#', '').padEnd(6, '0');
    const r = parseInt(h.slice(0, 2), 16) || 0;
    const g = parseInt(h.slice(2, 4), 16) || 0;
    const b = parseInt(h.slice(4, 6), 16) || 0;
    return `rgba(${r},${g},${b},${Math.min(1, Math.max(0, opacity / 100))})`;
}

/** Convert any color (hex or CSS variable) + 0-100 opacity to a background string. */
function colorToRgba(color, opacity) {
    if (!color || color.startsWith('#')) return hexToRgba(color || '#000000', opacity);
    return `color-mix(in srgb, ${color} ${opacity}%, transparent)`;
}

/** Returns #000000 or #ffffff for best contrast against a hex background. */
function contrastColor(hex) {
    if (!hex || !hex.startsWith('#')) return '#ffffff';
    const r = parseInt(hex.slice(1, 3), 16) || 0;
    const g = parseInt(hex.slice(3, 5), 16) || 0;
    const b = parseInt(hex.slice(5, 7), 16) || 0;
    return (0.299 * r + 0.587 * g + 0.114 * b) > 128 ? '#000000' : '#ffffff';
}

/** Replace {{variable}} and {{variable | fallback}} placeholders with guest data.
 *  Fallback is used when D.guest is absent or the field is empty. */
function resolveVars(text) {
    if (!text) return text;
    const g   = D.guest;
    const cfg = D.config || {};
    const tok = BASE + '/?g=' + (g?.token || '');
    const vars = {
        'hochzeitsdatum': cfg.wedding_date ? cfg.wedding_date.split('-').reverse().join('.') : '',
        'wedding_date':   cfg.wedding_date ? cfg.wedding_date.split('-').reverse().join('.') : '',
        ...(g ? {
            'name':        g.name     || '',
            'begrüssung':  g.greeting || '',
            'begrussung':  g.greeting || '',
            'greeting':    g.greeting || '',
            'websitelink': tok,
            'website_link':tok,
            'rsvp_link':   tok + '&lang=' + (D.lang || 'de'),
        } : {}),
    };
    return text.replace(
        /\{\{([\w\u00C0-\u017E]+)(?:\s*\|\s*([^}]*))?\}\}/gi,
        (_, varName, fallback) => {
            const val = vars[varName.toLowerCase()];
            if (val !== undefined && val !== '') return val;
            return fallback !== undefined ? fallback.trim() : '';
        }
    );
}

/** Convert plain text (with newlines) to safe HTML paragraphs. Allows basic inline tags. */
function renderBody(text) {
    if (!text) return '';
    let s = text
        .replace(/&/g, '&amp;')
        .replace(/</g, '&lt;')
        .replace(/>/g, '&gt;');
    // Unescape safe inline tags
    s = s.replace(/&lt;(\/?(b|i|em|strong|u|s|span)\s*\/?)&gt;/gi, '<$1>');
    s = s.replace(/&lt;br\s*\/?\s*&gt;/gi, '<br>');
    s = s.replace(/&lt;a\s+href="([^"]*)"\s*&gt;/gi, (_, href) =>
        /^(https?:|mailto:|tel:|\/)/i.test(href)
            ? `<a href="${href}" target="_blank" rel="noopener noreferrer">`
            : ''
    );
    s = s.replace(/&lt;\/a&gt;/gi, '</a>');
    return s
        .split(/\n{2,}/)
        .map(p => `<p>${p.replace(/\n/g, '<br>')}</p>`)
        .join('');
}

// text size value → text-* CSS class
const TEXT_SIZE_CLASS = { s: 'text-s', m: 'text-m', l: 'text-l', xl: 'text-xl', '2xl': 'text-2xl', '3xl': 'text-3xl', '4xl': 'text-4xl' };
// photo_border_radius value → CSS border-radius value
// 'full' maps to 50% (circle) rather than var(--radius-full) which is a pill shape
const RADIUS_TO_CSS = { s: 'var(--radius-s)', m: 'var(--radius-m)', l: 'var(--radius-l)', xl: 'var(--radius-xl)', full: '50%' };
// spacing size value → padding-* / gap-* / margin-* classes all use the same scale: `padding-${val}`, `gap-${val}` etc.

// headline_align → CSS class
const ALIGN_CLASS = {
    left:    'text-left',
    center:  'text-center',
    right:   'text-right',
    justify: 'text-justify',
};

// Shared options injected into every section component
const sectionBase = {
    props: ['section', 'lang'],
    computed: {
        d() { return this.section?.data || {}; },
    },
    methods: {
        t(key)               { return resolveVars(getText(this.d, key, this.lang)); },
        img(url)             { return getImg(url); },
        imgAttrs(url, sizes) { return imgAttrs(url, sizes); },
        imgBg(url)           { return imgBg(url); },
        headlineSize(s)      { return TEXT_SIZE_CLASS[s] || ''; },
        align(a)             { return ALIGN_CLASS[a]       || ''; },
        bodySize(s)          { return TEXT_SIZE_CLASS[s]   || 'text-m'; },
        colorStyle(c)        { return c ? { color: c } : {}; },
        body(key)            { return renderBody(this.t(key)); },
    },
};

// ══════════════════════════════════════════════════════════════════════════════
// SECTION COMPONENTS
// ══════════════════════════════════════════════════════════════════════════════

// ── hero-header — Container mit 50:50-Split: Bild | Content ────────────────────
const HwHeroHeader = {
    mixins: [sectionBase],
    computed: {
        splitClass() {
            return `hw-hero-split--img-${this.d.image_side || 'left'}`;
        },
        splitStyle() {
            const mh = parseInt(this.d.min_height, 10);
            return mh > 0 ? { minHeight: mh + 'dvh' } : {};
        },
        sectionStyle() {
            return this.d.background_image
                ? { backgroundImage: `url(${this.imgBg(this.d.background_image)})`, backgroundSize: 'cover', backgroundPosition: '50% 50%' }
                : {};
        },
        sectionPadClass() {
            return this.d.section_padding ? `padding-${this.d.section_padding}` : '';
        },
        containerStyle() {
            const s = {};
            if (this.d.container_color) s.backgroundColor = this.d.container_color;
            const bw = parseInt(this.d.border_width || 0, 10);
            if (bw > 0) s.border = `${bw}px solid ${this.d.border_color || '#000000'}`;
            return s;
        },

        textPadClass() {
            const pad = this.d.text_padding || this.d.padding;
            return pad ? `padding-${pad}` : '';
        },
        contentStyle() {
            const valignMap = { top: 'flex-start', middle: 'center', bottom: 'flex-end' };
            const halignMap = { left: 'flex-start', center: 'center', right: 'flex-end' };
            const s = {};
            s.justifyContent = valignMap[this.d.content_valign] || 'center';
            s.alignItems     = halignMap[this.d.content_halign] || 'flex-start';
            s.textAlign      = this.d.content_halign || 'left';
            if (this.d.content_bg_color) s.backgroundColor = this.d.content_bg_color;
            return s;
        },
        objPos()   { return this.d.image_position || '50% 50%'; },
        bodyText() { return this.body('body'); },
    },
    template: `
    <section class="hw-section hw-section--hero-header" :class="sectionPadClass" :style="sectionStyle">
      <div class="container-m" :style="containerStyle">
        <div class="hw-hero-split grid-2 grid-1--on-m" :class="splitClass" :style="splitStyle">
          <div class="hw-hero-split__media">
            <img v-if="d.image"
              v-bind="imgAttrs(d.image, '(max-width:768px) 100vw, 50vw')"
              :style="{ objectPosition: objPos }"
              class="hw-hero-split__img hw-abs-fill"
              alt=""
              loading="eager"
            >
          </div>
          <div class="hw-hero-split__content" :class="textPadClass" :style="contentStyle">
            <h2 v-if="t('headline')"
              class="hw-hero__headline"
              :class="headlineSize(d.headline_size)"
              :style="colorStyle(d.headline_color)"
            >{{ t('headline') }}</h2>
            <p v-if="t('headline2')"
              class="hw-hero__subline"
              :class="headlineSize(d.headline2_size)"
              :style="colorStyle(d.headline2_color)"
            >{{ t('headline2') }}</p>
            <div v-if="bodyText"
              class="hw-hero__body"
              :class="[align(d.body_align), d.body_size ? bodySize(d.body_size) : '']"
              :style="colorStyle(d.body_color)"
              v-html="bodyText"
            ></div>
          </div>
        </div>
      </div>
    </section>`,
};

// ── big-header — Vollbild mit Overlay und frei positioniertem Content ───────────
const HwBigHeader = {
    mixins: [sectionBase],
    computed: {
        sectionStyle() {
            const mh = parseInt(this.d.min_height, 10);
            return mh > 0 ? { minHeight: mh + 'dvh' } : {};
        },
        contentClass() {
            const pos = this.d.text_align || 'center';
            const pad = this.d.padding ? ` padding-${this.d.padding}` : '';
            return `hw-hero__content hw-hero__content--${pos}${pad}`;
        },
        overlayStyle() {
            const op = parseInt(this.d.overlay_opacity || 0, 10);
            return { background: colorToRgba(this.d.overlay_color || '#000000', op) };
        },
        objPos() { return this.d.background_position || '50% 50%'; },
    },
    template: `
    <section class="hw-section hw-section--big-header" :style="sectionStyle">
      <img v-if="d.background_image"
        v-bind="imgAttrs(d.background_image, '100vw')"
        :style="{ objectPosition: objPos }"
        class="hw-hero__img hw-abs-fill"
        alt=""
        loading="eager"
      >
      <div class="hw-hero__overlay hw-abs-fill" :style="overlayStyle"></div>
      <div :class="contentClass">
        <h2 v-if="t('headline')"
          class="hw-hero__headline"
          :class="headlineSize(d.headline_size)"
          :style="colorStyle(d.headline_color)"
        >{{ t('headline') }}</h2>
        <p v-if="t('subline')"
          class="hw-hero__subline"
          :class="headlineSize(d.subline_size)"
          :style="colorStyle(d.subline_color)"
        >{{ t('subline') }}</p>
      </div>
    </section>`,
};

// ── intro ──────────────────────────────────────────────────────────────────────
const HwIntro = {
    mixins: [sectionBase],
    computed: {
        hasImage() { return !!this.d.image; },
        imgPos()   { return this.d.image_position || 'none'; },
        isTwoCols() { return this.hasImage && (this.imgPos === 'left' || this.imgPos === 'right'); },

        sectionStyle() {
            return this.d.section_bg_color ? { backgroundColor: this.d.section_bg_color } : {};
        },
        bodyStyle() {
            const cols = parseInt(this.d.body_cols || '1', 10);
            return cols >= 2 ? { columnCount: 2 } : {};
        },
        bodyClass() {
            return ['hw-intro__body', this.align(this.d.body_align), this.d.body_size ? this.bodySize(this.d.body_size) : ''];
        },
        bodyHtml() { return this.body('body'); },
    },
    template: `
    <section class="hw-section hw-section--intro" :style="sectionStyle">
      <div class="hw-intro container-s">

        <div v-if="hasImage && imgPos === 'top'" class="hw-intro__img-top">
          <img v-bind="imgAttrs(d.image, '(max-width:768px) 100vw, 600px')" alt="" class="hw-intro__img" loading="lazy">
        </div>

        <div :class="isTwoCols ? 'grid-2 grid-1--on-m gap-xl' : null">
          <div v-if="isTwoCols && imgPos === 'left'" class="hw-intro__img-col">
            <img v-bind="imgAttrs(d.image, '(max-width:768px) 100vw, 50vw')" alt="" class="hw-intro__img" loading="lazy">
          </div>

          <div class="hw-intro__text">
            <h2 v-if="t('headline')"
              class="hw-intro__headline"
              :class="[align(d.headline_align), headlineSize(d.headline_size)]"
              :style="colorStyle(d.headline_color)"
            >{{ t('headline') }}</h2>
            <div v-if="bodyHtml"
              :class="bodyClass"
              :style="[bodyStyle, colorStyle(d.body_color)]"
              v-html="bodyHtml"
            ></div>
          </div>

          <div v-if="isTwoCols && imgPos === 'right'" class="hw-intro__img-col">
            <img v-bind="imgAttrs(d.image, '(max-width:768px) 100vw, 50vw')" alt="" class="hw-intro__img" loading="lazy">
          </div>
        </div>

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

// ── countdown ──────────────────────────────────────────────────────────────────
const HwCountdown = {
    mixins: [sectionBase],
    data() {
        return { days: 0, hours: 0, minutes: 0, seconds: 0, _timer: null, past: false };
    },
    mounted() {
        this.tick();
        this._timer = setInterval(this.tick, 1000);
    },
    unmounted() {
        clearInterval(this._timer);
    },
    computed: {
        unitLabels() {
            return this.lang === 'en'
                ? { d: 'Days', h: 'Hrs', m: 'Min', s: 'Sec' }
                : { d: 'Tage', h: 'Std', m: 'Min', s: 'Sek' };
        },
        headlineClass() {
            const sz = this.headlineSize(this.d.headline_size);
            return [this.align(this.d.headline_align), sz];
        },
        sectionStyle() {
            const s = {};
            if (this.d.section_bg_color) s.backgroundColor = this.d.section_bg_color;
            if (this.d.background_image) {
                s.backgroundImage = `url(${this.imgBg(this.d.background_image)})`;
                s.backgroundSize = 'cover';
                s.backgroundPosition = this.d.background_position || '50% 50%';
            }
            return s;
        },
        sectionPadClass() {
            return this.d.section_padding ? `padding-${this.d.section_padding}` : '';
        },
        overlayStyle() {
            const op = parseInt(this.d.overlay_opacity || 0, 10);
            return { background: colorToRgba(this.d.overlay_color || '#000000', op) };
        },
        contentPadClass() {
            return this.d.countdown_padding ? `padding-${this.d.countdown_padding}` : '';
        },
        contentStyle() {
            const s = {};
            const op = parseInt(this.d.countdown_bg_opacity ?? 0, 10);
            if (this.d.countdown_bg_color) s.backgroundColor = colorToRgba(this.d.countdown_bg_color, op);
            if (this.d.countdown_radius) s.borderRadius = `var(--radius-${this.d.countdown_radius})`;
            return s;
        },
        valueClass() { return this.d.countdown_value_size ? this.bodySize(this.d.countdown_value_size) : ''; },
        valueStyle() { return this.colorStyle(this.d.countdown_value_color); },
        labelClass() { return this.d.countdown_label_size ? this.bodySize(this.d.countdown_label_size) : ''; },
        labelStyle() { return this.colorStyle(this.d.countdown_label_color); },
        afterClass() { return this.d.after_text_size ? this.bodySize(this.d.after_text_size) : ''; },
        afterStyle() { return this.colorStyle(this.d.after_text_color); },
    },
    methods: {
        tick() {
            const dateStr = this.d.countdown_date || D.config?.wedding_date;
            if (!dateStr) return;
            const target = new Date(dateStr + 'T' + (D.config?.wedding_time || '00:00') + ':00');
            const diff   = target - new Date();
            if (diff <= 0) {
                this.days = this.hours = this.minutes = this.seconds = 0;
                this.past = true;
                clearInterval(this._timer);
                return;
            }
            this.days    = Math.floor(diff / 86400000);
            this.hours   = Math.floor((diff % 86400000) / 3600000);
            this.minutes = Math.floor((diff % 3600000) / 60000);
            this.seconds = Math.floor((diff % 60000) / 1000);
        },
        pad(n) { return String(n).padStart(2, '0'); },
    },
    template: `
    <section class="hw-section hw-section--countdown" :class="sectionPadClass" :style="sectionStyle">
      <div class="hw-overlay hw-abs-fill" :style="overlayStyle"></div>
      <div class="hw-countdown" :class="contentPadClass" :style="contentStyle">
        <h2 v-if="t('headline')"
          class="hw-countdown__headline"
          :class="headlineClass"
          :style="colorStyle(d.headline_color)"
        >{{ t('headline') }}</h2>
        <div class="hw-countdown__timer">
          <div class="hw-countdown__unit">
            <span class="hw-countdown__value" :class="valueClass" :style="valueStyle">{{ days }}</span>
            <span class="hw-countdown__label" :class="labelClass" :style="labelStyle">{{ unitLabels.d }}</span>
          </div>
          <span class="hw-countdown__sep" aria-hidden="true">:</span>
          <div class="hw-countdown__unit">
            <span class="hw-countdown__value" :class="valueClass" :style="valueStyle">{{ pad(hours) }}</span>
            <span class="hw-countdown__label" :class="labelClass" :style="labelStyle">{{ unitLabels.h }}</span>
          </div>
          <span class="hw-countdown__sep" aria-hidden="true">:</span>
          <div class="hw-countdown__unit">
            <span class="hw-countdown__value" :class="valueClass" :style="valueStyle">{{ pad(minutes) }}</span>
            <span class="hw-countdown__label" :class="labelClass" :style="labelStyle">{{ unitLabels.m }}</span>
          </div>
          <span class="hw-countdown__sep" aria-hidden="true">:</span>
          <div class="hw-countdown__unit">
            <span class="hw-countdown__value" :class="valueClass" :style="valueStyle">{{ pad(seconds) }}</span>
            <span class="hw-countdown__label" :class="labelClass" :style="labelStyle">{{ unitLabels.s }}</span>
          </div>
        </div>
        <p v-if="t('after_text')" class="hw-countdown__after" :class="afterClass" :style="afterStyle">{{ t('after_text') }}</p>
      </div>
    </section>`,
};

// ── quote ──────────────────────────────────────────────────────────────────────
const QUOTE_SIZE = { s: 'text-l', m: 'text-xl', l: 'text-2xl', xl: 'text-3xl', '2xl': 'text-4xl', '3xl': 'text-4xl', '4xl': 'text-4xl' };

const HwQuote = {
    mixins: [sectionBase],
    computed: {
        quoteClass() {
            const sz    = this.d.quote_size ? (QUOTE_SIZE[this.d.quote_size] || 'text-xl') : '';
            const align = this.d.quote_align || 'center';
            return `hw-quote__text ${sz} text-${align}`;
        },
        authorClass() {
            const parts = ['hw-quote__author'];
            if (this.d.author_size) parts.push(this.bodySize(this.d.author_size));
            if (this.d.author_align) parts.push(`text-${this.d.author_align}`);
            return parts.join(' ');
        },
        sectionStyle() {
            const s = {};
            if (this.d.section_bg_color) s.backgroundColor = this.d.section_bg_color;
            if (this.d.background_image) {
                s.backgroundImage = `url(${this.imgBg(this.d.background_image)})`;
                s.backgroundSize = 'cover';
                s.backgroundPosition = this.d.background_position || '50% 50%';
            }
            return s;
        },
        sectionPadClass() {
            return this.d.section_padding ? `padding-${this.d.section_padding}` : '';
        },
        overlayStyle() {
            const op = parseInt(this.d.overlay_opacity || 0, 10);
            return { background: colorToRgba(this.d.overlay_color || '#000000', op) };
        },
        quoteHtml() { return renderBody(this.t('quote')); },
    },
    template: `
    <section class="hw-section hw-section--quote" :class="sectionPadClass" :style="sectionStyle">
      <div class="hw-quote__overlay hw-abs-fill" :style="overlayStyle"></div>
      <div class="hw-quote">
        <blockquote class="hw-quote__blockquote">
          <div :class="quoteClass" :style="colorStyle(d.quote_color)" v-html="quoteHtml"></div>
          <cite v-if="t('author')" :class="authorClass" :style="colorStyle(d.author_color)">{{ t('author') }}</cite>
        </blockquote>
      </div>
    </section>`,
};

// ── text-image ─────────────────────────────────────────────────────────────────
const HwTextImage = {
    mixins: [sectionBase],
    computed: {
        layout()        { return this.d.layout || 'image-right'; },
        bodyHtml()      { return this.body('body'); },
        bodyClass()     { return this.d.body_size ? 'hw-ti__body ' + this.bodySize(this.d.body_size) : 'hw-ti__body'; },
        sectionStyle()  { return this.d.section_bg_color ? { backgroundColor: this.d.section_bg_color } : {}; },
        sectionPadClass(){ return this.d.section_padding ? `padding-${this.d.section_padding}` : ''; },
        imgStyle() {
            const bw = parseInt(this.d.image_border_width || 0, 10);
            return bw > 0 ? { border: `${bw}px solid ${this.d.image_border_color || 'transparent'}` } : {};
        },
    },
    template: `
    <section class="hw-section hw-section--text-image" :class="sectionPadClass" :style="sectionStyle">
      <div class="hw-ti" :class="'hw-ti--' + layout">
        <div v-if="d.image" class="hw-ti__media">
          <img v-bind="imgAttrs(d.image, '(max-width:768px) 100vw, 50vw')" :style="imgStyle" alt="" class="hw-ti__img" loading="lazy">
        </div>
        <div class="hw-ti__content">
          <h2 v-if="t('headline')"
            class="hw-ti__headline"
            :class="[align(d.headline_align), headlineSize(d.headline_size)]"
            :style="colorStyle(d.headline_color)"
          >{{ t('headline') }}</h2>
          <div v-if="bodyHtml"
            :class="bodyClass"
            :style="colorStyle(d.body_color)"
            v-html="bodyHtml"
          ></div>
          <a v-if="t('button_text')"
            :href="d.button_url || '#'"
            class="hw-btn hw-btn--secondary hw-ti__btn"
          >{{ t('button_text') }}</a>
        </div>
      </div>
    </section>`,
};

// ── locations ──────────────────────────────────────────────────────────────────
const HwLocations = {
    mixins: [sectionBase],
    methods: {
        ti(item, key)    { return getText(item, key, this.lang); },
        descHtml(item)   { return renderBody(this.ti(item, 'description')); },
    },
    computed: {
        introHtml() { return this.body('intro_text'); },
        layout()    { return this.d.layout || 'cards'; },
        cardStyle() {
            const s = {};
            if (this.d.card_bg_color) s.backgroundColor = this.d.card_bg_color;
            s.borderRadius = RADIUS_TO_CSS[this.d.card_radius || 'l'];
            return s;
        },
    },
    template: `
    <section class="hw-section hw-section--locations" :style="d.section_bg_color ? { backgroundColor: d.section_bg_color } : {}">
      <div class="hw-locations container-m">
        <h2 v-if="t('headline')"
          class="hw-locations__headline"
          :class="[align(d.headline_align), headlineSize(d.headline_size)]"
          :style="colorStyle(d.headline_color)"
        >{{ t('headline') }}</h2>
        <div v-if="introHtml" class="hw-locations__intro"
          :class="[align(d.intro_align), d.intro_size ? bodySize(d.intro_size) : '']"
          :style="colorStyle(d.intro_color)"
          v-html="introHtml"
        ></div>
        <div v-if="(d.items||[]).length" class="hw-locations__grid gap-l grid-1--on-m" :class="layout !== 'cards' ? 'hw-locations__grid--' + layout : ''">
          <div v-for="(item,i) in d.items" :key="i" class="hw-loc-card" :style="cardStyle">
            <div v-if="item.image" class="hw-loc-card__img-wrap">
              <img v-bind="imgAttrs(item.image, '(max-width:768px) 100vw, 400px')" alt="" class="hw-loc-card__img" loading="lazy">
            </div>
            <div class="hw-loc-card__body">
              <h3 v-if="ti(item,'name')" class="hw-loc-card__name" :style="colorStyle(d.name_color)">{{ ti(item,'name') }}</h3>
              <address v-if="ti(item,'address')" class="hw-loc-card__address" :style="colorStyle(d.address_color)">{{ ti(item,'address') }}</address>
              <div v-if="descHtml(item)" class="hw-loc-card__desc" :style="colorStyle(d.description_color)" v-html="descHtml(item)"></div>
              <a v-if="item.maps_url" :href="item.maps_url" target="_blank" rel="noopener"
                class="hw-btn hw-btn--secondary hw-loc-card__btn">{{ item.link_label || 'Link' }}</a>
            </div>
          </div>
        </div>
      </div>
    </section>`,
};

// ── witnesses ──────────────────────────────────────────────────────────────────
const HwWitnesses = {
    mixins: [sectionBase],
    methods: {
        ti(item, key)    { return getText(item, key, this.lang); },
        descHtml(item)   { return renderBody(this.ti(item, 'description')); },
    },
    computed: {
        layout() { return this.d.layout || 'cards'; },
        photoWrapStyle() {
            const s = {};
            const r = this.d.photo_border_radius;
            if (r) s.borderRadius = RADIUS_TO_CSS[r] || '50%';
            const bw = parseInt(this.d.photo_border_width || 0, 10);
            if (bw > 0) {
                s.borderWidth = bw + 'px';
                s.borderStyle = 'solid';
                s.borderColor = this.d.photo_border_color || 'currentColor';
            }
            return s;
        },
        nameClass() { return this.d.name_size ? this.bodySize(this.d.name_size) : ''; },
        roleClass() { return this.d.role_size ? this.bodySize(this.d.role_size) : ''; },
        descClass() { return this.d.desc_size ? this.bodySize(this.d.desc_size) : ''; },
    },
    template: `
    <section class="hw-section hw-section--witnesses">
      <div class="hw-witnesses" :class="'hw-witnesses--' + layout">
        <h2 v-if="t('headline')"
          class="hw-witnesses__headline"
          :class="[align(d.headline_align), headlineSize(d.headline_size)]"
          :style="colorStyle(d.headline_color)"
        >{{ t('headline') }}</h2>
        <div v-if="(d.items||[]).length" :class="'hw-witnesses__grid hw-witnesses__grid--' + layout">
          <div v-for="(item,i) in d.items" :key="i" class="hw-witness-card">
            <div class="hw-witness-card__photo-wrap" :style="photoWrapStyle">
              <img v-if="item.photo" v-bind="imgAttrs(item.photo, '(max-width:768px) 40vw, 200px')" alt="" class="hw-witness-card__photo" loading="lazy">
              <div v-else class="hw-witness-card__photo hw-witness-card__photo--placeholder"></div>
            </div>
            <div class="hw-witness-card__body">
              <p v-if="ti(item,'role')"
                class="hw-witness-card__role"
                :class="roleClass"
                :style="colorStyle(d.role_color)"
              >{{ ti(item,'role') }}</p>
              <h3 v-if="ti(item,'name')"
                class="hw-witness-card__name"
                :class="nameClass"
                :style="colorStyle(d.name_color)"
              >{{ ti(item,'name') }}</h3>
              <div v-if="descHtml(item)"
                class="hw-witness-card__desc"
                :class="descClass"
                :style="colorStyle(d.desc_color)"
                v-html="descHtml(item)"
              ></div>
            </div>
          </div>
        </div>
      </div>
    </section>`,
};

// ── couple ─────────────────────────────────────────────────────────────────────
const HwCouple = {
    mixins: [sectionBase],
    methods: {
        ti(item, key)    { return getText(item, key, this.lang); },
        personText(item) { return renderBody(this.ti(item, 'text')); },
    },
    computed: {
        introHtml() { return this.body('intro_text'); },
        photoWrapStyle() {
            const s = {};
            const r = this.d.photo_border_radius;
            if (r) s.borderRadius = RADIUS_TO_CSS[r] || '50%';
            const bw = parseInt(this.d.photo_border_width || 0, 10);
            if (bw > 0) { s.borderWidth = bw + 'px'; s.borderStyle = 'solid'; s.borderColor = this.d.photo_border_color || 'currentColor'; }
            return s;
        },
        layout()    { return this.d.layout || 'image-top'; },
        nameClass() { return this.d.name_size ? this.bodySize(this.d.name_size) : ''; },
        textClass() { return this.d.text_size ? this.bodySize(this.d.text_size) : ''; },
    },
    template: `
    <section class="hw-section hw-section--couple">
      <div class="hw-couple" :class="'hw-couple--' + layout">
        <h2 v-if="t('headline')"
          class="hw-couple__headline"
          :class="[align(d.headline_align), headlineSize(d.headline_size)]"
          :style="colorStyle(d.headline_color)"
        >{{ t('headline') }}</h2>
        <div v-if="introHtml" class="hw-couple__intro"
          :class="[align(d.intro_align), d.intro_size ? bodySize(d.intro_size) : '']"
          :style="colorStyle(d.intro_color)"
          v-html="introHtml"
        ></div>
        <div :class="'hw-couple__grid hw-couple__grid--' + layout">
          <div v-for="(item, i) in (d.items || [])" :key="i" class="hw-couple-person">
            <div class="hw-couple-person__photo-wrap" :style="photoWrapStyle">
              <img v-if="item.photo"
                v-bind="imgAttrs(item.photo, '(max-width:768px) 80vw, 500px')"
                alt="" class="hw-couple-person__photo" loading="lazy">
              <div v-else class="hw-couple-person__photo hw-couple-person__photo--placeholder"></div>
            </div>
            <div class="hw-couple-person__body">
              <h3 v-if="item.name"
                class="hw-couple-person__name"
                :class="nameClass"
                :style="colorStyle(d.name_color)"
              >{{ item.name }}</h3>
              <div v-if="personText(item)"
                class="hw-couple-person__text"
                :class="textClass"
                :style="colorStyle(d.text_color)"
                v-html="personText(item)"
              ></div>
            </div>
          </div>
        </div>
      </div>
    </section>`,
};

// ── program ────────────────────────────────────────────────────────────────────
const HwProgram = {
    mixins: [sectionBase],
    methods: {
        ti(item, key)    { return getText(item, key, this.lang); },
        descHtml(item)   { return renderBody(this.ti(item, 'description')); },
    },
    computed: {
        sectionStyle() {
            return this.d.section_bg_color ? { backgroundColor: this.d.section_bg_color } : {};
        },
        sectionPadClass() {
            return this.d.section_padding ? `padding-${this.d.section_padding}` : '';
        },
        timelineStyle() {
            const s = {};
            if (this.d.timeline_color) s['--hw-timeline-color'] = this.d.timeline_color;
            if (this.d.emoji_bg_color) s['--hw-emoji-bg-color'] = this.d.emoji_bg_color;
            return s;
        },
        introHtml() { return this.body('intro_text'); },
    },
    template: `
    <section class="hw-section hw-section--program" :class="sectionPadClass" :style="sectionStyle">
      <div class="hw-program" :style="timelineStyle">
        <h2 v-if="t('headline')"
          class="hw-program__headline"
          :class="[align(d.headline_align), headlineSize(d.headline_size)]"
          :style="colorStyle(d.headline_color)"
        >{{ t('headline') }}</h2>
        <div v-if="introHtml"
          class="hw-program__intro"
          :class="[align(d.intro_align), d.intro_size ? bodySize(d.intro_size) : '']"
          :style="colorStyle(d.intro_color)"
          v-html="introHtml"
        ></div>
        <div class="hw-program__items">
          <div v-for="(item,i) in d.items" :key="i" class="hw-program-item">
            <div class="hw-program-item__connector" aria-hidden="true"></div>
            <div class="hw-program-item__dot" aria-hidden="true">
              <span v-if="item.icon" class="hw-program-item__icon">{{ item.icon }}</span>
            </div>
            <div class="hw-program-item__head">
              <span v-if="item.time" class="hw-program-item__time" :style="colorStyle(d.time_color)">{{ item.time }}</span>
            </div>
            <h3 v-if="ti(item,'title')"
              class="hw-program-item__title"
              :class="d.title_size ? bodySize(d.title_size) : ''"
              :style="colorStyle(d.title_color)"
            >{{ ti(item,'title') }}</h3>
            <div v-if="descHtml(item)"
              class="hw-program-item__desc"
              :class="d.desc_size ? bodySize(d.desc_size) : ''"
              :style="colorStyle(d.desc_color)"
              v-html="descHtml(item)"
            ></div>
          </div>
        </div>
      </div>
    </section>`,
};

// ── accommodation ──────────────────────────────────────────────────────────────
const HwAccommodation = {
    mixins: [sectionBase],
    methods: {
        ti(item, key) { return getText(item, key, this.lang); },
    },
    computed: {
        sectionStyle() {
            return this.d.section_bg_color ? { backgroundColor: this.d.section_bg_color } : {};
        },
        introHtml() { return this.body('intro_text'); },
        layout()    { return this.d.layout || 'image-left'; },
    },
    template: `
    <section class="hw-section hw-section--accommodation" :style="sectionStyle">
      <div class="hw-accommodation grid-2 grid-1--on-m" :class="'hw-accommodation--' + layout">
        <div class="hw-accommodation__media">
          <img v-if="d.image"
            v-bind="imgAttrs(d.image, '(max-width:768px) 100vw, 50vw')"
            class="hw-accommodation__img"
            alt=""
            loading="lazy"
          >
        </div>
        <div class="hw-accommodation__content">
          <h2 v-if="t('headline')"
            class="hw-accommodation__headline"
            :class="[align(d.headline_align), headlineSize(d.headline_size)]"
            :style="colorStyle(d.headline_color)"
          >{{ t('headline') }}</h2>
          <div v-if="introHtml"
            class="hw-accommodation__intro"
            :class="[align(d.intro_align), d.intro_size ? bodySize(d.intro_size) : '']"
            :style="colorStyle(d.intro_color)"
            v-html="introHtml"
          ></div>
          <div v-if="(d.items||[]).length" class="hw-accommodation__items grid-1--on-s">
            <div v-for="(item,i) in d.items" :key="i" class="hw-unk-item">
              <div class="hw-unk-item__icon" aria-hidden="true">{{ item.icon || '🏩' }}</div>
              <div class="hw-unk-item__body">
                <h3 v-if="ti(item,'name')" class="hw-unk-item__name" :style="colorStyle(d.name_color)">{{ ti(item,'name') }}</h3>
                <address v-if="ti(item,'address')" class="hw-unk-item__address" :style="colorStyle(d.address_color)">{{ ti(item,'address') }}</address>
                <a v-if="item.website_url" :href="item.website_url" target="_blank" rel="noopener"
                  class="hw-btn hw-btn--primitive hw-btn--sm hw-unk-item__btn" :style="colorStyle(d.name_color)">
                  Website <i class="hgi-stroke hgi-arrow-up-right-01"></i>
                </a>
              </div>
            </div>
          </div>
        </div>
      </div>
    </section>`,
};

// ══════════════════════════════════════════════════════════════════════════════
// NEW SECTION COMPONENTS
// ══════════════════════════════════════════════════════════════════════════════

// ── navigation section ────────────────────────────────────────────────────────
const SECTION_LABEL_KEYS = {
  'hero-header':    'section_hero',
  'intro':          'section_intro',
  'big-header':     'section_bigheader',
  'countdown':      'section_countdown',
  'locations':      'section_locations',
  'quote':          'section_quote',
  'witnesses':      'section_witnesses',
  'couple':         'section_couple',
  'program':        'section_program',
  'accommodation':  'section_accommodation',
  'save-the-date':  'section_savethedate',
  'hochzeits-abc':  'section_abc',
  'faq':            'section_faq',
  'geschenketisch': 'section_gifts',
  'kuchenliste':    'section_cakes',
  'abendprogramm':  'section_evening',
  'gallery-section':'section_gallery',
  'text-image':     'section_textimage',
  'rsvp':           'section_rsvp',
  'navigation':     'section_nav',
};

const HwNavigationSection = {
    mixins: [sectionBase],
    props: ['section', 'lang', 'sections'],
    setup(props) {
        const open = ref(false);
        function scrollTo(sectionId) {
            open.value = false;
            const el = document.querySelector(`[data-section-id="${sectionId}"]`);
            if (el) el.scrollIntoView({ behavior: 'smooth', block: 'start' });
        }
        function onDocClick(e) {
            if (!e.target.closest('.hw-nav-section')) open.value = false;
        }
        onMounted(()   => document.addEventListener('click', onDocClick));
        onUnmounted(() => document.removeEventListener('click', onDocClick));
        return { open, scrollTo };
    },
    computed: {
        d()            { return this.section?.data || {}; },
        sectionStyle() {
            const s = {};
            if (this.d.section_bg_color)  s.backgroundColor           = this.d.section_bg_color;
            if (this.d.link_color)        s['--nav-link-color']       = this.d.link_color;
            if (this.d.link_hover_color)  s['--nav-link-hover-color'] = this.d.link_hover_color;
            return s;
        },
        sectionPadClass() { return this.d.section_padding ? `padding-${this.d.section_padding}` : ''; },
        navLinksStyle() {
            return this.d.nav_justify ? { justifyContent: this.d.nav_justify } : {};
        },
        visibleItems() {
            const sectionIds = new Set((this.sections || []).map(s => s.id));
            return (this.d.items || []).filter(item => !item.hidden && sectionIds.has(item.section_id));
        },
        itemLabel() {
            return (item) => {
                const label = item[this.lang]?.label || item.de?.label || '';
                if (label) return label;
                const sec = (this.sections || []).find(s => s.id === item.section_id);
                if (!sec) return '';
                const dict = window['HW_LANG_' + (this.lang || 'de').toUpperCase()] || window.HW_LANG_DE || {};
                const key = SECTION_LABEL_KEYS[sec.type];
                return key ? (dict[key] || sec.type) : sec.type;
            };
        },
    },
    template: `
    <section class="hw-section hw-nav-section" :class="sectionPadClass" :style="sectionStyle">
      <div class="hw-nav-section__inner container-l">
        <h2 v-if="d.headline" class="hw-nav-section__title" :class="headlineSize(d.headline_size)" :style="colorStyle(d.headline_color)">{{ t('headline') }}</h2>
        <nav class="hw-nav-section__links" :style="navLinksStyle" aria-label="Seitennavigation">
          <button v-for="item in visibleItems" :key="item.section_id"
            class="hw-nav-section__btn"
            @click="scrollTo(item.section_id)"
          >{{ itemLabel(item) }}</button>
        </nav>
        <button class="hw-nav-section__burger" @click.stop="open = !open" :aria-label="open ? 'Menü schließen' : 'Menü öffnen'">
          <i :class="['hgi-stroke', open ? 'hgi-cancel-01' : 'hgi-rounded hgi-menu-09']"></i>
        </button>
      </div>
      <div v-if="open" class="hw-nav-section__drawer">
        <button v-for="item in visibleItems" :key="item.section_id"
          class="hw-nav-section__drawer-item"
          @click="scrollTo(item.section_id)"
        >{{ itemLabel(item) }}</button>
      </div>
    </section>`,
};

// ── lang switcher widget ───────────────────────────────────────────────────────
const ALL_LANG_LABELS = {
    de: 'Deutsch', en: 'English', es: 'Español', fr: 'Français',
    it: 'Italiano', pt: 'Português', nl: 'Nederlands', pl: 'Polski',
    ru: 'Русский', tr: 'Türkçe',
};
const HwLangSwitcher = {
    props: ['lang', 'langs'],
    setup(props) {
        const open = ref(false);
        function switchLang(code) {
            const url = new URL(window.location.href);
            url.searchParams.set('lang', code);
            window.location.href = url.toString();
        }
        function onDocClick(e) {
            if (!e.target.closest('.hw-lang-switcher')) open.value = false;
        }
        onMounted(()   => document.addEventListener('click', onDocClick));
        onUnmounted(() => document.removeEventListener('click', onDocClick));
        const activeLangs = computed(() =>
            (props.langs?.active || []).map(l => ({ code: l.code, label: ALL_LANG_LABELS[l.code] || l.code }))
        );
        return { open, switchLang, activeLangs };
    },
    template: `
    <div v-if="activeLangs.length > 1" class="hw-lang-switcher">
      <button class="hw-lang-switcher__toggle" @click.stop="open = !open" :aria-label="'Sprache wechseln'">
        <i class="hgi-stroke hgi-language-circle"></i>
      </button>
      <div v-if="open" class="hw-lang-switcher__drawer">
        <button v-for="l in activeLangs" :key="l.code"
          class="hw-lang-switcher__item"
          :class="{ 'hw-lang-switcher__item--active': l.code === lang }"
          @click="switchLang(l.code)"
        >{{ l.label }}</button>
      </div>
    </div>`,
};

// ── abendprogramm ─────────────────────────────────────────────────────────────
const HwAbendprogramm = {
    mixins: [sectionBase],
    computed: {
        sectionStyle() {
            const s = {};
            const mh = parseInt(this.d.min_height, 10);
            if (mh > 0) s.minHeight = mh + 'dvh';
            return s;
        },
        contentPadClass() { return this.d.content_padding ? `padding-${this.d.content_padding}` : ''; },
        contentStyle() {
            const op  = parseInt(this.d.overlay_opacity || 0, 10);
            const r   = this.d.content_radius || 'm';
            const pos = (this.d.content_align || 'center').split('-');
            const v   = pos.length > 1 ? pos[0] : 'center';
            const h   = pos.length > 1 ? pos[1] : pos[0];
            const mbMap = { top: '0 auto', center: 'auto', bottom: 'auto 0' };
            const miMap = { left: '0 auto', center: 'auto', right: 'auto 0' };
            const taMap = { left: 'left',   center: 'center', right: 'right' };
            const aiMap = { left: 'flex-start', center: 'center', right: 'flex-end' };
            return {
                background:    colorToRgba(this.d.overlay_color || '#000000', op),
                borderRadius:  `var(--radius-${r})`,
                marginBlock:   mbMap[v]  || 'auto',
                marginInline:  miMap[h]  || 'auto',
                textAlign:     taMap[h]  || 'center',
                alignItems:    aiMap[h]  || 'center',
            };
        },
        objPos()    { return this.d.background_position || 'center'; },
        bodyHtml()  { return this.body('body'); },
    },
    template: `
    <section class="hw-section hw-section--abendprogramm hw-stack" :style="sectionStyle">
      <img v-if="d.background_image"
        v-bind="imgAttrs(d.background_image, '100vw')"
        :style="{ objectPosition: objPos }"
        class="hw-abs-fill hw-abend__bg-img"
        alt="" loading="lazy"
      >
      <div :class="['hw-abend__content', contentPadClass]" :style="contentStyle">
        <h2 v-if="t('headline')"
          :class="[align(d.headline_align), headlineSize(d.headline_size)]"
          :style="colorStyle(d.headline_color)"
        >{{ t('headline') }}</h2>
        <div v-if="bodyHtml"
          class="hw-abend__body"
          :class="[align(d.body_align), d.body_size ? bodySize(d.body_size) : '']"
          :style="colorStyle(d.body_color)"
          v-html="bodyHtml"
        ></div>
      </div>
    </section>`,
};

// ── faq ───────────────────────────────────────────────────────────────────────
const HwFaq = {
    mixins: [sectionBase],
    data() { return { openIndex: null }; },
    methods: {
        ti(item, key)    { return getText(item, key, this.lang); },
        toggle(i)        { this.openIndex = this.openIndex === i ? null : i; },
        answerHtml(item) { return renderBody(this.ti(item, 'answer')); },
    },
    computed: {
        introHtml()  { return this.body('intro_text'); },
        introStyle() {
            const s = {};
            if (this.d.intro_bg_color) s.backgroundColor = this.d.intro_bg_color;
            if (this.d.intro_bg_image) {
                s.backgroundImage = `url(${imgBg(this.d.intro_bg_image)})`;
                s.backgroundSize  = 'cover';
                s.backgroundPosition = 'center';
            }
            return s;
        },
        introOverlayStyle() {
            const op = parseInt(this.d.intro_overlay_opacity || 0, 10);
            return { background: colorToRgba(this.d.intro_overlay_color || '#000000', op) };
        },
        sectionStyle() {
            return this.d.section_bg_color ? { backgroundColor: this.d.section_bg_color } : {};
        },
        sectionPadClass() { return this.d.section_padding ? `padding-${this.d.section_padding}` : ''; },
        isAccRight() { return (this.d.layout || 'acc-right') === 'acc-right'; },
    },
    template: `
    <section class="hw-section hw-section--faq" :style="sectionStyle">
      <div class="hw-faq grid-2 grid-1--on-m" :class="isAccRight ? 'hw-faq--acc-right' : 'hw-faq--acc-left'">
        <div class="hw-faq__intro hw-stack" :style="introStyle">
          <div class="hw-overlay hw-abs-fill" :style="introOverlayStyle"></div>
          <div class="hw-faq__intro-content" :class="sectionPadClass">
            <h2 v-if="t('headline')"
              :class="[align(d.headline_align), headlineSize(d.headline_size)]"
              :style="colorStyle(d.headline_color)"
            >{{ t('headline') }}</h2>
            <div v-if="introHtml" class="hw-faq__intro-text"
              :class="[align(d.intro_align), d.intro_size ? bodySize(d.intro_size) : '']"
              :style="colorStyle(d.intro_color)"
              v-html="introHtml"></div>
          </div>
        </div>
        <div class="hw-faq__accordion" :class="sectionPadClass">
          <div v-for="(item,i) in (d.items||[])" :key="i"
            class="hw-faq-item"
            :class="{ 'hw-faq-item--open': openIndex === i }"
          >
            <span class="hw-faq-item__q" @click="toggle(i)">
              {{ ti(item,'question') }}
              <i class="hgi-stroke hgi-arrow-right-01 hw-faq-item__chevron"></i>
            </span>
            <div v-if="openIndex === i" class="hw-faq-item__a" v-html="answerHtml(item)"></div>
          </div>
        </div>
      </div>
    </section>`,
};

// ── rsvp ──────────────────────────────────────────────────────────────────────
const HwRsvp = {
    mixins: [sectionBase],
    data() {
        const g = D.guest || {};
        return {
            form: {
                name:    g.name    || '',
                email:   g.email   || '',
                rsvp:    (g.rsvp && g.rsvp !== 'pending') ? g.rsvp : 'confirmed',
                persons: g.persons || 1,
                meal:    {},
                message: g.message || '',
                _hp:     '',
                token:   g.token   || '',
            },
            submitted:  false,
            submitting: false,
            error:      '',
        };
    },
    computed: {
        introStyle() {
            const s = {};
            if (this.d.intro_bg_image) {
                s.backgroundImage    = `url(${this.imgBg(this.d.intro_bg_image)})`;
                s.backgroundSize     = 'cover';
                s.backgroundPosition = 'center';
            }
            if (this.d.intro_bg_color) s.backgroundColor = this.d.intro_bg_color;
            return s;
        },
        introOverlayStyle() {
            const op = parseInt(this.d.intro_overlay_opacity || 0, 10);
            return { background: colorToRgba(this.d.intro_overlay_color || '#000000', op) };
        },
        isFormRight() { return (this.d.layout || 'form-right') === 'form-right'; },
        formColStyle() {
            const s = {};
            if (this.d.form_bg_color) s.backgroundColor = this.d.form_bg_color;
            return s;
        },
        sectionPadClass() { return this.d.section_padding ? `padding-${this.d.section_padding}` : ''; },
        introHtml()   { return this.body('intro_text'); },
        mealOptions() { return (this.d.meal_options || []).map(o => getText(o, 'label', this.lang)).filter(Boolean); },
        isPast() {
            if (!this.d.deadline) return false;
            return new Date(this.d.deadline) < new Date();
        },
        fl() {
            const defaults = this.lang === 'en'
                ? ['Your name', 'Email address', 'Will you attend?', 'Yes, I\'ll be there', 'No, I can\'t make it',
                   'Number of guests', 'Meal preference', 'Message', 'Submit', 'Thank you for your reply!', 'The RSVP deadline has passed.']
                : ['Dein Name', 'E-Mail-Adresse', 'Werdet ihr dabei sein?', 'Ja, ich komme', 'Nein, ich kann leider nicht',
                   'Anzahl Personen', 'Essenswunsch', 'Nachricht', 'Anmelden', 'Vielen Dank für deine Rückmeldung!', 'Die Anmeldefrist ist leider abgelaufen.'];
            return defaults.map((fb, i) => {
                const item = this.d.form_labels?.[i];
                return getText(item, 'text', this.lang) || fb;
            });
        },
    },
    methods: {
        async submit() {
            this.error = '';
            if (!this.form.name.trim()) { this.error = this.fl[0] + ' ' + (this.lang === 'en' ? 'is required.' : 'ist erforderlich.'); return; }
            this.submitting = true;
            try {
                const csrf = await fetchCsrfToken();
                const r = await fetch(BASE + '/api/rsvp', {
                    method:  'POST',
                    headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrf },
                    body:    JSON.stringify({
                        name:     this.form.name,
                        email:    this.form.email,
                        rsvp:     this.form.rsvp,
                        persons:  this.form.persons,
                        meal:     Object.values(this.form.meal),
                        message:  this.form.message,
                        language: this.lang,
                        _hp:      this.form._hp,
                        token:    this.form.token,
                    }),
                });
                let data = null;
                try { data = await r.json(); } catch (_) {}
                if (!r.ok) throw new Error(data?.error || `Fehler ${r.status}`);
                this.submitted = true;
            } catch (e) {
                this.error = e.message;
            } finally {
                this.submitting = false;
            }
        },
    },
    template: `
    <section class="hw-section hw-section--rsvp">
      <div class="hw-rsvp grid-2 grid-1--on-m" :class="isFormRight ? 'hw-rsvp--form-right' : 'hw-rsvp--form-left'">

        <div class="hw-rsvp__intro hw-stack" :style="introStyle">
          <div class="hw-overlay hw-abs-fill"  :style="introOverlayStyle"></div>
          <div class="hw-rsvp__intro-content" :class="sectionPadClass">
            <h2 class="margin-bottom-s" v-if="t('headline')"
              :class="[align(d.headline_align), headlineSize(d.headline_size)]"
              :style="colorStyle(d.headline_color)"
            >{{ t('headline') }}</h2>
            <div v-if="introHtml"
              class="hw-rsvp__intro-text"
              :class="[align(d.intro_align), d.intro_size ? bodySize(d.intro_size) : '']"
              :style="colorStyle(d.intro_color)"
              v-html="introHtml"
            ></div>
          </div>
        </div>

        <div class="hw-rsvp__form-col" :class="sectionPadClass" :style="formColStyle">
          <p v-if="isPast" class="hw-rsvp__deadline">{{ fl[10] }}</p>
          <div v-else-if="submitted" class="hw-rsvp__success">{{ fl[9] }}</div>
          <form v-else class="hw-rsvp__form" @submit.prevent="submit">
            <input type="text" name="_hp" v-model="form._hp" tabindex="-1" aria-hidden="true" style="display:none">

            <div class="hw-form-field">
              <label>{{ fl[0] }}</label>
              <input type="text" v-model="form.name" class="hw-input input" required>
            </div>

            <div v-if="d.show_email !== false" class="hw-form-field">
              <label>{{ fl[1] }}</label>
              <input type="email" v-model="form.email" class="hw-input input">
            </div>

            <div class="hw-form-field">
              <label>{{ fl[2] }}</label>
              <div class="hw-rsvp__radio-group">
                <label class="hw-rsvp__radio"><input class="radio" type="radio" v-model="form.rsvp" value="confirmed"> {{ fl[3] }}</label>
                <label class="hw-rsvp__radio"><input class="radio" type="radio" v-model="form.rsvp" value="declined"> {{ fl[4] }}</label>
              </div>
            </div>

            <div v-if="d.show_persons && form.rsvp === 'confirmed'" class="hw-form-field">
              <label>{{ fl[5] }}</label>
              <input type="number" v-model.number="form.persons" min="1" max="20" class="hw-input input">
            </div>

            <template v-if="d.show_meal && form.rsvp === 'confirmed' && mealOptions.length">
              <div v-for="p in form.persons" :key="p" class="hw-form-field">
                <label>{{ fl[6] }}<template v-if="form.persons > 1"> {{ p }}</template></label>
                <select v-model="form.meal[p]" class="hw-input input">
                  <option value="">–</option>
                  <option v-for="opt in mealOptions" :key="opt" :value="opt">{{ opt }}</option>
                </select>
              </div>
            </template>

            <div v-if="d.show_message" class="hw-form-field">
              <label>{{ fl[7] }}</label>
              <textarea v-model="form.message" rows="3" class="hw-input input"></textarea>
            </div>

            <p v-if="error" class="hw-rsvp__error">{{ error }}</p>
            <button type="submit" class="button" :disabled="submitting">
              {{ submitting ? '…' : fl[8] }}
            </button>
          </form>
        </div>

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

// ── hochzeits-abc ─────────────────────────────────────────────────────────────
const HwHochzeitsAbc = {
    mixins: [sectionBase],
    methods: {
        ti(item, key)    { return getText(item, key, this.lang); },
        descHtml(item)   { return renderBody(this.ti(item, 'description')); },
    },
    computed: {
        sectionStyle()    { return this.d.section_bg_color ? { backgroundColor: this.d.section_bg_color } : {}; },
        letterStyle()     { return this.colorStyle(this.d.letter_color); },
        textStyle()       { return this.colorStyle(this.d.text_color); },
        letterSizeClass() { return this.d.letter_size ? this.bodySize(this.d.letter_size) : ''; },
        descSizeClass()   { return this.d.desc_size   ? this.bodySize(this.d.desc_size)   : ''; },
    },
    template: `
    <section class="hw-section hw-section--hochzeits-abc" :style="sectionStyle">
      <div class="container-s">
        <h2 v-if="t('headline')"
          class="hw-abc__headline"
          :class="[align(d.headline_align), headlineSize(d.headline_size)]"
          :style="colorStyle(d.headline_color)"
        >{{ t('headline') }}</h2>
        <div class="hw-abc__grid">
          <template v-for="(item,i) in (d.items||[])" :key="i">
            <div v-if="!item.hidden" class="hw-abc__item">
              <span class="hw-abc__letter" :class="letterSizeClass" :style="letterStyle">{{ item.term }}</span>
              <div class="hw-abc__desc" :class="descSizeClass" :style="textStyle" v-html="descHtml(item)"></div>
            </div>
          </template>
        </div>
      </div>
    </section>`,
};

// ── save the date ─────────────────────────────────────────────────────────────
const HwSaveTheDate = {
    mixins: [sectionBase],
    computed: {
        sectionStyle() {
            const s = {};
            if (this.d.section_bg_color) s.backgroundColor = this.d.section_bg_color;
            return s;
        },
        sectionPadClass() { return this.d.section_padding ? `padding-${this.d.section_padding}` : ''; },
        dateparts() {
            if (!this.d.date) return { day: '--', month: '--', year: '--' };
            const parts = this.d.date.split('-');
            return {
                day:   (parts[2] || '').padStart(2, '0'),
                month: (parts[1] || '').padStart(2, '0'),
                year:  (parts[0] || '').slice(-2) || '--',
            };
        },
        overlayStyle() {
            const op = parseInt(this.d.overlay_opacity || 0, 10);
            return { background: colorToRgba('#000000', op) };
        },
        numStyle() { return this.d.section_bg_color ? { color: this.d.section_bg_color } : {}; },
    },
    template: `
    <section class="hw-section hw-section--save-the-date" :class="sectionPadClass" :style="sectionStyle">
      <h2 v-if="t('headline')"
        class="hw-std__headline"
        :class="[align(d.headline_align), headlineSize(d.headline_size)]"
        :style="colorStyle(d.headline_color)"
      >{{ t('headline') }}</h2>
      <div class="hw-std__photos grid-3 grid-1--on-s">
        <div v-for="part in ['day','month','year']" :key="part" class="hw-std__photo-col hw-stack">
          <img v-if="d['image_'+part]" v-bind="imgAttrs(d['image_'+part], '33vw')" class="hw-std__img hw-abs-fill" alt="" loading="lazy">
          <div class="hw-overlay hw-abs-fill" :style="overlayStyle"></div>
          <span class="hw-std__num" :style="numStyle">{{ dateparts[part] }}</span>
        </div>
      </div>
      <div class="flex-column gap-xs">
        <p v-if="t('text1')" class="hw-std__text1">{{ t('text1') }}</p>
        <p v-if="t('text2')" class="hw-std__text2">{{ t('text2') }}</p>
      </div>
    </section>`,
};

// ── kuchenliste ───────────────────────────────────────────────────────────────
const HwKuchenliste = {
    mixins: [sectionBase],
    data() {
        return {
            form: { name: '', kuchen: '', anmerkung: '', _hp: '' },
            entries:   [],
            submitted: false,
            submitting: false,
            error:     '',
        };
    },
    mounted() { this.loadEntries(); },
    methods: {
        async loadEntries() {
            try {
                const r = await fetch(BASE + '/api/kuchenliste/list');
                if (r.ok) this.entries = (await r.json()).data ?? [];
            } catch (_) {}
        },
        async submit() {
            this.error = '';
            if (!this.form.name || !this.form.kuchen) { this.error = 'Name und Kuchenart sind erforderlich.'; return; }
            this.submitting = true;
            try {
                const csrf = await fetchCsrfToken();
                const body = { ...this.form, section_id: this.section?.id || '' };
                const r = await fetch(BASE + '/api/kuchenliste', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrf }, body: JSON.stringify(body) });
                let data = null;
                try { data = await r.json(); } catch (_) {}
                if (!r.ok) throw new Error(data?.error || `Server-Fehler ${r.status}`);
                this.submitted = true;
                await this.loadEntries();
            } catch (e) {
                this.error = e.message;
            } finally {
                this.submitting = false;
            }
        },
    },
    computed: {
        sectionStyle() {
            const s = {};
            if (this.d.background_image) {
                s.backgroundImage = `url(${imgBg(this.d.background_image)})`;
                s.backgroundSize  = 'cover';
                s.backgroundPosition = this.d.background_position || 'center';
            }
            return s;
        },
        sectionPadClass() { return this.d.section_padding ? `padding-${this.d.section_padding}` : ''; },
        containerStyle() {
            const s = {};
            if (this.d.container_radius) s.borderRadius = `var(--radius-${this.d.container_radius})`;
            return s;
        },
        formBoxStyle() {
            const s = {};
            if (this.d.form_bg_color) s.backgroundColor = this.d.form_bg_color;
            if (this.d.padding)       s.padding          = `var(--space-${this.d.padding})`;
            if (this.d.container_radius) s.borderRadius = `var(--radius-${this.d.container_radius})`;
            return s;
        },
        col2Style() {
            const s = {};
            if (this.d.col2_bg_color) {
                const op = parseInt(this.d.col2_opacity ?? 100, 10);
                s.backgroundColor = colorToRgba(this.d.col2_bg_color, op);
            }
            return s;
        },
        layout()    { return this.d.layout || 'list-right'; },
        listColor() { return this.colorStyle(this.d.list_text_color); },
        introHtml() { return this.body('intro_text'); },
        fl() {
            const defaults = [
                'Dein Name',
                'Welchen Kuchen bringst du?',
                'Anmerkungen (optional)',
                'Eintragen',
                'Vielen Dank für deine Anmeldung!',
            ];
            return defaults.map((fb, i) => {
                const item = this.d.form_labels?.[i];
                return getText(item, 'text', this.lang) || fb;
            });
        },
    },
    template: `
    <section class="hw-section hw-section--kuchenliste padding-0" :style="sectionStyle">
      <div :class="['hw-kuchen grid-2 grid-1--on-m', 'hw-kuchen--' + layout]" :style="containerStyle">
        <div class="hw-kuchen__form-col" :class="sectionPadClass">
          <div class="hw-kuchen__contentwrapper" :style="formBoxStyle">
          <h2 v-if="t('headline')"
            :class="[align(d.headline_align), headlineSize(d.headline_size)]"
            :style="colorStyle(d.headline_color)"
          >{{ t('headline') }}</h2>
          <div v-if="introHtml" class="hw-kuchen__intro" v-html="introHtml"></div>
          <div v-if="submitted" class="hw-kuchen__success">{{ fl[4] }}</div>
          <form v-else @submit.prevent="submit" class="hw-kuchen__form">
            <input type="text" name="_hp" v-model="form._hp" tabindex="-1" aria-hidden="true" style="display:none">
            <div class="hw-form-field">
              <label>{{ fl[0] }}</label>
              <input type="text" v-model="form.name" class="hw-input input">
            </div>
            <div class="hw-form-field">
              <label>{{ fl[1] }}</label>
              <input type="text" v-model="form.kuchen" class="hw-input input">
            </div>
            <div class="hw-form-field">
              <label>{{ fl[2] }}</label>
              <textarea v-model="form.anmerkung" rows="3" class="hw-input input"></textarea>
            </div>
            <p v-if="error" class="hw-kuchen__error">{{ error }}</p>
            <button type="submit" class="button" :disabled="submitting">
              {{ submitting ? '…' : fl[3] }}
            </button>
          </form>
          </div>
        </div>
        <div class="hw-kuchen__list-col" :class="sectionPadClass" :style="col2Style">
          <table v-if="entries.length" class="hw-kuchen__table">
            <tbody>
              <tr v-for="(e,i) in entries" :key="i" class="hw-kuchen__row" :style="listColor">
                <td class="hw-kuchen__cell hw-kuchen__cell--name">{{ e.name }}</td>
                <td class="hw-kuchen__cell hw-kuchen__cell--kuchen">{{ e.kuchen }}</td>
              </tr>
            </tbody>
          </table>
        </div>
      </div>
    </section>`,
};

// ── geschenketisch ────────────────────────────────────────────────────────────
const HwGeschenketisch = {
    mixins: [sectionBase],
    data() {
        return {
            reservingIndex: null,
            form: { name: '', email: '', _hp: '' },
            submitting: false,
            error: '',
            localReserved: {},
        };
    },
    methods: {
        ti(item, key)  { return getText(item, key, this.lang); },
        noteHtml(item) { return renderBody(this.ti(item, 'note')); },
        openForm(i) {
            this.reservingIndex = i;
            this.form = { name: '', email: '', _hp: '' };
            this.error = '';
        },
        cancelForm() { this.reservingIndex = null; },
        async reserve(i) {
            this.error = '';
            if (!this.form.name || !this.form.email) { this.error = this.fl[5]; return; }
            this.submitting = true;
            try {
                const csrf = await fetchCsrfToken();
                const body = {
                    section_id:  this.section?.id || '',
                    item_index:  i,
                    name:        this.form.name,
                    email:       this.form.email,
                    _hp:         this.form._hp,
                };
                const r = await fetch(BASE + '/api/geschenketisch/reserve', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrf }, body: JSON.stringify(body) });
                const data = await r.json();
                if (!r.ok) throw new Error(data.error || 'Fehler');
                this.localReserved[i] = true;
                this.reservingIndex = null;
            } catch (e) {
                this.error = e.message;
            } finally {
                this.submitting = false;
            }
        },
    },
    computed: {
        sectionStyle()        { return this.d.section_bg_color ? { backgroundColor: this.d.section_bg_color } : {}; },
        sectionPadClass()     { return this.d.section_padding ? `padding-${this.d.section_padding}` : ''; },
        layout()              { return this.d.layout || 'list-right'; },
        introHtml()           { return this.body('intro_text'); },
        items()               { return this.d.items || []; },
        surpriseModeGifts()   { return !!CONFIG.surprise_mode_gifts; },
        fl() {
            const defaults = [
                'Dein Name',
                'E-Mail',
                'Reservieren',
                'Abbrechen',
                'Reserviert',
                'Name und E-Mail sind erforderlich.',
            ];
            return defaults.map((fb, i) => {
                const item = this.d.form_labels?.[i];
                return getText(item, 'text', this.lang) || fb;
            });
        },
    },
    template: `
    <section class="hw-section hw-section--geschenketisch" :style="sectionStyle">
      <div class="hw-geschenk grid-2 grid-1--on-m" :class="'hw-geschenk--' + layout">
        <div class="hw-geschenk__media">
          <img v-if="d.image"
            v-bind="imgAttrs(d.image, '(max-width:768px) 100vw, 50vw')"
            class="hw-geschenk__img hw-abs-fill"
            alt="" loading="lazy"
          >
        </div>
        <div class="hw-geschenk__content" :class="sectionPadClass">
          <h2 v-if="t('headline')"
            :class="[align(d.headline_align), headlineSize(d.headline_size)]"
            :style="colorStyle(d.headline_color)"
          >{{ t('headline') }}</h2>
          <div v-if="introHtml" class="hw-geschenk__intro"
            :class="[align(d.intro_align), d.intro_size ? bodySize(d.intro_size) : '']"
            :style="colorStyle(d.intro_color)"
            v-html="introHtml"
          ></div>
          <ul v-if="!surpriseModeGifts && items.length" class="hw-geschenk__items">
            <li v-for="(item,i) in items" :key="i" class="hw-geschenk-item">
              <a class="hw-geschenk-item__icon" aria-hidden="true" target="_blank" :href="item.link">
                <i class="hgi-stroke hgi-gift"></i>
              </a>
              <div class="hw-geschenk-item__body">
                <div class="hw-geschenk-item__head">
                  <h3 v-if="ti(item,'title')" class="hw-geschenk-item__title" :class="d.title_size ? bodySize(d.title_size) : ''" :style="colorStyle(d.title_color)">{{ ti(item,'title') }}</h3>
                  <a v-if="item.link" :href="item.link" target="_blank" rel="noopener" class="hw-geschenk-item__link">
                    <i class="hgi-stroke hgi-arrow-up-right-01"></i>
                  </a>
                </div>
                <div v-if="noteHtml(item)" class="hw-geschenk-item__note" :class="d.desc_size ? bodySize(d.desc_size) : ''" :style="colorStyle(d.desc_color)" v-html="noteHtml(item)"></div>
                <div class="hw-geschenk-item__action">
                  <span v-if="localReserved[i] || item.status === 'reserved'" class="hw-geschenk-item__reserved">
                    <i class="hgi-stroke hgi-checkmark-circle-01"></i>
                    {{ fl[4] }}<template v-if="item.giver_name"> von {{ item.giver_name }}</template>
                  </span>
                  <template v-else>
                    <template v-if="reservingIndex === i">
                      <input type="text" style="display:none" v-model="form._hp" tabindex="-1" aria-hidden="true">
                      <div class="hw-geschenk-item__form">
                        <input type="text"  v-model="form.name"  :placeholder="fl[0]" class="hw-input input" required>
                        <input type="email" v-model="form.email" :placeholder="fl[1]" class="hw-input input" required>
                        <p v-if="error" class="hw-geschenk-item__error">{{ error }}</p>
                        <div class="hw-geschenk-item__btns">
                          <button class="hw-btn hw-btn--primary" :disabled="submitting" @click="reserve(i)">
                            {{ submitting ? '…' : fl[2] }}
                          </button>
                          <button class="hw-btn hw-btn--secondary" @click="cancelForm">{{ fl[3] }}</button>
                        </div>
                      </div>
                    </template>
                    <button v-else class="hw-btn hw-btn--primary" @click="openForm(i)">{{ fl[2] }}</button>
                  </template>
                </div>
              </div>
            </li>
          </ul>
        </div>
      </div>
    </section>`,
};

// ── gallery section ───────────────────────────────────────────────────────────
const HwGallerySection = {
    mixins: [sectionBase],
    props: ['section', 'lang', 'gallery'],
    data() { return { lightbox: null }; },
    mounted() {
        if (typeof GLightbox !== 'undefined') {
            this.lightbox = GLightbox({ selector: `.hw-gallery-${this.section?.id} .hw-gallery-link` });
        }
    },
    unmounted() {
        if (this.lightbox) this.lightbox.destroy();
    },
    computed: {
        filteredImages() {
            const images = (this.gallery?.images || []);
            const album  = this.d.album || 'all';
            if (album === 'all') return images;
            return images.filter(img => String(img.album_id) === album);
        },
        sectionStyle()   { return this.d.section_bg_color ? { backgroundColor: this.d.section_bg_color } : {}; },
        sectionPadClass(){ return this.d.section_padding ? `padding-${this.d.section_padding}` : ''; },
        layout()         { return this.d.layout || 'gallery-bottom'; },
        cols()           { return this.d.columns || '3'; },
        gridClass()      { return `grid-${this.cols} grid-8--on-m grid-6--on-s`; },
        introHtml()      { return this.body('intro_text'); },
        downloadHref()   { return `${BASE}/api/gallery-zip?album=${this.d.album || 'all'}`; },
        downloadBtnStyle() {
            const primary = getComputedStyle(document.documentElement).getPropertyValue('--primary').trim() || '#333333';
            return { color: contrastColor(primary) };
        },
        galleryId()      { return `hw-gallery-${this.section?.id}`; },
    },
    template: `
    <section class="hw-section hw-section--gallery" :class="[sectionPadClass, 'hw-gallery-' + (section && section.id)]" :style="sectionStyle">
      <div class="container-l">
        <div :class="layout === 'gallery-bottom' ? null : ['grid-2 gap-l grid-1--on-m', 'hw-gallery--' + layout]">
          <div class="hw-gallery__intro">
            <h2 v-if="t('headline')"
              :class="[align(d.headline_align), headlineSize(d.headline_size)]"
              :style="colorStyle(d.headline_color)"
            >{{ t('headline') }}</h2>
            <div v-if="introHtml" class="hw-gallery__intro-text"
              :class="[align(d.intro_align), d.intro_size ? bodySize(d.intro_size) : '']"
              :style="colorStyle(d.intro_color)"
              v-html="introHtml"
            ></div>
            <a v-if="filteredImages.length" :href="downloadHref" download class="hw-btn hw-btn--primary hw-gallery__dl-btn" :style="downloadBtnStyle">
              <i class="hgi-stroke hgi-download-01"></i> Fotos herunterladen
            </a>
          </div>
          <div v-if="layout !== 'gallery-bottom'" :class="'hw-gallery__grid ' + gridClass">
            <a v-for="image in filteredImages" :key="image.id"
              :href="img(image.file_xl)"
              class="hw-gallery-link"
              data-gallery="gallery"
              data-type="image"
            >
              <img :src="img(image.file_thumb)" :alt="image.id" class="hw-gallery__thumb" loading="lazy">
            </a>
          </div>
        </div>
        <div v-if="layout === 'gallery-bottom'" :class="'hw-gallery__grid ' + gridClass">
          <a v-for="image in filteredImages" :key="image.id"
            :href="img(image.file_xl)"
            class="hw-gallery-link"
            data-gallery="gallery"
            data-type="image"
          >
            <img :src="img(image.file_thumb)" :alt="image.id" class="hw-gallery__thumb" loading="lazy">
          </a>
        </div>
      </div>
    </section>`,
};

// ══════════════════════════════════════════════════════════════════════════════
// SECTION REGISTRY
// ══════════════════════════════════════════════════════════════════════════════
const COMPONENTS = {
    'hero-header':    HwHeroHeader,
    'big-header':     HwBigHeader,
    'intro':          HwIntro,
    'countdown':      HwCountdown,
    'quote':          HwQuote,
    'text-image':     HwTextImage,
    'locations':      HwLocations,
    'witnesses':      HwWitnesses,
    'couple':         HwCouple,
    'program':        HwProgram,
    'accommodation':  HwAccommodation,
    'navigation':     HwNavigationSection,
    'rsvp':           HwRsvp,
    'hochzeits-abc':  HwHochzeitsAbc,
    'kuchenliste':    HwKuchenliste,
    'geschenketisch': HwGeschenketisch,
    'abendprogramm':  HwAbendprogramm,
    'faq':            HwFaq,
    'gallery-section':HwGallerySection,
    'save-the-date':  HwSaveTheDate,
};

// ══════════════════════════════════════════════════════════════════════════════
// MAIN APP
// ══════════════════════════════════════════════════════════════════════════════
createApp({
    setup() {
        const sections        = ref(D.sections || []);
        const lang            = ref(D.lang || 'de');
        const langs           = ref(D.langs || { active: [], default: 'de' });
        const gallery         = ref(D.gallery || { images: [], albums: [] });
        const isEditorPreview = D.isEditorPreview || false;

        // Apply design tokens to :root CSS custom properties
        function applyDesign(d) {
            const r = document.documentElement;
            if (d.color_primary)   r.style.setProperty('--primary',      d.color_primary);
            if (d.color_secondary) r.style.setProperty('--secondary',    d.color_secondary);
            if (d.color_bg)        r.style.setProperty('--bg-body',      d.color_bg);
            if (d.color_headline)  r.style.setProperty('--text-title',   d.color_headline);
            if (d.color_body)      r.style.setProperty('--text-body',    d.color_body);
            if (d.font_heading)    r.style.setProperty('--font-heading', `'${d.font_heading}', Georgia, serif`);
            if (d.font_body)       r.style.setProperty('--font-body',    `'${d.font_body}', system-ui, sans-serif`);
        }

        // postMessage from editor (preview iframe)
        function onMessage(e) {
            const msg = e.data;
            if (!msg || typeof msg !== 'object') return;

            if (msg.type === 'hw-draft-update' && msg.section?.id) {
                const idx = sections.value.findIndex(s => s.id === msg.section.id);
                if (idx >= 0) {
                    sections.value.splice(idx, 1, msg.section);
                }
            }
            if (msg.type === 'hw-design-update' && msg.design) {
                applyDesign(msg.design);
            }
            if (msg.type === 'hw-scroll-to-section' && msg.id) {
                const el = document.querySelector(`[data-section-id="${msg.id}"]`);
                if (el) el.scrollIntoView({ behavior: 'smooth', block: 'center' });
            }
        }

        function resolveComponent(type) {
            return COMPONENTS[type] || null;
        }

        function extraProps(s) {
            if (s.type === 'navigation')      return { sections: sections.value };
            if (s.type === 'gallery-section') return { gallery: gallery.value };
            return {};
        }

        onMounted(()   => window.addEventListener('message', onMessage));
        onUnmounted(() => window.removeEventListener('message', onMessage));

        return { sections, lang, langs, gallery, resolveComponent, extraProps, isEditorPreview };
    },

    template: `
    <hw-lang-switcher :lang="lang" :langs="langs" />
    <template v-for="s in sections" :key="s.id">
      <div v-if="isEditorPreview && !s.visible"
           class="hw-section--hidden-preview"
           :data-section-id="s.id"
           :title="lang === 'en' ? 'This section is not visible for visitors' : 'Diese Sektion ist für Besucher nicht sichtbar'">
        <span class="hw-editor-hidden-badge"
              :title="lang === 'en' ? 'This section is not visible for visitors' : 'Diese Sektion ist für Besucher nicht sichtbar'">
          <i class="hgi-stroke hgi-view-off"></i>
        </span>
        <component
          v-if="resolveComponent(s.type)"
          :is="resolveComponent(s.type)"
          :section="s"
          :lang="lang"
          v-bind="extraProps(s)"
        />
      </div>
      <component
        v-else-if="resolveComponent(s.type)"
        :is="resolveComponent(s.type)"
        :section="s"
        :lang="lang"
        :data-section-id="s.id"
        v-bind="extraProps(s)"
      />
    </template>`,

}).component('hw-lang-switcher', HwLangSwitcher).mount('#hw-app');
