import React from 'react';
import { useNavigate, useParams, useSearchParams } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { ArrowLeft, RefreshCcw, Plus, ChevronDown } from 'lucide-react';
import { YStack, XStack } from '@tamagui/stacks';
import { SizableText as Text } from '@tamagui/text';
import { Pressable } from '@tamagui/react-native-web-lite';
import { Accordion } from '@tamagui/accordion';
import { HexColorPicker } from 'react-colorful';
import { Portal } from '@tamagui/portal';
import { MobileShell, HeaderActionButton } from './components/MobileShell';
import { MobileCard, CTAButton, PillBadge } from './components/Primitives';
import { MobileInput, MobileSelect, MobileTextArea } from './components/FormControls';
import {
  TenantEvent,
  EventQrInvite,
  EventQrInviteLayout,
  TenantFont,
  getEvent,
  getEventQrInvites,
  updateEventQrInvite,
} from '../api';
import { isAuthError } from '../auth/tokens';
import { getApiErrorMessage } from '../lib/apiError';
import { useTenantFonts, ensureFontLoaded } from '../lib/fonts';
import toast from 'react-hot-toast';
import {
  generatePdfBytes,
  generatePngDataUrl,
  triggerDownloadFromBlob,
  triggerDownloadFromDataUrl,
} from './invite-layout/export-utils';
import { LayoutElement, CANVAS_WIDTH, CANVAS_HEIGHT } from './invite-layout/schema';
import { buildInitialTextFields } from './qr/utils';
import { adminPath } from '../constants';
import { useBackNavigation } from './hooks/useBackNavigation';
import { ADMIN_COLORS, ADMIN_GRADIENTS, useAdminTheme } from './theme';

type Step = 'background' | 'text' | 'preview';

const BACKGROUND_PRESETS = [
  {
    id: 'bg-blue-floral',
    src: '/storage/layouts/backgrounds-portrait/bg-blue-floral.png',
    labelKey: 'events.qr.backgroundPresets.blueFloral',
    label: 'Blue Floral',
  },
  {
    id: 'bg-goldframe',
    src: '/storage/layouts/backgrounds-portrait/bg-goldframe.png',
    labelKey: 'events.qr.backgroundPresets.goldFrame',
    label: 'Gold Frame',
  },
  {
    id: 'gr-green-floral',
    src: '/storage/layouts/backgrounds-portrait/gr-green-floral.png',
    labelKey: 'events.qr.backgroundPresets.greenFloral',
    label: 'Green Floral',
  },
];
const DEFAULT_BODY_FONT = 'Manrope';
const DEFAULT_DISPLAY_FONT = 'Archivo Black';

export default function MobileQrLayoutCustomizePage() {
  const { slug: slugParam, tokenId: tokenParam } = useParams<{ slug?: string; tokenId?: string }>();
  const [searchParams] = useSearchParams();
  const slug = slugParam ?? null;
  const tokenId = tokenParam ? Number(tokenParam) : null;
  const layoutParam = searchParams.get('layout');
  const navigate = useNavigate();
  const { t } = useTranslation('management');
  const { textStrong, danger, muted, border, primary, surface, surfaceMuted, overlay, shadow } = useAdminTheme();

  const [event, setEvent] = React.useState<TenantEvent | null>(null);
  const [invite, setInvite] = React.useState<EventQrInvite | null>(null);
  const [selectedLayoutId, setSelectedLayoutId] = React.useState<string | null>(layoutParam);
  const [backgroundPreset, setBackgroundPreset] = React.useState<string | null>(null);
  const [backgroundGradient, setBackgroundGradient] = React.useState<{ angle: number; stops: string[] } | null>(null);
  const [backgroundSolid, setBackgroundSolid] = React.useState<string | null>(null);
  const [textFields, setTextFields] = React.useState({
    headline: '',
    subtitle: '',
    description: '',
    instructions: [''],
  });
  const [slotOverrides, setSlotOverrides] = React.useState<Record<string, Partial<SlotDefinition>>>({});
  const [qrPngUrl, setQrPngUrl] = React.useState<string | null>(null);
  const [step, setStep] = React.useState<Step>('background');
  const [saving, setSaving] = React.useState(false);
  const [loading, setLoading] = React.useState(true);
  const [error, setError] = React.useState<string | null>(null);
  const { fonts: tenantFonts } = useTenantFonts();
  const back = useBackNavigation(slug ? adminPath(`/mobile/events/${slug}/qr`) : adminPath('/mobile/events'));

  const handleSelectPreset = React.useCallback((id: string | null) => {
    setBackgroundPreset(id);
    if (id) {
      setBackgroundGradient(null);
      setBackgroundSolid(null);
    }
  }, []);

  const handleSelectGradient = React.useCallback((gradient: { angle: number; stops: string[] } | null) => {
    setBackgroundGradient(gradient);
    if (gradient) {
      setBackgroundPreset(null);
      setBackgroundSolid(null);
    }
  }, []);

  const handleSelectSolid = React.useCallback((color: string | null) => {
    setBackgroundSolid(color);
    if (color) {
      setBackgroundPreset(null);
      setBackgroundGradient(null);
    }
  }, []);

  const handleUpdateSlot = React.useCallback((slotKey: string, patch: Partial<SlotDefinition>) => {
    setSlotOverrides((prev) => ({
      ...prev,
      [slotKey]: {
        ...(prev[slotKey] ?? {}),
        ...patch,
      },
    }));
  }, []);

  React.useEffect(() => {
    if (!slug) return;
    (async () => {
      setLoading(true);
      try {
        const eventData = await getEvent(slug);
        const invites = await getEventQrInvites(slug);
        const matchedInvite = tokenId ? invites.find((item) => item.id === tokenId) : invites.find((i) => i.is_active) ?? invites[0] ?? null;
        const eventName = renderEventName(eventData?.name);
        setEvent(eventData);
        setInvite(matchedInvite ?? null);
        setQrPngUrl(matchedInvite?.qr_code_data_url ?? null);
        const customization = (matchedInvite?.metadata as any)?.layout_customization ?? {};
        const initialLayoutId = layoutParam ?? customization.layout_id ?? matchedInvite?.layouts?.[0]?.id ?? null;
        const layoutDefaults = matchedInvite?.layouts?.find((l) => l.id === initialLayoutId) ?? matchedInvite?.layouts?.[0] ?? null;
        setBackgroundPreset(typeof customization.background_preset === 'string' ? customization.background_preset : null);
        setSelectedLayoutId(initialLayoutId);
        setBackgroundGradient(customization.background_gradient ?? null);
        setBackgroundSolid(customization.background_color ?? null);
        setTextFields(
          buildInitialTextFields({
            customization,
            layoutDefaults,
            eventName,
          })
        );
        setSlotOverrides(normalizeSlotOverrides((customization as any)?.slots));
        setError(null);
      } catch (err) {
        if (!isAuthError(err)) {
          setError(getApiErrorMessage(err, t('events.errors.loadFailed', 'QR-Daten konnten nicht geladen werden.')));
        }
      } finally {
        setLoading(false);
      }
    })();
  }, [slug, tokenId, layoutParam, t]);

  const selectedLayout = invite?.layouts.find((l) => l.id === selectedLayoutId) ?? invite?.layouts?.[0] ?? null;
  const isFoldable = (selectedLayout?.panel_mode ?? '').toLowerCase() === 'double-mirror';

  const handleSave = async () => {
    if (!slug || !invite || !selectedLayout) {
      toast.error(t('events.qr.missing', 'Kein QR-Link vorhanden'));
      return;
    }
    setSaving(true);
    try {
      const payload = {
        metadata: {
          layout_customization: {
            layout_id: selectedLayout.id,
            background_preset: backgroundPreset,
            background_gradient: backgroundGradient ?? undefined,
            background_color: backgroundSolid ?? undefined,
            headline: textFields.headline || null,
            subtitle: textFields.subtitle || null,
            description: textFields.description || null,
            instructions: textFields.instructions.filter((item) => item.trim().length > 0),
            slots: slotOverrides,
          },
        },
      };
      const updated = await updateEventQrInvite(slug, invite.id, payload);
      setInvite(updated);
      toast.success(t('common.saved', 'Gespeichert'));
    } catch (err) {
      toast.error(getApiErrorMessage(err, t('events.errors.saveFailed', 'Speichern fehlgeschlagen.')));
    } finally {
      setSaving(false);
    }
  };

  return (
    <MobileShell
      activeTab="home"
      title={t('events.qr.customize', 'Layout anpassen')}
      onBack={back}
      headerActions={
        <HeaderActionButton onPress={() => window.location.reload()} ariaLabel={t('common.refresh', 'Refresh')}>
          <RefreshCcw size={18} color={textStrong} />
        </HeaderActionButton>
      }
    >
      {error ? (
        <MobileCard>
          <Text fontWeight="700" color={danger}>
            {error}
          </Text>
        </MobileCard>
      ) : null}

      <Stepper current={step} onStepChange={setStep} />

      <MobileCard space="$2" marginTop="$2">
        {step === 'background' && (
          <BackgroundStep
            onBack={back}
            presets={BACKGROUND_PRESETS}
            selectedPreset={backgroundPreset}
            onSelectPreset={handleSelectPreset}
            selectedLayout={selectedLayout}
            layouts={invite?.layouts ?? []}
            onSelectGradient={handleSelectGradient}
            onSelectSolid={handleSelectSolid}
            selectedGradient={backgroundGradient}
            selectedSolid={backgroundSolid}
            onSave={() => {
              handleSave();
              setStep('text');
            }}
            saving={saving || loading}
          />
        )}

        {step === 'text' && (
          <TextStep
            onBack={() => setStep('background')}
            textFields={textFields}
            onChange={(fields) => setTextFields(fields)}
            onSave={() => {
              handleSave();
              setStep('preview');
            }}
            saving={saving || loading}
          />
        )}

        {step === 'preview' && (
        <PreviewStep
          onBack={() => setStep('text')}
          layout={selectedLayout}
          backgroundPreset={backgroundPreset}
          backgroundGradient={backgroundGradient}
          backgroundSolid={backgroundSolid}
          presets={BACKGROUND_PRESETS}
          textFields={textFields}
          qrUrl={qrPngUrl ?? invite?.url ?? ''}
          qrPngUrl={qrPngUrl}
          isFoldable={isFoldable}
          slotOverrides={slotOverrides}
          onUpdateSlot={handleUpdateSlot}
          tenantFonts={tenantFonts}
          />
        )}
      </MobileCard>
    </MobileShell>
  );
}

function Stepper({ current, onStepChange }: { current: Step; onStepChange: (step: Step) => void }) {
  const { t } = useTranslation('management');
  const { textStrong, muted, border, primary, accentSoft, surface, successText } = useAdminTheme();
  const steps: { key: Step; label: string }[] = [
    { key: 'background', label: t('events.qr.background', 'Hintergrund') },
    { key: 'text', label: t('events.qr.text', 'Text') },
    { key: 'preview', label: t('events.qr.preview', 'Vorschau') },
  ];
  const currentIndex = steps.findIndex((s) => s.key === current);
  const progress = ((currentIndex + 1) / steps.length) * 100;

  return (
    <YStack space="$2" marginTop="$2">
      <XStack space="$2" alignItems="center">
        {steps.map((step, idx) => {
          const active = step.key === current;
          const completed = idx < currentIndex;
          return (
            <Pressable key={step.key} onPress={() => onStepChange(step.key)} style={{ flex: 1 }}>
              <YStack
                borderRadius={12}
                borderWidth={1}
                borderColor={active ? primary : completed ? successText : border}
                backgroundColor={active ? accentSoft : surface}
                padding="$2"
                alignItems="center"
                justifyContent="center"
              >
                <Text fontSize="$sm" fontWeight="700" color={textStrong}>
                  {step.label}
                </Text>
                <Text fontSize="$xs" color={muted}>
                  {t('events.qr.step', 'Schritt')} {idx + 1}
                </Text>
              </YStack>
            </Pressable>
          );
        })}
      </XStack>
      <YStack height={8} borderRadius={999} backgroundColor={border} overflow="hidden">
        <YStack height="100%" width={`${progress}%`} backgroundColor={primary} />
      </YStack>
    </YStack>
  );
}

type SlotDefinition = {
  x: number;
  y: number;
  w: number;
  h?: number;
  fontSize?: number;
  fontWeight?: string | number;
  fontFamily?: string;
  lineHeight?: number;
  align?: 'left' | 'center' | 'right';
  color?: string;
};

type SlotOverrides = Record<string, Partial<SlotDefinition>>;
type PaperSize = 'a4' | 'a5';

const PAPER_MM: Record<PaperSize, { width: number; height: number }> = {
  a4: { width: 210, height: 297 },
  a5: { width: 148, height: 210 },
};

function toNumber(value: unknown): number | undefined {
  if (typeof value === 'number' && Number.isFinite(value)) {
    return value;
  }
  const parsed = typeof value === 'string' ? Number(value) : NaN;
  return Number.isFinite(parsed) ? parsed : undefined;
}

function clampValue(value: number, min: number, max: number): number {
  if (!Number.isFinite(value)) return min;
  return Math.min(Math.max(value, min), max);
}

function normalizeSlotOverrides(raw: unknown): SlotOverrides {
  if (!raw || typeof raw !== 'object') {
    return {};
  }
  return Object.entries(raw as Record<string, unknown>).reduce<SlotOverrides>((acc, [key, value]) => {
    if (!value || typeof value !== 'object') return acc;
    const slot = value as Record<string, unknown>;
    const align = typeof slot.align === 'string' ? (slot.align as SlotDefinition['align']) : undefined;
    acc[key] = {
      x: toNumber(slot.x),
      y: toNumber(slot.y),
      w: toNumber(slot.w),
      h: toNumber(slot.h),
      fontSize: toNumber(slot.fontSize),
      lineHeight: toNumber(slot.lineHeight),
      fontFamily: typeof slot.fontFamily === 'string' ? slot.fontFamily : undefined,
      color: typeof slot.color === 'string' ? slot.color : undefined,
      align,
    };
    return acc;
  }, {});
}

function resolvePaper(layout: EventQrInviteLayout | null): PaperSize {
  const paper = (layout?.paper ?? 'a4').toString().toLowerCase();
  return paper === 'a5' ? 'a5' : 'a4';
}

function resolveCanvasBase(layout: EventQrInviteLayout | null, isFoldable: boolean): { width: number; height: number } {
  const svgWidth = (layout as any)?.preview?.svg?.width;
  const svgHeight = (layout as any)?.preview?.svg?.height;
  if (svgWidth && svgHeight) {
    return { width: svgWidth, height: svgHeight };
  }

  const paper = resolvePaper(layout);
  const orientation = ((layout?.orientation ?? (isFoldable ? 'landscape' : 'portrait')) || '').toString().toLowerCase();
  const mm = PAPER_MM[paper];
  // Keep the existing pixel density consistent with the A4 canvas width.
  const pxPerMm = CANVAS_WIDTH / PAPER_MM.a4.width;
  let width = Math.round(mm.width * pxPerMm);
  let height = Math.round(mm.height * pxPerMm);

  if (orientation === 'landscape') {
    [width, height] = [height, width];
  }

  return { width, height };
}

function renderEventName(name: TenantEvent['name'] | null | undefined): string | null {
  if (!name) return null;
  if (typeof name === 'string' && name.trim()) return name.trim();
  if (typeof name === 'object') {
    const values = Object.values(name).filter((val) => typeof val === 'string' && val.trim()) as string[];
    if (values.length) {
      return values[0].trim();
    }
  }
  return null;
}

function getDefaultSlots(): Record<string, SlotDefinition> {
  return {
    headline: { x: 0.08, y: 0.12, w: 0.84, fontSize: 90, fontWeight: 800, fontFamily: DEFAULT_DISPLAY_FONT, align: 'center' as const },
    subtitle: { x: 0.1, y: 0.21, w: 0.8, fontSize: 37, fontWeight: 600, fontFamily: DEFAULT_BODY_FONT, align: 'center' as const },
    description: { x: 0.1, y: 0.66, w: 0.8, fontSize: 26, fontFamily: DEFAULT_BODY_FONT, lineHeight: 1.4, align: 'center' as const },
    instructions: { x: 0.1, y: 0.7, w: 0.8, fontSize: 24, fontFamily: DEFAULT_BODY_FONT, lineHeight: 1.5 },
    qr: { x: 0.39, y: 0.37, w: 0.27 },
  };
}

function resolveSlots(layout: EventQrInviteLayout | null, isFoldable: boolean, overrides: SlotOverrides = {}): Record<string, SlotDefinition> {
  const fromLayout = (layout as any)?.slots;
  if (fromLayout && typeof fromLayout === 'object') {
    const base = Object.entries(fromLayout as Record<string, unknown>).reduce<Record<string, SlotDefinition>>((acc, [key, value]) => {
      if (value && typeof value === 'object') {
        const slot = value as Record<string, unknown>;
        acc[key] = {
          x: Number(slot.x ?? 0),
          y: Number(slot.y ?? 0),
          w: Number(slot.w ?? 0.8),
          h: typeof slot.h === 'number' ? slot.h : undefined,
          fontSize: typeof slot.fontSize === 'number' ? slot.fontSize : undefined,
          fontWeight: typeof slot.fontWeight === 'string' || typeof slot.fontWeight === 'number' ? slot.fontWeight : undefined,
          fontFamily: typeof slot.fontFamily === 'string' ? slot.fontFamily : undefined,
          lineHeight: typeof slot.lineHeight === 'number' ? slot.lineHeight : undefined,
          align: typeof slot.align === 'string' ? (slot.align as SlotDefinition['align']) : undefined,
        };
      }
      return acc;
    }, {});
    return applySlotOverrides(base, overrides);
  }

  const baseSlots = isFoldable
    ? {
        headline: { x: 0.08, y: 0.15, w: 0.84, fontSize: 50, fontWeight: 800, fontFamily: DEFAULT_DISPLAY_FONT, align: 'center' as const },
        subtitle: { x: 0.1, y: 0.21, w: 0.8, fontSize: 37, fontWeight: 600, fontFamily: DEFAULT_BODY_FONT, align: 'center' as const },
        description: { x: 0.1, y: 0.66, w: 0.8, fontSize: 13, fontFamily: DEFAULT_BODY_FONT, lineHeight: 1.4, align: 'center' as const },
        instructions: { x: 0.12, y: 0.36, w: 0.76, fontSize: 13, fontFamily: DEFAULT_BODY_FONT, lineHeight: 1.3 },
        qr: { x: 0.3, y: 0.3, w: 0.28 },
      }
    : getDefaultSlots();

  return applySlotOverrides(baseSlots, overrides);
}

function applySlotOverrides(baseSlots: Record<string, SlotDefinition>, overrides: SlotOverrides): Record<string, SlotDefinition> {
  const keys = new Set([...Object.keys(baseSlots), ...Object.keys(overrides)]);
  return Array.from(keys).reduce<Record<string, SlotDefinition>>((acc, key) => {
    const base = baseSlots[key] ?? baseSlots.headline ?? { x: 0, y: 0, w: 1 };
    const override = overrides[key] ?? {};
    acc[key] = {
      ...base,
      x: override.x ?? base.x,
      y: override.y ?? base.y,
      w: override.w ?? base.w,
      h: override.h ?? base.h,
      fontSize: override.fontSize ?? base.fontSize,
      fontWeight: override.fontWeight ?? base.fontWeight,
      fontFamily: override.fontFamily ?? base.fontFamily,
      lineHeight: override.lineHeight ?? base.lineHeight,
      align: override.align ?? base.align,
      color: override.color ?? base.color,
    };
    return acc;
  }, {});
}

function buildFabricOptions({
  layout,
  isFoldable,
  backgroundGradient,
  backgroundSolid,
  backgroundImageUrl,
  textFields,
  qrUrl,
  qrPngUrl,
  baseWidth = CANVAS_WIDTH,
  baseHeight = CANVAS_HEIGHT,
  slotOverrides = {},
}: {
  layout: EventQrInviteLayout | null;
  isFoldable: boolean;
  backgroundGradient: { angle: number; stops: string[] } | null;
  backgroundSolid: string | null;
  backgroundImageUrl: string | null;
  textFields: { headline: string; subtitle: string; description: string; instructions: string[] };
  qrUrl: string;
  qrPngUrl?: string | null;
  baseWidth?: number;
  baseHeight?: number;
  slotOverrides?: SlotOverrides;
}) {
  const scaleX = CANVAS_WIDTH / baseWidth;
  const scaleY = CANVAS_HEIGHT / baseHeight;
  const panelWidth = isFoldable ? baseWidth / 2 : baseWidth;
  const panelHeight = baseHeight;
  const slots = resolveSlots(layout, isFoldable, slotOverrides);

  const elements: LayoutElement[] = [];
  const textColor = layout?.preview?.text ?? ADMIN_COLORS.backdrop;
  const accentColor = layout?.preview?.accent ?? ADMIN_COLORS.primary;
  const secondaryColor = (layout?.preview as any)?.secondary ?? ADMIN_COLORS.text;
  const badgeColor = (layout?.preview as any)?.badge ?? accentColor;

  const rotatePoint = (cx: number, cy: number, x: number, y: number, angleDeg: number) => {
    const rad = (angleDeg * Math.PI) / 180;
    const cos = Math.cos(rad);
    const sin = Math.sin(rad);
    const dx = x - cx;
    const dy = y - cy;
    return {
      x: cx + dx * cos - dy * sin,
      y: cy + dx * sin + dy * cos,
    };
  };

  const addTextElement = (
    type: LayoutElement['type'],
    content: string,
    slot: SlotDefinition,
    opts: { panelOffset: number; mirrored: boolean }
  ) => {
    if (!content) return;
    const localWidth = slot.w * panelWidth;
    const localHeight = (slot.h ?? 0.12) * panelHeight;
    const widthPx = localWidth * scaleX;
    const heightPx = localHeight * scaleY;

    // Default: top-left positioning for non-rotated panels
    let targetX = opts.panelOffset + slot.x * panelWidth;
    let targetY = slot.y * panelHeight;
    let rotation = 0;

    // Foldable layouts rotate each panel around its own center; Fabric expects center-based coords when rotated.
    if (isFoldable) {
      const baseCenterX = opts.panelOffset + slot.x * panelWidth + localWidth / 2;
      const baseCenterY = slot.y * panelHeight + localHeight / 2;
      const panelCenterX = opts.panelOffset + panelWidth / 2;
      const panelCenterY = panelHeight / 2;
      rotation = opts.panelOffset === 0 ? 90 : -90;
      const rotated = rotatePoint(panelCenterX, panelCenterY, baseCenterX, baseCenterY, rotation);
      targetX = rotated.x;
      targetY = rotated.y;
    }

    elements.push({
      id: `${type}-${opts.panelOffset}-${opts.mirrored ? 'm' : 'n'}`,
      type,
      x: targetX * scaleX,
      y: targetY * scaleY,
      width: widthPx,
      height: heightPx,
      fontSize: slot.fontSize ?? (type === 'headline' ? 30 : type === 'subtitle' ? 18 : 16),
      align: slot.align ?? 'center',
      lineHeight: slot.lineHeight ?? 1.35,
      content,
      fill: slot.color ?? textColor,
      fontFamily: slot.fontFamily,
      rotation,
    });
  };

  const addQr = (slot: SlotDefinition, opts: { panelOffset: number; mirrored: boolean }) => {
    if (!qrUrl) return;
    const localSize = slot.w * panelWidth;
    const sizePx = localSize * scaleX;
    let targetX = opts.panelOffset + slot.x * panelWidth;
    let targetY = slot.y * panelHeight;
    let rotation = 0;

    if (isFoldable) {
      const baseCenterX = opts.panelOffset + slot.x * panelWidth + localSize / 2;
      const baseCenterY = slot.y * panelHeight + localSize / 2;
      const panelCenterX = opts.panelOffset + panelWidth / 2;
      const panelCenterY = panelHeight / 2;
      rotation = opts.panelOffset === 0 ? 90 : -90;
      const rotated = rotatePoint(panelCenterX, panelCenterY, baseCenterX, baseCenterY, rotation);
      targetX = rotated.x;
      targetY = rotated.y;
    }

    elements.push({
      id: `qr-${opts.panelOffset}-${opts.mirrored ? 'm' : 'n'}`,
      type: 'qr',
      x: targetX * scaleX,
      y: targetY * scaleY,
      width: sizePx,
      height: sizePx,
      rotation,
    });
  };

  const instructions = textFields.instructions.filter((i) => i.trim().length > 0);
  const instructionContent = instructions.map((line, idx) => `${idx + 1}. ${line}`).join('\n');

  const drawPanelElements = (panelOffset: number, mirrored: boolean) => {
    addTextElement('headline', textFields.headline, slots.headline, { panelOffset, mirrored });
    addTextElement('subtitle', textFields.subtitle, slots.subtitle, { panelOffset, mirrored });
    addTextElement('description', textFields.description, slots.description, { panelOffset, mirrored });
    if (instructionContent) {
      addTextElement('description', instructionContent, slots.instructions, { panelOffset, mirrored });
    }
    addQr(slots.qr, { panelOffset, mirrored });
  };

  drawPanelElements(0, false);
  if (isFoldable) {
    drawPanelElements(panelWidth, true);
  }

  return {
    elements,
    accentColor,
    textColor,
    secondaryColor,
    badgeColor,
    qrCodeDataUrl: qrPngUrl ?? (qrUrl ? `https://api.qrserver.com/v1/create-qr-code/?size=400x400&data=${encodeURIComponent(qrUrl)}` : null),
    logoDataUrl: null,
    backgroundColor: backgroundSolid ?? layout?.preview?.background ?? ADMIN_COLORS.surface,
    backgroundGradient,
    backgroundImageUrl,
    readOnly: true,
  } as const;
}

function BackgroundStep({
  onBack,
  presets,
  selectedPreset,
  onSelectPreset,
  selectedLayout,
  layouts,
  onSelectGradient,
  onSelectSolid,
  selectedGradient,
  selectedSolid,
  onSave,
  saving,
}: {
  onBack: () => void;
  presets: { id: string; src: string; labelKey: string; label: string }[];
  selectedPreset: string | null;
  onSelectPreset: (id: string) => void;
  selectedLayout: EventQrInviteLayout | null;
  layouts: EventQrInviteLayout[];
  onSelectGradient: (gradient: { angle: number; stops: string[] } | null) => void;
  onSelectSolid: (color: string | null) => void;
  selectedGradient: { angle: number; stops: string[] } | null;
  selectedSolid: string | null;
  onSave: () => void;
  saving: boolean;
}) {
  const { t } = useTranslation('management');
  const { textStrong, muted, border, primary, accentSoft, surface, surfaceMuted } = useAdminTheme();
  const resolvedLayout =
    selectedLayout ??
    layouts.find((layout) => {
      const panel = (layout.panel_mode ?? '').toLowerCase();
      const orientation = (layout.orientation ?? '').toLowerCase();
      return panel === 'double-mirror' || orientation === 'landscape';
    }) ??
    layouts[0] ??
    null;

  const isFoldable = (resolvedLayout?.panel_mode ?? '').toLowerCase() === 'double-mirror';
  const formatPaper = resolvePaper(resolvedLayout).toUpperCase();
  const isLandscape = ((resolvedLayout?.orientation ?? (isFoldable ? 'landscape' : 'portrait')) || '').toLowerCase() === 'landscape';
  const orientationLabel = isLandscape
    ? t('events.qr.orientation.landscape', 'Landscape')
    : t('events.qr.orientation.portrait', 'Portrait');
  const formatLabel = isFoldable
    ? t('events.qr.formatLabel.foldable', '{{paper}} {{orientation}} (double A5/mirrored)', {
        paper: formatPaper,
        orientation: orientationLabel,
      })
    : t('events.qr.formatLabel.standard', '{{paper}} {{orientation}}', {
        paper: formatPaper,
        orientation: orientationLabel,
      });
  const disablePresets = isFoldable;
  const gradientPresets = [
    {
      angle: 180,
      stops: ['#F8FAFC', '#EEF2FF', '#F8FAFC'],
      labelKey: 'events.qr.gradientPresets.softLilac',
      label: 'Soft Lilac',
    },
    {
      angle: 135,
      stops: ['#FEE2E2', '#EFF6FF', '#ECFDF3'],
      labelKey: 'events.qr.gradientPresets.pastel',
      label: 'Pastel',
    },
    {
      angle: 210,
      stops: ['#0B132B', '#1C2541', '#274690'],
      labelKey: 'events.qr.gradientPresets.midnight',
      label: 'Midnight',
    },
  ];
  const solidPresets = [
    ADMIN_COLORS.surface,
    ADMIN_COLORS.surfaceMuted,
    ADMIN_COLORS.backdrop,
    ADMIN_COLORS.primary,
    ADMIN_COLORS.warning,
  ];

  return (
    <YStack space="$3" marginTop="$2">
      <XStack alignItems="center" space="$2">
        <Pressable onPress={onBack}>
          <XStack alignItems="center" space="$1">
            <ArrowLeft size={16} color={textStrong} />
            <Text fontSize="$sm" color={textStrong}>
              {t('common.back', 'Zurück')}
            </Text>
          </XStack>
        </Pressable>
        <PillBadge tone="muted">{formatLabel}</PillBadge>
      </XStack>

      <YStack space="$2">
        <Text fontSize="$sm" fontWeight="700" color={textStrong}>
          {disablePresets
            ? t('events.qr.backgroundPickerFoldable', 'Hintergrund für A5 (Gradient/Farbe)')
            : t('events.qr.backgroundPicker', 'Hintergrund auswählen ({{formatLabel}})', { formatLabel })}
        </Text>
        {!disablePresets ? (
          <>
            <XStack flexWrap="wrap" gap="$2">
              {presets.map((preset) => {
                const isSelected = selectedPreset === preset.id;
                return (
                  <Pressable key={preset.id ?? preset.labelKey} onPress={() => onSelectPreset(preset.id)} style={{ width: '48%' }}>
                    <YStack
                      aspectRatio={210 / 297}
                      maxHeight={220}
                      borderRadius={14}
                      overflow="hidden"
                      borderWidth={isSelected ? 2 : 1}
                      borderColor={isSelected ? primary : border}
                      backgroundColor={surfaceMuted}
                    >
                      <YStack
                        flex={1}
                        backgroundImage={`url(${preset.src})`}
                        backgroundSize="cover"
                        backgroundPosition="center"
                      />
                      <XStack padding="$2" justifyContent="space-between" alignItems="center" backgroundColor="rgba(255,255,255,0.92)">
                        <Text fontSize="$xs" color={textStrong}>
                          {t(preset.labelKey, preset.label)}
                        </Text>
                        {isSelected ? <PillBadge tone="muted">{t('common.selected', 'Ausgewählt')}</PillBadge> : null}
                      </XStack>
                    </YStack>
                  </Pressable>
                );
              })}
            </XStack>
            <Text fontSize="$xs" color={muted}>
              {t('events.qr.backgroundNote', 'Diese Presets sind für A4 Hochformat. Spiegelung erfolgt automatisch bei Tischkarten.')}
            </Text>
          </>
        ) : (
          <Text fontSize="$xs" color={muted}>
            {t('events.qr.foldableBackgroundNote', 'Für A5 Tischkarten bitte Gradient oder Vollfarbe wählen.')}
          </Text>
        )}

        <YStack space="$2" marginTop="$2">
        <Text fontSize="$sm" fontWeight="700" color={textStrong}>
          {t('events.qr.gradients', 'Gradienten')}
        </Text>
          <XStack flexWrap="wrap" gap="$2">
            {gradientPresets.map((gradient) => {
              const isSelected =
                selectedGradient &&
                selectedGradient.angle === gradient.angle &&
                JSON.stringify(selectedGradient.stops) === JSON.stringify(gradient.stops);
              const style = {
                backgroundImage: `linear-gradient(${gradient.angle}deg, ${gradient.stops.join(',')})`,
              } as React.CSSProperties;
              const key = `${gradient.labelKey}-${gradient.angle}`;
              return (
                <Pressable
                  key={key}
                  onPress={() => onSelectGradient(gradient)}
                  style={{ width: '30%' }}
                  aria-label={t(gradient.labelKey, gradient.label)}
                >
                  <YStack
                    height={70}
                    borderRadius={12}
                    overflow="hidden"
                    borderWidth={isSelected ? 2 : 1}
                    borderColor={isSelected ? primary : border}
                    style={style}
                  />
                </Pressable>
              );
            })}
          </XStack>
        </YStack>

        <YStack space="$2" marginTop="$2">
          <Text fontSize="$sm" fontWeight="700" color={textStrong}>
            {t('events.qr.colors', 'Vollfarbe')}
          </Text>
          <XStack flexWrap="wrap" gap="$2">
            {solidPresets.map((color, idx) => {
              const isSelected = selectedSolid === color;
              const key = `${color}-${idx}`;
              return (
                <Pressable key={key} onPress={() => onSelectSolid(color)} style={{ width: '20%' }}>
                  <YStack
                    height={50}
                    borderRadius={12}
                    overflow="hidden"
                    borderWidth={isSelected ? 2 : 1}
                    borderColor={isSelected ? primary : border}
                    backgroundColor={color}
                  />
                </Pressable>
              );
            })}
          </XStack>
        </YStack>
      </YStack>

      <CTAButton
        label={saving ? t('common.saving', 'Speichern …') : t('common.next', 'Weiter')}
        disabled={saving || !resolvedLayout}
        onPress={onSave}
      />
    </YStack>
  );
}

function TextStep({
  onBack,
  textFields,
  onChange,
  onSave,
  onBulkAdd,
  saving,
}: {
  onBack: () => void;
  textFields: { headline: string; subtitle: string; description: string; instructions: string[] };
  onChange: (fields: { headline: string; subtitle: string; description: string; instructions: string[] }) => void;
  onSave: () => void;
  onBulkAdd?: () => void;
  saving: boolean;
}) {
  const { t } = useTranslation('management');
  const { textStrong, border, surface, muted } = useAdminTheme();

  const updateField = (key: 'headline' | 'subtitle' | 'description', value: string) => {
    onChange({ ...textFields, [key]: value });
  };

  const updateInstruction = (idx: number, value: string) => {
    const next = [...textFields.instructions];
    next[idx] = value;
    onChange({ ...textFields, instructions: next });
  };

  const addInstruction = () => {
    onChange({ ...textFields, instructions: [...textFields.instructions, ''] });
  };

  const removeInstruction = (idx: number) => {
    const next = textFields.instructions.filter((_, i) => i !== idx);
    onChange({ ...textFields, instructions: next.length ? next : [''] });
  };

  return (
    <YStack space="$3" marginTop="$2">
      <XStack alignItems="center" space="$2">
        <Pressable onPress={onBack}>
          <XStack alignItems="center" space="$1">
            <ArrowLeft size={16} color={textStrong} />
            <Text fontSize="$sm" color={textStrong}>
              {t('common.back', 'Zurück')}
            </Text>
          </XStack>
        </Pressable>
        <PillBadge tone="muted">{t('events.qr.textStep', 'Texte & Hinweise')}</PillBadge>
      </XStack>

      <YStack space="$2">
        <Text fontSize="$sm" fontWeight="700" color={textStrong}>
          {t('events.qr.textFields', 'Texte')}
        </Text>
        <MobileInput
          placeholder={t('events.qr.headline', 'Headline')}
          value={textFields.headline}
          onChange={(event) => updateField('headline', event.target.value)}
        />
        <MobileInput
          placeholder={t('events.qr.subtitle', 'Subtitle')}
          value={textFields.subtitle}
          onChange={(event) => updateField('subtitle', event.target.value)}
        />
        <MobileTextArea
          placeholder={t('events.qr.bottomNote', 'Unterer Hinweistext')}
          value={textFields.description}
          onChange={(event) => updateField('description', event.target.value)}
        />
      </YStack>

      <YStack space="$2">
        <Text fontSize="$sm" fontWeight="700" color={textStrong}>
          {t('events.qr.instructions', 'Anleitung')}
        </Text>
        {textFields.instructions.map((item, idx) => (
          <XStack key={idx} alignItems="center" space="$2">
            <MobileTextArea
              value={item}
              onChange={(event) => updateInstruction(idx, event.target.value)}
              placeholder={t('events.qr.instructionsPlaceholder', 'Schritt hinzufügen')}
              style={{ flex: 1 }}
              compact
            />
            <Pressable onPress={() => removeInstruction(idx)} disabled={textFields.instructions.length === 1}>
              <XStack
                width={44}
                height={44}
                borderRadius={12}
                alignItems="center"
                justifyContent="center"
                borderWidth={1}
                borderColor={border}
                backgroundColor={surface}
              >
                <Text fontSize="$md" fontWeight="800" color={textStrong}>
                  –
                </Text>
              </XStack>
            </Pressable>
            {idx === textFields.instructions.length - 1 ? (
              <Pressable onPress={addInstruction}>
                <XStack
                  width={44}
                  height={44}
                  borderRadius={12}
                  alignItems="center"
                  justifyContent="center"
                  borderWidth={1}
                  borderColor={border}
                  backgroundColor={surface}
                >
                  <Plus size={18} color={textStrong} />
                </XStack>
              </Pressable>
            ) : null}
          </XStack>
        ))}
      </YStack>

      <CTAButton
        label={saving ? t('common.saving', 'Speichern …') : t('events.qr.preview', 'Vorschau')}
        disabled={saving}
        onPress={onSave}
      />
    </YStack>
  );
}

function PreviewStep({
  onBack,
  layout,
  backgroundPreset,
  backgroundGradient,
  backgroundSolid,
  presets,
  textFields,
  qrUrl,
  qrPngUrl,
  isFoldable,
  slotOverrides,
  onUpdateSlot,
  tenantFonts,
}: {
  onBack: () => void;
  layout: EventQrInviteLayout | null;
  backgroundPreset: string | null;
  backgroundGradient: { angle: number; stops: string[] } | null;
  backgroundSolid: string | null;
  presets: { id: string; src: string; labelKey: string; label: string }[];
  textFields: { headline: string; subtitle: string; description: string; instructions: string[] };
  qrUrl: string;
  qrPngUrl?: string | null;
  isFoldable: boolean;
  slotOverrides: SlotOverrides;
  onUpdateSlot: (slot: string, patch: Partial<SlotDefinition>) => void;
  tenantFonts: TenantFont[];
}) {
  const { t } = useTranslation('management');
  const { textStrong, muted, border, surface } = useAdminTheme();
  const presetSrc = backgroundPreset ? presets.find((p) => p.id === backgroundPreset)?.src ?? null : null;
  const resolvedBgGradient =
    backgroundGradient ??
    layout?.preview?.background_gradient ??
    (layout?.preview?.background
      ? { angle: 180, stops: [layout.preview.background, layout.preview.background, layout.preview.background] }
      : null);
  const resolvedBgSolid = backgroundSolid ?? layout?.preview?.background ?? null;
  const backgroundImageUrl = presetSrc ?? null;
  const canvasBase = React.useMemo(() => resolveCanvasBase(layout, isFoldable), [layout, isFoldable]);
  const resolvedSlots = React.useMemo(() => resolveSlots(layout, isFoldable, slotOverrides), [isFoldable, layout, slotOverrides]);
  const exportOptions = React.useMemo(() => {
    return buildFabricOptions({
      layout,
      isFoldable,
      backgroundGradient: resolvedBgGradient,
      backgroundSolid: resolvedBgSolid,
      backgroundImageUrl,
      textFields,
      qrUrl,
      qrPngUrl,
      baseWidth: canvasBase.width,
      baseHeight: canvasBase.height,
      slotOverrides,
    });
  }, [backgroundImageUrl, canvasBase.height, canvasBase.width, isFoldable, layout, qrPngUrl, qrUrl, resolvedBgGradient, resolvedBgSolid, slotOverrides, textFields]);

  const [previewUrl, setPreviewUrl] = React.useState<string | null>(null);
  const [previewLoading, setPreviewLoading] = React.useState(false);
  const loadFonts = React.useCallback(async () => {
    const families = Array.from(
      new Set(
        Object.values(resolvedSlots)
          .map((slot) => slot.fontFamily)
          .filter(Boolean) as string[]
      )
    );
    for (const family of families) {
      const font = tenantFonts.find((item) => item.family === family);
      if (font) {
        await ensureFontLoaded(font);
      } else if (typeof document !== 'undefined' && document.fonts?.load) {
        await document.fonts.load(`400 1rem "${family}"`);
      }
    }
  }, [resolvedSlots, tenantFonts]);

  React.useEffect(() => {
    let cancelled = false;
    async function renderPreview() {
      setPreviewLoading(true);
      try {
        await loadFonts();
        const url = await generatePngDataUrl(exportOptions, 1);
        if (!cancelled) {
          setPreviewUrl(url);
        }
      } catch (error) {
        console.error('[QR Preview] failed to render', error);
        if (!cancelled) {
          setPreviewUrl(null);
        }
      } finally {
        if (!cancelled) {
          setPreviewLoading(false);
        }
      }
    }
    renderPreview();
    return () => {
      cancelled = true;
    };
  }, [exportOptions, loadFonts]);

  const aspectRatio = `${canvasBase.width}/${canvasBase.height}`;
  const paper = resolvePaper(layout);
  const orientation = ((layout?.orientation ?? (isFoldable ? 'landscape' : 'portrait')) || '').toString().toLowerCase();
  const isLandscape = orientation === 'landscape';
  const orientationLabel = isLandscape
    ? t('events.qr.orientation.landscape', 'Landscape')
    : t('events.qr.orientation.portrait', 'Portrait');
  const qrImageSrc = qrPngUrl ?? qrUrl ?? '';

  return (
    <YStack space="$3" marginTop="$2">
      <XStack alignItems="center" space="$2">
        <Pressable onPress={onBack}>
          <XStack alignItems="center" space="$1">
            <ArrowLeft size={16} color={textStrong} />
            <Text fontSize="$sm" color={textStrong}>
              {t('common.back', 'Zurück')}
            </Text>
          </XStack>
        </Pressable>
        <PillBadge tone="muted">
          {paper.toUpperCase()} {orientationLabel}
        </PillBadge>
      </XStack>

      <YStack space="$2">
        <Text fontSize="$sm" fontWeight="700" color={textStrong}>
          {t('events.qr.preview', 'Vorschau')}
        </Text>
        <YStack
          width="100%"
          maxWidth={420}
          borderRadius={16}
          overflow="hidden"
          borderWidth={1}
          borderColor={border}
          backgroundColor={surface}
          style={{ aspectRatio }}
          alignItems="center"
          justifyContent="center"
        >
          {previewLoading ? (
            <Text color={muted}>{t('common.loading', 'Lädt Vorschau …')}</Text>
          ) : previewUrl ? (
            <img
              src={previewUrl}
              alt={t('events.qr.previewAlt', 'QR layout preview')}
              style={{ width: '100%', height: '100%', objectFit: 'cover' }}
            />
          ) : (
            <Text color={muted}>{t('events.qr.missing', 'Kein QR-Link vorhanden')}</Text>
          )}
        </YStack>
      </YStack>

      <LayoutControls slots={resolvedSlots} slotOverrides={slotOverrides} onUpdateSlot={onUpdateSlot} tenantFonts={tenantFonts} qrUrl={qrImageSrc} />

      <XStack space="$2" width="100%" flexWrap="wrap">
        <CTAButton
          label={t('events.qr.exportPdf', 'Export PDF')}
          onPress={async () => {
            try {
              await loadFonts();
              const pdfBytes = await generatePdfBytes(exportOptions, paper, orientation);
              const blob = new Blob([pdfBytes as any], { type: 'application/pdf' });
              triggerDownloadFromBlob(blob, 'qr-layout.pdf');
            } catch (err) {
              toast.error(t('events.qr.exportFailed', 'Export fehlgeschlagen'));
              console.error(err);
            }
          }}
          style={{ flex: 1, minWidth: 0 } as any}
        />
        <CTAButton
          label={t('events.qr.exportPng', 'Export PNG')}
          onPress={async () => {
            try {
              await loadFonts();
              const dataUrl = await generatePngDataUrl(exportOptions);
              await triggerDownloadFromDataUrl(dataUrl, 'qr-layout.png');
            } catch (err) {
              toast.error(t('events.qr.exportFailed', 'Export fehlgeschlagen'));
              console.error(err);
            }
          }}
          style={{ flex: 1, minWidth: 0 } as any}
        />
      </XStack>
    </YStack>
  );
}

function LayoutControls({
  slots,
  slotOverrides,
  onUpdateSlot,
  tenantFonts,
  qrUrl,
}: {
  slots: Record<string, SlotDefinition>;
  slotOverrides: SlotOverrides;
  onUpdateSlot: (slot: string, patch: Partial<SlotDefinition>) => void;
  tenantFonts: TenantFont[];
  qrUrl: string | null;
}) {
  const { t } = useTranslation('management');
  const [openColorSlot, setOpenColorSlot] = React.useState<string | null>(null);
  const { border, surface, surfaceMuted, textStrong, muted, overlay, shadow, danger } = useAdminTheme();
  const fontOptions = React.useMemo(() => {
    const preset = ['Archivo Black', 'Manrope', 'Inter', 'Roboto', 'Lora'];
    const tenant = tenantFonts.map((font) => font.family);
    return Array.from(new Set([...tenant, ...preset]));
  }, [tenantFonts]);

  const StepperInput = ({
    value,
    min,
    max,
    step,
    onChange,
  }: {
    value: number;
    min: number;
    max: number;
    step: number;
    onChange: (value: number) => void;
  }) => {
    const dec = () => onChange(clampValue(value - step, min, max));
    const inc = () => onChange(clampValue(value + step, min, max));
    const decimals = step % 1 === 0 ? 0 : Math.min(4, `${step}`.split('.')[1]?.length ?? 1);
    const formatValue = (val: number) => val.toFixed(decimals);
    return (
      <XStack space="$1" alignItems="center">
        <Pressable onPress={dec}>
          <XStack width={36} height={36} borderRadius={10} alignItems="center" justifyContent="center" borderWidth={1} borderColor={border} backgroundColor={surface}>
            <Text fontSize="$md" fontWeight="800" color={textStrong}>
              –
            </Text>
          </XStack>
        </Pressable>
        <MobileInput
          type="text"
          inputMode="decimal"
          value={formatValue(value)}
          onChange={(event) => {
            const next = Number(event.target.value);
            if (Number.isFinite(next)) {
              onChange(next);
            }
          }}
          compact
          style={{
            width: 90,
            textAlign: 'center',
            fontFamily: DEFAULT_BODY_FONT,
            backgroundColor: surfaceMuted,
          }}
        />
        <Pressable onPress={inc}>
          <XStack width={36} height={36} borderRadius={10} alignItems="center" justifyContent="center" borderWidth={1} borderColor={border} backgroundColor={surface}>
            <Text fontSize="$md" fontWeight="800" color={textStrong}>
              +
            </Text>
          </XStack>
        </Pressable>
      </XStack>
    );
  };

  const renderTextSlot = (slotKey: string, label: string) => {
    const slot = slots[slotKey];
    if (!slot) return null;
    const override = slotOverrides[slotKey] ?? {};

    const onPercentChange = (field: 'x' | 'y' | 'w') => (value: number) => {
      const pct = clampValue(value, 0, 100);
      onUpdateSlot(slotKey, { [field]: pct / 100 });
    };

    const onFontSizeChange = (value: number) => {
      const next = clampValue(value, 8, 200);
      onUpdateSlot(slotKey, { fontSize: next });
    };

    const onLineHeightChange = (value: number) => {
      const next = clampValue(value, 0.8, 3);
      onUpdateSlot(slotKey, { lineHeight: next });
    };

    const onFontFamilyChange = (value: string) => {
      onUpdateSlot(slotKey, { fontFamily: value || undefined });
    };

    const currentX = override.x ?? slot.x;
    const currentY = override.y ?? slot.y;
    const currentW = override.w ?? slot.w;
    const centerX = () => {
      const centered = clampValue((1 - currentW) / 2, 0, 1);
      onUpdateSlot(slotKey, { x: centered });
    };

    return (
      <Accordion.Item value={slotKey} key={slotKey}>
        <Accordion.Trigger {...({ padding: "$2", borderWidth: 1, borderColor: border, borderRadius: 12, backgroundColor: surfaceMuted } as any)}>
          <XStack justifyContent="space-between" alignItems="center" flex={1}>
            <Text fontSize="$sm" fontWeight="700" color={textStrong}>
              {label}
            </Text>
            <ChevronDown size={16} color={muted} />
          </XStack>
        </Accordion.Trigger>
        <Accordion.Content {...({ paddingTop: "$2" } as any)}>
          <YStack space="$2" padding="$2" borderWidth={1} borderColor={border} borderRadius={12}>
            <XStack space="$3">
              <YStack flex={1} space="$1">
                <Text fontSize="$xs" color={muted}>
                  {t('events.qr.positionX', 'X (%)')}
                </Text>
                <XStack space="$2" alignItems="center">
                  <StepperInput
                    value={currentX * 100}
                    min={0}
                    max={100}
                    step={0.5}
                    onChange={(val) => onPercentChange('x')(val)}
                  />
                  <Pressable onPress={centerX}>
                    <XStack
                      paddingHorizontal="$3"
                      paddingVertical="$2"
                      borderRadius={10}
                      borderWidth={1}
                      borderColor={border}
                      backgroundColor={surface}
                    >
                      <Text fontSize="$xs" color={textStrong}>
                        {t('common.center', 'Zentrieren')}
                      </Text>
                    </XStack>
                  </Pressable>
                </XStack>
              </YStack>
              <YStack flex={1} space="$1">
                <Text fontSize="$xs" color={muted}>
                  {t('events.qr.positionY', 'Y (%)')}
                </Text>
                <StepperInput
                  value={currentY * 100}
                  min={0}
                  max={100}
                  step={0.5}
                  onChange={(val) => onPercentChange('y')(val)}
                />
              </YStack>
              <YStack flex={1} space="$1">
                <Text fontSize="$xs" color={muted}>
                  {t('events.qr.width', 'Breite (%)')}
                </Text>
                <StepperInput
                  value={currentW * 100}
                  min={10}
                  max={100}
                  step={0.5}
                  onChange={(val) => onPercentChange('w')(val)}
                />
              </YStack>
            </XStack>

            <XStack space="$3">
              <YStack flex={1} space="$1">
                <Text fontSize="$xs" color={muted}>
                  {t('events.qr.fontSize', 'Font Size (px)')}
                </Text>
                <StepperInput value={override.fontSize ?? slot.fontSize ?? 16} min={8} max={200} step={1} onChange={(val) => onFontSizeChange(val)} />
              </YStack>
              <YStack flex={1} space="$1">
                <Text fontSize="$xs" color={muted}>
                  {t('events.qr.fontFamily', 'Font Family')}
                </Text>
                <MobileSelect
                  value={override.fontFamily ?? slot.fontFamily ?? ''}
                  onChange={(event) => onFontFamilyChange(event.target.value)}
                >
                  <option value="">{t('events.qr.defaultFont', 'Standard')}</option>
                  {fontOptions.map((family) => (
                    <option key={family} value={family}>
                      {family}
                    </option>
                  ))}
                </MobileSelect>
              </YStack>
              <YStack flex={1} space="$1">
                <Text fontSize="$xs" color={muted}>
                  {t('events.qr.fontColor', 'Schriftfarbe')}
                </Text>
                <YStack space="$2">
                  <XStack space="$2" alignItems="center">
                    <Pressable onPress={() => setOpenColorSlot(openColorSlot === slotKey ? null : slotKey)}>
                      <XStack
                        width={48}
                        height={36}
                        borderRadius={10}
                        borderWidth={1}
                        borderColor={border}
                        backgroundColor={override.color ?? slot.color ?? textStrong}
                      />
                    </Pressable>
                    <MobileInput
                      value={override.color ?? slot.color ?? ''}
                      placeholder={ADMIN_COLORS.text}
                      onChange={(event) => onUpdateSlot(slotKey, { color: event.target.value })}
                      compact
                      style={{ width: 110, backgroundColor: surfaceMuted }}
                    />
                  </XStack>
                  {openColorSlot === slotKey ? (
                    <Portal>
                      <YStack
                        position="fixed"
                        top={0}
                        left={0}
                        right={0}
                        bottom={0}
                        alignItems="center"
                        justifyContent="center"
                        backgroundColor={overlay}
                        zIndex={9999}
                        onPress={() => setOpenColorSlot(null)}
                      >
                        <YStack
                          padding="$3"
                          borderRadius={16}
                          backgroundColor={surface}
                          borderWidth={1}
                          borderColor={border}
                          elevation="$4"
                          shadowColor={shadow}
                          shadowOffset={{ width: 0, height: 4 }}
                          shadowOpacity={0.2}
                          shadowRadius={12}
                          gap="$2"
                        >
                          <Text fontSize="$sm" fontWeight="700" color={textStrong}>
                            {t('events.qr.fontColor', 'Schriftfarbe')}
                          </Text>
                          <HexColorPicker
                            color={override.color ?? slot.color ?? ADMIN_COLORS.text}
                            onChange={(val) => onUpdateSlot(slotKey, { color: val })}
                            style={{ width: 240, height: 200 }}
                          />
                          <XStack space="$2" justifyContent="flex-end">
                            <Pressable onPress={() => setOpenColorSlot(null)}>
                              <XStack
                                paddingHorizontal="$3"
                                paddingVertical="$2"
                                borderRadius={10}
                                borderWidth={1}
                                borderColor={border}
                                backgroundColor={surface}
                              >
                                <Text fontSize="$xs" color={textStrong}>
                                  {t('common.close', 'Schließen')}
                                </Text>
                              </XStack>
                            </Pressable>
                          </XStack>
                        </YStack>
                      </YStack>
                    </Portal>
                  ) : null}
                </YStack>
              </YStack>
            </XStack>

            <XStack space="$2">
              <YStack flex={1} space="$1">
                <Text fontSize="$xs" color={muted}>
                  {t('events.qr.align', 'Align')}
                </Text>
                <MobileSelect
                  value={override.align ?? slot.align ?? 'left'}
                  onChange={(event) => onUpdateSlot(slotKey, { align: event.target.value as SlotDefinition['align'] })}
                >
                  <option value="left">{t('common.left', 'Links')}</option>
                  <option value="center">{t('common.center', 'Zentriert')}</option>
                  <option value="right">{t('common.right', 'Rechts')}</option>
                </MobileSelect>
              </YStack>
              <YStack flex={1} space="$1">
                <Text fontSize="$xs" color={muted}>
                  {t('events.qr.lineHeight', 'Line Height')}
                </Text>
                <StepperInput
                  value={override.lineHeight ?? slot.lineHeight ?? 1.35}
                  min={0.8}
                  max={3}
                  step={0.05}
                  onChange={(val) => onLineHeightChange(val)}
                />
              </YStack>
              <YStack flex={1} />
              <YStack flex={1} />
            </XStack>
          </YStack>
        </Accordion.Content>
      </Accordion.Item>
    );
  };

  const qrSlot = slots.qr;
  const qrOverride = slotOverrides.qr ?? {};

  const onQrPercentChange = (field: 'x' | 'y' | 'w') => (value: number) => {
    const pct = clampValue(value, 0, 100);
    onUpdateSlot('qr', { [field]: pct / 100 });
  };

  const accordionDefaults = ['headline'];

  return (
    <YStack space="$3" marginTop="$2">
      <Text fontSize="$sm" fontWeight="700" color={textStrong}>
        {t('events.qr.layoutControls', 'Layout & Schrift')}
      </Text>
      <Accordion type="multiple" defaultValue={accordionDefaults}>
        {renderTextSlot('headline', t('events.qr.headline', 'Headline'))}
        {renderTextSlot('subtitle', t('events.qr.subtitle', 'Subtitle'))}
        {renderTextSlot('description', t('events.qr.bottomNote', 'Unterer Hinweistext'))}
        {renderTextSlot('instructions', t('events.qr.instructions', 'Anleitung'))}

        {qrSlot ? (
          <Accordion.Item value="qr">
            <Accordion.Trigger {...({ padding: "$2", borderWidth: 1, borderColor: border, borderRadius: 12, backgroundColor: surfaceMuted } as any)}>
              <XStack justifyContent="space-between" alignItems="center" flex={1}>
                <Text fontSize="$sm" fontWeight="700" color={textStrong}>
                  {t('events.qr.qr_code_label', 'QR‑Code')}
                </Text>
                <ChevronDown size={16} color={muted} />
              </XStack>
            </Accordion.Trigger>
            <Accordion.Content {...({ paddingTop: "$2" } as any)}>
              <YStack space="$2" padding="$2" borderWidth={1} borderColor={border} borderRadius={12}>
                <XStack space="$2">
                  <YStack flex={1} space="$1">
                    <Text fontSize="$xs" color={muted}>
                      {t('events.qr.positionX', 'X (%)')}
                    </Text>
                    <StepperInput
                      value={(qrOverride.x ?? qrSlot.x) * 100}
                      min={0}
                      max={100}
                      step={0.5}
                      onChange={(val) => onQrPercentChange('x')(val)}
                    />
                  </YStack>
                  <YStack flex={1} space="$1">
                    <Text fontSize="$xs" color={muted}>
                      {t('events.qr.positionY', 'Y (%)')}
                    </Text>
                    <StepperInput
                      value={(qrOverride.y ?? qrSlot.y) * 100}
                      min={0}
                      max={100}
                      step={0.5}
                      onChange={(val) => onQrPercentChange('y')(val)}
                    />
                  </YStack>
                  <YStack flex={1} space="$1">
                    <Text fontSize="$xs" color={muted}>
                      {t('events.qr.size', 'Größe (%)')}
                    </Text>
                    <StepperInput
                      value={(qrOverride.w ?? qrSlot.w) * 100}
                      min={10}
                      max={100}
                      step={0.5}
                      onChange={(val) => onQrPercentChange('w')(val)}
                    />
                  </YStack>
                </XStack>
                {!qrUrl ? (
                  <Text fontSize="$xs" color={danger}>
                    {t('events.qr.missing', 'Kein QR-Link vorhanden')}
                  </Text>
                ) : null}
              </YStack>
            </Accordion.Content>
          </Accordion.Item>
        ) : null}
      </Accordion>
    </YStack>
  );
}
